mirror of
https://github.com/openai/codex.git
synced 2026-04-30 11:21:34 +03:00
feat: fork thread multi agent (#12499)
This commit is contained in:
@@ -20,6 +20,8 @@ use tokio::time::sleep;
|
||||
use wiremock::MockServer;
|
||||
|
||||
const SPAWN_CALL_ID: &str = "spawn-call-1";
|
||||
const FORKED_SPAWN_AGENT_OUTPUT_MESSAGE: &str = "You are the newly spawned agent. The prior conversation history was forked from your parent agent. Treat the next user message as your new task, and use the forked history only as background context.";
|
||||
const TURN_0_FORK_PROMPT: &str = "seed fork context";
|
||||
const TURN_1_PROMPT: &str = "spawn a child and continue";
|
||||
const TURN_2_NO_WAIT_PROMPT: &str = "follow up without wait";
|
||||
const CHILD_PROMPT: &str = "child: do work";
|
||||
@@ -194,3 +196,116 @@ async fn subagent_notification_is_included_without_wait() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn spawned_child_receives_forked_parent_context() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
||||
let seed_turn = mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| body_contains(req, TURN_0_FORK_PROMPT),
|
||||
sse(vec![
|
||||
ev_response_created("resp-seed-1"),
|
||||
ev_assistant_message("msg-seed-1", "seeded"),
|
||||
ev_completed("resp-seed-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let spawn_args = serde_json::to_string(&json!({
|
||||
"message": CHILD_PROMPT,
|
||||
"fork_context": true,
|
||||
}))?;
|
||||
let spawn_turn = mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| body_contains(req, TURN_1_PROMPT),
|
||||
sse(vec![
|
||||
ev_response_created("resp-turn1-1"),
|
||||
ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args),
|
||||
ev_completed("resp-turn1-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let _child_request_log = mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| body_contains(req, CHILD_PROMPT),
|
||||
sse(vec![
|
||||
ev_response_created("resp-child-1"),
|
||||
ev_assistant_message("msg-child-1", "child done"),
|
||||
ev_completed("resp-child-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let _turn1_followup = mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| body_contains(req, SPAWN_CALL_ID),
|
||||
sse(vec![
|
||||
ev_response_created("resp-turn1-2"),
|
||||
ev_assistant_message("msg-turn1-2", "parent done"),
|
||||
ev_completed("resp-turn1-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config.features.enable(Feature::Collab);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn(TURN_0_FORK_PROMPT).await?;
|
||||
let _ = seed_turn.single_request();
|
||||
|
||||
test.submit_turn(TURN_1_PROMPT).await?;
|
||||
let _ = spawn_turn.single_request();
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
let child_request = loop {
|
||||
if let Some(request) = server
|
||||
.received_requests()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.find(|request| {
|
||||
body_contains(request, CHILD_PROMPT)
|
||||
&& body_contains(request, FORKED_SPAWN_AGENT_OUTPUT_MESSAGE)
|
||||
})
|
||||
{
|
||||
break request;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
anyhow::bail!("timed out waiting for forked child request");
|
||||
}
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
};
|
||||
assert!(body_contains(&child_request, TURN_0_FORK_PROMPT));
|
||||
assert!(body_contains(&child_request, "seeded"));
|
||||
|
||||
let child_body = child_request
|
||||
.body_json::<serde_json::Value>()
|
||||
.expect("forked child request body should be json");
|
||||
let function_call_output = child_body["input"]
|
||||
.as_array()
|
||||
.and_then(|items| {
|
||||
items.iter().find(|item| {
|
||||
item["type"].as_str() == Some("function_call_output")
|
||||
&& item["call_id"].as_str() == Some(SPAWN_CALL_ID)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| panic!("expected forked child request to include spawn_agent output"));
|
||||
let (content, success) = match &function_call_output["output"] {
|
||||
serde_json::Value::String(text) => (Some(text.as_str()), None),
|
||||
serde_json::Value::Object(output) => (
|
||||
output.get("content").and_then(serde_json::Value::as_str),
|
||||
output.get("success").and_then(serde_json::Value::as_bool),
|
||||
),
|
||||
_ => (None, None),
|
||||
};
|
||||
assert_eq!(content, Some(FORKED_SPAWN_AGENT_OUTPUT_MESSAGE));
|
||||
assert_ne!(success, Some(false));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user