From 5157d268248a13e2866bd3a2c18c61eee5a184d7 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 2 Oct 2026 12:13:57 +0200 Subject: [PATCH 1/2] feat: generate Outgoing types for extensible unions An extensible union ends in a catch-all variant whose tag is any string, because receivers must accept future ACP variants. That also lets a producer's malformed known variant, with a misspelled or missing field, type-check as the catch-all. For each extensible union, the generator now also emits an Outgoing type for values a producer sends: the known variants, exactly as the guards narrow them, plus custom variants under a `_`-prefixed tag, which the protocol requires for implementation-specific values. Known tags never start with `_`, so a malformed known variant no longer falls through to the catch-all, while extensions stay expressible. The types live in a new outgoing.gen.ts per lane, re-exported from both entry points. A generated compile-time check asserts that every outgoing type is a value of its open union. --- scripts/generate.js | 71 ++++- src/acp.test.ts | 10 + src/acp.ts | 2 + src/schema/outgoing.gen.ts | 138 ++++++++++ src/v2/acp.test.ts | 66 +++++ src/v2/acp.ts | 2 + src/v2/schema/outgoing.gen.ts | 499 ++++++++++++++++++++++++++++++++++ 7 files changed, 783 insertions(+), 5 deletions(-) create mode 100644 src/schema/outgoing.gen.ts create mode 100644 src/v2/schema/outgoing.gen.ts diff --git a/scripts/generate.js b/scripts/generate.js index 0f3032fc..45afc7d7 100644 --- a/scripts/generate.js +++ b/scripts/generate.js @@ -31,6 +31,10 @@ const CHECK_GENERATED = process.argv.includes("--check"); // vendor payloads). Both helpers live in src/schema-deserialize.ts. // 3. emitExtensibleUnionGuards writes each lane's guards.gen.ts — validated, // declaration-merged type guards consumers use to narrow the unions. +// 4. emitOutgoingTypes writes each lane's outgoing.gen.ts — an +// `Outgoing` type per union for the values a producer sends: the +// known variants as the guards narrow them, plus custom variants under +// a `_`-prefixed tag. // Drift protection, each assertion guarding a different failure mode: // - The lane's expectedExtensibleUnions list (below): detection missed a // union in the raw schema, or found an unexpected one. @@ -226,15 +230,18 @@ async function generateSchema(config, checkGenerated) { ); await fs.writeFile(tsPath, ts); - // Always write the file: the staging swap replaces the whole directory, so - // skipping the write here would silently delete guards.gen.ts. - const guardsSrc = emitExtensibleUnionGuards( + // Always write the files: the staging swap replaces the whole directory, so + // skipping a write here would silently delete guards.gen.ts or + // outgoing.gen.ts. + const unions = detectExtensibleUnions( schemaDefs, config.expectedExtensibleUnions, config.name, ); - const guards = await formatStable(guardsSrc); + const guards = await formatStable(emitExtensibleUnionGuards(unions)); await fs.writeFile(`${stagingDir}/guards.gen.ts`, guards); + const outgoing = await formatStable(emitOutgoingTypes(unions)); + await fs.writeFile(`${stagingDir}/outgoing.gen.ts`, outgoing); const meta = `export const AGENT_METHODS = ${JSON.stringify(metadata.agentMethods, null, 2)} as const; @@ -638,7 +645,7 @@ function notClauseExclusion(not) { // payload where the catch-all carries structure). A malformed known variant // matches no guard — the same classification the wire validators apply via // excludeKnownTags (see createDeserializationResolvers' union resolver). -function emitExtensibleUnionGuards(schemaDefs, expectedUnions, lane) { +function detectExtensibleUnions(schemaDefs, expectedUnions, lane) { const unions = []; for (const [name, def] of Object.entries(schemaDefs)) { const union = analyzeExtensibleUnion(name, def); @@ -656,6 +663,60 @@ function emitExtensibleUnionGuards(schemaDefs, expectedUnions, lane) { `scripts/generate.js and that lane's guard exports.`, ); } + return unions; +} + +// Outgoing types: for each extensible union, the values a producer may send. +// The open union must accept anything with a string tag, because receivers +// must tolerate future ACP variants; that also lets a malformed known variant +// (right tag, misspelled or missing field) type-check as the catch-all. The +// outgoing type keeps the known variants exactly as the guards narrow them and +// narrows the catch-all's tag to `_${string}`, the protocol's prefix for +// implementation-specific values. Known tags never start with `_`, so a +// malformed known variant can no longer fall through to the catch-all. +function emitOutgoingTypes(unions) { + if (unions.length === 0) + return "// This file is auto-generated by scripts/generate.js\nexport {};\n"; + + const aliases = unions.map((union) => { + const tag = union.discriminant; + const extension = `(${union.catchAll.tsType} & { ${tag}: \`_\${string}\` })`; + const members = [ + ...union.known.map((variant) => variant.tsType), + extension, + ]; + const doc = + `A value to send as \`${union.name}\`: one of its known variants, or a\n` + + `custom variant whose \`${tag}\` starts with \`_\`.\n\n` + + `\`${union.name}\` itself is for values you receive, so it also accepts\n` + + `future ACP variants: any object with a string \`${tag}\` fits it,\n` + + `including a known variant with a misspelled or missing field. This type\n` + + `catches those mistakes at compile time. Custom variants stay\n` + + `expressible under a \`_\`-prefixed tag, which the protocol requires for\n` + + `implementation-specific values.\n\n` + + `Extensible unions nested inside the value keep their open types: type\n` + + `the nested values you build with their own \`Outgoing\` types.` + + (union.description?.includes("@experimental") ? `\n\n@experimental` : ""); + return `${formatJsdoc(doc)}export type Outgoing${union.name} =\n | ${members.join("\n | ")};`; + }); + + // Fails type-checking if an outgoing type is not a value of its open union, + // so every union the generator emits is covered without a hand-kept list. + const checks = unions.map( + (union) => ` IsSendableAs,`, + ); + return ( + `// This file is auto-generated by scripts/generate.js\n\n` + + `import type * as types from "./types.gen.js";\n\n` + + `${aliases.join("\n\n")}\n\n` + + `// Compile-time check: every outgoing type is a value of its open union.\n` + + `type IsSendableAs = [Union, Outgoing];\n` + + `// eslint-disable-next-line @typescript-eslint/no-unused-vars\n` + + `type OutgoingTypesAreSendable = [\n${checks.join("\n")}\n];\n` + ); +} + +function emitExtensibleUnionGuards(unions) { if (unions.length === 0) return "// This file is auto-generated by scripts/generate.js\nexport {};\n"; diff --git a/src/acp.test.ts b/src/acp.test.ts index e004fdfa..792fa58f 100644 --- a/src/acp.test.ts +++ b/src/acp.test.ts @@ -6604,4 +6604,14 @@ describe("extensible union narrowing helpers", () => { ); } }); + + it("exports outgoing types that reject a malformed known variant", () => { + const custom: sdk.OutgoingCreateElicitationResponse = { + action: "_defer", + until: "later", + }; + // @ts-expect-error unknown tags without `_` are reserved for future ACP versions + const reserved: sdk.OutgoingCreateElicitationResponse = { action: "defer" }; + expect([custom, reserved]).toHaveLength(2); + }); }); diff --git a/src/acp.ts b/src/acp.ts index 392b5979..9cd1adec 100644 --- a/src/acp.ts +++ b/src/acp.ts @@ -3,6 +3,8 @@ import * as validate from "./schema/zod.gen.js"; import type { AnyMessage } from "./jsonrpc.js"; import { ndJsonStream as createJsonStream } from "./stream.js"; export type * from "./schema/types.gen.js"; +// `Outgoing` types for the extensible unions: what a producer may send. +export type * from "./schema/outgoing.gen.js"; // Runtime narrowing helpers for extensible unions, exposed as companion values // that merge (declaration merging) with the like-named types — e.g. // `CreateElicitationResponse.isAccept(response)`. See schema/guards.gen.ts. diff --git a/src/schema/outgoing.gen.ts b/src/schema/outgoing.gen.ts new file mode 100644 index 00000000..cb49a548 --- /dev/null +++ b/src/schema/outgoing.gen.ts @@ -0,0 +1,138 @@ +// This file is auto-generated by scripts/generate.js + +import type * as types from "./types.gen.js"; + +/** + * A value to send as `CreateElicitationRequest`: one of its known variants, or a + * custom variant whose `mode` starts with `_`. + * + * `CreateElicitationRequest` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `mode` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingCreateElicitationRequest = + | ((types.ElicitationFormMode & { mode: "form" }) & + Pick) + | ((types.ElicitationUrlMode & { mode: "url" }) & + Pick) + | (((types.ElicitationSessionScope | types.ElicitationRequestScope) & { + mode: string; + [key: string]: unknown; + }) & + Pick & { + mode: `_${string}`; + }); + +/** + * A value to send as `ElicitationPropertySchema`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `ElicitationPropertySchema` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingElicitationPropertySchema = + | (types.StringPropertySchema & { type: "string" }) + | (types.NumberPropertySchema & { type: "number" }) + | (types.IntegerPropertySchema & { type: "integer" }) + | (types.BooleanPropertySchema & { type: "boolean" }) + | (types.MultiSelectPropertySchema & { type: "array" }) + | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); + +/** + * A value to send as `MultiSelectItems`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `MultiSelectItems` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingMultiSelectItems = + | (types.StringMultiSelectItems & { type: "string" }) + | types.TitledMultiSelectItems + | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); + +/** + * A value to send as `StateUpdate`: one of its known variants, or a + * custom variant whose `state` starts with `_`. + * + * `StateUpdate` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `state` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + * + * @experimental + */ +export type OutgoingStateUpdate = + | (types.RunningStateUpdate & { state: "running" }) + | (types.IdleStateUpdate & { state: "idle" }) + | (types.RequiresActionStateUpdate & { state: "requires_action" }) + | (types.UnknownStateUpdate & { state: "unknown" }) + | ({ state: string; [key: string]: unknown } & { state: `_${string}` }); + +/** + * A value to send as `CreateElicitationResponse`: one of its known variants, or a + * custom variant whose `action` starts with `_`. + * + * `CreateElicitationResponse` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `action` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingCreateElicitationResponse = + | ((types.ElicitationAcceptAction & { action: "accept" }) & + Pick) + | ({ action: "decline" } & Pick) + | ({ action: "cancel" } & Pick) + | ({ action: string; [key: string]: unknown } & Pick< + types.CreateElicitationResponse, + "_meta" + > & { action: `_${string}` }); + +// Compile-time check: every outgoing type is a value of its open union. +type IsSendableAs = [Union, Outgoing]; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type OutgoingTypesAreSendable = [ + IsSendableAs< + types.CreateElicitationRequest, + OutgoingCreateElicitationRequest + >, + IsSendableAs< + types.ElicitationPropertySchema, + OutgoingElicitationPropertySchema + >, + IsSendableAs, + IsSendableAs, + IsSendableAs< + types.CreateElicitationResponse, + OutgoingCreateElicitationResponse + >, +]; diff --git a/src/v2/acp.test.ts b/src/v2/acp.test.ts index d6ea2b69..4b8e8092 100644 --- a/src/v2/acp.test.ts +++ b/src/v2/acp.test.ts @@ -413,6 +413,72 @@ describe("experimental v2 app API", () => { } }); + describe("outgoing extensible-union types", () => { + it("reject a malformed known variant that the open union accepts", () => { + const openOption: sdk.SessionConfigOption = { + type: "select", + configId: "model", + name: "Model", + currentValue: "fast", + options: [{ value: "fast", name: "Fast", title: "Fast model" }], + }; + const outgoingOption: sdk.OutgoingSessionConfigOption = { + type: "select", + configId: "model", + name: "Model", + currentValue: "fast", + // @ts-expect-error a field that the known variant does not have + options: [{ value: "fast", name: "Fast", title: "Fast model" }], + }; + const openUpdate: SessionUpdate = { + sessionUpdate: "usage_update", + used: 1, + }; + // @ts-expect-error a known variant missing a required field + const outgoingUpdate: sdk.OutgoingSessionUpdate = { + sessionUpdate: "usage_update", + used: 1, + }; + expect([ + openOption, + outgoingOption, + openUpdate, + outgoingUpdate, + ]).toHaveLength(4); + }); + + it("accept custom variants only under a `_`-prefixed tag", () => { + const custom: sdk.OutgoingSessionUpdate = { + sessionUpdate: "_acme/progress", + percent: 40, + }; + const customOption: sdk.OutgoingSessionConfigOption = { + type: "_slider", + configId: "temperature", + name: "Temperature", + min: 0, + max: 1, + }; + // @ts-expect-error unknown tags without `_` are reserved for future ACP versions + const reserved: sdk.OutgoingSessionUpdate = { sessionUpdate: "progress" }; + // @ts-expect-error a custom variant still carries the union's shared fields + const missingShared: sdk.OutgoingSessionConfigOption = { + type: "_slider", + name: "Temperature", + }; + // A custom variant is still a value of the open union. + expectTypeOf(custom).toMatchTypeOf(); + expect([custom, customOption, reserved, missingShared]).toHaveLength(4); + }); + + it("keep known variants that have no tag", () => { + const titled: sdk.OutgoingMultiSelectItems = { + anyOf: [{ const: "a", title: "A" }], + }; + expect(titled).toBeDefined(); + }); + }); + it("initializes exactly once, queues later calls, and exposes the exchange", async () => { const initializeGate = Promise.withResolvers(); const agentReady = Promise.withResolvers(); diff --git a/src/v2/acp.ts b/src/v2/acp.ts index c35e68ed..b6eae0fe 100644 --- a/src/v2/acp.ts +++ b/src/v2/acp.ts @@ -17,6 +17,8 @@ import * as guards from "./schema/guards.gen.js"; import { ndJsonStream as createJsonStream } from "../stream.js"; import type { NdJsonStreamOptions } from "../stream.js"; export type * from "./schema/types.gen.js"; +// `Outgoing` types for the extensible unions: what a producer may send. +export type * from "./schema/outgoing.gen.js"; // Runtime narrowing helpers for extensible unions, exposed as companion values // that merge (declaration merging) with the like-named types — e.g. // `CreateElicitationResponse.isAccept(response)`. See schema/guards.gen.ts. diff --git a/src/v2/schema/outgoing.gen.ts b/src/v2/schema/outgoing.gen.ts new file mode 100644 index 00000000..459abab7 --- /dev/null +++ b/src/v2/schema/outgoing.gen.ts @@ -0,0 +1,499 @@ +// This file is auto-generated by scripts/generate.js + +import type * as types from "./types.gen.js"; + +/** + * A value to send as `RequestPermissionSubject`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `RequestPermissionSubject` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingRequestPermissionSubject = + | (types.ToolCallPermissionSubject & { type: "tool_call" }) + | (types.CommandPermissionSubject & { type: "command" }) + | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); + +/** + * A value to send as `ToolCallContent`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `ToolCallContent` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingToolCallContent = + | (types.Content & { type: "content" }) + | (types.Diff & { type: "diff" }) + | (types.Terminal & { type: "terminal" }) + | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); + +/** + * A value to send as `ContentBlock`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `ContentBlock` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingContentBlock = + | (types.TextContent & { type: "text" }) + | (types.ImageContent & { type: "image" }) + | (types.AudioContent & { type: "audio" }) + | (types.ResourceLink & { type: "resource_link" }) + | (types.EmbeddedResource & { type: "resource" }) + | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); + +/** + * A value to send as `DiffChange`: one of its known variants, or a + * custom variant whose `operation` starts with `_`. + * + * `DiffChange` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `operation` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingDiffChange = + | ((types.DiffPathChange & { operation: "add" }) & + Pick) + | ((types.DiffPathChange & { operation: "delete" }) & + Pick) + | ((types.DiffPathChange & { operation: "modify" }) & + Pick) + | ((types.DiffPathPairChange & { operation: "move" }) & + Pick) + | ((types.DiffPathPairChange & { operation: "copy" }) & + Pick) + | ({ operation: string; [key: string]: unknown } & Pick< + types.DiffChange, + "fileType" | "mimeType" | "_meta" + > & { operation: `_${string}` }); + +/** + * A value to send as `CreateElicitationRequest`: one of its known variants, or a + * custom variant whose `mode` starts with `_`. + * + * `CreateElicitationRequest` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `mode` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingCreateElicitationRequest = + | ((types.ElicitationFormMode & { mode: "form" }) & + Pick) + | ((types.ElicitationUrlMode & { mode: "url" }) & + Pick) + | (((types.ElicitationSessionScope | types.ElicitationRequestScope) & { + mode: string; + [key: string]: unknown; + }) & + Pick & { + mode: `_${string}`; + }); + +/** + * A value to send as `ElicitationPropertySchema`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `ElicitationPropertySchema` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingElicitationPropertySchema = + | (types.StringPropertySchema & { type: "string" }) + | (types.NumberPropertySchema & { type: "number" }) + | (types.IntegerPropertySchema & { type: "integer" }) + | (types.BooleanPropertySchema & { type: "boolean" }) + | (types.MultiSelectPropertySchema & { type: "array" }) + | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); + +/** + * A value to send as `MultiSelectItems`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `MultiSelectItems` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingMultiSelectItems = + | (types.StringMultiSelectItems & { type: "string" }) + | types.TitledMultiSelectItems + | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); + +/** + * A value to send as `AuthMethod`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `AuthMethod` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingAuthMethod = + | (types.AuthMethodTerminal & { type: "terminal" }) + | (types.AuthMethodAgent & { type: "agent" }) + | (({ type: string; [key: string]: unknown } & { + methodId: types.AuthMethodId; + name: string; + }) & { type: `_${string}` }); + +/** + * A value to send as `SessionConfigOption`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `SessionConfigOption` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingSessionConfigOption = + | ((types.SessionConfigSelect & { type: "select" }) & + Pick< + types.SessionConfigOption, + "configId" | "name" | "description" | "category" | "_meta" + >) + | ((types.SessionConfigBoolean & { type: "boolean" }) & + Pick< + types.SessionConfigOption, + "configId" | "name" | "description" | "category" | "_meta" + >) + | ({ type: string; [key: string]: unknown } & Pick< + types.SessionConfigOption, + "configId" | "name" | "description" | "category" | "_meta" + > & { type: `_${string}` }); + +/** + * A value to send as `AvailableCommandInput`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `AvailableCommandInput` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingAvailableCommandInput = + | (types.TextCommandInput & { type: "text" }) + | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); + +/** + * A value to send as `NesSuggestion`: one of its known variants, or a + * custom variant whose `kind` starts with `_`. + * + * `NesSuggestion` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `kind` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingNesSuggestion = + | (types.NesEditSuggestion & { kind: "edit" }) + | (types.NesJumpSuggestion & { kind: "jump" }) + | (types.NesRenameSuggestion & { kind: "rename" }) + | (types.NesSearchAndReplaceSuggestion & { kind: "searchAndReplace" }) + | (({ kind: string; [key: string]: unknown } & { + suggestionId: types.NesSuggestionId; + }) & { kind: `_${string}` }); + +/** + * A value to send as `SessionUpdate`: one of its known variants, or a + * custom variant whose `sessionUpdate` starts with `_`. + * + * `SessionUpdate` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `sessionUpdate` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingSessionUpdate = + | (types.ContentChunk & { sessionUpdate: "user_message_chunk" }) + | (types.UserMessage & { sessionUpdate: "user_message" }) + | (types.ContentChunk & { sessionUpdate: "agent_message_chunk" }) + | (types.AgentMessage & { sessionUpdate: "agent_message" }) + | (types.ContentChunk & { sessionUpdate: "agent_thought_chunk" }) + | (types.AgentThought & { sessionUpdate: "agent_thought" }) + | (types.StateUpdate & { sessionUpdate: "state_update" }) + | (types.ToolCallContentChunk & { sessionUpdate: "tool_call_content_chunk" }) + | (types.ToolCallUpdate & { sessionUpdate: "tool_call_update" }) + | (types.TerminalUpdate & { sessionUpdate: "terminal_update" }) + | (types.TerminalOutputChunk & { sessionUpdate: "terminal_output_chunk" }) + | (types.PlanUpdate & { sessionUpdate: "plan_update" }) + | (types.PlanRemoved & { sessionUpdate: "plan_removed" }) + | (types.AvailableCommandsUpdate & { + sessionUpdate: "available_commands_update"; + }) + | (types.ConfigOptionUpdate & { sessionUpdate: "config_option_update" }) + | (types.SessionInfoUpdate & { sessionUpdate: "session_info_update" }) + | (types.UsageUpdate & { sessionUpdate: "usage_update" }) + | (types.Notice & { sessionUpdate: "notice" }) + | (types.CompactionUpdate & { sessionUpdate: "compaction_update" }) + | (types.CompactionSummaryChunk & { + sessionUpdate: "compaction_summary_chunk"; + }) + | (types.SubagentUpdate & { sessionUpdate: "subagent_update" }) + | (types.SessionMessage & { sessionUpdate: "session_message" }) + | (types.SessionMessageChunk & { sessionUpdate: "session_message_chunk" }) + | ({ sessionUpdate: string; [key: string]: unknown } & { + sessionUpdate: `_${string}`; + }); + +/** + * A value to send as `StateUpdate`: one of its known variants, or a + * custom variant whose `state` starts with `_`. + * + * `StateUpdate` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `state` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingStateUpdate = + | (types.RunningStateUpdate & { state: "running" }) + | (types.IdleStateUpdate & { state: "idle" }) + | (types.RequiresActionStateUpdate & { state: "requires_action" }) + | (types.UnknownStateUpdate & { state: "unknown" }) + | ({ state: string; [key: string]: unknown } & { state: `_${string}` }); + +/** + * A value to send as `PlanUpdateContent`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `PlanUpdateContent` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingPlanUpdateContent = + | (types.PlanItems & { type: "items" }) + | (types.PlanFile & { type: "file" }) + | (types.PlanMarkdown & { type: "markdown" }) + | (({ type: string; [key: string]: unknown } & { planId: types.PlanId }) & { + type: `_${string}`; + }); + +/** + * A value to send as `McpServer`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `McpServer` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingMcpServer = + | (types.McpServerHttp & { type: "http" }) + | (types.McpServerAcp & { type: "acp" }) + | (types.McpServerStdio & { type: "stdio" }) + | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); + +/** + * A value to send as `ReplayFrom`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `ReplayFrom` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingReplayFrom = + | (types.ReplayFromStart & { type: "start" }) + | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); + +/** + * A value to send as `SetSessionConfigOptionRequest`: one of its known variants, or a + * custom variant whose `type` starts with `_`. + * + * `SetSessionConfigOptionRequest` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `type` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingSetSessionConfigOptionRequest = + | (({ type: "id" } & { value: types.SessionConfigValueId }) & + Pick< + types.SetSessionConfigOptionRequest, + "sessionId" | "configId" | "_meta" + >) + | (({ type: "boolean" } & { value: boolean }) & + Pick< + types.SetSessionConfigOptionRequest, + "sessionId" | "configId" | "_meta" + >) + | (({ type: string; [key: string]: unknown } & { value: unknown }) & + Pick< + types.SetSessionConfigOptionRequest, + "sessionId" | "configId" | "_meta" + > & { type: `_${string}` }); + +/** + * A value to send as `RequestPermissionOutcome`: one of its known variants, or a + * custom variant whose `outcome` starts with `_`. + * + * `RequestPermissionOutcome` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `outcome` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingRequestPermissionOutcome = + | { outcome: "cancelled" } + | (types.SelectedPermissionOutcome & { outcome: "selected" }) + | ({ outcome: string; [key: string]: unknown } & { outcome: `_${string}` }); + +/** + * A value to send as `CreateElicitationResponse`: one of its known variants, or a + * custom variant whose `action` starts with `_`. + * + * `CreateElicitationResponse` itself is for values you receive, so it also accepts + * future ACP variants: any object with a string `action` fits it, + * including a known variant with a misspelled or missing field. This type + * catches those mistakes at compile time. Custom variants stay + * expressible under a `_`-prefixed tag, which the protocol requires for + * implementation-specific values. + * + * Extensible unions nested inside the value keep their open types: type + * the nested values you build with their own `Outgoing` types. + */ +export type OutgoingCreateElicitationResponse = + | ((types.ElicitationAcceptAction & { action: "accept" }) & + Pick) + | ({ action: "decline" } & Pick) + | ({ action: "cancel" } & Pick) + | ({ action: string; [key: string]: unknown } & Pick< + types.CreateElicitationResponse, + "_meta" + > & { action: `_${string}` }); + +// Compile-time check: every outgoing type is a value of its open union. +type IsSendableAs = [Union, Outgoing]; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type OutgoingTypesAreSendable = [ + IsSendableAs< + types.RequestPermissionSubject, + OutgoingRequestPermissionSubject + >, + IsSendableAs, + IsSendableAs, + IsSendableAs, + IsSendableAs< + types.CreateElicitationRequest, + OutgoingCreateElicitationRequest + >, + IsSendableAs< + types.ElicitationPropertySchema, + OutgoingElicitationPropertySchema + >, + IsSendableAs, + IsSendableAs, + IsSendableAs, + IsSendableAs, + IsSendableAs, + IsSendableAs, + IsSendableAs, + IsSendableAs, + IsSendableAs, + IsSendableAs, + IsSendableAs< + types.SetSessionConfigOptionRequest, + OutgoingSetSessionConfigOptionRequest + >, + IsSendableAs< + types.RequestPermissionOutcome, + OutgoingRequestPermissionOutcome + >, + IsSendableAs< + types.CreateElicitationResponse, + OutgoingCreateElicitationResponse + >, +]; From 2f2f6e25724778f32f73bbd6d2ba4f07376b093b Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 2 Oct 2026 12:41:56 +0200 Subject: [PATCH 2/2] feat(v2): check the extensible-union values you build Replaces the opt-in Outgoing types with a change to the v2 types themselves. A catch-all variant must accept any string tag, so receivers tolerate future ACP variants. Typed that way, it also let a value you build pass a malformed known variant (misspelled or missing field) as the catch-all, at any depth. The v2 types now split each catch-all in two: - custom variants, whose tag starts with `_`, the protocol's prefix for implementation-specific values. Known tags never start with `_`. - UnknownVariant, with any string tag, branded with a symbol that only the types of received values carry, so a value you build cannot be one. So a value you build is checked against the known variants at any depth, with nothing to import or annotate, while received values, unknown variants included, pass on unchanged: an agent echoing a prompt's content into its user message needs no conversion. The brand exists only in the types: the zod validators are generated from the unchanged schema, and a received() helper types parsed values. The generator produces the branded types from a copy of the schema whose catch-all tags are pinned to a marker. Narrowing works as before, so the guards stay. v1 is unchanged. --- README.md | 7 + scripts/generate.js | 228 +++++++--- src/acp.test.ts | 10 - src/acp.ts | 2 - src/schema/outgoing.gen.ts | 138 ------ src/v2/acp.test.ts | 103 +++-- src/v2/acp.ts | 64 ++- src/v2/schema/guards.gen.ts | 124 ++++-- src/v2/schema/outgoing.gen.ts | 499 --------------------- src/v2/schema/types.gen.ts | 814 +++++++++++++++++++++++----------- 10 files changed, 923 insertions(+), 1066 deletions(-) delete mode 100644 src/schema/outgoing.gen.ts delete mode 100644 src/v2/schema/outgoing.gen.ts diff --git a/README.md b/README.md index ca317a21..624f9ab1 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,13 @@ Browse the [experimental v2 TypeScript API reference](https://agentclientprotoco and the [draft ACP v2 protocol documentation](https://agentclientprotocol.com/protocol/v2/draft/overview) for the current SDK and protocol designs. +In v2, the extensible unions (`SessionUpdate`, `ContentBlock`, and so on) also +accept variants this SDK does not know, such as ones added by a newer ACP +version. Only received values can be such an `UnknownVariant`, so a value you +build is checked field by field at any depth, and a custom variant needs a tag +that starts with `_`. Received values pass on unchanged, for example when an +agent echoes a prompt's content into its user message. + ## Get Started ### Understand the Protocol diff --git a/scripts/generate.js b/scripts/generate.js index 45afc7d7..173844a0 100644 --- a/scripts/generate.js +++ b/scripts/generate.js @@ -31,10 +31,11 @@ const CHECK_GENERATED = process.argv.includes("--check"); // vendor payloads). Both helpers live in src/schema-deserialize.ts. // 3. emitExtensibleUnionGuards writes each lane's guards.gen.ts — validated, // declaration-merged type guards consumers use to narrow the unions. -// 4. emitOutgoingTypes writes each lane's outgoing.gen.ts — an -// `Outgoing` type per union for the values a producer sends: the -// known variants as the guards narrow them, plus custom variants under -// a `_`-prefixed tag. +// 4. For a lane with `brandUnknownVariants`, brandedTypesSource splits each +// catch-all in the generated *types* into custom variants under a +// `_`-prefixed tag and `UnknownVariant`s, which only received values +// have. A value you build then cannot pass a malformed known variant as +// the catch-all, while received values pass on unchanged. // Drift protection, each assertion guarding a different failure mode: // - The lane's expectedExtensibleUnions list (below): detection missed a // union in the raw schema, or found an unexpected one. @@ -94,6 +95,7 @@ const SCHEMA_CONFIGS = [ previousDir: "./src/.schema-v2-previous", schemaDeserializeImport: "../../schema-deserialize.js", expectedExtensibleUnions: V2_EXTENSIBLE_UNIONS, + brandUnknownVariants: true, openApiVersion: "2.0.0", releaseTag: CURRENT_V2_SCHEMA_RELEASE, }, @@ -105,6 +107,27 @@ const SCHEMA_CONFIGS = [ // be initialized. const EXCLUDE_KNOWN_TAGS_ATTR = "x-exclude-known-tags"; const OPEN_OBJECT_ATTR = "x-acp-open-object"; +// The constant that pins each catch-all tag in the types pass of a lane with +// `brandUnknownVariants` (see brandedTypesSource). +const CATCH_ALL_TAG_MARKER = "__ACP_CATCH_ALL_TAG__"; +// Declared in the types of a lane with `brandUnknownVariants`. +const UNKNOWN_VARIANT_DECLARATION = ` +declare const unknownVariant: unique symbol; + +/** + * A variant of an extensible union that this SDK does not know, as received + * from a peer, for example one added by a newer ACP version. + * + * Only received values have this type. A value you build is a known variant, + * checked field by field, or a custom variant whose tag starts with \`_\`, so + * a misspelled or missing field on a known variant is a compile error. + * Received values keep the type, so passing them on needs no conversion. The + * brand exists only in the types. + * + * @experimental + */ +export type UnknownVariant = T & { readonly [unknownVariant]: true }; +`; await main(); @@ -138,6 +161,10 @@ async function generateSchema(config, checkGenerated) { const defExclusions = annotateExtensibleUnions(jsonSchema.$defs); annotateOpenObjects(jsonSchema.$defs); const schemaDefs = jsonSchema.$defs; + // Copied before the main pass, which may rewrite the schemas it reads. + const typesDefs = config.brandUnknownVariants + ? structuredClone(schemaDefs) + : undefined; // Generate into a staging directory and swap into place only after every // step (including the drift assertions below) has succeeded: hey-api wipes @@ -218,7 +245,9 @@ async function generateSchema(config, checkGenerated) { await fs.writeFile(zodPath, zod); const tsPath = `${stagingDir}/types.gen.ts`; - const tsSrc = await fs.readFile(tsPath, "utf8"); + const tsSrc = typesDefs + ? await brandedTypesSource(config, typesDefs) + : await fs.readFile(tsPath, "utf8"); const ts = await formatStable( updateDocs( tsSrc.replace( @@ -230,18 +259,16 @@ async function generateSchema(config, checkGenerated) { ); await fs.writeFile(tsPath, ts); - // Always write the files: the staging swap replaces the whole directory, so - // skipping a write here would silently delete guards.gen.ts or - // outgoing.gen.ts. + // Always write the file: the staging swap replaces the whole directory, so + // skipping the write here would silently delete guards.gen.ts. const unions = detectExtensibleUnions( schemaDefs, config.expectedExtensibleUnions, config.name, + config.brandUnknownVariants === true, ); const guards = await formatStable(emitExtensibleUnionGuards(unions)); await fs.writeFile(`${stagingDir}/guards.gen.ts`, guards); - const outgoing = await formatStable(emitOutgoingTypes(unions)); - await fs.writeFile(`${stagingDir}/outgoing.gen.ts`, outgoing); const meta = `export const AGENT_METHODS = ${JSON.stringify(metadata.agentMethods, null, 2)} as const; @@ -645,10 +672,10 @@ function notClauseExclusion(not) { // payload where the catch-all carries structure). A malformed known variant // matches no guard — the same classification the wire validators apply via // excludeKnownTags (see createDeserializationResolvers' union resolver). -function detectExtensibleUnions(schemaDefs, expectedUnions, lane) { +function detectExtensibleUnions(schemaDefs, expectedUnions, lane, branded) { const unions = []; for (const [name, def] of Object.entries(schemaDefs)) { - const union = analyzeExtensibleUnion(name, def); + const union = analyzeExtensibleUnion(name, def, branded); if (union) unions.push(union); } @@ -666,56 +693,123 @@ function detectExtensibleUnions(schemaDefs, expectedUnions, lane) { return unions; } -// Outgoing types: for each extensible union, the values a producer may send. -// The open union must accept anything with a string tag, because receivers -// must tolerate future ACP variants; that also lets a malformed known variant -// (right tag, misspelled or missing field) type-check as the catch-all. The -// outgoing type keeps the known variants exactly as the guards narrow them and -// narrows the catch-all's tag to `_${string}`, the protocol's prefix for -// implementation-specific values. Known tags never start with `_`, so a -// malformed known variant can no longer fall through to the catch-all. -function emitOutgoingTypes(unions) { - if (unions.length === 0) - return "// This file is auto-generated by scripts/generate.js\nexport {};\n"; +// Unknown variants. A catch-all must accept any string tag, because receivers +// must tolerate future ACP variants. Typed that way, it also lets a value you +// build pass a malformed known variant (right tag, misspelled or missing +// field) as the catch-all, at any depth of a message. In a branded lane the +// types split each catch-all in two: +// +// - custom variants, whose tag is `_${string}`, the protocol's prefix for +// implementation-specific values. Known tags never start with `_`. +// - `UnknownVariant`s, with any string tag, branded with a symbol that only +// the types of received values carry. A value you build cannot have it. +// +// Received values keep the brand, so passing them on needs no conversion. +// The brand exists only in the types: zod still validates the catch-all as +// before. TypeScript cannot narrow past a member with a wide string tag in +// either form, so receivers narrow with the generated guards as before. + +// Generates the types of a branded lane from a copy of its schema whose +// catch-all tags are pinned to a marker, then splits the object type that +// holds each marker into its custom and unknown forms. +async function brandedTypesSource(config, typesDefs) { + let marked = 0; + for (const def of Object.values(typesDefs)) { + walkSchema(def, (node) => { + const exclusion = node[EXCLUDE_KNOWN_TAGS_ATTR]; + if (!exclusion) return; + node.properties = { + ...node.properties, + [exclusion.key]: { + ...node.properties?.[exclusion.key], + type: "string", + const: CATCH_ALL_TAG_MARKER, + }, + }; + marked += 1; + }); + } - const aliases = unions.map((union) => { - const tag = union.discriminant; - const extension = `(${union.catchAll.tsType} & { ${tag}: \`_\${string}\` })`; - const members = [ - ...union.known.map((variant) => variant.tsType), - extension, - ]; - const doc = - `A value to send as \`${union.name}\`: one of its known variants, or a\n` + - `custom variant whose \`${tag}\` starts with \`_\`.\n\n` + - `\`${union.name}\` itself is for values you receive, so it also accepts\n` + - `future ACP variants: any object with a string \`${tag}\` fits it,\n` + - `including a known variant with a misspelled or missing field. This type\n` + - `catches those mistakes at compile time. Custom variants stay\n` + - `expressible under a \`_\`-prefixed tag, which the protocol requires for\n` + - `implementation-specific values.\n\n` + - `Extensible unions nested inside the value keep their open types: type\n` + - `the nested values you build with their own \`Outgoing\` types.` + - (union.description?.includes("@experimental") ? `\n\n@experimental` : ""); - return `${formatJsdoc(doc)}export type Outgoing${union.name} =\n | ${members.join("\n | ")};`; + const outputDir = `${config.stagingDir}-types`; + await fs.rm(outputDir, { recursive: true, force: true }); + await createClient({ + input: { + openapi: "3.1.0", + info: { + title: "Agent Client Protocol", + version: config.openApiVersion, + }, + components: { schemas: typesDefs }, + }, + output: { path: outputDir }, + plugins: [typescriptPlugin()], }); + const generated = await fs.readFile(`${outputDir}/types.gen.ts`, "utf8"); + await fs.rm(outputDir, { recursive: true, force: true }); - // Fails type-checking if an outgoing type is not a value of its open union, - // so every union the generator emits is covered without a hand-kept list. - const checks = unions.map( - (union) => ` IsSendableAs,`, - ); + const { source, split } = splitCatchAllMembers(generated); + if (split !== marked) { + throw new Error( + `[${config.name}] Marked ${marked} catch-all tags, but split ${split} ` + + `catch-all members in the generated types; the branded types pass ` + + `may have drifted`, + ); + } + const banner = source.indexOf("\n") + 1; return ( - `// This file is auto-generated by scripts/generate.js\n\n` + - `import type * as types from "./types.gen.js";\n\n` + - `${aliases.join("\n\n")}\n\n` + - `// Compile-time check: every outgoing type is a value of its open union.\n` + - `type IsSendableAs = [Union, Outgoing];\n` + - `// eslint-disable-next-line @typescript-eslint/no-unused-vars\n` + - `type OutgoingTypesAreSendable = [\n${checks.join("\n")}\n];\n` + source.slice(0, banner) + UNKNOWN_VARIANT_DECLARATION + source.slice(banner) ); } +// Replaces each object type literal that directly holds the marker with +// `(custom | UnknownVariant)`. Scans the source once, skipping +// comments and string literals (the docs contain braces), to find the +// innermost object type literal around each marker. +function splitCatchAllMembers(source) { + const markerLiterals = new Set([ + `"${CATCH_ALL_TAG_MARKER}"`, + `'${CATCH_ALL_TAG_MARKER}'`, + ]); + const opens = []; + const owners = new Set(); + const spans = []; + for (let i = 0; i < source.length; i++) { + const char = source[i]; + if (char === "/" && source[i + 1] === "*") { + i = source.indexOf("*/", i + 2) + 1; + } else if (char === "/" && source[i + 1] === "/") { + const end = source.indexOf("\n", i); + i = end === -1 ? source.length : end; + } else if (char === '"' || char === "'" || char === "`") { + let end = i + 1; + while (source[end] !== char) end += source[end] === "\\" ? 2 : 1; + if (markerLiterals.has(source.slice(i, end + 1))) { + owners.add(opens.at(-1)); + } + i = end; + } else if (char === "{") { + opens.push(i); + } else if (char === "}") { + const start = opens.pop(); + if (owners.has(start)) spans.push([start, i + 1]); + } + } + + const marker = new RegExp(`["']${CATCH_ALL_TAG_MARKER}["']`, "g"); + let result = source; + // Spans close in source order and never nest, so replace from the end. + for (const [start, end] of spans.reverse()) { + const member = result.slice(start, end); + const custom = member.replace(marker, "`_${string}`"); + const unknown = member.replace(marker, "string"); + result = + result.slice(0, start) + + `(${custom} | UnknownVariant<${unknown}>)` + + result.slice(end); + } + return { source: result, split: spans.length }; +} + function emitExtensibleUnionGuards(unions) { if (unions.length === 0) return "// This file is auto-generated by scripts/generate.js\nexport {};\n"; @@ -815,7 +909,7 @@ function emitExtensibleUnionGuards(unions) { ); } -function analyzeExtensibleUnion(name, def) { +function analyzeExtensibleUnion(name, def, branded) { const variants = def.anyOf ?? def.oneOf; if (!Array.isArray(variants)) return undefined; @@ -912,7 +1006,12 @@ function analyzeExtensibleUnion(name, def) { // Reconstruct the catch-all's TS type so `isCustom`'s predicate stays assignable // to the union, plus a zod expression for its payload when it carries structure // beyond an open bag of properties (e.g. a nested scope union). - const catchAll = analyzeCatchAll(name, catchAllVariant, discriminant); + const catchAll = analyzeCatchAll( + name, + catchAllVariant, + discriminant, + branded, + ); catchAll.tsType += commonPick; return { @@ -994,16 +1093,21 @@ function isUnconstrainedSchema(schema) { ); } -function analyzeCatchAll(name, variant, discriminant) { +function analyzeCatchAll(name, variant, discriminant, branded) { const inlineRequired = requiredInlineProps(name, variant, [discriminant]); const nested = variant.anyOf ?? variant.oneOf; const refUnion = Array.isArray(nested) ? nested.flatMap(allOfRefs) : []; const directRefs = allOfRefs(variant); + // Matches the generated index-signature variant: in a branded lane, split + // into its custom and unknown forms (see brandedTypesSource). + const bag = (tag) => `{ ${discriminant}: ${tag}; [key: string]: unknown }`; + const openBag = branded + ? `(${bag("`_${string}`")} | types.UnknownVariant<${bag("string")}>)` + : `(${bag("string")})`; if (directRefs.length === 0 && refUnion.length === 0) { - // Open bag of properties — matches the generated index-signature variant, - // and the discriminant check in isCustom validates its tag. - const openBag = `({ ${discriminant}: string; [key: string]: unknown })`; + // Open bag of properties; the discriminant check in isCustom validates + // its tag. return { tsType: inlineRequired ? `(${openBag} & ${inlineRequired.tsType})` @@ -1025,7 +1129,7 @@ function analyzeCatchAll(name, variant, discriminant) { // The index signature matches the generated union member: a custom // variant's extra keys are its payload and survive parsing. tsType: - `(${tsRefs} & { ${discriminant}: string; [key: string]: unknown }` + + `(${tsRefs} & ${openBag}` + `${inlineRequired ? ` & ${inlineRequired.tsType}` : ""})`, zodExpr: chainAnd(zodParts), }; diff --git a/src/acp.test.ts b/src/acp.test.ts index 792fa58f..e004fdfa 100644 --- a/src/acp.test.ts +++ b/src/acp.test.ts @@ -6604,14 +6604,4 @@ describe("extensible union narrowing helpers", () => { ); } }); - - it("exports outgoing types that reject a malformed known variant", () => { - const custom: sdk.OutgoingCreateElicitationResponse = { - action: "_defer", - until: "later", - }; - // @ts-expect-error unknown tags without `_` are reserved for future ACP versions - const reserved: sdk.OutgoingCreateElicitationResponse = { action: "defer" }; - expect([custom, reserved]).toHaveLength(2); - }); }); diff --git a/src/acp.ts b/src/acp.ts index 9cd1adec..392b5979 100644 --- a/src/acp.ts +++ b/src/acp.ts @@ -3,8 +3,6 @@ import * as validate from "./schema/zod.gen.js"; import type { AnyMessage } from "./jsonrpc.js"; import { ndJsonStream as createJsonStream } from "./stream.js"; export type * from "./schema/types.gen.js"; -// `Outgoing` types for the extensible unions: what a producer may send. -export type * from "./schema/outgoing.gen.js"; // Runtime narrowing helpers for extensible unions, exposed as companion values // that merge (declaration merging) with the like-named types — e.g. // `CreateElicitationResponse.isAccept(response)`. See schema/guards.gen.ts. diff --git a/src/schema/outgoing.gen.ts b/src/schema/outgoing.gen.ts deleted file mode 100644 index cb49a548..00000000 --- a/src/schema/outgoing.gen.ts +++ /dev/null @@ -1,138 +0,0 @@ -// This file is auto-generated by scripts/generate.js - -import type * as types from "./types.gen.js"; - -/** - * A value to send as `CreateElicitationRequest`: one of its known variants, or a - * custom variant whose `mode` starts with `_`. - * - * `CreateElicitationRequest` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `mode` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingCreateElicitationRequest = - | ((types.ElicitationFormMode & { mode: "form" }) & - Pick) - | ((types.ElicitationUrlMode & { mode: "url" }) & - Pick) - | (((types.ElicitationSessionScope | types.ElicitationRequestScope) & { - mode: string; - [key: string]: unknown; - }) & - Pick & { - mode: `_${string}`; - }); - -/** - * A value to send as `ElicitationPropertySchema`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `ElicitationPropertySchema` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingElicitationPropertySchema = - | (types.StringPropertySchema & { type: "string" }) - | (types.NumberPropertySchema & { type: "number" }) - | (types.IntegerPropertySchema & { type: "integer" }) - | (types.BooleanPropertySchema & { type: "boolean" }) - | (types.MultiSelectPropertySchema & { type: "array" }) - | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); - -/** - * A value to send as `MultiSelectItems`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `MultiSelectItems` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingMultiSelectItems = - | (types.StringMultiSelectItems & { type: "string" }) - | types.TitledMultiSelectItems - | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); - -/** - * A value to send as `StateUpdate`: one of its known variants, or a - * custom variant whose `state` starts with `_`. - * - * `StateUpdate` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `state` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - * - * @experimental - */ -export type OutgoingStateUpdate = - | (types.RunningStateUpdate & { state: "running" }) - | (types.IdleStateUpdate & { state: "idle" }) - | (types.RequiresActionStateUpdate & { state: "requires_action" }) - | (types.UnknownStateUpdate & { state: "unknown" }) - | ({ state: string; [key: string]: unknown } & { state: `_${string}` }); - -/** - * A value to send as `CreateElicitationResponse`: one of its known variants, or a - * custom variant whose `action` starts with `_`. - * - * `CreateElicitationResponse` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `action` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingCreateElicitationResponse = - | ((types.ElicitationAcceptAction & { action: "accept" }) & - Pick) - | ({ action: "decline" } & Pick) - | ({ action: "cancel" } & Pick) - | ({ action: string; [key: string]: unknown } & Pick< - types.CreateElicitationResponse, - "_meta" - > & { action: `_${string}` }); - -// Compile-time check: every outgoing type is a value of its open union. -type IsSendableAs = [Union, Outgoing]; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type OutgoingTypesAreSendable = [ - IsSendableAs< - types.CreateElicitationRequest, - OutgoingCreateElicitationRequest - >, - IsSendableAs< - types.ElicitationPropertySchema, - OutgoingElicitationPropertySchema - >, - IsSendableAs, - IsSendableAs, - IsSendableAs< - types.CreateElicitationResponse, - OutgoingCreateElicitationResponse - >, -]; diff --git a/src/v2/acp.test.ts b/src/v2/acp.test.ts index 4b8e8092..1a13b533 100644 --- a/src/v2/acp.test.ts +++ b/src/v2/acp.test.ts @@ -413,66 +413,105 @@ describe("experimental v2 app API", () => { } }); - describe("outgoing extensible-union types", () => { - it("reject a malformed known variant that the open union accepts", () => { - const openOption: sdk.SessionConfigOption = { + describe("extensible unions", () => { + it("check a known variant you build, at any depth", () => { + // @ts-expect-error a select option with a field it does not have + const option: sdk.SessionConfigOption = { type: "select", configId: "model", name: "Model", currentValue: "fast", options: [{ value: "fast", name: "Fast", title: "Fast model" }], }; - const outgoingOption: sdk.OutgoingSessionConfigOption = { - type: "select", - configId: "model", - name: "Model", - currentValue: "fast", - // @ts-expect-error a field that the known variant does not have - options: [{ value: "fast", name: "Fast", title: "Fast model" }], - }; - const openUpdate: SessionUpdate = { - sessionUpdate: "usage_update", - used: 1, - }; // @ts-expect-error a known variant missing a required field - const outgoingUpdate: sdk.OutgoingSessionUpdate = { - sessionUpdate: "usage_update", - used: 1, + const usage: SessionUpdate = { sessionUpdate: "usage_update", used: 1 }; + // @ts-expect-error a misnamed field in a nested known block + const chunk: SessionUpdate = { + sessionUpdate: "agent_message_chunk", + messageId: "m", + content: { type: "text", txt: "hello" }, }; - expect([ - openOption, - outgoingOption, - openUpdate, - outgoingUpdate, - ]).toHaveLength(4); + expect([option, usage, chunk]).toHaveLength(3); }); it("accept custom variants only under a `_`-prefixed tag", () => { - const custom: sdk.OutgoingSessionUpdate = { + const custom: SessionUpdate = { sessionUpdate: "_acme/progress", percent: 40, }; - const customOption: sdk.OutgoingSessionConfigOption = { + const customOption: sdk.SessionConfigOption = { type: "_slider", configId: "temperature", name: "Temperature", min: 0, max: 1, }; + const customBlock: SessionUpdate = { + sessionUpdate: "agent_message_chunk", + messageId: "m", + content: { type: "_acme/widget", spec: 1 }, + }; // @ts-expect-error unknown tags without `_` are reserved for future ACP versions - const reserved: sdk.OutgoingSessionUpdate = { sessionUpdate: "progress" }; + const reserved: SessionUpdate = { sessionUpdate: "progress" }; // @ts-expect-error a custom variant still carries the union's shared fields - const missingShared: sdk.OutgoingSessionConfigOption = { + const missingShared: sdk.SessionConfigOption = { type: "_slider", name: "Temperature", }; - // A custom variant is still a value of the open union. - expectTypeOf(custom).toMatchTypeOf(); - expect([custom, customOption, reserved, missingShared]).toHaveLength(4); + expect([ + custom, + customOption, + customBlock, + reserved, + missingShared, + ]).toHaveLength(5); + }); + + it("pass on received values, unknown variants included", async () => { + // A newer client sends a content block this SDK does not know. The agent + // echoes the prompt into its user message without converting it. + const futureBlock = { type: "future_block", payload: 1 }; + const echoed = Promise.withResolvers(); + const agentApp = testAgent() + .onRequest(methods.agent.session.new, () => ({ sessionId: "s" })) + .onRequest( + methods.agent.session.prompt, + async ({ params, client: agentClient }) => { + await agentClient.notify(methods.client.session.update, { + sessionId: params.sessionId, + update: { + sessionUpdate: "user_message", + messageId: "u", + content: params.prompt, + }, + }); + return { messageId: "u" }; + }, + ); + const clientApp = client().onNotification( + methods.client.session.update, + ({ params }) => echoed.resolve(params.update), + ); + await clientApp.connectWith(agentApp, async (agent) => { + await agent.request(methods.agent.initialize, { + protocolVersion: PROTOCOL_VERSION, + info: clientInfo, + }); + await agent.request(methods.agent.session.prompt, { + sessionId: "s", + // Simulates a newer peer: a value you build cannot be unknown. + prompt: [futureBlock as sdk.UnknownVariant], + }); + }); + expect(await echoed.promise).toEqual({ + sessionUpdate: "user_message", + messageId: "u", + content: [futureBlock], + }); }); it("keep known variants that have no tag", () => { - const titled: sdk.OutgoingMultiSelectItems = { + const titled: sdk.MultiSelectItems = { anyOf: [{ const: "a", title: "A" }], }; expect(titled).toBeDefined(); diff --git a/src/v2/acp.ts b/src/v2/acp.ts index b6eae0fe..b8a0755d 100644 --- a/src/v2/acp.ts +++ b/src/v2/acp.ts @@ -17,8 +17,6 @@ import * as guards from "./schema/guards.gen.js"; import { ndJsonStream as createJsonStream } from "../stream.js"; import type { NdJsonStreamOptions } from "../stream.js"; export type * from "./schema/types.gen.js"; -// `Outgoing` types for the extensible unions: what a producer may send. -export type * from "./schema/outgoing.gen.js"; // Runtime narrowing helpers for extensible unions, exposed as companion values // that merge (declaration merging) with the like-named types — e.g. // `CreateElicitationResponse.isAccept(response)`. See schema/guards.gen.ts. @@ -391,7 +389,9 @@ function assertV2BatchMethods( } function parseV2InitializeRequest(params: unknown): schema.InitializeRequest { - const request = validate.zInitializeRequest.parse(params); + const request = received( + validate.zInitializeRequest, + ).parse(params); if (request.protocolVersion !== schema.PROTOCOL_VERSION) { throw RequestError.invalidParams( { @@ -418,7 +418,9 @@ function normalizeOutgoingV2InitializeRequest( } function mapV2InitializeResponse(response: unknown): schema.InitializeResponse { - const parsed = validate.zInitializeResponse.parse(response); + const parsed = received( + validate.zInitializeResponse, + ).parse(response); if (parsed.protocolVersion !== schema.PROTOCOL_VERSION) { throw RequestError.invalidRequest( { @@ -2172,6 +2174,20 @@ type AcpNotificationSpec = { params?: ParamsParser; }; +/** + * A generated validator as the parser of received values. + * + * The validators type the tag of an unknown variant as a plain string, while + * the protocol types brand it as an `UnknownVariant`, which only received + * values have. Everything a validator parses is received, so its output has + * the protocol type. + */ +function received(validator: { parse(value: unknown): unknown }): { + parse(value: unknown): T; +} { + return validator as { parse(value: unknown): T }; +} + function requestSpec( method: string, params: ParamsParser, @@ -2302,21 +2318,21 @@ const agentRequestSpecs = { ), newSession: requestSpec( schema.AGENT_METHODS.session_new, - validate.zNewSessionRequest, - validate.zNewSessionResponse, + received(validate.zNewSessionRequest), + received(validate.zNewSessionResponse), ), setSessionConfigOption: requestSpec< schema.SetSessionConfigOptionRequest, schema.SetSessionConfigOptionResponse >( schema.AGENT_METHODS.session_set_config_option, - validate.zSetSessionConfigOptionRequest, - validate.zSetSessionConfigOptionResponse, + received(validate.zSetSessionConfigOptionRequest), + received(validate.zSetSessionConfigOptionResponse), ), prompt: requestSpec( schema.AGENT_METHODS.session_prompt, - validate.zPromptRequest, - validate.zPromptResponse, + received(validate.zPromptRequest), + received(validate.zPromptResponse), ), listSessions: requestSpec< schema.ListSessionsRequest, @@ -2341,16 +2357,16 @@ const agentRequestSpecs = { schema.ForkSessionResponse >( schema.AGENT_METHODS.session_fork, - validate.zForkSessionRequest, - validate.zForkSessionResponse, + received(validate.zForkSessionRequest), + received(validate.zForkSessionResponse), ), resumeSession: requestSpec< schema.ResumeSessionRequest, schema.ResumeSessionResponse >( schema.AGENT_METHODS.session_resume, - validate.zResumeSessionRequest, - validate.zResumeSessionResponse, + received(validate.zResumeSessionRequest), + received(validate.zResumeSessionResponse), ), closeSession: requestSpec< schema.CloseSessionRequest, @@ -2385,8 +2401,8 @@ const agentRequestSpecs = { schema.SuggestNesResponse >( schema.AGENT_METHODS.nes_suggest, - validate.zSuggestNesRequest, - validate.zSuggestNesResponse, + received(validate.zSuggestNesRequest), + received(validate.zSuggestNesResponse), ), unstable_closeNes: requestSpec< schema.CloseNesRequest, @@ -2450,8 +2466,8 @@ const clientRequestSpecs = { schema.RequestPermissionResponse >( schema.CLIENT_METHODS.session_request_permission, - validate.zRequestPermissionRequest, - validate.zRequestPermissionResponse, + received(validate.zRequestPermissionRequest), + received(validate.zRequestPermissionResponse), ), unstable_messageMcp: requestSpec< schema.MessageMcpRequest, @@ -2466,15 +2482,15 @@ const clientRequestSpecs = { schema.CreateElicitationResponse >( schema.CLIENT_METHODS.elicitation_create, - validate.zCreateElicitationRequest, - validate.zCreateElicitationResponse, + received(validate.zCreateElicitationRequest), + received(validate.zCreateElicitationResponse), ), }; const clientNotificationSpecs = { sessionUpdate: notificationSpec( schema.CLIENT_METHODS.session_update, - validate.zUpdateSessionNotification, + received(validate.zUpdateSessionNotification), ), completeElicitation: notificationSpec( schema.CLIENT_METHODS.elicitation_complete, @@ -2788,9 +2804,9 @@ class SessionUpdateRouter { return Handled.no(message); } - const notification = validate.zUpdateSessionNotification.parse( - message.params, - ); + const notification = received( + validate.zUpdateSessionNotification, + ).parse(message.params); const { update } = notification; const isIdle = guards.SessionUpdate.isStateUpdate(update) && diff --git a/src/v2/schema/guards.gen.ts b/src/v2/schema/guards.gen.ts index f5708847..5afbf066 100644 --- a/src/v2/schema/guards.gen.ts +++ b/src/v2/schema/guards.gen.ts @@ -323,7 +323,9 @@ export const RequestPermissionSubject = { */ isCustom( value: types.RequestPermissionSubject, - ): value is { type: string; [key: string]: unknown } { + ): value is + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> { const tag = tagOf(value, "type"); return typeof tag === "string" && !["command", "tool_call"].includes(tag); }, @@ -391,7 +393,9 @@ export const ToolCallContent = { */ isCustom( value: types.ToolCallContent, - ): value is { type: string; [key: string]: unknown } { + ): value is + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> { const tag = tagOf(value, "type"); return ( typeof tag === "string" && !["content", "diff", "terminal"].includes(tag) @@ -492,7 +496,9 @@ export const ContentBlock = { */ isCustom( value: types.ContentBlock, - ): value is { type: string; [key: string]: unknown } { + ): value is + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> { const tag = tagOf(value, "type"); return ( typeof tag === "string" && @@ -588,10 +594,11 @@ export const DiffChange = { */ isCustom( value: types.DiffChange, - ): value is { operation: string; [key: string]: unknown } & Pick< - types.DiffChange, - "fileType" | "mimeType" | "_meta" - > { + ): value is ( + | { operation: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ operation: string; [key: string]: unknown }> + ) & + Pick { const tag = tagOf(value, "operation"); return ( typeof tag === "string" && @@ -655,9 +662,11 @@ export const CreateElicitationRequest = { */ isCustom( value: types.CreateElicitationRequest, - ): value is (( - types.ElicitationSessionScope | types.ElicitationRequestScope - ) & { mode: string; [key: string]: unknown }) & + ): value is ((types.ElicitationSessionScope | types.ElicitationRequestScope) & + ( + | { mode: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ mode: string; [key: string]: unknown }> + )) & Pick { const tag = tagOf(value, "mode"); return ( @@ -751,7 +760,9 @@ export const ElicitationPropertySchema = { */ isCustom( value: types.ElicitationPropertySchema, - ): value is { type: string; [key: string]: unknown } { + ): value is + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> { const tag = tagOf(value, "type"); return ( typeof tag === "string" && @@ -809,7 +820,9 @@ export const MultiSelectItems = { */ isCustom( value: types.MultiSelectItems, - ): value is { type: string; [key: string]: unknown } { + ): value is + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> { const tag = tagOf(value, "type"); return typeof tag === "string" && !["string"].includes(tag); }, @@ -864,13 +877,12 @@ export const AuthMethod = { * structural subtypes of the catch-all), so read vendor payload keys * via a widening cast: `(value as Record).someKey`. */ - isCustom(value: types.AuthMethod): value is { - type: string; - [key: string]: unknown; - } & { - methodId: types.AuthMethodId; - name: string; - } { + isCustom( + value: types.AuthMethod, + ): value is ( + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> + ) & { methodId: types.AuthMethodId; name: string } { const tag = tagOf(value, "type"); return ( typeof tag === "string" && @@ -937,10 +949,14 @@ export const SessionConfigOption = { */ isCustom( value: types.SessionConfigOption, - ): value is { type: string; [key: string]: unknown } & Pick< - types.SessionConfigOption, - "configId" | "name" | "description" | "category" | "_meta" - > { + ): value is ( + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> + ) & + Pick< + types.SessionConfigOption, + "configId" | "name" | "description" | "category" | "_meta" + > { const tag = tagOf(value, "type"); return ( typeof tag === "string" && @@ -989,7 +1005,9 @@ export const AvailableCommandInput = { */ isCustom( value: types.AvailableCommandInput, - ): value is { type: string; [key: string]: unknown } { + ): value is + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> { const tag = tagOf(value, "type"); return typeof tag === "string" && !["text"].includes(tag); }, @@ -1064,12 +1082,12 @@ export const NesSuggestion = { * structural subtypes of the catch-all), so read vendor payload keys * via a widening cast: `(value as Record).someKey`. */ - isCustom(value: types.NesSuggestion): value is { - kind: string; - [key: string]: unknown; - } & { - suggestionId: types.NesSuggestionId; - } { + isCustom( + value: types.NesSuggestion, + ): value is ( + | { kind: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ kind: string; [key: string]: unknown }> + ) & { suggestionId: types.NesSuggestionId } { const tag = tagOf(value, "kind"); return ( typeof tag === "string" && @@ -1356,7 +1374,9 @@ export const SessionUpdate = { */ isCustom( value: types.SessionUpdate, - ): value is { sessionUpdate: string; [key: string]: unknown } { + ): value is + | { sessionUpdate: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ sessionUpdate: string; [key: string]: unknown }> { const tag = tagOf(value, "sessionUpdate"); return ( typeof tag === "string" && @@ -1461,7 +1481,9 @@ export const StateUpdate = { */ isCustom( value: types.StateUpdate, - ): value is { state: string; [key: string]: unknown } { + ): value is + | { state: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ state: string; [key: string]: unknown }> { const tag = tagOf(value, "state"); return ( typeof tag === "string" && @@ -1527,12 +1549,12 @@ export const PlanUpdateContent = { * structural subtypes of the catch-all), so read vendor payload keys * via a widening cast: `(value as Record).someKey`. */ - isCustom(value: types.PlanUpdateContent): value is { - type: string; - [key: string]: unknown; - } & { - planId: types.PlanId; - } { + isCustom( + value: types.PlanUpdateContent, + ): value is ( + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> + ) & { planId: types.PlanId } { const tag = tagOf(value, "type"); return ( typeof tag === "string" && @@ -1604,7 +1626,9 @@ export const McpServer = { */ isCustom( value: types.McpServer, - ): value is { type: string; [key: string]: unknown } { + ): value is + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> { const tag = tagOf(value, "type"); return typeof tag === "string" && !["acp", "http", "stdio"].includes(tag); }, @@ -1651,7 +1675,9 @@ export const ReplayFrom = { */ isCustom( value: types.ReplayFrom, - ): value is { type: string; [key: string]: unknown } { + ): value is + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> { const tag = tagOf(value, "type"); return typeof tag === "string" && !["start"].includes(tag); }, @@ -1714,7 +1740,10 @@ export const SetSessionConfigOptionRequest = { */ isCustom( value: types.SetSessionConfigOptionRequest, - ): value is ({ type: string; [key: string]: unknown } & { value: unknown }) & + ): value is (( + | { type: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ type: string; [key: string]: unknown }> + ) & { value: unknown }) & Pick< types.SetSessionConfigOptionRequest, "sessionId" | "configId" | "_meta" @@ -1777,7 +1806,9 @@ export const RequestPermissionOutcome = { */ isCustom( value: types.RequestPermissionOutcome, - ): value is { outcome: string; [key: string]: unknown } { + ): value is + | { outcome: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ outcome: string; [key: string]: unknown }> { const tag = tagOf(value, "outcome"); return typeof tag === "string" && !["cancelled", "selected"].includes(tag); }, @@ -1849,10 +1880,11 @@ export const CreateElicitationResponse = { */ isCustom( value: types.CreateElicitationResponse, - ): value is { action: string; [key: string]: unknown } & Pick< - types.CreateElicitationResponse, - "_meta" - > { + ): value is ( + | { action: `_${string}`; [key: string]: unknown } + | types.UnknownVariant<{ action: string; [key: string]: unknown }> + ) & + Pick { const tag = tagOf(value, "action"); return ( typeof tag === "string" && !["accept", "cancel", "decline"].includes(tag) diff --git a/src/v2/schema/outgoing.gen.ts b/src/v2/schema/outgoing.gen.ts deleted file mode 100644 index 459abab7..00000000 --- a/src/v2/schema/outgoing.gen.ts +++ /dev/null @@ -1,499 +0,0 @@ -// This file is auto-generated by scripts/generate.js - -import type * as types from "./types.gen.js"; - -/** - * A value to send as `RequestPermissionSubject`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `RequestPermissionSubject` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingRequestPermissionSubject = - | (types.ToolCallPermissionSubject & { type: "tool_call" }) - | (types.CommandPermissionSubject & { type: "command" }) - | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); - -/** - * A value to send as `ToolCallContent`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `ToolCallContent` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingToolCallContent = - | (types.Content & { type: "content" }) - | (types.Diff & { type: "diff" }) - | (types.Terminal & { type: "terminal" }) - | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); - -/** - * A value to send as `ContentBlock`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `ContentBlock` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingContentBlock = - | (types.TextContent & { type: "text" }) - | (types.ImageContent & { type: "image" }) - | (types.AudioContent & { type: "audio" }) - | (types.ResourceLink & { type: "resource_link" }) - | (types.EmbeddedResource & { type: "resource" }) - | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); - -/** - * A value to send as `DiffChange`: one of its known variants, or a - * custom variant whose `operation` starts with `_`. - * - * `DiffChange` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `operation` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingDiffChange = - | ((types.DiffPathChange & { operation: "add" }) & - Pick) - | ((types.DiffPathChange & { operation: "delete" }) & - Pick) - | ((types.DiffPathChange & { operation: "modify" }) & - Pick) - | ((types.DiffPathPairChange & { operation: "move" }) & - Pick) - | ((types.DiffPathPairChange & { operation: "copy" }) & - Pick) - | ({ operation: string; [key: string]: unknown } & Pick< - types.DiffChange, - "fileType" | "mimeType" | "_meta" - > & { operation: `_${string}` }); - -/** - * A value to send as `CreateElicitationRequest`: one of its known variants, or a - * custom variant whose `mode` starts with `_`. - * - * `CreateElicitationRequest` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `mode` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingCreateElicitationRequest = - | ((types.ElicitationFormMode & { mode: "form" }) & - Pick) - | ((types.ElicitationUrlMode & { mode: "url" }) & - Pick) - | (((types.ElicitationSessionScope | types.ElicitationRequestScope) & { - mode: string; - [key: string]: unknown; - }) & - Pick & { - mode: `_${string}`; - }); - -/** - * A value to send as `ElicitationPropertySchema`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `ElicitationPropertySchema` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingElicitationPropertySchema = - | (types.StringPropertySchema & { type: "string" }) - | (types.NumberPropertySchema & { type: "number" }) - | (types.IntegerPropertySchema & { type: "integer" }) - | (types.BooleanPropertySchema & { type: "boolean" }) - | (types.MultiSelectPropertySchema & { type: "array" }) - | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); - -/** - * A value to send as `MultiSelectItems`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `MultiSelectItems` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingMultiSelectItems = - | (types.StringMultiSelectItems & { type: "string" }) - | types.TitledMultiSelectItems - | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); - -/** - * A value to send as `AuthMethod`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `AuthMethod` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingAuthMethod = - | (types.AuthMethodTerminal & { type: "terminal" }) - | (types.AuthMethodAgent & { type: "agent" }) - | (({ type: string; [key: string]: unknown } & { - methodId: types.AuthMethodId; - name: string; - }) & { type: `_${string}` }); - -/** - * A value to send as `SessionConfigOption`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `SessionConfigOption` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingSessionConfigOption = - | ((types.SessionConfigSelect & { type: "select" }) & - Pick< - types.SessionConfigOption, - "configId" | "name" | "description" | "category" | "_meta" - >) - | ((types.SessionConfigBoolean & { type: "boolean" }) & - Pick< - types.SessionConfigOption, - "configId" | "name" | "description" | "category" | "_meta" - >) - | ({ type: string; [key: string]: unknown } & Pick< - types.SessionConfigOption, - "configId" | "name" | "description" | "category" | "_meta" - > & { type: `_${string}` }); - -/** - * A value to send as `AvailableCommandInput`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `AvailableCommandInput` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingAvailableCommandInput = - | (types.TextCommandInput & { type: "text" }) - | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); - -/** - * A value to send as `NesSuggestion`: one of its known variants, or a - * custom variant whose `kind` starts with `_`. - * - * `NesSuggestion` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `kind` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingNesSuggestion = - | (types.NesEditSuggestion & { kind: "edit" }) - | (types.NesJumpSuggestion & { kind: "jump" }) - | (types.NesRenameSuggestion & { kind: "rename" }) - | (types.NesSearchAndReplaceSuggestion & { kind: "searchAndReplace" }) - | (({ kind: string; [key: string]: unknown } & { - suggestionId: types.NesSuggestionId; - }) & { kind: `_${string}` }); - -/** - * A value to send as `SessionUpdate`: one of its known variants, or a - * custom variant whose `sessionUpdate` starts with `_`. - * - * `SessionUpdate` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `sessionUpdate` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingSessionUpdate = - | (types.ContentChunk & { sessionUpdate: "user_message_chunk" }) - | (types.UserMessage & { sessionUpdate: "user_message" }) - | (types.ContentChunk & { sessionUpdate: "agent_message_chunk" }) - | (types.AgentMessage & { sessionUpdate: "agent_message" }) - | (types.ContentChunk & { sessionUpdate: "agent_thought_chunk" }) - | (types.AgentThought & { sessionUpdate: "agent_thought" }) - | (types.StateUpdate & { sessionUpdate: "state_update" }) - | (types.ToolCallContentChunk & { sessionUpdate: "tool_call_content_chunk" }) - | (types.ToolCallUpdate & { sessionUpdate: "tool_call_update" }) - | (types.TerminalUpdate & { sessionUpdate: "terminal_update" }) - | (types.TerminalOutputChunk & { sessionUpdate: "terminal_output_chunk" }) - | (types.PlanUpdate & { sessionUpdate: "plan_update" }) - | (types.PlanRemoved & { sessionUpdate: "plan_removed" }) - | (types.AvailableCommandsUpdate & { - sessionUpdate: "available_commands_update"; - }) - | (types.ConfigOptionUpdate & { sessionUpdate: "config_option_update" }) - | (types.SessionInfoUpdate & { sessionUpdate: "session_info_update" }) - | (types.UsageUpdate & { sessionUpdate: "usage_update" }) - | (types.Notice & { sessionUpdate: "notice" }) - | (types.CompactionUpdate & { sessionUpdate: "compaction_update" }) - | (types.CompactionSummaryChunk & { - sessionUpdate: "compaction_summary_chunk"; - }) - | (types.SubagentUpdate & { sessionUpdate: "subagent_update" }) - | (types.SessionMessage & { sessionUpdate: "session_message" }) - | (types.SessionMessageChunk & { sessionUpdate: "session_message_chunk" }) - | ({ sessionUpdate: string; [key: string]: unknown } & { - sessionUpdate: `_${string}`; - }); - -/** - * A value to send as `StateUpdate`: one of its known variants, or a - * custom variant whose `state` starts with `_`. - * - * `StateUpdate` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `state` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingStateUpdate = - | (types.RunningStateUpdate & { state: "running" }) - | (types.IdleStateUpdate & { state: "idle" }) - | (types.RequiresActionStateUpdate & { state: "requires_action" }) - | (types.UnknownStateUpdate & { state: "unknown" }) - | ({ state: string; [key: string]: unknown } & { state: `_${string}` }); - -/** - * A value to send as `PlanUpdateContent`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `PlanUpdateContent` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingPlanUpdateContent = - | (types.PlanItems & { type: "items" }) - | (types.PlanFile & { type: "file" }) - | (types.PlanMarkdown & { type: "markdown" }) - | (({ type: string; [key: string]: unknown } & { planId: types.PlanId }) & { - type: `_${string}`; - }); - -/** - * A value to send as `McpServer`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `McpServer` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingMcpServer = - | (types.McpServerHttp & { type: "http" }) - | (types.McpServerAcp & { type: "acp" }) - | (types.McpServerStdio & { type: "stdio" }) - | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); - -/** - * A value to send as `ReplayFrom`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `ReplayFrom` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingReplayFrom = - | (types.ReplayFromStart & { type: "start" }) - | ({ type: string; [key: string]: unknown } & { type: `_${string}` }); - -/** - * A value to send as `SetSessionConfigOptionRequest`: one of its known variants, or a - * custom variant whose `type` starts with `_`. - * - * `SetSessionConfigOptionRequest` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `type` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingSetSessionConfigOptionRequest = - | (({ type: "id" } & { value: types.SessionConfigValueId }) & - Pick< - types.SetSessionConfigOptionRequest, - "sessionId" | "configId" | "_meta" - >) - | (({ type: "boolean" } & { value: boolean }) & - Pick< - types.SetSessionConfigOptionRequest, - "sessionId" | "configId" | "_meta" - >) - | (({ type: string; [key: string]: unknown } & { value: unknown }) & - Pick< - types.SetSessionConfigOptionRequest, - "sessionId" | "configId" | "_meta" - > & { type: `_${string}` }); - -/** - * A value to send as `RequestPermissionOutcome`: one of its known variants, or a - * custom variant whose `outcome` starts with `_`. - * - * `RequestPermissionOutcome` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `outcome` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingRequestPermissionOutcome = - | { outcome: "cancelled" } - | (types.SelectedPermissionOutcome & { outcome: "selected" }) - | ({ outcome: string; [key: string]: unknown } & { outcome: `_${string}` }); - -/** - * A value to send as `CreateElicitationResponse`: one of its known variants, or a - * custom variant whose `action` starts with `_`. - * - * `CreateElicitationResponse` itself is for values you receive, so it also accepts - * future ACP variants: any object with a string `action` fits it, - * including a known variant with a misspelled or missing field. This type - * catches those mistakes at compile time. Custom variants stay - * expressible under a `_`-prefixed tag, which the protocol requires for - * implementation-specific values. - * - * Extensible unions nested inside the value keep their open types: type - * the nested values you build with their own `Outgoing` types. - */ -export type OutgoingCreateElicitationResponse = - | ((types.ElicitationAcceptAction & { action: "accept" }) & - Pick) - | ({ action: "decline" } & Pick) - | ({ action: "cancel" } & Pick) - | ({ action: string; [key: string]: unknown } & Pick< - types.CreateElicitationResponse, - "_meta" - > & { action: `_${string}` }); - -// Compile-time check: every outgoing type is a value of its open union. -type IsSendableAs = [Union, Outgoing]; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type OutgoingTypesAreSendable = [ - IsSendableAs< - types.RequestPermissionSubject, - OutgoingRequestPermissionSubject - >, - IsSendableAs, - IsSendableAs, - IsSendableAs, - IsSendableAs< - types.CreateElicitationRequest, - OutgoingCreateElicitationRequest - >, - IsSendableAs< - types.ElicitationPropertySchema, - OutgoingElicitationPropertySchema - >, - IsSendableAs, - IsSendableAs, - IsSendableAs, - IsSendableAs, - IsSendableAs, - IsSendableAs, - IsSendableAs, - IsSendableAs, - IsSendableAs, - IsSendableAs, - IsSendableAs< - types.SetSessionConfigOptionRequest, - OutgoingSetSessionConfigOptionRequest - >, - IsSendableAs< - types.RequestPermissionOutcome, - OutgoingRequestPermissionOutcome - >, - IsSendableAs< - types.CreateElicitationResponse, - OutgoingCreateElicitationResponse - >, -]; diff --git a/src/v2/schema/types.gen.ts b/src/v2/schema/types.gen.ts index 0138a478..6a1199bc 100644 --- a/src/v2/schema/types.gen.ts +++ b/src/v2/schema/types.gen.ts @@ -1,5 +1,21 @@ // This file is auto-generated by @hey-api/openapi-ts +declare const unknownVariant: unique symbol; + +/** + * A variant of an extensible union that this SDK does not know, as received + * from a peer, for example one added by a newer ACP version. + * + * Only received values have this type. A value you build is a known variant, + * checked field by field, or a custom variant whose tag starts with `_`, so + * a misspelled or missing field on a known variant is a compile error. + * Received values keep the type, so passing them on needs no conversion. The + * brand exists only in the types. + * + * @experimental + */ +export type UnknownVariant = T & { readonly [unknownVariant]: true }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}); @@ -111,17 +127,30 @@ export type RequestPermissionSubject = | (CommandPermissionSubject & { type: "command"; }) - | { - /** - * Custom or future permission subject type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future permission subject type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future permission subject type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + [key: string]: unknown; + }> + ); /** * Represents an upsert for a tool call that the language model has requested. @@ -248,17 +277,30 @@ export type ToolCallContent = | (Terminal & { type: "terminal"; }) - | { - /** - * Custom or future tool call content type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future tool call content type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future tool call content type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + [key: string]: unknown; + }> + ); /** * Content blocks represent displayable information in the Agent Client Protocol. @@ -293,17 +335,30 @@ export type ContentBlock = | (EmbeddedResource & { type: "resource"; }) - | { - /** - * Custom or future content block type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future content block type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future content block type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + [key: string]: unknown; + }> + ); /** * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed @@ -636,17 +691,30 @@ export type DiffChange = ( | (DiffPathPairChange & { operation: "copy"; }) - | { - /** - * Custom or future file operation. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - operation: string; - [key: string]: unknown; - } + | ( + | { + /** + * Custom or future file operation. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + operation: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future file operation. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + operation: string; + [key: string]: unknown; + }> + ) ) & { /** * File content kind. @@ -916,17 +984,31 @@ export type CreateElicitationRequest = ( | (ElicitationUrlMode & { mode: "url"; }) - | ((ElicitationSessionScope | ElicitationRequestScope) & { - /** - * Custom or future elicitation mode. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - mode: string; - [key: string]: unknown; - }) + | ((ElicitationSessionScope | ElicitationRequestScope) & + ( + | { + /** + * Custom or future elicitation mode. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + mode: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future elicitation mode. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + mode: string; + [key: string]: unknown; + }> + )) ) & { /** * A human-readable message describing what input is needed. @@ -1055,17 +1137,30 @@ export type ElicitationPropertySchema = | (MultiSelectPropertySchema & { type: "array"; }) - | { - /** - * Custom or future elicitation property schema type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future elicitation property schema type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future elicitation property schema type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + [key: string]: unknown; + }> + ); /** * String format types for string properties in elicitation schemas. @@ -1320,17 +1415,30 @@ export type MultiSelectItems = | (StringMultiSelectItems & { type: "string"; }) - | { - /** - * Custom or future multi-select item type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - [key: string]: unknown; - } + | ( + | { + /** + * Custom or future multi-select item type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future multi-select item type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + [key: string]: unknown; + }> + ) | TitledMultiSelectItems; /** @@ -2460,39 +2568,74 @@ export type AuthMethod = | (AuthMethodAgent & { type: "agent"; }) - | { - /** - * Custom or future authentication method type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - /** - * Unique identifier for this authentication method. - */ - methodId: AuthMethodId; - /** - * Human-readable name of the authentication method. - */ - name: string; - /** - * Optional description providing more details about this authentication method. - */ - description?: string | null; - /** - * The _meta property is reserved by ACP to allow clients and agents to attach additional - * metadata to their interactions. Implementations MUST NOT make assumptions about values at - * these keys. - * - * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - */ - _meta?: { - [key: string]: unknown; - } | null; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future authentication method type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + /** + * Unique identifier for this authentication method. + */ + methodId: AuthMethodId; + /** + * Human-readable name of the authentication method. + */ + name: string; + /** + * Optional description providing more details about this authentication method. + */ + description?: string | null; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future authentication method type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + /** + * Unique identifier for this authentication method. + */ + methodId: AuthMethodId; + /** + * Human-readable name of the authentication method. + */ + name: string; + /** + * Optional description providing more details about this authentication method. + */ + description?: string | null; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; + [key: string]: unknown; + }> + ); /** * Typed identifier used for auth method values on the wire. @@ -2843,17 +2986,30 @@ export type SessionConfigOption = ( | (SessionConfigBoolean & { type: "boolean"; }) - | { - /** - * Custom or future session configuration option type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - [key: string]: unknown; - } + | ( + | { + /** + * Custom or future session configuration option type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future session configuration option type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + [key: string]: unknown; + }> + ) ) & { /** * Unique identifier for the configuration option. @@ -3033,17 +3189,30 @@ export type AvailableCommandInput = | (TextCommandInput & { type: "text"; }) - | { - /** - * Custom or future command input type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future command input type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future command input type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + [key: string]: unknown; + }> + ); /** * All text that was typed after the command name is provided as input. @@ -3346,21 +3515,38 @@ export type NesSuggestion = | (NesSearchAndReplaceSuggestion & { kind: "searchAndReplace"; }) - | { - /** - * Custom or future NES suggestion kind. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - kind: string; - /** - * Unique identifier for accept/reject tracking. - */ - suggestionId: NesSuggestionId; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future NES suggestion kind. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + kind: `_${string}`; + /** + * Unique identifier for accept/reject tracking. + */ + suggestionId: NesSuggestionId; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future NES suggestion kind. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + kind: string; + /** + * Unique identifier for accept/reject tracking. + */ + suggestionId: NesSuggestionId; + [key: string]: unknown; + }> + ); /** * **UNSTABLE** @@ -3766,17 +3952,30 @@ export type SessionUpdate = | (SessionMessageChunk & { sessionUpdate: "session_message_chunk"; }) - | { - /** - * Custom or future session update type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - sessionUpdate: string; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future session update type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + sessionUpdate: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future session update type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + sessionUpdate: string; + [key: string]: unknown; + }> + ); /** * A streamed item of message content. @@ -4087,17 +4286,30 @@ export type StateUpdate = | (UnknownStateUpdate & { state: "unknown"; }) - | { - /** - * Custom or future session state. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - state: string; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future session state. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + state: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future session state. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + state: string; + [key: string]: unknown; + }> + ); /** * A streamed item of tool-call content. @@ -4260,21 +4472,38 @@ export type PlanUpdateContent = | (PlanMarkdown & { type: "markdown"; }) - | { - /** - * Custom or future plan update content type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - /** - * The plan ID to update. - */ - planId: PlanId; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future plan update content type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + /** + * The plan ID to update. + */ + planId: PlanId; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future plan update content type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + /** + * The plan ID to update. + */ + planId: PlanId; + [key: string]: unknown; + }> + ); /** * Unique identifier for a plan within a session. @@ -5488,17 +5717,30 @@ export type McpServer = | (McpServerStdio & { type: "stdio"; }) - | { - /** - * Custom or future MCP server transport type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future MCP server transport type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future MCP server transport type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + [key: string]: unknown; + }> + ); /** * An HTTP header to set when making requests to the MCP server. @@ -5772,27 +6014,50 @@ export type ReplayFrom = | (ReplayFromStart & { type: "start"; }) - | { - /** - * Custom or future replay cursor type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - /** - * The _meta property is reserved by ACP to allow clients and agents to attach additional - * metadata to their interactions. Implementations MUST NOT make assumptions about values at - * these keys. - * - * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - */ - _meta?: { - [key: string]: unknown; - } | null; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future replay cursor type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future replay cursor type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; + [key: string]: unknown; + }> + ); /** * Inclusive replay cursor requesting replay from the start of retained conversation history. @@ -5852,21 +6117,38 @@ export type SetSessionConfigOptionRequest = ( value: boolean; type: "boolean"; } - | { - /** - * Custom or future session configuration option value type. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - type: string; - /** - * Raw value payload for the custom or future value type. - */ - value: unknown; - [key: string]: unknown; - } + | ( + | { + /** + * Custom or future session configuration option value type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: `_${string}`; + /** + * Raw value payload for the custom or future value type. + */ + value: unknown; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future session configuration option value type. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + type: string; + /** + * Raw value payload for the custom or future value type. + */ + value: unknown; + [key: string]: unknown; + }> + ) ) & { /** * The ID of the session to set the configuration option for. @@ -6385,17 +6667,30 @@ export type RequestPermissionOutcome = | (SelectedPermissionOutcome & { outcome: "selected"; }) - | { - /** - * Custom or future permission outcome. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - outcome: string; - [key: string]: unknown; - }; + | ( + | { + /** + * Custom or future permission outcome. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + outcome: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future permission outcome. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + outcome: string; + [key: string]: unknown; + }> + ); /** * The user selected one of the provided options. @@ -6430,17 +6725,30 @@ export type CreateElicitationResponse = ( | { action: "cancel"; } - | { - /** - * Custom or future elicitation action. - * - * Values beginning with `_` are reserved for implementation-specific - * extensions. Unknown values that do not begin with `_` are reserved for - * future ACP variants. - */ - action: string; - [key: string]: unknown; - } + | ( + | { + /** + * Custom or future elicitation action. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + action: `_${string}`; + [key: string]: unknown; + } + | UnknownVariant<{ + /** + * Custom or future elicitation action. + * + * Values beginning with `_` are reserved for implementation-specific + * extensions. Unknown values that do not begin with `_` are reserved for + * future ACP variants. + */ + action: string; + [key: string]: unknown; + }> + ) ) & { /** * The _meta property is reserved by ACP to allow clients and agents to attach additional