Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/evolving-trader/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ async fn agent(host_crate: &str) -> symbiont::Result<Agent> {
let agent = Agent::new(
symbiont::agent_builder(
Some(host_crate),
DocMode::default(),
DocMode::IndexAndTools,
&base_url,
&api_key,
&model,
Expand Down
3 changes: 2 additions & 1 deletion examples/fractal-studio/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use rayon::prelude::*;
use symbiont::{
DocMode,
Runtime,
ThinkingLevel,
};
use tracing::{
info,
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion examples/struct-support/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions symbiont/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
44 changes: 41 additions & 3 deletions symbiont/src/inference/agent_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::{
ApiIndexTool,
DocIndex,
DocMode,
Error,
MeteredHttpClient,
Result,
ThinkingLevel,
Expand Down Expand Up @@ -120,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.
Expand All @@ -131,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,
Expand Down Expand Up @@ -171,6 +178,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)
Expand Down Expand Up @@ -207,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,
Expand All @@ -239,7 +253,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",
Expand All @@ -249,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:?}"
);
}
}
}
36 changes: 23 additions & 13 deletions symbiont/src/system_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use tracing::info;
use crate::{
DocIndex,
DocIndexError,
Error,
Result,
doc_string::write_prelude_doc_string,
};
Expand Down Expand Up @@ -182,11 +183,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.
Expand Down Expand Up @@ -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<String> {
let mut prompt = BASE_PROMPT.to_string();
match (opt_crate_name, doc_mode) {
Expand All @@ -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());

Expand All @@ -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:?}"
);
}
}

Expand Down
Loading