remote-codex 0.1.9 → 0.11.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.
- package/apps/supervisor-api/dist/index.js +11942 -6101
- package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-BFD4Ytvg.js → highlighted-body-OFNGDK62-ChrwAL9u.js} +1 -1
- package/apps/supervisor-web/dist/assets/index-DHf2HOXx.js +381 -0
- package/apps/supervisor-web/dist/assets/index-DpWxXCgt.css +32 -0
- package/apps/supervisor-web/dist/assets/{xterm-CukFWbxr.js → xterm-D4sevve4.js} +1 -1
- package/apps/supervisor-web/dist/index.html +2 -2
- package/config/codex-model-pricing.json +63 -0
- package/package.json +5 -2
- package/packages/agent-runtime/src/index.ts +4 -0
- package/packages/agent-runtime/src/management-errors.ts +11 -0
- package/packages/agent-runtime/src/model-pricing.ts +312 -0
- package/packages/agent-runtime/src/registry.ts +19 -4
- package/packages/agent-runtime/src/runtime-errors.ts +97 -0
- package/packages/agent-runtime/src/types.ts +50 -4
- package/packages/agent-runtime/src/unavailable-runtime.ts +169 -0
- package/packages/claude/src/historyItems.ts +693 -0
- package/packages/claude/src/index.ts +2 -0
- package/packages/claude/src/runtimeAdapter.test.ts +2138 -0
- package/packages/claude/src/runtimeAdapter.ts +2145 -0
- package/packages/codex/src/appServerManager.ts +12 -3
- package/packages/codex/src/historyItems.test.ts +110 -0
- package/packages/codex/src/historyItems.ts +97 -16
- package/packages/codex/src/hookHistory.test.ts +59 -0
- package/packages/codex/src/index.ts +7 -0
- package/packages/codex/src/local-session-store.ts +390 -0
- package/packages/codex/src/management/codex-management-service.ts +454 -0
- package/packages/codex/src/management/codexHostConfig.test.ts +88 -0
- package/packages/codex/src/management/codexHostConfig.ts +188 -0
- package/packages/codex/src/management/errors.ts +20 -0
- package/packages/codex/src/modelPricing.test.ts +184 -0
- package/packages/codex/src/modelPricing.ts +9 -0
- package/packages/codex/src/runtime-errors.test.ts +72 -0
- package/packages/codex/src/runtime-errors.ts +37 -0
- package/packages/codex/src/runtimeAdapter.ts +25 -2
- package/packages/codex/src/thread-title.ts +1 -0
- package/packages/db/src/repositories.ts +30 -0
- package/packages/opencode/src/historyItems.test.ts +504 -0
- package/packages/opencode/src/historyItems.ts +896 -0
- package/packages/opencode/src/index.ts +2 -0
- package/packages/opencode/src/runtimeAdapter.test.ts +1355 -0
- package/packages/opencode/src/runtimeAdapter.ts +1469 -0
- package/packages/shared/src/agent-providers.ts +56 -0
- package/packages/shared/src/index.ts +174 -35
- package/apps/supervisor-web/dist/assets/index-CbIt0KnL.css +0 -32
- package/apps/supervisor-web/dist/assets/index-Rd2EBQac.js +0 -377
|
@@ -0,0 +1,2145 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
AgentActionQuestion,
|
|
12
|
+
AgentActionRequestResponseInput,
|
|
13
|
+
AgentHistoryItem,
|
|
14
|
+
AgentMcpServer,
|
|
15
|
+
AgentModel,
|
|
16
|
+
AgentPendingProviderRequest,
|
|
17
|
+
AgentProviderRequest,
|
|
18
|
+
AgentProviderRequestMapping,
|
|
19
|
+
AgentProviderCapabilities,
|
|
20
|
+
AgentRuntime,
|
|
21
|
+
AgentRuntimeEvent,
|
|
22
|
+
AgentRuntimeManagementSchema,
|
|
23
|
+
AgentRuntimeStatus,
|
|
24
|
+
AgentSessionDetail,
|
|
25
|
+
AgentSessionSummary,
|
|
26
|
+
AgentTurn,
|
|
27
|
+
InterruptAgentTurnInput,
|
|
28
|
+
ReadAgentSessionOptions,
|
|
29
|
+
ResumeAgentSessionInput,
|
|
30
|
+
StartAgentSessionInput,
|
|
31
|
+
StartAgentSessionResult,
|
|
32
|
+
StartAgentTurnInput,
|
|
33
|
+
} from '../../agent-runtime/src/index';
|
|
34
|
+
import type {
|
|
35
|
+
AgentBackendInstallationDto,
|
|
36
|
+
} from '../../shared/src/index';
|
|
37
|
+
import {
|
|
38
|
+
AgentRuntimeError,
|
|
39
|
+
markTransientAgentHistoryItem,
|
|
40
|
+
} from '../../agent-runtime/src/index';
|
|
41
|
+
import {
|
|
42
|
+
assistantMessageToHistoryItems,
|
|
43
|
+
buildAgentTurn,
|
|
44
|
+
hiddenInitPrompt,
|
|
45
|
+
isHiddenContinuationMessage,
|
|
46
|
+
isHiddenInitMessage,
|
|
47
|
+
messageContentText,
|
|
48
|
+
partialReasoningDelta,
|
|
49
|
+
partialTextDelta,
|
|
50
|
+
resultForToolUse,
|
|
51
|
+
suppressedClaudeToolUseIds,
|
|
52
|
+
toolUseFromPartialStart,
|
|
53
|
+
toolUseToHistoryItem,
|
|
54
|
+
toolResultBlocks,
|
|
55
|
+
userMessageHistoryItem,
|
|
56
|
+
userMessageToHistoryItem,
|
|
57
|
+
} from './historyItems';
|
|
58
|
+
|
|
59
|
+
const execFileAsync = promisify(execFile);
|
|
60
|
+
|
|
61
|
+
type ClaudePromptInput = string | AsyncIterable<SDKUserMessage>;
|
|
62
|
+
type ClaudeMessageContent =
|
|
63
|
+
| string
|
|
64
|
+
| Array<{
|
|
65
|
+
type: 'text';
|
|
66
|
+
text: string;
|
|
67
|
+
} | {
|
|
68
|
+
type: 'image';
|
|
69
|
+
source: {
|
|
70
|
+
type: 'base64';
|
|
71
|
+
media_type: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
|
|
72
|
+
data: string;
|
|
73
|
+
};
|
|
74
|
+
}>;
|
|
75
|
+
type ClaudeMessageContentBlock = Exclude<ClaudeMessageContent, string>[number];
|
|
76
|
+
type ClaudeQueryFunction = (input: {
|
|
77
|
+
prompt: ClaudePromptInput;
|
|
78
|
+
options: ClaudeQueryOptions;
|
|
79
|
+
}) => Query;
|
|
80
|
+
type ClaudeListSessionsFunction = (_options: ListSessionsOptions) => Promise<SDKSessionInfo[]>;
|
|
81
|
+
type ClaudeGetSessionMessagesFunction = (
|
|
82
|
+
sessionId: string,
|
|
83
|
+
options: GetSessionMessagesOptions,
|
|
84
|
+
) => Promise<SessionMessage[]>;
|
|
85
|
+
type ClaudeGetSessionInfoFunction = (
|
|
86
|
+
sessionId: string,
|
|
87
|
+
options: GetSessionInfoOptions,
|
|
88
|
+
) => Promise<SDKSessionInfo | null>;
|
|
89
|
+
interface ClaudeSdkModule {
|
|
90
|
+
query: ClaudeQueryFunction;
|
|
91
|
+
listSessions: ClaudeListSessionsFunction;
|
|
92
|
+
getSessionMessages: ClaudeGetSessionMessagesFunction;
|
|
93
|
+
getSessionInfo: ClaudeGetSessionInfoFunction;
|
|
94
|
+
}
|
|
95
|
+
interface Query extends AsyncIterable<SDKMessage> {
|
|
96
|
+
close(): void;
|
|
97
|
+
interrupt(): Promise<void>;
|
|
98
|
+
supportedModels(): Promise<ModelInfo[]>;
|
|
99
|
+
mcpServerStatus(): Promise<McpServerStatus[]>;
|
|
100
|
+
}
|
|
101
|
+
interface SDKMessage {
|
|
102
|
+
type: string;
|
|
103
|
+
subtype?: string;
|
|
104
|
+
session_id?: string;
|
|
105
|
+
cwd?: string;
|
|
106
|
+
model?: string;
|
|
107
|
+
uuid?: string;
|
|
108
|
+
message?: unknown;
|
|
109
|
+
event?: unknown;
|
|
110
|
+
parent_tool_use_id?: string | null;
|
|
111
|
+
tool_use_result?: unknown;
|
|
112
|
+
tool_use_id?: string;
|
|
113
|
+
tool_name?: string;
|
|
114
|
+
elapsed_time_seconds?: number;
|
|
115
|
+
usage?: unknown;
|
|
116
|
+
modelUsage?: unknown;
|
|
117
|
+
errors?: string[];
|
|
118
|
+
stop_reason?: string;
|
|
119
|
+
}
|
|
120
|
+
interface SDKUserMessage {
|
|
121
|
+
type: 'user';
|
|
122
|
+
message: {
|
|
123
|
+
role: 'user';
|
|
124
|
+
content: ClaudeMessageContent;
|
|
125
|
+
};
|
|
126
|
+
parent_tool_use_id: string | null;
|
|
127
|
+
}
|
|
128
|
+
interface SDKSessionInfo {
|
|
129
|
+
sessionId: string;
|
|
130
|
+
cwd?: string;
|
|
131
|
+
firstPrompt?: string | null;
|
|
132
|
+
summary?: string | null;
|
|
133
|
+
customTitle?: string | null;
|
|
134
|
+
createdAt?: number | null;
|
|
135
|
+
lastModified?: number | null;
|
|
136
|
+
}
|
|
137
|
+
type SessionMessage = SDKMessage;
|
|
138
|
+
interface ModelInfo {
|
|
139
|
+
value: string;
|
|
140
|
+
displayName: string;
|
|
141
|
+
description: string;
|
|
142
|
+
supportedEffortLevels?: string[];
|
|
143
|
+
supportsEffort?: boolean;
|
|
144
|
+
}
|
|
145
|
+
type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan';
|
|
146
|
+
interface SandboxSettings {
|
|
147
|
+
enabled: boolean;
|
|
148
|
+
autoAllowBashIfSandboxed?: boolean;
|
|
149
|
+
allowUnsandboxedCommands?: boolean;
|
|
150
|
+
filesystem?: {
|
|
151
|
+
allowWrite?: string[];
|
|
152
|
+
denyWrite?: string[];
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
interface ClaudeQueryOptions {
|
|
156
|
+
includeHookEvents?: boolean;
|
|
157
|
+
permissionMode?: PermissionMode;
|
|
158
|
+
thinking?: {
|
|
159
|
+
type: string;
|
|
160
|
+
display: string;
|
|
161
|
+
};
|
|
162
|
+
env?: Record<string, string | undefined>;
|
|
163
|
+
cwd?: string;
|
|
164
|
+
sandbox?: SandboxSettings;
|
|
165
|
+
model?: string;
|
|
166
|
+
betas?: string[];
|
|
167
|
+
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
168
|
+
resume?: string;
|
|
169
|
+
includePartialMessages?: boolean;
|
|
170
|
+
maxTurns?: number;
|
|
171
|
+
tools?: unknown;
|
|
172
|
+
allowDangerouslySkipPermissions?: boolean;
|
|
173
|
+
pathToClaudeCodeExecutable?: string;
|
|
174
|
+
}
|
|
175
|
+
interface GetSessionInfoOptions {}
|
|
176
|
+
interface GetSessionMessagesOptions {
|
|
177
|
+
includeSystemMessages?: boolean;
|
|
178
|
+
}
|
|
179
|
+
interface ListSessionsOptions {}
|
|
180
|
+
interface McpServerStatus {
|
|
181
|
+
name: string;
|
|
182
|
+
status: string;
|
|
183
|
+
tools?: Array<{
|
|
184
|
+
name: string;
|
|
185
|
+
description?: string | null;
|
|
186
|
+
}>;
|
|
187
|
+
}
|
|
188
|
+
export interface ClaudeRuntimeAdapterOptions {
|
|
189
|
+
home: string;
|
|
190
|
+
command?: string;
|
|
191
|
+
clientInfo?: {
|
|
192
|
+
name: string;
|
|
193
|
+
title?: string;
|
|
194
|
+
version?: string;
|
|
195
|
+
};
|
|
196
|
+
query?: ClaudeQueryFunction;
|
|
197
|
+
listSessions?: ClaudeListSessionsFunction;
|
|
198
|
+
getSessionMessages?: ClaudeGetSessionMessagesFunction;
|
|
199
|
+
getSessionInfo?: ClaudeGetSessionInfoFunction;
|
|
200
|
+
sdk?: ClaudeSdkModule;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface ActiveClaudeTurn {
|
|
204
|
+
providerSessionId: string;
|
|
205
|
+
providerTurnId: string;
|
|
206
|
+
startedAt: string;
|
|
207
|
+
query: Query;
|
|
208
|
+
items: Map<string, AgentHistoryItem>;
|
|
209
|
+
itemOrder: string[];
|
|
210
|
+
emittedItems: Set<string>;
|
|
211
|
+
currentStreamMessageId: string | null;
|
|
212
|
+
interrupted: boolean;
|
|
213
|
+
completed: boolean;
|
|
214
|
+
suppressedToolUseIds: Set<string>;
|
|
215
|
+
assistantUsage: ClaudeTokenUsageBreakdown | null;
|
|
216
|
+
resultUsage: ClaudeTokenUsageBreakdown | null;
|
|
217
|
+
modelContextWindow: number | null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const promptPhotoTokenPattern = /\[PHOTO\s+([^\]]+)\]/g;
|
|
221
|
+
|
|
222
|
+
function mimeTypeForImagePath(filePath: string) {
|
|
223
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
224
|
+
switch (extension) {
|
|
225
|
+
case '.jpg':
|
|
226
|
+
case '.jpeg':
|
|
227
|
+
return 'image/jpeg' as const;
|
|
228
|
+
case '.png':
|
|
229
|
+
return 'image/png' as const;
|
|
230
|
+
case '.gif':
|
|
231
|
+
return 'image/gif' as const;
|
|
232
|
+
case '.webp':
|
|
233
|
+
return 'image/webp' as const;
|
|
234
|
+
default:
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function extensionForImageMediaType(mediaType: string | null | undefined) {
|
|
240
|
+
switch (mediaType?.toLowerCase()) {
|
|
241
|
+
case 'image/jpeg':
|
|
242
|
+
case 'image/jpg':
|
|
243
|
+
return 'jpg';
|
|
244
|
+
case 'image/png':
|
|
245
|
+
return 'png';
|
|
246
|
+
case 'image/gif':
|
|
247
|
+
return 'gif';
|
|
248
|
+
case 'image/webp':
|
|
249
|
+
return 'webp';
|
|
250
|
+
default:
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function resolvePromptAssetPath(assetPath: string, cwd: string | null | undefined) {
|
|
256
|
+
if (!cwd) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const resolvedPath = path.isAbsolute(assetPath)
|
|
260
|
+
? path.normalize(assetPath)
|
|
261
|
+
: path.resolve(cwd, assetPath);
|
|
262
|
+
const relativePath = path.relative(cwd, resolvedPath);
|
|
263
|
+
if (relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath))) {
|
|
264
|
+
return resolvedPath;
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function safeAssetFilePart(value: string) {
|
|
270
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || randomUUID();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function* singleUserMessage(content: ClaudeMessageContent): AsyncIterable<SDKUserMessage> {
|
|
274
|
+
yield {
|
|
275
|
+
type: 'user',
|
|
276
|
+
message: {
|
|
277
|
+
role: 'user',
|
|
278
|
+
content,
|
|
279
|
+
},
|
|
280
|
+
parent_tool_use_id: null,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function promptWithImageBlocks(
|
|
285
|
+
prompt: string,
|
|
286
|
+
cwd: string | null | undefined,
|
|
287
|
+
): Promise<ClaudePromptInput> {
|
|
288
|
+
const matches = [...prompt.matchAll(promptPhotoTokenPattern)];
|
|
289
|
+
if (matches.length === 0) {
|
|
290
|
+
return prompt;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const blocks: ClaudeMessageContentBlock[] = [];
|
|
294
|
+
let cursor = 0;
|
|
295
|
+
let includedImage = false;
|
|
296
|
+
|
|
297
|
+
for (const match of matches) {
|
|
298
|
+
const token = match[0];
|
|
299
|
+
const assetPath = match[1]?.trim() ?? '';
|
|
300
|
+
const start = match.index ?? 0;
|
|
301
|
+
const precedingText = prompt.slice(cursor, start);
|
|
302
|
+
if (precedingText) {
|
|
303
|
+
blocks.push({ type: 'text', text: precedingText });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const resolvedPath = resolvePromptAssetPath(assetPath, cwd);
|
|
307
|
+
const mediaType = resolvedPath ? mimeTypeForImagePath(resolvedPath) : null;
|
|
308
|
+
if (!resolvedPath || !mediaType) {
|
|
309
|
+
blocks.push({ type: 'text', text: token });
|
|
310
|
+
cursor = start + token.length;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const data = await fs.readFile(resolvedPath, 'base64');
|
|
316
|
+
blocks.push({
|
|
317
|
+
type: 'image',
|
|
318
|
+
source: {
|
|
319
|
+
type: 'base64',
|
|
320
|
+
media_type: mediaType,
|
|
321
|
+
data,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
includedImage = true;
|
|
325
|
+
} catch {
|
|
326
|
+
blocks.push({ type: 'text', text: token });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
cursor = start + token.length;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const trailingText = prompt.slice(cursor);
|
|
333
|
+
if (trailingText) {
|
|
334
|
+
blocks.push({ type: 'text', text: trailingText });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!includedImage) {
|
|
338
|
+
return prompt;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return singleUserMessage(blocks);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function mergeActiveTranscriptItems(
|
|
345
|
+
transcriptItems: AgentHistoryItem[],
|
|
346
|
+
activeItems: AgentHistoryItem[],
|
|
347
|
+
) {
|
|
348
|
+
const mergedItems = [...transcriptItems];
|
|
349
|
+
const itemIds = new Set(mergedItems.map((item) => item.id));
|
|
350
|
+
for (const item of activeItems) {
|
|
351
|
+
if (item.kind === 'userMessage' || itemIds.has(item.id)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
mergedItems.push(item);
|
|
355
|
+
itemIds.add(item.id);
|
|
356
|
+
}
|
|
357
|
+
return mergedItems;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
interface ClaudeTokenUsageBreakdown {
|
|
361
|
+
totalTokens: number;
|
|
362
|
+
inputTokens: number;
|
|
363
|
+
cachedInputTokens: number;
|
|
364
|
+
outputTokens: number;
|
|
365
|
+
reasoningOutputTokens: number;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export const claudeCapabilities: AgentProviderCapabilities = {
|
|
369
|
+
sessions: {
|
|
370
|
+
list: true,
|
|
371
|
+
read: true,
|
|
372
|
+
resume: true,
|
|
373
|
+
importLocal: false,
|
|
374
|
+
},
|
|
375
|
+
turns: {
|
|
376
|
+
start: true,
|
|
377
|
+
streamInput: false,
|
|
378
|
+
steer: false,
|
|
379
|
+
interrupt: true,
|
|
380
|
+
compact: false,
|
|
381
|
+
},
|
|
382
|
+
branching: {
|
|
383
|
+
fork: false,
|
|
384
|
+
hardRollback: false,
|
|
385
|
+
resumeAt: false,
|
|
386
|
+
rewindFiles: false,
|
|
387
|
+
},
|
|
388
|
+
controls: {
|
|
389
|
+
planMode: true,
|
|
390
|
+
permissionRequests: false,
|
|
391
|
+
sandboxMode: true,
|
|
392
|
+
performanceMode: false,
|
|
393
|
+
goals: false,
|
|
394
|
+
},
|
|
395
|
+
management: {
|
|
396
|
+
models: true,
|
|
397
|
+
mcpStatus: true,
|
|
398
|
+
skills: false,
|
|
399
|
+
hooks: false,
|
|
400
|
+
hookTrust: false,
|
|
401
|
+
hostConfigFiles: false,
|
|
402
|
+
providerSettings: false,
|
|
403
|
+
},
|
|
404
|
+
usage: {
|
|
405
|
+
contextWindow: true,
|
|
406
|
+
tokenUsage: true,
|
|
407
|
+
costUsd: true,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const DEFAULT_CLAUDE_MODELS: AgentModel[] = [
|
|
412
|
+
{
|
|
413
|
+
id: 'sonnet',
|
|
414
|
+
model: 'sonnet',
|
|
415
|
+
displayName: 'Claude Sonnet',
|
|
416
|
+
description: 'Claude Code default Sonnet model alias.',
|
|
417
|
+
isDefault: true,
|
|
418
|
+
hidden: false,
|
|
419
|
+
supportedReasoningEfforts: [
|
|
420
|
+
{ reasoningEffort: 'low', description: 'Low effort' },
|
|
421
|
+
{ reasoningEffort: 'medium', description: 'Medium effort' },
|
|
422
|
+
{ reasoningEffort: 'high', description: 'High effort' },
|
|
423
|
+
{ reasoningEffort: 'xhigh', description: 'Extra high effort' },
|
|
424
|
+
],
|
|
425
|
+
defaultReasoningEffort: 'medium',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
id: 'sonnet-1m',
|
|
429
|
+
model: 'sonnet[1m]',
|
|
430
|
+
displayName: 'Claude Sonnet 1M',
|
|
431
|
+
description: 'Claude Code Sonnet with the 1M token context beta enabled.',
|
|
432
|
+
isDefault: false,
|
|
433
|
+
hidden: false,
|
|
434
|
+
supportedReasoningEfforts: [
|
|
435
|
+
{ reasoningEffort: 'low', description: 'Low effort' },
|
|
436
|
+
{ reasoningEffort: 'medium', description: 'Medium effort' },
|
|
437
|
+
{ reasoningEffort: 'high', description: 'High effort' },
|
|
438
|
+
{ reasoningEffort: 'xhigh', description: 'Extra high effort' },
|
|
439
|
+
],
|
|
440
|
+
defaultReasoningEffort: 'medium',
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
id: 'opus',
|
|
444
|
+
model: 'opus',
|
|
445
|
+
displayName: 'Claude Opus',
|
|
446
|
+
description: 'Claude Code Opus model alias.',
|
|
447
|
+
isDefault: false,
|
|
448
|
+
hidden: false,
|
|
449
|
+
supportedReasoningEfforts: [
|
|
450
|
+
{ reasoningEffort: 'low', description: 'Low effort' },
|
|
451
|
+
{ reasoningEffort: 'medium', description: 'Medium effort' },
|
|
452
|
+
{ reasoningEffort: 'high', description: 'High effort' },
|
|
453
|
+
{ reasoningEffort: 'xhigh', description: 'Extra high effort' },
|
|
454
|
+
],
|
|
455
|
+
defaultReasoningEffort: 'medium',
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
id: 'haiku',
|
|
459
|
+
model: 'haiku',
|
|
460
|
+
displayName: 'Claude Haiku',
|
|
461
|
+
description: 'Claude Code Haiku model alias.',
|
|
462
|
+
isDefault: false,
|
|
463
|
+
hidden: false,
|
|
464
|
+
supportedReasoningEfforts: [],
|
|
465
|
+
defaultReasoningEffort: null,
|
|
466
|
+
},
|
|
467
|
+
];
|
|
468
|
+
|
|
469
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
470
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function toIsoFromMs(value: number | null | undefined) {
|
|
474
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
return new Date(value).toISOString();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function isoFromUuidV7(value: string | null | undefined) {
|
|
481
|
+
const normalized = value?.replace(/-/g, '').trim();
|
|
482
|
+
if (!normalized || normalized.length !== 32 || normalized[12] !== '7') {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const timestampHex = normalized.slice(0, 12);
|
|
487
|
+
const timestamp = Number.parseInt(timestampHex, 16);
|
|
488
|
+
if (!Number.isFinite(timestamp)) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const date = new Date(timestamp);
|
|
493
|
+
if (Number.isNaN(date.getTime())) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
return date.toISOString();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function mapModelInfo(model: ModelInfo, index: number): AgentModel {
|
|
500
|
+
return {
|
|
501
|
+
id: model.value,
|
|
502
|
+
model: model.value,
|
|
503
|
+
displayName: model.displayName,
|
|
504
|
+
description: model.description,
|
|
505
|
+
isDefault: index === 0,
|
|
506
|
+
hidden: false,
|
|
507
|
+
supportedReasoningEfforts: (model.supportedEffortLevels ?? []).map((effort) => ({
|
|
508
|
+
reasoningEffort: effort === 'max' ? 'xhigh' : effort,
|
|
509
|
+
description: `${effort} effort`,
|
|
510
|
+
})),
|
|
511
|
+
defaultReasoningEffort: model.supportsEffort ? 'medium' : null,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function withClaudeCodeModelAliases(models: AgentModel[]) {
|
|
516
|
+
const output = [...models];
|
|
517
|
+
const defaultSonnet = DEFAULT_CLAUDE_MODELS[0]!;
|
|
518
|
+
const oneMillionSonnet = DEFAULT_CLAUDE_MODELS[1]!;
|
|
519
|
+
const hasSonnetAlias = output.some((model) => model.model === 'sonnet');
|
|
520
|
+
if (!hasSonnetAlias) {
|
|
521
|
+
output.unshift(defaultSonnet);
|
|
522
|
+
}
|
|
523
|
+
if (!output.some((model) => model.model === 'sonnet[1m]')) {
|
|
524
|
+
output.splice(1, 0, oneMillionSonnet);
|
|
525
|
+
}
|
|
526
|
+
return output.map((model, index) => ({
|
|
527
|
+
...model,
|
|
528
|
+
isDefault: index === 0,
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function normalizeClaudeModelForQuery(model: string | null | undefined) {
|
|
533
|
+
return model === 'sonnet[1m]' ? 'sonnet' : model;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function shouldEnableOneMillionContext(model: string | null | undefined) {
|
|
537
|
+
return model === 'sonnet[1m]';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function displayClaudeModel(
|
|
541
|
+
requestedModel: string | null | undefined,
|
|
542
|
+
runtimeModel: string | null | undefined,
|
|
543
|
+
) {
|
|
544
|
+
return shouldEnableOneMillionContext(requestedModel)
|
|
545
|
+
? requestedModel!
|
|
546
|
+
: runtimeModel ?? requestedModel ?? null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function permissionModeForInput(
|
|
550
|
+
input: Pick<StartAgentSessionInput, 'approvalMode'> & {
|
|
551
|
+
collaborationMode?: StartAgentTurnInput['collaborationMode'];
|
|
552
|
+
sandboxMode?: StartAgentTurnInput['sandboxMode'];
|
|
553
|
+
},
|
|
554
|
+
): { permissionMode: PermissionMode; allowDangerouslySkipPermissions?: boolean } {
|
|
555
|
+
if (input.collaborationMode === 'plan') {
|
|
556
|
+
return { permissionMode: 'plan' };
|
|
557
|
+
}
|
|
558
|
+
if (input.approvalMode === 'yolo' || input.sandboxMode === 'danger-full-access') {
|
|
559
|
+
return {
|
|
560
|
+
permissionMode: 'bypassPermissions',
|
|
561
|
+
allowDangerouslySkipPermissions: true,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
if (input.sandboxMode === 'workspace-write') {
|
|
565
|
+
return { permissionMode: 'acceptEdits' };
|
|
566
|
+
}
|
|
567
|
+
return { permissionMode: 'default' };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function sandboxSettingsForInput(
|
|
571
|
+
input: {
|
|
572
|
+
cwd?: string | null | undefined;
|
|
573
|
+
sandboxMode?: StartAgentTurnInput['sandboxMode'];
|
|
574
|
+
},
|
|
575
|
+
): SandboxSettings | undefined {
|
|
576
|
+
if (!input.sandboxMode || input.sandboxMode === 'danger-full-access') {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (input.sandboxMode === 'read-only') {
|
|
581
|
+
return {
|
|
582
|
+
enabled: true,
|
|
583
|
+
autoAllowBashIfSandboxed: true,
|
|
584
|
+
allowUnsandboxedCommands: false,
|
|
585
|
+
...(input.cwd
|
|
586
|
+
? {
|
|
587
|
+
filesystem: {
|
|
588
|
+
denyWrite: [input.cwd],
|
|
589
|
+
},
|
|
590
|
+
}
|
|
591
|
+
: {}),
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
enabled: true,
|
|
597
|
+
autoAllowBashIfSandboxed: true,
|
|
598
|
+
allowUnsandboxedCommands: false,
|
|
599
|
+
...(input.cwd
|
|
600
|
+
? {
|
|
601
|
+
filesystem: {
|
|
602
|
+
allowWrite: [input.cwd],
|
|
603
|
+
},
|
|
604
|
+
}
|
|
605
|
+
: {}),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function errorMessage(error: unknown) {
|
|
610
|
+
return error instanceof Error ? error.message : String(error);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function isHiddenInitSessionText(value: string | null | undefined) {
|
|
614
|
+
const normalized = value?.trim().toLowerCase();
|
|
615
|
+
return (
|
|
616
|
+
normalized === hiddenInitPrompt().toLowerCase() ||
|
|
617
|
+
normalized === 'initialize remote codex session'
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function sessionSummaryFromInfo(info: SDKSessionInfo): AgentSessionSummary {
|
|
622
|
+
const firstPrompt = info.firstPrompt && !isHiddenInitSessionText(info.firstPrompt)
|
|
623
|
+
? info.firstPrompt
|
|
624
|
+
: null;
|
|
625
|
+
const summary = info.summary && !isHiddenInitSessionText(info.summary)
|
|
626
|
+
? info.summary
|
|
627
|
+
: null;
|
|
628
|
+
return {
|
|
629
|
+
provider: 'claude',
|
|
630
|
+
providerSessionId: info.sessionId,
|
|
631
|
+
cwd: info.cwd ?? '',
|
|
632
|
+
title: info.customTitle ?? summary,
|
|
633
|
+
preview: firstPrompt ?? summary,
|
|
634
|
+
createdAt: toIsoFromMs(info.createdAt),
|
|
635
|
+
updatedAt: toIsoFromMs(info.lastModified),
|
|
636
|
+
status: 'idle',
|
|
637
|
+
rawSession: info,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function queryResultStatus(message: SDKMessage): AgentTurn['status'] | null {
|
|
642
|
+
if (message.type !== 'result') {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
return message.subtype === 'success' ? 'completed' : 'failed';
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function queryResultError(message: SDKMessage): string | null {
|
|
649
|
+
if (message.type !== 'result' || message.subtype === 'success') {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
return message.errors?.join('\n') || message.stop_reason || 'Claude turn failed.';
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function assistantMessagePayload(message: SDKMessage) {
|
|
656
|
+
return message.type === 'assistant' ? message.message : null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function messageIdFromPayload(message: unknown) {
|
|
660
|
+
return isRecord(message) && typeof message.id === 'string' && message.id
|
|
661
|
+
? message.id
|
|
662
|
+
: null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function messageUuid(message: SDKMessage | SessionMessage, fallback: string) {
|
|
666
|
+
return typeof message.uuid === 'string' && message.uuid ? message.uuid : fallback;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function finiteNumber(value: unknown) {
|
|
670
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
671
|
+
? value
|
|
672
|
+
: 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function nullableFiniteNumber(value: unknown) {
|
|
676
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
677
|
+
? value
|
|
678
|
+
: null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function addClaudeUsage(
|
|
682
|
+
left: ClaudeTokenUsageBreakdown,
|
|
683
|
+
right: ClaudeTokenUsageBreakdown,
|
|
684
|
+
): ClaudeTokenUsageBreakdown {
|
|
685
|
+
return {
|
|
686
|
+
totalTokens: left.totalTokens + right.totalTokens,
|
|
687
|
+
inputTokens: left.inputTokens + right.inputTokens,
|
|
688
|
+
cachedInputTokens: left.cachedInputTokens + right.cachedInputTokens,
|
|
689
|
+
outputTokens: left.outputTokens + right.outputTokens,
|
|
690
|
+
reasoningOutputTokens: left.reasoningOutputTokens + right.reasoningOutputTokens,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function normalizeClaudeUsage(value: unknown): ClaudeTokenUsageBreakdown | null {
|
|
695
|
+
if (!isRecord(value)) {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const baseInputTokens = finiteNumber(value.input_tokens ?? value.inputTokens);
|
|
700
|
+
const cacheCreationInputTokens = finiteNumber(
|
|
701
|
+
value.cache_creation_input_tokens ?? value.cacheCreationInputTokens,
|
|
702
|
+
);
|
|
703
|
+
const cacheReadInputTokens = finiteNumber(
|
|
704
|
+
value.cache_read_input_tokens ?? value.cacheReadInputTokens,
|
|
705
|
+
);
|
|
706
|
+
const outputTokens = finiteNumber(value.output_tokens ?? value.outputTokens);
|
|
707
|
+
const inputTokens = baseInputTokens + cacheCreationInputTokens + cacheReadInputTokens;
|
|
708
|
+
const totalTokens = inputTokens + outputTokens;
|
|
709
|
+
|
|
710
|
+
if (totalTokens <= 0) {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return {
|
|
715
|
+
totalTokens,
|
|
716
|
+
inputTokens,
|
|
717
|
+
cachedInputTokens: cacheReadInputTokens,
|
|
718
|
+
outputTokens,
|
|
719
|
+
reasoningOutputTokens: 0,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function usageFromAssistantMessage(message: SDKMessage) {
|
|
724
|
+
if (message.type !== 'assistant' || !isRecord(message.message)) {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
return normalizeClaudeUsage(message.message.usage);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function usageFromResultMessage(message: SDKMessage) {
|
|
731
|
+
return message.type === 'result' ? normalizeClaudeUsage(message.usage) : null;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function contextWindowFromResultMessage(message: SDKMessage) {
|
|
735
|
+
if (message.type !== 'result' || !isRecord(message.modelUsage)) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
for (const usage of Object.values(message.modelUsage)) {
|
|
739
|
+
if (!isRecord(usage)) {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const contextWindow = nullableFiniteNumber(usage.contextWindow);
|
|
743
|
+
if (contextWindow && contextWindow > 0) {
|
|
744
|
+
return contextWindow;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function claudeUsagePayload(
|
|
751
|
+
usage: ClaudeTokenUsageBreakdown,
|
|
752
|
+
modelContextWindow: number | null,
|
|
753
|
+
) {
|
|
754
|
+
return {
|
|
755
|
+
total: usage,
|
|
756
|
+
last: usage,
|
|
757
|
+
modelContextWindow,
|
|
758
|
+
cumulative: false,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function streamMessageId(event: unknown) {
|
|
763
|
+
if (!isRecord(event) || event.type !== 'message_start') {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
const message = event.message;
|
|
767
|
+
return messageIdFromPayload(message);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function addOrUpdateItem(state: ActiveClaudeTurn, item: AgentHistoryItem) {
|
|
771
|
+
if (!state.items.has(item.id)) {
|
|
772
|
+
state.itemOrder.push(item.id);
|
|
773
|
+
}
|
|
774
|
+
state.items.set(item.id, item);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function orderedItems(state: ActiveClaudeTurn) {
|
|
778
|
+
return state.itemOrder
|
|
779
|
+
.map((id) => state.items.get(id))
|
|
780
|
+
.filter((item): item is AgentHistoryItem => Boolean(item));
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function stringFromRecord(value: Record<string, unknown>, key: string) {
|
|
784
|
+
const raw = value[key];
|
|
785
|
+
return typeof raw === 'string' && raw.trim() ? raw : null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function normalizeClaudeQuestionOptions(value: unknown): AgentActionQuestion['options'] {
|
|
789
|
+
if (!Array.isArray(value)) {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
const options = value
|
|
793
|
+
.map((entry) => {
|
|
794
|
+
if (!isRecord(entry)) {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
const label = stringFromRecord(entry, 'label');
|
|
798
|
+
if (!label) {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
label,
|
|
803
|
+
description: stringFromRecord(entry, 'description') ?? label,
|
|
804
|
+
};
|
|
805
|
+
})
|
|
806
|
+
.filter((entry): entry is NonNullable<AgentActionQuestion['options']>[number] =>
|
|
807
|
+
Boolean(entry),
|
|
808
|
+
);
|
|
809
|
+
return options.length > 0 ? options : null;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function normalizeClaudeAskUserQuestions(input: unknown): AgentActionQuestion[] {
|
|
813
|
+
if (!isRecord(input) || !Array.isArray(input.questions)) {
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
816
|
+
return input.questions
|
|
817
|
+
.map((entry, index): AgentActionQuestion | null => {
|
|
818
|
+
if (!isRecord(entry)) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
const question = stringFromRecord(entry, 'question');
|
|
822
|
+
if (!question) {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
const header = stringFromRecord(entry, 'header') ?? `Question ${index + 1}`;
|
|
826
|
+
return {
|
|
827
|
+
id: `question-${index + 1}`,
|
|
828
|
+
header,
|
|
829
|
+
question,
|
|
830
|
+
multiSelect: entry.multiSelect === true,
|
|
831
|
+
isOther: true,
|
|
832
|
+
isSecret: false,
|
|
833
|
+
options: normalizeClaudeQuestionOptions(entry.options),
|
|
834
|
+
};
|
|
835
|
+
})
|
|
836
|
+
.filter((entry): entry is AgentActionQuestion => Boolean(entry));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function claudeAskUserToolUseFromAssistantMessage(message: unknown) {
|
|
840
|
+
if (!isRecord(message) || !Array.isArray(message.content)) {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
for (const [index, block] of message.content.entries()) {
|
|
844
|
+
if (!isRecord(block) || block.type !== 'tool_use') {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const name = stringFromRecord(block, 'name');
|
|
848
|
+
if (name !== 'AskUserQuestion') {
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const id = stringFromRecord(block, 'id') ?? `ask-user-question-${index}`;
|
|
852
|
+
return {
|
|
853
|
+
id,
|
|
854
|
+
input: block.input,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function mapClaudeAskUserQuestionRequest(request: AgentProviderRequest): AgentProviderRequestMapping | null {
|
|
861
|
+
if (request.method !== 'tool/AskUserQuestion') {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
if (!isRecord(request.params)) {
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
const providerSessionId = stringFromRecord(request.params, 'providerSessionId');
|
|
868
|
+
if (!providerSessionId) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
const questions = normalizeClaudeAskUserQuestions(request.params.input);
|
|
872
|
+
if (questions.length === 0) {
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
const requestId = String(request.id);
|
|
876
|
+
const turnId = stringFromRecord(request.params, 'providerTurnId');
|
|
877
|
+
const firstQuestion = questions[0] ?? null;
|
|
878
|
+
return {
|
|
879
|
+
providerRequestId: request.id,
|
|
880
|
+
providerSessionId,
|
|
881
|
+
autoApprovedResult: null,
|
|
882
|
+
pendingRequest: {
|
|
883
|
+
providerRequestId: request.id,
|
|
884
|
+
responseKind: 'askUserQuestion',
|
|
885
|
+
responsePayload: {
|
|
886
|
+
continueAsPrompt: true,
|
|
887
|
+
},
|
|
888
|
+
request: {
|
|
889
|
+
id: requestId,
|
|
890
|
+
kind: 'requestUserInput',
|
|
891
|
+
title: firstQuestion?.header ?? 'User input required',
|
|
892
|
+
description: firstQuestion?.question ?? null,
|
|
893
|
+
turnId,
|
|
894
|
+
itemId: stringFromRecord(request.params, 'toolUseId'),
|
|
895
|
+
createdAt: new Date().toISOString(),
|
|
896
|
+
questions,
|
|
897
|
+
},
|
|
898
|
+
},
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function buildClaudeProviderRequestResponse(
|
|
903
|
+
pending: AgentPendingProviderRequest,
|
|
904
|
+
input: AgentActionRequestResponseInput,
|
|
905
|
+
) {
|
|
906
|
+
if (pending.responseKind !== 'askUserQuestion') {
|
|
907
|
+
return input;
|
|
908
|
+
}
|
|
909
|
+
const answers = Object.fromEntries(
|
|
910
|
+
pending.request.questions.map((question) => [
|
|
911
|
+
question.question,
|
|
912
|
+
input.answers[question.id]?.answers.join(', ') ?? '',
|
|
913
|
+
]),
|
|
914
|
+
);
|
|
915
|
+
return {
|
|
916
|
+
questions: pending.request.questions.map((question) => ({
|
|
917
|
+
question: question.question,
|
|
918
|
+
header: question.header,
|
|
919
|
+
answer: input.answers[question.id]?.answers ?? [],
|
|
920
|
+
})),
|
|
921
|
+
answers,
|
|
922
|
+
annotations: {},
|
|
923
|
+
toolResult: {
|
|
924
|
+
questions: pending.request.questions.map((question) => ({
|
|
925
|
+
question: question.question,
|
|
926
|
+
header: question.header,
|
|
927
|
+
options: question.options ?? [],
|
|
928
|
+
multiSelect: question.multiSelect === true,
|
|
929
|
+
})),
|
|
930
|
+
answers,
|
|
931
|
+
annotations: {},
|
|
932
|
+
},
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function queryOptionsForRuntime(
|
|
937
|
+
input: {
|
|
938
|
+
home: string;
|
|
939
|
+
command: string | undefined;
|
|
940
|
+
clientApp: string;
|
|
941
|
+
cwd?: string | null | undefined;
|
|
942
|
+
model?: string | null | undefined;
|
|
943
|
+
reasoningEffort?: string | null | undefined;
|
|
944
|
+
resume?: string | null | undefined;
|
|
945
|
+
approvalMode: StartAgentSessionInput['approvalMode'];
|
|
946
|
+
collaborationMode?: StartAgentTurnInput['collaborationMode'] | undefined;
|
|
947
|
+
sandboxMode?: StartAgentTurnInput['sandboxMode'] | undefined;
|
|
948
|
+
includePartialMessages?: boolean | undefined;
|
|
949
|
+
tools?: ClaudeQueryOptions['tools'] | undefined;
|
|
950
|
+
maxTurns?: number | undefined;
|
|
951
|
+
},
|
|
952
|
+
): ClaudeQueryOptions {
|
|
953
|
+
const permission = permissionModeForInput(input);
|
|
954
|
+
const options: ClaudeQueryOptions = {
|
|
955
|
+
includeHookEvents: false,
|
|
956
|
+
permissionMode: permission.permissionMode,
|
|
957
|
+
thinking: {
|
|
958
|
+
type: 'adaptive',
|
|
959
|
+
display: 'summarized',
|
|
960
|
+
},
|
|
961
|
+
env: {
|
|
962
|
+
...process.env,
|
|
963
|
+
CLAUDE_CONFIG_DIR: input.home,
|
|
964
|
+
CLAUDE_HOME: input.home,
|
|
965
|
+
CLAUDE_AGENT_SDK_CLIENT_APP: input.clientApp,
|
|
966
|
+
},
|
|
967
|
+
};
|
|
968
|
+
if (input.cwd) {
|
|
969
|
+
options.cwd = input.cwd;
|
|
970
|
+
}
|
|
971
|
+
const sandbox = sandboxSettingsForInput(input);
|
|
972
|
+
if (sandbox) {
|
|
973
|
+
options.sandbox = sandbox;
|
|
974
|
+
}
|
|
975
|
+
if (input.model) {
|
|
976
|
+
const model = normalizeClaudeModelForQuery(input.model);
|
|
977
|
+
if (model) {
|
|
978
|
+
options.model = model;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (shouldEnableOneMillionContext(input.model)) {
|
|
982
|
+
options.betas = ['context-1m-2025-08-07'];
|
|
983
|
+
}
|
|
984
|
+
if (input.reasoningEffort) {
|
|
985
|
+
const effort = input.reasoningEffort === 'xhigh' ? 'max' : input.reasoningEffort;
|
|
986
|
+
if (['low', 'medium', 'high', 'xhigh', 'max'].includes(effort)) {
|
|
987
|
+
options.effort = effort as NonNullable<ClaudeQueryOptions['effort']>;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (input.resume) {
|
|
991
|
+
options.resume = input.resume;
|
|
992
|
+
}
|
|
993
|
+
if (input.includePartialMessages !== undefined) {
|
|
994
|
+
options.includePartialMessages = input.includePartialMessages;
|
|
995
|
+
}
|
|
996
|
+
if (input.maxTurns !== undefined) {
|
|
997
|
+
options.maxTurns = input.maxTurns;
|
|
998
|
+
}
|
|
999
|
+
if (input.tools !== undefined && (!Array.isArray(input.tools) || input.tools.length > 0)) {
|
|
1000
|
+
options.tools = input.tools;
|
|
1001
|
+
}
|
|
1002
|
+
if (permission.allowDangerouslySkipPermissions) {
|
|
1003
|
+
options.allowDangerouslySkipPermissions = true;
|
|
1004
|
+
}
|
|
1005
|
+
options.pathToClaudeCodeExecutable = input.command?.trim() || 'claude';
|
|
1006
|
+
return options;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
1010
|
+
readonly provider = 'claude' as const;
|
|
1011
|
+
readonly displayName = 'Claude Code';
|
|
1012
|
+
readonly description = 'Local Claude Code Agent SDK runtime.';
|
|
1013
|
+
readonly capabilities = claudeCapabilities;
|
|
1014
|
+
readonly installation: AgentBackendInstallationDto = {
|
|
1015
|
+
packageName: '@anthropic-ai/claude-agent-sdk',
|
|
1016
|
+
installed: false,
|
|
1017
|
+
installedVersion: null,
|
|
1018
|
+
latestVersion: null,
|
|
1019
|
+
installCommand: 'npm install -g @anthropic-ai/claude-code @anthropic-ai/claude-agent-sdk',
|
|
1020
|
+
updateCommand: 'npm install -g @anthropic-ai/claude-code@latest @anthropic-ai/claude-agent-sdk@latest',
|
|
1021
|
+
busy: false,
|
|
1022
|
+
lastError: null,
|
|
1023
|
+
};
|
|
1024
|
+
readonly managementSchema: AgentRuntimeManagementSchema = {
|
|
1025
|
+
hostConfigFiles: [],
|
|
1026
|
+
toolboxItems: [
|
|
1027
|
+
{ action: 'mcp', command: '/mcp', label: 'MCP', panel: 'mcp' },
|
|
1028
|
+
],
|
|
1029
|
+
hookCommandTemplates: [],
|
|
1030
|
+
providerConfigFormat: 'none',
|
|
1031
|
+
mcpConfigFormat: 'none',
|
|
1032
|
+
configArchives: false,
|
|
1033
|
+
buildRestart: false,
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
private status: AgentRuntimeStatus = {
|
|
1037
|
+
state: 'stopped',
|
|
1038
|
+
transport: 'sdk',
|
|
1039
|
+
lastStartedAt: null,
|
|
1040
|
+
lastError: null,
|
|
1041
|
+
restartCount: 0,
|
|
1042
|
+
};
|
|
1043
|
+
private queryFactory: ClaudeQueryFunction;
|
|
1044
|
+
private listSessionsFn: ClaudeListSessionsFunction;
|
|
1045
|
+
private getSessionMessagesFn: ClaudeGetSessionMessagesFunction;
|
|
1046
|
+
private getSessionInfoFn: ClaudeGetSessionInfoFunction;
|
|
1047
|
+
private readonly activeTurns = new Map<string, ActiveClaudeTurn>();
|
|
1048
|
+
private readonly knownSessionIds = new Set<string>();
|
|
1049
|
+
private readonly sessionCwds = new Map<string, string>();
|
|
1050
|
+
private readonly sessionModels = new Map<string, string | null>();
|
|
1051
|
+
private readonly sessionApprovalModes = new Map<string, StartAgentSessionInput['approvalMode']>();
|
|
1052
|
+
private readonly liveUserPrompts = new Map<string, Map<string, string>>();
|
|
1053
|
+
private readonly clientApp: string;
|
|
1054
|
+
private sdkLoadError: string | null = null;
|
|
1055
|
+
|
|
1056
|
+
constructor(private readonly options: ClaudeRuntimeAdapterOptions) {
|
|
1057
|
+
super();
|
|
1058
|
+
const sdk = options.sdk;
|
|
1059
|
+
this.queryFactory = options.query ?? sdk?.query ?? this.unavailableQueryFactory.bind(this);
|
|
1060
|
+
this.listSessionsFn = options.listSessions ?? sdk?.listSessions ?? this.unavailableListSessions.bind(this);
|
|
1061
|
+
this.getSessionMessagesFn = options.getSessionMessages ?? sdk?.getSessionMessages ?? this.unavailableGetSessionMessages.bind(this);
|
|
1062
|
+
this.getSessionInfoFn = options.getSessionInfo ?? sdk?.getSessionInfo ?? this.unavailableGetSessionInfo.bind(this);
|
|
1063
|
+
this.clientApp = [
|
|
1064
|
+
options.clientInfo?.name ?? 'remote-codex-supervisor',
|
|
1065
|
+
options.clientInfo?.version,
|
|
1066
|
+
].filter(Boolean).join('/');
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
getStatus(): AgentRuntimeStatus {
|
|
1070
|
+
return { ...this.status };
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async start() {
|
|
1074
|
+
await fs.mkdir(this.options.home, { recursive: true });
|
|
1075
|
+
if (!this.options.query && !this.options.sdk) {
|
|
1076
|
+
try {
|
|
1077
|
+
const sdk = await this.loadSdk();
|
|
1078
|
+
this.queryFactory = sdk.query;
|
|
1079
|
+
this.listSessionsFn = sdk.listSessions;
|
|
1080
|
+
this.getSessionMessagesFn = sdk.getSessionMessages;
|
|
1081
|
+
this.getSessionInfoFn = sdk.getSessionInfo;
|
|
1082
|
+
this.sdkLoadError = null;
|
|
1083
|
+
this.installation.installed = true;
|
|
1084
|
+
this.installation.lastError = null;
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
this.sdkLoadError = errorMessage(error);
|
|
1087
|
+
this.installation.installed = false;
|
|
1088
|
+
this.installation.lastError = this.sdkLoadError;
|
|
1089
|
+
this.status = {
|
|
1090
|
+
...this.status,
|
|
1091
|
+
state: 'stopped',
|
|
1092
|
+
lastError: `Claude Code SDK is not installed or could not be loaded. ${this.sdkLoadError}`,
|
|
1093
|
+
};
|
|
1094
|
+
this.emit('status', this.getStatus());
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
this.status = {
|
|
1099
|
+
...this.status,
|
|
1100
|
+
state: 'ready',
|
|
1101
|
+
lastStartedAt: new Date().toISOString(),
|
|
1102
|
+
lastError: null,
|
|
1103
|
+
restartCount: this.status.state === 'stopped' ? this.status.restartCount : this.status.restartCount + 1,
|
|
1104
|
+
};
|
|
1105
|
+
this.emit('status', this.getStatus());
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async stop() {
|
|
1109
|
+
for (const state of this.activeTurns.values()) {
|
|
1110
|
+
state.interrupted = true;
|
|
1111
|
+
state.query.close();
|
|
1112
|
+
}
|
|
1113
|
+
this.activeTurns.clear();
|
|
1114
|
+
this.status = {
|
|
1115
|
+
...this.status,
|
|
1116
|
+
state: 'stopped',
|
|
1117
|
+
lastError: null,
|
|
1118
|
+
};
|
|
1119
|
+
this.emit('status', this.getStatus());
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async listModels(): Promise<AgentModel[]> {
|
|
1123
|
+
const active = [...this.activeTurns.values()][0];
|
|
1124
|
+
if (!active) {
|
|
1125
|
+
return DEFAULT_CLAUDE_MODELS;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
try {
|
|
1129
|
+
const models = await active.query.supportedModels();
|
|
1130
|
+
return withClaudeCodeModelAliases(models.map(mapModelInfo));
|
|
1131
|
+
} catch {
|
|
1132
|
+
return DEFAULT_CLAUDE_MODELS;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
async listSessions(): Promise<AgentSessionSummary[]> {
|
|
1137
|
+
const sessions = await this.withClaudeConfigEnv(() => this.listSessionsFn({} as ListSessionsOptions));
|
|
1138
|
+
return sessions.map((session) => {
|
|
1139
|
+
this.knownSessionIds.add(session.sessionId);
|
|
1140
|
+
return sessionSummaryFromInfo(session);
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async listLoadedSessions(): Promise<string[]> {
|
|
1145
|
+
for (const state of this.activeTurns.values()) {
|
|
1146
|
+
this.knownSessionIds.add(state.providerSessionId);
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
const sessions = await this.listSessions();
|
|
1150
|
+
for (const session of sessions) {
|
|
1151
|
+
this.knownSessionIds.add(session.providerSessionId);
|
|
1152
|
+
}
|
|
1153
|
+
} catch {
|
|
1154
|
+
// Keep in-memory known sessions if Claude's local history cannot be read.
|
|
1155
|
+
}
|
|
1156
|
+
return [...this.knownSessionIds];
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async readSession(
|
|
1160
|
+
providerSessionId: string,
|
|
1161
|
+
options: ReadAgentSessionOptions = {},
|
|
1162
|
+
): Promise<AgentSessionDetail> {
|
|
1163
|
+
const [info, messages] = await this.withClaudeConfigEnv(async () => Promise.all([
|
|
1164
|
+
this.getSessionInfoFn(providerSessionId, {} as GetSessionInfoOptions),
|
|
1165
|
+
this.getSessionMessagesFn(providerSessionId, {
|
|
1166
|
+
includeSystemMessages: true,
|
|
1167
|
+
} as GetSessionMessagesOptions),
|
|
1168
|
+
]));
|
|
1169
|
+
this.knownSessionIds.add(providerSessionId);
|
|
1170
|
+
const summary = info
|
|
1171
|
+
? sessionSummaryFromInfo(info)
|
|
1172
|
+
: {
|
|
1173
|
+
provider: 'claude' as const,
|
|
1174
|
+
providerSessionId,
|
|
1175
|
+
cwd: this.sessionCwds.get(providerSessionId) ?? '',
|
|
1176
|
+
title: null,
|
|
1177
|
+
preview: null,
|
|
1178
|
+
createdAt: null,
|
|
1179
|
+
updatedAt: null,
|
|
1180
|
+
status: 'idle' as const,
|
|
1181
|
+
rawSession: null,
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
const cwd = summary.cwd || this.sessionCwds.get(providerSessionId) || '';
|
|
1185
|
+
const historyAssetContext = {
|
|
1186
|
+
workspacePath: options.workspacePath || cwd,
|
|
1187
|
+
...(options.localThreadId ? { localThreadId: options.localThreadId } : {}),
|
|
1188
|
+
};
|
|
1189
|
+
const turns = await this.sessionMessagesToTurns(messages, historyAssetContext);
|
|
1190
|
+
const activeTurn = [...this.activeTurns.values()].find(
|
|
1191
|
+
(turn) => turn.providerSessionId === providerSessionId,
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
return {
|
|
1195
|
+
...summary,
|
|
1196
|
+
cwd,
|
|
1197
|
+
turns: this.reconcileActiveTranscriptTurn(providerSessionId, turns, activeTurn),
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async startSession(input: StartAgentSessionInput): Promise<StartAgentSessionResult> {
|
|
1202
|
+
await fs.mkdir(this.options.home, { recursive: true });
|
|
1203
|
+
const query = this.queryFactory({
|
|
1204
|
+
prompt: hiddenInitPrompt(),
|
|
1205
|
+
options: queryOptionsForRuntime({
|
|
1206
|
+
home: this.options.home,
|
|
1207
|
+
command: this.options.command,
|
|
1208
|
+
clientApp: this.clientApp,
|
|
1209
|
+
cwd: input.cwd,
|
|
1210
|
+
model: input.model,
|
|
1211
|
+
approvalMode: input.approvalMode,
|
|
1212
|
+
sandboxMode: input.sandboxMode,
|
|
1213
|
+
includePartialMessages: false,
|
|
1214
|
+
tools: [],
|
|
1215
|
+
maxTurns: 1,
|
|
1216
|
+
}),
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
let providerSessionId: string | null = null;
|
|
1220
|
+
let model: string | null = input.model;
|
|
1221
|
+
const rawMessages: SDKMessage[] = [];
|
|
1222
|
+
try {
|
|
1223
|
+
for await (const message of query) {
|
|
1224
|
+
rawMessages.push(message);
|
|
1225
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
1226
|
+
const sessionId = message.session_id;
|
|
1227
|
+
if (sessionId) {
|
|
1228
|
+
providerSessionId = sessionId;
|
|
1229
|
+
model = displayClaudeModel(input.model, message.model ?? model);
|
|
1230
|
+
this.sessionCwds.set(sessionId, message.cwd ?? input.cwd);
|
|
1231
|
+
}
|
|
1232
|
+
} else if ('session_id' in message && typeof message.session_id === 'string') {
|
|
1233
|
+
providerSessionId ??= message.session_id;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
this.markFailed(error);
|
|
1238
|
+
throw new AgentRuntimeError(errorMessage(error), 'claude', 'request_failed', undefined, error);
|
|
1239
|
+
} finally {
|
|
1240
|
+
query.close();
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (!providerSessionId) {
|
|
1244
|
+
throw new AgentRuntimeError(
|
|
1245
|
+
'Claude did not return a session id during initialization.',
|
|
1246
|
+
'claude',
|
|
1247
|
+
'invalid_response',
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
this.sessionCwds.set(providerSessionId, input.cwd);
|
|
1252
|
+
this.knownSessionIds.add(providerSessionId);
|
|
1253
|
+
this.sessionModels.set(providerSessionId, displayClaudeModel(input.model, model));
|
|
1254
|
+
this.sessionApprovalModes.set(providerSessionId, input.approvalMode);
|
|
1255
|
+
const session: AgentSessionDetail = {
|
|
1256
|
+
provider: 'claude',
|
|
1257
|
+
providerSessionId,
|
|
1258
|
+
cwd: input.cwd,
|
|
1259
|
+
title: null,
|
|
1260
|
+
preview: null,
|
|
1261
|
+
createdAt: new Date().toISOString(),
|
|
1262
|
+
updatedAt: new Date().toISOString(),
|
|
1263
|
+
status: 'idle',
|
|
1264
|
+
turns: [],
|
|
1265
|
+
rawSession: rawMessages,
|
|
1266
|
+
};
|
|
1267
|
+
return {
|
|
1268
|
+
provider: 'claude',
|
|
1269
|
+
providerSessionId,
|
|
1270
|
+
model: displayClaudeModel(input.model, model),
|
|
1271
|
+
reasoningEffort: null,
|
|
1272
|
+
sandboxMode: input.sandboxMode ?? null,
|
|
1273
|
+
session,
|
|
1274
|
+
rawSession: rawMessages,
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async resumeSession(input: ResumeAgentSessionInput): Promise<StartAgentSessionResult> {
|
|
1279
|
+
const session = await this.readSession(input.providerSessionId);
|
|
1280
|
+
this.knownSessionIds.add(input.providerSessionId);
|
|
1281
|
+
if (input.model !== undefined) {
|
|
1282
|
+
this.sessionModels.set(input.providerSessionId, displayClaudeModel(input.model, input.model));
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
provider: 'claude',
|
|
1286
|
+
providerSessionId: input.providerSessionId,
|
|
1287
|
+
model: displayClaudeModel(
|
|
1288
|
+
input.model,
|
|
1289
|
+
input.model ?? this.sessionModels.get(input.providerSessionId) ?? null,
|
|
1290
|
+
),
|
|
1291
|
+
reasoningEffort: null,
|
|
1292
|
+
sandboxMode: input.sandboxMode ?? null,
|
|
1293
|
+
session,
|
|
1294
|
+
rawSession: session.rawSession,
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async startTurn(input: StartAgentTurnInput): Promise<AgentTurn> {
|
|
1299
|
+
const providerTurnId = input.displayTurnId ?? randomUUID();
|
|
1300
|
+
const runtimeTurnId = providerTurnId === input.displayTurnId ? randomUUID() : providerTurnId;
|
|
1301
|
+
const startedAt = new Date().toISOString();
|
|
1302
|
+
const cwd = input.workspacePath ?? this.sessionCwds.get(input.providerSessionId) ?? undefined;
|
|
1303
|
+
const approvalMode = this.sessionApprovalModes.get(input.providerSessionId) ?? 'guarded';
|
|
1304
|
+
const queryPrompt = await promptWithImageBlocks(input.prompt, cwd);
|
|
1305
|
+
const query = this.queryFactory({
|
|
1306
|
+
prompt: queryPrompt,
|
|
1307
|
+
options: queryOptionsForRuntime({
|
|
1308
|
+
home: this.options.home,
|
|
1309
|
+
command: this.options.command,
|
|
1310
|
+
clientApp: this.clientApp,
|
|
1311
|
+
cwd,
|
|
1312
|
+
model: input.model ?? this.sessionModels.get(input.providerSessionId) ?? undefined,
|
|
1313
|
+
reasoningEffort: input.reasoningEffort,
|
|
1314
|
+
resume: input.providerSessionId,
|
|
1315
|
+
approvalMode,
|
|
1316
|
+
collaborationMode: input.collaborationMode,
|
|
1317
|
+
sandboxMode: input.sandboxMode,
|
|
1318
|
+
includePartialMessages: true,
|
|
1319
|
+
tools: { type: 'preset', preset: 'claude_code' },
|
|
1320
|
+
}),
|
|
1321
|
+
});
|
|
1322
|
+
const userItem = userMessageToHistoryItem(`${providerTurnId}:user`, {
|
|
1323
|
+
content: input.prompt,
|
|
1324
|
+
});
|
|
1325
|
+
const initialItems = input.hidden ? [] : [userItem];
|
|
1326
|
+
const state: ActiveClaudeTurn = {
|
|
1327
|
+
providerSessionId: input.providerSessionId,
|
|
1328
|
+
providerTurnId,
|
|
1329
|
+
startedAt,
|
|
1330
|
+
query,
|
|
1331
|
+
items: new Map(initialItems.map((item) => [item.id, item])),
|
|
1332
|
+
itemOrder: initialItems.map((item) => item.id),
|
|
1333
|
+
emittedItems: new Set(),
|
|
1334
|
+
currentStreamMessageId: null,
|
|
1335
|
+
interrupted: false,
|
|
1336
|
+
completed: false,
|
|
1337
|
+
suppressedToolUseIds: new Set(),
|
|
1338
|
+
assistantUsage: null,
|
|
1339
|
+
resultUsage: null,
|
|
1340
|
+
modelContextWindow: null,
|
|
1341
|
+
};
|
|
1342
|
+
this.knownSessionIds.add(input.providerSessionId);
|
|
1343
|
+
let sessionPrompts = this.liveUserPrompts.get(input.providerSessionId);
|
|
1344
|
+
if (!sessionPrompts) {
|
|
1345
|
+
sessionPrompts = new Map();
|
|
1346
|
+
this.liveUserPrompts.set(input.providerSessionId, sessionPrompts);
|
|
1347
|
+
}
|
|
1348
|
+
sessionPrompts.set(providerTurnId, input.prompt);
|
|
1349
|
+
this.activeTurns.set(providerTurnId, state);
|
|
1350
|
+
if (runtimeTurnId !== providerTurnId) {
|
|
1351
|
+
this.activeTurns.set(runtimeTurnId, state);
|
|
1352
|
+
}
|
|
1353
|
+
this.emitRuntimeEvent({
|
|
1354
|
+
type: 'turn.started',
|
|
1355
|
+
provider: 'claude',
|
|
1356
|
+
providerSessionId: input.providerSessionId,
|
|
1357
|
+
turn: buildAgentTurn({
|
|
1358
|
+
providerTurnId,
|
|
1359
|
+
startedAt,
|
|
1360
|
+
status: 'inProgress',
|
|
1361
|
+
items: initialItems,
|
|
1362
|
+
}),
|
|
1363
|
+
});
|
|
1364
|
+
void this.consumeQuery(state);
|
|
1365
|
+
return buildAgentTurn({
|
|
1366
|
+
providerTurnId,
|
|
1367
|
+
startedAt,
|
|
1368
|
+
status: 'inProgress',
|
|
1369
|
+
items: initialItems,
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
async interruptTurn(input: InterruptAgentTurnInput): Promise<AgentTurn | null> {
|
|
1374
|
+
const state =
|
|
1375
|
+
this.activeTurns.get(input.providerTurnId) ??
|
|
1376
|
+
[...this.activeTurns.values()].find(
|
|
1377
|
+
(entry) => entry.providerSessionId === input.providerSessionId,
|
|
1378
|
+
) ??
|
|
1379
|
+
null;
|
|
1380
|
+
if (!state) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
state.interrupted = true;
|
|
1385
|
+
try {
|
|
1386
|
+
await state.query.interrupt();
|
|
1387
|
+
} catch {
|
|
1388
|
+
// Some SDK query modes cannot interrupt; close still terminates the child process.
|
|
1389
|
+
}
|
|
1390
|
+
state.query.close();
|
|
1391
|
+
state.completed = true;
|
|
1392
|
+
this.deleteActiveTurn(state);
|
|
1393
|
+
this.knownSessionIds.add(state.providerSessionId);
|
|
1394
|
+
return buildAgentTurn({
|
|
1395
|
+
providerTurnId: state.providerTurnId,
|
|
1396
|
+
startedAt: state.startedAt,
|
|
1397
|
+
status: 'interrupted',
|
|
1398
|
+
items: orderedItems(state),
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
private deleteActiveTurn(state: ActiveClaudeTurn) {
|
|
1403
|
+
for (const [turnId, active] of this.activeTurns.entries()) {
|
|
1404
|
+
if (active === state || turnId === state.providerTurnId) {
|
|
1405
|
+
this.activeTurns.delete(turnId);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
const sessionPrompts = this.liveUserPrompts.get(state.providerSessionId);
|
|
1409
|
+
sessionPrompts?.delete(state.providerTurnId);
|
|
1410
|
+
if (sessionPrompts?.size === 0) {
|
|
1411
|
+
this.liveUserPrompts.delete(state.providerSessionId);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
private reconcileActiveTranscriptTurn(
|
|
1416
|
+
providerSessionId: string,
|
|
1417
|
+
turns: AgentTurn[],
|
|
1418
|
+
activeTurn: ActiveClaudeTurn | undefined,
|
|
1419
|
+
) {
|
|
1420
|
+
if (!activeTurn || turns.length === 0) {
|
|
1421
|
+
return turns;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const prompt = this.liveUserPrompts
|
|
1425
|
+
.get(providerSessionId)
|
|
1426
|
+
?.get(activeTurn.providerTurnId);
|
|
1427
|
+
if (!prompt) {
|
|
1428
|
+
return turns;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const activeUserItem = userMessageHistoryItem(`${activeTurn.providerTurnId}:user`, prompt);
|
|
1432
|
+
const activeItems = [...activeTurn.itemOrder]
|
|
1433
|
+
.map((itemId) => activeTurn.items.get(itemId))
|
|
1434
|
+
.filter((item): item is AgentHistoryItem => Boolean(item));
|
|
1435
|
+
const transcriptTurnIndex = turns.findLastIndex(
|
|
1436
|
+
(turn) =>
|
|
1437
|
+
turn.status === 'completed' &&
|
|
1438
|
+
turn.items.some((item) => item.kind === 'userMessage') &&
|
|
1439
|
+
!turn.items.some((item) => item.kind === 'agentMessage'),
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
if (transcriptTurnIndex < 0) {
|
|
1443
|
+
return turns;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
return turns.map((turn, index) => {
|
|
1447
|
+
if (index !== transcriptTurnIndex) {
|
|
1448
|
+
return turn;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
return buildAgentTurn({
|
|
1452
|
+
providerTurnId: activeTurn.providerTurnId,
|
|
1453
|
+
startedAt: activeTurn.startedAt,
|
|
1454
|
+
status: 'inProgress',
|
|
1455
|
+
items: mergeActiveTranscriptItems(
|
|
1456
|
+
[activeUserItem, ...turn.items.filter((item) => item.kind !== 'userMessage')],
|
|
1457
|
+
activeItems,
|
|
1458
|
+
),
|
|
1459
|
+
});
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
async listMcpServers(): Promise<AgentMcpServer[]> {
|
|
1464
|
+
const active = [...this.activeTurns.values()][0];
|
|
1465
|
+
if (!active) {
|
|
1466
|
+
return [];
|
|
1467
|
+
}
|
|
1468
|
+
const servers = await active.query.mcpServerStatus();
|
|
1469
|
+
return servers.map((server) => this.mapMcpServer(server));
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
mapProviderRequest(
|
|
1473
|
+
request: AgentProviderRequest,
|
|
1474
|
+
_options: { approvalMode: 'yolo' | 'guarded' },
|
|
1475
|
+
): AgentProviderRequestMapping | null {
|
|
1476
|
+
return mapClaudeAskUserQuestionRequest(request);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
buildProviderRequestResponse(
|
|
1480
|
+
pending: AgentPendingProviderRequest,
|
|
1481
|
+
input: AgentActionRequestResponseInput,
|
|
1482
|
+
) {
|
|
1483
|
+
return buildClaudeProviderRequestResponse(pending, input);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
respondToProviderRequest(_id: string | number, _result: unknown) {
|
|
1487
|
+
// Claude Code's built-in AskUserQuestion arrives as transcripted tool use in
|
|
1488
|
+
// this SDK mode. The supervisor records the user's answer locally so the
|
|
1489
|
+
// interaction matches other backends, but there is no live JSON-RPC request
|
|
1490
|
+
// to resolve back into the Claude process.
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
private async consumeQuery(state: ActiveClaudeTurn) {
|
|
1494
|
+
const rawMessages: SDKMessage[] = [];
|
|
1495
|
+
try {
|
|
1496
|
+
for await (const message of state.query) {
|
|
1497
|
+
rawMessages.push(message);
|
|
1498
|
+
if (state.completed) {
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
this.consumeMessage(state, message);
|
|
1502
|
+
const status = queryResultStatus(message);
|
|
1503
|
+
if (status) {
|
|
1504
|
+
state.completed = true;
|
|
1505
|
+
this.deleteActiveTurn(state);
|
|
1506
|
+
this.emitUsage(state);
|
|
1507
|
+
this.emitRuntimeEvent({
|
|
1508
|
+
type: 'turn.completed',
|
|
1509
|
+
provider: 'claude',
|
|
1510
|
+
providerSessionId: state.providerSessionId,
|
|
1511
|
+
turn: buildAgentTurn({
|
|
1512
|
+
providerTurnId: state.providerTurnId,
|
|
1513
|
+
startedAt: state.startedAt,
|
|
1514
|
+
status: state.interrupted ? 'interrupted' : status,
|
|
1515
|
+
error: queryResultError(message),
|
|
1516
|
+
items: orderedItems(state),
|
|
1517
|
+
rawTurn: rawMessages,
|
|
1518
|
+
}),
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (!state.completed) {
|
|
1524
|
+
state.completed = true;
|
|
1525
|
+
this.deleteActiveTurn(state);
|
|
1526
|
+
this.emitUsage(state);
|
|
1527
|
+
this.emitRuntimeEvent({
|
|
1528
|
+
type: 'turn.completed',
|
|
1529
|
+
provider: 'claude',
|
|
1530
|
+
providerSessionId: state.providerSessionId,
|
|
1531
|
+
turn: buildAgentTurn({
|
|
1532
|
+
providerTurnId: state.providerTurnId,
|
|
1533
|
+
startedAt: state.startedAt,
|
|
1534
|
+
status: state.interrupted ? 'interrupted' : 'completed',
|
|
1535
|
+
items: orderedItems(state),
|
|
1536
|
+
rawTurn: rawMessages,
|
|
1537
|
+
}),
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
this.deleteActiveTurn(state);
|
|
1542
|
+
if (state.interrupted) {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
this.emitRuntimeEvent({
|
|
1546
|
+
type: 'turn.failed',
|
|
1547
|
+
provider: 'claude',
|
|
1548
|
+
providerSessionId: state.providerSessionId,
|
|
1549
|
+
providerTurnId: state.providerTurnId,
|
|
1550
|
+
error: errorMessage(error),
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
private consumeMessage(state: ActiveClaudeTurn, message: SDKMessage) {
|
|
1556
|
+
this.captureUsage(state, message);
|
|
1557
|
+
|
|
1558
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
1559
|
+
if (message.session_id) {
|
|
1560
|
+
this.sessionCwds.set(message.session_id, message.cwd ?? '');
|
|
1561
|
+
this.sessionModels.set(message.session_id, message.model ?? null);
|
|
1562
|
+
}
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
if (message.type === 'stream_event') {
|
|
1567
|
+
const nextStreamMessageId = streamMessageId(message.event);
|
|
1568
|
+
if (nextStreamMessageId) {
|
|
1569
|
+
state.currentStreamMessageId = nextStreamMessageId;
|
|
1570
|
+
}
|
|
1571
|
+
const activeMessageId = state.currentStreamMessageId ?? messageUuid(message, state.providerTurnId);
|
|
1572
|
+
const toolItem = toolUseFromPartialStart({
|
|
1573
|
+
messageId: activeMessageId,
|
|
1574
|
+
event: message.event,
|
|
1575
|
+
});
|
|
1576
|
+
if (toolItem) {
|
|
1577
|
+
addOrUpdateItem(state, toolItem);
|
|
1578
|
+
this.emitItem(state, toolItem, 'item.started');
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
const reasoningItem = partialReasoningDelta({
|
|
1583
|
+
messageId: activeMessageId,
|
|
1584
|
+
event: message.event,
|
|
1585
|
+
});
|
|
1586
|
+
if (reasoningItem) {
|
|
1587
|
+
const existing = state.items.get(reasoningItem.id);
|
|
1588
|
+
const nextItem: AgentHistoryItem = existing?.kind === 'reasoning'
|
|
1589
|
+
? {
|
|
1590
|
+
...existing,
|
|
1591
|
+
text: `${existing.text}${reasoningItem.text}`,
|
|
1592
|
+
status: reasoningItem.status ?? existing.status ?? null,
|
|
1593
|
+
}
|
|
1594
|
+
: reasoningItem;
|
|
1595
|
+
addOrUpdateItem(state, nextItem);
|
|
1596
|
+
this.emitItem(state, nextItem, existing ? 'item.completed' : 'item.started', {
|
|
1597
|
+
force: Boolean(existing),
|
|
1598
|
+
});
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
const delta = partialTextDelta({
|
|
1603
|
+
messageId: activeMessageId,
|
|
1604
|
+
event: message.event,
|
|
1605
|
+
});
|
|
1606
|
+
if (delta) {
|
|
1607
|
+
const existing = state.items.get(delta.itemId);
|
|
1608
|
+
const nextItem: AgentHistoryItem = existing?.kind === 'agentMessage'
|
|
1609
|
+
? markTransientAgentHistoryItem({
|
|
1610
|
+
...existing,
|
|
1611
|
+
text: `${existing.text}${delta.delta}`,
|
|
1612
|
+
})
|
|
1613
|
+
: markTransientAgentHistoryItem({
|
|
1614
|
+
id: delta.itemId,
|
|
1615
|
+
kind: 'agentMessage',
|
|
1616
|
+
text: delta.delta,
|
|
1617
|
+
});
|
|
1618
|
+
addOrUpdateItem(state, nextItem);
|
|
1619
|
+
this.emitRuntimeEvent({
|
|
1620
|
+
type: 'output.delta',
|
|
1621
|
+
provider: 'claude',
|
|
1622
|
+
providerSessionId: state.providerSessionId,
|
|
1623
|
+
providerTurnId: state.providerTurnId,
|
|
1624
|
+
itemId: delta.itemId,
|
|
1625
|
+
delta: delta.delta,
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if (message.type === 'user') {
|
|
1632
|
+
const rawToolResults = toolResultBlocks(message.message);
|
|
1633
|
+
const toolResults = rawToolResults.filter(
|
|
1634
|
+
(toolResult) => !state.suppressedToolUseIds.has(toolResult.toolUseId),
|
|
1635
|
+
);
|
|
1636
|
+
if (toolResults.length > 0) {
|
|
1637
|
+
for (const toolResult of toolResults) {
|
|
1638
|
+
const item = resultForToolUse({
|
|
1639
|
+
toolUseId: toolResult.toolUseId,
|
|
1640
|
+
result: message.tool_use_result ?? toolResult.result,
|
|
1641
|
+
previous: state.items.get(toolResult.toolUseId) ?? null,
|
|
1642
|
+
});
|
|
1643
|
+
addOrUpdateItem(state, item);
|
|
1644
|
+
this.emitItem(state, item, 'item.completed');
|
|
1645
|
+
}
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (rawToolResults.length > 0) {
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (message.type === 'assistant') {
|
|
1654
|
+
const payload = assistantMessagePayload(message);
|
|
1655
|
+
const assistantMessageId = messageIdFromPayload(payload) ?? messageUuid(message, state.providerTurnId);
|
|
1656
|
+
const askUserQuestion = claudeAskUserToolUseFromAssistantMessage(payload);
|
|
1657
|
+
if (askUserQuestion) {
|
|
1658
|
+
state.suppressedToolUseIds.add(askUserQuestion.id);
|
|
1659
|
+
this.emit('provider-request', {
|
|
1660
|
+
provider: 'claude',
|
|
1661
|
+
id: askUserQuestion.id,
|
|
1662
|
+
method: 'tool/AskUserQuestion',
|
|
1663
|
+
params: {
|
|
1664
|
+
providerSessionId: state.providerSessionId,
|
|
1665
|
+
providerTurnId: state.providerTurnId,
|
|
1666
|
+
toolUseId: askUserQuestion.id,
|
|
1667
|
+
input: askUserQuestion.input,
|
|
1668
|
+
},
|
|
1669
|
+
rawRequest: message,
|
|
1670
|
+
} satisfies AgentProviderRequest);
|
|
1671
|
+
}
|
|
1672
|
+
for (const item of assistantMessageToHistoryItems({
|
|
1673
|
+
messageId: assistantMessageId,
|
|
1674
|
+
message: payload,
|
|
1675
|
+
})) {
|
|
1676
|
+
const existing = state.items.get(item.id);
|
|
1677
|
+
addOrUpdateItem(state, item);
|
|
1678
|
+
if (item.kind !== 'agentMessage' && !existing) {
|
|
1679
|
+
this.emitItem(state, item, 'item.started');
|
|
1680
|
+
} else if (item.kind !== 'agentMessage' && existing) {
|
|
1681
|
+
this.emitItem(state, item, 'item.started', { force: true });
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
if (message.type === 'user' && message.parent_tool_use_id) {
|
|
1688
|
+
if (state.suppressedToolUseIds.has(message.parent_tool_use_id)) {
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
const item = resultForToolUse({
|
|
1692
|
+
toolUseId: message.parent_tool_use_id,
|
|
1693
|
+
result: message.tool_use_result ?? message.message,
|
|
1694
|
+
previous: state.items.get(message.parent_tool_use_id) ?? null,
|
|
1695
|
+
});
|
|
1696
|
+
addOrUpdateItem(state, item);
|
|
1697
|
+
this.emitItem(state, item, 'item.completed');
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
if (message.type === 'tool_progress') {
|
|
1702
|
+
const toolUseId = message.tool_use_id;
|
|
1703
|
+
const toolName = message.tool_name;
|
|
1704
|
+
if (!toolUseId || !toolName) {
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
if (!state.items.has(toolUseId)) {
|
|
1708
|
+
const item = toolUseToHistoryItem({
|
|
1709
|
+
id: toolUseId,
|
|
1710
|
+
name: toolName,
|
|
1711
|
+
toolInput: {
|
|
1712
|
+
elapsed_time_seconds: message.elapsed_time_seconds,
|
|
1713
|
+
},
|
|
1714
|
+
status: 'running',
|
|
1715
|
+
});
|
|
1716
|
+
if (item) {
|
|
1717
|
+
addOrUpdateItem(state, item);
|
|
1718
|
+
this.emitItem(state, item, 'item.started');
|
|
1719
|
+
} else {
|
|
1720
|
+
state.suppressedToolUseIds.add(toolUseId);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
if (message.type === 'system' && message.subtype === 'permission_denied') {
|
|
1727
|
+
const toolUseId = message.tool_use_id;
|
|
1728
|
+
if (!toolUseId) {
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
const previous = state.items.get(toolUseId);
|
|
1732
|
+
const item: AgentHistoryItem = previous
|
|
1733
|
+
? {
|
|
1734
|
+
...previous,
|
|
1735
|
+
status: 'denied',
|
|
1736
|
+
detailText: [previous.detailText ?? previous.text, '', message.message].join('\n'),
|
|
1737
|
+
}
|
|
1738
|
+
: {
|
|
1739
|
+
id: toolUseId,
|
|
1740
|
+
kind: 'toolCall',
|
|
1741
|
+
text: `${message.tool_name} denied`,
|
|
1742
|
+
detailText: typeof message.message === 'string' ? message.message : null,
|
|
1743
|
+
status: 'denied',
|
|
1744
|
+
};
|
|
1745
|
+
addOrUpdateItem(state, item);
|
|
1746
|
+
this.emitItem(state, item, 'item.completed');
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
private captureUsage(state: ActiveClaudeTurn, message: SDKMessage) {
|
|
1751
|
+
const assistantUsage = usageFromAssistantMessage(message);
|
|
1752
|
+
if (assistantUsage) {
|
|
1753
|
+
state.assistantUsage = state.assistantUsage
|
|
1754
|
+
? addClaudeUsage(state.assistantUsage, assistantUsage)
|
|
1755
|
+
: assistantUsage;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
const resultUsage = usageFromResultMessage(message);
|
|
1759
|
+
if (resultUsage) {
|
|
1760
|
+
state.resultUsage = resultUsage;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
const modelContextWindow = contextWindowFromResultMessage(message);
|
|
1764
|
+
if (modelContextWindow) {
|
|
1765
|
+
state.modelContextWindow = modelContextWindow;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
private emitUsage(state: ActiveClaudeTurn) {
|
|
1770
|
+
const usage = state.resultUsage ?? state.assistantUsage;
|
|
1771
|
+
if (!usage) {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
this.emitRuntimeEvent({
|
|
1775
|
+
type: 'usage.updated',
|
|
1776
|
+
provider: 'claude',
|
|
1777
|
+
providerSessionId: state.providerSessionId,
|
|
1778
|
+
providerTurnId: state.providerTurnId,
|
|
1779
|
+
usage: claudeUsagePayload(usage, state.modelContextWindow),
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
private emitItem(
|
|
1784
|
+
state: ActiveClaudeTurn,
|
|
1785
|
+
item: AgentHistoryItem,
|
|
1786
|
+
type: 'item.started' | 'item.completed',
|
|
1787
|
+
options: { force?: boolean } = {},
|
|
1788
|
+
) {
|
|
1789
|
+
if (type === 'item.started') {
|
|
1790
|
+
if (state.emittedItems.has(item.id) && !options.force) {
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
state.emittedItems.add(item.id);
|
|
1794
|
+
}
|
|
1795
|
+
this.emitRuntimeEvent({
|
|
1796
|
+
type,
|
|
1797
|
+
provider: 'claude',
|
|
1798
|
+
providerSessionId: state.providerSessionId,
|
|
1799
|
+
providerTurnId: state.providerTurnId,
|
|
1800
|
+
item,
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
private emitRuntimeEvent(event: AgentRuntimeEvent) {
|
|
1805
|
+
this.emit('event', event);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
private async loadSdk(): Promise<ClaudeSdkModule> {
|
|
1809
|
+
try {
|
|
1810
|
+
return await importOptionalPackage('@anthropic-ai/claude-agent-sdk') as unknown as ClaudeSdkModule;
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
throw new AgentRuntimeError(
|
|
1813
|
+
'Install Claude Code support with npm install -g @anthropic-ai/claude-agent-sdk, or add @anthropic-ai/claude-agent-sdk to this checkout.',
|
|
1814
|
+
'claude',
|
|
1815
|
+
'provider_unavailable',
|
|
1816
|
+
undefined,
|
|
1817
|
+
error,
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
private unavailableError() {
|
|
1823
|
+
return new AgentRuntimeError(
|
|
1824
|
+
this.sdkLoadError ?? 'Claude Code SDK is not installed.',
|
|
1825
|
+
'claude',
|
|
1826
|
+
'provider_unavailable',
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
private unavailableQueryFactory(): Query {
|
|
1831
|
+
throw this.unavailableError();
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
private async unavailableListSessions(): Promise<SDKSessionInfo[]> {
|
|
1835
|
+
throw this.unavailableError();
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
private async unavailableGetSessionMessages(): Promise<SessionMessage[]> {
|
|
1839
|
+
throw this.unavailableError();
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
private async unavailableGetSessionInfo(): Promise<SDKSessionInfo | null> {
|
|
1843
|
+
throw this.unavailableError();
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
private mapMcpServer(server: McpServerStatus): AgentMcpServer {
|
|
1847
|
+
return {
|
|
1848
|
+
name: server.name,
|
|
1849
|
+
authStatus: server.status === 'needs-auth' ? 'notLoggedIn' : 'unsupported',
|
|
1850
|
+
tools: (server.tools ?? []).map((tool) => ({
|
|
1851
|
+
name: tool.name,
|
|
1852
|
+
title: tool.name,
|
|
1853
|
+
description: tool.description ?? null,
|
|
1854
|
+
})),
|
|
1855
|
+
resourceCount: 0,
|
|
1856
|
+
resourceTemplateCount: 0,
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
private async userMessageToHistoryItem(
|
|
1861
|
+
id: string,
|
|
1862
|
+
message: unknown,
|
|
1863
|
+
context: {
|
|
1864
|
+
localThreadId?: string;
|
|
1865
|
+
workspacePath?: string;
|
|
1866
|
+
},
|
|
1867
|
+
): Promise<AgentHistoryItem> {
|
|
1868
|
+
if (!isRecord(message) || !Array.isArray(message.content)) {
|
|
1869
|
+
return userMessageToHistoryItem(id, message);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const parts: string[] = [];
|
|
1873
|
+
for (let index = 0; index < message.content.length; index += 1) {
|
|
1874
|
+
const block = message.content[index];
|
|
1875
|
+
if (!isRecord(block)) {
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
if (block.type === 'image') {
|
|
1879
|
+
const photoToken = await this.persistHistoryImageBlock({
|
|
1880
|
+
messageId: id,
|
|
1881
|
+
blockIndex: index,
|
|
1882
|
+
block,
|
|
1883
|
+
...context,
|
|
1884
|
+
});
|
|
1885
|
+
if (photoToken) {
|
|
1886
|
+
parts.push(photoToken);
|
|
1887
|
+
}
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
if (typeof block.text === 'string') {
|
|
1891
|
+
parts.push(block.text);
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
if (typeof block.content === 'string') {
|
|
1895
|
+
parts.push(block.content);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
return userMessageHistoryItem(id, parts.filter(Boolean).join('\n'));
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
private async persistHistoryImageBlock(input: {
|
|
1903
|
+
messageId: string;
|
|
1904
|
+
blockIndex: number;
|
|
1905
|
+
block: Record<string, unknown>;
|
|
1906
|
+
localThreadId?: string;
|
|
1907
|
+
workspacePath?: string;
|
|
1908
|
+
}): Promise<string | null> {
|
|
1909
|
+
if (!input.localThreadId || !input.workspacePath) {
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
const source = isRecord(input.block.source) ? input.block.source : null;
|
|
1914
|
+
if (!source || source.type !== 'base64') {
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const data = typeof source.data === 'string' ? source.data : null;
|
|
1919
|
+
if (!data) {
|
|
1920
|
+
return null;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
const extension = extensionForImageMediaType(
|
|
1924
|
+
typeof source.media_type === 'string' ? source.media_type : null,
|
|
1925
|
+
);
|
|
1926
|
+
if (!extension) {
|
|
1927
|
+
return null;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const relativePath = `./.temp/threads/${input.localThreadId}/claude-history-${safeAssetFilePart(input.messageId)}-${input.blockIndex}.${extension}`;
|
|
1931
|
+
const targetPath = path.resolve(input.workspacePath, relativePath);
|
|
1932
|
+
const relativeToWorkspace = path.relative(input.workspacePath, targetPath);
|
|
1933
|
+
if (
|
|
1934
|
+
relativeToWorkspace === '' ||
|
|
1935
|
+
relativeToWorkspace.startsWith('..') ||
|
|
1936
|
+
path.isAbsolute(relativeToWorkspace)
|
|
1937
|
+
) {
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
try {
|
|
1942
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
1943
|
+
await fs.writeFile(targetPath, Buffer.from(data, 'base64'));
|
|
1944
|
+
return `[PHOTO ${relativePath}]`;
|
|
1945
|
+
} catch {
|
|
1946
|
+
return null;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
private async sessionMessagesToTurns(
|
|
1951
|
+
messages: SessionMessage[],
|
|
1952
|
+
context: {
|
|
1953
|
+
localThreadId?: string;
|
|
1954
|
+
workspacePath?: string;
|
|
1955
|
+
} = {},
|
|
1956
|
+
): Promise<AgentTurn[]> {
|
|
1957
|
+
const turns: AgentTurn[] = [];
|
|
1958
|
+
let current: {
|
|
1959
|
+
providerTurnId: string;
|
|
1960
|
+
startedAt: string | null;
|
|
1961
|
+
items: AgentHistoryItem[];
|
|
1962
|
+
itemsById: Map<string, AgentHistoryItem>;
|
|
1963
|
+
} | null = null;
|
|
1964
|
+
let skippingHiddenInit = false;
|
|
1965
|
+
const suppressedToolUseIds = new Set<string>();
|
|
1966
|
+
|
|
1967
|
+
const upsertCurrentItem = (item: AgentHistoryItem) => {
|
|
1968
|
+
if (!current) {
|
|
1969
|
+
current = {
|
|
1970
|
+
providerTurnId: randomUUID(),
|
|
1971
|
+
startedAt: null,
|
|
1972
|
+
items: [],
|
|
1973
|
+
itemsById: new Map(),
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
const existingIndex = current.items.findIndex((entry) => entry.id === item.id);
|
|
1977
|
+
if (existingIndex >= 0) {
|
|
1978
|
+
current.items[existingIndex] = item;
|
|
1979
|
+
} else {
|
|
1980
|
+
current.items.push(item);
|
|
1981
|
+
}
|
|
1982
|
+
current.itemsById.set(item.id, item);
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1985
|
+
for (const message of messages) {
|
|
1986
|
+
if (message.type === 'user') {
|
|
1987
|
+
const rawToolResults = toolResultBlocks(message.message);
|
|
1988
|
+
const toolResults = rawToolResults.filter(
|
|
1989
|
+
(toolResult) => !suppressedToolUseIds.has(toolResult.toolUseId),
|
|
1990
|
+
);
|
|
1991
|
+
if (toolResults.length > 0) {
|
|
1992
|
+
for (const toolResult of toolResults) {
|
|
1993
|
+
const previous = current?.itemsById.get(toolResult.toolUseId) ?? null;
|
|
1994
|
+
upsertCurrentItem(resultForToolUse({
|
|
1995
|
+
toolUseId: toolResult.toolUseId,
|
|
1996
|
+
result: toolResult.result,
|
|
1997
|
+
previous,
|
|
1998
|
+
}));
|
|
1999
|
+
}
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
if (rawToolResults.length > 0) {
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
if (message.type === 'user' && !message.parent_tool_use_id) {
|
|
2008
|
+
if (isHiddenInitMessage(message.message)) {
|
|
2009
|
+
skippingHiddenInit = true;
|
|
2010
|
+
current = null;
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
if (isHiddenContinuationMessage(message.message)) {
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
skippingHiddenInit = false;
|
|
2017
|
+
if (current && current.items.length > 0) {
|
|
2018
|
+
turns.push(buildAgentTurn({
|
|
2019
|
+
providerTurnId: current.providerTurnId,
|
|
2020
|
+
startedAt: current.startedAt,
|
|
2021
|
+
status: 'completed',
|
|
2022
|
+
items: current.items,
|
|
2023
|
+
}));
|
|
2024
|
+
}
|
|
2025
|
+
const messageUuid = message.uuid ?? randomUUID();
|
|
2026
|
+
const userItem = await this.userMessageToHistoryItem(
|
|
2027
|
+
messageUuid,
|
|
2028
|
+
message.message,
|
|
2029
|
+
context,
|
|
2030
|
+
);
|
|
2031
|
+
current = {
|
|
2032
|
+
providerTurnId: `claude-turn-${messageUuid}`,
|
|
2033
|
+
startedAt: isoFromUuidV7(messageUuid),
|
|
2034
|
+
items: [userItem],
|
|
2035
|
+
itemsById: new Map([[messageUuid, userItem]]),
|
|
2036
|
+
};
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
if (skippingHiddenInit && message.type === 'assistant') {
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
if (message.type === 'assistant') {
|
|
2045
|
+
for (const toolUseId of suppressedClaudeToolUseIds(message.message)) {
|
|
2046
|
+
suppressedToolUseIds.add(toolUseId);
|
|
2047
|
+
current?.itemsById.delete(toolUseId);
|
|
2048
|
+
}
|
|
2049
|
+
for (const item of assistantMessageToHistoryItems({
|
|
2050
|
+
messageId: message.uuid ?? randomUUID(),
|
|
2051
|
+
message: message.message,
|
|
2052
|
+
})) {
|
|
2053
|
+
upsertCurrentItem(item);
|
|
2054
|
+
}
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
if (message.type === 'user' && message.parent_tool_use_id) {
|
|
2059
|
+
if (suppressedToolUseIds.has(message.parent_tool_use_id)) {
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
const previous = current?.itemsById.get(message.parent_tool_use_id) ?? null;
|
|
2063
|
+
upsertCurrentItem(resultForToolUse({
|
|
2064
|
+
toolUseId: message.parent_tool_use_id,
|
|
2065
|
+
result: isRecord(message.message) && 'content' in message.message
|
|
2066
|
+
? message.message.content
|
|
2067
|
+
: message.message,
|
|
2068
|
+
previous,
|
|
2069
|
+
}));
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
if (current && current.items.length > 0) {
|
|
2074
|
+
turns.push(buildAgentTurn({
|
|
2075
|
+
providerTurnId: current.providerTurnId,
|
|
2076
|
+
startedAt: current.startedAt,
|
|
2077
|
+
status: 'completed',
|
|
2078
|
+
items: current.items,
|
|
2079
|
+
}));
|
|
2080
|
+
}
|
|
2081
|
+
return turns;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
private async withClaudeConfigEnv<T>(callback: () => Promise<T>): Promise<T> {
|
|
2085
|
+
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
|
2086
|
+
const previousClaudeHome = process.env.CLAUDE_HOME;
|
|
2087
|
+
process.env.CLAUDE_CONFIG_DIR = this.options.home;
|
|
2088
|
+
process.env.CLAUDE_HOME = this.options.home;
|
|
2089
|
+
try {
|
|
2090
|
+
return await callback();
|
|
2091
|
+
} finally {
|
|
2092
|
+
if (previousConfigDir === undefined) {
|
|
2093
|
+
delete process.env.CLAUDE_CONFIG_DIR;
|
|
2094
|
+
} else {
|
|
2095
|
+
process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
|
|
2096
|
+
}
|
|
2097
|
+
if (previousClaudeHome === undefined) {
|
|
2098
|
+
delete process.env.CLAUDE_HOME;
|
|
2099
|
+
} else {
|
|
2100
|
+
process.env.CLAUDE_HOME = previousClaudeHome;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
private markFailed(error: unknown) {
|
|
2106
|
+
this.status = {
|
|
2107
|
+
...this.status,
|
|
2108
|
+
state: 'failed',
|
|
2109
|
+
lastError: errorMessage(error),
|
|
2110
|
+
};
|
|
2111
|
+
this.emit('status', this.getStatus());
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
async function importOptionalPackage(specifier: string) {
|
|
2116
|
+
const dynamicImport = new Function('specifier', 'return import(specifier);') as (
|
|
2117
|
+
specifier: string,
|
|
2118
|
+
) => Promise<unknown>;
|
|
2119
|
+
try {
|
|
2120
|
+
return await dynamicImport(specifier);
|
|
2121
|
+
} catch (localError) {
|
|
2122
|
+
const globalRoot = await npmGlobalRoot();
|
|
2123
|
+
if (!globalRoot) {
|
|
2124
|
+
throw localError;
|
|
2125
|
+
}
|
|
2126
|
+
try {
|
|
2127
|
+
const requireFromGlobal = createRequire(path.join(globalRoot, 'remote-codex-global.cjs'));
|
|
2128
|
+
const resolved = requireFromGlobal.resolve(specifier);
|
|
2129
|
+
return await dynamicImport(pathToFileURL(resolved).href);
|
|
2130
|
+
} catch {
|
|
2131
|
+
throw localError;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
async function npmGlobalRoot() {
|
|
2137
|
+
try {
|
|
2138
|
+
const { stdout } = await execFileAsync('npm', ['root', '-g'], {
|
|
2139
|
+
timeout: 3_000,
|
|
2140
|
+
});
|
|
2141
|
+
return stdout.trim() || null;
|
|
2142
|
+
} catch {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
}
|