465
match name {
466
TASK_TOOL_NAME => exec_task(arguments).await,
467
WEB_SEARCH_TOOL_NAME => exec_web_search(arguments).await,
468
- "command_output" => exec_command_output_wait(arguments).await,
468
+ "command_output" => exec_command_output_wait(arguments, active_session()).await,
469
// Tools discovered from MCP servers are namespaced `mcp__<server>__<tool>`
470
// and forwarded to the owning server.
471
_ if crate::mcp::is_mcp_tool(name) => crate::mcp::call_tool(name, arguments).await,
2516
poll_command_output(arguments, owner).text
2517
}
2518
2519
-/// One `command_output` check, with the tool text and whether anything
2520
-/// changed, so the long-poll loop never has to parse its own wording.
2519
+/// One `command_output` check, with the tool text and whether the task is
2520
+/// still running, so the long-poll loop knows when to keep waiting.
2521
struct CommandPoll {
2522
text: String,
2523
- /// Still running and nothing new printed since the last check.
2524
- idle: bool,
2523
+ /// True while the task's child process is still running.
2524
+ running: bool,
2525
}
2526
2527
impl CommandPoll {
2528
/// A result that ends the wait: an error, or a task that isn't ours.
2529
fn done(text: String) -> Self {
2530
- Self { text, idle: false }
2530
+ Self {
2531
+ text,
2532
+ running: false,
2533
+ }
2534
}
2535
}
2536
2565
Some(code) => format!("Task {task_id} (`{command}`) exited with code {code}."),
2566
};
2567
2565
- let idle = exit_code.is_none() && new_output.is_empty();
2568
+ let running = exit_code.is_none();
2569
let text = if new_output.is_empty() {
2570
format!(
2571
"{status} No new output since the last check.{}",
2577
dropped_note(dropped)
2578
)
2579
};
2577
- CommandPoll { text, idle }
2580
+ CommandPoll { text, running }
2581
}
2582
2583
/// Long-poll a background task without occupying Tokio's blocking pool while
2584
/// nothing changes. This keeps model tool rounds from being consumed by a
2585
/// tight status-check loop.
2583
-async fn exec_command_output_wait(arguments: &str) -> String {
2586
+async fn exec_command_output_wait(arguments: &str, owner: Option<String>) -> String {
2587
const DEFAULT_WAIT_SECONDS: u64 = 10;
2588
const MAX_WAIT_SECONDS: u64 = 30;
2589
const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(200);
2593
.and_then(|args| args.get("wait_seconds").and_then(Value::as_u64))
2594
.unwrap_or(DEFAULT_WAIT_SECONDS)
2595
.min(MAX_WAIT_SECONDS);
2593
- let owner = active_session();
2596
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(wait_seconds);
2597
2598
loop {
2605
CommandPoll::done(format!("Error: command_output task failed: {err}"))
2606
});
2607
2606
- if wait_seconds == 0 || !poll.idle || tokio::time::Instant::now() >= deadline {
2608
+ if wait_seconds == 0 || !poll.running || tokio::time::Instant::now() >= deadline {
2609
return poll.text;
2610
}
2611
3869
"wait_seconds": 5
3870
})
3871
.to_string();
3870
- let poll = exec_command_output_wait(&poll_args).await;
3872
+ let poll = exec_command_output_wait(&poll_args, None).await;
3873
3874
assert!(poll.contains("exited with code 0"), "got: {poll}");
3875
assert!(poll.contains("done"), "got: {poll}");
3892
3893
let poll_args = serde_json::json!({ "task_id": task_id, "wait_seconds": 1 }).to_string();
3894
let started = std::time::Instant::now();
3893
- let poll = exec_command_output_wait(&poll_args).await;
3895
+ let poll = exec_command_output_wait(&poll_args, None).await;
3896
3897
assert!(poll.contains("still running"), "got: {poll}");
3898
assert!(