@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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -0
  3. package/README.zh.md +195 -0
  4. package/cordis.patch.yml +11 -0
  5. package/lib/client.js +166 -0
  6. package/lib/index.js +2073 -0
  7. package/lib/tsconfig.client.tsbuildinfo +1 -0
  8. package/lib/tsconfig.tsbuildinfo +1 -0
  9. package/lib/types/api.d.ts +41 -0
  10. package/lib/types/api.d.ts.map +1 -0
  11. package/lib/types/api.js +86 -0
  12. package/lib/types/api.js.map +1 -0
  13. package/lib/types/client/index.d.ts +35 -0
  14. package/lib/types/client/index.d.ts.map +1 -0
  15. package/lib/types/client/index.js +155 -0
  16. package/lib/types/client/index.js.map +1 -0
  17. package/lib/types/commands.d.ts +10 -0
  18. package/lib/types/commands.d.ts.map +1 -0
  19. package/lib/types/commands.js +93 -0
  20. package/lib/types/commands.js.map +1 -0
  21. package/lib/types/events.d.ts +74 -0
  22. package/lib/types/events.d.ts.map +1 -0
  23. package/lib/types/events.js +10 -0
  24. package/lib/types/events.js.map +1 -0
  25. package/lib/types/index.d.ts +54 -0
  26. package/lib/types/index.d.ts.map +1 -0
  27. package/lib/types/index.js +296 -0
  28. package/lib/types/index.js.map +1 -0
  29. package/lib/types/jsonrpc.d.ts +20 -0
  30. package/lib/types/jsonrpc.d.ts.map +1 -0
  31. package/lib/types/jsonrpc.js +48 -0
  32. package/lib/types/jsonrpc.js.map +1 -0
  33. package/lib/types/outbound/calls.d.ts +37 -0
  34. package/lib/types/outbound/calls.d.ts.map +1 -0
  35. package/lib/types/outbound/calls.js +120 -0
  36. package/lib/types/outbound/calls.js.map +1 -0
  37. package/lib/types/outbound/registry.d.ts +102 -0
  38. package/lib/types/outbound/registry.d.ts.map +1 -0
  39. package/lib/types/outbound/registry.js +249 -0
  40. package/lib/types/outbound/registry.js.map +1 -0
  41. package/lib/types/outbound/tools.d.ts +30 -0
  42. package/lib/types/outbound/tools.d.ts.map +1 -0
  43. package/lib/types/outbound/tools.js +86 -0
  44. package/lib/types/outbound/tools.js.map +1 -0
  45. package/lib/types/protocol.d.ts +194 -0
  46. package/lib/types/protocol.d.ts.map +1 -0
  47. package/lib/types/protocol.js +78 -0
  48. package/lib/types/protocol.js.map +1 -0
  49. package/lib/types/server/a2a-server.d.ts +98 -0
  50. package/lib/types/server/a2a-server.d.ts.map +1 -0
  51. package/lib/types/server/a2a-server.js +397 -0
  52. package/lib/types/server/a2a-server.js.map +1 -0
  53. package/lib/types/server/card.d.ts +48 -0
  54. package/lib/types/server/card.d.ts.map +1 -0
  55. package/lib/types/server/card.js +75 -0
  56. package/lib/types/server/card.js.map +1 -0
  57. package/lib/types/server/exec/agent-runtime.d.ts +126 -0
  58. package/lib/types/server/exec/agent-runtime.d.ts.map +1 -0
  59. package/lib/types/server/exec/agent-runtime.js +138 -0
  60. package/lib/types/server/exec/agent-runtime.js.map +1 -0
  61. package/lib/types/server/exec/session.d.ts +11 -0
  62. package/lib/types/server/exec/session.d.ts.map +1 -0
  63. package/lib/types/server/exec/session.js +22 -0
  64. package/lib/types/server/exec/session.js.map +1 -0
  65. package/lib/types/server/exec/subagent.d.ts +39 -0
  66. package/lib/types/server/exec/subagent.d.ts.map +1 -0
  67. package/lib/types/server/exec/subagent.js +44 -0
  68. package/lib/types/server/exec/subagent.js.map +1 -0
  69. package/lib/types/server/executor.d.ts +63 -0
  70. package/lib/types/server/executor.d.ts.map +1 -0
  71. package/lib/types/server/executor.js +38 -0
  72. package/lib/types/server/executor.js.map +1 -0
  73. package/lib/types/server/routes.d.ts +32 -0
  74. package/lib/types/server/routes.d.ts.map +1 -0
  75. package/lib/types/server/routes.js +131 -0
  76. package/lib/types/server/routes.js.map +1 -0
  77. package/lib/types/server/store.d.ts +157 -0
  78. package/lib/types/server/store.d.ts.map +1 -0
  79. package/lib/types/server/store.js +239 -0
  80. package/lib/types/server/store.js.map +1 -0
  81. package/lib/types/service.d.ts +49 -0
  82. package/lib/types/service.d.ts.map +1 -0
  83. package/lib/types/service.js +46 -0
  84. package/lib/types/service.js.map +1 -0
  85. package/package.json +82 -0
  86. package/src/api.ts +115 -0
  87. package/src/client/index.ts +293 -0
  88. package/src/commands.ts +92 -0
  89. package/src/events.ts +58 -0
  90. package/src/index.ts +353 -0
  91. package/src/jsonrpc.ts +61 -0
  92. package/src/outbound/calls.ts +145 -0
  93. package/src/outbound/registry.ts +290 -0
  94. package/src/outbound/tools.ts +118 -0
  95. package/src/protocol.ts +212 -0
  96. package/src/server/a2a-server.ts +455 -0
  97. package/src/server/card.ts +106 -0
  98. package/src/server/exec/agent-runtime.ts +197 -0
  99. package/src/server/exec/session.ts +25 -0
  100. package/src/server/exec/subagent.ts +67 -0
  101. package/src/server/executor.ts +73 -0
  102. package/src/server/routes.ts +156 -0
  103. package/src/server/store.ts +320 -0
  104. 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 };