@hanphone/dsh-a2a 0.1.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.
Files changed (88) hide show
  1. package/README.md +133 -87
  2. package/README.zh.md +64 -87
  3. package/cordis.patch.yml +4 -7
  4. package/lib/client.js +228 -36
  5. package/lib/index.js +1271 -625
  6. package/lib/types/api.d.ts +43 -13
  7. package/lib/types/api.d.ts.map +1 -1
  8. package/lib/types/api.js +60 -20
  9. package/lib/types/api.js.map +1 -1
  10. package/lib/types/client/index.d.ts +50 -14
  11. package/lib/types/client/index.d.ts.map +1 -1
  12. package/lib/types/commands.d.ts +4 -2
  13. package/lib/types/commands.d.ts.map +1 -1
  14. package/lib/types/commands.js +59 -46
  15. package/lib/types/commands.js.map +1 -1
  16. package/lib/types/events.d.ts +32 -9
  17. package/lib/types/events.d.ts.map +1 -1
  18. package/lib/types/index.d.ts +21 -32
  19. package/lib/types/index.d.ts.map +1 -1
  20. package/lib/types/index.js +178 -213
  21. package/lib/types/index.js.map +1 -1
  22. package/lib/types/outbound/calls.d.ts.map +1 -1
  23. package/lib/types/outbound/calls.js +8 -6
  24. package/lib/types/outbound/calls.js.map +1 -1
  25. package/lib/types/protocol.d.ts +174 -109
  26. package/lib/types/protocol.d.ts.map +1 -1
  27. package/lib/types/protocol.js +58 -34
  28. package/lib/types/protocol.js.map +1 -1
  29. package/lib/types/server/a2a-server.d.ts +7 -1
  30. package/lib/types/server/a2a-server.d.ts.map +1 -1
  31. package/lib/types/server/a2a-server.js +11 -3
  32. package/lib/types/server/a2a-server.js.map +1 -1
  33. package/lib/types/server/card.d.ts +6 -30
  34. package/lib/types/server/card.d.ts.map +1 -1
  35. package/lib/types/server/card.js +13 -49
  36. package/lib/types/server/card.js.map +1 -1
  37. package/lib/types/server/exec/agent-runtime.d.ts +3 -0
  38. package/lib/types/server/exec/agent-runtime.d.ts.map +1 -1
  39. package/lib/types/server/exec/agent-runtime.js +2 -1
  40. package/lib/types/server/exec/agent-runtime.js.map +1 -1
  41. package/lib/types/server/identity.d.ts +38 -0
  42. package/lib/types/server/identity.d.ts.map +1 -0
  43. package/lib/types/server/identity.js +89 -0
  44. package/lib/types/server/identity.js.map +1 -0
  45. package/lib/types/server/inbound-registry.d.ts +60 -0
  46. package/lib/types/server/inbound-registry.d.ts.map +1 -0
  47. package/lib/types/server/inbound-registry.js +86 -0
  48. package/lib/types/server/inbound-registry.js.map +1 -0
  49. package/lib/types/server/routes.d.ts +5 -1
  50. package/lib/types/server/routes.d.ts.map +1 -1
  51. package/lib/types/server/routes.js +9 -5
  52. package/lib/types/server/routes.js.map +1 -1
  53. package/lib/types/server/store.d.ts +37 -1
  54. package/lib/types/server/store.d.ts.map +1 -1
  55. package/lib/types/server/store.js +14 -0
  56. package/lib/types/server/store.js.map +1 -1
  57. package/lib/types/servers/inbound-manager.d.ts +131 -0
  58. package/lib/types/servers/inbound-manager.d.ts.map +1 -0
  59. package/lib/types/servers/inbound-manager.js +312 -0
  60. package/lib/types/servers/inbound-manager.js.map +1 -0
  61. package/lib/types/servers/outbound-manager.d.ts +129 -0
  62. package/lib/types/servers/outbound-manager.d.ts.map +1 -0
  63. package/lib/types/servers/outbound-manager.js +238 -0
  64. package/lib/types/servers/outbound-manager.js.map +1 -0
  65. package/lib/types/service.d.ts +108 -23
  66. package/lib/types/service.d.ts.map +1 -1
  67. package/lib/types/service.js +39 -18
  68. package/lib/types/service.js.map +1 -1
  69. package/package.json +3 -3
  70. package/src/api.ts +70 -31
  71. package/src/client/index.ts +339 -81
  72. package/src/commands.ts +59 -43
  73. package/src/events.ts +14 -7
  74. package/src/index.ts +204 -249
  75. package/src/outbound/calls.ts +8 -6
  76. package/src/protocol.ts +204 -95
  77. package/src/server/a2a-server.ts +16 -4
  78. package/src/server/card.ts +13 -62
  79. package/src/server/exec/agent-runtime.ts +5 -2
  80. package/src/server/identity.ts +109 -0
  81. package/src/server/inbound-registry.ts +129 -0
  82. package/src/server/routes.ts +6 -4
  83. package/src/server/store.ts +50 -4
  84. package/src/servers/inbound-manager.ts +385 -0
  85. package/src/servers/outbound-manager.ts +289 -0
  86. package/src/service.ts +136 -28
  87. package/lib/tsconfig.client.tsbuildinfo +0 -1
  88. package/lib/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Service identity: the inbound AgentCard's name/description/version,
3
+ * editable at runtime from the GUI dashboard and persisted in the `a2a`
4
+ * domain's `identity` table. Skills stay derived from the live tool registry
5
+ * (the "card derives from ctx.tools" design); identity editing builds a fresh
6
+ * card preserving the endpoint URL and swaps it onto the server, so routes
7
+ * and the facade see the new value immediately.
8
+ * @module dsh-a2a/server/identity
9
+ */
10
+
11
+ import { buildCard, type CardOptions } from './card.ts'
12
+ import type { AgentCard, AgentSkill } from '../protocol.ts'
13
+ import type { A2aDomain } from './store.ts'
14
+
15
+ /** Persisted service identity (only the card identity fields; skills derive). */
16
+ export interface A2aIdentity {
17
+ readonly name: string
18
+ readonly description: string
19
+ readonly version: string
20
+ }
21
+
22
+ const IDENTITY_KEY = 'service'
23
+
24
+ function decode(raw: string | undefined): A2aIdentity | undefined {
25
+ if (raw === undefined) return undefined
26
+ try {
27
+ const parsed = JSON.parse(raw) as Partial<A2aIdentity>
28
+ if (typeof parsed.name !== 'string' || typeof parsed.description !== 'string' || typeof parsed.version !== 'string') {
29
+ return undefined
30
+ }
31
+ return { name: parsed.name, description: parsed.description, version: parsed.version }
32
+ } catch {
33
+ return undefined
34
+ }
35
+ }
36
+
37
+ function encode(value: A2aIdentity): string {
38
+ return JSON.stringify(value)
39
+ }
40
+
41
+ /** Read the persisted identity (undefined when never configured). */
42
+ export function readIdentity(domain: A2aDomain): A2aIdentity | undefined {
43
+ return decode(domain.identity.get(IDENTITY_KEY))
44
+ }
45
+
46
+ /** Persist a new identity (fire-and-forget like the task store writes). */
47
+ export function writeIdentity(domain: A2aDomain, identity: A2aIdentity): void {
48
+ void domain.identity.put(IDENTITY_KEY, encode(identity)).catch((err: unknown) => {
49
+ throw new Error(`a2a: identity persistence failed: ${String(err)}`)
50
+ })
51
+ }
52
+
53
+ /**
54
+ * Build the AgentCard options for a given base URL/path and the override
55
+ * identity. `identity` (persisted) wins over the composition `defaults`; when
56
+ * no identity is stored, the composition defaults stand.
57
+ */
58
+ export function cardOptionsFor(
59
+ baseUrl: string,
60
+ endpointPath: string,
61
+ defaults: { readonly name: string; readonly description: string; readonly version: string },
62
+ identity: A2aIdentity | undefined,
63
+ skills: readonly AgentSkill[],
64
+ authToken?: string,
65
+ ): CardOptions {
66
+ return {
67
+ baseUrl,
68
+ endpointPath,
69
+ name: identity?.name ?? defaults.name,
70
+ description: identity?.description ?? defaults.description,
71
+ version: identity?.version ?? defaults.version,
72
+ skills,
73
+ ...(authToken !== undefined ? { authToken } : {}),
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Build a fresh AgentCard from an existing one plus a new identity and skill
79
+ * list, preserving the endpoint URL (baseUrl/path) and security scheme.
80
+ */
81
+ export function rebuildCardWithIdentity(card: AgentCard, identity: A2aIdentity, skills: readonly AgentSkill[]): AgentCard {
82
+ return buildCard({
83
+ baseUrl: endpointBaseOf(card),
84
+ endpointPath: endpointPathOf(card),
85
+ name: identity.name,
86
+ description: identity.description,
87
+ version: identity.version,
88
+ skills,
89
+ // Preserve the advertised scheme; buildCard only uses the presence (never
90
+ // the value) to declare securitySchemes.
91
+ ...(card.securitySchemes !== undefined ? { authToken: 'preserved-scheme' } : {}),
92
+ })
93
+ }
94
+
95
+ function endpointBaseOf(card: AgentCard): string {
96
+ const url = card.supportedInterfaces?.[0]?.url
97
+ if (!url) return 'http://127.0.0.1'
98
+ return url.replace(/\/[^/]*$/, '')
99
+ }
100
+
101
+ function endpointPathOf(card: AgentCard): string {
102
+ const url = card.supportedInterfaces?.[0]?.url
103
+ if (!url) return '/a2a'
104
+ try {
105
+ return new URL(url).pathname
106
+ } catch {
107
+ return '/a2a'
108
+ }
109
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Inbound connection registry: which remote peers are talking to this DSH's
3
+ * A2A server. Fed by the A2A server's `onInbound` hook (every JSON-RPC
4
+ * request / SSE open), surfaced through the dashboard API, and disconnectable
5
+ * (`closePeer` cancels the peer's active tasks and drops the record).
6
+ * @module dsh-a2a/server/inbound-registry
7
+ */
8
+
9
+ /** One live inbound connection (observed peer). */
10
+ export interface InboundPeerRecord {
11
+ /** Stable per-process id (random). */
12
+ readonly id: string
13
+ /** Short display label derived from the request source. */
14
+ readonly label: string
15
+ /** Source socket address, when visible ("127.0.0.1:54321"). */
16
+ readonly source: string | null
17
+ /** First-seen ISO timestamp. */
18
+ readonly firstSeen: string
19
+ /** Last-activity ISO timestamp. */
20
+ readonly lastSeen: string
21
+ /** Number of tasks this peer created or continued. */
22
+ readonly taskCount: number
23
+ /** Ids of tasks still running for this peer. */
24
+ readonly activeTaskIds: readonly string[]
25
+ /** True while at least one streaming (SSE) connection is open. */
26
+ readonly streaming: boolean
27
+ }
28
+
29
+ /** Control and observation contract the dashboard API and facade use. */
30
+ export interface InboundRegistry {
31
+ list(): readonly InboundPeerRecord[]
32
+ /** Cancel a peer's active tasks and remove its record. */
33
+ closePeer(peerId: string): { readonly ok: boolean; readonly message: string }
34
+ /** Active task ids of one peer (the facade cancels them on close). */
35
+ activeTasksOf(peerId: string): readonly string[]
36
+ }
37
+
38
+ /** Mutable tracking state behind one peer record. */
39
+ interface PeerState {
40
+ readonly id: string
41
+ readonly label: string
42
+ readonly source: string | null
43
+ readonly firstSeen: string
44
+ lastSeen: string
45
+ taskCount: number
46
+ readonly active: Set<string>
47
+ streamingCount: number
48
+ }
49
+
50
+ function newPeerId(): string {
51
+ return `peer-${crypto.randomUUID()}`
52
+ }
53
+
54
+ /** In-memory inbound peer registry. */
55
+ export class LiveInboundRegistry implements InboundRegistry {
56
+ private readonly peers = new Map<string, PeerState>()
57
+
58
+ private findBySource(source: string | null): PeerState | undefined {
59
+ if (source === null) return undefined
60
+ for (const peer of this.peers.values()) {
61
+ if (peer.source === source) return peer
62
+ }
63
+ return undefined
64
+ }
65
+
66
+ /** Observe one inbound event: method, source, task ids, streaming flag. */
67
+ note(input: { readonly method: string; readonly source?: string; readonly taskIds: readonly string[]; readonly streaming: boolean }): void {
68
+ const now = new Date().toISOString()
69
+ const source = input.source ?? null
70
+ let peer = this.findBySource(source)
71
+ if (peer === undefined) {
72
+ peer = {
73
+ id: newPeerId(),
74
+ label: source ?? 'unknown',
75
+ source,
76
+ firstSeen: now,
77
+ lastSeen: now,
78
+ taskCount: 0,
79
+ active: new Set<string>(),
80
+ streamingCount: 0,
81
+ }
82
+ this.peers.set(peer.id, peer)
83
+ }
84
+ peer.lastSeen = now
85
+ peer.taskCount += input.taskIds.length
86
+ for (const id of input.taskIds) peer.active.add(id)
87
+ if (input.streaming) peer.streamingCount += 1
88
+ }
89
+
90
+ /** A task settled: drop it from every peer's active set. */
91
+ settle(taskId: string): void {
92
+ for (const peer of this.peers.values()) {
93
+ if (peer.active.delete(taskId)) peer.lastSeen = new Date().toISOString()
94
+ }
95
+ }
96
+
97
+ /** Decrement streaming count when an SSE connection closes. */
98
+ endStream(source?: string): void {
99
+ const peer = this.findBySource(source ?? null)
100
+ if (peer !== undefined && peer.streamingCount > 0) {
101
+ peer.streamingCount -= 1
102
+ peer.lastSeen = new Date().toISOString()
103
+ }
104
+ }
105
+
106
+ list(): readonly InboundPeerRecord[] {
107
+ return [...this.peers.values()].map((p) => ({
108
+ id: p.id,
109
+ label: p.label,
110
+ source: p.source,
111
+ firstSeen: p.firstSeen,
112
+ lastSeen: p.lastSeen,
113
+ taskCount: p.taskCount,
114
+ activeTaskIds: [...p.active],
115
+ streaming: p.streamingCount > 0,
116
+ })).sort((a, b) => (a.lastSeen < b.lastSeen ? 1 : -1))
117
+ }
118
+
119
+ closePeer(peerId: string): { readonly ok: boolean; readonly message: string } {
120
+ const peer = this.peers.get(peerId)
121
+ if (peer === undefined) return { ok: false, message: `inbound peer ${peerId} not found` }
122
+ this.peers.delete(peerId)
123
+ return { ok: true, message: `inbound peer ${peer.label} closed` }
124
+ }
125
+
126
+ activeTasksOf(peerId: string): readonly string[] {
127
+ return [...(this.peers.get(peerId)?.active ?? [])]
128
+ }
129
+ }
@@ -17,7 +17,7 @@ export interface WebServerLike {
17
17
  }): () => void
18
18
  }
19
19
 
20
- const CARD_PATH = '/.well-known/agent-card.json'
20
+ const DEFAULT_CARD_PATH = '/.well-known/agent-card.json'
21
21
 
22
22
  /** Runtime-registered HTTP routes for one A2A server. */
23
23
  export class A2aRoutes {
@@ -27,6 +27,8 @@ export class A2aRoutes {
27
27
  constructor(
28
28
  private readonly webServer: WebServerLike,
29
29
  private readonly server: A2AServer,
30
+ /** AgentCard route path; default = the well-known A2A path. */
31
+ private readonly cardPath: string = DEFAULT_CARD_PATH,
30
32
  ) {}
31
33
 
32
34
  get active(): boolean {
@@ -39,7 +41,7 @@ export class A2aRoutes {
39
41
  this.disposers.push(
40
42
  this.webServer.register({
41
43
  kind: 'exact',
42
- path: CARD_PATH,
44
+ path: this.cardPath,
43
45
  handler: (_req, res) => {
44
46
  res.writeHead(200, { 'content-type': 'application/json' })
45
47
  res.end(JSON.stringify(this.server.card))
@@ -100,8 +102,8 @@ export class A2aRoutes {
100
102
  return
101
103
  }
102
104
 
103
- // GET AgentCard at the well-known path; anything else is the server's call.
104
- const out = await this.server.handle(toServerReq(req), path === CARD_PATH ? '' : '')
105
+ // GET AgentCard at this instance's card path; anything else is the server's call.
106
+ const out = await this.server.handle(toServerReq(req), path === this.cardPath ? '' : '')
105
107
  res.writeHead(out.status, { 'content-type': out.contentType, ...(out.headers ?? {}) })
106
108
  res.end(out.body)
107
109
  }
@@ -15,7 +15,7 @@ import {
15
15
  type DomainFacility,
16
16
  type KvTable,
17
17
  } from '@deepseek-ai/dsh-storage-domain'
18
- import { TaskState, type Part } from '../protocol.ts'
18
+ import { TaskState, type AgentSkill, type Part } from '../protocol.ts'
19
19
 
20
20
  /** One durable inbound/outbound task record. */
21
21
  export interface TaskRecord {
@@ -32,6 +32,8 @@ export interface TaskRecord {
32
32
  readonly executor: 'session' | 'subagent'
33
33
  readonly summary: string | null
34
34
  readonly error?: { readonly code: string; readonly message: string }
35
+ /** The inbound server instance this task arrived through. */
36
+ readonly serverId?: string
35
37
  }
36
38
 
37
39
  /** One durable context→session binding (inbound conversation ↔ DSH session). */
@@ -50,6 +52,33 @@ export interface OutboundAgentRecord {
50
52
  readonly lastCardAt: string | null
51
53
  }
52
54
 
55
+ /** One durable inbound server instance. */
56
+ export interface InboundServerRecord {
57
+ readonly id: string
58
+ readonly name: string
59
+ readonly description: string
60
+ readonly version: string
61
+ readonly endpointPath: string
62
+ /** Agent preset id composing inbound sessions; absent = deployment default. */
63
+ readonly preset?: string
64
+ readonly authTokenEnv?: string
65
+ /** Advertised skills (creator-entered declarations, default = preset name). */
66
+ readonly skills: readonly AgentSkill[]
67
+ readonly enabled: boolean
68
+ }
69
+
70
+ /** One durable outbound server connection instance. */
71
+ export interface OutboundServerRecord {
72
+ readonly id: string
73
+ readonly name: string
74
+ readonly agentCardUrl: string
75
+ readonly bearerTokenEnv?: string
76
+ /** Preset composing this DSH's local session when talking to the remote. */
77
+ readonly preset?: string
78
+ readonly enabled: boolean
79
+ readonly timeoutMs: number
80
+ }
81
+
53
82
  /** JSON-encoded record schema shared by every `a2a` table. */
54
83
  const json = z.string()
55
84
 
@@ -61,6 +90,9 @@ export const a2aDomainSpec = defineDomain({
61
90
  tasks: domainTable<string, string>(json),
62
91
  contexts: domainTable<string, string>(json),
63
92
  agents: domainTable<string, string>(json),
93
+ identity: domainTable<string, string>(json),
94
+ inbound_servers: domainTable<string, string>(json),
95
+ outbound_servers: domainTable<string, string>(json),
64
96
  },
65
97
  })
66
98
 
@@ -107,6 +139,18 @@ export class A2aDomain {
107
139
  return this.handle.table('agents')
108
140
  }
109
141
 
142
+ get identity(): KvTable<string, string> {
143
+ return this.handle.table('identity')
144
+ }
145
+
146
+ get inbound_servers(): KvTable<string, string> {
147
+ return this.handle.table('inbound_servers')
148
+ }
149
+
150
+ get outbound_servers(): KvTable<string, string> {
151
+ return this.handle.table('outbound_servers')
152
+ }
153
+
110
154
  async close(): Promise<void> {
111
155
  await this.handle.close()
112
156
  }
@@ -114,7 +158,7 @@ export class A2aDomain {
114
158
 
115
159
  /** Task-store contract shared by the domain-backed and memory stores. */
116
160
  export interface TaskStore {
117
- create(input: { contextId: string; skill: string; parts: readonly Part[]; remotePeerId: string | null }): TaskRecord
161
+ create(input: { contextId: string; skill: string; parts: readonly Part[]; remotePeerId: string | null; serverId?: string }): TaskRecord
118
162
  get(taskId: string): TaskRecord | undefined
119
163
  list(): TaskRecord[]
120
164
  setState(taskId: string, state: TaskState, message?: { code: string; text: string }): TaskRecord
@@ -141,7 +185,7 @@ export class MemoryTaskStore implements TaskStore {
141
185
  private tasks = new Map<string, TaskRecord>()
142
186
  private contexts = new Map<string, string>()
143
187
 
144
- create(input: { contextId: string; skill: string; parts: readonly Part[]; remotePeerId: string | null }): TaskRecord {
188
+ create(input: { contextId: string; skill: string; parts: readonly Part[]; remotePeerId: string | null; serverId?: string }): TaskRecord {
145
189
  const record: TaskRecord = {
146
190
  taskId: `a2a-${crypto.randomUUID()}`,
147
191
  contextId: input.contextId,
@@ -155,6 +199,7 @@ export class MemoryTaskStore implements TaskStore {
155
199
  artifacts: [],
156
200
  executor: 'session',
157
201
  summary: null,
202
+ ...(input.serverId !== undefined ? { serverId: input.serverId } : {}),
158
203
  }
159
204
  this.tasks.set(record.taskId, record)
160
205
  return record
@@ -239,7 +284,7 @@ export class DomainTaskStore implements TaskStore {
239
284
  }
240
285
  }
241
286
 
242
- create(input: { contextId: string; skill: string; parts: readonly Part[]; remotePeerId: string | null }): TaskRecord {
287
+ create(input: { contextId: string; skill: string; parts: readonly Part[]; remotePeerId: string | null; serverId?: string }): TaskRecord {
243
288
  const record: TaskRecord = {
244
289
  taskId: `a2a-${crypto.randomUUID()}`,
245
290
  contextId: input.contextId,
@@ -253,6 +298,7 @@ export class DomainTaskStore implements TaskStore {
253
298
  artifacts: [],
254
299
  executor: 'session',
255
300
  summary: null,
301
+ ...(input.serverId !== undefined ? { serverId: input.serverId } : {}),
256
302
  }
257
303
  this.records.set(record.taskId, record)
258
304
  // Persist eagerly; a failed write must fail the task, not silently lose it.