@tanstack/ai 0.44.1 → 0.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/esm/activities/chat/adapter.d.ts +13 -1
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.js +122 -69
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +24 -27
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.js +14 -13
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/approval-schema.js +11 -8
- package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +40 -31
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +3 -2
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -0
- package/dist/esm/adapter-internals.js +3 -1
- package/dist/esm/interrupt-resume.js +24 -20
- package/dist/esm/interrupt-resume.js.map +1 -1
- package/dist/esm/interrupts.js +2 -1
- package/dist/esm/interrupts.js.map +1 -1
- package/dist/esm/logger/console-logger.js +1 -3
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/resolve.js +2 -1
- package/dist/esm/logger/resolve.js.map +1 -1
- package/dist/esm/stream-durability.js +1 -1
- package/dist/esm/stream-durability.js.map +1 -1
- package/dist/esm/types.d.ts +19 -16
- package/dist/esm/utilities/chat-params.js +1 -3
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/media-prompt.js +1 -3
- package/dist/esm/utilities/media-prompt.js.map +1 -1
- package/dist/esm/utilities/structured-output-events.d.ts +17 -0
- package/dist/esm/utilities/structured-output-events.js +32 -0
- package/dist/esm/utilities/structured-output-events.js.map +1 -0
- package/dist/esm/utilities/structured-output-text.d.ts +7 -0
- package/dist/esm/utilities/structured-output-text.js +50 -0
- package/dist/esm/utilities/structured-output-text.js.map +1 -0
- package/package.json +2 -2
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +6 -2
- package/skills/ai-core/media-generation/SKILL.md +33 -19
- package/skills/ai-core/structured-outputs/SKILL.md +73 -1
- package/skills/ai-core/tool-calling/SKILL.md +1 -1
- package/src/activities/chat/adapter.ts +16 -1
- package/src/activities/chat/index.ts +155 -52
- package/src/activities/chat/stream/processor.ts +6 -3
- package/src/activities/chat/tools/tool-calls.ts +12 -4
- package/src/adapter-internals.ts +8 -0
- package/src/types.ts +27 -19
- package/src/utilities/structured-output-events.ts +44 -0
- package/src/utilities/structured-output-text.ts +63 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interrupts.js","names":[],"sources":["../../src/interrupts.ts"],"sourcesContent":["import {\n canonicalInterruptJson,\n cloneAndDeepFreezeJson,\n digestInterruptJson,\n} from './interrupt-serialization'\nimport type { RunAgentResumeItem } from './types'\n\nexport interface InterruptCorrelation {\n threadId: string\n interruptedRunId: string\n generation: number\n submissionId?: string\n continuationRunId?: string\n}\n\nexport type ItemInterruptErrorCode =\n | 'invalid-payload'\n | 'invalid-edited-args'\n | 'invalid-tool-output'\n | 'invalid-response-schema'\n | 'unknown-interrupt'\n | 'expired'\n | 'stale'\n | 'conflict'\n | 'legacy-unsupported'\n\nexport type BatchInterruptErrorCode =\n | 'incomplete-batch'\n | 'item-validation-failed'\n | 'unsupported-bulk-operation'\n | 'async-resolver'\n | 'inactive-transaction'\n | 'mixed-provenance'\n | 'transport'\n | 'server'\n | 'protocol'\n | 'invalid-response-schema'\n | 'expired'\n | 'stale'\n | 'conflict'\n | 'legacy-submit-failed'\n\nexport interface ItemInterruptError extends InterruptCorrelation {\n scope: 'item'\n interruptId: string\n code: ItemInterruptErrorCode\n message: string\n path?: ReadonlyArray<string | number>\n source: 'client' | 'server'\n retryable: boolean\n}\n\nexport interface BatchInterruptError extends InterruptCorrelation {\n scope: 'batch'\n code: BatchInterruptErrorCode\n message: string\n source: 'client' | 'server' | 'transport'\n retryable: boolean\n interruptIds: ReadonlyArray<string>\n}\n\nexport type InterruptSubmissionError = ItemInterruptError | BatchInterruptError\n\n/**\n * Wire version of {@link InterruptBinding}.\n *\n * The binding is the only part of an AG-UI `Interrupt` that this package\n * claims — it rides in `metadata` under\n * {@link INTERRUPT_BINDING_METADATA_KEY} and tells the resume path how to\n * correlate an answer back to a paused run. Producers stamp `v`; readers\n * reject any version they don't understand rather than duck-typing the fields.\n *\n * That matters because an AG-UI `Interrupt` is a shared envelope. Another\n * producer — a workflow engine projecting a durable approval, a third-party\n * agent — can legitimately put its own binding in the same envelope. Versioning\n * makes \"not mine\" a clean rejection instead of a partial match that resumes\n * against the wrong owner.\n */\nexport const INTERRUPT_BINDING_VERSION = 1 as const\n\ninterface InterruptBindingBase {\n /** @see INTERRUPT_BINDING_VERSION */\n v: typeof INTERRUPT_BINDING_VERSION\n interruptId: string\n interruptedRunId: string\n generation: number\n responseSchemaHash: string\n expiresAt?: string\n}\n\nexport type InterruptBinding =\n | (InterruptBindingBase & {\n kind: 'tool-approval'\n toolName: string\n toolCallId: string\n originalArgs: unknown\n inputSchemaHash: string\n approvalSchemaHash: string\n })\n | (InterruptBindingBase & {\n kind: 'client-tool-execution'\n toolName: string\n toolCallId: string\n outputSchemaHash: string\n })\n | (InterruptBindingBase & {\n kind: 'generic'\n })\n\nexport type UnopenedInterruptBinding = InterruptBinding extends infer TBinding\n ? TBinding extends InterruptBinding\n ? Omit<TBinding, 'interruptedRunId' | 'generation'>\n : never\n : never\n\nexport type ToolApprovalResolution =\n | boolean\n | {\n approved: true\n editedArgs?: unknown\n payload?: unknown\n }\n | {\n approved: false\n payload?: unknown\n editedArgs?: never\n }\n\nexport function canonicalizeInterruptResolutions(\n resolutions: ReadonlyArray<RunAgentResumeItem>,\n): {\n resolutions: ReadonlyArray<RunAgentResumeItem>\n canonicalResolutions: string\n fingerprint: string\n} {\n const sorted = [...resolutions].sort((left, right) =>\n left.interruptId.localeCompare(right.interruptId),\n )\n const frozen = cloneAndDeepFreezeJson(sorted)\n const canonicalResolutions = canonicalInterruptJson(frozen)\n return Object.freeze({\n resolutions: frozen,\n canonicalResolutions,\n fingerprint: digestInterruptJson(canonicalResolutions),\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA8EA,IAAa,4BAA4B;AAkDzC,SAAgB,iCACd,aAKA;
|
|
1
|
+
{"version":3,"file":"interrupts.js","names":[],"sources":["../../src/interrupts.ts"],"sourcesContent":["import {\n canonicalInterruptJson,\n cloneAndDeepFreezeJson,\n digestInterruptJson,\n} from './interrupt-serialization'\nimport type { RunAgentResumeItem } from './types'\n\nexport interface InterruptCorrelation {\n threadId: string\n interruptedRunId: string\n generation: number\n submissionId?: string\n continuationRunId?: string\n}\n\nexport type ItemInterruptErrorCode =\n | 'invalid-payload'\n | 'invalid-edited-args'\n | 'invalid-tool-output'\n | 'invalid-response-schema'\n | 'unknown-interrupt'\n | 'expired'\n | 'stale'\n | 'conflict'\n | 'legacy-unsupported'\n\nexport type BatchInterruptErrorCode =\n | 'incomplete-batch'\n | 'item-validation-failed'\n | 'unsupported-bulk-operation'\n | 'async-resolver'\n | 'inactive-transaction'\n | 'mixed-provenance'\n | 'transport'\n | 'server'\n | 'protocol'\n | 'invalid-response-schema'\n | 'expired'\n | 'stale'\n | 'conflict'\n | 'legacy-submit-failed'\n\nexport interface ItemInterruptError extends InterruptCorrelation {\n scope: 'item'\n interruptId: string\n code: ItemInterruptErrorCode\n message: string\n path?: ReadonlyArray<string | number>\n source: 'client' | 'server'\n retryable: boolean\n}\n\nexport interface BatchInterruptError extends InterruptCorrelation {\n scope: 'batch'\n code: BatchInterruptErrorCode\n message: string\n source: 'client' | 'server' | 'transport'\n retryable: boolean\n interruptIds: ReadonlyArray<string>\n}\n\nexport type InterruptSubmissionError = ItemInterruptError | BatchInterruptError\n\n/**\n * Wire version of {@link InterruptBinding}.\n *\n * The binding is the only part of an AG-UI `Interrupt` that this package\n * claims — it rides in `metadata` under\n * {@link INTERRUPT_BINDING_METADATA_KEY} and tells the resume path how to\n * correlate an answer back to a paused run. Producers stamp `v`; readers\n * reject any version they don't understand rather than duck-typing the fields.\n *\n * That matters because an AG-UI `Interrupt` is a shared envelope. Another\n * producer — a workflow engine projecting a durable approval, a third-party\n * agent — can legitimately put its own binding in the same envelope. Versioning\n * makes \"not mine\" a clean rejection instead of a partial match that resumes\n * against the wrong owner.\n */\nexport const INTERRUPT_BINDING_VERSION = 1 as const\n\ninterface InterruptBindingBase {\n /** @see INTERRUPT_BINDING_VERSION */\n v: typeof INTERRUPT_BINDING_VERSION\n interruptId: string\n interruptedRunId: string\n generation: number\n responseSchemaHash: string\n expiresAt?: string\n}\n\nexport type InterruptBinding =\n | (InterruptBindingBase & {\n kind: 'tool-approval'\n toolName: string\n toolCallId: string\n originalArgs: unknown\n inputSchemaHash: string\n approvalSchemaHash: string\n })\n | (InterruptBindingBase & {\n kind: 'client-tool-execution'\n toolName: string\n toolCallId: string\n outputSchemaHash: string\n })\n | (InterruptBindingBase & {\n kind: 'generic'\n })\n\nexport type UnopenedInterruptBinding = InterruptBinding extends infer TBinding\n ? TBinding extends InterruptBinding\n ? Omit<TBinding, 'interruptedRunId' | 'generation'>\n : never\n : never\n\nexport type ToolApprovalResolution =\n | boolean\n | {\n approved: true\n editedArgs?: unknown\n payload?: unknown\n }\n | {\n approved: false\n payload?: unknown\n editedArgs?: never\n }\n\nexport function canonicalizeInterruptResolutions(\n resolutions: ReadonlyArray<RunAgentResumeItem>,\n): {\n resolutions: ReadonlyArray<RunAgentResumeItem>\n canonicalResolutions: string\n fingerprint: string\n} {\n const sorted = [...resolutions].sort((left, right) =>\n left.interruptId.localeCompare(right.interruptId),\n )\n const frozen = cloneAndDeepFreezeJson(sorted)\n const canonicalResolutions = canonicalInterruptJson(frozen)\n return Object.freeze({\n resolutions: frozen,\n canonicalResolutions,\n fingerprint: digestInterruptJson(canonicalResolutions),\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA8EA,IAAa,4BAA4B;AAkDzC,SAAgB,iCACd,aAKA;CACA,MAAM,SAAS,CAAC,GAAG,WAAW,CAAC,CAAC,MAAM,MAAM,UAC1C,KAAK,YAAY,cAAc,MAAM,WAAW,CAClD;CACA,MAAM,SAAS,uBAAuB,MAAM;CAC5C,MAAM,uBAAuB,uBAAuB,MAAM;CAC1D,OAAO,OAAO,OAAO;EACnB,aAAa;EACb;EACA,aAAa,oBAAoB,oBAAoB;CACvD,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"console-logger.js","names":[],"sources":["../../../src/logger/console-logger.ts"],"sourcesContent":["import type { Logger } from './types'\n\n/**\n * `util.inspect` options used with `console.dir` on Node so deeply nested\n * structures (e.g. provider chunk payloads with `usage`, `output`,\n * `reasoning`, `tools`) render in full instead of truncating to\n * `[Object]` / `[Array]`.\n */\nconst DIR_OPTIONS = { depth: null, colors: true } as const\n\n/**\n * How `meta` should be rendered on the current runtime:\n *\n * - `dir` — Node. `console.dir(meta, { depth: null, colors: true })` gives a\n * depth-unlimited, colored inspect dump.\n * - `json` — Cloudflare Workers / workerd. workerd never forwards\n * `console.dir` output to the terminal (with or without options), and its\n * own inspect of extra console arguments truncates nested objects, so the\n * payload is appended as circular-safe pretty-printed JSON instead.\n * - `arg` — everything else (browsers, Deno, Bun). `meta` is passed as an\n * extra console argument: devtools keep collapsible object trees and the\n * runtime's inspect handles circular references natively.\n */\ntype MetaStrategy = 'dir' | 'json' | 'arg'\n\nfunction resolveMetaStrategy(): MetaStrategy {\n // workerd must be detected before the Node check: under the `nodejs_compat`\n // flag it emulates `process.versions.node`, but still drops `console.dir`.\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- navigator is missing on Node < 21 despite the DOM lib typing it as always present\n if (globalThis.navigator?.userAgent === 'Cloudflare-Workers') return 'json'\n } catch {\n // A locked-down runtime with a throwing `userAgent` getter is not workerd;\n // fall through to the remaining checks rather than crash the log call.\n }\n if (\n typeof process !== 'undefined' &&\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- a partial process global (bundler shims) may lack versions\n typeof process.versions?.node === 'string'\n ) {\n return 'dir'\n }\n return 'arg'\n}\n\n/**\n * `JSON.stringify` hardened for debug payloads: circular references collapse\n * to `\"[Circular]\"`, `Error` instances expand to `name`/`message`/`stack`\n * (they would otherwise stringify to `{}`), and `bigint` values become\n * strings (they would otherwise throw). Never throws — falls back to\n * `String(value)` and, if even that coercion throws, a placeholder.\n */\nfunction stringifyMetaSafely(value: unknown): string {\n const seen = new WeakSet<object>()\n try {\n return JSON.stringify(\n value,\n (_key, entry: unknown) => {\n if (typeof entry === 'bigint') return entry.toString()\n if (entry instanceof Error) {\n return {\n name: entry.name,\n message: entry.message,\n stack: entry.stack,\n }\n }\n if (typeof entry === 'object' && entry !== null) {\n if (seen.has(entry)) return '[Circular]'\n seen.add(entry)\n }\n return entry\n },\n 2,\n )\n } catch {\n try {\n return String(value)\n } catch {\n return '[Unserializable meta]'\n }\n }\n}\n\n/**\n * Default `Logger` implementation that routes each level to the matching\n * `console` method:\n *\n * - `debug` → `console.debug`\n * - `info` → `console.info`\n * - `warn` → `console.warn`\n * - `error` → `console.error`\n *\n * When a `meta` object is supplied it is rendered with the strategy that\n * actually surfaces it on the current runtime (see {@link MetaStrategy}):\n * depth-unlimited `console.dir` on Node, circular-safe JSON on Cloudflare\n * Workers, and an extra console argument everywhere else.\n *\n * This is the logger used when `debug` is enabled on any activity and no\n * custom `logger` is supplied via `debug: { logger }`.\n */\nexport class ConsoleLogger implements Logger {\n /** Log a debug-level message; forwards to `console.debug`. */\n debug(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', message, meta)\n }\n\n /** Log an info-level message; forwards to `console.info`. */\n info(message: string, meta?: Record<string, unknown>): void {\n this.emit('info', message, meta)\n }\n\n /** Log a warning-level message; forwards to `console.warn`. */\n warn(message: string, meta?: Record<string, unknown>): void {\n this.emit('warn', message, meta)\n }\n\n /** Log an error-level message; forwards to `console.error`. */\n error(message: string, meta?: Record<string, unknown>): void {\n this.emit('error', message, meta)\n }\n\n private emit(\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n meta?: Record<string, unknown>,\n ): void {\n if (meta === undefined) {\n console[level](message)\n return\n }\n switch (resolveMetaStrategy()) {\n case 'dir':\n console[level](message)\n console.dir(meta, DIR_OPTIONS)\n break\n case 'json':\n console[level](`${message}\\n${stringifyMetaSafely(meta)}`)\n break\n case 'arg':\n console[level](message, meta)\n break\n }\n }\n}\n"],"mappings":";;;;;;;AAQA,IAAM,cAAc;CAAE,OAAO;CAAM,QAAQ;AAAK;AAiBhD,SAAS,sBAAoC;CAG3C,IAAI;EAEF,IAAI,WAAW,WAAW,cAAc,sBAAsB,OAAO;CACvE,QAAQ,CAGR;CACA,IACE,OAAO,YAAY,eAEnB,OAAO,QAAQ,UAAU,SAAS,UAElC,OAAO;CAET,OAAO;AACT;;;;;;;;AASA,SAAS,oBAAoB,OAAwB;CACnD,MAAM,uBAAO,IAAI,QAAgB;CACjC,IAAI;EACF,OAAO,KAAK,UACV,QACC,MAAM,UAAmB;GACxB,IAAI,OAAO,UAAU,UAAU,OAAO,MAAM,SAAS;GACrD,IAAI,iBAAiB,OACnB,OAAO;IACL,MAAM,MAAM;IACZ,SAAS,MAAM;IACf,OAAO,MAAM;GACf;GAEF,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;IAC/C,IAAI,KAAK,IAAI,KAAK,GAAG,OAAO;IAC5B,KAAK,IAAI,KAAK;GAChB;GACA,OAAO;EACT,GACA,CACF;CACF,QAAQ;EACN,IAAI;GACF,OAAO,OAAO,KAAK;EACrB,QAAQ;GACN,OAAO;EACT;CACF;AACF;;;;;;;;;;;;;;;;;;AAmBA,IAAa,gBAAb,MAA6C;;CAE3C,MAAM,SAAiB,MAAsC;EAC3D,KAAK,KAAK,SAAS,SAAS,IAAI;CAClC;;CAGA,KAAK,SAAiB,MAAsC;EAC1D,KAAK,KAAK,QAAQ,SAAS,IAAI;CACjC;;CAGA,KAAK,SAAiB,MAAsC;EAC1D,KAAK,KAAK,QAAQ,SAAS,IAAI;CACjC;;CAGA,MAAM,SAAiB,MAAsC;EAC3D,KAAK,KAAK,SAAS,SAAS,IAAI;CAClC;CAEA,KACE,OACA,SACA,MACM;EACN,IAAI,SAAS,KAAA,GAAW;GACtB,QAAQ,MAAM,CAAC,OAAO;GACtB;EACF;EACA,QAAQ,oBAAoB,GAA5B;GACE,KAAK;IACH,QAAQ,MAAM,CAAC,OAAO;IACtB,QAAQ,IAAI,MAAM,WAAW;IAC7B;GACF,KAAK;IACH,QAAQ,MAAM,CAAC,GAAG,QAAQ,IAAI,oBAAoB,IAAI,GAAG;IACzD;GACF,KAAK
|
|
1
|
+
{"version":3,"file":"console-logger.js","names":[],"sources":["../../../src/logger/console-logger.ts"],"sourcesContent":["import type { Logger } from './types'\n\n/**\n * `util.inspect` options used with `console.dir` on Node so deeply nested\n * structures (e.g. provider chunk payloads with `usage`, `output`,\n * `reasoning`, `tools`) render in full instead of truncating to\n * `[Object]` / `[Array]`.\n */\nconst DIR_OPTIONS = { depth: null, colors: true } as const\n\n/**\n * How `meta` should be rendered on the current runtime:\n *\n * - `dir` — Node. `console.dir(meta, { depth: null, colors: true })` gives a\n * depth-unlimited, colored inspect dump.\n * - `json` — Cloudflare Workers / workerd. workerd never forwards\n * `console.dir` output to the terminal (with or without options), and its\n * own inspect of extra console arguments truncates nested objects, so the\n * payload is appended as circular-safe pretty-printed JSON instead.\n * - `arg` — everything else (browsers, Deno, Bun). `meta` is passed as an\n * extra console argument: devtools keep collapsible object trees and the\n * runtime's inspect handles circular references natively.\n */\ntype MetaStrategy = 'dir' | 'json' | 'arg'\n\nfunction resolveMetaStrategy(): MetaStrategy {\n // workerd must be detected before the Node check: under the `nodejs_compat`\n // flag it emulates `process.versions.node`, but still drops `console.dir`.\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- navigator is missing on Node < 21 despite the DOM lib typing it as always present\n if (globalThis.navigator?.userAgent === 'Cloudflare-Workers') return 'json'\n } catch {\n // A locked-down runtime with a throwing `userAgent` getter is not workerd;\n // fall through to the remaining checks rather than crash the log call.\n }\n if (\n typeof process !== 'undefined' &&\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- a partial process global (bundler shims) may lack versions\n typeof process.versions?.node === 'string'\n ) {\n return 'dir'\n }\n return 'arg'\n}\n\n/**\n * `JSON.stringify` hardened for debug payloads: circular references collapse\n * to `\"[Circular]\"`, `Error` instances expand to `name`/`message`/`stack`\n * (they would otherwise stringify to `{}`), and `bigint` values become\n * strings (they would otherwise throw). Never throws — falls back to\n * `String(value)` and, if even that coercion throws, a placeholder.\n */\nfunction stringifyMetaSafely(value: unknown): string {\n const seen = new WeakSet<object>()\n try {\n return JSON.stringify(\n value,\n (_key, entry: unknown) => {\n if (typeof entry === 'bigint') return entry.toString()\n if (entry instanceof Error) {\n return {\n name: entry.name,\n message: entry.message,\n stack: entry.stack,\n }\n }\n if (typeof entry === 'object' && entry !== null) {\n if (seen.has(entry)) return '[Circular]'\n seen.add(entry)\n }\n return entry\n },\n 2,\n )\n } catch {\n try {\n return String(value)\n } catch {\n return '[Unserializable meta]'\n }\n }\n}\n\n/**\n * Default `Logger` implementation that routes each level to the matching\n * `console` method:\n *\n * - `debug` → `console.debug`\n * - `info` → `console.info`\n * - `warn` → `console.warn`\n * - `error` → `console.error`\n *\n * When a `meta` object is supplied it is rendered with the strategy that\n * actually surfaces it on the current runtime (see {@link MetaStrategy}):\n * depth-unlimited `console.dir` on Node, circular-safe JSON on Cloudflare\n * Workers, and an extra console argument everywhere else.\n *\n * This is the logger used when `debug` is enabled on any activity and no\n * custom `logger` is supplied via `debug: { logger }`.\n */\nexport class ConsoleLogger implements Logger {\n /** Log a debug-level message; forwards to `console.debug`. */\n debug(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', message, meta)\n }\n\n /** Log an info-level message; forwards to `console.info`. */\n info(message: string, meta?: Record<string, unknown>): void {\n this.emit('info', message, meta)\n }\n\n /** Log a warning-level message; forwards to `console.warn`. */\n warn(message: string, meta?: Record<string, unknown>): void {\n this.emit('warn', message, meta)\n }\n\n /** Log an error-level message; forwards to `console.error`. */\n error(message: string, meta?: Record<string, unknown>): void {\n this.emit('error', message, meta)\n }\n\n private emit(\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n meta?: Record<string, unknown>,\n ): void {\n if (meta === undefined) {\n console[level](message)\n return\n }\n switch (resolveMetaStrategy()) {\n case 'dir':\n console[level](message)\n console.dir(meta, DIR_OPTIONS)\n break\n case 'json':\n console[level](`${message}\\n${stringifyMetaSafely(meta)}`)\n break\n case 'arg':\n console[level](message, meta)\n break\n }\n }\n}\n"],"mappings":";;;;;;;AAQA,IAAM,cAAc;CAAE,OAAO;CAAM,QAAQ;AAAK;AAiBhD,SAAS,sBAAoC;CAG3C,IAAI;EAEF,IAAI,WAAW,WAAW,cAAc,sBAAsB,OAAO;CACvE,QAAQ,CAGR;CACA,IACE,OAAO,YAAY,eAEnB,OAAO,QAAQ,UAAU,SAAS,UAElC,OAAO;CAET,OAAO;AACT;;;;;;;;AASA,SAAS,oBAAoB,OAAwB;CACnD,MAAM,uBAAO,IAAI,QAAgB;CACjC,IAAI;EACF,OAAO,KAAK,UACV,QACC,MAAM,UAAmB;GACxB,IAAI,OAAO,UAAU,UAAU,OAAO,MAAM,SAAS;GACrD,IAAI,iBAAiB,OACnB,OAAO;IACL,MAAM,MAAM;IACZ,SAAS,MAAM;IACf,OAAO,MAAM;GACf;GAEF,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;IAC/C,IAAI,KAAK,IAAI,KAAK,GAAG,OAAO;IAC5B,KAAK,IAAI,KAAK;GAChB;GACA,OAAO;EACT,GACA,CACF;CACF,QAAQ;EACN,IAAI;GACF,OAAO,OAAO,KAAK;EACrB,QAAQ;GACN,OAAO;EACT;CACF;AACF;;;;;;;;;;;;;;;;;;AAmBA,IAAa,gBAAb,MAA6C;;CAE3C,MAAM,SAAiB,MAAsC;EAC3D,KAAK,KAAK,SAAS,SAAS,IAAI;CAClC;;CAGA,KAAK,SAAiB,MAAsC;EAC1D,KAAK,KAAK,QAAQ,SAAS,IAAI;CACjC;;CAGA,KAAK,SAAiB,MAAsC;EAC1D,KAAK,KAAK,QAAQ,SAAS,IAAI;CACjC;;CAGA,MAAM,SAAiB,MAAsC;EAC3D,KAAK,KAAK,SAAS,SAAS,IAAI;CAClC;CAEA,KACE,OACA,SACA,MACM;EACN,IAAI,SAAS,KAAA,GAAW;GACtB,QAAQ,MAAM,CAAC,OAAO;GACtB;EACF;EACA,QAAQ,oBAAoB,GAA5B;GACE,KAAK;IACH,QAAQ,MAAM,CAAC,OAAO;IACtB,QAAQ,IAAI,MAAM,WAAW;IAC7B;GACF,KAAK;IACH,QAAQ,MAAM,CAAC,GAAG,QAAQ,IAAI,oBAAoB,IAAI,GAAG;IACzD;GACF,KAAK,OACH,QAAQ,MAAM,CAAC,SAAS,IAAI;EAEhC;CACF;AACF"}
|
|
@@ -54,7 +54,8 @@ function resolveDebugOption(debug) {
|
|
|
54
54
|
if (debug === true) return new InternalLogger(new ConsoleLogger(), ALL_ON);
|
|
55
55
|
if (debug === false) return new InternalLogger(new ConsoleLogger(), ALL_OFF);
|
|
56
56
|
const { logger, ...cats } = debug;
|
|
57
|
-
|
|
57
|
+
const userLogger = logger ?? new ConsoleLogger();
|
|
58
|
+
return new InternalLogger(userLogger, resolveCategoriesFromPartial(cats));
|
|
58
59
|
}
|
|
59
60
|
//#endregion
|
|
60
61
|
export { resolveDebugOption };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.js","names":[],"sources":["../../../src/logger/resolve.ts"],"sourcesContent":["import { ConsoleLogger } from './console-logger'\nimport { InternalLogger } from './internal-logger'\nimport type { ResolvedCategories } from './internal-logger'\nimport type { DebugCategories, DebugConfig, DebugOption, Logger } from './types'\n\nconst ALL_OFF: ResolvedCategories = {\n provider: false,\n output: false,\n middleware: false,\n tools: false,\n agentLoop: false,\n config: false,\n errors: false,\n request: false,\n sandbox: false,\n}\n\nconst ALL_ON: ResolvedCategories = {\n provider: true,\n output: true,\n middleware: true,\n tools: true,\n agentLoop: true,\n config: true,\n errors: true,\n request: true,\n sandbox: true,\n}\n\nconst errorsOnlyCategories = (): ResolvedCategories => ({\n ...ALL_OFF,\n errors: true,\n})\n\nconst resolveCategoriesFromPartial = (\n partial: DebugCategories,\n): ResolvedCategories => ({\n provider: partial.provider ?? true,\n output: partial.output ?? true,\n middleware: partial.middleware ?? true,\n tools: partial.tools ?? true,\n agentLoop: partial.agentLoop ?? true,\n config: partial.config ?? true,\n errors: partial.errors ?? true,\n request: partial.request ?? true,\n sandbox: partial.sandbox ?? true,\n})\n\n/**\n * Normalize a `DebugOption` into an `InternalLogger` ready to be threaded\n * through the library's activities and adapters. See the `DebugOption`\n * resolution table in the spec for the complete rules.\n *\n * - `undefined`: only the `errors` category is enabled; default `ConsoleLogger`.\n * - `true`: all categories enabled; default `ConsoleLogger`.\n * - `false`: all categories disabled (including `errors`); default `ConsoleLogger`.\n * - `DebugConfig`: each unspecified category defaults to `true`; an optional\n * `logger` replaces the default `ConsoleLogger`.\n */\nexport function resolveDebugOption(\n debug: DebugOption | undefined,\n): InternalLogger {\n if (debug === undefined) {\n return new InternalLogger(new ConsoleLogger(), errorsOnlyCategories())\n }\n if (debug === true) {\n return new InternalLogger(new ConsoleLogger(), ALL_ON)\n }\n if (debug === false) {\n return new InternalLogger(new ConsoleLogger(), ALL_OFF)\n }\n const { logger, ...cats }: DebugConfig = debug\n const userLogger: Logger = logger ?? new ConsoleLogger()\n return new InternalLogger(userLogger, resolveCategoriesFromPartial(cats))\n}\n"],"mappings":";;;AAKA,IAAM,UAA8B;CAClC,UAAU;CACV,QAAQ;CACR,YAAY;CACZ,OAAO;CACP,WAAW;CACX,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;AACX;AAEA,IAAM,SAA6B;CACjC,UAAU;CACV,QAAQ;CACR,YAAY;CACZ,OAAO;CACP,WAAW;CACX,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;AACX;AAEA,IAAM,8BAAkD;CACtD,GAAG;CACH,QAAQ;AACV;AAEA,IAAM,gCACJ,aACwB;CACxB,UAAU,QAAQ,YAAY;CAC9B,QAAQ,QAAQ,UAAU;CAC1B,YAAY,QAAQ,cAAc;CAClC,OAAO,QAAQ,SAAS;CACxB,WAAW,QAAQ,aAAa;CAChC,QAAQ,QAAQ,UAAU;CAC1B,QAAQ,QAAQ,UAAU;CAC1B,SAAS,QAAQ,WAAW;CAC5B,SAAS,QAAQ,WAAW;AAC9B;;;;;;;;;;;;AAaA,SAAgB,mBACd,OACgB;CAChB,IAAI,UAAU,KAAA,GACZ,OAAO,IAAI,eAAe,IAAI,cAAc,GAAG,qBAAqB,CAAC;CAEvE,IAAI,UAAU,MACZ,OAAO,IAAI,eAAe,IAAI,cAAc,GAAG,MAAM;CAEvD,IAAI,UAAU,OACZ,OAAO,IAAI,eAAe,IAAI,cAAc,GAAG,OAAO;CAExD,MAAM,EAAE,QAAQ,GAAG,SAAsB;
|
|
1
|
+
{"version":3,"file":"resolve.js","names":[],"sources":["../../../src/logger/resolve.ts"],"sourcesContent":["import { ConsoleLogger } from './console-logger'\nimport { InternalLogger } from './internal-logger'\nimport type { ResolvedCategories } from './internal-logger'\nimport type { DebugCategories, DebugConfig, DebugOption, Logger } from './types'\n\nconst ALL_OFF: ResolvedCategories = {\n provider: false,\n output: false,\n middleware: false,\n tools: false,\n agentLoop: false,\n config: false,\n errors: false,\n request: false,\n sandbox: false,\n}\n\nconst ALL_ON: ResolvedCategories = {\n provider: true,\n output: true,\n middleware: true,\n tools: true,\n agentLoop: true,\n config: true,\n errors: true,\n request: true,\n sandbox: true,\n}\n\nconst errorsOnlyCategories = (): ResolvedCategories => ({\n ...ALL_OFF,\n errors: true,\n})\n\nconst resolveCategoriesFromPartial = (\n partial: DebugCategories,\n): ResolvedCategories => ({\n provider: partial.provider ?? true,\n output: partial.output ?? true,\n middleware: partial.middleware ?? true,\n tools: partial.tools ?? true,\n agentLoop: partial.agentLoop ?? true,\n config: partial.config ?? true,\n errors: partial.errors ?? true,\n request: partial.request ?? true,\n sandbox: partial.sandbox ?? true,\n})\n\n/**\n * Normalize a `DebugOption` into an `InternalLogger` ready to be threaded\n * through the library's activities and adapters. See the `DebugOption`\n * resolution table in the spec for the complete rules.\n *\n * - `undefined`: only the `errors` category is enabled; default `ConsoleLogger`.\n * - `true`: all categories enabled; default `ConsoleLogger`.\n * - `false`: all categories disabled (including `errors`); default `ConsoleLogger`.\n * - `DebugConfig`: each unspecified category defaults to `true`; an optional\n * `logger` replaces the default `ConsoleLogger`.\n */\nexport function resolveDebugOption(\n debug: DebugOption | undefined,\n): InternalLogger {\n if (debug === undefined) {\n return new InternalLogger(new ConsoleLogger(), errorsOnlyCategories())\n }\n if (debug === true) {\n return new InternalLogger(new ConsoleLogger(), ALL_ON)\n }\n if (debug === false) {\n return new InternalLogger(new ConsoleLogger(), ALL_OFF)\n }\n const { logger, ...cats }: DebugConfig = debug\n const userLogger: Logger = logger ?? new ConsoleLogger()\n return new InternalLogger(userLogger, resolveCategoriesFromPartial(cats))\n}\n"],"mappings":";;;AAKA,IAAM,UAA8B;CAClC,UAAU;CACV,QAAQ;CACR,YAAY;CACZ,OAAO;CACP,WAAW;CACX,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;AACX;AAEA,IAAM,SAA6B;CACjC,UAAU;CACV,QAAQ;CACR,YAAY;CACZ,OAAO;CACP,WAAW;CACX,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;AACX;AAEA,IAAM,8BAAkD;CACtD,GAAG;CACH,QAAQ;AACV;AAEA,IAAM,gCACJ,aACwB;CACxB,UAAU,QAAQ,YAAY;CAC9B,QAAQ,QAAQ,UAAU;CAC1B,YAAY,QAAQ,cAAc;CAClC,OAAO,QAAQ,SAAS;CACxB,WAAW,QAAQ,aAAa;CAChC,QAAQ,QAAQ,UAAU;CAC1B,QAAQ,QAAQ,UAAU;CAC1B,SAAS,QAAQ,WAAW;CAC5B,SAAS,QAAQ,WAAW;AAC9B;;;;;;;;;;;;AAaA,SAAgB,mBACd,OACgB;CAChB,IAAI,UAAU,KAAA,GACZ,OAAO,IAAI,eAAe,IAAI,cAAc,GAAG,qBAAqB,CAAC;CAEvE,IAAI,UAAU,MACZ,OAAO,IAAI,eAAe,IAAI,cAAc,GAAG,MAAM;CAEvD,IAAI,UAAU,OACZ,OAAO,IAAI,eAAe,IAAI,cAAc,GAAG,OAAO;CAExD,MAAM,EAAE,QAAQ,GAAG,SAAsB;CACzC,MAAM,aAAqB,UAAU,IAAI,cAAc;CACvD,OAAO,IAAI,eAAe,YAAY,6BAA6B,IAAI,CAAC;AAC1E"}
|
|
@@ -67,7 +67,7 @@ function memoryThreshold(offset, runId, tail) {
|
|
|
67
67
|
* (incomplete) logs are never evicted, so an in-flight run is never dropped.
|
|
68
68
|
*/
|
|
69
69
|
var MAX_MEMORY_RUNS = 1024;
|
|
70
|
-
var COMPLETED_LOG_TTL_MS =
|
|
70
|
+
var COMPLETED_LOG_TTL_MS = 3e5;
|
|
71
71
|
/**
|
|
72
72
|
* How long a from-start join (`-1` / `now`) waits for a run's first chunk before
|
|
73
73
|
* failing. Bounds the "joined a run that never produces" case so a consumer
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-durability.js","names":[],"sources":["../../src/stream-durability.ts"],"sourcesContent":["import type { StreamChunk } from './types'\n\n/**\n * A pluggable delivery-durability backend.\n *\n * Offsets are owned by the adapter and opaque to the transport. The generic\n * parameter lets an adapter retain a branded string type across append, read,\n * and resume without requiring core to understand its cursor format.\n */\nexport interface StreamDurability<TOffset extends string = string> {\n /** Return the adapter offset captured from the request, or null for a producer. */\n resumeFrom: () => TOffset | null\n /**\n * Persist a batch before it is delivered and return exactly one resumable\n * offset for each chunk, in the same order.\n */\n append: (chunks: Array<StreamChunk>) => Promise<Array<TOffset>>\n /** Replay chunks strictly after the supplied adapter-owned offset. */\n read: (\n offset: TOffset,\n signal?: AbortSignal,\n ) => AsyncIterable<{ offset: TOffset; chunk: StreamChunk }>\n /**\n * Terminalize the producer log and unblock live readers. Core awaits this\n * for every producer exit, including completion, cancellation, and failure.\n */\n close: () => Promise<void>\n /**\n * Everything stored for this run **at the moment of the call**, in append\n * order, then resolve.\n *\n * This is the bounded counterpart to {@link StreamDurability.read}. `read`\n * tails: it parks until the log is terminalized or the caller aborts, so it\n * cannot be used to inspect a log whose producer died without calling\n * `close` — that log stays open forever and a `for await` over it never\n * finishes. `snapshot` exists for exactly that case: a producer resuming a\n * run needs to see the prefix a previous host already stored so it can line\n * its own output up against it, and it needs that read to *return*.\n *\n * Implementations MUST:\n *\n * - never wait for more entries — resolve with what is stored, including\n * while the log is still open and still being appended to;\n * - resolve to an empty array for a run with nothing stored, rather than\n * throwing. In particular an implementation must not reuse the\n * unknown-run failure path a from-start `read` join takes (`read('-1')` on\n * an empty log is allowed to fail; `snapshot()` is not). A backend over a\n * network may of course still reject on a transport, protocol, or\n * authorization failure — that is a failed call, not an empty run;\n * - return a fresh array the caller can keep or mutate without reaching the\n * stored log through it.\n *\n * The result is a point-in-time view and carries no lock: a concurrent\n * `append` may land immediately after the snapshot is taken, so a caller\n * must not treat the last returned offset as the permanent tail.\n */\n snapshot: () => Promise<Array<{ offset: TOffset; chunk: StreamChunk }>>\n}\n\n/**\n * A {@link StreamDurability} that can re-persist an already-stored range\n * idempotently.\n *\n * A run driver resuming after a crash re-derives the same offsets from its\n * source position, so replaying an overlapping range must be a no-op rather\n * than producing duplicates. That capability is deliberately a **separate,\n * optional method** instead of an optional parameter on `append`:\n *\n * - Only adapters that actually support it return this type, so a consumer\n * requiring the capability asks for `UpsertableStreamDurability` and a\n * mismatch is a compile error rather than a runtime failure buried in a\n * run log.\n * - Pairing each chunk with its offset structurally makes a length mismatch\n * and an unpaired chunk unrepresentable. A sparse hole is still\n * representable, so implementations must reject one explicitly.\n *\n * Implementations MUST validate the entire batch before mutating any stored\n * state (so a rejected call never partially applies), MUST reject an offset\n * they did not mint themselves (every accepted offset is resumable by\n * definition), MUST reject an offset repeated within one batch, and MUST\n * reject a hole in the entries array.\n */\nexport interface UpsertableStreamDurability<\n TOffset extends string = string,\n> extends StreamDurability<TOffset> {\n /**\n * Persist a batch at caller-supplied offsets, replacing any entry already\n * stored at the same offset. Returns the offsets in the order supplied.\n */\n upsert: (\n entries: Array<{ chunk: StreamChunk; offset: TOffset }>,\n ) => Promise<Array<TOffset>>\n}\n\nconst MEMORY_OFFSET_PREFIX = 'memory:v1:'\n\ninterface MemoryOffset {\n runId: string\n seq: number\n}\n\nfunction encodeMemoryOffset(runId: string, seq: number): string {\n return `${MEMORY_OFFSET_PREFIX}${encodeURIComponent(runId)}:${seq}`\n}\n\nfunction decodeMemoryOffset(offset: string): MemoryOffset {\n if (!offset.startsWith(MEMORY_OFFSET_PREFIX)) {\n throw new Error(`Invalid memory stream offset: ${offset}`)\n }\n const encoded = offset.slice(MEMORY_OFFSET_PREFIX.length)\n const separator = encoded.lastIndexOf(':')\n if (separator === -1) {\n throw new Error(`Invalid memory stream offset: ${offset}`)\n }\n const runId = decodeURIComponent(encoded.slice(0, separator))\n const seq = Number(encoded.slice(separator + 1))\n if (!Number.isSafeInteger(seq) || seq < 1) {\n throw new Error(`Invalid memory stream offset: ${offset}`)\n }\n return { runId, seq }\n}\n\nfunction readResumeOffset(request: Request): string | null {\n const header = request.headers.get('Last-Event-ID')\n if (header) return header\n try {\n return new URL(request.url).searchParams.get('offset')\n } catch {\n return null\n }\n}\n\n/**\n * The run id a request names: `X-Run-Id` header first, then `?runId`.\n *\n * The single implementation of that precedence, shared by the durability\n * adapters below and by the resume response helpers' run driver\n * (`stream-to-response.ts`), so the helper and the adapter can never disagree\n * about which run a request is talking about.\n */\nexport function resolveResumeRunId(request: Request): string | null {\n // A POST producer carries its client-chosen run id in the X-Run-Id header so\n // the request URL stays byte-identical to a plain, non-durable request; the\n // GET join path carries it in the ?runId query instead. Prefer the header,\n // fall back to the query.\n const header = request.headers.get('X-Run-Id')\n if (header) return header\n try {\n return new URL(request.url).searchParams.get('runId')\n } catch {\n return null\n }\n}\n\nfunction assertValidRunId(runId: string): string {\n if (runId.length === 0 || /[\\r\\n]/.test(runId)) {\n throw new Error(\n `Invalid runId (must be non-empty and contain no CR/LF): ${JSON.stringify(runId)}`,\n )\n }\n return runId\n}\n\nfunction resolveMemoryRunId(\n request: Request,\n resumeOffset: string | null,\n): string {\n if (\n resumeOffset !== null &&\n resumeOffset !== '-1' &&\n resumeOffset !== 'now'\n ) {\n return assertValidRunId(decodeMemoryOffset(resumeOffset).runId)\n }\n const requestedRunId = resolveResumeRunId(request)\n return requestedRunId === null\n ? crypto.randomUUID()\n : assertValidRunId(requestedRunId)\n}\n\nfunction memoryThreshold(offset: string, runId: string, tail: number): number {\n if (offset === '-1') return -1\n if (offset === 'now') return tail\n const decoded = decodeMemoryOffset(offset)\n if (decoded.runId !== runId) {\n throw new Error(\n `Memory stream offset belongs to run ${JSON.stringify(decoded.runId)}, not ${JSON.stringify(runId)}`,\n )\n }\n return decoded.seq\n}\n\ninterface MemoryEntry {\n seq: number\n offset: string\n chunk: StreamChunk\n}\n\n/**\n * One validated action from an `upsert` batch. Building the whole plan before\n * applying any of it is what keeps a rejected `upsert` from partially mutating\n * the log.\n */\ntype UpsertStep =\n | { kind: 'replace'; existing: MemoryEntry; chunk: StreamChunk }\n | { kind: 'push'; seq: number; offset: string; chunk: StreamChunk }\n\ninterface MemoryLog {\n entries: Array<MemoryEntry>\n complete: boolean\n /** Epoch ms when the log was terminalized; undefined while still producing. */\n completedAt: number | undefined\n waiters: Array<() => void>\n}\n\n/**\n * Bounds for the in-process log store. `memoryStream` is the dev/single-process\n * backend; without eviction its module-global Map would grow without bound on a\n * long-lived server (one retained chunk buffer per run, forever). Completed logs\n * are swept after a grace window — late resumers/joiners still work briefly —\n * and a hard cap drops the oldest completed logs under pressure. Active\n * (incomplete) logs are never evicted, so an in-flight run is never dropped.\n */\nconst MAX_MEMORY_RUNS = 1024\nconst COMPLETED_LOG_TTL_MS = 5 * 60_000\n\n/**\n * How long a from-start join (`-1` / `now`) waits for a run's first chunk before\n * failing. Bounds the \"joined a run that never produces\" case so a consumer\n * gets a surfaced error instead of an indefinitely-open, event-less connection.\n *\n * Defaults short: the common from-start join is a reload rejoining a run whose\n * producer ran in a PRIOR request, so an in-flight run's log already holds\n * chunks (it streams immediately, deadline never applies) and an empty log means\n * the run is gone — failing fast lets the client re-enable input near-instantly\n * instead of hanging. Raise `firstChunkDeadlineMs` for backends where a producer\n * legitimately starts well after a joiner attaches (a queued/deferred job).\n */\nconst DEFAULT_FIRST_CHUNK_DEADLINE_MS = 100\n\n/** Options for the in-process delivery-durability backend. */\nexport interface MemoryStreamOptions {\n /**\n * Milliseconds a from-start join waits for the run's first chunk before\n * throwing. Defaults to {@link DEFAULT_FIRST_CHUNK_DEADLINE_MS} (100ms) —\n * raise it if a producer can legitimately start long after a joiner attaches.\n */\n firstChunkDeadlineMs?: number\n}\n\nconst memoryLogs = new Map<string, MemoryLog>()\n\n/**\n * Evict completed logs past their grace window, then, if still over the cap,\n * drop the oldest completed logs (the Map preserves insertion order) until back\n * under the cap. Never touches an incomplete (in-flight) log.\n */\nfunction sweepMemoryLogs(now: number): void {\n for (const [id, log] of memoryLogs) {\n if (\n log.complete &&\n log.completedAt !== undefined &&\n now - log.completedAt > COMPLETED_LOG_TTL_MS\n ) {\n memoryLogs.delete(id)\n }\n }\n if (memoryLogs.size <= MAX_MEMORY_RUNS) return\n for (const [id, log] of memoryLogs) {\n if (memoryLogs.size <= MAX_MEMORY_RUNS) break\n if (log.complete) memoryLogs.delete(id)\n }\n}\n\nfunction getOrCreateLog(id: string): MemoryLog {\n let log = memoryLogs.get(id)\n if (!log) {\n sweepMemoryLogs(Date.now())\n log = { entries: [], complete: false, completedAt: undefined, waiters: [] }\n memoryLogs.set(id, log)\n }\n return log\n}\n\nfunction markComplete(log: MemoryLog): void {\n if (!log.complete) {\n log.complete = true\n log.completedAt = Date.now()\n }\n}\n\nfunction wakeWaiters(log: MemoryLog): void {\n const waiters = log.waiters\n log.waiters = []\n for (const wake of waiters) wake()\n}\n\n/**\n * Explicit construction for {@link memoryStream}, for callers that don't have\n * the incoming `Request` — e.g. a TanStack Start server function implementing\n * a `joinRun` replay for a run id it received as call data:\n *\n * ```ts\n * const durability = memoryStream({ runId })\n * for await (const chunk of replayRunStream(durability)) yield chunk\n * ```\n */\nexport interface MemoryStreamInit {\n /** The run this durability adapter attaches to. */\n runId: string\n /**\n * Resume offset captured by the consumer (`resumeFrom()` returns it).\n * Defaults to `null` (a producer / from-start reader).\n */\n offset?: string | null\n}\n\n/**\n * The zero-infrastructure delivery-durability backend. Its versioned cursor is\n * deliberately private: callers and core only pass the returned string back.\n *\n * Construct from the incoming `Request` (HTTP transports) or from an explicit\n * {@link MemoryStreamInit} (server functions / direct calls that already know\n * the run id).\n *\n * Logs live in a process-global map, so this backend is for development, tests,\n * and single-process deployments only. Completed runs are evicted after a grace\n * window (see {@link COMPLETED_LOG_TTL_MS}); a resume of an evicted or unknown\n * run fails loudly rather than hanging.\n */\nexport function memoryStream(\n source: Request | MemoryStreamInit,\n options: MemoryStreamOptions = {},\n): UpsertableStreamDurability {\n const resumeOffset =\n source instanceof Request\n ? readResumeOffset(source)\n : (source.offset ?? null)\n const runId =\n source instanceof Request\n ? resolveMemoryRunId(source, resumeOffset)\n : assertValidRunId(source.runId)\n const firstChunkDeadlineMs =\n options.firstChunkDeadlineMs ?? DEFAULT_FIRST_CHUNK_DEADLINE_MS\n\n return {\n resumeFrom: () => resumeOffset,\n // `async` so every failure surfaces as a rejected promise rather than a\n // synchronous throw at the call site — `append` is declared to return a\n // Promise, so callers must be able to `.catch()` every failure mode.\n append: async (chunks) => {\n const log = getOrCreateLog(runId)\n const firstSeq = (log.entries.at(-1)?.seq ?? 0) + 1\n const offsets = chunks.map((chunk, index) => {\n const seq = firstSeq + index\n const offset = encodeMemoryOffset(runId, seq)\n log.entries.push({ seq, offset, chunk })\n return offset\n })\n wakeWaiters(log)\n return offsets\n },\n // `async` for the same reason as `append`: every validation failure below\n // must be observable via `.catch()`, never as a synchronous throw.\n upsert: async (entries) => {\n const log = getOrCreateLog(runId)\n const tailSeq = log.entries.at(-1)?.seq ?? 0\n\n // Validate the WHOLE batch before touching `log.entries`, so a rejected\n // upsert never partially applies and a caller that catches and retries\n // can be sure no prefix landed.\n const seen = new Set<string>()\n // Tail as it will stand once every push planned so far has been applied,\n // so intra-batch ordering is validated up front too.\n let plannedTailSeq = tailSeq\n // `Array.from` rather than `entries.map`: `map` SKIPS holes in a sparse\n // array, which would leave the plan short and make the apply loop below\n // read `undefined` partway through, after earlier steps had already\n // mutated the log. `Array.from` invokes this callback for every index,\n // so a hole is rejected here, before anything is touched.\n const plan = Array.from(entries, (entry, index): UpsertStep => {\n if (entry === undefined) {\n throw new Error(\n `memoryStream: entries[${index}] is missing; entries must be dense`,\n )\n }\n const { chunk, offset } = entry\n let decoded: MemoryOffset\n try {\n decoded = decodeMemoryOffset(offset)\n } catch (cause) {\n throw new Error(\n `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} is not a resumable memory stream offset: ${cause instanceof Error ? cause.message : String(cause)}`,\n )\n }\n if (decoded.runId !== runId) {\n throw new Error(\n `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} belongs to run ${JSON.stringify(decoded.runId)}, not ${JSON.stringify(runId)}`,\n )\n }\n const seq = decoded.seq\n if (seen.has(offset)) {\n throw new Error(\n `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} is repeated within the batch; each offset may appear at most once`,\n )\n }\n seen.add(offset)\n const existing = log.entries.find((stored) => stored.offset === offset)\n if (existing) return { kind: 'replace', existing, chunk }\n // A not-yet-stored offset must sit strictly after the current tail.\n // `read()` walks `entries` in array order and filters `seq > threshold`,\n // so a pushed entry has to keep the seqs monotonically increasing;\n // reusing the offset's own decoded seq also keeps a returned offset's\n // threshold exactly consistent with the entry it names. Gaps are fine —\n // nothing depends on seqs being contiguous, only on them increasing —\n // so do NOT \"fix\" this to renumber densely.\n if (seq <= plannedTailSeq) {\n throw new Error(\n `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} is not stored yet but claims position ${seq}, at or before the tail ${plannedTailSeq}; a new offset must come after every stored and preceding entry`,\n )\n }\n plannedTailSeq = seq\n return { kind: 'push', seq, offset, chunk }\n })\n\n // Validation passed for every entry — mutation below cannot fail.\n for (const step of plan) {\n if (step.kind === 'replace') {\n step.existing.chunk = step.chunk\n } else {\n log.entries.push({\n seq: step.seq,\n offset: step.offset,\n chunk: step.chunk,\n })\n }\n }\n wakeWaiters(log)\n return plan.map((step) =>\n step.kind === 'replace' ? step.existing.offset : step.offset,\n )\n },\n snapshot: () => {\n // Peek, never getOrCreateLog: an unknown run must resolve to `[]`, and\n // inserting an empty, never-completed log here would leave a permanent\n // entry the sweep cannot reclaim (it only reclaims complete logs).\n const log = memoryLogs.get(runId)\n if (log === undefined) return Promise.resolve([])\n // Fresh outer array AND fresh pair objects, so a caller that mutates the\n // result cannot reach `log.entries` or the stored entries through it.\n // Never touches `log.waiters` — a snapshot is a point-in-time read and\n // returns even while the log is open and still being appended to.\n return Promise.resolve(\n log.entries.map((entry) => ({\n offset: entry.offset,\n chunk: entry.chunk,\n })),\n )\n },\n close: () => {\n const log = getOrCreateLog(runId)\n markComplete(log)\n wakeWaiters(log)\n return Promise.resolve()\n },\n read: async function* (offset, signal) {\n const isFromStartJoin = offset === '-1' || offset === 'now'\n\n // Peek, never getOrCreateLog. A concrete resume offset for an absent run\n // means the run was evicted (or never lived in this process) and will not\n // reappear — fail WITHOUT inserting a log. Inserting here would leave a\n // permanent empty, never-completed log (sweep only reclaims complete\n // ones), so client-supplied offsets could grow the map without bound and\n // defeat the eviction this backend relies on.\n let log = memoryLogs.get(runId)\n if (log === undefined || (log.entries.length === 0 && !log.complete)) {\n if (!isFromStartJoin) {\n throw new Error(\n `Unknown or expired memory stream run: ${JSON.stringify(runId)}`,\n )\n }\n // A from-start join may legitimately attach before the producer creates\n // the log (second-tab race); create it so a later append reuses the\n // same entry. If no producer ever arrives, the first-chunk deadline\n // below deletes this phantom before rejecting.\n log = getOrCreateLog(runId)\n }\n\n const threshold = memoryThreshold(\n offset,\n runId,\n log.entries.at(-1)?.seq ?? 0,\n )\n let index = 0\n\n for (;;) {\n while (index < log.entries.length) {\n const entry = log.entries[index]\n index += 1\n if (entry && entry.seq > threshold) {\n yield { offset: entry.offset, chunk: entry.chunk }\n }\n }\n // A terminal chunk (RUN_FINISHED / RUN_ERROR) does NOT end the read: an\n // agent-loop run emits one per iteration (finishReason \"tool_calls\" then\n // \"stop\"), so stopping on the first would truncate a tool-calling run at\n // its first tool call. The producer signals true completion by calling\n // `close()` (it does so on every exit — see StreamDurability.close), which\n // sets `log.complete`. Read tails until then, or until the caller aborts.\n if (log.complete || signal?.aborted) return\n\n // Bound only the wait for the very first chunk: once a run has produced\n // anything, its producer owns termination and a caught-up reader may\n // legitimately park indefinitely between chunks.\n const deadlineForFirstChunk =\n log.entries.length === 0 ? firstChunkDeadlineMs : undefined\n\n await new Promise<void>((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const cleanup = () => {\n if (timer !== undefined) clearTimeout(timer)\n signal?.removeEventListener('abort', onAbort)\n const waiterIndex = log.waiters.indexOf(wake)\n if (waiterIndex !== -1) log.waiters.splice(waiterIndex, 1)\n }\n const onAbort = () => {\n cleanup()\n resolve()\n }\n const wake = () => {\n cleanup()\n resolve()\n }\n log.waiters.push(wake)\n signal?.addEventListener('abort', onAbort, { once: true })\n if (deadlineForFirstChunk !== undefined) {\n timer = setTimeout(() => {\n cleanup()\n // No producer ever created data for this joined run. Drop the\n // phantom log we created above so it does not linger uncollected\n // (it is empty and will never be marked complete).\n if (\n log.entries.length === 0 &&\n !log.complete &&\n memoryLogs.get(runId) === log\n ) {\n memoryLogs.delete(runId)\n }\n reject(\n new Error(\n `Memory stream run produced no data within ${deadlineForFirstChunk}ms: ${JSON.stringify(runId)}`,\n ),\n )\n }, deadlineForFirstChunk)\n }\n })\n }\n },\n }\n}\n\n/**\n * Replay a run's delivery-durability log as a bare stream of chunks, for\n * callers that serve a `joinRun` handler without an HTTP `Response` — e.g. a\n * TanStack Start server function returning an async iterable:\n *\n * ```ts\n * async function* joinImageRun({ data: runId }: { data: string }) {\n * yield* replayRunStream(memoryStream({ runId }))\n * }\n *\n * // Serve it from a server function whose handler is the generator above\n * // (`createServerFn({ method: 'GET' }).inputValidator(...)`).\n * ```\n *\n * NOTE: the example deliberately declares the generator separately instead of\n * inlining it into the server-fn builder chain. TanStack Start's server-fn\n * Vite plugin decides whether a module needs compiling by regex-matching the\n * SOURCE for a dotted `handler(` call, and JSDoc survives into `dist` — an\n * inlined chain here would make every Start app treat this package as a\n * server-fn module and try to resolve its framework's `@tanstack/*-start`\n * package, failing the build wherever that framework is not the one installed.\n *\n * Reads from `offset` (default `'-1'` — from the start) and tails until the\n * producer closes the log or `signal` aborts, exactly like the HTTP\n * `resumeServerSentEventsResponse` path.\n */\nexport async function* replayRunStream<TOffset extends string>(\n durability: StreamDurability<TOffset>,\n offset?: TOffset,\n signal?: AbortSignal,\n): AsyncGenerator<StreamChunk> {\n // '-1' is the from-start replay sentinel every shipped backend honors.\n const from = offset ?? ('-1' as TOffset)\n for await (const { chunk } of durability.read(from, signal)) {\n yield chunk\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stream-durability.js","names":[],"sources":["../../src/stream-durability.ts"],"sourcesContent":["import type { StreamChunk } from './types'\n\n/**\n * A pluggable delivery-durability backend.\n *\n * Offsets are owned by the adapter and opaque to the transport. The generic\n * parameter lets an adapter retain a branded string type across append, read,\n * and resume without requiring core to understand its cursor format.\n */\nexport interface StreamDurability<TOffset extends string = string> {\n /** Return the adapter offset captured from the request, or null for a producer. */\n resumeFrom: () => TOffset | null\n /**\n * Persist a batch before it is delivered and return exactly one resumable\n * offset for each chunk, in the same order.\n */\n append: (chunks: Array<StreamChunk>) => Promise<Array<TOffset>>\n /** Replay chunks strictly after the supplied adapter-owned offset. */\n read: (\n offset: TOffset,\n signal?: AbortSignal,\n ) => AsyncIterable<{ offset: TOffset; chunk: StreamChunk }>\n /**\n * Terminalize the producer log and unblock live readers. Core awaits this\n * for every producer exit, including completion, cancellation, and failure.\n */\n close: () => Promise<void>\n /**\n * Everything stored for this run **at the moment of the call**, in append\n * order, then resolve.\n *\n * This is the bounded counterpart to {@link StreamDurability.read}. `read`\n * tails: it parks until the log is terminalized or the caller aborts, so it\n * cannot be used to inspect a log whose producer died without calling\n * `close` — that log stays open forever and a `for await` over it never\n * finishes. `snapshot` exists for exactly that case: a producer resuming a\n * run needs to see the prefix a previous host already stored so it can line\n * its own output up against it, and it needs that read to *return*.\n *\n * Implementations MUST:\n *\n * - never wait for more entries — resolve with what is stored, including\n * while the log is still open and still being appended to;\n * - resolve to an empty array for a run with nothing stored, rather than\n * throwing. In particular an implementation must not reuse the\n * unknown-run failure path a from-start `read` join takes (`read('-1')` on\n * an empty log is allowed to fail; `snapshot()` is not). A backend over a\n * network may of course still reject on a transport, protocol, or\n * authorization failure — that is a failed call, not an empty run;\n * - return a fresh array the caller can keep or mutate without reaching the\n * stored log through it.\n *\n * The result is a point-in-time view and carries no lock: a concurrent\n * `append` may land immediately after the snapshot is taken, so a caller\n * must not treat the last returned offset as the permanent tail.\n */\n snapshot: () => Promise<Array<{ offset: TOffset; chunk: StreamChunk }>>\n}\n\n/**\n * A {@link StreamDurability} that can re-persist an already-stored range\n * idempotently.\n *\n * A run driver resuming after a crash re-derives the same offsets from its\n * source position, so replaying an overlapping range must be a no-op rather\n * than producing duplicates. That capability is deliberately a **separate,\n * optional method** instead of an optional parameter on `append`:\n *\n * - Only adapters that actually support it return this type, so a consumer\n * requiring the capability asks for `UpsertableStreamDurability` and a\n * mismatch is a compile error rather than a runtime failure buried in a\n * run log.\n * - Pairing each chunk with its offset structurally makes a length mismatch\n * and an unpaired chunk unrepresentable. A sparse hole is still\n * representable, so implementations must reject one explicitly.\n *\n * Implementations MUST validate the entire batch before mutating any stored\n * state (so a rejected call never partially applies), MUST reject an offset\n * they did not mint themselves (every accepted offset is resumable by\n * definition), MUST reject an offset repeated within one batch, and MUST\n * reject a hole in the entries array.\n */\nexport interface UpsertableStreamDurability<\n TOffset extends string = string,\n> extends StreamDurability<TOffset> {\n /**\n * Persist a batch at caller-supplied offsets, replacing any entry already\n * stored at the same offset. Returns the offsets in the order supplied.\n */\n upsert: (\n entries: Array<{ chunk: StreamChunk; offset: TOffset }>,\n ) => Promise<Array<TOffset>>\n}\n\nconst MEMORY_OFFSET_PREFIX = 'memory:v1:'\n\ninterface MemoryOffset {\n runId: string\n seq: number\n}\n\nfunction encodeMemoryOffset(runId: string, seq: number): string {\n return `${MEMORY_OFFSET_PREFIX}${encodeURIComponent(runId)}:${seq}`\n}\n\nfunction decodeMemoryOffset(offset: string): MemoryOffset {\n if (!offset.startsWith(MEMORY_OFFSET_PREFIX)) {\n throw new Error(`Invalid memory stream offset: ${offset}`)\n }\n const encoded = offset.slice(MEMORY_OFFSET_PREFIX.length)\n const separator = encoded.lastIndexOf(':')\n if (separator === -1) {\n throw new Error(`Invalid memory stream offset: ${offset}`)\n }\n const runId = decodeURIComponent(encoded.slice(0, separator))\n const seq = Number(encoded.slice(separator + 1))\n if (!Number.isSafeInteger(seq) || seq < 1) {\n throw new Error(`Invalid memory stream offset: ${offset}`)\n }\n return { runId, seq }\n}\n\nfunction readResumeOffset(request: Request): string | null {\n const header = request.headers.get('Last-Event-ID')\n if (header) return header\n try {\n return new URL(request.url).searchParams.get('offset')\n } catch {\n return null\n }\n}\n\n/**\n * The run id a request names: `X-Run-Id` header first, then `?runId`.\n *\n * The single implementation of that precedence, shared by the durability\n * adapters below and by the resume response helpers' run driver\n * (`stream-to-response.ts`), so the helper and the adapter can never disagree\n * about which run a request is talking about.\n */\nexport function resolveResumeRunId(request: Request): string | null {\n // A POST producer carries its client-chosen run id in the X-Run-Id header so\n // the request URL stays byte-identical to a plain, non-durable request; the\n // GET join path carries it in the ?runId query instead. Prefer the header,\n // fall back to the query.\n const header = request.headers.get('X-Run-Id')\n if (header) return header\n try {\n return new URL(request.url).searchParams.get('runId')\n } catch {\n return null\n }\n}\n\nfunction assertValidRunId(runId: string): string {\n if (runId.length === 0 || /[\\r\\n]/.test(runId)) {\n throw new Error(\n `Invalid runId (must be non-empty and contain no CR/LF): ${JSON.stringify(runId)}`,\n )\n }\n return runId\n}\n\nfunction resolveMemoryRunId(\n request: Request,\n resumeOffset: string | null,\n): string {\n if (\n resumeOffset !== null &&\n resumeOffset !== '-1' &&\n resumeOffset !== 'now'\n ) {\n return assertValidRunId(decodeMemoryOffset(resumeOffset).runId)\n }\n const requestedRunId = resolveResumeRunId(request)\n return requestedRunId === null\n ? crypto.randomUUID()\n : assertValidRunId(requestedRunId)\n}\n\nfunction memoryThreshold(offset: string, runId: string, tail: number): number {\n if (offset === '-1') return -1\n if (offset === 'now') return tail\n const decoded = decodeMemoryOffset(offset)\n if (decoded.runId !== runId) {\n throw new Error(\n `Memory stream offset belongs to run ${JSON.stringify(decoded.runId)}, not ${JSON.stringify(runId)}`,\n )\n }\n return decoded.seq\n}\n\ninterface MemoryEntry {\n seq: number\n offset: string\n chunk: StreamChunk\n}\n\n/**\n * One validated action from an `upsert` batch. Building the whole plan before\n * applying any of it is what keeps a rejected `upsert` from partially mutating\n * the log.\n */\ntype UpsertStep =\n | { kind: 'replace'; existing: MemoryEntry; chunk: StreamChunk }\n | { kind: 'push'; seq: number; offset: string; chunk: StreamChunk }\n\ninterface MemoryLog {\n entries: Array<MemoryEntry>\n complete: boolean\n /** Epoch ms when the log was terminalized; undefined while still producing. */\n completedAt: number | undefined\n waiters: Array<() => void>\n}\n\n/**\n * Bounds for the in-process log store. `memoryStream` is the dev/single-process\n * backend; without eviction its module-global Map would grow without bound on a\n * long-lived server (one retained chunk buffer per run, forever). Completed logs\n * are swept after a grace window — late resumers/joiners still work briefly —\n * and a hard cap drops the oldest completed logs under pressure. Active\n * (incomplete) logs are never evicted, so an in-flight run is never dropped.\n */\nconst MAX_MEMORY_RUNS = 1024\nconst COMPLETED_LOG_TTL_MS = 5 * 60_000\n\n/**\n * How long a from-start join (`-1` / `now`) waits for a run's first chunk before\n * failing. Bounds the \"joined a run that never produces\" case so a consumer\n * gets a surfaced error instead of an indefinitely-open, event-less connection.\n *\n * Defaults short: the common from-start join is a reload rejoining a run whose\n * producer ran in a PRIOR request, so an in-flight run's log already holds\n * chunks (it streams immediately, deadline never applies) and an empty log means\n * the run is gone — failing fast lets the client re-enable input near-instantly\n * instead of hanging. Raise `firstChunkDeadlineMs` for backends where a producer\n * legitimately starts well after a joiner attaches (a queued/deferred job).\n */\nconst DEFAULT_FIRST_CHUNK_DEADLINE_MS = 100\n\n/** Options for the in-process delivery-durability backend. */\nexport interface MemoryStreamOptions {\n /**\n * Milliseconds a from-start join waits for the run's first chunk before\n * throwing. Defaults to {@link DEFAULT_FIRST_CHUNK_DEADLINE_MS} (100ms) —\n * raise it if a producer can legitimately start long after a joiner attaches.\n */\n firstChunkDeadlineMs?: number\n}\n\nconst memoryLogs = new Map<string, MemoryLog>()\n\n/**\n * Evict completed logs past their grace window, then, if still over the cap,\n * drop the oldest completed logs (the Map preserves insertion order) until back\n * under the cap. Never touches an incomplete (in-flight) log.\n */\nfunction sweepMemoryLogs(now: number): void {\n for (const [id, log] of memoryLogs) {\n if (\n log.complete &&\n log.completedAt !== undefined &&\n now - log.completedAt > COMPLETED_LOG_TTL_MS\n ) {\n memoryLogs.delete(id)\n }\n }\n if (memoryLogs.size <= MAX_MEMORY_RUNS) return\n for (const [id, log] of memoryLogs) {\n if (memoryLogs.size <= MAX_MEMORY_RUNS) break\n if (log.complete) memoryLogs.delete(id)\n }\n}\n\nfunction getOrCreateLog(id: string): MemoryLog {\n let log = memoryLogs.get(id)\n if (!log) {\n sweepMemoryLogs(Date.now())\n log = { entries: [], complete: false, completedAt: undefined, waiters: [] }\n memoryLogs.set(id, log)\n }\n return log\n}\n\nfunction markComplete(log: MemoryLog): void {\n if (!log.complete) {\n log.complete = true\n log.completedAt = Date.now()\n }\n}\n\nfunction wakeWaiters(log: MemoryLog): void {\n const waiters = log.waiters\n log.waiters = []\n for (const wake of waiters) wake()\n}\n\n/**\n * Explicit construction for {@link memoryStream}, for callers that don't have\n * the incoming `Request` — e.g. a TanStack Start server function implementing\n * a `joinRun` replay for a run id it received as call data:\n *\n * ```ts\n * const durability = memoryStream({ runId })\n * for await (const chunk of replayRunStream(durability)) yield chunk\n * ```\n */\nexport interface MemoryStreamInit {\n /** The run this durability adapter attaches to. */\n runId: string\n /**\n * Resume offset captured by the consumer (`resumeFrom()` returns it).\n * Defaults to `null` (a producer / from-start reader).\n */\n offset?: string | null\n}\n\n/**\n * The zero-infrastructure delivery-durability backend. Its versioned cursor is\n * deliberately private: callers and core only pass the returned string back.\n *\n * Construct from the incoming `Request` (HTTP transports) or from an explicit\n * {@link MemoryStreamInit} (server functions / direct calls that already know\n * the run id).\n *\n * Logs live in a process-global map, so this backend is for development, tests,\n * and single-process deployments only. Completed runs are evicted after a grace\n * window (see {@link COMPLETED_LOG_TTL_MS}); a resume of an evicted or unknown\n * run fails loudly rather than hanging.\n */\nexport function memoryStream(\n source: Request | MemoryStreamInit,\n options: MemoryStreamOptions = {},\n): UpsertableStreamDurability {\n const resumeOffset =\n source instanceof Request\n ? readResumeOffset(source)\n : (source.offset ?? null)\n const runId =\n source instanceof Request\n ? resolveMemoryRunId(source, resumeOffset)\n : assertValidRunId(source.runId)\n const firstChunkDeadlineMs =\n options.firstChunkDeadlineMs ?? DEFAULT_FIRST_CHUNK_DEADLINE_MS\n\n return {\n resumeFrom: () => resumeOffset,\n // `async` so every failure surfaces as a rejected promise rather than a\n // synchronous throw at the call site — `append` is declared to return a\n // Promise, so callers must be able to `.catch()` every failure mode.\n append: async (chunks) => {\n const log = getOrCreateLog(runId)\n const firstSeq = (log.entries.at(-1)?.seq ?? 0) + 1\n const offsets = chunks.map((chunk, index) => {\n const seq = firstSeq + index\n const offset = encodeMemoryOffset(runId, seq)\n log.entries.push({ seq, offset, chunk })\n return offset\n })\n wakeWaiters(log)\n return offsets\n },\n // `async` for the same reason as `append`: every validation failure below\n // must be observable via `.catch()`, never as a synchronous throw.\n upsert: async (entries) => {\n const log = getOrCreateLog(runId)\n const tailSeq = log.entries.at(-1)?.seq ?? 0\n\n // Validate the WHOLE batch before touching `log.entries`, so a rejected\n // upsert never partially applies and a caller that catches and retries\n // can be sure no prefix landed.\n const seen = new Set<string>()\n // Tail as it will stand once every push planned so far has been applied,\n // so intra-batch ordering is validated up front too.\n let plannedTailSeq = tailSeq\n // `Array.from` rather than `entries.map`: `map` SKIPS holes in a sparse\n // array, which would leave the plan short and make the apply loop below\n // read `undefined` partway through, after earlier steps had already\n // mutated the log. `Array.from` invokes this callback for every index,\n // so a hole is rejected here, before anything is touched.\n const plan = Array.from(entries, (entry, index): UpsertStep => {\n if (entry === undefined) {\n throw new Error(\n `memoryStream: entries[${index}] is missing; entries must be dense`,\n )\n }\n const { chunk, offset } = entry\n let decoded: MemoryOffset\n try {\n decoded = decodeMemoryOffset(offset)\n } catch (cause) {\n throw new Error(\n `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} is not a resumable memory stream offset: ${cause instanceof Error ? cause.message : String(cause)}`,\n )\n }\n if (decoded.runId !== runId) {\n throw new Error(\n `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} belongs to run ${JSON.stringify(decoded.runId)}, not ${JSON.stringify(runId)}`,\n )\n }\n const seq = decoded.seq\n if (seen.has(offset)) {\n throw new Error(\n `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} is repeated within the batch; each offset may appear at most once`,\n )\n }\n seen.add(offset)\n const existing = log.entries.find((stored) => stored.offset === offset)\n if (existing) return { kind: 'replace', existing, chunk }\n // A not-yet-stored offset must sit strictly after the current tail.\n // `read()` walks `entries` in array order and filters `seq > threshold`,\n // so a pushed entry has to keep the seqs monotonically increasing;\n // reusing the offset's own decoded seq also keeps a returned offset's\n // threshold exactly consistent with the entry it names. Gaps are fine —\n // nothing depends on seqs being contiguous, only on them increasing —\n // so do NOT \"fix\" this to renumber densely.\n if (seq <= plannedTailSeq) {\n throw new Error(\n `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} is not stored yet but claims position ${seq}, at or before the tail ${plannedTailSeq}; a new offset must come after every stored and preceding entry`,\n )\n }\n plannedTailSeq = seq\n return { kind: 'push', seq, offset, chunk }\n })\n\n // Validation passed for every entry — mutation below cannot fail.\n for (const step of plan) {\n if (step.kind === 'replace') {\n step.existing.chunk = step.chunk\n } else {\n log.entries.push({\n seq: step.seq,\n offset: step.offset,\n chunk: step.chunk,\n })\n }\n }\n wakeWaiters(log)\n return plan.map((step) =>\n step.kind === 'replace' ? step.existing.offset : step.offset,\n )\n },\n snapshot: () => {\n // Peek, never getOrCreateLog: an unknown run must resolve to `[]`, and\n // inserting an empty, never-completed log here would leave a permanent\n // entry the sweep cannot reclaim (it only reclaims complete logs).\n const log = memoryLogs.get(runId)\n if (log === undefined) return Promise.resolve([])\n // Fresh outer array AND fresh pair objects, so a caller that mutates the\n // result cannot reach `log.entries` or the stored entries through it.\n // Never touches `log.waiters` — a snapshot is a point-in-time read and\n // returns even while the log is open and still being appended to.\n return Promise.resolve(\n log.entries.map((entry) => ({\n offset: entry.offset,\n chunk: entry.chunk,\n })),\n )\n },\n close: () => {\n const log = getOrCreateLog(runId)\n markComplete(log)\n wakeWaiters(log)\n return Promise.resolve()\n },\n read: async function* (offset, signal) {\n const isFromStartJoin = offset === '-1' || offset === 'now'\n\n // Peek, never getOrCreateLog. A concrete resume offset for an absent run\n // means the run was evicted (or never lived in this process) and will not\n // reappear — fail WITHOUT inserting a log. Inserting here would leave a\n // permanent empty, never-completed log (sweep only reclaims complete\n // ones), so client-supplied offsets could grow the map without bound and\n // defeat the eviction this backend relies on.\n let log = memoryLogs.get(runId)\n if (log === undefined || (log.entries.length === 0 && !log.complete)) {\n if (!isFromStartJoin) {\n throw new Error(\n `Unknown or expired memory stream run: ${JSON.stringify(runId)}`,\n )\n }\n // A from-start join may legitimately attach before the producer creates\n // the log (second-tab race); create it so a later append reuses the\n // same entry. If no producer ever arrives, the first-chunk deadline\n // below deletes this phantom before rejecting.\n log = getOrCreateLog(runId)\n }\n\n const threshold = memoryThreshold(\n offset,\n runId,\n log.entries.at(-1)?.seq ?? 0,\n )\n let index = 0\n\n for (;;) {\n while (index < log.entries.length) {\n const entry = log.entries[index]\n index += 1\n if (entry && entry.seq > threshold) {\n yield { offset: entry.offset, chunk: entry.chunk }\n }\n }\n // A terminal chunk (RUN_FINISHED / RUN_ERROR) does NOT end the read: an\n // agent-loop run emits one per iteration (finishReason \"tool_calls\" then\n // \"stop\"), so stopping on the first would truncate a tool-calling run at\n // its first tool call. The producer signals true completion by calling\n // `close()` (it does so on every exit — see StreamDurability.close), which\n // sets `log.complete`. Read tails until then, or until the caller aborts.\n if (log.complete || signal?.aborted) return\n\n // Bound only the wait for the very first chunk: once a run has produced\n // anything, its producer owns termination and a caught-up reader may\n // legitimately park indefinitely between chunks.\n const deadlineForFirstChunk =\n log.entries.length === 0 ? firstChunkDeadlineMs : undefined\n\n await new Promise<void>((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const cleanup = () => {\n if (timer !== undefined) clearTimeout(timer)\n signal?.removeEventListener('abort', onAbort)\n const waiterIndex = log.waiters.indexOf(wake)\n if (waiterIndex !== -1) log.waiters.splice(waiterIndex, 1)\n }\n const onAbort = () => {\n cleanup()\n resolve()\n }\n const wake = () => {\n cleanup()\n resolve()\n }\n log.waiters.push(wake)\n signal?.addEventListener('abort', onAbort, { once: true })\n if (deadlineForFirstChunk !== undefined) {\n timer = setTimeout(() => {\n cleanup()\n // No producer ever created data for this joined run. Drop the\n // phantom log we created above so it does not linger uncollected\n // (it is empty and will never be marked complete).\n if (\n log.entries.length === 0 &&\n !log.complete &&\n memoryLogs.get(runId) === log\n ) {\n memoryLogs.delete(runId)\n }\n reject(\n new Error(\n `Memory stream run produced no data within ${deadlineForFirstChunk}ms: ${JSON.stringify(runId)}`,\n ),\n )\n }, deadlineForFirstChunk)\n }\n })\n }\n },\n }\n}\n\n/**\n * Replay a run's delivery-durability log as a bare stream of chunks, for\n * callers that serve a `joinRun` handler without an HTTP `Response` — e.g. a\n * TanStack Start server function returning an async iterable:\n *\n * ```ts\n * async function* joinImageRun({ data: runId }: { data: string }) {\n * yield* replayRunStream(memoryStream({ runId }))\n * }\n *\n * // Serve it from a server function whose handler is the generator above\n * // (`createServerFn({ method: 'GET' }).inputValidator(...)`).\n * ```\n *\n * NOTE: the example deliberately declares the generator separately instead of\n * inlining it into the server-fn builder chain. TanStack Start's server-fn\n * Vite plugin decides whether a module needs compiling by regex-matching the\n * SOURCE for a dotted `handler(` call, and JSDoc survives into `dist` — an\n * inlined chain here would make every Start app treat this package as a\n * server-fn module and try to resolve its framework's `@tanstack/*-start`\n * package, failing the build wherever that framework is not the one installed.\n *\n * Reads from `offset` (default `'-1'` — from the start) and tails until the\n * producer closes the log or `signal` aborts, exactly like the HTTP\n * `resumeServerSentEventsResponse` path.\n */\nexport async function* replayRunStream<TOffset extends string>(\n durability: StreamDurability<TOffset>,\n offset?: TOffset,\n signal?: AbortSignal,\n): AsyncGenerator<StreamChunk> {\n // '-1' is the from-start replay sentinel every shipped backend honors.\n const from = offset ?? ('-1' as TOffset)\n for await (const { chunk } of durability.read(from, signal)) {\n yield chunk\n }\n}\n"],"mappings":"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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -766,10 +766,13 @@ export interface TextOptions<TProviderOptionsSuperset extends Record<string, any
|
|
|
766
766
|
* `supportsCombinedToolsAndSchema(modelOptions) === true`. The adapter
|
|
767
767
|
* should then wire the schema into the upstream request (e.g.
|
|
768
768
|
* `response_format: { type: 'json_schema', ... }`, `text.format`,
|
|
769
|
-
* `output_format`) alongside any `tools`.
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
769
|
+
* `output_format`, `--json-schema`) alongside any `tools`.
|
|
770
|
+
*
|
|
771
|
+
* How the engine then takes the object depends on
|
|
772
|
+
* `combinedStructuredOutputSource()`:
|
|
773
|
+
* - `'text'` (default): the final-turn assistant text is the JSON.
|
|
774
|
+
* - `'event'`: the adapter emits `structured-output.complete` during
|
|
775
|
+
* `chatStream`. Accumulated prose is not parsed.
|
|
773
776
|
*
|
|
774
777
|
* Adapters that did NOT declare the capability never see this field
|
|
775
778
|
* populated — the engine instead invokes `structuredOutput` /
|
|
@@ -1321,34 +1324,34 @@ export interface CodeModeExternalErrorEvent extends CustomEvent {
|
|
|
1321
1324
|
duration: number;
|
|
1322
1325
|
};
|
|
1323
1326
|
}
|
|
1324
|
-
export interface
|
|
1325
|
-
name: 'code_mode:
|
|
1327
|
+
export interface CodeModeSnippetCallEvent extends CustomEvent {
|
|
1328
|
+
name: 'code_mode:snippet_call';
|
|
1326
1329
|
value: {
|
|
1327
|
-
|
|
1330
|
+
snippet: string;
|
|
1328
1331
|
input: unknown;
|
|
1329
1332
|
timestamp: number;
|
|
1330
1333
|
};
|
|
1331
1334
|
}
|
|
1332
|
-
export interface
|
|
1333
|
-
name: 'code_mode:
|
|
1335
|
+
export interface CodeModeSnippetResultEvent extends CustomEvent {
|
|
1336
|
+
name: 'code_mode:snippet_result';
|
|
1334
1337
|
value: {
|
|
1335
|
-
|
|
1338
|
+
snippet: string;
|
|
1336
1339
|
result: unknown;
|
|
1337
1340
|
duration: number;
|
|
1338
1341
|
timestamp: number;
|
|
1339
1342
|
};
|
|
1340
1343
|
}
|
|
1341
|
-
export interface
|
|
1342
|
-
name: 'code_mode:
|
|
1344
|
+
export interface CodeModeSnippetErrorEvent extends CustomEvent {
|
|
1345
|
+
name: 'code_mode:snippet_error';
|
|
1343
1346
|
value: {
|
|
1344
|
-
|
|
1347
|
+
snippet: string;
|
|
1345
1348
|
error: string;
|
|
1346
1349
|
duration: number;
|
|
1347
1350
|
timestamp: number;
|
|
1348
1351
|
};
|
|
1349
1352
|
}
|
|
1350
|
-
export interface
|
|
1351
|
-
name: '
|
|
1353
|
+
export interface SnippetRegisteredEvent extends CustomEvent {
|
|
1354
|
+
name: 'snippet:registered';
|
|
1352
1355
|
value: {
|
|
1353
1356
|
id: string;
|
|
1354
1357
|
name: string;
|
|
@@ -1361,7 +1364,7 @@ export interface SkillRegisteredEvent extends CustomEvent {
|
|
|
1361
1364
|
* `name`. User-emitted custom events (via `emitCustomEvent` with a custom name)
|
|
1362
1365
|
* are intentionally absent — they still flow at runtime.
|
|
1363
1366
|
*/
|
|
1364
|
-
export type KnownCustomEvent = SandboxFileCustomEvent | SandboxFileDiffEvent | FileChangedEvent | SessionIdEvent | CodeModeExecutionStartedEvent | CodeModeConsoleEvent | CodeModeExternalCallEvent | CodeModeExternalResultEvent | CodeModeExternalErrorEvent |
|
|
1367
|
+
export type KnownCustomEvent = SandboxFileCustomEvent | SandboxFileDiffEvent | FileChangedEvent | SessionIdEvent | CodeModeExecutionStartedEvent | CodeModeConsoleEvent | CodeModeExternalCallEvent | CodeModeExternalResultEvent | CodeModeExternalErrorEvent | CodeModeSnippetCallEvent | CodeModeSnippetResultEvent | CodeModeSnippetErrorEvent | SnippetRegisteredEvent | StructuredOutputStartEvent | StructuredOutputCompleteEvent | ApprovalRequestedEvent | ToolInputAvailableEvent | UIResourceEvent;
|
|
1365
1368
|
/** The default chat streaming result: standard chunks plus every typed
|
|
1366
1369
|
* framework CUSTOM event, with the `value: any` catch-all excluded so
|
|
1367
1370
|
* literal-`name` narrowing types `value`. User-emitted custom names are typed
|
|
@@ -80,9 +80,7 @@ function assertAGUIMessage(value, at) {
|
|
|
80
80
|
break;
|
|
81
81
|
case "developer":
|
|
82
82
|
case "system":
|
|
83
|
-
case "reasoning":
|
|
84
|
-
requireString(value.content, `${at}.content`);
|
|
85
|
-
break;
|
|
83
|
+
case "reasoning": requireString(value.content, `${at}.content`);
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
86
|
function validateMessage(value, index) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-params.js","names":[],"sources":["../../../src/utilities/chat-params.ts"],"sourcesContent":["import { AGUIError } from '@ag-ui/core'\nimport type {\n Context as AGUIContext,\n Message as AGUIMessage,\n ResumeEntry as AGUIResumeEntry,\n Role as AGUIRole,\n} from '@ag-ui/core'\nimport type {\n AnyTool,\n JSONSchema,\n ModelMessage,\n RunAgentResumeItem,\n UIMessage,\n} from '../types'\n\nconst KNOWN_PART_TYPES = new Set([\n 'text',\n 'image',\n 'audio',\n 'video',\n 'document',\n 'tool-call',\n 'tool-result',\n 'thinking',\n])\n\nfunction isValidParts(value: unknown): value is Array<{ type: string }> {\n if (!Array.isArray(value)) return false\n for (const p of value) {\n if (!p || typeof p !== 'object') return false\n const type = (p as { type?: unknown }).type\n if (typeof type !== 'string' || !KNOWN_PART_TYPES.has(type)) return false\n }\n return true\n}\n\n/**\n * Keyed by `AGUIRole` so a role added upstream fails to compile here until it\n * is handled, rather than silently falling through as an unknown role.\n */\nconst AGUI_ROLES: Record<AGUIRole, true> = {\n developer: true,\n system: true,\n assistant: true,\n user: true,\n tool: true,\n activity: true,\n reasoning: true,\n}\n\nfunction isAGUIRole(value: unknown): value is AGUIRole {\n return typeof value === 'string' && value in AGUI_ROLES\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * Reject the request body, pointing at the migration guide. Mirrors the\n * message the previous `RunAgentInputSchema.safeParse` failure produced.\n */\nfunction invalidBody(reason: string): never {\n throw new AGUIError(\n `Request body is not a valid AG-UI RunAgentInput. ` +\n `If you're upgrading from a previous @tanstack/ai-client release, ` +\n `see docs/migration/ag-ui-compliance.md. ` +\n `Validation errors: ${reason}`,\n )\n}\n\nfunction requireString(value: unknown, at: string): string {\n if (typeof value !== 'string') invalidBody(`${at} must be a string`)\n return value\n}\n\nfunction requireArray(value: unknown, at: string): Array<unknown> {\n if (!Array.isArray(value)) invalidBody(`${at} must be an array`)\n return value\n}\n\n/**\n * Assert one AG-UI `Message`, discriminating on `role` exactly as the upstream\n * `MessageSchema` discriminated union does. The record view is retained on the\n * asserted type so callers can still inspect non-AG-UI extras like `parts`.\n */\nfunction assertAGUIMessage(\n value: Record<string, unknown>,\n at: string,\n): asserts value is Record<string, unknown> & AGUIMessage {\n requireString(value.id, `${at}.id`)\n\n const role = value.role\n if (!isAGUIRole(role)) {\n invalidBody(\n `${at}.role must be one of ${Object.keys(AGUI_ROLES).join(' | ')}`,\n )\n }\n\n switch (role) {\n case 'assistant':\n // Both optional: a tool-calling turn carries no text content.\n if (value.content !== undefined) {\n requireString(value.content, `${at}.content`)\n }\n if (value.toolCalls !== undefined) {\n requireArray(value.toolCalls, `${at}.toolCalls`)\n }\n break\n case 'user':\n if (typeof value.content !== 'string' && !Array.isArray(value.content)) {\n invalidBody(\n `${at}.content must be a string or an array of content parts`,\n )\n }\n break\n case 'tool':\n requireString(value.content, `${at}.content`)\n requireString(value.toolCallId, `${at}.toolCallId`)\n break\n case 'activity':\n requireString(value.activityType, `${at}.activityType`)\n if (!isRecord(value.content)) {\n invalidBody(`${at}.content must be an object`)\n }\n break\n case 'developer':\n case 'system':\n case 'reasoning':\n requireString(value.content, `${at}.content`)\n break\n }\n}\n\nfunction validateMessage(value: unknown, index: number): AGUIMessage {\n const at = `messages[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n assertAGUIMessage(value, at)\n\n // `parts` is TanStack's canonical extra, carried through so the UIMessage\n // path inside `chat()` can use it. Keep it only when it holds recognized\n // part types — the previous schema-based path dropped `parts` during parse\n // and re-attached it from the raw body behind this same check.\n if ('parts' in value && !isValidParts(value.parts)) {\n const withoutParts = { ...value }\n Reflect.deleteProperty(withoutParts, 'parts')\n return withoutParts\n }\n return value\n}\n\nfunction validateTool(\n value: unknown,\n index: number,\n): { name: string; description: string; parameters: JSONSchema } {\n const at = `tools[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n return {\n name: requireString(value.name, `${at}.name`),\n description: requireString(value.description, `${at}.description`),\n // Upstream `ToolSchema` types this as optional `any`; it reaches the\n // provider as a raw JSON Schema either way.\n parameters: value.parameters as JSONSchema,\n }\n}\n\nfunction validateContext(value: unknown, index: number): AGUIContext {\n const at = `context[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n return {\n description: requireString(value.description, `${at}.description`),\n value: requireString(value.value, `${at}.value`),\n }\n}\n\nfunction validateResumeEntry(value: unknown, index: number): AGUIResumeEntry {\n const at = `resume[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n const status = value.status\n if (status !== 'resolved' && status !== 'cancelled') {\n invalidBody(`${at}.status must be \"resolved\" or \"cancelled\"`)\n }\n const entry: AGUIResumeEntry = {\n interruptId: requireString(value.interruptId, `${at}.interruptId`),\n status,\n }\n // Omit the key entirely when absent, matching the optional-field shape the\n // schema produced.\n if (value.payload !== undefined) entry.payload = value.payload\n return entry\n}\n\n/**\n * Parse and validate an HTTP request body as an AG-UI `RunAgentInput`.\n *\n * Returns a spread-friendly object whose `messages` field is suitable for\n * passing directly to `chat({ messages })`. The existing\n * `convertMessagesToModelMessages` handles AG-UI fan-out dedup and\n * reasoning/activity/developer-role normalization internally.\n *\n * Validated structurally against the AG-UI `RunAgentInput` contract without a\n * schema library, so this package pulls in no validation runtime of its own.\n *\n * @throws An error with a migration-pointing message when the body does\n * not conform to AG-UI `RunAgentInput`. Surface this as a\n * 400 Bad Request to the client.\n */\nexport async function chatParamsFromRequestBody(body: unknown): Promise<{\n messages: Array<UIMessage | ModelMessage>\n threadId: string\n runId: string\n parentRunId?: string\n tools: Array<{ name: string; description: string; parameters: JSONSchema }>\n forwardedProps: Record<string, unknown>\n state: unknown\n resume?: Array<RunAgentResumeItem>\n /**\n * @deprecated Use `aguiContext` instead. This alias will be removed in a\n * future release.\n */\n context: Array<AGUIContext>\n aguiContext: Array<AGUIContext>\n}> {\n if (!isRecord(body)) invalidBody('body must be a JSON object')\n\n const threadId = requireString(body.threadId, 'threadId')\n const runId = requireString(body.runId, 'runId')\n const parentRunId =\n body.parentRunId === undefined\n ? undefined\n : requireString(body.parentRunId, 'parentRunId')\n\n const messages = requireArray(body.messages, 'messages').map(validateMessage)\n const tools = requireArray(body.tools, 'tools').map(validateTool)\n const aguiContext = requireArray(body.context, 'context').map(validateContext)\n const resume =\n body.resume === undefined\n ? undefined\n : requireArray(body.resume, 'resume').map(validateResumeEntry)\n\n if (body.forwardedProps !== undefined && !isRecord(body.forwardedProps)) {\n invalidBody('forwardedProps must be an object')\n }\n\n return {\n // Unknown top-level fields (e.g. a legacy `cursor`) are dropped by\n // construction: only the fields below are copied onto the result.\n messages: messages as Array<UIMessage | ModelMessage>,\n threadId,\n runId,\n parentRunId,\n tools,\n forwardedProps: (body.forwardedProps ?? {}) as Record<string, unknown>,\n state: body.state,\n resume: resume as Array<RunAgentResumeItem> | undefined,\n context: aguiContext,\n aguiContext,\n }\n}\n\n/**\n * Read an HTTP `Request`, parse its JSON body, and validate it as an\n * AG-UI `RunAgentInput` — collapsing the standard `req.json()` +\n * `chatParamsFromRequestBody(...)` pair into a single call.\n *\n * On a malformed body or invalid AG-UI shape, this **throws a\n * `Response`** with status 400 and a migration-pointing message in the\n * body. Frameworks that natively handle thrown `Response` objects\n * (TanStack Start, SolidStart, Remix, React Router 7) will return the\n * 400 to the client automatically, so the handler reduces to:\n *\n * ```ts\n * export async function POST(req: Request) {\n * const params = await chatParamsFromRequest(req)\n * // ...use params\n * }\n * ```\n *\n * In frameworks that do not auto-handle thrown `Response` objects\n * (Next.js Route Handlers, SvelteKit, Hono, raw Node), wrap the call\n * with try/catch and return the caught Response yourself, or use\n * `chatParamsFromRequestBody` directly with your own JSON-parsing.\n *\n * @throws {Response} 400 on malformed JSON or invalid AG-UI shape.\n */\nexport async function chatParamsFromRequest(\n req: Request,\n): Promise<Awaited<ReturnType<typeof chatParamsFromRequestBody>>> {\n let body: unknown\n try {\n body = await req.json()\n } catch (cause) {\n // Preserve the underlying error on the thrown Response for\n // server-side observability without leaking it to the client.\n const res = new Response(\n 'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',\n { status: 400 },\n )\n ;(res as { cause?: unknown }).cause = cause\n throw res\n }\n try {\n return await chatParamsFromRequestBody(body)\n } catch (cause) {\n // Generic public message — avoid echoing Zod paths (which can contain\n // user payload fragments) or internal validator strings to the client.\n // The original AGUIError is attached as `cause` so server logs can\n // surface it without exposing it to remote callers.\n const res = new Response(\n 'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',\n { status: 400 },\n )\n ;(res as { cause?: unknown }).cause = cause\n throw res\n }\n}\n\n/**\n * Client-declared tool stub (no execute). `name` is `string`, so arrays that\n * include these stubs intentionally widen `TypedStreamChunk` tool-name\n * discrimination — pass server tools alone when you need a closed name union.\n */\nexport type ClientToolDeclaration = {\n name: string\n description: string\n inputSchema: JSONSchema\n}\n\nexport type MergedAgentTools<TServerTools extends ReadonlyArray<AnyTool>> =\n ReadonlyArray<TServerTools[number] | ClientToolDeclaration>\n\n/**\n * Merge a server-side tool array with the AG-UI client-declared tools\n * received in the request body.\n *\n * Rules:\n * - Server tools win on name collision. The client's declaration is\n * ignored if the server already has a tool with that name. The client's\n * UI-side handler still fires when the streamed tool-result event comes\n * through (see `chat-client.ts` `onToolCall`), giving the\n * \"after server execution the client also handles\" semantic for free.\n * - Client-only tools (name not in `serverTools`) become no-execute\n * entries: the runtime's existing `ClientToolRequest` path handles\n * them — server emits a tool-call request, client executes via its\n * registered handler, client posts back the result.\n *\n * Typing:\n * - Empty `clientTools` preserves the server tuple (closed name union).\n * - Non-empty `clientTools` returns a widened array that honestly includes\n * client stubs, so `TypedStreamChunk` does not claim a closed server-only\n * name union.\n *\n * @param serverTools - The server's tool array (e.g. from\n * `[myToolDef.server(...)]`). Pass directly to `chat({ tools })`.\n * @param clientTools - The `tools` array received from\n * `chatParamsFromRequest(...)` / `chatParamsFromRequestBody(...)`.\n * @returns A merged array suitable for `chat({ tools })`.\n */\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(serverTools: TServerTools, clientTools: readonly []): TServerTools\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(\n serverTools: TServerTools,\n clientTools: ReadonlyArray<{\n name: string\n description: string\n parameters: JSONSchema\n }>,\n): MergedAgentTools<TServerTools>\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(\n serverTools: TServerTools,\n clientTools: ReadonlyArray<{\n name: string\n description: string\n parameters: JSONSchema\n }>,\n): TServerTools | MergedAgentTools<TServerTools> {\n if (clientTools.length === 0) {\n return serverTools\n }\n const seen = new Set(serverTools.map((t) => t.name))\n const merged: Array<TServerTools[number] | ClientToolDeclaration> = [\n ...serverTools,\n ]\n for (const ct of clientTools) {\n if (seen.has(ct.name)) {\n // Server wins on name collision.\n continue\n }\n seen.add(ct.name)\n merged.push({\n name: ct.name,\n description: ct.description,\n inputSchema: ct.parameters,\n // No `execute` — runtime treats this as a client-side tool and\n // emits ClientToolRequest events.\n })\n }\n return merged\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-params.js","names":[],"sources":["../../../src/utilities/chat-params.ts"],"sourcesContent":["import { AGUIError } from '@ag-ui/core'\nimport type {\n Context as AGUIContext,\n Message as AGUIMessage,\n ResumeEntry as AGUIResumeEntry,\n Role as AGUIRole,\n} from '@ag-ui/core'\nimport type {\n AnyTool,\n JSONSchema,\n ModelMessage,\n RunAgentResumeItem,\n UIMessage,\n} from '../types'\n\nconst KNOWN_PART_TYPES = new Set([\n 'text',\n 'image',\n 'audio',\n 'video',\n 'document',\n 'tool-call',\n 'tool-result',\n 'thinking',\n])\n\nfunction isValidParts(value: unknown): value is Array<{ type: string }> {\n if (!Array.isArray(value)) return false\n for (const p of value) {\n if (!p || typeof p !== 'object') return false\n const type = (p as { type?: unknown }).type\n if (typeof type !== 'string' || !KNOWN_PART_TYPES.has(type)) return false\n }\n return true\n}\n\n/**\n * Keyed by `AGUIRole` so a role added upstream fails to compile here until it\n * is handled, rather than silently falling through as an unknown role.\n */\nconst AGUI_ROLES: Record<AGUIRole, true> = {\n developer: true,\n system: true,\n assistant: true,\n user: true,\n tool: true,\n activity: true,\n reasoning: true,\n}\n\nfunction isAGUIRole(value: unknown): value is AGUIRole {\n return typeof value === 'string' && value in AGUI_ROLES\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * Reject the request body, pointing at the migration guide. Mirrors the\n * message the previous `RunAgentInputSchema.safeParse` failure produced.\n */\nfunction invalidBody(reason: string): never {\n throw new AGUIError(\n `Request body is not a valid AG-UI RunAgentInput. ` +\n `If you're upgrading from a previous @tanstack/ai-client release, ` +\n `see docs/migration/ag-ui-compliance.md. ` +\n `Validation errors: ${reason}`,\n )\n}\n\nfunction requireString(value: unknown, at: string): string {\n if (typeof value !== 'string') invalidBody(`${at} must be a string`)\n return value\n}\n\nfunction requireArray(value: unknown, at: string): Array<unknown> {\n if (!Array.isArray(value)) invalidBody(`${at} must be an array`)\n return value\n}\n\n/**\n * Assert one AG-UI `Message`, discriminating on `role` exactly as the upstream\n * `MessageSchema` discriminated union does. The record view is retained on the\n * asserted type so callers can still inspect non-AG-UI extras like `parts`.\n */\nfunction assertAGUIMessage(\n value: Record<string, unknown>,\n at: string,\n): asserts value is Record<string, unknown> & AGUIMessage {\n requireString(value.id, `${at}.id`)\n\n const role = value.role\n if (!isAGUIRole(role)) {\n invalidBody(\n `${at}.role must be one of ${Object.keys(AGUI_ROLES).join(' | ')}`,\n )\n }\n\n switch (role) {\n case 'assistant':\n // Both optional: a tool-calling turn carries no text content.\n if (value.content !== undefined) {\n requireString(value.content, `${at}.content`)\n }\n if (value.toolCalls !== undefined) {\n requireArray(value.toolCalls, `${at}.toolCalls`)\n }\n break\n case 'user':\n if (typeof value.content !== 'string' && !Array.isArray(value.content)) {\n invalidBody(\n `${at}.content must be a string or an array of content parts`,\n )\n }\n break\n case 'tool':\n requireString(value.content, `${at}.content`)\n requireString(value.toolCallId, `${at}.toolCallId`)\n break\n case 'activity':\n requireString(value.activityType, `${at}.activityType`)\n if (!isRecord(value.content)) {\n invalidBody(`${at}.content must be an object`)\n }\n break\n case 'developer':\n case 'system':\n case 'reasoning':\n requireString(value.content, `${at}.content`)\n break\n }\n}\n\nfunction validateMessage(value: unknown, index: number): AGUIMessage {\n const at = `messages[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n assertAGUIMessage(value, at)\n\n // `parts` is TanStack's canonical extra, carried through so the UIMessage\n // path inside `chat()` can use it. Keep it only when it holds recognized\n // part types — the previous schema-based path dropped `parts` during parse\n // and re-attached it from the raw body behind this same check.\n if ('parts' in value && !isValidParts(value.parts)) {\n const withoutParts = { ...value }\n Reflect.deleteProperty(withoutParts, 'parts')\n return withoutParts\n }\n return value\n}\n\nfunction validateTool(\n value: unknown,\n index: number,\n): { name: string; description: string; parameters: JSONSchema } {\n const at = `tools[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n return {\n name: requireString(value.name, `${at}.name`),\n description: requireString(value.description, `${at}.description`),\n // Upstream `ToolSchema` types this as optional `any`; it reaches the\n // provider as a raw JSON Schema either way.\n parameters: value.parameters as JSONSchema,\n }\n}\n\nfunction validateContext(value: unknown, index: number): AGUIContext {\n const at = `context[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n return {\n description: requireString(value.description, `${at}.description`),\n value: requireString(value.value, `${at}.value`),\n }\n}\n\nfunction validateResumeEntry(value: unknown, index: number): AGUIResumeEntry {\n const at = `resume[${index}]`\n if (!isRecord(value)) invalidBody(`${at} must be an object`)\n const status = value.status\n if (status !== 'resolved' && status !== 'cancelled') {\n invalidBody(`${at}.status must be \"resolved\" or \"cancelled\"`)\n }\n const entry: AGUIResumeEntry = {\n interruptId: requireString(value.interruptId, `${at}.interruptId`),\n status,\n }\n // Omit the key entirely when absent, matching the optional-field shape the\n // schema produced.\n if (value.payload !== undefined) entry.payload = value.payload\n return entry\n}\n\n/**\n * Parse and validate an HTTP request body as an AG-UI `RunAgentInput`.\n *\n * Returns a spread-friendly object whose `messages` field is suitable for\n * passing directly to `chat({ messages })`. The existing\n * `convertMessagesToModelMessages` handles AG-UI fan-out dedup and\n * reasoning/activity/developer-role normalization internally.\n *\n * Validated structurally against the AG-UI `RunAgentInput` contract without a\n * schema library, so this package pulls in no validation runtime of its own.\n *\n * @throws An error with a migration-pointing message when the body does\n * not conform to AG-UI `RunAgentInput`. Surface this as a\n * 400 Bad Request to the client.\n */\nexport async function chatParamsFromRequestBody(body: unknown): Promise<{\n messages: Array<UIMessage | ModelMessage>\n threadId: string\n runId: string\n parentRunId?: string\n tools: Array<{ name: string; description: string; parameters: JSONSchema }>\n forwardedProps: Record<string, unknown>\n state: unknown\n resume?: Array<RunAgentResumeItem>\n /**\n * @deprecated Use `aguiContext` instead. This alias will be removed in a\n * future release.\n */\n context: Array<AGUIContext>\n aguiContext: Array<AGUIContext>\n}> {\n if (!isRecord(body)) invalidBody('body must be a JSON object')\n\n const threadId = requireString(body.threadId, 'threadId')\n const runId = requireString(body.runId, 'runId')\n const parentRunId =\n body.parentRunId === undefined\n ? undefined\n : requireString(body.parentRunId, 'parentRunId')\n\n const messages = requireArray(body.messages, 'messages').map(validateMessage)\n const tools = requireArray(body.tools, 'tools').map(validateTool)\n const aguiContext = requireArray(body.context, 'context').map(validateContext)\n const resume =\n body.resume === undefined\n ? undefined\n : requireArray(body.resume, 'resume').map(validateResumeEntry)\n\n if (body.forwardedProps !== undefined && !isRecord(body.forwardedProps)) {\n invalidBody('forwardedProps must be an object')\n }\n\n return {\n // Unknown top-level fields (e.g. a legacy `cursor`) are dropped by\n // construction: only the fields below are copied onto the result.\n messages: messages as Array<UIMessage | ModelMessage>,\n threadId,\n runId,\n parentRunId,\n tools,\n forwardedProps: (body.forwardedProps ?? {}) as Record<string, unknown>,\n state: body.state,\n resume: resume as Array<RunAgentResumeItem> | undefined,\n context: aguiContext,\n aguiContext,\n }\n}\n\n/**\n * Read an HTTP `Request`, parse its JSON body, and validate it as an\n * AG-UI `RunAgentInput` — collapsing the standard `req.json()` +\n * `chatParamsFromRequestBody(...)` pair into a single call.\n *\n * On a malformed body or invalid AG-UI shape, this **throws a\n * `Response`** with status 400 and a migration-pointing message in the\n * body. Frameworks that natively handle thrown `Response` objects\n * (TanStack Start, SolidStart, Remix, React Router 7) will return the\n * 400 to the client automatically, so the handler reduces to:\n *\n * ```ts\n * export async function POST(req: Request) {\n * const params = await chatParamsFromRequest(req)\n * // ...use params\n * }\n * ```\n *\n * In frameworks that do not auto-handle thrown `Response` objects\n * (Next.js Route Handlers, SvelteKit, Hono, raw Node), wrap the call\n * with try/catch and return the caught Response yourself, or use\n * `chatParamsFromRequestBody` directly with your own JSON-parsing.\n *\n * @throws {Response} 400 on malformed JSON or invalid AG-UI shape.\n */\nexport async function chatParamsFromRequest(\n req: Request,\n): Promise<Awaited<ReturnType<typeof chatParamsFromRequestBody>>> {\n let body: unknown\n try {\n body = await req.json()\n } catch (cause) {\n // Preserve the underlying error on the thrown Response for\n // server-side observability without leaking it to the client.\n const res = new Response(\n 'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',\n { status: 400 },\n )\n ;(res as { cause?: unknown }).cause = cause\n throw res\n }\n try {\n return await chatParamsFromRequestBody(body)\n } catch (cause) {\n // Generic public message — avoid echoing Zod paths (which can contain\n // user payload fragments) or internal validator strings to the client.\n // The original AGUIError is attached as `cause` so server logs can\n // surface it without exposing it to remote callers.\n const res = new Response(\n 'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',\n { status: 400 },\n )\n ;(res as { cause?: unknown }).cause = cause\n throw res\n }\n}\n\n/**\n * Client-declared tool stub (no execute). `name` is `string`, so arrays that\n * include these stubs intentionally widen `TypedStreamChunk` tool-name\n * discrimination — pass server tools alone when you need a closed name union.\n */\nexport type ClientToolDeclaration = {\n name: string\n description: string\n inputSchema: JSONSchema\n}\n\nexport type MergedAgentTools<TServerTools extends ReadonlyArray<AnyTool>> =\n ReadonlyArray<TServerTools[number] | ClientToolDeclaration>\n\n/**\n * Merge a server-side tool array with the AG-UI client-declared tools\n * received in the request body.\n *\n * Rules:\n * - Server tools win on name collision. The client's declaration is\n * ignored if the server already has a tool with that name. The client's\n * UI-side handler still fires when the streamed tool-result event comes\n * through (see `chat-client.ts` `onToolCall`), giving the\n * \"after server execution the client also handles\" semantic for free.\n * - Client-only tools (name not in `serverTools`) become no-execute\n * entries: the runtime's existing `ClientToolRequest` path handles\n * them — server emits a tool-call request, client executes via its\n * registered handler, client posts back the result.\n *\n * Typing:\n * - Empty `clientTools` preserves the server tuple (closed name union).\n * - Non-empty `clientTools` returns a widened array that honestly includes\n * client stubs, so `TypedStreamChunk` does not claim a closed server-only\n * name union.\n *\n * @param serverTools - The server's tool array (e.g. from\n * `[myToolDef.server(...)]`). Pass directly to `chat({ tools })`.\n * @param clientTools - The `tools` array received from\n * `chatParamsFromRequest(...)` / `chatParamsFromRequestBody(...)`.\n * @returns A merged array suitable for `chat({ tools })`.\n */\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(serverTools: TServerTools, clientTools: readonly []): TServerTools\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(\n serverTools: TServerTools,\n clientTools: ReadonlyArray<{\n name: string\n description: string\n parameters: JSONSchema\n }>,\n): MergedAgentTools<TServerTools>\nexport function mergeAgentTools<\n const TServerTools extends ReadonlyArray<AnyTool>,\n>(\n serverTools: TServerTools,\n clientTools: ReadonlyArray<{\n name: string\n description: string\n parameters: JSONSchema\n }>,\n): TServerTools | MergedAgentTools<TServerTools> {\n if (clientTools.length === 0) {\n return serverTools\n }\n const seen = new Set(serverTools.map((t) => t.name))\n const merged: Array<TServerTools[number] | ClientToolDeclaration> = [\n ...serverTools,\n ]\n for (const ct of clientTools) {\n if (seen.has(ct.name)) {\n // Server wins on name collision.\n continue\n }\n seen.add(ct.name)\n merged.push({\n name: ct.name,\n description: ct.description,\n inputSchema: ct.parameters,\n // No `execute` — runtime treats this as a client-side tool and\n // emits ClientToolRequest events.\n })\n }\n return merged\n}\n"],"mappings":";;AAeA,IAAM,mCAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,SAAS,aAAa,OAAkD;CACtE,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,OAAO;CAClC,KAAK,MAAM,KAAK,OAAO;EACrB,IAAI,CAAC,KAAK,OAAO,MAAM,UAAU,OAAO;EACxC,MAAM,OAAQ,EAAyB;EACvC,IAAI,OAAO,SAAS,YAAY,CAAC,iBAAiB,IAAI,IAAI,GAAG,OAAO;CACtE;CACA,OAAO;AACT;;;;;AAMA,IAAM,aAAqC;CACzC,WAAW;CACX,QAAQ;CACR,WAAW;CACX,MAAM;CACN,MAAM;CACN,UAAU;CACV,WAAW;AACb;AAEA,SAAS,WAAW,OAAmC;CACrD,OAAO,OAAO,UAAU,YAAY,SAAS;AAC/C;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;;AAMA,SAAS,YAAY,QAAuB;CAC1C,MAAM,IAAI,UACR,gLAGwB,QAC1B;AACF;AAEA,SAAS,cAAc,OAAgB,IAAoB;CACzD,IAAI,OAAO,UAAU,UAAU,YAAY,GAAG,GAAG,kBAAkB;CACnE,OAAO;AACT;AAEA,SAAS,aAAa,OAAgB,IAA4B;CAChE,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,YAAY,GAAG,GAAG,kBAAkB;CAC/D,OAAO;AACT;;;;;;AAOA,SAAS,kBACP,OACA,IACwD;CACxD,cAAc,MAAM,IAAI,GAAG,GAAG,IAAI;CAElC,MAAM,OAAO,MAAM;CACnB,IAAI,CAAC,WAAW,IAAI,GAClB,YACE,GAAG,GAAG,uBAAuB,OAAO,KAAK,UAAU,CAAC,CAAC,KAAK,KAAK,GACjE;CAGF,QAAQ,MAAR;EACE,KAAK;GAEH,IAAI,MAAM,YAAY,KAAA,GACpB,cAAc,MAAM,SAAS,GAAG,GAAG,SAAS;GAE9C,IAAI,MAAM,cAAc,KAAA,GACtB,aAAa,MAAM,WAAW,GAAG,GAAG,WAAW;GAEjD;EACF,KAAK;GACH,IAAI,OAAO,MAAM,YAAY,YAAY,CAAC,MAAM,QAAQ,MAAM,OAAO,GACnE,YACE,GAAG,GAAG,uDACR;GAEF;EACF,KAAK;GACH,cAAc,MAAM,SAAS,GAAG,GAAG,SAAS;GAC5C,cAAc,MAAM,YAAY,GAAG,GAAG,YAAY;GAClD;EACF,KAAK;GACH,cAAc,MAAM,cAAc,GAAG,GAAG,cAAc;GACtD,IAAI,CAAC,SAAS,MAAM,OAAO,GACzB,YAAY,GAAG,GAAG,2BAA2B;GAE/C;EACF,KAAK;EACL,KAAK;EACL,KAAK,aACH,cAAc,MAAM,SAAS,GAAG,GAAG,SAAS;CAEhD;AACF;AAEA,SAAS,gBAAgB,OAAgB,OAA4B;CACnE,MAAM,KAAK,YAAY,MAAM;CAC7B,IAAI,CAAC,SAAS,KAAK,GAAG,YAAY,GAAG,GAAG,mBAAmB;CAC3D,kBAAkB,OAAO,EAAE;CAM3B,IAAI,WAAW,SAAS,CAAC,aAAa,MAAM,KAAK,GAAG;EAClD,MAAM,eAAe,EAAE,GAAG,MAAM;EAChC,QAAQ,eAAe,cAAc,OAAO;EAC5C,OAAO;CACT;CACA,OAAO;AACT;AAEA,SAAS,aACP,OACA,OAC+D;CAC/D,MAAM,KAAK,SAAS,MAAM;CAC1B,IAAI,CAAC,SAAS,KAAK,GAAG,YAAY,GAAG,GAAG,mBAAmB;CAC3D,OAAO;EACL,MAAM,cAAc,MAAM,MAAM,GAAG,GAAG,MAAM;EAC5C,aAAa,cAAc,MAAM,aAAa,GAAG,GAAG,aAAa;EAGjE,YAAY,MAAM;CACpB;AACF;AAEA,SAAS,gBAAgB,OAAgB,OAA4B;CACnE,MAAM,KAAK,WAAW,MAAM;CAC5B,IAAI,CAAC,SAAS,KAAK,GAAG,YAAY,GAAG,GAAG,mBAAmB;CAC3D,OAAO;EACL,aAAa,cAAc,MAAM,aAAa,GAAG,GAAG,aAAa;EACjE,OAAO,cAAc,MAAM,OAAO,GAAG,GAAG,OAAO;CACjD;AACF;AAEA,SAAS,oBAAoB,OAAgB,OAAgC;CAC3E,MAAM,KAAK,UAAU,MAAM;CAC3B,IAAI,CAAC,SAAS,KAAK,GAAG,YAAY,GAAG,GAAG,mBAAmB;CAC3D,MAAM,SAAS,MAAM;CACrB,IAAI,WAAW,cAAc,WAAW,aACtC,YAAY,GAAG,GAAG,0CAA0C;CAE9D,MAAM,QAAyB;EAC7B,aAAa,cAAc,MAAM,aAAa,GAAG,GAAG,aAAa;EACjE;CACF;CAGA,IAAI,MAAM,YAAY,KAAA,GAAW,MAAM,UAAU,MAAM;CACvD,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,eAAsB,0BAA0B,MAe7C;CACD,IAAI,CAAC,SAAS,IAAI,GAAG,YAAY,4BAA4B;CAE7D,MAAM,WAAW,cAAc,KAAK,UAAU,UAAU;CACxD,MAAM,QAAQ,cAAc,KAAK,OAAO,OAAO;CAC/C,MAAM,cACJ,KAAK,gBAAgB,KAAA,IACjB,KAAA,IACA,cAAc,KAAK,aAAa,aAAa;CAEnD,MAAM,WAAW,aAAa,KAAK,UAAU,UAAU,CAAC,CAAC,IAAI,eAAe;CAC5E,MAAM,QAAQ,aAAa,KAAK,OAAO,OAAO,CAAC,CAAC,IAAI,YAAY;CAChE,MAAM,cAAc,aAAa,KAAK,SAAS,SAAS,CAAC,CAAC,IAAI,eAAe;CAC7E,MAAM,SACJ,KAAK,WAAW,KAAA,IACZ,KAAA,IACA,aAAa,KAAK,QAAQ,QAAQ,CAAC,CAAC,IAAI,mBAAmB;CAEjE,IAAI,KAAK,mBAAmB,KAAA,KAAa,CAAC,SAAS,KAAK,cAAc,GACpE,YAAY,kCAAkC;CAGhD,OAAO;EAGK;EACV;EACA;EACA;EACA;EACA,gBAAiB,KAAK,kBAAkB,CAAC;EACzC,OAAO,KAAK;EACJ;EACR,SAAS;EACT;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,eAAsB,sBACpB,KACgE;CAChE,IAAI;CACJ,IAAI;EACF,OAAO,MAAM,IAAI,KAAK;CACxB,SAAS,OAAO;EAGd,MAAM,MAAM,IAAI,SACd,uEACA,EAAE,QAAQ,IAAI,CAChB;EACC,IAA6B,QAAQ;EACtC,MAAM;CACR;CACA,IAAI;EACF,OAAO,MAAM,0BAA0B,IAAI;CAC7C,SAAS,OAAO;EAKd,MAAM,MAAM,IAAI,SACd,uEACA,EAAE,QAAQ,IAAI,CAChB;EACC,IAA6B,QAAQ;EACtC,MAAM;CACR;AACF;AAwDA,SAAgB,gBAGd,aACA,aAK+C;CAC/C,IAAI,YAAY,WAAW,GACzB,OAAO;CAET,MAAM,OAAO,IAAI,IAAI,YAAY,KAAK,MAAM,EAAE,IAAI,CAAC;CACnD,MAAM,SAA8D,CAClE,GAAG,WACL;CACA,KAAK,MAAM,MAAM,aAAa;EAC5B,IAAI,KAAK,IAAI,GAAG,IAAI,GAElB;EAEF,KAAK,IAAI,GAAG,IAAI;EAChB,OAAO,KAAK;GACV,MAAM,GAAG;GACT,aAAa,GAAG;GAChB,aAAa,GAAG;EAGlB,CAAC;CACH;CACA,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-prompt.js","names":[],"sources":["../../../src/utilities/media-prompt.ts"],"sourcesContent":["import type {\n AudioPart,\n ImagePart,\n MediaInputMetadata,\n MediaPrompt,\n MediaPromptPart,\n TextPart,\n VideoPart,\n} from '../types'\n\n/**\n * A {@link MediaPrompt} decomposed into the views adapters consume.\n *\n * Adapters with native multimodal prompts (Gemini `contents`, OpenRouter\n * chat content parts) consume `parts` to preserve interleaving; named-field\n * providers (fal, OpenAI) consume `text` plus the typed media buckets.\n *\n * Prompt text is **never rewritten**: text parts are concatenated verbatim.\n * Providers that support referencing inputs from the prompt (e.g. fal's\n * `@Image1`, OpenAI's \"image 1\" prose) expect the user to write that syntax\n * themselves — the SDK does not inject or substitute markers.\n */\nexport interface ResolvedMediaPrompt {\n /**\n * Text parts concatenated verbatim (paragraph-separated). Empty string\n * for media-only prompts.\n */\n text: string\n /** The prompt as ordered parts; a string prompt becomes one text part. */\n parts: Array<MediaPromptPart>\n /** Image parts in prompt order. */\n images: Array<ImagePart<MediaInputMetadata>>\n /** Video parts in prompt order. */\n videos: Array<VideoPart<MediaInputMetadata>>\n /** Audio parts in prompt order. */\n audios: Array<AudioPart<MediaInputMetadata>>\n}\n\n/**\n * Decompose a {@link MediaPrompt} into flattened text and per-modality part\n * buckets, preserving prompt order everywhere. This is the single downrev\n * point from the canonical interleaved prompt shape to the named-field\n * request shapes most providers expose.\n */\nexport function resolveMediaPrompt(prompt: MediaPrompt): ResolvedMediaPrompt {\n if (typeof prompt === 'string') {\n const textPart: TextPart = { type: 'text', content: prompt }\n return {\n text: prompt,\n parts: [textPart],\n images: [],\n videos: [],\n audios: [],\n }\n }\n\n const images: Array<ImagePart<MediaInputMetadata>> = []\n const videos: Array<VideoPart<MediaInputMetadata>> = []\n const audios: Array<AudioPart<MediaInputMetadata>> = []\n const textSegments: Array<string> = []\n\n for (const part of prompt) {\n switch (part.type) {\n case 'text':\n if (part.content) textSegments.push(part.content)\n break\n case 'image':\n images.push(part)\n break\n case 'video':\n videos.push(part)\n break\n case 'audio':\n audios.push(part)\n break\n }\n }\n\n return {\n text: textSegments.join('\\n\\n'),\n parts: prompt,\n images,\n videos,\n audios,\n }\n}\n"],"mappings":";;;;;;;AA4CA,SAAgB,mBAAmB,QAA0C;CAC3E,IAAI,OAAO,WAAW,UAEpB,OAAO;EACL,MAAM;EACN,OAAO,CAAC;GAHmB,MAAM;GAAQ,SAAS;EAG1C,CAAQ;EAChB,QAAQ,CAAC;EACT,QAAQ,CAAC;EACT,QAAQ,CAAC;CACX;CAGF,MAAM,SAA+C,CAAC;CACtD,MAAM,SAA+C,CAAC;CACtD,MAAM,SAA+C,CAAC;CACtD,MAAM,eAA8B,CAAC;CAErC,KAAK,MAAM,QAAQ,QACjB,QAAQ,KAAK,MAAb;EACE,KAAK;GACH,IAAI,KAAK,SAAS,aAAa,KAAK,KAAK,OAAO;GAChD;EACF,KAAK;GACH,OAAO,KAAK,IAAI;GAChB;EACF,KAAK;GACH,OAAO,KAAK,IAAI;GAChB;EACF,KAAK
|
|
1
|
+
{"version":3,"file":"media-prompt.js","names":[],"sources":["../../../src/utilities/media-prompt.ts"],"sourcesContent":["import type {\n AudioPart,\n ImagePart,\n MediaInputMetadata,\n MediaPrompt,\n MediaPromptPart,\n TextPart,\n VideoPart,\n} from '../types'\n\n/**\n * A {@link MediaPrompt} decomposed into the views adapters consume.\n *\n * Adapters with native multimodal prompts (Gemini `contents`, OpenRouter\n * chat content parts) consume `parts` to preserve interleaving; named-field\n * providers (fal, OpenAI) consume `text` plus the typed media buckets.\n *\n * Prompt text is **never rewritten**: text parts are concatenated verbatim.\n * Providers that support referencing inputs from the prompt (e.g. fal's\n * `@Image1`, OpenAI's \"image 1\" prose) expect the user to write that syntax\n * themselves — the SDK does not inject or substitute markers.\n */\nexport interface ResolvedMediaPrompt {\n /**\n * Text parts concatenated verbatim (paragraph-separated). Empty string\n * for media-only prompts.\n */\n text: string\n /** The prompt as ordered parts; a string prompt becomes one text part. */\n parts: Array<MediaPromptPart>\n /** Image parts in prompt order. */\n images: Array<ImagePart<MediaInputMetadata>>\n /** Video parts in prompt order. */\n videos: Array<VideoPart<MediaInputMetadata>>\n /** Audio parts in prompt order. */\n audios: Array<AudioPart<MediaInputMetadata>>\n}\n\n/**\n * Decompose a {@link MediaPrompt} into flattened text and per-modality part\n * buckets, preserving prompt order everywhere. This is the single downrev\n * point from the canonical interleaved prompt shape to the named-field\n * request shapes most providers expose.\n */\nexport function resolveMediaPrompt(prompt: MediaPrompt): ResolvedMediaPrompt {\n if (typeof prompt === 'string') {\n const textPart: TextPart = { type: 'text', content: prompt }\n return {\n text: prompt,\n parts: [textPart],\n images: [],\n videos: [],\n audios: [],\n }\n }\n\n const images: Array<ImagePart<MediaInputMetadata>> = []\n const videos: Array<VideoPart<MediaInputMetadata>> = []\n const audios: Array<AudioPart<MediaInputMetadata>> = []\n const textSegments: Array<string> = []\n\n for (const part of prompt) {\n switch (part.type) {\n case 'text':\n if (part.content) textSegments.push(part.content)\n break\n case 'image':\n images.push(part)\n break\n case 'video':\n videos.push(part)\n break\n case 'audio':\n audios.push(part)\n break\n }\n }\n\n return {\n text: textSegments.join('\\n\\n'),\n parts: prompt,\n images,\n videos,\n audios,\n }\n}\n"],"mappings":";;;;;;;AA4CA,SAAgB,mBAAmB,QAA0C;CAC3E,IAAI,OAAO,WAAW,UAEpB,OAAO;EACL,MAAM;EACN,OAAO,CAAC;GAHmB,MAAM;GAAQ,SAAS;EAG1C,CAAQ;EAChB,QAAQ,CAAC;EACT,QAAQ,CAAC;EACT,QAAQ,CAAC;CACX;CAGF,MAAM,SAA+C,CAAC;CACtD,MAAM,SAA+C,CAAC;CACtD,MAAM,SAA+C,CAAC;CACtD,MAAM,eAA8B,CAAC;CAErC,KAAK,MAAM,QAAQ,QACjB,QAAQ,KAAK,MAAb;EACE,KAAK;GACH,IAAI,KAAK,SAAS,aAAa,KAAK,KAAK,OAAO;GAChD;EACF,KAAK;GACH,OAAO,KAAK,IAAI;GAChB;EACF,KAAK;GACH,OAAO,KAAK,IAAI;GAChB;EACF,KAAK,SACH,OAAO,KAAK,IAAI;CAEpB;CAGF,OAAO;EACL,MAAM,aAAa,KAAK,MAAM;EAC9B,OAAO;EACP;EACA;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StreamChunk } from '../types.js';
|
|
2
|
+
export declare function structuredOutputStartChunk(args: {
|
|
3
|
+
messageId: string;
|
|
4
|
+
model: string;
|
|
5
|
+
threadId: string;
|
|
6
|
+
runId: string;
|
|
7
|
+
timestamp?: number;
|
|
8
|
+
}): StreamChunk;
|
|
9
|
+
export declare function structuredOutputCompleteChunk(args: {
|
|
10
|
+
messageId: string;
|
|
11
|
+
model: string;
|
|
12
|
+
threadId: string;
|
|
13
|
+
runId: string;
|
|
14
|
+
object: unknown;
|
|
15
|
+
raw: string;
|
|
16
|
+
timestamp?: number;
|
|
17
|
+
}): StreamChunk;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EventType } from "../types.js";
|
|
2
|
+
//#region src/utilities/structured-output-events.ts
|
|
3
|
+
function structuredOutputStartChunk(args) {
|
|
4
|
+
return {
|
|
5
|
+
type: EventType.CUSTOM,
|
|
6
|
+
name: "structured-output.start",
|
|
7
|
+
value: { messageId: args.messageId },
|
|
8
|
+
model: args.model,
|
|
9
|
+
timestamp: args.timestamp ?? Date.now(),
|
|
10
|
+
threadId: args.threadId,
|
|
11
|
+
runId: args.runId
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function structuredOutputCompleteChunk(args) {
|
|
15
|
+
return {
|
|
16
|
+
type: EventType.CUSTOM,
|
|
17
|
+
name: "structured-output.complete",
|
|
18
|
+
value: {
|
|
19
|
+
object: args.object,
|
|
20
|
+
raw: args.raw,
|
|
21
|
+
messageId: args.messageId
|
|
22
|
+
},
|
|
23
|
+
model: args.model,
|
|
24
|
+
timestamp: args.timestamp ?? Date.now(),
|
|
25
|
+
threadId: args.threadId,
|
|
26
|
+
runId: args.runId
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { structuredOutputCompleteChunk, structuredOutputStartChunk };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=structured-output-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-output-events.js","names":[],"sources":["../../../src/utilities/structured-output-events.ts"],"sourcesContent":["import { EventType } from '../types'\nimport type { StreamChunk } from '../types'\n\nexport function structuredOutputStartChunk(args: {\n messageId: string\n model: string\n threadId: string\n runId: string\n timestamp?: number\n}): StreamChunk {\n return {\n type: EventType.CUSTOM,\n name: 'structured-output.start',\n value: { messageId: args.messageId },\n model: args.model,\n timestamp: args.timestamp ?? Date.now(),\n threadId: args.threadId,\n runId: args.runId,\n }\n}\n\nexport function structuredOutputCompleteChunk(args: {\n messageId: string\n model: string\n threadId: string\n runId: string\n object: unknown\n raw: string\n timestamp?: number\n}): StreamChunk {\n return {\n type: EventType.CUSTOM,\n name: 'structured-output.complete',\n value: {\n object: args.object,\n raw: args.raw,\n messageId: args.messageId,\n },\n model: args.model,\n timestamp: args.timestamp ?? Date.now(),\n threadId: args.threadId,\n runId: args.runId,\n }\n}\n"],"mappings":";;AAGA,SAAgB,2BAA2B,MAM3B;CACd,OAAO;EACL,MAAM,UAAU;EAChB,MAAM;EACN,OAAO,EAAE,WAAW,KAAK,UAAU;EACnC,OAAO,KAAK;EACZ,WAAW,KAAK,aAAa,KAAK,IAAI;EACtC,UAAU,KAAK;EACf,OAAO,KAAK;CACd;AACF;AAEA,SAAgB,8BAA8B,MAQ9B;CACd,OAAO;EACL,MAAM,UAAU;EAChB,MAAM;EACN,OAAO;GACL,QAAQ,KAAK;GACb,KAAK,KAAK;GACV,WAAW,KAAK;EAClB;EACA,OAAO,KAAK;EACZ,WAAW,KAAK,aAAa,KAAK,IAAI;EACtC,UAAU,KAAK;EACf,OAAO,KAAK;CACd;AACF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse JSON from a model/harness assistant string.
|
|
3
|
+
* Strips a wrapping markdown fence when the whole payload is fenced.
|
|
4
|
+
* If the model wrote prose first, take the last JSON object or array.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseJsonFromAssistantText(raw: string): unknown;
|
|
7
|
+
export declare function appendOutputSchemaInstruction(prompt: string, schema: unknown): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/utilities/structured-output-text.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parse JSON from a model/harness assistant string.
|
|
4
|
+
* Strips a wrapping markdown fence when the whole payload is fenced.
|
|
5
|
+
* If the model wrote prose first, take the last JSON object or array.
|
|
6
|
+
*/
|
|
7
|
+
function parseJsonFromAssistantText(raw) {
|
|
8
|
+
const trimmed = raw.trim();
|
|
9
|
+
if (trimmed === "") throw new SyntaxError("Assistant text is empty");
|
|
10
|
+
const candidates = [];
|
|
11
|
+
const wholeFence = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
|
|
12
|
+
if (wholeFence?.[1]) candidates.push(wholeFence[1].trim());
|
|
13
|
+
candidates.push(trimmed);
|
|
14
|
+
const lastFence = [...trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)\s*```/g)].at(-1);
|
|
15
|
+
if (lastFence?.[1]) candidates.push(lastFence[1].trim());
|
|
16
|
+
const extracted = extractLastJsonSlice(trimmed);
|
|
17
|
+
if (extracted !== void 0) candidates.push(extracted);
|
|
18
|
+
let lastError;
|
|
19
|
+
for (const candidate of candidates) try {
|
|
20
|
+
return JSON.parse(candidate);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
lastError = error;
|
|
23
|
+
}
|
|
24
|
+
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new SyntaxError("No JSON object found in assistant text");
|
|
25
|
+
}
|
|
26
|
+
function extractLastJsonSlice(text) {
|
|
27
|
+
for (let end = text.length - 1; end >= 0; end--) {
|
|
28
|
+
if (text[end] !== "}" && text[end] !== "]") continue;
|
|
29
|
+
for (let start = end; start >= 0; start--) {
|
|
30
|
+
const opener = text[start];
|
|
31
|
+
if (opener !== "{" && opener !== "[") continue;
|
|
32
|
+
const slice = text.slice(start, end + 1);
|
|
33
|
+
try {
|
|
34
|
+
JSON.parse(slice);
|
|
35
|
+
return slice;
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function appendOutputSchemaInstruction(prompt, schema) {
|
|
41
|
+
return `${prompt}
|
|
42
|
+
|
|
43
|
+
Respond with a single JSON object that matches this JSON Schema. Do not wrap the object in markdown unless you must.
|
|
44
|
+
|
|
45
|
+
${JSON.stringify(schema)}`;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { appendOutputSchemaInstruction, parseJsonFromAssistantText };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=structured-output-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-output-text.js","names":[],"sources":["../../../src/utilities/structured-output-text.ts"],"sourcesContent":["/**\n * Parse JSON from a model/harness assistant string.\n * Strips a wrapping markdown fence when the whole payload is fenced.\n * If the model wrote prose first, take the last JSON object or array.\n */\nexport function parseJsonFromAssistantText(raw: string): unknown {\n const trimmed = raw.trim()\n if (trimmed === '') {\n throw new SyntaxError('Assistant text is empty')\n }\n\n const candidates: Array<string> = []\n const wholeFence = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/)\n if (wholeFence?.[1]) candidates.push(wholeFence[1].trim())\n candidates.push(trimmed)\n const lastFence = [\n ...trimmed.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g),\n ].at(-1)\n if (lastFence?.[1]) candidates.push(lastFence[1].trim())\n const extracted = extractLastJsonSlice(trimmed)\n if (extracted !== undefined) candidates.push(extracted)\n\n let lastError: unknown\n for (const candidate of candidates) {\n try {\n return JSON.parse(candidate)\n } catch (error) {\n lastError = error\n }\n }\n throw lastError instanceof Error\n ? lastError\n : new SyntaxError('No JSON object found in assistant text')\n}\n\nfunction extractLastJsonSlice(text: string): string | undefined {\n for (let end = text.length - 1; end >= 0; end--) {\n if (text[end] !== '}' && text[end] !== ']') continue\n for (let start = end; start >= 0; start--) {\n const opener = text[start]\n if (opener !== '{' && opener !== '[') continue\n const slice = text.slice(start, end + 1)\n try {\n JSON.parse(slice)\n return slice\n } catch {\n // Try an earlier opener, then an earlier closer.\n }\n }\n }\n return undefined\n}\n\nexport function appendOutputSchemaInstruction(\n prompt: string,\n schema: unknown,\n): string {\n return `${prompt}\n\nRespond with a single JSON object that matches this JSON Schema. Do not wrap the object in markdown unless you must.\n\n${JSON.stringify(schema)}`\n}\n"],"mappings":";;;;;;AAKA,SAAgB,2BAA2B,KAAsB;CAC/D,MAAM,UAAU,IAAI,KAAK;CACzB,IAAI,YAAY,IACd,MAAM,IAAI,YAAY,yBAAyB;CAGjD,MAAM,aAA4B,CAAC;CACnC,MAAM,aAAa,QAAQ,MAAM,mCAAmC;CACpE,IAAI,aAAa,IAAI,WAAW,KAAK,WAAW,EAAE,CAAC,KAAK,CAAC;CACzD,WAAW,KAAK,OAAO;CACvB,MAAM,YAAY,CAChB,GAAG,QAAQ,SAAS,kCAAkC,CACxD,CAAC,CAAC,GAAG,EAAE;CACP,IAAI,YAAY,IAAI,WAAW,KAAK,UAAU,EAAE,CAAC,KAAK,CAAC;CACvD,MAAM,YAAY,qBAAqB,OAAO;CAC9C,IAAI,cAAc,KAAA,GAAW,WAAW,KAAK,SAAS;CAEtD,IAAI;CACJ,KAAK,MAAM,aAAa,YACtB,IAAI;EACF,OAAO,KAAK,MAAM,SAAS;CAC7B,SAAS,OAAO;EACd,YAAY;CACd;CAEF,MAAM,qBAAqB,QACvB,4BACA,IAAI,YAAY,wCAAwC;AAC9D;AAEA,SAAS,qBAAqB,MAAkC;CAC9D,KAAK,IAAI,MAAM,KAAK,SAAS,GAAG,OAAO,GAAG,OAAO;EAC/C,IAAI,KAAK,SAAS,OAAO,KAAK,SAAS,KAAK;EAC5C,KAAK,IAAI,QAAQ,KAAK,SAAS,GAAG,SAAS;GACzC,MAAM,SAAS,KAAK;GACpB,IAAI,WAAW,OAAO,WAAW,KAAK;GACtC,MAAM,QAAQ,KAAK,MAAM,OAAO,MAAM,CAAC;GACvC,IAAI;IACF,KAAK,MAAM,KAAK;IAChB,OAAO;GACT,QAAQ,CAER;EACF;CACF;AAEF;AAEA,SAAgB,8BACd,QACA,QACQ;CACR,OAAO,GAAG,OAAO;;;;EAIjB,KAAK,UAAU,MAAM;AACvB"}
|