Remove casts from Claude agent

This commit is contained in:
Mohamed Boudra
2025-12-25 11:13:29 +07:00
parent efdec18ad3
commit f02d97a851
3 changed files with 226 additions and 86 deletions
@@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
import { createServer as createHTTPServer } from "http"; import { createServer } from "http";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { import {
copyFileSync, copyFileSync,
@@ -35,6 +35,8 @@ import { AgentManager } from "../agent-manager.js";
import { AgentRegistry } from "../agent-registry.js"; import { AgentRegistry } from "../agent-registry.js";
import { createAgentMcpServer } from "../mcp-server.js"; import { createAgentMcpServer } from "../mcp-server.js";
const createHTTPServer = createServer;
function tmpCwd(): string { function tmpCwd(): string {
const dir = mkdtempSync(path.join(os.tmpdir(), "claude-agent-e2e-")); const dir = mkdtempSync(path.join(os.tmpdir(), "claude-agent-e2e-"));
try { try {
@@ -92,11 +94,21 @@ async function autoApprove(session: Awaited<ReturnType<ClaudeAgentClient["create
type ToolCallItem = Extract<AgentTimelineItem, { type: "tool_call" }>; type ToolCallItem = Extract<AgentTimelineItem, { type: "tool_call" }>;
type KeyValueObject = { [key: string]: unknown };
function isKeyValueObject(value: unknown): value is KeyValueObject {
return typeof value === "object" && value !== null;
}
function isFileWriteResult(value: unknown): value is { type: "file_write"; filePath: string } {
return isKeyValueObject(value) && value.type === "file_write" && typeof value.filePath === "string";
}
function extractCommandText(input: unknown): string | null { function extractCommandText(input: unknown): string | null {
if (!input || typeof input !== "object") { if (!isKeyValueObject(input)) {
return null; return null;
} }
const command = (input as { command?: unknown }).command; const command = input.command;
if (typeof command === "string" && command.length > 0) { if (typeof command === "string" && command.length > 0) {
return command; return command;
} }
@@ -106,8 +118,8 @@ function extractCommandText(input: unknown): string | null {
return tokens.join(" "); return tokens.join(" ");
} }
} }
if (typeof (input as { description?: string }).description === "string") { if (typeof input.description === "string") {
const description = (input as { description?: string }).description as string; const description = input.description;
if (description.length > 0) { if (description.length > 0) {
return description; return description;
} }
@@ -218,7 +230,7 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
transport = await createAgentMcpTransport(callerAgentId); transport = await createAgentMcpTransport(callerAgentId);
} }
await transport.handleRequest(req as any, res as any, req.body); await transport.handleRequest(req, res, req.body);
} catch (error) { } catch (error) {
console.error("[Agent MCP] Failed to handle request:", error); console.error("[Agent MCP] Failed to handle request:", error);
if (!res.headersSent) { if (!res.headersSent) {
@@ -452,15 +464,15 @@ describe("ClaudeAgentClient (SDK integration)", () => {
); );
const fileChangeEvent = toolCalls.find((item) => { const fileChangeEvent = toolCalls.find((item) => {
// Check for file changes in structured output.files array // Check for file changes in structured output.files array
if (item.output && typeof item.output === "object") { if (isKeyValueObject(item.output)) {
const files = (item.output as Record<string, unknown>).files; const files = item.output.files;
if (Array.isArray(files) && files.some((file) => typeof file?.path === "string" && file.path.includes("tool-test.txt"))) { if (Array.isArray(files) && files.some((file) => typeof file?.path === "string" && file.path.includes("tool-test.txt"))) {
return true; return true;
} }
} }
// Also check for file path in output structure (write/edit tools) // Also check for file path in output structure (write/edit tools)
if (item.output && typeof item.output === "object") { if (isKeyValueObject(item.output)) {
const output = item.output as Record<string, unknown>; const output = item.output;
if (output.type === "file_write" || output.type === "file_edit") { if (output.type === "file_write" || output.type === "file_edit") {
const filePath = output.filePath; const filePath = output.filePath;
if (typeof filePath === "string" && filePath.includes("tool-test.txt")) { if (typeof filePath === "string" && filePath.includes("tool-test.txt")) {
@@ -989,9 +1001,8 @@ describe("ClaudeAgentClient (SDK integration)", () => {
editTool!, editTool!,
hydratedMap, hydratedMap,
(data) => (data) =>
(data.result as any)?.type === "file_write" && isFileWriteResult(data.result) &&
typeof (data.result as any)?.filePath === "string" && data.result.filePath.includes("hydrate-proof.txt"),
((data.result as any).filePath as string).includes("hydrate-proof.txt"),
({ live, hydrated }) => { ({ live, hydrated }) => {
const liveDiff = JSON.stringify(live.result ?? {}); const liveDiff = JSON.stringify(live.result ?? {});
const hydratedDiff = JSON.stringify(hydrated.result ?? {}); const hydratedDiff = JSON.stringify(hydrated.result ?? {});
@@ -1104,7 +1115,7 @@ describe("convertClaudeHistoryEntry", () => {
server: "editor", server: "editor",
tool: "read_file", tool: "read_file",
status: "completed", status: "completed",
} as AgentTimelineItem, },
]; ];
const mapBlocks = vi.fn().mockReturnValue(stubTimeline); const mapBlocks = vi.fn().mockReturnValue(stubTimeline);
@@ -1147,9 +1158,12 @@ function recordTimelineUpdate(target: StreamHydrationUpdate[], event: AgentStrea
if (event.type !== "timeline") { if (event.type !== "timeline") {
return; return;
} }
const payload = event as Extract<AgentStreamEvent, { type: "timeline" }>;
target.push({ target.push({
event: payload as StreamHydrationUpdate["event"], event: {
type: "timeline",
provider: event.provider,
item: event.item,
},
timestamp: new Date(), timestamp: new Date(),
}); });
} }
@@ -1275,8 +1289,8 @@ function rawContainsText(raw: unknown, text: string, depth = 0): boolean {
if (Array.isArray(raw)) { if (Array.isArray(raw)) {
return raw.some((entry) => rawContainsText(entry, text, depth + 1)); return raw.some((entry) => rawContainsText(entry, text, depth + 1));
} }
if (typeof raw === "object") { if (isKeyValueObject(raw)) {
return Object.values(raw as Record<string, unknown>).some((value) => return Object.values(raw).some((value) =>
rawContainsText(value, text, depth + 1) rawContainsText(value, text, depth + 1)
); );
} }
@@ -1,13 +1,14 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import fs, { promises as fsPromises } from "node:fs"; import fs from "node:fs";
import { promises } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { import {
query, query,
type AgentDefinition, type AgentDefinition,
type CanUseTool, type CanUseTool,
type McpServerConfig as ClaudeMcpServerConfig, type McpServerConfig,
type Options as ClaudeOptions, type Options,
type PermissionMode, type PermissionMode,
type PermissionResult, type PermissionResult,
type PermissionUpdate, type PermissionUpdate,
@@ -22,6 +23,7 @@ import {
import type { import type {
AgentCapabilityFlags, AgentCapabilityFlags,
AgentClient, AgentClient,
AgentMetadata,
AgentMode, AgentMode,
AgentPermissionRequest, AgentPermissionRequest,
AgentPermissionResponse, AgentPermissionResponse,
@@ -41,6 +43,8 @@ import type {
} from "../agent-sdk-types.js"; } from "../agent-sdk-types.js";
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js"; import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
const fsPromises = promises;
const CLAUDE_CAPABILITIES: AgentCapabilityFlags = { const CLAUDE_CAPABILITIES: AgentCapabilityFlags = {
supportsStreaming: true, supportsStreaming: true,
supportsSessionPersistence: true, supportsSessionPersistence: true,
@@ -82,6 +86,9 @@ type ClaudeAgentConfig = AgentSessionConfig & { provider: "claude" };
export type ClaudeContentChunk = { type: string; [key: string]: any }; export type ClaudeContentChunk = { type: string; [key: string]: any };
type ClaudeMcpServerConfig = McpServerConfig;
type ClaudeOptions = Options;
type ClaudeAgentClientOptions = { type ClaudeAgentClientOptions = {
defaults?: { agents?: Record<string, AgentDefinition> }; defaults?: { agents?: Record<string, AgentDefinition> };
}; };
@@ -102,9 +109,7 @@ function appendCallerAgentId(url: string, agentId: string): string {
} }
} }
export function extractUserMessageText( export function extractUserMessageText(content: unknown): string | null {
content: string | ClaudeContentChunk[]
): string | null {
if (typeof content === "string") { if (typeof content === "string") {
const normalized = content.trim(); const normalized = content.trim();
return normalized.length > 0 ? normalized : null; return normalized.length > 0 ? normalized : null;
@@ -156,17 +161,127 @@ type ToolUseCacheEntry = {
started: boolean; started: boolean;
commandText?: string; commandText?: string;
files?: { path: string; kind: string }[]; files?: { path: string; kind: string }[];
input?: Record<string, unknown> | null; input?: AgentMetadata | null;
}; };
const DEFAULT_PERMISSION_TIMEOUT_MS = 120_000; const DEFAULT_PERMISSION_TIMEOUT_MS = 120_000;
function isRecord(value: unknown): value is Record<string, unknown> { function isMetadata(value: unknown): value is AgentMetadata {
return typeof value === "object" && value !== null; return typeof value === "object" && value !== null;
} }
function isPermissionMode(value: string | undefined): value is PermissionMode {
return typeof value === "string" && VALID_CLAUDE_MODES.has(value);
}
function coerceSessionMetadata(metadata: AgentMetadata | undefined): Partial<AgentSessionConfig> {
if (!isMetadata(metadata)) {
return {};
}
const result: Partial<AgentSessionConfig> = {};
if (metadata.provider === "claude" || metadata.provider === "codex" || metadata.provider === "codex-mcp") {
result.provider = metadata.provider;
}
if (typeof metadata.cwd === "string") {
result.cwd = metadata.cwd;
}
if (typeof metadata.modeId === "string") {
result.modeId = metadata.modeId;
}
if (typeof metadata.model === "string") {
result.model = metadata.model;
}
if (typeof metadata.title === "string" || metadata.title === null) {
result.title = metadata.title;
}
if (typeof metadata.approvalPolicy === "string") {
result.approvalPolicy = metadata.approvalPolicy;
}
if (typeof metadata.sandboxMode === "string") {
result.sandboxMode = metadata.sandboxMode;
}
if (typeof metadata.networkAccess === "boolean") {
result.networkAccess = metadata.networkAccess;
}
if (typeof metadata.webSearch === "boolean") {
result.webSearch = metadata.webSearch;
}
if (typeof metadata.reasoningEffort === "string") {
result.reasoningEffort = metadata.reasoningEffort;
}
if (isMetadata(metadata.agentControlMcp)) {
const url = metadata.agentControlMcp.url;
const headers = metadata.agentControlMcp.headers;
if (typeof url === "string") {
const agentControlMcp: AgentSessionConfig["agentControlMcp"] = { url };
if (isMetadata(headers)) {
const normalizedHeaders: { [key: string]: string } = {};
for (const [key, value] of Object.entries(headers)) {
if (typeof value === "string") {
normalizedHeaders[key] = value;
}
}
if (Object.keys(normalizedHeaders).length > 0) {
agentControlMcp.headers = normalizedHeaders;
}
}
result.agentControlMcp = agentControlMcp;
}
}
if (isMetadata(metadata.extra)) {
const extra: AgentSessionConfig["extra"] = {};
if (isMetadata(metadata.extra.codex)) {
extra.codex = metadata.extra.codex;
}
if (isClaudeExtra(metadata.extra.claude)) {
extra.claude = metadata.extra.claude;
}
if (extra.codex || extra.claude) {
result.extra = extra;
}
}
if (isMetadata(metadata.mcpServers)) {
result.mcpServers = metadata.mcpServers;
}
if (typeof metadata.parentAgentId === "string") {
result.parentAgentId = metadata.parentAgentId;
}
return result;
}
function isClaudeMcpServerConfig(value: unknown): value is ClaudeMcpServerConfig {
if (!isMetadata(value)) {
return false;
}
return typeof value.type === "string" || typeof value.command === "string" || typeof value.url === "string";
}
function isClaudeContentChunk(value: unknown): value is ClaudeContentChunk {
return isMetadata(value) && typeof value.type === "string";
}
function isClaudeExtra(value: unknown): value is Partial<ClaudeOptions> {
return isMetadata(value);
}
function isPermissionUpdate(value: AgentPermissionUpdate): value is PermissionUpdate {
if (!isMetadata(value)) {
return false;
}
const type = value.type;
if (type !== "addRules" && type !== "replaceRules" && type !== "removeRules") {
return false;
}
const rules = value.rules;
const behavior = value.behavior;
const destination = value.destination;
return Array.isArray(rules) && typeof behavior === "string" && typeof destination === "string";
}
export class ClaudeAgentClient implements AgentClient { export class ClaudeAgentClient implements AgentClient {
readonly provider = "claude" as const; readonly provider: "claude" = "claude";
readonly capabilities = CLAUDE_CAPABILITIES; readonly capabilities = CLAUDE_CAPABILITIES;
private readonly defaults?: { agents?: Record<string, AgentDefinition> }; private readonly defaults?: { agents?: Record<string, AgentDefinition> };
@@ -186,12 +301,12 @@ export class ClaudeAgentClient implements AgentClient {
handle: AgentPersistenceHandle, handle: AgentPersistenceHandle,
overrides?: Partial<AgentSessionConfig> overrides?: Partial<AgentSessionConfig>
): Promise<AgentSession> { ): Promise<AgentSession> {
const metadata = (handle.metadata ?? {}) as Partial<AgentSessionConfig>; const metadata = coerceSessionMetadata(handle.metadata);
const merged = { ...metadata, ...overrides } as Partial<AgentSessionConfig>; const merged: Partial<AgentSessionConfig> = { ...metadata, ...overrides };
if (!merged.cwd) { if (!merged.cwd) {
throw new Error("Claude resume requires the original working directory in metadata"); throw new Error("Claude resume requires the original working directory in metadata");
} }
const mergedConfig = { ...merged, provider: "claude" } as AgentSessionConfig; const mergedConfig: AgentSessionConfig = { ...merged, provider: "claude", cwd: merged.cwd };
const claudeConfig = this.assertConfig(mergedConfig); const claudeConfig = this.assertConfig(mergedConfig);
return new ClaudeAgentSession(claudeConfig, { return new ClaudeAgentSession(claudeConfig, {
defaults: this.defaults, defaults: this.defaults,
@@ -225,12 +340,12 @@ export class ClaudeAgentClient implements AgentClient {
if (config.provider !== "claude") { if (config.provider !== "claude") {
throw new Error(`ClaudeAgentClient received config for provider '${config.provider}'`); throw new Error(`ClaudeAgentClient received config for provider '${config.provider}'`);
} }
return config as ClaudeAgentConfig; return config;
} }
} }
class ClaudeAgentSession implements AgentSession { class ClaudeAgentSession implements AgentSession {
readonly provider = "claude" as const; readonly provider: "claude" = "claude";
readonly capabilities = CLAUDE_CAPABILITIES; readonly capabilities = CLAUDE_CAPABILITIES;
private readonly config: ClaudeAgentConfig; private readonly config: ClaudeAgentConfig;
@@ -276,7 +391,7 @@ class ClaudeAgentSession implements AgentSession {
); );
} }
this.currentMode = (config.modeId as PermissionMode) ?? "default"; this.currentMode = isPermissionMode(config.modeId) ? config.modeId : "default";
if (this.claudeSessionId) { if (this.claudeSessionId) {
this.loadPersistedHistory(this.claudeSessionId); this.loadPersistedHistory(this.claudeSessionId);
} }
@@ -420,7 +535,7 @@ class ClaudeAgentSession implements AgentSession {
); );
} }
const normalized = modeId as PermissionMode; const normalized = isPermissionMode(modeId) ? modeId : "default";
const query = await this.ensureQuery(); const query = await this.ensureQuery();
await query.setPermissionMode(normalized); await query.setPermissionMode(normalized);
this.currentMode = normalized; this.currentMode = normalized;
@@ -602,9 +717,8 @@ class ClaudeAgentSession implements AgentSession {
} }
const result: Record<string, ClaudeMcpServerConfig> = {}; const result: Record<string, ClaudeMcpServerConfig> = {};
for (const [name, config] of Object.entries(servers)) { for (const [name, config] of Object.entries(servers)) {
if (!isRecord(config)) continue; if (isClaudeMcpServerConfig(config)) {
if ("type" in config || "command" in config || "url" in config) { result[name] = config;
result[name] = config as ClaudeMcpServerConfig;
} }
} }
return Object.keys(result).length ? result : undefined; return Object.keys(result).length ? result : undefined;
@@ -692,11 +806,11 @@ class ClaudeAgentSession implements AgentSession {
switch (message.type) { switch (message.type) {
case "system": case "system":
if (message.subtype === "init") { if (message.subtype === "init") {
this.handleSystemMessage(message as SDKSystemMessage); this.handleSystemMessage(message);
} }
break; break;
case "user": { case "user": {
const content = (message as SDKUserMessage).message?.content; const content = message.message?.content;
if (Array.isArray(content)) { if (Array.isArray(content)) {
const timelineItems = this.mapBlocksToTimeline(content); const timelineItems = this.mapBlocksToTimeline(content);
for (const item of timelineItems) { for (const item of timelineItems) {
@@ -781,10 +895,9 @@ class ClaudeAgentSession implements AgentSession {
} }
const requestId = `permission-${randomUUID()}`; const requestId = `permission-${randomUUID()}`;
const metadata: Record<string, unknown> = {}; const metadata: AgentMetadata = {};
const permissionOptions = options as { toolUseID?: string } | undefined; if (options.toolUseID) {
if (permissionOptions?.toolUseID) { metadata.toolUseId = options.toolUseID;
metadata.toolUseId = permissionOptions.toolUseID;
} }
if (toolName === "ExitPlanMode" && typeof input.plan === "string") { if (toolName === "ExitPlanMode" && typeof input.plan === "string") {
metadata.planText = input.plan; metadata.planText = input.plan;
@@ -796,7 +909,7 @@ class ClaudeAgentSession implements AgentSession {
name: toolName, name: toolName,
kind: toolName === "ExitPlanMode" ? "plan" : "tool", kind: toolName === "ExitPlanMode" ? "plan" : "tool",
input, input,
suggestions: options?.suggestions as AgentPermissionUpdate[] | undefined, suggestions: options.suggestions?.map((suggestion) => ({ ...suggestion })),
metadata: Object.keys(metadata).length ? metadata : undefined, metadata: Object.keys(metadata).length ? metadata : undefined,
}; };
@@ -871,7 +984,7 @@ class ClaudeAgentSession implements AgentSession {
}); });
}; };
private emitPlanTodoItems(input: Record<string, unknown>) { private emitPlanTodoItems(input: AgentMetadata) {
const planText = typeof input.plan === "string" ? input.plan : JSON.stringify(input); const planText = typeof input.plan === "string" ? input.plan : JSON.stringify(input);
const todoItems = planText const todoItems = planText
.split("\n") .split("\n")
@@ -931,7 +1044,8 @@ class ClaudeAgentSession implements AgentSession {
if (!updates || updates.length === 0) { if (!updates || updates.length === 0) {
return undefined; return undefined;
} }
return updates as PermissionUpdate[]; const normalized = updates.filter(isPermissionUpdate);
return normalized.length > 0 ? normalized : undefined;
} }
private rejectAllPendingPermissions(error: Error) { private rejectAllPendingPermissions(error: Error) {
@@ -1092,7 +1206,7 @@ class ClaudeAgentSession implements AgentSession {
const tool = entry?.name ?? block.tool_name ?? "tool"; const tool = entry?.name ?? block.tool_name ?? "tool";
const status = block.is_error ? "failed" : "completed"; const status = block.is_error ? "failed" : "completed";
// Extract output from block.content (SDK always returns content as string) // Extract output from block.content (SDK always returns content in string form)
const output = this.buildToolOutput(block, entry); const output = this.buildToolOutput(block, entry);
this.pushToolCall( this.pushToolCall(
@@ -1117,7 +1231,7 @@ class ClaudeAgentSession implements AgentSession {
private buildToolOutput( private buildToolOutput(
block: ClaudeContentChunk, block: ClaudeContentChunk,
entry: ToolUseCacheEntry | undefined entry: ToolUseCacheEntry | undefined
): Record<string, unknown> | undefined { ): AgentMetadata | undefined {
if (block.is_error) { if (block.is_error) {
return undefined; return undefined;
} }
@@ -1135,14 +1249,14 @@ class ClaudeAgentSession implements AgentSession {
} }
// Fallback format - try to parse JSON first // Fallback format - try to parse JSON first
const result: Record<string, unknown> = {}; const result: AgentMetadata = {};
if (content.length > 0) { if (content.length > 0) {
try { try {
// If content is a JSON string, parse it // If content is a JSON string, parse it
result.output = JSON.parse(content); result.output = JSON.parse(content);
} catch { } catch {
// If not JSON, return as-is (no extra wrapping) // If not JSON, return unchanged (no extra wrapping)
result.output = content; result.output = content;
} }
} }
@@ -1159,8 +1273,8 @@ class ClaudeAgentSession implements AgentSession {
server: string, server: string,
tool: string, tool: string,
output: string, output: string,
input?: Record<string, unknown> | null input?: AgentMetadata | null
): Record<string, unknown> | undefined { ): AgentMetadata | undefined {
const normalizedServer = server.toLowerCase(); const normalizedServer = server.toLowerCase();
const normalizedTool = tool.toLowerCase(); const normalizedTool = tool.toLowerCase();
@@ -1239,15 +1353,19 @@ class ClaudeAgentSession implements AgentSession {
} }
private mapPartialEvent(event: SDKPartialAssistantMessage["event"]): AgentTimelineItem[] { private mapPartialEvent(event: SDKPartialAssistantMessage["event"]): AgentTimelineItem[] {
if (event.type === "content_block_start" && (event.content_block as ClaudeContentChunk | undefined)?.type === "tool_use") { if (event.type === "content_block_start") {
const block = event.content_block as ClaudeContentChunk; const block = isClaudeContentChunk(event.content_block) ? event.content_block : null;
if (typeof event.index === "number" && typeof block?.id === "string") { if (block?.type === "tool_use" && typeof event.index === "number" && typeof block.id === "string") {
this.toolUseIndexToId.set(event.index, block.id); this.toolUseIndexToId.set(event.index, block.id);
this.toolUseInputBuffers.delete(block.id); this.toolUseInputBuffers.delete(block.id);
} }
} else if (event.type === "content_block_delta" && (event.delta as ClaudeContentChunk | undefined)?.type === "input_json_delta") { } else if (event.type === "content_block_delta") {
this.handleToolInputDelta(event.index, (event.delta as { partial_json?: string })?.partial_json); const delta = isClaudeContentChunk(event.delta) ? event.delta : null;
return []; if (delta?.type === "input_json_delta") {
const partialJson = typeof delta.partial_json === "string" ? delta.partial_json : undefined;
this.handleToolInputDelta(event.index, partialJson);
return [];
}
} else if (event.type === "content_block_stop" && typeof event.index === "number") { } else if (event.type === "content_block_stop" && typeof event.index === "number") {
const toolId = this.toolUseIndexToId.get(event.index); const toolId = this.toolUseIndexToId.get(event.index);
if (toolId) { if (toolId) {
@@ -1258,9 +1376,11 @@ class ClaudeAgentSession implements AgentSession {
switch (event.type) { switch (event.type) {
case "content_block_start": case "content_block_start":
return this.mapBlocksToTimeline([event.content_block as ClaudeContentChunk]); return isClaudeContentChunk(event.content_block)
? this.mapBlocksToTimeline([event.content_block])
: [];
case "content_block_delta": case "content_block_delta":
return this.mapBlocksToTimeline([event.delta as ClaudeContentChunk]); return isClaudeContentChunk(event.delta) ? this.mapBlocksToTimeline([event.delta]) : [];
default: default:
return []; return [];
} }
@@ -1340,14 +1460,14 @@ class ClaudeAgentSession implements AgentSession {
}); });
} }
private normalizeToolInput(input: unknown): Record<string, unknown> | null { private normalizeToolInput(input: unknown): AgentMetadata | null {
if (!input || typeof input !== "object") { if (!isMetadata(input)) {
return null; return null;
} }
return input as Record<string, unknown>; return input;
} }
private applyToolInput(entry: ToolUseCacheEntry, input: Record<string, unknown>): void { private applyToolInput(entry: ToolUseCacheEntry, input: AgentMetadata): void {
entry.input = input; entry.input = input;
if (this.isCommandTool(entry.name, input)) { if (this.isCommandTool(entry.name, input)) {
entry.classification = "command"; entry.classification = "command";
@@ -1361,7 +1481,7 @@ class ClaudeAgentSession implements AgentSession {
} }
} }
private isCommandTool(name: string, input: Record<string, unknown>): boolean { private isCommandTool(name: string, input: AgentMetadata): boolean {
const normalized = name.toLowerCase(); const normalized = name.toLowerCase();
if (normalized.includes("bash") || normalized.includes("shell") || normalized.includes("terminal") || normalized.includes("command")) { if (normalized.includes("bash") || normalized.includes("shell") || normalized.includes("terminal") || normalized.includes("command")) {
return true; return true;
@@ -1372,7 +1492,7 @@ class ClaudeAgentSession implements AgentSession {
return false; return false;
} }
private extractCommandText(input: Record<string, unknown>): string | undefined { private extractCommandText(input: AgentMetadata): string | undefined {
const command = input.command; const command = input.command;
if (typeof command === "string" && command.length > 0) { if (typeof command === "string" && command.length > 0) {
return command; return command;
@@ -1389,7 +1509,7 @@ class ClaudeAgentSession implements AgentSession {
return undefined; return undefined;
} }
private extractFileChanges(input: Record<string, unknown>): { path: string; kind: string }[] | undefined { private extractFileChanges(input: AgentMetadata): { path: string; kind: string }[] | undefined {
if (typeof input.file_path === "string" && input.file_path.length > 0) { if (typeof input.file_path === "string" && input.file_path.length > 0) {
const relative = this.relativizePath(input.file_path); const relative = this.relativizePath(input.file_path);
if (relative) { if (relative) {
@@ -1473,14 +1593,13 @@ function hasToolLikeBlock(block?: ClaudeContentChunk | null): boolean {
return type.includes("tool"); return type.includes("tool");
} }
function normalizeHistoryBlocks( function normalizeHistoryBlocks(content: unknown): ClaudeContentChunk[] | null {
content: string | ClaudeContentChunk[]
): ClaudeContentChunk[] | null {
if (Array.isArray(content)) { if (Array.isArray(content)) {
return content; const blocks = content.filter((entry) => isClaudeContentChunk(entry));
return blocks.length > 0 ? blocks : null;
} }
if (content && typeof content === "object") { if (isClaudeContentChunk(content)) {
return [content as ClaudeContentChunk]; return [content];
} }
return null; return null;
} }
@@ -1494,8 +1613,12 @@ export function convertClaudeHistoryEntry(
return []; return [];
} }
const content = message.content as string | ClaudeContentChunk[]; const content = message.content;
const normalizedBlocks = normalizeHistoryBlocks(content); const normalizedBlocks = normalizeHistoryBlocks(content);
const contentValue =
typeof content === "string"
? content
: normalizedBlocks;
const hasToolBlock = normalizedBlocks?.some((block) => hasToolLikeBlock(block)) ?? false; const hasToolBlock = normalizedBlocks?.some((block) => hasToolLikeBlock(block)) ?? false;
const timeline: AgentTimelineItem[] = []; const timeline: AgentTimelineItem[] = [];
@@ -1510,7 +1633,7 @@ export function convertClaudeHistoryEntry(
} }
if (hasToolBlock && normalizedBlocks) { if (hasToolBlock && normalizedBlocks) {
const mapped = mapBlocks(Array.isArray(content) ? content : normalizedBlocks); const mapped = mapBlocks(normalizedBlocks);
if (entry.type === "user") { if (entry.type === "user") {
const toolItems = mapped.filter((item) => item.type === "tool_call"); const toolItems = mapped.filter((item) => item.type === "tool_call");
return timeline.length ? [...timeline, ...toolItems] : toolItems; return timeline.length ? [...timeline, ...toolItems] : toolItems;
@@ -1518,8 +1641,8 @@ export function convertClaudeHistoryEntry(
return mapped; return mapped;
} }
if (entry.type === "assistant" && content) { if (entry.type === "assistant" && contentValue) {
return mapBlocks(content); return mapBlocks(contentValue);
} }
return timeline; return timeline;
@@ -1527,7 +1650,7 @@ export function convertClaudeHistoryEntry(
class Pushable<T> implements AsyncIterable<T> { class Pushable<T> implements AsyncIterable<T> {
private queue: T[] = []; private queue: T[] = [];
private resolvers: ((value: IteratorResult<T>) => void)[] = []; private resolvers: Array<(value: IteratorResult<T, void>) => void> = [];
private closed = false; private closed = false;
push(item: T) { push(item: T) {
@@ -1546,21 +1669,23 @@ class Pushable<T> implements AsyncIterable<T> {
this.closed = true; this.closed = true;
while (this.resolvers.length > 0) { while (this.resolvers.length > 0) {
const resolve = this.resolvers.shift()!; const resolve = this.resolvers.shift()!;
resolve({ value: undefined as any, done: true }); resolve({ value: undefined, done: true });
} }
} }
[Symbol.asyncIterator](): AsyncIterator<T> { [Symbol.asyncIterator](): AsyncIterator<T, void> {
return { return {
next: (): Promise<IteratorResult<T>> => { next: (): Promise<IteratorResult<T, void>> => {
if (this.queue.length > 0) { if (this.queue.length > 0) {
const value = this.queue.shift()!; const value = this.queue.shift();
return Promise.resolve({ value, done: false }); if (value !== undefined) {
return Promise.resolve({ value, done: false });
}
} }
if (this.closed) { if (this.closed) {
return Promise.resolve({ value: undefined as any, done: true }); return Promise.resolve({ value: undefined, done: true });
} }
return new Promise<IteratorResult<T>>((resolve) => { return new Promise<IteratorResult<T, void>>((resolve) => {
this.resolvers.push(resolve); this.resolvers.push(resolve);
}); });
}, },
+2 -1
View File
@@ -577,9 +577,10 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
- `packages/server/src/server/agent/stt-openai.ts:127` - `packages/server/src/server/agent/stt-openai.ts:127`
- **Done (2025-12-25 10:55)**: WHAT: replaced Record/cast usage with type guards and index signatures in `packages/server/src/server/agent/model-catalog.ts:75`, `packages/server/src/server/agent/activity-curator.ts:27`, `packages/server/src/server/agent/agent-projections.ts:31`, `packages/server/src/server/agent/stt-openai.ts:29`. RESULT: core agent files now parse model list responses, tool-call outputs, JSON sanitization, and STT logprobs/language without `Record<string, unknown>` or casted access. EVIDENCE: `rg -n "Record<string, unknown>|\\bas\\b" packages/server/src/server/agent/model-catalog.ts packages/server/src/server/agent/activity-curator.ts packages/server/src/server/agent/agent-projections.ts packages/server/src/server/agent/stt-openai.ts` (no matches). - **Done (2025-12-25 10:55)**: WHAT: replaced Record/cast usage with type guards and index signatures in `packages/server/src/server/agent/model-catalog.ts:75`, `packages/server/src/server/agent/activity-curator.ts:27`, `packages/server/src/server/agent/agent-projections.ts:31`, `packages/server/src/server/agent/stt-openai.ts:29`. RESULT: core agent files now parse model list responses, tool-call outputs, JSON sanitization, and STT logprobs/language without `Record<string, unknown>` or casted access. EVIDENCE: `rg -n "Record<string, unknown>|\\bas\\b" packages/server/src/server/agent/model-catalog.ts packages/server/src/server/agent/activity-curator.ts packages/server/src/server/agent/agent-projections.ts packages/server/src/server/agent/stt-openai.ts` (no matches).
- [ ] **Fix**: Remove `Record<string, unknown>`/`as` usage in Claude agent files: - [x] **Fix**: Remove `Record<string, unknown>`/`as` usage in Claude agent files:
- `packages/server/src/server/agent/providers/claude-agent.test.ts:99`, `packages/server/src/server/agent/providers/claude-agent.test.ts:109`, `packages/server/src/server/agent/providers/claude-agent.test.ts:110`, `packages/server/src/server/agent/providers/claude-agent.test.ts:456`, `packages/server/src/server/agent/providers/claude-agent.test.ts:463`, `packages/server/src/server/agent/providers/claude-agent.test.ts:1279` - `packages/server/src/server/agent/providers/claude-agent.test.ts:99`, `packages/server/src/server/agent/providers/claude-agent.test.ts:109`, `packages/server/src/server/agent/providers/claude-agent.test.ts:110`, `packages/server/src/server/agent/providers/claude-agent.test.ts:456`, `packages/server/src/server/agent/providers/claude-agent.test.ts:463`, `packages/server/src/server/agent/providers/claude-agent.test.ts:1279`
- `packages/server/src/server/agent/providers/claude-agent.ts:159`, `packages/server/src/server/agent/providers/claude-agent.ts:164`, `packages/server/src/server/agent/providers/claude-agent.ts:784`, `packages/server/src/server/agent/providers/claude-agent.ts:785`, `packages/server/src/server/agent/providers/claude-agent.ts:874`, `packages/server/src/server/agent/providers/claude-agent.ts:1120`, `packages/server/src/server/agent/providers/claude-agent.ts:1138`, `packages/server/src/server/agent/providers/claude-agent.ts:1162`, `packages/server/src/server/agent/providers/claude-agent.ts:1163`, `packages/server/src/server/agent/providers/claude-agent.ts:1249`, `packages/server/src/server/agent/providers/claude-agent.ts:1343`, `packages/server/src/server/agent/providers/claude-agent.ts:1347`, `packages/server/src/server/agent/providers/claude-agent.ts:1350`, `packages/server/src/server/agent/providers/claude-agent.ts:1364`, `packages/server/src/server/agent/providers/claude-agent.ts:1375`, `packages/server/src/server/agent/providers/claude-agent.ts:1392` - `packages/server/src/server/agent/providers/claude-agent.ts:159`, `packages/server/src/server/agent/providers/claude-agent.ts:164`, `packages/server/src/server/agent/providers/claude-agent.ts:784`, `packages/server/src/server/agent/providers/claude-agent.ts:785`, `packages/server/src/server/agent/providers/claude-agent.ts:874`, `packages/server/src/server/agent/providers/claude-agent.ts:1120`, `packages/server/src/server/agent/providers/claude-agent.ts:1138`, `packages/server/src/server/agent/providers/claude-agent.ts:1162`, `packages/server/src/server/agent/providers/claude-agent.ts:1163`, `packages/server/src/server/agent/providers/claude-agent.ts:1249`, `packages/server/src/server/agent/providers/claude-agent.ts:1343`, `packages/server/src/server/agent/providers/claude-agent.ts:1347`, `packages/server/src/server/agent/providers/claude-agent.ts:1350`, `packages/server/src/server/agent/providers/claude-agent.ts:1364`, `packages/server/src/server/agent/providers/claude-agent.ts:1375`, `packages/server/src/server/agent/providers/claude-agent.ts:1392`
- **Done (2025-12-25 11:12)**: WHAT: replaced casted command/output handling with key-value type guards in `packages/server/src/server/agent/providers/claude-agent.test.ts:99`, `packages/server/src/server/agent/providers/claude-agent.test.ts:467`, `packages/server/src/server/agent/providers/claude-agent.test.ts:1157` and normalized Claude event/input parsing without casts in `packages/server/src/server/agent/providers/claude-agent.ts:112`, `packages/server/src/server/agent/providers/claude-agent.ts:1355`, `packages/server/src/server/agent/providers/claude-agent.ts:1463`, `packages/server/src/server/agent/providers/claude-agent.ts:1651`. RESULT: Claude agent/provider tests no longer use `Record<string, unknown>` or `as` assertions while preserving tool input/output and history parsing. EVIDENCE: `rg -n "Record<string, unknown>|\\bas\\b" packages/server/src/server/agent/providers/claude-agent.ts packages/server/src/server/agent/providers/claude-agent.test.ts` (no matches).
- [x] **SKIP**: Codex SDK agent files (`codex-agent.ts`, `codex-agent.test.ts`) - DEPRECATED. - [x] **SKIP**: Codex SDK agent files (`codex-agent.ts`, `codex-agent.test.ts`) - DEPRECATED.
- `codex-agent.ts` is the old SDK provider, replaced by `codex-mcp-agent.ts` - `codex-agent.ts` is the old SDK provider, replaced by `codex-mcp-agent.ts`