@pixelbyte-software/pixcode 1.33.11 → 1.34.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 (37) hide show
  1. package/dist/assets/{index-oLYHJ2X5.js → index-BvClqlMf.js} +1 -1
  2. package/dist/index.html +1 -1
  3. package/dist-server/server/index.js +4 -0
  4. package/dist-server/server/index.js.map +1 -1
  5. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js +47 -0
  6. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js.map +1 -0
  7. package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js +17 -0
  8. package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js.map +1 -0
  9. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js +233 -0
  10. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js.map +1 -0
  11. package/dist-server/server/modules/orchestration/a2a/agent-card.js +50 -0
  12. package/dist-server/server/modules/orchestration/a2a/agent-card.js.map +1 -0
  13. package/dist-server/server/modules/orchestration/a2a/auth.middleware.js +25 -0
  14. package/dist-server/server/modules/orchestration/a2a/auth.middleware.js.map +1 -0
  15. package/dist-server/server/modules/orchestration/a2a/bus.js +34 -0
  16. package/dist-server/server/modules/orchestration/a2a/bus.js.map +1 -0
  17. package/dist-server/server/modules/orchestration/a2a/routes.js +233 -0
  18. package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -0
  19. package/dist-server/server/modules/orchestration/a2a/types.js +6 -0
  20. package/dist-server/server/modules/orchestration/a2a/types.js.map +1 -0
  21. package/dist-server/server/modules/orchestration/a2a/validator.js +85 -0
  22. package/dist-server/server/modules/orchestration/a2a/validator.js.map +1 -0
  23. package/dist-server/server/modules/orchestration/index.js +10 -0
  24. package/dist-server/server/modules/orchestration/index.js.map +1 -0
  25. package/package.json +1 -1
  26. package/scripts/smoke/a2a-roundtrip.mjs +98 -0
  27. package/server/index.js +9 -0
  28. package/server/modules/orchestration/a2a/adapter-registry.ts +58 -0
  29. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +49 -0
  30. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +283 -0
  31. package/server/modules/orchestration/a2a/agent-card.ts +55 -0
  32. package/server/modules/orchestration/a2a/auth.middleware.ts +29 -0
  33. package/server/modules/orchestration/a2a/bus.ts +46 -0
  34. package/server/modules/orchestration/a2a/routes.ts +264 -0
  35. package/server/modules/orchestration/a2a/types.ts +111 -0
  36. package/server/modules/orchestration/a2a/validator.ts +90 -0
  37. package/server/modules/orchestration/index.ts +26 -0
@@ -0,0 +1,85 @@
1
+ // server/modules/orchestration/a2a/validator.ts
2
+ // Hand-written validators for incoming A2A payloads.
3
+ // We deliberately avoid adding a new dep (zod, ajv) for the
4
+ // foundation; a follow-on plan can swap to a schema lib if needed.
5
+ //
6
+ // All path strings use JSONPath-style "$" as the document root so
7
+ // callers can map errors to wire-payload locations consistently.
8
+ export class A2AValidationError extends Error {
9
+ path;
10
+ constructor(message, path) {
11
+ super(`${path}: ${message}`);
12
+ this.path = path;
13
+ this.name = 'A2AValidationError';
14
+ }
15
+ }
16
+ function assertNonEmptyString(value, path) {
17
+ if (typeof value !== 'string' || value.length === 0) {
18
+ throw new A2AValidationError('expected non-empty string', path);
19
+ }
20
+ }
21
+ function assertPart(value, path) {
22
+ if (!value || typeof value !== 'object') {
23
+ throw new A2AValidationError('expected object', path);
24
+ }
25
+ const part = value;
26
+ if (part.kind === 'text') {
27
+ assertNonEmptyString(part.text, `${path}.text`);
28
+ return;
29
+ }
30
+ if (part.kind === 'file') {
31
+ assertNonEmptyString(part.name, `${path}.name`);
32
+ return;
33
+ }
34
+ if (part.kind === 'data') {
35
+ const data = part.data;
36
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
37
+ throw new A2AValidationError('data must be a plain object', `${path}.data`);
38
+ }
39
+ return;
40
+ }
41
+ throw new A2AValidationError('part.kind must be text|file|data', `${path}.kind`);
42
+ }
43
+ export function assertMessage(value, path = '$') {
44
+ if (!value || typeof value !== 'object') {
45
+ throw new A2AValidationError('expected object', path);
46
+ }
47
+ const m = value;
48
+ assertNonEmptyString(m.messageId, `${path}.messageId`);
49
+ if (m.role !== 'user' && m.role !== 'agent') {
50
+ throw new A2AValidationError('role must be user|agent', `${path}.role`);
51
+ }
52
+ if (!Array.isArray(m.parts) || m.parts.length === 0) {
53
+ throw new A2AValidationError('parts must be non-empty array', `${path}.parts`);
54
+ }
55
+ m.parts.forEach((p, i) => assertPart(p, `${path}.parts[${i}]`));
56
+ }
57
+ export function assertSubmitTaskInput(value) {
58
+ if (!value || typeof value !== 'object') {
59
+ throw new A2AValidationError('expected object', '$');
60
+ }
61
+ const v = value;
62
+ assertMessage(v.message, '$.message');
63
+ assertNonEmptyString(v.adapterId, '$.adapterId');
64
+ }
65
+ export function assertAgentCard(value) {
66
+ if (!value || typeof value !== 'object') {
67
+ throw new A2AValidationError('expected object', '$');
68
+ }
69
+ const card = value;
70
+ assertNonEmptyString(card.name, '$.name');
71
+ assertNonEmptyString(card.description, '$.description');
72
+ assertNonEmptyString(card.url, '$.url');
73
+ assertNonEmptyString(card.version, '$.version');
74
+ if (!Array.isArray(card.capabilities)) {
75
+ throw new A2AValidationError('capabilities must be array', '$.capabilities');
76
+ }
77
+ if (!Array.isArray(card.skills)) {
78
+ throw new A2AValidationError('skills must be array', '$.skills');
79
+ }
80
+ card.skills.forEach((s, i) => {
81
+ assertNonEmptyString(s.id, `$.skills[${i}].id`);
82
+ assertNonEmptyString(s.description, `$.skills[${i}].description`);
83
+ });
84
+ }
85
+ //# sourceMappingURL=validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../../../../server/modules/orchestration/a2a/validator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ // server/modules/orchestration/index.ts
2
+ // Public surface for the orchestration module.
3
+ // All cross-module consumers must import from here per
4
+ // eslint.config.js boundaries rules.
5
+ export { createA2ARouter } from './a2a/routes.js';
6
+ export { adapterRegistry } from './a2a/adapter-registry.js';
7
+ export { ClaudeCodeA2AAdapter } from './a2a/adapters/claude-code.adapter.js';
8
+ export { AbstractA2AAdapter } from './a2a/adapters/abstract-a2a.adapter.js';
9
+ export { a2aBus } from './a2a/bus.js';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../server/modules/orchestration/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,+CAA+C;AAC/C,uDAAuD;AACvD,qCAAqC;AAErC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAK7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelbyte-software/pixcode",
3
- "version": "1.33.11",
3
+ "version": "1.34.0",
4
4
  "description": "Pixcode — a desktop and mobile web UI for Claude Code, Cursor CLI, Codex, Gemini CLI, Qwen Code, and OpenCode.",
5
5
  "type": "module",
6
6
  "main": "dist-server/server/index.js",
@@ -0,0 +1,98 @@
1
+ // scripts/smoke/a2a-roundtrip.mjs
2
+ // End-to-end smoke check for the A2A foundation.
3
+ //
4
+ // Usage: node scripts/smoke/a2a-roundtrip.mjs [baseUrl]
5
+ // Default: http://127.0.0.1:3001
6
+ //
7
+ // Pre-reqs:
8
+ // - pixcode server running (npm run server:dev-watch)
9
+ // - ANTHROPIC_API_KEY (or pixcode auth) configured for Claude Code
10
+ //
11
+ // What it does:
12
+ // 1. GET /a2a/.well-known/agent-card.json - sanity check
13
+ // 2. GET /a2a/agents - confirms claude-code is registered
14
+ // 3. POST /a2a/tasks - submits a tiny task
15
+ // 4. Streams /a2a/tasks/:id/stream - prints events until terminal state
16
+ //
17
+ // Pass/fail:
18
+ // Exits 0 on terminal state "completed". Non-zero otherwise.
19
+
20
+ const baseUrl = process.argv[2] ?? 'http://127.0.0.1:3001';
21
+
22
+ async function jget(path) {
23
+ const r = await fetch(`${baseUrl}${path}`);
24
+ if (!r.ok) throw new Error(`GET ${path} -> ${r.status}`);
25
+ return r.json();
26
+ }
27
+
28
+ async function main() {
29
+ console.log('1) /a2a/.well-known/agent-card.json');
30
+ const card = await jget('/a2a/.well-known/agent-card.json');
31
+ console.log(' name=', card.name, 'version=', card.version);
32
+ if (card.name !== 'pixcode') throw new Error('AgentCard.name != "pixcode"');
33
+
34
+ console.log('2) /a2a/agents');
35
+ const agents = await jget('/a2a/agents');
36
+ const ids = agents.agents.map((a) => a.name);
37
+ console.log(' registered:', ids.join(', '));
38
+ if (!ids.includes('pixcode-claude-code')) {
39
+ throw new Error('claude-code adapter not registered');
40
+ }
41
+
42
+ console.log('3) POST /a2a/tasks');
43
+ const submitRes = await fetch(`${baseUrl}/a2a/tasks`, {
44
+ method: 'POST',
45
+ headers: { 'content-type': 'application/json' },
46
+ body: JSON.stringify({
47
+ adapterId: 'claude-code',
48
+ message: {
49
+ messageId: 'm_smoke_1',
50
+ role: 'user',
51
+ parts: [{ kind: 'text', text: 'Reply with the single word: ok' }],
52
+ },
53
+ }),
54
+ });
55
+ if (!submitRes.ok) throw new Error(`submit -> ${submitRes.status}`);
56
+ const task = await submitRes.json();
57
+ console.log(' task.id=', task.id, 'state=', task.state);
58
+
59
+ console.log('4) GET /a2a/tasks/:id/stream (SSE)');
60
+ const streamRes = await fetch(`${baseUrl}/a2a/tasks/${task.id}/stream`);
61
+ if (!streamRes.ok) throw new Error(`stream -> ${streamRes.status}`);
62
+
63
+ const reader = streamRes.body.getReader();
64
+ const dec = new TextDecoder();
65
+ let buffer = '';
66
+ let terminalState = null;
67
+
68
+ while (true) {
69
+ const { done, value } = await reader.read();
70
+ if (done) break;
71
+ buffer += dec.decode(value, { stream: true });
72
+
73
+ let idx;
74
+ while ((idx = buffer.indexOf('\n\n')) !== -1) {
75
+ const frame = buffer.slice(0, idx);
76
+ buffer = buffer.slice(idx + 2);
77
+ const dataLine = frame.split('\n').find((l) => l.startsWith('data: '));
78
+ if (!dataLine) continue;
79
+ const event = JSON.parse(dataLine.slice('data: '.length));
80
+ console.log(' event:', event.kind ?? 'snapshot', '->', event);
81
+ if (event.kind === 'task-state') {
82
+ terminalState = event.state;
83
+ if (['completed', 'canceled', 'failed'].includes(terminalState)) break;
84
+ }
85
+ }
86
+ if (terminalState && ['completed', 'canceled', 'failed'].includes(terminalState)) break;
87
+ }
88
+
89
+ console.log('terminal state:', terminalState);
90
+ if (terminalState !== 'completed') {
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ main().catch((err) => {
96
+ console.error('SMOKE FAILED:', err);
97
+ process.exit(2);
98
+ });
package/server/index.js CHANGED
@@ -71,6 +71,11 @@ import qwenRoutes from './routes/qwen.js';
71
71
  import pluginsRoutes from './routes/plugins.js';
72
72
  import messagesRoutes from './routes/messages.js';
73
73
  import providerRoutes from './modules/providers/provider.routes.js';
74
+ import {
75
+ createA2ARouter,
76
+ adapterRegistry,
77
+ ClaudeCodeA2AAdapter,
78
+ } from './modules/orchestration/index.js';
74
79
  import networkRoutes from './routes/network.js';
75
80
  import telegramRoutes from './routes/telegram.js';
76
81
  import { restoreBotFromConfig } from './services/telegram/bot.js';
@@ -376,6 +381,10 @@ app.use('/api/sessions', authenticateToken, messagesRoutes);
376
381
  // Unified provider MCP routes (protected)
377
382
  app.use('/api/providers', authenticateToken, providerRoutes);
378
383
 
384
+ // A2A protocol router — has its own auth middleware, do NOT wrap with authenticateToken
385
+ adapterRegistry.register(new ClaudeCodeA2AAdapter());
386
+ app.use('/a2a', createA2ARouter());
387
+
379
388
  // Network discovery / QR endpoints (protected)
380
389
  app.use('/api/network', authenticateToken, networkRoutes);
381
390
 
@@ -0,0 +1,58 @@
1
+ // server/modules/orchestration/a2a/adapter-registry.ts
2
+ // In-process registry mapping adapter ids to AbstractA2AAdapter
3
+ // instances. Resolution supports three id forms:
4
+ // - "claude-code" explicit
5
+ // - "skill:<skillId>" first REGISTERED adapter advertising that skill
6
+ // (Map iteration is insertion-ordered per ES spec).
7
+ // - "auto" first registered adapter (placeholder until
8
+ // AI-suggested routing arrives in a later plan)
9
+
10
+ import type { AbstractA2AAdapter } from '@/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js';
11
+ import type { AgentCard } from '@/modules/orchestration/a2a/types.js';
12
+
13
+ class AdapterRegistry {
14
+ // Map iteration order is insertion-ordered (ES spec); auto and skill: resolution depend on this.
15
+ private readonly byId = new Map<string, AbstractA2AAdapter>();
16
+
17
+ register(adapter: AbstractA2AAdapter): void {
18
+ if (this.byId.has(adapter.id)) {
19
+ throw new Error(`A2A adapter already registered: ${adapter.id}`);
20
+ }
21
+ this.byId.set(adapter.id, adapter);
22
+ }
23
+
24
+ get(idOrSelector: string): AbstractA2AAdapter | undefined {
25
+ if (idOrSelector === 'auto') {
26
+ if (this.byId.size > 1) {
27
+ throw new Error(
28
+ 'A2A adapter selector "auto" is not yet implemented for multi-adapter registries. ' +
29
+ 'Pass an explicit adapter id ("claude-code") or a "skill:<id>" selector. ' +
30
+ 'AI-suggested routing will replace this stub in a later plan.',
31
+ );
32
+ }
33
+ const first = this.byId.values().next().value;
34
+ return first ?? undefined;
35
+ }
36
+ if (idOrSelector.startsWith('skill:')) {
37
+ const skill = idOrSelector.slice('skill:'.length);
38
+ for (const adapter of this.byId.values()) {
39
+ if (adapter.agentCard.skills.some((s) => s.id === skill)) {
40
+ return adapter;
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+ return this.byId.get(idOrSelector);
46
+ }
47
+
48
+ list(): AbstractA2AAdapter[] {
49
+ return [...this.byId.values()];
50
+ }
51
+
52
+ agentCards(): AgentCard[] {
53
+ return this.list().map((a) => a.agentCard);
54
+ }
55
+ }
56
+
57
+ export const adapterRegistry = new AdapterRegistry();
58
+ export type { AdapterRegistry };
@@ -0,0 +1,49 @@
1
+ // server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts
2
+ // Base class every CLI adapter extends. Adapters wrap the
3
+ // existing per-CLI runtime files (claude-sdk.js, openai-codex.js, ...)
4
+ // and translate between A2A messages and the CLI's native I/O.
5
+
6
+ import { a2aBus } from '@/modules/orchestration/a2a/bus.js';
7
+ import type {
8
+ AgentCard,
9
+ Artifact,
10
+ Message,
11
+ Task,
12
+ TaskError,
13
+ TaskState,
14
+ } from '@/modules/orchestration/a2a/types.js';
15
+
16
+ export interface AdapterContext {
17
+ /** Where the adapter executes — for now this is the project cwd; a future
18
+ * plan introduces WorkspaceHandle (worktree / docker). */
19
+ cwd: string;
20
+ /** pixcode permission mode passed through to the underlying CLI. */
21
+ permissionMode?: 'acceptEdits' | 'plan' | 'bypassPermissions' | 'default';
22
+ /** Optional parent task id when this adapter is invoked inside a workflow. */
23
+ parentTaskId?: string;
24
+ }
25
+
26
+ export interface TaskHandle {
27
+ cancel(): Promise<void>;
28
+ finished: Promise<void>;
29
+ }
30
+
31
+ export abstract class AbstractA2AAdapter {
32
+ abstract readonly id: string;
33
+ abstract readonly agentCard: AgentCard;
34
+
35
+ abstract submitTask(task: Task, ctx: AdapterContext): Promise<TaskHandle>;
36
+ abstract cancelTask(taskId: string): Promise<void>;
37
+
38
+ protected emitState(taskId: string, state: TaskState, error?: TaskError): void {
39
+ a2aBus.publish({ kind: 'task-state', taskId, state, error });
40
+ }
41
+
42
+ protected emitMessage(taskId: string, message: Message): void {
43
+ a2aBus.publish({ kind: 'message', taskId, message });
44
+ }
45
+
46
+ protected emitArtifact(taskId: string, artifact: Artifact): void {
47
+ a2aBus.publish({ kind: 'artifact', taskId, artifact });
48
+ }
49
+ }
@@ -0,0 +1,283 @@
1
+ // server/modules/orchestration/a2a/adapters/claude-code.adapter.ts
2
+ // Wraps the existing server/claude-sdk.js queryClaudeSDK() function.
3
+ // claude-sdk.js was designed to stream SDK messages over a WebSocket
4
+ // connection, so we feed it a "fake WS" that captures send() calls and
5
+ // emits A2A bus events instead.
6
+ //
7
+ // IMPORTANT: claude-sdk.js calls ws.send(<NormalizedMessage object>) — it
8
+ // does NOT JSON.stringify before send. Our shim therefore receives objects
9
+ // (not strings) and dispatches on `frame.kind` (not `frame.type`). See
10
+ // server/shared/types.ts for the MessageKind enum.
11
+
12
+ import crypto from 'node:crypto';
13
+
14
+ // eslint-disable-next-line boundaries/no-unknown -- claude-sdk.js is a top-level CLI runtime not yet classified by eslint.config.js; cleanup deferred (cascades into a server/services classification gap).
15
+ import { abortClaudeSDKSession, queryClaudeSDK } from '@/claude-sdk.js';
16
+ import { AbstractA2AAdapter } from '@/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js';
17
+ import type {
18
+ AdapterContext,
19
+ TaskHandle,
20
+ } from '@/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js';
21
+ import type { AgentCard, Part, Task } from '@/modules/orchestration/a2a/types.js';
22
+
23
+ interface FakeWS {
24
+ send(data: unknown): void;
25
+ readyState: number;
26
+ }
27
+
28
+ // WebSocket.OPEN per the ws library — claude-sdk.js gates send() on readyState === 1.
29
+ const WS_OPEN = 1;
30
+
31
+ function joinPartsToPrompt(parts: Part[]): string {
32
+ return parts
33
+ .map((p) => {
34
+ if (p.kind === 'text') return p.text;
35
+ if (p.kind === 'data') return JSON.stringify(p.data);
36
+ // file parts: include name + uri/inline marker
37
+ return `[file:${p.name}${p.uri ? ` uri=${p.uri}` : ''}]`;
38
+ })
39
+ .join('\n');
40
+ }
41
+
42
+ function newId(prefix: string): string {
43
+ return `${prefix}_${crypto.randomBytes(8).toString('hex')}`;
44
+ }
45
+
46
+ export class ClaudeCodeA2AAdapter extends AbstractA2AAdapter {
47
+ readonly id = 'claude-code';
48
+
49
+ readonly agentCard: AgentCard = {
50
+ name: 'pixcode-claude-code',
51
+ description: 'Anthropic Claude Code, accessed via Pixcode',
52
+ url: '/a2a/agents/claude-code',
53
+ version: '1.0.0',
54
+ capabilities: ['streaming', 'fileEdit', 'commandExec', 'mcp'],
55
+ skills: [
56
+ {
57
+ id: 'architectural-review',
58
+ description: 'Review code architecture and propose structural changes',
59
+ },
60
+ {
61
+ id: 'typescript-edit',
62
+ description: 'Edit TypeScript files with type-aware reasoning',
63
+ },
64
+ {
65
+ id: 'multi-file-refactor',
66
+ description: 'Coordinated edits across many files',
67
+ },
68
+ {
69
+ id: 'test-run',
70
+ description: 'Run test suites and react to results',
71
+ },
72
+ ],
73
+ authentication: { type: 'bearer' },
74
+ };
75
+
76
+ private readonly active = new Map<string, { sessionId: string | null }>();
77
+
78
+ async submitTask(task: Task, ctx: AdapterContext): Promise<TaskHandle> {
79
+ // Foundation: only the last user message is fed in. Multi-turn resumption
80
+ // (input-required tasks, workflow chaining) needs to pass options.sessionId
81
+ // and append history; deferred to a follow-on plan.
82
+ const promptText = joinPartsToPrompt(
83
+ task.history[task.history.length - 1]?.parts ?? [],
84
+ );
85
+ const session = { sessionId: null as string | null };
86
+ this.active.set(task.id, session);
87
+
88
+ this.emitState(task.id, 'working');
89
+
90
+ const fakeWS: FakeWS = {
91
+ readyState: WS_OPEN,
92
+ send: (data) => this.handleSdkFrame(task.id, data, session),
93
+ };
94
+
95
+ const finished = (async () => {
96
+ try {
97
+ await queryClaudeSDK(
98
+ promptText,
99
+ {
100
+ cwd: ctx.cwd,
101
+ permissionMode: ctx.permissionMode ?? 'default',
102
+ },
103
+ fakeWS,
104
+ );
105
+ // If cancelTask removed us from `active` first, suppress the spurious
106
+ // 'completed' that would otherwise race the 'canceled' state.
107
+ if (this.active.has(task.id)) {
108
+ this.emitState(task.id, 'completed');
109
+ }
110
+ } catch (err) {
111
+ if (this.active.has(task.id)) {
112
+ this.emitState(task.id, 'failed', {
113
+ code: 'ADAPTER_RUNTIME_ERROR',
114
+ message: err instanceof Error ? err.message : String(err),
115
+ });
116
+ }
117
+ } finally {
118
+ this.active.delete(task.id);
119
+ }
120
+ })();
121
+
122
+ return {
123
+ cancel: () => this.cancelTask(task.id),
124
+ finished,
125
+ };
126
+ }
127
+
128
+ async cancelTask(taskId: string): Promise<void> {
129
+ const session = this.active.get(taskId);
130
+ if (!session) {
131
+ this.emitState(taskId, 'canceled');
132
+ return;
133
+ }
134
+ // Delete BEFORE awaiting so submitTask's IIFE guard (this.active.has)
135
+ // suppresses the spurious 'completed' state when queryClaudeSDK's
136
+ // for-await loop unwinds from the abort.
137
+ this.active.delete(taskId);
138
+ if (session.sessionId) {
139
+ try {
140
+ await abortClaudeSDKSession(session.sessionId);
141
+ } catch {
142
+ // swallow — adapter has already cleaned its own state
143
+ }
144
+ }
145
+ this.emitState(taskId, 'canceled');
146
+ }
147
+
148
+ /**
149
+ * claude-sdk.js calls `ws.send(<NormalizedMessage>)` with a JS OBJECT
150
+ * (not a JSON string). We translate each frame into A2A bus events.
151
+ * See server/shared/types.ts for the MessageKind union.
152
+ */
153
+ private handleSdkFrame(
154
+ taskId: string,
155
+ frame: unknown,
156
+ session: { sessionId: string | null },
157
+ ): void {
158
+ if (!frame || typeof frame !== 'object') return;
159
+ const f = frame as {
160
+ kind?: string;
161
+ sessionId?: unknown;
162
+ newSessionId?: unknown;
163
+ text?: unknown;
164
+ content?: unknown;
165
+ toolName?: unknown;
166
+ toolInput?: unknown;
167
+ toolResult?: unknown;
168
+ };
169
+
170
+ // session_created carries the new session id in `newSessionId`. Capture
171
+ // it here so cancelTask can call abortClaudeSDKSession with the right id.
172
+ if (
173
+ f.kind === 'session_created' &&
174
+ typeof f.newSessionId === 'string' &&
175
+ !session.sessionId
176
+ ) {
177
+ session.sessionId = f.newSessionId;
178
+ }
179
+
180
+ switch (f.kind) {
181
+ case 'session_created':
182
+ case 'status':
183
+ case 'stream_delta':
184
+ case 'stream_end':
185
+ // session_created and status are not user-facing.
186
+ // stream_delta and stream_end CARRY user-visible delta text but are
187
+ // not currently emitted by claude-sdk.js (it doesn't pass
188
+ // includePartialMessages: true to query()). If that flag flips on
189
+ // upstream, these cases must be re-routed to emit text Messages.
190
+ return;
191
+
192
+ case 'text':
193
+ case 'thinking': {
194
+ const text =
195
+ typeof f.text === 'string'
196
+ ? f.text
197
+ : typeof f.content === 'string'
198
+ ? f.content
199
+ : null;
200
+ if (text) {
201
+ this.emitMessage(taskId, {
202
+ messageId: newId('msg'),
203
+ role: 'agent',
204
+ parts: [{ kind: 'text', text }],
205
+ taskId,
206
+ });
207
+ }
208
+ return;
209
+ }
210
+
211
+ case 'tool_use': {
212
+ this.emitArtifact(taskId, {
213
+ artifactId: newId('art'),
214
+ type: 'command-output',
215
+ parts: [
216
+ {
217
+ kind: 'data',
218
+ data: { toolName: f.toolName, toolInput: f.toolInput },
219
+ },
220
+ ],
221
+ metadata: { source: 'claude-tool-use' },
222
+ });
223
+ return;
224
+ }
225
+
226
+ case 'tool_result': {
227
+ this.emitArtifact(taskId, {
228
+ artifactId: newId('art'),
229
+ type: 'command-output',
230
+ parts: [{ kind: 'data', data: { toolResult: f.toolResult } }],
231
+ metadata: { source: 'claude-tool-result' },
232
+ });
233
+ return;
234
+ }
235
+
236
+ case 'permission_request':
237
+ case 'permission_cancelled':
238
+ case 'interactive_prompt':
239
+ case 'task_notification':
240
+ // Informational — surface as data artifact for visibility.
241
+ this.emitArtifact(taskId, {
242
+ artifactId: newId('art'),
243
+ type: 'data',
244
+ parts: [{ kind: 'data', data: f as Record<string, unknown> }],
245
+ metadata: { source: `claude-${f.kind}` },
246
+ });
247
+ return;
248
+
249
+ case 'error': {
250
+ // claude-sdk.js catches internally and emits an error frame without
251
+ // rethrowing, so the IIFE await would resolve cleanly. Force the
252
+ // failed state here and remove from active so the IIFE's
253
+ // 'completed' emit is suppressed by its active.has() guard.
254
+ const message =
255
+ typeof f.content === 'string'
256
+ ? f.content
257
+ : typeof f.text === 'string'
258
+ ? f.text
259
+ : 'Claude Code reported an error';
260
+ this.emitState(taskId, 'failed', {
261
+ code: 'CLAUDE_RUNTIME_ERROR',
262
+ message,
263
+ details: f as Record<string, unknown>,
264
+ });
265
+ this.active.delete(taskId);
266
+ return;
267
+ }
268
+
269
+ case 'complete':
270
+ // Lifecycle redundant with the IIFE's 'completed' emit; suppress to
271
+ // avoid double-signaling. The IIFE owns terminal state transitions.
272
+ return;
273
+
274
+ default:
275
+ // Unknown kind — surface for visibility
276
+ this.emitArtifact(taskId, {
277
+ artifactId: newId('art'),
278
+ type: 'data',
279
+ parts: [{ kind: 'data', data: f as Record<string, unknown> }],
280
+ });
281
+ }
282
+ }
283
+ }