@hanphone/dsh-a2a 0.1.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 (88) hide show
  1. package/README.md +133 -87
  2. package/README.zh.md +64 -87
  3. package/cordis.patch.yml +4 -7
  4. package/lib/client.js +228 -36
  5. package/lib/index.js +1271 -625
  6. package/lib/types/api.d.ts +43 -13
  7. package/lib/types/api.d.ts.map +1 -1
  8. package/lib/types/api.js +60 -20
  9. package/lib/types/api.js.map +1 -1
  10. package/lib/types/client/index.d.ts +50 -14
  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 +178 -213
  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 +7 -1
  30. package/lib/types/server/a2a-server.d.ts.map +1 -1
  31. package/lib/types/server/a2a-server.js +11 -3
  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/identity.d.ts +38 -0
  42. package/lib/types/server/identity.d.ts.map +1 -0
  43. package/lib/types/server/identity.js +89 -0
  44. package/lib/types/server/identity.js.map +1 -0
  45. package/lib/types/server/inbound-registry.d.ts +60 -0
  46. package/lib/types/server/inbound-registry.d.ts.map +1 -0
  47. package/lib/types/server/inbound-registry.js +86 -0
  48. package/lib/types/server/inbound-registry.js.map +1 -0
  49. package/lib/types/server/routes.d.ts +5 -1
  50. package/lib/types/server/routes.d.ts.map +1 -1
  51. package/lib/types/server/routes.js +9 -5
  52. package/lib/types/server/routes.js.map +1 -1
  53. package/lib/types/server/store.d.ts +37 -1
  54. package/lib/types/server/store.d.ts.map +1 -1
  55. package/lib/types/server/store.js +14 -0
  56. package/lib/types/server/store.js.map +1 -1
  57. package/lib/types/servers/inbound-manager.d.ts +131 -0
  58. package/lib/types/servers/inbound-manager.d.ts.map +1 -0
  59. package/lib/types/servers/inbound-manager.js +312 -0
  60. package/lib/types/servers/inbound-manager.js.map +1 -0
  61. package/lib/types/servers/outbound-manager.d.ts +129 -0
  62. package/lib/types/servers/outbound-manager.d.ts.map +1 -0
  63. package/lib/types/servers/outbound-manager.js +238 -0
  64. package/lib/types/servers/outbound-manager.js.map +1 -0
  65. package/lib/types/service.d.ts +108 -23
  66. package/lib/types/service.d.ts.map +1 -1
  67. package/lib/types/service.js +39 -18
  68. package/lib/types/service.js.map +1 -1
  69. package/package.json +3 -3
  70. package/src/api.ts +70 -31
  71. package/src/client/index.ts +339 -81
  72. package/src/commands.ts +59 -43
  73. package/src/events.ts +14 -7
  74. package/src/index.ts +204 -249
  75. package/src/outbound/calls.ts +8 -6
  76. package/src/protocol.ts +204 -95
  77. package/src/server/a2a-server.ts +16 -4
  78. package/src/server/card.ts +13 -62
  79. package/src/server/exec/agent-runtime.ts +5 -2
  80. package/src/server/identity.ts +109 -0
  81. package/src/server/inbound-registry.ts +129 -0
  82. package/src/server/routes.ts +6 -4
  83. package/src/server/store.ts +50 -4
  84. package/src/servers/inbound-manager.ts +385 -0
  85. package/src/servers/outbound-manager.ts +289 -0
  86. package/src/service.ts +136 -28
  87. package/lib/tsconfig.client.tsbuildinfo +0 -1
  88. 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
  }
@@ -175,7 +111,10 @@ const a2aDomainSpec = defineDomain({
175
111
  tables: {
176
112
  tasks: domainTable(json$1),
177
113
  contexts: domainTable(json$1),
178
- agents: domainTable(json$1)
114
+ agents: domainTable(json$1),
115
+ identity: domainTable(json$1),
116
+ inbound_servers: domainTable(json$1),
117
+ outbound_servers: domainTable(json$1)
179
118
  }
180
119
  });
181
120
  /**
@@ -214,6 +153,15 @@ var A2aDomain = class {
214
153
  get agents() {
215
154
  return this.handle.table("agents");
216
155
  }
156
+ get identity() {
157
+ return this.handle.table("identity");
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
+ }
217
165
  async close() {
218
166
  await this.handle.close();
219
167
  }
@@ -269,7 +217,8 @@ var DomainTaskStore = class {
269
217
  parts: input.parts,
270
218
  artifacts: [],
271
219
  executor: "session",
272
- summary: null
220
+ summary: null,
221
+ ...input.serverId !== void 0 ? { serverId: input.serverId } : {}
273
222
  };
274
223
  this.records.set(record.taskId, record);
275
224
  this.domain.tasks.put(recordKey(record.taskId), encodeRecord(record)).catch((err) => {
@@ -329,41 +278,6 @@ function withState(current, state, message, timestamp) {
329
278
  };
330
279
  }
331
280
  //#endregion
332
- //#region lib/types/server/executor.js
333
- /**
334
- * Executor abstraction for inbound tasks: an executor turns one task into
335
- * output parts while reporting status and artifact updates. The registry is
336
- * INTERNAL (built by the plugin from config) — public registration was cut:
337
- * there is no external caller yet, so the seam stays an interface with two
338
- * built-in implementations (session, subagent).
339
- * @module dsh-a2a/server/executor
340
- */
341
- /**
342
- * Skill → executor resolution. The subagent executor is absent on
343
- * compositions without the subagent seam; resolving a skill bound to a
344
- * missing kind fails loudly with a readable message.
345
- */
346
- var ExecutorSet = class {
347
- bySkill;
348
- session;
349
- subagent;
350
- constructor(bySkill, session, subagent) {
351
- this.bySkill = bySkill;
352
- this.session = session;
353
- this.subagent = subagent;
354
- }
355
- resolve(skill) {
356
- if ((this.bySkill[skill] ?? "session") === "session") return this.session;
357
- const subagent = this.subagent;
358
- if (subagent === void 0) throw new Error(`a2a: skill "${skill}" is bound to the subagent executor but no subagent seam is mounted`);
359
- return subagent;
360
- }
361
- async disposeAll() {
362
- await this.session.disposeAll?.();
363
- await this.subagent?.disposeAll?.();
364
- }
365
- };
366
- //#endregion
367
281
  //#region lib/types/server/exec/agent-runtime.js
368
282
  /**
369
283
  * Shared agent machinery for the inbound executors: one DSH agent session per
@@ -406,7 +320,8 @@ var ContextSessionPool = class ContextSessionPool {
406
320
  let presetId;
407
321
  let setup;
408
322
  if (presets !== void 0) {
409
- 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;
410
325
  setup = async (agentCtx) => {
411
326
  await presets.mount(agentCtx, presetId);
412
327
  };
@@ -499,62 +414,328 @@ function probeService(ctx, name, method) {
499
414
  } catch {}
500
415
  }
501
416
  //#endregion
502
- //#region lib/types/server/exec/session.js
417
+ //#region lib/types/api.js
503
418
  /**
504
- * The `session` executor: one inbound task is one turn on the context's DSH
505
- * agent session; reply text becomes the task's output part. Works on any
506
- * composition with an agent loop (the `chat` skill and the default path).
507
- * @module dsh-a2a/server/exec/session
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
508
429
  */
509
- /** Build the session executor over a context session pool. */
510
- function createSessionExecutor(pool) {
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
+ });
437
+ }
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();
511
486
  return {
512
- name: "session",
513
- async execute(input) {
514
- if (!input.prompt) return { parts: [{ text: "No prompt provided." }] };
515
- const reply = await pool.runTurn(input.contextId, input.prompt, input.signal);
516
- return { parts: [{ text: (input.signal.aborted && !reply ? "(canceled)" : reply) || "(no reply)" }] };
517
- },
518
- disposeAll: () => pool.disposeAll()
487
+ inbounds: status.inbounds ?? [],
488
+ outbounds: status.outbounds ?? [],
489
+ tasks: impl.listTasks(),
490
+ peers: status.peers ?? impl.inbounds()
519
491
  };
520
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
+ }
521
537
  //#endregion
522
- //#region lib/types/server/exec/subagent.js
538
+ //#region lib/types/service.js
523
539
  /**
524
- * The `subagent` executor: one inbound task is delegated to a named one-shot
525
- * subagent under the context's parent session. The child runs with the
526
- * composition's own tools and the delegation boundary; its output becomes the
527
- * task's artifact. The subagent seam must be mounted, else the skill binding
528
- * fails loudly at resolution time.
529
- * @module dsh-a2a/server/exec/subagent
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
530
544
  */
531
- /** Build a subagent executor delegating inbound tasks to one-shot children. */
532
- function createSubagentExecutor(opts) {
533
- 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) {
534
615
  return {
535
- name: "subagent",
536
- async execute(input, execCtx) {
537
- if (!input.prompt) return { parts: [{ text: "No prompt provided." }] };
538
- const { agent } = await pool.agentFor(input.contextId);
539
- const text = (await (await subagents.start(provider, {
540
- label: `a2a:${input.skill}`,
541
- prompt: [{
542
- type: "text",
543
- text: input.prompt
544
- }],
545
- parent: agent,
546
- signal: input.signal
547
- })).result).output.filter((b) => typeof b.text === "string").map((b) => b.text).filter(Boolean).join("\n").trim();
548
- execCtx.onEvent({
549
- type: "artifact",
550
- artifactId: "result",
551
- parts: [{ text: text || "(subagent returned no output)" }],
552
- name: input.skill,
553
- lastChunk: true
554
- });
555
- return { parts: [{ text: text || "(subagent returned no output)" }] };
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"
556
721
  },
557
- 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
+ } : {}
558
739
  };
559
740
  }
560
741
  //#endregion
@@ -621,6 +802,14 @@ var A2AServer = class {
621
802
  this.opts = opts;
622
803
  this.card = opts.card;
623
804
  }
805
+ /** Swap the served AgentCard (runtime identity edits). Routes re-read `server.card` on every request. */
806
+ setCard(next) {
807
+ this.card = next;
808
+ }
809
+ /** The AgentCard route path for this instance. */
810
+ cardPath() {
811
+ return this.opts.cardPath ?? "/.well-known/agent-card.json";
812
+ }
624
813
  /**
625
814
  * Abort a running task by control path (facade /a2a task cancel): abort the
626
815
  * executor's signal, settle the task CANCELED, and wake stream waiters.
@@ -648,7 +837,7 @@ var A2AServer = class {
648
837
  /** Route one inbound HTTP request (GET card, POST JSON-RPC). */
649
838
  async handle(req, body) {
650
839
  const path = (req.url ?? "").split("?")[0] ?? "";
651
- if (req.method === "GET" && path === "/.well-known/agent-card.json") return {
840
+ if (req.method === "GET" && path === this.cardPath()) return {
652
841
  status: 200,
653
842
  contentType: "application/json",
654
843
  body: JSON.stringify(this.card)
@@ -661,7 +850,7 @@ var A2AServer = class {
661
850
  if (!this.authorized(req)) return {
662
851
  status: 401,
663
852
  contentType: "application/json",
664
- body: JSON.stringify(rpcError(null, A2A_ERROR_CODES.UNAUTHORIZED, "Unauthorized")),
853
+ body: JSON.stringify(rpcError(null, -32040, "Unauthorized")),
665
854
  headers: { "WWW-Authenticate": "Bearer" }
666
855
  };
667
856
  const rpc = parseRpc(body);
@@ -790,7 +979,6 @@ var A2AServer = class {
790
979
  const gateOutcome = await this.opts.gate(gateInputFrom(message, null));
791
980
  if (!gateOutcome.ok) throw rpcFault(A2A_ERROR_CODES.INVALID_PARAMS, gateOutcome.reason);
792
981
  const record = this.ensureTask(message, null);
793
- this.noteInbound({ method }, method, [record.taskId], false);
794
982
  await this.runTask(record);
795
983
  return toTask(this.opts.store.get(record.taskId) ?? record);
796
984
  }
@@ -1013,16 +1201,18 @@ function extractTaskIds(result) {
1013
1201
  * `@deepseek-ai/dsh-host-webserver`), with runtime enable/disable.
1014
1202
  * @module dsh-a2a/server/routes
1015
1203
  */
1016
- const CARD_PATH = "/.well-known/agent-card.json";
1204
+ const DEFAULT_CARD_PATH = "/.well-known/agent-card.json";
1017
1205
  /** Runtime-registered HTTP routes for one A2A server. */
1018
1206
  var A2aRoutes = class {
1019
1207
  webServer;
1020
1208
  server;
1209
+ cardPath;
1021
1210
  disposers = [];
1022
1211
  registered = false;
1023
- constructor(webServer, server) {
1212
+ constructor(webServer, server, cardPath = DEFAULT_CARD_PATH) {
1024
1213
  this.webServer = webServer;
1025
1214
  this.server = server;
1215
+ this.cardPath = cardPath;
1026
1216
  }
1027
1217
  get active() {
1028
1218
  return this.registered;
@@ -1032,7 +1222,7 @@ var A2aRoutes = class {
1032
1222
  if (this.registered) return;
1033
1223
  this.disposers.push(this.webServer.register({
1034
1224
  kind: "exact",
1035
- path: CARD_PATH,
1225
+ path: this.cardPath,
1036
1226
  handler: (_req, res) => {
1037
1227
  res.writeHead(200, { "content-type": "application/json" });
1038
1228
  res.end(JSON.stringify(this.server.card));
@@ -1057,7 +1247,7 @@ var A2aRoutes = class {
1057
1247
  const path = (req.url ?? "").split("?")[0] ?? "";
1058
1248
  const streaming = req.method === "POST" && (req.headers.accept ?? "").includes("text/event-stream");
1059
1249
  if (req.method === "POST" && path === endpointPathOf(this.server)) {
1060
- const body = await readBody$1(req);
1250
+ const body = await readBody(req);
1061
1251
  if (streaming) {
1062
1252
  if (!this.server.authorized(toServerReq(req))) {
1063
1253
  res.writeHead(401, {
@@ -1093,7 +1283,7 @@ var A2aRoutes = class {
1093
1283
  res.end(out.body);
1094
1284
  return;
1095
1285
  }
1096
- const out = await this.server.handle(toServerReq(req), path === CARD_PATH ? "" : "");
1286
+ const out = await this.server.handle(toServerReq(req), path === this.cardPath ? "" : "");
1097
1287
  res.writeHead(out.status, {
1098
1288
  "content-type": out.contentType,
1099
1289
  ...out.headers ?? {}
@@ -1110,7 +1300,7 @@ function endpointPathOf(server) {
1110
1300
  return "/a2a";
1111
1301
  }
1112
1302
  }
1113
- function readBody$1(req) {
1303
+ function readBody(req) {
1114
1304
  return new Promise((resolve, reject) => {
1115
1305
  const chunks = [];
1116
1306
  req.on("data", (chunk) => chunks.push(chunk));
@@ -1136,94 +1326,494 @@ function toServerReq(req) {
1136
1326
  };
1137
1327
  }
1138
1328
  //#endregion
1139
- //#region lib/types/api.js
1329
+ //#region lib/types/server/exec/session.js
1140
1330
  /**
1141
- * GUI control API for the A2A settings dashboard: one `GET /a2a/api`
1142
- * snapshot plus `POST /a2a/api` control actions (server toggle, outbound
1143
- * agent management, task view). The browser half talks only to this route;
1144
- * it carries no protocol knowledge. Loopback-only by default so the
1145
- * dashboard cannot be driven from the wire.
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
1335
+ */
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
+ };
1347
+ }
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
+ };
1386
+ }
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.
1146
1514
  *
1147
- * The route body is bound to the same `A2AServiceImpl` facade the `/a2a`
1148
- * command surface uses, so GUI actions and commands cannot disagree.
1149
- * @module dsh-a2a/api
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
1150
1520
  */
1151
- function readBody(req) {
1152
- return new Promise((resolve, reject) => {
1153
- const chunks = [];
1154
- req.on("data", (chunk) => chunks.push(chunk));
1155
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1156
- req.once("error", reject);
1157
- });
1521
+ function refuseExecutor(reason) {
1522
+ return {
1523
+ name: "refuse",
1524
+ async execute() {
1525
+ throw new Error(`a2a: ${reason}; refusing inbound task`);
1526
+ }
1527
+ };
1158
1528
  }
1159
- /** The caller is the local machine (never a remote peer). */
1160
- function isLoopback(req) {
1161
- const address = req.socket?.remoteAddress;
1162
- return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
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
+ }];
1163
1544
  }
1164
- function json(res, status, value) {
1165
- res.writeHead(status, {
1166
- "content-type": "application/json",
1167
- "cache-control": "no-cache"
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] } : {}
1168
1558
  });
1169
- 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
+ };
1170
1625
  }
1171
- /** Handle one dashboard API request against the service facade. */
1172
- async function handleApiRequest(req, res, impl) {
1173
- if (!isLoopback(req)) {
1174
- res.writeHead(403, { "content-type": "text/plain" });
1175
- res.end("forbidden");
1176
- return;
1626
+ /** Persisted-instance store over the inbound_servers table. */
1627
+ var DomainInboundStore = class DomainInboundStore {
1628
+ table;
1629
+ static KEY(id) {
1630
+ return `in:${id}`;
1177
1631
  }
1178
- if (req.method === "GET") {
1179
- 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 {
1180
1663
  return;
1181
1664
  }
1182
- if (req.method === "POST") {
1183
- 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;
1184
1694
  try {
1185
- payload = JSON.parse(await readBody(req));
1695
+ const resolved = await this.host.agentPresets.resolve(preset);
1696
+ return resolved.name ?? resolved.id;
1186
1697
  } catch {
1187
- json(res, 400, {
1188
- ok: false,
1189
- message: "invalid JSON body"
1190
- });
1191
1698
  return;
1192
1699
  }
1193
- const result = await dispatch(payload, impl);
1194
- json(res, result.ok ? 200 : 409, result);
1195
- return;
1196
1700
  }
1197
- res.writeHead(405, { "content-type": "text/plain" });
1198
- res.end("Method Not Allowed");
1199
- }
1200
- function snapshotOf(impl) {
1201
- return {
1202
- server: impl.status().server,
1203
- tasks: impl.listTasks(),
1204
- agents: impl.agents()
1205
- };
1206
- }
1207
- async function dispatch(payload, impl) {
1208
- switch (payload.action) {
1209
- case "server.enable": return impl.enableServer(true);
1210
- case "server.disable": return impl.enableServer(false);
1211
- case "agent.add": return impl.addAgent({
1212
- name: payload.name,
1213
- agentCardUrl: payload.agentCardUrl,
1214
- ...payload.bearerTokenEnv ? { bearerTokenEnv: payload.bearerTokenEnv } : {}
1215
- });
1216
- case "agent.remove": return impl.removeAgent(payload.id);
1217
- case "agent.enable": return impl.setAgentEnabled(payload.id, true);
1218
- case "agent.disable": return impl.setAgentEnabled(payload.id, false);
1219
- case "agent.refresh": return impl.refreshAgentCard(payload.id);
1220
- case "task.cancel": return impl.cancelTask(payload.id);
1221
- default: return {
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 {
1222
1778
  ok: false,
1223
- 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"}`
1224
1791
  };
1225
1792
  }
1226
- }
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
+ };
1227
1817
  //#endregion
1228
1818
  //#region lib/types/outbound/calls.js
1229
1819
  /**
@@ -1262,11 +1852,11 @@ var A2AClient = class A2AClient {
1262
1852
  },
1263
1853
  signal: AbortSignal.timeout(timeoutMs)
1264
1854
  });
1265
- 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}`);
1266
1856
  const card = await response.json();
1267
- if (typeof card.name !== "string" || typeof card.description !== "string") throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_SIGNATURE_INVALID, `invalid agent card at ${agentCardUrl}`);
1268
- const endpoint = (card.supportedInterfaces?.find((i) => (i.protocolBinding ?? "JSONRPC") === "JSONRPC") ?? card.supportedInterfaces?.[0])?.url ?? card.url;
1269
- if (!endpoint) throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_NOT_FOUND, `agent card at ${agentCardUrl} advertises no JSON-RPC interface`);
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`);
1270
1860
  return new A2AClient(card, endpoint, opts);
1271
1861
  }
1272
1862
  /** Send a message and wait (or poll) until the task settles. */
@@ -1319,7 +1909,7 @@ var A2AClient = class A2AClient {
1319
1909
  if (err instanceof Error && err.name === "TimeoutError") throw new A2AError(-32e3, `A2A call ${method} timed out after ${timeoutMs}ms`);
1320
1910
  throw err;
1321
1911
  }
1322
- 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)");
1323
1913
  if (!response.ok) throw new A2AError(A2A_ERROR_CODES.INTERNAL_ERROR, `A2A call ${method} failed: HTTP ${response.status}`);
1324
1914
  const payload = await response.json();
1325
1915
  if ("error" in payload) {
@@ -1413,27 +2003,6 @@ function textOf(artifacts, state, statusParts) {
1413
2003
  * connection. Loaded at boot, mutations persist through the agent store.
1414
2004
  * @module dsh-a2a/client/registry
1415
2005
  */
1416
- /** Domain-backed agent store: one JSON array under the `agents` table key. */
1417
- var DomainAgentStore = class DomainAgentStore {
1418
- table;
1419
- static KEY = "records";
1420
- constructor(table) {
1421
- this.table = table;
1422
- }
1423
- list() {
1424
- const raw = this.table.get(DomainAgentStore.KEY);
1425
- if (raw === void 0) return [];
1426
- try {
1427
- const parsed = JSON.parse(raw);
1428
- return Array.isArray(parsed) ? parsed : [];
1429
- } catch {
1430
- return [];
1431
- }
1432
- }
1433
- async save(records) {
1434
- await this.table.put(DomainAgentStore.KEY, JSON.stringify(records));
1435
- }
1436
- };
1437
2006
  /** A list of remote agents, each with live tool registrations when enabled. */
1438
2007
  var OutboundAgentRegistry = class {
1439
2008
  opts;
@@ -1664,150 +2233,265 @@ var OutboundAgentRegistry = class {
1664
2233
  }
1665
2234
  };
1666
2235
  //#endregion
1667
- //#region lib/types/service.js
2236
+ //#region lib/types/servers/outbound-manager.js
1668
2237
  /**
1669
- * The `ctx.a2a` service facade: the thin read/control surface commands and
1670
- * other plugins use. Registered as a Cordis Service so the key disappears
1671
- * with the plugin fiber.
1672
- * @module dsh-a2a/service
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
1673
2253
  */
1674
- /** The service other plugins read as `ctx.a2a`. */
1675
- var A2AService = class extends Service {
1676
- impl;
1677
- constructor(ctx, impl) {
1678
- super(ctx, "a2a");
1679
- this.impl = impl;
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;
1680
2261
  }
1681
- status() {
1682
- 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
+ }
1683
2271
  }
1684
- async enableServer(enable) {
1685
- return this.impl.enableServer(enable);
2272
+ async save(records) {
2273
+ await this.table.put(this.key, JSON.stringify(records));
1686
2274
  }
1687
- getTask(taskId) {
1688
- 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}`;
1689
2281
  }
1690
- listTasks() {
1691
- return this.impl.listTasks();
2282
+ constructor(table) {
2283
+ this.table = table;
1692
2284
  }
1693
- async cancelTask(taskId) {
1694
- 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;
1695
2295
  }
1696
- agents() {
1697
- 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);
1698
2299
  }
1699
- async addAgent(spec) {
1700
- return this.impl.addAgent(spec);
2300
+ async save(record) {
2301
+ await this.table.put(DomainOutboundStore.KEY(record.id), JSON.stringify(record));
1701
2302
  }
1702
- async removeAgent(id) {
1703
- return this.impl.removeAgent(id);
2303
+ async remove(id) {
2304
+ await this.table.put(DomainOutboundStore.KEY(id), "");
1704
2305
  }
1705
- async setAgentEnabled(id, enabled) {
1706
- 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;
1707
2314
  }
1708
- async refreshAgentCard(id) {
1709
- 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
+ });
1710
2345
  }
1711
- };
1712
- //#endregion
1713
- //#region lib/types/commands.js
1714
- /**
1715
- * The `/a2a` command surface: status, server enable/disable, card, agents,
1716
- * and task control the P0 operations surface (the settings panel is P1).
1717
- * @module dsh-a2a/commands
1718
- */
1719
- /** Build the `/a2a` command definition over the service facade. */
1720
- function buildA2aCommand(impl) {
1721
- return {
1722
- name: "a2a",
1723
- description: "Manage the A2A plugin: status, server, agents, tasks, card.",
1724
- handler: async (invocation) => {
1725
- const [verb, ...rest] = invocation.rawInput.trim().split(/\s+/);
1726
- switch (verb ?? "") {
1727
- case "": {
1728
- const status = impl.status();
1729
- return success(`A2A status\n${JSON.stringify(status, null, 2)}`);
1730
- }
1731
- case "status": return success(JSON.stringify(impl.status(), null, 2));
1732
- case "enable": return outcome(await impl.enableServer(true));
1733
- case "disable": return outcome(await impl.enableServer(false));
1734
- case "card": return success(JSON.stringify(impl.listTasks(), null, 2).slice(0, 4e3));
1735
- case "agents": return success(JSON.stringify(impl.agents(), null, 2));
1736
- case "agent": {
1737
- const [action, ...args] = rest;
1738
- switch (action ?? "") {
1739
- case "add": {
1740
- const url = args[0];
1741
- if (!url) return error("usage: /a2a agent add <agentCardUrl> [name]");
1742
- const name = args[1] ?? url;
1743
- return outcome(await impl.addAgent({
1744
- name,
1745
- agentCardUrl: url
1746
- }));
1747
- }
1748
- case "remove": {
1749
- const id = args[0];
1750
- if (!id) return error("usage: /a2a agent remove <id>");
1751
- return outcome(await impl.removeAgent(id));
1752
- }
1753
- case "enable": {
1754
- const id = args[0];
1755
- if (!id) return error("usage: /a2a agent enable <id>");
1756
- return outcome(await impl.setAgentEnabled(id, true));
1757
- }
1758
- case "disable": {
1759
- const id = args[0];
1760
- if (!id) return error("usage: /a2a agent disable <id>");
1761
- return outcome(await impl.setAgentEnabled(id, false));
1762
- }
1763
- case "refresh": {
1764
- const id = args[0];
1765
- if (!id) return error("usage: /a2a agent refresh <id>");
1766
- return outcome(await impl.refreshAgentCard(id));
1767
- }
1768
- default: return error("usage: /a2a agent add|remove|enable|disable|refresh ...");
1769
- }
1770
- }
1771
- case "tasks": return success(JSON.stringify(impl.listTasks(), null, 2));
1772
- case "task": {
1773
- const [action, id] = rest;
1774
- if (action === "get" && id) return success(JSON.stringify(impl.getTask(id), null, 2));
1775
- if (action === "cancel" && id) return outcome(await impl.cancelTask(id));
1776
- return error("usage: /a2a task get|cancel <taskId>");
1777
- }
1778
- case "help": return success("a2a commands: status | enable | disable | card | agents | agent add/remove/enable/disable/refresh | tasks | task get/cancel <id> | help");
1779
- default: return error(`unknown a2a verb "${verb}" (try /a2a help)`);
1780
- }
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
+ };
2355
+ }
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)}`);
1781
2364
  }
1782
- };
1783
- }
1784
- function success(text) {
1785
- return {
1786
- kind: "success",
1787
- text
1788
- };
1789
- }
1790
- function error(text) {
1791
- return {
1792
- kind: "error",
1793
- text
1794
- };
1795
- }
1796
- function outcome(result) {
1797
- return result.ok ? success(result.message) : error(result.message);
1798
- }
2365
+ }
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);
2369
+ }
2370
+ get(id) {
2371
+ const registry = this.registries.get(id);
2372
+ return registry === void 0 ? void 0 : {
2373
+ id,
2374
+ registry
2375
+ };
2376
+ }
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;
2419
+ }
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
+ };
1799
2478
  //#endregion
1800
2479
  //#region lib/types/index.js
1801
2480
  /**
1802
2481
  * @hanphone/dsh-a2a — Cordis plugin entry.
1803
2482
  *
1804
- * Mounts the A2A v1.0 dual-end plugin: the inbound server (AgentCard derived
1805
- * from the live tool registry, JSON-RPC + SSE, durable SQLite task store,
1806
- * session/subagent executors, the `a2a/inbound-task` policy gate + audit)
1807
- * and the outbound client (persisted multi-agent registry, skills mapped to
1808
- * model tools, sync calls with per-agent timeout). Optional services are
1809
- * probed rather than injected so a composition lacking one half idles just
1810
- * that half; only the storage domain is required.
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.
1811
2495
  *
1812
2496
  * Function-plugin export shape: named `name`/`inject`/`Config`/`apply`, no
1813
2497
  * default export (mixing forms makes the Loader drop the namespace).
@@ -1818,245 +2502,197 @@ const name = "a2a";
1818
2502
  const inject = ["storageDomain"];
1819
2503
  /** Loader-validated config schema (defaults applied by the Loader). */
1820
2504
  const Config = z.object({
1821
- server: z.object({
1822
- enabled: z.boolean().default(true),
1823
- name: z.string().default("My DSH Agent"),
1824
- description: z.string().default("A DeepSeek Harness agent exposed over A2A v1.0"),
1825
- version: z.string().default("0.1.0"),
1826
- baseUrl: z.string(),
1827
- endpointPath: z.string().default("/a2a"),
1828
- authTokenEnv: z.string(),
1829
- skills: z.object({
1830
- ids: z.array(z.string()).default([]),
1831
- exclude: z.array(z.string()).default([])
1832
- }),
1833
- executors: z.dict(z.union(["session", "subagent"])).default({ chat: "session" }),
1834
- subagentProvider: z.string().default("in-process")
1835
- }),
1836
- client: z.object({
1837
- agents: z.array(z.object({
1838
- name: z.string(),
1839
- agentCardUrl: z.string(),
1840
- bearerTokenEnv: z.string(),
1841
- enabled: z.boolean().default(true),
1842
- timeoutMs: z.number().default(6e4)
1843
- })).default([]),
1844
- toolPrefix: z.string().default("a2a")
1845
- })
2505
+ baseUrl: z.string(),
2506
+ subagentProvider: z.string().default("in-process"),
2507
+ defaultTimeoutMs: z.number().default(6e4)
1846
2508
  });
1847
2509
  function apply(ctx, config) {
1848
2510
  const logger = ctx.logger("a2a");
1849
- const serverConfig = { ...config.server };
1850
- const clientConfig = { ...config.client };
1851
- const authToken = serverConfig.authTokenEnv !== void 0 ? process.env[serverConfig.authTokenEnv] : void 0;
1852
- const toolPrefix = clientConfig.toolPrefix;
1853
- const subagentProvider = serverConfig.subagentProvider;
2511
+ const subagentProvider = config.subagentProvider;
1854
2512
  const inboundCwd = inboundSessionCwd();
1855
2513
  ctx.inject(["storageDomain"], (ctx) => {
1856
2514
  ctx.effect(async () => {
1857
2515
  const domain = await openDomain(ctx.storageDomain);
1858
2516
  const store = new DomainTaskStore(domain);
1859
- 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({
1860
2547
  domain,
1861
2548
  store,
1862
- registry: void 0,
1863
- server: void 0,
1864
- routes: void 0,
1865
- card: void 0,
1866
- executors: void 0,
1867
- enabled: serverConfig.enabled
1868
- };
1869
- const webServer = probeService(ctx, "webServer", "register");
1870
- if (webServer === void 0) logger.warn("a2a: webServer not mounted; inbound server idle");
1871
- else try {
1872
- const skills = deriveSkills(probeService(ctx, "tools", "get") ?? { get: () => void 0 }, {
1873
- ids: serverConfig.skills.ids,
1874
- exclude: serverConfig.skills.exclude
1875
- });
1876
- const card = buildCard({
1877
- baseUrl: serverConfig.baseUrl ?? `http://127.0.0.1:${process.env["DSH_A2A_PORT"] ?? "3000"}`,
1878
- endpointPath: serverConfig.endpointPath ?? "/a2a",
1879
- name: serverConfig.name ?? "My DSH Agent",
1880
- description: serverConfig.description ?? "A DeepSeek Harness agent exposed over A2A v1.0",
1881
- version: serverConfig.version ?? "0.1.0",
1882
- skills,
1883
- ...authToken ? { authToken } : {}
1884
- });
1885
- holder.card = card;
1886
- const agents = probeService(ctx, "agents", "create");
1887
- const presets = probeService(ctx, "agentPresets", "resolve");
1888
- const sessionPool = agents ? new ContextSessionPool(agents, {
1889
- cwd: inboundCwd,
1890
- ...presets ? { agentPresets: presets } : {},
1891
- resolveAgentOptions: () => resolveDefaultModel(ctx)
1892
- }) : void 0;
1893
- const sessionExecutor = sessionPool ? createSessionExecutor(sessionPool) : refuseExecutor("no agent loop mounted");
1894
- const subagents = probeService(ctx, "subagents", "start");
1895
- const subagentExecutor = sessionPool !== void 0 && subagents !== void 0 ? createSubagentExecutor({
1896
- pool: sessionPool,
1897
- subagents,
1898
- provider: subagentProvider
1899
- }) : void 0;
1900
- const executors = new ExecutorSet(serverConfig.executors, sessionExecutor, subagentExecutor);
1901
- holder.executors = executors;
1902
- const skillIds = new Set(skills.map((s) => s.id));
1903
- const gate = async (input) => {
1904
- if (!skillIds.has(input.skill)) return {
1905
- ok: false,
1906
- reason: `unknown skill "${input.skill}"`
1907
- };
1908
- const decision = {
1909
- contextId: input.contextId,
1910
- skill: input.skill,
1911
- parts: input.parts,
1912
- remotePeerId: input.remotePeerId
1913
- };
1914
- const decided = await ctx.waterfall("a2a/inbound-task", decision, async (d) => d);
1915
- logger.info(`[a2a] inbound skill=${decided.skill} remote=${decided.remotePeerId} rejected=${decided.rejected?.reason ?? "no"}`);
1916
- if (decided.rejected !== void 0) return {
1917
- ok: false,
1918
- reason: decided.rejected.reason
1919
- };
1920
- return { ok: true };
1921
- };
1922
- const server = new A2AServer({
1923
- card,
1924
- store,
1925
- executors,
1926
- ...authToken ? { authToken } : {},
1927
- gate,
1928
- onTaskSettled: (taskId) => logger.info(`[a2a] task settled ${taskId}`)
1929
- });
1930
- holder.server = server;
1931
- const routes = new A2aRoutes(webServer, server);
1932
- holder.routes = routes;
1933
- if (holder.enabled) routes.enable();
1934
- } catch (err) {
1935
- logger.error(`a2a: server half failed to build: ${err.message}`);
1936
- }
1937
- const toolsService = probeService(ctx, "tools", "get");
1938
- if (toolsService === void 0) logger.warn("a2a: tools service not mounted; outbound client idle");
1939
- else {
1940
- const registry = new OutboundAgentRegistry({
1941
- registrar: { register: (def) => toolsService.register(def) },
1942
- store: new DomainAgentStore(domain.agents),
1943
- toolPrefix,
1944
- tokenOf: (env) => env ? process.env[env] : void 0,
1945
- onError: (message) => logger.warn(message)
1946
- });
1947
- holder.registry = registry;
1948
- registry.loadAll();
1949
- if (clientConfig.agents.length > 0) await registry.seed(clientConfig.agents);
1950
- }
1951
- const impl = makeFacade(holder, logger.info.bind(logger));
2549
+ inboundManager,
2550
+ outboundManager,
2551
+ agentPresets,
2552
+ logger: logger.info.bind(logger)
2553
+ });
1952
2554
  new A2AService(ctx, impl);
1953
- const commands = probeService(ctx, "commands", "register");
1954
- if (commands !== void 0) ctx.effect(() => commands.register(buildA2aCommand(impl)), "a2a: command");
1955
- const dashboardWebServer = probeService(ctx, "webServer", "register");
1956
- if (dashboardWebServer !== void 0) ctx.effect(() => dashboardWebServer.register({
2555
+ if (commandsRef !== void 0) ctx.effect(() => commandsRef.register(buildA2aCommand(impl)), "a2a: command");
2556
+ if (webServer !== void 0) ctx.effect(() => webServer.register({
1957
2557
  kind: "prefix",
1958
2558
  path: "/a2a/api",
1959
2559
  handler: (req, res) => handleApiRequest(req, res, impl)
1960
2560
  }), "a2a: dashboard api");
1961
2561
  else logger.warn("a2a: webServer not mounted; GUI dashboard API idle");
1962
2562
  return async () => {
1963
- holder.routes?.dispose();
1964
- await holder.executors?.disposeAll();
1965
- await holder.registry?.disposeAll();
2563
+ inboundManager.disposeAll();
2564
+ await outboundManager.disposeAll();
1966
2565
  await domain.close();
1967
2566
  };
1968
- }, "a2a: domain + halves");
2567
+ }, "a2a: multi-instance composition");
1969
2568
  });
1970
2569
  }
1971
- /** Facade closure over the shared holder (commands and `ctx.a2a` consumers). */
1972
- function makeFacade(holder, log) {
1973
- 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
+ };
1974
2594
  return {
1975
2595
  status() {
1976
2596
  return {
1977
- server: {
1978
- enabled: serverEnabled(),
1979
- cardUrl: holder.card?.supportedInterfaces?.[0]?.url,
1980
- skills: holder.card?.skills?.map((s) => s.id) ?? [],
1981
- executors: holder.executors ? ["session", ...holder.executors["subagent"] !== void 0 ? ["subagent"] : []] : []
1982
- },
1983
- tasks: holder.store.list().length,
1984
- agents: holder.registry?.list() ?? []
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
1985
2600
  };
1986
2601
  },
1987
- async enableServer(enable) {
1988
- if (holder.routes === void 0) return {
1989
- ok: false,
1990
- message: "inbound server is not mounted (no webServer)"
1991
- };
1992
- if (enable) holder.routes.enable();
1993
- else holder.routes.disable();
1994
- log(`[a2a] server ${enable ? "enabled" : "disabled"}`);
1995
- return {
1996
- ok: true,
1997
- message: `server ${enable ? "enabled" : "disabled"}`
1998
- };
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
+ }
1999
2614
  },
2000
- getTask(taskId) {
2001
- return holder.store.get(taskId);
2615
+ listInboundServers() {
2616
+ return host.inboundManager.list().map((live) => inboundView(live.id)).filter((v) => v !== void 0);
2002
2617
  },
2003
- listTasks() {
2004
- 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;
2005
2631
  },
2006
- async cancelTask(taskId) {
2007
- if (holder.store.get(taskId) === void 0) return {
2008
- ok: false,
2009
- message: `task ${taskId} not found`
2010
- };
2011
- return {
2012
- ok: true,
2013
- message: holder.server?.abort?.(taskId) ?? false ? `task ${taskId} canceled` : `task ${taskId} already terminal`
2014
- };
2632
+ async removeInboundServer(id) {
2633
+ return host.inboundManager.remove(id);
2015
2634
  },
2016
- agents() {
2017
- return holder.registry?.list() ?? [];
2635
+ async setInboundServerEnabled(id, enabled) {
2636
+ return host.inboundManager.setEnabled(id, enabled);
2018
2637
  },
2019
- async addAgent(spec) {
2020
- const registry = holder.registry;
2021
- if (registry === void 0) return {
2022
- ok: false,
2023
- message: "outbound client not mounted (no tools service)"
2024
- };
2025
- return await registry.add({
2026
- ...spec,
2027
- enabled: true,
2028
- timeoutMs: 6e4
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
2029
2653
  });
2030
2654
  },
2031
- async removeAgent(id) {
2032
- return holder.registry?.remove(id) ?? {
2033
- ok: false,
2034
- message: "outbound client not mounted"
2035
- };
2655
+ async removeOutboundServer(id) {
2656
+ return host.outboundManager.remove(id);
2036
2657
  },
2037
- async setAgentEnabled(id, enabled) {
2038
- return holder.registry?.setEnabled(id, enabled) ?? {
2039
- ok: false,
2040
- message: "outbound client not mounted"
2658
+ async setOutboundServerEnabled(id, enabled) {
2659
+ return host.outboundManager.setEnabled(id, enabled);
2660
+ },
2661
+ async refreshOutboundServer(id) {
2662
+ return host.outboundManager.refresh(id);
2663
+ },
2664
+ getTask(taskId) {
2665
+ return host.store.get(taskId);
2666
+ },
2667
+ listTasks() {
2668
+ return host.store.list();
2669
+ },
2670
+ async cancelTask(taskId) {
2671
+ const live = host.inboundManager.list().find((l) => l.server.abort(taskId));
2672
+ return {
2673
+ ok: live !== void 0,
2674
+ message: live !== void 0 ? `task ${taskId} canceled` : `task ${taskId} not found or already terminal`
2041
2675
  };
2042
2676
  },
2043
- async refreshAgentCard(id) {
2044
- return holder.registry?.refresh(id) ?? {
2677
+ inbounds() {
2678
+ return host.inboundManager.list().flatMap((live) => live.inbound.list());
2679
+ },
2680
+ async closeInbound(peerId) {
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 {
2045
2690
  ok: false,
2046
- message: "outbound client not mounted"
2691
+ message: `inbound peer ${peerId} not found`
2047
2692
  };
2048
2693
  }
2049
2694
  };
2050
2695
  }
2051
- /** A task that refuses readably on compositions without an agent loop. */
2052
- function refuseExecutor(reason) {
2053
- return {
2054
- name: "refuse",
2055
- async execute() {
2056
- throw new Error(`a2a: ${reason}; refusing inbound task`);
2057
- }
2058
- };
2059
- }
2060
2696
  function resolveDefaultModel(ctx) {
2061
2697
  return probeService(ctx, "agentDefaultModel", "currentSelection")?.currentSelection();
2062
2698
  }
@@ -2069,5 +2705,15 @@ function inboundSessionCwd() {
2069
2705
  } catch {}
2070
2706
  return dir;
2071
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
+ }
2072
2718
  //#endregion
2073
2719
  export { A2AClient, A2AError, A2AServer, A2AService, Config, apply, inject, name };