diff --git a/.codespellignore b/.codespellignore index 546a19270..d31656406 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1 +1,2 @@ iTerm +trys diff --git a/.codespellrc b/.codespellrc index da831d895..5f9365cb1 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,6 +1,6 @@ [codespell] # Ref: https://github.com/codespell-project/codespell#using-a-config-file -skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt +skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,.codespellignore,*test.ts,*.jsonl,frame*.txt,*/target/*,*/node_modules/*,*/.pnpm-store/*,*/dist/*,*/.yarn/*,package-lock.json,*.map check-hidden = true ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b ignore-words-list = ratatui,ser diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a62bf69f2..970a80721 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,12 +29,13 @@ jobs: node-version: 22 - name: Install dependencies - run: npm_config_optional=false pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile # stage_npm_packages.py requires DotSlash when staging releases. - uses: facebook/install-dotslash@v2 - name: Stage npm package + if: github.repository == 'openai/codex-native' id: stage_npm_package env: GH_TOKEN: ${{ github.token }} @@ -50,6 +51,7 @@ jobs: echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT" - name: Upload staged npm package artifact + if: github.repository == 'openai/codex-native' uses: actions/upload-artifact@v5 with: name: codex-npm-staging diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index ad1676947..eb36b6d6a 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -8,6 +8,10 @@ on: pull_request: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read diff --git a/.github/workflows/issue-deduplicator.yml b/.github/workflows/issue-deduplicator.yml index c36857cab..f13856bdc 100644 --- a/.github/workflows/issue-deduplicator.yml +++ b/.github/workflows/issue-deduplicator.yml @@ -6,6 +6,10 @@ on: - opened - labeled +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + jobs: gather-duplicates: name: Identify potential duplicates diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml index 82ffb8391..d0b096b2b 100644 --- a/.github/workflows/issue-labeler.yml +++ b/.github/workflows/issue-labeler.yml @@ -6,6 +6,10 @@ on: - opened - labeled +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + jobs: gather-labels: name: Generate label suggestions diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 103e8cf6b..6a6df7754 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -6,6 +6,10 @@ on: - main workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + # CI builds in debug (dev) for faster signal. jobs: diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 10f7102ac..da74797b7 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -29,19 +29,30 @@ jobs: cache: pnpm - uses: dtolnay/rust-toolchain@1.90 + with: + components: rustfmt, clippy - name: build codex run: cargo build --bin codex working-directory: codex-rs - name: Install dependencies - run: npm_config_optional=false pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile - - name: Build SDK packages + - name: Build TypeScript SDK packages run: pnpm -r --filter ./sdk/typescript run build - - name: Lint SDK packages + - name: Build native SDK package + run: pnpm -r --filter ./sdk/native run build + + - name: Lint TypeScript SDK packages run: pnpm -r --filter ./sdk/typescript run lint - - name: Test SDK packages + - name: Lint native SDK package + run: pnpm -r --filter ./sdk/native run lint + + - name: Test TypeScript SDK packages run: pnpm -r --filter ./sdk/typescript run test + + - name: Test native SDK package + run: pnpm -r --filter ./sdk/native run test diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index e418cdf24..26508ecb7 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -6,6 +6,7 @@ use std::time::Duration; use async_trait::async_trait; use codex_protocol::models::ResponseInputItem; +use tracing::error; use tracing::warn; use crate::client_common::tools::ToolSpec; @@ -45,19 +46,34 @@ fn pending_external_tools() -> &'static Mutex> { } pub fn set_pending_external_tools(tools: Vec) { - let mut guard = pending_external_tools() - .lock() - .expect("pending external tools mutex poisoned"); - *guard = tools; + match pending_external_tools().lock() { + Ok(mut guard) => { + *guard = tools; + } + Err(err) => { + error!( + error = ?err, + "failed to acquire pending external tools mutex; pending tools unchanged" + ); + } + } } pub fn take_pending_external_tools() -> Vec { - let mut guard = pending_external_tools() - .lock() - .expect("pending external tools mutex poisoned"); - let tools = guard.clone(); - guard.clear(); - tools + match pending_external_tools().lock() { + Ok(mut guard) => { + let tools = guard.clone(); + guard.clear(); + tools + } + Err(err) => { + error!( + error = ?err, + "failed to acquire pending external tools mutex; returning empty list" + ); + Vec::new() + } + } } #[async_trait] @@ -196,6 +212,12 @@ pub struct ToolRegistryBuilder { specs: Vec, } +impl Default for ToolRegistryBuilder { + fn default() -> Self { + Self::new() + } +} + impl ToolRegistryBuilder { pub fn new() -> Self { Self { @@ -217,6 +239,23 @@ impl ToolRegistryBuilder { .push(ConfiguredToolSpec::new(spec, supports_parallel_tool_calls)); } + pub fn upsert_spec_with_parallel_support( + &mut self, + spec: ToolSpec, + supports_parallel_tool_calls: bool, + ) { + let spec_name = spec.name().to_string(); + if let Some(existing) = self + .specs + .iter_mut() + .find(|configured| configured.spec.name() == spec_name) + { + *existing = ConfiguredToolSpec::new(spec, supports_parallel_tool_calls); + } else { + self.push_spec_with_parallel_support(spec, supports_parallel_tool_calls); + } + } + pub fn register_handler(&mut self, name: impl Into, handler: Arc) { let name = name.into(); if self diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index 310b0c56b..9f050c422 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -38,11 +38,11 @@ impl ToolRouter { ) -> Self { let mut builder = build_specs(config, mcp_tools); for tool in external_tools { - builder.push_spec_with_parallel_support( - tool.spec.clone(), - tool.supports_parallel_tool_calls, - ); - builder.register_handler(tool.spec.name(), Arc::clone(&tool.handler)); + let spec = tool.spec.clone(); + let supports_parallel = tool.supports_parallel_tool_calls; + let spec_name = spec.name().to_string(); + builder.upsert_spec_with_parallel_support(spec, supports_parallel); + builder.register_handler(spec_name, Arc::clone(&tool.handler)); } let (specs, registry) = builder.build(); @@ -196,3 +196,90 @@ impl ToolRouter { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::client_common::tools::ToolSpec; + use crate::features::Features; + use crate::model_family::find_family_for_model; + use crate::tools::context::ToolOutput; + use crate::tools::registry::ExternalToolRegistration; + use crate::tools::registry::ToolHandler; + use crate::tools::registry::ToolKind; + use crate::tools::spec::ToolsConfigParams; + use crate::tools::spec::create_function_tool_spec_from_schema; + use async_trait::async_trait; + use serde_json::json; + use std::sync::Arc; + + struct DummyHandler; + + #[async_trait] + impl ToolHandler for DummyHandler { + fn kind(&self) -> ToolKind { + ToolKind::Function + } + + async fn handle( + &self, + _invocation: ToolInvocation, + ) -> Result { + Err(FunctionCallError::Fatal("not implemented".to_string())) + } + } + + #[test] + fn external_tool_can_override_builtin_spec() { + let model_family = find_family_for_model("codex-mini-latest") + .expect("valid model family for codex-mini-latest"); + let features = Features::with_defaults(); + let config = ToolsConfig::new(&ToolsConfigParams { + model_family: &model_family, + features: &features, + }); + + let custom_spec = create_function_tool_spec_from_schema( + "list_mcp_resources".to_string(), + Some("custom override".to_string()), + json!({ + "type": "object", + "properties": { + "hint": { "type": "string" } + }, + "required": ["hint"], + "additionalProperties": false + }), + true, + ) + .expect("schema converts"); + + let external_tools = vec![ExternalToolRegistration { + spec: custom_spec, + handler: Arc::new(DummyHandler), + supports_parallel_tool_calls: false, + }]; + + let router = ToolRouter::from_config(&config, None, &external_tools); + let specs = router.specs(); + + let overrides: Vec<_> = specs + .iter() + .filter(|spec| spec.name() == "list_mcp_resources") + .collect(); + assert_eq!(overrides.len(), 1, "override should replace built-in spec"); + + match overrides[0] { + ToolSpec::Function(tool) => { + assert_eq!(tool.description, "custom override"); + assert!(tool.strict); + } + _ => panic!("expected function tool"), + } + + assert!( + !router.tool_supports_parallel("list_mcp_resources"), + "override should control parallel flag" + ); + } +} diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output.rs b/codex-rs/exec/src/event_processor_with_jsonl_output.rs index e302dd53d..69631b853 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output.rs @@ -20,6 +20,7 @@ use crate::exec_events::McpToolCallItemResult; use crate::exec_events::McpToolCallStatus; use crate::exec_events::PatchApplyStatus; use crate::exec_events::PatchChangeKind; +use crate::exec_events::RawEvent; use crate::exec_events::ReasoningItem; use crate::exec_events::ThreadErrorEvent; use crate::exec_events::ThreadEvent; @@ -107,7 +108,7 @@ impl EventProcessorWithJsonOutput { } pub fn collect_thread_events(&mut self, event: &Event) -> Vec { - match &event.msg { + let mut events = match &event.msg { EventMsg::SessionConfigured(ev) => self.handle_session_configured(ev), EventMsg::AgentMessage(ev) => self.handle_agent_message(ev), EventMsg::AgentReasoning(ev) => self.handle_reasoning_event(ev), @@ -148,7 +149,15 @@ impl EventProcessorWithJsonOutput { })], EventMsg::PlanUpdate(ev) => self.handle_plan_update(ev), _ => Vec::new(), + }; + + if events.is_empty() + && let Ok(raw) = serde_json::to_value(&event.msg) + { + events.push(ThreadEvent::Raw(RawEvent { raw })); } + + events } fn get_next_item_id(&self) -> String { diff --git a/codex-rs/exec/src/exec_events.rs b/codex-rs/exec/src/exec_events.rs index 64288e138..c63d77188 100644 --- a/codex-rs/exec/src/exec_events.rs +++ b/codex-rs/exec/src/exec_events.rs @@ -33,6 +33,12 @@ pub enum ThreadEvent { /// Represents an unrecoverable error emitted directly by the event stream. #[serde(rename = "error")] Error(ThreadErrorEvent), + /// Signals that Codex exited review mode and optionally provides structured output. + #[serde(rename = "exited_review_mode")] + ExitedReviewMode(ExitedReviewModeEvent), + /// Raw protocol event payload forwarded for consumers that need full fidelity. + #[serde(rename = "raw_event")] + Raw(RawEvent), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] @@ -87,6 +93,47 @@ pub struct ThreadErrorEvent { pub message: String, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] +pub struct ReviewFinding { + pub title: String, + pub body: String, + pub confidence_score: f32, + pub priority: i32, + pub code_location: ReviewCodeLocation, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] +pub struct ReviewCodeLocation { + pub absolute_file_path: String, + pub line_range: ReviewLineRange, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] +pub struct ReviewLineRange { + pub start: i32, + pub end: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] +pub struct ReviewOutputEvent { + pub findings: Vec, + pub overall_correctness: String, + pub overall_explanation: String, + pub overall_confidence_score: f32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] +pub struct ExitedReviewModeEvent { + #[serde(default)] + pub review_output: Option, +} + +/// Raw protocol event payload as emitted by codex-core. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] +pub struct RawEvent { + pub raw: JsonValue, +} + /// Canonical representation of a thread item and its domain-specific payload. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] pub struct ThreadItem { diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 4c1e0c77b..71f984c63 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -28,6 +28,7 @@ use codex_exec::exec_events::McpToolCallItemResult; use codex_exec::exec_events::McpToolCallStatus; use codex_exec::exec_events::PatchApplyStatus; use codex_exec::exec_events::PatchChangeKind; +use codex_exec::exec_events::RawEvent; use codex_exec::exec_events::ReasoningItem; use codex_exec::exec_events::ThreadErrorEvent; use codex_exec::exec_events::ThreadEvent; @@ -753,7 +754,14 @@ fn exec_command_end_without_begin_is_ignored() { }), ); let out = ep.collect_thread_events(&end_only); - assert!(out.is_empty()); + assert_eq!(out.len(), 1); + match &out[0] { + ThreadEvent::Raw(raw) => { + assert_eq!(raw.raw["type"], "exec_command_end"); + assert_eq!(raw.raw["call_id"], "no-begin"); + } + other => panic!("expected raw event, got {other:?}"), + } } #[test] @@ -782,7 +790,7 @@ fn patch_apply_success_produces_item_completed_patchapply() { }, ); - // Begin -> no output + // Begin -> raw event forwarded let begin = event( "p1", EventMsg::PatchApplyBegin(PatchApplyBeginEvent { @@ -792,7 +800,11 @@ fn patch_apply_success_produces_item_completed_patchapply() { }), ); let out_begin = ep.collect_thread_events(&begin); - assert!(out_begin.is_empty()); + assert_eq!(out_begin.len(), 1); + match &out_begin[0] { + ThreadEvent::Raw(raw) => assert_eq!(raw.raw["type"], "patch_apply_begin"), + other => panic!("expected raw event, got {other:?}"), + } // End (success) -> item.completed (item_0) let end = event( @@ -860,7 +872,12 @@ fn patch_apply_failure_produces_item_completed_patchapply_failed() { changes: changes.clone(), }), ); - assert!(ep.collect_thread_events(&begin).is_empty()); + let begin_events = ep.collect_thread_events(&begin); + assert_eq!(begin_events.len(), 1); + match &begin_events[0] { + ThreadEvent::Raw(raw) => assert_eq!(raw.raw["type"], "patch_apply_begin"), + other => panic!("expected raw event, got {other:?}"), + } // End (failure) -> item.completed (item_0) with Failed status let end = event( @@ -916,7 +933,33 @@ fn task_complete_produces_turn_completed_with_usage() { rate_limits: None, }), ); - assert!(ep.collect_thread_events(&token_count_event).is_empty()); + let token_events = ep.collect_thread_events(&token_count_event); + assert_eq!( + token_events, + vec![ThreadEvent::Raw(RawEvent { + raw: json!({ + "type": "token_count", + "info": { + "total_token_usage": { + "input_tokens": 1200, + "cached_input_tokens": 200, + "output_tokens": 345, + "reasoning_output_tokens": 0, + "total_tokens": 0, + }, + "last_token_usage": { + "input_tokens": 1200, + "cached_input_tokens": 200, + "output_tokens": 345, + "reasoning_output_tokens": 0, + "total_tokens": 0, + }, + "model_context_window": null, + }, + "rate_limits": null, + }) + })] + ); // Then TaskComplete should produce turn.completed with the captured usage. let complete_event = event( diff --git a/codex-rs/justfile b/codex-rs/justfile index 9ddc4a37a..78bf5518a 100644 --- a/codex-rs/justfile +++ b/codex-rs/justfile @@ -23,7 +23,7 @@ file-search *args: # format code fmt: - cargo fmt -- --config imports_granularity=Item + cargo +nightly fmt -- --unstable-features --config imports_granularity=Item fix *args: cargo clippy --fix --all-features --tests --allow-dirty "$@" diff --git a/package.json b/package.json index 09bf222fb..2a70b8349 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "private": true, "description": "Tools for repo-wide maintenance.", "scripts": { + "preinstall": "node -e \"if (!/pnpm/.test(process.env.npm_execpath||'') && !/pnpm/.test(process.env.npm_config_user_agent||'')) { console.error('This repository uses PNPM. Please run: pnpm install'); process.exit(1) }\"", "format": "prettier --check *.json *.md docs/*.md .github/workflows/*.yml **/*.js", "format:fix": "prettier --write *.json *.md docs/*.md .github/workflows/*.yml **/*.js", - "sdk:build": "pnpm --filter @codex-native/sdk run build", + "sdk:build": "node sdk/native/scripts/build-all.mjs", "sdk:test": "pnpm --filter @codex-native/sdk run test", "sdk:publish-platforms": "pnpm --filter @codex-native/sdk run publish:platforms", "sdk:release": "pnpm --filter @codex-native/sdk run release", @@ -15,7 +16,7 @@ "sdk:release:major": "pnpm --filter @codex-native/sdk run release:major" }, "devDependencies": { - "@napi-rs/cli": "^3.4.1", + "@napi-rs/cli": "^2.18.4", "prettier": "^3.5.3" }, "resolutions": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fec3e10d..9a20a87d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,8 +14,8 @@ importers: .: devDependencies: '@napi-rs/cli': - specifier: ^3.4.1 - version: 3.4.1(@emnapi/runtime@1.7.0)(@types/node@22.19.0) + specifier: ^2.18.4 + version: 2.18.4 prettier: specifier: ^3.5.3 version: 3.5.3 @@ -36,7 +36,7 @@ importers: version: 22.19.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)) + version: 29.7.0(@types/node@22.19.0) tsup: specifier: ^8.5.0 version: 8.5.0(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1) @@ -103,10 +103,10 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.3.4 - version: 29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2) + version: 29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2) ts-jest-mock-import-meta: specifier: ^1.3.1 - version: 1.3.1(ts-jest@29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2)) + version: 1.3.1(ts-jest@29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.19.18)(typescript@5.9.2) @@ -799,6 +799,11 @@ packages: resolution: {integrity: sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==} engines: {node: '>=18'} + '@napi-rs/cli@2.18.4': + resolution: {integrity: sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==} + engines: {node: '>= 10'} + hasBin: true + '@napi-rs/cli@3.4.1': resolution: {integrity: sha512-ayhm+NfrP5Hmh7vy5pfyYm/ktYtLh2PrgdLuqHTAubO7RoO2JkUE4F991AtgYxNewwXI8+guZLxU8itV7QnDrQ==} engines: {node: '>= 16'} @@ -3716,7 +3721,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2))': + '@jest/core@29.7.0': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -3730,7 +3735,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) + jest-config: 29.7.0(@types/node@22.19.0) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3751,7 +3756,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -3765,7 +3770,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)) + jest-config: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3945,6 +3950,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@napi-rs/cli@2.18.4': {} + '@napi-rs/cli@3.4.1(@emnapi/runtime@1.7.0)(@types/node@22.19.0)': dependencies: '@inquirer/prompts': 7.9.0(@types/node@22.19.0) @@ -4827,13 +4834,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)): + create-jest@29.7.0(@types/node@22.19.0): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)) + jest-config: 29.7.0(@types/node@22.19.0) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -5425,16 +5432,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)): + jest-cli@29.7.0(@types/node@22.19.0): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)) + create-jest: 29.7.0(@types/node@22.19.0) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)) + jest-config: 29.7.0(@types/node@22.19.0) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -5475,7 +5482,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)): + jest-config@29.7.0(@types/node@22.19.0): dependencies: '@babel/core': 7.28.4 '@jest/test-sequencer': 29.7.0 @@ -5501,12 +5508,11 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.19.0 - ts-node: 10.9.2(@types/node@20.19.18)(typescript@5.9.2) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)): + jest-config@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)): dependencies: '@babel/core': 7.28.4 '@jest/test-sequencer': 29.7.0 @@ -5532,7 +5538,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.19.0 - ts-node: 10.9.2(@types/node@22.19.0)(typescript@5.9.2) + ts-node: 10.9.2(@types/node@20.19.18)(typescript@5.9.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -5764,12 +5770,12 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)): + jest@29.7.0(@types/node@22.19.0): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)) + '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2)) + jest-cli: 29.7.0(@types/node@22.19.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -6337,11 +6343,11 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest-mock-import-meta@1.3.1(ts-jest@29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2)): + ts-jest-mock-import-meta@1.3.1(ts-jest@29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2)): dependencies: - ts-jest: 29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2) + ts-jest: 29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2) - ts-jest@29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2): + ts-jest@29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -6359,6 +6365,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.28.4) + esbuild: 0.25.10 jest-util: 29.7.0 ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2): @@ -6379,25 +6386,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.2): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.19.0 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.2 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optional: true - tslib@2.8.1: optional: true diff --git a/renovate.json b/renovate.json index 5db72dd6a..22a994324 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,4 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ] + "extends": ["config:recommended"] } diff --git a/sdk/native/.cargo/config.toml b/sdk/native/.cargo/config.toml deleted file mode 100644 index ac2b23f81..000000000 --- a/sdk/native/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.x86_64-pc-windows-msvc] -rustflags = ["-C", "target-feature=+crt-static"] diff --git a/sdk/native/.github/renovate.json b/sdk/native/.github/renovate.json deleted file mode 100644 index cc26fc165..000000000 --- a/sdk/native/.github/renovate.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base", "group:allNonMajor", ":preserveSemverRanges", ":disablePeerDependencies"], - "labels": ["dependencies"], - "packageRules": [ - { - "matchPackageNames": ["@napi/cli", "napi", "napi-build", "napi-derive"], - "addLabels": ["napi-rs"], - "groupName": "napi-rs" - }, - { - "matchPackagePatterns": ["^eslint", "^@typescript-eslint"], - "groupName": "linter" - } - ], - "commitMessagePrefix": "chore: ", - "commitMessageAction": "bump up", - "commitMessageTopic": "{{depName}} version", - "ignoreDeps": [] -} diff --git a/sdk/native/.github/workflows/CI.yml b/sdk/native/.github/workflows/CI.yml deleted file mode 100644 index 931bb038d..000000000 --- a/sdk/native/.github/workflows/CI.yml +++ /dev/null @@ -1,384 +0,0 @@ -name: CI -env: - DEBUG: napi:* - APP_NAME: package-template - MACOSX_DEPLOYMENT_TARGET: '10.13' - CARGO_INCREMENTAL: '1' -'on': - push: - branches: - - main - tags-ignore: - - '**' - paths-ignore: - - '**/*.md' - - LICENSE - - '**/*.gitignore' - - .editorconfig - - docs/** - pull_request: null -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - - name: Setup node - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: 'yarn' - - - name: Install - uses: dtolnay/rust-toolchain@stable - with: - components: clippy, rustfmt - - - name: Install dependencies - run: yarn install - - - name: Oxlint - run: yarn lint - - - name: Cargo fmt - run: cargo fmt -- --check - - - name: Clippy - run: cargo clippy - build: - strategy: - fail-fast: false - matrix: - settings: - - host: macos-latest - target: x86_64-apple-darwin - build: yarn build --target x86_64-apple-darwin - - host: windows-latest - build: yarn build --target x86_64-pc-windows-msvc - target: x86_64-pc-windows-msvc - - host: windows-latest - build: yarn build --target i686-pc-windows-msvc - target: i686-pc-windows-msvc - - host: ubuntu-latest - target: x86_64-unknown-linux-gnu - build: yarn build --target x86_64-unknown-linux-gnu --use-napi-cross - - host: ubuntu-latest - target: x86_64-unknown-linux-musl - build: yarn build --target x86_64-unknown-linux-musl -x - - host: macos-latest - target: aarch64-apple-darwin - build: yarn build --target aarch64-apple-darwin - - host: ubuntu-latest - target: aarch64-unknown-linux-gnu - build: yarn build --target aarch64-unknown-linux-gnu --use-napi-cross - - host: ubuntu-latest - target: armv7-unknown-linux-gnueabihf - build: yarn build --target armv7-unknown-linux-gnueabihf --use-napi-cross - - host: ubuntu-latest - target: aarch64-linux-android - build: yarn build --target aarch64-linux-android - - host: ubuntu-latest - target: armv7-linux-androideabi - build: yarn build --target armv7-linux-androideabi - - host: ubuntu-latest - target: aarch64-unknown-linux-musl - build: yarn build --target aarch64-unknown-linux-musl -x - - host: windows-latest - target: aarch64-pc-windows-msvc - build: yarn build --target aarch64-pc-windows-msvc - - host: ubuntu-latest - target: wasm32-wasip1-threads - build: yarn build --target wasm32-wasip1-threads - name: stable - ${{ matrix.settings.target }} - node@22 - runs-on: ${{ matrix.settings.host }} - steps: - - uses: actions/checkout@v5 - - name: Setup node - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: yarn - - name: Install - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - targets: ${{ matrix.settings.target }} - - name: Cache cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - ~/.napi-rs - .cargo-cache - target/ - key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} - - uses: mlugg/setup-zig@v2 - if: ${{ contains(matrix.settings.target, 'musl') }} - with: - version: 0.14.1 - - name: Install cargo-zigbuild - uses: taiki-e/install-action@v2 - if: ${{ contains(matrix.settings.target, 'musl') }} - env: - GITHUB_TOKEN: ${{ github.token }} - with: - tool: cargo-zigbuild - - name: Setup toolchain - run: ${{ matrix.settings.setup }} - if: ${{ matrix.settings.setup }} - shell: bash - - name: Install dependencies - run: yarn install - - name: Build - run: ${{ matrix.settings.build }} - shell: bash - - name: Upload artifact - uses: actions/upload-artifact@v5 - with: - name: bindings-${{ matrix.settings.target }} - path: | - ${{ env.APP_NAME }}.*.node - ${{ env.APP_NAME }}.*.wasm - if-no-files-found: error - build-freebsd: - runs-on: ubuntu-latest - name: Build FreeBSD - steps: - - uses: actions/checkout@v5 - - name: Build - id: build - uses: cross-platform-actions/action@v0.29.0 - env: - DEBUG: napi:* - RUSTUP_IO_THREADS: 1 - with: - operating_system: freebsd - version: '14.2' - memory: 8G - cpu_count: 3 - environment_variables: 'DEBUG RUSTUP_IO_THREADS' - shell: bash - run: | - sudo pkg install -y -f curl node libnghttp2 npm cmake - sudo npm install -g yarn --ignore-scripts - curl https://sh.rustup.rs -sSf --output rustup.sh - sh rustup.sh -y --profile minimal --default-toolchain stable - source "$HOME/.cargo/env" - echo "~~~~ rustc --version ~~~~" - rustc --version - echo "~~~~ node -v ~~~~" - node -v - echo "~~~~ yarn --version ~~~~" - yarn --version - pwd - ls -lah - whoami - env - freebsd-version - yarn install - yarn build - rm -rf node_modules - rm -rf target - rm -rf .yarn/cache - - name: Upload artifact - uses: actions/upload-artifact@v5 - with: - name: bindings-freebsd - path: ${{ env.APP_NAME }}.*.node - if-no-files-found: error - test-macOS-windows-binding: - name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} - needs: - - build - strategy: - fail-fast: false - matrix: - settings: - - host: windows-latest - target: x86_64-pc-windows-msvc - architecture: x64 - - host: windows-11-arm - target: aarch64-pc-windows-msvc - architecture: arm64 - - host: macos-latest - target: aarch64-apple-darwin - architecture: arm64 - - host: macos-latest - target: x86_64-apple-darwin - architecture: x64 - node: - - '20' - - '22' - runs-on: ${{ matrix.settings.host }} - steps: - - uses: actions/checkout@v5 - - name: Setup node - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node }} - cache: yarn - architecture: ${{ matrix.settings.architecture }} - - name: Install dependencies - run: yarn install - - name: Download artifacts - uses: actions/download-artifact@v6 - with: - name: bindings-${{ matrix.settings.target }} - path: . - - name: List packages - run: ls -R . - shell: bash - - name: Test bindings - run: yarn test - test-linux-binding: - name: Test ${{ matrix.target }} - node@${{ matrix.node }} - needs: - - build - strategy: - fail-fast: false - matrix: - target: - - x86_64-unknown-linux-gnu - - x86_64-unknown-linux-musl - - aarch64-unknown-linux-gnu - - aarch64-unknown-linux-musl - - armv7-unknown-linux-gnueabihf - node: - - '20' - - '22' - runs-on: ${{ contains(matrix.target, 'aarch64') && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - steps: - - uses: actions/checkout@v5 - - name: Setup node - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node }} - cache: yarn - - name: Output docker params - id: docker - run: | - node -e " - if ('${{ matrix.target }}'.startsWith('aarch64')) { - console.log('PLATFORM=linux/arm64') - } else if ('${{ matrix.target }}'.startsWith('armv7')) { - console.log('PLATFORM=linux/arm/v7') - } else { - console.log('PLATFORM=linux/amd64') - } - " >> $GITHUB_OUTPUT - node -e " - if ('${{ matrix.target }}'.endsWith('-musl')) { - console.log('IMAGE=node:${{ matrix.node }}-alpine') - } else { - console.log('IMAGE=node:${{ matrix.node }}-slim') - } - " >> $GITHUB_OUTPUT - - name: Install dependencies - run: | - yarn config set --json supportedArchitectures.cpu '["current", "arm64", "x64", "arm"]' - yarn config set --json supportedArchitectures.libc '["current", "musl", "gnu"]' - yarn install - - name: Download artifacts - uses: actions/download-artifact@v6 - with: - name: bindings-${{ matrix.target }} - path: . - - name: List packages - run: ls -R . - shell: bash - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - if: ${{ contains(matrix.target, 'armv7') }} - with: - platforms: all - - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - if: ${{ contains(matrix.target, 'armv7') }} - - name: Test bindings - uses: addnab/docker-run-action@v3 - with: - image: ${{ steps.docker.outputs.IMAGE }} - options: -v ${{ github.workspace }}:${{ github.workspace }} -w ${{ github.workspace }} --platform ${{ steps.docker.outputs.PLATFORM }} - run: yarn test - test-wasi: - name: Test WASI target - needs: - - build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Setup node - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: yarn - - name: Install dependencies - run: | - yarn config set supportedArchitectures.cpu "wasm32" - yarn install - - name: Download artifacts - uses: actions/download-artifact@v6 - with: - name: bindings-wasm32-wasip1-threads - path: . - - name: List packages - run: ls -R . - shell: bash - - name: Test bindings - run: yarn test - env: - NAPI_RS_FORCE_WASI: 1 - publish: - name: Publish - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - needs: - - lint - - build-freebsd - - test-macOS-windows-binding - - test-linux-binding - - test-wasi - steps: - - uses: actions/checkout@v5 - - name: Setup node - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: yarn - - name: Install dependencies - run: yarn install - - name: create npm dirs - run: yarn napi create-npm-dirs - - name: Download all artifacts - uses: actions/download-artifact@v6 - with: - path: artifacts - - name: Move artifacts - run: yarn artifacts - - name: List packages - run: ls -R ./npm - shell: bash - - name: Publish - run: | - npm config set provenance true - if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$"; - then - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - npm publish --access public - elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; - then - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - npm publish --tag next --access public - else - echo "Not a release, skipping publish" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/sdk/native/.npmrc b/sdk/native/.npmrc new file mode 100644 index 000000000..86b4537c0 --- /dev/null +++ b/sdk/native/.npmrc @@ -0,0 +1,3 @@ +# Force PNPM usage in this package +enable-pre-post-scripts=true + diff --git a/sdk/native/.yarnrc.yml b/sdk/native/.yarnrc.yml deleted file mode 100644 index 260713f2a..000000000 --- a/sdk/native/.yarnrc.yml +++ /dev/null @@ -1,5 +0,0 @@ -nodeLinker: node-modules - -npmAuditRegistry: "https://registry.npmjs.org" - -yarnPath: .yarn/releases/yarn-4.10.3.cjs diff --git a/sdk/native/AGENTS.md b/sdk/native/AGENTS.md index 8a23e6976..841d924a9 100644 --- a/sdk/native/AGENTS.md +++ b/sdk/native/AGENTS.md @@ -16,8 +16,7 @@ import { Agent, Runner } from '@openai/agents'; // Create the provider const provider = new CodexProvider({ - apiKey: process.env.CODEX_API_KEY, - defaultModel: 'claude-sonnet-4.5' + defaultModel: 'gpt-5-codex' }); // Create an agent @@ -143,17 +142,18 @@ interface CodexProviderOptions { const provider = new CodexProvider({ workingDirectory: process.cwd(), skipGitRepoCheck: true, - defaultModel: 'claude-sonnet-4.5' + defaultModel: 'gpt-5-codex' }); ``` **Production:** ```typescript const provider = new CodexProvider({ - apiKey: process.env.CODEX_API_KEY, baseUrl: 'https://api.codex.example.com', + defaultModel: 'gpt-5-codex', workingDirectory: '/app', - skipGitRepoCheck: false + skipGitRepoCheck: false, + // apiKey: '...' // Optional: only needed if your deployment enforces explicit credentials }); ``` @@ -207,7 +207,7 @@ console.log(review.finalOutput); ### Streaming Progress ```typescript -const model = provider.getModel('claude-sonnet-4.5'); +const model = provider.getModel('gpt-5-codex'); const stream = model.getStreamedResponse({ systemInstructions: 'You are a coding assistant', diff --git a/sdk/native/Cargo.toml b/sdk/native/Cargo.toml index 04e37c15e..926131e01 100644 --- a/sdk/native/Cargo.toml +++ b/sdk/native/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" publish = false [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] path = "rust-bindings/lib.rs" [dependencies] @@ -36,9 +36,16 @@ path = "../../codex-rs/core" [dependencies.codex-exec] path = "../../codex-rs/exec" +[dependencies.codex-protocol] +path = "../../codex-rs/protocol" + [build-dependencies] napi-build = "2" +[dev-dependencies] +pretty_assertions = "1" +uuid = { version = "1", features = ["v4"] } + [profile.release] lto = true strip = "symbols" diff --git a/sdk/native/README.md b/sdk/native/README.md index 8167fa207..f8f8571ef 100644 --- a/sdk/native/README.md +++ b/sdk/native/README.md @@ -105,6 +105,53 @@ const thread = codex.resumeThread(savedThreadId); await thread.run("Implement the fix"); ``` +### Running code reviews + +Invoke the native review workflow without crafting prompts manually. The SDK provides presets that mirror the `/review` slash command: + +```typescript +const codex = new Codex(); + +// Review everything that is staged, unstaged, or untracked +const review = await codex.review({ + target: { type: "current_changes" }, +}); + +for (const finding of review.items) { + if (finding.type === "agent_message") { + console.log(finding.text); + } +} +``` + +Additional presets let you review against another branch or a specific commit: + +```typescript +await codex.review({ + target: { type: "branch", baseBranch: "main" }, +}); + +await codex.review({ + target: { + type: "commit", + sha: "abc1234def5678", + subject: "Tighten input validation", + }, +}); +``` + +For bespoke instructions, pass a custom prompt and optional hint: + +```typescript +await codex.review({ + target: { + type: "custom", + prompt: "Review only the data-access layer for regression risks.", + hint: "data-access layer", + }, +}); +``` + ### Working directory controls Codex runs in the current working directory by default. To avoid unrecoverable errors, Codex requires the working directory to be a Git repository. You can skip the Git repository check by passing the `skipGitRepoCheck` option when creating a thread. @@ -124,6 +171,134 @@ The Native SDK provides additional capabilities beyond the TypeScript SDK: Register JavaScript functions as tools that Codex can discover and invoke during execution. Tools are registered globally on the `Codex` instance and become available to all threads and agents. +> **Override built-ins:** If you register a tool whose `name` matches one of Codex's built-in tools (for example `read_file`, `local_shell`, or `apply_patch`), the native implementation is replaced for the lifetime of that `Codex` instance. This lets you customize or disable default behaviors while keeping the same tool interface. + +#### Built-in tool override cheat sheet + +All snippets assume you already created an instance with `const codex = new Codex();`. Registering any of these names swaps out Codex's default implementation. + +- `shell` – sandboxed shell command runner (models without unified exec) + ```typescript + codex.registerTool({ + name: "shell", + handler: () => ({ error: "Shell disabled by policy", success: false }), + }); + ``` + +- `exec_command` – streaming command execution (available when unified exec is enabled) + ```typescript + codex.registerTool({ + name: "exec_command", + handler: (_, invocation) => ({ + output: `Pretend ran: ${invocation.arguments}`, + success: true, + }), + }); + ``` + +- `write_stdin` – feeds additional input into an in-flight `exec_command` + ```typescript + codex.registerTool({ + name: "write_stdin", + handler: () => ({ output: "stdin blocked", success: false }), + }); + ``` + +- `local_shell` – simplified shell command helper (models that prefer local shell over unified exec) + ```typescript + codex.registerTool({ + name: "local_shell", + handler: () => ({ output: "local shell override", success: true }), + }); + ``` + +- `list_mcp_resources`, `list_mcp_resource_templates`, `read_mcp_resource` – MCP discovery helpers + ```typescript + for (const name of [ + "list_mcp_resources", + "list_mcp_resource_templates", + "read_mcp_resource", + ]) { + codex.registerTool({ + name, + handler: () => ({ output: JSON.stringify({ notice: `${name} overridden` }) }), + }); + } + ``` + +- `update_plan` – emits high-level plan updates back to the host UI + ```typescript + codex.registerTool({ + name: "update_plan", + handler: () => ({ output: "Plan updates disabled" }), + }); + ``` + +- `apply_patch` – applies patches authored by the agent + ```typescript + codex.registerTool({ + name: "apply_patch", + handler: (_, { arguments }) => ({ + output: `Custom patch handler received: ${arguments}`, + success: true, + }), + }); + ``` + +- `web_search` – performs outbound web searches (only on models with the feature enabled) + ```typescript + codex.registerTool({ + name: "web_search", + handler: (_, { arguments }) => ({ + output: `Search stub: ${arguments}`, + success: true, + }), + }); + ``` + +- `view_image` – attaches a local image for the model to inspect + ```typescript + codex.registerTool({ + name: "view_image", + handler: (_, { arguments }) => ({ + output: `Ignoring image path ${arguments}`, + success: true, + }), + }); + ``` + +- `grep_files`, `read_file`, `list_dir` – workspace inspection helpers (enabled via experimental flags) + ```typescript + for (const name of ["grep_files", "read_file", "list_dir"]) { + codex.registerTool({ + name, + handler: (_, { arguments }) => ({ + output: JSON.stringify({ name, arguments, overridden: true }), + success: true, + }), + }); + } + ``` + +- `test_sync_tool` – synchronization helper used in concurrency tests + ```typescript + codex.registerTool({ + name: "test_sync_tool", + handler: () => ({ output: "Barrier skipped" }), + }); + ``` + +- MCP server tools – any name of the form `server::tool` + ```typescript + codex.registerTool({ + name: "jira::create_issue", + handler: (_, { arguments }) => ({ + output: `Custom Jira integration received ${arguments}`, + success: true, + }), + }); + ``` + ```typescript const codex = new Codex(); @@ -244,7 +419,7 @@ Agents automatically have access to the conversation history, enabling seamless ```typescript interface CodexOptions { - apiKey?: string; // Responses API key (defaults to ANTHROPIC_API_KEY env var) + apiKey?: string; // Responses API key (defaults to OPENAI_API_KEY env var) baseUrl?: string; // API base URL override skipGitRepoCheck?: boolean; // Skip Git repository validation } @@ -254,7 +429,7 @@ interface CodexOptions { ```typescript interface ThreadOptions { - model?: string; // Model to use (e.g., "claude-sonnet-4") + model?: string; // Model to use (e.g., "gpt-5-codex") sandboxMode?: "read-only" | "workspace-write" | "danger-full-access"; workingDirectory?: string; // Directory to run Codex in skipGitRepoCheck?: boolean; // Skip Git repository validation diff --git a/sdk/native/build.rs b/sdk/native/build.rs index ce054a990..812298d63 100644 --- a/sdk/native/build.rs +++ b/sdk/native/build.rs @@ -47,10 +47,7 @@ fn build_linux_sandbox() -> Result<(), Box> { "codex-linux-sandbox" }; - let built_path = target_dir - .join(&target) - .join("release") - .join(binary_name); + let built_path = target_dir.join(&target).join("release").join(binary_name); if !built_path.exists() { return Err(format!("expected sandbox binary at {built_path:?}").into()); diff --git a/sdk/native/dist-examples/examples/agents-integration.js b/sdk/native/dist-examples/examples/agents-integration.js new file mode 100644 index 000000000..a27f53856 --- /dev/null +++ b/sdk/native/dist-examples/examples/agents-integration.js @@ -0,0 +1,177 @@ +"use strict"; +/** + * Example: Using CodexProvider with OpenAI Agents framework + * + * This example demonstrates how to use the Codex SDK as a model provider + * for the OpenAI Agents JS framework, enabling powerful multi-agent workflows + * with Codex's coding capabilities. + * + * Installation: + * ```bash + * npm install @codex-native/sdk @openai/agents + * ``` + * + * Usage: + * ```bash + * npx tsx examples/agents-integration.ts + * ``` + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.main = main; +var index_ts_1 = require("../src/index.ts"); +function main() { + return __awaiter(this, void 0, void 0, function () { + var provider, codingAssistant, testRunner, model, response, _i, _a, item, error_1; + var _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + console.log("🚀 Codex Provider for OpenAI Agents\n"); + provider = new index_ts_1.CodexProvider({ + defaultModel: "gpt-5", + workingDirectory: process.cwd(), + skipGitRepoCheck: true, // For example purposes + }); + console.log("✓ Created CodexProvider with Codex backend\n"); + codingAssistant = { + name: "CodingAssistant", + instructions: "You are an expert coding assistant. You help developers:\n- Fix bugs and errors in their code\n- Write tests for their functions\n- Refactor code for better quality\n- Explain complex code sections\n\nYou have access to the full file system and can execute commands to verify your work.", + }; + testRunner = { + name: "TestRunner", + instructions: "You are a test execution specialist. Your job is to:\n- Run test suites and analyze results\n- Identify failing tests and their causes\n- Suggest fixes for test failures\n- Verify that fixes resolve the issues", + }; + console.log("✓ Defined agents: CodingAssistant, TestRunner\n"); + // ============================================================================ + // Step 3: Run agent workflows + // ============================================================================ + console.log("Example 1: Single agent task"); + console.log("─".repeat(60)); + // This would work with the actual Runner from @openai/agents + // const runner = new Runner({ modelProvider: provider }); + // + // const result = await runner.run( + // codingAssistant, + // "Review the test files and fix any failing tests" + // ); + // + // console.log(result.finalOutput); + console.log("\nInput: \"Review the test files and fix any failing tests\"\nAgent: CodingAssistant\nModel: Codex (via CodexProvider)\n\nExpected output:\n- Codex would execute: pnpm test\n- Analyze test failures\n- Make code changes to fix failures\n- Re-run tests to verify\n- Report results\n"); + console.log("\nExample 2: Multi-agent workflow"); + console.log("─".repeat(60)); + console.log("\nWorkflow:\n1. CodingAssistant: \"Implement a new feature X\"\n \u2192 Codex writes the code and tests\n\n2. TestRunner: \"Run tests and verify the implementation\"\n \u2192 Codex executes tests, reports results\n\n3. CodingAssistant: \"Fix any issues found\"\n \u2192 Codex makes corrections based on test results\n\nThis demonstrates how multiple agents can collaborate,\neach using Codex as their backend through the provider.\n"); + // ============================================================================ + // Step 4: Advanced features + // ============================================================================ + console.log("\nAdvanced Features:"); + console.log("─".repeat(60)); + console.log("\n\u2713 Structured Output:\n - Provider converts OpenAI's JSON schema format\n - Codex enforces the schema during generation\n\n\u2713 Streaming:\n - Real-time progress updates via getStreamedResponse()\n - Token-by-token generation for better UX\n\n\u2713 Conversation Continuity:\n - Provider maintains thread state across turns\n - Codex remembers context and previous actions\n\n\u2713 Tool Execution:\n - Codex handles tools internally (commands, file edits, MCP)\n - No need for framework-level tool configuration\n\n\u2713 Multi-modal Input:\n - Support for text and images (available now!)\n - Images can be URLs, base64 data, or file paths\n - CodexProvider automatically handles image conversion\n - Codex can analyze screenshots and diagrams\n"); + // ============================================================================ + // Step 5: Direct usage without OpenAI Agents (for testing) + // ============================================================================ + console.log("\n\nDirect Provider Usage (for testing):"); + console.log("─".repeat(60)); + model = provider.getModel("gpt-5"); + _c.label = 1; + case 1: + _c.trys.push([1, 3, , 4]); + return [4 /*yield*/, model.getResponse({ + systemInstructions: "You are a helpful coding assistant.", + input: "What is the current working directory?", + modelSettings: { + temperature: 0.7, + maxTokens: 1000, + }, + tools: [], + outputType: { + type: "json_schema", + schema: { + type: "object", + properties: { + answer: { + type: "string", + description: "The answer to the question", + }, + }, + required: ["answer"], + additionalProperties: false, + }, + }, + handoffs: [], + tracing: { enabled: false }, + })]; + case 2: + response = _c.sent(); + console.log("\n✓ Response received:"); + console.log(" Input tokens: ".concat(response.usage.inputTokens)); + console.log(" Output tokens: ".concat(response.usage.outputTokens)); + console.log(" Response ID: ".concat(response.responseId)); + console.log("\n Output items: ".concat(response.output.length)); + for (_i = 0, _a = response.output; _i < _a.length; _i++) { + item = _a[_i]; + if (!item.type || item.type === "message") { + console.log("\n Message: ".concat(((_b = item.content[0]) === null || _b === void 0 ? void 0 : _b.type) === "output_text" ? item.content[0].text : "(non-text)")); + } + } + return [3 /*break*/, 4]; + case 3: + error_1 = _c.sent(); + console.error("\n✗ Error:", error_1 instanceof Error ? error_1.message : String(error_1)); + return [3 /*break*/, 4]; + case 4: + console.log("\n\n" + "=".repeat(60)); + console.log("🎉 CodexProvider demo complete!"); + console.log("=".repeat(60)); + return [2 /*return*/]; + } + }); + }); +} +// Run if executed directly +if (require.main === module) { + main() + .then(function () { + // Force exit after completion to avoid hanging + process.exit(0); + }) + .catch(function (error) { + console.error("Fatal error:", error); + process.exit(1); + }); +} diff --git a/sdk/native/dist-examples/examples/agents-with-tools.js b/sdk/native/dist-examples/examples/agents-with-tools.js new file mode 100644 index 000000000..7d58d7fee --- /dev/null +++ b/sdk/native/dist-examples/examples/agents-with-tools.js @@ -0,0 +1,216 @@ +"use strict"; +/** + * Example: Using CodexProvider with OpenAI Agents framework + * + * This example demonstrates how to: + * - Use CodexProvider as a ModelProvider for the OpenAI Agents framework + * - Create simple agents that interact via Codex + * - Handle basic conversational queries + * + * Note: Custom tool execution (like weather tools defined in this file) is not + * yet fully supported. The CodexProvider can execute Codex's built-in tools + * (bash, file operations, web search, etc.) but bidirectional tool execution + * with the OpenAI Agents framework requires additional integration work. + * See CodexProvider.ts executeToolViaFramework() for details. + * + * Installation: + * ```bash + * npm install @codex-native/sdk @openai/agents zod + * ``` + * + * Usage: + * ```bash + * npx tsx examples/agents-with-tools.ts + * ``` + * + * This example demonstrates using Codex's native NAPI bindings as the model + * provider. Codex handles authentication and connection to OpenAI's GPT-5 + * Responses API internally via the native binding, so no API key configuration + * is needed in your code. + * + * Key features demonstrated: + * - CodexProvider integration with OpenAI Agents framework + * - Basic conversational agents + * - Automatic cleanup of temporary directories + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.convertTemperatureTool = exports.getWeatherTool = void 0; +exports.main = main; +var node_os_1 = require("node:os"); +var node_path_1 = require("node:path"); +var node_fs_1 = require("node:fs"); +var zod_1 = require("zod"); +var agents_1 = require("@openai/agents"); +var index_ts_1 = require("../src/index.ts"); +// Define a weather tool using zod for type-safe parameters +var getWeatherTool = (0, agents_1.tool)({ + name: 'get_weather', + description: 'Get the weather for a given city', + parameters: zod_1.z.object({ + city: zod_1.z.string().describe('The city to get weather for'), + }), + execute: function (input) { return __awaiter(void 0, void 0, void 0, function () { + var weatherConditions, condition, temp; + return __generator(this, function (_a) { + console.log("[debug] Getting weather for ".concat(input.city, "\n")); + weatherConditions = ['sunny', 'cloudy', 'rainy', 'snowy']; + condition = weatherConditions[Math.floor(Math.random() * weatherConditions.length)]; + temp = Math.floor(Math.random() * 30) + 10; + return [2 /*return*/, "The weather in ".concat(input.city, " is ").concat(condition, " with a temperature of ").concat(temp, "\u00B0C")]; + }); + }); }, +}); +exports.getWeatherTool = getWeatherTool; +// Define a temperature conversion tool +var convertTemperatureTool = (0, agents_1.tool)({ + name: 'convert_temperature', + description: 'Convert temperature between Celsius and Fahrenheit', + parameters: zod_1.z.object({ + value: zod_1.z.number().describe('The temperature value to convert'), + from: zod_1.z.enum(['celsius', 'fahrenheit']).describe('The unit to convert from'), + to: zod_1.z.enum(['celsius', 'fahrenheit']).describe('The unit to convert to'), + }), + execute: function (input) { return __awaiter(void 0, void 0, void 0, function () { + var result; + return __generator(this, function (_a) { + console.log("[debug] Converting ".concat(input.value, "\u00B0").concat(input.from[0].toUpperCase(), " to ").concat(input.to, "\n")); + if (input.from === input.to) { + return [2 /*return*/, "".concat(input.value, "\u00B0").concat(input.from === 'celsius' ? 'C' : 'F')]; + } + if (input.from === 'celsius' && input.to === 'fahrenheit') { + result = (input.value * 9 / 5) + 32; + } + else { + result = (input.value - 32) * 5 / 9; + } + return [2 /*return*/, "".concat(input.value, "\u00B0").concat(input.from === 'celsius' ? 'C' : 'F', " is ").concat(result.toFixed(1), "\u00B0").concat(input.to === 'celsius' ? 'C' : 'F')]; + }); + }); }, +}); +exports.convertTemperatureTool = convertTemperatureTool; +function main() { + return __awaiter(this, void 0, void 0, function () { + var tmpDir, codexProvider, codexModel, simpleAgent, e_1; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + console.log('🤖 OpenAI Agents with Codex Provider\n'); + console.log('NOTE: This example demonstrates the CodexProvider integration,'); + console.log('but custom tools (get_weather, convert_temperature) are not yet'); + console.log('fully supported due to limitations in the current implementation.\n'); + console.log('The CodexProvider can execute its built-in tools (bash, file operations,'); + console.log('web search, etc.) but bidirectional tool execution with the OpenAI Agents'); + console.log('framework requires additional integration work.\n'); + return [4 /*yield*/, node_fs_1.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'codex-agents-example-'))]; + case 1: + tmpDir = _a.sent(); + console.log("Using temporary directory: ".concat(tmpDir, "\n")); + codexProvider = new index_ts_1.CodexProvider({ + defaultModel: 'gpt-5', + workingDirectory: tmpDir, + skipGitRepoCheck: true, + }); + return [4 /*yield*/, codexProvider.getModel()]; + case 2: + codexModel = _a.sent(); + // Example 1: Simple text query + console.log('Example 1: Basic conversational query'); + console.log('─'.repeat(60)); + simpleAgent = new agents_1.Agent({ + name: 'Assistant', + model: codexModel, + instructions: 'You are a helpful assistant.', + }); + return [4 /*yield*/, (0, agents_1.withTrace)('Conversation Example', function () { return __awaiter(_this, void 0, void 0, function () { + var question, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + question = "Hello! Please respond with a brief greeting and confirm you can help."; + console.log("Query: \"".concat(question, "\"\n")); + return [4 /*yield*/, (0, agents_1.run)(simpleAgent, question)]; + case 1: + result = _a.sent(); + console.log('\n[Final response]'); + console.log(result.finalOutput); + // Verify we got a response + if (!result.finalOutput || result.finalOutput.length === 0) { + throw new Error('No response received from agent'); + } + return [2 /*return*/]; + } + }); + }); })]; + case 3: + _a.sent(); + console.log('\n' + '='.repeat(60)); + console.log('✓ Example complete!'); + console.log('\nKey takeaways:'); + console.log(' • CodexProvider successfully integrates with OpenAI Agents framework'); + console.log(' • Basic queries work without custom tools'); + console.log(' • Custom tool execution requires bidirectional framework integration'); + console.log(' • See CodexProvider.ts executeToolViaFramework() for details'); + _a.label = 4; + case 4: + _a.trys.push([4, 6, , 7]); + return [4 /*yield*/, node_fs_1.promises.rm(tmpDir, { recursive: true, force: true })]; + case 5: + _a.sent(); + return [3 /*break*/, 7]; + case 6: + e_1 = _a.sent(); + return [3 /*break*/, 7]; + case 7: return [2 /*return*/]; + } + }); + }); +} +// Run if executed directly +if (require.main === module) { + main() + .then(function () { + // Force exit after completion to avoid hanging + process.exit(0); + }) + .catch(function (error) { + console.error('Fatal error:', error); + process.exit(1); + }); +} diff --git a/sdk/native/dist-examples/examples/automatic-tool-registration.js b/sdk/native/dist-examples/examples/automatic-tool-registration.js new file mode 100644 index 000000000..6ac0c3195 --- /dev/null +++ b/sdk/native/dist-examples/examples/automatic-tool-registration.js @@ -0,0 +1,413 @@ +"use strict"; +/** + * Example: Automatic Tool Registration with CodexProvider + * + * This example demonstrates how tools are automatically registered when + * passed to an Agent using CodexProvider. No manual tool registration + * or configuration is required - the provider handles everything seamlessly. + * + * Key concepts: + * - Tools defined with zod schemas are automatically validated + * - Multiple tools can be provided to a single agent + * - Tools are available immediately when the agent runs + * - Tool execution results are automatically passed back to the model + * + * Installation: + * ```bash + * npm install @codex-native/sdk @openai/agents zod + * ``` + * + * Usage: + * ```bash + * npx tsx examples/automatic-tool-registration.ts + * ``` + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.main = main; +var zod_1 = require("zod"); +var agents_1 = require("@openai/agents"); +var index_ts_1 = require("../src/index.ts"); +var promises_1 = require("fs/promises"); +var path_1 = require("path"); +var os_1 = require("os"); +// Define a calculator tool +var calculatorTool = (0, agents_1.tool)({ + name: 'calculator', + description: 'Perform basic arithmetic operations', + parameters: zod_1.z.object({ + operation: zod_1.z.enum(['add', 'subtract', 'multiply', 'divide']).describe('The operation to perform'), + a: zod_1.z.number().describe('First number'), + b: zod_1.z.number().describe('Second number'), + }), + execute: function (input) { return __awaiter(void 0, void 0, void 0, function () { + var result; + return __generator(this, function (_a) { + console.log("[Tool called: calculator(".concat(input.operation, ", ").concat(input.a, ", ").concat(input.b, ")]")); + switch (input.operation) { + case 'add': + result = input.a + input.b; + break; + case 'subtract': + result = input.a - input.b; + break; + case 'multiply': + result = input.a * input.b; + break; + case 'divide': + if (input.b === 0) { + return [2 /*return*/, 'Error: Division by zero']; + } + result = input.a / input.b; + break; + } + return [2 /*return*/, "".concat(input.a, " ").concat(input.operation, " ").concat(input.b, " = ").concat(result)]; + }); + }); }, +}); +// Define a unit conversion tool +var unitConverterTool = (0, agents_1.tool)({ + name: 'convert_units', + description: 'Convert between different units of measurement', + parameters: zod_1.z.object({ + value: zod_1.z.number().describe('The value to convert'), + fromUnit: zod_1.z.enum(['meters', 'feet', 'kilograms', 'pounds', 'celsius', 'fahrenheit']) + .describe('The unit to convert from'), + toUnit: zod_1.z.enum(['meters', 'feet', 'kilograms', 'pounds', 'celsius', 'fahrenheit']) + .describe('The unit to convert to'), + }), + execute: function (input) { return __awaiter(void 0, void 0, void 0, function () { + var result; + return __generator(this, function (_a) { + console.log("[Tool called: convert_units(".concat(input.value, " ").concat(input.fromUnit, " \u2192 ").concat(input.toUnit, ")]")); + // Length conversions + if (input.fromUnit === 'meters' && input.toUnit === 'feet') { + result = input.value * 3.28084; + } + else if (input.fromUnit === 'feet' && input.toUnit === 'meters') { + result = input.value / 3.28084; + } + // Weight conversions + else if (input.fromUnit === 'kilograms' && input.toUnit === 'pounds') { + result = input.value * 2.20462; + } + else if (input.fromUnit === 'pounds' && input.toUnit === 'kilograms') { + result = input.value / 2.20462; + } + // Temperature conversions + else if (input.fromUnit === 'celsius' && input.toUnit === 'fahrenheit') { + result = (input.value * 9 / 5) + 32; + } + else if (input.fromUnit === 'fahrenheit' && input.toUnit === 'celsius') { + result = (input.value - 32) * 5 / 9; + } + // Same unit + else if (input.fromUnit === input.toUnit) { + result = input.value; + } + else { + return [2 /*return*/, "Error: Cannot convert from ".concat(input.fromUnit, " to ").concat(input.toUnit)]; + } + return [2 /*return*/, "".concat(input.value, " ").concat(input.fromUnit, " = ").concat(result.toFixed(2), " ").concat(input.toUnit)]; + }); + }); }, +}); +// Define a text analysis tool +var textAnalysisTool = (0, agents_1.tool)({ + name: 'analyze_text', + description: 'Analyze text and return statistics', + parameters: zod_1.z.object({ + text: zod_1.z.string().describe('The text to analyze'), + }), + execute: function (input) { return __awaiter(void 0, void 0, void 0, function () { + var words, sentences, characters, charactersNoSpaces; + return __generator(this, function (_a) { + console.log("[Tool called: analyze_text(text length: ".concat(input.text.length, ")]")); + words = input.text.split(/\s+/).filter(function (w) { return w.length > 0; }); + sentences = input.text.split(/[.!?]+/).filter(function (s) { return s.trim().length > 0; }); + characters = input.text.length; + charactersNoSpaces = input.text.replace(/\s/g, '').length; + return [2 /*return*/, JSON.stringify({ + characters: characters, + charactersNoSpaces: charactersNoSpaces, + words: words.length, + sentences: sentences.length, + averageWordLength: (charactersNoSpaces / words.length).toFixed(2), + }, null, 2)]; + }); + }); }, +}); +function basicToolExample() { + return __awaiter(this, void 0, void 0, function () { + var tmpDir, provider, model, calculatorAgent, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + console.log('\n' + '='.repeat(70)); + console.log('Example 1: Basic Automatic Tool Registration'); + console.log('='.repeat(70) + '\n'); + return [4 /*yield*/, promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'codex-tool-example-'))]; + case 1: + tmpDir = _a.sent(); + _a.label = 2; + case 2: + _a.trys.push([2, , 5, 7]); + provider = new index_ts_1.CodexProvider({ + defaultModel: 'gpt-5', + workingDirectory: tmpDir, + skipGitRepoCheck: true, + }); + return [4 /*yield*/, provider.getModel()]; + case 3: + model = _a.sent(); + calculatorAgent = new agents_1.Agent({ + name: 'CalculatorAgent', + model: model, + instructions: 'You are a helpful calculator assistant. Use the calculator tool to perform operations. Answer directly with just the calculation result.', + tools: [calculatorTool], + }); + console.log('✓ Created CalculatorAgent'); + console.log('✓ Tool "calculator" automatically registered\n'); + console.log('─'.repeat(70)); + console.log('Query: "What is 123 multiplied by 456?"\n'); + return [4 /*yield*/, (0, agents_1.run)(calculatorAgent, 'What is 123 multiplied by 456?')]; + case 4: + result = _a.sent(); + console.log('\n[Final response]'); + console.log(result.finalOutput); + return [3 /*break*/, 7]; + case 5: + // Cleanup temp directory + return [4 /*yield*/, promises_1.default.rm(tmpDir, { recursive: true, force: true }).catch(function () { })]; + case 6: + // Cleanup temp directory + _a.sent(); + return [7 /*endfinally*/]; + case 7: return [2 /*return*/]; + } + }); + }); +} +function multipleToolsExample() { + return __awaiter(this, void 0, void 0, function () { + var tmpDir, provider, model, multiToolAgent, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + console.log('\n\n' + '='.repeat(70)); + console.log('Example 2: Multiple Tools Automatically Registered'); + console.log('='.repeat(70) + '\n'); + return [4 /*yield*/, promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'codex-tool-example-'))]; + case 1: + tmpDir = _a.sent(); + _a.label = 2; + case 2: + _a.trys.push([2, , 5, 7]); + provider = new index_ts_1.CodexProvider({ + defaultModel: 'gpt-5', + workingDirectory: tmpDir, + skipGitRepoCheck: true, + }); + return [4 /*yield*/, provider.getModel()]; + case 3: + model = _a.sent(); + multiToolAgent = new agents_1.Agent({ + name: 'MultiToolAgent', + model: model, + instructions: 'You are a helpful assistant with access to calculator, unit converter, and text analysis tools. Use the tools to answer questions directly.', + tools: [calculatorTool, unitConverterTool, textAnalysisTool], + }); + console.log('✓ Created MultiToolAgent'); + console.log('✓ Tools automatically registered:'); + console.log(' - calculator'); + console.log(' - convert_units'); + console.log(' - analyze_text\n'); + console.log('─'.repeat(70)); + console.log('Query: "Convert 100 pounds to kilograms, then multiply by 2"\n'); + return [4 /*yield*/, (0, agents_1.run)(multiToolAgent, 'Convert 100 pounds to kilograms, then multiply by 2')]; + case 4: + result = _a.sent(); + console.log('\n[Final response]'); + console.log(result.finalOutput); + return [3 /*break*/, 7]; + case 5: + // Cleanup temp directory + return [4 /*yield*/, promises_1.default.rm(tmpDir, { recursive: true, force: true }).catch(function () { })]; + case 6: + // Cleanup temp directory + _a.sent(); + return [7 /*endfinally*/]; + case 7: return [2 /*return*/]; + } + }); + }); +} +function toolChainExample() { + return __awaiter(this, void 0, void 0, function () { + var tmpDir, provider, model, agent, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + console.log('\n\n' + '='.repeat(70)); + console.log('Example 3: Chaining Multiple Tool Calls'); + console.log('='.repeat(70) + '\n'); + return [4 /*yield*/, promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'codex-tool-example-'))]; + case 1: + tmpDir = _a.sent(); + _a.label = 2; + case 2: + _a.trys.push([2, , 5, 7]); + provider = new index_ts_1.CodexProvider({ + defaultModel: 'gpt-5', + workingDirectory: tmpDir, + skipGitRepoCheck: true, + }); + return [4 /*yield*/, provider.getModel()]; + case 3: + model = _a.sent(); + agent = new agents_1.Agent({ + name: 'ToolChainAgent', + model: model, + instructions: 'You are a helpful assistant. Use multiple tools in sequence when needed to answer complex questions. Answer directly with the results.', + tools: [calculatorTool, unitConverterTool, textAnalysisTool], + }); + console.log('✓ Created ToolChainAgent with 3 tools\n'); + console.log('─'.repeat(70)); + console.log('Complex query requiring multiple tool calls:\n'); + console.log('Query: "I weigh 150 pounds. Convert that to kilograms, then calculate'); + console.log(' what 20% of my weight would be. Also analyze this sentence:'); + console.log(' \'The quick brown fox jumps over the lazy dog.\'"\n'); + return [4 /*yield*/, (0, agents_1.run)(agent, 'I weigh 150 pounds. Convert that to kilograms, then calculate what 20% of my weight would be. Also analyze this sentence: "The quick brown fox jumps over the lazy dog."')]; + case 4: + result = _a.sent(); + console.log('\n[Final response]'); + console.log(result.finalOutput); + return [3 /*break*/, 7]; + case 5: + // Cleanup temp directory + return [4 /*yield*/, promises_1.default.rm(tmpDir, { recursive: true, force: true }).catch(function () { })]; + case 6: + // Cleanup temp directory + _a.sent(); + return [7 /*endfinally*/]; + case 7: return [2 /*return*/]; + } + }); + }); +} +function toolValidationExample() { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + console.log('\n\n' + '='.repeat(70)); + console.log('Example 4: Automatic Tool Parameter Validation'); + console.log('='.repeat(70) + '\n'); + console.log('Tools defined with Zod schemas have automatic validation:'); + console.log(' • Type checking (string, number, enum, etc.)'); + console.log(' • Required vs optional parameters'); + console.log(' • Value constraints and descriptions'); + console.log(' • Automatic error messages for invalid inputs\n'); + console.log('Example tool schema:'); + console.log("\n const calculatorTool = tool({\n name: 'calculator',\n parameters: z.object({\n operation: z.enum(['add', 'subtract', 'multiply', 'divide']),\n a: z.number().describe('First number'),\n b: z.number().describe('Second number'),\n }),\n execute: async (input) => { /* implementation */ }\n });\n "); + console.log('\n✓ When this tool is registered with CodexProvider:'); + console.log(' • The model receives the schema definition'); + console.log(' • Invalid calls are caught and handled gracefully'); + console.log(' • The agent can understand parameter requirements'); + console.log(' • No manual validation code needed!'); + return [2 /*return*/]; + }); + }); +} +function main() { + return __awaiter(this, void 0, void 0, function () { + var error_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + console.log('🔧 Automatic Tool Registration Examples\n'); + console.log('This demonstrates how CodexProvider automatically registers'); + console.log('tools when they are passed to an Agent - no manual configuration!\n'); + _a.label = 1; + case 1: + _a.trys.push([1, 6, , 7]); + // Run all examples + return [4 /*yield*/, basicToolExample()]; + case 2: + // Run all examples + _a.sent(); + return [4 /*yield*/, multipleToolsExample()]; + case 3: + _a.sent(); + return [4 /*yield*/, toolChainExample()]; + case 4: + _a.sent(); + return [4 /*yield*/, toolValidationExample()]; + case 5: + _a.sent(); + console.log('\n\n' + '='.repeat(70)); + console.log('✓ All automatic tool registration examples complete!'); + console.log('='.repeat(70)); + console.log('\nKey takeaways:'); + console.log(' • Tools are automatically registered when passed to Agent()'); + console.log(' • No manual tool.register() or provider.addTool() calls needed'); + console.log(' • Multiple tools can be registered at once'); + console.log(' • Tools defined with Zod have automatic validation'); + console.log(' • Tool execution results flow back to the model automatically'); + console.log(' • The agent can chain multiple tool calls to complete tasks'); + return [3 /*break*/, 7]; + case 6: + error_1 = _a.sent(); + console.error('\n✗ Error:', error_1 instanceof Error ? error_1.message : String(error_1)); + process.exit(1); + return [3 /*break*/, 7]; + case 7: return [2 /*return*/]; + } + }); + }); +} +// Run if executed directly +if (require.main === module) { + main() + .then(function () { + // Force exit after completion to avoid hanging + process.exit(0); + }) + .catch(function (error) { + console.error('Fatal error:', error); + process.exit(1); + }); +} diff --git a/sdk/native/dist-examples/examples/codex-provider-direct.js b/sdk/native/dist-examples/examples/codex-provider-direct.js new file mode 100644 index 000000000..42cdb8a23 --- /dev/null +++ b/sdk/native/dist-examples/examples/codex-provider-direct.js @@ -0,0 +1,105 @@ +"use strict"; +/** + * Example: Pass a Codex model instance directly to an Agent + * + * This demonstrates the most direct way to use Codex with the agents framework: + * create a CodexProvider, get a model, and pass it directly to your Agent. + * + * CodexProvider features: + * - Multi-modal input support (text + images) + * - Streaming response deltas + * - Automatic tool registration + * - No API key configuration needed (handled by native bindings) + * + * Usage: + * ```bash + * npx tsx examples/codex-provider-direct.ts + * ``` + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var agents_1 = require("@openai/agents"); +var index_ts_1 = require("../src/index.ts"); +function main() { + return __awaiter(this, void 0, void 0, function () { + var provider, model, agent, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + provider = new index_ts_1.CodexProvider({ + defaultModel: 'gpt-5', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + return [4 /*yield*/, provider.getModel('gpt-5')]; + case 1: + model = _a.sent(); + agent = new agents_1.Agent({ + name: 'CodexAssistant', + model: model, + instructions: 'You are a helpful coding assistant powered by GPT-5 through Codex. You support both text and image inputs.', + }); + // Run the agent with text input + console.log('Example 1: Text input\n'); + return [4 /*yield*/, (0, agents_1.run)(agent, 'What is the capital of France?')]; + case 2: + result = _a.sent(); + console.log('\nAgent response:'); + console.log(result.finalOutput); + console.log('\n✓ Example 1 completed successfully'); + // Example with image input (multi-modal) + // CodexProvider automatically handles image conversion + console.log('\n\nExample 2: Multi-modal input (text + image)\n'); + console.log('Note: Images can be provided as URLs, base64 data, or file paths'); + console.log(' CodexProvider handles the conversion automatically'); + console.log('\n✓ Example 2 info displayed (no actual image run in this demo)'); + return [2 /*return*/]; + } + }); + }); +} +main() + .then(function () { + console.log('\nExamples completed successfully.'); + // Force exit after completion - native bindings may keep handles open + setTimeout(function () { return process.exit(0); }, 100); +}) + .catch(function (error) { + console.error('Error:', error); + process.exit(1); +}); diff --git a/sdk/native/dist-examples/examples/codex-provider-global.js b/sdk/native/dist-examples/examples/codex-provider-global.js new file mode 100644 index 000000000..5c93fc178 --- /dev/null +++ b/sdk/native/dist-examples/examples/codex-provider-global.js @@ -0,0 +1,102 @@ +"use strict"; +/** + * Example: Using CodexProvider with OpenAI Agents framework + * + * This demonstrates how to create a CodexProvider and use it + * with the OpenAI Agents framework to run queries. + * + * CodexProvider features: + * - Multi-modal input support (text + images) + * - Streaming response deltas for real-time updates + * - Automatic tool registration when passed to agents + * - No API key required (uses local Codex instance) + * + * Usage: + * ```bash + * npx tsx examples/codex-provider-global.ts + * ``` + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var agents_1 = require("@openai/agents"); +var index_ts_1 = require("../src/index.ts"); +function main() { + return __awaiter(this, void 0, void 0, function () { + var codexProvider, model, agent, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + codexProvider = new index_ts_1.CodexProvider({ + defaultModel: 'gpt-5', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + model = codexProvider.getModel(); + agent = new agents_1.Agent({ + name: 'SharedCodexAgent', + model: model, + instructions: 'You are a helpful assistant powered by Codex. Answer concisely in one sentence.', + }); + // Run a single query to demonstrate the integration + console.log('Query:'); + return [4 /*yield*/, (0, agents_1.run)(agent, 'What is 2+2?')]; + case 1: + result = _a.sent(); + console.log(result.finalOutput); + console.log('\nQuery completed successfully!'); + return [2 /*return*/]; + } + }); + }); +} +main() + .then(function () { + console.log('\nExiting...'); + // Force exit after completion to avoid hanging from native binding + // Use a small delay to ensure stdout flushes + setTimeout(function () { return process.exit(0); }, 100); +}) + .catch(function (error) { + console.error('Error:', error); + setTimeout(function () { return process.exit(1); }, 100); +}); +// Fallback timeout in case the native binding hangs +setTimeout(function () { + console.error('\nERROR: Script timed out after 30 seconds'); + process.exit(124); +}, 30000); diff --git a/sdk/native/dist-examples/examples/codex-provider-run.js b/sdk/native/dist-examples/examples/codex-provider-run.js new file mode 100644 index 000000000..71161cf5a --- /dev/null +++ b/sdk/native/dist-examples/examples/codex-provider-run.js @@ -0,0 +1,97 @@ +"use strict"; +/** + * Example: Use CodexProvider for a single execution run + * + * This demonstrates how to create and use a Codex provider for a specific + * operation without registering it globally. + * + * CodexProvider capabilities: + * - Multi-modal inputs (text and images) + * - Streaming deltas for real-time response updates + * - Automatic tool registration + * - Local file path, URL, and base64 image support + * + * Usage: + * ```bash + * npx tsx examples/codex-provider-run.ts + * ``` + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var agents_1 = require("@openai/agents"); +var index_ts_1 = require("../src/index.ts"); +function main() { + return __awaiter(this, void 0, void 0, function () { + var provider, codexModel, agent, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + provider = new index_ts_1.CodexProvider({ + defaultModel: 'gpt-5', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + return [4 /*yield*/, provider.getModel()]; + case 1: + codexModel = _a.sent(); + agent = new agents_1.Agent({ + name: 'TaskSpecificAgent', + model: codexModel, + instructions: 'You are a helpful assistant. Answer questions directly and concisely in 1-2 sentences.', + }); + return [4 /*yield*/, (0, agents_1.run)(agent, 'What is 2+2? Answer with just the number.')]; + case 2: + result = _a.sent(); + console.log('Result:'); + console.log(result.finalOutput); + return [2 /*return*/]; + } + }); + }); +} +main() + .then(function () { + console.log('\nMain completed successfully.'); + // Force exit after completion - native bindings may keep handles open + // Using setTimeout to ensure output is flushed before exit + setTimeout(function () { return process.exit(0); }, 100); +}) + .catch(function (error) { + console.error('Error:', error); + process.exit(1); +}); diff --git a/sdk/native/dist-examples/examples/review-example.js b/sdk/native/dist-examples/examples/review-example.js new file mode 100644 index 000000000..701f06bd7 --- /dev/null +++ b/sdk/native/dist-examples/examples/review-example.js @@ -0,0 +1,126 @@ +"use strict"; +/** + * Example: Using Codex review method to review code changes + * + * This demonstrates how to use the review() method on the Codex class + * to perform code reviews with different targets. + * + * Review targets: + * - current_changes: Review staged/unstaged files + * - custom: Custom review prompt + * + * Usage: + * ```bash + * npx tsx examples/review-example.ts + * ``` + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var index_ts_1 = require("../src/index.ts"); +var promises_1 = require("fs/promises"); +var path_1 = require("path"); +var os_1 = require("os"); +function main() { + return __awaiter(this, void 0, void 0, function () { + var tmpDir, sampleCode, codex, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "codex-review-test-"))]; + case 1: + tmpDir = _a.sent(); + _a.label = 2; + case 2: + _a.trys.push([2, , 5, 7]); + sampleCode = "\n// Sample code with some issues\nfunction calculateTotal(items) {\n var total = 0; // Using var instead of const/let\n for (var i = 0; i < items.length; i++) { // Could use forEach/map\n total = total + items[i].price;\n }\n return total; // Missing input validation\n}\n\n// Function with potential bug\nfunction divideNumbers(a, b) {\n return a / b; // No zero-division check\n}\n\n// Unused function\nfunction unusedFunction() {\n console.log(\"This is never called\");\n}\n"; + return [4 /*yield*/, promises_1.default.writeFile(path_1.default.join(tmpDir, "sample.js"), sampleCode)]; + case 3: + _a.sent(); + console.log("Created sample code for review in:", tmpDir); + console.log("\nStarting code review with custom prompt...\n"); + codex = new index_ts_1.Codex(); + return [4 /*yield*/, codex.review({ + target: { + type: "custom", + prompt: "Review the JavaScript code in sample.js. Look for:\n1. Outdated JavaScript patterns (var, for loops)\n2. Missing error handling\n3. Potential bugs\n4. Unused code\n\nProvide a concise summary of findings with 2-3 key issues.", + hint: "JavaScript code review", + }, + threadOptions: { + model: "gpt-5-codex", + workingDirectory: tmpDir, + skipGitRepoCheck: true, + fullAuto: true, + }, + })]; + case 4: + result = _a.sent(); + console.log("\n" + "=".repeat(70)); + console.log("Review Results:"); + console.log("=".repeat(70) + "\n"); + if (result.finalResponse) { + console.log(result.finalResponse); + } + else { + console.log("[No final review summary produced]"); + } + if (result.usage) { + console.log("\nToken usage: ".concat(result.usage.input_tokens, " input, ").concat(result.usage.output_tokens, " output")); + } + console.log("\n✓ Review completed successfully"); + return [3 /*break*/, 7]; + case 5: + // Cleanup + return [4 /*yield*/, promises_1.default.rm(tmpDir, { recursive: true, force: true }).catch(function () { })]; + case 6: + // Cleanup + _a.sent(); + console.log("✓ Cleaned up temporary directory"); + return [7 /*endfinally*/]; + case 7: return [2 /*return*/]; + } + }); + }); +} +main() + .then(function () { + console.log("\nExample completed successfully."); + setTimeout(function () { return process.exit(0); }, 100); +}) + .catch(function (error) { + console.error("Error:", error); + process.exit(1); +}); diff --git a/sdk/native/dist-examples/examples/streaming-deltas.js b/sdk/native/dist-examples/examples/streaming-deltas.js new file mode 100644 index 000000000..18d546f24 --- /dev/null +++ b/sdk/native/dist-examples/examples/streaming-deltas.js @@ -0,0 +1,475 @@ +"use strict"; +/** + * Example: Streaming responses with CodexProvider + * + * This example demonstrates how to use CodexProvider's streaming capabilities + * to receive events as the model generates a response. This is useful for + * building responsive UIs that show progress in real-time. + * + * Features demonstrated: + * - Streaming text responses (complete text when generation finishes) + * - Streaming reasoning updates (for extended thinking models) + * - Handling different stream event types + * - Real-time progress monitoring + * - Image input support with streaming + * + * Note: The current implementation emits complete text/reasoning blocks + * rather than character-by-character deltas. Events are emitted when + * each phase completes (reasoning done, text done, etc.). + * + * Installation: + * ```bash + * npm install @codex-native/sdk @openai/agents + * ``` + * + * Usage: + * ```bash + * npx tsx examples/streaming-deltas.ts + * ``` + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __asyncValues = (this && this.__asyncValues) || function (o) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var m = o[Symbol.asyncIterator], i; + return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); + function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } + function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.main = main; +var agents_1 = require("@openai/agents"); +var index_ts_1 = require("../src/index.ts"); +function streamingTextExample() { + return __awaiter(this, void 0, void 0, function () { + var provider, model, agent, request, fullText, fullReasoning, _a, _b, _c, event_1, e_1_1; + var _d, e_1, _e, _f; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + console.log('\n' + '='.repeat(70)); + console.log('Example 1: Streaming Text Response'); + console.log('='.repeat(70) + '\n'); + provider = new index_ts_1.CodexProvider({ + defaultModel: 'gpt-5', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + return [4 /*yield*/, provider.getModel()]; + case 1: + model = _g.sent(); + agent = new agents_1.Agent({ + name: 'StreamingAssistant', + model: model, + instructions: 'You are a helpful assistant. Provide detailed explanations.', + }); + console.log('Requesting: "Explain how neural networks work in 3 paragraphs"\n'); + console.log('─'.repeat(70)); + console.log('Streaming response:'); + console.log('─'.repeat(70) + '\n'); + request = { + systemInstructions: 'You are a helpful assistant.', + input: 'Explain how neural networks work in 3 paragraphs', + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + fullText = ''; + fullReasoning = ''; + _g.label = 2; + case 2: + _g.trys.push([2, 7, 8, 13]); + _a = true, _b = __asyncValues(model.getStreamedResponse(request)); + _g.label = 3; + case 3: return [4 /*yield*/, _b.next()]; + case 4: + if (!(_c = _g.sent(), _d = _c.done, !_d)) return [3 /*break*/, 6]; + _f = _c.value; + _a = false; + event_1 = _f; + switch (event_1.type) { + case 'response_started': + console.log('[Stream started]'); + console.log('[Generating response...]\n'); + break; + case 'output_text_delta': + // Note: Delta events are not currently emitted by the backend + // Text is provided as a complete block in output_text_done + process.stdout.write(event_1.delta); + fullText += event_1.delta; + break; + case 'output_text_done': + // Text is provided here as a complete block + fullText = event_1.text; + console.log(event_1.text); + console.log('\n[Text generation complete]'); + break; + case 'reasoning_delta': + // Note: Delta events are not currently emitted by the backend + // Reasoning is provided as a complete block in reasoning_done + fullReasoning += event_1.delta; + break; + case 'reasoning_done': + if (event_1.reasoning) { + fullReasoning = event_1.reasoning; + console.log("[Extended thinking complete: ".concat(event_1.reasoning.length, " chars]")); + } + break; + case 'response_done': + console.log("\n[Response done]"); + console.log(" Input tokens: ".concat(event_1.response.usage.inputTokens)); + console.log(" Output tokens: ".concat(event_1.response.usage.outputTokens)); + console.log(" Total tokens: ".concat(event_1.response.usage.totalTokens)); + break; + case 'error': + console.error("\n[Error]: ".concat(event_1.error.message)); + break; + } + _g.label = 5; + case 5: + _a = true; + return [3 /*break*/, 3]; + case 6: return [3 /*break*/, 13]; + case 7: + e_1_1 = _g.sent(); + e_1 = { error: e_1_1 }; + return [3 /*break*/, 13]; + case 8: + _g.trys.push([8, , 11, 12]); + if (!(!_a && !_d && (_e = _b.return))) return [3 /*break*/, 10]; + return [4 /*yield*/, _e.call(_b)]; + case 9: + _g.sent(); + _g.label = 10; + case 10: return [3 /*break*/, 12]; + case 11: + if (e_1) throw e_1.error; + return [7 /*endfinally*/]; + case 12: return [7 /*endfinally*/]; + case 13: + console.log('\n' + '─'.repeat(70)); + console.log("Final text length: ".concat(fullText.length, " characters")); + if (fullReasoning) { + console.log("Reasoning length: ".concat(fullReasoning.length, " characters")); + } + return [2 /*return*/]; + } + }); + }); +} +function streamingWithImageExample() { + return __awaiter(this, void 0, void 0, function () { + var provider, model, imageUrl, request, responseText, _a, _b, _c, event_2, e_2_1; + var _d, e_2, _e, _f; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + console.log('\n\n' + '='.repeat(70)); + console.log('Example 2: Streaming with Image Input'); + console.log('='.repeat(70) + '\n'); + provider = new index_ts_1.CodexProvider({ + defaultModel: 'gpt-5', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + return [4 /*yield*/, provider.getModel()]; + case 1: + model = _g.sent(); + console.log('Sending multi-modal input (text + image)...'); + console.log('CodexProvider automatically handles image conversion for streaming\n'); + console.log('─'.repeat(70)); + console.log('Streaming response:'); + console.log('─'.repeat(70) + '\n'); + imageUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg'; + request = { + systemInstructions: 'You are a helpful assistant that can analyze images.', + input: [ + { type: 'input_text', text: 'Describe what you see in this image concisely' }, + { type: 'input_image', image: imageUrl } + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + responseText = ''; + _g.label = 2; + case 2: + _g.trys.push([2, 7, 8, 13]); + _a = true, _b = __asyncValues(model.getStreamedResponse(request)); + _g.label = 3; + case 3: return [4 /*yield*/, _b.next()]; + case 4: + if (!(_c = _g.sent(), _d = _c.done, !_d)) return [3 /*break*/, 6]; + _f = _c.value; + _a = false; + event_2 = _f; + switch (event_2.type) { + case 'response_started': + console.log('[Stream started - processing image...]'); + console.log('[Generating response...]\n'); + break; + case 'output_text_delta': + process.stdout.write(event_2.delta); + responseText += event_2.delta; + break; + case 'output_text_done': + responseText = event_2.text; + console.log(event_2.text); + console.log('\n[Text generation complete]'); + break; + case 'reasoning_done': + if (event_2.reasoning) { + console.log("[Extended thinking complete: ".concat(event_2.reasoning.length, " chars]")); + } + break; + case 'response_done': + console.log("\n[Response done]"); + console.log(" Tokens used: ".concat(event_2.response.usage.totalTokens)); + break; + case 'error': + console.error("\n[Error]: ".concat(event_2.error.message)); + break; + } + _g.label = 5; + case 5: + _a = true; + return [3 /*break*/, 3]; + case 6: return [3 /*break*/, 13]; + case 7: + e_2_1 = _g.sent(); + e_2 = { error: e_2_1 }; + return [3 /*break*/, 13]; + case 8: + _g.trys.push([8, , 11, 12]); + if (!(!_a && !_d && (_e = _b.return))) return [3 /*break*/, 10]; + return [4 /*yield*/, _e.call(_b)]; + case 9: + _g.sent(); + _g.label = 10; + case 10: return [3 /*break*/, 12]; + case 11: + if (e_2) throw e_2.error; + return [7 /*endfinally*/]; + case 12: return [7 /*endfinally*/]; + case 13: + console.log('\n' + '─'.repeat(70)); + console.log("Response length: ".concat(responseText.length, " characters")); + return [2 /*return*/]; + } + }); + }); +} +function detailedStreamEventExample() { + return __awaiter(this, void 0, void 0, function () { + var provider, model, request, eventCounts, _a, _b, _c, event_3, count, e_3_1, _i, _d, _e, eventType, count; + var _f, e_3, _g, _h; + return __generator(this, function (_j) { + switch (_j.label) { + case 0: + console.log('\n\n' + '='.repeat(70)); + console.log('Example 3: Understanding All Stream Event Types'); + console.log('='.repeat(70) + '\n'); + provider = new index_ts_1.CodexProvider({ + defaultModel: 'gpt-5', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + return [4 /*yield*/, provider.getModel()]; + case 1: + model = _j.sent(); + console.log('This example shows all possible stream event types:\n'); + console.log('Event Types:'); + console.log(' • response_started - Stream begins'); + console.log(' • output_text_delta - Incremental text chunk (not currently emitted)'); + console.log(' • output_text_done - Text generation complete (includes full text)'); + console.log(' • reasoning_delta - Incremental reasoning (not currently emitted)'); + console.log(' • reasoning_done - Reasoning complete (includes full reasoning)'); + console.log(' • response_done - Full response complete with usage stats'); + console.log(' • error - An error occurred\n'); + console.log('Note: Currently only complete blocks are emitted (reasoning_done, output_text_done)'); + console.log(' rather than incremental deltas.\n'); + console.log('─'.repeat(70)); + console.log('Event stream:\n'); + request = { + systemInstructions: 'You are a helpful assistant.', + input: 'Count from 1 to 5 and explain each number.', + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + eventCounts = new Map(); + _j.label = 2; + case 2: + _j.trys.push([2, 7, 8, 13]); + _a = true, _b = __asyncValues(model.getStreamedResponse(request)); + _j.label = 3; + case 3: return [4 /*yield*/, _b.next()]; + case 4: + if (!(_c = _j.sent(), _f = _c.done, !_f)) return [3 /*break*/, 6]; + _h = _c.value; + _a = false; + event_3 = _h; + count = eventCounts.get(event_3.type) || 0; + eventCounts.set(event_3.type, count + 1); + // Log each event type + switch (event_3.type) { + case 'response_started': + console.log("[".concat(event_3.type, "]")); + break; + case 'output_text_delta': + // Delta events are not currently emitted, but handler is here for future compatibility + if (count < 3) { + console.log("[".concat(event_3.type, "] delta=\"").concat(event_3.delta, "\"")); + } + else if (count === 3) { + console.log("[".concat(event_3.type, "] ... (").concat(count, " more delta events) ...")); + } + break; + case 'output_text_done': + console.log("[".concat(event_3.type, "] text length=").concat(event_3.text.length)); + break; + case 'reasoning_delta': + // Delta events are not currently emitted, but handler is here for future compatibility + if (count < 3) { + console.log("[".concat(event_3.type, "] delta=\"").concat(event_3.delta, "\"")); + } + break; + case 'reasoning_done': + console.log("[".concat(event_3.type, "] reasoning length=").concat(event_3.reasoning.length)); + break; + case 'response_done': + console.log("[".concat(event_3.type, "] usage=").concat(JSON.stringify(event_3.response.usage))); + break; + case 'error': + console.log("[".concat(event_3.type, "] message=\"").concat(event_3.error.message, "\"")); + break; + } + _j.label = 5; + case 5: + _a = true; + return [3 /*break*/, 3]; + case 6: return [3 /*break*/, 13]; + case 7: + e_3_1 = _j.sent(); + e_3 = { error: e_3_1 }; + return [3 /*break*/, 13]; + case 8: + _j.trys.push([8, , 11, 12]); + if (!(!_a && !_f && (_g = _b.return))) return [3 /*break*/, 10]; + return [4 /*yield*/, _g.call(_b)]; + case 9: + _j.sent(); + _j.label = 10; + case 10: return [3 /*break*/, 12]; + case 11: + if (e_3) throw e_3.error; + return [7 /*endfinally*/]; + case 12: return [7 /*endfinally*/]; + case 13: + console.log('\n' + '─'.repeat(70)); + console.log('Event Statistics:'); + for (_i = 0, _d = eventCounts.entries(); _i < _d.length; _i++) { + _e = _d[_i], eventType = _e[0], count = _e[1]; + console.log(" ".concat(eventType, ": ").concat(count, " events")); + } + return [2 /*return*/]; + } + }); + }); +} +function main() { + return __awaiter(this, void 0, void 0, function () { + var error_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + console.log('🌊 CodexProvider Streaming Response Examples\n'); + console.log('These examples demonstrate streaming capabilities for monitoring'); + console.log('response generation in real-time.\n'); + _a.label = 1; + case 1: + _a.trys.push([1, 5, , 6]); + // Run all examples + return [4 /*yield*/, streamingTextExample()]; + case 2: + // Run all examples + _a.sent(); + return [4 /*yield*/, streamingWithImageExample()]; + case 3: + _a.sent(); + return [4 /*yield*/, detailedStreamEventExample()]; + case 4: + _a.sent(); + console.log('\n\n' + '='.repeat(70)); + console.log('✓ All streaming examples complete!'); + console.log('='.repeat(70)); + console.log('\nKey takeaways:'); + console.log(' • Stream events provide real-time progress updates'); + console.log(' • output_text_done provides the complete generated text'); + console.log(' • reasoning_done provides the complete extended thinking'); + console.log(' • Image inputs work seamlessly with streaming'); + console.log(' • Usage statistics are provided in the response_done event'); + console.log(' • Current implementation emits complete blocks rather than character deltas'); + return [3 /*break*/, 6]; + case 5: + error_1 = _a.sent(); + console.error('\n✗ Error:', error_1 instanceof Error ? error_1.message : String(error_1)); + process.exit(1); + return [3 /*break*/, 6]; + case 6: return [2 /*return*/]; + } + }); + }); +} +// Run if executed directly +if (require.main === module) { + main() + .then(function () { + // Force exit after completion to avoid hanging + process.exit(0); + }) + .catch(function (error) { + console.error('Fatal error:', error); + process.exit(1); + }); +} diff --git a/sdk/native/dist-examples/src/agents/CodexProvider.js b/sdk/native/dist-examples/src/agents/CodexProvider.js new file mode 100644 index 000000000..b4e3ae4ca --- /dev/null +++ b/sdk/native/dist-examples/src/agents/CodexProvider.js @@ -0,0 +1,853 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } +var __asyncValues = (this && this.__asyncValues) || function (o) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var m = o[Symbol.asyncIterator], i; + return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); + function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } + function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } +}; +var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var g = generator.apply(thisArg, _arguments || []), i, q = []; + return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i; + function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; } + function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } } + function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } + function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } + function fulfill(value) { resume("next", value); } + function reject(value) { resume("throw", value); } + function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CodexProvider = void 0; +var fs = require("fs"); +var path = require("path"); +var os = require("os"); +/** + * Provider implementation that uses Codex as the backend for OpenAI Agents + * + * @example + * ```typescript + * import { CodexProvider } from '@openai/codex-native/agents'; + * import { Agent, Runner } from '@openai/agents'; + * + * defaultModel: 'gpt-5' + * }); + * + * const agent = new Agent({ + * name: 'CodeAssistant', + * instructions: 'You are a helpful coding assistant' + * }); + * + * const runner = new Runner({ modelProvider: provider }); + * const result = await runner.run(agent, 'Fix the failing tests'); + * ``` + */ +var CodexProvider = /** @class */ (function () { + function CodexProvider(options) { + if (options === void 0) { options = {}; } + var _a; + this.codex = null; + this.options = __assign({ workingDirectory: options.workingDirectory || process.cwd(), skipGitRepoCheck: (_a = options.skipGitRepoCheck) !== null && _a !== void 0 ? _a : false }, options); + } + /** + * Lazy initialization of Codex instance + */ + CodexProvider.prototype.getCodex = function () { + if (!this.codex) { + try { + // Dynamic import to avoid circular dependencies + // eslint-disable-next-line @typescript-eslint/no-var-requires + var CodexClass = require("../codex").Codex; + if (!CodexClass) { + throw new Error("Codex class not found in module"); + } + this.codex = new CodexClass({ + apiKey: this.options.apiKey, + baseUrl: this.options.baseUrl, + }); + } + catch (error) { + throw new Error("Failed to initialize Codex: ".concat(error instanceof Error ? error.message : String(error))); + } + } + return this.codex; + }; + CodexProvider.prototype.getModel = function (modelName) { + var model = modelName || this.options.defaultModel; + return new CodexModel(this.getCodex(), model, this.options); + }; + return CodexProvider; +}()); +exports.CodexProvider = CodexProvider; +/** + * Model implementation that wraps a Codex Thread + */ +var CodexModel = /** @class */ (function () { + function CodexModel(codex, modelName, options) { + this.thread = null; + this.registeredTools = new Set(); + this.pendingToolCalls = new Map(); + this.tempImageFiles = new Set(); + this.codex = codex; + this.modelName = modelName; + this.options = options; + } + /** + * Cleanup temporary image files created during request processing + */ + CodexModel.prototype.cleanupTempFiles = function () { + return __awaiter(this, void 0, void 0, function () { + var _i, _a, filepath, error_1; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _i = 0, _a = this.tempImageFiles; + _b.label = 1; + case 1: + if (!(_i < _a.length)) return [3 /*break*/, 6]; + filepath = _a[_i]; + _b.label = 2; + case 2: + _b.trys.push([2, 4, , 5]); + return [4 /*yield*/, fs.promises.unlink(filepath)]; + case 3: + _b.sent(); + return [3 /*break*/, 5]; + case 4: + error_1 = _b.sent(); + return [3 /*break*/, 5]; + case 5: + _i++; + return [3 /*break*/, 1]; + case 6: + this.tempImageFiles.clear(); + return [2 /*return*/]; + } + }); + }); + }; + /** + * Get or create the thread for this model instance + */ + CodexModel.prototype.getThread = function (conversationId) { + // If we have a conversation ID and either no thread or a different thread + if (conversationId) { + if (!this.thread || this.thread.id !== conversationId) { + // Resume the specified thread + this.thread = this.codex.resumeThread(conversationId, this.getThreadOptions()); + } + } + else if (!this.thread) { + // Create new thread only if we don't have one + this.thread = this.codex.startThread(this.getThreadOptions()); + } + return this.thread; + }; + CodexModel.prototype.getThreadOptions = function () { + return { + model: this.modelName, + workingDirectory: this.options.workingDirectory, + skipGitRepoCheck: this.options.skipGitRepoCheck, + fullAuto: true, + }; + }; + CodexModel.prototype.getResponse = function (request) { + return __awaiter(this, void 0, void 0, function () { + var thread, input, turn; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, , 3, 5]); + thread = this.getThread(request.conversationId || request.previousResponseId); + // Register any tools provided in the request + if (request.tools && request.tools.length > 0) { + this.registerRequestTools(request.tools); + } + return [4 /*yield*/, this.convertRequestToInput(request)]; + case 1: + input = _b.sent(); + return [4 /*yield*/, thread.run(input, { + outputSchema: (_a = request.outputType) === null || _a === void 0 ? void 0 : _a.schema, + })]; + case 2: + turn = _b.sent(); + // Convert Codex response to ModelResponse format + return [2 /*return*/, { + usage: this.convertUsage(turn.usage), + output: this.convertItemsToOutput(turn.items, turn.finalResponse), + responseId: thread.id || undefined, + }]; + case 3: + // Clean up temporary image files + return [4 /*yield*/, this.cleanupTempFiles()]; + case 4: + // Clean up temporary image files + _b.sent(); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; + } + }); + }); + }; + CodexModel.prototype.getStreamedResponse = function (request) { + return __asyncGenerator(this, arguments, function getStreamedResponse_1() { + var MAX_ACCUMULATED_SIZE, thread, input, events, textAccumulator, _a, events_1, events_1_1, event_1, totalSize, _i, _b, text, streamEvents, _c, streamEvents_1, streamEvent, e_1_1; + var _d, e_1, _e, _f; + var _g; + return __generator(this, function (_h) { + switch (_h.label) { + case 0: + MAX_ACCUMULATED_SIZE = 10000000; + _h.label = 1; + case 1: + _h.trys.push([1, , 20, 22]); + thread = this.getThread(request.conversationId || request.previousResponseId); + // Register any tools provided in the request + if (request.tools && request.tools.length > 0) { + this.registerRequestTools(request.tools); + } + return [4 /*yield*/, __await(this.convertRequestToInput(request))]; + case 2: + input = _h.sent(); + return [4 /*yield*/, __await(thread.runStreamed(input, { + outputSchema: (_g = request.outputType) === null || _g === void 0 ? void 0 : _g.schema, + }))]; + case 3: + events = (_h.sent()).events; + textAccumulator = new Map(); + _h.label = 4; + case 4: + _h.trys.push([4, 13, 14, 19]); + _a = true, events_1 = __asyncValues(events); + _h.label = 5; + case 5: return [4 /*yield*/, __await(events_1.next())]; + case 6: + if (!(events_1_1 = _h.sent(), _d = events_1_1.done, !_d)) return [3 /*break*/, 12]; + _f = events_1_1.value; + _a = false; + event_1 = _f; + totalSize = 0; + for (_i = 0, _b = textAccumulator.values(); _i < _b.length; _i++) { + text = _b[_i]; + totalSize += text.length; + } + if (totalSize > MAX_ACCUMULATED_SIZE) { + throw new Error("Accumulated text exceeded maximum size limit (".concat(MAX_ACCUMULATED_SIZE, " bytes)")); + } + streamEvents = this.convertCodexEventToStreamEvent(event_1, textAccumulator); + _c = 0, streamEvents_1 = streamEvents; + _h.label = 7; + case 7: + if (!(_c < streamEvents_1.length)) return [3 /*break*/, 11]; + streamEvent = streamEvents_1[_c]; + return [4 /*yield*/, __await(streamEvent)]; + case 8: return [4 /*yield*/, _h.sent()]; + case 9: + _h.sent(); + _h.label = 10; + case 10: + _c++; + return [3 /*break*/, 7]; + case 11: + _a = true; + return [3 /*break*/, 5]; + case 12: return [3 /*break*/, 19]; + case 13: + e_1_1 = _h.sent(); + e_1 = { error: e_1_1 }; + return [3 /*break*/, 19]; + case 14: + _h.trys.push([14, , 17, 18]); + if (!(!_a && !_d && (_e = events_1.return))) return [3 /*break*/, 16]; + return [4 /*yield*/, __await(_e.call(events_1))]; + case 15: + _h.sent(); + _h.label = 16; + case 16: return [3 /*break*/, 18]; + case 17: + if (e_1) throw e_1.error; + return [7 /*endfinally*/]; + case 18: return [7 /*endfinally*/]; + case 19: return [3 /*break*/, 22]; + case 20: + // Clean up temporary image files + return [4 /*yield*/, __await(this.cleanupTempFiles())]; + case 21: + // Clean up temporary image files + _h.sent(); + return [7 /*endfinally*/]; + case 22: return [2 /*return*/]; + } + }); + }); + }; + /** + * Register tools from ModelRequest with the Codex instance + * + * Converts SerializedTool format (OpenAI Agents) to NativeToolDefinition format (Codex) + * and registers them with the Codex instance for bidirectional tool execution. + */ + CodexModel.prototype.registerRequestTools = function (tools) { + var _this = this; + for (var _i = 0, tools_1 = tools; _i < tools_1.length; _i++) { + var tool = tools_1[_i]; + if (tool.type !== "function") { + continue; + } + // Skip if already registered + if (this.registeredTools.has(tool.name)) { + continue; + } + try { + // Convert SerializedTool to NativeToolDefinition + var nativeToolDef = { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + // The handler is called when Codex wants to execute this tool + handler: function (invocation) { return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.executeToolViaFramework(invocation)]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); }, + }; + // Register the tool with Codex + this.codex.registerTool(nativeToolDef); + this.registeredTools.add(tool.name); + console.log("Registered tool with Codex: ".concat(tool.name)); + } + catch (error) { + var errorMessage = "Failed to register tool ".concat(tool.name, ": ").concat(error instanceof Error ? error.message : String(error)); + console.error(errorMessage); + // Don't throw - allow other tools to register even if one fails + // Individual tool failures shouldn't block the entire request + } + } + }; + /** + * Execute a tool via the OpenAI Agents framework + * + * This is the bridge between Codex's tool execution and the framework's tool handlers. + * + * FRAMEWORK INTEGRATION NOTE: + * This method currently returns a placeholder result because the actual execution + * requires integration with the OpenAI Agents framework's tool execution loop. + * + * In a full implementation, this would: + * 1. Emit a "tool_call_requested" event that the framework can listen to + * 2. Wait for the framework to execute the tool and provide the result + * 3. Return that result to Codex + * + * For now, this creates a promise that could be resolved by framework code, + * but the framework integration is not yet complete. + */ + CodexModel.prototype.executeToolViaFramework = function (invocation) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + console.log("Tool execution requested by Codex: ".concat(invocation.toolName, " (callId: ").concat(invocation.callId, ")")); + // FRAMEWORK INTEGRATION POINT: + // The framework would need to: + // 1. Listen for tool execution requests (e.g., via an event emitter) + // 2. Execute the tool using its own tool handlers + // 3. Resolve the pending promise with the result + // + // Example integration pattern (not implemented): + // ``` + // this.emit('tool_execution_requested', { + // toolName: invocation.toolName, + // callId: invocation.callId, + // arguments: invocation.arguments, + // onResult: (result: string) => resolve({ output: result, success: true }), + // onError: (error: string) => resolve({ error, success: false }) + // }); + // ``` + // For now, return a placeholder response indicating the tool was called + // but could not be executed without framework integration + return [2 /*return*/, { + output: JSON.stringify({ + message: "Tool execution via framework is not yet implemented", + toolName: invocation.toolName, + callId: invocation.callId, + arguments: invocation.arguments, + note: "This requires bidirectional communication between Codex and the OpenAI Agents framework. The framework needs to listen for tool execution requests and provide results back to Codex.", + }), + success: false, + error: "Framework integration not complete - tool execution requires the OpenAI Agents framework to handle the tool call and return results", + }]; + }); + }); + }; + /** + * Handle image input by converting to local file path + * Supports: base64 data URLs, HTTP(S) URLs, and file IDs (not yet implemented) + */ + CodexModel.prototype.handleImageInput = function (item) { + return __awaiter(this, void 0, void 0, function () { + var imageValue; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + imageValue = item.image; + if (!(typeof imageValue === "string")) return [3 /*break*/, 6]; + if (!imageValue.startsWith("data:image/")) return [3 /*break*/, 2]; + return [4 /*yield*/, this.saveBase64Image(imageValue)]; + case 1: return [2 /*return*/, _a.sent()]; + case 2: + if (!(imageValue.startsWith("http://") || imageValue.startsWith("https://"))) return [3 /*break*/, 4]; + return [4 /*yield*/, this.downloadImage(imageValue)]; + case 3: return [2 /*return*/, _a.sent()]; + case 4: + if (fs.existsSync(imageValue)) { + return [2 /*return*/, imageValue]; + } + // Invalid format + else { + throw new Error("Invalid image format: ".concat(imageValue.substring(0, 50), "...")); + } + _a.label = 5; + case 5: return [3 /*break*/, 9]; + case 6: + if (!(typeof imageValue === "object" && "url" in imageValue)) return [3 /*break*/, 8]; + return [4 /*yield*/, this.downloadImage(imageValue.url)]; + case 7: return [2 /*return*/, _a.sent()]; + case 8: + if (typeof imageValue === "object" && "fileId" in imageValue) { + throw new Error("Image fileId references are not yet supported. " + + "File IDs would need to be downloaded from the service first."); + } + _a.label = 9; + case 9: return [2 /*return*/, null]; + } + }); + }); + }; + /** + * Save base64-encoded image to temporary file + */ + CodexModel.prototype.saveBase64Image = function (dataUrl) { + return __awaiter(this, void 0, void 0, function () { + var matches, mediaType, base64Data, sanitizedBase64, normalizedBase64, buffer, reencoded, normalizedInput, extension, tempDir, filename, filepath; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + matches = dataUrl.match(/^data:image\/([^;]+);base64,(.+)$/); + if (!matches) { + throw new Error("Invalid base64 image data URL"); + } + mediaType = matches[1]; + base64Data = matches[2]; + if (!base64Data) { + throw new Error("Invalid base64 data in image URL"); + } + sanitizedBase64 = base64Data.replace(/\s/g, ""); + if (sanitizedBase64.length === 0) { + throw new Error("Invalid base64 data in image URL"); + } + if (!/^[A-Za-z0-9+/=_-]+$/.test(sanitizedBase64)) { + throw new Error("Invalid base64 data in image URL"); + } + normalizedBase64 = sanitizedBase64.replace(/-/g, "+").replace(/_/g, "/"); + try { + buffer = Buffer.from(normalizedBase64, "base64"); + } + catch (_b) { + throw new Error("Invalid base64 data in image URL"); + } + if (buffer.length === 0) { + throw new Error("Invalid base64 data in image URL"); + } + reencoded = buffer.toString("base64").replace(/=+$/, ""); + normalizedInput = normalizedBase64.replace(/=+$/, ""); + if (reencoded !== normalizedInput) { + throw new Error("Invalid base64 data in image URL"); + } + extension = this.getExtensionFromMediaType(mediaType, "png"); + tempDir = os.tmpdir(); + filename = "codex-image-".concat(Date.now(), ".").concat(extension); + filepath = path.join(tempDir, filename); + return [4 /*yield*/, fs.promises.writeFile(filepath, buffer)]; + case 1: + _a.sent(); + this.tempImageFiles.add(filepath); + return [2 /*return*/, filepath]; + } + }); + }); + }; + /** + * Download image from URL to temporary file + */ + CodexModel.prototype.downloadImage = function (url) { + return __awaiter(this, void 0, void 0, function () { + var response, buffer, contentType, mediaTypePart, mediaType, extension, tempDir, filename, filepath; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, fetch(url)]; + case 1: + response = _b.sent(); + if (!response.ok) { + throw new Error("Failed to download image from ".concat(url, ": ").concat(response.statusText)); + } + return [4 /*yield*/, response.arrayBuffer()]; + case 2: + buffer = _b.sent(); + contentType = response.headers.get("content-type") || "image/png"; + mediaTypePart = ((_a = contentType.split(";")[0]) === null || _a === void 0 ? void 0 : _a.trim()) || "image/png"; + mediaType = mediaTypePart.split("/")[1] || "png"; + extension = this.getExtensionFromMediaType(mediaType, "png"); + tempDir = os.tmpdir(); + filename = "codex-image-".concat(Date.now(), ".").concat(extension); + filepath = path.join(tempDir, filename); + return [4 /*yield*/, fs.promises.writeFile(filepath, Buffer.from(buffer))]; + case 3: + _b.sent(); + this.tempImageFiles.add(filepath); + return [2 /*return*/, filepath]; + } + }); + }); + }; + /** + * Convert media type to file extension + * Handles special cases like "jpeg" -> "jpg", "svg+xml" -> "svg" + */ + CodexModel.prototype.getExtensionFromMediaType = function (mediaType, defaultExt) { + if (!mediaType) { + return defaultExt; + } + // Normalize the media type + var normalized = mediaType.toLowerCase().trim(); + // Handle special cases + var extensionMap = { + "jpeg": "jpg", + "svg+xml": "svg", + "vnd.microsoft.icon": "ico", + "x-icon": "ico", + }; + // Check if we have a mapping for this media type + if (extensionMap[normalized]) { + return extensionMap[normalized]; + } + // For standard types like "png", "gif", "webp", "bmp", "tiff" + // Just use the media type as the extension + var simpleExtension = normalized.split("+")[0]; // Handle cases like "svg+xml" + // Validate it's a reasonable extension (alphanumeric only) + if (simpleExtension && /^[a-z0-9]+$/.test(simpleExtension)) { + return simpleExtension; + } + // Fall back to default if we can't determine a valid extension + return defaultExt; + }; + CodexModel.prototype.convertRequestToInput = function (request) { + return __awaiter(this, void 0, void 0, function () { + var parts, _i, _a, item, imagePath, result, refusal; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + parts = []; + // Add system instructions as a text preamble if provided + if (request.systemInstructions) { + parts.push({ + type: "text", + text: "\n".concat(request.systemInstructions, "\n\n\n"), + }); + } + if (!(typeof request.input === "string")) return [3 /*break*/, 1]; + parts.push({ type: "text", text: request.input }); + return [3 /*break*/, 7]; + case 1: + _i = 0, _a = request.input; + _b.label = 2; + case 2: + if (!(_i < _a.length)) return [3 /*break*/, 7]; + item = _a[_i]; + if (!(item.type === "input_text")) return [3 /*break*/, 3]; + parts.push({ type: "text", text: item.text }); + return [3 /*break*/, 6]; + case 3: + if (!(item.type === "input_image")) return [3 /*break*/, 5]; + return [4 /*yield*/, this.handleImageInput(item)]; + case 4: + imagePath = _b.sent(); + if (imagePath) { + parts.push({ type: "local_image", path: imagePath }); + } + return [3 /*break*/, 6]; + case 5: + if (item.type === "input_file") { + // Files could potentially be handled similarly to images + // For now, throw an error as we'd need to handle different file types + throw new Error("CodexProvider does not yet support input_file type. " + + "File handling needs to be implemented based on file type and format."); + } + else if (item.type === "input_audio") { + throw new Error("CodexProvider does not yet support input_audio type. " + + "Audio handling needs to be implemented."); + } + else if (item.type === "function_call_result") { + result = item; + parts.push({ + type: "text", + text: "[Tool ".concat(result.name, " returned: ").concat(result.result, "]") + }); + } + else if (item.type === "input_refusal") { + refusal = item; + parts.push({ + type: "text", + text: "[Refusal: ".concat(refusal.refusal, "]") + }); + } + _b.label = 6; + case 6: + _i++; + return [3 /*break*/, 2]; + case 7: + // If only one text part, return as string + if (parts.length === 1 && parts[0].type === "text") { + return [2 /*return*/, parts[0].text]; + } + return [2 /*return*/, parts]; + } + }); + }); + }; + /** + * Convert Codex Usage to ModelResponse Usage + */ + CodexModel.prototype.convertUsage = function (usage) { + if (!usage) { + return { requests: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0 }; + } + var inputTokensDetails = usage.cached_input_tokens + ? [{ cachedTokens: usage.cached_input_tokens }] + : undefined; + var converted = { + requests: 1, + inputTokens: usage.input_tokens, + outputTokens: usage.output_tokens, + totalTokens: usage.input_tokens + usage.output_tokens, + }; + if (inputTokensDetails) { + converted.inputTokensDetails = inputTokensDetails; + } + return converted; + }; + /** + * Convert Codex ThreadItems to AgentOutputItems + */ + CodexModel.prototype.convertItemsToOutput = function (items, finalResponse) { + var output = []; + for (var _i = 0, items_1 = items; _i < items_1.length; _i++) { + var item = items_1[_i]; + switch (item.type) { + case "agent_message": { + var content = [ + { + type: "output_text", + text: item.text, + }, + ]; + output.push({ + type: "message", + role: "assistant", + status: "completed", + content: content, + }); + break; + } + case "reasoning": { + output.push({ + type: "reasoning", + reasoning: item.text, + }); + break; + } + // Codex handles tools internally, so we don't expose them as function calls + // The results are already incorporated into the agent_message + case "command_execution": + case "file_change": + case "mcp_tool_call": + // Skip - these are internal to Codex + break; + default: + // Unknown item type - skip + break; + } + } + // If no items were converted, add the final response as a message + if (output.length === 0 && finalResponse) { + output.push({ + type: "message", + role: "assistant", + status: "completed", + content: [ + { + type: "output_text", + text: finalResponse, + }, + ], + }); + } + return output; + }; + /** + * Convert Codex ThreadEvent to OpenAI Agents StreamEvent + */ + CodexModel.prototype.convertCodexEventToStreamEvent = function (event, textAccumulator) { + var _a; + var events = []; + switch (event.type) { + case "thread.started": + events.push({ type: "response_started" }); + break; + case "turn.started": + // No equivalent in StreamEvent - skip + break; + case "item.started": + // Initialize accumulator for this item + if (event.item.type === "agent_message" || event.item.type === "reasoning") { + var itemKey = "".concat(event.item.type); + textAccumulator.set(itemKey, ""); + } + break; + case "item.updated": + // Emit delta events for incremental text updates + if (event.item.type === "agent_message") { + var itemKey = "agent_message"; + var previousText = textAccumulator.get(itemKey) || ""; + var currentText = event.item.text; + // Validate: current text should be longer than previous (no backwards updates) + if (currentText.length < previousText.length) { + console.warn("Received backwards update for text - ignoring delta"); + break; + } + if (currentText.length > previousText.length) { + var delta = currentText.slice(previousText.length); + textAccumulator.set(itemKey, currentText); + events.push({ + type: "output_text_delta", + delta: delta, + }); + } + } + else if (event.item.type === "reasoning") { + var itemKey = "reasoning"; + var previousText = textAccumulator.get(itemKey) || ""; + var currentText = event.item.text; + if (currentText.length > previousText.length) { + var delta = currentText.slice(previousText.length); + textAccumulator.set(itemKey, currentText); + events.push({ + type: "reasoning_delta", + delta: delta, + }); + } + } + break; + case "item.completed": + if (event.item.type === "agent_message") { + // Emit final text done event + events.push({ + type: "output_text_done", + text: event.item.text, + }); + textAccumulator.delete("agent_message"); + } + else if (event.item.type === "reasoning") { + events.push({ + type: "reasoning_done", + reasoning: event.item.text, + }); + textAccumulator.delete("reasoning"); + } + break; + case "turn.completed": + // Emit response done with full response + events.push({ + type: "response_done", + response: { + usage: this.convertUsage(event.usage), + output: [], // Items were already emitted as deltas + responseId: ((_a = this.thread) === null || _a === void 0 ? void 0 : _a.id) || undefined, + }, + }); + break; + case "turn.failed": + events.push({ + type: "error", + error: { + message: event.error.message, + }, + }); + break; + case "error": + events.push({ + type: "error", + error: { + message: event.message, + }, + }); + break; + default: + // Unknown event type - skip + break; + } + return events; + }; + return CodexModel; +}()); diff --git a/sdk/native/dist-examples/src/agents/index.js b/sdk/native/dist-examples/src/agents/index.js new file mode 100644 index 000000000..586a365d7 --- /dev/null +++ b/sdk/native/dist-examples/src/agents/index.js @@ -0,0 +1,32 @@ +"use strict"; +/** + * OpenAI Agents framework integration for Codex + * + * This module provides a ModelProvider implementation that allows using Codex + * as the backend for the OpenAI Agents JS framework. + * + * @example + * ```typescript + * import { CodexProvider } from '@openai/codex-native/agents'; + * import { Agent, Runner } from '@openai/agents'; + * + * const provider = new CodexProvider({ + * defaultModel: 'gpt-5' + * }); + * + * const agent = new Agent({ + * name: 'CodeAssistant', + * instructions: 'You are a helpful coding assistant' + * }); + * + * const runner = new Runner({ modelProvider: provider }); + * const result = await runner.run(agent, 'Fix the failing tests'); + * console.log(result.finalOutput); + * ``` + * + * @module agents + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CodexProvider = void 0; +var CodexProvider_1 = require("./CodexProvider"); +Object.defineProperty(exports, "CodexProvider", { enumerable: true, get: function () { return CodexProvider_1.CodexProvider; } }); diff --git a/sdk/native/dist-examples/src/agents/types.js b/sdk/native/dist-examples/src/agents/types.js new file mode 100644 index 000000000..36a8aedb7 --- /dev/null +++ b/sdk/native/dist-examples/src/agents/types.js @@ -0,0 +1,6 @@ +"use strict"; +/** + * Type definitions for OpenAI Agents JS framework compatibility + * Based on @openai/agents-core package + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/sdk/native/dist-examples/src/codex.js b/sdk/native/dist-examples/src/codex.js new file mode 100644 index 000000000..b453ef749 --- /dev/null +++ b/sdk/native/dist-examples/src/codex.js @@ -0,0 +1,349 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +var __asyncValues = (this && this.__asyncValues) || function (o) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var m = o[Symbol.asyncIterator], i; + return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); + function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } + function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } +}; +var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } +var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var g = generator.apply(thisArg, _arguments || []), i, q = []; + return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i; + function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; } + function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } } + function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } + function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } + function fulfill(value) { resume("next", value); } + function reject(value) { resume("throw", value); } + function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Codex = void 0; +var exec_1 = require("./exec"); +var nativeBinding_1 = require("./nativeBinding"); +var thread_1 = require("./thread"); +var outputSchemaFile_1 = require("./outputSchemaFile"); +var reviewOptions_1 = require("./reviewOptions"); +/** + * Codex is the main class for interacting with the Codex agent. + * + * This is the native NAPI-based implementation that uses Rust bindings directly. + * + * Use the `startThread()` method to start a new thread or `resumeThread()` to resume a previously started thread. + */ +var Codex = /** @class */ (function () { + function Codex(options) { + if (options === void 0) { options = {}; } + var predefinedTools = options.tools ? __spreadArray([], options.tools, true) : []; + this.nativeBinding = (0, nativeBinding_1.getNativeBinding)(); + this.options = __assign(__assign({}, options), { tools: [] }); + if (this.nativeBinding) { + // clearRegisteredTools may not be available in all builds + if (typeof this.nativeBinding.clearRegisteredTools === 'function') { + this.nativeBinding.clearRegisteredTools(); + } + for (var _i = 0, predefinedTools_1 = predefinedTools; _i < predefinedTools_1.length; _i++) { + var tool = predefinedTools_1[_i]; + this.registerTool(tool); + } + } + this.exec = new exec_1.CodexExec(); + } + Codex.prototype.registerTool = function (tool) { + if (!this.nativeBinding) { + throw new Error("Native tool registration requires the NAPI binding"); + } + // registerTool may not be available in all builds + if (typeof this.nativeBinding.registerTool !== 'function') { + console.warn("registerTool is not available in this build - tools feature may be incomplete"); + return; + } + var handler = tool.handler, info = __rest(tool, ["handler"]); + this.nativeBinding.registerTool(info, handler); + if (!this.options.tools) { + this.options.tools = []; + } + this.options.tools.push(tool); + }; + /** + * Starts a new conversation with an agent. + * @returns A new thread instance. + */ + Codex.prototype.startThread = function (options) { + if (options === void 0) { options = {}; } + return new thread_1.Thread(this.exec, this.options, options); + }; + /** + * Resumes a conversation with an agent based on the thread id. + * Threads are persisted in ~/.codex/sessions. + * + * @param id The id of the thread to resume. + * @returns A new thread instance. + */ + Codex.prototype.resumeThread = function (id, options) { + if (options === void 0) { options = {}; } + return new thread_1.Thread(this.exec, this.options, options, id); + }; + /** + * Starts a review task using the built-in Codex review flow. + */ + Codex.prototype.review = function (options) { + return __awaiter(this, void 0, void 0, function () { + var generator, items, finalResponse, usage, turnFailure, _loop_1, _a, generator_1, generator_1_1, state_1, e_1_1; + var _b, e_1, _c, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + generator = this.reviewStreamedInternal(options); + items = []; + finalResponse = ""; + usage = null; + turnFailure = null; + _e.label = 1; + case 1: + _e.trys.push([1, 6, 7, 12]); + _loop_1 = function () { + _d = generator_1_1.value; + _a = false; + var event_1 = _d; + if (event_1.type === "item.completed") { + if (event_1.item.type === "agent_message") { + finalResponse = event_1.item.text; + } + items.push(event_1.item); + } + else if (event_1.type === "exited_review_mode") { + // Capture the structured review output + if (event_1.review_output) { + var reviewOutput = event_1.review_output; + var reviewText_1 = ""; + // Add overall explanation + if (reviewOutput.overall_explanation) { + reviewText_1 += reviewOutput.overall_explanation; + } + // Add findings if present + if (reviewOutput.findings && reviewOutput.findings.length > 0) { + if (reviewText_1) + reviewText_1 += "\n\n"; + reviewText_1 += "## Review Findings\n\n"; + reviewOutput.findings.forEach(function (finding, index) { + reviewText_1 += "### ".concat(index + 1, ". ").concat(finding.title, "\n"); + reviewText_1 += "".concat(finding.body, "\n"); + reviewText_1 += "**Priority:** ".concat(finding.priority, " | **Confidence:** ").concat(finding.confidence_score, "\n"); + reviewText_1 += "**Location:** ".concat(finding.code_location.absolute_file_path, ":").concat(finding.code_location.line_range.start, "-").concat(finding.code_location.line_range.end, "\n\n"); + }); + } + finalResponse = reviewText_1; + } + } + else if (event_1.type === "turn.completed") { + usage = event_1.usage; + } + else if (event_1.type === "turn.failed") { + turnFailure = event_1.error; + return "break"; + } + }; + _a = true, generator_1 = __asyncValues(generator); + _e.label = 2; + case 2: return [4 /*yield*/, generator_1.next()]; + case 3: + if (!(generator_1_1 = _e.sent(), _b = generator_1_1.done, !_b)) return [3 /*break*/, 5]; + state_1 = _loop_1(); + if (state_1 === "break") + return [3 /*break*/, 5]; + _e.label = 4; + case 4: + _a = true; + return [3 /*break*/, 2]; + case 5: return [3 /*break*/, 12]; + case 6: + e_1_1 = _e.sent(); + e_1 = { error: e_1_1 }; + return [3 /*break*/, 12]; + case 7: + _e.trys.push([7, , 10, 11]); + if (!(!_a && !_b && (_c = generator_1.return))) return [3 /*break*/, 9]; + return [4 /*yield*/, _c.call(generator_1)]; + case 8: + _e.sent(); + _e.label = 9; + case 9: return [3 /*break*/, 11]; + case 10: + if (e_1) throw e_1.error; + return [7 /*endfinally*/]; + case 11: return [7 /*endfinally*/]; + case 12: + if (turnFailure) { + throw new Error(turnFailure.message); + } + return [2 /*return*/, { items: items, finalResponse: finalResponse, usage: usage }]; + } + }); + }); + }; + /** + * Starts a review task and returns the event stream. + */ + Codex.prototype.reviewStreamed = function (options) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, { events: this.reviewStreamedInternal(options) }]; + }); + }); + }; + Codex.prototype.reviewStreamedInternal = function (options) { + return __asyncGenerator(this, arguments, function reviewStreamedInternal_1() { + var target, _a, threadOptions, _b, turnOptions, _c, prompt, hint, _d, schemaPath, cleanup, generator, _e, generator_2, generator_2_1, item, parsed, e_2_1; + var _f, e_2, _g, _h; + return __generator(this, function (_j) { + switch (_j.label) { + case 0: + target = options.target, _a = options.threadOptions, threadOptions = _a === void 0 ? {} : _a, _b = options.turnOptions, turnOptions = _b === void 0 ? {} : _b; + _c = (0, reviewOptions_1.buildReviewPrompt)(target), prompt = _c.prompt, hint = _c.hint; + return [4 /*yield*/, __await((0, outputSchemaFile_1.createOutputSchemaFile)(turnOptions.outputSchema))]; + case 1: + _d = _j.sent(), schemaPath = _d.schemaPath, cleanup = _d.cleanup; + generator = this.exec.run({ + input: prompt, + baseUrl: this.options.baseUrl, + apiKey: this.options.apiKey, + model: threadOptions.model, + sandboxMode: threadOptions.sandboxMode, + workingDirectory: threadOptions.workingDirectory, + skipGitRepoCheck: threadOptions.skipGitRepoCheck, + outputSchemaFile: schemaPath, + outputSchema: turnOptions.outputSchema, + fullAuto: threadOptions.fullAuto, + review: { + userFacingHint: hint, + }, + }); + _j.label = 2; + case 2: + _j.trys.push([2, , 17, 19]); + _j.label = 3; + case 3: + _j.trys.push([3, 10, 11, 16]); + _e = true, generator_2 = __asyncValues(generator); + _j.label = 4; + case 4: return [4 /*yield*/, __await(generator_2.next())]; + case 5: + if (!(generator_2_1 = _j.sent(), _f = generator_2_1.done, !_f)) return [3 /*break*/, 9]; + _h = generator_2_1.value; + _e = false; + item = _h; + parsed = void 0; + try { + parsed = JSON.parse(item); + } + catch (error) { + throw new Error("Failed to parse item: ".concat(item), { cause: error }); + } + return [4 /*yield*/, __await(parsed)]; + case 6: return [4 /*yield*/, _j.sent()]; + case 7: + _j.sent(); + _j.label = 8; + case 8: + _e = true; + return [3 /*break*/, 4]; + case 9: return [3 /*break*/, 16]; + case 10: + e_2_1 = _j.sent(); + e_2 = { error: e_2_1 }; + return [3 /*break*/, 16]; + case 11: + _j.trys.push([11, , 14, 15]); + if (!(!_e && !_f && (_g = generator_2.return))) return [3 /*break*/, 13]; + return [4 /*yield*/, __await(_g.call(generator_2))]; + case 12: + _j.sent(); + _j.label = 13; + case 13: return [3 /*break*/, 15]; + case 14: + if (e_2) throw e_2.error; + return [7 /*endfinally*/]; + case 15: return [7 /*endfinally*/]; + case 16: return [3 /*break*/, 19]; + case 17: return [4 /*yield*/, __await(cleanup())]; + case 18: + _j.sent(); + return [7 /*endfinally*/]; + case 19: return [2 /*return*/]; + } + }); + }); + }; + return Codex; +}()); +exports.Codex = Codex; diff --git a/sdk/native/dist-examples/src/codexOptions.js b/sdk/native/dist-examples/src/codexOptions.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/sdk/native/dist-examples/src/codexOptions.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/sdk/native/dist-examples/src/events.js b/sdk/native/dist-examples/src/events.js new file mode 100644 index 000000000..c67687337 --- /dev/null +++ b/sdk/native/dist-examples/src/events.js @@ -0,0 +1,3 @@ +"use strict"; +// based on event types from codex-rs/exec/src/exec_events.rs +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/sdk/native/dist-examples/src/exec.js b/sdk/native/dist-examples/src/exec.js new file mode 100644 index 000000000..1a1aa43fe --- /dev/null +++ b/sdk/native/dist-examples/src/exec.js @@ -0,0 +1,249 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __asyncValues = (this && this.__asyncValues) || function (o) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var m = o[Symbol.asyncIterator], i; + return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); + function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } + function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } +}; +var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } +var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var g = generator.apply(thisArg, _arguments || []), i, q = []; + return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i; + function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; } + function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } } + function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } + function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } + function fulfill(value) { resume("next", value); } + function reject(value) { resume("throw", value); } + function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CodexExec = void 0; +var nativeBinding_1 = require("./nativeBinding"); +/** + * CodexExec for the native package - uses NAPI bindings exclusively. + * No CLI fallback. + */ +var CodexExec = /** @class */ (function () { + function CodexExec() { + var nativeBinding = (0, nativeBinding_1.getNativeBinding)(); + if (!nativeBinding) { + throw new Error("Native NAPI binding not available. Make sure @openai/codex-native is properly installed and built."); + } + this.native = nativeBinding; + } + CodexExec.prototype.run = function (args) { + return __asyncGenerator(this, arguments, function run_1() { + var binding, queue, request, runPromise, loopError, _a, queue_1, queue_1_1, value, e_1_1, error_1; + var _b, e_1, _c, _d; + var _e, _f; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + binding = this.native; + queue = new AsyncQueue(); + request = { + prompt: args.input, + threadId: (_e = args.threadId) !== null && _e !== void 0 ? _e : undefined, + images: args.images && args.images.length > 0 ? args.images : undefined, + model: args.model, + sandboxMode: args.sandboxMode, + workingDirectory: args.workingDirectory, + skipGitRepoCheck: args.skipGitRepoCheck, + outputSchema: args.outputSchema, + baseUrl: args.baseUrl, + apiKey: args.apiKey, + fullAuto: args.fullAuto, + reviewMode: args.review ? true : undefined, + reviewHint: (_f = args.review) === null || _f === void 0 ? void 0 : _f.userFacingHint, + }; + try { + runPromise = binding + .runThreadStream(request, function (err, eventJson) { + if (err) { + queue.fail(err); + return; + } + try { + queue.push(eventJson !== null && eventJson !== void 0 ? eventJson : "null"); + } + catch (error) { + queue.fail(error); + } + }) + .then(function () { + queue.end(); + }, function (error) { + queue.fail(error); + }); + } + catch (error) { + queue.fail(error); + throw error; + } + _g.label = 1; + case 1: + _g.trys.push([1, 17, 18, 21]); + _g.label = 2; + case 2: + _g.trys.push([2, 9, 10, 15]); + _a = true, queue_1 = __asyncValues(queue); + _g.label = 3; + case 3: return [4 /*yield*/, __await(queue_1.next())]; + case 4: + if (!(queue_1_1 = _g.sent(), _b = queue_1_1.done, !_b)) return [3 /*break*/, 8]; + _d = queue_1_1.value; + _a = false; + value = _d; + return [4 /*yield*/, __await(value)]; + case 5: return [4 /*yield*/, _g.sent()]; + case 6: + _g.sent(); + _g.label = 7; + case 7: + _a = true; + return [3 /*break*/, 3]; + case 8: return [3 /*break*/, 15]; + case 9: + e_1_1 = _g.sent(); + e_1 = { error: e_1_1 }; + return [3 /*break*/, 15]; + case 10: + _g.trys.push([10, , 13, 14]); + if (!(!_a && !_b && (_c = queue_1.return))) return [3 /*break*/, 12]; + return [4 /*yield*/, __await(_c.call(queue_1))]; + case 11: + _g.sent(); + _g.label = 12; + case 12: return [3 /*break*/, 14]; + case 13: + if (e_1) throw e_1.error; + return [7 /*endfinally*/]; + case 14: return [7 /*endfinally*/]; + case 15: return [4 /*yield*/, __await(runPromise)]; + case 16: + _g.sent(); + return [3 /*break*/, 21]; + case 17: + error_1 = _g.sent(); + loopError = error_1; + throw error_1; + case 18: + queue.end(); + if (!loopError) return [3 /*break*/, 20]; + return [4 /*yield*/, __await(runPromise.catch(function () { }))]; + case 19: + _g.sent(); + _g.label = 20; + case 20: return [7 /*endfinally*/]; + case 21: return [2 /*return*/]; + } + }); + }); + }; + return CodexExec; +}()); +exports.CodexExec = CodexExec; +var AsyncQueue = /** @class */ (function () { + function AsyncQueue() { + this.buffer = []; + this.waiters = []; + this.ended = false; + } + AsyncQueue.prototype.push = function (value) { + if (this.ended) + return; + if (this.waiters.length > 0) { + var waiter = this.waiters.shift(); + waiter.resolve({ value: value, done: false }); + return; + } + this.buffer.push(value); + }; + AsyncQueue.prototype.end = function () { + if (this.ended) + return; + this.ended = true; + var waiters = this.waiters; + this.waiters = []; + for (var _i = 0, waiters_1 = waiters; _i < waiters_1.length; _i++) { + var waiter = waiters_1[_i]; + waiter.resolve({ value: undefined, done: true }); + } + }; + AsyncQueue.prototype.fail = function (error) { + if (this.ended) + return; + this.error = error; + this.ended = true; + var waiters = this.waiters; + this.waiters = []; + for (var _i = 0, waiters_2 = waiters; _i < waiters_2.length; _i++) { + var waiter = waiters_2[_i]; + waiter.reject(error); + } + }; + AsyncQueue.prototype.next = function () { + return __awaiter(this, void 0, void 0, function () { + var value; + var _this = this; + return __generator(this, function (_a) { + if (this.buffer.length > 0) { + value = this.buffer.shift(); + return [2 /*return*/, { value: value, done: false }]; + } + if (this.error) { + return [2 /*return*/, Promise.reject(this.error)]; + } + if (this.ended) { + return [2 /*return*/, { value: undefined, done: true }]; + } + return [2 /*return*/, new Promise(function (resolve, reject) { + _this.waiters.push({ resolve: resolve, reject: reject }); + })]; + }); + }); + }; + AsyncQueue.prototype[Symbol.asyncIterator] = function () { + return this; + }; + return AsyncQueue; +}()); diff --git a/sdk/native/dist-examples/src/index.js b/sdk/native/dist-examples/src/index.js new file mode 100644 index 000000000..08c1a66bc --- /dev/null +++ b/sdk/native/dist-examples/src/index.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CodexProvider = exports.Codex = exports.Thread = void 0; +var thread_1 = require("./thread"); +Object.defineProperty(exports, "Thread", { enumerable: true, get: function () { return thread_1.Thread; } }); +var codex_1 = require("./codex"); +Object.defineProperty(exports, "Codex", { enumerable: true, get: function () { return codex_1.Codex; } }); +// OpenAI Agents framework integration +var agents_1 = require("./agents"); +Object.defineProperty(exports, "CodexProvider", { enumerable: true, get: function () { return agents_1.CodexProvider; } }); diff --git a/sdk/native/dist-examples/src/items.js b/sdk/native/dist-examples/src/items.js new file mode 100644 index 000000000..8ad255fa5 --- /dev/null +++ b/sdk/native/dist-examples/src/items.js @@ -0,0 +1,3 @@ +"use strict"; +// based on item types from codex-rs/exec/src/exec_events.rs +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/sdk/native/dist-examples/src/nativeBinding.js b/sdk/native/dist-examples/src/nativeBinding.js new file mode 100644 index 000000000..0fd26aa3a --- /dev/null +++ b/sdk/native/dist-examples/src/nativeBinding.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getNativeBinding = getNativeBinding; +var node_module_1 = require("node:module"); +var cachedBinding; +function getNativeBinding() { + if (cachedBinding !== undefined) { + return cachedBinding; + } + var require = (0, node_module_1.createRequire)(import.meta.url); + // For sdk/native: load the NAPI binding from the package root + // The index.js is auto-generated by napi-rs and loads the .node file + try { + var binding = require("../index.js"); + cachedBinding = binding; + return cachedBinding; + } + catch (error) { + console.warn("Failed to load native NAPI binding:", error); + cachedBinding = null; + return cachedBinding; + } +} diff --git a/sdk/native/dist-examples/src/outputSchemaFile.js b/sdk/native/dist-examples/src/outputSchemaFile.js new file mode 100644 index 000000000..54d1c1972 --- /dev/null +++ b/sdk/native/dist-examples/src/outputSchemaFile.js @@ -0,0 +1,111 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createOutputSchemaFile = createOutputSchemaFile; +var node_fs_1 = require("node:fs"); +var node_os_1 = require("node:os"); +var node_path_1 = require("node:path"); +function createOutputSchemaFile(schema) { + return __awaiter(this, void 0, void 0, function () { + var normalizedSchema, schemaDir, schemaPath, cleanup, error_1; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (schema === undefined) { + return [2 /*return*/, { cleanup: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/]; + }); }); } }]; + } + if (!isJsonObject(schema)) { + throw new Error("outputSchema must be a plain JSON object"); + } + normalizedSchema = __assign(__assign({}, schema), { additionalProperties: schema.additionalProperties !== undefined ? schema.additionalProperties : false }); + return [4 /*yield*/, node_fs_1.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "codex-output-schema-"))]; + case 1: + schemaDir = _a.sent(); + schemaPath = node_path_1.default.join(schemaDir, "schema.json"); + cleanup = function () { return __awaiter(_this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, node_fs_1.promises.rm(schemaDir, { recursive: true, force: true })]; + case 1: + _b.sent(); + return [3 /*break*/, 3]; + case 2: + _a = _b.sent(); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }; + _a.label = 2; + case 2: + _a.trys.push([2, 4, , 6]); + return [4 /*yield*/, node_fs_1.promises.writeFile(schemaPath, JSON.stringify(normalizedSchema), "utf8")]; + case 3: + _a.sent(); + return [2 /*return*/, { schemaPath: schemaPath, cleanup: cleanup }]; + case 4: + error_1 = _a.sent(); + return [4 /*yield*/, cleanup()]; + case 5: + _a.sent(); + throw error_1; + case 6: return [2 /*return*/]; + } + }); + }); +} +function isJsonObject(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/sdk/native/dist-examples/src/reviewOptions.js b/sdk/native/dist-examples/src/reviewOptions.js new file mode 100644 index 000000000..b27c20cf3 --- /dev/null +++ b/sdk/native/dist-examples/src/reviewOptions.js @@ -0,0 +1,40 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.buildReviewPrompt = buildReviewPrompt; +function buildReviewPrompt(target) { + var _a, _b; + switch (target.type) { + case "current_changes": + return { + prompt: "Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.", + hint: "current changes", + }; + case "branch": { + var branch = target.baseBranch; + var prompt_1 = "Review the code changes against the base branch '".concat(branch, "'. Start by finding the merge diff between the current branch and ").concat(branch, "'s upstream e.g. (`git merge-base HEAD \"$(git rev-parse --abbrev-ref \"").concat(branch, "@{upstream}\")\"`), then run `git diff` against that SHA to see what changes we would merge into the ").concat(branch, " branch. Provide prioritized, actionable findings."); + return { + prompt: prompt_1, + hint: "changes against '".concat(branch, "'"), + }; + } + case "commit": { + var shortSha = target.sha.slice(0, 7); + var subject = (_a = target.subject) !== null && _a !== void 0 ? _a : target.sha; + return { + prompt: "Review the code changes introduced by commit ".concat(target.sha, " (\"").concat(subject, "\"). Provide prioritized, actionable findings."), + hint: "commit ".concat(shortSha), + }; + } + case "custom": { + var hint = (_b = target.hint) !== null && _b !== void 0 ? _b : "custom review"; + return { + prompt: target.prompt, + hint: hint, + }; + } + default: { + var exhaustive = target; + throw new Error("Unsupported review target: ".concat(String(exhaustive))); + } + } +} diff --git a/sdk/native/dist-examples/src/thread.js b/sdk/native/dist-examples/src/thread.js new file mode 100644 index 000000000..4dc882380 --- /dev/null +++ b/sdk/native/dist-examples/src/thread.js @@ -0,0 +1,260 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } +var __asyncValues = (this && this.__asyncValues) || function (o) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var m = o[Symbol.asyncIterator], i; + return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); + function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } + function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } +}; +var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var g = generator.apply(thisArg, _arguments || []), i, q = []; + return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i; + function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; } + function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } } + function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } + function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } + function fulfill(value) { resume("next", value); } + function reject(value) { resume("throw", value); } + function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Thread = void 0; +var outputSchemaFile_1 = require("./outputSchemaFile"); +/** Respesent a thread of conversation with the agent. One thread can have multiple consecutive turns. */ +var Thread = /** @class */ (function () { + /* @internal */ + function Thread(exec, options, threadOptions, id) { + if (id === void 0) { id = null; } + this._exec = exec; + this._options = options; + this._id = id; + this._threadOptions = threadOptions; + } + Object.defineProperty(Thread.prototype, "id", { + /** Returns the ID of the thread. Populated after the first turn starts. */ + get: function () { + return this._id; + }, + enumerable: false, + configurable: true + }); + /** Provides the input to the agent and streams events as they are produced during the turn. */ + Thread.prototype.runStreamed = function (input_1) { + return __awaiter(this, arguments, void 0, function (input, turnOptions) { + if (turnOptions === void 0) { turnOptions = {}; } + return __generator(this, function (_a) { + return [2 /*return*/, { events: this.runStreamedInternal(input, turnOptions) }]; + }); + }); + }; + Thread.prototype.runStreamedInternal = function (input_1) { + return __asyncGenerator(this, arguments, function runStreamedInternal_1(input, turnOptions) { + var _a, schemaPath, cleanup, options, _b, prompt, images, generator, _c, generator_1, generator_1_1, item, parsed, e_1_1; + var _d, e_1, _e, _f; + if (turnOptions === void 0) { turnOptions = {}; } + return __generator(this, function (_g) { + switch (_g.label) { + case 0: return [4 /*yield*/, __await((0, outputSchemaFile_1.createOutputSchemaFile)(turnOptions.outputSchema))]; + case 1: + _a = _g.sent(), schemaPath = _a.schemaPath, cleanup = _a.cleanup; + options = this._threadOptions; + _b = normalizeInput(input), prompt = _b.prompt, images = _b.images; + generator = this._exec.run({ + input: prompt, + baseUrl: this._options.baseUrl, + apiKey: this._options.apiKey, + threadId: this._id, + images: images, + model: options === null || options === void 0 ? void 0 : options.model, + sandboxMode: options === null || options === void 0 ? void 0 : options.sandboxMode, + workingDirectory: options === null || options === void 0 ? void 0 : options.workingDirectory, + skipGitRepoCheck: options === null || options === void 0 ? void 0 : options.skipGitRepoCheck, + outputSchemaFile: schemaPath, + outputSchema: turnOptions.outputSchema, + fullAuto: options === null || options === void 0 ? void 0 : options.fullAuto, + }); + _g.label = 2; + case 2: + _g.trys.push([2, , 17, 19]); + _g.label = 3; + case 3: + _g.trys.push([3, 10, 11, 16]); + _c = true, generator_1 = __asyncValues(generator); + _g.label = 4; + case 4: return [4 /*yield*/, __await(generator_1.next())]; + case 5: + if (!(generator_1_1 = _g.sent(), _d = generator_1_1.done, !_d)) return [3 /*break*/, 9]; + _f = generator_1_1.value; + _c = false; + item = _f; + parsed = void 0; + try { + parsed = JSON.parse(item); + } + catch (error) { + throw new Error("Failed to parse item: ".concat(item), { cause: error }); + } + if (parsed.type === "thread.started") { + this._id = parsed.thread_id; + } + return [4 /*yield*/, __await(parsed)]; + case 6: return [4 /*yield*/, _g.sent()]; + case 7: + _g.sent(); + _g.label = 8; + case 8: + _c = true; + return [3 /*break*/, 4]; + case 9: return [3 /*break*/, 16]; + case 10: + e_1_1 = _g.sent(); + e_1 = { error: e_1_1 }; + return [3 /*break*/, 16]; + case 11: + _g.trys.push([11, , 14, 15]); + if (!(!_c && !_d && (_e = generator_1.return))) return [3 /*break*/, 13]; + return [4 /*yield*/, __await(_e.call(generator_1))]; + case 12: + _g.sent(); + _g.label = 13; + case 13: return [3 /*break*/, 15]; + case 14: + if (e_1) throw e_1.error; + return [7 /*endfinally*/]; + case 15: return [7 /*endfinally*/]; + case 16: return [3 /*break*/, 19]; + case 17: return [4 /*yield*/, __await(cleanup())]; + case 18: + _g.sent(); + return [7 /*endfinally*/]; + case 19: return [2 /*return*/]; + } + }); + }); + }; + /** Provides the input to the agent and returns the completed turn. */ + Thread.prototype.run = function (input_1) { + return __awaiter(this, arguments, void 0, function (input, turnOptions) { + var generator, items, finalResponse, usage, turnFailure, _a, generator_2, generator_2_1, event_1, e_2_1; + var _b, e_2, _c, _d; + if (turnOptions === void 0) { turnOptions = {}; } + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + generator = this.runStreamedInternal(input, turnOptions); + items = []; + finalResponse = ""; + usage = null; + turnFailure = null; + _e.label = 1; + case 1: + _e.trys.push([1, 6, 7, 12]); + _a = true, generator_2 = __asyncValues(generator); + _e.label = 2; + case 2: return [4 /*yield*/, generator_2.next()]; + case 3: + if (!(generator_2_1 = _e.sent(), _b = generator_2_1.done, !_b)) return [3 /*break*/, 5]; + _d = generator_2_1.value; + _a = false; + event_1 = _d; + if (event_1.type === "item.completed") { + if (event_1.item.type === "agent_message") { + finalResponse = event_1.item.text; + } + items.push(event_1.item); + } + else if (event_1.type === "turn.completed") { + usage = event_1.usage; + } + else if (event_1.type === "turn.failed") { + turnFailure = event_1.error; + return [3 /*break*/, 5]; + } + _e.label = 4; + case 4: + _a = true; + return [3 /*break*/, 2]; + case 5: return [3 /*break*/, 12]; + case 6: + e_2_1 = _e.sent(); + e_2 = { error: e_2_1 }; + return [3 /*break*/, 12]; + case 7: + _e.trys.push([7, , 10, 11]); + if (!(!_a && !_b && (_c = generator_2.return))) return [3 /*break*/, 9]; + return [4 /*yield*/, _c.call(generator_2)]; + case 8: + _e.sent(); + _e.label = 9; + case 9: return [3 /*break*/, 11]; + case 10: + if (e_2) throw e_2.error; + return [7 /*endfinally*/]; + case 11: return [7 /*endfinally*/]; + case 12: + if (turnFailure) { + throw new Error(turnFailure.message); + } + return [2 /*return*/, { items: items, finalResponse: finalResponse, usage: usage }]; + } + }); + }); + }; + return Thread; +}()); +exports.Thread = Thread; +function normalizeInput(input) { + if (typeof input === "string") { + return { prompt: input, images: [] }; + } + var promptParts = []; + var images = []; + for (var _i = 0, input_1 = input; _i < input_1.length; _i++) { + var item = input_1[_i]; + if (item.type === "text") { + promptParts.push(item.text); + } + else if (item.type === "local_image") { + images.push(item.path); + } + } + return { prompt: promptParts.join("\n\n"), images: images }; +} diff --git a/sdk/native/dist-examples/src/threadOptions.js b/sdk/native/dist-examples/src/threadOptions.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/sdk/native/dist-examples/src/threadOptions.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/sdk/native/dist-examples/src/turnOptions.js b/sdk/native/dist-examples/src/turnOptions.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/sdk/native/dist-examples/src/turnOptions.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/sdk/native/examples/README.md b/sdk/native/examples/README.md new file mode 100644 index 000000000..4697697f1 --- /dev/null +++ b/sdk/native/examples/README.md @@ -0,0 +1,190 @@ +# Codex Native SDK Examples + +This directory contains example scripts demonstrating various features of the `@codex-native/sdk`. + +## Directory Structure + +### `/basic` - Core Features +Basic SDK functionality and common use cases. + +- **`streaming-deltas.ts`** — Real-time streaming response examples + - Text streaming with incremental updates + - Multi-modal input (text + images) + - Understanding all stream event types + - Token usage tracking + +- **`review-example.ts`** — Code review functionality + - Creating review sessions + - Custom review prompts + - Processing review findings + - Review output handling + +### `/tools` - Tool Management +Custom tool registration and override capabilities. + +- **`automatic-tool-registration.ts`** — Automatic tool registration with agents + - Tools registered automatically when passed to Agent + - Multiple tools with different capabilities + - Tool chaining and validation + - Zod schema integration + +- **`tool-override-example.ts`** — Built-in tool override + - Replacing built-in tools with custom implementations + - Mock implementations for testing + - Safety wrappers (blocking dangerous commands) + - Logging and instrumentation + - Multiple tool overrides + +### `/provider` - CodexProvider Integration +OpenAI Agents framework integration examples. + +- **`codex-provider-direct.ts`** — Direct provider usage + - Basic text input + - Multi-modal input (text + images) + - Manual provider instantiation + +- **`codex-provider-global.ts`** — Global provider configuration + - Setting provider as default for all agents + - Shared configuration across agents + +- **`codex-provider-run.ts`** — Simple provider execution + - Minimal example of running with CodexProvider + - Quick-start pattern + +### `/agents` - OpenAI Agents Framework +Advanced multi-agent workflows. + +- **`agents-integration.ts`** — CodexProvider with OpenAI Agents + - Single and multi-agent workflows + - Structured output + - Streaming support + - Conversation continuity + - Internal tool execution + +- **`agents-with-tools.ts`** — Agents with custom tools + - Tool definition with Agents framework + - Custom weather and temperature tools + - Framework tool execution patterns + +## Running Examples + +### Prerequisites + +Build the SDK first: + +```bash +cd sdk/native +npm install +npm run build +``` + +### Execute an Example + +Using `tsx`: + +```bash +npx tsx examples/basic/streaming-deltas.ts +npx tsx examples/tools/tool-override-example.ts +npx tsx examples/agents/agents-integration.ts +``` + +Using `node` (after building): + +```bash +npm run build:ts +node dist-examples/examples/basic/streaming-deltas.js +``` + +## Example Categories by Feature + +### Getting Started +1. `provider/codex-provider-run.ts` — Simplest example +2. `basic/streaming-deltas.ts` — Core streaming features +3. `tools/automatic-tool-registration.ts` — Working with tools + +### Advanced Features +- **Multi-Agent**: `agents/agents-integration.ts` +- **Tool Override**: `tools/tool-override-example.ts` +- **Code Review**: `basic/review-example.ts` + +### Integration Patterns +- **OpenAI Agents**: All files in `/agents` and `/provider` +- **Direct SDK Usage**: Files in `/basic` and `/tools` + +## Additional Resources + +- **Main README**: `../README.md` — Full SDK documentation +- **Agents Guide**: `../AGENTS.md` — OpenAI Agents integration details +- **API Docs**: TypeScript definitions in `../dist/index.d.mts` + +## Contributing + +When adding new examples: + +1. Place in the appropriate category directory +2. Include clear comments explaining key concepts +3. Use descriptive console output +4. Handle errors gracefully +5. Update this README with the new example + +## Common Patterns + +### Initialize Codex + +```typescript +import { Codex } from "@codex-native/sdk"; + +const codex = new Codex({ + model: "gpt-5-codex", + workingDirectory: process.cwd(), +}); +``` + +### Stream Responses + +```typescript +const thread = codex.startThread(); +const result = await thread.runStreamed("Your prompt here"); + +for await (const event of result.events) { + if (event.type === "item.completed") { + console.log(event.item.text); + } +} +``` + +### Register Custom Tools + +```typescript +codex.registerTool({ + name: "my_tool", + description: "What this tool does", + parameters: { /* JSON schema */ }, + handler: async (args) => { + // Tool implementation + return JSON.stringify(result); + }, +}); +``` + +### Use with OpenAI Agents + +```typescript +import { CodexProvider } from "@codex-native/sdk"; +import { Agent, Runner } from "@openai/agents"; + +const provider = new CodexProvider(); +const agent = new Agent({ + name: "MyAgent", + instructions: "Agent instructions", +}); + +const runner = new Runner({ modelProvider: provider }); +const result = await runner.run(agent, "Task description"); +``` + +## Support + +For issues or questions: +- GitHub Issues: [codex-native/issues](https://github.com/ScriptedAlchemy/codex-native/issues) +- Documentation: Check the main README and AGENTS.md diff --git a/sdk/native/examples/agents-with-tools.ts b/sdk/native/examples/agents-with-tools.ts deleted file mode 100644 index b123ecdd1..000000000 --- a/sdk/native/examples/agents-with-tools.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Example: Using CodexProvider with OpenAI Agents framework and custom tools - * - * This example demonstrates how to: - * - Use zod for type-safe tool parameters - * - Register custom tools with the agents framework - * - Use CodexProvider as the model backend - * - Create a weather assistant agent - * - * Installation: - * ```bash - * npm install @codex-native/sdk @openai/agents zod - * ``` - * - * Usage: - * ```bash - * export CODEX_API_KEY="your-api-key" - * export OPENAI_API_KEY="your-openai-api-key" - * npx tsx examples/agents-with-tools.ts - * ``` - */ - -import { z } from 'zod'; -import { - Agent, - run, - withTrace, - OpenAIChatCompletionsModel, - tool, -} from '@openai/agents'; -import { OpenAI } from 'openai'; -import { CodexProvider } from '../src/index.js'; - -// Define a weather tool using zod for type-safe parameters -const getWeatherTool = tool({ - name: 'get_weather', - description: 'Get the weather for a given city', - parameters: z.object({ - city: z.string().describe('The city to get weather for'), - }), - execute: async (input) => { - console.log(`[debug] Getting weather for ${input.city}\n`); - // Simulate weather API call - const weatherConditions = ['sunny', 'cloudy', 'rainy', 'snowy']; - const condition = weatherConditions[Math.floor(Math.random() * weatherConditions.length)]; - const temp = Math.floor(Math.random() * 30) + 10; - return `The weather in ${input.city} is ${condition} with a temperature of ${temp}°C`; - }, -}); - -// Define a temperature conversion tool -const convertTemperatureTool = tool({ - name: 'convert_temperature', - description: 'Convert temperature between Celsius and Fahrenheit', - parameters: z.object({ - value: z.number().describe('The temperature value to convert'), - from: z.enum(['celsius', 'fahrenheit']).describe('The unit to convert from'), - to: z.enum(['celsius', 'fahrenheit']).describe('The unit to convert to'), - }), - execute: async (input) => { - console.log(`[debug] Converting ${input.value}°${input.from[0].toUpperCase()} to ${input.to}\n`); - - if (input.from === input.to) { - return `${input.value}°${input.from === 'celsius' ? 'C' : 'F'}`; - } - - let result: number; - if (input.from === 'celsius' && input.to === 'fahrenheit') { - result = (input.value * 9/5) + 32; - } else { - result = (input.value - 32) * 5/9; - } - - return `${input.value}°${input.from === 'celsius' ? 'C' : 'F'} is ${result.toFixed(1)}°${input.to === 'celsius' ? 'C' : 'F'}`; - }, -}); - -async function main() { - console.log('🤖 OpenAI Agents with Codex Provider\n'); - - // Create OpenAI client for GPT-5 - const client = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - }); - - // Create an agent with GPT-5 and custom tools - const weatherAgent = new Agent({ - name: 'WeatherAssistant', - model: new OpenAIChatCompletionsModel(client, 'gpt-5'), - instructions: 'You are a helpful weather assistant. You respond in haikus when providing weather information.', - tools: [getWeatherTool, convertTemperatureTool], - }); - - console.log('✓ Created WeatherAssistant agent with GPT-5\n'); - - // Run the agent with tracing - await withTrace('Weather Assistant Example', async () => { - console.log('Running query: "What\'s the weather in Tokyo?"\n'); - console.log('─'.repeat(60)); - - const result = await run(weatherAgent, "What's the weather in Tokyo?"); - - console.log('\n[Final response]'); - console.log(result.finalOutput); - }); - - console.log('\n' + '='.repeat(60)); - console.log('✓ Example complete!'); -} - -// Run if executed directly -if (require.main === module) { - main().catch((error) => { - console.error('Fatal error:', error); - process.exit(1); - }); -} - -export { main, getWeatherTool, convertTemperatureTool }; diff --git a/sdk/native/examples/agents-integration.ts b/sdk/native/examples/agents/agents-integration.ts similarity index 87% rename from sdk/native/examples/agents-integration.ts rename to sdk/native/examples/agents/agents-integration.ts index 31842058f..1c2750ead 100644 --- a/sdk/native/examples/agents-integration.ts +++ b/sdk/native/examples/agents/agents-integration.ts @@ -7,17 +7,16 @@ * * Installation: * ```bash - * npm install @openai/codex-native @openai/agents + * npm install @codex-native/sdk @openai/agents * ``` * * Usage: * ```bash - * export CODEX_API_KEY="your-api-key" * npx tsx examples/agents-integration.ts * ``` */ -import { CodexProvider } from "@openai/codex-native"; +import { CodexProvider } from "../../src/index"; // These would come from @openai/agents when installed // import { Agent, Runner } from "@openai/agents"; @@ -40,9 +39,7 @@ async function main() { // Step 1: Create the CodexProvider // ============================================================================ const provider = new CodexProvider({ - apiKey: process.env.CODEX_API_KEY, - baseUrl: process.env.CODEX_BASE_URL, - defaultModel: "claude-sonnet-4.5", + defaultModel: "gpt-5-codex", workingDirectory: process.cwd(), skipGitRepoCheck: true, // For example purposes }); @@ -149,7 +146,9 @@ each using Codex as their backend through the provider. - No need for framework-level tool configuration ✓ Multi-modal Input: - - Support for text and images (future) + - Support for text and images (available now!) + - Images can be URLs, base64 data, or file paths + - CodexProvider automatically handles image conversion - Codex can analyze screenshots and diagrams `); @@ -160,7 +159,7 @@ each using Codex as their backend through the provider. console.log("\n\nDirect Provider Usage (for testing):"); console.log("─".repeat(60)); - const model = provider.getModel("claude-sonnet-4.5"); + const model = provider.getModel("gpt-5-codex"); try { const response = await model.getResponse({ @@ -171,7 +170,20 @@ each using Codex as their backend through the provider. maxTokens: 1000, }, tools: [], - outputType: { type: "json_schema", schema: {} }, + outputType: { + type: "json_schema", + schema: { + type: "object", + properties: { + answer: { + type: "string", + description: "The answer to the question", + }, + }, + required: ["answer"], + additionalProperties: false, + }, + }, handoffs: [], tracing: { enabled: false }, }); @@ -183,7 +195,7 @@ each using Codex as their backend through the provider. console.log(`\n Output items: ${response.output.length}`); for (const item of response.output) { - if (item.type === "assistant_message") { + if (!item.type || item.type === "message") { console.log(`\n Message: ${item.content[0]?.type === "output_text" ? item.content[0].text : "(non-text)"}`); } } @@ -198,7 +210,12 @@ each using Codex as their backend through the provider. // Run if executed directly if (require.main === module) { - main().catch((error) => { + main() + .then(() => { + // Force exit after completion to avoid hanging + process.exit(0); + }) + .catch((error) => { console.error("Fatal error:", error); process.exit(1); }); diff --git a/sdk/native/examples/agents/agents-with-tools.ts b/sdk/native/examples/agents/agents-with-tools.ts new file mode 100644 index 000000000..8bbb6e64f --- /dev/null +++ b/sdk/native/examples/agents/agents-with-tools.ts @@ -0,0 +1,165 @@ +/** + * Example: Using CodexProvider with OpenAI Agents framework + * + * This example demonstrates how to: + * - Use CodexProvider as a ModelProvider for the OpenAI Agents framework + * - Create simple agents that interact via Codex + * - Handle basic conversational queries + * + * Note: Custom tool execution (like weather tools defined in this file) is not + * yet fully supported. The CodexProvider can execute Codex's built-in tools + * (bash, file operations, web search, etc.) but bidirectional tool execution + * with the OpenAI Agents framework requires additional integration work. + * See CodexProvider.ts executeToolViaFramework() for details. + * + * Installation: + * ```bash + * npm install @codex-native/sdk @openai/agents zod + * ``` + * + * Usage: + * ```bash + * npx tsx examples/agents-with-tools.ts + * ``` + * + * This example demonstrates using Codex's native NAPI bindings as the model + * provider. Codex handles authentication and connection to OpenAI's GPT-5 + * Responses API internally via the native binding, so no API key configuration + * is needed in your code. + * + * Key features demonstrated: + * - CodexProvider integration with OpenAI Agents framework + * - Basic conversational agents + * - Automatic cleanup of temporary directories + */ + +import os from 'node:os'; +import path from 'node:path'; +import { promises as fs } from 'node:fs'; +import { z } from 'zod'; +import { Agent, run, withTrace, tool } from '@openai/agents'; +import { CodexProvider } from '../../src/index'; + +// Define a weather tool using zod for type-safe parameters +const getWeatherTool = tool({ + name: 'get_weather', + description: 'Get the weather for a given city', + parameters: z.object({ + city: z.string().describe('The city to get weather for'), + }), + execute: async (input) => { + console.log(`[debug] Getting weather for ${input.city}\n`); + // Simulate weather API call + const weatherConditions = ['sunny', 'cloudy', 'rainy', 'snowy']; + const condition = weatherConditions[Math.floor(Math.random() * weatherConditions.length)]; + const temp = Math.floor(Math.random() * 30) + 10; + return `The weather in ${input.city} is ${condition} with a temperature of ${temp}°C`; + }, +}); + +// Define a temperature conversion tool +const convertTemperatureTool = tool({ + name: 'convert_temperature', + description: 'Convert temperature between Celsius and Fahrenheit', + parameters: z.object({ + value: z.number().describe('The temperature value to convert'), + from: z.enum(['celsius', 'fahrenheit']).describe('The unit to convert from'), + to: z.enum(['celsius', 'fahrenheit']).describe('The unit to convert to'), + }), + execute: async (input) => { + console.log(`[debug] Converting ${input.value}°${input.from[0].toUpperCase()} to ${input.to}\n`); + + if (input.from === input.to) { + return `${input.value}°${input.from === 'celsius' ? 'C' : 'F'}`; + } + + let result: number; + if (input.from === 'celsius' && input.to === 'fahrenheit') { + result = (input.value * 9/5) + 32; + } else { + result = (input.value - 32) * 5/9; + } + + return `${input.value}°${input.from === 'celsius' ? 'C' : 'F'} is ${result.toFixed(1)}°${input.to === 'celsius' ? 'C' : 'F'}`; + }, +}); + +async function main() { + console.log('🤖 OpenAI Agents with Codex Provider\n'); + console.log('NOTE: This example demonstrates the CodexProvider integration,'); + console.log('but custom tools (get_weather, convert_temperature) are not yet'); + console.log('fully supported due to limitations in the current implementation.\n'); + console.log('The CodexProvider can execute its built-in tools (bash, file operations,'); + console.log('web search, etc.) but bidirectional tool execution with the OpenAI Agents'); + console.log('framework requires additional integration work.\n'); + + // Create a temporary directory to avoid loading workspace AGENTS.md + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'codex-agents-example-')); + console.log(`Using temporary directory: ${tmpDir}\n`); + + // Create Codex provider with temporary directory + // This avoids loading workspace-specific configuration files + const codexProvider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: tmpDir, + skipGitRepoCheck: true, + }); + + // Get the Codex model + const codexModel = await codexProvider.getModel(); + + // Example 1: Simple text query + console.log('Example 1: Basic conversational query'); + console.log('─'.repeat(60)); + + // Create a simple agent + const simpleAgent = new Agent({ + name: 'Assistant', + model: codexModel, + instructions: 'You are a helpful assistant.', + }); + + await withTrace('Conversation Example', async () => { + const question = "Hello! Please respond with a brief greeting and confirm you can help."; + console.log(`Query: "${question}"\n`); + const result = await run(simpleAgent, question); + + console.log('\n[Final response]'); + console.log(result.finalOutput); + + // Verify we got a response + if (!result.finalOutput || result.finalOutput.length === 0) { + throw new Error('No response received from agent'); + } + }); + + console.log('\n' + '='.repeat(60)); + console.log('✓ Example complete!'); + console.log('\nKey takeaways:'); + console.log(' • CodexProvider successfully integrates with OpenAI Agents framework'); + console.log(' • Basic queries work without custom tools'); + console.log(' • Custom tool execution requires bidirectional framework integration'); + console.log(' • See CodexProvider.ts executeToolViaFramework() for details'); + + // Cleanup + try { + await fs.rm(tmpDir, { recursive: true, force: true }); + } catch (e) { + // Ignore cleanup errors + } +} + +// Run if executed directly +if (require.main === module) { + main() + .then(() => { + // Force exit after completion to avoid hanging + process.exit(0); + }) + .catch((error) => { + console.error('Fatal error:', error); + process.exit(1); + }); +} + +export { main, getWeatherTool, convertTemperatureTool }; diff --git a/sdk/native/examples/basic/review-example.ts b/sdk/native/examples/basic/review-example.ts new file mode 100644 index 000000000..320eac244 --- /dev/null +++ b/sdk/native/examples/basic/review-example.ts @@ -0,0 +1,110 @@ +/** + * Example: Using Codex review method to review code changes + * + * This demonstrates how to use the review() method on the Codex class + * to perform code reviews with different targets. + * + * Review targets: + * - current_changes: Review staged/unstaged files + * - custom: Custom review prompt + * + * Usage: + * ```bash + * npx tsx examples/review-example.ts + * ``` + */ + +import { Codex } from "../../src/index"; +import fs from "fs/promises"; +import path from "path"; +import os from "os"; + +async function main() { + // Create a temporary directory with some sample code to review + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-review-test-")); + + try { + // Create a sample file with intentional issues + const sampleCode = ` +// Sample code with some issues +function calculateTotal(items) { + var total = 0; // Using var instead of const/let + for (var i = 0; i < items.length; i++) { // Could use forEach/map + total = total + items[i].price; + } + return total; // Missing input validation +} + +// Function with potential bug +function divideNumbers(a, b) { + return a / b; // No zero-division check +} + +// Unused function +function unusedFunction() { + console.log("This is never called"); +} +`; + + await fs.writeFile(path.join(tmpDir, "sample.js"), sampleCode); + + console.log("Created sample code for review in:", tmpDir); + console.log("\nStarting code review with custom prompt...\n"); + + // Create Codex instance + const codex = new Codex(); + + // Perform a custom review + const result = await codex.review({ + target: { + type: "custom", + prompt: `Review the JavaScript code in sample.js. Look for: +1. Outdated JavaScript patterns (var, for loops) +2. Missing error handling +3. Potential bugs +4. Unused code + +Provide a concise summary of findings with 2-3 key issues.`, + hint: "JavaScript code review", + }, + threadOptions: { + model: "gpt-5-codex", + workingDirectory: tmpDir, + skipGitRepoCheck: true, + fullAuto: true, + }, + }); + + console.log("\n" + "=".repeat(70)); + console.log("Review Results:"); + console.log("=".repeat(70) + "\n"); + + if (result.finalResponse) { + console.log(result.finalResponse); + } else { + console.log("[No final review summary produced]"); + } + + if (result.usage) { + console.log( + `\nToken usage: ${result.usage.input_tokens} input, ${result.usage.output_tokens} output`, + ); + } + + console.log("\n✓ Review completed successfully"); + } finally { + // Cleanup + await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); + console.log("✓ Cleaned up temporary directory"); + } +} + +main() + .then(() => { + console.log("\nExample completed successfully."); + setTimeout(() => process.exit(0), 100); + }) + .catch((error) => { + console.error("Error:", error); + process.exit(1); + }); diff --git a/sdk/native/examples/basic/streaming-deltas.ts b/sdk/native/examples/basic/streaming-deltas.ts new file mode 100644 index 000000000..c5819b0f5 --- /dev/null +++ b/sdk/native/examples/basic/streaming-deltas.ts @@ -0,0 +1,336 @@ +/** + * Example: Streaming responses with CodexProvider + * + * This example demonstrates how to use CodexProvider's streaming capabilities + * to receive events as the model generates a response. This is useful for + * building responsive UIs that show progress in real-time. + * + * Features demonstrated: + * - Streaming text responses (complete text when generation finishes) + * - Streaming reasoning updates (for extended thinking models) + * - Handling different stream event types + * - Real-time progress monitoring + * - Image input support with streaming + * + * Note: The current implementation emits complete text/reasoning blocks + * rather than character-by-character deltas. Events are emitted when + * each phase completes (reasoning done, text done, etc.). + * + * Installation: + * ```bash + * npm install @codex-native/sdk @openai/agents + * ``` + * + * Usage: + * ```bash + * npx tsx examples/streaming-deltas.ts + * ``` + */ + +import { Agent, run } from '@openai/agents'; +import { CodexProvider } from '../../src/index'; + +async function streamingTextExample() { + console.log('\n' + '='.repeat(70)); + console.log('Example 1: Streaming Text Response'); + console.log('='.repeat(70) + '\n'); + + // Create provider and model + const provider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + + const model = await provider.getModel(); + + // Create a simple agent + const agent = new Agent({ + name: 'StreamingAssistant', + model: model, + instructions: 'You are a helpful assistant. Provide detailed explanations.', + }); + + console.log('Requesting: "Explain how neural networks work in 3 paragraphs"\n'); + console.log('─'.repeat(70)); + console.log('Streaming response:'); + console.log('─'.repeat(70) + '\n'); + + // Use the streaming API to get incremental updates + const request = { + systemInstructions: 'You are a helpful assistant.', + input: 'Explain how neural networks work in 3 paragraphs', + modelSettings: {}, + tools: [], + outputType: { type: 'text' as const }, + handoffs: [], + tracing: { enabled: false }, + }; + + let fullText = ''; + let fullReasoning = ''; + + // Stream the response + for await (const event of model.getStreamedResponse(request)) { + switch (event.type) { + case 'response_started': + console.log('[Stream started]'); + console.log('[Generating response...]\n'); + break; + + case 'output_text_delta': + // Note: Delta events are not currently emitted by the backend + // Text is provided as a complete block in output_text_done + process.stdout.write(event.delta); + fullText += event.delta; + break; + + case 'output_text_done': + // Text is provided here as a complete block + fullText = event.text; + console.log(event.text); + console.log('\n[Text generation complete]'); + break; + + case 'reasoning_delta': + // Note: Delta events are not currently emitted by the backend + // Reasoning is provided as a complete block in reasoning_done + fullReasoning += event.delta; + break; + + case 'reasoning_done': + if (event.reasoning) { + fullReasoning = event.reasoning; + console.log(`[Extended thinking complete: ${event.reasoning.length} chars]`); + } + break; + + case 'response_done': + console.log(`\n[Response done]`); + console.log(` Input tokens: ${event.response.usage.inputTokens}`); + console.log(` Output tokens: ${event.response.usage.outputTokens}`); + console.log(` Total tokens: ${event.response.usage.totalTokens}`); + break; + + case 'error': + console.error(`\n[Error]: ${event.error.message}`); + break; + } + } + + console.log('\n' + '─'.repeat(70)); + console.log(`Final text length: ${fullText.length} characters`); + if (fullReasoning) { + console.log(`Reasoning length: ${fullReasoning.length} characters`); + } +} + +async function streamingWithImageExample() { + console.log('\n\n' + '='.repeat(70)); + console.log('Example 2: Streaming with Image Input'); + console.log('='.repeat(70) + '\n'); + + const provider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + + const model = await provider.getModel(); + + console.log('Sending multi-modal input (text + image)...'); + console.log('CodexProvider automatically handles image conversion for streaming\n'); + console.log('─'.repeat(70)); + console.log('Streaming response:'); + console.log('─'.repeat(70) + '\n'); + + // Example with image - using a sample image URL + const imageUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg'; + + const request = { + systemInstructions: 'You are a helpful assistant that can analyze images.', + input: [ + { type: 'input_text' as const, text: 'Describe what you see in this image concisely' }, + { type: 'input_image' as const, image: imageUrl } + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' as const }, + handoffs: [], + tracing: { enabled: false }, + }; + + let responseText = ''; + + for await (const event of model.getStreamedResponse(request)) { + switch (event.type) { + case 'response_started': + console.log('[Stream started - processing image...]'); + console.log('[Generating response...]\n'); + break; + + case 'output_text_delta': + process.stdout.write(event.delta); + responseText += event.delta; + break; + + case 'output_text_done': + responseText = event.text; + console.log(event.text); + console.log('\n[Text generation complete]'); + break; + + case 'reasoning_done': + if (event.reasoning) { + console.log(`[Extended thinking complete: ${event.reasoning.length} chars]`); + } + break; + + case 'response_done': + console.log(`\n[Response done]`); + console.log(` Tokens used: ${event.response.usage.totalTokens}`); + break; + + case 'error': + console.error(`\n[Error]: ${event.error.message}`); + break; + } + } + + console.log('\n' + '─'.repeat(70)); + console.log(`Response length: ${responseText.length} characters`); +} + +async function detailedStreamEventExample() { + console.log('\n\n' + '='.repeat(70)); + console.log('Example 3: Understanding All Stream Event Types'); + console.log('='.repeat(70) + '\n'); + + const provider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + + const model = await provider.getModel(); + + console.log('This example shows all possible stream event types:\n'); + console.log('Event Types:'); + console.log(' • response_started - Stream begins'); + console.log(' • output_text_delta - Incremental text chunk (not currently emitted)'); + console.log(' • output_text_done - Text generation complete (includes full text)'); + console.log(' • reasoning_delta - Incremental reasoning (not currently emitted)'); + console.log(' • reasoning_done - Reasoning complete (includes full reasoning)'); + console.log(' • response_done - Full response complete with usage stats'); + console.log(' • error - An error occurred\n'); + console.log('Note: Currently only complete blocks are emitted (reasoning_done, output_text_done)'); + console.log(' rather than incremental deltas.\n'); + + console.log('─'.repeat(70)); + console.log('Event stream:\n'); + + const request = { + systemInstructions: 'You are a helpful assistant.', + input: 'Count from 1 to 5 and explain each number.', + modelSettings: {}, + tools: [], + outputType: { type: 'text' as const }, + handoffs: [], + tracing: { enabled: false }, + }; + + const eventCounts = new Map(); + + for await (const event of model.getStreamedResponse(request)) { + // Track event counts + const count = eventCounts.get(event.type) || 0; + eventCounts.set(event.type, count + 1); + + // Log each event type + switch (event.type) { + case 'response_started': + console.log(`[${event.type}]`); + break; + + case 'output_text_delta': + // Delta events are not currently emitted, but handler is here for future compatibility + if (count < 3) { + console.log(`[${event.type}] delta="${event.delta}"`); + } else if (count === 3) { + console.log(`[${event.type}] ... (${count} more delta events) ...`); + } + break; + + case 'output_text_done': + console.log(`[${event.type}] text length=${event.text.length}`); + break; + + case 'reasoning_delta': + // Delta events are not currently emitted, but handler is here for future compatibility + if (count < 3) { + console.log(`[${event.type}] delta="${event.delta}"`); + } + break; + + case 'reasoning_done': + console.log(`[${event.type}] reasoning length=${event.reasoning.length}`); + break; + + case 'response_done': + console.log(`[${event.type}] usage=${JSON.stringify(event.response.usage)}`); + break; + + case 'error': + console.log(`[${event.type}] message="${event.error.message}"`); + break; + } + } + + console.log('\n' + '─'.repeat(70)); + console.log('Event Statistics:'); + for (const [eventType, count] of eventCounts.entries()) { + console.log(` ${eventType}: ${count} events`); + } +} + +async function main() { + console.log('🌊 CodexProvider Streaming Response Examples\n'); + console.log('These examples demonstrate streaming capabilities for monitoring'); + console.log('response generation in real-time.\n'); + + try { + // Run all examples + await streamingTextExample(); + await streamingWithImageExample(); + await detailedStreamEventExample(); + + console.log('\n\n' + '='.repeat(70)); + console.log('✓ All streaming examples complete!'); + console.log('='.repeat(70)); + console.log('\nKey takeaways:'); + console.log(' • Stream events provide real-time progress updates'); + console.log(' • output_text_done provides the complete generated text'); + console.log(' • reasoning_done provides the complete extended thinking'); + console.log(' • Image inputs work seamlessly with streaming'); + console.log(' • Usage statistics are provided in the response_done event'); + console.log(' • Current implementation emits complete blocks rather than character deltas'); + } catch (error) { + console.error('\n✗ Error:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +// Run if executed directly +if (require.main === module) { + main() + .then(() => { + // Force exit after completion to avoid hanging + process.exit(0); + }) + .catch((error) => { + console.error('Fatal error:', error); + process.exit(1); + }); +} + +export { main }; diff --git a/sdk/native/examples/provider/codex-provider-direct.ts b/sdk/native/examples/provider/codex-provider-direct.ts new file mode 100644 index 000000000..f33f1abc3 --- /dev/null +++ b/sdk/native/examples/provider/codex-provider-direct.ts @@ -0,0 +1,65 @@ +/** + * Example: Pass a Codex model instance directly to an Agent + * + * This demonstrates the most direct way to use Codex with the agents framework: + * create a CodexProvider, get a model, and pass it directly to your Agent. + * + * CodexProvider features: + * - Multi-modal input support (text + images) + * - Streaming response deltas + * - Automatic tool registration + * - No API key configuration needed (handled by native bindings) + * + * Usage: + * ```bash + * npx tsx examples/codex-provider-direct.ts + * ``` + */ + +import { Agent, run } from '@openai/agents'; +import { CodexProvider } from '../../src/index'; + +async function main() { + // Create a Codex provider (no API key needed - Codex handles auth internally) + const provider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + + // Get a model instance from the provider + const model = await provider.getModel('gpt-5-codex'); + + // Create an agent with the Codex model + const agent = new Agent({ + name: 'CodexAssistant', + model, + instructions: + 'You are a helpful coding assistant powered by GPT-5 through Codex. You support both text and image inputs.', + }); + + // Run the agent with text input + console.log('Example 1: Text input\n'); + const result = await run(agent, 'What is the capital of France?'); + console.log('\nAgent response:'); + console.log(result.finalOutput); + console.log('\n✓ Example 1 completed successfully'); + + // Example with image input (multi-modal) + // CodexProvider automatically handles image conversion + console.log('\n\nExample 2: Multi-modal input (text + image)\n'); + console.log('Note: Images can be provided as URLs, base64 data, or file paths'); + console.log(' CodexProvider handles the conversion automatically'); + console.log('\n✓ Example 2 info displayed (no actual image run in this demo)'); +} + +main() + .then(() => { + console.log('\nExamples completed successfully.'); + // Force exit after completion - native bindings may keep handles open + setTimeout(() => process.exit(0), 100); + }) + .catch((error) => { + console.error('Error:', error); + process.exit(1); + }); diff --git a/sdk/native/examples/provider/codex-provider-global.ts b/sdk/native/examples/provider/codex-provider-global.ts new file mode 100644 index 000000000..0c40232b6 --- /dev/null +++ b/sdk/native/examples/provider/codex-provider-global.ts @@ -0,0 +1,64 @@ +/** + * Example: Using CodexProvider with OpenAI Agents framework + * + * This demonstrates how to create a CodexProvider and use it + * with the OpenAI Agents framework to run queries. + * + * CodexProvider features: + * - Multi-modal input support (text + images) + * - Streaming response deltas for real-time updates + * - Automatic tool registration when passed to agents + * - No API key required (uses local Codex instance) + * + * Usage: + * ```bash + * npx tsx examples/codex-provider-global.ts + * ``` + */ + +import { Agent, run } from '@openai/agents'; +import { CodexProvider } from '../../src/index'; + +async function main() { + // Create a shared Codex provider (no API key needed) + const codexProvider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + + // Get the model once and reuse it across multiple agents + const model = codexProvider.getModel(); + + // Create an agent with the Codex model + const agent = new Agent({ + name: 'SharedCodexAgent', + model: model, + instructions: 'You are a helpful assistant powered by Codex. Answer concisely in one sentence.', + }); + + // Run a single query to demonstrate the integration + console.log('Query:'); + const result = await run(agent, 'What is 2+2?'); + console.log(result.finalOutput); + + console.log('\nQuery completed successfully!'); +} + +main() + .then(() => { + console.log('\nExiting...'); + // Force exit after completion to avoid hanging from native binding + // Use a small delay to ensure stdout flushes + setTimeout(() => process.exit(0), 100); + }) + .catch((error) => { + console.error('Error:', error); + setTimeout(() => process.exit(1), 100); + }); + +// Fallback timeout in case the native binding hangs +setTimeout(() => { + console.error('\nERROR: Script timed out after 30 seconds'); + process.exit(124); +}, 30000); diff --git a/sdk/native/examples/provider/codex-provider-run.ts b/sdk/native/examples/provider/codex-provider-run.ts new file mode 100644 index 000000000..23d15573a --- /dev/null +++ b/sdk/native/examples/provider/codex-provider-run.ts @@ -0,0 +1,57 @@ +/** + * Example: Use CodexProvider for a single execution run + * + * This demonstrates how to create and use a Codex provider for a specific + * operation without registering it globally. + * + * CodexProvider capabilities: + * - Multi-modal inputs (text and images) + * - Streaming deltas for real-time response updates + * - Automatic tool registration + * - Local file path, URL, and base64 image support + * + * Usage: + * ```bash + * npx tsx examples/codex-provider-run.ts + * ``` + */ + +import { Agent, run } from '@openai/agents'; +import { CodexProvider } from '../../src/index'; + +async function main() { + // Create a provider instance for this specific run (no API key needed) + const provider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + + const codexModel = await provider.getModel(); + + // Create an agent configured for this specific run + const agent = new Agent({ + name: 'TaskSpecificAgent', + model: codexModel, + instructions: + 'You are a helpful assistant. Answer questions directly and concisely in 1-2 sentences.', + }); + + // Run the agent once with the model obtained from the provider + const result = await run(agent, 'What is 2+2? Answer with just the number.'); + + console.log('Result:'); + console.log(result.finalOutput); +} + +main() + .then(() => { + console.log('\nMain completed successfully.'); + // Force exit after completion - native bindings may keep handles open + // Using setTimeout to ensure output is flushed before exit + setTimeout(() => process.exit(0), 100); + }) + .catch((error) => { + console.error('Error:', error); + process.exit(1); + }); diff --git a/sdk/native/examples/tools/automatic-tool-registration.ts b/sdk/native/examples/tools/automatic-tool-registration.ts new file mode 100644 index 000000000..89cb106ca --- /dev/null +++ b/sdk/native/examples/tools/automatic-tool-registration.ts @@ -0,0 +1,343 @@ +/** + * Example: Automatic Tool Registration with CodexProvider + * + * This example demonstrates how tools are automatically registered when + * passed to an Agent using CodexProvider. No manual tool registration + * or configuration is required - the provider handles everything seamlessly. + * + * Key concepts: + * - Tools defined with zod schemas are automatically validated + * - Multiple tools can be provided to a single agent + * - Tools are available immediately when the agent runs + * - Tool execution results are automatically passed back to the model + * + * Installation: + * ```bash + * npm install @codex-native/sdk @openai/agents zod + * ``` + * + * Usage: + * ```bash + * npx tsx examples/automatic-tool-registration.ts + * ``` + */ + +import { z } from 'zod'; +import { Agent, run, tool } from '@openai/agents'; +import { CodexProvider } from '../../src/index'; +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; + +// Define a calculator tool +const calculatorTool = tool({ + name: 'calculator', + description: 'Perform basic arithmetic operations', + parameters: z.object({ + operation: z.enum(['add', 'subtract', 'multiply', 'divide']).describe('The operation to perform'), + a: z.number().describe('First number'), + b: z.number().describe('Second number'), + }), + execute: async (input) => { + console.log(`[Tool called: calculator(${input.operation}, ${input.a}, ${input.b})]`); + + let result: number; + switch (input.operation) { + case 'add': + result = input.a + input.b; + break; + case 'subtract': + result = input.a - input.b; + break; + case 'multiply': + result = input.a * input.b; + break; + case 'divide': + if (input.b === 0) { + return 'Error: Division by zero'; + } + result = input.a / input.b; + break; + } + + return `${input.a} ${input.operation} ${input.b} = ${result}`; + }, +}); + +// Define a unit conversion tool +const unitConverterTool = tool({ + name: 'convert_units', + description: 'Convert between different units of measurement', + parameters: z.object({ + value: z.number().describe('The value to convert'), + fromUnit: z.enum(['meters', 'feet', 'kilograms', 'pounds', 'celsius', 'fahrenheit']) + .describe('The unit to convert from'), + toUnit: z.enum(['meters', 'feet', 'kilograms', 'pounds', 'celsius', 'fahrenheit']) + .describe('The unit to convert to'), + }), + execute: async (input) => { + console.log(`[Tool called: convert_units(${input.value} ${input.fromUnit} → ${input.toUnit})]`); + + // Conversion logic + let result: number; + + // Length conversions + if (input.fromUnit === 'meters' && input.toUnit === 'feet') { + result = input.value * 3.28084; + } else if (input.fromUnit === 'feet' && input.toUnit === 'meters') { + result = input.value / 3.28084; + } + // Weight conversions + else if (input.fromUnit === 'kilograms' && input.toUnit === 'pounds') { + result = input.value * 2.20462; + } else if (input.fromUnit === 'pounds' && input.toUnit === 'kilograms') { + result = input.value / 2.20462; + } + // Temperature conversions + else if (input.fromUnit === 'celsius' && input.toUnit === 'fahrenheit') { + result = (input.value * 9/5) + 32; + } else if (input.fromUnit === 'fahrenheit' && input.toUnit === 'celsius') { + result = (input.value - 32) * 5/9; + } + // Same unit + else if (input.fromUnit === input.toUnit) { + result = input.value; + } else { + return `Error: Cannot convert from ${input.fromUnit} to ${input.toUnit}`; + } + + return `${input.value} ${input.fromUnit} = ${result.toFixed(2)} ${input.toUnit}`; + }, +}); + +// Define a text analysis tool +const textAnalysisTool = tool({ + name: 'analyze_text', + description: 'Analyze text and return statistics', + parameters: z.object({ + text: z.string().describe('The text to analyze'), + }), + execute: async (input) => { + console.log(`[Tool called: analyze_text(text length: ${input.text.length})]`); + + const words = input.text.split(/\s+/).filter(w => w.length > 0); + const sentences = input.text.split(/[.!?]+/).filter(s => s.trim().length > 0); + const characters = input.text.length; + const charactersNoSpaces = input.text.replace(/\s/g, '').length; + + return JSON.stringify({ + characters, + charactersNoSpaces, + words: words.length, + sentences: sentences.length, + averageWordLength: (charactersNoSpaces / words.length).toFixed(2), + }, null, 2); + }, +}); + +async function basicToolExample() { + console.log('\n' + '='.repeat(70)); + console.log('Example 1: Basic Automatic Tool Registration'); + console.log('='.repeat(70) + '\n'); + + // Create temporary working directory to avoid loading workspace config + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'codex-tool-example-')); + + try { + // Create provider and model + const provider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: tmpDir, + skipGitRepoCheck: true, + }); + + const model = await provider.getModel(); + + // Create agent with a single tool + // The tool is automatically registered - no manual configuration needed! + const calculatorAgent = new Agent({ + name: 'CalculatorAgent', + model: model, + instructions: 'You are a helpful calculator assistant. Use the calculator tool to perform operations. Answer directly with just the calculation result.', + tools: [calculatorTool], + }); + + console.log('✓ Created CalculatorAgent'); + console.log('✓ Tool "calculator" automatically registered\n'); + + console.log('─'.repeat(70)); + console.log('Query: "What is 123 multiplied by 456?"\n'); + + const result = await run(calculatorAgent, 'What is 123 multiplied by 456?'); + + console.log('\n[Final response]'); + console.log(result.finalOutput); + } finally { + // Cleanup temp directory + await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); + } +} + +async function multipleToolsExample() { + console.log('\n\n' + '='.repeat(70)); + console.log('Example 2: Multiple Tools Automatically Registered'); + console.log('='.repeat(70) + '\n'); + + // Create temporary working directory to avoid loading workspace config + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'codex-tool-example-')); + + try { + const provider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: tmpDir, + skipGitRepoCheck: true, + }); + + const model = await provider.getModel(); + + // Create agent with multiple tools + // ALL tools are automatically registered when passed to the Agent + const multiToolAgent = new Agent({ + name: 'MultiToolAgent', + model: model, + instructions: 'You are a helpful assistant with access to calculator, unit converter, and text analysis tools. Use the tools to answer questions directly.', + tools: [calculatorTool, unitConverterTool, textAnalysisTool], + }); + + console.log('✓ Created MultiToolAgent'); + console.log('✓ Tools automatically registered:'); + console.log(' - calculator'); + console.log(' - convert_units'); + console.log(' - analyze_text\n'); + + console.log('─'.repeat(70)); + console.log('Query: "Convert 100 pounds to kilograms, then multiply by 2"\n'); + + const result = await run(multiToolAgent, 'Convert 100 pounds to kilograms, then multiply by 2'); + + console.log('\n[Final response]'); + console.log(result.finalOutput); + } finally { + // Cleanup temp directory + await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); + } +} + +async function toolChainExample() { + console.log('\n\n' + '='.repeat(70)); + console.log('Example 3: Chaining Multiple Tool Calls'); + console.log('='.repeat(70) + '\n'); + + // Create temporary working directory to avoid loading workspace config + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'codex-tool-example-')); + + try { + const provider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: tmpDir, + skipGitRepoCheck: true, + }); + + const model = await provider.getModel(); + + const agent = new Agent({ + name: 'ToolChainAgent', + model: model, + instructions: 'You are a helpful assistant. Use multiple tools in sequence when needed to answer complex questions. Answer directly with the results.', + tools: [calculatorTool, unitConverterTool, textAnalysisTool], + }); + + console.log('✓ Created ToolChainAgent with 3 tools\n'); + + console.log('─'.repeat(70)); + console.log('Complex query requiring multiple tool calls:\n'); + console.log('Query: "I weigh 150 pounds. Convert that to kilograms, then calculate'); + console.log(' what 20% of my weight would be. Also analyze this sentence:'); + console.log(' \'The quick brown fox jumps over the lazy dog.\'"\n'); + + const result = await run( + agent, + 'I weigh 150 pounds. Convert that to kilograms, then calculate what 20% of my weight would be. Also analyze this sentence: "The quick brown fox jumps over the lazy dog."' + ); + + console.log('\n[Final response]'); + console.log(result.finalOutput); + } finally { + // Cleanup temp directory + await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); + } +} + +async function toolValidationExample() { + console.log('\n\n' + '='.repeat(70)); + console.log('Example 4: Automatic Tool Parameter Validation'); + console.log('='.repeat(70) + '\n'); + + console.log('Tools defined with Zod schemas have automatic validation:'); + console.log(' • Type checking (string, number, enum, etc.)'); + console.log(' • Required vs optional parameters'); + console.log(' • Value constraints and descriptions'); + console.log(' • Automatic error messages for invalid inputs\n'); + + console.log('Example tool schema:'); + console.log(` + const calculatorTool = tool({ + name: 'calculator', + parameters: z.object({ + operation: z.enum(['add', 'subtract', 'multiply', 'divide']), + a: z.number().describe('First number'), + b: z.number().describe('Second number'), + }), + execute: async (input) => { /* implementation */ } + }); + `); + + console.log('\n✓ When this tool is registered with CodexProvider:'); + console.log(' • The model receives the schema definition'); + console.log(' • Invalid calls are caught and handled gracefully'); + console.log(' • The agent can understand parameter requirements'); + console.log(' • No manual validation code needed!'); +} + +async function main() { + console.log('🔧 Automatic Tool Registration Examples\n'); + console.log('This demonstrates how CodexProvider automatically registers'); + console.log('tools when they are passed to an Agent - no manual configuration!\n'); + + try { + // Run all examples + await basicToolExample(); + await multipleToolsExample(); + await toolChainExample(); + await toolValidationExample(); + + console.log('\n\n' + '='.repeat(70)); + console.log('✓ All automatic tool registration examples complete!'); + console.log('='.repeat(70)); + console.log('\nKey takeaways:'); + console.log(' • Tools are automatically registered when passed to Agent()'); + console.log(' • No manual tool.register() or provider.addTool() calls needed'); + console.log(' • Multiple tools can be registered at once'); + console.log(' • Tools defined with Zod have automatic validation'); + console.log(' • Tool execution results flow back to the model automatically'); + console.log(' • The agent can chain multiple tool calls to complete tasks'); + } catch (error) { + console.error('\n✗ Error:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +// Run if executed directly +if (require.main === module) { + main() + .then(() => { + // Force exit after completion to avoid hanging + process.exit(0); + }) + .catch((error) => { + console.error('Fatal error:', error); + process.exit(1); + }); +} + +export { main }; diff --git a/sdk/native/examples/tools/tool-override-example.ts b/sdk/native/examples/tools/tool-override-example.ts new file mode 100644 index 000000000..f9f609f1e --- /dev/null +++ b/sdk/native/examples/tools/tool-override-example.ts @@ -0,0 +1,215 @@ +import { Codex } from "../../src/index"; + +console.log("🔧 Tool Override Examples\n"); +console.log("This demonstrates how to override Codex's built-in tools"); +console.log("with custom implementations.\n"); + +console.log("=" .repeat(70)); +console.log("Example 1: Override read_file with a mock implementation"); +console.log("=" .repeat(70)); + +const codex1 = new Codex(); + +// Override read_file to return mock data +codex1.registerTool({ + name: "read_file", + description: "Read a file from the filesystem (mocked)", + parameters: { + type: "object", + properties: { + target_file: { + type: "string", + description: "Path to the file to read", + }, + }, + required: ["target_file"], + }, + handler: async ({ target_file }) => { + console.log(` 📄 Mock read_file called for: ${target_file}`); + return JSON.stringify({ + content: `[MOCK CONTENT] This is fake content for ${target_file}`, + }); + }, +}); + +console.log("✓ Registered custom read_file handler"); +console.log( + " When Codex tries to read files, it will use the mock implementation\n", +); + +console.log("=" .repeat(70)); +console.log("Example 2: Override grep with a logging wrapper"); +console.log("=" .repeat(70)); + +const codex2 = new Codex(); + +// Override grep to add logging +codex2.registerTool({ + name: "grep", + description: "Search for patterns with logging", + parameters: { + type: "object", + properties: { + pattern: { type: "string", description: "Pattern to search for" }, + path: { type: "string", description: "Path to search in" }, + output_mode: { + type: "string", + enum: ["content", "files_with_matches", "count"], + }, + }, + required: ["pattern"], + }, + handler: async ({ pattern, path, output_mode }) => { + console.log(` 🔍 Grep override called:`); + console.log(` Pattern: ${pattern}`); + console.log(` Path: ${path || "(workspace root)"}`); + console.log(` Mode: ${output_mode || "content"}`); + + // Return mock results + return JSON.stringify({ + matches: [`Mock result for pattern: ${pattern}`], + }); + }, +}); + +console.log("✓ Registered custom grep handler"); +console.log(" All grep operations will now be logged\n"); + +console.log("=" .repeat(70)); +console.log("Example 3: Override local_shell to block dangerous commands"); +console.log("=" .repeat(70)); + +const codex3 = new Codex(); + +const BLOCKED_COMMANDS = ["rm -rf", "dd if=", "mkfs", "> /dev/"]; + +codex3.registerTool({ + name: "local_shell", + description: "Execute shell commands with safety checks", + parameters: { + type: "object", + properties: { + command: { type: "string", description: "Command to execute" }, + }, + required: ["command"], + }, + handler: async ({ command }) => { + console.log(` 🛡️ Shell command intercepted: ${command}`); + + // Check if command is blocked + for (const blocked of BLOCKED_COMMANDS) { + if (command.includes(blocked)) { + console.log(` ❌ BLOCKED: Command contains dangerous pattern "${blocked}"`); + return JSON.stringify({ + error: `Command blocked for safety: contains "${blocked}"`, + exit_code: 1, + }); + } + } + + console.log(` ✓ Command allowed (would execute in real implementation)`); + return JSON.stringify({ + stdout: "[Mock output] Command would run here", + exit_code: 0, + }); + }, +}); + +console.log("✓ Registered safety-checking shell handler"); +console.log(" Dangerous commands will be blocked before execution\n"); + +console.log("=" .repeat(70)); +console.log("Example 4: Override multiple tools at once"); +console.log("=" .repeat(70)); + +const codex4 = new Codex(); + +// Override multiple file operations +const fileTools = [ + { + name: "read_file", + handler: async ({ target_file }: any) => { + return JSON.stringify({ content: `[READ] ${target_file}` }); + }, + }, + { + name: "write", + handler: async ({ file_path, contents }: any) => { + return JSON.stringify({ success: true, path: file_path }); + }, + }, + { + name: "search_replace", + handler: async ({ file_path, old_string, new_string }: any) => { + return JSON.stringify({ + success: true, + file: file_path, + replacements: 1, + }); + }, + }, +]; + +for (const tool of fileTools) { + codex4.registerTool({ + name: tool.name, + description: `Custom ${tool.name}`, + parameters: { type: "object", properties: {} }, + handler: tool.handler, + }); + console.log(` ✓ Registered custom ${tool.name}`); +} + +console.log("\nAll file operations now use custom implementations\n"); + +console.log("=" .repeat(70)); +console.log("Example 5: Restore defaults by clearing tools"); +console.log("=" .repeat(70)); + +const codex5 = new Codex(); + +// Register an override +codex5.registerTool({ + name: "grep", + description: "Custom grep", + parameters: { type: "object", properties: {} }, + handler: async () => "custom", +}); + +console.log("✓ Registered custom grep"); + +// Clear to restore defaults +codex5.clearTools(); +console.log("✓ Cleared all custom tools"); +console.log(" Built-in tools are now restored\n"); + +console.log("=" .repeat(70)); +console.log("Key Takeaways"); +console.log("=" .repeat(70)); +console.log(` +Tool Override Use Cases: + • Testing: Mock file operations, commands, and network calls + • Safety: Add validation or block dangerous operations + • Logging: Wrap built-in tools with instrumentation + • Customization: Replace default behavior with domain-specific logic + • Integration: Bridge Codex tools with existing systems + +Supported Override Targets: + • File operations: read_file, write, search_replace, delete_file + • Search: grep, glob_file_search, codebase_search + • Execution: local_shell, exec_command + • Git: git operations + • MCP: Any MCP server tool + • And all other built-in Codex tools + +Notes: + • Overrides apply to the specific Codex instance + • clearTools() removes overrides and restores defaults + • Tool handlers run in native code (Rust), not JavaScript + • The handler signature must match the tool's parameter schema +`); + +console.log("=" .repeat(70)); +console.log("✓ All tool override examples complete!"); +console.log("=" .repeat(70)); + diff --git a/sdk/native/index.d.ts b/sdk/native/index.d.ts index 040e9a52d..e116bb848 100644 --- a/sdk/native/index.d.ts +++ b/sdk/native/index.d.ts @@ -37,6 +37,9 @@ export interface RunRequest { baseUrl?: string apiKey?: string linuxSandboxPath?: string + fullAuto?: boolean + reviewMode?: boolean + reviewHint?: string } export declare function runThread(req: RunRequest): Promise> diff --git a/sdk/native/package.json b/sdk/native/package.json index fc3d512cf..55f679c4f 100644 --- a/sdk/native/package.json +++ b/sdk/native/package.json @@ -2,8 +2,15 @@ "name": "@codex-native/sdk", "version": "0.0.1", "description": "Native NAPI-based Codex SDK - complete standalone implementation.", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "dist/index.mjs", + "module": "dist/index.mjs", + "types": "dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + }, "files": [ "dist", "README.md", @@ -11,10 +18,14 @@ "*.node" ], "scripts": { - "build": "npm run build:napi && npm run build:ts", + "clean": "rm -rf dist *.node npm target/release/*.node && cargo clean --release", + "clean:all": "rm -rf dist *.node npm target node_modules && cargo clean", + "build": "npm run build:rust && npm run build:napi && npm run build:ts && npm run verify:build", + "build:rust": "cargo fmt && cargo clippy --release -- -D warnings", "build:napi": "napi build --platform --release", "build:napi:debug": "napi build --platform", "build:ts": "tsup", + "verify:build": "node -e \"const fs=require('fs');const hasNode=fs.existsSync('codex_native.darwin-arm64.node')||fs.existsSync('codex_native.linux-x64-gnu.node')||fs.existsSync('codex_native.win32-x64-msvc.node');const hasDist=fs.existsSync('dist/index.mjs')&&fs.existsSync('dist/index.d.mts');if(!hasNode||!hasDist){console.error('Build incomplete: missing .node or dist files');process.exit(1)}console.log('✓ Build verification passed')\"", "create-npm-dirs": "napi create-npm-dirs", "artifacts": "napi artifacts", "prepublishOnly": "napi prepublish -t npm", @@ -22,6 +33,8 @@ "test": "NODE_OPTIONS=--experimental-vm-modules jest", "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", "coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage", + "typecheck": "tsc --noEmit", + "lint": "cargo fmt --check && cargo clippy --release -- -D warnings && npm run typecheck", "version": "napi version", "release:patch": "npm version patch && npm run release", "release:minor": "npm version minor && npm run release", diff --git a/sdk/native/rust-bindings/lib.rs b/sdk/native/rust-bindings/lib.rs index 5a5b1fcc1..06344222a 100644 --- a/sdk/native/rust-bindings/lib.rs +++ b/sdk/native/rust-bindings/lib.rs @@ -1,36 +1,48 @@ #![deny(clippy::all)] use std::path::PathBuf; -use std::sync::{Arc, Mutex}; use std::sync::OnceLock; +use std::sync::{Arc, Mutex}; use async_trait::async_trait; use codex_common::CliConfigOverrides; use codex_common::SandboxModeCliArg; -use codex_exec::exec_events::ThreadEvent as ExecThreadEvent; -use codex_exec::run_with_thread_event_callback; -use codex_exec::{Cli, Color, Command, ResumeArgs}; +use codex_core::auth::enforce_login_restrictions; +use codex_core::config::Config; +use codex_core::config::ConfigOverrides; use codex_core::function_tool::FunctionCallError; +use codex_core::git_info::get_git_repo_root; +use codex_core::protocol::AskForApproval; +use codex_core::protocol::SessionSource; +use codex_core::protocol::{Event, EventMsg, Op, ReviewOutputEvent, ReviewRequest, TokenUsage}; use codex_core::tools::context::ToolInvocation; use codex_core::tools::context::ToolOutput; use codex_core::tools::context::ToolPayload; -use codex_core::tools::registry::{ExternalToolRegistration, ToolHandler, ToolKind, set_pending_external_tools}; +use codex_core::tools::registry::{ + ExternalToolRegistration, ToolHandler, ToolKind, set_pending_external_tools, +}; use codex_core::tools::spec::create_function_tool_spec_from_schema; -use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; +use codex_core::{AuthManager, ConversationManager, NewConversation}; +use codex_exec::exec_events::{ + AgentMessageItem, ExitedReviewModeEvent as ExecExitedReviewModeEvent, ItemCompletedEvent, + ReasoningItem, ReviewCodeLocation, ReviewFinding, ReviewLineRange, + ReviewOutputEvent as ExecReviewOutputEvent, ThreadErrorEvent as ExecThreadErrorEvent, + ThreadEvent, ThreadItem, ThreadItemDetails, ThreadStartedEvent, TurnCompletedEvent, + TurnFailedEvent, TurnStartedEvent, Usage, +}; +use codex_exec::run_with_thread_event_callback; +use codex_exec::{Cli, Color, Command, ResumeArgs}; +use codex_protocol::config_types::SandboxMode; use napi::bindgen_prelude::{Function, Status}; +use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; -use serde_json::json; use serde_json::Value as JsonValue; +use serde_json::json; use tempfile::NamedTempFile; #[cfg(target_os = "linux")] use std::io::Write; -#[cfg(target_os = "linux")] -fn io_to_napi(err: std::io::Error) -> napi::Error { - napi::Error::from_reason(err.to_string()) -} - #[cfg(target_os = "linux")] fn ensure_embedded_linux_sandbox() -> napi::Result { use std::fs; @@ -38,30 +50,52 @@ fn ensure_embedded_linux_sandbox() -> napi::Result { static SANDBOX_PATH: OnceLock = OnceLock::new(); - SANDBOX_PATH - .get_or_try_init(|| { - let root = std::env::temp_dir().join("codex-native"); - fs::create_dir_all(&root).map_err(io_to_napi)?; - let target_path = root.join("codex-linux-sandbox"); + // Use get_or_init with a closure that handles errors manually + SANDBOX_PATH.get_or_init(|| { + let root = std::env::temp_dir().join("codex-native"); + if fs::create_dir_all(&root).is_err() { + return PathBuf::new(); + } + let target_path = root.join("codex-linux-sandbox"); - let mut tmp = NamedTempFile::new_in(&root).map_err(io_to_napi)?; - tmp.write_all(EMBEDDED_LINUX_SANDBOX_BYTES) - .map_err(io_to_napi)?; - tmp.flush().map_err(io_to_napi)?; + let mut tmp = match NamedTempFile::new_in(&root) { + Ok(t) => t, + Err(_) => return PathBuf::new(), + }; + if tmp.write_all(EMBEDDED_LINUX_SANDBOX_BYTES).is_err() { + return PathBuf::new(); + } + if tmp.flush().is_err() { + return PathBuf::new(); + } - let temp_path = tmp.into_temp_path(); - if target_path.exists() { - fs::remove_file(&target_path).map_err(io_to_napi)?; - } - temp_path.persist(&target_path).map_err(|(_, err)| io_to_napi(err))?; + let temp_path = tmp.into_temp_path(); + if target_path.exists() { + let _ = fs::remove_file(&target_path); + } + if temp_path.persist(&target_path).is_err() { + return PathBuf::new(); + } + + let mut perms = match fs::metadata(&target_path) { + Ok(m) => m.permissions(), + Err(_) => return PathBuf::new(), + }; + perms.set_mode(0o755); + if fs::set_permissions(&target_path, perms).is_err() { + return PathBuf::new(); + } - let mut perms = fs::metadata(&target_path).map_err(io_to_napi)?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(&target_path, perms).map_err(io_to_napi)?; + target_path + }); - Ok(target_path) - }) - .cloned() + let path = SANDBOX_PATH.get().unwrap(); + if path.as_os_str().is_empty() { + return Err(napi::Error::from_reason( + "Failed to initialize Linux sandbox binary", + )); + } + Ok(path.clone()) } #[cfg(target_os = "linux")] @@ -77,9 +111,6 @@ fn default_linux_sandbox_path() -> napi::Result> { #[cfg(target_os = "linux")] const EMBEDDED_LINUX_SANDBOX_BYTES: &[u8] = include_bytes!(env!("CODEX_LINUX_SANDBOX_BIN")); -const ORIGINATOR_ENV: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"; -const NATIVE_ORIGINATOR: &str = "codex_sdk_native"; - fn registered_native_tools() -> &'static Mutex> { static TOOLS: OnceLock>> = OnceLock::new(); TOOLS.get_or_init(|| Mutex::new(Vec::new())) @@ -115,10 +146,12 @@ pub fn register_tool( info: NativeToolInfo, handler: Function, ) -> napi::Result<()> { - let schema = info.parameters.unwrap_or_else(|| json!({ - "type": "object", - "properties": {} - })); + let schema = info.parameters.unwrap_or_else(|| { + json!({ + "type": "object", + "properties": {} + }) + }); let spec = create_function_tool_spec_from_schema( info.name.clone(), info.description.clone(), @@ -159,7 +192,8 @@ pub struct JsToolInvocation { } struct JsToolHandler { - callback: ThreadsafeFunction, + callback: + ThreadsafeFunction, } #[async_trait] @@ -230,25 +264,51 @@ pub struct RunRequest { pub api_key: Option, #[napi(js_name = "linuxSandboxPath")] pub linux_sandbox_path: Option, + #[napi(js_name = "fullAuto")] + pub full_auto: Option, + #[napi(js_name = "reviewMode")] + pub review_mode: Option, + #[napi(js_name = "reviewHint")] + pub review_hint: Option, } -struct InternalRunRequest { - prompt: String, - thread_id: Option, - images: Vec, - model: Option, - sandbox_mode: Option, - working_directory: Option, - skip_git_repo_check: bool, - output_schema: Option, - base_url: Option, - api_key: Option, - linux_sandbox_path: Option, +#[cfg_attr(test, derive(Debug))] +pub struct InternalRunRequest { + pub prompt: String, + pub thread_id: Option, + pub images: Vec, + pub model: Option, + pub sandbox_mode: Option, + pub working_directory: Option, + pub skip_git_repo_check: bool, + pub output_schema: Option, + pub base_url: Option, + pub api_key: Option, + pub linux_sandbox_path: Option, + pub full_auto: bool, + pub review_request: Option, } impl RunRequest { fn into_internal(self) -> napi::Result { - let sandbox_mode = match self.sandbox_mode.as_deref() { + let RunRequest { + prompt, + thread_id, + images, + model, + sandbox_mode, + working_directory, + skip_git_repo_check, + output_schema, + base_url, + api_key, + linux_sandbox_path, + full_auto, + review_mode, + review_hint, + } = self; + + let sandbox_mode = match sandbox_mode.as_deref() { None => None, Some("read-only") => Some(SandboxModeCliArg::ReadOnly), Some("workspace-write") => Some(SandboxModeCliArg::WorkspaceWrite), @@ -256,64 +316,83 @@ impl RunRequest { Some(other) => { return Err(napi::Error::from_reason(format!( "Unsupported sandbox mode: {other}", - ))) + ))); } }; - let images = self - .images + let images = images .unwrap_or_default() .into_iter() .map(PathBuf::from) .collect(); - let working_directory = self.working_directory.map(PathBuf::from); + let working_directory = working_directory.map(PathBuf::from); + let full_auto = full_auto.unwrap_or(false); + let review_request = if review_mode.unwrap_or(false) { + if prompt.trim().is_empty() { + return Err(napi::Error::from_reason( + "Review mode requires a non-empty prompt".to_string(), + )); + } + let hint = review_hint.unwrap_or_else(|| "code review".to_string()); + Some(ReviewRequest { + prompt: prompt.clone(), + user_facing_hint: hint, + }) + } else { + None + }; Ok(InternalRunRequest { - prompt: self.prompt, - thread_id: self.thread_id, + prompt, + thread_id, images, - model: self.model, + model, sandbox_mode, working_directory, - skip_git_repo_check: self.skip_git_repo_check.unwrap_or(false), - output_schema: self.output_schema, - base_url: self.base_url, - api_key: self.api_key, - linux_sandbox_path: self.linux_sandbox_path.map(PathBuf::from), + skip_git_repo_check: skip_git_repo_check.unwrap_or(false), + output_schema, + base_url, + api_key, + linux_sandbox_path: linux_sandbox_path.map(PathBuf::from), + full_auto, + review_request, }) } } -struct TempSchemaFile { - path: PathBuf, - _guard: tempfile::TempPath, +pub struct TempSchemaFile { + pub path: PathBuf, + pub _guard: tempfile::TempPath, } -fn prepare_schema(schema: Option) -> napi::Result> { +pub fn prepare_schema(schema: Option) -> napi::Result> { if let Some(schema_value) = schema { let mut file = NamedTempFile::new().map_err(|e| napi::Error::from_reason(e.to_string()))?; serde_json::to_writer(&mut file, &schema_value) .map_err(|e| napi::Error::from_reason(e.to_string()))?; let path = file.path().to_path_buf(); let temp_path = file.into_temp_path(); - Ok(Some(TempSchemaFile { path, _guard: temp_path })) + Ok(Some(TempSchemaFile { + path, + _guard: temp_path, + })) } else { Ok(None) } } -struct EnvOverride { - key: &'static str, - previous: Option, +pub struct EnvOverride { + pub key: &'static str, + pub previous: Option, } -struct EnvOverrides { - entries: Vec, +pub struct EnvOverrides { + pub entries: Vec, } impl EnvOverrides { - fn apply(pairs: Vec<(&'static str, Option, bool)>) -> Self { + pub fn apply(pairs: Vec<(&'static str, Option, bool)>) -> Self { let mut entries = Vec::new(); for (key, value, force) in pairs { if !force && value.is_none() { @@ -343,12 +422,79 @@ impl Drop for EnvOverrides { } } -fn build_cli(options: &InternalRunRequest, schema_path: Option) -> Cli { - let command = options.thread_id.as_ref().map(|id| Command::Resume(ResumeArgs { - session_id: Some(id.clone()), - last: false, - prompt: Some(options.prompt.clone()), - })); +fn prepare_environment( + options: &InternalRunRequest, +) -> napi::Result<(EnvOverrides, Option)> { + let mut env_pairs: Vec<(&'static str, Option, bool)> = Vec::new(); + if let Some(base_url) = options.base_url.clone() { + env_pairs.push(("OPENAI_BASE_URL", Some(base_url), true)); + } + if let Some(api_key) = options.api_key.clone() { + env_pairs.push(("CODEX_API_KEY", Some(api_key), true)); + } + + let linux_sandbox_path = if let Some(path) = options.linux_sandbox_path.clone() { + Some(path) + } else if let Ok(path) = std::env::var("CODEX_LINUX_SANDBOX_EXE") { + Some(PathBuf::from(path)) + } else { + default_linux_sandbox_path()? + }; + + if let Some(path) = linux_sandbox_path.as_ref() { + env_pairs.push(( + "CODEX_LINUX_SANDBOX_EXE", + Some(path.to_string_lossy().to_string()), + false, + )); + } + + let guard = EnvOverrides::apply(env_pairs); + Ok((guard, linux_sandbox_path)) +} + +pub fn build_config_overrides( + options: &InternalRunRequest, + sandbox_path: Option, +) -> ConfigOverrides { + let sandbox_mode = if options.full_auto { + Some(SandboxMode::WorkspaceWrite) + } else { + options.sandbox_mode.map(Into::::into) + }; + let cwd = options + .working_directory + .as_ref() + .map(|path| path.canonicalize().unwrap_or(path.clone())); + + ConfigOverrides { + model: options.model.clone(), + review_model: None, + cwd, + approval_policy: Some(AskForApproval::Never), + sandbox_mode, + model_provider: None, + config_profile: None, + codex_linux_sandbox_exe: sandbox_path, + base_instructions: None, + developer_instructions: None, + compact_prompt: None, + include_apply_patch_tool: None, + show_raw_agent_reasoning: Some(false), + tools_web_search_request: None, + experimental_sandbox_command_assessment: None, + additional_writable_roots: Vec::new(), + } +} + +pub fn build_cli(options: &InternalRunRequest, schema_path: Option) -> Cli { + let command = options.thread_id.as_ref().map(|id| { + Command::Resume(ResumeArgs { + session_id: Some(id.clone()), + last: false, + prompt: Some(options.prompt.clone()), + }) + }); Cli { command, @@ -357,12 +503,14 @@ fn build_cli(options: &InternalRunRequest, schema_path: Option) -> Cli oss: false, sandbox_mode: options.sandbox_mode, config_profile: None, - full_auto: false, + full_auto: options.full_auto, dangerously_bypass_approvals_and_sandbox: false, cwd: options.working_directory.clone(), skip_git_repo_check: options.skip_git_repo_check, output_schema: schema_path, - config_overrides: CliConfigOverrides { raw_overrides: Vec::new() }, + config_overrides: CliConfigOverrides { + raw_overrides: Vec::new(), + }, color: Color::Never, json: false, last_message_file: None, @@ -374,14 +522,298 @@ fn build_cli(options: &InternalRunRequest, schema_path: Option) -> Cli } } -fn event_to_json(event: &ExecThreadEvent) -> napi::Result { +pub struct ReviewEventCollector { + pub next_item_id: u64, + pub last_usage: Option, + pub last_error: Option, + pub emitted_review_exit: bool, +} + +impl Default for ReviewEventCollector { + fn default() -> Self { + Self::new() + } +} + +impl ReviewEventCollector { + pub fn new() -> Self { + Self { + next_item_id: 0, + last_usage: None, + last_error: None, + emitted_review_exit: false, + } + } + + pub fn next_item_id(&mut self) -> String { + let id = self.next_item_id; + self.next_item_id += 1; + format!("item_{id}") + } + + pub fn handle(&mut self, event: &Event) -> Vec { + match &event.msg { + EventMsg::SessionConfigured(ev) => { + vec![ThreadEvent::ThreadStarted(ThreadStartedEvent { + thread_id: ev.session_id.to_string(), + })] + } + EventMsg::TaskStarted(_) => { + self.last_error = None; + vec![ThreadEvent::TurnStarted(TurnStartedEvent {})] + } + EventMsg::AgentReasoning(ev) => { + let item = ThreadItem { + id: self.next_item_id(), + details: ThreadItemDetails::Reasoning(ReasoningItem { + text: ev.text.clone(), + }), + }; + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { item })] + } + EventMsg::AgentMessage(ev) => { + let item = ThreadItem { + id: self.next_item_id(), + details: ThreadItemDetails::AgentMessage(AgentMessageItem { + text: ev.message.clone(), + }), + }; + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { item })] + } + EventMsg::TokenCount(ev) => { + if let Some(info) = &ev.info { + self.last_usage = Some(info.total_token_usage.clone()); + } + Vec::new() + } + EventMsg::Warning(ev) => { + let item = ThreadItem { + id: self.next_item_id(), + details: ThreadItemDetails::Error(codex_exec::exec_events::ErrorItem { + message: ev.message.clone(), + }), + }; + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { item })] + } + EventMsg::Error(ev) => { + let error = ExecThreadErrorEvent { + message: ev.message.clone(), + }; + self.last_error = Some(error.clone()); + vec![ThreadEvent::Error(error)] + } + EventMsg::ExitedReviewMode(ev) => { + let converted = self.convert_review_output(&ev.review_output); + self.emitted_review_exit = true; + vec![ThreadEvent::ExitedReviewMode(ExecExitedReviewModeEvent { + review_output: converted, + })] + } + EventMsg::TaskComplete(task_complete) => { + let mut events = Vec::new(); + if let Some(text) = task_complete.last_agent_message.as_deref() { + if !self.emitted_review_exit { + let review_output = self.parse_review_output(text); + let converted = self.convert_review_output(&Some(review_output)); + events.push(ThreadEvent::ExitedReviewMode(ExecExitedReviewModeEvent { + review_output: converted, + })); + self.emitted_review_exit = true; + } + let item = ThreadItem { + id: self.next_item_id(), + details: ThreadItemDetails::AgentMessage(AgentMessageItem { + text: text.to_string(), + }), + }; + events.push(ThreadEvent::ItemCompleted(ItemCompletedEvent { item })); + } + + let usage = self.last_usage.clone().unwrap_or_default(); + + if let Some(error) = self.last_error.take() { + events.push(ThreadEvent::TurnFailed(TurnFailedEvent { error })); + } else { + events.push(ThreadEvent::TurnCompleted(TurnCompletedEvent { + usage: Usage { + input_tokens: usage.input_tokens, + cached_input_tokens: usage.cached_input_tokens, + output_tokens: usage.output_tokens, + }, + })); + } + events + } + _ => Vec::new(), + } + } + + pub fn convert_review_output( + &self, + review_output: &Option, + ) -> Option { + review_output.as_ref().map(|output| ExecReviewOutputEvent { + findings: output + .findings + .iter() + .map(|finding| ReviewFinding { + title: finding.title.clone(), + body: finding.body.clone(), + confidence_score: finding.confidence_score, + priority: finding.priority, + code_location: ReviewCodeLocation { + absolute_file_path: finding + .code_location + .absolute_file_path + .to_string_lossy() + .to_string(), + line_range: ReviewLineRange { + start: finding.code_location.line_range.start as i32, + end: finding.code_location.line_range.end as i32, + }, + }, + }) + .collect(), + overall_correctness: output.overall_correctness.clone(), + overall_explanation: output.overall_explanation.clone(), + overall_confidence_score: output.overall_confidence_score, + }) + } + + pub fn parse_review_output(&self, text: &str) -> ReviewOutputEvent { + if let Ok(ev) = serde_json::from_str::(text) { + return ev; + } + if let (Some(start), Some(end)) = (text.find('{'), text.rfind('}')) + && start < end + && let Some(slice) = text.get(start..=end) + && let Ok(ev) = serde_json::from_str::(slice) + { + return ev; + } + ReviewOutputEvent { + overall_explanation: text.to_string(), + ..Default::default() + } + } +} + +pub fn event_to_json(event: &ThreadEvent) -> napi::Result { serde_json::to_value(event).map_err(|e| napi::Error::from_reason(e.to_string())) } +fn run_review_sync( + options: InternalRunRequest, + review_request: ReviewRequest, + handler: F, +) -> napi::Result<()> +where + F: FnMut(ThreadEvent) + Send + 'static, +{ + let pending_tools = { + let guard = registered_native_tools() + .lock() + .map_err(|e| napi::Error::from_reason(format!("tools mutex poisoned: {e}")))?; + guard.clone() + }; + set_pending_external_tools(pending_tools); + + let (_env_guard, linux_sandbox_path) = prepare_environment(&options)?; + + let handler_arc = Arc::new(Mutex::new(handler)); + let handler_for_callback = Arc::clone(&handler_arc); + + let runtime = tokio::runtime::Runtime::new() + .map_err(|e| napi::Error::from_reason(format!("Failed to create runtime: {}", e)))?; + + runtime.block_on(async { + let config_overrides = build_config_overrides(&options, linux_sandbox_path.clone()); + let config = Config::load_with_cli_overrides(Vec::new(), config_overrides) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + + if let Err(err) = enforce_login_restrictions(&config).await { + return Err(napi::Error::from_reason(err.to_string())); + } + + if !options.skip_git_repo_check && get_git_repo_root(&config.cwd).is_none() { + return Err(napi::Error::from_reason( + "Not inside a trusted directory and --skip-git-repo-check was not specified.".to_string(), + )); + } + + let auth_manager = AuthManager::shared( + config.codex_home.clone(), + true, + config.cli_auth_credentials_store_mode, + ); + let conversation_manager = ConversationManager::new(auth_manager.clone(), SessionSource::Exec); + let NewConversation { + conversation, + session_configured, + .. + } = conversation_manager + .new_conversation(config.clone()) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + + let mut collector = ReviewEventCollector::new(); + + let session_event = Event { + id: String::new(), + msg: EventMsg::SessionConfigured(session_configured.clone()), + }; + for exec_event in collector.handle(&session_event) { + if let Ok(mut handler) = handler_for_callback.lock() { + (*handler)(exec_event); + } + } + + conversation + .submit(Op::Review { review_request }) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + + let mut shutdown_initiated = false; + loop { + let event = conversation + .next_event() + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + + let events = collector.handle(&event); + for exec_event in events { + if let Ok(mut handler) = handler_for_callback.lock() { + (*handler)(exec_event); + } + } + + // Initiate shutdown after TaskComplete (review flow doesn't auto-shutdown) + if !shutdown_initiated && matches!(event.msg, EventMsg::TaskComplete(_)) { + conversation + .submit(Op::Shutdown) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + shutdown_initiated = true; + } + + if matches!(event.msg, EventMsg::ShutdownComplete) { + break; + } + } + + Ok(()) + }) +} + fn run_internal_sync(options: InternalRunRequest, handler: F) -> napi::Result<()> where - F: FnMut(ExecThreadEvent) + Send + 'static, + F: FnMut(ThreadEvent) + Send + 'static, { + if let Some(review_request) = options.review_request.clone() { + return run_review_sync(options, review_request, handler); + } + let schema_file = prepare_schema(options.output_schema.clone())?; let schema_path = schema_file.as_ref().map(|file| file.path.clone()); let cli = build_cli(&options, schema_path); @@ -394,34 +826,7 @@ where }; set_pending_external_tools(pending_tools); - let mut env_pairs: Vec<(&'static str, Option, bool)> = Vec::new(); - if std::env::var(ORIGINATOR_ENV).is_err() { - env_pairs.push((ORIGINATOR_ENV, Some(NATIVE_ORIGINATOR.to_string()), true)); - } - if let Some(base_url) = options.base_url.clone() { - env_pairs.push(("OPENAI_BASE_URL", Some(base_url), true)); - } - if let Some(api_key) = options.api_key.clone() { - env_pairs.push(("CODEX_API_KEY", Some(api_key), true)); - } - - let linux_sandbox_path = if let Some(path) = options.linux_sandbox_path.clone() { - Some(path) - } else if let Ok(path) = std::env::var("CODEX_LINUX_SANDBOX_EXE") { - Some(PathBuf::from(path)) - } else { - default_linux_sandbox_path()? - }; - - if let Some(path) = linux_sandbox_path.as_ref() { - env_pairs.push(( - "CODEX_LINUX_SANDBOX_EXE", - Some(path.to_string_lossy().to_string()), - false, - )); - } - - let _env_guard = EnvOverrides::apply(env_pairs); + let (_env_guard, linux_sandbox_path) = prepare_environment(&options)?; let handler_arc = Arc::new(Mutex::new(handler)); let handler_for_callback = Arc::clone(&handler_arc); @@ -501,7 +906,9 @@ pub async fn run_thread_stream( Ok(JsonValue::String(text)), ThreadsafeFunctionCallMode::NonBlocking, ); - if status != Status::Ok && let Ok(mut guard) = error_clone.lock() { + if status != Status::Ok + && let Ok(mut guard) = error_clone.lock() + { *guard = Some(napi::Error::from_status(status)); } } @@ -510,7 +917,7 @@ pub async fn run_thread_stream( *guard = Some(napi::Error::from_reason(err.to_string())); } } - } + }, Err(err) => { if let Ok(mut guard) = error_clone.lock() { *guard = Some(err); diff --git a/sdk/native/scripts/build-all.mjs b/sdk/native/scripts/build-all.mjs new file mode 100644 index 000000000..f1e5c6361 --- /dev/null +++ b/sdk/native/scripts/build-all.mjs @@ -0,0 +1,16 @@ +import { execSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; + +const rootDir = dirname(dirname(fileURLToPath(import.meta.url))); + +const run = (cmd) => { + console.log(` +[build-all] ${cmd}`); + execSync(cmd, { cwd: rootDir, stdio: 'inherit' }); +}; + +run('pnpm --filter "@codex-native/sdk" run build:napi'); +run('pnpm --filter "@codex-native/sdk" run build:ts'); +run('pnpm --filter "@openai/codex-sdk" install'); +run('pnpm --filter "@openai/codex-sdk" run build'); diff --git a/sdk/native/src/agents/CodexProvider.ts b/sdk/native/src/agents/CodexProvider.ts index 30cebb36a..05aac6ccc 100644 --- a/sdk/native/src/agents/CodexProvider.ts +++ b/sdk/native/src/agents/CodexProvider.ts @@ -3,8 +3,12 @@ import type { Thread } from "../thread"; import type { ThreadEvent, Usage as CodexUsage } from "../events"; import type { ThreadItem } from "../items"; import type { Input, UserInput } from "../thread"; -import type { CodexOptions } from "../codexOptions"; +import type { CodexOptions, NativeToolDefinition } from "../codexOptions"; import type { ThreadOptions } from "../threadOptions"; +import type { NativeToolInvocation, NativeToolResult } from "../nativeBinding"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; import type { ModelProvider, Model, @@ -16,6 +20,7 @@ import type { Usage, AssistantMessageItem, OutputText, + SerializedTool, } from "./types"; /** @@ -48,9 +53,7 @@ export interface CodexProviderOptions extends CodexOptions { * import { CodexProvider } from '@openai/codex-native/agents'; * import { Agent, Runner } from '@openai/agents'; * - * const provider = new CodexProvider({ - * apiKey: process.env.CODEX_API_KEY, - * defaultModel: 'claude-sonnet-4.5' + * defaultModel: 'gpt-5-codex' * }); * * const agent = new Agent({ @@ -79,13 +82,22 @@ export class CodexProvider implements ModelProvider { */ private getCodex(): Codex { if (!this.codex) { - // Dynamic import to avoid circular dependencies - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { Codex: CodexClass } = require("../codex"); - this.codex = new CodexClass({ - apiKey: this.options.apiKey, - baseUrl: this.options.baseUrl, - }) as Codex; + try { + // Dynamic import to avoid circular dependencies + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { Codex: CodexClass } = require("../codex"); + if (!CodexClass) { + throw new Error("Codex class not found in module"); + } + this.codex = new CodexClass({ + apiKey: this.options.apiKey, + baseUrl: this.options.baseUrl, + }) as Codex; + } catch (error) { + throw new Error( + `Failed to initialize Codex: ${error instanceof Error ? error.message : String(error)}` + ); + } } return this.codex; } @@ -104,6 +116,12 @@ class CodexModel implements Model { private modelName?: string; private thread: Thread | null = null; private options: CodexProviderOptions; + private registeredTools: Set = new Set(); + private pendingToolCalls: Map void; + reject: (error: Error) => void; + }> = new Map(); + private tempImageFiles: Set = new Set(); constructor(codex: Codex, modelName: string | undefined, options: CodexProviderOptions) { this.codex = codex; @@ -111,15 +129,32 @@ class CodexModel implements Model { this.options = options; } + /** + * Cleanup temporary image files created during request processing + */ + private async cleanupTempFiles(): Promise { + for (const filepath of this.tempImageFiles) { + try { + await fs.promises.unlink(filepath); + } catch (error) { + // Silently ignore cleanup errors (file may already be deleted) + } + } + this.tempImageFiles.clear(); + } + /** * Get or create the thread for this model instance */ private getThread(conversationId?: string): Thread { - if (conversationId && !this.thread) { - // Resume existing thread - this.thread = this.codex.resumeThread(conversationId, this.getThreadOptions()); + // If we have a conversation ID and either no thread or a different thread + if (conversationId) { + if (!this.thread || this.thread.id !== conversationId) { + // Resume the specified thread + this.thread = this.codex.resumeThread(conversationId, this.getThreadOptions()); + } } else if (!this.thread) { - // Create new thread + // Create new thread only if we don't have one this.thread = this.codex.startThread(this.getThreadOptions()); } return this.thread; @@ -130,54 +165,348 @@ class CodexModel implements Model { model: this.modelName, workingDirectory: this.options.workingDirectory, skipGitRepoCheck: this.options.skipGitRepoCheck, + fullAuto: true, }; } async getResponse(request: ModelRequest): Promise { - const thread = this.getThread(request.conversationId || request.previousResponseId); - const input = this.convertRequestToInput(request); + try { + const thread = this.getThread(request.conversationId || request.previousResponseId); - // Run Codex (it handles tools internally) - const turn = await thread.run(input, { - outputSchema: request.outputType?.schema, - }); + // Register any tools provided in the request + if (request.tools && request.tools.length > 0) { + this.registerRequestTools(request.tools); + } - // Convert Codex response to ModelResponse format - return { - usage: this.convertUsage(turn.usage), - output: this.convertItemsToOutput(turn.items, turn.finalResponse), - responseId: thread.id || undefined, - }; + const input = await this.convertRequestToInput(request); + + // Note: ModelSettings like temperature, maxTokens, topP, etc. are not currently + // supported by the Codex native binding. The Rust layer handles model configuration. + + // Run Codex (tools are now registered and will be available) + const turn = await thread.run(input, { + outputSchema: request.outputType?.schema, + }); + + // Convert Codex response to ModelResponse format + return { + usage: this.convertUsage(turn.usage), + output: this.convertItemsToOutput(turn.items, turn.finalResponse), + responseId: thread.id || undefined, + }; + } finally { + // Clean up temporary image files + await this.cleanupTempFiles(); + } } async *getStreamedResponse(request: ModelRequest): AsyncIterable { - const thread = this.getThread(request.conversationId || request.previousResponseId); - const input = this.convertRequestToInput(request); + const MAX_ACCUMULATED_SIZE = 10_000_000; // 10MB limit + + try { + const thread = this.getThread(request.conversationId || request.previousResponseId); + + // Register any tools provided in the request + if (request.tools && request.tools.length > 0) { + this.registerRequestTools(request.tools); + } + + const input = await this.convertRequestToInput(request); + + const { events } = await thread.runStreamed(input, { + outputSchema: request.outputType?.schema, + }); - const { events } = await thread.runStreamed(input, { - outputSchema: request.outputType?.schema, - }); + // Track text accumulation for delta calculation + const textAccumulator = new Map(); - let accumulatedText = ""; + for await (const event of events) { + // Check accumulated text size to prevent memory issues + let totalSize = 0; + for (const text of textAccumulator.values()) { + totalSize += text.length; + } + if (totalSize > MAX_ACCUMULATED_SIZE) { + throw new Error(`Accumulated text exceeded maximum size limit (${MAX_ACCUMULATED_SIZE} bytes)`); + } - for await (const event of events) { - const streamEvents = this.convertCodexEventToStreamEvent(event, accumulatedText); + const streamEvents = this.convertCodexEventToStreamEvent(event, textAccumulator); - // Update accumulated text for text deltas - if (event.type === "item.completed" && event.item.type === "agent_message") { - accumulatedText = event.item.text; + for (const streamEvent of streamEvents) { + yield streamEvent; + } } + } finally { + // Clean up temporary image files + await this.cleanupTempFiles(); + } + } - for (const streamEvent of streamEvents) { - yield streamEvent; + /** + * Register tools from ModelRequest with the Codex instance + * + * Converts SerializedTool format (OpenAI Agents) to NativeToolDefinition format (Codex) + * and registers them with the Codex instance for bidirectional tool execution. + */ + private registerRequestTools(tools: SerializedTool[]): void { + for (const tool of tools) { + if (tool.type !== "function") { + continue; + } + + // Skip if already registered + if (this.registeredTools.has(tool.name)) { + continue; + } + + try { + // Convert SerializedTool to NativeToolDefinition + const nativeToolDef: NativeToolDefinition = { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + // The handler is called when Codex wants to execute this tool + handler: async (invocation: NativeToolInvocation): Promise => { + return await this.executeToolViaFramework(invocation); + }, + }; + + // Register the tool with Codex + this.codex.registerTool(nativeToolDef); + this.registeredTools.add(tool.name); + + console.log(`Registered tool with Codex: ${tool.name}`); + } catch (error) { + const errorMessage = `Failed to register tool ${tool.name}: ${error instanceof Error ? error.message : String(error)}`; + console.error(errorMessage); + // Don't throw - allow other tools to register even if one fails + // Individual tool failures shouldn't block the entire request } } } /** - * Convert ModelRequest to Codex Input format + * Execute a tool via the OpenAI Agents framework + * + * This is the bridge between Codex's tool execution and the framework's tool handlers. + * + * FRAMEWORK INTEGRATION NOTE: + * This method currently returns a placeholder result because the actual execution + * requires integration with the OpenAI Agents framework's tool execution loop. + * + * In a full implementation, this would: + * 1. Emit a "tool_call_requested" event that the framework can listen to + * 2. Wait for the framework to execute the tool and provide the result + * 3. Return that result to Codex + * + * For now, this creates a promise that could be resolved by framework code, + * but the framework integration is not yet complete. + */ + private async executeToolViaFramework( + invocation: NativeToolInvocation + ): Promise { + console.log( + `Tool execution requested by Codex: ${invocation.toolName} (callId: ${invocation.callId})` + ); + + // FRAMEWORK INTEGRATION POINT: + // The framework would need to: + // 1. Listen for tool execution requests (e.g., via an event emitter) + // 2. Execute the tool using its own tool handlers + // 3. Resolve the pending promise with the result + // + // Example integration pattern (not implemented): + // ``` + // this.emit('tool_execution_requested', { + // toolName: invocation.toolName, + // callId: invocation.callId, + // arguments: invocation.arguments, + // onResult: (result: string) => resolve({ output: result, success: true }), + // onError: (error: string) => resolve({ error, success: false }) + // }); + // ``` + + // For now, return a placeholder response indicating the tool was called + // but could not be executed without framework integration + return { + output: JSON.stringify({ + message: "Tool execution via framework is not yet implemented", + toolName: invocation.toolName, + callId: invocation.callId, + arguments: invocation.arguments, + note: "This requires bidirectional communication between Codex and the OpenAI Agents framework. The framework needs to listen for tool execution requests and provide results back to Codex.", + }), + success: false, + error: "Framework integration not complete - tool execution requires the OpenAI Agents framework to handle the tool call and return results", + }; + } + + /** + * Handle image input by converting to local file path + * Supports: base64 data URLs, HTTP(S) URLs, and file IDs (not yet implemented) */ - private convertRequestToInput(request: ModelRequest): Input { + private async handleImageInput(item: any): Promise { + const imageValue = item.image; + + // Case 1: Already a local file path (less common but possible) + if (typeof imageValue === "string") { + // Check if it's a base64 data URL + if (imageValue.startsWith("data:image/")) { + return await this.saveBase64Image(imageValue); + } + // Check if it's an HTTP(S) URL + else if (imageValue.startsWith("http://") || imageValue.startsWith("https://")) { + return await this.downloadImage(imageValue); + } + // Assume it's already a file path + else if (fs.existsSync(imageValue)) { + return imageValue; + } + // Invalid format + else { + throw new Error(`Invalid image format: ${imageValue.substring(0, 50)}...`); + } + } + // Case 2: Object with url property + else if (typeof imageValue === "object" && "url" in imageValue) { + return await this.downloadImage(imageValue.url); + } + // Case 3: Object with fileId property (would need API access to download) + else if (typeof imageValue === "object" && "fileId" in imageValue) { + throw new Error( + `Image fileId references are not yet supported. ` + + `File IDs would need to be downloaded from the service first.` + ); + } + + return null; + } + + /** + * Save base64-encoded image to temporary file + */ + private async saveBase64Image(dataUrl: string): Promise { + // Extract media type and base64 data + const matches = dataUrl.match(/^data:image\/([^;]+);base64,(.+)$/); + if (!matches) { + throw new Error("Invalid base64 image data URL"); + } + + const mediaType = matches[1]; + const base64Data = matches[2]; + if (!base64Data) { + throw new Error("Invalid base64 data in image URL"); + } + + const sanitizedBase64 = base64Data.replace(/\s/g, ""); + if (sanitizedBase64.length === 0) { + throw new Error("Invalid base64 data in image URL"); + } + + if (!/^[A-Za-z0-9+/=_-]+$/.test(sanitizedBase64)) { + throw new Error("Invalid base64 data in image URL"); + } + + const normalizedBase64 = sanitizedBase64.replace(/-/g, "+").replace(/_/g, "/"); + + let buffer: Buffer; + try { + buffer = Buffer.from(normalizedBase64, "base64"); + } catch { + throw new Error("Invalid base64 data in image URL"); + } + + if (buffer.length === 0) { + throw new Error("Invalid base64 data in image URL"); + } + + const reencoded = buffer.toString("base64").replace(/=+$/, ""); + const normalizedInput = normalizedBase64.replace(/=+$/, ""); + if (reencoded !== normalizedInput) { + throw new Error("Invalid base64 data in image URL"); + } + + // Extract extension from media type, handling various formats + // Examples: "png", "jpeg", "svg+xml", "vnd.microsoft.icon" + const extension = this.getExtensionFromMediaType(mediaType, "png"); + + // Create temp file + const tempDir = os.tmpdir(); + const filename = `codex-image-${Date.now()}.${extension}`; + const filepath = path.join(tempDir, filename); + + await fs.promises.writeFile(filepath, buffer); + this.tempImageFiles.add(filepath); + return filepath; + } + + /** + * Download image from URL to temporary file + */ + private async downloadImage(url: string): Promise { + // Use fetch to download the image + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to download image from ${url}: ${response.statusText}`); + } + + const buffer = await response.arrayBuffer(); + const contentType = response.headers.get("content-type") || "image/png"; + + // Extract media type from content-type (e.g., "image/png; charset=utf-8" -> "png") + const mediaTypePart = contentType.split(";")[0]?.trim() || "image/png"; + const mediaType = mediaTypePart.split("/")[1] || "png"; + const extension = this.getExtensionFromMediaType(mediaType, "png"); + + // Create temp file + const tempDir = os.tmpdir(); + const filename = `codex-image-${Date.now()}.${extension}`; + const filepath = path.join(tempDir, filename); + + await fs.promises.writeFile(filepath, Buffer.from(buffer)); + this.tempImageFiles.add(filepath); + return filepath; + } + + /** + * Convert media type to file extension + * Handles special cases like "jpeg" -> "jpg", "svg+xml" -> "svg" + */ + private getExtensionFromMediaType(mediaType: string | undefined, defaultExt: string): string { + if (!mediaType) { + return defaultExt; + } + + // Normalize the media type + const normalized = mediaType.toLowerCase().trim(); + + // Handle special cases + const extensionMap: Record = { + "jpeg": "jpg", + "svg+xml": "svg", + "vnd.microsoft.icon": "ico", + "x-icon": "ico", + }; + + // Check if we have a mapping for this media type + if (extensionMap[normalized]) { + return extensionMap[normalized]; + } + + // For standard types like "png", "gif", "webp", "bmp", "tiff" + // Just use the media type as the extension + const simpleExtension = normalized.split("+")[0]; // Handle cases like "svg+xml" + + // Validate it's a reasonable extension (alphanumeric only) + if (simpleExtension && /^[a-z0-9]+$/.test(simpleExtension)) { + return simpleExtension; + } + + // Fall back to default if we can't determine a valid extension + return defaultExt; + } + + private async convertRequestToInput(request: ModelRequest): Promise { const parts: UserInput[] = []; // Add system instructions as a text preamble if provided @@ -197,20 +526,39 @@ class CodexModel implements Model { if (item.type === "input_text") { parts.push({ type: "text", text: item.text }); } else if (item.type === "input_image") { - // Handle image input - if (typeof item.image === "string") { - // Base64 or URL - we need to save to temp file for Codex - // For now, skip images (TODO: implement temp file handling) - continue; - } else if ("url" in item.image) { - // URL images - skip for now - continue; - } else if ("fileId" in item.image) { - // File ID - skip for now - continue; + // Handle image input - Codex supports local_image with file paths + const imagePath = await this.handleImageInput(item as any); + if (imagePath) { + parts.push({ type: "local_image", path: imagePath }); } + } else if (item.type === "input_file") { + // Files could potentially be handled similarly to images + // For now, throw an error as we'd need to handle different file types + throw new Error( + `CodexProvider does not yet support input_file type. ` + + `File handling needs to be implemented based on file type and format.` + ); + } else if (item.type === "input_audio") { + throw new Error( + `CodexProvider does not yet support input_audio type. ` + + `Audio handling needs to be implemented.` + ); + } else if (item.type === "function_call_result") { + // Tool results - for now, convert to text describing the result + // This allows the agentic loop to continue even if we can't pass structured results + const result = item as any; + parts.push({ + type: "text", + text: `[Tool ${result.name} returned: ${result.result}]` + }); + } else if (item.type === "input_refusal") { + // Model refused - treat as text + const refusal = item as any; + parts.push({ + type: "text", + text: `[Refusal: ${refusal.refusal}]` + }); } - // Other input types (audio, files) are not yet supported } } @@ -227,23 +575,25 @@ class CodexModel implements Model { */ private convertUsage(usage: CodexUsage | null): Usage { if (!usage) { - return { - inputTokens: 0, - outputTokens: 0, - totalTokens: 0, - }; + return { requests: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0 }; } const inputTokensDetails = usage.cached_input_tokens ? [{ cachedTokens: usage.cached_input_tokens }] : undefined; - return { + const converted: Usage = { + requests: 1, inputTokens: usage.input_tokens, outputTokens: usage.output_tokens, totalTokens: usage.input_tokens + usage.output_tokens, - inputTokensDetails, }; + + if (inputTokensDetails) { + converted.inputTokensDetails = inputTokensDetails; + } + + return converted; } /** @@ -267,7 +617,7 @@ class CodexModel implements Model { role: "assistant", status: "completed", content, - } as any); // Using 'any' because our type definition has 'assistant_message' but the framework expects 'message' + }); break; } @@ -305,7 +655,7 @@ class CodexModel implements Model { text: finalResponse, }, ], - } as any); + }); } return output; @@ -316,7 +666,7 @@ class CodexModel implements Model { */ private convertCodexEventToStreamEvent( event: ThreadEvent, - previousText: string + textAccumulator: Map ): StreamEvent[] { const events: StreamEvent[] = []; @@ -330,21 +680,66 @@ class CodexModel implements Model { break; case "item.started": - // Could emit output_text_delta if we had partial text + // Initialize accumulator for this item + if (event.item.type === "agent_message" || event.item.type === "reasoning") { + const itemKey = `${event.item.type}`; + textAccumulator.set(itemKey, ""); + } + break; + + case "item.updated": + // Emit delta events for incremental text updates + if (event.item.type === "agent_message") { + const itemKey = "agent_message"; + const previousText = textAccumulator.get(itemKey) || ""; + const currentText = event.item.text; + + // Validate: current text should be longer than previous (no backwards updates) + if (currentText.length < previousText.length) { + console.warn("Received backwards update for text - ignoring delta"); + break; + } + + if (currentText.length > previousText.length) { + const delta = currentText.slice(previousText.length); + textAccumulator.set(itemKey, currentText); + + events.push({ + type: "output_text_delta", + delta, + }); + } + } else if (event.item.type === "reasoning") { + const itemKey = "reasoning"; + const previousText = textAccumulator.get(itemKey) || ""; + const currentText = event.item.text; + + if (currentText.length > previousText.length) { + const delta = currentText.slice(previousText.length); + textAccumulator.set(itemKey, currentText); + + events.push({ + type: "reasoning_delta", + delta, + }); + } + } break; case "item.completed": if (event.item.type === "agent_message") { - // Emit text done event + // Emit final text done event events.push({ type: "output_text_done", text: event.item.text, }); + textAccumulator.delete("agent_message"); } else if (event.item.type === "reasoning") { events.push({ type: "reasoning_done", reasoning: event.item.text, }); + textAccumulator.delete("reasoning"); } break; @@ -354,7 +749,7 @@ class CodexModel implements Model { type: "response_done", response: { usage: this.convertUsage(event.usage), - output: [], // Items were already emitted + output: [], // Items were already emitted as deltas responseId: this.thread?.id || undefined, }, }); @@ -369,6 +764,15 @@ class CodexModel implements Model { }); break; + case "error": + events.push({ + type: "error", + error: { + message: event.message, + }, + }); + break; + default: // Unknown event type - skip break; diff --git a/sdk/native/src/agents/index.ts b/sdk/native/src/agents/index.ts index cc6eaf63a..e6aadb285 100644 --- a/sdk/native/src/agents/index.ts +++ b/sdk/native/src/agents/index.ts @@ -10,8 +10,7 @@ * import { Agent, Runner } from '@openai/agents'; * * const provider = new CodexProvider({ - * apiKey: process.env.CODEX_API_KEY, - * defaultModel: 'claude-sonnet-4.5' + * defaultModel: 'gpt-5-codex' * }); * * const agent = new Agent({ diff --git a/sdk/native/src/agents/types.ts b/sdk/native/src/agents/types.ts index 0a721f7c5..9c37ba5fb 100644 --- a/sdk/native/src/agents/types.ts +++ b/sdk/native/src/agents/types.ts @@ -136,6 +136,7 @@ export interface ModelResponse { } export interface Usage { + requests: number; inputTokens: number; outputTokens: number; totalTokens: number; @@ -156,9 +157,9 @@ export type AgentOutputItem = | Reasoning; export interface AssistantMessageItem { - type: "assistant_message"; + type?: "message" | "assistant_message"; role: "assistant"; - status: "in_progress" | "completed"; + status: "in_progress" | "completed" | "incomplete"; content: OutputContent[]; } diff --git a/sdk/native/src/codex.ts b/sdk/native/src/codex.ts index 6c7584414..4369b00e4 100644 --- a/sdk/native/src/codex.ts +++ b/sdk/native/src/codex.ts @@ -1,8 +1,14 @@ import { CodexOptions, NativeToolDefinition } from "./codexOptions"; import { CodexExec } from "./exec"; import { NativeBinding, getNativeBinding } from "./nativeBinding"; +import type { StreamedTurn, Turn } from "./thread"; import { Thread } from "./thread"; import { ThreadOptions } from "./threadOptions"; +import { TurnOptions } from "./turnOptions"; +import { ThreadEvent, ThreadError, Usage } from "./events"; +import { ThreadItem } from "./items"; +import { createOutputSchemaFile, normalizeOutputSchema } from "./outputSchemaFile"; +import { buildReviewPrompt, type ReviewInvocationOptions } from "./reviewOptions"; /** * Codex is the main class for interacting with the Codex agent. @@ -32,6 +38,10 @@ export class Codex { this.exec = new CodexExec(); } + /** + * Register a tool for Codex. When `tool.name` matches a built-in Codex tool, + * the native implementation is replaced for this Codex instance. + */ registerTool(tool: NativeToolDefinition): void { if (!this.nativeBinding) { throw new Error("Native tool registration requires the NAPI binding"); @@ -49,6 +59,21 @@ export class Codex { this.options.tools.push(tool); } + /** + * Clear all registered tools, restoring built-in defaults. + */ + clearTools(): void { + if (!this.nativeBinding) { + throw new Error("Native tool management requires the NAPI binding"); + } + if (typeof this.nativeBinding.clearRegisteredTools === 'function') { + this.nativeBinding.clearRegisteredTools(); + } + if (this.options.tools) { + this.options.tools = []; + } + } + /** * Starts a new conversation with an agent. * @returns A new thread instance. @@ -67,4 +92,104 @@ export class Codex { resumeThread(id: string, options: ThreadOptions = {}): Thread { return new Thread(this.exec, this.options, options, id); } + + /** + * Starts a review task using the built-in Codex review flow. + */ + async review(options: ReviewInvocationOptions): Promise { + const generator = this.reviewStreamedInternal(options); + const items: ThreadItem[] = []; + let finalResponse = ""; + let usage: Usage | null = null; + let turnFailure: ThreadError | null = null; + for await (const event of generator) { + if (event.type === "item.completed") { + if (event.item.type === "agent_message") { + finalResponse = event.item.text; + } + items.push(event.item); + } else if (event.type === "exited_review_mode") { + // Capture the structured review output + if (event.review_output) { + const reviewOutput = event.review_output; + let reviewText = ""; + + // Add overall explanation + if (reviewOutput.overall_explanation) { + reviewText += reviewOutput.overall_explanation; + } + + // Add findings if present + if (reviewOutput.findings && reviewOutput.findings.length > 0) { + if (reviewText) reviewText += "\n\n"; + reviewText += "## Review Findings\n\n"; + reviewOutput.findings.forEach((finding, index) => { + reviewText += `### ${index + 1}. ${finding.title}\n`; + reviewText += `${finding.body}\n`; + reviewText += `**Priority:** ${finding.priority} | **Confidence:** ${finding.confidence_score}\n`; + reviewText += `**Location:** ${finding.code_location.absolute_file_path}:${finding.code_location.line_range.start}-${finding.code_location.line_range.end}\n\n`; + }); + } + + finalResponse = reviewText; + } + } else if (event.type === "turn.completed") { + usage = event.usage; + } else if (event.type === "turn.failed") { + turnFailure = event.error; + break; + } + } + if (turnFailure) { + throw new Error(turnFailure.message); + } + return { items, finalResponse, usage }; + } + + /** + * Starts a review task and returns the event stream. + */ + async reviewStreamed(options: ReviewInvocationOptions): Promise { + return { events: this.reviewStreamedInternal(options) }; + } + + private async *reviewStreamedInternal( + options: ReviewInvocationOptions, + ): AsyncGenerator { + const { target, threadOptions = {}, turnOptions = {} } = options; + const { prompt, hint } = buildReviewPrompt(target); + const normalizedSchema = normalizeOutputSchema(turnOptions.outputSchema); + const needsSchemaFile = this.exec.requiresOutputSchemaFile(); + const schemaFile = needsSchemaFile + ? await createOutputSchemaFile(normalizedSchema) + : { schemaPath: undefined, cleanup: async () => {} }; + const generator = this.exec.run({ + input: prompt, + baseUrl: this.options.baseUrl, + apiKey: this.options.apiKey, + model: threadOptions.model, + sandboxMode: threadOptions.sandboxMode, + workingDirectory: threadOptions.workingDirectory, + skipGitRepoCheck: threadOptions.skipGitRepoCheck, + outputSchemaFile: schemaFile.schemaPath, + outputSchema: normalizedSchema, + fullAuto: threadOptions.fullAuto, + review: { + userFacingHint: hint, + }, + }); + try { + for await (const item of generator) { + let parsed: ThreadEvent; + try { + parsed = JSON.parse(item) as ThreadEvent; + } catch (error) { + throw new Error(`Failed to parse item: ${item}`, { cause: error }); + } + yield parsed; + } + } finally { + await schemaFile.cleanup(); + } + } } diff --git a/sdk/native/src/events.ts b/sdk/native/src/events.ts index b8adcfb4b..791f20710 100644 --- a/sdk/native/src/events.ts +++ b/sdk/native/src/events.ts @@ -68,6 +68,35 @@ export type ThreadErrorEvent = { message: string; }; +/** Review finding with code location */ +export type ReviewFinding = { + title: string; + body: string; + confidence_score: number; + priority: number; + code_location: { + absolute_file_path: string; + line_range: { + start: number; + end: number; + }; + }; +}; + +/** Structured review output */ +export type ReviewOutputEvent = { + findings: ReviewFinding[]; + overall_correctness: string; + overall_explanation: string; + overall_confidence_score: number; +}; + +/** Emitted when exiting review mode with optional structured results */ +export type ExitedReviewModeEvent = { + type: "exited_review_mode"; + review_output: ReviewOutputEvent | null; +}; + /** Top-level JSONL events emitted by codex exec. */ export type ThreadEvent = | ThreadStartedEvent @@ -77,4 +106,12 @@ export type ThreadEvent = | ItemStartedEvent | ItemUpdatedEvent | ItemCompletedEvent - | ThreadErrorEvent; + | ExitedReviewModeEvent + | ThreadErrorEvent + | RawThreadEvent; + +/** Raw protocol event forwarded without transformation. */ +export type RawThreadEvent = { + type: "raw_event"; + raw: unknown; +}; diff --git a/sdk/native/src/exec.ts b/sdk/native/src/exec.ts index 2503c9b2c..8c744582c 100644 --- a/sdk/native/src/exec.ts +++ b/sdk/native/src/exec.ts @@ -13,6 +13,12 @@ export type CodexExecArgs = { skipGitRepoCheck?: boolean; outputSchemaFile?: string; outputSchema?: unknown; + fullAuto?: boolean; + review?: ReviewExecOptions | null; +}; + +export type ReviewExecOptions = { + userFacingHint?: string; }; /** @@ -32,6 +38,10 @@ export class CodexExec { this.native = nativeBinding; } + requiresOutputSchemaFile(): boolean { + return false; + } + async *run(args: CodexExecArgs): AsyncGenerator { const binding = this.native; const queue = new AsyncQueue(); @@ -47,9 +57,12 @@ export class CodexExec { outputSchema: args.outputSchema, baseUrl: args.baseUrl, apiKey: args.apiKey, + fullAuto: args.fullAuto, + reviewMode: args.review ? true : undefined, + reviewHint: args.review?.userFacingHint, }; - let runPromise: Promise; + let runPromise: Promise = Promise.resolve(); try { runPromise = binding .runThreadStream(request, (err, eventJson) => { diff --git a/sdk/native/src/index.ts b/sdk/native/src/index.ts index b952e02e5..42e585f13 100644 --- a/sdk/native/src/index.ts +++ b/sdk/native/src/index.ts @@ -33,6 +33,14 @@ export type { NativeToolInvocation, NativeToolResult } from "./nativeBinding"; export type { ThreadOptions, ApprovalMode, SandboxMode } from "./threadOptions"; export type { TurnOptions } from "./turnOptions"; +export type { + ReviewInvocationOptions, + ReviewTarget, + CurrentChangesReview, + BranchReview, + CommitReview, + CustomReview, +} from "./reviewOptions"; // OpenAI Agents framework integration export { CodexProvider } from "./agents"; diff --git a/sdk/native/src/nativeBinding.ts b/sdk/native/src/nativeBinding.ts index 0635c7237..7b27c0479 100644 --- a/sdk/native/src/nativeBinding.ts +++ b/sdk/native/src/nativeBinding.ts @@ -14,6 +14,9 @@ export type NativeRunRequest = { baseUrl?: string; apiKey?: string; linuxSandboxPath?: string; + fullAuto?: boolean; + reviewMode?: boolean; + reviewHint?: string; }; export type NativeBinding = { diff --git a/sdk/native/src/outputSchemaFile.ts b/sdk/native/src/outputSchemaFile.ts index 13adb4c72..cca0714c3 100644 --- a/sdk/native/src/outputSchemaFile.ts +++ b/sdk/native/src/outputSchemaFile.ts @@ -7,15 +7,33 @@ export type OutputSchemaFile = { cleanup: () => Promise; }; -export async function createOutputSchemaFile(schema: unknown): Promise { +export function normalizeOutputSchema(schema: unknown): Record | undefined { if (schema === undefined) { - return { cleanup: async () => {} }; + return undefined; } if (!isJsonObject(schema)) { throw new Error("outputSchema must be a plain JSON object"); } + const record = { ...schema } as Record & { + additionalProperties?: unknown; + }; + const additionalProperties = + typeof record.additionalProperties === "boolean" ? record.additionalProperties : false; + + return { + ...record, + additionalProperties, + }; +} + +export async function createOutputSchemaFile(schema: unknown): Promise { + const normalizedSchema = normalizeOutputSchema(schema); + if (!normalizedSchema) { + return { cleanup: async () => {} }; + } + const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-")); const schemaPath = path.join(schemaDir, "schema.json"); const cleanup = async () => { @@ -27,7 +45,7 @@ export async function createOutputSchemaFile(schema: unknown): Promise { - const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema); + const normalizedSchema = normalizeOutputSchema(turnOptions.outputSchema); + const needsSchemaFile = this._exec.requiresOutputSchemaFile(); + const schemaFile = needsSchemaFile + ? await createOutputSchemaFile(normalizedSchema) + : { schemaPath: undefined, cleanup: async () => {} }; const options = this._threadOptions; const { prompt, images } = normalizeInput(input); const generator = this._exec.run({ @@ -84,8 +88,9 @@ export class Thread { sandboxMode: options?.sandboxMode, workingDirectory: options?.workingDirectory, skipGitRepoCheck: options?.skipGitRepoCheck, - outputSchemaFile: schemaPath, - outputSchema: turnOptions.outputSchema, + outputSchemaFile: schemaFile.schemaPath, + outputSchema: normalizedSchema, + fullAuto: options?.fullAuto, }); try { for await (const item of generator) { @@ -101,7 +106,7 @@ export class Thread { yield parsed; } } finally { - await cleanup(); + await schemaFile.cleanup(); } } diff --git a/sdk/native/src/threadOptions.ts b/sdk/native/src/threadOptions.ts index 7f01488d2..35693156f 100644 --- a/sdk/native/src/threadOptions.ts +++ b/sdk/native/src/threadOptions.ts @@ -7,4 +7,5 @@ export type ThreadOptions = { sandboxMode?: SandboxMode; workingDirectory?: string; skipGitRepoCheck?: boolean; + fullAuto?: boolean; }; diff --git a/sdk/native/tests/agents-handoffs.test.mjs b/sdk/native/tests/agents-handoffs.test.mjs index bf034c25f..47360b3f7 100644 --- a/sdk/native/tests/agents-handoffs.test.mjs +++ b/sdk/native/tests/agents-handoffs.test.mjs @@ -5,7 +5,7 @@ * tasks to specialized agents using handoffs. */ -import { describe, expect, it, beforeAll } from "@jest/globals"; +import { describe, expect, it, beforeAll, beforeEach, jest } from "@jest/globals"; import { fileURLToPath } from "node:url"; function resolveNativeBindingPath() { @@ -37,269 +37,197 @@ beforeAll(async () => { ({ CodexProvider } = await import("../dist/index.mjs")); }); +/** + * Create a mock Codex thread that simulates agent responses + */ +function createMockThread(responseText) { + const createUsage = () => ({ + input_tokens: 10, + output_tokens: 5, + reasoning_tokens: 0, + }); + + return { + id: 'mock-thread', + run: jest.fn(async () => ({ + items: [ + { + type: 'agent_message', + text: responseText, + }, + ], + finalResponse: responseText, + usage: createUsage(), + })), + runStreamed: jest.fn(async () => ({ + events: (async function* () { + const usage = createUsage(); + yield { type: 'thread.started', thread_id: 'mock-thread' }; + yield { type: 'item.started', item: { type: 'agent_message', text: '' } }; + yield { type: 'item.updated', item: { type: 'agent_message', text: responseText } }; + yield { type: 'item.completed', item: { type: 'agent_message', text: responseText } }; + yield { type: 'turn.completed', usage }; + })(), + })), + }; +} + describe("CodexProvider - Agent Handoffs", () => { + let provider; + let mockCodex; + + beforeEach(() => { + jest.clearAllMocks(); + + mockCodex = { + startThread: jest.fn(), + resumeThread: jest.fn(), + registerTool: jest.fn(), + }; + + provider = new CodexProvider({ + apiKey: "test", + skipGitRepoCheck: true, + }); + + // Inject mock Codex instance + provider.codex = mockCodex; + }); + it("works with agent handoffs using mock backend", async () => { const { Agent, Runner } = await import("@openai/agents"); - const { - startResponsesTestProxy, - sse, - responseStarted, - assistantMessage, - responseCompleted, - } = await import("./responsesProxy.mjs"); - - // Set up mock responses for the handoff flow - // Response 1: Primary agent recognizes need for specialist and hands off - // Response 2: Specialist agent handles the task - const { url, close, requests } = await startResponsesTestProxy({ - statusCode: 200, - responseBodies: [ - // First response: Primary agent delegates to code specialist - sse( - responseStarted("response_1"), - assistantMessage( - "I see you need code review. Let me hand this off to the Code Specialist.", - "item_1" - ), - responseCompleted("response_1") - ), - // Second response: Code specialist handles the review - sse( - responseStarted("response_2"), - assistantMessage( - "I've reviewed the code and found: The function looks good, but could use better error handling.", - "item_2" - ), - responseCompleted("response_2") - ), - ], + + // Setup mock thread for primary agent + const primaryResponse = "I see you need code review. Let me hand this off to the Code Specialist."; + const mockThread = createMockThread(primaryResponse); + mockCodex.startThread.mockReturnValue(mockThread); + + // Create agents with handoff + const codeSpecialist = new Agent({ + name: "CodeSpecialist", + instructions: "You are an expert code reviewer. You analyze code quality, security, and best practices.", + handoffDescription: "Expert at code review, refactoring, and identifying bugs", }); - try { - const provider = new CodexProvider({ - baseUrl: url, - apiKey: "test", - skipGitRepoCheck: true, - }); - - // Create specialized code review agent - const codeSpecialist = new Agent({ - name: "CodeSpecialist", - instructions: "You are an expert code reviewer. You analyze code quality, security, and best practices.", - handoffDescription: "Expert at code review, refactoring, and identifying bugs", - }); - - // Create primary agent with handoff to specialist - const primaryAgent = new Agent({ - name: "PrimaryAgent", - instructions: "You are a general-purpose assistant. Delegate code review tasks to the CodeSpecialist.", - handoffs: [codeSpecialist], - }); - - const runner = new Runner({ modelProvider: provider }); - const result = await runner.run(primaryAgent, "Can you review this function for me?"); - - // Verify the runner got a result - expect(result).toBeDefined(); - expect(result.finalOutput).toBeDefined(); - - // Verify handoff occurred by checking multiple requests were made - expect(requests.length).toBeGreaterThanOrEqual(1); - - // The result should contain specialist's analysis - expect(result.finalOutput.toLowerCase()).toMatch(/code|review|function/i); - } finally { - await close(); - } - }, 20000); // Longer timeout for multi-agent workflow + const primaryAgent = new Agent({ + name: "PrimaryAgent", + instructions: "You are a general-purpose assistant. Delegate code review tasks to the CodeSpecialist.", + handoffs: [codeSpecialist], + }); + + const runner = new Runner({ modelProvider: provider }); + const result = await runner.run(primaryAgent, "Can you review this function for me?"); + + // Verify the runner got a result + expect(result).toBeDefined(); + expect(result.finalOutput).toBeDefined(); + expect(result.finalOutput).toContain("code review"); + + // Verify thread was started + expect(mockCodex.startThread).toHaveBeenCalled(); + }); it("works with multiple specialized agents", async () => { const { Agent, Runner } = await import("@openai/agents"); - const { - startResponsesTestProxy, - sse, - responseStarted, - assistantMessage, - responseCompleted, - } = await import("./responsesProxy.mjs"); - - const { url, close } = await startResponsesTestProxy({ - statusCode: 200, - responseBodies: [ - // Response from bug fixer - sse( - responseStarted("response_1"), - assistantMessage("Bug fixed! The issue was a null pointer exception.", "item_1"), - responseCompleted("response_1") - ), - // Response from test writer - sse( - responseStarted("response_2"), - assistantMessage("Tests written! Added unit tests for edge cases.", "item_2"), - responseCompleted("response_2") - ), - ], + + // Setup mock thread that simulates coordinator delegating to specialists + const coordinatorResponse = "I'll coordinate with the specialists to fix bugs and add tests."; + const mockThread = createMockThread(coordinatorResponse); + mockCodex.startThread.mockReturnValue(mockThread); + + // Create multiple specialized agents + const bugFixer = new Agent({ + name: "BugFixer", + instructions: "You fix bugs in code", + handoffDescription: "Specialist in debugging and fixing code issues", }); - try { - const provider = new CodexProvider({ - baseUrl: url, - apiKey: "test", - skipGitRepoCheck: true, - }); - - // Create multiple specialized agents - const bugFixer = new Agent({ - name: "BugFixer", - instructions: "You fix bugs in code", - handoffDescription: "Specialist in debugging and fixing code issues", - }); - - const testWriter = new Agent({ - name: "TestWriter", - instructions: "You write comprehensive tests", - handoffDescription: "Expert at writing unit and integration tests", - }); - - // Primary agent can delegate to multiple specialists - const coordinator = new Agent({ - name: "Coordinator", - instructions: "You coordinate work between specialists", - handoffs: [bugFixer, testWriter], - }); - - const runner = new Runner({ modelProvider: provider }); - const result = await runner.run(coordinator, "Fix bugs and add tests"); - - expect(result).toBeDefined(); - expect(result.finalOutput).toBeDefined(); - } finally { - await close(); - } - }, 20000); + const testWriter = new Agent({ + name: "TestWriter", + instructions: "You write comprehensive tests", + handoffDescription: "Expert at writing unit and integration tests", + }); + + // Primary agent can delegate to multiple specialists + const coordinator = new Agent({ + name: "Coordinator", + instructions: "You coordinate work between specialists", + handoffs: [bugFixer, testWriter], + }); + + const runner = new Runner({ modelProvider: provider }); + const result = await runner.run(coordinator, "Fix bugs and add tests"); + + expect(result).toBeDefined(); + expect(result.finalOutput).toBeDefined(); + expect(mockCodex.startThread).toHaveBeenCalled(); + }); it("handles handoff chain (agent A -> agent B -> agent C)", async () => { const { Agent, Runner } = await import("@openai/agents"); - const { - startResponsesTestProxy, - sse, - responseStarted, - assistantMessage, - responseCompleted, - } = await import("./responsesProxy.mjs"); - - const { url, close } = await startResponsesTestProxy({ - statusCode: 200, - responseBodies: [ - sse( - responseStarted("response_1"), - assistantMessage("Coordinator: Delegating to Analyzer...", "item_1"), - responseCompleted("response_1") - ), - sse( - responseStarted("response_2"), - assistantMessage("Analyzer: Analysis complete. Delegating to Fixer...", "item_2"), - responseCompleted("response_2") - ), - sse( - responseStarted("response_3"), - assistantMessage("Fixer: All issues resolved!", "item_3"), - responseCompleted("response_3") - ), - ], + + // Setup mock thread that simulates a chain of handoffs + const chainResponse = "Coordinator delegated to Analyzer, which delegated to Fixer. All issues resolved!"; + const mockThread = createMockThread(chainResponse); + mockCodex.startThread.mockReturnValue(mockThread); + + // Create a chain of agents: Coordinator -> Analyzer -> Fixer + const fixer = new Agent({ + name: "Fixer", + instructions: "You fix identified issues", + handoffDescription: "Fixes problems found during analysis", }); - try { - const provider = new CodexProvider({ - baseUrl: url, - apiKey: "test", - skipGitRepoCheck: true, - }); - - // Create a chain of agents: Coordinator -> Analyzer -> Fixer - const fixer = new Agent({ - name: "Fixer", - instructions: "You fix identified issues", - handoffDescription: "Fixes problems found during analysis", - }); - - const analyzer = new Agent({ - name: "Analyzer", - instructions: "You analyze code for issues", - handoffDescription: "Analyzes code and identifies problems", - handoffs: [fixer], - }); - - const coordinator = new Agent({ - name: "Coordinator", - instructions: "You coordinate the workflow", - handoffs: [analyzer], - }); - - const runner = new Runner({ modelProvider: provider }); - const result = await runner.run(coordinator, "Analyze and fix this code"); - - expect(result).toBeDefined(); - expect(result.finalOutput).toBeDefined(); - // Verify the workflow ran (could be coordinator, analyzer, or fixer) - expect(result.finalOutput.toLowerCase()).toMatch(/coordinat|analyz|fix|resolve|complet/i); - } finally { - await close(); - } - }, 25000); + const analyzer = new Agent({ + name: "Analyzer", + instructions: "You analyze code for issues", + handoffDescription: "Analyzes code and identifies problems", + handoffs: [fixer], + }); + + const coordinator = new Agent({ + name: "Coordinator", + instructions: "You coordinate the workflow", + handoffs: [analyzer], + }); + + const runner = new Runner({ modelProvider: provider }); + const result = await runner.run(coordinator, "Analyze and fix this code"); + + expect(result).toBeDefined(); + expect(result.finalOutput).toBeDefined(); + expect(result.finalOutput.toLowerCase()).toMatch(/delegat|fix|resolv/i); + expect(mockCodex.startThread).toHaveBeenCalled(); + }); it("verifies handoff data is passed in ModelRequest", async () => { const { Agent, Runner } = await import("@openai/agents"); - const { - startResponsesTestProxy, - sse, - responseStarted, - assistantMessage, - responseCompleted, - } = await import("./responsesProxy.mjs"); - - const { url, close, requests } = await startResponsesTestProxy({ - statusCode: 200, - responseBodies: [ - sse( - responseStarted("response_1"), - assistantMessage("Task completed!", "item_1"), - responseCompleted("response_1") - ), - ], + + // Setup mock thread + const responseText = "Task completed!"; + const mockThread = createMockThread(responseText); + mockCodex.startThread.mockReturnValue(mockThread); + + const specialist = new Agent({ + name: "Specialist", + instructions: "You are a specialist", + handoffDescription: "Handles specialized tasks", }); - try { - const provider = new CodexProvider({ - baseUrl: url, - apiKey: "test", - skipGitRepoCheck: true, - }); - - const specialist = new Agent({ - name: "Specialist", - instructions: "You are a specialist", - handoffDescription: "Handles specialized tasks", - }); - - const primary = new Agent({ - name: "Primary", - instructions: "You are the primary agent", - handoffs: [specialist], - }); - - const runner = new Runner({ modelProvider: provider }); - await runner.run(primary, "Do something"); - - // Verify that our provider received the handoff information - // (The framework serializes handoffs and passes them in ModelRequest) - expect(requests.length).toBeGreaterThan(0); - - // The request should have been made through our provider - const firstRequest = requests[0]; - expect(firstRequest).toBeDefined(); - } finally { - await close(); - } - }, 15000); + const primary = new Agent({ + name: "Primary", + instructions: "You are the primary agent", + handoffs: [specialist], + }); + + const runner = new Runner({ modelProvider: provider }); + const result = await runner.run(primary, "Do something"); + + // Verify provider was called + expect(result).toBeDefined(); + expect(result.finalOutput).toBe(responseText); + + // Verify thread was created + expect(mockCodex.startThread).toHaveBeenCalled(); + }); }); diff --git a/sdk/native/tests/agents-integration.test.mjs b/sdk/native/tests/agents-integration.test.mjs index eb6cc9bb0..01bb91116 100644 --- a/sdk/native/tests/agents-integration.test.mjs +++ b/sdk/native/tests/agents-integration.test.mjs @@ -44,7 +44,6 @@ describe("CodexProvider - OpenAI Agents Integration", () => { describe("Provider Interface", () => { it("implements ModelProvider interface", () => { const provider = new CodexProvider({ - apiKey: "test-key", skipGitRepoCheck: true, }); @@ -54,11 +53,10 @@ describe("CodexProvider - OpenAI Agents Integration", () => { it("returns a Model instance from getModel()", () => { const provider = new CodexProvider({ - apiKey: "test-key", skipGitRepoCheck: true, }); - const model = provider.getModel("claude-sonnet-4.5"); + const model = provider.getModel("gpt-5-codex"); expect(model).toBeDefined(); expect(typeof model.getResponse).toBe("function"); @@ -83,7 +81,6 @@ describe("CodexProvider - OpenAI Agents Integration", () => { describe("Model Interface", () => { it("implements getResponse() method", async () => { const provider = new CodexProvider({ - apiKey: "test-key", skipGitRepoCheck: true, }); @@ -99,7 +96,6 @@ describe("CodexProvider - OpenAI Agents Integration", () => { it("implements getStreamedResponse() method", async () => { const provider = new CodexProvider({ - apiKey: "test-key", skipGitRepoCheck: true, }); @@ -147,7 +143,7 @@ describe("CodexProvider - OpenAI Agents Integration", () => { const options = { apiKey: "test-key", baseUrl: "https://test.example.com", - defaultModel: "claude-sonnet-4.5", + defaultModel: "gpt-5-codex", workingDirectory: "/tmp", skipGitRepoCheck: true, }; @@ -191,7 +187,6 @@ describe("CodexProvider - OpenAI Agents Integration", () => { try { const provider = new CodexProvider({ baseUrl: url, - apiKey: "test", skipGitRepoCheck: true, }); @@ -214,14 +209,12 @@ describe("CodexProvider - OpenAI Agents Integration", () => { } }, 15000); // Longer timeout for runner execution - it.skip("works with real Codex backend (requires CODEX_API_KEY)", async () => { - // This test requires a real Codex backend - // Run with: CODEX_API_KEY=... npm test -- agents-integration + it("works with real Codex backend", async () => { + // This test requires a real Codex backend (no API key needed) const { Agent, Runner } = await import("@openai/agents"); const provider = new CodexProvider({ - apiKey: process.env.CODEX_API_KEY, skipGitRepoCheck: true, }); diff --git a/sdk/native/tests/agents-tools.test.mjs b/sdk/native/tests/agents-tools.test.mjs index 72a345f6f..3c748bc71 100644 --- a/sdk/native/tests/agents-tools.test.mjs +++ b/sdk/native/tests/agents-tools.test.mjs @@ -120,13 +120,13 @@ describe('Agents with Tools', () => { // Test agent configuration structure const agentConfig = { name: 'TestAgent', - model: 'gpt-5', // Using GPT-5 as specified + model: 'gpt-5-codex', // Using Codex default model instructions: 'You are a test agent.', tools: [], }; expect(agentConfig.name).toBe('TestAgent'); - expect(agentConfig.model).toBe('gpt-5'); + expect(agentConfig.model).toBe('gpt-5-codex'); expect(agentConfig.instructions).toBeDefined(); expect(Array.isArray(agentConfig.tools)).toBe(true); }); diff --git a/sdk/native/tests/cli_builder.rs b/sdk/native/tests/cli_builder.rs new file mode 100644 index 000000000..83a6b6ef0 --- /dev/null +++ b/sdk/native/tests/cli_builder.rs @@ -0,0 +1,114 @@ +use codex_common::SandboxModeCliArg; +use codex_exec::Command; +use codex_native::*; +use pretty_assertions::assert_eq; +use std::path::PathBuf; + +#[test] +fn test_build_cli_new_conversation() { + let options = InternalRunRequest { + prompt: "hello".to_string(), + thread_id: None, + images: vec![PathBuf::from("/test/image.png")], + model: Some("gpt-4".to_string()), + sandbox_mode: Some(SandboxModeCliArg::WorkspaceWrite), + working_directory: Some(PathBuf::from("/workspace")), + skip_git_repo_check: true, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: true, + review_request: None, + }; + + let cli = build_cli(&options, None); + assert!(cli.command.is_none()); + assert_eq!(cli.prompt, Some("hello".to_string())); + assert_eq!(cli.images.len(), 1); + assert_eq!(cli.model, Some("gpt-4".to_string())); + assert_eq!(cli.sandbox_mode, Some(SandboxModeCliArg::WorkspaceWrite)); + assert!(cli.full_auto); + assert!(cli.skip_git_repo_check); +} + +#[test] +fn test_build_cli_resume_conversation() { + let options = InternalRunRequest { + prompt: "continue".to_string(), + thread_id: Some("thread-123".to_string()), + images: vec![], + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: false, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: false, + review_request: None, + }; + + let cli = build_cli(&options, None); + assert!(cli.command.is_some()); + assert_eq!(cli.prompt, None); + + if let Some(Command::Resume(resume_args)) = cli.command { + assert_eq!(resume_args.session_id, Some("thread-123".to_string())); + assert_eq!(resume_args.prompt, Some("continue".to_string())); + assert!(!resume_args.last); + } else { + panic!("Expected Resume command"); + } +} + +#[test] +fn test_build_cli_with_schema_path() { + let schema_path = PathBuf::from("/tmp/schema.json"); + let options = InternalRunRequest { + prompt: "test".to_string(), + thread_id: None, + images: vec![], + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: false, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: false, + review_request: None, + }; + + let cli = build_cli(&options, Some(schema_path.clone())); + assert_eq!(cli.output_schema, Some(schema_path)); +} + +#[test] +fn test_build_cli_minimal() { + let options = InternalRunRequest { + prompt: "minimal".to_string(), + thread_id: None, + images: vec![], + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: false, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: false, + review_request: None, + }; + + let cli = build_cli(&options, None); + assert!(cli.command.is_none()); + assert_eq!(cli.prompt, Some("minimal".to_string())); + assert!(cli.images.is_empty()); + assert!(cli.model.is_none()); + assert!(!cli.full_auto); + assert!(!cli.skip_git_repo_check); +} diff --git a/sdk/native/tests/codex-provider.test.mjs b/sdk/native/tests/codex-provider.test.mjs new file mode 100644 index 000000000..92968339a --- /dev/null +++ b/sdk/native/tests/codex-provider.test.mjs @@ -0,0 +1,477 @@ +/** + * Tests for CodexProvider - OpenAI Agents framework integration + */ + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { CodexProvider } from '../dist/index.mjs'; + +const createMockThread = () => { + const createUsage = () => ({ + input_tokens: 10, + output_tokens: 5, + reasoning_tokens: 0, + }); + + return { + id: 'mock-thread', + run: jest.fn(async () => ({ + items: [ + { + type: 'agent_message', + text: 'Mock response', + }, + ], + finalResponse: 'Mock response', + usage: createUsage(), + })), + runStreamed: jest.fn(async () => ({ + events: (async function* () { + const usage = createUsage(); + yield { type: 'thread.started', thread_id: 'mock-thread' }; + yield { type: 'item.started', item: { type: 'agent_message', text: '' } }; + yield { type: 'item.updated', item: { type: 'agent_message', text: 'Mock response' } }; + yield { type: 'item.completed', item: { type: 'agent_message', text: 'Mock response' } }; + yield { type: 'turn.completed', usage }; + })(), + })), + }; +}; + +describe('CodexProvider', () => { + let provider; + let mockCodex; + let mockThread; + let originalFetch; + let mockFetch; + + beforeEach(() => { + jest.clearAllMocks(); + + originalFetch = global.fetch; + mockFetch = jest.fn(async () => new Response(new Uint8Array([0x89, 0x50, 0x4e, 0x47]), { + status: 200, + headers: { 'Content-Type': 'image/png' }, + })); + global.fetch = mockFetch; + + mockThread = createMockThread(); + mockCodex = { + startThread: jest.fn(() => mockThread), + resumeThread: jest.fn(() => mockThread), + registerTool: jest.fn(), + }; + + provider = new CodexProvider({ + defaultModel: 'gpt-5-codex', + workingDirectory: process.cwd(), + skipGitRepoCheck: true, + }); + + // Inject mock Codex instance to avoid network/native binding usage + provider.codex = mockCodex; + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + describe('ModelProvider Interface', () => { + it('should implement getModel method', () => { + expect(provider.getModel).toBeDefined(); + expect(typeof provider.getModel).toBe('function'); + }); + + it('should return a Model instance from getModel()', () => { + const model = provider.getModel(); + expect(model).toBeDefined(); + expect(model.getResponse).toBeDefined(); + expect(model.getStreamedResponse).toBeDefined(); + }); + + it('should return a Model with specific model name', () => { + const model = provider.getModel('gpt-5-codex'); + expect(model).toBeDefined(); + }); + + it('should use default model when no name provided', () => { + const model = provider.getModel(); + expect(model).toBeDefined(); + }); + }); + + describe('Model Interface', () => { + let model; + + beforeEach(() => { + model = provider.getModel(); + }); + + it('should have getResponse method', () => { + expect(model.getResponse).toBeDefined(); + expect(typeof model.getResponse).toBe('function'); + }); + + it('should have getStreamedResponse method', () => { + expect(model.getStreamedResponse).toBeDefined(); + expect(typeof model.getStreamedResponse).toBe('function'); + }); + }); + + describe('Request Conversion', () => { + let model; + + beforeEach(() => { + model = provider.getModel(); + }); + + it('should handle string input', async () => { + const request = { + input: 'Hello, world!', + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).resolves.toMatchObject({ + output: expect.any(Array), + }); + }); + + it('should handle text input items', async () => { + const request = { + input: [ + { type: 'input_text', text: 'Hello' }, + { type: 'input_text', text: 'World' }, + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).resolves.toMatchObject({ + output: expect.any(Array), + }); + }); + + it('should handle base64 image input', async () => { + const request = { + input: [ + { + type: 'input_image', + image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' + }, + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).resolves.toMatchObject({ + output: expect.any(Array), + }); + }); + + it('should handle image URL input', async () => { + const request = { + input: [ + { + type: 'input_image', + image: 'https://example.com/image.png' + }, + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).resolves.toMatchObject({ + output: expect.any(Array), + }); + }); + + it('should handle image object with URL property', async () => { + const request = { + input: [ + { + type: 'input_image', + image: { url: 'https://example.com/image.jpg' } + }, + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).resolves.toMatchObject({ + output: expect.any(Array), + }); + }); + + it('should reject invalid base64 image format', async () => { + const request = { + input: [ + { + type: 'input_image', + image: 'data:image/png;base64,invalid!!!' + }, + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + // Should reject invalid base64 data + await expect(model.getResponse(request)).rejects.toThrow(); + }); + + it('should throw error for image fileId references', async () => { + const request = { + input: [ + { + type: 'input_image', + image: { fileId: 'file-123' } + }, + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).rejects.toThrow( + 'Image fileId references are not yet supported' + ); + }); + + it('should throw error for unsupported file input', async () => { + const request = { + input: [ + { type: 'input_file', file: { fileId: '123' } }, + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).rejects.toThrow( + 'does not yet support input_file type' + ); + }); + + it('should throw error for unsupported audio input', async () => { + const request = { + input: [ + { type: 'input_audio', audio: 'audiodata' }, + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).rejects.toThrow( + 'does not yet support input_audio type' + ); + }); + + it('should handle function_call_result by converting to text', async () => { + const request = { + input: [ + { + type: 'function_call_result', + callId: 'call_123', + name: 'get_weather', + result: 'sunny' + }, + ], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + const response = await model.getResponse(request); + expect(response.output).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'message', + }), + ]), + ); + }); + + it('should handle system instructions', async () => { + const request = { + systemInstructions: 'You are a helpful assistant.', + input: 'Hello', + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).resolves.toMatchObject({ + output: expect.any(Array), + }); + }); + }); + + describe('Response Format', () => { + it('should return ModelResponse with correct structure', async () => { + const model = provider.getModel(); + + const request = { + input: 'Test', + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + const response = await model.getResponse(request); + + expect(response.usage).toMatchObject({ + inputTokens: expect.any(Number), + outputTokens: expect.any(Number), + totalTokens: expect.any(Number), + }); + + expect(response.output).toEqual( + expect.arrayContaining([expect.objectContaining({ type: 'message' })]), + ); + + expect(response.responseId).toBe('mock-thread'); + }, 30000); // 30 second timeout for backend connection + }); + + describe('Streaming', () => { + it('should return async iterable from getStreamedResponse', () => { + const model = provider.getModel(); + const request = { + input: 'Test', + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + const stream = model.getStreamedResponse(request); + + // Should be async iterable + expect(stream[Symbol.asyncIterator]).toBeDefined(); + }); + + it('should emit proper stream event types', async () => { + const model = provider.getModel(); + const request = { + input: 'Test', + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + const expectedEventTypes = [ + 'response_started', + 'output_text_delta', + 'output_text_done', + 'response_done', + ]; + + const stream = model.getStreamedResponse(request); + const events = []; + + for await (const event of stream) { + events.push(event.type); + } + + expect(events).toEqual(expectedEventTypes); + }, 30000); // 30 second timeout for backend connection + }); + + describe('Conversation State', () => { + it('should support conversationId for multi-turn conversations', async () => { + const model = provider.getModel(); + + const request1 = { + input: 'First message', + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + const response1 = await model.getResponse(request1); + expect(response1.responseId).toBe('mock-thread'); + + const request2 = { + input: 'Second message', + conversationId: response1.responseId, + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + const response2 = await model.getResponse(request2); + expect(response2.responseId).toBe(response1.responseId); + }, 60000); // 60 second timeout for multi-turn conversation + }); + + describe('Error Handling', () => { + it('should handle empty input gracefully', async () => { + const model = provider.getModel(); + const request = { + input: '', + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).resolves.toMatchObject({ + output: expect.any(Array), + }); + }); + + it('should handle array input items', async () => { + const model = provider.getModel(); + const request = { + input: [], + modelSettings: {}, + tools: [], + outputType: { type: 'text' }, + handoffs: [], + tracing: { enabled: false }, + }; + + await expect(model.getResponse(request)).resolves.toMatchObject({ + output: expect.any(Array), + }); + }); + }); +}); diff --git a/sdk/native/tests/common/mod.rs b/sdk/native/tests/common/mod.rs new file mode 100644 index 000000000..5a8fc4120 --- /dev/null +++ b/sdk/native/tests/common/mod.rs @@ -0,0 +1,30 @@ +// Common test utilities shared across test files +use serde_json::json; + +pub fn sample_output_schema() -> serde_json::Value { + json!({ + "type": "object", + "properties": { + "result": {"type": "string"}, + "confidence": {"type": "number"} + }, + "required": ["result"] + }) +} + +pub fn sample_tool_parameters() -> serde_json::Value { + json!({ + "type": "object", + "properties": { + "input": {"type": "string"}, + "options": { + "type": "object", + "properties": { + "verbose": {"type": "boolean"} + } + } + }, + "required": ["input"] + }) +} + diff --git a/sdk/native/tests/config_builder.rs b/sdk/native/tests/config_builder.rs new file mode 100644 index 000000000..a2fc28a22 --- /dev/null +++ b/sdk/native/tests/config_builder.rs @@ -0,0 +1,98 @@ +use codex_common::SandboxModeCliArg; +use codex_core::protocol::AskForApproval; +use codex_native::*; +use codex_protocol::config_types::SandboxMode; +use pretty_assertions::assert_eq; +use std::path::PathBuf; + +#[test] +fn test_build_config_overrides_full_auto() { + let options = InternalRunRequest { + prompt: "test".to_string(), + thread_id: None, + images: vec![], + model: Some("gpt-4".to_string()), + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: false, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: true, + review_request: None, + }; + + let overrides = build_config_overrides(&options, None); + assert_eq!(overrides.sandbox_mode, Some(SandboxMode::WorkspaceWrite)); + assert_eq!(overrides.model, Some("gpt-4".to_string())); + assert_eq!(overrides.approval_policy, Some(AskForApproval::Never)); +} + +#[test] +fn test_build_config_overrides_with_sandbox_mode() { + let options = InternalRunRequest { + prompt: "test".to_string(), + thread_id: None, + images: vec![], + model: None, + sandbox_mode: Some(SandboxModeCliArg::ReadOnly), + working_directory: None, + skip_git_repo_check: false, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: false, + review_request: None, + }; + + let overrides = build_config_overrides(&options, None); + assert_eq!(overrides.sandbox_mode, Some(SandboxMode::ReadOnly)); +} + +#[test] +fn test_build_config_overrides_with_working_directory() { + let working_dir = std::env::current_dir().unwrap(); + let options = InternalRunRequest { + prompt: "test".to_string(), + thread_id: None, + images: vec![], + model: None, + sandbox_mode: None, + working_directory: Some(working_dir.clone()), + skip_git_repo_check: false, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: false, + review_request: None, + }; + + let overrides = build_config_overrides(&options, None); + assert_eq!(overrides.cwd, Some(working_dir)); +} + +#[test] +fn test_build_config_overrides_with_sandbox_path() { + let sandbox_path = PathBuf::from("/path/to/sandbox"); + let options = InternalRunRequest { + prompt: "test".to_string(), + thread_id: None, + images: vec![], + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: false, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: false, + review_request: None, + }; + + let overrides = build_config_overrides(&options, Some(sandbox_path.clone())); + assert_eq!(overrides.codex_linux_sandbox_exe, Some(sandbox_path)); +} diff --git a/sdk/native/tests/env_helpers.rs b/sdk/native/tests/env_helpers.rs new file mode 100644 index 000000000..acecbcc48 --- /dev/null +++ b/sdk/native/tests/env_helpers.rs @@ -0,0 +1,82 @@ +use codex_native::*; +use pretty_assertions::assert_eq; + +#[test] +fn test_env_overrides_sets_and_restores_vars() { + let test_key = "CODEX_TEST_VAR_UNIQUE_123"; + unsafe { std::env::remove_var(test_key) }; + + { + let _guard = EnvOverrides::apply(vec![(test_key, Some("test_value".to_string()), true)]); + assert_eq!(std::env::var(test_key).ok(), Some("test_value".to_string())); + } + + assert_eq!(std::env::var(test_key).ok(), None); +} + +#[test] +fn test_env_overrides_restores_previous_value() { + let test_key = "CODEX_TEST_VAR_PREV_456"; + unsafe { std::env::set_var(test_key, "original") }; + + { + let _guard = EnvOverrides::apply(vec![(test_key, Some("new_value".to_string()), true)]); + assert_eq!(std::env::var(test_key).ok(), Some("new_value".to_string())); + } + + assert_eq!(std::env::var(test_key).ok(), Some("original".to_string())); + unsafe { std::env::remove_var(test_key) }; +} + +#[test] +fn test_env_overrides_removes_when_none_and_force() { + let test_key = "CODEX_TEST_VAR_REMOVE_789"; + unsafe { std::env::set_var(test_key, "to_be_removed") }; + + { + let _guard = EnvOverrides::apply(vec![(test_key, None, true)]); + assert_eq!(std::env::var(test_key).ok(), None); + } + + assert_eq!( + std::env::var(test_key).ok(), + Some("to_be_removed".to_string()) + ); + unsafe { std::env::remove_var(test_key) }; +} + +#[test] +fn test_env_overrides_multiple_vars() { + let key1 = "CODEX_TEST_MULTI_1"; + let key2 = "CODEX_TEST_MULTI_2"; + unsafe { + std::env::remove_var(key1); + std::env::remove_var(key2); + } + + { + let _guard = EnvOverrides::apply(vec![ + (key1, Some("value1".to_string()), true), + (key2, Some("value2".to_string()), true), + ]); + assert_eq!(std::env::var(key1).ok(), Some("value1".to_string())); + assert_eq!(std::env::var(key2).ok(), Some("value2".to_string())); + } + + assert_eq!(std::env::var(key1).ok(), None); + assert_eq!(std::env::var(key2).ok(), None); +} + +#[test] +fn test_env_overrides_skip_when_not_forced_and_none() { + let test_key = "CODEX_TEST_VAR_SKIP"; + unsafe { std::env::set_var(test_key, "original") }; + + { + let _guard = EnvOverrides::apply(vec![(test_key, None, false)]); + assert_eq!(std::env::var(test_key).ok(), Some("original".to_string())); + } + + assert_eq!(std::env::var(test_key).ok(), Some("original".to_string())); + unsafe { std::env::remove_var(test_key) }; +} diff --git a/sdk/native/tests/event_conversion.rs b/sdk/native/tests/event_conversion.rs new file mode 100644 index 000000000..705747e72 --- /dev/null +++ b/sdk/native/tests/event_conversion.rs @@ -0,0 +1,46 @@ +use codex_exec::exec_events::{ThreadEvent, ThreadStartedEvent}; +use codex_native::*; +use pretty_assertions::assert_eq; + +#[test] +fn test_event_to_json_thread_started() { + let event = ThreadEvent::ThreadStarted(ThreadStartedEvent { + thread_id: "thread-123".to_string(), + }); + + let json = event_to_json(&event).unwrap(); + assert!(json.is_object()); + assert!(json.get("ThreadStarted").is_some()); + + let thread_started = json.get("ThreadStarted").unwrap(); + assert_eq!( + thread_started.get("thread_id").unwrap().as_str().unwrap(), + "thread-123" + ); +} + +#[test] +fn test_event_to_json_turn_started() { + let event = ThreadEvent::TurnStarted(codex_exec::exec_events::TurnStartedEvent {}); + + let json = event_to_json(&event).unwrap(); + assert!(json.is_object()); + assert!(json.get("TurnStarted").is_some()); +} + +#[test] +fn test_event_to_json_error() { + let event = ThreadEvent::Error(codex_exec::exec_events::ThreadErrorEvent { + message: "Something went wrong".to_string(), + }); + + let json = event_to_json(&event).unwrap(); + assert!(json.is_object()); + assert!(json.get("Error").is_some()); + + let error = json.get("Error").unwrap(); + assert_eq!( + error.get("message").unwrap().as_str().unwrap(), + "Something went wrong" + ); +} diff --git a/sdk/native/tests/output-schema.test.mjs b/sdk/native/tests/output-schema.test.mjs new file mode 100644 index 000000000..7edd3483a --- /dev/null +++ b/sdk/native/tests/output-schema.test.mjs @@ -0,0 +1,238 @@ +import { describe, expect, it, beforeAll, jest } from "@jest/globals"; +import { fileURLToPath } from "node:url"; +import { promises as fs } from "node:fs"; + +import { + assistantMessage, + responseCompleted, + responseStarted, + sse, + startResponsesTestProxy, +} from "./responsesProxy.mjs"; + +function resolveNativeBindingPath() { + const { platform, arch } = process; + if (platform === "darwin") { + const suffix = arch === "arm64" ? "arm64" : "x64"; + return fileURLToPath(new URL(`../codex_native.darwin-${suffix}.node`, import.meta.url)); + } + if (platform === "win32") { + const suffix = arch === "arm64" ? "arm64" : "x64"; + return fileURLToPath(new URL(`../codex_native.win32-${suffix}-msvc.node`, import.meta.url)); + } + if (platform === "linux") { + const suffix = process.env.MUSL ? "musl" : "gnu"; + return fileURLToPath(new URL(`../codex_native.linux-${arch}-${suffix}.node`, import.meta.url)); + } + throw new Error(`Unsupported platform for tests: ${platform} ${arch}`); +} + +process.env.CODEX_NATIVE_BINDING = resolveNativeBindingPath(); + +let Codex; +beforeAll(async () => { + ({ Codex } = await import("../dist/index.mjs")); +}); + +function createClient(baseUrl) { + return new Codex({ baseUrl, apiKey: "test" }); +} + +describe("Output schema file optimization", () => { + it("does not create temp schema file for native runs", async () => { + const schema = { + type: "object", + properties: { + answer: { type: "string" }, + }, + required: ["answer"], + }; + + const { url, close, requests } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse(responseStarted(), assistantMessage('{"answer":"42"}'), responseCompleted()), + ], + }); + + const mkdtempSpy = jest.spyOn(fs, "mkdtemp"); + try { + const client = createClient(url); + const thread = client.startThread(); + const streamed = await thread.runStreamed("structured", { outputSchema: schema }); + for await (const _ of streamed.events) { + // drain events + } + + const payload = requests[0].json; + expect(payload.text?.format?.name).toEqual("codex_output_schema"); + expect(payload.text?.format?.type).toEqual("json_schema"); + expect(payload.text?.format?.strict).toEqual(true); + // Schema should include additionalProperties: false + expect(payload.text?.format?.schema?.additionalProperties).toBe(false); + // No temp directory should be created for native runs + expect(mkdtempSpy).not.toHaveBeenCalled(); + } finally { + mkdtempSpy.mockRestore(); + await close(); + } + }); + + it("normalizes schema with additionalProperties", async () => { + const schemaWithoutAdditionalProperties = { + type: "object", + properties: { + name: { type: "string" }, + }, + }; + + const { url, close, requests } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse(responseStarted(), assistantMessage('{"name":"test"}'), responseCompleted()), + ], + }); + + try { + const client = createClient(url); + const thread = client.startThread(); + const streamed = await thread.runStreamed("test", { + outputSchema: schemaWithoutAdditionalProperties, + }); + + for await (const _ of streamed.events) { + // drain + } + + const payload = requests[0].json; + // Should add additionalProperties: false + expect(payload.text?.format?.schema?.additionalProperties).toBe(false); + } finally { + await close(); + } + }); + + it("preserves explicit additionalProperties setting", async () => { + const schemaWithExplicitTrue = { + type: "object", + properties: { + data: { type: "string" }, + }, + additionalProperties: true, + }; + + const { url, close, requests } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse(responseStarted(), assistantMessage('{"data":"ok"}'), responseCompleted()), + ], + }); + + try { + const client = createClient(url); + const thread = client.startThread(); + const streamed = await thread.runStreamed("test", { + outputSchema: schemaWithExplicitTrue, + }); + + for await (const _ of streamed.events) { + // drain + } + + const payload = requests[0].json; + // Should preserve the explicit true + expect(payload.text?.format?.schema?.additionalProperties).toBe(true); + } finally { + await close(); + } + }); + + it("handles undefined schema gracefully", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [sse(responseStarted(), assistantMessage("OK"), responseCompleted())], + }); + + try { + const client = createClient(url); + const thread = client.startThread(); + const streamed = await thread.runStreamed("test", { + outputSchema: undefined, + }); + + const events = []; + for await (const event of streamed.events) { + events.push(event); + } + + expect(events.length).toBeGreaterThan(0); + } finally { + await close(); + } + }); +}); + +describe("Schema validation", () => { + it("rejects non-object schemas", async () => { + const client = new Codex({ baseUrl: "http://invalid", apiKey: "test" }); + const thread = client.startThread(); + + const result = await thread.runStreamed("test", { outputSchema: "not an object" }); + // Error should be thrown when iterating the events generator + await expect(async () => { + for await (const _ of result.events) { + // will throw before yielding + } + }).rejects.toThrow("outputSchema must be a plain JSON object"); + }); + + it("rejects array schemas", async () => { + const client = new Codex({ baseUrl: "http://invalid", apiKey: "test" }); + const thread = client.startThread(); + + const result = await thread.runStreamed("test", { outputSchema: [] }); + // Error should be thrown when iterating the events generator + await expect(async () => { + for await (const _ of result.events) { + // will throw before yielding + } + }).rejects.toThrow("outputSchema must be a plain JSON object"); + }); + + it("accepts valid JSON object schemas", async () => { + const validSchema = { + type: "object", + properties: { + result: { type: "string" }, + count: { type: "number" }, + }, + }; + + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted(), + assistantMessage('{"result":"test","count":5}'), + responseCompleted(), + ), + ], + }); + + try { + const client = createClient(url); + const thread = client.startThread(); + const streamed = await thread.runStreamed("test", { outputSchema: validSchema }); + + const events = []; + for await (const event of streamed.events) { + events.push(event); + } + + expect(events.length).toBeGreaterThan(0); + } finally { + await close(); + } + }); +}); + diff --git a/sdk/native/tests/raw-events.test.mjs b/sdk/native/tests/raw-events.test.mjs new file mode 100644 index 000000000..bd0aab044 --- /dev/null +++ b/sdk/native/tests/raw-events.test.mjs @@ -0,0 +1,154 @@ +import { describe, expect, it, beforeAll } from "@jest/globals"; +import { fileURLToPath } from "node:url"; + +import { + assistantMessage, + responseCompleted, + responseStarted, + sse, + startResponsesTestProxy, +} from "./responsesProxy.mjs"; + +function resolveNativeBindingPath() { + const { platform, arch } = process; + if (platform === "darwin") { + const suffix = arch === "arm64" ? "arm64" : "x64"; + return fileURLToPath(new URL(`../codex_native.darwin-${suffix}.node`, import.meta.url)); + } + if (platform === "win32") { + const suffix = arch === "arm64" ? "arm64" : "x64"; + return fileURLToPath(new URL(`../codex_native.win32-${suffix}-msvc.node`, import.meta.url)); + } + if (platform === "linux") { + const suffix = process.env.MUSL ? "musl" : "gnu"; + return fileURLToPath(new URL(`../codex_native.linux-${arch}-${suffix}.node`, import.meta.url)); + } + throw new Error(`Unsupported platform for tests: ${platform} ${arch}`); +} + +process.env.CODEX_NATIVE_BINDING = resolveNativeBindingPath(); + +let Codex; +beforeAll(async () => { + ({ Codex } = await import("../dist/index.mjs")); +}); + +function createClient(baseUrl) { + return new Codex({ baseUrl, apiKey: "test" }); +} + +describe("Raw event forwarding", () => { + it("forwards unhandled protocol events as raw_event", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted(), + // Include a raw custom event that won't be recognized + 'event: custom_event\ndata: {"type":"custom_event","custom_data":"test"}\n\n', + assistantMessage("Hi!"), + responseCompleted(), + ), + ], + }); + + try { + const client = createClient(url); + const thread = client.startThread(); + const result = await thread.runStreamed("Hello!"); + + const events = []; + for await (const event of result.events) { + events.push(event); + } + + // Should include raw_event for unhandled events + const rawEvents = events.filter((e) => e.type === "raw_event"); + expect(rawEvents.length).toBeGreaterThanOrEqual(0); + + // Standard events should still work + const threadStarted = events.find((e) => e.type === "thread.started"); + expect(threadStarted).toBeDefined(); + + const itemCompleted = events.find((e) => e.type === "item.completed"); + expect(itemCompleted).toBeDefined(); + } finally { + await close(); + } + }); + + it("includes raw event data in the payload", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted(), + assistantMessage("Done"), + responseCompleted(), + ), + ], + }); + + try { + const client = createClient(url); + const thread = client.startThread(); + const result = await thread.runStreamed("Test"); + + const events = []; + for await (const event of result.events) { + events.push(event); + } + + // Should have raw events + const rawEvents = events.filter((e) => e.type === "raw_event"); + expect(rawEvents.length).toBeGreaterThan(0); + + // Raw events should have raw field + for (const rawEvent of rawEvents) { + expect(rawEvent.raw).toBeDefined(); + } + } finally { + await close(); + } + }); +}); + +describe("Event forwarding completeness", () => { + it("emits all recognized event types", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted(), + assistantMessage("Response text"), + responseCompleted(), + ), + ], + }); + + try { + const client = createClient(url); + const thread = client.startThread(); + const result = await thread.runStreamed("Test all events"); + + const events = []; + for await (const event of result.events) { + events.push(event); + } + + const eventTypes = new Set(events.map((e) => e.type)); + + // Should have at least these standard events + expect(eventTypes.has("thread.started")).toBe(true); + expect(eventTypes.has("turn.started")).toBe(true); + expect(eventTypes.has("turn.completed")).toBe(true); + + // May have additional raw events + const hasRawEvents = eventTypes.has("raw_event"); + expect(typeof hasRawEvents).toBe("boolean"); + } finally { + await close(); + } + }); +}); + diff --git a/sdk/native/tests/request_conversion.rs b/sdk/native/tests/request_conversion.rs new file mode 100644 index 000000000..c11eb9cd2 --- /dev/null +++ b/sdk/native/tests/request_conversion.rs @@ -0,0 +1,227 @@ +use codex_common::SandboxModeCliArg; +use codex_native::*; +use pretty_assertions::assert_eq; +use serde_json::json; +use std::path::PathBuf; + +#[test] +fn test_run_request_into_internal_default_values() { + let req = RunRequest { + prompt: "test prompt".to_string(), + thread_id: None, + images: None, + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: None, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: None, + review_hint: None, + }; + + let internal = req.into_internal().unwrap(); + assert_eq!(internal.prompt, "test prompt"); + assert!(internal.thread_id.is_none()); + assert_eq!(internal.images.len(), 0); + assert!(!internal.skip_git_repo_check); + assert!(!internal.full_auto); + assert!(internal.review_request.is_none()); +} + +#[test] +fn test_run_request_with_images() { + let req = RunRequest { + prompt: "test".to_string(), + thread_id: None, + images: Some(vec![ + "/path/to/image1.png".to_string(), + "/path/to/image2.jpg".to_string(), + ]), + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: None, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: None, + review_hint: None, + }; + + let internal = req.into_internal().unwrap(); + assert_eq!(internal.images.len(), 2); + assert_eq!(internal.images[0], PathBuf::from("/path/to/image1.png")); + assert_eq!(internal.images[1], PathBuf::from("/path/to/image2.jpg")); +} + +#[test] +fn test_run_request_sandbox_mode_conversions() { + let modes = vec![ + ("read-only", SandboxModeCliArg::ReadOnly), + ("workspace-write", SandboxModeCliArg::WorkspaceWrite), + ("danger-full-access", SandboxModeCliArg::DangerFullAccess), + ]; + + for (mode_str, expected) in modes { + let req = RunRequest { + prompt: "test".to_string(), + thread_id: None, + images: None, + model: None, + sandbox_mode: Some(mode_str.to_string()), + working_directory: None, + skip_git_repo_check: None, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: None, + review_hint: None, + }; + + let internal = req.into_internal().unwrap(); + assert_eq!(internal.sandbox_mode, Some(expected)); + } +} + +#[test] +fn test_run_request_invalid_sandbox_mode() { + let req = RunRequest { + prompt: "test".to_string(), + thread_id: None, + images: None, + model: None, + sandbox_mode: Some("invalid-mode".to_string()), + working_directory: None, + skip_git_repo_check: None, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: None, + review_hint: None, + }; + + let result = req.into_internal(); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.reason.contains("Unsupported sandbox mode")); +} + +#[test] +fn test_run_request_review_mode_with_empty_prompt() { + let req = RunRequest { + prompt: " ".to_string(), + thread_id: None, + images: None, + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: None, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: Some(true), + review_hint: None, + }; + + let result = req.into_internal(); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err + .reason + .contains("Review mode requires a non-empty prompt") + ); +} + +#[test] +fn test_run_request_review_mode_with_prompt() { + let req = RunRequest { + prompt: "review this code".to_string(), + thread_id: None, + images: None, + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: None, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: Some(true), + review_hint: Some("security review".to_string()), + }; + + let internal = req.into_internal().unwrap(); + assert!(internal.review_request.is_some()); + let review = internal.review_request.unwrap(); + assert_eq!(review.prompt, "review this code"); + assert_eq!(review.user_facing_hint, "security review"); +} + +#[test] +fn test_run_request_review_mode_default_hint() { + let req = RunRequest { + prompt: "review this".to_string(), + thread_id: None, + images: None, + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: None, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: Some(true), + review_hint: None, + }; + + let internal = req.into_internal().unwrap(); + assert!(internal.review_request.is_some()); + let review = internal.review_request.unwrap(); + assert_eq!(review.user_facing_hint, "code review"); +} + +#[test] +fn test_run_request_with_output_schema() { + let schema = json!({ + "type": "object", + "properties": { + "result": {"type": "string"} + } + }); + + let req = RunRequest { + prompt: "test".to_string(), + thread_id: None, + images: None, + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: None, + output_schema: Some(schema.clone()), + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: None, + review_hint: None, + }; + + let internal = req.into_internal().unwrap(); + assert_eq!(internal.output_schema, Some(schema)); +} diff --git a/sdk/native/tests/responsesProxy.mjs b/sdk/native/tests/responsesProxy.mjs index 83e4b2966..a25d9b9ea 100644 --- a/sdk/native/tests/responsesProxy.mjs +++ b/sdk/native/tests/responsesProxy.mjs @@ -136,10 +136,10 @@ export function responseFailed(errorMessage) { }; } -export function responseCompleted(responseId = DEFAULT_RESPONSE_ID, usage = DEFAULT_COMPLETED_USAGE) { +export function responseCompleted(responseId = DEFAULT_RESPONSE_ID, usage = DEFAULT_COMPLETED_USAGE, finalText) { const inputDetails = usage.input_tokens_details ? { ...usage.input_tokens_details } : null; const outputDetails = usage.output_tokens_details ? { ...usage.output_tokens_details } : null; - return { + const response = { type: "response.completed", response: { id: responseId, @@ -152,4 +152,20 @@ export function responseCompleted(responseId = DEFAULT_RESPONSE_ID, usage = DEFA }, }, }; + if (typeof finalText === "string") { + response.response.output = [ + { + id: DEFAULT_MESSAGE_ID, + role: "assistant", + content: [ + { + type: "output_text", + text: finalText, + }, + ], + }, + ]; + response.response.output_text = finalText; + } + return response; } diff --git a/sdk/native/tests/review_collector.rs b/sdk/native/tests/review_collector.rs new file mode 100644 index 000000000..1a8eb0928 --- /dev/null +++ b/sdk/native/tests/review_collector.rs @@ -0,0 +1,197 @@ +use codex_core::protocol::{Event, EventMsg, TokenUsage}; +use codex_exec::exec_events::{ThreadEvent, ThreadItemDetails}; +use codex_native::*; +use pretty_assertions::assert_eq; + +#[test] +fn test_review_event_collector_next_item_id() { + let mut collector = ReviewEventCollector::new(); + assert_eq!(collector.next_item_id(), "item_0"); + assert_eq!(collector.next_item_id(), "item_1"); + assert_eq!(collector.next_item_id(), "item_2"); +} + +#[test] +fn test_review_event_collector_next_item_id_sequence() { + let mut collector = ReviewEventCollector::new(); + assert_eq!(collector.next_item_id, 0); + let _id = collector.next_item_id(); + assert_eq!(collector.next_item_id, 1); +} + +#[test] +fn test_review_event_collector_task_started() { + let mut collector = ReviewEventCollector::new(); + let event = Event { + id: "1".to_string(), + msg: EventMsg::TaskStarted(codex_core::protocol::TaskStartedEvent { task_id: 1 }), + }; + + let events = collector.handle(&event); + assert_eq!(events.len(), 1); + assert!(matches!(events[0], ThreadEvent::TurnStarted(_))); +} + +#[test] +fn test_review_event_collector_agent_reasoning() { + let mut collector = ReviewEventCollector::new(); + let event = Event { + id: "1".to_string(), + msg: EventMsg::AgentReasoning(codex_core::protocol::AgentReasoningEvent { + text: "thinking...".to_string(), + }), + }; + + let events = collector.handle(&event); + assert_eq!(events.len(), 1); + + if let ThreadEvent::ItemCompleted(item_event) = &events[0] { + assert_eq!(item_event.item.id, "item_0"); + if let ThreadItemDetails::Reasoning(reasoning) = &item_event.item.details { + assert_eq!(reasoning.text, "thinking..."); + } else { + panic!("Expected Reasoning item"); + } + } else { + panic!("Expected ItemCompleted event"); + } +} + +#[test] +fn test_review_event_collector_agent_message() { + let mut collector = ReviewEventCollector::new(); + let event = Event { + id: "1".to_string(), + msg: EventMsg::AgentMessage(codex_core::protocol::AgentMessageEvent { + message: "Hello!".to_string(), + }), + }; + + let events = collector.handle(&event); + assert_eq!(events.len(), 1); + + if let ThreadEvent::ItemCompleted(item_event) = &events[0] { + if let ThreadItemDetails::AgentMessage(msg) = &item_event.item.details { + assert_eq!(msg.text, "Hello!"); + } else { + panic!("Expected AgentMessage item"); + } + } else { + panic!("Expected ItemCompleted event"); + } +} + +#[test] +fn test_review_event_collector_warning() { + let mut collector = ReviewEventCollector::new(); + let event = Event { + id: "1".to_string(), + msg: EventMsg::Warning(codex_core::protocol::WarningEvent { + message: "Warning message".to_string(), + }), + }; + + let events = collector.handle(&event); + assert_eq!(events.len(), 1); + + if let ThreadEvent::ItemCompleted(item_event) = &events[0] { + if let ThreadItemDetails::Error(error) = &item_event.item.details { + assert_eq!(error.message, "Warning message"); + } else { + panic!("Expected Error item"); + } + } else { + panic!("Expected ItemCompleted event"); + } +} + +#[test] +fn test_review_event_collector_error() { + let mut collector = ReviewEventCollector::new(); + let event = Event { + id: "1".to_string(), + msg: EventMsg::Error(codex_core::protocol::ErrorEvent { + message: "Error occurred".to_string(), + }), + }; + + let events = collector.handle(&event); + assert_eq!(events.len(), 1); + + if let ThreadEvent::Error(error_event) = &events[0] { + assert_eq!(error_event.message, "Error occurred"); + } else { + panic!("Expected Error event"); + } + + assert!(collector.last_error.is_some()); + assert_eq!( + collector.last_error.as_ref().unwrap().message, + "Error occurred" + ); +} + +#[test] +fn test_review_event_collector_token_count() { + let mut collector = ReviewEventCollector::new(); + let event = Event { + id: "1".to_string(), + msg: EventMsg::TokenCount(codex_core::protocol::TokenCountEvent { + info: Some(codex_core::protocol::TokenCountInfoEvent { + total_token_usage: TokenUsage { + input_tokens: 100, + cached_input_tokens: 50, + output_tokens: 25, + reasoning_output_tokens: 0, + total_tokens: 175, + }, + this_turn_token_usage: TokenUsage { + input_tokens: 0, + cached_input_tokens: 0, + output_tokens: 0, + reasoning_output_tokens: 0, + total_tokens: 0, + }, + estimated_cost: None, + }), + }), + }; + + let events = collector.handle(&event); + assert_eq!(events.len(), 0); + + assert!(collector.last_usage.is_some()); + let usage = collector.last_usage.as_ref().unwrap(); + assert_eq!(usage.input_tokens, 100); + assert_eq!(usage.cached_input_tokens, 50); + assert_eq!(usage.output_tokens, 25); +} + +#[test] +fn test_review_event_collector_parse_review_output_valid_json() { + let collector = ReviewEventCollector::new(); + let json_text = r#"{"overall_explanation":"Good code","findings":[]}"#; + + let output = collector.parse_review_output(json_text); + assert_eq!(output.overall_explanation, "Good code"); + assert_eq!(output.findings.len(), 0); +} + +#[test] +fn test_review_event_collector_parse_review_output_embedded_json() { + let collector = ReviewEventCollector::new(); + let text = r#"Some text {"overall_explanation":"Found issues","findings":[]} after"#; + + let output = collector.parse_review_output(text); + assert_eq!(output.overall_explanation, "Found issues"); +} + +#[test] +fn test_review_event_collector_parse_review_output_invalid_json() { + let collector = ReviewEventCollector::new(); + let text = "This is not JSON at all"; + + let output = collector.parse_review_output(text); + assert_eq!(output.overall_explanation, "This is not JSON at all"); + assert_eq!(output.findings.len(), 0); +} diff --git a/sdk/native/tests/run.test.mjs b/sdk/native/tests/run.test.mjs index 17f932775..91ea5d88e 100644 --- a/sdk/native/tests/run.test.mjs +++ b/sdk/native/tests/run.test.mjs @@ -343,7 +343,7 @@ describe("Codex native bridge", () => { } }); - it("throws if working directory is not git and skipGitRepoCheck is not provided", async () => { +it("throws if working directory is not git and skipGitRepoCheck is not provided", async () => { const { url, close } = await startResponsesTestProxy({ statusCode: 200, responseBodies: [ @@ -367,7 +367,7 @@ describe("Codex native bridge", () => { fs.rmSync(workingDirectory, { recursive: true, force: true }); await close(); } - }); +}); it("sets the codex sdk originator header", async () => { const { url, close, requests } = await startResponsesTestProxy({ @@ -387,7 +387,6 @@ describe("Codex native bridge", () => { await close(); } }); - it("throws ThreadRunError on turn failures", async () => { const { url, close } = await startResponsesTestProxy({ statusCode: 200, diff --git a/sdk/native/tests/runStreamed.test.mjs b/sdk/native/tests/runStreamed.test.mjs index 7d9a6db65..111a63547 100644 --- a/sdk/native/tests/runStreamed.test.mjs +++ b/sdk/native/tests/runStreamed.test.mjs @@ -1,5 +1,6 @@ -import { describe, expect, it, beforeAll } from "@jest/globals"; +import { describe, expect, it, beforeAll, jest } from "@jest/globals"; import { fileURLToPath } from "node:url"; +import { promises as fs } from "node:fs"; import { assistantMessage, @@ -54,7 +55,10 @@ describe("Codex runStreamed with native binding", () => { events.push(event); } - expect(events).toEqual([ + // Filter out raw_event types for this test + const standardEvents = events.filter((e) => e.type !== "raw_event"); + + expect(standardEvents).toEqual([ { type: "thread.started", thread_id: expect.any(String), @@ -183,6 +187,7 @@ describe("Codex runStreamed with native binding", () => { additionalProperties: false, }; + const mkdtempSpy = jest.spyOn(fs, "mkdtemp"); try { const client = createClient(url); const thread = client.startThread(); @@ -198,7 +203,9 @@ describe("Codex runStreamed with native binding", () => { strict: true, schema, }); + expect(mkdtempSpy).not.toHaveBeenCalled(); } finally { + mkdtempSpy.mockRestore(); await close(); } }); diff --git a/sdk/native/tests/run_request.rs b/sdk/native/tests/run_request.rs new file mode 100644 index 000000000..45b25a81f --- /dev/null +++ b/sdk/native/tests/run_request.rs @@ -0,0 +1,196 @@ +use codex_native::*; +use pretty_assertions::assert_eq; +use serde_json::json; + +#[test] +fn test_run_request_default_values() { + let req = RunRequest { + prompt: "test prompt".to_string(), + thread_id: None, + images: None, + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: None, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: None, + review_hint: None, + }; + + // Verify struct construction works + assert_eq!(req.prompt, "test prompt"); + assert!(req.thread_id.is_none()); + assert!(req.images.is_none()); + assert!(req.full_auto.is_none()); +} + +#[test] +fn test_run_request_with_all_fields() { + let schema = json!({ + "type": "object", + "properties": { + "name": {"type": "string"} + } + }); + + let req = RunRequest { + prompt: "complex request".to_string(), + thread_id: Some("thread-123".to_string()), + images: Some(vec!["/path/to/image.png".to_string()]), + model: Some("gpt-4".to_string()), + sandbox_mode: Some("workspace-write".to_string()), + working_directory: Some("/workspace".to_string()), + skip_git_repo_check: Some(true), + output_schema: Some(schema.clone()), + base_url: Some("https://api.example.com".to_string()), + api_key: Some("sk-test-key".to_string()), + linux_sandbox_path: Some("/path/to/sandbox".to_string()), + full_auto: Some(true), + review_mode: Some(false), + review_hint: None, + }; + + assert_eq!(req.prompt, "complex request"); + assert_eq!(req.thread_id, Some("thread-123".to_string())); + assert_eq!(req.images, Some(vec!["/path/to/image.png".to_string()])); + assert_eq!(req.model, Some("gpt-4".to_string())); + assert_eq!(req.sandbox_mode, Some("workspace-write".to_string())); + assert_eq!(req.full_auto, Some(true)); + assert_eq!(req.output_schema, Some(schema)); +} + +#[test] +fn test_run_request_review_mode() { + let req = RunRequest { + prompt: "review this code".to_string(), + thread_id: None, + images: None, + model: None, + sandbox_mode: None, + working_directory: None, + skip_git_repo_check: None, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: Some(true), + review_hint: Some("security review".to_string()), + }; + + assert_eq!(req.review_mode, Some(true)); + assert_eq!(req.review_hint, Some("security review".to_string())); +} + +#[test] +fn test_native_tool_info_construction() { + let tool_info = NativeToolInfo { + name: "test_tool".to_string(), + description: Some("A test tool".to_string()), + parameters: Some(json!({ + "type": "object", + "properties": { + "input": {"type": "string"} + } + })), + strict: Some(true), + supports_parallel: Some(false), + }; + + assert_eq!(tool_info.name, "test_tool"); + assert_eq!(tool_info.description, Some("A test tool".to_string())); + assert_eq!(tool_info.strict, Some(true)); + assert_eq!(tool_info.supports_parallel, Some(false)); +} + +#[test] +fn test_native_tool_response_construction() { + let response = NativeToolResponse { + output: Some("tool output".to_string()), + success: Some(true), + error: None, + }; + + assert_eq!(response.output, Some("tool output".to_string())); + assert_eq!(response.success, Some(true)); + assert!(response.error.is_none()); +} + +#[test] +fn test_native_tool_response_with_error() { + let response = NativeToolResponse { + output: None, + success: Some(false), + error: Some("Tool failed".to_string()), + }; + + assert!(response.output.is_none()); + assert_eq!(response.success, Some(false)); + assert_eq!(response.error, Some("Tool failed".to_string())); +} + +#[test] +fn test_js_tool_invocation_function_payload() { + let invocation = JsToolInvocation { + call_id: "call-123".to_string(), + tool_name: "my_tool".to_string(), + arguments: Some(r#"{"key": "value"}"#.to_string()), + input: None, + }; + + assert_eq!(invocation.call_id, "call-123"); + assert_eq!(invocation.tool_name, "my_tool"); + assert!(invocation.arguments.is_some()); + assert!(invocation.input.is_none()); +} + +#[test] +fn test_js_tool_invocation_custom_payload() { + let invocation = JsToolInvocation { + call_id: "call-456".to_string(), + tool_name: "custom_tool".to_string(), + arguments: None, + input: Some("raw input".to_string()), + }; + + assert_eq!(invocation.call_id, "call-456"); + assert_eq!(invocation.tool_name, "custom_tool"); + assert!(invocation.arguments.is_none()); + assert_eq!(invocation.input, Some("raw input".to_string())); +} + +#[test] +fn test_clear_registered_tools() { + let result = clear_registered_tools(); + assert!(result.is_ok()); +} + +#[test] +fn test_sandbox_mode_values() { + let modes = vec!["read-only", "workspace-write", "danger-full-access"]; + + for mode in modes { + let req = RunRequest { + prompt: "test".to_string(), + thread_id: None, + images: None, + model: None, + sandbox_mode: Some(mode.to_string()), + working_directory: None, + skip_git_repo_check: None, + output_schema: None, + base_url: None, + api_key: None, + linux_sandbox_path: None, + full_auto: None, + review_mode: None, + review_hint: None, + }; + + assert_eq!(req.sandbox_mode, Some(mode.to_string())); + } +} diff --git a/sdk/native/tests/schema_helpers.rs b/sdk/native/tests/schema_helpers.rs new file mode 100644 index 000000000..14c34f7c1 --- /dev/null +++ b/sdk/native/tests/schema_helpers.rs @@ -0,0 +1,62 @@ +use codex_native::*; +use pretty_assertions::assert_eq; +use serde_json::json; + +#[test] +fn test_prepare_schema_none() { + let result = prepare_schema(None); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); +} + +#[test] +fn test_prepare_schema_with_value() { + let schema = json!({ + "type": "object", + "properties": { + "name": {"type": "string"} + } + }); + let result = prepare_schema(Some(schema.clone())); + assert!(result.is_ok()); + let temp_file = result.unwrap(); + assert!(temp_file.is_some()); + + let temp_file = temp_file.unwrap(); + assert!(temp_file.path.exists()); + + let content = std::fs::read_to_string(&temp_file.path).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(parsed, schema); +} + +#[test] +fn test_prepare_schema_with_complex_schema() { + let schema = json!({ + "type": "object", + "properties": { + "result": {"type": "string"}, + "confidence": {"type": "number"}, + "tags": { + "type": "array", + "items": {"type": "string"} + }, + "metadata": { + "type": "object", + "properties": { + "timestamp": {"type": "string"}, + "version": {"type": "integer"} + } + } + }, + "required": ["result"] + }); + + let result = prepare_schema(Some(schema.clone())); + assert!(result.is_ok()); + let temp_file = result.unwrap().unwrap(); + + let content = std::fs::read_to_string(&temp_file.path).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(parsed, schema); +} diff --git a/sdk/native/tests/tool-override.test.mjs b/sdk/native/tests/tool-override.test.mjs new file mode 100644 index 000000000..ee8b0ecd7 --- /dev/null +++ b/sdk/native/tests/tool-override.test.mjs @@ -0,0 +1,196 @@ +import { describe, expect, it, beforeAll } from "@jest/globals"; +import { fileURLToPath } from "node:url"; + +import { + assistantMessage, + responseCompleted, + responseStarted, + sse, + startResponsesTestProxy, +} from "./responsesProxy.mjs"; + +function resolveNativeBindingPath() { + const { platform, arch } = process; + if (platform === "darwin") { + const suffix = arch === "arm64" ? "arm64" : "x64"; + return fileURLToPath(new URL(`../codex_native.darwin-${suffix}.node`, import.meta.url)); + } + if (platform === "win32") { + const suffix = arch === "arm64" ? "arm64" : "x64"; + return fileURLToPath(new URL(`../codex_native.win32-${suffix}-msvc.node`, import.meta.url)); + } + if (platform === "linux") { + const suffix = process.env.MUSL ? "musl" : "gnu"; + return fileURLToPath(new URL(`../codex_native.linux-${arch}-${suffix}.node`, import.meta.url)); + } + throw new Error(`Unsupported platform for tests: ${platform} ${arch}`); +} + +process.env.CODEX_NATIVE_BINDING = resolveNativeBindingPath(); + +let Codex; +beforeAll(async () => { + ({ Codex } = await import("../dist/index.mjs")); +}); + +function createClient(baseUrl) { + return new Codex({ baseUrl, apiKey: "test" }); +} + +describe("Tool override capability", () => { + it("allows registering a tool to override built-ins", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [sse(responseStarted(), assistantMessage("OK"), responseCompleted())], + }); + + try { + const client = createClient(url); + + // Register a custom read_file tool + client.registerTool({ + name: "read_file", + description: "Custom file reader", + parameters: { + type: "object", + properties: { + target_file: { type: "string" }, + }, + required: ["target_file"], + }, + handler: async (args) => { + return JSON.stringify({ content: "Custom read implementation" }); + }, + }); + + const thread = client.startThread(); + const result = await thread.runStreamed("Test"); + + const events = []; + for await (const event of result.events) { + events.push(event); + } + + // The tool was registered successfully + const completedEvent = events.find((e) => e.type === "turn.completed"); + expect(completedEvent).toBeDefined(); + } finally { + await close(); + } + }); + + it("allows overriding multiple built-in tools", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [sse(responseStarted(), assistantMessage("OK"), responseCompleted())], + }); + + try { + const client = createClient(url); + + // Register multiple overrides + const tools = ["read_file", "write_file", "grep", "local_shell"]; + + for (const toolName of tools) { + client.registerTool({ + name: toolName, + description: `Custom ${toolName}`, + parameters: { type: "object", properties: {} }, + handler: async () => "custom", + }); + } + + // Should not throw + const thread = client.startThread(); + const result = await thread.runStreamed("Test"); + + const events = []; + for await (const event of result.events) { + events.push(event); + } + + expect(events.length).toBeGreaterThan(0); + } finally { + await close(); + } + }); + + it("clear tools removes all registered tools including overrides", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [sse(responseStarted(), assistantMessage("OK"), responseCompleted())], + }); + + try { + const client = createClient(url); + + // Register an override + client.registerTool({ + name: "read_file", + description: "Custom reader", + parameters: { type: "object", properties: {} }, + handler: async () => "custom", + }); + + // Clear all tools + client.clearTools(); + + // Should still work (built-ins restored) + const thread = client.startThread(); + const result = await thread.runStreamed("Test"); + + const events = []; + for await (const event of result.events) { + events.push(event); + } + + expect(events.length).toBeGreaterThan(0); + } finally { + await close(); + } + }); +}); + +describe("Tool override documentation", () => { + it("supports the documented override pattern", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [sse(responseStarted(), assistantMessage("OK"), responseCompleted())], + }); + + try { + const client = createClient(url); + + // Example from README: override grep with custom logic + client.registerTool({ + name: "grep", + description: "Search for patterns (custom)", + parameters: { + type: "object", + properties: { + pattern: { type: "string" }, + path: { type: "string" }, + }, + required: ["pattern"], + }, + handler: async ({ pattern, path }) => { + // Custom grep implementation + return JSON.stringify({ matches: [`custom result for ${pattern}`] }); + }, + }); + + const thread = client.startThread(); + const result = await thread.runStreamed("Test"); + + const events = []; + for await (const event of result.events) { + events.push(event); + } + + expect(events.length).toBeGreaterThan(0); + } finally { + await close(); + } + }); +}); +