Include Luna AI Assistant design docs covering channels, configuration, core architecture, memory, scheduler, and skills. Add reference docs from OpenClaw and ZeroClaw projects, plus Mistral and OpenAI API specs.
5.5 KiB
Agent Loop & Tools
The ZeroClaw agent architecture is built around a recursive execution loop that allows an LLM to invoke external capabilities, observe their results, and refine its response until a final answer is reached.
Tool Trait
The Tool trait in tools/traits.rs defines the interface for any capability exposed to the agent. It provides both the metadata needed for LLM registration and the execution logic for the tool itself.
/// Result of a tool execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub success: bool,
pub output: String,
pub error: Option<String>,
}
/// Description of a tool for the LLM
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSpec {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
/// Core tool trait — implement for any capability
#[async_trait]
pub trait Tool: Send + Sync {
/// Tool name (used in LLM function calling)
fn name(&self) -> &str;
/// Human-readable description
fn description(&self) -> &str;
/// JSON schema for parameters
fn parameters_schema(&self) -> serde_json::Value;
/// Execute the tool with given arguments
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult>;
/// Get the full spec for LLM registration
fn spec(&self) -> ToolSpec {
ToolSpec {
name: self.name().to_string(),
description: self.description().to_string(),
parameters: self.parameters_schema(),
}
}
}
Agent Loop
The core logic resides in agent/loop_.rs within run_tool_call_loop. This function orchestrates the interaction between the provider and the tool registry.
The process follows a repeating cycle:
- Send the current conversation history and tool definitions to the LLM.
- Receive a response.
- Parse the response for tool calls (native or prompt-guided).
- If tool calls exist, execute them and append the results to the history, then repeat.
- If no tool calls exist, return the final text response.
Iteration Safety
To prevent infinite loops or runaway execution, the system enforces a strict iteration cap:
DEFAULT_MAX_TOOL_ITERATIONS= 10
Streaming and Progress
Streaming responses are handled by accumulating chunks before relaying them to the draft channel to minimize noise:
STREAM_CHUNK_MIN_CHARS= 80PROGRESS_MIN_INTERVAL_MS= 500 (minimum time between progress updates)DRAFT_CLEAR_SENTINEL=\x00CLEAR\x00(used to clear progress lines before showing the final answer)
Auto-Compaction
Conversation history is managed through an auto-compaction mechanism to stay within context window limits while preserving essential information.
- Threshold: Triggered when non-system message count exceeds 50 (
DEFAULT_MAX_HISTORY_MESSAGES). - Retention: Keeps the 20 most recent messages (
COMPACTION_KEEP_RECENT_MESSAGES). - Summarization: Older messages are summarized by the LLM.
- Caps: Summarization source is capped at 12,000 characters; the resulting summary is capped at 2,000 characters.
Security & Pattern Matching
Credential Scrubbing
ZeroClaw proactively redacts sensitive information from tool outputs before they are fed back into the LLM history.
static SENSITIVE_KV_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"(?i)(token|api[_-]?key|password|secret|user[_-]?key|bearer|credential)["']?\s*[:=]\s*(?:"([^"]{8,})"|'([^']{8,})'|([a-zA-Z0-9_\-\.]{8,}))"#).unwrap()
});
The scrubbing logic preserves the first 4 characters for context then appends *[REDACTED].
Autosave
Meaningful exchanges are automatically persisted to memory based on message length:
AUTOSAVE_MIN_MESSAGE_CHARS= 20
Tool Serialization & Formats
Tools are converted into the appropriate format for the specific LLM provider.
tools_to_openai_format()
This function serializes the tool registry into the standard OpenAI function-calling schema used by many providers.
fn tools_to_openai_format(tools_registry: &[Box<dyn Tool>]) -> Vec<serde_json::Value> {
tools_registry
.iter()
.map(|tool| {
serde_json::json!({
"type": "function",
"function": {
"name": tool.name(),
"description": tool.description(),
"parameters": tool.parameters_schema()
}
})
})
.collect()
}
Parsing Priority
The system handles multiple response formats to support different providers:
- Native structured JSON (OpenAI style)
- XML tags (
<tool_call>,<invoke>) - Markdown code blocks with
tool_callidentifiers - Provider-specific shortened formats (GLM, etc.)
Relevance to Luna
Luna currently operates as a pure chat relay, streaming LLM responses without any capability to interact with the system or external APIs. This lack of a tool execution layer is the primary bottleneck preventing Luna from becoming an autonomous assistant.
Bridging the Gap
Implementing the agent loop pattern in Luna would involve:
- Defining an
IToolinterface in C# that mirrors the RustTooltrait. - Transitioning from a single request-response model to a recursive loop in the Core engine.
- Integrating the tool loop with Skills to allow dynamic capability discovery.
The implementation of this protocol will turn Luna from a passive interface into an active participant capable of executing commands and managing its own Memory.