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