mirror of
https://github.com/openai/codex.git
synced 2026-04-01 21:14:08 +03:00
Compare commits
13 Commits
model-fall
...
pr16450
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
086ca7c728 | ||
|
|
75365bf718 | ||
|
|
5cca5c0093 | ||
|
|
d3b99ef110 | ||
|
|
f839f3ff2e | ||
|
|
c846a57d03 | ||
|
|
5bbfee69b6 | ||
|
|
609ac0c7ab | ||
|
|
df5f79da36 | ||
|
|
0c776c433b | ||
|
|
3152d1a557 | ||
|
|
23d638a573 | ||
|
|
d0474f2bc1 |
6
.github/workflows/README.md
vendored
6
.github/workflows/README.md
vendored
@@ -5,15 +5,15 @@ The workflows in this directory are split so that pull requests get fast, review
|
||||
## Pull Requests
|
||||
|
||||
- `bazel.yml` is the main pre-merge verification path for Rust code.
|
||||
It runs Bazel `test` and Bazel `clippy` on the supported Bazel targets.
|
||||
It runs Bazel `test` and Bazel `clippy` on the supported Bazel targets,
|
||||
including the generated Rust test binaries needed to lint inline `#[cfg(test)]`
|
||||
code.
|
||||
- `rust-ci.yml` keeps the Cargo-native PR checks intentionally small:
|
||||
- `cargo fmt --check`
|
||||
- `cargo shear`
|
||||
- `argument-comment-lint` on Linux, macOS, and Windows
|
||||
- `tools/argument-comment-lint` package tests when the lint or its workflow wiring changes
|
||||
|
||||
The PR workflow still keeps the Linux lint lane on the default-targets-only invocation for now, but the released linter runs on Linux, macOS, and Windows before merge.
|
||||
|
||||
## Post-Merge On `main`
|
||||
|
||||
- `bazel.yml` also runs on pushes to `main`.
|
||||
|
||||
13
.github/workflows/bazel.yml
vendored
13
.github/workflows/bazel.yml
vendored
@@ -126,13 +126,17 @@ jobs:
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: bazel build --config=clippy //codex-rs/...
|
||||
- name: bazel build --config=clippy lint targets
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
# Keep the initial Bazel clippy scope on codex-rs and out of the
|
||||
# V8 proof-of-concept target for now.
|
||||
bazel_target_lines="$(./scripts/list-bazel-clippy-targets.sh)"
|
||||
bazel_targets=()
|
||||
while IFS= read -r target; do
|
||||
bazel_targets+=("${target}")
|
||||
done <<< "${bazel_target_lines}"
|
||||
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
-- \
|
||||
build \
|
||||
@@ -140,8 +144,7 @@ jobs:
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
--build_metadata=TAG_job=clippy \
|
||||
-- \
|
||||
//codex-rs/... \
|
||||
-//codex-rs/v8-poc:all
|
||||
"${bazel_targets[@]}"
|
||||
|
||||
# Save bazel repository cache explicitly; make non-fatal so cache uploading
|
||||
# never fails the overall job. Only save when key wasn't hit.
|
||||
|
||||
@@ -15,7 +15,8 @@ In the codex-rs folder where the rust code lives:
|
||||
- When you cannot make that API change and still need a small positional-literal callsite in Rust, follow the `argument_comment_lint` convention:
|
||||
- Use an exact `/*param_name*/` comment before opaque literal arguments such as `None`, booleans, and numeric literals when passing them by position.
|
||||
- Do not add these comments for string or char literals unless the comment adds real clarity; those literals are intentionally exempt from the lint.
|
||||
- If you add one of these comments, the parameter name must exactly match the callee signature.
|
||||
- The parameter name in the comment must exactly match the callee signature.
|
||||
- You can run `just argument-comment-lint` to run the lint check locally. This is powered by Bazel, so running it the first time can be slow if Bazel is not warmed up, though incremental invocations should take <15s. Most of the time, it is best to update the PR and let CI take responsibility for checking this (or run it asynchronously in the background after submitting the PR). Note CI checks all three platforms, which the local run does not.
|
||||
- When possible, make `match` statements exhaustive and avoid wildcard arms.
|
||||
- Newly added traits should include doc comments that explain their role and how implementations are expected to use them.
|
||||
- When writing tests, prefer comparing the equality of entire objects over fields one by one.
|
||||
@@ -50,8 +51,6 @@ Run `just fmt` (in `codex-rs` directory) automatically after you have finished m
|
||||
|
||||
Before finalizing a large change to `codex-rs`, run `just fix -p <project>` (in `codex-rs` directory) to fix any linter issues in the code. Prefer scoping with `-p` to avoid slow workspace‑wide Clippy builds; only run `just fix` without `-p` if you changed shared crates. Do not re-run tests after running `fix` or `fmt`.
|
||||
|
||||
Also run `just argument-comment-lint` to ensure the codebase is clean of comment lint errors.
|
||||
|
||||
## The `codex-core` crate
|
||||
|
||||
Over time, the `codex-core` crate (defined in `codex-rs/core/`) has become bloated because it is the largest crate, so it is often easier to add something new to `codex-core` rather than refactor out the library code you need so your new code neither takes a dependency on, nor contributes to the size of, `codex-core`.
|
||||
|
||||
@@ -314,7 +314,6 @@ pub fn build_exec_request(
|
||||
|
||||
pub(crate) async fn execute_exec_request(
|
||||
exec_request: ExecRequest,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
stdout_stream: Option<StdoutStream>,
|
||||
after_spawn: Option<Box<dyn FnOnce() + Send>>,
|
||||
) -> Result<ExecToolCallOutput> {
|
||||
@@ -328,13 +327,12 @@ pub(crate) async fn execute_exec_request(
|
||||
sandbox,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
sandbox_policy: _sandbox_policy_from_env,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
windows_restricted_token_filesystem_overlay,
|
||||
arg0,
|
||||
} = exec_request;
|
||||
let _ = _sandbox_policy_from_env;
|
||||
|
||||
let params = ExecParams {
|
||||
command,
|
||||
@@ -354,7 +352,7 @@ pub(crate) async fn execute_exec_request(
|
||||
let raw_output_result = exec(
|
||||
params,
|
||||
sandbox,
|
||||
sandbox_policy,
|
||||
&sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
windows_restricted_token_filesystem_overlay.as_ref(),
|
||||
network_sandbox_policy,
|
||||
|
||||
@@ -141,14 +141,7 @@ pub async fn execute_env(
|
||||
exec_request: ExecRequest,
|
||||
stdout_stream: Option<StdoutStream>,
|
||||
) -> crate::error::Result<ExecToolCallOutput> {
|
||||
let effective_policy = exec_request.sandbox_policy.clone();
|
||||
execute_exec_request(
|
||||
exec_request,
|
||||
&effective_policy,
|
||||
stdout_stream,
|
||||
/*after_spawn*/ None,
|
||||
)
|
||||
.await
|
||||
execute_exec_request(exec_request, stdout_stream, /*after_spawn*/ None).await
|
||||
}
|
||||
|
||||
pub async fn execute_exec_request_with_after_spawn(
|
||||
@@ -156,6 +149,5 @@ pub async fn execute_exec_request_with_after_spawn(
|
||||
stdout_stream: Option<StdoutStream>,
|
||||
after_spawn: Option<Box<dyn FnOnce() + Send>>,
|
||||
) -> crate::error::Result<ExecToolCallOutput> {
|
||||
let effective_policy = exec_request.sandbox_policy.clone();
|
||||
execute_exec_request(exec_request, &effective_policy, stdout_stream, after_spawn).await
|
||||
execute_exec_request(exec_request, stdout_stream, after_spawn).await
|
||||
}
|
||||
|
||||
@@ -185,14 +185,9 @@ pub(crate) async fn execute_user_shell_command(
|
||||
tx_event: session.get_tx_event(),
|
||||
});
|
||||
|
||||
let exec_result = execute_exec_request(
|
||||
exec_env,
|
||||
&sandbox_policy,
|
||||
stdout_stream,
|
||||
/*after_spawn*/ None,
|
||||
)
|
||||
.or_cancel(&cancellation_token)
|
||||
.await;
|
||||
let exec_result = execute_exec_request(exec_env, stdout_stream, /*after_spawn*/ None)
|
||||
.or_cancel(&cancellation_token)
|
||||
.await;
|
||||
|
||||
match exec_result {
|
||||
Err(CancelErr::Cancelled) => {
|
||||
|
||||
@@ -347,7 +347,7 @@ async fn multi_agent_v2_spawn_requires_task_name() {
|
||||
Arc::new(turn),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}]
|
||||
"message": "inspect this repo"
|
||||
})),
|
||||
);
|
||||
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
|
||||
@@ -360,7 +360,7 @@ async fn multi_agent_v2_spawn_requires_task_name() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_spawn_rejects_legacy_message_field() {
|
||||
async fn multi_agent_v2_spawn_rejects_legacy_items_field() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
@@ -387,12 +387,12 @@ async fn multi_agent_v2_spawn_rejects_legacy_message_field() {
|
||||
})),
|
||||
);
|
||||
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
|
||||
panic!("legacy message field should be rejected");
|
||||
panic!("legacy items field should be rejected");
|
||||
};
|
||||
let FunctionCallError::RespondToModel(message) = err else {
|
||||
panic!("legacy message field should surface as a model-facing error");
|
||||
panic!("legacy items field should surface as a model-facing error");
|
||||
};
|
||||
assert!(message.contains("unknown field `message`"));
|
||||
assert!(message.contains("unknown field `items`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -444,7 +444,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"task_name": "test_process"
|
||||
})),
|
||||
))
|
||||
@@ -496,7 +496,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
||||
"send_message",
|
||||
function_payload(json!({
|
||||
"target": "test_process",
|
||||
"items": [{"type": "text", "text": "continue"}]
|
||||
"message": "continue"
|
||||
})),
|
||||
))
|
||||
.await
|
||||
@@ -539,7 +539,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_fork_context() {
|
||||
Arc::new(turn),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"task_name": "worker",
|
||||
"fork_context": true
|
||||
})),
|
||||
@@ -578,7 +578,7 @@ async fn multi_agent_v2_spawn_rejects_invalid_fork_turns_string() {
|
||||
Arc::new(turn),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"task_name": "worker",
|
||||
"fork_turns": "banana"
|
||||
})),
|
||||
@@ -617,7 +617,7 @@ async fn multi_agent_v2_spawn_rejects_zero_fork_turns() {
|
||||
Arc::new(turn),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"task_name": "worker",
|
||||
"fork_turns": "0"
|
||||
})),
|
||||
@@ -689,7 +689,7 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
|
||||
"send_message",
|
||||
function_payload(json!({
|
||||
"target": "/root",
|
||||
"items": [{"type": "text", "text": "done"}]
|
||||
"message": "done"
|
||||
})),
|
||||
))
|
||||
.await
|
||||
@@ -709,6 +709,86 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_assign_task_rejects_root_target_from_child() {
|
||||
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 child_path = AgentPath::try_from("/root/worker").expect("agent path");
|
||||
let child_thread_id = session
|
||||
.services
|
||||
.agent_control
|
||||
.spawn_agent_with_metadata(
|
||||
(*turn.config).clone(),
|
||||
vec![UserInput::Text {
|
||||
text: "inspect this repo".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
.into(),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: root.thread_id,
|
||||
depth: 1,
|
||||
agent_path: Some(child_path.clone()),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
crate::agent::control::SpawnAgentOptions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("worker spawn should succeed")
|
||||
.thread_id;
|
||||
session.conversation_id = child_thread_id;
|
||||
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: root.thread_id,
|
||||
depth: 1,
|
||||
agent_path: Some(child_path),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
|
||||
let err = AssignTaskHandlerV2
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"assign_task",
|
||||
function_payload(json!({
|
||||
"target": "/root",
|
||||
"message": "run this",
|
||||
"interrupt": true
|
||||
})),
|
||||
))
|
||||
.await
|
||||
.expect_err("assign_task should reject the root target");
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel("Tasks can't be assigned to the root agent".to_string())
|
||||
);
|
||||
let root_ops = manager
|
||||
.captured_ops()
|
||||
.into_iter()
|
||||
.filter_map(|(id, op)| (id == root.thread_id).then_some(op))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(!root_ops.iter().any(|op| matches!(op, Op::Interrupt)));
|
||||
assert!(
|
||||
!root_ops
|
||||
.iter()
|
||||
.any(|op| matches!(op, Op::InterAgentCommunication { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_message() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
@@ -731,7 +811,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
@@ -909,7 +989,7 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
@@ -952,7 +1032,7 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_send_message_rejects_structured_items() {
|
||||
async fn multi_agent_v2_send_message_rejects_legacy_items_field() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
@@ -973,7 +1053,7 @@ async fn multi_agent_v2_send_message_rejects_structured_items() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "boot worker"}],
|
||||
"message": "boot worker",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
@@ -999,14 +1079,12 @@ async fn multi_agent_v2_send_message_rejects_structured_items() {
|
||||
);
|
||||
|
||||
let Err(err) = SendMessageHandlerV2.handle(invocation).await else {
|
||||
panic!("structured items should be rejected in v2");
|
||||
panic!("legacy items field should be rejected in v2");
|
||||
};
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel(
|
||||
"send_message only supports text content in MultiAgentV2 for now".to_string()
|
||||
)
|
||||
);
|
||||
let FunctionCallError::RespondToModel(message) = err else {
|
||||
panic!("legacy items field should surface as a model-facing error");
|
||||
};
|
||||
assert!(message.contains("unknown field `items`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1031,7 +1109,7 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "boot worker"}],
|
||||
"message": "boot worker",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
@@ -1050,7 +1128,7 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
|
||||
"send_message",
|
||||
function_payload(json!({
|
||||
"target": agent_id.to_string(),
|
||||
"items": [{"type": "text", "text": "continue"}],
|
||||
"message": "continue",
|
||||
"interrupt": true
|
||||
})),
|
||||
);
|
||||
@@ -1062,7 +1140,7 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
|
||||
panic!("expected model-facing parse error");
|
||||
};
|
||||
assert!(message.starts_with(
|
||||
"failed to parse function arguments: unknown field `interrupt`, expected `target` or `items`"
|
||||
"failed to parse function arguments: unknown field `interrupt`, expected `target` or `message`"
|
||||
));
|
||||
|
||||
let ops = manager.captured_ops();
|
||||
@@ -1104,7 +1182,7 @@ async fn multi_agent_v2_assign_task_interrupts_busy_child_without_losing_message
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "boot worker"}],
|
||||
"message": "boot worker",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
@@ -1142,7 +1220,7 @@ async fn multi_agent_v2_assign_task_interrupts_busy_child_without_losing_message
|
||||
"assign_task",
|
||||
function_payload(json!({
|
||||
"target": agent_id.to_string(),
|
||||
"items": [{"type": "text", "text": "continue"}],
|
||||
"message": "continue",
|
||||
"interrupt": true
|
||||
})),
|
||||
))
|
||||
@@ -1233,7 +1311,7 @@ async fn multi_agent_v2_assign_task_completion_notifies_parent_on_every_turn() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "boot worker"}],
|
||||
"message": "boot worker",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
@@ -1271,7 +1349,7 @@ async fn multi_agent_v2_assign_task_completion_notifies_parent_on_every_turn() {
|
||||
"assign_task",
|
||||
function_payload(json!({
|
||||
"target": agent_id.to_string(),
|
||||
"items": [{"type": "text", "text": "continue"}],
|
||||
"message": "continue",
|
||||
})),
|
||||
))
|
||||
.await
|
||||
@@ -1340,6 +1418,59 @@ async fn multi_agent_v2_assign_task_completion_notifies_parent_on_every_turn() {
|
||||
assert_eq!(notifications.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_assign_task_rejects_legacy_items_field() {
|
||||
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.as_ref().clone();
|
||||
let _ = config.features.enable(Feature::MultiAgentV2);
|
||||
turn.config = Arc::new(config);
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
SpawnAgentHandlerV2
|
||||
.handle(invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"message": "boot worker",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
.await
|
||||
.expect("spawn worker");
|
||||
let agent_id = session
|
||||
.services
|
||||
.agent_control
|
||||
.resolve_agent_reference(session.conversation_id, &turn.session_source, "worker")
|
||||
.await
|
||||
.expect("worker should resolve");
|
||||
let invocation = invocation(
|
||||
session,
|
||||
turn,
|
||||
"assign_task",
|
||||
function_payload(json!({
|
||||
"target": agent_id.to_string(),
|
||||
"items": [{"type": "text", "text": "continue"}],
|
||||
})),
|
||||
);
|
||||
|
||||
let Err(err) = AssignTaskHandlerV2.handle(invocation).await else {
|
||||
panic!("legacy items field should be rejected in v2");
|
||||
};
|
||||
let FunctionCallError::RespondToModel(message) = err else {
|
||||
panic!("legacy items field should surface as a model-facing error");
|
||||
};
|
||||
assert!(message.contains("unknown field `items`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
@@ -1362,7 +1493,7 @@ async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "boot worker"}],
|
||||
"message": "boot worker",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
@@ -1438,7 +1569,7 @@ async fn multi_agent_v2_spawn_includes_agent_id_key_when_named() {
|
||||
Arc::new(turn),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"task_name": "test_process"
|
||||
})),
|
||||
))
|
||||
@@ -1476,7 +1607,7 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() {
|
||||
Arc::new(turn),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"task_name": "BadName"
|
||||
})),
|
||||
);
|
||||
@@ -2103,7 +2234,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "boot worker"}],
|
||||
"message": "boot worker",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
@@ -2349,7 +2480,7 @@ async fn multi_agent_v2_wait_agent_returns_summary_for_mailbox_activity() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"task_name": "test_process"
|
||||
})),
|
||||
))
|
||||
@@ -2440,7 +2571,7 @@ async fn multi_agent_v2_wait_agent_waits_for_new_mail_after_start() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "boot worker"}],
|
||||
"message": "boot worker",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
@@ -2540,7 +2671,7 @@ async fn multi_agent_v2_wait_agent_wakes_on_any_mailbox_notification() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": format!("boot {task_name}")}],
|
||||
"message": format!("boot {task_name}"),
|
||||
"task_name": task_name
|
||||
})),
|
||||
))
|
||||
@@ -2627,7 +2758,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "boot worker"}],
|
||||
"message": "boot worker",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
@@ -2713,7 +2844,7 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() {
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"items": [{"type": "text", "text": "inspect this repo"}],
|
||||
"message": "inspect this repo",
|
||||
"task_name": "worker"
|
||||
})),
|
||||
))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::message_tool::AssignTaskArgs;
|
||||
use super::message_tool::MessageDeliveryMode;
|
||||
use super::message_tool::MessageToolResult;
|
||||
use super::message_tool::handle_message_tool;
|
||||
use super::message_tool::handle_message_string_tool;
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
@@ -21,11 +21,11 @@ impl ToolHandler for Handler {
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
let arguments = function_arguments(invocation.payload.clone())?;
|
||||
let args: AssignTaskArgs = parse_arguments(&arguments)?;
|
||||
handle_message_tool(
|
||||
handle_message_string_tool(
|
||||
invocation,
|
||||
MessageDeliveryMode::TriggerTurn,
|
||||
args.target,
|
||||
args.items,
|
||||
args.message,
|
||||
args.interrupt,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -106,6 +106,7 @@ impl ToolHandler for Handler {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct CloseAgentArgs {
|
||||
target: String,
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ impl ToolHandler for Handler {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ListAgentsArgs {
|
||||
path_prefix: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
//! Shared argument parsing and dispatch for the v2 text-only agent messaging tools.
|
||||
//!
|
||||
//! `send_message` and `assign_task` intentionally expose the same input shape and differ only in
|
||||
//! whether the resulting `InterAgentCommunication` should wake the target immediately.
|
||||
//! `send_message` and `assign_task` share the same submission path and differ only in whether the
|
||||
//! resulting `InterAgentCommunication` should wake the target immediately.
|
||||
|
||||
use super::*;
|
||||
use crate::agent::control::render_input_preview;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum MessageDeliveryMode {
|
||||
QueueOnly,
|
||||
TriggerTurn,
|
||||
}
|
||||
|
||||
impl MessageDeliveryMode {
|
||||
/// Returns the model-visible error message for non-text inputs.
|
||||
fn unsupported_items_error(self) -> &'static str {
|
||||
match self {
|
||||
Self::QueueOnly => "send_message only supports text content in MultiAgentV2 for now",
|
||||
Self::TriggerTurn => "assign_task only supports text content in MultiAgentV2 for now",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the produced communication should start a turn immediately.
|
||||
fn apply(self, communication: InterAgentCommunication) -> InterAgentCommunication {
|
||||
match self {
|
||||
@@ -42,7 +33,7 @@ impl MessageDeliveryMode {
|
||||
/// Input for the MultiAgentV2 `send_message` tool.
|
||||
pub(crate) struct SendMessageArgs {
|
||||
pub(crate) target: String,
|
||||
pub(crate) items: Vec<UserInput>,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -50,7 +41,7 @@ pub(crate) struct SendMessageArgs {
|
||||
/// Input for the MultiAgentV2 `assign_task` tool.
|
||||
pub(crate) struct AssignTaskArgs {
|
||||
pub(crate) target: String,
|
||||
pub(crate) items: Vec<UserInput>,
|
||||
pub(crate) message: String,
|
||||
#[serde(default)]
|
||||
pub(crate) interrupt: bool,
|
||||
}
|
||||
@@ -79,33 +70,38 @@ impl ToolOutput for MessageToolResult {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that the tool input is non-empty text-only content and returns its preview string.
|
||||
fn text_content(
|
||||
items: &[UserInput],
|
||||
mode: MessageDeliveryMode,
|
||||
) -> Result<String, FunctionCallError> {
|
||||
if items.is_empty() {
|
||||
fn message_content(message: String) -> Result<String, FunctionCallError> {
|
||||
if message.trim().is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"Items can't be empty".to_string(),
|
||||
"Empty message can't be sent to an agent".to_string(),
|
||||
));
|
||||
}
|
||||
if items
|
||||
.iter()
|
||||
.all(|item| matches!(item, UserInput::Text { .. }))
|
||||
{
|
||||
return Ok(render_input_preview(&(items.to_vec().into())));
|
||||
}
|
||||
Err(FunctionCallError::RespondToModel(
|
||||
mode.unsupported_items_error().to_string(),
|
||||
))
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Handles the shared MultiAgentV2 text-message flow for both `send_message` and `assign_task`.
|
||||
pub(crate) async fn handle_message_tool(
|
||||
/// Handles the shared MultiAgentV2 plain-text message flow for both `send_message` and `assign_task`.
|
||||
pub(crate) async fn handle_message_string_tool(
|
||||
invocation: ToolInvocation,
|
||||
mode: MessageDeliveryMode,
|
||||
target: String,
|
||||
items: Vec<UserInput>,
|
||||
message: String,
|
||||
interrupt: bool,
|
||||
) -> Result<MessageToolResult, FunctionCallError> {
|
||||
handle_message_submission(
|
||||
invocation,
|
||||
mode,
|
||||
target,
|
||||
message_content(message)?,
|
||||
interrupt,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_message_submission(
|
||||
invocation: ToolInvocation,
|
||||
mode: MessageDeliveryMode,
|
||||
target: String,
|
||||
prompt: String,
|
||||
interrupt: bool,
|
||||
) -> Result<MessageToolResult, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
@@ -117,12 +113,21 @@ pub(crate) async fn handle_message_tool(
|
||||
} = invocation;
|
||||
let _ = payload;
|
||||
let receiver_thread_id = resolve_agent_target(&session, &turn, &target).await?;
|
||||
let prompt = text_content(&items, mode)?;
|
||||
let receiver_agent = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_metadata(receiver_thread_id)
|
||||
.unwrap_or_default();
|
||||
if mode == MessageDeliveryMode::TriggerTurn
|
||||
&& receiver_agent
|
||||
.agent_path
|
||||
.as_ref()
|
||||
.is_some_and(AgentPath::is_root)
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"Tasks can't be assigned to the root agent".to_string(),
|
||||
));
|
||||
}
|
||||
if interrupt {
|
||||
session
|
||||
.services
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::message_tool::MessageDeliveryMode;
|
||||
use super::message_tool::MessageToolResult;
|
||||
use super::message_tool::SendMessageArgs;
|
||||
use super::message_tool::handle_message_tool;
|
||||
use super::message_tool::handle_message_string_tool;
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
@@ -21,11 +21,11 @@ impl ToolHandler for Handler {
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
let arguments = function_arguments(invocation.payload.clone())?;
|
||||
let args: SendMessageArgs = parse_arguments(&arguments)?;
|
||||
handle_message_tool(
|
||||
handle_message_string_tool(
|
||||
invocation,
|
||||
MessageDeliveryMode::QueueOnly,
|
||||
args.target,
|
||||
args.items,
|
||||
args.message,
|
||||
/*interrupt*/ false,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -40,7 +40,7 @@ impl ToolHandler for Handler {
|
||||
.map(str::trim)
|
||||
.filter(|role| !role.is_empty());
|
||||
|
||||
let initial_operation = parse_collab_input(/*message*/ None, Some(args.items))?;
|
||||
let initial_operation = parse_collab_input(Some(args.message), /*items*/ None)?;
|
||||
let prompt = render_input_preview(&initial_operation);
|
||||
|
||||
let session_source = turn.session_source.clone();
|
||||
@@ -202,7 +202,7 @@ impl ToolHandler for Handler {
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct SpawnAgentArgs {
|
||||
items: Vec<UserInput>,
|
||||
message: String,
|
||||
task_name: String,
|
||||
agent_type: Option<String>,
|
||||
model: Option<String>,
|
||||
|
||||
@@ -75,6 +75,7 @@ impl ToolHandler for Handler {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct WaitArgs {
|
||||
timeout_ms: Option<i64>,
|
||||
}
|
||||
|
||||
@@ -497,13 +497,13 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
|
||||
panic!("spawn_agent should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("task_name"));
|
||||
assert!(properties.contains_key("items"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert!(properties.contains_key("fork_turns"));
|
||||
assert!(!properties.contains_key("message"));
|
||||
assert!(!properties.contains_key("items"));
|
||||
assert!(!properties.contains_key("fork_context"));
|
||||
assert_eq!(
|
||||
required.as_ref(),
|
||||
Some(&vec!["task_name".to_string(), "items".to_string()])
|
||||
Some(&vec!["task_name".to_string(), "message".to_string()])
|
||||
);
|
||||
let output_schema = output_schema
|
||||
.as_ref()
|
||||
@@ -527,10 +527,11 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
|
||||
};
|
||||
assert!(properties.contains_key("target"));
|
||||
assert!(!properties.contains_key("interrupt"));
|
||||
assert!(!properties.contains_key("message"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert!(!properties.contains_key("items"));
|
||||
assert_eq!(
|
||||
required.as_ref(),
|
||||
Some(&vec!["target".to_string(), "items".to_string()])
|
||||
Some(&vec!["target".to_string(), "message".to_string()])
|
||||
);
|
||||
|
||||
let assign_task = find_tool(&tools, "assign_task");
|
||||
@@ -546,10 +547,11 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
|
||||
panic!("assign_task should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("target"));
|
||||
assert!(!properties.contains_key("message"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert!(!properties.contains_key("items"));
|
||||
assert_eq!(
|
||||
required.as_ref(),
|
||||
Some(&vec!["target".to_string(), "items".to_string()])
|
||||
Some(&vec!["target".to_string(), "message".to_string()])
|
||||
);
|
||||
|
||||
let wait_agent = find_tool(&tools, "wait_agent");
|
||||
|
||||
3
codex-rs/state/migrations/0023_drop_logs.sql
Normal file
3
codex-rs/state/migrations/0023_drop_logs.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
PRAGMA auto_vacuum = INCREMENTAL;
|
||||
|
||||
DROP TABLE IF EXISTS logs;
|
||||
@@ -147,12 +147,28 @@ fn base_sqlite_options(path: &Path) -> SqliteConnectOptions {
|
||||
}
|
||||
|
||||
async fn open_state_sqlite(path: &Path, migrator: &'static Migrator) -> anyhow::Result<SqlitePool> {
|
||||
let options = base_sqlite_options(path);
|
||||
let options = base_sqlite_options(path).auto_vacuum(SqliteAutoVacuum::Incremental);
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(options)
|
||||
.await?;
|
||||
migrator.run(&pool).await?;
|
||||
let auto_vacuum = sqlx::query_scalar::<_, i64>("PRAGMA auto_vacuum")
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
if auto_vacuum != SqliteAutoVacuum::Incremental as i64 {
|
||||
// Existing state DBs need one non-transactional `VACUUM` before
|
||||
// SQLite persists `auto_vacuum = INCREMENTAL` in the database header.
|
||||
sqlx::query("PRAGMA auto_vacuum = INCREMENTAL")
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
// We do it on best effort. If the lock can't be acquired, it will be done at next run.
|
||||
let _ = sqlx::query("VACUUM").execute(&pool).await;
|
||||
}
|
||||
// We do it on best effort. If the lock can't be acquired, it will be done at next run.
|
||||
let _ = sqlx::query("PRAGMA incremental_vacuum")
|
||||
.execute(&pool)
|
||||
.await;
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
|
||||
@@ -537,7 +537,6 @@ mod tests {
|
||||
use crate::LogQuery;
|
||||
use crate::logs_db_path;
|
||||
use crate::migrations::LOGS_MIGRATOR;
|
||||
use crate::state_db_path;
|
||||
use chrono::Utc;
|
||||
use pretty_assertions::assert_eq;
|
||||
use sqlx::SqlitePool;
|
||||
@@ -590,10 +589,8 @@ mod tests {
|
||||
.await
|
||||
.expect("insert test logs");
|
||||
|
||||
let state_count = log_row_count(state_db_path(codex_home.as_path()).as_path()).await;
|
||||
let logs_count = log_row_count(logs_db_path(codex_home.as_path()).as_path()).await;
|
||||
|
||||
assert_eq!(state_count, 0);
|
||||
assert_eq!(logs_count, 1);
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(codex_home).await;
|
||||
|
||||
@@ -66,7 +66,7 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions<'_>) -> ToolSpe
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["task_name".to_string(), "items".to_string()]),
|
||||
required: Some(vec!["task_name".to_string(), "message".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: Some(spawn_agent_output_schema_v2()),
|
||||
@@ -127,7 +127,12 @@ pub fn create_send_message_tool() -> ToolSpec {
|
||||
),
|
||||
},
|
||||
),
|
||||
("items".to_string(), create_collab_input_items_schema()),
|
||||
(
|
||||
"message".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Message text to queue on the target agent.".to_string()),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
@@ -138,7 +143,7 @@ pub fn create_send_message_tool() -> ToolSpec {
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["target".to_string(), "items".to_string()]),
|
||||
required: Some(vec!["target".to_string(), "message".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: Some(send_input_output_schema()),
|
||||
@@ -155,7 +160,12 @@ pub fn create_assign_task_tool() -> ToolSpec {
|
||||
),
|
||||
},
|
||||
),
|
||||
("items".to_string(), create_collab_input_items_schema()),
|
||||
(
|
||||
"message".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Message text to send to the target agent.".to_string()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"interrupt".to_string(),
|
||||
JsonSchema::Boolean {
|
||||
@@ -169,13 +179,13 @@ pub fn create_assign_task_tool() -> ToolSpec {
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "assign_task".to_string(),
|
||||
description: "Add a message to an existing agent and trigger a turn in the target. Use interrupt=true to redirect work immediately. In MultiAgentV2, this tool currently supports text content only."
|
||||
description: "Add a message to an existing non-root agent and trigger a turn in the target. Use interrupt=true to redirect work immediately. In MultiAgentV2, this tool currently supports text content only."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["target".to_string(), "items".to_string()]),
|
||||
required: Some(vec!["target".to_string(), "message".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: Some(send_input_output_schema()),
|
||||
@@ -221,7 +231,7 @@ pub fn create_wait_agent_tool_v1(options: WaitAgentTimeoutOptions) -> ToolSpec {
|
||||
pub fn create_wait_agent_tool_v2(options: WaitAgentTimeoutOptions) -> ToolSpec {
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "wait_agent".to_string(),
|
||||
description: "Wait for agents to reach a final status. Returns a brief wait summary instead of the agent's final content. Returns a timeout summary when no agent reaches a final status before the deadline."
|
||||
description: "Wait for a mailbox update from any live agent, including queued messages and final-status notifications. Returns a brief wait summary instead of agent content, or a timeout summary if no mailbox update arrives before the deadline."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
@@ -308,7 +318,7 @@ fn agent_status_output_schema() -> Value {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["pending_init", "running", "shutdown", "not_found"]
|
||||
"enum": ["pending_init", "running", "interrupted", "shutdown", "not_found"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
@@ -585,7 +595,12 @@ fn spawn_agent_common_properties_v1(agent_type_description: &str) -> BTreeMap<St
|
||||
|
||||
fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<String, JsonSchema> {
|
||||
BTreeMap::from([
|
||||
("items".to_string(), create_collab_input_items_schema()),
|
||||
(
|
||||
"message".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Initial plain-text task for the new agent.".to_string()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"agent_type".to_string(),
|
||||
JsonSchema::String {
|
||||
|
||||
@@ -56,9 +56,9 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
assert!(description.contains("visible display (`visible-model`)"));
|
||||
assert!(!description.contains("hidden display (`hidden-model`)"));
|
||||
assert!(properties.contains_key("task_name"));
|
||||
assert!(properties.contains_key("items"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert!(properties.contains_key("fork_turns"));
|
||||
assert!(!properties.contains_key("message"));
|
||||
assert!(!properties.contains_key("items"));
|
||||
assert!(!properties.contains_key("fork_context"));
|
||||
assert_eq!(
|
||||
properties.get("agent_type"),
|
||||
@@ -68,7 +68,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
);
|
||||
assert_eq!(
|
||||
required,
|
||||
Some(vec!["task_name".to_string(), "items".to_string()])
|
||||
Some(vec!["task_name".to_string(), "message".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
output_schema.expect("spawn_agent output schema")["required"],
|
||||
@@ -95,7 +95,7 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_tool_requires_items_and_uses_submission_output() {
|
||||
fn send_message_tool_requires_message_and_uses_submission_output() {
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
parameters,
|
||||
output_schema,
|
||||
@@ -113,12 +113,12 @@ fn send_message_tool_requires_items_and_uses_submission_output() {
|
||||
panic!("send_message should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("target"));
|
||||
assert!(properties.contains_key("items"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert!(!properties.contains_key("interrupt"));
|
||||
assert!(!properties.contains_key("message"));
|
||||
assert!(!properties.contains_key("items"));
|
||||
assert_eq!(
|
||||
required,
|
||||
Some(vec!["target".to_string(), "items".to_string()])
|
||||
Some(vec!["target".to_string(), "message".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
output_schema.expect("send_message output schema")["required"],
|
||||
@@ -126,6 +126,38 @@ fn send_message_tool_requires_items_and_uses_submission_output() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assign_task_tool_requires_message_and_uses_submission_output() {
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
parameters,
|
||||
output_schema,
|
||||
..
|
||||
}) = create_assign_task_tool()
|
||||
else {
|
||||
panic!("assign_task should be a function tool");
|
||||
};
|
||||
let JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
..
|
||||
} = parameters
|
||||
else {
|
||||
panic!("assign_task should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("target"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert!(properties.contains_key("interrupt"));
|
||||
assert!(!properties.contains_key("items"));
|
||||
assert_eq!(
|
||||
required,
|
||||
Some(vec!["target".to_string(), "message".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
output_schema.expect("assign_task output schema")["required"],
|
||||
json!(["submission_id"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wait_agent_tool_v2_uses_timeout_only_summary_output() {
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
@@ -176,3 +208,23 @@ fn list_agents_tool_includes_path_prefix_and_agent_fields() {
|
||||
json!(["agent_name", "agent_status", "last_task_message"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_agents_tool_status_schema_includes_interrupted() {
|
||||
let ToolSpec::Function(ResponsesApiTool { output_schema, .. }) = create_list_agents_tool()
|
||||
else {
|
||||
panic!("list_agents should be a function tool");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
output_schema.expect("list_agents output schema")["properties"]["agents"]["items"]["properties"]
|
||||
["agent_status"]["allOf"][0]["oneOf"][0]["enum"],
|
||||
json!([
|
||||
"pending_init",
|
||||
"running",
|
||||
"interrupted",
|
||||
"shutdown",
|
||||
"not_found"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
3
justfile
3
justfile
@@ -69,8 +69,9 @@ bazel-lock-check:
|
||||
bazel-test:
|
||||
bazel test --test_tag_filters=-argument-comment-lint //... --keep_going
|
||||
|
||||
[no-cd]
|
||||
bazel-clippy:
|
||||
bazel build --config=clippy -- //codex-rs/... -//codex-rs/v8-poc:all
|
||||
bazel_targets="$(./scripts/list-bazel-clippy-targets.sh)" && bazel build --config=clippy -- ${bazel_targets}
|
||||
|
||||
[no-cd]
|
||||
bazel-argument-comment-lint:
|
||||
|
||||
20
scripts/list-bazel-clippy-targets.sh
Executable file
20
scripts/list-bazel-clippy-targets.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
# Resolve the dynamic targets before printing anything so callers do not
|
||||
# continue with a partial list if `bazel query` fails.
|
||||
manual_rust_test_targets="$(bazel query 'kind("rust_test rule", attr(tags, "manual", //codex-rs/... except //codex-rs/v8-poc/...))')"
|
||||
|
||||
printf '%s\n' \
|
||||
"//codex-rs/..." \
|
||||
"-//codex-rs/v8-poc:all"
|
||||
|
||||
# `--config=clippy` on the `workspace_root_test` wrappers does not lint the
|
||||
# underlying `rust_test` binaries. Add the internal manual `*-unit-tests-bin`
|
||||
# targets explicitly so inline `#[cfg(test)]` code is linted like
|
||||
# `cargo clippy --tests`.
|
||||
printf '%s\n' "${manual_rust_test_targets}"
|
||||
Reference in New Issue
Block a user