@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.
- package/README.md +133 -87
- package/README.zh.md +64 -87
- package/cordis.patch.yml +4 -7
- package/lib/client.js +228 -36
- package/lib/index.js +1271 -625
- package/lib/types/api.d.ts +43 -13
- package/lib/types/api.d.ts.map +1 -1
- package/lib/types/api.js +60 -20
- package/lib/types/api.js.map +1 -1
- package/lib/types/client/index.d.ts +50 -14
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/commands.d.ts +4 -2
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +59 -46
- package/lib/types/commands.js.map +1 -1
- package/lib/types/events.d.ts +32 -9
- package/lib/types/events.d.ts.map +1 -1
- package/lib/types/index.d.ts +21 -32
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +178 -213
- package/lib/types/index.js.map +1 -1
- package/lib/types/outbound/calls.d.ts.map +1 -1
- package/lib/types/outbound/calls.js +8 -6
- package/lib/types/outbound/calls.js.map +1 -1
- package/lib/types/protocol.d.ts +174 -109
- package/lib/types/protocol.d.ts.map +1 -1
- package/lib/types/protocol.js +58 -34
- package/lib/types/protocol.js.map +1 -1
- package/lib/types/server/a2a-server.d.ts +7 -1
- package/lib/types/server/a2a-server.d.ts.map +1 -1
- package/lib/types/server/a2a-server.js +11 -3
- package/lib/types/server/a2a-server.js.map +1 -1
- package/lib/types/server/card.d.ts +6 -30
- package/lib/types/server/card.d.ts.map +1 -1
- package/lib/types/server/card.js +13 -49
- package/lib/types/server/card.js.map +1 -1
- package/lib/types/server/exec/agent-runtime.d.ts +3 -0
- package/lib/types/server/exec/agent-runtime.d.ts.map +1 -1
- package/lib/types/server/exec/agent-runtime.js +2 -1
- package/lib/types/server/exec/agent-runtime.js.map +1 -1
- package/lib/types/server/identity.d.ts +38 -0
- package/lib/types/server/identity.d.ts.map +1 -0
- package/lib/types/server/identity.js +89 -0
- package/lib/types/server/identity.js.map +1 -0
- package/lib/types/server/inbound-registry.d.ts +60 -0
- package/lib/types/server/inbound-registry.d.ts.map +1 -0
- package/lib/types/server/inbound-registry.js +86 -0
- package/lib/types/server/inbound-registry.js.map +1 -0
- package/lib/types/server/routes.d.ts +5 -1
- package/lib/types/server/routes.d.ts.map +1 -1
- package/lib/types/server/routes.js +9 -5
- package/lib/types/server/routes.js.map +1 -1
- package/lib/types/server/store.d.ts +37 -1
- package/lib/types/server/store.d.ts.map +1 -1
- package/lib/types/server/store.js +14 -0
- package/lib/types/server/store.js.map +1 -1
- package/lib/types/servers/inbound-manager.d.ts +131 -0
- package/lib/types/servers/inbound-manager.d.ts.map +1 -0
- package/lib/types/servers/inbound-manager.js +312 -0
- package/lib/types/servers/inbound-manager.js.map +1 -0
- package/lib/types/servers/outbound-manager.d.ts +129 -0
- package/lib/types/servers/outbound-manager.d.ts.map +1 -0
- package/lib/types/servers/outbound-manager.js +238 -0
- package/lib/types/servers/outbound-manager.js.map +1 -0
- package/lib/types/service.d.ts +108 -23
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +39 -18
- package/lib/types/service.js.map +1 -1
- package/package.json +3 -3
- package/src/api.ts +70 -31
- package/src/client/index.ts +339 -81
- package/src/commands.ts +59 -43
- package/src/events.ts +14 -7
- package/src/index.ts +204 -249
- package/src/outbound/calls.ts +8 -6
- package/src/protocol.ts +204 -95
- package/src/server/a2a-server.ts +16 -4
- package/src/server/card.ts +13 -62
- package/src/server/exec/agent-runtime.ts +5 -2
- package/src/server/identity.ts +109 -0
- package/src/server/inbound-registry.ts +129 -0
- package/src/server/routes.ts +6 -4
- package/src/server/store.ts +50 -4
- package/src/servers/inbound-manager.ts +385 -0
- package/src/servers/outbound-manager.ts +289 -0
- package/src/service.ts +136 -28
- package/lib/tsconfig.client.tsbuildinfo +0 -1
- 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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Mounts the A2A v1.0.1 dual-end plugin as a MULTI-INSTANCE composition: an
|
|
5
|
+
* arbitrary set of inbound A2A servers (each a preset-bound session pool, its
|
|
6
|
+
* own endpoint + AgentCard + creator-declared skills + auth) and an arbitrary
|
|
7
|
+
* set of outbound A2A connections (each a remote URL + preset + timeout).
|
|
8
|
+
* Instances are persisted in the `a2a` domain (`inbound_servers` /
|
|
9
|
+
* `outbound_servers` tables) and created/edited/started/stopped entirely from
|
|
10
|
+
* the GUI — the plugin `Config` is minimal and instance setup is not
|
|
11
|
+
* patch-config driven. The protocol surface is aligned with the official A2A
|
|
12
|
+
* v1.0.1 spec (see docs/design.md).
|
|
13
|
+
*
|
|
14
|
+
* Optional services are probed rather than injected so a composition lacking
|
|
15
|
+
* one half idles just that half; only the storage domain is required.
|
|
11
16
|
*
|
|
12
17
|
* Function-plugin export shape: named `name`/`inject`/`Config`/`apply`, no
|
|
13
18
|
* default export (mixing forms makes the Loader drop the namespace).
|
|
@@ -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 {
|
|
25
|
-
import
|
|
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 {
|
|
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
|
|
35
|
-
import
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
//
|
|
129
|
-
const webServer = probeService(ctx, 'webServer', 'register') as
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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(
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
255
|
-
await
|
|
256
|
-
await holder.registry?.disposeAll()
|
|
136
|
+
inboundManager.disposeAll()
|
|
137
|
+
await outboundManager.disposeAll()
|
|
257
138
|
await domain.close()
|
|
258
139
|
}
|
|
259
|
-
}, 'a2a:
|
|
140
|
+
}, 'a2a: multi-instance composition')
|
|
260
141
|
})
|
|
261
142
|
}
|
|
262
143
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
|
293
|
-
|
|
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
|
-
|
|
299
|
-
return
|
|
216
|
+
async setInboundServerEnabled(id: string, enabled: boolean): Promise<OpResult> {
|
|
217
|
+
return host.inboundManager.setEnabled(id, enabled)
|
|
300
218
|
},
|
|
301
|
-
async
|
|
302
|
-
|
|
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
|
-
|
|
308
|
-
|
|
222
|
+
// ── outbound ────────────────────────────────────────────────────────
|
|
223
|
+
listOutboundServers(): OutboundServerView[] {
|
|
224
|
+
return [...host.outboundManager.list()]
|
|
309
225
|
},
|
|
310
|
-
async
|
|
311
|
-
return
|
|
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
|
|
314
|
-
return
|
|
237
|
+
async removeOutboundServer(id: string): Promise<OpResult> {
|
|
238
|
+
return host.outboundManager.remove(id)
|
|
315
239
|
},
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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'
|
package/src/outbound/calls.ts
CHANGED
|
@@ -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.
|
|
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' ||
|
|
67
|
-
throw new A2AError(A2A_ERROR_CODES.
|
|
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
|
|
71
|
+
const endpoint = iface?.url
|
|
72
72
|
if (!endpoint) {
|
|
73
|
-
throw new A2AError(A2A_ERROR_CODES.
|
|
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
|
-
|
|
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}`)
|