@ai_mind/dsh-a2a 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +444 -2
- package/client.js +868 -0
- package/cordis.patch.yml +21 -0
- package/icon.svg +15 -0
- package/index.js +59 -0
- package/lib/auth.js +196 -0
- package/lib/bridge.js +518 -0
- package/lib/card.js +214 -0
- package/lib/client-discovery.js +58 -0
- package/lib/config.js +565 -0
- package/lib/executor.js +357 -0
- package/lib/files.js +52 -0
- package/lib/limits.js +133 -0
- package/lib/panel.js +357 -0
- package/lib/prompt.js +161 -0
- package/lib/server.js +456 -0
- package/lib/store.js +288 -0
- package/lib/util.js +221 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +54 -4
package/lib/server.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A server assembly: Agent Card, transports, authentication and DSH routes.
|
|
3
|
+
*
|
|
4
|
+
* The official `@a2a-js/sdk` supplies the protocol engine — task lifecycle,
|
|
5
|
+
* JSON-RPC and HTTP+JSON transports, SSE framing, ProtoJSON codecs, push
|
|
6
|
+
* notifications and Agent Card signing. This module mounts that engine on the
|
|
7
|
+
* Harness Web server and wires it to the local agent runtime.
|
|
8
|
+
*
|
|
9
|
+
* @module @ai_mind/dsh-a2a/server
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import express from 'express'
|
|
13
|
+
import { A2A_PROTOCOL_VERSION } from '@a2a-js/sdk'
|
|
14
|
+
import {
|
|
15
|
+
DefaultPushNotificationSender,
|
|
16
|
+
DefaultRequestHandler,
|
|
17
|
+
InMemoryPushNotificationStore,
|
|
18
|
+
} from '@a2a-js/sdk/server'
|
|
19
|
+
import { agentCardHandler, jsonRpcHandler, restHandler } from '@a2a-js/sdk/server/express'
|
|
20
|
+
import { buildAgentCard, buildExtendedCard, createCardSigner, isAuthEnabled } from './card.js'
|
|
21
|
+
import { createAuthenticator } from './auth.js'
|
|
22
|
+
import { createPanelRouter } from './panel.js'
|
|
23
|
+
import { A2ATaskStore, GuardedPushNotificationStore } from './store.js'
|
|
24
|
+
import { HarnessAgentExecutor } from './executor.js'
|
|
25
|
+
import { AgentRuntime } from './bridge.js'
|
|
26
|
+
import { errorMessage, trimTrailingSlash } from './util.js'
|
|
27
|
+
import { sweepAttachmentDirectories } from './files.js'
|
|
28
|
+
import { resolveCredentials } from './config.js'
|
|
29
|
+
|
|
30
|
+
const JSON_CONTENT_TYPE = 'application/json'
|
|
31
|
+
const A2A_CONTENT_TYPE = 'application/a2a+json'
|
|
32
|
+
|
|
33
|
+
/** Minimal structured logger over the Cordis logger. */
|
|
34
|
+
function createLogger(ctx) {
|
|
35
|
+
const render = (args) => ['[a2a]', ...args.map((value) => (typeof value === 'string' ? value : errorMessage(value)))].join(' ')
|
|
36
|
+
const emit = (level, args) => {
|
|
37
|
+
try {
|
|
38
|
+
const logger = ctx.logger
|
|
39
|
+
if (typeof logger?.[level] === 'function') logger[level](render(args))
|
|
40
|
+
else if (typeof logger?.log === 'function') logger.log(render(args))
|
|
41
|
+
} catch {
|
|
42
|
+
/* logging must never fail a request */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
info: (...args) => emit('info', args),
|
|
47
|
+
warn: (...args) => emit('warn', args),
|
|
48
|
+
error: (...args) => emit('error', args),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Derive the externally visible origin, preferring the configured URL. */
|
|
53
|
+
function resolvePublicBase(config, webServer, logger) {
|
|
54
|
+
if (config.server.publicUrl) return trimTrailingSlash(config.server.publicUrl)
|
|
55
|
+
try {
|
|
56
|
+
const origin = webServer.getOrigin()
|
|
57
|
+
if (typeof origin === 'string' && origin !== '') return trimTrailingSlash(origin)
|
|
58
|
+
} catch {
|
|
59
|
+
/* not listening yet; fall through */
|
|
60
|
+
}
|
|
61
|
+
const port = Number(webServer.port)
|
|
62
|
+
if (Number.isInteger(port) && port > 0) {
|
|
63
|
+
logger.warn(`[a2a] webServer.getOrigin() unavailable; advertising http://127.0.0.1:${port}`)
|
|
64
|
+
return `http://127.0.0.1:${port}`
|
|
65
|
+
}
|
|
66
|
+
logger.warn('[a2a] cannot determine a public origin; set server.publicUrl in the plugin config')
|
|
67
|
+
return 'http://127.0.0.1'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @returns {boolean} whether the request targets the REST binding. */
|
|
71
|
+
function isRestBinding(req, mountPath) {
|
|
72
|
+
const path = String(req.originalUrl ?? req.url ?? '')
|
|
73
|
+
return path.startsWith(`${mountPath}/rest`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Build the CORS middleware for the configured policy. */
|
|
77
|
+
function createCors(config) {
|
|
78
|
+
const cors = config.server.cors
|
|
79
|
+
return (req, res, next) => {
|
|
80
|
+
if (!cors.enabled) {
|
|
81
|
+
next()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const origin = req.headers.origin
|
|
85
|
+
const wildcard = cors.origins.includes('*')
|
|
86
|
+
const allowed = wildcard ? '*' : origin !== undefined && cors.origins.includes(origin) ? origin : null
|
|
87
|
+
if (allowed !== null) {
|
|
88
|
+
res.setHeader('Access-Control-Allow-Origin', allowed)
|
|
89
|
+
if (!wildcard) res.setHeader('Vary', 'Origin')
|
|
90
|
+
if (cors.allowCredentials) res.setHeader('Access-Control-Allow-Credentials', 'true')
|
|
91
|
+
res.setHeader('Access-Control-Expose-Headers', 'A2A-Extensions, A2A-Version, ETag')
|
|
92
|
+
}
|
|
93
|
+
if (req.method === 'OPTIONS') {
|
|
94
|
+
if (allowed !== null) {
|
|
95
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
|
|
96
|
+
res.setHeader(
|
|
97
|
+
'Access-Control-Allow-Headers',
|
|
98
|
+
`authorization, content-type, a2a-version, a2a-extensions, ${config.auth.headerName}`,
|
|
99
|
+
)
|
|
100
|
+
res.setHeader('Access-Control-Max-Age', String(cors.maxAgeSeconds))
|
|
101
|
+
}
|
|
102
|
+
res.statusCode = 204
|
|
103
|
+
res.end()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
next()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Write one error body in the shape of the binding the caller used. */
|
|
111
|
+
function writeError(req, res, { status, code, message, restBinding, rpcCode = -32603 }) {
|
|
112
|
+
if (res.headersSent) {
|
|
113
|
+
res.end()
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
res.statusCode = status
|
|
117
|
+
res.setHeader('Content-Type', restBinding ? A2A_CONTENT_TYPE : JSON_CONTENT_TYPE)
|
|
118
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
119
|
+
const details = [
|
|
120
|
+
{
|
|
121
|
+
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
122
|
+
reason: code,
|
|
123
|
+
domain: 'a2a-protocol.org',
|
|
124
|
+
metadata: { path: String(req.originalUrl ?? req.url ?? '') },
|
|
125
|
+
},
|
|
126
|
+
]
|
|
127
|
+
if (restBinding) {
|
|
128
|
+
res.end(JSON.stringify({ error: { code: status, status: code, message, details } }))
|
|
129
|
+
} else {
|
|
130
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: rpcCode, message, data: details } }))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* One mounted A2A deployment. Mounting is synchronous so route registration
|
|
136
|
+
* can participate in Cordis effects; only request handling touches async
|
|
137
|
+
* agent work.
|
|
138
|
+
*/
|
|
139
|
+
export class A2AService {
|
|
140
|
+
#ctx
|
|
141
|
+
#config
|
|
142
|
+
#logger
|
|
143
|
+
#mounted = undefined
|
|
144
|
+
#unmount = undefined
|
|
145
|
+
#disposed = false
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {object} ctx owning Cordis context
|
|
149
|
+
* @param {Readonly<object>} config normalized configuration
|
|
150
|
+
*/
|
|
151
|
+
constructor(ctx, config) {
|
|
152
|
+
this.#ctx = ctx
|
|
153
|
+
this.#config = config
|
|
154
|
+
this.#logger = createLogger(ctx)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** @returns {object} the structured logger used by every component */
|
|
158
|
+
get logger() {
|
|
159
|
+
return this.#logger
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Register routes on the Harness Web server.
|
|
164
|
+
*
|
|
165
|
+
* @param {object} webServer the `webServer` service
|
|
166
|
+
* @returns {() => Promise<void>} disposer that unregisters routes and drains work
|
|
167
|
+
*/
|
|
168
|
+
mount(webServer) {
|
|
169
|
+
if (this.#mounted !== undefined) throw new Error('a2a: already mounted')
|
|
170
|
+
if (this.#disposed) throw new Error('a2a: service is disposed')
|
|
171
|
+
const config = this.#config
|
|
172
|
+
const logger = this.#logger
|
|
173
|
+
|
|
174
|
+
const credentials = resolveCredentials(config)
|
|
175
|
+
const publicBaseUrl = resolvePublicBase(config, webServer, logger)
|
|
176
|
+
const authEnabled = isAuthEnabled(config, credentials)
|
|
177
|
+
let extendedCard = config.server.extendedCard
|
|
178
|
+
if (extendedCard && !authEnabled) {
|
|
179
|
+
logger.warn(
|
|
180
|
+
'[a2a] server.extendedCard requires authentication; the extended Agent Card stays disabled because auth.mode is "none"',
|
|
181
|
+
)
|
|
182
|
+
extendedCard = false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const effective = {
|
|
186
|
+
...config,
|
|
187
|
+
server: { ...config.server, extendedCard, pushNotifications: config.server.pushNotifications },
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const cardOptions = { publicBaseUrl, credentials }
|
|
191
|
+
const baseCard = buildAgentCard(effective, cardOptions)
|
|
192
|
+
const extendedAgentCard = buildExtendedCard(effective, cardOptions)
|
|
193
|
+
const signer = createCardSigner(config)
|
|
194
|
+
|
|
195
|
+
const runtime = new AgentRuntime({ ctx: this.#ctx, config, logger })
|
|
196
|
+
const executor = new HarnessAgentExecutor({ runtime, config, logger })
|
|
197
|
+
const taskStore = new A2ATaskStore({ ...config.tasks, logger })
|
|
198
|
+
|
|
199
|
+
const pushStore = config.server.pushNotifications
|
|
200
|
+
? new GuardedPushNotificationStore(new InMemoryPushNotificationStore(), {
|
|
201
|
+
allowPrivateTargets: config.push.allowPrivateTargets,
|
|
202
|
+
maxConfigsPerTask: 10,
|
|
203
|
+
logger,
|
|
204
|
+
})
|
|
205
|
+
: undefined
|
|
206
|
+
const pushSender =
|
|
207
|
+
pushStore === undefined
|
|
208
|
+
? undefined
|
|
209
|
+
: new DefaultPushNotificationSender(pushStore, { timeout: config.push.timeoutMs })
|
|
210
|
+
|
|
211
|
+
const requestHandler = new DefaultRequestHandler(
|
|
212
|
+
baseCard,
|
|
213
|
+
taskStore,
|
|
214
|
+
executor,
|
|
215
|
+
undefined,
|
|
216
|
+
pushStore,
|
|
217
|
+
pushSender,
|
|
218
|
+
extendedCard ? async () => extendedAgentCard : undefined,
|
|
219
|
+
signer ?? undefined,
|
|
220
|
+
{ validateInputModes: true },
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
const authenticator = createAuthenticator({
|
|
224
|
+
config,
|
|
225
|
+
credentials,
|
|
226
|
+
logger,
|
|
227
|
+
isRestBinding: (req) => isRestBinding(req, config.server.mountPath),
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const app = express()
|
|
231
|
+
app.disable('x-powered-by')
|
|
232
|
+
app.set('trust proxy', config.server.trustProxy)
|
|
233
|
+
app.set('query parser', 'simple')
|
|
234
|
+
|
|
235
|
+
app.use(createCors(config))
|
|
236
|
+
app.use((req, res, next) => {
|
|
237
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
238
|
+
next()
|
|
239
|
+
})
|
|
240
|
+
if (config.server.logRequests) {
|
|
241
|
+
app.use((req, res, next) => {
|
|
242
|
+
const started = Date.now()
|
|
243
|
+
res.on('finish', () => {
|
|
244
|
+
logger.info(`${req.method} ${req.originalUrl} → ${res.statusCode} (${Date.now() - started}ms)`)
|
|
245
|
+
})
|
|
246
|
+
next()
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Parse bodies once with the configured limit. The SDK transports skip
|
|
251
|
+
// their own `express.json()` because body-parser marks the request parsed.
|
|
252
|
+
app.use(
|
|
253
|
+
express.json({
|
|
254
|
+
limit: config.limits.maxRequestBytes,
|
|
255
|
+
strict: false,
|
|
256
|
+
type: [JSON_CONTENT_TYPE, A2A_CONTENT_TYPE],
|
|
257
|
+
}),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const mountPath = config.server.mountPath
|
|
261
|
+
const jsonRpcRouter = jsonRpcHandler({ requestHandler, userBuilder: authenticator.userBuilder })
|
|
262
|
+
const restRouter = restHandler({ requestHandler, userBuilder: authenticator.userBuilder })
|
|
263
|
+
const cardRouter = agentCardHandler({
|
|
264
|
+
agentCardProvider: requestHandler,
|
|
265
|
+
cache: { maxAge: config.server.cardCacheSeconds },
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// The Agent Card is discovery metadata and stays public.
|
|
269
|
+
app.use(config.server.cardPath, cardRouter)
|
|
270
|
+
// The settings panel is authenticated by the Harness Web connection, not
|
|
271
|
+
// by the A2A token, and must be mounted before the A2A auth middleware.
|
|
272
|
+
app.use(`${mountPath}/panel`, createPanelRouter({ ctx: this.#ctx, config, mountPath, logger }))
|
|
273
|
+
// Everything else below the mount path is the A2A endpoint.
|
|
274
|
+
app.use(mountPath, authenticator.middleware)
|
|
275
|
+
app.use(`${mountPath}/rest`, restRouter)
|
|
276
|
+
app.use(mountPath, jsonRpcRouter)
|
|
277
|
+
|
|
278
|
+
app.get(mountPath, (req, res) => {
|
|
279
|
+
res.setHeader('Allow', 'POST')
|
|
280
|
+
writeError(req, res, {
|
|
281
|
+
status: 405,
|
|
282
|
+
code: 'METHOD_NOT_ALLOWED',
|
|
283
|
+
message: `The A2A JSON-RPC endpoint only accepts POST; REST clients use ${mountPath}/rest.`,
|
|
284
|
+
restBinding: false,
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
if (config.server.health) {
|
|
289
|
+
app.get(`${mountPath}/health`, (req, res) => {
|
|
290
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
291
|
+
res.json(this.#health({ runtime, executor, taskStore, authenticator, publicBaseUrl, extendedCard }))
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
app.use((req, res) => {
|
|
296
|
+
writeError(req, res, {
|
|
297
|
+
status: 404,
|
|
298
|
+
code: 'NOT_FOUND',
|
|
299
|
+
message: `No A2A route matches ${req.method} ${req.originalUrl}`,
|
|
300
|
+
restBinding: isRestBinding(req, mountPath),
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
app.use((error, req, res, next) => {
|
|
305
|
+
if (res.headersSent) {
|
|
306
|
+
res.end()
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
const status =
|
|
310
|
+
typeof error?.status === 'number' && error.status >= 400 && error.status <= 599 ? error.status : 500
|
|
311
|
+
const restBinding = isRestBinding(req, mountPath)
|
|
312
|
+
if (status >= 500) logger.warn(`[a2a] request failed: ${errorMessage(error)}`)
|
|
313
|
+
const isParseError = error?.type === 'entity.parse.failed'
|
|
314
|
+
const isTooLarge = error?.type === 'entity.too.large'
|
|
315
|
+
writeError(req, res, {
|
|
316
|
+
status: isTooLarge ? 413 : isParseError ? 400 : status,
|
|
317
|
+
code: isTooLarge ? 'RESOURCE_EXHAUSTED' : isParseError ? 'INVALID_ARGUMENT' : status >= 500 ? 'INTERNAL' : 'INVALID_ARGUMENT',
|
|
318
|
+
message: isTooLarge
|
|
319
|
+
? `Request body exceeds limits.maxRequestBytes (${config.limits.maxRequestBytes} bytes)`
|
|
320
|
+
: isParseError
|
|
321
|
+
? 'Invalid JSON payload'
|
|
322
|
+
: errorMessage(error),
|
|
323
|
+
restBinding,
|
|
324
|
+
rpcCode: isParseError ? -32700 : isTooLarge ? -32600 : -32603,
|
|
325
|
+
})
|
|
326
|
+
void next
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
const callApp = (req, res) => {
|
|
330
|
+
try {
|
|
331
|
+
app(req, res)
|
|
332
|
+
} catch (error) {
|
|
333
|
+
logger.warn(`[a2a] transport failure: ${errorMessage(error)}`)
|
|
334
|
+
if (!res.headersSent) {
|
|
335
|
+
res.statusCode = 500
|
|
336
|
+
res.setHeader('Content-Type', JSON_CONTENT_TYPE)
|
|
337
|
+
res.end(JSON.stringify({ error: { code: 500, status: 'INTERNAL', message: 'internal error' } }))
|
|
338
|
+
} else if (!res.writableEnded) {
|
|
339
|
+
res.end()
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const routeDisposers = []
|
|
345
|
+
const registerRoute = (route, label, { optional = false } = {}) => {
|
|
346
|
+
try {
|
|
347
|
+
routeDisposers.push(webServer.register({ ...route, handler: callApp }))
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (optional) {
|
|
350
|
+
logger.warn(`[a2a] cannot register ${label} at ${route.path}: ${errorMessage(error)}`)
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
throw new Error(`a2a: cannot register ${label} at ${route.path}: ${errorMessage(error)}`)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
registerRoute({ kind: 'exact', path: config.server.cardPath }, 'agent card')
|
|
358
|
+
registerRoute({ kind: 'prefix', path: mountPath }, 'A2A endpoint')
|
|
359
|
+
|
|
360
|
+
const sweepTimer = setInterval(() => {
|
|
361
|
+
try {
|
|
362
|
+
taskStore.sweep()
|
|
363
|
+
} catch (error) {
|
|
364
|
+
logger.warn(`[a2a] task sweep failed: ${errorMessage(error)}`)
|
|
365
|
+
}
|
|
366
|
+
if (config.files.enabled) {
|
|
367
|
+
runtime
|
|
368
|
+
.resolveCwd()
|
|
369
|
+
.then((cwd) =>
|
|
370
|
+
sweepAttachmentDirectories({
|
|
371
|
+
cwd,
|
|
372
|
+
directory: config.files.directory,
|
|
373
|
+
retentionMs: config.files.retentionMs,
|
|
374
|
+
logger,
|
|
375
|
+
}),
|
|
376
|
+
)
|
|
377
|
+
.then(({ removed }) => {
|
|
378
|
+
if (removed > 0) logger.info(`removed ${removed} stale attachment director${removed === 1 ? 'y' : 'ies'}`)
|
|
379
|
+
})
|
|
380
|
+
.catch((error) => logger.warn(`[a2a] attachment sweep failed: ${errorMessage(error)}`))
|
|
381
|
+
}
|
|
382
|
+
}, config.tasks.sweepIntervalMs)
|
|
383
|
+
sweepTimer.unref?.()
|
|
384
|
+
runtime.start()
|
|
385
|
+
|
|
386
|
+
const startedAt = Date.now()
|
|
387
|
+
this.#mounted = { startedAt }
|
|
388
|
+
logger.info(
|
|
389
|
+
`A2A v${A2A_PROTOCOL_VERSION} server ready: ${publicBaseUrl}${mountPath} (JSON-RPC), ` +
|
|
390
|
+
`${publicBaseUrl}${mountPath}/rest (HTTP+JSON), card ${publicBaseUrl}${config.server.cardPath}`,
|
|
391
|
+
)
|
|
392
|
+
logger.info(
|
|
393
|
+
`auth=${authenticator.describe().mode} streaming=${config.server.streaming} ` +
|
|
394
|
+
`push=${config.server.pushNotifications} extendedCard=${extendedCard}`,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
let released = false
|
|
398
|
+
const unmount = async () => {
|
|
399
|
+
if (released) return
|
|
400
|
+
released = true
|
|
401
|
+
clearInterval(sweepTimer)
|
|
402
|
+
for (const dispose of routeDisposers.splice(0).reverse()) {
|
|
403
|
+
try {
|
|
404
|
+
dispose()
|
|
405
|
+
} catch (error) {
|
|
406
|
+
logger.warn(`[a2a] route disposal failed: ${errorMessage(error)}`)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
await executor.dispose()
|
|
410
|
+
await runtime.shutdown()
|
|
411
|
+
this.#mounted = undefined
|
|
412
|
+
this.#unmount = undefined
|
|
413
|
+
logger.info('[a2a] server stopped')
|
|
414
|
+
}
|
|
415
|
+
this.#unmount = unmount
|
|
416
|
+
return unmount
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#health({ runtime, executor, taskStore, authenticator, publicBaseUrl, extendedCard }) {
|
|
420
|
+
const config = this.#config
|
|
421
|
+
const mounted = this.#mounted
|
|
422
|
+
return {
|
|
423
|
+
status: 'ok',
|
|
424
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
425
|
+
name: config.card.name,
|
|
426
|
+
version: config.card.version,
|
|
427
|
+
publicUrl: publicBaseUrl,
|
|
428
|
+
uptimeSeconds: mounted === undefined ? 0 : Math.round((Date.now() - mounted.startedAt) / 1000),
|
|
429
|
+
interfaces: [
|
|
430
|
+
{ protocolBinding: 'JSONRPC', url: `${publicBaseUrl}${config.server.mountPath}` },
|
|
431
|
+
{ protocolBinding: 'HTTP+JSON', url: `${publicBaseUrl}${config.server.mountPath}/rest` },
|
|
432
|
+
],
|
|
433
|
+
capabilities: {
|
|
434
|
+
streaming: config.server.streaming,
|
|
435
|
+
pushNotifications: config.server.pushNotifications,
|
|
436
|
+
extendedAgentCard: extendedCard,
|
|
437
|
+
},
|
|
438
|
+
auth: authenticator.describe(),
|
|
439
|
+
signing: config.signing.enabled
|
|
440
|
+
? { enabled: true, algorithm: config.signing.algorithm, keyId: config.signing.keyId }
|
|
441
|
+
: { enabled: false },
|
|
442
|
+
tasks: taskStore.stats(),
|
|
443
|
+
agents: runtime.stats(),
|
|
444
|
+
concurrency: executor.concurrency(),
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Release service-wide resources. Safe to call multiple times. */
|
|
449
|
+
async dispose() {
|
|
450
|
+
if (this.#disposed) return
|
|
451
|
+
this.#disposed = true
|
|
452
|
+
const unmount = this.#unmount
|
|
453
|
+
this.#unmount = undefined
|
|
454
|
+
if (unmount !== undefined) await unmount()
|
|
455
|
+
}
|
|
456
|
+
}
|