Skip to content

Create Log File for User #780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
86 changes: 85 additions & 1 deletion codex-cli/src/utils/agent/agent-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import {
getBaseUrl,
} from "../config.js";
import { log } from "../logger/log.js";
import {
initWorkspaceLogger
} from "../logger/workspace-logger.js";
import { parseToolCallArguments } from "../parsers.js";
import { responsesCreateViaChatCompletions } from "../responses.js";
import {
Expand All @@ -32,6 +35,7 @@ import { handleExecCommand } from "./handle-exec-command.js";
import { randomUUID } from "node:crypto";
import OpenAI, { APIConnectionTimeoutError } from "openai";


// Wait time before retrying after rate limit errors (ms).
const RATE_LIMIT_RETRY_WAIT_MS = parseInt(
process.env["OPENAI_RATE_LIMIT_RETRY_WAIT_MS"] || "500",
Expand Down Expand Up @@ -158,6 +162,8 @@ export class AgentLoop {
/** Master abort controller – fires when terminate() is invoked. */
private readonly hardAbort = new AbortController();

private workspaceLogger;

/**
* Abort the ongoing request/stream, if any. This allows callers (typically
* the UI layer) to interrupt the current agent step so the user can issue
Expand Down Expand Up @@ -236,6 +242,11 @@ export class AgentLoop {
if (this.terminated) {
return;
}

if (this.workspaceLogger) {
this.workspaceLogger.logSessionEnd();
}

this.terminated = true;

this.hardAbort.abort();
Expand Down Expand Up @@ -327,6 +338,9 @@ export class AgentLoop {
() => this.execAbortController?.abort(),
{ once: true },
);

this.workspaceLogger = initWorkspaceLogger(process.cwd());
this.workspaceLogger.logSessionStart(process.cwd());
}

private async handleFunctionCall(
Expand Down Expand Up @@ -423,6 +437,19 @@ export class AgentLoop {
this.getCommandConfirmation,
this.execAbortController?.signal,
);

const cmd = args.command || args.cmd || []; // Try both possible property names
const commandStr = Array.isArray(cmd)
? cmd.join(" ")
: String(cmd);

this.workspaceLogger?.logCommand(
commandStr,
metadata && typeof metadata['exit_code'] === 'number' ? metadata['exit_code'] : 0,
metadata && typeof metadata['duration_seconds'] === 'number' ? metadata['duration_seconds'] : 0,
outputText,
);

outputItem.output = JSON.stringify({ output: outputText, metadata });

if (additionalItemsFromExec) {
Expand All @@ -445,10 +472,32 @@ export class AgentLoop {
// the user retry the request if desired.
// ---------------------------------------------------------------------

// ---------------------------------------------------------------------
// Top‑level error wrapper so that known transient network issues like
// `ERR_STREAM_PREMATURE_CLOSE` do not crash the entire CLI process.
// Instead we surface the failure to the user as a regular system‑message
// and terminate the current run gracefully. The calling UI can then let
// the user retry the request if desired.
// ---------------------------------------------------------------------

try {
if (this.terminated) {
throw new Error("AgentLoop has been terminated");
}

// Log user input at the start of a run
const userInput = input.find(
(item) => item.type === "message" && item.role === "user",
);

if (userInput) {
const contentItem = userInput.type === "message" && userInput.content && userInput.content[0];
const content = typeof contentItem === 'object' && contentItem != null && 'text' in contentItem
? contentItem.text
: JSON.stringify(userInput);
this.workspaceLogger?.logUserInput(content);
}

// Record when we start "thinking" so we can report accurate elapsed time.
const thinkingStart = Date.now();
// Bump generation so that any late events from previous runs can be
Expand Down Expand Up @@ -923,10 +972,20 @@ export class AgentLoop {
if (event.type === "response.output_item.done") {
const item = event.item;
// 1) if it's a reasoning item, annotate it
type ReasoningItem = { type?: string; duration_ms?: number };
type ReasoningItem = {
type?: string;
text?: string;
duration_ms?: number;
summary?: Array<string>;
};

const maybeReasoning = item as ReasoningItem;
if (maybeReasoning.type === "reasoning") {
maybeReasoning.duration_ms = Date.now() - thinkingStart;
// Add debug logging for reasoning summary
log(
`Reasoning summary received: ${JSON.stringify(maybeReasoning.summary)}`,
);
}
if (item.type === "function_call") {
// Track outstanding tool call so we can abort later if needed.
Expand All @@ -942,6 +1001,23 @@ export class AgentLoop {
} else {
stageItem(item as ResponseItem);
}

if (item.type === "reasoning") {
const reasoningItem = item as ReasoningItem;
const reasoningText = reasoningItem.text || JSON.stringify(item);
log(
`Logging reasoning to workspace: ${reasoningText.substring(0, 100)}...`,
);
this.workspaceLogger?.logModelReasoning(reasoningText);
if (
reasoningItem.summary &&
Array.isArray(reasoningItem.summary)
) {
log(
`Reasoning summary: ${JSON.stringify(reasoningItem.summary)}`,
);
}
}
}

if (event.type === "response.completed") {
Expand Down Expand Up @@ -1233,6 +1309,14 @@ export class AgentLoop {
// End of main logic. The corresponding catch block for the wrapper at the
// start of this method follows next.
} catch (err) {
// Log errors
if (err instanceof Error) {
this.workspaceLogger?.logError(
`Error in agent loop: ${err.message}`,
err,
);
}

// Handle known transient network/streaming issues so they do not crash the
// CLI. We currently match Node/undici's `ERR_STREAM_PREMATURE_CLOSE`
// error which manifests when the HTTP/2 stream terminates unexpectedly
Expand Down
7 changes: 7 additions & 0 deletions codex-cli/src/utils/agent/apply-patch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Based on reference implementation from
// https://cookbook.openai.com/examples/gpt4-1_prompting_guide#reference-implementation-apply_patchpy

import { getWorkspaceLogger } from "../logger/workspace-logger";
import fs from "fs";
import path from "path";
import {
Expand Down Expand Up @@ -676,17 +677,23 @@ export function apply_commit(
writeFn: (p: string, c: string) => void,
removeFn: (p: string) => void,
): void {
const logger = getWorkspaceLogger();

for (const [p, change] of Object.entries(commit.changes)) {
if (change.type === ActionType.DELETE) {
removeFn(p);
logger?.logFileChange("Deleted", p);
} else if (change.type === ActionType.ADD) {
writeFn(p, change.new_content ?? "");
logger?.logFileChange("Created", p);
} else if (change.type === ActionType.UPDATE) {
if (change.move_path) {
writeFn(change.move_path, change.new_content ?? "");
removeFn(p);
logger?.logFileChange("Modified", `${p} β†’ ${change.move_path}`);
} else {
writeFn(p, change.new_content ?? "");
logger?.logFileChange("Modified", p);
}
}
}
Expand Down
37 changes: 35 additions & 2 deletions codex-cli/src/utils/agent/handle-exec-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ReviewDecision } from "./review.js";
import { isLoggingEnabled, log } from "../logger/log.js";
import { SandboxType } from "./sandbox/interface.js";
import { PATH_TO_SEATBELT_EXECUTABLE } from "./sandbox/macos-seatbelt.js";
import { getWorkspaceLogger } from "../logger/workspace-logger.js";
import fs from "fs/promises";

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -82,20 +83,32 @@ export async function handleExecCommand(
) => Promise<CommandConfirmation>,
abortSignal?: AbortSignal,
): Promise<HandleExecCommandResult> {
const startTime = Date.now();
const { cmd: command, workdir } = args;

const key = deriveCommandKey(command);

// 1) If the user has already said "always approve", skip
// any policy & never sandbox.
if (alwaysApprovedCommands.has(key)) {
return execCommand(
const summary = await execCommand(
args,
/* applyPatch */ undefined,
/* runInSandbox */ false,
additionalWritableRoots,
abortSignal,
).then(convertSummaryToResult);
);
const duration = (Date.now() - startTime) / 1000;
const logger = getWorkspaceLogger();
if (logger) {
await logger.logCommand(
args.cmd.join(" "),
summary.exitCode,
duration,
summary.stdout || summary.stderr,
);
}
return convertSummaryToResult(summary);
}

// 2) Otherwise fall back to the normal policy
Expand Down Expand Up @@ -181,9 +194,29 @@ export async function handleExecCommand(
additionalWritableRoots,
abortSignal,
);
const duration = (Date.now() - startTime) / 1000;
const logger = getWorkspaceLogger();
if (logger) {
await logger.logCommand(
args.cmd.join(" "),
summary.exitCode,
duration,
summary.stdout || summary.stderr,
);
}
return convertSummaryToResult(summary);
}
} else {
const duration = (Date.now() - startTime) / 1000;
const logger = getWorkspaceLogger();
if (logger) {
await logger.logCommand(
args.cmd.join(" "),
summary.exitCode,
duration,
summary.stdout || summary.stderr,
);
}
return convertSummaryToResult(summary);
}
}
Expand Down
Loading