@hanphone/dsh-a2a 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +101 -106
  2. package/README.zh.md +50 -74
  3. package/cordis.patch.yml +4 -7
  4. package/lib/client.js +217 -73
  5. package/lib/index.js +1258 -867
  6. package/lib/types/api.d.ts +40 -23
  7. package/lib/types/api.d.ts.map +1 -1
  8. package/lib/types/api.js +58 -23
  9. package/lib/types/api.js.map +1 -1
  10. package/lib/types/client/index.d.ts +50 -19
  11. package/lib/types/client/index.d.ts.map +1 -1
  12. package/lib/types/commands.d.ts +4 -2
  13. package/lib/types/commands.d.ts.map +1 -1
  14. package/lib/types/commands.js +59 -46
  15. package/lib/types/commands.js.map +1 -1
  16. package/lib/types/events.d.ts +32 -9
  17. package/lib/types/events.d.ts.map +1 -1
  18. package/lib/types/index.d.ts +21 -32
  19. package/lib/types/index.d.ts.map +1 -1
  20. package/lib/types/index.js +173 -272
  21. package/lib/types/index.js.map +1 -1
  22. package/lib/types/outbound/calls.d.ts.map +1 -1
  23. package/lib/types/outbound/calls.js +8 -6
  24. package/lib/types/outbound/calls.js.map +1 -1
  25. package/lib/types/protocol.d.ts +174 -109
  26. package/lib/types/protocol.d.ts.map +1 -1
  27. package/lib/types/protocol.js +58 -34
  28. package/lib/types/protocol.js.map +1 -1
  29. package/lib/types/server/a2a-server.d.ts +4 -0
  30. package/lib/types/server/a2a-server.d.ts.map +1 -1
  31. package/lib/types/server/a2a-server.js +6 -2
  32. package/lib/types/server/a2a-server.js.map +1 -1
  33. package/lib/types/server/card.d.ts +6 -30
  34. package/lib/types/server/card.d.ts.map +1 -1
  35. package/lib/types/server/card.js +13 -49
  36. package/lib/types/server/card.js.map +1 -1
  37. package/lib/types/server/exec/agent-runtime.d.ts +3 -0
  38. package/lib/types/server/exec/agent-runtime.d.ts.map +1 -1
  39. package/lib/types/server/exec/agent-runtime.js +2 -1
  40. package/lib/types/server/exec/agent-runtime.js.map +1 -1
  41. package/lib/types/server/routes.d.ts +5 -1
  42. package/lib/types/server/routes.d.ts.map +1 -1
  43. package/lib/types/server/routes.js +9 -5
  44. package/lib/types/server/routes.js.map +1 -1
  45. package/lib/types/server/store.d.ts +35 -1
  46. package/lib/types/server/store.d.ts.map +1 -1
  47. package/lib/types/server/store.js +10 -0
  48. package/lib/types/server/store.js.map +1 -1
  49. package/lib/types/servers/inbound-manager.d.ts +131 -0
  50. package/lib/types/servers/inbound-manager.d.ts.map +1 -0
  51. package/lib/types/servers/inbound-manager.js +312 -0
  52. package/lib/types/servers/inbound-manager.js.map +1 -0
  53. package/lib/types/servers/outbound-manager.d.ts +129 -0
  54. package/lib/types/servers/outbound-manager.d.ts.map +1 -0
  55. package/lib/types/servers/outbound-manager.js +238 -0
  56. package/lib/types/servers/outbound-manager.js.map +1 -0
  57. package/lib/types/service.d.ts +106 -40
  58. package/lib/types/service.d.ts.map +1 -1
  59. package/lib/types/service.js +36 -27
  60. package/lib/types/service.js.map +1 -1
  61. package/package.json +2 -2
  62. package/src/api.ts +67 -53
  63. package/src/client/index.ts +309 -148
  64. package/src/commands.ts +59 -43
  65. package/src/events.ts +14 -7
  66. package/src/index.ts +199 -310
  67. package/src/outbound/calls.ts +8 -6
  68. package/src/protocol.ts +204 -95
  69. package/src/server/a2a-server.ts +9 -2
  70. package/src/server/card.ts +13 -62
  71. package/src/server/exec/agent-runtime.ts +5 -2
  72. package/src/server/routes.ts +6 -4
  73. package/src/server/store.ts +45 -4
  74. package/src/servers/inbound-manager.ts +385 -0
  75. package/src/servers/outbound-manager.ts +289 -0
  76. package/src/service.ts +125 -40
  77. package/lib/tsconfig.client.tsbuildinfo +0 -1
  78. package/lib/tsconfig.tsbuildinfo +0 -1
package/lib/index.js CHANGED
@@ -5,107 +5,28 @@ import { mkdirSync } from "node:fs";
5
5
  import { z as z$1 } from "zod";
6
6
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
7
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
8
  //#region lib/types/protocol.js
89
9
  /**
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.
10
+ * Agent2Agent (A2A) Protocol v1.0.1 — types, JSON-RPC methods, task states,
11
+ * error codes and JSON serialization, transcribed from the official
12
+ * `a2aproject/A2A` specification (Apache-2.0), cross-checked against
13
+ * `specification/a2a.proto` and `docs/specification.md` (JSON-RPC binding,
14
+ * ADR-001 ProtoJSON). This file is the plugin's single source of protocol
15
+ * truth for the JSON-RPC over HTTP binding. Other bindings (gRPC, REST) are
16
+ * not provided.
96
17
  * @module dsh-a2a/protocol
97
18
  */
98
- /** Task lifecycle states (spec `TaskState`). */
19
+ /** Task lifecycle states (spec `TaskState`; JSON = SCREAMING_SNAKE_CASE per ADR-001). */
99
20
  var TaskState;
100
21
  (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";
22
+ TaskState["SUBMITTED"] = "TASK_STATE_SUBMITTED";
23
+ TaskState["WORKING"] = "TASK_STATE_WORKING";
24
+ TaskState["INPUT_REQUIRED"] = "TASK_STATE_INPUT_REQUIRED";
25
+ TaskState["AUTH_REQUIRED"] = "TASK_STATE_AUTH_REQUIRED";
26
+ TaskState["COMPLETED"] = "TASK_STATE_COMPLETED";
27
+ TaskState["FAILED"] = "TASK_STATE_FAILED";
28
+ TaskState["CANCELED"] = "TASK_STATE_CANCELED";
29
+ TaskState["REJECTED"] = "TASK_STATE_REJECTED";
109
30
  })(TaskState || (TaskState = {}));
110
31
  /** States that settle a task; a settled task no longer transitions. */
111
32
  const TERMINAL_STATES = new Set([
@@ -114,17 +35,34 @@ const TERMINAL_STATES = new Set([
114
35
  TaskState.CANCELED,
115
36
  TaskState.REJECTED
116
37
  ]);
38
+ new Set([TaskState.INPUT_REQUIRED, TaskState.AUTH_REQUIRED]);
117
39
  /** @returns whether the state settles the task. */
118
40
  function isTerminal(state) {
119
41
  return TERMINAL_STATES.has(state);
120
42
  }
121
- /** Message sender role (spec `Role`). */
43
+ /** Message sender role (spec `Role`; JSON = `ROLE_*` per ADR-001). */
122
44
  var Role;
123
45
  (function(Role) {
124
- Role["USER"] = "user";
125
- Role["AGENT"] = "agent";
46
+ Role["USER"] = "ROLE_USER";
47
+ Role["AGENT"] = "ROLE_AGENT";
126
48
  })(Role || (Role = {}));
127
- /** A2A v1.0 JSON-RPC method names (canonical PascalCase as in a2a.proto). */
49
+ /** JSON-RPC + A2A error codes. */
50
+ const A2A_ERROR_CODES = {
51
+ PARSE_ERROR: -32700,
52
+ INVALID_REQUEST: -32600,
53
+ METHOD_NOT_FOUND: -32601,
54
+ INVALID_PARAMS: -32602,
55
+ INTERNAL_ERROR: -32603,
56
+ TASK_NOT_FOUND: -32001,
57
+ TASK_CANCEL_NOT_ALLOWED: -32002,
58
+ PUSH_NOTIFICATION_NOT_SUPPORTED: -32003,
59
+ UNSUPPORTED_OPERATION: -32004,
60
+ CONTENT_TYPE_NOT_SUPPORTED: -32005,
61
+ STREAMING_NOT_SUPPORTED: -32006,
62
+ VERSION_NOT_SUPPORTED: -32007,
63
+ INVALID_AGENT_RESPONSE: -32008
64
+ };
65
+ /** A2A v1.0 JSON-RPC method names (spec §5.3 Method Mapping Reference). */
128
66
  const A2A_METHODS = {
129
67
  sendMessage: "SendMessage",
130
68
  sendStreamingMessage: "SendStreamingMessage",
@@ -132,27 +70,25 @@ const A2A_METHODS = {
132
70
  listTasks: "ListTasks",
133
71
  cancelTask: "CancelTask",
134
72
  subscribeToTask: "SubscribeToTask",
73
+ createTaskPushNotificationConfig: "CreateTaskPushNotificationConfig",
74
+ getTaskPushNotificationConfig: "GetTaskPushNotificationConfig",
75
+ listTaskPushNotificationConfigs: "ListTaskPushNotificationConfigs",
76
+ deleteTaskPushNotificationConfig: "DeleteTaskPushNotificationConfig",
135
77
  getExtendedAgentCard: "GetExtendedAgentCard"
136
78
  };
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). */
79
+ /**
80
+ * Read a message's text parts as a single string (model-facing convenience).
81
+ * File parts render as their URL/name; data parts as JSON.
82
+ */
150
83
  function partsToText(parts) {
151
84
  if (!parts) return "";
152
85
  return parts.map((p) => {
153
86
  if ("text" in p && p.text) return p.text;
154
87
  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"}]`;
88
+ if ("raw" in p || "url" in p || "filename" in p) {
89
+ const f = p;
90
+ return f.url ?? `[file ${f.filename ?? f.mediaType ?? "binary"}]`;
91
+ }
156
92
  return "";
157
93
  }).filter(Boolean).join("\n");
158
94
  }
@@ -176,7 +112,9 @@ const a2aDomainSpec = defineDomain({
176
112
  tasks: domainTable(json$1),
177
113
  contexts: domainTable(json$1),
178
114
  agents: domainTable(json$1),
179
- identity: domainTable(json$1)
115
+ identity: domainTable(json$1),
116
+ inbound_servers: domainTable(json$1),
117
+ outbound_servers: domainTable(json$1)
180
118
  }
181
119
  });
182
120
  /**
@@ -218,6 +156,12 @@ var A2aDomain = class {
218
156
  get identity() {
219
157
  return this.handle.table("identity");
220
158
  }
159
+ get inbound_servers() {
160
+ return this.handle.table("inbound_servers");
161
+ }
162
+ get outbound_servers() {
163
+ return this.handle.table("outbound_servers");
164
+ }
221
165
  async close() {
222
166
  await this.handle.close();
223
167
  }
@@ -273,7 +217,8 @@ var DomainTaskStore = class {
273
217
  parts: input.parts,
274
218
  artifacts: [],
275
219
  executor: "session",
276
- summary: null
220
+ summary: null,
221
+ ...input.serverId !== void 0 ? { serverId: input.serverId } : {}
277
222
  };
278
223
  this.records.set(record.taskId, record);
279
224
  this.domain.tasks.put(recordKey(record.taskId), encodeRecord(record)).catch((err) => {
@@ -333,207 +278,6 @@ function withState(current, state, message, timestamp) {
333
278
  };
334
279
  }
335
280
  //#endregion
336
- //#region lib/types/server/inbound-registry.js
337
- /**
338
- * Inbound connection registry: which remote peers are talking to this DSH's
339
- * A2A server. Fed by the A2A server's `onInbound` hook (every JSON-RPC
340
- * request / SSE open), surfaced through the dashboard API, and disconnectable
341
- * (`closePeer` cancels the peer's active tasks and drops the record).
342
- * @module dsh-a2a/server/inbound-registry
343
- */
344
- function newPeerId() {
345
- return `peer-${crypto.randomUUID()}`;
346
- }
347
- /** In-memory inbound peer registry. */
348
- var LiveInboundRegistry = class {
349
- peers = /* @__PURE__ */ new Map();
350
- findBySource(source) {
351
- if (source === null) return void 0;
352
- for (const peer of this.peers.values()) if (peer.source === source) return peer;
353
- }
354
- /** Observe one inbound event: method, source, task ids, streaming flag. */
355
- note(input) {
356
- const now = (/* @__PURE__ */ new Date()).toISOString();
357
- const source = input.source ?? null;
358
- let peer = this.findBySource(source);
359
- if (peer === void 0) {
360
- peer = {
361
- id: newPeerId(),
362
- label: source ?? "unknown",
363
- source,
364
- firstSeen: now,
365
- lastSeen: now,
366
- taskCount: 0,
367
- active: /* @__PURE__ */ new Set(),
368
- streamingCount: 0
369
- };
370
- this.peers.set(peer.id, peer);
371
- }
372
- peer.lastSeen = now;
373
- peer.taskCount += input.taskIds.length;
374
- for (const id of input.taskIds) peer.active.add(id);
375
- if (input.streaming) peer.streamingCount += 1;
376
- }
377
- /** A task settled: drop it from every peer's active set. */
378
- settle(taskId) {
379
- for (const peer of this.peers.values()) if (peer.active.delete(taskId)) peer.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
380
- }
381
- /** Decrement streaming count when an SSE connection closes. */
382
- endStream(source) {
383
- const peer = this.findBySource(source ?? null);
384
- if (peer !== void 0 && peer.streamingCount > 0) {
385
- peer.streamingCount -= 1;
386
- peer.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
387
- }
388
- }
389
- list() {
390
- return [...this.peers.values()].map((p) => ({
391
- id: p.id,
392
- label: p.label,
393
- source: p.source,
394
- firstSeen: p.firstSeen,
395
- lastSeen: p.lastSeen,
396
- taskCount: p.taskCount,
397
- activeTaskIds: [...p.active],
398
- streaming: p.streamingCount > 0
399
- })).sort((a, b) => a.lastSeen < b.lastSeen ? 1 : -1);
400
- }
401
- closePeer(peerId) {
402
- const peer = this.peers.get(peerId);
403
- if (peer === void 0) return {
404
- ok: false,
405
- message: `inbound peer ${peerId} not found`
406
- };
407
- this.peers.delete(peerId);
408
- return {
409
- ok: true,
410
- message: `inbound peer ${peer.label} closed`
411
- };
412
- }
413
- activeTasksOf(peerId) {
414
- return [...this.peers.get(peerId)?.active ?? []];
415
- }
416
- };
417
- //#endregion
418
- //#region lib/types/server/identity.js
419
- /**
420
- * Service identity: the inbound AgentCard's name/description/version,
421
- * editable at runtime from the GUI dashboard and persisted in the `a2a`
422
- * domain's `identity` table. Skills stay derived from the live tool registry
423
- * (the "card derives from ctx.tools" design); identity editing builds a fresh
424
- * card preserving the endpoint URL and swaps it onto the server, so routes
425
- * and the facade see the new value immediately.
426
- * @module dsh-a2a/server/identity
427
- */
428
- const IDENTITY_KEY = "service";
429
- function decode(raw) {
430
- if (raw === void 0) return void 0;
431
- try {
432
- const parsed = JSON.parse(raw);
433
- if (typeof parsed.name !== "string" || typeof parsed.description !== "string" || typeof parsed.version !== "string") return;
434
- return {
435
- name: parsed.name,
436
- description: parsed.description,
437
- version: parsed.version
438
- };
439
- } catch {
440
- return;
441
- }
442
- }
443
- function encode(value) {
444
- return JSON.stringify(value);
445
- }
446
- /** Read the persisted identity (undefined when never configured). */
447
- function readIdentity(domain) {
448
- return decode(domain.identity.get(IDENTITY_KEY));
449
- }
450
- /** Persist a new identity (fire-and-forget like the task store writes). */
451
- function writeIdentity(domain, identity) {
452
- domain.identity.put(IDENTITY_KEY, encode(identity)).catch((err) => {
453
- throw new Error(`a2a: identity persistence failed: ${String(err)}`);
454
- });
455
- }
456
- /**
457
- * Build the AgentCard options for a given base URL/path and the override
458
- * identity. `identity` (persisted) wins over the composition `defaults`; when
459
- * no identity is stored, the composition defaults stand.
460
- */
461
- function cardOptionsFor(baseUrl, endpointPath, defaults, identity, skills, authToken) {
462
- return {
463
- baseUrl,
464
- endpointPath,
465
- name: identity?.name ?? defaults.name,
466
- description: identity?.description ?? defaults.description,
467
- version: identity?.version ?? defaults.version,
468
- skills,
469
- ...authToken !== void 0 ? { authToken } : {}
470
- };
471
- }
472
- /**
473
- * Build a fresh AgentCard from an existing one plus a new identity and skill
474
- * list, preserving the endpoint URL (baseUrl/path) and security scheme.
475
- */
476
- function rebuildCardWithIdentity(card, identity, skills) {
477
- return buildCard({
478
- baseUrl: endpointBaseOf(card),
479
- endpointPath: endpointPathOf$1(card),
480
- name: identity.name,
481
- description: identity.description,
482
- version: identity.version,
483
- skills,
484
- ...card.securitySchemes !== void 0 ? { authToken: "preserved-scheme" } : {}
485
- });
486
- }
487
- function endpointBaseOf(card) {
488
- const url = card.supportedInterfaces?.[0]?.url;
489
- if (!url) return "http://127.0.0.1";
490
- return url.replace(/\/[^/]*$/, "");
491
- }
492
- function endpointPathOf$1(card) {
493
- const url = card.supportedInterfaces?.[0]?.url;
494
- if (!url) return "/a2a";
495
- try {
496
- return new URL(url).pathname;
497
- } catch {
498
- return "/a2a";
499
- }
500
- }
501
- //#endregion
502
- //#region lib/types/server/executor.js
503
- /**
504
- * Executor abstraction for inbound tasks: an executor turns one task into
505
- * output parts while reporting status and artifact updates. The registry is
506
- * INTERNAL (built by the plugin from config) — public registration was cut:
507
- * there is no external caller yet, so the seam stays an interface with two
508
- * built-in implementations (session, subagent).
509
- * @module dsh-a2a/server/executor
510
- */
511
- /**
512
- * Skill → executor resolution. The subagent executor is absent on
513
- * compositions without the subagent seam; resolving a skill bound to a
514
- * missing kind fails loudly with a readable message.
515
- */
516
- var ExecutorSet = class {
517
- bySkill;
518
- session;
519
- subagent;
520
- constructor(bySkill, session, subagent) {
521
- this.bySkill = bySkill;
522
- this.session = session;
523
- this.subagent = subagent;
524
- }
525
- resolve(skill) {
526
- if ((this.bySkill[skill] ?? "session") === "session") return this.session;
527
- const subagent = this.subagent;
528
- if (subagent === void 0) throw new Error(`a2a: skill "${skill}" is bound to the subagent executor but no subagent seam is mounted`);
529
- return subagent;
530
- }
531
- async disposeAll() {
532
- await this.session.disposeAll?.();
533
- await this.subagent?.disposeAll?.();
534
- }
535
- };
536
- //#endregion
537
281
  //#region lib/types/server/exec/agent-runtime.js
538
282
  /**
539
283
  * Shared agent machinery for the inbound executors: one DSH agent session per
@@ -576,7 +320,8 @@ var ContextSessionPool = class ContextSessionPool {
576
320
  let presetId;
577
321
  let setup;
578
322
  if (presets !== void 0) {
579
- presetId = (await presets.resolve()).id;
323
+ const wanted = this.opts.presetId?.() ?? void 0;
324
+ presetId = (wanted !== void 0 ? await presets.resolve(wanted) : await presets.resolve()).id;
580
325
  setup = async (agentCtx) => {
581
326
  await presets.mount(agentCtx, presetId);
582
327
  };
@@ -669,62 +414,328 @@ function probeService(ctx, name, method) {
669
414
  } catch {}
670
415
  }
671
416
  //#endregion
672
- //#region lib/types/server/exec/session.js
417
+ //#region lib/types/api.js
673
418
  /**
674
- * The `session` executor: one inbound task is one turn on the context's DSH
675
- * agent session; reply text becomes the task's output part. Works on any
676
- * composition with an agent loop (the `chat` skill and the default path).
677
- * @module dsh-a2a/server/exec/session
419
+ * GUI control API for the A2A settings dashboard: one `GET /a2a/api`
420
+ * snapshot plus `POST /a2a/api` control actions (inbound/outbound server
421
+ * CRUD + enable/disable, task view/cancel) and `GET /a2a/api/presets` for the
422
+ * preset roster the pickers need. The browser half talks only to this route;
423
+ * it carries no protocol knowledge. Loopback-only by default so the dashboard
424
+ * cannot be driven from the wire.
425
+ *
426
+ * The route body is bound to the same `A2AServiceImpl` facade the `/a2a`
427
+ * command surface uses, so GUI actions and commands cannot disagree.
428
+ * @module dsh-a2a/api
678
429
  */
679
- /** Build the session executor over a context session pool. */
680
- function createSessionExecutor(pool) {
681
- return {
682
- name: "session",
683
- async execute(input) {
684
- if (!input.prompt) return { parts: [{ text: "No prompt provided." }] };
685
- const reply = await pool.runTurn(input.contextId, input.prompt, input.signal);
686
- return { parts: [{ text: (input.signal.aborted && !reply ? "(canceled)" : reply) || "(no reply)" }] };
687
- },
688
- disposeAll: () => pool.disposeAll()
689
- };
430
+ function readBody$1(req) {
431
+ return new Promise((resolve, reject) => {
432
+ const chunks = [];
433
+ req.on("data", (chunk) => chunks.push(chunk));
434
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
435
+ req.once("error", reject);
436
+ });
690
437
  }
691
- //#endregion
692
- //#region lib/types/server/exec/subagent.js
438
+ /** The caller is the local machine (never a remote peer). */
439
+ function isLoopback(req) {
440
+ const address = req.socket?.remoteAddress;
441
+ return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
442
+ }
443
+ function json(res, status, value) {
444
+ res.writeHead(status, {
445
+ "content-type": "application/json",
446
+ "cache-control": "no-cache"
447
+ });
448
+ res.end(JSON.stringify(value));
449
+ }
450
+ /** Handle one dashboard API request against the service facade. */
451
+ async function handleApiRequest(req, res, impl) {
452
+ if (!isLoopback(req)) {
453
+ res.writeHead(403, { "content-type": "text/plain" });
454
+ res.end("forbidden");
455
+ return;
456
+ }
457
+ const path = (req.url ?? "").split("?")[0] ?? "";
458
+ if (req.method === "GET" && path === "/a2a/api/presets") {
459
+ json(res, 200, await impl.presets());
460
+ return;
461
+ }
462
+ if (req.method === "GET") {
463
+ json(res, 200, snapshotOf(impl));
464
+ return;
465
+ }
466
+ if (req.method === "POST") {
467
+ let payload;
468
+ try {
469
+ payload = JSON.parse(await readBody$1(req));
470
+ } catch {
471
+ json(res, 400, {
472
+ ok: false,
473
+ message: "invalid JSON body"
474
+ });
475
+ return;
476
+ }
477
+ const result = await dispatch(payload, impl);
478
+ json(res, result.ok ? 200 : 409, result);
479
+ return;
480
+ }
481
+ res.writeHead(405, { "content-type": "text/plain" });
482
+ res.end("Method Not Allowed");
483
+ }
484
+ function snapshotOf(impl) {
485
+ const status = impl.status();
486
+ return {
487
+ inbounds: status.inbounds ?? [],
488
+ outbounds: status.outbounds ?? [],
489
+ tasks: impl.listTasks(),
490
+ peers: status.peers ?? impl.inbounds()
491
+ };
492
+ }
493
+ async function dispatch(payload, impl) {
494
+ switch (payload.action) {
495
+ case "inbound.create": return impl.createInboundServer({
496
+ name: payload.name,
497
+ description: payload.description,
498
+ version: payload.version,
499
+ ...payload.endpointPath !== void 0 ? { endpointPath: payload.endpointPath } : {},
500
+ ...payload.preset !== void 0 ? { preset: payload.preset } : {},
501
+ ...payload.authTokenEnv !== void 0 ? { authTokenEnv: payload.authTokenEnv } : {},
502
+ ...payload.skills !== void 0 ? { skills: payload.skills } : {},
503
+ ...payload.enabled !== void 0 ? { enabled: payload.enabled } : {}
504
+ });
505
+ case "inbound.remove": return impl.removeInboundServer(payload.id);
506
+ case "inbound.enable": return impl.setInboundServerEnabled(payload.id, true);
507
+ case "inbound.disable": return impl.setInboundServerEnabled(payload.id, false);
508
+ case "inbound.update": return impl.updateInboundServer(payload.id, {
509
+ ...payload.name !== void 0 ? { name: payload.name } : {},
510
+ ...payload.description !== void 0 ? { description: payload.description } : {},
511
+ ...payload.version !== void 0 ? { version: payload.version } : {},
512
+ ...payload.endpointPath !== void 0 ? { endpointPath: payload.endpointPath } : {},
513
+ ...payload.preset !== void 0 ? { preset: payload.preset } : {},
514
+ ...payload.authTokenEnv !== void 0 ? { authTokenEnv: payload.authTokenEnv } : {},
515
+ ...payload.skills !== void 0 ? { skills: payload.skills } : {}
516
+ });
517
+ case "outbound.create": return impl.createOutboundServer({
518
+ name: payload.name,
519
+ agentCardUrl: payload.agentCardUrl,
520
+ ...payload.bearerTokenEnv !== void 0 ? { bearerTokenEnv: payload.bearerTokenEnv } : {},
521
+ ...payload.preset !== void 0 ? { preset: payload.preset } : {},
522
+ ...payload.enabled !== void 0 ? { enabled: payload.enabled } : {},
523
+ ...payload.timeoutMs !== void 0 ? { timeoutMs: payload.timeoutMs } : {}
524
+ });
525
+ case "outbound.remove": return impl.removeOutboundServer(payload.id);
526
+ case "outbound.enable": return impl.setOutboundServerEnabled(payload.id, true);
527
+ case "outbound.disable": return impl.setOutboundServerEnabled(payload.id, false);
528
+ case "outbound.refresh": return impl.refreshOutboundServer(payload.id);
529
+ case "task.cancel": return impl.cancelTask(payload.id);
530
+ case "inbound.close": return impl.closeInbound(payload.id);
531
+ default: return {
532
+ ok: false,
533
+ message: `unknown action ${String(payload.action)}`
534
+ };
535
+ }
536
+ }
537
+ //#endregion
538
+ //#region lib/types/service.js
693
539
  /**
694
- * The `subagent` executor: one inbound task is delegated to a named one-shot
695
- * subagent under the context's parent session. The child runs with the
696
- * composition's own tools and the delegation boundary; its output becomes the
697
- * task's artifact. The subagent seam must be mounted, else the skill binding
698
- * fails loudly at resolution time.
699
- * @module dsh-a2a/server/exec/subagent
540
+ * The `ctx.a2a` service facade: the thin read/control surface commands, the
541
+ * GUI API, and other plugins use. Registered as a Cordis Service so the key
542
+ * disappears with the plugin fiber.
543
+ * @module dsh-a2a/service
700
544
  */
701
- /** Build a subagent executor delegating inbound tasks to one-shot children. */
702
- function createSubagentExecutor(opts) {
703
- const { pool, subagents, provider } = opts;
545
+ /** The service other plugins read as `ctx.a2a`. */
546
+ var A2AService = class extends Service {
547
+ impl;
548
+ constructor(ctx, impl) {
549
+ super(ctx, "a2a");
550
+ this.impl = impl;
551
+ }
552
+ status() {
553
+ return this.impl.status();
554
+ }
555
+ async presets() {
556
+ return this.impl.presets();
557
+ }
558
+ listInboundServers() {
559
+ return this.impl.listInboundServers();
560
+ }
561
+ async createInboundServer(input) {
562
+ return this.impl.createInboundServer(input);
563
+ }
564
+ async removeInboundServer(id) {
565
+ return this.impl.removeInboundServer(id);
566
+ }
567
+ async setInboundServerEnabled(id, enabled) {
568
+ return this.impl.setInboundServerEnabled(id, enabled);
569
+ }
570
+ async updateInboundServer(id, patch) {
571
+ return this.impl.updateInboundServer(id, patch);
572
+ }
573
+ listOutboundServers() {
574
+ return this.impl.listOutboundServers();
575
+ }
576
+ async createOutboundServer(input) {
577
+ return this.impl.createOutboundServer(input);
578
+ }
579
+ async removeOutboundServer(id) {
580
+ return this.impl.removeOutboundServer(id);
581
+ }
582
+ async setOutboundServerEnabled(id, enabled) {
583
+ return this.impl.setOutboundServerEnabled(id, enabled);
584
+ }
585
+ async refreshOutboundServer(id) {
586
+ return this.impl.refreshOutboundServer(id);
587
+ }
588
+ getTask(taskId) {
589
+ return this.impl.getTask(taskId);
590
+ }
591
+ listTasks() {
592
+ return this.impl.listTasks();
593
+ }
594
+ async cancelTask(taskId) {
595
+ return this.impl.cancelTask(taskId);
596
+ }
597
+ inbounds() {
598
+ return this.impl.inbounds();
599
+ }
600
+ async closeInbound(peerId) {
601
+ return this.impl.closeInbound(peerId);
602
+ }
603
+ };
604
+ //#endregion
605
+ //#region lib/types/commands.js
606
+ /**
607
+ * The `/a2a` command surface: status, inbound/outbound server CRUD +
608
+ * enable/disable, and task control — a text backup to the GUI (the settings
609
+ * panel is the primary surface). All operations delegate to the same
610
+ * `A2AServiceImpl` facade as the dashboard API.
611
+ * @module dsh-a2a/commands
612
+ */
613
+ /** Build the `/a2a` command definition over the service facade. */
614
+ function buildA2aCommand(impl) {
704
615
  return {
705
- name: "subagent",
706
- async execute(input, execCtx) {
707
- if (!input.prompt) return { parts: [{ text: "No prompt provided." }] };
708
- const { agent } = await pool.agentFor(input.contextId);
709
- const text = (await (await subagents.start(provider, {
710
- label: `a2a:${input.skill}`,
711
- prompt: [{
712
- type: "text",
713
- text: input.prompt
714
- }],
715
- parent: agent,
716
- signal: input.signal
717
- })).result).output.filter((b) => typeof b.text === "string").map((b) => b.text).filter(Boolean).join("\n").trim();
718
- execCtx.onEvent({
719
- type: "artifact",
720
- artifactId: "result",
721
- parts: [{ text: text || "(subagent returned no output)" }],
722
- name: input.skill,
723
- lastChunk: true
724
- });
725
- return { parts: [{ text: text || "(subagent returned no output)" }] };
616
+ name: "a2a",
617
+ description: "Manage the A2A plugin: status, inbound/outbound servers, tasks.",
618
+ handler: async (invocation) => {
619
+ const [verb, ...rest] = invocation.rawInput.trim().split(/\s+/);
620
+ switch (verb ?? "") {
621
+ case "":
622
+ case "status": return success(JSON.stringify(impl.status(), null, 2));
623
+ case "presets": return success(JSON.stringify(await impl.presets(), null, 2));
624
+ case "inbound": {
625
+ const [action, id, ...args] = rest;
626
+ switch (action ?? "") {
627
+ case "list": return success(JSON.stringify(impl.listInboundServers(), null, 2));
628
+ case "create": {
629
+ const name = args[0];
630
+ if (!name) return error("usage: /a2a inbound create <name> [preset] [description]");
631
+ const preset = args[1];
632
+ return outcome(await impl.createInboundServer({
633
+ name,
634
+ description: args[2] ?? "An A2A inbound server",
635
+ version: "1.0.0",
636
+ ...preset && preset !== "default" ? { preset } : {}
637
+ }));
638
+ }
639
+ case "remove": return id ? outcome(await impl.removeInboundServer(id)) : error("usage: /a2a inbound remove <id>");
640
+ case "enable": return id ? outcome(await impl.setInboundServerEnabled(id, true)) : error("usage: /a2a inbound enable <id>");
641
+ case "disable": return id ? outcome(await impl.setInboundServerEnabled(id, false)) : error("usage: /a2a inbound disable <id>");
642
+ default: return error("usage: /a2a inbound list|create|remove|enable|disable ...");
643
+ }
644
+ }
645
+ case "outbound": {
646
+ const [action, id, ...args] = rest;
647
+ switch (action ?? "") {
648
+ case "list": return success(JSON.stringify(impl.listOutboundServers(), null, 2));
649
+ case "create": {
650
+ const url = args[0];
651
+ if (!url) return error("usage: /a2a outbound create <agentCardUrl> [name] [preset]");
652
+ const name = args[1] ?? url;
653
+ const preset = args[2];
654
+ return outcome(await impl.createOutboundServer({
655
+ name,
656
+ agentCardUrl: url,
657
+ ...preset && preset !== "default" ? { preset } : {}
658
+ }));
659
+ }
660
+ case "remove": return id ? outcome(await impl.removeOutboundServer(id)) : error("usage: /a2a outbound remove <id>");
661
+ case "enable": return id ? outcome(await impl.setOutboundServerEnabled(id, true)) : error("usage: /a2a outbound enable <id>");
662
+ case "disable": return id ? outcome(await impl.setOutboundServerEnabled(id, false)) : error("usage: /a2a outbound disable <id>");
663
+ case "refresh": return id ? outcome(await impl.refreshOutboundServer(id)) : error("usage: /a2a outbound refresh <id>");
664
+ default: return error("usage: /a2a outbound list|create|remove|enable|disable|refresh ...");
665
+ }
666
+ }
667
+ case "tasks": return success(JSON.stringify(impl.listTasks(), null, 2));
668
+ case "task": {
669
+ const [action, id] = rest;
670
+ if (action === "get" && id) return success(JSON.stringify(impl.getTask(id), null, 2));
671
+ if (action === "cancel" && id) return outcome(await impl.cancelTask(id));
672
+ return error("usage: /a2a task get|cancel <taskId>");
673
+ }
674
+ case "peers": return success(JSON.stringify(impl.inbounds(), null, 2));
675
+ case "help": return success("a2a: status | presets | inbound list/create/remove/enable/disable | outbound list/create/remove/enable/disable/refresh | tasks | task get/cancel <id> | peers | help");
676
+ default: return error(`unknown a2a verb "${verb}" (try /a2a help)`);
677
+ }
678
+ }
679
+ };
680
+ }
681
+ function success(text) {
682
+ return {
683
+ kind: "success",
684
+ text
685
+ };
686
+ }
687
+ function error(text) {
688
+ return {
689
+ kind: "error",
690
+ text
691
+ };
692
+ }
693
+ function outcome(result) {
694
+ return result.ok ? success(result.message) : error(result.message);
695
+ }
696
+ //#endregion
697
+ //#region lib/types/server/card.js
698
+ /**
699
+ * AgentCard assembly for the inbound half: identity and skill DECLARATION at
700
+ * the instance level. The skill list is exactly the creator-declared
701
+ * `AgentSkill[]` stored on the inbound server instance — no derivation from
702
+ * the live tool registry (the v0.2 white-list mechanism is removed). The
703
+ * advertised card is a pure function of the instance record plus its base URL
704
+ * and endpoint path.
705
+ * @module dsh-a2a/server/card
706
+ */
707
+ /** Assemble the AgentCard advertised by this DSH. */
708
+ function buildCard(options) {
709
+ const iface = {
710
+ url: `${options.baseUrl.replace(/\/$/, "")}${options.endpointPath}`,
711
+ protocolBinding: "JSONRPC",
712
+ protocolVersion: "1.0"
713
+ };
714
+ return {
715
+ name: options.name,
716
+ description: options.description,
717
+ version: options.version,
718
+ provider: {
719
+ url: "https://deepseek.com",
720
+ organization: "DeepSeek"
726
721
  },
727
- disposeAll: () => pool.disposeAll()
722
+ capabilities: {
723
+ streaming: true,
724
+ pushNotifications: false,
725
+ extendedAgentCard: true,
726
+ stateTransitionHistory: false
727
+ },
728
+ defaultInputModes: ["text/plain"],
729
+ defaultOutputModes: ["text/plain"],
730
+ skills: options.skills,
731
+ supportedInterfaces: [iface],
732
+ ...options.authToken !== void 0 ? {
733
+ securitySchemes: { bearer: { httpAuthSecurityScheme: {
734
+ scheme: "bearer",
735
+ description: "Shared bearer token"
736
+ } } },
737
+ securityRequirements: [{ schemes: { bearer: { list: ["bearer"] } } }]
738
+ } : {}
728
739
  };
729
740
  }
730
741
  //#endregion
@@ -795,6 +806,10 @@ var A2AServer = class {
795
806
  setCard(next) {
796
807
  this.card = next;
797
808
  }
809
+ /** The AgentCard route path for this instance. */
810
+ cardPath() {
811
+ return this.opts.cardPath ?? "/.well-known/agent-card.json";
812
+ }
798
813
  /**
799
814
  * Abort a running task by control path (facade /a2a task cancel): abort the
800
815
  * executor's signal, settle the task CANCELED, and wake stream waiters.
@@ -822,7 +837,7 @@ var A2AServer = class {
822
837
  /** Route one inbound HTTP request (GET card, POST JSON-RPC). */
823
838
  async handle(req, body) {
824
839
  const path = (req.url ?? "").split("?")[0] ?? "";
825
- if (req.method === "GET" && path === "/.well-known/agent-card.json") return {
840
+ if (req.method === "GET" && path === this.cardPath()) return {
826
841
  status: 200,
827
842
  contentType: "application/json",
828
843
  body: JSON.stringify(this.card)
@@ -835,7 +850,7 @@ var A2AServer = class {
835
850
  if (!this.authorized(req)) return {
836
851
  status: 401,
837
852
  contentType: "application/json",
838
- body: JSON.stringify(rpcError(null, A2A_ERROR_CODES.UNAUTHORIZED, "Unauthorized")),
853
+ body: JSON.stringify(rpcError(null, -32040, "Unauthorized")),
839
854
  headers: { "WWW-Authenticate": "Bearer" }
840
855
  };
841
856
  const rpc = parseRpc(body);
@@ -1186,16 +1201,18 @@ function extractTaskIds(result) {
1186
1201
  * `@deepseek-ai/dsh-host-webserver`), with runtime enable/disable.
1187
1202
  * @module dsh-a2a/server/routes
1188
1203
  */
1189
- const CARD_PATH = "/.well-known/agent-card.json";
1204
+ const DEFAULT_CARD_PATH = "/.well-known/agent-card.json";
1190
1205
  /** Runtime-registered HTTP routes for one A2A server. */
1191
1206
  var A2aRoutes = class {
1192
1207
  webServer;
1193
1208
  server;
1209
+ cardPath;
1194
1210
  disposers = [];
1195
1211
  registered = false;
1196
- constructor(webServer, server) {
1212
+ constructor(webServer, server, cardPath = DEFAULT_CARD_PATH) {
1197
1213
  this.webServer = webServer;
1198
1214
  this.server = server;
1215
+ this.cardPath = cardPath;
1199
1216
  }
1200
1217
  get active() {
1201
1218
  return this.registered;
@@ -1205,7 +1222,7 @@ var A2aRoutes = class {
1205
1222
  if (this.registered) return;
1206
1223
  this.disposers.push(this.webServer.register({
1207
1224
  kind: "exact",
1208
- path: CARD_PATH,
1225
+ path: this.cardPath,
1209
1226
  handler: (_req, res) => {
1210
1227
  res.writeHead(200, { "content-type": "application/json" });
1211
1228
  res.end(JSON.stringify(this.server.card));
@@ -1230,7 +1247,7 @@ var A2aRoutes = class {
1230
1247
  const path = (req.url ?? "").split("?")[0] ?? "";
1231
1248
  const streaming = req.method === "POST" && (req.headers.accept ?? "").includes("text/event-stream");
1232
1249
  if (req.method === "POST" && path === endpointPathOf(this.server)) {
1233
- const body = await readBody$1(req);
1250
+ const body = await readBody(req);
1234
1251
  if (streaming) {
1235
1252
  if (!this.server.authorized(toServerReq(req))) {
1236
1253
  res.writeHead(401, {
@@ -1266,7 +1283,7 @@ var A2aRoutes = class {
1266
1283
  res.end(out.body);
1267
1284
  return;
1268
1285
  }
1269
- const out = await this.server.handle(toServerReq(req), path === CARD_PATH ? "" : "");
1286
+ const out = await this.server.handle(toServerReq(req), path === this.cardPath ? "" : "");
1270
1287
  res.writeHead(out.status, {
1271
1288
  "content-type": out.contentType,
1272
1289
  ...out.headers ?? {}
@@ -1283,7 +1300,7 @@ function endpointPathOf(server) {
1283
1300
  return "/a2a";
1284
1301
  }
1285
1302
  }
1286
- function readBody$1(req) {
1303
+ function readBody(req) {
1287
1304
  return new Promise((resolve, reject) => {
1288
1305
  const chunks = [];
1289
1306
  req.on("data", (chunk) => chunks.push(chunk));
@@ -1309,102 +1326,494 @@ function toServerReq(req) {
1309
1326
  };
1310
1327
  }
1311
1328
  //#endregion
1312
- //#region lib/types/api.js
1329
+ //#region lib/types/server/exec/session.js
1313
1330
  /**
1314
- * GUI control API for the A2A settings dashboard: one `GET /a2a/api`
1315
- * snapshot plus `POST /a2a/api` control actions (server toggle, outbound
1316
- * agent management, task view). The browser half talks only to this route;
1317
- * it carries no protocol knowledge. Loopback-only by default so the
1318
- * dashboard cannot be driven from the wire.
1319
- *
1320
- * The route body is bound to the same `A2AServiceImpl` facade the `/a2a`
1321
- * command surface uses, so GUI actions and commands cannot disagree.
1322
- * @module dsh-a2a/api
1331
+ * The `session` executor: one inbound task is one turn on the context's DSH
1332
+ * agent session; reply text becomes the task's output part. Works on any
1333
+ * composition with an agent loop (the `chat` skill and the default path).
1334
+ * @module dsh-a2a/server/exec/session
1323
1335
  */
1324
- function readBody(req) {
1325
- return new Promise((resolve, reject) => {
1326
- const chunks = [];
1327
- req.on("data", (chunk) => chunks.push(chunk));
1328
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1329
- req.once("error", reject);
1330
- });
1336
+ /** Build the session executor over a context session pool. */
1337
+ function createSessionExecutor(pool) {
1338
+ return {
1339
+ name: "session",
1340
+ async execute(input) {
1341
+ if (!input.prompt) return { parts: [{ text: "No prompt provided." }] };
1342
+ const reply = await pool.runTurn(input.contextId, input.prompt, input.signal);
1343
+ return { parts: [{ text: (input.signal.aborted && !reply ? "(canceled)" : reply) || "(no reply)" }] };
1344
+ },
1345
+ disposeAll: () => pool.disposeAll()
1346
+ };
1331
1347
  }
1332
- /** The caller is the local machine (never a remote peer). */
1333
- function isLoopback(req) {
1334
- const address = req.socket?.remoteAddress;
1335
- return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
1348
+ //#endregion
1349
+ //#region lib/types/server/exec/subagent.js
1350
+ /**
1351
+ * The `subagent` executor: one inbound task is delegated to a named one-shot
1352
+ * subagent under the context's parent session. The child runs with the
1353
+ * composition's own tools and the delegation boundary; its output becomes the
1354
+ * task's artifact. The subagent seam must be mounted, else the skill binding
1355
+ * fails loudly at resolution time.
1356
+ * @module dsh-a2a/server/exec/subagent
1357
+ */
1358
+ /** Build a subagent executor delegating inbound tasks to one-shot children. */
1359
+ function createSubagentExecutor(opts) {
1360
+ const { pool, subagents, provider } = opts;
1361
+ return {
1362
+ name: "subagent",
1363
+ async execute(input, execCtx) {
1364
+ if (!input.prompt) return { parts: [{ text: "No prompt provided." }] };
1365
+ const { agent } = await pool.agentFor(input.contextId);
1366
+ const text = (await (await subagents.start(provider, {
1367
+ label: `a2a:${input.skill}`,
1368
+ prompt: [{
1369
+ type: "text",
1370
+ text: input.prompt
1371
+ }],
1372
+ parent: agent,
1373
+ signal: input.signal
1374
+ })).result).output.filter((b) => typeof b.text === "string").map((b) => b.text).filter(Boolean).join("\n").trim();
1375
+ execCtx.onEvent({
1376
+ type: "artifact",
1377
+ artifactId: "result",
1378
+ parts: [{ text: text || "(subagent returned no output)" }],
1379
+ name: input.skill,
1380
+ lastChunk: true
1381
+ });
1382
+ return { parts: [{ text: text || "(subagent returned no output)" }] };
1383
+ },
1384
+ disposeAll: () => pool.disposeAll()
1385
+ };
1336
1386
  }
1337
- function json(res, status, value) {
1338
- res.writeHead(status, {
1339
- "content-type": "application/json",
1340
- "cache-control": "no-cache"
1387
+ //#endregion
1388
+ //#region lib/types/server/executor.js
1389
+ /**
1390
+ * Executor abstraction for inbound tasks: an executor turns one task into
1391
+ * output parts while reporting status and artifact updates. The registry is
1392
+ * INTERNAL (built by the plugin from config) — public registration was cut:
1393
+ * there is no external caller yet, so the seam stays an interface with two
1394
+ * built-in implementations (session, subagent).
1395
+ * @module dsh-a2a/server/executor
1396
+ */
1397
+ /**
1398
+ * Skill → executor resolution. The subagent executor is absent on
1399
+ * compositions without the subagent seam; resolving a skill bound to a
1400
+ * missing kind fails loudly with a readable message.
1401
+ */
1402
+ var ExecutorSet = class {
1403
+ bySkill;
1404
+ session;
1405
+ subagent;
1406
+ constructor(bySkill, session, subagent) {
1407
+ this.bySkill = bySkill;
1408
+ this.session = session;
1409
+ this.subagent = subagent;
1410
+ }
1411
+ resolve(skill) {
1412
+ if ((this.bySkill[skill] ?? "session") === "session") return this.session;
1413
+ const subagent = this.subagent;
1414
+ if (subagent === void 0) throw new Error(`a2a: skill "${skill}" is bound to the subagent executor but no subagent seam is mounted`);
1415
+ return subagent;
1416
+ }
1417
+ async disposeAll() {
1418
+ await this.session.disposeAll?.();
1419
+ await this.subagent?.disposeAll?.();
1420
+ }
1421
+ };
1422
+ //#endregion
1423
+ //#region lib/types/server/inbound-registry.js
1424
+ /**
1425
+ * Inbound connection registry: which remote peers are talking to this DSH's
1426
+ * A2A server. Fed by the A2A server's `onInbound` hook (every JSON-RPC
1427
+ * request / SSE open), surfaced through the dashboard API, and disconnectable
1428
+ * (`closePeer` cancels the peer's active tasks and drops the record).
1429
+ * @module dsh-a2a/server/inbound-registry
1430
+ */
1431
+ function newPeerId() {
1432
+ return `peer-${crypto.randomUUID()}`;
1433
+ }
1434
+ /** In-memory inbound peer registry. */
1435
+ var LiveInboundRegistry = class {
1436
+ peers = /* @__PURE__ */ new Map();
1437
+ findBySource(source) {
1438
+ if (source === null) return void 0;
1439
+ for (const peer of this.peers.values()) if (peer.source === source) return peer;
1440
+ }
1441
+ /** Observe one inbound event: method, source, task ids, streaming flag. */
1442
+ note(input) {
1443
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1444
+ const source = input.source ?? null;
1445
+ let peer = this.findBySource(source);
1446
+ if (peer === void 0) {
1447
+ peer = {
1448
+ id: newPeerId(),
1449
+ label: source ?? "unknown",
1450
+ source,
1451
+ firstSeen: now,
1452
+ lastSeen: now,
1453
+ taskCount: 0,
1454
+ active: /* @__PURE__ */ new Set(),
1455
+ streamingCount: 0
1456
+ };
1457
+ this.peers.set(peer.id, peer);
1458
+ }
1459
+ peer.lastSeen = now;
1460
+ peer.taskCount += input.taskIds.length;
1461
+ for (const id of input.taskIds) peer.active.add(id);
1462
+ if (input.streaming) peer.streamingCount += 1;
1463
+ }
1464
+ /** A task settled: drop it from every peer's active set. */
1465
+ settle(taskId) {
1466
+ for (const peer of this.peers.values()) if (peer.active.delete(taskId)) peer.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
1467
+ }
1468
+ /** Decrement streaming count when an SSE connection closes. */
1469
+ endStream(source) {
1470
+ const peer = this.findBySource(source ?? null);
1471
+ if (peer !== void 0 && peer.streamingCount > 0) {
1472
+ peer.streamingCount -= 1;
1473
+ peer.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
1474
+ }
1475
+ }
1476
+ list() {
1477
+ return [...this.peers.values()].map((p) => ({
1478
+ id: p.id,
1479
+ label: p.label,
1480
+ source: p.source,
1481
+ firstSeen: p.firstSeen,
1482
+ lastSeen: p.lastSeen,
1483
+ taskCount: p.taskCount,
1484
+ activeTaskIds: [...p.active],
1485
+ streaming: p.streamingCount > 0
1486
+ })).sort((a, b) => a.lastSeen < b.lastSeen ? 1 : -1);
1487
+ }
1488
+ closePeer(peerId) {
1489
+ const peer = this.peers.get(peerId);
1490
+ if (peer === void 0) return {
1491
+ ok: false,
1492
+ message: `inbound peer ${peerId} not found`
1493
+ };
1494
+ this.peers.delete(peerId);
1495
+ return {
1496
+ ok: true,
1497
+ message: `inbound peer ${peer.label} closed`
1498
+ };
1499
+ }
1500
+ activeTasksOf(peerId) {
1501
+ return [...this.peers.get(peerId)?.active ?? []];
1502
+ }
1503
+ };
1504
+ //#endregion
1505
+ //#region lib/types/servers/inbound-manager.js
1506
+ /**
1507
+ * Inbound server manager: owns every inbound A2A server instance. Each
1508
+ * instance binds one agent preset (its session pool is composed from that
1509
+ * preset), advertises creator-declared skills, and serves its own endpoint and
1510
+ * AgentCard route. Instances are persisted in the `inbound_servers` domain
1511
+ * table and assembled on boot; the manager owns route registration and the
1512
+ * full lifecycle (add/enable/disable/update/remove) driven from the GUI and
1513
+ * facade, and disposes every instance's environment on teardown.
1514
+ *
1515
+ * Skill declaration: the stored `skills` list is exactly what the creator
1516
+ * declared. When a creator leaves it empty at creation time, the default skill
1517
+ * derives from the bound preset's display name (else the built-in `chat`
1518
+ * skill), per the v1.0 design — the v0.2 tool white-list derivation is gone.
1519
+ * @module dsh-a2a/servers/inbound-manager
1520
+ */
1521
+ function refuseExecutor(reason) {
1522
+ return {
1523
+ name: "refuse",
1524
+ async execute() {
1525
+ throw new Error(`a2a: ${reason}; refusing inbound task`);
1526
+ }
1527
+ };
1528
+ }
1529
+ /** Default skkill when a creator declares none: the preset display name. */
1530
+ function defaultSkillFor(skills, presetName) {
1531
+ if (skills !== void 0 && skills.length > 0) return skills;
1532
+ if (presetName !== void 0 && presetName.length > 0) return [{
1533
+ id: "chat",
1534
+ name: presetName,
1535
+ description: `Compose inbound sessions from the "${presetName}" agent preset.`,
1536
+ tags: ["preset"]
1537
+ }];
1538
+ return [{
1539
+ id: "chat",
1540
+ name: "chat",
1541
+ description: "Conversational assistance over a DSH agent session.",
1542
+ tags: ["chat"]
1543
+ }];
1544
+ }
1545
+ /** Assemble one live instance from a persisted record. */
1546
+ function assemble(record, host) {
1547
+ const skills = record.skills;
1548
+ const endpointPath = record.endpointPath;
1549
+ const cardPath = `${endpointPath.replace(/\/$/, "")}/agent-card.json`;
1550
+ const card = buildCard({
1551
+ baseUrl: host.defaultBaseUrl,
1552
+ endpointPath,
1553
+ name: record.name,
1554
+ description: record.description,
1555
+ version: record.version,
1556
+ skills,
1557
+ ...record.authTokenEnv !== void 0 && process.env[record.authTokenEnv] !== void 0 ? { authToken: process.env[record.authTokenEnv] } : {}
1341
1558
  });
1342
- res.end(JSON.stringify(value));
1559
+ const sessionPool = host.agents ? new ContextSessionPool(host.agents, {
1560
+ cwd: host.sessionCwd,
1561
+ ...host.agentPresets !== void 0 ? { agentPresets: host.agentPresets } : {},
1562
+ ...record.preset !== void 0 ? { presetId: () => record.preset } : {},
1563
+ resolveAgentOptions: () => host.resolveDefaultModel?.()
1564
+ }) : void 0;
1565
+ const executors = new ExecutorSet({ chat: "session" }, sessionPool ? createSessionExecutor(sessionPool) : refuseExecutor("no agent loop mounted"), sessionPool !== void 0 && host.subagents !== void 0 ? createSubagentExecutor({
1566
+ pool: sessionPool,
1567
+ subagents: host.subagents,
1568
+ provider: host.subagentProvider
1569
+ }) : void 0);
1570
+ const inbound = new LiveInboundRegistry();
1571
+ const skillIds = new Set(skills.map((s) => s.id));
1572
+ const gate = async (input) => {
1573
+ if (!skillIds.has(input.skill)) return {
1574
+ ok: false,
1575
+ reason: `unknown skill "${input.skill}"`
1576
+ };
1577
+ const decision = {
1578
+ contextId: input.contextId,
1579
+ skill: input.skill,
1580
+ parts: input.parts,
1581
+ remotePeerId: input.remotePeerId
1582
+ };
1583
+ const decided = await host.ctx.waterfall("a2a/inbound-task", decision, async (d) => d);
1584
+ host.logger.info(`[a2a:${record.id}] inbound skill=${decided.skill} rejected=${decided.rejected?.reason ?? "no"}`);
1585
+ if (decided.rejected !== void 0) return {
1586
+ ok: false,
1587
+ reason: decided.rejected.reason
1588
+ };
1589
+ return { ok: true };
1590
+ };
1591
+ const server = new A2AServer({
1592
+ card,
1593
+ store: host.tasks,
1594
+ executors,
1595
+ ...card.securitySchemes !== void 0 ? { authToken: "configured" } : {},
1596
+ cardPath,
1597
+ gate,
1598
+ onInbound: (facts) => inbound.note({
1599
+ method: facts.method,
1600
+ ...facts.source !== void 0 ? { source: facts.source } : {},
1601
+ taskIds: facts.taskIds,
1602
+ streaming: facts.streaming
1603
+ }),
1604
+ onTaskSettled: (taskId) => {
1605
+ host.logger.info(`[a2a:${record.id}] task settled ${taskId}`);
1606
+ inbound.settle(taskId);
1607
+ }
1608
+ });
1609
+ const routes = new A2aRoutes(host.webServer, server, cardPath);
1610
+ return {
1611
+ id: record.id,
1612
+ record,
1613
+ card,
1614
+ server,
1615
+ routes,
1616
+ inbound,
1617
+ executors,
1618
+ endpointPath,
1619
+ cardPath,
1620
+ dispose: () => {
1621
+ routes.dispose();
1622
+ executors.disposeAll().catch(() => {});
1623
+ }
1624
+ };
1343
1625
  }
1344
- /** Handle one dashboard API request against the service facade. */
1345
- async function handleApiRequest(req, res, impl) {
1346
- if (!isLoopback(req)) {
1347
- res.writeHead(403, { "content-type": "text/plain" });
1348
- res.end("forbidden");
1349
- return;
1626
+ /** Persisted-instance store over the inbound_servers table. */
1627
+ var DomainInboundStore = class DomainInboundStore {
1628
+ table;
1629
+ static KEY(id) {
1630
+ return `in:${id}`;
1350
1631
  }
1351
- if (req.method === "GET") {
1352
- json(res, 200, snapshotOf(impl));
1632
+ constructor(table) {
1633
+ this.table = table;
1634
+ }
1635
+ list() {
1636
+ const out = [];
1637
+ const entries = this.table.entries;
1638
+ if (entries === void 0) return out;
1639
+ for (const [key, raw] of entries.call(this.table)) {
1640
+ if (!key.startsWith("in:")) continue;
1641
+ const parsed = safeParse$1(raw);
1642
+ if (parsed !== void 0) out.push(parsed);
1643
+ }
1644
+ return out;
1645
+ }
1646
+ get(id) {
1647
+ const raw = this.table.get(DomainInboundStore.KEY(id));
1648
+ return raw === void 0 ? void 0 : safeParse$1(raw);
1649
+ }
1650
+ async save(record) {
1651
+ await this.table.put(DomainInboundStore.KEY(record.id), JSON.stringify(record));
1652
+ }
1653
+ async remove(id) {
1654
+ await this.table.put(DomainInboundStore.KEY(id), "");
1655
+ }
1656
+ };
1657
+ function safeParse$1(raw) {
1658
+ try {
1659
+ const parsed = JSON.parse(raw);
1660
+ if (typeof parsed.id !== "string" || typeof parsed.name !== "string" || !Array.isArray(parsed.skills)) return void 0;
1661
+ return parsed;
1662
+ } catch {
1353
1663
  return;
1354
1664
  }
1355
- if (req.method === "POST") {
1356
- let payload;
1665
+ }
1666
+ /** Manages the live set of inbound server instances. */
1667
+ var InboundServerManager = class {
1668
+ host;
1669
+ store;
1670
+ live = /* @__PURE__ */ new Map();
1671
+ constructor(host, store) {
1672
+ this.host = host;
1673
+ this.store = store;
1674
+ }
1675
+ /** Assemble every persisted enabled instance (boot). */
1676
+ boot() {
1677
+ for (const record of this.store.list()) try {
1678
+ const live = assemble(record, this.host);
1679
+ this.live.set(record.id, live);
1680
+ if (record.enabled) live.routes.enable();
1681
+ } catch (err) {
1682
+ this.host.logger.error(`[a2a:${record.id}] failed to assemble: ${String(err.message)}`);
1683
+ }
1684
+ }
1685
+ list() {
1686
+ return [...this.live.values()];
1687
+ }
1688
+ get(id) {
1689
+ return this.live.get(id);
1690
+ }
1691
+ /** Resolve the display name of a preset id (undefined when unnamed/absent). */
1692
+ async presetDisplayName(preset) {
1693
+ if (preset === void 0 || this.host.agentPresets === void 0) return void 0;
1357
1694
  try {
1358
- payload = JSON.parse(await readBody(req));
1695
+ const resolved = await this.host.agentPresets.resolve(preset);
1696
+ return resolved.name ?? resolved.id;
1359
1697
  } catch {
1360
- json(res, 400, {
1361
- ok: false,
1362
- message: "invalid JSON body"
1363
- });
1364
1698
  return;
1365
1699
  }
1366
- const result = await dispatch(payload, impl);
1367
- json(res, result.ok ? 200 : 409, result);
1368
- return;
1369
1700
  }
1370
- res.writeHead(405, { "content-type": "text/plain" });
1371
- res.end("Method Not Allowed");
1372
- }
1373
- function snapshotOf(impl) {
1374
- const status = impl.status();
1375
- return {
1376
- server: status.server,
1377
- tasks: impl.listTasks(),
1378
- agents: impl.agents(),
1379
- inbounds: status.inbounds ?? []
1380
- };
1381
- }
1382
- async function dispatch(payload, impl) {
1383
- switch (payload.action) {
1384
- case "server.enable": return impl.enableServer(true);
1385
- case "server.disable": return impl.enableServer(false);
1386
- case "agent.add": return impl.addAgent({
1387
- name: payload.name,
1388
- agentCardUrl: payload.agentCardUrl,
1389
- ...payload.bearerTokenEnv ? { bearerTokenEnv: payload.bearerTokenEnv } : {}
1390
- });
1391
- case "agent.remove": return impl.removeAgent(payload.id);
1392
- case "agent.enable": return impl.setAgentEnabled(payload.id, true);
1393
- case "agent.disable": return impl.setAgentEnabled(payload.id, false);
1394
- case "agent.refresh": return impl.refreshAgentCard(payload.id);
1395
- case "task.cancel": return impl.cancelTask(payload.id);
1396
- case "identity.update": return impl.updateIdentity({
1397
- ...payload.name !== void 0 ? { name: payload.name } : {},
1398
- ...payload.description !== void 0 ? { description: payload.description } : {},
1399
- ...payload.version !== void 0 ? { version: payload.version } : {}
1400
- });
1401
- case "inbound.close": return impl.closeInbound(payload.id);
1402
- default: return {
1701
+ /** Create, persist, and assemble a new inbound instance. */
1702
+ async add(input) {
1703
+ const id = this.newId(input.name);
1704
+ if (this.live.has(id)) return {
1705
+ ok: false,
1706
+ message: `inbound server ${id} already exists`
1707
+ };
1708
+ const presetName = await this.presetDisplayName(input.preset);
1709
+ const record = {
1710
+ id,
1711
+ name: input.name,
1712
+ description: input.description,
1713
+ version: input.version,
1714
+ endpointPath: input.endpointPath ?? `/a2a/${id}`,
1715
+ ...input.preset !== void 0 ? { preset: input.preset } : {},
1716
+ ...input.authTokenEnv !== void 0 ? { authTokenEnv: input.authTokenEnv } : {},
1717
+ skills: [...defaultSkillFor(input.skills, presetName)],
1718
+ enabled: input.enabled ?? true
1719
+ };
1720
+ try {
1721
+ const live = assemble(record, this.host);
1722
+ await this.store.save(record);
1723
+ this.live.set(id, live);
1724
+ if (record.enabled) live.routes.enable();
1725
+ return {
1726
+ ok: true,
1727
+ message: `inbound server "${record.name}" created`,
1728
+ id
1729
+ };
1730
+ } catch (err) {
1731
+ return {
1732
+ ok: false,
1733
+ message: `failed to create inbound server: ${String(err.message)}`
1734
+ };
1735
+ }
1736
+ }
1737
+ /** Persist, rebuild, and apply a patch onto a live instance. */
1738
+ async update(id, patch) {
1739
+ const live = this.live.get(id);
1740
+ const stored = this.store.get(id);
1741
+ if (live === void 0 || stored === void 0) return {
1742
+ ok: false,
1743
+ message: `inbound server ${id} not found`
1744
+ };
1745
+ const next = {
1746
+ ...stored,
1747
+ name: patch.name ?? stored.name,
1748
+ description: patch.description ?? stored.description,
1749
+ version: patch.version ?? stored.version,
1750
+ endpointPath: patch.endpointPath ?? stored.endpointPath,
1751
+ ...patch.preset !== void 0 ? { preset: patch.preset } : {},
1752
+ ...patch.authTokenEnv !== void 0 ? { authTokenEnv: patch.authTokenEnv } : {},
1753
+ skills: patch.skills ?? stored.skills,
1754
+ enabled: stored.enabled
1755
+ };
1756
+ try {
1757
+ await this.store.save(next);
1758
+ const replaced = assemble(next, this.host);
1759
+ const wasEnabled = live.routes.active;
1760
+ live.dispose();
1761
+ this.live.set(id, replaced);
1762
+ if (wasEnabled || next.enabled) replaced.routes.enable();
1763
+ return {
1764
+ ok: true,
1765
+ message: `inbound server ${id} updated`
1766
+ };
1767
+ } catch (err) {
1768
+ return {
1769
+ ok: false,
1770
+ message: `failed to update inbound server: ${String(err.message)}`
1771
+ };
1772
+ }
1773
+ }
1774
+ /** Enable/disable an instance's routes. */
1775
+ setEnabled(id, enabled) {
1776
+ const live = this.live.get(id);
1777
+ if (live === void 0) return {
1403
1778
  ok: false,
1404
- message: `unknown action ${String(payload.action)}`
1779
+ message: `inbound server ${id} not found`
1780
+ };
1781
+ if (enabled) live.routes.enable();
1782
+ else live.routes.disable();
1783
+ const stored = this.store.get(id);
1784
+ if (stored !== void 0) this.store.save({
1785
+ ...stored,
1786
+ enabled
1787
+ }).catch(() => {});
1788
+ return {
1789
+ ok: true,
1790
+ message: `inbound server ${id} ${enabled ? "enabled" : "disabled"}`
1405
1791
  };
1406
1792
  }
1407
- }
1793
+ /** Remove, dispose, and unpersist an instance. */
1794
+ async remove(id) {
1795
+ const live = this.live.get(id);
1796
+ if (live === void 0) return {
1797
+ ok: false,
1798
+ message: `inbound server ${id} not found`
1799
+ };
1800
+ live.dispose();
1801
+ this.live.delete(id);
1802
+ await this.store.remove(id);
1803
+ return {
1804
+ ok: true,
1805
+ message: `inbound server ${id} removed`
1806
+ };
1807
+ }
1808
+ /** Dispose every instance. */
1809
+ disposeAll() {
1810
+ for (const live of this.live.values()) live.dispose();
1811
+ this.live.clear();
1812
+ }
1813
+ newId(seed) {
1814
+ return `${seed.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "server"}-${this.host.newId()}`;
1815
+ }
1816
+ };
1408
1817
  //#endregion
1409
1818
  //#region lib/types/outbound/calls.js
1410
1819
  /**
@@ -1443,11 +1852,11 @@ var A2AClient = class A2AClient {
1443
1852
  },
1444
1853
  signal: AbortSignal.timeout(timeoutMs)
1445
1854
  });
1446
- if (!response.ok) throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_NOT_FOUND, `agent card fetch failed: HTTP ${response.status}`);
1855
+ if (!response.ok) throw new A2AError(A2A_ERROR_CODES.INVALID_AGENT_RESPONSE, `agent card fetch failed: HTTP ${response.status}`);
1447
1856
  const card = await response.json();
1448
- if (typeof card.name !== "string" || typeof card.description !== "string") throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_SIGNATURE_INVALID, `invalid agent card at ${agentCardUrl}`);
1449
- const endpoint = (card.supportedInterfaces?.find((i) => (i.protocolBinding ?? "JSONRPC") === "JSONRPC") ?? card.supportedInterfaces?.[0])?.url ?? card.url;
1450
- if (!endpoint) throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_NOT_FOUND, `agent card at ${agentCardUrl} advertises no JSON-RPC interface`);
1857
+ if (typeof card.name !== "string" || !Array.isArray(card.skills)) throw new A2AError(A2A_ERROR_CODES.INVALID_AGENT_RESPONSE, `invalid agent card at ${agentCardUrl}`);
1858
+ const endpoint = (card.supportedInterfaces?.find((i) => (i.protocolBinding ?? "JSONRPC") === "JSONRPC") ?? card.supportedInterfaces?.[0])?.url;
1859
+ if (!endpoint) throw new A2AError(A2A_ERROR_CODES.INVALID_AGENT_RESPONSE, `agent card at ${agentCardUrl} advertises no JSON-RPC interface`);
1451
1860
  return new A2AClient(card, endpoint, opts);
1452
1861
  }
1453
1862
  /** Send a message and wait (or poll) until the task settles. */
@@ -1500,7 +1909,7 @@ var A2AClient = class A2AClient {
1500
1909
  if (err instanceof Error && err.name === "TimeoutError") throw new A2AError(-32e3, `A2A call ${method} timed out after ${timeoutMs}ms`);
1501
1910
  throw err;
1502
1911
  }
1503
- if (response.status === 401) throw new A2AError(A2A_ERROR_CODES.UNAUTHORIZED, "remote agent rejected credentials (401)");
1912
+ if (response.status === 401) throw new A2AError(-32040, "remote agent rejected credentials (401)");
1504
1913
  if (!response.ok) throw new A2AError(A2A_ERROR_CODES.INTERNAL_ERROR, `A2A call ${method} failed: HTTP ${response.status}`);
1505
1914
  const payload = await response.json();
1506
1915
  if ("error" in payload) {
@@ -1594,27 +2003,6 @@ function textOf(artifacts, state, statusParts) {
1594
2003
  * connection. Loaded at boot, mutations persist through the agent store.
1595
2004
  * @module dsh-a2a/client/registry
1596
2005
  */
1597
- /** Domain-backed agent store: one JSON array under the `agents` table key. */
1598
- var DomainAgentStore = class DomainAgentStore {
1599
- table;
1600
- static KEY = "records";
1601
- constructor(table) {
1602
- this.table = table;
1603
- }
1604
- list() {
1605
- const raw = this.table.get(DomainAgentStore.KEY);
1606
- if (raw === void 0) return [];
1607
- try {
1608
- const parsed = JSON.parse(raw);
1609
- return Array.isArray(parsed) ? parsed : [];
1610
- } catch {
1611
- return [];
1612
- }
1613
- }
1614
- async save(records) {
1615
- await this.table.put(DomainAgentStore.KEY, JSON.stringify(records));
1616
- }
1617
- };
1618
2006
  /** A list of remote agents, each with live tool registrations when enabled. */
1619
2007
  var OutboundAgentRegistry = class {
1620
2008
  opts;
@@ -1845,162 +2233,265 @@ var OutboundAgentRegistry = class {
1845
2233
  }
1846
2234
  };
1847
2235
  //#endregion
1848
- //#region lib/types/service.js
2236
+ //#region lib/types/servers/outbound-manager.js
1849
2237
  /**
1850
- * The `ctx.a2a` service facade: the thin read/control surface commands and
1851
- * other plugins use. Registered as a Cordis Service so the key disappears
1852
- * with the plugin fiber.
1853
- * @module dsh-a2a/service
2238
+ * Outbound server manager: owns every outbound A2A connection instance. Each
2239
+ * instance is one connected remote its own AgentCard URL, auth env, timeout,
2240
+ * creator-declared name, and an optional agent-preset binding. Each live
2241
+ * instance is one `OutboundAgentRegistry` with an isolated per-instance agent
2242
+ * store; enabled instances map their remote skills to `a2a__<name>__<skill>`
2243
+ * model tools. The manager owns the full lifecycle (add/enable/disable/remove/
2244
+ * refresh) driven from the GUI and facade, persists each instance in the
2245
+ * `outbound_servers` domain table, and disposes every connection on teardown.
2246
+ *
2247
+ * The `preset` field names the agent preset this DSH would compose its local
2248
+ * hand-off session with when driving that remote. P0 connects skills to tools
2249
+ * and manages the connection; the hand-off session composition behind `preset`
2250
+ * is a documented extension point (the outbound tools currently call the
2251
+ * remote directly) and the value is persisted metadata surfaced to the GUI.
2252
+ * @module dsh-a2a/servers/outbound-manager
1854
2253
  */
1855
- /** The service other plugins read as `ctx.a2a`. */
1856
- var A2AService = class extends Service {
1857
- impl;
1858
- constructor(ctx, impl) {
1859
- super(ctx, "a2a");
1860
- this.impl = impl;
2254
+ /** An `AgentStore` bound to one outbound server instance's agents-table key. */
2255
+ var ServerAgentStore = class {
2256
+ table;
2257
+ key;
2258
+ constructor(table, key) {
2259
+ this.table = table;
2260
+ this.key = key;
1861
2261
  }
1862
- status() {
1863
- return this.impl.status();
2262
+ list() {
2263
+ const raw = this.table.get(this.key);
2264
+ if (raw === void 0 || raw === "") return [];
2265
+ try {
2266
+ const parsed = JSON.parse(raw);
2267
+ return Array.isArray(parsed) ? parsed : [];
2268
+ } catch {
2269
+ return [];
2270
+ }
1864
2271
  }
1865
- async enableServer(enable) {
1866
- return this.impl.enableServer(enable);
2272
+ async save(records) {
2273
+ await this.table.put(this.key, JSON.stringify(records));
1867
2274
  }
1868
- getTask(taskId) {
1869
- return this.impl.getTask(taskId);
2275
+ };
2276
+ /** Persisted-instance store over the outbound_servers table. */
2277
+ var DomainOutboundStore = class DomainOutboundStore {
2278
+ table;
2279
+ static KEY(id) {
2280
+ return `out:${id}`;
1870
2281
  }
1871
- listTasks() {
1872
- return this.impl.listTasks();
2282
+ constructor(table) {
2283
+ this.table = table;
1873
2284
  }
1874
- async cancelTask(taskId) {
1875
- return this.impl.cancelTask(taskId);
2285
+ list() {
2286
+ const out = [];
2287
+ const entries = this.table.entries;
2288
+ if (entries === void 0) return out;
2289
+ for (const [key, raw] of entries.call(this.table)) {
2290
+ if (!key.startsWith("out:")) continue;
2291
+ const parsed = safeParse(raw);
2292
+ if (parsed !== void 0) out.push(parsed);
2293
+ }
2294
+ return out;
1876
2295
  }
1877
- agents() {
1878
- return this.impl.agents();
2296
+ get(id) {
2297
+ const raw = this.table.get(DomainOutboundStore.KEY(id));
2298
+ return raw === void 0 ? void 0 : safeParse(raw);
1879
2299
  }
1880
- async addAgent(spec) {
1881
- return this.impl.addAgent(spec);
2300
+ async save(record) {
2301
+ await this.table.put(DomainOutboundStore.KEY(record.id), JSON.stringify(record));
1882
2302
  }
1883
- async removeAgent(id) {
1884
- return this.impl.removeAgent(id);
2303
+ async remove(id) {
2304
+ await this.table.put(DomainOutboundStore.KEY(id), "");
1885
2305
  }
1886
- async setAgentEnabled(id, enabled) {
1887
- return this.impl.setAgentEnabled(id, enabled);
2306
+ };
2307
+ function safeParse(raw) {
2308
+ try {
2309
+ const parsed = JSON.parse(raw);
2310
+ if (typeof parsed.id !== "string" || typeof parsed.name !== "string" || typeof parsed.agentCardUrl !== "string") return void 0;
2311
+ return parsed;
2312
+ } catch {
2313
+ return;
1888
2314
  }
1889
- async refreshAgentCard(id) {
1890
- return this.impl.refreshAgentCard(id);
2315
+ }
2316
+ /** Manages the live set of outbound connection instances. */
2317
+ var OutboundServerManager = class {
2318
+ host;
2319
+ agentSink;
2320
+ records;
2321
+ newId;
2322
+ registries = /* @__PURE__ */ new Map();
2323
+ constructor(host, agentSink, records, newId) {
2324
+ this.host = host;
2325
+ this.agentSink = agentSink;
2326
+ this.records = records;
2327
+ this.newId = newId;
2328
+ }
2329
+ /** The host default connection timeout (what a minimal record falls back to). */
2330
+ get defaultTimeoutMs() {
2331
+ return this.host.defaultTimeoutMs;
2332
+ }
2333
+ agentKeyFor(id) {
2334
+ return `out:${id}`;
2335
+ }
2336
+ makeRegistry(record) {
2337
+ return new OutboundAgentRegistry({
2338
+ registrar: { register: (def) => this.host.registrar.register(def) },
2339
+ store: new ServerAgentStore(this.agentSink, this.agentKeyFor(record.id)),
2340
+ toolPrefix: "a2a",
2341
+ defaultTimeoutMs: this.host.defaultTimeoutMs,
2342
+ tokenOf: (env) => this.host.tokenOf(env),
2343
+ onError: this.host.onError
2344
+ });
1891
2345
  }
1892
- identity() {
1893
- return this.impl.identity();
2346
+ /** Record → registry spec for connecting one instance. */
2347
+ toSpec(record) {
2348
+ return {
2349
+ name: record.name,
2350
+ agentCardUrl: record.agentCardUrl,
2351
+ ...record.bearerTokenEnv !== void 0 ? { bearerTokenEnv: record.bearerTokenEnv } : {},
2352
+ enabled: record.enabled,
2353
+ timeoutMs: record.timeoutMs ?? this.host.defaultTimeoutMs
2354
+ };
1894
2355
  }
1895
- async updateIdentity(patch) {
1896
- return this.impl.updateIdentity(patch);
2356
+ /** Boot every persisted enabled instance as a connected registry. */
2357
+ boot() {
2358
+ for (const record of this.records.list()) try {
2359
+ const registry = this.makeRegistry(record);
2360
+ this.registries.set(record.id, registry);
2361
+ if (record.enabled) registry.loadAll();
2362
+ } catch (err) {
2363
+ this.host.onError(`[a2a:out:${record.id}] failed to boot: ${String(err.message)}`);
2364
+ }
1897
2365
  }
1898
- async closeInbound(peerId) {
1899
- return this.impl.closeInbound(peerId);
2366
+ /** One view per live instance (enriched from its registry / record). */
2367
+ list() {
2368
+ return [...this.registries.keys()].map((id) => this.viewFor(id)).filter((v) => v !== void 0);
1900
2369
  }
1901
- inbounds() {
1902
- return this.impl.inbounds();
2370
+ get(id) {
2371
+ const registry = this.registries.get(id);
2372
+ return registry === void 0 ? void 0 : {
2373
+ id,
2374
+ registry
2375
+ };
1903
2376
  }
1904
- };
1905
- //#endregion
1906
- //#region lib/types/commands.js
1907
- /**
1908
- * The `/a2a` command surface: status, server enable/disable, card, agents,
1909
- * and task control — the P0 operations surface (the settings panel is P1).
1910
- * @module dsh-a2a/commands
1911
- */
1912
- /** Build the `/a2a` command definition over the service facade. */
1913
- function buildA2aCommand(impl) {
1914
- return {
1915
- name: "a2a",
1916
- description: "Manage the A2A plugin: status, server, agents, tasks, card.",
1917
- handler: async (invocation) => {
1918
- const [verb, ...rest] = invocation.rawInput.trim().split(/\s+/);
1919
- switch (verb ?? "") {
1920
- case "": {
1921
- const status = impl.status();
1922
- return success(`A2A status\n${JSON.stringify(status, null, 2)}`);
1923
- }
1924
- case "status": return success(JSON.stringify(impl.status(), null, 2));
1925
- case "enable": return outcome(await impl.enableServer(true));
1926
- case "disable": return outcome(await impl.enableServer(false));
1927
- case "card": return success(JSON.stringify(impl.listTasks(), null, 2).slice(0, 4e3));
1928
- case "agents": return success(JSON.stringify(impl.agents(), null, 2));
1929
- case "agent": {
1930
- const [action, ...args] = rest;
1931
- switch (action ?? "") {
1932
- case "add": {
1933
- const url = args[0];
1934
- if (!url) return error("usage: /a2a agent add <agentCardUrl> [name]");
1935
- const name = args[1] ?? url;
1936
- return outcome(await impl.addAgent({
1937
- name,
1938
- agentCardUrl: url
1939
- }));
1940
- }
1941
- case "remove": {
1942
- const id = args[0];
1943
- if (!id) return error("usage: /a2a agent remove <id>");
1944
- return outcome(await impl.removeAgent(id));
1945
- }
1946
- case "enable": {
1947
- const id = args[0];
1948
- if (!id) return error("usage: /a2a agent enable <id>");
1949
- return outcome(await impl.setAgentEnabled(id, true));
1950
- }
1951
- case "disable": {
1952
- const id = args[0];
1953
- if (!id) return error("usage: /a2a agent disable <id>");
1954
- return outcome(await impl.setAgentEnabled(id, false));
1955
- }
1956
- case "refresh": {
1957
- const id = args[0];
1958
- if (!id) return error("usage: /a2a agent refresh <id>");
1959
- return outcome(await impl.refreshAgentCard(id));
1960
- }
1961
- default: return error("usage: /a2a agent add|remove|enable|disable|refresh ...");
1962
- }
1963
- }
1964
- case "tasks": return success(JSON.stringify(impl.listTasks(), null, 2));
1965
- case "task": {
1966
- const [action, id] = rest;
1967
- if (action === "get" && id) return success(JSON.stringify(impl.getTask(id), null, 2));
1968
- if (action === "cancel" && id) return outcome(await impl.cancelTask(id));
1969
- return error("usage: /a2a task get|cancel <taskId>");
1970
- }
1971
- case "help": return success("a2a commands: status | enable | disable | card | agents | agent add/remove/enable/disable/refresh | tasks | task get/cancel <id> | help");
1972
- default: return error(`unknown a2a verb "${verb}" (try /a2a help)`);
1973
- }
2377
+ viewFor(id) {
2378
+ const registry = this.registries.get(id);
2379
+ const record = this.records.get(id);
2380
+ if (registry === void 0 || record === void 0) return void 0;
2381
+ const agent = registry.list()[0];
2382
+ return {
2383
+ id,
2384
+ name: record.name,
2385
+ agentCardUrl: record.agentCardUrl,
2386
+ ...record.preset !== void 0 ? { preset: record.preset } : {},
2387
+ enabled: agent?.enabled ?? record.enabled,
2388
+ timeoutMs: record.timeoutMs,
2389
+ state: agent?.state ?? "disconnected",
2390
+ skillCount: agent?.skillCount ?? 0,
2391
+ toolCount: agent?.toolCount ?? 0,
2392
+ ...agent?.lastError !== void 0 ? { lastError: agent.lastError } : {}
2393
+ };
2394
+ }
2395
+ /** Create, persist, and connect a new outbound instance. */
2396
+ async add(input) {
2397
+ const record = {
2398
+ id: input.id ?? this.newId(),
2399
+ name: input.name,
2400
+ agentCardUrl: input.agentCardUrl,
2401
+ ...input.bearerTokenEnv !== void 0 ? { bearerTokenEnv: input.bearerTokenEnv } : {},
2402
+ ...input.preset !== void 0 ? { preset: input.preset } : {},
2403
+ enabled: input.enabled ?? true,
2404
+ timeoutMs: input.timeoutMs ?? this.host.defaultTimeoutMs
2405
+ };
2406
+ if (this.registries.has(record.id)) return {
2407
+ ok: false,
2408
+ message: `outbound server ${record.id} already exists`
2409
+ };
2410
+ const registry = this.makeRegistry(record);
2411
+ await this.records.save(record);
2412
+ this.registries.set(record.id, registry);
2413
+ if (record.enabled) {
2414
+ const result = await registry.add(this.toSpec(record));
2415
+ return result.ok ? {
2416
+ ...result,
2417
+ id: record.id
2418
+ } : result;
1974
2419
  }
1975
- };
1976
- }
1977
- function success(text) {
1978
- return {
1979
- kind: "success",
1980
- text
1981
- };
1982
- }
1983
- function error(text) {
1984
- return {
1985
- kind: "error",
1986
- text
1987
- };
1988
- }
1989
- function outcome(result) {
1990
- return result.ok ? success(result.message) : error(result.message);
1991
- }
2420
+ return {
2421
+ ok: true,
2422
+ message: `outbound server "${record.name}" added (disabled)`,
2423
+ id: record.id
2424
+ };
2425
+ }
2426
+ async setEnabled(id, enabled) {
2427
+ const registry = this.registries.get(id);
2428
+ if (registry === void 0) return {
2429
+ ok: false,
2430
+ message: `outbound server ${id} not found`
2431
+ };
2432
+ const view = registry.list()[0];
2433
+ const record = this.records.get(id);
2434
+ if (record !== void 0) await this.records.save({
2435
+ ...record,
2436
+ enabled
2437
+ });
2438
+ if (view === void 0) return {
2439
+ ok: true,
2440
+ message: `outbound server ${id} marked ${enabled ? "enabled" : "disabled"}`
2441
+ };
2442
+ return registry.setEnabled(view.id, enabled);
2443
+ }
2444
+ async remove(id) {
2445
+ const registry = this.registries.get(id);
2446
+ if (registry === void 0) return {
2447
+ ok: false,
2448
+ message: `outbound server ${id} not found`
2449
+ };
2450
+ const view = registry.list()[0];
2451
+ if (view !== void 0) await registry.remove(view.id);
2452
+ await registry.disposeAll();
2453
+ this.registries.delete(id);
2454
+ await this.records.remove(id);
2455
+ return {
2456
+ ok: true,
2457
+ message: `outbound server ${id} removed`
2458
+ };
2459
+ }
2460
+ async refresh(id) {
2461
+ const registry = this.registries.get(id);
2462
+ if (registry === void 0) return {
2463
+ ok: false,
2464
+ message: `outbound server ${id} not found`
2465
+ };
2466
+ const view = registry.list()[0];
2467
+ if (view === void 0) return {
2468
+ ok: false,
2469
+ message: `outbound server ${id} has no connected agent`
2470
+ };
2471
+ return registry.refresh(view.id);
2472
+ }
2473
+ async disposeAll() {
2474
+ for (const registry of this.registries.values()) await registry.disposeAll();
2475
+ this.registries.clear();
2476
+ }
2477
+ };
1992
2478
  //#endregion
1993
2479
  //#region lib/types/index.js
1994
2480
  /**
1995
2481
  * @hanphone/dsh-a2a — Cordis plugin entry.
1996
2482
  *
1997
- * Mounts the A2A v1.0 dual-end plugin: the inbound server (AgentCard derived
1998
- * from the live tool registry, JSON-RPC + SSE, durable SQLite task store,
1999
- * session/subagent executors, the `a2a/inbound-task` policy gate + audit)
2000
- * and the outbound client (persisted multi-agent registry, skills mapped to
2001
- * model tools, sync calls with per-agent timeout). Optional services are
2002
- * probed rather than injected so a composition lacking one half idles just
2003
- * that half; only the storage domain is required.
2483
+ * Mounts the A2A v1.0.1 dual-end plugin as a MULTI-INSTANCE composition: an
2484
+ * arbitrary set of inbound A2A servers (each a preset-bound session pool, its
2485
+ * own endpoint + AgentCard + creator-declared skills + auth) and an arbitrary
2486
+ * set of outbound A2A connections (each a remote URL + preset + timeout).
2487
+ * Instances are persisted in the `a2a` domain (`inbound_servers` /
2488
+ * `outbound_servers` tables) and created/edited/started/stopped entirely from
2489
+ * the GUI the plugin `Config` is minimal and instance setup is not
2490
+ * patch-config driven. The protocol surface is aligned with the official A2A
2491
+ * v1.0.1 spec (see docs/design.md).
2492
+ *
2493
+ * Optional services are probed rather than injected so a composition lacking
2494
+ * one half idles just that half; only the storage domain is required.
2004
2495
  *
2005
2496
  * Function-plugin export shape: named `name`/`inject`/`Config`/`apply`, no
2006
2497
  * default export (mixing forms makes the Loader drop the namespace).
@@ -2011,304 +2502,194 @@ const name = "a2a";
2011
2502
  const inject = ["storageDomain"];
2012
2503
  /** Loader-validated config schema (defaults applied by the Loader). */
2013
2504
  const Config = z.object({
2014
- server: z.object({
2015
- enabled: z.boolean().default(true),
2016
- name: z.string().default("My DSH Agent"),
2017
- description: z.string().default("A DeepSeek Harness agent exposed over A2A v1.0"),
2018
- version: z.string().default("0.1.0"),
2019
- baseUrl: z.string(),
2020
- endpointPath: z.string().default("/a2a"),
2021
- authTokenEnv: z.string(),
2022
- skills: z.object({
2023
- ids: z.array(z.string()).default([]),
2024
- exclude: z.array(z.string()).default([])
2025
- }),
2026
- executors: z.dict(z.union(["session", "subagent"])).default({ chat: "session" }),
2027
- subagentProvider: z.string().default("in-process")
2028
- }),
2029
- client: z.object({
2030
- agents: z.array(z.object({
2031
- name: z.string(),
2032
- agentCardUrl: z.string(),
2033
- bearerTokenEnv: z.string(),
2034
- enabled: z.boolean().default(true),
2035
- timeoutMs: z.number().default(6e4)
2036
- })).default([]),
2037
- toolPrefix: z.string().default("a2a")
2038
- })
2505
+ baseUrl: z.string(),
2506
+ subagentProvider: z.string().default("in-process"),
2507
+ defaultTimeoutMs: z.number().default(6e4)
2039
2508
  });
2040
2509
  function apply(ctx, config) {
2041
2510
  const logger = ctx.logger("a2a");
2042
- const serverConfig = { ...config.server };
2043
- const clientConfig = { ...config.client };
2044
- const authToken = serverConfig.authTokenEnv !== void 0 ? process.env[serverConfig.authTokenEnv] : void 0;
2045
- const toolPrefix = clientConfig.toolPrefix;
2046
- const subagentProvider = serverConfig.subagentProvider;
2511
+ const subagentProvider = config.subagentProvider;
2047
2512
  const inboundCwd = inboundSessionCwd();
2048
2513
  ctx.inject(["storageDomain"], (ctx) => {
2049
2514
  ctx.effect(async () => {
2050
2515
  const domain = await openDomain(ctx.storageDomain);
2051
2516
  const store = new DomainTaskStore(domain);
2052
- const holder = {
2517
+ const webServer = probeService(ctx, "webServer", "register");
2518
+ const tools = probeService(ctx, "tools", "register");
2519
+ const agents = probeService(ctx, "agents", "create");
2520
+ const agentPresets = probeService(ctx, "agentPresets", "list");
2521
+ const subagents = probeService(ctx, "subagents", "start");
2522
+ const commandsRef = probeService(ctx, "commands", "register");
2523
+ const baseUrl = config.baseUrl ?? `http://127.0.0.1:${webServerPort(webServer) ?? process.env["DSH_A2A_PORT"] ?? "3000"}`;
2524
+ const inboundManager = new InboundServerManager({
2525
+ ctx,
2526
+ webServer: webServer ?? { register: noopRegister },
2527
+ tasks: store,
2528
+ logger,
2529
+ ...agents !== void 0 ? { agents } : {},
2530
+ ...agentPresets !== void 0 ? { agentPresets } : {},
2531
+ ...subagents !== void 0 ? { subagents } : {},
2532
+ resolveDefaultModel: () => resolveDefaultModel(ctx),
2533
+ sessionCwd: inboundCwd,
2534
+ defaultBaseUrl: baseUrl,
2535
+ subagentProvider,
2536
+ newId: () => crypto.randomUUID().slice(0, 8)
2537
+ }, new DomainInboundStore(domain.inbound_servers));
2538
+ inboundManager.boot();
2539
+ const outboundManager = new OutboundServerManager({
2540
+ registrar: { register: (def) => tools?.register(def) },
2541
+ tokenOf: (env) => env ? process.env[env] : void 0,
2542
+ onError: (message) => logger.warn(message),
2543
+ defaultTimeoutMs: config.defaultTimeoutMs
2544
+ }, domain.agents, new DomainOutboundStore(domain.outbound_servers), () => `out-${crypto.randomUUID().slice(0, 12)}`);
2545
+ outboundManager.boot();
2546
+ const impl = makeFacade({
2053
2547
  domain,
2054
2548
  store,
2055
- registry: void 0,
2056
- inbound: void 0,
2057
- server: void 0,
2058
- routes: void 0,
2059
- card: void 0,
2060
- executors: void 0,
2061
- enabled: serverConfig.enabled,
2062
- identityDefaults: {
2063
- name: serverConfig.name ?? "My DSH Agent",
2064
- description: serverConfig.description ?? "A DeepSeek Harness agent exposed over A2A v1.0",
2065
- version: serverConfig.version ?? "0.1.0"
2066
- }
2067
- };
2068
- const webServer = probeService(ctx, "webServer", "register");
2069
- if (webServer === void 0) logger.warn("a2a: webServer not mounted; inbound server idle");
2070
- else try {
2071
- const skills = deriveSkills(probeService(ctx, "tools", "get") ?? { get: () => void 0 }, {
2072
- ids: serverConfig.skills.ids,
2073
- exclude: serverConfig.skills.exclude
2074
- });
2075
- const baseUrl = serverConfig.baseUrl ?? `http://127.0.0.1:${process.env["DSH_A2A_PORT"] ?? "3000"}`;
2076
- const endpointPath = serverConfig.endpointPath ?? "/a2a";
2077
- const persistedIdentity = readIdentity(domain);
2078
- const card = buildCard(cardOptionsFor(baseUrl, endpointPath, holder.identityDefaults, persistedIdentity, skills, authToken));
2079
- holder.card = card;
2080
- const inbound = new LiveInboundRegistry();
2081
- holder.inbound = inbound;
2082
- const agents = probeService(ctx, "agents", "create");
2083
- const presets = probeService(ctx, "agentPresets", "resolve");
2084
- const sessionPool = agents ? new ContextSessionPool(agents, {
2085
- cwd: inboundCwd,
2086
- ...presets ? { agentPresets: presets } : {},
2087
- resolveAgentOptions: () => resolveDefaultModel(ctx)
2088
- }) : void 0;
2089
- const sessionExecutor = sessionPool ? createSessionExecutor(sessionPool) : refuseExecutor("no agent loop mounted");
2090
- const subagents = probeService(ctx, "subagents", "start");
2091
- const subagentExecutor = sessionPool !== void 0 && subagents !== void 0 ? createSubagentExecutor({
2092
- pool: sessionPool,
2093
- subagents,
2094
- provider: subagentProvider
2095
- }) : void 0;
2096
- const executors = new ExecutorSet(serverConfig.executors, sessionExecutor, subagentExecutor);
2097
- holder.executors = executors;
2098
- const skillIds = new Set(skills.map((s) => s.id));
2099
- const gate = async (input) => {
2100
- if (!skillIds.has(input.skill)) return {
2101
- ok: false,
2102
- reason: `unknown skill "${input.skill}"`
2103
- };
2104
- const decision = {
2105
- contextId: input.contextId,
2106
- skill: input.skill,
2107
- parts: input.parts,
2108
- remotePeerId: input.remotePeerId
2109
- };
2110
- const decided = await ctx.waterfall("a2a/inbound-task", decision, async (d) => d);
2111
- logger.info(`[a2a] inbound skill=${decided.skill} remote=${decided.remotePeerId} rejected=${decided.rejected?.reason ?? "no"}`);
2112
- if (decided.rejected !== void 0) return {
2113
- ok: false,
2114
- reason: decided.rejected.reason
2115
- };
2116
- return { ok: true };
2117
- };
2118
- const server = new A2AServer({
2119
- card,
2120
- store,
2121
- executors,
2122
- ...authToken ? { authToken } : {},
2123
- gate,
2124
- onInbound: (facts) => inbound.note({
2125
- method: facts.method,
2126
- ...facts.source !== void 0 ? { source: facts.source } : {},
2127
- taskIds: facts.taskIds,
2128
- streaming: facts.streaming
2129
- }),
2130
- onTaskSettled: (taskId) => {
2131
- logger.info(`[a2a] task settled ${taskId}`);
2132
- inbound.settle(taskId);
2133
- }
2134
- });
2135
- holder.server = server;
2136
- const routes = new A2aRoutes(webServer, server);
2137
- holder.routes = routes;
2138
- if (holder.enabled) routes.enable();
2139
- } catch (err) {
2140
- logger.error(`a2a: server half failed to build: ${err.message}`);
2141
- }
2142
- const toolsService = probeService(ctx, "tools", "get");
2143
- if (toolsService === void 0) logger.warn("a2a: tools service not mounted; outbound client idle");
2144
- else {
2145
- const registry = new OutboundAgentRegistry({
2146
- registrar: { register: (def) => toolsService.register(def) },
2147
- store: new DomainAgentStore(domain.agents),
2148
- toolPrefix,
2149
- tokenOf: (env) => env ? process.env[env] : void 0,
2150
- onError: (message) => logger.warn(message)
2151
- });
2152
- holder.registry = registry;
2153
- registry.loadAll();
2154
- if (clientConfig.agents.length > 0) await registry.seed(clientConfig.agents);
2155
- }
2156
- const impl = makeFacade(holder, logger.info.bind(logger));
2549
+ inboundManager,
2550
+ outboundManager,
2551
+ agentPresets,
2552
+ logger: logger.info.bind(logger)
2553
+ });
2157
2554
  new A2AService(ctx, impl);
2158
- const commands = probeService(ctx, "commands", "register");
2159
- if (commands !== void 0) ctx.effect(() => commands.register(buildA2aCommand(impl)), "a2a: command");
2160
- const dashboardWebServer = probeService(ctx, "webServer", "register");
2161
- if (dashboardWebServer !== void 0) ctx.effect(() => dashboardWebServer.register({
2555
+ if (commandsRef !== void 0) ctx.effect(() => commandsRef.register(buildA2aCommand(impl)), "a2a: command");
2556
+ if (webServer !== void 0) ctx.effect(() => webServer.register({
2162
2557
  kind: "prefix",
2163
2558
  path: "/a2a/api",
2164
2559
  handler: (req, res) => handleApiRequest(req, res, impl)
2165
2560
  }), "a2a: dashboard api");
2166
2561
  else logger.warn("a2a: webServer not mounted; GUI dashboard API idle");
2167
2562
  return async () => {
2168
- holder.routes?.dispose();
2169
- await holder.executors?.disposeAll();
2170
- await holder.registry?.disposeAll();
2563
+ inboundManager.disposeAll();
2564
+ await outboundManager.disposeAll();
2171
2565
  await domain.close();
2172
2566
  };
2173
- }, "a2a: domain + halves");
2567
+ }, "a2a: multi-instance composition");
2174
2568
  });
2175
2569
  }
2176
- /** Facade closure over the shared holder (commands and `ctx.a2a` consumers). */
2177
- function makeFacade(holder, log) {
2178
- const serverEnabled = () => holder.routes?.active ?? false;
2570
+ /** Facade closure over the two managers (commands and `ctx.a2a` consumers). */
2571
+ function makeFacade(host) {
2572
+ const inboundView = (id) => {
2573
+ const live = host.inboundManager.get(id);
2574
+ if (live === void 0) return void 0;
2575
+ const r = live.record;
2576
+ return {
2577
+ id: r.id,
2578
+ name: r.name,
2579
+ description: r.description,
2580
+ version: r.version,
2581
+ endpointPath: live.endpointPath,
2582
+ ...r.preset !== void 0 ? { preset: r.preset } : {},
2583
+ ...r.authTokenEnv !== void 0 ? { authTokenEnv: r.authTokenEnv } : {},
2584
+ cardPath: live.cardPath,
2585
+ ...live.card.supportedInterfaces?.[0]?.url !== void 0 ? { cardUrl: live.card.supportedInterfaces[0].url } : {},
2586
+ enabled: live.routes.active,
2587
+ skills: r.skills.map((s) => ({
2588
+ id: s.id,
2589
+ name: s.name,
2590
+ ...s.description != null ? { description: s.description } : {}
2591
+ }))
2592
+ };
2593
+ };
2179
2594
  return {
2180
2595
  status() {
2181
2596
  return {
2182
- server: {
2183
- enabled: serverEnabled(),
2184
- cardUrl: holder.card?.supportedInterfaces?.[0]?.url,
2185
- skills: holder.card?.skills?.map((s) => s.id) ?? [],
2186
- executors: holder.executors ? ["session", ...holder.executors["subagent"] !== void 0 ? ["subagent"] : []] : [],
2187
- name: holder.card?.name,
2188
- description: holder.card?.description,
2189
- version: holder.card?.version,
2190
- configured: readIdentity(holder.domain) !== void 0
2191
- },
2192
- tasks: holder.store.list().length,
2193
- agents: holder.registry?.list() ?? [],
2194
- inbounds: holder.inbound?.list() ?? []
2597
+ inbounds: host.inboundManager.list().map((live) => inboundView(live.id)).filter((v) => v !== void 0),
2598
+ outbounds: host.outboundManager.list(),
2599
+ tasks: host.store.list().length
2195
2600
  };
2196
2601
  },
2197
- async enableServer(enable) {
2198
- if (holder.routes === void 0) return {
2199
- ok: false,
2200
- message: "inbound server is not mounted (no webServer)"
2201
- };
2202
- if (enable) holder.routes.enable();
2203
- else holder.routes.disable();
2204
- log(`[a2a] server ${enable ? "enabled" : "disabled"}`);
2205
- return {
2206
- ok: true,
2207
- message: `server ${enable ? "enabled" : "disabled"}`
2208
- };
2602
+ async presets() {
2603
+ if (host.agentPresets === void 0) return [];
2604
+ try {
2605
+ return (await host.agentPresets.list()).map((p) => ({
2606
+ id: p.id,
2607
+ ...p.name !== void 0 ? { name: p.name } : {},
2608
+ ...p.description !== void 0 ? { description: p.description } : {},
2609
+ ...p.isDefault === true ? { isDefault: true } : {}
2610
+ }));
2611
+ } catch {
2612
+ return [];
2613
+ }
2209
2614
  },
2210
- getTask(taskId) {
2211
- return holder.store.get(taskId);
2615
+ listInboundServers() {
2616
+ return host.inboundManager.list().map((live) => inboundView(live.id)).filter((v) => v !== void 0);
2212
2617
  },
2213
- listTasks() {
2214
- return holder.store.list();
2618
+ async createInboundServer(input) {
2619
+ const result = await host.inboundManager.add({
2620
+ name: input.name,
2621
+ description: input.description,
2622
+ version: input.version,
2623
+ ...input.endpointPath !== void 0 ? { endpointPath: input.endpointPath } : {},
2624
+ ...input.preset !== void 0 ? { preset: input.preset } : {},
2625
+ ...input.authTokenEnv !== void 0 ? { authTokenEnv: input.authTokenEnv } : {},
2626
+ ...input.skills !== void 0 ? { skills: input.skills } : {},
2627
+ ...input.enabled !== void 0 ? { enabled: input.enabled } : {}
2628
+ });
2629
+ host.logger(`[a2a] inbound created: ${result.message}`);
2630
+ return result;
2215
2631
  },
2216
- async cancelTask(taskId) {
2217
- if (holder.store.get(taskId) === void 0) return {
2218
- ok: false,
2219
- message: `task ${taskId} not found`
2220
- };
2221
- return {
2222
- ok: true,
2223
- message: holder.server?.abort?.(taskId) ?? false ? `task ${taskId} canceled` : `task ${taskId} already terminal`
2224
- };
2632
+ async removeInboundServer(id) {
2633
+ return host.inboundManager.remove(id);
2225
2634
  },
2226
- agents() {
2227
- return holder.registry?.list() ?? [];
2635
+ async setInboundServerEnabled(id, enabled) {
2636
+ return host.inboundManager.setEnabled(id, enabled);
2228
2637
  },
2229
- async addAgent(spec) {
2230
- const registry = holder.registry;
2231
- if (registry === void 0) return {
2232
- ok: false,
2233
- message: "outbound client not mounted (no tools service)"
2234
- };
2235
- return await registry.add({
2236
- ...spec,
2237
- enabled: true,
2238
- timeoutMs: 6e4
2638
+ async updateInboundServer(id, patch) {
2639
+ return host.inboundManager.update(id, patch);
2640
+ },
2641
+ listOutboundServers() {
2642
+ return [...host.outboundManager.list()];
2643
+ },
2644
+ async createOutboundServer(input) {
2645
+ return host.outboundManager.add({
2646
+ ...input.id !== void 0 ? { id: input.id } : {},
2647
+ name: input.name,
2648
+ agentCardUrl: input.agentCardUrl,
2649
+ ...input.bearerTokenEnv !== void 0 ? { bearerTokenEnv: input.bearerTokenEnv } : {},
2650
+ ...input.preset !== void 0 ? { preset: input.preset } : {},
2651
+ enabled: input.enabled ?? true,
2652
+ timeoutMs: input.timeoutMs ?? host.outboundManager.defaultTimeoutMs
2239
2653
  });
2240
2654
  },
2241
- async removeAgent(id) {
2242
- return holder.registry?.remove(id) ?? {
2243
- ok: false,
2244
- message: "outbound client not mounted"
2245
- };
2655
+ async removeOutboundServer(id) {
2656
+ return host.outboundManager.remove(id);
2246
2657
  },
2247
- async setAgentEnabled(id, enabled) {
2248
- return holder.registry?.setEnabled(id, enabled) ?? {
2249
- ok: false,
2250
- message: "outbound client not mounted"
2251
- };
2658
+ async setOutboundServerEnabled(id, enabled) {
2659
+ return host.outboundManager.setEnabled(id, enabled);
2252
2660
  },
2253
- async refreshAgentCard(id) {
2254
- return holder.registry?.refresh(id) ?? {
2255
- ok: false,
2256
- message: "outbound client not mounted"
2257
- };
2661
+ async refreshOutboundServer(id) {
2662
+ return host.outboundManager.refresh(id);
2258
2663
  },
2259
- identity() {
2260
- const current = holder.card;
2261
- return {
2262
- ...readIdentity(holder.domain) ?? {},
2263
- defaults: holder.identityDefaults,
2264
- name: current?.name ?? holder.identityDefaults.name,
2265
- description: current?.description ?? holder.identityDefaults.description,
2266
- version: current?.version ?? holder.identityDefaults.version
2267
- };
2664
+ getTask(taskId) {
2665
+ return host.store.get(taskId);
2268
2666
  },
2269
- async updateIdentity(patch) {
2270
- const card = holder.card;
2271
- const server = holder.server;
2272
- if (card === void 0 || server === void 0) return {
2273
- ok: false,
2274
- message: "inbound server not mounted (no card)"
2275
- };
2276
- const base = readIdentity(holder.domain) ?? holder.identityDefaults;
2277
- const next = {
2278
- name: patch.name?.trim() || base.name,
2279
- description: patch.description?.trim() || base.description,
2280
- version: patch.version?.trim() || base.version
2281
- };
2282
- writeIdentity(holder.domain, next);
2283
- const rebuilt = rebuildCardWithIdentity(card, next, card.skills ?? []);
2284
- server.setCard(rebuilt);
2285
- holder.card = rebuilt;
2286
- log(`[a2a] identity updated: ${next.name}`);
2667
+ listTasks() {
2668
+ return host.store.list();
2669
+ },
2670
+ async cancelTask(taskId) {
2671
+ const live = host.inboundManager.list().find((l) => l.server.abort(taskId));
2287
2672
  return {
2288
- ok: true,
2289
- message: `service identity updated (${next.name})`
2673
+ ok: live !== void 0,
2674
+ message: live !== void 0 ? `task ${taskId} canceled` : `task ${taskId} not found or already terminal`
2290
2675
  };
2291
2676
  },
2677
+ inbounds() {
2678
+ return host.inboundManager.list().flatMap((live) => live.inbound.list());
2679
+ },
2292
2680
  async closeInbound(peerId) {
2293
- const inbound = holder.inbound;
2294
- if (inbound === void 0) return {
2681
+ for (const live of host.inboundManager.list()) {
2682
+ const peers = live.inbound.activeTasksOf(peerId);
2683
+ for (const taskId of peers) live.server.abort(taskId);
2684
+ if (live.inbound.closePeer(peerId)) return {
2685
+ ok: true,
2686
+ message: `inbound peer ${peerId} closed`
2687
+ };
2688
+ }
2689
+ return {
2295
2690
  ok: false,
2296
- message: "inbound registry not mounted"
2691
+ message: `inbound peer ${peerId} not found`
2297
2692
  };
2298
- for (const taskId of inbound.activeTasksOf(peerId)) await holder.server?.abort?.(taskId);
2299
- return inbound.closePeer(peerId);
2300
- },
2301
- inbounds() {
2302
- return holder.inbound?.list() ?? [];
2303
- }
2304
- };
2305
- }
2306
- /** A task that refuses readably on compositions without an agent loop. */
2307
- function refuseExecutor(reason) {
2308
- return {
2309
- name: "refuse",
2310
- async execute() {
2311
- throw new Error(`a2a: ${reason}; refusing inbound task`);
2312
2693
  }
2313
2694
  };
2314
2695
  }
@@ -2324,5 +2705,15 @@ function inboundSessionCwd() {
2324
2705
  } catch {}
2325
2706
  return dir;
2326
2707
  }
2708
+ function noopRegister() {
2709
+ return () => {};
2710
+ }
2711
+ /** The listening port of a mounted webServer (undefined when unavailable). */
2712
+ function webServerPort(webServer) {
2713
+ const candidate = webServer;
2714
+ if (candidate === void 0) return void 0;
2715
+ const value = typeof candidate.port === "function" ? candidate.port() : candidate.port;
2716
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : void 0;
2717
+ }
2327
2718
  //#endregion
2328
2719
  export { A2AClient, A2AError, A2AServer, A2AService, Config, apply, inject, name };