@agent-harness-experimental/adapter-claude-code 0.0.0 → 0.0.1
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.
- package/LICENSE +1 -0
- package/README.md +102 -2
- package/dist/adapter.d.ts +29 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +215 -0
- package/dist/adapter.js.map +1 -0
- package/dist/bridge-continuation.mts +1 -0
- package/dist/bridge-delegation.mts +1 -0
- package/dist/bridge-helpers.mts +122 -0
- package/dist/bridge-tool-policy.mts +1 -0
- package/dist/bridge-ws-server.mts +1 -0
- package/dist/bridge.mts +745 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/pnpm-lock.yaml +1038 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +61 -0
- package/dist/setup.js.map +1 -0
- package/package.json +56 -3
package/dist/bridge.mts
ADDED
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code bridge — runs inside the Vercel Sandbox.
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. WS server starts → prints bridge-ready
|
|
7
|
+
* 2. Waits for harness to connect and send 'start'
|
|
8
|
+
* 3. Starts Claude Code with the prompt from 'start'
|
|
9
|
+
* 4. Streams events over WS
|
|
10
|
+
* 5. Waits for harness to receive all events before exiting
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
initBridge,
|
|
14
|
+
waitForStart,
|
|
15
|
+
waitForNextPrompt,
|
|
16
|
+
setRunning,
|
|
17
|
+
emit,
|
|
18
|
+
requestToolResult,
|
|
19
|
+
requestToolDecision,
|
|
20
|
+
drainUserMessages,
|
|
21
|
+
consumeCompactRequest,
|
|
22
|
+
finish,
|
|
23
|
+
} from './bridge-ws-server.mts';
|
|
24
|
+
import { createClaudeQueryToolOptions, recordClaudeSubagentFinish, recordClaudeSubagentStart } from './bridge-helpers.mts';
|
|
25
|
+
import { createDelegateToolSchema, parseDelegateTaskInput } from './bridge-delegation.mts';
|
|
26
|
+
import { isToolAllowed } from './bridge-tool-policy.mts';
|
|
27
|
+
import { applyBridgeContinuationConfig } from './bridge-continuation.mts';
|
|
28
|
+
import { msg as protocolMessage } from '@agent-harness-experimental/protocol';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
|
|
31
|
+
// Crash loud
|
|
32
|
+
process.on('uncaughtException', (err) => {
|
|
33
|
+
emit(protocolMessage.error({ message: `Bridge crash: ${err.message}\n${err.stack}` }));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
process.on('unhandledRejection', (err: unknown) => {
|
|
37
|
+
emit(protocolMessage.error({ message: `Bridge rejection: ${err instanceof Error ? err.message : String(err)}` }));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
initBridge();
|
|
42
|
+
|
|
43
|
+
let { prompt, tools: toolSchemas, config } = await waitForStart();
|
|
44
|
+
setRunning();
|
|
45
|
+
|
|
46
|
+
const BRIDGE_DEBUG = process.env.BRIDGE_DEBUG === '1';
|
|
47
|
+
|
|
48
|
+
function debug(message: string) {
|
|
49
|
+
if (BRIDGE_DEBUG) {
|
|
50
|
+
process.stderr.write(`[bridge-claude] ${message}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const claudeStderrLines: string[] = [];
|
|
55
|
+
|
|
56
|
+
function recordClaudeStderr(data: string) {
|
|
57
|
+
for (const line of data.split(/\r?\n/)) {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (!trimmed) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
claudeStderrLines.push(trimmed);
|
|
63
|
+
if (claudeStderrLines.length > 50) {
|
|
64
|
+
claudeStderrLines.shift();
|
|
65
|
+
}
|
|
66
|
+
debug(`[claude-code:stderr] ${trimmed}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatClaudeProcessError(error: unknown, observedTerminalError: string | undefined): string {
|
|
71
|
+
const message = observedTerminalError || String(error);
|
|
72
|
+
const stderrTail = claudeStderrLines.slice(-10).join('\n');
|
|
73
|
+
return stderrTail ? `${message}\n${stderrTail}` : message;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let isFirstTurn = true;
|
|
77
|
+
|
|
78
|
+
type JsonSchemaObject = {
|
|
79
|
+
properties?: Record<string, unknown>;
|
|
80
|
+
required?: string[];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type ClaudeBridgeConfig = {
|
|
84
|
+
workDir?: string;
|
|
85
|
+
interceptBuiltinTools?: boolean | string[];
|
|
86
|
+
activeTools?: string[];
|
|
87
|
+
builtinToolMappings?: Array<{
|
|
88
|
+
name: string;
|
|
89
|
+
nativeName?: string;
|
|
90
|
+
}>;
|
|
91
|
+
model?: string;
|
|
92
|
+
maxTurns?: number;
|
|
93
|
+
executableArgs?: string[];
|
|
94
|
+
instructions?: string;
|
|
95
|
+
thinking?: 'adaptive' | 'disabled' | Record<string, unknown>;
|
|
96
|
+
effort?: string;
|
|
97
|
+
agents?: Record<string, AgentDefinition>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type AgentDefinition = {
|
|
101
|
+
description: string;
|
|
102
|
+
prompt: string;
|
|
103
|
+
model?: string;
|
|
104
|
+
tools?: string[];
|
|
105
|
+
maxTurns?: number;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type ClaudeTextContent = {
|
|
109
|
+
type?: string;
|
|
110
|
+
text?: string;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type ClaudeMessageBlock = {
|
|
114
|
+
type?: string;
|
|
115
|
+
id?: string;
|
|
116
|
+
name?: string;
|
|
117
|
+
input?: unknown;
|
|
118
|
+
tool_use_id?: string;
|
|
119
|
+
content?: string | ClaudeTextContent[];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type ClaudeQueryMessage = {
|
|
123
|
+
type?: string;
|
|
124
|
+
event?: {
|
|
125
|
+
type?: string;
|
|
126
|
+
delta?: {
|
|
127
|
+
type?: string;
|
|
128
|
+
text?: string;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
message?: {
|
|
132
|
+
content?: ClaudeMessageBlock[];
|
|
133
|
+
};
|
|
134
|
+
subtype?: string;
|
|
135
|
+
stop_reason?: string;
|
|
136
|
+
errors?: string[];
|
|
137
|
+
usage?: {
|
|
138
|
+
input_tokens?: number;
|
|
139
|
+
output_tokens?: number;
|
|
140
|
+
};
|
|
141
|
+
total_cost_usd?: number;
|
|
142
|
+
result?: string;
|
|
143
|
+
terminal_reason?: string;
|
|
144
|
+
error?: string;
|
|
145
|
+
error_status?: number | null;
|
|
146
|
+
output?: string[];
|
|
147
|
+
patch?: {
|
|
148
|
+
status?: 'pending' | 'running' | 'completed' | 'failed' | 'killed';
|
|
149
|
+
error?: string;
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const claudeMessageBlockSchema = z
|
|
154
|
+
.object({
|
|
155
|
+
type: z.string().optional(),
|
|
156
|
+
id: z.string().optional(),
|
|
157
|
+
name: z.string().optional(),
|
|
158
|
+
input: z.unknown().optional(),
|
|
159
|
+
tool_use_id: z.string().optional(),
|
|
160
|
+
content: z.union([z.string(), z.array(z.object({ type: z.string().optional(), text: z.string().optional() }).passthrough())]).optional(),
|
|
161
|
+
})
|
|
162
|
+
.passthrough();
|
|
163
|
+
|
|
164
|
+
const claudeQueryMessageSchema: z.ZodType<ClaudeQueryMessage> = z
|
|
165
|
+
.object({
|
|
166
|
+
type: z.string().optional(),
|
|
167
|
+
event: z
|
|
168
|
+
.object({
|
|
169
|
+
type: z.string().optional(),
|
|
170
|
+
delta: z
|
|
171
|
+
.object({
|
|
172
|
+
type: z.string().optional(),
|
|
173
|
+
text: z.string().optional(),
|
|
174
|
+
})
|
|
175
|
+
.passthrough()
|
|
176
|
+
.optional(),
|
|
177
|
+
})
|
|
178
|
+
.passthrough()
|
|
179
|
+
.optional(),
|
|
180
|
+
message: z
|
|
181
|
+
.object({ content: z.array(claudeMessageBlockSchema).optional() })
|
|
182
|
+
.passthrough()
|
|
183
|
+
.optional(),
|
|
184
|
+
subtype: z.string().optional(),
|
|
185
|
+
stop_reason: z.string().optional(),
|
|
186
|
+
errors: z.array(z.string()).optional(),
|
|
187
|
+
usage: z.object({ input_tokens: z.number().optional(), output_tokens: z.number().optional() }).passthrough().optional(),
|
|
188
|
+
total_cost_usd: z.number().optional(),
|
|
189
|
+
result: z.string().optional(),
|
|
190
|
+
terminal_reason: z.string().optional(),
|
|
191
|
+
error: z.string().optional(),
|
|
192
|
+
error_status: z.number().nullable().optional(),
|
|
193
|
+
output: z.array(z.string()).optional(),
|
|
194
|
+
patch: z
|
|
195
|
+
.object({
|
|
196
|
+
status: z.enum(['pending', 'running', 'completed', 'failed', 'killed']).optional(),
|
|
197
|
+
error: z.string().optional(),
|
|
198
|
+
})
|
|
199
|
+
.passthrough()
|
|
200
|
+
.optional(),
|
|
201
|
+
})
|
|
202
|
+
.passthrough();
|
|
203
|
+
|
|
204
|
+
function parseNativeEvent(raw: unknown): ClaudeQueryMessage {
|
|
205
|
+
return claudeQueryMessageSchema.parse(raw);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
type ZodLikeSchema = {
|
|
209
|
+
describe(description: string): ZodLikeSchema;
|
|
210
|
+
optional(): ZodLikeSchema;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
type ZodLike = {
|
|
214
|
+
string(): ZodLikeSchema;
|
|
215
|
+
number(): ZodLikeSchema;
|
|
216
|
+
boolean(): ZodLikeSchema;
|
|
217
|
+
any(): ZodLikeSchema;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
type BridgeMcpServer = {
|
|
221
|
+
tool(
|
|
222
|
+
name: string,
|
|
223
|
+
description: string,
|
|
224
|
+
shape: Record<string, ZodLikeSchema>,
|
|
225
|
+
handler: (input: Record<string, unknown>) => Promise<{ content: Array<{ type: 'text'; text: string }> }>,
|
|
226
|
+
): void;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
type ClaudeQueryStream = AsyncIterable<ClaudeQueryMessage> & {
|
|
230
|
+
close(): void;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
type ClaudeQueryFn = (args: { prompt: string; options: Record<string, unknown> }) => ClaudeQueryStream;
|
|
234
|
+
|
|
235
|
+
function extractBlockOutput(content: string | ClaudeTextContent[] | unknown): string {
|
|
236
|
+
if (typeof content === 'string') {
|
|
237
|
+
return content;
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(content)) {
|
|
240
|
+
return content
|
|
241
|
+
.filter((entry) => entry.type === 'text')
|
|
242
|
+
.map((entry) => entry.text ?? '')
|
|
243
|
+
.join('\n');
|
|
244
|
+
}
|
|
245
|
+
return JSON.stringify(content);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const { query } = (await import('@anthropic-ai/claude-agent-sdk')) as { query: ClaudeQueryFn };
|
|
249
|
+
let bridgeConfig = config as ClaudeBridgeConfig;
|
|
250
|
+
|
|
251
|
+
function applyContinuationConfig(next: Partial<ClaudeBridgeConfig> | undefined) {
|
|
252
|
+
applyBridgeContinuationConfig(bridgeConfig, next, ['instructions', 'activeTools', 'interceptBuiltinTools']);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function requestContinuationToolResult(requestId: string) {
|
|
256
|
+
debug(`waiting for tool result requestId=${requestId}`);
|
|
257
|
+
const response = await requestToolResult(requestId);
|
|
258
|
+
debug(`received tool result requestId=${requestId}`);
|
|
259
|
+
applyContinuationConfig(response.config as Partial<ClaudeBridgeConfig> | undefined);
|
|
260
|
+
return response.output;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function requestContinuationToolDecision(requestId: string) {
|
|
264
|
+
const decision = await requestToolDecision(requestId);
|
|
265
|
+
applyContinuationConfig(decision.config as Partial<ClaudeBridgeConfig> | undefined);
|
|
266
|
+
return decision;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildSubagentSystemPrompt(agentName: string, definition: AgentDefinition, instructions?: string): string {
|
|
270
|
+
return [
|
|
271
|
+
`You are the subagent "${agentName}".`,
|
|
272
|
+
definition.prompt,
|
|
273
|
+
instructions ?? '',
|
|
274
|
+
definition.tools?.length ? `Only use these tools for this delegated task: ${definition.tools.join(', ')}.` : '',
|
|
275
|
+
]
|
|
276
|
+
.filter(Boolean)
|
|
277
|
+
.join('\n\n');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function buildDelegationSystemPrompt(agents: Record<string, AgentDefinition> | undefined): string | undefined {
|
|
281
|
+
if (!agents || Object.keys(agents).length === 0) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return [
|
|
286
|
+
'Configured subagents are available through the `delegate_task` tool.',
|
|
287
|
+
'When the user asks you to delegate, review with a subagent, or call `delegate_task`, call that tool and wait for its result before the final response.',
|
|
288
|
+
'Do not simulate subagent work inline, and do not output a reviewer confirmation marker unless it was returned by `delegate_task`.',
|
|
289
|
+
'Available subagents:',
|
|
290
|
+
...Object.entries(agents).map(([name, definition]) => `- ${name}: ${definition.description}`),
|
|
291
|
+
].join('\n');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildMainSystemPromptAppend(): string | undefined {
|
|
295
|
+
const append = [bridgeConfig.instructions, delegationSystemPrompt].filter(Boolean).join('\n\n');
|
|
296
|
+
return append || undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Map from MCP-prefixed name to original name
|
|
300
|
+
const delegateToolSchema = createDelegateToolSchema(bridgeConfig.agents);
|
|
301
|
+
const mcpToolSchemas = delegateToolSchema ? [...toolSchemas, delegateToolSchema] : toolSchemas;
|
|
302
|
+
let delegationSystemPrompt = buildDelegationSystemPrompt(bridgeConfig.agents);
|
|
303
|
+
const mcpNameMap = new Map<string, string>();
|
|
304
|
+
for (const t of mcpToolSchemas) {
|
|
305
|
+
mcpNameMap.set(`mcp__harness-tools__${t.name}`, t.name);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Build MCP server for user-defined tools
|
|
309
|
+
let mcpServers: Record<string, unknown> = {};
|
|
310
|
+
|
|
311
|
+
if (mcpToolSchemas.length > 0) {
|
|
312
|
+
const { McpServer } = (await import('@modelcontextprotocol/sdk/server/mcp.js')) as {
|
|
313
|
+
McpServer: new (config: { name: string; version: string }) => BridgeMcpServer;
|
|
314
|
+
};
|
|
315
|
+
const { z } = (await import('zod')) as { z: ZodLike };
|
|
316
|
+
const server = new McpServer({ name: 'harness-tools', version: '1.0.0' });
|
|
317
|
+
|
|
318
|
+
for (const schema of mcpToolSchemas) {
|
|
319
|
+
const inputSchema = schema.inputSchema as JsonSchemaObject;
|
|
320
|
+
const zodShape: Record<string, ZodLikeSchema> = {};
|
|
321
|
+
const props = inputSchema.properties ?? {};
|
|
322
|
+
const required = new Set(inputSchema.required ?? []);
|
|
323
|
+
for (const [key, val] of Object.entries(props)) {
|
|
324
|
+
const v = val as Record<string, unknown>;
|
|
325
|
+
let zType: ZodLikeSchema;
|
|
326
|
+
switch (v.type) {
|
|
327
|
+
case 'string':
|
|
328
|
+
zType = z.string();
|
|
329
|
+
break;
|
|
330
|
+
case 'number':
|
|
331
|
+
zType = z.number();
|
|
332
|
+
break;
|
|
333
|
+
case 'boolean':
|
|
334
|
+
zType = z.boolean();
|
|
335
|
+
break;
|
|
336
|
+
default:
|
|
337
|
+
zType = z.any();
|
|
338
|
+
}
|
|
339
|
+
if (v.description) zType = zType.describe(v.description as string);
|
|
340
|
+
zodShape[key] = required.has(key) ? zType : zType.optional();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
server.tool(schema.name, schema.description ?? '', zodShape, async (_input: Record<string, unknown>) => {
|
|
344
|
+
// The tool-call event was already emitted from the assistant message
|
|
345
|
+
// observation below (with raw input). Just wait for the result.
|
|
346
|
+
debug(`mcp tool invoked name=${schema.name}`);
|
|
347
|
+
const requestId = pendingToolRequestIds.get(schema.name);
|
|
348
|
+
if (!requestId) {
|
|
349
|
+
debug(`mcp tool missing pending request name=${schema.name}`);
|
|
350
|
+
return { content: [{ type: 'text', text: 'Error: no pending tool call' }] };
|
|
351
|
+
}
|
|
352
|
+
pendingToolRequestIds.delete(schema.name);
|
|
353
|
+
const currentSubagentAllowedTools = subagentAllowedToolStack.at(-1);
|
|
354
|
+
if (currentSubagentAllowedTools?.length && !isToolAllowed(schema.name, currentSubagentAllowedTools)) {
|
|
355
|
+
return { content: [{ type: 'text', text: `Error: tool "${schema.name}" is not allowed for this subagent` }] };
|
|
356
|
+
}
|
|
357
|
+
if (schema.name === 'delegate_task') {
|
|
358
|
+
const decision = await requestContinuationToolDecision(requestId);
|
|
359
|
+
if (!decision.approved) {
|
|
360
|
+
return { content: [{ type: 'text', text: `Error: ${decision.reason || 'delegate_task denied'}` }] };
|
|
361
|
+
}
|
|
362
|
+
const result = await runClaudeDelegateTask(_input, requestId);
|
|
363
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
364
|
+
}
|
|
365
|
+
const result = await requestContinuationToolResult(requestId);
|
|
366
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
mcpServers = { 'harness-tools': { type: 'sdk', name: 'harness-tools', instance: server } };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Track pending tool calls — the assistant message handler emits the event,
|
|
374
|
+
// the MCP callback picks up the requestId to wait for the result
|
|
375
|
+
const pendingToolRequestIds = new Map<string, string>();
|
|
376
|
+
const subagentAllowedToolStack: Array<string[] | undefined> = [];
|
|
377
|
+
|
|
378
|
+
// Track toolCallId → toolName for emitting tool-result with the correct name
|
|
379
|
+
const toolCallNames = new Map<string, string>();
|
|
380
|
+
const activeSubagents = new Map<string, string>();
|
|
381
|
+
const startedSubagentNames = new Set<string>();
|
|
382
|
+
|
|
383
|
+
function observeClaudeAssistantToolCall(block: ClaudeMessageBlock) {
|
|
384
|
+
if (block.type !== 'tool_use' || typeof block.id !== 'string' || typeof block.name !== 'string') {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const mcpOriginalName = mcpNameMap.get(block.name);
|
|
389
|
+
if (mcpOriginalName) {
|
|
390
|
+
const requestId = crypto.randomUUID();
|
|
391
|
+
pendingToolRequestIds.set(mcpOriginalName, requestId);
|
|
392
|
+
toolCallNames.set(block.id, mcpOriginalName);
|
|
393
|
+
emit(protocolMessage.toolCall({ requestId, toolName: mcpOriginalName, toolCallId: block.id, input: block.input }));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
toolCallNames.set(block.id, block.name);
|
|
398
|
+
emit(
|
|
399
|
+
protocolMessage.toolCall({
|
|
400
|
+
requestId: block.id,
|
|
401
|
+
toolName: bridgeConfig.builtinToolMappings?.find((entry) => (entry.nativeName ?? entry.name) === block.name)?.name ?? block.name,
|
|
402
|
+
toolCallId: block.id,
|
|
403
|
+
input: block.input,
|
|
404
|
+
observeOnly: true,
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
407
|
+
let agentName = recordClaudeSubagentStart(activeSubagents, emit, block.name, block.id, block.input);
|
|
408
|
+
if (!agentName && (block.name === 'Agent' || block.name === 'Task') && bridgeConfig.agents && Object.keys(bridgeConfig.agents).length === 1) {
|
|
409
|
+
[agentName] = Object.keys(bridgeConfig.agents);
|
|
410
|
+
activeSubagents.set(block.id, agentName);
|
|
411
|
+
emit(protocolMessage.subagentStart({ agentName, parentToolCallId: block.id }));
|
|
412
|
+
}
|
|
413
|
+
if (agentName) {
|
|
414
|
+
startedSubagentNames.add(agentName);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function observeClaudeToolResult(block: ClaudeMessageBlock) {
|
|
419
|
+
if (block.type !== 'tool_result' || typeof block.tool_use_id !== 'string') {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const toolUseId = block.tool_use_id;
|
|
424
|
+
const toolName = toolCallNames.get(toolUseId) ?? 'unknown';
|
|
425
|
+
const output = extractBlockOutput(block.content);
|
|
426
|
+
emit(protocolMessage.toolResult({ toolName, toolCallId: toolUseId, output }));
|
|
427
|
+
const finishedAgentName = recordClaudeSubagentFinish(activeSubagents, emit, toolUseId, output);
|
|
428
|
+
if (!finishedAgentName && (toolName === 'Agent' || toolName === 'Task') && bridgeConfig.agents && Object.keys(bridgeConfig.agents).length === 1) {
|
|
429
|
+
const [agentName] = Object.keys(bridgeConfig.agents);
|
|
430
|
+
startedSubagentNames.add(agentName);
|
|
431
|
+
emit(protocolMessage.subagentStart({ agentName, parentToolCallId: toolUseId }));
|
|
432
|
+
emit(protocolMessage.subagentFinish({ agentName, parentToolCallId: toolUseId, output }));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function runClaudeDelegateTask(input: unknown, parentToolCallId: string) {
|
|
437
|
+
if (!bridgeConfig.agents) {
|
|
438
|
+
throw new Error('delegate_task requires configured agents');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const { agentName, task, definition } = parseDelegateTaskInput(input, bridgeConfig.agents);
|
|
442
|
+
startedSubagentNames.add(agentName);
|
|
443
|
+
emit(protocolMessage.subagentStart({ agentName, parentToolCallId }));
|
|
444
|
+
let text = '';
|
|
445
|
+
let result: { agent: string; text: string } | undefined;
|
|
446
|
+
let usage: { inputTokens: number; outputTokens: number; costUsd?: number } | undefined;
|
|
447
|
+
const allowedTools = definition.tools?.length ? definition.tools : undefined;
|
|
448
|
+
const queryToolOptions = createClaudeQueryToolOptions(bridgeConfig, emit, requestContinuationToolDecision, allowedTools);
|
|
449
|
+
subagentAllowedToolStack.push(allowedTools);
|
|
450
|
+
const q = query({
|
|
451
|
+
prompt: task,
|
|
452
|
+
options: {
|
|
453
|
+
...(definition.model || bridgeConfig.model ? { model: definition.model ?? bridgeConfig.model } : {}),
|
|
454
|
+
...(definition.maxTurns ? { maxTurns: definition.maxTurns } : bridgeConfig.maxTurns ? { maxTurns: bridgeConfig.maxTurns } : {}),
|
|
455
|
+
systemPrompt: {
|
|
456
|
+
type: 'preset' as const,
|
|
457
|
+
preset: 'claude_code' as const,
|
|
458
|
+
append: buildSubagentSystemPrompt(agentName, definition, bridgeConfig.instructions),
|
|
459
|
+
},
|
|
460
|
+
debug: BRIDGE_DEBUG,
|
|
461
|
+
stderr: recordClaudeStderr,
|
|
462
|
+
includePartialMessages: true,
|
|
463
|
+
permissionMode: queryToolOptions.permissionMode,
|
|
464
|
+
allowDangerouslySkipPermissions: queryToolOptions.allowDangerouslySkipPermissions,
|
|
465
|
+
mcpServers,
|
|
466
|
+
...(queryToolOptions.canUseTool ? { canUseTool: queryToolOptions.canUseTool } : {}),
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
for await (const rawMessage of q) {
|
|
472
|
+
const message = parseNativeEvent(rawMessage);
|
|
473
|
+
|
|
474
|
+
if (message.type === 'stream_event' && message.event?.type === 'content_block_delta' && message.event.delta?.type === 'text_delta') {
|
|
475
|
+
text += message.event.delta.text;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (message.type === 'assistant') {
|
|
479
|
+
for (const block of message.message?.content ?? []) {
|
|
480
|
+
observeClaudeAssistantToolCall(block);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (message.type === 'user') {
|
|
485
|
+
const content = message.message?.content;
|
|
486
|
+
if (Array.isArray(content)) {
|
|
487
|
+
for (const block of content) {
|
|
488
|
+
observeClaudeToolResult(block);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (message.type === 'result') {
|
|
494
|
+
if (message.subtype !== 'success') {
|
|
495
|
+
throw new Error(message.errors?.join('\n') || message.result || 'Unknown subagent error');
|
|
496
|
+
}
|
|
497
|
+
if (message.result?.trim()) {
|
|
498
|
+
text = message.result;
|
|
499
|
+
}
|
|
500
|
+
usage = {
|
|
501
|
+
inputTokens: message.usage?.input_tokens ?? 0,
|
|
502
|
+
outputTokens: message.usage?.output_tokens ?? 0,
|
|
503
|
+
...(typeof message.total_cost_usd === 'number' ? { costUsd: message.total_cost_usd } : {}),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
result = { agent: agentName, text: text.trim() };
|
|
509
|
+
return result;
|
|
510
|
+
} finally {
|
|
511
|
+
subagentAllowedToolStack.pop();
|
|
512
|
+
q.close();
|
|
513
|
+
emit(protocolMessage.subagentFinish({ agentName, parentToolCallId, ...(result ? { output: result } : {}), ...(usage ? { usage } : {}) }));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function promptRequestedDelegation() {
|
|
518
|
+
const text = [prompt, bridgeConfig.instructions].filter(Boolean).join('\n');
|
|
519
|
+
return /\bdelegate_task\b|\bsubagent\b|\bdelegate\b|\breviewer\b/i.test(text);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function shouldAppendAutoDelegateText(finalText: string | undefined, delegateText: string) {
|
|
523
|
+
const trimmedFinal = finalText?.trim() ?? '';
|
|
524
|
+
const trimmedDelegate = delegateText.trim();
|
|
525
|
+
if (!trimmedDelegate || trimmedFinal.includes(trimmedDelegate)) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
if (trimmedFinal.startsWith('{') || /return only json|raw json|json object/i.test(prompt)) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function runMissingDelegateTaskIfNeeded(finalText: string | undefined) {
|
|
535
|
+
if (!bridgeConfig.agents || startedSubagentNames.size > 0 || !promptRequestedDelegation()) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const [agentName] = Object.keys(bridgeConfig.agents);
|
|
540
|
+
if (!agentName) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const requestId = `auto-delegate-${crypto.randomUUID()}`;
|
|
545
|
+
const input = {
|
|
546
|
+
agent: agentName,
|
|
547
|
+
task: [
|
|
548
|
+
'The parent task requested a subagent review through `delegate_task`, but the primary run reached a final answer before invoking it.',
|
|
549
|
+
'Run the requested review now and return only the reviewer result.',
|
|
550
|
+
'',
|
|
551
|
+
`Parent prompt:\n${prompt}`,
|
|
552
|
+
finalText?.trim() ? `\nPrimary final text:\n${finalText.trim()}` : '',
|
|
553
|
+
]
|
|
554
|
+
.filter(Boolean)
|
|
555
|
+
.join('\n'),
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
emit(protocolMessage.toolCall({ requestId, toolName: 'delegate_task', toolCallId: requestId, input }));
|
|
559
|
+
const decision = await requestContinuationToolDecision(requestId);
|
|
560
|
+
if (!decision.approved) {
|
|
561
|
+
emit(
|
|
562
|
+
protocolMessage.toolResult({
|
|
563
|
+
toolName: 'delegate_task',
|
|
564
|
+
toolCallId: requestId,
|
|
565
|
+
output: { type: 'execution-denied', reason: decision.reason || 'delegate_task denied' },
|
|
566
|
+
}),
|
|
567
|
+
);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const result = await runClaudeDelegateTask(input, requestId);
|
|
571
|
+
emit(protocolMessage.toolResult({ toolName: 'delegate_task', toolCallId: requestId, output: result }));
|
|
572
|
+
|
|
573
|
+
if (shouldAppendAutoDelegateText(finalText, result.text)) {
|
|
574
|
+
emit(protocolMessage.textDelta({ delta: `\n${result.text}` }));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// Multi-turn loop: run prompt → finish → wait for next → run again
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
while (true) {
|
|
583
|
+
const queryToolOptions = createClaudeQueryToolOptions(bridgeConfig, emit, requestContinuationToolDecision);
|
|
584
|
+
const mainSystemPromptAppend = buildMainSystemPromptAppend();
|
|
585
|
+
const abortController = new AbortController();
|
|
586
|
+
let observedTerminalError: string | undefined;
|
|
587
|
+
let emittedTerminalError = false;
|
|
588
|
+
let emittedTerminalFinish = false;
|
|
589
|
+
|
|
590
|
+
function emitTerminalError(message: string | undefined) {
|
|
591
|
+
const normalized = message?.trim();
|
|
592
|
+
if (!normalized || emittedTerminalError || emittedTerminalFinish) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
observedTerminalError = normalized;
|
|
597
|
+
emittedTerminalError = true;
|
|
598
|
+
emit(protocolMessage.error({ message: normalized }));
|
|
599
|
+
abortController.abort();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const q = query({
|
|
603
|
+
prompt,
|
|
604
|
+
options: {
|
|
605
|
+
abortController,
|
|
606
|
+
...(bridgeConfig.model ? { model: bridgeConfig.model } : {}),
|
|
607
|
+
...(bridgeConfig.maxTurns ? { maxTurns: bridgeConfig.maxTurns } : {}),
|
|
608
|
+
...(bridgeConfig.executableArgs ? { executableArgs: bridgeConfig.executableArgs } : {}),
|
|
609
|
+
...(mainSystemPromptAppend
|
|
610
|
+
? {
|
|
611
|
+
systemPrompt: { type: 'preset' as const, preset: 'claude_code' as const, append: mainSystemPromptAppend },
|
|
612
|
+
}
|
|
613
|
+
: {}),
|
|
614
|
+
...(bridgeConfig.thinking
|
|
615
|
+
? {
|
|
616
|
+
thinking:
|
|
617
|
+
bridgeConfig.thinking === 'adaptive' ? { type: 'adaptive' } : bridgeConfig.thinking === 'disabled' ? { type: 'disabled' } : bridgeConfig.thinking,
|
|
618
|
+
}
|
|
619
|
+
: {}),
|
|
620
|
+
...(bridgeConfig.effort ? { effort: bridgeConfig.effort } : {}),
|
|
621
|
+
debug: BRIDGE_DEBUG,
|
|
622
|
+
stderr: recordClaudeStderr,
|
|
623
|
+
// Multi-turn: continue the most recent conversation on subsequent turns
|
|
624
|
+
...(!isFirstTurn ? { continue: true } : {}),
|
|
625
|
+
includePartialMessages: true,
|
|
626
|
+
// Sandbox is already isolated — default to maximum-liberal permissions
|
|
627
|
+
// (bypass all prompts, auto-approve writes and shell). When the harness
|
|
628
|
+
// opts into tool interception, we must use 'default' permissionMode so
|
|
629
|
+
// the SDK calls canUseTool; the callback itself still auto-approves
|
|
630
|
+
// anything not explicitly intercepted.
|
|
631
|
+
permissionMode: queryToolOptions.permissionMode,
|
|
632
|
+
allowDangerouslySkipPermissions: queryToolOptions.allowDangerouslySkipPermissions,
|
|
633
|
+
mcpServers,
|
|
634
|
+
...(queryToolOptions.canUseTool ? { canUseTool: queryToolOptions.canUseTool } : {}),
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
for await (const rawMessage of q) {
|
|
640
|
+
const message = parseNativeEvent(rawMessage);
|
|
641
|
+
const type = message.type;
|
|
642
|
+
debug(
|
|
643
|
+
`message type=${String(type)} subtype=${String(message.subtype ?? '')} stop_reason=${String(message.stop_reason ?? '')} result=${JSON.stringify(message.result ?? null)}`,
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
if (typeof message.error === 'string' && message.error.trim()) {
|
|
647
|
+
observedTerminalError = message.error.trim();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (type === 'auth_status' && typeof message.error === 'string' && message.error.trim()) {
|
|
651
|
+
emitTerminalError(message.error);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (type === 'system' && message.subtype === 'api_retry' && message.error_status && [401, 403, 404].includes(message.error_status)) {
|
|
656
|
+
emitTerminalError(`HTTP ${message.error_status}: ${message.error ?? 'provider request failed'}`);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (type === 'system' && message.subtype === 'task_updated' && message.patch?.status === 'failed' && typeof message.patch.error === 'string') {
|
|
661
|
+
emitTerminalError(message.patch.error);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (type === 'stream_event') {
|
|
666
|
+
const event = message.event;
|
|
667
|
+
if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta' && typeof event.delta.text === 'string') {
|
|
668
|
+
emit(protocolMessage.textDelta({ delta: event.delta.text }));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (type === 'assistant') {
|
|
673
|
+
for (const block of message.message?.content ?? []) {
|
|
674
|
+
observeClaudeAssistantToolCall(block);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (type === 'user') {
|
|
679
|
+
const content = message.message?.content;
|
|
680
|
+
if (Array.isArray(content)) {
|
|
681
|
+
for (const block of content) {
|
|
682
|
+
observeClaudeToolResult(block);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (type === 'result') {
|
|
688
|
+
if (message.subtype === 'success') {
|
|
689
|
+
const emptyResult = !message.result?.trim();
|
|
690
|
+
if (emptyResult && observedTerminalError) {
|
|
691
|
+
emitTerminalError(observedTerminalError);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (
|
|
695
|
+
emptyResult &&
|
|
696
|
+
bridgeConfig.model &&
|
|
697
|
+
message.stop_reason === 'stop_sequence' &&
|
|
698
|
+
(message.terminal_reason === 'model_error' || ((message.usage?.input_tokens ?? 0) === 0 && (message.usage?.output_tokens ?? 0) === 0))
|
|
699
|
+
) {
|
|
700
|
+
emitTerminalError(`Unknown model "${bridgeConfig.model}"`);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
emittedTerminalFinish = true;
|
|
705
|
+
await runMissingDelegateTaskIfNeeded(message.result);
|
|
706
|
+
emit(
|
|
707
|
+
protocolMessage.finish({
|
|
708
|
+
finishReason: message.stop_reason || 'stop',
|
|
709
|
+
usage: {
|
|
710
|
+
inputTokens: message.usage?.input_tokens ?? 0,
|
|
711
|
+
outputTokens: message.usage?.output_tokens ?? 0,
|
|
712
|
+
costUsd: message.total_cost_usd,
|
|
713
|
+
},
|
|
714
|
+
}),
|
|
715
|
+
);
|
|
716
|
+
} else {
|
|
717
|
+
emitTerminalError(message.errors?.join('\n') || observedTerminalError || message.result || 'Unknown error');
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} catch (err) {
|
|
722
|
+
if (!(abortController.signal.aborted && emittedTerminalError)) {
|
|
723
|
+
emit(protocolMessage.error({ message: formatClaudeProcessError(err, observedTerminalError) }));
|
|
724
|
+
}
|
|
725
|
+
} finally {
|
|
726
|
+
q.close();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
isFirstTurn = false;
|
|
730
|
+
await finish();
|
|
731
|
+
|
|
732
|
+
// Wait for next prompt via WS
|
|
733
|
+
process.stderr.write('[bridge] Waiting for next prompt...\n');
|
|
734
|
+
const next = await waitForNextPrompt();
|
|
735
|
+
const queuedMessages = drainUserMessages();
|
|
736
|
+
if (consumeCompactRequest()) {
|
|
737
|
+
process.stderr.write('[bridge] Compact requested (queued for adapter-native support later)\n');
|
|
738
|
+
}
|
|
739
|
+
prompt = queuedMessages.length > 0 ? `${queuedMessages.join('\n\n')}\n\n${next.prompt}` : next.prompt;
|
|
740
|
+
config = next.config;
|
|
741
|
+
bridgeConfig = next.config as ClaudeBridgeConfig;
|
|
742
|
+
delegationSystemPrompt = buildDelegationSystemPrompt(bridgeConfig.agents);
|
|
743
|
+
startedSubagentNames.clear();
|
|
744
|
+
setRunning();
|
|
745
|
+
}
|