mirror of
https://github.com/openai/codex.git
synced 2026-05-06 06:12:59 +03:00
feat: add connector capabilities to sub-agents (#11191)
This commit is contained in:
@@ -40,7 +40,7 @@ impl AgentControl {
|
||||
pub(crate) async fn spawn_agent(
|
||||
&self,
|
||||
config: crate::config::Config,
|
||||
prompt: String,
|
||||
items: Vec<UserInput>,
|
||||
session_source: Option<SessionSource>,
|
||||
) -> CodexResult<ThreadId> {
|
||||
let state = self.upgrade()?;
|
||||
@@ -62,7 +62,7 @@ impl AgentControl {
|
||||
// TODO(jif) add helper for drain
|
||||
state.notify_thread_created(new_thread.thread_id);
|
||||
|
||||
self.send_prompt(new_thread.thread_id, prompt).await?;
|
||||
self.send_input(new_thread.thread_id, items).await?;
|
||||
|
||||
Ok(new_thread.thread_id)
|
||||
}
|
||||
@@ -93,22 +93,18 @@ impl AgentControl {
|
||||
Ok(resumed_thread.thread_id)
|
||||
}
|
||||
|
||||
/// Send a `user` prompt to an existing agent thread.
|
||||
pub(crate) async fn send_prompt(
|
||||
/// Send rich user input items to an existing agent thread.
|
||||
pub(crate) async fn send_input(
|
||||
&self,
|
||||
agent_id: ThreadId,
|
||||
prompt: String,
|
||||
items: Vec<UserInput>,
|
||||
) -> CodexResult<String> {
|
||||
let state = self.upgrade()?;
|
||||
let result = state
|
||||
.send_op(
|
||||
agent_id,
|
||||
Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: prompt,
|
||||
// Agent control prompts are plain text with no UI text elements.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
items,
|
||||
final_output_json_schema: None,
|
||||
},
|
||||
)
|
||||
@@ -202,6 +198,13 @@ mod tests {
|
||||
test_config_with_cli_overrides(Vec::new()).await
|
||||
}
|
||||
|
||||
fn text_input(text: &str) -> Vec<UserInput> {
|
||||
vec![UserInput::Text {
|
||||
text: text.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
}
|
||||
|
||||
struct AgentControlHarness {
|
||||
_home: TempDir,
|
||||
config: Config,
|
||||
@@ -237,12 +240,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_prompt_errors_when_manager_dropped() {
|
||||
async fn send_input_errors_when_manager_dropped() {
|
||||
let control = AgentControl::default();
|
||||
let err = control
|
||||
.send_prompt(ThreadId::new(), "hello".to_string())
|
||||
.send_input(
|
||||
ThreadId::new(),
|
||||
vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.expect_err("send_prompt should fail without a manager");
|
||||
.expect_err("send_input should fail without a manager");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"unsupported operation: thread manager dropped"
|
||||
@@ -306,7 +315,7 @@ mod tests {
|
||||
let control = AgentControl::default();
|
||||
let (_home, config) = test_config().await;
|
||||
let err = control
|
||||
.spawn_agent(config, "hello".to_string(), None)
|
||||
.spawn_agent(config, text_input("hello"), None)
|
||||
.await
|
||||
.expect_err("spawn_agent should fail without a manager");
|
||||
assert_eq!(
|
||||
@@ -334,14 +343,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_prompt_errors_when_thread_missing() {
|
||||
async fn send_input_errors_when_thread_missing() {
|
||||
let harness = AgentControlHarness::new().await;
|
||||
let thread_id = ThreadId::new();
|
||||
let err = harness
|
||||
.control
|
||||
.send_prompt(thread_id, "hello".to_string())
|
||||
.send_input(
|
||||
thread_id,
|
||||
vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.expect_err("send_prompt should fail for missing thread");
|
||||
.expect_err("send_input should fail for missing thread");
|
||||
assert_matches!(err, CodexErr::ThreadNotFound(id) if id == thread_id);
|
||||
}
|
||||
|
||||
@@ -393,15 +408,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_prompt_submits_user_message() {
|
||||
async fn send_input_submits_user_message() {
|
||||
let harness = AgentControlHarness::new().await;
|
||||
let (thread_id, _thread) = harness.start_thread().await;
|
||||
|
||||
let submission_id = harness
|
||||
.control
|
||||
.send_prompt(thread_id, "hello from tests".to_string())
|
||||
.send_input(
|
||||
thread_id,
|
||||
vec![UserInput::Text {
|
||||
text: "hello from tests".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.expect("send_prompt should succeed");
|
||||
.expect("send_input should succeed");
|
||||
assert!(!submission_id.is_empty());
|
||||
let expected = (
|
||||
thread_id,
|
||||
@@ -426,7 +447,7 @@ mod tests {
|
||||
let harness = AgentControlHarness::new().await;
|
||||
let thread_id = harness
|
||||
.control
|
||||
.spawn_agent(harness.config.clone(), "spawned".to_string(), None)
|
||||
.spawn_agent(harness.config.clone(), text_input("spawned"), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
let _thread = harness
|
||||
@@ -473,12 +494,12 @@ mod tests {
|
||||
.expect("start thread");
|
||||
|
||||
let first_agent_id = control
|
||||
.spawn_agent(config.clone(), "hello".to_string(), None)
|
||||
.spawn_agent(config.clone(), text_input("hello"), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
|
||||
let err = control
|
||||
.spawn_agent(config, "hello again".to_string(), None)
|
||||
.spawn_agent(config, text_input("hello again"), None)
|
||||
.await
|
||||
.expect_err("spawn_agent should respect max threads");
|
||||
let CodexErr::AgentLimitReached {
|
||||
@@ -511,7 +532,7 @@ mod tests {
|
||||
let control = manager.agent_control();
|
||||
|
||||
let first_agent_id = control
|
||||
.spawn_agent(config.clone(), "hello".to_string(), None)
|
||||
.spawn_agent(config.clone(), text_input("hello"), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
let _ = control
|
||||
@@ -520,7 +541,7 @@ mod tests {
|
||||
.expect("shutdown agent");
|
||||
|
||||
let second_agent_id = control
|
||||
.spawn_agent(config.clone(), "hello again".to_string(), None)
|
||||
.spawn_agent(config.clone(), text_input("hello again"), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed after shutdown");
|
||||
let _ = control
|
||||
@@ -546,12 +567,12 @@ mod tests {
|
||||
let cloned = control.clone();
|
||||
|
||||
let first_agent_id = cloned
|
||||
.spawn_agent(config.clone(), "hello".to_string(), None)
|
||||
.spawn_agent(config.clone(), text_input("hello"), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
|
||||
let err = control
|
||||
.spawn_agent(config, "hello again".to_string(), None)
|
||||
.spawn_agent(config, text_input("hello again"), None)
|
||||
.await
|
||||
.expect_err("spawn_agent should respect shared guard");
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
@@ -581,7 +602,7 @@ mod tests {
|
||||
let control = manager.agent_control();
|
||||
|
||||
let resumable_id = control
|
||||
.spawn_agent(config.clone(), "hello".to_string(), None)
|
||||
.spawn_agent(config.clone(), text_input("hello"), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
let rollout_path = manager
|
||||
@@ -596,7 +617,7 @@ mod tests {
|
||||
.expect("shutdown resumable thread");
|
||||
|
||||
let active_id = control
|
||||
.spawn_agent(config.clone(), "occupy".to_string(), None)
|
||||
.spawn_agent(config.clone(), text_input("occupy"), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed for active slot");
|
||||
|
||||
@@ -640,7 +661,7 @@ mod tests {
|
||||
.expect_err("resume should fail for missing rollout path");
|
||||
|
||||
let resumed_id = control
|
||||
.spawn_agent(config, "hello".to_string(), None)
|
||||
.spawn_agent(config, text_input("hello"), None)
|
||||
.await
|
||||
.expect("spawn should succeed after failed resume");
|
||||
let _ = control
|
||||
|
||||
Reference in New Issue
Block a user