pub mod prompt;
use crate::checker::check_graph;
use crate::index::SemanticIndex;
use crate::lagrange::{parse_graph, CompositionGraph};
use crate::llm::{LlmConfig, LlmProvider, Message};
use ed25519_dalek::SigningKey;
use noether_core::stage::validation::infer_type;
use noether_core::stage::{StageBuilder, StageId, StageLifecycle};
use noether_core::types::{is_subtype_of, TypeCompatibility};
use noether_store::{StageStore, StoreError};
use prompt::{
build_effect_inference_prompt, build_synthesis_prompt, build_system_prompt,
extract_effect_response, extract_json, extract_synthesis_response, extract_synthesis_spec,
SynthesisSpec,
};
#[derive(Debug, thiserror::Error)]
pub enum AgentError {
#[error("search failed: {0}")]
Search(String),
#[error("LLM call failed: {0}")]
Llm(#[from] crate::llm::LlmError),
#[error("no JSON found in LLM response")]
NoJsonInResponse,
#[error("invalid graph JSON: {0}")]
InvalidGraph(String),
#[error("type check failed after {attempts} attempts: {errors}")]
TypeCheckFailed { attempts: u32, errors: String },
#[error("stage synthesis failed: {0}")]
SynthesisFailed(String),
}
#[derive(Debug)]
pub struct SynthesisResult {
pub stage_id: StageId,
pub implementation: String,
pub language: String,
pub attempts: u32,
pub is_new: bool,
}
#[derive(Debug)]
pub struct ComposeResult {
pub graph: CompositionGraph,
pub attempts: u32,
pub synthesized: Vec<SynthesisResult>,
}
pub struct CompositionAgent<'a> {
index: &'a mut SemanticIndex,
llm: &'a dyn LlmProvider,
llm_config: LlmConfig,
max_retries: u32,
ephemeral_signing_key: SigningKey,
}
impl<'a> CompositionAgent<'a> {
pub fn new(
index: &'a mut SemanticIndex,
llm: &'a dyn LlmProvider,
llm_config: LlmConfig,
max_retries: u32,
) -> Self {
Self {
index,
llm,
llm_config,
max_retries,
ephemeral_signing_key: SigningKey::generate(&mut rand::rngs::OsRng),
}
}
pub fn compose(
&mut self,
problem: &str,
store: &mut dyn StageStore,
) -> Result<ComposeResult, AgentError> {
let verbose = std::env::var("NOETHER_VERBOSE").is_ok();
let mut synthesized: Vec<SynthesisResult> = Vec::new();
let mut synthesis_done = false;
loop {
let (system_prompt, user_msg) = {
let search_results = self
.index
.search(problem, 20)
.map_err(|e| AgentError::Search(e.to_string()))?;
if verbose {
eprintln!("\n[compose] Semantic search: \"{}\"", problem);
eprintln!("[compose] Found {} candidates:", search_results.len());
for (i, r) in search_results.iter().enumerate().take(10) {
if let Ok(Some(s)) = store.get(&r.stage_id) {
eprintln!(
" {:>2}. {:.3} {} {}",
i + 1,
r.score,
&s.id.0[..8],
&s.description[..s.description.len().min(60)]
);
}
}
if search_results.len() > 10 {
eprintln!(" ... and {} more", search_results.len() - 10);
}
}
let candidates: Vec<_> = search_results
.iter()
.filter_map(|r| {
store
.get(&r.stage_id)
.ok()
.flatten()
.map(|stage| (r, stage))
})
.collect();
let sp = build_system_prompt(&candidates);
if verbose {
eprintln!(
"\n[compose] System prompt: {} chars, {} candidate stages",
sp.len(),
candidates.len()
);
}
let um = match synthesized.last() {
Some(syn) => format!(
"{problem}\n\nIMPORTANT: Stage `{id}` has been synthesized and added to \
the Available Stages list above. Now output a COMPOSITION GRAPH (not \
another synthesis request) that uses this stage. Output ONLY a JSON \
code block containing the CompositionGraph.",
id = syn.stage_id.0
),
None => problem.to_string(),
};
(sp, um)
};
let mut messages = vec![Message::system(&system_prompt), Message::user(&user_msg)];
let mut last_errors = String::new();
let mut last_error_type = LastErrorType::None;
let mut did_synthesize_this_round = false;
for attempt in 1..=self.max_retries {
if verbose {
eprintln!(
"\n[compose] LLM call (attempt {}/{}, model: {})",
attempt, self.max_retries, self.llm_config.model
);
}
let response = self.llm.complete(&messages, &self.llm_config)?;
if verbose {
let trimmed = response.trim();
if trimmed.len() <= 300 {
eprintln!("[compose] LLM response:\n{trimmed}");
} else {
eprintln!(
"[compose] LLM response ({} chars):\n{}...",
trimmed.len(),
&trimmed[..300]
);
}
}
if std::env::var("NOETHER_DEBUG").is_ok() {
eprintln!(
"[agent debug] attempt {attempt} raw response:\n---\n{response}\n---"
);
}
if !synthesis_done {
if let Some(spec) = extract_synthesis_spec(&response) {
let syn = self.synthesize_stage(&spec, store)?;
if syn.is_new {
let new_stage = store
.get(&syn.stage_id)
.map_err(|e| AgentError::SynthesisFailed(e.to_string()))?
.ok_or_else(|| {
AgentError::SynthesisFailed(
"synthesized stage missing from store".into(),
)
})?;
self.index
.add_stage(new_stage)
.map_err(|e| AgentError::SynthesisFailed(e.to_string()))?;
}
synthesized.push(syn);
synthesis_done = true;
did_synthesize_this_round = true;
break; }
} else if extract_synthesis_spec(&response).is_some() {
let stage_id = synthesized
.last()
.map(|s| s.stage_id.0.as_str())
.unwrap_or("the newly synthesized stage");
last_error_type = LastErrorType::InvalidGraph;
last_errors = "synthesis already performed".into();
if attempt < self.max_retries {
messages.push(Message::assistant(&response));
messages.push(Message::user(format!(
"The new stage has already been synthesized (id: `{stage_id}`). \
Now produce a COMPOSITION GRAPH (not another synthesis request) \
that uses this stage. Output ONLY a JSON code block."
)));
}
continue;
}
let json_str = match extract_json(&response) {
Some(j) => j.to_string(),
None => {
last_error_type = LastErrorType::NoJson;
if attempt < self.max_retries {
messages.push(Message::assistant(&response));
messages.push(Message::user(
"Your response contained no JSON code block. \
Respond with ONLY a JSON code block containing the \
CompositionGraph.",
));
}
continue;
}
};
let graph = match parse_graph(&json_str) {
Ok(g) => g,
Err(e) => {
last_errors = e.to_string();
last_error_type = LastErrorType::InvalidGraph;
if attempt < self.max_retries {
messages.push(Message::assistant(&response));
let hint = if last_errors.contains("missing field `op`") {
" REMINDER: every node in the graph MUST have an \"op\" field \
(\"Stage\", \"Sequential\", \"Parallel\", \"Branch\", etc.). \
A synthesis request (\"action\": \"synthesize\") is NOT a valid \
graph node — it must be a standalone top-level response."
} else {
""
};
messages.push(Message::user(format!(
"The JSON was not a valid CompositionGraph: {e}.{hint} \
Please fix and try again."
)));
}
continue;
}
};
match check_graph(&graph.root, store) {
Ok(_) => {
if verbose {
eprintln!("[compose] ✓ Type check passed on attempt {attempt}");
}
return Ok(ComposeResult {
graph,
attempts: attempt,
synthesized,
});
}
Err(errors) => {
last_errors = errors
.iter()
.map(|e| format!("{e}"))
.collect::<Vec<_>>()
.join("; ");
last_error_type = LastErrorType::TypeCheck;
if verbose {
eprintln!(
"[compose] ✗ Type error on attempt {attempt}: {}",
&last_errors[..last_errors.len().min(150)]
);
}
if attempt < self.max_retries {
messages.push(Message::assistant(&response));
messages.push(Message::user(format!(
"The composition graph has type errors:\n{last_errors}\n\n\
If the error is about a bare value (List, Text, Number) not matching \
a Record input, DO NOT try to fix it with Parallel+Const wiring. \
Instead, SYNTHESIZE a single stage that performs the entire operation. \
Otherwise, fix the graph and try again."
)));
}
}
}
}
if did_synthesize_this_round {
continue;
}
return Err(match last_error_type {
LastErrorType::NoJson => AgentError::NoJsonInResponse,
LastErrorType::InvalidGraph => AgentError::InvalidGraph(last_errors),
LastErrorType::TypeCheck | LastErrorType::None => AgentError::TypeCheckFailed {
attempts: self.max_retries,
errors: last_errors,
},
});
}
}
fn synthesize_stage(
&self,
spec: &SynthesisSpec,
store: &mut dyn StageStore,
) -> Result<SynthesisResult, AgentError> {
let verbose = std::env::var("NOETHER_VERBOSE").is_ok();
if verbose {
eprintln!(
"\n[synthesis] Generating implementation for `{}`",
spec.name
);
eprintln!("[synthesis] Input: {}, Output: {}", spec.input, spec.output);
}
let synthesis_prompt = build_synthesis_prompt(spec);
let messages = vec![
Message::system(&synthesis_prompt),
Message::user(format!("Implement the `{}` stage.", spec.name)),
];
let mut last_error = String::new();
for attempt in 1..=self.max_retries {
if verbose {
eprintln!(
"[synthesis] Codegen attempt {}/{}",
attempt, self.max_retries
);
}
let response = self.llm.complete(&messages, &self.llm_config)?;
if verbose {
let trimmed = response.trim();
eprintln!(
"[synthesis] LLM response ({} chars): {}",
trimmed.len(),
&trimmed[..trimmed.len().min(200)]
);
}
let syn_resp = match extract_synthesis_response(&response) {
Some(r) => r,
None => {
last_error = "no valid synthesis JSON in LLM response".into();
if verbose {
eprintln!("[synthesis] ✗ Failed to parse synthesis response");
}
continue;
}
};
if let Err(e) =
validate_synthesis_examples(&syn_resp.examples, &spec.input, &spec.output)
{
last_error = e;
continue;
}
let impl_hash = compute_impl_hash(&syn_resp.implementation);
let inferred_effects = {
let inference_prompt =
build_effect_inference_prompt(&syn_resp.implementation, &syn_resp.language);
let inference_messages = vec![
Message::system(&inference_prompt),
Message::user("Analyze the code above and return the effects JSON array."),
];
match self.llm.complete(&inference_messages, &self.llm_config) {
Ok(resp) => extract_effect_response(&resp),
Err(_) => noether_core::effects::EffectSet::unknown(),
}
};
let mut builder = StageBuilder::new(&spec.name)
.input(spec.input.clone())
.output(spec.output.clone())
.description(&spec.description)
.implementation_code(&syn_resp.implementation, &syn_resp.language)
.effects(inferred_effects);
for ex in &syn_resp.examples {
builder = builder.example(ex.input.clone(), ex.output.clone());
}
let stage: noether_core::stage::Stage =
match builder.build_signed(&self.ephemeral_signing_key, impl_hash) {
Ok(s) => s,
Err(e) => {
last_error = e.to_string();
continue;
}
};
if let Ok(Some((existing_id, similarity))) = self
.index
.check_duplicate_before_insert(&spec.description, 0.92)
{
let existing_is_signed = store
.get(&existing_id)
.ok()
.flatten()
.map(|s| s.ed25519_signature.is_some())
.unwrap_or(false);
if existing_is_signed {
eprintln!(
"Synthesis dedup: description matches existing stage {} \
(similarity {similarity:.3}); reusing.",
existing_id.0
);
return Ok(SynthesisResult {
stage_id: existing_id,
implementation: syn_resp.implementation,
language: syn_resp.language,
attempts: attempt,
is_new: false,
});
}
eprintln!(
"Synthesis dedup: existing stage {} is unsigned; replacing with signed version.",
existing_id.0
);
}
let (stage_id, is_new) = match store.put(stage.clone()) {
Ok(id) => {
store
.update_lifecycle(&id, StageLifecycle::Active)
.map_err(|e| AgentError::SynthesisFailed(e.to_string()))?;
(id, true)
}
Err(StoreError::AlreadyExists(id)) => {
let needs_signing = store
.get(&id)
.ok()
.flatten()
.map(|s| s.ed25519_signature.is_none())
.unwrap_or(false);
if needs_signing {
store
.upsert(stage)
.map_err(|e| AgentError::SynthesisFailed(e.to_string()))?;
eprintln!(
"Synthesis: replaced unsigned stage {} with signed version.",
id.0
);
}
(id, false)
}
Err(e) => return Err(AgentError::SynthesisFailed(e.to_string())),
};
return Ok(SynthesisResult {
stage_id,
implementation: syn_resp.implementation,
language: syn_resp.language,
attempts: attempt,
is_new,
});
}
Err(AgentError::SynthesisFailed(last_error))
}
}
#[derive(Debug)]
enum LastErrorType {
None,
NoJson,
InvalidGraph,
TypeCheck,
}
fn validate_synthesis_examples(
examples: &[prompt::SynthesisExample],
input_type: &noether_core::types::NType,
output_type: &noether_core::types::NType,
) -> Result<(), String> {
if examples.len() < 3 {
return Err(format!("need at least 3 examples, got {}", examples.len()));
}
use noether_core::types::NType;
fn contains_any_or_map(t: &NType) -> bool {
match t {
NType::Any | NType::Map { .. } => true,
NType::List(inner) | NType::Stream(inner) => contains_any_or_map(inner),
NType::Record(fields) => fields.values().any(contains_any_or_map),
NType::Union(variants) => variants.iter().any(contains_any_or_map),
_ => false,
}
}
let strict_check = !contains_any_or_map(input_type) && !contains_any_or_map(output_type);
if strict_check {
for (i, ex) in examples.iter().enumerate() {
let inferred = infer_type(&ex.input);
if matches!(
is_subtype_of(&inferred, input_type),
TypeCompatibility::Incompatible(_)
) {
return Err(format!(
"example {i} input `{inferred}` is not subtype of `{input_type}`"
));
}
let inferred = infer_type(&ex.output);
if matches!(
is_subtype_of(&inferred, output_type),
TypeCompatibility::Incompatible(_)
) {
return Err(format!(
"example {i} output `{inferred}` is not subtype of `{output_type}`"
));
}
}
}
Ok(())
}
fn compute_impl_hash(implementation: &str) -> String {
use sha2::{Digest, Sha256};
hex::encode(Sha256::digest(implementation.as_bytes()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::index::embedding::MockEmbeddingProvider;
use crate::index::IndexConfig;
use crate::llm::{MockLlmProvider, SequenceMockLlmProvider};
use noether_core::stdlib::load_stdlib;
use noether_core::types::NType;
use noether_store::{MemoryStore, StageStore};
fn test_setup() -> (MemoryStore, SemanticIndex) {
let mut store = MemoryStore::new();
for stage in load_stdlib() {
store.put(stage).unwrap();
}
let index = SemanticIndex::build(
&store,
Box::new(MockEmbeddingProvider::new(128)),
IndexConfig::default(),
)
.unwrap();
(store, index)
}
fn find_stage_id(store: &MemoryStore, desc_contains: &str) -> String {
store
.list(None)
.into_iter()
.find(|s| s.description.contains(desc_contains))
.unwrap()
.id
.0
.clone()
}
#[test]
fn compose_with_valid_mock_response() {
let (mut store, mut index) = test_setup();
let to_text_id = find_stage_id(&store, "Convert any value to its text");
let mock_response = format!(
"```json\n{}\n```",
serde_json::json!({
"description": "convert to text",
"version": "0.1.0",
"root": { "op": "Stage", "id": to_text_id }
})
);
let llm = MockLlmProvider::new(mock_response);
let mut agent = CompositionAgent::new(&mut index, &llm, LlmConfig::default(), 3);
let result = agent.compose("convert input to text", &mut store).unwrap();
assert_eq!(result.attempts, 1);
assert_eq!(result.graph.description, "convert to text");
assert!(result.synthesized.is_empty());
}
#[test]
fn compose_with_valid_sequential() {
let (mut store, mut index) = test_setup();
let to_json_id = find_stage_id(&store, "Serialize any value to a JSON");
let parse_json_id = find_stage_id(&store, "Parse a JSON string");
let mock_response = format!(
"```json\n{}\n```",
serde_json::json!({
"description": "round-trip JSON",
"version": "0.1.0",
"root": {
"op": "Sequential",
"stages": [
{"op": "Stage", "id": to_json_id},
{"op": "Stage", "id": parse_json_id}
]
}
})
);
let llm = MockLlmProvider::new(mock_response);
let mut agent = CompositionAgent::new(&mut index, &llm, LlmConfig::default(), 3);
let result = agent
.compose("serialize and parse JSON", &mut store)
.unwrap();
assert_eq!(result.attempts, 1);
}
#[test]
fn compose_fails_with_no_json() {
let (mut store, mut index) = test_setup();
let llm = MockLlmProvider::new("I don't know how to help with that.");
let mut agent = CompositionAgent::new(&mut index, &llm, LlmConfig::default(), 1);
assert!(agent.compose("do something", &mut store).is_err());
}
#[test]
fn compose_fails_with_invalid_stage_id() {
let (mut store, mut index) = test_setup();
let mock_response = "```json\n{\"description\":\"test\",\"version\":\"0.1.0\",\"root\":{\"op\":\"Stage\",\"id\":\"nonexistent\"}}\n```";
let llm = MockLlmProvider::new(mock_response);
let mut agent = CompositionAgent::new(&mut index, &llm, LlmConfig::default(), 1);
assert!(agent.compose("test", &mut store).is_err());
}
#[test]
fn validate_examples_accepts_valid_set() {
use serde_json::json;
let examples = vec![
prompt::SynthesisExample {
input: json!("hello"),
output: json!(5),
},
prompt::SynthesisExample {
input: json!("hi"),
output: json!(2),
},
prompt::SynthesisExample {
input: json!("world"),
output: json!(5),
},
];
assert!(validate_synthesis_examples(&examples, &NType::Text, &NType::Number).is_ok());
}
#[test]
fn validate_examples_rejects_wrong_output_type() {
use serde_json::json;
let examples = vec![
prompt::SynthesisExample {
input: json!("hello"),
output: json!("five"), },
prompt::SynthesisExample {
input: json!("hi"),
output: json!("two"),
},
prompt::SynthesisExample {
input: json!("world"),
output: json!("five"),
},
];
assert!(validate_synthesis_examples(&examples, &NType::Text, &NType::Number).is_err());
}
#[test]
fn validate_examples_rejects_too_few() {
use serde_json::json;
let examples = vec![
prompt::SynthesisExample {
input: json!("hello"),
output: json!(5),
},
prompt::SynthesisExample {
input: json!("hi"),
output: json!(2),
},
];
assert!(validate_synthesis_examples(&examples, &NType::Text, &NType::Number).is_err());
}
#[test]
fn compose_triggers_synthesis_then_succeeds() {
use serde_json::json;
let (mut store, mut index) = test_setup();
let to_text_id = find_stage_id(&store, "Convert any value to its text");
let synthesis_request = format!(
"```json\n{}\n```",
json!({
"action": "synthesize",
"spec": {
"name": "count_words",
"description": "Count the number of words in a text string",
"input": {"kind": "Text"},
"output": {"kind": "Number"},
"rationale": "No existing stage counts words in text"
}
})
);
let synthesis_response = format!(
"```json\n{}\n```",
json!({
"examples": [
{"input": "hello world", "output": 2.0},
{"input": "one two three", "output": 3.0},
{"input": "single", "output": 1.0}
],
"implementation": "def execute(input_value):\n return len(input_value.split())",
"language": "python"
})
);
let effect_inference_response = "```json\n[\"Pure\"]\n```".to_string();
let composition = format!(
"```json\n{}\n```",
json!({
"description": "convert input to text",
"version": "0.1.0",
"root": {"op": "Stage", "id": to_text_id}
})
);
let llm = SequenceMockLlmProvider::new(
vec![
synthesis_request,
synthesis_response,
effect_inference_response,
composition,
],
"no more responses".to_string(),
);
let mut agent = CompositionAgent::new(&mut index, &llm, LlmConfig::default(), 3);
let result = agent
.compose("count the words in some text", &mut store)
.unwrap();
assert_eq!(result.synthesized.len(), 1);
let syn = &result.synthesized[0];
assert_eq!(syn.language, "python");
assert!(syn.implementation.contains("execute"));
let new_stage = store.get(&syn.stage_id).unwrap().unwrap();
assert_eq!(new_stage.lifecycle, StageLifecycle::Active);
assert_eq!(new_stage.signature.input, NType::Text);
assert_eq!(new_stage.signature.output, NType::Number);
assert_eq!(new_stage.examples.len(), 3);
}
#[test]
fn compose_synthesis_fails_on_bad_examples() {
use serde_json::json;
let (mut store, mut index) = test_setup();
let synthesis_request = format!(
"```json\n{}\n```",
json!({
"action": "synthesize",
"spec": {
"name": "bad_stage",
"description": "A stage with wrong example types",
"input": {"kind": "Text"},
"output": {"kind": "Number"},
"rationale": "testing"
}
})
);
let bad_codegen = format!(
"```json\n{}\n```",
json!({
"examples": [
{"input": "a", "output": "wrong"},
{"input": "b", "output": "wrong"},
{"input": "c", "output": "wrong"}
],
"implementation": "def execute(v): return 'wrong'",
"language": "python"
})
);
let llm = SequenceMockLlmProvider::new(
vec![
synthesis_request,
bad_codegen.clone(),
bad_codegen.clone(),
bad_codegen,
],
String::new(),
);
let mut agent = CompositionAgent::new(&mut index, &llm, LlmConfig::default(), 1);
let result = agent.compose("do something", &mut store);
assert!(result.is_err());
assert!(
matches!(result.unwrap_err(), AgentError::SynthesisFailed(_)),
"expected SynthesisFailed"
);
}
#[test]
fn compose_redirects_after_duplicate_synthesis_request() {
use serde_json::json;
let (mut store, mut index) = test_setup();
let to_text_id = find_stage_id(&store, "Convert any value to its text");
let synthesis_request = format!(
"```json\n{}\n```",
json!({
"action": "synthesize",
"spec": {
"name": "count_chars",
"description": "Count characters in a string",
"input": {"kind": "Text"},
"output": {"kind": "Number"},
"rationale": "No existing stage counts characters"
}
})
);
let codegen = format!(
"```json\n{}\n```",
json!({
"examples": [
{"input": "hi", "output": 2.0},
{"input": "hello", "output": 5.0},
{"input": "world", "output": 5.0}
],
"implementation": "def execute(v): return len(v)",
"language": "python"
})
);
let effect_resp = "```json\n[\"Pure\"]\n```".to_string();
let graph = format!(
"```json\n{}\n```",
json!({
"description": "count chars",
"version": "0.1.0",
"root": {"op": "Stage", "id": to_text_id}
})
);
let llm = SequenceMockLlmProvider::new(
vec![
synthesis_request.clone(), codegen, effect_resp, synthesis_request, graph, ],
String::new(),
);
let mut agent = CompositionAgent::new(&mut index, &llm, LlmConfig::default(), 3);
let result = agent.compose("count characters in text", &mut store);
assert!(result.is_ok(), "expected Ok, got: {result:?}");
assert_eq!(result.unwrap().synthesized.len(), 1);
}
#[test]
fn synthesize_stage_is_idempotent() {
use serde_json::json;
let (mut store, mut index) = test_setup();
let synthesis_request = format!(
"```json\n{}\n```",
json!({
"action": "synthesize",
"spec": {
"name": "noop_stage",
"description": "Return input unchanged",
"input": {"kind": "Text"},
"output": {"kind": "Text"},
"rationale": "testing idempotency"
}
})
);
let codegen = format!(
"```json\n{}\n```",
json!({
"examples": [
{"input": "a", "output": "a"},
{"input": "b", "output": "b"},
{"input": "c", "output": "c"}
],
"implementation": "def execute(v): return v",
"language": "python"
})
);
let effect_inference_response = "```json\n[\"Pure\"]\n```".to_string();
let to_text_id = find_stage_id(&store, "Convert any value to its text");
let graph_json = format!(
"```json\n{}\n```",
json!({
"description": "noop",
"version": "0.1.0",
"root": {"op": "Stage", "id": to_text_id}
})
);
{
let llm = SequenceMockLlmProvider::new(
vec![
synthesis_request.clone(),
codegen.clone(),
effect_inference_response.clone(),
graph_json.clone(),
],
String::new(),
);
let mut agent = CompositionAgent::new(&mut index, &llm, LlmConfig::default(), 3);
agent.compose("noop", &mut store).unwrap();
}
{
let llm = SequenceMockLlmProvider::new(
vec![
synthesis_request,
codegen,
effect_inference_response,
graph_json,
],
String::new(),
);
let mut agent = CompositionAgent::new(&mut index, &llm, LlmConfig::default(), 3);
let result = agent.compose("noop", &mut store);
assert!(result.is_ok());
}
}
}