Skip to content

Commit 218661f

Browse files
authored
ACP Session Usage (agentclientprotocol#345)
The AI in agentclientprotocol#344 had one good idea: only use the usage from the last assistant message for context size. Of course, `usage` on the AssistantMessage is not documented in the Agent SDK (we need to do an ugly cast), but this seems to work alright. We don't get the exact new usage after compaction, so I set it to zero.
1 parent 5ede2b7 commit 218661f

1 file changed

Lines changed: 81 additions & 4 deletions

File tree

src/acp-agent.ts

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,20 @@ export interface Logger {
134134
error: (...args: any[]) => void;
135135
}
136136

137+
type AccumulatedUsage = {
138+
inputTokens: number;
139+
outputTokens: number;
140+
cachedReadTokens: number;
141+
cachedWriteTokens: number;
142+
};
143+
137144
type Session = {
138145
query: Query;
139146
input: Pushable<SDKUserMessage>;
140147
cancelled: boolean;
141148
permissionMode: PermissionMode;
142149
settingsManager: SettingsManager;
150+
accumulatedUsage: AccumulatedUsage;
143151
configOptions: SessionConfigOption[];
144152
};
145153

@@ -407,6 +415,14 @@ export class ClaudeAcpAgent implements Agent {
407415
}
408416

409417
this.sessions[params.sessionId].cancelled = false;
418+
this.sessions[params.sessionId].accumulatedUsage = {
419+
inputTokens: 0,
420+
outputTokens: 0,
421+
cachedReadTokens: 0,
422+
cachedWriteTokens: 0,
423+
};
424+
425+
let lastAssistantTotalUsage: number | null = null;
410426

411427
const { query, input } = this.sessions[params.sessionId];
412428

@@ -423,6 +439,11 @@ export class ClaudeAcpAgent implements Agent {
423439

424440
switch (message.type) {
425441
case "system":
442+
if (message.subtype === "compact_boundary") {
443+
// We don't know the exact size, but since we compacted,
444+
// we set it to zero. The client gets the exact size on the next message.
445+
lastAssistantTotalUsage = 0;
446+
}
426447
switch (message.subtype) {
427448
case "init":
428449
break;
@@ -447,6 +468,47 @@ export class ClaudeAcpAgent implements Agent {
447468
return { stopReason: "cancelled" };
448469
}
449470

471+
// Accumulate usage from this result
472+
const session = this.sessions[params.sessionId];
473+
session.accumulatedUsage.inputTokens += message.usage.input_tokens;
474+
session.accumulatedUsage.outputTokens += message.usage.output_tokens;
475+
session.accumulatedUsage.cachedReadTokens += message.usage.cache_read_input_tokens;
476+
session.accumulatedUsage.cachedWriteTokens += message.usage.cache_creation_input_tokens;
477+
478+
// Calculate context window size from modelUsage (minimum across all models used)
479+
const contextWindows = Object.values(message.modelUsage).map((m) => m.contextWindow);
480+
const contextWindowSize =
481+
contextWindows.length > 0 ? Math.min(...contextWindows) : 200000;
482+
483+
// Send usage_update notification
484+
if (lastAssistantTotalUsage !== null) {
485+
await this.client.sessionUpdate({
486+
sessionId: params.sessionId,
487+
update: {
488+
sessionUpdate: "usage_update",
489+
used: lastAssistantTotalUsage,
490+
size: contextWindowSize,
491+
cost: {
492+
amount: message.total_cost_usd,
493+
currency: "USD",
494+
},
495+
},
496+
});
497+
}
498+
499+
// Build the usage response
500+
const usage: PromptResponse["usage"] = {
501+
inputTokens: session.accumulatedUsage.inputTokens,
502+
outputTokens: session.accumulatedUsage.outputTokens,
503+
cachedReadTokens: session.accumulatedUsage.cachedReadTokens,
504+
cachedWriteTokens: session.accumulatedUsage.cachedWriteTokens,
505+
totalTokens:
506+
session.accumulatedUsage.inputTokens +
507+
session.accumulatedUsage.outputTokens +
508+
session.accumulatedUsage.cachedReadTokens +
509+
session.accumulatedUsage.cachedWriteTokens,
510+
};
511+
450512
switch (message.subtype) {
451513
case "success": {
452514
if (message.result.includes("Please run /login")) {
@@ -455,7 +517,7 @@ export class ClaudeAcpAgent implements Agent {
455517
if (message.is_error) {
456518
throw RequestError.internalError(undefined, message.result);
457519
}
458-
return { stopReason: "end_turn" };
520+
return { stopReason: "end_turn", usage };
459521
}
460522
case "error_during_execution":
461523
if (message.is_error) {
@@ -464,7 +526,7 @@ export class ClaudeAcpAgent implements Agent {
464526
message.errors.join(", ") || message.subtype,
465527
);
466528
}
467-
return { stopReason: "end_turn" };
529+
return { stopReason: "end_turn", usage };
468530
case "error_max_budget_usd":
469531
case "error_max_turns":
470532
case "error_max_structured_output_retries":
@@ -474,7 +536,7 @@ export class ClaudeAcpAgent implements Agent {
474536
message.errors.join(", ") || message.subtype,
475537
);
476538
}
477-
return { stopReason: "max_turn_requests" };
539+
return { stopReason: "max_turn_requests", usage };
478540
default:
479541
unreachable(message, this.logger);
480542
break;
@@ -500,6 +562,16 @@ export class ClaudeAcpAgent implements Agent {
500562
break;
501563
}
502564

565+
// Store latest assistant usage (excluding subagents)
566+
if ((message.message as any).usage && message.parent_tool_use_id === null) {
567+
const messageWithUsage = message.message as unknown as SDKResultMessage;
568+
lastAssistantTotalUsage =
569+
messageWithUsage.usage.input_tokens +
570+
messageWithUsage.usage.output_tokens +
571+
messageWithUsage.usage.cache_read_input_tokens +
572+
messageWithUsage.usage.cache_creation_input_tokens;
573+
}
574+
503575
// Slash commands like /compact can generate invalid output... doesn't match
504576
// their own docs: https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-slash-commands#%2Fcompact-compact-conversation-history
505577
if (
@@ -978,7 +1050,6 @@ export class ClaudeAcpAgent implements Agent {
9781050
const options: Options = {
9791051
systemPrompt,
9801052
settingSources: ["user", "project", "local"],
981-
stderr: (err) => this.logger.error(err),
9821053
...(maxThinkingTokens !== undefined && { maxThinkingTokens }),
9831054
...userProvidedOptions,
9841055
// Override certain fields that must be controlled by ACP
@@ -1052,6 +1123,12 @@ export class ClaudeAcpAgent implements Agent {
10521123
cancelled: false,
10531124
permissionMode,
10541125
settingsManager,
1126+
accumulatedUsage: {
1127+
inputTokens: 0,
1128+
outputTokens: 0,
1129+
cachedReadTokens: 0,
1130+
cachedWriteTokens: 0,
1131+
},
10551132
configOptions: [],
10561133
};
10571134

0 commit comments

Comments
 (0)