@hanphone/dsh-a2a 0.2.0 → 0.3.1

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 +106 -106
  2. package/README.zh.md +50 -74
  3. package/cordis.patch.yml +4 -7
  4. package/lib/client.js +186 -73
  5. package/lib/index.js +1275 -868
  6. package/lib/types/api.d.ts +30 -23
  7. package/lib/types/api.d.ts.map +1 -1
  8. package/lib/types/api.js +56 -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 +177 -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 +20 -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 +32 -0
  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 +154 -0
  50. package/lib/types/servers/inbound-manager.d.ts.map +1 -0
  51. package/lib/types/servers/inbound-manager.js +335 -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 +104 -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 +65 -53
  63. package/src/client/index.ts +272 -148
  64. package/src/commands.ts +59 -43
  65. package/src/events.ts +14 -7
  66. package/src/index.ts +203 -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 +20 -2
  72. package/src/server/routes.ts +6 -4
  73. package/src/server/store.ts +42 -3
  74. package/src/servers/inbound-manager.ts +416 -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
package/src/index.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: 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).
@@ -19,246 +24,109 @@ import z from '@deepseek-ai/schemastery'
19
24
  import { homedir } from 'node:os'
20
25
  import { join } from 'node:path'
21
26
  import { mkdirSync } from 'node:fs'
22
- import { deriveSkills, buildCard, type ToolGetter } from './server/card.ts'
23
27
  import { openDomain, DomainTaskStore, type A2aDomain, type TaskStore } from './server/store.ts'
24
- import { LiveInboundRegistry, type InboundRegistry } from './server/inbound-registry.ts'
25
- import { cardOptionsFor, readIdentity, rebuildCardWithIdentity, writeIdentity, type A2aIdentity } from './server/identity.ts'
26
- import { ExecutorSet } from './server/executor.ts'
27
- import { ContextSessionPool, probeService, type AgentPresetsLike, type AgentRegistryLike } from './server/exec/agent-runtime.ts'
28
- import { createSessionExecutor } from './server/exec/session.ts'
29
- import { createSubagentExecutor, type SubagentsLike } from './server/exec/subagent.ts'
30
- import { A2AServer, type GateInput, type GateResult } from './server/a2a-server.ts'
31
- import { A2aRoutes } from './server/routes.ts'
28
+ import { probeService, type AgentPresetsLike, type AgentRegistryLike, type SkillsLike } from './server/exec/agent-runtime.ts'
29
+ import type { SubagentsLike } from './server/exec/subagent.ts'
32
30
  import { handleApiRequest } from './api.ts'
33
- import { OutboundAgentRegistry, DomainAgentStore, type OutboundAgentSpec } from './outbound/registry.ts'
34
- import { A2AService, type A2AServiceImpl, type OpResult } from './service.ts'
31
+ import { A2AService, type A2AServiceImpl, type InboundCreateInput, type InboundServerView, type OutboundCreateInput, type OpResult, type PresetView, type SkillView } from './service.ts'
35
32
  import { buildA2aCommand } from './commands.ts'
36
- import type { InboundTaskDecision } from './events.ts'
37
- import type { AgentCard, AgentSkill, TaskState } from './protocol.ts'
33
+ import { InboundServerManager, DomainInboundStore, type InboundManagerHost, type WebServerLike as InboundWebServerLike } from './servers/inbound-manager.ts'
34
+ import { OutboundServerManager, DomainOutboundStore, type OutboundManagerHost, type OutboundServerView } from './servers/outbound-manager.ts'
38
35
 
39
36
  export const name = 'a2a'
40
37
 
41
38
  /** Only the storage domain is required; the other halves are probed. */
42
39
  export const inject = ['storageDomain'] as const
43
40
 
41
+ /** Minimal host-level config; per-instance setup happens in the GUI/domain. */
44
42
  export interface A2AConfig {
45
- server: {
46
- enabled: boolean
47
- name: string
48
- description: string
49
- version: string
50
- /** Advertised base URL; null/absent lets the host webServer address be used. */
51
- baseUrl?: string
52
- endpointPath: string
53
- /** Environment variable name for the inbound bearer token; absent = anonymous. */
54
- authTokenEnv?: string
55
- skills: { ids: string[]; exclude: string[] }
56
- executors: Record<string, 'session' | 'subagent'>
57
- subagentProvider: string
58
- }
59
- client: {
60
- agents: OutboundAgentSpec[]
61
- toolPrefix: string
62
- }
43
+ /** Advertised base URL for every inbound card; absent lets a default stand. */
44
+ baseUrl?: string
45
+ /** Subagent driver provider for the inbound subagent executors. */
46
+ subagentProvider: string
47
+ /** Default outbound connection timeout (ms). */
48
+ defaultTimeoutMs: number
63
49
  }
64
50
 
65
51
  /** Loader-validated config schema (defaults applied by the Loader). */
66
52
  export const Config: z<A2AConfig> = z.object({
67
- server: z.object({
68
- enabled: z.boolean().default(true),
69
- name: z.string().default('My DSH Agent'),
70
- description: z.string().default('A DeepSeek Harness agent exposed over A2A v1.0'),
71
- version: z.string().default('0.1.0'),
72
- baseUrl: z.string(),
73
- endpointPath: z.string().default('/a2a'),
74
- authTokenEnv: z.string(),
75
- skills: z.object({
76
- ids: z.array(z.string()).default([]),
77
- exclude: z.array(z.string()).default([]),
78
- }),
79
- executors: z.dict(z.union(['session', 'subagent'] as const)).default({ chat: 'session' }),
80
- subagentProvider: z.string().default('in-process'),
81
- }),
82
- client: z.object({
83
- agents: z.array(z.object({
84
- name: z.string(),
85
- agentCardUrl: z.string(),
86
- bearerTokenEnv: z.string(),
87
- enabled: z.boolean().default(true),
88
- timeoutMs: z.number().default(60000),
89
- })).default([]),
90
- toolPrefix: z.string().default('a2a'),
91
- }),
53
+ baseUrl: z.string(),
54
+ subagentProvider: z.string().default('in-process'),
55
+ defaultTimeoutMs: z.number().default(60_000),
92
56
  })
93
57
 
94
- /** Mutable state shared across the plugin's halves. */
95
- interface Shared {
96
- readonly domain: A2aDomain
97
- readonly store: TaskStore
98
- registry: OutboundAgentRegistry | undefined
99
- inbound: InboundRegistry | undefined
100
- server: A2AServer | undefined
101
- routes: A2aRoutes | undefined
102
- card: AgentCard | undefined
103
- executors: ExecutorSet | undefined
104
- enabled: boolean
105
- /** Composition defaults for the card identity (before any persisted override). */
106
- identityDefaults: { name: string; description: string; version: string }
107
- }
108
-
109
58
  export function apply(ctx: Context, config: A2AConfig) {
110
59
  const logger = ctx.logger('a2a')
111
- const serverConfig = { ...config.server }
112
- const clientConfig = { ...config.client }
113
- const authToken = serverConfig.authTokenEnv !== undefined ? process.env[serverConfig.authTokenEnv] : undefined
114
- const toolPrefix = clientConfig.toolPrefix
115
- const subagentProvider = serverConfig.subagentProvider
60
+ const subagentProvider = config.subagentProvider
116
61
  const inboundCwd = inboundSessionCwd()
117
62
 
118
63
  ctx.inject(['storageDomain'], (ctx) => {
119
64
  ctx.effect(async () => {
120
65
  const domain = await openDomain(ctx.storageDomain)
121
66
  const store = new DomainTaskStore(domain)
122
- const holder: Shared = {
123
- domain,
124
- store,
125
- registry: undefined,
126
- inbound: undefined,
127
- server: undefined,
128
- routes: undefined,
129
- card: undefined,
130
- executors: undefined,
131
- enabled: serverConfig.enabled,
132
- identityDefaults: {
133
- name: serverConfig.name ?? 'My DSH Agent',
134
- description: serverConfig.description ?? 'A DeepSeek Harness agent exposed over A2A v1.0',
135
- version: serverConfig.version ?? '0.1.0',
136
- },
137
- }
138
67
 
139
- // ── inbound server half ────────────────────────────────────────────
140
- const webServer = probeService(ctx, 'webServer', 'register') as
141
- | { register(route: { kind: 'exact' | 'prefix'; path: string; handler(...args: unknown[]): unknown }): () => void }
68
+ // Probe the optional host services each half uses.
69
+ const webServer = probeService(ctx, 'webServer', 'register') as InboundWebServerLike | undefined
70
+ const tools = probeService(ctx, 'tools', 'register') as
71
+ | ({ get(name: string): unknown } & { register(def: unknown): (() => void) | void })
72
+ | undefined
73
+ const agents = probeService(ctx, 'agents', 'create') as AgentRegistryLike | undefined
74
+ const agentPresets = probeService(ctx, 'agentPresets', 'list') as
75
+ | (AgentPresetsLike & { list(): Promise<Array<{ id: string; name?: string; description?: string }>>; readonly defaultId?: string })
76
+ | undefined
77
+ const skills = probeService(ctx, 'skills', 'list') as SkillsLike | undefined
78
+ const subagents = probeService(ctx, 'subagents', 'start') as SubagentsLike | undefined
79
+ const commandsRef = probeService(ctx, 'commands', 'register') as
80
+ | { register(def: { name: string; description: string; handler(...args: unknown[]): unknown }): () => void }
142
81
  | undefined
143
- if (webServer === undefined) {
144
- logger.warn('a2a: webServer not mounted; inbound server idle')
145
- } else {
146
- try {
147
- const skills = deriveSkills(
148
- probeService(ctx, 'tools', 'get') as ToolGetter | undefined ?? { get: () => undefined },
149
- { ids: serverConfig.skills.ids, exclude: serverConfig.skills.exclude },
150
- )
151
- const baseUrl = serverConfig.baseUrl ?? `http://127.0.0.1:${process.env['DSH_A2A_PORT'] ?? '3000'}`
152
- const endpointPath = serverConfig.endpointPath ?? '/a2a'
153
- const persistedIdentity = readIdentity(domain)
154
- const card = buildCard(cardOptionsFor(baseUrl, endpointPath, holder.identityDefaults, persistedIdentity, skills, authToken))
155
- holder.card = card
156
-
157
- // Inbound connection registry (dashboard "who is talking to us").
158
- const inbound = new LiveInboundRegistry()
159
- holder.inbound = inbound
160
-
161
- // Executors: probe the agent loop; refuse tasks readably without it.
162
- const agents = probeService(ctx, 'agents', 'create') as AgentRegistryLike | undefined
163
- const presets = probeService(ctx, 'agentPresets', 'resolve') as AgentPresetsLike | undefined
164
- const sessionPool = agents
165
- ? new ContextSessionPool(agents, {
166
- cwd: inboundCwd,
167
- ...(presets ? { agentPresets: presets } : {}),
168
- resolveAgentOptions: () => resolveDefaultModel(ctx),
169
- })
170
- : undefined
171
- const sessionExecutor = sessionPool ? createSessionExecutor(sessionPool) : refuseExecutor('no agent loop mounted')
172
- const subagents = probeService(ctx, 'subagents', 'start') as SubagentsLike | undefined
173
- const subagentExecutor = sessionPool !== undefined && subagents !== undefined
174
- ? createSubagentExecutor({ pool: sessionPool, subagents, provider: subagentProvider })
175
- : undefined
176
- const executors = new ExecutorSet(serverConfig.executors, sessionExecutor, subagentExecutor)
177
- holder.executors = executors
178
82
 
179
- const skillIds = new Set(skills.map((s) => s.id))
180
- const gate = async (input: GateInput): Promise<GateResult> => {
181
- if (!skillIds.has(input.skill)) {
182
- return { ok: false, reason: `unknown skill "${input.skill}"` }
183
- }
184
- const decision: InboundTaskDecision = {
185
- contextId: input.contextId,
186
- skill: input.skill,
187
- parts: input.parts,
188
- remotePeerId: input.remotePeerId,
189
- }
190
- const decided = await ctx.waterfall('a2a/inbound-task', decision, async (d) => d)
191
- // Built-in audit: every task decision is logged with its source.
192
- logger.info(`[a2a] inbound skill=${decided.skill} remote=${decided.remotePeerId} rejected=${decided.rejected?.reason ?? 'no'}`)
193
- if (decided.rejected !== undefined) return { ok: false, reason: decided.rejected.reason }
194
- return { ok: true }
195
- }
83
+ const baseUrl = config.baseUrl ?? `http://127.0.0.1:${webServerPort(webServer) ?? process.env['DSH_A2A_PORT'] ?? '3000'}`
196
84
 
197
- const server = new A2AServer({
198
- card,
199
- store,
200
- executors,
201
- ...(authToken ? { authToken } : {}),
202
- gate,
203
- onInbound: (facts) => inbound.note({
204
- method: facts.method,
205
- ...(facts.source !== undefined ? { source: facts.source } : {}),
206
- taskIds: facts.taskIds,
207
- streaming: facts.streaming,
208
- }),
209
- onTaskSettled: (taskId) => {
210
- logger.info(`[a2a] task settled ${taskId}`)
211
- inbound.settle(taskId)
212
- },
213
- })
214
- holder.server = server
215
- const routes = new A2aRoutes(webServer as never, server)
216
- holder.routes = routes
217
- if (holder.enabled) routes.enable()
218
- } catch (err) {
219
- logger.error(`a2a: server half failed to build: ${(err as Error).message}`)
220
- }
85
+ // ── inbound manager ───────────────────────────────────────────────
86
+ const inboundHost: InboundManagerHost = {
87
+ ctx,
88
+ webServer: webServer ?? { register: noopRegister },
89
+ tasks: store,
90
+ logger,
91
+ ...(agents !== undefined ? { agents } : {}),
92
+ ...(agentPresets !== undefined ? { agentPresets } : {}),
93
+ ...(skills !== undefined ? { skills } : {}),
94
+ ...(subagents !== undefined ? { subagents } : {}),
95
+ ...(agentPresets?.defaultId !== undefined ? { defaultPresetId: agentPresets.defaultId } : {}),
96
+ resolveDefaultModel: () => resolveDefaultModel(ctx),
97
+ sessionCwd: inboundCwd,
98
+ defaultBaseUrl: baseUrl,
99
+ subagentProvider,
100
+ newId: () => crypto.randomUUID().slice(0, 8),
221
101
  }
102
+ const inboundManager = new InboundServerManager(inboundHost, new DomainInboundStore(domain.inbound_servers))
103
+ await inboundManager.boot()
222
104
 
223
- // ── outbound client half (after the inbound routes exist, so a
224
- // loopback agent can fetch this server's own card) ──────────────────
225
- const toolsService = probeService(ctx, 'tools', 'get') as
226
- | (ToolGetter & { register(def: unknown): (() => void) | void })
227
- | undefined
228
- if (toolsService === undefined) {
229
- logger.warn('a2a: tools service not mounted; outbound client idle')
230
- } else {
231
- const registry = new OutboundAgentRegistry({
232
- registrar: { register: (def) => toolsService.register(def) },
233
- store: new DomainAgentStore(domain.agents),
234
- toolPrefix,
235
- tokenOf: (env) => (env ? process.env[env] : undefined),
236
- onError: (message) => logger.warn(message),
237
- })
238
- holder.registry = registry
239
- registry.loadAll()
240
- // Declared agents seed the registry: the persisted store stays the
241
- // runtime state, so a disabled or removed agent is not reconnected.
242
- if (clientConfig.agents.length > 0) await registry.seed(clientConfig.agents)
105
+ // ── outbound manager ──────────────────────────────────────────────
106
+ const outboundHost: OutboundManagerHost = {
107
+ registrar: { register: (def) => tools?.register(def) },
108
+ tokenOf: (env) => (env ? process.env[env] : undefined),
109
+ onError: (message) => logger.warn(message),
110
+ defaultTimeoutMs: config.defaultTimeoutMs,
243
111
  }
112
+ const outboundManager = new OutboundServerManager(
113
+ outboundHost,
114
+ domain.agents,
115
+ new DomainOutboundStore(domain.outbound_servers),
116
+ () => `out-${crypto.randomUUID().slice(0, 12)}`,
117
+ )
118
+ outboundManager.boot()
244
119
 
245
- // ── service facade + commands ──────────────────────────────────────
246
- const impl: A2AServiceImpl = makeFacade(holder, logger.info.bind(logger))
120
+ // ── service facade + commands + GUI API ───────────────────────────
121
+ const impl: A2AServiceImpl = makeFacade({ domain, store, inboundManager, outboundManager, agentPresets, logger: logger.info.bind(logger) })
247
122
  new A2AService(ctx, impl)
248
123
 
249
- const commands = probeService(ctx, 'commands', 'register') as
250
- | { register(def: { name: string; description: string; handler(...args: unknown[]): unknown }): () => void }
251
- | undefined
252
- if (commands !== undefined) {
253
- ctx.effect(() => commands.register(buildA2aCommand(impl) as never), 'a2a: command')
124
+ if (commandsRef !== undefined) {
125
+ ctx.effect(() => commandsRef.register(buildA2aCommand(impl) as never), 'a2a: command')
254
126
  }
255
127
 
256
- // ── GUI dashboard API (loopback-only /a2a/api) ─────────────────────
257
- const dashboardWebServer = probeService(ctx, 'webServer', 'register') as
258
- | { register(route: { kind: 'exact' | 'prefix'; path: string; handler(...args: unknown[]): unknown }): () => void }
259
- | undefined
260
- if (dashboardWebServer !== undefined) {
261
- ctx.effect(() => dashboardWebServer.register({
128
+ if (webServer !== undefined) {
129
+ ctx.effect(() => webServer.register({
262
130
  kind: 'prefix',
263
131
  path: '/a2a/api',
264
132
  handler: (req: unknown, res: unknown) => handleApiRequest(req as never, res as never, impl),
@@ -268,126 +136,140 @@ export function apply(ctx: Context, config: A2AConfig) {
268
136
  }
269
137
 
270
138
  return async () => {
271
- holder.routes?.dispose()
272
- await holder.executors?.disposeAll()
273
- await holder.registry?.disposeAll()
139
+ inboundManager.disposeAll()
140
+ await outboundManager.disposeAll()
274
141
  await domain.close()
275
142
  }
276
- }, 'a2a: domain + halves')
143
+ }, 'a2a: multi-instance composition')
277
144
  })
278
145
  }
279
146
 
280
- /** Facade closure over the shared holder (commands and `ctx.a2a` consumers). */
281
- function makeFacade(holder: Shared, log: (message: string) => void): A2AServiceImpl {
282
- const serverEnabled = (): boolean => holder.routes?.active ?? false
147
+ interface FacadeHost {
148
+ readonly domain: A2aDomain
149
+ readonly store: TaskStore
150
+ readonly inboundManager: InboundServerManager
151
+ readonly outboundManager: OutboundServerManager
152
+ readonly agentPresets?: (AgentPresetsLike & { list(): Promise<Array<{ id: string; name?: string; description?: string }>>; readonly defaultId?: string }) | undefined
153
+ readonly logger: (message: string) => void
154
+ }
155
+
156
+ /** Facade closure over the two managers (commands and `ctx.a2a` consumers). */
157
+ function makeFacade(host: FacadeHost): A2AServiceImpl {
158
+ const inboundView = (id: string): InboundServerView | undefined => {
159
+ const live = host.inboundManager.get(id)
160
+ if (live === undefined) return undefined
161
+ const r = live.record
162
+ return {
163
+ id: r.id,
164
+ name: r.name,
165
+ description: r.description,
166
+ version: r.version,
167
+ endpointPath: live.endpointPath,
168
+ // Effective preset: the record's, else the deployment default.
169
+ ...(live.preset !== undefined ? { preset: live.preset } : {}),
170
+ ...(r.authTokenEnv !== undefined ? { authTokenEnv: r.authTokenEnv } : {}),
171
+ cardPath: live.cardPath,
172
+ ...(live.card.supportedInterfaces?.[0]?.url !== undefined ? { cardUrl: live.card.supportedInterfaces[0].url } : {}),
173
+ enabled: live.routes.active,
174
+ skills: live.skills.map((s): SkillView => ({ id: s.id, name: s.name, ...(s.description != null ? { description: s.description } : {}) })),
175
+ }
176
+ }
177
+
283
178
  return {
284
179
  status(): unknown {
285
180
  return {
286
- server: {
287
- enabled: serverEnabled(),
288
- cardUrl: holder.card?.supportedInterfaces?.[0]?.url,
289
- skills: holder.card?.skills?.map((s) => s.id) ?? [],
290
- executors: holder.executors ? ['session', ...(holder.executors['subagent'] !== undefined ? ['subagent'] : [])] : [],
291
- name: holder.card?.name,
292
- description: holder.card?.description,
293
- version: holder.card?.version,
294
- // True when a persisted identity override exists (the dashboard has
295
- // been configured); guides the first-run onboarding.
296
- configured: readIdentity(holder.domain) !== undefined,
297
- },
298
- tasks: holder.store.list().length,
299
- agents: holder.registry?.list() ?? [],
300
- inbounds: holder.inbound?.list() ?? [],
181
+ inbounds: host.inboundManager.list().map((live) => inboundView(live.id)).filter((v): v is InboundServerView => v !== undefined),
182
+ outbounds: host.outboundManager.list(),
183
+ tasks: host.store.list().length,
301
184
  }
302
185
  },
303
- async enableServer(enable: boolean): Promise<OpResult> {
304
- if (holder.routes === undefined) return { ok: false, message: 'inbound server is not mounted (no webServer)' }
305
- if (enable) holder.routes.enable()
306
- else holder.routes.disable()
307
- log(`[a2a] server ${enable ? 'enabled' : 'disabled'}`)
308
- return { ok: true, message: `server ${enable ? 'enabled' : 'disabled'}` }
186
+ async presets(): Promise<PresetView[]> {
187
+ if (host.agentPresets === undefined) return []
188
+ const defaultId = host.agentPresets.defaultId
189
+ try {
190
+ return (await host.agentPresets.list()).map((p) => ({
191
+ id: p.id,
192
+ ...(p.name !== undefined ? { name: p.name } : {}),
193
+ ...(p.description !== undefined ? { description: p.description } : {}),
194
+ ...(defaultId !== undefined && p.id === defaultId ? { isDefault: true } : {}),
195
+ }))
196
+ } catch {
197
+ return []
198
+ }
309
199
  },
310
- getTask(taskId: string): unknown {
311
- return holder.store.get(taskId)
200
+ // ── inbound ─────────────────────────────────────────────────────────
201
+ listInboundServers(): InboundServerView[] {
202
+ return host.inboundManager.list().map((live) => inboundView(live.id)).filter((v): v is InboundServerView => v !== undefined)
312
203
  },
313
- listTasks(): unknown {
314
- return holder.store.list()
204
+ async createInboundServer(input: InboundCreateInput): Promise<OpResult> {
205
+ const result = await host.inboundManager.add({
206
+ name: input.name,
207
+ description: input.description,
208
+ version: input.version,
209
+ ...(input.endpointPath !== undefined ? { endpointPath: input.endpointPath } : {}),
210
+ ...(input.preset !== undefined ? { preset: input.preset } : {}),
211
+ ...(input.authTokenEnv !== undefined ? { authTokenEnv: input.authTokenEnv } : {}),
212
+ ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
213
+ })
214
+ host.logger(`[a2a] inbound created: ${result.message}`)
215
+ return result
315
216
  },
316
- async cancelTask(taskId: string): Promise<OpResult> {
317
- const record = holder.store.get(taskId)
318
- if (record === undefined) return { ok: false, message: `task ${taskId} not found` }
319
- const aborted = holder.server?.abort?.(taskId) ?? false
320
- return { ok: true, message: aborted ? `task ${taskId} canceled` : `task ${taskId} already terminal` }
217
+ async removeInboundServer(id: string): Promise<OpResult> {
218
+ return host.inboundManager.remove(id)
321
219
  },
322
- agents(): unknown {
323
- return holder.registry?.list() ?? []
220
+ async setInboundServerEnabled(id: string, enabled: boolean): Promise<OpResult> {
221
+ return host.inboundManager.setEnabled(id, enabled)
324
222
  },
325
- async addAgent(spec: { name: string; agentCardUrl: string; bearerTokenEnv?: string }): Promise<OpResult> {
326
- const registry = holder.registry
327
- if (registry === undefined) return { ok: false, message: 'outbound client not mounted (no tools service)' }
328
- const result = await registry.add({ ...spec, enabled: true, timeoutMs: 60000 })
329
- return result
223
+ async updateInboundServer(id: string, patch: { name?: string; description?: string; version?: string; endpointPath?: string; preset?: string; authTokenEnv?: string }): Promise<OpResult> {
224
+ return host.inboundManager.update(id, patch)
330
225
  },
331
- async removeAgent(id: string): Promise<OpResult> {
332
- return holder.registry?.remove(id) ?? { ok: false, message: 'outbound client not mounted' }
226
+ // ── outbound ────────────────────────────────────────────────────────
227
+ listOutboundServers(): OutboundServerView[] {
228
+ return [...host.outboundManager.list()]
333
229
  },
334
- async setAgentEnabled(id: string, enabled: boolean): Promise<OpResult> {
335
- return holder.registry?.setEnabled(id, enabled) ?? { ok: false, message: 'outbound client not mounted' }
230
+ async createOutboundServer(input: OutboundCreateInput): Promise<OpResult> {
231
+ return host.outboundManager.add({
232
+ ...(input.id !== undefined ? { id: input.id } : {}),
233
+ name: input.name,
234
+ agentCardUrl: input.agentCardUrl,
235
+ ...(input.bearerTokenEnv !== undefined ? { bearerTokenEnv: input.bearerTokenEnv } : {}),
236
+ ...(input.preset !== undefined ? { preset: input.preset } : {}),
237
+ enabled: input.enabled ?? true,
238
+ timeoutMs: input.timeoutMs ?? host.outboundManager.defaultTimeoutMs,
239
+ })
336
240
  },
337
- async refreshAgentCard(id: string): Promise<OpResult> {
338
- return holder.registry?.refresh(id) ?? { ok: false, message: 'outbound client not mounted' }
241
+ async removeOutboundServer(id: string): Promise<OpResult> {
242
+ return host.outboundManager.remove(id)
339
243
  },
340
- identity(): unknown {
341
- const current = holder.card
342
- const persisted = readIdentity(holder.domain)
343
- return {
344
- ...(persisted ?? {}),
345
- // Composition defaults shown when nothing is persisted yet.
346
- defaults: holder.identityDefaults,
347
- name: current?.name ?? holder.identityDefaults.name,
348
- description: current?.description ?? holder.identityDefaults.description,
349
- version: current?.version ?? holder.identityDefaults.version,
350
- }
244
+ async setOutboundServerEnabled(id: string, enabled: boolean): Promise<OpResult> {
245
+ return host.outboundManager.setEnabled(id, enabled)
351
246
  },
352
- async updateIdentity(patch: { name?: string; description?: string; version?: string }): Promise<OpResult> {
353
- const card = holder.card
354
- const server = holder.server
355
- if (card === undefined || server === undefined) return { ok: false, message: 'inbound server not mounted (no card)' }
356
- const persisted = readIdentity(holder.domain)
357
- const base = persisted ?? holder.identityDefaults
358
- const next: A2aIdentity = {
359
- name: patch.name?.trim() || base.name,
360
- description: patch.description?.trim() || base.description,
361
- version: patch.version?.trim() || base.version,
362
- }
363
- writeIdentity(holder.domain, next)
364
- const rebuilt = rebuildCardWithIdentity(card, next, card.skills ?? [])
365
- server.setCard(rebuilt)
366
- holder.card = rebuilt
367
- log(`[a2a] identity updated: ${next.name}`)
368
- return { ok: true, message: `service identity updated (${next.name})` }
247
+ async refreshOutboundServer(id: string): Promise<OpResult> {
248
+ return host.outboundManager.refresh(id)
369
249
  },
370
- async closeInbound(peerId: string): Promise<OpResult> {
371
- const inbound = holder.inbound
372
- if (inbound === undefined) return { ok: false, message: 'inbound registry not mounted' }
373
- // Cancel the peer's active tasks, then drop the record.
374
- for (const taskId of inbound.activeTasksOf(peerId)) {
375
- await holder.server?.abort?.(taskId)
376
- }
377
- return inbound.closePeer(peerId)
250
+ // ── tasks ───────────────────────────────────────────────────────────
251
+ getTask(taskId: string): unknown {
252
+ return host.store.get(taskId)
253
+ },
254
+ listTasks(): unknown {
255
+ return host.store.list()
378
256
  },
257
+ async cancelTask(taskId: string): Promise<OpResult> {
258
+ const live = host.inboundManager.list().find((l) => l.server.abort(taskId))
259
+ return { ok: live !== undefined, message: live !== undefined ? `task ${taskId} canceled` : `task ${taskId} not found or already terminal` }
260
+ },
261
+ // ── inbound peers (aggregated across instances) ─────────────────────
379
262
  inbounds(): unknown {
380
- return holder.inbound?.list() ?? []
263
+ return host.inboundManager.list().flatMap((live) => live.inbound.list())
381
264
  },
382
- }
383
- }
384
-
385
- /** A task that refuses readably on compositions without an agent loop. */
386
- function refuseExecutor(reason: string) {
387
- return {
388
- name: 'refuse',
389
- async execute(): Promise<{ parts: { text: string }[] }> {
390
- throw new Error(`a2a: ${reason}; refusing inbound task`)
265
+ async closeInbound(peerId: string): Promise<OpResult> {
266
+ for (const live of host.inboundManager.list()) {
267
+ const peers = live.inbound.activeTasksOf(peerId)
268
+ for (const taskId of peers) live.server.abort(taskId)
269
+ const result = live.inbound.closePeer(peerId)
270
+ if (result) return { ok: true, message: `inbound peer ${peerId} closed` }
271
+ }
272
+ return { ok: false, message: `inbound peer ${peerId} not found` }
391
273
  },
392
274
  }
393
275
  }
@@ -412,8 +294,19 @@ function inboundSessionCwd(): string {
412
294
  return dir
413
295
  }
414
296
 
415
- export type { TaskState }
416
- export type { AgentSkill }
297
+ function noopRegister(): () => void {
298
+ return () => {}
299
+ }
300
+
301
+ /** The listening port of a mounted webServer (undefined when unavailable). */
302
+ function webServerPort(webServer: InboundWebServerLike | undefined): string | undefined {
303
+ const candidate = webServer as { port?: number | (() => number) } | undefined
304
+ if (candidate === undefined) return undefined
305
+ const value = typeof candidate.port === 'function' ? (candidate as { port(): number }).port() : candidate.port
306
+ return typeof value === 'number' && Number.isFinite(value) ? String(value) : undefined
307
+ }
308
+
309
+ export type { OpResult }
417
310
  export { A2AServer } from './server/a2a-server.ts'
418
311
  export { A2AClient, A2AError } from './outbound/calls.ts'
419
- export { A2AService } from './service.ts'
312
+ export { A2AService } from './service.ts'
@@ -60,17 +60,17 @@ export class A2AClient {
60
60
  signal: AbortSignal.timeout(timeoutMs),
61
61
  })
62
62
  if (!response.ok) {
63
- throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_NOT_FOUND, `agent card fetch failed: HTTP ${response.status}`)
63
+ throw new A2AError(A2A_ERROR_CODES.INVALID_AGENT_RESPONSE, `agent card fetch failed: HTTP ${response.status}`)
64
64
  }
65
65
  const card = (await response.json()) as AgentCard
66
- if (typeof card.name !== 'string' || typeof card.description !== 'string') {
67
- throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_SIGNATURE_INVALID, `invalid agent card at ${agentCardUrl}`)
66
+ if (typeof card.name !== 'string' || !Array.isArray(card.skills)) {
67
+ throw new A2AError(A2A_ERROR_CODES.INVALID_AGENT_RESPONSE, `invalid agent card at ${agentCardUrl}`)
68
68
  }
69
69
  const iface = card.supportedInterfaces?.find((i) => (i.protocolBinding ?? 'JSONRPC') === 'JSONRPC')
70
70
  ?? card.supportedInterfaces?.[0]
71
- const endpoint = iface?.url ?? card.url
71
+ const endpoint = iface?.url
72
72
  if (!endpoint) {
73
- throw new A2AError(A2A_ERROR_CODES.AGENT_CARD_NOT_FOUND, `agent card at ${agentCardUrl} advertises no JSON-RPC interface`)
73
+ throw new A2AError(A2A_ERROR_CODES.INVALID_AGENT_RESPONSE, `agent card at ${agentCardUrl} advertises no JSON-RPC interface`)
74
74
  }
75
75
  return new A2AClient(card, endpoint, opts)
76
76
  }
@@ -130,7 +130,9 @@ export class A2AClient {
130
130
  throw err
131
131
  }
132
132
  if (response.status === 401) {
133
- throw new A2AError(A2A_ERROR_CODES.UNAUTHORIZED, 'remote agent rejected credentials (401)')
133
+ // HTTP 401 has no dedicated JSON-RPC code in the spec; surface it as a
134
+ // distinct negative code the tools layer can map to a readable message.
135
+ throw new A2AError(-32040, 'remote agent rejected credentials (401)')
134
136
  }
135
137
  if (!response.ok) {
136
138
  throw new A2AError(A2A_ERROR_CODES.INTERNAL_ERROR, `A2A call ${method} failed: HTTP ${response.status}`)