@ryubyte/dsh-a2a 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,1896 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ //#region src/protocol.ts
5
+ /**
6
+ * A2A (Agent2Agent) Protocol v1.0 — TypeScript type definitions.
7
+ *
8
+ * These types mirror the normative source of truth `specification/a2a.proto`
9
+ * (package `lf.a2a.v1`) from https://github.com/a2aproject/A2A, v1.0.0.
10
+ * JSON wire names are camelCase per ProtoJSON serialization; enum values use
11
+ * SCREAMING_SNAKE_CASE as mandated by A2A v1.0.
12
+ */
13
+ /** The lifecycle states of a Task (a2a.proto `TaskState`). */
14
+ let TaskState = /* @__PURE__ */ function(TaskState) {
15
+ TaskState["UNSPECIFIED"] = "TASK_STATE_UNSPECIFIED";
16
+ TaskState["SUBMITTED"] = "TASK_STATE_SUBMITTED";
17
+ TaskState["WORKING"] = "TASK_STATE_WORKING";
18
+ TaskState["COMPLETED"] = "TASK_STATE_COMPLETED";
19
+ TaskState["FAILED"] = "TASK_STATE_FAILED";
20
+ TaskState["CANCELED"] = "TASK_STATE_CANCELED";
21
+ TaskState["INPUT_REQUIRED"] = "TASK_STATE_INPUT_REQUIRED";
22
+ TaskState["REJECTED"] = "TASK_STATE_REJECTED";
23
+ TaskState["AUTH_REQUIRED"] = "TASK_STATE_AUTH_REQUIRED";
24
+ return TaskState;
25
+ }({});
26
+ function isTerminal(state) {
27
+ return state === "TASK_STATE_COMPLETED" || state === "TASK_STATE_FAILED" || state === "TASK_STATE_CANCELED" || state === "TASK_STATE_REJECTED";
28
+ }
29
+ /** The sender of a Message (a2a.proto `Role`). */
30
+ let Role = /* @__PURE__ */ function(Role) {
31
+ Role["UNSPECIFIED"] = "ROLE_UNSPECIFIED";
32
+ Role["USER"] = "ROLE_USER";
33
+ Role["AGENT"] = "ROLE_AGENT";
34
+ return Role;
35
+ }({});
36
+ /** A2A JSON-RPC method names (v1.0 canonical PascalCase as in a2a.proto RPCs). */
37
+ const A2A_METHODS = {
38
+ sendMessage: "SendMessage",
39
+ sendStreamingMessage: "SendStreamingMessage",
40
+ getTask: "GetTask",
41
+ listTasks: "ListTasks",
42
+ cancelTask: "CancelTask",
43
+ subscribeToTask: "SubscribeToTask",
44
+ getExtendedAgentCard: "GetExtendedAgentCard"
45
+ };
46
+ /** Standard error codes from the A2A JSON-RPC binding. */
47
+ const A2A_ERROR_CODES = {
48
+ INVALID_REQUEST: -32600,
49
+ METHOD_NOT_FOUND: -32601,
50
+ INVALID_PARAMS: -32602,
51
+ INTERNAL_ERROR: -32603,
52
+ TASK_NOT_FOUND: -32001,
53
+ TASK_CANCEL_NOT_ALLOWED: -32002,
54
+ TASK_ALREADY_CANCELED: -32003,
55
+ AGENT_CARD_NOT_FOUND: -32004,
56
+ AGENT_CARD_SIGNATURE_INVALID: -32005,
57
+ CONTEXT_NOT_FOUND: -32006,
58
+ AUTHENTICATION_REQUIRED: -32007,
59
+ PUSH_NOTIFICATION_CONFIG_NOT_FOUND: -32008,
60
+ EXTENSION_NOT_SUPPORTED: -32009,
61
+ UNSUPPORTED_OPERATION: -32010,
62
+ VERSION_NOT_SUPPORTED: -32011
63
+ };
64
+ //#endregion
65
+ //#region src/errors.ts
66
+ /** Error class for A2A protocol failures, mirroring JSON-RPC error semantics. */
67
+ var A2AError = class A2AError extends Error {
68
+ code;
69
+ data;
70
+ /** True when this error came from a remote agent over the wire. */
71
+ remote;
72
+ constructor(code, message, data, remote = false) {
73
+ super(message);
74
+ this.name = "A2AError";
75
+ this.code = code;
76
+ this.data = data;
77
+ this.remote = remote;
78
+ }
79
+ toJsonRpc() {
80
+ const out = {
81
+ code: this.code,
82
+ message: this.message
83
+ };
84
+ if (this.data !== void 0) out.data = this.data;
85
+ return out;
86
+ }
87
+ static fromJsonRpc(err) {
88
+ return new A2AError(err.code, err.message, err.data, true);
89
+ }
90
+ };
91
+ //#endregion
92
+ //#region src/card.ts
93
+ function isAgentCard(value) {
94
+ if (typeof value !== "object" || value === null) return false;
95
+ const v = value;
96
+ return typeof v.name === "string" && typeof v.description === "string" && typeof v.version === "string" && Array.isArray(v.supportedInterfaces) && typeof v.capabilities === "object" && v.capabilities !== null && Array.isArray(v.defaultInputModes) && Array.isArray(v.defaultOutputModes) && Array.isArray(v.skills);
97
+ }
98
+ function normalizeUrl(base, path) {
99
+ return `${base.endsWith("/") ? base.slice(0, -1) : base}${path.startsWith("/") ? path : `/${path}`}`;
100
+ }
101
+ /**
102
+ * Fetch and validate an agent's AgentCard.
103
+ *
104
+ * Discovery order (per spec §AgentCard discovery):
105
+ * 1. Probe `GET {origin}/.well-known/agent-card.json` first.
106
+ * 2. Fall back to the exact configured URL.
107
+ * A configured URL that already ends in `agent-card.json` is used directly.
108
+ */
109
+ async function fetchAgentCard(baseUrl, options = {}) {
110
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
111
+ const timeoutMs = options.timeoutMs ?? 15e3;
112
+ const headers = { Accept: "application/json" };
113
+ if (options.bearerToken) headers.Authorization = `Bearer ${options.bearerToken}`;
114
+ const wellKnown = normalizeUrl(baseUrl, "/.well-known/agent-card.json");
115
+ const candidates = baseUrl.endsWith("agent-card.json") || baseUrl.endsWith("agent-card.json/") ? [baseUrl] : [wellKnown, baseUrl];
116
+ let lastError;
117
+ for (const url of candidates) {
118
+ const ctrl = new AbortController();
119
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
120
+ try {
121
+ const res = await fetchImpl(url, {
122
+ headers,
123
+ signal: ctrl.signal
124
+ });
125
+ if (res.status === 404) {
126
+ lastError = new A2AError(404, `AgentCard not found at ${url}`, void 0, true);
127
+ continue;
128
+ }
129
+ if (!res.ok) throw new A2AError(res.status, `AgentCard request failed (${res.status}) at ${url}`, void 0, true);
130
+ const json = await res.json();
131
+ if (!isAgentCard(json)) throw new A2AError(400, `Invalid AgentCard at ${url}: missing required fields`, void 0, true);
132
+ return json;
133
+ } catch (err) {
134
+ if (err instanceof Error && err.name === "AbortError") throw new A2AError(408, `AgentCard fetch timed out after ${timeoutMs}ms at ${url}`, void 0, true);
135
+ lastError = err instanceof Error ? err : new Error(String(err));
136
+ } finally {
137
+ clearTimeout(timer);
138
+ }
139
+ }
140
+ throw lastError ?? new A2AError(404, `AgentCard could not be fetched from ${baseUrl}`, void 0, true);
141
+ }
142
+ /**
143
+ * Pick the best interface to talk to an agent from its AgentCard.
144
+ *
145
+ * Preference order: a JSONRPC interface first, then gRPC, then any other
146
+ * binding; the card's own ordering ("first entry is preferred") is respected
147
+ * within each binding class. Returns the interface and a normalized base URL
148
+ * to post JSON-RPC requests to.
149
+ */
150
+ function pickInterface(card, preferred) {
151
+ const ifaces = card.supportedInterfaces ?? [];
152
+ if (ifaces.length === 0) throw new A2AError(-32e3, `AgentCard for "${card.name}" declares no supportedInterfaces`);
153
+ const want = preferred ?? "JSONRPC";
154
+ const iface = [...ifaces].sort((a, b) => {
155
+ return (a.protocolBinding === want ? 0 : a.protocolBinding === "GRPC" ? 1 : 2) - (b.protocolBinding === want ? 0 : b.protocolBinding === "GRPC" ? 1 : 2);
156
+ })[0];
157
+ return {
158
+ iface,
159
+ url: iface.url.endsWith("/") ? iface.url.slice(0, -1) : iface.url
160
+ };
161
+ }
162
+ //#endregion
163
+ //#region src/jsonrpc.ts
164
+ var JsonRpcClient = class {
165
+ url;
166
+ timeoutMs;
167
+ bearerToken;
168
+ fetchImpl;
169
+ seq = 0;
170
+ constructor(url, options = {}) {
171
+ this.url = url;
172
+ this.timeoutMs = options.timeoutMs ?? 6e4;
173
+ this.bearerToken = options.bearerToken;
174
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
175
+ }
176
+ headers(extra) {
177
+ const h = {
178
+ "content-type": "application/json",
179
+ accept: "application/json",
180
+ ...extra
181
+ };
182
+ if (this.bearerToken) h.authorization = `Bearer ${this.bearerToken}`;
183
+ return h;
184
+ }
185
+ /** Perform a unary JSON-RPC call. */
186
+ async call(method, params, options = {}) {
187
+ const req = {
188
+ jsonrpc: "2.0",
189
+ id: ++this.seq,
190
+ method,
191
+ params
192
+ };
193
+ const ctrl = options.signal ? new AbortController() : void 0;
194
+ const onAbort = () => ctrl?.abort();
195
+ options.signal?.addEventListener("abort", onAbort);
196
+ const timeout = setTimeout(() => ctrl?.abort(), options.timeoutMs ?? this.timeoutMs);
197
+ try {
198
+ const res = await this.fetchImpl(this.url, {
199
+ method: "POST",
200
+ headers: this.headers(),
201
+ body: JSON.stringify(req),
202
+ signal: ctrl?.signal
203
+ });
204
+ if (!res.ok) throw new A2AError(res.status, `A2A JSON-RPC request failed (HTTP ${res.status}) at ${this.url}`, void 0, true);
205
+ const json = await res.json();
206
+ if ("error" in json && json.error) throw A2AError.fromJsonRpc(json.error);
207
+ return json.result;
208
+ } catch (err) {
209
+ if (err instanceof Error && err.name === "AbortError") throw new A2AError(408, `A2A JSON-RPC call "${method}" timed out`, void 0, true);
210
+ throw err;
211
+ } finally {
212
+ clearTimeout(timeout);
213
+ options.signal?.removeEventListener("abort", onAbort);
214
+ }
215
+ }
216
+ /**
217
+ * Open a streaming JSON-RPC call (`SendStreamingMessage`) over SSE and
218
+ * yield the `StreamResponse` events as they arrive. The connection is
219
+ * closed when the returned async iterator is broken or the request is
220
+ * cancelled. Events are decoded from SSE `data:` lines (JSON-RPC result
221
+ * frames); per A2A v1.0 the JSON structure uses member names to
222
+ * discriminate `task` / `message` / `statusUpdate` / `artifactUpdate`.
223
+ */
224
+ async *stream(method, params, options = {}) {
225
+ const req = {
226
+ jsonrpc: "2.0",
227
+ id: ++this.seq,
228
+ method,
229
+ params
230
+ };
231
+ const ctrl = new AbortController();
232
+ const onAbort = () => ctrl.abort();
233
+ options.signal?.addEventListener("abort", onAbort);
234
+ const timeout = setTimeout(() => ctrl.abort(), options.timeoutMs ?? this.timeoutMs);
235
+ let res;
236
+ try {
237
+ res = await this.fetchImpl(this.url, {
238
+ method: "POST",
239
+ headers: this.headers({ accept: "text/event-stream" }),
240
+ body: JSON.stringify(req),
241
+ signal: ctrl.signal
242
+ });
243
+ } catch (err) {
244
+ clearTimeout(timeout);
245
+ options.signal?.removeEventListener("abort", onAbort);
246
+ if (err instanceof Error && err.name === "AbortError") throw new A2AError(408, `A2A stream "${method}" timed out`, void 0, true);
247
+ throw err;
248
+ }
249
+ if (!res.ok || !res.body) {
250
+ clearTimeout(timeout);
251
+ options.signal?.removeEventListener("abort", onAbort);
252
+ throw new A2AError(res.status, `A2A stream request failed (HTTP ${res.status}) at ${this.url}`, void 0, true);
253
+ }
254
+ try {
255
+ const reader = res.body.getReader();
256
+ const decoder = new TextDecoder();
257
+ let buffer = "";
258
+ for (;;) {
259
+ const { done, value } = await reader.read();
260
+ if (done) break;
261
+ buffer += decoder.decode(value, { stream: true });
262
+ const frames = buffer.split(/\r?\n\r?\n/);
263
+ buffer = frames.pop() ?? "";
264
+ for (const frame of frames) {
265
+ const data = frame.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart()).join("\n");
266
+ if (!data) continue;
267
+ let payload;
268
+ try {
269
+ payload = JSON.parse(data);
270
+ } catch {
271
+ continue;
272
+ }
273
+ const envelope = payload;
274
+ if (envelope && "error" in envelope && envelope.error) throw A2AError.fromJsonRpc(envelope.error);
275
+ yield payload;
276
+ }
277
+ }
278
+ } finally {
279
+ clearTimeout(timeout);
280
+ options.signal?.removeEventListener("abort", onAbort);
281
+ }
282
+ }
283
+ };
284
+ //#endregion
285
+ //#region src/client.ts
286
+ /**
287
+ * A2A (Agent2Agent) Protocol v1.0 — outbound client.
288
+ *
289
+ * A thin client that discovers an agent via its AgentCard and speaks the
290
+ * JSONRPC binding: SendMessage (sync/async), SendStreamingMessage,
291
+ * GetTask, CancelTask, ListTasks.
292
+ */
293
+ /** A2A client bound to one remote agent. */
294
+ var A2AClient = class A2AClient {
295
+ card;
296
+ rpc;
297
+ endpointUrl;
298
+ tenant;
299
+ static async connect(agentCardUrlOrBase, options = {}) {
300
+ const card = await fetchAgentCard(agentCardUrlOrBase, options);
301
+ return new A2AClient(card, options);
302
+ }
303
+ constructor(card, options = {}) {
304
+ this.card = card;
305
+ const { iface, url } = pickInterface(card, options.preferredBinding);
306
+ this.endpointUrl = url;
307
+ this.tenant = iface.tenant;
308
+ this.rpc = new JsonRpcClient(url, options);
309
+ }
310
+ /** Send a message; returns a Task or Message per the server's choice. */
311
+ async sendMessage(message, configuration, options = {}) {
312
+ return this.rpc.call("SendMessage", {
313
+ message,
314
+ configuration,
315
+ ...this.tenant ? { tenant: this.tenant } : {}
316
+ }, options);
317
+ }
318
+ /** Send a message over a stream, yielding all StreamResponse events. */
319
+ streamMessage(message, configuration = {}, options = {}) {
320
+ return this.rpc.stream("SendStreamingMessage", {
321
+ message,
322
+ configuration,
323
+ ...this.tenant ? { tenant: this.tenant } : {}
324
+ }, options);
325
+ }
326
+ async getTask(id, historyLength, options = {}) {
327
+ return this.rpc.call("GetTask", {
328
+ id,
329
+ ...historyLength !== void 0 ? { historyLength } : {},
330
+ ...this.tenant ? { tenant: this.tenant } : {}
331
+ }, options);
332
+ }
333
+ async listTasks(params = {}, options = {}) {
334
+ return this.rpc.call("ListTasks", {
335
+ ...params,
336
+ ...this.tenant ? { tenant: this.tenant } : {}
337
+ }, options);
338
+ }
339
+ async cancelTask(id, options = {}) {
340
+ return this.rpc.call("CancelTask", {
341
+ id,
342
+ ...this.tenant ? { tenant: this.tenant } : {}
343
+ }, options);
344
+ }
345
+ /**
346
+ * Send a message and wait (polling) until the created task reaches a
347
+ * terminal state. Errors if the task fails or is rejected.
348
+ */
349
+ async sendAndWait(message, configuration = {}, options = {}) {
350
+ const resp = await this.sendMessage(message, {
351
+ ...configuration,
352
+ returnImmediately: true
353
+ }, options);
354
+ if ("message" in resp && resp.message) return {
355
+ id: "message",
356
+ status: {
357
+ state: "TASK_STATE_COMPLETED",
358
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
359
+ },
360
+ artifacts: [{
361
+ artifactId: "message",
362
+ parts: resp.message.parts
363
+ }],
364
+ history: [resp.message]
365
+ };
366
+ const task = "task" in resp && resp.task ? resp.task : void 0;
367
+ if (!task) throw new A2AError(-32602, "SendMessage returned neither a Task nor a Message");
368
+ let t = task;
369
+ for (;;) {
370
+ if (isTerminal(t.status.state)) return t;
371
+ if (t.status.state === "TASK_STATE_INPUT_REQUIRED" || t.status.state === "TASK_STATE_AUTH_REQUIRED") return t;
372
+ if (options.signal?.aborted) throw new A2AError(408, "Task wait aborted");
373
+ await new Promise((r) => setTimeout(r, options.intervalMs ?? 500));
374
+ t = await this.getTask(t.id, 0, options);
375
+ }
376
+ }
377
+ };
378
+ //#endregion
379
+ //#region src/outbound.ts
380
+ function normalizeToolName(raw) {
381
+ const norm = raw.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
382
+ if (norm.length > 0 && /^[A-Za-z0-9_-]+$/.test(norm)) return norm;
383
+ let h = 0;
384
+ for (let i = 0; i < raw.length; i++) h = h * 31 + raw.charCodeAt(i) >>> 0;
385
+ return `_${h.toString(16)}`;
386
+ }
387
+ function skillToToolName(agentName, skill) {
388
+ const raw = `a2a__${agentName}__${skill.id}`;
389
+ const base = raw.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
390
+ if (base === raw || base.length < 64) return base;
391
+ let h = 0;
392
+ for (let i = 0; i < raw.length; i++) h = h * 31 + raw.charCodeAt(i) >>> 0;
393
+ return `${base.slice(0, 56)}_${h.toString(16)}`;
394
+ }
395
+ /** Extract the first meaningful text from message parts. */
396
+ function partsToText(parts) {
397
+ if (!parts) return "";
398
+ return parts.map((p) => {
399
+ if ("text" in p && p.text) return p.text;
400
+ if ("url" in p && p.url) return p.url;
401
+ if ("data" in p && p.data !== void 0) return JSON.stringify(p.data);
402
+ if ("raw" in p && p.raw) return `[raw ${p.mediaType ?? "application/octet-stream"}]`;
403
+ return "";
404
+ }).filter(Boolean).join("\n");
405
+ }
406
+ /**
407
+ * Register all skills of a remote agent on `ctx.tools` and return disposers.
408
+ */
409
+ async function registerAgentTools(tools, options) {
410
+ const connectionId = crypto.randomUUID();
411
+ const client = await A2AClient.connect(options.agentCardUrl, {
412
+ bearerToken: options.bearerToken,
413
+ timeoutMs: options.timeoutMs
414
+ });
415
+ const card = client.card;
416
+ const skills = card.skills ?? [];
417
+ options.onReady?.({
418
+ connectionId,
419
+ card
420
+ });
421
+ let disposers = [];
422
+ if (options.mapSkills === false || skills.length === 0) {
423
+ const name = normalizeToolName(`a2a__${options.name}__agent`);
424
+ const dispose = tools.register?.(makeTool(name, client, {
425
+ id: "agent",
426
+ name: options.name,
427
+ description: `${card.name}: ${card.description} (no skills advertised)`
428
+ }));
429
+ if (dispose) disposers = [{
430
+ name,
431
+ dispose
432
+ }];
433
+ } else {
434
+ const out = [];
435
+ for (const skill of skills) {
436
+ const name = skillToToolName(options.name, skill);
437
+ const dispose = tools.register?.(makeTool(name, client, skill));
438
+ if (dispose) out.push({
439
+ name,
440
+ dispose
441
+ });
442
+ }
443
+ disposers = out;
444
+ }
445
+ return disposers.map((r) => ({
446
+ ...r,
447
+ connectionId,
448
+ dispose: () => {
449
+ r.dispose();
450
+ options.onDispose?.(connectionId);
451
+ }
452
+ }));
453
+ }
454
+ function makeTool(name, client, skill) {
455
+ return {
456
+ name,
457
+ description: [skill.description, ...skill.examples?.length ? [`Examples: ${skill.examples.join(" | ")}`] : []].filter(Boolean).join("\n"),
458
+ parameters: {
459
+ type: "object",
460
+ properties: { prompt: {
461
+ type: "string",
462
+ description: "The task or instruction to send to the remote agent."
463
+ } },
464
+ required: ["prompt"]
465
+ },
466
+ output: {
467
+ schema: { type: "string" },
468
+ render: (_args, value) => [{
469
+ type: "text",
470
+ text: String(value)
471
+ }]
472
+ },
473
+ async execute(args, exec) {
474
+ const message = {
475
+ messageId: crypto.randomUUID(),
476
+ role: "ROLE_USER",
477
+ parts: [{ text: args.prompt }]
478
+ };
479
+ try {
480
+ const task = await client.sendAndWait(message, {}, { signal: exec.signal });
481
+ const text = partsToText(task.artifacts?.flatMap((a) => a.parts));
482
+ const failureMsg = task.status.message?.parts?.find((p) => "text" in p && p.text);
483
+ if (task.status.state === "TASK_STATE_FAILED") throw new A2AError(-32e3, `Remote agent task failed: ${text || (failureMsg && "text" in failureMsg ? failureMsg.text : "no detail")}`);
484
+ if (task.status.state === "TASK_STATE_INPUT_REQUIRED" || task.status.state === "TASK_STATE_AUTH_REQUIRED") {
485
+ const ask = (failureMsg && "text" in failureMsg ? failureMsg.text : "") || text || "agent awaits input";
486
+ return `[remote agent ${task.status.state}] ${ask}`;
487
+ }
488
+ return text || "(remote agent returned no output)";
489
+ } catch (err) {
490
+ if (err instanceof A2AError && err.code === -32001 && err.message.includes("interrupted")) throw new Error(`Remote agent requires input: ${err.message}`);
491
+ throw err;
492
+ }
493
+ }
494
+ };
495
+ }
496
+ //#endregion
497
+ //#region src/server.ts
498
+ /** In-memory task store with the full A2A Task lifecycle. */
499
+ var TaskStore = class {
500
+ tasks = /* @__PURE__ */ new Map();
501
+ messages = /* @__PURE__ */ new Map();
502
+ create(message) {
503
+ const taskId = crypto.randomUUID();
504
+ const task = {
505
+ id: taskId,
506
+ contextId: message.contextId ?? crypto.randomUUID(),
507
+ status: {
508
+ state: "TASK_STATE_SUBMITTED",
509
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
510
+ },
511
+ artifacts: [],
512
+ history: [message],
513
+ metadata: {}
514
+ };
515
+ this.tasks.set(taskId, task);
516
+ this.messages.set(message.messageId, message);
517
+ return task;
518
+ }
519
+ get(id) {
520
+ return this.tasks.get(id);
521
+ }
522
+ list() {
523
+ return [...this.tasks.values()];
524
+ }
525
+ setStatus(taskId, state, message) {
526
+ const task = this.tasks.get(taskId);
527
+ if (!task) throw new Error(`task ${taskId} not found`);
528
+ task.status = {
529
+ state,
530
+ message,
531
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
532
+ };
533
+ if (message) {
534
+ this.messages.set(message.messageId, message);
535
+ task.history = [...task.history ?? [], message];
536
+ }
537
+ return task;
538
+ }
539
+ addArtifact(taskId, artifact) {
540
+ const task = this.tasks.get(taskId);
541
+ if (!task) throw new Error(`task ${taskId} not found`);
542
+ const existing = task.artifacts?.find((a) => a.artifactId === artifact.artifactId);
543
+ if (existing) existing.parts.push(...artifact.parts);
544
+ else task.artifacts = [...task.artifacts ?? [], {
545
+ artifactId: artifact.artifactId,
546
+ name: artifact.name,
547
+ parts: artifact.parts
548
+ }];
549
+ return task;
550
+ }
551
+ getMessage(id) {
552
+ return this.messages.get(id);
553
+ }
554
+ };
555
+ /** Extract a human-readable source address from a request (when visible). */
556
+ function sourceOf(req) {
557
+ const s = req.socket;
558
+ if (!s?.remoteAddress) return void 0;
559
+ return `${s.remoteAddress}:${s.remotePort ?? ""}`;
560
+ }
561
+ /** A2A JSON-RPC server handler over a raw HTTP request/response pair. */
562
+ var A2AServer = class {
563
+ options;
564
+ store;
565
+ card;
566
+ endpointPath;
567
+ execute;
568
+ listeners = /* @__PURE__ */ new Set();
569
+ baseUrl;
570
+ constructor(options, store = new TaskStore()) {
571
+ this.options = options;
572
+ this.store = store;
573
+ this.execute = options.execute ?? defaultExecutor;
574
+ this.endpointPath = options.endpointPath ?? "/a2a";
575
+ this.baseUrl = options.baseUrl;
576
+ this.card = this.buildCard();
577
+ }
578
+ /**
579
+ * Update the base URL advertised in the AgentCard after the real listen
580
+ * address is known (e.g. ephemeral port). Mutates `card` in place.
581
+ */
582
+ setBaseUrl(baseUrl) {
583
+ this.baseUrl = baseUrl.replace(/\/$/, "");
584
+ this.card.supportedInterfaces = this.card.supportedInterfaces.map((iface) => ({
585
+ ...iface,
586
+ url: `${this.baseUrl}${this.endpointPath}`
587
+ }));
588
+ }
589
+ buildCard() {
590
+ const iface = {
591
+ url: `${this.baseUrl.replace(/\/$/, "")}${this.endpointPath}`,
592
+ protocolBinding: "JSONRPC",
593
+ protocolVersion: "1.0"
594
+ };
595
+ const skills = this.options.skills ?? [];
596
+ return {
597
+ name: this.options.agentName,
598
+ description: this.options.agentDescription,
599
+ version: this.options.agentVersion,
600
+ supportedInterfaces: [iface],
601
+ provider: this.options.agentProvider ?? {
602
+ url: "https://deepseek.com",
603
+ organization: "DeepSeek"
604
+ },
605
+ capabilities: {
606
+ streaming: true,
607
+ pushNotifications: false,
608
+ extensions: []
609
+ },
610
+ defaultInputModes: this.options.defaultInputModes ?? ["text/plain"],
611
+ defaultOutputModes: this.options.defaultOutputModes ?? ["text/plain"],
612
+ skills,
613
+ ...this.options.iconUrl ? { iconUrl: this.options.iconUrl } : {}
614
+ };
615
+ }
616
+ emit(ev) {
617
+ for (const l of this.listeners) l(ev);
618
+ }
619
+ /** Route an inbound HTTP request; returns true when handled. */
620
+ async handle(req, body) {
621
+ const path = (req.url ?? "").split("?")[0];
622
+ if (req.method === "GET" && (path === "/.well-known/agent-card.json" || path === this.endpointPath + "/card")) return {
623
+ status: 200,
624
+ contentType: "application/json",
625
+ body: JSON.stringify(this.card)
626
+ };
627
+ if (req.method === "POST" && path === this.endpointPath) {
628
+ let payload;
629
+ try {
630
+ payload = JSON.parse(body);
631
+ } catch {
632
+ return this.jsonRpcError(null, A2A_ERROR_CODES.INVALID_REQUEST, "Invalid JSON", "Parse error");
633
+ }
634
+ const rpc = payload;
635
+ if (!rpc || rpc.jsonrpc !== "2.0" || typeof rpc.method !== "string") return this.jsonRpcError(rpc?.id ?? null, A2A_ERROR_CODES.INVALID_REQUEST, "Invalid JSON-RPC request");
636
+ try {
637
+ const result = await this.dispatch(rpc);
638
+ this.options.onInbound?.({
639
+ method: rpc.method,
640
+ headers: req.headers,
641
+ source: sourceOf(req),
642
+ taskIds: extractTaskIds(result),
643
+ streaming: false
644
+ });
645
+ return {
646
+ status: 200,
647
+ contentType: "application/json",
648
+ body: JSON.stringify({
649
+ jsonrpc: "2.0",
650
+ id: rpc.id,
651
+ result
652
+ })
653
+ };
654
+ } catch (err) {
655
+ const e = err;
656
+ return this.jsonRpcError(rpc.id ?? null, typeof e.code === "number" ? e.code : A2A_ERROR_CODES.INTERNAL_ERROR, e.message ?? String(err), e.data);
657
+ }
658
+ }
659
+ return {
660
+ status: 404,
661
+ contentType: "text/plain",
662
+ body: "Not Found"
663
+ };
664
+ }
665
+ async handleStream(req, body, onEvent) {
666
+ const path = (req.url ?? "").split("?")[0];
667
+ if (req.method !== "POST" || path !== this.endpointPath) {
668
+ onEvent(`event: error\ndata: ${JSON.stringify({
669
+ code: 404,
670
+ message: "Not Found"
671
+ })}\n\n`);
672
+ return { status: 200 };
673
+ }
674
+ let payload;
675
+ try {
676
+ payload = JSON.parse(body);
677
+ } catch {
678
+ onEvent(`event: error\ndata: ${JSON.stringify({
679
+ code: A2A_ERROR_CODES.INVALID_REQUEST,
680
+ message: "Parse error"
681
+ })}\n\n`);
682
+ return { status: 200 };
683
+ }
684
+ const rpc = payload;
685
+ const method = rpc?.method;
686
+ if (method === A2A_METHODS.sendStreamingMessage || method === A2A_METHODS.subscribeToTask) {
687
+ const taskId = method === A2A_METHODS.subscribeToTask ? rpc.params?.id : await this.ensureTask(rpc.params);
688
+ if (!taskId) {
689
+ onEvent(`event: error\ndata: ${JSON.stringify({
690
+ code: A2A_ERROR_CODES.INVALID_PARAMS,
691
+ message: "Missing message"
692
+ })}\n\n`);
693
+ return { status: 200 };
694
+ }
695
+ const task = this.store.get(taskId);
696
+ if (!task) {
697
+ onEvent(`event: error\ndata: ${JSON.stringify({
698
+ code: A2A_ERROR_CODES.TASK_NOT_FOUND,
699
+ message: `Task ${taskId} not found`
700
+ })}\n\n`);
701
+ return { status: 200 };
702
+ }
703
+ const sub = (ev) => onEvent(`data: ${JSON.stringify(ev)}\n\n`);
704
+ this.listeners.add(sub);
705
+ this.options.onInbound?.({
706
+ method,
707
+ headers: req.headers,
708
+ source: sourceOf(req),
709
+ taskIds: taskId ? [taskId] : [],
710
+ streaming: true
711
+ });
712
+ try {
713
+ if (method === A2A_METHODS.sendStreamingMessage) {
714
+ const msg = rpc.params.message;
715
+ this.runTask(task.id, task.contextId, msg);
716
+ }
717
+ await this.waitTerminal(task.id);
718
+ onEvent(`data: ${JSON.stringify({ task: this.store.get(taskId) })}\n\n`);
719
+ } finally {
720
+ this.listeners.delete(sub);
721
+ }
722
+ return { status: 200 };
723
+ }
724
+ onEvent(`event: error\ndata: ${JSON.stringify({
725
+ code: A2A_ERROR_CODES.METHOD_NOT_FOUND,
726
+ message: `Unknown method ${method}`
727
+ })}\n\n`);
728
+ return { status: 200 };
729
+ }
730
+ async ensureTask(params) {
731
+ if (!params?.message) return void 0;
732
+ const msg = params.message;
733
+ if (msg.taskId && this.store.get(msg.taskId)) return msg.taskId;
734
+ return this.store.create(msg).id;
735
+ }
736
+ async waitTerminal(taskId) {
737
+ for (;;) {
738
+ const t = this.store.get(taskId);
739
+ if (t && isTerminal(t.status.state)) return;
740
+ await new Promise((r) => setTimeout(r, 200));
741
+ }
742
+ }
743
+ /** Execute a task in the background, streaming status/artifact updates. */
744
+ async runTask(taskId, contextId, msg) {
745
+ this.store.setStatus(taskId, "TASK_STATE_WORKING");
746
+ this.emit({ statusUpdate: {
747
+ taskId,
748
+ contextId,
749
+ status: this.store.get(taskId).status
750
+ } });
751
+ const signal = new AbortController().signal;
752
+ try {
753
+ const reply = await this.execute({
754
+ message: msg,
755
+ taskId,
756
+ contextId,
757
+ signal
758
+ });
759
+ this.store.addArtifact(taskId, {
760
+ artifactId: "result",
761
+ parts: reply.parts,
762
+ lastChunk: true
763
+ });
764
+ this.store.setStatus(taskId, "TASK_STATE_COMPLETED", reply);
765
+ this.options.onTaskSettled?.(taskId);
766
+ const task = this.store.get(taskId);
767
+ this.emit({ task });
768
+ this.emit({ artifactUpdate: {
769
+ taskId,
770
+ contextId,
771
+ artifact: task.artifacts[task.artifacts.length - 1],
772
+ lastChunk: true
773
+ } });
774
+ } catch (err) {
775
+ const message = {
776
+ messageId: crypto.randomUUID(),
777
+ role: "ROLE_AGENT",
778
+ parts: [{ text: `Task failed: ${err.message ?? String(err)}` }]
779
+ };
780
+ this.store.setStatus(taskId, "TASK_STATE_FAILED", message);
781
+ this.options.onTaskSettled?.(taskId);
782
+ this.emit({ statusUpdate: {
783
+ taskId,
784
+ contextId,
785
+ status: this.store.get(taskId).status
786
+ } });
787
+ }
788
+ }
789
+ async dispatch(rpc) {
790
+ const method = rpc.method;
791
+ const params = rpc.params ?? {};
792
+ switch (method) {
793
+ case A2A_METHODS.sendMessage: {
794
+ const msg = params.message;
795
+ if (!msg?.parts?.length) throw {
796
+ code: A2A_ERROR_CODES.INVALID_PARAMS,
797
+ message: "Missing message"
798
+ };
799
+ const taskId = await this.ensureTask(params);
800
+ const task = this.store.get(taskId);
801
+ this.runTask(task.id, task.contextId, msg);
802
+ await this.waitTerminal(task.id);
803
+ return { task: this.store.get(task.id) };
804
+ }
805
+ case A2A_METHODS.getTask: {
806
+ const id = params.id;
807
+ const task = this.store.get(id);
808
+ if (!task) throw {
809
+ code: A2A_ERROR_CODES.TASK_NOT_FOUND,
810
+ message: `Task ${id} not found`
811
+ };
812
+ return task;
813
+ }
814
+ case A2A_METHODS.listTasks: {
815
+ const tasks = this.store.list();
816
+ return {
817
+ tasks,
818
+ nextPageToken: "",
819
+ pageSize: tasks.length,
820
+ totalSize: tasks.length
821
+ };
822
+ }
823
+ case A2A_METHODS.cancelTask: {
824
+ const id = params.id;
825
+ const task = this.store.get(id);
826
+ if (!task) throw {
827
+ code: A2A_ERROR_CODES.TASK_NOT_FOUND,
828
+ message: `Task ${id} not found`
829
+ };
830
+ if (isTerminal(task.status.state)) throw {
831
+ code: A2A_ERROR_CODES.TASK_CANCEL_NOT_ALLOWED,
832
+ message: `Task ${id} already in terminal state ${task.status.state}`
833
+ };
834
+ this.store.setStatus(id, "TASK_STATE_CANCELED");
835
+ this.options.onTaskSettled?.(id);
836
+ return this.store.get(id);
837
+ }
838
+ case A2A_METHODS.getExtendedAgentCard: return this.card;
839
+ default: throw {
840
+ code: A2A_ERROR_CODES.METHOD_NOT_FOUND,
841
+ message: `Method ${method} not supported`
842
+ };
843
+ }
844
+ }
845
+ jsonRpcError(id, code, message, data) {
846
+ const err = {
847
+ jsonrpc: "2.0",
848
+ id,
849
+ error: {
850
+ code,
851
+ message,
852
+ ...data !== void 0 ? { data } : {}
853
+ }
854
+ };
855
+ return {
856
+ status: 200,
857
+ contentType: "application/json",
858
+ body: JSON.stringify(err)
859
+ };
860
+ }
861
+ };
862
+ /** Pull task ids out of a dispatch result for inbound peer tracking. */
863
+ function extractTaskIds(result) {
864
+ if (!result || typeof result !== "object") return [];
865
+ const r = result;
866
+ if (typeof r.task?.id === "string") return [r.task.id];
867
+ if (Array.isArray(r.tasks)) return r.tasks.map((t) => t.id).filter((id) => typeof id === "string");
868
+ return [];
869
+ }
870
+ const defaultSkills = [{
871
+ id: "coding",
872
+ name: "Coding",
873
+ description: "Execute coding and shell tasks inside the DSH workspace.",
874
+ tags: ["coding", "shell"],
875
+ inputModes: ["text/plain"],
876
+ outputModes: ["text/plain"]
877
+ }];
878
+ /**
879
+ * Default executor: run the incoming prompt through the system shell and
880
+ * return the captured stdout/stderr as the task artifact. Honors the
881
+ * AbortSignal (task cancellation / shutdown) by killing the child process.
882
+ */
883
+ const defaultExecutor = async ({ message, signal }) => {
884
+ const text = message.parts.map((p) => "text" in p ? p.text : "").filter(Boolean).join("\n");
885
+ if (!text.trim()) return {
886
+ messageId: crypto.randomUUID(),
887
+ role: "ROLE_AGENT",
888
+ parts: [{ text: "No prompt provided." }]
889
+ };
890
+ try {
891
+ const { execFile } = await import("node:child_process");
892
+ const out = await new Promise((resolve, _reject) => {
893
+ const child = execFile("/bin/sh", ["-c", text], {
894
+ timeout: 3e4,
895
+ maxBuffer: 1048576,
896
+ signal
897
+ }, (err, stdout, stderr) => {
898
+ if (err) {
899
+ if (err.killed || signal?.aborted) {
900
+ resolve(`(aborted) ${stdout}${stderr}`.trim() || "(aborted)");
901
+ return;
902
+ }
903
+ resolve(`(exit ${err.code ?? "?"}) ${stdout}\n${stderr}`);
904
+ return;
905
+ }
906
+ resolve(`${stdout}\n${stderr}`.trim());
907
+ });
908
+ signal?.addEventListener("abort", () => child.kill(), { once: true });
909
+ });
910
+ return {
911
+ messageId: crypto.randomUUID(),
912
+ role: "ROLE_AGENT",
913
+ parts: [{ text: out || "(no output)" }]
914
+ };
915
+ } catch (err) {
916
+ return {
917
+ messageId: crypto.randomUUID(),
918
+ role: "ROLE_AGENT",
919
+ parts: [{ text: `Executor error: ${err.message}` }]
920
+ };
921
+ }
922
+ };
923
+ //#endregion
924
+ //#region src/dashboard.ts
925
+ /** Registry of live A2A connections, both directions. */
926
+ var DashboardRegistry = class {
927
+ peers = /* @__PURE__ */ new Map();
928
+ agents = /* @__PURE__ */ new Map();
929
+ hooks = {};
930
+ seq = 0;
931
+ setHooks(hooks) {
932
+ this.hooks = {
933
+ ...this.hooks,
934
+ ...hooks
935
+ };
936
+ }
937
+ /** Record one inbound request (or stream open). Returns the peer id. */
938
+ touchInbound(req, taskIds = []) {
939
+ const now = (/* @__PURE__ */ new Date()).toISOString();
940
+ const ua = req.headers?.["user-agent"] ?? req.headers?.["User-Agent"] ?? "unknown";
941
+ const remote = req.source;
942
+ const label = peerLabel(ua, remote);
943
+ const key = `${label}|${remote?.replace(/:\d+$/, "") ?? ""}`;
944
+ let peer;
945
+ for (const p of this.peers.values()) {
946
+ const pHost = p.source?.replace(/:\d+$/, "") ?? "";
947
+ if (`${p.label}|${pHost}` === key) {
948
+ peer = p;
949
+ break;
950
+ }
951
+ }
952
+ if (!peer) {
953
+ const id = `in-${++this.seq}`;
954
+ peer = {
955
+ id,
956
+ label,
957
+ source: remote,
958
+ firstSeen: now,
959
+ lastSeen: now,
960
+ taskCount: 0,
961
+ activeTaskIds: [],
962
+ streaming: false
963
+ };
964
+ this.peers.set(id, peer);
965
+ }
966
+ peer.source = remote ?? peer.source;
967
+ peer.lastSeen = now;
968
+ if (taskIds.length) {
969
+ peer.taskCount += taskIds.length;
970
+ for (const t of taskIds) if (!peer.activeTaskIds.includes(t)) peer.activeTaskIds.push(t);
971
+ }
972
+ return peer.id;
973
+ }
974
+ /** Mark a task finished for a peer (drops it from the active list). */
975
+ finishTask(peerId, taskId) {
976
+ const peer = this.peers.get(peerId);
977
+ if (!peer) return;
978
+ peer.activeTaskIds = peer.activeTaskIds.filter((t) => t !== taskId);
979
+ }
980
+ /** Mark a peer's streaming flag. */
981
+ setStreaming(peerId, streaming) {
982
+ const peer = this.peers.get(peerId);
983
+ if (peer) peer.streaming = streaming;
984
+ }
985
+ /** Remove an inbound peer (the "关闭" action severs tracking). */
986
+ removePeer(id) {
987
+ this.peers.delete(id);
988
+ }
989
+ /** Register or refresh an outbound agent connection. */
990
+ upsertAgent(agent) {
991
+ const now = (/* @__PURE__ */ new Date()).toISOString();
992
+ let id;
993
+ if (agent.connectionId) {
994
+ for (const [k, v] of this.agents) if (v.connectionId === agent.connectionId) {
995
+ id = k;
996
+ break;
997
+ }
998
+ }
999
+ if (!id) {
1000
+ id = `out-${++this.seq}`;
1001
+ this.agents.set(id, {
1002
+ ...agent,
1003
+ id,
1004
+ lastSeen: now
1005
+ });
1006
+ } else {
1007
+ const existing = this.agents.get(id);
1008
+ this.agents.set(id, {
1009
+ ...existing,
1010
+ ...agent,
1011
+ skillCount: agent.skillCount > 0 ? agent.skillCount : existing.skillCount,
1012
+ toolCount: agent.toolCount > 0 ? agent.toolCount : existing.toolCount,
1013
+ agentName: agent.agentName ?? existing.agentName,
1014
+ skills: agent.skills ? agent.skills : existing.skills,
1015
+ state: agent.state ?? existing.state,
1016
+ id,
1017
+ lastSeen: now
1018
+ });
1019
+ }
1020
+ return id;
1021
+ }
1022
+ /** Remove an outbound agent by id or connectionId. */
1023
+ removeAgent(connectionId) {
1024
+ for (const [k, v] of this.agents) if (v.connectionId === connectionId || k === connectionId) {
1025
+ this.agents.delete(k);
1026
+ return true;
1027
+ }
1028
+ return false;
1029
+ }
1030
+ /** Update one agent's state. */
1031
+ setAgentState(connectionId, state) {
1032
+ for (const v of this.agents.values()) if (v.connectionId === connectionId) {
1033
+ v.state = state;
1034
+ v.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
1035
+ }
1036
+ }
1037
+ /** Touch one agent's last activity. */
1038
+ touchAgent(connectionId) {
1039
+ for (const v of this.agents.values()) if (v.connectionId === connectionId) v.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
1040
+ }
1041
+ snapshot() {
1042
+ return {
1043
+ inbound: [...this.peers.values()].map((p) => ({
1044
+ ...p,
1045
+ activeTaskIds: [...p.activeTaskIds]
1046
+ })),
1047
+ outbound: [...this.agents.values()].map((a) => ({ ...a })),
1048
+ at: Date.now()
1049
+ };
1050
+ }
1051
+ async control(action, target, payload) {
1052
+ switch (action) {
1053
+ case "reconnect-agent": return this.hooks.reconnectAgent ? this.hooks.reconnectAgent(target) : {
1054
+ ok: false,
1055
+ message: "reconnect-agent is not wired"
1056
+ };
1057
+ case "close-agent": return this.hooks.closeAgent ? this.hooks.closeAgent(target) : {
1058
+ ok: false,
1059
+ message: "close-agent is not wired"
1060
+ };
1061
+ case "disable-agent": return this.hooks.disableAgent ? this.hooks.disableAgent(target) : {
1062
+ ok: false,
1063
+ message: "disable-agent is not wired (client mode not mounted)"
1064
+ };
1065
+ case "enable-agent": return this.hooks.enableAgent ? this.hooks.enableAgent(target) : {
1066
+ ok: false,
1067
+ message: "enable-agent is not wired (client mode not mounted)"
1068
+ };
1069
+ case "reconnect-peer": return this.hooks.reconnectPeer ? this.hooks.reconnectPeer(target) : {
1070
+ ok: false,
1071
+ message: "reconnect-peer is not supported"
1072
+ };
1073
+ case "close-peer": return Promise.resolve(this.closePeerLocal(target));
1074
+ case "discover-agent": {
1075
+ const url = payload?.agentCardUrl ?? target;
1076
+ if (!url) return {
1077
+ ok: false,
1078
+ message: "discover-agent requires agentCardUrl"
1079
+ };
1080
+ return this.hooks.discoverAgent ? this.hooks.discoverAgent(url) : {
1081
+ ok: false,
1082
+ message: "discover-agent is not wired"
1083
+ };
1084
+ }
1085
+ case "add-agent": {
1086
+ const name = payload?.name ?? target;
1087
+ const url = payload?.agentCardUrl;
1088
+ if (!url) return {
1089
+ ok: false,
1090
+ message: "add-agent requires agentCardUrl"
1091
+ };
1092
+ return this.hooks.addAgent ? this.hooks.addAgent(name, url) : {
1093
+ ok: false,
1094
+ message: "add-agent is not wired (client mode not mounted)"
1095
+ };
1096
+ }
1097
+ case "remove-agent": return this.hooks.removeAgent ? this.hooks.removeAgent(target) : {
1098
+ ok: false,
1099
+ message: "remove-agent is not wired (client mode not mounted)"
1100
+ };
1101
+ case "server-enable": return this.hooks.setServerEnabled ? this.hooks.setServerEnabled(true) : {
1102
+ ok: false,
1103
+ message: "server control is not wired (server mode not mounted)"
1104
+ };
1105
+ case "server-disable": return this.hooks.setServerEnabled ? this.hooks.setServerEnabled(false) : {
1106
+ ok: false,
1107
+ message: "server control is not wired (server mode not mounted)"
1108
+ };
1109
+ case "server-status": return this.hooks.serverStatus ? Promise.resolve(this.hooks.serverStatus()) : {
1110
+ ok: false,
1111
+ message: "server-status is not wired (server mode not mounted)"
1112
+ };
1113
+ default: return {
1114
+ ok: false,
1115
+ message: `unknown action ${action}`
1116
+ };
1117
+ }
1118
+ }
1119
+ closePeerLocal(id) {
1120
+ if (!this.peers.has(id)) return {
1121
+ ok: false,
1122
+ message: `peer ${id} not found`
1123
+ };
1124
+ this.removePeer(id);
1125
+ return {
1126
+ ok: true,
1127
+ message: `peer ${id} closed`
1128
+ };
1129
+ }
1130
+ };
1131
+ function peerLabel(ua, remote) {
1132
+ const lower = ua.toLowerCase();
1133
+ if (lower.includes("python-requests") || lower.includes("a2a-python")) return "Python A2A client";
1134
+ if (lower.includes("node")) return "Node A2A client";
1135
+ if (lower.includes("curl")) return "curl";
1136
+ if (lower.includes("mozilla")) return "Browser";
1137
+ return ua.replace(/[^A-Za-z0-9._-]/g, " ").trim().split(/\s+/).slice(0, 3).join(" ") || (remote ?? "unknown");
1138
+ }
1139
+ let shared;
1140
+ /** Get the process-wide dashboard registry (creating it on first use). */
1141
+ function getSharedRegistry() {
1142
+ if (!shared) shared = new DashboardRegistry();
1143
+ return shared;
1144
+ }
1145
+ //#endregion
1146
+ //#region src/a2a-config.ts
1147
+ /**
1148
+ * dsh-a2a — persisted configuration (`a2a.json`).
1149
+ *
1150
+ * Runtime-editable configuration lives in a per-profile `a2a.json`, so the
1151
+ * plugin's UI changes survive restarts without touching the composition
1152
+ * (`cordis.patch.yml` / `package.json` stay pure entry points).
1153
+ *
1154
+ * File resolution order:
1155
+ * 1. `process.cwd()/a2a.json` — profile dir when launched via `dsh --profile <x>`
1156
+ * 2. `~/.dsh/profiles/<cwd-basename>/a2a.json` — fallback when cwd is not the profile dir
1157
+ * 3. `$DSH_HOME/a2a.json` — last-resort global location
1158
+ *
1159
+ * Shape (all fields optional):
1160
+ * {
1161
+ * "mode": "both", // client | server | both (default both)
1162
+ * "timeoutMs": 8000, // client global default (ms)
1163
+ * "mapSkills": true, // per-skill tools vs one agent-wide tool
1164
+ * "agents": [{ "name", "agentCardUrl", "bearerToken?", "timeoutMs?", "mapSkills?", "enabled?" }],
1165
+ * "server": { "baseUrl"?, "agentName"?, "agentDescription"?, "agentVersion"?,
1166
+ * "endpointPath"?, "skills"?: [{ "id","name","description"?, "tags"? }] },
1167
+ * "dashboard": true
1168
+ * }
1169
+ */
1170
+ const A2A_CONFIG_FILENAME = "a2a.json";
1171
+ /**
1172
+ * Parse `--profile <name>` (or `--profile=<name>`) from the process argv.
1173
+ *
1174
+ * dsh keeps its launcher flags in the process command line (`node …/dsh
1175
+ * --profile a2a-test --port 3083`), so the active profile name is directly
1176
+ * observable no matter which directory the process was started from. This is
1177
+ * the most reliable way to locate the per-profile config — far better than
1178
+ * guessing from cwd's basename.
1179
+ */
1180
+ function profileNameFromArgv() {
1181
+ const argv = process.argv ?? [];
1182
+ for (let i = 0; i < argv.length; i++) {
1183
+ const a = argv[i];
1184
+ if (a === "--profile" && i + 1 < argv.length) {
1185
+ const v = argv[i + 1];
1186
+ if (v && !v.startsWith("-")) return v;
1187
+ } else if (a.startsWith("--profile=")) {
1188
+ const v = a.slice(10);
1189
+ if (v) return v;
1190
+ }
1191
+ }
1192
+ }
1193
+ /**
1194
+ * Resolve the a2a.json path for the current process, without depending on
1195
+ * the launch directory. The plugin must work no matter where `dsh` was
1196
+ * started from (dsh does NOT chdir into the profile directory), so this
1197
+ * prefers the profile named in argv (`--profile <name>`), then walks
1198
+ * outward from cwd, then falls back to the profile dir that matches cwd's
1199
+ * basename, then the harness home.
1200
+ *
1201
+ * Order:
1202
+ * 0. `$DSH_HOME/profiles/<argv --profile>/a2a.json` — the active profile
1203
+ * as named on the command line (most authoritative; works from ANY cwd)
1204
+ * 1. `cwd/a2a.json` — explicit when launched inside a profile dir
1205
+ * 2. outward from cwd: the nearest ancestor holding `a2a.json` — covers
1206
+ * launching from e.g. a repo checkout whose parent chain includes the
1207
+ * profile, or any subdir of the profile
1208
+ * 3. `$DSH_HOME/profiles/<cwd-basename>/a2a.json` — started from the
1209
+ * profile's *parent* (e.g. `~/.dsh/profiles`) or when the profile dir
1210
+ * is cwd but the file hasn't been created yet
1211
+ * 4. `$DSH_HOME/a2a.json` — last-resort global location
1212
+ *
1213
+ * Writes always go to the *resolved* path (the one we read), so a config
1214
+ * loaded from any step is saved back to the same place, never re-resolved
1215
+ * to cwd.
1216
+ */
1217
+ function resolveConfigPath() {
1218
+ const cwd = process.cwd();
1219
+ const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
1220
+ const base = cwd.split(/[\\/]/).pop() ?? "";
1221
+ const profileName = profileNameFromArgv();
1222
+ if (profileName && profileName !== "." && profileName !== ".." && !profileName.includes("/") && !profileName.includes("\\")) {
1223
+ const argvFile = join(home, "profiles", profileName, A2A_CONFIG_FILENAME);
1224
+ if (existsSync(argvFile)) return argvFile;
1225
+ }
1226
+ const cwdFile = join(cwd, A2A_CONFIG_FILENAME);
1227
+ if (existsSync(cwdFile)) return cwdFile;
1228
+ let dir = cwd;
1229
+ for (let i = 0; i < 12; i++) {
1230
+ const up = join(dir, A2A_CONFIG_FILENAME);
1231
+ if (existsSync(up)) return up;
1232
+ const parent = dirname(dir);
1233
+ if (parent === dir) break;
1234
+ dir = parent;
1235
+ }
1236
+ const profileFile = join(home, "profiles", base, A2A_CONFIG_FILENAME);
1237
+ if (base && existsSync(profileFile)) return profileFile;
1238
+ const globalFile = join(home, A2A_CONFIG_FILENAME);
1239
+ if (existsSync(globalFile)) return globalFile;
1240
+ return cwdFile;
1241
+ }
1242
+ /** Read the persisted config (empty object when absent or unreadable). */
1243
+ function loadConfig() {
1244
+ const path = resolveConfigPath();
1245
+ if (!existsSync(path)) return {};
1246
+ try {
1247
+ const raw = readFileSync(path, "utf8");
1248
+ const parsed = JSON.parse(raw);
1249
+ return parsed && typeof parsed === "object" ? parsed : {};
1250
+ } catch (err) {
1251
+ console.error(`[a2a] failed to read ${path}: ${err.message}`);
1252
+ return {};
1253
+ }
1254
+ }
1255
+ /** Atomically write the persisted config (creates the directory when needed). */
1256
+ function saveConfig(config) {
1257
+ const path = resolveConfigPath();
1258
+ try {
1259
+ const dir = dirname(path);
1260
+ mkdirSync(dir, { recursive: true });
1261
+ const tmp = `${path}.tmp`;
1262
+ writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf8");
1263
+ renameSync(tmp, path);
1264
+ return {
1265
+ ok: true,
1266
+ path
1267
+ };
1268
+ } catch (err) {
1269
+ return {
1270
+ ok: false,
1271
+ path,
1272
+ message: err.message
1273
+ };
1274
+ }
1275
+ }
1276
+ /**
1277
+ * Merge persisted config on top of the in-composition config, so the file
1278
+ * (UI-editable, survives restart) wins over static defaults.
1279
+ */
1280
+ function mergePersisted(base, persisted) {
1281
+ const merged = { ...base };
1282
+ if (persisted.mode) merged.mode = persisted.mode;
1283
+ if (persisted.timeoutMs !== void 0) merged.timeoutMs = persisted.timeoutMs;
1284
+ if (persisted.mapSkills !== void 0) merged.mapSkills = persisted.mapSkills;
1285
+ if (persisted.dashboard !== void 0) merged.dashboard = persisted.dashboard;
1286
+ const pServer = persisted.server;
1287
+ if (pServer) {
1288
+ if (pServer.enabled !== void 0) merged.serverEnabled = pServer.enabled;
1289
+ if (pServer.baseUrl) merged.baseUrl = pServer.baseUrl;
1290
+ if (pServer.agentName) merged.agentName = pServer.agentName;
1291
+ if (pServer.agentDescription) merged.agentDescription = pServer.agentDescription;
1292
+ if (pServer.agentVersion) merged.agentVersion = pServer.agentVersion;
1293
+ if (pServer.endpointPath) merged.endpointPath = pServer.endpointPath;
1294
+ if (pServer.skills) merged.skills = pServer.skills;
1295
+ }
1296
+ return merged;
1297
+ }
1298
+ //#endregion
1299
+ //#region src/index.ts
1300
+ const DEFAULT_ENDPOINT = "/a2a";
1301
+ const DEFAULT_API_PATH = "/a2a/api";
1302
+ /** Whether the shared dashboard API route is currently registered (once per process). */
1303
+ let dashboardApiRegistered = false;
1304
+ const name = "a2a";
1305
+ /**
1306
+ * Services this plugin may read as context properties. Declared so property
1307
+ * reads inside `apply` never trip Cordis's "without inject" guard. The
1308
+ * mode-specific wiring runs inside `ctx.inject(...)` for the single service it
1309
+ * needs, so a composition lacking the other one simply idles that half.
1310
+ */
1311
+ const inject = ["webServer", "tools"];
1312
+ function apply(ctx, config = {}) {
1313
+ const persisted = loadConfig();
1314
+ const mergedConfig = mergePersisted(config, persisted);
1315
+ const configPath = resolveConfigPath();
1316
+ const mode = mergedConfig.mode ?? "client";
1317
+ const logger = ctx.logger;
1318
+ const dashboard = getSharedRegistry();
1319
+ const wantsClient = mode === "client" || mode === "both";
1320
+ const serverEnabled = mergedConfig.serverEnabled === true;
1321
+ const wantsServer = mode === "server" || mode === "both" || serverEnabled;
1322
+ if (mergedConfig.dashboard !== false) ctx.inject(["webServer"], (ctx) => {
1323
+ if (dashboardApiRegistered) return;
1324
+ dashboardApiRegistered = true;
1325
+ let apiRoute;
1326
+ apiRoute = ctx.webServer.register({
1327
+ kind: "prefix",
1328
+ path: DEFAULT_API_PATH,
1329
+ handler: async (req, res) => {
1330
+ if (!trustedLoopback(req)) {
1331
+ res.writeHead(403, { "content-type": "text/plain" });
1332
+ res.end("forbidden");
1333
+ return;
1334
+ }
1335
+ if (req.method === "GET") {
1336
+ res.writeHead(200, {
1337
+ "content-type": "application/json",
1338
+ "cache-control": "no-cache"
1339
+ });
1340
+ res.end(JSON.stringify(dashboard.snapshot()));
1341
+ return;
1342
+ }
1343
+ if (req.method === "POST") {
1344
+ let payload;
1345
+ try {
1346
+ payload = JSON.parse(await readBody(req));
1347
+ } catch {
1348
+ res.writeHead(400, { "content-type": "application/json" });
1349
+ res.end(JSON.stringify({
1350
+ ok: false,
1351
+ message: "invalid JSON body"
1352
+ }));
1353
+ return;
1354
+ }
1355
+ const { action, target, ...rest } = payload ?? {};
1356
+ if (!action) {
1357
+ res.writeHead(400, { "content-type": "application/json" });
1358
+ res.end(JSON.stringify({
1359
+ ok: false,
1360
+ message: "action is required"
1361
+ }));
1362
+ return;
1363
+ }
1364
+ const result = await dashboard.control(action, target ?? "", rest);
1365
+ res.writeHead(result.ok ? 200 : 409, { "content-type": "application/json" });
1366
+ res.end(JSON.stringify(result));
1367
+ return;
1368
+ }
1369
+ res.writeHead(405, { "content-type": "text/plain" });
1370
+ res.end("Method Not Allowed");
1371
+ }
1372
+ });
1373
+ ctx.effect(() => () => {
1374
+ dashboardApiRegistered = false;
1375
+ apiRoute?.();
1376
+ }, "a2a: dashboard api cleanup");
1377
+ });
1378
+ if (wantsClient) {
1379
+ const agentName = mergedConfig.name ?? "remote";
1380
+ const initialAgents = [];
1381
+ for (const a of persisted.agents ?? []) if (a.name && a.agentCardUrl) initialAgents.push({
1382
+ name: a.name,
1383
+ agentCardUrl: a.agentCardUrl,
1384
+ configured: true,
1385
+ enabled: a.enabled !== false,
1386
+ bearerToken: a.bearerToken,
1387
+ timeoutMs: a.timeoutMs,
1388
+ mapSkills: a.mapSkills
1389
+ });
1390
+ if (mergedConfig.agentCardUrl) initialAgents.push({
1391
+ name: agentName,
1392
+ agentCardUrl: mergedConfig.agentCardUrl,
1393
+ configured: true,
1394
+ enabled: true,
1395
+ bearerToken: mergedConfig.bearerToken,
1396
+ timeoutMs: mergedConfig.timeoutMs,
1397
+ mapSkills: mergedConfig.mapSkills
1398
+ });
1399
+ ctx.inject(["tools"], (ctx) => {
1400
+ const live = /* @__PURE__ */ new Map();
1401
+ const configuredAgents = /* @__PURE__ */ new Set();
1402
+ const connect = async (name, agentCardUrl, configured = false, agentOpts = {}) => {
1403
+ let disposers = [];
1404
+ let connectionId;
1405
+ try {
1406
+ const regs = await registerAgentTools(ctx.tools, {
1407
+ name,
1408
+ agentCardUrl,
1409
+ bearerToken: agentOpts.bearerToken ?? mergedConfig.bearerToken,
1410
+ timeoutMs: agentOpts.timeoutMs ?? mergedConfig.timeoutMs,
1411
+ mapSkills: agentOpts.mapSkills ?? mergedConfig.mapSkills,
1412
+ onReady: (info) => {
1413
+ connectionId = info.connectionId;
1414
+ dashboard.upsertAgent({
1415
+ connectionId: info.connectionId,
1416
+ name,
1417
+ agentCardUrl,
1418
+ agentName: info.card.name,
1419
+ skillCount: (info.card.skills ?? []).length,
1420
+ toolCount: 0,
1421
+ state: "connected",
1422
+ configured: configured ? true : void 0,
1423
+ skills: (info.card.skills ?? []).map((s) => ({
1424
+ id: s.id,
1425
+ name: s.name,
1426
+ description: s.description,
1427
+ tags: s.tags
1428
+ }))
1429
+ });
1430
+ },
1431
+ onDispose: (cid) => {
1432
+ dashboard.removeAgent(cid);
1433
+ }
1434
+ });
1435
+ disposers = regs.map((r) => r.dispose);
1436
+ if (connectionId) {
1437
+ dashboard.upsertAgent({
1438
+ connectionId,
1439
+ name,
1440
+ agentCardUrl,
1441
+ skillCount: 0,
1442
+ toolCount: regs.length,
1443
+ state: "connected",
1444
+ configured: configured ? true : void 0
1445
+ });
1446
+ live.set(connectionId, {
1447
+ disposeAll: () => disposers.forEach((d) => d()),
1448
+ name,
1449
+ agentCardUrl,
1450
+ configured,
1451
+ enabled: true
1452
+ });
1453
+ if (configured) configuredAgents.add(connectionId);
1454
+ }
1455
+ logger?.info?.(`[a2a] registered ${regs.length} tool(s) for agent "${name}"`);
1456
+ return connectionId;
1457
+ } catch (err) {
1458
+ logger?.error?.(`[a2a] failed to register agent "${name}": ${err.message}`);
1459
+ return;
1460
+ }
1461
+ };
1462
+ const persistAgents = () => {
1463
+ const next = {
1464
+ ...loadConfig(),
1465
+ agents: []
1466
+ };
1467
+ for (const a of initialAgents) if (a.configured) next.agents.push({
1468
+ name: a.name,
1469
+ agentCardUrl: a.agentCardUrl,
1470
+ bearerToken: a.bearerToken,
1471
+ timeoutMs: a.timeoutMs,
1472
+ mapSkills: a.mapSkills,
1473
+ enabled: a.enabled === false ? false : void 0
1474
+ });
1475
+ const res = saveConfig(next);
1476
+ if (!res.ok) logger?.error?.(`[a2a] failed to save ${res.path}: ${res.message}`);
1477
+ };
1478
+ const findInitial = (entry) => {
1479
+ const idx = initialAgents.findIndex((a) => a.configured && a.name === entry.name && a.agentCardUrl === entry.agentCardUrl);
1480
+ if (idx < 0) return void 0;
1481
+ return {
1482
+ idx,
1483
+ item: initialAgents[idx]
1484
+ };
1485
+ };
1486
+ dashboard.setHooks({
1487
+ closeAgent: async (connectionId) => {
1488
+ const entry = live.get(connectionId);
1489
+ if (!entry) return {
1490
+ ok: false,
1491
+ message: `connection ${connectionId} not found`
1492
+ };
1493
+ entry.disposeAll();
1494
+ live.delete(connectionId);
1495
+ dashboard.removeAgent(connectionId);
1496
+ dashboard.setAgentState(connectionId, "disconnected");
1497
+ dashboard.upsertAgent({
1498
+ connectionId,
1499
+ name: entry.name,
1500
+ agentCardUrl: entry.agentCardUrl,
1501
+ skillCount: 0,
1502
+ toolCount: 0,
1503
+ state: "disconnected",
1504
+ enabled: false,
1505
+ configured: entry.configured ? true : void 0
1506
+ });
1507
+ return {
1508
+ ok: true,
1509
+ message: `connection ${connectionId} closed`
1510
+ };
1511
+ },
1512
+ disableAgent: async (connectionId) => {
1513
+ const entry = live.get(connectionId);
1514
+ let name = entry?.name;
1515
+ let url = entry?.agentCardUrl;
1516
+ let configured = entry?.configured ?? false;
1517
+ if (!entry) {
1518
+ const snap = dashboard.snapshot().outbound.find((o) => o.connectionId === connectionId);
1519
+ if (!snap) return {
1520
+ ok: false,
1521
+ message: `connection ${connectionId} not found`
1522
+ };
1523
+ name = snap.name;
1524
+ url = snap.agentCardUrl;
1525
+ configured = snap.configured ?? false;
1526
+ }
1527
+ if (entry) {
1528
+ entry.disposeAll();
1529
+ live.delete(connectionId);
1530
+ dashboard.setAgentState(connectionId, "disconnected");
1531
+ } else dashboard.setAgentState(connectionId, "disconnected");
1532
+ const found = name !== void 0 && url !== void 0 ? findInitial({
1533
+ name,
1534
+ agentCardUrl: url
1535
+ }) : void 0;
1536
+ if (found) {
1537
+ found.item.enabled = false;
1538
+ persistAgents();
1539
+ }
1540
+ const prevSnap = dashboard.snapshot().outbound.find((o) => o.connectionId === connectionId);
1541
+ dashboard.upsertAgent({
1542
+ connectionId,
1543
+ name: name ?? "unknown",
1544
+ agentCardUrl: url ?? "",
1545
+ agentName: prevSnap?.agentName ?? name,
1546
+ skillCount: 0,
1547
+ toolCount: 0,
1548
+ state: "disconnected",
1549
+ enabled: false,
1550
+ configured: configured ? true : void 0
1551
+ });
1552
+ return {
1553
+ ok: true,
1554
+ message: `agent ${connectionId} disabled (config kept)`
1555
+ };
1556
+ },
1557
+ enableAgent: async (connectionId) => {
1558
+ const snap = dashboard.snapshot().outbound.find((o) => o.connectionId === connectionId);
1559
+ if (!snap) return {
1560
+ ok: false,
1561
+ message: `connection ${connectionId} not found`
1562
+ };
1563
+ const found = findInitial({
1564
+ name: snap.name,
1565
+ agentCardUrl: snap.agentCardUrl
1566
+ });
1567
+ if (found) {
1568
+ found.item.enabled = true;
1569
+ persistAgents();
1570
+ }
1571
+ dashboard.setAgentState(connectionId, "reconnecting");
1572
+ const newId = await connect(snap.name, snap.agentCardUrl, snap.configured ?? false);
1573
+ if (!newId) return {
1574
+ ok: false,
1575
+ message: `enable of ${connectionId} failed to connect`
1576
+ };
1577
+ dashboard.removeAgent(connectionId);
1578
+ return {
1579
+ ok: true,
1580
+ message: `agent ${connectionId} enabled and reconnected as ${newId}`
1581
+ };
1582
+ },
1583
+ reconnectAgent: async (connectionId) => {
1584
+ const existing = live.get(connectionId);
1585
+ const prevName = existing?.name ?? agentName;
1586
+ const prevUrl = existing?.agentCardUrl ?? mergedConfig.agentCardUrl;
1587
+ const wasConfigured = !!existing?.configured || configuredAgents.has(connectionId);
1588
+ if (existing) existing.disposeAll();
1589
+ live.delete(connectionId);
1590
+ dashboard.setAgentState(connectionId, "reconnecting");
1591
+ const newId = await connect(prevName, prevUrl, wasConfigured);
1592
+ if (!newId) return {
1593
+ ok: false,
1594
+ message: `reconnect of ${connectionId} failed`
1595
+ };
1596
+ dashboard.removeAgent(connectionId);
1597
+ return {
1598
+ ok: true,
1599
+ message: `connection ${connectionId} reconnected as ${newId}`
1600
+ };
1601
+ },
1602
+ addAgent: async (name, agentCardUrl) => {
1603
+ const cid = await connect(name, agentCardUrl, true);
1604
+ if (!cid) return {
1605
+ ok: false,
1606
+ message: `failed to connect to ${agentCardUrl}`
1607
+ };
1608
+ initialAgents.push({
1609
+ name,
1610
+ agentCardUrl,
1611
+ configured: true,
1612
+ enabled: true
1613
+ });
1614
+ persistAgents();
1615
+ return {
1616
+ ok: true,
1617
+ message: `agent "${name}" connected as ${cid} (saved to ${configPath})`
1618
+ };
1619
+ },
1620
+ discoverAgent: async (agentCardUrl) => {
1621
+ try {
1622
+ const card = await fetchAgentCard(agentCardUrl, {
1623
+ timeoutMs: mergedConfig.timeoutMs ?? 8e3,
1624
+ bearerToken: mergedConfig.bearerToken
1625
+ });
1626
+ const iface = pickInterface(card);
1627
+ return {
1628
+ ok: true,
1629
+ message: JSON.stringify({
1630
+ name: card.name,
1631
+ description: card.description,
1632
+ version: card.version,
1633
+ agentCardUrl,
1634
+ endpoint: iface?.url,
1635
+ skills: (card.skills ?? []).map((s) => ({
1636
+ id: s.id,
1637
+ name: s.name,
1638
+ description: s.description
1639
+ })),
1640
+ capabilities: card.capabilities,
1641
+ defaultInputModes: card.defaultInputModes,
1642
+ defaultOutputModes: card.defaultOutputModes
1643
+ })
1644
+ };
1645
+ } catch (err) {
1646
+ return {
1647
+ ok: false,
1648
+ message: `discover failed: ${err.message}`
1649
+ };
1650
+ }
1651
+ },
1652
+ removeAgent: async (connectionId) => {
1653
+ const entry = live.get(connectionId);
1654
+ let name;
1655
+ let url;
1656
+ let configured = false;
1657
+ if (entry) {
1658
+ name = entry.name;
1659
+ url = entry.agentCardUrl;
1660
+ configured = entry.configured ?? false;
1661
+ entry.disposeAll();
1662
+ live.delete(connectionId);
1663
+ } else {
1664
+ const snap = dashboard.snapshot().outbound.find((o) => o.connectionId === connectionId);
1665
+ if (!snap) return {
1666
+ ok: false,
1667
+ message: `connection ${connectionId} not found`
1668
+ };
1669
+ name = snap.name;
1670
+ url = snap.agentCardUrl;
1671
+ configured = snap.configured ?? false;
1672
+ }
1673
+ if (configured && name !== void 0 && url !== void 0) {
1674
+ const found = findInitial({
1675
+ name,
1676
+ agentCardUrl: url
1677
+ });
1678
+ if (found) {
1679
+ initialAgents.splice(found.idx, 1);
1680
+ persistAgents();
1681
+ }
1682
+ }
1683
+ dashboard.removeAgent(connectionId);
1684
+ return {
1685
+ ok: true,
1686
+ message: `agent ${connectionId} removed`
1687
+ };
1688
+ }
1689
+ });
1690
+ ctx.effect(() => {
1691
+ for (const a of initialAgents) {
1692
+ if (a.enabled === false) {
1693
+ const phId = `out-cfg-${a.name}-${a.agentCardUrl}`;
1694
+ dashboard.upsertAgent({
1695
+ connectionId: phId,
1696
+ name: a.name,
1697
+ agentCardUrl: a.agentCardUrl,
1698
+ agentName: a.name,
1699
+ skillCount: 0,
1700
+ toolCount: 0,
1701
+ state: "disconnected",
1702
+ enabled: false,
1703
+ configured: true
1704
+ });
1705
+ continue;
1706
+ }
1707
+ connect(a.name, a.agentCardUrl, a.configured, {
1708
+ bearerToken: a.bearerToken,
1709
+ timeoutMs: a.timeoutMs,
1710
+ mapSkills: a.mapSkills
1711
+ });
1712
+ }
1713
+ return () => {
1714
+ for (const { disposeAll } of live.values()) disposeAll();
1715
+ live.clear();
1716
+ };
1717
+ }, "a2a: client tools");
1718
+ });
1719
+ }
1720
+ if (wantsServer) {
1721
+ const store = new TaskStore();
1722
+ const taskPeer = /* @__PURE__ */ new Map();
1723
+ const pendingSettled = /* @__PURE__ */ new Set();
1724
+ ctx.inject(["webServer"], (ctx) => {
1725
+ ctx.effect(() => {
1726
+ const webServer = ctx.webServer;
1727
+ const baseUrl = mergedConfig.baseUrl ?? `http://${webServer.host === "0.0.0.0" ? "127.0.0.1" : webServer.host}:${webServer.port}`;
1728
+ const endpointPath = mergedConfig.endpointPath ?? DEFAULT_ENDPOINT;
1729
+ const server = new A2AServer({
1730
+ baseUrl,
1731
+ agentName: mergedConfig.agentName ?? "DSH Agent",
1732
+ agentDescription: mergedConfig.agentDescription ?? "DeepSeek Harness agent exposed over A2A v1.0",
1733
+ agentVersion: mergedConfig.agentVersion ?? "0.1.0",
1734
+ skills: mergedConfig.skills,
1735
+ endpointPath,
1736
+ execute: mergedConfig.execute,
1737
+ onInbound: (facts) => {
1738
+ const peerId = dashboard.touchInbound({
1739
+ headers: facts.headers,
1740
+ source: facts.source
1741
+ }, facts.taskIds);
1742
+ for (const taskId of facts.taskIds) {
1743
+ taskPeer.set(taskId, peerId);
1744
+ if (pendingSettled.delete(taskId)) dashboard.finishTask(peerId, taskId);
1745
+ }
1746
+ },
1747
+ onTaskSettled: (taskId) => {
1748
+ const peerId = taskPeer.get(taskId);
1749
+ if (peerId) {
1750
+ dashboard.finishTask(peerId, taskId);
1751
+ taskPeer.delete(taskId);
1752
+ } else pendingSettled.add(taskId);
1753
+ }
1754
+ }, store);
1755
+ let enabled = true;
1756
+ let disposers = [];
1757
+ const registerRoutes = () => {
1758
+ disposers.push(webServer.register({
1759
+ kind: "exact",
1760
+ path: "/.well-known/agent-card.json",
1761
+ handler: async (_req, res) => {
1762
+ res.writeHead(200, { "content-type": "application/json" });
1763
+ res.end(JSON.stringify(server.card));
1764
+ }
1765
+ }));
1766
+ disposers.push(webServer.register({
1767
+ kind: "prefix",
1768
+ path: server.endpointPath,
1769
+ handler: async (req, res) => {
1770
+ const isStream = (req.headers.accept ?? "").includes("text/event-stream");
1771
+ if (req.method === "GET" && (req.url ?? "").startsWith(server.endpointPath + "/card")) {
1772
+ res.writeHead(200, { "content-type": "application/json" });
1773
+ res.end(JSON.stringify(server.card));
1774
+ return;
1775
+ }
1776
+ if (req.method !== "POST") {
1777
+ res.writeHead(405, { "content-type": "text/plain" });
1778
+ res.end("Method Not Allowed");
1779
+ return;
1780
+ }
1781
+ const body = await readBody(req);
1782
+ if (isStream) {
1783
+ res.writeHead(200, {
1784
+ "content-type": "text/event-stream",
1785
+ "cache-control": "no-cache",
1786
+ connection: "keep-alive"
1787
+ });
1788
+ await server.handleStream(toServerReq(req), body, (frame) => res.write(frame));
1789
+ res.end();
1790
+ return;
1791
+ }
1792
+ const out = await server.handle(toServerReq(req), body);
1793
+ res.writeHead(out.status, { "content-type": out.contentType });
1794
+ res.end(out.body);
1795
+ }
1796
+ }));
1797
+ };
1798
+ const unregisterRoutes = () => {
1799
+ for (const d of disposers) d();
1800
+ disposers = [];
1801
+ };
1802
+ registerRoutes();
1803
+ logger?.info?.(`[a2a] A2A server mounted: AgentCard at ${baseUrl}/.well-known/agent-card.json, JSON-RPC at ${server.endpointPath}`);
1804
+ const serverCard = () => ({
1805
+ baseUrl,
1806
+ agentName: server.card.name,
1807
+ agentDescription: server.card.description,
1808
+ agentVersion: server.card.version,
1809
+ endpoint: `${baseUrl}${server.endpointPath}`,
1810
+ agentCardUrl: `${baseUrl}/.well-known/agent-card.json`,
1811
+ skills: (server.card.skills ?? []).map((s) => ({
1812
+ id: s.id,
1813
+ name: s.name,
1814
+ description: s.description
1815
+ })),
1816
+ customExecutor: Boolean(mergedConfig.execute)
1817
+ });
1818
+ dashboard.setHooks({
1819
+ setServerEnabled: async (enable) => {
1820
+ if (enable === enabled) return {
1821
+ ok: true,
1822
+ message: `server already ${enable ? "enabled" : "disabled"}`
1823
+ };
1824
+ if (enable) {
1825
+ registerRoutes();
1826
+ enabled = true;
1827
+ logger?.info?.("[a2a] A2A server enabled");
1828
+ return {
1829
+ ok: true,
1830
+ message: "A2A server enabled"
1831
+ };
1832
+ }
1833
+ unregisterRoutes();
1834
+ enabled = false;
1835
+ logger?.info?.("[a2a] A2A server disabled");
1836
+ return {
1837
+ ok: true,
1838
+ message: "A2A server disabled"
1839
+ };
1840
+ },
1841
+ serverStatus: () => ({
1842
+ ok: true,
1843
+ message: JSON.stringify({
1844
+ enabled,
1845
+ ...serverCard()
1846
+ })
1847
+ })
1848
+ });
1849
+ return () => {
1850
+ unregisterRoutes();
1851
+ };
1852
+ }, "a2a: server routes");
1853
+ });
1854
+ }
1855
+ }
1856
+ /** Drain the request body from a Node IncomingMessage. */
1857
+ async function readBody(req) {
1858
+ return new Promise((resolve, reject) => {
1859
+ const chunks = [];
1860
+ req.on("data", (c) => chunks.push(c));
1861
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1862
+ req.once("error", reject);
1863
+ });
1864
+ }
1865
+ /** Loopback/same-origin trust fence for the dashboard API (no remote write). */
1866
+ function trustedLoopback(req) {
1867
+ const host = req.headers.host;
1868
+ if (!host) return false;
1869
+ const hostname = host.split(":")[0];
1870
+ if (hostname !== "127.0.0.1" && hostname !== "localhost" && hostname !== "::1") return false;
1871
+ if (req.headers["sec-fetch-site"] === "cross-site") return false;
1872
+ const origin = req.headers.origin;
1873
+ if (!origin) return true;
1874
+ try {
1875
+ return new URL(origin).host === host;
1876
+ } catch {
1877
+ return false;
1878
+ }
1879
+ }
1880
+ /** Adapt a Node IncomingMessage to the A2A server's structural request type. */
1881
+ function toServerReq(req) {
1882
+ const headers = {};
1883
+ for (const [k, v] of Object.entries(req.headers)) if (typeof v === "string") headers[k] = v;
1884
+ else if (Array.isArray(v)) headers[k] = v.join(", ");
1885
+ return {
1886
+ method: req.method,
1887
+ url: req.url,
1888
+ headers,
1889
+ socket: req.socket ? {
1890
+ remoteAddress: req.socket.remoteAddress,
1891
+ remotePort: req.socket.remotePort
1892
+ } : void 0
1893
+ };
1894
+ }
1895
+ //#endregion
1896
+ export { A2AClient, A2AError, A2AServer, A2A_ERROR_CODES, A2A_METHODS, DashboardRegistry, Role, TaskState, TaskStore, apply, defaultExecutor, defaultSkills, fetchAgentCard, getSharedRegistry, inject, isTerminal, name, registerAgentTools };