diff --git a/CODEX_SDK_BINARY_INVESTIGATION.md b/CODEX_SDK_BINARY_INVESTIGATION.md new file mode 100644 index 000000000..5ff2f9fb6 --- /dev/null +++ b/CODEX_SDK_BINARY_INVESTIGATION.md @@ -0,0 +1,299 @@ +# Codex SDK Binary Investigation Report + +**Date:** 2025-12-20 +**SDK Version:** @openai/codex-sdk@0.76.0 +**Status:** ✅ System binary usage is fully supported + +--- + +## Executive Summary + +The `@openai/codex-sdk` library **DOES support using a system-installed Codex binary** instead of the embedded one. This is accomplished via the `codexPathOverride` configuration option in the `CodexOptions` interface. + +--- + +## How the SDK Currently Locates the Codex Binary + +### Default Behavior (Embedded Binary) + +When no override is provided, the SDK uses the `findCodexPath()` function to locate the embedded binary: + +**Source:** `dist/index.js:278-330` + +```typescript +function findCodexPath() { + const { platform, arch } = process; + let targetTriple = null; + + // Determines platform-specific binary path + // e.g., "aarch64-apple-darwin" for Apple Silicon Macs + + const vendorRoot = path2.join(scriptDirName, "..", "vendor"); + const archRoot = path2.join(vendorRoot, targetTriple); + const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex"; + const binaryPath = path2.join(archRoot, "codex", codexBinaryName); + return binaryPath; +} +``` + +### Binary Location Structure + +The SDK ships with pre-compiled binaries in the `vendor/` directory: + +``` +node_modules/@openai/codex-sdk/ +├── vendor/ +│ ├── aarch64-apple-darwin/codex/codex (40MB - Apple Silicon) +│ ├── x86_64-apple-darwin/codex/codex (Intel Mac) +│ ├── aarch64-unknown-linux-musl/codex/codex (ARM Linux) +│ ├── x86_64-unknown-linux-musl/codex/codex (x86_64 Linux) +│ ├── aarch64-pc-windows-msvc/codex/codex.exe (ARM Windows) +│ └── x86_64-pc-windows-msvc/codex/codex.exe (x86_64 Windows) +``` + +--- + +## Configuration Options Available + +### `codexPathOverride` Option + +**Type Definition:** `dist/index.d.ts:209-218` + +```typescript +type CodexOptions = { + codexPathOverride?: string; // ← THIS IS THE KEY OPTION + baseUrl?: string; + apiKey?: string; + env?: Record; +}; +``` + +**Implementation:** `dist/index.js:336-338` + +```typescript +constructor(options = {}) { + this.exec = new CodexExec(options.codexPathOverride, options.env); + this.options = options; +} +``` + +**How it works:** `dist/index.js:145-147` + +```typescript +constructor(executablePath = null, env) { + this.executablePath = executablePath || findCodexPath(); + this.envOverride = env; +} +``` + +The `codexPathOverride` is passed as `executablePath`. If provided, it bypasses the `findCodexPath()` function entirely. + +--- + +## How to Use a System Codex Binary + +### Method 1: Direct Path Specification + +```typescript +import { Codex } from '@openai/codex-sdk'; + +const codex = new Codex({ + codexPathOverride: '/Users/moboudra/.asdf/installs/nodejs/22.20.0/bin/codex' +}); +``` + +### Method 2: Dynamic PATH Resolution + +```typescript +import { Codex } from '@openai/codex-sdk'; +import { execSync } from 'child_process'; + +const systemCodexPath = execSync('which codex', { encoding: 'utf8' }).trim(); + +const codex = new Codex({ + codexPathOverride: systemCodexPath +}); +``` + +### Method 3: Environment Variable Based (Custom Implementation) + +```typescript +import { Codex } from '@openai/codex-sdk'; + +const codex = new Codex({ + codexPathOverride: process.env.CODEX_BINARY_PATH || undefined +}); +``` + +**Note:** The SDK does NOT have a built-in environment variable for this purpose. The `CODEX_BINARY_PATH` above is a custom convention you'd need to implement in your application. + +--- + +## Verification Test Results + +### Test Configuration + +- **System Codex:** `/Users/moboudra/.asdf/installs/nodejs/22.20.0/bin/codex` +- **Embedded Codex:** `node_modules/@openai/codex-sdk/vendor/aarch64-apple-darwin/codex/codex` +- **Versions:** Both are `codex-cli 0.76.0` + +### Test Results + +```bash +$ node test-codex-path-override.mjs +Testing codexPathOverride option... + +Test 1: Using default embedded binary +✓ Created Codex instance with default settings + +Test 2: Using system binary via codexPathOverride +✓ Created Codex instance with codexPathOverride + +Test 3: Using which codex result +System codex location: /Users/moboudra/.asdf/installs/nodejs/22.20.0/bin/codex +✓ Created Codex instance with system codex from PATH + +✅ All tests passed! +``` + +--- + +## Benefits of Using System Codex + +### 1. **Version Control** +- Update codex independently via package manager (npm, brew, asdf, etc.) +- No need to wait for SDK release to get latest codex features +- Easier to test beta/development versions + +### 2. **Disk Space Savings** +- Embedded binary: ~40MB per architecture +- In a monorepo with multiple node_modules: Saves 40MB × number of installations +- System binary: Single installation regardless of projects + +### 3. **Consistency** +- Same codex version across all tools and SDKs +- Easier debugging when CLI and SDK use identical binaries +- Simplified version management + +### 4. **Development Workflow** +- Test local codex builds without reinstalling SDK +- Point to custom-compiled binaries for debugging +- A/B test different codex versions easily + +--- + +## Potential Considerations + +### 1. **Version Compatibility** +- SDK expects specific codex CLI behavior +- Using mismatched versions might cause issues +- SDK is at v0.76.0, ensure system codex matches or is compatible + +### 2. **PATH Availability** +- System binary must be accessible when Node.js process runs +- Consider environment differences (development vs production) +- Absolute paths are safer than relying on PATH resolution + +### 3. **No Fallback** +- If codexPathOverride points to invalid/missing binary, execution will fail +- SDK doesn't automatically fall back to embedded binary +- Implement error handling if using dynamic resolution + +--- + +## Environment Variables (Built-in) + +The SDK does NOT provide a built-in environment variable for binary path override, but it does use these: + +### Used by SDK + +1. **`CODEX_INTERNAL_ORIGINATOR_OVERRIDE`** (Internal) + - Source: `dist/index.js:140-141` + - Set to `"codex_sdk_ts"` to identify SDK usage + - Not for user configuration + +2. **`OPENAI_BASE_URL`** (Configurable) + - Override via `CodexOptions.baseUrl` + - Also injectable via env + +3. **`CODEX_API_KEY`** (Configurable) + - Override via `CodexOptions.apiKey` + - Also injectable via env + +### NOT Provided + +- ❌ `CODEX_BINARY_PATH` - Not a built-in feature +- ❌ `CODEX_PATH` - Not a built-in feature +- ❌ Auto-discovery via system PATH - Not implemented + +--- + +## Recommended Implementation Pattern + +For maximum flexibility, consider this pattern: + +```typescript +import { Codex } from '@openai/codex-sdk'; +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; + +function createCodexInstance(options = {}) { + // Priority order: + // 1. Explicit override in options + // 2. Environment variable + // 3. System PATH + // 4. Embedded binary (SDK default) + + let codexPath = options.codexPathOverride; + + if (!codexPath && process.env.CODEX_BINARY_PATH) { + codexPath = process.env.CODEX_BINARY_PATH; + } + + if (!codexPath) { + try { + codexPath = execSync('which codex', { encoding: 'utf8' }).trim(); + if (!existsSync(codexPath)) { + codexPath = undefined; + } + } catch { + // Fall back to embedded binary + codexPath = undefined; + } + } + + return new Codex({ + ...options, + codexPathOverride: codexPath + }); +} + +// Usage +const codex = createCodexInstance(); +``` + +--- + +## Summary + +| Question | Answer | +|----------|--------| +| **Can SDK use system codex?** | ✅ Yes, via `codexPathOverride` | +| **Is configuration option available?** | ✅ Yes, in `CodexOptions` interface | +| **Built-in env variable support?** | ❌ No, must implement custom logic | +| **Automatic PATH resolution?** | ❌ No, must provide explicit path | +| **Fallback to embedded?** | ❌ No, fails if override is invalid | +| **Version tested** | 0.76.0 (both system and embedded) | + +--- + +## Conclusion + +The `@openai/codex-sdk` library provides full support for using a system-installed Codex binary through the `codexPathOverride` configuration option. This feature is well-designed and allows developers to choose between the convenience of embedded binaries and the flexibility of system-managed installations. + +To use a system binary: +1. Install codex globally (via npm, brew, asdf, etc.) +2. Pass the path via `codexPathOverride` when instantiating `Codex` +3. Optionally implement PATH resolution logic for automatic discovery + +The embedded binary remains the default, ensuring the SDK works out-of-the-box without requiring system-wide codex installation. diff --git a/MODEL_FETCHING_REPORT.md b/MODEL_FETCHING_REPORT.md new file mode 100644 index 000000000..ffbd9b2a7 --- /dev/null +++ b/MODEL_FETCHING_REPORT.md @@ -0,0 +1,87 @@ +# Model Fetching Investigation Report + +## Summary + +Investigated the model fetching system to determine what models are actually available for both Claude and Codex agents. + +## Code Location + +The model fetching logic is located in: +- **File**: `packages/server/src/server/agent/model-catalog.ts` (lines 22-101) +- **Main Function**: `fetchProviderModelCatalog(provider, options)` +- **Claude Function**: `fetchClaudeModelCatalog(options)` (lines 35-66) +- **Codex Function**: `fetchCodexModelCatalog()` (lines 68-101) + +## How It Works + +### Claude Model Fetching +- Uses the Claude Agent SDK's `query.supportedModels()` method +- Creates a query instance with the given `cwd` (working directory) +- Returns models from the SDK with their display names and descriptions + +### Codex Model Fetching +- Spawns the Codex binary (`codex app-server`) as a subprocess +- Communicates via JSON-RPC protocol +- Sends `initialize` followed by `model/list` requests +- Returns models with metadata including reasoning efforts + +## Actual Models Returned + +### Claude Models (3 total) + +1. **Default (recommended)** (`default`) + - Description: "Sonnet 4.5 · Smartest model for daily use" + - Is Default: false + +2. **Opus** (`opus`) + - Description: "Legacy: Opus 4.1 · Reaches usage limits faster" + - Is Default: false + +3. **Haiku** (`haiku`) + - Description: "Haiku 4.5 · Fastest model for simple tasks" + - Is Default: false + +### Codex Models (3 total) + +1. **gpt-5.1-codex** (`gpt-5.1-codex`) ⭐ DEFAULT + - Description: "Optimized for codex." + - Is Default: true + - Model: gpt-5.1-codex + - Default Reasoning Effort: medium + +2. **gpt-5.1-codex-mini** (`gpt-5.1-codex-mini`) + - Description: "Optimized for codex. Cheaper, faster, but less capable." + - Is Default: false + - Model: gpt-5.1-codex-mini + - Default Reasoning Effort: medium + +3. **gpt-5.1** (`gpt-5.1`) + - Description: "Broad world knowledge with strong general reasoning." + - Is Default: false + - Model: gpt-5.1 + - Default Reasoning Effort: medium + +## Key Observations + +1. **Claude models**: The model IDs returned are simplified (`default`, `opus`, `haiku`) rather than full model identifiers like `claude-sonnet-4-5-20250929`. + +2. **Codex models**: All three models are available and properly marked with `isDefault` flag on `gpt-5.1-codex`. + +3. **No Sonnet variant**: Interestingly, Claude returns a "default" model (which is Sonnet 4.5 based on the description) but doesn't expose a model ID called "sonnet" - only "default", "opus", and "haiku". + +4. **Model resolution**: The `model-resolver.ts` file uses these catalogs to find the default model when none is specified, preferring models marked with `isDefault: true`. + +## Testing Method + +Created and ran a test script (`test-models.ts`) that directly imports and calls: +- `fetchClaudeModelCatalog()` +- `fetchCodexModelCatalog()` + +Executed with: `npx tsx test-models.ts` + +## Related Files + +- `src/server/agent/model-catalog.ts` - Model fetching implementation +- `src/server/agent/model-resolver.ts` - Default model resolution +- `src/server/agent/model-catalog.e2e.test.ts` - End-to-end tests +- `src/server/session.ts:1589` - Usage in session handling (list_provider_models_response) diff --git a/packages/app/src/app/agent/[serverId]/[agentId].tsx b/packages/app/src/app/agent/[serverId]/[agentId].tsx index b573ee57c..12655ddb2 100644 --- a/packages/app/src/app/agent/[serverId]/[agentId].tsx +++ b/packages/app/src/app/agent/[serverId]/[agentId].tsx @@ -33,6 +33,7 @@ import { HOME_NAVIGATION_KEY, startNavigationTiming, } from "@/utils/navigation-timing"; +import { extractAgentModel } from "@/utils/extract-agent-model"; const DROPDOWN_WIDTH = 220; const EMPTY_STREAM_ITEMS: StreamItem[] = []; @@ -44,47 +45,6 @@ type GitRepoInfoResponseMessage = Extract< type BranchStatus = "idle" | "loading" | "ready" | "error"; -function extractAgentModel(agent?: Agent | null): string | null { - if (!agent) { - return null; - } - - const directModel = typeof agent.model === "string" ? agent.model.trim() : ""; - if (directModel.length > 0) { - return directModel; - } - - const metadata = agent.persistence?.metadata; - if (!metadata || typeof metadata !== "object") { - return null; - } - - const persistedModel = (metadata as Record).model; - if (typeof persistedModel === "string" && persistedModel.trim().length > 0) { - return persistedModel.trim(); - } - - const extra = (metadata as Record).extra; - if (!extra || typeof extra !== "object") { - return null; - } - - const getModelFrom = (source: unknown) => { - if (!source || typeof source !== "object") { - return null; - } - const candidate = (source as Record).model; - return typeof candidate === "string" && candidate.trim().length > 0 - ? candidate.trim() - : null; - }; - - return ( - getModelFrom((extra as Record).codex) ?? - getModelFrom((extra as Record).claude) - ); -} - export default function AgentScreen() { const router = useRouter(); const { serverId, agentId } = useLocalSearchParams<{ serverId: string; agentId: string }>(); diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 4f2bddc40..608302f78 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -130,6 +130,7 @@ function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload, serverId: string availableModes: snapshot.availableModes ?? [], pendingPermissions: snapshot.pendingPermissions ?? [], persistence: snapshot.persistence ?? null, + runtimeInfo: snapshot.runtimeInfo, lastUsage: snapshot.lastUsage, lastError: snapshot.lastError ?? null, title: snapshot.title ?? null, diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index a63c8fa91..181baffe6 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -74,6 +74,14 @@ export type ProviderModelState = { isLoading: boolean; }; +export interface AgentRuntimeInfo { + provider: AgentProvider; + sessionId: string | null; + model?: string | null; + modeId?: string | null; + extra?: Record; +} + export interface Agent { serverId: string; id: string; @@ -89,6 +97,7 @@ export interface Agent { availableModes: AgentMode[]; pendingPermissions: any[]; persistence: AgentPersistenceHandle | null; + runtimeInfo?: AgentRuntimeInfo; lastUsage?: AgentUsage; lastError?: string | null; title: string | null; diff --git a/packages/app/src/utils/extract-agent-model.test.ts b/packages/app/src/utils/extract-agent-model.test.ts new file mode 100644 index 000000000..3a2f94843 --- /dev/null +++ b/packages/app/src/utils/extract-agent-model.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import type { Agent } from "@/stores/session-store"; +import { extractAgentModel } from "./extract-agent-model"; + +describe("extractAgentModel", () => { + it("returns runtime model when present", () => { + const agent = { + runtimeInfo: { model: "gpt-5.2-codex" }, + } as Partial as Agent; + + expect(extractAgentModel(agent)).toBe("gpt-5.2-codex"); + }); + + it("returns null when runtime model missing even if config model is set", () => { + const agent = { + model: "gpt-5.1-codex", + runtimeInfo: {}, + } as Partial as Agent; + + expect(extractAgentModel(agent)).toBeNull(); + }); +}); diff --git a/packages/app/src/utils/extract-agent-model.ts b/packages/app/src/utils/extract-agent-model.ts new file mode 100644 index 000000000..d277ebe90 --- /dev/null +++ b/packages/app/src/utils/extract-agent-model.ts @@ -0,0 +1,9 @@ +import type { Agent } from "@/stores/session-store"; + +export function extractAgentModel(agent?: Agent | null): string | null { + if (!agent) return null; + const runtimeModel = agent.runtimeInfo?.model; + return typeof runtimeModel === "string" && runtimeModel.trim().length > 0 + ? runtimeModel.trim() + : null; +} diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 8c8427f2d..5aa67f66f 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -43,6 +43,7 @@ class TestAgentSession implements AgentSession { readonly provider = "codex" as const; readonly capabilities = TEST_CAPABILITIES; readonly id = randomUUID(); + private runtimeModel: string | null = null; constructor(private readonly config: AgentSessionConfig) {} @@ -54,10 +55,23 @@ class TestAgentSession implements AgentSession { }; } - async *stream(): AsyncGenerator {} + async *stream(): AsyncGenerator { + yield { type: "turn_started", provider: this.provider }; + yield { type: "turn_completed", provider: this.provider }; + this.runtimeModel = "gpt-5.2-codex"; + } async *streamHistory(): AsyncGenerator {} + async getRuntimeInfo() { + return { + provider: this.provider, + sessionId: this.id, + model: this.runtimeModel ?? this.config.model ?? null, + modeId: this.config.modeId ?? null, + }; + } + async getAvailableModes() { return []; } @@ -84,6 +98,26 @@ class TestAgentSession implements AgentSession { } describe("AgentManager", () => { + test("normalizeConfig does not inject default model when omitted", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const registryPath = join(workdir, "agents.json"); + const registry = new AgentRegistry(registryPath); + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry, + idFactory: () => "agent-without-model", + }); + + const snapshot = await manager.createAgent({ + provider: "codex", + cwd: workdir, + }); + + expect(snapshot.model).toBeUndefined(); + }); + test("createAgent persists provided title before returning", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const registryPath = join(workdir, "agents.json"); @@ -109,4 +143,53 @@ describe("AgentManager", () => { expect(persisted?.title).toBe("Fix Login Bug"); expect(persisted?.id).toBe("agent-with-title"); }); + + test("createAgent populates runtimeInfo after session creation", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const registryPath = join(workdir, "agents.json"); + const registry = new AgentRegistry(registryPath); + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry, + idFactory: () => "agent-with-runtime-info", + }); + + const snapshot = await manager.createAgent({ + provider: "codex", + cwd: workdir, + model: "gpt-5.2-codex", + modeId: "full-access", + }); + + expect(snapshot.runtimeInfo).toBeDefined(); + expect(snapshot.runtimeInfo?.model).toBe("gpt-5.2-codex"); + expect(snapshot.runtimeInfo?.sessionId).toBe(snapshot.sessionId); + }); + + test("runAgent refreshes runtimeInfo after completion", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const registryPath = join(workdir, "agents.json"); + const registry = new AgentRegistry(registryPath); + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry, + idFactory: () => "agent-with-run-runtime", + }); + + const snapshot = await manager.createAgent({ + provider: "codex", + cwd: workdir, + }); + + expect(snapshot.runtimeInfo?.model ?? null).toBeNull(); + + await manager.runAgent(snapshot.id, "hello"); + + const refreshed = manager.getAgent(snapshot.id); + expect(refreshed?.runtimeInfo?.model).toBe("gpt-5.2-codex"); + }); }); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index db5c29522..f7fce1496 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -17,10 +17,10 @@ import type { AgentStreamEvent, AgentTimelineItem, AgentUsage, + AgentRuntimeInfo, ListPersistedAgentsOptions, PersistedAgentDescriptor, } from "./agent-sdk-types.js"; -import { resolveAgentModel } from "./model-resolver.js"; import type { AgentRegistry } from "./agent-registry.js"; export const AGENT_LIFECYCLE_STATUSES = [ @@ -80,6 +80,7 @@ type ManagedAgentBase = { cwd: string; capabilities: AgentCapabilityFlags; config: AgentSessionConfig; + runtimeInfo?: AgentRuntimeInfo; createdAt: Date; updatedAt: Date; availableModes: AgentMode[]; @@ -479,21 +480,21 @@ export class AgentManager { const self = this; return (async function* streamForwarder() { + let finalizeError: string | undefined; try { for await (const event of iterator) { self.handleStreamEvent(agent, event); yield event; } - finalize(); } catch (error) { - const message = + finalizeError = error instanceof Error ? error.message : "Agent stream failed"; - finalize(message); throw error; } finally { + await self.refreshRuntimeInfo(agent); // Ensure we always clear the pending run and emit state when the stream is // cancelled early (e.g., via .return()) so the UI can exit the cancelling state. - finalize(); + finalize(finalizeError); } })(); } @@ -712,6 +713,7 @@ export class AgentManager { sessionId: session.id, capabilities: session.capabilities, config, + runtimeInfo: undefined, lifecycle: "initializing", createdAt: new Date(), updatedAt: new Date(), @@ -729,6 +731,7 @@ export class AgentManager { this.agents.set(agentId, managed); // Initialize previousStatus to track transitions this.previousStatuses.set(agentId, managed.lifecycle); + await this.refreshRuntimeInfo(managed); await this.persistSnapshot(managed, { title: config.title ?? null, }); @@ -773,6 +776,16 @@ export class AgentManager { } catch { agent.pendingPermissions.clear(); } + + await this.refreshRuntimeInfo(agent); + } + + private async refreshRuntimeInfo(agent: ActiveManagedAgent): Promise { + try { + agent.runtimeInfo = await agent.session.getRuntimeInfo(); + } catch { + // Keep existing runtimeInfo if refresh fails. + } } private async primeHistory(agent: ActiveManagedAgent): Promise { @@ -813,6 +826,7 @@ export class AgentManager { case "turn_completed": agent.lastUsage = event.usage; agent.lastError = undefined; + void this.refreshRuntimeInfo(agent); break; case "turn_failed": agent.lastError = event.error; @@ -949,15 +963,7 @@ export class AgentManager { normalized.cwd = resolve(normalized.cwd); } - const resolvedModel = await resolveAgentModel({ - provider: normalized.provider, - requestedModel: normalized.model, - cwd: normalized.cwd, - }); - - if (resolvedModel) { - normalized.model = resolvedModel; - } else if (typeof normalized.model === "string") { + if (typeof normalized.model === "string") { const trimmed = normalized.model.trim(); normalized.model = trimmed.length > 0 ? trimmed : undefined; } diff --git a/packages/server/src/server/agent/agent-projections.test.ts b/packages/server/src/server/agent/agent-projections.test.ts index d7cd05fa8..60ab5f642 100644 --- a/packages/server/src/server/agent/agent-projections.test.ts +++ b/packages/server/src/server/agent/agent-projections.test.ts @@ -83,11 +83,18 @@ function createManagedAgent( pendingPermissionsOverride ?? new Map(), pendingRun: pendingRunValue as ManagedAgent["pendingRun"], timeline: [], + runtimeInfo: { + provider: "claude", + sessionId: "session-123", + model: "claude-3.5-sonnet", + modeId: "plan", + }, persistence: { ...basePersistence }, lastUsage: undefined, lastError: lastErrorValue, historyPrimed: true, lastUserMessageAt: now, + attention: { requiresAttention: false }, }; return { @@ -149,6 +156,12 @@ describe("toStoredAgentRecord", () => { note: "warm", }, }); + expect(record.runtimeInfo).toEqual({ + provider: "claude", + sessionId: "session-123", + model: "claude-3.5-sonnet", + modeId: "plan", + }); expect(record.config).toEqual({ modeId: agent.config.modeId, model: agent.config.model, @@ -232,6 +245,8 @@ describe("toAgentPayload", () => { requestedAt: "2025-02-02T00:00:00.000Z", extra: { flag: true }, }); + expect(payload.runtimeInfo).toEqual(agent.runtimeInfo); + expect(payload.runtimeInfo).not.toBe(agent.runtimeInfo); expect(payload.availableModes).not.toBe(agent.availableModes); expect(payload.availableModes).toEqual(agent.availableModes); expect(payload.capabilities).not.toBe(agent.capabilities); diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index 92a91e89a..b6f20195d 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -27,6 +27,7 @@ export function toStoredAgentRecord( const createdAt = options?.createdAt ?? agent.createdAt.toISOString(); const config = buildSerializableConfig(agent.config); const persistence = sanitizePersistenceHandle(agent.persistence); + const runtimeInfo = sanitizeOptionalJsonValue(agent.runtimeInfo); return { id: agent.id, @@ -42,6 +43,7 @@ export function toStoredAgentRecord( lastStatus: agent.lifecycle, lastModeId: agent.currentModeId ?? config?.modeId ?? null, config: config ?? null, + runtimeInfo, persistence, requiresAttention: agent.attention.requiresAttention, attentionReason: agent.attention.requiresAttention @@ -57,11 +59,14 @@ export function toAgentPayload( agent: ManagedAgent, options?: ProjectionOptions ): AgentSnapshotPayload { + const runtimeInfo = sanitizeOptionalJsonValue(agent.runtimeInfo); + const payload: AgentSnapshotPayload = { id: agent.id, provider: agent.provider, cwd: agent.cwd, model: agent.config.model ?? null, + runtimeInfo, createdAt: agent.createdAt.toISOString(), updatedAt: agent.updatedAt.toISOString(), lastUserMessageAt: agent.lastUserMessageAt diff --git a/packages/server/src/server/agent/agent-registry.test.ts b/packages/server/src/server/agent/agent-registry.test.ts index 7f1755693..56805e2ea 100644 --- a/packages/server/src/server/agent/agent-registry.test.ts +++ b/packages/server/src/server/agent/agent-registry.test.ts @@ -19,6 +19,8 @@ type ManagedAgentOverrides = Omit< pendingPermissions?: Map; session?: AgentSession | null; pendingRun?: ManagedAgent["pendingRun"]; + runtimeInfo?: ManagedAgent["runtimeInfo"]; + attention?: ManagedAgent["attention"]; }; function createManagedAgent( @@ -71,6 +73,14 @@ function createManagedAgent( new Map(), pendingRun, timeline: overrides.timeline ?? [], + attention: overrides.attention ?? { requiresAttention: false }, + runtimeInfo: + overrides.runtimeInfo ?? { + provider, + sessionId: overrides.sessionId ?? "session-123", + model: config.model ?? null, + modeId: config.modeId ?? null, + }, persistence: overrides.persistence ?? null, historyPrimed: overrides.historyPrimed ?? true, lastUserMessageAt: overrides.lastUserMessageAt ?? now, diff --git a/packages/server/src/server/agent/agent-registry.ts b/packages/server/src/server/agent/agent-registry.ts index c1a898a77..bc3963a6d 100644 --- a/packages/server/src/server/agent/agent-registry.ts +++ b/packages/server/src/server/agent/agent-registry.ts @@ -40,6 +40,15 @@ const STORED_AGENT_SCHEMA = z.object({ lastStatus: AgentStatusSchema.default("closed"), lastModeId: z.string().nullable().optional(), config: SERIALIZABLE_CONFIG_SCHEMA, + runtimeInfo: z + .object({ + provider: z.string(), + sessionId: z.string().nullable(), + model: z.string().nullable().optional(), + modeId: z.string().nullable().optional(), + extra: z.record(z.unknown()).optional(), + }) + .optional(), persistence: PERSISTENCE_HANDLE_SCHEMA, requiresAttention: z.boolean().optional(), attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(), diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 17b906fe3..fa38e5f70 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -123,6 +123,14 @@ export type AgentRunResult = { timeline: AgentTimelineItem[]; }; +export type AgentRuntimeInfo = { + provider: AgentProvider; + sessionId: string | null; + model?: string | null; + modeId?: string | null; + extra?: Record; +}; + export type ListPersistedAgentsOptions = { limit?: number; }; @@ -162,6 +170,7 @@ export interface AgentSession { run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise; stream(prompt: AgentPromptInput, options?: AgentRunOptions): AsyncGenerator; streamHistory(): AsyncGenerator; + getRuntimeInfo(): Promise; getAvailableModes(): Promise; getCurrentMode(): Promise; setMode(modeId: string): Promise; diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 3709dab4e..6a6e9c59e 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -35,6 +35,7 @@ import type { AgentStreamEvent, AgentTimelineItem, AgentUsage, + AgentRuntimeInfo, ListPersistedAgentsOptions, PersistedAgentDescriptor, } from "../agent-sdk-types.js"; @@ -386,6 +387,8 @@ class ClaudeAgentSession implements AgentSession { private streamedAssistantTextThisTurn = false; private streamedReasoningThisTurn = false; private cancelCurrentTurn: (() => void) | null = null; + private cachedRuntimeInfo: AgentRuntimeInfo | null = null; + private lastOptionsModel: string | null = null; constructor( config: ClaudeAgentConfig, @@ -417,6 +420,20 @@ class ClaudeAgentSession implements AgentSession { return this.claudeSessionId; } + async getRuntimeInfo(): Promise { + if (this.cachedRuntimeInfo) { + return { ...this.cachedRuntimeInfo }; + } + const info: AgentRuntimeInfo = { + provider: "claude", + sessionId: this.claudeSessionId ?? this.pendingLocalId ?? null, + model: this.lastOptionsModel, + modeId: this.currentMode ?? null, + }; + this.cachedRuntimeInfo = info; + return { ...info }; + } + async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise { const events = this.stream(prompt, options); const timeline: AgentTimelineItem[] = []; @@ -436,6 +453,13 @@ class ClaudeAgentSession implements AgentSession { } } + this.cachedRuntimeInfo = { + provider: "claude", + sessionId: this.claudeSessionId ?? this.pendingLocalId ?? null, + model: this.lastOptionsModel, + modeId: this.currentMode ?? null, + }; + return { sessionId: this.claudeSessionId ?? this.pendingLocalId, finalText, @@ -608,11 +632,12 @@ class ClaudeAgentSession implements AgentSession { if (!this.claudeSessionId) { return null; } + const { model: _ignoredModel, ...restConfig } = this.config; this.persistence = { provider: "claude", sessionId: this.claudeSessionId, nativeHandle: this.claudeSessionId, - metadata: this.config, + metadata: restConfig, }; return this.persistence; } @@ -684,6 +709,7 @@ class ClaudeAgentSession implements AgentSession { if (this.config.model) { base.model = this.config.model; } + this.lastOptionsModel = base.model ?? null; if (this.claudeSessionId) { base.resume = this.claudeSessionId; base.continue = true; diff --git a/packages/server/src/server/agent/providers/codex-agent.ts b/packages/server/src/server/agent/providers/codex-agent.ts index a06aeb198..28f8393fe 100644 --- a/packages/server/src/server/agent/providers/codex-agent.ts +++ b/packages/server/src/server/agent/providers/codex-agent.ts @@ -35,6 +35,7 @@ import type { AgentStreamEvent, AgentTimelineItem, AgentUsage, + AgentRuntimeInfo, ListPersistedAgentsOptions, PersistedAgentDescriptor, } from "../agent-sdk-types.js"; @@ -66,19 +67,21 @@ const CODEX_MODES: AgentMode[] = [ { id: "full-access", label: "Full Access", - description: "Codex can edit files, run commands, and access the network without additional prompts.", + description: + "Codex can edit files, run commands, and access the network without additional prompts.", }, ]; -const VALID_CODEX_MODES = new Set( - CODEX_MODES.map((mode) => mode.id) -); +const VALID_CODEX_MODES = new Set(CODEX_MODES.map((mode) => mode.id)); const DEFAULT_CODEX_MODE_ID = "auto"; const MODE_PRESETS: Record< string, - Partial & { networkAccessEnabled?: boolean; webSearchEnabled?: boolean } + Partial & { + networkAccessEnabled?: boolean; + webSearchEnabled?: boolean; + } > = { "read-only": { sandboxMode: "read-only", @@ -100,7 +103,9 @@ const MODE_PRESETS: Record< }, }; -type CodexOptionsOverrides = Partial & { skipGitRepoCheck?: boolean }; +type CodexOptionsOverrides = Partial & { + skipGitRepoCheck?: boolean; +}; const MAX_ROLLOUT_SEARCH_DEPTH = 5; const PERSISTED_TIMELINE_LIMIT = 20; @@ -142,7 +147,10 @@ function normalizeCallId(value?: string | null): string | undefined { function extractThreadItemCallId(item: ThreadItem): string | undefined { const withCallId = item as ThreadItemWithOptionalCallId; - return normalizeCallId(withCallId.call_id) ?? normalizeCallId((item as { id?: string }).id); + return ( + normalizeCallId(withCallId.call_id) ?? + normalizeCallId((item as { id?: string }).id) + ); } function buildCommandDisplayName(command?: unknown): string { @@ -155,7 +163,9 @@ function buildCommandDisplayName(command?: unknown): string { return "Command"; } -function buildFileChangeSummary(files: { path: string; kind: string }[]): string { +function buildFileChangeSummary( + files: { path: string; kind: string }[] +): string { if (!files.length) { return "File change"; } @@ -167,7 +177,11 @@ function buildFileChangeSummary(files: { path: string; kind: string }[]): string function coerceSandboxMode(value?: string): SandboxMode | undefined { if (!value) return undefined; - if (value === "read-only" || value === "workspace-write" || value === "danger-full-access") { + if ( + value === "read-only" || + value === "workspace-write" || + value === "danger-full-access" + ) { return value; } return undefined; @@ -175,21 +189,35 @@ function coerceSandboxMode(value?: string): SandboxMode | undefined { function coerceApprovalMode(value?: string): ApprovalMode | undefined { if (!value) return undefined; - if (value === "never" || value === "on-request" || value === "on-failure" || value === "untrusted") { + if ( + value === "never" || + value === "on-request" || + value === "on-failure" || + value === "untrusted" + ) { return value; } return undefined; } -function coerceReasoningEffort(value?: string): ModelReasoningEffort | undefined { +function coerceReasoningEffort( + value?: string +): ModelReasoningEffort | undefined { if (!value) return undefined; - if (value === "minimal" || value === "low" || value === "medium" || value === "high") { + if ( + value === "minimal" || + value === "low" || + value === "medium" || + value === "high" + ) { return value; } return undefined; } -function normalizeExtraOptions(extra?: Record): CodexOptionsOverrides | undefined { +function normalizeExtraOptions( + extra?: Record +): CodexOptionsOverrides | undefined { if (!extra) return undefined; const allowedKeys = new Set([ "model", @@ -211,7 +239,10 @@ function normalizeExtraOptions(extra?: Record): CodexOptionsOve function detectSystemCodexPath(): string | undefined { try { - const codexPath = execSync("which codex", { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }).trim(); + const codexPath = execSync("which codex", { + encoding: "utf8", + stdio: ["pipe", "pipe", "ignore"], + }).trim(); if (codexPath && codexPath.length > 0) { return codexPath; } @@ -236,7 +267,9 @@ export class CodexAgentClient implements AgentClient { codexOptions.codexPathOverride = systemCodexPath; console.log(`[Codex] Using system binary: ${systemCodexPath}`); } else { - console.log("[Codex] Using embedded binary (no system codex found in PATH)"); + console.log( + "[Codex] Using embedded binary (no system codex found in PATH)" + ); } } @@ -248,18 +281,25 @@ export class CodexAgentClient implements AgentClient { return CodexAgentSession.create(this.codex, codexConfig); } - async resumeSession(handle: AgentPersistenceHandle, overrides?: Partial): Promise { + async resumeSession( + handle: AgentPersistenceHandle, + overrides?: Partial + ): Promise { const metadata = (handle.metadata ?? {}) as Partial; const merged = { ...metadata, ...overrides } as Partial; if (!merged.cwd) { - throw new Error("Codex resume requires the original working directory in metadata"); + throw new Error( + "Codex resume requires the original working directory in metadata" + ); } const mergedConfig = { ...merged, provider: "codex" } as AgentSessionConfig; const codexConfig = this.assertConfig(mergedConfig); return CodexAgentSession.create(this.codex, codexConfig, handle); } - async listPersistedAgents(options?: ListPersistedAgentsOptions): Promise { + async listPersistedAgents( + options?: ListPersistedAgentsOptions + ): Promise { const sessionRoot = resolveCodexSessionRoot(); if (!sessionRoot) { return []; @@ -271,7 +311,11 @@ export class CodexAgentClient implements AgentClient { const candidates = await collectRecentRolloutFiles(sessionRoot, limit * 3); const descriptors: PersistedAgentDescriptor[] = []; for (const candidate of candidates) { - const descriptor = await parseRolloutDescriptor(candidate.path, candidate.mtime, sessionRoot); + const descriptor = await parseRolloutDescriptor( + candidate.path, + candidate.mtime, + sessionRoot + ); if (descriptor) { descriptors.push(descriptor); } @@ -284,14 +328,20 @@ export class CodexAgentClient implements AgentClient { private assertConfig(config: AgentSessionConfig): CodexAgentConfig { if (config.provider !== "codex") { - throw new Error(`CodexAgentClient received config for provider '${config.provider}'`); + throw new Error( + `CodexAgentClient received config for provider '${config.provider}'` + ); } return config as CodexAgentConfig; } } class CodexAgentSession implements AgentSession { - static async create(codex: Codex, config: CodexAgentConfig, handle?: AgentPersistenceHandle): Promise { + static async create( + codex: Codex, + config: CodexAgentConfig, + handle?: AgentPersistenceHandle + ): Promise { const session = new CodexAgentSession(codex, config, handle); if (handle) { await session.loadReplayHistory(handle); @@ -316,8 +366,13 @@ class CodexAgentSession implements AgentSession { private historyEvents: AgentStreamEvent[] = []; private pendingPermissions = new Map(); private cancelCurrentTurn: (() => void) | null = null; + private cachedRuntimeInfo: AgentRuntimeInfo | null = null; - constructor(codex: Codex, config: CodexAgentConfig, handle?: AgentPersistenceHandle) { + constructor( + codex: Codex, + config: CodexAgentConfig, + handle?: AgentPersistenceHandle + ) { this.codex = codex; this.config = { ...config }; @@ -336,6 +391,7 @@ class CodexAgentSession implements AgentSession { this.threadId = handle?.sessionId ?? handle?.nativeHandle ?? null; this.pendingLocalId = this.threadId ?? `codex-${randomUUID()}`; this.persistence = handle ?? null; + this.cachedRuntimeInfo = null; if (handle && !this.threadId) { throw new Error("Codex resume requires a thread id"); } @@ -348,7 +404,31 @@ class CodexAgentSession implements AgentSession { return this.threadId; } - async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise { + async getRuntimeInfo(): Promise { + if (!this.cachedRuntimeInfo) { + await this.refreshRuntimeInfoFromRollout(); + } + if (this.cachedRuntimeInfo) { + return { ...this.cachedRuntimeInfo }; + } + const modelFromRollout = await this.readRuntimeModelFromRollout(); + const info: AgentRuntimeInfo = { + provider: "codex", + sessionId: this.threadId ?? this.pendingLocalId ?? null, + model: + modelFromRollout ?? + this.threadOptions.model ?? + null, + modeId: this.currentMode ?? null, + }; + this.cachedRuntimeInfo = info; + return { ...info }; + } + + async run( + prompt: AgentPromptInput, + options?: AgentRunOptions + ): Promise { const events = this.stream(prompt, options); const timeline: AgentTimelineItem[] = []; let finalText = ""; @@ -367,6 +447,9 @@ class CodexAgentSession implements AgentSession { } } + // Update runtime info cache after the turn completes + await this.refreshRuntimeInfoFromRollout(); + return { sessionId: this.threadId ?? this.pendingLocalId, finalText, @@ -375,7 +458,10 @@ class CodexAgentSession implements AgentSession { }; } - async *stream(prompt: AgentPromptInput, options?: AgentRunOptions): AsyncGenerator { + async *stream( + prompt: AgentPromptInput, + options?: AgentRunOptions + ): AsyncGenerator { const thread = this.thread; if (!thread) { throw new Error("Codex session is closed"); @@ -405,7 +491,10 @@ class CodexAgentSession implements AgentSession { type: "turn.completed", } as ThreadEvent); void cancellation.catch((error) => { - console.warn("[CodexAgentSession] Failed to stop Codex stream:", error); + console.warn( + "[CodexAgentSession] Failed to stop Codex stream:", + error + ); }); } catch (error) { console.warn("[CodexAgentSession] Failed to stop Codex stream:", error); @@ -433,6 +522,7 @@ class CodexAgentSession implements AgentSession { if (this.cancelCurrentTurn === requestCancel) { this.cancelCurrentTurn = null; } + await this.refreshRuntimeInfoFromRollout(); } } @@ -484,10 +574,15 @@ class CodexAgentSession implements AgentSession { return Array.from(this.pendingPermissions.values()); } - async respondToPermission(requestId: string, response: AgentPermissionResponse): Promise { + async respondToPermission( + requestId: string, + response: AgentPermissionResponse + ): Promise { const request = this.pendingPermissions.get(requestId); if (!request) { - throw new Error(`No pending Codex permission request with id '${requestId}'`); + throw new Error( + `No pending Codex permission request with id '${requestId}'` + ); } this.pendingPermissions.delete(requestId); @@ -522,12 +617,13 @@ class CodexAgentSession implements AgentSession { if (!this.threadId) { return null; } + const { model: _ignoredModel, ...restConfig } = this.config; this.persistence = { provider: "codex", sessionId: this.threadId, nativeHandle: this.threadId, metadata: { - ...this.config, + ...restConfig, codexSessionDir: this.codexSessionDir ?? undefined, codexRolloutPath: this.rolloutPath ?? undefined, }, @@ -568,10 +664,13 @@ class CodexAgentSession implements AgentSession { options.model = this.config.model; } if (this.config.sandboxMode) { - options.sandboxMode = coerceSandboxMode(this.config.sandboxMode) ?? options.sandboxMode; + options.sandboxMode = + coerceSandboxMode(this.config.sandboxMode) ?? options.sandboxMode; } if (this.config.approvalPolicy) { - options.approvalPolicy = coerceApprovalMode(this.config.approvalPolicy) ?? options.approvalPolicy; + options.approvalPolicy = + coerceApprovalMode(this.config.approvalPolicy) ?? + options.approvalPolicy; } if (typeof this.config.networkAccess === "boolean") { options.networkAccessEnabled = this.config.networkAccess; @@ -581,10 +680,13 @@ class CodexAgentSession implements AgentSession { } if (this.config.reasoningEffort) { options.modelReasoningEffort = - coerceReasoningEffort(this.config.reasoningEffort) ?? options.modelReasoningEffort; + coerceReasoningEffort(this.config.reasoningEffort) ?? + options.modelReasoningEffort; } - const extra = normalizeExtraOptions(this.config.extra?.codex as Record | undefined); + const extra = normalizeExtraOptions( + this.config.extra?.codex as Record | undefined + ); if (extra) { Object.assign(options, extra); } @@ -592,7 +694,9 @@ class CodexAgentSession implements AgentSession { return options; } - private async loadReplayHistory(handle: AgentPersistenceHandle): Promise { + private async loadReplayHistory( + handle: AgentPersistenceHandle + ): Promise { const threadIdentifier = handle.sessionId ?? handle.nativeHandle; if (!threadIdentifier || !this.codexSessionDir) { return; @@ -604,7 +708,10 @@ class CodexAgentSession implements AgentSession { } if (!rolloutFile) { - rolloutFile = await findRolloutFile(threadIdentifier, this.codexSessionDir); + rolloutFile = await findRolloutFile( + threadIdentifier, + this.codexSessionDir + ); } if (!rolloutFile) { @@ -612,6 +719,7 @@ class CodexAgentSession implements AgentSession { } this.rolloutPath = rolloutFile; + await this.refreshRuntimeInfoFromRollout(); try { const events = await parseRolloutFile(rolloutFile, threadIdentifier); @@ -619,11 +727,16 @@ class CodexAgentSession implements AgentSession { this.historyEvents = events; } } catch (error) { - console.warn(`[CodexAgentSession] Failed to parse rollout ${rolloutFile}:`, error); + console.warn( + `[CodexAgentSession] Failed to parse rollout ${rolloutFile}:`, + error + ); } } - private resolveCodexSessionDir(handle?: AgentPersistenceHandle): string | null { + private resolveCodexSessionDir( + handle?: AgentPersistenceHandle + ): string | null { const metadataDir = readMetadataString(handle, "codexSessionDir"); if (metadataDir) { return metadataDir; @@ -639,7 +752,8 @@ class CodexAgentSession implements AgentSession { } const extraHome = readCodexExtraString(this.config, "home"); - const codexHome = extraHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"); + const codexHome = + extraHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"); return path.join(codexHome, "sessions"); } @@ -652,6 +766,46 @@ class CodexAgentSession implements AgentSession { return extraRollout ?? null; } + private async refreshRuntimeInfoFromRollout(): Promise { + if (!this.rolloutPath) { + const threadIdentifier = this.threadId; + if (threadIdentifier && this.codexSessionDir) { + const rolloutFile = await findRolloutFile( + threadIdentifier, + this.codexSessionDir + ); + if (rolloutFile) { + this.rolloutPath = rolloutFile; + } + } + } + const modelFromRollout = await this.readRuntimeModelFromRollout(); + if (!modelFromRollout && this.cachedRuntimeInfo) { + return; + } + this.cachedRuntimeInfo = { + provider: "codex", + sessionId: this.threadId ?? this.pendingLocalId ?? null, + model: + modelFromRollout ?? + this.threadOptions.model ?? + null, + modeId: this.currentMode ?? null, + }; + } + + private async readRuntimeModelFromRollout(): Promise { + const rollout = this.rolloutPath; + if (!rollout) { + return null; + } + try { + return await readLatestTurnContextModel(rollout); + } catch { + return null; + } + } + private buildTurnOptions(options?: AgentRunOptions): TurnOptions { if (!options?.outputSchema) { return {}; @@ -680,7 +834,11 @@ class CodexAgentSession implements AgentSession { this.threadId = event.thread_id; this.pendingLocalId = event.thread_id; this.persistence = null; - yield { type: "thread_started", provider: "codex", sessionId: event.thread_id }; + yield { + type: "thread_started", + provider: "codex", + sessionId: event.thread_id, + }; break; case "turn.started": yield { type: "turn_started", provider: "codex" }; @@ -724,7 +882,10 @@ class CodexAgentSession implements AgentSession { } private handlePermissionEvent(event: ThreadEvent): AgentStreamEvent[] | null { - const eventType = typeof (event as { type?: string }).type === "string" ? (event as { type?: string }).type : null; + const eventType = + typeof (event as { type?: string }).type === "string" + ? (event as { type?: string }).type + : null; if (!eventType) { return null; } @@ -742,7 +903,9 @@ class CodexAgentSession implements AgentSession { return null; } - private enqueuePermissionRequest(request: AgentPermissionRequest | null): AgentStreamEvent[] | null { + private enqueuePermissionRequest( + request: AgentPermissionRequest | null + ): AgentStreamEvent[] | null { if (!request) { return null; } @@ -769,7 +932,9 @@ class CodexAgentSession implements AgentSession { return events; } - private buildExecPermissionRequest(rawInput: unknown): AgentPermissionRequest | null { + private buildExecPermissionRequest( + rawInput: unknown + ): AgentPermissionRequest | null { if (!rawInput || typeof rawInput !== "object") { return null; } @@ -778,14 +943,24 @@ class CodexAgentSession implements AgentSession { const commandParts: string[] = Array.isArray(raw.command) ? raw.command.filter((entry: unknown) => typeof entry === "string") : typeof raw.command === "string" - ? [raw.command] - : []; + ? [raw.command] + : []; const commandText = commandParts.join(" ").trim(); - const cwd = typeof raw.cwd === "string" && raw.cwd.length ? raw.cwd : undefined; - const requestId = typeof raw.call_id === "string" && raw.call_id.length ? raw.call_id : randomUUID(); - const reason = typeof raw.reason === "string" && raw.reason.length ? raw.reason : undefined; - const risk = raw.risk && typeof raw.risk === "object" ? raw.risk : undefined; - const parsedCommand = Array.isArray(raw.parsed_cmd) ? raw.parsed_cmd : undefined; + const cwd = + typeof raw.cwd === "string" && raw.cwd.length ? raw.cwd : undefined; + const requestId = + typeof raw.call_id === "string" && raw.call_id.length + ? raw.call_id + : randomUUID(); + const reason = + typeof raw.reason === "string" && raw.reason.length + ? raw.reason + : undefined; + const risk = + raw.risk && typeof raw.risk === "object" ? raw.risk : undefined; + const parsedCommand = Array.isArray(raw.parsed_cmd) + ? raw.parsed_cmd + : undefined; const metadata: Record = { callId: raw.call_id, @@ -822,21 +997,35 @@ class CodexAgentSession implements AgentSession { return request; } - private buildPatchPermissionRequest(rawInput: unknown): AgentPermissionRequest | null { + 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; + 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; const filePaths = changes ? Object.keys(changes) : []; - const grantRoot = typeof raw.grant_root === "string" && raw.grant_root.length ? raw.grant_root : undefined; - const reason = typeof raw.reason === "string" && raw.reason.length ? raw.reason : undefined; + const grantRoot = + typeof raw.grant_root === "string" && raw.grant_root.length + ? raw.grant_root + : undefined; + const reason = + typeof raw.reason === "string" && raw.reason.length + ? raw.reason + : undefined; const title = filePaths.length > 0 - ? `Apply patch to ${filePaths.length} file${filePaths.length === 1 ? "" : "s"}` + ? `Apply patch to ${filePaths.length} file${ + filePaths.length === 1 ? "" : "s" + }` : "Apply patch"; const metadata: Record = { @@ -881,17 +1070,24 @@ class CodexAgentSession implements AgentSession { const exitCode = (item as any)?.exit_code; const cwd = (item as any)?.cwd; const commandValue = item.command; - const command = typeof commandValue === "string" ? commandValue : - Array.isArray(commandValue) ? (commandValue as string[]).join(" ") : "command"; + const command = + typeof commandValue === "string" + ? commandValue + : Array.isArray(commandValue) + ? (commandValue as string[]).join(" ") + : "command"; // Build structured command result matching StructuredToolResult type - const structuredOutput = typeof aggregatedOutput === "string" ? { - type: "command" as const, - command, - output: aggregatedOutput, - exitCode: typeof exitCode === "number" ? exitCode : undefined, - cwd, - } : undefined; + const structuredOutput = + typeof aggregatedOutput === "string" + ? { + type: "command" as const, + command, + output: aggregatedOutput, + exitCode: typeof exitCode === "number" ? exitCode : undefined, + cwd, + } + : undefined; return createToolCallTimelineItem({ server: "command", @@ -906,7 +1102,10 @@ class CodexAgentSession implements AgentSession { }); } case "file_change": { - const files = item.changes.map((change) => ({ path: change.path, kind: change.kind })); + const files = item.changes.map((change) => ({ + path: change.path, + kind: change.kind, + })); return createToolCallTimelineItem({ server: "file_change", tool: "apply_patch", @@ -968,7 +1167,10 @@ async function fileExists(filePath: string): Promise { } } -async function findRolloutFile(threadId: string, root: string): Promise { +async function findRolloutFile( + threadId: string, + root: string +): Promise { const stack: { dir: string; depth: number }[] = [{ dir: root, depth: 0 }]; while (stack.length > 0) { const { dir, depth } = stack.pop()!; @@ -983,7 +1185,8 @@ async function findRolloutFile(threadId: string, root: string): Promise { +async function parseRolloutFile( + filePath: string, + threadId: string +): Promise { const content = await fs.readFile(filePath, "utf8"); - const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); - const events: AgentStreamEvent[] = [{ type: "thread_started", provider: "codex", sessionId: threadId }]; + const lines = content + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + const events: AgentStreamEvent[] = [ + { type: "thread_started", provider: "codex", sessionId: threadId }, + ]; const commandCalls = new Map(); for (const line of lines) { @@ -1015,6 +1226,40 @@ async function parseRolloutFile(filePath: string, threadId: string): Promise { + let content: string; + try { + content = await fs.readFile(filePath, "utf8"); + } catch { + return null; + } + const lines = content.split(/\r?\n/).filter(Boolean); + for (let i = lines.length - 1; i >= 0; i -= 1) { + const line = lines[i]; + try { + const parsed = JSON.parse(line); + if ( + parsed && + typeof parsed === "object" && + (parsed as { type?: string }).type === "turn_context" + ) { + const payload = (parsed as { payload?: unknown }).payload; + if (payload && typeof payload === "object") { + const model = (payload as Record).model; + if (typeof model === "string" && model.trim().length > 0) { + return model.trim(); + } + } + } + } catch { + continue; + } + } + return null; +} + function parseRolloutEntry(line: string): RolloutEntry | null { if (!line) { return null; @@ -1024,7 +1269,11 @@ function parseRolloutEntry(line: string): RolloutEntry | null { if (isRolloutEntry(parsed)) { return parsed; } - if (parsed && typeof parsed === "object" && typeof (parsed as { output?: unknown }).output === "string") { + if ( + parsed && + typeof parsed === "object" && + typeof (parsed as { output?: unknown }).output === "string" + ) { return parseRolloutEntry((parsed as { output: string }).output); } } catch { @@ -1042,7 +1291,11 @@ function isRolloutEntry(value: unknown): value is RolloutEntry { } function isSessionMetaEntry(value: unknown): value is SessionMetaEntry { - return Boolean(value && typeof value === "object" && (value as { type?: unknown }).type === "session_meta"); + return Boolean( + value && + typeof value === "object" && + (value as { type?: unknown }).type === "session_meta" + ); } function handleRolloutResponseItem( @@ -1059,12 +1312,20 @@ function handleRolloutResponseItem( const text = extractMessageText(payload.content); if (text) { if (payload.role === "assistant") { - events.push({ type: "timeline", provider: "codex", item: { type: "assistant_message", text } }); + events.push({ + type: "timeline", + provider: "codex", + item: { type: "assistant_message", text }, + }); } else if (payload.role === "user") { if (isSyntheticRolloutUserMessage(text)) { break; } - events.push({ type: "timeline", provider: "codex", item: { type: "user_message", text } }); + events.push({ + type: "timeline", + provider: "codex", + item: { type: "user_message", text }, + }); } } break; @@ -1072,7 +1333,11 @@ function handleRolloutResponseItem( case "reasoning": { const text = extractReasoningText(payload); if (text) { - events.push({ type: "timeline", provider: "codex", item: { type: "reasoning", text } }); + events.push({ + type: "timeline", + provider: "codex", + item: { type: "reasoning", text }, + }); } break; } @@ -1093,12 +1358,19 @@ function handleRolloutResponseItem( } } -function handleRolloutEventMessage(payload: RolloutEventPayload | undefined, events: AgentStreamEvent[]): void { +function handleRolloutEventMessage( + payload: RolloutEventPayload | undefined, + events: AgentStreamEvent[] +): void { if (!payload || typeof payload !== "object") { return; } if (payload.type === "agent_reasoning" && typeof payload.text === "string") { - events.push({ type: "timeline", provider: "codex", item: { type: "reasoning", text: payload.text } }); + events.push({ + type: "timeline", + provider: "codex", + item: { type: "reasoning", text: payload.text }, + }); } } @@ -1115,9 +1387,12 @@ function handleRolloutFunctionCall( if (SHELL_FUNCTION_NAMES.has(name)) { const args = safeJsonParse>(payload.arguments); const command = formatCommand(args); - const cwd = args && typeof args === "object" && typeof (args as { workdir?: unknown }).workdir === "string" - ? ((args as { workdir?: unknown }).workdir as string) - : undefined; + const cwd = + args && + typeof args === "object" && + typeof (args as { workdir?: unknown }).workdir === "string" + ? ((args as { workdir?: unknown }).workdir as string) + : undefined; if (command && typeof payload.call_id === "string") { commandCalls.set(payload.call_id, { command, cwd }); events.push({ @@ -1141,7 +1416,11 @@ function handleRolloutFunctionCall( 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 } }); + events.push({ + type: "timeline", + provider: "codex", + item: { type: "todo", items: planItems }, + }); } return; } @@ -1175,16 +1454,19 @@ function finalizeRolloutFunctionCall( } const result = safeJsonParse(payload.output); const exitCode = result?.metadata?.exit_code; - const status = exitCode === undefined || exitCode === 0 ? "completed" : "failed"; + const status = + exitCode === undefined || exitCode === 0 ? "completed" : "failed"; // Build structured command output - const output = result?.stdout ? { - type: "command" as const, - command: command.command, - output: result.stdout, - exitCode, - cwd: command.cwd, - } : result; + const output = result?.stdout + ? { + type: "command" as const, + command: command.command, + output: result.stdout, + exitCode, + cwd: command.cwd, + } + : result; events.push({ type: "timeline", @@ -1203,7 +1485,10 @@ function finalizeRolloutFunctionCall( commandCalls.delete(payload.call_id); } -function handleRolloutCustomToolCall(payload: RolloutCustomToolCallPayload, events: AgentStreamEvent[]): void { +function handleRolloutCustomToolCall( + payload: RolloutCustomToolCallPayload, + events: AgentStreamEvent[] +): void { if (payload?.name === "apply_patch" && typeof payload.input === "string") { const patchText = payload.input; const files = parsePatchFiles(patchText); @@ -1339,7 +1624,10 @@ type SessionMetaEntry = { payload?: SessionMetaPayload; }; -async function collectRecentRolloutFiles(rootDir: string, limit: number): Promise { +async function collectRecentRolloutFiles( + rootDir: string, + limit: number +): Promise { const candidates: RolloutCandidate[] = []; async function traverse(dir: string): Promise { let entries: Dirent[]; @@ -1402,8 +1690,12 @@ async function parseRolloutDescriptor( continue; } 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) { + 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; } } @@ -1467,7 +1759,8 @@ function extractMessageText(content: unknown): string { parts.push(text.trim()); continue; } - const message = typeof record.message === "string" ? record.message : undefined; + const message = + typeof record.message === "string" ? record.message : undefined; if (message && message.trim()) { parts.push(message.trim()); } @@ -1482,7 +1775,10 @@ export function isSyntheticRolloutUserMessage(text: string): boolean { } const lower = normalized.toLowerCase(); - if (lower.startsWith("# agents.md instructions for") && lower.includes("")) { + if ( + lower.startsWith("# agents.md instructions for") && + lower.includes("") + ) { return true; } @@ -1510,7 +1806,10 @@ function extractReasoningText(payload: RolloutReasoningPayload): string { return ""; } -async function loadCodexPersistedTimeline(filePath: string, threadId: string): Promise { +async function loadCodexPersistedTimeline( + filePath: string, + threadId: string +): Promise { try { const events = await parseRolloutFile(filePath, threadId); const timeline: AgentTimelineItem[] = []; @@ -1525,13 +1824,19 @@ async function loadCodexPersistedTimeline(filePath: string, threadId: string): P } return timeline; } catch (error) { - console.warn(`[CodexAgentSession] Failed to load persisted timeline for ${threadId}:`, error); + console.warn( + `[CodexAgentSession] Failed to load persisted timeline for ${threadId}:`, + error + ); return []; } } function parsePlanItems(args: unknown): { text: string; completed: boolean }[] { - const planValue = typeof args === "object" && args ? (args as { plan?: unknown }).plan : undefined; + 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) { @@ -1539,7 +1844,12 @@ function parsePlanItems(args: unknown): { text: string; completed: boolean }[] { continue; } const record = step as { step?: unknown; text?: unknown; status?: unknown }; - const text = typeof record.step === "string" ? record.step : typeof record.text === "string" ? record.text : ""; + const text = + typeof record.step === "string" + ? record.step + : typeof record.text === "string" + ? record.text + : ""; if (!text) { continue; } @@ -1600,7 +1910,10 @@ function formatCommand(args: unknown): string | null { return null; } -function readCodexExtraString(config: CodexAgentConfig, key: string): string | undefined { +function readCodexExtraString( + config: CodexAgentConfig, + key: string +): string | undefined { const extras = config.extra?.codex; if (!extras || typeof extras !== "object") { return undefined; @@ -1609,7 +1922,10 @@ function readCodexExtraString(config: CodexAgentConfig, key: string): string | u return typeof value === "string" && value.length > 0 ? value : undefined; } -function readMetadataString(handle: AgentPersistenceHandle | undefined, key: string): string | undefined { +function readMetadataString( + handle: AgentPersistenceHandle | undefined, + key: string +): string | undefined { if (!handle?.metadata || typeof handle.metadata !== "object") { return undefined; } @@ -1617,8 +1933,12 @@ function readMetadataString(handle: AgentPersistenceHandle | undefined, key: str return typeof value === "string" && value.length > 0 ? value : undefined; } -function sanitizeMetadata(metadata: Record): Record | undefined { - const entries = Object.entries(metadata).filter(([_, value]) => value !== undefined && value !== null); +function sanitizeMetadata( + metadata: Record +): Record | undefined { + const entries = Object.entries(metadata).filter( + ([_, value]) => value !== undefined && value !== null + ); if (!entries.length) { return undefined; } diff --git a/packages/server/src/server/agent/providers/codex-agent.unit.test.ts b/packages/server/src/server/agent/providers/codex-agent.unit.test.ts new file mode 100644 index 000000000..ccb8ff62f --- /dev/null +++ b/packages/server/src/server/agent/providers/codex-agent.unit.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { promises as fs } from "node:fs"; +import path from "node:path"; +import os from "node:os"; + +import { readLatestTurnContextModel } from "./codex-agent.js"; + +describe("readLatestTurnContextModel", () => { + it("returns the most recent turn_context model from rollout", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-rollout-")); + const file = path.join(dir, "rollout.jsonl"); + const lines = [ + JSON.stringify({ type: "session_meta", payload: { id: "thread-1" } }), + JSON.stringify({ + type: "turn_context", + payload: { model: "gpt-5.1-codex", cwd: "/tmp" }, + }), + JSON.stringify({ + type: "turn_context", + payload: { model: "gpt-5.2-codex", cwd: "/tmp" }, + }), + ]; + await fs.writeFile(file, lines.join("\n"), "utf8"); + + const model = await readLatestTurnContextModel(file); + expect(model).toBe("gpt-5.2-codex"); + }); +}); diff --git a/packages/server/src/server/messages.ts b/packages/server/src/server/messages.ts index 50e0b70f8..627c9febd 100644 --- a/packages/server/src/server/messages.ts +++ b/packages/server/src/server/messages.ts @@ -12,6 +12,7 @@ import type { AgentPermissionResponse, AgentPersistenceHandle, AgentProvider, + AgentRuntimeInfo, AgentStreamEvent, AgentTimelineItem, AgentUsage, @@ -214,6 +215,14 @@ const AgentPersistenceHandleSchema: z.ZodType = }) .nullable(); +const AgentRuntimeInfoSchema: z.ZodType = z.object({ + provider: AgentProviderSchema, + sessionId: z.string().nullable(), + model: z.string().nullable().optional(), + modeId: z.string().nullable().optional(), + extra: z.record(z.unknown()).optional(), +}); + export const AgentSnapshotPayloadSchema = z.object({ id: z.string(), provider: AgentProviderSchema, @@ -229,6 +238,7 @@ export const AgentSnapshotPayloadSchema = z.object({ availableModes: z.array(AgentModeSchema), pendingPermissions: z.array(AgentPermissionRequestPayloadSchema), persistence: AgentPersistenceHandleSchema.nullable(), + runtimeInfo: AgentRuntimeInfoSchema.optional(), lastUsage: AgentUsageSchema.optional(), lastError: z.string().optional(), title: z.string().nullable(), diff --git a/packages/server/test-sdk-spawn.ts b/packages/server/test-sdk-spawn.ts new file mode 100644 index 000000000..abec5e2de --- /dev/null +++ b/packages/server/test-sdk-spawn.ts @@ -0,0 +1,34 @@ +import { CodexAgentClient } from "@paseo/server/src/server/agent/providers/codex-agent.js"; + +async function test() { + console.log("Creating CodexAgentClient..."); + const client = new CodexAgentClient(); + + console.log("Creating session..."); + const session = await client.createSession({ + provider: "codex", + cwd: process.cwd(), + modeId: "full-access", + }); + + console.log("Session created:", session.id); + console.log("\nNow check ps to see which codex binary is running!\n"); + + // Run a simple prompt using stream to see all events + console.log("Running test prompt..."); + console.log("Sleeping 30s - check ps for the args now!"); + await new Promise((resolve) => setTimeout(resolve, 30000)); + + const events = session.stream('echo "test"'); + + for await (const event of events) { + if (event.type === "turn_completed") { + console.log("Turn completed"); + } + } + + await session.close(); + console.log("Session closed"); +} + +test().catch(console.error); diff --git a/test-codex-path-override.mjs b/test-codex-path-override.mjs new file mode 100644 index 000000000..c8761334a --- /dev/null +++ b/test-codex-path-override.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +import { Codex } from '@openai/codex-sdk'; + +console.log('Testing codexPathOverride option...\n'); + +// Test 1: Default (embedded binary) +console.log('Test 1: Using default embedded binary'); +const defaultCodex = new Codex(); +console.log('✓ Created Codex instance with default settings'); + +// Test 2: System binary via codexPathOverride +console.log('\nTest 2: Using system binary via codexPathOverride'); +const systemCodex = new Codex({ + codexPathOverride: '/Users/moboudra/.asdf/installs/nodejs/22.20.0/bin/codex' +}); +console.log('✓ Created Codex instance with codexPathOverride'); + +// Test 3: Using which codex +console.log('\nTest 3: Using which codex result'); +import { execSync } from 'child_process'; +const whichCodex = execSync('which codex', { encoding: 'utf8' }).trim(); +console.log('System codex location:', whichCodex); + +const whichCodexInstance = new Codex({ + codexPathOverride: whichCodex +}); +console.log('✓ Created Codex instance with system codex from PATH'); + +console.log('\n✅ All tests passed! codexPathOverride option is available and working.'); +console.log('\nConclusion: You can use the system-installed codex by passing:'); +console.log(' new Codex({ codexPathOverride: "/path/to/system/codex" })'); diff --git a/test-models.mjs b/test-models.mjs new file mode 100644 index 000000000..54e34ed03 --- /dev/null +++ b/test-models.mjs @@ -0,0 +1,36 @@ +import { + fetchClaudeModelCatalog, + fetchCodexModelCatalog, +} from "./packages/server/dist/server/agent/model-catalog.js"; + +console.log("=== Testing Claude Model Catalog ===\n"); +try { + const claudeModels = await fetchClaudeModelCatalog(); + console.log(`Found ${claudeModels.length} Claude models:\n`); + claudeModels.forEach((model, idx) => { + console.log(`${idx + 1}. ${model.label} (${model.id})`); + console.log(` Description: ${model.description || "N/A"}`); + console.log(` Is Default: ${model.isDefault || false}`); + console.log(""); + }); +} catch (error) { + console.error("Error fetching Claude models:", error.message); +} + +console.log("\n=== Testing Codex Model Catalog ===\n"); +try { + const codexModels = await fetchCodexModelCatalog(); + console.log(`Found ${codexModels.length} Codex models:\n`); + codexModels.forEach((model, idx) => { + console.log(`${idx + 1}. ${model.label} (${model.id})`); + console.log(` Description: ${model.description || "N/A"}`); + console.log(` Is Default: ${model.isDefault || false}`); + if (model.metadata) { + console.log(` Model: ${model.metadata.model || "N/A"}`); + console.log(` Default Reasoning Effort: ${model.metadata.defaultReasoningEffort || "N/A"}`); + } + console.log(""); + }); +} catch (error) { + console.error("Error fetching Codex models:", error.message); +} diff --git a/test-models.ts b/test-models.ts new file mode 100644 index 000000000..46b37e6bb --- /dev/null +++ b/test-models.ts @@ -0,0 +1,40 @@ +import { + fetchClaudeModelCatalog, + fetchCodexModelCatalog, +} from "./packages/server/src/server/agent/model-catalog.js"; + +async function main() { + console.log("=== Testing Claude Model Catalog ===\n"); + try { + const claudeModels = await fetchClaudeModelCatalog(); + console.log(`Found ${claudeModels.length} Claude models:\n`); + claudeModels.forEach((model, idx) => { + console.log(`${idx + 1}. ${model.label} (${model.id})`); + console.log(` Description: ${model.description || "N/A"}`); + console.log(` Is Default: ${model.isDefault || false}`); + console.log(""); + }); + } catch (error: any) { + console.error("Error fetching Claude models:", error.message); + } + + console.log("\n=== Testing Codex Model Catalog ===\n"); + try { + const codexModels = await fetchCodexModelCatalog(); + console.log(`Found ${codexModels.length} Codex models:\n`); + codexModels.forEach((model, idx) => { + console.log(`${idx + 1}. ${model.label} (${model.id})`); + console.log(` Description: ${model.description || "N/A"}`); + console.log(` Is Default: ${model.isDefault || false}`); + if (model.metadata) { + console.log(` Model: ${model.metadata.model || "N/A"}`); + console.log(` Default Reasoning Effort: ${model.metadata.defaultReasoningEffort || "N/A"}`); + } + console.log(""); + }); + } catch (error: any) { + console.error("Error fetching Codex models:", error.message); + } +} + +main(); diff --git a/test-sdk-spawn.ts b/test-sdk-spawn.ts new file mode 100644 index 000000000..abec5e2de --- /dev/null +++ b/test-sdk-spawn.ts @@ -0,0 +1,34 @@ +import { CodexAgentClient } from "@paseo/server/src/server/agent/providers/codex-agent.js"; + +async function test() { + console.log("Creating CodexAgentClient..."); + const client = new CodexAgentClient(); + + console.log("Creating session..."); + const session = await client.createSession({ + provider: "codex", + cwd: process.cwd(), + modeId: "full-access", + }); + + console.log("Session created:", session.id); + console.log("\nNow check ps to see which codex binary is running!\n"); + + // Run a simple prompt using stream to see all events + console.log("Running test prompt..."); + console.log("Sleeping 30s - check ps for the args now!"); + await new Promise((resolve) => setTimeout(resolve, 30000)); + + const events = session.stream('echo "test"'); + + for await (const event of events) { + if (event.type === "turn_completed") { + console.log("Turn completed"); + } + } + + await session.close(); + console.log("Session closed"); +} + +test().catch(console.error);