@hanphone/dsh-a2a 0.1.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/LICENSE +21 -0
- package/README.md +196 -0
- package/README.zh.md +195 -0
- package/cordis.patch.yml +11 -0
- package/lib/client.js +166 -0
- package/lib/index.js +2073 -0
- package/lib/tsconfig.client.tsbuildinfo +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/api.d.ts +41 -0
- package/lib/types/api.d.ts.map +1 -0
- package/lib/types/api.js +86 -0
- package/lib/types/api.js.map +1 -0
- package/lib/types/client/index.d.ts +35 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +155 -0
- package/lib/types/client/index.js.map +1 -0
- package/lib/types/commands.d.ts +10 -0
- package/lib/types/commands.d.ts.map +1 -0
- package/lib/types/commands.js +93 -0
- package/lib/types/commands.js.map +1 -0
- package/lib/types/events.d.ts +74 -0
- package/lib/types/events.d.ts.map +1 -0
- package/lib/types/events.js +10 -0
- package/lib/types/events.js.map +1 -0
- package/lib/types/index.d.ts +54 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +296 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/jsonrpc.d.ts +20 -0
- package/lib/types/jsonrpc.d.ts.map +1 -0
- package/lib/types/jsonrpc.js +48 -0
- package/lib/types/jsonrpc.js.map +1 -0
- package/lib/types/outbound/calls.d.ts +37 -0
- package/lib/types/outbound/calls.d.ts.map +1 -0
- package/lib/types/outbound/calls.js +120 -0
- package/lib/types/outbound/calls.js.map +1 -0
- package/lib/types/outbound/registry.d.ts +102 -0
- package/lib/types/outbound/registry.d.ts.map +1 -0
- package/lib/types/outbound/registry.js +249 -0
- package/lib/types/outbound/registry.js.map +1 -0
- package/lib/types/outbound/tools.d.ts +30 -0
- package/lib/types/outbound/tools.d.ts.map +1 -0
- package/lib/types/outbound/tools.js +86 -0
- package/lib/types/outbound/tools.js.map +1 -0
- package/lib/types/protocol.d.ts +194 -0
- package/lib/types/protocol.d.ts.map +1 -0
- package/lib/types/protocol.js +78 -0
- package/lib/types/protocol.js.map +1 -0
- package/lib/types/server/a2a-server.d.ts +98 -0
- package/lib/types/server/a2a-server.d.ts.map +1 -0
- package/lib/types/server/a2a-server.js +397 -0
- package/lib/types/server/a2a-server.js.map +1 -0
- package/lib/types/server/card.d.ts +48 -0
- package/lib/types/server/card.d.ts.map +1 -0
- package/lib/types/server/card.js +75 -0
- package/lib/types/server/card.js.map +1 -0
- package/lib/types/server/exec/agent-runtime.d.ts +126 -0
- package/lib/types/server/exec/agent-runtime.d.ts.map +1 -0
- package/lib/types/server/exec/agent-runtime.js +138 -0
- package/lib/types/server/exec/agent-runtime.js.map +1 -0
- package/lib/types/server/exec/session.d.ts +11 -0
- package/lib/types/server/exec/session.d.ts.map +1 -0
- package/lib/types/server/exec/session.js +22 -0
- package/lib/types/server/exec/session.js.map +1 -0
- package/lib/types/server/exec/subagent.d.ts +39 -0
- package/lib/types/server/exec/subagent.d.ts.map +1 -0
- package/lib/types/server/exec/subagent.js +44 -0
- package/lib/types/server/exec/subagent.js.map +1 -0
- package/lib/types/server/executor.d.ts +63 -0
- package/lib/types/server/executor.d.ts.map +1 -0
- package/lib/types/server/executor.js +38 -0
- package/lib/types/server/executor.js.map +1 -0
- package/lib/types/server/routes.d.ts +32 -0
- package/lib/types/server/routes.d.ts.map +1 -0
- package/lib/types/server/routes.js +131 -0
- package/lib/types/server/routes.js.map +1 -0
- package/lib/types/server/store.d.ts +157 -0
- package/lib/types/server/store.d.ts.map +1 -0
- package/lib/types/server/store.js +239 -0
- package/lib/types/server/store.js.map +1 -0
- package/lib/types/service.d.ts +49 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/service.js +46 -0
- package/lib/types/service.js.map +1 -0
- package/package.json +82 -0
- package/src/api.ts +115 -0
- package/src/client/index.ts +293 -0
- package/src/commands.ts +92 -0
- package/src/events.ts +58 -0
- package/src/index.ts +353 -0
- package/src/jsonrpc.ts +61 -0
- package/src/outbound/calls.ts +145 -0
- package/src/outbound/registry.ts +290 -0
- package/src/outbound/tools.ts +118 -0
- package/src/protocol.ts +212 -0
- package/src/server/a2a-server.ts +455 -0
- package/src/server/card.ts +106 -0
- package/src/server/exec/agent-runtime.ts +197 -0
- package/src/server/exec/session.ts +25 -0
- package/src/server/exec/subagent.ts +67 -0
- package/src/server/executor.ts +73 -0
- package/src/server/routes.ts +156 -0
- package/src/server/store.ts +320 -0
- package/src/service.ts +78 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,2073 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { mkdirSync } from "node:fs";
|
|
5
|
+
import { z as z$1 } from "zod";
|
|
6
|
+
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
7
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
8
|
+
//#region lib/types/server/card.js
|
|
9
|
+
/**
|
|
10
|
+
* AgentCard assembly for the inbound half: skills are DERIVED from the live
|
|
11
|
+
* tool registry by an explicit id list (never invented), plus a built-in
|
|
12
|
+
* `chat` skill so a fresh install is immediately exercisable end-to-end.
|
|
13
|
+
* @module dsh-a2a/server/card
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Derive the inbound skill list from the live tool registry.
|
|
17
|
+
*
|
|
18
|
+
* Every configured id must resolve to a registered tool — a missing referent
|
|
19
|
+
* fails the derivation loudly (the plugin's misconfiguration contract) with
|
|
20
|
+
* all missing ids listed. The built-in `chat` skill is always present so the
|
|
21
|
+
* server answers before any tool is exposed.
|
|
22
|
+
*
|
|
23
|
+
* @param tools - the tool registry getter.
|
|
24
|
+
* @param policy - explicit-id derivation policy.
|
|
25
|
+
* @returns the derived skills, `chat` first, configured tools after, excludes applied.
|
|
26
|
+
*/
|
|
27
|
+
function deriveSkills(tools, policy) {
|
|
28
|
+
const excluded = new Set(policy.exclude);
|
|
29
|
+
const skills = [{
|
|
30
|
+
id: "chat",
|
|
31
|
+
name: "chat",
|
|
32
|
+
description: "Conversational assistance over a DSH agent session.",
|
|
33
|
+
tags: ["chat"]
|
|
34
|
+
}];
|
|
35
|
+
const missing = [];
|
|
36
|
+
for (const id of policy.ids) {
|
|
37
|
+
if (excluded.has(id)) continue;
|
|
38
|
+
const tool = tools.get(id);
|
|
39
|
+
if (tool === void 0) {
|
|
40
|
+
missing.push(id);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
skills.push({
|
|
44
|
+
id,
|
|
45
|
+
name: id,
|
|
46
|
+
description: tool.description ?? `Expose the DSH tool \`${id}\` over A2A.`,
|
|
47
|
+
tags: ["tool"]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (missing.length > 0) throw new Error(`dsh-a2a: server.skills.ids reference unregistered tools: ${missing.join(", ")}`);
|
|
51
|
+
return skills;
|
|
52
|
+
}
|
|
53
|
+
/** Assemble the AgentCard advertised by this DSH. */
|
|
54
|
+
function buildCard(options) {
|
|
55
|
+
const iface = {
|
|
56
|
+
url: `${options.baseUrl.replace(/\/$/, "")}${options.endpointPath}`,
|
|
57
|
+
protocolBinding: "JSONRPC",
|
|
58
|
+
protocolVersion: "1.0"
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
name: options.name,
|
|
62
|
+
description: options.description,
|
|
63
|
+
version: options.version,
|
|
64
|
+
provider: {
|
|
65
|
+
url: "https://deepseek.com",
|
|
66
|
+
organization: "DeepSeek"
|
|
67
|
+
},
|
|
68
|
+
capabilities: {
|
|
69
|
+
streaming: true,
|
|
70
|
+
pushNotifications: false,
|
|
71
|
+
extensions: []
|
|
72
|
+
},
|
|
73
|
+
defaultInputModes: ["text/plain"],
|
|
74
|
+
defaultOutputModes: ["text/plain"],
|
|
75
|
+
skills: options.skills,
|
|
76
|
+
supportedInterfaces: [iface],
|
|
77
|
+
...options.authToken !== void 0 ? {
|
|
78
|
+
securitySchemes: { bearerAuth: {
|
|
79
|
+
type: "http",
|
|
80
|
+
scheme: "bearer",
|
|
81
|
+
description: "Shared bearer token"
|
|
82
|
+
} },
|
|
83
|
+
securityRequirements: [{ bearerAuth: [] }]
|
|
84
|
+
} : {}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region lib/types/protocol.js
|
|
89
|
+
/**
|
|
90
|
+
* Agent2Agent (A2A) Protocol v1.0 — types, task states, JSON-RPC methods and
|
|
91
|
+
* error codes used by this plugin's JSON-RPC over HTTP binding.
|
|
92
|
+
*
|
|
93
|
+
* The surface mirrors the normative `a2a.proto` of the A2A project (Apache-2.0);
|
|
94
|
+
* this file is the plugin's own transcription and the single source of protocol
|
|
95
|
+
* truth. Bindings other than JSON-RPC over HTTP (gRPC, REST) are not provided.
|
|
96
|
+
* @module dsh-a2a/protocol
|
|
97
|
+
*/
|
|
98
|
+
/** Task lifecycle states (spec `TaskState`). */
|
|
99
|
+
var TaskState;
|
|
100
|
+
(function(TaskState) {
|
|
101
|
+
TaskState["SUBMITTED"] = "SUBMITTED";
|
|
102
|
+
TaskState["WORKING"] = "WORKING";
|
|
103
|
+
TaskState["INPUT_REQUIRED"] = "INPUT_REQUIRED";
|
|
104
|
+
TaskState["AUTH_REQUIRED"] = "AUTH_REQUIRED";
|
|
105
|
+
TaskState["COMPLETED"] = "COMPLETED";
|
|
106
|
+
TaskState["FAILED"] = "FAILED";
|
|
107
|
+
TaskState["CANCELED"] = "CANCELED";
|
|
108
|
+
TaskState["REJECTED"] = "REJECTED";
|
|
109
|
+
})(TaskState || (TaskState = {}));
|
|
110
|
+
/** States that settle a task; a settled task no longer transitions. */
|
|
111
|
+
const TERMINAL_STATES = new Set([
|
|
112
|
+
TaskState.COMPLETED,
|
|
113
|
+
TaskState.FAILED,
|
|
114
|
+
TaskState.CANCELED,
|
|
115
|
+
TaskState.REJECTED
|
|
116
|
+
]);
|
|
117
|
+
/** @returns whether the state settles the task. */
|
|
118
|
+
function isTerminal(state) {
|
|
119
|
+
return TERMINAL_STATES.has(state);
|
|
120
|
+
}
|
|
121
|
+
/** Message sender role (spec `Role`). */
|
|
122
|
+
var Role;
|
|
123
|
+
(function(Role) {
|
|
124
|
+
Role["USER"] = "user";
|
|
125
|
+
Role["AGENT"] = "agent";
|
|
126
|
+
})(Role || (Role = {}));
|
|
127
|
+
/** A2A v1.0 JSON-RPC method names (canonical PascalCase as in a2a.proto). */
|
|
128
|
+
const A2A_METHODS = {
|
|
129
|
+
sendMessage: "SendMessage",
|
|
130
|
+
sendStreamingMessage: "SendStreamingMessage",
|
|
131
|
+
getTask: "GetTask",
|
|
132
|
+
listTasks: "ListTasks",
|
|
133
|
+
cancelTask: "CancelTask",
|
|
134
|
+
subscribeToTask: "SubscribeToTask",
|
|
135
|
+
getExtendedAgentCard: "GetExtendedAgentCard"
|
|
136
|
+
};
|
|
137
|
+
/** JSON-RPC error codes for the A2A binding (spec §error codes). */
|
|
138
|
+
const A2A_ERROR_CODES = {
|
|
139
|
+
INVALID_REQUEST: -32600,
|
|
140
|
+
METHOD_NOT_FOUND: -32601,
|
|
141
|
+
INVALID_PARAMS: -32602,
|
|
142
|
+
INTERNAL_ERROR: -32603,
|
|
143
|
+
UNAUTHORIZED: -32e3,
|
|
144
|
+
TASK_NOT_FOUND: -32001,
|
|
145
|
+
TASK_CANCEL_NOT_ALLOWED: -32002,
|
|
146
|
+
AGENT_CARD_NOT_FOUND: -32004,
|
|
147
|
+
AGENT_CARD_SIGNATURE_INVALID: -32005
|
|
148
|
+
};
|
|
149
|
+
/** Read a message's text parts as a single string (model-facing convenience). */
|
|
150
|
+
function partsToText(parts) {
|
|
151
|
+
if (!parts) return "";
|
|
152
|
+
return parts.map((p) => {
|
|
153
|
+
if ("text" in p && p.text) return p.text;
|
|
154
|
+
if ("data" in p && p.data !== void 0) return JSON.stringify(p.data);
|
|
155
|
+
if ("file" in p) return p.file.uri ?? `[file ${p.file.name ?? p.file.mimeType ?? "binary"}]`;
|
|
156
|
+
return "";
|
|
157
|
+
}).filter(Boolean).join("\n");
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region lib/types/server/store.js
|
|
161
|
+
/**
|
|
162
|
+
* Durable task storage for the inbound half: one `a2a` domain over the routed
|
|
163
|
+
* storage backend (the deployment picks the medium, normally SQLite). Records
|
|
164
|
+
* are serialized JSON so the domain tables stay plain kv rows; the domain
|
|
165
|
+
* SCHEMA_VERSION guards format evolution. A memory store keeps unit tests and
|
|
166
|
+
* minimal embeddings free of a backend.
|
|
167
|
+
* @module dsh-a2a/server/store
|
|
168
|
+
*/
|
|
169
|
+
/** JSON-encoded record schema shared by every `a2a` table. */
|
|
170
|
+
const json$1 = z$1.string();
|
|
171
|
+
/** The `a2a` storage domain: JSON-encoded records, one table per record kind. */
|
|
172
|
+
const a2aDomainSpec = defineDomain({
|
|
173
|
+
name: "a2a",
|
|
174
|
+
version: 1,
|
|
175
|
+
tables: {
|
|
176
|
+
tasks: domainTable(json$1),
|
|
177
|
+
contexts: domainTable(json$1),
|
|
178
|
+
agents: domainTable(json$1)
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
/**
|
|
182
|
+
* Serialize/deserialize helpers for the domain tables. Plain JSON round-trip
|
|
183
|
+
* with shape assertions; `decodeRecord` returns undefined on any mismatch.
|
|
184
|
+
*/
|
|
185
|
+
function encodeRecord(value) {
|
|
186
|
+
return JSON.stringify(value);
|
|
187
|
+
}
|
|
188
|
+
function decodeRecord(raw) {
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(raw);
|
|
191
|
+
} catch {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Open the `a2a` domain on the mounted storage-domain facility and expose
|
|
197
|
+
* its tables. The caller owns the returned handle and closes it on teardown.
|
|
198
|
+
*/
|
|
199
|
+
async function openDomain(domain) {
|
|
200
|
+
return new A2aDomain(await domain.open(a2aDomainSpec));
|
|
201
|
+
}
|
|
202
|
+
/** A2a domain facade: typed JSON tables over the routed backend. */
|
|
203
|
+
var A2aDomain = class {
|
|
204
|
+
handle;
|
|
205
|
+
constructor(handle) {
|
|
206
|
+
this.handle = handle;
|
|
207
|
+
}
|
|
208
|
+
get tasks() {
|
|
209
|
+
return this.handle.table("tasks");
|
|
210
|
+
}
|
|
211
|
+
get contexts() {
|
|
212
|
+
return this.handle.table("contexts");
|
|
213
|
+
}
|
|
214
|
+
get agents() {
|
|
215
|
+
return this.handle.table("agents");
|
|
216
|
+
}
|
|
217
|
+
async close() {
|
|
218
|
+
await this.handle.close();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
function now() {
|
|
222
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
223
|
+
}
|
|
224
|
+
function recordKey(taskId) {
|
|
225
|
+
return `task:${taskId}`;
|
|
226
|
+
}
|
|
227
|
+
function contextKey(contextId) {
|
|
228
|
+
return `ctx:${contextId}`;
|
|
229
|
+
}
|
|
230
|
+
function appendArtifactRecord(artifacts, artifact) {
|
|
231
|
+
if (artifacts.find((a) => a.artifactId === artifact.artifactId)) return artifacts.map((a) => a.artifactId === artifact.artifactId ? {
|
|
232
|
+
...a,
|
|
233
|
+
parts: [...a.parts, ...artifact.parts]
|
|
234
|
+
} : a);
|
|
235
|
+
return [...artifacts, {
|
|
236
|
+
artifactId: artifact.artifactId,
|
|
237
|
+
...artifact.name !== void 0 ? { name: artifact.name } : {},
|
|
238
|
+
parts: artifact.parts
|
|
239
|
+
}];
|
|
240
|
+
}
|
|
241
|
+
/** Domain-backed TaskStore (persistent; the composition path). */
|
|
242
|
+
var DomainTaskStore = class {
|
|
243
|
+
domain;
|
|
244
|
+
records = /* @__PURE__ */ new Map();
|
|
245
|
+
bindings = /* @__PURE__ */ new Map();
|
|
246
|
+
constructor(domain) {
|
|
247
|
+
this.domain = domain;
|
|
248
|
+
for (const [key, raw] of domain.tasks.entries()) {
|
|
249
|
+
if (!key.startsWith("task:")) continue;
|
|
250
|
+
const record = decodeRecord(raw);
|
|
251
|
+
if (record !== void 0) this.records.set(record.taskId, record);
|
|
252
|
+
}
|
|
253
|
+
for (const [key, raw] of domain.contexts.entries()) {
|
|
254
|
+
if (!key.startsWith("ctx:")) continue;
|
|
255
|
+
const binding = decodeRecord(raw);
|
|
256
|
+
if (binding !== void 0) this.bindings.set(key.slice(4), binding.sessionId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
create(input) {
|
|
260
|
+
const record = {
|
|
261
|
+
taskId: `a2a-${crypto.randomUUID()}`,
|
|
262
|
+
contextId: input.contextId,
|
|
263
|
+
skill: input.skill,
|
|
264
|
+
state: TaskState.SUBMITTED,
|
|
265
|
+
createdAt: now(),
|
|
266
|
+
updatedAt: now(),
|
|
267
|
+
sessionId: null,
|
|
268
|
+
remotePeerId: input.remotePeerId,
|
|
269
|
+
parts: input.parts,
|
|
270
|
+
artifacts: [],
|
|
271
|
+
executor: "session",
|
|
272
|
+
summary: null
|
|
273
|
+
};
|
|
274
|
+
this.records.set(record.taskId, record);
|
|
275
|
+
this.domain.tasks.put(recordKey(record.taskId), encodeRecord(record)).catch((err) => {
|
|
276
|
+
throw new Error(`a2a: task persistence failed: ${String(err)}`);
|
|
277
|
+
});
|
|
278
|
+
return record;
|
|
279
|
+
}
|
|
280
|
+
get(taskId) {
|
|
281
|
+
return this.records.get(taskId);
|
|
282
|
+
}
|
|
283
|
+
list() {
|
|
284
|
+
return [...this.records.values()];
|
|
285
|
+
}
|
|
286
|
+
setState(taskId, state, message) {
|
|
287
|
+
const current = this.records.get(taskId);
|
|
288
|
+
if (current === void 0) throw new Error(`task ${taskId} not found`);
|
|
289
|
+
const next = withState(current, state, message, now());
|
|
290
|
+
this.records.set(taskId, next);
|
|
291
|
+
this.domain.tasks.put(recordKey(taskId), encodeRecord(next)).catch((err) => {
|
|
292
|
+
throw new Error(`a2a: task persistence failed: ${String(err)}`);
|
|
293
|
+
});
|
|
294
|
+
return next;
|
|
295
|
+
}
|
|
296
|
+
appendArtifact(taskId, artifact) {
|
|
297
|
+
const current = this.records.get(taskId);
|
|
298
|
+
if (current === void 0) throw new Error(`task ${taskId} not found`);
|
|
299
|
+
const next = {
|
|
300
|
+
...current,
|
|
301
|
+
updatedAt: now(),
|
|
302
|
+
artifacts: appendArtifactRecord(current.artifacts, artifact)
|
|
303
|
+
};
|
|
304
|
+
this.records.set(taskId, next);
|
|
305
|
+
this.domain.tasks.put(recordKey(taskId), encodeRecord(next)).catch((err) => {
|
|
306
|
+
throw new Error(`a2a: task persistence failed: ${String(err)}`);
|
|
307
|
+
});
|
|
308
|
+
return next;
|
|
309
|
+
}
|
|
310
|
+
async setContextSession(contextId, sessionId) {
|
|
311
|
+
const binding = { sessionId };
|
|
312
|
+
this.bindings.set(contextId, sessionId);
|
|
313
|
+
await this.domain.contexts.put(contextKey(contextId), encodeRecord(binding));
|
|
314
|
+
}
|
|
315
|
+
async getContextSession(contextId) {
|
|
316
|
+
return this.bindings.get(contextId);
|
|
317
|
+
}
|
|
318
|
+
async close() {}
|
|
319
|
+
};
|
|
320
|
+
function withState(current, state, message, timestamp) {
|
|
321
|
+
return {
|
|
322
|
+
...current,
|
|
323
|
+
state,
|
|
324
|
+
updatedAt: timestamp,
|
|
325
|
+
...message ? { error: {
|
|
326
|
+
code: message.code,
|
|
327
|
+
message: message.text
|
|
328
|
+
} } : {}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region lib/types/server/executor.js
|
|
333
|
+
/**
|
|
334
|
+
* Executor abstraction for inbound tasks: an executor turns one task into
|
|
335
|
+
* output parts while reporting status and artifact updates. The registry is
|
|
336
|
+
* INTERNAL (built by the plugin from config) — public registration was cut:
|
|
337
|
+
* there is no external caller yet, so the seam stays an interface with two
|
|
338
|
+
* built-in implementations (session, subagent).
|
|
339
|
+
* @module dsh-a2a/server/executor
|
|
340
|
+
*/
|
|
341
|
+
/**
|
|
342
|
+
* Skill → executor resolution. The subagent executor is absent on
|
|
343
|
+
* compositions without the subagent seam; resolving a skill bound to a
|
|
344
|
+
* missing kind fails loudly with a readable message.
|
|
345
|
+
*/
|
|
346
|
+
var ExecutorSet = class {
|
|
347
|
+
bySkill;
|
|
348
|
+
session;
|
|
349
|
+
subagent;
|
|
350
|
+
constructor(bySkill, session, subagent) {
|
|
351
|
+
this.bySkill = bySkill;
|
|
352
|
+
this.session = session;
|
|
353
|
+
this.subagent = subagent;
|
|
354
|
+
}
|
|
355
|
+
resolve(skill) {
|
|
356
|
+
if ((this.bySkill[skill] ?? "session") === "session") return this.session;
|
|
357
|
+
const subagent = this.subagent;
|
|
358
|
+
if (subagent === void 0) throw new Error(`a2a: skill "${skill}" is bound to the subagent executor but no subagent seam is mounted`);
|
|
359
|
+
return subagent;
|
|
360
|
+
}
|
|
361
|
+
async disposeAll() {
|
|
362
|
+
await this.session.disposeAll?.();
|
|
363
|
+
await this.subagent?.disposeAll?.();
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region lib/types/server/exec/agent-runtime.js
|
|
368
|
+
/**
|
|
369
|
+
* Shared agent machinery for the inbound executors: one DSH agent session per
|
|
370
|
+
* A2A context (create, or resume a persisted one), serialized turns, and
|
|
371
|
+
* optional agent-preset composition so a spawned session actually carries
|
|
372
|
+
* tools. Adapted from the createDshAgentExecutor pattern pioneered by
|
|
373
|
+
* dsh-a2a (MIT).
|
|
374
|
+
* @module dsh-a2a/server/exec/agent-runtime
|
|
375
|
+
*/
|
|
376
|
+
/**
|
|
377
|
+
* Per-context session pool: one live DSH agent per A2A contextId, created or
|
|
378
|
+
* resumed through the registry, turns serialized per context, every session
|
|
379
|
+
* disposeable on teardown.
|
|
380
|
+
*/
|
|
381
|
+
var ContextSessionPool = class ContextSessionPool {
|
|
382
|
+
agents;
|
|
383
|
+
opts;
|
|
384
|
+
handles = /* @__PURE__ */ new Map();
|
|
385
|
+
tails = /* @__PURE__ */ new Map();
|
|
386
|
+
constructor(agents, opts) {
|
|
387
|
+
this.agents = agents;
|
|
388
|
+
this.opts = opts;
|
|
389
|
+
}
|
|
390
|
+
/** Session id derived from the A2A context id (stable across restarts). */
|
|
391
|
+
static sessionIdFor(contextId) {
|
|
392
|
+
return `a2a-${contextId}`;
|
|
393
|
+
}
|
|
394
|
+
/** Get or open the live agent for a context; errors propagate. */
|
|
395
|
+
async agentFor(contextId) {
|
|
396
|
+
const sessionId = ContextSessionPool.sessionIdFor(contextId);
|
|
397
|
+
const existing = this.handles.get(contextId);
|
|
398
|
+
if (existing !== void 0) return {
|
|
399
|
+
agent: existing.agent,
|
|
400
|
+
sessionId,
|
|
401
|
+
justOpened: false
|
|
402
|
+
};
|
|
403
|
+
const agentOptions = this.opts.resolveAgentOptions?.();
|
|
404
|
+
const withModel = agentOptions ? { agentOptions } : {};
|
|
405
|
+
const presets = this.opts.agentPresets;
|
|
406
|
+
let presetId;
|
|
407
|
+
let setup;
|
|
408
|
+
if (presets !== void 0) {
|
|
409
|
+
presetId = (await presets.resolve()).id;
|
|
410
|
+
setup = async (agentCtx) => {
|
|
411
|
+
await presets.mount(agentCtx, presetId);
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const createOptions = {
|
|
415
|
+
sessionId,
|
|
416
|
+
meta: presetId ? {
|
|
417
|
+
cwd: this.opts.cwd,
|
|
418
|
+
agentPreset: presetId
|
|
419
|
+
} : { cwd: this.opts.cwd },
|
|
420
|
+
...withModel,
|
|
421
|
+
...setup ? { setup } : {}
|
|
422
|
+
};
|
|
423
|
+
let handle;
|
|
424
|
+
try {
|
|
425
|
+
handle = await this.agents.create(createOptions);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
if (/already (has|owns)|persisted/i.test(err.message)) handle = await this.agents.resume({
|
|
428
|
+
resumeSessionId: sessionId,
|
|
429
|
+
...withModel,
|
|
430
|
+
...setup ? { setup } : {}
|
|
431
|
+
});
|
|
432
|
+
else throw err;
|
|
433
|
+
}
|
|
434
|
+
this.handles.set(contextId, handle);
|
|
435
|
+
return {
|
|
436
|
+
agent: handle.agent,
|
|
437
|
+
sessionId,
|
|
438
|
+
justOpened: true
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/** Run one turn on a context's session, serialized behind its tail. */
|
|
442
|
+
async runTurn(contextId, prompt, signal) {
|
|
443
|
+
const { agent, sessionId, justOpened } = await this.agentFor(contextId);
|
|
444
|
+
const onAbort = () => agent.cancel("a2a-canceled");
|
|
445
|
+
if (signal.aborted) onAbort();
|
|
446
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
447
|
+
const userMessage = {
|
|
448
|
+
id: crypto.randomUUID(),
|
|
449
|
+
role: "user",
|
|
450
|
+
content: [{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: prompt
|
|
453
|
+
}],
|
|
454
|
+
source: {
|
|
455
|
+
kind: "plugin",
|
|
456
|
+
plugin: "a2a"
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
agent.send(userMessage, "next-turn", true);
|
|
460
|
+
if (justOpened) try {
|
|
461
|
+
await this.opts.onSessionOpened?.({
|
|
462
|
+
sessionId,
|
|
463
|
+
contextId,
|
|
464
|
+
firstPrompt: prompt
|
|
465
|
+
});
|
|
466
|
+
} catch {}
|
|
467
|
+
await agent.whenIdle();
|
|
468
|
+
return lastAssistantText(agent);
|
|
469
|
+
}
|
|
470
|
+
/** Dispose every live per-context session (called on plugin unload). */
|
|
471
|
+
async disposeAll() {
|
|
472
|
+
const live = [...this.handles.values()];
|
|
473
|
+
const pending = [...this.tails.values()];
|
|
474
|
+
this.handles.clear();
|
|
475
|
+
this.tails.clear();
|
|
476
|
+
await Promise.allSettled(pending);
|
|
477
|
+
await Promise.allSettled(live.map((h) => h.dispose()));
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
/** Read the last assistant message's text off the derived history. */
|
|
481
|
+
function lastAssistantText(agent) {
|
|
482
|
+
const history = agent.session.deriveMessages();
|
|
483
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
484
|
+
const message = history[i];
|
|
485
|
+
if (message === void 0) continue;
|
|
486
|
+
if (message.role === "assistant") return message.content.map((b) => "text" in b && typeof b.text === "string" ? b.text ?? "" : "").filter(Boolean).join("\n").trim();
|
|
487
|
+
}
|
|
488
|
+
return "";
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Probe for a readable service method via the reflection layer WITHOUT taking
|
|
492
|
+
* an inject dependency (optional services). Returns undefined when the service
|
|
493
|
+
* is absent or the method missing.
|
|
494
|
+
*/
|
|
495
|
+
function probeService(ctx, name, method) {
|
|
496
|
+
try {
|
|
497
|
+
const svc = ctx.reflect?.get(name, true);
|
|
498
|
+
if (svc !== void 0 && svc !== null && typeof svc[method] === "function") return svc;
|
|
499
|
+
} catch {}
|
|
500
|
+
}
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region lib/types/server/exec/session.js
|
|
503
|
+
/**
|
|
504
|
+
* The `session` executor: one inbound task is one turn on the context's DSH
|
|
505
|
+
* agent session; reply text becomes the task's output part. Works on any
|
|
506
|
+
* composition with an agent loop (the `chat` skill and the default path).
|
|
507
|
+
* @module dsh-a2a/server/exec/session
|
|
508
|
+
*/
|
|
509
|
+
/** Build the session executor over a context session pool. */
|
|
510
|
+
function createSessionExecutor(pool) {
|
|
511
|
+
return {
|
|
512
|
+
name: "session",
|
|
513
|
+
async execute(input) {
|
|
514
|
+
if (!input.prompt) return { parts: [{ text: "No prompt provided." }] };
|
|
515
|
+
const reply = await pool.runTurn(input.contextId, input.prompt, input.signal);
|
|
516
|
+
return { parts: [{ text: (input.signal.aborted && !reply ? "(canceled)" : reply) || "(no reply)" }] };
|
|
517
|
+
},
|
|
518
|
+
disposeAll: () => pool.disposeAll()
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region lib/types/server/exec/subagent.js
|
|
523
|
+
/**
|
|
524
|
+
* The `subagent` executor: one inbound task is delegated to a named one-shot
|
|
525
|
+
* subagent under the context's parent session. The child runs with the
|
|
526
|
+
* composition's own tools and the delegation boundary; its output becomes the
|
|
527
|
+
* task's artifact. The subagent seam must be mounted, else the skill binding
|
|
528
|
+
* fails loudly at resolution time.
|
|
529
|
+
* @module dsh-a2a/server/exec/subagent
|
|
530
|
+
*/
|
|
531
|
+
/** Build a subagent executor delegating inbound tasks to one-shot children. */
|
|
532
|
+
function createSubagentExecutor(opts) {
|
|
533
|
+
const { pool, subagents, provider } = opts;
|
|
534
|
+
return {
|
|
535
|
+
name: "subagent",
|
|
536
|
+
async execute(input, execCtx) {
|
|
537
|
+
if (!input.prompt) return { parts: [{ text: "No prompt provided." }] };
|
|
538
|
+
const { agent } = await pool.agentFor(input.contextId);
|
|
539
|
+
const text = (await (await subagents.start(provider, {
|
|
540
|
+
label: `a2a:${input.skill}`,
|
|
541
|
+
prompt: [{
|
|
542
|
+
type: "text",
|
|
543
|
+
text: input.prompt
|
|
544
|
+
}],
|
|
545
|
+
parent: agent,
|
|
546
|
+
signal: input.signal
|
|
547
|
+
})).result).output.filter((b) => typeof b.text === "string").map((b) => b.text).filter(Boolean).join("\n").trim();
|
|
548
|
+
execCtx.onEvent({
|
|
549
|
+
type: "artifact",
|
|
550
|
+
artifactId: "result",
|
|
551
|
+
parts: [{ text: text || "(subagent returned no output)" }],
|
|
552
|
+
name: input.skill,
|
|
553
|
+
lastChunk: true
|
|
554
|
+
});
|
|
555
|
+
return { parts: [{ text: text || "(subagent returned no output)" }] };
|
|
556
|
+
},
|
|
557
|
+
disposeAll: () => pool.disposeAll()
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region lib/types/jsonrpc.js
|
|
562
|
+
/**
|
|
563
|
+
* JSON-RPC 2.0 helpers and SSE framing for the A2A binding. Transport-agnostic:
|
|
564
|
+
* the A2AServer consumes these, and the HTTP route layer only maps requests to
|
|
565
|
+
* `handle` / `handleStream` and bytes back.
|
|
566
|
+
* @module dsh-a2a/jsonrpc
|
|
567
|
+
*/
|
|
568
|
+
/** Parse a request body into a JSON-RPC request, or undefined on malformed input. */
|
|
569
|
+
function parseRpc(body) {
|
|
570
|
+
let value;
|
|
571
|
+
try {
|
|
572
|
+
value = JSON.parse(body);
|
|
573
|
+
} catch {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
577
|
+
const rpc = value;
|
|
578
|
+
if (rpc.jsonrpc !== "2.0" || typeof rpc.method !== "string") return void 0;
|
|
579
|
+
return rpc;
|
|
580
|
+
}
|
|
581
|
+
/** Build a JSON-RPC error response object. */
|
|
582
|
+
function rpcError(id, code, message, data) {
|
|
583
|
+
return {
|
|
584
|
+
jsonrpc: "2.0",
|
|
585
|
+
id: id ?? null,
|
|
586
|
+
error: {
|
|
587
|
+
code,
|
|
588
|
+
message,
|
|
589
|
+
...data === void 0 ? {} : { data }
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/** Build a JSON-RPC success response object. */
|
|
594
|
+
function rpcSuccess(id, result) {
|
|
595
|
+
return {
|
|
596
|
+
jsonrpc: "2.0",
|
|
597
|
+
id: id ?? null,
|
|
598
|
+
result
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
/** Invalid-method error for the SSE path (no request id on the wire). */
|
|
602
|
+
function methodNotFound(method) {
|
|
603
|
+
return rpcError(null, A2A_ERROR_CODES.METHOD_NOT_FOUND, `Unknown method ${method}`);
|
|
604
|
+
}
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region lib/types/server/a2a-server.js
|
|
607
|
+
/**
|
|
608
|
+
* The inbound A2A server: JSON-RPC dispatch, the task lifecycle, and SSE
|
|
609
|
+
* streaming. Transport-agnostic — `handle` / `handleStream` take a structural
|
|
610
|
+
* request and return bytes; the HTTP binding lives in routes.ts.
|
|
611
|
+
* @module dsh-a2a/server/a2a-server
|
|
612
|
+
*/
|
|
613
|
+
/** A2A JSON-RPC / SSE server over a structural request. */
|
|
614
|
+
var A2AServer = class {
|
|
615
|
+
opts;
|
|
616
|
+
card;
|
|
617
|
+
listeners = /* @__PURE__ */ new Set();
|
|
618
|
+
waiters = /* @__PURE__ */ new Map();
|
|
619
|
+
running = /* @__PURE__ */ new Map();
|
|
620
|
+
constructor(opts) {
|
|
621
|
+
this.opts = opts;
|
|
622
|
+
this.card = opts.card;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Abort a running task by control path (facade /a2a task cancel): abort the
|
|
626
|
+
* executor's signal, settle the task CANCELED, and wake stream waiters.
|
|
627
|
+
* @returns false when the task is unknown or already terminal.
|
|
628
|
+
*/
|
|
629
|
+
abort(taskId) {
|
|
630
|
+
const record = this.opts.store.get(taskId);
|
|
631
|
+
if (record === void 0 || isTerminal(record.state)) return false;
|
|
632
|
+
this.running.get(taskId)?.abort();
|
|
633
|
+
this.opts.store.setState(taskId, TaskState.CANCELED, {
|
|
634
|
+
code: "canceled",
|
|
635
|
+
text: "Task canceled"
|
|
636
|
+
});
|
|
637
|
+
this.settle(taskId);
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
/** True when the request carries the configured bearer token. */
|
|
641
|
+
authorized(req) {
|
|
642
|
+
const token = this.opts.authToken;
|
|
643
|
+
if (!token) return true;
|
|
644
|
+
const header = req.headers?.["authorization"] ?? "";
|
|
645
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
646
|
+
return match !== null && match[1] === token;
|
|
647
|
+
}
|
|
648
|
+
/** Route one inbound HTTP request (GET card, POST JSON-RPC). */
|
|
649
|
+
async handle(req, body) {
|
|
650
|
+
const path = (req.url ?? "").split("?")[0] ?? "";
|
|
651
|
+
if (req.method === "GET" && path === "/.well-known/agent-card.json") return {
|
|
652
|
+
status: 200,
|
|
653
|
+
contentType: "application/json",
|
|
654
|
+
body: JSON.stringify(this.card)
|
|
655
|
+
};
|
|
656
|
+
if (req.method !== "POST" || path !== endpointOf(this.card)) return {
|
|
657
|
+
status: 404,
|
|
658
|
+
contentType: "text/plain",
|
|
659
|
+
body: "Not Found"
|
|
660
|
+
};
|
|
661
|
+
if (!this.authorized(req)) return {
|
|
662
|
+
status: 401,
|
|
663
|
+
contentType: "application/json",
|
|
664
|
+
body: JSON.stringify(rpcError(null, A2A_ERROR_CODES.UNAUTHORIZED, "Unauthorized")),
|
|
665
|
+
headers: { "WWW-Authenticate": "Bearer" }
|
|
666
|
+
};
|
|
667
|
+
const rpc = parseRpc(body);
|
|
668
|
+
if (rpc === void 0) return {
|
|
669
|
+
status: 200,
|
|
670
|
+
contentType: "application/json",
|
|
671
|
+
body: JSON.stringify(rpcError(null, A2A_ERROR_CODES.INVALID_REQUEST, "Invalid JSON-RPC request"))
|
|
672
|
+
};
|
|
673
|
+
try {
|
|
674
|
+
const result = await this.dispatch(rpc);
|
|
675
|
+
const source = sourceOf(req);
|
|
676
|
+
this.opts.onInbound?.({
|
|
677
|
+
method: rpc.method,
|
|
678
|
+
...source !== void 0 ? { source } : {},
|
|
679
|
+
taskIds: extractTaskIds(result),
|
|
680
|
+
streaming: false
|
|
681
|
+
});
|
|
682
|
+
return {
|
|
683
|
+
status: 200,
|
|
684
|
+
contentType: "application/json",
|
|
685
|
+
body: JSON.stringify(rpcSuccess(rpc.id, result))
|
|
686
|
+
};
|
|
687
|
+
} catch (err) {
|
|
688
|
+
const e = err;
|
|
689
|
+
return {
|
|
690
|
+
status: 200,
|
|
691
|
+
contentType: "application/json",
|
|
692
|
+
body: JSON.stringify(rpcError(rpc.id, typeof e.code === "number" ? e.code : A2A_ERROR_CODES.INTERNAL_ERROR, e.message ?? String(err), e.data))
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Route one SSE request (SendStreamingMessage / SubscribeToTask). Frames are
|
|
698
|
+
* pushed to `onEvent` as they happen; the caller must have checked
|
|
699
|
+
* `authorized()` before writing 200 headers.
|
|
700
|
+
*/
|
|
701
|
+
async handleStream(req, body, onEvent) {
|
|
702
|
+
if (req.method !== "POST") {
|
|
703
|
+
onEvent(`event: error\ndata: ${JSON.stringify({
|
|
704
|
+
code: 404,
|
|
705
|
+
message: "Not Found"
|
|
706
|
+
})}\n\n`);
|
|
707
|
+
return { status: 200 };
|
|
708
|
+
}
|
|
709
|
+
if (!this.authorized(req)) return {
|
|
710
|
+
status: 401,
|
|
711
|
+
headers: { "WWW-Authenticate": "Bearer" }
|
|
712
|
+
};
|
|
713
|
+
const rpc = parseRpc(body);
|
|
714
|
+
if (rpc === void 0) {
|
|
715
|
+
onEvent(`event: error\ndata: ${JSON.stringify({
|
|
716
|
+
code: A2A_ERROR_CODES.INVALID_REQUEST,
|
|
717
|
+
message: "Invalid JSON-RPC request"
|
|
718
|
+
})}\n\n`);
|
|
719
|
+
return { status: 200 };
|
|
720
|
+
}
|
|
721
|
+
if (rpc.method !== A2A_METHODS.sendStreamingMessage && rpc.method !== A2A_METHODS.subscribeToTask) {
|
|
722
|
+
onEvent(`event: error\ndata: ${JSON.stringify(methodNotFound(rpc.method ?? ""))}\n\n`);
|
|
723
|
+
return { status: 200 };
|
|
724
|
+
}
|
|
725
|
+
const params = rpc.params ?? {};
|
|
726
|
+
let record;
|
|
727
|
+
if (rpc.method === A2A_METHODS.sendStreamingMessage) {
|
|
728
|
+
if (params.message === void 0) {
|
|
729
|
+
onEvent(`event: error\ndata: ${JSON.stringify({
|
|
730
|
+
code: A2A_ERROR_CODES.INVALID_PARAMS,
|
|
731
|
+
message: "Missing message"
|
|
732
|
+
})}\n\n`);
|
|
733
|
+
return { status: 200 };
|
|
734
|
+
}
|
|
735
|
+
const gateOutcome = await this.opts.gate(gateInputFrom(params.message, this.remotePeerOf(req)));
|
|
736
|
+
if (!gateOutcome.ok) {
|
|
737
|
+
onEvent(`event: error\ndata: ${JSON.stringify({
|
|
738
|
+
code: -32e3,
|
|
739
|
+
message: gateOutcome.reason
|
|
740
|
+
})}\n\n`);
|
|
741
|
+
return { status: 200 };
|
|
742
|
+
}
|
|
743
|
+
record = this.ensureTask(params.message, this.remotePeerOf(req));
|
|
744
|
+
this.noteInbound(req, rpc.method, [record.taskId], true);
|
|
745
|
+
this.runTask(record);
|
|
746
|
+
} else {
|
|
747
|
+
const id = params.id;
|
|
748
|
+
if (id === void 0) {
|
|
749
|
+
onEvent(`event: error\ndata: ${JSON.stringify({
|
|
750
|
+
code: A2A_ERROR_CODES.INVALID_PARAMS,
|
|
751
|
+
message: "Missing task id"
|
|
752
|
+
})}\n\n`);
|
|
753
|
+
return { status: 200 };
|
|
754
|
+
}
|
|
755
|
+
record = this.opts.store.get(id);
|
|
756
|
+
this.noteInbound(req, rpc.method, id ? [id] : [], true);
|
|
757
|
+
if (record === void 0) {
|
|
758
|
+
onEvent(`event: error\ndata: ${JSON.stringify({
|
|
759
|
+
code: A2A_ERROR_CODES.TASK_NOT_FOUND,
|
|
760
|
+
message: `Task ${id} not found`
|
|
761
|
+
})}\n\n`);
|
|
762
|
+
return { status: 200 };
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const sub = (frame) => onEvent(`data: ${JSON.stringify(frame)}\n\n`);
|
|
766
|
+
this.listeners.add(sub);
|
|
767
|
+
try {
|
|
768
|
+
const current = this.opts.store.get(record.taskId);
|
|
769
|
+
if (current !== void 0) sub({ statusUpdate: {
|
|
770
|
+
taskId: record.taskId,
|
|
771
|
+
contextId: record.contextId,
|
|
772
|
+
status: statusOf(current)
|
|
773
|
+
} });
|
|
774
|
+
await this.waitTerminal(record.taskId);
|
|
775
|
+
const finalRecord = this.opts.store.get(record.taskId);
|
|
776
|
+
if (finalRecord !== void 0) onEvent(`data: ${JSON.stringify({ task: toTask(finalRecord) })}\n\n`);
|
|
777
|
+
} finally {
|
|
778
|
+
this.listeners.delete(sub);
|
|
779
|
+
}
|
|
780
|
+
return { status: 200 };
|
|
781
|
+
}
|
|
782
|
+
/** Dispatch one JSON-RPC request; throws {code, message} on errors. */
|
|
783
|
+
async dispatch(rpc) {
|
|
784
|
+
const method = rpc.method;
|
|
785
|
+
const params = rpc.params ?? {};
|
|
786
|
+
switch (method) {
|
|
787
|
+
case A2A_METHODS.sendMessage: {
|
|
788
|
+
const message = params["message"];
|
|
789
|
+
if (message === void 0) throw rpcFault(A2A_ERROR_CODES.INVALID_PARAMS, "Missing message");
|
|
790
|
+
const gateOutcome = await this.opts.gate(gateInputFrom(message, null));
|
|
791
|
+
if (!gateOutcome.ok) throw rpcFault(A2A_ERROR_CODES.INVALID_PARAMS, gateOutcome.reason);
|
|
792
|
+
const record = this.ensureTask(message, null);
|
|
793
|
+
this.noteInbound({ method }, method, [record.taskId], false);
|
|
794
|
+
await this.runTask(record);
|
|
795
|
+
return toTask(this.opts.store.get(record.taskId) ?? record);
|
|
796
|
+
}
|
|
797
|
+
case A2A_METHODS.getTask: {
|
|
798
|
+
const id = params["id"];
|
|
799
|
+
if (id === void 0) throw rpcFault(A2A_ERROR_CODES.INVALID_PARAMS, "Missing task id");
|
|
800
|
+
const record = this.opts.store.get(id);
|
|
801
|
+
if (record === void 0) throw rpcFault(A2A_ERROR_CODES.TASK_NOT_FOUND, `Task ${id} not found`);
|
|
802
|
+
return toTask(record);
|
|
803
|
+
}
|
|
804
|
+
case A2A_METHODS.listTasks: return this.opts.store.list().map(toTask);
|
|
805
|
+
case A2A_METHODS.cancelTask: {
|
|
806
|
+
const id = params["id"];
|
|
807
|
+
if (id === void 0) throw rpcFault(A2A_ERROR_CODES.INVALID_PARAMS, "Missing task id");
|
|
808
|
+
const record = this.opts.store.get(id);
|
|
809
|
+
if (record === void 0) throw rpcFault(A2A_ERROR_CODES.TASK_NOT_FOUND, `Task ${id} not found`);
|
|
810
|
+
if (isTerminal(record.state)) throw rpcFault(A2A_ERROR_CODES.TASK_CANCEL_NOT_ALLOWED, `Task ${id} already in terminal state ${record.state}`);
|
|
811
|
+
this.running.get(id)?.abort();
|
|
812
|
+
this.opts.store.setState(id, TaskState.CANCELED, {
|
|
813
|
+
code: "canceled",
|
|
814
|
+
text: "Task canceled by caller"
|
|
815
|
+
});
|
|
816
|
+
this.settle(id);
|
|
817
|
+
return toTask(this.opts.store.get(id));
|
|
818
|
+
}
|
|
819
|
+
case A2A_METHODS.getExtendedAgentCard: return this.card;
|
|
820
|
+
default: throw rpcFault(A2A_ERROR_CODES.METHOD_NOT_FOUND, `Unknown method ${method}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/** Create (or continue) the task record for one message. */
|
|
824
|
+
ensureTask(message, remotePeerId) {
|
|
825
|
+
if (message.taskId !== void 0) {
|
|
826
|
+
const existing = this.opts.store.get(message.taskId);
|
|
827
|
+
if (existing !== void 0) return this.opts.store.create({
|
|
828
|
+
contextId: existing.contextId,
|
|
829
|
+
skill: existing.skill,
|
|
830
|
+
parts: message.parts,
|
|
831
|
+
remotePeerId
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
const contextId = message.contextId ?? crypto.randomUUID();
|
|
835
|
+
const skill = message.metadata?.["skill"] ?? "chat";
|
|
836
|
+
return this.opts.store.create({
|
|
837
|
+
contextId,
|
|
838
|
+
skill,
|
|
839
|
+
parts: message.parts,
|
|
840
|
+
remotePeerId
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
/** Run one task to a terminal state, emitting status/artifact frames. */
|
|
844
|
+
async runTask(record) {
|
|
845
|
+
const controller = new AbortController();
|
|
846
|
+
this.running.set(record.taskId, controller);
|
|
847
|
+
this.opts.store.setState(record.taskId, TaskState.WORKING);
|
|
848
|
+
this.emit({ statusUpdate: {
|
|
849
|
+
taskId: record.taskId,
|
|
850
|
+
contextId: record.contextId,
|
|
851
|
+
status: statusOf(this.opts.store.get(record.taskId))
|
|
852
|
+
} });
|
|
853
|
+
const onEvent = this.eventSink(record.taskId, record.contextId);
|
|
854
|
+
try {
|
|
855
|
+
const output = await this.opts.executors.resolve(record.skill).execute({
|
|
856
|
+
taskId: record.taskId,
|
|
857
|
+
contextId: record.contextId,
|
|
858
|
+
skill: record.skill,
|
|
859
|
+
prompt: partsToText(record.parts),
|
|
860
|
+
signal: controller.signal
|
|
861
|
+
}, { onEvent });
|
|
862
|
+
const settled = this.opts.store.get(record.taskId);
|
|
863
|
+
if (settled !== void 0 && settled.state === TaskState.CANCELED) {} else {
|
|
864
|
+
this.opts.store.appendArtifact(record.taskId, {
|
|
865
|
+
artifactId: "result",
|
|
866
|
+
parts: output.parts,
|
|
867
|
+
lastChunk: true
|
|
868
|
+
});
|
|
869
|
+
this.opts.store.setState(record.taskId, TaskState.COMPLETED);
|
|
870
|
+
}
|
|
871
|
+
} catch (err) {
|
|
872
|
+
const settled = this.opts.store.get(record.taskId);
|
|
873
|
+
if (settled !== void 0 && settled.state === TaskState.CANCELED) {} else this.opts.store.setState(record.taskId, TaskState.FAILED, {
|
|
874
|
+
code: "executor",
|
|
875
|
+
text: err.message
|
|
876
|
+
});
|
|
877
|
+
} finally {
|
|
878
|
+
this.running.delete(record.taskId);
|
|
879
|
+
const terminal = this.opts.store.get(record.taskId);
|
|
880
|
+
if (terminal !== void 0) this.emit({ task: toTask(terminal) });
|
|
881
|
+
this.settle(record.taskId);
|
|
882
|
+
this.opts.onTaskSettled?.(record.taskId);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/** Build the event sink that folds executor events into store + frames. */
|
|
886
|
+
eventSink(taskId, contextId) {
|
|
887
|
+
return (ev) => {
|
|
888
|
+
if (ev.type === "status") {
|
|
889
|
+
this.opts.store.setState(taskId, ev.state ?? TaskState.WORKING);
|
|
890
|
+
this.emit({ statusUpdate: {
|
|
891
|
+
taskId,
|
|
892
|
+
contextId,
|
|
893
|
+
status: statusOf(this.opts.store.get(taskId))
|
|
894
|
+
} });
|
|
895
|
+
} else if (ev.type === "artifact" && ev.artifactId !== void 0 && ev.parts !== void 0) {
|
|
896
|
+
const artifact = this.opts.store.appendArtifact(taskId, {
|
|
897
|
+
artifactId: ev.artifactId,
|
|
898
|
+
...ev.name !== void 0 ? { name: ev.name } : {},
|
|
899
|
+
parts: ev.parts,
|
|
900
|
+
...ev.lastChunk !== void 0 ? { lastChunk: ev.lastChunk } : {}
|
|
901
|
+
}).artifacts.find((a) => a.artifactId === ev.artifactId);
|
|
902
|
+
if (artifact !== void 0) this.emit({ artifactUpdate: {
|
|
903
|
+
taskId,
|
|
904
|
+
contextId,
|
|
905
|
+
artifact: {
|
|
906
|
+
artifactId: artifact.artifactId,
|
|
907
|
+
...artifact.name !== void 0 ? { name: artifact.name } : {},
|
|
908
|
+
parts: artifact.parts
|
|
909
|
+
},
|
|
910
|
+
...ev.lastChunk !== void 0 ? { lastChunk: ev.lastChunk } : {}
|
|
911
|
+
} });
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
emit(frame) {
|
|
916
|
+
for (const listener of [...this.listeners]) listener(frame);
|
|
917
|
+
}
|
|
918
|
+
noteInbound(req, method, taskIds, streaming) {
|
|
919
|
+
const source = sourceOf(req);
|
|
920
|
+
this.opts.onInbound?.({
|
|
921
|
+
method,
|
|
922
|
+
...source !== void 0 ? { source } : {},
|
|
923
|
+
taskIds,
|
|
924
|
+
streaming
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
remotePeerOf(req) {
|
|
928
|
+
const source = sourceOf(req);
|
|
929
|
+
return source ? hashOf(source) : null;
|
|
930
|
+
}
|
|
931
|
+
async waitTerminal(taskId) {
|
|
932
|
+
const current = this.opts.store.get(taskId);
|
|
933
|
+
if (current !== void 0 && isTerminal(current.state)) return;
|
|
934
|
+
await new Promise((resolve) => {
|
|
935
|
+
this.waiters.set(taskId, resolve);
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
settle(taskId) {
|
|
939
|
+
const waiter = this.waiters.get(taskId);
|
|
940
|
+
if (waiter !== void 0) {
|
|
941
|
+
this.waiters.delete(taskId);
|
|
942
|
+
waiter();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
/** Read the JSON-RPC endpoint path off the card's first interface. */
|
|
947
|
+
function endpointOf(card) {
|
|
948
|
+
const url = card.supportedInterfaces?.[0]?.url;
|
|
949
|
+
if (!url) return "/a2a";
|
|
950
|
+
try {
|
|
951
|
+
return new URL(url).pathname;
|
|
952
|
+
} catch {
|
|
953
|
+
return url;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function sourceOf(req) {
|
|
957
|
+
const s = req.socket;
|
|
958
|
+
if (!s?.remoteAddress) return void 0;
|
|
959
|
+
return `${s.remoteAddress}:${s.remotePort ?? ""}`;
|
|
960
|
+
}
|
|
961
|
+
function hashOf(value) {
|
|
962
|
+
let h = 0;
|
|
963
|
+
for (let i = 0; i < value.length; i++) h = h * 31 + value.charCodeAt(i) >>> 0;
|
|
964
|
+
return `peer-${h.toString(16)}`;
|
|
965
|
+
}
|
|
966
|
+
function gateInputFrom(message, remotePeerId) {
|
|
967
|
+
return {
|
|
968
|
+
contextId: message.contextId ?? "new",
|
|
969
|
+
skill: message.metadata?.["skill"] ?? "chat",
|
|
970
|
+
parts: message.parts,
|
|
971
|
+
remotePeerId
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function rpcFault(code, message) {
|
|
975
|
+
return {
|
|
976
|
+
code,
|
|
977
|
+
message
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function statusOf(record) {
|
|
981
|
+
return {
|
|
982
|
+
state: record.state,
|
|
983
|
+
timestamp: record.updatedAt,
|
|
984
|
+
...record.error ? { message: {
|
|
985
|
+
messageId: `a2a-status-${record.taskId}`,
|
|
986
|
+
role: Role.AGENT,
|
|
987
|
+
parts: [{ text: record.error.message }]
|
|
988
|
+
} } : {}
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
function toTask(record) {
|
|
992
|
+
return {
|
|
993
|
+
id: record.taskId,
|
|
994
|
+
contextId: record.contextId,
|
|
995
|
+
status: statusOf(record),
|
|
996
|
+
artifacts: record.artifacts,
|
|
997
|
+
metadata: { skill: record.skill }
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
function extractTaskIds(result) {
|
|
1001
|
+
if (result === null || typeof result !== "object") return [];
|
|
1002
|
+
const value = result;
|
|
1003
|
+
if (typeof value.id === "string") return [value.id];
|
|
1004
|
+
if (typeof value.task?.id === "string") return [value.task.id];
|
|
1005
|
+
if (Array.isArray(result)) return result.map((t) => t.id).filter((id) => typeof id === "string");
|
|
1006
|
+
return [];
|
|
1007
|
+
}
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region lib/types/server/routes.js
|
|
1010
|
+
/**
|
|
1011
|
+
* HTTP binding for the inbound server: registers the AgentCard route and the
|
|
1012
|
+
* JSON-RPC / SSE endpoint on a structural webServer (normally
|
|
1013
|
+
* `@deepseek-ai/dsh-host-webserver`), with runtime enable/disable.
|
|
1014
|
+
* @module dsh-a2a/server/routes
|
|
1015
|
+
*/
|
|
1016
|
+
const CARD_PATH = "/.well-known/agent-card.json";
|
|
1017
|
+
/** Runtime-registered HTTP routes for one A2A server. */
|
|
1018
|
+
var A2aRoutes = class {
|
|
1019
|
+
webServer;
|
|
1020
|
+
server;
|
|
1021
|
+
disposers = [];
|
|
1022
|
+
registered = false;
|
|
1023
|
+
constructor(webServer, server) {
|
|
1024
|
+
this.webServer = webServer;
|
|
1025
|
+
this.server = server;
|
|
1026
|
+
}
|
|
1027
|
+
get active() {
|
|
1028
|
+
return this.registered;
|
|
1029
|
+
}
|
|
1030
|
+
/** Register the AgentCard + endpoint routes (idempotent). */
|
|
1031
|
+
enable() {
|
|
1032
|
+
if (this.registered) return;
|
|
1033
|
+
this.disposers.push(this.webServer.register({
|
|
1034
|
+
kind: "exact",
|
|
1035
|
+
path: CARD_PATH,
|
|
1036
|
+
handler: (_req, res) => {
|
|
1037
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1038
|
+
res.end(JSON.stringify(this.server.card));
|
|
1039
|
+
}
|
|
1040
|
+
}));
|
|
1041
|
+
this.disposers.push(this.webServer.register({
|
|
1042
|
+
kind: "prefix",
|
|
1043
|
+
path: endpointPathOf(this.server),
|
|
1044
|
+
handler: (req, res) => this.onRequest(req, res)
|
|
1045
|
+
}));
|
|
1046
|
+
this.registered = true;
|
|
1047
|
+
}
|
|
1048
|
+
/** Unregister the routes (idempotent). */
|
|
1049
|
+
disable() {
|
|
1050
|
+
for (const disposer of this.disposers.splice(0)) disposer();
|
|
1051
|
+
this.registered = false;
|
|
1052
|
+
}
|
|
1053
|
+
dispose() {
|
|
1054
|
+
this.disable();
|
|
1055
|
+
}
|
|
1056
|
+
async onRequest(req, res) {
|
|
1057
|
+
const path = (req.url ?? "").split("?")[0] ?? "";
|
|
1058
|
+
const streaming = req.method === "POST" && (req.headers.accept ?? "").includes("text/event-stream");
|
|
1059
|
+
if (req.method === "POST" && path === endpointPathOf(this.server)) {
|
|
1060
|
+
const body = await readBody$1(req);
|
|
1061
|
+
if (streaming) {
|
|
1062
|
+
if (!this.server.authorized(toServerReq(req))) {
|
|
1063
|
+
res.writeHead(401, {
|
|
1064
|
+
"content-type": "text/plain",
|
|
1065
|
+
"WWW-Authenticate": "Bearer"
|
|
1066
|
+
});
|
|
1067
|
+
res.end("Unauthorized");
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
res.writeHead(200, {
|
|
1071
|
+
"content-type": "text/event-stream",
|
|
1072
|
+
"cache-control": "no-cache",
|
|
1073
|
+
connection: "keep-alive",
|
|
1074
|
+
"x-accel-buffering": "no"
|
|
1075
|
+
});
|
|
1076
|
+
const outcome = await this.server.handleStream(toServerReq(req), body, (frame) => res.write(frame));
|
|
1077
|
+
if (outcome.status !== 200) {
|
|
1078
|
+
res.writeHead(outcome.status, {
|
|
1079
|
+
"content-type": "text/plain",
|
|
1080
|
+
...outcome.headers ?? {}
|
|
1081
|
+
});
|
|
1082
|
+
res.end("Unauthorized");
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
res.end();
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const out = await this.server.handle(toServerReq(req), body);
|
|
1089
|
+
res.writeHead(out.status, {
|
|
1090
|
+
"content-type": out.contentType,
|
|
1091
|
+
...out.headers ?? {}
|
|
1092
|
+
});
|
|
1093
|
+
res.end(out.body);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const out = await this.server.handle(toServerReq(req), path === CARD_PATH ? "" : "");
|
|
1097
|
+
res.writeHead(out.status, {
|
|
1098
|
+
"content-type": out.contentType,
|
|
1099
|
+
...out.headers ?? {}
|
|
1100
|
+
});
|
|
1101
|
+
res.end(out.body);
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
function endpointPathOf(server) {
|
|
1105
|
+
const url = server.card.supportedInterfaces?.[0]?.url;
|
|
1106
|
+
if (!url) return "/a2a";
|
|
1107
|
+
try {
|
|
1108
|
+
return new URL(url).pathname;
|
|
1109
|
+
} catch {
|
|
1110
|
+
return "/a2a";
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function readBody$1(req) {
|
|
1114
|
+
return new Promise((resolve, reject) => {
|
|
1115
|
+
const chunks = [];
|
|
1116
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1117
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
1118
|
+
req.once("error", reject);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
function toServerReq(req) {
|
|
1122
|
+
const headers = {};
|
|
1123
|
+
for (const [key, value] of Object.entries(req.headers)) if (typeof value === "string") headers[key] = value;
|
|
1124
|
+
else if (Array.isArray(value)) headers[key] = value.join(", ");
|
|
1125
|
+
const method = req.method;
|
|
1126
|
+
const url = req.url;
|
|
1127
|
+
const socket = req.socket;
|
|
1128
|
+
return {
|
|
1129
|
+
...method !== void 0 ? { method } : {},
|
|
1130
|
+
...url !== void 0 ? { url } : {},
|
|
1131
|
+
headers,
|
|
1132
|
+
...socket ? { socket: {
|
|
1133
|
+
...socket.remoteAddress !== void 0 ? { remoteAddress: socket.remoteAddress } : {},
|
|
1134
|
+
...socket.remotePort !== void 0 ? { remotePort: socket.remotePort } : {}
|
|
1135
|
+
} } : {}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
//#endregion
|
|
1139
|
+
//#region lib/types/api.js
|
|
1140
|
+
/**
|
|
1141
|
+
* GUI control API for the A2A settings dashboard: one `GET /a2a/api`
|
|
1142
|
+
* snapshot plus `POST /a2a/api` control actions (server toggle, outbound
|
|
1143
|
+
* agent management, task view). The browser half talks only to this route;
|
|
1144
|
+
* it carries no protocol knowledge. Loopback-only by default so the
|
|
1145
|
+
* dashboard cannot be driven from the wire.
|
|
1146
|
+
*
|
|
1147
|
+
* The route body is bound to the same `A2AServiceImpl` facade the `/a2a`
|
|
1148
|
+
* command surface uses, so GUI actions and commands cannot disagree.
|
|
1149
|
+
* @module dsh-a2a/api
|
|
1150
|
+
*/
|
|
1151
|
+
function readBody(req) {
|
|
1152
|
+
return new Promise((resolve, reject) => {
|
|
1153
|
+
const chunks = [];
|
|
1154
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1155
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
1156
|
+
req.once("error", reject);
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
/** The caller is the local machine (never a remote peer). */
|
|
1160
|
+
function isLoopback(req) {
|
|
1161
|
+
const address = req.socket?.remoteAddress;
|
|
1162
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
1163
|
+
}
|
|
1164
|
+
function json(res, status, value) {
|
|
1165
|
+
res.writeHead(status, {
|
|
1166
|
+
"content-type": "application/json",
|
|
1167
|
+
"cache-control": "no-cache"
|
|
1168
|
+
});
|
|
1169
|
+
res.end(JSON.stringify(value));
|
|
1170
|
+
}
|
|
1171
|
+
/** Handle one dashboard API request against the service facade. */
|
|
1172
|
+
async function handleApiRequest(req, res, impl) {
|
|
1173
|
+
if (!isLoopback(req)) {
|
|
1174
|
+
res.writeHead(403, { "content-type": "text/plain" });
|
|
1175
|
+
res.end("forbidden");
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (req.method === "GET") {
|
|
1179
|
+
json(res, 200, snapshotOf(impl));
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (req.method === "POST") {
|
|
1183
|
+
let payload;
|
|
1184
|
+
try {
|
|
1185
|
+
payload = JSON.parse(await readBody(req));
|
|
1186
|
+
} catch {
|
|
1187
|
+
json(res, 400, {
|
|
1188
|
+
ok: false,
|
|
1189
|
+
message: "invalid JSON body"
|
|
1190
|
+
});
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const result = await dispatch(payload, impl);
|
|
1194
|
+
json(res, result.ok ? 200 : 409, result);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
res.writeHead(405, { "content-type": "text/plain" });
|
|
1198
|
+
res.end("Method Not Allowed");
|
|
1199
|
+
}
|
|
1200
|
+
function snapshotOf(impl) {
|
|
1201
|
+
return {
|
|
1202
|
+
server: impl.status().server,
|
|
1203
|
+
tasks: impl.listTasks(),
|
|
1204
|
+
agents: impl.agents()
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
async function dispatch(payload, impl) {
|
|
1208
|
+
switch (payload.action) {
|
|
1209
|
+
case "server.enable": return impl.enableServer(true);
|
|
1210
|
+
case "server.disable": return impl.enableServer(false);
|
|
1211
|
+
case "agent.add": return impl.addAgent({
|
|
1212
|
+
name: payload.name,
|
|
1213
|
+
agentCardUrl: payload.agentCardUrl,
|
|
1214
|
+
...payload.bearerTokenEnv ? { bearerTokenEnv: payload.bearerTokenEnv } : {}
|
|
1215
|
+
});
|
|
1216
|
+
case "agent.remove": return impl.removeAgent(payload.id);
|
|
1217
|
+
case "agent.enable": return impl.setAgentEnabled(payload.id, true);
|
|
1218
|
+
case "agent.disable": return impl.setAgentEnabled(payload.id, false);
|
|
1219
|
+
case "agent.refresh": return impl.refreshAgentCard(payload.id);
|
|
1220
|
+
case "task.cancel": return impl.cancelTask(payload.id);
|
|
1221
|
+
default: return {
|
|
1222
|
+
ok: false,
|
|
1223
|
+
message: `unknown action ${String(payload.action)}`
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
//#endregion
|
|
1228
|
+
//#region lib/types/outbound/calls.js
|
|
1229
|
+
/**
|
|
1230
|
+
* Outbound A2A client: AgentCard discovery plus the JSON-RPC methods the
|
|
1231
|
+
* tools layer needs. Sync-by-polling for P0 (each tool call waits for the
|
|
1232
|
+
* task to settle up to a timeout); passive result injection replaces this in
|
|
1233
|
+
* P1 without changing the transport.
|
|
1234
|
+
* @module dsh-a2a/client/calls
|
|
1235
|
+
*/
|
|
1236
|
+
/** Error carrying the remote JSON-RPC error code. */
|
|
1237
|
+
var A2AError = class extends Error {
|
|
1238
|
+
code;
|
|
1239
|
+
constructor(code, message) {
|
|
1240
|
+
super(message);
|
|
1241
|
+
this.code = code;
|
|
1242
|
+
this.name = "A2AError";
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
/** A connected remote A2A agent (one AgentCard endpoint). */
|
|
1246
|
+
var A2AClient = class A2AClient {
|
|
1247
|
+
opts;
|
|
1248
|
+
card;
|
|
1249
|
+
endpoint;
|
|
1250
|
+
constructor(card, endpoint, opts) {
|
|
1251
|
+
this.opts = opts;
|
|
1252
|
+
this.card = card;
|
|
1253
|
+
this.endpoint = endpoint;
|
|
1254
|
+
}
|
|
1255
|
+
/** Fetch and validate an AgentCard, then connect to its JSON-RPC endpoint. */
|
|
1256
|
+
static async connect(agentCardUrl, opts = {}) {
|
|
1257
|
+
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
1258
|
+
const response = await (opts.fetchImpl ?? fetch)(agentCardUrl, {
|
|
1259
|
+
headers: {
|
|
1260
|
+
accept: "application/json",
|
|
1261
|
+
...opts.bearerToken ? { authorization: `Bearer ${opts.bearerToken}` } : {}
|
|
1262
|
+
},
|
|
1263
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1264
|
+
});
|
|
1265
|
+
if (!response.ok) throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_NOT_FOUND, `agent card fetch failed: HTTP ${response.status}`);
|
|
1266
|
+
const card = await response.json();
|
|
1267
|
+
if (typeof card.name !== "string" || typeof card.description !== "string") throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_SIGNATURE_INVALID, `invalid agent card at ${agentCardUrl}`);
|
|
1268
|
+
const endpoint = (card.supportedInterfaces?.find((i) => (i.protocolBinding ?? "JSONRPC") === "JSONRPC") ?? card.supportedInterfaces?.[0])?.url ?? card.url;
|
|
1269
|
+
if (!endpoint) throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_NOT_FOUND, `agent card at ${agentCardUrl} advertises no JSON-RPC interface`);
|
|
1270
|
+
return new A2AClient(card, endpoint, opts);
|
|
1271
|
+
}
|
|
1272
|
+
/** Send a message and wait (or poll) until the task settles. */
|
|
1273
|
+
async sendMessage(message) {
|
|
1274
|
+
const task = await this.call(A2A_METHODS.sendMessage, { message });
|
|
1275
|
+
if (isTerminal(task.status.state)) return task;
|
|
1276
|
+
return this.pollTask(task.id);
|
|
1277
|
+
}
|
|
1278
|
+
async getTask(taskId) {
|
|
1279
|
+
return this.call(A2A_METHODS.getTask, { id: taskId });
|
|
1280
|
+
}
|
|
1281
|
+
async listTasks() {
|
|
1282
|
+
return this.call(A2A_METHODS.listTasks, {});
|
|
1283
|
+
}
|
|
1284
|
+
async cancelTask(taskId) {
|
|
1285
|
+
return this.call(A2A_METHODS.cancelTask, { id: taskId });
|
|
1286
|
+
}
|
|
1287
|
+
/** Poll a task until terminal or the overall timeout elapses. */
|
|
1288
|
+
async pollTask(taskId) {
|
|
1289
|
+
const budget = this.opts.timeoutMs ?? 6e4;
|
|
1290
|
+
const deadline = Date.now() + budget;
|
|
1291
|
+
for (;;) {
|
|
1292
|
+
const task = await this.getTask(taskId);
|
|
1293
|
+
if (isTerminal(task.status.state)) return task;
|
|
1294
|
+
if (Date.now() >= deadline) throw new A2AError(-32e3, `task ${taskId} did not settle within ${budget}ms`);
|
|
1295
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
async call(method, params) {
|
|
1299
|
+
const timeoutMs = this.opts.timeoutMs ?? 6e4;
|
|
1300
|
+
const fetchImpl = this.opts.fetchImpl ?? fetch;
|
|
1301
|
+
let response;
|
|
1302
|
+
try {
|
|
1303
|
+
response = await fetchImpl(this.endpoint, {
|
|
1304
|
+
method: "POST",
|
|
1305
|
+
headers: {
|
|
1306
|
+
"content-type": "application/json",
|
|
1307
|
+
accept: "application/json",
|
|
1308
|
+
...this.opts.bearerToken ? { authorization: `Bearer ${this.opts.bearerToken}` } : {}
|
|
1309
|
+
},
|
|
1310
|
+
body: JSON.stringify({
|
|
1311
|
+
jsonrpc: "2.0",
|
|
1312
|
+
id: crypto.randomUUID(),
|
|
1313
|
+
method,
|
|
1314
|
+
params
|
|
1315
|
+
}),
|
|
1316
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1317
|
+
});
|
|
1318
|
+
} catch (err) {
|
|
1319
|
+
if (err instanceof Error && err.name === "TimeoutError") throw new A2AError(-32e3, `A2A call ${method} timed out after ${timeoutMs}ms`);
|
|
1320
|
+
throw err;
|
|
1321
|
+
}
|
|
1322
|
+
if (response.status === 401) throw new A2AError(A2A_ERROR_CODES.UNAUTHORIZED, "remote agent rejected credentials (401)");
|
|
1323
|
+
if (!response.ok) throw new A2AError(A2A_ERROR_CODES.INTERNAL_ERROR, `A2A call ${method} failed: HTTP ${response.status}`);
|
|
1324
|
+
const payload = await response.json();
|
|
1325
|
+
if ("error" in payload) {
|
|
1326
|
+
const error = payload.error;
|
|
1327
|
+
throw new A2AError(error.code, error.message);
|
|
1328
|
+
}
|
|
1329
|
+
return payload.result;
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
//#endregion
|
|
1333
|
+
//#region lib/types/outbound/tools.js
|
|
1334
|
+
/**
|
|
1335
|
+
* Outbound half: register one remote agent's skills as model-facing tools on
|
|
1336
|
+
* `ctx.tools`. Tool names are `a2a__<agent>__<skill>` normalized to the DSH
|
|
1337
|
+
* function-name contract (≤64 chars, collision hash), mirroring the naming
|
|
1338
|
+
* rule of dsh-mcp-client. An agent advertising zero skills registers nothing.
|
|
1339
|
+
* @module dsh-a2a/client/tools
|
|
1340
|
+
*/
|
|
1341
|
+
function normalizeToolName(raw) {
|
|
1342
|
+
const norm = raw.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
1343
|
+
if (norm.length > 0 && /^[A-Za-z0-9_-]+$/.test(norm)) return norm;
|
|
1344
|
+
let hash = 0;
|
|
1345
|
+
for (let i = 0; i < raw.length; i++) hash = hash * 31 + raw.charCodeAt(i) >>> 0;
|
|
1346
|
+
return `_${hash.toString(16)}`;
|
|
1347
|
+
}
|
|
1348
|
+
/** Register one tool per advertised skill; empty skills register nothing. */
|
|
1349
|
+
function registerAgentTools(registrar, opts) {
|
|
1350
|
+
const prefix = opts.toolPrefix ?? "a2a";
|
|
1351
|
+
const skills = opts.client.card.skills ?? [];
|
|
1352
|
+
const out = [];
|
|
1353
|
+
for (const skill of skills) {
|
|
1354
|
+
const raw = `${prefix}__${opts.agentName}__${skill.id}`;
|
|
1355
|
+
const name = normalizeToolName(raw);
|
|
1356
|
+
let toolName = name;
|
|
1357
|
+
if (out.some((t) => t.name === toolName)) {
|
|
1358
|
+
let hash = 0;
|
|
1359
|
+
for (let i = 0; i < raw.length; i++) hash = hash * 31 + raw.charCodeAt(i) >>> 0;
|
|
1360
|
+
toolName = `${name.slice(0, 56)}_${hash.toString(16)}`;
|
|
1361
|
+
}
|
|
1362
|
+
const dispose = registrar.register(makeTool(toolName, opts.client, skill, opts.contextFor));
|
|
1363
|
+
if (dispose) out.push({
|
|
1364
|
+
name: toolName,
|
|
1365
|
+
dispose
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
return out;
|
|
1369
|
+
}
|
|
1370
|
+
function makeTool(name, client, skill, contextFor) {
|
|
1371
|
+
return {
|
|
1372
|
+
name,
|
|
1373
|
+
description: [skill.description, ...skill.examples?.length ? [`Examples: ${skill.examples.join(" | ")}`] : []].filter(Boolean).join("\n"),
|
|
1374
|
+
parameters: {
|
|
1375
|
+
type: "object",
|
|
1376
|
+
properties: { prompt: {
|
|
1377
|
+
type: "string",
|
|
1378
|
+
description: "The task or instruction to send to the remote agent."
|
|
1379
|
+
} },
|
|
1380
|
+
required: ["prompt"]
|
|
1381
|
+
},
|
|
1382
|
+
output: {
|
|
1383
|
+
schema: { type: "string" },
|
|
1384
|
+
render: (_args, value) => [{
|
|
1385
|
+
type: "text",
|
|
1386
|
+
text: String(value)
|
|
1387
|
+
}]
|
|
1388
|
+
},
|
|
1389
|
+
async execute(args, exec) {
|
|
1390
|
+
const message = {
|
|
1391
|
+
messageId: crypto.randomUUID(),
|
|
1392
|
+
role: Role.USER,
|
|
1393
|
+
contextId: contextFor(exec.agent?.id),
|
|
1394
|
+
parts: [{ text: args.prompt }]
|
|
1395
|
+
};
|
|
1396
|
+
const task = await client.sendMessage(message);
|
|
1397
|
+
return textOf(task.artifacts, task.status.state, task.status.message?.parts);
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
/** Render a settled task's output as tool text (INPUT_REQUIRED surfaced, not thrown). */
|
|
1402
|
+
function textOf(artifacts, state, statusParts) {
|
|
1403
|
+
const output = partsToText(artifacts?.flatMap((a) => a.parts ?? []));
|
|
1404
|
+
if (state === "FAILED") throw new A2AError(-32e3, `Remote agent task failed: ${output || partsToText(statusParts) || "no detail"}`);
|
|
1405
|
+
if (state === "INPUT_REQUIRED" || state === "AUTH_REQUIRED") return `[remote agent ${state}] ${partsToText(statusParts) || output || "agent awaits input"}`;
|
|
1406
|
+
return output || "(remote agent returned no output)";
|
|
1407
|
+
}
|
|
1408
|
+
//#endregion
|
|
1409
|
+
//#region lib/types/outbound/registry.js
|
|
1410
|
+
/**
|
|
1411
|
+
* Outbound agent registry: the persisted list of connected remote agents,
|
|
1412
|
+
* their connection state, and the tool-registration disposers for each live
|
|
1413
|
+
* connection. Loaded at boot, mutations persist through the agent store.
|
|
1414
|
+
* @module dsh-a2a/client/registry
|
|
1415
|
+
*/
|
|
1416
|
+
/** Domain-backed agent store: one JSON array under the `agents` table key. */
|
|
1417
|
+
var DomainAgentStore = class DomainAgentStore {
|
|
1418
|
+
table;
|
|
1419
|
+
static KEY = "records";
|
|
1420
|
+
constructor(table) {
|
|
1421
|
+
this.table = table;
|
|
1422
|
+
}
|
|
1423
|
+
list() {
|
|
1424
|
+
const raw = this.table.get(DomainAgentStore.KEY);
|
|
1425
|
+
if (raw === void 0) return [];
|
|
1426
|
+
try {
|
|
1427
|
+
const parsed = JSON.parse(raw);
|
|
1428
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1429
|
+
} catch {
|
|
1430
|
+
return [];
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
async save(records) {
|
|
1434
|
+
await this.table.put(DomainAgentStore.KEY, JSON.stringify(records));
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
/** A list of remote agents, each with live tool registrations when enabled. */
|
|
1438
|
+
var OutboundAgentRegistry = class {
|
|
1439
|
+
opts;
|
|
1440
|
+
live = /* @__PURE__ */ new Map();
|
|
1441
|
+
views = /* @__PURE__ */ new Map();
|
|
1442
|
+
seedRecords;
|
|
1443
|
+
constructor(opts) {
|
|
1444
|
+
this.opts = opts;
|
|
1445
|
+
}
|
|
1446
|
+
/** Load persisted records and connect every enabled agent (background). */
|
|
1447
|
+
loadAll() {
|
|
1448
|
+
const records = this.opts.store.list();
|
|
1449
|
+
this.seedRecords = records;
|
|
1450
|
+
for (const record of records) if (record.enabled) this.connectRecord(record).catch((err) => {
|
|
1451
|
+
this.opts.onError?.(`[a2a] failed to connect "${record.name}": ${err.message}`);
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Seed configured agents that the persisted store does not know yet. The
|
|
1456
|
+
* store is the runtime state: an existing record (enabled or disabled,
|
|
1457
|
+
* present or removed) keeps its state — a disabled agent stays
|
|
1458
|
+
* disconnected and a removed one is not resurrected. Only specs with a name
|
|
1459
|
+
* absent from the store (and not already live this boot) connect and
|
|
1460
|
+
* persist.
|
|
1461
|
+
* @param specs - declared agents from the profile config.
|
|
1462
|
+
*/
|
|
1463
|
+
async seed(specs) {
|
|
1464
|
+
const known = new Set(this.opts.store.list().map((r) => r.name));
|
|
1465
|
+
for (const connection of this.live.values()) known.add(connection.record.name);
|
|
1466
|
+
for (const spec of specs) {
|
|
1467
|
+
if (known.has(spec.name)) continue;
|
|
1468
|
+
const result = await this.add(spec);
|
|
1469
|
+
if (result.ok) known.add(spec.name);
|
|
1470
|
+
else this.opts.onError?.(`[a2a] seed agent "${spec.name}": ${result.message}`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
list() {
|
|
1474
|
+
return [...this.views.values()];
|
|
1475
|
+
}
|
|
1476
|
+
/** Add and connect an agent, persisting the record on success. */
|
|
1477
|
+
async add(spec) {
|
|
1478
|
+
const record = {
|
|
1479
|
+
id: `agent-${crypto.randomUUID()}`,
|
|
1480
|
+
name: spec.name,
|
|
1481
|
+
agentCardUrl: spec.agentCardUrl,
|
|
1482
|
+
...spec.bearerTokenEnv ? { bearerTokenEnv: spec.bearerTokenEnv } : {},
|
|
1483
|
+
enabled: spec.enabled ?? true,
|
|
1484
|
+
timeoutMs: spec.timeoutMs ?? this.opts.defaultTimeoutMs ?? 6e4,
|
|
1485
|
+
lastCardAt: null
|
|
1486
|
+
};
|
|
1487
|
+
try {
|
|
1488
|
+
await this.connectRecord(record);
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
this.views.set(record.id, {
|
|
1491
|
+
id: record.id,
|
|
1492
|
+
name: record.name,
|
|
1493
|
+
agentCardUrl: record.agentCardUrl,
|
|
1494
|
+
enabled: record.enabled,
|
|
1495
|
+
state: "failed",
|
|
1496
|
+
skillCount: 0,
|
|
1497
|
+
toolCount: 0,
|
|
1498
|
+
lastError: err.message
|
|
1499
|
+
});
|
|
1500
|
+
return {
|
|
1501
|
+
ok: false,
|
|
1502
|
+
message: `failed to connect to ${record.agentCardUrl}: ${err.message}`
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
this.persist(record);
|
|
1506
|
+
return {
|
|
1507
|
+
ok: true,
|
|
1508
|
+
message: `agent "${record.name}" connected`
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
async remove(id) {
|
|
1512
|
+
const live = this.live.get(id);
|
|
1513
|
+
const view = this.views.get(id);
|
|
1514
|
+
if (live === void 0 && view === void 0) return {
|
|
1515
|
+
ok: false,
|
|
1516
|
+
message: `agent ${id} not found`
|
|
1517
|
+
};
|
|
1518
|
+
for (const disposer of live?.disposers ?? []) disposer.dispose();
|
|
1519
|
+
this.live.delete(id);
|
|
1520
|
+
this.views.delete(id);
|
|
1521
|
+
await this.persistAll();
|
|
1522
|
+
return {
|
|
1523
|
+
ok: true,
|
|
1524
|
+
message: `agent ${id} removed`
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
async setEnabled(id, enabled) {
|
|
1528
|
+
const live = this.live.get(id);
|
|
1529
|
+
const view = this.views.get(id);
|
|
1530
|
+
if (live === void 0 && view === void 0) return {
|
|
1531
|
+
ok: false,
|
|
1532
|
+
message: `agent ${id} not found`
|
|
1533
|
+
};
|
|
1534
|
+
if (enabled && live === void 0) {
|
|
1535
|
+
const record = this.findRecord(id);
|
|
1536
|
+
if (record === void 0) return {
|
|
1537
|
+
ok: false,
|
|
1538
|
+
message: `agent ${id} has no persisted record`
|
|
1539
|
+
};
|
|
1540
|
+
try {
|
|
1541
|
+
await this.connectRecord(record);
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
return {
|
|
1544
|
+
ok: false,
|
|
1545
|
+
message: `failed to connect: ${err.message}`
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
await this.persistAll();
|
|
1549
|
+
return {
|
|
1550
|
+
ok: true,
|
|
1551
|
+
message: `agent ${id} enabled`
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
if (!enabled && live !== void 0) {
|
|
1555
|
+
for (const disposer of live.disposers) disposer.dispose();
|
|
1556
|
+
this.live.delete(id);
|
|
1557
|
+
const current = this.views.get(id);
|
|
1558
|
+
if (current !== void 0) this.views.set(id, {
|
|
1559
|
+
...current,
|
|
1560
|
+
enabled: false,
|
|
1561
|
+
state: "disconnected",
|
|
1562
|
+
toolCount: 0,
|
|
1563
|
+
skillCount: 0
|
|
1564
|
+
});
|
|
1565
|
+
await this.persistAll();
|
|
1566
|
+
return {
|
|
1567
|
+
ok: true,
|
|
1568
|
+
message: `agent ${id} disabled`
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
return {
|
|
1572
|
+
ok: true,
|
|
1573
|
+
message: `agent ${id} already ${enabled ? "enabled" : "disabled"}`
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
async refresh(id) {
|
|
1577
|
+
const record = this.findRecord(id) ?? this.live.get(id)?.record;
|
|
1578
|
+
if (record === void 0) return {
|
|
1579
|
+
ok: false,
|
|
1580
|
+
message: `agent ${id} not found`
|
|
1581
|
+
};
|
|
1582
|
+
const live = this.live.get(id);
|
|
1583
|
+
for (const disposer of live?.disposers ?? []) disposer.dispose();
|
|
1584
|
+
this.live.delete(id);
|
|
1585
|
+
try {
|
|
1586
|
+
await this.connectRecord(record);
|
|
1587
|
+
} catch (err) {
|
|
1588
|
+
return {
|
|
1589
|
+
ok: false,
|
|
1590
|
+
message: `refresh failed: ${err.message}`
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
await this.persistAll();
|
|
1594
|
+
return {
|
|
1595
|
+
ok: true,
|
|
1596
|
+
message: `agent "${record.name}" reconnected`
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
async disposeAll() {
|
|
1600
|
+
for (const connection of this.live.values()) for (const disposer of connection.disposers) disposer.dispose();
|
|
1601
|
+
this.live.clear();
|
|
1602
|
+
this.views.clear();
|
|
1603
|
+
}
|
|
1604
|
+
async connectRecord(record) {
|
|
1605
|
+
const token = this.opts.tokenOf(record.bearerTokenEnv);
|
|
1606
|
+
const client = await A2AClient.connect(record.agentCardUrl, {
|
|
1607
|
+
...token ? { bearerToken: token } : {},
|
|
1608
|
+
timeoutMs: record.timeoutMs
|
|
1609
|
+
});
|
|
1610
|
+
const contextByAgent = /* @__PURE__ */ new Map();
|
|
1611
|
+
const fallback = `a2a-out-${crypto.randomUUID()}`;
|
|
1612
|
+
const contextFor = (agentId) => {
|
|
1613
|
+
const key = agentId ?? "\0fallback";
|
|
1614
|
+
const existing = contextByAgent.get(key);
|
|
1615
|
+
if (existing !== void 0) return existing;
|
|
1616
|
+
const id = agentId ? `a2a-out-${crypto.randomUUID()}` : fallback;
|
|
1617
|
+
contextByAgent.set(key, id);
|
|
1618
|
+
return id;
|
|
1619
|
+
};
|
|
1620
|
+
const disposers = registerAgentTools(this.opts.registrar, {
|
|
1621
|
+
agentName: record.name,
|
|
1622
|
+
client,
|
|
1623
|
+
...this.opts.toolPrefix !== void 0 ? { toolPrefix: this.opts.toolPrefix } : {},
|
|
1624
|
+
contextFor
|
|
1625
|
+
});
|
|
1626
|
+
const skillCount = client.card.skills?.length ?? 0;
|
|
1627
|
+
this.live.set(record.id, {
|
|
1628
|
+
record,
|
|
1629
|
+
client,
|
|
1630
|
+
disposers,
|
|
1631
|
+
skillCount
|
|
1632
|
+
});
|
|
1633
|
+
this.views.set(record.id, {
|
|
1634
|
+
id: record.id,
|
|
1635
|
+
name: record.name,
|
|
1636
|
+
agentCardUrl: record.agentCardUrl,
|
|
1637
|
+
enabled: record.enabled,
|
|
1638
|
+
state: "connected",
|
|
1639
|
+
skillCount,
|
|
1640
|
+
toolCount: disposers.length
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
findRecord(id) {
|
|
1644
|
+
const current = () => this.opts.store.list().find((r) => r.id === id);
|
|
1645
|
+
return current() ?? this.seedRecords?.find((r) => r.id === id);
|
|
1646
|
+
}
|
|
1647
|
+
persist(record) {
|
|
1648
|
+
const next = [...this.opts.store.list().filter((r) => r.id !== record.id), record];
|
|
1649
|
+
this.opts.store.save(next).catch((err) => {
|
|
1650
|
+
this.opts.onError?.(`[a2a] failed to persist agent list: ${String(err)}`);
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
async persistAll() {
|
|
1654
|
+
const liveRecords = [...this.live.values()].map((c) => c.record);
|
|
1655
|
+
const liveIds = new Set(liveRecords.map((r) => r.id));
|
|
1656
|
+
const kept = this.opts.store.list().filter((r) => !liveIds.has(r.id) && this.views.has(r.id)).map((r) => {
|
|
1657
|
+
const view = this.views.get(r.id);
|
|
1658
|
+
return view === void 0 ? r : {
|
|
1659
|
+
...r,
|
|
1660
|
+
enabled: view.enabled
|
|
1661
|
+
};
|
|
1662
|
+
});
|
|
1663
|
+
await this.opts.store.save([...kept, ...liveRecords]);
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
//#endregion
|
|
1667
|
+
//#region lib/types/service.js
|
|
1668
|
+
/**
|
|
1669
|
+
* The `ctx.a2a` service facade: the thin read/control surface commands and
|
|
1670
|
+
* other plugins use. Registered as a Cordis Service so the key disappears
|
|
1671
|
+
* with the plugin fiber.
|
|
1672
|
+
* @module dsh-a2a/service
|
|
1673
|
+
*/
|
|
1674
|
+
/** The service other plugins read as `ctx.a2a`. */
|
|
1675
|
+
var A2AService = class extends Service {
|
|
1676
|
+
impl;
|
|
1677
|
+
constructor(ctx, impl) {
|
|
1678
|
+
super(ctx, "a2a");
|
|
1679
|
+
this.impl = impl;
|
|
1680
|
+
}
|
|
1681
|
+
status() {
|
|
1682
|
+
return this.impl.status();
|
|
1683
|
+
}
|
|
1684
|
+
async enableServer(enable) {
|
|
1685
|
+
return this.impl.enableServer(enable);
|
|
1686
|
+
}
|
|
1687
|
+
getTask(taskId) {
|
|
1688
|
+
return this.impl.getTask(taskId);
|
|
1689
|
+
}
|
|
1690
|
+
listTasks() {
|
|
1691
|
+
return this.impl.listTasks();
|
|
1692
|
+
}
|
|
1693
|
+
async cancelTask(taskId) {
|
|
1694
|
+
return this.impl.cancelTask(taskId);
|
|
1695
|
+
}
|
|
1696
|
+
agents() {
|
|
1697
|
+
return this.impl.agents();
|
|
1698
|
+
}
|
|
1699
|
+
async addAgent(spec) {
|
|
1700
|
+
return this.impl.addAgent(spec);
|
|
1701
|
+
}
|
|
1702
|
+
async removeAgent(id) {
|
|
1703
|
+
return this.impl.removeAgent(id);
|
|
1704
|
+
}
|
|
1705
|
+
async setAgentEnabled(id, enabled) {
|
|
1706
|
+
return this.impl.setAgentEnabled(id, enabled);
|
|
1707
|
+
}
|
|
1708
|
+
async refreshAgentCard(id) {
|
|
1709
|
+
return this.impl.refreshAgentCard(id);
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
//#endregion
|
|
1713
|
+
//#region lib/types/commands.js
|
|
1714
|
+
/**
|
|
1715
|
+
* The `/a2a` command surface: status, server enable/disable, card, agents,
|
|
1716
|
+
* and task control — the P0 operations surface (the settings panel is P1).
|
|
1717
|
+
* @module dsh-a2a/commands
|
|
1718
|
+
*/
|
|
1719
|
+
/** Build the `/a2a` command definition over the service facade. */
|
|
1720
|
+
function buildA2aCommand(impl) {
|
|
1721
|
+
return {
|
|
1722
|
+
name: "a2a",
|
|
1723
|
+
description: "Manage the A2A plugin: status, server, agents, tasks, card.",
|
|
1724
|
+
handler: async (invocation) => {
|
|
1725
|
+
const [verb, ...rest] = invocation.rawInput.trim().split(/\s+/);
|
|
1726
|
+
switch (verb ?? "") {
|
|
1727
|
+
case "": {
|
|
1728
|
+
const status = impl.status();
|
|
1729
|
+
return success(`A2A status\n${JSON.stringify(status, null, 2)}`);
|
|
1730
|
+
}
|
|
1731
|
+
case "status": return success(JSON.stringify(impl.status(), null, 2));
|
|
1732
|
+
case "enable": return outcome(await impl.enableServer(true));
|
|
1733
|
+
case "disable": return outcome(await impl.enableServer(false));
|
|
1734
|
+
case "card": return success(JSON.stringify(impl.listTasks(), null, 2).slice(0, 4e3));
|
|
1735
|
+
case "agents": return success(JSON.stringify(impl.agents(), null, 2));
|
|
1736
|
+
case "agent": {
|
|
1737
|
+
const [action, ...args] = rest;
|
|
1738
|
+
switch (action ?? "") {
|
|
1739
|
+
case "add": {
|
|
1740
|
+
const url = args[0];
|
|
1741
|
+
if (!url) return error("usage: /a2a agent add <agentCardUrl> [name]");
|
|
1742
|
+
const name = args[1] ?? url;
|
|
1743
|
+
return outcome(await impl.addAgent({
|
|
1744
|
+
name,
|
|
1745
|
+
agentCardUrl: url
|
|
1746
|
+
}));
|
|
1747
|
+
}
|
|
1748
|
+
case "remove": {
|
|
1749
|
+
const id = args[0];
|
|
1750
|
+
if (!id) return error("usage: /a2a agent remove <id>");
|
|
1751
|
+
return outcome(await impl.removeAgent(id));
|
|
1752
|
+
}
|
|
1753
|
+
case "enable": {
|
|
1754
|
+
const id = args[0];
|
|
1755
|
+
if (!id) return error("usage: /a2a agent enable <id>");
|
|
1756
|
+
return outcome(await impl.setAgentEnabled(id, true));
|
|
1757
|
+
}
|
|
1758
|
+
case "disable": {
|
|
1759
|
+
const id = args[0];
|
|
1760
|
+
if (!id) return error("usage: /a2a agent disable <id>");
|
|
1761
|
+
return outcome(await impl.setAgentEnabled(id, false));
|
|
1762
|
+
}
|
|
1763
|
+
case "refresh": {
|
|
1764
|
+
const id = args[0];
|
|
1765
|
+
if (!id) return error("usage: /a2a agent refresh <id>");
|
|
1766
|
+
return outcome(await impl.refreshAgentCard(id));
|
|
1767
|
+
}
|
|
1768
|
+
default: return error("usage: /a2a agent add|remove|enable|disable|refresh ...");
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
case "tasks": return success(JSON.stringify(impl.listTasks(), null, 2));
|
|
1772
|
+
case "task": {
|
|
1773
|
+
const [action, id] = rest;
|
|
1774
|
+
if (action === "get" && id) return success(JSON.stringify(impl.getTask(id), null, 2));
|
|
1775
|
+
if (action === "cancel" && id) return outcome(await impl.cancelTask(id));
|
|
1776
|
+
return error("usage: /a2a task get|cancel <taskId>");
|
|
1777
|
+
}
|
|
1778
|
+
case "help": return success("a2a commands: status | enable | disable | card | agents | agent add/remove/enable/disable/refresh | tasks | task get/cancel <id> | help");
|
|
1779
|
+
default: return error(`unknown a2a verb "${verb}" (try /a2a help)`);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
function success(text) {
|
|
1785
|
+
return {
|
|
1786
|
+
kind: "success",
|
|
1787
|
+
text
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
function error(text) {
|
|
1791
|
+
return {
|
|
1792
|
+
kind: "error",
|
|
1793
|
+
text
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
function outcome(result) {
|
|
1797
|
+
return result.ok ? success(result.message) : error(result.message);
|
|
1798
|
+
}
|
|
1799
|
+
//#endregion
|
|
1800
|
+
//#region lib/types/index.js
|
|
1801
|
+
/**
|
|
1802
|
+
* @hanphone/dsh-a2a — Cordis plugin entry.
|
|
1803
|
+
*
|
|
1804
|
+
* Mounts the A2A v1.0 dual-end plugin: the inbound server (AgentCard derived
|
|
1805
|
+
* from the live tool registry, JSON-RPC + SSE, durable SQLite task store,
|
|
1806
|
+
* session/subagent executors, the `a2a/inbound-task` policy gate + audit)
|
|
1807
|
+
* and the outbound client (persisted multi-agent registry, skills mapped to
|
|
1808
|
+
* model tools, sync calls with per-agent timeout). Optional services are
|
|
1809
|
+
* probed rather than injected so a composition lacking one half idles just
|
|
1810
|
+
* that half; only the storage domain is required.
|
|
1811
|
+
*
|
|
1812
|
+
* Function-plugin export shape: named `name`/`inject`/`Config`/`apply`, no
|
|
1813
|
+
* default export (mixing forms makes the Loader drop the namespace).
|
|
1814
|
+
* @module dsh-a2a
|
|
1815
|
+
*/
|
|
1816
|
+
const name = "a2a";
|
|
1817
|
+
/** Only the storage domain is required; the other halves are probed. */
|
|
1818
|
+
const inject = ["storageDomain"];
|
|
1819
|
+
/** Loader-validated config schema (defaults applied by the Loader). */
|
|
1820
|
+
const Config = z.object({
|
|
1821
|
+
server: z.object({
|
|
1822
|
+
enabled: z.boolean().default(true),
|
|
1823
|
+
name: z.string().default("My DSH Agent"),
|
|
1824
|
+
description: z.string().default("A DeepSeek Harness agent exposed over A2A v1.0"),
|
|
1825
|
+
version: z.string().default("0.1.0"),
|
|
1826
|
+
baseUrl: z.string(),
|
|
1827
|
+
endpointPath: z.string().default("/a2a"),
|
|
1828
|
+
authTokenEnv: z.string(),
|
|
1829
|
+
skills: z.object({
|
|
1830
|
+
ids: z.array(z.string()).default([]),
|
|
1831
|
+
exclude: z.array(z.string()).default([])
|
|
1832
|
+
}),
|
|
1833
|
+
executors: z.dict(z.union(["session", "subagent"])).default({ chat: "session" }),
|
|
1834
|
+
subagentProvider: z.string().default("in-process")
|
|
1835
|
+
}),
|
|
1836
|
+
client: z.object({
|
|
1837
|
+
agents: z.array(z.object({
|
|
1838
|
+
name: z.string(),
|
|
1839
|
+
agentCardUrl: z.string(),
|
|
1840
|
+
bearerTokenEnv: z.string(),
|
|
1841
|
+
enabled: z.boolean().default(true),
|
|
1842
|
+
timeoutMs: z.number().default(6e4)
|
|
1843
|
+
})).default([]),
|
|
1844
|
+
toolPrefix: z.string().default("a2a")
|
|
1845
|
+
})
|
|
1846
|
+
});
|
|
1847
|
+
function apply(ctx, config) {
|
|
1848
|
+
const logger = ctx.logger("a2a");
|
|
1849
|
+
const serverConfig = { ...config.server };
|
|
1850
|
+
const clientConfig = { ...config.client };
|
|
1851
|
+
const authToken = serverConfig.authTokenEnv !== void 0 ? process.env[serverConfig.authTokenEnv] : void 0;
|
|
1852
|
+
const toolPrefix = clientConfig.toolPrefix;
|
|
1853
|
+
const subagentProvider = serverConfig.subagentProvider;
|
|
1854
|
+
const inboundCwd = inboundSessionCwd();
|
|
1855
|
+
ctx.inject(["storageDomain"], (ctx) => {
|
|
1856
|
+
ctx.effect(async () => {
|
|
1857
|
+
const domain = await openDomain(ctx.storageDomain);
|
|
1858
|
+
const store = new DomainTaskStore(domain);
|
|
1859
|
+
const holder = {
|
|
1860
|
+
domain,
|
|
1861
|
+
store,
|
|
1862
|
+
registry: void 0,
|
|
1863
|
+
server: void 0,
|
|
1864
|
+
routes: void 0,
|
|
1865
|
+
card: void 0,
|
|
1866
|
+
executors: void 0,
|
|
1867
|
+
enabled: serverConfig.enabled
|
|
1868
|
+
};
|
|
1869
|
+
const webServer = probeService(ctx, "webServer", "register");
|
|
1870
|
+
if (webServer === void 0) logger.warn("a2a: webServer not mounted; inbound server idle");
|
|
1871
|
+
else try {
|
|
1872
|
+
const skills = deriveSkills(probeService(ctx, "tools", "get") ?? { get: () => void 0 }, {
|
|
1873
|
+
ids: serverConfig.skills.ids,
|
|
1874
|
+
exclude: serverConfig.skills.exclude
|
|
1875
|
+
});
|
|
1876
|
+
const card = buildCard({
|
|
1877
|
+
baseUrl: serverConfig.baseUrl ?? `http://127.0.0.1:${process.env["DSH_A2A_PORT"] ?? "3000"}`,
|
|
1878
|
+
endpointPath: serverConfig.endpointPath ?? "/a2a",
|
|
1879
|
+
name: serverConfig.name ?? "My DSH Agent",
|
|
1880
|
+
description: serverConfig.description ?? "A DeepSeek Harness agent exposed over A2A v1.0",
|
|
1881
|
+
version: serverConfig.version ?? "0.1.0",
|
|
1882
|
+
skills,
|
|
1883
|
+
...authToken ? { authToken } : {}
|
|
1884
|
+
});
|
|
1885
|
+
holder.card = card;
|
|
1886
|
+
const agents = probeService(ctx, "agents", "create");
|
|
1887
|
+
const presets = probeService(ctx, "agentPresets", "resolve");
|
|
1888
|
+
const sessionPool = agents ? new ContextSessionPool(agents, {
|
|
1889
|
+
cwd: inboundCwd,
|
|
1890
|
+
...presets ? { agentPresets: presets } : {},
|
|
1891
|
+
resolveAgentOptions: () => resolveDefaultModel(ctx)
|
|
1892
|
+
}) : void 0;
|
|
1893
|
+
const sessionExecutor = sessionPool ? createSessionExecutor(sessionPool) : refuseExecutor("no agent loop mounted");
|
|
1894
|
+
const subagents = probeService(ctx, "subagents", "start");
|
|
1895
|
+
const subagentExecutor = sessionPool !== void 0 && subagents !== void 0 ? createSubagentExecutor({
|
|
1896
|
+
pool: sessionPool,
|
|
1897
|
+
subagents,
|
|
1898
|
+
provider: subagentProvider
|
|
1899
|
+
}) : void 0;
|
|
1900
|
+
const executors = new ExecutorSet(serverConfig.executors, sessionExecutor, subagentExecutor);
|
|
1901
|
+
holder.executors = executors;
|
|
1902
|
+
const skillIds = new Set(skills.map((s) => s.id));
|
|
1903
|
+
const gate = async (input) => {
|
|
1904
|
+
if (!skillIds.has(input.skill)) return {
|
|
1905
|
+
ok: false,
|
|
1906
|
+
reason: `unknown skill "${input.skill}"`
|
|
1907
|
+
};
|
|
1908
|
+
const decision = {
|
|
1909
|
+
contextId: input.contextId,
|
|
1910
|
+
skill: input.skill,
|
|
1911
|
+
parts: input.parts,
|
|
1912
|
+
remotePeerId: input.remotePeerId
|
|
1913
|
+
};
|
|
1914
|
+
const decided = await ctx.waterfall("a2a/inbound-task", decision, async (d) => d);
|
|
1915
|
+
logger.info(`[a2a] inbound skill=${decided.skill} remote=${decided.remotePeerId} rejected=${decided.rejected?.reason ?? "no"}`);
|
|
1916
|
+
if (decided.rejected !== void 0) return {
|
|
1917
|
+
ok: false,
|
|
1918
|
+
reason: decided.rejected.reason
|
|
1919
|
+
};
|
|
1920
|
+
return { ok: true };
|
|
1921
|
+
};
|
|
1922
|
+
const server = new A2AServer({
|
|
1923
|
+
card,
|
|
1924
|
+
store,
|
|
1925
|
+
executors,
|
|
1926
|
+
...authToken ? { authToken } : {},
|
|
1927
|
+
gate,
|
|
1928
|
+
onTaskSettled: (taskId) => logger.info(`[a2a] task settled ${taskId}`)
|
|
1929
|
+
});
|
|
1930
|
+
holder.server = server;
|
|
1931
|
+
const routes = new A2aRoutes(webServer, server);
|
|
1932
|
+
holder.routes = routes;
|
|
1933
|
+
if (holder.enabled) routes.enable();
|
|
1934
|
+
} catch (err) {
|
|
1935
|
+
logger.error(`a2a: server half failed to build: ${err.message}`);
|
|
1936
|
+
}
|
|
1937
|
+
const toolsService = probeService(ctx, "tools", "get");
|
|
1938
|
+
if (toolsService === void 0) logger.warn("a2a: tools service not mounted; outbound client idle");
|
|
1939
|
+
else {
|
|
1940
|
+
const registry = new OutboundAgentRegistry({
|
|
1941
|
+
registrar: { register: (def) => toolsService.register(def) },
|
|
1942
|
+
store: new DomainAgentStore(domain.agents),
|
|
1943
|
+
toolPrefix,
|
|
1944
|
+
tokenOf: (env) => env ? process.env[env] : void 0,
|
|
1945
|
+
onError: (message) => logger.warn(message)
|
|
1946
|
+
});
|
|
1947
|
+
holder.registry = registry;
|
|
1948
|
+
registry.loadAll();
|
|
1949
|
+
if (clientConfig.agents.length > 0) await registry.seed(clientConfig.agents);
|
|
1950
|
+
}
|
|
1951
|
+
const impl = makeFacade(holder, logger.info.bind(logger));
|
|
1952
|
+
new A2AService(ctx, impl);
|
|
1953
|
+
const commands = probeService(ctx, "commands", "register");
|
|
1954
|
+
if (commands !== void 0) ctx.effect(() => commands.register(buildA2aCommand(impl)), "a2a: command");
|
|
1955
|
+
const dashboardWebServer = probeService(ctx, "webServer", "register");
|
|
1956
|
+
if (dashboardWebServer !== void 0) ctx.effect(() => dashboardWebServer.register({
|
|
1957
|
+
kind: "prefix",
|
|
1958
|
+
path: "/a2a/api",
|
|
1959
|
+
handler: (req, res) => handleApiRequest(req, res, impl)
|
|
1960
|
+
}), "a2a: dashboard api");
|
|
1961
|
+
else logger.warn("a2a: webServer not mounted; GUI dashboard API idle");
|
|
1962
|
+
return async () => {
|
|
1963
|
+
holder.routes?.dispose();
|
|
1964
|
+
await holder.executors?.disposeAll();
|
|
1965
|
+
await holder.registry?.disposeAll();
|
|
1966
|
+
await domain.close();
|
|
1967
|
+
};
|
|
1968
|
+
}, "a2a: domain + halves");
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
/** Facade closure over the shared holder (commands and `ctx.a2a` consumers). */
|
|
1972
|
+
function makeFacade(holder, log) {
|
|
1973
|
+
const serverEnabled = () => holder.routes?.active ?? false;
|
|
1974
|
+
return {
|
|
1975
|
+
status() {
|
|
1976
|
+
return {
|
|
1977
|
+
server: {
|
|
1978
|
+
enabled: serverEnabled(),
|
|
1979
|
+
cardUrl: holder.card?.supportedInterfaces?.[0]?.url,
|
|
1980
|
+
skills: holder.card?.skills?.map((s) => s.id) ?? [],
|
|
1981
|
+
executors: holder.executors ? ["session", ...holder.executors["subagent"] !== void 0 ? ["subagent"] : []] : []
|
|
1982
|
+
},
|
|
1983
|
+
tasks: holder.store.list().length,
|
|
1984
|
+
agents: holder.registry?.list() ?? []
|
|
1985
|
+
};
|
|
1986
|
+
},
|
|
1987
|
+
async enableServer(enable) {
|
|
1988
|
+
if (holder.routes === void 0) return {
|
|
1989
|
+
ok: false,
|
|
1990
|
+
message: "inbound server is not mounted (no webServer)"
|
|
1991
|
+
};
|
|
1992
|
+
if (enable) holder.routes.enable();
|
|
1993
|
+
else holder.routes.disable();
|
|
1994
|
+
log(`[a2a] server ${enable ? "enabled" : "disabled"}`);
|
|
1995
|
+
return {
|
|
1996
|
+
ok: true,
|
|
1997
|
+
message: `server ${enable ? "enabled" : "disabled"}`
|
|
1998
|
+
};
|
|
1999
|
+
},
|
|
2000
|
+
getTask(taskId) {
|
|
2001
|
+
return holder.store.get(taskId);
|
|
2002
|
+
},
|
|
2003
|
+
listTasks() {
|
|
2004
|
+
return holder.store.list();
|
|
2005
|
+
},
|
|
2006
|
+
async cancelTask(taskId) {
|
|
2007
|
+
if (holder.store.get(taskId) === void 0) return {
|
|
2008
|
+
ok: false,
|
|
2009
|
+
message: `task ${taskId} not found`
|
|
2010
|
+
};
|
|
2011
|
+
return {
|
|
2012
|
+
ok: true,
|
|
2013
|
+
message: holder.server?.abort?.(taskId) ?? false ? `task ${taskId} canceled` : `task ${taskId} already terminal`
|
|
2014
|
+
};
|
|
2015
|
+
},
|
|
2016
|
+
agents() {
|
|
2017
|
+
return holder.registry?.list() ?? [];
|
|
2018
|
+
},
|
|
2019
|
+
async addAgent(spec) {
|
|
2020
|
+
const registry = holder.registry;
|
|
2021
|
+
if (registry === void 0) return {
|
|
2022
|
+
ok: false,
|
|
2023
|
+
message: "outbound client not mounted (no tools service)"
|
|
2024
|
+
};
|
|
2025
|
+
return await registry.add({
|
|
2026
|
+
...spec,
|
|
2027
|
+
enabled: true,
|
|
2028
|
+
timeoutMs: 6e4
|
|
2029
|
+
});
|
|
2030
|
+
},
|
|
2031
|
+
async removeAgent(id) {
|
|
2032
|
+
return holder.registry?.remove(id) ?? {
|
|
2033
|
+
ok: false,
|
|
2034
|
+
message: "outbound client not mounted"
|
|
2035
|
+
};
|
|
2036
|
+
},
|
|
2037
|
+
async setAgentEnabled(id, enabled) {
|
|
2038
|
+
return holder.registry?.setEnabled(id, enabled) ?? {
|
|
2039
|
+
ok: false,
|
|
2040
|
+
message: "outbound client not mounted"
|
|
2041
|
+
};
|
|
2042
|
+
},
|
|
2043
|
+
async refreshAgentCard(id) {
|
|
2044
|
+
return holder.registry?.refresh(id) ?? {
|
|
2045
|
+
ok: false,
|
|
2046
|
+
message: "outbound client not mounted"
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
/** A task that refuses readably on compositions without an agent loop. */
|
|
2052
|
+
function refuseExecutor(reason) {
|
|
2053
|
+
return {
|
|
2054
|
+
name: "refuse",
|
|
2055
|
+
async execute() {
|
|
2056
|
+
throw new Error(`a2a: ${reason}; refusing inbound task`);
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
function resolveDefaultModel(ctx) {
|
|
2061
|
+
return probeService(ctx, "agentDefaultModel", "currentSelection")?.currentSelection();
|
|
2062
|
+
}
|
|
2063
|
+
/** Absolute stable cwd for inbound per-context sessions (never profile root). */
|
|
2064
|
+
function inboundSessionCwd() {
|
|
2065
|
+
const home = join(homedir(), ".dsh");
|
|
2066
|
+
const dir = join(process.env["DSH_HOME"] ?? home, "a2a-sessions");
|
|
2067
|
+
try {
|
|
2068
|
+
mkdirSync(dir, { recursive: true });
|
|
2069
|
+
} catch {}
|
|
2070
|
+
return dir;
|
|
2071
|
+
}
|
|
2072
|
+
//#endregion
|
|
2073
|
+
export { A2AClient, A2AError, A2AServer, A2AService, Config, apply, inject, name };
|