feat: add agent runtime info tracking with model detection from rollout files
This commit is contained in:
@@ -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<string, string>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**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.
|
||||||
@@ -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)
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
HOME_NAVIGATION_KEY,
|
HOME_NAVIGATION_KEY,
|
||||||
startNavigationTiming,
|
startNavigationTiming,
|
||||||
} from "@/utils/navigation-timing";
|
} from "@/utils/navigation-timing";
|
||||||
|
import { extractAgentModel } from "@/utils/extract-agent-model";
|
||||||
|
|
||||||
const DROPDOWN_WIDTH = 220;
|
const DROPDOWN_WIDTH = 220;
|
||||||
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
||||||
@@ -44,47 +45,6 @@ type GitRepoInfoResponseMessage = Extract<
|
|||||||
|
|
||||||
type BranchStatus = "idle" | "loading" | "ready" | "error";
|
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<string, unknown>).model;
|
|
||||||
if (typeof persistedModel === "string" && persistedModel.trim().length > 0) {
|
|
||||||
return persistedModel.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
const extra = (metadata as Record<string, unknown>).extra;
|
|
||||||
if (!extra || typeof extra !== "object") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getModelFrom = (source: unknown) => {
|
|
||||||
if (!source || typeof source !== "object") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const candidate = (source as Record<string, unknown>).model;
|
|
||||||
return typeof candidate === "string" && candidate.trim().length > 0
|
|
||||||
? candidate.trim()
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
getModelFrom((extra as Record<string, unknown>).codex) ??
|
|
||||||
getModelFrom((extra as Record<string, unknown>).claude)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AgentScreen() {
|
export default function AgentScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { serverId, agentId } = useLocalSearchParams<{ serverId: string; agentId: string }>();
|
const { serverId, agentId } = useLocalSearchParams<{ serverId: string; agentId: string }>();
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload, serverId: string
|
|||||||
availableModes: snapshot.availableModes ?? [],
|
availableModes: snapshot.availableModes ?? [],
|
||||||
pendingPermissions: snapshot.pendingPermissions ?? [],
|
pendingPermissions: snapshot.pendingPermissions ?? [],
|
||||||
persistence: snapshot.persistence ?? null,
|
persistence: snapshot.persistence ?? null,
|
||||||
|
runtimeInfo: snapshot.runtimeInfo,
|
||||||
lastUsage: snapshot.lastUsage,
|
lastUsage: snapshot.lastUsage,
|
||||||
lastError: snapshot.lastError ?? null,
|
lastError: snapshot.lastError ?? null,
|
||||||
title: snapshot.title ?? null,
|
title: snapshot.title ?? null,
|
||||||
|
|||||||
@@ -74,6 +74,14 @@ export type ProviderModelState = {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface AgentRuntimeInfo {
|
||||||
|
provider: AgentProvider;
|
||||||
|
sessionId: string | null;
|
||||||
|
model?: string | null;
|
||||||
|
modeId?: string | null;
|
||||||
|
extra?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Agent {
|
export interface Agent {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -89,6 +97,7 @@ export interface Agent {
|
|||||||
availableModes: AgentMode[];
|
availableModes: AgentMode[];
|
||||||
pendingPermissions: any[];
|
pendingPermissions: any[];
|
||||||
persistence: AgentPersistenceHandle | null;
|
persistence: AgentPersistenceHandle | null;
|
||||||
|
runtimeInfo?: AgentRuntimeInfo;
|
||||||
lastUsage?: AgentUsage;
|
lastUsage?: AgentUsage;
|
||||||
lastError?: string | null;
|
lastError?: string | null;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
|
|||||||
@@ -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<Agent> 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<Agent> as Agent;
|
||||||
|
|
||||||
|
expect(extractAgentModel(agent)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ class TestAgentSession implements AgentSession {
|
|||||||
readonly provider = "codex" as const;
|
readonly provider = "codex" as const;
|
||||||
readonly capabilities = TEST_CAPABILITIES;
|
readonly capabilities = TEST_CAPABILITIES;
|
||||||
readonly id = randomUUID();
|
readonly id = randomUUID();
|
||||||
|
private runtimeModel: string | null = null;
|
||||||
|
|
||||||
constructor(private readonly config: AgentSessionConfig) {}
|
constructor(private readonly config: AgentSessionConfig) {}
|
||||||
|
|
||||||
@@ -54,10 +55,23 @@ class TestAgentSession implements AgentSession {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async *stream(): AsyncGenerator<AgentStreamEvent> {}
|
async *stream(): AsyncGenerator<AgentStreamEvent> {
|
||||||
|
yield { type: "turn_started", provider: this.provider };
|
||||||
|
yield { type: "turn_completed", provider: this.provider };
|
||||||
|
this.runtimeModel = "gpt-5.2-codex";
|
||||||
|
}
|
||||||
|
|
||||||
async *streamHistory(): AsyncGenerator<AgentStreamEvent> {}
|
async *streamHistory(): AsyncGenerator<AgentStreamEvent> {}
|
||||||
|
|
||||||
|
async getRuntimeInfo() {
|
||||||
|
return {
|
||||||
|
provider: this.provider,
|
||||||
|
sessionId: this.id,
|
||||||
|
model: this.runtimeModel ?? this.config.model ?? null,
|
||||||
|
modeId: this.config.modeId ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async getAvailableModes() {
|
async getAvailableModes() {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -84,6 +98,26 @@ class TestAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("AgentManager", () => {
|
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 () => {
|
test("createAgent persists provided title before returning", async () => {
|
||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
const registryPath = join(workdir, "agents.json");
|
const registryPath = join(workdir, "agents.json");
|
||||||
@@ -109,4 +143,53 @@ describe("AgentManager", () => {
|
|||||||
expect(persisted?.title).toBe("Fix Login Bug");
|
expect(persisted?.title).toBe("Fix Login Bug");
|
||||||
expect(persisted?.id).toBe("agent-with-title");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import type {
|
|||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
AgentTimelineItem,
|
AgentTimelineItem,
|
||||||
AgentUsage,
|
AgentUsage,
|
||||||
|
AgentRuntimeInfo,
|
||||||
ListPersistedAgentsOptions,
|
ListPersistedAgentsOptions,
|
||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "./agent-sdk-types.js";
|
} from "./agent-sdk-types.js";
|
||||||
import { resolveAgentModel } from "./model-resolver.js";
|
|
||||||
import type { AgentRegistry } from "./agent-registry.js";
|
import type { AgentRegistry } from "./agent-registry.js";
|
||||||
|
|
||||||
export const AGENT_LIFECYCLE_STATUSES = [
|
export const AGENT_LIFECYCLE_STATUSES = [
|
||||||
@@ -80,6 +80,7 @@ type ManagedAgentBase = {
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
capabilities: AgentCapabilityFlags;
|
capabilities: AgentCapabilityFlags;
|
||||||
config: AgentSessionConfig;
|
config: AgentSessionConfig;
|
||||||
|
runtimeInfo?: AgentRuntimeInfo;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
availableModes: AgentMode[];
|
availableModes: AgentMode[];
|
||||||
@@ -479,21 +480,21 @@ export class AgentManager {
|
|||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
return (async function* streamForwarder() {
|
return (async function* streamForwarder() {
|
||||||
|
let finalizeError: string | undefined;
|
||||||
try {
|
try {
|
||||||
for await (const event of iterator) {
|
for await (const event of iterator) {
|
||||||
self.handleStreamEvent(agent, event);
|
self.handleStreamEvent(agent, event);
|
||||||
yield event;
|
yield event;
|
||||||
}
|
}
|
||||||
finalize();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
finalizeError =
|
||||||
error instanceof Error ? error.message : "Agent stream failed";
|
error instanceof Error ? error.message : "Agent stream failed";
|
||||||
finalize(message);
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
await self.refreshRuntimeInfo(agent);
|
||||||
// Ensure we always clear the pending run and emit state when the stream is
|
// 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.
|
// 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,
|
sessionId: session.id,
|
||||||
capabilities: session.capabilities,
|
capabilities: session.capabilities,
|
||||||
config,
|
config,
|
||||||
|
runtimeInfo: undefined,
|
||||||
lifecycle: "initializing",
|
lifecycle: "initializing",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -729,6 +731,7 @@ export class AgentManager {
|
|||||||
this.agents.set(agentId, managed);
|
this.agents.set(agentId, managed);
|
||||||
// Initialize previousStatus to track transitions
|
// Initialize previousStatus to track transitions
|
||||||
this.previousStatuses.set(agentId, managed.lifecycle);
|
this.previousStatuses.set(agentId, managed.lifecycle);
|
||||||
|
await this.refreshRuntimeInfo(managed);
|
||||||
await this.persistSnapshot(managed, {
|
await this.persistSnapshot(managed, {
|
||||||
title: config.title ?? null,
|
title: config.title ?? null,
|
||||||
});
|
});
|
||||||
@@ -773,6 +776,16 @@ export class AgentManager {
|
|||||||
} catch {
|
} catch {
|
||||||
agent.pendingPermissions.clear();
|
agent.pendingPermissions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.refreshRuntimeInfo(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshRuntimeInfo(agent: ActiveManagedAgent): Promise<void> {
|
||||||
|
try {
|
||||||
|
agent.runtimeInfo = await agent.session.getRuntimeInfo();
|
||||||
|
} catch {
|
||||||
|
// Keep existing runtimeInfo if refresh fails.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async primeHistory(agent: ActiveManagedAgent): Promise<void> {
|
private async primeHistory(agent: ActiveManagedAgent): Promise<void> {
|
||||||
@@ -813,6 +826,7 @@ export class AgentManager {
|
|||||||
case "turn_completed":
|
case "turn_completed":
|
||||||
agent.lastUsage = event.usage;
|
agent.lastUsage = event.usage;
|
||||||
agent.lastError = undefined;
|
agent.lastError = undefined;
|
||||||
|
void this.refreshRuntimeInfo(agent);
|
||||||
break;
|
break;
|
||||||
case "turn_failed":
|
case "turn_failed":
|
||||||
agent.lastError = event.error;
|
agent.lastError = event.error;
|
||||||
@@ -949,15 +963,7 @@ export class AgentManager {
|
|||||||
normalized.cwd = resolve(normalized.cwd);
|
normalized.cwd = resolve(normalized.cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedModel = await resolveAgentModel({
|
if (typeof normalized.model === "string") {
|
||||||
provider: normalized.provider,
|
|
||||||
requestedModel: normalized.model,
|
|
||||||
cwd: normalized.cwd,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resolvedModel) {
|
|
||||||
normalized.model = resolvedModel;
|
|
||||||
} else if (typeof normalized.model === "string") {
|
|
||||||
const trimmed = normalized.model.trim();
|
const trimmed = normalized.model.trim();
|
||||||
normalized.model = trimmed.length > 0 ? trimmed : undefined;
|
normalized.model = trimmed.length > 0 ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,11 +83,18 @@ function createManagedAgent(
|
|||||||
pendingPermissionsOverride ?? new Map<string, AgentPermissionRequest>(),
|
pendingPermissionsOverride ?? new Map<string, AgentPermissionRequest>(),
|
||||||
pendingRun: pendingRunValue as ManagedAgent["pendingRun"],
|
pendingRun: pendingRunValue as ManagedAgent["pendingRun"],
|
||||||
timeline: [],
|
timeline: [],
|
||||||
|
runtimeInfo: {
|
||||||
|
provider: "claude",
|
||||||
|
sessionId: "session-123",
|
||||||
|
model: "claude-3.5-sonnet",
|
||||||
|
modeId: "plan",
|
||||||
|
},
|
||||||
persistence: { ...basePersistence },
|
persistence: { ...basePersistence },
|
||||||
lastUsage: undefined,
|
lastUsage: undefined,
|
||||||
lastError: lastErrorValue,
|
lastError: lastErrorValue,
|
||||||
historyPrimed: true,
|
historyPrimed: true,
|
||||||
lastUserMessageAt: now,
|
lastUserMessageAt: now,
|
||||||
|
attention: { requiresAttention: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -149,6 +156,12 @@ describe("toStoredAgentRecord", () => {
|
|||||||
note: "warm",
|
note: "warm",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
expect(record.runtimeInfo).toEqual({
|
||||||
|
provider: "claude",
|
||||||
|
sessionId: "session-123",
|
||||||
|
model: "claude-3.5-sonnet",
|
||||||
|
modeId: "plan",
|
||||||
|
});
|
||||||
expect(record.config).toEqual({
|
expect(record.config).toEqual({
|
||||||
modeId: agent.config.modeId,
|
modeId: agent.config.modeId,
|
||||||
model: agent.config.model,
|
model: agent.config.model,
|
||||||
@@ -232,6 +245,8 @@ describe("toAgentPayload", () => {
|
|||||||
requestedAt: "2025-02-02T00:00:00.000Z",
|
requestedAt: "2025-02-02T00:00:00.000Z",
|
||||||
extra: { flag: true },
|
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).not.toBe(agent.availableModes);
|
||||||
expect(payload.availableModes).toEqual(agent.availableModes);
|
expect(payload.availableModes).toEqual(agent.availableModes);
|
||||||
expect(payload.capabilities).not.toBe(agent.capabilities);
|
expect(payload.capabilities).not.toBe(agent.capabilities);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function toStoredAgentRecord(
|
|||||||
const createdAt = options?.createdAt ?? agent.createdAt.toISOString();
|
const createdAt = options?.createdAt ?? agent.createdAt.toISOString();
|
||||||
const config = buildSerializableConfig(agent.config);
|
const config = buildSerializableConfig(agent.config);
|
||||||
const persistence = sanitizePersistenceHandle(agent.persistence);
|
const persistence = sanitizePersistenceHandle(agent.persistence);
|
||||||
|
const runtimeInfo = sanitizeOptionalJsonValue(agent.runtimeInfo);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: agent.id,
|
id: agent.id,
|
||||||
@@ -42,6 +43,7 @@ export function toStoredAgentRecord(
|
|||||||
lastStatus: agent.lifecycle,
|
lastStatus: agent.lifecycle,
|
||||||
lastModeId: agent.currentModeId ?? config?.modeId ?? null,
|
lastModeId: agent.currentModeId ?? config?.modeId ?? null,
|
||||||
config: config ?? null,
|
config: config ?? null,
|
||||||
|
runtimeInfo,
|
||||||
persistence,
|
persistence,
|
||||||
requiresAttention: agent.attention.requiresAttention,
|
requiresAttention: agent.attention.requiresAttention,
|
||||||
attentionReason: agent.attention.requiresAttention
|
attentionReason: agent.attention.requiresAttention
|
||||||
@@ -57,11 +59,14 @@ export function toAgentPayload(
|
|||||||
agent: ManagedAgent,
|
agent: ManagedAgent,
|
||||||
options?: ProjectionOptions
|
options?: ProjectionOptions
|
||||||
): AgentSnapshotPayload {
|
): AgentSnapshotPayload {
|
||||||
|
const runtimeInfo = sanitizeOptionalJsonValue(agent.runtimeInfo);
|
||||||
|
|
||||||
const payload: AgentSnapshotPayload = {
|
const payload: AgentSnapshotPayload = {
|
||||||
id: agent.id,
|
id: agent.id,
|
||||||
provider: agent.provider,
|
provider: agent.provider,
|
||||||
cwd: agent.cwd,
|
cwd: agent.cwd,
|
||||||
model: agent.config.model ?? null,
|
model: agent.config.model ?? null,
|
||||||
|
runtimeInfo,
|
||||||
createdAt: agent.createdAt.toISOString(),
|
createdAt: agent.createdAt.toISOString(),
|
||||||
updatedAt: agent.updatedAt.toISOString(),
|
updatedAt: agent.updatedAt.toISOString(),
|
||||||
lastUserMessageAt: agent.lastUserMessageAt
|
lastUserMessageAt: agent.lastUserMessageAt
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ type ManagedAgentOverrides = Omit<
|
|||||||
pendingPermissions?: Map<string, AgentPermissionRequest>;
|
pendingPermissions?: Map<string, AgentPermissionRequest>;
|
||||||
session?: AgentSession | null;
|
session?: AgentSession | null;
|
||||||
pendingRun?: ManagedAgent["pendingRun"];
|
pendingRun?: ManagedAgent["pendingRun"];
|
||||||
|
runtimeInfo?: ManagedAgent["runtimeInfo"];
|
||||||
|
attention?: ManagedAgent["attention"];
|
||||||
};
|
};
|
||||||
|
|
||||||
function createManagedAgent(
|
function createManagedAgent(
|
||||||
@@ -71,6 +73,14 @@ function createManagedAgent(
|
|||||||
new Map<string, AgentPermissionRequest>(),
|
new Map<string, AgentPermissionRequest>(),
|
||||||
pendingRun,
|
pendingRun,
|
||||||
timeline: overrides.timeline ?? [],
|
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,
|
persistence: overrides.persistence ?? null,
|
||||||
historyPrimed: overrides.historyPrimed ?? true,
|
historyPrimed: overrides.historyPrimed ?? true,
|
||||||
lastUserMessageAt: overrides.lastUserMessageAt ?? now,
|
lastUserMessageAt: overrides.lastUserMessageAt ?? now,
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ const STORED_AGENT_SCHEMA = z.object({
|
|||||||
lastStatus: AgentStatusSchema.default("closed"),
|
lastStatus: AgentStatusSchema.default("closed"),
|
||||||
lastModeId: z.string().nullable().optional(),
|
lastModeId: z.string().nullable().optional(),
|
||||||
config: SERIALIZABLE_CONFIG_SCHEMA,
|
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,
|
persistence: PERSISTENCE_HANDLE_SCHEMA,
|
||||||
requiresAttention: z.boolean().optional(),
|
requiresAttention: z.boolean().optional(),
|
||||||
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
||||||
|
|||||||
@@ -123,6 +123,14 @@ export type AgentRunResult = {
|
|||||||
timeline: AgentTimelineItem[];
|
timeline: AgentTimelineItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AgentRuntimeInfo = {
|
||||||
|
provider: AgentProvider;
|
||||||
|
sessionId: string | null;
|
||||||
|
model?: string | null;
|
||||||
|
modeId?: string | null;
|
||||||
|
extra?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ListPersistedAgentsOptions = {
|
export type ListPersistedAgentsOptions = {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
@@ -162,6 +170,7 @@ export interface AgentSession {
|
|||||||
run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult>;
|
run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult>;
|
||||||
stream(prompt: AgentPromptInput, options?: AgentRunOptions): AsyncGenerator<AgentStreamEvent>;
|
stream(prompt: AgentPromptInput, options?: AgentRunOptions): AsyncGenerator<AgentStreamEvent>;
|
||||||
streamHistory(): AsyncGenerator<AgentStreamEvent>;
|
streamHistory(): AsyncGenerator<AgentStreamEvent>;
|
||||||
|
getRuntimeInfo(): Promise<AgentRuntimeInfo>;
|
||||||
getAvailableModes(): Promise<AgentMode[]>;
|
getAvailableModes(): Promise<AgentMode[]>;
|
||||||
getCurrentMode(): Promise<string | null>;
|
getCurrentMode(): Promise<string | null>;
|
||||||
setMode(modeId: string): Promise<void>;
|
setMode(modeId: string): Promise<void>;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import type {
|
|||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
AgentTimelineItem,
|
AgentTimelineItem,
|
||||||
AgentUsage,
|
AgentUsage,
|
||||||
|
AgentRuntimeInfo,
|
||||||
ListPersistedAgentsOptions,
|
ListPersistedAgentsOptions,
|
||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
@@ -386,6 +387,8 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
private streamedAssistantTextThisTurn = false;
|
private streamedAssistantTextThisTurn = false;
|
||||||
private streamedReasoningThisTurn = false;
|
private streamedReasoningThisTurn = false;
|
||||||
private cancelCurrentTurn: (() => void) | null = null;
|
private cancelCurrentTurn: (() => void) | null = null;
|
||||||
|
private cachedRuntimeInfo: AgentRuntimeInfo | null = null;
|
||||||
|
private lastOptionsModel: string | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: ClaudeAgentConfig,
|
config: ClaudeAgentConfig,
|
||||||
@@ -417,6 +420,20 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
return this.claudeSessionId;
|
return this.claudeSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRuntimeInfo(): Promise<AgentRuntimeInfo> {
|
||||||
|
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<AgentRunResult> {
|
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
|
||||||
const events = this.stream(prompt, options);
|
const events = this.stream(prompt, options);
|
||||||
const timeline: AgentTimelineItem[] = [];
|
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 {
|
return {
|
||||||
sessionId: this.claudeSessionId ?? this.pendingLocalId,
|
sessionId: this.claudeSessionId ?? this.pendingLocalId,
|
||||||
finalText,
|
finalText,
|
||||||
@@ -608,11 +632,12 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
if (!this.claudeSessionId) {
|
if (!this.claudeSessionId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const { model: _ignoredModel, ...restConfig } = this.config;
|
||||||
this.persistence = {
|
this.persistence = {
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
sessionId: this.claudeSessionId,
|
sessionId: this.claudeSessionId,
|
||||||
nativeHandle: this.claudeSessionId,
|
nativeHandle: this.claudeSessionId,
|
||||||
metadata: this.config,
|
metadata: restConfig,
|
||||||
};
|
};
|
||||||
return this.persistence;
|
return this.persistence;
|
||||||
}
|
}
|
||||||
@@ -684,6 +709,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
if (this.config.model) {
|
if (this.config.model) {
|
||||||
base.model = this.config.model;
|
base.model = this.config.model;
|
||||||
}
|
}
|
||||||
|
this.lastOptionsModel = base.model ?? null;
|
||||||
if (this.claudeSessionId) {
|
if (this.claudeSessionId) {
|
||||||
base.resume = this.claudeSessionId;
|
base.resume = this.claudeSessionId;
|
||||||
base.continue = true;
|
base.continue = true;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import type {
|
|||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
AgentTimelineItem,
|
AgentTimelineItem,
|
||||||
AgentUsage,
|
AgentUsage,
|
||||||
|
AgentRuntimeInfo,
|
||||||
ListPersistedAgentsOptions,
|
ListPersistedAgentsOptions,
|
||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
@@ -66,19 +67,21 @@ const CODEX_MODES: AgentMode[] = [
|
|||||||
{
|
{
|
||||||
id: "full-access",
|
id: "full-access",
|
||||||
label: "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(
|
const VALID_CODEX_MODES = new Set(CODEX_MODES.map((mode) => mode.id));
|
||||||
CODEX_MODES.map((mode) => mode.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const DEFAULT_CODEX_MODE_ID = "auto";
|
const DEFAULT_CODEX_MODE_ID = "auto";
|
||||||
|
|
||||||
const MODE_PRESETS: Record<
|
const MODE_PRESETS: Record<
|
||||||
string,
|
string,
|
||||||
Partial<ThreadOptions> & { networkAccessEnabled?: boolean; webSearchEnabled?: boolean }
|
Partial<ThreadOptions> & {
|
||||||
|
networkAccessEnabled?: boolean;
|
||||||
|
webSearchEnabled?: boolean;
|
||||||
|
}
|
||||||
> = {
|
> = {
|
||||||
"read-only": {
|
"read-only": {
|
||||||
sandboxMode: "read-only",
|
sandboxMode: "read-only",
|
||||||
@@ -100,7 +103,9 @@ const MODE_PRESETS: Record<
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type CodexOptionsOverrides = Partial<ThreadOptions> & { skipGitRepoCheck?: boolean };
|
type CodexOptionsOverrides = Partial<ThreadOptions> & {
|
||||||
|
skipGitRepoCheck?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_ROLLOUT_SEARCH_DEPTH = 5;
|
const MAX_ROLLOUT_SEARCH_DEPTH = 5;
|
||||||
const PERSISTED_TIMELINE_LIMIT = 20;
|
const PERSISTED_TIMELINE_LIMIT = 20;
|
||||||
@@ -142,7 +147,10 @@ function normalizeCallId(value?: string | null): string | undefined {
|
|||||||
|
|
||||||
function extractThreadItemCallId(item: ThreadItem): string | undefined {
|
function extractThreadItemCallId(item: ThreadItem): string | undefined {
|
||||||
const withCallId = item as ThreadItemWithOptionalCallId;
|
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 {
|
function buildCommandDisplayName(command?: unknown): string {
|
||||||
@@ -155,7 +163,9 @@ function buildCommandDisplayName(command?: unknown): string {
|
|||||||
return "Command";
|
return "Command";
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFileChangeSummary(files: { path: string; kind: string }[]): string {
|
function buildFileChangeSummary(
|
||||||
|
files: { path: string; kind: string }[]
|
||||||
|
): string {
|
||||||
if (!files.length) {
|
if (!files.length) {
|
||||||
return "File change";
|
return "File change";
|
||||||
}
|
}
|
||||||
@@ -167,7 +177,11 @@ function buildFileChangeSummary(files: { path: string; kind: string }[]): string
|
|||||||
|
|
||||||
function coerceSandboxMode(value?: string): SandboxMode | undefined {
|
function coerceSandboxMode(value?: string): SandboxMode | undefined {
|
||||||
if (!value) return 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 value;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -175,21 +189,35 @@ function coerceSandboxMode(value?: string): SandboxMode | undefined {
|
|||||||
|
|
||||||
function coerceApprovalMode(value?: string): ApprovalMode | undefined {
|
function coerceApprovalMode(value?: string): ApprovalMode | undefined {
|
||||||
if (!value) return 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 value;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function coerceReasoningEffort(value?: string): ModelReasoningEffort | undefined {
|
function coerceReasoningEffort(
|
||||||
|
value?: string
|
||||||
|
): ModelReasoningEffort | undefined {
|
||||||
if (!value) return 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 value;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeExtraOptions(extra?: Record<string, unknown>): CodexOptionsOverrides | undefined {
|
function normalizeExtraOptions(
|
||||||
|
extra?: Record<string, unknown>
|
||||||
|
): CodexOptionsOverrides | undefined {
|
||||||
if (!extra) return undefined;
|
if (!extra) return undefined;
|
||||||
const allowedKeys = new Set([
|
const allowedKeys = new Set([
|
||||||
"model",
|
"model",
|
||||||
@@ -211,7 +239,10 @@ function normalizeExtraOptions(extra?: Record<string, unknown>): CodexOptionsOve
|
|||||||
|
|
||||||
function detectSystemCodexPath(): string | undefined {
|
function detectSystemCodexPath(): string | undefined {
|
||||||
try {
|
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) {
|
if (codexPath && codexPath.length > 0) {
|
||||||
return codexPath;
|
return codexPath;
|
||||||
}
|
}
|
||||||
@@ -236,7 +267,9 @@ export class CodexAgentClient implements AgentClient {
|
|||||||
codexOptions.codexPathOverride = systemCodexPath;
|
codexOptions.codexPathOverride = systemCodexPath;
|
||||||
console.log(`[Codex] Using system binary: ${systemCodexPath}`);
|
console.log(`[Codex] Using system binary: ${systemCodexPath}`);
|
||||||
} else {
|
} 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);
|
return CodexAgentSession.create(this.codex, codexConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resumeSession(handle: AgentPersistenceHandle, overrides?: Partial<AgentSessionConfig>): Promise<AgentSession> {
|
async resumeSession(
|
||||||
|
handle: AgentPersistenceHandle,
|
||||||
|
overrides?: Partial<AgentSessionConfig>
|
||||||
|
): Promise<AgentSession> {
|
||||||
const metadata = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
|
const metadata = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
|
||||||
const merged = { ...metadata, ...overrides } as Partial<AgentSessionConfig>;
|
const merged = { ...metadata, ...overrides } as Partial<AgentSessionConfig>;
|
||||||
if (!merged.cwd) {
|
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 mergedConfig = { ...merged, provider: "codex" } as AgentSessionConfig;
|
||||||
const codexConfig = this.assertConfig(mergedConfig);
|
const codexConfig = this.assertConfig(mergedConfig);
|
||||||
return CodexAgentSession.create(this.codex, codexConfig, handle);
|
return CodexAgentSession.create(this.codex, codexConfig, handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listPersistedAgents(options?: ListPersistedAgentsOptions): Promise<PersistedAgentDescriptor[]> {
|
async listPersistedAgents(
|
||||||
|
options?: ListPersistedAgentsOptions
|
||||||
|
): Promise<PersistedAgentDescriptor[]> {
|
||||||
const sessionRoot = resolveCodexSessionRoot();
|
const sessionRoot = resolveCodexSessionRoot();
|
||||||
if (!sessionRoot) {
|
if (!sessionRoot) {
|
||||||
return [];
|
return [];
|
||||||
@@ -271,7 +311,11 @@ export class CodexAgentClient implements AgentClient {
|
|||||||
const candidates = await collectRecentRolloutFiles(sessionRoot, limit * 3);
|
const candidates = await collectRecentRolloutFiles(sessionRoot, limit * 3);
|
||||||
const descriptors: PersistedAgentDescriptor[] = [];
|
const descriptors: PersistedAgentDescriptor[] = [];
|
||||||
for (const candidate of candidates) {
|
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) {
|
if (descriptor) {
|
||||||
descriptors.push(descriptor);
|
descriptors.push(descriptor);
|
||||||
}
|
}
|
||||||
@@ -284,14 +328,20 @@ export class CodexAgentClient implements AgentClient {
|
|||||||
|
|
||||||
private assertConfig(config: AgentSessionConfig): CodexAgentConfig {
|
private assertConfig(config: AgentSessionConfig): CodexAgentConfig {
|
||||||
if (config.provider !== "codex") {
|
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;
|
return config as CodexAgentConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CodexAgentSession implements AgentSession {
|
class CodexAgentSession implements AgentSession {
|
||||||
static async create(codex: Codex, config: CodexAgentConfig, handle?: AgentPersistenceHandle): Promise<CodexAgentSession> {
|
static async create(
|
||||||
|
codex: Codex,
|
||||||
|
config: CodexAgentConfig,
|
||||||
|
handle?: AgentPersistenceHandle
|
||||||
|
): Promise<CodexAgentSession> {
|
||||||
const session = new CodexAgentSession(codex, config, handle);
|
const session = new CodexAgentSession(codex, config, handle);
|
||||||
if (handle) {
|
if (handle) {
|
||||||
await session.loadReplayHistory(handle);
|
await session.loadReplayHistory(handle);
|
||||||
@@ -316,8 +366,13 @@ class CodexAgentSession implements AgentSession {
|
|||||||
private historyEvents: AgentStreamEvent[] = [];
|
private historyEvents: AgentStreamEvent[] = [];
|
||||||
private pendingPermissions = new Map<string, AgentPermissionRequest>();
|
private pendingPermissions = new Map<string, AgentPermissionRequest>();
|
||||||
private cancelCurrentTurn: (() => void) | null = null;
|
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.codex = codex;
|
||||||
this.config = { ...config };
|
this.config = { ...config };
|
||||||
|
|
||||||
@@ -336,6 +391,7 @@ class CodexAgentSession implements AgentSession {
|
|||||||
this.threadId = handle?.sessionId ?? handle?.nativeHandle ?? null;
|
this.threadId = handle?.sessionId ?? handle?.nativeHandle ?? null;
|
||||||
this.pendingLocalId = this.threadId ?? `codex-${randomUUID()}`;
|
this.pendingLocalId = this.threadId ?? `codex-${randomUUID()}`;
|
||||||
this.persistence = handle ?? null;
|
this.persistence = handle ?? null;
|
||||||
|
this.cachedRuntimeInfo = null;
|
||||||
if (handle && !this.threadId) {
|
if (handle && !this.threadId) {
|
||||||
throw new Error("Codex resume requires a thread id");
|
throw new Error("Codex resume requires a thread id");
|
||||||
}
|
}
|
||||||
@@ -348,7 +404,31 @@ class CodexAgentSession implements AgentSession {
|
|||||||
return this.threadId;
|
return this.threadId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
|
async getRuntimeInfo(): Promise<AgentRuntimeInfo> {
|
||||||
|
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<AgentRunResult> {
|
||||||
const events = this.stream(prompt, options);
|
const events = this.stream(prompt, options);
|
||||||
const timeline: AgentTimelineItem[] = [];
|
const timeline: AgentTimelineItem[] = [];
|
||||||
let finalText = "";
|
let finalText = "";
|
||||||
@@ -367,6 +447,9 @@ class CodexAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update runtime info cache after the turn completes
|
||||||
|
await this.refreshRuntimeInfoFromRollout();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId: this.threadId ?? this.pendingLocalId,
|
sessionId: this.threadId ?? this.pendingLocalId,
|
||||||
finalText,
|
finalText,
|
||||||
@@ -375,7 +458,10 @@ class CodexAgentSession implements AgentSession {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async *stream(prompt: AgentPromptInput, options?: AgentRunOptions): AsyncGenerator<AgentStreamEvent> {
|
async *stream(
|
||||||
|
prompt: AgentPromptInput,
|
||||||
|
options?: AgentRunOptions
|
||||||
|
): AsyncGenerator<AgentStreamEvent> {
|
||||||
const thread = this.thread;
|
const thread = this.thread;
|
||||||
if (!thread) {
|
if (!thread) {
|
||||||
throw new Error("Codex session is closed");
|
throw new Error("Codex session is closed");
|
||||||
@@ -405,7 +491,10 @@ class CodexAgentSession implements AgentSession {
|
|||||||
type: "turn.completed",
|
type: "turn.completed",
|
||||||
} as ThreadEvent);
|
} as ThreadEvent);
|
||||||
void cancellation.catch((error) => {
|
void cancellation.catch((error) => {
|
||||||
console.warn("[CodexAgentSession] Failed to stop Codex stream:", error);
|
console.warn(
|
||||||
|
"[CodexAgentSession] Failed to stop Codex stream:",
|
||||||
|
error
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[CodexAgentSession] Failed to stop Codex stream:", error);
|
console.warn("[CodexAgentSession] Failed to stop Codex stream:", error);
|
||||||
@@ -433,6 +522,7 @@ class CodexAgentSession implements AgentSession {
|
|||||||
if (this.cancelCurrentTurn === requestCancel) {
|
if (this.cancelCurrentTurn === requestCancel) {
|
||||||
this.cancelCurrentTurn = null;
|
this.cancelCurrentTurn = null;
|
||||||
}
|
}
|
||||||
|
await this.refreshRuntimeInfoFromRollout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,10 +574,15 @@ class CodexAgentSession implements AgentSession {
|
|||||||
return Array.from(this.pendingPermissions.values());
|
return Array.from(this.pendingPermissions.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
async respondToPermission(requestId: string, response: AgentPermissionResponse): Promise<void> {
|
async respondToPermission(
|
||||||
|
requestId: string,
|
||||||
|
response: AgentPermissionResponse
|
||||||
|
): Promise<void> {
|
||||||
const request = this.pendingPermissions.get(requestId);
|
const request = this.pendingPermissions.get(requestId);
|
||||||
if (!request) {
|
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);
|
this.pendingPermissions.delete(requestId);
|
||||||
@@ -522,12 +617,13 @@ class CodexAgentSession implements AgentSession {
|
|||||||
if (!this.threadId) {
|
if (!this.threadId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const { model: _ignoredModel, ...restConfig } = this.config;
|
||||||
this.persistence = {
|
this.persistence = {
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
sessionId: this.threadId,
|
sessionId: this.threadId,
|
||||||
nativeHandle: this.threadId,
|
nativeHandle: this.threadId,
|
||||||
metadata: {
|
metadata: {
|
||||||
...this.config,
|
...restConfig,
|
||||||
codexSessionDir: this.codexSessionDir ?? undefined,
|
codexSessionDir: this.codexSessionDir ?? undefined,
|
||||||
codexRolloutPath: this.rolloutPath ?? undefined,
|
codexRolloutPath: this.rolloutPath ?? undefined,
|
||||||
},
|
},
|
||||||
@@ -568,10 +664,13 @@ class CodexAgentSession implements AgentSession {
|
|||||||
options.model = this.config.model;
|
options.model = this.config.model;
|
||||||
}
|
}
|
||||||
if (this.config.sandboxMode) {
|
if (this.config.sandboxMode) {
|
||||||
options.sandboxMode = coerceSandboxMode(this.config.sandboxMode) ?? options.sandboxMode;
|
options.sandboxMode =
|
||||||
|
coerceSandboxMode(this.config.sandboxMode) ?? options.sandboxMode;
|
||||||
}
|
}
|
||||||
if (this.config.approvalPolicy) {
|
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") {
|
if (typeof this.config.networkAccess === "boolean") {
|
||||||
options.networkAccessEnabled = this.config.networkAccess;
|
options.networkAccessEnabled = this.config.networkAccess;
|
||||||
@@ -581,10 +680,13 @@ class CodexAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
if (this.config.reasoningEffort) {
|
if (this.config.reasoningEffort) {
|
||||||
options.modelReasoningEffort =
|
options.modelReasoningEffort =
|
||||||
coerceReasoningEffort(this.config.reasoningEffort) ?? options.modelReasoningEffort;
|
coerceReasoningEffort(this.config.reasoningEffort) ??
|
||||||
|
options.modelReasoningEffort;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extra = normalizeExtraOptions(this.config.extra?.codex as Record<string, unknown> | undefined);
|
const extra = normalizeExtraOptions(
|
||||||
|
this.config.extra?.codex as Record<string, unknown> | undefined
|
||||||
|
);
|
||||||
if (extra) {
|
if (extra) {
|
||||||
Object.assign(options, extra);
|
Object.assign(options, extra);
|
||||||
}
|
}
|
||||||
@@ -592,7 +694,9 @@ class CodexAgentSession implements AgentSession {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadReplayHistory(handle: AgentPersistenceHandle): Promise<void> {
|
private async loadReplayHistory(
|
||||||
|
handle: AgentPersistenceHandle
|
||||||
|
): Promise<void> {
|
||||||
const threadIdentifier = handle.sessionId ?? handle.nativeHandle;
|
const threadIdentifier = handle.sessionId ?? handle.nativeHandle;
|
||||||
if (!threadIdentifier || !this.codexSessionDir) {
|
if (!threadIdentifier || !this.codexSessionDir) {
|
||||||
return;
|
return;
|
||||||
@@ -604,7 +708,10 @@ class CodexAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!rolloutFile) {
|
if (!rolloutFile) {
|
||||||
rolloutFile = await findRolloutFile(threadIdentifier, this.codexSessionDir);
|
rolloutFile = await findRolloutFile(
|
||||||
|
threadIdentifier,
|
||||||
|
this.codexSessionDir
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rolloutFile) {
|
if (!rolloutFile) {
|
||||||
@@ -612,6 +719,7 @@ class CodexAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.rolloutPath = rolloutFile;
|
this.rolloutPath = rolloutFile;
|
||||||
|
await this.refreshRuntimeInfoFromRollout();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const events = await parseRolloutFile(rolloutFile, threadIdentifier);
|
const events = await parseRolloutFile(rolloutFile, threadIdentifier);
|
||||||
@@ -619,11 +727,16 @@ class CodexAgentSession implements AgentSession {
|
|||||||
this.historyEvents = events;
|
this.historyEvents = events;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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");
|
const metadataDir = readMetadataString(handle, "codexSessionDir");
|
||||||
if (metadataDir) {
|
if (metadataDir) {
|
||||||
return metadataDir;
|
return metadataDir;
|
||||||
@@ -639,7 +752,8 @@ class CodexAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const extraHome = readCodexExtraString(this.config, "home");
|
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");
|
return path.join(codexHome, "sessions");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,6 +766,46 @@ class CodexAgentSession implements AgentSession {
|
|||||||
return extraRollout ?? null;
|
return extraRollout ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async refreshRuntimeInfoFromRollout(): Promise<void> {
|
||||||
|
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<string | null> {
|
||||||
|
const rollout = this.rolloutPath;
|
||||||
|
if (!rollout) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await readLatestTurnContextModel(rollout);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private buildTurnOptions(options?: AgentRunOptions): TurnOptions {
|
private buildTurnOptions(options?: AgentRunOptions): TurnOptions {
|
||||||
if (!options?.outputSchema) {
|
if (!options?.outputSchema) {
|
||||||
return {};
|
return {};
|
||||||
@@ -680,7 +834,11 @@ class CodexAgentSession implements AgentSession {
|
|||||||
this.threadId = event.thread_id;
|
this.threadId = event.thread_id;
|
||||||
this.pendingLocalId = event.thread_id;
|
this.pendingLocalId = event.thread_id;
|
||||||
this.persistence = null;
|
this.persistence = null;
|
||||||
yield { type: "thread_started", provider: "codex", sessionId: event.thread_id };
|
yield {
|
||||||
|
type: "thread_started",
|
||||||
|
provider: "codex",
|
||||||
|
sessionId: event.thread_id,
|
||||||
|
};
|
||||||
break;
|
break;
|
||||||
case "turn.started":
|
case "turn.started":
|
||||||
yield { type: "turn_started", provider: "codex" };
|
yield { type: "turn_started", provider: "codex" };
|
||||||
@@ -724,7 +882,10 @@ class CodexAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handlePermissionEvent(event: ThreadEvent): AgentStreamEvent[] | null {
|
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) {
|
if (!eventType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -742,7 +903,9 @@ class CodexAgentSession implements AgentSession {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private enqueuePermissionRequest(request: AgentPermissionRequest | null): AgentStreamEvent[] | null {
|
private enqueuePermissionRequest(
|
||||||
|
request: AgentPermissionRequest | null
|
||||||
|
): AgentStreamEvent[] | null {
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -769,7 +932,9 @@ class CodexAgentSession implements AgentSession {
|
|||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildExecPermissionRequest(rawInput: unknown): AgentPermissionRequest | null {
|
private buildExecPermissionRequest(
|
||||||
|
rawInput: unknown
|
||||||
|
): AgentPermissionRequest | null {
|
||||||
if (!rawInput || typeof rawInput !== "object") {
|
if (!rawInput || typeof rawInput !== "object") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -778,14 +943,24 @@ class CodexAgentSession implements AgentSession {
|
|||||||
const commandParts: string[] = Array.isArray(raw.command)
|
const commandParts: string[] = Array.isArray(raw.command)
|
||||||
? raw.command.filter((entry: unknown) => typeof entry === "string")
|
? raw.command.filter((entry: unknown) => typeof entry === "string")
|
||||||
: typeof raw.command === "string"
|
: typeof raw.command === "string"
|
||||||
? [raw.command]
|
? [raw.command]
|
||||||
: [];
|
: [];
|
||||||
const commandText = commandParts.join(" ").trim();
|
const commandText = commandParts.join(" ").trim();
|
||||||
const cwd = typeof raw.cwd === "string" && raw.cwd.length ? raw.cwd : undefined;
|
const cwd =
|
||||||
const requestId = typeof raw.call_id === "string" && raw.call_id.length ? raw.call_id : randomUUID();
|
typeof raw.cwd === "string" && raw.cwd.length ? raw.cwd : undefined;
|
||||||
const reason = typeof raw.reason === "string" && raw.reason.length ? raw.reason : undefined;
|
const requestId =
|
||||||
const risk = raw.risk && typeof raw.risk === "object" ? raw.risk : undefined;
|
typeof raw.call_id === "string" && raw.call_id.length
|
||||||
const parsedCommand = Array.isArray(raw.parsed_cmd) ? raw.parsed_cmd : undefined;
|
? 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<string, unknown> = {
|
const metadata: Record<string, unknown> = {
|
||||||
callId: raw.call_id,
|
callId: raw.call_id,
|
||||||
@@ -822,21 +997,35 @@ class CodexAgentSession implements AgentSession {
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildPatchPermissionRequest(rawInput: unknown): AgentPermissionRequest | null {
|
private buildPatchPermissionRequest(
|
||||||
|
rawInput: unknown
|
||||||
|
): AgentPermissionRequest | null {
|
||||||
if (!rawInput || typeof rawInput !== "object") {
|
if (!rawInput || typeof rawInput !== "object") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const raw = rawInput as PatchPermissionRequestPayload;
|
const raw = rawInput as PatchPermissionRequestPayload;
|
||||||
|
|
||||||
const requestId = typeof raw.call_id === "string" && raw.call_id.length ? raw.call_id : randomUUID();
|
const requestId =
|
||||||
const changes = raw.changes && typeof raw.changes === "object" ? raw.changes : undefined;
|
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 filePaths = changes ? Object.keys(changes) : [];
|
||||||
const grantRoot = typeof raw.grant_root === "string" && raw.grant_root.length ? raw.grant_root : undefined;
|
const grantRoot =
|
||||||
const reason = typeof raw.reason === "string" && raw.reason.length ? raw.reason : undefined;
|
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 =
|
const title =
|
||||||
filePaths.length > 0
|
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";
|
: "Apply patch";
|
||||||
|
|
||||||
const metadata: Record<string, unknown> = {
|
const metadata: Record<string, unknown> = {
|
||||||
@@ -881,17 +1070,24 @@ class CodexAgentSession implements AgentSession {
|
|||||||
const exitCode = (item as any)?.exit_code;
|
const exitCode = (item as any)?.exit_code;
|
||||||
const cwd = (item as any)?.cwd;
|
const cwd = (item as any)?.cwd;
|
||||||
const commandValue = item.command;
|
const commandValue = item.command;
|
||||||
const command = typeof commandValue === "string" ? commandValue :
|
const command =
|
||||||
Array.isArray(commandValue) ? (commandValue as string[]).join(" ") : "command";
|
typeof commandValue === "string"
|
||||||
|
? commandValue
|
||||||
|
: Array.isArray(commandValue)
|
||||||
|
? (commandValue as string[]).join(" ")
|
||||||
|
: "command";
|
||||||
|
|
||||||
// Build structured command result matching StructuredToolResult type
|
// Build structured command result matching StructuredToolResult type
|
||||||
const structuredOutput = typeof aggregatedOutput === "string" ? {
|
const structuredOutput =
|
||||||
type: "command" as const,
|
typeof aggregatedOutput === "string"
|
||||||
command,
|
? {
|
||||||
output: aggregatedOutput,
|
type: "command" as const,
|
||||||
exitCode: typeof exitCode === "number" ? exitCode : undefined,
|
command,
|
||||||
cwd,
|
output: aggregatedOutput,
|
||||||
} : undefined;
|
exitCode: typeof exitCode === "number" ? exitCode : undefined,
|
||||||
|
cwd,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return createToolCallTimelineItem({
|
return createToolCallTimelineItem({
|
||||||
server: "command",
|
server: "command",
|
||||||
@@ -906,7 +1102,10 @@ class CodexAgentSession implements AgentSession {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
case "file_change": {
|
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({
|
return createToolCallTimelineItem({
|
||||||
server: "file_change",
|
server: "file_change",
|
||||||
tool: "apply_patch",
|
tool: "apply_patch",
|
||||||
@@ -968,7 +1167,10 @@ async function fileExists(filePath: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findRolloutFile(threadId: string, root: string): Promise<string | null> {
|
async function findRolloutFile(
|
||||||
|
threadId: string,
|
||||||
|
root: string
|
||||||
|
): Promise<string | null> {
|
||||||
const stack: { dir: string; depth: number }[] = [{ dir: root, depth: 0 }];
|
const stack: { dir: string; depth: number }[] = [{ dir: root, depth: 0 }];
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
const { dir, depth } = stack.pop()!;
|
const { dir, depth } = stack.pop()!;
|
||||||
@@ -983,7 +1185,8 @@ async function findRolloutFile(threadId: string, root: string): Promise<string |
|
|||||||
if (entry.isFile()) {
|
if (entry.isFile()) {
|
||||||
const matchesThread = entry.name.includes(threadId);
|
const matchesThread = entry.name.includes(threadId);
|
||||||
const matchesPrefix = entry.name.startsWith("rollout-");
|
const matchesPrefix = entry.name.startsWith("rollout-");
|
||||||
const matchesExtension = entry.name.endsWith(".json") || entry.name.endsWith(".jsonl");
|
const matchesExtension =
|
||||||
|
entry.name.endsWith(".json") || entry.name.endsWith(".jsonl");
|
||||||
if (matchesThread && matchesPrefix && matchesExtension) {
|
if (matchesThread && matchesPrefix && matchesExtension) {
|
||||||
return entryPath;
|
return entryPath;
|
||||||
}
|
}
|
||||||
@@ -995,10 +1198,18 @@ async function findRolloutFile(threadId: string, root: string): Promise<string |
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseRolloutFile(filePath: string, threadId: string): Promise<AgentStreamEvent[]> {
|
async function parseRolloutFile(
|
||||||
|
filePath: string,
|
||||||
|
threadId: string
|
||||||
|
): Promise<AgentStreamEvent[]> {
|
||||||
const content = await fs.readFile(filePath, "utf8");
|
const content = await fs.readFile(filePath, "utf8");
|
||||||
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
const lines = content
|
||||||
const events: AgentStreamEvent[] = [{ type: "thread_started", provider: "codex", sessionId: threadId }];
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const events: AgentStreamEvent[] = [
|
||||||
|
{ type: "thread_started", provider: "codex", sessionId: threadId },
|
||||||
|
];
|
||||||
const commandCalls = new Map<string, { command: string; cwd?: string }>();
|
const commandCalls = new Map<string, { command: string; cwd?: string }>();
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
@@ -1015,6 +1226,40 @@ async function parseRolloutFile(filePath: string, threadId: string): Promise<Age
|
|||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readLatestTurnContextModel(
|
||||||
|
filePath: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
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<string, unknown>).model;
|
||||||
|
if (typeof model === "string" && model.trim().length > 0) {
|
||||||
|
return model.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function parseRolloutEntry(line: string): RolloutEntry | null {
|
function parseRolloutEntry(line: string): RolloutEntry | null {
|
||||||
if (!line) {
|
if (!line) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1024,7 +1269,11 @@ function parseRolloutEntry(line: string): RolloutEntry | null {
|
|||||||
if (isRolloutEntry(parsed)) {
|
if (isRolloutEntry(parsed)) {
|
||||||
return 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);
|
return parseRolloutEntry((parsed as { output: string }).output);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1042,7 +1291,11 @@ function isRolloutEntry(value: unknown): value is RolloutEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isSessionMetaEntry(value: unknown): value is SessionMetaEntry {
|
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(
|
function handleRolloutResponseItem(
|
||||||
@@ -1059,12 +1312,20 @@ function handleRolloutResponseItem(
|
|||||||
const text = extractMessageText(payload.content);
|
const text = extractMessageText(payload.content);
|
||||||
if (text) {
|
if (text) {
|
||||||
if (payload.role === "assistant") {
|
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") {
|
} else if (payload.role === "user") {
|
||||||
if (isSyntheticRolloutUserMessage(text)) {
|
if (isSyntheticRolloutUserMessage(text)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
events.push({ type: "timeline", provider: "codex", item: { type: "user_message", text } });
|
events.push({
|
||||||
|
type: "timeline",
|
||||||
|
provider: "codex",
|
||||||
|
item: { type: "user_message", text },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1072,7 +1333,11 @@ function handleRolloutResponseItem(
|
|||||||
case "reasoning": {
|
case "reasoning": {
|
||||||
const text = extractReasoningText(payload);
|
const text = extractReasoningText(payload);
|
||||||
if (text) {
|
if (text) {
|
||||||
events.push({ type: "timeline", provider: "codex", item: { type: "reasoning", text } });
|
events.push({
|
||||||
|
type: "timeline",
|
||||||
|
provider: "codex",
|
||||||
|
item: { type: "reasoning", text },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
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") {
|
if (!payload || typeof payload !== "object") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (payload.type === "agent_reasoning" && typeof payload.text === "string") {
|
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)) {
|
if (SHELL_FUNCTION_NAMES.has(name)) {
|
||||||
const args = safeJsonParse<Record<string, unknown>>(payload.arguments);
|
const args = safeJsonParse<Record<string, unknown>>(payload.arguments);
|
||||||
const command = formatCommand(args);
|
const command = formatCommand(args);
|
||||||
const cwd = args && typeof args === "object" && typeof (args as { workdir?: unknown }).workdir === "string"
|
const cwd =
|
||||||
? ((args as { workdir?: unknown }).workdir as string)
|
args &&
|
||||||
: undefined;
|
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") {
|
if (command && typeof payload.call_id === "string") {
|
||||||
commandCalls.set(payload.call_id, { command, cwd });
|
commandCalls.set(payload.call_id, { command, cwd });
|
||||||
events.push({
|
events.push({
|
||||||
@@ -1141,7 +1416,11 @@ function handleRolloutFunctionCall(
|
|||||||
const args = safeJsonParse<{ plan?: unknown }>(payload.arguments);
|
const args = safeJsonParse<{ plan?: unknown }>(payload.arguments);
|
||||||
const planItems = parsePlanItems(args);
|
const planItems = parsePlanItems(args);
|
||||||
if (planItems.length) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1175,16 +1454,19 @@ function finalizeRolloutFunctionCall(
|
|||||||
}
|
}
|
||||||
const result = safeJsonParse<CommandExecutionResult>(payload.output);
|
const result = safeJsonParse<CommandExecutionResult>(payload.output);
|
||||||
const exitCode = result?.metadata?.exit_code;
|
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
|
// Build structured command output
|
||||||
const output = result?.stdout ? {
|
const output = result?.stdout
|
||||||
type: "command" as const,
|
? {
|
||||||
command: command.command,
|
type: "command" as const,
|
||||||
output: result.stdout,
|
command: command.command,
|
||||||
exitCode,
|
output: result.stdout,
|
||||||
cwd: command.cwd,
|
exitCode,
|
||||||
} : result;
|
cwd: command.cwd,
|
||||||
|
}
|
||||||
|
: result;
|
||||||
|
|
||||||
events.push({
|
events.push({
|
||||||
type: "timeline",
|
type: "timeline",
|
||||||
@@ -1203,7 +1485,10 @@ function finalizeRolloutFunctionCall(
|
|||||||
commandCalls.delete(payload.call_id);
|
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") {
|
if (payload?.name === "apply_patch" && typeof payload.input === "string") {
|
||||||
const patchText = payload.input;
|
const patchText = payload.input;
|
||||||
const files = parsePatchFiles(patchText);
|
const files = parsePatchFiles(patchText);
|
||||||
@@ -1339,7 +1624,10 @@ type SessionMetaEntry = {
|
|||||||
payload?: SessionMetaPayload;
|
payload?: SessionMetaPayload;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function collectRecentRolloutFiles(rootDir: string, limit: number): Promise<RolloutCandidate[]> {
|
async function collectRecentRolloutFiles(
|
||||||
|
rootDir: string,
|
||||||
|
limit: number
|
||||||
|
): Promise<RolloutCandidate[]> {
|
||||||
const candidates: RolloutCandidate[] = [];
|
const candidates: RolloutCandidate[] = [];
|
||||||
async function traverse(dir: string): Promise<void> {
|
async function traverse(dir: string): Promise<void> {
|
||||||
let entries: Dirent[];
|
let entries: Dirent[];
|
||||||
@@ -1402,8 +1690,12 @@ async function parseRolloutDescriptor(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!sessionId && isSessionMetaEntry(entry) && entry.payload) {
|
if (!sessionId && isSessionMetaEntry(entry) && entry.payload) {
|
||||||
sessionId = typeof entry.payload.id === "string" ? entry.payload.id : null;
|
sessionId =
|
||||||
if (typeof entry.payload.cwd === "string" && entry.payload.cwd.length > 0) {
|
typeof entry.payload.id === "string" ? entry.payload.id : null;
|
||||||
|
if (
|
||||||
|
typeof entry.payload.cwd === "string" &&
|
||||||
|
entry.payload.cwd.length > 0
|
||||||
|
) {
|
||||||
cwd = entry.payload.cwd;
|
cwd = entry.payload.cwd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1467,7 +1759,8 @@ function extractMessageText(content: unknown): string {
|
|||||||
parts.push(text.trim());
|
parts.push(text.trim());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const message = typeof record.message === "string" ? record.message : undefined;
|
const message =
|
||||||
|
typeof record.message === "string" ? record.message : undefined;
|
||||||
if (message && message.trim()) {
|
if (message && message.trim()) {
|
||||||
parts.push(message.trim());
|
parts.push(message.trim());
|
||||||
}
|
}
|
||||||
@@ -1482,7 +1775,10 @@ export function isSyntheticRolloutUserMessage(text: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lower = normalized.toLowerCase();
|
const lower = normalized.toLowerCase();
|
||||||
if (lower.startsWith("# agents.md instructions for") && lower.includes("<instructions>")) {
|
if (
|
||||||
|
lower.startsWith("# agents.md instructions for") &&
|
||||||
|
lower.includes("<instructions>")
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1510,7 +1806,10 @@ function extractReasoningText(payload: RolloutReasoningPayload): string {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCodexPersistedTimeline(filePath: string, threadId: string): Promise<AgentTimelineItem[]> {
|
async function loadCodexPersistedTimeline(
|
||||||
|
filePath: string,
|
||||||
|
threadId: string
|
||||||
|
): Promise<AgentTimelineItem[]> {
|
||||||
try {
|
try {
|
||||||
const events = await parseRolloutFile(filePath, threadId);
|
const events = await parseRolloutFile(filePath, threadId);
|
||||||
const timeline: AgentTimelineItem[] = [];
|
const timeline: AgentTimelineItem[] = [];
|
||||||
@@ -1525,13 +1824,19 @@ async function loadCodexPersistedTimeline(filePath: string, threadId: string): P
|
|||||||
}
|
}
|
||||||
return timeline;
|
return timeline;
|
||||||
} catch (error) {
|
} 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 [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePlanItems(args: unknown): { text: string; completed: boolean }[] {
|
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 plan = Array.isArray(planValue) ? planValue : [];
|
||||||
const items: { text: string; completed: boolean }[] = [];
|
const items: { text: string; completed: boolean }[] = [];
|
||||||
for (const step of plan) {
|
for (const step of plan) {
|
||||||
@@ -1539,7 +1844,12 @@ function parsePlanItems(args: unknown): { text: string; completed: boolean }[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const record = step as { step?: unknown; text?: unknown; status?: unknown };
|
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) {
|
if (!text) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1600,7 +1910,10 @@ function formatCommand(args: unknown): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCodexExtraString(config: CodexAgentConfig, key: string): string | undefined {
|
function readCodexExtraString(
|
||||||
|
config: CodexAgentConfig,
|
||||||
|
key: string
|
||||||
|
): string | undefined {
|
||||||
const extras = config.extra?.codex;
|
const extras = config.extra?.codex;
|
||||||
if (!extras || typeof extras !== "object") {
|
if (!extras || typeof extras !== "object") {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -1609,7 +1922,10 @@ function readCodexExtraString(config: CodexAgentConfig, key: string): string | u
|
|||||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
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") {
|
if (!handle?.metadata || typeof handle.metadata !== "object") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -1617,8 +1933,12 @@ function readMetadataString(handle: AgentPersistenceHandle | undefined, key: str
|
|||||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeMetadata(metadata: Record<string, unknown>): Record<string, unknown> | undefined {
|
function sanitizeMetadata(
|
||||||
const entries = Object.entries(metadata).filter(([_, value]) => value !== undefined && value !== null);
|
metadata: Record<string, unknown>
|
||||||
|
): Record<string, unknown> | undefined {
|
||||||
|
const entries = Object.entries(metadata).filter(
|
||||||
|
([_, value]) => value !== undefined && value !== null
|
||||||
|
);
|
||||||
if (!entries.length) {
|
if (!entries.length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
AgentPermissionResponse,
|
AgentPermissionResponse,
|
||||||
AgentPersistenceHandle,
|
AgentPersistenceHandle,
|
||||||
AgentProvider,
|
AgentProvider,
|
||||||
|
AgentRuntimeInfo,
|
||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
AgentTimelineItem,
|
AgentTimelineItem,
|
||||||
AgentUsage,
|
AgentUsage,
|
||||||
@@ -214,6 +215,14 @@ const AgentPersistenceHandleSchema: z.ZodType<AgentPersistenceHandle | null> =
|
|||||||
})
|
})
|
||||||
.nullable();
|
.nullable();
|
||||||
|
|
||||||
|
const AgentRuntimeInfoSchema: z.ZodType<AgentRuntimeInfo> = 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({
|
export const AgentSnapshotPayloadSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
provider: AgentProviderSchema,
|
provider: AgentProviderSchema,
|
||||||
@@ -229,6 +238,7 @@ export const AgentSnapshotPayloadSchema = z.object({
|
|||||||
availableModes: z.array(AgentModeSchema),
|
availableModes: z.array(AgentModeSchema),
|
||||||
pendingPermissions: z.array(AgentPermissionRequestPayloadSchema),
|
pendingPermissions: z.array(AgentPermissionRequestPayloadSchema),
|
||||||
persistence: AgentPersistenceHandleSchema.nullable(),
|
persistence: AgentPersistenceHandleSchema.nullable(),
|
||||||
|
runtimeInfo: AgentRuntimeInfoSchema.optional(),
|
||||||
lastUsage: AgentUsageSchema.optional(),
|
lastUsage: AgentUsageSchema.optional(),
|
||||||
lastError: z.string().optional(),
|
lastError: z.string().optional(),
|
||||||
title: z.string().nullable(),
|
title: z.string().nullable(),
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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" })');
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user