Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/examples/agent_smoke.rs

6.4 KiB, 1 run

created by r2519314175:845, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! End-to-end smoke test of the agentic tool loop against a real
2//! OpenAI-compatible endpoint. Not a unit test (it needs network and
3//! an API key); run manually:
4//!
5//! ```bash
6//! RED_LLM_KEY=fw_... cargo run --example agent_smoke -p oxedyne_daimond
7//! ```
8
9use oxedyne_fe2o3_core::prelude::*;
10use oxedyne_daimond::agent::{Agent, build_tls_client_config};
11use oxedyne_daimond::executor::Executor;
12use oxedyne_daimond::llm::LlmClient;
13use oxedyne_daimond::protocol::{AgentEvent, Session};
14use oxedyne_daimond::tools::{Tool, ToolContext, ToolRegistry};
15use oxedyne_daimond::workspace::Workspace;
16
17#[tokio::main]
18async fn main() {
19 match run().await {
20 Ok(()) => println!("\n=== smoke test OK ==="),
21 Err(e) => { eprintln!("smoke test failed: {}", e); std::process::exit(1); }
22 }
23}
24
25async fn run() -> Outcome<()> {
26 let _ = rustls::crypto::ring::default_provider().install_default();
27 // Provide your own key: this smoke test talks to a real provider.
28 let key = res!(std::env::var("RED_LLM_KEY").map_err(|_| err!(
29 "set RED_LLM_KEY to your provider API key to run this smoke test";
30 Init, Missing)));
31 let tls = res!(build_tls_client_config());
32 let llm = LlmClient::new(
33 "api.fireworks.ai", 443, "/inference/v1/chat/completions",
34 &key, "accounts/fireworks/models/glm-5p2", 2048, tls,
35 );
36 let agent = Agent::new(llm, "You are Daimond, a coding agent.");
37
38 // A fresh workspace under the user cache. Not `std::env::temp_dir()`: `/tmp`
39 // is a tmpfs, so whatever the agent writes there stays resident in memory.
40 let dir = res!(oxedyne_fe2o3_test::scratch::scratch_dir("daimond_smoke"));
41 let ws = res!(Workspace::new(dir.clone()));
42 let ctx = ToolContext { workspace: ws, executor: Executor::local_default(), cwd: String::new(), path_prefix: String::new(), root: oxedyne_daimond::tools::FileRoot::Workspace, read_seen: oxedyne_daimond::tools::new_read_cache(), no_write: Vec::new(),
43 // The Diamond this turn belongs to. A smoke run belongs to none, which is
44 // the same thing an ordinary chat says.
45 daimon_of: String::new() };
46 let registry = ToolRegistry::new(Tool::defaults(), ctx);
47
48 let mut session = Session::new("smoke".to_string(), "Smoke".to_string(),
49 "accounts/fireworks/models/glm-5p2".to_string());
50
51 let prompt = "Create a file called hello.txt containing exactly the text \
52 'Hi from Daimond', then read it back and confirm its contents. \
53 Use your tools.".to_string();
54
55 println!("workspace: {:?}\nprompt: {}\n--- events ---", dir, prompt);
56 let mut on_event = |ev: AgentEvent| {
57 match ev {
58 AgentEvent::Text(t) => print!("{}", t),
59 // `id` is the provider's own call id, which pairs a call with its reply.
60 // A smoke run prints neither side by id, so it is named and dropped rather
61 // than matched with `..` -- a new field should break this again on purpose.
62 AgentEvent::ToolCall { name, args, id: _ } => println!("\n[tool_call] {} {}", name, args),
63 // The OUTCOME is printed and never inferred from the text. A smoke run
64 // whose tool was refused prints the same 200 characters as one that
65 // worked, and telling them apart by reading the prose is the defect the
66 // outcome field was added to end.
67 AgentEvent::ToolResult { name, result, outcome } => {
68 let r = if result.len() > 200 { &result[..200] } else { &result };
69 println!("[tool_result] {} {} -> {}", name, outcome.wire(), r);
70 }
71 AgentEvent::Done => println!("\n[done]"),
72 AgentEvent::Error(e) => println!("\n[error] {}", e),
73 // Printed rather than ignored: a smoke run whose turn was folded, cut
74 // short by the provider, or interrupted looks identical to one that
75 // simply answered, and the difference is the whole point of running it.
76 AgentEvent::Compacted { folded, kept, note } =>
77 println!("\n[compacted] folded {} kept {} — {}", folded, kept, note),
78 AgentEvent::Truncated => println!("\n[truncated] the provider cut the reply short"),
79 // A tool call being made again because the road went, which a smoke run has
80 // to see: a turn that spent its whole ladder into a dead page and one that
81 // answered first time otherwise print the same thing. The model is never
82 // told this -- that is the point of the mechanism -- so the only place it
83 // can be read is here.
84 AgentEvent::Roading { name, attempt, of, wait_ms } =>
85 println!("\n[roading] {} again, attempt {} of {}, after {}ms",
86 name, attempt, of, wait_ms),
87 AgentEvent::Interjected(text) => println!("\n[interjected] {}", text),
88 AgentEvent::Unseeable { images, model } =>
89 println!("\n[unseeable] {} image(s) withheld from {}", images, model),
90 // The working, not the answer. Printed whole, because a smoke run is read
91 // by a person deciding whether the turn went the way they meant.
92 AgentEvent::Thinking(text) => println!("\n[thinking] {}", text),
93 // How the turn ended, and what it did. Printed rather than dropped for the
94 // reason every other arm here is: a smoke run is read by somebody deciding
95 // whether the turn went the way they meant, and "answered with 4 calls, 1
96 // refused" is that sentence.
97 AgentEvent::Ended { how, offered, rounds, calls, refused, failed, missing } =>
98 println!("\n[ended] {} — {} tool(s) offered, {} round(s), {} call(s), \
99 {} refused, {} failed{}",
100 how, offered, rounds, calls, refused, failed,
101 if missing.is_empty() { String::new() }
102 else { format!(", missing: {}", missing.join(" ")) }),
103 }
104 };
105 res!(agent.run_turn(&mut session, prompt, &registry, &mut on_event).await);
106
107 // Verify the file exists with the expected content.
108 let created = dir.join("hello.txt");
109 match std::fs::read_to_string(&created) {
110 Ok(c) => println!("\n--- hello.txt on disk: {:?}", c.trim()),
111 Err(e) => return Err(err!(e, "hello.txt was not created."; Test, File)),
112 }
113 println!("session tokens: prompt={} completion={}",
114 session.prompt_tokens, session.completion_tokens);
115 Ok(())
116}