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