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.
- package/API_STABILITY.md +36 -0
- package/CHANGELOG.md +102 -1
- package/README.md +9 -3
- package/dist/agent/create-agent.d.ts +37 -2
- package/dist/agent/create-agent.js +62 -4
- package/dist/agent/index.d.ts +2 -1
- package/dist/agent/tool.d.ts +19 -4
- package/dist/agent/tool.js +27 -4
- package/dist/graph/event-stream.js +2 -0
- package/dist/graph/graph.js +4 -1
- package/dist/mcp/client.d.ts +9 -1
- package/dist/mcp/client.js +38 -8
- package/dist/mcp/server.js +42 -2
- package/dist/protocols/a2a.d.ts +166 -1
- package/dist/protocols/a2a.js +499 -132
- package/dist/protocols/acp-editor.d.ts +43 -0
- package/dist/protocols/acp-editor.js +164 -0
- package/dist/protocols/acp.d.ts +76 -3
- package/dist/protocols/acp.js +180 -7
- package/dist/protocols/ag-ui.d.ts +23 -0
- package/dist/protocols/ag-ui.js +128 -8
- package/dist/tracing/instrument.js +14 -3
- package/dist/types/graph.d.ts +7 -0
- package/dist/types/messages.d.ts +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/agent/create-agent.d.ts +37 -2
- package/dist-cjs/agent/create-agent.js +62 -4
- package/dist-cjs/agent/index.d.ts +2 -1
- package/dist-cjs/agent/tool.d.ts +19 -4
- package/dist-cjs/agent/tool.js +27 -4
- package/dist-cjs/graph/event-stream.js +2 -0
- package/dist-cjs/graph/graph.js +4 -1
- package/dist-cjs/mcp/client.d.ts +9 -1
- package/dist-cjs/mcp/client.js +37 -7
- package/dist-cjs/mcp/server.js +42 -2
- package/dist-cjs/protocols/a2a.d.ts +166 -1
- package/dist-cjs/protocols/a2a.js +501 -132
- package/dist-cjs/protocols/acp-editor.d.ts +43 -0
- package/dist-cjs/protocols/acp-editor.js +167 -0
- package/dist-cjs/protocols/acp.d.ts +76 -3
- package/dist-cjs/protocols/acp.js +183 -8
- package/dist-cjs/protocols/ag-ui.d.ts +23 -0
- package/dist-cjs/protocols/ag-ui.js +127 -7
- package/dist-cjs/tracing/instrument.js +14 -3
- package/dist-cjs/types/graph.d.ts +7 -0
- package/dist-cjs/types/messages.d.ts +12 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +1 -1
- 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.
|
|
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.
|
|
278
|
-
|
|
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
|
-
...(
|
|
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:
|
|
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
|
|
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
|
|
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,
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/agent/tool.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
package/dist/agent/tool.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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':
|
package/dist/graph/graph.js
CHANGED
|
@@ -477,7 +477,10 @@ export class CompiledGraph {
|
|
|
477
477
|
this.writers.add(writer);
|
|
478
478
|
let failed = false;
|
|
479
479
|
try {
|
|
480
|
-
|
|
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;
|
package/dist/mcp/client.d.ts
CHANGED
|
@@ -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
|
|
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. */
|
package/dist/mcp/client.js
CHANGED
|
@@ -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
|
|
44
|
-
|
|
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
|
|
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
|
|
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
|
}
|