@posthog/agent 2.1.131 → 2.1.137
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +14 -28
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +116 -164
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
- package/dist/adapters/claude/permissions/permission-options.js +33 -0
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -1
- package/dist/adapters/claude/tools.js +21 -11
- package/dist/adapters/claude/tools.js.map +1 -1
- package/dist/agent.js +1251 -606
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.js +2 -2
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +1300 -655
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1278 -635
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +2 -2
- package/src/adapters/base-acp-agent.ts +6 -3
- package/src/adapters/claude/UPSTREAM.md +63 -0
- package/src/adapters/claude/claude-agent.ts +682 -421
- package/src/adapters/claude/conversion/sdk-to-acp.ts +249 -85
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +174 -149
- package/src/adapters/claude/hooks.ts +53 -1
- package/src/adapters/claude/permissions/permission-handlers.ts +39 -21
- package/src/adapters/claude/session/commands.ts +13 -9
- package/src/adapters/claude/session/mcp-config.ts +2 -5
- package/src/adapters/claude/session/options.ts +58 -6
- package/src/adapters/claude/session/settings.ts +326 -0
- package/src/adapters/claude/tools.ts +1 -0
- package/src/adapters/claude/types.ts +38 -0
- package/src/execution-mode.ts +26 -10
- package/src/server/agent-server.test.ts +41 -1
- package/src/utils/common.ts +1 -1
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import {
|
|
6
|
+
type ModelInfo as AcpModelInfo,
|
|
5
7
|
type AgentSideConnection,
|
|
6
|
-
type AuthenticateRequest,
|
|
7
|
-
type AvailableCommand,
|
|
8
8
|
type ClientCapabilities,
|
|
9
|
+
type ForkSessionRequest,
|
|
10
|
+
type ForkSessionResponse,
|
|
9
11
|
type InitializeRequest,
|
|
10
12
|
type InitializeResponse,
|
|
13
|
+
type ListSessionsRequest,
|
|
14
|
+
type ListSessionsResponse,
|
|
11
15
|
type LoadSessionRequest,
|
|
12
16
|
type LoadSessionResponse,
|
|
13
17
|
type NewSessionRequest,
|
|
@@ -15,18 +19,27 @@ import {
|
|
|
15
19
|
type PromptRequest,
|
|
16
20
|
type PromptResponse,
|
|
17
21
|
RequestError,
|
|
22
|
+
type ResumeSessionRequest,
|
|
23
|
+
type ResumeSessionResponse,
|
|
18
24
|
type SessionConfigOption,
|
|
19
25
|
type SessionConfigOptionCategory,
|
|
20
26
|
type SessionConfigSelectOption,
|
|
27
|
+
type SessionModelState,
|
|
28
|
+
type SessionModeState,
|
|
21
29
|
type SetSessionConfigOptionRequest,
|
|
22
30
|
type SetSessionConfigOptionResponse,
|
|
31
|
+
type SetSessionModelRequest,
|
|
32
|
+
type SetSessionModelResponse,
|
|
33
|
+
type SetSessionModeRequest,
|
|
34
|
+
type SetSessionModeResponse,
|
|
35
|
+
type Usage,
|
|
23
36
|
} from "@agentclientprotocol/sdk";
|
|
24
37
|
import {
|
|
25
38
|
type CanUseTool,
|
|
26
|
-
|
|
39
|
+
getSessionMessages,
|
|
40
|
+
listSessions,
|
|
27
41
|
type Query,
|
|
28
42
|
query,
|
|
29
|
-
type SDKMessage,
|
|
30
43
|
type SDKUserMessage,
|
|
31
44
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
32
45
|
import { v7 as uuidv7 } from "uuid";
|
|
@@ -52,6 +65,7 @@ import {
|
|
|
52
65
|
buildSystemPrompt,
|
|
53
66
|
type ProcessSpawnedInfo,
|
|
54
67
|
} from "./session/options.js";
|
|
68
|
+
import { SettingsManager } from "./session/settings.js";
|
|
55
69
|
import {
|
|
56
70
|
getAvailableModes,
|
|
57
71
|
TWIG_EXECUTION_MODES,
|
|
@@ -65,6 +79,18 @@ import type {
|
|
|
65
79
|
} from "./types.js";
|
|
66
80
|
|
|
67
81
|
const SESSION_VALIDATION_TIMEOUT_MS = 10_000;
|
|
82
|
+
const MAX_TITLE_LENGTH = 256;
|
|
83
|
+
|
|
84
|
+
function sanitizeTitle(text: string): string {
|
|
85
|
+
const sanitized = text
|
|
86
|
+
.replace(/[\r\n]+/g, " ")
|
|
87
|
+
.replace(/\s+/g, " ")
|
|
88
|
+
.trim();
|
|
89
|
+
if (sanitized.length <= MAX_TITLE_LENGTH) {
|
|
90
|
+
return sanitized;
|
|
91
|
+
}
|
|
92
|
+
return `${sanitized.slice(0, MAX_TITLE_LENGTH - 1)}…`;
|
|
93
|
+
}
|
|
68
94
|
|
|
69
95
|
export interface ClaudeAcpAgentOptions {
|
|
70
96
|
onProcessSpawned?: (info: ProcessSpawnedInfo) => void;
|
|
@@ -78,7 +104,6 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
78
104
|
backgroundTerminals: { [key: string]: BackgroundTerminal } = {};
|
|
79
105
|
clientCapabilities?: ClientCapabilities;
|
|
80
106
|
private options?: ClaudeAcpAgentOptions;
|
|
81
|
-
private lastSentConfigOptions?: SessionConfigOption[];
|
|
82
107
|
|
|
83
108
|
constructor(client: AgentSideConnection, options?: ClaudeAcpAgentOptions) {
|
|
84
109
|
super(client);
|
|
@@ -102,165 +127,571 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
102
127
|
sse: true,
|
|
103
128
|
},
|
|
104
129
|
loadSession: true,
|
|
130
|
+
sessionCapabilities: {
|
|
131
|
+
list: {},
|
|
132
|
+
fork: {},
|
|
133
|
+
resume: {},
|
|
134
|
+
},
|
|
105
135
|
_meta: {
|
|
106
136
|
posthog: {
|
|
107
137
|
resumeSession: true,
|
|
108
138
|
},
|
|
139
|
+
claudeCode: {
|
|
140
|
+
promptQueueing: true,
|
|
141
|
+
},
|
|
109
142
|
},
|
|
110
143
|
},
|
|
111
144
|
agentInfo: {
|
|
112
145
|
name: packageJson.name,
|
|
113
|
-
title: "Claude
|
|
146
|
+
title: "Claude Agent",
|
|
114
147
|
version: packageJson.version,
|
|
115
148
|
},
|
|
116
|
-
authMethods: [
|
|
117
|
-
{
|
|
118
|
-
id: "claude-login",
|
|
119
|
-
name: "Log in with Claude Code",
|
|
120
|
-
description: "Run `claude /login` in the terminal",
|
|
121
|
-
},
|
|
122
|
-
],
|
|
149
|
+
authMethods: [],
|
|
123
150
|
};
|
|
124
151
|
}
|
|
125
152
|
|
|
126
|
-
async authenticate(_params: AuthenticateRequest): Promise<void> {
|
|
127
|
-
throw new Error("Method not implemented.");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
153
|
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
|
|
131
|
-
|
|
154
|
+
// Upstream Claude Code renames .claude.json to .claude.json.backup on logout.
|
|
155
|
+
// If the backup exists but the original doesn't, the user is logged out.
|
|
156
|
+
if (
|
|
157
|
+
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
|
|
158
|
+
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
|
|
159
|
+
) {
|
|
160
|
+
throw RequestError.authRequired();
|
|
161
|
+
}
|
|
132
162
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
sessionId,
|
|
138
|
-
taskId,
|
|
139
|
-
taskRunId: meta?.taskRunId,
|
|
140
|
-
cwd: params.cwd,
|
|
163
|
+
const response = await this.createSession(params, {
|
|
164
|
+
// Revisit these meta values once we support resume
|
|
165
|
+
resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options
|
|
166
|
+
?.resume as string | undefined,
|
|
141
167
|
});
|
|
142
|
-
const permissionMode: TwigExecutionMode =
|
|
143
|
-
meta?.permissionMode &&
|
|
144
|
-
TWIG_EXECUTION_MODES.includes(meta.permissionMode as TwigExecutionMode)
|
|
145
|
-
? (meta.permissionMode as TwigExecutionMode)
|
|
146
|
-
: "default";
|
|
147
168
|
|
|
148
|
-
|
|
169
|
+
return response;
|
|
170
|
+
}
|
|
149
171
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
onProcessExited: this.options?.onProcessExited,
|
|
163
|
-
});
|
|
172
|
+
async unstable_forkSession(
|
|
173
|
+
params: ForkSessionRequest,
|
|
174
|
+
): Promise<ForkSessionResponse> {
|
|
175
|
+
return this.createSession(
|
|
176
|
+
{
|
|
177
|
+
cwd: params.cwd,
|
|
178
|
+
mcpServers: params.mcpServers ?? [],
|
|
179
|
+
_meta: params._meta,
|
|
180
|
+
},
|
|
181
|
+
{ resume: params.sessionId, forkSession: true },
|
|
182
|
+
);
|
|
183
|
+
}
|
|
164
184
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
185
|
+
async unstable_resumeSession(
|
|
186
|
+
params: ResumeSessionRequest,
|
|
187
|
+
): Promise<ResumeSessionResponse> {
|
|
188
|
+
const response = await this.createSession(
|
|
189
|
+
{
|
|
190
|
+
cwd: params.cwd,
|
|
191
|
+
mcpServers: params.mcpServers ?? [],
|
|
192
|
+
_meta: params._meta,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
resume: params.sessionId,
|
|
196
|
+
},
|
|
197
|
+
);
|
|
169
198
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
199
|
+
return response;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
|
|
203
|
+
const response = await this.createSession(
|
|
204
|
+
{
|
|
205
|
+
cwd: params.cwd,
|
|
206
|
+
mcpServers: params.mcpServers ?? [],
|
|
207
|
+
_meta: params._meta,
|
|
208
|
+
},
|
|
209
|
+
{ resume: params.sessionId, skipBackgroundFetches: true },
|
|
177
210
|
);
|
|
178
|
-
session.taskRunId = meta?.taskRunId;
|
|
179
211
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
212
|
+
await this.replaySessionHistory(params.sessionId);
|
|
213
|
+
|
|
214
|
+
// Send available commands after replay so they don't interleave with history
|
|
215
|
+
this.deferBackgroundFetches(this.session.query);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
modes: response.modes,
|
|
219
|
+
models: response.models,
|
|
220
|
+
configOptions: response.configOptions,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async unstable_listSessions(
|
|
225
|
+
params: ListSessionsRequest,
|
|
226
|
+
): Promise<ListSessionsResponse> {
|
|
227
|
+
const sdkSessions = await listSessions({ dir: params.cwd ?? undefined });
|
|
228
|
+
const sessions = [];
|
|
229
|
+
|
|
230
|
+
for (const session of sdkSessions) {
|
|
231
|
+
if (!session.cwd) continue;
|
|
232
|
+
sessions.push({
|
|
233
|
+
sessionId: session.sessionId,
|
|
234
|
+
cwd: session.cwd,
|
|
235
|
+
title: sanitizeTitle(session.customTitle || session.summary || ""),
|
|
236
|
+
updatedAt: new Date(session.lastModified).toISOString(),
|
|
185
237
|
});
|
|
186
238
|
}
|
|
239
|
+
return {
|
|
240
|
+
sessions,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
187
243
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
244
|
+
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
|
245
|
+
this.session.cancelled = false;
|
|
246
|
+
this.session.interruptReason = undefined;
|
|
247
|
+
this.session.accumulatedUsage = {
|
|
248
|
+
inputTokens: 0,
|
|
249
|
+
outputTokens: 0,
|
|
250
|
+
cachedReadTokens: 0,
|
|
251
|
+
cachedWriteTokens: 0,
|
|
252
|
+
};
|
|
191
253
|
|
|
192
|
-
|
|
193
|
-
this.deferBackgroundFetches(q, sessionId);
|
|
254
|
+
const userMessage = promptToClaude(params);
|
|
194
255
|
|
|
195
|
-
session.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await
|
|
256
|
+
if (this.session.promptRunning) {
|
|
257
|
+
const uuid = randomUUID();
|
|
258
|
+
userMessage.uuid = uuid;
|
|
259
|
+
this.session.input.push(userMessage);
|
|
260
|
+
const order = this.session.nextPendingOrder++;
|
|
261
|
+
const cancelled = await new Promise<boolean>((resolve) => {
|
|
262
|
+
this.session.pendingMessages.set(uuid, { resolve, order });
|
|
263
|
+
});
|
|
264
|
+
if (cancelled) {
|
|
265
|
+
return { stopReason: "cancelled" };
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
this.session.input.push(userMessage);
|
|
201
269
|
}
|
|
202
270
|
|
|
203
|
-
|
|
271
|
+
// Broadcast user message to client
|
|
272
|
+
await this.broadcastUserMessage(params);
|
|
273
|
+
|
|
274
|
+
this.session.promptRunning = true;
|
|
275
|
+
let handedOff = false;
|
|
276
|
+
let lastAssistantTotalUsage: number | null = null;
|
|
204
277
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
278
|
+
const supportsTerminalOutput =
|
|
279
|
+
(
|
|
280
|
+
this.clientCapabilities?._meta as
|
|
281
|
+
| ClientCapabilities["_meta"]
|
|
282
|
+
| undefined
|
|
283
|
+
)?.terminal_output === true;
|
|
284
|
+
|
|
285
|
+
const context = {
|
|
286
|
+
session: this.session,
|
|
287
|
+
sessionId: params.sessionId,
|
|
288
|
+
client: this.client,
|
|
289
|
+
toolUseCache: this.toolUseCache,
|
|
290
|
+
fileContentCache: this.fileContentCache,
|
|
291
|
+
logger: this.logger,
|
|
292
|
+
supportsTerminalOutput,
|
|
208
293
|
};
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
while (true) {
|
|
297
|
+
const { value: message, done } = await this.session.query.next();
|
|
298
|
+
|
|
299
|
+
if (done || !message) {
|
|
300
|
+
if (this.session.cancelled) {
|
|
301
|
+
return {
|
|
302
|
+
stopReason: "cancelled",
|
|
303
|
+
_meta: this.session.interruptReason
|
|
304
|
+
? { interruptReason: this.session.interruptReason }
|
|
305
|
+
: undefined,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
switch (message.type) {
|
|
312
|
+
case "system":
|
|
313
|
+
if (message.subtype === "compact_boundary") {
|
|
314
|
+
lastAssistantTotalUsage = 0;
|
|
315
|
+
}
|
|
316
|
+
await handleSystemMessage(message, context);
|
|
317
|
+
break;
|
|
318
|
+
|
|
319
|
+
case "result": {
|
|
320
|
+
if (this.session.cancelled) {
|
|
321
|
+
return { stopReason: "cancelled" };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Accumulate usage from this result
|
|
325
|
+
this.session.accumulatedUsage.inputTokens +=
|
|
326
|
+
message.usage.input_tokens;
|
|
327
|
+
this.session.accumulatedUsage.outputTokens +=
|
|
328
|
+
message.usage.output_tokens;
|
|
329
|
+
this.session.accumulatedUsage.cachedReadTokens +=
|
|
330
|
+
message.usage.cache_read_input_tokens;
|
|
331
|
+
this.session.accumulatedUsage.cachedWriteTokens +=
|
|
332
|
+
message.usage.cache_creation_input_tokens;
|
|
333
|
+
|
|
334
|
+
// Calculate context window size from modelUsage (minimum across all models used)
|
|
335
|
+
const contextWindows = Object.values(message.modelUsage).map(
|
|
336
|
+
(m) => m.contextWindow,
|
|
337
|
+
);
|
|
338
|
+
const contextWindowSize =
|
|
339
|
+
contextWindows.length > 0 ? Math.min(...contextWindows) : 200000;
|
|
340
|
+
|
|
341
|
+
// Send usage_update notification
|
|
342
|
+
if (lastAssistantTotalUsage !== null) {
|
|
343
|
+
await this.client.sessionUpdate({
|
|
344
|
+
sessionId: params.sessionId,
|
|
345
|
+
update: {
|
|
346
|
+
sessionUpdate: "usage_update",
|
|
347
|
+
used: lastAssistantTotalUsage as unknown as bigint,
|
|
348
|
+
size: contextWindowSize as unknown as bigint,
|
|
349
|
+
cost: {
|
|
350
|
+
amount: message.total_cost_usd,
|
|
351
|
+
currency: "USD",
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
await this.client.extNotification("_posthog/usage_update", {
|
|
358
|
+
sessionId: params.sessionId,
|
|
359
|
+
used: {
|
|
360
|
+
inputTokens: message.usage.input_tokens,
|
|
361
|
+
outputTokens: message.usage.output_tokens,
|
|
362
|
+
cachedReadTokens: message.usage.cache_read_input_tokens,
|
|
363
|
+
cachedWriteTokens: message.usage.cache_creation_input_tokens,
|
|
364
|
+
},
|
|
365
|
+
cost: message.total_cost_usd,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Build usage for PromptResponse
|
|
369
|
+
// ACP SDK types declare these as bigint but JSON.stringify can't
|
|
370
|
+
// serialize BigInt. Token counts never exceed MAX_SAFE_INTEGER so
|
|
371
|
+
// we pass plain numbers and cast to satisfy the type system.
|
|
372
|
+
const usage = {
|
|
373
|
+
inputTokens: this.session.accumulatedUsage.inputTokens,
|
|
374
|
+
outputTokens: this.session.accumulatedUsage.outputTokens,
|
|
375
|
+
cachedReadTokens: this.session.accumulatedUsage.cachedReadTokens,
|
|
376
|
+
cachedWriteTokens:
|
|
377
|
+
this.session.accumulatedUsage.cachedWriteTokens,
|
|
378
|
+
totalTokens:
|
|
379
|
+
this.session.accumulatedUsage.inputTokens +
|
|
380
|
+
this.session.accumulatedUsage.outputTokens +
|
|
381
|
+
this.session.accumulatedUsage.cachedReadTokens +
|
|
382
|
+
this.session.accumulatedUsage.cachedWriteTokens,
|
|
383
|
+
} as unknown as Usage;
|
|
384
|
+
|
|
385
|
+
const result = handleResultMessage(message);
|
|
386
|
+
if (result.error) throw result.error;
|
|
387
|
+
|
|
388
|
+
switch (message.subtype) {
|
|
389
|
+
case "error_max_budget_usd":
|
|
390
|
+
case "error_max_turns":
|
|
391
|
+
case "error_max_structured_output_retries":
|
|
392
|
+
return { stopReason: "max_turn_requests", usage };
|
|
393
|
+
default:
|
|
394
|
+
return { stopReason: "end_turn", usage };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
case "stream_event":
|
|
399
|
+
await handleStreamEvent(message, context);
|
|
400
|
+
break;
|
|
401
|
+
|
|
402
|
+
case "user":
|
|
403
|
+
case "assistant": {
|
|
404
|
+
if (this.session.cancelled) {
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Check for queued prompt replay
|
|
409
|
+
if (message.type === "user" && "uuid" in message && message.uuid) {
|
|
410
|
+
const pending = this.session.pendingMessages.get(
|
|
411
|
+
message.uuid as string,
|
|
412
|
+
);
|
|
413
|
+
if (pending) {
|
|
414
|
+
pending.resolve(false);
|
|
415
|
+
this.session.pendingMessages.delete(message.uuid as string);
|
|
416
|
+
handedOff = true;
|
|
417
|
+
// the current loop stops with end_turn,
|
|
418
|
+
// the loop of the next prompt continues running
|
|
419
|
+
return { stopReason: "end_turn" };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Store latest assistant usage (excluding subagents)
|
|
424
|
+
if (
|
|
425
|
+
"usage" in message.message &&
|
|
426
|
+
message.parent_tool_use_id === null
|
|
427
|
+
) {
|
|
428
|
+
const usage = (
|
|
429
|
+
message.message as unknown as Record<string, unknown>
|
|
430
|
+
).usage as {
|
|
431
|
+
input_tokens: number;
|
|
432
|
+
output_tokens: number;
|
|
433
|
+
cache_read_input_tokens: number;
|
|
434
|
+
cache_creation_input_tokens: number;
|
|
435
|
+
};
|
|
436
|
+
lastAssistantTotalUsage =
|
|
437
|
+
usage.input_tokens +
|
|
438
|
+
usage.output_tokens +
|
|
439
|
+
usage.cache_read_input_tokens +
|
|
440
|
+
usage.cache_creation_input_tokens;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const result = await handleUserAssistantMessage(message, context);
|
|
444
|
+
if (result.error) throw result.error;
|
|
445
|
+
if (result.shouldStop) {
|
|
446
|
+
return { stopReason: "end_turn" };
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
case "tool_progress":
|
|
452
|
+
case "auth_status":
|
|
453
|
+
case "tool_use_summary":
|
|
454
|
+
break;
|
|
455
|
+
|
|
456
|
+
default:
|
|
457
|
+
unreachable(message as never, this.logger);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
throw new Error("Session did not end in result");
|
|
462
|
+
} finally {
|
|
463
|
+
if (!handedOff) {
|
|
464
|
+
this.session.promptRunning = false;
|
|
465
|
+
// Resolve all remaining pending prompts so no callers get stuck.
|
|
466
|
+
for (const [key, pending] of this.session.pendingMessages) {
|
|
467
|
+
pending.resolve(true);
|
|
468
|
+
this.session.pendingMessages.delete(key);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
209
472
|
}
|
|
210
473
|
|
|
211
|
-
|
|
212
|
-
|
|
474
|
+
// Called by BaseAcpAgent#cancel() to interrupt the session
|
|
475
|
+
protected async interrupt(): Promise<void> {
|
|
476
|
+
this.session.cancelled = true;
|
|
477
|
+
for (const [, pending] of this.session.pendingMessages) {
|
|
478
|
+
pending.resolve(true);
|
|
479
|
+
}
|
|
480
|
+
this.session.pendingMessages.clear();
|
|
481
|
+
await this.session.query.interrupt();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async unstable_setSessionModel(
|
|
485
|
+
params: SetSessionModelRequest,
|
|
486
|
+
): Promise<SetSessionModelResponse | undefined> {
|
|
487
|
+
const sdkModelId = toSdkModelId(params.modelId);
|
|
488
|
+
await this.session.query.setModel(sdkModelId);
|
|
489
|
+
this.session.modelId = params.modelId;
|
|
490
|
+
await this.updateConfigOption("model", params.modelId);
|
|
491
|
+
return {};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async setSessionMode(
|
|
495
|
+
params: SetSessionModeRequest,
|
|
496
|
+
): Promise<SetSessionModeResponse> {
|
|
497
|
+
await this.applySessionMode(params.modeId);
|
|
498
|
+
await this.updateConfigOption("mode", params.modeId);
|
|
499
|
+
return {};
|
|
213
500
|
}
|
|
214
501
|
|
|
215
|
-
async
|
|
216
|
-
params:
|
|
217
|
-
): Promise<
|
|
502
|
+
async setSessionConfigOption(
|
|
503
|
+
params: SetSessionConfigOptionRequest,
|
|
504
|
+
): Promise<SetSessionConfigOptionResponse> {
|
|
505
|
+
const option = this.session.configOptions.find(
|
|
506
|
+
(o) => o.id === params.configId,
|
|
507
|
+
);
|
|
508
|
+
if (!option) {
|
|
509
|
+
throw new Error(`Unknown config option: ${params.configId}`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const allValues: { value: string }[] =
|
|
513
|
+
"options" in option && Array.isArray(option.options)
|
|
514
|
+
? (option.options as Array<Record<string, unknown>>).flatMap((o) =>
|
|
515
|
+
"options" in o && Array.isArray(o.options)
|
|
516
|
+
? (o.options as { value: string }[])
|
|
517
|
+
: [o as { value: string }],
|
|
518
|
+
)
|
|
519
|
+
: [];
|
|
520
|
+
const validValue = allValues.find((o) => o.value === params.value);
|
|
521
|
+
if (!validValue) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`Invalid value for config option ${params.configId}: ${params.value}`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (params.configId === "mode") {
|
|
528
|
+
await this.applySessionMode(params.value);
|
|
529
|
+
await this.client.sessionUpdate({
|
|
530
|
+
sessionId: this.sessionId,
|
|
531
|
+
update: {
|
|
532
|
+
sessionUpdate: "current_mode_update",
|
|
533
|
+
currentModeId: params.value,
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
} else if (params.configId === "model") {
|
|
537
|
+
const sdkModelId = toSdkModelId(params.value);
|
|
538
|
+
await this.session.query.setModel(sdkModelId);
|
|
539
|
+
this.session.modelId = params.value;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
this.session.configOptions = this.session.configOptions.map((o) =>
|
|
543
|
+
o.id === params.configId ? { ...o, currentValue: params.value } : o,
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
return { configOptions: this.session.configOptions };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private async updateConfigOption(
|
|
550
|
+
configId: string,
|
|
551
|
+
value: string,
|
|
552
|
+
): Promise<void> {
|
|
553
|
+
this.session.configOptions = this.session.configOptions.map((o) =>
|
|
554
|
+
o.id === configId ? { ...o, currentValue: value } : o,
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
await this.client.sessionUpdate({
|
|
558
|
+
sessionId: this.sessionId,
|
|
559
|
+
update: {
|
|
560
|
+
sessionUpdate: "config_option_update",
|
|
561
|
+
configOptions: this.session.configOptions,
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private async applySessionMode(modeId: string): Promise<void> {
|
|
567
|
+
if (!TWIG_EXECUTION_MODES.includes(modeId as TwigExecutionMode)) {
|
|
568
|
+
throw new Error("Invalid Mode");
|
|
569
|
+
}
|
|
570
|
+
const previousMode = this.session.permissionMode;
|
|
571
|
+
this.session.permissionMode = modeId as TwigExecutionMode;
|
|
572
|
+
try {
|
|
573
|
+
await this.session.query.setPermissionMode(modeId as TwigExecutionMode);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
this.session.permissionMode = previousMode;
|
|
576
|
+
if (error instanceof Error) {
|
|
577
|
+
if (!error.message) {
|
|
578
|
+
error.message = "Invalid Mode";
|
|
579
|
+
}
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
throw new Error("Invalid Mode");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private async createSession(
|
|
587
|
+
params: {
|
|
588
|
+
cwd: string;
|
|
589
|
+
mcpServers: NewSessionRequest["mcpServers"];
|
|
590
|
+
_meta?: unknown;
|
|
591
|
+
},
|
|
592
|
+
creationOpts: {
|
|
593
|
+
resume?: string;
|
|
594
|
+
forkSession?: boolean;
|
|
595
|
+
skipBackgroundFetches?: boolean;
|
|
596
|
+
} = {},
|
|
597
|
+
): Promise<NewSessionResponse> {
|
|
598
|
+
const { cwd } = params;
|
|
599
|
+
const { resume, forkSession } = creationOpts;
|
|
600
|
+
|
|
601
|
+
const isResume = !!resume;
|
|
602
|
+
|
|
218
603
|
const meta = params._meta as NewSessionMeta | undefined;
|
|
219
604
|
const taskId = meta?.persistence?.taskId;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
605
|
+
|
|
606
|
+
// We want to create a new session id unless it is resume,
|
|
607
|
+
// but not resume + forkSession.
|
|
608
|
+
let sessionId: string;
|
|
609
|
+
if (forkSession) {
|
|
610
|
+
sessionId = uuidv7();
|
|
611
|
+
} else if (isResume) {
|
|
612
|
+
sessionId = resume;
|
|
613
|
+
} else {
|
|
614
|
+
sessionId = uuidv7();
|
|
226
615
|
}
|
|
227
616
|
|
|
228
|
-
|
|
617
|
+
const input = new Pushable<SDKUserMessage>();
|
|
618
|
+
|
|
619
|
+
const settingsManager = new SettingsManager(cwd);
|
|
620
|
+
await settingsManager.initialize();
|
|
621
|
+
|
|
622
|
+
const mcpServers = parseMcpServers(params);
|
|
623
|
+
const systemPrompt = buildSystemPrompt(meta?.systemPrompt);
|
|
624
|
+
|
|
625
|
+
this.logger.info(isResume ? "Resuming session" : "Creating new session", {
|
|
229
626
|
sessionId,
|
|
230
627
|
taskId,
|
|
231
628
|
taskRunId: meta?.taskRunId,
|
|
232
|
-
cwd
|
|
629
|
+
cwd,
|
|
233
630
|
});
|
|
234
631
|
|
|
235
|
-
const mcpServers = parseMcpServers(params);
|
|
236
|
-
|
|
237
632
|
const permissionMode: TwigExecutionMode =
|
|
238
633
|
meta?.permissionMode &&
|
|
239
634
|
TWIG_EXECUTION_MODES.includes(meta.permissionMode as TwigExecutionMode)
|
|
240
635
|
? (meta.permissionMode as TwigExecutionMode)
|
|
241
636
|
: "default";
|
|
242
637
|
|
|
243
|
-
const
|
|
244
|
-
cwd
|
|
245
|
-
permissionMode,
|
|
638
|
+
const options = buildSessionOptions({
|
|
639
|
+
cwd,
|
|
246
640
|
mcpServers,
|
|
247
|
-
|
|
641
|
+
permissionMode,
|
|
642
|
+
canUseTool: this.createCanUseTool(sessionId),
|
|
643
|
+
logger: this.logger,
|
|
644
|
+
systemPrompt,
|
|
248
645
|
userProvidedOptions: meta?.claudeCode?.options,
|
|
249
646
|
sessionId,
|
|
250
|
-
isResume
|
|
647
|
+
isResume,
|
|
648
|
+
forkSession,
|
|
251
649
|
additionalDirectories: meta?.claudeCode?.options?.additionalDirectories,
|
|
650
|
+
disableBuiltInTools: meta?.disableBuiltInTools,
|
|
651
|
+
settingsManager,
|
|
652
|
+
onModeChange: this.createOnModeChange(),
|
|
653
|
+
onProcessSpawned: this.options?.onProcessSpawned,
|
|
654
|
+
onProcessExited: this.options?.onProcessExited,
|
|
252
655
|
});
|
|
253
656
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
657
|
+
// Use the same abort controller that buildSessionOptions gave to the query
|
|
658
|
+
const abortController = options.abortController as AbortController;
|
|
659
|
+
|
|
660
|
+
const q = query({ prompt: input, options });
|
|
661
|
+
|
|
662
|
+
const session: Session = {
|
|
663
|
+
query: q,
|
|
664
|
+
input,
|
|
665
|
+
cancelled: false,
|
|
666
|
+
settingsManager,
|
|
667
|
+
permissionMode,
|
|
668
|
+
abortController,
|
|
669
|
+
accumulatedUsage: {
|
|
670
|
+
inputTokens: 0,
|
|
671
|
+
outputTokens: 0,
|
|
672
|
+
cachedReadTokens: 0,
|
|
673
|
+
cachedWriteTokens: 0,
|
|
674
|
+
},
|
|
675
|
+
configOptions: [],
|
|
676
|
+
promptRunning: false,
|
|
677
|
+
pendingMessages: new Map(),
|
|
678
|
+
nextPendingOrder: 0,
|
|
679
|
+
|
|
680
|
+
// Custom properties
|
|
681
|
+
cwd,
|
|
682
|
+
notificationHistory: [],
|
|
257
683
|
taskRunId: meta?.taskRunId,
|
|
258
|
-
}
|
|
684
|
+
};
|
|
685
|
+
this.session = session;
|
|
686
|
+
this.sessionId = sessionId;
|
|
259
687
|
|
|
260
|
-
|
|
688
|
+
this.logger.info(
|
|
689
|
+
isResume
|
|
690
|
+
? "Session query initialized, awaiting resumption"
|
|
691
|
+
: "Session query initialized, awaiting initialization",
|
|
692
|
+
{ sessionId, taskId, taskRunId: meta?.taskRunId },
|
|
693
|
+
);
|
|
261
694
|
|
|
262
|
-
// Check the resumed session is alive. For stale sessions this throws
|
|
263
|
-
// (e.g. "No conversation found"), preventing a broken session.
|
|
264
695
|
try {
|
|
265
696
|
const result = await withTimeout(
|
|
266
697
|
q.initializationResult(),
|
|
@@ -268,306 +699,219 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
268
699
|
);
|
|
269
700
|
if (result.result === "timeout") {
|
|
270
701
|
throw new Error(
|
|
271
|
-
`Session resumption timed out for sessionId=${sessionId}`,
|
|
702
|
+
`Session ${isResume ? (forkSession ? "fork" : "resumption") : "initialization"} timed out for sessionId=${sessionId}`,
|
|
272
703
|
);
|
|
273
704
|
}
|
|
274
705
|
} catch (err) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
706
|
+
settingsManager.dispose();
|
|
707
|
+
this.logger.error(
|
|
708
|
+
isResume
|
|
709
|
+
? forkSession
|
|
710
|
+
? "Session fork failed"
|
|
711
|
+
: "Session resumption failed"
|
|
712
|
+
: "Session initialization failed",
|
|
713
|
+
{
|
|
714
|
+
sessionId,
|
|
715
|
+
taskId,
|
|
716
|
+
taskRunId: meta?.taskRunId,
|
|
717
|
+
error: err instanceof Error ? err.message : String(err),
|
|
718
|
+
},
|
|
719
|
+
);
|
|
281
720
|
throw err;
|
|
282
721
|
}
|
|
283
722
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// Deferred: slash commands + MCP metadata (not needed to return configOptions)
|
|
291
|
-
this.deferBackgroundFetches(q, sessionId);
|
|
292
|
-
|
|
293
|
-
const configOptions = await this.buildConfigOptions();
|
|
294
|
-
|
|
295
|
-
return { configOptions };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
|
299
|
-
this.session.cancelled = false;
|
|
300
|
-
this.session.interruptReason = undefined;
|
|
301
|
-
|
|
302
|
-
await this.broadcastUserMessage(params);
|
|
303
|
-
this.session.input.push(promptToClaude(params));
|
|
304
|
-
|
|
305
|
-
return this.processMessages(params.sessionId);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async setSessionConfigOption(
|
|
309
|
-
params: SetSessionConfigOptionRequest,
|
|
310
|
-
): Promise<SetSessionConfigOptionResponse> {
|
|
311
|
-
const configId = params.configId;
|
|
312
|
-
const value = params.value;
|
|
313
|
-
|
|
314
|
-
if (configId === "mode") {
|
|
315
|
-
const modeId = value as TwigExecutionMode;
|
|
316
|
-
if (!TWIG_EXECUTION_MODES.includes(modeId)) {
|
|
317
|
-
throw new Error("Invalid Mode");
|
|
318
|
-
}
|
|
319
|
-
this.session.permissionMode = modeId;
|
|
320
|
-
await this.session.query.setPermissionMode(modeId);
|
|
321
|
-
} else if (configId === "model") {
|
|
322
|
-
await this.setModelWithFallback(this.session.query, value);
|
|
323
|
-
this.session.modelId = value;
|
|
324
|
-
} else {
|
|
325
|
-
throw new Error("Unsupported config option");
|
|
723
|
+
if (meta?.taskRunId) {
|
|
724
|
+
await this.client.extNotification("_posthog/sdk_session", {
|
|
725
|
+
taskRunId: meta.taskRunId,
|
|
726
|
+
sessionId,
|
|
727
|
+
adapter: "claude",
|
|
728
|
+
});
|
|
326
729
|
}
|
|
327
730
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
await this.session.query.interrupt();
|
|
334
|
-
}
|
|
731
|
+
// Resolve model: settings model takes priority, then gateway
|
|
732
|
+
const settingsModel = settingsManager.getSettings().model;
|
|
733
|
+
const modelOptions = await this.getModelConfigOptions();
|
|
734
|
+
const resolvedModelId = settingsModel || modelOptions.currentModelId;
|
|
735
|
+
session.modelId = resolvedModelId;
|
|
335
736
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const result = await this.resumeSession(
|
|
342
|
-
params as unknown as LoadSessionRequest,
|
|
343
|
-
);
|
|
344
|
-
return {
|
|
345
|
-
_meta: {
|
|
346
|
-
configOptions: result.configOptions,
|
|
347
|
-
},
|
|
348
|
-
};
|
|
737
|
+
if (!isResume) {
|
|
738
|
+
const resolvedSdkModel = toSdkModelId(resolvedModelId);
|
|
739
|
+
if (resolvedSdkModel !== DEFAULT_MODEL) {
|
|
740
|
+
await this.session.query.setModel(resolvedSdkModel);
|
|
741
|
+
}
|
|
349
742
|
}
|
|
350
743
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
cwd: string,
|
|
360
|
-
abortController: AbortController,
|
|
361
|
-
): Session {
|
|
362
|
-
const session: Session = {
|
|
363
|
-
query: q,
|
|
364
|
-
input,
|
|
365
|
-
cancelled: false,
|
|
366
|
-
permissionMode,
|
|
367
|
-
cwd,
|
|
368
|
-
notificationHistory: [],
|
|
369
|
-
abortController,
|
|
744
|
+
const availableModes = getAvailableModes();
|
|
745
|
+
const modes: SessionModeState = {
|
|
746
|
+
currentModeId: permissionMode,
|
|
747
|
+
availableModes: availableModes.map((mode) => ({
|
|
748
|
+
id: mode.id,
|
|
749
|
+
name: mode.name,
|
|
750
|
+
description: mode.description ?? undefined,
|
|
751
|
+
})),
|
|
370
752
|
};
|
|
371
|
-
this.session = session;
|
|
372
|
-
this.sessionId = sessionId;
|
|
373
|
-
return session;
|
|
374
|
-
}
|
|
375
753
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
query: Query;
|
|
387
|
-
input: Pushable<SDKUserMessage>;
|
|
388
|
-
session: Session;
|
|
389
|
-
}> {
|
|
390
|
-
const input = new Pushable<SDKUserMessage>();
|
|
754
|
+
const models: SessionModelState = {
|
|
755
|
+
currentModelId: resolvedModelId,
|
|
756
|
+
availableModels: modelOptions.options.map(
|
|
757
|
+
(opt): AcpModelInfo => ({
|
|
758
|
+
modelId: opt.value,
|
|
759
|
+
name: opt.name,
|
|
760
|
+
description: opt.description,
|
|
761
|
+
}),
|
|
762
|
+
),
|
|
763
|
+
};
|
|
391
764
|
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
mcpServers: config.mcpServers,
|
|
395
|
-
permissionMode: config.permissionMode,
|
|
396
|
-
canUseTool: this.createCanUseTool(config.sessionId),
|
|
397
|
-
logger: this.logger,
|
|
398
|
-
systemPrompt: config.systemPrompt,
|
|
399
|
-
userProvidedOptions: config.userProvidedOptions,
|
|
400
|
-
sessionId: config.sessionId,
|
|
401
|
-
isResume: config.isResume,
|
|
402
|
-
additionalDirectories: config.additionalDirectories,
|
|
403
|
-
onModeChange: this.createOnModeChange(config.sessionId),
|
|
404
|
-
onProcessSpawned: this.options?.onProcessSpawned,
|
|
405
|
-
onProcessExited: this.options?.onProcessExited,
|
|
406
|
-
});
|
|
765
|
+
const configOptions = this.buildConfigOptions(permissionMode, modelOptions);
|
|
766
|
+
session.configOptions = configOptions;
|
|
407
767
|
|
|
408
|
-
|
|
409
|
-
|
|
768
|
+
if (!creationOpts.skipBackgroundFetches) {
|
|
769
|
+
this.deferBackgroundFetches(q);
|
|
770
|
+
}
|
|
410
771
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
772
|
+
this.logger.info(
|
|
773
|
+
isResume
|
|
774
|
+
? "Session resumed successfully"
|
|
775
|
+
: "Session created successfully",
|
|
776
|
+
{
|
|
777
|
+
sessionId,
|
|
778
|
+
taskId,
|
|
779
|
+
taskRunId: meta?.taskRunId,
|
|
780
|
+
},
|
|
418
781
|
);
|
|
419
782
|
|
|
420
|
-
return {
|
|
783
|
+
return { sessionId, modes, models, configOptions };
|
|
421
784
|
}
|
|
422
785
|
|
|
423
786
|
private createCanUseTool(sessionId: string): CanUseTool {
|
|
424
|
-
return async (toolName, toolInput, { suggestions, toolUseID }) =>
|
|
787
|
+
return async (toolName, toolInput, { suggestions, toolUseID, signal }) =>
|
|
425
788
|
canUseTool({
|
|
426
789
|
session: this.session,
|
|
427
790
|
toolName,
|
|
428
791
|
toolInput: toolInput as Record<string, unknown>,
|
|
429
792
|
toolUseID,
|
|
430
793
|
suggestions,
|
|
794
|
+
signal,
|
|
431
795
|
client: this.client,
|
|
432
796
|
sessionId,
|
|
433
797
|
fileContentCache: this.fileContentCache,
|
|
434
798
|
logger: this.logger,
|
|
435
|
-
|
|
799
|
+
updateConfigOption: (configId: string, value: string) =>
|
|
800
|
+
this.updateConfigOption(configId, value),
|
|
436
801
|
});
|
|
437
802
|
}
|
|
438
803
|
|
|
439
|
-
private createOnModeChange(
|
|
804
|
+
private createOnModeChange() {
|
|
440
805
|
return async (newMode: TwigExecutionMode) => {
|
|
441
806
|
if (this.session) {
|
|
442
807
|
this.session.permissionMode = newMode;
|
|
443
808
|
}
|
|
444
|
-
await this.
|
|
809
|
+
await this.updateConfigOption("mode", newMode);
|
|
445
810
|
};
|
|
446
811
|
}
|
|
447
812
|
|
|
448
|
-
private
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
813
|
+
private buildConfigOptions(
|
|
814
|
+
currentModeId: string,
|
|
815
|
+
modelOptions: {
|
|
816
|
+
currentModelId: string;
|
|
817
|
+
options: SessionConfigSelectOption[];
|
|
818
|
+
},
|
|
819
|
+
): SessionConfigOption[] {
|
|
454
820
|
const modeOptions = getAvailableModes().map((mode) => ({
|
|
455
821
|
value: mode.id,
|
|
456
822
|
name: mode.name,
|
|
457
823
|
description: mode.description ?? undefined,
|
|
458
824
|
}));
|
|
459
825
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
category: "model" as SessionConfigOptionCategory,
|
|
482
|
-
description: "Choose which model Claude should use",
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
return options;
|
|
826
|
+
return [
|
|
827
|
+
{
|
|
828
|
+
id: "mode",
|
|
829
|
+
name: "Approval Preset",
|
|
830
|
+
type: "select",
|
|
831
|
+
currentValue: currentModeId,
|
|
832
|
+
options: modeOptions,
|
|
833
|
+
category: "mode" as SessionConfigOptionCategory,
|
|
834
|
+
description:
|
|
835
|
+
"Choose an approval and sandboxing preset for your session",
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
id: "model",
|
|
839
|
+
name: "Model",
|
|
840
|
+
type: "select",
|
|
841
|
+
currentValue: modelOptions.currentModelId,
|
|
842
|
+
options: modelOptions.options,
|
|
843
|
+
category: "model" as SessionConfigOptionCategory,
|
|
844
|
+
description: "Choose which model Claude should use",
|
|
845
|
+
},
|
|
846
|
+
];
|
|
486
847
|
}
|
|
487
848
|
|
|
488
|
-
private async
|
|
489
|
-
const
|
|
490
|
-
const serialized = JSON.stringify(configOptions);
|
|
491
|
-
if (
|
|
492
|
-
this.lastSentConfigOptions &&
|
|
493
|
-
JSON.stringify(this.lastSentConfigOptions) === serialized
|
|
494
|
-
) {
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
this.lastSentConfigOptions = configOptions;
|
|
849
|
+
private async sendAvailableCommandsUpdate(): Promise<void> {
|
|
850
|
+
const commands = await this.session.query.supportedCommands();
|
|
499
851
|
await this.client.sessionUpdate({
|
|
500
|
-
sessionId:
|
|
852
|
+
sessionId: this.sessionId,
|
|
501
853
|
update: {
|
|
502
|
-
sessionUpdate: "
|
|
503
|
-
|
|
854
|
+
sessionUpdate: "available_commands_update",
|
|
855
|
+
availableCommands: getAvailableSlashCommands(commands),
|
|
504
856
|
},
|
|
505
857
|
});
|
|
506
858
|
}
|
|
507
859
|
|
|
508
|
-
private
|
|
509
|
-
const backupExists = fs.existsSync(
|
|
510
|
-
path.resolve(os.homedir(), ".claude.json.backup"),
|
|
511
|
-
);
|
|
512
|
-
const configExists = fs.existsSync(
|
|
513
|
-
path.resolve(os.homedir(), ".claude.json"),
|
|
514
|
-
);
|
|
515
|
-
if (backupExists && !configExists) {
|
|
516
|
-
throw RequestError.authRequired();
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
private async trySetModel(q: Query, modelId: string) {
|
|
860
|
+
private async replaySessionHistory(sessionId: string): Promise<void> {
|
|
521
861
|
try {
|
|
522
|
-
await
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
}
|
|
862
|
+
const messages = await getSessionMessages(sessionId, {
|
|
863
|
+
dir: this.session.cwd,
|
|
864
|
+
});
|
|
527
865
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
866
|
+
const replayContext = {
|
|
867
|
+
session: this.session,
|
|
868
|
+
sessionId,
|
|
869
|
+
client: this.client,
|
|
870
|
+
toolUseCache: this.toolUseCache,
|
|
871
|
+
fileContentCache: this.fileContentCache,
|
|
872
|
+
logger: this.logger,
|
|
873
|
+
registerHooks: false,
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
for (const msg of messages) {
|
|
877
|
+
const sdkMessage = {
|
|
878
|
+
type: msg.type,
|
|
879
|
+
message: msg.message as {
|
|
880
|
+
content: string | Array<{ type: string; text?: string }>;
|
|
881
|
+
role: typeof msg.type;
|
|
882
|
+
},
|
|
883
|
+
parent_tool_use_id: msg.parent_tool_use_id,
|
|
884
|
+
};
|
|
885
|
+
await handleUserAssistantMessage(
|
|
886
|
+
sdkMessage as Parameters<typeof handleUserAssistantMessage>[0],
|
|
887
|
+
replayContext,
|
|
888
|
+
);
|
|
535
889
|
}
|
|
536
|
-
|
|
537
|
-
|
|
890
|
+
} catch (err) {
|
|
891
|
+
this.logger.warn("Failed to replay session history", {
|
|
892
|
+
sessionId,
|
|
893
|
+
error: err instanceof Error ? err.message : String(err),
|
|
894
|
+
});
|
|
538
895
|
}
|
|
539
896
|
}
|
|
540
897
|
|
|
898
|
+
// ================================
|
|
899
|
+
// EXTENSION METHODS
|
|
900
|
+
// ================================
|
|
901
|
+
|
|
541
902
|
/**
|
|
542
903
|
* Fire-and-forget: fetch slash commands and MCP tool metadata in parallel.
|
|
543
904
|
* Both populate caches used later — neither is needed to return configOptions.
|
|
544
905
|
*/
|
|
545
|
-
private deferBackgroundFetches(q: Query
|
|
906
|
+
private deferBackgroundFetches(q: Query): void {
|
|
546
907
|
Promise.all([
|
|
547
|
-
|
|
908
|
+
new Promise<void>((resolve) => setTimeout(resolve, 10)).then(() =>
|
|
909
|
+
this.sendAvailableCommandsUpdate(),
|
|
910
|
+
),
|
|
548
911
|
fetchMcpToolMetadata(q, this.logger),
|
|
549
|
-
])
|
|
550
|
-
.
|
|
551
|
-
|
|
552
|
-
})
|
|
553
|
-
.catch((err) => {
|
|
554
|
-
this.logger.warn("Failed to fetch deferred session data", { err });
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
private sendAvailableCommandsUpdate(
|
|
559
|
-
sessionId: string,
|
|
560
|
-
availableCommands: AvailableCommand[],
|
|
561
|
-
) {
|
|
562
|
-
setTimeout(() => {
|
|
563
|
-
this.client.sessionUpdate({
|
|
564
|
-
sessionId,
|
|
565
|
-
update: {
|
|
566
|
-
sessionUpdate: "available_commands_update",
|
|
567
|
-
availableCommands,
|
|
568
|
-
},
|
|
569
|
-
});
|
|
570
|
-
}, 0);
|
|
912
|
+
]).catch((err) =>
|
|
913
|
+
this.logger.error("Background fetch failed", { error: err }),
|
|
914
|
+
);
|
|
571
915
|
}
|
|
572
916
|
|
|
573
917
|
private async broadcastUserMessage(params: PromptRequest): Promise<void> {
|
|
@@ -583,87 +927,4 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
583
927
|
this.appendNotification(params.sessionId, notification);
|
|
584
928
|
}
|
|
585
929
|
}
|
|
586
|
-
|
|
587
|
-
private async processMessages(sessionId: string): Promise<PromptResponse> {
|
|
588
|
-
const context = {
|
|
589
|
-
session: this.session,
|
|
590
|
-
sessionId,
|
|
591
|
-
client: this.client,
|
|
592
|
-
toolUseCache: this.toolUseCache,
|
|
593
|
-
fileContentCache: this.fileContentCache,
|
|
594
|
-
logger: this.logger,
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
while (true) {
|
|
598
|
-
const { value: message, done } = await this.session.query.next();
|
|
599
|
-
|
|
600
|
-
if (done || !message) {
|
|
601
|
-
return this.handleSessionEnd();
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const response = await this.handleMessage(message, context);
|
|
605
|
-
if (response) {
|
|
606
|
-
return response;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
private handleSessionEnd(): PromptResponse {
|
|
612
|
-
if (this.session.cancelled) {
|
|
613
|
-
return {
|
|
614
|
-
stopReason: "cancelled",
|
|
615
|
-
_meta: this.session.interruptReason
|
|
616
|
-
? { interruptReason: this.session.interruptReason }
|
|
617
|
-
: undefined,
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
throw new Error("Session did not end in result");
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
private async handleMessage(
|
|
624
|
-
message: SDKMessage,
|
|
625
|
-
context: Parameters<typeof handleSystemMessage>[1],
|
|
626
|
-
): Promise<PromptResponse | null> {
|
|
627
|
-
switch (message.type) {
|
|
628
|
-
case "system":
|
|
629
|
-
await handleSystemMessage(message, context);
|
|
630
|
-
return null;
|
|
631
|
-
|
|
632
|
-
case "result": {
|
|
633
|
-
const result = handleResultMessage(message, context);
|
|
634
|
-
if (result.error) throw result.error;
|
|
635
|
-
if (result.shouldStop) {
|
|
636
|
-
return {
|
|
637
|
-
stopReason: result.stopReason as "end_turn" | "max_turn_requests",
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
case "stream_event":
|
|
644
|
-
await handleStreamEvent(message, context);
|
|
645
|
-
return null;
|
|
646
|
-
|
|
647
|
-
case "user":
|
|
648
|
-
case "assistant": {
|
|
649
|
-
const result = await handleUserAssistantMessage(message, context);
|
|
650
|
-
if (result.error) throw result.error;
|
|
651
|
-
if (result.shouldStop) {
|
|
652
|
-
return { stopReason: "end_turn" };
|
|
653
|
-
}
|
|
654
|
-
return null;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
case "tool_progress":
|
|
658
|
-
case "auth_status":
|
|
659
|
-
case "tool_use_summary":
|
|
660
|
-
return null;
|
|
661
|
-
|
|
662
|
-
default:
|
|
663
|
-
// SDKMessage union includes undefined types (SDKRateLimitEvent, SDKPromptSuggestionMessage)
|
|
664
|
-
// that resolve to `any`, preventing exhaustive narrowing
|
|
665
|
-
unreachable(message as never, this.logger);
|
|
666
|
-
return null;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
930
|
}
|