Refactor: Make AgentLoop stateless, clean up architecture
This commit is contained in:
parent
1abac85034
commit
c11eb348f9
@ -51,14 +51,17 @@ Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionM
|
|||||||
| `session` | Conversation session lifecycle, dialog operations | `SessionManager`, `Session` |
|
| `session` | Conversation session lifecycle, dialog operations | `SessionManager`, `Session` |
|
||||||
| `agent` | LLM call loop, tool execution, context compression | `AgentLoop` |
|
| `agent` | LLM call loop, tool execution, context compression | `AgentLoop` |
|
||||||
| `providers` | LLM API clients (OpenAI-compatible, Anthropic) | `LLMProvider` trait, factory `create_provider()` |
|
| `providers` | LLM API clients (OpenAI-compatible, Anthropic) | `LLMProvider` trait, factory `create_provider()` |
|
||||||
| `tools` | Agent tools (bash, file operations, http, web) | `ToolRegistry`, `Tool` trait |
|
| `tools` | Agent tools (bash, file operations, http, web, get_skill) | `ToolRegistry`, `Tool` trait |
|
||||||
|
| `skills` | Skills loading, management, and prompt building | `SkillsLoader`, `Skill` |
|
||||||
|
|
||||||
### Functional Boundaries
|
### Functional Boundaries
|
||||||
|
|
||||||
- **Channels** only send/receive messages via `MessageBus`; they know nothing about sessions or LLM
|
- **Channels** only send/receive messages via `MessageBus`; they know nothing about sessions or LLM
|
||||||
- **MessageBus** is a pure async queue; it routes nothing, just passes messages
|
- **MessageBus** is a pure async queue; it routes nothing, just passes messages
|
||||||
- **SessionManager** owns session state and dialog operations; it does NOT call LLM directly
|
- **SessionManager** owns session state and dialog operations; it does NOT call LLM directly
|
||||||
|
- SessionManager is responsible for injecting skills prompt into conversation history
|
||||||
- **AgentLoop** receives dialog events from `SessionManager`, calls LLM via `providers`, executes tools, returns text responses
|
- **AgentLoop** receives dialog events from `SessionManager`, calls LLM via `providers`, executes tools, returns text responses
|
||||||
|
- AgentLoop is stateless; all state is managed by Session/SessionManager
|
||||||
- **Providers** are pure HTTP clients; no bus/session/channel awareness
|
- **Providers** are pure HTTP clients; no bus/session/channel awareness
|
||||||
- **Tools** are executed by `AgentLoop`; they receive raw arguments and return string results
|
- **Tools** are executed by `AgentLoop`; they receive raw arguments and return string results
|
||||||
|
|
||||||
@ -70,4 +73,4 @@ Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionM
|
|||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
- `src/session/session.rs:657` — `LLMProviderConfig` struct requires `workspace_dir` but test helper at line 656-669 doesn't provide it; test code needs `workspace_dir: PathBuf::new()` added
|
- (No known issues at this time)
|
||||||
|
|||||||
@ -6,7 +6,6 @@ use crate::observability::{
|
|||||||
truncate_args, Observer, ObserverEvent, ToolExecutionOutcome,
|
truncate_args, Observer, ObserverEvent, ToolExecutionOutcome,
|
||||||
};
|
};
|
||||||
use crate::providers::{create_provider, LLMProvider, ChatCompletionRequest, Message, ToolCall};
|
use crate::providers::{create_provider, LLMProvider, ChatCompletionRequest, Message, ToolCall};
|
||||||
use crate::skills::SkillsLoader;
|
|
||||||
use crate::tools::ToolRegistry;
|
use crate::tools::ToolRegistry;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
@ -227,7 +226,6 @@ pub struct AgentLoop {
|
|||||||
max_iterations: usize,
|
max_iterations: usize,
|
||||||
workspace_dir: PathBuf,
|
workspace_dir: PathBuf,
|
||||||
model_name: String,
|
model_name: String,
|
||||||
skills_loader: Arc<SkillsLoader>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -238,7 +236,7 @@ pub struct AgentProcessResult {
|
|||||||
|
|
||||||
impl AgentLoop {
|
impl AgentLoop {
|
||||||
/// Create a new AgentLoop with a provider created from config.
|
/// Create a new AgentLoop with a provider created from config.
|
||||||
pub fn new(provider_config: LLMProviderConfig, skills_loader: Arc<SkillsLoader>) -> Result<Self, AgentError> {
|
pub fn new(provider_config: LLMProviderConfig) -> Result<Self, AgentError> {
|
||||||
let max_iterations = provider_config.max_tool_iterations;
|
let max_iterations = provider_config.max_tool_iterations;
|
||||||
let model_name = provider_config.model_id.clone();
|
let model_name = provider_config.model_id.clone();
|
||||||
let workspace_dir = provider_config.workspace_dir.clone();
|
let workspace_dir = provider_config.workspace_dir.clone();
|
||||||
@ -252,12 +250,11 @@ impl AgentLoop {
|
|||||||
max_iterations,
|
max_iterations,
|
||||||
workspace_dir,
|
workspace_dir,
|
||||||
model_name,
|
model_name,
|
||||||
skills_loader,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new AgentLoop with provider created from config and given tools.
|
/// Create a new AgentLoop with provider created from config and given tools.
|
||||||
pub fn with_tools(provider_config: LLMProviderConfig, tools: Arc<ToolRegistry>, skills_loader: Arc<SkillsLoader>) -> Result<Self, AgentError> {
|
pub fn with_tools(provider_config: LLMProviderConfig, tools: Arc<ToolRegistry>) -> Result<Self, AgentError> {
|
||||||
let max_iterations = provider_config.max_tool_iterations;
|
let max_iterations = provider_config.max_tool_iterations;
|
||||||
let model_name = provider_config.model_id.clone();
|
let model_name = provider_config.model_id.clone();
|
||||||
let workspace_dir = provider_config.workspace_dir.clone();
|
let workspace_dir = provider_config.workspace_dir.clone();
|
||||||
@ -271,12 +268,11 @@ impl AgentLoop {
|
|||||||
max_iterations,
|
max_iterations,
|
||||||
workspace_dir,
|
workspace_dir,
|
||||||
model_name,
|
model_name,
|
||||||
skills_loader,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new AgentLoop with an existing shared provider.
|
/// Create a new AgentLoop with an existing shared provider.
|
||||||
pub fn with_provider(provider: Arc<dyn LLMProvider>, max_iterations: usize, model_name: String, workspace_dir: PathBuf, skills_loader: Arc<SkillsLoader>) -> Self {
|
pub fn with_provider(provider: Arc<dyn LLMProvider>, max_iterations: usize, model_name: String, workspace_dir: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
provider,
|
provider,
|
||||||
tools: Arc::new(ToolRegistry::new()),
|
tools: Arc::new(ToolRegistry::new()),
|
||||||
@ -284,7 +280,6 @@ impl AgentLoop {
|
|||||||
max_iterations,
|
max_iterations,
|
||||||
workspace_dir,
|
workspace_dir,
|
||||||
model_name,
|
model_name,
|
||||||
skills_loader,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +290,6 @@ impl AgentLoop {
|
|||||||
max_iterations: usize,
|
max_iterations: usize,
|
||||||
model_name: String,
|
model_name: String,
|
||||||
workspace_dir: PathBuf,
|
workspace_dir: PathBuf,
|
||||||
skills_loader: Arc<SkillsLoader>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
provider,
|
provider,
|
||||||
@ -304,7 +298,6 @@ impl AgentLoop {
|
|||||||
max_iterations,
|
max_iterations,
|
||||||
workspace_dir,
|
workspace_dir,
|
||||||
model_name,
|
model_name,
|
||||||
skills_loader,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,17 +331,7 @@ impl AgentLoop {
|
|||||||
// Build and inject system prompt if not present
|
// Build and inject system prompt if not present
|
||||||
let has_system = messages.first().map_or(false, |m| m.role == "system");
|
let has_system = messages.first().map_or(false, |m| m.role == "system");
|
||||||
if !has_system {
|
if !has_system {
|
||||||
let mut system_prompt = build_system_prompt(&self.workspace_dir, &self.model_name, &self.tools);
|
let system_prompt = build_system_prompt(&self.workspace_dir, &self.model_name, &self.tools);
|
||||||
|
|
||||||
// Add skills prompt if there are skills
|
|
||||||
let skills_prompt = self.skills_loader.build_skills_prompt();
|
|
||||||
if !skills_prompt.is_empty() {
|
|
||||||
if !system_prompt.is_empty() {
|
|
||||||
system_prompt.push_str("\n\n");
|
|
||||||
}
|
|
||||||
system_prompt.push_str(&skills_prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
tracing::debug!("System prompt injected:\n{}", system_prompt);
|
tracing::debug!("System prompt injected:\n{}", system_prompt);
|
||||||
messages.insert(0, ChatMessage::system(system_prompt));
|
messages.insert(0, ChatMessage::system(system_prompt));
|
||||||
|
|||||||
@ -36,7 +36,6 @@ pub struct Session {
|
|||||||
tools: Arc<ToolRegistry>,
|
tools: Arc<ToolRegistry>,
|
||||||
compressor: ContextCompressor,
|
compressor: ContextCompressor,
|
||||||
store: Arc<SessionStore>,
|
store: Arc<SessionStore>,
|
||||||
skills_loader: Arc<SkillsLoader>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
@ -46,7 +45,6 @@ impl Session {
|
|||||||
user_tx: mpsc::Sender<WsOutbound>,
|
user_tx: mpsc::Sender<WsOutbound>,
|
||||||
tools: Arc<ToolRegistry>,
|
tools: Arc<ToolRegistry>,
|
||||||
store: Arc<SessionStore>,
|
store: Arc<SessionStore>,
|
||||||
skills_loader: Arc<SkillsLoader>,
|
|
||||||
) -> Result<Self, AgentError> {
|
) -> Result<Self, AgentError> {
|
||||||
let provider_box = create_provider(provider_config.clone())
|
let provider_box = create_provider(provider_config.clone())
|
||||||
.map_err(|e| AgentError::Other(format!("provider creation error: {}", e)))?;
|
.map_err(|e| AgentError::Other(format!("provider creation error: {}", e)))?;
|
||||||
@ -66,7 +64,6 @@ impl Session {
|
|||||||
tools,
|
tools,
|
||||||
compressor: ContextCompressor::with_config(provider.clone(), provider_config.token_limit, compressor_config),
|
compressor: ContextCompressor::with_config(provider.clone(), provider_config.token_limit, compressor_config),
|
||||||
store,
|
store,
|
||||||
skills_loader,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +178,6 @@ impl Session {
|
|||||||
self.provider_config.max_tool_iterations,
|
self.provider_config.max_tool_iterations,
|
||||||
self.provider_config.model_id.clone(),
|
self.provider_config.model_id.clone(),
|
||||||
self.provider_config.workspace_dir.clone(),
|
self.provider_config.workspace_dir.clone(),
|
||||||
self.skills_loader.clone(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -393,7 +389,6 @@ impl SessionManager {
|
|||||||
user_tx,
|
user_tx,
|
||||||
self.tools.clone(),
|
self.tools.clone(),
|
||||||
self.store.clone(),
|
self.store.clone(),
|
||||||
self.skills_loader.clone(),
|
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
let arc = Arc::new(Mutex::new(session));
|
let arc = Arc::new(Mutex::new(session));
|
||||||
@ -421,7 +416,6 @@ impl SessionManager {
|
|||||||
user_tx,
|
user_tx,
|
||||||
self.tools.clone(),
|
self.tools.clone(),
|
||||||
self.store.clone(),
|
self.store.clone(),
|
||||||
self.skills_loader.clone(),
|
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
let arc = Arc::new(Mutex::new(session));
|
let arc = Arc::new(Mutex::new(session));
|
||||||
@ -437,7 +431,6 @@ impl SessionManager {
|
|||||||
user_tx,
|
user_tx,
|
||||||
self.tools.clone(),
|
self.tools.clone(),
|
||||||
self.store.clone(),
|
self.store.clone(),
|
||||||
self.skills_loader.clone(),
|
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
let arc = Arc::new(Mutex::new(session));
|
let arc = Arc::new(Mutex::new(session));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user