use std::sync::Arc;
use tracing::{debug, info};
use clawft_platform::Platform;
use clawft_types::config::Config;
use clawft_types::routing::RoutingConfig;
use crate::agent::context::ContextBuilder;
use crate::agent::loop_core::{AgentLoop, AutoDelegation};
use crate::agent::memory::MemoryStore;
use crate::agent::skills::SkillsLoader;
use crate::bus::MessageBus;
use crate::pipeline::assembler::TokenBudgetAssembler;
use crate::pipeline::classifier::KeywordClassifier;
use crate::pipeline::cost_tracker::CostTracker;
use crate::pipeline::rate_limiter::RateLimiter;
use crate::pipeline::router::StaticRouter;
use crate::pipeline::tiered_router::TieredRouter;
use crate::pipeline::traits::{ModelRouter, Pipeline, PipelineRegistry};
use crate::pipeline::transport::OpenAiCompatTransport;
use crate::session::SessionManager;
use crate::tools::registry::ToolRegistry;
pub struct AppContext<P: Platform> {
config: Config,
platform: Arc<P>,
bus: Arc<MessageBus>,
sessions: Arc<SessionManager<P>>,
tools: Arc<ToolRegistry>,
pipeline: PipelineRegistry,
context: ContextBuilder<P>,
memory: Arc<MemoryStore<P>>,
skills: Arc<SkillsLoader<P>>,
auto_delegation: Option<Arc<dyn AutoDelegation>>,
agent_router: Option<Arc<crate::agent_routing::AgentRouter>>,
#[cfg(feature = "native")]
agent_bus: Option<Arc<crate::agent_bus::AgentBus>>,
global_routing: Option<RoutingConfig>,
workspace_routing: Option<RoutingConfig>,
}
impl<P: Platform> AppContext<P> {
pub async fn new(config: Config, platform: Arc<P>) -> clawft_types::Result<Self> {
info!("bootstrapping application context");
let bus = Arc::new(MessageBus::new());
debug!("message bus created");
let sessions = SessionManager::new(platform.clone()).await?;
debug!("session manager initialized");
let memory = match crate::workspace::discover_workspace() {
Some(ws_root) if ws_root.join(".clawft").is_dir() => {
debug!(
workspace = %ws_root.display(),
"routing memory through workspace overlay"
);
Arc::new(MemoryStore::for_workspace(&ws_root, platform.clone()))
}
_ => Arc::new(MemoryStore::new(platform.clone())?),
};
debug!(
memory_path = %memory.memory_path().display(),
"memory store initialized"
);
let mut skills_loader = match crate::workspace::discover_workspace() {
Some(ws_root) if ws_root.join(".clawft").is_dir() => {
debug!(
workspace = %ws_root.display(),
"routing skills through workspace overlay"
);
SkillsLoader::for_workspace(&ws_root, platform.clone())
}
_ => SkillsLoader::new(platform.clone())?,
};
for candidate in [
config.workspace_path().join("skills"),
std::env::current_dir().unwrap_or_default().join("skills"),
] {
if candidate.is_dir() {
skills_loader.add_extra_dir(candidate);
}
}
let skills = Arc::new(skills_loader);
debug!(
skills_dir = %skills.skills_dir().display(),
"skills loader initialized"
);
let context = ContextBuilder::new(
config.agents.clone(),
memory.clone(),
skills.clone(),
platform.clone(),
);
debug!("context builder created");
let tools = Arc::new(ToolRegistry::new());
let pipeline = build_default_pipeline(&config);
debug!("default pipeline wired");
info!("bootstrap complete");
Ok(Self {
config,
platform,
bus,
sessions: Arc::new(sessions),
tools,
pipeline,
context,
memory,
skills,
auto_delegation: None,
agent_router: None,
#[cfg(feature = "native")]
agent_bus: None,
global_routing: None,
workspace_routing: None,
})
}
pub fn with_routing_layers(
mut self,
global: RoutingConfig,
workspace: Option<RoutingConfig>,
) -> Self {
if let Some(ref ws) = workspace {
let violations =
crate::pipeline::permissions::PermissionResolver::validate_workspace_ceiling(
&global, ws,
);
if !violations.is_empty() {
tracing::warn!(
violations = ?violations,
"workspace routing exceeds global permission ceiling; \
elevated grants will be silently clamped at resolve time"
);
}
}
self.global_routing = Some(global);
self.workspace_routing = workspace;
self
}
pub fn into_agent_loop(self) -> AgentLoop<P>
where
P: 'static,
{
let global = self
.global_routing
.as_ref()
.unwrap_or(&self.config.routing);
let resolver = crate::pipeline::permissions::PermissionResolver::new(
global,
self.workspace_routing.as_ref(),
);
let sink = Arc::new(crate::agent::local_file_sink::LocalFileSink::with_dir(
self.platform.clone(),
self.sessions.sessions_dir().clone(),
));
let mut agent = AgentLoop::new(
self.config.agents,
self.platform,
self.bus,
self.pipeline,
self.tools.clone(),
self.context,
resolver,
)
.with_sink(sink);
if let Some(delegation) = self.auto_delegation {
agent = agent.with_auto_delegation(delegation);
}
agent
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn platform(&self) -> &Arc<P> {
&self.platform
}
pub fn bus(&self) -> &Arc<MessageBus> {
&self.bus
}
pub fn tools_mut(&mut self) -> &mut ToolRegistry {
Arc::get_mut(&mut self.tools)
.expect("tools already shared -- register tools before cloning Arc")
}
pub fn tools(&self) -> &Arc<ToolRegistry> {
&self.tools
}
pub fn tools_arc(&self) -> Arc<ToolRegistry> {
self.tools.clone()
}
pub fn sessions(&self) -> &Arc<SessionManager<P>> {
&self.sessions
}
pub fn memory(&self) -> &Arc<MemoryStore<P>> {
&self.memory
}
pub fn skills(&self) -> &Arc<SkillsLoader<P>> {
&self.skills
}
#[cfg(feature = "native")]
pub async fn start_skill_watcher(
&self,
) -> Option<crate::agent::skill_watcher::SkillWatcherHandle> {
use std::sync::Arc as ArcAlias;
use tokio::sync::RwLock;
use crate::agent::skill_watcher::{SkillWatcherConfig, start_watching};
use crate::agent::skills_v2::SkillRegistry;
let workspace_dir = Some(self.skills.skills_dir().clone());
let registry = match SkillRegistry::discover(workspace_dir.as_deref(), None, vec![]).await {
Ok(r) => ArcAlias::new(RwLock::new(r)),
Err(e) => {
tracing::warn!(error = %e, "skill watcher: initial discover failed; not starting");
return None;
}
};
let config = SkillWatcherConfig {
workspace_dir: workspace_dir.clone(),
user_dir: None,
extra_dirs: Vec::new(),
debounce: std::time::Duration::from_millis(500),
builtin_skills: Vec::new(),
trust_workspace: true,
};
match start_watching(config, registry) {
Ok(handle) => {
tracing::info!(
skills_dir = %self.skills.skills_dir().display(),
"skill hot-reload watcher started"
);
Some(handle)
}
Err(e) => {
tracing::warn!(error = %e, "failed to start skill watcher");
None
}
}
}
pub fn set_auto_delegation(&mut self, delegation: Arc<dyn AutoDelegation>) {
self.auto_delegation = Some(delegation);
}
pub fn set_agent_router(&mut self, router: Arc<crate::agent_routing::AgentRouter>) {
self.agent_router = Some(router);
}
pub fn agent_router(&self) -> Option<&Arc<crate::agent_routing::AgentRouter>> {
self.agent_router.as_ref()
}
#[cfg(feature = "native")]
pub fn set_agent_bus(&mut self, bus: Arc<crate::agent_bus::AgentBus>) {
self.agent_bus = Some(bus);
}
#[cfg(feature = "native")]
pub fn enable_agent_bus(&mut self, capacity: Option<usize>) -> Arc<crate::agent_bus::AgentBus> {
if let Some(bus) = self.agent_bus.as_ref() {
return bus.clone();
}
let bus = Arc::new(match capacity {
Some(c) => crate::agent_bus::AgentBus::with_capacity(c),
None => crate::agent_bus::AgentBus::new(),
});
self.agent_bus = Some(bus.clone());
info!(
capacity = bus.inbox_capacity(),
"agent bus enabled (WEFT-185)"
);
bus
}
#[cfg(feature = "native")]
pub fn agent_bus(&self) -> Option<&Arc<crate::agent_bus::AgentBus>> {
self.agent_bus.as_ref()
}
pub fn set_pipeline(&mut self, pipeline: PipelineRegistry) {
self.pipeline = pipeline;
}
pub fn trajectory_learner(
&self,
) -> Option<Arc<crate::pipeline::learner::TrajectoryLearner>> {
self.pipeline.trajectory_learner().cloned()
}
pub fn decision_history(
&self,
) -> Arc<crate::pipeline::decision_history::RoutingDecisionHistory> {
self.pipeline.decision_history().clone()
}
#[cfg(feature = "native")]
pub fn enable_live_llm(&mut self) {
let pipeline = build_live_pipeline(&self.config);
self.set_pipeline(pipeline);
}
}
#[cfg(feature = "native")]
pub fn build_live_pipeline(config: &Config) -> PipelineRegistry {
crate::pipeline::llm_adapter::build_live_pipeline(config)
}
fn build_default_pipeline(config: &Config) -> PipelineRegistry {
let classifier = Arc::new(KeywordClassifier::new());
let router: Arc<dyn ModelRouter> = build_router_from_config(config);
let assembler = Arc::new(TokenBudgetAssembler::new(
config.agents.defaults.max_tokens.max(1) as usize,
));
let transport = build_default_transport();
let scorer = crate::pipeline::build_scorer(&config.pipeline);
let (learner, trajectory) = crate::pipeline::build_learner_parts(&config.pipeline);
let pipeline = Pipeline {
classifier,
router,
assembler,
transport,
scorer,
learner,
};
let mut registry = PipelineRegistry::new(pipeline);
if let Some(t) = trajectory {
registry = registry.with_trajectory_learner(t);
}
registry
}
#[cfg(feature = "native")]
fn build_default_transport() -> Arc<OpenAiCompatTransport> {
use clawft_service_llm::{LlmClient, LlmConfig};
use crate::pipeline::service_llm_adapter::ServiceLlmAdapter;
use crate::pipeline::transport::LlmProvider;
let llm_config = LlmConfig::from_env();
match LlmClient::new(llm_config) {
Ok(client) => {
let adapter: Arc<dyn LlmProvider> = Arc::new(ServiceLlmAdapter::from_client(client));
tracing::info!(
"pipeline: transport wired to clawft-service-llm (LlmClient over llama-server)"
);
Arc::new(OpenAiCompatTransport::with_provider(adapter))
}
Err(e) => {
tracing::warn!(
error = %e,
"pipeline: failed to construct LlmClient — falling back to stub transport"
);
Arc::new(OpenAiCompatTransport::new())
}
}
}
#[cfg(not(feature = "native"))]
fn build_default_transport() -> Arc<OpenAiCompatTransport> {
Arc::new(OpenAiCompatTransport::new())
}
#[cfg(feature = "browser")]
pub fn build_browser_pipeline(
config: &Config,
client: Arc<clawft_llm::browser_transport::BrowserLlmClient>,
) -> PipelineRegistry {
use crate::pipeline::browser_llm_adapter::BrowserLlmAdapter;
use crate::pipeline::transport::LlmProvider;
let adapter: Arc<dyn LlmProvider> = Arc::new(BrowserLlmAdapter::new(client));
let transport = Arc::new(OpenAiCompatTransport::with_provider(adapter));
let classifier = Arc::new(KeywordClassifier::new());
let router: Arc<dyn ModelRouter> = build_router_from_config(config);
let assembler = Arc::new(TokenBudgetAssembler::new(
config.agents.defaults.max_tokens.max(1) as usize,
));
let scorer = crate::pipeline::build_scorer(&config.pipeline);
let (learner, trajectory) = crate::pipeline::build_learner_parts(&config.pipeline);
let mut registry = PipelineRegistry::new(Pipeline {
classifier,
router,
assembler,
transport,
scorer,
learner,
});
if let Some(t) = trajectory {
registry = registry.with_trajectory_learner(t);
}
registry
}
#[cfg(feature = "native")]
#[allow(clippy::too_many_arguments)]
pub async fn build_daemon_agent_loop(
llm: clawft_service_llm::SharedLlmClient,
tools: Arc<ToolRegistry>,
_identity_loader: Arc<crate::agent::identity::IdentityLoader<clawft_platform::NativePlatform>>,
workspace: &std::path::Path,
agent_id: Option<String>,
context_router: Option<Arc<dyn crate::agent::context_router::ContextRouter>>,
gate: Option<Arc<dyn crate::agent::gate::EffectGate>>,
sink: Option<Arc<dyn crate::agent::sink::ConversationSink>>,
identity_provider: Option<Arc<dyn crate::agent::identity::IdentityProvider>>,
routing: Option<&clawft_types::routing::RoutingConfig>,
workspace_routing: Option<&clawft_types::routing::RoutingConfig>,
graft_provider: Option<Arc<dyn crate::agent::graft::ContextGraftProvider>>,
soul_journal: Option<Arc<dyn crate::agent::soul_journal::SoulJournal>>,
routing_log: Option<Arc<dyn crate::agent::routing_log::RouterDecisionLog>>,
) -> Arc<crate::agent::loop_core::AgentLoop<clawft_platform::NativePlatform>> {
use clawft_platform::NativePlatform;
use crate::pipeline::service_llm_adapter::ServiceLlmAdapter;
use crate::pipeline::transport::{LlmProvider, OpenAiCompatTransport};
let mut config = Config::default();
config.agents.defaults.workspace = workspace.display().to_string();
{
let upstream_model = llm.read().await.config().model.clone();
if !upstream_model.is_empty() {
config.agents.defaults.model = upstream_model;
}
}
if let Some(r) = routing {
config.routing = r.clone();
}
let platform = Arc::new(NativePlatform::new());
let bus = Arc::new(MessageBus::new());
let memory = Arc::new(crate::agent::memory::MemoryStore::for_workspace(
workspace,
platform.clone(),
));
let skills_loader =
crate::agent::skills::SkillsLoader::for_workspace(workspace, platform.clone());
let skills = Arc::new(skills_loader);
let context = ContextBuilder::new(config.agents.clone(), memory, skills, platform.clone());
let adapter: Arc<dyn LlmProvider> = Arc::new(ServiceLlmAdapter::new(llm));
let transport = Arc::new(OpenAiCompatTransport::with_provider(adapter));
let classifier = Arc::new(KeywordClassifier::new());
let router: Arc<dyn ModelRouter> = build_router_from_config(&config);
let assembler = Arc::new(TokenBudgetAssembler::new(
config.agents.defaults.max_tokens.max(1) as usize,
));
let scorer = crate::pipeline::build_scorer(&config.pipeline);
let (learner, trajectory) = crate::pipeline::build_learner_parts(&config.pipeline);
let mut pipeline = PipelineRegistry::new(Pipeline {
classifier,
router,
assembler,
transport,
scorer,
learner,
});
if let Some(t) = trajectory {
pipeline = pipeline.with_trajectory_learner(t);
}
if let (Some(global), Some(ws)) = (routing, workspace_routing) {
let violations =
crate::pipeline::permissions::PermissionResolver::validate_workspace_ceiling(
global, ws,
);
if !violations.is_empty() {
tracing::warn!(
violations = ?violations,
"workspace routing exceeds global permission ceiling; \
elevated grants will be silently clamped at resolve time"
);
}
}
let resolver = crate::pipeline::permissions::PermissionResolver::new(
&config.routing,
workspace_routing,
);
let binding_thread_mode = config.agents.binding_thread_mode;
let mut agent = crate::agent::loop_core::AgentLoop::new(
config.agents,
platform,
bus,
pipeline,
tools,
context,
resolver,
)
.with_chat_id_as_conv_id();
if let Some(s) = sink {
agent = agent.with_sink(s);
}
if let Some(provider) = identity_provider {
let builder = Arc::new(
crate::agent::system_prompt::SystemPromptBuilder::new(
provider,
workspace.to_path_buf(),
)
.with_binding_thread_mode(binding_thread_mode),
);
agent = agent.with_system_prompt_builder(builder);
}
if let Some(g) = gate {
agent = agent.with_gate(g);
}
if let Some(id) = agent_id {
agent = agent.with_daemon_agent_id(id);
}
if let Some(r) = context_router {
agent = agent.with_context_router(r);
}
if let Some(gp) = graft_provider {
agent = agent.with_graft_provider(gp);
}
if let Some(j) = soul_journal {
agent = agent.with_soul_journal(j);
}
if let Some(rlog) = routing_log {
agent = agent.with_routing_log(rlog);
}
Arc::new(agent)
}
fn build_router_from_config(config: &Config) -> Arc<dyn ModelRouter> {
if config.routing.mode == "tiered" {
let routing = config.routing.clone();
let cost_tracker = Arc::new(CostTracker::new(routing.cost_budgets.reset_hour_utc));
let rate_limiter = Arc::new(RateLimiter::new(
routing.rate_limiting.window_seconds,
routing.rate_limiting.global_rate_limit_rpm,
));
Arc::new(
TieredRouter::new(routing)
.with_cost_tracker(cost_tracker)
.with_rate_limiter(rate_limiter),
)
} else {
Arc::new(StaticRouter::from_config(&config.agents))
}
}
#[cfg(test)]
mod tests {
use super::*;
use clawft_platform::NativePlatform;
use clawft_types::config::{AgentDefaults, AgentsConfig};
use std::sync::Arc;
fn test_config() -> Config {
Config {
agents: AgentsConfig {
defaults: AgentDefaults {
workspace: "~/.clawft/workspace".into(),
model: clawft_types::config::DEFAULT_LOCAL_LLM_MODEL_ROUTED.into(),
max_tokens: 4096,
temperature: 0.7,
max_tool_iterations: 10,
memory_window: 50,
},
..AgentsConfig::default()
},
..Config::default()
}
}
#[tokio::test]
async fn new_creates_app_context() {
let platform = Arc::new(NativePlatform::new());
let ctx = AppContext::new(test_config(), platform).await;
assert!(ctx.is_ok());
}
#[tokio::test]
async fn config_accessor() {
let platform = Arc::new(NativePlatform::new());
let ctx = AppContext::new(test_config(), platform).await.unwrap();
assert_eq!(ctx.config().agents.defaults.model, clawft_types::config::DEFAULT_LOCAL_LLM_MODEL_ROUTED);
assert_eq!(ctx.config().agents.defaults.max_tokens, 4096);
}
#[tokio::test]
async fn platform_accessor() {
let platform = Arc::new(NativePlatform::new());
let ctx = AppContext::new(test_config(), platform).await.unwrap();
let _p = ctx.platform();
}
#[tokio::test]
async fn bus_accessor() {
let platform = Arc::new(NativePlatform::new());
let ctx = AppContext::new(test_config(), platform).await.unwrap();
let bus = ctx.bus();
let _tx = bus.inbound_sender();
}
#[cfg(feature = "native")]
#[tokio::test]
async fn enable_agent_bus_constructs_once() {
let platform = Arc::new(NativePlatform::new());
let mut ctx = AppContext::new(test_config(), platform).await.unwrap();
assert!(ctx.agent_bus().is_none());
let bus1 = ctx.enable_agent_bus(Some(32));
assert_eq!(bus1.inbox_capacity(), 32);
assert!(ctx.agent_bus().is_some());
let bus2 = ctx.enable_agent_bus(Some(99));
assert_eq!(bus2.inbox_capacity(), 32);
assert!(Arc::ptr_eq(&bus1, &bus2));
}
#[tokio::test]
async fn tools_starts_empty() {
let platform = Arc::new(NativePlatform::new());
let ctx = AppContext::new(test_config(), platform).await.unwrap();
assert!(ctx.tools().is_empty());
}
#[tokio::test]
async fn tools_mut_allows_registration() {
let platform = Arc::new(NativePlatform::new());
let mut ctx = AppContext::new(test_config(), platform).await.unwrap();
use crate::tools::registry::Tool;
use async_trait::async_trait;
struct DummyTool;
#[async_trait]
impl Tool for DummyTool {
fn name(&self) -> &str {
"dummy"
}
fn description(&self) -> &str {
"A dummy tool"
}
fn parameters(&self) -> serde_json::Value {
serde_json::json!({"type": "object", "properties": {}})
}
async fn execute(
&self,
_args: serde_json::Value,
) -> Result<serde_json::Value, crate::tools::registry::ToolError> {
Ok(serde_json::json!({}))
}
}
ctx.tools_mut().register(Arc::new(DummyTool));
assert_eq!(ctx.tools().len(), 1);
assert_eq!(ctx.tools().list(), vec!["dummy"]);
}
#[tokio::test]
async fn memory_accessor() {
let platform = Arc::new(NativePlatform::new());
let ctx = AppContext::new(test_config(), platform).await.unwrap();
let memory = ctx.memory();
assert!(memory.memory_path().is_absolute());
}
#[tokio::test]
async fn skills_accessor() {
let platform = Arc::new(NativePlatform::new());
let ctx = AppContext::new(test_config(), platform).await.unwrap();
let skills = ctx.skills();
assert!(skills.skills_dir().is_absolute());
}
#[tokio::test]
async fn with_routing_layers_wires_permission_ceiling() {
use clawft_types::routing::{PermissionLevelConfig, PermissionsConfig, RoutingConfig};
let platform = Arc::new(NativePlatform::new());
let global = RoutingConfig {
permissions: PermissionsConfig {
user: PermissionLevelConfig {
level: Some(1),
tool_access: Some(vec!["read_file".into()]),
cost_budget_daily_usd: Some(5.0),
..Default::default()
},
..Default::default()
},
..Default::default()
};
let workspace = RoutingConfig {
permissions: PermissionsConfig {
user: PermissionLevelConfig {
level: Some(2),
tool_access: Some(vec!["read_file".into(), "shell".into()]),
cost_budget_daily_usd: Some(999.0),
..Default::default()
},
..Default::default()
},
..Default::default()
};
let ctx = AppContext::new(test_config(), platform)
.await
.unwrap()
.with_routing_layers(global.clone(), Some(workspace.clone()));
let resolver = crate::pipeline::permissions::PermissionResolver::new(
ctx.global_routing.as_ref().unwrap(),
ctx.workspace_routing.as_ref(),
);
let perms = resolver.resolve("someone", "telegram", true);
assert!(
perms.level <= 1,
"workspace level elevation must clamp, got {}",
perms.level
);
assert!(
!perms.tool_access.contains(&"shell".to_string()),
"workspace-only tool must be filtered"
);
assert!(perms.cost_budget_daily_usd <= 5.0);
}
#[tokio::test]
async fn into_agent_loop_produces_valid_loop() {
let platform = Arc::new(NativePlatform::new());
let ctx = AppContext::new(test_config(), platform).await.unwrap();
let agent = ctx.into_agent_loop();
assert_eq!(agent.config().defaults.model, clawft_types::config::DEFAULT_LOCAL_LLM_MODEL_ROUTED);
assert_eq!(agent.config().defaults.max_tokens, 4096);
}
#[tokio::test]
async fn set_pipeline_replaces_default() {
let platform = Arc::new(NativePlatform::new());
let mut ctx = AppContext::new(test_config(), platform).await.unwrap();
let custom_pipeline = build_default_pipeline(&test_config());
ctx.set_pipeline(custom_pipeline);
let agent = ctx.into_agent_loop();
assert_eq!(agent.config().defaults.model, clawft_types::config::DEFAULT_LOCAL_LLM_MODEL_ROUTED);
}
#[tokio::test]
async fn default_config_bootstrap() {
let platform = Arc::new(NativePlatform::new());
let ctx = AppContext::new(Config::default(), platform).await;
assert!(ctx.is_ok());
}
#[test]
fn build_default_pipeline_creates_registry() {
let config = test_config();
let _registry = build_default_pipeline(&config);
}
#[test]
fn build_default_pipeline_with_defaults() {
let config = Config::default();
let _registry = build_default_pipeline(&config);
}
#[tokio::test]
async fn bus_clone_survives_into_agent_loop() {
let platform = Arc::new(NativePlatform::new());
let ctx = AppContext::new(test_config(), platform).await.unwrap();
let tx = ctx.bus().inbound_sender();
let _agent = ctx.into_agent_loop();
use chrono::Utc;
use clawft_types::event::InboundMessage;
use std::collections::HashMap;
let msg = InboundMessage {
channel: "test".into(),
sender_id: "user1".into(),
chat_id: "chat1".into(),
content: "hello".into(),
timestamp: Utc::now(),
media: vec![],
metadata: HashMap::new(),
};
assert!(tx.send(msg).await.is_ok());
}
#[tokio::test]
async fn app_context_is_send() {
fn assert_send<T: Send>() {}
assert_send::<AppContext<NativePlatform>>();
}
#[tokio::test]
async fn build_live_pipeline_transport_is_configured() {
use crate::pipeline::traits::{LlmMessage, TaskType, TransportRequest};
let config = test_config();
let registry = build_live_pipeline(&config);
let pipeline = registry.get(&TaskType::Unknown);
let transport_req = TransportRequest {
provider: "anthropic".into(),
model: "claude-opus-4-5".into(),
messages: vec![LlmMessage {
role: "user".into(),
content: "test".into(),
tool_call_id: None,
tool_calls: None,
}],
tools: vec![],
max_tokens: Some(10),
temperature: Some(0.0),
tool_choice: None,
};
let result = pipeline.transport.complete(&transport_req).await;
match result {
Ok(_) => {} Err(e) => {
let msg = e.to_string();
assert!(
!msg.contains("transport not configured"),
"live pipeline should not use stub transport, but got: {msg}"
);
}
}
}
#[tokio::test]
async fn app_context_enable_live_llm() {
let platform = Arc::new(NativePlatform::new());
let mut ctx = AppContext::new(test_config(), platform).await.unwrap();
ctx.enable_live_llm();
let agent = ctx.into_agent_loop();
assert_eq!(agent.config().defaults.model, clawft_types::config::DEFAULT_LOCAL_LLM_MODEL_ROUTED);
}
#[test]
fn default_pipeline_builds_with_service_llm_transport() {
use crate::pipeline::traits::TaskType;
let config = test_config();
let registry = build_default_pipeline(&config);
let _pipeline = registry.get(&TaskType::Unknown);
}
#[test]
fn build_live_pipeline_creates_registry() {
let config = test_config();
let registry = build_live_pipeline(&config);
use crate::pipeline::traits::TaskType;
let _pipeline = registry.get(&TaskType::Chat);
let _pipeline = registry.get(&TaskType::CodeGeneration);
let _pipeline = registry.get(&TaskType::Unknown);
}
}