Skip to content

Commit 7e79104

Browse files
committed
feat(agent): unify orchestrator loop and global sqlite memory
1 parent b499dd3 commit 7e79104

94 files changed

Lines changed: 12127 additions & 4061 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ lru = "0.12.4"
147147
lz4_flex = "0.11.3"
148148
# Hex encoding/decoding - pin to exact version
149149
hex = "0.4.3"
150+
# OS-specific data directories
151+
directories = "5.0.1"
150152
# Additional dependencies from fluent-agent
151153
# WebSocket support - pin to exact version
152154
tokio-tungstenite = "0.20.1"

crates/fluent-agent/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ prometheus = { workspace = true }
4444
# Additional utilities
4545
base64 = { workspace = true }
4646
thiserror = { workspace = true }
47+
sha2 = { workspace = true }
48+
hex = { workspace = true }
49+
directories = { workspace = true }
4750
urlencoding = "2.1"
4851
bincode = "1.3"
4952
toml = { workspace = true }
@@ -53,6 +56,12 @@ futures-util = { version = "0.3", features = ["sink", "std"] }
5356
# Async cancellation support
5457
tokio-util = "0.7"
5558

59+
[features]
60+
# Deprecated integration suites kept for reference.
61+
deprecated_agent_tests = []
62+
deprecated_memory_tests = []
63+
deprecated_mcp_tests = []
64+
5665
[dev-dependencies]
5766
tempfile = { workspace = true }
5867
tokio-stream = "0.1"

crates/fluent-agent/src/adapters.rs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -573,12 +573,17 @@ impl act::ActionPlanner for LongFormWriterPlanner {
573573

574574
pub struct McpRegistryExecutor {
575575
client_mgr: std::sync::Arc<ProductionMcpClientManager>,
576+
policy: crate::tools::ToolExecutionConfig,
576577
}
577578

578579
impl McpRegistryExecutor {
579-
pub fn new(manager: std::sync::Arc<ProductionMcpManager>) -> Self {
580+
pub fn new(
581+
manager: std::sync::Arc<ProductionMcpManager>,
582+
policy: crate::tools::ToolExecutionConfig,
583+
) -> Self {
580584
Self {
581585
client_mgr: manager.client_manager(),
586+
policy,
582587
}
583588
}
584589
}
@@ -649,10 +654,31 @@ impl crate::tools::ToolExecutor for McpRegistryExecutor {
649654

650655
fn validate_tool_request(
651656
&self,
652-
_tool_name: &str,
653-
_parameters: &std::collections::HashMap<String, serde_json::Value>,
657+
tool_name: &str,
658+
parameters: &std::collections::HashMap<String, serde_json::Value>,
654659
) -> anyhow::Result<()> {
655-
// Basic pass-through validation; MCP server handles schema
660+
// Enforce the same basic policy checks as local tools.
661+
// MCP servers may have their own validation, but we do not delegate safety.
662+
if self.policy.read_only {
663+
let lower = tool_name.to_lowercase();
664+
if lower.contains("write") || lower.contains("create") || lower.contains("delete") {
665+
return Err(anyhow::anyhow!(
666+
"MCP tool '{}' is blocked in read-only mode",
667+
tool_name
668+
));
669+
}
670+
}
671+
672+
for key in ["path", "file_path", "out_path", "dest", "directory", "dir"] {
673+
if let Some(v) = parameters.get(key).and_then(|v| v.as_str()) {
674+
let _ = validation::validate_path(v, &self.policy.allowed_paths)?;
675+
}
676+
}
677+
678+
if let Some(cmd) = parameters.get("command").and_then(|v| v.as_str()) {
679+
validation::validate_command(cmd, &self.policy.allowed_commands)?;
680+
}
681+
656682
Ok(())
657683
}
658684
}
@@ -689,11 +715,11 @@ impl act::ToolExecutor for RegistryToolAdapter {
689715

690716
/// Simple LLM-backed code generator
691717
pub struct LlmCodeGenerator {
692-
engine: Arc<Box<dyn Engine>>,
718+
engine: Arc<dyn Engine>,
693719
}
694720

695721
impl LlmCodeGenerator {
696-
pub fn new(engine: Arc<Box<dyn Engine>>) -> Self {
722+
pub fn new(engine: Arc<dyn Engine>) -> Self {
697723
Self { engine }
698724
}
699725
}

crates/fluent-agent/src/advanced_tools.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ impl AdvancedToolRegistry {
249249

250250
self.tools_by_category
251251
.entry(category)
252-
.or_insert_with(Vec::new)
252+
.or_default()
253253
.push(tool.clone());
254254
self.tools_by_name.insert(name.clone(), tool);
255255

crates/fluent-agent/src/autonomy/supervisor.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ impl AutonomySupervisor {
271271
}
272272

273273
if let Some(stderr) = action_result.metadata.get("stderr") {
274-
if stderr.as_str().unwrap_or_default().len() > 0 {
274+
if !stderr.as_str().unwrap_or_default().is_empty() {
275275
score += 0.1;
276276
triggers.push("stderr_present".to_string());
277277
}
@@ -287,7 +287,7 @@ impl AutonomySupervisor {
287287
stage: SupervisorStage::PostAction,
288288
risk_score: score,
289289
risk_level,
290-
confidence: action_result.success.then(|| 0.8).unwrap_or(0.3),
290+
confidence: if action_result.success { 0.8 } else { 0.3 },
291291
triggers,
292292
recommended_action: decision,
293293
})

crates/fluent-agent/src/benchmarks.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,8 @@ impl AutonomousBenchmarkSuite {
408408
"Optimize algorithm performance for dataset size {}",
409409
i * 1000
410410
);
411-
match tot_engine.reason(&problem, &context).await {
412-
Ok(_) => success_count += 1,
413-
Err(_) => {}
411+
if tot_engine.reason(&problem, &context).await.is_ok() {
412+
success_count += 1;
414413
}
415414
}
416415

@@ -456,9 +455,12 @@ impl AutonomousBenchmarkSuite {
456455
GoalType::Analysis,
457456
);
458457

459-
match htn_planner.plan_decomposition(&goal, &context).await {
460-
Ok(_) => success_count += 1,
461-
Err(_) => {}
458+
if htn_planner
459+
.plan_decomposition(&goal, &context)
460+
.await
461+
.is_ok()
462+
{
463+
success_count += 1;
462464
}
463465
}
464466

@@ -510,9 +512,8 @@ impl AutonomousBenchmarkSuite {
510512
);
511513
}
512514

513-
match memory_system.update_context(&context).await {
514-
Ok(_) => success_count += 1,
515-
Err(_) => {}
515+
if memory_system.update_context(&context).await.is_ok() {
516+
success_count += 1;
516517
}
517518
}
518519

crates/fluent-agent/src/config.rs

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ impl Default for RateLimitConfig {
3131
Self {
3232
enabled: true,
3333
reasoning_rps: 5.0, // 5 requests per second
34-
action_rps: 10.0, // 10 requests per second
34+
action_rps: 10.0, // 10 requests per second
3535
reflection_rps: 3.0, // 3 requests per second
3636
}
3737
}
@@ -82,6 +82,8 @@ pub struct AgentEngineConfig {
8282
pub action_engine: String,
8383
pub reflection_engine: String,
8484
pub memory_database: String,
85+
#[serde(default = "default_memory_enabled")]
86+
pub memory_enabled: bool,
8587
pub tools: ToolConfig,
8688
pub config_path: Option<String>,
8789
pub max_iterations: Option<u32>,
@@ -109,12 +111,16 @@ fn default_web_browsing() -> bool {
109111
true
110112
}
111113

114+
fn default_memory_enabled() -> bool {
115+
true
116+
}
117+
112118
/// Runtime configuration with loaded engines and credentials
113119
#[derive(Clone)]
114120
pub struct AgentRuntimeConfig {
115-
pub reasoning_engine: Arc<Box<dyn Engine>>,
116-
pub action_engine: Arc<Box<dyn Engine>>,
117-
pub reflection_engine: Arc<Box<dyn Engine>>,
121+
pub reasoning_engine: Arc<dyn Engine>,
122+
pub action_engine: Arc<dyn Engine>,
123+
pub reflection_engine: Arc<dyn Engine>,
118124
pub config: AgentEngineConfig,
119125
pub credentials: HashMap<String, String>,
120126
pub supervisor: Option<AutonomySupervisorConfig>,
@@ -129,11 +135,7 @@ pub struct AgentRuntimeConfig {
129135
impl AgentRuntimeConfig {
130136
/// Get the base engine for enhanced reasoning
131137
pub fn get_base_engine(&self) -> Option<Arc<dyn Engine>> {
132-
// Return a clone of the reasoning engine for use as base engine
133-
// We need to convert from Arc<Box<dyn Engine>> to Arc<dyn Engine>
134-
// This is a workaround - we can't directly cast, so we'll return None for now
135-
// In a real implementation, we'd need to restructure to avoid this type mismatch
136-
None
138+
Some(self.reasoning_engine.clone())
137139
}
138140

139141
/// Acquire a rate limit token for reasoning operations
@@ -276,52 +278,42 @@ impl AgentEngineConfig {
276278
};
277279

278280
// Create reflection engine (can be the same as reasoning)
279-
let reflection_engine = if self.reflection_engine == self.reasoning_engine {
280-
// Create a new instance of the same engine
281-
self.create_engine(
282-
&fluent_config_content,
283-
&self.reflection_engine,
284-
&credentials,
285-
model_override,
286-
)
287-
.await?
288-
} else if self.reflection_engine == self.action_engine {
289-
// Create a new instance of the same engine
290-
self.create_engine(
291-
&fluent_config_content,
292-
&self.reflection_engine,
293-
&credentials,
294-
model_override,
295-
)
296-
.await?
297-
} else {
298-
self.create_engine(
281+
let reflection_engine = self
282+
.create_engine(
299283
&fluent_config_content,
300284
&self.reflection_engine,
301285
&credentials,
302286
model_override,
303287
)
304-
.await?
305-
};
288+
.await?;
306289

307290
// Initialize rate limiters based on config
308-
let rate_limit_config = self.rate_limit.clone().unwrap_or_else(RateLimitConfig::from_environment);
291+
let rate_limit_config = self
292+
.rate_limit
293+
.clone()
294+
.unwrap_or_else(RateLimitConfig::from_environment);
309295

310296
let (reasoning_rate_limiter, action_rate_limiter, reflection_rate_limiter) =
311297
if rate_limit_config.enabled {
312298
(
313-
Some(Arc::new(fluent_engines::RateLimiter::new(rate_limit_config.reasoning_rps))),
314-
Some(Arc::new(fluent_engines::RateLimiter::new(rate_limit_config.action_rps))),
315-
Some(Arc::new(fluent_engines::RateLimiter::new(rate_limit_config.reflection_rps))),
299+
Some(Arc::new(fluent_engines::RateLimiter::new(
300+
rate_limit_config.reasoning_rps,
301+
))),
302+
Some(Arc::new(fluent_engines::RateLimiter::new(
303+
rate_limit_config.action_rps,
304+
))),
305+
Some(Arc::new(fluent_engines::RateLimiter::new(
306+
rate_limit_config.reflection_rps,
307+
))),
316308
)
317309
} else {
318310
(None, None, None)
319311
};
320312

321313
Ok(AgentRuntimeConfig {
322-
reasoning_engine: Arc::new(reasoning_engine),
323-
action_engine: Arc::new(action_engine),
324-
reflection_engine: Arc::new(reflection_engine),
314+
reasoning_engine: Arc::from(reasoning_engine),
315+
action_engine: Arc::from(action_engine),
316+
reflection_engine: Arc::from(reflection_engine),
325317
config: self.clone(),
326318
credentials,
327319
supervisor: self.supervisor.clone(),
@@ -334,6 +326,31 @@ impl AgentEngineConfig {
334326
})
335327
}
336328

329+
/// Resolve the configured SQLite memory database to a filesystem path.
330+
///
331+
/// Supported forms:
332+
/// - `sqlite://global` (default)
333+
/// - `sqlite://:memory:`
334+
/// - `sqlite:///absolute/path/to/db`
335+
/// - `sqlite://./relative/path/to/db`
336+
pub fn resolve_memory_db_path(&self) -> std::path::PathBuf {
337+
let url = self.memory_database.trim();
338+
let Some(rest) = url.strip_prefix("sqlite://") else {
339+
return crate::paths::global_agent_memory_db_path();
340+
};
341+
342+
if rest.is_empty() || rest == "global" {
343+
return crate::paths::global_agent_memory_db_path();
344+
}
345+
if rest == ":memory:" {
346+
return std::path::PathBuf::from(":memory:");
347+
}
348+
349+
// `sqlite:///abs/path` yields rest like "/abs/path" (keep it absolute)
350+
// `sqlite://./rel/path` yields rest like "./rel/path"
351+
std::path::PathBuf::from(rest)
352+
}
353+
337354
pub fn supervisor_config(&self) -> AutonomySupervisorConfig {
338355
self.supervisor.clone().unwrap_or_default()
339356
}
@@ -531,7 +548,8 @@ impl AgentEngineConfig {
531548
reasoning_engine: "sonnet3.5".to_string(),
532549
action_engine: "gpt-4o".to_string(),
533550
reflection_engine: "gemini-flash".to_string(),
534-
memory_database: "sqlite://./agent_memory.db".to_string(),
551+
memory_database: "sqlite://global".to_string(),
552+
memory_enabled: true,
535553
tools: ToolConfig {
536554
file_operations: true,
537555
shell_commands: false, // Disabled by default for security
@@ -627,8 +645,7 @@ pub mod credentials {
627645

628646
// Load CREDENTIAL_ prefixed variables (fluent_cli pattern)
629647
for (key, value) in env::vars() {
630-
if key.starts_with("CREDENTIAL_") {
631-
let credential_key = &key[11..]; // Remove CREDENTIAL_ prefix
648+
if let Some(credential_key) = key.strip_prefix("CREDENTIAL_") {
632649
credentials.insert(credential_key.to_string(), value);
633650
}
634651
}

0 commit comments

Comments
 (0)