Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/code/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ export const executionModeSchema = z.enum([
export type ExecutionMode = z.infer<typeof executionModeSchema>;

// Effort level schema and type - shared between main and renderer
export const effortLevelSchema = z.enum(["low", "medium", "high", "max"]);
export const effortLevelSchema = z.enum([
"low",
"medium",
"high",
"xhigh",
"max",
]);
export type EffortLevel = z.infer<typeof effortLevelSchema>;

interface UserBasic {
Expand Down Expand Up @@ -72,7 +78,7 @@ export interface TaskRun {
branch: string | null;
runtime_adapter?: "claude" | "codex" | null;
model?: string | null;
reasoning_effort?: "low" | "medium" | "high" | "max" | null;
reasoning_effort?: "low" | "medium" | "high" | "xhigh" | "max" | null;
stage?: string | null; // Current stage (e.g., 'research', 'plan', 'build')
environment?: "local" | "cloud";
status: TaskRunStatus;
Expand Down
4 changes: 2 additions & 2 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@
"vitest": "^2.1.8"
},
"dependencies": {
"@agentclientprotocol/sdk": "0.16.1",
"@agentclientprotocol/sdk": "0.19.0",
"ajv": "^8.17.1",
"@anthropic-ai/claude-agent-sdk": "0.2.112",
"@anthropic-ai/sdk": "^0.78.0",
"@anthropic-ai/sdk": "0.89.0",
"@hono/node-server": "^1.19.9",
"@opentelemetry/api-logs": "^0.208.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
Expand Down
29 changes: 25 additions & 4 deletions packages/agent/src/adapters/claude/UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
## Fork Point

- **Forked**: v0.10.9, commit `5411e0f4`, Dec 2 2025
- **Last sync**: v0.22.2, commit `07db59e`, March 25 2026
- **SDK**: `@anthropic-ai/claude-agent-sdk` 0.2.76, `@agentclientprotocol/sdk` 0.16.1
- **Last sync**: v0.30.0, commit `e9dd452`, April 20 2026
- **SDK**: `@anthropic-ai/claude-agent-sdk` 0.2.112 (0.2.114 breaks session init, see agentclientprotocol/claude-agent-acp#575), `@agentclientprotocol/sdk` 0.19.0

## File Mapping

Expand Down Expand Up @@ -50,11 +50,32 @@ Fork of `@anthropic-ai/claude-agent-acp`. Upstream repo: https://github.com/anth
| permissionMode | Hardcoded `"default"` | Reads from `meta.permissionMode` | More flexible mode selection |
| Session storage | `this.sessions[sessionId]` (multi) | `this.session` (single) | Architectural choice |
| bypassPermissions | `updatedPermissions` with `destination: "session"` | No `updatedPermissions` | Different permission persistence |
| Auth methods | Always returns `claude-login` auth method | Returns empty `authMethods` | Auth handled externally |
| Auth methods | `claude-ai-login` + `console-login` | Returns empty `authMethods` | Auth handled externally |
| `auto` mode | Model classifier for auto-approval | Not implemented | PostHog uses its own permission model |
| Session fingerprinting | Implicit teardown on cwd/mcp change | Explicit `refreshSession()` | Caller-initiated is more predictable |
| Shutdown on ACP close | Process exits | No standalone process | Agent is embedded in server |

## Changes Ported in v0.30.0 Sync

- **SDK bumps**: claude-agent-sdk 0.2.112 -> 0.2.114, ACP SDK 0.16.1 -> 0.19.0, anthropic SDK -> 0.89.0
- **Null-safe usage tokens** (v0.29.2): Guard against null usage fields from SDK
- **SettingsManager race fix** (v0.25.0): `initPromise` prevents concurrent `initialize()`/`setCwd()` corruption
- **Malformed settings warning** (v0.25.0): Log warning for non-ENOENT settings file errors
- **Idle state end-of-turn** (v0.23.0): `CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS=1` + `session_state_changed` idle handler
- **Mid-stream usage updates** (v0.29.1): Fire `usage_update` from `message_start`/`message_delta` stream events
- **Raw SDK message relay** (v0.27.0): `emitRawSDKMessages` on `NewSessionMeta` for opt-in diagnostics
- **Effort level sync** (v0.25.x): `xhigh` level added, `applyFlagSettings` on effort change

## Skipped in v0.30.0 Sync

- **`auto` permission mode** (v0.25.0): PostHog has its own permission model
- **Separate auth methods** (v0.25.0): PostHog returns empty authMethods
- **Session fingerprinting** (v0.25.3): PostHog uses explicit `refreshSession()` instead
- **Process exit on ACP close** (v0.27.0): PostHog embeds agent in server

## Next Sync

1. Check upstream changelog since v0.22.2
1. Check upstream changelog since v0.30.0
2. Diff upstream source against PostHog Code using the file mapping above
3. Port in phases: bug fixes first, then features
4. After each phase: `pnpm --filter agent typecheck && pnpm --filter agent build && pnpm lint`
Expand Down
146 changes: 132 additions & 14 deletions packages/agent/src/adapters/claude/claude-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import type {
BackgroundTerminal,
EffortLevel,
NewSessionMeta,
SDKMessageFilter,
Session,
ToolUseCache,
} from "./types";
Expand All @@ -118,6 +119,19 @@ function sanitizeTitle(text: string): string {
return `${sanitized.slice(0, MAX_TITLE_LENGTH - 1)}…`;
}

function shouldEmitRawMessage(
config: boolean | SDKMessageFilter[],
message: { type: string; subtype?: string },
): boolean {
if (config === true) return true;
if (config === false) return false;
return config.some(
(f) =>
f.type === message.type &&
(f.subtype === undefined || f.subtype === message.subtype),
);
}

export interface ClaudeAcpAgentOptions {
onProcessSpawned?: (info: ProcessSpawnedInfo) => void;
onProcessExited?: (pid: number) => void;
Expand Down Expand Up @@ -356,6 +370,12 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
this.session.promptRunning = true;
let handedOff = false;
let lastAssistantTotalUsage: number | null = null;
let lastStreamUsage = {
input_tokens: 0,
output_tokens: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
};
if (this.session.lastContextWindowSize == null) {
this.session.lastContextWindowSize = this.getContextWindowForModel(
this.session.modelId ?? "",
Expand Down Expand Up @@ -401,6 +421,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
break;
}

if (
this.session.emitRawSDKMessages &&
shouldEmitRawMessage(this.session.emitRawSDKMessages, message)
) {
await this.client.extNotification("_claude/sdkMessage", {
sessionId: params.sessionId,
message: message as Record<string, unknown>,
});
}

switch (message.type) {
case "system":
if (message.subtype === "compact_boundary") {
Expand All @@ -420,6 +450,35 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
if (message.subtype === "local_command_output") {
promptReplayed = true;
}
if (
message.subtype === "session_state_changed" &&
(message as Record<string, unknown>).state === "idle"
) {
if (!promptReplayed) {
this.logger.debug("Skipping idle state before prompt replay", {
sessionId: params.sessionId,
});
break;
}

const acc = this.session.accumulatedUsage;
const totalUsed =
acc.inputTokens +
acc.outputTokens +
acc.cachedReadTokens +
acc.cachedWriteTokens;

await this.client.sessionUpdate({
sessionId: params.sessionId,
update: {
sessionUpdate: "usage_update",
used: totalUsed,
size: lastContextWindowSize,
},
});

return { stopReason: "end_turn" };
}
await handleSystemMessage(message, context);
break;

Expand All @@ -437,15 +496,15 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
return { stopReason: "cancelled" };
}

// Accumulate usage from this result
// Accumulate usage from this result (guard against null from SDK)
this.session.accumulatedUsage.inputTokens +=
message.usage.input_tokens;
message.usage.input_tokens ?? 0;
this.session.accumulatedUsage.outputTokens +=
message.usage.output_tokens;
message.usage.output_tokens ?? 0;
this.session.accumulatedUsage.cachedReadTokens +=
message.usage.cache_read_input_tokens;
message.usage.cache_read_input_tokens ?? 0;
this.session.accumulatedUsage.cachedWriteTokens +=
message.usage.cache_creation_input_tokens;
message.usage.cache_creation_input_tokens ?? 0;

// SDK can underreport context window (e.g. 200k for 1M models).
// Use SDK value only if it's larger than what gateway reported.
Expand Down Expand Up @@ -540,9 +599,56 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
return { stopReason: result.stopReason ?? "end_turn", usage };
}

case "stream_event":
case "stream_event": {
if (
message.parent_tool_use_id === null &&
(message.event.type === "message_start" ||
message.event.type === "message_delta")
) {
if (message.event.type === "message_start") {
const u = message.event.message.usage;
lastStreamUsage = {
input_tokens: u.input_tokens ?? 0,
output_tokens: u.output_tokens ?? 0,
cache_read_input_tokens: u.cache_read_input_tokens ?? 0,
cache_creation_input_tokens:
u.cache_creation_input_tokens ?? 0,
};
} else {
const u = message.event.usage;
lastStreamUsage = {
input_tokens: u.input_tokens ?? lastStreamUsage.input_tokens,
output_tokens: u.output_tokens,
Comment thread
charlesvien marked this conversation as resolved.
cache_read_input_tokens:
u.cache_read_input_tokens ??
lastStreamUsage.cache_read_input_tokens,
cache_creation_input_tokens:
u.cache_creation_input_tokens ??
lastStreamUsage.cache_creation_input_tokens,
};
}

const nextTotal =
lastStreamUsage.input_tokens +
lastStreamUsage.output_tokens +
lastStreamUsage.cache_read_input_tokens +
lastStreamUsage.cache_creation_input_tokens;

if (nextTotal !== lastAssistantTotalUsage) {
lastAssistantTotalUsage = nextTotal;
await this.client.sessionUpdate({
sessionId: params.sessionId,
update: {
sessionUpdate: "usage_update",
used: nextTotal,
size: lastContextWindowSize,
},
});
}
}
await handleStreamEvent(message, context);
break;
}

case "user":
case "assistant": {
Expand Down Expand Up @@ -591,16 +697,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
const usage = (
message.message as unknown as Record<string, unknown>
).usage as {
input_tokens: number;
output_tokens: number;
cache_read_input_tokens: number;
cache_creation_input_tokens: number;
input_tokens: number | null;
output_tokens: number | null;
cache_read_input_tokens: number | null;
cache_creation_input_tokens: number | null;
};
lastAssistantTotalUsage =
usage.input_tokens +
usage.output_tokens +
usage.cache_read_input_tokens +
usage.cache_creation_input_tokens;
(usage.input_tokens ?? 0) +
(usage.output_tokens ?? 0) +
(usage.cache_read_input_tokens ?? 0) +
(usage.cache_creation_input_tokens ?? 0);

await this.client.sessionUpdate({
sessionId: params.sessionId,
Expand Down Expand Up @@ -884,6 +990,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
const newEffort = resolvedValue as EffortLevel;
this.session.effort = newEffort;
this.session.queryOptions.effort = newEffort;
await this.session.query.applyFlagSettings({
// @ts-expect-error SDK Settings.effortLevel omits "max" but runtime accepts it
effortLevel: newEffort,
});
}

this.session.configOptions = this.session.configOptions.map((o) =>
Expand Down Expand Up @@ -1047,6 +1157,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
promptRunning: false,
pendingMessages: new Map(),
nextPendingOrder: 0,
emitRawSDKMessages: meta?.claudeCode?.emitRawSDKMessages ?? false,

// Custom properties
cwd,
Expand Down Expand Up @@ -1325,6 +1436,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
if (this.session.effort) {
this.session.effort = undefined;
this.session.queryOptions.effort = undefined;
void this.session.query.applyFlagSettings({
Comment thread
charlesvien marked this conversation as resolved.
effortLevel: undefined,
});
}
return;
}
Expand All @@ -1338,6 +1452,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
if (resolvedValue !== currentValue && this.session.effort) {
this.session.effort = resolvedValue as EffortLevel;
this.session.queryOptions.effort = resolvedValue as EffortLevel;
void this.session.query.applyFlagSettings({
// @ts-expect-error SDK Settings.effortLevel omits "max" but runtime accepts it
effortLevel: resolvedValue,
});
}

const effortConfig: SessionConfigOption = {
Expand Down
3 changes: 0 additions & 3 deletions packages/agent/src/adapters/claude/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,6 @@ export const createPostToolUseHook =
);
delete toolUseCallbacks[toolUseID];
} else {
logger?.error(
`No onPostToolUseHook found for tool use ID: ${toolUseID}`,
);
delete toolUseCallbacks[toolUseID];
}
}
Expand Down
16 changes: 11 additions & 5 deletions packages/agent/src/adapters/claude/session/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ const MODELS_WITH_EFFORT = new Set([
"claude-sonnet-4-6",
]);

const MODELS_WITH_MAX_EFFORT = new Set(["claude-opus-4-6", "claude-opus-4-7"]);
const MODELS_WITH_XHIGH_EFFORT = new Set([
"claude-opus-4-6",
"claude-opus-4-7",
]);

export function supportsEffort(modelId: string): boolean {
return MODELS_WITH_EFFORT.has(modelId);
}

export function supportsMaxEffort(modelId: string): boolean {
return MODELS_WITH_MAX_EFFORT.has(modelId);
export function supportsXhighEffort(modelId: string): boolean {
return MODELS_WITH_XHIGH_EFFORT.has(modelId);
}

const MODELS_TO_EXCLUDE_MCP_TOOLS = new Set(["claude-haiku-4-5"]);
Expand All @@ -60,8 +63,11 @@ export function getEffortOptions(modelId: string): EffortOption[] | null {
{ value: "high", name: "High" },
];

if (supportsMaxEffort(modelId)) {
options.push({ value: "max", name: "Max" });
if (supportsXhighEffort(modelId)) {
options.push(
{ value: "xhigh", name: "Extra High" },
{ value: "max", name: "Max" },
);
}

return options;
Expand Down
2 changes: 2 additions & 0 deletions packages/agent/src/adapters/claude/session/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ function buildEnvironment(): Record<string, string> {
CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL: "true",
// Offload all MCP tools by default
ENABLE_TOOL_SEARCH: "auto:0",
// Enable idle state as end-of-turn signal (required for SDK 0.2.114+)
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
};
}

Expand Down
Loading
Loading