@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,320 @@
1
+ /**
2
+ * Durable task storage for the inbound half: one `a2a` domain over the routed
3
+ * storage backend (the deployment picks the medium, normally SQLite). Records
4
+ * are serialized JSON so the domain tables stay plain kv rows; the domain
5
+ * SCHEMA_VERSION guards format evolution. A memory store keeps unit tests and
6
+ * minimal embeddings free of a backend.
7
+ * @module dsh-a2a/server/store
8
+ */
9
+
10
+ import { z } from 'zod'
11
+ import {
12
+ defineDomain,
13
+ domainTable,
14
+ type Domain,
15
+ type DomainFacility,
16
+ type KvTable,
17
+ } from '@deepseek-ai/dsh-storage-domain'
18
+ import { TaskState, type Part } from '../protocol.ts'
19
+
20
+ /** One durable inbound/outbound task record. */
21
+ export interface TaskRecord {
22
+ readonly taskId: string
23
+ readonly contextId: string
24
+ readonly skill: string
25
+ readonly state: TaskState
26
+ readonly createdAt: string
27
+ readonly updatedAt: string
28
+ readonly sessionId: string | null
29
+ readonly remotePeerId: string | null
30
+ readonly parts: readonly Part[]
31
+ readonly artifacts: readonly { readonly artifactId: string; readonly name?: string; readonly parts: readonly Part[] }[]
32
+ readonly executor: 'session' | 'subagent'
33
+ readonly summary: string | null
34
+ readonly error?: { readonly code: string; readonly message: string }
35
+ }
36
+
37
+ /** One durable context→session binding (inbound conversation ↔ DSH session). */
38
+ export interface ContextBinding {
39
+ readonly sessionId: string
40
+ }
41
+
42
+ /** One durable outbound agent record (client registry persistence). */
43
+ export interface OutboundAgentRecord {
44
+ readonly id: string
45
+ readonly name: string
46
+ readonly agentCardUrl: string
47
+ readonly bearerTokenEnv?: string
48
+ readonly enabled: boolean
49
+ readonly timeoutMs: number
50
+ readonly lastCardAt: string | null
51
+ }
52
+
53
+ /** JSON-encoded record schema shared by every `a2a` table. */
54
+ const json = z.string()
55
+
56
+ /** The `a2a` storage domain: JSON-encoded records, one table per record kind. */
57
+ export const a2aDomainSpec = defineDomain({
58
+ name: 'a2a',
59
+ version: 1,
60
+ tables: {
61
+ tasks: domainTable<string, string>(json),
62
+ contexts: domainTable<string, string>(json),
63
+ agents: domainTable<string, string>(json),
64
+ },
65
+ })
66
+
67
+ export type A2aDomainSpec = typeof a2aDomainSpec
68
+
69
+ /**
70
+ * Serialize/deserialize helpers for the domain tables. Plain JSON round-trip
71
+ * with shape assertions; `decodeRecord` returns undefined on any mismatch.
72
+ */
73
+ export function encodeRecord(value: unknown): string {
74
+ return JSON.stringify(value)
75
+ }
76
+
77
+ export function decodeRecord<T>(raw: string): T | undefined {
78
+ try {
79
+ return JSON.parse(raw) as T
80
+ } catch {
81
+ return undefined
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Open the `a2a` domain on the mounted storage-domain facility and expose
87
+ * its tables. The caller owns the returned handle and closes it on teardown.
88
+ */
89
+ export async function openDomain(domain: DomainFacility): Promise<A2aDomain> {
90
+ const handle = await domain.open(a2aDomainSpec)
91
+ return new A2aDomain(handle)
92
+ }
93
+
94
+ /** A2a domain facade: typed JSON tables over the routed backend. */
95
+ export class A2aDomain {
96
+ constructor(readonly handle: Domain<A2aDomainSpec>) {}
97
+
98
+ get tasks(): KvTable<string, string> {
99
+ return this.handle.table('tasks')
100
+ }
101
+
102
+ get contexts(): KvTable<string, string> {
103
+ return this.handle.table('contexts')
104
+ }
105
+
106
+ get agents(): KvTable<string, string> {
107
+ return this.handle.table('agents')
108
+ }
109
+
110
+ async close(): Promise<void> {
111
+ await this.handle.close()
112
+ }
113
+ }
114
+
115
+ /** Task-store contract shared by the domain-backed and memory stores. */
116
+ export interface TaskStore {
117
+ create(input: { contextId: string; skill: string; parts: readonly Part[]; remotePeerId: string | null }): TaskRecord
118
+ get(taskId: string): TaskRecord | undefined
119
+ list(): TaskRecord[]
120
+ setState(taskId: string, state: TaskState, message?: { code: string; text: string }): TaskRecord
121
+ appendArtifact(taskId: string, artifact: { artifactId: string; name?: string; parts: readonly Part[]; lastChunk?: boolean }): TaskRecord
122
+ setContextSession(contextId: string, sessionId: string): Promise<void>
123
+ getContextSession(contextId: string): Promise<string | undefined>
124
+ close(): Promise<void>
125
+ }
126
+
127
+ function now(): string {
128
+ return new Date().toISOString()
129
+ }
130
+
131
+ function recordKey(taskId: string): string {
132
+ return `task:${taskId}`
133
+ }
134
+
135
+ function contextKey(contextId: string): string {
136
+ return `ctx:${contextId}`
137
+ }
138
+
139
+ /** In-memory TaskStore (unit tests, minimal embeddings). */
140
+ export class MemoryTaskStore implements TaskStore {
141
+ private tasks = new Map<string, TaskRecord>()
142
+ private contexts = new Map<string, string>()
143
+
144
+ create(input: { contextId: string; skill: string; parts: readonly Part[]; remotePeerId: string | null }): TaskRecord {
145
+ const record: TaskRecord = {
146
+ taskId: `a2a-${crypto.randomUUID()}`,
147
+ contextId: input.contextId,
148
+ skill: input.skill,
149
+ state: TaskState.SUBMITTED,
150
+ createdAt: now(),
151
+ updatedAt: now(),
152
+ sessionId: null,
153
+ remotePeerId: input.remotePeerId,
154
+ parts: input.parts,
155
+ artifacts: [],
156
+ executor: 'session',
157
+ summary: null,
158
+ }
159
+ this.tasks.set(record.taskId, record)
160
+ return record
161
+ }
162
+
163
+ get(taskId: string): TaskRecord | undefined {
164
+ return this.tasks.get(taskId)
165
+ }
166
+
167
+ list(): TaskRecord[] {
168
+ return [...this.tasks.values()]
169
+ }
170
+
171
+ setState(taskId: string, state: TaskState, message?: { code: string; text: string }): TaskRecord {
172
+ const current = this.tasks.get(taskId)
173
+ if (current === undefined) throw new Error(`task ${taskId} not found`)
174
+ const next: TaskRecord = {
175
+ ...current,
176
+ state,
177
+ updatedAt: now(),
178
+ ...(message ? { error: { code: message.code, message: message.text } } : current.error ? { error: current.error } : {}),
179
+ }
180
+ this.tasks.set(taskId, next)
181
+ return next
182
+ }
183
+
184
+ appendArtifact(taskId: string, artifact: { artifactId: string; name?: string; parts: readonly Part[]; lastChunk?: boolean }): TaskRecord {
185
+ const current = this.tasks.get(taskId)
186
+ if (current === undefined) throw new Error(`task ${taskId} not found`)
187
+ const next: TaskRecord = {
188
+ ...current,
189
+ updatedAt: now(),
190
+ artifacts: appendArtifactRecord(current.artifacts, artifact),
191
+ }
192
+ this.tasks.set(taskId, next)
193
+ return next
194
+ }
195
+
196
+ async setContextSession(contextId: string, sessionId: string): Promise<void> {
197
+ this.contexts.set(contextId, sessionId)
198
+ }
199
+
200
+ async getContextSession(contextId: string): Promise<string | undefined> {
201
+ return this.contexts.get(contextId)
202
+ }
203
+
204
+ async close(): Promise<void> {}
205
+ }
206
+
207
+ function appendArtifactRecord(
208
+ artifacts: TaskRecord['artifacts'],
209
+ artifact: { artifactId: string; name?: string; parts: readonly Part[] },
210
+ ): TaskRecord['artifacts'] {
211
+ const existing = artifacts.find((a) => a.artifactId === artifact.artifactId)
212
+ if (existing) {
213
+ return artifacts.map((a) => (a.artifactId === artifact.artifactId ? { ...a, parts: [...a.parts, ...artifact.parts] } : a))
214
+ }
215
+ return [...artifacts, { artifactId: artifact.artifactId, ...(artifact.name !== undefined ? { name: artifact.name } : {}), parts: artifact.parts }]
216
+ }
217
+
218
+ /** Domain-backed TaskStore (persistent; the composition path). */
219
+ export class DomainTaskStore implements TaskStore {
220
+ // The real `KvTable.put` queues on the domain's write chain: durability
221
+ // first, then the in-memory table updates. TaskStore's synchronous readers
222
+ // must see their own writes immediately, so this store keeps the live view
223
+ // in memory, seeds it from the domain's opened tables (already loaded from
224
+ // the medium), updates it before each queued write, and never reads the
225
+ // table back for its own writes.
226
+ private readonly records = new Map<string, TaskRecord>()
227
+ private readonly bindings = new Map<string, string>()
228
+
229
+ constructor(private readonly domain: A2aDomain) {
230
+ for (const [key, raw] of domain.tasks.entries()) {
231
+ if (!key.startsWith('task:')) continue
232
+ const record = decodeRecord<TaskRecord>(raw)
233
+ if (record !== undefined) this.records.set(record.taskId, record)
234
+ }
235
+ for (const [key, raw] of domain.contexts.entries()) {
236
+ if (!key.startsWith('ctx:')) continue
237
+ const binding = decodeRecord<ContextBinding>(raw)
238
+ if (binding !== undefined) this.bindings.set(key.slice('ctx:'.length), binding.sessionId)
239
+ }
240
+ }
241
+
242
+ create(input: { contextId: string; skill: string; parts: readonly Part[]; remotePeerId: string | null }): TaskRecord {
243
+ const record: TaskRecord = {
244
+ taskId: `a2a-${crypto.randomUUID()}`,
245
+ contextId: input.contextId,
246
+ skill: input.skill,
247
+ state: TaskState.SUBMITTED,
248
+ createdAt: now(),
249
+ updatedAt: now(),
250
+ sessionId: null,
251
+ remotePeerId: input.remotePeerId,
252
+ parts: input.parts,
253
+ artifacts: [],
254
+ executor: 'session',
255
+ summary: null,
256
+ }
257
+ this.records.set(record.taskId, record)
258
+ // Persist eagerly; a failed write must fail the task, not silently lose it.
259
+ void this.domain.tasks.put(recordKey(record.taskId), encodeRecord(record)).catch((err: unknown) => {
260
+ throw new Error(`a2a: task persistence failed: ${String(err)}`)
261
+ })
262
+ return record
263
+ }
264
+
265
+ get(taskId: string): TaskRecord | undefined {
266
+ return this.records.get(taskId)
267
+ }
268
+
269
+ list(): TaskRecord[] {
270
+ return [...this.records.values()]
271
+ }
272
+
273
+ setState(taskId: string, state: TaskState, message?: { code: string; text: string }): TaskRecord {
274
+ const current = this.records.get(taskId)
275
+ if (current === undefined) throw new Error(`task ${taskId} not found`)
276
+ const next: TaskRecord = withState(current, state, message, now())
277
+ this.records.set(taskId, next)
278
+ void this.domain.tasks.put(recordKey(taskId), encodeRecord(next)).catch((err: unknown) => {
279
+ throw new Error(`a2a: task persistence failed: ${String(err)}`)
280
+ })
281
+ return next
282
+ }
283
+
284
+ appendArtifact(taskId: string, artifact: { artifactId: string; name?: string; parts: readonly Part[]; lastChunk?: boolean }): TaskRecord {
285
+ const current = this.records.get(taskId)
286
+ if (current === undefined) throw new Error(`task ${taskId} not found`)
287
+ const next: TaskRecord = { ...current, updatedAt: now(), artifacts: appendArtifactRecord(current.artifacts, artifact) }
288
+ this.records.set(taskId, next)
289
+ void this.domain.tasks.put(recordKey(taskId), encodeRecord(next)).catch((err: unknown) => {
290
+ throw new Error(`a2a: task persistence failed: ${String(err)}`)
291
+ })
292
+ return next
293
+ }
294
+
295
+ async setContextSession(contextId: string, sessionId: string): Promise<void> {
296
+ const binding: ContextBinding = { sessionId }
297
+ this.bindings.set(contextId, sessionId)
298
+ await this.domain.contexts.put(contextKey(contextId), encodeRecord(binding))
299
+ }
300
+
301
+ async getContextSession(contextId: string): Promise<string | undefined> {
302
+ return this.bindings.get(contextId)
303
+ }
304
+
305
+ async close(): Promise<void> {}
306
+ }
307
+
308
+ function withState(
309
+ current: TaskRecord,
310
+ state: TaskState,
311
+ message: { code: string; text: string } | undefined,
312
+ timestamp: string,
313
+ ): TaskRecord {
314
+ return {
315
+ ...current,
316
+ state,
317
+ updatedAt: timestamp,
318
+ ...(message ? { error: { code: message.code, message: message.text } } : {}),
319
+ }
320
+ }
package/src/service.ts ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The `ctx.a2a` service facade: the thin read/control surface commands and
3
+ * other plugins use. Registered as a Cordis Service so the key disappears
4
+ * with the plugin fiber.
5
+ * @module dsh-a2a/service
6
+ */
7
+
8
+ import { Service, type Context } from '@deepseek-ai/cordis'
9
+
10
+ /** A result with a user-facing message. */
11
+ export interface OpResult {
12
+ readonly ok: boolean
13
+ readonly message: string
14
+ }
15
+
16
+ /** The implementation the facade delegates to (owned by the plugin assembly). */
17
+ export interface A2AServiceImpl {
18
+ status(): unknown
19
+ enableServer(enable: boolean): Promise<OpResult>
20
+ getTask(taskId: string): unknown
21
+ listTasks(): unknown
22
+ cancelTask(taskId: string): Promise<OpResult>
23
+ agents(): unknown
24
+ addAgent(spec: { readonly name: string; readonly agentCardUrl: string; readonly bearerTokenEnv?: string }): Promise<OpResult>
25
+ removeAgent(id: string): Promise<OpResult>
26
+ setAgentEnabled(id: string, enabled: boolean): Promise<OpResult>
27
+ refreshAgentCard(id: string): Promise<OpResult>
28
+ }
29
+
30
+ /** The service other plugins read as `ctx.a2a`. */
31
+ export class A2AService extends Service {
32
+ constructor(
33
+ ctx: Context,
34
+ private readonly impl: A2AServiceImpl,
35
+ ) {
36
+ super(ctx, 'a2a')
37
+ }
38
+
39
+ status(): unknown {
40
+ return this.impl.status()
41
+ }
42
+
43
+ async enableServer(enable: boolean): Promise<OpResult> {
44
+ return this.impl.enableServer(enable)
45
+ }
46
+
47
+ getTask(taskId: string): unknown {
48
+ return this.impl.getTask(taskId)
49
+ }
50
+
51
+ listTasks(): unknown {
52
+ return this.impl.listTasks()
53
+ }
54
+
55
+ async cancelTask(taskId: string): Promise<OpResult> {
56
+ return this.impl.cancelTask(taskId)
57
+ }
58
+
59
+ agents(): unknown {
60
+ return this.impl.agents()
61
+ }
62
+
63
+ async addAgent(spec: { readonly name: string; readonly agentCardUrl: string; readonly bearerTokenEnv?: string }): Promise<OpResult> {
64
+ return this.impl.addAgent(spec)
65
+ }
66
+
67
+ async removeAgent(id: string): Promise<OpResult> {
68
+ return this.impl.removeAgent(id)
69
+ }
70
+
71
+ async setAgentEnabled(id: string, enabled: boolean): Promise<OpResult> {
72
+ return this.impl.setAgentEnabled(id, enabled)
73
+ }
74
+
75
+ async refreshAgentCard(id: string): Promise<OpResult> {
76
+ return this.impl.refreshAgentCard(id)
77
+ }
78
+ }