@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,197 @@
1
+ /**
2
+ * Shared agent machinery for the inbound executors: one DSH agent session per
3
+ * A2A context (create, or resume a persisted one), serialized turns, and
4
+ * optional agent-preset composition so a spawned session actually carries
5
+ * tools. Adapted from the createDshAgentExecutor pattern pioneered by
6
+ * dsh-a2a (MIT).
7
+ * @module dsh-a2a/server/exec/agent-runtime
8
+ */
9
+
10
+ import type { Context } from '@deepseek-ai/cordis'
11
+
12
+ /** The slice of a live DSH `Agent` this runtime drives (structural mirror). */
13
+ export interface LiveAgent {
14
+ readonly session: { deriveMessages(): readonly AgentMessage[] }
15
+ send(message: AgentUserMessage, target: 'next-turn', wakeup: boolean): void
16
+ whenIdle(): Promise<void>
17
+ cancel(cause: string): void
18
+ }
19
+
20
+ /** One derived history message. */
21
+ export interface AgentMessage {
22
+ readonly role: 'user' | 'assistant'
23
+ readonly content: readonly { readonly type: string; readonly text?: string }[]
24
+ }
25
+
26
+ /** One user message handed to the agent. */
27
+ export interface AgentUserMessage {
28
+ readonly id: string
29
+ readonly role: 'user'
30
+ readonly content: readonly { readonly type: 'text'; readonly text: string }[]
31
+ readonly source: { readonly kind: 'plugin'; readonly plugin: string }
32
+ }
33
+
34
+ /** Owned agent plus its capability disposer (structural mirror of AgentHandle). */
35
+ export interface AgentHandle {
36
+ readonly agent: LiveAgent
37
+ dispose(): Promise<void>
38
+ }
39
+
40
+ /** Structural slice of `ctx.agents` this runtime calls. */
41
+ export interface AgentRegistryLike {
42
+ create(options: {
43
+ readonly sessionId: string
44
+ readonly meta?: { readonly cwd?: string; readonly agentPreset?: string }
45
+ readonly agentOptions?: { readonly provider?: string; readonly model?: string; readonly maxTokens?: number }
46
+ readonly setup?: (agentCtx: unknown) => void | Promise<void>
47
+ }): Promise<AgentHandle>
48
+ resume(options: {
49
+ readonly resumeSessionId: string
50
+ readonly agentOptions?: { readonly provider?: string; readonly model?: string; readonly maxTokens?: number }
51
+ readonly setup?: (agentCtx: unknown) => void | Promise<void>
52
+ }): Promise<AgentHandle>
53
+ }
54
+
55
+ /** Structural slice of `ctx.agentPresets` (preset roster). */
56
+ export interface AgentPresetsLike {
57
+ resolve(id?: string): Promise<{ readonly id: string }>
58
+ mount(agentCtx: unknown, id: string): Promise<unknown>
59
+ }
60
+
61
+ /** Runtime options shared by the executors. */
62
+ export interface AgentRuntimeOptions {
63
+ /** Absolute cwd frozen into each spawned session's durable header. */
64
+ readonly cwd: string
65
+ /** Optional per-task model resolution (reads the deployment default). */
66
+ readonly resolveAgentOptions?: () => { readonly provider?: string; readonly model?: string; readonly maxTokens?: number } | undefined
67
+ /** Preset roster; when present every spawned session joins the default preset. */
68
+ readonly agentPresets?: AgentPresetsLike
69
+ /** Once-per-context hook after the first prompt lands (cosmetic naming). */
70
+ readonly onSessionOpened?: (info: { readonly sessionId: string; readonly contextId: string; readonly firstPrompt: string }) => void | Promise<void>
71
+ }
72
+
73
+ /**
74
+ * Per-context session pool: one live DSH agent per A2A contextId, created or
75
+ * resumed through the registry, turns serialized per context, every session
76
+ * disposeable on teardown.
77
+ */
78
+ export class ContextSessionPool {
79
+ private readonly handles = new Map<string, AgentHandle>()
80
+ private readonly tails = new Map<string, Promise<unknown>>()
81
+
82
+ constructor(
83
+ private readonly agents: AgentRegistryLike,
84
+ private readonly opts: AgentRuntimeOptions,
85
+ ) {}
86
+
87
+ /** Session id derived from the A2A context id (stable across restarts). */
88
+ static sessionIdFor(contextId: string): string {
89
+ return `a2a-${contextId}`
90
+ }
91
+
92
+ /** Get or open the live agent for a context; errors propagate. */
93
+ async agentFor(contextId: string): Promise<{ readonly agent: LiveAgent; readonly sessionId: string; readonly justOpened: boolean }> {
94
+ const sessionId = ContextSessionPool.sessionIdFor(contextId)
95
+ const existing = this.handles.get(contextId)
96
+ if (existing !== undefined) return { agent: existing.agent, sessionId, justOpened: false }
97
+ const agentOptions = this.opts.resolveAgentOptions?.()
98
+ const withModel = agentOptions ? { agentOptions } : {}
99
+ const presets = this.opts.agentPresets
100
+ let presetId: string | undefined
101
+ let setup: ((agentCtx: unknown) => Promise<void>) | undefined
102
+ if (presets !== undefined) {
103
+ presetId = (await presets.resolve()).id
104
+ setup = async (agentCtx: unknown): Promise<void> => {
105
+ await presets.mount(agentCtx, presetId!)
106
+ }
107
+ }
108
+ const meta: { readonly cwd: string; readonly agentPreset?: string } = presetId
109
+ ? { cwd: this.opts.cwd, agentPreset: presetId }
110
+ : { cwd: this.opts.cwd }
111
+ const createOptions = {
112
+ sessionId,
113
+ meta,
114
+ ...withModel,
115
+ ...(setup ? { setup } : {}),
116
+ }
117
+ let handle: AgentHandle
118
+ try {
119
+ handle = await this.agents.create(createOptions)
120
+ } catch (err) {
121
+ if (/already (has|owns)|persisted/i.test((err as Error).message)) {
122
+ handle = await this.agents.resume({ resumeSessionId: sessionId, ...withModel, ...(setup ? { setup } : {}) })
123
+ } else {
124
+ throw err
125
+ }
126
+ }
127
+ this.handles.set(contextId, handle)
128
+ return { agent: handle.agent, sessionId, justOpened: true }
129
+ }
130
+
131
+ /** Run one turn on a context's session, serialized behind its tail. */
132
+ async runTurn(contextId: string, prompt: string, signal: AbortSignal): Promise<string> {
133
+ const { agent, sessionId, justOpened } = await this.agentFor(contextId)
134
+ const onAbort = (): void => agent.cancel('a2a-canceled')
135
+ if (signal.aborted) onAbort()
136
+ else signal.addEventListener('abort', onAbort, { once: true })
137
+ const userMessage: AgentUserMessage = {
138
+ id: crypto.randomUUID(),
139
+ role: 'user',
140
+ content: [{ type: 'text', text: prompt }],
141
+ source: { kind: 'plugin', plugin: 'a2a' },
142
+ }
143
+ agent.send(userMessage, 'next-turn', true)
144
+ if (justOpened) {
145
+ try {
146
+ await this.opts.onSessionOpened?.({ sessionId, contextId, firstPrompt: prompt })
147
+ } catch {
148
+ // Cosmetic naming must never fail the task.
149
+ }
150
+ }
151
+ await agent.whenIdle()
152
+ return lastAssistantText(agent)
153
+ }
154
+
155
+ /** Dispose every live per-context session (called on plugin unload). */
156
+ async disposeAll(): Promise<void> {
157
+ const live = [...this.handles.values()]
158
+ const pending = [...this.tails.values()]
159
+ this.handles.clear()
160
+ this.tails.clear()
161
+ await Promise.allSettled(pending)
162
+ await Promise.allSettled(live.map((h) => h.dispose()))
163
+ }
164
+ }
165
+
166
+ /** Read the last assistant message's text off the derived history. */
167
+ function lastAssistantText(agent: LiveAgent): string {
168
+ const history = agent.session.deriveMessages()
169
+ for (let i = history.length - 1; i >= 0; i--) {
170
+ const message = history[i]
171
+ if (message === undefined) continue
172
+ if (message.role === 'assistant') {
173
+ return message.content
174
+ .map((b) => ('text' in b && typeof b.text === 'string' ? (b.text ?? '') : ''))
175
+ .filter(Boolean)
176
+ .join('\n')
177
+ .trim()
178
+ }
179
+ }
180
+ return ''
181
+ }
182
+
183
+ /**
184
+ * Probe for a readable service method via the reflection layer WITHOUT taking
185
+ * an inject dependency (optional services). Returns undefined when the service
186
+ * is absent or the method missing.
187
+ */
188
+ export function probeService(ctx: Context, name: string, method: string): unknown {
189
+ try {
190
+ const reflect = (ctx as unknown as { reflect?: { get(n: string, strict?: boolean): unknown } }).reflect
191
+ const svc = reflect?.get(name, true)
192
+ if (svc !== undefined && svc !== null && typeof (svc as Record<string, unknown>)[method] === 'function') return svc
193
+ } catch {
194
+ // reflection unavailable or resolution threw — treat as absent
195
+ }
196
+ return undefined
197
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The `session` executor: one inbound task is one turn on the context's DSH
3
+ * agent session; reply text becomes the task's output part. Works on any
4
+ * composition with an agent loop (the `chat` skill and the default path).
5
+ * @module dsh-a2a/server/exec/session
6
+ */
7
+
8
+ import type { A2aExecutor, A2aExecutorInput, A2aExecutorOutput } from '../executor.ts'
9
+ import { ContextSessionPool } from './agent-runtime.ts'
10
+
11
+ /** Build the session executor over a context session pool. */
12
+ export function createSessionExecutor(pool: ContextSessionPool): A2aExecutor {
13
+ return {
14
+ name: 'session',
15
+ async execute(input: A2aExecutorInput): Promise<A2aExecutorOutput> {
16
+ if (!input.prompt) {
17
+ return { parts: [{ text: 'No prompt provided.' }] }
18
+ }
19
+ const reply = await pool.runTurn(input.contextId, input.prompt, input.signal)
20
+ const text = (input.signal.aborted && !reply ? '(canceled)' : reply) || '(no reply)'
21
+ return { parts: [{ text }] }
22
+ },
23
+ disposeAll: () => pool.disposeAll(),
24
+ }
25
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The `subagent` executor: one inbound task is delegated to a named one-shot
3
+ * subagent under the context's parent session. The child runs with the
4
+ * composition's own tools and the delegation boundary; its output becomes the
5
+ * task's artifact. The subagent seam must be mounted, else the skill binding
6
+ * fails loudly at resolution time.
7
+ * @module dsh-a2a/server/exec/subagent
8
+ */
9
+
10
+ import type { A2aExecutor, A2aExecutorInput, A2aExecutorOutput } from '../executor.ts'
11
+ import { ContextSessionPool } from './agent-runtime.ts'
12
+
13
+ /** Structural slice of `ctx.subagents` this executor calls. */
14
+ export interface SubagentsLike {
15
+ start(
16
+ name: string,
17
+ request: {
18
+ readonly label?: string
19
+ readonly prompt: readonly { readonly type: 'text'; readonly text: string }[]
20
+ readonly parent: unknown
21
+ readonly signal: AbortSignal
22
+ },
23
+ ): Promise<{ readonly result: Promise<{ readonly output: readonly { readonly type: string; readonly text?: string }[] }>; dispose(): Promise<void> }>
24
+ }
25
+
26
+ export interface SubagentExecutorOptions {
27
+ readonly pool: ContextSessionPool
28
+ readonly subagents: SubagentsLike
29
+ /** Provider name (e.g. 'in-process'); configurable per deployment. */
30
+ readonly provider: string
31
+ }
32
+
33
+ /** Build a subagent executor delegating inbound tasks to one-shot children. */
34
+ export function createSubagentExecutor(opts: SubagentExecutorOptions): A2aExecutor {
35
+ const { pool, subagents, provider } = opts
36
+ return {
37
+ name: 'subagent',
38
+ async execute(input: A2aExecutorInput, execCtx): Promise<A2aExecutorOutput> {
39
+ if (!input.prompt) {
40
+ return { parts: [{ text: 'No prompt provided.' }] }
41
+ }
42
+ const { agent } = await pool.agentFor(input.contextId)
43
+ const run = await subagents.start(provider, {
44
+ label: `a2a:${input.skill}`,
45
+ prompt: [{ type: 'text', text: input.prompt }],
46
+ parent: agent,
47
+ signal: input.signal,
48
+ })
49
+ const result = await run.result
50
+ const text = result.output
51
+ .filter((b): b is { readonly type: string; readonly text: string } => typeof b.text === 'string')
52
+ .map((b) => b.text)
53
+ .filter(Boolean)
54
+ .join('\n')
55
+ .trim()
56
+ execCtx.onEvent({
57
+ type: 'artifact',
58
+ artifactId: 'result',
59
+ parts: [{ text: text || '(subagent returned no output)' }],
60
+ name: input.skill,
61
+ lastChunk: true,
62
+ })
63
+ return { parts: [{ text: text || '(subagent returned no output)' }] }
64
+ },
65
+ disposeAll: () => pool.disposeAll(),
66
+ }
67
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Executor abstraction for inbound tasks: an executor turns one task into
3
+ * output parts while reporting status and artifact updates. The registry is
4
+ * INTERNAL (built by the plugin from config) — public registration was cut:
5
+ * there is no external caller yet, so the seam stays an interface with two
6
+ * built-in implementations (session, subagent).
7
+ * @module dsh-a2a/server/executor
8
+ */
9
+
10
+ import type { Part, TaskState } from '../protocol.ts'
11
+
12
+ /** One update from a running executor. */
13
+ export type A2aExecutorEvent =
14
+ | { readonly type: 'status'; readonly state: TaskState; readonly message?: string }
15
+ | { readonly type: 'artifact'; readonly artifactId: string; readonly parts: readonly Part[]; readonly name?: string; readonly lastChunk?: boolean }
16
+
17
+ /** Input a task executor receives. */
18
+ export interface A2aExecutorInput {
19
+ readonly taskId: string
20
+ readonly contextId: string
21
+ readonly skill: string
22
+ readonly prompt: string
23
+ readonly signal: AbortSignal
24
+ }
25
+
26
+ /** Output contract of one executor run. */
27
+ export interface A2aExecutorOutput {
28
+ readonly parts: readonly Part[]
29
+ }
30
+
31
+ /** One inbound task executor. */
32
+ export interface A2aExecutor {
33
+ /** Stable kind name ('session' | 'subagent'). */
34
+ readonly name: string
35
+ /**
36
+ * Run one task. The executor reports progress through `opts.onEvent` and
37
+ * must honor `opts.signal` (cancel = abort remaining work).
38
+ */
39
+ execute(input: A2aExecutorInput, opts: { readonly onEvent: (ev: A2aExecutorEvent) => void }): Promise<A2aExecutorOutput>
40
+ /** Dispose resources owned by the executor (sessions, children). */
41
+ disposeAll?(): Promise<void>
42
+ }
43
+
44
+ /** Executor kind names configurable per skill. */
45
+ export type ExecutorKind = 'session' | 'subagent'
46
+
47
+ /**
48
+ * Skill → executor resolution. The subagent executor is absent on
49
+ * compositions without the subagent seam; resolving a skill bound to a
50
+ * missing kind fails loudly with a readable message.
51
+ */
52
+ export class ExecutorSet {
53
+ constructor(
54
+ private readonly bySkill: Readonly<Record<string, ExecutorKind>>,
55
+ private readonly session: A2aExecutor,
56
+ private readonly subagent: A2aExecutor | undefined,
57
+ ) {}
58
+
59
+ resolve(skill: string): A2aExecutor {
60
+ const kind = this.bySkill[skill] ?? 'session'
61
+ if (kind === 'session') return this.session
62
+ const subagent = this.subagent
63
+ if (subagent === undefined) {
64
+ throw new Error(`a2a: skill "${skill}" is bound to the subagent executor but no subagent seam is mounted`)
65
+ }
66
+ return subagent
67
+ }
68
+
69
+ async disposeAll(): Promise<void> {
70
+ await this.session.disposeAll?.()
71
+ await this.subagent?.disposeAll?.()
72
+ }
73
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * HTTP binding for the inbound server: registers the AgentCard route and the
3
+ * JSON-RPC / SSE endpoint on a structural webServer (normally
4
+ * `@deepseek-ai/dsh-host-webserver`), with runtime enable/disable.
5
+ * @module dsh-a2a/server/routes
6
+ */
7
+
8
+ import type { IncomingMessage, ServerResponse } from 'node:http'
9
+ import type { A2AServer } from './a2a-server.ts'
10
+
11
+ /** Structural slice of `ctx.webServer` this module registers on. */
12
+ export interface WebServerLike {
13
+ register(route: {
14
+ readonly kind: 'exact' | 'prefix'
15
+ readonly path: string
16
+ handler(req: IncomingMessage, res: ServerResponse): void | Promise<void>
17
+ }): () => void
18
+ }
19
+
20
+ const CARD_PATH = '/.well-known/agent-card.json'
21
+
22
+ /** Runtime-registered HTTP routes for one A2A server. */
23
+ export class A2aRoutes {
24
+ private disposers: Array<() => void> = []
25
+ private registered = false
26
+
27
+ constructor(
28
+ private readonly webServer: WebServerLike,
29
+ private readonly server: A2AServer,
30
+ ) {}
31
+
32
+ get active(): boolean {
33
+ return this.registered
34
+ }
35
+
36
+ /** Register the AgentCard + endpoint routes (idempotent). */
37
+ enable(): void {
38
+ if (this.registered) return
39
+ this.disposers.push(
40
+ this.webServer.register({
41
+ kind: 'exact',
42
+ path: CARD_PATH,
43
+ handler: (_req, res) => {
44
+ res.writeHead(200, { 'content-type': 'application/json' })
45
+ res.end(JSON.stringify(this.server.card))
46
+ },
47
+ }),
48
+ )
49
+ this.disposers.push(
50
+ this.webServer.register({
51
+ kind: 'prefix',
52
+ path: endpointPathOf(this.server),
53
+ handler: (req, res) => this.onRequest(req, res),
54
+ }),
55
+ )
56
+ this.registered = true
57
+ }
58
+
59
+ /** Unregister the routes (idempotent). */
60
+ disable(): void {
61
+ for (const disposer of this.disposers.splice(0)) disposer()
62
+ this.registered = false
63
+ }
64
+
65
+ dispose(): void {
66
+ this.disable()
67
+ }
68
+
69
+ private async onRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
70
+ const path = (req.url ?? '').split('?')[0] ?? ''
71
+ const streaming = (req.method === 'POST' && (req.headers.accept ?? '').includes('text/event-stream'))
72
+
73
+ if (req.method === 'POST' && path === endpointPathOf(this.server)) {
74
+ const body = await readBody(req)
75
+ if (streaming) {
76
+ // Authorize BEFORE writing 200 so a 401 can still carry a plain status.
77
+ if (!this.server.authorized(toServerReq(req))) {
78
+ res.writeHead(401, { 'content-type': 'text/plain', 'WWW-Authenticate': 'Bearer' })
79
+ res.end('Unauthorized')
80
+ return
81
+ }
82
+ res.writeHead(200, {
83
+ 'content-type': 'text/event-stream',
84
+ 'cache-control': 'no-cache',
85
+ connection: 'keep-alive',
86
+ 'x-accel-buffering': 'no',
87
+ })
88
+ const outcome = await this.server.handleStream(toServerReq(req), body, (frame) => res.write(frame))
89
+ if (outcome.status !== 200) {
90
+ res.writeHead(outcome.status, { 'content-type': 'text/plain', ...(outcome.headers ?? {}) })
91
+ res.end('Unauthorized')
92
+ return
93
+ }
94
+ res.end()
95
+ return
96
+ }
97
+ const out = await this.server.handle(toServerReq(req), body)
98
+ res.writeHead(out.status, { 'content-type': out.contentType, ...(out.headers ?? {}) })
99
+ res.end(out.body)
100
+ return
101
+ }
102
+
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
+ res.writeHead(out.status, { 'content-type': out.contentType, ...(out.headers ?? {}) })
106
+ res.end(out.body)
107
+ }
108
+ }
109
+
110
+ function endpointPathOf(server: A2AServer): string {
111
+ const url = server.card.supportedInterfaces?.[0]?.url
112
+ if (!url) return '/a2a'
113
+ try {
114
+ return new URL(url).pathname
115
+ } catch {
116
+ return '/a2a'
117
+ }
118
+ }
119
+
120
+ function readBody(req: IncomingMessage): Promise<string> {
121
+ return new Promise<string>((resolve, reject) => {
122
+ const chunks: Buffer[] = []
123
+ req.on('data', (chunk: Buffer) => chunks.push(chunk))
124
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
125
+ req.once('error', reject)
126
+ })
127
+ }
128
+
129
+ function toServerReq(req: IncomingMessage): {
130
+ readonly method?: string
131
+ readonly url?: string
132
+ readonly headers?: Record<string, string>
133
+ readonly socket?: { readonly remoteAddress?: string; readonly remotePort?: number }
134
+ } {
135
+ const headers: Record<string, string> = {}
136
+ for (const [key, value] of Object.entries(req.headers)) {
137
+ if (typeof value === 'string') headers[key] = value
138
+ else if (Array.isArray(value)) headers[key] = value.join(', ')
139
+ }
140
+ const method = req.method
141
+ const url = req.url
142
+ const socket = req.socket
143
+ return {
144
+ ...(method !== undefined ? { method } : {}),
145
+ ...(url !== undefined ? { url } : {}),
146
+ headers,
147
+ ...(socket
148
+ ? {
149
+ socket: {
150
+ ...(socket.remoteAddress !== undefined ? { remoteAddress: socket.remoteAddress } : {}),
151
+ ...(socket.remotePort !== undefined ? { remotePort: socket.remotePort } : {}),
152
+ },
153
+ }
154
+ : {}),
155
+ }
156
+ }