docs(agent): Add doc-comments to all public items in agent-core
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//! Context compaction for long conversations
|
||||
//! Context compaction for long conversations.
|
||||
//!
|
||||
//! When the conversation context grows too large, this module compacts
|
||||
//! earlier messages into a summary while preserving recent context.
|
||||
@@ -6,16 +6,16 @@
|
||||
use color_eyre::eyre::Result;
|
||||
use llm_core::{ChatMessage, ChatOptions, LlmProvider};
|
||||
|
||||
/// Token limit threshold for triggering compaction
|
||||
/// Token limit threshold for triggering compaction.
|
||||
const CONTEXT_LIMIT: usize = 180_000;
|
||||
|
||||
/// Threshold ratio at which to trigger compaction (90% of limit)
|
||||
/// Threshold ratio at which to trigger compaction (90% of limit).
|
||||
const COMPACTION_THRESHOLD: f64 = 0.9;
|
||||
|
||||
/// Number of recent messages to preserve during compaction
|
||||
/// Number of recent messages to preserve during compaction.
|
||||
const PRESERVE_RECENT: usize = 10;
|
||||
|
||||
/// Token counter for estimating context size
|
||||
/// Token counter for estimating the size of conversation history in tokens.
|
||||
pub struct TokenCounter {
|
||||
chars_per_token: f64,
|
||||
}
|
||||
@@ -27,12 +27,13 @@ impl Default for TokenCounter {
|
||||
}
|
||||
|
||||
impl TokenCounter {
|
||||
/// Creates a new `TokenCounter` with default settings.
|
||||
pub fn new() -> Self {
|
||||
// Rough estimate: ~4 chars per token for English text
|
||||
Self { chars_per_token: 4.0 }
|
||||
}
|
||||
|
||||
/// Estimate token count for a message
|
||||
/// Estimates the token count for a single message.
|
||||
pub fn count_message(&self, message: &ChatMessage) -> usize {
|
||||
let content_len = message.content.as_ref().map(|c| c.len()).unwrap_or(0);
|
||||
// Add overhead for role, metadata
|
||||
@@ -40,19 +41,19 @@ impl TokenCounter {
|
||||
((content_len as f64 / self.chars_per_token) as usize) + overhead
|
||||
}
|
||||
|
||||
/// Estimate total token count for all messages
|
||||
/// Estimates the total token count for a list of messages.
|
||||
pub fn count_messages(&self, messages: &[ChatMessage]) -> usize {
|
||||
messages.iter().map(|m| self.count_message(m)).sum()
|
||||
}
|
||||
|
||||
/// Check if context should be compacted
|
||||
/// Determines if the conversation history should be compacted based on token limits.
|
||||
pub fn should_compact(&self, messages: &[ChatMessage]) -> bool {
|
||||
let count = self.count_messages(messages);
|
||||
count > (CONTEXT_LIMIT as f64 * COMPACTION_THRESHOLD) as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Context compactor that summarizes conversation history
|
||||
/// Context compactor that uses an LLM to summarize conversation history.
|
||||
pub struct Compactor {
|
||||
token_counter: TokenCounter,
|
||||
}
|
||||
@@ -64,22 +65,22 @@ impl Default for Compactor {
|
||||
}
|
||||
|
||||
impl Compactor {
|
||||
/// Creates a new `Compactor`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
token_counter: TokenCounter::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if messages need compaction
|
||||
/// Returns `true` if the provided messages exceed the compaction threshold.
|
||||
pub fn needs_compaction(&self, messages: &[ChatMessage]) -> bool {
|
||||
self.token_counter.should_compact(messages)
|
||||
}
|
||||
|
||||
/// Compact messages by summarizing earlier conversation
|
||||
/// Compacts conversation history by summarizing earlier messages.
|
||||
///
|
||||
/// Returns compacted messages with:
|
||||
/// - A system message containing the summary of earlier context
|
||||
/// - The most recent N messages preserved in full
|
||||
/// The resulting message list will contain a summary system message followed
|
||||
/// by the most recent conversation context.
|
||||
pub async fn compact<P: LlmProvider>(
|
||||
&self,
|
||||
provider: &P,
|
||||
@@ -169,7 +170,7 @@ impl Compactor {
|
||||
Ok(summary.trim().to_string())
|
||||
}
|
||||
|
||||
/// Get token counter for external use
|
||||
/// Returns a reference to the internal `TokenCounter`.
|
||||
pub fn token_counter(&self) -> &TokenCounter {
|
||||
&self.token_counter
|
||||
}
|
||||
|
||||
@@ -9,23 +9,25 @@ use color_eyre::eyre::Result;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/// Status of a file in the git working tree
|
||||
/// Represents the git status of a specific file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GitFileStatus {
|
||||
/// File has been modified
|
||||
/// File has been modified.
|
||||
Modified { path: String },
|
||||
/// File has been added (staged)
|
||||
/// File has been added to the index (staged).
|
||||
Added { path: String },
|
||||
/// File has been deleted
|
||||
/// File has been deleted.
|
||||
Deleted { path: String },
|
||||
/// File has been renamed
|
||||
/// File has been renamed.
|
||||
Renamed { from: String, to: String },
|
||||
/// File is untracked
|
||||
/// File is not tracked by git.
|
||||
Untracked { path: String },
|
||||
}
|
||||
|
||||
impl GitFileStatus {
|
||||
/// Get the primary path associated with this status
|
||||
/// Returns the primary path associated with this status entry.
|
||||
///
|
||||
/// For renames, this returns the new path.
|
||||
pub fn path(&self) -> &str {
|
||||
match self {
|
||||
Self::Modified { path } => path,
|
||||
@@ -37,25 +39,25 @@ impl GitFileStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete state of a git repository
|
||||
/// Represents the captured state of a git repository.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitState {
|
||||
/// Whether the current directory is in a git repository
|
||||
/// `true` if the directory is a git repository.
|
||||
pub is_git_repo: bool,
|
||||
/// Current branch name (None if not in a repo or detached HEAD)
|
||||
/// The name of the current branch, if any.
|
||||
pub current_branch: Option<String>,
|
||||
/// Main branch name (main/master, None if not detected)
|
||||
/// The name of the detected main/master branch.
|
||||
pub main_branch: Option<String>,
|
||||
/// Status of files in the working tree
|
||||
/// List of file status entries for the working tree.
|
||||
pub status: Vec<GitFileStatus>,
|
||||
/// Whether there are any uncommitted changes
|
||||
/// `true` if there are any staged or unstaged changes.
|
||||
pub has_uncommitted_changes: bool,
|
||||
/// Remote URL for the repository (None if no remote configured)
|
||||
/// The URL of the 'origin' remote, if configured.
|
||||
pub remote_url: Option<String>,
|
||||
}
|
||||
|
||||
impl GitState {
|
||||
/// Create a default GitState for non-git directories
|
||||
/// Creates a `GitState` representing a non-repository directory.
|
||||
pub fn not_a_repo() -> Self {
|
||||
Self {
|
||||
is_git_repo: false,
|
||||
@@ -68,10 +70,11 @@ impl GitState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect the current git repository state
|
||||
/// Detects the git state of the specified working directory.
|
||||
///
|
||||
/// This function runs various git commands to gather information about the repository.
|
||||
/// If git is not available or the directory is not a git repo, returns a default state.
|
||||
/// This function executes git commands to inspect the repository. It handles cases
|
||||
/// where git is missing or the directory is not a repository by returning a
|
||||
/// "not a repo" state rather than an error.
|
||||
pub fn detect_git_state(working_dir: &Path) -> Result<GitState> {
|
||||
// Check if this is a git repository
|
||||
let is_repo = Command::new("git")
|
||||
@@ -252,13 +255,10 @@ fn get_remote_url(working_dir: &Path) -> Result<Option<String>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a git command is safe (read-only)
|
||||
/// Heuristically determines if a git command string is "safe" (read-only).
|
||||
///
|
||||
/// Safe commands include:
|
||||
/// - status, log, show, diff, branch (without -D)
|
||||
/// - remote (without add/remove)
|
||||
/// - config --get
|
||||
/// - rev-parse, ls-files, ls-tree
|
||||
/// Safe commands (like `status` or `diff`) are generally allowed without
|
||||
/// explicit confirmation in certain modes.
|
||||
pub fn is_safe_git_command(command: &str) -> bool {
|
||||
let parts: Vec<&str> = command.split_whitespace().collect();
|
||||
|
||||
@@ -287,13 +287,11 @@ pub fn is_safe_git_command(command: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a git command is destructive
|
||||
/// Heuristically determines if a git command is "destructive" or potentially dangerous.
|
||||
///
|
||||
/// Returns (is_destructive, warning_message) tuple.
|
||||
/// Destructive commands include:
|
||||
/// - push --force, reset --hard, clean -fd
|
||||
/// - rebase, amend, filter-branch
|
||||
/// - branch -D, tag -d
|
||||
/// Returns a tuple of `(is_destructive, warning_message)`. Destructive commands
|
||||
/// (like `reset --hard` or `push --force`) should always trigger a warning or
|
||||
/// require explicit approval.
|
||||
pub fn is_destructive_git_command(command: &str) -> (bool, &'static str) {
|
||||
let cmd_lower = command.to_lowercase();
|
||||
|
||||
@@ -347,16 +345,7 @@ pub fn is_destructive_git_command(command: &str) -> (bool, &'static str) {
|
||||
(false, "")
|
||||
}
|
||||
|
||||
/// Format git state for human-readable display
|
||||
///
|
||||
/// Example output:
|
||||
/// ```text
|
||||
/// Git Repository: yes
|
||||
/// Current branch: feature-branch
|
||||
/// Main branch: main
|
||||
/// Status: 3 modified, 1 untracked
|
||||
/// Remote: https://github.com/user/repo.git
|
||||
/// ```
|
||||
/// Formats a `GitState` into a human-readable summary string.
|
||||
pub fn format_git_status(state: &GitState) -> String {
|
||||
if !state.is_git_repo {
|
||||
return "Not a git repository".to_string();
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
//! Core agent orchestration for the Owlen AI agent.
|
||||
//!
|
||||
//! This crate provides the central engine that coordinates Large Language Models (LLMs),
|
||||
//! a rich set of system tools, and user interaction through an event-driven loop.
|
||||
|
||||
pub mod session;
|
||||
pub mod system_prompt;
|
||||
pub mod git;
|
||||
@@ -71,27 +76,27 @@ pub enum AgentEvent {
|
||||
pub type AgentEventSender = mpsc::Sender<AgentEvent>;
|
||||
pub type AgentEventReceiver = mpsc::Receiver<AgentEvent>;
|
||||
|
||||
/// Create channel for agent events
|
||||
/// Creates a channel for agent events with a buffer size of 100.
|
||||
pub fn create_event_channel() -> (AgentEventSender, AgentEventReceiver) {
|
||||
mpsc::channel(100)
|
||||
}
|
||||
|
||||
/// Optional context for tools that need external dependencies
|
||||
/// Optional context for tools that need external dependencies or shared state.
|
||||
#[derive(Clone)]
|
||||
pub struct ToolContext {
|
||||
/// Todo list for TodoWrite tool
|
||||
/// Todo list for the `todo_write` tool.
|
||||
pub todo_list: Option<TodoList>,
|
||||
|
||||
/// Channel for asking user questions
|
||||
/// Channel for asking user questions via the `ask_user` tool.
|
||||
pub ask_sender: Option<AskSender>,
|
||||
|
||||
/// Shell manager for background shells
|
||||
/// Shell manager for handling background shells.
|
||||
pub shell_manager: Option<ShellManager>,
|
||||
|
||||
/// Plan manager for planning mode
|
||||
/// Plan manager for managing implementation plans in planning mode.
|
||||
pub plan_manager: Option<Arc<PlanManager>>,
|
||||
|
||||
/// Current agent mode (normal or planning)
|
||||
/// Current agent mode (e.g., Normal or Planning).
|
||||
pub agent_mode: Arc<RwLock<AgentMode>>,
|
||||
}
|
||||
|
||||
@@ -108,52 +113,58 @@ impl Default for ToolContext {
|
||||
}
|
||||
|
||||
impl ToolContext {
|
||||
/// Creates a new, empty `ToolContext`.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Adds a `TodoList` to the context.
|
||||
pub fn with_todo_list(mut self, list: TodoList) -> Self {
|
||||
self.todo_list = Some(list);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an `AskSender` to the context for user interaction.
|
||||
pub fn with_ask_sender(mut self, sender: AskSender) -> Self {
|
||||
self.ask_sender = Some(sender);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `ShellManager` to the context for managing bash sessions.
|
||||
pub fn with_shell_manager(mut self, manager: ShellManager) -> Self {
|
||||
self.shell_manager = Some(manager);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `PlanManager` to the context.
|
||||
pub fn with_plan_manager(mut self, manager: PlanManager) -> Self {
|
||||
self.plan_manager = Some(Arc::new(manager));
|
||||
self
|
||||
}
|
||||
|
||||
/// Initializes a `PlanManager` with the given project root and adds it to the context.
|
||||
pub fn with_project_root(mut self, project_root: PathBuf) -> Self {
|
||||
self.plan_manager = Some(Arc::new(PlanManager::new(project_root)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if agent is in planning mode
|
||||
/// Checks if the agent is currently in planning mode.
|
||||
pub async fn is_planning(&self) -> bool {
|
||||
self.agent_mode.read().await.is_planning()
|
||||
}
|
||||
|
||||
/// Get current agent mode
|
||||
/// Returns the current `AgentMode`.
|
||||
pub async fn get_mode(&self) -> AgentMode {
|
||||
self.agent_mode.read().await.clone()
|
||||
}
|
||||
|
||||
/// Set agent mode
|
||||
/// Sets the current `AgentMode`.
|
||||
pub async fn set_mode(&self, mode: AgentMode) {
|
||||
*self.agent_mode.write().await = mode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Define all available tools for the LLM
|
||||
/// Returns definitions for all available tools that the agent can use.
|
||||
pub fn get_tool_definitions() -> Vec<Tool> {
|
||||
vec![
|
||||
Tool::function(
|
||||
@@ -466,7 +477,10 @@ impl ToolCallsBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a tool call and return the result
|
||||
/// Executes a single tool call and returns its result as a string.
|
||||
///
|
||||
/// This function handles permission checking and interacts with various tool-specific
|
||||
/// backends (filesystem, shell, etc.) using the provided `ToolContext`.
|
||||
pub async fn execute_tool(
|
||||
tool_name: &str,
|
||||
arguments: &Value,
|
||||
@@ -858,7 +872,11 @@ pub async fn execute_tool(
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the agent loop with tool calling
|
||||
/// Runs the core agent loop for a given user prompt.
|
||||
///
|
||||
/// This function iteratively calls the LLM provider, processes tool execution requests,
|
||||
/// and feeds the results back to the model until a final text response is generated
|
||||
/// or the iteration limit is reached.
|
||||
pub async fn run_agent_loop<P: LlmProvider>(
|
||||
provider: &P,
|
||||
user_prompt: &str,
|
||||
@@ -988,7 +1006,10 @@ pub async fn run_agent_loop<P: LlmProvider>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Run agent loop with event streaming
|
||||
/// Runs the agent loop and streams events back through the provided channel.
|
||||
///
|
||||
/// This allows UIs to provide real-time feedback as the LLM generates text and
|
||||
/// as tools are being executed.
|
||||
pub async fn run_agent_loop_streaming<P: LlmProvider>(
|
||||
provider: &P,
|
||||
user_prompt: &str,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
//! Session state and history management.
|
||||
//!
|
||||
//! This module provides tools for tracking conversation history, capturing
|
||||
//! usage statistics, and managing session checkpoints for persistence and rewind.
|
||||
|
||||
use color_eyre::eyre::{Result, eyre};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -5,16 +10,23 @@ use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// Statistics for a single chat session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionStats {
|
||||
/// The time when the session started.
|
||||
pub start_time: SystemTime,
|
||||
/// Total number of messages exchanged.
|
||||
pub total_messages: usize,
|
||||
/// Total number of tools executed by the agent.
|
||||
pub total_tool_calls: usize,
|
||||
/// Total wall-clock time spent in the session.
|
||||
pub total_duration: Duration,
|
||||
/// Rough estimate of the total tokens used.
|
||||
pub estimated_tokens: usize,
|
||||
}
|
||||
|
||||
impl SessionStats {
|
||||
/// Creates a new `SessionStats` instance with zeroed values.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
start_time: SystemTime::now(),
|
||||
@@ -25,16 +37,19 @@ impl SessionStats {
|
||||
}
|
||||
}
|
||||
|
||||
/// Records a new message in the statistics.
|
||||
pub fn record_message(&mut self, tokens: usize, duration: Duration) {
|
||||
self.total_messages += 1;
|
||||
self.estimated_tokens += tokens;
|
||||
self.total_duration += duration;
|
||||
}
|
||||
|
||||
/// Increments the tool call counter.
|
||||
pub fn record_tool_call(&mut self) {
|
||||
self.total_tool_calls += 1;
|
||||
}
|
||||
|
||||
/// Formats a duration into a human-readable string.
|
||||
pub fn format_duration(d: Duration) -> String {
|
||||
let secs = d.as_secs();
|
||||
if secs < 60 {
|
||||
@@ -53,22 +68,32 @@ impl Default for SessionStats {
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory history of the current session.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionHistory {
|
||||
/// List of prompts provided by the user.
|
||||
pub user_prompts: Vec<String>,
|
||||
/// List of responses generated by the assistant.
|
||||
pub assistant_responses: Vec<String>,
|
||||
/// Chronological log of all tool calls made.
|
||||
pub tool_calls: Vec<ToolCallRecord>,
|
||||
}
|
||||
|
||||
/// Record of a single tool execution.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolCallRecord {
|
||||
/// Name of the tool that was called.
|
||||
pub tool_name: String,
|
||||
/// JSON-encoded arguments provided to the tool.
|
||||
pub arguments: String,
|
||||
/// Output produced by the tool.
|
||||
pub result: String,
|
||||
/// Whether the tool execution was successful.
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
impl SessionHistory {
|
||||
/// Creates a new, empty `SessionHistory`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
user_prompts: Vec::new(),
|
||||
@@ -77,18 +102,22 @@ impl SessionHistory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends a user message to history.
|
||||
pub fn add_user_message(&mut self, message: String) {
|
||||
self.user_prompts.push(message);
|
||||
}
|
||||
|
||||
/// Appends an assistant response to history.
|
||||
pub fn add_assistant_message(&mut self, message: String) {
|
||||
self.assistant_responses.push(message);
|
||||
}
|
||||
|
||||
/// Appends a tool call record to history.
|
||||
pub fn add_tool_call(&mut self, record: ToolCallRecord) {
|
||||
self.tool_calls.push(record);
|
||||
}
|
||||
|
||||
/// Clears all stored history.
|
||||
pub fn clear(&mut self) {
|
||||
self.user_prompts.clear();
|
||||
self.assistant_responses.clear();
|
||||
@@ -102,17 +131,21 @@ impl Default for SessionHistory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a file modification with before/after content
|
||||
/// Represents a file modification with before/after content.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileDiff {
|
||||
/// Absolute path to the file.
|
||||
pub path: PathBuf,
|
||||
/// Content of the file before modification.
|
||||
pub before: String,
|
||||
/// Content of the file after modification.
|
||||
pub after: String,
|
||||
/// When the modification occurred.
|
||||
pub timestamp: SystemTime,
|
||||
}
|
||||
|
||||
impl FileDiff {
|
||||
/// Create a new file diff
|
||||
/// Creates a new `FileDiff`.
|
||||
pub fn new(path: PathBuf, before: String, after: String) -> Self {
|
||||
Self {
|
||||
path,
|
||||
@@ -123,20 +156,27 @@ impl FileDiff {
|
||||
}
|
||||
}
|
||||
|
||||
/// A checkpoint captures the state of a session at a point in time
|
||||
/// A checkpoint captures the full state of a session at a point in time.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Checkpoint {
|
||||
/// Unique identifier for the checkpoint.
|
||||
pub id: String,
|
||||
/// When the checkpoint was created.
|
||||
pub timestamp: SystemTime,
|
||||
/// Session statistics at the time of checkpoint.
|
||||
pub stats: SessionStats,
|
||||
/// History of user prompts.
|
||||
pub user_prompts: Vec<String>,
|
||||
/// History of assistant responses.
|
||||
pub assistant_responses: Vec<String>,
|
||||
/// History of tool calls.
|
||||
pub tool_calls: Vec<ToolCallRecord>,
|
||||
/// List of file modifications made during the session.
|
||||
pub file_diffs: Vec<FileDiff>,
|
||||
}
|
||||
|
||||
impl Checkpoint {
|
||||
/// Create a new checkpoint from current session state
|
||||
/// Creates a new checkpoint from the current session state.
|
||||
pub fn new(
|
||||
id: String,
|
||||
stats: SessionStats,
|
||||
@@ -154,7 +194,7 @@ impl Checkpoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Save checkpoint to disk
|
||||
/// Saves the checkpoint to a JSON file on disk.
|
||||
pub fn save(&self, checkpoint_dir: &Path) -> Result<()> {
|
||||
fs::create_dir_all(checkpoint_dir)?;
|
||||
let path = checkpoint_dir.join(format!("{}.json", self.id));
|
||||
@@ -163,7 +203,7 @@ impl Checkpoint {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load checkpoint from disk
|
||||
/// Loads a checkpoint from disk by ID.
|
||||
pub fn load(checkpoint_dir: &Path, id: &str) -> Result<Self> {
|
||||
let path = checkpoint_dir.join(format!("{}.json", id));
|
||||
let content = fs::read_to_string(&path)
|
||||
@@ -173,7 +213,7 @@ impl Checkpoint {
|
||||
Ok(checkpoint)
|
||||
}
|
||||
|
||||
/// List all available checkpoints in a directory
|
||||
/// Lists all available checkpoint IDs in the given directory.
|
||||
pub fn list(checkpoint_dir: &Path) -> Result<Vec<String>> {
|
||||
if !checkpoint_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
@@ -196,14 +236,14 @@ impl Checkpoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Session checkpoint manager
|
||||
/// Manages the creation and restoration of session checkpoints.
|
||||
pub struct CheckpointManager {
|
||||
checkpoint_dir: PathBuf,
|
||||
file_snapshots: HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
impl CheckpointManager {
|
||||
/// Create a new checkpoint manager
|
||||
/// Creates a new `CheckpointManager` pointing to the specified directory.
|
||||
pub fn new(checkpoint_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
checkpoint_dir,
|
||||
@@ -211,7 +251,7 @@ impl CheckpointManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot a file's current content before modification
|
||||
/// Snapshots a file's current content before modification to track changes.
|
||||
pub fn snapshot_file(&mut self, path: &Path) -> Result<()> {
|
||||
if !self.file_snapshots.contains_key(path) {
|
||||
let content = fs::read_to_string(path).unwrap_or_default();
|
||||
@@ -220,7 +260,7 @@ impl CheckpointManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a file diff after modification
|
||||
/// Creates a `FileDiff` if the file has been modified since it was snapshotted.
|
||||
pub fn create_diff(&self, path: &Path) -> Result<Option<FileDiff>> {
|
||||
if let Some(before) = self.file_snapshots.get(path) {
|
||||
let after = fs::read_to_string(path).unwrap_or_default();
|
||||
@@ -238,7 +278,7 @@ impl CheckpointManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all file diffs since last checkpoint
|
||||
/// Returns all file modifications tracked since the last checkpoint.
|
||||
pub fn get_all_diffs(&self) -> Result<Vec<FileDiff>> {
|
||||
let mut diffs = Vec::new();
|
||||
for (path, before) in &self.file_snapshots {
|
||||
@@ -250,12 +290,12 @@ impl CheckpointManager {
|
||||
Ok(diffs)
|
||||
}
|
||||
|
||||
/// Clear file snapshots
|
||||
/// Clears all internal file snapshots.
|
||||
pub fn clear_snapshots(&mut self) {
|
||||
self.file_snapshots.clear();
|
||||
}
|
||||
|
||||
/// Save a checkpoint
|
||||
/// Saves the current session state as a new checkpoint.
|
||||
pub fn save_checkpoint(
|
||||
&mut self,
|
||||
id: String,
|
||||
@@ -269,17 +309,19 @@ impl CheckpointManager {
|
||||
Ok(checkpoint)
|
||||
}
|
||||
|
||||
/// Load a checkpoint
|
||||
/// Loads a checkpoint by ID.
|
||||
pub fn load_checkpoint(&self, id: &str) -> Result<Checkpoint> {
|
||||
Checkpoint::load(&self.checkpoint_dir, id)
|
||||
}
|
||||
|
||||
/// List all checkpoints
|
||||
/// Lists all available checkpoints.
|
||||
pub fn list_checkpoints(&self) -> Result<Vec<String>> {
|
||||
Checkpoint::list(&self.checkpoint_dir)
|
||||
}
|
||||
|
||||
/// Rewind to a checkpoint by restoring file contents
|
||||
/// Rewinds the local filesystem to the state captured in the specified checkpoint.
|
||||
///
|
||||
/// Returns a list of paths that were restored.
|
||||
pub fn rewind_to(&self, checkpoint_id: &str) -> Result<Vec<PathBuf>> {
|
||||
let checkpoint = self.load_checkpoint(checkpoint_id)?;
|
||||
let mut restored_files = Vec::new();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//! System Prompt Management
|
||||
//! System Prompt Management.
|
||||
//!
|
||||
//! Composes system prompts from multiple sources for agent sessions.
|
||||
//! This module is responsible for composing the complex system prompts sent to the LLM.
|
||||
//! It merges base instructions, tool definitions, project-specific context, and
|
||||
//! dynamically injected content from skills and hooks.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// Builder for composing system prompts
|
||||
/// Builder for incrementally composing a system prompt from various sources.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SystemPromptBuilder {
|
||||
sections: Vec<PromptSection>,
|
||||
@@ -19,11 +21,12 @@ struct PromptSection {
|
||||
}
|
||||
|
||||
impl SystemPromptBuilder {
|
||||
/// Creates a new, empty `SystemPromptBuilder`.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Add the base agent prompt
|
||||
/// Adds the base agent identity and instructions section.
|
||||
pub fn with_base_prompt(mut self, content: impl Into<String>) -> Self {
|
||||
self.sections.push(PromptSection {
|
||||
name: "base".to_string(),
|
||||
@@ -33,7 +36,7 @@ impl SystemPromptBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Add tool usage instructions
|
||||
/// Adds tool usage instructions.
|
||||
pub fn with_tool_instructions(mut self, content: impl Into<String>) -> Self {
|
||||
self.sections.push(PromptSection {
|
||||
name: "tools".to_string(),
|
||||
@@ -43,7 +46,8 @@ impl SystemPromptBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Load and add project instructions from CLAUDE.md or .owlen.md
|
||||
/// Attempts to load project-specific instructions from `CLAUDE.md` or `.owlen.md`
|
||||
/// located in the provided project root.
|
||||
pub fn with_project_instructions(mut self, project_root: &Path) -> Self {
|
||||
// Try CLAUDE.md first (Claude Code compatibility)
|
||||
let claude_md = project_root.join("CLAUDE.md");
|
||||
@@ -73,7 +77,7 @@ impl SystemPromptBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Add skill content
|
||||
/// Adds domain-specific knowledge or instructions as a "skill".
|
||||
pub fn with_skill(mut self, skill_name: &str, content: impl Into<String>) -> Self {
|
||||
self.sections.push(PromptSection {
|
||||
name: format!("skill:{}", skill_name),
|
||||
@@ -83,7 +87,7 @@ impl SystemPromptBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Add hook-injected content (from SessionStart hooks)
|
||||
/// Injects context dynamically from hooks (e.g., during session initialization).
|
||||
pub fn with_hook_injection(mut self, content: impl Into<String>) -> Self {
|
||||
self.sections.push(PromptSection {
|
||||
name: "hook".to_string(),
|
||||
@@ -93,7 +97,7 @@ impl SystemPromptBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Add custom section
|
||||
/// Adds a generic custom section with a specific name and priority.
|
||||
pub fn with_section(mut self, name: impl Into<String>, content: impl Into<String>, priority: i32) -> Self {
|
||||
self.sections.push(PromptSection {
|
||||
name: name.into(),
|
||||
@@ -103,7 +107,9 @@ impl SystemPromptBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final system prompt
|
||||
/// Finalizes the build process and returns the combined system prompt string.
|
||||
///
|
||||
/// Sections are sorted by priority and separated by a horizontal rule (`---`).
|
||||
pub fn build(mut self) -> String {
|
||||
// Sort by priority
|
||||
self.sections.sort_by_key(|s| s.priority);
|
||||
@@ -116,13 +122,13 @@ impl SystemPromptBuilder {
|
||||
.join("\n\n---\n\n")
|
||||
}
|
||||
|
||||
/// Check if any content has been added
|
||||
/// Returns `true` if no prompt sections have been added yet.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.sections.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Default base prompt for Owlen agent
|
||||
/// Returns the standard default identity and guideline prompt for the Owlen agent.
|
||||
pub fn default_base_prompt() -> &'static str {
|
||||
r#"You are Owlen, an AI assistant that helps with software engineering tasks.
|
||||
|
||||
@@ -145,7 +151,7 @@ You have access to tools for reading files, writing code, running commands, and
|
||||
- Use `web_search` for current information"#
|
||||
}
|
||||
|
||||
/// Generate tool instructions based on available tools
|
||||
/// Dynamically generates a summary of available tools to be included in the system prompt.
|
||||
pub fn generate_tool_instructions(tool_names: &[&str]) -> String {
|
||||
let mut instructions = String::from("## Available Tools\n\n");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user