mirror of
https://github.com/openai/codex.git
synced 2026-05-08 13:26:34 +00:00
Compare commits
25 Commits
fix/nonint
...
starr/wind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
077a3970d7 | ||
|
|
5815dd6a4b | ||
|
|
296fa6df0c | ||
|
|
64c684bd57 | ||
|
|
ce5d84e43a | ||
|
|
926b8d77cd | ||
|
|
7cd5127421 | ||
|
|
6a2ce743f1 | ||
|
|
32deb67fc6 | ||
|
|
59d9e96d66 | ||
|
|
097e3ef949 | ||
|
|
f3afa1132d | ||
|
|
a666109389 | ||
|
|
16648c8d1c | ||
|
|
7d2c8dbec4 | ||
|
|
bfe33e5a7a | ||
|
|
8abcc5357d | ||
|
|
27ec488ad5 | ||
|
|
8367ef4522 | ||
|
|
163eac9306 | ||
|
|
4242bba2eb | ||
|
|
0274398901 | ||
|
|
56823ec46b | ||
|
|
0dc1885a5c | ||
|
|
566f2cb612 |
2
.github/actions/prepare-bazel-ci/action.yml
vendored
2
.github/actions/prepare-bazel-ci/action.yml
vendored
@@ -50,7 +50,7 @@ runs:
|
||||
- name: Restore bazel repository cache
|
||||
id: cache_bazel_repository_restore
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ steps.setup_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.cache_bazel_repository_key.outputs.repository-cache-key }}
|
||||
|
||||
21
.github/actions/setup-bazel-ci/action.yml
vendored
21
.github/actions/setup-bazel-ci/action.yml
vendored
@@ -35,6 +35,11 @@ runs:
|
||||
- name: Set up Bazel
|
||||
uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
|
||||
|
||||
- name: Configure Dev Drive (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/setup-dev-drive.ps1
|
||||
|
||||
- name: Configure Bazel repository cache
|
||||
id: configure_bazel_repository_cache
|
||||
shell: pwsh
|
||||
@@ -42,7 +47,12 @@ runs:
|
||||
# Keep the repository cache under HOME on all runners. Windows `D:\a`
|
||||
# cache paths match `.bazelrc`, but `actions/cache/restore` currently
|
||||
# returns HTTP 400 for that path in the Windows clippy job.
|
||||
$repositoryCachePath = Join-Path $HOME '.cache/bazel-repo-cache'
|
||||
$cacheRoot = if ($env:RUNNER_OS -eq 'Windows' -and $env:DEV_DRIVE) {
|
||||
$env:DEV_DRIVE
|
||||
} else {
|
||||
$HOME
|
||||
}
|
||||
$repositoryCachePath = Join-Path $cacheRoot '.cache/bazel-repo-cache'
|
||||
"repository-cache-path=$repositoryCachePath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"BAZEL_REPOSITORY_CACHE=$repositoryCachePath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
@@ -50,11 +60,10 @@ runs:
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Use the shortest available drive to reduce argv/path length issues,
|
||||
# but avoid the drive root because some Windows test launchers mis-handle
|
||||
# MANIFEST paths there.
|
||||
$hasDDrive = Test-Path 'D:\'
|
||||
$bazelOutputUserRoot = if ($hasDDrive) { 'D:\b' } else { 'C:\b' }
|
||||
# Keep Bazel on the fast Windows work drive, but avoid the drive root
|
||||
# because some Windows test launchers mis-handle MANIFEST paths there.
|
||||
$driveRoot = if ($env:DEV_DRIVE) { $env:DEV_DRIVE } elseif (Test-Path 'D:\') { 'D:' } else { 'C:' }
|
||||
$bazelOutputUserRoot = Join-Path $driveRoot 'b'
|
||||
$repoContentsCache = Join-Path $env:RUNNER_TEMP "bazel-repo-contents-cache-$env:GITHUB_RUN_ID-$env:GITHUB_JOB"
|
||||
"BAZEL_OUTPUT_USER_ROOT=$bazelOutputUserRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"BAZEL_REPO_CONTENTS_CACHE=$repoContentsCache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
4
.github/actions/windows-code-sign/action.yml
vendored
4
.github/actions/windows-code-sign/action.yml
vendored
@@ -30,7 +30,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Azure login for Trusted Signing (OIDC)
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
with:
|
||||
client-id: ${{ inputs.client-id }}
|
||||
tenant-id: ${{ inputs.tenant-id }}
|
||||
@@ -54,7 +54,7 @@ runs:
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sign Windows binaries with Azure Trusted Signing
|
||||
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0
|
||||
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0.5.11
|
||||
with:
|
||||
endpoint: ${{ inputs.endpoint }}
|
||||
trusted-signing-account-name: ${{ inputs.account-name }}
|
||||
|
||||
62
.github/scripts/setup-dev-drive.ps1
vendored
Normal file
62
.github/scripts/setup-dev-drive.ps1
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Configure a fast drive for Windows CI jobs.
|
||||
#
|
||||
# GitHub-hosted Windows runners do not always expose a secondary D: volume. When
|
||||
# they do not, try to create a Dev Drive VHD and fall back to C: if the runner
|
||||
# image does not allow that provisioning path.
|
||||
|
||||
function Use-FallbackDrive {
|
||||
param([string]$Reason)
|
||||
|
||||
Write-Warning "$Reason Falling back to C:"
|
||||
return "C:"
|
||||
}
|
||||
|
||||
function Invoke-BestEffort {
|
||||
param([scriptblock]$Script, [string]$Description)
|
||||
|
||||
try {
|
||||
& $Script
|
||||
} catch {
|
||||
Write-Warning "$Description failed: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path "D:\") {
|
||||
Write-Output "Using existing drive at D:"
|
||||
$Drive = "D:"
|
||||
} else {
|
||||
try {
|
||||
$VhdPath = Join-Path $env:RUNNER_TEMP "codex-dev-drive.vhdx"
|
||||
$SizeBytes = 64GB
|
||||
|
||||
if (Test-Path $VhdPath) {
|
||||
Remove-Item -Path $VhdPath -Force
|
||||
}
|
||||
|
||||
New-VHD -Path $VhdPath -SizeBytes $SizeBytes -Dynamic -ErrorAction Stop | Out-Null
|
||||
$Mounted = Mount-VHD -Path $VhdPath -Passthru -ErrorAction Stop
|
||||
$Disk = $Mounted | Get-Disk -ErrorAction Stop
|
||||
$Disk | Initialize-Disk -PartitionStyle GPT -ErrorAction Stop
|
||||
$Partition = $Disk | New-Partition -AssignDriveLetter -UseMaximumSize -ErrorAction Stop
|
||||
$Volume = $Partition | Format-Volume -FileSystem ReFS -NewFileSystemLabel "CodexDevDrive" -DevDrive -Confirm:$false -Force -ErrorAction Stop
|
||||
|
||||
$Drive = "$($Volume.DriveLetter):"
|
||||
|
||||
Invoke-BestEffort { fsutil devdrv trust $Drive } "Trusting Dev Drive $Drive"
|
||||
Invoke-BestEffort { fsutil devdrv enable /disallowAv } "Disabling AV filter attachment for Dev Drives"
|
||||
Invoke-BestEffort { fsutil devdrv query $Drive } "Querying Dev Drive $Drive"
|
||||
|
||||
Write-Output "Using Dev Drive at $Drive"
|
||||
} catch {
|
||||
$Drive = Use-FallbackDrive "Failed to create Dev Drive: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
$Tmp = "$Drive\codex-tmp"
|
||||
New-Item -Path $Tmp -ItemType Directory -Force | Out-Null
|
||||
|
||||
@(
|
||||
"DEV_DRIVE=$Drive"
|
||||
"TMP=$Tmp"
|
||||
"TEMP=$Tmp"
|
||||
) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
24
.github/workflows/bazel.yml
vendored
24
.github/workflows/bazel.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
name: Bazel test on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check rusty_v8 MODULE.bazel checksums
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: bazel-execution-logs-test-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm (native main)
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare Bazel CI
|
||||
id: prepare_bazel
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: bazel-execution-logs-test-windows-native-x86_64-pc-windows-gnullvm
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare Bazel CI
|
||||
id: prepare_bazel
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: bazel-execution-logs-clippy-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
@@ -297,7 +297,7 @@ jobs:
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
name: Verify release build on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare Bazel CI
|
||||
id: prepare_bazel
|
||||
@@ -390,7 +390,7 @@ jobs:
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: bazel-execution-logs-verify-release-build-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
@@ -401,7 +401,7 @@ jobs:
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
|
||||
|
||||
2
.github/workflows/blob-size-policy.yml
vendored
2
.github/workflows/blob-size-policy.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
name: Blob size policy
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
2
.github/workflows/cargo-deny.yml
vendored
2
.github/workflows/cargo-deny.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
working-directory: ./codex-rs
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Verify codex-rs Cargo manifests inherit workspace settings
|
||||
run: python3 .github/scripts/verify_cargo_workspace_manifests.py
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload staged npm package artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: codex-npm-staging
|
||||
path: ${{ steps.stage_npm_package.outputs.pack_output }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close inactive PRs from contributors
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
4
.github/workflows/codespell.yml
vendored
4
.github/workflows/codespell.yml
vendored
@@ -18,9 +18,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Annotate locations with typos
|
||||
uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1
|
||||
uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1.1.0
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
||||
with:
|
||||
|
||||
6
.github/workflows/issue-deduplicator.yml
vendored
6
.github/workflows/issue-deduplicator.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
reason: ${{ steps.normalize-all.outputs.reason }}
|
||||
has_matches: ${{ steps.normalize-all.outputs.has_matches }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare Codex inputs
|
||||
env:
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
reason: ${{ steps.normalize-open.outputs.reason }}
|
||||
has_matches: ${{ steps.normalize-open.outputs.has_matches }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare Codex inputs
|
||||
env:
|
||||
@@ -342,7 +342,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Comment on issue
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
CODEX_OUTPUT: ${{ needs.select-final.outputs.codex_output }}
|
||||
with:
|
||||
|
||||
2
.github/workflows/issue-labeler.yml
vendored
2
.github/workflows/issue-labeler.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
outputs:
|
||||
codex_output: ${{ steps.codex.outputs.final-message }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- id: codex
|
||||
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02 # v1.7
|
||||
|
||||
98
.github/workflows/rust-ci-full.yml
vendored
98
.github/workflows/rust-ci-full.yml
vendored
@@ -1,10 +1,21 @@
|
||||
name: rust-ci-full
|
||||
run-name: >-
|
||||
rust-ci-full${{
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
format(' windows-nextest-{0}', inputs.windows_nextest_threads) ||
|
||||
''
|
||||
}}
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "**full-ci**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
windows_nextest_threads:
|
||||
description: "Optional nextest --test-threads override for Windows test jobs"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
# CI builds in debug (dev) for faster signal.
|
||||
|
||||
@@ -17,7 +28,7 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
components: rustfmt
|
||||
@@ -31,12 +42,12 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: cargo-shear
|
||||
version: 1.5.1
|
||||
version: 1.11.2
|
||||
- name: cargo shear
|
||||
run: cargo shear
|
||||
|
||||
@@ -47,14 +58,14 @@ jobs:
|
||||
CARGO_DYLINT_VERSION: 5.0.0
|
||||
DYLINT_LINK_VERSION: 5.0.0
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
toolchain: nightly-2025-09-18
|
||||
components: llvm-tools-preview, rustc-dev, rust-src
|
||||
- name: Cache cargo-dylint tooling
|
||||
id: cargo_dylint_cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-dylint
|
||||
@@ -97,7 +108,7 @@ jobs:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/setup-bazel-ci
|
||||
with:
|
||||
target: ${{ runner.os }}
|
||||
@@ -233,7 +244,11 @@ jobs:
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Configure Dev Drive (Windows)
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
shell: pwsh
|
||||
run: ../.github/scripts/setup-dev-drive.ps1
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -276,7 +291,7 @@ jobs:
|
||||
# avoid caching the large target dir on the gnu-dev job.
|
||||
- name: Restore cargo home cache
|
||||
id: cache_cargo_home_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -294,7 +309,7 @@ jobs:
|
||||
# Install and restore sccache cache
|
||||
- name: Install sccache
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: sccache
|
||||
version: 0.7.5
|
||||
@@ -321,7 +336,7 @@ jobs:
|
||||
- name: Restore sccache cache (fallback)
|
||||
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
|
||||
id: cache_sccache_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
@@ -348,7 +363,7 @@ jobs:
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Restore APT cache (musl)
|
||||
id: cache_apt_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
/var/cache/apt
|
||||
@@ -356,7 +371,7 @@ jobs:
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Install Zig
|
||||
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
|
||||
with:
|
||||
version: 0.14.0
|
||||
|
||||
@@ -430,7 +445,7 @@ jobs:
|
||||
|
||||
- name: Install cargo-chef
|
||||
if: ${{ matrix.profile == 'release' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: cargo-chef
|
||||
version: 0.1.71
|
||||
@@ -449,7 +464,7 @@ jobs:
|
||||
|
||||
- name: Upload Cargo timings (clippy)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-ci-clippy-${{ matrix.target }}-${{ matrix.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
@@ -460,7 +475,7 @@ jobs:
|
||||
- name: Save cargo home cache
|
||||
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -476,7 +491,7 @@ jobs:
|
||||
- name: Save sccache cache (fallback)
|
||||
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
@@ -501,7 +516,7 @@ jobs:
|
||||
- name: Save APT cache (musl)
|
||||
if: always() && !cancelled() && (matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl') && steps.cache_apt_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
/var/cache/apt
|
||||
@@ -510,10 +525,10 @@ jobs:
|
||||
tests:
|
||||
name: Tests — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.remote_env == 'true' && ' (remote)' || '' }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
# Perhaps we can bring this back down to 30m once we finish the cutover
|
||||
# from tui_app_server/ to tui/. Incidentally, windows-arm64 was the main
|
||||
# offender for exceeding the timeout.
|
||||
timeout-minutes: 45
|
||||
# Perhaps we can bring this back down once we finish the cutover from
|
||||
# tui_app_server/ to tui/. Incidentally, windows-arm64 was the main offender
|
||||
# for exceeding the timeout.
|
||||
timeout-minutes: ${{ matrix.timeout_minutes || 45 }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
@@ -524,6 +539,7 @@ jobs:
|
||||
USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
|
||||
CARGO_INCREMENTAL: "0"
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
WINDOWS_NEXTEST_THREADS: ${{ github.event_name == 'workflow_dispatch' && inputs.windows_nextest_threads || '' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -554,12 +570,17 @@ jobs:
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
profile: dev
|
||||
timeout_minutes: 75
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Configure Dev Drive (Windows)
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
shell: pwsh
|
||||
run: ../.github/scripts/setup-dev-drive.ps1
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -590,7 +611,7 @@ jobs:
|
||||
|
||||
- name: Restore cargo home cache
|
||||
id: cache_cargo_home_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -603,7 +624,7 @@ jobs:
|
||||
|
||||
- name: Install sccache
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: sccache
|
||||
version: 0.7.5
|
||||
@@ -630,7 +651,7 @@ jobs:
|
||||
- name: Restore sccache cache (fallback)
|
||||
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
|
||||
id: cache_sccache_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
@@ -638,10 +659,9 @@ jobs:
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
|
||||
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: nextest
|
||||
version: 0.9.103
|
||||
tool: nextest@0.9.103
|
||||
|
||||
- name: Enable unprivileged user namespaces (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
@@ -666,7 +686,19 @@ jobs:
|
||||
|
||||
- name: tests
|
||||
id: test
|
||||
run: cargo nextest run --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test --timings
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nextest_args=(
|
||||
--no-fail-fast
|
||||
--target "${{ matrix.target }}"
|
||||
--cargo-profile ci-test
|
||||
--timings
|
||||
)
|
||||
if [[ "${{ runner.os }}" == "Windows" && -n "${WINDOWS_NEXTEST_THREADS}" ]]; then
|
||||
nextest_args+=(--test-threads "${WINDOWS_NEXTEST_THREADS}")
|
||||
fi
|
||||
cargo nextest run "${nextest_args[@]}"
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
RUST_MIN_STACK: "8388608" # 8 MiB
|
||||
@@ -674,7 +706,7 @@ jobs:
|
||||
|
||||
- name: Upload Cargo timings (nextest)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-ci-nextest-${{ matrix.target }}-${{ matrix.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
@@ -683,7 +715,7 @@ jobs:
|
||||
- name: Save cargo home cache
|
||||
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -695,7 +727,7 @@ jobs:
|
||||
- name: Save sccache cache (fallback)
|
||||
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
|
||||
16
.github/workflows/rust-ci.yml
vendored
16
.github/workflows/rust-ci.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
codex: ${{ steps.detect.outputs.codex }}
|
||||
workflows: ${{ steps.detect.outputs.workflows }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Detect changed paths (no external action)
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
components: rustfmt
|
||||
@@ -77,12 +77,12 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: cargo-shear
|
||||
version: 1.5.1
|
||||
version: 1.11.2
|
||||
- name: cargo shear
|
||||
run: cargo shear
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
CARGO_DYLINT_VERSION: 5.0.0
|
||||
DYLINT_LINK_VERSION: 5.0.0
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- name: Install nightly argument-comment-lint toolchain
|
||||
shell: bash
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
rustup default nightly-2025-09-18
|
||||
- name: Cache cargo-dylint tooling
|
||||
id: cargo_dylint_cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-dylint
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
|
||||
echo "No argument-comment-lint relevant changes."
|
||||
echo "run=false" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
if: ${{ steps.argument_comment_lint_gate.outputs.run == 'true' }}
|
||||
- name: Run argument comment lint on codex-rs via Bazel
|
||||
if: ${{ steps.argument_comment_lint_gate.outputs.run == 'true' }}
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
labels: codex-windows-x64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
(cd "${RUNNER_TEMP}" && tar -czf "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint)
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: argument-comment-lint-${{ matrix.target }}
|
||||
path: dist/argument-comment-lint/${{ matrix.target }}/*
|
||||
|
||||
4
.github/workflows/rust-release-prepare.yml
vendored
4
.github/workflows/rust-release-prepare.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
if: github.repository == 'openai/codex'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
curl --http1.1 --fail --show-error --location "${headers[@]}" "${url}" | jq '.' > codex-rs/models-manager/models.json
|
||||
|
||||
- name: Open pull request (if changed)
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
commit-message: "Update models.json"
|
||||
title: "Update models.json"
|
||||
|
||||
16
.github/workflows/rust-release-windows.yml
vendored
16
.github/workflows/rust-release-windows.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Print runner specs (Windows)
|
||||
shell: powershell
|
||||
run: |
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
|
||||
|
||||
- name: Upload Cargo timings
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-release-windows-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Upload Windows binaries
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: |
|
||||
@@ -165,22 +165,22 @@ jobs:
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download prebuilt Windows primary binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-primary
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
|
||||
- name: Download prebuilt Windows helper binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-helpers
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
|
||||
- name: Download prebuilt Windows app-server binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-app-server
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
"${GITHUB_WORKSPACE}/.github/workflows/zstd" -T0 -19 "$dest/$base"
|
||||
done
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: |
|
||||
|
||||
8
.github/workflows/rust-release-zsh.yml
vendored
8
.github/workflows/rust-release-zsh.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
git \
|
||||
libncursesw5-dev
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build, smoke-test, and stage zsh artifact
|
||||
shell: bash
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
|
||||
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: codex-zsh-${{ matrix.target }}
|
||||
path: dist/zsh/${{ matrix.target }}/*
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
brew install autoconf
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build, smoke-test, and stage zsh artifact
|
||||
shell: bash
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
|
||||
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: codex-zsh-${{ matrix.target }}
|
||||
path: dist/zsh/${{ matrix.target }}/*
|
||||
|
||||
20
.github/workflows/rust-release.yml
vendored
20
.github/workflows/rust-release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
tag-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- name: Validate tag matches Cargo.toml version
|
||||
shell: bash
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
build_dmg: "false"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Print runner specs (Linux)
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Install Zig
|
||||
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
|
||||
with:
|
||||
version: 0.14.0
|
||||
|
||||
@@ -284,7 +284,7 @@ jobs:
|
||||
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
|
||||
|
||||
- name: Upload Cargo timings
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
@@ -430,7 +430,7 @@ jobs:
|
||||
zstd -T0 -19 --rm "$dest/$base"
|
||||
done
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
# Upload the per-binary .zst files, .tar.gz equivalents, and any
|
||||
@@ -476,7 +476,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Generate release notes from tag commit message
|
||||
id: release_notes
|
||||
@@ -498,7 +498,7 @@ jobs:
|
||||
|
||||
echo "path=${notes_path}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: dist
|
||||
|
||||
@@ -553,7 +553,7 @@ jobs:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js for npm packaging
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -579,7 +579,7 @@ jobs:
|
||||
cp scripts/install/install.ps1 dist/install.ps1
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
name: ${{ steps.release_name.outputs.name }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
@@ -638,7 +638,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
# Node 24 bundles npm >= 11.5.1, which trusted publishing requires.
|
||||
node-version: 24
|
||||
|
||||
14
.github/workflows/rusty-v8-release.yml
vendored
14
.github/workflows/rusty-v8-release.yml
vendored
@@ -17,10 +17,10 @@ jobs:
|
||||
v8_version: ${{ steps.v8_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
target: aarch64-unknown-linux-musl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: ./.github/actions/setup-bazel-ci
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
--output-dir "dist/${TARGET}"
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
|
||||
path: dist/${{ matrix.target }}/*
|
||||
@@ -161,12 +161,12 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: dist
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
tag_name: ${{ needs.metadata.outputs.release_tag }}
|
||||
name: ${{ needs.metadata.outputs.release_tag }}
|
||||
|
||||
6
.github/workflows/sdk.yml
vendored
6
.github/workflows/sdk.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install Linux bwrap build dependencies
|
||||
shell: bash
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/bazel-repo-cache
|
||||
|
||||
10
.github/workflows/v8-canary.yml
vendored
10
.github/workflows/v8-canary.yml
vendored
@@ -40,10 +40,10 @@ jobs:
|
||||
v8_version: ${{ steps.v8_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
target: aarch64-unknown-linux-musl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: ./.github/actions/setup-bazel-ci
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
--output-dir "dist/${TARGET}"
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
|
||||
path: dist/${{ matrix.target }}/*
|
||||
|
||||
@@ -14,6 +14,9 @@ max-threads = 1
|
||||
[test-groups.windows_sandbox_legacy_sessions]
|
||||
max-threads = 1
|
||||
|
||||
[test-groups.windows_process_heavy]
|
||||
max-threads = 1
|
||||
|
||||
[[profile.default.overrides]]
|
||||
# Do not add new tests here
|
||||
filter = 'test(rmcp_client) | test(humanlike_typing_1000_chars_appears_live_no_placeholder)'
|
||||
@@ -27,6 +30,41 @@ slow-timeout = { period = "30s", terminate-after = 2 }
|
||||
filter = 'package(codex-app-server-protocol) & (test(typescript_schema_fixtures_match_generated) | test(json_schema_fixtures_match_generated) | test(generate_ts_with_experimental_api_retains_experimental_entries) | test(generated_ts_optional_nullable_fields_only_in_params) | test(generate_json_filters_experimental_fields_and_methods))'
|
||||
test-group = 'app_server_protocol_codegen'
|
||||
|
||||
[[profile.default.overrides]]
|
||||
# These Windows CI tests launch full Codex/app-server process trees. They have
|
||||
# repeatedly timed out when nextest schedules them alongside similar tests.
|
||||
platform = 'cfg(windows)'
|
||||
filter = 'package(codex-core) & kind(test) & (test(cli_stream) | test(realtime_conversation))'
|
||||
test-group = 'windows_process_heavy'
|
||||
threads-required = "num-test-threads"
|
||||
slow-timeout = { period = "1m", terminate-after = 4 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
# The exec resume tests spawn the CLI and touch shared session state on disk.
|
||||
platform = 'cfg(windows)'
|
||||
filter = 'package(codex-exec) & kind(test) & test(exec_resume)'
|
||||
test-group = 'windows_process_heavy'
|
||||
threads-required = "num-test-threads"
|
||||
slow-timeout = { period = "1m", terminate-after = 4 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
# Keep the specific app-server subprocess-heavy cases isolated on Windows. This
|
||||
# must stay before the broader codex-app-server override below.
|
||||
platform = 'cfg(windows)'
|
||||
filter = 'package(codex-app-server) & kind(test) & (test(thread_fork_can_exclude_turns_and_skip_restored_token_usage) | test(turn_start_resolves_sticky_thread_environments_and_turn_overrides) | test(message_processor_tracing_tests))'
|
||||
test-group = 'windows_process_heavy'
|
||||
threads-required = "num-test-threads"
|
||||
slow-timeout = { period = "1m", terminate-after = 4 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
# These tests create restricted-token Windows child processes and private
|
||||
# desktops. Running them alone avoids contention with other subprocess tests.
|
||||
platform = 'cfg(windows)'
|
||||
filter = 'package(codex-windows-sandbox) & kind(test) & test(legacy_)'
|
||||
test-group = 'windows_process_heavy'
|
||||
threads-required = "num-test-threads"
|
||||
slow-timeout = { period = "1m", terminate-after = 4 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
# These integration tests spawn a fresh app-server subprocess per case.
|
||||
# Keep the library unit tests parallel.
|
||||
|
||||
@@ -475,13 +475,13 @@ ignored = [
|
||||
[profile.dev]
|
||||
# Keep line tables/backtraces while avoiding expensive full variable debug info
|
||||
# across local dev builds.
|
||||
debug = 1
|
||||
debug = "limited"
|
||||
|
||||
[profile.dev-small]
|
||||
inherits = "dev"
|
||||
opt-level = 0
|
||||
debug = 0
|
||||
strip = true
|
||||
debug = "none"
|
||||
strip = "symbols"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
@@ -493,8 +493,15 @@ strip = "symbols"
|
||||
# See https://github.com/openai/codex/issues/1411 for details.
|
||||
codegen-units = 1
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
debug = "full"
|
||||
lto = false
|
||||
strip = false
|
||||
|
||||
[profile.ci-test]
|
||||
debug = 1 # Reduce debug symbol size
|
||||
# Reduce binary size to reduce disk pressure.
|
||||
debug = "limited"
|
||||
inherits = "test"
|
||||
opt-level = 0
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ clap = { workspace = true, features = ["derive"] }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
time = { workspace = true }
|
||||
@@ -87,20 +86,19 @@ tokio = { workspace = true, features = [
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||
|
||||
[dev-dependencies]
|
||||
app_test_support = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
axum = { workspace = true, default-features = false, features = [
|
||||
"http1",
|
||||
"json",
|
||||
"tokio",
|
||||
] }
|
||||
core_test_support = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
core_test_support = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
opentelemetry = { workspace = true }
|
||||
@@ -113,8 +111,10 @@ rmcp = { workspace = true, default-features = false, features = [
|
||||
"transport-streamable-http-server",
|
||||
] }
|
||||
serial_test = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tracing-opentelemetry = { workspace = true }
|
||||
url = { workspace = true }
|
||||
wiremock = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
|
||||
@@ -13,10 +13,8 @@ use crate::outgoing_message::RequestContext;
|
||||
use crate::outgoing_message::ThreadScopedOutgoingMessageSender;
|
||||
use crate::thread_status::ThreadWatchManager;
|
||||
use crate::thread_status::resolve_thread_status;
|
||||
use chrono::DateTime;
|
||||
use chrono::Duration as ChronoDuration;
|
||||
use chrono::SecondsFormat;
|
||||
use chrono::Utc;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_analytics::AnalyticsJsonRpcError;
|
||||
use codex_analytics::InputError;
|
||||
@@ -274,7 +272,6 @@ use codex_core::exec::ExecCapturePolicy;
|
||||
use codex_core::exec::ExecExpiration;
|
||||
use codex_core::exec::ExecParams;
|
||||
use codex_core::exec_env::create_env;
|
||||
use codex_core::find_thread_name_by_id;
|
||||
use codex_core::find_thread_path_by_id_str;
|
||||
use codex_core::path_utils;
|
||||
#[cfg(test)]
|
||||
@@ -496,6 +493,7 @@ pub(crate) use self::thread_lifecycle::populate_thread_turns_from_history;
|
||||
pub(crate) use self::thread_processor::thread_from_stored_thread;
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::thread_summary::read_summary_from_rollout;
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::thread_summary::summary_to_thread;
|
||||
|
||||
pub(crate) fn build_api_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {
|
||||
|
||||
@@ -1605,11 +1605,8 @@ impl ThreadRequestProcessor {
|
||||
.unarchive_thread(StoreArchiveThreadParams { thread_id })
|
||||
.await
|
||||
.map_err(|err| thread_store_archive_error("unarchive", err))?;
|
||||
let summary = summary_from_stored_thread(stored_thread, fallback_provider.as_str())
|
||||
.ok_or_else(|| {
|
||||
internal_error(format!("failed to read unarchived thread {thread_id}"))
|
||||
})?;
|
||||
let mut thread = summary_to_thread(summary, &self.config.cwd);
|
||||
let (mut thread, _) =
|
||||
thread_from_stored_thread(stored_thread, fallback_provider.as_str(), &self.config.cwd);
|
||||
|
||||
thread.status = resolve_thread_status(
|
||||
self.thread_watch_manager
|
||||
@@ -2914,10 +2911,19 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
|
||||
async fn attach_thread_name(&self, thread_id: ThreadId, thread: &mut Thread) {
|
||||
if let Some(title) =
|
||||
title_from_state_db(&self.config, self.state_db.as_ref(), thread_id).await
|
||||
if let Ok(stored_thread) = self
|
||||
.thread_store
|
||||
.read_thread(StoreReadThreadParams {
|
||||
thread_id,
|
||||
include_archived: true,
|
||||
include_history: false,
|
||||
})
|
||||
.await
|
||||
&& let Some(title) = stored_thread.name.as_deref().map(str::trim)
|
||||
&& !title.is_empty()
|
||||
&& stored_thread.preview.trim() != title
|
||||
{
|
||||
set_thread_name_from_title(thread, title);
|
||||
set_thread_name_from_title(thread, title.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3206,12 +3212,7 @@ impl ThreadRequestProcessor {
|
||||
};
|
||||
|
||||
let stored_thread = read_result?;
|
||||
let summary =
|
||||
summary_from_stored_thread(stored_thread, fallback_provider).ok_or_else(|| {
|
||||
internal_error(
|
||||
"failed to load conversation summary: thread is missing rollout path",
|
||||
)
|
||||
})?;
|
||||
let summary = summary_from_stored_thread(stored_thread, fallback_provider);
|
||||
Ok(GetConversationSummaryResponse { summary })
|
||||
}
|
||||
|
||||
@@ -3625,37 +3626,6 @@ fn thread_store_archive_error(operation: &str, err: ThreadStoreError) -> JSONRPC
|
||||
}
|
||||
}
|
||||
|
||||
async fn title_from_state_db(
|
||||
config: &Config,
|
||||
state_db_ctx: Option<&StateDbHandle>,
|
||||
thread_id: ThreadId,
|
||||
) -> Option<String> {
|
||||
if let Some(state_db_ctx) = state_db_ctx
|
||||
&& let Some(metadata) = state_db_ctx.get_thread(thread_id).await.ok().flatten()
|
||||
&& let Some(title) = distinct_title(&metadata)
|
||||
{
|
||||
return Some(title);
|
||||
}
|
||||
find_thread_name_by_id(&config.codex_home, &thread_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn non_empty_title(metadata: &ThreadMetadata) -> Option<String> {
|
||||
let title = metadata.title.trim();
|
||||
(!title.is_empty()).then(|| title.to_string())
|
||||
}
|
||||
|
||||
fn distinct_title(metadata: &ThreadMetadata) -> Option<String> {
|
||||
let title = non_empty_title(metadata)?;
|
||||
if metadata.first_user_message.as_deref().map(str::trim) == Some(title.as_str()) {
|
||||
None
|
||||
} else {
|
||||
Some(title)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_thread_name_from_title(thread: &mut Thread, title: String) {
|
||||
if title.trim().is_empty() || thread.preview.trim() == title.trim() {
|
||||
return;
|
||||
@@ -3719,8 +3689,8 @@ pub(crate) fn thread_from_stored_thread(
|
||||
fn summary_from_stored_thread(
|
||||
thread: StoredThread,
|
||||
fallback_provider: &str,
|
||||
) -> Option<ConversationSummary> {
|
||||
let path = thread.rollout_path?;
|
||||
) -> ConversationSummary {
|
||||
let path = thread.rollout_path.unwrap_or_default();
|
||||
let source = with_thread_spawn_agent_metadata(
|
||||
thread.source,
|
||||
thread.agent_nickname.clone(),
|
||||
@@ -3731,7 +3701,7 @@ fn summary_from_stored_thread(
|
||||
branch: git.branch,
|
||||
origin_url: git.repository_url,
|
||||
});
|
||||
Some(ConversationSummary {
|
||||
ConversationSummary {
|
||||
conversation_id: thread.thread_id,
|
||||
path,
|
||||
preview: thread.first_user_message.unwrap_or(thread.preview),
|
||||
@@ -3756,7 +3726,7 @@ fn summary_from_stored_thread(
|
||||
cli_version: thread.cli_version,
|
||||
source,
|
||||
git_info,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -409,8 +409,7 @@ mod thread_processor_behavior_tests {
|
||||
history: None,
|
||||
};
|
||||
|
||||
let summary =
|
||||
summary_from_stored_thread(stored_thread, "fallback").expect("summary should exist");
|
||||
let summary = summary_from_stored_thread(stored_thread, "fallback");
|
||||
|
||||
assert_eq!(
|
||||
summary.timestamp.as_deref(),
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
use chrono::DateTime;
|
||||
#[cfg(test)]
|
||||
use chrono::Utc;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn read_summary_from_rollout(
|
||||
path: &Path,
|
||||
@@ -203,6 +208,7 @@ pub(super) fn thread_response_sandbox_policy(
|
||||
sandbox_policy.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parse_datetime(timestamp: Option<&str>) -> Option<DateTime<Utc>> {
|
||||
timestamp.and_then(|ts| {
|
||||
chrono::DateTime::parse_from_rfc3339(ts)
|
||||
@@ -229,6 +235,7 @@ pub(super) fn thread_started_notification(mut thread: Thread) -> ThreadStartedNo
|
||||
ThreadStartedNotification { thread }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn summary_to_thread(
|
||||
summary: ConversationSummary,
|
||||
fallback_cwd: &AbsolutePathBuf,
|
||||
@@ -257,6 +264,7 @@ pub(crate) fn summary_to_thread(
|
||||
AbsolutePathBuf::relative_to_current_dir(path_utils::normalize_for_native_workdir(cwd))
|
||||
.unwrap_or_else(|err| {
|
||||
warn!(
|
||||
conversation_id = %conversation_id,
|
||||
path = %path.display(),
|
||||
"failed to normalize thread cwd while summarizing thread: {err}"
|
||||
);
|
||||
@@ -274,7 +282,7 @@ pub(crate) fn summary_to_thread(
|
||||
created_at: created_at.map(|dt| dt.timestamp()).unwrap_or(0),
|
||||
updated_at: updated_at.map(|dt| dt.timestamp()).unwrap_or(0),
|
||||
status: ThreadStatus::NotLoaded,
|
||||
path: Some(path),
|
||||
path: (!path.as_os_str().is_empty()).then_some(path),
|
||||
cwd,
|
||||
cli_version,
|
||||
agent_nickname: source.get_nickname(),
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::TokenCountEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use core_test_support::test_path_buf;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
use std::fs::FileTimes;
|
||||
@@ -134,7 +135,7 @@ pub fn create_fake_rollout_with_source(
|
||||
id: conversation_id,
|
||||
forked_from_id: None,
|
||||
timestamp: meta_rfc3339.to_string(),
|
||||
cwd: PathBuf::from("/"),
|
||||
cwd: test_path_buf("/"),
|
||||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source,
|
||||
@@ -218,7 +219,7 @@ pub fn create_fake_rollout_with_text_elements(
|
||||
id: conversation_id,
|
||||
forked_from_id: None,
|
||||
timestamp: meta_rfc3339.to_string(),
|
||||
cwd: PathBuf::from("/"),
|
||||
cwd: test_path_buf("/"),
|
||||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
|
||||
@@ -3,20 +3,42 @@ use app_test_support::McpProcess;
|
||||
use app_test_support::create_fake_rollout;
|
||||
use app_test_support::rollout_path;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server::in_process;
|
||||
use codex_app_server::in_process::InProcessStartArgs;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ConversationSummary;
|
||||
use codex_app_server_protocol::GetConversationSummaryParams;
|
||||
use codex_app_server_protocol::GetConversationSummaryResponse;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_thread_store::CreateThreadParams;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_thread_store::ThreadEventPersistenceMode;
|
||||
use codex_thread_store::ThreadPersistenceMetadata;
|
||||
use codex_thread_store::ThreadStore;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::test_path_buf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const FILENAME_TS: &str = "2025-01-02T12-00-00";
|
||||
@@ -35,7 +57,7 @@ fn expected_summary(conversation_id: ThreadId, path: PathBuf) -> ConversationSum
|
||||
timestamp: Some(CREATED_AT_RFC3339.to_string()),
|
||||
updated_at: Some(UPDATED_AT_RFC3339.to_string()),
|
||||
model_provider: MODEL_PROVIDER.to_string(),
|
||||
cwd: PathBuf::from("/"),
|
||||
cwd: test_path_buf("/"),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
git_info: None,
|
||||
@@ -47,7 +69,9 @@ fn normalized_canonical_path(path: impl AsRef<Path>) -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
fn normalized_summary_path(mut summary: ConversationSummary) -> Result<ConversationSummary> {
|
||||
summary.path = normalized_canonical_path(&summary.path)?;
|
||||
if !summary.path.as_os_str().is_empty() {
|
||||
summary.path = normalized_canonical_path(summary.path)?;
|
||||
}
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
@@ -122,6 +146,87 @@ async fn get_conversation_summary_by_rollout_path_rejects_remote_thread_store()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let store_id = Uuid::new_v4().to_string();
|
||||
create_config_toml_with_in_memory_thread_store(codex_home.path(), &store_id)?;
|
||||
let store = InMemoryThreadStore::for_id(store_id.clone());
|
||||
let _in_memory_store = InMemoryThreadStoreId { store_id };
|
||||
let thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000125")?;
|
||||
store
|
||||
.create_thread(CreateThreadParams {
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
source: SessionSource::Cli,
|
||||
thread_source: None,
|
||||
base_instructions: BaseInstructions::default(),
|
||||
dynamic_tools: Vec::new(),
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
memory_mode: ThreadMemoryMode::Disabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let loader_overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.loader_overrides(loader_overrides.clone())
|
||||
.build()
|
||||
.await?;
|
||||
let client = in_process::start(InProcessStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config: Arc::new(config),
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides,
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli,
|
||||
enable_codex_api_key_env: false,
|
||||
initialize: InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "codex-app-server-tests".to_string(),
|
||||
title: None,
|
||||
version: "0.1.0".to_string(),
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
channel_capacity: in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let result = client
|
||||
.request(ClientRequest::GetConversationSummary {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: GetConversationSummaryParams::ThreadId {
|
||||
conversation_id: thread_id,
|
||||
},
|
||||
})
|
||||
.await?
|
||||
.expect("getConversationSummary should succeed");
|
||||
let GetConversationSummaryResponse { summary } = serde_json::from_value(result)?;
|
||||
|
||||
assert_eq!(summary.conversation_id, thread_id);
|
||||
assert_eq!(summary.path, PathBuf::new());
|
||||
assert_eq!(summary.cwd, PathBuf::new());
|
||||
assert_eq!(summary.model_provider, "test");
|
||||
|
||||
client.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_home() -> Result<()>
|
||||
{
|
||||
@@ -157,3 +262,39 @@ async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_h
|
||||
assert_eq!(normalized_summary_path(received.summary)?, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct InMemoryThreadStoreId {
|
||||
store_id: String,
|
||||
}
|
||||
|
||||
impl Drop for InMemoryThreadStoreId {
|
||||
fn drop(&mut self) {
|
||||
InMemoryThreadStore::remove_id(&self.store_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_config_toml_with_in_memory_thread_store(
|
||||
codex_home: &Path,
|
||||
store_id: &str,
|
||||
) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
experimental_thread_store = {{ type = "in_memory", id = "{store_id}" }}
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "http://127.0.0.1:1/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -426,6 +426,8 @@ fn realtime_sideband_connection(
|
||||
WebSocketConnectionConfig {
|
||||
requests: realtime_server_events,
|
||||
response_headers: Vec::new(),
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
accept_delay: None,
|
||||
close_after_requests: true,
|
||||
}
|
||||
@@ -1044,6 +1046,8 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> {
|
||||
"session": { "id": "sess_webrtc", "instructions": "backend prompt" }
|
||||
})]],
|
||||
response_headers: Vec::new(),
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
accept_delay: None,
|
||||
close_after_requests: false,
|
||||
}])
|
||||
@@ -1836,7 +1840,10 @@ async fn webrtc_v2_tool_call_delegated_turn_can_execute_shell_tool() -> Result<(
|
||||
};
|
||||
assert_eq!(id.as_str(), "shell_call");
|
||||
assert_eq!(status, CommandExecutionStatus::Completed);
|
||||
assert_eq!(aggregated_output.as_deref(), Some("realtime-tool-ok"));
|
||||
assert_eq!(
|
||||
aggregated_output.as_deref().map(str::trim),
|
||||
Some("realtime-tool-ok")
|
||||
);
|
||||
|
||||
// Phase 3: verify the shell output reached Responses and the final delegated answer returned
|
||||
// to realtime as a single function-call-output item.
|
||||
@@ -2154,10 +2161,10 @@ fn realtime_tool_ok_command() -> Vec<String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
vec![
|
||||
"powershell.exe".to_string(),
|
||||
"-NoProfile".to_string(),
|
||||
"-Command".to_string(),
|
||||
"[Console]::Write('realtime-tool-ok')".to_string(),
|
||||
"cmd.exe".to_string(),
|
||||
"/D".to_string(),
|
||||
"/C".to_string(),
|
||||
"echo realtime-tool-ok".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@ use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server::in_process;
|
||||
use codex_app_server::in_process::InProcessStartArgs;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
@@ -15,17 +21,36 @@ use codex_app_server_protocol::ThreadUnarchivedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::find_archived_thread_path_by_id_str;
|
||||
use codex_core::find_thread_path_by_id_str;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_thread_store::CreateThreadParams;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_thread_store::ThreadEventPersistenceMode;
|
||||
use codex_thread_store::ThreadMetadataPatch;
|
||||
use codex_thread_store::ThreadPersistenceMetadata;
|
||||
use codex_thread_store::ThreadStore;
|
||||
use codex_thread_store::UpdateThreadMetadataParams;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use std::fs::FileTimes;
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
@@ -172,11 +197,139 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let store_id = Uuid::new_v4().to_string();
|
||||
create_config_toml_with_in_memory_thread_store(codex_home.path(), &store_id)?;
|
||||
let store = InMemoryThreadStore::for_id(store_id.clone());
|
||||
let _in_memory_store = InMemoryThreadStoreId { store_id };
|
||||
let thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000126")?;
|
||||
let parent_thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000127")?;
|
||||
store
|
||||
.create_thread(CreateThreadParams {
|
||||
thread_id,
|
||||
forked_from_id: Some(parent_thread_id),
|
||||
source: SessionSource::Cli,
|
||||
thread_source: None,
|
||||
base_instructions: BaseInstructions::default(),
|
||||
dynamic_tools: Vec::new(),
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
memory_mode: ThreadMemoryMode::Disabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::default(),
|
||||
})
|
||||
.await?;
|
||||
store
|
||||
.update_thread_metadata(UpdateThreadMetadataParams {
|
||||
thread_id,
|
||||
patch: ThreadMetadataPatch {
|
||||
name: Some("named pathless thread".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
include_archived: true,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let loader_overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.loader_overrides(loader_overrides.clone())
|
||||
.build()
|
||||
.await?;
|
||||
let client = in_process::start(InProcessStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config: Arc::new(config),
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides,
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli,
|
||||
enable_codex_api_key_env: false,
|
||||
initialize: InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "codex-app-server-tests".to_string(),
|
||||
title: None,
|
||||
version: "0.1.0".to_string(),
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
channel_capacity: in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let result = client
|
||||
.request(ClientRequest::ThreadUnarchive {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: ThreadUnarchiveParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
},
|
||||
})
|
||||
.await?
|
||||
.expect("thread/unarchive should succeed");
|
||||
let ThreadUnarchiveResponse { thread } = serde_json::from_value(result)?;
|
||||
|
||||
assert_eq!(thread.id, thread_id.to_string());
|
||||
assert_eq!(thread.path, None);
|
||||
assert_eq!(thread.forked_from_id, Some(parent_thread_id.to_string()));
|
||||
assert_eq!(thread.name, Some("named pathless thread".to_string()));
|
||||
|
||||
client.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(config_toml, config_contents(server_uri))
|
||||
}
|
||||
|
||||
struct InMemoryThreadStoreId {
|
||||
store_id: String,
|
||||
}
|
||||
|
||||
impl Drop for InMemoryThreadStoreId {
|
||||
fn drop(&mut self) {
|
||||
InMemoryThreadStore::remove_id(&self.store_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_config_toml_with_in_memory_thread_store(
|
||||
codex_home: &Path,
|
||||
store_id: &str,
|
||||
) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
experimental_thread_store = {{ type = "in_memory", id = "{store_id}" }}
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "http://127.0.0.1:1/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn config_contents(server_uri: &str) -> String {
|
||||
format!(
|
||||
r#"model = "mock-model"
|
||||
|
||||
@@ -239,6 +239,65 @@ async fn wait_for_live_thread_spawn_children(
|
||||
.expect("expected persisted child tree");
|
||||
}
|
||||
|
||||
async fn wait_for_agent_shutdown(
|
||||
thread_id: ThreadId,
|
||||
mut status_rx: tokio::sync::watch::Receiver<AgentStatus>,
|
||||
) {
|
||||
if matches!(status_rx.borrow().clone(), AgentStatus::Shutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
status_rx
|
||||
.changed()
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("thread {thread_id} status should reach shutdown"));
|
||||
if matches!(status_rx.borrow().clone(), AgentStatus::Shutdown) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("thread {thread_id} should shut down before resume"));
|
||||
}
|
||||
|
||||
async fn shutdown_live_agent_and_wait(control: &AgentControl, thread_id: ThreadId) {
|
||||
let status_rx = control
|
||||
.subscribe_status(thread_id)
|
||||
.await
|
||||
.expect("status subscription should succeed before shutdown");
|
||||
let _ = control
|
||||
.shutdown_live_agent(thread_id)
|
||||
.await
|
||||
.expect("thread shutdown should submit");
|
||||
wait_for_agent_shutdown(thread_id, status_rx).await;
|
||||
}
|
||||
|
||||
async fn close_agent_and_wait(
|
||||
control: &AgentControl,
|
||||
agent_id: ThreadId,
|
||||
shutdown_ids: &[ThreadId],
|
||||
) {
|
||||
let mut status_rxs = Vec::with_capacity(shutdown_ids.len());
|
||||
for thread_id in shutdown_ids {
|
||||
status_rxs.push((
|
||||
*thread_id,
|
||||
control
|
||||
.subscribe_status(*thread_id)
|
||||
.await
|
||||
.expect("status subscription should succeed before close"),
|
||||
));
|
||||
}
|
||||
let _ = control
|
||||
.close_agent(agent_id)
|
||||
.await
|
||||
.expect("agent close should succeed");
|
||||
for (thread_id, status_rx) in status_rxs {
|
||||
wait_for_agent_shutdown(thread_id, status_rx).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_input_errors_when_manager_dropped() {
|
||||
let control = AgentControl::default();
|
||||
@@ -1626,11 +1685,9 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
||||
.await
|
||||
.expect("child thread metadata should be persisted to sqlite before shutdown");
|
||||
|
||||
let _ = harness
|
||||
.control
|
||||
.shutdown_live_agent(child_thread_id)
|
||||
.await
|
||||
.expect("child shutdown should submit");
|
||||
drop(status_rx);
|
||||
shutdown_live_agent_and_wait(&harness.control, child_thread_id).await;
|
||||
drop(child_thread);
|
||||
|
||||
let resumed_thread_id = harness
|
||||
.control
|
||||
@@ -1699,11 +1756,8 @@ async fn resume_agent_from_rollout_reads_archived_rollout_path() {
|
||||
.await
|
||||
.expect("child thread should exist");
|
||||
persist_thread_for_tree_resume(&child_thread, "persist before archiving").await;
|
||||
let _ = harness
|
||||
.control
|
||||
.shutdown_live_agent(child_thread_id)
|
||||
.await
|
||||
.expect("child shutdown should succeed");
|
||||
shutdown_live_agent_and_wait(&harness.control, child_thread_id).await;
|
||||
drop(child_thread);
|
||||
let store = LocalThreadStore::new(
|
||||
LocalThreadStoreConfig::from_config(&harness.config),
|
||||
harness.state_db.clone(),
|
||||
@@ -1993,11 +2047,12 @@ async fn shutdown_agent_tree_closes_descendants_when_started_at_child() {
|
||||
wait_for_live_thread_spawn_children(&harness.control, child_thread_id, &[grandchild_thread_id])
|
||||
.await;
|
||||
|
||||
let _ = harness
|
||||
.control
|
||||
.close_agent(child_thread_id)
|
||||
.await
|
||||
.expect("child close should succeed");
|
||||
close_agent_and_wait(
|
||||
&harness.control,
|
||||
child_thread_id,
|
||||
&[child_thread_id, grandchild_thread_id],
|
||||
)
|
||||
.await;
|
||||
|
||||
let _ = harness
|
||||
.control
|
||||
@@ -2085,16 +2140,14 @@ async fn resume_agent_from_rollout_does_not_reopen_closed_descendants() {
|
||||
wait_for_live_thread_spawn_children(&harness.control, child_thread_id, &[grandchild_thread_id])
|
||||
.await;
|
||||
|
||||
let _ = harness
|
||||
.control
|
||||
.close_agent(child_thread_id)
|
||||
.await
|
||||
.expect("child close should succeed");
|
||||
let _ = harness
|
||||
.control
|
||||
.shutdown_live_agent(parent_thread_id)
|
||||
.await
|
||||
.expect("parent shutdown should succeed");
|
||||
close_agent_and_wait(
|
||||
&harness.control,
|
||||
child_thread_id,
|
||||
&[child_thread_id, grandchild_thread_id],
|
||||
)
|
||||
.await;
|
||||
shutdown_live_agent_and_wait(&harness.control, parent_thread_id).await;
|
||||
drop(parent_thread);
|
||||
|
||||
let resumed_parent_thread_id = harness
|
||||
.control
|
||||
@@ -2180,11 +2233,12 @@ async fn resume_closed_child_reopens_open_descendants() {
|
||||
wait_for_live_thread_spawn_children(&harness.control, child_thread_id, &[grandchild_thread_id])
|
||||
.await;
|
||||
|
||||
let _ = harness
|
||||
.control
|
||||
.close_agent(child_thread_id)
|
||||
.await
|
||||
.expect("child close should succeed");
|
||||
close_agent_and_wait(
|
||||
&harness.control,
|
||||
child_thread_id,
|
||||
&[child_thread_id, grandchild_thread_id],
|
||||
)
|
||||
.await;
|
||||
|
||||
let resumed_child_thread_id = harness
|
||||
.control
|
||||
|
||||
@@ -35,7 +35,6 @@ use crate::exec_policy::ExecPolicyManager;
|
||||
use crate::parse_turn_item;
|
||||
use crate::path_utils::normalize_for_native_workdir;
|
||||
use crate::realtime_conversation::RealtimeConversationManager;
|
||||
use crate::rollout::find_thread_name_by_id;
|
||||
use crate::session_prefix::format_subagent_notification_message;
|
||||
use crate::skills::SkillRenderSideEffects;
|
||||
use crate::skills_load_input_from_config;
|
||||
@@ -133,6 +132,7 @@ use codex_thread_store::CreateThreadParams;
|
||||
use codex_thread_store::LiveThread;
|
||||
use codex_thread_store::LiveThreadInitGuard;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
use codex_thread_store::ReadThreadParams;
|
||||
use codex_thread_store::ResumeThreadParams;
|
||||
use codex_thread_store::ThreadEventPersistenceMode;
|
||||
use codex_thread_store::ThreadPersistenceMetadata;
|
||||
@@ -829,24 +829,33 @@ pub(crate) fn session_loop_termination_from_handle(
|
||||
.shared()
|
||||
}
|
||||
|
||||
async fn thread_title_from_state_db(
|
||||
state_db: Option<&state_db::StateDbHandle>,
|
||||
codex_home: &AbsolutePathBuf,
|
||||
async fn thread_title_from_thread_store(
|
||||
live_thread: Option<&LiveThread>,
|
||||
thread_store: &Arc<dyn ThreadStore>,
|
||||
conversation_id: ThreadId,
|
||||
) -> Option<String> {
|
||||
if let Some(metadata) = state_db
|
||||
&& let Some(metadata) = metadata.get_thread(conversation_id).await.ok().flatten()
|
||||
{
|
||||
let title = metadata.title.trim();
|
||||
if !title.is_empty() && metadata.first_user_message.as_deref().map(str::trim) != Some(title)
|
||||
{
|
||||
return Some(title.to_string());
|
||||
let thread = match live_thread {
|
||||
Some(live_thread) => {
|
||||
live_thread
|
||||
.read_thread(
|
||||
/*include_archived*/ true, /*include_history*/ false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
thread_store
|
||||
.read_thread(ReadThreadParams {
|
||||
thread_id: conversation_id,
|
||||
include_archived: true,
|
||||
include_history: false,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
find_thread_name_by_id(codex_home, &conversation_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok()?;
|
||||
|
||||
let title = thread.name.as_deref()?.trim();
|
||||
(!title.is_empty() && thread.preview.trim() != title).then(|| title.to_string())
|
||||
}
|
||||
|
||||
impl Session {
|
||||
|
||||
@@ -717,7 +717,7 @@ impl Session {
|
||||
tx
|
||||
};
|
||||
let thread_name =
|
||||
thread_title_from_state_db(state_db_ctx.as_ref(), &config.codex_home, thread_id)
|
||||
thread_title_from_thread_store(live_thread_init.as_ref(), &thread_store, thread_id)
|
||||
.instrument(info_span!(
|
||||
"session_init.thread_name_lookup",
|
||||
otel.name = "session_init.thread_name_lookup",
|
||||
|
||||
@@ -8908,7 +8908,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
let tool_name = "shell";
|
||||
let call_id = "test-call".to_string();
|
||||
|
||||
let handler = ShellHandler;
|
||||
let handler = ShellHandler::default();
|
||||
let resp = handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
@@ -8983,7 +8983,7 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request()
|
||||
let turn_context = Arc::new(turn_context_raw);
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
|
||||
let handler = ExecCommandHandler;
|
||||
let handler = ExecCommandHandler::default();
|
||||
let resp = handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
|
||||
@@ -323,7 +323,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
|
||||
arg0: None,
|
||||
};
|
||||
|
||||
let handler = ShellHandler;
|
||||
let handler = ShellHandler::default();
|
||||
let resp = handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
@@ -437,7 +437,7 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_policy_skip() {
|
||||
let session = Arc::new(session);
|
||||
let turn_context = Arc::new(turn_context_raw);
|
||||
|
||||
let handler = ShellHandler;
|
||||
let handler = ShellHandler::default();
|
||||
let command = if cfg!(windows) {
|
||||
vec![
|
||||
"cmd.exe".to_string(),
|
||||
@@ -498,7 +498,7 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic
|
||||
let turn_context = Arc::new(turn_context_raw);
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
|
||||
let handler = ExecCommandHandler;
|
||||
let handler = ExecCommandHandler::default();
|
||||
let resp = handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
@@ -615,7 +615,7 @@ async fn shell_handler_allows_sticky_turn_permissions_without_inline_request_per
|
||||
let session = Arc::new(session);
|
||||
let turn_context = Arc::new(turn_context_raw);
|
||||
|
||||
let handler = ShellHandler;
|
||||
let handler = ShellHandler::default();
|
||||
let resp = handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::environment_selection::default_thread_environment_selections;
|
||||
use crate::environment_selection::resolve_environment_selections;
|
||||
use crate::file_watcher::FileWatcher;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::rollout::RolloutRecorder;
|
||||
use crate::rollout::truncation;
|
||||
use crate::session::Codex;
|
||||
use crate::session::CodexSpawnArgs;
|
||||
@@ -41,6 +40,7 @@ use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::ResumedHistory;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionConfiguredEvent;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
@@ -55,6 +55,7 @@ use codex_state::DirectionalThreadSpawnEdgeStatus;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
use codex_thread_store::LocalThreadStoreConfig;
|
||||
use codex_thread_store::ReadThreadByRolloutPathParams;
|
||||
use codex_thread_store::ReadThreadParams;
|
||||
use codex_thread_store::RemoteThreadStore;
|
||||
use codex_thread_store::StoredThread;
|
||||
@@ -615,7 +616,7 @@ impl ThreadManager {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
|
||||
let initial_history = self.initial_history_from_rollout_path(rollout_path).await?;
|
||||
Box::pin(self.resume_thread_with_history(
|
||||
config,
|
||||
initial_history,
|
||||
@@ -687,7 +688,7 @@ impl ThreadManager {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
user_shell_override: crate::shell::Shell,
|
||||
) -> CodexResult<NewThread> {
|
||||
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
|
||||
let initial_history = self.initial_history_from_rollout_path(rollout_path).await?;
|
||||
let environments = default_thread_environment_selections(
|
||||
self.state.environment_manager.as_ref(),
|
||||
&config.cwd,
|
||||
@@ -784,7 +785,7 @@ impl ThreadManager {
|
||||
S: Into<ForkSnapshot>,
|
||||
{
|
||||
let snapshot = snapshot.into();
|
||||
let history = RolloutRecorder::get_rollout_history(&path).await?;
|
||||
let history = self.initial_history_from_rollout_path(path).await?;
|
||||
self.fork_thread_from_history(
|
||||
snapshot,
|
||||
config,
|
||||
@@ -796,6 +797,24 @@ impl ThreadManager {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn initial_history_from_rollout_path(
|
||||
&self,
|
||||
rollout_path: PathBuf,
|
||||
) -> CodexResult<InitialHistory> {
|
||||
let requested_rollout_path = rollout_path.clone();
|
||||
let stored_thread = self
|
||||
.state
|
||||
.thread_store
|
||||
.read_thread_by_rollout_path(ReadThreadByRolloutPathParams {
|
||||
rollout_path,
|
||||
include_archived: true,
|
||||
include_history: true,
|
||||
})
|
||||
.await
|
||||
.map_err(thread_store_rollout_read_error)?;
|
||||
stored_thread_to_initial_history(stored_thread, Some(requested_rollout_path))
|
||||
}
|
||||
|
||||
/// Fork an existing thread from already-loaded store history.
|
||||
pub async fn fork_thread_from_history<S>(
|
||||
&self,
|
||||
@@ -1280,6 +1299,31 @@ impl ThreadManagerState {
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_thread_to_initial_history(
|
||||
stored_thread: StoredThread,
|
||||
rollout_path: Option<PathBuf>,
|
||||
) -> CodexResult<InitialHistory> {
|
||||
let thread_id = stored_thread.thread_id;
|
||||
let history = stored_thread.history.ok_or_else(|| {
|
||||
CodexErr::Fatal(format!(
|
||||
"thread {thread_id} did not include persisted history"
|
||||
))
|
||||
})?;
|
||||
Ok(InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: thread_id,
|
||||
history: history.items,
|
||||
rollout_path: rollout_path.or(stored_thread.rollout_path),
|
||||
}))
|
||||
}
|
||||
|
||||
fn thread_store_rollout_read_error(err: ThreadStoreError) -> CodexErr {
|
||||
match err {
|
||||
ThreadStoreError::ThreadNotFound { thread_id } => CodexErr::ThreadNotFound(thread_id),
|
||||
ThreadStoreError::InvalidRequest { message } => CodexErr::InvalidRequest(message),
|
||||
err => CodexErr::Fatal(format!("failed to read thread by rollout path: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a fork snapshot cut strictly before the nth user message (0-based).
|
||||
///
|
||||
/// Out-of-range values keep the full committed history at a turn boundary, but
|
||||
|
||||
@@ -16,6 +16,7 @@ use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::protocol::AgentMessageEvent;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::InternalSessionSource;
|
||||
use codex_protocol::protocol::ResumedHistory;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
@@ -730,6 +731,111 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
|
||||
.expect("shutdown resumed thread");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
config.cwd = config.codex_home.abs();
|
||||
config.experimental_thread_store = ThreadStoreConfig::InMemory {
|
||||
id: format!("thread-manager-{}", uuid::Uuid::new_v4()),
|
||||
};
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
|
||||
let state_db = init_state_db(&config).await;
|
||||
let thread_store = thread_store_from_config(&config, state_db.clone());
|
||||
let in_memory_store = thread_store
|
||||
.as_any()
|
||||
.downcast_ref::<InMemoryThreadStore>()
|
||||
.expect("configured in-memory store");
|
||||
let manager = ThreadManager::new(
|
||||
&config,
|
||||
auth_manager.clone(),
|
||||
SessionSource::Exec,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store.clone(),
|
||||
state_db,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
);
|
||||
|
||||
let source = manager
|
||||
.start_thread(config.clone())
|
||||
.await
|
||||
.expect("start source thread");
|
||||
source
|
||||
.thread
|
||||
.shutdown_and_wait()
|
||||
.await
|
||||
.expect("shutdown source thread");
|
||||
let _ = manager.remove_thread(&source.thread_id).await;
|
||||
|
||||
let rollout_path = config
|
||||
.codex_home
|
||||
.join("rollouts/source.jsonl")
|
||||
.to_path_buf();
|
||||
let resumed = manager
|
||||
.resume_thread_with_history(
|
||||
config.clone(),
|
||||
InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: source.thread_id,
|
||||
history: vec![RolloutItem::ResponseItem(user_msg("hello"))],
|
||||
rollout_path: Some(rollout_path.clone()),
|
||||
}),
|
||||
auth_manager.clone(),
|
||||
/*persist_extended_history*/ false,
|
||||
/*parent_trace*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("seed rollout path in store");
|
||||
resumed
|
||||
.thread
|
||||
.shutdown_and_wait()
|
||||
.await
|
||||
.expect("shutdown seeded resumed thread");
|
||||
let _ = manager.remove_thread(&resumed.thread_id).await;
|
||||
|
||||
let resumed_from_path = manager
|
||||
.resume_thread_from_rollout(
|
||||
config.clone(),
|
||||
rollout_path.clone(),
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("resume from rollout path");
|
||||
assert_eq!(resumed_from_path.thread_id, resumed.thread_id);
|
||||
|
||||
let forked = manager
|
||||
.fork_thread(
|
||||
ForkSnapshot::Interrupted,
|
||||
config,
|
||||
rollout_path,
|
||||
/*thread_source*/ None,
|
||||
/*persist_extended_history*/ false,
|
||||
/*parent_trace*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("fork from rollout path");
|
||||
assert_ne!(forked.thread_id, resumed.thread_id);
|
||||
|
||||
let calls = in_memory_store.calls().await;
|
||||
assert_eq!(calls.read_thread_by_rollout_path, 2);
|
||||
|
||||
resumed_from_path
|
||||
.thread
|
||||
.shutdown_and_wait()
|
||||
.await
|
||||
.expect("shutdown path-resumed thread");
|
||||
forked
|
||||
.thread
|
||||
.shutdown_and_wait()
|
||||
.await
|
||||
.expect("shutdown forked thread");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn new_uses_active_provider_for_model_refresh() {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::tools::context::ToolPayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use super::ExecContext;
|
||||
use super::PUBLIC_TOOL_NAME;
|
||||
@@ -12,9 +13,15 @@ use super::build_enabled_tools;
|
||||
use super::handle_runtime_response;
|
||||
use super::is_exec_tool_name;
|
||||
|
||||
pub struct CodeModeExecuteHandler;
|
||||
pub struct CodeModeExecuteHandler {
|
||||
spec: ToolSpec,
|
||||
}
|
||||
|
||||
impl CodeModeExecuteHandler {
|
||||
pub(crate) fn new(spec: ToolSpec) -> Self {
|
||||
Self { spec }
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
session: std::sync::Arc<crate::session::session::Session>,
|
||||
@@ -83,6 +90,10 @@ impl ToolHandler for CodeModeExecuteHandler {
|
||||
ToolName::plain(PUBLIC_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(self.spec.clone())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ use crate::tools::context::ToolPayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use super::DEFAULT_WAIT_YIELD_TIME_MS;
|
||||
use super::ExecContext;
|
||||
use super::WAIT_TOOL_NAME;
|
||||
use super::handle_runtime_response;
|
||||
use super::wait_spec::create_wait_tool;
|
||||
|
||||
pub struct CodeModeWaitHandler;
|
||||
|
||||
@@ -46,6 +48,10 @@ impl ToolHandler for CodeModeWaitHandler {
|
||||
ToolName::plain(WAIT_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_wait_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -196,6 +196,12 @@ async fn run_agent_job_loop(
|
||||
)
|
||||
.await?;
|
||||
for item in pending_items {
|
||||
let claimed = db
|
||||
.mark_agent_job_item_running(job_id.as_str(), item.item_id.as_str())
|
||||
.await?;
|
||||
if !claimed {
|
||||
continue;
|
||||
}
|
||||
let prompt = build_worker_prompt(&job, &item)?;
|
||||
let items = vec![UserInput::Text {
|
||||
text: prompt,
|
||||
@@ -240,7 +246,7 @@ async fn run_agent_job_loop(
|
||||
}
|
||||
};
|
||||
let assigned = db
|
||||
.mark_agent_job_item_running_with_thread(
|
||||
.set_agent_job_item_thread(
|
||||
job_id.as_str(),
|
||||
item.item_id.as_str(),
|
||||
thread_id.to_string().as_str(),
|
||||
|
||||
@@ -2,9 +2,11 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::agent_jobs_spec::create_report_agent_job_result_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -17,6 +19,10 @@ impl ToolHandler for ReportAgentJobResultHandler {
|
||||
ToolName::plain("report_agent_job_result")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_report_agent_job_result_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
@@ -55,27 +61,31 @@ pub async fn handle(
|
||||
}
|
||||
let db = required_state_db(&session)?;
|
||||
let reporting_thread_id = session.conversation_id.to_string();
|
||||
let accepted = db
|
||||
.report_agent_job_item_result(
|
||||
let accepted = if args.stop.unwrap_or(false) {
|
||||
db.report_agent_job_item_result_and_cancel_job(
|
||||
args.job_id.as_str(),
|
||||
args.item_id.as_str(),
|
||||
reporting_thread_id.as_str(),
|
||||
&args.result,
|
||||
"cancelled by worker request",
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
db.report_agent_job_item_result(
|
||||
args.job_id.as_str(),
|
||||
args.item_id.as_str(),
|
||||
reporting_thread_id.as_str(),
|
||||
&args.result,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let job_id = args.job_id.as_str();
|
||||
let item_id = args.item_id.as_str();
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"failed to record agent job result for {job_id} / {item_id}: {err}"
|
||||
))
|
||||
})?;
|
||||
if accepted && args.stop.unwrap_or(false) {
|
||||
let message = "cancelled by worker request";
|
||||
let _ = db
|
||||
.mark_agent_job_cancelled(args.job_id.as_str(), message)
|
||||
.await;
|
||||
}
|
||||
.map_err(|err| {
|
||||
let job_id = args.job_id.as_str();
|
||||
let item_id = args.item_id.as_str();
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"failed to record agent job result for {job_id} / {item_id}: {err}"
|
||||
))
|
||||
})?;
|
||||
let content =
|
||||
serde_json::to_string(&ReportAgentJobResultToolResult { accepted }).map_err(|err| {
|
||||
FunctionCallError::Fatal(format!(
|
||||
|
||||
@@ -2,9 +2,11 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::agent_jobs_spec::create_spawn_agents_on_csv_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -17,6 +19,10 @@ impl ToolHandler for SpawnAgentsOnCsvHandler {
|
||||
ToolName::plain("spawn_agents_on_csv")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_spawn_agents_on_csv_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ use crate::tools::events::ToolEmitter;
|
||||
use crate::tools::events::ToolEventCtx;
|
||||
use crate::tools::handlers::apply_granted_turn_permissions;
|
||||
use crate::tools::handlers::apply_patch_spec::ApplyPatchToolArgs;
|
||||
use crate::tools::handlers::apply_patch_spec::create_apply_patch_freeform_tool;
|
||||
use crate::tools::handlers::apply_patch_spec::create_apply_patch_json_tool;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::orchestrator::ToolOrchestrator;
|
||||
@@ -41,6 +43,7 @@ use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::openai_models::ApplyPatchToolType;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::PatchApplyUpdatedEvent;
|
||||
@@ -48,11 +51,30 @@ use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
|
||||
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
||||
use codex_sandboxing::policy_transforms::normalize_additional_permissions;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
const APPLY_PATCH_ARGUMENT_DIFF_BUFFER_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
pub struct ApplyPatchHandler;
|
||||
pub struct ApplyPatchHandler {
|
||||
options: ApplyPatchToolType,
|
||||
}
|
||||
|
||||
impl Default for ApplyPatchHandler {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
options: ApplyPatchToolType::Freeform,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplyPatchHandler {
|
||||
pub(crate) fn new(apply_patch_tool_type: ApplyPatchToolType) -> Self {
|
||||
Self {
|
||||
options: apply_patch_tool_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ApplyPatchArgumentDiffConsumer {
|
||||
@@ -297,6 +319,13 @@ impl ToolHandler for ApplyPatchHandler {
|
||||
ToolName::plain("apply_patch")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(match self.options {
|
||||
ApplyPatchToolType::Freeform => create_apply_patch_freeform_tool(),
|
||||
ApplyPatchToolType::Function => create_apply_patch_json_tool(),
|
||||
})
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ async fn pre_tool_use_payload_uses_json_patch_input() {
|
||||
arguments: json!({ "input": patch }).to_string(),
|
||||
};
|
||||
let invocation = invocation_for_payload(payload).await;
|
||||
let handler = ApplyPatchHandler;
|
||||
let handler = ApplyPatchHandler::default();
|
||||
|
||||
assert_eq!(
|
||||
handler.pre_tool_use_payload(&invocation),
|
||||
@@ -67,7 +67,7 @@ async fn pre_tool_use_payload_uses_freeform_patch_input() {
|
||||
input: patch.to_string(),
|
||||
};
|
||||
let invocation = invocation_for_payload(payload).await;
|
||||
let handler = ApplyPatchHandler;
|
||||
let handler = ApplyPatchHandler::default();
|
||||
|
||||
assert_eq!(
|
||||
handler.pre_tool_use_payload(&invocation),
|
||||
@@ -86,7 +86,7 @@ async fn post_tool_use_payload_uses_patch_input_and_tool_output() {
|
||||
};
|
||||
let invocation = invocation_for_payload(payload).await;
|
||||
let output = ApplyPatchToolOutput::from_text("Success. Updated files.".to_string());
|
||||
let handler = ApplyPatchHandler;
|
||||
let handler = ApplyPatchHandler::default();
|
||||
|
||||
assert_eq!(
|
||||
handler.post_tool_use_payload(&invocation, &output),
|
||||
|
||||
@@ -4,10 +4,12 @@ use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::goal_spec::CREATE_GOAL_TOOL_NAME;
|
||||
use crate::tools::handlers::goal_spec::create_create_goal_tool;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use super::CompletionBudgetReport;
|
||||
use super::CreateGoalArgs;
|
||||
@@ -23,6 +25,10 @@ impl ToolHandler for CreateGoalHandler {
|
||||
ToolName::plain(CREATE_GOAL_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_create_goal_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::goal_spec::GET_GOAL_TOOL_NAME;
|
||||
use crate::tools::handlers::goal_spec::create_get_goal_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use super::CompletionBudgetReport;
|
||||
use super::format_goal_error;
|
||||
@@ -20,6 +22,10 @@ impl ToolHandler for GetGoalHandler {
|
||||
ToolName::plain(GET_GOAL_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_get_goal_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::goal_spec::UPDATE_GOAL_TOOL_NAME;
|
||||
use crate::tools::handlers::goal_spec::create_update_goal_tool;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::protocol::ThreadGoalStatus;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use super::CompletionBudgetReport;
|
||||
use super::UpdateGoalArgs;
|
||||
@@ -25,6 +27,10 @@ impl ToolHandler for UpdateGoalHandler {
|
||||
ToolName::plain(UPDATE_GOAL_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_update_goal_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resource_templates_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::models::function_call_output_content_items_to_text;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use rmcp::model::PaginatedRequestParams;
|
||||
|
||||
@@ -31,6 +33,14 @@ impl ToolHandler for ListMcpResourceTemplatesHandler {
|
||||
ToolName::plain("list_mcp_resource_templates")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_list_mcp_resource_templates_tool())
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resources_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::models::function_call_output_content_items_to_text;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use rmcp::model::PaginatedRequestParams;
|
||||
|
||||
@@ -31,6 +33,14 @@ impl ToolHandler for ListMcpResourcesHandler {
|
||||
ToolName::plain("list_mcp_resources")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_list_mcp_resources_tool())
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::mcp_resource_spec::create_read_mcp_resource_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::models::function_call_output_content_items_to_text;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use rmcp::model::ReadResourceRequestParams;
|
||||
|
||||
@@ -31,6 +33,14 @@ impl ToolHandler for ReadMcpResourceHandler {
|
||||
ToolName::plain("read_mcp_resource")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_read_mcp_resource_tool())
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -64,12 +64,14 @@ pub use request_user_input::RequestUserInputHandler;
|
||||
pub use shell::ContainerExecHandler;
|
||||
pub use shell::LocalShellHandler;
|
||||
pub use shell::ShellCommandHandler;
|
||||
pub(crate) use shell::ShellCommandHandlerOptions;
|
||||
pub use shell::ShellHandler;
|
||||
pub use test_sync::TestSyncHandler;
|
||||
pub use tool_search::ToolSearchHandler;
|
||||
pub use unavailable_tool::UnavailableToolHandler;
|
||||
pub(crate) use unavailable_tool::unavailable_tool_message;
|
||||
pub use unified_exec::ExecCommandHandler;
|
||||
pub(crate) use unified_exec::ExecCommandHandlerOptions;
|
||||
pub use unified_exec::WriteStdinHandler;
|
||||
pub use view_image::ViewImageHandler;
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v1;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
|
||||
@@ -10,6 +12,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("close_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_close_agent_tool_v1())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::*;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
use codex_tools::ToolSpec;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
@@ -12,6 +14,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("resume_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_resume_agent_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::*;
|
||||
use crate::agent::control::render_input_preview;
|
||||
use crate::tools::handlers::multi_agents_spec::create_send_input_tool_v1;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
|
||||
@@ -11,6 +13,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("send_input")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_send_input_tool_v1())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -6,9 +6,21 @@ use crate::agent::exceeds_thread_spawn_depth_limit;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use crate::agent::role::DEFAULT_ROLE_NAME;
|
||||
use crate::agent::role::apply_role_to_config;
|
||||
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
|
||||
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v1;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Handler {
|
||||
options: SpawnAgentToolOptions,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
pub(crate) fn new(options: SpawnAgentToolOptions) -> Self {
|
||||
Self { options }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for Handler {
|
||||
type Output = SpawnAgentResult;
|
||||
@@ -17,6 +29,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("spawn_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_spawn_agent_tool_v1(self.options.clone()))
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use super::*;
|
||||
use crate::agent::status::is_final;
|
||||
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
|
||||
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_tools::ToolSpec;
|
||||
use futures::FutureExt;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::FuturesUnordered;
|
||||
@@ -13,7 +16,16 @@ use tokio::time::Instant;
|
||||
|
||||
use tokio::time::timeout_at;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Handler {
|
||||
options: WaitAgentTimeoutOptions,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
pub(crate) fn new(options: WaitAgentTimeoutOptions) -> Self {
|
||||
Self { options }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for Handler {
|
||||
type Output = WaitAgentResult;
|
||||
@@ -22,6 +34,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("wait_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_wait_agent_tool_v1(self.options))
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ use std::collections::BTreeMap;
|
||||
const SPAWN_AGENT_INHERITED_MODEL_GUIDANCE: &str = "Spawned agents inherit your current model by default. Omit `model` to use that preferred default; set `model` only when an explicit override is needed.";
|
||||
const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str = "Optional model override for the new agent. Leave unset to inherit the same model as the parent, which is the preferred default. Only set this when the user explicitly asks for a different model or the task clearly requires one.";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpawnAgentToolOptions<'a> {
|
||||
pub available_models: &'a [ModelPreset],
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SpawnAgentToolOptions {
|
||||
pub available_models: Vec<ModelPreset>,
|
||||
pub agent_type_description: String,
|
||||
pub hide_agent_type_model_reasoning: bool,
|
||||
pub include_usage_hint: bool,
|
||||
@@ -26,9 +26,19 @@ pub struct WaitAgentTimeoutOptions {
|
||||
pub max_timeout_ms: i64,
|
||||
}
|
||||
|
||||
pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions<'_>) -> ToolSpec {
|
||||
impl Default for WaitAgentTimeoutOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_timeout_ms: super::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS,
|
||||
min_timeout_ms: super::multi_agents_common::MIN_WAIT_TIMEOUT_MS,
|
||||
max_timeout_ms: super::multi_agents_common::MAX_WAIT_TIMEOUT_MS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec {
|
||||
let available_models_description = (!options.hide_agent_type_model_reasoning)
|
||||
.then(|| spawn_agent_models_description(options.available_models));
|
||||
.then(|| spawn_agent_models_description(&options.available_models));
|
||||
let return_value_description =
|
||||
"Returns the spawned agent id plus the user-facing nickname when available.";
|
||||
let mut properties = spawn_agent_common_properties_v1(&options.agent_type_description);
|
||||
@@ -51,9 +61,9 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions<'_>) -> ToolSpe
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions<'_>) -> ToolSpec {
|
||||
pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions) -> ToolSpec {
|
||||
let available_models_description = (!options.hide_agent_type_model_reasoning)
|
||||
.then(|| spawn_agent_models_description(options.available_models));
|
||||
.then(|| spawn_agent_models_description(&options.available_models));
|
||||
let mut properties = spawn_agent_common_properties_v2(&options.agent_type_description);
|
||||
if options.hide_agent_type_model_reasoning {
|
||||
hide_spawn_agent_metadata_options(&mut properties);
|
||||
|
||||
@@ -33,7 +33,7 @@ fn model_preset(id: &str, show_in_picker: bool) -> ModelPreset {
|
||||
#[test]
|
||||
fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {
|
||||
available_models: &[
|
||||
available_models: vec![
|
||||
model_preset("visible", /*show_in_picker*/ true),
|
||||
model_preset("hidden", /*show_in_picker*/ false),
|
||||
],
|
||||
@@ -99,7 +99,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
#[test]
|
||||
fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
|
||||
let tool = create_spawn_agent_tool_v1(SpawnAgentToolOptions {
|
||||
available_models: &[],
|
||||
available_models: Vec::new(),
|
||||
agent_type_description: "role help".to_string(),
|
||||
hide_agent_type_model_reasoning: false,
|
||||
include_usage_hint: true,
|
||||
|
||||
@@ -180,7 +180,7 @@ async fn handler_rejects_non_function_payloads() {
|
||||
input: "hello".to_string(),
|
||||
},
|
||||
);
|
||||
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
|
||||
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
|
||||
panic!("payload should be rejected");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -200,7 +200,7 @@ async fn spawn_agent_rejects_empty_message() {
|
||||
"spawn_agent",
|
||||
function_payload(json!({"message": " "})),
|
||||
);
|
||||
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
|
||||
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
|
||||
panic!("empty message should be rejected");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -221,7 +221,7 @@ async fn spawn_agent_rejects_when_message_and_items_are_both_set() {
|
||||
"items": [{"type": "mention", "name": "drive", "path": "app://drive"}]
|
||||
})),
|
||||
);
|
||||
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
|
||||
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
|
||||
panic!("message+items should be rejected");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -268,7 +268,7 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
|
||||
"agent_type": "explorer"
|
||||
})),
|
||||
);
|
||||
let output = SpawnAgentHandler
|
||||
let output = SpawnAgentHandler::default()
|
||||
.handle(invocation)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
@@ -303,7 +303,7 @@ async fn spawn_agent_fork_context_rejects_agent_type_override() {
|
||||
.expect("root thread should start");
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let err = SpawnAgentHandler
|
||||
let err = SpawnAgentHandler::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -336,7 +336,7 @@ async fn spawn_agent_fork_context_rejects_child_model_overrides() {
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
|
||||
let err = SpawnAgentHandler
|
||||
let err = SpawnAgentHandler::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -380,7 +380,7 @@ async fn multi_agent_v2_spawn_fork_turns_all_rejects_agent_type_override() {
|
||||
..turn
|
||||
};
|
||||
|
||||
let err = SpawnAgentHandlerV2
|
||||
let err = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -420,7 +420,7 @@ async fn multi_agent_v2_spawn_defaults_to_full_fork_and_rejects_child_model_over
|
||||
.expect("test config should allow feature update");
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let err = SpawnAgentHandlerV2
|
||||
let err = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -464,7 +464,7 @@ async fn multi_agent_v2_spawn_partial_fork_turns_allows_agent_type_override() {
|
||||
..turn
|
||||
};
|
||||
|
||||
let output = SpawnAgentHandlerV2
|
||||
let output = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -506,7 +506,7 @@ async fn spawn_agent_returns_agent_id_without_task_name() {
|
||||
let manager = thread_manager();
|
||||
session.services.agent_control = manager.agent_control();
|
||||
|
||||
let output = SpawnAgentHandler
|
||||
let output = SpawnAgentHandler::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -552,7 +552,7 @@ async fn multi_agent_v2_spawn_requires_task_name() {
|
||||
"message": "inspect this repo"
|
||||
})),
|
||||
);
|
||||
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
|
||||
let Err(err) = SpawnAgentHandlerV2::default().handle(invocation).await else {
|
||||
panic!("missing task_name should be rejected");
|
||||
};
|
||||
let FunctionCallError::RespondToModel(message) = err else {
|
||||
@@ -588,7 +588,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_items_field() {
|
||||
"task_name": "worker"
|
||||
})),
|
||||
);
|
||||
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
|
||||
let Err(err) = SpawnAgentHandlerV2::default().handle(invocation).await else {
|
||||
panic!("legacy items field should be rejected");
|
||||
};
|
||||
let FunctionCallError::RespondToModel(message) = err else {
|
||||
@@ -606,7 +606,7 @@ async fn spawn_agent_errors_when_manager_dropped() {
|
||||
"spawn_agent",
|
||||
function_payload(json!({"message": "hello"})),
|
||||
);
|
||||
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
|
||||
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
|
||||
panic!("spawn should fail without a manager");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -640,7 +640,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
let spawn_output = SpawnAgentHandlerV2
|
||||
let spawn_output = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -735,7 +735,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_fork_context() {
|
||||
.expect("test config should allow feature update");
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let err = SpawnAgentHandlerV2
|
||||
let err = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -774,7 +774,7 @@ async fn multi_agent_v2_spawn_rejects_invalid_fork_turns_string() {
|
||||
.expect("test config should allow feature update");
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let err = SpawnAgentHandlerV2
|
||||
let err = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -813,7 +813,7 @@ async fn multi_agent_v2_spawn_rejects_zero_fork_turns() {
|
||||
.expect("test config should allow feature update");
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let err = SpawnAgentHandlerV2
|
||||
let err = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -1008,7 +1008,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
let spawn_output = SpawnAgentHandlerV2
|
||||
let spawn_output = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -1189,7 +1189,7 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() {
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
let spawn_output = SpawnAgentHandlerV2
|
||||
let spawn_output = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -1253,7 +1253,7 @@ async fn multi_agent_v2_send_message_rejects_legacy_items_field() {
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
SpawnAgentHandlerV2
|
||||
SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -1309,7 +1309,7 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
SpawnAgentHandlerV2
|
||||
SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -1382,7 +1382,7 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
SpawnAgentHandlerV2
|
||||
SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -1517,7 +1517,7 @@ async fn multi_agent_v2_followup_task_rejects_legacy_items_field() {
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
SpawnAgentHandlerV2
|
||||
SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -1570,7 +1570,7 @@ async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() {
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
SpawnAgentHandlerV2
|
||||
SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -1648,7 +1648,7 @@ async fn multi_agent_v2_spawn_omits_agent_id_when_named() {
|
||||
.expect("test config should allow feature update");
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let output = SpawnAgentHandlerV2
|
||||
let output = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -1696,7 +1696,7 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() {
|
||||
"task_name": "BadName"
|
||||
})),
|
||||
);
|
||||
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
|
||||
let Err(err) = SpawnAgentHandlerV2::default().handle(invocation).await else {
|
||||
panic!("invalid agent name should be rejected");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -1754,7 +1754,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
|
||||
"agent_type": "explorer"
|
||||
})),
|
||||
);
|
||||
let output = SpawnAgentHandler
|
||||
let output = SpawnAgentHandler::default()
|
||||
.handle(invocation)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
@@ -1815,7 +1815,7 @@ async fn spawn_agent_rejects_when_depth_limit_exceeded() {
|
||||
"spawn_agent",
|
||||
function_payload(json!({"message": "hello"})),
|
||||
);
|
||||
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
|
||||
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
|
||||
panic!("spawn should fail when depth limit exceeded");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -1855,7 +1855,7 @@ async fn spawn_agent_allows_depth_up_to_configured_max_depth() {
|
||||
"spawn_agent",
|
||||
function_payload(json!({"message": "hello"})),
|
||||
);
|
||||
let output = SpawnAgentHandler
|
||||
let output = SpawnAgentHandler::default()
|
||||
.handle(invocation)
|
||||
.await
|
||||
.expect("spawn should succeed within configured depth");
|
||||
@@ -1914,7 +1914,7 @@ async fn multi_agent_v2_spawn_agent_ignores_configured_max_depth() {
|
||||
"fork_turns": "none"
|
||||
})),
|
||||
);
|
||||
let output = SpawnAgentHandlerV2
|
||||
let output = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation)
|
||||
.await
|
||||
.expect("multi-agent v2 spawn should ignore max depth");
|
||||
@@ -2306,7 +2306,7 @@ async fn wait_agent_rejects_non_positive_timeout() {
|
||||
"timeout_ms": 0
|
||||
})),
|
||||
);
|
||||
let Err(err) = WaitAgentHandler.handle(invocation).await else {
|
||||
let Err(err) = WaitAgentHandler::default().handle(invocation).await else {
|
||||
panic!("non-positive timeout should be rejected");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -2324,7 +2324,7 @@ async fn wait_agent_rejects_invalid_target() {
|
||||
"wait_agent",
|
||||
function_payload(json!({"targets": ["invalid"]})),
|
||||
);
|
||||
let Err(err) = WaitAgentHandler.handle(invocation).await else {
|
||||
let Err(err) = WaitAgentHandler::default().handle(invocation).await else {
|
||||
panic!("invalid id should be rejected");
|
||||
};
|
||||
let FunctionCallError::RespondToModel(msg) = err else {
|
||||
@@ -2342,7 +2342,7 @@ async fn wait_agent_rejects_empty_targets() {
|
||||
"wait_agent",
|
||||
function_payload(json!({"targets": []})),
|
||||
);
|
||||
let Err(err) = WaitAgentHandler.handle(invocation).await else {
|
||||
let Err(err) = WaitAgentHandler::default().handle(invocation).await else {
|
||||
panic!("empty ids should be rejected");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -2370,7 +2370,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() {
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
SpawnAgentHandlerV2
|
||||
SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -2400,7 +2400,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() {
|
||||
let session = session.clone();
|
||||
let turn = turn.clone();
|
||||
async move {
|
||||
WaitAgentHandlerV2
|
||||
WaitAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session,
|
||||
turn,
|
||||
@@ -2452,7 +2452,7 @@ async fn multi_agent_v2_wait_agent_uses_configured_min_timeout() {
|
||||
|
||||
let early = timeout(
|
||||
Duration::from_millis(/*millis*/ 20),
|
||||
WaitAgentHandlerV2.handle(invocation(
|
||||
WaitAgentHandlerV2::default().handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
"wait_agent",
|
||||
@@ -2467,7 +2467,7 @@ async fn multi_agent_v2_wait_agent_uses_configured_min_timeout() {
|
||||
|
||||
let output = timeout(
|
||||
Duration::from_secs(/*secs*/ 1),
|
||||
WaitAgentHandlerV2.handle(invocation(
|
||||
WaitAgentHandlerV2::default().handle(invocation(
|
||||
session,
|
||||
turn,
|
||||
"wait_agent",
|
||||
@@ -2506,7 +2506,7 @@ async fn wait_agent_returns_not_found_for_missing_agents() {
|
||||
"timeout_ms": 1000
|
||||
})),
|
||||
);
|
||||
let output = WaitAgentHandler
|
||||
let output = WaitAgentHandler::default()
|
||||
.handle(invocation)
|
||||
.await
|
||||
.expect("wait_agent should succeed");
|
||||
@@ -2546,7 +2546,7 @@ async fn wait_agent_times_out_when_status_is_not_final() {
|
||||
"timeout_ms": MIN_WAIT_TIMEOUT_MS
|
||||
})),
|
||||
);
|
||||
let output = WaitAgentHandler
|
||||
let output = WaitAgentHandler::default()
|
||||
.handle(invocation)
|
||||
.await
|
||||
.expect("wait_agent should succeed");
|
||||
@@ -2592,7 +2592,7 @@ async fn wait_agent_clamps_short_timeouts_to_minimum() {
|
||||
|
||||
let early = timeout(
|
||||
Duration::from_millis(50),
|
||||
WaitAgentHandler.handle(invocation),
|
||||
WaitAgentHandler::default().handle(invocation),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
@@ -2642,7 +2642,7 @@ async fn wait_agent_returns_final_status_without_timeout() {
|
||||
"timeout_ms": 1000
|
||||
})),
|
||||
);
|
||||
let output = WaitAgentHandler
|
||||
let output = WaitAgentHandler::default()
|
||||
.handle(invocation)
|
||||
.await
|
||||
.expect("wait_agent should succeed");
|
||||
@@ -2678,7 +2678,7 @@ async fn multi_agent_v2_wait_agent_returns_summary_for_mailbox_activity() {
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
let spawn_output = SpawnAgentHandlerV2
|
||||
let spawn_output = SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -2713,7 +2713,7 @@ async fn multi_agent_v2_wait_agent_returns_summary_for_mailbox_activity() {
|
||||
let session = session.clone();
|
||||
let turn = turn.clone();
|
||||
async move {
|
||||
WaitAgentHandlerV2
|
||||
WaitAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session,
|
||||
turn,
|
||||
@@ -2769,7 +2769,7 @@ async fn multi_agent_v2_wait_agent_returns_for_already_queued_mail() {
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
SpawnAgentHandlerV2
|
||||
SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -2805,7 +2805,7 @@ async fn multi_agent_v2_wait_agent_returns_for_already_queued_mail() {
|
||||
|
||||
let output = timeout(
|
||||
Duration::from_millis(500),
|
||||
WaitAgentHandlerV2.handle(invocation(
|
||||
WaitAgentHandlerV2::default().handle(invocation(
|
||||
session,
|
||||
turn,
|
||||
"wait_agent",
|
||||
@@ -2848,7 +2848,7 @@ async fn multi_agent_v2_wait_agent_wakes_on_any_mailbox_notification() {
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
for task_name in ["worker_a", "worker_b"] {
|
||||
SpawnAgentHandlerV2
|
||||
SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -2879,7 +2879,7 @@ async fn multi_agent_v2_wait_agent_wakes_on_any_mailbox_notification() {
|
||||
let session = session.clone();
|
||||
let turn = turn.clone();
|
||||
async move {
|
||||
WaitAgentHandlerV2
|
||||
WaitAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session,
|
||||
turn,
|
||||
@@ -2935,7 +2935,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() {
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
SpawnAgentHandlerV2
|
||||
SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -2964,7 +2964,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() {
|
||||
let session = session.clone();
|
||||
let turn = turn.clone();
|
||||
async move {
|
||||
WaitAgentHandlerV2
|
||||
WaitAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session,
|
||||
turn,
|
||||
@@ -3021,7 +3021,7 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() {
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
SpawnAgentHandlerV2
|
||||
SpawnAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
@@ -3176,7 +3176,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
||||
let parent_thread_id = parent.thread_id;
|
||||
let parent_session = parent.thread.codex.session.clone();
|
||||
|
||||
let child_spawn_output = SpawnAgentHandler
|
||||
let child_spawn_output = SpawnAgentHandler::default()
|
||||
.handle(invocation(
|
||||
parent_session.clone(),
|
||||
parent_session.new_default_turn().await,
|
||||
@@ -3201,7 +3201,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
||||
.await
|
||||
.expect("child thread should exist");
|
||||
let child_session = child_thread.codex.session.clone();
|
||||
let grandchild_spawn_output = SpawnAgentHandler
|
||||
let grandchild_spawn_output = SpawnAgentHandler::default()
|
||||
.handle(invocation(
|
||||
child_session.clone(),
|
||||
child_session.new_default_turn().await,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v2;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
|
||||
@@ -10,6 +12,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("close_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_close_agent_tool_v2())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ use super::message_tool::MessageDeliveryMode;
|
||||
use super::message_tool::handle_message_string_tool;
|
||||
use super::*;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::handlers::multi_agents_spec::create_followup_task_tool;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
|
||||
@@ -13,6 +15,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("followup_task")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_followup_task_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use crate::agent::control::ListedAgent;
|
||||
use crate::tools::handlers::multi_agents_spec::create_list_agents_tool;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
|
||||
@@ -10,6 +12,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("list_agents")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_list_agents_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ use super::message_tool::SendMessageArgs;
|
||||
use super::message_tool::handle_message_string_tool;
|
||||
use super::*;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::handlers::multi_agents_spec::create_send_message_tool;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
|
||||
@@ -13,6 +15,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("send_message")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_send_message_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -5,12 +5,24 @@ use crate::agent::control::render_input_preview;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use crate::agent::role::DEFAULT_ROLE_NAME;
|
||||
use crate::agent::role::apply_role_to_config;
|
||||
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
|
||||
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Handler {
|
||||
options: SpawnAgentToolOptions,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
pub(crate) fn new(options: SpawnAgentToolOptions) -> Self {
|
||||
Self { options }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for Handler {
|
||||
type Output = SpawnAgentResult;
|
||||
@@ -19,6 +31,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("spawn_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_spawn_agent_tool_v2(self.options.clone()))
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
use super::*;
|
||||
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
|
||||
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
use codex_tools::ToolSpec;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
use tokio::time::timeout_at;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Handler {
|
||||
options: WaitAgentTimeoutOptions,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
pub(crate) fn new(options: WaitAgentTimeoutOptions) -> Self {
|
||||
Self { options }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for Handler {
|
||||
type Output = WaitAgentResult;
|
||||
@@ -14,6 +26,10 @@ impl ToolHandler for Handler {
|
||||
ToolName::plain("wait_agent")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_wait_agent_tool_v2(self.options))
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::plan_spec::create_update_plan_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
@@ -10,6 +11,7 @@ use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
pub struct PlanHandler;
|
||||
@@ -49,6 +51,10 @@ impl ToolHandler for PlanHandler {
|
||||
ToolName::plain("update_plan")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_update_plan_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@ use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments_with_base_path;
|
||||
use crate::tools::handlers::shell_spec::create_request_permissions_tool;
|
||||
use crate::tools::handlers::shell_spec::request_permissions_tool_description;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub struct RequestPermissionsHandler;
|
||||
|
||||
@@ -19,6 +22,12 @@ impl ToolHandler for RequestPermissionsHandler {
|
||||
ToolName::plain("request_permissions")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_request_permissions_tool(
|
||||
request_permissions_tool_description(),
|
||||
))
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -12,10 +12,13 @@ use codex_tools::REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE;
|
||||
use codex_tools::REQUEST_PLUGIN_INSTALL_PERSIST_KEY;
|
||||
use codex_tools::REQUEST_PLUGIN_INSTALL_TOOL_NAME;
|
||||
use codex_tools::RequestPluginInstallArgs;
|
||||
use codex_tools::RequestPluginInstallEntry;
|
||||
use codex_tools::RequestPluginInstallResult;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::all_requested_connectors_picked_up;
|
||||
use codex_tools::build_request_plugin_install_elicitation_request;
|
||||
use codex_tools::collect_request_plugin_install_entries;
|
||||
use codex_tools::filter_request_plugin_install_discoverable_tools_for_client;
|
||||
use codex_tools::verified_connector_install_completed;
|
||||
use rmcp::model::RequestId;
|
||||
@@ -30,10 +33,22 @@ use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::handlers::request_plugin_install_spec::create_request_plugin_install_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
|
||||
pub struct RequestPluginInstallHandler;
|
||||
#[derive(Default)]
|
||||
pub struct RequestPluginInstallHandler {
|
||||
discoverable_tools: Vec<RequestPluginInstallEntry>,
|
||||
}
|
||||
|
||||
impl RequestPluginInstallHandler {
|
||||
pub(crate) fn new(discoverable_tools: &[DiscoverableTool]) -> Self {
|
||||
Self {
|
||||
discoverable_tools: collect_request_plugin_install_entries(discoverable_tools),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for RequestPluginInstallHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
@@ -42,6 +57,14 @@ impl ToolHandler for RequestPluginInstallHandler {
|
||||
ToolName::plain(REQUEST_PLUGIN_INSTALL_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_request_plugin_install_tool(&self.discoverable_tools))
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -4,13 +4,16 @@ use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::handlers::request_user_input_spec::REQUEST_USER_INPUT_TOOL_NAME;
|
||||
use crate::tools::handlers::request_user_input_spec::create_request_user_input_tool;
|
||||
use crate::tools::handlers::request_user_input_spec::normalize_request_user_input_args;
|
||||
use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description;
|
||||
use crate::tools::handlers::request_user_input_spec::request_user_input_unavailable_message;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub struct RequestUserInputHandler {
|
||||
pub available_modes: Vec<ModeKind>,
|
||||
@@ -23,6 +26,12 @@ impl ToolHandler for RequestUserInputHandler {
|
||||
ToolName::plain(REQUEST_USER_INPUT_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_request_user_input_tool(
|
||||
request_user_input_tool_description(&self.available_modes),
|
||||
))
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ mod shell_handler;
|
||||
pub use container_exec::ContainerExecHandler;
|
||||
pub use local_shell::LocalShellHandler;
|
||||
pub use shell_command::ShellCommandHandler;
|
||||
pub(crate) use shell_command::ShellCommandHandlerOptions;
|
||||
pub use shell_handler::ShellHandler;
|
||||
|
||||
fn shell_function_payload_command(payload: &ToolPayload) -> Option<String> {
|
||||
|
||||
@@ -12,13 +12,24 @@ use crate::tools::registry::PreToolUsePayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use crate::tools::runtimes::shell::ShellRuntimeBackend;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use super::super::shell_spec::create_local_shell_tool;
|
||||
use super::RunExecLikeArgs;
|
||||
use super::local_shell_payload_command;
|
||||
use super::run_exec_like;
|
||||
use super::shell_handler::ShellHandler;
|
||||
|
||||
pub struct LocalShellHandler;
|
||||
#[derive(Default)]
|
||||
pub struct LocalShellHandler {
|
||||
include_spec: bool,
|
||||
}
|
||||
|
||||
impl LocalShellHandler {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { include_spec: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for LocalShellHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
@@ -27,6 +38,14 @@ impl ToolHandler for LocalShellHandler {
|
||||
ToolName::plain("local_shell")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
self.include_spec.then(create_local_shell_tool)
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
self.include_spec
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ use crate::tools::registry::PreToolUsePayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use crate::tools::runtimes::shell::ShellRuntimeBackend;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use super::super::shell_spec::CommandToolOptions;
|
||||
use super::super::shell_spec::create_shell_command_tool;
|
||||
use super::RunExecLikeArgs;
|
||||
use super::run_exec_like;
|
||||
use super::shell_command_payload_command;
|
||||
@@ -36,9 +39,24 @@ enum ShellCommandBackend {
|
||||
|
||||
pub struct ShellCommandHandler {
|
||||
backend: ShellCommandBackend,
|
||||
options: Option<ShellCommandHandlerOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ShellCommandHandlerOptions {
|
||||
pub(crate) backend_config: ShellCommandBackendConfig,
|
||||
pub(crate) allow_login_shell: bool,
|
||||
pub(crate) exec_permission_approvals_enabled: bool,
|
||||
}
|
||||
|
||||
impl ShellCommandHandler {
|
||||
pub(crate) fn new(options: ShellCommandHandlerOptions) -> Self {
|
||||
Self {
|
||||
options: Some(options),
|
||||
..Self::from(options.backend_config)
|
||||
}
|
||||
}
|
||||
|
||||
fn shell_runtime_backend(&self) -> ShellRuntimeBackend {
|
||||
match self.backend {
|
||||
ShellCommandBackend::Classic => ShellRuntimeBackend::ShellCommandClassic,
|
||||
@@ -99,7 +117,10 @@ impl From<ShellCommandBackendConfig> for ShellCommandHandler {
|
||||
ShellCommandBackendConfig::Classic => ShellCommandBackend::Classic,
|
||||
ShellCommandBackendConfig::ZshFork => ShellCommandBackend::ZshFork,
|
||||
};
|
||||
Self { backend }
|
||||
Self {
|
||||
backend,
|
||||
options: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +131,19 @@ impl ToolHandler for ShellCommandHandler {
|
||||
ToolName::plain("shell_command")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
self.options.map(|options| {
|
||||
create_shell_command_tool(CommandToolOptions {
|
||||
allow_login_shell: options.allow_login_shell,
|
||||
exec_permission_approvals_enabled: options.exec_permission_approvals_enabled,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
self.options.is_some()
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -18,15 +18,27 @@ use crate::tools::registry::PreToolUsePayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use crate::tools::runtimes::shell::ShellRuntimeBackend;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use super::super::shell_spec::ShellToolOptions;
|
||||
use super::super::shell_spec::create_shell_tool;
|
||||
use super::RunExecLikeArgs;
|
||||
use super::run_exec_like;
|
||||
use super::shell_function_post_tool_use_payload;
|
||||
use super::shell_function_pre_tool_use_payload;
|
||||
|
||||
pub struct ShellHandler;
|
||||
#[derive(Default)]
|
||||
pub struct ShellHandler {
|
||||
options: Option<ShellToolOptions>,
|
||||
}
|
||||
|
||||
impl ShellHandler {
|
||||
pub(crate) fn new(options: ShellToolOptions) -> Self {
|
||||
Self {
|
||||
options: Some(options),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn to_exec_params(
|
||||
params: &ShellToolCallParams,
|
||||
turn_context: &TurnContext,
|
||||
@@ -58,6 +70,14 @@ impl ToolHandler for ShellHandler {
|
||||
ToolName::plain("shell")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
self.options.map(create_shell_tool)
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
self.options.is_some()
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ async fn local_shell_pre_tool_use_payload_uses_joined_command() {
|
||||
},
|
||||
};
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let handler = LocalShellHandler;
|
||||
let handler = LocalShellHandler::default();
|
||||
|
||||
assert_eq!(
|
||||
handler.pre_tool_use_payload(&ToolInvocation {
|
||||
|
||||
@@ -13,9 +13,11 @@ use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::handlers::test_sync_spec::create_test_sync_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub struct TestSyncHandler;
|
||||
|
||||
@@ -61,6 +63,14 @@ impl ToolHandler for TestSyncHandler {
|
||||
ToolName::plain("test_sync_tool")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_test_sync_tool())
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::context::ToolSearchOutput;
|
||||
use crate::tools::handlers::tool_search_spec::create_tool_search_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use crate::tools::tool_search_entry::ToolSearchEntry;
|
||||
@@ -13,6 +14,8 @@ use codex_tools::LoadableToolSpec;
|
||||
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use codex_tools::TOOL_SEARCH_TOOL_NAME;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchSourceInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::coalesce_loadable_tool_specs;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -21,11 +24,15 @@ const COMPUTER_USE_TOOL_SEARCH_LIMIT: usize = 20;
|
||||
|
||||
pub struct ToolSearchHandler {
|
||||
entries: Vec<ToolSearchEntry>,
|
||||
search_source_infos: Vec<ToolSearchSourceInfo>,
|
||||
search_engine: SearchEngine<usize>,
|
||||
}
|
||||
|
||||
impl ToolSearchHandler {
|
||||
pub(crate) fn new(entries: Vec<ToolSearchEntry>) -> Self {
|
||||
pub(crate) fn new(
|
||||
entries: Vec<ToolSearchEntry>,
|
||||
search_source_infos: Vec<ToolSearchSourceInfo>,
|
||||
) -> Self {
|
||||
let documents: Vec<Document<usize>> = entries
|
||||
.iter()
|
||||
.map(|entry| entry.search_text.clone())
|
||||
@@ -37,6 +44,7 @@ impl ToolSearchHandler {
|
||||
|
||||
Self {
|
||||
entries,
|
||||
search_source_infos,
|
||||
search_engine,
|
||||
}
|
||||
}
|
||||
@@ -49,6 +57,17 @@ impl ToolHandler for ToolSearchHandler {
|
||||
ToolName::plain(TOOL_SEARCH_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_tool_search_tool(
|
||||
&self.search_source_infos,
|
||||
TOOL_SEARCH_DEFAULT_LIMIT,
|
||||
))
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
@@ -415,6 +434,9 @@ mod tests {
|
||||
mcp_tools: Option<&[ToolInfo]>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolSearchHandler {
|
||||
ToolSearchHandler::new(build_tool_search_entries(mcp_tools, dynamic_tools))
|
||||
ToolSearchHandler::new(
|
||||
build_tool_search_entries(mcp_tools, dynamic_tools),
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,26 @@ use crate::tools::context::ToolPayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub struct UnavailableToolHandler {
|
||||
tool_name: ToolName,
|
||||
spec: Option<ToolSpec>,
|
||||
}
|
||||
|
||||
impl UnavailableToolHandler {
|
||||
pub fn new(tool_name: ToolName) -> Self {
|
||||
Self { tool_name }
|
||||
pub fn new(tool_name: ToolName, spec: ToolSpec) -> Self {
|
||||
Self {
|
||||
tool_name,
|
||||
spec: Some(spec),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn without_spec(tool_name: ToolName) -> Self {
|
||||
Self {
|
||||
tool_name,
|
||||
spec: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +44,10 @@ impl ToolHandler for UnavailableToolHandler {
|
||||
self.tool_name.clone()
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
self.spec.clone()
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ mod exec_command;
|
||||
mod write_stdin;
|
||||
|
||||
pub use exec_command::ExecCommandHandler;
|
||||
pub(crate) use exec_command::ExecCommandHandlerOptions;
|
||||
pub use write_stdin::WriteStdinHandler;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
@@ -27,15 +27,45 @@ use codex_otel::SessionTelemetry;
|
||||
use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC;
|
||||
use codex_shell_command::is_safe_command::is_known_safe_command;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
|
||||
use super::super::shell_spec::CommandToolOptions;
|
||||
use super::super::shell_spec::create_exec_command_tool_with_environment_id;
|
||||
use super::ExecCommandArgs;
|
||||
use super::ExecCommandEnvironmentArgs;
|
||||
use super::effective_max_output_tokens;
|
||||
use super::get_command;
|
||||
use super::post_unified_exec_tool_use_payload;
|
||||
|
||||
pub struct ExecCommandHandler;
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ExecCommandHandlerOptions {
|
||||
pub(crate) allow_login_shell: bool,
|
||||
pub(crate) exec_permission_approvals_enabled: bool,
|
||||
pub(crate) include_environment_id: bool,
|
||||
}
|
||||
|
||||
pub struct ExecCommandHandler {
|
||||
options: ExecCommandHandlerOptions,
|
||||
}
|
||||
|
||||
impl Default for ExecCommandHandler {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
options: ExecCommandHandlerOptions {
|
||||
allow_login_shell: false,
|
||||
exec_permission_approvals_enabled: false,
|
||||
include_environment_id: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecCommandHandler {
|
||||
pub(crate) fn new(options: ExecCommandHandlerOptions) -> Self {
|
||||
Self { options }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for ExecCommandHandler {
|
||||
type Output = ExecCommandToolOutput;
|
||||
@@ -44,6 +74,20 @@ impl ToolHandler for ExecCommandHandler {
|
||||
ToolName::plain("exec_command")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_exec_command_tool_with_environment_id(
|
||||
CommandToolOptions {
|
||||
allow_login_shell: self.options.allow_login_shell,
|
||||
exec_permission_approvals_enabled: self.options.exec_permission_approvals_enabled,
|
||||
},
|
||||
self.options.include_environment_id,
|
||||
))
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ use crate::unified_exec::WriteStdinRequest;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::TerminalInteractionEvent;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::super::shell_spec::create_write_stdin_tool;
|
||||
use super::effective_max_output_tokens;
|
||||
use super::post_unified_exec_tool_use_payload;
|
||||
|
||||
@@ -36,6 +38,10 @@ impl ToolHandler for WriteStdinHandler {
|
||||
ToolName::plain("write_stdin")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_write_stdin_tool())
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ async fn exec_command_pre_tool_use_payload_uses_raw_command() {
|
||||
arguments: serde_json::json!({ "cmd": "printf exec command" }).to_string(),
|
||||
};
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let handler = ExecCommandHandler;
|
||||
let handler = ExecCommandHandler::default();
|
||||
|
||||
assert_eq!(
|
||||
handler.pre_tool_use_payload(&ToolInvocation {
|
||||
@@ -244,7 +244,7 @@ async fn exec_command_post_tool_use_payload_uses_output_for_noninteractive_one_s
|
||||
hook_command: Some("echo three".to_string()),
|
||||
};
|
||||
let invocation = invocation_for_payload("exec_command", "call-43", payload).await;
|
||||
let handler = ExecCommandHandler;
|
||||
let handler = ExecCommandHandler::default();
|
||||
assert_eq!(
|
||||
handler.post_tool_use_payload(&invocation, &output),
|
||||
Some(crate::tools::registry::PostToolUsePayload {
|
||||
@@ -273,7 +273,7 @@ async fn exec_command_post_tool_use_payload_uses_output_for_interactive_completi
|
||||
hook_command: Some("echo three".to_string()),
|
||||
};
|
||||
let invocation = invocation_for_payload("exec_command", "call-44", payload).await;
|
||||
let handler = ExecCommandHandler;
|
||||
let handler = ExecCommandHandler::default();
|
||||
|
||||
assert_eq!(
|
||||
handler.post_tool_use_payload(&invocation, &output),
|
||||
@@ -303,7 +303,7 @@ async fn exec_command_post_tool_use_payload_skips_running_sessions() {
|
||||
hook_command: Some("echo three".to_string()),
|
||||
};
|
||||
let invocation = invocation_for_payload("exec_command", "call-45", payload).await;
|
||||
let handler = ExecCommandHandler;
|
||||
let handler = ExecCommandHandler::default();
|
||||
assert_eq!(handler.post_tool_use_payload(&invocation, &output), None);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,32 @@ use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
|
||||
use crate::tools::handlers::view_image_spec::create_view_image_tool;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub struct ViewImageHandler;
|
||||
pub struct ViewImageHandler {
|
||||
options: ViewImageToolOptions,
|
||||
}
|
||||
|
||||
impl Default for ViewImageHandler {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
options: ViewImageToolOptions {
|
||||
can_request_original_image_detail: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewImageHandler {
|
||||
pub(crate) fn new(options: ViewImageToolOptions) -> Self {
|
||||
Self { options }
|
||||
}
|
||||
}
|
||||
|
||||
const VIEW_IMAGE_UNSUPPORTED_MESSAGE: &str =
|
||||
"view_image is not allowed because you do not support image inputs";
|
||||
@@ -44,6 +65,14 @@ impl ToolHandler for ViewImageHandler {
|
||||
ToolName::plain("view_image")
|
||||
}
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
Some(create_view_image_tool(self.options))
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
|
||||
use crate::util::error_or_panic;
|
||||
use codex_hooks::HookEvent;
|
||||
use codex_hooks::HookEventAfterToolUse;
|
||||
use codex_hooks::HookPayload;
|
||||
@@ -47,6 +48,14 @@ pub trait ToolHandler: Send + Sync {
|
||||
/// The concrete tool name handled by this handler instance.
|
||||
fn tool_name(&self) -> ToolName;
|
||||
|
||||
fn spec(&self) -> Option<ToolSpec> {
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind;
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
@@ -512,37 +521,26 @@ impl ToolRegistry {
|
||||
pub struct ToolRegistryBuilder {
|
||||
handlers: HashMap<ToolName, Arc<dyn AnyToolHandler>>,
|
||||
specs: Vec<ConfiguredToolSpec>,
|
||||
code_mode_enabled: bool,
|
||||
}
|
||||
|
||||
impl ToolRegistryBuilder {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(code_mode_enabled: bool) -> Self {
|
||||
Self {
|
||||
handlers: HashMap::new(),
|
||||
specs: Vec::new(),
|
||||
code_mode_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_spec_with_parallel_support(
|
||||
&mut self,
|
||||
spec: ToolSpec,
|
||||
supports_parallel_tool_calls: bool,
|
||||
) {
|
||||
self.specs
|
||||
.push(ConfiguredToolSpec::new(spec, supports_parallel_tool_calls));
|
||||
}
|
||||
|
||||
pub(crate) fn push_spec(
|
||||
&mut self,
|
||||
spec: ToolSpec,
|
||||
supports_parallel_tool_calls: bool,
|
||||
code_mode_enabled: bool,
|
||||
) {
|
||||
let spec = if code_mode_enabled {
|
||||
pub(crate) fn push_spec(&mut self, spec: ToolSpec, supports_parallel_tool_calls: bool) {
|
||||
let spec = if self.code_mode_enabled {
|
||||
codex_tools::augment_tool_spec_for_code_mode(spec)
|
||||
} else {
|
||||
spec
|
||||
};
|
||||
self.push_spec_with_parallel_support(spec, supports_parallel_tool_calls);
|
||||
self.specs
|
||||
.push(ConfiguredToolSpec::new(spec, supports_parallel_tool_calls));
|
||||
}
|
||||
|
||||
pub fn register_handler<H>(&mut self, handler: Arc<H>)
|
||||
@@ -550,11 +548,18 @@ impl ToolRegistryBuilder {
|
||||
H: ToolHandler + 'static,
|
||||
{
|
||||
let name = handler.tool_name();
|
||||
let display_name = name.display();
|
||||
let handler: Arc<dyn AnyToolHandler> = handler;
|
||||
if self.handlers.insert(name, handler).is_some() {
|
||||
warn!("overwriting handler for tool {display_name}");
|
||||
if self.handlers.contains_key(&name) {
|
||||
error_or_panic(format!("handler for tool {name} already registered"));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(spec) = handler.spec() {
|
||||
let supports_parallel_tool_calls = handler.supports_parallel_tool_calls();
|
||||
self.push_spec(spec, supports_parallel_tool_calls);
|
||||
}
|
||||
|
||||
let handler: Arc<dyn AnyToolHandler> = handler;
|
||||
self.handlers.insert(name, handler);
|
||||
}
|
||||
|
||||
pub(crate) fn specs(&self) -> &[ConfiguredToolSpec] {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::*;
|
||||
use crate::tools::handlers::GetGoalHandler;
|
||||
use crate::tools::handlers::goal_spec::GET_GOAL_TOOL_NAME;
|
||||
use crate::tools::handlers::goal_spec::create_get_goal_tool;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
struct TestHandler {
|
||||
@@ -62,3 +65,18 @@ fn handler_looks_up_namespaced_aliases_explicitly() {
|
||||
.is_some_and(|handler| Arc::ptr_eq(handler, &namespaced_handler))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_handler_adds_handler_and_augments_specs_for_code_mode() {
|
||||
let mut builder = ToolRegistryBuilder::new(/*code_mode_enabled*/ true);
|
||||
builder.register_handler(Arc::new(GetGoalHandler));
|
||||
|
||||
let (specs, registry) = builder.build();
|
||||
|
||||
assert_eq!(specs.len(), 1);
|
||||
assert_eq!(
|
||||
specs[0].spec,
|
||||
codex_tools::augment_tool_spec_for_code_mode(create_get_goal_tool())
|
||||
);
|
||||
assert!(registry.has_handler(&codex_tools::ToolName::plain(GET_GOAL_TOOL_NAME)));
|
||||
}
|
||||
|
||||
@@ -153,13 +153,15 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
output_schema: None,
|
||||
defer_loading: None,
|
||||
});
|
||||
builder.push_spec(
|
||||
builder.register_handler(Arc::new(UnavailableToolHandler::new(
|
||||
unavailable_tool,
|
||||
spec,
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
)));
|
||||
} else {
|
||||
builder.register_handler(Arc::new(UnavailableToolHandler::without_spec(
|
||||
unavailable_tool,
|
||||
)));
|
||||
}
|
||||
builder.register_handler(Arc::new(UnavailableToolHandler::new(unavailable_tool)));
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::tools::code_mode::execute_spec::create_code_mode_tool;
|
||||
use crate::tools::code_mode::wait_spec::create_wait_tool;
|
||||
use crate::tools::handlers::ApplyPatchHandler;
|
||||
use crate::tools::handlers::CodeModeExecuteHandler;
|
||||
use crate::tools::handlers::CodeModeWaitHandler;
|
||||
@@ -7,6 +6,7 @@ use crate::tools::handlers::ContainerExecHandler;
|
||||
use crate::tools::handlers::CreateGoalHandler;
|
||||
use crate::tools::handlers::DynamicToolHandler;
|
||||
use crate::tools::handlers::ExecCommandHandler;
|
||||
use crate::tools::handlers::ExecCommandHandlerOptions;
|
||||
use crate::tools::handlers::GetGoalHandler;
|
||||
use crate::tools::handlers::ListMcpResourceTemplatesHandler;
|
||||
use crate::tools::handlers::ListMcpResourcesHandler;
|
||||
@@ -18,6 +18,7 @@ use crate::tools::handlers::RequestPermissionsHandler;
|
||||
use crate::tools::handlers::RequestPluginInstallHandler;
|
||||
use crate::tools::handlers::RequestUserInputHandler;
|
||||
use crate::tools::handlers::ShellCommandHandler;
|
||||
use crate::tools::handlers::ShellCommandHandlerOptions;
|
||||
use crate::tools::handlers::ShellHandler;
|
||||
use crate::tools::handlers::TestSyncHandler;
|
||||
use crate::tools::handlers::ToolSearchHandler;
|
||||
@@ -26,67 +27,29 @@ use crate::tools::handlers::ViewImageHandler;
|
||||
use crate::tools::handlers::WriteStdinHandler;
|
||||
use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler;
|
||||
use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler;
|
||||
use crate::tools::handlers::agent_jobs_spec::create_report_agent_job_result_tool;
|
||||
use crate::tools::handlers::agent_jobs_spec::create_spawn_agents_on_csv_tool;
|
||||
use crate::tools::handlers::apply_patch_spec::create_apply_patch_freeform_tool;
|
||||
use crate::tools::handlers::apply_patch_spec::create_apply_patch_json_tool;
|
||||
use crate::tools::handlers::goal_spec::create_create_goal_tool;
|
||||
use crate::tools::handlers::goal_spec::create_get_goal_tool;
|
||||
use crate::tools::handlers::goal_spec::create_update_goal_tool;
|
||||
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resource_templates_tool;
|
||||
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resources_tool;
|
||||
use crate::tools::handlers::mcp_resource_spec::create_read_mcp_resource_tool;
|
||||
use crate::tools::handlers::multi_agents::CloseAgentHandler;
|
||||
use crate::tools::handlers::multi_agents::ResumeAgentHandler;
|
||||
use crate::tools::handlers::multi_agents::SendInputHandler;
|
||||
use crate::tools::handlers::multi_agents::SpawnAgentHandler;
|
||||
use crate::tools::handlers::multi_agents::WaitAgentHandler;
|
||||
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
|
||||
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v1;
|
||||
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v2;
|
||||
use crate::tools::handlers::multi_agents_spec::create_followup_task_tool;
|
||||
use crate::tools::handlers::multi_agents_spec::create_list_agents_tool;
|
||||
use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool;
|
||||
use crate::tools::handlers::multi_agents_spec::create_send_input_tool_v1;
|
||||
use crate::tools::handlers::multi_agents_spec::create_send_message_tool;
|
||||
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v1;
|
||||
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
|
||||
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
|
||||
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
|
||||
use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
|
||||
use crate::tools::handlers::plan_spec::create_update_plan_tool;
|
||||
use crate::tools::handlers::request_plugin_install_spec::create_request_plugin_install_tool;
|
||||
use crate::tools::handlers::request_user_input_spec::create_request_user_input_tool;
|
||||
use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description;
|
||||
use crate::tools::handlers::shell_spec::CommandToolOptions;
|
||||
use crate::tools::handlers::shell_spec::ShellToolOptions;
|
||||
use crate::tools::handlers::shell_spec::create_exec_command_tool_with_environment_id;
|
||||
use crate::tools::handlers::shell_spec::create_local_shell_tool;
|
||||
use crate::tools::handlers::shell_spec::create_request_permissions_tool;
|
||||
use crate::tools::handlers::shell_spec::create_shell_command_tool;
|
||||
use crate::tools::handlers::shell_spec::create_shell_tool;
|
||||
use crate::tools::handlers::shell_spec::create_write_stdin_tool;
|
||||
use crate::tools::handlers::shell_spec::request_permissions_tool_description;
|
||||
use crate::tools::handlers::test_sync_spec::create_test_sync_tool;
|
||||
use crate::tools::handlers::tool_search_spec::create_tool_search_tool;
|
||||
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
|
||||
use crate::tools::handlers::view_image_spec::create_view_image_tool;
|
||||
use crate::tools::hosted_spec::WebSearchToolOptions;
|
||||
use crate::tools::hosted_spec::create_image_generation_tool;
|
||||
use crate::tools::hosted_spec::create_web_search_tool;
|
||||
use crate::tools::registry::ToolRegistryBuilder;
|
||||
use crate::tools::spec_plan_types::ToolRegistryBuildParams;
|
||||
use crate::tools::spec_plan_types::agent_type_description;
|
||||
use codex_protocol::openai_models::ApplyPatchToolType;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_tools::ResponsesApiNamespace;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use codex_tools::ToolEnvironmentMode;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchSource;
|
||||
@@ -95,7 +58,6 @@ use codex_tools::ToolSpec;
|
||||
use codex_tools::ToolsConfig;
|
||||
use codex_tools::coalesce_loadable_tool_specs;
|
||||
use codex_tools::collect_code_mode_exec_prompt_tool_definitions;
|
||||
use codex_tools::collect_request_plugin_install_entries;
|
||||
use codex_tools::collect_tool_search_source_infos;
|
||||
use codex_tools::default_namespace_description;
|
||||
use codex_tools::dynamic_tool_to_loadable_tool_spec;
|
||||
@@ -108,7 +70,7 @@ pub fn build_tool_registry_builder(
|
||||
config: &ToolsConfig,
|
||||
params: ToolRegistryBuildParams<'_>,
|
||||
) -> ToolRegistryBuilder {
|
||||
let mut builder = ToolRegistryBuilder::new();
|
||||
let mut builder = ToolRegistryBuilder::new(config.code_mode_enabled);
|
||||
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
|
||||
|
||||
if config.code_mode_enabled {
|
||||
@@ -142,7 +104,7 @@ pub fn build_tool_registry_builder(
|
||||
);
|
||||
enabled_tools
|
||||
.sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions));
|
||||
builder.push_spec(
|
||||
builder.register_handler(Arc::new(CodeModeExecuteHandler::new(
|
||||
create_code_mode_tool(
|
||||
&enabled_tools,
|
||||
&namespace_descriptions,
|
||||
@@ -152,15 +114,7 @@ pub fn build_tool_registry_builder(
|
||||
.deferred_mcp_tools
|
||||
.is_some_and(|tools| !tools.is_empty()),
|
||||
),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(CodeModeExecuteHandler));
|
||||
builder.push_spec(
|
||||
create_wait_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
)));
|
||||
builder.register_handler(Arc::new(CodeModeWaitHandler));
|
||||
}
|
||||
|
||||
@@ -169,51 +123,32 @@ pub fn build_tool_registry_builder(
|
||||
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
|
||||
match &config.shell_type {
|
||||
ConfigShellToolType::Default => {
|
||||
builder.push_spec(
|
||||
create_shell_tool(ShellToolOptions {
|
||||
exec_permission_approvals_enabled,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(ShellHandler::new(ShellToolOptions {
|
||||
exec_permission_approvals_enabled,
|
||||
})));
|
||||
}
|
||||
ConfigShellToolType::Local => {
|
||||
builder.push_spec(
|
||||
create_local_shell_tool(),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(LocalShellHandler::new()));
|
||||
}
|
||||
ConfigShellToolType::UnifiedExec => {
|
||||
builder.push_spec(
|
||||
create_exec_command_tool_with_environment_id(
|
||||
CommandToolOptions {
|
||||
allow_login_shell: config.allow_login_shell,
|
||||
exec_permission_approvals_enabled,
|
||||
},
|
||||
builder.register_handler(Arc::new(ExecCommandHandler::new(
|
||||
ExecCommandHandlerOptions {
|
||||
allow_login_shell: config.allow_login_shell,
|
||||
exec_permission_approvals_enabled,
|
||||
include_environment_id,
|
||||
),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_write_stdin_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(ExecCommandHandler));
|
||||
},
|
||||
)));
|
||||
builder.register_handler(Arc::new(WriteStdinHandler));
|
||||
}
|
||||
ConfigShellToolType::Disabled => {}
|
||||
ConfigShellToolType::ShellCommand => {
|
||||
builder.push_spec(
|
||||
create_shell_command_tool(CommandToolOptions {
|
||||
builder.register_handler(Arc::new(ShellCommandHandler::new(
|
||||
ShellCommandHandlerOptions {
|
||||
backend_config: config.shell_command_backend,
|
||||
allow_login_shell: config.allow_login_shell,
|
||||
exec_permission_approvals_enabled,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
},
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,79 +156,56 @@ pub fn build_tool_registry_builder(
|
||||
if config.environment_mode.has_environment()
|
||||
&& config.shell_type != ConfigShellToolType::Disabled
|
||||
{
|
||||
builder.register_handler(Arc::new(ShellHandler));
|
||||
builder.register_handler(Arc::new(ContainerExecHandler));
|
||||
builder.register_handler(Arc::new(LocalShellHandler));
|
||||
builder.register_handler(Arc::new(ShellCommandHandler::from(
|
||||
config.shell_command_backend,
|
||||
)));
|
||||
match &config.shell_type {
|
||||
ConfigShellToolType::Default => {
|
||||
builder.register_handler(Arc::new(ContainerExecHandler));
|
||||
builder.register_handler(Arc::new(LocalShellHandler::default()));
|
||||
builder.register_handler(Arc::new(ShellCommandHandler::from(
|
||||
config.shell_command_backend,
|
||||
)));
|
||||
}
|
||||
ConfigShellToolType::Local => {
|
||||
builder.register_handler(Arc::new(ShellHandler::default()));
|
||||
builder.register_handler(Arc::new(ContainerExecHandler));
|
||||
builder.register_handler(Arc::new(ShellCommandHandler::from(
|
||||
config.shell_command_backend,
|
||||
)));
|
||||
}
|
||||
ConfigShellToolType::UnifiedExec => {
|
||||
builder.register_handler(Arc::new(ShellHandler::default()));
|
||||
builder.register_handler(Arc::new(ContainerExecHandler));
|
||||
builder.register_handler(Arc::new(LocalShellHandler::default()));
|
||||
builder.register_handler(Arc::new(ShellCommandHandler::from(
|
||||
config.shell_command_backend,
|
||||
)));
|
||||
}
|
||||
ConfigShellToolType::ShellCommand => {
|
||||
builder.register_handler(Arc::new(ShellHandler::default()));
|
||||
builder.register_handler(Arc::new(ContainerExecHandler));
|
||||
builder.register_handler(Arc::new(LocalShellHandler::default()));
|
||||
}
|
||||
ConfigShellToolType::Disabled => {}
|
||||
}
|
||||
}
|
||||
|
||||
if params.mcp_tools.is_some() {
|
||||
builder.push_spec(
|
||||
create_list_mcp_resources_tool(),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_list_mcp_resource_templates_tool(),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_read_mcp_resource_tool(),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(ListMcpResourcesHandler));
|
||||
builder.register_handler(Arc::new(ListMcpResourceTemplatesHandler));
|
||||
builder.register_handler(Arc::new(ReadMcpResourceHandler));
|
||||
}
|
||||
|
||||
builder.push_spec(
|
||||
create_update_plan_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(PlanHandler));
|
||||
if config.goal_tools {
|
||||
builder.push_spec(
|
||||
create_get_goal_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(GetGoalHandler));
|
||||
builder.push_spec(
|
||||
create_create_goal_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(CreateGoalHandler));
|
||||
builder.push_spec(
|
||||
create_update_goal_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(UpdateGoalHandler));
|
||||
}
|
||||
|
||||
builder.push_spec(
|
||||
create_request_user_input_tool(request_user_input_tool_description(
|
||||
&config.request_user_input_available_modes,
|
||||
)),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(RequestUserInputHandler {
|
||||
available_modes: config.request_user_input_available_modes.clone(),
|
||||
}));
|
||||
|
||||
if config.request_permissions_tool_enabled {
|
||||
builder.push_spec(
|
||||
create_request_permissions_tool(request_permissions_tool_description()),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(RequestPermissionsHandler));
|
||||
}
|
||||
|
||||
@@ -330,13 +242,9 @@ pub fn build_tool_registry_builder(
|
||||
});
|
||||
}
|
||||
|
||||
builder.push_spec(
|
||||
create_tool_search_tool(&search_source_infos, TOOL_SEARCH_DEFAULT_LIMIT),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(ToolSearchHandler::new(
|
||||
params.tool_search_entries.to_vec(),
|
||||
search_source_infos,
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -344,36 +252,17 @@ pub fn build_tool_registry_builder(
|
||||
&& let Some(discoverable_tools) =
|
||||
params.discoverable_tools.filter(|tools| !tools.is_empty())
|
||||
{
|
||||
builder.push_spec(
|
||||
create_request_plugin_install_tool(&collect_request_plugin_install_entries(
|
||||
discoverable_tools,
|
||||
)),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
/*code_mode_enabled*/ false,
|
||||
);
|
||||
builder.register_handler(Arc::new(RequestPluginInstallHandler));
|
||||
builder.register_handler(Arc::new(RequestPluginInstallHandler::new(
|
||||
discoverable_tools,
|
||||
)));
|
||||
}
|
||||
|
||||
if config.environment_mode.has_environment()
|
||||
&& let Some(apply_patch_tool_type) = &config.apply_patch_tool_type
|
||||
{
|
||||
match apply_patch_tool_type {
|
||||
ApplyPatchToolType::Freeform => {
|
||||
builder.push_spec(
|
||||
create_apply_patch_freeform_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
}
|
||||
ApplyPatchToolType::Function => {
|
||||
builder.push_spec(
|
||||
create_apply_patch_json_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
builder.register_handler(Arc::new(ApplyPatchHandler));
|
||||
builder.register_handler(Arc::new(ApplyPatchHandler::new(
|
||||
apply_patch_tool_type.clone(),
|
||||
)));
|
||||
}
|
||||
|
||||
if config
|
||||
@@ -381,11 +270,6 @@ pub fn build_tool_registry_builder(
|
||||
.iter()
|
||||
.any(|tool| tool == "test_sync_tool")
|
||||
{
|
||||
builder.push_spec(
|
||||
create_test_sync_tool(),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(TestSyncHandler));
|
||||
}
|
||||
|
||||
@@ -394,135 +278,62 @@ pub fn build_tool_registry_builder(
|
||||
web_search_config: config.web_search_config.as_ref(),
|
||||
web_search_tool_type: config.web_search_tool_type,
|
||||
}) {
|
||||
builder.push_spec(
|
||||
web_search_tool,
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(web_search_tool, /*supports_parallel_tool_calls*/ false);
|
||||
}
|
||||
|
||||
if config.image_gen_tool {
|
||||
builder.push_spec(
|
||||
create_image_generation_tool("png"),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
}
|
||||
|
||||
if config.environment_mode.has_environment() {
|
||||
builder.push_spec(
|
||||
create_view_image_tool(ViewImageToolOptions {
|
||||
can_request_original_image_detail: config.can_request_original_image_detail,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(ViewImageHandler));
|
||||
builder.register_handler(Arc::new(ViewImageHandler::new(ViewImageToolOptions {
|
||||
can_request_original_image_detail: config.can_request_original_image_detail,
|
||||
})));
|
||||
}
|
||||
|
||||
if config.collab_tools {
|
||||
if config.multi_agent_v2 {
|
||||
let agent_type_description =
|
||||
agent_type_description(config, params.default_agent_type_description);
|
||||
builder.push_spec(
|
||||
create_spawn_agent_tool_v2(SpawnAgentToolOptions {
|
||||
available_models: &config.available_models,
|
||||
agent_type_description,
|
||||
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
|
||||
include_usage_hint: config.spawn_agent_usage_hint,
|
||||
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
|
||||
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_send_message_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_followup_task_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_wait_agent_tool_v2(params.wait_agent_timeouts),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_close_agent_tool_v2(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_list_agents_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(SpawnAgentHandlerV2));
|
||||
builder.register_handler(Arc::new(SpawnAgentHandlerV2::new(SpawnAgentToolOptions {
|
||||
available_models: config.available_models.clone(),
|
||||
agent_type_description,
|
||||
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
|
||||
include_usage_hint: config.spawn_agent_usage_hint,
|
||||
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
|
||||
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
|
||||
})));
|
||||
builder.register_handler(Arc::new(SendMessageHandlerV2));
|
||||
builder.register_handler(Arc::new(FollowupTaskHandlerV2));
|
||||
builder.register_handler(Arc::new(WaitAgentHandlerV2));
|
||||
builder.register_handler(Arc::new(WaitAgentHandlerV2::new(
|
||||
params.wait_agent_timeouts,
|
||||
)));
|
||||
builder.register_handler(Arc::new(CloseAgentHandlerV2));
|
||||
builder.register_handler(Arc::new(ListAgentsHandlerV2));
|
||||
} else {
|
||||
let agent_type_description =
|
||||
agent_type_description(config, params.default_agent_type_description);
|
||||
builder.push_spec(
|
||||
create_spawn_agent_tool_v1(SpawnAgentToolOptions {
|
||||
available_models: &config.available_models,
|
||||
agent_type_description,
|
||||
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
|
||||
include_usage_hint: config.spawn_agent_usage_hint,
|
||||
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
|
||||
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_send_input_tool_v1(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_resume_agent_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(ResumeAgentHandler));
|
||||
builder.push_spec(
|
||||
create_wait_agent_tool_v1(params.wait_agent_timeouts),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(
|
||||
create_close_agent_tool_v1(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(SpawnAgentHandler));
|
||||
builder.register_handler(Arc::new(SpawnAgentHandler::new(SpawnAgentToolOptions {
|
||||
available_models: config.available_models.clone(),
|
||||
agent_type_description,
|
||||
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
|
||||
include_usage_hint: config.spawn_agent_usage_hint,
|
||||
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
|
||||
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
|
||||
})));
|
||||
builder.register_handler(Arc::new(SendInputHandler));
|
||||
builder.register_handler(Arc::new(WaitAgentHandler));
|
||||
builder.register_handler(Arc::new(ResumeAgentHandler));
|
||||
builder.register_handler(Arc::new(WaitAgentHandler::new(params.wait_agent_timeouts)));
|
||||
builder.register_handler(Arc::new(CloseAgentHandler));
|
||||
}
|
||||
}
|
||||
|
||||
if config.agent_jobs_tools {
|
||||
builder.push_spec(
|
||||
create_spawn_agents_on_csv_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(SpawnAgentsOnCsvHandler));
|
||||
if config.agent_jobs_worker_tools {
|
||||
builder.push_spec(
|
||||
create_report_agent_job_result_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler(Arc::new(ReportAgentJobResultHandler));
|
||||
}
|
||||
}
|
||||
@@ -584,7 +395,6 @@ pub fn build_tool_registry_builder(
|
||||
tools,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -609,11 +419,7 @@ pub fn build_tool_registry_builder(
|
||||
for spec in coalesce_loadable_tool_specs(dynamic_tool_specs) {
|
||||
let spec = spec.into();
|
||||
if config.namespace_tools || !matches!(spec, ToolSpec::Namespace(_)) {
|
||||
builder.push_spec(
|
||||
spec,
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.push_spec(spec, /*supports_parallel_tool_calls*/ false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
use super::*;
|
||||
use crate::tools::handlers::apply_patch_spec::create_apply_patch_freeform_tool;
|
||||
use crate::tools::handlers::goal_spec::create_create_goal_tool;
|
||||
use crate::tools::handlers::goal_spec::create_get_goal_tool;
|
||||
use crate::tools::handlers::goal_spec::create_update_goal_tool;
|
||||
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
|
||||
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v1;
|
||||
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v2;
|
||||
use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool;
|
||||
use crate::tools::handlers::multi_agents_spec::create_send_input_tool_v1;
|
||||
use crate::tools::handlers::multi_agents_spec::create_send_message_tool;
|
||||
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v1;
|
||||
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
|
||||
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
|
||||
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
|
||||
use crate::tools::handlers::plan_spec::create_update_plan_tool;
|
||||
use crate::tools::handlers::request_user_input_spec::REQUEST_USER_INPUT_TOOL_NAME;
|
||||
use crate::tools::handlers::request_user_input_spec::create_request_user_input_tool;
|
||||
use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description;
|
||||
use crate::tools::handlers::shell_spec::CommandToolOptions;
|
||||
use crate::tools::handlers::shell_spec::create_exec_command_tool;
|
||||
use crate::tools::handlers::shell_spec::create_request_permissions_tool;
|
||||
use crate::tools::handlers::shell_spec::create_write_stdin_tool;
|
||||
use crate::tools::handlers::shell_spec::request_permissions_tool_description;
|
||||
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
|
||||
use crate::tools::handlers::view_image_spec::create_view_image_tool;
|
||||
use crate::tools::registry::ToolRegistry;
|
||||
use crate::tools::spec_plan_types::ToolNamespace;
|
||||
use crate::tools::spec_plan_types::ToolRegistryBuildDeferredTool;
|
||||
@@ -2423,9 +2444,9 @@ fn request_user_input_tool_spec(available_modes: &[ModeKind]) -> ToolSpec {
|
||||
create_request_user_input_tool(request_user_input_tool_description(available_modes))
|
||||
}
|
||||
|
||||
fn spawn_agent_tool_options(config: &ToolsConfig) -> SpawnAgentToolOptions<'_> {
|
||||
fn spawn_agent_tool_options(config: &ToolsConfig) -> SpawnAgentToolOptions {
|
||||
SpawnAgentToolOptions {
|
||||
available_models: &config.available_models,
|
||||
available_models: config.available_models.clone(),
|
||||
agent_type_description: agent_type_description(config, DEFAULT_AGENT_TYPE_DESCRIPTION),
|
||||
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
|
||||
include_usage_hint: config.spawn_agent_usage_hint,
|
||||
|
||||
@@ -446,6 +446,11 @@ impl WebSocketHandshake {
|
||||
pub struct WebSocketConnectionConfig {
|
||||
pub requests: Vec<Vec<Value>>,
|
||||
pub response_headers: Vec<(String, String)>,
|
||||
/// Optional notification fired after the TCP connection is accepted and before the websocket
|
||||
/// handshake is accepted.
|
||||
pub accept_started: Option<Arc<Notify>>,
|
||||
/// Optional gate that blocks websocket handshake acceptance until the notifier is signalled.
|
||||
pub accept_release: Option<Arc<Notify>>,
|
||||
/// Optional delay inserted before accepting the websocket handshake.
|
||||
///
|
||||
/// Tests use this to force websocket setup into an in-flight state so first-turn warmup paths
|
||||
@@ -1254,6 +1259,8 @@ pub async fn start_websocket_server(connections: Vec<Vec<Vec<Value>>>) -> WebSoc
|
||||
.map(|requests| WebSocketConnectionConfig {
|
||||
requests,
|
||||
response_headers: Vec::new(),
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
accept_delay: None,
|
||||
close_after_requests: true,
|
||||
})
|
||||
@@ -1298,12 +1305,27 @@ pub async fn start_websocket_server_with_headers(
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(accept_started) = &connection.accept_started {
|
||||
accept_started.notify_one();
|
||||
}
|
||||
|
||||
if let Some(accept_release) = &connection.accept_release {
|
||||
tokio::select! {
|
||||
_ = accept_release.notified() => {}
|
||||
_ = &mut shutdown_rx => return,
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(delay) = connection.accept_delay {
|
||||
tokio::time::sleep(delay).await;
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(delay) => {}
|
||||
_ = &mut shutdown_rx => return,
|
||||
}
|
||||
}
|
||||
|
||||
let response_headers = connection.response_headers.clone();
|
||||
let handshake_log = Arc::clone(&handshakes);
|
||||
let pending_handshake = Arc::new(Mutex::new(None));
|
||||
let callback_handshake = Arc::clone(&pending_handshake);
|
||||
let callback = move |req: &Request, mut response: Response| {
|
||||
let headers = req
|
||||
.headers()
|
||||
@@ -1315,7 +1337,7 @@ pub async fn start_websocket_server_with_headers(
|
||||
.map(|value| (name.as_str().to_string(), value.to_string()))
|
||||
})
|
||||
.collect();
|
||||
handshake_log.lock().unwrap().push(WebSocketHandshake {
|
||||
*callback_handshake.lock().unwrap() = Some(WebSocketHandshake {
|
||||
uri: req.uri().to_string(),
|
||||
headers,
|
||||
});
|
||||
@@ -1344,6 +1366,10 @@ pub async fn start_websocket_server_with_headers(
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if let Some(handshake) = pending_handshake.lock().unwrap().take() {
|
||||
handshakes.lock().unwrap().push(handshake);
|
||||
}
|
||||
|
||||
let connection_index = {
|
||||
let mut log = requests.lock().unwrap();
|
||||
log.push(Vec::new());
|
||||
|
||||
@@ -121,6 +121,8 @@ async fn websocket_first_turn_handles_handshake_delay_with_startup_prewarm() ->
|
||||
],
|
||||
],
|
||||
response_headers: Vec::new(),
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
// Delay handshake so turn processing must tolerate websocket startup latency.
|
||||
accept_delay: Some(Duration::from_millis(150)),
|
||||
close_after_requests: true,
|
||||
|
||||
@@ -941,6 +941,8 @@ async fn responses_websocket_emits_reasoning_included_event() {
|
||||
let server = start_websocket_server_with_headers(vec![WebSocketConnectionConfig {
|
||||
requests: vec![vec![ev_response_created("resp-1"), ev_completed("resp-1")]],
|
||||
response_headers: vec![("X-Reasoning-Included".to_string(), "true".to_string())],
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
accept_delay: None,
|
||||
close_after_requests: true,
|
||||
}])
|
||||
@@ -1015,6 +1017,8 @@ async fn responses_websocket_emits_rate_limit_events() {
|
||||
("X-Models-Etag".to_string(), "etag-123".to_string()),
|
||||
("X-Reasoning-Included".to_string(), "true".to_string()),
|
||||
],
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
accept_delay: None,
|
||||
close_after_requests: true,
|
||||
}])
|
||||
@@ -1751,6 +1755,8 @@ async fn responses_websocket_v2_surfaces_terminal_error_without_close_handshake(
|
||||
})],
|
||||
],
|
||||
response_headers: Vec::new(),
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
accept_delay: None,
|
||||
close_after_requests: false,
|
||||
}])
|
||||
|
||||
@@ -48,6 +48,7 @@ use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Notify;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::timeout;
|
||||
use wiremock::Match;
|
||||
@@ -492,6 +493,8 @@ async fn conversation_webrtc_start_posts_generated_session() -> Result<()> {
|
||||
vec![],
|
||||
],
|
||||
response_headers: Vec::new(),
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
accept_delay: Some(sideband_accept_delay),
|
||||
close_after_requests: false,
|
||||
}])
|
||||
@@ -659,10 +662,14 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join(
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let accept_started = Arc::new(Notify::new());
|
||||
let accept_release = Arc::new(Notify::new());
|
||||
let realtime_server = start_websocket_server_with_headers(vec![WebSocketConnectionConfig {
|
||||
requests: vec![vec![]],
|
||||
response_headers: Vec::new(),
|
||||
accept_delay: Some(Duration::from_millis(500)),
|
||||
accept_started: Some(Arc::clone(&accept_started)),
|
||||
accept_release: Some(Arc::clone(&accept_release)),
|
||||
accept_delay: None,
|
||||
close_after_requests: false,
|
||||
}])
|
||||
.await;
|
||||
@@ -699,6 +706,9 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join(
|
||||
realtime_server.handshakes().is_empty(),
|
||||
"sideband websocket should still be pending when SDP is emitted"
|
||||
);
|
||||
timeout(Duration::from_secs(5), accept_started.notified())
|
||||
.await
|
||||
.context("sideband websocket should connect before close")?;
|
||||
|
||||
test.codex.submit(Op::RealtimeConversationClose).await?;
|
||||
let closed = wait_for_event_match(&test.codex, |msg| match msg {
|
||||
@@ -726,9 +736,17 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join(
|
||||
"pending sideband task leaked after close: {:?}",
|
||||
stale_event.ok()
|
||||
);
|
||||
accept_release.notify_one();
|
||||
let stale_request = timeout(Duration::from_millis(250), async {
|
||||
realtime_server
|
||||
.wait_for_request(/*connection_index*/ 0, /*request_index*/ 0)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
assert!(
|
||||
realtime_server.handshakes().is_empty(),
|
||||
"pending sideband task should abort before websocket handshake completes"
|
||||
stale_request.is_err(),
|
||||
"pending sideband task sent websocket request after close: {:?}",
|
||||
stale_request.ok().map(|request| request.body_json())
|
||||
);
|
||||
|
||||
realtime_server.shutdown().await;
|
||||
@@ -749,11 +767,13 @@ async fn conversation_webrtc_sideband_connect_failure_closes_with_error() -> Res
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
let realtime_base_url = server.uri();
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config.experimental_realtime_ws_backend_prompt = Some("backend prompt".to_string());
|
||||
config.experimental_realtime_ws_model = Some("realtime-test-model".to_string());
|
||||
config.experimental_realtime_ws_startup_context = Some(String::new());
|
||||
config.experimental_realtime_ws_base_url = Some("http://127.0.0.1:1".to_string());
|
||||
config.experimental_realtime_ws_base_url = Some(realtime_base_url);
|
||||
config.model_provider.request_max_retries = Some(0);
|
||||
config.realtime.version = RealtimeWsVersion::V1;
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -102,6 +102,8 @@ async fn websocket_turn_state_persists_within_turn_and_resets_after() -> Result<
|
||||
ev_completed("resp-1"),
|
||||
]],
|
||||
response_headers: vec![(TURN_STATE_HEADER.to_string(), "ts-1".to_string())],
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
accept_delay: None,
|
||||
close_after_requests: true,
|
||||
},
|
||||
@@ -112,6 +114,8 @@ async fn websocket_turn_state_persists_within_turn_and_resets_after() -> Result<
|
||||
ev_completed("resp-2"),
|
||||
]],
|
||||
response_headers: Vec::new(),
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
accept_delay: None,
|
||||
close_after_requests: true,
|
||||
},
|
||||
@@ -122,6 +126,8 @@ async fn websocket_turn_state_persists_within_turn_and_resets_after() -> Result<
|
||||
ev_completed("resp-3"),
|
||||
]],
|
||||
response_headers: Vec::new(),
|
||||
accept_started: None,
|
||||
accept_release: None,
|
||||
accept_delay: None,
|
||||
close_after_requests: true,
|
||||
},
|
||||
|
||||
@@ -52,7 +52,7 @@ Use the separate `codex mcp` subcommand to manage configured MCP server launcher
|
||||
|
||||
Use the v2 thread and turn APIs for all new integrations. `thread/start` creates a thread, `turn/start` submits user input, `turn/interrupt` stops an in-flight turn, and `thread/list` / `thread/read` expose persisted history.
|
||||
|
||||
`getConversationSummary` remains as a compatibility helper for clients that still need a summary lookup by `conversationId` or `rolloutPath`.
|
||||
`getConversationSummary` remains as a compatibility helper for clients that still need a summary lookup by `conversationId` or `rolloutPath`. Lookups by `conversationId` are preferred; lookups by `rolloutPath` won't work with non-local thread stores.
|
||||
|
||||
For complete request and response shapes, see the app-server README and the protocol definitions in `app-server-protocol/src/protocol/v2.rs`.
|
||||
|
||||
|
||||
@@ -372,7 +372,16 @@ async fn wait_for_single_request(mock: &ResponseMock) -> ResponsesRequest {
|
||||
async fn wait_for_file_removed(path: &Path) -> anyhow::Result<()> {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
if !tokio::fs::try_exists(path).await? {
|
||||
let exists = match tokio::fs::try_exists(path).await {
|
||||
Ok(exists) => exists,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
// Windows can transiently deny metadata reads while another task
|
||||
// is removing or resetting files in this workspace.
|
||||
true
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
if !exists {
|
||||
return Ok(());
|
||||
}
|
||||
assert!(
|
||||
|
||||
@@ -227,22 +227,23 @@ WHERE id = ?
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mark_agent_job_completed(&self, job_id: &str) -> anyhow::Result<()> {
|
||||
pub async fn mark_agent_job_completed(&self, job_id: &str) -> anyhow::Result<bool> {
|
||||
let now = Utc::now().timestamp();
|
||||
sqlx::query(
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE agent_jobs
|
||||
SET status = ?, updated_at = ?, completed_at = ?, last_error = NULL
|
||||
WHERE id = ?
|
||||
WHERE id = ? AND status = ?
|
||||
"#,
|
||||
)
|
||||
.bind(AgentJobStatus::Completed.as_str())
|
||||
.bind(now)
|
||||
.bind(now)
|
||||
.bind(job_id)
|
||||
.bind(AgentJobStatus::Running.as_str())
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
Ok(())
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn mark_agent_job_failed(
|
||||
@@ -428,9 +429,46 @@ WHERE job_id = ? AND item_id = ? AND status = ?
|
||||
item_id: &str,
|
||||
reporting_thread_id: &str,
|
||||
result_json: &Value,
|
||||
) -> anyhow::Result<bool> {
|
||||
self.report_agent_job_item_result_inner(
|
||||
job_id,
|
||||
item_id,
|
||||
reporting_thread_id,
|
||||
result_json,
|
||||
/*cancel_job_reason*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn report_agent_job_item_result_and_cancel_job(
|
||||
&self,
|
||||
job_id: &str,
|
||||
item_id: &str,
|
||||
reporting_thread_id: &str,
|
||||
result_json: &Value,
|
||||
cancel_job_reason: &str,
|
||||
) -> anyhow::Result<bool> {
|
||||
self.report_agent_job_item_result_inner(
|
||||
job_id,
|
||||
item_id,
|
||||
reporting_thread_id,
|
||||
result_json,
|
||||
Some(cancel_job_reason),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn report_agent_job_item_result_inner(
|
||||
&self,
|
||||
job_id: &str,
|
||||
item_id: &str,
|
||||
reporting_thread_id: &str,
|
||||
result_json: &Value,
|
||||
cancel_job_reason: Option<&str>,
|
||||
) -> anyhow::Result<bool> {
|
||||
let now = Utc::now().timestamp();
|
||||
let serialized = serde_json::to_string(result_json)?;
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE agent_job_items
|
||||
@@ -446,7 +484,7 @@ WHERE
|
||||
job_id = ?
|
||||
AND item_id = ?
|
||||
AND status = ?
|
||||
AND assigned_thread_id = ?
|
||||
AND (assigned_thread_id = ? OR assigned_thread_id IS NULL)
|
||||
"#,
|
||||
)
|
||||
.bind(AgentJobItemStatus::Completed.as_str())
|
||||
@@ -458,9 +496,29 @@ WHERE
|
||||
.bind(item_id)
|
||||
.bind(AgentJobItemStatus::Running.as_str())
|
||||
.bind(reporting_thread_id)
|
||||
.execute(self.pool.as_ref())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
let accepted = result.rows_affected() > 0;
|
||||
if accepted && let Some(reason) = cancel_job_reason {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE agent_jobs
|
||||
SET status = ?, updated_at = ?, completed_at = ?, last_error = ?
|
||||
WHERE id = ? AND status IN (?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(AgentJobStatus::Cancelled.as_str())
|
||||
.bind(now)
|
||||
.bind(now)
|
||||
.bind(reason)
|
||||
.bind(job_id)
|
||||
.bind(AgentJobStatus::Pending.as_str())
|
||||
.bind(AgentJobStatus::Running.as_str())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(accepted)
|
||||
}
|
||||
|
||||
pub async fn mark_agent_job_item_completed(
|
||||
@@ -652,6 +710,113 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_agent_job_item_result_can_cancel_job_atomically() -> anyhow::Result<()> {
|
||||
let codex_home = unique_temp_dir();
|
||||
let runtime = StateRuntime::init(codex_home, "test-provider".to_string()).await?;
|
||||
let (job_id, item_id, thread_id) = create_running_single_item_job(runtime.as_ref()).await?;
|
||||
|
||||
let accepted = runtime
|
||||
.report_agent_job_item_result_and_cancel_job(
|
||||
job_id.as_str(),
|
||||
item_id.as_str(),
|
||||
thread_id.as_str(),
|
||||
&json!({"ok": true}),
|
||||
"cancelled by worker request",
|
||||
)
|
||||
.await?;
|
||||
assert!(accepted);
|
||||
|
||||
let job = runtime
|
||||
.get_agent_job(job_id.as_str())
|
||||
.await?
|
||||
.expect("job should exist");
|
||||
assert_eq!(job.status, AgentJobStatus::Cancelled);
|
||||
assert_eq!(
|
||||
job.last_error,
|
||||
Some("cancelled by worker request".to_string())
|
||||
);
|
||||
|
||||
let item = runtime
|
||||
.get_agent_job_item(job_id.as_str(), item_id.as_str())
|
||||
.await?
|
||||
.expect("job item should exist");
|
||||
assert_eq!(item.status, AgentJobItemStatus::Completed);
|
||||
assert_eq!(item.result_json, Some(json!({"ok": true})));
|
||||
assert_eq!(item.assigned_thread_id, None);
|
||||
|
||||
let completed = runtime.mark_agent_job_completed(job_id.as_str()).await?;
|
||||
assert!(!completed);
|
||||
let job = runtime
|
||||
.get_agent_job(job_id.as_str())
|
||||
.await?
|
||||
.expect("job should exist");
|
||||
assert_eq!(job.status, AgentJobStatus::Cancelled);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_agent_job_item_result_accepts_unassigned_running_item() -> anyhow::Result<()> {
|
||||
let codex_home = unique_temp_dir();
|
||||
let runtime = StateRuntime::init(codex_home, "test-provider".to_string()).await?;
|
||||
let job_id = "job-1".to_string();
|
||||
let item_id = "item-1".to_string();
|
||||
let thread_id = "thread-1".to_string();
|
||||
runtime
|
||||
.create_agent_job(
|
||||
&AgentJobCreateParams {
|
||||
id: job_id.clone(),
|
||||
name: "test-job".to_string(),
|
||||
instruction: "Return a result".to_string(),
|
||||
auto_export: true,
|
||||
max_runtime_seconds: None,
|
||||
output_schema_json: None,
|
||||
input_headers: vec!["path".to_string()],
|
||||
input_csv_path: "/tmp/in.csv".to_string(),
|
||||
output_csv_path: "/tmp/out.csv".to_string(),
|
||||
},
|
||||
&[AgentJobItemCreateParams {
|
||||
item_id: item_id.clone(),
|
||||
row_index: 0,
|
||||
source_id: None,
|
||||
row_json: json!({"path":"file-1"}),
|
||||
}],
|
||||
)
|
||||
.await?;
|
||||
runtime.mark_agent_job_running(job_id.as_str()).await?;
|
||||
let marked_running = runtime
|
||||
.mark_agent_job_item_running(job_id.as_str(), item_id.as_str())
|
||||
.await?;
|
||||
assert!(marked_running);
|
||||
|
||||
let accepted = runtime
|
||||
.report_agent_job_item_result_and_cancel_job(
|
||||
job_id.as_str(),
|
||||
item_id.as_str(),
|
||||
thread_id.as_str(),
|
||||
&json!({"ok": true}),
|
||||
"cancelled by worker request",
|
||||
)
|
||||
.await?;
|
||||
assert!(accepted);
|
||||
|
||||
let job = runtime
|
||||
.get_agent_job(job_id.as_str())
|
||||
.await?
|
||||
.expect("job should exist");
|
||||
assert_eq!(job.status, AgentJobStatus::Cancelled);
|
||||
let item = runtime
|
||||
.get_agent_job_item(job_id.as_str(), item_id.as_str())
|
||||
.await?
|
||||
.expect("job item should exist");
|
||||
assert_eq!(item.status, AgentJobItemStatus::Completed);
|
||||
assert_eq!(item.result_json, Some(json!({"ok": true})));
|
||||
assert_eq!(item.assigned_thread_id, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_agent_job_item_result_rejects_late_reports() -> anyhow::Result<()> {
|
||||
let codex_home = unique_temp_dir();
|
||||
|
||||
@@ -256,10 +256,16 @@ fn stored_thread_from_state(
|
||||
items: history_items.clone(),
|
||||
});
|
||||
let name = state.names.get(&thread_id).cloned().flatten();
|
||||
let rollout_path = state
|
||||
.rollout_paths
|
||||
.iter()
|
||||
.find_map(|(path, mapped_thread_id)| {
|
||||
(*mapped_thread_id == thread_id).then(|| path.clone())
|
||||
});
|
||||
|
||||
Ok(StoredThread {
|
||||
thread_id,
|
||||
rollout_path: None,
|
||||
rollout_path,
|
||||
forked_from_id: created.forked_from_id,
|
||||
preview: String::new(),
|
||||
name,
|
||||
|
||||
@@ -69,6 +69,8 @@ const DENY_ACCESS: i32 = 3;
|
||||
|
||||
mod read_acl_mutex;
|
||||
mod sandbox_users;
|
||||
#[path = "setup_runtime_bin.rs"]
|
||||
mod setup_runtime_bin;
|
||||
use read_acl_mutex::acquire_read_acl_mutex;
|
||||
use read_acl_mutex::read_acl_mutex_exists;
|
||||
use sandbox_users::provision_sandbox_users;
|
||||
@@ -510,8 +512,7 @@ fn run_read_acl_only(payload: &Payload, log: &mut File) -> Result<()> {
|
||||
|
||||
fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<()> {
|
||||
let refresh_only = payload.refresh_only;
|
||||
if refresh_only {
|
||||
} else {
|
||||
if !refresh_only {
|
||||
let provision_result = provision_sandbox_users(
|
||||
&payload.codex_home,
|
||||
&payload.offline_username,
|
||||
@@ -647,6 +648,14 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
}
|
||||
}
|
||||
|
||||
if refresh_only {
|
||||
setup_runtime_bin::ensure_codex_app_runtime_bin_readable(
|
||||
sandbox_group_psid,
|
||||
&mut refresh_errors,
|
||||
log,
|
||||
)?;
|
||||
}
|
||||
|
||||
let cap_sid_str = caps.workspace;
|
||||
let sandbox_group_sid_str =
|
||||
string_from_sid_bytes(&sandbox_group_sid).map_err(anyhow::Error::msg)?;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user