nexus-ai-pro 2.5.0 → 2.6.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 (51) hide show
  1. package/API_STABILITY.md +36 -0
  2. package/CHANGELOG.md +102 -1
  3. package/README.md +9 -3
  4. package/dist/agent/create-agent.d.ts +37 -2
  5. package/dist/agent/create-agent.js +62 -4
  6. package/dist/agent/index.d.ts +2 -1
  7. package/dist/agent/tool.d.ts +19 -4
  8. package/dist/agent/tool.js +27 -4
  9. package/dist/graph/event-stream.js +2 -0
  10. package/dist/graph/graph.js +4 -1
  11. package/dist/mcp/client.d.ts +9 -1
  12. package/dist/mcp/client.js +38 -8
  13. package/dist/mcp/server.js +42 -2
  14. package/dist/protocols/a2a.d.ts +166 -1
  15. package/dist/protocols/a2a.js +499 -132
  16. package/dist/protocols/acp-editor.d.ts +43 -0
  17. package/dist/protocols/acp-editor.js +164 -0
  18. package/dist/protocols/acp.d.ts +76 -3
  19. package/dist/protocols/acp.js +180 -7
  20. package/dist/protocols/ag-ui.d.ts +23 -0
  21. package/dist/protocols/ag-ui.js +128 -8
  22. package/dist/tracing/instrument.js +14 -3
  23. package/dist/types/graph.d.ts +7 -0
  24. package/dist/types/messages.d.ts +12 -0
  25. package/dist/version.d.ts +1 -1
  26. package/dist/version.js +1 -1
  27. package/dist-cjs/agent/create-agent.d.ts +37 -2
  28. package/dist-cjs/agent/create-agent.js +62 -4
  29. package/dist-cjs/agent/index.d.ts +2 -1
  30. package/dist-cjs/agent/tool.d.ts +19 -4
  31. package/dist-cjs/agent/tool.js +27 -4
  32. package/dist-cjs/graph/event-stream.js +2 -0
  33. package/dist-cjs/graph/graph.js +4 -1
  34. package/dist-cjs/mcp/client.d.ts +9 -1
  35. package/dist-cjs/mcp/client.js +37 -7
  36. package/dist-cjs/mcp/server.js +42 -2
  37. package/dist-cjs/protocols/a2a.d.ts +166 -1
  38. package/dist-cjs/protocols/a2a.js +501 -132
  39. package/dist-cjs/protocols/acp-editor.d.ts +43 -0
  40. package/dist-cjs/protocols/acp-editor.js +167 -0
  41. package/dist-cjs/protocols/acp.d.ts +76 -3
  42. package/dist-cjs/protocols/acp.js +183 -8
  43. package/dist-cjs/protocols/ag-ui.d.ts +23 -0
  44. package/dist-cjs/protocols/ag-ui.js +127 -7
  45. package/dist-cjs/tracing/instrument.js +14 -3
  46. package/dist-cjs/types/graph.d.ts +7 -0
  47. package/dist-cjs/types/messages.d.ts +12 -0
  48. package/dist-cjs/version.d.ts +1 -1
  49. package/dist-cjs/version.js +1 -1
  50. package/llms.txt +1 -1
  51. package/package.json +17 -2
package/API_STABILITY.md CHANGED
@@ -41,6 +41,42 @@ Both are covered by this policy:
41
41
  Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
42
42
  one of its explicit subpaths.
43
43
 
44
+ ## 2.6 stage (2.6.0)
45
+
46
+ Everything 2.6 adds is stable from the start and additive.
47
+
48
+ New and stable:
49
+ - `ToolDefinition.stream`, `ToolContext.progress`, and `ToolBody`; `tool()` with `stream`. The
50
+ AG-UI event name `tool_progress`, and the `progress` and `callId` fields of a tool run's metadata;
51
+ - `McpClient.callTool()`'s `progress` option, and `notifications/progress` from `McpServer`;
52
+ - `AgUiOptions.stateDeltas`, `messagesSnapshot`, and `frontendTools`, and `AgUiPatchOperation`;
53
+ - `CreateAgentOptions.toolSource` and `ToolSourceContext`; `ClientToolDefinition`, the `clientTools`
54
+ channel, and the `AgentStopReason` member `client_tools`;
55
+ - the `step` member of `GraphEvent`;
56
+ - on `nexus-ai-pro/protocols/a2a`: `A2aTaskStore`, `A2aTaskRecord`, `A2aTaskQuery`,
57
+ `MemoryA2aTaskStore`, `storeA2aTasks()`; `A2aServerOptions.store`, `pushNotifications`,
58
+ `extendedCard`, `pollMs`, and `onError`; `A2aPushOptions` and `A2aPushNotificationConfig`; the
59
+ methods `ListTasks`, `SubscribeToTask`, the four push notification config methods, and
60
+ `GetExtendedAgentCard`; the client's new methods, `A2aListTasksOptions`, and `A2aTaskPage`; and
61
+ the error codes `pushNotificationNotSupported` and `extendedCardNotConfigured`.
62
+
63
+ - on `nexus-ai-pro/protocols/acp`: `AcpSessionStore`, `AcpSessionRecord`, `MemoryAcpSessionStore`,
64
+ `storeAcpSessions()`, `AcpOptions.sessions` and `editor`, `AcpEditorBridge`, `AcpConnection`,
65
+ `AcpClientCapabilities`, `session/load`, image and audio blocks, and the fourth argument of
66
+ `AcpOptions.input`;
67
+ - `nexus-ai-pro/protocols/acp/editor`: `acpEditor()`, `AcpEditor`, `AcpEditorOptions`, and the tool
68
+ names `editor_read_file`, `editor_write_file`, and `editor_run_command`.
69
+
70
+ An AG-UI run reports a node's tasks running side by side as one step, and finishes every step it
71
+ started before `RUN_FINISHED` or `RUN_ERROR`. A2A `ListTasks` refuses a status that is not a task
72
+ state, and reads `TASK_STATE_UNSPECIFIED` and `UNRECOGNIZED` as no filter.
73
+
74
+ A streamed A2A task now reports its move to `TASK_STATE_WORKING` as a status update, before its
75
+ other events.
76
+
77
+ `GraphEvent` and `AgentStopReason` gain members, as the compatibility rules allow in a minor
78
+ release. Code that switches over either exhaustively should handle the new ones.
79
+
44
80
  ## 2.5 stage (2.5.0)
45
81
 
46
82
  Everything 2.5 adds is stable from the start and additive.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,106 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.6.0] - 2026-10-11
8
+
9
+ Protocols complete, and progress on every stream.
10
+
11
+ ### Added
12
+
13
+ - **Tool progress, end to end.**
14
+ - A `ToolDefinition` can have `stream`: an async generator that yields progress and returns the
15
+ result. `tool()` takes one instead of `execute`, and gives it an `execute` that runs the stream,
16
+ so it works wherever a tool is executed. `ToolExecutor` runs a stream itself.
17
+ - `ToolContext.progress` reports progress from any tool. An agent sets it, and turns each report
18
+ into a `progress` tool event while the tool runs.
19
+ - Progress reaches the event stream and the server's run events; AG-UI, as a `CUSTOM`
20
+ `tool_progress` event; A2A, as a working status update with the progress as data; ACP, as an
21
+ update of the tool call in progress; traces, in the tool run's `metadata.progress`; and the
22
+ studio's run view.
23
+ - MCP: `McpServer` runs `stream` tools and sends `notifications/progress` for a call that names a
24
+ `progressToken`. `McpClient.callTool()` takes a `progress` listener, and the tools from
25
+ `toNexusTools()` pass a remote tool's progress to the agent.
26
+
27
+ - **AG-UI, complete**, each part opt-in on `AgUiOptions`:
28
+ - `stateDeltas` reports state as it changes: a `STATE_SNAPSHOT` after the first step, then a
29
+ `STATE_DELTA` with an RFC 6902 JSON Patch after every step that changed it, appending to arrays
30
+ item by item. `AgUiPatchOperation` is its type.
31
+ - `messagesSnapshot` sends the whole transcript as `MESSAGES_SNAPSHOT` before the run finishes.
32
+ - `frontendTools` offers the tools the frontend sends to the model. The frontend runs a call to one,
33
+ and the next run's messages carry its result.
34
+ - **Tools the client runs.** An agent's input may carry `clientTools`, each a `ClientToolDefinition`.
35
+ The model is offered them; a call to one ends the turn with the new `stopReason: 'client_tools'`
36
+ once the agent's own calls of that step have run, and the client's result continues it. A client
37
+ tool never replaces a tool of the agent's own.
38
+ - **Tools that depend on the run.** `CreateAgentOptions.toolSource` supplies tools per run, from a
39
+ `ToolSourceContext`, offered and run like `tools`, under the same permission policy.
40
+ - **A2A, durable and complete.**
41
+ - Tasks and conversations live in an `A2aTaskStore`: `MemoryA2aTaskStore` by default, or
42
+ `storeA2aTasks()` over any long-term `Store`, so a task paused for an answer survives a restart
43
+ and another replica finishes it.
44
+ - `ListTasks` by context and state, a page at a time; `SubscribeToTask` on any replica; and a
45
+ cancel that stands against the replica running the task.
46
+ - Push notifications, with `pushNotifications`: each state change POSTed to a task's webhook,
47
+ with the caller's token and signed as the server's operation webhooks are. A webhook must be
48
+ `https` and not a private address, unless `allowPrivateNetworks` is set.
49
+ - `GetExtendedAgentCard`, with `extendedCard`, for authenticated callers.
50
+ - The client gains `extendedCard()`, `listTasks()`, `subscribe()`, `setPushNotification()`,
51
+ `listPushNotifications()`, `deletePushNotification()`, and `push` and `returnImmediately` when
52
+ sending.
53
+ - **ACP, durable and complete.**
54
+ - Sessions live in an `AcpSessionStore`: `MemoryAcpSessionStore` by default, or
55
+ `storeAcpSessions()` over any long-term `Store`. The agent offers `session/load`, replays the
56
+ conversation, and asks again a question it was waiting on.
57
+ - A prompt can carry images and audio, which reach the model as content parts.
58
+ - `acpEditor()`, on the new `nexus-ai-pro/protocols/acp/editor`, gives an agent the editor's files,
59
+ terminal, and MCP servers, as tools from its `toolSource`, each declaring what it touches.
60
+ `serveAcp()` takes it as `editor`, and detaches it when the editor closes its end.
61
+ - **Interoperability, proven.**
62
+ - A browser in CI: `npm run test:browser` serves the build to headless Chromium, which imports
63
+ every kernel entry point and runs a graph, an agent, and AG-UI. It runs in `npm test`.
64
+ - The previous minor on the other side of the wire: a 2.5 client against a 2.6 server, and the
65
+ other way round, on the server's HTTP API and A2A, and one harness across both releases for
66
+ AG-UI and ACP.
67
+ - A nightly `Interoperability` workflow drives each protocol with its reference client:
68
+ `@ag-ui/client`, `@a2a-js/sdk`, and `@agentclientprotocol/sdk`.
69
+ - **A `step` graph event.** `GraphRunOptions.onEvent` receives `step` after each superstep, with the
70
+ state after it.
71
+
72
+ ### Fixed
73
+
74
+ - AG-UI: a run that paused inside a node left that node's step open, so a strict client refused its
75
+ `RUN_FINISHED`; and a node's tasks running side by side each started the same step. A node's tasks
76
+ are now one step, and every step still open is finished before the run finishes or fails. Found by
77
+ the reference client.
78
+ - A2A: `ListTasks` without a status filter, as the reference SDK sends it (`UNRECOGNIZED`), matched
79
+ no task. An unset filter now filters nothing, and an unknown state is refused as invalid.
80
+ - MCP: the stdio transport's `close()` killed the server without waiting for it, so what the process
81
+ held, such as its working directory, could outlive the close. It now waits up to two seconds for
82
+ the process to exit.
83
+
84
+ ### Proof
85
+
86
+ - Headless Chromium imports every kernel entry point and runs a graph, an agent with a streaming
87
+ tool, and AG-UI. A 2.5 client and a 2.6 server pass the protocol suites in both directions, and the
88
+ reference clients of AG-UI, A2A, and ACP each drive this release's servers through a whole run.
89
+ - An ACP session paused on a question survives the agent's restart, on memory and SQLite; the
90
+ editor reopens it, sees the conversation replayed, and answers. An image and audio reach the model.
91
+ Through the bridge, an agent reads, writes (after the person allows it), and runs the tests in the
92
+ editor, and searches docs through an MCP server the editor offered, started as a real process.
93
+ - An A2A task paused for an approval survives a restart and is finished by another replica, sending
94
+ its email once, on the memory, SQLite, Postgres, and Redis stores. A running task is followed where
95
+ it runs and from another replica; a cancel on one replica stands against the other. Every state
96
+ change reaches the webhook, signed and with the caller's token, and nine webhook URLs that point
97
+ into the agent's own network or are not https are refused.
98
+ - An AG-UI client applying a run's snapshot and its deltas rebuilds the final state exactly, with
99
+ each new message one `add`. A frontend tool is offered to the model beside the agent's own; the
100
+ agent runs its own call, waits without calling the model again, and continues from the page's
101
+ result in the next run. A page cannot shadow the agent's tool, and without the options the events
102
+ are what they were.
103
+ - A tool that will not go on until a reader has seen its first report finishes on every stream: the
104
+ event stream, the server's events, AG-UI, A2A, ACP, and MCP. Its reports arrive in order, after the
105
+ call and before the result, and its traced run keeps them.
106
+
7
107
  ## [2.5.0] - 2026-10-09
8
108
 
9
109
  Graduation finished, and conversations evaluated.
@@ -2104,7 +2204,8 @@ shared policy vocabulary is in place for a future release that revisits this.
2104
2204
  - URL fetching rejects unsafe private-network targets and limits response reads.
2105
2205
  - CLI and audit findings no longer disclose detected secret values.
2106
2206
 
2107
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.5.0...HEAD
2207
+ [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.6.0...HEAD
2208
+ [2.6.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.5.0...v2.6.0
2108
2209
  [2.5.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.4.0...v2.5.0
2109
2210
  [2.4.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.3.0...v2.4.0
2110
2211
  [2.3.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.2.0...v2.3.0
package/README.md CHANGED
@@ -270,12 +270,18 @@ Shipped so far:
270
270
  - 2.5 graduation finished, and conversations evaluated: deployments that change atomically across
271
271
  replicas, revision worker pools, fair canaries judged on every run, and the studio out of
272
272
  experimental after a threat review; Postgres tenant usage; signed context named on every trace;
273
- simulated users and thread evaluators; and a doctor that knows the opt-in settings.
273
+ simulated users and thread evaluators; and a doctor that knows the opt-in settings;
274
+ - 2.6 protocols complete, and progress on every stream: a tool's progress reaching the event stream,
275
+ the protocols, traces, and the studio; AG-UI state deltas and frontend tools; A2A tasks and ACP
276
+ sessions that survive a restart, with push notifications, `session/load`, and the editor's files,
277
+ terminal, and MCP servers; and every protocol proven in a browser, against the previous release,
278
+ and against its reference client.
274
279
 
275
280
  Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
276
281
  [MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
277
- Next, 2.6 completes the agent protocols: tool progress on every stream, AG-UI deltas and frontend
278
- tools, A2A tasks and ACP sessions that survive a restart, and a browser in CI. The [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists the
282
+ Next, 2.7 lets agents do more: provider tool search, an interpreter contract, workspace search,
283
+ helpers that run beside the agent, and a container sandbox. Evaluation grows its own datasets and
284
+ runs on workers, within a budget. The [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists the
279
285
  releases after it.
280
286
 
281
287
  ## Before Production
@@ -163,12 +163,44 @@ export interface CreateAgentOptions {
163
163
  * event once the model returns. Middleware sees the assembled response either way.
164
164
  */
165
165
  streamTokens?: boolean;
166
+ /**
167
+ * Tools that depend on the run, added to `tools` on every model call and run like them: an editor's
168
+ * file system for one session, or the MCP servers one user connected. A tool in `tools` wins over a
169
+ * source's tool of the same name.
170
+ */
171
+ toolSource?: (context: ToolSourceContext) => ToolDefinition[] | Promise<ToolDefinition[]>;
172
+ }
173
+ /** What a tool source is told about the run it supplies tools for. */
174
+ export interface ToolSourceContext {
175
+ /** The run's thread. */
176
+ threadId: string;
177
+ /** The tenant the run is for. */
178
+ tenantId?: string;
179
+ /** Who the run is for. */
180
+ principal?: Readonly<Principal>;
181
+ /** Aborted when the run is cancelled. */
182
+ signal: AbortSignal;
183
+ }
184
+ /**
185
+ * A tool the client runs, such as one an AG-UI frontend offers: a name, a description, and a schema,
186
+ * with no implementation on the agent's side. Given in the input as `clientTools`, it is offered to
187
+ * the model like any tool. When the model calls one, the agent ends its turn with `stopReason:
188
+ * 'client_tools'`, and the client sends the result back in the next run's messages.
189
+ */
190
+ export interface ClientToolDefinition {
191
+ /** The name the model calls it by. */
192
+ name: string;
193
+ /** What it does, written for the model. */
194
+ description: string;
195
+ /** JSON Schema for the arguments. */
196
+ parameters: Record<string, unknown>;
166
197
  }
167
198
  declare const agentChannels: () => {
168
199
  messages: import("../types/graph.js").Channel<Message[]>;
169
200
  iterations: import("../types/graph.js").Channel<number>;
170
201
  answer: import("../types/graph.js").Channel<string>;
171
202
  stopReason: import("../types/graph.js").Channel<AgentStopReason | undefined>;
203
+ clientTools: import("../types/graph.js").Channel<ClientToolDefinition[] | undefined>;
172
204
  };
173
205
  /** The agent's state channels, as a graph schema. */
174
206
  export type AgentChannels = ReturnType<typeof agentChannels>;
@@ -182,12 +214,15 @@ export type AgentState = {
182
214
  answer: string;
183
215
  /** Why the agent stopped. */
184
216
  stopReason?: AgentStopReason;
217
+ /** Tools the client runs, given in the input; see `ClientToolDefinition`. */
218
+ clientTools?: ClientToolDefinition[];
185
219
  };
186
220
  /**
187
221
  * `completed` when the model answered, `max_iterations` when it ran out of model calls, `stopped`
188
- * when a middleware ended the run through `context.stop()`.
222
+ * when a middleware ended the run through `context.stop()`, and `client_tools` when the model called
223
+ * a tool the client runs, which waits for the client's result.
189
224
  */
190
- export type AgentStopReason = 'completed' | 'max_iterations' | 'stopped';
225
+ export type AgentStopReason = 'completed' | 'max_iterations' | 'stopped' | 'client_tools';
191
226
  /** An agent: a compiled graph over the agent's channels. */
192
227
  export type AgentGraph = CompiledGraph<AgentChannels>;
193
228
  /** Wraps a question into the state an agent starts from. */
@@ -8,6 +8,7 @@ const agentChannels = () => ({
8
8
  iterations: counter(),
9
9
  answer: lastValue(''),
10
10
  stopReason: lastValue(undefined),
11
+ clientTools: lastValue(undefined),
11
12
  });
12
13
  export function agentInput(goal) {
13
14
  return { messages: [{ role: 'user', content: goal }] };
@@ -20,16 +21,33 @@ export function createAgent(options) {
20
21
  const executor = new ToolExecutor(options.tools ?? []);
21
22
  const middleware = options.middleware ?? [];
22
23
  const approvals = options.interruptOn ?? {};
24
+ const toolsFor = async (context) => {
25
+ if (!options.toolSource)
26
+ return executor;
27
+ const supplied = await options.toolSource({
28
+ threadId: context.threadId,
29
+ ...(context.tenantId !== undefined ? { tenantId: context.tenantId } : {}),
30
+ ...(context.principal ? { principal: context.principal } : {}),
31
+ signal: context.signal,
32
+ });
33
+ return supplied.length ? new ToolExecutor([...supplied, ...executor.list()]) : executor;
34
+ };
23
35
  const graph = createGraph({ channels: agentChannels() })
24
36
  .addNode('model', async (context) => {
25
37
  const state = context.state;
38
+ const runTools = await toolsFor(context);
39
+ const clientTools = clientToolsOf(state, runTools);
40
+ if (clientTools.length > 0 && waitingOnClient(state.messages, clientTools)) {
41
+ return new Command({ update: { stopReason: 'client_tools' }, goto: END });
42
+ }
43
+ const offered = [...runTools.list(), ...clientTools];
26
44
  let request = {
27
45
  model: options.model ?? 'auto',
28
46
  messages: [
29
47
  ...(options.systemPrompt ? [{ role: 'system', content: options.systemPrompt }] : []),
30
48
  ...state.messages,
31
49
  ],
32
- ...(options.tools?.length ? { tools: executor.list() } : {}),
50
+ ...(offered.length ? { tools: offered } : {}),
33
51
  ...(options.temperature === undefined ? {} : { temperature: options.temperature }),
34
52
  ...(options.maxTokens === undefined ? {} : { maxTokens: options.maxTokens }),
35
53
  ...(options.responseFormat ? { responseFormat: options.responseFormat } : {}),
@@ -88,9 +106,28 @@ export function createAgent(options) {
88
106
  goto: END,
89
107
  });
90
108
  }
109
+ const forClient = new Set(clientTools.map((item) => item.name));
110
+ const ours = calls.filter((call) => !forClient.has(call.function.name));
111
+ for (const call of calls) {
112
+ if (!forClient.has(call.function.name))
113
+ continue;
114
+ const parsed = parseArguments(call.function.arguments);
115
+ context.tool({
116
+ phase: 'start',
117
+ id: call.id,
118
+ name: call.function.name,
119
+ args: parsed.ok ? parsed.args : call.function.arguments,
120
+ });
121
+ }
122
+ if (ours.length === 0) {
123
+ return new Command({
124
+ update: { messages: [assistant], iterations: 1, stopReason: 'client_tools' },
125
+ goto: END,
126
+ });
127
+ }
91
128
  return new Command({
92
129
  update: { messages: [assistant], iterations: 1 },
93
- goto: calls.map((call) => new Send('tools', call)),
130
+ goto: ours.map((call) => new Send('tools', call)),
94
131
  });
95
132
  }, { ends: ['tools', END] })
96
133
  .addNode('tools', async (context) => {
@@ -100,7 +137,8 @@ export function createAgent(options) {
100
137
  return { messages: [toolMessage(call, { ok: false, error: parsed.error })] };
101
138
  }
102
139
  let args = parsed.args;
103
- const definition = executor.get(call.function.name);
140
+ const runTools = await toolsFor(context);
141
+ const definition = runTools.get(call.function.name);
104
142
  const capabilities = capabilitiesOf(definition, args);
105
143
  const description = {
106
144
  id: call.id,
@@ -159,13 +197,14 @@ export function createAgent(options) {
159
197
  if (recheck.decision === 'deny')
160
198
  return { ok: false, error: `Permission denied: ${recheck.reason}` };
161
199
  }
162
- return executor.execute(description.name, current.args, {
200
+ return runTools.execute(description.name, current.args, {
163
201
  toolCallId: call.id,
164
202
  threadId: context.threadId,
165
203
  ...(context.tenantId !== undefined ? { tenantId: context.tenantId } : {}),
166
204
  ...(context.principal ? { principal: context.principal } : {}),
167
205
  signal: context.signal,
168
206
  ...(context.store ? { store: context.store } : {}),
207
+ progress: (progress) => context.tool({ phase: 'progress', id: call.id, name: description.name, progress }),
169
208
  });
170
209
  };
171
210
  context.tool({ phase: 'start', id: call.id, name: description.name, args });
@@ -250,6 +289,25 @@ function parseArguments(raw) {
250
289
  return { ok: false, error: `Tool arguments are not valid JSON (${reason}); the tool was not run` };
251
290
  }
252
291
  }
292
+ function clientToolsOf(state, runTools) {
293
+ return (state.clientTools ?? []).filter((item) => typeof item?.name === 'string' && item.name !== '' && !runTools.has(item.name));
294
+ }
295
+ function waitingOnClient(messages, clientTools) {
296
+ const names = new Set(clientTools.map((item) => item.name));
297
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
298
+ const message = messages[index];
299
+ if (message.role === 'user')
300
+ return false;
301
+ if (message.role !== 'assistant' || !message.toolCalls?.length)
302
+ continue;
303
+ const answered = new Set(messages
304
+ .slice(index + 1)
305
+ .filter((item) => item.role === 'tool')
306
+ .map((item) => item.toolCallId));
307
+ return message.toolCalls.some((call) => names.has(call.function.name) && !answered.has(call.id));
308
+ }
309
+ return false;
310
+ }
253
311
  function hookContext(context, state, store) {
254
312
  return {
255
313
  state,
@@ -1,6 +1,7 @@
1
1
  export { AgentLoop, type AgentModelClient as AgentLoopModelClient } from './loop.js';
2
2
  export { isToolOutput, tool, ToolExecutor, toolMessageContent, toolOutput } from './tool.js';
3
+ export type { ToolBody } from './tool.js';
3
4
  export { limitToolCalls, redactMessages, type RedactOptions, summarizeHistory, type SummarizeOptions, } from './middleware/basic.js';
4
- export { agentAsTool, agentInput, type AgentApproval, type AgentApprovalPolicy, type AgentChannels, type AgentGraph, type AgentMiddleware, type AgentMiddlewareContext, type AgentModelCallContext, type AgentModelClient, type AgentState, type AgentStopReason, type AgentToolCall, type AgentToolResult, createAgent, type CreateAgentOptions, } from './create-agent.js';
5
+ export { agentAsTool, agentInput, type AgentApproval, type AgentApprovalPolicy, type AgentChannels, type AgentGraph, type AgentMiddleware, type AgentMiddlewareContext, type AgentModelCallContext, type AgentModelClient, type AgentState, type AgentStopReason, type AgentToolCall, type AgentToolResult, type ClientToolDefinition, createAgent, type CreateAgentOptions, type ToolSourceContext, } from './create-agent.js';
5
6
  export type { AgentConfig, AgentResult, AgentStep, ToolExecutionResult } from '../types/agent.js';
6
7
  export type { ToolContext } from '../types/messages.js';
@@ -1,15 +1,30 @@
1
1
  import type { ContentPart, Message, ToolContext, ToolDefinition, ToolOutput } from '../types/messages.js';
2
2
  import type { ToolExecutionResult } from '../types/agent.js';
3
- /** Defines a tool the model can call, typing its arguments. */
3
+ /** How a tool built with `tool()` runs: once, with `execute`, or reporting progress, with `stream`. */
4
+ export type ToolBody<TArgs> = {
5
+ /** Runs the tool. In an agent, `context` says who the run is for and carries its signal and store. */
6
+ execute: (args: TArgs, context: ToolContext) => Promise<unknown> | unknown;
7
+ stream?: never;
8
+ } | {
9
+ /**
10
+ * Runs the tool, yielding progress while it works and returning the result. See
11
+ * `ToolDefinition.stream`.
12
+ */
13
+ stream: (args: TArgs, context: ToolContext) => AsyncGenerator<unknown, unknown, undefined>;
14
+ execute?: never;
15
+ };
16
+ /**
17
+ * Defines a tool the model can call, typing its arguments. A tool given `stream` gets an `execute`
18
+ * too, which runs the stream and reports each yield to `context.progress`, so it works wherever a
19
+ * tool is executed.
20
+ */
4
21
  export declare function tool<TArgs extends Record<string, unknown> = Record<string, unknown>>(definition: {
5
22
  name: string;
6
23
  description: string;
7
24
  parameters: Record<string, unknown>;
8
- /** Runs the tool. In an agent, `context` says who the run is for and carries its signal and store. */
9
- execute: (args: TArgs, context: ToolContext) => Promise<unknown> | unknown;
10
25
  /** What the tool does to the world, for a permission policy; see `ToolDefinition.capabilities`. */
11
26
  capabilities?: readonly string[] | ((args: TArgs) => readonly string[]);
12
- }): ToolDefinition;
27
+ } & ToolBody<TArgs>): ToolDefinition;
13
28
  /**
14
29
  * Builds a tool's result as content: text, images, and asset references, in order. A string becomes
15
30
  * a text part. Returned from `execute`, it is sent as the tool message itself, so an image a tool
@@ -1,10 +1,19 @@
1
1
  export function tool(definition) {
2
2
  const capabilities = definition.capabilities;
3
+ const stream = definition.stream;
4
+ const execute = definition.execute;
3
5
  return {
4
6
  name: definition.name,
5
7
  description: definition.description,
6
8
  parameters: definition.parameters,
7
- execute: async (args, context) => definition.execute(args, context ?? {}),
9
+ ...(stream
10
+ ? {
11
+ stream: (args, context) => stream(args, context ?? {}),
12
+ execute: async (args, context) => drain(stream(args, context ?? {}), context?.progress),
13
+ }
14
+ : {
15
+ execute: async (args, context) => execute(args, context ?? {}),
16
+ }),
8
17
  ...(capabilities === undefined
9
18
  ? {}
10
19
  : {
@@ -31,6 +40,18 @@ export function toolMessageContent(result) {
31
40
  return result.result.content;
32
41
  return JSON.stringify(result.ok ? (result.result ?? null) : { error: result.error });
33
42
  }
43
+ async function drain(generator, progress) {
44
+ for (;;) {
45
+ const step = await generator.next();
46
+ if (step.done)
47
+ return step.value;
48
+ try {
49
+ progress?.(step.value);
50
+ }
51
+ catch {
52
+ }
53
+ }
54
+ }
34
55
  export class ToolExecutor {
35
56
  tools = new Map();
36
57
  constructor(tools = []) {
@@ -56,11 +77,13 @@ export class ToolExecutor {
56
77
  if (!toolDefinition) {
57
78
  return { ok: false, error: `Tool "${name}" is not registered` };
58
79
  }
59
- if (!toolDefinition.execute) {
60
- return { ok: false, error: `Tool "${name}" does not have an execute function` };
80
+ if (!toolDefinition.execute && !toolDefinition.stream) {
81
+ return { ok: false, error: `Tool "${name}" does not have an execute or stream function` };
61
82
  }
62
83
  try {
63
- const result = await toolDefinition.execute(args, context);
84
+ const result = toolDefinition.stream
85
+ ? await drain(toolDefinition.stream(args, context), context?.progress)
86
+ : await toolDefinition.execute(args, context);
64
87
  return { ok: true, result };
65
88
  }
66
89
  catch (err) {
@@ -265,6 +265,8 @@ function fromGraphEvent(event) {
265
265
  return [
266
266
  { type: 'custom', step: event.step, node: event.node, taskId: event.taskId, data: event.data, namespace },
267
267
  ];
268
+ case 'step':
269
+ return [];
268
270
  case 'checkpoint':
269
271
  return [{ type: 'checkpoints', step: event.step, status: event.status, next: event.next, namespace }];
270
272
  case 'task_end':
@@ -477,7 +477,10 @@ export class CompiledGraph {
477
477
  this.writers.add(writer);
478
478
  let failed = false;
479
479
  try {
480
- yield* this.steps(threadId, runOptions, seed, (written, durable) => writer.persist(written, durable));
480
+ for await (const event of this.steps(threadId, runOptions, seed, (written, durable) => writer.persist(written, durable))) {
481
+ notify(runOptions, { type: 'step', step: event.step, status: event.status, state: event.state });
482
+ yield event;
483
+ }
481
484
  }
482
485
  catch (error) {
483
486
  failed = true;
@@ -61,6 +61,8 @@ export declare class McpClient {
61
61
  private readonly options;
62
62
  private readonly pending;
63
63
  private nextId;
64
+ /** Where each progress token's reports go, while its tool call runs. */
65
+ private readonly progress;
64
66
  private initialized;
65
67
  private connecting;
66
68
  private serverInfo;
@@ -80,7 +82,13 @@ export declare class McpClient {
80
82
  * Calls a tool. A tool that fails reports `isError` rather than throwing; a protocol failure
81
83
  * throws `McpError`.
82
84
  */
83
- callTool(name: string, args?: Record<string, unknown>): Promise<McpToolResult>;
85
+ callTool(name: string, args?: Record<string, unknown>, options?: {
86
+ progress?: (progress: {
87
+ progress: number;
88
+ total?: number;
89
+ message?: string;
90
+ }) => void;
91
+ }): Promise<McpToolResult>;
84
92
  /** Lists the server's resources. */
85
93
  listResources(): Promise<McpResourceDescriptor[]>;
86
94
  /** Reads a resource's content. */
@@ -1,9 +1,10 @@
1
- import { isRequest, isResponse, LineDecoder, MCP_PROTOCOL_VERSION, McpError, } from './protocol.js';
1
+ import { isNotification, isRequest, isResponse, LineDecoder, MCP_PROTOCOL_VERSION, McpError, } from './protocol.js';
2
2
  export class McpClient {
3
3
  transport;
4
4
  options;
5
5
  pending = new Map();
6
6
  nextId = 1;
7
+ progress = new Map();
7
8
  initialized = false;
8
9
  connecting;
9
10
  serverInfo = {};
@@ -38,10 +39,23 @@ export class McpClient {
38
39
  const result = (await this.request('tools/list'));
39
40
  return result?.tools ?? [];
40
41
  }
41
- async callTool(name, args = {}) {
42
+ async callTool(name, args = {}, options = {}) {
42
43
  await this.connect();
43
- const result = (await this.request('tools/call', { name, arguments: args }));
44
- return { content: result?.content ?? [], ...(result?.isError ? { isError: true } : {}) };
44
+ const token = options.progress ? `progress-${this.nextId}` : undefined;
45
+ if (token && options.progress)
46
+ this.progress.set(token, options.progress);
47
+ try {
48
+ const result = (await this.request('tools/call', {
49
+ name,
50
+ arguments: args,
51
+ ...(token ? { _meta: { progressToken: token } } : {}),
52
+ }));
53
+ return { content: result?.content ?? [], ...(result?.isError ? { isError: true } : {}) };
54
+ }
55
+ finally {
56
+ if (token)
57
+ this.progress.delete(token);
58
+ }
45
59
  }
46
60
  async listResources() {
47
61
  await this.connect();
@@ -69,8 +83,9 @@ export class McpClient {
69
83
  name: options.prefix ? `${options.prefix}_${descriptor.name}` : descriptor.name,
70
84
  description: descriptor.description ?? `MCP tool ${descriptor.name}`,
71
85
  parameters: descriptor.inputSchema ?? { type: 'object', properties: {} },
72
- execute: async (args) => {
73
- const result = await this.callTool(descriptor.name, args);
86
+ execute: async (args, context) => {
87
+ const progress = context?.progress;
88
+ const result = await this.callTool(descriptor.name, args, progress ? { progress } : {});
74
89
  const text = result.content
75
90
  .map((part) => part.text ?? (part.type === 'text' ? '' : JSON.stringify(part)))
76
91
  .filter(Boolean)
@@ -107,6 +122,16 @@ export class McpClient {
107
122
  this.settle(message.id, message.result, message.error ? new McpError(message.error.message, message.error.code, message.error.data) : undefined);
108
123
  return;
109
124
  }
125
+ if (isNotification(message) && message.method === 'notifications/progress') {
126
+ const params = (message.params ?? {});
127
+ const report = this.progress.get(String(params.progressToken));
128
+ report?.({
129
+ progress: typeof params.progress === 'number' ? params.progress : 0,
130
+ ...(typeof params.total === 'number' ? { total: params.total } : {}),
131
+ ...(typeof params.message === 'string' ? { message: params.message } : {}),
132
+ });
133
+ return;
134
+ }
110
135
  if (isRequest(message)) {
111
136
  void this.transport.send({
112
137
  jsonrpc: '2.0',
@@ -221,9 +246,14 @@ export function createStdioTransport(options) {
221
246
  onClose(handler) {
222
247
  onClose = handler;
223
248
  },
224
- close() {
225
- child?.kill();
249
+ async close() {
250
+ const running = child;
226
251
  child = undefined;
252
+ if (!running || running.exitCode !== null || running.signalCode !== null)
253
+ return;
254
+ const exited = new Promise((resolve) => running.once('close', () => resolve()));
255
+ running.kill();
256
+ await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, 2_000).unref?.())]);
227
257
  },
228
258
  };
229
259
  }