From da88d7bc464dd36e6c3a96ed04475ea73834d528 Mon Sep 17 00:00:00 2001 From: MathisWellmann Date: Mon, 31 Aug 2026 03:03:08 +0200 Subject: [PATCH 1/4] set thinking level to medium in `fractal-studio-example` --- examples/fractal-studio/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/fractal-studio/src/main.rs b/examples/fractal-studio/src/main.rs index bcdc441..9dc3e4f 100644 --- a/examples/fractal-studio/src/main.rs +++ b/examples/fractal-studio/src/main.rs @@ -48,6 +48,7 @@ use rayon::prelude::*; use symbiont::{ DocMode, Runtime, + ThinkingLevel, }; use tracing::{ info, @@ -770,7 +771,7 @@ fn main() -> eframe::Result<()> { None, DocMode::default(), &model, - false, + ThinkingLevel::Medium, )) .expect("can initialize the agent; check the API_KEY and BASE_URL env vars"); let tokio_handle = tokio_rt.handle().clone(); From 5b14db8cd76e57fa4125ac0ac1c75f9ff2c83dbd Mon Sep 17 00:00:00 2001 From: MathisWellmann Date: Mon, 31 Aug 2026 17:12:25 +0200 Subject: [PATCH 2/4] return error when no crate_name is set but `DocMode` uses tools. --- symbiont/src/error.rs | 3 +++ symbiont/src/inference/agent_builder.rs | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/symbiont/src/error.rs b/symbiont/src/error.rs index 05707b2..ecfd709 100644 --- a/symbiont/src/error.rs +++ b/symbiont/src/error.rs @@ -90,6 +90,9 @@ pub enum Error { #[error(transparent)] Fmt(#[from] std::fmt::Error), + + #[error("A DocMode with tools requires an `opt_crate_name` to be set.")] + InvalidDocMode, } /// Result type alias for symbiont operations. diff --git a/symbiont/src/inference/agent_builder.rs b/symbiont/src/inference/agent_builder.rs index bee837b..5020799 100644 --- a/symbiont/src/inference/agent_builder.rs +++ b/symbiont/src/inference/agent_builder.rs @@ -26,6 +26,7 @@ use crate::{ ApiIndexTool, DocIndex, DocMode, + Error, MeteredHttpClient, Result, ThinkingLevel, @@ -171,6 +172,7 @@ pub async fn agent_builder( .tool(ApiDocTool::new(index)) .default_max_turns(DOC_TOOLS_MAX_TURNS) } + (None, true) => return Err(Error::InvalidDocMode), _ => builder.dynamic_tools(Vec::new()), }; Ok(builder) @@ -239,7 +241,7 @@ mod tests { async fn init_agent_carries_the_base_url_as_provider() { let agent = init_agent( None, - DocMode::default(), + DocMode::Inline, "http://127.0.0.1:8321/v1", "", "model", From 4d87e3bcbe68e2d748dcc6b46c430ec90229f2d4 Mon Sep 17 00:00:00 2001 From: MathisWellmann Date: Mon, 31 Aug 2026 17:27:34 +0200 Subject: [PATCH 3/4] change `DocMode::default` to `Inline` variant as now the `agent_builder` returns an error if no crate name is given --- symbiont/src/inference/agent_builder.rs | 2 +- symbiont/src/system_prompt.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/symbiont/src/inference/agent_builder.rs b/symbiont/src/inference/agent_builder.rs index 5020799..247c5ee 100644 --- a/symbiont/src/inference/agent_builder.rs +++ b/symbiont/src/inference/agent_builder.rs @@ -121,7 +121,7 @@ pub const DOC_TOOLS_MAX_TURNS: usize = 8; /// # Arguments: /// - `opt_crate_name`: The crate whose API the evolved code can use, usually /// `Some(env!("CARGO_PKG_NAME"))`. With `None`, no host API is documented -/// and `doc_mode` has no effect. +/// and `doc_mode` must be `DocMode::Inline`. /// - `doc_mode`: How the agent gets the host API documentation: inline in /// the system prompt, or on demand through the `api_index` and `api_doc` /// tools. diff --git a/symbiont/src/system_prompt.rs b/symbiont/src/system_prompt.rs index f48bb86..1a03df1 100644 --- a/symbiont/src/system_prompt.rs +++ b/symbiont/src/system_prompt.rs @@ -182,11 +182,11 @@ pub enum DocMode { /// The system prompt contains the full API synopsis. The prompt grows /// with the size of the host API, and every inference request sends it /// again. + #[default] Inline, /// The system prompt contains the compact index of the prelude. The /// `api_index` and `api_doc` tools give the agent the full definitions /// on demand. - #[default] IndexAndTools, /// The system prompt contains no API content. The agent explores the API /// with the `api_index` and `api_doc` tools. From 3732d6ddb1560eaf3716b9065ea6f5cd8d7c0165 Mon Sep 17 00:00:00 2001 From: MathisWellmann Date: Mon, 31 Aug 2026 18:11:24 +0200 Subject: [PATCH 4/4] address review comments --- examples/evolving-trader/src/main.rs | 2 +- examples/struct-support/src/main.rs | 2 +- symbiont/src/inference/agent_builder.rs | 38 ++++++++++++++++++++++++- symbiont/src/system_prompt.rs | 34 ++++++++++++++-------- 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/examples/evolving-trader/src/main.rs b/examples/evolving-trader/src/main.rs index 836319c..d24b0d4 100644 --- a/examples/evolving-trader/src/main.rs +++ b/examples/evolving-trader/src/main.rs @@ -693,7 +693,7 @@ async fn agent(host_crate: &str) -> symbiont::Result { let agent = Agent::new( symbiont::agent_builder( Some(host_crate), - DocMode::default(), + DocMode::IndexAndTools, &base_url, &api_key, &model, diff --git a/examples/struct-support/src/main.rs b/examples/struct-support/src/main.rs index 4ba7098..2a91c7b 100644 --- a/examples/struct-support/src/main.rs +++ b/examples/struct-support/src/main.rs @@ -36,7 +36,7 @@ async fn main() -> symbiont::Result<()> { let doc_crate = Some(host_crate); // Document the host crate API for the agent. let model = std::env::var("MODEL").expect("the MODEL env var names the model slug"); - let agent = symbiont::agent_from_env(doc_crate, DocMode::default(), &model, false).await?; + let agent = symbiont::agent_from_env(doc_crate, DocMode::IndexAndTools, &model, false).await?; let base_prompt = format!( "Give an implementation for this evolvable function:\n diff --git a/symbiont/src/inference/agent_builder.rs b/symbiont/src/inference/agent_builder.rs index 247c5ee..867b4c5 100644 --- a/symbiont/src/inference/agent_builder.rs +++ b/symbiont/src/inference/agent_builder.rs @@ -132,6 +132,12 @@ pub const DOC_TOOLS_MAX_TURNS: usize = 8; /// (vLLM, llama-server, OpenRouter, OpenAI, etc.). Can also be passed as `bool` (`false` -> `Disabled`, `true` -> `Medium`). /// Keep this [`ThinkingLevel::Disabled`] for thinking models (e.g. Qwen3, DeepSeek) in latency-sensitive loops. /// +/// # Errors +/// +/// Returns [`Error::InvalidDocMode`] if `doc_mode` registers tools but +/// `opt_crate_name` is `None`. Also returns errors from building the +/// inference client or the host API documentation. +/// pub async fn agent_builder( opt_crate_name: Option<&str>, doc_mode: DocMode, @@ -209,13 +215,19 @@ pub async fn agent_from_env( /// # Arguments: /// - `opt_crate_name`: The crate whose API the evolved code can use, usually /// `Some(env!("CARGO_PKG_NAME"))`. With `None`, no host API is documented -/// and `doc_mode` has no effect. +/// and `doc_mode` must be `DocMode::Inline`. /// - `doc_mode`: How the agent gets the host API documentation. See [`agent_builder`]. /// - `base_url`: The inference endpoint for `/v1/chat/completions` based requests. /// - `api_key`: The API key for authenticating the requests, if any. Can be empty. /// - `model`: The model slug served at `base_url`. /// - `thinking`: The [`ThinkingLevel`] configuring reasoning effort. See [`agent_builder`]. /// +/// # Errors +/// +/// Returns the errors that [`agent_builder`] returns, in particular +/// [`Error::InvalidDocMode`] if `doc_mode` registers tools but +/// `opt_crate_name` is `None`. +/// pub async fn init_agent( opt_crate_name: Option<&str>, doc_mode: DocMode, @@ -251,4 +263,28 @@ mod tests { .expect("building a local agent needs no network"); assert_eq!(agent.provider(), "http://127.0.0.1:8321/v1"); } + + // ponytail: building the real reqwest client initializes aws-lc-rs via FFI, + // which Miri cannot execute; runs fine under a normal test. + #[cfg_attr(miri, ignore)] + #[tokio::test] + async fn agent_builder_rejects_tool_doc_modes_without_crate() { + for doc_mode in [DocMode::IndexAndTools, DocMode::Tools] { + assert!( + matches!( + agent_builder( + None, + doc_mode, + "http://127.0.0.1:8321/v1", + "", + "model", + ThinkingLevel::Disabled, + ) + .await, + Err(Error::InvalidDocMode) + ), + "tool doc modes need a crate, got doc_mode {doc_mode:?}" + ); + } + } } diff --git a/symbiont/src/system_prompt.rs b/symbiont/src/system_prompt.rs index 1a03df1..0ca2bed 100644 --- a/symbiont/src/system_prompt.rs +++ b/symbiont/src/system_prompt.rs @@ -4,6 +4,7 @@ use tracing::info; use crate::{ DocIndex, DocIndexError, + Error, Result, doc_string::write_prelude_doc_string, }; @@ -229,13 +230,14 @@ impl DocMode { /// /// - `opt_crate_name`: The crate to document, usually /// `Some(env!("CARGO_PKG_NAME"))`. With `None`, no host API is documented -/// and `doc_mode` has no effect. +/// and `doc_mode` must be `DocMode::Inline`. /// - `doc_mode`: How the prompt carries the host API documentation. /// /// # Errors /// -/// Returns an error if the runtime cannot build or parse the documentation -/// of the host crate. +/// Returns [`Error::InvalidDocMode`] if `doc_mode` registers tools but +/// `opt_crate_name` is `None`. Otherwise, returns an error if the runtime +/// cannot build or parse the documentation of the host crate. pub async fn system_prompt(opt_crate_name: Option<&str>, doc_mode: DocMode) -> Result { let mut prompt = BASE_PROMPT.to_string(); match (opt_crate_name, doc_mode) { @@ -256,7 +258,8 @@ pub async fn system_prompt(opt_crate_name: Option<&str>, doc_mode: DocMode) -> R } } (Some(_), DocMode::Tools) => prompt.push_str(TOOL_DOC_SECTION), - (None, _) => prompt.push_str(INLINE_DOC_SECTION), + (None, DocMode::Inline) => prompt.push_str(INLINE_DOC_SECTION), + (None, _) => return Err(Error::InvalidDocMode), } info!("system_prompt: {}", prompt.green()); @@ -277,14 +280,21 @@ mod tests { use super::*; #[tokio::test(flavor = "current_thread")] - async fn system_prompt_without_crate_ignores_doc_mode() { - for doc_mode in [DocMode::Inline, DocMode::IndexAndTools, DocMode::Tools] { - let prompt = system_prompt(None, doc_mode) - .await - .expect("no docs to build"); - assert!(prompt.contains("# Host API documentation")); - assert!(prompt.contains("only `std` is available")); - assert!(!prompt.contains("api_doc")); + async fn system_prompt_without_crate_requires_inline() { + let prompt = system_prompt(None, DocMode::Inline) + .await + .expect("no docs to build"); + assert!(prompt.contains("# Host API documentation")); + assert!(prompt.contains("only `std` is available")); + assert!(!prompt.contains("api_doc")); + for doc_mode in [DocMode::IndexAndTools, DocMode::Tools] { + assert!( + matches!( + system_prompt(None, doc_mode).await, + Err(Error::InvalidDocMode) + ), + "tool doc modes need a crate, got doc_mode {doc_mode:?}" + ); } }