@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.
- package/README.md +106 -106
- package/README.zh.md +50 -74
- package/cordis.patch.yml +4 -7
- package/lib/client.js +186 -73
- package/lib/index.js +1275 -868
- package/lib/types/api.d.ts +30 -23
- package/lib/types/api.d.ts.map +1 -1
- package/lib/types/api.js +56 -23
- package/lib/types/api.js.map +1 -1
- package/lib/types/client/index.d.ts +50 -19
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/commands.d.ts +4 -2
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +59 -46
- package/lib/types/commands.js.map +1 -1
- package/lib/types/events.d.ts +32 -9
- package/lib/types/events.d.ts.map +1 -1
- package/lib/types/index.d.ts +21 -32
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +177 -272
- package/lib/types/index.js.map +1 -1
- package/lib/types/outbound/calls.d.ts.map +1 -1
- package/lib/types/outbound/calls.js +8 -6
- package/lib/types/outbound/calls.js.map +1 -1
- package/lib/types/protocol.d.ts +174 -109
- package/lib/types/protocol.d.ts.map +1 -1
- package/lib/types/protocol.js +58 -34
- package/lib/types/protocol.js.map +1 -1
- package/lib/types/server/a2a-server.d.ts +4 -0
- package/lib/types/server/a2a-server.d.ts.map +1 -1
- package/lib/types/server/a2a-server.js +6 -2
- package/lib/types/server/a2a-server.js.map +1 -1
- package/lib/types/server/card.d.ts +6 -30
- package/lib/types/server/card.d.ts.map +1 -1
- package/lib/types/server/card.js +13 -49
- package/lib/types/server/card.js.map +1 -1
- package/lib/types/server/exec/agent-runtime.d.ts +20 -0
- package/lib/types/server/exec/agent-runtime.d.ts.map +1 -1
- package/lib/types/server/exec/agent-runtime.js +2 -1
- package/lib/types/server/exec/agent-runtime.js.map +1 -1
- package/lib/types/server/routes.d.ts +5 -1
- package/lib/types/server/routes.d.ts.map +1 -1
- package/lib/types/server/routes.js +9 -5
- package/lib/types/server/routes.js.map +1 -1
- package/lib/types/server/store.d.ts +32 -0
- package/lib/types/server/store.d.ts.map +1 -1
- package/lib/types/server/store.js +10 -0
- package/lib/types/server/store.js.map +1 -1
- package/lib/types/servers/inbound-manager.d.ts +154 -0
- package/lib/types/servers/inbound-manager.d.ts.map +1 -0
- package/lib/types/servers/inbound-manager.js +335 -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 +104 -40
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +36 -27
- package/lib/types/service.js.map +1 -1
- package/package.json +2 -2
- package/src/api.ts +65 -53
- package/src/client/index.ts +272 -148
- package/src/commands.ts +59 -43
- package/src/events.ts +14 -7
- package/src/index.ts +203 -310
- package/src/outbound/calls.ts +8 -6
- package/src/protocol.ts +204 -95
- package/src/server/a2a-server.ts +9 -2
- package/src/server/card.ts +13 -62
- package/src/server/exec/agent-runtime.ts +20 -2
- package/src/server/routes.ts +6 -4
- package/src/server/store.ts +42 -3
- package/src/servers/inbound-manager.ts +416 -0
- package/src/servers/outbound-manager.ts +289 -0
- package/src/service.ts +125 -40
- package/lib/tsconfig.client.tsbuildinfo +0 -1
- package/lib/tsconfig.tsbuildinfo +0 -1
package/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,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 {
|
|
25
|
-
import {
|
|
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 {
|
|
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
|
|
37
|
-
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'
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
//
|
|
140
|
-
const webServer = probeService(ctx, 'webServer', 'register') as
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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(
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
272
|
-
await
|
|
273
|
-
await holder.registry?.disposeAll()
|
|
139
|
+
inboundManager.disposeAll()
|
|
140
|
+
await outboundManager.disposeAll()
|
|
274
141
|
await domain.close()
|
|
275
142
|
}
|
|
276
|
-
}, 'a2a:
|
|
143
|
+
}, 'a2a: multi-instance composition')
|
|
277
144
|
})
|
|
278
145
|
}
|
|
279
146
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
|
317
|
-
|
|
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
|
-
|
|
323
|
-
return
|
|
220
|
+
async setInboundServerEnabled(id: string, enabled: boolean): Promise<OpResult> {
|
|
221
|
+
return host.inboundManager.setEnabled(id, enabled)
|
|
324
222
|
},
|
|
325
|
-
async
|
|
326
|
-
|
|
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
|
-
|
|
332
|
-
|
|
226
|
+
// ── outbound ────────────────────────────────────────────────────────
|
|
227
|
+
listOutboundServers(): OutboundServerView[] {
|
|
228
|
+
return [...host.outboundManager.list()]
|
|
333
229
|
},
|
|
334
|
-
async
|
|
335
|
-
return
|
|
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
|
|
338
|
-
return
|
|
241
|
+
async removeOutboundServer(id: string): Promise<OpResult> {
|
|
242
|
+
return host.outboundManager.remove(id)
|
|
339
243
|
},
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
353
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
263
|
+
return host.inboundManager.list().flatMap((live) => live.inbound.list())
|
|
381
264
|
},
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
416
|
-
|
|
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'
|
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}`)
|