refactor: collapse codergen run arguments

Introduce CodergenRunRequest so multi-turn backend calls use the same request-object shape as one-shot calls and stop growing the trait parameter list.
This commit is contained in:
Bryan Helmkamp 2026-05-11 16:25:30 -04:00
parent 2baa4f0608
commit 4f4f576439
No known key found for this signature in database
9 changed files with 238 additions and 496 deletions

View file

@ -5,7 +5,7 @@ use fabro_graphviz::graph::{AttrValue, Node};
use fabro_llm::provider::Provider;
use fabro_workflow::context::Context;
use fabro_workflow::event::Emitter;
use fabro_workflow::handler::agent::{CodergenBackend, CodergenResult};
use fabro_workflow::handler::agent::{CodergenBackend, CodergenResult, CodergenRunRequest};
use fabro_workflow::handler::llm::cli::AgentCliBackend;
/// Run a real CLI tool via LocalSandbox and verify the full flow.
@ -26,16 +26,16 @@ async fn run_real_cli_test(provider: Provider, model: &str) {
let context = Context::new();
let emitter = Arc::new(Emitter::default());
let result = backend
.run(
&node,
"What is 2+2? Reply with just the number.",
&context,
None,
&emitter,
&env,
None,
tokio_util::sync::CancellationToken::new(),
)
.run(CodergenRunRequest {
node: &node,
prompt: "What is 2+2? Reply with just the number.",
context: &context,
thread_id: None,
emitter: &emitter,
sandbox: &env,
tool_hooks: None,
cancel_token: tokio_util::sync::CancellationToken::new(),
})
.await
.unwrap_or_else(|_| panic!("CLI backend ({provider}/{model}) should succeed"));

View file

@ -28,6 +28,17 @@ pub enum CodergenResult {
Full(Outcome),
}
pub struct CodergenRunRequest<'a> {
pub node: &'a Node,
pub prompt: &'a str,
pub context: &'a Context,
pub thread_id: Option<&'a str>,
pub emitter: &'a Arc<Emitter>,
pub sandbox: &'a Arc<dyn Sandbox>,
pub tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
pub cancel_token: CancellationToken,
}
pub struct OneShotRequest<'a> {
pub node: &'a Node,
pub prompt: &'a str,
@ -39,24 +50,10 @@ pub struct OneShotRequest<'a> {
}
/// Backend interface for LLM execution in codergen nodes.
#[allow(
clippy::too_many_arguments,
reason = "Codergen run mode needs the node, prompt, context, and runtime handles separately."
)]
#[async_trait]
pub trait CodergenBackend: Send + Sync {
/// Run a multi-turn agent loop (the default codergen mode).
async fn run(
&self,
node: &Node,
prompt: &str,
context: &Context,
thread_id: Option<&str>,
emitter: &Arc<Emitter>,
sandbox: &Arc<dyn Sandbox>,
tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
cancel_token: CancellationToken,
) -> Result<CodergenResult, Error>;
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error>;
/// Run a single LLM call with no tools (one_shot mode).
async fn one_shot(&self, _request: OneShotRequest<'_>) -> Result<CodergenResult, Error> {
@ -304,16 +301,16 @@ impl Handler for AgentHandler {
let (response_text, stage_usage, backend_files_touched, last_file_touched) =
if let Some(backend) = &self.backend {
let result = backend
.run(
.run(CodergenRunRequest {
node,
&prompt,
prompt: &prompt,
context,
thread_id.as_deref(),
&services.run.emitter,
&services.run.sandbox,
thread_id: thread_id.as_deref(),
emitter: &services.run.emitter,
sandbox: &services.run.sandbox,
tool_hooks,
services.run.cancel_token(),
)
cancel_token: services.run.cancel_token(),
})
.await;
match result {
Ok(CodergenResult::Full(outcome)) => return Ok(outcome),
@ -432,7 +429,6 @@ mod tests {
use tempfile::TempDir;
use super::*;
use crate::event::Emitter;
fn make_services() -> EngineServices {
EngineServices::test_default()
@ -644,25 +640,13 @@ mod tests {
#[tokio::test]
async fn codergen_handler_prefers_response_text_over_status_json() {
use std::sync::Arc;
// Backend returns response text with routing directives — status.json
// in the sandbox should be ignored.
struct DirectiveBackend;
#[async_trait]
impl CodergenBackend for DirectiveBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn fabro_agent::Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, _request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
Ok(CodergenResult::Text {
text:
r#"Done. {"outcome": "succeeded", "preferred_next_label": "approve"}"#
@ -707,23 +691,11 @@ mod tests {
#[tokio::test]
async fn codergen_handler_extracts_status_from_last_file_touched() {
use std::sync::Arc;
struct LastFileBackend;
#[async_trait]
impl CodergenBackend for LastFileBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn fabro_agent::Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, _request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
Ok(CodergenResult::Text {
text: "Done writing results.".to_string(),
usage: None,
@ -775,21 +747,11 @@ mod tests {
#[async_trait]
impl CodergenBackend for ProviderEventBackend {
async fn run(
&self,
node: &Node,
_prompt: &str,
context: &Context,
_thread_id: Option<&str>,
emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn fabro_agent::Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
let scope = StageScope::for_handler(context, &node.id);
emitter.emit_scoped(
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
let scope = StageScope::for_handler(request.context, &request.node.id);
request.emitter.emit_scoped(
&crate::event::Event::AgentSessionActivated {
node_id: node.id.clone(),
node_id: request.node.id.clone(),
visit: scope.visit,
session_id: "session_123".to_string(),
thread_id: None,
@ -886,18 +848,9 @@ mod tests {
#[async_trait]
impl CodergenBackend for ThreadCapturingBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
*self.captured_thread_id.lock().unwrap() = Some(thread_id.map(String::from));
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
*self.captured_thread_id.lock().unwrap() =
Some(request.thread_id.map(String::from));
Ok(CodergenResult::Text {
text: "ok".to_string(),
usage: None,
@ -939,18 +892,9 @@ mod tests {
#[async_trait]
impl CodergenBackend for ThreadCapturingBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
*self.captured_thread_id.lock().unwrap() = Some(thread_id.map(String::from));
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
*self.captured_thread_id.lock().unwrap() =
Some(request.thread_id.map(String::from));
Ok(CodergenResult::Text {
text: "ok".to_string(),
usage: None,
@ -987,17 +931,7 @@ mod tests {
#[async_trait]
impl CodergenBackend for FailingBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, _request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
Err(Error::handler("Request timed out".to_string()))
}
}
@ -1135,17 +1069,7 @@ Some text in between.
#[async_trait]
impl CodergenBackend for ValidationFailBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, _request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
Err(Error::Validation("bad config".to_string()))
}
}
@ -1176,18 +1100,8 @@ Some text in between.
#[async_trait]
impl CodergenBackend for PromptCapturingBackend {
async fn run(
&self,
_node: &Node,
prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
*self.captured_prompt.lock().unwrap() = Some(prompt.to_string());
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
*self.captured_prompt.lock().unwrap() = Some(request.prompt.to_string());
Ok(CodergenResult::Text {
text: "ok".to_string(),
usage: None,
@ -1246,18 +1160,8 @@ Some text in between.
#[async_trait]
impl CodergenBackend for PromptCapturingBackend {
async fn run(
&self,
_node: &Node,
prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
*self.captured_prompt.lock().unwrap() = Some(prompt.to_string());
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
*self.captured_prompt.lock().unwrap() = Some(request.prompt.to_string());
Ok(CodergenResult::Text {
text: "ok".to_string(),
usage: None,

View file

@ -6,7 +6,7 @@ use fabro_agent::Sandbox;
use fabro_graphviz::graph::{Graph, Node};
use tokio_util::sync::CancellationToken;
use super::agent::{CodergenBackend, CodergenResult};
use super::agent::{CodergenBackend, CodergenResult, CodergenRunRequest};
use super::{EngineServices, Handler};
use crate::context::{Context, keys};
use crate::error::Error;
@ -260,16 +260,16 @@ async fn llm_evaluate(
// Fan-in evaluation runs outside a thread context, so pass None
match backend
.run(
&eval_node,
&full_prompt,
.run(CodergenRunRequest {
node: &eval_node,
prompt: &full_prompt,
context,
None,
thread_id: None,
emitter,
sandbox,
None,
tool_hooks: None,
cancel_token,
)
})
.await
{
Ok(CodergenResult::Full(outcome)) => {
@ -469,23 +469,13 @@ mod tests {
async fn fan_in_with_backend_llm_eval() {
use tempfile::TempDir;
use crate::handler::agent::CodergenBackend;
use crate::handler::agent::{CodergenBackend, CodergenRunRequest};
struct MockBackend;
#[async_trait]
impl CodergenBackend for MockBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, _request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
// Return text that contains the ID "branch_b"
Ok(CodergenResult::Text {
text: "The best candidate is branch_b".to_string(),

View file

@ -12,11 +12,10 @@ use fabro_model::Provider;
use fabro_util::time::elapsed_ms;
use tokio_util::sync::CancellationToken;
use super::super::agent::{CodergenBackend, CodergenResult, OneShotRequest};
use super::super::agent::{CodergenBackend, CodergenResult, CodergenRunRequest, OneShotRequest};
use super::changed_files;
use super::cli::AgentCli;
use super::launch_env::{AgentLaunchEnvRequest, resolve_agent_launch_env};
use crate::context::Context;
use crate::error::Error;
use crate::event::{Emitter, Event, StageScope};
@ -199,25 +198,15 @@ impl AgentAcpBackend {
#[async_trait]
impl CodergenBackend for AgentAcpBackend {
async fn run(
&self,
node: &Node,
prompt: &str,
context: &Context,
_thread_id: Option<&str>,
emitter: &Arc<Emitter>,
sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
let stage_scope = StageScope::for_handler(context, &node.id);
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
let stage_scope = StageScope::for_handler(request.context, &request.node.id);
self.run_turn(
node,
prompt.to_string(),
emitter,
request.node,
request.prompt.to_string(),
request.emitter,
&stage_scope,
sandbox,
cancel_token,
request.sandbox,
request.cancel_token,
)
.await
}
@ -294,7 +283,9 @@ mod tests {
use super::AgentAcpBackend;
use crate::context::Context;
use crate::event::{Emitter, StageScope};
use crate::handler::agent::{CodergenBackend, CodergenResult, OneShotRequest};
use crate::handler::agent::{
CodergenBackend, CodergenResult, CodergenRunRequest, OneShotRequest,
};
#[tokio::test]
async fn acp_backend_run_sends_prompt_and_returns_text() {
@ -329,17 +320,19 @@ mod tests {
HashMap::from([("ACP_MODE".to_string(), "write_file".to_string())]),
);
let sandbox: Arc<dyn Sandbox> = Arc::new(LocalSandbox::new(tempdir.path().to_path_buf()));
let emitter = Arc::new(Emitter::default());
let context = Context::new();
let result = backend
.run(
&node,
"write hello",
&Context::new(),
None,
&Arc::new(Emitter::default()),
&sandbox,
None,
CancellationToken::new(),
)
.run(CodergenRunRequest {
node: &node,
prompt: "write hello",
context: &context,
thread_id: None,
emitter: &emitter,
sandbox: &sandbox,
tool_hooks: None,
cancel_token: CancellationToken::new(),
})
.await
.unwrap();
@ -441,17 +434,19 @@ mod tests {
HashMap::from([("ACP_STOP_REASON".to_string(), "cancelled".to_string())]),
);
let sandbox: Arc<dyn Sandbox> = Arc::new(LocalSandbox::new(tempdir.path().to_path_buf()));
let emitter = Arc::new(Emitter::default());
let context = Context::new();
let result = backend
.run(
&node,
"cancel",
&Context::new(),
None,
&Arc::new(Emitter::default()),
&sandbox,
None,
CancellationToken::new(),
)
.run(CodergenRunRequest {
node: &node,
prompt: "cancel",
context: &context,
thread_id: None,
emitter: &emitter,
sandbox: &sandbox,
tool_hooks: None,
cancel_token: CancellationToken::new(),
})
.await;
let Err(err) = result else {
panic!("expected cancellation error");
@ -497,17 +492,18 @@ mod tests {
move |event| events.lock().unwrap().push(event.clone())
});
let context = Context::new();
backend
.run(
&node,
"write hello",
&Context::new(),
None,
&emitter,
&sandbox,
None,
CancellationToken::new(),
)
.run(CodergenRunRequest {
node: &node,
prompt: "write hello",
context: &context,
thread_id: None,
emitter: &emitter,
sandbox: &sandbox,
tool_hooks: None,
cancel_token: CancellationToken::new(),
})
.await
.unwrap();
@ -540,17 +536,19 @@ mod tests {
.insert("backend".to_string(), AttrValue::String("acp".to_string()));
let backend = AgentAcpBackend::new_from_env("fake-acp".to_string(), Provider::OpenAi);
let emitter = Arc::new(Emitter::default());
let context = Context::new();
let result = backend
.run(
&node,
"write hello",
&Context::new(),
None,
&Arc::new(Emitter::default()),
&sandbox_dyn,
None,
CancellationToken::new(),
)
.run(CodergenRunRequest {
node: &node,
prompt: "write hello",
context: &context,
thread_id: None,
emitter: &emitter,
sandbox: &sandbox_dyn,
tool_hooks: None,
cancel_token: CancellationToken::new(),
})
.await;
let Err(err) = result else {
panic!("ACP without acp_command should fail");

View file

@ -19,10 +19,10 @@ use tokio::sync::Mutex as TokioMutex;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use super::super::agent::{CodergenBackend, CodergenResult, OneShotRequest};
use super::super::agent::{CodergenBackend, CodergenResult, CodergenRunRequest, OneShotRequest};
use super::activation_lease::{ActivationLease, ActivationLeaseOptions};
use crate::context::WorkflowContext;
use crate::context::keys::Fidelity;
use crate::context::{Context, WorkflowContext};
use crate::error::Error;
use crate::event::{Emitter, Event, StageScope};
use crate::outcome::billed_model_usage_from_llm;
@ -616,17 +616,16 @@ impl CodergenBackend for AgentApiBackend {
})
}
async fn run(
&self,
node: &Node,
prompt: &str,
context: &Context,
thread_id: Option<&str>,
emitter: &Arc<Emitter>,
sandbox: &Arc<dyn Sandbox>,
tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
let node = request.node;
let prompt = request.prompt;
let context = request.context;
let thread_id = request.thread_id;
let emitter = request.emitter;
let sandbox = request.sandbox;
let tool_hooks = request.tool_hooks;
let cancel_token = request.cancel_token;
let actual_model = node.model().unwrap_or(&self.model).to_string();
let _actual_provider = node
.provider()

View file

@ -38,11 +38,10 @@ fn cli_failure_detail(stdout: &str, stderr: &str, command: &str) -> String {
}
}
use super::super::agent::{CodergenBackend, CodergenResult, OneShotRequest};
use super::super::agent::{CodergenBackend, CodergenResult, CodergenRunRequest, OneShotRequest};
use super::acp::AgentAcpBackend;
use super::changed_files;
use super::launch_env::{AgentLaunchEnvRequest, resolve_agent_launch_env};
use crate::context::Context;
use crate::error::Error;
use crate::event::{Emitter, Event, StageScope};
use crate::outcome::billed_model_usage_from_llm;
@ -390,17 +389,14 @@ impl AgentCliBackend {
#[async_trait]
impl CodergenBackend for AgentCliBackend {
async fn run(
&self,
node: &Node,
prompt: &str,
context: &Context,
_thread_id: Option<&str>,
emitter: &Arc<Emitter>,
sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
let node = request.node;
let prompt = request.prompt;
let context = request.context;
let emitter = request.emitter;
let sandbox = request.sandbox;
let cancel_token = request.cancel_token;
// 1. Snapshot git state before the CLI run
let files_before = changed_files::detect_changed_files(sandbox).await;
@ -722,60 +718,11 @@ fn unsupported_backend_error(raw: &str) -> Error {
#[async_trait]
impl CodergenBackend for BackendRouter {
async fn run(
&self,
node: &Node,
prompt: &str,
context: &Context,
thread_id: Option<&str>,
emitter: &Arc<Emitter>,
sandbox: &Arc<dyn Sandbox>,
tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
match Self::select_backend(node)? {
LlmBackend::Api => {
self.api
.run(
node,
prompt,
context,
thread_id,
emitter,
sandbox,
tool_hooks,
cancel_token,
)
.await
}
LlmBackend::Cli => {
self.cli
.run(
node,
prompt,
context,
thread_id,
emitter,
sandbox,
tool_hooks,
cancel_token,
)
.await
}
LlmBackend::Acp => {
self.acp
.run(
node,
prompt,
context,
thread_id,
emitter,
sandbox,
tool_hooks,
cancel_token,
)
.await
}
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
match Self::select_backend(request.node)? {
LlmBackend::Api => self.api.run(request).await,
LlmBackend::Cli => self.cli.run(request).await,
LlmBackend::Acp => self.acp.run(request).await,
}
}
@ -800,6 +747,7 @@ mod tests {
use fabro_graphviz::graph::AttrValue;
use super::*;
use crate::context::Context;
// -- AgentCli --
@ -1348,17 +1296,7 @@ mod tests {
#[async_trait]
impl CodergenBackend for StubBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, _request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
Ok(CodergenResult::Text {
text: "stub".to_string(),
usage: None,
@ -1518,16 +1456,16 @@ mod tests {
let events = collect_events(&emitter);
let result = backend
.run(
&node,
"Do something",
&context,
None,
&emitter,
&sandbox,
None,
CancellationToken::new(),
)
.run(CodergenRunRequest {
node: &node,
prompt: "Do something",
context: &context,
thread_id: None,
emitter: &emitter,
sandbox: &sandbox,
tool_hooks: None,
cancel_token: CancellationToken::new(),
})
.await;
let Err(err) = result else {
@ -1571,16 +1509,16 @@ mod tests {
let events = collect_events(&emitter);
let result = backend
.run(
&node,
"Do something slow",
&context,
None,
&emitter,
&sandbox,
None,
CancellationToken::new(),
)
.run(CodergenRunRequest {
node: &node,
prompt: "Do something slow",
context: &context,
thread_id: None,
emitter: &emitter,
sandbox: &sandbox,
tool_hooks: None,
cancel_token: CancellationToken::new(),
})
.await;
let Err(err) = result else {

View file

@ -210,10 +210,10 @@ mod tests {
use fabro_types::fixtures;
use object_store::memory::InMemory;
use tempfile::TempDir;
use tokio_util::sync::CancellationToken;
use super::*;
use crate::event::Emitter;
use crate::handler::agent::CodergenRunRequest;
fn make_services() -> EngineServices {
EngineServices::test_default()
@ -311,23 +311,11 @@ mod tests {
#[tokio::test]
async fn prompt_handler_dispatches_to_backend_one_shot() {
use fabro_agent::Sandbox;
struct OneShotBackend;
#[async_trait]
impl CodergenBackend for OneShotBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, _request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
panic!("run() should not be called for prompt handler");
}
@ -370,23 +358,11 @@ mod tests {
#[tokio::test]
async fn prompt_handler_projects_provider_used_from_prompt_events() {
use fabro_agent::Sandbox;
struct ProviderOneShotBackend;
#[async_trait]
impl CodergenBackend for ProviderOneShotBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, _request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
panic!("run() should not be called for prompt handler");
}
@ -432,17 +408,7 @@ mod tests {
#[async_trait]
impl CodergenBackend for OneShotCapturingBackend {
async fn run(
&self,
_node: &Node,
_prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn fabro_agent::Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, _request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
panic!("run() should not be called for prompt handler");
}

View file

@ -995,7 +995,7 @@ async fn daytona_parallel_git_branching_e2e() {
// CLI Backend on Daytona — real CLI tools via exec_command
// ---------------------------------------------------------------------------
use fabro_workflow::handler::agent::{CodergenBackend, CodergenResult};
use fabro_workflow::handler::agent::{CodergenBackend, CodergenResult, CodergenRunRequest};
use fabro_workflow::handler::llm::AgentCliBackend;
/// Helper: run a real CLI backend test on Daytona.
@ -1072,16 +1072,16 @@ async fn run_daytona_cli_test(provider: Provider, model: &str, install_command:
let emitter = Arc::new(Emitter::default());
let result = backend
.run(
&node,
"What is 2+2? Reply with just the number.",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
.run(CodergenRunRequest {
node: &node,
prompt: "What is 2+2? Reply with just the number.",
context: &context,
thread_id: None,
emitter: &emitter,
sandbox: &env,
tool_hooks: None,
cancel_token: CancellationToken::new(),
})
.await;
match result {

View file

@ -38,7 +38,9 @@ use fabro_validate::{Severity, validate, validate_or_raise};
use fabro_workflow::context::Context;
use fabro_workflow::error::{Error, FailureSignatureExt};
use fabro_workflow::event::{Emitter, Event};
use fabro_workflow::handler::agent::{AgentHandler, CodergenBackend, CodergenResult};
use fabro_workflow::handler::agent::{
AgentHandler, CodergenBackend, CodergenResult, CodergenRunRequest,
};
use fabro_workflow::handler::command::CommandHandler;
use fabro_workflow::handler::conditional::ConditionalHandler;
use fabro_workflow::handler::exit::ExitHandler;
@ -65,6 +67,25 @@ fn local_env() -> Arc<dyn fabro_agent::Sandbox> {
))
}
fn codergen_run_request<'a>(
node: &'a Node,
prompt: &'a str,
context: &'a Context,
emitter: &'a Arc<Emitter>,
sandbox: &'a Arc<dyn fabro_agent::Sandbox>,
) -> CodergenRunRequest<'a> {
CodergenRunRequest {
node,
prompt,
context,
thread_id: None,
emitter,
sandbox,
tool_hooks: None,
cancel_token: CancellationToken::new(),
}
}
fn test_run_id(label: &str) -> RunId {
let mut hasher = DefaultHasher::new();
label.hash(&mut hasher);
@ -1606,22 +1627,12 @@ struct MockCodergenBackend;
#[async_trait::async_trait]
impl CodergenBackend for MockCodergenBackend {
async fn run(
&self,
node: &Node,
prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn fabro_agent::Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: tokio_util::sync::CancellationToken,
) -> Result<CodergenResult, Error> {
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
Ok(CodergenResult::Text {
text: format!(
"Response for {}: processed prompt '{}'",
node.id,
&prompt[..prompt.len().min(50)]
request.node.id,
&request.prompt[..request.prompt.len().min(50)]
),
usage: None,
files_touched: Vec::new(),
@ -6283,10 +6294,9 @@ mod real_llm {
use fabro_llm::providers::OpenAiAdapter;
use fabro_llm::types::{Message, Request};
use fabro_types::WorkflowSettings;
use fabro_workflow::context::Context;
use fabro_workflow::error::Error;
use fabro_workflow::handler::agent::{
AgentHandler, CodergenBackend, CodergenResult, OneShotRequest,
AgentHandler, CodergenBackend, CodergenResult, CodergenRunRequest, OneShotRequest,
};
use tokio_util::sync::CancellationToken;
@ -6298,18 +6308,8 @@ mod real_llm {
#[async_trait]
impl CodergenBackend for LlmCodergenBackend {
async fn run(
&self,
_node: &Node,
prompt: &str,
_context: &Context,
_thread_id: Option<&str>,
_emitter: &Arc<Emitter>,
_sandbox: &Arc<dyn fabro_agent::Sandbox>,
_tool_hooks: Option<Arc<dyn fabro_agent::ToolHookCallback>>,
_cancel_token: tokio_util::sync::CancellationToken,
) -> Result<CodergenResult, Error> {
self.complete(prompt).await
async fn run(&self, request: CodergenRunRequest<'_>) -> Result<CodergenResult, Error> {
self.complete(request.prompt).await
}
async fn one_shot(&self, request: OneShotRequest<'_>) -> Result<CodergenResult, Error> {
@ -9804,16 +9804,13 @@ async fn cli_backend_run_writes_prompt_and_calls_exec() {
let emitter = Arc::new(Emitter::default());
let result = backend
.run(
.run(codergen_run_request(
&node,
"Fix the authentication bug",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
))
.await
.expect("CLI backend should succeed");
@ -9878,16 +9875,13 @@ async fn cli_backend_run_detects_changed_files() {
let emitter = Arc::new(Emitter::default());
let result = backend
.run(
.run(codergen_run_request(
&node,
"Add a new feature",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
))
.await
.expect("CLI backend should succeed");
@ -9912,16 +9906,13 @@ async fn cli_backend_run_with_codex_provider() {
let emitter = Arc::new(Emitter::default());
let result = backend
.run(
.run(codergen_run_request(
&node,
"Build the API",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
))
.await
.expect("CLI backend should succeed");
@ -10080,16 +10071,13 @@ async fn cli_backend_run_fails_on_nonzero_exit() {
let _ = env; // unused, just for the above struct
let result = backend
.run(
.run(codergen_run_request(
&node,
"do something",
&context,
None,
&emitter,
&failing_env,
None,
CancellationToken::new(),
)
))
.await;
let err = match result {
@ -10118,16 +10106,13 @@ async fn cli_backend_run_fails_on_unparseable_output() {
let emitter = Arc::new(Emitter::default());
let result = backend
.run(
.run(codergen_run_request(
&node,
"do something",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
))
.await;
let err = match result {
@ -10160,16 +10145,9 @@ async fn cli_backend_run_uses_node_model_override() {
let emitter = Arc::new(Emitter::default());
backend
.run(
&node,
"test",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
.run(codergen_run_request(
&node, "test", &context, &emitter, &env,
))
.await
.expect("should succeed");
@ -10210,16 +10188,9 @@ async fn cli_backend_run_uses_node_provider_override() {
let emitter = Arc::new(Emitter::default());
backend
.run(
&node,
"test",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
.run(codergen_run_request(
&node, "test", &context, &emitter, &env,
))
.await
.expect("should succeed");
@ -10244,16 +10215,9 @@ async fn cli_backend_run_returns_text_and_usage() {
let emitter = Arc::new(Emitter::default());
let result = backend
.run(
&node,
"test",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
.run(codergen_run_request(
&node, "test", &context, &emitter, &env,
))
.await
.expect("should succeed");
@ -10297,16 +10261,13 @@ async fn backend_router_delegates_to_cli_for_cli_node() {
let emitter = Arc::new(Emitter::default());
let result = router
.run(
.run(codergen_run_request(
&node,
"Fix the bug",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
))
.await
.expect("router should succeed");
@ -10340,16 +10301,13 @@ async fn backend_router_delegates_to_api_for_normal_node() {
let emitter = Arc::new(Emitter::default());
let result = router
.run(
.run(codergen_run_request(
&node,
"Plan the work",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
))
.await
.expect("router should succeed");
@ -10386,16 +10344,9 @@ async fn backend_router_delegates_to_cli_for_backend_attr() {
let emitter = Arc::new(Emitter::default());
let result = router
.run(
&node,
"Build it",
&context,
None,
&emitter,
&env,
None,
CancellationToken::new(),
)
.run(codergen_run_request(
&node, "Build it", &context, &emitter, &env,
))
.await
.expect("router should succeed");
@ -10448,17 +10399,13 @@ async fn backend_router_delegates_to_acp_for_acp_node() {
)),
);
let context = Context::new();
let emitter = Arc::new(Emitter::default());
let result = router
.run(
&node,
"Build it",
&Context::new(),
None,
&Arc::new(Emitter::default()),
&env,
None,
CancellationToken::new(),
)
.run(codergen_run_request(
&node, "Build it", &context, &emitter, &env,
))
.await
.expect("router should succeed");