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 | |
| 9 | use oxedyne_fe2o3_core::prelude::*; |
| 10 | use oxedyne_daimond::agent::{Agent, build_tls_client_config}; |
| 11 | use oxedyne_daimond::executor::Executor; |
| 12 | use oxedyne_daimond::llm::LlmClient; |
| 13 | use oxedyne_daimond::protocol::{AgentEvent, Session}; |
| 14 | use oxedyne_daimond::tools::{Tool, ToolContext, ToolRegistry}; |
| 15 | use oxedyne_daimond::workspace::Workspace; |
| 16 | |
| 17 | #[tokio::main] |
| 18 | async 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 | |
| 25 | async 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, ®istry, &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 | } |