use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use agentplane::case::TaskStore;
use agentplane::core::{
CorrelationKey, DeadlineSpec, Decision, Justification, Operator, ProtectedField, SourceId,
TaskSpec,
};
use agentplane::manifest::{Budgets, Manifest, ModelRef, ToolGrant};
use agentplane::model::ModelProvider;
use agentplane::model::fake::FakeProvider;
use agentplane::prelude::*;
use agentplane::runtime::{Agent, RunTerms};
use agentplane::tools::{Tool, ToolFailure};
use serde_json::{Value, json};
static WRITTEN: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct WriteFile {
path: String,
contents: String,
}
#[async_trait]
impl Tool for WriteFile {
const SERVER: &'static str = "fs";
const NAME: &'static str = "write";
async fn call(self) -> Result<Value, ToolFailure> {
WRITTEN.fetch_add(1, Ordering::SeqCst);
println!(
" → wrote {} bytes to {}",
self.contents.len(),
self.path
);
Ok(json!({ "path": self.path, "bytes": self.contents.len() }))
}
}
#[derive(Debug)]
struct Patch;
#[async_trait]
impl Skill for Patch {
fn descriptor(&self) -> SkillDescriptor {
SkillDescriptor::new("patch").provides("code.patch")
}
async fn invoke(
&self,
cx: &mut StepCtx<'_>,
input: Tainted<Value>,
) -> Result<Outcome, SkillError> {
let answer = cx.complete(&input).await?;
let call = answer.map(|c| serde_json::from_str::<Value>(&c.text).unwrap_or(Value::Null));
cx.deadline("review", &DeadlineSpec::hours(4), None).await?;
let shown = Justification::new(
Tainted::trusted("write the file the model proposed".to_owned()),
json!({ "tool": "tool://fs/write", "arguments": call.peek() }),
);
let task = TaskSpec::new("patch-review", shown, "review").role("maintainer");
if !cx.task(&task).await?.approved {
return Ok(Outcome::done(Tainted::trusted(json!({ "written": false }))));
}
let written = cx.call_tool(ToolId::new("fs", "write"), call).await?;
Ok(Outcome::done(
written.map(|v| json!({ "written": true, "file": v })),
))
}
}
fn declaration() -> Result<Manifest, agentplane::manifest::ManifestError> {
let model = SourceId::new("model:fake/coder-1");
Manifest::builder("patcher", "1.0.0")
.provides("code.patch")
.privileged_model(ModelRef::new("fake", "coder-1"))
.egress_ceiling(Sensitivity::Internal)
.tool(ToolGrant {
description: Some("Write a file in the working tree.".to_owned()),
max_sensitivity: Some(Sensitivity::Internal),
protected_fields: vec![
ProtectedField::from_sources("/path", [model.clone()]),
ProtectedField::from_sources("/contents", [model]),
],
..ToolGrant::new("tool://fs/write")
})
.budgets(Budgets {
max_tokens: Some(100_000),
..Budgets::default()
})
.build()
}
fn maintainers() -> Vec<String> {
vec!["maintainer".to_owned()]
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = std::env::temp_dir().join(format!("agentplane-local-{}", std::process::id()));
std::fs::create_dir_all(&dir)?;
let store = Arc::new(RedbStore::open(dir.join("journal.redb"))?);
let provider = FakeProvider::new();
provider.will_say(r#"{ "path": "src/lib.rs", "contents": "pub fn answer() -> u8 { 42 }\n" }"#);
let driver: Arc<dyn ModelProvider> = provider.clone();
let manifest = declaration()?;
let rt = Runtime::builder_on(Arc::clone(&store))
.provider("fake", driver)
.agent(Agent::new(&manifest).skill(Patch))
.toolbox(ToolBox::new().with::<WriteFile>())
.build();
let run = rt
.start(
"code.patch",
Tainted::trusted(json!("make `answer` return 42")),
)
.terms(RunTerms::default().correlated("feature", &[CorrelationKey::new("feature", "F-1")]))
.run()
.await?;
println!("1. the model proposes a write, and the run waits");
println!(" run → {}", run.status.as_str());
assert!(matches!(run.status, RunStatus::Suspended(_)));
assert_eq!(
WRITTEN.load(Ordering::SeqCst),
0,
"nothing is written before the decision"
);
let task = store
.queue(&maintainers(), 10)
.await?
.pop()
.expect("the review is queued");
let shown = &task.justification.proposed_action;
println!(
" shown → {} {}",
shown["tool"], shown["arguments"]["path"]
);
assert_eq!(shown["arguments"]["path"], "src/lib.rs");
println!("\n2. the maintainer approves");
rt.decide_task(
task.id,
&Decision::approve(
Operator::asserted("sam")?,
"the patch is what was asked for",
),
&maintainers(),
)
.await?;
let done = rt
.recorded_outcome(run.run_id)
.await?
.expect("the run concluded");
println!(" run → {}", done.status.as_str());
assert!(
matches!(done.status, RunStatus::Succeeded),
"{:?}",
done.status
);
assert_eq!(WRITTEN.load(Ordering::SeqCst), 1);
let replayed = rt
.replay(run.run_id, agentplane::runtime::Mode::Strict)
.await?;
println!("\n3. strict replay");
println!(" run → {}", replayed.status.as_str());
assert!(matches!(replayed.status, RunStatus::Succeeded));
assert_eq!(WRITTEN.load(Ordering::SeqCst), 1, "replay wrote nothing");
drop(rt);
drop(store);
std::fs::remove_dir_all(&dir)?;
Ok(())
}