@hanphone/dsh-a2a 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -0
  3. package/README.zh.md +195 -0
  4. package/cordis.patch.yml +11 -0
  5. package/lib/client.js +166 -0
  6. package/lib/index.js +2073 -0
  7. package/lib/tsconfig.client.tsbuildinfo +1 -0
  8. package/lib/tsconfig.tsbuildinfo +1 -0
  9. package/lib/types/api.d.ts +41 -0
  10. package/lib/types/api.d.ts.map +1 -0
  11. package/lib/types/api.js +86 -0
  12. package/lib/types/api.js.map +1 -0
  13. package/lib/types/client/index.d.ts +35 -0
  14. package/lib/types/client/index.d.ts.map +1 -0
  15. package/lib/types/client/index.js +155 -0
  16. package/lib/types/client/index.js.map +1 -0
  17. package/lib/types/commands.d.ts +10 -0
  18. package/lib/types/commands.d.ts.map +1 -0
  19. package/lib/types/commands.js +93 -0
  20. package/lib/types/commands.js.map +1 -0
  21. package/lib/types/events.d.ts +74 -0
  22. package/lib/types/events.d.ts.map +1 -0
  23. package/lib/types/events.js +10 -0
  24. package/lib/types/events.js.map +1 -0
  25. package/lib/types/index.d.ts +54 -0
  26. package/lib/types/index.d.ts.map +1 -0
  27. package/lib/types/index.js +296 -0
  28. package/lib/types/index.js.map +1 -0
  29. package/lib/types/jsonrpc.d.ts +20 -0
  30. package/lib/types/jsonrpc.d.ts.map +1 -0
  31. package/lib/types/jsonrpc.js +48 -0
  32. package/lib/types/jsonrpc.js.map +1 -0
  33. package/lib/types/outbound/calls.d.ts +37 -0
  34. package/lib/types/outbound/calls.d.ts.map +1 -0
  35. package/lib/types/outbound/calls.js +120 -0
  36. package/lib/types/outbound/calls.js.map +1 -0
  37. package/lib/types/outbound/registry.d.ts +102 -0
  38. package/lib/types/outbound/registry.d.ts.map +1 -0
  39. package/lib/types/outbound/registry.js +249 -0
  40. package/lib/types/outbound/registry.js.map +1 -0
  41. package/lib/types/outbound/tools.d.ts +30 -0
  42. package/lib/types/outbound/tools.d.ts.map +1 -0
  43. package/lib/types/outbound/tools.js +86 -0
  44. package/lib/types/outbound/tools.js.map +1 -0
  45. package/lib/types/protocol.d.ts +194 -0
  46. package/lib/types/protocol.d.ts.map +1 -0
  47. package/lib/types/protocol.js +78 -0
  48. package/lib/types/protocol.js.map +1 -0
  49. package/lib/types/server/a2a-server.d.ts +98 -0
  50. package/lib/types/server/a2a-server.d.ts.map +1 -0
  51. package/lib/types/server/a2a-server.js +397 -0
  52. package/lib/types/server/a2a-server.js.map +1 -0
  53. package/lib/types/server/card.d.ts +48 -0
  54. package/lib/types/server/card.d.ts.map +1 -0
  55. package/lib/types/server/card.js +75 -0
  56. package/lib/types/server/card.js.map +1 -0
  57. package/lib/types/server/exec/agent-runtime.d.ts +126 -0
  58. package/lib/types/server/exec/agent-runtime.d.ts.map +1 -0
  59. package/lib/types/server/exec/agent-runtime.js +138 -0
  60. package/lib/types/server/exec/agent-runtime.js.map +1 -0
  61. package/lib/types/server/exec/session.d.ts +11 -0
  62. package/lib/types/server/exec/session.d.ts.map +1 -0
  63. package/lib/types/server/exec/session.js +22 -0
  64. package/lib/types/server/exec/session.js.map +1 -0
  65. package/lib/types/server/exec/subagent.d.ts +39 -0
  66. package/lib/types/server/exec/subagent.d.ts.map +1 -0
  67. package/lib/types/server/exec/subagent.js +44 -0
  68. package/lib/types/server/exec/subagent.js.map +1 -0
  69. package/lib/types/server/executor.d.ts +63 -0
  70. package/lib/types/server/executor.d.ts.map +1 -0
  71. package/lib/types/server/executor.js +38 -0
  72. package/lib/types/server/executor.js.map +1 -0
  73. package/lib/types/server/routes.d.ts +32 -0
  74. package/lib/types/server/routes.d.ts.map +1 -0
  75. package/lib/types/server/routes.js +131 -0
  76. package/lib/types/server/routes.js.map +1 -0
  77. package/lib/types/server/store.d.ts +157 -0
  78. package/lib/types/server/store.d.ts.map +1 -0
  79. package/lib/types/server/store.js +239 -0
  80. package/lib/types/server/store.js.map +1 -0
  81. package/lib/types/service.d.ts +49 -0
  82. package/lib/types/service.d.ts.map +1 -0
  83. package/lib/types/service.js +46 -0
  84. package/lib/types/service.js.map +1 -0
  85. package/package.json +82 -0
  86. package/src/api.ts +115 -0
  87. package/src/client/index.ts +293 -0
  88. package/src/commands.ts +92 -0
  89. package/src/events.ts +58 -0
  90. package/src/index.ts +353 -0
  91. package/src/jsonrpc.ts +61 -0
  92. package/src/outbound/calls.ts +145 -0
  93. package/src/outbound/registry.ts +290 -0
  94. package/src/outbound/tools.ts +118 -0
  95. package/src/protocol.ts +212 -0
  96. package/src/server/a2a-server.ts +455 -0
  97. package/src/server/card.ts +106 -0
  98. package/src/server/exec/agent-runtime.ts +197 -0
  99. package/src/server/exec/session.ts +25 -0
  100. package/src/server/exec/subagent.ts +67 -0
  101. package/src/server/executor.ts +73 -0
  102. package/src/server/routes.ts +156 -0
  103. package/src/server/store.ts +320 -0
  104. package/src/service.ts +78 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Outbound agent registry: the persisted list of connected remote agents,
3
+ * their connection state, and the tool-registration disposers for each live
4
+ * connection. Loaded at boot, mutations persist through the agent store.
5
+ * @module dsh-a2a/client/registry
6
+ */
7
+
8
+ import { A2AClient } from './calls.ts'
9
+ import { registerAgentTools, type ToolRegistrar } from './tools.ts'
10
+ import { type OutboundAgentRecord } from '../server/store.ts'
11
+
12
+ /** User-supplied agent spec (from config or the /a2a command). */
13
+ export interface OutboundAgentSpec {
14
+ readonly name: string
15
+ readonly agentCardUrl: string
16
+ readonly bearerTokenEnv?: string
17
+ readonly enabled?: boolean
18
+ readonly timeoutMs?: number
19
+ }
20
+
21
+ export interface OutboundAgentView {
22
+ readonly id: string
23
+ readonly name: string
24
+ readonly agentCardUrl: string
25
+ readonly enabled: boolean
26
+ readonly state: 'connected' | 'disconnected' | 'failed'
27
+ readonly skillCount: number
28
+ readonly toolCount: number
29
+ readonly lastError?: string
30
+ }
31
+
32
+ /** Durable store for the registry records. */
33
+ export interface AgentStore {
34
+ list(): OutboundAgentRecord[]
35
+ save(records: readonly OutboundAgentRecord[]): Promise<void>
36
+ }
37
+
38
+ /** In-memory agent store (tests and minimal embeddings). */
39
+ export class MemoryAgentStore implements AgentStore {
40
+ private records: OutboundAgentRecord[] = []
41
+
42
+ constructor(initial: readonly OutboundAgentRecord[] = []) {
43
+ this.records = [...initial]
44
+ }
45
+
46
+ list(): OutboundAgentRecord[] {
47
+ return [...this.records]
48
+ }
49
+
50
+ async save(records: readonly OutboundAgentRecord[]): Promise<void> {
51
+ this.records = [...records]
52
+ }
53
+ }
54
+
55
+ /** Domain-backed agent store: one JSON array under the `agents` table key. */
56
+ export class DomainAgentStore implements AgentStore {
57
+ private static readonly KEY = 'records'
58
+
59
+ constructor(private readonly table: { get(key: string): string | undefined; put(key: string, value: string): Promise<void> }) {}
60
+
61
+ list(): OutboundAgentRecord[] {
62
+ const raw = this.table.get(DomainAgentStore.KEY)
63
+ if (raw === undefined) return []
64
+ try {
65
+ const parsed = JSON.parse(raw) as unknown
66
+ return Array.isArray(parsed) ? (parsed as OutboundAgentRecord[]) : []
67
+ } catch {
68
+ return []
69
+ }
70
+ }
71
+
72
+ async save(records: readonly OutboundAgentRecord[]): Promise<void> {
73
+ await this.table.put(DomainAgentStore.KEY, JSON.stringify(records))
74
+ }
75
+ }
76
+
77
+ export interface RegistryOptions {
78
+ readonly registrar: ToolRegistrar
79
+ readonly store: AgentStore
80
+ readonly toolPrefix?: string
81
+ readonly defaultTimeoutMs?: number
82
+ /** Resolve a bearer token from an env-var name; undefined when unset. */
83
+ readonly tokenOf: (env: string | undefined) => string | undefined
84
+ readonly onError?: (message: string) => void
85
+ }
86
+
87
+ interface LiveConnection {
88
+ readonly record: OutboundAgentRecord
89
+ readonly client: A2AClient
90
+ readonly disposers: readonly { dispose(): void }[]
91
+ readonly skillCount: number
92
+ }
93
+
94
+ /** A list of remote agents, each with live tool registrations when enabled. */
95
+ export class OutboundAgentRegistry {
96
+ private readonly live = new Map<string, LiveConnection>()
97
+ private readonly views = new Map<string, OutboundAgentView>()
98
+ private seedRecords: OutboundAgentRecord[] | undefined
99
+
100
+ constructor(private readonly opts: RegistryOptions) {}
101
+
102
+ /** Load persisted records and connect every enabled agent (background). */
103
+ loadAll(): void {
104
+ const records = this.opts.store.list()
105
+ this.seedRecords = records
106
+ for (const record of records) {
107
+ if (record.enabled) void this.connectRecord(record).catch((err: unknown) => {
108
+ this.opts.onError?.(`[a2a] failed to connect "${record.name}": ${(err as Error).message}`)
109
+ })
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Seed configured agents that the persisted store does not know yet. The
115
+ * store is the runtime state: an existing record (enabled or disabled,
116
+ * present or removed) keeps its state — a disabled agent stays
117
+ * disconnected and a removed one is not resurrected. Only specs with a name
118
+ * absent from the store (and not already live this boot) connect and
119
+ * persist.
120
+ * @param specs - declared agents from the profile config.
121
+ */
122
+ async seed(specs: readonly OutboundAgentSpec[]): Promise<void> {
123
+ const known = new Set(this.opts.store.list().map((r) => r.name))
124
+ for (const connection of this.live.values()) known.add(connection.record.name)
125
+ for (const spec of specs) {
126
+ if (known.has(spec.name)) continue
127
+ const result = await this.add(spec)
128
+ if (result.ok) known.add(spec.name)
129
+ else this.opts.onError?.(`[a2a] seed agent "${spec.name}": ${result.message}`)
130
+ }
131
+ }
132
+
133
+ list(): OutboundAgentView[] {
134
+ return [...this.views.values()]
135
+ }
136
+
137
+ /** Add and connect an agent, persisting the record on success. */
138
+ async add(spec: OutboundAgentSpec): Promise<{ ok: boolean; message: string }> {
139
+ const record: OutboundAgentRecord = {
140
+ id: `agent-${crypto.randomUUID()}`,
141
+ name: spec.name,
142
+ agentCardUrl: spec.agentCardUrl,
143
+ ...(spec.bearerTokenEnv ? { bearerTokenEnv: spec.bearerTokenEnv } : {}),
144
+ enabled: spec.enabled ?? true,
145
+ timeoutMs: spec.timeoutMs ?? this.opts.defaultTimeoutMs ?? 60_000,
146
+ lastCardAt: null,
147
+ }
148
+ try {
149
+ await this.connectRecord(record)
150
+ } catch (err) {
151
+ this.views.set(record.id, {
152
+ id: record.id,
153
+ name: record.name,
154
+ agentCardUrl: record.agentCardUrl,
155
+ enabled: record.enabled,
156
+ state: 'failed',
157
+ skillCount: 0,
158
+ toolCount: 0,
159
+ lastError: (err as Error).message,
160
+ })
161
+ return { ok: false, message: `failed to connect to ${record.agentCardUrl}: ${(err as Error).message}` }
162
+ }
163
+ this.persist(record)
164
+ return { ok: true, message: `agent "${record.name}" connected` }
165
+ }
166
+
167
+ async remove(id: string): Promise<{ ok: boolean; message: string }> {
168
+ const live = this.live.get(id)
169
+ const view = this.views.get(id)
170
+ if (live === undefined && view === undefined) return { ok: false, message: `agent ${id} not found` }
171
+ for (const disposer of live?.disposers ?? []) disposer.dispose()
172
+ this.live.delete(id)
173
+ this.views.delete(id)
174
+ await this.persistAll()
175
+ return { ok: true, message: `agent ${id} removed` }
176
+ }
177
+
178
+ async setEnabled(id: string, enabled: boolean): Promise<{ ok: boolean; message: string }> {
179
+ const live = this.live.get(id)
180
+ const view = this.views.get(id)
181
+ if (live === undefined && view === undefined) return { ok: false, message: `agent ${id} not found` }
182
+ if (enabled && live === undefined) {
183
+ const record = this.findRecord(id)
184
+ if (record === undefined) return { ok: false, message: `agent ${id} has no persisted record` }
185
+ try {
186
+ await this.connectRecord(record)
187
+ } catch (err) {
188
+ return { ok: false, message: `failed to connect: ${(err as Error).message}` }
189
+ }
190
+ await this.persistAll()
191
+ return { ok: true, message: `agent ${id} enabled` }
192
+ }
193
+ if (!enabled && live !== undefined) {
194
+ for (const disposer of live.disposers) disposer.dispose()
195
+ this.live.delete(id)
196
+ const current = this.views.get(id)
197
+ if (current !== undefined) {
198
+ this.views.set(id, { ...current, enabled: false, state: 'disconnected', toolCount: 0, skillCount: 0 })
199
+ }
200
+ await this.persistAll()
201
+ return { ok: true, message: `agent ${id} disabled` }
202
+ }
203
+ return { ok: true, message: `agent ${id} already ${enabled ? 'enabled' : 'disabled'}` }
204
+ }
205
+
206
+ async refresh(id: string): Promise<{ ok: boolean; message: string }> {
207
+ const record = this.findRecord(id) ?? this.live.get(id)?.record
208
+ if (record === undefined) return { ok: false, message: `agent ${id} not found` }
209
+ const live = this.live.get(id)
210
+ for (const disposer of live?.disposers ?? []) disposer.dispose()
211
+ this.live.delete(id)
212
+ try {
213
+ await this.connectRecord(record)
214
+ } catch (err) {
215
+ return { ok: false, message: `refresh failed: ${(err as Error).message}` }
216
+ }
217
+ await this.persistAll()
218
+ return { ok: true, message: `agent "${record.name}" reconnected` }
219
+ }
220
+
221
+ async disposeAll(): Promise<void> {
222
+ for (const connection of this.live.values()) {
223
+ for (const disposer of connection.disposers) disposer.dispose()
224
+ }
225
+ this.live.clear()
226
+ this.views.clear()
227
+ }
228
+
229
+ private async connectRecord(record: OutboundAgentRecord): Promise<void> {
230
+ const token = this.opts.tokenOf(record.bearerTokenEnv)
231
+ const client = await A2AClient.connect(record.agentCardUrl, {
232
+ ...(token ? { bearerToken: token } : {}),
233
+ timeoutMs: record.timeoutMs,
234
+ })
235
+ const contextByAgent = new Map<string, string>()
236
+ const fallback = `a2a-out-${crypto.randomUUID()}`
237
+ const contextFor = (agentId: string | undefined): string => {
238
+ const key = agentId ?? '\0fallback'
239
+ const existing = contextByAgent.get(key)
240
+ if (existing !== undefined) return existing
241
+ const id = agentId ? `a2a-out-${crypto.randomUUID()}` : fallback
242
+ contextByAgent.set(key, id)
243
+ return id
244
+ }
245
+ const disposers = registerAgentTools(this.opts.registrar, {
246
+ agentName: record.name,
247
+ client,
248
+ ...(this.opts.toolPrefix !== undefined ? { toolPrefix: this.opts.toolPrefix } : {}),
249
+ contextFor,
250
+ })
251
+ const skillCount = client.card.skills?.length ?? 0
252
+ this.live.set(record.id, { record, client, disposers, skillCount })
253
+ this.views.set(record.id, {
254
+ id: record.id,
255
+ name: record.name,
256
+ agentCardUrl: record.agentCardUrl,
257
+ enabled: record.enabled,
258
+ state: 'connected',
259
+ skillCount,
260
+ toolCount: disposers.length,
261
+ })
262
+ }
263
+
264
+ private findRecord(id: string): OutboundAgentRecord | undefined {
265
+ const current = () => this.opts.store.list().find((r) => r.id === id)
266
+ return current() ?? this.seedRecords?.find((r) => r.id === id)
267
+ }
268
+
269
+ private persist(record: OutboundAgentRecord): void {
270
+ const next = [...this.opts.store.list().filter((r) => r.id !== record.id), record]
271
+ void this.opts.store.save(next).catch((err: unknown) => {
272
+ this.opts.onError?.(`[a2a] failed to persist agent list: ${String(err)}`)
273
+ })
274
+ }
275
+
276
+ private async persistAll(): Promise<void> {
277
+ const liveRecords = [...this.live.values()].map((c) => c.record)
278
+ const liveIds = new Set(liveRecords.map((r) => r.id))
279
+ // Non-live known records (disabled agents) keep the store record with the
280
+ // view's up-to-date enabled flag; removed agents (gone from live AND
281
+ // views) are dropped so a restart does not resurrect them.
282
+ const kept = this.opts.store.list()
283
+ .filter((r) => !liveIds.has(r.id) && this.views.has(r.id))
284
+ .map((r) => {
285
+ const view = this.views.get(r.id)
286
+ return view === undefined ? r : { ...r, enabled: view.enabled }
287
+ })
288
+ await this.opts.store.save([...kept, ...liveRecords])
289
+ }
290
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Outbound half: register one remote agent's skills as model-facing tools on
3
+ * `ctx.tools`. Tool names are `a2a__<agent>__<skill>` normalized to the DSH
4
+ * function-name contract (≤64 chars, collision hash), mirroring the naming
5
+ * rule of dsh-mcp-client. An agent advertising zero skills registers nothing.
6
+ * @module dsh-a2a/client/tools
7
+ */
8
+
9
+ import { Role, partsToText, type AgentSkill, type Message, type Part } from '../protocol.ts'
10
+ import { A2AError, type A2AClient } from './calls.ts'
11
+
12
+ /** Structural slice of `ctx.tools` used for registration. */
13
+ export interface ToolRegistrar {
14
+ register(definition: unknown): (() => void) | void
15
+ }
16
+
17
+ export interface AgentToolsOptions {
18
+ readonly agentName: string
19
+ readonly client: A2AClient
20
+ /** Tool prefix; default 'a2a'. */
21
+ readonly toolPrefix?: string
22
+ /**
23
+ * Resolve the remote conversation contextId for one calling local agent
24
+ * (stable per caller, so multi-turn tool use is one remote conversation).
25
+ */
26
+ readonly contextFor: (agentId: string | undefined) => string
27
+ }
28
+
29
+ export interface RegisteredTool {
30
+ readonly name: string
31
+ dispose(): void
32
+ }
33
+
34
+ function normalizeToolName(raw: string): string {
35
+ const norm = raw.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 64)
36
+ if (norm.length > 0 && /^[A-Za-z0-9_-]+$/.test(norm)) return norm
37
+ let hash = 0
38
+ for (let i = 0; i < raw.length; i++) hash = (hash * 31 + raw.charCodeAt(i)) >>> 0
39
+ return `_${hash.toString(16)}`
40
+ }
41
+
42
+ /** Register one tool per advertised skill; empty skills register nothing. */
43
+ export function registerAgentTools(registrar: ToolRegistrar, opts: AgentToolsOptions): RegisteredTool[] {
44
+ const prefix = opts.toolPrefix ?? 'a2a'
45
+ const skills = opts.client.card.skills ?? []
46
+ const out: RegisteredTool[] = []
47
+ for (const skill of skills) {
48
+ const raw = `${prefix}__${opts.agentName}__${skill.id}`
49
+ const name = normalizeToolName(raw)
50
+ let toolName = name
51
+ if (out.some((t) => t.name === toolName)) {
52
+ let hash = 0
53
+ for (let i = 0; i < raw.length; i++) hash = (hash * 31 + raw.charCodeAt(i)) >>> 0
54
+ toolName = `${name.slice(0, 56)}_${hash.toString(16)}`
55
+ }
56
+ const dispose = registrar.register(makeTool(toolName, opts.client, skill, opts.contextFor))
57
+ if (dispose) out.push({ name: toolName, dispose })
58
+ }
59
+ return out
60
+ }
61
+
62
+ function makeTool(
63
+ name: string,
64
+ client: A2AClient,
65
+ skill: Pick<AgentSkill, 'id' | 'name' | 'description' | 'examples'>,
66
+ contextFor: (agentId: string | undefined) => string,
67
+ ): unknown {
68
+ const description = [
69
+ skill.description,
70
+ ...(skill.examples?.length ? [`Examples: ${skill.examples.join(' | ')}`] : []),
71
+ ].filter(Boolean).join('\n')
72
+
73
+ return {
74
+ name,
75
+ description,
76
+ parameters: {
77
+ type: 'object',
78
+ properties: {
79
+ prompt: {
80
+ type: 'string',
81
+ description: 'The task or instruction to send to the remote agent.',
82
+ },
83
+ },
84
+ required: ['prompt'],
85
+ },
86
+ output: {
87
+ schema: { type: 'string' },
88
+ render: (_args: unknown, value: unknown) => [{ type: 'text', text: String(value) }],
89
+ },
90
+ async execute(args: { prompt: string }, exec: { signal?: AbortSignal; agent?: { id?: string } }): Promise<string> {
91
+ const message: Message = {
92
+ messageId: crypto.randomUUID(),
93
+ role: Role.USER,
94
+ contextId: contextFor(exec.agent?.id),
95
+ parts: [{ text: args.prompt }],
96
+ }
97
+ const task = await client.sendMessage(message)
98
+ return textOf(task.artifacts, task.status.state, task.status.message?.parts)
99
+ },
100
+ }
101
+ }
102
+
103
+ /** Render a settled task's output as tool text (INPUT_REQUIRED surfaced, not thrown). */
104
+ function textOf(
105
+ artifacts: readonly { readonly parts?: readonly Part[] }[] | undefined,
106
+ state: string,
107
+ statusParts: readonly Part[] | undefined,
108
+ ): string {
109
+ const output = partsToText(artifacts?.flatMap((a) => a.parts ?? []))
110
+ if (state === 'FAILED') {
111
+ throw new A2AError(-32000, `Remote agent task failed: ${output || partsToText(statusParts) || 'no detail'}`)
112
+ }
113
+ if (state === 'INPUT_REQUIRED' || state === 'AUTH_REQUIRED') {
114
+ const ask = partsToText(statusParts) || output || 'agent awaits input'
115
+ return `[remote agent ${state}] ${ask}`
116
+ }
117
+ return output || '(remote agent returned no output)'
118
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Agent2Agent (A2A) Protocol v1.0 — types, task states, JSON-RPC methods and
3
+ * error codes used by this plugin's JSON-RPC over HTTP binding.
4
+ *
5
+ * The surface mirrors the normative `a2a.proto` of the A2A project (Apache-2.0);
6
+ * this file is the plugin's own transcription and the single source of protocol
7
+ * truth. Bindings other than JSON-RPC over HTTP (gRPC, REST) are not provided.
8
+ * @module dsh-a2a/protocol
9
+ */
10
+
11
+ /** Task lifecycle states (spec `TaskState`). */
12
+ export enum TaskState {
13
+ SUBMITTED = 'SUBMITTED',
14
+ WORKING = 'WORKING',
15
+ INPUT_REQUIRED = 'INPUT_REQUIRED',
16
+ AUTH_REQUIRED = 'AUTH_REQUIRED',
17
+ COMPLETED = 'COMPLETED',
18
+ FAILED = 'FAILED',
19
+ CANCELED = 'CANCELED',
20
+ REJECTED = 'REJECTED',
21
+ }
22
+
23
+ /** States that settle a task; a settled task no longer transitions. */
24
+ export const TERMINAL_STATES: ReadonlySet<TaskState> = new Set([
25
+ TaskState.COMPLETED,
26
+ TaskState.FAILED,
27
+ TaskState.CANCELED,
28
+ TaskState.REJECTED,
29
+ ])
30
+
31
+ /** @returns whether the state settles the task. */
32
+ export function isTerminal(state: TaskState): boolean {
33
+ return TERMINAL_STATES.has(state)
34
+ }
35
+
36
+ /** Message sender role (spec `Role`). */
37
+ export enum Role {
38
+ USER = 'user',
39
+ AGENT = 'agent',
40
+ }
41
+
42
+ /** One message part: text, file (bytes or uri), or structured data. */
43
+ export type Part =
44
+ | { readonly text: string; readonly metadata?: Record<string, unknown> }
45
+ | {
46
+ readonly file: {
47
+ readonly mimeType?: string
48
+ readonly name?: string
49
+ readonly bytes?: string
50
+ readonly uri?: string
51
+ }
52
+ readonly metadata?: Record<string, unknown>
53
+ }
54
+ | { readonly data: unknown; readonly metadata?: Record<string, unknown> }
55
+
56
+ /** An interaction payload exchanged between agents. */
57
+ export interface Message {
58
+ readonly messageId: string
59
+ readonly role: Role
60
+ readonly contextId?: string
61
+ /** Continuation: attaches this message to an existing task's conversation. */
62
+ readonly taskId?: string
63
+ readonly parts: readonly Part[]
64
+ readonly metadata?: Record<string, unknown>
65
+ }
66
+
67
+ /** Current state of a task, with an optional explanatory message. */
68
+ export interface TaskStatus {
69
+ readonly state: TaskState
70
+ readonly message?: Message
71
+ readonly timestamp: string
72
+ }
73
+
74
+ /** A chunk of task output. */
75
+ export interface Artifact {
76
+ readonly name?: string
77
+ readonly parts: readonly Part[]
78
+ readonly artifactId?: string
79
+ }
80
+
81
+ /** A task: the durable work unit of A2A. */
82
+ export interface Task {
83
+ readonly id: string
84
+ readonly contextId?: string
85
+ readonly status: TaskStatus
86
+ readonly artifacts?: readonly Artifact[]
87
+ readonly history?: readonly Message[]
88
+ readonly metadata?: Record<string, unknown>
89
+ }
90
+
91
+ /** JSON-RPC 2.0 request as used by the A2A binding. */
92
+ export interface JsonRpcRequest {
93
+ readonly jsonrpc: '2.0'
94
+ readonly id?: string | number | null
95
+ readonly method: string
96
+ readonly params?: unknown
97
+ }
98
+
99
+ export interface JsonRpcSuccess {
100
+ readonly jsonrpc: '2.0'
101
+ readonly id: string | number | null
102
+ readonly result: unknown
103
+ }
104
+
105
+ export interface JsonRpcErrorBody {
106
+ readonly code: number
107
+ readonly message: string
108
+ readonly data?: unknown
109
+ }
110
+
111
+ export interface JsonRpcError {
112
+ readonly jsonrpc: '2.0'
113
+ readonly id: string | number | null
114
+ readonly error: JsonRpcErrorBody
115
+ }
116
+
117
+ export type JsonRpcResponse = JsonRpcSuccess | JsonRpcError
118
+
119
+ /** A skill advertised by an agent. */
120
+ export interface AgentSkill {
121
+ readonly id: string
122
+ readonly name?: string
123
+ readonly description?: string
124
+ readonly tags?: readonly string[]
125
+ readonly examples?: readonly string[]
126
+ readonly inputModes?: readonly string[]
127
+ readonly outputModes?: readonly string[]
128
+ }
129
+
130
+ export interface AgentCapabilities {
131
+ readonly streaming?: boolean
132
+ readonly pushNotifications?: boolean
133
+ readonly stateTransitionHistory?: boolean
134
+ readonly extensions?: readonly string[]
135
+ }
136
+
137
+ /** One supported interface (transport binding) of an agent. */
138
+ export interface AgentInterface {
139
+ readonly url: string
140
+ readonly protocolBinding?: 'JSONRPC' | 'REST' | 'gRPC' | string
141
+ readonly protocolVersion?: string
142
+ readonly authSchemes?: readonly string[]
143
+ }
144
+
145
+ /** Bearer-token security scheme advertised by the AgentCard. */
146
+ export interface AgentSecurityScheme {
147
+ readonly type: 'http'
148
+ readonly scheme: 'bearer'
149
+ readonly description?: string
150
+ }
151
+
152
+ /** The discovery manifest of an A2A agent. */
153
+ export interface AgentCard {
154
+ readonly name: string
155
+ readonly description: string
156
+ readonly version: string
157
+ readonly url?: string
158
+ readonly provider?: { readonly url: string; readonly organization: string }
159
+ readonly skills?: readonly AgentSkill[]
160
+ readonly capabilities?: AgentCapabilities
161
+ readonly defaultInputModes?: readonly string[]
162
+ readonly defaultOutputModes?: readonly string[]
163
+ readonly securitySchemes?: Record<string, AgentSecurityScheme>
164
+ readonly securityRequirements?: readonly Record<string, readonly string[]>[]
165
+ readonly supportedInterfaces?: readonly AgentInterface[]
166
+ readonly custom?: Record<string, unknown>
167
+ }
168
+
169
+ /** Streamed updates during a task run (SSE payloads). */
170
+ export type StreamResponse =
171
+ | { readonly statusUpdate: { readonly taskId: string; readonly contextId?: string; readonly status: TaskStatus } }
172
+ | { readonly artifactUpdate: { readonly taskId: string; readonly contextId?: string; readonly artifact: Artifact; readonly lastChunk?: boolean } }
173
+ | { readonly task: Task }
174
+ | { readonly error: { readonly code: number; readonly message: string } }
175
+
176
+ /** A2A v1.0 JSON-RPC method names (canonical PascalCase as in a2a.proto). */
177
+ export const A2A_METHODS = {
178
+ sendMessage: 'SendMessage',
179
+ sendStreamingMessage: 'SendStreamingMessage',
180
+ getTask: 'GetTask',
181
+ listTasks: 'ListTasks',
182
+ cancelTask: 'CancelTask',
183
+ subscribeToTask: 'SubscribeToTask',
184
+ getExtendedAgentCard: 'GetExtendedAgentCard',
185
+ } as const
186
+
187
+ /** JSON-RPC error codes for the A2A binding (spec §error codes). */
188
+ export const A2A_ERROR_CODES = {
189
+ INVALID_REQUEST: -32600,
190
+ METHOD_NOT_FOUND: -32601,
191
+ INVALID_PARAMS: -32602,
192
+ INTERNAL_ERROR: -32603,
193
+ UNAUTHORIZED: -32000,
194
+ TASK_NOT_FOUND: -32001,
195
+ TASK_CANCEL_NOT_ALLOWED: -32002,
196
+ AGENT_CARD_NOT_FOUND: -32004,
197
+ AGENT_CARD_SIGNATURE_INVALID: -32005,
198
+ } as const
199
+
200
+ /** Read a message's text parts as a single string (model-facing convenience). */
201
+ export function partsToText(parts: readonly Part[] | undefined): string {
202
+ if (!parts) return ''
203
+ return parts
204
+ .map((p) => {
205
+ if ('text' in p && p.text) return p.text
206
+ if ('data' in p && p.data !== undefined) return JSON.stringify(p.data)
207
+ if ('file' in p) return p.file.uri ?? `[file ${p.file.name ?? p.file.mimeType ?? 'binary'}]`
208
+ return ''
209
+ })
210
+ .filter(Boolean)
211
+ .join('\n')
212
+ }