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
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"test": "vitest run"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.111",
"@anthropic-ai/claude-agent-sdk": "^0.3.154",
"@cursor/sdk": "^1.0.13",
"@effect/platform-bun": "catalog:",
"@effect/platform-node": "catalog:",
Expand Down
24 changes: 23 additions & 1 deletion apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,14 +345,36 @@ function runtimeEventToActivities(
];
}

case "tool.denied": {
return [
{
id: event.eventId,
createdAt: event.createdAt,
tone: "error",
kind: "tool.denied",
summary: `Tool denied: ${event.payload.toolName}`,
payload: {
toolName: event.payload.toolName,
...(event.payload.toolUseId ? { toolUseId: event.payload.toolUseId } : {}),
...(event.payload.reason ? { detail: truncateDetail(event.payload.reason) } : {}),
...(event.payload.agentId ? { agentId: event.payload.agentId } : {}),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
},
];
}

case "runtime.warning": {
return [
{
id: event.eventId,
createdAt: event.createdAt,
tone: "info",
kind: "runtime.warning",
summary: "Runtime warning",
// Use the adapter-supplied message as the row label so the work log
// shows what the warning was about, not a generic "Runtime warning".
summary: truncateDetail(event.payload.message, 120),
payload: {
message: truncateDetail(event.payload.message),
...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}),
Expand Down
87 changes: 80 additions & 7 deletions apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { ServerConfig } from "../../config.ts";
import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts";
import {
getClaudeModelCapabilities,
isClaudeUltracodeEffort,
normalizeClaudeCliEffort,
resolveClaudeApiModelId,
resolveClaudeEffort,
Expand Down Expand Up @@ -255,8 +256,11 @@ function normalizeClaudeStreamMessages(
return squashed.length > 0 ? [squashed] : [];
}

function getEffectiveClaudeAgentEffort(effort: string | null | undefined): ClaudeSdkEffort | null {
const normalized = normalizeClaudeCliEffort(effort);
function getEffectiveClaudeAgentEffort(
effort: string | null | undefined,
model: string | null | undefined,
): ClaudeSdkEffort | null {
const normalized = normalizeClaudeCliEffort(effort, model);
return normalized ? (normalized as ClaudeSdkEffort) : null;
}

Expand Down Expand Up @@ -964,6 +968,52 @@ function sdkNativeMethod(message: SDKMessage): string {
return `claude/${message.type}`;
}

// Discriminator/identity keys carry no human-readable content; everything else
// on an unmodeled SDK message is potentially worth surfacing in the work log.
const SDK_MESSAGE_NOISE_KEYS = new Set([
"type",
"subtype",
"uuid",
"parent_uuid",
"session_id",
"parent_tool_use_id",
"request_id",
]);

// Pull the salient scalar content out of a message the adapter doesn't model
// yet, so the work-log row shows what actually arrived (e.g. a notification's
// text) instead of an opaque "unhandled subtype" placeholder. Nested structures
// are left to the full payload retained in the event's `detail`.
function previewUnknownSdkContent(message: unknown): string | undefined {
if (!message || typeof message !== "object") {
return undefined;
}
const parts: string[] = [];
for (const [key, value] of Object.entries(message as Record<string, unknown>)) {
if (SDK_MESSAGE_NOISE_KEYS.has(key)) {
continue;
}
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.length > 0) {
parts.push(`${key}: ${trimmed}`);
}
} else if (typeof value === "number" || typeof value === "boolean") {
parts.push(`${key}: ${String(value)}`);
}
}
if (parts.length === 0) {
return undefined;
}
const joined = parts.join(" · ");
return joined.length > 280 ? `${joined.slice(0, 279)}…` : joined;
}

function describeUnknownSdkMessage(kind: string, message: unknown): string {
const preview = previewUnknownSdkContent(message);
return preview ? `${kind} — ${preview}` : `${kind} (no displayable text content)`;
}

function sdkNativeItemId(message: SDKMessage): string | undefined {
if (message.type === "assistant") {
const maybeId = (message.message as { id?: unknown }).id;
Expand Down Expand Up @@ -2247,10 +2297,31 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
},
});
return;
case "thinking_tokens":
return;
case "permission_denied":
yield* offerRuntimeEvent({
...base,
type: "tool.denied",
payload: {
toolName: message.tool_name,
...(message.tool_use_id ? { toolUseId: message.tool_use_id } : {}),
...(message.decision_reason ? { reason: message.decision_reason } : {}),
...(message.agent_id ? { agentId: message.agent_id } : {}),
},
});
return;
case "mirror_error":
yield* emitRuntimeError(
context,
`Claude workspace mirror error: ${message.error}`,
message,
);
return;
default:
yield* emitRuntimeWarning(
context,
`Unhandled Claude system message subtype '${message.subtype}'.`,
describeUnknownSdkMessage(`Claude system message '${message.subtype}'`, message),
message,
);
return;
Expand Down Expand Up @@ -2364,7 +2435,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
default:
yield* emitRuntimeWarning(
context,
`Unhandled Claude SDK message type '${message.type}'.`,
describeUnknownSdkMessage(`Claude SDK message '${message.type}'`, message),
message,
);
return;
Expand Down Expand Up @@ -2887,7 +2958,8 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
const thinking = thinkingSupported
? getModelSelectionBooleanOptionValue(modelSelection, "thinking")
: undefined;
const effectiveEffort = getEffectiveClaudeAgentEffort(effort);
const ultracode = isClaudeUltracodeEffort(effort);
const effectiveEffort = getEffectiveClaudeAgentEffort(effort, modelSelection?.model);
const runtimeModeToPermission: Record<string, PermissionMode> = {
"auto-accept-edits": "acceptEdits",
"full-access": "bypassPermissions",
Expand All @@ -2896,15 +2968,16 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
const settings = {
...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
...(fastMode ? { fastMode: true } : {}),
...(ultracode ? { ultracode: true } : {}),
};
const queryOptions: ClaudeQueryOptions = {
...(input.cwd ? { cwd: input.cwd } : {}),
...(apiModelId ? { model: apiModelId } : {}),
pathToClaudeCodeExecutable: claudeBinaryPath,
systemPrompt: { type: "preset", preset: "claude_code" },
settingSources: [...CLAUDE_SETTING_SOURCES],
// The SDK type lags the CLI here: Opus 4.7 accepts `xhigh` even though
// the published `Options["effort"]` union currently stops at `max`.
// `ultracode` is a Claude Code setting, not an API effort level. It is
// normalized to `xhigh` above and paired with `settings.ultracode`.
...(effectiveEffort
? {
effort: effectiveEffort as unknown as NonNullable<ClaudeQueryOptions["effort"]>,
Expand Down
Loading
Loading