@mantyx/sdk 0.2.0 → 0.3.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/CHANGELOG.md +12 -1
- package/README.md +211 -12
- package/dist/a2a-server.cjs +404 -0
- package/dist/a2a-server.cjs.map +1 -0
- package/dist/a2a-server.d.cts +170 -0
- package/dist/a2a-server.d.ts +170 -0
- package/dist/a2a-server.js +344 -0
- package/dist/a2a-server.js.map +1 -0
- package/dist/chunk-ZJINVTHD.js +1080 -0
- package/dist/chunk-ZJINVTHD.js.map +1 -0
- package/dist/client-Ce02_fV8.d.cts +591 -0
- package/dist/client-Ce02_fV8.d.ts +591 -0
- package/dist/index.cjs +590 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -261
- package/dist/index.d.ts +3 -261
- package/dist/index.js +30 -587
- package/dist/index.js.map +1 -1
- package/docs/agent-runs-protocol.md +370 -18
- package/package.json +24 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Add support for a2a, mcp and local versions
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Changelo
|
|
15
|
+
|
|
16
|
+
## [0.2.0] — 2026-05-04
|
|
17
|
+
|
|
8
18
|
### Fixed
|
|
9
19
|
|
|
10
20
|
- Use correct default base url
|
|
@@ -21,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
31
|
|
|
22
32
|
## [0.1.0] — 2026-05-02
|
|
23
33
|
|
|
24
|
-
[unreleased]: https://github.com/mantyx-io/mantyx-sdk/compare/v0.
|
|
34
|
+
[unreleased]: https://github.com/mantyx-io/mantyx-sdk/compare/v0.2.0..HEAD
|
|
35
|
+
[0.2.0]: https://github.com/mantyx-io/mantyx-sdk/compare/v0.1.1..v0.2.0
|
|
25
36
|
[0.1.1]: https://github.com/mantyx-io/mantyx-sdk/compare/v0.1.0..v0.1.1
|
|
26
37
|
[0.1.0]: https://github.com/mantyx-io/mantyx-sdk/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -6,13 +6,20 @@ locally-executed tools, run them remotely, and stream events back into your
|
|
|
6
6
|
process.
|
|
7
7
|
|
|
8
8
|
- LLM loop runs on MANTYX (BYOK or platform-hosted models).
|
|
9
|
-
- Server-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
- Server-resolved tools (`mantyx`, `mantyx_plugin`, `a2a`, `mcp`) execute
|
|
10
|
+
inside MANTYX — including remote Agent2Agent peers and remote MCP servers.
|
|
11
|
+
- Client-resolved tools (`local`, `a2a_local`, `mcp_local`) execute inside
|
|
12
|
+
*your* process; the SDK shuttles inputs and outputs over an SSE stream +
|
|
13
|
+
a tool-result POST.
|
|
14
|
+
- Tunable provider thinking via `reasoningLevel` (string anchors or 0–100).
|
|
12
15
|
- One-shot runs and multi-turn sessions, both with persisted observability.
|
|
13
16
|
- Authenticated with a single workspace API key.
|
|
14
17
|
|
|
15
|
-
For background, see the [agent-runs protocol spec](./docs/agent-runs-protocol.md)
|
|
18
|
+
For background, see the [agent-runs protocol spec](./docs/agent-runs-protocol.md)
|
|
19
|
+
and the messaging-layer reference in [`docs/wire-protocol.md`](./docs/wire-protocol.md)
|
|
20
|
+
— the latter pins down the exact `local_tool_call` event shape and the
|
|
21
|
+
resolved data structures (`a2a_local` Agent Card, `mcp_local` `Tool[]`)
|
|
22
|
+
that this SDK ships.
|
|
16
23
|
|
|
17
24
|
## Install
|
|
18
25
|
|
|
@@ -22,9 +29,11 @@ npm install @mantyx/sdk zod
|
|
|
22
29
|
# or: yarn add @mantyx/sdk zod
|
|
23
30
|
```
|
|
24
31
|
|
|
25
|
-
Requires Node.js 18.17+ (for `fetch` and `ReadableStream`).
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
Requires Node.js 18.17+ (for `fetch` and `ReadableStream`). The SDK depends
|
|
33
|
+
on `zod` (parameter schemas) and `@modelcontextprotocol/sdk` (the official
|
|
34
|
+
MCP TypeScript SDK that powers `defineLocalMcp`'s stdio + Streamable HTTP
|
|
35
|
+
transports). The MCP SDK is loaded lazily — apps that never use
|
|
36
|
+
`defineLocalMcp` don't pay its startup cost.
|
|
28
37
|
|
|
29
38
|
## Quickstart
|
|
30
39
|
|
|
@@ -103,6 +112,190 @@ Notes:
|
|
|
103
112
|
The same `agentId` field works on `client.createSession({ ... })` for
|
|
104
113
|
multi-turn conversations against a persisted agent.
|
|
105
114
|
|
|
115
|
+
## Agent2Agent delegation
|
|
116
|
+
|
|
117
|
+
Hand a turn off to another agent — either a remote peer MANTYX dials directly
|
|
118
|
+
(`mantyxA2A`) or a peer that only the SDK can reach (`defineLocalA2A`). The
|
|
119
|
+
model addresses both with the same `{ message: string }` argument shape, so
|
|
120
|
+
an agent prompt that uses one works unchanged with the other.
|
|
121
|
+
|
|
122
|
+
`defineLocalA2A` is fully URL-driven: pass the Agent Card URL and the SDK
|
|
123
|
+
takes care of the rest — fetching the card on the first run, shipping it
|
|
124
|
+
inline as part of the spec, and POSTing JSON-RPC `message/send` to the
|
|
125
|
+
card's `url` whenever MANTYX emits a `local_tool_call`. You don't write any
|
|
126
|
+
A2A code yourself.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { MantyxClient, defineLocalA2A, mantyxA2A } from "@mantyx/sdk";
|
|
130
|
+
|
|
131
|
+
const client = new MantyxClient({ apiKey: "...", workspaceSlug: "acme" });
|
|
132
|
+
|
|
133
|
+
await client.runAgent({
|
|
134
|
+
systemPrompt: "You are a helpful router. Delegate billing to billing_agent.",
|
|
135
|
+
prompt: "Why was I charged twice last month?",
|
|
136
|
+
tools: [
|
|
137
|
+
// Public peer MANTYX dials directly.
|
|
138
|
+
mantyxA2A({
|
|
139
|
+
name: "billing_agent",
|
|
140
|
+
description: "Delegate billing questions to the Acme billing agent.",
|
|
141
|
+
agentCardUrl: "https://billing.acme.com/.well-known/agent-card.json",
|
|
142
|
+
headers: { Authorization: `Bearer ${process.env.BILLING_TOKEN}` },
|
|
143
|
+
}),
|
|
144
|
+
// Intranet peer the SDK reaches on MANTYX's behalf — URL only.
|
|
145
|
+
defineLocalA2A({
|
|
146
|
+
name: "intranet_hr",
|
|
147
|
+
agentCardUrl: "https://hr.intranet.acme/.well-known/agent-card.json",
|
|
148
|
+
headers: { Authorization: `Bearer ${process.env.HR_TOKEN}` },
|
|
149
|
+
}),
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The same `headers` are sent on both the card fetch *and* every subsequent
|
|
155
|
+
`message/send` POST, which is typically what intranet peers want. The SDK
|
|
156
|
+
caches the resolved card on the tool ref for the duration of the run /
|
|
157
|
+
session — re-construct the ref to force a refetch.
|
|
158
|
+
|
|
159
|
+
> **Headers and secrets.** The `headers` you pass to `mantyxA2A` are forwarded
|
|
160
|
+
> as-is. For long-lived credentials, register the peer as a workspace
|
|
161
|
+
> `ExternalAgent` instead — those headers support `{{secret:NAME}}`
|
|
162
|
+
> placeholders. Use `mantyxA2A` for short-lived, per-run tokens minted by
|
|
163
|
+
> your application.
|
|
164
|
+
|
|
165
|
+
### Exposing an agent over A2A
|
|
166
|
+
|
|
167
|
+
The inverse direction also works: wrap a MANTYX agent (ephemeral spec or a
|
|
168
|
+
persisted `agentId`) and serve it as an Agent2Agent peer using the official
|
|
169
|
+
[`@a2a-js/sdk`](https://www.npmjs.com/package/@a2a-js/sdk) library. Other
|
|
170
|
+
agents can then discover it at `/.well-known/agent-card.json` and call
|
|
171
|
+
`message/send` over JSON-RPC — including MANTYX agents elsewhere in your
|
|
172
|
+
estate consuming this one via `mantyxA2A` or `defineLocalA2A`.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { MantyxClient } from "@mantyx/sdk";
|
|
176
|
+
import { serveAgentOverA2A } from "@mantyx/sdk/a2a-server";
|
|
177
|
+
|
|
178
|
+
const client = new MantyxClient({ apiKey: "...", workspaceSlug: "acme" });
|
|
179
|
+
|
|
180
|
+
const handle = await serveAgentOverA2A({
|
|
181
|
+
client,
|
|
182
|
+
agent: { agentId: "agent_cm6abc123" }, // or { systemPrompt, modelId, tools }
|
|
183
|
+
port: 4000,
|
|
184
|
+
agentCard: {
|
|
185
|
+
name: "Acme Support",
|
|
186
|
+
description: "Customer support questions.",
|
|
187
|
+
protocolVersion: "0.3.0",
|
|
188
|
+
version: "1.0.0",
|
|
189
|
+
url: "http://localhost:4000",
|
|
190
|
+
skills: [{ id: "support", name: "Support", tags: ["support"] }],
|
|
191
|
+
capabilities: { streaming: true, pushNotifications: false },
|
|
192
|
+
defaultInputModes: ["text"],
|
|
193
|
+
defaultOutputModes: ["text"],
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
console.log(`A2A peer up on ${handle.url}`);
|
|
198
|
+
// later: await handle.close();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`@a2a-js/sdk` and `express` are declared as **optional peer dependencies**,
|
|
202
|
+
so apps that don't expose an A2A server pay zero bundle cost. Install them
|
|
203
|
+
on demand:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npm install @a2a-js/sdk express
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Each unique A2A `contextId` opens a long-lived MANTYX session by default, so
|
|
210
|
+
multi-turn `message/send` calls share conversational history. Pass
|
|
211
|
+
`conversation: "stateless"` to reduce every A2A request to a one-shot
|
|
212
|
+
`runAgent` call.
|
|
213
|
+
|
|
214
|
+
For lower-level integration (mounting the executor in your own Express /
|
|
215
|
+
Fastify / Connect app), `@mantyx/sdk/a2a-server` also exports a
|
|
216
|
+
`MantyxAgentExecutor` class implementing `@a2a-js/sdk/server`'s
|
|
217
|
+
`AgentExecutor` interface.
|
|
218
|
+
|
|
219
|
+
## MCP connectors
|
|
220
|
+
|
|
221
|
+
Expose every tool published by an MCP server to the agent loop in one go,
|
|
222
|
+
without listing them individually.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { MantyxClient, mantyxMcp, defineLocalMcp } from "@mantyx/sdk";
|
|
226
|
+
|
|
227
|
+
const client = new MantyxClient({ apiKey: "...", workspaceSlug: "acme" });
|
|
228
|
+
|
|
229
|
+
await client.runAgent({
|
|
230
|
+
systemPrompt: "You are a developer assistant with GitHub + filesystem access.",
|
|
231
|
+
prompt: "Summarize the latest 5 issues on octocat/hello-world.",
|
|
232
|
+
tools: [
|
|
233
|
+
// Remote MCP server (Streamable HTTP) — MANTYX lists the catalog at run
|
|
234
|
+
// start and proxies every call. Tools surface as `github_<tool>`.
|
|
235
|
+
mantyxMcp({
|
|
236
|
+
name: "github",
|
|
237
|
+
url: "https://mcp.github.com/v1",
|
|
238
|
+
headers: { Authorization: `Bearer ${process.env.GH_PAT}` },
|
|
239
|
+
toolFilter: ["search_issues", "get_repo"],
|
|
240
|
+
}),
|
|
241
|
+
// Local MCP server — fully managed by the SDK. Pass either a
|
|
242
|
+
// Streamable HTTP `url` *or* an stdio `command`; the SDK opens the
|
|
243
|
+
// transport, runs `Initialize` + `tools/list`, ships the resolved
|
|
244
|
+
// catalog inline, and forwards every invocation to `tools/call`. The
|
|
245
|
+
// model sees `<server>_<tool>` (`fs_read_file`, `fs_list_dir`, …) —
|
|
246
|
+
// same shape as `mantyxMcp` above.
|
|
247
|
+
|
|
248
|
+
// (a) Streamable HTTP MCP server.
|
|
249
|
+
defineLocalMcp({
|
|
250
|
+
name: "fs",
|
|
251
|
+
url: "http://localhost:8080/mcp",
|
|
252
|
+
headers: { Authorization: `Bearer ${process.env.FS_TOKEN}` },
|
|
253
|
+
}),
|
|
254
|
+
|
|
255
|
+
// (b) stdio MCP server — the SDK spawns the process for you.
|
|
256
|
+
// defineLocalMcp({
|
|
257
|
+
// name: "fs",
|
|
258
|
+
// command: "mcp-server-filesystem",
|
|
259
|
+
// args: ["/workspace"],
|
|
260
|
+
// env: { LOG_LEVEL: "info" },
|
|
261
|
+
// }),
|
|
262
|
+
],
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The MCP transport is opened lazily on the first `runAgent` / first
|
|
267
|
+
`session.send`, kept warm for subsequent calls within the same run /
|
|
268
|
+
session, and closed when the run completes or `session.end()` is called.
|
|
269
|
+
If the MCP server can't be reached, the SDK throws before submitting the
|
|
270
|
+
spec — you get the failure synchronously rather than mid-conversation.
|
|
271
|
+
|
|
272
|
+
If a remote (`kind: "mcp"`) MCP server is unreachable when the run starts,
|
|
273
|
+
MANTYX still exposes a single `<server>_unavailable` stub so the model can
|
|
274
|
+
tell the user why the connector is missing.
|
|
275
|
+
|
|
276
|
+
## Reasoning effort (`reasoningLevel`)
|
|
277
|
+
|
|
278
|
+
Crank up provider thinking on reasoning models without writing
|
|
279
|
+
provider-specific code:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
await client.runAgent({
|
|
283
|
+
systemPrompt: "...",
|
|
284
|
+
prompt: "Plan a multi-week migration.",
|
|
285
|
+
reasoningLevel: "high", // or 80, etc.
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
| Form | Values | Notes |
|
|
290
|
+
| ------------ | -------------------------------------------- | ----- |
|
|
291
|
+
| String | `"off"`, `"low"`, `"medium"`, `"high"` | Snaps to the same anchors the web composer uses (Fast=30, Moderate=50, Smart=80; off=0). |
|
|
292
|
+
| Number | integer `0`–`100` | `0` explicitly disables provider thinking on reasoning models. |
|
|
293
|
+
|
|
294
|
+
The server maps this onto each LLM's native dial — `reasoning.effort` for
|
|
295
|
+
OpenAI, `thinkingConfig` for Gemini, extended-thinking budget for Anthropic.
|
|
296
|
+
Non-reasoning models silently ignore it. On sessions, `reasoningLevel`
|
|
297
|
+
inherits from the session and can be overridden per `session.send`.
|
|
298
|
+
|
|
106
299
|
## Picking a model
|
|
107
300
|
|
|
108
301
|
```ts
|
|
@@ -248,11 +441,15 @@ interface MantyxClientOptions {
|
|
|
248
441
|
|
|
249
442
|
### Tools
|
|
250
443
|
|
|
251
|
-
| Helper | Use case
|
|
252
|
-
| -------------------------- |
|
|
253
|
-
| `defineLocalTool(opts)` | Define a local tool with a Zod parameter schema and handler.
|
|
254
|
-
| `
|
|
255
|
-
| `
|
|
444
|
+
| Helper | Use case |
|
|
445
|
+
| -------------------------- | ----------------------------------------------------------------------- |
|
|
446
|
+
| `defineLocalTool(opts)` | Define a local tool with a Zod parameter schema and handler. |
|
|
447
|
+
| `defineLocalA2A(opts)` | Local Agent2Agent peer — pass an `agentCardUrl`; the SDK fetches the card and speaks `message/send` for you. |
|
|
448
|
+
| `defineLocalMcp(opts)` | Local MCP server — pass either a Streamable HTTP `url` or an stdio `command`; the SDK runs `Initialize` + `tools/list` + `tools/call` for you. |
|
|
449
|
+
| `mantyxTool(id)` | Reference an existing MANTYX tool by id. |
|
|
450
|
+
| `mantyxPluginTool(name)` | Reference an installed platform plugin tool by name. |
|
|
451
|
+
| `mantyxA2A(opts)` | Remote Agent2Agent peer reachable from MANTYX (server-resolved). |
|
|
452
|
+
| `mantyxMcp(opts)` | Remote MCP server (Streamable HTTP) MANTYX dials and proxies for you. |
|
|
256
453
|
|
|
257
454
|
### Errors
|
|
258
455
|
|
|
@@ -272,6 +469,8 @@ Self-contained example projects live under [`examples/`](./examples/):
|
|
|
272
469
|
- `examples/mixed-tools` — combines local, MANTYX, and plugin tools.
|
|
273
470
|
- `examples/streaming` — token streaming to stdout.
|
|
274
471
|
- `examples/list-models` — model catalog + pick-and-run.
|
|
472
|
+
- `examples/a2a-tools` — remote (`mantyxA2A`) + local (`defineLocalA2A`) Agent2Agent peers.
|
|
473
|
+
- `examples/mcp-tools` — remote (`mantyxMcp`) + local (`defineLocalMcp`) MCP servers.
|
|
275
474
|
|
|
276
475
|
Each example is its own project (`package.json`, `tsconfig.json`, `README.md`)
|
|
277
476
|
so you can copy any one of them out of the repo and run it standalone.
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/a2a-server.ts
|
|
31
|
+
var a2a_server_exports = {};
|
|
32
|
+
__export(a2a_server_exports, {
|
|
33
|
+
MantyxAgentExecutor: () => MantyxAgentExecutor,
|
|
34
|
+
serveAgentOverA2A: () => serveAgentOverA2A
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(a2a_server_exports);
|
|
37
|
+
|
|
38
|
+
// src/errors.ts
|
|
39
|
+
var MantyxError = class extends Error {
|
|
40
|
+
code;
|
|
41
|
+
status;
|
|
42
|
+
hint;
|
|
43
|
+
constructor(message, opts = {}) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "MantyxError";
|
|
46
|
+
this.code = opts.code ?? "mantyx_error";
|
|
47
|
+
this.status = opts.status;
|
|
48
|
+
this.hint = opts.hint;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var MantyxRunError = class extends MantyxError {
|
|
52
|
+
runId;
|
|
53
|
+
subtype;
|
|
54
|
+
constructor(runId, subtype, message) {
|
|
55
|
+
super(message, { code: subtype });
|
|
56
|
+
this.name = "MantyxRunError";
|
|
57
|
+
this.runId = runId;
|
|
58
|
+
this.subtype = subtype;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/zod-to-json-schema.ts
|
|
63
|
+
var import_zod = require("zod");
|
|
64
|
+
|
|
65
|
+
// src/a2a-server.ts
|
|
66
|
+
var MantyxAgentExecutor = class {
|
|
67
|
+
client;
|
|
68
|
+
agent;
|
|
69
|
+
conversation;
|
|
70
|
+
maxSessions;
|
|
71
|
+
onAssistantDelta;
|
|
72
|
+
/** contextId -> live MANTYX session. Maintained as an LRU map. */
|
|
73
|
+
sessions = /* @__PURE__ */ new Map();
|
|
74
|
+
/** taskIds we've been asked to cancel; checked between turns. */
|
|
75
|
+
cancelled = /* @__PURE__ */ new Set();
|
|
76
|
+
/** Pending AbortControllers per task, used for cooperative cancel. */
|
|
77
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
78
|
+
constructor(options) {
|
|
79
|
+
if (!options.client) {
|
|
80
|
+
throw new MantyxError("MantyxAgentExecutor: `client` is required");
|
|
81
|
+
}
|
|
82
|
+
validateAgentSpec(options.agent);
|
|
83
|
+
this.client = options.client;
|
|
84
|
+
this.agent = options.agent;
|
|
85
|
+
this.conversation = options.conversation ?? "auto";
|
|
86
|
+
this.maxSessions = options.maxSessions ?? 1024;
|
|
87
|
+
if (options.onAssistantDelta) this.onAssistantDelta = options.onAssistantDelta;
|
|
88
|
+
}
|
|
89
|
+
async execute(requestContext, eventBus) {
|
|
90
|
+
const { userMessage, taskId, contextId, task } = requestContext;
|
|
91
|
+
const userText = extractText(userMessage);
|
|
92
|
+
const abort = new AbortController();
|
|
93
|
+
this.inFlight.set(taskId, abort);
|
|
94
|
+
try {
|
|
95
|
+
if (!task) {
|
|
96
|
+
eventBus.publish({
|
|
97
|
+
kind: "task",
|
|
98
|
+
id: taskId,
|
|
99
|
+
contextId,
|
|
100
|
+
status: { state: "submitted", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
101
|
+
history: [userMessage]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
eventBus.publish(statusUpdate(taskId, contextId, "working", false));
|
|
105
|
+
if (this.cancelled.has(taskId)) {
|
|
106
|
+
eventBus.publish(statusUpdate(taskId, contextId, "canceled", true));
|
|
107
|
+
eventBus.finished();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const onDelta = (delta) => {
|
|
111
|
+
if (this.onAssistantDelta) {
|
|
112
|
+
this.onAssistantDelta(delta, requestContext, eventBus);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
eventBus.publish(deltaStatusUpdate(taskId, contextId, delta));
|
|
116
|
+
};
|
|
117
|
+
let result;
|
|
118
|
+
try {
|
|
119
|
+
result = await this.runOnce(contextId, userText, onDelta, abort.signal);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
eventBus.publish(
|
|
122
|
+
completedStatusUpdate(
|
|
123
|
+
taskId,
|
|
124
|
+
contextId,
|
|
125
|
+
this.cancelled.has(taskId) ? "canceled" : "failed",
|
|
126
|
+
errorText(err)
|
|
127
|
+
)
|
|
128
|
+
);
|
|
129
|
+
eventBus.finished();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
eventBus.publish(completedStatusUpdate(taskId, contextId, "completed", result.text ?? ""));
|
|
133
|
+
eventBus.finished();
|
|
134
|
+
} finally {
|
|
135
|
+
this.inFlight.delete(taskId);
|
|
136
|
+
this.cancelled.delete(taskId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async cancelTask(taskId, eventBus) {
|
|
140
|
+
this.cancelled.add(taskId);
|
|
141
|
+
const ctrl = this.inFlight.get(taskId);
|
|
142
|
+
if (ctrl) ctrl.abort();
|
|
143
|
+
void eventBus;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Close every cached session. Idempotent. Safe to call from server shutdown
|
|
147
|
+
* paths.
|
|
148
|
+
*/
|
|
149
|
+
async close() {
|
|
150
|
+
const sessions = Array.from(this.sessions.values());
|
|
151
|
+
this.sessions.clear();
|
|
152
|
+
await Promise.allSettled(sessions.map((s) => s.end()));
|
|
153
|
+
}
|
|
154
|
+
// -------------------------------------------------- private session helpers
|
|
155
|
+
async runOnce(contextId, prompt, onAssistantDelta, signal) {
|
|
156
|
+
if (this.conversation === "stateless") {
|
|
157
|
+
const runSpec = {
|
|
158
|
+
...specForRun(this.agent),
|
|
159
|
+
prompt,
|
|
160
|
+
onAssistantDelta,
|
|
161
|
+
signal
|
|
162
|
+
};
|
|
163
|
+
return this.client.runAgent(runSpec);
|
|
164
|
+
}
|
|
165
|
+
const session = await this.getOrCreateSession(contextId);
|
|
166
|
+
return session.send(prompt, { onAssistantDelta, signal });
|
|
167
|
+
}
|
|
168
|
+
async getOrCreateSession(contextId) {
|
|
169
|
+
const existing = this.sessions.get(contextId);
|
|
170
|
+
if (existing) {
|
|
171
|
+
this.sessions.delete(contextId);
|
|
172
|
+
this.sessions.set(contextId, existing);
|
|
173
|
+
return existing;
|
|
174
|
+
}
|
|
175
|
+
const sessionSpec = specForSession(this.agent, contextId);
|
|
176
|
+
const session = await this.client.createSession(sessionSpec);
|
|
177
|
+
this.sessions.set(contextId, session);
|
|
178
|
+
await this.evictIfNeeded();
|
|
179
|
+
return session;
|
|
180
|
+
}
|
|
181
|
+
async evictIfNeeded() {
|
|
182
|
+
while (this.sessions.size > this.maxSessions) {
|
|
183
|
+
const oldestKey = this.sessions.keys().next().value;
|
|
184
|
+
if (!oldestKey) break;
|
|
185
|
+
const oldest = this.sessions.get(oldestKey);
|
|
186
|
+
this.sessions.delete(oldestKey);
|
|
187
|
+
try {
|
|
188
|
+
await oldest.end();
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
async function serveAgentOverA2A(options) {
|
|
195
|
+
const a2a = await loadServerSdk();
|
|
196
|
+
const expressMod = await loadExpress();
|
|
197
|
+
const executor = new MantyxAgentExecutor(options);
|
|
198
|
+
const requestHandler = new a2a.DefaultRequestHandler(
|
|
199
|
+
options.agentCard,
|
|
200
|
+
new a2a.InMemoryTaskStore(),
|
|
201
|
+
executor
|
|
202
|
+
);
|
|
203
|
+
const app = expressMod();
|
|
204
|
+
app.use(expressMod.json());
|
|
205
|
+
const cardPath = options.agentCardPath ?? "/.well-known/agent-card.json";
|
|
206
|
+
const jsonRpcPath = options.jsonRpcPath ?? "/";
|
|
207
|
+
const restPath = options.restPath === void 0 ? "/v1" : options.restPath;
|
|
208
|
+
app.use(
|
|
209
|
+
cardPath,
|
|
210
|
+
a2a.expressApp.agentCardHandler({ agentCardProvider: requestHandler })
|
|
211
|
+
);
|
|
212
|
+
if (restPath !== false) {
|
|
213
|
+
app.use(
|
|
214
|
+
restPath,
|
|
215
|
+
a2a.expressApp.restHandler({
|
|
216
|
+
requestHandler,
|
|
217
|
+
userBuilder: a2a.expressApp.UserBuilder.noAuthentication
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
app.use(
|
|
222
|
+
jsonRpcPath,
|
|
223
|
+
a2a.expressApp.jsonRpcHandler({
|
|
224
|
+
requestHandler,
|
|
225
|
+
userBuilder: a2a.expressApp.UserBuilder.noAuthentication
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
const port = options.port ?? 0;
|
|
229
|
+
const host = options.host ?? "0.0.0.0";
|
|
230
|
+
const server = app.listen(port, host);
|
|
231
|
+
await new Promise((resolve, reject) => {
|
|
232
|
+
server.once("listening", resolve);
|
|
233
|
+
server.once("error", reject);
|
|
234
|
+
});
|
|
235
|
+
const address = server.address();
|
|
236
|
+
if (!address || typeof address === "string") {
|
|
237
|
+
server.close();
|
|
238
|
+
throw new MantyxError("serveAgentOverA2A: failed to bind HTTP listener");
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
port: address.port,
|
|
242
|
+
url: `http://${displayHost(host)}:${address.port}`,
|
|
243
|
+
close: async () => {
|
|
244
|
+
await new Promise(
|
|
245
|
+
(resolve, reject) => server.close((err) => err ? reject(err) : resolve())
|
|
246
|
+
);
|
|
247
|
+
await executor.close();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function statusUpdate(taskId, contextId, state, final) {
|
|
252
|
+
return {
|
|
253
|
+
kind: "status-update",
|
|
254
|
+
taskId,
|
|
255
|
+
contextId,
|
|
256
|
+
status: { state, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
257
|
+
final
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function deltaStatusUpdate(taskId, contextId, delta) {
|
|
261
|
+
return {
|
|
262
|
+
kind: "status-update",
|
|
263
|
+
taskId,
|
|
264
|
+
contextId,
|
|
265
|
+
status: {
|
|
266
|
+
state: "working",
|
|
267
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
268
|
+
message: {
|
|
269
|
+
kind: "message",
|
|
270
|
+
messageId: randomMessageId(),
|
|
271
|
+
role: "agent",
|
|
272
|
+
parts: [{ kind: "text", text: delta }],
|
|
273
|
+
contextId,
|
|
274
|
+
taskId
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
final: false
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function completedStatusUpdate(taskId, contextId, state, text) {
|
|
281
|
+
return {
|
|
282
|
+
kind: "status-update",
|
|
283
|
+
taskId,
|
|
284
|
+
contextId,
|
|
285
|
+
status: {
|
|
286
|
+
state,
|
|
287
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
288
|
+
message: {
|
|
289
|
+
kind: "message",
|
|
290
|
+
messageId: randomMessageId(),
|
|
291
|
+
role: "agent",
|
|
292
|
+
parts: [{ kind: "text", text }],
|
|
293
|
+
contextId,
|
|
294
|
+
taskId
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
final: true
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function extractText(message) {
|
|
301
|
+
if (!message) return "";
|
|
302
|
+
const parts = message.parts ?? [];
|
|
303
|
+
const out = [];
|
|
304
|
+
for (const p of parts) {
|
|
305
|
+
if (p.kind === "text") {
|
|
306
|
+
const t = p.text;
|
|
307
|
+
if (typeof t === "string") out.push(t);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return out.join("\n");
|
|
311
|
+
}
|
|
312
|
+
function specForRun(spec) {
|
|
313
|
+
const out = {};
|
|
314
|
+
if (spec.agentId) out.agentId = spec.agentId;
|
|
315
|
+
if (spec.systemPrompt) out.systemPrompt = spec.systemPrompt;
|
|
316
|
+
if (spec.modelId) out.modelId = spec.modelId;
|
|
317
|
+
if (spec.tools) out.tools = spec.tools;
|
|
318
|
+
if (spec.reasoningLevel !== void 0) out.reasoningLevel = spec.reasoningLevel;
|
|
319
|
+
if (spec.metadata) out.metadata = spec.metadata;
|
|
320
|
+
if (spec.budgets) out.budgets = spec.budgets;
|
|
321
|
+
if (spec.name) out.name = spec.name;
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
function specForSession(spec, contextId) {
|
|
325
|
+
const out = {};
|
|
326
|
+
if (spec.agentId) out.agentId = spec.agentId;
|
|
327
|
+
if (spec.systemPrompt) out.systemPrompt = spec.systemPrompt;
|
|
328
|
+
if (spec.modelId) out.modelId = spec.modelId;
|
|
329
|
+
if (spec.tools) out.tools = spec.tools;
|
|
330
|
+
if (spec.reasoningLevel !== void 0) out.reasoningLevel = spec.reasoningLevel;
|
|
331
|
+
const meta = { ...spec.metadata ?? {} };
|
|
332
|
+
if (!meta.a2a_context_id) meta.a2a_context_id = contextId;
|
|
333
|
+
out.metadata = meta;
|
|
334
|
+
if (spec.budgets) out.budgets = spec.budgets;
|
|
335
|
+
if (spec.name) out.name = spec.name;
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
function validateAgentSpec(spec) {
|
|
339
|
+
if (!spec.agentId && (!spec.systemPrompt || spec.systemPrompt.length === 0)) {
|
|
340
|
+
throw new MantyxError(
|
|
341
|
+
"MantyxAgentExecutor: `agent.agentId` or `agent.systemPrompt` is required"
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function errorText(err) {
|
|
346
|
+
if (err instanceof MantyxRunError) {
|
|
347
|
+
return `MANTYX run failed (${err.subtype ?? "unknown"}): ${err.message}`;
|
|
348
|
+
}
|
|
349
|
+
if (err instanceof Error) return err.message;
|
|
350
|
+
try {
|
|
351
|
+
return String(err);
|
|
352
|
+
} catch {
|
|
353
|
+
return "unknown error";
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function randomMessageId() {
|
|
357
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
358
|
+
return globalThis.crypto.randomUUID();
|
|
359
|
+
}
|
|
360
|
+
return `msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
361
|
+
}
|
|
362
|
+
function displayHost(host) {
|
|
363
|
+
if (host === "0.0.0.0" || host === "::") return "localhost";
|
|
364
|
+
return host;
|
|
365
|
+
}
|
|
366
|
+
async function loadExpress() {
|
|
367
|
+
try {
|
|
368
|
+
const mod = await import("express");
|
|
369
|
+
return "default" in mod ? mod.default : mod;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
throw new MantyxError(
|
|
372
|
+
"serveAgentOverA2A: `express` is required but not installed. Run `npm install express @a2a-js/sdk` to enable the A2A server."
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function loadServerSdk() {
|
|
377
|
+
let server;
|
|
378
|
+
let express;
|
|
379
|
+
try {
|
|
380
|
+
server = await import("@a2a-js/sdk/server");
|
|
381
|
+
} catch (err) {
|
|
382
|
+
throw new MantyxError(
|
|
383
|
+
"serveAgentOverA2A: `@a2a-js/sdk` is required but not installed. Run `npm install @a2a-js/sdk express` to enable the A2A server."
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
express = await import("@a2a-js/sdk/server/express");
|
|
388
|
+
} catch (err) {
|
|
389
|
+
throw new MantyxError(
|
|
390
|
+
"serveAgentOverA2A: `@a2a-js/sdk/server/express` could not be loaded; ensure the installed `@a2a-js/sdk` is at least v0.3."
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
DefaultRequestHandler: server.DefaultRequestHandler,
|
|
395
|
+
InMemoryTaskStore: server.InMemoryTaskStore,
|
|
396
|
+
expressApp: express
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
400
|
+
0 && (module.exports = {
|
|
401
|
+
MantyxAgentExecutor,
|
|
402
|
+
serveAgentOverA2A
|
|
403
|
+
});
|
|
404
|
+
//# sourceMappingURL=a2a-server.cjs.map
|