diff --git a/packages/server/src/server/agent/providers/codex-agent.ts b/packages/server/src/server/agent/providers/codex-agent.ts index 415ec8c52..dec7e1749 100644 --- a/packages/server/src/server/agent/providers/codex-agent.ts +++ b/packages/server/src/server/agent/providers/codex-agent.ts @@ -100,6 +100,24 @@ type CodexOptionsOverrides = Partial & { skipGitRepoCheck?: boole const MAX_ROLLOUT_SEARCH_DEPTH = 5; type ToolCallTimelineItem = Extract; +type ExecPermissionRequestPayload = { + call_id?: string; + command?: string | string[]; + cwd?: string; + reason?: string; + risk?: { description?: string } & Record; + parsed_cmd?: string[]; + [key: string]: unknown; +}; + +type PatchPermissionRequestPayload = { + call_id?: string; + changes?: Record; + grant_root?: string; + reason?: string; + [key: string]: unknown; +}; + function createToolCallTimelineItem( data: Omit ): AgentTimelineItem { @@ -612,12 +630,12 @@ class CodexAgentSession implements AgentSession { } if (eventType === "exec_approval_request") { - const request = this.buildExecPermissionRequest(event as any); + const request = this.buildExecPermissionRequest(event); return this.enqueuePermissionRequest(request); } if (eventType === "apply_patch_approval_request") { - const request = this.buildPatchPermissionRequest(event as any); + const request = this.buildPatchPermissionRequest(event); return this.enqueuePermissionRequest(request); } @@ -652,10 +670,11 @@ class CodexAgentSession implements AgentSession { return events; } - private buildExecPermissionRequest(raw: any): AgentPermissionRequest | null { - if (!raw || typeof raw !== "object") { + private buildExecPermissionRequest(rawInput: unknown): AgentPermissionRequest | null { + if (!rawInput || typeof rawInput !== "object") { return null; } + const raw = rawInput as ExecPermissionRequestPayload; const commandParts: string[] = Array.isArray(raw.command) ? raw.command.filter((entry: unknown) => typeof entry === "string") @@ -705,10 +724,11 @@ class CodexAgentSession implements AgentSession { return request; } - private buildPatchPermissionRequest(raw: any): AgentPermissionRequest | null { - if (!raw || typeof raw !== "object") { + private buildPatchPermissionRequest(rawInput: unknown): AgentPermissionRequest | null { + if (!rawInput || typeof rawInput !== "object") { return null; } + const raw = rawInput as PatchPermissionRequestPayload; const requestId = typeof raw.call_id === "string" && raw.call_id.length ? raw.call_id : randomUUID(); const changes = raw.changes && typeof raw.changes === "object" ? raw.changes : undefined; @@ -883,17 +903,17 @@ async function parseRolloutFile(filePath: string, threadId: string): Promise ): void { @@ -946,7 +986,7 @@ function handleRolloutResponseItem( } } -function handleRolloutEventMessage(payload: any, events: AgentStreamEvent[]): void { +function handleRolloutEventMessage(payload: RolloutEventPayload | undefined, events: AgentStreamEvent[]): void { if (!payload || typeof payload !== "object") { return; } @@ -956,7 +996,7 @@ function handleRolloutEventMessage(payload: any, events: AgentStreamEvent[]): vo } function handleRolloutFunctionCall( - payload: any, + payload: RolloutFunctionCallPayload, events: AgentStreamEvent[], commandCalls: Map ): void { @@ -966,7 +1006,7 @@ function handleRolloutFunctionCall( } if (name === "shell") { - const args = safeJsonParse(payload.arguments); + const args = safeJsonParse>(payload.arguments); const command = formatCommand(args); if (command && typeof payload.call_id === "string") { commandCalls.set(payload.call_id, { command }); @@ -975,7 +1015,7 @@ function handleRolloutFunctionCall( } if (name === "update_plan") { - const args = safeJsonParse(payload.arguments); + const args = safeJsonParse<{ plan?: unknown }>(payload.arguments); const planItems = parsePlanItems(args); if (planItems.length) { events.push({ type: "timeline", provider: "codex", item: { type: "todo", items: planItems, raw: payload } }); @@ -1000,7 +1040,7 @@ function handleRolloutFunctionCall( } function finalizeRolloutFunctionCall( - payload: any, + payload: RolloutFunctionCallOutputPayload, events: AgentStreamEvent[], commandCalls: Map ): void { @@ -1011,7 +1051,7 @@ function finalizeRolloutFunctionCall( if (!command) { return; } - const result = safeJsonParse(payload.output); + const result = safeJsonParse(payload.output); const exitCode = result?.metadata?.exit_code; const status = exitCode === undefined || exitCode === 0 ? "completed" : "failed"; events.push({ @@ -1032,7 +1072,7 @@ function finalizeRolloutFunctionCall( commandCalls.delete(payload.call_id); } -function handleRolloutCustomToolCall(payload: any, events: AgentStreamEvent[]): void { +function handleRolloutCustomToolCall(payload: RolloutCustomToolCallPayload, events: AgentStreamEvent[]): void { if (payload?.name === "apply_patch" && typeof payload.input === "string") { const files = parsePatchFiles(payload.input); if (files.length) { @@ -1076,6 +1116,91 @@ type RolloutCandidate = { mtime: Date; }; +type RolloutContentBlock = { + text?: string; + message?: string; + [key: string]: unknown; +}; + +type RolloutMessagePayload = { + type: "message"; + role?: string; + content?: RolloutContentBlock[]; +}; + +type RolloutReasoningSummary = { + text?: string; +}; + +type RolloutReasoningPayload = { + type: "reasoning"; + summary?: RolloutReasoningSummary[]; + text?: string; +}; + +type RolloutFunctionCallPayload = { + type: "function_call"; + name?: string; + arguments?: string; + call_id?: string; + status?: string; +}; + +type RolloutFunctionCallOutputPayload = { + type: "function_call_output"; + call_id?: string; + output?: string; +}; + +type RolloutCustomToolCallPayload = { + type: "custom_tool_call"; + name?: string; + status?: string; + input?: string | Record; + output?: unknown; +}; + +type RolloutResponsePayload = + | RolloutMessagePayload + | RolloutReasoningPayload + | RolloutFunctionCallPayload + | RolloutFunctionCallOutputPayload + | RolloutCustomToolCallPayload; + +type RolloutEventPayload = { + type: string; + text?: string; + [key: string]: unknown; +}; + +type RolloutEntry = + | { type: "response_item"; payload?: RolloutResponsePayload } + | { type: "event_msg"; payload?: RolloutEventPayload }; + +type CommandExecutionResult = { + metadata?: { + exit_code?: number; + [key: string]: unknown; + }; + [key: string]: unknown; +}; + +type SessionMetaPayload = { + id?: string; + cwd?: string; + [key: string]: unknown; +}; + +type SessionMetaEntry = { + type: "session_meta"; + payload?: SessionMetaPayload; +}; + +type ResponseItemEntry = { + type: "response_item"; + payload?: RolloutResponsePayload; +}; + async function collectRecentRolloutFiles(rootDir: string, limit: number): Promise { const candidates: RolloutCandidate[] = []; async function traverse(dir: string): Promise { @@ -1132,18 +1257,23 @@ async function parseRolloutDescriptor( for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; - let entry: any; + let entry: unknown; try { entry = JSON.parse(line); } catch { continue; } - if (!sessionId && entry?.type === "session_meta" && entry.payload) { + if (!sessionId && isSessionMetaEntry(entry) && entry.payload) { sessionId = typeof entry.payload.id === "string" ? entry.payload.id : null; if (typeof entry.payload.cwd === "string" && entry.payload.cwd.length > 0) { cwd = entry.payload.cwd; } - } else if (!title && entry?.type === "response_item" && entry.payload?.role === "user") { + } else if ( + !title && + isResponseItemEntry(entry) && + isRolloutMessagePayload(entry.payload) && + entry.payload.role === "user" + ) { title = extractCodexUserPrompt(entry.payload); } if (sessionId && cwd && title) { @@ -1177,12 +1307,11 @@ async function parseRolloutDescriptor( }; } -function extractCodexUserPrompt(payload: any): string | null { - const content = payload?.content; - if (!Array.isArray(content)) { +function extractCodexUserPrompt(payload: RolloutMessagePayload): string | null { + if (!Array.isArray(payload.content)) { return null; } - for (const block of content) { + for (const block of payload.content) { if (block && typeof block === "object" && typeof block.text === "string") { return block.text.trim(); } @@ -1207,12 +1336,13 @@ function extractMessageText(content: unknown): string { if (!block || typeof block !== "object") { continue; } - const text = typeof (block as any).text === "string" ? (block as any).text : undefined; + const record = block as Record; + const text = typeof record.text === "string" ? record.text : undefined; if (text && text.trim()) { parts.push(text.trim()); continue; } - const message = typeof (block as any).message === "string" ? (block as any).message : undefined; + const message = typeof record.message === "string" ? record.message : undefined; if (message && message.trim()) { parts.push(message.trim()); } @@ -1220,10 +1350,10 @@ function extractMessageText(content: unknown): string { return parts.join("\n").trim(); } -function extractReasoningText(payload: any): string { +function extractReasoningText(payload: RolloutReasoningPayload): string { if (Array.isArray(payload?.summary)) { const text = payload.summary - .map((item: any) => (typeof item?.text === "string" ? item.text : "")) + .map((item) => (item && typeof item.text === "string" ? item.text : "")) .filter(Boolean) .join("\n") .trim(); @@ -1237,18 +1367,20 @@ function extractReasoningText(payload: any): string { return ""; } -function parsePlanItems(args: any): { text: string; completed: boolean }[] { - const plan = Array.isArray(args?.plan) ? args.plan : []; +function parsePlanItems(args: unknown): { text: string; completed: boolean }[] { + const planValue = typeof args === "object" && args ? (args as { plan?: unknown }).plan : undefined; + const plan = Array.isArray(planValue) ? planValue : []; const items: { text: string; completed: boolean }[] = []; for (const step of plan) { if (!step || typeof step !== "object") { continue; } - const text = typeof step.step === "string" ? step.step : typeof step.text === "string" ? step.text : ""; + const record = step as { step?: unknown; text?: unknown; status?: unknown }; + const text = typeof record.step === "string" ? record.step : typeof record.text === "string" ? record.text : ""; if (!text) { continue; } - const status = typeof step.status === "string" ? step.status : ""; + const status = typeof record.status === "string" ? record.status : ""; items.push({ text, completed: status === "completed" }); } return items; @@ -1280,7 +1412,7 @@ function parsePatchFiles(patch: string): { path: string; kind: string }[] { return files; } -function safeJsonParse(value: unknown): T | null { +function safeJsonParse(value: unknown): T | null { if (typeof value !== "string") { return null; } @@ -1291,15 +1423,16 @@ function safeJsonParse(value: unknown): T | null { } } -function formatCommand(args: any): string | null { - if (!args) { +function formatCommand(args: unknown): string | null { + if (!args || typeof args !== "object") { return null; } - if (Array.isArray(args.command)) { - return args.command.join(" "); + const record = args as { command?: unknown }; + if (Array.isArray(record.command)) { + return record.command.join(" "); } - if (typeof args.command === "string") { - return args.command; + if (typeof record.command === "string") { + return record.command; } return null; } diff --git a/plan.md b/plan.md index a6959f539..fcdaff08b 100644 --- a/plan.md +++ b/plan.md @@ -84,6 +84,7 @@ - Dropped the old status dot from `AgentStatusBar` (the permission/mode selector now stands alone) and introduced an in-stream "Working" chip inside `AgentStreamView` that renders three bouncing dots via Reanimated whenever the agent reports `status === "running"`, so busy turns surface directly in the chat timeline. - [x] npm run typecheck and fix all problems - Split the server build/typecheck configs so the typechecker can include the app stream helpers without breaking builds, added path aliases for `@server/*`, and tightened the shared stream types (status unions + `isAgentToolCallItem`) so the server e2e suites can import them safely. Cleaned up every failing app/server test by narrowing tool-call payloads via the helper, updated the harness + Codex/Claude specs, and re-ran `npm run typecheck` (app + server) to green. -- [ ] audit codebase for unecessary untyped code, hacks, casts, and `any`, type things properly +- [x] audit codebase for unecessary untyped code, hacks, casts, and `any`, type things properly + - Tightened the Codex rollout parser typings so permission requests, rollout/event payloads, plan arguments, and JSON helpers all operate on well-defined TypeScript structures instead of `any`. Added explicit payload/entry guards, removed unsafe casts throughout `packages/server/src/server/agent/providers/codex-agent.ts`, and re-ran `npm run typecheck --workspace=@voice-dev/server` to validate the stricter typing. - [ ] audit codebase for duplicated code, clean it up - [ ] rename the project to Paseo, including the Expo app, package names etc.