harness-core now has everything the headless agent loop needs:
- Tool trait/ToolCtx/ToolRegistry + 30k-char head+tail output truncation
- PermissionService: async ask over a oneshot + AppEvent::PermissionAsked,
Once/Always/Reject replies, an auto-approve stub for tests/headless runs
- Config: JSONC loading, bundled/global/project-chain/env precedence,
{env:VAR} and {file:path} interpolation
- llm.rs: LlmEvent/LlmRequest/Provider trait, wire message/content types
- engine/: outer loop (run_session), inner stream processor (persists
parts/messages as events arrive, executes tool calls inline), retry
policy (retries only the pre-first-event window), doom-loop guard,
system prompt assembly
Verified end-to-end against a scripted MockProvider: text -> tool call
(read) -> final text, with messages/parts persisted in the right shape,
plus a provider-error-before-any-event case surfacing as Errored (no
partial message left behind). 49 tests passing, clippy clean.
57 lines
1.8 KiB
Rust
57 lines
1.8 KiB
Rust
use std::path::Path;
|
|
|
|
/// cwd, platform, date, best-effort git status. Kept as its own block (not merged into the
|
|
/// agent prompt) because Anthropic cache breakpoints need the system prompt structured as
|
|
/// separate blocks (see `LlmRequest.system: Vec<String>`).
|
|
pub fn env_header(cwd: &Path) -> String {
|
|
let git_status = std::process::Command::new("git")
|
|
.arg("status")
|
|
.arg("--short")
|
|
.current_dir(cwd)
|
|
.output()
|
|
.ok()
|
|
.filter(|o| o.status.success())
|
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
|
.filter(|s| !s.is_empty());
|
|
|
|
let mut header = format!(
|
|
"Working directory: {}\nPlatform: {}\n",
|
|
cwd.display(),
|
|
std::env::consts::OS
|
|
);
|
|
if let Some(status) = git_status {
|
|
header.push_str(&format!("Git status:\n{status}\n"));
|
|
}
|
|
header
|
|
}
|
|
|
|
/// Ordered system prompt blocks: environment header, agent prompt, then project
|
|
/// instructions (e.g. AGENTS.md contents). Order matches `02-engine.md`.
|
|
pub fn assemble(env_header: String, agent_prompt: &str, instructions: &[String]) -> Vec<String> {
|
|
let mut blocks = vec![env_header, agent_prompt.to_string()];
|
|
blocks.extend(instructions.iter().cloned());
|
|
blocks
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn env_header_includes_cwd_and_platform() {
|
|
let header = env_header(Path::new("."));
|
|
assert!(header.contains("Working directory:"));
|
|
assert!(header.contains(std::env::consts::OS));
|
|
}
|
|
|
|
#[test]
|
|
fn assemble_orders_env_agent_then_instructions() {
|
|
let blocks = assemble(
|
|
"ENV".to_string(),
|
|
"AGENT",
|
|
&["AGENTS.md contents".to_string()],
|
|
);
|
|
assert_eq!(blocks, vec!["ENV", "AGENT", "AGENTS.md contents"]);
|
|
}
|
|
}
|