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