Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ Filters are applied in order: `TextOnly` / `ExcludeAgents` first, then `MaxTurnA
| `MaxTurnAge` | int | `0` | Keep only messages from the last N agent turns (each turn ends at an assistant reply). Applied after `TextOnly`/`ExcludeAgents` and before `MaxTailMessages`. Semantic alternative to a raw message count — discards entire early-session phases rather than an arbitrary number of messages. `0` means no limit. |
| `MaxTailMessages` | int | `0` | After the above filters, keep only the last N messages. `0` means no limit. |
| `ContextCapFraction` | double | `0.0` | Soft-cap threshold expressed as a fraction of `MaxTailMessages` (e.g. `0.8` = 80%). When the filtered count exceeds this threshold a `context_cap_warning` event is emitted. Does not change trim behavior — use `MaxTailMessages` to hard-cap. `0.0` disables the warning. |
| `MaxToolResultChars` | int | `0` | Truncate `FunctionResultContent` strings in the replayed history slice to this many characters. A suffix noting the omitted count is appended. `0` disables truncation. See [context-management — Tool-result truncation](context-management.md#tool-result-truncation-maxtoolresultchars). |
| `ToolResultCharOverrides` | object | `{}` | Per-tool-name character cap overrides. Keys are tool function names (case-insensitive); values are the character limit for that tool's results, overriding `MaxToolResultChars`. A value of `0` disables truncation for that tool. Only meaningful when `MaxToolResultChars` is also set. |
| `MaxReplayChars` | int | `0` | Truncate non-summary assistant messages in the replayed history to this many characters. `0` uses the global 2,000-character fallback. Compaction summaries are never truncated. |

**`TextOnly: true`** is the primary lever for context reduction. A Reviewer that independently re-reads files and re-runs commands gains nothing from hundreds of tool results produced by the Developer — stripping them can reduce input tokens by 90%+ in typical sessions.

Expand Down Expand Up @@ -721,11 +724,11 @@ Compaction:
| `Model` | object | first agent's model | Model used for generating the summary (`llm` and `hybrid` modes only). |
| `Mode` | string | `"llm"` | Compaction mode. See below. |
| `TokenBudget` | int | `80000` | Estimated token budget for `window` mode. Oldest message pairs are dropped until the total estimated token count (characters ÷ 4) falls within this limit. Ignored by all other modes. |
| `IncludeReasoning` | bool | `false` | When `true`, reasoning excerpts from the compacted turns are prepended to the summary as a `[REASONING EXCERPTS]` block. Each excerpt is truncated to ~500 tokens so agents resuming after compaction can see the WHY behind prior decisions. Reads `reasoning` events from the session events log (`Events.Path`). Has no effect when `Events` is not configured. |
| `IncludeSymbolGraph` | bool | `false` | When `true`, a `[SYMBOL DEPENDENCY GRAPH]` block is prepended to the summary (before `[REASONING EXCERPTS]` when both are enabled). The block lists every `SymbolDefinition` and `SymbolReference` node in the evidence graph for files written during the session, giving agents an explicit map of what symbols were in scope. Requires `EvidenceStore` and `ChangeTracking` to be configured. |
| `IncludeReasoning` | bool | `true` | Prepends a `[REASONING EXCERPTS]` block to the compaction summary. Each excerpt is truncated to ~500 tokens so agents resuming after compaction can see the WHY behind prior decisions. Reads `reasoning` events from the session events log (`Events.Path`). Omitted silently when `Events` is not configured or contains no reasoning events. Set to `false` to suppress. |
| `IncludeSymbolGraph` | bool | `true` | Prepends a `[SYMBOL DEPENDENCY GRAPH]` block to the summary (before `[REASONING EXCERPTS]` when both are enabled). Lists every `SymbolDefinition` and `SymbolReference` node in the evidence graph for files written during the session. Omitted silently when no evidence store is wired or no symbol nodes are found. Requires `EvidenceStore` and `ChangeTracking` to be configured. Set to `false` to suppress. |
| `MaxCharsPerHistoryMessage` | int | `8000` | Maximum characters to include from any single message when building the history text passed to the LLM summarizer. Messages that exceed this limit are truncated and annotated with a `[TRUNCATED]` marker; any tool calls recorded for that turn are appended as a compact one-line list so the summarizer still knows what happened. Set to `0` to disable truncation. |
| `AntiThrashMinSavingsRatio` | float | `0.10` | Minimum savings ratio (0–1) a compaction must achieve to count as effective. If the last `AntiThrashWindow` compactions all saved less than this fraction of the conversation, `ShouldCompact` returns `false` until the history grows past the trigger again. Prevents repeated LLM calls that reduce size by less than 10%. Set to `0` to disable. |
| `AntiThrashWindow` | int | `3` | Number of recent compaction outcomes to examine for the anti-thrash guard. The guard only suppresses compaction once this many outcomes have been recorded. Set to `0` to disable. |
| `AntiThrashWindow` | int | `10` | Number of recent compaction outcomes to examine for the anti-thrash guard. The guard only suppresses compaction once this many outcomes have been recorded. Set to `0` to disable. |
| `SummaryTemplate` | string | built-in | Custom Liquid-style template for the LLM summary prompt. Supports `{{$task}}`, `{{$turn_count}}`, `{{$change_log}}`, and `{{$history}}` substitutions. When omitted, the built-in structured template is used — see [Compaction summary template](#compaction-summary-template). |

**Compaction modes**
Expand Down
54 changes: 36 additions & 18 deletions docs/context-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ Agents:
MaxTailMessages: 40 # hard cap after the above filters
ContextCapFraction: 0.8 # emit context_cap_warning when at 80% of MaxTailMessages
MaxToolResultChars: 8000 # truncate individual tool results in replayed history
ToolResultCharOverrides: # raise the cap for specific tools
search_content: 20000
grep_file: 20000
MaxReplayChars: 4000 # truncate verbose assistant messages in replayed history
```

### TextOnly
Expand Down Expand Up @@ -201,13 +205,22 @@ Hard cap applied after the other filters. When the filtered list still exceeds t
the oldest messages are dropped. Set `ContextCapFraction` to receive a `context_cap_warning`
event as an early signal before the hard cap is reached.

### Replay truncation
### Replay truncation (`MaxReplayChars`)

Agents sometimes produce verbose stream-of-consciousness output (3–5k tokens). When that text
is replayed verbatim in every subsequent turn, compaction summaries grow each cycle and input
tokens balloon. fuseraft automatically truncates verbose non-summary assistant messages to
2,000 characters when replaying them into the next turn's history. Compaction summaries are
never truncated.
tokens balloon. fuseraft truncates verbose non-summary assistant messages to 2,000 characters
by default when replaying them; set `MaxReplayChars` to override this cap per agent.
Compaction summaries are never truncated regardless of this setting.

```yaml
Agents:
- Name: Developer
ContextWindow:
MaxReplayChars: 4000 # truncate replayed assistant messages to 4 000 chars
```

Default: `0` (uses the global 2,000-character fallback).

### Tool-result truncation (`MaxToolResultChars`)

Expand All @@ -225,9 +238,12 @@ Agents:
- Name: Developer
ContextWindow:
MaxToolResultChars: 8000 # truncate tool results in replayed history to 8 000 chars
ToolResultCharOverrides: # per-tool overrides (search tools can afford a higher cap)
search_content: 20000
grep_file: 20000
```

Default: `0` (no truncation).
Default: `0` (no truncation). `ToolResultCharOverrides` is only meaningful when `MaxToolResultChars` is also set; a value of `0` in the overrides map disables truncation for that specific tool entirely.

**Consumed-read optimisation:** fuseraft distinguishes between `read_file` results that
the agent has already acted on and those that are still load-bearing:
Expand Down Expand Up @@ -396,23 +412,25 @@ Compaction:
Two optional flags add structured context blocks before the LLM summary text. Both are
prefixed in this order when both are enabled: symbol graph first, then reasoning excerpts.

**`IncludeReasoning`** — prepends a `[REASONING EXCERPTS]` block containing the model's
thinking for each compacted turn (truncated to ~500 tokens per turn). Useful when the *why*
behind prior decisions matters as much as the *what*. Requires `Events` to be configured
(reasoning excerpts are read from the session events log).
**`IncludeReasoning`** (default `true`) — prepends a `[REASONING EXCERPTS]` block containing
the model's thinking for each compacted turn (truncated to ~500 tokens per turn). Useful when
the *why* behind prior decisions matters as much as the *what*. Requires `Events` to be
configured (reasoning excerpts are read from the session events log). When the events log is
absent or contains no reasoning events the block is omitted silently.

**`IncludeSymbolGraph`** — prepends a `[SYMBOL DEPENDENCY GRAPH]` block listing every
`SymbolDefinition` and `SymbolReference` node in the evidence store for files written during
the session. Gives agents an explicit map of what symbols were in scope during the compacted
turns. Requires `EvidenceStore` and `ChangeTracking` to be configured.
**`IncludeSymbolGraph`** (default `true`) — prepends a `[SYMBOL DEPENDENCY GRAPH]` block
listing every `SymbolDefinition` and `SymbolReference` node in the evidence store for files
written during the session. Gives agents an explicit map of what symbols were in scope during
the compacted turns. Requires `EvidenceStore` and `ChangeTracking` to be configured. When no
evidence store is wired the block is omitted silently.

```yaml
Compaction:
TriggerTurnCount: 40
KeepRecentTurns: 8
Mode: hybrid
IncludeReasoning: true
IncludeSymbolGraph: true
IncludeReasoning: true # default; set to false to suppress
IncludeSymbolGraph: true # default; set to false to suppress
```

### History pre-pruning
Expand Down Expand Up @@ -441,7 +459,7 @@ If repeated compactions save very little — for example, a conversation that is
threshold but whose LLM summary is nearly as long as the history it replaced — fuseraft
suppresses further compaction until the history grows meaningfully.

The guard tracks the savings ratio of the last `AntiThrashWindow` compactions (default 3). If
The guard tracks the savings ratio of the last `AntiThrashWindow` compactions (default 10). If
every entry in that window is below `AntiThrashMinSavingsRatio` (default 10%), `ShouldCompact`
returns `false`. The guard resets automatically as new turns extend the conversation past the
trigger again.
Expand All @@ -450,8 +468,8 @@ trigger again.
Compaction:
TriggerTurnCount: 20
KeepRecentTurns: 5
AntiThrashMinSavingsRatio: 0.15 # suppress if saving less than 15%
AntiThrashWindow: 4 # look at last 4 compactions
AntiThrashMinSavingsRatio: 0.15 # suppress if saving less than 15% (default: 0.10)
AntiThrashWindow: 4 # look at last 4 compactions (default: 10)
```

Set either field to `0` to disable the guard entirely.
Expand Down
13 changes: 6 additions & 7 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ Security:

### What is checked

| Plugin | Argument | Check type |
|--------|----------|-----------|
| `FileSystem` | `path` | Hard deny if resolved path is outside sandbox |
| `FileSystem` | `directory` | Hard deny if resolved path is outside sandbox |
| `Shell` | `workingDirectory` | Hard deny if resolved path is outside sandbox |
| `Shell` | `command` | Best-effort scan for absolute paths escaping sandbox |
| `Shell` | `script` | Best-effort scan for absolute paths escaping sandbox |
| Plugin | Functions / Argument | Check type |
|--------|----------------------|-----------|
| `FileSystem` | `read_file`, `write_file`, `delete_file`, `list_files` — `path` / `directory` | Hard deny if resolved path is outside sandbox |
| `FileSystem` | `patch_file`, `create_directory`, `delete_directory`, `set_permissions`, `copy_file`, `move_file` | Hard deny if resolved path is outside sandbox (always enforced, regardless of whether `FileSystemPermissions` globs are configured) |
| `Shell` | `shell_run`, `shell_run_script` — `workingDirectory` | Hard deny if resolved path is outside sandbox |
| `Shell` | `shell_run`, `shell_run_script` — `command` / `script` | Best-effort scan for absolute paths escaping sandbox |

### Path resolution

Expand Down
9 changes: 9 additions & 0 deletions src/Cli/OrchestratorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,15 @@ t.Pattern is not null ||
chatClientFactory.Create(summaryModel), compactionConfig,
loggerFactory.CreateLogger<ConversationCompactor>(),
resumptionNote, changeLogPath, intentLog, config.Events?.Path, evidenceStore);

if ((compactionConfig.Mode ?? string.Empty).Equals("intent", StringComparison.OrdinalIgnoreCase)
&& intentLog is null)
{
loggerFactory.CreateLogger(nameof(OrchestratorBuilder)).LogWarning(
"Compaction.Mode is 'intent' but no ChangeTracking.IntentLogPath is configured — " +
"compaction will fall back to lossless or LLM mode at runtime. " +
"Set ChangeTracking.IntentLogPath to enable deterministic intent compaction.");
}
}

// Build the post-session skill curator when curation is enabled.
Expand Down
16 changes: 9 additions & 7 deletions src/Core/Models/CompactionConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,21 @@ public record CompactionConfig
/// When <c>true</c>, reasoning excerpts from the compacted turn range are prepended to
/// the compaction summary. Each excerpt is truncated to approximately 500 tokens so agents
/// resuming after compaction can see the WHY behind prior decisions, not just the artifacts.
/// Reads <c>reasoning</c> events from the session's events log. Default: <c>false</c>.
/// Reads <c>reasoning</c> events from the session's events log. When the events log is
/// absent or contains no reasoning events the block is omitted silently.
/// Default: <c>true</c>.
/// </summary>
public bool IncludeReasoning { get; init; } = false;
public bool IncludeReasoning { get; init; } = true;

/// <summary>
/// When <c>true</c>, a symbol dependency graph derived from the session's changed files is
/// prepended to the compaction summary (before reasoning excerpts when both are enabled).
/// Queries <c>SymbolDefinition</c> and <c>SymbolReference</c> nodes from the evidence store
/// for every file written during the session, giving agents an explicit map of what symbols
/// were in scope across the compacted turns. Requires an active <c>EvidenceStore</c>.
/// Default: <c>false</c>.
/// were in scope across the compacted turns. When no evidence store is wired or no symbol
/// nodes are found the block is omitted silently. Default: <c>true</c>.
/// </summary>
public bool IncludeSymbolGraph { get; init; } = false;
public bool IncludeSymbolGraph { get; init; } = true;

/// <summary>
/// Optional custom prompt template for LLM-mode compaction. When set, replaces the
Expand Down Expand Up @@ -111,7 +113,7 @@ public record CompactionConfig
/// <summary>
/// Number of recent compaction outcomes to examine for the anti-thrash guard.
/// Only suppresses compaction once this many outcomes have been recorded.
/// Default: <c>3</c>. Set to <c>0</c> to disable the anti-thrash check.
/// Default: <c>10</c>. Set to <c>0</c> to disable the anti-thrash check.
/// </summary>
public int AntiThrashWindow { get; init; } = 3;
public int AntiThrashWindow { get; init; } = 10;
}
34 changes: 34 additions & 0 deletions src/Core/Models/ContextWindowConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,38 @@ public sealed record ContextWindowConfig
/// Default: <c>0</c> (no truncation).
/// </summary>
public int MaxToolResultChars { get; init; }

/// <summary>
/// Maximum characters to replay from a single non-summary assistant message in the
/// history slice passed to this agent. When an assistant message text exceeds this limit
/// the content is truncated and annotated with the omitted character count.
///
/// <para>
/// Agents sometimes produce multi-thousand-character reasoning blocks that are replayed
/// verbatim on every subsequent turn, compounding input-token growth. Compaction-summary
/// messages are never truncated regardless of this setting.
/// </para>
///
/// Default: <c>0</c> (uses the global 2,000-char fallback applied during session replay).
/// </summary>
public int MaxReplayChars { get; init; }

/// <summary>
/// Per-tool-name character limit overrides applied during tool result truncation.
/// When a key matches a tool function name (case-insensitive), its value is used as the
/// character cap for that tool's results instead of <see cref="MaxToolResultChars"/>.
///
/// <para>
/// The primary use case is giving search and grep tools a higher limit than file-read
/// tools. For example:
/// <code>
/// "ToolResultCharOverrides": { "search_content": 20000, "grep_file": 20000 }
/// </code>
/// A value of <c>0</c> disables truncation for that tool entirely.
/// </para>
///
/// Only meaningful when <see cref="MaxToolResultChars"/> is also set.
/// Default: empty (no overrides).
/// </summary>
public Dictionary<string, int> ToolResultCharOverrides { get; init; } = [];
}
7 changes: 6 additions & 1 deletion src/Infrastructure/AgentFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,12 @@ public AIAgent Create(AgentConfig config, Action<string, string, string?>? onToo
// Deterministic sliding-window cap: always keep only the last N tool call/result
// pairs in full, replacing older ones with placeholders unconditionally.
// Applied before the budget-reactive trim so the window runs first.
var maxInTurnToolPairs = config.MaxInTurnToolPairs;
// When MaxContextTokens is set but no explicit pair limit is configured, default
// to 12 pairs to prevent O(N²) tool-result accumulation within a turn.
const int DefaultToolPairsWhenBudgeted = 12;
var maxInTurnToolPairs = config.MaxInTurnToolPairs > 0
? config.MaxInTurnToolPairs
: (resolvedModel.MaxContextTokens > 0 ? DefaultToolPairsWhenBudgeted : 0);

// Tool schema overhead: computed once at build time since the tool list is fixed
// for the lifetime of this agent. Included in the context budget and payload
Expand Down
Loading
Loading