Yield orchestrators for child groups and correct subscription context catalogs
This commit is contained in:
@@ -1261,9 +1261,26 @@ impl ProviderRun {
|
||||
});
|
||||
};
|
||||
let next_epoch = self.next_epoch()?;
|
||||
let yielded_to_children = batch.calls.iter().any(|pending| {
|
||||
is_local_agent_launch(&pending.call)
|
||||
&& pending
|
||||
.state
|
||||
.result()
|
||||
.is_some_and(|result| result.status == ToolResultStatus::Success)
|
||||
});
|
||||
self.transcript.push(tool_result_message(results));
|
||||
self.epoch = next_epoch;
|
||||
self.state = ProviderRunState::ReadyToCallModel;
|
||||
// Launch acknowledgement is a yield, not an invitation to issue more tools.
|
||||
// The client persists child membership and supplies a new input once the group finishes.
|
||||
self.state = if yielded_to_children {
|
||||
ProviderRunState::Done {
|
||||
completion: ProviderRunCompletion {
|
||||
stop_reason: StopReason::Completed,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
ProviderRunState::ReadyToCallModel
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1382,12 +1399,25 @@ impl ProviderRun {
|
||||
|
||||
fn build_tool_batch(&self, turn: &CompletedModelTurn) -> PendingToolBatch {
|
||||
let policy = ToolPolicy::from_names(turn.advertised_tools.iter().cloned());
|
||||
let has_launch = turn.tool_calls.iter().any(|call| {
|
||||
is_local_agent_launch(call)
|
||||
&& call.arguments.is_object()
|
||||
&& turn.advertised_tools.contains(&call.name)
|
||||
});
|
||||
let calls = turn
|
||||
.tool_calls
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|call| {
|
||||
let state = if !call.arguments.is_object() {
|
||||
let state = if has_launch && !is_local_agent_launch(&call) {
|
||||
PendingToolCallState::Resolved {
|
||||
result: ToolResult {
|
||||
call_id: call.id.clone(),
|
||||
content: "Not executed: launching a child group yields this turn. Wait for the group's completion before scheduling dependent work.".to_string(),
|
||||
status: ToolResultStatus::Error,
|
||||
},
|
||||
}
|
||||
} else if !call.arguments.is_object() {
|
||||
PendingToolCallState::Resolved {
|
||||
result: ToolResult {
|
||||
call_id: call.id.clone(),
|
||||
@@ -1626,6 +1656,11 @@ fn usage_contains(total: &Usage, part: &Usage) -> bool {
|
||||
&& total.cache_creation_input_tokens >= part.cache_creation_input_tokens
|
||||
}
|
||||
|
||||
fn is_local_agent_launch(call: &ToolCall) -> bool {
|
||||
// Galaxy normalizes direct-provider delegation to local child execution.
|
||||
matches!(call.name.as_str(), "run_agents" | "start_agent")
|
||||
}
|
||||
|
||||
fn validate_model_turn(turn: &CompletedModelTurn) -> Result<(), ProviderRunProtocolError> {
|
||||
for part in &turn.assistant_content {
|
||||
match part {
|
||||
|
||||
@@ -1206,20 +1206,20 @@ fn restore_normalization_preserves_selected_executing_tools_for_recovery() {
|
||||
&mut run,
|
||||
tool_turn(
|
||||
vec![
|
||||
tool_call("run-agents", "run_agents"),
|
||||
tool_call("command", "run_command"),
|
||||
tool_call("read", "read_files"),
|
||||
],
|
||||
&["run_agents", "read_files"],
|
||||
&["run_command", "read_files"],
|
||||
),
|
||||
);
|
||||
run.start_tool(&batch.work_id, "run-agents").unwrap();
|
||||
run.start_tool(&batch.work_id, "command").unwrap();
|
||||
run.start_tool(&batch.work_id, "read").unwrap();
|
||||
|
||||
let normalization = run
|
||||
.normalize_after_restore_with_recoverable_calls(&HashSet::from(["run-agents".to_string()]))
|
||||
.normalize_after_restore_with_recoverable_calls(&HashSet::from(["command".to_string()]))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(normalization.recovery_call_ids, vec!["run-agents"]);
|
||||
assert_eq!(normalization.recovery_call_ids, vec!["command"]);
|
||||
assert_eq!(normalization.interrupted_call_ids, vec!["read"]);
|
||||
let ProviderRunState::AwaitingTools { batch } = run.state() else {
|
||||
panic!("recovered tool batch should remain pending");
|
||||
@@ -1252,7 +1252,7 @@ fn recovery_pending_tool_survives_another_restore_and_completes_once() {
|
||||
restored
|
||||
.complete_tool(
|
||||
&batch.work_id,
|
||||
successful_result("run-agents", "children completed"),
|
||||
successful_result("run-agents", "children launched"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
@@ -1263,7 +1263,7 @@ fn recovery_pending_tool_survives_another_restore_and_completes_once() {
|
||||
Err(ProviderRunProtocolError::DuplicateToolUpdate { .. })
|
||||
));
|
||||
restored.commit_tool_batch(&batch.work_id).unwrap();
|
||||
assert_eq!(restored.state().phase(), ProviderRunPhase::ReadyToCallModel);
|
||||
assert_eq!(restored.state().phase(), ProviderRunPhase::Done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1465,3 +1465,58 @@ fn multipart_archive_is_not_multiplied_and_legacy_duplicates_are_repaired() {
|
||||
archive_tool_results(&mut archive, transcript);
|
||||
assert_eq!(archive, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_child_launch_yields_and_does_not_execute_batched_follow_up_tools() {
|
||||
let mut run = run();
|
||||
let batch = accept_tool_turn(
|
||||
&mut run,
|
||||
tool_turn(
|
||||
vec![
|
||||
tool_call("children", "run_agents"),
|
||||
tool_call("edit", "apply_file_diffs"),
|
||||
],
|
||||
&["run_agents", "apply_file_diffs"],
|
||||
),
|
||||
);
|
||||
assert!(matches!(
|
||||
batch.calls[0].state,
|
||||
PendingToolCallState::Proposed
|
||||
));
|
||||
assert!(
|
||||
matches!(&batch.calls[1].state, PendingToolCallState::Resolved { result }
|
||||
if result.status == ToolResultStatus::Error)
|
||||
);
|
||||
run.complete_tool(
|
||||
&batch.work_id,
|
||||
successful_result("children", "Children launched"),
|
||||
)
|
||||
.unwrap();
|
||||
run.commit_tool_batch(&batch.work_id).unwrap();
|
||||
assert!(matches!(run.state(), ProviderRunState::Done { .. }));
|
||||
assert_serialization_round_trip(&run);
|
||||
assert!(matches!(
|
||||
run.next_step().unwrap(),
|
||||
Some(ProviderRunStep::Done(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_child_launch_allows_a_corrective_model_turn() {
|
||||
let mut run = run();
|
||||
let batch = accept_tool_turn(
|
||||
&mut run,
|
||||
tool_turn(vec![tool_call("children", "run_agents")], &["run_agents"]),
|
||||
);
|
||||
run.complete_tool(
|
||||
&batch.work_id,
|
||||
ToolResult {
|
||||
call_id: "children".to_string(),
|
||||
content: "Launch failed".to_string(),
|
||||
status: ToolResultStatus::Error,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
run.commit_tool_batch(&batch.work_id).unwrap();
|
||||
assert!(matches!(run.state(), ProviderRunState::ReadyToCallModel));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user