diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index 406e661d..1b4c4705 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -153,8 +153,24 @@ async fn serve( }); let prompt = format_wake_prompt(&label, &from, &body); let outcome = - drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Agent).await; - emit_turn_end(&bus, &outcome); + turn::run_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Agent).await; + match outcome { + Ok(()) => { + bus.emit(LiveEvent::TurnEnd { + ok: true, + note: None, + }); + tracing::info!("claude turn finished"); + } + Err(e) => { + let note = format!("{e:#}"); + bus.emit(LiveEvent::TurnEnd { + ok: false, + note: Some(note.clone()), + }); + tracing::warn!(error = %note, "claude turn failed"); + } + } } Ok(AgentResponse::Empty) => {} Ok(AgentResponse::Ok | AgentResponse::Status { .. }) => { @@ -171,47 +187,6 @@ async fn serve( } } -/// Drive one turn end-to-end. If claude hits `Prompt is too long`, run -/// `/compact` against the persistent session and retry once. Returns the -/// final `TurnOutcome` to drive the `TurnEnd` live event. -async fn drive_turn( - prompt: &str, - mcp_config: &Path, - bus: &Bus, - flavor: mcp::Flavor, -) -> turn::TurnOutcome { - match turn::run_turn(prompt, mcp_config, bus, flavor).await { - turn::TurnOutcome::PromptTooLong => { - if let Err(e) = turn::compact_session(bus).await { - tracing::warn!(error = %format!("{e:#}"), "compact failed"); - return turn::TurnOutcome::Failed(e); - } - turn::run_turn(prompt, mcp_config, bus, flavor).await - } - other => other, - } -} - -fn emit_turn_end(bus: &Bus, outcome: &turn::TurnOutcome) { - match outcome { - turn::TurnOutcome::Ok | turn::TurnOutcome::PromptTooLong => { - bus.emit(LiveEvent::TurnEnd { - ok: true, - note: None, - }); - tracing::info!("claude turn finished"); - } - turn::TurnOutcome::Failed(e) => { - let note = format!("{e:#}"); - bus.emit(LiveEvent::TurnEnd { - ok: false, - note: Some(note.clone()), - }); - tracing::warn!(error = %note, "claude turn failed"); - } - } -} - /// System prompt handed to claude on each turn. The harness has already /// popped one message off the inbox (the wake signal); claude is told /// about it and the MCP tools, and is expected to drive any further diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index eb2035a3..abaae900 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -175,8 +175,24 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { }); let prompt = format_wake_prompt(&label, &from, &body); let outcome = - drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Manager).await; - emit_turn_end(&bus, &outcome); + turn::run_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Manager).await; + match outcome { + Ok(()) => { + bus.emit(LiveEvent::TurnEnd { + ok: true, + note: None, + }); + tracing::info!("manager turn finished"); + } + Err(e) => { + let note = format!("{e:#}"); + bus.emit(LiveEvent::TurnEnd { + ok: false, + note: Some(note.clone()), + }); + tracing::warn!(error = %note, "manager turn failed"); + } + } } Ok(ManagerResponse::Empty) => {} Ok(ManagerResponse::Ok | ManagerResponse::Status { .. }) => { @@ -193,46 +209,6 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> { } } -/// Drive one manager turn end-to-end with the same overflow-then-compact -/// retry as sub-agents. -async fn drive_turn( - prompt: &str, - mcp_config: &Path, - bus: &Bus, - flavor: mcp::Flavor, -) -> turn::TurnOutcome { - match turn::run_turn(prompt, mcp_config, bus, flavor).await { - turn::TurnOutcome::PromptTooLong => { - if let Err(e) = turn::compact_session(bus).await { - tracing::warn!(error = %format!("{e:#}"), "compact failed"); - return turn::TurnOutcome::Failed(e); - } - turn::run_turn(prompt, mcp_config, bus, flavor).await - } - other => other, - } -} - -fn emit_turn_end(bus: &Bus, outcome: &turn::TurnOutcome) { - match outcome { - turn::TurnOutcome::Ok | turn::TurnOutcome::PromptTooLong => { - bus.emit(LiveEvent::TurnEnd { - ok: true, - note: None, - }); - tracing::info!("manager turn finished"); - } - turn::TurnOutcome::Failed(e) => { - let note = format!("{e:#}"); - bus.emit(LiveEvent::TurnEnd { - ok: false, - note: Some(note.clone()), - }); - tracing::warn!(error = %note, "manager turn failed"); - } - } -} - /// Manager-flavored wake prompt. Mentions the privileged tools the sub-agent /// prompt doesn't have access to, and points the manager at its own /// editable config repo for self-modification. diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 5af016b9..8943fd6f 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -5,8 +5,6 @@ use std::path::{Path, PathBuf}; use std::process::Stdio; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; use anyhow::{Result, bail}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; @@ -15,21 +13,6 @@ use tokio::process::Command; use crate::events::{Bus, LiveEvent}; use crate::mcp; -/// Inline `--settings` JSON applied to every claude invocation. We turn off -/// claude's in-session auto-compaction and its cross-session auto-memory -/// because hyperhive owns those concerns: compaction is operator/harness- -/// driven (`/compact` on overflow), notes persistence is a hyperhive -/// concern (planned, not yet wired). Unknown keys are silently ignored by -/// claude-code; if the key names ever rename, we'll spot it because -/// auto-compact will start firing mid-turn again. -const CLAUDE_SETTINGS: &str = r#"{"autoCompactEnabled":false,"autoMemoryEnabled":false}"#; - -/// Regex-ish marker claude-code emits when context overflows. Same string -/// bitburner-agent watches for. Empirically reliable across claude-code -/// versions; if it ever changes, compaction won't fire and we'll see a -/// claude exit with a useful error in the live view. -const PROMPT_TOO_LONG_MARKER: &str = "Prompt is too long"; - /// Drop the MCP config blob claude reads from `--mcp-config `. /// `socket` is the hyperhive per-container socket (forwarded to the child /// as `--socket `); `binary_subcommand` is e.g. `"mcp"` for sub-agents @@ -48,88 +31,30 @@ pub async fn write_mcp_config(socket: &Path) -> Result { Ok(path) } -/// One claude turn's outcome. The harness uses this to decide whether to -/// transparently kick off a compaction and retry. -#[derive(Debug)] -pub enum TurnOutcome { - Ok, - /// claude saw "Prompt is too long" — the session needs compacting. - /// Run `compact_session()` then retry the same wake-up prompt. - PromptTooLong, - Failed(anyhow::Error), -} - /// Spawn `claude` for one turn and pump `stream-json` stdout into the /// live event bus. Prompt goes over stdin (variadic /// `--allowedTools`/`--tools` would otherwise eat a trailing positional -/// prompt). The session is persistent across turns via `--continue` and -/// claude's in-session auto-compact is disabled via `--settings` so it -/// doesn't stall mid-turn — hyperhive owns compaction. +/// prompt). On non-zero exit returns an error; the caller emits the +/// `TurnEnd` event. pub async fn run_turn( prompt: &str, mcp_config: &Path, bus: &Bus, flavor: mcp::Flavor, -) -> TurnOutcome { - match run_claude(prompt, mcp_config, bus, flavor, ClaudeMode::Turn).await { - Ok(too_long) if too_long => TurnOutcome::PromptTooLong, - Ok(_) => TurnOutcome::Ok, - Err(e) => TurnOutcome::Failed(e), - } -} - -/// Run claude's built-in `/compact` slash command on the persistent -/// session so the next turn can fit. No MCP tools needed; we just feed -/// `/compact` over stdin and let claude rewrite its own history. -pub async fn compact_session(bus: &Bus) -> Result<()> { - bus.emit(LiveEvent::Note( - "context overflow — running /compact on the persistent session".into(), - )); - let _ = run_claude( - "/compact", - Path::new("/dev/null"), - bus, - mcp::Flavor::Agent, // tool surface unused for /compact - ClaudeMode::Compact, - ) - .await?; - bus.emit(LiveEvent::Note("/compact done".into())); - Ok(()) -} - -#[derive(Clone, Copy)] -enum ClaudeMode { - Turn, - Compact, -} - -async fn run_claude( - prompt: &str, - mcp_config: &Path, - bus: &Bus, - flavor: mcp::Flavor, - mode: ClaudeMode, -) -> Result { - let mut cmd = Command::new("claude"); - cmd.arg("--print") +) -> Result<()> { + let mut child = Command::new("claude") + .arg("--print") .arg("--verbose") .arg("--output-format") .arg("stream-json") .arg("--model") .arg("haiku") - .arg("--continue") - .arg("--settings") - .arg(CLAUDE_SETTINGS); - if let ClaudeMode::Turn = mode { - cmd.arg("--mcp-config") - .arg(mcp_config) - .arg("--strict-mcp-config") - .arg("--tools") - .arg(mcp::builtin_tools_arg()) - .arg("--allowedTools") - .arg(mcp::allowed_tools_arg(flavor)); - } - let mut child = cmd + .arg("--mcp-config") + .arg(mcp_config) + .arg("--tools") + .arg(mcp::builtin_tools_arg()) + .arg("--allowedTools") + .arg(mcp::allowed_tools_arg(flavor)) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -143,17 +68,11 @@ async fn run_claude( let stdout = child.stdout.take().expect("piped stdout"); let stderr = child.stderr.take().expect("piped stderr"); - let prompt_too_long = Arc::new(AtomicBool::new(false)); - let flag_out = prompt_too_long.clone(); - let flag_err = prompt_too_long.clone(); let bus_out = bus.clone(); let bus_err = bus.clone(); let pump_stdout = tokio::spawn(async move { let mut reader = BufReader::new(stdout).lines(); while let Ok(Some(line)) = reader.next_line().await { - if line.contains(PROMPT_TOO_LONG_MARKER) { - flag_out.store(true, Ordering::Relaxed); - } match serde_json::from_str::(&line) { Ok(v) => bus_out.emit(LiveEvent::Stream(v)), Err(_) => bus_out.emit(LiveEvent::Note(format!("(non-json) {line}"))), @@ -163,9 +82,6 @@ async fn run_claude( let pump_stderr = tokio::spawn(async move { let mut reader = BufReader::new(stderr).lines(); while let Ok(Some(line)) = reader.next_line().await { - if line.contains(PROMPT_TOO_LONG_MARKER) { - flag_err.store(true, Ordering::Relaxed); - } bus_err.emit(LiveEvent::Note(format!("stderr: {line}"))); } }); @@ -173,9 +89,8 @@ async fn run_claude( let status = child.wait().await?; let _ = pump_stdout.await; let _ = pump_stderr.await; - let too_long = prompt_too_long.load(Ordering::Relaxed); - if !status.success() && !too_long { + if !status.success() { bail!("claude exited {status}"); } - Ok(too_long) + Ok(()) } diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index c09e929e..3f16df27 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -114,109 +114,81 @@ fn render_online(label: &str) -> String { /// reload, so the login flow and other forms aren't clobbered. const LIVE_PANEL: &str = r#"

live

-
connecting…
+
connecting…
@@ -465,46 +437,10 @@ const STYLE: &str = r#" word-break: break-all; max-height: 30em; } - .live { - background: rgba(255, 255, 255, 0.02); - border: 1px solid var(--purple-dim); - padding: 0.4em 0.6em; - overflow-y: auto; - max-height: 32em; - font-family: inherit; - } - .live .row { - white-space: pre-wrap; - word-break: break-word; - padding: 0.05em 0; - line-height: 1.45; - border-left: 2px solid transparent; - padding-left: 0.5em; - margin: 0.1em 0; - } - .live .row + .row { border-top: 0; } - .live .turn-start { - color: var(--amber); - font-weight: bold; - margin-top: 1em; - border-left-color: var(--amber); - padding-top: 0.3em; - } - .live .turn-start:first-child { margin-top: 0; } - .live .turn-body { - color: var(--fg); - font-weight: normal; - margin-top: 0.15em; - padding-left: 1.2em; - opacity: 0.85; - } - .live .turn-end-ok { color: #66ff99; border-left-color: #66ff99; margin-bottom: 0.4em; } - .live .turn-end-fail { color: #ff6b6b; border-left-color: #ff6b6b; margin-bottom: 0.4em; } - .live .text { color: var(--fg); padding-left: 1.2em; } - .live .thinking { color: var(--muted); font-style: italic; padding-left: 1.2em; } - .live .tool-use { color: #66e0ff; padding-left: 1.2em; } - .live .tool-result { color: var(--muted); padding-left: 1.2em; } - .live .result { color: var(--green); padding-left: 0.5em; } - .live .sys, .live .note { color: var(--muted); } + #live { max-height: 24em; overflow-y: auto; } + #live span { display: block; } + #live .turnstart { color: var(--amber); } + #live .turnok { color: var(--green); } + #live .turnfail { color: #ff6b6b; } "#;