@hanphone/dsh-a2a 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +101 -106
  2. package/README.zh.md +50 -74
  3. package/cordis.patch.yml +4 -7
  4. package/lib/client.js +217 -73
  5. package/lib/index.js +1258 -867
  6. package/lib/types/api.d.ts +40 -23
  7. package/lib/types/api.d.ts.map +1 -1
  8. package/lib/types/api.js +58 -23
  9. package/lib/types/api.js.map +1 -1
  10. package/lib/types/client/index.d.ts +50 -19
  11. package/lib/types/client/index.d.ts.map +1 -1
  12. package/lib/types/commands.d.ts +4 -2
  13. package/lib/types/commands.d.ts.map +1 -1
  14. package/lib/types/commands.js +59 -46
  15. package/lib/types/commands.js.map +1 -1
  16. package/lib/types/events.d.ts +32 -9
  17. package/lib/types/events.d.ts.map +1 -1
  18. package/lib/types/index.d.ts +21 -32
  19. package/lib/types/index.d.ts.map +1 -1
  20. package/lib/types/index.js +173 -272
  21. package/lib/types/index.js.map +1 -1
  22. package/lib/types/outbound/calls.d.ts.map +1 -1
  23. package/lib/types/outbound/calls.js +8 -6
  24. package/lib/types/outbound/calls.js.map +1 -1
  25. package/lib/types/protocol.d.ts +174 -109
  26. package/lib/types/protocol.d.ts.map +1 -1
  27. package/lib/types/protocol.js +58 -34
  28. package/lib/types/protocol.js.map +1 -1
  29. package/lib/types/server/a2a-server.d.ts +4 -0
  30. package/lib/types/server/a2a-server.d.ts.map +1 -1
  31. package/lib/types/server/a2a-server.js +6 -2
  32. package/lib/types/server/a2a-server.js.map +1 -1
  33. package/lib/types/server/card.d.ts +6 -30
  34. package/lib/types/server/card.d.ts.map +1 -1
  35. package/lib/types/server/card.js +13 -49
  36. package/lib/types/server/card.js.map +1 -1
  37. package/lib/types/server/exec/agent-runtime.d.ts +3 -0
  38. package/lib/types/server/exec/agent-runtime.d.ts.map +1 -1
  39. package/lib/types/server/exec/agent-runtime.js +2 -1
  40. package/lib/types/server/exec/agent-runtime.js.map +1 -1
  41. package/lib/types/server/routes.d.ts +5 -1
  42. package/lib/types/server/routes.d.ts.map +1 -1
  43. package/lib/types/server/routes.js +9 -5
  44. package/lib/types/server/routes.js.map +1 -1
  45. package/lib/types/server/store.d.ts +35 -1
  46. package/lib/types/server/store.d.ts.map +1 -1
  47. package/lib/types/server/store.js +10 -0
  48. package/lib/types/server/store.js.map +1 -1
  49. package/lib/types/servers/inbound-manager.d.ts +131 -0
  50. package/lib/types/servers/inbound-manager.d.ts.map +1 -0
  51. package/lib/types/servers/inbound-manager.js +312 -0
  52. package/lib/types/servers/inbound-manager.js.map +1 -0
  53. package/lib/types/servers/outbound-manager.d.ts +129 -0
  54. package/lib/types/servers/outbound-manager.d.ts.map +1 -0
  55. package/lib/types/servers/outbound-manager.js +238 -0
  56. package/lib/types/servers/outbound-manager.js.map +1 -0
  57. package/lib/types/service.d.ts +106 -40
  58. package/lib/types/service.d.ts.map +1 -1
  59. package/lib/types/service.js +36 -27
  60. package/lib/types/service.js.map +1 -1
  61. package/package.json +2 -2
  62. package/src/api.ts +67 -53
  63. package/src/client/index.ts +309 -148
  64. package/src/commands.ts +59 -43
  65. package/src/events.ts +14 -7
  66. package/src/index.ts +199 -310
  67. package/src/outbound/calls.ts +8 -6
  68. package/src/protocol.ts +204 -95
  69. package/src/server/a2a-server.ts +9 -2
  70. package/src/server/card.ts +13 -62
  71. package/src/server/exec/agent-runtime.ts +5 -2
  72. package/src/server/routes.ts +6 -4
  73. package/src/server/store.ts +45 -4
  74. package/src/servers/inbound-manager.ts +385 -0
  75. package/src/servers/outbound-manager.ts +289 -0
  76. package/src/service.ts +125 -40
  77. package/lib/tsconfig.client.tsbuildinfo +0 -1
  78. package/lib/tsconfig.tsbuildinfo +0 -1
@@ -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: the inbound server (AgentCard derived
5
- * from the live tool registry, JSON-RPC + SSE, durable SQLite task store,
6
- * session/subagent executors, the `a2a/inbound-task` policy gate + audit)
7
- * and the outbound client (persisted multi-agent registry, skills mapped to
8
- * model tools, sync calls with per-agent timeout). Optional services are
9
- * probed rather than injected so a composition lacking one half idles just
10
- * that half; only the storage domain is required.
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 OutboundAgentSpec } from './outbound/registry.ts';
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
- server: {
25
- enabled: boolean;
26
- name: string;
27
- description: string;
28
- version: string;
29
- /** Advertised base URL; null/absent lets the host webServer address be used. */
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 { TaskState };
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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAexC,OAAO,EAA2C,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAIxG,OAAO,KAAK,EAAa,UAAU,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAErE,eAAO,MAAM,IAAI,QAAQ,CAAA;AAEzB,wEAAwE;AACxE,eAAO,MAAM,MAAM,4BAA6B,CAAA;AAEhD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE;QACN,OAAO,EAAE,OAAO,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,MAAM,CAAA;QACnB,OAAO,EAAE,MAAM,CAAA;QACf,gFAAgF;QAChF,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,YAAY,EAAE,MAAM,CAAA;QACpB,kFAAkF;QAClF,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,EAAE,CAAC;YAAC,OAAO,EAAE,MAAM,EAAE,CAAA;SAAE,CAAA;QAC5C,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC,CAAA;QACjD,gBAAgB,EAAE,MAAM,CAAA;KACzB,CAAA;IACD,MAAM,EAAE;QACN,MAAM,EAAE,iBAAiB,EAAE,CAAA;QAC3B,UAAU,EAAE,MAAM,CAAA;KACnB,CAAA;CACF;AAED,uEAAuE;AACvE,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,SAAS,CA0B9B,CAAA;AAiBF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,QAyKpD;AAyID,YAAY,EAAE,SAAS,EAAE,CAAA;AACzB,YAAY,EAAE,UAAU,EAAE,CAAA;AAC1B,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"}
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"}
@@ -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: the inbound server (AgentCard derived
5
- * from the live tool registry, JSON-RPC + SSE, durable SQLite task store,
6
- * session/subagent executors, the `a2a/inbound-task` policy gate + audit)
7
- * and the outbound client (persisted multi-agent registry, skills mapped to
8
- * model tools, sync calls with per-agent timeout). Optional services are
9
- * probed rather than injected so a composition lacking one half idles just
10
- * that half; only the storage domain is required.
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 { LiveInboundRegistry } from "./server/inbound-registry.js";
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
- server: z.object({
40
- enabled: z.boolean().default(true),
41
- name: z.string().default('My DSH Agent'),
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 serverConfig = { ...config.server };
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
- const holder = {
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
- if (webServer === undefined) {
96
- logger.warn('a2a: webServer not mounted; inbound server idle');
97
- }
98
- else {
99
- try {
100
- const skills = deriveSkills(probeService(ctx, 'tools', 'get') ?? { get: () => undefined }, { ids: serverConfig.skills.ids, exclude: serverConfig.skills.exclude });
101
- const baseUrl = serverConfig.baseUrl ?? `http://127.0.0.1:${process.env['DSH_A2A_PORT'] ?? '3000'}`;
102
- const endpointPath = serverConfig.endpointPath ?? '/a2a';
103
- const persistedIdentity = readIdentity(domain);
104
- const card = buildCard(cardOptionsFor(baseUrl, endpointPath, holder.identityDefaults, persistedIdentity, skills, authToken));
105
- holder.card = card;
106
- // Inbound connection registry (dashboard "who is talking to us").
107
- const inbound = new LiveInboundRegistry();
108
- holder.inbound = inbound;
109
- // Executors: probe the agent loop; refuse tasks readably without it.
110
- const agents = probeService(ctx, 'agents', 'create');
111
- const presets = probeService(ctx, 'agentPresets', 'resolve');
112
- const sessionPool = agents
113
- ? new ContextSessionPool(agents, {
114
- cwd: inboundCwd,
115
- ...(presets ? { agentPresets: presets } : {}),
116
- resolveAgentOptions: () => resolveDefaultModel(ctx),
117
- })
118
- : undefined;
119
- const sessionExecutor = sessionPool ? createSessionExecutor(sessionPool) : refuseExecutor('no agent loop mounted');
120
- const subagents = probeService(ctx, 'subagents', 'start');
121
- const subagentExecutor = sessionPool !== undefined && subagents !== undefined
122
- ? createSubagentExecutor({ pool: sessionPool, subagents, provider: subagentProvider })
123
- : undefined;
124
- const executors = new ExecutorSet(serverConfig.executors, sessionExecutor, subagentExecutor);
125
- holder.executors = executors;
126
- const skillIds = new Set(skills.map((s) => s.id));
127
- const gate = async (input) => {
128
- if (!skillIds.has(input.skill)) {
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
- const commands = probeService(ctx, 'commands', 'register');
196
- if (commands !== undefined) {
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
- // ── GUI dashboard API (loopback-only /a2a/api) ─────────────────────
200
- const dashboardWebServer = probeService(ctx, 'webServer', 'register');
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
- holder.routes?.dispose();
213
- await holder.executors?.disposeAll();
214
- await holder.registry?.disposeAll();
100
+ inboundManager.disposeAll();
101
+ await outboundManager.disposeAll();
215
102
  await domain.close();
216
103
  };
217
- }, 'a2a: domain + halves');
104
+ }, 'a2a: multi-instance composition');
218
105
  });
219
106
  }
220
- /** Facade closure over the shared holder (commands and `ctx.a2a` consumers). */
221
- function makeFacade(holder, log) {
222
- const serverEnabled = () => holder.routes?.active ?? false;
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
- server: {
227
- enabled: serverEnabled(),
228
- cardUrl: holder.card?.supportedInterfaces?.[0]?.url,
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 enableServer(enable) {
244
- if (holder.routes === undefined)
245
- return { ok: false, message: 'inbound server is not mounted (no webServer)' };
246
- if (enable)
247
- holder.routes.enable();
248
- else
249
- holder.routes.disable();
250
- log(`[a2a] server ${enable ? 'enabled' : 'disabled'}`);
251
- return { ok: true, message: `server ${enable ? 'enabled' : 'disabled'}` };
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
- getTask(taskId) {
254
- return holder.store.get(taskId);
151
+ // ── inbound ─────────────────────────────────────────────────────────
152
+ listInboundServers() {
153
+ return host.inboundManager.list().map((live) => inboundView(live.id)).filter((v) => v !== undefined);
255
154
  },
256
- listTasks() {
257
- return holder.store.list();
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 cancelTask(taskId) {
260
- const record = holder.store.get(taskId);
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
- agents() {
267
- return holder.registry?.list() ?? [];
172
+ async setInboundServerEnabled(id, enabled) {
173
+ return host.inboundManager.setEnabled(id, enabled);
268
174
  },
269
- async addAgent(spec) {
270
- const registry = holder.registry;
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
- async removeAgent(id) {
277
- return holder.registry?.remove(id) ?? { ok: false, message: 'outbound client not mounted' };
178
+ // ── outbound ────────────────────────────────────────────────────────
179
+ listOutboundServers() {
180
+ return [...host.outboundManager.list()];
278
181
  },
279
- async setAgentEnabled(id, enabled) {
280
- return holder.registry?.setEnabled(id, enabled) ?? { ok: false, message: 'outbound client not mounted' };
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 refreshAgentCard(id) {
283
- return holder.registry?.refresh(id) ?? { ok: false, message: 'outbound client not mounted' };
193
+ async removeOutboundServer(id) {
194
+ return host.outboundManager.remove(id);
284
195
  },
285
- identity() {
286
- const current = holder.card;
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 updateIdentity(patch) {
298
- const card = holder.card;
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
- async closeInbound(peerId) {
317
- const inbound = holder.inbound;
318
- if (inbound === undefined)
319
- return { ok: false, message: 'inbound registry not mounted' };
320
- // Cancel the peer's active tasks, then drop the record.
321
- for (const taskId of inbound.activeTasksOf(peerId)) {
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 holder.inbound?.list() ?? [];
215
+ return host.inboundManager.list().flatMap((live) => live.inbound.list());
328
216
  },
329
- };
330
- }
331
- /** A task that refuses readably on compositions without an agent loop. */
332
- function refuseExecutor(reason) {
333
- return {
334
- name: 'refuse',
335
- async execute() {
336
- throw new Error(`a2a: ${reason}; refusing inbound task`);
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";