Files
goclaw/docs/01-agent-loop.md
thotam 3f90057c24 fix(pipeline): stop aborting runs on heuristic context budget estimates (#1587)
Runs on models without a registered tokenizer (e.g. 9router brand models)
ended with the generic "Agent couldn't generate a response" fallback even
though the real request used about 55% of the context window.

PruneStage counted history with TokenCounter, which falls back to a
chars/2 heuristic for unregistered models and overcounted about 1.8x.
Once over budget it ran memory flush (~35s, invisible in traces), then
mid-loop compaction, which cannot summarize a history made only of tool
call/result pairs. The callback reported the untouched history as
compacted, PruneStage still saw it over budget and returned AbortRun
before any LLM call, and FinalizeStage replaced the empty reply with the
fallback.

- PruneStage and ContextStage overhead count with the request guard's
  BudgetCounter. PruneStage no longer controls loop flow; the final
  request guard in ThinkStage decides.
- CompactMessages returns ErrNotCompacted when history is unchanged.
  Callers stop counting it as a compaction and do not retry it in the
  same run, while post-run summarization still sees the pressure.
- When the guard exhausts every reduction step, ThinkStage stops the run
  with a localized chat.context_budget_exceeded notice instead of an
  error, so the run's tool results are still persisted. The stop reason
  marks the trace and agent span as error; team tasks, cron and
  heartbeat treat it as a failure via RunOutcome.Failure().
- Memory flush and mid-loop compaction emit event spans.
- Web and desktop UIs treat an unset context_pruning as enabled (the
  backend default since 7639a8c0), keep it unset when untouched, and can
  re-enable pruning after it was turned off.
2026-09-29 18:04:08 +07:00

742 lines
35 KiB
Markdown

# 01 - Agent Loop
## Overview
The Agent Loop implements a **Think --> Act --> Observe** cycle. Each agent owns a `Loop` instance configured with a provider, model, tools, workspace, and agent type. A user message enters as a `RunRequest`, passes through the loop, and exits as a `RunResult`.
**V3 Dual Mode**: The loop supports two execution paths:
- **V2 (monolithic)**: Original `runLoop()` function (default for backward compatibility)
- **V3 (pipeline)**: Pluggable 8-stage pipeline (`internal/pipeline/`, enabled via feature flag)
Both paths implement the same external behavior; the difference is internal architecture. The loop iterates up to 20 times: the LLM thinks, optionally calls tools, observes results, and repeats until it produces a final text response.
---
## V3 Pipeline Architecture
When `pipeline_enabled` is true, `Loop.Run()` delegates to `runViaPipeline()`, which orchestrates the v3 pipeline:
```mermaid
flowchart TD
RUN["Loop.Run<br/>runRequest"] --> GATE{pipeline_enabled?}
GATE -->|false| V2["runLoop<br/>v2 monolithic"]
GATE -->|true| V3["runViaPipeline<br/>v3 pipeline"]
V3 --> NEWSTATE["NewRunState<br/>input, nil, model, provider"]
NEWSTATE --> NEWPIPE["NewDefaultPipeline<br/>8 stages"]
NEWPIPE --> PIPE_RUN["Pipeline.Run<br/>setup → iteration loop → finalize"]
PIPE_RUN --> CONVERT["convertRunResult<br/>pResult → RunResult"]
CONVERT --> RESULT["RunResult"]
```
### Stage Execution Order
```
Setup (runs once)
├─ ContextStage: Inject context, compute workspace, ensure per-user files
│
Iteration Loop (max 20 iterations)
├─ ThinkStage: Build system prompt, filter tools, call LLM
├─ PruneStage: Soft/hard trim context, run memory flush if needed
├─ ToolStage: Execute tool calls (parallel)
├─ ObserveStage: Process tool results, append messages
└─ CheckpointStage: Check iteration state, conditionally break
Finalize (runs once, uses background context if cancelled)
└─ FinalizeStage: Sanitize output, flush messages, update metadata
```
### Stage Details
**ContextStage**
- Inject context: `WithAgentID()`, `WithUserID()`, `WithAgentType()`, `WithLocale()`
- Resolve per-user workspace (base + sanitized userID)
- Ensure per-user files exist (idempotent via `sync.Map` cache)
- Persist agent/user IDs on session
**ThinkStage**
- Resolve workspace + context files dynamically
- Build system prompt (15+ sections)
- Inject conversation summary if exists
- Run history pipeline (limitHistoryTurns → sanitizeHistory)
- Filter tools through PolicyEngine (RBAC)
- Call LLM, record span with token counts
- Emit `chunk` events (streaming) or single response
**PruneStage** (pruning enabled by default; disable with `contextPruning.mode: "off"`)
- Count history with the same `BudgetCounter` as ThinkStage's request guard
- If >= 30%, run soft trim pass (keep first/last 3000 chars, replace middle with "...")
- If >= 50%, run hard clear pass (replace with placeholder)
- Run sanitizeHistory to fix broken tool_use/tool_result pairs after prune
- Trigger memory flush (synchronous) if compaction threshold exceeded
- Never stops the run: if compaction cannot bring history under budget, ThinkStage's request guard decides
**ToolStage**
- Execute single tool sequentially (no goroutine overhead)
- Execute eligible read-only multi-tool batches in bounded parallel, then process results in original order
- Keep mutating, async, MCP-bridged, `exec`/`bash`, `wait`, and unknown tools sequential
- Emit `tool.call` before, `tool.result` after
- Record tool span
- Append tool messages to buffer
**ObserveStage**
- Process tool result stream
- Handle `NO_REPLY` convention (silent completion)
- Append assistant message with tool call info
**CheckpointStage**
- Increment iteration counter
- Check if max iterations reached → `BreakLoop`
- Check if context cancelled → `AbortRun`
**FinalizeStage**
- Run 7-step output sanitization pipeline
- Flush buffered messages atomically
- Update session metadata (model, provider, token counts)
- Emit `run.completed` or `run.failed` event
---
## Orchestration Modes
Agents support three orchestration modes that determine which inter-agent tools are available:
### ModeSpawn (Default)
- **Use case**: Single independent agent
- **Tools available**: `spawn` (self-clone child agents)
- **Tools hidden**: `delegate`, `team_tasks`
- **Resolution**: Default when no team or delegate links
### ModeDelegate
- **Use case**: Agent with linked delegate targets
- **Tools available**: `spawn`, `delegate` (dispatch to linked agents)
- **Tools hidden**: `team_tasks`
- **Resolution**: When `agent_links` grants this agent an effective outbound delegation edge
### ModeTeam
- **Use case**: Agent in a team (multiple agents collaborating)
- **Tools available**: `spawn`, `delegate`, `team_tasks` (full team workspace)
- **Tools hidden**: None
- **Resolution**: When `teams` table has a row with agent_id = this agent
**Mode Resolution Priority**: Team > Delegate > Spawn
The system prompt includes relevant details for each mode. Agent Team runs use
their existing shared workspace. Agent Link runs receive only delegation-scoped
`inputs/` and `outputs/` aliases.
---
## Self-Evolution System
Agents can auto-adapt their behavior based on metrics and admin-approved suggestions.
### Evolution Suggestion Engine
Analyzes agent metrics on a periodic schedule (cron job):
1. **LowRetrievalUsageRule** — Detects if `memory_search` or `knowledge_graph_search` is underutilized; suggests enabling vault
2. **ToolFailureRule** — Identifies frequently failing tools; suggests limiting tool set or retraining
3. **RepeatedToolRule** — Detects repetitive tool calls (loop detection); suggests prompt adjustment
### Adaptation Guardrails
**AdaptationGuardrails** struct controls safety limits (stored in `agents.other_config.evolution_guardrails`):
| Field | Default | Purpose |
|-------|---------|---------|
| `max_delta_per_cycle` | 0.1 | Max parameter change per cycle (prevents wild swings) |
| `min_data_points` | 100 | Require at least N metrics before applying suggestion |
| `rollback_on_drop_pct` | 20.0 | Revert if quality drops >20% after applying |
| `locked_params` | [] | Parameter names that cannot auto-change (e.g., "temperature") |
### Suggestion Workflow
1. **SuggestionEngine.Analyze()** evaluates rules against 7-day metrics window
2. Generates `EvolutionSuggestion` records (status="pending")
3. Admin reviews in dashboard, approves/rejects
4. On approval, auto-adapt worker applies suggestion + records baseline metrics
5. Next cycle detects quality regression and auto-rolls back if threshold exceeded
---
## 1. RunRequest Flow (V2 Monolithic - Original)
The full lifecycle of a single agent run is broken into seven phases.
```mermaid
flowchart TD
START([RunRequest]) --> PH1
subgraph PH1["Phase 1: Setup"]
P1A[Increment activeRuns atomic counter] --> P1B[Emit run.started event]
P1B --> P1C[Create trace record]
P1C --> P1D[Inject agentType / userID / agentID into context]
P1D --> P1E0[Compute per-user workspace + WithToolWorkspace]
P1E0 --> P1E[Ensure per-user files via sync.Map cache]
P1E --> P1F[Persist agent + user IDs on session]
end
PH1 --> PH2
subgraph PH2["Phase 2: Input Validation"]
P2A["InputGuard.Scan - 6 injection patterns"] --> P2B["Message truncation at max_message_chars (default 32K)"]
end
PH2 --> PH3
subgraph PH3["Phase 3: Build Messages"]
P3A[Build system prompt - 15+ sections] --> P3B[Inject conversation summary if present]
P3B --> P3C["History pipeline: limitHistoryTurns --> sanitizeHistory"]
P3C --> P3D[Append current user message]
P3D --> P3E[Buffer user message locally - deferred write]
end
PH3 --> PH4
subgraph PH4["Phase 4: LLM Iteration Loop (max 20)"]
P4A[Filter tools via PolicyEngine] --> P4B["Call LLM (ChatStream or Chat)"]
P4B --> P4C[Accumulate tokens + record LLM span]
P4C --> P4D{Tool calls in response?}
P4D -->|No| EXIT[Exit loop with final content]
P4D -->|Yes| PH5
end
subgraph PH5["Phase 5: Tool Execution"]
P5A[Append assistant message with tool calls] --> P5B{Single or multiple tools?}
P5B -->|Single| P5C[Execute sequentially]
P5B -->|Multiple| P5D["Execute in parallel via goroutines, sort results by index"]
P5C & P5D --> P5E["Emit tool.call / tool.result events, record tool spans, save tool messages"]
end
PH5 --> PH4
EXIT --> PH6
subgraph PH6["Phase 6: Response Finalization"]
P6A["SanitizeAssistantContent (7-step pipeline)"] --> P6B["Detect NO_REPLY - suppress delivery if silent"]
P6B --> P6C[Flush all buffered messages atomically to session]
P6C --> P6D[Update metadata: model, provider, token counts]
end
PH6 --> PH7
subgraph PH7["Phase 7: Auto-Summarization"]
P7A{"> 50 messages OR > 75% context window?"}
P7A -->|No| P7D[Skip]
P7A -->|Yes| P7B["Memory flush (synchronous, max 5 iterations, 90s timeout)"]
P7B --> P7C["Summarize in background goroutine (120s timeout)"]
end
PH7 --> POST
subgraph POST["Post-processing"]
PP1[Emit root agent span] --> PP2["Emit run.completed or run.failed"]
PP2 --> PP3[Finish trace]
end
POST --> RESULT([RunResult])
```
### Phase 1: Setup
- Increment the `activeRuns` atomic counter (no mutex -- true concurrency, especially in group chats with `maxConcurrent = 3`).
- Emit a `run.started` event to notify connected clients.
- Create a trace record with a generated trace UUID.
- Propagate context values: `WithAgentID()`, `WithUserID()`, `WithAgentType()`. Downstream tools and interceptors rely on these.
- Compute per-user workspace: `base + "/" + sanitize(userID)`. Inject via `WithToolWorkspace(ctx)` so all filesystem and shell tools use the correct directory.
- Ensure per-user files exist. A `sync.Map` cache guarantees the seeding function runs at most once per user.
- Persist the agent ID and user ID on the session for later reference.
### Phase 2: Input Validation
- **InputGuard**: scans the user message against 6 regex patterns that detect prompt injection attempts. See Section 4 for details.
- **Message truncation**: if the message exceeds `max_message_chars` (default 32,768), the content is truncated and the LLM receives a notification that the input was shortened. The message is never rejected outright.
### Phase 3: Build Messages
- Build the system prompt (15+ sections). Context files are resolved dynamically based on agent type.
- Inject the conversation summary (if one exists from a previous compaction) as the first two messages.
- Run the history pipeline (3 stages, see Section 5).
- Append the current user message. Messages are buffered locally (deferred write) to avoid race conditions with concurrent runs on the same session.
### Phase 4: LLM Iteration Loop
- Filter the available tools through the PolicyEngine (RBAC).
- Call the LLM. Streaming calls emit `chunk` events in real time; non-streaming calls return a single response.
- Record an LLM span for tracing with token counts and timing.
- **Mid-loop compaction**: if prompt tokens exceed 75% of context window (or `MaxHistoryShare` if configured), summarize ~70% of in-memory messages, keeping the last ~30%. This happens during active iterations to prevent context overflow in long-running tasks.
- If the response contains no tool calls, exit the loop.
- If tool calls are present, proceed to Phase 5 and then loop back.
- Maximum iterations before loop forcibly exits (default 20, set via `maxIterations` in agent config or `req.MaxIterations` per-request).
### Phase 5: Tool Execution
- Append the assistant message (with tool calls) to the message list.
- **Single tool call**: execute sequentially (no goroutine overhead).
- **Multiple eligible read-only tool calls**: run raw I/O through a bounded goroutine pool, collect results, then process sequentially in original assistant order.
- **Unsafe or mixed batches**: run sequentially when any call is mutating, async, MCP-bridged, `exec`/`bash`, `wait`, unknown, or over the remaining tool-call budget.
- Run synchronous `PreToolUse` hooks before any raw parallel I/O; blocked calls append synthetic tool messages and are not executed.
- Emit `tool.call` before execution and `tool.result` after.
- Record a tool span for each call. Track async tools (spawn, cron) separately.
- Save tool messages to the session.
### Phase 6: Response Finalization
- Run the 7-step output sanitization pipeline (see Section 3).
- Detect `NO_REPLY` in the final content. If present, suppress message delivery (silent reply).
- Flush all buffered messages atomically to the session (user message, tool messages, assistant message). This prevents concurrent runs from interleaving partial history.
- Update session metadata: model name, provider name, cumulative token counts.
### Phase 7: Auto-Summarization
- **Trigger condition**: the history has more than 50 messages OR the estimated token count exceeds 75% of the context window.
- **Per-session TryLock**: before summarizing, acquire a non-blocking per-session lock. If another concurrent run is already summarizing, skip. This prevents concurrent summarization from corrupting session history.
- **Memory flush first**: run synchronously so the agent can persist durable memories before history is truncated. Max 5 LLM iterations, 90-second timeout.
- **Summarize**: launch a background goroutine with a 120-second timeout. The LLM produces a summary of all messages except the last 4. The summary is saved and the history is truncated to those 4 messages. The compaction counter is incremented.
### Cancel Handling
When the context is cancelled (via `/stop` or `/stopall`), the loop exits immediately:
- Trace finalization uses `context.Background()` fallback when `ctx.Err() != nil` to ensure the final DB write succeeds.
- Trace status is set to `"cancelled"` instead of `"error"`.
- An empty outbound message triggers cleanup (stop typing indicator, clear reactions).
---
## 2. System Prompt
The system prompt is assembled dynamically from 19 sections. Two modes control the amount of content included:
- **PromptFull**: used for main agent runs. Includes all sections.
- **PromptMinimal**: used for sub-agents and cron jobs. Reduced sections (only AGENTS.md and TOOLS.md from bootstrap files).
### Sections (In Build Order)
1. **Identity** -- channel-aware context with platform type (Telegram, Zalo, etc.) and chat type (direct/group).
2. **First-run bootstrap** -- `[MANDATORY]` notice injected if BOOTSTRAP.md is present, forcing immediate execution.
3. **Persona** -- SOUL.md and IDENTITY.md injected early in the "primacy zone" to prevent drift in long conversations.
4. **Tooling** -- core tool descriptions, filtered by policy and sandbox status.
5. **Credentialed CLI** -- optional secure CLI context for credentialed exec tool access.
6. **Safety** -- defensive preamble for handling external content, identity anchoring for predefined agents.
7. **Self-Evolution** -- rules for predefined agents to update SOUL.md (style/tone) from user feedback.
8. **Skills (inline)** -- skill content injected directly when the skill set is small (≤15 skills).
9. **Skills (search mode)** -- use `skill_search` tool when the skill set is large.
10. **MCP Tools (inline)** -- external integration tools with real descriptions.
11. **MCP Tools (search mode)** -- use `mcp_tool_search` when many MCP tools are available.
12. **Workspace** -- working directory path, file structure, sandbox container workdir.
13. **Team Workspace** -- absolute path to shared team workspace (for team agents).
14. **Sandbox** -- Docker container instructions, available commands, policy notes.
15. **User Identity** -- owner IDs for permission checks (full mode only).
16. **Time** -- current UTC date/time for temporal awareness.
17. **Channel Formatting** -- platform-specific output hints (e.g., Zalo → plain text).
18. **Extra Context** -- additional context wrapped in `<extra_context>` tags (subagent context, etc.).
19. **Project Context** -- bootstrap context files (remaining after persona extraction), wrapped in defensive preamble.
20. **Sub-Agent Spawning** -- rules for launching child agents (skipped for team agents with TEAM.md).
21. **Runtime** -- agent ID, session key, provider info, model pricing.
22. **Persona Reminder** -- recency reinforcement to combat "lost in the middle" in long conversations.
23. **Memory Reminders** -- prompts to run memory_search and knowledge_graph_search before answering.
---
## 3. Sanitize Output
A 7-step pipeline cleans raw LLM output before delivering it to the user.
```mermaid
flowchart TD
IN[Raw LLM Output] --> S1
S1["1. Strip garbled tool XML<br/>Remove broken XML tool artifacts<br/>from DeepSeek, GLM, Minimax"] --> S2
S2["2. Strip downgraded tool call text<br/>Remove text-format tool calls:<br/>[Tool Call: ...], [Tool Result ...]"] --> S3
S3["3. Strip thinking tags<br/>Remove reasoning tags:<br/>think, thinking, thought, antThinking"] --> S4
S4["4. Strip final wrapper tags<br/>Remove final tag wrappers,<br/>preserve inner content"] --> S5
S5["5. Strip echoed system messages<br/>Remove hallucinated<br/>[System Message] blocks"] --> S6
S6["6. Collapse consecutive duplicates<br/>Deduplicate repeated paragraphs<br/>caused by model stuttering"] --> S7
S7["7. Strip leading blank lines<br/>Remove leading whitespace lines"] --> TRIM
TRIM["Trim whitespace"] --> OUT[Clean Output]
```
### Step Details
1. **Garbled tool XML** — Some models (DeepSeek, GLM, Minimax) emit tool-call XML as plain text instead of proper structured tool calls. Tags like `<tool_call>`, `<function_call>`, `<tool_use>`, `<minimax:tool_call>`, and `<parameter name=...>` are stripped. If the entire response consists of garbled XML, an empty string is returned.
2. **Downgraded tool call text** — Text-format tool calls such as `[Tool Call: ...]`, `[Tool Result ...]`, and `[Historical context: ...]` are removed along with any accompanying JSON arguments. Scanning is line-by-line.
3. **Thinking tags** — Internal reasoning tags (`<think>`, `<thinking>`, `<thought>`, `<antThinking>`) are stripped. Case-insensitive, non-greedy matching.
4. **Final wrapper tags** — `<final>` and `</final>` wrapper tags are removed while the inner content is preserved.
5. **Echoed system messages** — `[System Message]` blocks that the LLM hallucinates or echoes back are stripped by scanning line by line until an empty line is reached.
6. **Consecutive duplicate blocks** — Paragraphs that repeat back-to-back (model stuttering) are collapsed. Each block is compared against its predecessor after splitting on `\n\n`.
7. **Leading blank lines** — Whitespace-only lines at the start of the output are removed while preserving indentation in the remaining content.
---
## 4. Input Guard
The Input Guard detects prompt injection attempts in user messages. It is a detection system -- by default it logs warnings but does not block requests.
### 6 Detection Patterns
| Pattern | Description | Example |
|---------|-------------|---------|
| `ignore_instructions` | Attempts to override prior instructions | "Ignore all previous instructions" |
| `role_override` | Attempts to redefine the agent's role | "You are now a different assistant" |
| `system_tags` | Injection of fake system-level tags | `<\|im_start\|>system`, `[SYSTEM]` |
| `instruction_injection` | Insertion of new directives | "New instructions:", "override:" |
| `null_bytes` | Null byte injection | `\x00` characters in the message |
| `delimiter_escape` | Attempts to escape context boundaries | "end of system", `</instructions>` |
### 4 Action Modes
| Action | Behavior |
|--------|----------|
| `"off"` | Scanning disabled entirely |
| `"log"` | Log at info level (`security.injection_detected`), continue processing |
| `"warn"` (default) | Log at warn level (`security.injection_detected`), continue processing |
| `"block"` | Log at warn level and return an error, halting the request |
All security events use the `slog.Warn("security.injection_detected")` convention.
---
## 5. History Pipeline
The history pipeline prepares conversation history before sending it to the LLM. It runs in two sequential stages. Context pruning is handled separately by PruneStage (opt-in via `contextPruning.mode: "cache-ttl"`).
```mermaid
flowchart TD
RAW[Raw Session History] --> S1
S1["Stage 1: limitHistoryTurns<br/>Keep the last N user turns<br/>plus their associated assistant/tool messages"] --> S2
S2["Stage 2: sanitizeHistory<br/>Repair broken tool_use / tool_result pairing<br/>after truncation"] --> OUT[Cleaned History]
```
### Stage 1: limitHistoryTurns
Takes the raw session history and a `historyLimit` parameter. Keeps only the last N user turns along with all associated assistant and tool messages that belong to those turns. Earlier messages are discarded.
### Stage 2: sanitizeHistory
Repairs tool message pairing that may have been broken by truncation or compaction:
1. Skip orphaned tool messages at the beginning of history (no preceding assistant message).
2. For each assistant message that contains tool calls, collect the expected tool_call IDs.
3. Validate that the following tool messages match those expected IDs. Drop mismatched tool messages.
4. Synthesize missing tool results with placeholder text: `"[Tool result missing -- session was compacted]"`.
---
## 6. Context Pruning
Context pruning reduces oversized tool results using a 2-pass algorithm. **It is enabled by default** (an unset `contextPruning.mode` prunes like `"cache-ttl"`, without the prompt-cache TTL gate); set `contextPruning.mode: "off"` to disable it with zero overhead. Owned by PruneStage in the agent pipeline.
```mermaid
flowchart TD
START[Check mode == cache-ttl?] --> GATE{Mode enabled?}
GATE -->|No| SKIP[No pruning - zero overhead]
GATE -->|Yes| CHECK{Ratio >= softTrimRatio 0.3?}
CHECK -->|No| DONE[No pruning needed]
CHECK -->|Yes| PASS1
PASS1["Pass 1: Soft Trim<br/>For each eligible tool result > 6000 chars:<br/>Keep first 3000 chars + last 3000 chars<br/>Replace middle with '...'"]
PASS1 --> CHECK2{"Ratio >= hardClearRatio 0.5?"}
CHECK2 -->|No| DONE
CHECK2 -->|Yes| PASS2
PASS2["Pass 2: Hard Clear<br/>Replace entire tool result content<br/>with '[Old tool result content cleared]'<br/>Stop when ratio drops below threshold"]
PASS2 --> DONE
```
### Configuration
Disable pruning by setting `contextPruning.mode` in agent defaults (per-agent `context_pruning` overrides it):
```json5
agents: {
defaults: {
contextPruning: { mode: "off" }
}
}
```
### Defaults
| Parameter | Default | Description |
|-----------|---------|-------------|
| `mode` | `""` (enabled) | `""` or `"cache-ttl"` = enabled; `"off"` = disabled |
| `keepLastAssistants` | 3 | Number of recent assistant messages protected from pruning |
| `softTrimRatio` | 0.3 | Token ratio threshold to trigger Pass 1 |
| `hardClearRatio` | 0.5 | Token ratio threshold to trigger Pass 2 |
| `minPrunableToolChars` | 50,000 | Minimum tool result length eligible for hard clear |
### Protected Zone
The following messages are never pruned:
- System messages
- The last N assistant messages (default: 3)
- The first user message in the conversation
---
## 7. Auto-Summarize and Compaction
The system uses a two-stage compaction strategy: **mid-loop** (during active iterations) and **post-run** (after completion).
### Mid-Loop Compaction (During Iteration)
When in-memory messages exceed 75% of context window during LLM iterations, the agent immediately summarizes the first ~70% of messages in place, keeping the last ~30%. This prevents context overflow in long-running tasks without waiting for post-run summarization.
```
Threshold: prompt_tokens >= contextWindow * 0.75 (configurable via MaxHistoryShare)
Trigger: Once per run, inside the iteration loop (between LLM calls)
Output: In-memory messages replaced with [summary] + [recent 4 messages]
```
If there is no clean split point (for example the history is only tool call/result pairs), compaction returns `ErrNotCompacted` and nothing is recorded as compacted. When the final request still exceeds the budget after every reduction step (prune, compact, shrink memory), ThinkStage stops the run with the localized `chat.context_budget_exceeded` notice instead of an error: the run's tool results are still persisted, and the trace is marked `error` with the stop reason. Memory flush and compaction attempts appear in the trace as `memory_flush` and `mid_loop_compaction` event spans.
### Post-Run Compaction (After Completion)
When the session history exceeds thresholds **after** a run completes, the session is compacted in the background.
```mermaid
flowchart TD
CHECK{"> 50 messages OR<br/>> 75% context window?"}
CHECK -->|No| SKIP[Skip compaction]
CHECK -->|Yes| LOCK["Per-session non-blocking lock<br/>(skip if another run already compacting)"]
LOCK -->|Lock acquired| FLUSH
LOCK -->|Already locked| SKIP
FLUSH["Step 1: Memory Flush (synchronous)<br/>Embedded agent turn with write_file tool<br/>Agent stores durable memories before truncation<br/>Uses PromptMinimal mode<br/>Max 5 iterations, 90s timeout"]
FLUSH --> SUMMARIZE
SUMMARIZE["Step 2: Summarize (background goroutine)<br/>Keep last 4 messages<br/>LLM summarizes older messages<br/>temp=0.3, max_tokens=1024, timeout 120s"]
SUMMARIZE --> SAVE
SAVE["Step 3: Save<br/>SetSummary() + TruncateHistory(4)<br/>IncrementCompaction()"]
```
### Summary Reuse
On the next request, the saved summary is injected at the beginning of the message list as two messages:
1. `{role: "user", content: "[Summary of earlier conversation]\n{summary}"}`
2. `{role: "assistant", content: "I understand the context..."}`
This gives the LLM continuity without replaying the full history. Protected zone: the last 3 assistant messages are never pruned.
---
## 8. Memory Flush
Memory flush runs **synchronously before post-run compaction** to give the agent an opportunity to persist important information before session history is truncated.
### Trigger Conditions
- **Primary**: compaction is about to run (message count or token ratio exceeded).
- **Token threshold**: only runs when session tokens are significant enough to warrant capture.
- **Deduplication**: runs at most once per compaction cycle, tracked by comparing compaction counter.
### Mechanism
An embedded agent turn with special configuration:
- **System prompt mode**: `PromptMinimal` (stripped-down context).
- **Message window**: latest 10 messages only (not the full history).
- **Available tools**: `write_file` and `read_file` for memory file operations.
- **Default prompt**: "Pre-compaction memory flush. Store durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed). If nothing to store, reply with NO_REPLY."
- **Output handling**: recognizes `NO_REPLY` convention (silent completion).
### Timing
- **Synchronous blocking**: blocks the entire post-run path until flush LLM call completes.
- **Timeout**: 90 seconds for the entire flush turn (5 max iterations).
- **Configurable**: can be disabled or customized via `compaction.memory_flush` config section.
### Results
The agent can write findings to `memory/YYYY-MM-DD.md` files. These persist across session compaction and are available to future sessions via `memory_search` and `memory_get` tools.
---
## 9. Agent Router
The Agent Router manages Loop instances with a cache layer. It supports lazy resolution, TTL-based expiration, and run abort.
```mermaid
flowchart TD
GET["Router: get agent"] --> CACHE{"Cache hit<br/>and TTL valid?"}
CACHE -->|Yes| RETURN[Return cached Loop]
CACHE -->|No or Expired| RESOLVE{"Resolver configured?"}
RESOLVE -->|No| ERR["Error: agent not found"]
RESOLVE -->|Yes| DB["Resolver: load from DB, create Loop"]
DB --> STORE[Store in cache with TTL]
STORE --> RETURN
```
### Cache Invalidation
Invalidating an agent removes it from the cache, forcing the next request to re-resolve from the database.
### Active Run Tracking
| Operation | Behavior |
|-----------|----------|
| Register run | Record a new active run with its agent, session, and cancellation handle |
| Abort run | Cancel a specific run; verifies session key ownership before aborting |
| Abort session runs | Cancel all active runs belonging to a session |
---
## 10. Resolver
The Resolver lazy-creates Loop instances from PostgreSQL data when the Router encounters a cache miss.
```mermaid
flowchart TD
MISS["Router cache miss"] --> LOAD["Step 1: Load agent from DB"]
LOAD --> PROV["Step 2: Resolve provider<br/>Fallback: first provider in registry"]
PROV --> BOOT["Step 3: Load bootstrap files from store"]
BOOT --> DEFAULTS["Step 4: Apply defaults<br/>contextWindow: 200K, maxIterations: 20"]
DEFAULTS --> CREATE["Step 5: Create Loop with resolved config"]
CREATE --> WIRE["Step 6: Wire hooks<br/>(user files, context file loader)"]
WIRE --> DONE["Return Loop to Router for caching"]
```
### Resolved Properties
- **Provider**: looked up by name from the provider registry. Falls back to the first registered provider if not found.
- **Bootstrap files**: loaded from the workspace directory. Standard files: AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md, BOOTSTRAP.md. Additional files (MEMORY.md, USER_PREDEFINED.md, DELEGATION.md, TEAM.md, AVAILABILITY.md) loaded separately as needed. Per-user files (USER.md) are created on first chat.
- **Agent type**: `open` (per-user context, seeded from template files) or `predefined` (agent-level context plus per-user USER.md overlay).
- **Per-user seeding**: Template files are seeded on first chat, idempotent — skips files that already exist. A database-level check distinguishes genuine new users from returning ones, triggering seeding only once.
- **Dynamic context loading**: Context files are resolved based on agent type and request context, with truncated content for system prompt injection. Open agents load per-user workspace files; predefined agents load agent-level files plus per-user USER.md.
- **Custom tools**: Each agent gets its own isolated clone of the tool registry with any per-agent custom tools appended.
- **Team context**: auto-resolved for agents that belong to a team. Lead agents get the team workspace as default workspace; non-lead members keep their own workspace with team workspace accessible via absolute path tool context.
---
## 11. Team Workspace Handling
Agents that belong to a team have access to shared team workspaces for collaboration.
### Workspace Resolution
**For dispatched tasks** (via `req.TeamWorkspace`):
- The team workspace becomes the **default workspace** for relative path operations
- All file tools (read_file, write_file, list_files) use team workspace by default
- Agent workspace is still accessible via `WithToolTeamWorkspace()` context for absolute-path access
**For direct chat** (auto-resolved via team membership):
- Lead agents get team workspace as their default workspace (primary job is team coordination)
- Non-lead member agents keep their own workspace as default
- Team workspace is accessible via `WithToolTeamWorkspace()` context
### Path Scoping
- **Shared workspace mode** (team.settings.shared_workspace): all agents in team share single workspace
- **Isolated workspace mode** (default): each agent gets a workspace scoped by `(teamID, chatID)` or `(teamID, userID)`
### Context Variables
During runs with team context, the following values are injected so tools can resolve the correct paths and scopes:
- Shared team workspace path (absolute, for cross-member file access)
- Effective default workspace (team or agent workspace depending on role)
- Team UUID (for team-scoped tool operations)
- Active task ID (for workspace file auto-linking during dispatched tasks)
---
## 12. Event System
The Loop publishes events via an `onEvent` callback. The WebSocket gateway forwards these as `EventFrame` messages to connected clients for real-time progress tracking.
### Event Types
| Event | When | Payload |
|-------|------|---------|
| `run.started` | Run begins | `{"message": "..."}` |
| `activity` | Phase transitions | `{"phase": "thinking"|"tool_exec"|"compacting", "iteration": N}` |
| `chunk` | Streaming: each text fragment from the LLM | `{"content": "..."}` |
| `thinking` | Streaming: thinking tokens (extended thinking models) | `{"content": "..."}` |
| `tool.call` | Tool execution begins | `{"name": "...", "id": "...", "arguments": {...}}` |
| `tool.result` | Tool execution completes | `{"name": "...", "id": "...", "is_error": bool, "result": "..."}` |
| `block.reply` | Intermediate assistant content during tool iterations | `{"content": "..."}` |
| `run.retrying` | LLM provider retry after failure | `{"attempt": N, "maxAttempts": M, "error": "..."}` |
| `run.completed` | Run finishes successfully | `{"content": "...", "usage": {...}}` |
| `run.failed` | Run finishes with an error | `{"error": "..."}` |
### Event Flow
```mermaid
sequenceDiagram
participant L as Agent Loop
participant GW as Gateway
participant C as WebSocket Client
L->>GW: emit(run.started)
GW->>C: EventFrame
loop LLM Iterations
L->>GW: emit(chunk) x N
GW->>C: EventFrame x N
L->>GW: emit(tool.call)
GW->>C: EventFrame
L->>GW: emit(tool.result)
GW->>C: EventFrame
end
L->>GW: emit(run.completed)
GW->>C: EventFrame
```
---
## 13. Tracing
Every agent run produces a trace with a hierarchy of spans for debugging, analysis, and cost tracking.
### Span Hierarchy
```mermaid
flowchart TD
T["Trace (one per Run)"] --> A["Root Agent Span<br/>Covers the entire run duration"]
A --> L1["LLM Span #1<br/>provider, model, iteration number"]
A --> T1["Tool Span #1a<br/>tool name, duration"]
A --> T2["Tool Span #1b<br/>tool name, duration"]
A --> L2["LLM Span #2<br/>provider, model, iteration number"]
A --> T3["Tool Span #2a<br/>tool name, duration"]
```
### 3 Span Types
| Span Type | Description |
|-----------|-------------|
| **Root Agent Span** | Parent span covering the full run. Contains agent ID, session key, and final status. |
| **LLM Call Span** | One per LLM invocation. Records provider, model, token counts (input/output), and duration. |
| **Tool Call Span** | One per tool execution. Records tool name, whether it errored, and duration. |
### Verbose Mode
Enabled via the `GOCLAW_TRACE_VERBOSE=1` environment variable.
| Field | Normal Mode | Verbose Mode |
|-------|-------------|--------------|
| `OutputPreview` | First 500 characters | First 500 characters |
| `InputPreview` | Not recorded | Full LLM input messages as JSON, truncated at 50,000 characters |
---
## 14. File Reference
| Module | Path | Purpose |
|---|---|---|
| Agent loop & pipeline | `internal/agent/` | V2 runLoop, V3 pipeline adapter, system prompt, resolver, input guard, sanitize, compaction, tracing, orchestration mode, suggestion engine |
| V3 pipeline stages | `internal/pipeline/` | 8-stage pipeline (context→think→prune→tool→observe→checkpoint→finalize→memory flush), RunState, MessageBuffer |
| Memory consolidation & vault | `internal/consolidation/`, `internal/vault/` | Episodic/semantic/dreaming workers, vault retriever, L0 auto-injector, wikilinks, FS sync |
| Infrastructure | `internal/eventbus/`, `internal/tokencount/`, `internal/workspace/`, `internal/bootstrap/` | DomainEventBus, tiktoken counter, WorkspaceContext resolver, bootstrap file loading |
Use `grep` or your editor's symbol search for specific files.