Summary
Predict which read-only tools the LLM will call and execute them before the model finishes generating, overlapping tool I/O with model inference. This cuts multi-tool turn latency by overlapping serial work.
The Problem
In the current agent loop, the LLM generates a response, then tools execute sequentially, then the LLM generates again:
LLM generating... (800ms)
→ read_file("src/auth.rs") (150ms)
→ grep("password_hash") (200ms)
→ read_file("src/db.rs") (120ms)
LLM generating... (600ms)
Total: ~1.9s of which 470ms is pure waiting on filesystem/network I/O.
If the model is going to call read_file("src/auth.rs"), we already know that from the streaming JSON before generation ends. We could have started reading it 500ms ago.
Proposed Design
Scope: Read-Only Only
Only speculate safe, idempotent tools:
- ✅
read_file
- ✅
grep
- ✅
glob
- ✅
list_dir
- ❌
bash (side effects)
- ❌
edit_file (mutates state)
- ❌
create_file (mutates state)
If the prediction is wrong, the only cost is a wasted read — no state corruption.
How It Works
- Stream parsing: While the LLM streams its response, inspect chunks for the beginning of a
tool_use JSON block.
- Early extraction: As soon as the tool name and arguments are parseable (even partially), check if it is a whitelisted read-only tool.
- Speculative execution: Fire the tool call immediately in a background task, storing the result in a per-turn cache keyed by
(tool_name, arguments_hash).
- Result reuse: When the agent loop reaches the tool execution phase, check the cache first. If the speculative result is ready (or still in-flight), use it directly.
// Pseudo-code
while let Some(delta) = stream.next().await {
// Accumulate into partial tool call buffer
if let Some(predicted) = try_extract_tool_call(&delta) {
if is_read_only(&predicted.name) {
let cache_key = (predicted.name.clone(), predicted.args.clone());
if !speculative_cache.contains(&cache_key) {
let tool = tools.get(&predicted.name).cloned();
tokio::spawn(async move {
if let Some(t) = tool {
let result = t.call(predicted.args).await;
speculative_cache.insert(cache_key, result);
}
});
}
}
}
}
Confidence Threshold
Not every partial match should trigger speculation. Start simple:
- Tool name is fully parsed and matches whitelist
- Arguments are at least 50% parseable (basic heuristic)
- Maximum 3 speculative calls per turn (cap waste)
Later: add a lightweight predictor based on prompt + recent history.
Measurement-First Approach
Do not build this until we have baseline data. Add one week of logging:
// In agent::run, log per turn:
tracing::info!(
turn_duration_ms,
model_duration_ms,
tool_duration_ms,
tool_count,
tool_names,
"turn.latency"
);
Build condition:
If P50(tool_duration_ms / turn_duration_ms) > 0.30:
→ Speculative prefetching has clear ROI
Else:
→ Model inference dominates; skip this feature
Benefits
| Scenario |
Latency Impact |
| Single-tool turn |
~0% (nothing to overlap) |
| Multi-tool turn (2–3 reads) |
~20–35% faster |
| Deep research (5+ reads) |
~30–50% faster |
Risks & Mitigations
| Risk |
Mitigation |
| Wrong prediction wastes reads |
Limit to read-only tools; cap concurrent speculations |
| Cache grows unbounded |
Per-turn cache dropped after each turn |
| Parse overhead slows streaming |
Parse only after detecting "tool_use" substring |
| Race: speculative result arrives after normal execution |
Use tokio::sync::OnceCell pattern; first caller wins |
Implementation Sketch
- Add
speculative module in agent/mod.rs
- Create
SpeculativeCache (HashMap + tokio::sync::Mutex)
- Modify stream consumer in
Agent::run to parse deltas and trigger prefetches
- Modify tool execution loop to check cache before calling
- Add
IGNIS_ENABLE_SPECULATIVE=true feature flag (default off)
Acceptance Criteria
Open Questions
- Should we cache speculative results across turns or only within a single turn?
- How do we handle tool calls with relative paths that depend on the agent's current working directory mid-stream?
- Should speculative calls run with lower CPU priority or on a dedicated tokio task pool to avoid starving the main agent loop?,
Summary
Predict which read-only tools the LLM will call and execute them before the model finishes generating, overlapping tool I/O with model inference. This cuts multi-tool turn latency by overlapping serial work.
The Problem
In the current agent loop, the LLM generates a response, then tools execute sequentially, then the LLM generates again:
Total: ~1.9s of which 470ms is pure waiting on filesystem/network I/O.
If the model is going to call
read_file("src/auth.rs"), we already know that from the streaming JSON before generation ends. We could have started reading it 500ms ago.Proposed Design
Scope: Read-Only Only
Only speculate safe, idempotent tools:
read_filegrepgloblist_dirbash(side effects)edit_file(mutates state)create_file(mutates state)If the prediction is wrong, the only cost is a wasted read — no state corruption.
How It Works
tool_useJSON block.(tool_name, arguments_hash).Confidence Threshold
Not every partial match should trigger speculation. Start simple:
Later: add a lightweight predictor based on prompt + recent history.
Measurement-First Approach
Do not build this until we have baseline data. Add one week of logging:
Build condition:
Benefits
Risks & Mitigations
"tool_use"substringtokio::sync::OnceCellpattern; first caller winsImplementation Sketch
speculativemodule inagent/mod.rsSpeculativeCache(HashMap + tokio::sync::Mutex)Agent::runto parse deltas and trigger prefetchesIGNIS_ENABLE_SPECULATIVE=truefeature flag (default off)Acceptance Criteria
agent::runIGNIS_ENABLE_SPECULATIVE=1tool_time_ratio < 0.30Open Questions