mirror of
https://github.com/openai/codex.git
synced 2026-04-01 21:14:08 +03:00
Compare commits
8 Commits
dev/friel/
...
latest-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b17e4e1a47 | ||
|
|
5bbfee69b6 | ||
|
|
609ac0c7ab | ||
|
|
df5f79da36 | ||
|
|
0c776c433b | ||
|
|
3152d1a557 | ||
|
|
23d638a573 | ||
|
|
d0474f2bc1 |
@@ -85,7 +85,7 @@ members = [
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.0"
|
||||
version = "0.119.0-alpha.2"
|
||||
# Track the edition for all workspace crates in one place. Individual
|
||||
# crates can still override this value, but keeping it here means new
|
||||
# crates created with `cargo new -w ...` automatically inherit the 2024
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user