390
name: "command_output",
391
description: "Get the output a background task (started with run_command's \
392
run_in_background) has produced since your last check, plus its \
393
- status: still running, or exited with an exit code. Poll this \
394
- periodically to follow builds, test suites, and servers. Between \
395
- polls output is buffered up to 50 000 bytes per task; older \
393
+ status: still running, or exited with an exit code. By default the \
394
+ call waits up to 10 seconds for new output or completion, so call it \
395
+ again when the task is still running rather than ending the turn. \
396
+ Between polls output is buffered up to 50 000 bytes per task; older \
397
output beyond that is dropped and the truncation is noted.",
398
parameters_schema: json!({
399
"type": "object",
401
"task_id": {
402
"type": "integer",
403
"description": "Id of the background task, as returned by run_command with run_in_background."
404
+ },
405
+ "wait_seconds": {
406
+ "type": "integer",
407
+ "minimum": 0,
408
+ "maximum": 30,
409
+ "description": "Seconds to wait for new output or task completion before returning. Defaults to 10; use 0 for an immediate status check."
410
}
411
},
412
"required": ["task_id"],
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,
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,
523
"remember" => exec_remember(arguments),
524
"delete_file" => exec_delete_file(arguments),
525
"run_command" => exec_run_command(arguments, owner),
518
- "command_output" => exec_command_output(arguments, owner),
526
"kill_command" => exec_kill_command(arguments, owner),
527
"skill" => crate::skills::activate_skill(arguments),
528
_ => format!("Unknown tool: {name}"),
2533
// the final output through the pipes before draining the buffer.
2534
std::thread::sleep(std::time::Duration::from_millis(100));
2535
}
2536
+
2537
let (new_output, dropped) = drain_task_output(task);
2538
2539
let command = &task.command;
2556
}
2557
}
2558
2559
+/// Long-poll a background task without occupying Tokio's blocking pool while
2560
+/// nothing changes. This keeps model tool rounds from being consumed by a
2561
+/// tight status-check loop.
2562
+async fn exec_command_output_wait(arguments: &str) -> String {
2563
+ const DEFAULT_WAIT_SECONDS: u64 = 10;
2564
+ const MAX_WAIT_SECONDS: u64 = 30;
2565
+ const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(200);
2566
+
2567
+ let wait_seconds = serde_json::from_str::<Value>(arguments)
2568
+ .ok()
2569
+ .and_then(|args| args.get("wait_seconds").and_then(Value::as_u64))
2570
+ .unwrap_or(DEFAULT_WAIT_SECONDS)
2571
+ .min(MAX_WAIT_SECONDS);
2572
+ let owner = active_session();
2573
+ let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(wait_seconds);
2574
+
2575
+ loop {
2576
+ let arguments = arguments.to_owned();
2577
+ let owner = owner.clone();
2578
+ let output =
2579
+ tokio::task::spawn_blocking(move || exec_command_output(&arguments, owner.as_deref()))
2580
+ .await
2581
+ .unwrap_or_else(|err| format!("Error: command_output task failed: {err}"));
2582
+
2583
+ if wait_seconds == 0
2584
+ || !output.contains(" is still running.")
2585
+ || !output.contains(" No new output since the last check.")
2586
+ || tokio::time::Instant::now() >= deadline
2587
+ {
2588
+ return output;
2589
+ }
2590
+
2591
+ tokio::time::sleep(POLL_INTERVAL).await;
2592
+ }
2593
+}
2594
+
2595
/// `kill_command` tool: stop a background task and report its output tail.
2596
fn exec_kill_command(arguments: &str, owner: Option<&str>) -> String {
2597
let task_id = match parse_task_id(arguments) {
3827
assert!(combined.contains("done"), "got: {combined}");
3828
}
3829
3830
+ #[tokio::test]
3831
+ async fn test_command_output_waits_for_background_completion() {
3832
+ #[cfg(unix)]
3833
+ let command = "sleep 1; echo done";
3834
+ #[cfg(windows)]
3835
+ let command = "ping -n 2 127.0.0.1 > nul&& echo done";
3836
+
3837
+ let args = serde_json::json!({
3838
+ "command": command,
3839
+ "cwd": std::env::temp_dir(),
3840
+ "run_in_background": true
3841
+ })
3842
+ .to_string();
3843
+ let result = exec_run_command(&args, None);
3844
+ let task_id = background_task_id(&result);
3845
+
3846
+ let poll_args = serde_json::json!({
3847
+ "task_id": task_id,
3848
+ "wait_seconds": 5
3849
+ })
3850
+ .to_string();
3851
+ let poll = exec_command_output_wait(&poll_args).await;
3852
+
3853
+ assert!(poll.contains("exited with code 0"), "got: {poll}");
3854
+ assert!(poll.contains("done"), "got: {poll}");
3855
+ }
3856
+
3857
#[test]
3858
fn test_kill_command_stops_background_task() {
3859
#[cfg(unix)]