use std::io::Write as _;
use std::sync::{Arc, Mutex};
use agentplane::core::{Outcome, Skill, SkillDescriptor, SkillError, Tainted, Trust};
use agentplane::journal::JournalStore;
use agentplane::model::{ModelCall, ModelId, ModelProvider, ModelStreamEvent, ModelStreamObserver};
use agentplane::runtime::{Mode, Runtime, StepCtx};
use agentplane::store::RedbStore;
use agentplane::testkit::FakeProvider;
use serde_json::{Value, json};
#[derive(Debug, Default)]
struct Printer {
buffered: Mutex<String>,
calls: Mutex<usize>,
}
impl ModelStreamObserver for Printer {
fn event(&self, event: Tainted<ModelStreamEvent>) {
*self.calls.lock().expect("not poisoned") += 1;
assert_eq!(event.label().trust, Trust::Untrusted);
match event.peek() {
ModelStreamEvent::TextDelta(delta) => {
print!("{delta}");
let _ = std::io::stdout().flush();
self.buffered.lock().expect("not poisoned").push_str(delta);
}
ModelStreamEvent::Usage(usage) => {
println!(
"\n [usage: {} in, {} out]",
usage.input_tokens, usage.output_tokens
);
}
}
}
}
#[derive(Debug)]
struct Answers {
provider: Arc<FakeProvider>,
printer: Arc<Printer>,
}
#[async_trait::async_trait]
impl Skill for Answers {
fn descriptor(&self) -> SkillDescriptor {
SkillDescriptor::new("desk.answer").provides("desk.answer")
}
async fn invoke(
&self,
cx: &mut StepCtx<'_>,
input: Tainted<Value>,
) -> Result<Outcome, SkillError> {
let call = ModelCall::new(
Arc::clone(&self.provider) as Arc<dyn ModelProvider>,
ModelId::new("fake", "scribe-1"),
input.peek().clone(),
)
.streaming_to(Arc::clone(&self.printer) as Arc<dyn ModelStreamObserver>);
let answer = cx.sink(call, &input).await?;
Ok(Outcome::done(answer.map(|c| json!({ "answer": c.text }))))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
const ANSWER: &str = "Settlement GB-4471 clears on Thursday, once the counterparty confirms.";
let provider = FakeProvider::new();
provider.streaming().will_say(ANSWER);
let printer = Arc::new(Printer::default());
let store: Arc<dyn JournalStore> = Arc::new(RedbStore::open_in_memory()?);
let runtime = Runtime::builder(Arc::clone(&store))
.skill(Answers {
provider: Arc::clone(&provider),
printer: Arc::clone(&printer),
})
.build();
println!("1. live run — deltas as they arrive\n");
print!(" ");
let outcome = runtime
.run(
"desk.answer",
Tainted::trusted(json!({"q": "when does GB-4471 clear?"})),
)
.await?;
let streamed = printer.buffered.lock().expect("not poisoned").clone();
let live_calls = *printer.calls.lock().expect("not poisoned");
println!("\n observer calls: {live_calls}");
println!(
" deltas concatenate to the completion, byte for byte: {}",
streamed == ANSWER
);
let records = store.read(outcome.run_id, 1).await?;
let effects = records
.iter()
.filter(|r| format!("{:?}", r.body.kind).contains("Effect"))
.count();
println!("\n2. the journal");
println!(" records: {}, of which effects: {effects}", records.len());
println!(" the deltas are not among them — a partial answer is not evidence");
let before = provider.calls();
let replayed = runtime.replay(outcome.run_id, Mode::Strict).await?;
let after_replay = *printer.calls.lock().expect("not poisoned");
println!("\n3. strict replay");
println!(" status: {:?}", replayed.status);
println!(
" model calls: {} (unchanged: {})",
provider.calls(),
provider.calls() == before
);
println!(
" observer calls: {after_replay} (unchanged: {})",
after_replay == live_calls
);
println!(
"\n Replay is not a rerun. The provider was not performed, so there was\n \
nothing to observe — and the answer came back from the journal anyway."
);
Ok(())
}