feat: add connector capabilities to sub-agents (#11191)

This commit is contained in:
jif-oai
2026-02-10 11:53:01 +00:00
committed by GitHub
parent 6049ff02a0
commit 87ccc5bbae
3 changed files with 314 additions and 89 deletions

View File

@@ -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