@tanstack/ai 0.15.0 → 0.17.0

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.
Files changed (54) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +20 -3
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.d.ts +16 -6
  4. package/dist/esm/activities/chat/index.js +235 -9
  5. package/dist/esm/activities/chat/index.js.map +1 -1
  6. package/dist/esm/activities/chat/messages.js +4 -2
  7. package/dist/esm/activities/chat/messages.js.map +1 -1
  8. package/dist/esm/activities/chat/stream/message-updaters.d.ts +1 -0
  9. package/dist/esm/activities/chat/stream/message-updaters.js +3 -1
  10. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  11. package/dist/esm/activities/chat/stream/processor.js +12 -4
  12. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  13. package/dist/esm/activities/chat/stream/types.d.ts +5 -0
  14. package/dist/esm/activities/chat/tools/tool-calls.js +1 -3
  15. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  16. package/dist/esm/activities/error-payload.d.ts +0 -8
  17. package/dist/esm/activities/error-payload.js +20 -2
  18. package/dist/esm/activities/error-payload.js.map +1 -1
  19. package/dist/esm/activities/generateImage/adapter.d.ts +2 -2
  20. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  21. package/dist/esm/activities/generateVideo/adapter.d.ts +2 -2
  22. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  23. package/dist/esm/activities/index.d.ts +1 -0
  24. package/dist/esm/activities/index.js +2 -0
  25. package/dist/esm/activities/index.js.map +1 -1
  26. package/dist/esm/activities/stream-generation-result.js +0 -2
  27. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  28. package/dist/esm/activities/summarize/adapter.d.ts +4 -4
  29. package/dist/esm/activities/summarize/adapter.js.map +1 -1
  30. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +45 -0
  31. package/dist/esm/activities/summarize/chat-stream-summarize.js +148 -0
  32. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -0
  33. package/dist/esm/activities/summarize/index.d.ts +1 -0
  34. package/dist/esm/activities/summarize/index.js +4 -2
  35. package/dist/esm/activities/summarize/index.js.map +1 -1
  36. package/dist/esm/types.d.ts +109 -10
  37. package/package.json +2 -2
  38. package/skills/ai-core/structured-outputs/SKILL.md +92 -1
  39. package/src/activities/chat/adapter.ts +25 -2
  40. package/src/activities/chat/index.ts +368 -26
  41. package/src/activities/chat/messages.ts +6 -0
  42. package/src/activities/chat/stream/message-updaters.ts +8 -0
  43. package/src/activities/chat/stream/processor.ts +12 -0
  44. package/src/activities/chat/stream/types.ts +5 -0
  45. package/src/activities/chat/tools/tool-calls.ts +1 -3
  46. package/src/activities/error-payload.ts +31 -2
  47. package/src/activities/generateImage/adapter.ts +8 -2
  48. package/src/activities/generateVideo/adapter.ts +8 -2
  49. package/src/activities/index.ts +5 -0
  50. package/src/activities/stream-generation-result.ts +4 -6
  51. package/src/activities/summarize/adapter.ts +8 -4
  52. package/src/activities/summarize/chat-stream-summarize.ts +238 -0
  53. package/src/activities/summarize/index.ts +12 -9
  54. package/src/types.ts +122 -10
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-stream-summarize.js","sources":["../../../../src/activities/summarize/chat-stream-summarize.ts"],"sourcesContent":["import { EventType } from '@ag-ui/core'\nimport { toRunErrorPayload } from '../error-payload'\nimport { BaseSummarizeAdapter } from './adapter'\nimport type {\n StreamChunk,\n SummarizationOptions,\n SummarizationResult,\n TextOptions,\n} from '../../types'\n\n/**\n * Minimal contract for a text adapter that supports `chatStream`. Lets\n * `ChatStreamSummarizeAdapter` work with any text adapter without coupling\n * to a specific implementation.\n *\n * The provider-options shape is intentionally `any` here — the wrapper only\n * forwards `modelOptions` straight through, so a text adapter with a richer\n * per-model options type (e.g. `ResolveProviderOptions<TModel>`) is still\n * acceptable. Summarize-level type safety is enforced via\n * `SummarizationOptions<TProviderOptions>` on the wrapper itself.\n */\nexport interface ChatStreamCapable {\n chatStream: (options: TextOptions<any>) => AsyncIterable<StreamChunk>\n}\n\n/**\n * Extract the per-model `modelOptions` type a text adapter accepts. Used by\n * provider summarize factories so their `modelOptions` IntelliSense matches\n * what the underlying text adapter actually understands.\n */\nexport type InferTextProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P }\n}\n ? P extends object\n ? P\n : object\n : object\n\n/**\n * Summarize adapter that wraps any `ChatStreamCapable` text adapter and\n * prompts it for summarization. Not tied to any wire format.\n */\nexport class ChatStreamSummarizeAdapter<\n TModel extends string,\n TProviderOptions extends object = Record<string, unknown>,\n> extends BaseSummarizeAdapter<TModel, TProviderOptions> {\n readonly name: string\n\n private textAdapter: ChatStreamCapable\n\n constructor(\n textAdapter: ChatStreamCapable,\n model: TModel,\n name: string = 'chat-stream-summarize',\n ) {\n super({}, model)\n this.name = name\n this.textAdapter = textAdapter\n }\n\n async summarize(\n options: SummarizationOptions<TProviderOptions>,\n ): Promise<SummarizationResult> {\n const systemPrompt = this.buildSummarizationPrompt(options)\n\n let summary = ''\n const id = this.generateId()\n let model = options.model\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }\n\n options.logger.request(\n `activity=summarize provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,\n { provider: this.name, model: options.model },\n )\n\n try {\n for await (const chunk of this.textAdapter.chatStream(\n this.buildTextOptions(options, systemPrompt),\n )) {\n if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n if (chunk.content) {\n summary = chunk.content\n } else if (chunk.delta) {\n // Append delta only when present — a content-less chunk with no\n // delta would otherwise concat literal `'undefined'`.\n summary += chunk.delta\n }\n model = chunk.model || model\n }\n if (chunk.type === 'RUN_FINISHED') {\n if (chunk.usage) {\n usage = chunk.usage\n }\n }\n // Surface failures: the underlying chatStream emits RUN_ERROR instead\n // of throwing, so without this branch summarize() would return an\n // empty summary and pretend a failed run succeeded.\n if (chunk.type === 'RUN_ERROR') {\n const message =\n (chunk.error && typeof chunk.error.message === 'string'\n ? chunk.error.message\n : null) ?? 'Summarization failed'\n const code =\n chunk.error && typeof chunk.error.code === 'string'\n ? chunk.error.code\n : undefined\n const err = new Error(message)\n if (code) {\n ;(err as Error & { code?: string }).code = code\n }\n throw err\n }\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n options.logger.errors(`${this.name}.summarize fatal`, {\n error: toRunErrorPayload(error, `${this.name}.summarize failed`),\n source: `${this.name}.summarize`,\n })\n throw error\n }\n\n return { id, model, summary, usage }\n }\n\n async *summarizeStream(\n options: SummarizationOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const systemPrompt = this.buildSummarizationPrompt(options)\n\n options.logger.request(\n `activity=summarizeStream provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,\n { provider: this.name, model: options.model },\n )\n\n const id = this.generateId()\n let summary = ''\n let model = options.model\n let usage: SummarizationResult['usage'] = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n }\n\n try {\n for await (const chunk of this.textAdapter.chatStream(\n this.buildTextOptions(options, systemPrompt),\n )) {\n // Accumulate the same way `summarize()` does so consumers see deltas\n // AND the terminal `generation:result` event below carries the same\n // final summary that non-streaming returns.\n if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n if (chunk.content) {\n summary = chunk.content\n } else if (chunk.delta) {\n summary += chunk.delta\n }\n if (chunk.model) model = chunk.model\n }\n\n // Emit the GenerationClient-shaped result event just before the\n // terminal RUN_FINISHED so subscribers (useSummarize) populate\n // `result` before flipping `status` to success.\n if (chunk.type === 'RUN_FINISHED') {\n if (chunk.usage) usage = chunk.usage\n if (chunk.model) model = chunk.model\n yield {\n type: EventType.CUSTOM,\n name: 'generation:result',\n value: { id, model, summary, usage } satisfies SummarizationResult,\n model,\n timestamp: Date.now(),\n }\n }\n\n yield chunk\n }\n } catch (error: unknown) {\n options.logger.errors(`${this.name}.summarizeStream fatal`, {\n error: toRunErrorPayload(error, `${this.name}.summarizeStream failed`),\n source: `${this.name}.summarizeStream`,\n })\n throw error\n }\n }\n\n /**\n * Build the TextOptions passed to the underlying chatStream. Provider\n * `modelOptions` from the summarize call are forwarded as-is so knobs like\n * Anthropic cache headers, Gemini safety settings, or Ollama tuning params\n * still reach the wire layer.\n */\n protected buildTextOptions(\n options: SummarizationOptions<TProviderOptions>,\n systemPrompt: string,\n ): TextOptions<TProviderOptions> {\n return {\n model: options.model,\n messages: [{ role: 'user', content: options.text }],\n systemPrompts: [systemPrompt],\n maxTokens: options.maxLength,\n temperature: 0.3,\n modelOptions: options.modelOptions,\n logger: options.logger,\n }\n }\n\n protected buildSummarizationPrompt(\n options: SummarizationOptions<TProviderOptions>,\n ): string {\n let prompt = 'You are a professional summarizer. '\n\n switch (options.style) {\n case 'bullet-points':\n prompt += 'Provide a summary in bullet point format. '\n break\n case 'paragraph':\n prompt += 'Provide a summary in paragraph format. '\n break\n case 'concise':\n prompt += 'Provide a very concise summary in 1-2 sentences. '\n break\n default:\n prompt += 'Provide a clear and concise summary. '\n }\n\n if (options.focus && options.focus.length > 0) {\n prompt += `Focus on the following aspects: ${options.focus.join(', ')}. `\n }\n\n if (options.maxLength) {\n prompt += `Keep the summary under ${options.maxLength} tokens. `\n }\n\n return prompt\n }\n}\n"],"names":[],"mappings":"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"}
@@ -105,3 +105,4 @@ export declare function summarize<TAdapter extends SummarizeAdapter<string, obje
105
105
  export declare function createSummarizeOptions<TAdapter extends SummarizeAdapter<string, object>, TStream extends boolean = false>(options: SummarizeActivityOptions<TAdapter, TStream>): SummarizeActivityOptions<TAdapter, TStream>;
106
106
  export type { SummarizeAdapter, SummarizeAdapterConfig, AnySummarizeAdapter, } from './adapter.js';
107
107
  export { BaseSummarizeAdapter } from './adapter.js';
108
+ export { ChatStreamSummarizeAdapter, type ChatStreamCapable, type InferTextProviderOptions, } from './chat-stream-summarize.js';
@@ -17,7 +17,7 @@ function summarize(options) {
17
17
  );
18
18
  }
19
19
  async function runSummarize(options) {
20
- const { adapter, text, maxLength, style, focus } = options;
20
+ const { adapter, text, maxLength, style, focus, modelOptions } = options;
21
21
  const model = adapter.model;
22
22
  const requestId = createId("summarize");
23
23
  const inputLength = text.length;
@@ -41,6 +41,7 @@ async function runSummarize(options) {
41
41
  maxLength,
42
42
  style,
43
43
  focus,
44
+ modelOptions,
44
45
  logger
45
46
  };
46
47
  try {
@@ -70,7 +71,7 @@ async function runSummarize(options) {
70
71
  }
71
72
  }
72
73
  async function* runStreamingSummarize(options) {
73
- const { adapter, text, maxLength, style, focus } = options;
74
+ const { adapter, text, maxLength, style, focus, modelOptions } = options;
74
75
  const model = adapter.model;
75
76
  const logger = resolveDebugOption(options.debug);
76
77
  logger.request(`activity=summarize provider=${adapter.name}`, {
@@ -84,6 +85,7 @@ async function* runStreamingSummarize(options) {
84
85
  maxLength,
85
86
  style,
86
87
  focus,
88
+ modelOptions,
87
89
  logger
88
90
  };
89
91
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/activities/summarize/index.ts"],"sourcesContent":["/**\n * Summarize Activity\n *\n * Generates summaries from text input.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { SummarizeAdapter } from './adapter'\nimport type {\n StreamChunk,\n SummarizationOptions,\n SummarizationResult,\n} from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'summarize' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/** Extract provider options from a SummarizeAdapter via ~types */\nexport type SummarizeProviderOptions<TAdapter> =\n TAdapter extends SummarizeAdapter<any, any>\n ? TAdapter['~types']['providerOptions']\n : object\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the summarize activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The summarize adapter type\n * @template TStream - Whether to stream the output\n */\nexport interface SummarizeActivityOptions<\n TAdapter extends SummarizeAdapter<string, object>,\n TStream extends boolean = false,\n> {\n /** The summarize adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** The text to summarize */\n text: string\n /** Maximum length of the summary (in words or characters, provider-dependent) */\n maxLength?: number\n /** Style of summary to generate */\n style?: 'bullet-points' | 'paragraph' | 'concise'\n /** Topics or aspects to focus on in the summary */\n focus?: Array<string>\n /** Provider-specific options */\n modelOptions?: SummarizeProviderOptions<TAdapter>\n /**\n * Whether to stream the summarization result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming output.\n * When false or not provided, returns a Promise<SummarizationResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n}\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the summarize activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<SummarizationResult>\n */\nexport type SummarizeActivityResult<TStream extends boolean> =\n TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<SummarizationResult>\n\n// ===========================\n// Helper Functions\n// ===========================\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Summarize activity - generates summaries from text.\n *\n * Supports both streaming and non-streaming modes.\n *\n * @example Basic summarization\n * ```ts\n * import { summarize } from '@tanstack/ai'\n * import { openaiSummarize } from '@tanstack/ai-openai'\n *\n * const result = await summarize({\n * adapter: openaiSummarize('gpt-4o-mini'),\n * text: 'Long article text here...'\n * })\n *\n * console.log(result.summary)\n * ```\n *\n * @example Summarization with style\n * ```ts\n * const result = await summarize({\n * adapter: openaiSummarize('gpt-4o-mini'),\n * text: 'Long article text here...',\n * style: 'bullet-points',\n * maxLength: 100\n * })\n * ```\n *\n * @example Focused summarization\n * ```ts\n * const result = await summarize({\n * adapter: openaiSummarize('gpt-4o-mini'),\n * text: 'Long technical document...',\n * focus: ['key findings', 'methodology']\n * })\n * ```\n *\n * @example Streaming summarization\n * ```ts\n * for await (const chunk of summarize({\n * adapter: openaiSummarize('gpt-4o-mini'),\n * text: 'Long article text here...',\n * stream: true\n * })) {\n * if (chunk.type === 'content') {\n * process.stdout.write(chunk.delta)\n * }\n * }\n * ```\n */\nexport function summarize<\n TAdapter extends SummarizeAdapter<string, object>,\n TStream extends boolean = false,\n>(\n options: SummarizeActivityOptions<TAdapter, TStream>,\n): SummarizeActivityResult<TStream> {\n const { stream } = options\n\n if (stream) {\n return runStreamingSummarize(\n options as unknown as SummarizeActivityOptions<\n SummarizeAdapter<string, object>,\n true\n >,\n ) as SummarizeActivityResult<TStream>\n }\n\n return runSummarize(\n options as unknown as SummarizeActivityOptions<\n SummarizeAdapter<string, object>,\n false\n >,\n ) as SummarizeActivityResult<TStream>\n}\n\n/**\n * Run non-streaming summarization\n */\nasync function runSummarize(\n options: SummarizeActivityOptions<SummarizeAdapter<string, object>, false>,\n): Promise<SummarizationResult> {\n const { adapter, text, maxLength, style, focus } = options\n const model = adapter.model\n const requestId = createId('summarize')\n const inputLength = text.length\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n\n aiEventClient.emit('summarize:request:started', {\n requestId,\n provider: adapter.name,\n model,\n inputLength,\n timestamp: startTime,\n })\n\n logger.request(`activity=summarize provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n inputLength,\n })\n\n const summarizeOptions: SummarizationOptions = {\n model,\n text,\n maxLength,\n style,\n focus,\n logger,\n }\n\n try {\n const result = await adapter.summarize(summarizeOptions)\n\n const duration = Date.now() - startTime\n const outputLength = result.summary.length\n\n aiEventClient.emit('summarize:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n inputLength,\n outputLength,\n duration,\n timestamp: Date.now(),\n })\n\n logger.output(`activity=summarize length=${outputLength}`, {\n hasSummary: !!result.summary,\n outputLength,\n })\n\n return result\n } catch (error) {\n logger.errors('summarize activity failed', {\n error,\n source: 'summarize',\n })\n throw error\n }\n}\n\n/**\n * Run streaming summarization\n * Uses the adapter's native streaming if available, otherwise falls back\n * to non-streaming and yields the result as a single chunk.\n */\nasync function* runStreamingSummarize(\n options: SummarizeActivityOptions<SummarizeAdapter<string, object>, true>,\n): AsyncIterable<StreamChunk> {\n const { adapter, text, maxLength, style, focus } = options\n const model = adapter.model\n const logger: InternalLogger = resolveDebugOption(options.debug)\n\n logger.request(`activity=summarize provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n stream: true,\n })\n\n const summarizeOptions: SummarizationOptions = {\n model,\n text,\n maxLength,\n style,\n focus,\n logger,\n }\n\n try {\n // Use real streaming if the adapter supports it\n if (adapter.summarizeStream) {\n yield* adapter.summarizeStream(summarizeOptions)\n return\n }\n\n // Fall back to non-streaming — wrap result with streamGenerationResult\n yield* streamGenerationResult(() => adapter.summarize(summarizeOptions))\n } catch (error) {\n logger.errors('summarize activity failed', {\n error,\n source: 'summarize',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the summarize() function without executing.\n */\nexport function createSummarizeOptions<\n TAdapter extends SummarizeAdapter<string, object>,\n TStream extends boolean = false,\n>(\n options: SummarizeActivityOptions<TAdapter, TStream>,\n): SummarizeActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n SummarizeAdapter,\n SummarizeAdapterConfig,\n AnySummarizeAdapter,\n} from './adapter'\nexport { BaseSummarizeAdapter } from './adapter'\n"],"names":[],"mappings":";;;AAwBO,MAAM,OAAO;AAyEpB,SAAS,SAAS,QAAwB;AACxC,SAAO,GAAG,MAAM,IAAI,KAAK,IAAA,CAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAC1E;AAwDO,SAAS,UAId,SACkC;AAClC,QAAM,EAAE,WAAW;AAEnB,MAAI,QAAQ;AACV,WAAO;AAAA,MACL;AAAA,IAAA;AAAA,EAKJ;AAEA,SAAO;AAAA,IACL;AAAA,EAAA;AAKJ;AAKA,eAAe,aACb,SAC8B;AAC9B,QAAM,EAAE,SAAS,MAAM,WAAW,OAAO,UAAU;AACnD,QAAM,QAAQ,QAAQ;AACtB,QAAM,YAAY,SAAS,WAAW;AACtC,QAAM,cAAc,KAAK;AACzB,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,SAAyB,mBAAmB,QAAQ,KAAK;AAE/D,gBAAc,KAAK,6BAA6B;AAAA,IAC9C;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EAAA,CACZ;AAED,SAAO,QAAQ,+BAA+B,QAAQ,IAAI,IAAI;AAAA,IAC5D,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA;AAAA,EAAA,CACD;AAED,QAAM,mBAAyC;AAAA,IAC7C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAGF,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,UAAU,gBAAgB;AAEvD,UAAM,WAAW,KAAK,IAAA,IAAQ;AAC9B,UAAM,eAAe,OAAO,QAAQ;AAEpC,kBAAc,KAAK,+BAA+B;AAAA,MAChD;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AAED,WAAO,OAAO,6BAA6B,YAAY,IAAI;AAAA,MACzD,YAAY,CAAC,CAAC,OAAO;AAAA,MACrB;AAAA,IAAA,CACD;AAED,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,OAAO,6BAA6B;AAAA,MACzC;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AACD,UAAM;AAAA,EACR;AACF;AAOA,gBAAgB,sBACd,SAC4B;AAC5B,QAAM,EAAE,SAAS,MAAM,WAAW,OAAO,UAAU;AACnD,QAAM,QAAQ,QAAQ;AACtB,QAAM,SAAyB,mBAAmB,QAAQ,KAAK;AAE/D,SAAO,QAAQ,+BAA+B,QAAQ,IAAI,IAAI;AAAA,IAC5D,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,QAAQ;AAAA,EAAA,CACT;AAED,QAAM,mBAAyC;AAAA,IAC7C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAGF,MAAI;AAEF,QAAI,QAAQ,iBAAiB;AAC3B,aAAO,QAAQ,gBAAgB,gBAAgB;AAC/C;AAAA,IACF;AAGA,WAAO,uBAAuB,MAAM,QAAQ,UAAU,gBAAgB,CAAC;AAAA,EACzE,SAAS,OAAO;AACd,WAAO,OAAO,6BAA6B;AAAA,MACzC;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AACD,UAAM;AAAA,EACR;AACF;AASO,SAAS,uBAId,SAC6C;AAC7C,SAAO;AACT;"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/activities/summarize/index.ts"],"sourcesContent":["/**\n * Summarize Activity\n *\n * Generates summaries from text input.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { SummarizeAdapter } from './adapter'\nimport type { StreamChunk, SummarizationResult } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'summarize' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/** Extract provider options from a SummarizeAdapter via ~types */\nexport type SummarizeProviderOptions<TAdapter> =\n TAdapter extends SummarizeAdapter<any, any>\n ? TAdapter['~types']['providerOptions']\n : object\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the summarize activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The summarize adapter type\n * @template TStream - Whether to stream the output\n */\nexport interface SummarizeActivityOptions<\n TAdapter extends SummarizeAdapter<string, object>,\n TStream extends boolean = false,\n> {\n /** The summarize adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** The text to summarize */\n text: string\n /** Maximum length of the summary (in words or characters, provider-dependent) */\n maxLength?: number\n /** Style of summary to generate */\n style?: 'bullet-points' | 'paragraph' | 'concise'\n /** Topics or aspects to focus on in the summary */\n focus?: Array<string>\n /** Provider-specific options */\n modelOptions?: SummarizeProviderOptions<TAdapter>\n /**\n * Whether to stream the summarization result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming output.\n * When false or not provided, returns a Promise<SummarizationResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n}\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the summarize activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<SummarizationResult>\n */\nexport type SummarizeActivityResult<TStream extends boolean> =\n TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<SummarizationResult>\n\n// ===========================\n// Helper Functions\n// ===========================\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Summarize activity - generates summaries from text.\n *\n * Supports both streaming and non-streaming modes.\n *\n * @example Basic summarization\n * ```ts\n * import { summarize } from '@tanstack/ai'\n * import { openaiSummarize } from '@tanstack/ai-openai'\n *\n * const result = await summarize({\n * adapter: openaiSummarize('gpt-4o-mini'),\n * text: 'Long article text here...'\n * })\n *\n * console.log(result.summary)\n * ```\n *\n * @example Summarization with style\n * ```ts\n * const result = await summarize({\n * adapter: openaiSummarize('gpt-4o-mini'),\n * text: 'Long article text here...',\n * style: 'bullet-points',\n * maxLength: 100\n * })\n * ```\n *\n * @example Focused summarization\n * ```ts\n * const result = await summarize({\n * adapter: openaiSummarize('gpt-4o-mini'),\n * text: 'Long technical document...',\n * focus: ['key findings', 'methodology']\n * })\n * ```\n *\n * @example Streaming summarization\n * ```ts\n * for await (const chunk of summarize({\n * adapter: openaiSummarize('gpt-4o-mini'),\n * text: 'Long article text here...',\n * stream: true\n * })) {\n * if (chunk.type === 'content') {\n * process.stdout.write(chunk.delta)\n * }\n * }\n * ```\n */\nexport function summarize<\n TAdapter extends SummarizeAdapter<string, object>,\n TStream extends boolean = false,\n>(\n options: SummarizeActivityOptions<TAdapter, TStream>,\n): SummarizeActivityResult<TStream> {\n const { stream } = options\n\n if (stream) {\n return runStreamingSummarize(\n options as unknown as SummarizeActivityOptions<\n SummarizeAdapter<string, object>,\n true\n >,\n ) as SummarizeActivityResult<TStream>\n }\n\n return runSummarize(\n options as unknown as SummarizeActivityOptions<\n SummarizeAdapter<string, object>,\n false\n >,\n ) as SummarizeActivityResult<TStream>\n}\n\n/**\n * Run non-streaming summarization\n */\nasync function runSummarize(\n options: SummarizeActivityOptions<SummarizeAdapter<string, object>, false>,\n): Promise<SummarizationResult> {\n const { adapter, text, maxLength, style, focus, modelOptions } = options\n const model = adapter.model\n const requestId = createId('summarize')\n const inputLength = text.length\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n\n aiEventClient.emit('summarize:request:started', {\n requestId,\n provider: adapter.name,\n model,\n inputLength,\n timestamp: startTime,\n })\n\n logger.request(`activity=summarize provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n inputLength,\n })\n\n const summarizeOptions = {\n model,\n text,\n maxLength,\n style,\n focus,\n modelOptions,\n logger,\n }\n\n try {\n const result = await adapter.summarize(summarizeOptions)\n\n const duration = Date.now() - startTime\n const outputLength = result.summary.length\n\n aiEventClient.emit('summarize:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n inputLength,\n outputLength,\n duration,\n timestamp: Date.now(),\n })\n\n logger.output(`activity=summarize length=${outputLength}`, {\n hasSummary: !!result.summary,\n outputLength,\n })\n\n return result\n } catch (error) {\n logger.errors('summarize activity failed', {\n error,\n source: 'summarize',\n })\n throw error\n }\n}\n\n/**\n * Run streaming summarization\n * Uses the adapter's native streaming if available, otherwise falls back\n * to non-streaming and yields the result as a single chunk.\n */\nasync function* runStreamingSummarize(\n options: SummarizeActivityOptions<SummarizeAdapter<string, object>, true>,\n): AsyncIterable<StreamChunk> {\n const { adapter, text, maxLength, style, focus, modelOptions } = options\n const model = adapter.model\n const logger: InternalLogger = resolveDebugOption(options.debug)\n\n logger.request(`activity=summarize provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n stream: true,\n })\n\n const summarizeOptions = {\n model,\n text,\n maxLength,\n style,\n focus,\n modelOptions,\n logger,\n }\n\n try {\n // Use real streaming if the adapter supports it\n if (adapter.summarizeStream) {\n yield* adapter.summarizeStream(summarizeOptions)\n return\n }\n\n // Fall back to non-streaming — wrap result with streamGenerationResult\n yield* streamGenerationResult(() => adapter.summarize(summarizeOptions))\n } catch (error) {\n logger.errors('summarize activity failed', {\n error,\n source: 'summarize',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the summarize() function without executing.\n */\nexport function createSummarizeOptions<\n TAdapter extends SummarizeAdapter<string, object>,\n TStream extends boolean = false,\n>(\n options: SummarizeActivityOptions<TAdapter, TStream>,\n): SummarizeActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n SummarizeAdapter,\n SummarizeAdapterConfig,\n AnySummarizeAdapter,\n} from './adapter'\nexport { BaseSummarizeAdapter } from './adapter'\nexport {\n ChatStreamSummarizeAdapter,\n type ChatStreamCapable,\n type InferTextProviderOptions,\n} from './chat-stream-summarize'\n"],"names":[],"mappings":";;;AAoBO,MAAM,OAAO;AAyEpB,SAAS,SAAS,QAAwB;AACxC,SAAO,GAAG,MAAM,IAAI,KAAK,IAAA,CAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAC1E;AAwDO,SAAS,UAId,SACkC;AAClC,QAAM,EAAE,WAAW;AAEnB,MAAI,QAAQ;AACV,WAAO;AAAA,MACL;AAAA,IAAA;AAAA,EAKJ;AAEA,SAAO;AAAA,IACL;AAAA,EAAA;AAKJ;AAKA,eAAe,aACb,SAC8B;AAC9B,QAAM,EAAE,SAAS,MAAM,WAAW,OAAO,OAAO,iBAAiB;AACjE,QAAM,QAAQ,QAAQ;AACtB,QAAM,YAAY,SAAS,WAAW;AACtC,QAAM,cAAc,KAAK;AACzB,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,SAAyB,mBAAmB,QAAQ,KAAK;AAE/D,gBAAc,KAAK,6BAA6B;AAAA,IAC9C;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EAAA,CACZ;AAED,SAAO,QAAQ,+BAA+B,QAAQ,IAAI,IAAI;AAAA,IAC5D,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA;AAAA,EAAA,CACD;AAED,QAAM,mBAAmB;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAGF,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,UAAU,gBAAgB;AAEvD,UAAM,WAAW,KAAK,IAAA,IAAQ;AAC9B,UAAM,eAAe,OAAO,QAAQ;AAEpC,kBAAc,KAAK,+BAA+B;AAAA,MAChD;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AAED,WAAO,OAAO,6BAA6B,YAAY,IAAI;AAAA,MACzD,YAAY,CAAC,CAAC,OAAO;AAAA,MACrB;AAAA,IAAA,CACD;AAED,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,OAAO,6BAA6B;AAAA,MACzC;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AACD,UAAM;AAAA,EACR;AACF;AAOA,gBAAgB,sBACd,SAC4B;AAC5B,QAAM,EAAE,SAAS,MAAM,WAAW,OAAO,OAAO,iBAAiB;AACjE,QAAM,QAAQ,QAAQ;AACtB,QAAM,SAAyB,mBAAmB,QAAQ,KAAK;AAE/D,SAAO,QAAQ,+BAA+B,QAAQ,IAAI,IAAI;AAAA,IAC5D,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,QAAQ;AAAA,EAAA,CACT;AAED,QAAM,mBAAmB;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAGF,MAAI;AAEF,QAAI,QAAQ,iBAAiB;AAC3B,aAAO,QAAQ,gBAAgB,gBAAgB;AAC/C;AAAA,IACF;AAGA,WAAO,uBAAuB,MAAM,QAAQ,UAAU,gBAAgB,CAAC;AAAA,EACzE,SAAS,OAAO;AACd,WAAO,OAAO,6BAA6B;AAAA,MACzC;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AACD,UAAM;AAAA,EACR;AACF;AASO,SAAS,uBAId,SAC6C;AAC7C,SAAO;AACT;"}
@@ -70,15 +70,17 @@ export type SchemaInput = StandardJSONSchemaV1<any, any> | JSONSchema;
70
70
  * For plain JSONSchema, returns `any` since we can't infer types from JSON Schema at compile time.
71
71
  */
72
72
  export type InferSchemaType<T> = T extends StandardJSONSchemaV1<infer TInput, unknown> ? TInput : unknown;
73
- export interface ToolCall {
73
+ export interface ToolCall<TMetadata = unknown> {
74
74
  id: string;
75
75
  type: 'function';
76
76
  function: {
77
77
  name: string;
78
78
  arguments: string;
79
79
  };
80
- /** Provider-specific metadata to carry through the tool call lifecycle */
81
- providerMetadata?: Record<string, unknown>;
80
+ /** Provider-specific metadata to carry through the tool call lifecycle.
81
+ * Typed per-adapter via `TToolCallMetadata`. For example,
82
+ * `@tanstack/ai-gemini` sets this to `{ thoughtSignature?: string }`. */
83
+ metadata?: TMetadata;
82
84
  }
83
85
  /**
84
86
  * Supported input modality types for multimodal content.
@@ -221,7 +223,7 @@ export interface TextPart<TMetadata = unknown> {
221
223
  content: string;
222
224
  metadata?: TMetadata;
223
225
  }
224
- export interface ToolCallPart {
226
+ export interface ToolCallPart<TMetadata = unknown> {
225
227
  type: 'tool-call';
226
228
  id: string;
227
229
  name: string;
@@ -235,6 +237,9 @@ export interface ToolCallPart {
235
237
  };
236
238
  /** Tool execution output (for client tools or after approval) */
237
239
  output?: any;
240
+ /** Provider-specific metadata that round-trips with the tool call.
241
+ * Typed per-adapter via `TToolCallMetadata`. */
242
+ metadata?: TMetadata;
238
243
  }
239
244
  export interface ToolResultPart {
240
245
  type: 'tool-result';
@@ -740,7 +745,7 @@ export interface TextMessageEndEvent extends AGUITextMessageEndEvent {
740
745
  * Emitted when a tool call starts.
741
746
  *
742
747
  * @ag-ui/core provides: `toolCallId`, `toolCallName`, `parentMessageId?`
743
- * TanStack AI adds: `model?`, `toolName` (deprecated alias), `index?`, `providerMetadata?`
748
+ * TanStack AI adds: `model?`, `toolName` (deprecated alias), `index?`, `metadata?`
744
749
  */
745
750
  export interface ToolCallStartEvent extends AGUIToolCallStartEvent {
746
751
  /** Model identifier for multi-model support */
@@ -752,8 +757,11 @@ export interface ToolCallStartEvent extends AGUIToolCallStartEvent {
752
757
  toolName: string;
753
758
  /** Index for parallel tool calls */
754
759
  index?: number;
755
- /** Provider-specific metadata to carry into the ToolCall */
756
- providerMetadata?: Record<string, unknown>;
760
+ /** Provider-specific metadata to carry into the ToolCall.
761
+ * Untyped at the event layer because events flow through a discriminated
762
+ * union that does not survive generics; adapters cast it to their typed
763
+ * `TToolCallMetadata` shape when emitting. */
764
+ metadata?: Record<string, unknown>;
757
765
  }
758
766
  /**
759
767
  * Emitted when tool call arguments are streaming.
@@ -887,6 +895,95 @@ export interface CustomEvent extends AGUICustomEvent {
887
895
  /** Model identifier for multi-model support */
888
896
  model?: string;
889
897
  }
898
+ /**
899
+ * Final event of a streaming structured-output run. Carries the validated
900
+ * `object` (typed as `T` after the orchestrator runs Standard Schema parsing),
901
+ * the `raw` JSON text that produced it, and — for thinking/reasoning models —
902
+ * the accumulated reasoning text. Adapters emit this with `T = unknown`; the
903
+ * chat orchestrator narrows to the schema's inferred type after validation.
904
+ *
905
+ * `reasoning` is `undefined` when the model produced none (most non-thinking
906
+ * models) and when the underlying adapter doesn't expose reasoning streams.
907
+ *
908
+ * `name` is a string literal so consumers can narrow directly:
909
+ *
910
+ * ```ts
911
+ * if (chunk.type === 'CUSTOM' && chunk.name === 'structured-output.complete') {
912
+ * chunk.value.object // typed as T
913
+ * }
914
+ * ```
915
+ */
916
+ export interface StructuredOutputCompleteEvent<T = unknown> extends Omit<CustomEvent, 'name' | 'value'> {
917
+ name: 'structured-output.complete';
918
+ value: {
919
+ object: T;
920
+ raw: string;
921
+ reasoning?: string;
922
+ };
923
+ }
924
+ /**
925
+ * Emitted when a server tool requires approval before execution. The agent
926
+ * loop yields this and pauses — `structured-output.complete` will not fire
927
+ * for that run. The shape is fixed by the orchestrator's tool-approval flow
928
+ * (see `buildApprovalChunks` in `activities/chat/index.ts`).
929
+ */
930
+ export interface ApprovalRequestedEvent extends Omit<CustomEvent, 'name' | 'value'> {
931
+ name: 'approval-requested';
932
+ value: {
933
+ toolCallId: string;
934
+ toolName: string;
935
+ input: unknown;
936
+ approval: {
937
+ id: string;
938
+ needsApproval: true;
939
+ };
940
+ };
941
+ }
942
+ /**
943
+ * Emitted when a client tool is invoked. The agent loop yields this and
944
+ * pauses to let the caller run the tool client-side — `structured-output.complete`
945
+ * will not fire for that run. Shape fixed by `buildClientToolChunks` in
946
+ * `activities/chat/index.ts`.
947
+ */
948
+ export interface ToolInputAvailableEvent extends Omit<CustomEvent, 'name' | 'value'> {
949
+ name: 'tool-input-available';
950
+ value: {
951
+ toolCallId: string;
952
+ toolName: string;
953
+ input: unknown;
954
+ };
955
+ }
956
+ /**
957
+ * Public type for streams returned by `chat({ outputSchema, stream: true })`.
958
+ *
959
+ * Yields all standard `StreamChunk` lifecycle events plus the three tagged
960
+ * `CUSTOM` events the orchestrator can emit through this path:
961
+ * - `structured-output.complete` — terminal event with typed `value.object: T`
962
+ * - `approval-requested` — server tool needs approval (pauses the run)
963
+ * - `tool-input-available` — client tool invocation (pauses the run)
964
+ *
965
+ * Each variant has a literal `name`, so a single discriminated narrow gives
966
+ * you a typed `value` with no helper or cast:
967
+ *
968
+ * ```ts
969
+ * for await (const chunk of stream) {
970
+ * if (chunk.type === 'CUSTOM' && chunk.name === 'structured-output.complete') {
971
+ * chunk.value.object // typed as T
972
+ * } else if (chunk.type === 'CUSTOM' && chunk.name === 'approval-requested') {
973
+ * chunk.value.toolCallId // typed as string
974
+ * }
975
+ * }
976
+ * ```
977
+ *
978
+ * Caveat: tools can emit arbitrary user-defined custom events via the
979
+ * `emitCustomEvent(name, value)` context API. Those flow through this stream
980
+ * at runtime but are intentionally absent from this type — including a bare
981
+ * `CustomEvent` (whose `value: any` would poison the union) would collapse
982
+ * `chunk.value` back to `any` after the narrow. If you rely on
983
+ * `emitCustomEvent` plus `outputSchema + stream: true`, branch on `CUSTOM`
984
+ * outside the literal-`name` narrows or cast explicitly.
985
+ */
986
+ export type StructuredOutputStream<T = unknown> = AsyncIterable<Exclude<StreamChunk, CustomEvent> | StructuredOutputCompleteEvent<T> | ApprovalRequestedEvent | ToolInputAvailableEvent>;
890
987
  /**
891
988
  * Emitted when reasoning starts for a message.
892
989
  *
@@ -968,12 +1065,14 @@ export interface TextCompletionChunk {
968
1065
  totalTokens: number;
969
1066
  };
970
1067
  }
971
- export interface SummarizationOptions {
1068
+ export interface SummarizationOptions<TProviderOptions extends object = Record<string, unknown>> {
972
1069
  model: string;
973
1070
  text: string;
974
1071
  maxLength?: number;
975
1072
  style?: 'bullet-points' | 'paragraph' | 'concise';
976
1073
  focus?: Array<string>;
1074
+ /** Provider-specific options forwarded by the summarize() activity. */
1075
+ modelOptions?: TProviderOptions;
977
1076
  /**
978
1077
  * Internal logger threaded from the summarize() entry point. Adapters must
979
1078
  * call logger.request() before the SDK call and logger.errors() in catch blocks.
@@ -994,7 +1093,7 @@ export interface SummarizationResult {
994
1093
  * Options for image generation.
995
1094
  * These are the common options supported across providers.
996
1095
  */
997
- export interface ImageGenerationOptions<TProviderOptions extends object = object, TSize extends string = string> {
1096
+ export interface ImageGenerationOptions<TProviderOptions extends object = object, TSize extends string | undefined = string> {
998
1097
  /** The model to use for image generation */
999
1098
  model: string;
1000
1099
  /** Text description of the desired image(s) */
@@ -1102,7 +1201,7 @@ export interface AudioGenerationResult {
1102
1201
  *
1103
1202
  * @experimental Video generation is an experimental feature and may change.
1104
1203
  */
1105
- export interface VideoGenerationOptions<TProviderOptions extends object = object, TSize extends string = string> {
1204
+ export interface VideoGenerationOptions<TProviderOptions extends object = object, TSize extends string | undefined = string> {
1106
1205
  /** The model to use for video generation */
1107
1206
  model: string;
1108
1207
  /** Text description of the desired video */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Core TanStack AI library - Open source AI SDK",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@ag-ui/core": "0.0.49",
57
57
  "partial-json": "^0.1.7",
58
- "@tanstack/ai-event-client": "0.2.9"
58
+ "@tanstack/ai-event-client": "0.3.1"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@opentelemetry/api": ">=1.9.0"
@@ -4,7 +4,9 @@ description: >
4
4
  Type-safe JSON schema responses from LLMs using outputSchema on chat().
5
5
  Supports Zod, ArkType, and Valibot schemas. The adapter handles
6
6
  provider-specific strategies transparently — never configure structured
7
- output at the provider level. convertSchemaToJsonSchema() for manual
7
+ output at the provider level. Pass stream:true alongside outputSchema for
8
+ incremental JSON deltas + a terminal validated object via the
9
+ `structured-output.complete` event. convertSchemaToJsonSchema() for manual
8
10
  schema conversion.
9
11
  type: sub-skill
10
12
  library: tanstack-ai
@@ -46,6 +48,8 @@ const stream = chat({
46
48
 
47
49
  When `outputSchema` is provided, `chat()` returns `Promise<InferSchemaType<TSchema>>` instead of `AsyncIterable<StreamChunk>`. The result is fully typed based on the schema.
48
50
 
51
+ Adding `stream: true` switches the return to `StructuredOutputStream<InferSchemaType<TSchema>>` — incremental JSON deltas plus a terminal validated object. See **Pattern 3** below.
52
+
49
53
  ## Core Patterns
50
54
 
51
55
  ### Pattern 1: Basic structured output with Zod
@@ -128,8 +132,94 @@ console.log(company.employees[0].role)
128
132
  console.log(company.financials?.revenue)
129
133
  ```
130
134
 
135
+ ### Pattern 3: Streaming structured output
136
+
137
+ Pass `stream: true` alongside `outputSchema` to receive incremental JSON deltas while the model generates, plus a final validated typed object. Useful for streaming partial UI (progress views, typewriter previews, partially-filled forms).
138
+
139
+ ```typescript
140
+ import { chat } from '@tanstack/ai'
141
+ import { openaiText } from '@tanstack/ai-openai'
142
+ import { z } from 'zod'
143
+
144
+ const PersonSchema = z.object({
145
+ name: z.string(),
146
+ age: z.number(),
147
+ email: z.string().email(),
148
+ })
149
+
150
+ const stream = chat({
151
+ adapter: openaiText('gpt-5.2'),
152
+ messages: [
153
+ { role: 'user', content: 'Extract: John Doe is 30, john@example.com' },
154
+ ],
155
+ outputSchema: PersonSchema,
156
+ stream: true,
157
+ })
158
+
159
+ let raw = ''
160
+ for await (const chunk of stream) {
161
+ if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
162
+ // Partial JSON text — drive progress UI only. Do NOT JSON.parse.
163
+ raw += chunk.delta
164
+ } else if (
165
+ chunk.type === 'CUSTOM' &&
166
+ chunk.name === 'structured-output.complete'
167
+ ) {
168
+ // Terminal event. `chunk.value.object` is fully validated and typed
169
+ // against the schema you passed in — no helper or cast required.
170
+ chunk.value.object.name // string
171
+ chunk.value.object.age // number
172
+ chunk.value.reasoning // string | undefined (thinking models only)
173
+ }
174
+ }
175
+ ```
176
+
177
+ The terminal event is a `CUSTOM` chunk: `{ type: 'CUSTOM', name: 'structured-output.complete', value: { object: T, raw: string, reasoning?: string } }`. The return type of `chat({ outputSchema, stream: true })` carries `T` through to the terminal event, so a plain discriminated narrow (`chunk.type === 'CUSTOM' && chunk.name === 'structured-output.complete'`) is enough — no type guard helper needed.
178
+
179
+ **Adapter coverage for streaming:**
180
+
181
+ | Adapter | `outputSchema` + `stream: true` |
182
+ | ------------------------------------------------- | --------------------------------------------------------------------------------------------- |
183
+ | `@tanstack/ai-openai` | Native single-request stream (Responses API) |
184
+ | `@tanstack/ai-openrouter` | Native single-request stream |
185
+ | `@tanstack/ai-grok` | Native single-request stream (Chat Completions) |
186
+ | `@tanstack/ai-groq` | Native single-request stream (Chat Completions) |
187
+ | All other adapters (anthropic, gemini, ollama, …) | Fallback: runs non-streaming `structuredOutput`, emits one `structured-output.complete` event |
188
+
189
+ The consumer code is identical across providers — always read the final object off `structured-output.complete`. You only see incremental deltas when the adapter implements `structuredOutputStream` natively.
190
+
131
191
  ## Common Mistakes
132
192
 
193
+ ### HIGH: Parsing streaming JSON deltas yourself
194
+
195
+ When using `chat({ outputSchema, stream: true })`, the `TEXT_MESSAGE_CONTENT` chunks contain _partial_ JSON fragments — they are not valid JSON until the stream completes. Always read the validated object from the terminal `structured-output.complete` event. Validation runs once, on the complete payload.
196
+
197
+ ```typescript
198
+ // WRONG -- partial JSON, throws SyntaxError mid-stream, no schema validation
199
+ for await (const chunk of stream) {
200
+ if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
201
+ const obj = JSON.parse(chunk.delta) // ❌ partial, invalid
202
+ }
203
+ }
204
+
205
+ // CORRECT -- accumulate deltas only for UX progress; trust the terminal event
206
+ let raw = ''
207
+ for await (const chunk of stream) {
208
+ if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
209
+ raw += chunk.delta // optional: render a "streaming JSON" preview
210
+ } else if (
211
+ chunk.type === 'CUSTOM' &&
212
+ chunk.name === 'structured-output.complete'
213
+ ) {
214
+ const result = chunk.value.object // ✅ typed and validated
215
+ }
216
+ }
217
+ ```
218
+
219
+ If you need progressive _parsed_ state (e.g. show fields as they arrive), use a partial-JSON parser on the accumulated `raw` string at render time — but do NOT treat the result as schema-validated; only the terminal event is.
220
+
221
+ Source: maintainer interview
222
+
133
223
  ### HIGH: Trying to implement provider-specific structured output strategies
134
224
 
135
225
  The adapter already handles provider differences (OpenAI uses `response_format`, Anthropic uses tool-based extraction, Gemini uses `responseSchema`). Never configure this yourself.
@@ -201,3 +291,4 @@ Source: maintainer interview
201
291
  ## Cross-References
202
292
 
203
293
  - See also: ai-core/adapter-configuration/SKILL.md -- Adapter handles structured output strategy transparently
294
+ - See also: ai-core/chat-experience/SKILL.md -- Consuming `StreamChunk` events on the client (the streaming variant uses the same chunk model plus the terminal `structured-output.complete` custom event)
@@ -54,6 +54,7 @@ export interface StructuredOutputResult<T = unknown> {
54
54
  * - TInputModalities: Supported input modalities for this model (already resolved)
55
55
  * - TMessageMetadata: Metadata types for content parts (already resolved)
56
56
  * - TToolCapabilities: Tuple of tool-kind strings supported by this model, resolved from `supports.tools`
57
+ * - TToolCallMetadata: Metadata type that round-trips with tool calls (e.g. Gemini's `thoughtSignature`)
57
58
  */
58
59
  export interface TextAdapter<
59
60
  TModel extends string,
@@ -61,6 +62,7 @@ export interface TextAdapter<
61
62
  TInputModalities extends ReadonlyArray<Modality>,
62
63
  TMessageMetadataByModality extends DefaultMessageMetadataByModality,
63
64
  TToolCapabilities extends ReadonlyArray<string> = ReadonlyArray<string>,
65
+ TToolCallMetadata = unknown,
64
66
  > {
65
67
  /** Discriminator for adapter kind */
66
68
  readonly kind: 'text'
@@ -77,6 +79,7 @@ export interface TextAdapter<
77
79
  inputModalities: TInputModalities
78
80
  messageMetadataByModality: TMessageMetadataByModality
79
81
  toolCapabilities: TToolCapabilities
82
+ toolCallMetadata: TToolCallMetadata
80
83
  }
81
84
 
82
85
  /**
@@ -97,13 +100,30 @@ export interface TextAdapter<
97
100
  structuredOutput: (
98
101
  options: StructuredOutputOptions<TProviderOptions>,
99
102
  ) => Promise<StructuredOutputResult<unknown>>
103
+
104
+ /**
105
+ * Stream structured output using the provider's native streaming structured
106
+ * output API (stream + response_format json_schema in a single request).
107
+ *
108
+ * Optional — adapters without native streaming JSON omit this method and the
109
+ * activity layer synthesizes a stream around the non-streaming
110
+ * `structuredOutput` call.
111
+ *
112
+ * Implementations must emit standard AG-UI lifecycle events (RUN_STARTED,
113
+ * TEXT_MESSAGE_*, RUN_FINISHED) carrying raw JSON text deltas, plus a final
114
+ * `CUSTOM` event named `structured-output.complete` whose `value` is
115
+ * `{ object, raw, reasoning? }`.
116
+ */
117
+ structuredOutputStream?: (
118
+ options: StructuredOutputOptions<TProviderOptions>,
119
+ ) => AsyncIterable<StreamChunk>
100
120
  }
101
121
 
102
122
  /**
103
123
  * A TextAdapter with any/unknown type parameters.
104
124
  * Useful as a constraint in generic functions and interfaces.
105
125
  */
106
- export type AnyTextAdapter = TextAdapter<any, any, any, any, any>
126
+ export type AnyTextAdapter = TextAdapter<any, any, any, any, any, any>
107
127
 
108
128
  /**
109
129
  * Abstract base class for text adapters.
@@ -117,12 +137,14 @@ export abstract class BaseTextAdapter<
117
137
  TInputModalities extends ReadonlyArray<Modality>,
118
138
  TMessageMetadataByModality extends DefaultMessageMetadataByModality,
119
139
  TToolCapabilities extends ReadonlyArray<string> = ReadonlyArray<string>,
140
+ TToolCallMetadata = unknown,
120
141
  > implements TextAdapter<
121
142
  TModel,
122
143
  TProviderOptions,
123
144
  TInputModalities,
124
145
  TMessageMetadataByModality,
125
- TToolCapabilities
146
+ TToolCapabilities,
147
+ TToolCallMetadata
126
148
  > {
127
149
  readonly kind = 'text' as const
128
150
  abstract readonly name: string
@@ -134,6 +156,7 @@ export abstract class BaseTextAdapter<
134
156
  inputModalities: TInputModalities
135
157
  messageMetadataByModality: TMessageMetadataByModality
136
158
  toolCapabilities: TToolCapabilities
159
+ toolCallMetadata: TToolCallMetadata
137
160
  }
138
161
 
139
162
  protected config: TextAdapterConfig