authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-05 09:40:45-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-05 09:40:45-07:00
logca3f9738c39c3502dd2258aae872f971080969ef
treeb2695644fbc171a1e1a86732ef41b71589a42ff1
parent51f7f77f87318a5329d3bab15d513fda5654200b
signature Signed by PGP key B5690EEEBB952194

feat(contracts): clients tolerate union members they don't know yet (#15951)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

5 files changed, 472 insertions(+), 31 deletions(-)

apps/server/src/orchestration-v2/ThreadLaunchService.test.ts+4-1
...@@ -329,7 +329,10 @@ it.effect.each(...@@ -329,7 +329,10 @@ it.effect.each(
329 assert.equal(wire.messages[0]?.text, task.prompt);329 assert.equal(wire.messages[0]?.text, task.prompt);
330 assert.equal(wire.messages[0]?.scheduledTaskId, task.id);330 assert.equal(wire.messages[0]?.scheduledTaskId, task.id);
331 assert.equal(wire.messages[0]?.createdBy, createdBy);331 assert.equal(wire.messages[0]?.createdBy, createdBy);
332 const turnItem = wire.turnItems.find((item) => item.type === "user_message");332 const turnItem = wire.turnItems.find(
333 (item): item is Extract<typeof item, { type: "user_message" }> =>
334 item.type === "user_message",
335 );
333 assert.equal(turnItem?.text, task.prompt);336 assert.equal(turnItem?.text, task.prompt);
334 assert.equal(turnItem?.scheduledTaskId, task.id);337 assert.equal(turnItem?.scheduledTaskId, task.id);
335 }).pipe(Effect.provide(Layer.mergeAll(harness.layer, scheduledTasks)));338 }).pipe(Effect.provide(Layer.mergeAll(harness.layer, scheduledTasks)));
packages/contracts/src/baseSchemas.test.ts created+107
...@@ -0,0 +1,107 @@
1import { describe, expect, it } from "@effect/vitest";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as Schema from "effect/Schema";
5
6import {
7 ForwardCompatibleArray,
8 ForwardCompatibleUnion,
9 ForwardCompatibleUnionArray,
10 isUnknownUnionMember,
11 UnknownUnionMember,
12} from "./baseSchemas.ts";
13
14const at = "2026-10-05T00:00:00.000Z";
15const Shape = Schema.Union([
16 Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number, at: Schema.DateTimeUtc }),
17 Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }),
18]);
19const members = Shape.members;
20/** How clients decode: the JSON wire codec over the runtime schema. */
21const fromWire = <S extends Schema.Top>(schema: S) =>
22 Schema.decodeUnknownSync(Schema.toCodecJson(schema) as never) as (value: unknown) => S["Type"];
23
24describe("ForwardCompatibleArray", () => {
25 it("decodes elements through their own codec and drops ones it cannot read", () => {
26 const decoded = fromWire(ForwardCompatibleArray(Shape))([
27 { kind: "circle", radius: 1, at },
28 { kind: "triangle" },
29 { kind: "square", side: "wide" },
30 ]);
31 expect(decoded).toHaveLength(1);
32 expect(DateTime.isDateTime((decoded[0] as { at: unknown }).at)).toBe(true);
33 });
34
35 it("applies decoding defaults instead of failing", () => {
36 const WithDefault = Schema.Struct({
37 name: Schema.String,
38 count: Schema.optionalKey(Schema.Number).pipe(
39 Schema.withDecodingDefaultKey(Effect.succeed(7)),
40 ),
41 });
42 expect(Schema.decodeUnknownSync(ForwardCompatibleArray(WithDefault))([{ name: "a" }])).toEqual([
43 { name: "a", count: 7 },
44 ]);
45 });
46});
47
48describe("ForwardCompatibleUnion", () => {
49 const decode = fromWire(ForwardCompatibleUnion(members, "kind"));
50
51 it("decodes a member from a newer server as unknown", () => {
52 const decoded = decode({ kind: "triangle", corners: 3 });
53 expect(isUnknownUnionMember(decoded)).toBe(true);
54 expect(decoded).toEqual(new UnknownUnionMember("kind", "triangle"));
55 });
56
57 it("decodes known members in full, transformations included", () => {
58 const decoded = decode({ kind: "circle", radius: 1, at });
59 expect(isUnknownUnionMember(decoded)).toBe(false);
60 expect(DateTime.isDateTime((decoded as { at: unknown }).at)).toBe(true);
61 });
62
63 it("still fails a known member whose payload is broken", () => {
64 expect(() => decode({ kind: "square", side: "wide" })).toThrow();
65 });
66
67 it("refuses to encode an unknown member", () => {
68 const encode = Schema.encodeSync(ForwardCompatibleUnion(members, "kind") as never);
69 expect(() => encode(new UnknownUnionMember("kind", "triangle") as never)).toThrow();
70 });
71});
72
73describe("ForwardCompatibleUnionArray", () => {
74 it("keeps a known member however its fields are named", () => {
75 const Flagged = Schema.Struct({
76 kind: Schema.Literal("flag"),
77 _unknown: Schema.Literal(true),
78 tag: Schema.String,
79 value: Schema.String,
80 });
81 const flag = { kind: "flag", _unknown: true, tag: "kind", value: "x" };
82 expect(fromWire(ForwardCompatibleUnionArray([Flagged], "kind"))([flag])).toEqual([flag]);
83 });
84
85 const decode = fromWire(ForwardCompatibleUnionArray(members, "kind"));
86
87 it("drops members a newer server added and keeps the rest", () => {
88 const decoded = decode([
89 { kind: "triangle" },
90 { kind: "circle", radius: 1, at },
91 { kind: "square", side: 2 },
92 ]);
93 expect(decoded.map((shape) => shape.kind)).toEqual(["circle", "square"]);
94 });
95
96 it("fails when a known member is broken, so real bugs stay visible", () => {
97 expect(() => decode([{ kind: "square", side: "wide" }])).toThrow();
98 });
99
100 it("encodes as a plain array", () => {
101 const encode = Schema.encodeSync(
102 Schema.toCodecJson(ForwardCompatibleUnionArray(members, "kind")),
103 );
104 const square = { kind: "square" as const, side: 2 };
105 expect(encode([square])).toEqual([square]);
106 });
107});
packages/contracts/src/baseSchemas.ts+160-15
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as Option from "effect/Option";2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";3import * as Schema from "effect/Schema";
4import * as SchemaGetter from "effect/SchemaGetter";
4import * as SchemaTransformation from "effect/SchemaTransformation";5import * as SchemaTransformation from "effect/SchemaTransformation";
56
6export const TrimmedString = Schema.String.pipe(7export const TrimmedString = Schema.String.pipe(
...@@ -35,14 +36,6 @@ export type DpopFailureReason = typeof DpopFailureReason.Type;...@@ -35,14 +36,6 @@ export type DpopFailureReason = typeof DpopFailureReason.Type;
35export const IsoDateTime = Schema.String;36export const IsoDateTime = Schema.String;
36export type IsoDateTime = typeof IsoDateTime.Type;37export type IsoDateTime = typeof IsoDateTime.Type;
3738
38/**
39 * Wire codec for server→client arrays whose element unions grow over time
40 * (new literal members, new struct variants). Decoding drops elements the
41 * current build cannot decode instead of failing the whole payload — a client
42 * has to keep decoding configs sent by servers newer than itself, and
43 * rejecting the payload would take down the connection over data the client
44 * couldn't act on anyway. Encoding is the plain array encoding.
45 */
46/**39/**
47 * Same idea for one optional value whose literal set grows over time: a40 * Same idea for one optional value whose literal set grows over time: a
48 * member this build does not know decodes as absent rather than failing the41 * member this build does not know decodes as absent rather than failing the
...@@ -107,20 +100,172 @@ export const OmittedWhenNull = <Value extends Schema.Top>(value: Value) => {...@@ -107,20 +100,172 @@ export const OmittedWhenNull = <Value extends Schema.Top>(value: Value) => {
107 );100 );
108};101};
109102
110export const ForwardCompatibleArray = <Element extends Schema.Top>(element: Element) => {103/**
111 const decodeElement = Schema.decodeUnknownOption(element as never);104 * Wire codec for a server→client value whose shape grows over time: a union
112 return Schema.Array(Schema.Unknown).pipe(105 * a newer server may add members to, or an array of such values. Clients keep
106 * decoding payloads from servers newer than themselves instead of failing the
107 * connection over data they could not act on anyway.
108 *
109 * Decoding runs each value through its own schema, so transformations (dates,
110 * trimming, decoding defaults) apply as usual. Only values that schema
111 * rejects are dropped; encoding is the plain encoding.
112 *
113 * For a tagged union, prefer {@link ForwardCompatibleUnion}: it drops only
114 * values whose tag this build does not know, so a known member with a broken
115 * payload still fails loudly.
116 */
117export const ForwardCompatibleArray = <Element extends Schema.Top>(element: Element) =>
118 Schema.Array(
119 Schema.UndefinedOr(element).pipe(
120 // An element this build cannot read becomes a hole, filtered out below.
121 Schema.catchDecoding(() => Effect.succeedSome(undefined)),
122 ),
123 ).pipe(
124 Schema.decodeTo(
125 Schema.Array(Schema.toType(element)),
126 SchemaTransformation.transform<
127 ReadonlyArray<Element["Type"]>,
128 ReadonlyArray<Element["Type"] | undefined>
129 >({
130 decode: (values) => values.filter((value) => value !== undefined),
131 encode: (values) => values,
132 }),
133 ),
134 ) as unknown as ForwardCompatibleArray<Element>;
135export type ForwardCompatibleArray<Element extends Schema.Top> = Schema.Codec<
136 ReadonlyArray<Element["Type"]>,
137 ReadonlyArray<Element["Encoded"]>,
138 Element["DecodingServices"],
139 Element["EncodingServices"]
140>;
141
142/**
143 * A member of a {@link ForwardCompatibleUnion} whose tag this build does not
144 * know. A class, so only the codec can make one: no decoded payload, however
145 * it is shaped, is ever mistaken for it.
146 */
147export class UnknownUnionMember<Tag extends string = string> {
148 readonly tag: Tag;
149 readonly value: string;
150 constructor(tag: Tag, value: string) {
151 this.tag = tag;
152 this.value = value;
153 }
154}
155
156/**
157 * Wire codec for a server→client tagged union a newer server may add members
158 * to. A value whose `tag` field holds a value this build does not know decodes
159 * to {@link UnknownUnionMember} instead of failing; a known member decodes
160 * through its own schema, so a broken known payload still fails. Callers
161 * decide what an unknown member means: skip it, or show a fallback.
162 *
163 * Unknown members are decode-only; encoding one fails.
164 */
165export const ForwardCompatibleUnion = <
166 const Members extends ReadonlyArray<Schema.Top & { readonly fields: object }>,
167 const Tag extends string,
168>(
169 members: Members,
170 tag: Tag,
171) => {
172 const known = knownTags(members, tag);
173 const unknownMember = Schema.Struct({
174 [tag]: Schema.String.check(
175 Schema.makeFilter(
176 (value: string) => !known.has(value) || `A known ${tag} must decode in full.`,
177 ),
178 ),
179 } as Record<Tag, Schema.String>).pipe(
180 Schema.decodeTo(Schema.instanceOf(UnknownUnionMember<Tag>), {
181 decode: SchemaGetter.transform(
182 (raw: Record<string, string>) => new UnknownUnionMember(tag, raw[tag]!),
183 ),
184 encode: SchemaGetter.forbidden(() => `Unknown ${tag} values are never sent.`),
185 }),
186 );
187 return Schema.Union([...members, unknownMember]) as unknown as ForwardCompatibleUnion<
188 Members,
189 Tag
190 >;
191};
192export type ForwardCompatibleUnion<
193 Members extends ReadonlyArray<Schema.Top>,
194 Tag extends string,
195> = Schema.Codec<
196 Members[number]["Type"] | UnknownUnionMember<Tag>,
197 Members[number]["Encoded"],
198 Members[number]["DecodingServices"],
199 Members[number]["EncodingServices"]
200>;
201
202/**
203 * Whether a raw, undecoded value carries a `tag` this build does not know
204 * among `members`. For checks outside a {@link ForwardCompatibleUnion}, such
205 * as an envelope that must skip a payload of an unknown kind.
206 */
207export const hasUnknownUnionTag = (
208 members: ReadonlyArray<Schema.Top & { readonly fields: object }>,
209 tag: string,
210): ((value: unknown) => boolean) => {
211 const known = knownTags(members, tag);
212 return (value) =>
213 typeof value === "object" &&
214 value !== null &&
215 tag in value &&
216 !known.has((value as Record<string, unknown>)[tag] as string);
217};
218
219/** Whether a decoded {@link ForwardCompatibleUnion} value is a member this build does not know. */
220export const isUnknownUnionMember = <A>(
221 value: A,
222): value is Extract<A, UnknownUnionMember<string>> => value instanceof UnknownUnionMember;
223
224/**
225 * An array of a growing tagged union that drops members this build does not
226 * know. Known members decode through their own schema and still fail when
227 * broken. Encoding is the plain array encoding.
228 */
229export const ForwardCompatibleUnionArray = <
230 const Members extends ReadonlyArray<Schema.Top & { readonly fields: object }>,
231 const Tag extends string,
232>(
233 members: Members,
234 tag: Tag,
235) =>
236 Schema.Array(ForwardCompatibleUnion(members, tag)).pipe(
113 Schema.decodeTo(237 Schema.decodeTo(
114 Schema.Array(element),238 Schema.Array(Schema.toType(Schema.Union(members))),
115 SchemaTransformation.transform<ReadonlyArray<Element["Encoded"]>, ReadonlyArray<unknown>>({239 SchemaTransformation.transform<
240 ReadonlyArray<Members[number]["Type"]>,
241 ReadonlyArray<Members[number]["Type"] | UnknownUnionMember<Tag>>
242 >({
116 decode: (values) =>243 decode: (values) =>
117 values.filter((value) => Option.isSome(decodeElement(value))) as ReadonlyArray<244 values.filter((value) => !isUnknownUnionMember(value)) as ReadonlyArray<
118 Element["Encoded"]245 Members[number]["Type"]
119 >,246 >,
120 encode: (values) => values,247 encode: (values) => values,
121 }),248 }),
122 ),249 ),
123 );250 );
251
252const knownTags = (
253 members: ReadonlyArray<Schema.Top & { readonly fields: object }>,
254 tag: string,
255): ReadonlySet<string> => {
256 const tags = new Set<string>();
257 for (const member of members) {
258 const field = (member.fields as Record<string, unknown>)[tag] as
259 | { readonly literal?: unknown; readonly literals?: ReadonlyArray<unknown> }
260 | undefined;
261 for (const literal of field?.literals ?? [field?.literal]) {
262 if (typeof literal !== "string") {
263 throw new Error(`Every ForwardCompatibleUnion member needs a string literal ${tag}.`);
264 }
265 tags.add(literal);
266 }
267 }
268 return tags;
124};269};
125270
126/**271/**
packages/contracts/src/orchestrationV2.test.ts+129
...@@ -34,6 +34,7 @@ import {...@@ -34,6 +34,7 @@ import {
34 OrchestrationV2ShellSnapshot,34 OrchestrationV2ShellSnapshot,
35 OrchestrationV2SubscribeThreadInput,35 OrchestrationV2SubscribeThreadInput,
36 OrchestrationV2Subagent,36 OrchestrationV2Subagent,
37 OrchestrationV2ThreadHistoryPage,
37 OrchestrationV2ThreadProjection,38 OrchestrationV2ThreadProjection,
38 OrchestrationV2ThreadStreamItem,39 OrchestrationV2ThreadStreamItem,
39 OrchestrationV2ThreadShell,40 OrchestrationV2ThreadShell,
...@@ -175,6 +176,134 @@ describe("orchestration V2 contracts", () => {...@@ -175,6 +176,134 @@ describe("orchestration V2 contracts", () => {
175 ).toThrow();176 ).toThrow();
176 });177 });
177178
179 it("skips turn item types from a newer server in snapshots and turn-item events", () => {
180 const decodeWireItems = Schema.decodeUnknownSync(
181 Schema.toCodecJson(Schema.Array(OrchestrationV2RpcSchemas.subscribeThread.output)),
182 );
183 const decodeHistoryPage = Schema.decodeUnknownSync(
184 Schema.toCodecJson(OrchestrationV2ThreadHistoryPage),
185 );
186 const item = (id: string, type: string, extra: Record<string, unknown>) => ({
187 id,
188 type,
189 threadId: "thread-1",
190 runId: null,
191 nodeId: null,
192 providerThreadId: null,
193 providerTurnId: null,
194 nativeItemRef: null,
195 parentItemId: null,
196 ordinal: 1,
197 status: "completed",
198 title: null,
199 startedAt: null,
200 completedAt: null,
201 updatedAt: DateTime.formatIso(now),
202 ...extra,
203 });
204 const known = item("item-known", "system_notice", { message: "Hello" });
205 // A type no build of this client knows, standing in for a newer server's item.
206 const future = item("item-future", "secret_request", { secretRef: "ref-1" });
207 const projected = (position: number, turnItem: { readonly id: string }) => ({
208 position,
209 visibility: "local",
210 sourceThreadId: "thread-1",
211 sourceItemId: turnItem.id,
212 item: turnItem,
213 });
214 const projection = {
215 thread: {
216 createdBy: "user",
217 creationSource: "web",
218 id: "thread-1",
219 projectId: "project-1",
220 title: "Thread",
221 providerInstanceId: "codex",
222 modelSelection: { instanceId: "codex", model: "gpt-5-codex" },
223 runtimeMode: "full-access",
224 interactionMode: "default",
225 branch: null,
226 worktreePath: null,
227 activeProviderThreadId: null,
228 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: "thread-1" },
229 forkedFrom: null,
230 createdAt: DateTime.formatIso(now),
231 updatedAt: DateTime.formatIso(now),
232 archivedAt: null,
233 deletedAt: null,
234 },
235 runs: [],
236 attempts: [],
237 nodes: [],
238 subagents: [],
239 providerSessions: [],
240 providerThreads: [],
241 providerTurns: [],
242 runtimeRequests: [],
243 messages: [],
244 plans: [],
245 turnItems: [future, known],
246 checkpointScopes: [],
247 checkpoints: [],
248 contextHandoffs: [],
249 contextTransfers: [],
250 visibleTurnItems: [projected(0, future), projected(1, known)],
251 updatedAt: DateTime.formatIso(now),
252 };
253 const turnItemEvent = (sequence: number, payload: unknown) => ({
254 kind: "event",
255 sequence,
256 event: {
257 id: `event-${sequence}`,
258 type: "turn-item.updated",
259 threadId: "thread-1",
260 occurredAt: DateTime.formatIso(now),
261 payload,
262 },
263 });
264
265 const [snapshot, futureEvent, knownEvent] = decodeWireItems([
266 { kind: "snapshot", snapshotSequence: 1, projection },
267 turnItemEvent(2, future),
268 turnItemEvent(3, known),
269 ]);
270
271 expect(snapshot).toMatchObject({ kind: "snapshot" });
272 if (snapshot?.kind !== "snapshot") throw new Error("expected a snapshot");
273 expect(snapshot.projection.turnItems.map((turnItem) => turnItem.id)).toEqual(["item-known"]);
274 expect(snapshot.projection.visibleTurnItems.map((row) => row.item.id)).toEqual(["item-known"]);
275 expect(futureEvent).toEqual({
276 kind: "unknown-event",
277 sequence: 2,
278 eventType: "turn-item.updated",
279 });
280 expect(knownEvent).toMatchObject({
281 kind: "event",
282 event: { type: "turn-item.updated", payload: { id: "item-known", type: "system_notice" } },
283 });
284 expect(
285 decodeHistoryPage({
286 snapshotSequence: 1,
287 items: [projected(0, future), projected(1, known)],
288 nextCursor: null,
289 hasMoreHistory: false,
290 }).items.map((row) => row.item.id),
291 ).toEqual(["item-known"]);
292
293 // A known turn item type with a broken payload is a real defect, not a newer item.
294 const broken = item("item-broken", "system_notice", {});
295 expect(() => decodeWireItems([turnItemEvent(4, broken)])).toThrow();
296 expect(() =>
297 decodeWireItems([
298 {
299 kind: "snapshot",
300 snapshotSequence: 1,
301 projection: { ...projection, turnItems: [broken] },
302 },
303 ]),
304 ).toThrow();
305 });
306
178 it("negotiates bounded socket snapshots as an optional capability", () => {307 it("negotiates bounded socket snapshots as an optional capability", () => {
179 expect(308 expect(
180 decodeOrchestrationV2SubscribeThreadInput({309 decodeOrchestrationV2SubscribeThreadInput({
packages/contracts/src/orchestrationV2.ts+72-15
...@@ -3,6 +3,7 @@ import * as Effect from "effect/Effect";...@@ -3,6 +3,7 @@ import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";3import * as Schema from "effect/Schema";
4import * as SchemaAST from "effect/SchemaAST";4import * as SchemaAST from "effect/SchemaAST";
5import * as SchemaGetter from "effect/SchemaGetter";5import * as SchemaGetter from "effect/SchemaGetter";
6import * as SchemaTransformation from "effect/SchemaTransformation";
67
7import {8import {
8 CheckpointId,9 CheckpointId,
...@@ -15,6 +16,10 @@ import {...@@ -15,6 +16,10 @@ import {
15 IsoDateTime,16 IsoDateTime,
16 MessageId,17 MessageId,
17 NodeId,18 NodeId,
19 ForwardCompatibleUnion,
20 ForwardCompatibleUnionArray,
21 hasUnknownUnionTag,
22 isUnknownUnionMember,
18 NonNegativeInt,23 NonNegativeInt,
19 PlanId,24 PlanId,
20 PositiveInt,25 PositiveInt,
...@@ -1494,6 +1499,36 @@ export const OrchestrationV2TurnItem = Schema.Union([...@@ -1494,6 +1499,36 @@ export const OrchestrationV2TurnItem = Schema.Union([
1494]);1499]);
1495export type OrchestrationV2TurnItem = typeof OrchestrationV2TurnItem.Type;1500export type OrchestrationV2TurnItem = typeof OrchestrationV2TurnItem.Type;
14961501
1502/**
1503 * Turn item types grow over time, so clients decode them forward-compatibly:
1504 * an item whose type this build does not know is dropped from snapshots and
1505 * history instead of failing the thread. A known type that does not decode
1506 * still fails. Arrays of projected rows filter on the row's nested item.
1507 */
1508const isUnknownTurnItem = hasUnknownUnionTag(OrchestrationV2TurnItem.members, "type");
1509
1510const turnItemArray = <Members extends ReadonlyArray<Schema.Top & { readonly fields: object }>>(
1511 union: Schema.Union<Members>,
1512) => ForwardCompatibleUnionArray(union.members, "type");
1513
1514/** Projected rows whose nested item may be of a type this build does not know. */
1515const projectedTurnItemArray = <Row extends Schema.Top, Item extends Schema.Top>(
1516 row: Row,
1517 rowWithUnknownItem: Item,
1518) =>
1519 Schema.Array(rowWithUnknownItem).pipe(
1520 Schema.decodeTo(
1521 Schema.Array(Schema.toType(row)),
1522 SchemaTransformation.transform<ReadonlyArray<Row["Type"]>, ReadonlyArray<Item["Type"]>>({
1523 decode: (rows) =>
1524 rows.filter(
1525 (projected) => !isUnknownUnionMember((projected as { readonly item: unknown }).item),
1526 ) as ReadonlyArray<Row["Type"]>,
1527 encode: (rows) => rows,
1528 }),
1529 ),
1530 );
1531
1497export const OrchestrationV2ProjectedTurnItem = Schema.Struct({1532export const OrchestrationV2ProjectedTurnItem = Schema.Struct({
1498 position: NonNegativeInt,1533 position: NonNegativeInt,
1499 visibility: Schema.Literals(["local", "inherited", "synthetic"]),1534 visibility: Schema.Literals(["local", "inherited", "synthetic"]),
...@@ -1681,12 +1716,18 @@ export const OrchestrationV2ThreadProjection = Schema.Struct({...@@ -1681,12 +1716,18 @@ export const OrchestrationV2ThreadProjection = Schema.Struct({
1681 runtimeRequests: Schema.Array(OrchestrationV2RuntimeRequest),1716 runtimeRequests: Schema.Array(OrchestrationV2RuntimeRequest),
1682 messages: Schema.Array(OrchestrationV2ConversationMessage),1717 messages: Schema.Array(OrchestrationV2ConversationMessage),
1683 plans: Schema.Array(OrchestrationV2PlanArtifact),1718 plans: Schema.Array(OrchestrationV2PlanArtifact),
1684 turnItems: Schema.Array(OrchestrationV2TurnItem),1719 turnItems: turnItemArray(OrchestrationV2TurnItem),
1685 checkpointScopes: Schema.Array(OrchestrationV2CheckpointScope),1720 checkpointScopes: Schema.Array(OrchestrationV2CheckpointScope),
1686 checkpoints: Schema.Array(OrchestrationV2Checkpoint),1721 checkpoints: Schema.Array(OrchestrationV2Checkpoint),
1687 contextHandoffs: Schema.Array(OrchestrationV2ContextHandoff),1722 contextHandoffs: Schema.Array(OrchestrationV2ContextHandoff),
1688 contextTransfers: Schema.Array(OrchestrationV2ContextTransfer),1723 contextTransfers: Schema.Array(OrchestrationV2ContextTransfer),
1689 visibleTurnItems: Schema.Array(OrchestrationV2ProjectedTurnItem),1724 visibleTurnItems: projectedTurnItemArray(
1725 OrchestrationV2ProjectedTurnItem,
1726 OrchestrationV2ProjectedTurnItem.mapFields((fields) => ({
1727 ...fields,
1728 item: ForwardCompatibleUnion(OrchestrationV2TurnItem.members, "type"),
1729 })),
1730 ),
1690 updatedAt: Schema.DateTimeUtc,1731 updatedAt: Schema.DateTimeUtc,
1691});1732});
1692export type OrchestrationV2ThreadProjection = typeof OrchestrationV2ThreadProjection.Type;1733export type OrchestrationV2ThreadProjection = typeof OrchestrationV2ThreadProjection.Type;
...@@ -2249,12 +2290,18 @@ export const OrchestrationV2ThreadProjectionJson = OrchestrationV2ThreadProjecti...@@ -2249,12 +2290,18 @@ export const OrchestrationV2ThreadProjectionJson = OrchestrationV2ThreadProjecti
2249 runtimeRequests: Schema.Array(OrchestrationV2RuntimeRequestJson),2290 runtimeRequests: Schema.Array(OrchestrationV2RuntimeRequestJson),
2250 messages: Schema.Array(OrchestrationV2ConversationMessageJson),2291 messages: Schema.Array(OrchestrationV2ConversationMessageJson),
2251 plans: Schema.Array(OrchestrationV2PlanArtifact),2292 plans: Schema.Array(OrchestrationV2PlanArtifact),
2252 turnItems: Schema.Array(OrchestrationV2TurnItemJson),2293 turnItems: turnItemArray(OrchestrationV2TurnItemJson),
2253 checkpointScopes: Schema.Array(OrchestrationV2CheckpointScopeJson),2294 checkpointScopes: Schema.Array(OrchestrationV2CheckpointScopeJson),
2254 checkpoints: Schema.Array(OrchestrationV2CheckpointJson),2295 checkpoints: Schema.Array(OrchestrationV2CheckpointJson),
2255 contextHandoffs: Schema.Array(OrchestrationV2ContextHandoffJson),2296 contextHandoffs: Schema.Array(OrchestrationV2ContextHandoffJson),
2256 contextTransfers: Schema.Array(OrchestrationV2ContextTransferJson),2297 contextTransfers: Schema.Array(OrchestrationV2ContextTransferJson),
2257 visibleTurnItems: Schema.Array(OrchestrationV2ProjectedTurnItemJson),2298 visibleTurnItems: projectedTurnItemArray(
2299 OrchestrationV2ProjectedTurnItemJson,
2300 OrchestrationV2ProjectedTurnItemJson.mapFields((fields) => ({
2301 ...fields,
2302 item: ForwardCompatibleUnion(OrchestrationV2TurnItemJson.members, "type"),
2303 })),
2304 ),
2258 updatedAt: Schema.DateTimeUtcFromString,2305 updatedAt: Schema.DateTimeUtcFromString,
2259 }),2306 }),
2260);2307);
...@@ -3129,7 +3176,13 @@ export type OrchestrationV2ThreadBoundedSnapshot = typeof OrchestrationV2ThreadB...@@ -3129,7 +3176,13 @@ export type OrchestrationV2ThreadBoundedSnapshot = typeof OrchestrationV2ThreadB
3129/** Older timeline page for progressive history. Rows are chronological. */3176/** Older timeline page for progressive history. Rows are chronological. */
3130export const OrchestrationV2ThreadHistoryPage = Schema.Struct({3177export const OrchestrationV2ThreadHistoryPage = Schema.Struct({
3131 snapshotSequence: NonNegativeInt,3178 snapshotSequence: NonNegativeInt,
3132 items: Schema.Array(OrchestrationV2ProjectedTurnItem),3179 items: projectedTurnItemArray(
3180 OrchestrationV2ProjectedTurnItem,
3181 OrchestrationV2ProjectedTurnItem.mapFields((fields) => ({
3182 ...fields,
3183 item: ForwardCompatibleUnion(OrchestrationV2TurnItem.members, "type"),
3184 })),
3185 ),
3133 nextCursor: Schema.NullOr(TrimmedNonEmptyString),3186 nextCursor: Schema.NullOr(TrimmedNonEmptyString),
3134 hasMoreHistory: Schema.Boolean,3187 hasMoreHistory: Schema.Boolean,
3135});3188});
...@@ -3143,22 +3196,26 @@ const knownDomainEventTypes: ReadonlySet<string> = new Set(...@@ -3143,22 +3196,26 @@ const knownDomainEventTypes: ReadonlySet<string> = new Set(
3143);3196);
31443197
3145/**3198/**
3146 * A thread event whose type this build does not know. Newer servers add event3199 * A thread event whose type this build does not know, or a turn-item.updated
3147 * types; older clients decode them to this case and skip them, still advancing3200 * carrying a turn item type it does not know. Newer servers add both; older
3148 * their resume cursor, instead of failing the whole subscription. A known type3201 * clients decode them to this case and skip them, still advancing their resume
3149 * whose payload does not decode still fails. Decode-only: servers never send it.3202 * cursor, instead of failing the whole subscription. A known type whose payload
3203 * does not decode still fails. Decode-only: servers never send it.
3150 */3204 */
3151const OrchestrationV2UnknownThreadStreamEvent = Schema.Struct({3205const OrchestrationV2UnknownThreadStreamEvent = Schema.Struct({
3152 kind: Schema.Literal("event"),3206 kind: Schema.Literal("event"),
3153 sequence: NonNegativeInt,3207 sequence: NonNegativeInt,
3154 event: Schema.Struct({3208 event: Schema.Struct({
3155 type: Schema.String.check(3209 type: Schema.String,
3156 Schema.makeFilter(3210 payload: Schema.optional(Schema.Unknown),
3157 (type: string) =>3211 }).check(
3158 !knownDomainEventTypes.has(type) || "A known event type must decode in full.",3212 Schema.makeFilter(
3159 ),3213 (event) =>
3214 !knownDomainEventTypes.has(event.type) ||
3215 (event.type === "turn-item.updated" && isUnknownTurnItem(event.payload)) ||
3216 "A known event type must decode in full.",
3160 ),3217 ),
3161 }),3218 ),
3162}).pipe(3219}).pipe(
3163 Schema.decodeTo(3220 Schema.decodeTo(
3164 Schema.Struct({3221 Schema.Struct({