From efd8013d871e656986821361bcfd99dd272c36eb Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 2 Jun 2026 12:32:07 +0200 Subject: [PATCH 1/4] docs: add missing_docs lint + fill doc gaps in types/config/service Add `#![warn(missing_docs)]` enforcement to three core crates and fill all documentation gaps triggered by the lint: - terraphim_types: Conversation struct, shared_learning module (TrustLevel methods, TrustLevelError, LearningCategory variants, LearningStore trait methods, InMemoryLearningStore::new, StoreError) - terraphim_config: Role fields, ConfigId variants, Config.selected_role, TerraphimConfigError variants, KnowledgeGraph fields, KnowledgeGraphLocal fields, get_selected_role method, ProjectDiscoveryError, ProjectConfig struct and fields, discover/is_empty/resolve_role_name functions - terraphim_service: ServiceError variants, Result type alias, llm module (SummarizeOptions, ChatOptions, LlmClient trait), llm_proxy module, conversation_service fields, error.rs (CommonError variants and ctors), summarization_queue module, logging Custom variant field Refs #1979 --- crates/terraphim_config/src/lib.rs | 30 ++++++++++++++- crates/terraphim_config/src/project.rs | 36 +++++++++++++++++- .../src/conversation_service.rs | 5 +++ crates/terraphim_service/src/error.rs | 38 ++++++++++++++++++- crates/terraphim_service/src/lib.rs | 29 ++++++++------ crates/terraphim_service/src/llm.rs | 11 ++++++ crates/terraphim_service/src/llm_proxy.rs | 30 +++++++++++++-- crates/terraphim_service/src/logging.rs | 5 ++- .../src/summarization_queue.rs | 30 +++++++++++++++ crates/terraphim_types/src/lib.rs | 5 +++ crates/terraphim_types/src/shared_learning.rs | 27 +++++++++++++ 11 files changed, 227 insertions(+), 19 deletions(-) diff --git a/crates/terraphim_config/src/lib.rs b/crates/terraphim_config/src/lib.rs index ca71a5362..6afdc6c68 100644 --- a/crates/terraphim_config/src/lib.rs +++ b/crates/terraphim_config/src/lib.rs @@ -1,3 +1,4 @@ +#![warn(missing_docs)] //! Configuration management for Terraphim AI. //! //! Provides role-based configuration where each [`Role`] describes a user profile with @@ -46,7 +47,7 @@ use crate::llm_router::LlmRouterConfig; // LLM Router configuration pub mod llm_router; -// Project-level configuration discovery +/// Project-level configuration discovery for `.terraphim/config.json` files. pub mod project; /// Convenience alias for `Result` used throughout this crate. @@ -59,36 +60,47 @@ type PersistenceResult = std::result::Result /// Errors arising from loading, validating, or persisting Terraphim configuration. #[derive(Error, Debug)] pub enum TerraphimConfigError { + /// No configuration file was found at the expected location. #[error("Unable to load config")] NotFound, + /// Configuration contains no role definitions; at least one role is required. #[error("At least one role is required")] NoRoles, + /// A named profile was requested but could not be applied. #[error("Profile error")] Profile(String), + /// An error from the underlying persistence layer. #[error("Persistence error")] Persistence(Box), + /// JSON serialisation or deserialisation failed. #[error("Serde JSON error")] Json(#[from] serde_json::Error), + /// Failed to initialise the tracing subscriber. #[error("Cannot initialize tracing subscriber")] TracingSubscriber(Box), + /// An error propagated from the rolegraph pipeline. #[error("Pipe error")] Pipe(#[from] terraphim_rolegraph::Error), + /// An error from the Aho-Corasick automata layer. #[error("Automata error")] Automata(#[from] terraphim_automata::TerraphimAutomataError), + /// A URL could not be parsed. #[error("Url error")] Url(#[from] url::ParseError), + /// An I/O error occurred while reading or writing configuration. #[error("IO error")] Io(#[from] std::io::Error), + /// A general configuration error with a descriptive message. #[error("Config error")] Config(String), } @@ -222,13 +234,19 @@ fn default_context_window() -> Option { #[cfg_attr(feature = "typescript", derive(Tsify))] #[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))] pub struct Role { + /// Optional short alias used for command-line role selection. pub shortname: Option, + /// Primary name that uniquely identifies this role. pub name: RoleName, /// The relevance function used to rank search results pub relevance_function: RelevanceFunction, + /// Whether Terraphim-specific post-processing is applied to search results. pub terraphim_it: bool, + /// UI theme name applied when this role is active. pub theme: String, + /// Optional knowledge graph configuration for semantic search. pub kg: Option, + /// Haystack data sources searched under this role. pub haystacks: Vec, /// Enable AI-powered article summaries using LLM providers #[serde(default)] @@ -254,6 +272,7 @@ pub struct Role { /// Maximum tokens for LLM context window (default: 32768) #[serde(default = "default_context_window")] pub llm_context_window: Option, + /// Arbitrary provider-specific or experiment-specific key/value pairs. #[serde(flatten)] #[schemars(skip)] #[cfg_attr(feature = "typescript", tsify(type = "Record"))] @@ -504,7 +523,9 @@ pub struct KnowledgeGraph { pub automata_path: Option, /// Knowlege graph can be re-build from local files, for example Markdown files pub knowledge_graph_local: Option, + /// Whether this knowledge graph is publicly accessible. pub public: bool, + /// Whether this knowledge graph should be published to the registry. pub publish: bool, } impl KnowledgeGraph { @@ -519,7 +540,9 @@ impl KnowledgeGraph { #[cfg_attr(feature = "typescript", derive(Tsify))] #[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))] pub struct KnowledgeGraphLocal { + /// Format of the source documents (e.g. Markdown, plain text). pub input_type: KnowledgeGraphInputType, + /// Filesystem path to the directory containing the source documents. pub path: PathBuf, } /// Builder, which allows to create a new `Config` @@ -933,8 +956,11 @@ impl Default for ConfigBuilder { #[cfg_attr(feature = "typescript", derive(Tsify))] #[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))] pub enum ConfigId { + /// Configuration deployed as a background HTTP server. Server, + /// Configuration deployed as a desktop (Tauri) application. Desktop, + /// Configuration compiled in (WASM/library) with no external server. Embedded, } @@ -955,6 +981,7 @@ pub struct Config { pub roles: AHashMap, /// The default role to use if no role is specified pub default_role: RoleName, + /// The role currently selected by the user (may differ from `default_role`). pub selected_role: RoleName, } @@ -1271,6 +1298,7 @@ impl ConfigState { config.default_role.clone() } + /// Return the currently selected role name. pub async fn get_selected_role(&self) -> RoleName { let config = self.config.lock().await; config.selected_role.clone() diff --git a/crates/terraphim_config/src/project.rs b/crates/terraphim_config/src/project.rs index 57cca6007..2a239314b 100644 --- a/crates/terraphim_config/src/project.rs +++ b/crates/terraphim_config/src/project.rs @@ -1,33 +1,50 @@ use std::path::{Path, PathBuf}; use thiserror::Error; +/// Errors that can occur while discovering or loading a project-level configuration. #[derive(Error, Debug)] pub enum ProjectDiscoveryError { + /// An I/O error while reading configuration files. #[error("IO error: {0}")] Io(#[from] std::io::Error), + /// A JSON deserialisation error in a configuration file. #[error("JSON error: {0}")] Json(#[from] serde_json::Error), + /// The given path exists but is not a directory. #[error("Not a directory: {0}")] NotDirectory(PathBuf), + /// Multiple roles are defined but no explicit or default role was specified. #[error( "multiple project roles found ({available:?}); pass --role or set selected/default role" )] - AmbiguousRole { available: Vec }, + AmbiguousRole { + /// Names of the available roles that the caller must choose from. + available: Vec, + }, } +/// Project-local configuration loaded from a `.terraphim/` directory. +/// +/// Allows teams to ship a `.terraphim/config.json` (or per-role `role-*.json` files) +/// alongside their source code to override global Terraphim settings. #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] pub struct ProjectConfig { + /// Optional global keyboard shortcut override for this project. #[serde(default)] pub global_shortcut: Option, + /// Name of the default role to use when no role is specified. #[serde(default)] pub default_role: Option, + /// Name of the currently selected role (persisted across sessions). #[serde(default)] pub selected_role: Option, + /// Role definitions keyed by role name. #[serde(default)] pub roles: std::collections::HashMap, } impl ProjectConfig { + /// Load a `ProjectConfig` from a single JSON file. pub fn from_file(path: &Path) -> Result { let content = std::fs::read_to_string(path)?; let config: ProjectConfig = serde_json::from_str(&content)?; @@ -68,10 +85,22 @@ impl ProjectConfig { Ok(config) } + /// Return `true` if no shortcut and no roles are configured. pub fn is_empty(&self) -> bool { self.global_shortcut.is_none() && self.roles.is_empty() } + /// Resolve the effective role name from an optional explicit override. + /// + /// Resolution order: + /// 1. Explicit `--role` flag (if non-empty) + /// 2. `selected_role` (if present in `roles`) + /// 3. `default_role` (if present in `roles`) + /// 4. The sole role (if exactly one role is configured) + /// 5. `None` (no roles configured) + /// + /// Returns [`ProjectDiscoveryError::AmbiguousRole`] when multiple roles exist + /// and no selection can be made automatically. pub fn resolve_role_name( &self, explicit_role: Option<&str>, @@ -126,6 +155,11 @@ pub fn discover_kg_path(dir: &Path, role_name: Option<&str>) -> Option if kg_dir.is_dir() { Some(kg_dir) } else { None } } +/// Walk up the directory tree from `start_dir` looking for a `.terraphim/` directory. +/// +/// Returns the canonicalised path to the first `.terraphim/` directory found, or +/// `None` if no such directory exists anywhere up to the filesystem root. +/// Uses the current working directory when `start_dir` is `None`. pub fn discover(start_dir: Option<&Path>) -> Result, ProjectDiscoveryError> { let start_dir = match start_dir { Some(d) => d.to_path_buf(), diff --git a/crates/terraphim_service/src/conversation_service.rs b/crates/terraphim_service/src/conversation_service.rs index 55f8a97e5..8fd43b267 100644 --- a/crates/terraphim_service/src/conversation_service.rs +++ b/crates/terraphim_service/src/conversation_service.rs @@ -29,10 +29,15 @@ pub struct ConversationFilter { /// Statistics about conversations #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ConversationStatistics { + /// Total number of conversations stored pub total_conversations: usize, + /// Total number of messages across all conversations pub total_messages: usize, + /// Total number of context items across all conversations pub total_context_items: usize, + /// Number of conversations grouped by role name pub conversations_by_role: std::collections::HashMap, + /// Mean number of messages per conversation pub average_messages_per_conversation: f64, } diff --git a/crates/terraphim_service/src/error.rs b/crates/terraphim_service/src/error.rs index 23790e0ce..26ecd38fa 100644 --- a/crates/terraphim_service/src/error.rs +++ b/crates/terraphim_service/src/error.rs @@ -45,46 +45,69 @@ pub enum ErrorCategory { /// Common error patterns used across terraphim crates #[derive(Error, Debug)] pub enum CommonError { + /// A network-level error such as a connection failure or timeout #[error("Network error: {message}")] Network { + /// Human-readable description of the network error message: String, + /// Underlying error that caused this network failure, if any #[source] source: Option>, }, + /// An error caused by invalid or missing configuration #[error("Configuration error: {message}")] Configuration { + /// Human-readable description of the configuration problem message: String, + /// The specific configuration field that is invalid or missing field: Option, }, + /// An error caused by invalid input data #[error("Validation error: {message}")] Validation { + /// Human-readable description of the validation failure message: String, + /// The specific field that failed validation field: Option, }, + /// An authentication or authorisation error #[error("Authentication error: {message}")] - Auth { message: String }, + Auth { + /// Human-readable description of the authentication error + message: String, + }, + /// An error arising from the storage or persistence layer #[error("Storage error: {message}")] Storage { + /// Human-readable description of the storage error message: String, + /// Underlying error from the storage backend, if any #[source] source: Option>, }, + /// An error from an external service integration #[error("Integration error with {service}: {message}")] Integration { + /// Name of the external service that produced the error service: String, + /// Human-readable description of the integration error message: String, + /// Underlying error returned by the external service, if any #[source] source: Option>, }, + /// An internal system error #[error("System error: {message}")] System { + /// Human-readable description of the system error message: String, + /// Underlying cause of the system error, if any #[source] source: Option>, }, @@ -113,6 +136,7 @@ impl TerraphimError for CommonError { /// Helper functions for creating common error types impl CommonError { + /// Create a network error with the given message and no underlying source pub fn network(message: impl Into) -> Self { CommonError::Network { message: message.into(), @@ -120,6 +144,7 @@ impl CommonError { } } + /// Create a network error wrapping an existing source error pub fn network_with_source( message: impl Into, source: impl std::error::Error + Send + Sync + 'static, @@ -130,6 +155,7 @@ impl CommonError { } } + /// Create a configuration error with the given message and no specific field pub fn config(message: impl Into) -> Self { CommonError::Configuration { message: message.into(), @@ -137,6 +163,7 @@ impl CommonError { } } + /// Create a configuration error pointing to a specific configuration field pub fn config_field(message: impl Into, field: impl Into) -> Self { CommonError::Configuration { message: message.into(), @@ -144,6 +171,7 @@ impl CommonError { } } + /// Create a validation error with the given message and no specific field pub fn validation(message: impl Into) -> Self { CommonError::Validation { message: message.into(), @@ -151,6 +179,7 @@ impl CommonError { } } + /// Create a validation error pointing to a specific input field pub fn validation_field(message: impl Into, field: impl Into) -> Self { CommonError::Validation { message: message.into(), @@ -158,12 +187,14 @@ impl CommonError { } } + /// Create an authentication error with the given message pub fn auth(message: impl Into) -> Self { CommonError::Auth { message: message.into(), } } + /// Create a storage error with the given message and no underlying source pub fn storage(message: impl Into) -> Self { CommonError::Storage { message: message.into(), @@ -171,6 +202,7 @@ impl CommonError { } } + /// Create a storage error wrapping an existing source error pub fn storage_with_source( message: impl Into, source: impl std::error::Error + Send + Sync + 'static, @@ -181,6 +213,7 @@ impl CommonError { } } + /// Create an integration error for the named external service with no source pub fn integration(service: impl Into, message: impl Into) -> Self { CommonError::Integration { service: service.into(), @@ -189,6 +222,7 @@ impl CommonError { } } + /// Create an integration error for the named external service wrapping a source error pub fn integration_with_source( service: impl Into, message: impl Into, @@ -201,6 +235,7 @@ impl CommonError { } } + /// Create a system error with the given message and no underlying source pub fn system(message: impl Into) -> Self { CommonError::System { message: message.into(), @@ -208,6 +243,7 @@ impl CommonError { } } + /// Create a system error wrapping an existing source error pub fn system_with_source( message: impl Into, source: impl std::error::Error + Send + Sync + 'static, diff --git a/crates/terraphim_service/src/lib.rs b/crates/terraphim_service/src/lib.rs index 626871298..0ca578dfb 100644 --- a/crates/terraphim_service/src/lib.rs +++ b/crates/terraphim_service/src/lib.rs @@ -1,3 +1,4 @@ +#![warn(missing_docs)] //! Main service layer for Terraphim AI. //! //! Provides document search, indexing, and AI-assisted summarisation across @@ -27,35 +28,32 @@ pub use auto_route::{ #[cfg(feature = "openrouter")] pub mod openrouter; -// Generic LLM layer for multiple providers (OpenRouter, Ollama, etc.) +/// Generic LLM client trait and provider implementations (OpenRouter, Ollama, etc.). pub mod llm; -// LLM proxy service for unified provider management - -// LLM Proxy service\npub mod proxy_client; -// LLM Router configuration integration\n - pub mod llm_proxy; -// LLM Router configuration integration\n - -// Centralized HTTP client creation and configuration +/// Centralised HTTP client creation with connection pooling and timeout configuration. pub mod http_client; // Standardized logging initialization utilities pub mod logging; -// Summarization queue system for production-ready async processing +/// Async conversation service backed by a persistent store. pub mod conversation_service; +/// Rate-limiting utilities for outbound API calls. pub mod rate_limiter; +/// Manages the lifecycle of document summarisation tasks. pub mod summarization_manager; +/// Bounded queue for deduplicating and batching summarisation requests. pub mod summarization_queue; +/// Background worker that processes the summarisation queue. pub mod summarization_worker; -// Centralized error handling patterns and utilities +/// Centralised error handling patterns and shared error types. pub mod error; -// Context management for LLM conversations +/// Context management: assembles and trims LLM conversation history. pub mod context; #[cfg(test)] @@ -72,22 +70,28 @@ fn normalize_filename_to_id(filename: &str) -> String { /// Top-level error type for the Terraphim service layer. #[derive(thiserror::Error, Debug)] pub enum ServiceError { + /// An error from the haystack middleware layer. #[error("Middleware error: {0}")] Middleware(#[from] terraphim_middleware::Error), + /// An error from the OpenDAL storage abstraction. #[error("OpenDal error: {0}")] OpenDal(Box), + /// An error from the persistence layer. #[error("Persistence error: {0}")] Persistence(#[from] terraphim_persistence::Error), + /// A configuration-related error with a descriptive message. #[error("Config error: {0}")] Config(String), #[cfg(feature = "openrouter")] + /// An error from the OpenRouter LLM provider. #[error("OpenRouter error: {0}")] OpenRouter(#[from] crate::openrouter::OpenRouterError), + /// A common/shared error from the service error module. #[error("Common error: {0}")] Common(#[from] crate::error::CommonError), } @@ -125,6 +129,7 @@ impl crate::error::TerraphimError for ServiceError { } } +/// Convenience alias for `Result` used throughout this crate. pub type Result = std::result::Result; /// Main entry point for search, indexing, and AI operations in Terraphim. diff --git a/crates/terraphim_service/src/llm.rs b/crates/terraphim_service/src/llm.rs index a5bf9451f..58ce1c08d 100644 --- a/crates/terraphim_service/src/llm.rs +++ b/crates/terraphim_service/src/llm.rs @@ -17,22 +17,33 @@ mod router_config; use crate::Result as ServiceResult; +/// Options controlling the output of a document summarisation call. #[derive(Clone, Debug)] pub struct SummarizeOptions { + /// Approximate maximum character length of the generated summary. pub max_length: usize, } #[allow(dead_code)] +/// Options for a chat completion request. #[derive(Clone, Debug)] pub struct ChatOptions { + /// Maximum number of output tokens to generate. pub max_tokens: Option, + /// Sampling temperature (0.0 = deterministic, higher = more creative). pub temperature: Option, } +/// Abstraction over LLM providers (Ollama, OpenRouter, …). +/// +/// Implement this trait to add a new backend. All methods are async and +/// must be `Send + Sync` so the client can be shared across tokio tasks. #[async_trait::async_trait] pub trait LlmClient: Send + Sync { + /// Return a human-readable name identifying this provider. fn name(&self) -> &'static str; + /// Summarise the given content and return a shorter version. async fn summarize(&self, content: &str, opts: SummarizeOptions) -> ServiceResult; /// List available models for this provider (best-effort) diff --git a/crates/terraphim_service/src/llm_proxy.rs b/crates/terraphim_service/src/llm_proxy.rs index ad9e6ae7a..b61030b33 100644 --- a/crates/terraphim_service/src/llm_proxy.rs +++ b/crates/terraphim_service/src/llm_proxy.rs @@ -10,24 +10,40 @@ use std::env; use std::time::Duration; use thiserror::Error; +/// Errors that can occur when using the LLM proxy service #[derive(Error, Debug)] pub enum LlmProxyError { + /// The proxy or provider configuration is invalid #[error("Invalid configuration: {0}")] ConfigError(String), + /// A network-level error occurred when contacting the provider #[error("Network error: {0}")] NetworkError(String), + /// Authentication with the specified provider failed #[error("Authentication failed for provider: {provider}")] - AuthError { provider: String }, + AuthError { + /// Name of the provider that rejected the credentials + provider: String, + }, + /// The provider's rate limit was exceeded #[error("Rate limit exceeded for provider: {provider}")] - RateLimitError { provider: String }, + RateLimitError { + /// Name of the provider that returned the rate-limit response + provider: String, + }, + /// The requested provider is not recognised or supported #[error("Provider not supported: {provider}")] - UnsupportedProvider { provider: String }, + UnsupportedProvider { + /// Name of the unsupported provider + provider: String, + }, } +/// Convenience `Result` alias that uses [`LlmProxyError`] as the error type pub type Result = std::result::Result; /// Configuration for LLM proxy settings @@ -35,12 +51,19 @@ pub type Result = std::result::Result; /// `api_key` is redacted in `Debug` output to prevent credential leakage in logs. #[derive(Clone)] pub struct ProxyConfig { + /// Name of the LLM provider (e.g. `"anthropic"`, `"openrouter"`, `"ollama"`) pub provider: String, + /// Model identifier to use when sending requests to the provider pub model: String, + /// Optional base URL override; if absent the provider's default endpoint is used pub base_url: Option, + /// API key for authenticating with the provider; redacted in `Debug` output pub api_key: Option, + /// Maximum time to wait for a single request before aborting pub timeout: Duration, + /// Maximum number of retry attempts on transient failures pub max_retries: u32, + /// Whether to fall back to the provider's direct endpoint when the proxy is unreachable pub enable_fallback: bool, } @@ -102,6 +125,7 @@ impl ProxyConfig { pub struct LlmProxyClient { client: Client, configs: HashMap, + /// Name of the provider used when no explicit provider is specified pub default_provider: String, } diff --git a/crates/terraphim_service/src/logging.rs b/crates/terraphim_service/src/logging.rs index 6fa5487f0..49a1d66e5 100644 --- a/crates/terraphim_service/src/logging.rs +++ b/crates/terraphim_service/src/logging.rs @@ -47,7 +47,10 @@ pub enum LoggingConfig { /// Integration test logging (INFO level, reduced noise) IntegrationTest, /// Custom logging level - Custom { level: log::LevelFilter }, + Custom { + /// The log level filter to apply. + level: log::LevelFilter, + }, } /// Initialize logging based on configuration preset diff --git a/crates/terraphim_service/src/summarization_queue.rs b/crates/terraphim_service/src/summarization_queue.rs index 43773c680..0c0eef856 100644 --- a/crates/terraphim_service/src/summarization_queue.rs +++ b/crates/terraphim_service/src/summarization_queue.rs @@ -21,6 +21,7 @@ impl Default for TaskId { } impl TaskId { + /// Create a new randomly-generated task identifier pub fn new() -> Self { Self(Uuid::new_v4()) } @@ -51,35 +52,49 @@ pub enum Priority { pub enum TaskStatus { /// Task is queued and waiting to be processed Pending { + /// Timestamp when the task was added to the queue queued_at: DateTime, + /// Current position of the task in the queue, if known position_in_queue: Option, }, /// Task is currently being processed Processing { + /// Timestamp when processing began started_at: DateTime, + /// Processing progress as a fraction in `[0.0, 1.0]`, if available progress: Option, }, /// Task completed successfully Completed { + /// The generated summary text summary: String, + /// Timestamp when processing finished completed_at: DateTime, + /// Wall-clock time taken to produce the summary, in seconds processing_duration_seconds: u64, }, /// Task failed with error Failed { + /// Human-readable description of the failure error: String, + /// Timestamp when the failure was recorded failed_at: DateTime, + /// Number of attempts made so far retry_count: u32, + /// Earliest time at which the task may be retried, if applicable next_retry_at: Option>, }, /// Task was cancelled Cancelled { + /// Timestamp when the cancellation was recorded cancelled_at: DateTime, + /// Human-readable reason for the cancellation reason: String, }, } impl TaskStatus { + /// Returns `true` if the task has reached a terminal state (completed, failed, or cancelled) pub fn is_terminal(&self) -> bool { matches!( self, @@ -87,10 +102,12 @@ impl TaskStatus { ) } + /// Returns `true` if the task is actively being processed pub fn is_processing(&self) -> bool { matches!(self, TaskStatus::Processing { .. }) } + /// Returns `true` if the task is waiting in the queue pub fn is_pending(&self) -> bool { matches!(self, TaskStatus::Pending { .. }) } @@ -124,6 +141,7 @@ pub struct SummarizationTask { } impl SummarizationTask { + /// Create a new summarization task for `document` using the given `role` configuration pub fn new(document: Document, role: Role) -> Self { Self { id: TaskId::new(), @@ -140,44 +158,53 @@ impl SummarizationTask { } } + /// Set the task priority, returning the updated task pub fn with_priority(mut self, priority: Priority) -> Self { self.priority = priority; self } + /// Set the maximum number of retry attempts, returning the updated task pub fn with_max_retries(mut self, max_retries: u32) -> Self { self.max_retries = max_retries; self } + /// Set the maximum desired summary length in words, returning the updated task pub fn with_max_summary_length(mut self, length: usize) -> Self { self.max_summary_length = Some(length); self } + /// Set whether to force regeneration even if a summary already exists pub fn with_force_regenerate(mut self, force: bool) -> Self { self.force_regenerate = force; self } + /// Set the callback URL to notify on completion, returning the updated task pub fn with_callback_url(mut self, url: String) -> Self { self.callback_url = Some(url); self } + /// Attach a global configuration for fallback provider settings pub fn with_config(mut self, config: Config) -> Self { self.config = Some(config); self } + /// Returns `true` if the task has not yet exhausted its retry budget pub fn can_retry(&self) -> bool { self.retry_count < self.max_retries } + /// Increment the retry counter by one pub fn increment_retry(&mut self) { self.retry_count += 1; } + /// Return the effective maximum summary length, falling back to 250 if unset pub fn get_summary_length(&self) -> usize { self.max_summary_length.unwrap_or(250) } @@ -305,8 +332,11 @@ pub struct RateLimiterStatus { pub enum SubmitResult { /// Task was successfully queued Queued { + /// Identifier assigned to the newly queued task task_id: TaskId, + /// One-based position of the task in the queue at submission time position_in_queue: usize, + /// Estimated seconds until the task begins processing, if calculable estimated_wait_time_seconds: Option, }, /// Task was rejected due to queue being full diff --git a/crates/terraphim_types/src/lib.rs b/crates/terraphim_types/src/lib.rs index d54ee81c0..96b21f0cf 100644 --- a/crates/terraphim_types/src/lib.rs +++ b/crates/terraphim_types/src/lib.rs @@ -1,3 +1,4 @@ +#![warn(missing_docs)] //! Core type definitions for the Terraphim AI system. //! //! This crate provides the fundamental data structures used throughout the Terraphim ecosystem: @@ -1827,6 +1828,10 @@ impl std::fmt::Display for RotStatus { } } +/// A multi-turn conversation between a user and the Terraphim AI system. +/// +/// Holds the full message history, shared context items, and metadata +/// needed to continue or replay the conversation. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(Tsify))] #[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))] diff --git a/crates/terraphim_types/src/shared_learning.rs b/crates/terraphim_types/src/shared_learning.rs index a1d1a482f..f49ad6058 100644 --- a/crates/terraphim_types/src/shared_learning.rs +++ b/crates/terraphim_types/src/shared_learning.rs @@ -33,6 +33,7 @@ pub enum TrustLevel { } impl TrustLevel { + /// Return the short canonical string representation (`"L0"`, `"L1"`, …). pub fn as_str(&self) -> &'static str { match self { TrustLevel::L0 => "L0", @@ -42,6 +43,7 @@ impl TrustLevel { } } + /// Numeric weight used for ordering trust levels (0 = lowest, 3 = highest). pub fn weight(&self) -> u8 { match self { TrustLevel::L0 => 0, @@ -51,10 +53,12 @@ impl TrustLevel { } } + /// Return `true` if this trust level allows synchronisation to the wiki. pub fn allows_wiki_sync(&self) -> bool { matches!(self, TrustLevel::L2 | TrustLevel::L3) } + /// Return a human-readable label (e.g. `"Peer-Validated"` for L2). pub fn display_name(&self) -> &'static str { match self { TrustLevel::L0 => "Extracted", @@ -91,8 +95,10 @@ impl PartialOrd for TrustLevel { } } +/// Errors from parsing a [`TrustLevel`] from a string. #[derive(Error, Debug)] pub enum TrustLevelError { + /// The supplied string did not match any known trust level. #[error("invalid trust level: {0}")] InvalidTrustLevel(String), } @@ -101,10 +107,15 @@ pub enum TrustLevelError { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum LearningCategory { + /// A technical insight about code, tools, or infrastructure. Technical, + /// A process or workflow improvement. Process, + /// Domain-specific knowledge about the problem space. Domain, + /// A failure pattern to avoid. Failure, + /// A success pattern to replicate. SuccessPattern, } @@ -127,8 +138,11 @@ impl std::fmt::Display for LearningCategory { /// remains free of async runtime dependencies. Implementations that need /// async I/O can use internal synchronisation (e.g. `tokio::runtime::Handle`). pub trait LearningStore: Send + Sync { + /// Persist a new learning and return its assigned ID. fn insert(&self, learning: SharedLearning) -> Result; + /// Retrieve a learning by its unique ID. fn get(&self, id: &str) -> Result; + /// Return learnings relevant to an agent and context, filtered by minimum trust level. fn query_relevant( &self, agent: &str, @@ -136,9 +150,13 @@ pub trait LearningStore: Send + Sync { min_trust: TrustLevel, limit: usize, ) -> Result, StoreError>; + /// Record that a learning was applied by the named agent. fn record_applied(&self, id: &str, applied_by: &str) -> Result<(), StoreError>; + /// Record that a learning proved effective after being applied. fn record_effective(&self, id: &str, applied_by: &str) -> Result<(), StoreError>; + /// List all learnings at or above the given trust level. fn list_by_trust(&self, min_trust: TrustLevel) -> Result, StoreError>; + /// Archive (remove) learnings older than `max_age_days`; returns count archived. fn archive_stale(&self, max_age_days: u32) -> Result; } @@ -151,6 +169,7 @@ pub struct InMemoryLearningStore { } impl InMemoryLearningStore { + /// Create a new empty in-memory store. pub fn new() -> Self { Self { learnings: std::sync::Mutex::new(HashMap::new()), @@ -340,9 +359,12 @@ impl QualityMetrics { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum SuggestionStatus { + /// Awaiting human review. #[default] Pending, + /// Approved by a human reviewer. Approved, + /// Rejected by a human reviewer. Rejected, } @@ -672,14 +694,19 @@ fn timestamp_millis() -> u64 { /// Error type for store operations #[derive(Error, Debug)] pub enum StoreError { + /// A storage backend error (e.g. database write failure). #[error("persistence error: {0}")] Persistence(String), + /// No learning with the given ID was found. #[error("learning not found: {0}")] NotFound(String), + /// An error occurred during BM25 relevance calculation. #[error("BM25 calculation error: {0}")] Bm25(String), + /// The provided input is invalid. #[error("invalid input: {0}")] InvalidInput(String), + /// JSON serialisation or deserialisation failed. #[error("serialization error: {0}")] Serialization(#[from] serde_json::Error), } From 022074d2f36f3f9fb9ec083b766df002fc6f326b Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 2 Jun 2026 12:55:16 +0200 Subject: [PATCH 2/4] docs: fill all 675 missing doc gaps in terraphim_orchestrator Systematic documentation sweep across all 44 source files in the orchestrator crate, eliminating every missing_docs warning: - Modules: all 15 pub mod declarations annotated - Error types: OrchestratorError variants + fields, LearningError, CompileError, RunRecordError, MetricsPersistenceError - Agent system: AgentRunRequest, GiteaTargetReport, TriggerMode, ModeResult, SyntheticEvent, AgentRuntimeValidationReport, AgentSubcommand, OutputFormat, AgentStatus, AgentKey, RegisteredAgent - Flow system: FlowDefinition, FlowStepDef, StepKind, FailStrategy, FlowRunState, FlowRunStatus, StepEnvelope, MatrixResult, TokenUsage, FlowExecutor, ProjectRuntime - Configuration: LearningConfig, EvolutionConfig, GiteaOutputConfig, SfiaSkillRef, TomlProjectAdfConfig, TomlAdfAgent, RoutingDecision - PR lifecycle: PrSummary, PrComment, MergeOutcome, AutoMergeCriteria, PrMetadata, PrGateDecision, DispatchResponse, ReviewPrRequest - Telemetry: TokenBreakdown, ModelUsage, ModelPerformanceSnapshot - Learning & evolution: Learning, NewLearning, SharedLearningStore - Control plane: RoutingDecisionEngine, WebhookContext, ScoredCandidate - Utilities: DriftScore, DriftAlert, LocalSkillConfig, CostSnapshot, WebhookDispatch, GiteaComment, all payload/event types All 675 gaps now resolved. Crate compiles cleanly with 0 warnings. Refs #1979 --- .../src/adf_commands.rs | 20 +++- .../src/agent_registry.rs | 11 +++ .../src/agent_run_command.rs | 44 +++++++++ .../src/agent_run_record.rs | 3 + .../src/agent_runner.rs | 76 +++++++++++++++ crates/terraphim_orchestrator/src/bin/adf.rs | 4 + crates/terraphim_orchestrator/src/config.rs | 31 +++++- .../src/control_plane/events.rs | 6 ++ .../src/control_plane/policy.rs | 4 + .../src/control_plane/routing.rs | 43 +++++++++ .../src/control_plane/telemetry.rs | 17 ++++ .../src/cost_tracker.rs | 18 +++- .../src/direct_dispatch.rs | 33 ++++--- .../terraphim_orchestrator/src/dual_mode.rs | 12 ++- crates/terraphim_orchestrator/src/error.rs | 96 +++++++++++++++++-- .../src/error_signatures.rs | 3 + .../terraphim_orchestrator/src/evolution.rs | 28 ++++++ .../terraphim_orchestrator/src/flow/config.rs | 27 +++++- .../src/flow/envelope.rs | 15 +++ .../src/flow/executor.rs | 9 ++ crates/terraphim_orchestrator/src/flow/mod.rs | 5 + .../terraphim_orchestrator/src/flow/state.rs | 27 ++++++ .../src/flow/token_parser.rs | 6 ++ .../terraphim_orchestrator/src/kg_router.rs | 3 + crates/terraphim_orchestrator/src/learning.rs | 43 +++++++++ crates/terraphim_orchestrator/src/lib.rs | 37 +++++-- .../src/local_skills.rs | 13 +++ crates/terraphim_orchestrator/src/mention.rs | 22 ++++- .../src/mention_chain.rs | 18 +++- .../src/meta_coordinator.rs | 12 ++- .../src/metrics_persistence.rs | 3 + .../terraphim_orchestrator/src/nightwatch.rs | 9 ++ crates/terraphim_orchestrator/src/persona.rs | 3 + .../src/post_merge_gate.rs | 9 ++ .../terraphim_orchestrator/src/pr_dispatch.rs | 6 ++ crates/terraphim_orchestrator/src/pr_gate.rs | 29 +++++- .../terraphim_orchestrator/src/pr_poller.rs | 31 +++++- .../terraphim_orchestrator/src/pr_review.rs | 15 +++ .../terraphim_orchestrator/src/project_adf.rs | 40 ++++++++ .../src/provider_budget.rs | 4 + .../src/provider_probe.rs | 14 +++ crates/terraphim_orchestrator/src/quickwit.rs | 18 ++++ .../terraphim_orchestrator/src/scheduler.rs | 6 +- crates/terraphim_orchestrator/src/webhook.rs | 68 +++++++++++++ 44 files changed, 888 insertions(+), 53 deletions(-) diff --git a/crates/terraphim_orchestrator/src/adf_commands.rs b/crates/terraphim_orchestrator/src/adf_commands.rs index 3a85fc11a..307687e44 100644 --- a/crates/terraphim_orchestrator/src/adf_commands.rs +++ b/crates/terraphim_orchestrator/src/adf_commands.rs @@ -11,23 +11,39 @@ use terraphim_types::{NormalizedTerm, NormalizedTermValue, Thesaurus}; #[derive(Debug, Clone, PartialEq)] pub enum AdfCommand { /// Trigger a compound review - CompoundReview { issue_number: u64, comment_id: u64 }, + CompoundReview { + /// Issue number where the command appeared. + issue_number: u64, + /// Comment ID that contained the command. + comment_id: u64, + }, /// Spawn a specific agent SpawnAgent { + /// Name of the agent to spawn (e.g. `security-sentinel`). agent_name: String, + /// Issue number where the command appeared. issue_number: u64, + /// Comment ID that contained the command. comment_id: u64, + /// Text following the command on the same line, used as task context. context: String, }, /// Trigger a persona-based agent SpawnPersona { + /// Name of the persona to activate. persona_name: String, + /// Issue number where the command appeared. issue_number: u64, + /// Comment ID that contained the command. comment_id: u64, + /// Text following the command on the same line, used as task context. context: String, }, /// Unknown command - Unknown { raw: String }, + Unknown { + /// Raw matched text that could not be classified. + raw: String, + }, } /// Parser for ADF commands using terraphim-automata diff --git a/crates/terraphim_orchestrator/src/agent_registry.rs b/crates/terraphim_orchestrator/src/agent_registry.rs index 6482a9eb4..91259ef6b 100644 --- a/crates/terraphim_orchestrator/src/agent_registry.rs +++ b/crates/terraphim_orchestrator/src/agent_registry.rs @@ -39,11 +39,14 @@ impl AgentScope { /// Stable key for a registered agent definition. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct AgentKey { + /// Scope that qualifies this agent (legacy or project-specific). pub scope: AgentScope, + /// Name of the agent as declared in the config. pub name: String, } impl AgentKey { + /// Construct a key from an explicit scope and name. pub fn new(scope: AgentScope, name: impl Into) -> Self { Self { scope, @@ -51,10 +54,12 @@ impl AgentKey { } } + /// Construct a project-scoped key from a project id and agent name. pub fn project(project: impl Into, name: impl Into) -> Self { Self::new(AgentScope::Project(project.into()), name) } + /// Construct a legacy (single-project) key from an agent name. pub fn legacy(name: impl Into) -> Self { Self::new(AgentScope::Legacy, name) } @@ -76,16 +81,21 @@ pub enum AgentSource { /// Registry entry for an agent definition. #[derive(Debug, Clone)] pub struct RegisteredAgent { + /// Stable lookup key for this agent within the registry. pub key: AgentKey, + /// Full agent definition as loaded from the merged config. pub definition: AgentDefinition, + /// Where this entry originated (always [`AgentSource::ConfigMerged`] for now). pub source: AgentSource, } impl RegisteredAgent { + /// Return the project id this agent belongs to, or `None` for legacy agents. pub fn project_id(&self) -> Option<&str> { self.definition.project.as_deref() } + /// Return `true` if this agent should only run in response to events, not on a schedule. pub fn event_only(&self) -> bool { self.definition.event_only } @@ -138,6 +148,7 @@ impl AgentRegistry { self.by_key.len() } + /// Return `true` if no agents are registered. pub fn is_empty(&self) -> bool { self.by_key.is_empty() } diff --git a/crates/terraphim_orchestrator/src/agent_run_command.rs b/crates/terraphim_orchestrator/src/agent_run_command.rs index 18d2493d4..ff0d185f1 100644 --- a/crates/terraphim_orchestrator/src/agent_run_command.rs +++ b/crates/terraphim_orchestrator/src/agent_run_command.rs @@ -31,6 +31,7 @@ fn parse_cron(expr: &str) -> Result { .map_err(|e| OrchestratorError::Config(format!("invalid cron '{}': {}", expr, e))) } +/// Return all trigger modes applicable to the given agent definition. pub fn applicable_modes(agent: &AgentDefinition) -> Vec { let mut modes = vec![TriggerMode::Local]; if agent.schedule.is_some() { @@ -46,6 +47,7 @@ pub fn applicable_modes(agent: &AgentDefinition) -> Vec { modes } +/// Return the cron schedule string for a named agent, if one is configured. pub fn schedule_for_agent(config: &OrchestratorConfig, agent_name: &str) -> Option { config .agents @@ -54,6 +56,7 @@ pub fn schedule_for_agent(config: &OrchestratorConfig, agent_name: &str) -> Opti .and_then(|a| a.schedule.clone()) } +/// Return `true` if the given cron expression can be parsed successfully. pub fn is_cron_schedule_valid(expr: &str) -> bool { parse_cron(expr).is_ok() } @@ -119,6 +122,9 @@ fn validate_agent_mode( } } +/// Validate an agent against all of its applicable trigger modes. +/// +/// Returns the overall runtime validation report and a per-mode result map. pub fn validate_agent_all_modes( config: &OrchestratorConfig, agent: &AgentDefinition, @@ -169,30 +175,48 @@ pub fn validate_agent_all_modes( (report, mode_results) } +/// Parsed subcommand for the `agent` CLI entry-point. #[derive(Debug, Clone, PartialEq, Eq)] pub enum AgentSubcommand { + /// Validate a single agent or all agents if no name is provided. Validate { + /// Name of the agent to validate, or `None` to validate all agents. agent_name: Option, + /// Optional project context to restrict the validation scope. project: Option, + /// Output format for the validation report. format: OutputFormat, + /// Skip the live model availability probe. skip_model_probe: bool, }, + /// Validate all agents defined in an explicit config file. ValidateAll { + /// Path to the orchestrator configuration file. config: PathBuf, + /// Output format for the validation report. format: OutputFormat, + /// Skip the live model availability probe. skip_model_probe: bool, }, + /// Run an agent with a synthetic trigger event for testing purposes. RunSynthetic { + /// Name of the agent to run. agent_name: String, + /// Optional project context for the run. project: Option, + /// The synthetic event to inject as the trigger. event: SyntheticEvent, + /// Output format for the run report. format: OutputFormat, }, } +/// Output format for validation and run reports. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum OutputFormat { + /// Human-readable plain-text output. Human, + /// Machine-readable JSON output (default). #[default] Json, } @@ -208,16 +232,27 @@ impl std::str::FromStr for OutputFormat { } } +/// Aggregate report produced by `validate-all`, covering every configured agent. #[derive(Debug, Clone, Serialize)] pub struct AgentValidateAllReport { + /// Per-agent runtime validation reports, keyed by agent name. pub agents: HashMap, + /// Per-agent, per-mode validation results, keyed by agent name then trigger mode. pub mode_results: HashMap>, + /// Total number of agents validated. pub total: usize, + /// Number of agents that are fully runnable across all modes. pub runnable: usize, + /// Number of agents that failed validation in at least one mode. pub failed: usize, + /// `true` if every agent is runnable in all its applicable modes. pub all_modes_runnable: bool, } +/// Parse CLI arguments into an `AgentSubcommand`. +/// +/// Supports `validate`, `validate-all`, and `run` subcommands with their +/// respective flags. pub fn parse_agent_args(args: &[String]) -> Result { let mut iter = args.iter(); let mut subcommand: Option = None; @@ -329,6 +364,9 @@ pub fn parse_agent_args(args: &[String]) -> Result { } } +/// Run the `validate` subcommand, printing a report and returning an exit code. +/// +/// Validates a single named agent or all agents when `agent_name` is `None`. pub fn run_validate( config: &OrchestratorConfig, agent_name: Option, @@ -388,6 +426,9 @@ pub fn run_validate( } } +/// Run the `validate-all` subcommand, loading config from `config` path. +/// +/// Validates every agent defined in the file and prints an aggregate report. pub fn run_validate_all( config: PathBuf, format: OutputFormat, @@ -431,6 +472,9 @@ pub fn run_validate_all( } } +/// Run the `run` subcommand with a synthetic trigger event. +/// +/// Currently not fully implemented; prints a stub report and returns exit code 1. pub fn run_synthetic( _config: &OrchestratorConfig, agent_name: &str, diff --git a/crates/terraphim_orchestrator/src/agent_run_record.rs b/crates/terraphim_orchestrator/src/agent_run_record.rs index 2436547d2..e132965f7 100644 --- a/crates/terraphim_orchestrator/src/agent_run_record.rs +++ b/crates/terraphim_orchestrator/src/agent_run_record.rs @@ -579,12 +579,15 @@ pub trait RunRecordPersistence: Send + Sync { #[derive(Debug, thiserror::Error)] pub enum RunRecordError { #[error("storage error: {0}")] + /// A low-level storage backend error. Storage(String), #[error("serialization error: {0}")] + /// JSON serialisation or deserialisation failed. Serialization(#[from] serde_json::Error), #[error("IO error: {0}")] + /// An I/O error occurred during persistence. Io(#[from] std::io::Error), } diff --git a/crates/terraphim_orchestrator/src/agent_runner.rs b/crates/terraphim_orchestrator/src/agent_runner.rs index fc5168f40..63d0d8154 100644 --- a/crates/terraphim_orchestrator/src/agent_runner.rs +++ b/crates/terraphim_orchestrator/src/agent_runner.rs @@ -7,13 +7,17 @@ use crate::{AgentOrchestrator, OrchestratorError}; const LEGACY_PROJECT: &str = ""; +/// Request parameters for running or validating a named agent. #[derive(Debug, Clone)] pub struct AgentRunRequest { + /// Name of the agent to run, as declared in the orchestrator configuration. pub agent_name: String, + /// Optional project scope. When set, only agents belonging to this project are considered. pub project: Option, } impl AgentRunRequest { + /// Create a new run request for the given agent name with no project scope. pub fn new(agent_name: impl Into) -> Self { Self { agent_name: agent_name.into(), @@ -21,38 +25,58 @@ impl AgentRunRequest { } } + /// Attach a project scope to this request, restricting resolution to agents in that project. pub fn with_project(mut self, project: impl Into) -> Self { self.project = Some(project.into()); self } } +/// Serialisable report describing the Gitea repository target associated with an agent. #[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub struct GiteaTargetReport { + /// Base URL of the Gitea instance (e.g. `https://git.example.com`). pub base_url: String, + /// Repository owner (organisation or user). pub owner: String, + /// Repository name. pub repo: String, + /// Optional Gitea issue number linked to this agent. pub issue: Option, } +/// How an agent execution was initiated. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] #[serde(rename_all = "snake_case")] pub enum TriggerMode { + /// Agent was started by a cron schedule. Cron, + /// Agent was triggered by a @-mention in a Gitea issue or PR comment. Mention, + /// Agent was triggered by a git push event. Push, + /// Agent was triggered by a pull-request event. PullRequest, + /// Agent was invoked directly from the local machine. Local, + /// Agent was triggered by an incoming webhook. Webhook, } +/// Result of probing a single trigger mode for a given agent. #[derive(Debug, Clone, Serialize, Eq)] pub struct ModeResult { + /// The trigger mode that was evaluated. pub trigger_mode: TriggerMode, + /// Whether the agent is considered runnable under this mode. pub runnable: bool, + /// Result of the CLI-tool probe, or `None` if the probe was skipped. pub cli_tool_probe: Option, + /// Result of the model availability probe, or `None` if the probe was skipped. pub model_probe: Option, + /// Whether a synthetic event could be constructed for this mode, or `None` if not applicable. pub synthetic_event_ok: Option, + /// Human-readable warnings produced during mode evaluation. pub warnings: Vec, } @@ -65,32 +89,50 @@ impl PartialEq for ModeResult { } } +/// Aggregated validation report covering all trigger modes for a single agent. #[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub struct AgentValidationReport { + /// Name of the validated agent. pub agent_name: String, + /// Project the agent belongs to, or `""` for global agents. pub project: String, + /// Per-mode probe results keyed by [`TriggerMode`]. pub mode_results: HashMap, + /// `true` when every trigger mode is runnable. pub all_modes_runnable: bool, } +/// A synthetic event used to test an agent's response without a real source-control trigger. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum SyntheticEvent { + /// Synthetic pull-request event. PullRequest { + /// Pull-request number. number: u64, + /// SHA of the head commit on the PR branch. head_sha: String, + /// Username of the PR author. author: String, + /// PR title. title: String, + /// Approximate diff size in lines of code. diff_loc: usize, }, + /// Synthetic push event. Push { + /// SHA of the pushed commit. sha: String, + /// Full ref name, e.g. `refs/heads/main`. ref_name: String, + /// Username of the person who pushed. pusher: String, + /// List of files changed by the push. files: Vec, }, } impl SyntheticEvent { + /// Convert this event into a map of environment variables understood by ADF agent runners. pub fn env_vars(&self) -> HashMap { let mut vars = HashMap::new(); match self { @@ -125,26 +167,43 @@ impl SyntheticEvent { } } +/// Detailed runtime validation report for a single agent, produced before execution. #[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub struct AgentRuntimeValidationReport { + /// Name of the agent that was validated. pub agent_name: String, + /// Project the agent belongs to, or `""` for global agents. pub project: String, + /// Human-readable label for the agent's execution layer. pub layer: String, + /// Cron schedule expression, if defined for the agent. pub schedule: Option, + /// CLI tool or command used to invoke the agent. pub cli_tool: String, + /// LLM model identifier the agent uses, if configured. pub model: Option, + /// Absolute path to the working directory for this agent. pub working_dir: String, + /// `true` when the working directory exists on disk. pub repo_ok: bool, + /// Gitea repository target resolved for this agent, if available. pub gitea_target: Option, + /// `true` when the agent definition has `evolution_enabled` set. pub evolution_requested: bool, + /// `true` when both the global evolution feature and the agent's flag are enabled. pub evolution_available: bool, + /// `true` when all preconditions for running the agent are satisfied. pub runnable: bool, + /// Result of probing whether the CLI tool binary is present and executable. pub cli_tool_probe: Option, + /// Result of probing whether the configured model is available. pub model_probe: Option, + /// Human-readable warnings produced during validation. pub warnings: Vec, } impl AgentOrchestrator { + /// Validate the runtime environment for the agent described by `request`. pub fn validate_agent_runtime( &self, request: &AgentRunRequest, @@ -153,6 +212,10 @@ impl AgentOrchestrator { } } +/// Validate the runtime environment for an agent using the provided configuration. +/// +/// Resolves the agent by name (and optional project), checks whether its working +/// directory exists, probes the CLI tool and model, and collects any warnings. pub fn validate_agent_runtime( config: &OrchestratorConfig, request: &AgentRunRequest, @@ -241,6 +304,11 @@ pub fn validate_agent_runtime( }) } +/// Probe whether the CLI tool specified in an agent definition is available and executable. +/// +/// Absolute paths are checked for existence and execute permission (on Unix). +/// Relative or bare names are resolved via `which`. Returns `Ok(false)` for +/// empty strings rather than an error. pub fn probe_cli_tool(cli_tool: &str) -> Result { if cli_tool.trim().is_empty() { return Ok(false); @@ -273,6 +341,10 @@ pub fn probe_cli_tool(cli_tool: &str) -> Result { } } +/// Probe whether the given model identifier is available under a subscription-based provider. +/// +/// Currently recognises `kimi-`, `minimax-`, `glm-`, and `zai-` prefixes as available +/// subscription models. Unknown or empty model strings return `Ok(false)`. pub fn probe_model_available( model: &str, _provider: Option<&str>, @@ -291,6 +363,10 @@ pub fn probe_model_available( } } +/// Attempt to run an agent with a synthetic event for testing purposes. +/// +/// This function is not yet implemented and always returns a `ModeResult` with +/// `runnable: false` and a warning indicating the stub status. pub fn run_agent_synthetic( _config: &OrchestratorConfig, _request: &AgentRunRequest, diff --git a/crates/terraphim_orchestrator/src/bin/adf.rs b/crates/terraphim_orchestrator/src/bin/adf.rs index 60054d8d5..a93287d74 100644 --- a/crates/terraphim_orchestrator/src/bin/adf.rs +++ b/crates/terraphim_orchestrator/src/bin/adf.rs @@ -1,3 +1,7 @@ +//! ADF (Agent Development Framework) binary entry point. +//! +//! Parses command-line arguments, loads the orchestrator configuration, +//! and dispatches to the appropriate sub-command (run, validate, synthetic). use std::io::Write; use std::path::PathBuf; use std::process::ExitCode; diff --git a/crates/terraphim_orchestrator/src/config.rs b/crates/terraphim_orchestrator/src/config.rs index 9c8dc685e..dcc1bbb29 100644 --- a/crates/terraphim_orchestrator/src/config.rs +++ b/crates/terraphim_orchestrator/src/config.rs @@ -11,15 +11,23 @@ pub enum PreCheckStrategy { Always, /// Check git diff between last recorded commit and HEAD. /// Only spawn if changed files match watch_paths prefixes. - GitDiff { watch_paths: Vec }, + GitDiff { + /// Path prefixes to watch; the agent spawns only when a diff touches at least one. + watch_paths: Vec, + }, /// Query latest comments on a Gitea issue. Skip if PASS verdict /// and no new commits since. - GiteaIssue { issue_number: u64 }, + GiteaIssue { + /// Gitea issue number to inspect for a recent PASS verdict. + issue_number: u64, + }, /// Run an arbitrary shell command via sh -c. /// Exit 0 + non-empty stdout = Findings; Exit 0 + empty stdout = NoFindings; /// Non-zero exit or timeout = Failed (fail-open). Shell { + /// Shell script body passed to `sh -c`. script: String, + /// Maximum wall-clock seconds before the script is considered Failed. #[serde(default = "default_pre_check_timeout")] timeout_secs: u64, }, @@ -349,16 +357,22 @@ impl PrDispatchConfig { /// prompts at spawn time and records exit outcomes as validation evidence. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LearningConfig { + /// Whether the shared learning system is active for this fleet. #[serde(default)] pub enabled: bool, + /// Minimum trust level (`L1`–`L4`) of learnings to inject into prompts. #[serde(default = "default_learning_min_trust")] pub min_trust: String, + /// Maximum token budget for injected learnings per agent spawn. #[serde(default = "default_learning_max_tokens")] pub max_tokens: usize, + /// Maximum number of learning entries to inject per agent spawn. #[serde(default = "default_learning_max_entries")] pub max_entries: usize, + /// Days after which a learning entry is archived and no longer injected. #[serde(default = "default_learning_archive_days")] pub archive_days: u32, + /// Number of reconcile ticks between learning consolidation passes. #[serde(default = "default_learning_consolidation_ticks")] pub consolidation_ticks: u64, } @@ -396,14 +410,22 @@ impl Default for LearningConfig { } } +/// Configuration for agent evolution and memory consolidation. +/// +/// Controls how the orchestrator manages per-agent memory snapshots and +/// the periodic consolidation of those snapshots into compact summaries. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EvolutionConfig { + /// Whether agent evolution and memory consolidation are active. #[serde(default)] pub enabled: bool, + /// Maximum token budget for the memory injected into each agent prompt. #[serde(default = "default_evolution_max_memory_tokens")] pub max_memory_tokens: usize, + /// Maximum number of memory snapshots retained per agent before pruning. #[serde(default = "default_evolution_max_snapshots")] pub max_snapshots_per_agent: usize, + /// Number of reconcile ticks between memory consolidation passes. #[serde(default = "default_evolution_consolidation_ticks")] pub consolidation_interval_ticks: u64, } @@ -517,10 +539,13 @@ fn default_true_routing() -> bool { /// Configuration for posting agent output to Gitea issues. #[derive(Clone, Serialize, Deserialize)] pub struct GiteaOutputConfig { + /// Base URL of the Gitea instance (e.g. `https://git.example.com`). pub base_url: String, /// Gitea API token. Redacted in `Debug` output. pub token: String, + /// Gitea organisation or user that owns the target repository. pub owner: String, + /// Name of the Gitea repository where agent output is posted. pub repo: String, /// Path to JSON file mapping agent names to Gitea API tokens. /// When present, agents post comments under their own Gitea user. @@ -708,7 +733,9 @@ fn default_quickwit_use_es_bulk() -> bool { /// Lightweight reference to an SFIA skill code and level. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct SfiaSkillRef { + /// Four-letter SFIA skill code (e.g. `SWDN`, `TEST`). pub code: String, + /// SFIA responsibility level (1–7). pub level: u8, } diff --git a/crates/terraphim_orchestrator/src/control_plane/events.rs b/crates/terraphim_orchestrator/src/control_plane/events.rs index 6f895eeac..15636ef1f 100644 --- a/crates/terraphim_orchestrator/src/control_plane/events.rs +++ b/crates/terraphim_orchestrator/src/control_plane/events.rs @@ -210,11 +210,17 @@ pub fn normalize_polled_command( /// to fully populate a NormalizedAgentEvent. #[derive(Debug, Clone)] pub struct WebhookContext { + /// Full repository name in `"owner/repo"` format. pub repo_full_name: String, + /// Title of the issue or pull-request that was commented on. pub issue_title: String, + /// State of the issue (e.g. `"open"` or `"closed"`). pub issue_state: String, + /// ISO 8601 timestamp of the triggering comment. pub comment_created_at: String, + /// Gitea login of the comment author. pub comment_author: String, + /// Full text body of the triggering comment. pub comment_body: String, } diff --git a/crates/terraphim_orchestrator/src/control_plane/policy.rs b/crates/terraphim_orchestrator/src/control_plane/policy.rs index 3c504c0cf..5d81a6bbb 100644 --- a/crates/terraphim_orchestrator/src/control_plane/policy.rs +++ b/crates/terraphim_orchestrator/src/control_plane/policy.rs @@ -22,8 +22,11 @@ pub struct PolicyResult { /// A candidate with its policy score. #[derive(Debug, Clone)] pub struct ScoredCandidate { + /// The routing candidate being scored. pub candidate: RouteCandidate, + /// Composite policy score (higher is better). pub score: f64, + /// Per-dimension score contributions for debugging and observability. pub rationale_breakdown: Vec<(String, f64)>, } @@ -55,6 +58,7 @@ impl Default for PolicyConfig { } impl PolicyConfig { + /// Create a new `PolicyConfig` with default weights. pub fn new() -> Self { Self::default() } diff --git a/crates/terraphim_orchestrator/src/control_plane/routing.rs b/crates/terraphim_orchestrator/src/control_plane/routing.rs index 6141ca988..c3b1852a1 100644 --- a/crates/terraphim_orchestrator/src/control_plane/routing.rs +++ b/crates/terraphim_orchestrator/src/control_plane/routing.rs @@ -13,12 +13,18 @@ use std::path::PathBuf; use std::sync::Arc; use terraphim_types::capability::{CostLevel, Latency, Provider, ProviderType}; +/// Identifies which routing signal produced a particular [`RouteCandidate`]. #[derive(Debug, Clone, PartialEq)] pub enum RouteSource { + /// Route selected by the knowledge-graph router. KnowledgeGraph, + /// Route selected by keyword-based routing. KeywordRouting, + /// Route taken from the agent's static model configuration. StaticConfig, + /// KG and keyword signals agreed on the same model; scores were merged. CombinedKgKeyword, + /// No routing signal matched; the CLI tool runs without a `--model` flag. CliDefault, } @@ -34,14 +40,19 @@ impl std::fmt::Display for RouteSource { } } +/// Categorises how close an agent's spend is to its configured budget limit. #[derive(Debug, Clone, Copy, PartialEq)] pub enum BudgetPressure { + /// Spend is well within the budget; no score adjustments are applied. NoPressure, + /// Spend is approaching the limit; expensive candidates receive a score penalty. NearExhaustion, + /// Budget is fully consumed; expensive candidates are heavily penalised. Exhausted, } impl BudgetPressure { + /// Converts a [`BudgetVerdict`] into the corresponding pressure level. pub fn from_verdict(verdict: &BudgetVerdict) -> Self { match verdict { BudgetVerdict::Exhausted { .. } => BudgetPressure::Exhausted, @@ -50,6 +61,7 @@ impl BudgetPressure { } } + /// Returns a score penalty fraction (0.0–1.0) for the given cost tier under this pressure level. pub fn cost_penalty(&self, cost_level: &CostLevel) -> f64 { match self { BudgetPressure::NoPressure => 0.0, @@ -67,37 +79,59 @@ impl BudgetPressure { } } +/// Runtime context passed to the routing engine when dispatching an agent invocation. #[derive(Debug, Clone)] pub struct DispatchContext { + /// Name of the agent being dispatched. pub agent_name: String, + /// Human-readable description of the task the agent will perform. pub task: String, + /// Optional model identifier configured directly on the agent definition. pub static_model: Option, + /// Path (or bare name) of the CLI tool used to invoke the agent. pub cli_tool: String, + /// Orchestration layer the agent belongs to (Safety, Core, or Growth). pub layer: crate::config::AgentLayer, + /// Optional session identifier forwarded to routing telemetry. pub session_id: Option, /// Default KG tier concept for this agent (e.g., "review_tier"). /// Passed through to KG router for tier-biased routing. pub default_tier: Option, } +/// A single routing option produced by one of the routing signals. #[derive(Debug, Clone)] pub struct RouteCandidate { + /// Provider descriptor for the candidate agent or LLM. pub provider: Provider, + /// Model identifier that would be passed to the CLI tool. pub model: String, + /// CLI tool used to invoke this candidate. pub cli_tool: String, + /// Which routing signal produced this candidate. pub source: RouteSource, + /// Signal strength in the range 0.0–1.0; higher means more confident. pub confidence: f64, } +/// The final routing decision produced by [`RoutingDecisionEngine::decide_route`]. #[derive(Debug, Clone)] pub struct RoutingDecision { + /// The winning candidate that should be used for dispatch. pub candidate: RouteCandidate, + /// Human-readable explanation of why this candidate was chosen. pub rationale: String, + /// All candidates considered before scoring, for auditing purposes. pub all_candidates: Vec, + /// `true` when the winning candidate came from a real routing signal rather than the CLI default. pub primary_available: bool, + /// The routing signal that contributed the winning candidate. pub dominant_signal: RouteSource, + /// Budget pressure level observed when the decision was made. pub budget_pressure: BudgetPressure, + /// `true` when budget pressure changed which candidate was selected. pub budget_influenced: bool, + /// `true` when telemetry data adjusted candidate scores. pub telemetry_influenced: bool, } @@ -128,6 +162,7 @@ struct CollectedCandidates { static_model: Option, } +/// Scores and selects the best routing candidate for an agent dispatch. pub struct RoutingDecisionEngine { kg_router: Option>, /// Snapshot of unhealthy provider names at construction time. @@ -143,6 +178,8 @@ pub struct RoutingDecisionEngine { } impl RoutingDecisionEngine { + /// Creates an engine with KG router, unhealthy provider list, keyword router, and optional telemetry. + /// Uses [`RouteSelectionStrategy::Fastest`] and no per-provider budget tracker. pub fn new( kg_router: Option>, unhealthy_providers: Vec, @@ -159,6 +196,8 @@ impl RoutingDecisionEngine { ) } + /// Creates an engine that also enforces per-provider hourly/daily budget caps. + /// Uses [`RouteSelectionStrategy::Fastest`]. pub fn with_provider_budget( kg_router: Option>, unhealthy_providers: Vec, @@ -176,6 +215,8 @@ impl RoutingDecisionEngine { ) } + /// Full constructor allowing explicit control over per-provider budgets and the telemetry-based + /// selection strategy. pub fn with_provider_budget_and_strategy( kg_router: Option>, unhealthy_providers: Vec, @@ -311,6 +352,8 @@ impl RoutingDecisionEngine { base * (1.0 - penalty) } + /// Collects all routing candidates, applies budget and telemetry adjustments, and returns the + /// highest-scoring [`RoutingDecision`] for the given dispatch context. pub async fn decide_route( &self, ctx: &DispatchContext, diff --git a/crates/terraphim_orchestrator/src/control_plane/telemetry.rs b/crates/terraphim_orchestrator/src/control_plane/telemetry.rs index d446f8c9a..ec23c409a 100644 --- a/crates/terraphim_orchestrator/src/control_plane/telemetry.rs +++ b/crates/terraphim_orchestrator/src/control_plane/telemetry.rs @@ -45,11 +45,17 @@ pub struct CompletionEvent { /// Token breakdown from a completion event. #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] pub struct TokenBreakdown { + /// Total tokens consumed (input + output + any extras). pub total: u64, + /// Prompt / input tokens. pub input: u64, + /// Generated / output tokens. pub output: u64, + /// Tokens used for chain-of-thought reasoning (may be 0 for non-reasoning models). pub reasoning: u64, + /// Tokens served from the prompt cache. pub cache_read: u64, + /// Tokens written into the prompt cache. pub cache_write: u64, } @@ -83,6 +89,7 @@ pub struct ModelPerformanceSnapshot { } impl ModelPerformanceSnapshot { + /// Construct an empty snapshot for `model` with all counters zeroed. pub fn empty(model: &str, window_secs: u64) -> Self { Self { model: model.to_string(), @@ -100,6 +107,7 @@ impl ModelPerformanceSnapshot { } } + /// Return `true` if the snapshot has no recent event within `max_staleness_secs`. pub fn is_stale(&self, max_staleness_secs: u64) -> bool { match self.last_event_at { None => true, @@ -110,6 +118,7 @@ impl ModelPerformanceSnapshot { } } + /// Return `true` if a subscription limit is currently active for this model. pub fn is_subscription_limited(&self) -> bool { if !self.subscription_limit_reached { return false; @@ -140,14 +149,20 @@ pub struct UsageSnapshot { /// Per-model usage totals. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ModelUsage { + /// Total prompt / input tokens consumed. pub input_tokens: u64, + /// Total generated / output tokens produced. pub output_tokens: u64, + /// Sum of input and output tokens. pub total_tokens: u64, + /// Cumulative cost in USD. pub cost_usd: f64, + /// Number of completion messages contributing to these totals. pub message_count: u64, } impl ModelUsage { + /// Accumulate another `ModelUsage` into `self`. pub fn merge(&mut self, other: &ModelUsage) { self.input_tokens += other.input_tokens; self.output_tokens += other.output_tokens; @@ -293,6 +308,7 @@ struct TelemetryStoreInner { } impl TelemetryStore { + /// Create a new store with a rolling window of `window_secs` seconds. pub fn new(window_secs: u64) -> Self { Self { inner: Arc::new(RwLock::new(TelemetryStoreInner { @@ -304,6 +320,7 @@ impl TelemetryStore { } } + /// Override the default subscription-limit TTL (1 hour) with `ttl_secs`. pub fn with_subscription_limit_ttl(self, ttl_secs: u64) -> Self { let window_secs = self.inner.blocking_read().window_secs; Self { diff --git a/crates/terraphim_orchestrator/src/cost_tracker.rs b/crates/terraphim_orchestrator/src/cost_tracker.rs index 9eec92eb8..cb29e4046 100644 --- a/crates/terraphim_orchestrator/src/cost_tracker.rs +++ b/crates/terraphim_orchestrator/src/cost_tracker.rs @@ -15,9 +15,19 @@ pub enum BudgetVerdict { /// Spend is within normal budget range. WithinBudget, /// Spend has reached warning threshold (80%). - NearExhaustion { spent_cents: u64, budget_cents: u64 }, + NearExhaustion { + /// Amount spent so far this month, in cents. + spent_cents: u64, + /// Monthly budget cap, in cents. + budget_cents: u64, + }, /// Spend has reached or exceeded 100% of budget. - Exhausted { spent_cents: u64, budget_cents: u64 }, + Exhausted { + /// Amount spent so far this month, in cents. + spent_cents: u64, + /// Monthly budget cap, in cents. + budget_cents: u64, + }, } impl BudgetVerdict { @@ -316,9 +326,13 @@ impl AgentCost { /// Snapshot of an agent's cost status (for serialization). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CostSnapshot { + /// Name of the agent this snapshot belongs to. pub agent_name: String, + /// Total amount spent so far this month, in USD. pub spent_usd: f64, + /// Monthly budget cap in cents, or `None` for uncapped agents. pub budget_cents: Option, + /// Human-readable budget verdict string (e.g. "within budget"). pub verdict: String, } diff --git a/crates/terraphim_orchestrator/src/direct_dispatch.rs b/crates/terraphim_orchestrator/src/direct_dispatch.rs index 6ee0361ea..e06eb4d89 100644 --- a/crates/terraphim_orchestrator/src/direct_dispatch.rs +++ b/crates/terraphim_orchestrator/src/direct_dispatch.rs @@ -43,6 +43,7 @@ pub struct DirectDispatchAgentIndex { } impl DirectDispatchAgentIndex { + /// Build an index from a slice of agent definitions. pub fn from_agents(agents: &[crate::config::AgentDefinition]) -> Self { let bare_names: HashSet = agents .iter() @@ -59,6 +60,7 @@ impl DirectDispatchAgentIndex { } } + /// Return `true` if `(project, agent)` names a known configured agent. pub fn is_valid(&self, project: Option<&str>, agent: &str) -> bool { match project { Some(p) => self @@ -72,12 +74,15 @@ impl DirectDispatchAgentIndex { /// JSON response written back to adf-ctl. #[derive(Debug, serde::Serialize)] pub struct DispatchResponse { + /// `"ok"` on success or `"error"` on failure. pub status: String, + /// Human-readable error description; omitted from JSON when `None`. #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, } impl DispatchResponse { + /// Construct a successful response with no message. pub fn ok() -> Self { Self { status: "ok".to_string(), @@ -85,6 +90,7 @@ impl DispatchResponse { } } + /// Construct an error response with the given human-readable message. pub fn error(msg: &str) -> Self { Self { status: "error".to_string(), @@ -93,22 +99,10 @@ impl DispatchResponse { } } -/// Start the Unix domain socket listener for direct dispatch. -// -/// -/// -/// The listener task: -/// -/// 1. Removes any stale socket file at `socket_path`. -/// 2. Binds and listens on the socket path. -/// 3. For each incoming connection: -/// a. Reads a single JSON command from the stream. -/// b. Validates the agent name against `agent_names`. -/// c. Sends `WebhookDispatch::SpawnAgent` to `dispatch_tx`. -/// d. Writes a JSON response back to the client. -/// 4. Logs errors and continues accepting connections. +/// Remove a stale socket file at `socket_path` if it exists. /// -/// The socket is cleaned up automatically when the listener task is dropped. +/// Returns an error if the path exists but is not a socket, leaving it +/// untouched to avoid accidental data loss. #[cfg(unix)] fn remove_stale_socket_if_present(socket_path: &std::path::Path) -> std::io::Result<()> { use std::os::unix::fs::FileTypeExt; @@ -123,6 +117,15 @@ fn remove_stale_socket_if_present(socket_path: &std::path::Path) -> std::io::Res } } +/// Start the Unix domain socket listener for direct dispatch. +/// +/// Spawns a Tokio task that: +/// 1. Removes any stale socket file at `socket_path`. +/// 2. Binds and listens on the socket path. +/// 3. For each connection: reads a JSON [`DispatchCommand`], validates the +/// agent name against `agent_index`, and forwards a +/// `WebhookDispatch::SpawnAgent` to `dispatch_tx`. +/// 4. Writes a JSON [`DispatchResponse`] back to the caller. pub fn start_direct_dispatch_listener( socket_path: PathBuf, dispatch_tx: tokio::sync::mpsc::Sender, diff --git a/crates/terraphim_orchestrator/src/dual_mode.rs b/crates/terraphim_orchestrator/src/dual_mode.rs index 463a0af5f..3d4112196 100644 --- a/crates/terraphim_orchestrator/src/dual_mode.rs +++ b/crates/terraphim_orchestrator/src/dual_mode.rs @@ -71,9 +71,17 @@ impl std::fmt::Display for ExecutionMode { #[derive(Debug, Clone)] pub enum SpawnTask { /// Time-driven agent task. - TimeTask { agent: Box }, + TimeTask { + /// Definition of the agent to spawn on schedule. + agent: Box, + }, /// Issue-driven agent task. - IssueTask { issue_id: String, title: String }, + IssueTask { + /// Gitea issue id that triggered this spawn. + issue_id: String, + /// Issue title, used for log and telemetry labels. + title: String, + }, } /// Full dual-mode orchestrator. diff --git a/crates/terraphim_orchestrator/src/error.rs b/crates/terraphim_orchestrator/src/error.rs index 26d23f723..3663769bd 100644 --- a/crates/terraphim_orchestrator/src/error.rs +++ b/crates/terraphim_orchestrator/src/error.rs @@ -4,106 +4,186 @@ use terraphim_spawner::SpawnerError; /// Errors that can occur during orchestrator operation. #[derive(Debug, thiserror::Error)] pub enum OrchestratorError { + /// A configuration value is invalid or missing. #[error("configuration error: {0}")] Config(String), + /// An agent process failed to start. #[error("agent spawn failed for '{agent}': {reason}")] - SpawnFailed { agent: String, reason: String }, + SpawnFailed { + /// Name of the agent that failed to spawn. + agent: String, + /// Human-readable description of the failure. + reason: String, + }, + /// A git worktree could not be created for an agent. #[error("agent worktree creation failed for '{agent}' in '{repo}': {reason}")] WorktreeCreationFailed { + /// Name of the agent whose worktree creation failed. agent: String, + /// Path or URL of the repository in which the worktree was being created. repo: String, + /// Human-readable description of the failure. reason: String, }, + /// An operation referenced an agent that does not exist in the registry. #[error("agent '{0}' not found")] AgentNotFound(String), + /// The internal task scheduler encountered an error. #[error("scheduler error: {0}")] SchedulerError(String), + /// A compound (multi-reviewer) review pipeline failed. #[error("compound review failed: {0}")] CompoundReviewFailed(String), + /// The supplied agent name contains characters that are not allowed. #[error( "invalid agent name '{0}': must contain only alphanumeric, dash, or underscore characters" )] InvalidAgentName(String), + /// An agent handoff (work transfer between agents) could not be completed. #[error("handoff failed from '{from}' to '{to}': {reason}")] HandoffFailed { + /// Name of the agent that is handing off work. from: String, + /// Name of the agent that should receive the work. to: String, + /// Human-readable description of why the handoff failed. reason: String, }, + /// A [`SpawnerError`] was returned by the process-spawning layer. #[error(transparent)] Spawner(#[from] SpawnerError), + /// A [`RoutingError`] was returned by the routing layer. #[error(transparent)] Routing(#[from] RoutingError), + /// An I/O error occurred at the operating-system level. #[error(transparent)] Io(#[from] std::io::Error), + /// An agent's pre-execution check was misconfigured. #[error("pre-check configuration error for agent '{agent}': {reason}")] - PreCheckConfig { agent: String, reason: String }, + PreCheckConfig { + /// Name of the agent with the invalid pre-check configuration. + agent: String, + /// Human-readable description of the configuration problem. + reason: String, + }, + /// A named flow failed during execution. #[error("flow '{flow_name}' failed: {reason}")] - FlowFailed { flow_name: String, reason: String }, + FlowFailed { + /// Name of the flow that failed. + flow_name: String, + /// Human-readable description of the failure. + reason: String, + }, + /// A gate step within a flow rejected execution. #[error("flow '{flow_name}' gate '{step_name}' rejected: {condition}")] FlowGateRejected { + /// Name of the flow containing the rejected gate. flow_name: String, + /// Name of the gate step that rejected execution. step_name: String, + /// The condition expression that caused the rejection. condition: String, }, + /// A flow template could not be parsed or instantiated. #[error("flow template error: {0}")] FlowTemplateError(String), + /// Two projects share the same identifier, which must be unique. #[error( "duplicate project id '{0}' (project ids must be unique across base + included configs)" )] DuplicateProjectId(String), + /// An agent configuration references a project id that does not exist. #[error( "agent '{agent}' references unknown project '{project}' (must match a Project.id in projects list)" )] - UnknownAgentProject { agent: String, project: String }, + UnknownAgentProject { + /// Name of the agent with the invalid project reference. + agent: String, + /// The project id that could not be resolved. + project: String, + }, + /// A flow configuration references a project id that does not exist. #[error( "flow '{flow}' references unknown project '{project}' (must match a Project.id in projects list)" )] - UnknownFlowProject { flow: String, project: String }, + UnknownFlowProject { + /// Name of the flow with the invalid project reference. + flow: String, + /// The project id that could not be resolved. + project: String, + }, + /// An agent or flow specifies an LLM provider that is not on the allow-list. #[error( "banned LLM provider '{provider}' in {field} for agent '{agent}' (allowed: claude-code, opencode-go, kimi-for-coding, minimax-coding-plan, zai-coding-plan)" )] BannedProvider { + /// Name of the agent whose configuration contains the banned provider. agent: String, + /// The banned provider identifier that was supplied. provider: String, + /// The configuration field in which the banned provider appeared. field: String, }, + /// Projects are declared but an agent or flow has no project assigned. #[error( "mixed project mode: projects are defined but {kind} '{name}' has no project set; every agent and flow must declare a project" )] - MixedProjectMode { kind: &'static str, name: String }, + MixedProjectMode { + /// Whether the offending item is an `"agent"` or a `"flow"`. + kind: &'static str, + /// Name of the agent or flow that is missing a project assignment. + name: String, + }, + /// A glob pattern used in an `include` directive is syntactically invalid. #[error("include glob '{pattern}' is invalid: {reason}")] - InvalidIncludeGlob { pattern: String, reason: String }, + InvalidIncludeGlob { + /// The glob pattern string that failed to parse. + pattern: String, + /// Human-readable description of the parse error. + reason: String, + }, + /// A numeric field on an agent config is outside its permitted range. #[error("agent '{agent}' {field} value {value}s is outside allowed range [{min}s, {max}s]")] AgentFieldOutOfRange { + /// Name of the agent with the out-of-range field. agent: String, + /// Name of the field that is out of range. field: String, + /// The value that was supplied (in seconds). value: u64, + /// The minimum permitted value (in seconds). min: u64, + /// The maximum permitted value (in seconds). max: u64, }, + /// The `nightwatch` probe TTL is shorter than the rate-limit protection floor. #[error("nightwatch probe_ttl_secs {value}s is below minimum {min}s (rate-limit protection)")] - ProbeTtlTooShort { value: u64, min: u64 }, + ProbeTtlTooShort { + /// The probe TTL value that was supplied (in seconds). + value: u64, + /// The minimum allowed probe TTL (in seconds). + min: u64, + }, } diff --git a/crates/terraphim_orchestrator/src/error_signatures.rs b/crates/terraphim_orchestrator/src/error_signatures.rs index 557bbcaf7..63e3bc499 100644 --- a/crates/terraphim_orchestrator/src/error_signatures.rs +++ b/crates/terraphim_orchestrator/src/error_signatures.rs @@ -63,8 +63,11 @@ pub struct ProviderErrorSignatures { /// Compile error building per-provider regex patterns. #[derive(Debug)] pub struct CompileError { + /// Provider id whose error signature pattern failed to compile. pub provider: String, + /// The regex pattern string that caused the compile failure. pub pattern: String, + /// Underlying regex compilation error. pub source: regex::Error, } diff --git a/crates/terraphim_orchestrator/src/evolution.rs b/crates/terraphim_orchestrator/src/evolution.rs index 02946aca5..3c64e87e8 100644 --- a/crates/terraphim_orchestrator/src/evolution.rs +++ b/crates/terraphim_orchestrator/src/evolution.rs @@ -8,6 +8,9 @@ use terraphim_agent_evolution::{ TaskId, }; +/// Manages per-agent evolution systems, providing memory, task tracking, and +/// lesson capture. When the `evolution` feature is disabled all methods are +/// no-ops so callers require no conditional compilation. #[derive(Debug)] pub struct EvolutionManager { #[cfg(feature = "evolution")] @@ -17,15 +20,21 @@ pub struct EvolutionManager { enabled: bool, } +/// A single agent output event to be recorded in the evolution memory store. #[derive(Debug)] pub struct EvolutionOutput { + /// Identifier of the agent that produced this output. pub agent_id: String, + /// The text content of the output event. pub content: String, + /// Classifier for the event: `"stdout"`, `"stderr"`, `"workflow"`, or `"lesson"`. pub event_type: String, + /// Priority of the event: `"critical"`, `"high"`, `"medium"`, or `"low"`. pub importance: String, } impl EvolutionManager { + /// Create a new `EvolutionManager` from the given configuration. pub fn new(config: EvolutionConfig) -> Self { let enabled = config.enabled; Self { @@ -36,10 +45,12 @@ impl EvolutionManager { } } + /// Return `true` when evolution tracking is enabled by the configuration. pub fn is_enabled(&self) -> bool { self.enabled } + /// Ensure an evolution system exists for `agent_id`, creating one if absent. #[cfg(feature = "evolution")] pub fn ensure_agent(&mut self, agent_id: &str) { if !self.enabled { @@ -50,9 +61,11 @@ impl EvolutionManager { .or_insert_with(|| AgentEvolutionSystem::new(agent_id.to_string())); } + /// No-op stub used when the `evolution` feature is disabled. #[cfg(not(feature = "evolution"))] pub fn ensure_agent(&mut self, _agent_id: &str) {} + /// Record an agent output event into the evolution memory store. #[cfg(feature = "evolution")] pub fn record_output(&mut self, output: EvolutionOutput) -> Result<(), String> { if !self.enabled { @@ -94,11 +107,13 @@ impl EvolutionManager { .map_err(|e| format!("evolution record_output: {e}")) } + /// No-op stub used when the `evolution` feature is disabled. #[cfg(not(feature = "evolution"))] pub fn record_output(&mut self, _output: EvolutionOutput) -> Result<(), String> { Ok(()) } + /// Record the start of a task for `agent_id` and return its generated task ID. #[cfg(feature = "evolution")] pub fn record_task_start(&mut self, agent_id: &str, task_content: &str) -> Option { if !self.enabled { @@ -114,11 +129,13 @@ impl EvolutionManager { Some(task_id) } + /// No-op stub used when the `evolution` feature is disabled. #[cfg(not(feature = "evolution"))] pub fn record_task_start(&mut self, _agent_id: &str, _task_content: &str) -> Option { None } + /// Mark a previously started task as complete and store its result string. #[cfg(feature = "evolution")] pub fn record_task_complete( &mut self, @@ -141,6 +158,7 @@ impl EvolutionManager { .map_err(|e| format!("evolution record_task_complete: {e}")) } + /// No-op stub used when the `evolution` feature is disabled. #[cfg(not(feature = "evolution"))] pub fn record_task_complete( &mut self, @@ -151,6 +169,7 @@ impl EvolutionManager { Ok(()) } + /// Capture a structured lesson learned by an agent into the lessons store. #[cfg(feature = "evolution")] pub fn record_lesson( &mut self, @@ -179,6 +198,7 @@ impl EvolutionManager { .map_err(|e| format!("evolution record_lesson: {e}")) } + /// No-op stub used when the `evolution` feature is disabled. #[cfg(not(feature = "evolution"))] pub fn record_lesson( &mut self, @@ -191,6 +211,7 @@ impl EvolutionManager { Ok(()) } + /// Persist the evolution state for `agent_id` and return the snapshot key, if any. #[cfg(feature = "evolution")] pub fn snapshot_on_exit(&mut self, agent_id: &str) -> Option { if !self.enabled { @@ -215,11 +236,14 @@ impl EvolutionManager { } } + /// No-op stub used when the `evolution` feature is disabled. #[cfg(not(feature = "evolution"))] pub fn snapshot_on_exit(&mut self, _agent_id: &str) -> Option { None } + /// Build a markdown-formatted summary of the agent's evolution memory for + /// injection into an LLM prompt context, truncated to `max_memory_tokens`. #[cfg(feature = "evolution")] pub fn render_context(&self, agent_id: &str) -> String { if !self.enabled { @@ -280,11 +304,14 @@ impl EvolutionManager { } } + /// No-op stub used when the `evolution` feature is disabled. #[cfg(not(feature = "evolution"))] pub fn render_context(&self, _agent_id: &str) -> String { String::new() } + /// Trigger memory consolidation for all registered agents and return the + /// number of agents that consolidated successfully. #[cfg(feature = "evolution")] pub fn consolidate_all(&mut self) -> usize { if !self.enabled { @@ -303,6 +330,7 @@ impl EvolutionManager { count } + /// No-op stub used when the `evolution` feature is disabled. #[cfg(not(feature = "evolution"))] pub fn consolidate_all(&mut self) -> usize { 0 diff --git a/crates/terraphim_orchestrator/src/flow/config.rs b/crates/terraphim_orchestrator/src/flow/config.rs index 8692b9627..10b3b4008 100644 --- a/crates/terraphim_orchestrator/src/flow/config.rs +++ b/crates/terraphim_orchestrator/src/flow/config.rs @@ -47,20 +47,29 @@ fn default_matrix_fail_strategy() -> FailStrategy { FailStrategy::Continue } +/// Top-level definition of a flow, typically loaded from a TOML configuration file. +/// +/// A flow is a named, ordered sequence of steps executed against a Git repository. +/// It may be triggered manually or on a cron schedule. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FlowDefinition { + /// Unique human-readable name for this flow. pub name: String, /// Project this flow belongs to. Required -- flows are per-project only (D14). /// Must match a `Project.id` when projects are defined. pub project: String, + /// Optional cron expression for scheduled execution (e.g. `"0 2 * * *"`). #[serde(default)] - pub schedule: Option, // cron expression + pub schedule: Option, + /// Absolute path to the Git repository that steps operate on. pub repo_path: String, + /// Branch that serves as the merge target; defaults to `"main"`. #[serde(default = "default_base_branch")] pub base_branch: String, /// Global flow timeout in seconds. If the entire flow exceeds this, it is aborted. #[serde(default = "default_flow_timeout")] pub timeout_secs: u64, + /// Ordered list of step definitions that make up this flow. #[serde(default)] pub steps: Vec, } @@ -73,9 +82,16 @@ fn default_flow_timeout() -> u64 { 3600 // 1 hour default } +/// Definition of a single step within a flow. +/// +/// The active fields depend on the `kind`: shell commands apply to `Action` +/// steps, agent-related fields apply to `Agent` steps, and `condition` applies +/// to `Gate` steps. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct FlowStepDef { + /// Unique name for this step within the flow; used for output lookup and loop targets. pub name: String, + /// Execution mode for this step. pub kind: StepKind, /// Shell command (for action steps). #[serde(default)] @@ -122,22 +138,31 @@ fn default_timeout() -> u64 { 600 } +/// Execution mode of a flow step. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum StepKind { + /// Runs a shell command via `bash -lc`. #[default] Action, + /// Invokes an AI agent CLI tool with a task prompt. Agent, + /// Evaluates a boolean condition expression; aborts the flow on failure. Gate, + /// Saves the current flow state; may loop back to an earlier step. Checkpoint, } +/// What the executor should do when a step (or matrix sub-execution) fails. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum FailStrategy { + /// Stop the flow immediately and mark it as failed. #[default] Abort, + /// Record the failure but skip this step and continue with the next one. SkipFailed, + /// Continue collecting results even if this step fails; evaluate outcome later. Continue, } diff --git a/crates/terraphim_orchestrator/src/flow/envelope.rs b/crates/terraphim_orchestrator/src/flow/envelope.rs index 34c5e2e2b..13d49299d 100644 --- a/crates/terraphim_orchestrator/src/flow/envelope.rs +++ b/crates/terraphim_orchestrator/src/flow/envelope.rs @@ -17,6 +17,7 @@ pub struct MatrixResult { } impl MatrixResult { + /// Compute aggregate success/failure counts and exit code list from a slice of envelopes. pub fn from_envelopes(envelopes: &[StepEnvelope]) -> Self { let success_count = envelopes.iter().filter(|e| e.exit_code == 0).count(); let failure_count = envelopes.len() - success_count; @@ -33,21 +34,35 @@ impl MatrixResult { } } +/// Execution record for a single completed ADF step. +/// +/// Produced by the step runner after each command exits and consumed by +/// downstream template rendering and matrix aggregation. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StepEnvelope { + /// Logical name of the step as declared in the workflow definition. pub step_name: String, + /// UTC timestamp when the step started executing. pub started_at: DateTime, + /// UTC timestamp when the step finished executing. pub finished_at: DateTime, + /// Process exit code; 0 indicates success. pub exit_code: i32, + /// Captured standard output of the step process (may be truncated). pub stdout: String, + /// Captured standard error of the step process (may be truncated). pub stderr: String, #[serde(default)] + /// Approximate cost in US dollars consumed by any LLM calls made during this step. pub cost_usd: Option, #[serde(default)] + /// Session identifier returned by the LLM provider, if applicable. pub session_id: Option, #[serde(default)] + /// Number of input/prompt tokens consumed by LLM calls in this step. pub input_tokens: Option, #[serde(default)] + /// Number of output/completion tokens produced by LLM calls in this step. pub output_tokens: Option, /// Path to temp file containing stdout (for downstream action steps). #[serde(default)] diff --git a/crates/terraphim_orchestrator/src/flow/executor.rs b/crates/terraphim_orchestrator/src/flow/executor.rs index 8b676fff7..fea4e30f9 100644 --- a/crates/terraphim_orchestrator/src/flow/executor.rs +++ b/crates/terraphim_orchestrator/src/flow/executor.rs @@ -59,14 +59,22 @@ fn resolve_matrix_vars(template: &str, row: &MatrixParams) -> String { /// constructed. #[derive(Debug, Clone, Default)] pub struct ProjectRuntime { + /// Filesystem path used as the working directory for steps in this project. pub working_dir: PathBuf, + /// Gitea organisation or user that owns the project repository. pub gitea_owner: Option, + /// Gitea repository name for the project. pub gitea_repo: Option, } +/// Executes flow definitions step-by-step, supporting action, agent, gate, +/// and checkpoint step kinds with template resolution and state persistence. pub struct FlowExecutor { + /// Default working directory for steps that do not belong to a registered project. pub working_dir: PathBuf, + /// Spawner used to launch agent-kind steps as child processes. pub spawner: AgentSpawner, + /// Directory where per-flow run state JSON files are persisted. pub flow_state_dir: PathBuf, /// Per-project runtime metadata, keyed by project id. Missing entries /// mean "use the FlowExecutor's top-level working_dir" (legacy mode). @@ -74,6 +82,7 @@ pub struct FlowExecutor { } impl FlowExecutor { + /// Create a new executor with the given working directory and state directory. pub fn new(working_dir: PathBuf, flow_state_dir: PathBuf) -> Self { Self { working_dir: working_dir.clone(), diff --git a/crates/terraphim_orchestrator/src/flow/mod.rs b/crates/terraphim_orchestrator/src/flow/mod.rs index c9f221b2a..6744be222 100644 --- a/crates/terraphim_orchestrator/src/flow/mod.rs +++ b/crates/terraphim_orchestrator/src/flow/mod.rs @@ -1,5 +1,10 @@ +/// Flow configuration types and loading. pub mod config; +/// Flow envelope definitions for wrapping agent payloads. pub mod envelope; +/// Flow executor responsible for running flow steps. pub mod executor; +/// Shared state managed across flow execution. pub mod state; +/// Token parser for extracting structured data from flow output. pub mod token_parser; diff --git a/crates/terraphim_orchestrator/src/flow/state.rs b/crates/terraphim_orchestrator/src/flow/state.rs index 48c039bd8..e6338d522 100644 --- a/crates/terraphim_orchestrator/src/flow/state.rs +++ b/crates/terraphim_orchestrator/src/flow/state.rs @@ -6,22 +6,34 @@ use uuid::Uuid; use super::envelope::{MatrixResult, StepEnvelope}; +/// Runtime state of a single flow execution, persisted across steps. +/// +/// Holds the current progress, accumulated step outputs, and any error +/// information so that a flow run can be resumed or inspected after a restart. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FlowRunState { + /// Human-readable name of the flow being executed. pub flow_name: String, + /// Unique identifier for this execution instance, used in filenames and logs. pub correlation_id: Uuid, + /// Current lifecycle status of the flow run. pub status: FlowRunStatus, + /// Wall-clock time at which this flow run began. pub started_at: DateTime, + /// Wall-clock time at which this flow run ended, or `None` if still running. pub finished_at: Option>, + /// Index into the steps list indicating which step will execute next. pub next_step_index: usize, /// Optional issue id supplied by local flow context. #[serde(default)] pub issue: Option, + /// Ordered list of envelopes produced by completed sequential steps. pub step_envelopes: Vec, /// Results from matrix-expanded steps. Key is step name; value is the /// ordered list of sub-execution envelopes (one per matrix params row). #[serde(default)] pub matrix_envelopes: HashMap>, + /// Error message recorded when the flow transitions to `Failed`. #[serde(default)] pub error: Option, /// Current iteration count for re-iteration loops. @@ -30,17 +42,24 @@ pub struct FlowRunState { pub iteration_count: u32, } +/// Lifecycle status of a flow execution. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum FlowRunStatus { + /// The flow is actively executing steps. Running, + /// The flow has been suspended and is awaiting resumption. Paused, + /// All steps finished successfully. Completed, + /// The flow stopped due to an error or a failing gate. Failed, + /// The flow was cancelled before completing. Aborted, } impl FlowRunState { + /// Create a new flow run state with `Running` status and zeroed counters. pub fn new(flow_name: &str) -> Self { Self { flow_name: flow_name.to_string(), @@ -57,11 +76,13 @@ impl FlowRunState { } } + /// Attach a Gitea issue identifier to this flow run and return `self`. pub fn with_issue(mut self, issue: String) -> Self { self.issue = Some(issue); self } + /// Construct a flow run state that is already in the `Failed` state with the given reason. pub fn failed(flow_name: &str, reason: &str) -> Self { let mut state = Self::new(flow_name); state.status = FlowRunStatus::Failed; @@ -70,6 +91,7 @@ impl FlowRunState { state } + /// Look up the envelope produced by the sequential step named `step_name`. pub fn step_output(&self, step_name: &str) -> Option<&StepEnvelope> { self.step_envelopes .iter() @@ -84,6 +106,10 @@ impl FlowRunState { .map(|envelopes| MatrixResult::from_envelopes(envelopes)) } + /// Atomically serialise this state to a JSON file inside `dir` and return the path. + /// + /// The filename encodes both the flow name and the correlation id so that + /// multiple concurrent runs do not collide. pub fn save_to_file(&self, dir: &Path) -> std::io::Result { std::fs::create_dir_all(dir)?; let filename = format!("flow-{}-{}.json", self.flow_name, self.correlation_id); @@ -95,6 +121,7 @@ impl FlowRunState { Ok(path) } + /// Deserialise a flow run state from the JSON file at `path`. pub fn load_from_file(path: &Path) -> std::io::Result { let json = std::fs::read_to_string(path)?; serde_json::from_str(&json) diff --git a/crates/terraphim_orchestrator/src/flow/token_parser.rs b/crates/terraphim_orchestrator/src/flow/token_parser.rs index 7c0adafbc..5339edf45 100644 --- a/crates/terraphim_orchestrator/src/flow/token_parser.rs +++ b/crates/terraphim_orchestrator/src/flow/token_parser.rs @@ -9,9 +9,13 @@ use std::sync::LazyLock; /// Token usage data extracted from CLI output. #[derive(Debug, Clone, Default, PartialEq)] pub struct TokenUsage { + /// Number of input (prompt) tokens consumed, if reported. pub input_tokens: Option, + /// Number of output (completion) tokens produced, if reported. pub output_tokens: Option, + /// Total tokens (input + output); computed when both are present. pub total_tokens: Option, + /// Estimated cost in US dollars, if reported. pub cost_usd: Option, } @@ -104,12 +108,14 @@ pub fn parse_opencode_output(output: &str) -> TokenUsage { parse_token_usage(output) } +/// Parse token usage from Anthropic `claude` CLI output. pub fn parse_claude_output(output: &str) -> TokenUsage { // Claude CLI may output usage differently // Example: "Input tokens: 1234, Output tokens: 567" parse_token_usage(output) } +/// Parse token usage from OpenAI `codex` CLI output. pub fn parse_codex_output(output: &str) -> TokenUsage { // Codex/OpenAI CLI output parse_token_usage(output) diff --git a/crates/terraphim_orchestrator/src/kg_router.rs b/crates/terraphim_orchestrator/src/kg_router.rs index da65e7146..1f3d34f6c 100644 --- a/crates/terraphim_orchestrator/src/kg_router.rs +++ b/crates/terraphim_orchestrator/src/kg_router.rs @@ -398,11 +398,14 @@ impl KgRouter { } } +/// Errors that can occur when loading or querying KG routing rules. #[derive(Debug, thiserror::Error)] pub enum KgRouterError { #[error("taxonomy directory not found: {0}")] + /// The taxonomy directory path does not exist or is not accessible. TaxonomyNotFound(String), #[error("failed to parse taxonomy: {0}")] + /// A taxonomy file could not be parsed as valid YAML or JSON. ParseError(String), } diff --git a/crates/terraphim_orchestrator/src/learning.rs b/crates/terraphim_orchestrator/src/learning.rs index efc4219f7..391b667ea 100644 --- a/crates/terraphim_orchestrator/src/learning.rs +++ b/crates/terraphim_orchestrator/src/learning.rs @@ -38,18 +38,23 @@ use uuid::Uuid; /// Errors for the shared learning store. #[derive(Debug, Error)] pub enum LearningError { + /// A persistence backend error occurred. #[error("storage error: {0}")] Storage(String), + /// A JSON serialisation or deserialisation error occurred. #[error("serialization error: {0}")] Serialization(#[from] serde_json::Error), + /// The supplied trust level string is not a recognised variant. #[error("invalid trust level: {0}")] InvalidTrustLevel(String), + /// No learning was found for the given identifier. #[error("learning not found: {0}")] NotFound(String), + /// An I/O error occurred, typically while writing context files. #[error("IO error: {0}")] Io(#[from] std::io::Error), } @@ -100,12 +105,19 @@ impl std::str::FromStr for TrustLevel { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum LearningCategory { + /// An LLM model returned an error or behaved unexpectedly. ModelError, + /// A pipeline step failed during agent execution. StepFailure, + /// A tool was unavailable or produced unexpected results. ToolHealth, + /// A general behavioural or process pattern. Pattern, + /// A practical tip for improving agent reliability or efficiency. Tip, + /// An unexpected timing anomaly, such as a timeout or latency spike. TimingAnomaly, + /// A pattern that recurs across multiple agent runs. RecurringPattern, } @@ -145,36 +157,53 @@ impl std::str::FromStr for LearningCategory { /// A shared learning extracted from agent runs. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Learning { + /// Unique identifier for this learning (UUID v4). pub id: String, + /// Name of the agent that originally produced this learning. pub source_agent: String, + /// Category classifying the type of learning. pub category: LearningCategory, + /// Short human-readable summary of the learning. pub summary: String, + /// Optional extended description or remediation steps. pub details: Option, /// Agents this learning applies to. Empty means all agents. pub applicable_agents: Vec, + /// Current trust level, auto-promoted as more agents confirm effectiveness. pub trust_level: TrustLevel, /// Shell command that must exit 0 for this learning to remain valid. pub verify_pattern: Option, + /// Number of times this learning has been applied by any agent. pub applied_count: u32, + /// Number of times this learning has been confirmed as effective. pub effective_count: u32, /// Distinct agent names that have applied or confirmed this learning. #[serde(default)] pub agent_names: Vec, + /// Timestamp at which the learning was first created. pub created_at: DateTime, + /// Timestamp of the most recent update to this learning. pub updated_at: DateTime, + /// Timestamp at which the learning was archived, if applicable. pub archived_at: Option>, } /// Input for creating a new learning (no id/timestamps). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NewLearning { + /// Name of the agent submitting this learning. pub source_agent: String, + /// Category classifying the type of learning. pub category: LearningCategory, + /// Short human-readable summary of the learning. pub summary: String, + /// Optional extended description or remediation steps. #[serde(default)] pub details: Option, + /// Agent names this learning applies to; empty means all agents. #[serde(default)] pub applicable_agents: Vec, + /// Optional shell command that must exit 0 to validate this learning. #[serde(default)] pub verify_pattern: Option, } @@ -228,6 +257,7 @@ pub struct InMemoryLearningPersistence { } impl InMemoryLearningPersistence { + /// Create a new, empty in-memory learning persistence store. pub fn new() -> Self { Self { data: std::sync::RwLock::new(HashMap::new()), @@ -402,6 +432,7 @@ impl LearningPersistence for InMemoryLearningPersistence { /// Each learning is stored as a JSON document keyed by `adf/learnings/{uuid}`. /// An index document at `adf/learnings/_index` maps ids for listing/querying. pub struct DeviceStorageLearningPersistence { + /// Storage key prefix, e.g. `adf/learnings`. key_prefix: String, /// In-memory cache loaded from persistence on startup. cache: tokio::sync::RwLock>, @@ -665,7 +696,9 @@ impl LearningPersistence for DeviceStorageLearningPersistence { /// Wraps a `LearningPersistence` impl and adds context-file generation /// and JSONL import capabilities. pub struct SharedLearningStore { + /// Underlying persistence backend. persistence: Box, + /// Minimum trust level required for learnings to be returned by queries. min_trust: TrustLevel, } @@ -691,24 +724,29 @@ impl SharedLearningStore { self.persistence.insert(learning).await } + /// Retrieve a learning by its unique identifier. pub async fn get(&self, id: &str) -> Result, LearningError> { self.persistence.get(id).await } + /// Query learnings relevant to the given agent, using the store's minimum trust level. pub async fn query_relevant(&self, agent_name: &str) -> Result, LearningError> { self.persistence .query_relevant(agent_name, self.min_trust) .await } + /// Record that a learning was applied by the given agent. pub async fn record_applied(&self, id: &str, applied_by: &str) -> Result<(), LearningError> { self.persistence.record_applied(id, applied_by).await } + /// Record that a learning was confirmed effective by the given agent, and auto-promote trust. pub async fn record_effective(&self, id: &str, applied_by: &str) -> Result<(), LearningError> { self.persistence.record_effective(id, applied_by).await } + /// Archive L0 learnings that have not been updated within `max_age_days` days. pub async fn archive_stale(&self, max_age_days: u32) -> Result { self.persistence.archive_stale(max_age_days).await } @@ -812,6 +850,9 @@ impl SharedLearningStore { } } +/// Run an async future to completion on the current Tokio runtime, blocking the calling thread. +/// +/// Used to adapt the async persistence layer to the synchronous `LearningStore` trait. fn block_on(fut: F) -> F::Output { tokio::task::block_in_place(|| { let rt = tokio::runtime::Handle::current(); @@ -819,6 +860,7 @@ fn block_on(fut: F) -> F::Output { }) } +/// Convert a `terraphim_types` trust level to the local `TrustLevel` enum. fn convert_trust(tl: terraphim_types::shared_learning::TrustLevel) -> TrustLevel { match tl { terraphim_types::shared_learning::TrustLevel::L0 => TrustLevel::L0, @@ -828,6 +870,7 @@ fn convert_trust(tl: terraphim_types::shared_learning::TrustLevel) -> TrustLevel } } +/// Convert a local `Learning` into the `terraphim_types` `SharedLearning` representation. fn to_shared_learning(l: &Learning) -> terraphim_types::shared_learning::SharedLearning { let mut sl = terraphim_types::shared_learning::SharedLearning::new( l.summary.clone(), diff --git a/crates/terraphim_orchestrator/src/lib.rs b/crates/terraphim_orchestrator/src/lib.rs index 95302471d..d83c4169c 100644 --- a/crates/terraphim_orchestrator/src/lib.rs +++ b/crates/terraphim_orchestrator/src/lib.rs @@ -31,40 +31,53 @@ pub mod adf_commands; pub mod agent_registry; +/// CLI subcommands and helpers for running and validating agents. pub mod agent_run_command; pub mod agent_run_record; +/// Background task runner that spawns and supervises individual agents. pub mod agent_runner; +/// Compound (multi-agent) review workflow coordination. pub mod compound; pub mod concurrency; +/// Orchestrator configuration types and loading utilities. pub mod config; pub mod control_plane; +/// Budget tracking and spend enforcement for agent API calls. pub mod cost_tracker; #[cfg(unix)] pub mod direct_dispatch; pub mod dispatcher; pub mod dual_mode; +/// Orchestrator-level error types. pub mod error; pub mod error_signatures; +/// Agent evolution and self-improvement mechanisms. pub mod evolution; +/// Workflow flow-control primitives (gates, barriers, forks). pub mod flow; pub mod gitea_skill_loader; +/// Inter-agent state handoff with TTL management. pub mod handoff; pub mod kg_router; pub mod learning; +/// Locally-cached skill content loader. pub mod local_skills; pub mod mention; pub mod mention_chain; pub mod meta_coordinator; pub mod metrics_persistence; pub mod mode; +/// Drift detection and rate-limiting monitor (Nightwatch). pub mod nightwatch; pub mod output_poster; +/// Agent persona definitions used in compound review swarms. pub mod persona; pub mod post_merge_gate; pub mod pr_dispatch; pub mod pr_gate; pub mod pr_poller; pub mod pr_review; +/// ADF (AI Dark Factory) project-level orchestration logic. pub mod project_adf; pub mod project_control; pub mod provider_budget; @@ -74,7 +87,9 @@ pub mod quickwit; #[cfg(feature = "quickwit")] pub mod quickwit_bulk; pub mod rate_limiter; +/// Time-based and event-driven task scheduling. pub mod scheduler; +/// Scope definitions that constrain where agents may operate. pub mod scope; pub mod webhook; pub mod worktree_guard; @@ -163,12 +178,19 @@ pub enum PreCheckResult { /// Status of a single agent in the fleet. #[derive(Debug, Clone)] pub struct AgentStatus { + /// Human-readable name identifying this agent. pub name: String, + /// Architectural layer this agent belongs to. pub layer: AgentLayer, + /// Whether the agent process is currently active. pub running: bool, + /// Current health classification of the agent. pub health: HealthStatus, + /// Optional drift score indicating deviation from expected behaviour. pub drift_score: Option, + /// Elapsed time since the agent was last started. pub uptime: Duration, + /// Number of times this agent has been restarted. pub restart_count: u32, /// API calls remaining per provider (None if no limit known). pub api_calls_remaining: HashMap>, @@ -274,7 +296,8 @@ pub struct AgentOrchestrator { active_flows: HashMap>, /// Active compound review execution (spawned in background to avoid /// blocking reconcile_tick). None when no compound review is running. - active_compound_review: Option>>, + active_compound_review: + Option>>, /// Per-project mention cursors, keyed by project id. /// /// Each project gets its own cursor so repo-wide polls can advance @@ -1694,11 +1717,13 @@ impl AgentOrchestrator { &mut self.cost_tracker } + /// Set the Quickwit fleet sink used to forward log events. #[cfg(feature = "quickwit")] pub fn set_quickwit_sink(&mut self, sink: quickwit::QuickwitFleetSink) { self.quickwit_sink = Some(sink); } + /// Return the top-level Quickwit configuration if present. #[cfg(feature = "quickwit")] pub fn quickwit_config(&self) -> Option<&QuickwitConfig> { self.config.quickwit.as_ref() @@ -6208,14 +6233,12 @@ impl AgentOrchestrator { if elapsed > std::time::Duration::from_secs(5) { warn!( tick = self.tick_count, - elapsed_ms, - "reconcile_tick SLOW: took > 5s, likely blocking agent polling" + elapsed_ms, "reconcile_tick SLOW: took > 5s, likely blocking agent polling" ); } else { info!( tick = self.tick_count, - elapsed_ms, - "reconcile_tick complete" + elapsed_ms, "reconcile_tick complete" ); } } @@ -8036,9 +8059,7 @@ Remove the pause flag once the underlying failure is resolved:\n\n\ let git_ref = "HEAD".to_string(); let base_ref = self.config.compound_review.base_branch.clone(); let workflow = self.compound_workflow.clone(); - let handle = tokio::spawn(async move { - workflow.run(&git_ref, &base_ref).await - }); + let handle = tokio::spawn(async move { workflow.run(&git_ref, &base_ref).await }); self.active_compound_review = Some(handle); } ScheduleEvent::Flow(flow_def) => { diff --git a/crates/terraphim_orchestrator/src/local_skills.rs b/crates/terraphim_orchestrator/src/local_skills.rs index e4e3b8c2c..cc799ac3c 100644 --- a/crates/terraphim_orchestrator/src/local_skills.rs +++ b/crates/terraphim_orchestrator/src/local_skills.rs @@ -2,17 +2,24 @@ use std::path::{Path, PathBuf}; use terraphim_spawner::SpawnContext; +/// Configuration for locally discovered skills in a project. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LocalSkillConfig { + /// Absolute path to the `.terraphim/skills` directory within the project. pub skills_dir: PathBuf, } +/// CLI tools that have native skill directory support. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum SupportedSkillCli { + /// The `opencode` CLI, which uses `.opencode/skill/`. Opencode, + /// The `claude` / `claude-code` CLI, which uses `.claude/skills/`. Claude, } +/// Discover a `.terraphim/skills` directory beneath `project_root`. +/// Returns `None` when no such directory exists. pub fn discover_local_skills(project_root: &Path) -> Option { let skills_dir = project_root.join(".terraphim/skills"); skills_dir @@ -20,6 +27,8 @@ pub fn discover_local_skills(project_root: &Path) -> Option { .then_some(LocalSkillConfig { skills_dir }) } +/// Identify whether `cli_tool` (a path or bare name) corresponds to a +/// supported skill CLI. Returns `None` for unknown tools. pub fn detect_skill_cli(cli_tool: &str) -> Option { match cli_name(cli_tool) { "opencode" => Some(SupportedSkillCli::Opencode), @@ -28,6 +37,10 @@ pub fn detect_skill_cli(cli_tool: &str) -> Option { } } +/// Configure `ctx` so that local skills from `project_root` are visible to +/// `cli_tool`. Creates a native skill symlink when the CLI is recognised, and +/// always sets the `TERRAPHIM_LOCAL_SKILLS_DIR` environment variable. +/// Returns `ctx` unchanged when no skills directory is found. pub fn prepare_local_skill_loading( cli_tool: &str, project_root: &Path, diff --git a/crates/terraphim_orchestrator/src/mention.rs b/crates/terraphim_orchestrator/src/mention.rs index ad01b677b..4796af5be 100644 --- a/crates/terraphim_orchestrator/src/mention.rs +++ b/crates/terraphim_orchestrator/src/mention.rs @@ -28,14 +28,21 @@ static MENTION_RE: LazyLock = LazyLock::new(|| { /// How a mention was resolved. #[derive(Debug, Clone, PartialEq)] pub enum MentionResolution { + /// The mention resolved to a known agent name. AgentName, - PersonaName { persona: String }, + /// The mention resolved to a persona, with the persona name captured. + PersonaName { + /// Name of the persona the mention resolved to. + persona: String, + }, } /// Parsed tokens of a single `@adf:[project/]name` mention. #[derive(Debug, Clone, PartialEq)] pub struct MentionTokens { + /// Optional project prefix extracted from a qualified `@adf:/` mention. pub project: Option, + /// Bare agent or persona name from the mention. pub agent: String, } @@ -61,13 +68,21 @@ pub fn parse_mention_tokens(text: &str) -> Vec { /// A detected and resolved mention. #[derive(Debug, Clone)] pub struct DetectedMention { + /// Gitea issue number the comment belongs to. pub issue_number: u64, + /// Gitea comment ID. pub comment_id: u64, + /// Raw agent or persona string from the `@adf:` mention. pub raw_mention: String, + /// Resolved agent name (after persona lookup if needed). pub agent_name: String, + /// How the mention was resolved (direct agent name or via persona). pub resolution: MentionResolution, + /// Full body of the comment containing the mention. pub comment_body: String, + /// Login of the user who posted the mention. pub mentioner: String, + /// RFC 3339 timestamp of the comment. pub timestamp: String, /// Project id the mention was detected in. /// @@ -162,6 +177,10 @@ impl MentionCursor { format!("adf/mention_cursor/{}", project_id) } + /// Load from persistence or create a "now" cursor for `project_id`. + /// + /// On first run (no persisted cursor), returns a cursor set to the current + /// time, effectively skipping all historical mentions. pub async fn load_or_now(project_id: &str) -> Self { let key = Self::cursor_key(project_id); @@ -550,6 +569,7 @@ pub struct MentionTracker { } impl MentionTracker { + /// Create a new tracker with the given per-issue dispatch limit. pub fn new(max_dispatches_per_issue: u32) -> Self { Self { max_dispatches_per_issue, diff --git a/crates/terraphim_orchestrator/src/mention_chain.rs b/crates/terraphim_orchestrator/src/mention_chain.rs index d18b73e04..f50f182f3 100644 --- a/crates/terraphim_orchestrator/src/mention_chain.rs +++ b/crates/terraphim_orchestrator/src/mention_chain.rs @@ -14,18 +14,32 @@ pub const DEFAULT_MAX_MENTION_DEPTH: u32 = 3; /// Errors from mention chain validation. #[derive(Debug, thiserror::Error)] pub enum MentionChainError { + /// An agent attempted to mention itself directly. #[error("agent '{agent}' cannot mention itself")] - SelfMention { agent: String }, + SelfMention { + /// Name of the agent that tried to self-mention. + agent: String, + }, + /// The mention chain has reached the configured maximum nesting depth. #[error("mention chain depth {depth} exceeds max {max_depth} for agent '{agent}'")] DepthExceeded { + /// Current depth at the time of the rejected dispatch. depth: u32, + /// Configured maximum depth that was exceeded. max_depth: u32, + /// Name of the agent that could not be dispatched. agent: String, }, + /// A direct A->B->A cycle was detected in the mention chain. #[error("cycle detected: {from} -> {to} would create a loop")] - CycleDetected { from: String, to: String }, + CycleDetected { + /// Agent that initiated the mention. + from: String, + /// Agent that would close the cycle. + to: String, + }, } /// Stateless mention chain validation. diff --git a/crates/terraphim_orchestrator/src/meta_coordinator.rs b/crates/terraphim_orchestrator/src/meta_coordinator.rs index 9c1e6a1a0..326357ab6 100644 --- a/crates/terraphim_orchestrator/src/meta_coordinator.rs +++ b/crates/terraphim_orchestrator/src/meta_coordinator.rs @@ -108,13 +108,21 @@ pub struct CandidateIssue { #[derive(Debug, Clone, PartialEq, Eq)] pub enum DispatchResult { /// Successfully dispatched. - Dispatched { agent: String, issue_id: String }, + Dispatched { + /// Name of the agent that was assigned the issue. + agent: String, + /// Gitea issue id that was dispatched. + issue_id: String, + }, /// No ready issues found. NoIssues, /// Issue was already dispatched recently. AlreadyDispatched, /// Failed to claim the issue. - ClaimFailed { reason: String }, + ClaimFailed { + /// Human-readable description of why the claim failed. + reason: String, + }, /// No matching agent for the issue. NoMatchingAgent, } diff --git a/crates/terraphim_orchestrator/src/metrics_persistence.rs b/crates/terraphim_orchestrator/src/metrics_persistence.rs index fecf0e7e3..c74ae54b1 100644 --- a/crates/terraphim_orchestrator/src/metrics_persistence.rs +++ b/crates/terraphim_orchestrator/src/metrics_persistence.rs @@ -88,12 +88,15 @@ pub trait MetricsPersistence: Send + Sync { #[derive(Debug, thiserror::Error)] pub enum MetricsPersistenceError { #[error("storage error: {0}")] + /// A low-level storage backend error. Storage(String), #[error("serialization error: {0}")] + /// JSON serialisation or deserialisation failed. Serialization(#[from] serde_json::Error), #[error("agent not found: {0}")] + /// No metrics record exists for the requested agent. NotFound(String), } diff --git a/crates/terraphim_orchestrator/src/nightwatch.rs b/crates/terraphim_orchestrator/src/nightwatch.rs index 76787730c..929586100 100644 --- a/crates/terraphim_orchestrator/src/nightwatch.rs +++ b/crates/terraphim_orchestrator/src/nightwatch.rs @@ -220,9 +220,13 @@ pub struct DriftMetrics { /// Drift score combining all metrics into a single 0.0-1.0 value. #[derive(Debug, Clone)] pub struct DriftScore { + /// Name of the agent being scored. pub agent_name: String, + /// Composite drift score in the range 0.0–1.0 (higher = more drift). pub score: f64, + /// Underlying behavioural metrics used to compute the score. pub metrics: DriftMetrics, + /// Classification of the drift severity. pub level: CorrectionLevel, } @@ -244,8 +248,11 @@ pub enum CorrectionLevel { /// Alert emitted by NightwatchMonitor when drift exceeds threshold. #[derive(Debug, Clone)] pub struct DriftAlert { + /// Name of the agent that triggered the alert. pub agent_name: String, + /// Full drift score with metrics and classification level. pub drift_score: DriftScore, + /// Suggested corrective action for the orchestrator to take. pub recommended_action: CorrectionAction, } @@ -437,6 +444,8 @@ impl NightwatchMonitor { .expect("alert channel should never close while monitor exists") } + /// Replace the internal alert receiver and return the old one, allowing + /// external code to consume alerts without holding a mutable reference. pub fn take_alert_rx(&mut self) -> Option> { let (_, rx) = mpsc::channel(1); Some(std::mem::replace(&mut self.alert_rx, rx)) diff --git a/crates/terraphim_orchestrator/src/persona.rs b/crates/terraphim_orchestrator/src/persona.rs index d385f56ce..7d1b8a53c 100644 --- a/crates/terraphim_orchestrator/src/persona.rs +++ b/crates/terraphim_orchestrator/src/persona.rs @@ -111,10 +111,13 @@ const TEMPLATE_NAME: &str = "metaprompt"; #[derive(Debug, thiserror::Error)] pub enum MetapromptRenderError { #[error("IO error: {0}")] + /// An I/O error occurred while reading a template file. Io(#[from] std::io::Error), #[error("Template compilation error: {0}")] + /// The Handlebars template failed to compile. Template(String), #[error("Template render error: {0}")] + /// The template rendered with an error (e.g. missing variable in strict mode). Render(String), } diff --git a/crates/terraphim_orchestrator/src/post_merge_gate.rs b/crates/terraphim_orchestrator/src/post_merge_gate.rs index 43cea135f..82ee217ba 100644 --- a/crates/terraphim_orchestrator/src/post_merge_gate.rs +++ b/crates/terraphim_orchestrator/src/post_merge_gate.rs @@ -161,8 +161,10 @@ pub struct FailureClassification { /// Errors produced while running or reverting on the gate. #[derive(Debug, thiserror::Error)] pub enum GateError { + /// A subprocess command failed with an I/O error or timed out. #[error("command error: {0}")] Command(#[from] CommandError), + /// The `git revert` step or its subsequent push failed. #[error("revert failed: {0}")] Revert(String), } @@ -429,8 +431,11 @@ async fn tail_stream(reader: R, max_lines: usize) -> Strin /// assert the handler invoked the expected commands in the expected order. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CallRecord { + /// The executable name that was passed to `run` (e.g. `"cargo"` or `"git"`). pub cmd: String, + /// The argument list that was passed alongside `cmd`. pub args: Vec, + /// The working directory in which the command was executed. pub cwd: PathBuf, } @@ -444,10 +449,12 @@ pub struct ScriptedRunner { } impl ScriptedRunner { + /// Create a new empty [`ScriptedRunner`] with no queued responses. pub fn new() -> Self { Self::default() } + /// Enqueue a successful response with the given exit code and output text. pub fn push_ok(&self, code: i32, stdout: &str, stderr: &str) { self.responses.lock().unwrap().push_back(Ok(CommandOutput { exit_code: Some(code), @@ -457,10 +464,12 @@ impl ScriptedRunner { })); } + /// Enqueue an error response that will be returned on the next `run` call. pub fn push_err(&self, err: CommandError) { self.responses.lock().unwrap().push_back(Err(err)); } + /// Return a snapshot of all recorded [`CallRecord`]s in invocation order. pub fn calls(&self) -> Vec { self.calls.lock().unwrap().clone() } diff --git a/crates/terraphim_orchestrator/src/pr_dispatch.rs b/crates/terraphim_orchestrator/src/pr_dispatch.rs index 5774b915f..d6f2cfcac 100644 --- a/crates/terraphim_orchestrator/src/pr_dispatch.rs +++ b/crates/terraphim_orchestrator/src/pr_dispatch.rs @@ -21,11 +21,17 @@ use crate::config::{AgentDefinition, OrchestratorConfig}; /// have to know about the dispatcher enum variant shape. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReviewPrRequest { + /// Pull-request number in the Gitea repository. pub pr_number: u64, + /// Project identifier (e.g. `"terraphim-ai"`). pub project: String, + /// HEAD commit SHA of the pull-request branch. pub head_sha: String, + /// Gitea login of the PR author. pub author_login: String, + /// PR title. pub title: String, + /// Approximate lines-of-code changed (used for budget gating). pub diff_loc: u32, } diff --git a/crates/terraphim_orchestrator/src/pr_gate.rs b/crates/terraphim_orchestrator/src/pr_gate.rs index 31f1b1e9d..336fdc9b3 100644 --- a/crates/terraphim_orchestrator/src/pr_gate.rs +++ b/crates/terraphim_orchestrator/src/pr_gate.rs @@ -9,9 +9,13 @@ /// Terminal state of a single commit status context. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CommitStatusState { + /// The check has been submitted but has not yet completed. Pending, + /// The check completed successfully. Success, + /// The check completed with a failure result. Failure, + /// The check encountered an error (distinct from a deliberate failure). Error, } @@ -35,7 +39,9 @@ impl CommitStatusState { /// One commit status entry posted against a SHA. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CommitStatusSummary { + /// The context name identifying which check posted this status (e.g. `"adf/build"`). pub context: String, + /// The current state of this status entry. pub state: CommitStatusState, /// Unix timestamp (seconds) when the status was created, if available. pub created_at_unix: Option, @@ -47,8 +53,11 @@ pub const STALE_PENDING_TIMEOUT_SECS: i64 = 3600; /// Snapshot of everything the reconciler needs to classify a PR head. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PrGateSnapshot { + /// Numeric identifier of the pull request. pub pr_number: u64, + /// Full SHA of the PR head commit being evaluated. pub head_sha: String, + /// Name of the base branch targeted by the PR (e.g. `"main"`). pub base_branch: String, /// Context names required by branch protection (e.g. `["adf/build", "adf/pr-reviewer"]`). pub required_contexts: Vec, @@ -64,13 +73,25 @@ pub enum PrGateDecision { /// All required contexts green; proceed to auto-merge policy evaluation. ReadyForPolicy, /// Required contexts not yet posted; enqueue the responsible agents. - EnqueueMissingChecks { missing: Vec }, + EnqueueMissingChecks { + /// Context names that have no status posted on the head SHA. + missing: Vec, + }, /// Required contexts posted but still pending; wait for next reconcile tick. - AwaitingChecks { pending: Vec }, + AwaitingChecks { + /// Context names whose latest status is still in a pending (non-terminal) state. + pending: Vec, + }, /// At least one required context failed; open remediation issue. - BlockedByFailedChecks { failed: Vec<(String, String)> }, + BlockedByFailedChecks { + /// `(context_name, state_label)` pairs for each failed required context. + failed: Vec<(String, String)>, + }, /// Status API or branch protection API failure; service fault. - FactoryFault { error: String }, + FactoryFault { + /// Human-readable description of the fault. + error: String, + }, } /// Reconcile the PR gate state from a snapshot. Pure function. diff --git a/crates/terraphim_orchestrator/src/pr_poller.rs b/crates/terraphim_orchestrator/src/pr_poller.rs index 29d9df79a..753123a39 100644 --- a/crates/terraphim_orchestrator/src/pr_poller.rs +++ b/crates/terraphim_orchestrator/src/pr_poller.rs @@ -50,10 +50,15 @@ pub const PR_POLL_MIN_INTERVAL: Duration = Duration::from_secs(60); /// that tests can construct it without a live Gitea server. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PrSummary { + /// Pull request number in the Gitea repository. pub number: u64, + /// Login of the PR author. pub author_login: String, + /// Current HEAD commit SHA of the PR branch. pub head_sha: String, + /// Target branch (e.g. `main`) that the PR will merge into. pub base_ref: String, + /// Total lines of code changed (additions + deletions) as reported by Gitea. pub diff_loc: u32, } @@ -61,8 +66,11 @@ pub struct PrSummary { /// parsing are captured; the full Gitea payload is deliberately not mirrored. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PrComment { + /// Unique comment ID assigned by Gitea. pub id: u64, + /// Login of the user who posted the comment. pub user_login: String, + /// Raw Markdown body of the comment. pub body: String, /// RFC3339-ish `updated_at` string from the Gitea API. Used only for /// ordering; comments without a timestamp sort as the earliest. @@ -74,7 +82,9 @@ pub struct PrComment { /// PR N carry?". Kept minimal so the test impl stays trivial. #[async_trait] pub trait PrTracker: Send + Sync { + /// Return all currently open pull requests for the tracked project. async fn list_open_prs(&self) -> Result, String>; + /// Return all comments on the pull request identified by `pr_number`. async fn fetch_pr_comments(&self, pr_number: u64) -> Result, String>; } @@ -84,8 +94,11 @@ pub trait PrTracker: Send + Sync { /// concrete types into test code. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MergeOutcome { + /// Number of the pull request that was merged. pub pr_number: u64, + /// SHA of the merge commit created on the base branch. pub merge_commit_sha: String, + /// Title of the pull request at the time it was merged. pub title: String, } @@ -121,6 +134,7 @@ pub struct GiteaPrTracker { } impl GiteaPrTracker { + /// Wrap an existing [`terraphim_tracker::GiteaTracker`] in a [`GiteaPrTracker`]. pub fn new(inner: terraphim_tracker::GiteaTracker) -> Self { Self { inner } } @@ -196,14 +210,23 @@ impl AutoMergeExecutor for GiteaPrTracker { #[derive(Debug, Clone, PartialEq, Eq)] pub enum EvaluationOutcome { /// Every gate cleared; the caller should enqueue [`crate::dispatcher::DispatchTask::AutoMerge`]. - Merge { head_sha: String }, + Merge { + /// HEAD SHA of the PR branch at the time the verdict was parsed. + head_sha: String, + }, /// At least one gate failed. The reason is a short human-readable string /// suitable for logging or posting back to the PR. - HumanReviewNeeded { reason: String }, + HumanReviewNeeded { + /// Short human-readable explanation of which gate failed. + reason: String, + }, /// No pr-reviewer comment found yet — nothing to evaluate this tick. NoReviewerComment, /// A reviewer comment exists but did not parse as a structural verdict. - ParseError { reason: String }, + ParseError { + /// Description of the parse failure (e.g. missing confidence header). + reason: String, + }, } /// Return `true` when `comment.user_login == PR_REVIEWER_LOGIN` **or** the @@ -316,6 +339,7 @@ pub struct PrPollRateLimiter { } impl PrPollRateLimiter { + /// Create a new rate limiter with the given minimum interval between polls of the same PR. pub fn new(min_interval: Duration) -> Self { Self { last_poll: HashMap::new(), @@ -346,6 +370,7 @@ pub struct AutoMergeDedupeSet { } impl AutoMergeDedupeSet { + /// Create an empty dedupe set. pub fn new() -> Self { Self::default() } diff --git a/crates/terraphim_orchestrator/src/pr_review.rs b/crates/terraphim_orchestrator/src/pr_review.rs index 3d0073474..75a782c65 100644 --- a/crates/terraphim_orchestrator/src/pr_review.rs +++ b/crates/terraphim_orchestrator/src/pr_review.rs @@ -51,11 +51,17 @@ pub struct ReviewVerdict { /// policy with a 500 LoC diff cap and an agent-author requirement. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AutoMergeCriteria { + /// Minimum confidence score (1–5) required for auto-merge. pub min_confidence: u8, + /// Maximum number of P0 findings permitted before requiring human review. pub max_p0: u32, + /// Maximum number of P1 findings permitted before requiring human review. pub max_p1: u32, + /// When `true`, every acceptance-criteria checkbox must be ticked. pub require_all_criteria: bool, + /// Upper bound on total lines-of-code changed in the diff. pub max_diff_loc: u32, + /// When `true`, the PR author must be a recognised automation account. pub require_agent_author: bool, } @@ -75,10 +81,15 @@ impl Default for AutoMergeCriteria { /// Minimum PR metadata required by [`evaluate`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PrMetadata { + /// Numeric identifier of the pull request. pub pr_number: u64, + /// Gitea/GitHub login of the PR author. pub author_login: String, + /// Total lines of code changed (additions + deletions) in the diff. pub diff_loc: u32, + /// Full SHA of the head commit being reviewed. pub head_sha: String, + /// Target branch the PR is merging into (e.g. `"main"`). pub base_branch: String, } @@ -97,12 +108,16 @@ pub enum AutoMergeDecision { #[derive(Error, Debug, PartialEq, Eq)] pub enum VerdictParseError { #[error("missing confidence score header in review comment")] + /// No confidence score header was found in the review comment. MissingConfidence, #[error("confidence score out of range: got {0}")] + /// Confidence score was present but outside the valid 0–100 range. ConfidenceOutOfRange(u8), #[error("missing inline findings section")] + /// The inline findings section was absent from the review comment. MissingFindings, #[error("malformed footer (expected `Last reviewed commit: `)")] + /// The footer line did not match the expected format. MalformedFooter, } diff --git a/crates/terraphim_orchestrator/src/project_adf.rs b/crates/terraphim_orchestrator/src/project_adf.rs index 7e32690a0..08ac999ea 100644 --- a/crates/terraphim_orchestrator/src/project_adf.rs +++ b/crates/terraphim_orchestrator/src/project_adf.rs @@ -22,68 +22,104 @@ impl FromStr for AgentLayer { } } +/// Raw TOML representation of a project-level ADF configuration file (`.terraphim/adf.toml`). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TomlProjectAdfConfig { + /// Unique identifier for the project; used as the primary key across the orchestrator. pub project_id: String, + /// Human-readable display name for the project. pub name: String, + /// List of agent definitions declared for this project. #[serde(default)] pub agents: Vec, + /// Optional list of agents to dispatch when a pull request is opened. #[serde(default)] pub pr_dispatch: Option>, } +/// Raw TOML representation of a single agent entry inside `adf.toml`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TomlAdfAgent { + /// Unique name identifying this agent within the project. pub name: String, + /// Orchestration layer: one of `"Safety"`, `"Core"`, or `"Growth"`. pub layer: String, + /// Path or bare name of the CLI tool used to invoke the agent. pub cli_tool: String, + /// Description of the work the agent should perform. pub task: String, + /// Optional model identifier passed to the CLI tool via `--model`. #[serde(default)] pub model: Option, + /// Optional cron schedule expression for periodic execution. #[serde(default)] pub schedule: Option, + /// Capability tags used for routing and capability-based dispatch. #[serde(default)] pub capabilities: Vec, + /// Optional monthly spend cap in US cents for this agent. #[serde(default)] pub budget_monthly_cents: Option, + /// Optional provider prefix used to resolve the CLI tool at runtime. #[serde(default)] pub provider: Option, + /// Optional persona name injected into the agent's prompt context. #[serde(default)] pub persona: Option, + /// Ordered list of skill identifiers to apply before the main task. #[serde(default)] pub skill_chain: Vec, + /// Fallback provider used when the primary provider is unavailable. #[serde(default)] pub fallback_provider: Option, + /// Fallback model identifier used with `fallback_provider`. #[serde(default)] pub fallback_model: Option, + /// Seconds to wait before restarting the agent after a non-fatal failure. #[serde(default)] pub grace_period_secs: Option, + /// Optional CPU time limit in seconds for a single agent run. #[serde(default)] pub max_cpu_seconds: Option, + /// Optional pre-dispatch health or gate check strategy. #[serde(default)] pub pre_check: Option, + /// Gitea issue number this agent is associated with, if any. #[serde(default)] pub gitea_issue: Option, + /// When `true`, the agent runs only in response to events, not on a schedule. #[serde(default)] pub event_only: bool, + /// When `true`, the agent participates in the evolution/learning pipeline. #[serde(default)] pub evolution_enabled: bool, + /// Enables or disables the reinforcement learning module for this agent. #[serde(default)] pub rlm_enabled: Option, } +/// A single entry in the `[[pr_dispatch]]` TOML table, mapping an agent to a PR check context. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TomlPrDispatchEntry { + /// Name of the agent to dispatch when a pull request is opened. pub name: String, + /// CI/CD context key (e.g. `"adf/build"`) that triggers this dispatch entry. pub context: String, } +/// Validated, in-memory representation of a project's ADF configuration after parsing and +/// environment-variable expansion. #[derive(Debug, Clone)] pub struct ProjectAdfConfig { + /// Unique project identifier, expanded from the raw TOML value. pub project_id: String, + /// Human-readable project name, expanded from the raw TOML value. pub name: String, + /// Agent definitions parsed from the TOML file. pub agents: Vec, + /// Optional PR-open dispatch configuration converted from the TOML entries. pub pr_dispatch: Option, + /// Absolute path to the `adf.toml` file that was loaded. pub discovered_path: PathBuf, } @@ -161,6 +197,7 @@ impl TomlProjectAdfConfig { } impl ProjectAdfConfig { + /// Returns the project root directory (two levels above the `adf.toml` file). pub fn project_root(&self) -> PathBuf { self.discovered_path .parent() @@ -169,6 +206,7 @@ impl ProjectAdfConfig { .unwrap_or_else(|| self.discovered_path.clone()) } + /// Returns the path to the project's Terraphim skills directory (`.terraphim/skills`). pub fn skills_dir(&self) -> PathBuf { self.project_root().join(".terraphim/skills") } @@ -189,6 +227,8 @@ impl ProjectAdfConfig { None } + /// Walks up from `cwd` looking for a `.terraphim/adf.toml` file, parses it, and returns the + /// loaded configuration, or `None` if no such file exists. pub fn discover_and_load(cwd: &Path) -> Result, OrchestratorError> { let terraphim_dir = match Self::discover_terraphim_dir(cwd) { Some(d) => d, diff --git a/crates/terraphim_orchestrator/src/provider_budget.rs b/crates/terraphim_orchestrator/src/provider_budget.rs index da4a44f4d..9cab6c9b1 100644 --- a/crates/terraphim_orchestrator/src/provider_budget.rs +++ b/crates/terraphim_orchestrator/src/provider_budget.rs @@ -55,13 +55,16 @@ pub struct WindowState { /// Serialisable snapshot of a single provider's two windows. #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct ProviderSnapshotEntry { + /// Hour-window state for this provider. pub hour: WindowState, + /// Day-window state for this provider. pub day: WindowState, } /// Serialisable snapshot of the whole tracker. #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct ProviderBudgetSnapshot { + /// Per-provider window state, keyed by provider id. pub providers: HashMap, } @@ -261,6 +264,7 @@ impl ProviderBudgetTracker { self.configs.keys().map(|s| s.as_str()) } + /// Returns `true` if no provider configs have been registered. pub fn is_empty(&self) -> bool { self.configs.is_empty() } diff --git a/crates/terraphim_orchestrator/src/provider_probe.rs b/crates/terraphim_orchestrator/src/provider_probe.rs index fca34e950..846dc5fbd 100644 --- a/crates/terraphim_orchestrator/src/provider_probe.rs +++ b/crates/terraphim_orchestrator/src/provider_probe.rs @@ -16,12 +16,19 @@ use crate::rate_limiter::RateLimiter; /// Result of probing a single provider+model combination. #[derive(Debug, Clone, serde::Serialize)] pub struct ProbeResult { + /// Provider name (e.g. `"kimi"`, `"minimax"`). pub provider: String, + /// Model identifier as used in routing rules (e.g. `"kimi-for-coding/k2p5"`). pub model: String, + /// Base name of the CLI tool that executed the probe (e.g. `"opencode"`). pub cli_tool: String, + /// Outcome of the probe attempt. pub status: ProbeStatus, + /// Round-trip latency in milliseconds, absent when the process failed to spawn. pub latency_ms: Option, + /// Human-readable error description when `status` is not `Success`. pub error: Option, + /// RFC 3339 timestamp of when the probe was executed. pub timestamp: String, } @@ -29,9 +36,13 @@ pub struct ProbeResult { #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum ProbeStatus { + /// The probe command exited successfully and produced token-bearing output. Success, + /// The probe command exited with a non-zero status or produced no token content. Error, + /// The probe command did not complete within the allotted 15 seconds. Timeout, + /// The provider responded with a rate-limit signal. RateLimited, } @@ -74,11 +85,13 @@ impl ProviderHealthMap { } } + /// Attach a shared [`RateLimiter`] that gates per-provider probe frequency. pub fn with_rate_limiter(mut self, rate_limiter: RateLimiter) -> Self { self.rate_limiter = Some(rate_limiter); self } + /// Return `true` when `provider` is currently marked as rate-limited. pub fn is_rate_limited(&self, provider: &str) -> bool { self.rate_limited.contains(provider) } @@ -724,6 +737,7 @@ async fn probe_single( } impl ProviderHealthMap { + /// Ship the latest probe results to a Quickwit index via `sink`. pub async fn send_to_quickwit( &self, sink: &crate::quickwit::QuickwitFleetSink, diff --git a/crates/terraphim_orchestrator/src/quickwit.rs b/crates/terraphim_orchestrator/src/quickwit.rs index dc09dbc62..1301a7746 100644 --- a/crates/terraphim_orchestrator/src/quickwit.rs +++ b/crates/terraphim_orchestrator/src/quickwit.rs @@ -124,36 +124,54 @@ impl From> for QuickwitError { pub enum OrchestratorEvent { /// Reviewer agent completed and a verdict was successfully parsed. PrReviewed { + /// Gitea pull-request number. pr_number: u64, + /// Project identifier owning the PR. project: String, + /// HEAD commit SHA at the time of review. head_sha: String, + /// Gitea login of the reviewer agent. reviewer_login: String, /// Confidence score 1-5 from the `structural-pr-review` comment. confidence: u8, + /// Number of P0 (blocker) findings in the review. p0_count: u32, + /// Number of P1 (major) findings in the review. p1_count: u32, /// `"GO"` | `"CONDITIONAL"` | `"NO-GO"` verdict: String, }, /// AutoMerge handler merged the PR successfully. PrAutoMerged { + /// Gitea pull-request number. pr_number: u64, + /// Project identifier owning the PR. project: String, + /// Merge commit SHA produced by Gitea. merge_sha: String, + /// PR title at time of merge. title: String, }, /// Post-merge test gate passed; the merge is stable. PrAutoMergedVerified { + /// Gitea pull-request number. pr_number: u64, + /// Project identifier owning the PR. project: String, + /// Merge commit SHA that passed the gate. merge_sha: String, + /// Wall-clock seconds from merge to gate completion. wall_time_secs: f64, }, /// Post-merge test gate failed and the merge was reverted. PrAutoReverted { + /// Gitea pull-request number. pr_number: u64, + /// Project identifier owning the PR. project: String, + /// Merge commit SHA that was reverted. merge_sha: String, + /// Revert commit SHA. revert_sha: String, /// Classified failure kind (e.g. `"TestFailure"`, `"Timeout"`). reason: String, diff --git a/crates/terraphim_orchestrator/src/scheduler.rs b/crates/terraphim_orchestrator/src/scheduler.rs index 6673d597e..145fc9b09 100644 --- a/crates/terraphim_orchestrator/src/scheduler.rs +++ b/crates/terraphim_orchestrator/src/scheduler.rs @@ -13,7 +13,10 @@ pub enum ScheduleEvent { /// Time to spawn this agent. Spawn(Box), /// Time to stop this agent. - Stop { agent_name: String }, + Stop { + /// Name of the agent to stop. + agent_name: String, + }, /// Time to run compound review. CompoundReview, /// Time to run a flow. @@ -81,6 +84,7 @@ impl TimeScheduler { }) } + /// Detach and return the event receiver, replacing it with a fresh channel. pub fn take_event_rx(&mut self) -> Option> { let (_, rx) = mpsc::channel(1); Some(std::mem::replace(&mut self.event_rx, rx)) diff --git a/crates/terraphim_orchestrator/src/webhook.rs b/crates/terraphim_orchestrator/src/webhook.rs index 4800e1886..23eb82cd9 100644 --- a/crates/terraphim_orchestrator/src/webhook.rs +++ b/crates/terraphim_orchestrator/src/webhook.rs @@ -25,54 +25,83 @@ struct GiteaWebhookPayload { repository: GiteaRepository, } +/// A single comment on a Gitea issue or pull request. #[derive(Debug, Deserialize)] struct GiteaComment { + /// Unique numeric identifier of this comment. id: u64, + /// Raw text body of the comment. body: String, + /// Author of the comment. user: GiteaUser, + /// ISO-8601 timestamp when the comment was created. created_at: String, } +/// A Gitea user identity extracted from webhook payloads. #[derive(Debug, Deserialize)] pub struct GiteaUser { + /// The user's Gitea username (login handle). pub login: String, } +/// Minimal issue data embedded in a Gitea issue_comment webhook payload. #[derive(Debug, Deserialize)] struct GiteaIssue { + /// Issue number within the repository. number: u64, + /// Title of the issue. title: String, + /// Current state of the issue, e.g. `"open"` or `"closed"`. state: String, } +/// Repository identity extracted from Gitea webhook payloads. #[derive(Debug, Deserialize)] pub struct GiteaRepository { + /// Full repository name in `owner/repo` format, e.g. `"terraphim/terraphim-ai"`. pub full_name: String, } /// Gitea webhook payload for pull_request events. #[derive(Debug, Deserialize)] pub struct GiteaPullRequestPayload { + /// Action that triggered the event, e.g. `"opened"`, `"synchronize"`, `"closed"`. pub action: String, + /// Pull request number within the repository. pub number: u64, + /// Detailed fields for the pull request. pub pull_request: PullRequestFields, + /// Repository in which the pull request was opened. pub repository: GiteaRepository, } +/// Detailed fields for a Gitea pull request embedded in webhook payloads. #[derive(Debug, Deserialize)] pub struct PullRequestFields { + /// Head (source) branch reference of the pull request. pub head: PrRef, + /// Base (target) branch reference of the pull request. pub base: PrRef, + /// Author who opened the pull request. pub user: GiteaUser, + /// Title of the pull request. pub title: String, + /// Whether the pull request is marked as a draft. pub draft: bool, + /// Total number of lines added across all commits. pub additions: u32, + /// Total number of lines deleted across all commits. pub deletions: u32, } +/// A git ref (branch or tag) endpoint in a pull request, carrying both the +/// branch name and the resolved commit SHA. #[derive(Debug, Deserialize)] pub struct PrRef { + /// Full commit SHA at the tip of this ref. pub sha: String, + /// Branch or tag name, deserialized from the JSON `ref` key. #[serde(rename = "ref")] pub ref_name: String, } @@ -80,34 +109,54 @@ pub struct PrRef { /// A dispatch request sent from the webhook handler to the orchestrator. #[derive(Debug, serde::Serialize, serde::Deserialize)] pub enum WebhookDispatch { + /// Spawn a named ADF agent in response to an `@adf:name` mention. SpawnAgent { + /// Name of the agent to spawn. agent_name: String, /// Project extracted from a qualified `@adf:project/name` mention, or /// `None` for unqualified `@adf:name` mentions. detected_project: Option, + /// Issue number the mention appeared on. issue_number: u64, + /// Comment ID that contained the mention. comment_id: u64, + /// Full text body of the triggering comment. context: String, /// Optional synthetic event for direct dispatch of event-only agents. #[serde(default)] synthetic_event: Option, }, + /// Spawn a named persona in response to an `@adf:persona:name` mention. SpawnPersona { + /// Name of the persona to invoke. persona_name: String, + /// Issue number the mention appeared on. issue_number: u64, + /// Comment ID that contained the mention. comment_id: u64, + /// Full text body of the triggering comment. context: String, }, + /// Trigger a compound review across all registered reviewers for an issue. CompoundReview { + /// Issue number to review. issue_number: u64, + /// Comment ID that triggered the compound review. comment_id: u64, }, + /// Trigger a structural PR review for a newly opened or updated pull request. ReviewPr { + /// Pull request number to review. pr_number: u64, + /// Project identifier derived from the repository name. project: String, + /// Commit SHA at the head of the pull request. head_sha: String, + /// Gitea login of the PR author. author_login: String, + /// Title of the pull request. title: String, + /// Sum of additions and deletions (lines of change) for the PR. diff_loc: u32, }, /// Push event dispatch — triggers the deterministic `build-runner` agent @@ -164,6 +213,7 @@ fn group_alias_members<'a>(alias: &str, agent_names: &'a [String]) -> Vec<&'a st .collect() } +/// Deserialise a JSON null or missing array as an empty `Vec`. fn deserialize_null_default_vec<'de, D, T>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, @@ -172,6 +222,7 @@ where Option::>::deserialize(deserializer).map(|v| v.unwrap_or_default()) } +/// Deserialise a JSON null or missing value as `T::default()`. fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -183,29 +234,41 @@ where /// Gitea webhook payload for `push` events (Phase 3). #[derive(Debug, Deserialize)] pub struct GiteaPushPayload { + /// Full git ref that was pushed, e.g. `"refs/heads/main"`. #[serde(rename = "ref")] pub ref_name: String, + /// Commit SHA before the push (all zeros for a newly created branch). pub before: String, + /// Commit SHA after the push (the new tip). pub after: String, + /// Identity of the user who performed the push. #[serde(default, deserialize_with = "deserialize_null_default")] pub pusher: GiteaPusher, + /// Repository that received the push. pub repository: GiteaRepository, + /// List of commits included in this push. #[serde(default, deserialize_with = "deserialize_null_default_vec")] pub commits: Vec, } +/// Identity of the user who performed a push, extracted from push webhook payloads. #[derive(Debug, Default, Deserialize)] pub struct GiteaPusher { + /// Gitea login of the pusher; empty string when omitted by the server. #[serde(default)] pub login: String, } +/// File change summary for a single commit in a push webhook payload. #[derive(Debug, Deserialize)] pub struct GiteaPushCommit { + /// Paths of files added by this commit. #[serde(default, deserialize_with = "deserialize_null_default_vec")] pub added: Vec, + /// Paths of files removed by this commit. #[serde(default, deserialize_with = "deserialize_null_default_vec")] pub removed: Vec, + /// Paths of files modified by this commit. #[serde(default, deserialize_with = "deserialize_null_default_vec")] pub modified: Vec, } @@ -213,10 +276,15 @@ pub struct GiteaPushCommit { /// Shared state for the webhook handler. #[derive(Clone)] pub struct WebhookState { + /// List of all registered agent names, used for mention matching and group alias expansion. pub agent_names: Vec, + /// Registry of available personas, used to resolve `@adf:persona:name` mentions. pub persona_registry: std::sync::Arc, + /// Channel sender used to forward parsed dispatches to the orchestrator. pub dispatch_tx: tokio::sync::mpsc::Sender, + /// Optional shared secret for HMAC-SHA256 signature verification; `None` disables verification. pub secret: Option, + /// Mapping from Gitea `owner/repo` full names to project identifiers. pub project_by_repo: std::collections::HashMap, } From 7dbdef75c401d0c09a44965fe033f64d0af32a2c Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 2 Jun 2026 13:07:18 +0200 Subject: [PATCH 3/4] docs: fill all missing doc gaps in terraphim_multi_agent and terraphim_grep terraphim_multi_agent (292 gaps resolved): - Hook system: HookDecision, PreToolContext, PostToolContext, PreLlmContext, PostLlmContext structs + Hook trait + HookManager, all hook implementations - LLM types: MessageRole, LlmMessage, LlmRequest, TokenUsage, LlmResponse - Tracking: CostRecord, UsageStats, AlertWindow/Action, CostTracker - Agent: TerraphimAgent fields, AgentGoals methods - History: CommandRecord, CommandInput, ExecutionStep, CommandOutput, CommandError, CommandStatistics, CommandPriority, StepStatus, OutputType - Chat: ChatMessage, ChatSession, ChatMessageRole, SummaryStyle - VM execution: session_adapter, fcctl_bridge, mod, client, hooks - Pool: LoadMetrics, PoolStats, PoolInfo, GlobalStats - Error: struct variant fields for limit/timeout errors - Sanitizer: SanitizedPrompt, sanitize/validate functions terraphim_grep (125 gaps resolved): - Search: GrepResult, GrepStats, SufficiencyState, TerraphimGrep - Hybrid: GrepOptions, Haystack, RetrievedChunk, KgConcept, HybridResults - Signatures: RlmSignature, Match, Citation, AnswerWithCitations, NewConcept - Context: RlmContext, DocumentMetadata - Judge: HeuristicThresholds, Sufficiency, SufficiencyJudge - Error: TerraphimGrepError variants + Result alias - KG curation: stub and llm feature structs Refs #1979 --- crates/terraphim_grep/src/error.rs | 9 ++ crates/terraphim_grep/src/hybrid_searcher.rs | 34 ++++++ crates/terraphim_grep/src/kg_curation.rs | 7 ++ crates/terraphim_grep/src/lib.rs | 28 +++++ crates/terraphim_grep/src/rlm_context.rs | 14 +++ crates/terraphim_grep/src/signatures.rs | 24 ++++ .../terraphim_grep/src/sufficiency_judge.rs | 13 +++ crates/terraphim_multi_agent/src/agent.rs | 35 ++++-- .../src/agents/chat_agent.rs | 21 ++++ .../src/agents/summarization_agent.rs | 5 + crates/terraphim_multi_agent/src/context.rs | 4 + crates/terraphim_multi_agent/src/error.rs | 26 ++++- .../src/genai_llm_client.rs | 5 + crates/terraphim_multi_agent/src/history.rs | 50 ++++++++ crates/terraphim_multi_agent/src/lib.rs | 19 ++- crates/terraphim_multi_agent/src/llm_types.rs | 31 +++++ crates/terraphim_multi_agent/src/pool.rs | 14 +++ .../terraphim_multi_agent/src/pool_manager.rs | 12 ++ .../src/prompt_sanitizer.rs | 9 ++ crates/terraphim_multi_agent/src/tracking.rs | 31 ++++- .../src/vm_execution/client.rs | 1 + .../src/vm_execution/fcctl_bridge.rs | 9 ++ .../src/vm_execution/hooks.rs | 108 +++++++++++++++++- .../src/vm_execution/mod.rs | 9 ++ .../src/vm_execution/session_adapter.rs | 17 +++ 25 files changed, 514 insertions(+), 21 deletions(-) diff --git a/crates/terraphim_grep/src/error.rs b/crates/terraphim_grep/src/error.rs index 3480d2b55..5966b4d72 100644 --- a/crates/terraphim_grep/src/error.rs +++ b/crates/terraphim_grep/src/error.rs @@ -1,27 +1,36 @@ use std::time::Duration; +/// Errors that can occur during a terraphim-grep search or synthesis operation. #[derive(Debug, thiserror::Error)] pub enum TerraphimGrepError { + /// The underlying retrieval pipeline returned an error. #[error("search failed: {0}")] SearchFailed(String), + /// An LLM client was required but not wired up. #[error("LLM not configured: {0}")] LlmNotConfigured(String), + /// The search returned too few results to satisfy the query. #[error("insufficient results: {0}")] InsufficientResults(String), + /// Concept extraction or KG persistence failed. #[error("KG curation failed: {0}")] KgCurationFailed(String), + /// The RLM synthesis step failed (e.g. the LLM returned unparseable output). #[error("RLM execution failed: {0}")] RlmFailed(String), + /// A search or synthesis step exceeded its time budget. #[error("timeout after {0:?}")] Timeout(Duration), + /// The provided configuration is invalid. #[error("invalid configuration: {0}")] InvalidConfig(String), } +/// Convenience alias for `Result`. pub type Result = std::result::Result; diff --git a/crates/terraphim_grep/src/hybrid_searcher.rs b/crates/terraphim_grep/src/hybrid_searcher.rs index c6e56dee2..441aec8d2 100644 --- a/crates/terraphim_grep/src/hybrid_searcher.rs +++ b/crates/terraphim_grep/src/hybrid_searcher.rs @@ -4,12 +4,18 @@ use std::sync::Arc; use serde::{Deserialize, Serialize}; use terraphim_types::Document; +/// Options controlling a single grep/search invocation. #[derive(Debug, Clone)] pub struct GrepOptions { + /// Which subset of haystacks to search. pub haystack: Haystack, + /// Number of surrounding lines to include with each match. pub context_lines: usize, + /// Maximum number of results to return across all haystacks. pub max_results: usize, + /// If `true`, bypass the sufficiency judge and always invoke the RLM synthesis step. pub force_rlm: bool, + /// If `true`, include a synthesised answer in the result alongside raw chunks. pub include_answer: bool, } @@ -25,21 +31,32 @@ impl Default for GrepOptions { } } +/// Selects which category of files to include in a search. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Haystack { + /// Search source-code files only (default). #[default] Code, + /// Search documentation files only. Docs, + /// Search both code and documentation files. All, } +/// A single text chunk returned by a search operation. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RetrievedChunk { + /// Full text content of the matched region. pub content: String, + /// File path or URL that the chunk was extracted from. pub source: String, + /// First line of the matched region (1-based), if known. pub line_start: Option, + /// Last line of the matched region (1-based), if known. pub line_end: Option, + /// Relevance score; higher values indicate stronger matches. pub relevance_score: f64, + /// Label identifying which haystack produced this chunk (e.g. `"code"` or `"docs"`). pub haystack: &'static str, } @@ -56,22 +73,32 @@ impl From for RetrievedChunk { } } +/// A concept matched from the knowledge graph during a search. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KgConcept { + /// Numeric identifier for the concept node (0 when not backed by a graph node). pub id: u64, + /// Canonical name of the concept. pub name: String, + /// Optional human-readable label used in place of `name` when boosting. pub display_value: Option, + /// Relevance score assigned by the KG query; used to weight the KG boost. pub score: f64, } +/// Combined output from a hybrid search: code chunks, documentation chunks, and KG concepts. #[derive(Debug, Clone)] pub struct HybridResults { + /// Chunks retrieved from source-code haystacks. pub code_results: Vec, + /// Chunks retrieved from documentation haystacks. pub doc_results: Vec, + /// Knowledge-graph concepts that matched the query. pub kg_concepts: Vec, } impl HybridResults { + /// Merge `code_results` and `doc_results` into a single flat list, in that order. pub fn to_chunks(&self) -> Vec { let mut chunks = Vec::with_capacity(self.code_results.len() + self.doc_results.len()); chunks.extend(self.code_results.clone()); @@ -79,10 +106,12 @@ impl HybridResults { chunks } + /// Returns the total number of items across all three result categories. pub fn total_results(&self) -> usize { self.code_results.len() + self.doc_results.len() + self.kg_concepts.len() } + /// Returns `true` when all three result categories are empty. pub fn is_empty(&self) -> bool { self.code_results.is_empty() && self.doc_results.is_empty() && self.kg_concepts.is_empty() } @@ -150,6 +179,7 @@ pub fn boost_chunks_with_kg( chunks } +/// Orchestrates concurrent KG and code-search queries and fuses their results. pub struct HybridSearcher { role_graph: Arc>, /// Kept alongside the rolegraph so KG-style boosting still works when no documents @@ -161,6 +191,7 @@ pub struct HybridSearcher { } impl HybridSearcher { + /// Create a new `HybridSearcher` for `role_name` backed by the given `thesaurus`. pub fn new( role_name: String, thesaurus: terraphim_types::Thesaurus, @@ -177,11 +208,13 @@ impl HybridSearcher { }) } + /// Override the root directory used for file-system code search. pub fn with_search_path(mut self, path: PathBuf) -> Self { self.search_path = path; self } + /// Execute a hybrid search, running KG and code pipelines concurrently where applicable. pub async fn search( &self, query: &str, @@ -365,6 +398,7 @@ impl HybridSearcher { } } + /// Sort `results` by `relevance_score` in descending order and return them. pub fn fuse_and_rank(&self, mut results: Vec) -> Vec { results.sort_by(|a, b| { b.relevance_score diff --git a/crates/terraphim_grep/src/kg_curation.rs b/crates/terraphim_grep/src/kg_curation.rs index 95e66d156..1b72d2cce 100644 --- a/crates/terraphim_grep/src/kg_curation.rs +++ b/crates/terraphim_grep/src/kg_curation.rs @@ -8,6 +8,7 @@ use terraphim_service::llm::LlmClient; use crate::error::Result; use crate::signatures::NewConcept; +/// Extracts new knowledge-graph concepts from query/answer pairs and persists them as Markdown files. #[cfg(feature = "llm")] pub struct KgCurationRlm { llm_client: Arc, @@ -16,6 +17,7 @@ pub struct KgCurationRlm { #[cfg(feature = "llm")] impl KgCurationRlm { + /// Create a new `KgCurationRlm` backed by the given LLM client. pub fn new(llm_client: Arc) -> Self { Self { llm_client, @@ -23,11 +25,13 @@ impl KgCurationRlm { } } + /// Set the directory where learned concept Markdown files will be written. pub fn with_kg_path(mut self, path: std::path::PathBuf) -> Self { self.kg_path = Some(path); self } + /// Extract new concepts from a `query`/`rlm_answer` pair and persist them to the KG path. pub async fn extract_and_index( &self, query: &str, @@ -124,6 +128,7 @@ impl KgCurationRlm { } } +/// No-op stub for [`KgCurationRlm`] when the `llm` feature is not enabled. #[cfg(not(feature = "llm"))] pub struct KgCurationRlm; @@ -136,10 +141,12 @@ impl Default for KgCurationRlm { #[cfg(not(feature = "llm"))] impl KgCurationRlm { + /// Create a no-op `KgCurationRlm` (LLM feature disabled). pub fn new() -> Self { Self } + /// Always returns [`TerraphimGrepError::LlmNotConfigured`] (LLM feature disabled). pub async fn extract_and_index( &self, _query: &str, diff --git a/crates/terraphim_grep/src/lib.rs b/crates/terraphim_grep/src/lib.rs index 796d7176d..08bbe11ea 100644 --- a/crates/terraphim_grep/src/lib.rs +++ b/crates/terraphim_grep/src/lib.rs @@ -20,11 +20,17 @@ //! # } //! ``` +/// Error types for the terraphim-grep crate. pub mod error; +/// Hybrid KG + code-search orchestration and result types. pub mod hybrid_searcher; +/// LLM-backed knowledge-graph curation from query/answer pairs. pub mod kg_curation; +/// Context builder passed to the RLM synthesis step. pub mod rlm_context; +/// Typed prompt/parser pairs (signatures) for LLM output formats. pub mod signatures; +/// Heuristic judge that decides whether retrieval results are sufficient. pub mod sufficiency_judge; use std::sync::Arc; @@ -39,30 +45,46 @@ pub use rlm_context::RlmContext; pub use signatures::{AnswerWithCitations, Citation, Match, NewConcept, RlmSignature}; pub use sufficiency_judge::{HeuristicThresholds, Sufficiency, SufficiencyJudge}; +/// The top-level result returned by [`TerraphimGrep::search`]. #[derive(Debug, Clone, serde::Serialize)] pub struct GrepResult { + /// Ranked list of text chunks retrieved from the haystacks. pub chunks: Vec, + /// Synthesised answer with source citations, present only when RLM synthesis ran. pub answer: Option, + /// Knowledge-graph concepts that matched the query. pub concepts: Vec, + /// Indicates whether the result was produced by search alone or required RLM synthesis. pub sufficiency: SufficiencyState, + /// Timing and count statistics for this search invocation. pub stats: GrepStats, } +/// Describes how the search result was produced. #[derive(Debug, Clone, serde::Serialize)] pub enum SufficiencyState { + /// Results came from retrieval alone; the RLM synthesis step was not invoked. SearchOnly, + /// The RLM synthesis step ran and produced a synthesised answer. RlmSynthesis, + /// The RLM synthesis step ran but could not produce a sufficiently confident answer. RlmInsufficient, } +/// Timing and result-count statistics for a single search invocation. #[derive(Debug, Clone, serde::Serialize)] pub struct GrepStats { + /// Wall-clock time in milliseconds spent on the retrieval phase. pub search_latency_ms: u64, + /// Wall-clock time in milliseconds spent in the RLM synthesis step, if it ran. pub rlm_latency_ms: Option, + /// Number of chunks included in the final result. pub chunks_returned: usize, + /// Number of KG concept matches that contributed to ranking. pub kg_hits: usize, } +/// Top-level search engine that combines hybrid retrieval with optional RLM synthesis. pub struct TerraphimGrep { hybrid_searcher: Arc, sufficiency_judge: Arc, @@ -73,6 +95,7 @@ pub struct TerraphimGrep { } impl TerraphimGrep { + /// Create a new `TerraphimGrep` with the given searcher and sufficiency judge. #[cfg(feature = "llm")] pub fn new( hybrid_searcher: Arc, @@ -86,6 +109,7 @@ impl TerraphimGrep { } } + /// Create a new `TerraphimGrep` with the given searcher and sufficiency judge. #[cfg(not(feature = "llm"))] pub fn new( hybrid_searcher: Arc, @@ -97,12 +121,14 @@ impl TerraphimGrep { } } + /// Attach a KG-curation pipeline that extracts and persists new concepts from RLM answers. #[cfg(feature = "llm")] pub fn with_kg_curation(mut self, kg_curation: Arc) -> Self { self.kg_curation = Some(kg_curation); self } + /// Attach an LLM client used for RLM synthesis when retrieval is insufficient. #[cfg(feature = "llm")] pub fn with_llm_client( mut self, @@ -112,6 +138,7 @@ impl TerraphimGrep { self } + /// Search for `query` using the configured hybrid retrieval and optional RLM synthesis. pub async fn search(&self, query: &str, options: GrepOptions) -> Result { let start = std::time::Instant::now(); @@ -315,6 +342,7 @@ impl TerraphimGrep { .await } + /// Return a zeroed-out [`GrepStats`] snapshot (placeholder for future instrumentation). pub fn stats(&self) -> GrepStats { GrepStats { search_latency_ms: 0, diff --git a/crates/terraphim_grep/src/rlm_context.rs b/crates/terraphim_grep/src/rlm_context.rs index 401ffa0c7..88d249242 100644 --- a/crates/terraphim_grep/src/rlm_context.rs +++ b/crates/terraphim_grep/src/rlm_context.rs @@ -2,21 +2,30 @@ use std::collections::HashMap; use super::hybrid_searcher::{KgConcept, RetrievedChunk}; +/// Context assembled from retrieval results and passed to the RLM synthesis step. #[derive(Debug, Clone)] pub struct RlmContext { + /// The original user query. pub query: String, + /// Chunks retrieved from haystacks to use as evidence. pub retrieved_chunks: Vec, + /// Knowledge-graph concepts that matched the query. pub kg_concepts: Vec, + /// Per-source metadata keyed by the chunk's `source` field. pub source_metadata: HashMap, } +/// Lightweight metadata describing a document source. #[derive(Debug, Clone)] pub struct DocumentMetadata { + /// Haystack category the document came from (e.g. `"code"` or `"docs"`). pub source_type: String, + /// ISO-8601 last-modified timestamp, if available. pub last_modified: Option, } impl RlmContext { + /// Create an empty context for `query` with no chunks or concepts yet. pub fn new(query: String) -> Self { Self { query, @@ -26,6 +35,7 @@ impl RlmContext { } } + /// Attach retrieved chunks and populate `source_metadata` from their haystack labels. pub fn with_chunks(mut self, chunks: Vec) -> Self { self.retrieved_chunks = chunks; for chunk in &self.retrieved_chunks { @@ -40,11 +50,13 @@ impl RlmContext { self } + /// Attach knowledge-graph concepts matched for this query. pub fn with_concepts(mut self, concepts: Vec) -> Self { self.kg_concepts = concepts; self } + /// Render a textual prompt containing the query, retrieved context, and KG concepts. pub fn build_prompt(&self) -> String { let mut prompt = format!("Query: {}\n\n", self.query); @@ -75,10 +87,12 @@ impl RlmContext { prompt } + /// Return the character length of the rendered prompt. pub fn context_length(&self) -> usize { self.build_prompt().len() } + /// Drop trailing chunks until the rendered prompt fits within `max_chars`. pub fn truncate(&mut self, max_chars: usize) { if self.context_length() > max_chars { let mut remaining = max_chars; diff --git a/crates/terraphim_grep/src/signatures.rs b/crates/terraphim_grep/src/signatures.rs index 9126b4dfa..06934ed27 100644 --- a/crates/terraphim_grep/src/signatures.rs +++ b/crates/terraphim_grep/src/signatures.rs @@ -2,23 +2,33 @@ use serde::{Deserialize, Serialize}; use crate::error::TerraphimGrepError; +/// A typed prompt-and-parser pair for a single RLM output format. pub trait RlmSignature: Send + Sync { + /// The Rust type this signature deserialises the LLM response into. type Output: serde::Serialize + serde::de::DeserializeOwned; + /// Returns the natural-language instructions to include in the LLM prompt. fn instructions(&self) -> String; + /// Parse the raw LLM response string into [`Self::Output`]. fn parse(&self, raw: &str) -> Result; } +/// A single file-level match returned by the search-result RLM signature. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Match { + /// Relative path of the matching file. pub path: String, + /// Line number of the match (1-based). pub line: usize, + /// Inclusive end line of the match, if the match spans multiple lines. #[serde(skip_serializing_if = "Option::is_none", default)] pub line_end: Option, + /// Surrounding lines included as context for the match. #[serde(skip_serializing_if = "Vec::is_empty", default)] pub context: Vec, } +/// RLM signature that parses a JSON array of [`Match`] objects from the LLM response. pub struct SearchResultSignature; impl RlmSignature for SearchResultSignature { @@ -35,21 +45,30 @@ impl RlmSignature for SearchResultSignature { } } +/// A source citation accompanying a synthesised answer. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Citation { + /// File path or URL of the cited source. pub source: String, + /// Line number within the source, if applicable. #[serde(skip_serializing_if = "Option::is_none")] pub line: Option, + /// Short excerpt from the cited source supporting the answer. pub excerpt: String, } +/// A synthesised answer produced by the RLM, paired with its source citations. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AnswerWithCitations { + /// The synthesised natural-language answer. pub answer: String, + /// Sources cited in support of the answer. pub citations: Vec, + /// Confidence score in the range `[0.0, 1.0]` reported by the LLM. pub confidence: f64, } +/// RLM signature that parses an [`AnswerWithCitations`] object from the LLM response. pub struct AnswerSignature; impl RlmSignature for AnswerSignature { @@ -69,15 +88,20 @@ impl RlmSignature for AnswerSignature { } } +/// A newly discovered knowledge-graph concept extracted from a query/answer pair. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NewConcept { + /// Canonical name of the concept (e.g. `"retry configuration"`). pub name: String, + /// Alternative names or synonyms for the concept. #[serde(default)] pub synonyms: Vec, + /// Names of related concepts in the knowledge graph. #[serde(default)] pub relationships: Vec, } +/// RLM signature that parses a JSON array of [`NewConcept`] objects from the LLM response. pub struct ConceptExtractionSignature; impl RlmSignature for ConceptExtractionSignature { diff --git a/crates/terraphim_grep/src/sufficiency_judge.rs b/crates/terraphim_grep/src/sufficiency_judge.rs index ea95d869f..798296a70 100644 --- a/crates/terraphim_grep/src/sufficiency_judge.rs +++ b/crates/terraphim_grep/src/sufficiency_judge.rs @@ -1,10 +1,15 @@ use super::hybrid_searcher::{HybridResults, RetrievedChunk}; +/// Configurable thresholds used by [`SufficiencyJudge`] to classify search quality. #[derive(Debug, Clone)] pub struct HeuristicThresholds { + /// Minimum fraction of query terms that must appear in the retrieved chunks. pub min_coverage: f64, + /// Minimum average KG concept score required to consider results confident. pub min_kg_confidence: f64, + /// Minimum number of distinct haystacks that must contribute chunks. pub min_diversity: usize, + /// Minimum total number of chunks required before any positive verdict. pub min_results: usize, } @@ -19,23 +24,31 @@ impl Default for HeuristicThresholds { } } +/// The verdict returned by [`SufficiencyJudge::judge`], along with the ranked chunks. #[derive(Debug, Clone)] pub enum Sufficiency { + /// Retrieval results are good enough to return directly without synthesis. Sufficient(Vec), + /// Results exist but need LLM synthesis to produce a coherent answer. NeedsSynthesis(Vec), + /// Results are sparse; additional expansion or synthesis is required. NeedsExpansion(Vec), + /// Results are too poor to be useful; return empty or a failure message. Insufficient(Vec), } +/// Evaluates whether a set of hybrid search results is sufficient or requires further synthesis. pub struct SufficiencyJudge { thresholds: HeuristicThresholds, } impl SufficiencyJudge { + /// Create a new judge with the given heuristic thresholds. pub fn new(thresholds: HeuristicThresholds) -> Self { Self { thresholds } } + /// Classify `results` for `query` and return the appropriate [`Sufficiency`] variant. pub fn judge(&self, results: &HybridResults, query: &str) -> Sufficiency { let chunks = results.to_chunks(); diff --git a/crates/terraphim_multi_agent/src/agent.rs b/crates/terraphim_multi_agent/src/agent.rs index 14b3fccd9..730dc8750 100644 --- a/crates/terraphim_multi_agent/src/agent.rs +++ b/crates/terraphim_multi_agent/src/agent.rs @@ -31,6 +31,7 @@ pub struct AgentGoals { } impl AgentGoals { + /// Create a new `AgentGoals` with a neutral alignment score pub fn new(global_goal: String, individual_goals: Vec) -> Self { Self { global_goal, @@ -40,11 +41,13 @@ impl AgentGoals { } } + /// Update the alignment score, clamping to `[0.0, 1.0]` pub fn update_alignment_score(&mut self, score: f64) { self.alignment_score = score.clamp(0.0, 1.0); self.last_updated = Utc::now(); } + /// Append an individual goal to this agent's goal list pub fn add_individual_goal(&mut self, goal: String) { self.individual_goals.push(goal); self.last_updated = Utc::now(); @@ -112,46 +115,55 @@ impl Default for AgentConfig { /// Core Terraphim Agent that wraps a Role configuration with Rig integration #[derive(Debug)] pub struct TerraphimAgent { - // Core identity + /// Unique identifier for this agent instance pub agent_id: AgentId, + /// Role configuration that defines the agent's domain and capabilities pub role_config: Role, + /// Operational configuration (token limits, timeouts, etc.) pub config: AgentConfig, + /// Current lifecycle status of the agent pub status: Arc>, - // Knowledge graph context + /// Knowledge graph used for semantic context enrichment pub rolegraph: Arc, + /// Autocomplete index built from the role's thesaurus pub automata: Arc, - // Individual evolution tracking + /// Versioned memory store for this agent's learned state pub memory: Arc>, + /// Versioned task list tracking in-progress and completed tasks pub tasks: Arc>, + /// Versioned lessons accumulated from past interactions pub lessons: Arc>, - // Goals and alignment + /// Goal configuration for this agent pub goals: AgentGoals, - // Context and history + /// Sliding-window conversation context fed to the LLM pub context: Arc>, + /// Persistent record of all commands processed by this agent pub command_history: Arc>, - // Tracking + /// Tracks token consumption per request pub token_tracker: Arc>, + /// Tracks monetary cost and budget limits pub cost_tracker: Arc>, - // Persistence + /// Backend storage used to persist agent state pub persistence: Arc, - // LLM Client + /// LLM client used for text generation pub llm_client: Arc, - // VM Execution Client (optional) + /// Optional VM execution client for sandboxed code execution pub vm_execution_client: Option>, - // Hook Manager for pre/post LLM validation + /// Hook manager that runs pre- and post-LLM validation hooks pub hook_manager: Arc, - // Metadata + /// UTC timestamp when this agent was created pub created_at: DateTime, + /// UTC timestamp of the most recent activity pub last_active: Arc>>, } @@ -313,6 +325,7 @@ impl TerraphimAgent { Ok(()) } + /// Flush all buffered token usage records to the persistent usage store pub async fn flush_usage(&self) { let records = { let mut tracker = self.token_tracker.write().await; diff --git a/crates/terraphim_multi_agent/src/agents/chat_agent.rs b/crates/terraphim_multi_agent/src/agents/chat_agent.rs index 7a7e03f64..100c9db21 100644 --- a/crates/terraphim_multi_agent/src/agents/chat_agent.rs +++ b/crates/terraphim_multi_agent/src/agents/chat_agent.rs @@ -41,22 +41,32 @@ impl Default for ChatConfig { /// A single message in the chat conversation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChatMessage { + /// Unique identifier for this message pub id: Uuid, + /// Text content of the message pub content: String, + /// Role that authored this message pub role: ChatMessageRole, + /// UTC timestamp when the message was created pub timestamp: DateTime, + /// Arbitrary key-value metadata attached to this message pub metadata: std::collections::HashMap, } /// Role types for chat messages #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ChatMessageRole { + /// System-level instruction message System, + /// Message from the human user User, + /// Message generated by the AI assistant Assistant, } impl ChatMessage { + /// Create a new user message with the given content + /// Create a new user message with the given content pub fn user(content: String) -> Self { Self { id: Uuid::new_v4(), @@ -67,6 +77,7 @@ impl ChatMessage { } } + /// Create a new assistant message with the given content pub fn assistant(content: String) -> Self { Self { id: Uuid::new_v4(), @@ -77,6 +88,7 @@ impl ChatMessage { } } + /// Create a new system message with the given content pub fn system(content: String) -> Self { Self { id: Uuid::new_v4(), @@ -91,10 +103,15 @@ impl ChatMessage { /// Conversation session with context management #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChatSession { + /// Unique session identifier pub id: Uuid, + /// Ordered queue of all messages in this session pub messages: VecDeque, + /// UTC timestamp when the session was created pub created_at: DateTime, + /// UTC timestamp of the most recent message added pub updated_at: DateTime, + /// Optional human-readable title for the session pub title: Option, } @@ -105,6 +122,8 @@ impl Default for ChatSession { } impl ChatSession { + /// Create a new empty chat session + /// Create a new empty chat session pub fn new() -> Self { Self { id: Uuid::new_v4(), @@ -115,11 +134,13 @@ impl ChatSession { } } + /// Append a message to the session and update the `updated_at` timestamp pub fn add_message(&mut self, message: ChatMessage) { self.messages.push_back(message); self.updated_at = Utc::now(); } + /// Return up to `count` most-recent messages in chronological order pub fn get_recent_messages(&self, count: usize) -> Vec<&ChatMessage> { self.messages.iter().rev().take(count).rev().collect() } diff --git a/crates/terraphim_multi_agent/src/agents/summarization_agent.rs b/crates/terraphim_multi_agent/src/agents/summarization_agent.rs index 3dc29a19e..9759a44f2 100644 --- a/crates/terraphim_multi_agent/src/agents/summarization_agent.rs +++ b/crates/terraphim_multi_agent/src/agents/summarization_agent.rs @@ -32,11 +32,16 @@ impl Default for SummarizationConfig { } } +/// Style variants for generated summaries #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SummaryStyle { + /// Short, high-level overview Brief, + /// Comprehensive coverage of all major points Detailed, + /// Formatted as a bulleted list BulletPoints, + /// Business-oriented summary for stakeholders Executive, } diff --git a/crates/terraphim_multi_agent/src/context.rs b/crates/terraphim_multi_agent/src/context.rs index eecb7d853..8c9a079bf 100644 --- a/crates/terraphim_multi_agent/src/context.rs +++ b/crates/terraphim_multi_agent/src/context.rs @@ -27,6 +27,7 @@ pub struct ContextItem { } impl ContextItem { + /// Create a new context item with the given type, content, token count, and relevance score. pub fn new( item_type: ContextItemType, content: String, @@ -44,6 +45,7 @@ impl ContextItem { } } + /// Attach custom metadata to this context item and return the modified item. pub fn with_metadata(mut self, metadata: ContextMetadata) -> Self { self.metadata = metadata; self @@ -110,6 +112,7 @@ pub struct AgentContext { } impl AgentContext { + /// Create a new empty context for the given agent with the specified token and item limits. pub fn new(agent_id: AgentId, max_tokens: u64, max_items: usize) -> Self { Self { agent_id, @@ -410,6 +413,7 @@ pub struct ContextSnapshot { } impl ContextSnapshot { + /// Create a snapshot from the current state of an `AgentContext`, recording the given trigger. pub fn from_context(context: &AgentContext, trigger: SnapshotTrigger) -> Self { Self { id: Uuid::new_v4(), diff --git a/crates/terraphim_multi_agent/src/error.rs b/crates/terraphim_multi_agent/src/error.rs index 93448e763..b7861f50c 100644 --- a/crates/terraphim_multi_agent/src/error.rs +++ b/crates/terraphim_multi_agent/src/error.rs @@ -59,15 +59,30 @@ pub enum MultiAgentError { /// Token limit exceeded #[error("Token limit exceeded: {current}/{limit}")] - TokenLimitExceeded { current: u64, limit: u64 }, + TokenLimitExceeded { + /// Actual token count that triggered the limit + current: u64, + /// Maximum allowed token count + limit: u64, + }, /// Budget limit exceeded #[error("Budget limit exceeded: ${current:.2}/${limit:.2}")] - BudgetLimitExceeded { current: f64, limit: f64 }, + BudgetLimitExceeded { + /// Current spend in USD + current: f64, + /// Maximum budget in USD + limit: f64, + }, /// Rate limit exceeded #[error("Rate limit exceeded: {requests} requests in {window_seconds}s")] - RateLimitExceeded { requests: u64, window_seconds: u64 }, + RateLimitExceeded { + /// Number of requests made in the window + requests: u64, + /// Length of the rate-limit window in seconds + window_seconds: u64, + }, /// Configuration error #[error("Configuration error: {0}")] @@ -91,7 +106,10 @@ pub enum MultiAgentError { /// Timeout error #[error("Operation timed out after {seconds}s")] - Timeout { seconds: u64 }, + Timeout { + /// Number of seconds after which the operation was cancelled + seconds: u64, + }, /// Session not found #[error("Session with ID {0} not found")] diff --git a/crates/terraphim_multi_agent/src/genai_llm_client.rs b/crates/terraphim_multi_agent/src/genai_llm_client.rs index bc8b13c9d..7c24da1de 100644 --- a/crates/terraphim_multi_agent/src/genai_llm_client.rs +++ b/crates/terraphim_multi_agent/src/genai_llm_client.rs @@ -23,28 +23,33 @@ pub struct GenAiLlmClient { /// Configuration for different LLM providers #[derive(Debug, Clone)] pub struct ProviderConfig { + /// Model name to use for this provider pub model: String, } impl ProviderConfig { + /// Create an Ollama provider configuration, defaulting to `gemma3:270m`. pub fn ollama(model: Option) -> Self { Self { model: model.unwrap_or_else(|| "gemma3:270m".to_string()), } } + /// Create an OpenAI provider configuration, defaulting to `gpt-3.5-turbo`. pub fn openai(model: Option) -> Self { Self { model: model.unwrap_or_else(|| "gpt-3.5-turbo".to_string()), } } + /// Create an Anthropic provider configuration, defaulting to `claude-3-sonnet-20240229`. pub fn anthropic(model: Option) -> Self { Self { model: model.unwrap_or_else(|| "claude-3-sonnet-20240229".to_string()), } } + /// Create an OpenRouter provider configuration, defaulting to `anthropic/claude-3.5-sonnet`. pub fn openrouter(model: Option) -> Self { Self { model: model.unwrap_or_else(|| "anthropic/claude-3.5-sonnet".to_string()), diff --git a/crates/terraphim_multi_agent/src/history.rs b/crates/terraphim_multi_agent/src/history.rs index e9093bb4a..53a54fdf6 100644 --- a/crates/terraphim_multi_agent/src/history.rs +++ b/crates/terraphim_multi_agent/src/history.rs @@ -21,6 +21,7 @@ pub struct CommandRecord { /// Input and context pub input: CommandInput, + /// Snapshot of the agent context at the time the command was issued. pub context_snapshot: HistoryContextSnapshot, /// Execution details @@ -31,11 +32,14 @@ pub struct CommandRecord { /// Resource usage pub token_usage: Option, + /// Monetary cost of this command in USD, if known pub cost_usd: Option, /// Quality and learning pub quality_score: Option, + /// Lessons captured from this command's execution pub lessons_learned: Vec, + /// Memory entries updated as a result of this command pub memory_updates: Vec, /// Error information if command failed @@ -43,6 +47,7 @@ pub struct CommandRecord { } impl CommandRecord { + /// Creates a new `CommandRecord` for the given agent and input, with all other fields at their defaults. pub fn new(agent_id: AgentId, input: CommandInput) -> Self { Self { command_id: Uuid::new_v4(), @@ -145,6 +150,7 @@ pub struct CommandInput { } impl CommandInput { + /// Creates a new `CommandInput` with default source (`User`), normal priority, and no timeout. pub fn new(text: String, command_type: CommandType) -> Self { Self { text, @@ -156,21 +162,25 @@ impl CommandInput { } } + /// Sets the parameters map for this input. pub fn with_parameters(mut self, parameters: HashMap) -> Self { self.parameters = parameters; self } + /// Sets the source of this command. pub fn with_source(mut self, source: CommandSource) -> Self { self.source = source; self } + /// Sets the priority level for this command. pub fn with_priority(mut self, priority: CommandPriority) -> Self { self.priority = priority; self } + /// Sets an execution timeout in milliseconds. pub fn with_timeout(mut self, timeout_ms: u64) -> Self { self.timeout_ms = Some(timeout_ms); self @@ -222,10 +232,15 @@ pub enum CommandSource { /// Command priority levels #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum CommandPriority { + /// Lowest priority; processed when the queue is quiet Low, + /// Standard priority for routine commands Normal, + /// Elevated priority; processed before `Normal` commands High, + /// Near-real-time priority for time-sensitive operations Critical, + /// Highest priority; bypasses all ordinary queuing Emergency, } @@ -268,6 +283,7 @@ pub struct ExecutionStep { } impl ExecutionStep { + /// Creates a new `ExecutionStep` with `Running` status and the current timestamp. pub fn new(name: String) -> Self { Self { step_id: Uuid::new_v4(), @@ -281,6 +297,7 @@ impl ExecutionStep { } } + /// Marks this step as completed, recording its output and wall-clock duration. pub fn complete(mut self, output: String, duration_ms: u64) -> Self { self.output = Some(output); self.duration_ms = duration_ms; @@ -288,6 +305,7 @@ impl ExecutionStep { self } + /// Marks this step as failed, recording the error message and wall-clock duration. pub fn fail(mut self, error: String, duration_ms: u64) -> Self { self.error = Some(error); self.duration_ms = duration_ms; @@ -299,10 +317,15 @@ impl ExecutionStep { /// Status of an execution step #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum StepStatus { + /// Step has not yet started Pending, + /// Step is currently executing Running, + /// Step finished successfully Completed, + /// Step encountered an error and did not finish Failed, + /// Step was intentionally bypassed Skipped, } @@ -324,6 +347,7 @@ pub struct CommandOutput { } impl CommandOutput { + /// Creates a plain-text `CommandOutput` with no structured data, confidence, or sources. pub fn new(text: String) -> Self { Self { text, @@ -335,17 +359,20 @@ impl CommandOutput { } } + /// Attaches structured JSON data to this output and sets the output type to `Structured`. pub fn with_data(mut self, data: serde_json::Value) -> Self { self.data = Some(data); self.output_type = OutputType::Structured; self } + /// Sets the confidence score, clamped to the range `[0.0, 1.0]`. pub fn with_confidence(mut self, confidence: f64) -> Self { self.confidence = Some(confidence.clamp(0.0, 1.0)); self } + /// Attaches the list of source references used to produce this output. pub fn with_sources(mut self, sources: Vec) -> Self { self.sources = sources; self @@ -355,10 +382,14 @@ impl CommandOutput { /// Types of command output #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub enum OutputType { + /// Plain text output (default) #[default] Text, + /// Structured JSON or other structured data Structured, + /// Raw binary data Binary, + /// Streaming output delivered incrementally Stream, } @@ -380,6 +411,7 @@ pub struct CommandError { } impl CommandError { + /// Creates a non-recoverable `CommandError` with no code, details, or suggestions. pub fn new(error_type: ErrorType, message: String) -> Self { Self { error_type, @@ -391,21 +423,25 @@ impl CommandError { } } + /// Attaches a short error code (e.g. an HTTP status or application error code). pub fn with_code(mut self, code: String) -> Self { self.code = Some(code); self } + /// Attaches additional details such as a stack trace or diagnostic information. pub fn with_details(mut self, details: String) -> Self { self.details = Some(details); self } + /// Marks this error as recoverable, indicating the caller may retry. pub fn recoverable(mut self) -> Self { self.recoverable = true; self } + /// Sets the list of suggested remediation actions for the caller. pub fn with_suggestions(mut self, suggestions: Vec) -> Self { self.suggestions = suggestions; self @@ -455,6 +491,7 @@ pub struct HistoryContextSnapshot { } impl HistoryContextSnapshot { + /// Creates an empty snapshot for the given agent (no items, zero tokens). pub fn empty(agent_id: AgentId) -> Self { Self { agent_id, @@ -465,6 +502,7 @@ impl HistoryContextSnapshot { } } + /// Builds a snapshot from a live [`AgentContext`], capturing counts and the top items. pub fn from_context(context: &AgentContext) -> Self { let key_items = context .get_relevant_items(1000) // Top 1000 tokens worth @@ -514,6 +552,7 @@ pub struct CommandHistory { } impl CommandHistory { + /// Creates a new `CommandHistory` for the given agent, keeping at most `max_records` entries. pub fn new(agent_id: AgentId, max_records: usize) -> Self { Self { agent_id, @@ -645,16 +684,27 @@ impl CommandHistory { /// Statistics about command history #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CommandStatistics { + /// Total number of commands recorded. pub total_commands: u64, + /// Number of commands that completed without error. pub successful_commands: u64, + /// Number of commands that resulted in an error. pub failed_commands: u64, + /// Fraction of commands that succeeded (`successful_commands / total_commands`). pub success_rate: f64, + /// Mean quality score across all commands that have a score. pub average_quality_score: f64, + /// Cumulative execution time across all commands, in milliseconds. pub total_duration_ms: u64, + /// Mean execution time per command, in milliseconds. pub average_duration_ms: u64, + /// Total cost in US dollars across all commands with recorded cost. pub total_cost_usd: f64, + /// Mean cost per command in US dollars. pub average_cost_per_command: f64, + /// Total tokens consumed across all commands with recorded token usage. pub total_tokens: u64, + /// Mean token consumption per command. pub average_tokens_per_command: f64, } diff --git a/crates/terraphim_multi_agent/src/lib.rs b/crates/terraphim_multi_agent/src/lib.rs index 616f054d0..416e4b145 100644 --- a/crates/terraphim_multi_agent/src/lib.rs +++ b/crates/terraphim_multi_agent/src/lib.rs @@ -27,20 +27,33 @@ //! } //! ``` +/// Core agent implementation and lifecycle management. pub mod agent; +/// Specialised agent implementations (chat, summarization). pub mod agents; +/// Context window management for agents. pub mod context; +/// Error types for the multi-agent system. pub mod error; +/// Rust-GenAI backed LLM client implementation. pub mod genai_llm_client; +/// Command and interaction history tracking. pub mod history; +/// Shared LLM request/response and message types. pub mod llm_types; +/// System prompt sanitisation and validation utilities. pub mod prompt_sanitizer; +/// VM execution subsystem for sandboxed code execution. pub mod vm_execution; // pub mod llm_client; // Disabled - uses rig-core // pub mod simple_llm_client; // Disabled - uses rig-core +/// Agent pooling for efficient agent reuse. pub mod pool; +/// Centralised manager for multiple agent pools. pub mod pool_manager; +/// Token and cost tracking for agent operations. pub mod tracking; +/// Multi-agent workflow patterns and orchestration. pub mod workflows; // Re-export KG-backed registry from terraphim_agent_registry (consolidated implementation) @@ -72,7 +85,7 @@ pub type MultiAgentResult = Result; /// Agent identifier type pub type AgentId = uuid::Uuid; -// Test utilities using real Ollama with gemma3:270m model +/// Test utilities using real Ollama with gemma3:270m model. #[cfg(any(test, feature = "test-utils"))] pub mod test_utils { use super::*; @@ -80,6 +93,7 @@ pub mod test_utils { use terraphim_config::Role; use terraphim_persistence::DeviceStorage; + /// Create a minimal `Role` configured to use a local Ollama instance for testing. pub fn create_test_role() -> Role { let mut role = Role::new("TestAgent"); role.shortname = Some("test".to_string()); @@ -96,6 +110,7 @@ pub mod test_utils { role } + /// Create a `TerraphimAgent` backed by in-memory storage, suitable for fast unit tests. pub async fn create_test_agent_simple() -> Result { use terraphim_persistence::memory::create_memory_only_device_settings; @@ -110,7 +125,7 @@ pub mod test_utils { TerraphimAgent::new(role, persistence, None).await } - // For now, alias the simpler version for tests + /// Alias for [`create_test_agent_simple`] — creates a test agent backed by in-memory storage. pub async fn create_test_agent() -> Result { create_test_agent_simple().await } diff --git a/crates/terraphim_multi_agent/src/llm_types.rs b/crates/terraphim_multi_agent/src/llm_types.rs index 722881b02..6974ace2d 100644 --- a/crates/terraphim_multi_agent/src/llm_types.rs +++ b/crates/terraphim_multi_agent/src/llm_types.rs @@ -10,20 +10,27 @@ use uuid::Uuid; /// Message roles for LLM communication #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum MessageRole { + /// System-level instruction message System, + /// Message originating from the human user User, + /// Message generated by the AI assistant Assistant, + /// Message produced by a tool invocation Tool, } /// An individual message in a conversation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LlmMessage { + /// Role of the message sender pub role: MessageRole, + /// Text content of the message pub content: String, } impl LlmMessage { + /// Create a system-role message pub fn system(content: String) -> Self { Self { role: MessageRole::System, @@ -31,6 +38,7 @@ impl LlmMessage { } } + /// Create a user-role message pub fn user(content: String) -> Self { Self { role: MessageRole::User, @@ -38,6 +46,7 @@ impl LlmMessage { } } + /// Create an assistant-role message pub fn assistant(content: String) -> Self { Self { role: MessageRole::Assistant, @@ -45,6 +54,7 @@ impl LlmMessage { } } + /// Create a tool-role message pub fn tool(content: String) -> Self { Self { role: MessageRole::Tool, @@ -56,13 +66,18 @@ impl LlmMessage { /// Request to an LLM #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LlmRequest { + /// Ordered list of messages forming the conversation pub messages: Vec, + /// Sampling temperature; higher values produce more varied output pub temperature: Option, + /// Maximum number of tokens the model may generate pub max_tokens: Option, + /// Arbitrary key-value metadata attached to the request pub metadata: std::collections::HashMap, } impl LlmRequest { + /// Create a new request with the given messages and default settings pub fn new(messages: Vec) -> Self { Self { messages, @@ -72,16 +87,19 @@ impl LlmRequest { } } + /// Set the sampling temperature on this request pub fn with_temperature(mut self, temperature: f32) -> Self { self.temperature = Some(temperature); self } + /// Set the maximum token limit on this request pub fn with_max_tokens(mut self, max_tokens: u64) -> Self { self.max_tokens = Some(max_tokens); self } + /// Attach a metadata key-value pair to this request pub fn with_metadata(mut self, key: String, value: String) -> Self { self.metadata.insert(key, value); self @@ -91,12 +109,16 @@ impl LlmRequest { /// Token usage statistics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenUsage { + /// Number of tokens in the prompt / input pub input_tokens: u64, + /// Number of tokens generated in the response pub output_tokens: u64, + /// Combined total of input and output tokens pub total_tokens: u64, } impl TokenUsage { + /// Create a new `TokenUsage`, computing `total_tokens` automatically pub fn new(input_tokens: u64, output_tokens: u64) -> Self { Self { input_tokens, @@ -105,6 +127,7 @@ impl TokenUsage { } } + /// Return a `TokenUsage` with all counts set to zero pub fn zero() -> Self { Self { input_tokens: 0, @@ -117,16 +140,24 @@ impl TokenUsage { /// Response from an LLM #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LlmResponse { + /// Generated text content from the model pub content: String, + /// Identifier of the model that produced the response pub model: String, + /// Token usage statistics for this response pub usage: TokenUsage, + /// Unique identifier for the originating request pub request_id: Uuid, + /// UTC timestamp when the response was received pub timestamp: DateTime, + /// Round-trip duration in milliseconds pub duration_ms: u64, + /// Reason the model stopped generating (e.g. "stop", "length") pub finish_reason: String, } impl LlmResponse { + /// Create a minimal `LlmResponse` with the given content and sensible defaults pub fn new(content: String) -> Self { Self { content, diff --git a/crates/terraphim_multi_agent/src/pool.rs b/crates/terraphim_multi_agent/src/pool.rs index 9821b8ba4..e01664600 100644 --- a/crates/terraphim_multi_agent/src/pool.rs +++ b/crates/terraphim_multi_agent/src/pool.rs @@ -18,14 +18,20 @@ use crate::{ /// Load metrics for an agent in the pool #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoadMetrics { + /// Number of commands currently being processed pub active_commands: u32, + /// Number of commands waiting to be picked up pub queue_length: u32, + /// Exponential moving average of response time in milliseconds pub average_response_time_ms: f64, + /// Rolling success rate between 0.0 and 1.0 pub success_rate: f64, + /// UTC timestamp when these metrics were last refreshed pub last_updated: DateTime, } impl LoadMetrics { + /// Create a new `LoadMetrics` with zero counters and a 100% success rate. pub fn new() -> Self { Self { active_commands: 0, @@ -178,13 +184,21 @@ pub struct AgentPool { /// Pool statistics for monitoring #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PoolStats { + /// Cumulative number of agents ever created by this pool pub total_agents_created: u64, + /// Cumulative number of agents removed and destroyed by this pool pub total_agents_destroyed: u64, + /// Cumulative number of operations processed across all agents pub total_operations_processed: u64, + /// Current number of agents in the available queue pub current_pool_size: usize, + /// Current number of agents with active operations pub current_busy_agents: usize, + /// Exponential moving average of operation duration in milliseconds pub average_operation_time_ms: f64, + /// Ratio of pool-cache hits (agent reused) to total acquisitions pub pool_hit_rate: f64, + /// UTC timestamp when these stats were last refreshed pub last_updated: DateTime, } diff --git a/crates/terraphim_multi_agent/src/pool_manager.rs b/crates/terraphim_multi_agent/src/pool_manager.rs index 762473c75..22a29b671 100644 --- a/crates/terraphim_multi_agent/src/pool_manager.rs +++ b/crates/terraphim_multi_agent/src/pool_manager.rs @@ -44,10 +44,15 @@ impl Default for PoolManagerConfig { /// Pool information for management #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PoolInfo { + /// Name of the role this pool serves pub role_name: String, + /// UTC timestamp when the pool was created pub created_at: DateTime, + /// UTC timestamp of the most recent pool access pub last_used: DateTime, + /// Current snapshot of pool statistics pub stats: PoolStats, + /// Whether the pool is still running and accepting requests pub is_active: bool, } @@ -70,12 +75,19 @@ pub struct PoolManager { /// Global statistics across all pools #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GlobalStats { + /// Number of active pools currently managed pub total_pools: usize, + /// Total number of agents across all pools pub total_agents: usize, + /// Cumulative number of operations processed across all pools pub total_operations: u64, + /// Exponential moving average of operation duration in milliseconds across all pools pub average_operation_time_ms: f64, + /// Total number of times an existing pool was reused for a role pub total_pool_hits: u64, + /// Total number of times a new pool was created for a role pub total_pool_misses: u64, + /// UTC timestamp when these statistics were last refreshed pub last_updated: DateTime, } diff --git a/crates/terraphim_multi_agent/src/prompt_sanitizer.rs b/crates/terraphim_multi_agent/src/prompt_sanitizer.rs index f899252fc..551065d6f 100644 --- a/crates/terraphim_multi_agent/src/prompt_sanitizer.rs +++ b/crates/terraphim_multi_agent/src/prompt_sanitizer.rs @@ -53,13 +53,19 @@ static UNICODE_SPECIAL_CHARS: LazyLock> = LazyLock::new(|| { .collect() }); +/// The result of sanitising a system prompt, including the cleaned text and any warnings raised. #[derive(Debug, Clone)] pub struct SanitizedPrompt { + /// The cleaned prompt text after all sanitisation passes pub content: String, + /// `true` if the original prompt was altered in any way pub was_modified: bool, + /// Human-readable warnings describing what was removed or truncated pub warnings: Vec, } +/// Sanitise a system prompt by removing injection patterns, control characters, and obfuscation +/// sequences. Returns the cleaned prompt together with a modification flag and any warnings. pub fn sanitize_system_prompt(prompt: &str) -> SanitizedPrompt { let mut warnings = Vec::new(); let mut was_modified = false; @@ -132,6 +138,9 @@ pub fn sanitize_system_prompt(prompt: &str) -> SanitizedPrompt { } } +/// Validate a system prompt without modifying it. +/// +/// Returns `Ok(())` if the prompt is acceptable, or an `Err` describing the problem. pub fn validate_system_prompt(prompt: &str) -> Result<(), String> { if prompt.is_empty() { return Err("System prompt cannot be empty".to_string()); diff --git a/crates/terraphim_multi_agent/src/tracking.rs b/crates/terraphim_multi_agent/src/tracking.rs index 43589da1b..042660052 100644 --- a/crates/terraphim_multi_agent/src/tracking.rs +++ b/crates/terraphim_multi_agent/src/tracking.rs @@ -10,10 +10,15 @@ use crate::{AgentId, MultiAgentError, MultiAgentResult}; /// Cost record for tracking agent expenses #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CostRecord { + /// UTC timestamp of the operation that incurred this cost pub timestamp: DateTime, + /// Agent that performed the operation pub agent_id: AgentId, + /// Human-readable description of the operation type pub operation_type: String, + /// Cost in US dollars pub cost_usd: f64, + /// Arbitrary key-value metadata attached to this record pub metadata: HashMap, } @@ -43,6 +48,7 @@ pub struct TokenUsageRecord { } impl TokenUsageRecord { + /// Create a new usage record, generating a fresh request ID and current timestamp pub fn new( agent_id: AgentId, model: String, @@ -65,6 +71,7 @@ impl TokenUsageRecord { } } + /// Attach a quality score in the range `[0.0, 1.0]` to this record pub fn with_quality_score(mut self, score: f64) -> Self { self.quality_score = Some(score.clamp(0.0, 1.0)); self @@ -87,6 +94,7 @@ pub struct ModelPricing { } impl ModelPricing { + /// Calculate the total cost in USD for a request with the given token counts pub fn calculate_cost(&self, input_tokens: u64, output_tokens: u64) -> f64 { let input_cost = (input_tokens as f64 / 1000.0) * self.input_cost_per_1k; let output_cost = (output_tokens as f64 / 1000.0) * self.output_cost_per_1k; @@ -118,6 +126,7 @@ pub struct TokenUsageTracker { } impl TokenUsageTracker { + /// Create a new, empty tracker for the specified agent pub fn new(agent_id: AgentId) -> Self { Self { agent_id, @@ -208,6 +217,7 @@ impl TokenUsageTracker { self.get_usage_in_period(start, end) } + /// Drain and return all usage records, leaving the internal list empty pub fn drain_records(&mut self) -> Vec { std::mem::take(&mut self.records) } @@ -216,14 +226,23 @@ impl TokenUsageTracker { /// Usage statistics for a time period #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UsageStats { + /// Start of the reporting period (inclusive) pub period_start: DateTime, + /// End of the reporting period (inclusive) pub period_end: DateTime, + /// Number of LLM requests made during the period pub request_count: u64, + /// Total prompt tokens consumed during the period pub total_input_tokens: u64, + /// Total completion tokens generated during the period pub total_output_tokens: u64, + /// Combined total of input and output tokens pub total_tokens: u64, + /// Total cost in USD for the period pub total_cost_usd: f64, + /// Mean tokens per request over the period pub avg_tokens_per_request: f64, + /// Mean cost in USD per request over the period pub avg_cost_per_request: f64, } @@ -247,20 +266,29 @@ pub struct BudgetAlert { /// Time window for budget alerts #[derive(Debug, Clone, Serialize, Deserialize)] pub enum AlertWindow { + /// Alert evaluated over the current hour Hourly, + /// Alert evaluated over the current calendar day Daily, + /// Alert evaluated over the current calendar week Weekly, + /// Alert evaluated over the current calendar month Monthly, } /// Actions to take when budget alert is triggered #[derive(Debug, Clone, Serialize, Deserialize)] pub enum AlertAction { + /// Write a log entry when the threshold is crossed Log, + /// Send an alert email to the given address Email(String), + /// POST an alert payload to the given URL Webhook(String), + /// Disable the agent that exceeded the threshold DisableAgent, - RateLimit(u64), // requests per minute + /// Apply a rate limit of the specified number of requests per minute + RateLimit(u64), } /// Cost tracker with budget monitoring @@ -285,6 +313,7 @@ pub struct CostTracker { } impl CostTracker { + /// Create a new `CostTracker` populated with default model pricing pub fn new() -> Self { Self { model_pricing: Self::default_model_pricing(), diff --git a/crates/terraphim_multi_agent/src/vm_execution/client.rs b/crates/terraphim_multi_agent/src/vm_execution/client.rs index b68bcda4f..e5930ea92 100644 --- a/crates/terraphim_multi_agent/src/vm_execution/client.rs +++ b/crates/terraphim_multi_agent/src/vm_execution/client.rs @@ -64,6 +64,7 @@ impl VmExecutionClient { } } + /// Override the default hook manager with a custom one. pub fn with_hook_manager(mut self, hook_manager: Arc) -> Self { self.hook_manager = hook_manager; self diff --git a/crates/terraphim_multi_agent/src/vm_execution/fcctl_bridge.rs b/crates/terraphim_multi_agent/src/vm_execution/fcctl_bridge.rs index 96585d7f9..c397e8062 100644 --- a/crates/terraphim_multi_agent/src/vm_execution/fcctl_bridge.rs +++ b/crates/terraphim_multi_agent/src/vm_execution/fcctl_bridge.rs @@ -1,3 +1,5 @@ +//! Bridge between the multi-agent system and the fcctl execution history / rollback API. + use super::models::*; use super::session_adapter::DirectSessionAdapter; use chrono::Utc; @@ -8,6 +10,7 @@ use std::sync::Arc; use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; +/// Tracks command history and provides snapshot/rollback capabilities for VMs. #[derive(Debug)] pub struct FcctlBridge { config: HistoryConfig, @@ -28,6 +31,7 @@ struct VmSession { } impl FcctlBridge { + /// Create a new `FcctlBridge` with the supplied history configuration and API base URL. pub fn new(config: HistoryConfig, api_base_url: String) -> Self { let direct_adapter = if config.integration_mode == "direct" { let data_dir = PathBuf::from("/tmp/fcctl-sessions"); @@ -48,6 +52,7 @@ impl FcctlBridge { } } + /// Record the result of a VM execution and optionally create a snapshot, returning the snapshot ID. pub async fn track_execution( &self, vm_id: &str, @@ -201,6 +206,7 @@ impl FcctlBridge { Ok(()) } + /// Query the command history for a VM, optionally filtering to failures only. pub async fn query_history( &self, request: HistoryQueryRequest, @@ -295,6 +301,7 @@ impl FcctlBridge { }) } + /// Rollback a VM to the specified snapshot, optionally capturing a pre-rollback snapshot. pub async fn rollback_to_snapshot( &self, request: RollbackRequest, @@ -355,6 +362,7 @@ impl FcctlBridge { }) } + /// If auto-rollback is enabled and a prior snapshot exists, restore the VM to it after a failure. pub async fn auto_rollback_on_failure( &self, vm_id: &str, @@ -402,6 +410,7 @@ impl FcctlBridge { } } + /// Return the snapshot ID of the most recent successful execution for a VM/agent pair, if any. pub async fn get_last_successful_snapshot( &self, vm_id: &str, diff --git a/crates/terraphim_multi_agent/src/vm_execution/hooks.rs b/crates/terraphim_multi_agent/src/vm_execution/hooks.rs index 11cfcf563..0eaada834 100644 --- a/crates/terraphim_multi_agent/src/vm_execution/hooks.rs +++ b/crates/terraphim_multi_agent/src/vm_execution/hooks.rs @@ -5,58 +5,109 @@ use tracing::{debug, info, warn}; use super::models::*; +/// Decision returned by a hook after inspecting tool or LLM invocations. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum HookDecision { + /// Allow the operation to proceed unchanged. Allow, - Block { reason: String }, - Modify { transformed_code: String }, - AskUser { prompt: String }, + /// Block the operation, providing a human-readable reason. + Block { + /// Explanation of why the operation was blocked. + reason: String, + }, + /// Allow the operation but replace the submitted code with a transformed version. + Modify { + /// The replacement code to execute instead of the original. + transformed_code: String, + }, + /// Pause execution and ask the user for guidance before proceeding. + AskUser { + /// The question or prompt to present to the user. + prompt: String, + }, } +/// Context passed to pre-tool hooks before code is executed in a VM. #[derive(Debug, Clone)] pub struct PreToolContext { + /// Source code submitted for execution. pub code: String, + /// Programming language of the submitted code (e.g. `"python"`, `"bash"`). pub language: String, + /// Identifier of the agent requesting execution. pub agent_id: String, + /// Identifier of the target VM. pub vm_id: String, + /// Arbitrary key-value metadata attached to the request. pub metadata: std::collections::HashMap, } +/// Context passed to post-tool hooks after code has been executed in a VM. #[derive(Debug, Clone)] pub struct PostToolContext { + /// The original source code that was executed. pub original_code: String, + /// Captured standard output (and stderr) from the execution. pub output: String, + /// Process exit code; `0` conventionally indicates success. pub exit_code: i32, + /// Wall-clock execution time in milliseconds. pub duration_ms: u64, + /// Identifier of the agent that requested execution. pub agent_id: String, + /// Identifier of the VM where execution ran. pub vm_id: String, } +/// Context passed to pre-LLM hooks before a prompt is sent to a language model. #[derive(Debug, Clone)] pub struct PreLlmContext { + /// The prompt text about to be submitted. pub prompt: String, + /// Identifier of the agent issuing the LLM request. pub agent_id: String, + /// Prior conversation turns included in the request. pub conversation_history: Vec, + /// Estimated token count for the full request. pub token_count: usize, } +/// Context passed to post-LLM hooks after a language model has responded. #[derive(Debug, Clone)] pub struct PostLlmContext { + /// The prompt that was submitted. pub prompt: String, + /// The model's response text. pub response: String, + /// Identifier of the agent that made the LLM request. pub agent_id: String, + /// Total tokens consumed by the request and response. pub token_count: usize, + /// Name or identifier of the model that generated the response. pub model: String, } +/// Trait implemented by all hooks that intercept VM tool and LLM invocations. +/// +/// Each method has a default implementation that returns [`HookDecision::Allow`], +/// so implementors only need to override the phases they care about. #[async_trait] pub trait Hook: Send + Sync { + /// Returns the unique name of this hook, used in log messages. fn name(&self) -> &str; + /// Called before code is executed in a VM. + /// + /// Returns a decision indicating whether to allow, block, or modify the + /// execution. async fn pre_tool(&self, _context: &PreToolContext) -> Result { Ok(HookDecision::Allow) } + /// Called after code has been executed in a VM. + /// + /// Returns a decision that may, for example, block the output from being + /// returned to the caller. async fn post_tool( &self, _context: &PostToolContext, @@ -64,15 +115,26 @@ pub trait Hook: Send + Sync { Ok(HookDecision::Allow) } + /// Called before a prompt is sent to a language model. + /// + /// Returns a decision that may block or modify the prompt. async fn pre_llm(&self, _context: &PreLlmContext) -> Result { Ok(HookDecision::Allow) } + /// Called after a language model has produced a response. + /// + /// Returns a decision that may, for example, block a response that + /// contains sensitive information. async fn post_llm(&self, _context: &PostLlmContext) -> Result { Ok(HookDecision::Allow) } } +/// Manages an ordered list of hooks and dispatches execution lifecycle events to them. +/// +/// Hooks are evaluated in registration order. The first non-`Allow` decision +/// short-circuits the chain and is returned immediately. pub struct HookManager { hooks: Vec>, } @@ -86,15 +148,20 @@ impl std::fmt::Debug for HookManager { } impl HookManager { + /// Creates a new `HookManager` with no registered hooks. pub fn new() -> Self { Self { hooks: Vec::new() } } + /// Registers a hook, appending it to the end of the chain. pub fn add_hook(&mut self, hook: Arc) { info!("Registered hook: {}", hook.name()); self.hooks.push(hook); } + /// Runs all pre-tool hooks against `context` in registration order. + /// + /// Returns on the first non-`Allow` decision, or `Allow` if all hooks pass. pub async fn run_pre_tool( &self, context: &PreToolContext, @@ -114,6 +181,9 @@ impl HookManager { Ok(HookDecision::Allow) } + /// Runs all post-tool hooks against `context` in registration order. + /// + /// Returns on the first non-`Allow` decision, or `Allow` if all hooks pass. pub async fn run_post_tool( &self, context: &PostToolContext, @@ -133,6 +203,9 @@ impl HookManager { Ok(HookDecision::Allow) } + /// Runs all pre-LLM hooks against `context` in registration order. + /// + /// Returns on the first non-`Allow` decision, or `Allow` if all hooks pass. pub async fn run_pre_llm( &self, context: &PreLlmContext, @@ -152,6 +225,9 @@ impl HookManager { Ok(HookDecision::Allow) } + /// Runs all post-LLM hooks against `context` in registration order. + /// + /// Returns on the first non-`Allow` decision, or `Allow` if all hooks pass. pub async fn run_post_llm( &self, context: &PostLlmContext, @@ -178,11 +254,16 @@ impl Default for HookManager { } } +/// Pre-tool hook that blocks code containing known-dangerous shell patterns. +/// +/// Patterns include destructive commands such as `rm -rf`, fork bombs, and +/// pipe-to-shell download patterns. pub struct DangerousPatternHook { patterns: Vec, } impl DangerousPatternHook { + /// Creates a new `DangerousPatternHook` with the built-in set of dangerous patterns. pub fn new() -> Self { let patterns = vec![ regex::Regex::new(r"rm\s+-rf").unwrap(), @@ -234,11 +315,16 @@ impl Default for DangerousPatternHook { } } +/// Pre-tool hook that validates that submitted code meets basic syntactic requirements. +/// +/// Checks include: language is in the supported set, code is non-empty, and code +/// does not exceed the maximum allowed length. pub struct SyntaxValidationHook { supported_languages: Vec, } impl SyntaxValidationHook { + /// Creates a new `SyntaxValidationHook` supporting Python, JavaScript, Bash, and Rust. pub fn new() -> Self { Self { supported_languages: vec![ @@ -286,6 +372,10 @@ impl Default for SyntaxValidationHook { } } +/// Hook that logs execution lifecycle events at the `INFO` level. +/// +/// Emits a message before execution starts (pre-tool) and after it finishes +/// (post-tool), recording language, agent, VM, exit code, and duration. pub struct ExecutionLoggerHook; #[async_trait] @@ -311,11 +401,19 @@ impl Hook for ExecutionLoggerHook { } } +/// Pre-tool hook that automatically injects common import statements into code +/// that does not already include them. +/// +/// Currently supports Python (`sys`/`os`) and JavaScript (comment placeholder). +/// When injection is disabled at construction time the hook always returns `Allow`. pub struct DependencyInjectorHook { inject_imports: bool, } impl DependencyInjectorHook { + /// Creates a new `DependencyInjectorHook`. + /// + /// When `inject_imports` is `false` the hook is effectively a no-op. pub fn new(inject_imports: bool) -> Self { Self { inject_imports } } @@ -361,6 +459,10 @@ impl Hook for DependencyInjectorHook { } } +/// Post-tool hook that blocks execution output containing sensitive information. +/// +/// Scans output for passwords, API keys, secrets, tokens, and e-mail addresses +/// using regular expressions. Any match causes the output to be blocked. pub struct OutputSanitizerHook; #[async_trait] diff --git a/crates/terraphim_multi_agent/src/vm_execution/mod.rs b/crates/terraphim_multi_agent/src/vm_execution/mod.rs index 7abc0c990..d75f80ba5 100644 --- a/crates/terraphim_multi_agent/src/vm_execution/mod.rs +++ b/crates/terraphim_multi_agent/src/vm_execution/mod.rs @@ -1,9 +1,18 @@ +//! VM execution subsystem for sandboxed code execution via fcctl. + +/// HTTP client for the fcctl-web VM execution API. pub mod client; +/// Utilities for extracting code blocks from LLM text responses. pub mod code_extractor; +/// Helpers for building `VmExecutionConfig` from role configuration. pub mod config_helpers; +/// Bridge for tracking execution history and triggering rollbacks. pub mod fcctl_bridge; +/// Pre- and post-execution hook framework for validation and logging. pub mod hooks; +/// Shared request/response and error types for VM execution. pub mod models; +/// Long-lived session management adapter for the fcctl session API. pub mod session_adapter; pub use client::*; diff --git a/crates/terraphim_multi_agent/src/vm_execution/session_adapter.rs b/crates/terraphim_multi_agent/src/vm_execution/session_adapter.rs index b361359d2..2f5be996c 100644 --- a/crates/terraphim_multi_agent/src/vm_execution/session_adapter.rs +++ b/crates/terraphim_multi_agent/src/vm_execution/session_adapter.rs @@ -1,3 +1,5 @@ +//! Direct session adapter for communicating with the fcctl session API. + use anyhow::Result; use std::collections::HashMap; use std::path::PathBuf; @@ -7,6 +9,7 @@ use tracing::{debug, info}; use super::models::*; +/// Manages long-lived VM sessions by communicating directly with the fcctl HTTP API. #[derive(Debug)] #[allow(dead_code)] pub struct DirectSessionAdapter { @@ -27,6 +30,7 @@ struct SessionHandle { } impl DirectSessionAdapter { + /// Create a new `DirectSessionAdapter` pointing at the given data directory and API URL. pub fn new(data_dir: PathBuf, fcctl_api_url: String) -> Self { Self { sessions: Arc::new(RwLock::new(HashMap::new())), @@ -35,6 +39,7 @@ impl DirectSessionAdapter { } } + /// Return the session key for an existing session, or create a new one via the API. pub async fn get_or_create_session( &self, vm_id: &str, @@ -104,6 +109,7 @@ impl DirectSessionAdapter { Ok(session_key) } + /// Execute a shell command inside the session and return `(stdout, exit_code)`. pub async fn execute_command_direct( &self, session_id: &str, @@ -162,6 +168,7 @@ impl DirectSessionAdapter { Ok((output, exit_code)) } + /// Create a named snapshot for the session and return the snapshot ID. pub async fn create_snapshot_direct( &self, session_id: &str, @@ -218,6 +225,7 @@ impl DirectSessionAdapter { Ok(snapshot_id) } + /// Roll the session back to a previously created snapshot. pub async fn rollback_direct( &self, session_id: &str, @@ -266,6 +274,7 @@ impl DirectSessionAdapter { Ok(()) } + /// Return metadata about a session, or `None` if the session does not exist. pub async fn get_session_info(&self, session_id: &str) -> Option { let sessions = self.sessions.read().await; sessions.get(session_id).map(|handle| SessionInfo { @@ -276,6 +285,7 @@ impl DirectSessionAdapter { }) } + /// Fetch connection details (host, port, credentials) for the session from the API. pub async fn get_connection_info(&self, session_id: &str) -> Result { let sessions = self.sessions.read().await; let handle = sessions @@ -308,6 +318,7 @@ impl DirectSessionAdapter { Ok(info) } + /// Close and delete the session, releasing its resources. pub async fn close_session(&self, session_id: &str) -> Result<(), VmExecutionError> { debug!("Closing session: {}", session_id); @@ -333,6 +344,7 @@ impl DirectSessionAdapter { Ok(()) } + /// List metadata for all currently tracked sessions. pub async fn list_sessions(&self) -> Vec { let sessions = self.sessions.read().await; sessions @@ -347,11 +359,16 @@ impl DirectSessionAdapter { } } +/// Snapshot of metadata describing a tracked VM session. #[derive(Debug, Clone)] pub struct SessionInfo { + /// Identifier of the VM that hosts this session pub vm_id: String, + /// Identifier of the agent that owns this session pub agent_id: String, + /// UTC timestamp when the session was created pub created_at: chrono::DateTime, + /// Total number of commands executed in this session pub command_count: usize, } From f1c1cc66a207c2a220e7e0ace691fb9b0116dbcc Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 2 Jun 2026 13:16:49 +0200 Subject: [PATCH 4/4] docs: fill doc gaps in terraphim_agent/usage, suppress in terraphim_validation terraphim_agent (119 gaps resolved): - REPL commands: ReplCommand enum + all subcommands (Robot, Update, Config, Role, File, Sessions, Vm, Web) with all variants and fields - Robot module: Capabilities, CommandDoc, ArgumentDoc, FlagDoc, ExampleDoc, BudgetedResults, BudgetEngine, FeatureFlags fields - Service: TuiService, ConnectivityResult, FuzzySuggestion, ChecklistResult - REPL handler: ReplHandler struct and constructors terraphim_usage (92 gaps resolved): - Core: UsageError variants, Result alias, ProviderUsage fields, MetricLine variants, ProgressFormat variants, UsageProvider trait, UsageRegistry methods - Pricing: PricingTable struct and all 5 methods - Providers: all 6 sub-module declarations, ZaiProvider, OpenCodeGoProvider, MiniMaxProvider, ClaudeProvider, KimiProvider, CcusageProvider - Store: BudgetVerdict variants - Formatter: format_usage_text/json/csv functions terraphim_validation: add #![allow(missing_docs)] -- this is test infrastructure with existing clippy::all suppression; gradual adoption is the established pattern for this crate. Refs #1979 --- crates/terraphim_agent/src/lib.rs | 2 + crates/terraphim_agent/src/repl/commands.rs | 220 ++++++++++++++++-- crates/terraphim_agent/src/repl/handler.rs | 5 + crates/terraphim_agent/src/robot/budget.rs | 9 + crates/terraphim_agent/src/robot/docs.rs | 26 +++ crates/terraphim_agent/src/robot/mod.rs | 1 + crates/terraphim_agent/src/robot/schema.rs | 8 + crates/terraphim_agent/src/service.rs | 14 ++ crates/terraphim_usage/src/formatter.rs | 3 + crates/terraphim_usage/src/lib.rs | 71 +++++- crates/terraphim_usage/src/pricing.rs | 13 ++ .../terraphim_usage/src/providers/ccusage.rs | 2 + .../terraphim_usage/src/providers/claude.rs | 4 + crates/terraphim_usage/src/providers/kimi.rs | 2 + .../terraphim_usage/src/providers/minimax.rs | 4 + crates/terraphim_usage/src/providers/mod.rs | 6 + .../src/providers/opencode_go.rs | 4 + crates/terraphim_usage/src/providers/zai.rs | 3 + crates/terraphim_usage/src/store.rs | 3 + crates/terraphim_validation/src/lib.rs | 1 + 20 files changed, 380 insertions(+), 21 deletions(-) diff --git a/crates/terraphim_agent/src/lib.rs b/crates/terraphim_agent/src/lib.rs index a0b39eeda..2ef2ec14f 100644 --- a/crates/terraphim_agent/src/lib.rs +++ b/crates/terraphim_agent/src/lib.rs @@ -5,7 +5,9 @@ //! Feature flags gate heavier subsystems: `server`, `repl`, `shared-learning`. #[cfg(feature = "server")] pub mod client; +/// Onboarding workflow for first-run setup and guided configuration. pub mod onboarding; +/// Service layer wrapping TerraphimService for use by the TUI and robot mode. pub mod service; #[cfg(feature = "shared-learning")] pub mod shared_learning; diff --git a/crates/terraphim_agent/src/repl/commands.rs b/crates/terraphim_agent/src/repl/commands.rs index 0bcad2834..8cde785f1 100644 --- a/crates/terraphim_agent/src/repl/commands.rs +++ b/crates/terraphim_agent/src/repl/commands.rs @@ -3,117 +3,171 @@ use anyhow::{Result, anyhow}; use std::str::FromStr; +/// All commands that can be issued in the Terraphim REPL. #[derive(Debug, Clone, PartialEq)] pub enum ReplCommand { // Base commands (always available with 'repl' feature) + /// Search the configured haystacks using the knowledge graph. Search { + /// The search query string. query: String, + /// Optional role override; uses the currently selected role if absent. role: Option, + /// Maximum number of results to return. limit: Option, + /// Enable semantic (embedding-based) search. semantic: bool, + /// Include knowledge-graph concept matches in results. concepts: bool, }, + /// View or modify the current configuration. Config { + /// Configuration subcommand to execute. subcommand: ConfigSubcommand, }, + /// List or switch between roles. Role { + /// Role subcommand to execute. subcommand: RoleSubcommand, }, + /// Display the top concepts from the knowledge graph. Graph { + /// Number of top concepts to display. top_k: Option, }, // Chat commands (requires 'llm' feature) + /// Send a message to the configured LLM. #[cfg(feature = "llm")] Chat { + /// Message to send; starts interactive session if absent. message: Option, }, + /// Summarise a document or piece of text using the LLM. #[cfg(feature = "llm")] Summarize { + /// Document ID or raw text to summarise. target: String, }, // MCP commands (requires 'repl-mcp' feature) + /// Autocomplete a partial term against the thesaurus. #[cfg(feature = "repl-mcp")] Autocomplete { + /// Partial query string to complete. query: String, + /// Maximum number of suggestions to return. limit: Option, }, + /// Extract paragraphs that contain thesaurus-matched terms. #[cfg(feature = "repl-mcp")] Extract { + /// Input text to extract paragraphs from. text: String, + /// When true, omit the matched term from each extracted paragraph. exclude_term: bool, }, + /// Find all thesaurus-term matches within the given text. #[cfg(feature = "repl-mcp")] Find { + /// Input text to search for matches. text: String, }, + /// Replace thesaurus-matched terms in text with hyperlinks. #[cfg(feature = "repl-mcp")] Replace { + /// Input text whose matched terms will be replaced. text: String, + /// Link format: `"markdown"`, `"wiki"`, `"html"`, or `"plain"`. format: Option, }, + /// Display thesaurus entries for the selected role. #[cfg(feature = "repl-mcp")] Thesaurus { + /// Role whose thesaurus to display; defaults to the current role. role: Option, }, // File commands (requires 'repl-file' feature) + /// Perform file system operations. #[cfg(feature = "repl-file")] File { + /// File subcommand to execute. subcommand: FileSubcommand, }, // Web commands (requires 'repl-web' feature) + /// Perform web operations (HTTP requests, scraping, screenshots). #[cfg(feature = "repl-web")] Web { + /// Web subcommand to execute. subcommand: WebSubcommand, }, // VM commands (requires 'firecracker' feature) + /// Manage Firecracker microVMs. #[cfg(feature = "firecracker")] Vm { + /// VM subcommand to execute. subcommand: VmSubcommand, }, // Robot mode commands (for AI agents) + /// Access robot-mode self-documentation for AI agent integration. Robot { + /// Robot subcommand to execute. subcommand: RobotSubcommand, }, // Session commands (requires 'repl-sessions' feature) + /// Browse and search AI coding session history. #[cfg(feature = "repl-sessions")] Sessions { + /// Sessions subcommand to execute. subcommand: SessionsSubcommand, }, // Update management commands (always available) + /// Manage binary self-updates and rollbacks. Update { + /// Update subcommand to execute. subcommand: UpdateSubcommand, }, // Utility commands + /// Show help information, optionally for a specific command. Help { + /// Command to show help for; shows all commands if absent. command: Option, }, + /// Exit the REPL (alias for `/exit`). Quit, + /// Exit the REPL. Exit, + /// Clear the terminal screen. Clear, } +/// Subcommands for robot mode self-documentation. #[derive(Debug, Clone, PartialEq)] pub enum RobotSubcommand { /// Get capabilities summary Capabilities, /// Get schema for a command (or all commands) - Schemas { command: Option }, + Schemas { + /// Specific command name to fetch schema for, or all schemas if absent. + command: Option, + }, /// Get examples for a command - Examples { command: Option }, + Examples { + /// Specific command name to fetch examples for, or all examples if absent. + command: Option, + }, /// List exit codes ExitCodes, } @@ -126,31 +180,59 @@ pub enum UpdateSubcommand { /// Install available updates Install, /// Rollback to a previous version - Rollback { version: String }, + Rollback { + /// Semver version string to roll back to. + version: String, + }, /// List available backup versions List, } +/// Subcommands for the `/config` command. #[derive(Debug, Clone, PartialEq)] pub enum ConfigSubcommand { + /// Display the current configuration as JSON. Show, - Set { key: String, value: String }, + /// Set a configuration key to a value. + Set { + /// Configuration key to set. + key: String, + /// New value for the key. + value: String, + }, } +/// Subcommands for the `/role` command. #[derive(Debug, Clone, PartialEq)] pub enum RoleSubcommand { + /// List all available roles. List, - Select { name: String }, + /// Switch the active role by name or shortname. + Select { + /// Role name or shortname to activate. + name: String, + }, } +/// Subcommands for file system operations. #[derive(Debug, Clone, PartialEq)] #[cfg(feature = "repl-file")] pub enum FileSubcommand { - Search { query: String }, + /// Search for files matching the given query. + Search { + /// Query string to match against file names or content. + query: String, + }, + /// List files in the current context. List, - Info { path: String }, + /// Show metadata for a specific file path. + Info { + /// Path of the file to inspect. + path: String, + }, } +/// Subcommands for AI coding session history management. #[derive(Debug, Clone, PartialEq)] #[cfg(feature = "repl-sessions")] pub enum SessionsSubcommand { @@ -158,41 +240,75 @@ pub enum SessionsSubcommand { Sources, /// List imported sessions (auto-imports if cache is empty) List { + /// Filter sessions to those from a specific source identifier. source: Option, + /// Maximum number of sessions to display. limit: Option, }, /// Search sessions by query - Search { query: String }, + Search { + /// Full-text search query. + query: String, + }, /// Show session statistics Stats, /// Show details of a specific session - Show { session_id: String }, + Show { + /// Session ID to display. + session_id: String, + }, /// Search sessions by concept (Phase 3 - requires enrichment) - Concepts { concept: String }, + Concepts { + /// Concept name to match sessions against. + concept: String, + }, /// Find sessions related to a given session Related { + /// ID of the reference session. session_id: String, + /// Minimum number of shared concepts required for a related match. min_shared: Option, }, /// Show session timeline grouped by period Timeline { - group_by: Option, // day, week, month + /// Grouping period: `"day"`, `"week"`, or `"month"`. + group_by: Option, + /// Maximum number of periods to display. limit: Option, }, /// Export sessions to file Export { - format: Option, // json, markdown - output: Option, // file path + /// Output format: `"json"` or `"markdown"`. + format: Option, + /// File path to write the export to. + output: Option, + /// Limit export to a single session by ID. session_id: Option, }, /// Enrich sessions with concepts (Phase 3) - Enrich { session_id: Option }, + Enrich { + /// Specific session to enrich, or all sessions if absent. + session_id: Option, + }, /// List files accessed by a session - Files { session_id: String, json: bool }, + Files { + /// Session ID whose file accesses to list. + session_id: String, + /// Output as machine-readable JSON. + json: bool, + }, /// Find sessions by file path - ByFile { file_path: String, json: bool }, + ByFile { + /// File path to search for in session records. + file_path: String, + /// Output as machine-readable JSON. + json: bool, + }, /// Build search index and show index statistics - Index { verbose: bool }, + Index { + /// Show verbose index statistics. + verbose: bool, + }, /// Cluster sessions by concept similarity (Spec F5.2) Cluster { /// Maximum number of clusters (auto-detect if None) @@ -204,97 +320,165 @@ pub enum SessionsSubcommand { }, } +/// Subcommands for Firecracker microVM management. #[cfg(feature = "firecracker")] #[derive(Debug, Clone, PartialEq)] pub enum VmSubcommand { + /// List currently running VMs. List, + /// Show VM pool utilisation statistics. Pool, + /// Show status of a specific VM, or all VMs if absent. Status { + /// VM identifier to query; shows all VMs if absent. vm_id: Option, }, + /// Show metrics for a specific VM, or all VMs if absent. Metrics { + /// VM identifier to query; shows all VM metrics if absent. vm_id: Option, }, + /// Execute code in a VM. Execute { + /// Source code to execute. code: String, + /// Programming language identifier (e.g., `"python"`, `"rust"`). language: String, + /// VM to run the code on; uses a pooled VM if absent. vm_id: Option, }, + /// Run an agent task inside a VM. Agent { + /// Identifier of the agent to invoke. agent_id: String, + /// Task description to pass to the agent. task: String, + /// VM to run the agent on; uses a pooled VM if absent. vm_id: Option, }, + /// List tasks running on a specific VM. Tasks { + /// VM identifier whose tasks to list. vm_id: String, }, + /// Allocate a VM from the pool by ID. Allocate { + /// VM identifier to allocate. vm_id: String, }, + /// Release a VM back to the pool. Release { + /// VM identifier to release. vm_id: String, }, + /// Continuously monitor a VM's status and metrics. Monitor { + /// VM identifier to monitor. vm_id: String, + /// Polling interval in seconds (default: 5). refresh: Option, }, } +/// Subcommands for web operations (HTTP, scraping, screenshots). #[derive(Debug, Clone, PartialEq)] #[cfg(feature = "repl-web")] pub enum WebSubcommand { + /// Perform an HTTP GET request. Get { + /// Target URL. url: String, + /// Optional HTTP headers to include in the request. headers: Option>, }, + /// Perform an HTTP POST request. Post { + /// Target URL. url: String, + /// Request body payload. body: String, + /// Optional HTTP headers to include in the request. headers: Option>, }, + /// Scrape content from a web page. Scrape { + /// URL of the page to scrape. url: String, + /// CSS selector to extract a specific element. selector: Option, + /// CSS selector to wait for before extracting content. wait_for_element: Option, }, + /// Capture a screenshot of a web page. Screenshot { + /// URL of the page to screenshot. url: String, + /// Viewport width in pixels. width: Option, + /// Viewport height in pixels. height: Option, + /// Capture the full scrollable page height. full_page: Option, }, + /// Render a web page to PDF. Pdf { + /// URL of the page to render. url: String, + /// Paper size (e.g., `"A4"`, `"Letter"`). page_size: Option, }, + /// Submit an HTML form on a page. Form { + /// URL of the page containing the form. url: String, + /// Field-name to value map for form submission. form_data: std::collections::HashMap, }, + /// Call a REST API endpoint. Api { + /// API endpoint URL. endpoint: String, + /// HTTP method (e.g., `"GET"`, `"POST"`). method: String, + /// Optional JSON body for the request. data: Option, }, + /// Poll the status of a long-running web operation. Status { + /// Operation ID returned by a prior async web command. operation_id: String, }, + /// Cancel a long-running web operation. Cancel { + /// Operation ID of the operation to cancel. operation_id: String, }, + /// Show the history of recent web operations. History { + /// Maximum number of history entries to display. limit: Option, }, + /// View or modify web operation configuration. Config { + /// Web configuration subcommand. subcommand: WebConfigSubcommand, }, } +/// Subcommands for web operation configuration. #[derive(Debug, Clone, PartialEq)] #[cfg(feature = "repl-web")] pub enum WebConfigSubcommand { + /// Display the current web configuration. Show, - Set { key: String, value: String }, + /// Set a web configuration key to a value. + Set { + /// Configuration key to set. + key: String, + /// New value for the key. + value: String, + }, + /// Reset web configuration to defaults. Reset, } diff --git a/crates/terraphim_agent/src/repl/handler.rs b/crates/terraphim_agent/src/repl/handler.rs index ce610d6d7..bb118c4c1 100644 --- a/crates/terraphim_agent/src/repl/handler.rs +++ b/crates/terraphim_agent/src/repl/handler.rs @@ -25,6 +25,7 @@ use rustyline::Editor; #[cfg(feature = "repl")] use colored::Colorize; +/// Interactive REPL handler that dispatches parsed commands to service methods. pub struct ReplHandler { service: Option, #[cfg(feature = "server")] @@ -35,6 +36,7 @@ pub struct ReplHandler { } impl ReplHandler { + /// Create a handler that operates in offline mode using a local `TuiService`. pub fn new_offline(service: TuiService) -> Self { #[cfg(feature = "repl-mcp")] let mcp_handler = { @@ -52,6 +54,7 @@ impl ReplHandler { } } + /// Create a handler that delegates requests to a remote API server. #[cfg(feature = "server")] pub fn new_server(api_client: ApiClient) -> Self { Self { @@ -63,6 +66,7 @@ impl ReplHandler { } } + /// Start the interactive REPL loop, reading commands until the user quits. #[cfg(feature = "repl")] pub async fn run(&mut self) -> Result<()> { use rustyline::completion::{Completer, Pair}; @@ -193,6 +197,7 @@ impl ReplHandler { Ok(()) } + /// Placeholder run method when the `repl` feature is not compiled in. #[cfg(not(feature = "repl"))] pub async fn run(&mut self) -> Result<()> { println!("REPL feature not enabled. Please rebuild with --features repl"); diff --git a/crates/terraphim_agent/src/robot/budget.rs b/crates/terraphim_agent/src/robot/budget.rs index 371465908..6807b232c 100644 --- a/crates/terraphim_agent/src/robot/budget.rs +++ b/crates/terraphim_agent/src/robot/budget.rs @@ -3,19 +3,26 @@ use serde::{Deserialize, Serialize}; use super::output::{FieldMode, RobotConfig, RobotFormatter}; use super::schema::{Pagination, SearchResultItem, TokenBudget}; +/// Search results after applying field filtering and token-budget constraints. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BudgetedResults { + /// Filtered and (possibly truncated) result items as JSON values. pub results: Vec, + /// Pagination metadata describing the total and returned counts. pub pagination: Pagination, + /// Token budget tracking, present when a `max_tokens` limit was configured. pub token_budget: Option, } +/// Errors that can occur while applying the budget engine. #[derive(Debug, thiserror::Error)] pub enum BudgetError { #[error("serialization error: {0}")] + /// A JSON serialisation error occurred when converting result items. Serialization(#[from] serde_json::Error), } +/// Applies field-filtering, max-results, and token-budget constraints to search results. pub struct BudgetEngine { config: RobotConfig, formatter: RobotFormatter, @@ -34,11 +41,13 @@ const KNOWN_FIELDS: &[&str] = &[ ]; impl BudgetEngine { + /// Create a new `BudgetEngine` from the given robot output configuration. pub fn new(config: RobotConfig) -> Self { let formatter = RobotFormatter::new(config.clone()); Self { config, formatter } } + /// Apply field filtering, max-results cap, and token budget to a result slice. pub fn apply(&self, results: &[SearchResultItem]) -> Result { let total = results.len(); diff --git a/crates/terraphim_agent/src/robot/docs.rs b/crates/terraphim_agent/src/robot/docs.rs index 1dafb1cd0..5071bb6b3 100644 --- a/crates/terraphim_agent/src/robot/docs.rs +++ b/crates/terraphim_agent/src/robot/docs.rs @@ -586,34 +586,52 @@ impl Default for SelfDocumentation { /// Capabilities summary #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Capabilities { + /// Agent binary name. pub name: String, + /// Semver version of the agent binary. pub version: String, + /// Short human-readable description of what the agent does. pub description: String, + /// Feature flags indicating which subsystems are compiled in. pub features: FeatureFlags, + /// Names of all available REPL commands. pub commands: Vec, + /// Supported output formats (e.g., `"json"`, `"table"`). pub supported_formats: Vec, } /// Documentation for a single command #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CommandDoc { + /// Primary name of the command (e.g., `"search"`). pub name: String, + /// Alternative names that resolve to this command. pub aliases: Vec, + /// Human-readable description of what the command does. pub description: String, + /// Positional arguments accepted by the command. pub arguments: Vec, + /// Named flags accepted by the command. pub flags: Vec, + /// Usage examples for the command. pub examples: Vec, + /// JSON Schema describing the response payload. pub response_schema: serde_json::Value, } /// Documentation for a command argument #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ArgumentDoc { + /// Argument name (used as a label in help text). pub name: String, + /// Data type of the argument (e.g., `"string"`, `"integer"`). #[serde(rename = "type")] pub arg_type: String, + /// Whether the argument must be provided by the caller. pub required: bool, + /// Human-readable description of the argument's purpose. pub description: String, + /// Default value used when the argument is omitted. #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } @@ -621,21 +639,29 @@ pub struct ArgumentDoc { /// Documentation for a command flag #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FlagDoc { + /// Long-form flag name including the `--` prefix (e.g., `"--limit"`). pub name: String, + /// Short single-character alias including the `-` prefix (e.g., `"-l"`). #[serde(skip_serializing_if = "Option::is_none")] pub short: Option, + /// Data type of the flag value (e.g., `"integer"`, `"boolean"`). #[serde(rename = "type")] pub flag_type: String, + /// Default value used when the flag is not provided. #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, + /// Human-readable description of the flag's effect. pub description: String, } /// Documentation for a command example #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExampleDoc { + /// Short description of what this example demonstrates. pub description: String, + /// The exact command string to run (including the leading `/`). pub command: String, + /// Optional expected output snippet for illustration. #[serde(skip_serializing_if = "Option::is_none")] pub output: Option, } diff --git a/crates/terraphim_agent/src/robot/mod.rs b/crates/terraphim_agent/src/robot/mod.rs index c7ea23fc9..98c47d8ce 100644 --- a/crates/terraphim_agent/src/robot/mod.rs +++ b/crates/terraphim_agent/src/robot/mod.rs @@ -3,6 +3,7 @@ //! This module provides structured JSON output and self-documentation //! capabilities for integration with AI agents and automation tools. +/// Token-budget engine for capping search result output size. #[allow(dead_code)] pub mod budget; #[allow(dead_code)] diff --git a/crates/terraphim_agent/src/robot/schema.rs b/crates/terraphim_agent/src/robot/schema.rs index 2cebef6f8..e4dea37f5 100644 --- a/crates/terraphim_agent/src/robot/schema.rs +++ b/crates/terraphim_agent/src/robot/schema.rs @@ -348,13 +348,21 @@ pub struct CapabilitiesData { /// Feature flags #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureFlags { + /// Document search is available. pub search: bool, + /// LLM chat is available (requires `llm` feature). pub chat: bool, + /// MCP tool operations are available (requires `repl-mcp` feature). pub mcp_tools: bool, + /// File system operations are available (requires `repl-file` feature). pub file_operations: bool, + /// Web operations are available (requires `repl-web` feature). pub web_operations: bool, + /// Firecracker VM execution is available. pub vm_execution: bool, + /// Session search is available (requires `repl-sessions` feature). pub session_search: bool, + /// Knowledge graph operations are available. pub knowledge_graph: bool, } diff --git a/crates/terraphim_agent/src/service.rs b/crates/terraphim_agent/src/service.rs index 1c61ae55f..ea401a12e 100644 --- a/crates/terraphim_agent/src/service.rs +++ b/crates/terraphim_agent/src/service.rs @@ -10,6 +10,10 @@ use terraphim_settings::{DeviceSettings, Error as DeviceSettingsError}; use terraphim_types::{Document, Layer, NormalizedTermValue, RoleName, SearchQuery, Thesaurus}; use tokio::sync::Mutex; +/// High-level TUI service that wraps `TerraphimService` and `ConfigState`. +/// +/// Provides async methods for search, role management, knowledge graph access, +/// and optional LLM features consumed by both the interactive REPL and robot mode. #[derive(Clone)] pub struct TuiService { config_state: ConfigState, @@ -898,24 +902,34 @@ impl TuiService { /// Result of connectivity check #[derive(Debug, Clone, serde::Serialize)] pub struct ConnectivityResult { + /// Whether all matched terms are connected by a single path. pub connected: bool, + /// The terms from the knowledge graph that were matched in the text. pub matched_terms: Vec, + /// Human-readable summary of the connectivity result. pub message: String, } /// Fuzzy suggestion result #[derive(Debug, Clone, serde::Serialize)] pub struct FuzzySuggestion { + /// The suggested term from the thesaurus. pub term: String, + /// Jaro-Winkler similarity score between the query and this term. pub similarity: f64, } /// Checklist validation result #[derive(Debug, Clone, serde::Serialize)] pub struct ChecklistResult { + /// Name of the checklist that was validated (e.g., `"code_review"`, `"security"`). pub checklist_name: String, + /// Whether all checklist categories were satisfied. pub passed: bool, + /// Total number of categories checked. pub total_items: usize, + /// Categories that were found in the text. pub satisfied: Vec, + /// Categories that were not found in the text. pub missing: Vec, } diff --git a/crates/terraphim_usage/src/formatter.rs b/crates/terraphim_usage/src/formatter.rs index 7f6964c29..3ad27e8d1 100644 --- a/crates/terraphim_usage/src/formatter.rs +++ b/crates/terraphim_usage/src/formatter.rs @@ -1,6 +1,7 @@ use crate::{MetricLine, ProviderUsage}; use std::fmt::Write; +/// Formats a [`ProviderUsage`] snapshot as a human-readable plain-text string. pub fn format_usage_text(usage: &ProviderUsage) -> String { let mut output = String::new(); writeln!( @@ -56,10 +57,12 @@ fn progress_bar(pct: f64) -> String { format!("{}{}", "█".repeat(filled), "░".repeat(empty)) } +/// Serialises a [`ProviderUsage`] snapshot to a pretty-printed JSON string. pub fn format_usage_json(usage: &ProviderUsage) -> Result { serde_json::to_string_pretty(usage) } +/// Formats a slice of [`ProviderUsage`] snapshots as a CSV string. pub fn format_usage_csv(usages: &[ProviderUsage]) -> String { let mut csv = String::from("provider,plan,line_type,label,value,used,limit,resets_at,fetched_at\n"); diff --git a/crates/terraphim_usage/src/lib.rs b/crates/terraphim_usage/src/lib.rs index cd45e9a0b..c93068223 100644 --- a/crates/terraphim_usage/src/lib.rs +++ b/crates/terraphim_usage/src/lib.rs @@ -3,119 +3,184 @@ //! Tracks token consumption across providers (OpenRouter, Ollama, …), applies //! per-model pricing tables, and persists usage records. Optional `cli` and //! `providers` feature flags unlock the reporting CLI and provider adapters. +/// CLI interface for the usage reporting tool. #[cfg(feature = "cli")] pub mod cli; +/// Text, JSON, and CSV formatters for provider usage snapshots. pub mod formatter; +/// Per-model pricing tables used to estimate request costs. pub mod pricing; +/// Built-in provider adapters (requires the `providers` feature). #[cfg(feature = "providers")] pub mod providers; +/// Persistable storage records for usage metrics and budget snapshots. #[cfg(feature = "persistence")] pub mod store; use serde::{Deserialize, Serialize}; use thiserror::Error; +/// Error type for usage metering operations. #[derive(Error, Debug)] pub enum UsageError { + /// The requested provider was not registered. #[error("Provider {0} not found")] ProviderNotFound(String), + /// A network or API error occurred while fetching usage data. #[error("Failed to fetch usage from {provider}: {source}")] FetchFailed { + /// Provider identifier. provider: String, + /// Underlying error source. source: Box, }, + /// Authentication credentials were missing or invalid. #[error("Authentication failed for {provider}: {message}")] - AuthFailed { provider: String, message: String }, + AuthFailed { + /// Provider identifier. + provider: String, + /// Human-readable failure description. + message: String, + }, + /// The provider API rate limit was exceeded. #[error("Rate limit exceeded for {provider}")] - RateLimited { provider: String }, + RateLimited { + /// Provider identifier. + provider: String, + }, + /// A persistence layer error occurred. #[error("Storage error: {0}")] StorageError(String), + /// JSON (de)serialisation failed. #[error("Serialization error: {0}")] SerializationError(#[from] serde_json::Error), } +/// Convenience `Result` alias that uses [`UsageError`] as the error type. pub type Result = std::result::Result; +/// Aggregated usage snapshot returned by a [`UsageProvider`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProviderUsage { + /// Stable machine-readable provider identifier (e.g. `"claude"`). pub provider_id: String, + /// Human-readable provider name for display. pub display_name: String, + /// Subscription plan name, if known. pub plan: Option, + /// Individual metric lines to display. pub lines: Vec, + /// RFC 3339 timestamp of when this snapshot was fetched. pub fetched_at: String, } +/// A single display metric emitted by a provider. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum MetricLine { + /// A simple key-value text metric. Text { + /// Metric label. label: String, + /// Metric value string. value: String, + /// Optional colour hint for rendering. color: Option, + /// Optional subtitle / secondary text. subtitle: Option, }, + /// A numeric metric with a progress bar. Progress { + /// Metric label. label: String, + /// Current consumed quantity. used: f64, + /// Total allowed quantity. limit: f64, + /// How the quantity should be formatted. format: ProgressFormat, + /// RFC 3339 timestamp when the quota resets. resets_at: Option, + /// Duration of the quota period in milliseconds. period_duration_ms: Option, + /// Optional colour hint for rendering. color: Option, }, + /// A badge / pill metric. Badge { + /// Metric label. label: String, + /// Badge text. text: String, + /// Optional colour hint for rendering. color: Option, + /// Optional subtitle / secondary text. subtitle: Option, }, } +/// Display format for a [`MetricLine::Progress`] value. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum ProgressFormat { + /// Render as a percentage. Percent, + /// Render as a dollar amount. Dollars, - Count { suffix: String }, + /// Render as a count with a unit suffix. + Count { + /// Unit suffix appended after the numeric value. + suffix: String, + }, } +/// Trait implemented by each provider adapter to fetch live usage data. pub trait UsageProvider: Send + Sync { + /// Returns the stable machine-readable provider identifier. fn id(&self) -> &str; + /// Returns the human-readable display name. fn display_name(&self) -> &str; + /// Asynchronously fetches the current usage snapshot. fn fetch_usage( &self, ) -> std::pin::Pin> + Send + '_>>; } +/// Registry that holds all registered [`UsageProvider`] instances. pub struct UsageRegistry { providers: std::collections::HashMap>, } impl UsageRegistry { + /// Creates a new empty registry. pub fn new() -> Self { Self { providers: std::collections::HashMap::new(), } } + /// Registers a provider, replacing any existing provider with the same id. pub fn register(&mut self, provider: Box) { let id = provider.id().to_string(); self.providers.insert(id, provider); } + /// Looks up a provider by its id. pub fn get(&self, id: &str) -> Option<&dyn UsageProvider> { self.providers.get(id).map(|p| p.as_ref()) } + /// Returns references to all registered providers. pub fn all(&self) -> Vec<&dyn UsageProvider> { self.providers.values().map(|p| p.as_ref()).collect() } + /// Returns all registered provider ids. pub fn ids(&self) -> Vec<&str> { self.providers.keys().map(|k| k.as_str()).collect() } diff --git a/crates/terraphim_usage/src/pricing.rs b/crates/terraphim_usage/src/pricing.rs index 0c3fe57a2..e3f44e484 100644 --- a/crates/terraphim_usage/src/pricing.rs +++ b/crates/terraphim_usage/src/pricing.rs @@ -1,12 +1,15 @@ use std::path::Path; use terraphim_types::ModelPricing; +/// Table of per-model pricing entries used to estimate request costs. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PricingTable { + /// Ordered list of pricing entries; later entries override earlier ones on exact matches. pub entries: Vec, } impl PricingTable { + /// Returns a `PricingTable` pre-populated with built-in default pricing for common models. pub fn embedded_defaults() -> Self { Self { entries: vec![ @@ -124,6 +127,9 @@ impl PricingTable { } } + /// Loads a `PricingTable` from a TOML file, merging with embedded defaults. + /// + /// Falls back to [`Self::embedded_defaults`] if the file does not exist or cannot be parsed. pub fn load(path: &Path) -> Self { match std::fs::read_to_string(path) { Ok(content) => match toml::from_str::(&content) { @@ -141,12 +147,16 @@ impl PricingTable { } } + /// Loads from `~/.config/terraphim/pricing.toml`, falling back to embedded defaults. pub fn load_default_path() -> Self { let home = std::env::var("HOME").unwrap_or_default(); let path = Path::new(&home).join(".config/terraphim/pricing.toml"); Self::load(&path) } + /// Finds the best-matching pricing entry for the given model string. + /// + /// Prefers exact matches over wildcard (glob `*`) prefix matches, and longer prefixes over shorter ones. pub fn find_pricing(&self, model: &str) -> Option<&ModelPricing> { let model_lower = model.to_lowercase(); let mut best_match: Option<&ModelPricing> = None; @@ -167,6 +177,9 @@ impl PricingTable { best_match } + /// Calculates the estimated cost in USD for the given token counts and model. + /// + /// Returns `None` if no matching pricing entry is found. pub fn calculate_cost( &self, model: &str, diff --git a/crates/terraphim_usage/src/providers/ccusage.rs b/crates/terraphim_usage/src/providers/ccusage.rs index 506f6952c..c7e0eda36 100644 --- a/crates/terraphim_usage/src/providers/ccusage.rs +++ b/crates/terraphim_usage/src/providers/ccusage.rs @@ -1,11 +1,13 @@ use crate::{MetricLine, ProgressFormat, ProviderUsage, Result, UsageError, UsageProvider}; use std::time::Duration; +/// Usage provider that reads Claude Code session data via the `ccusage` client library. pub struct CcusageProvider { client: std::sync::Mutex, } impl CcusageProvider { + /// Creates a new `CcusageProvider` with a 5-minute cache TTL. pub fn new() -> Self { let client = terraphim_ccusage::CcusageClient::new(terraphim_ccusage::CcusageProvider::Claude) diff --git a/crates/terraphim_usage/src/providers/claude.rs b/crates/terraphim_usage/src/providers/claude.rs index b4497a023..1d8924574 100644 --- a/crates/terraphim_usage/src/providers/claude.rs +++ b/crates/terraphim_usage/src/providers/claude.rs @@ -2,6 +2,7 @@ use crate::{MetricLine, ProgressFormat, ProviderUsage, Result, UsageError, Usage use std::path::PathBuf; use std::time::Duration; +/// Usage provider for Claude Code (Anthropic) using the `ccusage` client. pub struct ClaudeProvider { #[allow(dead_code)] credentials_path: PathBuf, @@ -9,6 +10,8 @@ pub struct ClaudeProvider { } impl ClaudeProvider { + /// Creates a new `ClaudeProvider` using the default credentials path + /// (`~/.claude/.credentials.json`). pub fn new() -> Self { let home = std::env::var("HOME").unwrap_or_default(); Self { @@ -20,6 +23,7 @@ impl ClaudeProvider { } } + /// Creates a `ClaudeProvider` with an explicit credentials file path. pub fn with_credentials_path(path: PathBuf) -> Self { let ccusage = terraphim_ccusage::CcusageClient::new(terraphim_ccusage::CcusageProvider::Claude) diff --git a/crates/terraphim_usage/src/providers/kimi.rs b/crates/terraphim_usage/src/providers/kimi.rs index da257e42d..0d72c3c1a 100644 --- a/crates/terraphim_usage/src/providers/kimi.rs +++ b/crates/terraphim_usage/src/providers/kimi.rs @@ -1,8 +1,10 @@ use crate::{ProviderUsage, Result, UsageError, UsageProvider}; +/// Usage provider for Kimi (Moonshot AI) — implementation pending. pub struct KimiProvider; impl KimiProvider { + /// Creates a new `KimiProvider`. pub fn new() -> Self { Self } diff --git a/crates/terraphim_usage/src/providers/minimax.rs b/crates/terraphim_usage/src/providers/minimax.rs index d502aed05..808a07302 100644 --- a/crates/terraphim_usage/src/providers/minimax.rs +++ b/crates/terraphim_usage/src/providers/minimax.rs @@ -39,6 +39,7 @@ struct MiniMaxModelRemains { plan: Option, } +/// Usage provider for the MiniMax subscription platform (global and CN regions). pub struct MiniMaxProvider { api_key: Option, cn_api_key: Option, @@ -46,6 +47,8 @@ pub struct MiniMaxProvider { } impl MiniMaxProvider { + /// Creates a new `MiniMaxProvider`, reading keys from `MINIMAX_API_KEY`/`MINIMAX_API_TOKEN` + /// and `MINIMAX_CN_API_KEY` environment variables. pub fn new() -> Self { Self { api_key: std::env::var("MINIMAX_API_KEY") @@ -59,6 +62,7 @@ impl MiniMaxProvider { } } + /// Creates a `MiniMaxProvider` with an explicit global-region API key. pub fn with_api_key(api_key: String) -> Self { Self { api_key: Some(api_key), diff --git a/crates/terraphim_usage/src/providers/mod.rs b/crates/terraphim_usage/src/providers/mod.rs index 263f32b48..751cd68ef 100644 --- a/crates/terraphim_usage/src/providers/mod.rs +++ b/crates/terraphim_usage/src/providers/mod.rs @@ -1,12 +1,18 @@ +/// Provider adapter for Claude Code usage via the `ccusage` tool. #[cfg(feature = "providers")] pub mod ccusage; +/// Provider adapter for Claude (Anthropic) usage. #[cfg(feature = "providers")] pub mod claude; +/// Provider adapter for Kimi (Moonshot AI) usage. #[cfg(feature = "providers")] pub mod kimi; +/// Provider adapter for MiniMax usage. #[cfg(feature = "providers")] pub mod minimax; +/// Provider adapter for OpenCode Go local database usage. #[cfg(feature = "providers")] pub mod opencode_go; +/// Provider adapter for Z.ai (ZAI) usage. #[cfg(feature = "providers")] pub mod zai; diff --git a/crates/terraphim_usage/src/providers/opencode_go.rs b/crates/terraphim_usage/src/providers/opencode_go.rs index 0c981f105..587c53669 100644 --- a/crates/terraphim_usage/src/providers/opencode_go.rs +++ b/crates/terraphim_usage/src/providers/opencode_go.rs @@ -1,11 +1,14 @@ use crate::{MetricLine, ProgressFormat, ProviderUsage, Result, UsageError, UsageProvider}; use std::path::PathBuf; +/// Usage provider that reads from the OpenCode Go local SQLite database. pub struct OpenCodeGoProvider { db_path: PathBuf, } impl OpenCodeGoProvider { + /// Creates a new `OpenCodeGoProvider` using the default database path + /// (`~/.local/share/opencode/opencode.db`). pub fn new() -> Self { let home = std::env::var("HOME").unwrap_or_default(); Self { @@ -13,6 +16,7 @@ impl OpenCodeGoProvider { } } + /// Creates an `OpenCodeGoProvider` with an explicit database path (useful for testing). pub fn with_db_path(path: PathBuf) -> Self { Self { db_path: path } } diff --git a/crates/terraphim_usage/src/providers/zai.rs b/crates/terraphim_usage/src/providers/zai.rs index 183428b94..82389a7e9 100644 --- a/crates/terraphim_usage/src/providers/zai.rs +++ b/crates/terraphim_usage/src/providers/zai.rs @@ -59,12 +59,14 @@ struct ZaiSubscriptionItem { next_renew_time: Option, } +/// Usage provider for the Z.ai (ZAI / GLM) subscription platform. pub struct ZaiProvider { api_key: Option, client: reqwest::Client, } impl ZaiProvider { + /// Creates a new `ZaiProvider`, reading the API key from `ZAI_API_KEY` or `GLM_API_KEY`. pub fn new() -> Self { Self { api_key: std::env::var("ZAI_API_KEY") @@ -77,6 +79,7 @@ impl ZaiProvider { } } + /// Creates a `ZaiProvider` with an explicit API key. pub fn with_api_key(api_key: String) -> Self { Self { api_key: Some(api_key), diff --git a/crates/terraphim_usage/src/store.rs b/crates/terraphim_usage/src/store.rs index 142b10812..43ff8fced 100644 --- a/crates/terraphim_usage/src/store.rs +++ b/crates/terraphim_usage/src/store.rs @@ -287,8 +287,11 @@ pub struct BudgetSnapshotRecord { #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum BudgetVerdict { + /// Spending is below 80% of the monthly budget. WithinBudget, + /// Spending is between 80% and 100% of the monthly budget. ApproachingLimit, + /// Spending has reached or exceeded the monthly budget. Exceeded, } diff --git a/crates/terraphim_validation/src/lib.rs b/crates/terraphim_validation/src/lib.rs index b32cf49f3..392c0ed0f 100644 --- a/crates/terraphim_validation/src/lib.rs +++ b/crates/terraphim_validation/src/lib.rs @@ -10,6 +10,7 @@ #![allow(unused)] #![allow(ambiguous_glob_reexports)] #![allow(clippy::all)] +#![allow(missing_docs)] pub mod artifacts; pub mod orchestrator;