@workbench-ai/agent-driver-anthropic-claude-code 0.0.44
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/dist/global-skills.d.ts +6 -0
- package/dist/global-skills.d.ts.map +1 -0
- package/dist/global-skills.js +35 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1138 -0
- package/dist/workbench-auth.d.ts +69 -0
- package/dist/workbench-auth.d.ts.map +1 -0
- package/dist/workbench-auth.js +212 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1138 @@
|
|
|
1
|
+
import { promises as fs, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { createCliHarnessManifest, defineHarnessProvider, applyNormalizedHarnessActivity, buildManagedHarnessEnv, createHarnessSession, createPendingHarnessTurn, ensureDir, HarnessTraceBuilder, getManagedHarnessHomePath, nowIso, persistStageSessionWorkspace, prepareStageSessionWorkspace, runHarnessPrepareCommand, resolveHarnessConfiguredEffort, resolveHarnessConfiguredModel, resolveRuntimeEnv, sharedHarnessEffortValues, terminateProcess, buildCanonicalToolCall, } from "@workbench-ai/agent-driver";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { projectClaudeGlobalSkills, syncClaudeGlobalSkills, } from "./global-skills.js";
|
|
8
|
+
import { CLAUDE_CODE_OAUTH_TOKEN_ENV, claudeWorkbenchProviderAuth, } from "./workbench-auth.js";
|
|
9
|
+
const ClaudeSecretRefAuthSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
strategy: z.literal("secret_ref"),
|
|
12
|
+
ref: z.string().min(1),
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
const ClaudeProfilePathAuthSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
strategy: z.literal("profile_path"),
|
|
18
|
+
path: z.string().min(1),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
const ClaudeBedrockEnvAuthSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
strategy: z.literal("bedrock_env"),
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
export const ClaudeHarnessAuthSchema = z.discriminatedUnion("strategy", [
|
|
27
|
+
ClaudeSecretRefAuthSchema,
|
|
28
|
+
ClaudeProfilePathAuthSchema,
|
|
29
|
+
ClaudeBedrockEnvAuthSchema,
|
|
30
|
+
]);
|
|
31
|
+
export const ClaudeHarnessConfigSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
max_turns: z.number().int().positive().default(24),
|
|
34
|
+
max_budget_usd: z.number().positive().optional(),
|
|
35
|
+
allowed_tools: z.array(z.string().min(1)).optional(),
|
|
36
|
+
add_dirs: z.array(z.string().min(1)).optional(),
|
|
37
|
+
permission_mode: z
|
|
38
|
+
.enum([
|
|
39
|
+
"acceptEdits",
|
|
40
|
+
"auto",
|
|
41
|
+
"bypassPermissions",
|
|
42
|
+
"default",
|
|
43
|
+
"dontAsk",
|
|
44
|
+
"plan",
|
|
45
|
+
])
|
|
46
|
+
.default("acceptEdits"),
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
49
|
+
export function createClaudeHarnessDefinition(options = {}) {
|
|
50
|
+
return {
|
|
51
|
+
id: "anthropic/claude-code",
|
|
52
|
+
displayName: "Anthropic Claude Code",
|
|
53
|
+
auth: ClaudeHarnessAuthSchema,
|
|
54
|
+
config: ClaudeHarnessConfigSchema,
|
|
55
|
+
defaults: {
|
|
56
|
+
auth: {
|
|
57
|
+
strategy: "secret_ref",
|
|
58
|
+
ref: "ANTHROPIC_API_KEY",
|
|
59
|
+
},
|
|
60
|
+
config: {
|
|
61
|
+
max_turns: 24,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
capabilities: {
|
|
65
|
+
supports_resume: true,
|
|
66
|
+
supports_interrupt: true,
|
|
67
|
+
required_runtime_capabilities: ["shell_execution", "dotenv_secrets"],
|
|
68
|
+
},
|
|
69
|
+
supportedWorkspaceModes: ["managed", "project"],
|
|
70
|
+
async checkReadiness(args) {
|
|
71
|
+
await ClaudeCodeHarnessAdapter.ensureAuthReady(args.plan, args.repoRoot, args.runtimeHome);
|
|
72
|
+
ClaudeCodeHarnessAdapter.validateConfiguredEffort(args.plan);
|
|
73
|
+
return {
|
|
74
|
+
availability_errors: [],
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
create() {
|
|
78
|
+
return new ClaudeCodeHarnessAdapter(options.executable?.trim() || "claude");
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export const claudeHarnessDefinition = createClaudeHarnessDefinition();
|
|
83
|
+
export const claudeHarnessManifest = createCliHarnessManifest(claudeHarnessDefinition);
|
|
84
|
+
export const claudeHarnessProvider = claudeCodeHarness();
|
|
85
|
+
export { projectClaudeGlobalSkills, syncClaudeGlobalSkills, } from "./global-skills.js";
|
|
86
|
+
export { CLAUDE_CODE_OAUTH_TOKEN_ENV, claudeWorkbenchProviderAuth, collectClaudeWorkbenchBedrockEnv, parseClaudeSetupTokenOutput, } from "./workbench-auth.js";
|
|
87
|
+
export function claudeCodeHarness(options = {}) {
|
|
88
|
+
const definition = createClaudeHarnessDefinition(options);
|
|
89
|
+
return defineHarnessProvider({
|
|
90
|
+
manifest: createCliHarnessManifest(definition),
|
|
91
|
+
schemas: {
|
|
92
|
+
auth: definition.auth,
|
|
93
|
+
config: definition.config,
|
|
94
|
+
},
|
|
95
|
+
checkReadiness: definition.checkReadiness,
|
|
96
|
+
create: definition.create,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export const claudeTraceReplayer = {
|
|
100
|
+
harnessId: claudeHarnessManifest.id,
|
|
101
|
+
parseRawReplayEntries(entries) {
|
|
102
|
+
const replayEntries = parseClaudeTraceReplayEntries(entries, (entry) => {
|
|
103
|
+
if ((entry.source !== "stdout" && entry.source !== "stderr") ||
|
|
104
|
+
typeof entry.at !== "string") {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
at: entry.at,
|
|
109
|
+
source: entry.source,
|
|
110
|
+
payload: entry.payload ?? {},
|
|
111
|
+
text: typeof entry.text === "string" ? entry.text : undefined,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
return replayEntries.length === 0 ? null : { entries: replayEntries };
|
|
115
|
+
},
|
|
116
|
+
parseHarnessReplayEntries(entries) {
|
|
117
|
+
const replayEntries = parseClaudeTraceReplayEntries(entries, (entry) => {
|
|
118
|
+
if (typeof entry.at !== "string" || typeof entry.name !== "string") {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (entry.name === "claude/stderr") {
|
|
122
|
+
const payloadValue = entry.payload;
|
|
123
|
+
const payload = isJsonObject(payloadValue) ? payloadValue : {};
|
|
124
|
+
return {
|
|
125
|
+
at: entry.at,
|
|
126
|
+
source: "stderr",
|
|
127
|
+
text: typeof payload.text === "string" ? payload.text : "",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (!entry.name.startsWith("claude/")) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
at: entry.at,
|
|
135
|
+
source: "stdout",
|
|
136
|
+
payload: entry.payload ?? {},
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
return replayEntries.length === 0 ? null : { entries: replayEntries };
|
|
140
|
+
},
|
|
141
|
+
async buildTraceBundle(args) {
|
|
142
|
+
const trace = new HarnessTraceBuilder({
|
|
143
|
+
attemptNumber: args.artifact.attempt_number,
|
|
144
|
+
stageId: args.artifact.stage_id,
|
|
145
|
+
stageRunIndex: args.artifact.run_index,
|
|
146
|
+
stageSpanId: args.stageSpanId,
|
|
147
|
+
});
|
|
148
|
+
const promptAttributes = promptAttributesFromSpan(args.oldTurnSpan);
|
|
149
|
+
const turnStartedAt = args.oldTurnSpan?.started_at ?? args.stageStartedAt;
|
|
150
|
+
applyNormalizedHarnessActivity(trace, {
|
|
151
|
+
type: "turn.started",
|
|
152
|
+
at: turnStartedAt,
|
|
153
|
+
provider: claudeHarnessManifest.id,
|
|
154
|
+
model: readTraceString(args.oldTurnSpan?.attributes, "model") ?? null,
|
|
155
|
+
sessionId: readTraceString(args.oldTurnSpan?.attributes, "session_id") ?? null,
|
|
156
|
+
operationId: readTraceString(args.oldTurnSpan?.attributes, "operation_id") ?? null,
|
|
157
|
+
attributes: Object.keys(promptAttributes).length > 0 ? promptAttributes : undefined,
|
|
158
|
+
});
|
|
159
|
+
const state = {
|
|
160
|
+
providerSessionId: readTraceString(args.oldTurnSpan?.attributes, "session_id") ?? null,
|
|
161
|
+
model: readTraceString(args.oldTurnSpan?.attributes, "model") ?? null,
|
|
162
|
+
operationId: readTraceString(args.oldTurnSpan?.attributes, "operation_id") ?? null,
|
|
163
|
+
lastAssistantText: "",
|
|
164
|
+
toolsById: new Map(),
|
|
165
|
+
};
|
|
166
|
+
for (const entry of args.source.entries) {
|
|
167
|
+
if (entry.source === "stderr") {
|
|
168
|
+
const text = typeof entry.text === "string" ? entry.text : "";
|
|
169
|
+
if (text.trim().length > 0) {
|
|
170
|
+
applyNormalizedHarnessActivity(trace, {
|
|
171
|
+
type: "error",
|
|
172
|
+
at: entry.at,
|
|
173
|
+
message: text.trim(),
|
|
174
|
+
attributes: {
|
|
175
|
+
stream: "stderr",
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const payload = entry.payload;
|
|
182
|
+
if (!isJsonObject(payload)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const normalized = normalizeClaudeEnvelope(state, payload, entry.at);
|
|
186
|
+
for (const activity of normalized.activities) {
|
|
187
|
+
applyNormalizedHarnessActivity(trace, activity);
|
|
188
|
+
}
|
|
189
|
+
if (normalized.providerSessionId) {
|
|
190
|
+
state.providerSessionId = normalized.providerSessionId;
|
|
191
|
+
}
|
|
192
|
+
if (normalized.model) {
|
|
193
|
+
state.model = normalized.model;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return trace.buildBundle(await args.readFinalOutput(), args.endedAt);
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
const CLAUDE_OAUTH_TOKEN_ENV = CLAUDE_CODE_OAUTH_TOKEN_ENV;
|
|
200
|
+
const claudePortableAuthRelativePaths = claudeWorkbenchProviderAuth.profile.optional;
|
|
201
|
+
const claudeManagedEnvDenylist = [
|
|
202
|
+
CLAUDE_OAUTH_TOKEN_ENV,
|
|
203
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
204
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
205
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
206
|
+
"ANTHROPIC_FOUNDRY_RESOURCE",
|
|
207
|
+
"AZURE_API_BASE",
|
|
208
|
+
"AZURE_API_KEY",
|
|
209
|
+
"AZURE_OPENAI_API_KEY",
|
|
210
|
+
];
|
|
211
|
+
const claudeBedrockEnvDenylist = [
|
|
212
|
+
CLAUDE_OAUTH_TOKEN_ENV,
|
|
213
|
+
"ANTHROPIC_API_KEY",
|
|
214
|
+
"AWS_PROFILE",
|
|
215
|
+
"AWS_DEFAULT_PROFILE",
|
|
216
|
+
"AWS_CONFIG_FILE",
|
|
217
|
+
"AWS_SHARED_CREDENTIALS_FILE",
|
|
218
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
219
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
220
|
+
"ANTHROPIC_FOUNDRY_RESOURCE",
|
|
221
|
+
"AZURE_API_BASE",
|
|
222
|
+
"AZURE_API_KEY",
|
|
223
|
+
"AZURE_OPENAI_API_KEY",
|
|
224
|
+
];
|
|
225
|
+
export class ClaudeCodeHarnessAdapter {
|
|
226
|
+
executable;
|
|
227
|
+
manifest = claudeHarnessManifest;
|
|
228
|
+
constructor(executable) {
|
|
229
|
+
this.executable = executable;
|
|
230
|
+
}
|
|
231
|
+
getManagedWorkspaceIgnoreEntries(_plan) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
async startSession(args) {
|
|
235
|
+
const childEnv = await this.prepareChildEnv(args.plan, args.repoRoot, args.runtimeHome, args.stageSessionPath, process.env);
|
|
236
|
+
const preparedWorkspace = await prepareStageSessionWorkspace({
|
|
237
|
+
workspaceMode: args.plan.workspace.mode,
|
|
238
|
+
workspacePath: args.workspacePath,
|
|
239
|
+
stageSessionPath: args.stageSessionPath,
|
|
240
|
+
excludedTopLevelEntries: [".claude"],
|
|
241
|
+
});
|
|
242
|
+
const { workspacePath, attemptWorkspacePath, sessionWorkspacePath } = preparedWorkspace;
|
|
243
|
+
await runHarnessPrepareCommand({
|
|
244
|
+
plan: args.plan,
|
|
245
|
+
workspacePath,
|
|
246
|
+
stageSessionPath: args.stageSessionPath,
|
|
247
|
+
childEnv,
|
|
248
|
+
});
|
|
249
|
+
await syncClaudeGlobalSkills(getManagedHarnessHomePath(args.stageSessionPath));
|
|
250
|
+
return {
|
|
251
|
+
adapter: this,
|
|
252
|
+
ownerStageId: args.ownerStageId,
|
|
253
|
+
session: createHarnessSession({
|
|
254
|
+
harnessId: this.manifest.id,
|
|
255
|
+
attemptNumber: args.attemptNumber,
|
|
256
|
+
stageId: args.stageId,
|
|
257
|
+
stageRunIndex: args.stageRunIndex,
|
|
258
|
+
harnessSession: args.sessionMode === "resume" ? (args.persistedSession ?? {}) : {},
|
|
259
|
+
}),
|
|
260
|
+
state: {
|
|
261
|
+
workspacePath,
|
|
262
|
+
attemptWorkspacePath,
|
|
263
|
+
sessionWorkspacePath,
|
|
264
|
+
childEnv,
|
|
265
|
+
sessionMode: args.sessionMode,
|
|
266
|
+
process: null,
|
|
267
|
+
reader: null,
|
|
268
|
+
pendingTurn: null,
|
|
269
|
+
stderrLines: [],
|
|
270
|
+
providerSessionId: args.sessionMode === "resume" &&
|
|
271
|
+
typeof args.persistedSession?.session_id === "string" &&
|
|
272
|
+
args.persistedSession.session_id.trim().length > 0
|
|
273
|
+
? args.persistedSession.session_id
|
|
274
|
+
: null,
|
|
275
|
+
model: null,
|
|
276
|
+
operationId: null,
|
|
277
|
+
lastAssistantText: null,
|
|
278
|
+
toolsById: new Map(),
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
async startTurn(context, args) {
|
|
283
|
+
const config = ClaudeCodeHarnessAdapter.getHarnessConfig(args.plan);
|
|
284
|
+
const harness = ClaudeCodeHarnessAdapter.getHarness(args.plan);
|
|
285
|
+
const pendingTurn = createPendingHarnessTurn({
|
|
286
|
+
session: context.session,
|
|
287
|
+
eventsFile: args.eventsFile,
|
|
288
|
+
rawEventsFile: args.rawEventsFile,
|
|
289
|
+
stageSpanId: args.stageSpanId,
|
|
290
|
+
promptText: args.prompt,
|
|
291
|
+
turnTimeoutMs: harness.turn_timeout_ms,
|
|
292
|
+
stallTimeoutMs: harness.stall_timeout_ms,
|
|
293
|
+
onTimeout: (message) => {
|
|
294
|
+
this.rejectPendingTurn(context, this.withStderr(context, new Error(message)));
|
|
295
|
+
void this.closeSession(context, args.plan.harness.cancel);
|
|
296
|
+
},
|
|
297
|
+
livePersistence: args.livePersistence,
|
|
298
|
+
});
|
|
299
|
+
context.state.operationId = context.session.id;
|
|
300
|
+
const command = buildClaudeCommand(this.executable, args.plan, config, args.prompt, {
|
|
301
|
+
sessionMode: context.state.sessionMode,
|
|
302
|
+
resumeSessionId: context.state.providerSessionId,
|
|
303
|
+
});
|
|
304
|
+
const child = spawn("sh", ["-lc", command], {
|
|
305
|
+
cwd: context.state.workspacePath,
|
|
306
|
+
env: context.state.childEnv,
|
|
307
|
+
stdio: "pipe",
|
|
308
|
+
});
|
|
309
|
+
child.stdout.setEncoding("utf8");
|
|
310
|
+
child.stderr.setEncoding("utf8");
|
|
311
|
+
child.stdin.end();
|
|
312
|
+
const reader = readline.createInterface({
|
|
313
|
+
input: child.stdout,
|
|
314
|
+
crlfDelay: Infinity,
|
|
315
|
+
});
|
|
316
|
+
context.state.process = child;
|
|
317
|
+
context.state.reader = reader;
|
|
318
|
+
pendingTurn.controller.record({
|
|
319
|
+
normalized: {
|
|
320
|
+
type: "turn.started",
|
|
321
|
+
at: nowIso(),
|
|
322
|
+
provider: claudeHarnessManifest.id,
|
|
323
|
+
sessionId: null,
|
|
324
|
+
operationId: context.state.operationId,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
context.state.stderrLines = [];
|
|
328
|
+
context.state.pendingTurn = pendingTurn;
|
|
329
|
+
reader.on("line", (line) => {
|
|
330
|
+
this.handleStdoutLine(context, line);
|
|
331
|
+
});
|
|
332
|
+
child.stderr.on("data", (chunk) => {
|
|
333
|
+
this.handleStderr(context, chunk.toString());
|
|
334
|
+
});
|
|
335
|
+
child.on("error", (error) => {
|
|
336
|
+
this.rejectPendingTurn(context, this.withStderr(context, error));
|
|
337
|
+
});
|
|
338
|
+
child.on("close", (code, signal) => {
|
|
339
|
+
if (context.state.pendingTurn) {
|
|
340
|
+
this.rejectPendingTurn(context, this.withStderr(context, new Error(`claude exited before returning a terminal result with code ${code ?? "null"} signal ${signal ?? "null"}`)));
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
return await pendingTurn.result;
|
|
344
|
+
}
|
|
345
|
+
async interruptTurn(context) {
|
|
346
|
+
if (context.state.process) {
|
|
347
|
+
await terminateProcess(context.state.process, 1_000, 1_000);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async closeSession(context, cancelConfig) {
|
|
351
|
+
if (context.state.reader) {
|
|
352
|
+
context.state.reader.close();
|
|
353
|
+
context.state.reader = null;
|
|
354
|
+
}
|
|
355
|
+
if (context.state.process) {
|
|
356
|
+
await terminateProcess(context.state.process, cancelConfig?.graceful_timeout_ms ?? 1_000, cancelConfig?.hard_kill_timeout_ms ?? 1_000);
|
|
357
|
+
context.state.process = null;
|
|
358
|
+
}
|
|
359
|
+
await persistStageSessionWorkspace({
|
|
360
|
+
sessionWorkspacePath: context.state.sessionWorkspacePath,
|
|
361
|
+
attemptWorkspacePath: context.state.attemptWorkspacePath,
|
|
362
|
+
excludedTopLevelEntries: [".claude"],
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
static getHarness(plan) {
|
|
366
|
+
const harness = plan.harness;
|
|
367
|
+
if (!harness) {
|
|
368
|
+
throw new Error(`Expected ${claudeHarnessManifest.id} harness, received no harness configuration`);
|
|
369
|
+
}
|
|
370
|
+
if (harness.id !== claudeHarnessManifest.id) {
|
|
371
|
+
throw new Error(`Expected ${claudeHarnessManifest.id} harness, received ${harness.id}`);
|
|
372
|
+
}
|
|
373
|
+
return harness;
|
|
374
|
+
}
|
|
375
|
+
static getHarnessAuth(plan) {
|
|
376
|
+
return ClaudeHarnessAuthSchema.parse(ClaudeCodeHarnessAdapter.getHarness(plan).auth);
|
|
377
|
+
}
|
|
378
|
+
static getHarnessConfig(plan) {
|
|
379
|
+
return ClaudeHarnessConfigSchema.parse(ClaudeCodeHarnessAdapter.getHarness(plan).config);
|
|
380
|
+
}
|
|
381
|
+
static async ensureAuthReady(plan, repoRoot, runtimeHome) {
|
|
382
|
+
const auth = ClaudeCodeHarnessAdapter.getHarnessAuth(plan);
|
|
383
|
+
if (auth.strategy === "secret_ref") {
|
|
384
|
+
ClaudeCodeHarnessAdapter.resolveApiKeyAuth(plan, repoRoot, runtimeHome);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (auth.strategy === "bedrock_env") {
|
|
388
|
+
ClaudeCodeHarnessAdapter.resolveBedrockAuth();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const { sourceRoot } = ClaudeCodeHarnessAdapter.resolveProfileAuth(plan, repoRoot);
|
|
392
|
+
await fs.access(path.join(sourceRoot, ".claude.json"));
|
|
393
|
+
await ensureClaudePortableAuthExists(sourceRoot);
|
|
394
|
+
}
|
|
395
|
+
static validateConfiguredEffort(plan) {
|
|
396
|
+
const harness = ClaudeCodeHarnessAdapter.getHarness(plan);
|
|
397
|
+
const effort = resolveHarnessConfiguredEffort(harness, sharedHarnessEffortValues);
|
|
398
|
+
if (harness.effort && !effort) {
|
|
399
|
+
throw new Error(`Unsupported Claude effort "${harness.effort}". Expected one of ${sharedHarnessEffortValues.join(", ")}.`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async prepareChildEnv(plan, repoRoot, runtimeHome, stageSessionPath, parentEnv) {
|
|
403
|
+
const homeDir = getManagedHarnessHomePath(stageSessionPath);
|
|
404
|
+
await ensureDir(homeDir);
|
|
405
|
+
const auth = ClaudeCodeHarnessAdapter.getHarnessAuth(plan);
|
|
406
|
+
if (auth.strategy === "secret_ref") {
|
|
407
|
+
const { apiKey } = ClaudeCodeHarnessAdapter.resolveApiKeyAuth(plan, repoRoot, runtimeHome);
|
|
408
|
+
await ClaudeCodeHarnessAdapter.copyAmbientHomeState(homeDir, parentEnv);
|
|
409
|
+
const sourceHome = parentEnv.HOME?.trim();
|
|
410
|
+
if (sourceHome) {
|
|
411
|
+
await projectClaudeGlobalSkills({
|
|
412
|
+
sourceHomeDir: path.resolve(sourceHome),
|
|
413
|
+
targetHomeDir: homeDir,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return buildClaudeChildEnv(apiKey, homeDir, parentEnv);
|
|
417
|
+
}
|
|
418
|
+
if (auth.strategy === "bedrock_env") {
|
|
419
|
+
ClaudeCodeHarnessAdapter.resolveBedrockAuth(parentEnv);
|
|
420
|
+
await ClaudeCodeHarnessAdapter.copyAmbientHomeState(homeDir, parentEnv);
|
|
421
|
+
const sourceHome = parentEnv.HOME?.trim();
|
|
422
|
+
if (sourceHome) {
|
|
423
|
+
await projectClaudeGlobalSkills({
|
|
424
|
+
sourceHomeDir: path.resolve(sourceHome),
|
|
425
|
+
targetHomeDir: homeDir,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
return buildClaudeBedrockChildEnv(homeDir, parentEnv);
|
|
429
|
+
}
|
|
430
|
+
const { sourceRoot } = ClaudeCodeHarnessAdapter.resolveProfileAuth(plan, repoRoot);
|
|
431
|
+
await ClaudeCodeHarnessAdapter.copyProfileAuth(sourceRoot, homeDir);
|
|
432
|
+
const sourceHome = parentEnv.HOME?.trim();
|
|
433
|
+
if (sourceHome) {
|
|
434
|
+
await projectClaudeGlobalSkills({
|
|
435
|
+
sourceHomeDir: path.resolve(sourceHome),
|
|
436
|
+
targetHomeDir: homeDir,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return buildClaudeChildEnv(null, homeDir, parentEnv);
|
|
440
|
+
}
|
|
441
|
+
handleStdoutLine(context, line) {
|
|
442
|
+
if (!line.trim()) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
let parsed;
|
|
446
|
+
try {
|
|
447
|
+
parsed = JSON.parse(line);
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
this.rejectPendingTurn(context, this.withStderr(context, new Error(`Failed to parse claude stream-json output: ${line}`)));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const pendingTurn = context.state.pendingTurn;
|
|
454
|
+
if (!pendingTurn) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const at = nowIso();
|
|
458
|
+
const redacted = redactClaudeValue(parsed);
|
|
459
|
+
const payload = isJsonObject(redacted)
|
|
460
|
+
? redacted
|
|
461
|
+
: { value: redacted ?? null };
|
|
462
|
+
const envelopeType = typeof payload.type === "string" ? payload.type : "unknown";
|
|
463
|
+
const harnessEvent = createClaudeHarnessEvent(context.session, envelopeType, payload, at);
|
|
464
|
+
const normalized = normalizeClaudeEnvelope(context.state, payload, at);
|
|
465
|
+
pendingTurn.controller.record({
|
|
466
|
+
rawEnvelope: {
|
|
467
|
+
at,
|
|
468
|
+
source: "stdout",
|
|
469
|
+
payload,
|
|
470
|
+
},
|
|
471
|
+
harnessEvent,
|
|
472
|
+
normalized: normalized.activities,
|
|
473
|
+
});
|
|
474
|
+
if (normalized.providerSessionId) {
|
|
475
|
+
context.state.providerSessionId = normalized.providerSessionId;
|
|
476
|
+
context.session.harness_session.session_id = normalized.providerSessionId;
|
|
477
|
+
}
|
|
478
|
+
if (normalized.model) {
|
|
479
|
+
context.state.model = normalized.model;
|
|
480
|
+
context.session.harness_session.model = normalized.model;
|
|
481
|
+
}
|
|
482
|
+
if (envelopeType !== "result") {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const semanticStatus = normalized.resultStatus;
|
|
486
|
+
if (semanticStatus !== "success") {
|
|
487
|
+
this.rejectPendingTurn(context, this.withStderr(context, new Error(normalized.errorMessage ??
|
|
488
|
+
`claude result subtype ${semanticStatus ?? "unknown"}`)));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const completedTurn = context.state.pendingTurn;
|
|
492
|
+
if (!completedTurn) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
context.state.pendingTurn = null;
|
|
496
|
+
completedTurn.resolve(completedTurn.controller.succeed({
|
|
497
|
+
endedAt: at,
|
|
498
|
+
...(normalized.finalOutput != null
|
|
499
|
+
? { finalOutput: normalized.finalOutput }
|
|
500
|
+
: {}),
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
handleStderr(context, text) {
|
|
504
|
+
this.captureStderr(context, text);
|
|
505
|
+
const pendingTurn = context.state.pendingTurn;
|
|
506
|
+
if (!pendingTurn) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const at = nowIso();
|
|
510
|
+
const severity = classifyStderr(text);
|
|
511
|
+
pendingTurn.controller.record({
|
|
512
|
+
rawEnvelope: {
|
|
513
|
+
at,
|
|
514
|
+
source: "stderr",
|
|
515
|
+
text,
|
|
516
|
+
},
|
|
517
|
+
harnessEvent: {
|
|
518
|
+
at,
|
|
519
|
+
attempt_number: context.session.attempt_number,
|
|
520
|
+
stage_id: context.session.stage_id,
|
|
521
|
+
stage_run_index: context.session.stage_run_index,
|
|
522
|
+
phase: severity === "error" ? "error" : "session",
|
|
523
|
+
name: "claude/stderr",
|
|
524
|
+
payload: {
|
|
525
|
+
text,
|
|
526
|
+
severity,
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
normalized: severity === "error"
|
|
530
|
+
? {
|
|
531
|
+
type: "error",
|
|
532
|
+
at,
|
|
533
|
+
message: text.trim() || "stderr",
|
|
534
|
+
attributes: {
|
|
535
|
+
stream: "stderr",
|
|
536
|
+
},
|
|
537
|
+
}
|
|
538
|
+
: null,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
rejectPendingTurn(context, error) {
|
|
542
|
+
const pendingTurn = context.state.pendingTurn;
|
|
543
|
+
if (!pendingTurn) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
context.state.pendingTurn = null;
|
|
547
|
+
pendingTurn.controller.dispose();
|
|
548
|
+
pendingTurn.reject(error);
|
|
549
|
+
}
|
|
550
|
+
captureStderr(context, text) {
|
|
551
|
+
const lines = text
|
|
552
|
+
.split(/\r?\n/u)
|
|
553
|
+
.map((line) => line.trim())
|
|
554
|
+
.filter(Boolean);
|
|
555
|
+
if (lines.length === 0) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
context.state.stderrLines.push(...lines);
|
|
559
|
+
if (context.state.stderrLines.length > 20) {
|
|
560
|
+
context.state.stderrLines.splice(0, context.state.stderrLines.length - 20);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
withStderr(context, error) {
|
|
564
|
+
if (context.state.stderrLines.length === 0) {
|
|
565
|
+
return error;
|
|
566
|
+
}
|
|
567
|
+
return new Error(`${error.message}. claude stderr: ${context.state.stderrLines.join(" | ")}`);
|
|
568
|
+
}
|
|
569
|
+
static resolveApiKeyAuth(plan, repoRoot, runtimeHome) {
|
|
570
|
+
const auth = ClaudeCodeHarnessAdapter.getHarnessAuth(plan);
|
|
571
|
+
if (auth.strategy !== "secret_ref") {
|
|
572
|
+
throw new Error("Claude secret_ref auth is required for API key access.");
|
|
573
|
+
}
|
|
574
|
+
const resolved = resolveRuntimeEnv(auth.ref, repoRoot, { runtimeHome });
|
|
575
|
+
const apiKey = resolved.value?.trim();
|
|
576
|
+
if (!apiKey) {
|
|
577
|
+
const location = resolved.envPath ?? "the environment";
|
|
578
|
+
throw new Error(`${auth.ref} must be set in ${location} before running Claude sessions.`);
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
apiKey,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
static resolveProfileAuth(plan, repoRoot) {
|
|
585
|
+
const auth = ClaudeCodeHarnessAdapter.getHarnessAuth(plan);
|
|
586
|
+
if (auth.strategy !== "profile_path") {
|
|
587
|
+
throw new Error("Claude profile_path auth is required for profile auth.");
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
sourceRoot: path.resolve(repoRoot, auth.path),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
static resolveBedrockAuth(parentEnv = process.env) {
|
|
594
|
+
const usesBearerToken = Boolean(parentEnv.AWS_BEARER_TOKEN_BEDROCK?.trim());
|
|
595
|
+
const hasAccessKey = Boolean(parentEnv.AWS_ACCESS_KEY_ID?.trim());
|
|
596
|
+
const hasSecretKey = Boolean(parentEnv.AWS_SECRET_ACCESS_KEY?.trim());
|
|
597
|
+
if (!usesBearerToken && (!hasAccessKey || !hasSecretKey)) {
|
|
598
|
+
throw new Error("Claude Bedrock auth requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or AWS_BEARER_TOKEN_BEDROCK.");
|
|
599
|
+
}
|
|
600
|
+
const region = parentEnv.AWS_REGION?.trim() || parentEnv.AWS_DEFAULT_REGION?.trim();
|
|
601
|
+
if (!region) {
|
|
602
|
+
throw new Error("Claude Bedrock auth requires AWS_REGION or AWS_DEFAULT_REGION.");
|
|
603
|
+
}
|
|
604
|
+
return { region };
|
|
605
|
+
}
|
|
606
|
+
static async copyProfileAuth(sourceRoot, homeDir) {
|
|
607
|
+
await ensureDir(path.join(homeDir, ".claude"));
|
|
608
|
+
await fs.copyFile(path.join(sourceRoot, ".claude.json"), path.join(homeDir, ".claude.json"));
|
|
609
|
+
await copyClaudePortableAuthFiles(sourceRoot, homeDir);
|
|
610
|
+
}
|
|
611
|
+
static async copyAmbientHomeState(homeDir, parentEnv) {
|
|
612
|
+
const sourceHome = parentEnv.HOME?.trim();
|
|
613
|
+
if (!sourceHome || path.resolve(sourceHome) === path.resolve(homeDir)) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
await fs.copyFile(path.join(sourceHome, ".claude.json"), path.join(homeDir, ".claude.json"));
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
if (isMissingFileError(error)) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function buildClaudeCommand(executable, plan, config, prompt, session) {
|
|
628
|
+
const harness = ClaudeCodeHarnessAdapter.getHarness(plan);
|
|
629
|
+
const configuredModel = resolveHarnessConfiguredModel(harness);
|
|
630
|
+
const configuredEffort = resolveHarnessConfiguredEffort(harness, sharedHarnessEffortValues);
|
|
631
|
+
const args = [
|
|
632
|
+
"-p",
|
|
633
|
+
"--output-format",
|
|
634
|
+
"stream-json",
|
|
635
|
+
"--verbose",
|
|
636
|
+
"--permission-mode",
|
|
637
|
+
config.permission_mode,
|
|
638
|
+
"--setting-sources",
|
|
639
|
+
"user",
|
|
640
|
+
"--max-turns",
|
|
641
|
+
String(config.max_turns),
|
|
642
|
+
];
|
|
643
|
+
if (session.sessionMode === "resume" && session.resumeSessionId) {
|
|
644
|
+
args.push("--continue");
|
|
645
|
+
}
|
|
646
|
+
if (configuredModel) {
|
|
647
|
+
args.push("--model", configuredModel);
|
|
648
|
+
}
|
|
649
|
+
if (config.max_budget_usd != null) {
|
|
650
|
+
args.push("--max-budget-usd", String(config.max_budget_usd));
|
|
651
|
+
}
|
|
652
|
+
if (config.allowed_tools?.length) {
|
|
653
|
+
args.push("--allowed-tools", config.allowed_tools.join(","));
|
|
654
|
+
}
|
|
655
|
+
if (config.add_dirs?.length) {
|
|
656
|
+
for (const directory of config.add_dirs) {
|
|
657
|
+
args.push("--add-dir", directory);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (configuredEffort) {
|
|
661
|
+
args.push("--effort", configuredEffort);
|
|
662
|
+
}
|
|
663
|
+
args.push("--settings", JSON.stringify({ disableAllHooks: true }));
|
|
664
|
+
args.push(prompt);
|
|
665
|
+
return buildClaudeCliCommand(executable, args);
|
|
666
|
+
}
|
|
667
|
+
function buildClaudeCliCommand(command, args) {
|
|
668
|
+
return `${command.trim()} ${args.map(quoteShellArg).join(" ")}`;
|
|
669
|
+
}
|
|
670
|
+
function quoteShellArg(value) {
|
|
671
|
+
if (value.length === 0) {
|
|
672
|
+
return "''";
|
|
673
|
+
}
|
|
674
|
+
return `'${value.replace(/'/gu, `'\"'\"'`)}'`;
|
|
675
|
+
}
|
|
676
|
+
function isMissingFileError(error) {
|
|
677
|
+
return (typeof error === "object" &&
|
|
678
|
+
error !== null &&
|
|
679
|
+
"code" in error &&
|
|
680
|
+
error.code === "ENOENT");
|
|
681
|
+
}
|
|
682
|
+
async function listExistingClaudePortableAuthRelativePaths(sourceRoot) {
|
|
683
|
+
const existing = [];
|
|
684
|
+
for (const relativePath of claudePortableAuthRelativePaths) {
|
|
685
|
+
try {
|
|
686
|
+
await fs.access(path.join(sourceRoot, relativePath));
|
|
687
|
+
existing.push(relativePath);
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
if (!isMissingFileError(error)) {
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return existing;
|
|
696
|
+
}
|
|
697
|
+
async function copyClaudePortableAuthFiles(sourceRoot, homeDir) {
|
|
698
|
+
const existing = await listExistingClaudePortableAuthRelativePaths(sourceRoot);
|
|
699
|
+
if (existing.length === 0) {
|
|
700
|
+
throw claudePortableAuthRequirementError(sourceRoot);
|
|
701
|
+
}
|
|
702
|
+
for (const relativePath of existing) {
|
|
703
|
+
const targetPath = path.join(homeDir, relativePath);
|
|
704
|
+
await ensureDir(path.dirname(targetPath));
|
|
705
|
+
await fs.copyFile(path.join(sourceRoot, relativePath), targetPath);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async function ensureClaudePortableAuthExists(sourceRoot) {
|
|
709
|
+
const existing = await listExistingClaudePortableAuthRelativePaths(sourceRoot);
|
|
710
|
+
if (existing.length > 0) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
throw claudePortableAuthRequirementError(sourceRoot);
|
|
714
|
+
}
|
|
715
|
+
function readClaudeOauthToken(homeDir) {
|
|
716
|
+
try {
|
|
717
|
+
const token = readFileSync(path.join(homeDir, ".claude", "oauth-token"), "utf8").trim();
|
|
718
|
+
return token.length > 0 ? token : null;
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
if (isMissingFileError(error)) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function claudePortableAuthRequirementError(sourceRoot) {
|
|
728
|
+
return new Error(`Claude profile_path auth requires ${claudePortableAuthRelativePaths
|
|
729
|
+
.map((relativePath) => `"${relativePath}"`)
|
|
730
|
+
.join(" or ")} under ${sourceRoot}.`);
|
|
731
|
+
}
|
|
732
|
+
function classifyStderr(text) {
|
|
733
|
+
const lines = text
|
|
734
|
+
.split(/\r?\n/u)
|
|
735
|
+
.map((line) => line.trim())
|
|
736
|
+
.filter((line) => line.length > 0);
|
|
737
|
+
if (lines.length === 0) {
|
|
738
|
+
return "empty";
|
|
739
|
+
}
|
|
740
|
+
if (lines.every((line) => /\bWARN(?:ING)?\b/i.test(line))) {
|
|
741
|
+
return "warning";
|
|
742
|
+
}
|
|
743
|
+
return "error";
|
|
744
|
+
}
|
|
745
|
+
function createClaudeHarnessEvent(session, envelopeType, payload, at) {
|
|
746
|
+
const phase = envelopeType === "system"
|
|
747
|
+
? "session"
|
|
748
|
+
: envelopeType === "result"
|
|
749
|
+
? "turn"
|
|
750
|
+
: envelopeType === "assistant" || envelopeType === "user"
|
|
751
|
+
? "item"
|
|
752
|
+
: envelopeType === "error"
|
|
753
|
+
? "error"
|
|
754
|
+
: "item";
|
|
755
|
+
const subtype = typeof payload.subtype === "string" ? `/${payload.subtype}` : "";
|
|
756
|
+
return {
|
|
757
|
+
at,
|
|
758
|
+
attempt_number: session.attempt_number,
|
|
759
|
+
stage_id: session.stage_id,
|
|
760
|
+
stage_run_index: session.stage_run_index,
|
|
761
|
+
phase,
|
|
762
|
+
name: `claude/${envelopeType}${subtype}`,
|
|
763
|
+
payload,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
export function buildClaudeChildEnv(apiKey, homeDir, parentEnv = process.env) {
|
|
767
|
+
const oauthToken = apiKey ? null : readClaudeOauthToken(homeDir);
|
|
768
|
+
const env = buildManagedHarnessEnv(parentEnv, {
|
|
769
|
+
HOME: homeDir,
|
|
770
|
+
...(apiKey ? { ANTHROPIC_API_KEY: apiKey } : {}),
|
|
771
|
+
...(oauthToken ? { [CLAUDE_OAUTH_TOKEN_ENV]: oauthToken } : {}),
|
|
772
|
+
});
|
|
773
|
+
for (const name of claudeManagedEnvDenylist) {
|
|
774
|
+
delete env[name];
|
|
775
|
+
}
|
|
776
|
+
if (!apiKey) {
|
|
777
|
+
delete env.ANTHROPIC_API_KEY;
|
|
778
|
+
}
|
|
779
|
+
if (oauthToken) {
|
|
780
|
+
env[CLAUDE_OAUTH_TOKEN_ENV] = oauthToken;
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
delete env[CLAUDE_OAUTH_TOKEN_ENV];
|
|
784
|
+
}
|
|
785
|
+
return env;
|
|
786
|
+
}
|
|
787
|
+
export function buildClaudeBedrockChildEnv(homeDir, parentEnv = process.env) {
|
|
788
|
+
const { region } = ClaudeCodeHarnessAdapter.resolveBedrockAuth(parentEnv);
|
|
789
|
+
const bedrockEnv = {
|
|
790
|
+
HOME: homeDir,
|
|
791
|
+
CLAUDE_CODE_USE_BEDROCK: "1",
|
|
792
|
+
AWS_REGION: parentEnv.AWS_REGION?.trim() || region,
|
|
793
|
+
};
|
|
794
|
+
for (const name of claudeWorkbenchProviderAuth.envAuth.bedrock.envAllowlist) {
|
|
795
|
+
const value = parentEnv[name]?.trim();
|
|
796
|
+
if (value) {
|
|
797
|
+
bedrockEnv[name] = value;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
bedrockEnv.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
801
|
+
if (!bedrockEnv.AWS_REGION && region) {
|
|
802
|
+
bedrockEnv.AWS_REGION = region;
|
|
803
|
+
}
|
|
804
|
+
const env = buildManagedHarnessEnv(parentEnv, bedrockEnv);
|
|
805
|
+
for (const name of claudeBedrockEnvDenylist) {
|
|
806
|
+
delete env[name];
|
|
807
|
+
}
|
|
808
|
+
env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
809
|
+
return env;
|
|
810
|
+
}
|
|
811
|
+
export function normalizeClaudeEnvelope(state, payload, at) {
|
|
812
|
+
const activities = [];
|
|
813
|
+
const type = typeof payload.type === "string" ? payload.type : null;
|
|
814
|
+
let providerSessionId = readString(payload.session_id) ?? readString(payload.sessionId);
|
|
815
|
+
let model = readString(payload.model);
|
|
816
|
+
let resultStatus = null;
|
|
817
|
+
let errorMessage = null;
|
|
818
|
+
let finalOutput = null;
|
|
819
|
+
if (type === "system" && payload.subtype === "init") {
|
|
820
|
+
activities.push({
|
|
821
|
+
type: "session.started",
|
|
822
|
+
at,
|
|
823
|
+
provider: claudeHarnessManifest.id,
|
|
824
|
+
model,
|
|
825
|
+
sessionId: providerSessionId,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
if (type === "assistant") {
|
|
829
|
+
const message = asJsonObject(payload.message);
|
|
830
|
+
model = readString(message?.model) ?? model;
|
|
831
|
+
const content = asJsonArray(message?.content);
|
|
832
|
+
let visibleText = "";
|
|
833
|
+
for (const block of content) {
|
|
834
|
+
const blockObject = asJsonObject(block);
|
|
835
|
+
const blockType = readString(blockObject?.type);
|
|
836
|
+
if (blockType === "tool_use") {
|
|
837
|
+
const toolId = readString(blockObject?.id);
|
|
838
|
+
const toolCall = buildCanonicalToolCall({
|
|
839
|
+
rawToolName: readString(blockObject?.name),
|
|
840
|
+
input: blockObject?.input ?? null,
|
|
841
|
+
});
|
|
842
|
+
if (toolId) {
|
|
843
|
+
state.toolsById.set(toolId, toolCall);
|
|
844
|
+
}
|
|
845
|
+
activities.push({
|
|
846
|
+
type: "tool.started",
|
|
847
|
+
at,
|
|
848
|
+
toolId,
|
|
849
|
+
toolName: toolCall.toolName,
|
|
850
|
+
attributes: toolCall.attributes,
|
|
851
|
+
});
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (blockType === "text") {
|
|
855
|
+
visibleText += readString(blockObject?.text) ?? "";
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (visibleText.trim().length > 0) {
|
|
859
|
+
state.lastAssistantText = visibleText;
|
|
860
|
+
finalOutput = visibleText;
|
|
861
|
+
activities.push({
|
|
862
|
+
type: "assistant_output.started",
|
|
863
|
+
at,
|
|
864
|
+
phase: "response",
|
|
865
|
+
itemId: readString(message?.id),
|
|
866
|
+
}, {
|
|
867
|
+
type: "assistant_output.completed",
|
|
868
|
+
at,
|
|
869
|
+
phase: "response",
|
|
870
|
+
itemId: readString(message?.id),
|
|
871
|
+
text: visibleText,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (type === "user") {
|
|
876
|
+
const message = asJsonObject(payload.message);
|
|
877
|
+
const content = asJsonArray(message?.content);
|
|
878
|
+
for (const block of content) {
|
|
879
|
+
const blockObject = asJsonObject(block);
|
|
880
|
+
if (readString(blockObject?.type) !== "tool_result") {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const toolId = readString(blockObject?.tool_use_id) ??
|
|
884
|
+
readString(blockObject?.toolUseId);
|
|
885
|
+
const toolCall = toolId ? (state.toolsById.get(toolId) ?? null) : null;
|
|
886
|
+
const resultPreview = readClaudeToolResultPreview(blockObject);
|
|
887
|
+
activities.push({
|
|
888
|
+
type: "tool.completed",
|
|
889
|
+
at,
|
|
890
|
+
toolId,
|
|
891
|
+
toolName: toolCall?.toolName ?? null,
|
|
892
|
+
attributes: toolCall ||
|
|
893
|
+
resultPreview ||
|
|
894
|
+
readBoolean(blockObject?.is_error) === true
|
|
895
|
+
? {
|
|
896
|
+
...(toolCall?.attributes ?? {}),
|
|
897
|
+
...(resultPreview ? { result_preview: resultPreview } : {}),
|
|
898
|
+
...(readBoolean(blockObject?.is_error) === true
|
|
899
|
+
? { is_error: true }
|
|
900
|
+
: {}),
|
|
901
|
+
}
|
|
902
|
+
: undefined,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (type === "result") {
|
|
907
|
+
const subtype = readString(payload.subtype);
|
|
908
|
+
const isError = readBoolean(payload.is_error);
|
|
909
|
+
const isSuccess = subtype ? subtype === "success" && !isError : !isError;
|
|
910
|
+
resultStatus = isSuccess
|
|
911
|
+
? "success"
|
|
912
|
+
: subtype === "success"
|
|
913
|
+
? "error"
|
|
914
|
+
: (subtype ?? "error");
|
|
915
|
+
const resultText = readString(payload.result);
|
|
916
|
+
const usage = asJsonObject(payload.usage);
|
|
917
|
+
if (usage) {
|
|
918
|
+
const inputTokens = readNumber(usage.input_tokens) ?? readNumber(usage.inputTokens);
|
|
919
|
+
const outputTokens = readNumber(usage.output_tokens) ?? readNumber(usage.outputTokens);
|
|
920
|
+
const cacheCreationInputTokens = readNumber(usage.cache_creation_input_tokens) ??
|
|
921
|
+
readNumber(usage.cacheCreationInputTokens);
|
|
922
|
+
const cacheReadInputTokens = readNumber(usage.cache_read_input_tokens) ??
|
|
923
|
+
readNumber(usage.cacheReadInputTokens);
|
|
924
|
+
const cachedInputTokens = cacheCreationInputTokens != null || cacheReadInputTokens != null
|
|
925
|
+
? (cacheCreationInputTokens ?? 0) + (cacheReadInputTokens ?? 0)
|
|
926
|
+
: null;
|
|
927
|
+
const totalTokens = inputTokens != null || outputTokens != null || cachedInputTokens != null
|
|
928
|
+
? (inputTokens ?? 0) + (outputTokens ?? 0) + (cachedInputTokens ?? 0)
|
|
929
|
+
: null;
|
|
930
|
+
activities.push({
|
|
931
|
+
type: "usage.updated",
|
|
932
|
+
at,
|
|
933
|
+
inputTokens,
|
|
934
|
+
outputTokens,
|
|
935
|
+
attributes: {
|
|
936
|
+
...(inputTokens != null
|
|
937
|
+
? { uncached_input_tokens: inputTokens }
|
|
938
|
+
: {}),
|
|
939
|
+
...(cachedInputTokens != null
|
|
940
|
+
? { cached_input_tokens: cachedInputTokens }
|
|
941
|
+
: {}),
|
|
942
|
+
...(cacheCreationInputTokens != null
|
|
943
|
+
? { cache_creation_input_tokens: cacheCreationInputTokens }
|
|
944
|
+
: {}),
|
|
945
|
+
...(cacheReadInputTokens != null
|
|
946
|
+
? { cache_read_input_tokens: cacheReadInputTokens }
|
|
947
|
+
: {}),
|
|
948
|
+
...(totalTokens != null ? { total_tokens: totalTokens } : {}),
|
|
949
|
+
...(payload.total_cost_usd != null
|
|
950
|
+
? {
|
|
951
|
+
total_cost_usd: payload.total_cost_usd,
|
|
952
|
+
cost_source: "provider",
|
|
953
|
+
}
|
|
954
|
+
: {}),
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
providerSessionId =
|
|
959
|
+
providerSessionId ??
|
|
960
|
+
readString(payload.session_id) ??
|
|
961
|
+
state.providerSessionId;
|
|
962
|
+
model = model ?? state.model;
|
|
963
|
+
finalOutput = resultText ?? state.lastAssistantText;
|
|
964
|
+
errorMessage = readString(payload.error) ?? readString(payload.message);
|
|
965
|
+
if (!isSuccess) {
|
|
966
|
+
errorMessage =
|
|
967
|
+
errorMessage ??
|
|
968
|
+
readString(payload.result) ??
|
|
969
|
+
`Claude completed with subtype ${resultStatus}`;
|
|
970
|
+
}
|
|
971
|
+
if (resultText &&
|
|
972
|
+
resultText.trim().length > 0 &&
|
|
973
|
+
resultText !== state.lastAssistantText) {
|
|
974
|
+
activities.push({
|
|
975
|
+
type: "assistant_output.started",
|
|
976
|
+
at,
|
|
977
|
+
phase: "response",
|
|
978
|
+
itemId: null,
|
|
979
|
+
}, {
|
|
980
|
+
type: "assistant_output.completed",
|
|
981
|
+
at,
|
|
982
|
+
phase: "response",
|
|
983
|
+
itemId: null,
|
|
984
|
+
text: resultText,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
activities.push({
|
|
988
|
+
type: "turn.completed",
|
|
989
|
+
at,
|
|
990
|
+
provider: claudeHarnessManifest.id,
|
|
991
|
+
model,
|
|
992
|
+
sessionId: providerSessionId,
|
|
993
|
+
operationId: state.operationId,
|
|
994
|
+
status: resultStatus === "success" ? "completed" : "failed",
|
|
995
|
+
errorMessage,
|
|
996
|
+
attributes: {
|
|
997
|
+
subtype,
|
|
998
|
+
is_error: isError,
|
|
999
|
+
total_cost_usd: payload.total_cost_usd ?? null,
|
|
1000
|
+
num_turns: payload.num_turns ?? null,
|
|
1001
|
+
duration_ms: payload.duration_ms ?? null,
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
activities,
|
|
1007
|
+
providerSessionId,
|
|
1008
|
+
model,
|
|
1009
|
+
resultStatus,
|
|
1010
|
+
errorMessage,
|
|
1011
|
+
finalOutput,
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
export function redactClaudeValue(value) {
|
|
1015
|
+
if (Array.isArray(value)) {
|
|
1016
|
+
return value
|
|
1017
|
+
.map((entry) => redactClaudeValue(entry))
|
|
1018
|
+
.filter((entry) => entry !== null);
|
|
1019
|
+
}
|
|
1020
|
+
if (!value || typeof value !== "object") {
|
|
1021
|
+
return value;
|
|
1022
|
+
}
|
|
1023
|
+
const record = value;
|
|
1024
|
+
const recordType = readString(record.type);
|
|
1025
|
+
if (recordType === "thinking" ||
|
|
1026
|
+
recordType === "redacted_thinking" ||
|
|
1027
|
+
recordType === "thinking_delta") {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
const delta = asJsonObject(record.delta);
|
|
1031
|
+
if (readString(delta?.type) === "thinking_delta") {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
const redacted = {};
|
|
1035
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
1036
|
+
if (key === "signature") {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const next = redactClaudeValue(entry);
|
|
1040
|
+
if (next !== null) {
|
|
1041
|
+
redacted[key] = next;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return redacted;
|
|
1045
|
+
}
|
|
1046
|
+
function readClaudeToolResultPreview(value) {
|
|
1047
|
+
if (!value) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
const direct = readString(value.content) ??
|
|
1051
|
+
readString(value.result) ??
|
|
1052
|
+
readString(value.error) ??
|
|
1053
|
+
readString(value.message);
|
|
1054
|
+
if (direct) {
|
|
1055
|
+
return truncateClaudeValue(direct, 160);
|
|
1056
|
+
}
|
|
1057
|
+
const content = asJsonArray(value.content);
|
|
1058
|
+
if (content.length === 0) {
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
const text = content
|
|
1062
|
+
.map((entry) => {
|
|
1063
|
+
const block = asJsonObject(entry);
|
|
1064
|
+
return readString(block?.text) ?? "";
|
|
1065
|
+
})
|
|
1066
|
+
.filter((entry) => entry.length > 0)
|
|
1067
|
+
.join(" ")
|
|
1068
|
+
.trim();
|
|
1069
|
+
return text.length > 0 ? truncateClaudeValue(text, 160) : null;
|
|
1070
|
+
}
|
|
1071
|
+
function truncateClaudeValue(value, maxLength) {
|
|
1072
|
+
if (value.length <= maxLength) {
|
|
1073
|
+
return value;
|
|
1074
|
+
}
|
|
1075
|
+
return `${value.slice(0, maxLength - 1)}…`;
|
|
1076
|
+
}
|
|
1077
|
+
function isJsonObject(value) {
|
|
1078
|
+
return !!value && !Array.isArray(value) && typeof value === "object";
|
|
1079
|
+
}
|
|
1080
|
+
function asJsonObject(value) {
|
|
1081
|
+
return value && !Array.isArray(value) && typeof value === "object"
|
|
1082
|
+
? value
|
|
1083
|
+
: null;
|
|
1084
|
+
}
|
|
1085
|
+
function asJsonArray(value) {
|
|
1086
|
+
return Array.isArray(value) ? value : [];
|
|
1087
|
+
}
|
|
1088
|
+
function readString(value) {
|
|
1089
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1090
|
+
}
|
|
1091
|
+
function readNumber(value) {
|
|
1092
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1093
|
+
}
|
|
1094
|
+
function parseClaudeTraceReplayEntries(entries, select) {
|
|
1095
|
+
const replayEntries = [];
|
|
1096
|
+
for (const [index, entry] of entries.entries()) {
|
|
1097
|
+
const selected = select(entry);
|
|
1098
|
+
if (!selected) {
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
replayEntries.push({
|
|
1102
|
+
at: selected.at,
|
|
1103
|
+
source: selected.source,
|
|
1104
|
+
...(selected.source === "stderr"
|
|
1105
|
+
? { text: selected.text ?? "" }
|
|
1106
|
+
: { payload: selected.payload ?? {} }),
|
|
1107
|
+
originalIndex: index,
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
replayEntries.sort((left, right) => {
|
|
1111
|
+
const atCompare = left.at.localeCompare(right.at);
|
|
1112
|
+
if (atCompare !== 0) {
|
|
1113
|
+
return atCompare;
|
|
1114
|
+
}
|
|
1115
|
+
return left.originalIndex - right.originalIndex;
|
|
1116
|
+
});
|
|
1117
|
+
return replayEntries.map(({ originalIndex: _originalIndex, ...entry }) => entry);
|
|
1118
|
+
}
|
|
1119
|
+
function promptAttributesFromSpan(span) {
|
|
1120
|
+
const attributes = {};
|
|
1121
|
+
if (!span?.attributes) {
|
|
1122
|
+
return attributes;
|
|
1123
|
+
}
|
|
1124
|
+
for (const key of ["prompt_text", "prompt_format", "prompt_source"]) {
|
|
1125
|
+
const value = span.attributes[key];
|
|
1126
|
+
if (value != null) {
|
|
1127
|
+
attributes[key] = value;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return attributes;
|
|
1131
|
+
}
|
|
1132
|
+
function readTraceString(attributes, key) {
|
|
1133
|
+
const value = attributes?.[key];
|
|
1134
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1135
|
+
}
|
|
1136
|
+
function readBoolean(value) {
|
|
1137
|
+
return value === true;
|
|
1138
|
+
}
|