@hanphone/dsh-a2a 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -106
- package/README.zh.md +50 -74
- package/cordis.patch.yml +4 -7
- package/lib/client.js +217 -73
- package/lib/index.js +1258 -867
- package/lib/types/api.d.ts +40 -23
- package/lib/types/api.d.ts.map +1 -1
- package/lib/types/api.js +58 -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 +173 -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 +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/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 +35 -1
- 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 +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 +106 -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 +67 -53
- package/src/client/index.ts +309 -148
- package/src/commands.ts +59 -43
- package/src/events.ts +14 -7
- package/src/index.ts +199 -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 +5 -2
- package/src/server/routes.ts +6 -4
- package/src/server/store.ts +45 -4
- package/src/servers/inbound-manager.ts +385 -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,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 { 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 } 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; 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 }
|
|
142
80
|
| 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
81
|
|
|
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
|
-
}
|
|
82
|
+
const baseUrl = config.baseUrl ?? `http://127.0.0.1:${webServerPort(webServer) ?? process.env['DSH_A2A_PORT'] ?? '3000'}`
|
|
196
83
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
inbound.settle(taskId)
|
|
212
|
-
},
|
|
213
|
-
})
|
|
214
|
-
holder.server = server
|
|
215
|
-
const routes = new A2aRoutes(webServer as never, server)
|
|
216
|
-
holder.routes = routes
|
|
217
|
-
if (holder.enabled) routes.enable()
|
|
218
|
-
} catch (err) {
|
|
219
|
-
logger.error(`a2a: server half failed to build: ${(err as Error).message}`)
|
|
220
|
-
}
|
|
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),
|
|
221
98
|
}
|
|
99
|
+
const inboundManager = new InboundServerManager(inboundHost, new DomainInboundStore(domain.inbound_servers))
|
|
100
|
+
inboundManager.boot()
|
|
222
101
|
|
|
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)
|
|
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,
|
|
243
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()
|
|
244
116
|
|
|
245
|
-
// ── service facade + commands
|
|
246
|
-
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) })
|
|
247
119
|
new A2AService(ctx, impl)
|
|
248
120
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
| undefined
|
|
252
|
-
if (commands !== undefined) {
|
|
253
|
-
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')
|
|
254
123
|
}
|
|
255
124
|
|
|
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({
|
|
125
|
+
if (webServer !== undefined) {
|
|
126
|
+
ctx.effect(() => webServer.register({
|
|
262
127
|
kind: 'prefix',
|
|
263
128
|
path: '/a2a/api',
|
|
264
129
|
handler: (req: unknown, res: unknown) => handleApiRequest(req as never, res as never, impl),
|
|
@@ -268,126 +133,139 @@ export function apply(ctx: Context, config: A2AConfig) {
|
|
|
268
133
|
}
|
|
269
134
|
|
|
270
135
|
return async () => {
|
|
271
|
-
|
|
272
|
-
await
|
|
273
|
-
await holder.registry?.disposeAll()
|
|
136
|
+
inboundManager.disposeAll()
|
|
137
|
+
await outboundManager.disposeAll()
|
|
274
138
|
await domain.close()
|
|
275
139
|
}
|
|
276
|
-
}, 'a2a:
|
|
140
|
+
}, 'a2a: multi-instance composition')
|
|
277
141
|
})
|
|
278
142
|
}
|
|
279
143
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
|
|
283
174
|
return {
|
|
284
175
|
status(): unknown {
|
|
285
176
|
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() ?? [],
|
|
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,
|
|
301
180
|
}
|
|
302
181
|
},
|
|
303
|
-
async
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
}
|
|
309
194
|
},
|
|
310
|
-
|
|
311
|
-
|
|
195
|
+
// ── inbound ─────────────────────────────────────────────────────────
|
|
196
|
+
listInboundServers(): InboundServerView[] {
|
|
197
|
+
return host.inboundManager.list().map((live) => inboundView(live.id)).filter((v): v is InboundServerView => v !== undefined)
|
|
312
198
|
},
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
315
212
|
},
|
|
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` }
|
|
213
|
+
async removeInboundServer(id: string): Promise<OpResult> {
|
|
214
|
+
return host.inboundManager.remove(id)
|
|
321
215
|
},
|
|
322
|
-
|
|
323
|
-
return
|
|
216
|
+
async setInboundServerEnabled(id: string, enabled: boolean): Promise<OpResult> {
|
|
217
|
+
return host.inboundManager.setEnabled(id, enabled)
|
|
324
218
|
},
|
|
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
|
|
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)
|
|
330
221
|
},
|
|
331
|
-
|
|
332
|
-
|
|
222
|
+
// ── outbound ────────────────────────────────────────────────────────
|
|
223
|
+
listOutboundServers(): OutboundServerView[] {
|
|
224
|
+
return [...host.outboundManager.list()]
|
|
333
225
|
},
|
|
334
|
-
async
|
|
335
|
-
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
|
+
})
|
|
336
236
|
},
|
|
337
|
-
async
|
|
338
|
-
return
|
|
237
|
+
async removeOutboundServer(id: string): Promise<OpResult> {
|
|
238
|
+
return host.outboundManager.remove(id)
|
|
339
239
|
},
|
|
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
|
-
}
|
|
240
|
+
async setOutboundServerEnabled(id: string, enabled: boolean): Promise<OpResult> {
|
|
241
|
+
return host.outboundManager.setEnabled(id, enabled)
|
|
351
242
|
},
|
|
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})` }
|
|
243
|
+
async refreshOutboundServer(id: string): Promise<OpResult> {
|
|
244
|
+
return host.outboundManager.refresh(id)
|
|
369
245
|
},
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
return inbound.closePeer(peerId)
|
|
246
|
+
// ── tasks ───────────────────────────────────────────────────────────
|
|
247
|
+
getTask(taskId: string): unknown {
|
|
248
|
+
return host.store.get(taskId)
|
|
249
|
+
},
|
|
250
|
+
listTasks(): unknown {
|
|
251
|
+
return host.store.list()
|
|
378
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) ─────────────────────
|
|
379
258
|
inbounds(): unknown {
|
|
380
|
-
return
|
|
259
|
+
return host.inboundManager.list().flatMap((live) => live.inbound.list())
|
|
381
260
|
},
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
throw new Error(`a2a: ${reason}; refusing inbound task`)
|
|
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` }
|
|
391
269
|
},
|
|
392
270
|
}
|
|
393
271
|
}
|
|
@@ -412,8 +290,19 @@ function inboundSessionCwd(): string {
|
|
|
412
290
|
return dir
|
|
413
291
|
}
|
|
414
292
|
|
|
415
|
-
|
|
416
|
-
|
|
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 }
|
|
417
306
|
export { A2AServer } from './server/a2a-server.ts'
|
|
418
307
|
export { A2AClient, A2AError } from './outbound/calls.ts'
|
|
419
|
-
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}`)
|