feat(#665): harness-internal async bash task runner (option B)
This commit is contained in:
parent
9bd37d4df3
commit
1178bb2999
5 changed files with 512 additions and 3 deletions
|
|
@ -410,6 +410,60 @@ pub struct RecvArgs {
|
|||
pub max: Option<u32>,
|
||||
}
|
||||
|
||||
/// MCP tool args for `bash_run`.
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct BashRunArgs {
|
||||
/// Shell command to run (passed to `sh -c`).
|
||||
pub cmd: String,
|
||||
/// Timeout in seconds. Defaults to 180. Task is killed and marked
|
||||
/// `timed_out` when the limit is exceeded.
|
||||
#[serde(default)]
|
||||
pub timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
/// MCP tool args for `bash_status`.
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct BashStatusArgs {
|
||||
/// Task ID returned by `bash_run`.
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
/// Format the result of `bash_status` from a task ID.
|
||||
#[must_use]
|
||||
fn format_bash_status(id: &str) -> String {
|
||||
use std::fmt::Write as _;
|
||||
let Some(task) = crate::bash_runner::read_task(id) else {
|
||||
return format!("bash_status: unknown task id `{id}`");
|
||||
};
|
||||
let mut out = format!(
|
||||
"task `{id}`: status={status:?}",
|
||||
status = task.status
|
||||
);
|
||||
if let Some(code) = task.exit_code {
|
||||
let _ = write!(out, ", exit={code}");
|
||||
}
|
||||
if let Some(t) = task.started_at {
|
||||
let age = crate::serve_common::now_unix() - t;
|
||||
let _ = write!(out, ", running for {age}s");
|
||||
}
|
||||
if let Some(t) = task.completed_at {
|
||||
if let Some(s) = task.started_at {
|
||||
let _ = write!(out, ", took {}s", t - s);
|
||||
}
|
||||
}
|
||||
if let Some(ref stdout) = task.stdout_tail {
|
||||
if !stdout.trim().is_empty() {
|
||||
let _ = write!(out, "\n\nstdout:\n```\n{}\n```", stdout.trim());
|
||||
}
|
||||
}
|
||||
if let Some(ref stderr) = task.stderr_tail {
|
||||
if !stderr.trim().is_empty() {
|
||||
let _ = write!(out, "\n\nstderr:\n```\n{}\n```", stderr.trim());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// MCP tool args for `remind`. Exactly one of `delay_seconds` or
|
||||
/// `at_unix_timestamp` must be set; both / neither is a tool-side error.
|
||||
/// Hides the tagged `ReminderTiming` enum behind a flatter schema so the
|
||||
|
|
@ -732,6 +786,38 @@ impl AgentServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Run a shell command in the background. Returns a task ID immediately — \
|
||||
do NOT wait inline. When the command finishes, the harness fires a wake with \
|
||||
`from: \"bash-task-<id>\"` and the exit code + last stdout lines in the body; \
|
||||
handle it on a future turn. Use `bash_status` to poll the task status within \
|
||||
the same turn if needed. `timeout_secs` defaults to 180."
|
||||
)]
|
||||
async fn bash_run(&self, Parameters(args): Parameters<BashRunArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("bash_run", log, async move {
|
||||
match crate::bash_runner::submit_task(args.cmd, args.timeout_secs) {
|
||||
Ok(id) => format!("task started: id={id}"),
|
||||
Err(e) => format!("bash_run failed: {e:#}"),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Check the status of a background bash task by its ID (from `bash_run`). \
|
||||
Returns the current status (pending/running/done/timed_out/interrupted), exit code \
|
||||
if finished, and a tail of stdout/stderr. Full output lives in \
|
||||
`harness/bash-tasks/<id>.out` / `.err`."
|
||||
)]
|
||||
async fn bash_status(&self, Parameters(args): Parameters<BashStatusArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("bash_status", log, async move {
|
||||
format_bash_status(&args.id)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Ask the harness to start another turn immediately after this one \
|
||||
completes, even if the inbox is empty. Use this when you have ongoing work that \
|
||||
|
|
@ -1740,8 +1826,8 @@ const TOOL_GROUPS_ENV: &str = "HIVE_TOOL_GROUPS";
|
|||
/// Reads `HIVE_TOOL_GROUPS` from the environment first. Each comma-separated
|
||||
/// token is matched (case-insensitive) against the `ToolGroup` serde names
|
||||
/// (`messaging`, `meta`, `inbox`, `lifecycle`, `approvals`, `scheduling`,
|
||||
/// `diagnostics`). Unrecognised tokens are logged and skipped. Falls back to
|
||||
/// the flavor default when the env var is absent or empty.
|
||||
/// `diagnostics`, `execution`). Unrecognised tokens are logged and skipped.
|
||||
/// Falls back to the flavor default when the env var is absent or empty.
|
||||
fn effective_tool_groups(flavor: Flavor) -> Vec<hive_sh4re::ToolGroup> {
|
||||
let raw = match std::env::var(TOOL_GROUPS_ENV) {
|
||||
Ok(v) if !v.trim().is_empty() => v,
|
||||
|
|
|
|||
Loading…
Reference in a new issue