mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Merge 75f47a060e into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -447,6 +447,24 @@ jobs:
|
||||
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
|
||||
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
|
||||
name: Configure musl rusty_v8 artifact overrides
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)"
|
||||
release_tag="rusty-v8-v${version}"
|
||||
base_url="https://github.com/openai/codex/releases/download/${release_tag}"
|
||||
archive="https://github.com/openai/codex/releases/download/rusty-v8-v${version}/librusty_v8_release_${TARGET}.a.gz"
|
||||
binding_dir="${RUNNER_TEMP}/rusty_v8"
|
||||
binding_path="${binding_dir}/src_binding_release_${TARGET}.rs"
|
||||
mkdir -p "${binding_dir}"
|
||||
curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}"
|
||||
echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV"
|
||||
echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install cargo-chef
|
||||
if: ${{ matrix.profile == 'release' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
|
||||
@@ -210,6 +210,24 @@ jobs:
|
||||
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
|
||||
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
|
||||
name: Configure musl rusty_v8 artifact overrides
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)"
|
||||
release_tag="rusty-v8-v${version}"
|
||||
base_url="https://github.com/openai/codex/releases/download/${release_tag}"
|
||||
archive="https://github.com/openai/codex/releases/download/rusty-v8-v${version}/librusty_v8_release_${TARGET}.a.gz"
|
||||
binding_dir="${RUNNER_TEMP}/rusty_v8"
|
||||
binding_path="${binding_dir}/src_binding_release_${TARGET}.rs"
|
||||
mkdir -p "${binding_dir}"
|
||||
curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}"
|
||||
echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV"
|
||||
echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cargo build
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
+107
@@ -144,6 +144,33 @@ crate.annotation(
|
||||
)
|
||||
|
||||
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
|
||||
new_local_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "new_local_repository")
|
||||
|
||||
new_local_repository(
|
||||
name = "v8_targets",
|
||||
build_file = "//third_party/v8:BUILD.bazel",
|
||||
path = "third_party/v8",
|
||||
)
|
||||
|
||||
crate.annotation(
|
||||
build_script_data = [
|
||||
"@v8_targets//:rusty_v8_archive_for_target",
|
||||
"@v8_targets//:rusty_v8_binding_for_target",
|
||||
],
|
||||
build_script_env = {
|
||||
"RUSTY_V8_ARCHIVE": "$(execpath @v8_targets//:rusty_v8_archive_for_target)",
|
||||
"RUSTY_V8_SRC_BINDING_PATH": "$(execpath @v8_targets//:rusty_v8_binding_for_target)",
|
||||
},
|
||||
crate = "v8",
|
||||
gen_build_script = "on",
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
"//patches:rusty_v8_prebuilt_out_dir.patch",
|
||||
],
|
||||
)
|
||||
|
||||
inject_repo(crate, "v8_targets")
|
||||
|
||||
llvm = use_extension("@llvm//extensions:llvm.bzl", "llvm")
|
||||
use_repo(llvm, "llvm-project")
|
||||
@@ -210,6 +237,86 @@ http_archive(
|
||||
urls = ["https://static.crates.io/crates/v8/v8-146.4.0.crate"],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_aarch64_apple_darwin_archive",
|
||||
downloaded_file_path = "librusty_v8_release_aarch64-apple-darwin.a.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_aarch64-apple-darwin.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_aarch64_unknown_linux_gnu_archive",
|
||||
downloaded_file_path = "librusty_v8_release_aarch64-unknown-linux-gnu.a.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_aarch64-unknown-linux-gnu.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_aarch64_pc_windows_msvc_archive",
|
||||
downloaded_file_path = "rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_x86_64_apple_darwin_archive",
|
||||
downloaded_file_path = "librusty_v8_release_x86_64-apple-darwin.a.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_x86_64-apple-darwin.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive",
|
||||
downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_x86_64_pc_windows_msvc_archive",
|
||||
downloaded_file_path = "rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_aarch64_unknown_linux_musl_archive",
|
||||
downloaded_file_path = "librusty_v8_release_aarch64-unknown-linux-musl.a.gz",
|
||||
urls = [
|
||||
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/librusty_v8_release_aarch64-unknown-linux-musl.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_aarch64_unknown_linux_musl_binding",
|
||||
downloaded_file_path = "src_binding_release_aarch64-unknown-linux-musl.rs",
|
||||
urls = [
|
||||
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/src_binding_release_aarch64-unknown-linux-musl.rs",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_x86_64_unknown_linux_musl_archive",
|
||||
downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-musl.a.gz",
|
||||
urls = [
|
||||
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/librusty_v8_release_x86_64-unknown-linux-musl.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_x86_64_unknown_linux_musl_binding",
|
||||
downloaded_file_path = "src_binding_release_x86_64-unknown-linux-musl.rs",
|
||||
urls = [
|
||||
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/src_binding_release_x86_64-unknown-linux-musl.rs",
|
||||
],
|
||||
)
|
||||
|
||||
use_repo(crate, "crates")
|
||||
|
||||
bazel_dep(name = "libcap", version = "2.27.bcr.1")
|
||||
|
||||
Generated
+21
-2
File diff suppressed because one or more lines are too long
Generated
+235
-11
@@ -800,9 +800,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.15.4"
|
||||
version = "1.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
|
||||
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"untrusted 0.7.1",
|
||||
@@ -811,9 +811,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.37.0"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
|
||||
checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
@@ -949,6 +949,8 @@ dependencies = [
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
@@ -1152,6 +1154,16 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
|
||||
|
||||
[[package]]
|
||||
name = "calendrical_calculations"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7"
|
||||
dependencies = [
|
||||
"core_maths",
|
||||
"displaydoc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
@@ -1584,7 +1596,7 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"url",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
"wiremock",
|
||||
"zip",
|
||||
]
|
||||
@@ -1930,7 +1942,7 @@ dependencies = [
|
||||
"url",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
"wildmatch",
|
||||
"windows-sys 0.52.0",
|
||||
"wiremock",
|
||||
@@ -2179,7 +2191,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
@@ -2432,7 +2444,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"urlencoding",
|
||||
"webbrowser",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2472,7 +2484,7 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"url",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2644,7 +2656,7 @@ dependencies = [
|
||||
"uuid",
|
||||
"vt100",
|
||||
"webbrowser",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
"windows-sys 0.52.0",
|
||||
"winsplit",
|
||||
]
|
||||
@@ -2736,7 +2748,7 @@ dependencies = [
|
||||
"uuid",
|
||||
"vt100",
|
||||
"webbrowser",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
"windows-sys 0.52.0",
|
||||
"winsplit",
|
||||
]
|
||||
@@ -2907,6 +2919,14 @@ dependencies = [
|
||||
"regex-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-v8-poc"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"pretty_assertions",
|
||||
"v8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-windows-sandbox"
|
||||
version = "0.0.0"
|
||||
@@ -3112,6 +3132,15 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core_maths"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core_test_support"
|
||||
version = "0.0.0"
|
||||
@@ -3717,6 +3746,38 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diplomat"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9adb46b05e2f53dcf6a7dfc242e4ce9eb60c369b6b6eb10826a01e93167f59c6"
|
||||
dependencies = [
|
||||
"diplomat_core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diplomat-runtime"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0569bd3caaf13829da7ee4e83dbf9197a0e1ecd72772da6d08f0b4c9285c8d29"
|
||||
|
||||
[[package]]
|
||||
name = "diplomat_core"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51731530ed7f2d4495019abc7df3744f53338e69e2863a6a64ae91821c763df1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"strck",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
@@ -4323,6 +4384,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fslock"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@@ -4551,6 +4622,15 @@ dependencies = [
|
||||
"regex-syntax 0.8.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gzip-header"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
@@ -5008,6 +5088,28 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_calendar"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e"
|
||||
dependencies = [
|
||||
"calendrical_calculations",
|
||||
"displaydoc",
|
||||
"icu_calendar_data",
|
||||
"icu_locale",
|
||||
"icu_locale_core",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_calendar_data"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
@@ -5442,6 +5544,12 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "ixdtf"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.18"
|
||||
@@ -7167,6 +7275,16 @@ dependencies = [
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.4.0"
|
||||
@@ -8000,6 +8118,16 @@ dependencies = [
|
||||
"webpki-roots 1.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resb"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a067ab3b5ca3b4dc307d0de9cf75f9f5e6ca9717b192b2f28a36c83e5de9e76"
|
||||
dependencies = [
|
||||
"potential_utf",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resolv-conf"
|
||||
version = "0.7.6"
|
||||
@@ -9380,6 +9508,15 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strck"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42316e70da376f3d113a68d138a60d8a9883c604fe97942721ec2068dab13a9f"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "streaming-iterator"
|
||||
version = "0.1.9"
|
||||
@@ -9624,6 +9761,39 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "temporal_capi"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a151e402c2bdb6a3a2a2f3f225eddaead2e7ce7dd5d3fa2090deb11b17aa4ed8"
|
||||
dependencies = [
|
||||
"diplomat",
|
||||
"diplomat-runtime",
|
||||
"icu_calendar",
|
||||
"icu_locale",
|
||||
"num-traits",
|
||||
"temporal_rs",
|
||||
"timezone_provider",
|
||||
"writeable",
|
||||
"zoneinfo64",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "temporal_rs"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88afde3bd75d2fc68d77a914bece426aa08aa7649ffd0cdd4a11c3d4d33474d1"
|
||||
dependencies = [
|
||||
"core_maths",
|
||||
"icu_calendar",
|
||||
"icu_locale",
|
||||
"ixdtf",
|
||||
"num-traits",
|
||||
"timezone_provider",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term"
|
||||
version = "0.7.0"
|
||||
@@ -9831,6 +10001,18 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "timezone_provider"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9ba0000e9e73862f3e7ca1ff159e2ddf915c9d8bb11e38a7874760f445d993"
|
||||
dependencies = [
|
||||
"tinystr",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
"zoneinfo64",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
@@ -10630,6 +10812,23 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "v8"
|
||||
version = "146.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d97bcac5cdc5a195a4813f1855a6bc658f240452aac36caa12fd6c6f16026ab1"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"bitflags 2.10.0",
|
||||
"fslock",
|
||||
"gzip-header",
|
||||
"home",
|
||||
"miniz_oxide",
|
||||
"paste",
|
||||
"temporal_capi",
|
||||
"which 6.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -10929,6 +11128,18 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "6.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"rustix 0.38.44",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "8.0.0"
|
||||
@@ -11932,6 +12143,19 @@ version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||
|
||||
[[package]]
|
||||
name = "zoneinfo64"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb2e5597efbe7c421da8a7fd396b20b571704e787c21a272eecf35dfe9d386f0"
|
||||
dependencies = [
|
||||
"calendrical_calculations",
|
||||
"icu_locale_core",
|
||||
"potential_utf",
|
||||
"resb",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
|
||||
+5
-1
@@ -45,6 +45,7 @@ members = [
|
||||
"otel",
|
||||
"tui",
|
||||
"tui_app_server",
|
||||
"v8-poc",
|
||||
"utils/absolute-path",
|
||||
"utils/cargo-bin",
|
||||
"utils/git",
|
||||
@@ -137,6 +138,7 @@ codex-test-macros = { path = "test-macros" }
|
||||
codex-terminal-detection = { path = "terminal-detection" }
|
||||
codex-tui = { path = "tui" }
|
||||
codex-tui-app-server = { path = "tui_app_server" }
|
||||
codex-v8-poc = { path = "v8-poc" }
|
||||
codex-utils-absolute-path = { path = "utils/absolute-path" }
|
||||
codex-utils-approval-presets = { path = "utils/approval-presets" }
|
||||
codex-utils-cache = { path = "utils/cache" }
|
||||
@@ -245,6 +247,7 @@ regex-lite = "0.1.8"
|
||||
reqwest = "0.12"
|
||||
rmcp = { version = "0.15.0", default-features = false }
|
||||
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
|
||||
v8 = "=146.4.0"
|
||||
rustls = { version = "0.23", default-features = false, features = [
|
||||
"ring",
|
||||
"std",
|
||||
@@ -370,7 +373,8 @@ ignored = [
|
||||
"icu_provider",
|
||||
"openssl-sys",
|
||||
"codex-utils-readiness",
|
||||
"codex-secrets"
|
||||
"codex-secrets",
|
||||
"codex-v8-poc"
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"AppBranding": {
|
||||
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
|
||||
"properties": {
|
||||
@@ -1999,6 +2002,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -4899,6 +4899,9 @@
|
||||
"title": "AgentMessageDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"AnalyticsConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
@@ -11515,6 +11518,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -139,6 +139,9 @@
|
||||
"title": "AgentMessageDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"AnalyticsConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
@@ -9275,6 +9278,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"ApprovalsReviewer": {
|
||||
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.",
|
||||
"enum": [
|
||||
@@ -900,6 +903,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -658,6 +661,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -658,6 +661,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -658,6 +661,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"ApprovalsReviewer": {
|
||||
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.",
|
||||
"enum": [
|
||||
@@ -900,6 +903,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -658,6 +661,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"ApprovalsReviewer": {
|
||||
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.",
|
||||
"enum": [
|
||||
@@ -900,6 +903,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -658,6 +661,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -658,6 +661,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentPath"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AgentPath = string;
|
||||
@@ -1,6 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AgentPath } from "./AgentPath";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type SubAgentSource = "review" | "compact" | { "thread_spawn": { parent_thread_id: ThreadId, depth: number, agent_nickname: string | null, agent_role: string | null, } } | "memory_consolidation" | { "other": string };
|
||||
export type SubAgentSource = "review" | "compact" | { "thread_spawn": { parent_thread_id: ThreadId, depth: number, agent_path: AgentPath | null, agent_nickname: string | null, agent_role: string | null, } } | "memory_consolidation" | { "other": string };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
export type { AbsolutePathBuf } from "./AbsolutePathBuf";
|
||||
export type { AgentPath } from "./AgentPath";
|
||||
export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
|
||||
export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse";
|
||||
export type { AuthMode } from "./AuthMode";
|
||||
|
||||
@@ -8240,6 +8240,7 @@ fn with_thread_spawn_agent_metadata(
|
||||
codex_protocol::protocol::SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_path,
|
||||
agent_nickname: existing_agent_nickname,
|
||||
agent_role: existing_agent_role,
|
||||
},
|
||||
@@ -8247,6 +8248,7 @@ fn with_thread_spawn_agent_metadata(
|
||||
codex_protocol::protocol::SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_path,
|
||||
agent_nickname: agent_nickname.or(existing_agent_nickname),
|
||||
agent_role: agent_role.or(existing_agent_role),
|
||||
},
|
||||
@@ -8793,6 +8795,7 @@ mod tests {
|
||||
source: SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
@@ -8885,6 +8888,7 @@ mod tests {
|
||||
serde_json::to_string(&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: ThreadId::from_string("ad7f0408-99b8-4f6e-a46f-bd0eec433370")?,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}))?;
|
||||
|
||||
@@ -133,6 +133,7 @@ mod tests {
|
||||
let spawn = CoreSessionSource::SubAgent(CoreSubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
|
||||
@@ -79,6 +79,7 @@ pub fn create_fake_rollout_with_source(
|
||||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
@@ -161,6 +162,7 @@ pub fn create_fake_rollout_with_text_elements(
|
||||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
|
||||
@@ -661,6 +661,7 @@ async fn thread_list_filters_by_source_kind_subagent_thread_spawn() -> Result<()
|
||||
CoreSessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
@@ -724,6 +725,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
|
||||
CoreSessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
|
||||
@@ -322,6 +322,7 @@ stream_max_retries = 0
|
||||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: RolloutSessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("mock_provider".to_string()),
|
||||
|
||||
@@ -422,6 +422,9 @@
|
||||
"multi_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"multi_agent_v2": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"personality": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -2028,6 +2031,9 @@
|
||||
"multi_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"multi_agent_v2": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"personality": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use codex_protocol::ThreadId;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Resolves a single tool-facing agent target to a thread id.
|
||||
pub(crate) async fn resolve_agent_target(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
target: &str,
|
||||
) -> Result<ThreadId, FunctionCallError> {
|
||||
register_session_root(session, turn);
|
||||
if let Ok(thread_id) = ThreadId::from_string(target) {
|
||||
return Ok(thread_id);
|
||||
}
|
||||
|
||||
session
|
||||
.services
|
||||
.agent_control
|
||||
.resolve_agent_reference(session.conversation_id, &turn.session_source, target)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
crate::error::CodexErr::UnsupportedOperation(message) => {
|
||||
FunctionCallError::RespondToModel(message)
|
||||
}
|
||||
other => FunctionCallError::RespondToModel(other.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolves multiple tool-facing agent targets to thread ids.
|
||||
pub(crate) async fn resolve_agent_targets(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
targets: Vec<String>,
|
||||
) -> Result<Vec<ThreadId>, FunctionCallError> {
|
||||
if targets.is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"agent targets must be non-empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut resolved = Vec::with_capacity(targets.len());
|
||||
for target in &targets {
|
||||
resolved.push(resolve_agent_target(session, turn, target).await?);
|
||||
}
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn register_session_root(session: &Arc<Session>, turn: &Arc<TurnContext>) {
|
||||
session
|
||||
.services
|
||||
.agent_control
|
||||
.register_session_root(session.conversation_id, &turn.session_source);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::agent::AgentStatus;
|
||||
use crate::agent::guards::AgentMetadata;
|
||||
use crate::agent::guards::Guards;
|
||||
use crate::agent::role::DEFAULT_ROLE_NAME;
|
||||
use crate::agent::role::resolve_role_config;
|
||||
@@ -15,6 +16,7 @@ use crate::shell_snapshot::ShellSnapshot;
|
||||
use crate::state_db;
|
||||
use crate::thread_manager::ThreadManagerState;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -41,6 +43,13 @@ pub(crate) struct SpawnAgentOptions {
|
||||
pub(crate) fork_parent_spawn_call_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct LiveAgent {
|
||||
pub(crate) thread_id: ThreadId,
|
||||
pub(crate) metadata: AgentMetadata,
|
||||
pub(crate) status: AgentStatus,
|
||||
}
|
||||
|
||||
fn default_agent_nickname_list() -> Vec<&'static str> {
|
||||
AGENT_NAMES
|
||||
.lines()
|
||||
@@ -69,9 +78,9 @@ fn agent_nickname_candidates(
|
||||
/// Control-plane handle for multi-agent operations.
|
||||
/// `AgentControl` is held by each session (via `SessionServices`). It provides capability to
|
||||
/// spawn new agents and the inter-agent communication layer.
|
||||
/// An `AgentControl` instance is shared per "user session" which means the same `AgentControl`
|
||||
/// is used for every sub-agent spawned by Codex. By doing so, we make sure the guards are
|
||||
/// scoped to a user session.
|
||||
/// An `AgentControl` instance is intended to be created at most once per root thread/session
|
||||
/// tree. That same `AgentControl` is then shared with every sub-agent spawned from that root,
|
||||
/// which keeps the guards scoped to that root thread rather than the entire `ThreadManager`.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct AgentControl {
|
||||
/// Weak handle back to the global thread registry/state.
|
||||
@@ -97,17 +106,30 @@ impl AgentControl {
|
||||
items: Vec<UserInput>,
|
||||
session_source: Option<SessionSource>,
|
||||
) -> CodexResult<ThreadId> {
|
||||
self.spawn_agent_with_options(config, items, session_source, SpawnAgentOptions::default())
|
||||
.await
|
||||
Ok(self
|
||||
.spawn_agent_internal(config, items, session_source, SpawnAgentOptions::default())
|
||||
.await?
|
||||
.thread_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn spawn_agent_with_options(
|
||||
pub(crate) async fn spawn_agent_with_metadata(
|
||||
&self,
|
||||
config: crate::config::Config,
|
||||
items: Vec<UserInput>,
|
||||
session_source: Option<SessionSource>,
|
||||
options: SpawnAgentOptions,
|
||||
) -> CodexResult<ThreadId> {
|
||||
) -> CodexResult<LiveAgent> {
|
||||
self.spawn_agent_internal(config, items, session_source, options)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn spawn_agent_internal(
|
||||
&self,
|
||||
config: crate::config::Config,
|
||||
items: Vec<UserInput>,
|
||||
session_source: Option<SessionSource>,
|
||||
options: SpawnAgentOptions,
|
||||
) -> CodexResult<LiveAgent> {
|
||||
let state = self.upgrade()?;
|
||||
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let inherited_shell_snapshot = self
|
||||
@@ -116,25 +138,26 @@ impl AgentControl {
|
||||
let inherited_exec_policy = self
|
||||
.inherited_exec_policy_for_source(&state, session_source.as_ref(), &config)
|
||||
.await;
|
||||
let session_source = match session_source {
|
||||
let (session_source, mut agent_metadata) = match session_source {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_path,
|
||||
agent_role,
|
||||
..
|
||||
})) => {
|
||||
let candidate_names = agent_nickname_candidates(&config, agent_role.as_deref());
|
||||
let candidate_name_refs: Vec<&str> =
|
||||
candidate_names.iter().map(String::as_str).collect();
|
||||
let agent_nickname = reservation.reserve_agent_nickname(&candidate_name_refs)?;
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
let (session_source, agent_metadata) = self.prepare_thread_spawn(
|
||||
&mut reservation,
|
||||
&config,
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: Some(agent_nickname),
|
||||
agent_path,
|
||||
agent_role,
|
||||
}))
|
||||
/*preferred_agent_nickname*/ None,
|
||||
)?;
|
||||
(Some(session_source), agent_metadata)
|
||||
}
|
||||
other => other,
|
||||
other => (other, AgentMetadata::default()),
|
||||
};
|
||||
let notification_source = session_source.clone();
|
||||
|
||||
@@ -217,7 +240,8 @@ impl AgentControl {
|
||||
}
|
||||
None => state.spawn_new_thread(config, self.clone()).await?,
|
||||
};
|
||||
reservation.commit(new_thread.thread_id);
|
||||
agent_metadata.agent_id = Some(new_thread.thread_id);
|
||||
reservation.commit(agent_metadata.clone());
|
||||
|
||||
// Notify a new thread has been created. This notification will be processed by clients
|
||||
// to subscribe or drain this newly created thread.
|
||||
@@ -232,9 +256,22 @@ impl AgentControl {
|
||||
.await;
|
||||
|
||||
self.send_input(new_thread.thread_id, items).await?;
|
||||
self.maybe_start_completion_watcher(new_thread.thread_id, notification_source);
|
||||
let child_reference = agent_metadata
|
||||
.agent_path
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| new_thread.thread_id.to_string());
|
||||
self.maybe_start_completion_watcher(
|
||||
new_thread.thread_id,
|
||||
notification_source,
|
||||
child_reference,
|
||||
);
|
||||
|
||||
Ok(new_thread.thread_id)
|
||||
Ok(LiveAgent {
|
||||
thread_id: new_thread.thread_id,
|
||||
metadata: agent_metadata,
|
||||
status: self.get_status(new_thread.thread_id).await,
|
||||
})
|
||||
}
|
||||
|
||||
/// Resume an existing agent thread from a recorded rollout file.
|
||||
@@ -283,6 +320,7 @@ impl AgentControl {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: child_depth,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
@@ -324,14 +362,14 @@ impl AgentControl {
|
||||
}
|
||||
let state = self.upgrade()?;
|
||||
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let session_source = match session_source {
|
||||
let (session_source, agent_metadata) = match session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
..
|
||||
agent_path,
|
||||
agent_role: _,
|
||||
agent_nickname: _,
|
||||
}) => {
|
||||
// Collab resume callers rebuild a placeholder ThreadSpawn source. Rehydrate the
|
||||
// stored nickname/role from sqlite when available; otherwise leave both unset.
|
||||
let (resumed_agent_nickname, resumed_agent_role) =
|
||||
if let Some(state_db_ctx) = state_db::get_state_db(&config).await {
|
||||
match state_db_ctx.get_thread(thread_id).await {
|
||||
@@ -341,27 +379,17 @@ impl AgentControl {
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let reserved_agent_nickname = resumed_agent_nickname
|
||||
.as_deref()
|
||||
.map(|agent_nickname| {
|
||||
let candidate_names =
|
||||
agent_nickname_candidates(&config, resumed_agent_role.as_deref());
|
||||
let candidate_name_refs: Vec<&str> =
|
||||
candidate_names.iter().map(String::as_str).collect();
|
||||
reservation.reserve_agent_nickname_with_preference(
|
||||
&candidate_name_refs,
|
||||
Some(agent_nickname),
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
self.prepare_thread_spawn(
|
||||
&mut reservation,
|
||||
&config,
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: reserved_agent_nickname,
|
||||
agent_role: resumed_agent_role,
|
||||
})
|
||||
agent_path,
|
||||
resumed_agent_role,
|
||||
resumed_agent_nickname,
|
||||
)?
|
||||
}
|
||||
other => other,
|
||||
other => (other, AgentMetadata::default()),
|
||||
};
|
||||
let notification_source = session_source.clone();
|
||||
let inherited_shell_snapshot = self
|
||||
@@ -393,13 +421,21 @@ impl AgentControl {
|
||||
inherited_exec_policy,
|
||||
)
|
||||
.await?;
|
||||
reservation.commit(resumed_thread.thread_id);
|
||||
let mut agent_metadata = agent_metadata;
|
||||
agent_metadata.agent_id = Some(resumed_thread.thread_id);
|
||||
reservation.commit(agent_metadata.clone());
|
||||
// Resumed threads are re-registered in-memory and need the same listener
|
||||
// attachment path as freshly spawned threads.
|
||||
state.notify_thread_created(resumed_thread.thread_id);
|
||||
let child_reference = agent_metadata
|
||||
.agent_path
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| resumed_thread.thread_id.to_string());
|
||||
self.maybe_start_completion_watcher(
|
||||
resumed_thread.thread_id,
|
||||
Some(notification_source.clone()),
|
||||
child_reference,
|
||||
);
|
||||
self.persist_thread_spawn_edge_for_source(
|
||||
resumed_thread.thread.as_ref(),
|
||||
@@ -500,21 +536,18 @@ impl AgentControl {
|
||||
thread.agent_status().await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_agent_nickname_and_role(
|
||||
pub(crate) fn register_session_root(
|
||||
&self,
|
||||
agent_id: ThreadId,
|
||||
) -> Option<(Option<String>, Option<String>)> {
|
||||
let Ok(state) = self.upgrade() else {
|
||||
return None;
|
||||
};
|
||||
let Ok(thread) = state.get_thread(agent_id).await else {
|
||||
return None;
|
||||
};
|
||||
let session_source = thread.config_snapshot().await.session_source;
|
||||
Some((
|
||||
session_source.get_nickname(),
|
||||
session_source.get_agent_role(),
|
||||
))
|
||||
current_thread_id: ThreadId,
|
||||
current_session_source: &SessionSource,
|
||||
) {
|
||||
if thread_spawn_parent_thread_id(current_session_source).is_none() {
|
||||
self.state.register_root_thread(current_thread_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_agent_metadata(&self, agent_id: ThreadId) -> Option<AgentMetadata> {
|
||||
self.state.agent_metadata_for_thread(agent_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_agent_config_snapshot(
|
||||
@@ -530,6 +563,33 @@ impl AgentControl {
|
||||
Some(thread.config_snapshot().await)
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_agent_reference(
|
||||
&self,
|
||||
_current_thread_id: ThreadId,
|
||||
current_session_source: &SessionSource,
|
||||
agent_reference: &str,
|
||||
) -> CodexResult<ThreadId> {
|
||||
let current_agent_path = current_session_source
|
||||
.get_agent_path()
|
||||
.unwrap_or_else(AgentPath::root);
|
||||
let agent_path = current_agent_path
|
||||
.resolve(agent_reference)
|
||||
.map_err(CodexErr::UnsupportedOperation)?;
|
||||
if agent_path.is_root() {
|
||||
return Err(CodexErr::UnsupportedOperation(
|
||||
"root is not a spawned agent".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(thread_id) = self.state.agent_id_for_path(&agent_path) {
|
||||
return Ok(thread_id);
|
||||
}
|
||||
Err(CodexErr::UnsupportedOperation(format!(
|
||||
"live agent path `{}` not found",
|
||||
agent_path.as_str()
|
||||
)))
|
||||
}
|
||||
|
||||
/// Subscribe to status updates for `agent_id`, yielding the latest value and changes.
|
||||
pub(crate) async fn subscribe_status(
|
||||
&self,
|
||||
@@ -560,8 +620,13 @@ impl AgentControl {
|
||||
|
||||
agents
|
||||
.into_iter()
|
||||
.map(|(thread_id, nickname)| {
|
||||
format_subagent_context_line(&thread_id.to_string(), nickname.as_deref())
|
||||
.map(|(thread_id, metadata)| {
|
||||
let reference = metadata
|
||||
.agent_path
|
||||
.as_ref()
|
||||
.map(|agent_path| agent_path.name().to_string())
|
||||
.unwrap_or_else(|| thread_id.to_string());
|
||||
format_subagent_context_line(reference.as_str(), metadata.agent_nickname.as_deref())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
@@ -575,6 +640,7 @@ impl AgentControl {
|
||||
&self,
|
||||
child_thread_id: ThreadId,
|
||||
session_source: Option<SessionSource>,
|
||||
child_reference: String,
|
||||
) {
|
||||
let Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id, ..
|
||||
@@ -610,13 +676,52 @@ impl AgentControl {
|
||||
};
|
||||
parent_thread
|
||||
.inject_user_message_without_turn(format_subagent_notification_message(
|
||||
&child_thread_id.to_string(),
|
||||
child_reference.as_str(),
|
||||
&status,
|
||||
))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepare_thread_spawn(
|
||||
&self,
|
||||
reservation: &mut crate::agent::guards::SpawnReservation,
|
||||
config: &crate::config::Config,
|
||||
parent_thread_id: ThreadId,
|
||||
depth: i32,
|
||||
agent_path: Option<AgentPath>,
|
||||
agent_role: Option<String>,
|
||||
preferred_agent_nickname: Option<String>,
|
||||
) -> CodexResult<(SessionSource, AgentMetadata)> {
|
||||
if depth == 1 {
|
||||
self.state.register_root_thread(parent_thread_id);
|
||||
}
|
||||
if let Some(agent_path) = agent_path.as_ref() {
|
||||
reservation.reserve_agent_path(agent_path)?;
|
||||
}
|
||||
let candidate_names = agent_nickname_candidates(config, agent_role.as_deref());
|
||||
let candidate_name_refs: Vec<&str> = candidate_names.iter().map(String::as_str).collect();
|
||||
let agent_nickname = Some(reservation.reserve_agent_nickname_with_preference(
|
||||
&candidate_name_refs,
|
||||
preferred_agent_nickname.as_deref(),
|
||||
)?);
|
||||
let session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_path: agent_path.clone(),
|
||||
agent_nickname: agent_nickname.clone(),
|
||||
agent_role: agent_role.clone(),
|
||||
});
|
||||
let agent_metadata = AgentMetadata {
|
||||
agent_id: None,
|
||||
agent_path,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
};
|
||||
Ok((session_source, agent_metadata))
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> CodexResult<Arc<ThreadManagerState>> {
|
||||
self.manager
|
||||
.upgrade()
|
||||
@@ -666,7 +771,7 @@ impl AgentControl {
|
||||
async fn open_thread_spawn_children(
|
||||
&self,
|
||||
parent_thread_id: ThreadId,
|
||||
) -> CodexResult<Vec<(ThreadId, Option<String>)>> {
|
||||
) -> CodexResult<Vec<(ThreadId, AgentMetadata)>> {
|
||||
let mut children_by_parent = self.live_thread_spawn_children().await?;
|
||||
Ok(children_by_parent
|
||||
.remove(&parent_thread_id)
|
||||
@@ -675,9 +780,9 @@ impl AgentControl {
|
||||
|
||||
async fn live_thread_spawn_children(
|
||||
&self,
|
||||
) -> CodexResult<HashMap<ThreadId, Vec<(ThreadId, Option<String>)>>> {
|
||||
) -> CodexResult<HashMap<ThreadId, Vec<(ThreadId, AgentMetadata)>>> {
|
||||
let state = self.upgrade()?;
|
||||
let mut children_by_parent = HashMap::<ThreadId, Vec<(ThreadId, Option<String>)>>::new();
|
||||
let mut children_by_parent = HashMap::<ThreadId, Vec<(ThreadId, AgentMetadata)>>::new();
|
||||
|
||||
for thread_id in state.list_thread_ids().await {
|
||||
let Ok(thread) = state.get_thread(thread_id).await else {
|
||||
@@ -691,11 +796,26 @@ impl AgentControl {
|
||||
children_by_parent
|
||||
.entry(parent_thread_id)
|
||||
.or_default()
|
||||
.push((thread_id, snapshot.session_source.get_nickname()));
|
||||
.push((
|
||||
thread_id,
|
||||
self.state
|
||||
.agent_metadata_for_thread(thread_id)
|
||||
.unwrap_or(AgentMetadata {
|
||||
agent_id: Some(thread_id),
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
for children in children_by_parent.values_mut() {
|
||||
children.sort_by(|left, right| left.0.to_string().cmp(&right.0.to_string()));
|
||||
children.sort_by(|left, right| {
|
||||
left.1
|
||||
.agent_path
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.cmp(right.1.agent_path.as_deref().unwrap_or_default())
|
||||
.then_with(|| left.0.to_string().cmp(&right.0.to_string()))
|
||||
});
|
||||
}
|
||||
|
||||
Ok(children_by_parent)
|
||||
|
||||
@@ -443,12 +443,13 @@ async fn spawn_agent_can_fork_parent_thread_history() {
|
||||
|
||||
let child_thread_id = harness
|
||||
.control
|
||||
.spawn_agent_with_options(
|
||||
.spawn_agent_with_metadata(
|
||||
harness.config.clone(),
|
||||
text_input("child task"),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
@@ -457,7 +458,8 @@ async fn spawn_agent_can_fork_parent_thread_history() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("forked spawn should succeed");
|
||||
.expect("forked spawn should succeed")
|
||||
.thread_id;
|
||||
|
||||
let child_thread = harness
|
||||
.manager
|
||||
@@ -526,12 +528,13 @@ async fn spawn_agent_fork_injects_output_for_parent_spawn_call() {
|
||||
|
||||
let child_thread_id = harness
|
||||
.control
|
||||
.spawn_agent_with_options(
|
||||
.spawn_agent_with_metadata(
|
||||
harness.config.clone(),
|
||||
text_input("child task"),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
@@ -540,7 +543,8 @@ async fn spawn_agent_fork_injects_output_for_parent_spawn_call() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("forked spawn should succeed");
|
||||
.expect("forked spawn should succeed")
|
||||
.thread_id;
|
||||
|
||||
let child_thread = harness
|
||||
.manager
|
||||
@@ -596,12 +600,13 @@ async fn spawn_agent_fork_flushes_parent_rollout_before_loading_history() {
|
||||
|
||||
let child_thread_id = harness
|
||||
.control
|
||||
.spawn_agent_with_options(
|
||||
.spawn_agent_with_metadata(
|
||||
harness.config.clone(),
|
||||
text_input("child task"),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
@@ -610,7 +615,8 @@ async fn spawn_agent_fork_flushes_parent_rollout_before_loading_history() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("forked spawn should flush parent rollout before loading history");
|
||||
.expect("forked spawn should flush parent rollout before loading history")
|
||||
.thread_id;
|
||||
|
||||
let child_thread = harness
|
||||
.manager
|
||||
@@ -855,6 +861,7 @@ async fn spawn_child_completion_notifies_parent_history() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
@@ -886,9 +893,11 @@ async fn completion_watcher_notifies_parent_when_child_is_missing() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
child_thread_id.to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(wait_for_subagent_notification(&parent_thread).await, true);
|
||||
@@ -903,7 +912,7 @@ async fn completion_watcher_notifies_parent_when_child_is_missing() {
|
||||
assert_eq!(
|
||||
history_contains_text(
|
||||
&history_items,
|
||||
&format!("\"agent_id\":\"{child_thread_id}\"")
|
||||
&format!("\"agent_path\":\"{child_thread_id}\"")
|
||||
),
|
||||
true
|
||||
);
|
||||
@@ -926,6 +935,7 @@ async fn spawn_thread_subagent_gets_random_nickname_in_session_source() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
@@ -945,6 +955,7 @@ async fn spawn_thread_subagent_gets_random_nickname_in_session_source() {
|
||||
depth,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
..
|
||||
}) = snapshot.session_source
|
||||
else {
|
||||
panic!("expected thread-spawn sub-agent source");
|
||||
@@ -976,6 +987,7 @@ async fn spawn_thread_subagent_uses_role_specific_nickname_candidates() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("researcher".to_string()),
|
||||
})),
|
||||
@@ -1018,6 +1030,8 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
||||
control,
|
||||
};
|
||||
let (parent_thread_id, _parent_thread) = harness.start_thread().await;
|
||||
let agent_path = AgentPath::from_string("/root/explorer".to_string())
|
||||
.expect("test agent path should be valid");
|
||||
|
||||
let child_thread_id = harness
|
||||
.control
|
||||
@@ -1027,6 +1041,7 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: Some(agent_path.clone()),
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
@@ -1095,6 +1110,7 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: Some(agent_path.clone()),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
@@ -1113,14 +1129,17 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
||||
let SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: resumed_parent_thread_id,
|
||||
depth: resumed_depth,
|
||||
agent_path: resumed_agent_path,
|
||||
agent_nickname: resumed_nickname,
|
||||
agent_role: resumed_role,
|
||||
..
|
||||
}) = resumed_snapshot.session_source
|
||||
else {
|
||||
panic!("expected thread-spawn sub-agent source");
|
||||
};
|
||||
assert_eq!(resumed_parent_thread_id, parent_thread_id);
|
||||
assert_eq!(resumed_depth, 1);
|
||||
assert_eq!(resumed_agent_path, Some(agent_path));
|
||||
assert_eq!(resumed_nickname, Some(original_nickname));
|
||||
assert_eq!(resumed_role, Some("explorer".to_string()));
|
||||
|
||||
@@ -1206,6 +1225,7 @@ async fn shutdown_agent_tree_closes_live_descendants() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
@@ -1220,6 +1240,7 @@ async fn shutdown_agent_tree_closes_live_descendants() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: child_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("worker".to_string()),
|
||||
})),
|
||||
@@ -1289,6 +1310,7 @@ async fn shutdown_agent_tree_closes_descendants_when_started_at_child() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
@@ -1303,6 +1325,7 @@ async fn shutdown_agent_tree_closes_descendants_when_started_at_child() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: child_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("worker".to_string()),
|
||||
})),
|
||||
@@ -1378,6 +1401,7 @@ async fn resume_agent_from_rollout_does_not_reopen_closed_descendants() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
@@ -1392,6 +1416,7 @@ async fn resume_agent_from_rollout_does_not_reopen_closed_descendants() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: child_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("worker".to_string()),
|
||||
})),
|
||||
@@ -1471,6 +1496,7 @@ async fn resume_closed_child_reopens_open_descendants() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
@@ -1485,6 +1511,7 @@ async fn resume_closed_child_reopens_open_descendants() {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: child_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("worker".to_string()),
|
||||
})),
|
||||
@@ -1524,6 +1551,7 @@ async fn resume_closed_child_reopens_open_descendants() {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
@@ -1565,6 +1593,7 @@ async fn resume_agent_from_rollout_reopens_open_descendants_after_manager_shutdo
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
@@ -1579,6 +1608,7 @@ async fn resume_agent_from_rollout_reopens_open_descendants_after_manager_shutdo
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: child_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("worker".to_string()),
|
||||
})),
|
||||
@@ -1654,6 +1684,7 @@ async fn resume_agent_from_rollout_uses_edge_data_when_descendant_metadata_sourc
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
@@ -1668,6 +1699,7 @@ async fn resume_agent_from_rollout_uses_edge_data_when_descendant_metadata_sourc
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: child_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("worker".to_string()),
|
||||
})),
|
||||
@@ -1705,6 +1737,7 @@ async fn resume_agent_from_rollout_uses_edge_data_when_descendant_metadata_sourc
|
||||
serde_json::to_string(&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: ThreadId::new(),
|
||||
depth: 99,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("worker".to_string()),
|
||||
}))
|
||||
@@ -1782,6 +1815,7 @@ async fn resume_agent_from_rollout_skips_descendants_when_parent_resume_fails()
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
@@ -1796,6 +1830,7 @@ async fn resume_agent_from_rollout_skips_descendants_when_parent_resume_fails()
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: child_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("worker".to_string()),
|
||||
})),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
@@ -25,12 +27,19 @@ pub(crate) struct Guards {
|
||||
|
||||
#[derive(Default)]
|
||||
struct ActiveAgents {
|
||||
threads_set: HashSet<ThreadId>,
|
||||
thread_agent_nicknames: HashMap<ThreadId, String>,
|
||||
agent_tree: HashMap<String, AgentMetadata>,
|
||||
used_agent_nicknames: HashSet<String>,
|
||||
nickname_reset_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct AgentMetadata {
|
||||
pub(crate) agent_id: Option<ThreadId>,
|
||||
pub(crate) agent_path: Option<AgentPath>,
|
||||
pub(crate) agent_nickname: Option<String>,
|
||||
pub(crate) agent_role: Option<String>,
|
||||
}
|
||||
|
||||
fn format_agent_nickname(name: &str, nickname_reset_count: usize) -> String {
|
||||
match nickname_reset_count {
|
||||
0 => name.to_string(),
|
||||
@@ -82,38 +91,83 @@ impl Guards {
|
||||
state: Arc::clone(self),
|
||||
active: true,
|
||||
reserved_agent_nickname: None,
|
||||
reserved_agent_path: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn release_spawned_thread(&self, thread_id: ThreadId) {
|
||||
let removed = {
|
||||
let removed_counted_agent = {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let removed = active_agents.threads_set.remove(&thread_id);
|
||||
active_agents.thread_agent_nicknames.remove(&thread_id);
|
||||
removed
|
||||
let removed_key = active_agents
|
||||
.agent_tree
|
||||
.iter()
|
||||
.find_map(|(key, metadata)| (metadata.agent_id == Some(thread_id)).then_some(key))
|
||||
.cloned();
|
||||
removed_key
|
||||
.and_then(|key| active_agents.agent_tree.remove(key.as_str()))
|
||||
.is_some_and(|metadata| {
|
||||
!metadata.agent_path.as_ref().is_some_and(AgentPath::is_root)
|
||||
})
|
||||
};
|
||||
if removed {
|
||||
if removed_counted_agent {
|
||||
self.total_count.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
fn register_spawned_thread(&self, thread_id: ThreadId, agent_nickname: Option<String>) {
|
||||
pub(crate) fn register_root_thread(&self, thread_id: ThreadId) {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
active_agents.threads_set.insert(thread_id);
|
||||
if let Some(agent_nickname) = agent_nickname {
|
||||
active_agents
|
||||
.used_agent_nicknames
|
||||
.insert(agent_nickname.clone());
|
||||
active_agents
|
||||
.thread_agent_nicknames
|
||||
.insert(thread_id, agent_nickname);
|
||||
active_agents
|
||||
.agent_tree
|
||||
.entry(AgentPath::ROOT.to_string())
|
||||
.or_insert_with(|| AgentMetadata {
|
||||
agent_id: Some(thread_id),
|
||||
agent_path: Some(AgentPath::root()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn agent_id_for_path(&self, agent_path: &AgentPath) -> Option<ThreadId> {
|
||||
self.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.agent_tree
|
||||
.get(agent_path.as_str())
|
||||
.and_then(|metadata| metadata.agent_id)
|
||||
}
|
||||
|
||||
pub(crate) fn agent_metadata_for_thread(&self, thread_id: ThreadId) -> Option<AgentMetadata> {
|
||||
self.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.agent_tree
|
||||
.values()
|
||||
.find(|metadata| metadata.agent_id == Some(thread_id))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn register_spawned_thread(&self, agent_metadata: AgentMetadata) {
|
||||
let Some(thread_id) = agent_metadata.agent_id else {
|
||||
return;
|
||||
};
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let key = agent_metadata
|
||||
.agent_path
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| format!("thread:{thread_id}"));
|
||||
if let Some(agent_nickname) = agent_metadata.agent_nickname.clone() {
|
||||
active_agents.used_agent_nicknames.insert(agent_nickname);
|
||||
}
|
||||
active_agents.agent_tree.insert(key, agent_metadata);
|
||||
}
|
||||
|
||||
fn reserve_agent_nickname(&self, names: &[&str], preferred: Option<&str>) -> Option<String> {
|
||||
@@ -156,6 +210,39 @@ impl Guards {
|
||||
Some(agent_nickname)
|
||||
}
|
||||
|
||||
fn reserve_agent_path(&self, agent_path: &AgentPath) -> Result<()> {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
match active_agents.agent_tree.entry(agent_path.to_string()) {
|
||||
Entry::Occupied(_) => Err(CodexErr::UnsupportedOperation(format!(
|
||||
"agent path `{agent_path}` already exists"
|
||||
))),
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(AgentMetadata {
|
||||
agent_path: Some(agent_path.clone()),
|
||||
..Default::default()
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn release_reserved_agent_path(&self, agent_path: &AgentPath) {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if active_agents
|
||||
.agent_tree
|
||||
.get(agent_path.as_str())
|
||||
.is_some_and(|metadata| metadata.agent_id.is_none())
|
||||
{
|
||||
active_agents.agent_tree.remove(agent_path.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
fn try_increment_spawned(&self, max_threads: usize) -> bool {
|
||||
let mut current = self.total_count.load(Ordering::Acquire);
|
||||
loop {
|
||||
@@ -179,13 +266,10 @@ pub(crate) struct SpawnReservation {
|
||||
state: Arc<Guards>,
|
||||
active: bool,
|
||||
reserved_agent_nickname: Option<String>,
|
||||
reserved_agent_path: Option<AgentPath>,
|
||||
}
|
||||
|
||||
impl SpawnReservation {
|
||||
pub(crate) fn reserve_agent_nickname(&mut self, names: &[&str]) -> Result<String> {
|
||||
self.reserve_agent_nickname_with_preference(names, /*preferred*/ None)
|
||||
}
|
||||
|
||||
pub(crate) fn reserve_agent_nickname_with_preference(
|
||||
&mut self,
|
||||
names: &[&str],
|
||||
@@ -201,18 +285,16 @@ impl SpawnReservation {
|
||||
Ok(agent_nickname)
|
||||
}
|
||||
|
||||
pub(crate) fn commit(self, thread_id: ThreadId) {
|
||||
self.commit_with_agent_nickname(thread_id, /*agent_nickname*/ None);
|
||||
pub(crate) fn reserve_agent_path(&mut self, agent_path: &AgentPath) -> Result<()> {
|
||||
self.state.reserve_agent_path(agent_path)?;
|
||||
self.reserved_agent_path = Some(agent_path.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn commit_with_agent_nickname(
|
||||
mut self,
|
||||
thread_id: ThreadId,
|
||||
agent_nickname: Option<String>,
|
||||
) {
|
||||
let agent_nickname = self.reserved_agent_nickname.take().or(agent_nickname);
|
||||
self.state
|
||||
.register_spawned_thread(thread_id, agent_nickname);
|
||||
pub(crate) fn commit(mut self, agent_metadata: AgentMetadata) {
|
||||
self.reserved_agent_nickname = None;
|
||||
self.reserved_agent_path = None;
|
||||
self.state.register_spawned_thread(agent_metadata);
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
@@ -220,6 +302,9 @@ impl SpawnReservation {
|
||||
impl Drop for SpawnReservation {
|
||||
fn drop(&mut self) {
|
||||
if self.active {
|
||||
if let Some(agent_path) = self.reserved_agent_path.take() {
|
||||
self.state.release_reserved_agent_path(&agent_path);
|
||||
}
|
||||
self.state.total_count.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
use super::*;
|
||||
use codex_protocol::AgentPath;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn agent_path(path: &str) -> AgentPath {
|
||||
AgentPath::try_from(path).expect("valid agent path")
|
||||
}
|
||||
|
||||
fn agent_metadata(thread_id: ThreadId) -> AgentMetadata {
|
||||
AgentMetadata {
|
||||
agent_id: Some(thread_id),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_agent_nickname_adds_ordinals_after_reset() {
|
||||
assert_eq!(format_agent_nickname("Plato", 0), "Plato");
|
||||
@@ -21,6 +33,7 @@ fn thread_spawn_depth_increments_and_enforces_limit() {
|
||||
let session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: ThreadId::new(),
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
@@ -52,7 +65,7 @@ fn commit_holds_slot_until_release() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
let thread_id = ThreadId::new();
|
||||
reservation.commit(thread_id);
|
||||
reservation.commit(agent_metadata(thread_id));
|
||||
|
||||
let err = match guards.reserve_spawn_slot(Some(1)) {
|
||||
Ok(_) => panic!("limit should be enforced"),
|
||||
@@ -75,7 +88,7 @@ fn release_ignores_unknown_thread_id() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
let thread_id = ThreadId::new();
|
||||
reservation.commit(thread_id);
|
||||
reservation.commit(agent_metadata(thread_id));
|
||||
|
||||
guards.release_spawned_thread(ThreadId::new());
|
||||
|
||||
@@ -100,13 +113,13 @@ fn release_is_idempotent_for_registered_threads() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
|
||||
let first_id = ThreadId::new();
|
||||
reservation.commit(first_id);
|
||||
reservation.commit(agent_metadata(first_id));
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let reservation = guards.reserve_spawn_slot(Some(1)).expect("slot reused");
|
||||
let second_id = ThreadId::new();
|
||||
reservation.commit(second_id);
|
||||
reservation.commit(agent_metadata(second_id));
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
@@ -131,14 +144,14 @@ fn failed_spawn_keeps_nickname_marked_used() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
let agent_nickname = reservation
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
|
||||
.expect("reserve agent name");
|
||||
assert_eq!(agent_nickname, "alpha");
|
||||
drop(reservation);
|
||||
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
let agent_nickname = reservation
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.reserve_agent_nickname_with_preference(&["alpha", "beta"], /*preferred*/ None)
|
||||
.expect("unused name should still be preferred");
|
||||
assert_eq!(agent_nickname, "beta");
|
||||
}
|
||||
@@ -148,17 +161,17 @@ fn agent_nickname_resets_used_pool_when_exhausted() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
first.commit(agent_metadata(first_id));
|
||||
assert_eq!(first_name, "alpha");
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
|
||||
.expect("name should be reused after pool reset");
|
||||
assert_eq!(second_name, "alpha the 2nd");
|
||||
let active_agents = guards
|
||||
@@ -174,10 +187,10 @@ fn released_nickname_stays_used_until_pool_reset() {
|
||||
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
first.commit(agent_metadata(first_id));
|
||||
assert_eq!(first_name, "alpha");
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
@@ -186,16 +199,16 @@ fn released_nickname_stays_used_until_pool_reset() {
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.reserve_agent_nickname_with_preference(&["alpha", "beta"], /*preferred*/ None)
|
||||
.expect("released name should still be marked used");
|
||||
assert_eq!(second_name, "beta");
|
||||
let second_id = ThreadId::new();
|
||||
second.commit(second_id);
|
||||
second.commit(agent_metadata(second_id));
|
||||
guards.release_spawned_thread(second_id);
|
||||
|
||||
let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot");
|
||||
let third_name = third
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.reserve_agent_nickname_with_preference(&["alpha", "beta"], /*preferred*/ None)
|
||||
.expect("pool reset should permit a duplicate");
|
||||
let expected_names = HashSet::from(["alpha the 2nd".to_string(), "beta the 2nd".to_string()]);
|
||||
assert!(expected_names.contains(&third_name));
|
||||
@@ -212,10 +225,10 @@ fn repeated_resets_advance_the_ordinal_suffix() {
|
||||
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["Plato"])
|
||||
.reserve_agent_nickname_with_preference(&["Plato"], /*preferred*/ None)
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
first.commit(agent_metadata(first_id));
|
||||
assert_eq!(first_name, "Plato");
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
@@ -223,16 +236,16 @@ fn repeated_resets_advance_the_ordinal_suffix() {
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["Plato"])
|
||||
.reserve_agent_nickname_with_preference(&["Plato"], /*preferred*/ None)
|
||||
.expect("reserve second agent name");
|
||||
let second_id = ThreadId::new();
|
||||
second.commit(second_id);
|
||||
second.commit(agent_metadata(second_id));
|
||||
assert_eq!(second_name, "Plato the 2nd");
|
||||
guards.release_spawned_thread(second_id);
|
||||
|
||||
let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot");
|
||||
let third_name = third
|
||||
.reserve_agent_nickname(&["Plato"])
|
||||
.reserve_agent_nickname_with_preference(&["Plato"], /*preferred*/ None)
|
||||
.expect("reserve third agent name");
|
||||
assert_eq!(third_name, "Plato the 3rd");
|
||||
let active_agents = guards
|
||||
@@ -241,3 +254,59 @@ fn repeated_resets_advance_the_ordinal_suffix() {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_root_thread_indexes_root_path() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let root_thread_id = ThreadId::new();
|
||||
|
||||
guards.register_root_thread(root_thread_id);
|
||||
|
||||
assert_eq!(
|
||||
guards.agent_id_for_path(&AgentPath::root()),
|
||||
Some(root_thread_id)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_agent_path_is_released_when_spawn_fails() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
first
|
||||
.reserve_agent_path(&agent_path("/root/researcher"))
|
||||
.expect("reserve first path");
|
||||
drop(first);
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
second
|
||||
.reserve_agent_path(&agent_path("/root/researcher"))
|
||||
.expect("dropped reservation should free the path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_agent_path_is_indexed_until_release() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let thread_id = ThreadId::new();
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
reservation
|
||||
.reserve_agent_path(&agent_path("/root/researcher"))
|
||||
.expect("reserve path");
|
||||
reservation.commit(AgentMetadata {
|
||||
agent_id: Some(thread_id),
|
||||
agent_path: Some(agent_path("/root/researcher")),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
guards.agent_id_for_path(&agent_path("/root/researcher")),
|
||||
Some(thread_id)
|
||||
);
|
||||
|
||||
guards.release_spawned_thread(thread_id);
|
||||
assert_eq!(
|
||||
guards.agent_id_for_path(&agent_path("/root/researcher")),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub(crate) mod agent_resolver;
|
||||
pub(crate) mod control;
|
||||
mod guards;
|
||||
pub(crate) mod role;
|
||||
|
||||
@@ -38,6 +38,7 @@ async fn write_session_with_user_event(codex_home: &Path) -> io::Result<()> {
|
||||
originator: "test_originator".to_string(),
|
||||
cli_version: "test_version".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
|
||||
@@ -23,6 +23,7 @@ fn thread_metadata(cwd: &str, title: &str, first_user_message: &str) -> ThreadMe
|
||||
.single()
|
||||
.expect("valid timestamp"),
|
||||
source: "cli".to_string(),
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
|
||||
@@ -49,6 +49,7 @@ pub(crate) fn builder_from_session_meta(
|
||||
builder.model_provider = session_meta.meta.model_provider.clone();
|
||||
builder.agent_nickname = session_meta.meta.agent_nickname.clone();
|
||||
builder.agent_role = session_meta.meta.agent_role.clone();
|
||||
builder.agent_path = session_meta.meta.agent_path.clone();
|
||||
builder.cwd = session_meta.meta.cwd.clone();
|
||||
builder.cli_version = Some(session_meta.meta.cli_version.clone());
|
||||
builder.sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
|
||||
@@ -38,6 +38,7 @@ async fn extract_metadata_from_rollout_uses_session_meta() {
|
||||
originator: "cli".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::default(),
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
@@ -88,6 +89,7 @@ async fn extract_metadata_from_rollout_returns_latest_memory_mode() {
|
||||
originator: "cli".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::default(),
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
@@ -355,6 +357,7 @@ fn write_rollout_in_sessions_with_cwd(
|
||||
originator: "cli".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::default(),
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".to_string()),
|
||||
|
||||
@@ -405,6 +405,7 @@ impl RolloutRecorder {
|
||||
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
agent_nickname: source.get_nickname(),
|
||||
agent_role: source.get_agent_role(),
|
||||
agent_path: source.get_agent_path().map(Into::into),
|
||||
source,
|
||||
model_provider: Some(config.model_provider_id.clone()),
|
||||
base_instructions: Some(base_instructions),
|
||||
|
||||
@@ -1101,6 +1101,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
|
||||
originator: "test_originator".into(),
|
||||
cli_version: "test_version".into(),
|
||||
source: SessionSource::VSCode,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".into()),
|
||||
|
||||
@@ -4,18 +4,25 @@ use codex_protocol::protocol::AgentStatus;
|
||||
/// messages but are not user intent.
|
||||
use crate::contextual_user_message::SUBAGENT_NOTIFICATION_FRAGMENT;
|
||||
|
||||
pub(crate) fn format_subagent_notification_message(agent_id: &str, status: &AgentStatus) -> String {
|
||||
// TODO(jif) unify with structured schema
|
||||
pub(crate) fn format_subagent_notification_message(
|
||||
agent_reference: &str,
|
||||
status: &AgentStatus,
|
||||
) -> String {
|
||||
let payload_json = serde_json::json!({
|
||||
"agent_id": agent_id,
|
||||
"agent_path": agent_reference,
|
||||
"status": status,
|
||||
})
|
||||
.to_string();
|
||||
SUBAGENT_NOTIFICATION_FRAGMENT.wrap(payload_json)
|
||||
}
|
||||
|
||||
pub(crate) fn format_subagent_context_line(agent_id: &str, agent_nickname: Option<&str>) -> String {
|
||||
pub(crate) fn format_subagent_context_line(
|
||||
agent_reference: &str,
|
||||
agent_nickname: Option<&str>,
|
||||
) -> String {
|
||||
match agent_nickname.filter(|nickname| !nickname.is_empty()) {
|
||||
Some(agent_nickname) => format!("- {agent_id}: {agent_nickname}"),
|
||||
None => format!("- {agent_id}"),
|
||||
Some(agent_nickname) => format!("- {agent_reference}: {agent_nickname}"),
|
||||
None => format!("- {agent_reference}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
//! then optionally layer role-specific config on top.
|
||||
|
||||
use crate::agent::AgentStatus;
|
||||
use crate::agent::agent_resolver::resolve_agent_target;
|
||||
use crate::agent::agent_resolver::resolve_agent_targets;
|
||||
use crate::agent::exceeds_thread_spawn_depth_limit;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
@@ -22,6 +24,7 @@ use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use async_trait::async_trait;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
@@ -59,11 +62,6 @@ pub(crate) const MIN_WAIT_TIMEOUT_MS: i64 = 10_000;
|
||||
pub(crate) const DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000;
|
||||
pub(crate) const MAX_WAIT_TIMEOUT_MS: i64 = 3600 * 1000;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CloseAgentArgs {
|
||||
id: String,
|
||||
}
|
||||
|
||||
fn function_arguments(payload: ToolPayload) -> Result<String, FunctionCallError> {
|
||||
match payload {
|
||||
ToolPayload::Function { arguments } => Ok(arguments),
|
||||
@@ -111,11 +109,6 @@ mod send_input;
|
||||
mod spawn;
|
||||
pub(crate) mod wait;
|
||||
|
||||
fn agent_id(id: &str) -> Result<ThreadId, FunctionCallError> {
|
||||
ThreadId::from_string(id)
|
||||
.map_err(|e| FunctionCallError::RespondToModel(format!("invalid agent id {id}: {e:?}")))
|
||||
}
|
||||
|
||||
fn build_wait_agent_statuses(
|
||||
statuses: &HashMap<ThreadId, AgentStatus>,
|
||||
receiver_agents: &[CollabAgentRef],
|
||||
@@ -155,9 +148,10 @@ fn build_wait_agent_statuses(
|
||||
|
||||
fn collab_spawn_error(err: CodexErr) -> FunctionCallError {
|
||||
match err {
|
||||
CodexErr::UnsupportedOperation(_) => {
|
||||
CodexErr::UnsupportedOperation(message) if message == "thread manager dropped" => {
|
||||
FunctionCallError::RespondToModel("collab manager unavailable".to_string())
|
||||
}
|
||||
CodexErr::UnsupportedOperation(message) => FunctionCallError::RespondToModel(message),
|
||||
err => FunctionCallError::RespondToModel(format!("collab spawn failed: {err}")),
|
||||
}
|
||||
}
|
||||
@@ -179,15 +173,28 @@ fn collab_agent_error(agent_id: ThreadId, err: CodexErr) -> FunctionCallError {
|
||||
|
||||
fn thread_spawn_source(
|
||||
parent_thread_id: ThreadId,
|
||||
parent_session_source: &SessionSource,
|
||||
depth: i32,
|
||||
agent_role: Option<&str>,
|
||||
) -> SessionSource {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
task_name: Option<String>,
|
||||
) -> Result<SessionSource, FunctionCallError> {
|
||||
let agent_path = task_name
|
||||
.as_deref()
|
||||
.map(|task_name| {
|
||||
parent_session_source
|
||||
.get_agent_path()
|
||||
.unwrap_or_else(AgentPath::root)
|
||||
.join(task_name)
|
||||
.map_err(FunctionCallError::RespondToModel)
|
||||
})
|
||||
.transpose()?;
|
||||
Ok(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_path,
|
||||
agent_nickname: None,
|
||||
agent_role: agent_role.map(str::to_string),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_collab_input(
|
||||
|
||||
@@ -24,13 +24,12 @@ impl ToolHandler for Handler {
|
||||
} = invocation;
|
||||
let arguments = function_arguments(payload)?;
|
||||
let args: CloseAgentArgs = parse_arguments(&arguments)?;
|
||||
let agent_id = agent_id(&args.id)?;
|
||||
let (receiver_agent_nickname, receiver_agent_role) = session
|
||||
let agent_id = resolve_agent_target(&session, &turn, &args.target).await?;
|
||||
let receiver_agent = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_nickname_and_role(agent_id)
|
||||
.await
|
||||
.unwrap_or((None, None));
|
||||
.get_agent_metadata(agent_id)
|
||||
.unwrap_or_default();
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
@@ -58,8 +57,8 @@ impl ToolHandler for Handler {
|
||||
call_id: call_id.clone(),
|
||||
sender_thread_id: session.conversation_id,
|
||||
receiver_thread_id: agent_id,
|
||||
receiver_agent_nickname: receiver_agent_nickname.clone(),
|
||||
receiver_agent_role: receiver_agent_role.clone(),
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname.clone(),
|
||||
receiver_agent_role: receiver_agent.agent_role.clone(),
|
||||
status,
|
||||
}
|
||||
.into(),
|
||||
@@ -82,8 +81,8 @@ impl ToolHandler for Handler {
|
||||
call_id,
|
||||
sender_thread_id: session.conversation_id,
|
||||
receiver_thread_id: agent_id,
|
||||
receiver_agent_nickname,
|
||||
receiver_agent_role,
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname,
|
||||
receiver_agent_role: receiver_agent.agent_role,
|
||||
status: status.clone(),
|
||||
}
|
||||
.into(),
|
||||
@@ -119,3 +118,8 @@ impl ToolOutput for CloseAgentResult {
|
||||
tool_output_code_mode_result(self, "close_agent")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CloseAgentArgs {
|
||||
target: String,
|
||||
}
|
||||
|
||||
@@ -25,13 +25,14 @@ impl ToolHandler for Handler {
|
||||
} = invocation;
|
||||
let arguments = function_arguments(payload)?;
|
||||
let args: ResumeAgentArgs = parse_arguments(&arguments)?;
|
||||
let receiver_thread_id = agent_id(&args.id)?;
|
||||
let (receiver_agent_nickname, receiver_agent_role) = session
|
||||
let receiver_thread_id = ThreadId::from_string(&args.id).map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!("invalid agent id {}: {err:?}", args.id))
|
||||
})?;
|
||||
let receiver_agent = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_nickname_and_role(receiver_thread_id)
|
||||
.await
|
||||
.unwrap_or((None, None));
|
||||
.get_agent_metadata(receiver_thread_id)
|
||||
.unwrap_or_default();
|
||||
let child_depth = next_thread_spawn_depth(&turn.session_source);
|
||||
let max_depth = turn.config.agent_max_depth;
|
||||
if exceeds_thread_spawn_depth_limit(child_depth, max_depth) {
|
||||
@@ -47,8 +48,8 @@ impl ToolHandler for Handler {
|
||||
call_id: call_id.clone(),
|
||||
sender_thread_id: session.conversation_id,
|
||||
receiver_thread_id,
|
||||
receiver_agent_nickname: receiver_agent_nickname.clone(),
|
||||
receiver_agent_role: receiver_agent_role.clone(),
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname.clone(),
|
||||
receiver_agent_role: receiver_agent.agent_role.clone(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
@@ -59,11 +60,22 @@ impl ToolHandler for Handler {
|
||||
.agent_control
|
||||
.get_status(receiver_thread_id)
|
||||
.await;
|
||||
let error = if matches!(status, AgentStatus::NotFound) {
|
||||
let (receiver_agent, error) = if matches!(status, AgentStatus::NotFound) {
|
||||
match try_resume_closed_agent(&session, &turn, receiver_thread_id, child_depth).await {
|
||||
Ok(resumed_status) => {
|
||||
status = resumed_status;
|
||||
None
|
||||
Ok(()) => {
|
||||
status = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_status(receiver_thread_id)
|
||||
.await;
|
||||
(
|
||||
session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_metadata(receiver_thread_id)
|
||||
.unwrap_or(receiver_agent),
|
||||
None,
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
status = session
|
||||
@@ -71,19 +83,12 @@ impl ToolHandler for Handler {
|
||||
.agent_control
|
||||
.get_status(receiver_thread_id)
|
||||
.await;
|
||||
Some(err)
|
||||
(receiver_agent, Some(err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
(receiver_agent, None)
|
||||
};
|
||||
|
||||
let (receiver_agent_nickname, receiver_agent_role) = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_nickname_and_role(receiver_thread_id)
|
||||
.await
|
||||
.unwrap_or((receiver_agent_nickname, receiver_agent_role));
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
@@ -91,8 +96,8 @@ impl ToolHandler for Handler {
|
||||
call_id,
|
||||
sender_thread_id: session.conversation_id,
|
||||
receiver_thread_id,
|
||||
receiver_agent_nickname,
|
||||
receiver_agent_role,
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname,
|
||||
receiver_agent_role: receiver_agent.agent_role,
|
||||
status: status.clone(),
|
||||
}
|
||||
.into(),
|
||||
@@ -142,9 +147,9 @@ async fn try_resume_closed_agent(
|
||||
turn: &Arc<TurnContext>,
|
||||
receiver_thread_id: ThreadId,
|
||||
child_depth: i32,
|
||||
) -> Result<AgentStatus, FunctionCallError> {
|
||||
) -> Result<(), FunctionCallError> {
|
||||
let config = build_agent_resume_config(turn.as_ref(), child_depth)?;
|
||||
let resumed_thread_id = session
|
||||
session
|
||||
.services
|
||||
.agent_control
|
||||
.resume_agent_from_rollout(
|
||||
@@ -152,16 +157,13 @@ async fn try_resume_closed_agent(
|
||||
receiver_thread_id,
|
||||
thread_spawn_source(
|
||||
session.conversation_id,
|
||||
&turn.session_source,
|
||||
child_depth,
|
||||
/*agent_role*/ None,
|
||||
),
|
||||
/*task_name*/ None,
|
||||
)?,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| collab_agent_error(receiver_thread_id, err))?;
|
||||
|
||||
Ok(session
|
||||
.services
|
||||
.agent_control
|
||||
.get_status(resumed_thread_id)
|
||||
.await)
|
||||
.map(|_| ())
|
||||
.map_err(|err| collab_agent_error(receiver_thread_id, err))
|
||||
}
|
||||
|
||||
@@ -24,15 +24,14 @@ impl ToolHandler for Handler {
|
||||
} = invocation;
|
||||
let arguments = function_arguments(payload)?;
|
||||
let args: SendInputArgs = parse_arguments(&arguments)?;
|
||||
let receiver_thread_id = agent_id(&args.id)?;
|
||||
let receiver_thread_id = resolve_agent_target(&session, &turn, &args.target).await?;
|
||||
let input_items = parse_collab_input(args.message, args.items)?;
|
||||
let prompt = input_preview(&input_items);
|
||||
let (receiver_agent_nickname, receiver_agent_role) = session
|
||||
let receiver_agent = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_nickname_and_role(receiver_thread_id)
|
||||
.await
|
||||
.unwrap_or((None, None));
|
||||
.get_agent_metadata(receiver_thread_id)
|
||||
.unwrap_or_default();
|
||||
if args.interrupt {
|
||||
session
|
||||
.services
|
||||
@@ -71,8 +70,8 @@ impl ToolHandler for Handler {
|
||||
call_id,
|
||||
sender_thread_id: session.conversation_id,
|
||||
receiver_thread_id,
|
||||
receiver_agent_nickname,
|
||||
receiver_agent_role,
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname,
|
||||
receiver_agent_role: receiver_agent.agent_role,
|
||||
prompt,
|
||||
status,
|
||||
}
|
||||
@@ -87,7 +86,7 @@ impl ToolHandler for Handler {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SendInputArgs {
|
||||
id: String,
|
||||
target: String,
|
||||
message: Option<String>,
|
||||
items: Option<Vec<UserInput>>,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -77,26 +77,29 @@ impl ToolHandler for Handler {
|
||||
let result = session
|
||||
.services
|
||||
.agent_control
|
||||
.spawn_agent_with_options(
|
||||
.spawn_agent_with_metadata(
|
||||
config,
|
||||
input_items,
|
||||
Some(thread_spawn_source(
|
||||
session.conversation_id,
|
||||
&turn.session_source,
|
||||
child_depth,
|
||||
role_name,
|
||||
)),
|
||||
args.task_name.clone(),
|
||||
)?),
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(collab_spawn_error);
|
||||
let (new_thread_id, status) = match &result {
|
||||
Ok(thread_id) => (
|
||||
Some(*thread_id),
|
||||
session.services.agent_control.get_status(*thread_id).await,
|
||||
let (new_thread_id, new_agent_metadata, status) = match &result {
|
||||
Ok(spawned_agent) => (
|
||||
Some(spawned_agent.thread_id),
|
||||
Some(spawned_agent.metadata.clone()),
|
||||
spawned_agent.status.clone(),
|
||||
),
|
||||
Err(_) => (None, AgentStatus::NotFound),
|
||||
Err(_) => (None, None, AgentStatus::NotFound),
|
||||
};
|
||||
let agent_snapshot = match new_thread_id {
|
||||
Some(thread_id) => {
|
||||
@@ -108,19 +111,20 @@ impl ToolHandler for Handler {
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let (new_agent_nickname, new_agent_role) = match (&agent_snapshot, new_thread_id) {
|
||||
(Some(snapshot), _) => (
|
||||
snapshot.session_source.get_nickname(),
|
||||
snapshot.session_source.get_agent_role(),
|
||||
),
|
||||
(None, Some(thread_id)) => session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_nickname_and_role(thread_id)
|
||||
.await
|
||||
.unwrap_or((None, None)),
|
||||
(None, None) => (None, None),
|
||||
};
|
||||
let (new_agent_path, new_agent_nickname, new_agent_role) =
|
||||
match (&agent_snapshot, new_agent_metadata) {
|
||||
(Some(snapshot), _) => (
|
||||
snapshot.session_source.get_agent_path().map(String::from),
|
||||
snapshot.session_source.get_nickname(),
|
||||
snapshot.session_source.get_agent_role(),
|
||||
),
|
||||
(None, Some(metadata)) => (
|
||||
metadata.agent_path.map(String::from),
|
||||
metadata.agent_nickname,
|
||||
metadata.agent_role,
|
||||
),
|
||||
(None, None) => (None, None, None),
|
||||
};
|
||||
let effective_model = agent_snapshot
|
||||
.as_ref()
|
||||
.map(|snapshot| snapshot.model.clone())
|
||||
@@ -130,6 +134,7 @@ impl ToolHandler for Handler {
|
||||
.and_then(|snapshot| snapshot.reasoning_effort)
|
||||
.unwrap_or(args.reasoning_effort.unwrap_or_default());
|
||||
let nickname = new_agent_nickname.clone();
|
||||
let task_name = new_agent_path.clone();
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
@@ -147,7 +152,7 @@ impl ToolHandler for Handler {
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
let new_thread_id = result?;
|
||||
let new_thread_id = result?.thread_id;
|
||||
let role_tag = role_name.unwrap_or(DEFAULT_ROLE_NAME);
|
||||
turn.session_telemetry.counter(
|
||||
"codex.multi_agent.spawn",
|
||||
@@ -156,7 +161,8 @@ impl ToolHandler for Handler {
|
||||
);
|
||||
|
||||
Ok(SpawnAgentResult {
|
||||
agent_id: new_thread_id.to_string(),
|
||||
agent_id: task_name.is_none().then(|| new_thread_id.to_string()),
|
||||
task_name,
|
||||
nickname,
|
||||
})
|
||||
}
|
||||
@@ -166,6 +172,7 @@ impl ToolHandler for Handler {
|
||||
struct SpawnAgentArgs {
|
||||
message: Option<String>,
|
||||
items: Option<Vec<UserInput>>,
|
||||
task_name: Option<String>,
|
||||
agent_type: Option<String>,
|
||||
model: Option<String>,
|
||||
reasoning_effort: Option<ReasoningEffort>,
|
||||
@@ -175,7 +182,8 @@ struct SpawnAgentArgs {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct SpawnAgentResult {
|
||||
agent_id: String,
|
||||
agent_id: Option<String>,
|
||||
task_name: Option<String>,
|
||||
nickname: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -34,28 +34,27 @@ impl ToolHandler for Handler {
|
||||
} = invocation;
|
||||
let arguments = function_arguments(payload)?;
|
||||
let args: WaitArgs = parse_arguments(&arguments)?;
|
||||
if args.ids.is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"ids must be non-empty".to_owned(),
|
||||
));
|
||||
}
|
||||
let receiver_thread_ids = args
|
||||
.ids
|
||||
.iter()
|
||||
.map(|id| agent_id(id))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let receiver_thread_ids = resolve_agent_targets(&session, &turn, args.targets).await?;
|
||||
let mut receiver_agents = Vec::with_capacity(receiver_thread_ids.len());
|
||||
let mut target_by_thread_id = HashMap::with_capacity(receiver_thread_ids.len());
|
||||
for receiver_thread_id in &receiver_thread_ids {
|
||||
let (agent_nickname, agent_role) = session
|
||||
let agent_metadata = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_nickname_and_role(*receiver_thread_id)
|
||||
.await
|
||||
.unwrap_or((None, None));
|
||||
.get_agent_metadata(*receiver_thread_id)
|
||||
.unwrap_or_default();
|
||||
target_by_thread_id.insert(
|
||||
*receiver_thread_id,
|
||||
agent_metadata
|
||||
.agent_path
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| receiver_thread_id.to_string()),
|
||||
);
|
||||
receiver_agents.push(CollabAgentRef {
|
||||
thread_id: *receiver_thread_id,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_nickname: agent_metadata.agent_nickname,
|
||||
agent_role: agent_metadata.agent_role,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,11 +150,20 @@ impl ToolHandler for Handler {
|
||||
results
|
||||
};
|
||||
|
||||
let statuses_map = statuses.clone().into_iter().collect::<HashMap<_, _>>();
|
||||
let agent_statuses = build_wait_agent_statuses(&statuses_map, &receiver_agents);
|
||||
let timed_out = statuses.is_empty();
|
||||
let statuses_by_id = statuses.clone().into_iter().collect::<HashMap<_, _>>();
|
||||
let agent_statuses = build_wait_agent_statuses(&statuses_by_id, &receiver_agents);
|
||||
let result = WaitAgentResult {
|
||||
status: statuses_map.clone(),
|
||||
timed_out: statuses.is_empty(),
|
||||
status: statuses
|
||||
.into_iter()
|
||||
.filter_map(|(thread_id, status)| {
|
||||
target_by_thread_id
|
||||
.get(&thread_id)
|
||||
.cloned()
|
||||
.map(|target| (target, status))
|
||||
})
|
||||
.collect(),
|
||||
timed_out,
|
||||
};
|
||||
|
||||
session
|
||||
@@ -165,7 +173,7 @@ impl ToolHandler for Handler {
|
||||
sender_thread_id: session.conversation_id,
|
||||
call_id,
|
||||
agent_statuses,
|
||||
statuses: statuses_map,
|
||||
statuses: statuses_by_id,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
@@ -177,13 +185,14 @@ impl ToolHandler for Handler {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WaitArgs {
|
||||
ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
targets: Vec<String>,
|
||||
timeout_ms: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub(crate) struct WaitAgentResult {
|
||||
pub(crate) status: HashMap<ThreadId, AgentStatus>,
|
||||
pub(crate) status: HashMap<String, AgentStatus>,
|
||||
pub(crate) timed_out: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ fn function_payload(args: serde_json::Value) -> ToolPayload {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_agent_id(id: &str) -> ThreadId {
|
||||
ThreadId::from_string(id).expect("agent id should be valid")
|
||||
}
|
||||
|
||||
fn thread_manager() -> ThreadManager {
|
||||
ThreadManager::with_models_provider_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
@@ -195,7 +199,7 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
|
||||
let (content, _) = expect_text_output(output);
|
||||
let result: SpawnAgentResult =
|
||||
serde_json::from_str(&content).expect("spawn_agent result should be json");
|
||||
let agent_id = agent_id(&result.agent_id).expect("agent_id should be valid");
|
||||
let agent_id = parse_agent_id(&result.agent_id);
|
||||
assert!(
|
||||
result
|
||||
.nickname
|
||||
@@ -212,6 +216,33 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
|
||||
assert_eq!(snapshot.model_provider_id, "ollama");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_agent_includes_task_name_key_when_not_named() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
session.services.agent_control = manager.agent_control();
|
||||
|
||||
let output = SpawnAgentHandler
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"message": "inspect this repo"
|
||||
})),
|
||||
))
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
let (content, success) = expect_text_output(output);
|
||||
let result: serde_json::Value =
|
||||
serde_json::from_str(&content).expect("spawn_agent result should be json");
|
||||
|
||||
assert!(result["agent_id"].is_string());
|
||||
assert_eq!(result["task_name"], serde_json::Value::Null);
|
||||
assert!(result.get("nickname").is_some());
|
||||
assert_eq!(success, Some(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_agent_errors_when_manager_dropped() {
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
@@ -230,6 +261,160 @@ async fn spawn_agent_errors_when_manager_dropped() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_spawn_returns_path_and_send_input_accepts_relative_path() {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SpawnAgentResult {
|
||||
task_name: String,
|
||||
nickname: Option<String>,
|
||||
}
|
||||
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
.await
|
||||
.expect("root thread should start");
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let mut config = (*turn.config).clone();
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
let spawn_output = SpawnAgentHandler
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"message": "inspect this repo",
|
||||
"task_name": "test_process"
|
||||
})),
|
||||
))
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
let (content, _) = expect_text_output(spawn_output);
|
||||
let spawn_result: SpawnAgentResult =
|
||||
serde_json::from_str(&content).expect("spawn result should parse");
|
||||
assert_eq!(spawn_result.task_name, "/root/test_process");
|
||||
assert!(spawn_result.nickname.is_some());
|
||||
|
||||
let child_thread_id = session
|
||||
.services
|
||||
.agent_control
|
||||
.resolve_agent_reference(
|
||||
session.conversation_id,
|
||||
&turn.session_source,
|
||||
"test_process",
|
||||
)
|
||||
.await
|
||||
.expect("relative path should resolve");
|
||||
let child_snapshot = manager
|
||||
.get_thread(child_thread_id)
|
||||
.await
|
||||
.expect("child thread should exist")
|
||||
.config_snapshot()
|
||||
.await;
|
||||
assert_eq!(
|
||||
child_snapshot.session_source.get_agent_path().as_deref(),
|
||||
Some("/root/test_process")
|
||||
);
|
||||
|
||||
SendInputHandler
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
"send_input",
|
||||
function_payload(json!({
|
||||
"target": "test_process",
|
||||
"message": "continue"
|
||||
})),
|
||||
))
|
||||
.await
|
||||
.expect("send_input should accept v2 path");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_spawn_includes_agent_id_key_when_named() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
.await
|
||||
.expect("root thread should start");
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let mut config = (*turn.config).clone();
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let output = SpawnAgentHandler
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"message": "inspect this repo",
|
||||
"task_name": "test_process"
|
||||
})),
|
||||
))
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
let (content, success) = expect_text_output(output);
|
||||
let result: serde_json::Value =
|
||||
serde_json::from_str(&content).expect("spawn_agent result should be json");
|
||||
|
||||
assert_eq!(result["agent_id"], serde_json::Value::Null);
|
||||
assert_eq!(result["task_name"], "/root/test_process");
|
||||
assert!(result.get("nickname").is_some());
|
||||
assert_eq!(success, Some(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
.await
|
||||
.expect("root thread should start");
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let mut config = (*turn.config).clone();
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let invocation = invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"message": "inspect this repo",
|
||||
"task_name": "BadName"
|
||||
})),
|
||||
);
|
||||
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
|
||||
panic!("invalid agent name should be rejected");
|
||||
};
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel(
|
||||
"agent_name must use only lowercase letters, digits, and underscores".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
|
||||
fn pick_allowed_sandbox_policy(
|
||||
@@ -293,7 +478,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
|
||||
let (content, _) = expect_text_output(output);
|
||||
let result: SpawnAgentResult =
|
||||
serde_json::from_str(&content).expect("spawn_agent result should be json");
|
||||
let agent_id = agent_id(&result.agent_id).expect("agent_id should be valid");
|
||||
let agent_id = parse_agent_id(&result.agent_id);
|
||||
assert!(
|
||||
result
|
||||
.nickname
|
||||
@@ -334,6 +519,7 @@ async fn spawn_agent_rejects_when_depth_limit_exceeded() {
|
||||
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: session.conversation_id,
|
||||
depth: max_depth,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
@@ -373,6 +559,7 @@ async fn spawn_agent_allows_depth_up_to_configured_max_depth() {
|
||||
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: session.conversation_id,
|
||||
depth: DEFAULT_AGENT_MAX_DEPTH,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
@@ -407,7 +594,7 @@ async fn send_input_rejects_empty_message() {
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"send_input",
|
||||
function_payload(json!({"id": ThreadId::new().to_string(), "message": ""})),
|
||||
function_payload(json!({"target": ThreadId::new().to_string(), "message": ""})),
|
||||
);
|
||||
let Err(err) = SendInputHandler.handle(invocation).await else {
|
||||
panic!("empty message should be rejected");
|
||||
@@ -426,7 +613,7 @@ async fn send_input_rejects_when_message_and_items_are_both_set() {
|
||||
Arc::new(turn),
|
||||
"send_input",
|
||||
function_payload(json!({
|
||||
"id": ThreadId::new().to_string(),
|
||||
"target": ThreadId::new().to_string(),
|
||||
"message": "hello",
|
||||
"items": [{"type": "mention", "name": "drive", "path": "app://drive"}]
|
||||
})),
|
||||
@@ -449,7 +636,7 @@ async fn send_input_rejects_invalid_id() {
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"send_input",
|
||||
function_payload(json!({"id": "not-a-uuid", "message": "hi"})),
|
||||
function_payload(json!({"target": "not-a-uuid", "message": "hi"})),
|
||||
);
|
||||
let Err(err) = SendInputHandler.handle(invocation).await else {
|
||||
panic!("invalid id should be rejected");
|
||||
@@ -457,7 +644,10 @@ async fn send_input_rejects_invalid_id() {
|
||||
let FunctionCallError::RespondToModel(msg) = err else {
|
||||
panic!("expected respond-to-model error");
|
||||
};
|
||||
assert!(msg.starts_with("invalid agent id not-a-uuid:"));
|
||||
assert_eq!(
|
||||
msg,
|
||||
"agent_name must use only lowercase letters, digits, and underscores"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -470,7 +660,7 @@ async fn send_input_reports_missing_agent() {
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"send_input",
|
||||
function_payload(json!({"id": agent_id.to_string(), "message": "hi"})),
|
||||
function_payload(json!({"target": agent_id.to_string(), "message": "hi"})),
|
||||
);
|
||||
let Err(err) = SendInputHandler.handle(invocation).await else {
|
||||
panic!("missing agent should be reported");
|
||||
@@ -494,7 +684,7 @@ async fn send_input_interrupts_before_prompt() {
|
||||
Arc::new(turn),
|
||||
"send_input",
|
||||
function_payload(json!({
|
||||
"id": agent_id.to_string(),
|
||||
"target": agent_id.to_string(),
|
||||
"message": "hi",
|
||||
"interrupt": true
|
||||
})),
|
||||
@@ -533,7 +723,7 @@ async fn send_input_accepts_structured_items() {
|
||||
Arc::new(turn),
|
||||
"send_input",
|
||||
function_payload(json!({
|
||||
"id": agent_id.to_string(),
|
||||
"target": agent_id.to_string(),
|
||||
"items": [
|
||||
{"type": "mention", "name": "drive", "path": "app://google_drive"},
|
||||
{"type": "text", "text": "read the folder"}
|
||||
@@ -703,7 +893,7 @@ async fn resume_agent_restores_closed_agent_and_accepts_send_input() {
|
||||
session,
|
||||
turn,
|
||||
"send_input",
|
||||
function_payload(json!({"id": agent_id.to_string(), "message": "hello"})),
|
||||
function_payload(json!({"target": agent_id.to_string(), "message": "hello"})),
|
||||
);
|
||||
let output = SendInputHandler
|
||||
.handle(send_invocation)
|
||||
@@ -736,6 +926,7 @@ async fn resume_agent_rejects_when_depth_limit_exceeded() {
|
||||
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: session.conversation_id,
|
||||
depth: max_depth,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
@@ -765,7 +956,7 @@ async fn wait_agent_rejects_non_positive_timeout() {
|
||||
Arc::new(turn),
|
||||
"wait_agent",
|
||||
function_payload(json!({
|
||||
"ids": [ThreadId::new().to_string()],
|
||||
"targets": [ThreadId::new().to_string()],
|
||||
"timeout_ms": 0
|
||||
})),
|
||||
);
|
||||
@@ -779,13 +970,13 @@ async fn wait_agent_rejects_non_positive_timeout() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_agent_rejects_invalid_id() {
|
||||
async fn wait_agent_rejects_invalid_target() {
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let invocation = invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"wait_agent",
|
||||
function_payload(json!({"ids": ["invalid"]})),
|
||||
function_payload(json!({"targets": ["invalid"]})),
|
||||
);
|
||||
let Err(err) = WaitAgentHandler.handle(invocation).await else {
|
||||
panic!("invalid id should be rejected");
|
||||
@@ -793,27 +984,62 @@ async fn wait_agent_rejects_invalid_id() {
|
||||
let FunctionCallError::RespondToModel(msg) = err else {
|
||||
panic!("expected respond-to-model error");
|
||||
};
|
||||
assert!(msg.starts_with("invalid agent id invalid:"));
|
||||
assert_eq!(msg, "live agent path `/root/invalid` not found");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_agent_rejects_empty_ids() {
|
||||
async fn wait_agent_rejects_empty_targets() {
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let invocation = invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"wait_agent",
|
||||
function_payload(json!({"ids": []})),
|
||||
function_payload(json!({"targets": []})),
|
||||
);
|
||||
let Err(err) = WaitAgentHandler.handle(invocation).await else {
|
||||
panic!("empty ids should be rejected");
|
||||
};
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel("ids must be non-empty".to_string())
|
||||
FunctionCallError::RespondToModel("agent targets must be non-empty".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_wait_agent_accepts_targets_argument() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let target = ThreadId::new().to_string();
|
||||
let manager = thread_manager();
|
||||
session.services.agent_control = manager.agent_control();
|
||||
let mut config = (*turn.config).clone();
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
turn.config = Arc::new(config);
|
||||
let invocation = invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"wait_agent",
|
||||
function_payload(json!({"targets": [target.clone()]})),
|
||||
);
|
||||
let output = WaitAgentHandler
|
||||
.handle(invocation)
|
||||
.await
|
||||
.expect("targets should be accepted in v2 mode");
|
||||
let (content, success) = expect_text_output(output);
|
||||
let result: wait::WaitAgentResult =
|
||||
serde_json::from_str(&content).expect("wait_agent result should be json");
|
||||
assert_eq!(
|
||||
result,
|
||||
wait::WaitAgentResult {
|
||||
status: HashMap::from([(target, AgentStatus::NotFound)]),
|
||||
timed_out: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(success, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_agent_returns_not_found_for_missing_agents() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
@@ -826,7 +1052,7 @@ async fn wait_agent_returns_not_found_for_missing_agents() {
|
||||
Arc::new(turn),
|
||||
"wait_agent",
|
||||
function_payload(json!({
|
||||
"ids": [id_a.to_string(), id_b.to_string()],
|
||||
"targets": [id_a.to_string(), id_b.to_string()],
|
||||
"timeout_ms": 1000
|
||||
})),
|
||||
);
|
||||
@@ -840,7 +1066,10 @@ async fn wait_agent_returns_not_found_for_missing_agents() {
|
||||
assert_eq!(
|
||||
result,
|
||||
wait::WaitAgentResult {
|
||||
status: HashMap::from([(id_a, AgentStatus::NotFound), (id_b, AgentStatus::NotFound),]),
|
||||
status: HashMap::from([
|
||||
(id_a.to_string(), AgentStatus::NotFound),
|
||||
(id_b.to_string(), AgentStatus::NotFound),
|
||||
]),
|
||||
timed_out: false
|
||||
}
|
||||
);
|
||||
@@ -860,7 +1089,7 @@ async fn wait_agent_times_out_when_status_is_not_final() {
|
||||
Arc::new(turn),
|
||||
"wait_agent",
|
||||
function_payload(json!({
|
||||
"ids": [agent_id.to_string()],
|
||||
"targets": [agent_id.to_string()],
|
||||
"timeout_ms": MIN_WAIT_TIMEOUT_MS
|
||||
})),
|
||||
);
|
||||
@@ -900,7 +1129,7 @@ async fn wait_agent_clamps_short_timeouts_to_minimum() {
|
||||
Arc::new(turn),
|
||||
"wait_agent",
|
||||
function_payload(json!({
|
||||
"ids": [agent_id.to_string()],
|
||||
"targets": [agent_id.to_string()],
|
||||
"timeout_ms": 10
|
||||
})),
|
||||
);
|
||||
@@ -950,7 +1179,7 @@ async fn wait_agent_returns_final_status_without_timeout() {
|
||||
Arc::new(turn),
|
||||
"wait_agent",
|
||||
function_payload(json!({
|
||||
"ids": [agent_id.to_string()],
|
||||
"targets": [agent_id.to_string()],
|
||||
"timeout_ms": 1000
|
||||
})),
|
||||
);
|
||||
@@ -964,13 +1193,106 @@ async fn wait_agent_returns_final_status_without_timeout() {
|
||||
assert_eq!(
|
||||
result,
|
||||
wait::WaitAgentResult {
|
||||
status: HashMap::from([(agent_id, AgentStatus::Shutdown)]),
|
||||
status: HashMap::from([(agent_id.to_string(), AgentStatus::Shutdown)]),
|
||||
timed_out: false
|
||||
}
|
||||
);
|
||||
assert_eq!(success, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_wait_agent_returns_statuses_keyed_by_path() {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SpawnAgentResult {
|
||||
task_name: String,
|
||||
}
|
||||
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
.start_thread((*turn.config).clone())
|
||||
.await
|
||||
.expect("root thread should start");
|
||||
session.services.agent_control = manager.agent_control();
|
||||
session.conversation_id = root.thread_id;
|
||||
let mut config = (*turn.config).clone();
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
let spawn_output = SpawnAgentHandler
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"message": "inspect this repo",
|
||||
"task_name": "test_process"
|
||||
})),
|
||||
))
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
let (content, _) = expect_text_output(spawn_output);
|
||||
let spawn_result: SpawnAgentResult =
|
||||
serde_json::from_str(&content).expect("spawn result should parse");
|
||||
|
||||
let agent_id = session
|
||||
.services
|
||||
.agent_control
|
||||
.resolve_agent_reference(
|
||||
session.conversation_id,
|
||||
&turn.session_source,
|
||||
"test_process",
|
||||
)
|
||||
.await
|
||||
.expect("relative path should resolve");
|
||||
let mut status_rx = manager
|
||||
.agent_control()
|
||||
.subscribe_status(agent_id)
|
||||
.await
|
||||
.expect("subscribe should succeed");
|
||||
|
||||
let child_thread = manager
|
||||
.get_thread(agent_id)
|
||||
.await
|
||||
.expect("child should exist");
|
||||
let _ = child_thread
|
||||
.submit(Op::Shutdown {})
|
||||
.await
|
||||
.expect("shutdown should submit");
|
||||
let _ = timeout(Duration::from_secs(1), status_rx.changed())
|
||||
.await
|
||||
.expect("shutdown status should arrive");
|
||||
|
||||
let wait_output = WaitAgentHandler
|
||||
.handle(invocation(
|
||||
session,
|
||||
turn,
|
||||
"wait_agent",
|
||||
function_payload(json!({
|
||||
"targets": ["test_process"],
|
||||
"timeout_ms": 1000
|
||||
})),
|
||||
))
|
||||
.await
|
||||
.expect("wait_agent should succeed");
|
||||
let (content, success) = expect_text_output(wait_output);
|
||||
let result: wait::WaitAgentResult =
|
||||
serde_json::from_str(&content).expect("wait_agent result should be json");
|
||||
assert_eq!(
|
||||
result,
|
||||
wait::WaitAgentResult {
|
||||
status: HashMap::from([(spawn_result.task_name, AgentStatus::Shutdown)]),
|
||||
timed_out: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(success, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_agent_submits_shutdown_and_returns_previous_status() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
@@ -985,7 +1307,7 @@ async fn close_agent_submits_shutdown_and_returns_previous_status() {
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"close_agent",
|
||||
function_payload(json!({"id": agent_id.to_string()})),
|
||||
function_payload(json!({"target": agent_id.to_string()})),
|
||||
);
|
||||
let output = CloseAgentHandler
|
||||
.handle(invocation)
|
||||
@@ -1037,13 +1359,12 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
||||
let (child_content, child_success) = expect_text_output(child_spawn_output);
|
||||
let child_result: serde_json::Value =
|
||||
serde_json::from_str(&child_content).expect("child spawn result should be json");
|
||||
let child_thread_id = agent_id(
|
||||
let child_thread_id = parse_agent_id(
|
||||
child_result
|
||||
.get("agent_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.expect("child spawn result should include agent_id"),
|
||||
)
|
||||
.expect("child agent_id should be valid");
|
||||
);
|
||||
assert_eq!(child_success, Some(true));
|
||||
|
||||
let child_thread = manager
|
||||
@@ -1063,13 +1384,12 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
||||
let (grandchild_content, grandchild_success) = expect_text_output(grandchild_spawn_output);
|
||||
let grandchild_result: serde_json::Value =
|
||||
serde_json::from_str(&grandchild_content).expect("grandchild spawn result should be json");
|
||||
let grandchild_thread_id = agent_id(
|
||||
let grandchild_thread_id = parse_agent_id(
|
||||
grandchild_result
|
||||
.get("agent_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.expect("grandchild spawn result should include agent_id"),
|
||||
)
|
||||
.expect("grandchild agent_id should be valid");
|
||||
);
|
||||
assert_eq!(grandchild_success, Some(true));
|
||||
|
||||
let close_output = CloseAgentHandler
|
||||
@@ -1077,7 +1397,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
||||
parent_session.clone(),
|
||||
parent_session.new_default_turn().await,
|
||||
"close_agent",
|
||||
function_payload(json!({"id": child_thread_id.to_string()})),
|
||||
function_payload(json!({"target": child_thread_id.to_string()})),
|
||||
))
|
||||
.await
|
||||
.expect("close_agent should close the child subtree");
|
||||
@@ -1129,7 +1449,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
||||
parent_session.clone(),
|
||||
parent_session.new_default_turn().await,
|
||||
"close_agent",
|
||||
function_payload(json!({"id": child_thread_id.to_string()})),
|
||||
function_payload(json!({"target": child_thread_id.to_string()})),
|
||||
))
|
||||
.await
|
||||
.expect("close_agent should be repeatable for the child subtree");
|
||||
|
||||
@@ -129,20 +129,29 @@ fn agent_status_output_schema() -> JsonValue {
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_agent_output_schema() -> JsonValue {
|
||||
fn spawn_agent_output_schema(multi_agent_v2: bool) -> JsonValue {
|
||||
let task_name_description = if multi_agent_v2 {
|
||||
"Canonical task name for the spawned agent."
|
||||
} else {
|
||||
"Canonical task name for the spawned agent when one was assigned."
|
||||
};
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_id": {
|
||||
"type": "string",
|
||||
"description": "Thread identifier for the spawned agent."
|
||||
"type": ["string", "null"],
|
||||
"description": "Thread identifier for the spawned agent when no task name was assigned."
|
||||
},
|
||||
"task_name": {
|
||||
"type": ["string", "null"],
|
||||
"description": task_name_description
|
||||
},
|
||||
"nickname": {
|
||||
"type": ["string", "null"],
|
||||
"description": "User-facing nickname for the spawned agent when available."
|
||||
}
|
||||
},
|
||||
"required": ["agent_id", "nickname"],
|
||||
"required": ["agent_id", "task_name", "nickname"],
|
||||
"additionalProperties": false
|
||||
})
|
||||
}
|
||||
@@ -178,7 +187,7 @@ fn wait_output_schema() -> JsonValue {
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "object",
|
||||
"description": "Final statuses keyed by agent id for agents that finished before the timeout.",
|
||||
"description": "Final statuses keyed by canonical task name when available, otherwise by agent id.",
|
||||
"additionalProperties": agent_status_output_schema()
|
||||
},
|
||||
"timed_out": {
|
||||
@@ -276,6 +285,7 @@ pub(crate) struct ToolsConfig {
|
||||
pub js_repl_tools_only: bool,
|
||||
pub can_request_original_image_detail: bool,
|
||||
pub collab_tools: bool,
|
||||
pub multi_agent_v2: bool,
|
||||
pub artifact_tools: bool,
|
||||
pub request_user_input: bool,
|
||||
pub default_mode_request_user_input: bool,
|
||||
@@ -325,6 +335,7 @@ impl ToolsConfig {
|
||||
let include_js_repl_tools_only =
|
||||
include_js_repl && features.enabled(Feature::JsReplToolsOnly);
|
||||
let include_collab_tools = features.enabled(Feature::Collab);
|
||||
let include_multi_agent_v2 = features.enabled(Feature::MultiAgentV2);
|
||||
let include_agent_jobs = features.enabled(Feature::SpawnCsv);
|
||||
let include_request_user_input = !matches!(session_source, SessionSource::SubAgent(_));
|
||||
let include_default_mode_request_user_input =
|
||||
@@ -408,6 +419,7 @@ impl ToolsConfig {
|
||||
js_repl_tools_only: include_js_repl_tools_only,
|
||||
can_request_original_image_detail: include_original_image_detail,
|
||||
collab_tools: include_collab_tools,
|
||||
multi_agent_v2: include_multi_agent_v2,
|
||||
artifact_tools: include_artifact_tools,
|
||||
request_user_input: include_request_user_input,
|
||||
default_mode_request_user_input: include_default_mode_request_user_input,
|
||||
@@ -1076,7 +1088,8 @@ fn create_collab_input_items_schema() -> JsonSchema {
|
||||
|
||||
fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec {
|
||||
let available_models_description = spawn_agent_models_description(&config.available_models);
|
||||
let properties = BTreeMap::from([
|
||||
let return_value_description = "Returns the canonical task name when the spawned agent was named, otherwise the agent id, plus the user-facing nickname when available.";
|
||||
let mut properties = BTreeMap::from([
|
||||
(
|
||||
"message".to_string(),
|
||||
JsonSchema::String {
|
||||
@@ -1123,6 +1136,15 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec {
|
||||
},
|
||||
),
|
||||
]);
|
||||
properties.insert(
|
||||
"task_name".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Optional task name for the new agent. Use lowercase letters, digits, and underscores."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "spawn_agent".to_string(),
|
||||
@@ -1131,7 +1153,7 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec {
|
||||
Only use `spawn_agent` if and only if the user explicitly asks for sub-agents, delegation, or parallel agent work.
|
||||
Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn.
|
||||
Agent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself.
|
||||
Spawn a sub-agent for a well-scoped task. Returns the agent id (and user-facing nickname when available) to use to communicate with this agent. This spawn_agent tool provides you access to smaller but more efficient sub-agents. A mini model can solve many tasks faster than the main model. You should follow the rules and guidelines below to use this tool.
|
||||
Spawn a sub-agent for a well-scoped task. {return_value_description} This spawn_agent tool provides you access to smaller but more efficient sub-agents. A mini model can solve many tasks faster than the main model. You should follow the rules and guidelines below to use this tool.
|
||||
|
||||
{available_models_description}
|
||||
### When to delegate vs. do the subtask yourself
|
||||
@@ -1170,7 +1192,7 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec {
|
||||
required: None,
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: Some(spawn_agent_output_schema()),
|
||||
output_schema: Some(spawn_agent_output_schema(config.multi_agent_v2)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1335,9 +1357,11 @@ fn create_report_agent_job_result_tool() -> ToolSpec {
|
||||
fn create_send_input_tool() -> ToolSpec {
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"id".to_string(),
|
||||
"target".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Agent id to message (from spawn_agent).".to_string()),
|
||||
description: Some(
|
||||
"Agent id or canonical task name to message (from spawn_agent).".to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
@@ -1369,7 +1393,7 @@ fn create_send_input_tool() -> ToolSpec {
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["id".to_string()]),
|
||||
required: Some(vec!["target".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: Some(send_input_output_schema()),
|
||||
@@ -1404,11 +1428,11 @@ fn create_resume_agent_tool() -> ToolSpec {
|
||||
fn create_wait_agent_tool() -> ToolSpec {
|
||||
let mut properties = BTreeMap::new();
|
||||
properties.insert(
|
||||
"ids".to_string(),
|
||||
"targets".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String { description: None }),
|
||||
description: Some(
|
||||
"Agent ids to wait on. Pass multiple ids to wait for whichever finishes first."
|
||||
"Agent ids or canonical task names to wait on. Pass multiple targets to wait for whichever finishes first."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
@@ -1430,7 +1454,7 @@ fn create_wait_agent_tool() -> ToolSpec {
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["ids".to_string()]),
|
||||
required: Some(vec!["targets".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: Some(wait_output_schema()),
|
||||
@@ -1556,9 +1580,11 @@ fn create_request_permissions_tool() -> ToolSpec {
|
||||
fn create_close_agent_tool() -> ToolSpec {
|
||||
let mut properties = BTreeMap::new();
|
||||
properties.insert(
|
||||
"id".to_string(),
|
||||
"target".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Agent id to close (from spawn_agent).".to_string()),
|
||||
description: Some(
|
||||
"Agent id or canonical task name to close (from spawn_agent).".to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1569,7 +1595,7 @@ fn create_close_agent_tool() -> ToolSpec {
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["id".to_string()]),
|
||||
required: Some(vec!["target".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: Some(close_agent_output_schema()),
|
||||
@@ -2966,12 +2992,15 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
push_tool_spec(
|
||||
&mut builder,
|
||||
create_resume_agent_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
if !config.multi_agent_v2 {
|
||||
push_tool_spec(
|
||||
&mut builder,
|
||||
create_resume_agent_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
builder.register_handler("resume_agent", Arc::new(ResumeAgentHandler));
|
||||
}
|
||||
push_tool_spec(
|
||||
&mut builder,
|
||||
create_wait_agent_tool(),
|
||||
@@ -2986,7 +3015,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
);
|
||||
builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandler));
|
||||
builder.register_handler("send_input", Arc::new(SendInputHandler));
|
||||
builder.register_handler("resume_agent", Arc::new(ResumeAgentHandler));
|
||||
builder.register_handler("wait_agent", Arc::new(WaitAgentHandler));
|
||||
builder.register_handler("close_agent", Arc::new(CloseAgentHandler));
|
||||
}
|
||||
|
||||
@@ -469,12 +469,15 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
||||
create_view_image_tool(config.can_request_original_image_detail),
|
||||
create_spawn_agent_tool(&config),
|
||||
create_send_input_tool(),
|
||||
create_resume_agent_tool(),
|
||||
create_wait_agent_tool(),
|
||||
create_close_agent_tool(),
|
||||
] {
|
||||
expected.insert(tool_name(&spec).to_string(), spec);
|
||||
}
|
||||
if !config.multi_agent_v2 {
|
||||
let spec = create_resume_agent_tool();
|
||||
expected.insert(tool_name(&spec).to_string(), spec);
|
||||
}
|
||||
|
||||
if config.exec_permission_approvals_enabled {
|
||||
let spec = create_request_permissions_tool();
|
||||
@@ -520,6 +523,96 @@ fn test_build_specs_collab_tools_enabled() {
|
||||
assert_lacks_tool_name(&tools, "spawn_agents_on_csv");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
|
||||
let config = test_config();
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Collab);
|
||||
features.enable(Feature::MultiAgentV2);
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
||||
|
||||
let spawn_agent = find_tool(&tools, "spawn_agent");
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
parameters,
|
||||
output_schema,
|
||||
..
|
||||
}) = &spawn_agent.spec
|
||||
else {
|
||||
panic!("spawn_agent should be a function tool");
|
||||
};
|
||||
let JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
..
|
||||
} = parameters
|
||||
else {
|
||||
panic!("spawn_agent should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("task_name"));
|
||||
assert_eq!(required.as_ref(), None);
|
||||
let output_schema = output_schema
|
||||
.as_ref()
|
||||
.expect("spawn_agent should define output schema");
|
||||
assert_eq!(
|
||||
output_schema["required"],
|
||||
json!(["agent_id", "task_name", "nickname"])
|
||||
);
|
||||
|
||||
let send_input = find_tool(&tools, "send_input");
|
||||
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &send_input.spec else {
|
||||
panic!("send_input should be a function tool");
|
||||
};
|
||||
let JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
..
|
||||
} = parameters
|
||||
else {
|
||||
panic!("send_input should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("target"));
|
||||
assert_eq!(required.as_ref(), Some(&vec!["target".to_string()]));
|
||||
|
||||
let wait_agent = find_tool(&tools, "wait_agent");
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
parameters,
|
||||
output_schema,
|
||||
..
|
||||
}) = &wait_agent.spec
|
||||
else {
|
||||
panic!("wait_agent should be a function tool");
|
||||
};
|
||||
let JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
..
|
||||
} = parameters
|
||||
else {
|
||||
panic!("wait_agent should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("targets"));
|
||||
assert_eq!(required.as_ref(), Some(&vec!["targets".to_string()]));
|
||||
let output_schema = output_schema
|
||||
.as_ref()
|
||||
.expect("wait_agent should define output schema");
|
||||
assert_eq!(
|
||||
output_schema["properties"]["status"]["description"],
|
||||
json!("Final statuses keyed by canonical task name when available, otherwise by agent id.")
|
||||
);
|
||||
assert_lacks_tool_name(&tools, "resume_agent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() {
|
||||
let config = test_config();
|
||||
|
||||
@@ -66,6 +66,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
|
||||
originator: "test_originator".to_string(),
|
||||
cli_version: "test_version".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
@@ -110,6 +111,7 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re
|
||||
originator: "test_originator".to_string(),
|
||||
cli_version: "test_version".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
|
||||
@@ -141,6 +141,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
originator: "test".to_string(),
|
||||
cli_version: "test".to_string(),
|
||||
source: SessionSource::default(),
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
|
||||
+6
-2
@@ -73,8 +73,12 @@ ignore = [
|
||||
{ id = "RUSTSEC-2024-0388", reason = "derivative is unmaintained; pulled in via starlark v0.13.0 used by execpolicy/cli/core; no fixed release yet" },
|
||||
{ id = "RUSTSEC-2025-0057", reason = "fxhash is unmaintained; pulled in via starlark_map/starlark v0.13.0 used by execpolicy/cli/core; no fixed release yet" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste is unmaintained; pulled in via ratatui/rmcp/starlark used by tui/execpolicy; no fixed release yet" },
|
||||
# TODO(joshka, nornagon): remove this exception when once we update the ratatui fork to a version that uses lru 0.13+.
|
||||
{ id = "RUSTSEC-2026-0002", reason = "lru 0.12.5 is pulled in via ratatui fork; cannot upgrade until the fork is updated" },
|
||||
# TODO(fcoury): remove these exceptions when the aws-lc-sys upgrade path is Bazel-compatible in this workspace.
|
||||
{ id = "RUSTSEC-2026-0044", reason = "aws-lc-sys remains on 0.37.0 because upgrading currently breaks Bazel fetch/build for this workspace" },
|
||||
{ id = "RUSTSEC-2026-0045", reason = "aws-lc-sys remains on 0.37.0 because upgrading currently breaks Bazel fetch/build for this workspace" },
|
||||
{ id = "RUSTSEC-2026-0046", reason = "aws-lc-sys remains on 0.37.0 because upgrading currently breaks Bazel fetch/build for this workspace" },
|
||||
{ id = "RUSTSEC-2026-0047", reason = "aws-lc-sys remains on 0.37.0 because upgrading currently breaks Bazel fetch/build for this workspace" },
|
||||
{ id = "RUSTSEC-2026-0048", reason = "aws-lc-sys remains on 0.37.0 because upgrading currently breaks Bazel fetch/build for this workspace" },
|
||||
# TODO(fcoury): remove this exception when syntect drops yaml-rust and bincode, or updates to versions that have fixed the vulnerabilities.
|
||||
{ id = "RUSTSEC-2024-0320", reason = "yaml-rust is unmaintained; pulled in via syntect v5.3.0 used by codex-tui for syntax highlighting; no fixed release yet" },
|
||||
{ id = "RUSTSEC-2025-0141", reason = "bincode is unmaintained; pulled in via syntect v5.3.0 used by codex-tui for syntax highlighting; no fixed release yet" },
|
||||
|
||||
@@ -138,6 +138,8 @@ pub enum Feature {
|
||||
EnableRequestCompression,
|
||||
/// Enable collab tools.
|
||||
Collab,
|
||||
/// Enable task-path-based multi-agent routing.
|
||||
MultiAgentV2,
|
||||
/// Enable CSV-backed agent job tools.
|
||||
SpawnCsv,
|
||||
/// Enable apps.
|
||||
@@ -711,6 +713,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::MultiAgentV2,
|
||||
key: "multi_agent_v2",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::SpawnCsv,
|
||||
key: "enable_fanout",
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, TS,
|
||||
)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[schemars(with = "String")]
|
||||
#[ts(type = "string")]
|
||||
pub struct AgentPath(String);
|
||||
|
||||
impl AgentPath {
|
||||
pub const ROOT: &str = "/root";
|
||||
const ROOT_SEGMENT: &str = "root";
|
||||
|
||||
pub fn root() -> Self {
|
||||
Self(Self::ROOT.to_string())
|
||||
}
|
||||
|
||||
pub fn from_string(path: String) -> Result<Self, String> {
|
||||
validate_absolute_path(path.as_str())?;
|
||||
Ok(Self(path))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.as_str() == Self::ROOT
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
if self.is_root() {
|
||||
return Self::ROOT_SEGMENT;
|
||||
}
|
||||
self.as_str()
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.filter(|segment| !segment.is_empty())
|
||||
.unwrap_or(Self::ROOT_SEGMENT)
|
||||
}
|
||||
|
||||
pub fn join(&self, agent_name: &str) -> Result<Self, String> {
|
||||
validate_agent_name(agent_name)?;
|
||||
Self::from_string(format!("{self}/{agent_name}"))
|
||||
}
|
||||
|
||||
pub fn resolve(&self, reference: &str) -> Result<Self, String> {
|
||||
if reference.is_empty() {
|
||||
return Err("agent path must not be empty".to_string());
|
||||
}
|
||||
if reference == Self::ROOT {
|
||||
return Ok(Self::root());
|
||||
}
|
||||
if reference.starts_with('/') {
|
||||
return Self::try_from(reference);
|
||||
}
|
||||
|
||||
validate_relative_reference(reference)?;
|
||||
Self::from_string(format!("{self}/{reference}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for AgentPath {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Self::from_string(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for AgentPath {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
Self::from_string(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AgentPath> for String {
|
||||
fn from(value: AgentPath) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AgentPath {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for AgentPath {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AgentPath {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AgentPath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_agent_name(agent_name: &str) -> Result<(), String> {
|
||||
if agent_name.is_empty() {
|
||||
return Err("agent_name must not be empty".to_string());
|
||||
}
|
||||
if agent_name == AgentPath::ROOT_SEGMENT {
|
||||
return Err("agent_name `root` is reserved".to_string());
|
||||
}
|
||||
if agent_name == "." || agent_name == ".." {
|
||||
return Err(format!("agent_name `{agent_name}` is reserved"));
|
||||
}
|
||||
if agent_name.contains('/') {
|
||||
return Err("agent_name must not contain `/`".to_string());
|
||||
}
|
||||
if !agent_name
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
|
||||
{
|
||||
return Err(
|
||||
"agent_name must use only lowercase letters, digits, and underscores".to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_absolute_path(path: &str) -> Result<(), String> {
|
||||
let Some(stripped) = path.strip_prefix('/') else {
|
||||
return Err("absolute agent paths must start with `/root`".to_string());
|
||||
};
|
||||
let mut segments = stripped.split('/');
|
||||
let Some(root) = segments.next() else {
|
||||
return Err("absolute agent path must not be empty".to_string());
|
||||
};
|
||||
if root != AgentPath::ROOT_SEGMENT {
|
||||
return Err("absolute agent paths must start with `/root`".to_string());
|
||||
}
|
||||
if stripped.ends_with('/') {
|
||||
return Err("absolute agent path must not end with `/`".to_string());
|
||||
}
|
||||
for segment in segments {
|
||||
validate_agent_name(segment)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_relative_reference(reference: &str) -> Result<(), String> {
|
||||
if reference.ends_with('/') {
|
||||
return Err("relative agent path must not end with `/`".to_string());
|
||||
}
|
||||
for segment in reference.split('/') {
|
||||
validate_agent_name(segment)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::AgentPath;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn root_has_expected_name() {
|
||||
let root = AgentPath::root();
|
||||
assert_eq!(root.as_str(), AgentPath::ROOT);
|
||||
assert_eq!(root.name(), "root");
|
||||
assert!(root.is_root());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_builds_child_paths() {
|
||||
let root = AgentPath::root();
|
||||
let child = root.join("researcher").expect("child path");
|
||||
assert_eq!(child.as_str(), "/root/researcher");
|
||||
assert_eq!(child.name(), "researcher");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_supports_relative_and_absolute_references() {
|
||||
let current = AgentPath::try_from("/root/researcher").expect("path");
|
||||
assert_eq!(
|
||||
current.resolve("worker").expect("relative path"),
|
||||
AgentPath::try_from("/root/researcher/worker").expect("path")
|
||||
);
|
||||
assert_eq!(
|
||||
current.resolve("/root/other").expect("absolute path"),
|
||||
AgentPath::try_from("/root/other").expect("path")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_names_and_paths_are_rejected() {
|
||||
assert_eq!(
|
||||
AgentPath::root().join("BadName"),
|
||||
Err("agent_name must use only lowercase letters, digits, and underscores".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
AgentPath::try_from("/not-root"),
|
||||
Err("absolute agent paths must start with `/root`".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
AgentPath::root().resolve("../sibling"),
|
||||
Err("agent_name `..` is reserved".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod account;
|
||||
mod agent_path;
|
||||
mod thread_id;
|
||||
pub use agent_path::AgentPath;
|
||||
pub use thread_id::ThreadId;
|
||||
pub mod approvals;
|
||||
pub mod config_types;
|
||||
|
||||
@@ -12,6 +12,7 @@ use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::AgentPath;
|
||||
use crate::ThreadId;
|
||||
use crate::approvals::ElicitationRequestEvent;
|
||||
use crate::config_types::ApprovalsReviewer;
|
||||
@@ -2288,6 +2289,8 @@ pub enum SubAgentSource {
|
||||
parent_thread_id: ThreadId,
|
||||
depth: i32,
|
||||
#[serde(default)]
|
||||
agent_path: Option<AgentPath>,
|
||||
#[serde(default)]
|
||||
agent_nickname: Option<String>,
|
||||
#[serde(default, alias = "agent_type")]
|
||||
agent_role: Option<String>,
|
||||
@@ -2351,6 +2354,16 @@ impl SessionSource {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_agent_path(&self) -> Option<AgentPath> {
|
||||
match self {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_path, .. }) => {
|
||||
agent_path.clone()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restriction_product(&self) -> Option<Product> {
|
||||
match self {
|
||||
SessionSource::Custom(source) => Product::from_session_source_name(source),
|
||||
@@ -2411,6 +2424,9 @@ pub struct SessionMeta {
|
||||
/// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
#[serde(default, alias = "agent_type", skip_serializing_if = "Option::is_none")]
|
||||
pub agent_role: Option<String>,
|
||||
/// Optional canonical agent path assigned to an AgentControl-spawned sub-agent.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_path: Option<String>,
|
||||
pub model_provider: Option<String>,
|
||||
/// base_instructions for the session. This *should* always be present when creating a new session,
|
||||
/// but may be missing for older sessions. If not present, fall back to rendering the base_instructions
|
||||
@@ -2434,6 +2450,7 @@ impl Default for SessionMeta {
|
||||
source: SessionSource::default(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE threads ADD COLUMN agent_path TEXT;
|
||||
@@ -50,6 +50,7 @@ fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &Sessi
|
||||
metadata.source = enum_to_string(&meta_line.meta.source);
|
||||
metadata.agent_nickname = meta_line.meta.agent_nickname.clone();
|
||||
metadata.agent_role = meta_line.meta.agent_role.clone();
|
||||
metadata.agent_path = meta_line.meta.agent_path.clone();
|
||||
if let Some(provider) = meta_line.meta.model_provider.as_deref() {
|
||||
metadata.model_provider = provider.to_string();
|
||||
}
|
||||
@@ -251,6 +252,7 @@ mod tests {
|
||||
originator: "codex_cli_rs".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
@@ -377,6 +379,7 @@ mod tests {
|
||||
originator: "codex_cli_rs".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
@@ -402,6 +405,7 @@ mod tests {
|
||||
created_at,
|
||||
updated_at: created_at,
|
||||
source: "cli".to_string(),
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: "openai".to_string(),
|
||||
|
||||
@@ -69,6 +69,8 @@ pub struct ThreadMetadata {
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
pub agent_role: Option<String>,
|
||||
/// Optional canonical agent path assigned to an AgentControl-spawned sub-agent.
|
||||
pub agent_path: Option<String>,
|
||||
/// The model provider identifier.
|
||||
pub model_provider: String,
|
||||
/// The latest observed model for the thread.
|
||||
@@ -116,6 +118,8 @@ pub struct ThreadMetadataBuilder {
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Optional role (agent_role) assigned to the session.
|
||||
pub agent_role: Option<String>,
|
||||
/// Optional canonical agent path assigned to the session.
|
||||
pub agent_path: Option<String>,
|
||||
/// The model provider identifier, if known.
|
||||
pub model_provider: Option<String>,
|
||||
/// The working directory for the thread.
|
||||
@@ -152,6 +156,7 @@ impl ThreadMetadataBuilder {
|
||||
source,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: None,
|
||||
cwd: PathBuf::new(),
|
||||
cli_version: None,
|
||||
@@ -182,6 +187,10 @@ impl ThreadMetadataBuilder {
|
||||
source,
|
||||
agent_nickname: self.agent_nickname.clone(),
|
||||
agent_role: self.agent_role.clone(),
|
||||
agent_path: self
|
||||
.agent_path
|
||||
.clone()
|
||||
.or_else(|| self.source.get_agent_path().map(Into::into)),
|
||||
model_provider: self
|
||||
.model_provider
|
||||
.clone()
|
||||
@@ -241,6 +250,9 @@ impl ThreadMetadata {
|
||||
if self.agent_role != other.agent_role {
|
||||
diffs.push("agent_role");
|
||||
}
|
||||
if self.agent_path != other.agent_path {
|
||||
diffs.push("agent_path");
|
||||
}
|
||||
if self.model_provider != other.model_provider {
|
||||
diffs.push("model_provider");
|
||||
}
|
||||
@@ -300,6 +312,7 @@ pub(crate) struct ThreadRow {
|
||||
source: String,
|
||||
agent_nickname: Option<String>,
|
||||
agent_role: Option<String>,
|
||||
agent_path: Option<String>,
|
||||
model_provider: String,
|
||||
model: Option<String>,
|
||||
reasoning_effort: Option<String>,
|
||||
@@ -326,6 +339,7 @@ impl ThreadRow {
|
||||
source: row.try_get("source")?,
|
||||
agent_nickname: row.try_get("agent_nickname")?,
|
||||
agent_role: row.try_get("agent_role")?,
|
||||
agent_path: row.try_get("agent_path")?,
|
||||
model_provider: row.try_get("model_provider")?,
|
||||
model: row.try_get("model")?,
|
||||
reasoning_effort: row.try_get("reasoning_effort")?,
|
||||
@@ -356,6 +370,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
@@ -379,6 +394,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort: reasoning_effort
|
||||
@@ -447,6 +463,7 @@ mod tests {
|
||||
source: "cli".to_string(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: "openai".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
reasoning_effort: reasoning_effort.map(str::to_string),
|
||||
@@ -474,6 +491,7 @@ mod tests {
|
||||
source: "cli".to_string(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: "openai".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
reasoning_effort,
|
||||
|
||||
@@ -166,6 +166,7 @@ SELECT
|
||||
created_at,
|
||||
updated_at,
|
||||
source,
|
||||
agent_path,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
|
||||
@@ -50,6 +50,7 @@ pub(super) fn test_thread_metadata(
|
||||
source: "cli".to_string(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
reasoning_effort: Some(ReasoningEffort::Medium),
|
||||
|
||||
@@ -13,6 +13,7 @@ SELECT
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
@@ -142,6 +143,62 @@ ON CONFLICT(child_thread_id) DO UPDATE SET
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find a direct spawned child of `parent_thread_id` by canonical agent path.
|
||||
pub async fn find_thread_spawn_child_by_path(
|
||||
&self,
|
||||
parent_thread_id: ThreadId,
|
||||
agent_path: &str,
|
||||
) -> anyhow::Result<Option<ThreadId>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT threads.id
|
||||
FROM thread_spawn_edges
|
||||
JOIN threads ON threads.id = thread_spawn_edges.child_thread_id
|
||||
WHERE thread_spawn_edges.parent_thread_id = ?
|
||||
AND threads.agent_path = ?
|
||||
ORDER BY threads.id
|
||||
LIMIT 2
|
||||
"#,
|
||||
)
|
||||
.bind(parent_thread_id.to_string())
|
||||
.bind(agent_path)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
one_thread_id_from_rows(rows, agent_path)
|
||||
}
|
||||
|
||||
/// Find a spawned descendant of `root_thread_id` by canonical agent path.
|
||||
pub async fn find_thread_spawn_descendant_by_path(
|
||||
&self,
|
||||
root_thread_id: ThreadId,
|
||||
agent_path: &str,
|
||||
) -> anyhow::Result<Option<ThreadId>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
WITH RECURSIVE subtree(child_thread_id) AS (
|
||||
SELECT child_thread_id
|
||||
FROM thread_spawn_edges
|
||||
WHERE parent_thread_id = ?
|
||||
UNION ALL
|
||||
SELECT edge.child_thread_id
|
||||
FROM thread_spawn_edges AS edge
|
||||
JOIN subtree ON edge.parent_thread_id = subtree.child_thread_id
|
||||
)
|
||||
SELECT threads.id
|
||||
FROM subtree
|
||||
JOIN threads ON threads.id = subtree.child_thread_id
|
||||
WHERE threads.agent_path = ?
|
||||
ORDER BY threads.id
|
||||
LIMIT 2
|
||||
"#,
|
||||
)
|
||||
.bind(root_thread_id.to_string())
|
||||
.bind(agent_path)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
one_thread_id_from_rows(rows, agent_path)
|
||||
}
|
||||
|
||||
async fn list_thread_spawn_children_matching(
|
||||
&self,
|
||||
parent_thread_id: ThreadId,
|
||||
@@ -293,6 +350,7 @@ SELECT
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
@@ -393,6 +451,7 @@ INSERT INTO threads (
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
@@ -409,7 +468,7 @@ INSERT INTO threads (
|
||||
git_branch,
|
||||
git_origin_url,
|
||||
memory_mode
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO NOTHING
|
||||
"#,
|
||||
)
|
||||
@@ -420,6 +479,7 @@ ON CONFLICT(id) DO NOTHING
|
||||
.bind(metadata.source.as_str())
|
||||
.bind(metadata.agent_nickname.as_deref())
|
||||
.bind(metadata.agent_role.as_deref())
|
||||
.bind(metadata.agent_path.as_deref())
|
||||
.bind(metadata.model_provider.as_str())
|
||||
.bind(metadata.model.as_deref())
|
||||
.bind(
|
||||
@@ -518,6 +578,7 @@ INSERT INTO threads (
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
@@ -534,7 +595,7 @@ INSERT INTO threads (
|
||||
git_branch,
|
||||
git_origin_url,
|
||||
memory_mode
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
rollout_path = excluded.rollout_path,
|
||||
created_at = excluded.created_at,
|
||||
@@ -542,6 +603,7 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
source = excluded.source,
|
||||
agent_nickname = excluded.agent_nickname,
|
||||
agent_role = excluded.agent_role,
|
||||
agent_path = excluded.agent_path,
|
||||
model_provider = excluded.model_provider,
|
||||
model = excluded.model,
|
||||
reasoning_effort = excluded.reasoning_effort,
|
||||
@@ -566,6 +628,7 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
.bind(metadata.source.as_str())
|
||||
.bind(metadata.agent_nickname.as_deref())
|
||||
.bind(metadata.agent_role.as_deref())
|
||||
.bind(metadata.agent_path.as_deref())
|
||||
.bind(metadata.model_provider.as_str())
|
||||
.bind(metadata.model.as_deref())
|
||||
.bind(
|
||||
@@ -753,6 +816,26 @@ ON CONFLICT(thread_id, position) DO NOTHING
|
||||
}
|
||||
}
|
||||
|
||||
fn one_thread_id_from_rows(
|
||||
rows: Vec<sqlx::sqlite::SqliteRow>,
|
||||
agent_path: &str,
|
||||
) -> anyhow::Result<Option<ThreadId>> {
|
||||
let mut ids = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let id: String = row.try_get("id")?;
|
||||
ThreadId::try_from(id).map_err(anyhow::Error::from)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
match ids.len() {
|
||||
0 => Ok(None),
|
||||
1 => Ok(ids.pop()),
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"multiple agents found for canonical path `{agent_path}`"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn extract_dynamic_tools(items: &[RolloutItem]) -> Option<Option<Vec<DynamicToolSpec>>> {
|
||||
items.iter().find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.dynamic_tools.clone()),
|
||||
@@ -942,6 +1025,7 @@ mod tests {
|
||||
originator: String::new(),
|
||||
cli_version: String::new(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
@@ -996,6 +1080,7 @@ mod tests {
|
||||
originator: String::new(),
|
||||
cli_version: String::new(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/target/
|
||||
/target-rs/
|
||||
@@ -0,0 +1,12 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "v8-poc",
|
||||
crate_name = "codex_v8_poc",
|
||||
deps_extra = ["@crates//:v8"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8-poc-rusty-v8",
|
||||
actual = ":v8-poc",
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "codex-v8-poc"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_v8_poc"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
v8 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Bazel-wired proof-of-concept crate reserved for future V8 experiments.
|
||||
|
||||
/// Returns the Bazel label for this proof-of-concept crate.
|
||||
#[must_use]
|
||||
pub fn bazel_target() -> &'static str {
|
||||
"//codex-rs/v8-poc:v8-poc"
|
||||
}
|
||||
|
||||
/// Returns the embedded V8 version.
|
||||
#[must_use]
|
||||
pub fn embedded_v8_version() -> &'static str {
|
||||
v8::V8::get_version()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::Once;
|
||||
|
||||
use super::bazel_target;
|
||||
|
||||
fn initialize_v8() {
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
v8::V8::initialize_platform(v8::new_default_platform(0, false).make_shared());
|
||||
v8::V8::initialize();
|
||||
});
|
||||
}
|
||||
|
||||
fn evaluate_expression(expression: &str) -> String {
|
||||
initialize_v8();
|
||||
|
||||
let isolate = &mut v8::Isolate::new(Default::default());
|
||||
v8::scope!(let scope, isolate);
|
||||
|
||||
let context = v8::Context::new(scope, Default::default());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let source = v8::String::new(scope, expression).expect("expression should be valid UTF-8");
|
||||
let script = v8::Script::compile(scope, source, None).expect("expression should compile");
|
||||
let result = script.run(scope).expect("expression should evaluate");
|
||||
|
||||
result.to_rust_string_lossy(scope)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exposes_expected_bazel_target() {
|
||||
assert_eq!(bazel_target(), "//codex-rs/v8-poc:v8-poc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exposes_embedded_v8_version() {
|
||||
assert!(!super::embedded_v8_version().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluates_integer_addition() {
|
||||
assert_eq!(evaluate_expression("1 + 2"), "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluates_string_concatenation() {
|
||||
assert_eq!(evaluate_expression("'hello ' + 'world'"), "hello world");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
exports_files([
|
||||
"aws-lc-sys_memcmp_check.patch",
|
||||
"rusty_v8_prebuilt_out_dir.patch",
|
||||
"v8_bazel_rules.patch",
|
||||
"v8_module_deps.patch",
|
||||
"v8_source_portability.patch",
|
||||
|
||||
@@ -10,7 +10,7 @@ diff --git a/builder/cc_builder.rs b/builder/cc_builder.rs
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(PartialEq, Eq)]
|
||||
@@ -661,6 +661,16 @@
|
||||
@@ -681,6 +681,16 @@
|
||||
}
|
||||
let mut memcmp_compile_args = Vec::from(memcmp_compiler.args());
|
||||
|
||||
@@ -27,7 +27,7 @@ diff --git a/builder/cc_builder.rs b/builder/cc_builder.rs
|
||||
// This check invokes the compiled executable and hence needs to link
|
||||
// it. CMake handles this via LDFLAGS but `cc` doesn't. In setups with
|
||||
// custom linker setups this could lead to a mismatch between the
|
||||
@@ -672,6 +682,15 @@
|
||||
@@ -692,6 +702,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ diff --git a/builder/cc_builder.rs b/builder/cc_builder.rs
|
||||
memcmp_compile_args.push(
|
||||
self.manifest_dir
|
||||
.join("aws-lc")
|
||||
@@ -725,6 +744,40 @@
|
||||
}
|
||||
@@ -742,6 +761,40 @@
|
||||
);
|
||||
let _ = fs::remove_file(exec_path);
|
||||
}
|
||||
+
|
||||
@@ -84,3 +84,31 @@ diff --git a/builder/cc_builder.rs b/builder/cc_builder.rs
|
||||
fn run_compiler_checks(&self, cc_build: &mut cc::Build) {
|
||||
if self.compiler_check("stdalign_check", Vec::<&'static str>::new()) {
|
||||
cc_build.define("AWS_LC_STDALIGN_AVAILABLE", Some("1"));
|
||||
diff --git a/builder/main.rs b/builder/main.rs
|
||||
--- a/builder/main.rs
|
||||
+++ b/builder/main.rs
|
||||
@@ -944,10 +944,12 @@
|
||||
// iterate over all the include paths and copy them into the final output
|
||||
for path in include_paths {
|
||||
for child in std::fs::read_dir(path).into_iter().flatten().flatten() {
|
||||
- if child.file_type().map_or(false, |t| t.is_file()) {
|
||||
+ let child_path = child.path();
|
||||
+
|
||||
+ if child_path.is_file() {
|
||||
std::fs::copy(
|
||||
- child.path(),
|
||||
- include_dir.join(child.path().file_name().unwrap()),
|
||||
+ &child_path,
|
||||
+ include_dir.join(child_path.file_name().unwrap()),
|
||||
)
|
||||
.expect("Failed to copy include file during build setup");
|
||||
continue;
|
||||
@@ -957,7 +959,7 @@
|
||||
let options = fs_extra::dir::CopyOptions::new()
|
||||
.skip_exist(true)
|
||||
.copy_inside(true);
|
||||
- fs_extra::dir::copy(child.path(), &include_dir, &options)
|
||||
+ fs_extra::dir::copy(child_path, &include_dir, &options)
|
||||
.expect("Failed to copy include directory during build setup");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
--- a/build.rs
|
||||
+++ b/build.rs
|
||||
@@ -577,7 +577,23 @@
|
||||
path
|
||||
}
|
||||
|
||||
+fn out_dir_abs() -> PathBuf {
|
||||
+ let cwd = env::current_dir().unwrap();
|
||||
+
|
||||
+ // target/debug/build/rusty_v8-d9e5a424d4f96994/out/
|
||||
+ let out_dir = env::var_os("OUT_DIR").expect(
|
||||
+ "The 'OUT_DIR' environment is not set (it should be something like \
|
||||
+ 'target/debug/rusty_v8-{hash}').",
|
||||
+ );
|
||||
+
|
||||
+ cwd.join(out_dir)
|
||||
+}
|
||||
+
|
||||
fn static_lib_dir() -> PathBuf {
|
||||
+ if env::var_os("RUSTY_V8_ARCHIVE").is_some() {
|
||||
+ return out_dir_abs().join("gn_out").join("obj");
|
||||
+ }
|
||||
+
|
||||
build_dir().join("gn_out").join("obj")
|
||||
}
|
||||
|
||||
@@ -794,22 +810,23 @@
|
||||
}
|
||||
|
||||
fn print_link_flags() {
|
||||
+ let target = env::var("TARGET").unwrap();
|
||||
println!("cargo:rustc-link-lib=static=rusty_v8");
|
||||
let should_dyn_link_libcxx = env::var("CARGO_FEATURE_USE_CUSTOM_LIBCXX")
|
||||
.is_err()
|
||||
+ || (target.contains("apple") && env::var("RUSTY_V8_ARCHIVE").is_ok())
|
||||
|| env::var("GN_ARGS").is_ok_and(|gn_args| {
|
||||
gn_args
|
||||
.split_whitespace()
|
||||
.any(|ba| ba == "use_custom_libcxx=false")
|
||||
});
|
||||
|
||||
if should_dyn_link_libcxx {
|
||||
// Based on https://github.com/alexcrichton/cc-rs/blob/fba7feded71ee4f63cfe885673ead6d7b4f2f454/src/lib.rs#L2462
|
||||
if let Ok(stdlib) = env::var("CXXSTDLIB") {
|
||||
if !stdlib.is_empty() {
|
||||
println!("cargo:rustc-link-lib=dylib={stdlib}");
|
||||
}
|
||||
} else {
|
||||
- let target = env::var("TARGET").unwrap();
|
||||
if target.contains("msvc") {
|
||||
// nothing to link to
|
||||
} else if target.contains("apple")
|
||||
@@ -121,7 +121,7 @@ index 85f31b7..7314584 100644
|
||||
],
|
||||
outs = [
|
||||
"include/inspector/Debugger.h",
|
||||
@@ -4426,15 +4426,19 @@ genrule(
|
||||
@@ -4426,15 +4426,18 @@ genrule(
|
||||
"src/inspector/protocol/Schema.cpp",
|
||||
"src/inspector/protocol/Schema.h",
|
||||
],
|
||||
@@ -134,7 +134,7 @@ index 85f31b7..7314584 100644
|
||||
--config $(location :src/inspector/inspector_protocol_config.json) \
|
||||
--config_value protocol.path=$(location :include/js_protocol.pdl) \
|
||||
--output_base $(@D)/src/inspector",
|
||||
local = 1,
|
||||
- local = 1,
|
||||
message = "Generating inspector files",
|
||||
tools = [
|
||||
- ":code_generator",
|
||||
|
||||
@@ -181,6 +181,7 @@ class AppServerClient:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
cwd=self.config.cwd,
|
||||
env=env,
|
||||
bufsize=1,
|
||||
|
||||
Vendored
+167
-77
@@ -4,6 +4,158 @@ load("@rules_cc//cc:defs.bzl", "cc_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
config_setting(
|
||||
name = "platform_aarch64_unknown_linux_musl",
|
||||
constraint_values = [
|
||||
"@platforms//cpu:aarch64",
|
||||
"@platforms//os:linux",
|
||||
"@llvm//constraints/libc:musl",
|
||||
],
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "platform_x86_64_unknown_linux_musl",
|
||||
constraint_values = [
|
||||
"@platforms//cpu:x86_64",
|
||||
"@platforms//os:linux",
|
||||
"@llvm//constraints/libc:musl",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_x86_64_apple_darwin",
|
||||
actual = "@rusty_v8_146_4_0_x86_64_apple_darwin_archive//file",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_aarch64_apple_darwin",
|
||||
actual = "@rusty_v8_146_4_0_aarch64_apple_darwin_archive//file",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_x86_64_unknown_linux_gnu",
|
||||
actual = "@rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive//file",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_aarch64_unknown_linux_gnu",
|
||||
actual = "@rusty_v8_146_4_0_aarch64_unknown_linux_gnu_archive//file",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_x86_64_unknown_linux_musl",
|
||||
actual = "@rusty_v8_146_4_0_x86_64_unknown_linux_musl_archive//file",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_aarch64_unknown_linux_musl",
|
||||
actual = "@rusty_v8_146_4_0_aarch64_unknown_linux_musl_archive//file",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_x86_64_pc_windows_msvc",
|
||||
actual = "@rusty_v8_146_4_0_x86_64_pc_windows_msvc_archive//file",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_aarch64_pc_windows_msvc",
|
||||
actual = "@rusty_v8_146_4_0_aarch64_pc_windows_msvc_archive//file",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_aarch64_pc_windows_gnullvm",
|
||||
actual = ":v8_146_4_0_aarch64_pc_windows_msvc",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_x86_64_pc_windows_gnullvm",
|
||||
actual = ":v8_146_4_0_x86_64_pc_windows_msvc",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_x86_64_apple_darwin",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_apple_darwin"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_aarch64_apple_darwin",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_apple_darwin"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_aarch64_unknown_linux_gnu",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_unknown_linux_gnu"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_x86_64_unknown_linux_gnu",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_unknown_linux_gnu"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "src_binding_release_x86_64_unknown_linux_musl",
|
||||
actual = "@rusty_v8_146_4_0_x86_64_unknown_linux_musl_binding//file",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "src_binding_release_aarch64_unknown_linux_musl",
|
||||
actual = "@rusty_v8_146_4_0_aarch64_unknown_linux_musl_binding//file",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_x86_64_pc_windows_msvc",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_pc_windows_msvc"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_aarch64_pc_windows_msvc",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_pc_windows_msvc"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "src_binding_release_x86_64_pc_windows_gnullvm",
|
||||
actual = ":src_binding_release_x86_64_pc_windows_msvc",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "src_binding_release_aarch64_pc_windows_gnullvm",
|
||||
actual = ":src_binding_release_aarch64_pc_windows_msvc",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "rusty_v8_archive_for_target",
|
||||
actual = select({
|
||||
"@rules_rs//rs/experimental/platforms/config:aarch64-apple-darwin": ":v8_146_4_0_aarch64_apple_darwin_bazel",
|
||||
"@rules_rs//rs/experimental/platforms/config:aarch64-pc-windows-gnullvm": ":v8_146_4_0_aarch64_pc_windows_gnullvm",
|
||||
"@rules_rs//rs/experimental/platforms/config:aarch64-pc-windows-msvc": ":v8_146_4_0_aarch64_pc_windows_msvc",
|
||||
"@rules_rs//rs/experimental/platforms/config:aarch64-unknown-linux-gnu": ":v8_146_4_0_aarch64_unknown_linux_gnu_bazel",
|
||||
":platform_aarch64_unknown_linux_musl": ":v8_146_4_0_aarch64_unknown_linux_musl_release_base",
|
||||
"@rules_rs//rs/experimental/platforms/config:x86_64-apple-darwin": ":v8_146_4_0_x86_64_apple_darwin_bazel",
|
||||
"@rules_rs//rs/experimental/platforms/config:x86_64-pc-windows-gnullvm": ":v8_146_4_0_x86_64_pc_windows_gnullvm",
|
||||
"@rules_rs//rs/experimental/platforms/config:x86_64-pc-windows-msvc": ":v8_146_4_0_x86_64_pc_windows_msvc",
|
||||
"@rules_rs//rs/experimental/platforms/config:x86_64-unknown-linux-gnu": ":v8_146_4_0_x86_64_unknown_linux_gnu_bazel",
|
||||
":platform_x86_64_unknown_linux_musl": ":v8_146_4_0_x86_64_unknown_linux_musl_release",
|
||||
"//conditions:default": ":v8_146_4_0_x86_64_unknown_linux_gnu_bazel",
|
||||
}),
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "rusty_v8_binding_for_target",
|
||||
actual = select({
|
||||
"@rules_rs//rs/experimental/platforms/config:aarch64-apple-darwin": ":src_binding_release_aarch64_apple_darwin",
|
||||
"@rules_rs//rs/experimental/platforms/config:aarch64-pc-windows-gnullvm": ":src_binding_release_aarch64_pc_windows_gnullvm",
|
||||
"@rules_rs//rs/experimental/platforms/config:aarch64-pc-windows-msvc": ":src_binding_release_aarch64_pc_windows_msvc",
|
||||
"@rules_rs//rs/experimental/platforms/config:aarch64-unknown-linux-gnu": ":src_binding_release_aarch64_unknown_linux_gnu",
|
||||
":platform_aarch64_unknown_linux_musl": ":src_binding_release_aarch64_unknown_linux_musl",
|
||||
"@rules_rs//rs/experimental/platforms/config:x86_64-apple-darwin": ":src_binding_release_x86_64_apple_darwin",
|
||||
"@rules_rs//rs/experimental/platforms/config:x86_64-pc-windows-gnullvm": ":src_binding_release_x86_64_pc_windows_gnullvm",
|
||||
"@rules_rs//rs/experimental/platforms/config:x86_64-pc-windows-msvc": ":src_binding_release_x86_64_pc_windows_msvc",
|
||||
"@rules_rs//rs/experimental/platforms/config:x86_64-unknown-linux-gnu": ":src_binding_release_x86_64_unknown_linux_gnu",
|
||||
":platform_x86_64_unknown_linux_musl": ":src_binding_release_x86_64_unknown_linux_musl",
|
||||
"//conditions:default": ":src_binding_release_x86_64_unknown_linux_gnu",
|
||||
}),
|
||||
)
|
||||
|
||||
V8_COPTS = ["-std=c++20"]
|
||||
|
||||
V8_STATIC_LIBRARY_FEATURES = [
|
||||
@@ -45,39 +197,39 @@ cc_library(
|
||||
)
|
||||
|
||||
cc_static_library(
|
||||
name = "v8_146_4_0_x86_64_apple_darwin",
|
||||
name = "v8_146_4_0_aarch64_apple_darwin_bazel",
|
||||
deps = [":v8_146_4_0_binding"],
|
||||
features = V8_STATIC_LIBRARY_FEATURES,
|
||||
)
|
||||
|
||||
cc_static_library(
|
||||
name = "v8_146_4_0_aarch64_apple_darwin",
|
||||
name = "v8_146_4_0_aarch64_unknown_linux_gnu_bazel",
|
||||
deps = [":v8_146_4_0_binding"],
|
||||
features = V8_STATIC_LIBRARY_FEATURES,
|
||||
)
|
||||
|
||||
cc_static_library(
|
||||
name = "v8_146_4_0_aarch64_unknown_linux_gnu",
|
||||
name = "v8_146_4_0_x86_64_apple_darwin_bazel",
|
||||
deps = [":v8_146_4_0_binding"],
|
||||
features = V8_STATIC_LIBRARY_FEATURES,
|
||||
)
|
||||
|
||||
cc_static_library(
|
||||
name = "v8_146_4_0_x86_64_unknown_linux_gnu",
|
||||
name = "v8_146_4_0_x86_64_unknown_linux_gnu_bazel",
|
||||
deps = [":v8_146_4_0_binding"],
|
||||
features = V8_STATIC_LIBRARY_FEATURES,
|
||||
)
|
||||
|
||||
cc_static_library(
|
||||
name = "v8_146_4_0_aarch64_unknown_linux_musl_base",
|
||||
name = "v8_146_4_0_aarch64_unknown_linux_musl_release_base",
|
||||
deps = [":v8_146_4_0_binding"],
|
||||
features = V8_STATIC_LIBRARY_FEATURES,
|
||||
)
|
||||
|
||||
genrule(
|
||||
name = "v8_146_4_0_aarch64_unknown_linux_musl",
|
||||
name = "v8_146_4_0_aarch64_unknown_linux_musl_release",
|
||||
srcs = [
|
||||
":v8_146_4_0_aarch64_unknown_linux_musl_base",
|
||||
":v8_146_4_0_aarch64_unknown_linux_musl_release_base",
|
||||
"@llvm//runtimes/compiler-rt:clang_rt.builtins.static",
|
||||
],
|
||||
tools = [
|
||||
@@ -88,7 +240,7 @@ genrule(
|
||||
cmd = """
|
||||
cat > "$(@D)/merge.mri" <<'EOF'
|
||||
create $@
|
||||
addlib $(location :v8_146_4_0_aarch64_unknown_linux_musl_base)
|
||||
addlib $(location :v8_146_4_0_aarch64_unknown_linux_musl_release_base)
|
||||
addlib $(location @llvm//runtimes/compiler-rt:clang_rt.builtins.static)
|
||||
save
|
||||
end
|
||||
@@ -99,83 +251,21 @@ EOF
|
||||
)
|
||||
|
||||
cc_static_library(
|
||||
name = "v8_146_4_0_x86_64_unknown_linux_musl",
|
||||
name = "v8_146_4_0_x86_64_unknown_linux_musl_release",
|
||||
deps = [":v8_146_4_0_binding"],
|
||||
features = V8_STATIC_LIBRARY_FEATURES,
|
||||
)
|
||||
|
||||
cc_static_library(
|
||||
name = "v8_146_4_0_aarch64_pc_windows_msvc",
|
||||
deps = [":v8_146_4_0_binding"],
|
||||
features = V8_STATIC_LIBRARY_FEATURES,
|
||||
)
|
||||
|
||||
cc_static_library(
|
||||
name = "v8_146_4_0_x86_64_pc_windows_msvc",
|
||||
deps = [":v8_146_4_0_binding"],
|
||||
features = V8_STATIC_LIBRARY_FEATURES,
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_aarch64_pc_windows_gnullvm",
|
||||
actual = ":v8_146_4_0_aarch64_pc_windows_msvc",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "v8_146_4_0_x86_64_pc_windows_gnullvm",
|
||||
actual = ":v8_146_4_0_x86_64_pc_windows_msvc",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_x86_64_apple_darwin",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_apple_darwin"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_aarch64_apple_darwin",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_apple_darwin"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_aarch64_unknown_linux_gnu",
|
||||
name = "src_binding_release_aarch64_unknown_linux_musl_release",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_unknown_linux_gnu"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_x86_64_unknown_linux_gnu",
|
||||
name = "src_binding_release_x86_64_unknown_linux_musl_release",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_unknown_linux_gnu"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_aarch64_unknown_linux_musl",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_unknown_linux_gnu"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_x86_64_unknown_linux_musl",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_unknown_linux_gnu"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_x86_64_pc_windows_msvc",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_pc_windows_msvc"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "src_binding_release_aarch64_pc_windows_msvc",
|
||||
srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_pc_windows_msvc"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "src_binding_release_x86_64_pc_windows_gnullvm",
|
||||
actual = ":src_binding_release_x86_64_pc_windows_msvc",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "src_binding_release_aarch64_pc_windows_gnullvm",
|
||||
actual = ":src_binding_release_aarch64_pc_windows_msvc",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "rusty_v8_release_pair_x86_64_apple_darwin",
|
||||
srcs = [
|
||||
@@ -211,16 +301,16 @@ filegroup(
|
||||
filegroup(
|
||||
name = "rusty_v8_release_pair_x86_64_unknown_linux_musl",
|
||||
srcs = [
|
||||
":v8_146_4_0_x86_64_unknown_linux_musl",
|
||||
":src_binding_release_x86_64_unknown_linux_musl",
|
||||
":v8_146_4_0_x86_64_unknown_linux_musl_release",
|
||||
":src_binding_release_x86_64_unknown_linux_musl_release",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "rusty_v8_release_pair_aarch64_unknown_linux_musl",
|
||||
srcs = [
|
||||
":v8_146_4_0_aarch64_unknown_linux_musl",
|
||||
":src_binding_release_aarch64_unknown_linux_musl",
|
||||
":v8_146_4_0_aarch64_unknown_linux_musl_release",
|
||||
":src_binding_release_aarch64_unknown_linux_musl_release",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
Vendored
+35
-33
@@ -1,45 +1,47 @@
|
||||
# `rusty_v8` Release Artifacts
|
||||
# `rusty_v8` Consumer Artifacts
|
||||
|
||||
This directory contains the Bazel packaging used to build and stage
|
||||
target-specific `rusty_v8` release artifacts for Bazel-managed consumers.
|
||||
This directory wires the `v8` crate to exact-version Bazel inputs.
|
||||
Bazel consumer builds use:
|
||||
|
||||
- upstream `denoland/rusty_v8` release archives on Windows
|
||||
- source-built V8 archives on Darwin, GNU Linux, and musl Linux
|
||||
- `openai/codex` release assets for published musl release pairs
|
||||
|
||||
Cargo builds still use prebuilt `rusty_v8` archives by default. Only Bazel
|
||||
overrides `RUSTY_V8_ARCHIVE`/`RUSTY_V8_SRC_BINDING_PATH` in `MODULE.bazel` to
|
||||
select source-built local archives for its consumer builds.
|
||||
|
||||
Current pinned versions:
|
||||
|
||||
- Rust crate: `v8 = =146.4.0`
|
||||
- Embedded upstream V8 source: `14.6.202.9`
|
||||
- Embedded upstream V8 source for musl release builds: `14.6.202.9`
|
||||
|
||||
The generated release pairs include:
|
||||
The consumer-facing selectors are:
|
||||
|
||||
- `//third_party/v8:rusty_v8_archive_for_target`
|
||||
- `//third_party/v8:rusty_v8_binding_for_target`
|
||||
|
||||
Musl release assets are expected at the tag:
|
||||
|
||||
- `rusty-v8-v<crate_version>`
|
||||
|
||||
with these raw asset names:
|
||||
|
||||
- `librusty_v8_release_<target>.a.gz`
|
||||
- `src_binding_release_<target>.rs`
|
||||
|
||||
The dedicated publishing workflow is `.github/workflows/rusty-v8-release.yml`.
|
||||
It builds musl release pairs from source and keeps the release artifacts as the
|
||||
statically linked form:
|
||||
|
||||
- `//third_party/v8:rusty_v8_release_pair_x86_64_apple_darwin`
|
||||
- `//third_party/v8:rusty_v8_release_pair_aarch64_apple_darwin`
|
||||
- `//third_party/v8:rusty_v8_release_pair_x86_64_unknown_linux_gnu`
|
||||
- `//third_party/v8:rusty_v8_release_pair_aarch64_unknown_linux_gnu`
|
||||
- `//third_party/v8:rusty_v8_release_pair_x86_64_unknown_linux_musl`
|
||||
- `//third_party/v8:rusty_v8_release_pair_aarch64_unknown_linux_musl`
|
||||
- `//third_party/v8:rusty_v8_release_pair_x86_64_pc_windows_msvc`
|
||||
- `//third_party/v8:rusty_v8_release_pair_aarch64_pc_windows_msvc`
|
||||
|
||||
Each release pair contains:
|
||||
|
||||
- a static library built from source
|
||||
- a Rust binding file copied from the exact same `v8` crate version for that
|
||||
target
|
||||
Cargo musl builds use `RUSTY_V8_ARCHIVE` plus a downloaded
|
||||
`RUSTY_V8_SRC_BINDING_PATH` to point at those `openai/codex` release assets
|
||||
directly. We do not use `RUSTY_V8_MIRROR` for musl because the upstream `v8`
|
||||
crate hardcodes a `v<crate_version>` tag layout, while our musl artifacts are
|
||||
published under `rusty-v8-v<crate_version>`.
|
||||
|
||||
Do not mix artifacts across crate versions. The archive and binding must match
|
||||
the exact pinned `v8` crate version used by this repo.
|
||||
|
||||
The dedicated publishing workflow is:
|
||||
|
||||
- `.github/workflows/rusty-v8-release.yml`
|
||||
|
||||
That workflow currently stages musl artifacts:
|
||||
|
||||
- `librusty_v8_release_x86_64-unknown-linux-musl.a.gz`
|
||||
- `librusty_v8_release_aarch64-unknown-linux-musl.a.gz`
|
||||
- `src_binding_release_x86_64-unknown-linux-musl.rs`
|
||||
- `src_binding_release_aarch64-unknown-linux-musl.rs`
|
||||
|
||||
During musl staging, the produced static archive is merged with the target's
|
||||
LLVM `libc++` and `libc++abi` static runtime archives. Rust's musl toolchain
|
||||
already provides the matching `libunwind`, so staging does not bundle a second
|
||||
copy.
|
||||
the exact resolved `v8` crate version in `codex-rs/Cargo.lock`.
|
||||
|
||||
Reference in New Issue
Block a user