mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 15:40:18 +00:00
Strategy C parallel deployment: when ENGINE_V2=true env var is set, user messages route through the engine instead of the existing agentic loop. All existing behavior is unchanged when the flag is off. Bridge module (src/bridge/): - LlmBridgeAdapter: wraps LlmProvider as engine LlmBackend, converts ThreadMessage↔ChatMessage, ActionDef↔ToolDefinition, depth-based model routing (primary vs cheap_llm) - EffectBridgeAdapter: wraps ToolRegistry+SafetyLayer as EffectExecutor, routes tool calls through existing execute_tool_with_safety pipeline - InMemoryStore: HashMap-backed Store impl (no DB tables needed yet) - EngineRouter: is_engine_v2_enabled() + handle_with_engine() that builds engine from Agent deps and processes messages end-to-end Integration touchpoint (4 lines in agent_loop.rs): After hook processing, before session resolution, check ENGINE_V2 flag and route UserInput through the engine path. Accessor visibility widened: llm(), cheap_llm(), safety(), tools() changed from pub(super) to pub(crate) for bridge access. 85 engine tests + main crate clippy clean. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
164 lines
5.3 KiB
Rust
164 lines
5.3 KiB
Rust
//! Engine v2 router — handles user messages via the engine when enabled.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use tracing::{debug, info};
|
|
|
|
use ironclaw_engine::{
|
|
Capability, CapabilityRegistry, LeaseManager, PolicyEngine, Project, Store,
|
|
ThreadConfig, ThreadManager, ThreadOutcome, ThreadType,
|
|
};
|
|
|
|
use crate::agent::Agent;
|
|
use crate::bridge::effect_adapter::EffectBridgeAdapter;
|
|
use crate::bridge::llm_adapter::LlmBridgeAdapter;
|
|
use crate::bridge::store_adapter::InMemoryStore;
|
|
use crate::channels::IncomingMessage;
|
|
use crate::error::Error;
|
|
|
|
/// Check if the engine v2 is enabled via `ENGINE_V2=true` environment variable.
|
|
pub fn is_engine_v2_enabled() -> bool {
|
|
std::env::var("ENGINE_V2")
|
|
.map(|v| v == "true" || v == "1")
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// Handle a user message through the engine v2 pipeline.
|
|
///
|
|
/// This is the engine-based equivalent of `Agent::process_user_input()`.
|
|
/// It builds bridge adapters from the agent's existing dependencies,
|
|
/// creates an engine `ThreadManager`, spawns a thread, and waits for
|
|
/// the result.
|
|
pub async fn handle_with_engine(
|
|
agent: &Agent,
|
|
message: &IncomingMessage,
|
|
content: &str,
|
|
) -> Result<Option<String>, Error> {
|
|
info!(
|
|
user_id = %message.user_id,
|
|
channel = %message.channel,
|
|
"engine v2: handling message"
|
|
);
|
|
|
|
// Build bridge adapters from agent's existing dependencies
|
|
let llm_adapter = Arc::new(LlmBridgeAdapter::new(
|
|
agent.llm().clone(),
|
|
Some(agent.cheap_llm().clone()),
|
|
));
|
|
|
|
let effect_adapter = Arc::new(EffectBridgeAdapter::new(
|
|
agent.tools().clone(),
|
|
agent.safety().clone(),
|
|
));
|
|
|
|
let store = Arc::new(InMemoryStore::new());
|
|
|
|
// Build capability registry from available tools
|
|
let mut capabilities = CapabilityRegistry::new();
|
|
let tool_defs = agent.tools().tool_definitions().await;
|
|
if !tool_defs.is_empty() {
|
|
capabilities.register(Capability {
|
|
name: "tools".into(),
|
|
description: "Available tools".into(),
|
|
actions: tool_defs
|
|
.into_iter()
|
|
.map(|td| ironclaw_engine::ActionDef {
|
|
name: td.name,
|
|
description: td.description,
|
|
parameters_schema: td.parameters,
|
|
effects: vec![],
|
|
requires_approval: false,
|
|
})
|
|
.collect(),
|
|
knowledge: vec![],
|
|
policies: vec![],
|
|
});
|
|
}
|
|
|
|
let leases = Arc::new(LeaseManager::new());
|
|
let policy = Arc::new(PolicyEngine::new());
|
|
|
|
// Create thread manager
|
|
let thread_manager = Arc::new(ThreadManager::new(
|
|
llm_adapter,
|
|
effect_adapter,
|
|
store.clone(),
|
|
Arc::new(capabilities),
|
|
leases,
|
|
policy,
|
|
));
|
|
|
|
// Create a default project for this session
|
|
let project = Project::new(
|
|
format!("{}:{}", message.channel, message.user_id),
|
|
"Auto-created project",
|
|
);
|
|
let project_id = project.id;
|
|
store.save_project(&project).await.map_err(|e| {
|
|
crate::error::Error::from(crate::error::JobError::ContextError {
|
|
id: uuid::Uuid::nil(),
|
|
reason: format!("engine v2 store error: {e}"),
|
|
})
|
|
})?;
|
|
|
|
// Spawn a thread for this message
|
|
let config = ThreadConfig::default();
|
|
let thread_id = thread_manager
|
|
.spawn_thread(
|
|
content,
|
|
ThreadType::Foreground,
|
|
project_id,
|
|
config,
|
|
None,
|
|
&message.user_id,
|
|
)
|
|
.await
|
|
.map_err(|e| {
|
|
crate::error::Error::from(crate::error::JobError::ContextError {
|
|
id: uuid::Uuid::nil(),
|
|
reason: format!("engine v2 spawn error: {e}"),
|
|
})
|
|
})?;
|
|
|
|
debug!(thread_id = %thread_id, "engine v2: thread spawned, waiting for completion");
|
|
|
|
// Wait for the thread to complete
|
|
let outcome = thread_manager.join_thread(thread_id).await.map_err(|e| {
|
|
crate::error::Error::from(crate::error::JobError::ContextError {
|
|
id: uuid::Uuid::nil(),
|
|
reason: format!("engine v2 join error: {e}"),
|
|
})
|
|
})?;
|
|
|
|
// Convert outcome to response
|
|
match outcome {
|
|
ThreadOutcome::Completed { response } => {
|
|
debug!(thread_id = %thread_id, "engine v2: thread completed");
|
|
Ok(response)
|
|
}
|
|
ThreadOutcome::Stopped => {
|
|
debug!(thread_id = %thread_id, "engine v2: thread stopped");
|
|
Ok(Some("Thread was stopped.".into()))
|
|
}
|
|
ThreadOutcome::MaxIterations => {
|
|
debug!(thread_id = %thread_id, "engine v2: max iterations");
|
|
Ok(Some("Reached maximum iterations without completing.".into()))
|
|
}
|
|
ThreadOutcome::Failed { error } => {
|
|
debug!(thread_id = %thread_id, error = %error, "engine v2: thread failed");
|
|
Ok(Some(format!("Error: {error}")))
|
|
}
|
|
ThreadOutcome::NeedApproval {
|
|
action_name,
|
|
call_id: _,
|
|
parameters: _,
|
|
} => {
|
|
// Phase 6: approval flow not yet wired — return as message
|
|
debug!(thread_id = %thread_id, action = %action_name, "engine v2: approval needed (not yet supported)");
|
|
Ok(Some(format!(
|
|
"Action '{action_name}' requires approval (engine v2 approval flow not yet implemented)"
|
|
)))
|
|
}
|
|
}
|
|
}
|