@tanstack/ai 0.59.0 → 0.63.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 (123) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/activities/chat/adapter.d.ts +9 -0
  3. package/dist/esm/activities/chat/adapter.js +1 -0
  4. package/dist/esm/activities/chat/adapter.js.map +1 -1
  5. package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
  6. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
  7. package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
  8. package/dist/esm/activities/chat/agents/spawn.js +8 -5
  9. package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
  10. package/dist/esm/activities/chat/index.js +289 -78
  11. package/dist/esm/activities/chat/index.js.map +1 -1
  12. package/dist/esm/activities/chat/messages.js +35 -19
  13. package/dist/esm/activities/chat/messages.js.map +1 -1
  14. package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
  15. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  16. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  17. package/dist/esm/activities/chat/stream/message-updaters.js +11 -3
  18. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  19. package/dist/esm/activities/chat/stream/processor.d.ts +11 -10
  20. package/dist/esm/activities/chat/stream/processor.js +51 -28
  21. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  22. package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
  23. package/dist/esm/activities/chat/tools/tool-calls.js +57 -16
  24. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  25. package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
  26. package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
  27. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  28. package/dist/esm/activities/embed/adapter.d.ts +7 -0
  29. package/dist/esm/activities/embed/adapter.js +1 -0
  30. package/dist/esm/activities/embed/adapter.js.map +1 -1
  31. package/dist/esm/activities/embed/index.js +2 -0
  32. package/dist/esm/activities/embed/index.js.map +1 -1
  33. package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
  34. package/dist/esm/activities/evaluate/adapter.js.map +1 -1
  35. package/dist/esm/activities/evaluate/index.d.ts +4 -0
  36. package/dist/esm/activities/evaluate/index.js +3 -1
  37. package/dist/esm/activities/evaluate/index.js.map +1 -1
  38. package/dist/esm/activities/files/adapter.d.ts +97 -0
  39. package/dist/esm/activities/files/adapter.js +45 -0
  40. package/dist/esm/activities/files/adapter.js.map +1 -0
  41. package/dist/esm/activities/files/index.d.ts +66 -0
  42. package/dist/esm/activities/files/index.js +78 -0
  43. package/dist/esm/activities/files/index.js.map +1 -0
  44. package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
  45. package/dist/esm/activities/generateImage/adapter.js +1 -0
  46. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  47. package/dist/esm/activities/generateImage/index.js +2 -0
  48. package/dist/esm/activities/generateImage/index.js.map +1 -1
  49. package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
  50. package/dist/esm/activities/generateVideo/adapter.js +1 -0
  51. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  52. package/dist/esm/activities/generateVideo/index.js +3 -0
  53. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  54. package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
  55. package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
  56. package/dist/esm/activities/generateWorld/index.d.ts +4 -3
  57. package/dist/esm/activities/generateWorld/index.js +5 -4
  58. package/dist/esm/activities/generateWorld/index.js.map +1 -1
  59. package/dist/esm/activities/index.d.ts +6 -3
  60. package/dist/esm/activities/index.js +13 -11
  61. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
  62. package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
  63. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  64. package/dist/esm/client.d.ts +3 -1
  65. package/dist/esm/client.js +2 -1
  66. package/dist/esm/client.js.map +1 -1
  67. package/dist/esm/index.d.ts +4 -3
  68. package/dist/esm/index.js +5 -3
  69. package/dist/esm/interrupt-resume.js +29 -4
  70. package/dist/esm/interrupt-resume.js.map +1 -1
  71. package/dist/esm/middlewares/otel.d.ts +5 -2
  72. package/dist/esm/middlewares/otel.js +114 -0
  73. package/dist/esm/middlewares/otel.js.map +1 -1
  74. package/dist/esm/types.d.ts +114 -14
  75. package/dist/esm/utilities/ag-ui-wire.js +39 -16
  76. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  77. package/dist/esm/utilities/content-source.d.ts +60 -0
  78. package/dist/esm/utilities/content-source.js +85 -0
  79. package/dist/esm/utilities/content-source.js.map +1 -0
  80. package/dist/esm/utilities/provider-executed.d.ts +7 -0
  81. package/dist/esm/utilities/provider-executed.js +10 -1
  82. package/dist/esm/utilities/provider-executed.js.map +1 -1
  83. package/dist/esm/utilities/tool-result.d.ts +14 -3
  84. package/dist/esm/utilities/tool-result.js +26 -3
  85. package/dist/esm/utilities/tool-result.js.map +1 -1
  86. package/package.json +4 -4
  87. package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
  88. package/skills/ai-core/chat-experience/SKILL.md +134 -0
  89. package/skills/ai-core/media-generation/SKILL.md +8 -0
  90. package/skills/ai-core/tool-calling/SKILL.md +103 -0
  91. package/src/activities/chat/adapter.ts +10 -0
  92. package/src/activities/chat/agents/define-agent.ts +20 -3
  93. package/src/activities/chat/agents/spawn.ts +20 -12
  94. package/src/activities/chat/index.ts +458 -99
  95. package/src/activities/chat/messages.ts +52 -6
  96. package/src/activities/chat/middleware/types.ts +1 -0
  97. package/src/activities/chat/stream/message-updaters.ts +27 -2
  98. package/src/activities/chat/stream/processor.ts +81 -49
  99. package/src/activities/chat/tools/tool-calls.ts +104 -9
  100. package/src/activities/chat/tools/tool-definition.ts +8 -0
  101. package/src/activities/embed/adapter.ts +7 -0
  102. package/src/activities/embed/index.ts +5 -0
  103. package/src/activities/evaluate/adapter.ts +4 -0
  104. package/src/activities/evaluate/index.ts +6 -0
  105. package/src/activities/files/adapter.ts +120 -0
  106. package/src/activities/files/index.ts +113 -0
  107. package/src/activities/generateImage/adapter.ts +8 -0
  108. package/src/activities/generateImage/index.ts +4 -0
  109. package/src/activities/generateVideo/adapter.ts +8 -0
  110. package/src/activities/generateVideo/index.ts +7 -0
  111. package/src/activities/generateWorld/adapter.ts +4 -2
  112. package/src/activities/generateWorld/index.ts +7 -6
  113. package/src/activities/index.ts +25 -1
  114. package/src/activities/summarize/chat-stream-summarize.ts +22 -12
  115. package/src/client.ts +8 -0
  116. package/src/index.ts +17 -0
  117. package/src/interrupt-resume.ts +55 -4
  118. package/src/middlewares/otel.ts +161 -3
  119. package/src/types.ts +114 -14
  120. package/src/utilities/ag-ui-wire.ts +72 -17
  121. package/src/utilities/content-source.ts +138 -0
  122. package/src/utilities/provider-executed.ts +13 -0
  123. package/src/utilities/tool-result.ts +45 -3
@@ -299,6 +299,8 @@ import type { UIMessage } from '@tanstack/ai-react'
299
299
 
300
300
  function ImagePart({ part }: { part: UIMessage['parts'][number] }) {
301
301
  if (part.type !== 'image') return null
302
+ // A provider file handle is an opaque id, so the browser cannot load it.
303
+ if (part.source.type === 'file') return null
302
304
  const src =
303
305
  part.source.type === 'url'
304
306
  ? part.source.value
@@ -307,6 +309,18 @@ function ImagePart({ part }: { part: UIMessage['parts'][number] }) {
307
309
  }
308
310
  ```
309
311
 
312
+ For media reused across turns, upload once via a provider Files adapter
313
+ (`openaiFiles()`, `anthropicFiles()`, `geminiFiles()`, `grokFiles()`,
314
+ `falFiles()`) and send a `{ type: 'file' }` source built with
315
+ `fileSourceFromHandle(handle)` instead of re-sending base64 each request. The
316
+ source is `{ type: 'file', value, provider }`: an opaque handle and the adapter
317
+ that issued it. A different provider (or one without Files API support at all)
318
+ rejects it with a clear error before any request is sent.
319
+ The source crosses the chat wire, so the browser can put it straight into the
320
+ `sendMessage` content. Import `fileSourceFromHandle` from the browser-safe
321
+ `@tanstack/ai/client` entry. See `ai-core/adapter-configuration/SKILL.md` §7
322
+ and `docs/advanced/files-api.md`.
323
+
310
324
  ### 4. Sending Audio Messages (Browser Recording)
311
325
 
312
326
  Use `useAudioRecorder` from `@tanstack/ai-react` (or `createAudioRecorder` in Svelte) to capture audio in the browser. The resolved `AudioRecording` includes a ready-to-use `part` that slots directly into `sendMessage`.
@@ -385,6 +399,9 @@ Pass `mcp` to let `chat()` own discovery **and** lifecycle for one or more MCP
385
399
  clients. Useful when you want minimal boilerplate and don't need to reuse the
386
400
  clients across calls.
387
401
 
402
+ `createMCPClient` tries spec `2026-07-28` first.
403
+ If the server does not support that spec, the client uses the 2025 initialize handshake.
404
+
388
405
  ```typescript
389
406
  // Prop shape:
390
407
  // chat({
@@ -443,6 +460,123 @@ export async function POST(request: Request) {
443
460
  }
444
461
  ```
445
462
 
463
+ **Host an MCP server.** Import `createMCPServer` from `@tanstack/ai-mcp/server`.
464
+ Call `server.fetch(request)` in your HTTP route.
465
+
466
+ ```typescript
467
+ import { toolDefinition } from '@tanstack/ai'
468
+ import { createMCPServer } from '@tanstack/ai-mcp/server'
469
+ import { z } from 'zod'
470
+
471
+ const getWeather = toolDefinition({
472
+ name: 'get_weather',
473
+ description: 'Current weather for a city',
474
+ inputSchema: z.object({ city: z.string() }),
475
+ }).server(async ({ city }) => {
476
+ return { city, temperature: 18, conditions: 'clear' }
477
+ })
478
+
479
+ const server = createMCPServer({
480
+ name: 'weather',
481
+ version: '1.0.0',
482
+ tools: [getWeather],
483
+ })
484
+
485
+ export function handleMcp(request: Request) {
486
+ return server.fetch(request)
487
+ }
488
+
489
+ // Mount handleMcp on GET, POST, and DELETE.
490
+ // GET is the spec 2025 stream.
491
+ // DELETE closes a spec 2025 session.
492
+ ```
493
+
494
+ `serveMCPStdio` from `@tanstack/ai-mcp/server/stdio` serves that server on stdin and stdout.
495
+ Write logs with `console.error`.
496
+ stdout carries only protocol messages.
497
+
498
+ ```typescript
499
+ import { toolDefinition } from '@tanstack/ai'
500
+ import { createMCPServer } from '@tanstack/ai-mcp/server'
501
+ import { serveMCPStdio } from '@tanstack/ai-mcp/server/stdio'
502
+ import { z } from 'zod'
503
+
504
+ const getWeather = toolDefinition({
505
+ name: 'get_weather',
506
+ description: 'Current weather for a city',
507
+ inputSchema: z.object({ city: z.string() }),
508
+ }).server(async ({ city }) => {
509
+ return { city, temperature: 18, conditions: 'clear' }
510
+ })
511
+
512
+ const server = createMCPServer({
513
+ name: 'weather',
514
+ version: '1.0.0',
515
+ tools: [getWeather],
516
+ })
517
+
518
+ serveMCPStdio(server)
519
+ ```
520
+
521
+ **MCP input interrupt.** When `chat()` receives an MCP input request, the run outcome is an interrupt.
522
+ The stream ends with `RUN_FINISHED`.
523
+ The outcome type is `interrupt`.
524
+ Read each interrupt whose `reason` is `mcp_input`.
525
+ The payload key is `tanstack:interruptPayload`.
526
+
527
+ `form` means the server asks the user for input.
528
+ `sampling` means the server asks for a model result.
529
+
530
+ ```typescript
531
+ import { chat, INTERRUPT_PAYLOAD_METADATA_KEY } from '@tanstack/ai'
532
+ import { openaiText } from '@tanstack/ai-openai'
533
+ import { createMCPClient } from '@tanstack/ai-mcp'
534
+
535
+ const client = await createMCPClient({
536
+ transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
537
+ })
538
+
539
+ try {
540
+ const stream = chat({
541
+ adapter: openaiText('gpt-5.6'),
542
+ messages: [{ role: 'user', content: 'Weather in Paris?' }],
543
+ tools: await client.tools(),
544
+ })
545
+
546
+ for await (const chunk of stream) {
547
+ if (chunk.type !== 'RUN_FINISHED') continue
548
+ if (chunk.outcome?.type !== 'interrupt') continue
549
+
550
+ for (const item of chunk.outcome.interrupts) {
551
+ if (item.reason !== 'mcp_input') continue
552
+ const payload = item.metadata?.[INTERRUPT_PAYLOAD_METADATA_KEY]
553
+ if (typeof payload !== 'object' || payload === null) continue
554
+ if (!('kind' in payload)) continue
555
+ // payload.kind is 'form' or 'sampling'
556
+ }
557
+ }
558
+ } finally {
559
+ await client.close()
560
+ }
561
+ ```
562
+
563
+ To continue, answer the interrupt:
564
+
565
+ 1. In `useChat`, the item `kind` is `generic`.
566
+ 2. For a `form`, call `resolveInterrupt` with an object that matches `request.requestedSchema`. A `createMCPServer` server asks for `{ value: string }`.
567
+ 3. For `sampling`, call `resolveInterrupt` with the reply text.
568
+ 4. Call `cancel()` to decline.
569
+ 5. The route passes `parentRunId` and `resume` to `chat()`. The tool runs again with the answer.
570
+
571
+ ```tsx ignore
572
+ // `interrupt` is one item from `useChat().interrupts`.
573
+ if (interrupt.reason === 'mcp_input' && interrupt.kind === 'generic') {
574
+ interrupt.resolveInterrupt({ value: 'Paris' })
575
+ }
576
+ ```
577
+
578
+ This works on spec `2026-07-28`. On spec 2025, the server asks the client in the middle of the tool call. `chat()` cannot pause that call, so the tool call fails.
579
+
446
580
  ### 7. Queueing Messages Sent While Streaming
447
581
 
448
582
  By default, a `sendMessage` call that arrives while a stream is in flight is
@@ -283,6 +283,14 @@ await generateVideo({
283
283
  })
284
284
  ```
285
285
 
286
+ Reference images / start frames that are reused (or arrive as inline base64 on
287
+ memory-constrained runtimes) can instead be uploaded once via the provider's
288
+ Files adapter and referenced with `source: fileSourceFromHandle(handle)` —
289
+ supported for Gemini image generation (`geminiFiles()`) and fal image/video
290
+ inputs (`falFiles()`). Endpoints that require raw bytes (OpenAI `images/edits`,
291
+ Sora `input_reference`, Gemini Veo) reject file sources with a clear error.
292
+ See `ai-core/adapter-configuration/SKILL.md` §7.
293
+
286
294
  **URL inputs that require an upload throw by default.** Most adapters pass a
287
295
  `type: 'url'` source straight through to the provider. Three paths can't —
288
296
  OpenAI `images.edit()`, OpenAI Sora `input_reference`, and Gemini **Veo** —
@@ -513,6 +513,9 @@ The post-discovery payload always returns the full description and schema regard
513
513
  `@tanstack/ai-mcp` lets a server-side `chat()` call discover and invoke tools
514
514
  hosted on any MCP server (Streamable HTTP, SSE, or stdio).
515
515
 
516
+ `createMCPClient` tries spec `2026-07-28` first.
517
+ If the server does not support that spec, the client uses the 2025 initialize handshake.
518
+
516
519
  **MCP tools and UI resources:** When an MCP tool result carries a `ui://`
517
520
  resource URI (via `_meta.ui.resourceUri`), TanStack AI surfaces it as a
518
521
  `UIResourcePart` on the assistant `UIMessage` in the client message list.
@@ -723,6 +726,106 @@ export async function POST(request: Request) {
723
726
  }
724
727
  ```
725
728
 
729
+ ### Host your own MCP server
730
+
731
+ Import `createMCPServer` from `@tanstack/ai-mcp/server`.
732
+ Pass tools from `toolDefinition().server()`.
733
+ Call `server.fetch(request)` in your HTTP route.
734
+
735
+ ```typescript
736
+ import { toolDefinition } from '@tanstack/ai'
737
+ import { createMCPServer } from '@tanstack/ai-mcp/server'
738
+ import { z } from 'zod'
739
+
740
+ const getWeather = toolDefinition({
741
+ name: 'get_weather',
742
+ description: 'Current weather for a city',
743
+ inputSchema: z.object({ city: z.string() }),
744
+ }).server(async ({ city }) => {
745
+ return { city, temperature: 18, conditions: 'clear' }
746
+ })
747
+
748
+ const server = createMCPServer({
749
+ name: 'weather',
750
+ version: '1.0.0',
751
+ tools: [getWeather],
752
+ })
753
+
754
+ export function POST(request: Request) {
755
+ return server.fetch(request)
756
+ }
757
+ ```
758
+
759
+ `stdioTransport` from `@tanstack/ai-mcp/stdio` connects your client to a command.
760
+ `serveMCPStdio` from `@tanstack/ai-mcp/server/stdio` serves your server on stdin and stdout.
761
+ Write logs with `console.error`.
762
+ stdout carries only protocol messages.
763
+
764
+ ```typescript
765
+ import { toolDefinition } from '@tanstack/ai'
766
+ import { createMCPServer } from '@tanstack/ai-mcp/server'
767
+ import { serveMCPStdio } from '@tanstack/ai-mcp/server/stdio'
768
+ import { z } from 'zod'
769
+
770
+ const getWeather = toolDefinition({
771
+ name: 'get_weather',
772
+ description: 'Current weather for a city',
773
+ inputSchema: z.object({ city: z.string() }),
774
+ }).server(async ({ city }) => {
775
+ return { city, temperature: 18, conditions: 'clear' }
776
+ })
777
+
778
+ const server = createMCPServer({
779
+ name: 'weather',
780
+ version: '1.0.0',
781
+ tools: [getWeather],
782
+ })
783
+
784
+ serveMCPStdio(server)
785
+ ```
786
+
787
+ The `@tanstack/ai-mcp` skill shows `ctx.context.requestInput` and `ctx.context.sample`.
788
+
789
+ ### Read an MCP input interrupt
790
+
791
+ When `chat()` receives an MCP input request, the run outcome is an interrupt.
792
+ The stream ends with `RUN_FINISHED`.
793
+ The outcome type is `interrupt`.
794
+ Read each interrupt whose `reason` is `mcp_input`.
795
+ The payload key is `tanstack:interruptPayload`.
796
+
797
+ `form` means the server asks the user for input.
798
+ `sampling` means the server asks for a model result.
799
+
800
+ ```typescript
801
+ import { chat, INTERRUPT_PAYLOAD_METADATA_KEY } from '@tanstack/ai'
802
+ import { openaiText } from '@tanstack/ai-openai'
803
+ import { createMCPClient } from '@tanstack/ai-mcp'
804
+
805
+ const client = await createMCPClient({
806
+ transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
807
+ })
808
+
809
+ const stream = chat({
810
+ adapter: openaiText('gpt-5.5'),
811
+ messages: [{ role: 'user', content: 'Weather in Paris?' }],
812
+ tools: await client.tools(),
813
+ })
814
+
815
+ for await (const chunk of stream) {
816
+ if (chunk.type !== 'RUN_FINISHED') continue
817
+ if (chunk.outcome?.type !== 'interrupt') continue
818
+
819
+ for (const item of chunk.outcome.interrupts) {
820
+ if (item.reason !== 'mcp_input') continue
821
+ const payload = item.metadata?.[INTERRUPT_PAYLOAD_METADATA_KEY]
822
+ if (typeof payload !== 'object' || payload === null) continue
823
+ if (!('kind' in payload)) continue
824
+ // payload.kind is 'form' or 'sampling'
825
+ }
826
+ }
827
+ ```
828
+
726
829
  ## Provider Skills
727
830
 
728
831
  > **Not to be confused with `@tanstack/ai-code-mode-snippets`**, whose snippets are TypeScript functions your application generates and runs in its own Code Mode sandbox (a local JS isolate). Provider Skills are hosted, provider-managed bundles that the model loads on demand and runs inside the provider's server-side sandbox.
@@ -89,6 +89,15 @@ export interface TextAdapter<
89
89
  */
90
90
  readonly requires?: ReadonlyArray<CapabilityHandle>
91
91
 
92
+ /**
93
+ * Declares that this adapter can consume `{ type: 'file' }` content sources
94
+ * (provider Files API references). `chat()` rejects file sources in preflight
95
+ * for adapters that don't declare this, so an adapter written before the
96
+ * file arm existed fails closed instead of silently mis-mapping a reference
97
+ * onto its URL/data branch.
98
+ */
99
+ readonly supportsFileSources?: boolean
100
+
92
101
  /**
93
102
  * @internal Type-only properties for inference. Not assigned at runtime.
94
103
  */
@@ -209,6 +218,7 @@ export abstract class BaseTextAdapter<
209
218
  abstract readonly name: string
210
219
  readonly model: TModel
211
220
  readonly requires?: ReadonlyArray<CapabilityHandle> = undefined
221
+ readonly supportsFileSources: boolean = false
212
222
 
213
223
  // Type-only property - never assigned at runtime
214
224
  declare '~types': {
@@ -2,6 +2,7 @@ import type { SubagentInfo as AGUISubagentInfo } from '@ag-ui/core'
2
2
  import type { InterruptDefinition } from '../../../interrupt-definition'
3
3
  import type {
4
4
  AnyTool,
5
+ InferSchemaType,
5
6
  ModelMessage,
6
7
  RunAgentResumeItem,
7
8
  SchemaInput,
@@ -12,8 +13,16 @@ import type { AnyClientTool } from '../tools/tool-definition'
12
13
 
13
14
  /**
14
15
  * Context the library passes into {@link defineAgent} `run`.
16
+ * `TInput` is the agent's `inputSchema`.
15
17
  */
16
- export interface SubagentRunContext {
18
+ export interface SubagentRunContext<
19
+ TInput extends SchemaInput | undefined = any,
20
+ > {
21
+ /**
22
+ * The input the parent model wrote for this child, checked against
23
+ * `inputSchema`. `undefined` when the agent has no `inputSchema`.
24
+ */
25
+ input: TInput extends SchemaInput ? InferSchemaType<TInput> : undefined
17
26
  messages: Array<UIMessage | ModelMessage>
18
27
  abortSignal?: AbortSignal
19
28
  threadId: string
@@ -53,13 +62,20 @@ export interface DefinedAgent<
53
62
  TSchema extends SchemaInput | undefined = SchemaInput | undefined,
54
63
  TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
55
64
  ReadonlyArray<InterruptDefinition<any, any, any, any>>,
65
+ TInput extends SchemaInput | undefined = any,
56
66
  > extends AGUISubagentInfo {
57
67
  name: TName
58
68
  /** Required here: the router and the synthetic tool both read it. */
59
69
  description: string
60
70
  run: (
61
- ctx: SubagentRunContext,
71
+ ctx: SubagentRunContext<TInput>,
62
72
  ) => AsyncIterable<StreamChunk> | Promise<AsyncIterable<StreamChunk>>
73
+ /**
74
+ * The input the parent model writes when it calls this agent's tool, such
75
+ * as a short brief. `run` reads it as `ctx.input`. Tool mode only: a
76
+ * `subagents.router` cannot start an agent that has `inputSchema`.
77
+ */
78
+ inputSchema?: TInput
63
79
  tools?: TTools
64
80
  interrupts?: TInterrupts
65
81
  outputSchema?: TSchema
@@ -104,7 +120,8 @@ export function defineAgent<
104
120
  const TInterrupts extends ReadonlyArray<
105
121
  InterruptDefinition<any, any, any, any>
106
122
  > = readonly [],
107
- >(agent: DefinedAgent<TName, TTools, TSchema, TInterrupts>) {
123
+ TInput extends SchemaInput | undefined = undefined,
124
+ >(agent: DefinedAgent<TName, TTools, TSchema, TInterrupts, TInput>) {
108
125
  if (agent.name.trim() === '') {
109
126
  throw new Error('defineAgent requires a non-empty name')
110
127
  }
@@ -93,6 +93,8 @@ export function createSubagentSink(): SubagentSink {
93
93
  /** One child to start, or a suspended child to continue. */
94
94
  export interface SpawnEntry {
95
95
  name: string
96
+ /** The checked tool input for an agent with `inputSchema`. */
97
+ input?: unknown
96
98
  resume?: {
97
99
  subagentRunId: string
98
100
  /** The child's own messages from the interrupted run. */
@@ -248,6 +250,7 @@ function openAgentStream(
248
250
  return spawnAgentStream(
249
251
  agent,
250
252
  {
253
+ input: entry.input,
251
254
  messages: resumed?.messages ?? ctx.messages,
252
255
  ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
253
256
  threadId: childThreadId(bag.sandbox, ctx.threadId, entry.name),
@@ -721,8 +724,10 @@ export function createSyntheticSubagentTools(
721
724
  return bag.agents.map((agent) => ({
722
725
  name: agent.name,
723
726
  description: agent.description,
727
+ ...(agent.inputSchema !== undefined && { inputSchema: agent.inputSchema }),
724
728
  [SUBAGENT_TOOL]: true,
725
- execute: async (_input: unknown, context?: unknown) => {
729
+ // The tool loop checks `input` against `inputSchema` before this runs.
730
+ execute: async (input: unknown, context?: unknown) => {
726
731
  const toolContext = context as
727
732
  | {
728
733
  toolCallId?: string
@@ -736,17 +741,20 @@ export function createSyntheticSubagentTools(
736
741
  child.parentToolCallId !== undefined &&
737
742
  child.parentToolCallId === toolCallId,
738
743
  )
739
- const entry: SpawnEntry = suspended
740
- ? {
741
- name: agent.name,
742
- resume: {
743
- subagentRunId: suspended.subagentRunId,
744
- messages: suspended.messages,
745
- entries: suspended.resume,
746
- text: suspended.text,
747
- },
748
- }
749
- : { name: agent.name }
744
+ // An agent without `inputSchema` still gets `{}` from the model. Its
745
+ // `ctx.input` stays undefined.
746
+ const entry: SpawnEntry = {
747
+ name: agent.name,
748
+ ...(agent.inputSchema !== undefined && { input }),
749
+ ...(suspended && {
750
+ resume: {
751
+ subagentRunId: suspended.subagentRunId,
752
+ messages: suspended.messages,
753
+ entries: suspended.resume,
754
+ text: suspended.text,
755
+ },
756
+ }),
757
+ }
750
758
  const sink = createSubagentSink()
751
759
  const link = linkAbort(parent.abortSignal)
752
760
  let subagentRunId = suspended?.subagentRunId ?? ''