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,2138 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { ClaudeRuntimeAdapter } from './runtimeAdapter';
|
|
7
|
+
import { hiddenInitPrompt } from './historyItems';
|
|
8
|
+
import type { AgentRuntimeEvent } from '../../agent-runtime/src/index';
|
|
9
|
+
|
|
10
|
+
type SDKMessage = Record<string, any>;
|
|
11
|
+
type SDKSessionInfo = Record<string, any>;
|
|
12
|
+
type SessionMessage = Record<string, any>;
|
|
13
|
+
interface Query extends AsyncIterable<SDKMessage> {
|
|
14
|
+
close(): void;
|
|
15
|
+
interrupt(): Promise<void>;
|
|
16
|
+
supportedModels(): Promise<any[]>;
|
|
17
|
+
mcpServerStatus(): Promise<any[]>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function wait(ms = 0) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class FakeQuery implements Query {
|
|
25
|
+
interrupted = false;
|
|
26
|
+
closed = false;
|
|
27
|
+
private resolveRelease: (() => void) | null = null;
|
|
28
|
+
private releasePromise: Promise<void> | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly messages: SDKMessage[],
|
|
32
|
+
private readonly options: { holdOpen?: boolean } = {},
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
[Symbol.asyncIterator]() {
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async next(): Promise<IteratorResult<SDKMessage, void>> {
|
|
40
|
+
if (this.messages.length === 0) {
|
|
41
|
+
if (this.options.holdOpen && !this.closed && !this.interrupted) {
|
|
42
|
+
this.releasePromise ??= new Promise((resolve) => {
|
|
43
|
+
this.resolveRelease = resolve;
|
|
44
|
+
});
|
|
45
|
+
await this.releasePromise;
|
|
46
|
+
}
|
|
47
|
+
return { done: true, value: undefined };
|
|
48
|
+
}
|
|
49
|
+
return { done: false, value: this.messages.shift()! };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async return(): Promise<IteratorResult<SDKMessage, void>> {
|
|
53
|
+
this.close();
|
|
54
|
+
return { done: true, value: undefined };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async throw(error?: unknown): Promise<IteratorResult<SDKMessage, void>> {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async interrupt() {
|
|
62
|
+
this.interrupted = true;
|
|
63
|
+
this.resolveRelease?.();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
close() {
|
|
67
|
+
this.closed = true;
|
|
68
|
+
this.resolveRelease?.();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async setPermissionMode() {}
|
|
72
|
+
async setModel() {}
|
|
73
|
+
async setMaxThinkingTokens() {}
|
|
74
|
+
async applyFlagSettings() {}
|
|
75
|
+
async initializationResult(): Promise<any> {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
async supportedCommands(): Promise<any[]> {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
async supportedModels(): Promise<any[]> {
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
value: 'claude-sonnet-4-5',
|
|
85
|
+
displayName: 'Claude Sonnet 4.5',
|
|
86
|
+
description: 'Test model',
|
|
87
|
+
supportsEffort: true,
|
|
88
|
+
supportedEffortLevels: ['low', 'medium', 'high'],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
async supportedAgents(): Promise<any[]> {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
async mcpServerStatus(): Promise<any[]> {
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
name: 'docs',
|
|
99
|
+
status: 'connected',
|
|
100
|
+
tools: [{ name: 'search', description: 'Search docs' }],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
async getContextUsage(): Promise<any> {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
async readFile(): Promise<any> {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
async reloadPlugins(): Promise<any> {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
async accountInfo(): Promise<any> {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
async rewindFiles(): Promise<any> {
|
|
117
|
+
return { canRewind: false };
|
|
118
|
+
}
|
|
119
|
+
async seedReadState() {}
|
|
120
|
+
async reconnectMcpServer() {}
|
|
121
|
+
async toggleMcpServer() {}
|
|
122
|
+
async setMcpServers(): Promise<any> {
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
async streamInput() {}
|
|
126
|
+
async stopTask() {}
|
|
127
|
+
async backgroundTasks(): Promise<boolean> {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function systemInit(sessionId = 'claude-session-1'): SDKMessage {
|
|
133
|
+
return {
|
|
134
|
+
type: 'system',
|
|
135
|
+
subtype: 'init',
|
|
136
|
+
apiKeySource: 'user',
|
|
137
|
+
claude_code_version: '2.1.146',
|
|
138
|
+
cwd: '/tmp/workspace',
|
|
139
|
+
tools: [],
|
|
140
|
+
mcp_servers: [],
|
|
141
|
+
model: 'sonnet',
|
|
142
|
+
permissionMode: 'default',
|
|
143
|
+
slash_commands: [],
|
|
144
|
+
output_style: 'default',
|
|
145
|
+
skills: [],
|
|
146
|
+
plugins: [],
|
|
147
|
+
uuid: '00000000-0000-4000-8000-000000000001' as any,
|
|
148
|
+
session_id: sessionId,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function result(sessionId = 'claude-session-1'): SDKMessage {
|
|
153
|
+
return {
|
|
154
|
+
type: 'result',
|
|
155
|
+
subtype: 'success',
|
|
156
|
+
duration_ms: 1,
|
|
157
|
+
duration_api_ms: 1,
|
|
158
|
+
is_error: false,
|
|
159
|
+
num_turns: 1,
|
|
160
|
+
result: 'ok',
|
|
161
|
+
stop_reason: 'end_turn',
|
|
162
|
+
total_cost_usd: 0,
|
|
163
|
+
usage: {} as any,
|
|
164
|
+
modelUsage: {},
|
|
165
|
+
permission_denials: [],
|
|
166
|
+
uuid: '00000000-0000-4000-8000-000000000002' as any,
|
|
167
|
+
session_id: sessionId,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function makeAdapter(
|
|
172
|
+
messagesForPrompt: (
|
|
173
|
+
prompt: string,
|
|
174
|
+
options: Record<string, unknown>,
|
|
175
|
+
) => SDKMessage[] | FakeQuery,
|
|
176
|
+
) {
|
|
177
|
+
return new ClaudeRuntimeAdapter({
|
|
178
|
+
home: '/tmp/claude-home',
|
|
179
|
+
command: 'claude',
|
|
180
|
+
clientInfo: {
|
|
181
|
+
name: 'test',
|
|
182
|
+
version: '0.1.0',
|
|
183
|
+
},
|
|
184
|
+
query: ((params: { prompt: string; options: Record<string, unknown> }) => {
|
|
185
|
+
const messages = messagesForPrompt(params.prompt, params.options);
|
|
186
|
+
return Array.isArray(messages) ? new FakeQuery(messages) : messages;
|
|
187
|
+
}) as any,
|
|
188
|
+
listSessions: (async () => [
|
|
189
|
+
{
|
|
190
|
+
sessionId: 'claude-session-1',
|
|
191
|
+
summary: 'Existing session',
|
|
192
|
+
lastModified: 1_772_000_000_000,
|
|
193
|
+
createdAt: 1_771_000_000_000,
|
|
194
|
+
cwd: '/tmp/workspace',
|
|
195
|
+
firstPrompt: 'Hello',
|
|
196
|
+
} satisfies SDKSessionInfo,
|
|
197
|
+
]) as any,
|
|
198
|
+
getSessionInfo: (async () => ({
|
|
199
|
+
sessionId: 'claude-session-1',
|
|
200
|
+
summary: 'Existing session',
|
|
201
|
+
lastModified: 1_772_000_000_000,
|
|
202
|
+
createdAt: 1_771_000_000_000,
|
|
203
|
+
cwd: '/tmp/workspace',
|
|
204
|
+
firstPrompt: 'Hello',
|
|
205
|
+
})) as any,
|
|
206
|
+
getSessionMessages: (async () => [] satisfies SessionMessage[]) as any,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
describe('ClaudeRuntimeAdapter', () => {
|
|
211
|
+
it('passes the configured Claude executable to the SDK', async () => {
|
|
212
|
+
const sdkOptions: Record<string, unknown>[] = [];
|
|
213
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
214
|
+
home: '/tmp/claude-home',
|
|
215
|
+
command: 'claude',
|
|
216
|
+
query: ((params: { prompt: string; options: Record<string, unknown> }) => {
|
|
217
|
+
sdkOptions.push(params.options);
|
|
218
|
+
return new FakeQuery([systemInit(), result()]);
|
|
219
|
+
}) as any,
|
|
220
|
+
listSessions: (async () => [] satisfies SDKSessionInfo[]) as any,
|
|
221
|
+
getSessionInfo: (async () => null) as any,
|
|
222
|
+
getSessionMessages: (async () => [] satisfies SessionMessage[]) as any,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await adapter.start();
|
|
226
|
+
await adapter.startSession({
|
|
227
|
+
cwd: '/tmp/workspace',
|
|
228
|
+
model: 'sonnet',
|
|
229
|
+
approvalMode: 'guarded',
|
|
230
|
+
sandboxMode: 'workspace-write',
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(sdkOptions[0]?.pathToClaudeCodeExecutable).toBe('claude');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('does not pass an empty tool list to Claude session initialization', async () => {
|
|
237
|
+
const sdkOptions: Record<string, unknown>[] = [];
|
|
238
|
+
const adapter = makeAdapter((_prompt, options) => {
|
|
239
|
+
sdkOptions.push(options);
|
|
240
|
+
return [systemInit(), result()];
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await adapter.startSession({
|
|
244
|
+
cwd: '/tmp/workspace',
|
|
245
|
+
model: 'sonnet',
|
|
246
|
+
approvalMode: 'guarded',
|
|
247
|
+
sandboxMode: 'workspace-write',
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(sdkOptions[0]).not.toHaveProperty('tools');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('maps thread sandbox modes to Claude permission and sandbox settings', async () => {
|
|
254
|
+
const turnOptions: Record<string, unknown>[] = [];
|
|
255
|
+
const adapter = makeAdapter((_prompt, options) => {
|
|
256
|
+
turnOptions.push(options);
|
|
257
|
+
return [systemInit(), result()];
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await adapter.startTurn({
|
|
261
|
+
providerSessionId: 'claude-session-1',
|
|
262
|
+
prompt: 'Read only',
|
|
263
|
+
model: 'sonnet',
|
|
264
|
+
sandboxMode: 'read-only',
|
|
265
|
+
workspacePath: '/tmp/workspace',
|
|
266
|
+
});
|
|
267
|
+
await wait();
|
|
268
|
+
await adapter.startTurn({
|
|
269
|
+
providerSessionId: 'claude-session-1',
|
|
270
|
+
prompt: 'Workspace write',
|
|
271
|
+
model: 'sonnet',
|
|
272
|
+
sandboxMode: 'workspace-write',
|
|
273
|
+
workspacePath: '/tmp/workspace',
|
|
274
|
+
});
|
|
275
|
+
await wait();
|
|
276
|
+
await adapter.startTurn({
|
|
277
|
+
providerSessionId: 'claude-session-1',
|
|
278
|
+
prompt: 'Full access',
|
|
279
|
+
model: 'sonnet',
|
|
280
|
+
sandboxMode: 'danger-full-access',
|
|
281
|
+
workspacePath: '/tmp/workspace',
|
|
282
|
+
});
|
|
283
|
+
await wait();
|
|
284
|
+
|
|
285
|
+
expect(turnOptions.at(-3)).toMatchObject({
|
|
286
|
+
permissionMode: 'default',
|
|
287
|
+
sandbox: {
|
|
288
|
+
enabled: true,
|
|
289
|
+
autoAllowBashIfSandboxed: true,
|
|
290
|
+
allowUnsandboxedCommands: false,
|
|
291
|
+
filesystem: {
|
|
292
|
+
denyWrite: ['/tmp/workspace'],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
expect(turnOptions.at(-2)).toMatchObject({
|
|
297
|
+
permissionMode: 'acceptEdits',
|
|
298
|
+
sandbox: {
|
|
299
|
+
enabled: true,
|
|
300
|
+
autoAllowBashIfSandboxed: true,
|
|
301
|
+
allowUnsandboxedCommands: false,
|
|
302
|
+
filesystem: {
|
|
303
|
+
allowWrite: ['/tmp/workspace'],
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
expect(turnOptions.at(-1)).toMatchObject({
|
|
308
|
+
permissionMode: 'bypassPermissions',
|
|
309
|
+
allowDangerouslySkipPermissions: true,
|
|
310
|
+
});
|
|
311
|
+
expect(turnOptions.at(-1)?.sandbox).toBeUndefined();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('sends prompt photo tokens as Claude image content blocks', async () => {
|
|
315
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-photo-prompt-'));
|
|
316
|
+
const workspacePath = path.join(tempDir, 'workspace');
|
|
317
|
+
const imagePath = path.join(workspacePath, '.temp', 'threads', 'thread-1', 'camera.png');
|
|
318
|
+
await fs.mkdir(path.dirname(imagePath), { recursive: true });
|
|
319
|
+
await fs.writeFile(imagePath, Buffer.from('fake-png'));
|
|
320
|
+
|
|
321
|
+
const prompts: unknown[] = [];
|
|
322
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
323
|
+
home: '/tmp/claude-home',
|
|
324
|
+
command: 'claude',
|
|
325
|
+
query: ((params: { prompt: unknown; options: Record<string, unknown> }) => {
|
|
326
|
+
prompts.push(params.prompt);
|
|
327
|
+
return new FakeQuery([systemInit(), result()]);
|
|
328
|
+
}) as any,
|
|
329
|
+
listSessions: (async () => [] satisfies SDKSessionInfo[]) as any,
|
|
330
|
+
getSessionInfo: (async () => null) as any,
|
|
331
|
+
getSessionMessages: (async () => [] satisfies SessionMessage[]) as any,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await adapter.startTurn({
|
|
335
|
+
providerSessionId: 'claude-session-1',
|
|
336
|
+
prompt: 'Please inspect [PHOTO ./.temp/threads/thread-1/camera.png] now.',
|
|
337
|
+
model: 'sonnet',
|
|
338
|
+
collaborationMode: 'default',
|
|
339
|
+
sandboxMode: 'danger-full-access',
|
|
340
|
+
workspacePath,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(typeof (prompts[0] as AsyncIterable<unknown>)[Symbol.asyncIterator]).toBe('function');
|
|
344
|
+
const messages: any[] = [];
|
|
345
|
+
for await (const message of prompts[0] as AsyncIterable<unknown>) {
|
|
346
|
+
messages.push(message);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
expect(messages).toHaveLength(1);
|
|
350
|
+
expect(messages[0]).toMatchObject({
|
|
351
|
+
type: 'user',
|
|
352
|
+
parent_tool_use_id: null,
|
|
353
|
+
message: {
|
|
354
|
+
role: 'user',
|
|
355
|
+
content: [
|
|
356
|
+
{ type: 'text', text: 'Please inspect ' },
|
|
357
|
+
{
|
|
358
|
+
type: 'image',
|
|
359
|
+
source: {
|
|
360
|
+
type: 'base64',
|
|
361
|
+
media_type: 'image/png',
|
|
362
|
+
data: Buffer.from('fake-png').toString('base64'),
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
{ type: 'text', text: ' now.' },
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('keeps unsupported or unreadable photo tokens as plain prompt text', async () => {
|
|
372
|
+
const prompts: unknown[] = [];
|
|
373
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
374
|
+
home: '/tmp/claude-home',
|
|
375
|
+
command: 'claude',
|
|
376
|
+
query: ((params: { prompt: unknown; options: Record<string, unknown> }) => {
|
|
377
|
+
prompts.push(params.prompt);
|
|
378
|
+
return new FakeQuery([systemInit(), result()]);
|
|
379
|
+
}) as any,
|
|
380
|
+
listSessions: (async () => [] satisfies SDKSessionInfo[]) as any,
|
|
381
|
+
getSessionInfo: (async () => null) as any,
|
|
382
|
+
getSessionMessages: (async () => [] satisfies SessionMessage[]) as any,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await adapter.startTurn({
|
|
386
|
+
providerSessionId: 'claude-session-1',
|
|
387
|
+
prompt: 'Please inspect [PHOTO ./.temp/threads/thread-1/camera.heic].',
|
|
388
|
+
model: 'sonnet',
|
|
389
|
+
collaborationMode: 'default',
|
|
390
|
+
sandboxMode: 'danger-full-access',
|
|
391
|
+
workspacePath: '/tmp/workspace',
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(prompts[0]).toBe('Please inspect [PHOTO ./.temp/threads/thread-1/camera.heic].');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('reconciles active multimodal transcript turns back to the live runtime turn id', async () => {
|
|
398
|
+
let queryMessages: SDKMessage[] = [systemInit()];
|
|
399
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
400
|
+
home: '/tmp/claude-home',
|
|
401
|
+
command: 'claude',
|
|
402
|
+
query: (() => new FakeQuery(queryMessages, { holdOpen: true })) as any,
|
|
403
|
+
listSessions: (async () => [] satisfies SDKSessionInfo[]) as any,
|
|
404
|
+
getSessionInfo: (async () => ({
|
|
405
|
+
sessionId: 'claude-session-1',
|
|
406
|
+
summary: 'Existing session',
|
|
407
|
+
lastModified: 1_772_000_000_000,
|
|
408
|
+
createdAt: 1_771_000_000_000,
|
|
409
|
+
cwd: '/tmp/workspace',
|
|
410
|
+
})) as any,
|
|
411
|
+
getSessionMessages: (async () => [
|
|
412
|
+
{
|
|
413
|
+
type: 'user',
|
|
414
|
+
uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
|
|
415
|
+
session_id: 'claude-session-1',
|
|
416
|
+
message: {
|
|
417
|
+
role: 'user',
|
|
418
|
+
content: [
|
|
419
|
+
{ type: 'text', text: 'What number is in the screenshot? ' },
|
|
420
|
+
{
|
|
421
|
+
type: 'image',
|
|
422
|
+
source: {
|
|
423
|
+
type: 'base64',
|
|
424
|
+
media_type: 'image/png',
|
|
425
|
+
data: 'ZmFrZS1wbmc=',
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
parent_tool_use_id: null,
|
|
431
|
+
},
|
|
432
|
+
] satisfies SessionMessage[]) as any,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const started = await adapter.startTurn({
|
|
436
|
+
providerSessionId: 'claude-session-1',
|
|
437
|
+
prompt: 'What number is in the screenshot? [PHOTO ./.temp/threads/thread-1/image.png]',
|
|
438
|
+
model: 'sonnet',
|
|
439
|
+
collaborationMode: 'default',
|
|
440
|
+
sandboxMode: 'danger-full-access',
|
|
441
|
+
workspacePath: '/tmp/workspace',
|
|
442
|
+
});
|
|
443
|
+
queryMessages = [];
|
|
444
|
+
const session = await adapter.readSession('claude-session-1');
|
|
445
|
+
|
|
446
|
+
expect(session.turns).toHaveLength(1);
|
|
447
|
+
expect(session.turns[0]).toMatchObject({
|
|
448
|
+
providerTurnId: started.providerTurnId,
|
|
449
|
+
status: 'inProgress',
|
|
450
|
+
items: [
|
|
451
|
+
expect.objectContaining({
|
|
452
|
+
id: `${started.providerTurnId}:user`,
|
|
453
|
+
kind: 'userMessage',
|
|
454
|
+
text: 'What number is in the screenshot? [PHOTO ./.temp/threads/thread-1/image.png]',
|
|
455
|
+
}),
|
|
456
|
+
],
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('keeps image blocks visible when reading Claude session history', async () => {
|
|
461
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-history-image-'));
|
|
462
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
463
|
+
home: '/tmp/claude-home',
|
|
464
|
+
command: 'claude',
|
|
465
|
+
query: (() => new FakeQuery([systemInit(), result()])) as any,
|
|
466
|
+
listSessions: (async () => [] satisfies SDKSessionInfo[]) as any,
|
|
467
|
+
getSessionInfo: (async () => ({
|
|
468
|
+
sessionId: 'claude-session-1',
|
|
469
|
+
summary: 'Existing session',
|
|
470
|
+
lastModified: 1_772_000_000_000,
|
|
471
|
+
createdAt: 1_771_000_000_000,
|
|
472
|
+
cwd: workspace,
|
|
473
|
+
})) as any,
|
|
474
|
+
getSessionMessages: (async () => [
|
|
475
|
+
{
|
|
476
|
+
type: 'user',
|
|
477
|
+
uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
|
|
478
|
+
session_id: 'claude-session-1',
|
|
479
|
+
message: {
|
|
480
|
+
role: 'user',
|
|
481
|
+
content: [
|
|
482
|
+
{ type: 'text', text: 'What number is in the screenshot? ' },
|
|
483
|
+
{
|
|
484
|
+
type: 'image',
|
|
485
|
+
source: {
|
|
486
|
+
type: 'base64',
|
|
487
|
+
media_type: 'image/png',
|
|
488
|
+
data: 'ZmFrZS1wbmc=',
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
},
|
|
493
|
+
parent_tool_use_id: null,
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
type: 'assistant',
|
|
497
|
+
uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ed',
|
|
498
|
+
session_id: 'claude-session-1',
|
|
499
|
+
message: {
|
|
500
|
+
role: 'assistant',
|
|
501
|
+
content: [{ type: 'text', text: 'The number is 9.' }],
|
|
502
|
+
},
|
|
503
|
+
parent_tool_use_id: null,
|
|
504
|
+
},
|
|
505
|
+
] satisfies SessionMessage[]) as any,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const session = await adapter.readSession('claude-session-1', {
|
|
509
|
+
localThreadId: 'thread-1',
|
|
510
|
+
workspacePath: workspace,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
expect(session.turns[0]?.items[0]).toMatchObject({
|
|
514
|
+
kind: 'userMessage',
|
|
515
|
+
text: 'What number is in the screenshot? \n[PHOTO ./.temp/threads/thread-1/claude-history-019e4657-bd3c-72d1-b59d-324ed8a4b1ec-1.png]',
|
|
516
|
+
});
|
|
517
|
+
await expect(
|
|
518
|
+
fs.readFile(
|
|
519
|
+
path.join(
|
|
520
|
+
workspace,
|
|
521
|
+
'.temp/threads/thread-1/claude-history-019e4657-bd3c-72d1-b59d-324ed8a4b1ec-1.png',
|
|
522
|
+
),
|
|
523
|
+
'utf8',
|
|
524
|
+
),
|
|
525
|
+
).resolves.toBe('fake-png');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('starts a session from the Claude init message and hides the synthetic prompt', async () => {
|
|
529
|
+
const adapter = makeAdapter((prompt) => {
|
|
530
|
+
expect(prompt).toBe(hiddenInitPrompt());
|
|
531
|
+
return [systemInit(), result()];
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
await adapter.start();
|
|
535
|
+
const response = await adapter.startSession({
|
|
536
|
+
cwd: '/tmp/workspace',
|
|
537
|
+
model: 'sonnet',
|
|
538
|
+
approvalMode: 'guarded',
|
|
539
|
+
sandboxMode: 'workspace-write',
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
expect(response).toMatchObject({
|
|
543
|
+
provider: 'claude',
|
|
544
|
+
providerSessionId: 'claude-session-1',
|
|
545
|
+
model: 'sonnet',
|
|
546
|
+
session: {
|
|
547
|
+
turns: [],
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
await expect(adapter.listLoadedSessions()).resolves.toEqual(['claude-session-1']);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('exposes the Claude Code Sonnet 1M option and enables the 1M context beta', async () => {
|
|
554
|
+
const sdkOptions: Record<string, unknown>[] = [];
|
|
555
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
556
|
+
home: '/tmp/claude-home',
|
|
557
|
+
command: 'claude',
|
|
558
|
+
query: ((params: { prompt: string; options: Record<string, unknown> }) => {
|
|
559
|
+
sdkOptions.push(params.options);
|
|
560
|
+
return new FakeQuery([systemInit(), result()]);
|
|
561
|
+
}) as any,
|
|
562
|
+
listSessions: (async () => [] satisfies SDKSessionInfo[]) as any,
|
|
563
|
+
getSessionInfo: (async () => null) as any,
|
|
564
|
+
getSessionMessages: (async () => [] satisfies SessionMessage[]) as any,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
await expect(adapter.listModels()).resolves.toEqual(
|
|
568
|
+
expect.arrayContaining([
|
|
569
|
+
expect.objectContaining({
|
|
570
|
+
model: 'sonnet',
|
|
571
|
+
displayName: 'Claude Sonnet',
|
|
572
|
+
isDefault: true,
|
|
573
|
+
}),
|
|
574
|
+
expect.objectContaining({
|
|
575
|
+
model: 'sonnet[1m]',
|
|
576
|
+
displayName: 'Claude Sonnet 1M',
|
|
577
|
+
}),
|
|
578
|
+
]),
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
await adapter.startSession({
|
|
582
|
+
cwd: '/tmp/workspace',
|
|
583
|
+
model: 'sonnet[1m]',
|
|
584
|
+
approvalMode: 'guarded',
|
|
585
|
+
sandboxMode: 'workspace-write',
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
expect(sdkOptions[0]).toMatchObject({
|
|
589
|
+
model: 'sonnet',
|
|
590
|
+
betas: ['context-1m-2025-08-07'],
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('does not expose the synthetic init prompt as a session summary', async () => {
|
|
595
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
596
|
+
home: '/tmp/claude-home',
|
|
597
|
+
query: (() => new FakeQuery([])) as any,
|
|
598
|
+
listSessions: (async () => [
|
|
599
|
+
{
|
|
600
|
+
sessionId: 'claude-session-1',
|
|
601
|
+
summary: hiddenInitPrompt(),
|
|
602
|
+
firstPrompt: hiddenInitPrompt(),
|
|
603
|
+
lastModified: 1_772_000_000_000,
|
|
604
|
+
createdAt: 1_771_000_000_000,
|
|
605
|
+
cwd: '/tmp/workspace',
|
|
606
|
+
} satisfies SDKSessionInfo,
|
|
607
|
+
]) as any,
|
|
608
|
+
getSessionInfo: (async () => ({
|
|
609
|
+
sessionId: 'claude-session-1',
|
|
610
|
+
summary: hiddenInitPrompt(),
|
|
611
|
+
firstPrompt: hiddenInitPrompt(),
|
|
612
|
+
lastModified: 1_772_000_000_000,
|
|
613
|
+
createdAt: 1_771_000_000_000,
|
|
614
|
+
cwd: '/tmp/workspace',
|
|
615
|
+
})) as any,
|
|
616
|
+
getSessionMessages: (async () => [] satisfies SessionMessage[]) as any,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await expect(adapter.listSessions()).resolves.toEqual([
|
|
620
|
+
expect.objectContaining({
|
|
621
|
+
providerSessionId: 'claude-session-1',
|
|
622
|
+
title: null,
|
|
623
|
+
preview: null,
|
|
624
|
+
}),
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
await expect(adapter.readSession('claude-session-1')).resolves.toMatchObject({
|
|
628
|
+
providerSessionId: 'claude-session-1',
|
|
629
|
+
title: null,
|
|
630
|
+
preview: null,
|
|
631
|
+
});
|
|
632
|
+
await expect(adapter.listLoadedSessions()).resolves.toEqual(['claude-session-1']);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('filters Claude generated titles derived from the synthetic init prompt', async () => {
|
|
636
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
637
|
+
home: '/tmp/claude-home',
|
|
638
|
+
query: (() => new FakeQuery([])) as any,
|
|
639
|
+
listSessions: (async () => [
|
|
640
|
+
{
|
|
641
|
+
sessionId: 'claude-session-1',
|
|
642
|
+
summary: 'Initialize Remote Codex session',
|
|
643
|
+
firstPrompt: hiddenInitPrompt(),
|
|
644
|
+
lastModified: 1_772_000_000_000,
|
|
645
|
+
createdAt: 1_771_000_000_000,
|
|
646
|
+
cwd: '/tmp/workspace',
|
|
647
|
+
} satisfies SDKSessionInfo,
|
|
648
|
+
]) as any,
|
|
649
|
+
getSessionInfo: (async () => ({
|
|
650
|
+
sessionId: 'claude-session-1',
|
|
651
|
+
summary: 'Initialize Remote Codex session',
|
|
652
|
+
firstPrompt: hiddenInitPrompt(),
|
|
653
|
+
lastModified: 1_772_000_000_000,
|
|
654
|
+
createdAt: 1_771_000_000_000,
|
|
655
|
+
cwd: '/tmp/workspace',
|
|
656
|
+
})) as any,
|
|
657
|
+
getSessionMessages: (async () => [] satisfies SessionMessage[]) as any,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
await expect(adapter.listSessions()).resolves.toEqual([
|
|
661
|
+
expect.objectContaining({
|
|
662
|
+
providerSessionId: 'claude-session-1',
|
|
663
|
+
title: null,
|
|
664
|
+
preview: null,
|
|
665
|
+
}),
|
|
666
|
+
]);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('emits streamed assistant output and tool events for a turn', async () => {
|
|
670
|
+
const turnOptions: Record<string, unknown>[] = [];
|
|
671
|
+
const adapter = makeAdapter((prompt, options) => {
|
|
672
|
+
if (prompt === hiddenInitPrompt()) {
|
|
673
|
+
return [systemInit(), result()];
|
|
674
|
+
}
|
|
675
|
+
turnOptions.push(options);
|
|
676
|
+
return [
|
|
677
|
+
systemInit(),
|
|
678
|
+
{
|
|
679
|
+
type: 'stream_event',
|
|
680
|
+
event: {
|
|
681
|
+
type: 'message_start',
|
|
682
|
+
message: {
|
|
683
|
+
id: 'msg_1',
|
|
684
|
+
type: 'message',
|
|
685
|
+
role: 'assistant',
|
|
686
|
+
model: 'sonnet',
|
|
687
|
+
content: [],
|
|
688
|
+
stop_reason: null,
|
|
689
|
+
stop_sequence: null,
|
|
690
|
+
stop_details: null,
|
|
691
|
+
usage: {} as any,
|
|
692
|
+
container: null,
|
|
693
|
+
context_management: null,
|
|
694
|
+
diagnostics: null,
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
parent_tool_use_id: null,
|
|
698
|
+
uuid: '00000000-0000-4000-8000-000000000009' as any,
|
|
699
|
+
session_id: 'claude-session-1',
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
type: 'stream_event',
|
|
703
|
+
event: {
|
|
704
|
+
type: 'content_block_start',
|
|
705
|
+
index: 0,
|
|
706
|
+
content_block: {
|
|
707
|
+
type: 'thinking',
|
|
708
|
+
thinking: 'Plan',
|
|
709
|
+
signature: 'sig',
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
parent_tool_use_id: null,
|
|
713
|
+
uuid: '00000000-0000-4000-8000-000000000008' as any,
|
|
714
|
+
session_id: 'claude-session-1',
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
type: 'stream_event',
|
|
718
|
+
event: {
|
|
719
|
+
type: 'content_block_delta',
|
|
720
|
+
index: 0,
|
|
721
|
+
delta: { type: 'thinking_delta', thinking: ' carefully' },
|
|
722
|
+
},
|
|
723
|
+
parent_tool_use_id: null,
|
|
724
|
+
uuid: '00000000-0000-4000-8000-000000000018' as any,
|
|
725
|
+
session_id: 'claude-session-1',
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
type: 'stream_event',
|
|
729
|
+
event: {
|
|
730
|
+
type: 'content_block_delta',
|
|
731
|
+
index: 1,
|
|
732
|
+
delta: { type: 'text_delta', text: 'Hel' },
|
|
733
|
+
},
|
|
734
|
+
parent_tool_use_id: null,
|
|
735
|
+
uuid: '00000000-0000-4000-8000-000000000010' as any,
|
|
736
|
+
session_id: 'claude-session-1',
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
type: 'stream_event',
|
|
740
|
+
event: {
|
|
741
|
+
type: 'content_block_delta',
|
|
742
|
+
index: 1,
|
|
743
|
+
delta: { type: 'text_delta', text: 'lo' },
|
|
744
|
+
},
|
|
745
|
+
parent_tool_use_id: null,
|
|
746
|
+
uuid: '00000000-0000-4000-8000-000000000011' as any,
|
|
747
|
+
session_id: 'claude-session-1',
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
type: 'stream_event',
|
|
751
|
+
event: {
|
|
752
|
+
type: 'message_start',
|
|
753
|
+
message: {
|
|
754
|
+
id: 'msg_2',
|
|
755
|
+
type: 'message',
|
|
756
|
+
role: 'assistant',
|
|
757
|
+
model: 'sonnet',
|
|
758
|
+
content: [],
|
|
759
|
+
stop_reason: null,
|
|
760
|
+
stop_sequence: null,
|
|
761
|
+
stop_details: null,
|
|
762
|
+
usage: {} as any,
|
|
763
|
+
container: null,
|
|
764
|
+
context_management: null,
|
|
765
|
+
diagnostics: null,
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
parent_tool_use_id: null,
|
|
769
|
+
uuid: '00000000-0000-4000-8000-000000000012' as any,
|
|
770
|
+
session_id: 'claude-session-1',
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
type: 'stream_event',
|
|
774
|
+
event: {
|
|
775
|
+
type: 'content_block_start',
|
|
776
|
+
index: 1,
|
|
777
|
+
content_block: {
|
|
778
|
+
type: 'tool_use',
|
|
779
|
+
id: 'toolu_1',
|
|
780
|
+
name: 'Bash',
|
|
781
|
+
input: {},
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
parent_tool_use_id: null,
|
|
785
|
+
uuid: '00000000-0000-4000-8000-000000000013' as any,
|
|
786
|
+
session_id: 'claude-session-1',
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
type: 'assistant',
|
|
790
|
+
message: {
|
|
791
|
+
id: 'msg_2',
|
|
792
|
+
type: 'message',
|
|
793
|
+
role: 'assistant',
|
|
794
|
+
model: 'sonnet',
|
|
795
|
+
content: [
|
|
796
|
+
{
|
|
797
|
+
type: 'tool_use',
|
|
798
|
+
id: 'toolu_1',
|
|
799
|
+
name: 'Bash',
|
|
800
|
+
input: { command: 'pwd' },
|
|
801
|
+
caller: { type: 'direct' },
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
stop_reason: null,
|
|
805
|
+
stop_sequence: null,
|
|
806
|
+
stop_details: null,
|
|
807
|
+
usage: {} as any,
|
|
808
|
+
container: null,
|
|
809
|
+
context_management: null,
|
|
810
|
+
diagnostics: null,
|
|
811
|
+
},
|
|
812
|
+
parent_tool_use_id: null,
|
|
813
|
+
uuid: '00000000-0000-4000-8000-000000000014' as any,
|
|
814
|
+
session_id: 'claude-session-1',
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
type: 'user',
|
|
818
|
+
message: {
|
|
819
|
+
role: 'user',
|
|
820
|
+
content: [
|
|
821
|
+
{
|
|
822
|
+
type: 'tool_result',
|
|
823
|
+
tool_use_id: 'toolu_1',
|
|
824
|
+
content: '/tmp/workspace\n',
|
|
825
|
+
},
|
|
826
|
+
],
|
|
827
|
+
},
|
|
828
|
+
parent_tool_use_id: null,
|
|
829
|
+
tool_use_result: { stdout: '/tmp/workspace\n' },
|
|
830
|
+
uuid: '00000000-0000-4000-8000-000000000015' as any,
|
|
831
|
+
session_id: 'claude-session-1',
|
|
832
|
+
},
|
|
833
|
+
result(),
|
|
834
|
+
];
|
|
835
|
+
});
|
|
836
|
+
const events: AgentRuntimeEvent[] = [];
|
|
837
|
+
adapter.on('event', (event) => events.push(event));
|
|
838
|
+
|
|
839
|
+
const started = await adapter.startTurn({
|
|
840
|
+
providerSessionId: 'claude-session-1',
|
|
841
|
+
prompt: 'Say hello and run pwd',
|
|
842
|
+
model: 'sonnet',
|
|
843
|
+
reasoningEffort: 'xhigh',
|
|
844
|
+
workspacePath: '/tmp/workspace',
|
|
845
|
+
});
|
|
846
|
+
expect(started.status).toBe('inProgress');
|
|
847
|
+
expect(started.startedAt).toEqual(expect.any(String));
|
|
848
|
+
await wait();
|
|
849
|
+
|
|
850
|
+
expect(events.map((event) => event.type)).toContain('turn.started');
|
|
851
|
+
expect(
|
|
852
|
+
events
|
|
853
|
+
.filter((event) => event.type === 'output.delta')
|
|
854
|
+
.map((event) => event.itemId),
|
|
855
|
+
).toEqual(['msg_1:content:1', 'msg_1:content:1']);
|
|
856
|
+
expect(
|
|
857
|
+
events
|
|
858
|
+
.filter((event) => event.type === 'output.delta')
|
|
859
|
+
.map((event) => event.delta)
|
|
860
|
+
.join(''),
|
|
861
|
+
).toBe('Hello');
|
|
862
|
+
expect(turnOptions.at(-1)?.effort).toBe('max');
|
|
863
|
+
expect(turnOptions.at(-1)?.thinking).toEqual({
|
|
864
|
+
type: 'adaptive',
|
|
865
|
+
display: 'summarized',
|
|
866
|
+
});
|
|
867
|
+
const firstReasoningEventIndex = events.findIndex(
|
|
868
|
+
(event) =>
|
|
869
|
+
event.type === 'item.started' &&
|
|
870
|
+
event.item.kind === 'reasoning' &&
|
|
871
|
+
event.item.text === 'Plan',
|
|
872
|
+
);
|
|
873
|
+
const firstOutputDeltaEventIndex = events.findIndex(
|
|
874
|
+
(event) => event.type === 'output.delta',
|
|
875
|
+
);
|
|
876
|
+
expect(firstReasoningEventIndex).toBeGreaterThanOrEqual(0);
|
|
877
|
+
expect(firstOutputDeltaEventIndex).toBeGreaterThan(firstReasoningEventIndex);
|
|
878
|
+
expect(events).toContainEqual(
|
|
879
|
+
expect.objectContaining({
|
|
880
|
+
type: 'item.completed',
|
|
881
|
+
item: expect.objectContaining({
|
|
882
|
+
kind: 'reasoning',
|
|
883
|
+
text: 'Plan carefully',
|
|
884
|
+
}),
|
|
885
|
+
}),
|
|
886
|
+
);
|
|
887
|
+
expect(events).toContainEqual(
|
|
888
|
+
expect.objectContaining({
|
|
889
|
+
type: 'item.started',
|
|
890
|
+
item: expect.objectContaining({
|
|
891
|
+
kind: 'commandExecution',
|
|
892
|
+
text: 'pwd',
|
|
893
|
+
}),
|
|
894
|
+
}),
|
|
895
|
+
);
|
|
896
|
+
expect(events).toContainEqual(
|
|
897
|
+
expect.objectContaining({
|
|
898
|
+
type: 'item.completed',
|
|
899
|
+
item: expect.objectContaining({
|
|
900
|
+
id: 'toolu_1',
|
|
901
|
+
status: 'completed',
|
|
902
|
+
}),
|
|
903
|
+
}),
|
|
904
|
+
);
|
|
905
|
+
expect(events.at(-1)).toMatchObject({
|
|
906
|
+
type: 'turn.completed',
|
|
907
|
+
turn: {
|
|
908
|
+
startedAt: started.startedAt,
|
|
909
|
+
status: 'completed',
|
|
910
|
+
items: expect.arrayContaining([
|
|
911
|
+
expect.objectContaining({ kind: 'userMessage' }),
|
|
912
|
+
expect.objectContaining({ kind: 'reasoning', text: 'Plan carefully' }),
|
|
913
|
+
expect.objectContaining({ kind: 'agentMessage', text: 'Hello' }),
|
|
914
|
+
expect.objectContaining({ kind: 'commandExecution', status: 'completed' }),
|
|
915
|
+
]),
|
|
916
|
+
},
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it('emits Claude token usage before completing a turn', async () => {
|
|
921
|
+
const adapter = makeAdapter((prompt) => {
|
|
922
|
+
if (prompt === hiddenInitPrompt()) {
|
|
923
|
+
return [systemInit(), result()];
|
|
924
|
+
}
|
|
925
|
+
return [
|
|
926
|
+
systemInit(),
|
|
927
|
+
{
|
|
928
|
+
type: 'assistant',
|
|
929
|
+
message: {
|
|
930
|
+
id: 'msg_usage',
|
|
931
|
+
type: 'message',
|
|
932
|
+
role: 'assistant',
|
|
933
|
+
model: 'sonnet',
|
|
934
|
+
content: [{ type: 'text', text: 'Done', citations: null }],
|
|
935
|
+
stop_reason: null,
|
|
936
|
+
stop_sequence: null,
|
|
937
|
+
stop_details: null,
|
|
938
|
+
usage: {
|
|
939
|
+
input_tokens: 3,
|
|
940
|
+
cache_creation_input_tokens: 10,
|
|
941
|
+
cache_read_input_tokens: 100,
|
|
942
|
+
output_tokens: 20,
|
|
943
|
+
} as any,
|
|
944
|
+
container: null,
|
|
945
|
+
context_management: null,
|
|
946
|
+
diagnostics: null,
|
|
947
|
+
},
|
|
948
|
+
parent_tool_use_id: null,
|
|
949
|
+
uuid: '00000000-0000-4000-8000-000000000040' as any,
|
|
950
|
+
session_id: 'claude-session-1',
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
...result(),
|
|
954
|
+
usage: {
|
|
955
|
+
input_tokens: 4,
|
|
956
|
+
cache_creation_input_tokens: 11,
|
|
957
|
+
cache_read_input_tokens: 101,
|
|
958
|
+
output_tokens: 21,
|
|
959
|
+
} as any,
|
|
960
|
+
modelUsage: {
|
|
961
|
+
sonnet: {
|
|
962
|
+
inputTokens: 4,
|
|
963
|
+
outputTokens: 21,
|
|
964
|
+
cacheReadInputTokens: 101,
|
|
965
|
+
cacheCreationInputTokens: 11,
|
|
966
|
+
webSearchRequests: 0,
|
|
967
|
+
costUSD: 0.001,
|
|
968
|
+
contextWindow: 200000,
|
|
969
|
+
maxOutputTokens: 32000,
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
} as SDKMessage,
|
|
973
|
+
];
|
|
974
|
+
});
|
|
975
|
+
const events: AgentRuntimeEvent[] = [];
|
|
976
|
+
adapter.on('event', (event) => events.push(event));
|
|
977
|
+
|
|
978
|
+
await adapter.startTurn({
|
|
979
|
+
providerSessionId: 'claude-session-1',
|
|
980
|
+
prompt: 'Measure usage',
|
|
981
|
+
model: 'sonnet',
|
|
982
|
+
workspacePath: '/tmp/workspace',
|
|
983
|
+
});
|
|
984
|
+
await wait();
|
|
985
|
+
|
|
986
|
+
const usageEvent = events.find((event) => event.type === 'usage.updated');
|
|
987
|
+
expect(usageEvent).toMatchObject({
|
|
988
|
+
type: 'usage.updated',
|
|
989
|
+
provider: 'claude',
|
|
990
|
+
providerSessionId: 'claude-session-1',
|
|
991
|
+
usage: {
|
|
992
|
+
total: {
|
|
993
|
+
totalTokens: 137,
|
|
994
|
+
inputTokens: 116,
|
|
995
|
+
cachedInputTokens: 101,
|
|
996
|
+
outputTokens: 21,
|
|
997
|
+
reasoningOutputTokens: 0,
|
|
998
|
+
},
|
|
999
|
+
last: {
|
|
1000
|
+
totalTokens: 137,
|
|
1001
|
+
inputTokens: 116,
|
|
1002
|
+
cachedInputTokens: 101,
|
|
1003
|
+
outputTokens: 21,
|
|
1004
|
+
reasoningOutputTokens: 0,
|
|
1005
|
+
},
|
|
1006
|
+
modelContextWindow: 200000,
|
|
1007
|
+
cumulative: false,
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
expect(events.map((event) => event.type).slice(-2)).toEqual([
|
|
1011
|
+
'usage.updated',
|
|
1012
|
+
'turn.completed',
|
|
1013
|
+
]);
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
it('uses Claude plan permission mode and maps ExitPlanMode to a plan item', async () => {
|
|
1017
|
+
const turnOptions: Record<string, unknown>[] = [];
|
|
1018
|
+
const adapter = makeAdapter((_prompt, options) => {
|
|
1019
|
+
turnOptions.push(options);
|
|
1020
|
+
return [
|
|
1021
|
+
systemInit(),
|
|
1022
|
+
{
|
|
1023
|
+
type: 'assistant',
|
|
1024
|
+
message: {
|
|
1025
|
+
id: 'msg_plan',
|
|
1026
|
+
type: 'message',
|
|
1027
|
+
role: 'assistant',
|
|
1028
|
+
model: 'sonnet',
|
|
1029
|
+
content: [
|
|
1030
|
+
{
|
|
1031
|
+
type: 'tool_use',
|
|
1032
|
+
id: 'toolu_plan',
|
|
1033
|
+
name: 'ExitPlanMode',
|
|
1034
|
+
input: { plan: '# Plan\n\n- Inspect.\n- Patch.\n- Verify.' },
|
|
1035
|
+
caller: { type: 'direct' },
|
|
1036
|
+
},
|
|
1037
|
+
],
|
|
1038
|
+
stop_reason: null,
|
|
1039
|
+
stop_sequence: null,
|
|
1040
|
+
stop_details: null,
|
|
1041
|
+
usage: {} as any,
|
|
1042
|
+
container: null,
|
|
1043
|
+
context_management: null,
|
|
1044
|
+
diagnostics: null,
|
|
1045
|
+
},
|
|
1046
|
+
parent_tool_use_id: null,
|
|
1047
|
+
uuid: '00000000-0000-4000-8000-000000000021' as any,
|
|
1048
|
+
session_id: 'claude-session-1',
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
type: 'user',
|
|
1052
|
+
message: {
|
|
1053
|
+
role: 'user',
|
|
1054
|
+
content: [
|
|
1055
|
+
{
|
|
1056
|
+
type: 'tool_result',
|
|
1057
|
+
tool_use_id: 'toolu_plan',
|
|
1058
|
+
content: '# Plan\n\n- Inspect.\n- Patch.\n- Verify.',
|
|
1059
|
+
},
|
|
1060
|
+
],
|
|
1061
|
+
},
|
|
1062
|
+
parent_tool_use_id: null,
|
|
1063
|
+
tool_use_result: {
|
|
1064
|
+
plan: '# Plan\n\n- Inspect.\n- Patch.\n- Verify.',
|
|
1065
|
+
isAgent: false,
|
|
1066
|
+
},
|
|
1067
|
+
uuid: '00000000-0000-4000-8000-000000000022' as any,
|
|
1068
|
+
session_id: 'claude-session-1',
|
|
1069
|
+
} satisfies SDKMessage,
|
|
1070
|
+
result(),
|
|
1071
|
+
];
|
|
1072
|
+
});
|
|
1073
|
+
const events: AgentRuntimeEvent[] = [];
|
|
1074
|
+
adapter.on('event', (event) => events.push(event));
|
|
1075
|
+
|
|
1076
|
+
const started = await adapter.startTurn({
|
|
1077
|
+
providerSessionId: 'claude-session-1',
|
|
1078
|
+
prompt: 'Plan the next change',
|
|
1079
|
+
model: 'sonnet',
|
|
1080
|
+
collaborationMode: 'plan',
|
|
1081
|
+
workspacePath: '/tmp/workspace',
|
|
1082
|
+
});
|
|
1083
|
+
await wait();
|
|
1084
|
+
|
|
1085
|
+
expect(started.startedAt).toEqual(expect.any(String));
|
|
1086
|
+
expect(turnOptions.at(-1)?.permissionMode).toBe('plan');
|
|
1087
|
+
expect(events.at(-1)).toMatchObject({
|
|
1088
|
+
type: 'turn.completed',
|
|
1089
|
+
turn: {
|
|
1090
|
+
startedAt: started.startedAt,
|
|
1091
|
+
status: 'completed',
|
|
1092
|
+
items: expect.arrayContaining([
|
|
1093
|
+
expect.objectContaining({
|
|
1094
|
+
id: 'toolu_plan',
|
|
1095
|
+
kind: 'plan',
|
|
1096
|
+
text: '# Plan\n\n- Inspect.\n- Patch.\n- Verify.',
|
|
1097
|
+
status: 'completed',
|
|
1098
|
+
}),
|
|
1099
|
+
]),
|
|
1100
|
+
},
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('suppresses Claude Code plan control tool plumbing from timeline items', async () => {
|
|
1105
|
+
const adapter = makeAdapter(() => [
|
|
1106
|
+
systemInit(),
|
|
1107
|
+
{
|
|
1108
|
+
type: 'assistant',
|
|
1109
|
+
message: {
|
|
1110
|
+
id: 'msg_plan_tools',
|
|
1111
|
+
type: 'message',
|
|
1112
|
+
role: 'assistant',
|
|
1113
|
+
model: 'sonnet',
|
|
1114
|
+
content: [
|
|
1115
|
+
{
|
|
1116
|
+
type: 'tool_use',
|
|
1117
|
+
id: 'toolu_search',
|
|
1118
|
+
name: 'ToolSearch',
|
|
1119
|
+
input: { query: 'select:EnterPlanMode', max_results: 1 },
|
|
1120
|
+
caller: { type: 'direct' },
|
|
1121
|
+
},
|
|
1122
|
+
],
|
|
1123
|
+
stop_reason: null,
|
|
1124
|
+
stop_sequence: null,
|
|
1125
|
+
stop_details: null,
|
|
1126
|
+
usage: {} as any,
|
|
1127
|
+
container: null,
|
|
1128
|
+
context_management: null,
|
|
1129
|
+
diagnostics: null,
|
|
1130
|
+
},
|
|
1131
|
+
parent_tool_use_id: null,
|
|
1132
|
+
uuid: '00000000-0000-4000-8000-000000000041' as any,
|
|
1133
|
+
session_id: 'claude-session-1',
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
type: 'user',
|
|
1137
|
+
message: {
|
|
1138
|
+
role: 'user',
|
|
1139
|
+
content: [
|
|
1140
|
+
{
|
|
1141
|
+
type: 'tool_result',
|
|
1142
|
+
tool_use_id: 'toolu_search',
|
|
1143
|
+
content: [{ type: 'tool_reference', tool_name: 'EnterPlanMode' }],
|
|
1144
|
+
},
|
|
1145
|
+
],
|
|
1146
|
+
},
|
|
1147
|
+
parent_tool_use_id: null,
|
|
1148
|
+
tool_use_result: {
|
|
1149
|
+
matches: ['EnterPlanMode'],
|
|
1150
|
+
query: 'select:EnterPlanMode',
|
|
1151
|
+
},
|
|
1152
|
+
uuid: '00000000-0000-4000-8000-000000000042' as any,
|
|
1153
|
+
session_id: 'claude-session-1',
|
|
1154
|
+
} satisfies SDKMessage,
|
|
1155
|
+
{
|
|
1156
|
+
type: 'assistant',
|
|
1157
|
+
message: {
|
|
1158
|
+
id: 'msg_enter_plan',
|
|
1159
|
+
type: 'message',
|
|
1160
|
+
role: 'assistant',
|
|
1161
|
+
model: 'sonnet',
|
|
1162
|
+
content: [
|
|
1163
|
+
{
|
|
1164
|
+
type: 'tool_use',
|
|
1165
|
+
id: 'toolu_enter',
|
|
1166
|
+
name: 'EnterPlanMode',
|
|
1167
|
+
input: {},
|
|
1168
|
+
caller: { type: 'direct' },
|
|
1169
|
+
},
|
|
1170
|
+
],
|
|
1171
|
+
stop_reason: null,
|
|
1172
|
+
stop_sequence: null,
|
|
1173
|
+
stop_details: null,
|
|
1174
|
+
usage: {} as any,
|
|
1175
|
+
container: null,
|
|
1176
|
+
context_management: null,
|
|
1177
|
+
diagnostics: null,
|
|
1178
|
+
},
|
|
1179
|
+
parent_tool_use_id: null,
|
|
1180
|
+
uuid: '00000000-0000-4000-8000-000000000043' as any,
|
|
1181
|
+
session_id: 'claude-session-1',
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
type: 'user',
|
|
1185
|
+
message: {
|
|
1186
|
+
role: 'user',
|
|
1187
|
+
content: [
|
|
1188
|
+
{
|
|
1189
|
+
type: 'tool_result',
|
|
1190
|
+
tool_use_id: 'toolu_enter',
|
|
1191
|
+
content: 'Entered plan mode.',
|
|
1192
|
+
},
|
|
1193
|
+
],
|
|
1194
|
+
},
|
|
1195
|
+
parent_tool_use_id: null,
|
|
1196
|
+
tool_use_result: { message: 'Entered plan mode.' },
|
|
1197
|
+
uuid: '00000000-0000-4000-8000-000000000044' as any,
|
|
1198
|
+
session_id: 'claude-session-1',
|
|
1199
|
+
} satisfies SDKMessage,
|
|
1200
|
+
{
|
|
1201
|
+
type: 'assistant',
|
|
1202
|
+
message: {
|
|
1203
|
+
id: 'msg_plan',
|
|
1204
|
+
type: 'message',
|
|
1205
|
+
role: 'assistant',
|
|
1206
|
+
model: 'sonnet',
|
|
1207
|
+
content: [
|
|
1208
|
+
{
|
|
1209
|
+
type: 'tool_use',
|
|
1210
|
+
id: 'toolu_plan',
|
|
1211
|
+
name: 'ExitPlanMode',
|
|
1212
|
+
input: { plan: 'Build a calculator.' },
|
|
1213
|
+
caller: { type: 'direct' },
|
|
1214
|
+
},
|
|
1215
|
+
],
|
|
1216
|
+
stop_reason: null,
|
|
1217
|
+
stop_sequence: null,
|
|
1218
|
+
stop_details: null,
|
|
1219
|
+
usage: {} as any,
|
|
1220
|
+
container: null,
|
|
1221
|
+
context_management: null,
|
|
1222
|
+
diagnostics: null,
|
|
1223
|
+
},
|
|
1224
|
+
parent_tool_use_id: null,
|
|
1225
|
+
uuid: '00000000-0000-4000-8000-000000000045' as any,
|
|
1226
|
+
session_id: 'claude-session-1',
|
|
1227
|
+
},
|
|
1228
|
+
result(),
|
|
1229
|
+
]);
|
|
1230
|
+
const events: AgentRuntimeEvent[] = [];
|
|
1231
|
+
adapter.on('event', (event) => events.push(event));
|
|
1232
|
+
|
|
1233
|
+
await adapter.startTurn({
|
|
1234
|
+
providerSessionId: 'claude-session-1',
|
|
1235
|
+
prompt: 'Plan a calculator',
|
|
1236
|
+
model: 'sonnet',
|
|
1237
|
+
collaborationMode: 'plan',
|
|
1238
|
+
workspacePath: '/tmp/workspace',
|
|
1239
|
+
});
|
|
1240
|
+
await wait();
|
|
1241
|
+
|
|
1242
|
+
const completed = events.at(-1);
|
|
1243
|
+
expect(completed).toMatchObject({ type: 'turn.completed' });
|
|
1244
|
+
expect(completed && 'turn' in completed ? completed.turn.items : []).toEqual(
|
|
1245
|
+
expect.arrayContaining([
|
|
1246
|
+
expect.objectContaining({
|
|
1247
|
+
id: 'toolu_plan',
|
|
1248
|
+
kind: 'plan',
|
|
1249
|
+
text: 'Build a calculator.',
|
|
1250
|
+
}),
|
|
1251
|
+
]),
|
|
1252
|
+
);
|
|
1253
|
+
expect(completed && 'turn' in completed ? completed.turn.items : []).toEqual(
|
|
1254
|
+
expect.not.arrayContaining([
|
|
1255
|
+
expect.objectContaining({ id: 'toolu_search' }),
|
|
1256
|
+
expect.objectContaining({ id: 'toolu_enter' }),
|
|
1257
|
+
expect.objectContaining({ text: expect.stringContaining('ToolSearch') }),
|
|
1258
|
+
expect.objectContaining({ text: expect.stringContaining('EnterPlanMode') }),
|
|
1259
|
+
]),
|
|
1260
|
+
);
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
it('maps Claude AskUserQuestion tool use to a provider request', async () => {
|
|
1264
|
+
const adapter = makeAdapter(() => [
|
|
1265
|
+
systemInit(),
|
|
1266
|
+
{
|
|
1267
|
+
type: 'assistant',
|
|
1268
|
+
message: {
|
|
1269
|
+
id: 'msg_question',
|
|
1270
|
+
type: 'message',
|
|
1271
|
+
role: 'assistant',
|
|
1272
|
+
model: 'sonnet',
|
|
1273
|
+
content: [
|
|
1274
|
+
{
|
|
1275
|
+
type: 'text',
|
|
1276
|
+
text: 'I need one choice before continuing.',
|
|
1277
|
+
citations: null,
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
type: 'tool_use',
|
|
1281
|
+
id: 'toolu_question',
|
|
1282
|
+
name: 'AskUserQuestion',
|
|
1283
|
+
input: {
|
|
1284
|
+
questions: [
|
|
1285
|
+
{
|
|
1286
|
+
header: 'Mode',
|
|
1287
|
+
question: 'Which plan style should I use?',
|
|
1288
|
+
multiSelect: false,
|
|
1289
|
+
options: [
|
|
1290
|
+
{
|
|
1291
|
+
label: 'Short',
|
|
1292
|
+
description: 'Keep the plan concise.',
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
label: 'Detailed',
|
|
1296
|
+
description: 'Include more context.',
|
|
1297
|
+
},
|
|
1298
|
+
],
|
|
1299
|
+
},
|
|
1300
|
+
],
|
|
1301
|
+
},
|
|
1302
|
+
caller: { type: 'direct' },
|
|
1303
|
+
},
|
|
1304
|
+
],
|
|
1305
|
+
stop_reason: null,
|
|
1306
|
+
stop_sequence: null,
|
|
1307
|
+
stop_details: null,
|
|
1308
|
+
usage: {} as any,
|
|
1309
|
+
container: null,
|
|
1310
|
+
context_management: null,
|
|
1311
|
+
diagnostics: null,
|
|
1312
|
+
},
|
|
1313
|
+
parent_tool_use_id: null,
|
|
1314
|
+
uuid: '00000000-0000-4000-8000-000000000031' as any,
|
|
1315
|
+
session_id: 'claude-session-1',
|
|
1316
|
+
},
|
|
1317
|
+
result(),
|
|
1318
|
+
]);
|
|
1319
|
+
const events: AgentRuntimeEvent[] = [];
|
|
1320
|
+
const providerRequests: unknown[] = [];
|
|
1321
|
+
adapter.on('event', (event) => events.push(event));
|
|
1322
|
+
adapter.on('provider-request', (request) => providerRequests.push(request));
|
|
1323
|
+
|
|
1324
|
+
await adapter.startTurn({
|
|
1325
|
+
providerSessionId: 'claude-session-1',
|
|
1326
|
+
prompt: 'Ask me a plan question',
|
|
1327
|
+
model: 'sonnet',
|
|
1328
|
+
collaborationMode: 'plan',
|
|
1329
|
+
workspacePath: '/tmp/workspace',
|
|
1330
|
+
});
|
|
1331
|
+
await wait();
|
|
1332
|
+
|
|
1333
|
+
expect(providerRequests).toEqual([
|
|
1334
|
+
expect.objectContaining({
|
|
1335
|
+
provider: 'claude',
|
|
1336
|
+
id: 'toolu_question',
|
|
1337
|
+
method: 'tool/AskUserQuestion',
|
|
1338
|
+
params: expect.objectContaining({
|
|
1339
|
+
providerSessionId: 'claude-session-1',
|
|
1340
|
+
toolUseId: 'toolu_question',
|
|
1341
|
+
input: expect.objectContaining({
|
|
1342
|
+
questions: expect.any(Array),
|
|
1343
|
+
}),
|
|
1344
|
+
}),
|
|
1345
|
+
}),
|
|
1346
|
+
]);
|
|
1347
|
+
const mapping = adapter.mapProviderRequest?.(providerRequests[0] as any, {
|
|
1348
|
+
approvalMode: 'guarded',
|
|
1349
|
+
});
|
|
1350
|
+
expect(mapping).toMatchObject({
|
|
1351
|
+
providerSessionId: 'claude-session-1',
|
|
1352
|
+
pendingRequest: {
|
|
1353
|
+
responseKind: 'askUserQuestion',
|
|
1354
|
+
request: {
|
|
1355
|
+
kind: 'requestUserInput',
|
|
1356
|
+
title: 'Mode',
|
|
1357
|
+
description: 'Which plan style should I use?',
|
|
1358
|
+
itemId: 'toolu_question',
|
|
1359
|
+
questions: [
|
|
1360
|
+
{
|
|
1361
|
+
id: 'question-1',
|
|
1362
|
+
header: 'Mode',
|
|
1363
|
+
question: 'Which plan style should I use?',
|
|
1364
|
+
isOther: true,
|
|
1365
|
+
options: [
|
|
1366
|
+
{
|
|
1367
|
+
label: 'Short',
|
|
1368
|
+
description: 'Keep the plan concise.',
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
label: 'Detailed',
|
|
1372
|
+
description: 'Include more context.',
|
|
1373
|
+
},
|
|
1374
|
+
],
|
|
1375
|
+
},
|
|
1376
|
+
],
|
|
1377
|
+
},
|
|
1378
|
+
},
|
|
1379
|
+
});
|
|
1380
|
+
expect(
|
|
1381
|
+
adapter.buildProviderRequestResponse?.(mapping!.pendingRequest!, {
|
|
1382
|
+
answers: {
|
|
1383
|
+
'question-1': {
|
|
1384
|
+
answers: ['Short'],
|
|
1385
|
+
},
|
|
1386
|
+
},
|
|
1387
|
+
}),
|
|
1388
|
+
).toMatchObject({
|
|
1389
|
+
answers: {
|
|
1390
|
+
'Which plan style should I use?': 'Short',
|
|
1391
|
+
},
|
|
1392
|
+
toolResult: {
|
|
1393
|
+
answers: {
|
|
1394
|
+
'Which plan style should I use?': 'Short',
|
|
1395
|
+
},
|
|
1396
|
+
},
|
|
1397
|
+
});
|
|
1398
|
+
expect(events.at(-1)).toMatchObject({
|
|
1399
|
+
type: 'turn.completed',
|
|
1400
|
+
turn: {
|
|
1401
|
+
items: expect.not.arrayContaining([
|
|
1402
|
+
expect.objectContaining({
|
|
1403
|
+
kind: 'toolCall',
|
|
1404
|
+
text: expect.stringContaining('AskUserQuestion'),
|
|
1405
|
+
}),
|
|
1406
|
+
]),
|
|
1407
|
+
},
|
|
1408
|
+
});
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
it('emits hidden continuation events on the requested display turn id', async () => {
|
|
1412
|
+
const adapter = makeAdapter(() => [
|
|
1413
|
+
systemInit(),
|
|
1414
|
+
{
|
|
1415
|
+
type: 'assistant',
|
|
1416
|
+
message: {
|
|
1417
|
+
id: 'msg_continuation',
|
|
1418
|
+
type: 'message',
|
|
1419
|
+
role: 'assistant',
|
|
1420
|
+
model: 'sonnet',
|
|
1421
|
+
content: [{ type: 'text', text: 'Continuing the same plan.', citations: null }],
|
|
1422
|
+
stop_reason: null,
|
|
1423
|
+
stop_sequence: null,
|
|
1424
|
+
stop_details: null,
|
|
1425
|
+
usage: {} as any,
|
|
1426
|
+
container: null,
|
|
1427
|
+
context_management: null,
|
|
1428
|
+
diagnostics: null,
|
|
1429
|
+
},
|
|
1430
|
+
parent_tool_use_id: null,
|
|
1431
|
+
uuid: '00000000-0000-4000-8000-000000000034' as any,
|
|
1432
|
+
session_id: 'claude-session-1',
|
|
1433
|
+
},
|
|
1434
|
+
result(),
|
|
1435
|
+
]);
|
|
1436
|
+
const events: AgentRuntimeEvent[] = [];
|
|
1437
|
+
adapter.on('event', (event) => events.push(event));
|
|
1438
|
+
|
|
1439
|
+
const started = await adapter.startTurn({
|
|
1440
|
+
providerSessionId: 'claude-session-1',
|
|
1441
|
+
prompt: 'Hidden continuation',
|
|
1442
|
+
model: 'sonnet',
|
|
1443
|
+
collaborationMode: 'plan',
|
|
1444
|
+
workspacePath: '/tmp/workspace',
|
|
1445
|
+
hidden: true,
|
|
1446
|
+
displayTurnId: 'claude-turn-visible',
|
|
1447
|
+
});
|
|
1448
|
+
await wait();
|
|
1449
|
+
|
|
1450
|
+
expect(started).toMatchObject({
|
|
1451
|
+
providerTurnId: 'claude-turn-visible',
|
|
1452
|
+
items: [],
|
|
1453
|
+
});
|
|
1454
|
+
expect(events).toEqual(
|
|
1455
|
+
expect.arrayContaining([
|
|
1456
|
+
expect.objectContaining({
|
|
1457
|
+
type: 'turn.started',
|
|
1458
|
+
turn: expect.objectContaining({
|
|
1459
|
+
providerTurnId: 'claude-turn-visible',
|
|
1460
|
+
items: [],
|
|
1461
|
+
}),
|
|
1462
|
+
}),
|
|
1463
|
+
expect.objectContaining({
|
|
1464
|
+
type: 'turn.completed',
|
|
1465
|
+
turn: expect.objectContaining({
|
|
1466
|
+
providerTurnId: 'claude-turn-visible',
|
|
1467
|
+
items: expect.arrayContaining([
|
|
1468
|
+
expect.objectContaining({
|
|
1469
|
+
kind: 'agentMessage',
|
|
1470
|
+
text: 'Continuing the same plan.',
|
|
1471
|
+
}),
|
|
1472
|
+
]),
|
|
1473
|
+
}),
|
|
1474
|
+
}),
|
|
1475
|
+
]),
|
|
1476
|
+
);
|
|
1477
|
+
expect(
|
|
1478
|
+
events
|
|
1479
|
+
.map((event) =>
|
|
1480
|
+
event.type === 'turn.started' || event.type === 'turn.completed'
|
|
1481
|
+
? event.turn.providerTurnId
|
|
1482
|
+
: 'providerTurnId' in event
|
|
1483
|
+
? event.providerTurnId
|
|
1484
|
+
: null,
|
|
1485
|
+
)
|
|
1486
|
+
.filter(Boolean),
|
|
1487
|
+
).toEqual(['claude-turn-visible', 'claude-turn-visible']);
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
it('interrupts an active query', async () => {
|
|
1491
|
+
let activeQuery: FakeQuery | null = null;
|
|
1492
|
+
const adapter = makeAdapter(() => {
|
|
1493
|
+
activeQuery = new FakeQuery([systemInit()], { holdOpen: true });
|
|
1494
|
+
return activeQuery;
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
const started = await adapter.startTurn({
|
|
1498
|
+
providerSessionId: 'claude-session-1',
|
|
1499
|
+
prompt: 'Keep running',
|
|
1500
|
+
model: 'sonnet',
|
|
1501
|
+
workspacePath: '/tmp/workspace',
|
|
1502
|
+
});
|
|
1503
|
+
const interrupted = await adapter.interruptTurn({
|
|
1504
|
+
providerSessionId: 'claude-session-1',
|
|
1505
|
+
providerTurnId: started.providerTurnId,
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
expect(interrupted).toMatchObject({
|
|
1509
|
+
providerTurnId: started.providerTurnId,
|
|
1510
|
+
status: 'interrupted',
|
|
1511
|
+
});
|
|
1512
|
+
const capturedQuery = activeQuery as unknown as FakeQuery | null;
|
|
1513
|
+
expect(capturedQuery).not.toBeNull();
|
|
1514
|
+
expect(capturedQuery!.interrupted).toBe(true);
|
|
1515
|
+
expect(capturedQuery!.closed).toBe(true);
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
it('maps historical session messages into turns without the hidden init prompt', async () => {
|
|
1519
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
1520
|
+
home: '/tmp/claude-home',
|
|
1521
|
+
query: (() => new FakeQuery([])) as any,
|
|
1522
|
+
getSessionInfo: (async () => ({
|
|
1523
|
+
sessionId: 'claude-session-1',
|
|
1524
|
+
summary: 'Existing session',
|
|
1525
|
+
lastModified: 1_772_000_000_000,
|
|
1526
|
+
createdAt: 1_771_000_000_000,
|
|
1527
|
+
cwd: '/tmp/workspace',
|
|
1528
|
+
})) as any,
|
|
1529
|
+
listSessions: (async () => []) as any,
|
|
1530
|
+
getSessionMessages: (async () => [
|
|
1531
|
+
{
|
|
1532
|
+
type: 'user',
|
|
1533
|
+
uuid: 'hidden-user',
|
|
1534
|
+
session_id: 'claude-session-1',
|
|
1535
|
+
message: { role: 'user', content: hiddenInitPrompt() },
|
|
1536
|
+
parent_tool_use_id: null,
|
|
1537
|
+
},
|
|
1538
|
+
{
|
|
1539
|
+
type: 'assistant',
|
|
1540
|
+
uuid: 'hidden-assistant',
|
|
1541
|
+
session_id: 'claude-session-1',
|
|
1542
|
+
message: { role: 'assistant', content: [{ type: 'text', text: 'Ready.' }] },
|
|
1543
|
+
parent_tool_use_id: null,
|
|
1544
|
+
},
|
|
1545
|
+
{
|
|
1546
|
+
type: 'user',
|
|
1547
|
+
uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
|
|
1548
|
+
session_id: 'claude-session-1',
|
|
1549
|
+
message: { role: 'user', content: 'Real prompt' },
|
|
1550
|
+
parent_tool_use_id: null,
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
type: 'assistant',
|
|
1554
|
+
uuid: 'assistant-1',
|
|
1555
|
+
session_id: 'claude-session-1',
|
|
1556
|
+
message: {
|
|
1557
|
+
role: 'assistant',
|
|
1558
|
+
content: [
|
|
1559
|
+
{
|
|
1560
|
+
type: 'tool_use',
|
|
1561
|
+
id: 'toolu_1',
|
|
1562
|
+
name: 'Bash',
|
|
1563
|
+
input: { command: 'pwd' },
|
|
1564
|
+
},
|
|
1565
|
+
],
|
|
1566
|
+
},
|
|
1567
|
+
parent_tool_use_id: null,
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
type: 'user',
|
|
1571
|
+
uuid: 'tool-result-1',
|
|
1572
|
+
session_id: 'claude-session-1',
|
|
1573
|
+
message: {
|
|
1574
|
+
role: 'user',
|
|
1575
|
+
content: [
|
|
1576
|
+
{
|
|
1577
|
+
type: 'tool_result',
|
|
1578
|
+
tool_use_id: 'toolu_1',
|
|
1579
|
+
content: '/tmp/workspace',
|
|
1580
|
+
is_error: false,
|
|
1581
|
+
},
|
|
1582
|
+
],
|
|
1583
|
+
},
|
|
1584
|
+
parent_tool_use_id: null,
|
|
1585
|
+
},
|
|
1586
|
+
{
|
|
1587
|
+
type: 'assistant',
|
|
1588
|
+
uuid: 'assistant-2',
|
|
1589
|
+
session_id: 'claude-session-1',
|
|
1590
|
+
message: { role: 'assistant', content: [{ type: 'text', text: 'Real answer' }] },
|
|
1591
|
+
parent_tool_use_id: null,
|
|
1592
|
+
},
|
|
1593
|
+
] satisfies SessionMessage[]) as any,
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
const session = await adapter.readSession('claude-session-1');
|
|
1597
|
+
expect(session.turns).toHaveLength(1);
|
|
1598
|
+
expect(session.turns[0]?.providerTurnId).toBe(
|
|
1599
|
+
'claude-turn-019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
|
|
1600
|
+
);
|
|
1601
|
+
expect(session.turns[0]?.startedAt).toBe('2026-05-20T17:03:35.740Z');
|
|
1602
|
+
expect(session.turns[0]?.items).toEqual([
|
|
1603
|
+
expect.objectContaining({ kind: 'userMessage', text: 'Real prompt' }),
|
|
1604
|
+
expect.objectContaining({ kind: 'commandExecution', text: 'pwd', status: 'completed' }),
|
|
1605
|
+
expect.objectContaining({ kind: 'agentMessage', text: 'Real answer' }),
|
|
1606
|
+
]);
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
it('omits Claude AskUserQuestion tool results from historical turns', async () => {
|
|
1610
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
1611
|
+
home: '/tmp/claude-home',
|
|
1612
|
+
query: (() => new FakeQuery([])) as any,
|
|
1613
|
+
getSessionInfo: (async () => ({
|
|
1614
|
+
sessionId: 'claude-session-1',
|
|
1615
|
+
summary: 'Existing session',
|
|
1616
|
+
lastModified: 1_772_000_000_000,
|
|
1617
|
+
createdAt: 1_771_000_000_000,
|
|
1618
|
+
cwd: '/tmp/workspace',
|
|
1619
|
+
})) as any,
|
|
1620
|
+
listSessions: (async () => []) as any,
|
|
1621
|
+
getSessionMessages: (async () => [
|
|
1622
|
+
{
|
|
1623
|
+
type: 'user',
|
|
1624
|
+
uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
|
|
1625
|
+
session_id: 'claude-session-1',
|
|
1626
|
+
message: { role: 'user', content: 'Ask me something.' },
|
|
1627
|
+
parent_tool_use_id: null,
|
|
1628
|
+
},
|
|
1629
|
+
{
|
|
1630
|
+
type: 'assistant',
|
|
1631
|
+
uuid: 'assistant-question',
|
|
1632
|
+
session_id: 'claude-session-1',
|
|
1633
|
+
message: {
|
|
1634
|
+
role: 'assistant',
|
|
1635
|
+
content: [
|
|
1636
|
+
{
|
|
1637
|
+
type: 'tool_use',
|
|
1638
|
+
id: 'toolu_question',
|
|
1639
|
+
name: 'AskUserQuestion',
|
|
1640
|
+
input: {
|
|
1641
|
+
questions: [
|
|
1642
|
+
{
|
|
1643
|
+
header: 'Mode',
|
|
1644
|
+
question: 'Which plan style should I use?',
|
|
1645
|
+
multiSelect: false,
|
|
1646
|
+
options: [
|
|
1647
|
+
{ label: 'Short', description: 'Keep the plan concise.' },
|
|
1648
|
+
{ label: 'Detailed', description: 'Include more context.' },
|
|
1649
|
+
],
|
|
1650
|
+
},
|
|
1651
|
+
],
|
|
1652
|
+
},
|
|
1653
|
+
},
|
|
1654
|
+
],
|
|
1655
|
+
},
|
|
1656
|
+
parent_tool_use_id: null,
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
type: 'user',
|
|
1660
|
+
uuid: 'question-result',
|
|
1661
|
+
session_id: 'claude-session-1',
|
|
1662
|
+
message: {
|
|
1663
|
+
role: 'user',
|
|
1664
|
+
content: [
|
|
1665
|
+
{
|
|
1666
|
+
type: 'tool_result',
|
|
1667
|
+
tool_use_id: 'toolu_question',
|
|
1668
|
+
content: {
|
|
1669
|
+
answers: {
|
|
1670
|
+
'Which plan style should I use?': 'Short',
|
|
1671
|
+
},
|
|
1672
|
+
},
|
|
1673
|
+
is_error: false,
|
|
1674
|
+
},
|
|
1675
|
+
],
|
|
1676
|
+
},
|
|
1677
|
+
parent_tool_use_id: null,
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
type: 'assistant',
|
|
1681
|
+
uuid: 'assistant-answer',
|
|
1682
|
+
session_id: 'claude-session-1',
|
|
1683
|
+
message: { role: 'assistant', content: [{ type: 'text', text: 'Continuing.' }] },
|
|
1684
|
+
parent_tool_use_id: null,
|
|
1685
|
+
},
|
|
1686
|
+
] satisfies SessionMessage[]) as any,
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
const session = await adapter.readSession('claude-session-1');
|
|
1690
|
+
expect(session.turns[0]?.items).toEqual([
|
|
1691
|
+
expect.objectContaining({ kind: 'userMessage', text: 'Ask me something.' }),
|
|
1692
|
+
expect.objectContaining({ kind: 'agentMessage', text: 'Continuing.' }),
|
|
1693
|
+
]);
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
it('omits Claude Code plan control tool plumbing from historical turns', async () => {
|
|
1697
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
1698
|
+
home: '/tmp/claude-home',
|
|
1699
|
+
query: (() => new FakeQuery([])) as any,
|
|
1700
|
+
getSessionInfo: (async () => ({
|
|
1701
|
+
sessionId: 'claude-session-1',
|
|
1702
|
+
summary: 'Existing session',
|
|
1703
|
+
lastModified: 1_772_000_000_000,
|
|
1704
|
+
createdAt: 1_771_000_000_000,
|
|
1705
|
+
cwd: '/tmp/workspace',
|
|
1706
|
+
})) as any,
|
|
1707
|
+
listSessions: (async () => []) as any,
|
|
1708
|
+
getSessionMessages: (async () => [
|
|
1709
|
+
{
|
|
1710
|
+
type: 'user',
|
|
1711
|
+
uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
|
|
1712
|
+
session_id: 'claude-session-1',
|
|
1713
|
+
message: { role: 'user', content: 'Plan a calculator.' },
|
|
1714
|
+
parent_tool_use_id: null,
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
type: 'assistant',
|
|
1718
|
+
uuid: 'assistant-search',
|
|
1719
|
+
session_id: 'claude-session-1',
|
|
1720
|
+
message: {
|
|
1721
|
+
role: 'assistant',
|
|
1722
|
+
content: [
|
|
1723
|
+
{
|
|
1724
|
+
type: 'tool_use',
|
|
1725
|
+
id: 'toolu_search',
|
|
1726
|
+
name: 'ToolSearch',
|
|
1727
|
+
input: { query: 'select:EnterPlanMode', max_results: 1 },
|
|
1728
|
+
},
|
|
1729
|
+
],
|
|
1730
|
+
},
|
|
1731
|
+
parent_tool_use_id: null,
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
type: 'user',
|
|
1735
|
+
uuid: 'search-result',
|
|
1736
|
+
session_id: 'claude-session-1',
|
|
1737
|
+
message: {
|
|
1738
|
+
role: 'user',
|
|
1739
|
+
content: [
|
|
1740
|
+
{
|
|
1741
|
+
type: 'tool_result',
|
|
1742
|
+
tool_use_id: 'toolu_search',
|
|
1743
|
+
content: [{ type: 'tool_reference', tool_name: 'EnterPlanMode' }],
|
|
1744
|
+
},
|
|
1745
|
+
],
|
|
1746
|
+
},
|
|
1747
|
+
parent_tool_use_id: null,
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
type: 'assistant',
|
|
1751
|
+
uuid: 'assistant-enter-plan',
|
|
1752
|
+
session_id: 'claude-session-1',
|
|
1753
|
+
message: {
|
|
1754
|
+
role: 'assistant',
|
|
1755
|
+
content: [
|
|
1756
|
+
{
|
|
1757
|
+
type: 'tool_use',
|
|
1758
|
+
id: 'toolu_enter',
|
|
1759
|
+
name: 'EnterPlanMode',
|
|
1760
|
+
input: {},
|
|
1761
|
+
},
|
|
1762
|
+
],
|
|
1763
|
+
},
|
|
1764
|
+
parent_tool_use_id: null,
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
type: 'user',
|
|
1768
|
+
uuid: 'enter-result',
|
|
1769
|
+
session_id: 'claude-session-1',
|
|
1770
|
+
message: {
|
|
1771
|
+
role: 'user',
|
|
1772
|
+
content: [
|
|
1773
|
+
{
|
|
1774
|
+
type: 'tool_result',
|
|
1775
|
+
tool_use_id: 'toolu_enter',
|
|
1776
|
+
content: 'Entered plan mode.',
|
|
1777
|
+
},
|
|
1778
|
+
],
|
|
1779
|
+
},
|
|
1780
|
+
parent_tool_use_id: null,
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
type: 'assistant',
|
|
1784
|
+
uuid: 'assistant-exit-plan',
|
|
1785
|
+
session_id: 'claude-session-1',
|
|
1786
|
+
message: {
|
|
1787
|
+
role: 'assistant',
|
|
1788
|
+
content: [
|
|
1789
|
+
{
|
|
1790
|
+
type: 'tool_use',
|
|
1791
|
+
id: 'toolu_plan',
|
|
1792
|
+
name: 'ExitPlanMode',
|
|
1793
|
+
input: { plan: 'Build a calculator.' },
|
|
1794
|
+
},
|
|
1795
|
+
],
|
|
1796
|
+
},
|
|
1797
|
+
parent_tool_use_id: null,
|
|
1798
|
+
},
|
|
1799
|
+
] satisfies SessionMessage[]) as any,
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
const session = await adapter.readSession('claude-session-1');
|
|
1803
|
+
expect(session.turns[0]?.items).toEqual([
|
|
1804
|
+
expect.objectContaining({ kind: 'userMessage', text: 'Plan a calculator.' }),
|
|
1805
|
+
expect.objectContaining({
|
|
1806
|
+
id: 'toolu_plan',
|
|
1807
|
+
kind: 'plan',
|
|
1808
|
+
text: 'Build a calculator.',
|
|
1809
|
+
}),
|
|
1810
|
+
]);
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
it('hides supervisor question continuation prompts from historical turns', async () => {
|
|
1814
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
1815
|
+
home: '/tmp/claude-home',
|
|
1816
|
+
query: (() => new FakeQuery([])) as any,
|
|
1817
|
+
getSessionInfo: (async () => ({
|
|
1818
|
+
sessionId: 'claude-session-1',
|
|
1819
|
+
summary: 'Existing session',
|
|
1820
|
+
lastModified: 1_772_000_000_000,
|
|
1821
|
+
createdAt: 1_771_000_000_000,
|
|
1822
|
+
cwd: '/tmp/workspace',
|
|
1823
|
+
})) as any,
|
|
1824
|
+
listSessions: (async () => []) as any,
|
|
1825
|
+
getSessionMessages: (async () => [
|
|
1826
|
+
{
|
|
1827
|
+
type: 'user',
|
|
1828
|
+
uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
|
|
1829
|
+
session_id: 'claude-session-1',
|
|
1830
|
+
message: { role: 'user', content: 'Plan a calculator.' },
|
|
1831
|
+
parent_tool_use_id: null,
|
|
1832
|
+
},
|
|
1833
|
+
{
|
|
1834
|
+
type: 'assistant',
|
|
1835
|
+
uuid: 'assistant-question',
|
|
1836
|
+
session_id: 'claude-session-1',
|
|
1837
|
+
message: { role: 'assistant', content: [{ type: 'text', text: 'Which features?' }] },
|
|
1838
|
+
parent_tool_use_id: null,
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
type: 'user',
|
|
1842
|
+
uuid: 'continuation-user',
|
|
1843
|
+
session_id: 'claude-session-1',
|
|
1844
|
+
message: {
|
|
1845
|
+
role: 'user',
|
|
1846
|
+
content:
|
|
1847
|
+
'The user answered the clarification questions below. Continue from the same plan-mode task using these answers. If you have enough information, produce the concrete plan for approval.\n\n- Which features?: History, Keyboard support',
|
|
1848
|
+
},
|
|
1849
|
+
parent_tool_use_id: null,
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
type: 'assistant',
|
|
1853
|
+
uuid: 'assistant-plan',
|
|
1854
|
+
session_id: 'claude-session-1',
|
|
1855
|
+
message: {
|
|
1856
|
+
role: 'assistant',
|
|
1857
|
+
content: [
|
|
1858
|
+
{
|
|
1859
|
+
type: 'tool_use',
|
|
1860
|
+
id: 'toolu_plan',
|
|
1861
|
+
name: 'ExitPlanMode',
|
|
1862
|
+
input: { plan: 'Build a basic calculator with history.' },
|
|
1863
|
+
},
|
|
1864
|
+
],
|
|
1865
|
+
},
|
|
1866
|
+
parent_tool_use_id: null,
|
|
1867
|
+
},
|
|
1868
|
+
] satisfies SessionMessage[]) as any,
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
const session = await adapter.readSession('claude-session-1');
|
|
1872
|
+
expect(session.turns).toHaveLength(1);
|
|
1873
|
+
expect(session.turns[0]?.items).toEqual([
|
|
1874
|
+
expect.objectContaining({ kind: 'userMessage', text: 'Plan a calculator.' }),
|
|
1875
|
+
expect.objectContaining({ kind: 'agentMessage', text: 'Which features?' }),
|
|
1876
|
+
expect.objectContaining({
|
|
1877
|
+
kind: 'plan',
|
|
1878
|
+
text: 'Build a basic calculator with history.',
|
|
1879
|
+
}),
|
|
1880
|
+
]);
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
it('keeps later Claude plan questions in the same historical turn after hidden continuations', async () => {
|
|
1884
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
1885
|
+
home: '/tmp/claude-home',
|
|
1886
|
+
query: (() => new FakeQuery([])) as any,
|
|
1887
|
+
getSessionInfo: (async () => ({
|
|
1888
|
+
sessionId: 'claude-session-1',
|
|
1889
|
+
summary: 'Existing session',
|
|
1890
|
+
lastModified: 1_772_000_000_000,
|
|
1891
|
+
createdAt: 1_771_000_000_000,
|
|
1892
|
+
cwd: '/tmp/workspace',
|
|
1893
|
+
})) as any,
|
|
1894
|
+
listSessions: (async () => []) as any,
|
|
1895
|
+
getSessionMessages: (async () => [
|
|
1896
|
+
{
|
|
1897
|
+
type: 'user',
|
|
1898
|
+
uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
|
|
1899
|
+
session_id: 'claude-session-1',
|
|
1900
|
+
message: { role: 'user', content: 'Plan a calculator.' },
|
|
1901
|
+
parent_tool_use_id: null,
|
|
1902
|
+
},
|
|
1903
|
+
{
|
|
1904
|
+
type: 'assistant',
|
|
1905
|
+
uuid: 'assistant-question-1',
|
|
1906
|
+
session_id: 'claude-session-1',
|
|
1907
|
+
message: { role: 'assistant', content: [{ type: 'text', text: 'Which features?' }] },
|
|
1908
|
+
parent_tool_use_id: null,
|
|
1909
|
+
},
|
|
1910
|
+
{
|
|
1911
|
+
type: 'user',
|
|
1912
|
+
uuid: 'continuation-user-1',
|
|
1913
|
+
session_id: 'claude-session-1',
|
|
1914
|
+
message: {
|
|
1915
|
+
role: 'user',
|
|
1916
|
+
content:
|
|
1917
|
+
'The user answered the clarification questions below. Continue from the same plan-mode task using these answers. If you have enough information, produce the concrete plan for approval.\n\n- Which features?: History',
|
|
1918
|
+
},
|
|
1919
|
+
parent_tool_use_id: null,
|
|
1920
|
+
},
|
|
1921
|
+
{
|
|
1922
|
+
type: 'assistant',
|
|
1923
|
+
uuid: 'assistant-question-2',
|
|
1924
|
+
session_id: 'claude-session-1',
|
|
1925
|
+
message: { role: 'assistant', content: [{ type: 'text', text: 'Keyboard support?' }] },
|
|
1926
|
+
parent_tool_use_id: null,
|
|
1927
|
+
},
|
|
1928
|
+
{
|
|
1929
|
+
type: 'user',
|
|
1930
|
+
uuid: 'continuation-user-2',
|
|
1931
|
+
session_id: 'claude-session-1',
|
|
1932
|
+
message: {
|
|
1933
|
+
role: 'user',
|
|
1934
|
+
content:
|
|
1935
|
+
'The user answered the clarification questions below. Continue from the same plan-mode task using these answers. If you have enough information, produce the concrete plan for approval.\n\n- Keyboard support?: Yes',
|
|
1936
|
+
},
|
|
1937
|
+
parent_tool_use_id: null,
|
|
1938
|
+
},
|
|
1939
|
+
{
|
|
1940
|
+
type: 'assistant',
|
|
1941
|
+
uuid: 'assistant-plan',
|
|
1942
|
+
session_id: 'claude-session-1',
|
|
1943
|
+
message: {
|
|
1944
|
+
role: 'assistant',
|
|
1945
|
+
content: [
|
|
1946
|
+
{
|
|
1947
|
+
type: 'tool_use',
|
|
1948
|
+
id: 'toolu_plan',
|
|
1949
|
+
name: 'ExitPlanMode',
|
|
1950
|
+
input: { plan: 'Build a calculator with history and keyboard support.' },
|
|
1951
|
+
},
|
|
1952
|
+
],
|
|
1953
|
+
},
|
|
1954
|
+
parent_tool_use_id: null,
|
|
1955
|
+
},
|
|
1956
|
+
] satisfies SessionMessage[]) as any,
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
const session = await adapter.readSession('claude-session-1');
|
|
1960
|
+
expect(session.turns).toHaveLength(1);
|
|
1961
|
+
expect(session.turns[0]?.items).toEqual([
|
|
1962
|
+
expect.objectContaining({ kind: 'userMessage', text: 'Plan a calculator.' }),
|
|
1963
|
+
expect.objectContaining({ kind: 'agentMessage', text: 'Which features?' }),
|
|
1964
|
+
expect.objectContaining({ kind: 'agentMessage', text: 'Keyboard support?' }),
|
|
1965
|
+
expect.objectContaining({
|
|
1966
|
+
kind: 'plan',
|
|
1967
|
+
text: 'Build a calculator with history and keyboard support.',
|
|
1968
|
+
}),
|
|
1969
|
+
]);
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
it('maps Claude file inspection and agent tools to readable timeline items', async () => {
|
|
1973
|
+
const adapter = new ClaudeRuntimeAdapter({
|
|
1974
|
+
home: '/tmp/claude-home',
|
|
1975
|
+
query: (() => new FakeQuery([])) as any,
|
|
1976
|
+
getSessionInfo: (async () => ({
|
|
1977
|
+
sessionId: 'claude-session-1',
|
|
1978
|
+
summary: 'Existing session',
|
|
1979
|
+
lastModified: 1_772_000_000_000,
|
|
1980
|
+
createdAt: 1_771_000_000_000,
|
|
1981
|
+
cwd: '/tmp/workspace',
|
|
1982
|
+
})) as any,
|
|
1983
|
+
listSessions: (async () => []) as any,
|
|
1984
|
+
getSessionMessages: (async () => [
|
|
1985
|
+
{
|
|
1986
|
+
type: 'user',
|
|
1987
|
+
uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
|
|
1988
|
+
session_id: 'claude-session-1',
|
|
1989
|
+
message: { role: 'user', content: 'Plan backend alignment.' },
|
|
1990
|
+
parent_tool_use_id: null,
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
type: 'assistant',
|
|
1994
|
+
uuid: 'assistant-tools',
|
|
1995
|
+
session_id: 'claude-session-1',
|
|
1996
|
+
message: {
|
|
1997
|
+
role: 'assistant',
|
|
1998
|
+
content: [
|
|
1999
|
+
{
|
|
2000
|
+
type: 'tool_use',
|
|
2001
|
+
id: 'toolu_agent',
|
|
2002
|
+
name: 'Agent',
|
|
2003
|
+
input: {
|
|
2004
|
+
description: 'Inspect backend runtime boundaries',
|
|
2005
|
+
prompt: 'Read the backend services and summarize risks.',
|
|
2006
|
+
},
|
|
2007
|
+
},
|
|
2008
|
+
{
|
|
2009
|
+
type: 'tool_use',
|
|
2010
|
+
id: 'toolu_grep',
|
|
2011
|
+
name: 'Grep',
|
|
2012
|
+
input: {
|
|
2013
|
+
pattern: 'AgentRuntime',
|
|
2014
|
+
path: 'apps/supervisor-api/src',
|
|
2015
|
+
},
|
|
2016
|
+
},
|
|
2017
|
+
{
|
|
2018
|
+
type: 'tool_use',
|
|
2019
|
+
id: 'toolu_read',
|
|
2020
|
+
name: 'Read',
|
|
2021
|
+
input: {
|
|
2022
|
+
file_path: 'packages/claude/src/runtimeAdapter.ts',
|
|
2023
|
+
},
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
type: 'tool_use',
|
|
2027
|
+
id: 'toolu_skill',
|
|
2028
|
+
name: 'Skill',
|
|
2029
|
+
input: {
|
|
2030
|
+
skill: 'update-config',
|
|
2031
|
+
args: 'Allow Bash and Edit for this workspace.',
|
|
2032
|
+
},
|
|
2033
|
+
},
|
|
2034
|
+
{
|
|
2035
|
+
type: 'tool_use',
|
|
2036
|
+
id: 'toolu_tool_search',
|
|
2037
|
+
name: 'ToolSearch',
|
|
2038
|
+
input: { query: 'select:EnterPlanMode' },
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
type: 'tool_use',
|
|
2042
|
+
id: 'toolu_plan',
|
|
2043
|
+
name: 'ExitPlanMode',
|
|
2044
|
+
input: { plan: '## Plan\n\n- Keep one visible turn.' },
|
|
2045
|
+
},
|
|
2046
|
+
],
|
|
2047
|
+
},
|
|
2048
|
+
parent_tool_use_id: null,
|
|
2049
|
+
},
|
|
2050
|
+
{
|
|
2051
|
+
type: 'user',
|
|
2052
|
+
uuid: 'tool-results',
|
|
2053
|
+
session_id: 'claude-session-1',
|
|
2054
|
+
message: {
|
|
2055
|
+
role: 'user',
|
|
2056
|
+
content: [
|
|
2057
|
+
{
|
|
2058
|
+
type: 'tool_result',
|
|
2059
|
+
tool_use_id: 'toolu_agent',
|
|
2060
|
+
content: 'Runtime boundaries summarized.',
|
|
2061
|
+
},
|
|
2062
|
+
{
|
|
2063
|
+
type: 'tool_result',
|
|
2064
|
+
tool_use_id: 'toolu_grep',
|
|
2065
|
+
content: 'apps/supervisor-api/src/thread-service.ts:AgentRuntime',
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
type: 'tool_result',
|
|
2069
|
+
tool_use_id: 'toolu_read',
|
|
2070
|
+
content: 'export class ClaudeRuntimeAdapter {}',
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
type: 'tool_result',
|
|
2074
|
+
tool_use_id: 'toolu_skill',
|
|
2075
|
+
content: 'Skill completed.',
|
|
2076
|
+
},
|
|
2077
|
+
{
|
|
2078
|
+
type: 'tool_result',
|
|
2079
|
+
tool_use_id: 'toolu_tool_search',
|
|
2080
|
+
content: [{ type: 'tool_reference', tool_name: 'EnterPlanMode' }],
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
type: 'tool_result',
|
|
2084
|
+
tool_use_id: 'toolu_plan',
|
|
2085
|
+
content: 'Exit plan mode?',
|
|
2086
|
+
},
|
|
2087
|
+
],
|
|
2088
|
+
},
|
|
2089
|
+
parent_tool_use_id: null,
|
|
2090
|
+
},
|
|
2091
|
+
] satisfies SessionMessage[]) as any,
|
|
2092
|
+
});
|
|
2093
|
+
|
|
2094
|
+
const session = await adapter.readSession('claude-session-1');
|
|
2095
|
+
const items = session.turns[0]?.items ?? [];
|
|
2096
|
+
|
|
2097
|
+
expect(items).toEqual(
|
|
2098
|
+
expect.arrayContaining([
|
|
2099
|
+
expect.objectContaining({
|
|
2100
|
+
id: 'toolu_agent',
|
|
2101
|
+
kind: 'agentToolCall',
|
|
2102
|
+
text: 'Agent: Inspect backend runtime boundaries',
|
|
2103
|
+
status: 'completed',
|
|
2104
|
+
}),
|
|
2105
|
+
expect.objectContaining({
|
|
2106
|
+
id: 'toolu_grep',
|
|
2107
|
+
kind: 'fileRead',
|
|
2108
|
+
text: 'Search files: AgentRuntime in apps/supervisor-api/src',
|
|
2109
|
+
status: 'completed',
|
|
2110
|
+
}),
|
|
2111
|
+
expect.objectContaining({
|
|
2112
|
+
id: 'toolu_read',
|
|
2113
|
+
kind: 'fileRead',
|
|
2114
|
+
text: 'Read file: packages/claude/src/runtimeAdapter.ts',
|
|
2115
|
+
status: 'completed',
|
|
2116
|
+
}),
|
|
2117
|
+
expect.objectContaining({
|
|
2118
|
+
id: 'toolu_skill',
|
|
2119
|
+
kind: 'skillToolCall',
|
|
2120
|
+
text: 'Skill: update-config',
|
|
2121
|
+
status: 'completed',
|
|
2122
|
+
}),
|
|
2123
|
+
expect.objectContaining({
|
|
2124
|
+
id: 'toolu_plan',
|
|
2125
|
+
kind: 'plan',
|
|
2126
|
+
text: '## Plan\n\n- Keep one visible turn.',
|
|
2127
|
+
status: 'completed',
|
|
2128
|
+
}),
|
|
2129
|
+
]),
|
|
2130
|
+
);
|
|
2131
|
+
expect(items).toEqual(
|
|
2132
|
+
expect.not.arrayContaining([
|
|
2133
|
+
expect.objectContaining({ id: 'toolu_tool_search' }),
|
|
2134
|
+
expect.objectContaining({ text: expect.stringContaining('Exit plan mode?') }),
|
|
2135
|
+
]),
|
|
2136
|
+
);
|
|
2137
|
+
});
|
|
2138
|
+
});
|