@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/types/index.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @hanphone/dsh-a2a — Cordis plugin entry.
|
|
3
3
|
*
|
|
4
|
-
* Mounts the A2A v1.0 dual-end plugin
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Mounts the A2A v1.0.1 dual-end plugin as a MULTI-INSTANCE composition: an
|
|
5
|
+
* arbitrary set of inbound A2A servers (each a preset-bound session pool, its
|
|
6
|
+
* own endpoint + AgentCard + creator-declared skills + auth) and an arbitrary
|
|
7
|
+
* set of outbound A2A connections (each a remote URL + preset + timeout).
|
|
8
|
+
* Instances are persisted in the `a2a` domain (`inbound_servers` /
|
|
9
|
+
* `outbound_servers` tables) and created/edited/started/stopped entirely from
|
|
10
|
+
* the GUI — the plugin `Config` is minimal and instance setup is not
|
|
11
|
+
* patch-config driven. The protocol surface is aligned with the official A2A
|
|
12
|
+
* v1.0.1 spec (see docs/design.md).
|
|
13
|
+
*
|
|
14
|
+
* Optional services are probed rather than injected so a composition lacking
|
|
15
|
+
* one half idles just that half; only the storage domain is required.
|
|
11
16
|
*
|
|
12
17
|
* Function-plugin export shape: named `name`/`inject`/`Config`/`apply`, no
|
|
13
18
|
* default export (mixing forms makes the Loader drop the namespace).
|
|
@@ -15,39 +20,23 @@
|
|
|
15
20
|
*/
|
|
16
21
|
import type { Context } from '@deepseek-ai/cordis';
|
|
17
22
|
import z from '@deepseek-ai/schemastery';
|
|
18
|
-
import { type
|
|
19
|
-
import type { AgentSkill, TaskState } from './protocol.ts';
|
|
23
|
+
import { type OpResult } from './service.ts';
|
|
20
24
|
export declare const name = "a2a";
|
|
21
25
|
/** Only the storage domain is required; the other halves are probed. */
|
|
22
26
|
export declare const inject: readonly ["storageDomain"];
|
|
27
|
+
/** Minimal host-level config; per-instance setup happens in the GUI/domain. */
|
|
23
28
|
export interface A2AConfig {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
baseUrl?: string;
|
|
31
|
-
endpointPath: string;
|
|
32
|
-
/** Environment variable name for the inbound bearer token; absent = anonymous. */
|
|
33
|
-
authTokenEnv?: string;
|
|
34
|
-
skills: {
|
|
35
|
-
ids: string[];
|
|
36
|
-
exclude: string[];
|
|
37
|
-
};
|
|
38
|
-
executors: Record<string, 'session' | 'subagent'>;
|
|
39
|
-
subagentProvider: string;
|
|
40
|
-
};
|
|
41
|
-
client: {
|
|
42
|
-
agents: OutboundAgentSpec[];
|
|
43
|
-
toolPrefix: string;
|
|
44
|
-
};
|
|
29
|
+
/** Advertised base URL for every inbound card; absent lets a default stand. */
|
|
30
|
+
baseUrl?: string;
|
|
31
|
+
/** Subagent driver provider for the inbound subagent executors. */
|
|
32
|
+
subagentProvider: string;
|
|
33
|
+
/** Default outbound connection timeout (ms). */
|
|
34
|
+
defaultTimeoutMs: number;
|
|
45
35
|
}
|
|
46
36
|
/** Loader-validated config schema (defaults applied by the Loader). */
|
|
47
37
|
export declare const Config: z<A2AConfig>;
|
|
48
38
|
export declare function apply(ctx: Context, config: A2AConfig): void;
|
|
49
|
-
export type {
|
|
50
|
-
export type { AgentSkill };
|
|
39
|
+
export type { OpResult };
|
|
51
40
|
export { A2AServer } from './server/a2a-server.ts';
|
|
52
41
|
export { A2AClient, A2AError } from './outbound/calls.ts';
|
|
53
42
|
export { A2AService } from './service.ts';
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAQxC,OAAO,EAA8G,KAAK,QAAQ,EAAmC,MAAM,cAAc,CAAA;AAKzL,eAAO,MAAM,IAAI,QAAQ,CAAA;AAEzB,wEAAwE;AACxE,eAAO,MAAM,MAAM,4BAA6B,CAAA;AAEhD,+EAA+E;AAC/E,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,CAAA;IACxB,gDAAgD;IAChD,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAED,uEAAuE;AACvE,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,SAAS,CAI9B,CAAA;AAEF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,QAoFpD;AAmKD,YAAY,EAAE,QAAQ,EAAE,CAAA;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA"}
|
package/lib/types/index.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @hanphone/dsh-a2a — Cordis plugin entry.
|
|
3
3
|
*
|
|
4
|
-
* Mounts the A2A v1.0 dual-end plugin
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Mounts the A2A v1.0.1 dual-end plugin as a MULTI-INSTANCE composition: an
|
|
5
|
+
* arbitrary set of inbound A2A servers (each a preset-bound session pool, its
|
|
6
|
+
* own endpoint + AgentCard + creator-declared skills + auth) and an arbitrary
|
|
7
|
+
* set of outbound A2A connections (each a remote URL + preset + timeout).
|
|
8
|
+
* Instances are persisted in the `a2a` domain (`inbound_servers` /
|
|
9
|
+
* `outbound_servers` tables) and created/edited/started/stopped entirely from
|
|
10
|
+
* the GUI — the plugin `Config` is minimal and instance setup is not
|
|
11
|
+
* patch-config driven. The protocol surface is aligned with the official A2A
|
|
12
|
+
* v1.0.1 spec (see docs/design.md).
|
|
13
|
+
*
|
|
14
|
+
* Optional services are probed rather than injected so a composition lacking
|
|
15
|
+
* one half idles just that half; only the storage domain is required.
|
|
11
16
|
*
|
|
12
17
|
* Function-plugin export shape: named `name`/`inject`/`Config`/`apply`, no
|
|
13
18
|
* default export (mixing forms makes the Loader drop the namespace).
|
|
@@ -17,189 +22,72 @@ import z from '@deepseek-ai/schemastery';
|
|
|
17
22
|
import { homedir } from 'node:os';
|
|
18
23
|
import { join } from 'node:path';
|
|
19
24
|
import { mkdirSync } from 'node:fs';
|
|
20
|
-
import { deriveSkills, buildCard } from "./server/card.js";
|
|
21
25
|
import { openDomain, DomainTaskStore } from "./server/store.js";
|
|
22
|
-
import {
|
|
23
|
-
import { cardOptionsFor, readIdentity, rebuildCardWithIdentity, writeIdentity } from "./server/identity.js";
|
|
24
|
-
import { ExecutorSet } from "./server/executor.js";
|
|
25
|
-
import { ContextSessionPool, probeService } from "./server/exec/agent-runtime.js";
|
|
26
|
-
import { createSessionExecutor } from "./server/exec/session.js";
|
|
27
|
-
import { createSubagentExecutor } from "./server/exec/subagent.js";
|
|
28
|
-
import { A2AServer } from "./server/a2a-server.js";
|
|
29
|
-
import { A2aRoutes } from "./server/routes.js";
|
|
26
|
+
import { probeService } from "./server/exec/agent-runtime.js";
|
|
30
27
|
import { handleApiRequest } from "./api.js";
|
|
31
|
-
import { OutboundAgentRegistry, DomainAgentStore } from "./outbound/registry.js";
|
|
32
28
|
import { A2AService } from "./service.js";
|
|
33
29
|
import { buildA2aCommand } from "./commands.js";
|
|
30
|
+
import { InboundServerManager, DomainInboundStore } from "./servers/inbound-manager.js";
|
|
31
|
+
import { OutboundServerManager, DomainOutboundStore } from "./servers/outbound-manager.js";
|
|
34
32
|
export const name = 'a2a';
|
|
35
33
|
/** Only the storage domain is required; the other halves are probed. */
|
|
36
34
|
export const inject = ['storageDomain'];
|
|
37
35
|
/** Loader-validated config schema (defaults applied by the Loader). */
|
|
38
36
|
export const Config = z.object({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
description: z.string().default('A DeepSeek Harness agent exposed over A2A v1.0'),
|
|
43
|
-
version: z.string().default('0.1.0'),
|
|
44
|
-
baseUrl: z.string(),
|
|
45
|
-
endpointPath: z.string().default('/a2a'),
|
|
46
|
-
authTokenEnv: z.string(),
|
|
47
|
-
skills: z.object({
|
|
48
|
-
ids: z.array(z.string()).default([]),
|
|
49
|
-
exclude: z.array(z.string()).default([]),
|
|
50
|
-
}),
|
|
51
|
-
executors: z.dict(z.union(['session', 'subagent'])).default({ chat: 'session' }),
|
|
52
|
-
subagentProvider: z.string().default('in-process'),
|
|
53
|
-
}),
|
|
54
|
-
client: z.object({
|
|
55
|
-
agents: z.array(z.object({
|
|
56
|
-
name: z.string(),
|
|
57
|
-
agentCardUrl: z.string(),
|
|
58
|
-
bearerTokenEnv: z.string(),
|
|
59
|
-
enabled: z.boolean().default(true),
|
|
60
|
-
timeoutMs: z.number().default(60000),
|
|
61
|
-
})).default([]),
|
|
62
|
-
toolPrefix: z.string().default('a2a'),
|
|
63
|
-
}),
|
|
37
|
+
baseUrl: z.string(),
|
|
38
|
+
subagentProvider: z.string().default('in-process'),
|
|
39
|
+
defaultTimeoutMs: z.number().default(60_000),
|
|
64
40
|
});
|
|
65
41
|
export function apply(ctx, config) {
|
|
66
42
|
const logger = ctx.logger('a2a');
|
|
67
|
-
const
|
|
68
|
-
const clientConfig = { ...config.client };
|
|
69
|
-
const authToken = serverConfig.authTokenEnv !== undefined ? process.env[serverConfig.authTokenEnv] : undefined;
|
|
70
|
-
const toolPrefix = clientConfig.toolPrefix;
|
|
71
|
-
const subagentProvider = serverConfig.subagentProvider;
|
|
43
|
+
const subagentProvider = config.subagentProvider;
|
|
72
44
|
const inboundCwd = inboundSessionCwd();
|
|
73
45
|
ctx.inject(['storageDomain'], (ctx) => {
|
|
74
46
|
ctx.effect(async () => {
|
|
75
47
|
const domain = await openDomain(ctx.storageDomain);
|
|
76
48
|
const store = new DomainTaskStore(domain);
|
|
77
|
-
|
|
78
|
-
domain,
|
|
79
|
-
store,
|
|
80
|
-
registry: undefined,
|
|
81
|
-
inbound: undefined,
|
|
82
|
-
server: undefined,
|
|
83
|
-
routes: undefined,
|
|
84
|
-
card: undefined,
|
|
85
|
-
executors: undefined,
|
|
86
|
-
enabled: serverConfig.enabled,
|
|
87
|
-
identityDefaults: {
|
|
88
|
-
name: serverConfig.name ?? 'My DSH Agent',
|
|
89
|
-
description: serverConfig.description ?? 'A DeepSeek Harness agent exposed over A2A v1.0',
|
|
90
|
-
version: serverConfig.version ?? '0.1.0',
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
// ── inbound server half ────────────────────────────────────────────
|
|
49
|
+
// Probe the optional host services each half uses.
|
|
94
50
|
const webServer = probeService(ctx, 'webServer', 'register');
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return { ok: false, reason: `unknown skill "${input.skill}"` };
|
|
130
|
-
}
|
|
131
|
-
const decision = {
|
|
132
|
-
contextId: input.contextId,
|
|
133
|
-
skill: input.skill,
|
|
134
|
-
parts: input.parts,
|
|
135
|
-
remotePeerId: input.remotePeerId,
|
|
136
|
-
};
|
|
137
|
-
const decided = await ctx.waterfall('a2a/inbound-task', decision, async (d) => d);
|
|
138
|
-
// Built-in audit: every task decision is logged with its source.
|
|
139
|
-
logger.info(`[a2a] inbound skill=${decided.skill} remote=${decided.remotePeerId} rejected=${decided.rejected?.reason ?? 'no'}`);
|
|
140
|
-
if (decided.rejected !== undefined)
|
|
141
|
-
return { ok: false, reason: decided.rejected.reason };
|
|
142
|
-
return { ok: true };
|
|
143
|
-
};
|
|
144
|
-
const server = new A2AServer({
|
|
145
|
-
card,
|
|
146
|
-
store,
|
|
147
|
-
executors,
|
|
148
|
-
...(authToken ? { authToken } : {}),
|
|
149
|
-
gate,
|
|
150
|
-
onInbound: (facts) => inbound.note({
|
|
151
|
-
method: facts.method,
|
|
152
|
-
...(facts.source !== undefined ? { source: facts.source } : {}),
|
|
153
|
-
taskIds: facts.taskIds,
|
|
154
|
-
streaming: facts.streaming,
|
|
155
|
-
}),
|
|
156
|
-
onTaskSettled: (taskId) => {
|
|
157
|
-
logger.info(`[a2a] task settled ${taskId}`);
|
|
158
|
-
inbound.settle(taskId);
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
holder.server = server;
|
|
162
|
-
const routes = new A2aRoutes(webServer, server);
|
|
163
|
-
holder.routes = routes;
|
|
164
|
-
if (holder.enabled)
|
|
165
|
-
routes.enable();
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
logger.error(`a2a: server half failed to build: ${err.message}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// ── outbound client half (after the inbound routes exist, so a
|
|
172
|
-
// loopback agent can fetch this server's own card) ──────────────────
|
|
173
|
-
const toolsService = probeService(ctx, 'tools', 'get');
|
|
174
|
-
if (toolsService === undefined) {
|
|
175
|
-
logger.warn('a2a: tools service not mounted; outbound client idle');
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
const registry = new OutboundAgentRegistry({
|
|
179
|
-
registrar: { register: (def) => toolsService.register(def) },
|
|
180
|
-
store: new DomainAgentStore(domain.agents),
|
|
181
|
-
toolPrefix,
|
|
182
|
-
tokenOf: (env) => (env ? process.env[env] : undefined),
|
|
183
|
-
onError: (message) => logger.warn(message),
|
|
184
|
-
});
|
|
185
|
-
holder.registry = registry;
|
|
186
|
-
registry.loadAll();
|
|
187
|
-
// Declared agents seed the registry: the persisted store stays the
|
|
188
|
-
// runtime state, so a disabled or removed agent is not reconnected.
|
|
189
|
-
if (clientConfig.agents.length > 0)
|
|
190
|
-
await registry.seed(clientConfig.agents);
|
|
191
|
-
}
|
|
192
|
-
// ── service facade + commands ──────────────────────────────────────
|
|
193
|
-
const impl = makeFacade(holder, logger.info.bind(logger));
|
|
51
|
+
const tools = probeService(ctx, 'tools', 'register');
|
|
52
|
+
const agents = probeService(ctx, 'agents', 'create');
|
|
53
|
+
const agentPresets = probeService(ctx, 'agentPresets', 'list');
|
|
54
|
+
const subagents = probeService(ctx, 'subagents', 'start');
|
|
55
|
+
const commandsRef = probeService(ctx, 'commands', 'register');
|
|
56
|
+
const baseUrl = config.baseUrl ?? `http://127.0.0.1:${webServerPort(webServer) ?? process.env['DSH_A2A_PORT'] ?? '3000'}`;
|
|
57
|
+
// ── inbound manager ───────────────────────────────────────────────
|
|
58
|
+
const inboundHost = {
|
|
59
|
+
ctx,
|
|
60
|
+
webServer: webServer ?? { register: noopRegister },
|
|
61
|
+
tasks: store,
|
|
62
|
+
logger,
|
|
63
|
+
...(agents !== undefined ? { agents } : {}),
|
|
64
|
+
...(agentPresets !== undefined ? { agentPresets } : {}),
|
|
65
|
+
...(subagents !== undefined ? { subagents } : {}),
|
|
66
|
+
resolveDefaultModel: () => resolveDefaultModel(ctx),
|
|
67
|
+
sessionCwd: inboundCwd,
|
|
68
|
+
defaultBaseUrl: baseUrl,
|
|
69
|
+
subagentProvider,
|
|
70
|
+
newId: () => crypto.randomUUID().slice(0, 8),
|
|
71
|
+
};
|
|
72
|
+
const inboundManager = new InboundServerManager(inboundHost, new DomainInboundStore(domain.inbound_servers));
|
|
73
|
+
inboundManager.boot();
|
|
74
|
+
// ── outbound manager ──────────────────────────────────────────────
|
|
75
|
+
const outboundHost = {
|
|
76
|
+
registrar: { register: (def) => tools?.register(def) },
|
|
77
|
+
tokenOf: (env) => (env ? process.env[env] : undefined),
|
|
78
|
+
onError: (message) => logger.warn(message),
|
|
79
|
+
defaultTimeoutMs: config.defaultTimeoutMs,
|
|
80
|
+
};
|
|
81
|
+
const outboundManager = new OutboundServerManager(outboundHost, domain.agents, new DomainOutboundStore(domain.outbound_servers), () => `out-${crypto.randomUUID().slice(0, 12)}`);
|
|
82
|
+
outboundManager.boot();
|
|
83
|
+
// ── service facade + commands + GUI API ───────────────────────────
|
|
84
|
+
const impl = makeFacade({ domain, store, inboundManager, outboundManager, agentPresets, logger: logger.info.bind(logger) });
|
|
194
85
|
new A2AService(ctx, impl);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
ctx.effect(() => commands.register(buildA2aCommand(impl)), 'a2a: command');
|
|
86
|
+
if (commandsRef !== undefined) {
|
|
87
|
+
ctx.effect(() => commandsRef.register(buildA2aCommand(impl)), 'a2a: command');
|
|
198
88
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (dashboardWebServer !== undefined) {
|
|
202
|
-
ctx.effect(() => dashboardWebServer.register({
|
|
89
|
+
if (webServer !== undefined) {
|
|
90
|
+
ctx.effect(() => webServer.register({
|
|
203
91
|
kind: 'prefix',
|
|
204
92
|
path: '/a2a/api',
|
|
205
93
|
handler: (req, res) => handleApiRequest(req, res, impl),
|
|
@@ -209,131 +97,133 @@ export function apply(ctx, config) {
|
|
|
209
97
|
logger.warn('a2a: webServer not mounted; GUI dashboard API idle');
|
|
210
98
|
}
|
|
211
99
|
return async () => {
|
|
212
|
-
|
|
213
|
-
await
|
|
214
|
-
await holder.registry?.disposeAll();
|
|
100
|
+
inboundManager.disposeAll();
|
|
101
|
+
await outboundManager.disposeAll();
|
|
215
102
|
await domain.close();
|
|
216
103
|
};
|
|
217
|
-
}, 'a2a:
|
|
104
|
+
}, 'a2a: multi-instance composition');
|
|
218
105
|
});
|
|
219
106
|
}
|
|
220
|
-
/** Facade closure over the
|
|
221
|
-
function makeFacade(
|
|
222
|
-
const
|
|
107
|
+
/** Facade closure over the two managers (commands and `ctx.a2a` consumers). */
|
|
108
|
+
function makeFacade(host) {
|
|
109
|
+
const inboundView = (id) => {
|
|
110
|
+
const live = host.inboundManager.get(id);
|
|
111
|
+
if (live === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
const r = live.record;
|
|
114
|
+
return {
|
|
115
|
+
id: r.id,
|
|
116
|
+
name: r.name,
|
|
117
|
+
description: r.description,
|
|
118
|
+
version: r.version,
|
|
119
|
+
endpointPath: live.endpointPath,
|
|
120
|
+
...(r.preset !== undefined ? { preset: r.preset } : {}),
|
|
121
|
+
...(r.authTokenEnv !== undefined ? { authTokenEnv: r.authTokenEnv } : {}),
|
|
122
|
+
cardPath: live.cardPath,
|
|
123
|
+
...(live.card.supportedInterfaces?.[0]?.url !== undefined ? { cardUrl: live.card.supportedInterfaces[0].url } : {}),
|
|
124
|
+
enabled: live.routes.active,
|
|
125
|
+
skills: r.skills.map((s) => ({ id: s.id, name: s.name, ...(s.description != null ? { description: s.description } : {}) })),
|
|
126
|
+
};
|
|
127
|
+
};
|
|
223
128
|
return {
|
|
224
129
|
status() {
|
|
225
130
|
return {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
skills: holder.card?.skills?.map((s) => s.id) ?? [],
|
|
230
|
-
executors: holder.executors ? ['session', ...(holder.executors['subagent'] !== undefined ? ['subagent'] : [])] : [],
|
|
231
|
-
name: holder.card?.name,
|
|
232
|
-
description: holder.card?.description,
|
|
233
|
-
version: holder.card?.version,
|
|
234
|
-
// True when a persisted identity override exists (the dashboard has
|
|
235
|
-
// been configured); guides the first-run onboarding.
|
|
236
|
-
configured: readIdentity(holder.domain) !== undefined,
|
|
237
|
-
},
|
|
238
|
-
tasks: holder.store.list().length,
|
|
239
|
-
agents: holder.registry?.list() ?? [],
|
|
240
|
-
inbounds: holder.inbound?.list() ?? [],
|
|
131
|
+
inbounds: host.inboundManager.list().map((live) => inboundView(live.id)).filter((v) => v !== undefined),
|
|
132
|
+
outbounds: host.outboundManager.list(),
|
|
133
|
+
tasks: host.store.list().length,
|
|
241
134
|
};
|
|
242
135
|
},
|
|
243
|
-
async
|
|
244
|
-
if (
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
136
|
+
async presets() {
|
|
137
|
+
if (host.agentPresets === undefined)
|
|
138
|
+
return [];
|
|
139
|
+
try {
|
|
140
|
+
return (await host.agentPresets.list()).map((p) => ({
|
|
141
|
+
id: p.id,
|
|
142
|
+
...(p.name !== undefined ? { name: p.name } : {}),
|
|
143
|
+
...(p.description !== undefined ? { description: p.description } : {}),
|
|
144
|
+
...(p.isDefault === true ? { isDefault: true } : {}),
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
252
150
|
},
|
|
253
|
-
|
|
254
|
-
|
|
151
|
+
// ── inbound ─────────────────────────────────────────────────────────
|
|
152
|
+
listInboundServers() {
|
|
153
|
+
return host.inboundManager.list().map((live) => inboundView(live.id)).filter((v) => v !== undefined);
|
|
255
154
|
},
|
|
256
|
-
|
|
257
|
-
|
|
155
|
+
async createInboundServer(input) {
|
|
156
|
+
const result = await host.inboundManager.add({
|
|
157
|
+
name: input.name,
|
|
158
|
+
description: input.description,
|
|
159
|
+
version: input.version,
|
|
160
|
+
...(input.endpointPath !== undefined ? { endpointPath: input.endpointPath } : {}),
|
|
161
|
+
...(input.preset !== undefined ? { preset: input.preset } : {}),
|
|
162
|
+
...(input.authTokenEnv !== undefined ? { authTokenEnv: input.authTokenEnv } : {}),
|
|
163
|
+
...(input.skills !== undefined ? { skills: input.skills } : {}),
|
|
164
|
+
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
|
165
|
+
});
|
|
166
|
+
host.logger(`[a2a] inbound created: ${result.message}`);
|
|
167
|
+
return result;
|
|
258
168
|
},
|
|
259
|
-
async
|
|
260
|
-
|
|
261
|
-
if (record === undefined)
|
|
262
|
-
return { ok: false, message: `task ${taskId} not found` };
|
|
263
|
-
const aborted = holder.server?.abort?.(taskId) ?? false;
|
|
264
|
-
return { ok: true, message: aborted ? `task ${taskId} canceled` : `task ${taskId} already terminal` };
|
|
169
|
+
async removeInboundServer(id) {
|
|
170
|
+
return host.inboundManager.remove(id);
|
|
265
171
|
},
|
|
266
|
-
|
|
267
|
-
return
|
|
172
|
+
async setInboundServerEnabled(id, enabled) {
|
|
173
|
+
return host.inboundManager.setEnabled(id, enabled);
|
|
268
174
|
},
|
|
269
|
-
async
|
|
270
|
-
|
|
271
|
-
if (registry === undefined)
|
|
272
|
-
return { ok: false, message: 'outbound client not mounted (no tools service)' };
|
|
273
|
-
const result = await registry.add({ ...spec, enabled: true, timeoutMs: 60000 });
|
|
274
|
-
return result;
|
|
175
|
+
async updateInboundServer(id, patch) {
|
|
176
|
+
return host.inboundManager.update(id, patch);
|
|
275
177
|
},
|
|
276
|
-
|
|
277
|
-
|
|
178
|
+
// ── outbound ────────────────────────────────────────────────────────
|
|
179
|
+
listOutboundServers() {
|
|
180
|
+
return [...host.outboundManager.list()];
|
|
278
181
|
},
|
|
279
|
-
async
|
|
280
|
-
return
|
|
182
|
+
async createOutboundServer(input) {
|
|
183
|
+
return host.outboundManager.add({
|
|
184
|
+
...(input.id !== undefined ? { id: input.id } : {}),
|
|
185
|
+
name: input.name,
|
|
186
|
+
agentCardUrl: input.agentCardUrl,
|
|
187
|
+
...(input.bearerTokenEnv !== undefined ? { bearerTokenEnv: input.bearerTokenEnv } : {}),
|
|
188
|
+
...(input.preset !== undefined ? { preset: input.preset } : {}),
|
|
189
|
+
enabled: input.enabled ?? true,
|
|
190
|
+
timeoutMs: input.timeoutMs ?? host.outboundManager.defaultTimeoutMs,
|
|
191
|
+
});
|
|
281
192
|
},
|
|
282
|
-
async
|
|
283
|
-
return
|
|
193
|
+
async removeOutboundServer(id) {
|
|
194
|
+
return host.outboundManager.remove(id);
|
|
284
195
|
},
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const persisted = readIdentity(holder.domain);
|
|
288
|
-
return {
|
|
289
|
-
...(persisted ?? {}),
|
|
290
|
-
// Composition defaults shown when nothing is persisted yet.
|
|
291
|
-
defaults: holder.identityDefaults,
|
|
292
|
-
name: current?.name ?? holder.identityDefaults.name,
|
|
293
|
-
description: current?.description ?? holder.identityDefaults.description,
|
|
294
|
-
version: current?.version ?? holder.identityDefaults.version,
|
|
295
|
-
};
|
|
196
|
+
async setOutboundServerEnabled(id, enabled) {
|
|
197
|
+
return host.outboundManager.setEnabled(id, enabled);
|
|
296
198
|
},
|
|
297
|
-
async
|
|
298
|
-
|
|
299
|
-
const server = holder.server;
|
|
300
|
-
if (card === undefined || server === undefined)
|
|
301
|
-
return { ok: false, message: 'inbound server not mounted (no card)' };
|
|
302
|
-
const persisted = readIdentity(holder.domain);
|
|
303
|
-
const base = persisted ?? holder.identityDefaults;
|
|
304
|
-
const next = {
|
|
305
|
-
name: patch.name?.trim() || base.name,
|
|
306
|
-
description: patch.description?.trim() || base.description,
|
|
307
|
-
version: patch.version?.trim() || base.version,
|
|
308
|
-
};
|
|
309
|
-
writeIdentity(holder.domain, next);
|
|
310
|
-
const rebuilt = rebuildCardWithIdentity(card, next, card.skills ?? []);
|
|
311
|
-
server.setCard(rebuilt);
|
|
312
|
-
holder.card = rebuilt;
|
|
313
|
-
log(`[a2a] identity updated: ${next.name}`);
|
|
314
|
-
return { ok: true, message: `service identity updated (${next.name})` };
|
|
199
|
+
async refreshOutboundServer(id) {
|
|
200
|
+
return host.outboundManager.refresh(id);
|
|
315
201
|
},
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
await holder.server?.abort?.(taskId);
|
|
323
|
-
}
|
|
324
|
-
return inbound.closePeer(peerId);
|
|
202
|
+
// ── tasks ───────────────────────────────────────────────────────────
|
|
203
|
+
getTask(taskId) {
|
|
204
|
+
return host.store.get(taskId);
|
|
205
|
+
},
|
|
206
|
+
listTasks() {
|
|
207
|
+
return host.store.list();
|
|
325
208
|
},
|
|
209
|
+
async cancelTask(taskId) {
|
|
210
|
+
const live = host.inboundManager.list().find((l) => l.server.abort(taskId));
|
|
211
|
+
return { ok: live !== undefined, message: live !== undefined ? `task ${taskId} canceled` : `task ${taskId} not found or already terminal` };
|
|
212
|
+
},
|
|
213
|
+
// ── inbound peers (aggregated across instances) ─────────────────────
|
|
326
214
|
inbounds() {
|
|
327
|
-
return
|
|
215
|
+
return host.inboundManager.list().flatMap((live) => live.inbound.list());
|
|
328
216
|
},
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
217
|
+
async closeInbound(peerId) {
|
|
218
|
+
for (const live of host.inboundManager.list()) {
|
|
219
|
+
const peers = live.inbound.activeTasksOf(peerId);
|
|
220
|
+
for (const taskId of peers)
|
|
221
|
+
live.server.abort(taskId);
|
|
222
|
+
const result = live.inbound.closePeer(peerId);
|
|
223
|
+
if (result)
|
|
224
|
+
return { ok: true, message: `inbound peer ${peerId} closed` };
|
|
225
|
+
}
|
|
226
|
+
return { ok: false, message: `inbound peer ${peerId} not found` };
|
|
337
227
|
},
|
|
338
228
|
};
|
|
339
229
|
}
|
|
@@ -354,6 +244,17 @@ function inboundSessionCwd() {
|
|
|
354
244
|
}
|
|
355
245
|
return dir;
|
|
356
246
|
}
|
|
247
|
+
function noopRegister() {
|
|
248
|
+
return () => { };
|
|
249
|
+
}
|
|
250
|
+
/** The listening port of a mounted webServer (undefined when unavailable). */
|
|
251
|
+
function webServerPort(webServer) {
|
|
252
|
+
const candidate = webServer;
|
|
253
|
+
if (candidate === undefined)
|
|
254
|
+
return undefined;
|
|
255
|
+
const value = typeof candidate.port === 'function' ? candidate.port() : candidate.port;
|
|
256
|
+
return typeof value === 'number' && Number.isFinite(value) ? String(value) : undefined;
|
|
257
|
+
}
|
|
357
258
|
export { A2AServer } from "./server/a2a-server.js";
|
|
358
259
|
export { A2AClient, A2AError } from "./outbound/calls.js";
|
|
359
260
|
export { A2AService } from "./service.js";
|