- steps: independent, run in parallel when annotated safe, outputs
concatenated. No stdout chaining.
- pipe: explicit pipeline, sequential, stdout of each stage feeds
stdin of the next.
steps and pipe are mutually exclusive in a single execute_code call.
steps is always a pipe (sequential, stdout chains to next stdin).
Concurrent fan-out requires explicit use of the parallel field.
The ollie:parallel annotation still drives agent-level tool call
parallelism via IsParallelRead, but no longer hijacks pipe semantics.
After interrupt, tool results with cancelled status are removed but
the assistant message kept its ToolCalls referencing them, causing
'Improperly formed request' errors.
Now also strips cancelled tool call IDs from assistant messages.
Only removes the assistant message entirely if it has zero remaining
tool calls AND no text content.
- Add configurable retryBaseDelay and streamDropBaseDelay for tests
- Fix hook kill: don't wait for done channel after Kill() since child
processes may keep stdout/stderr pipes open
- Update tests to use short delays
Tests now run in ~0.4s instead of ~59s.
- On interrupt or error, keep user message, assistant response, and
completed tool results (success or error)
- Only remove cancelled/interrupted tool results
- Use deep copy for message snapshots to prevent aliasing bugs
- Snapshot now only used for context overflow retry
WrapCommand now returns an error instead of silently falling back to
unsandboxed execution when landrun is not in PATH. execute_code
propagates this as a hard failure.
Identical read-safe tool calls in the same batch now execute only
once. Duplicates get the result copied with the correct ToolCallID.
Fixes a race where concurrent goroutines both missed the cache
before either could store, causing double execution.
On user interrupt (stop/CTRL+C), the session history now rolls back
to the pre-turn snapshot instead of keeping partial/cancelled tool
results in the model's context. This prevents raw errors like
'context canceled' from poisoning subsequent turns.
Tool results interrupted by cancellation now report 'tool execution
interrupted by user' instead of the raw Go error chain.
When tools is false in agent config, BuildAgentEnv skips ListTools
entirely so no tool schemas are sent to the backend. Models without
tool support now work cleanly.
currentAction was stored after setState("thinking"), creating a window
where Interrupt() would see a nil action handle and return false despite
the agent being mid-turn.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add {PLAN9} to test sandbox rwx list so plan9 binaries (cat, sleep,
grep, bc, sed) are accessible. Drop -q from bc invocation since
plan9 bc does not support it; -l alone works on both GNU and plan9.
Entries in the prompt array that contain a newline are now treated as
literal text instead of shell commands. This enables JIT agent configs
to embed identity text directly without escaping.
Also clarify that execute_code timeout is a top-level parameter, not
per-step, to prevent models from placing it in the wrong location.
Track consecutive rounds where every tool call returns an error.
At 5, inject a nudge telling the model to try a different approach.
At 10, hard-abort the loop.
Also fix a data race in collectEvents (test helper) that caused
flaky failures in TestResultCache_IdenticalParallelReads.
Previously, all reads of a path were pruned when a write occurred. This
caused the model to lose the base state of files it edited, leaving its
reasoning ungrounded and edit diffs without context.
Now only earlier reads are pruned; the most recent read before a write is
kept. Combined with the edit diff in the tool result, the model retains
full knowledge: base contents + deltas = current state.
Exposes the current context window (messages sent to the backend) as a
read-only JSONL file. Each line is a serialized backend.Message with the
system prompt prepended and stale reads pruned, matching what run() sends
to ChatStream. Useful for debugging agent forgetfulness.
When pruning stale file_read results, also remove the corresponding
tool calls from assistant messages. Leaving orphaned tool calls
(toolUses without matching toolResults) causes ValidationException
from the Kiro API.
When a model emits tool invocations as plain assistant text instead of
using the function-calling API (e.g. "execute_code: steps=[...]"), the
loop now returns ToolUnsupportedError. This lets freeloader blacklist the
model and cycle to one that properly supports function calling.
Detection: after a turn with stopReason="stop" and no API-level tool
calls, scan assistant text for any registered tool name followed by ':'
or '('. Only fires when tools are configured.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend: add HTTP 404 and "no endpoints found" to isToolUnsupported
so OpenRouter 404s are correctly classified as tool_unsupported
- core: drain FIFO on error return from executeTurn so freeloader's
resubmitted prompt (queued via fifo.in while running) is picked up
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /model command no longer rejects switches while agent is running;
needed for turnError hook recovery (freeloader switches model mid-error)
- TurnError hook uses context.Background()-derived context so hook can
safely issue ctl commands without cancelling its own execution
- update TestCommand_Model_WhileRunning to expect success
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HookResult.Handled: true only when all commands exit 0; loop checks
Handled (not Ran) so exit-1 from freeloader falls through to normal retry
- runHookCmd: non-zero non-2 exits now return Ran:false with warning
instead of Ran:true, so allHandled tracking is accurate
- loop: check r.Handled instead of r.Ran for turnError intercept
- tests: updated to return Handled:true from mock hook functions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend: ToolUnsupportedError type, isToolUnsupported detector (400/422)
- backend: classifyError maps error types to hook payload strings
- agent: HookTurnError constant; TurnError func on agentConfig set at init
- loop: fire TurnError hook on first backend error; if ran, skip retries
- store: MemoryStore — in-memory KV Store implementation
- store: Session.prevPrompt field; prompt.prev read-only session file
- hooks: inject payload map as OLLIE_* env vars in runHookCmd
- agent: agentSpawn payload includes model field
- agent: limitretry state emitted on RateLimitError backoff
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
loop_prune_test.go — three new prune tests:
- ReadWriteReadCycle: pre-write read pruned, post-write read kept (primary invariant)
- AssistantMessagesUnaffected: only role:"tool" messages are dropped, never assistant messages
- NonExecuteCodeUnaffected: non-execute_code tool calls don't confuse the path tracker
loop_cache_test.go — one new cache test:
- IdenticalParallelReads: two goroutines simultaneously miss the cache, both execute,
results are consistent and no panic (sync.Map race safety)
cost_test.go — six computeCostUSD unit tests:
- BaseTokens, ClaudeCacheReadDiscount (10%), NonClaudeCacheReadDiscount (50%),
CacheCreationSurcharge (125%), AllFieldsCombined, UnknownModelZero
openai_encode_test.go — four OpenRouter cache injection tests:
- OpenRouterClaude_SystemCacheControl: system content encoded as array with cache_control
- OpenRouterClaude_LastToolCacheControl: last tool gets cache_control:ephemeral
- OpenRouterNonClaude_NoCacheControl: non-Claude model on openrouter gets no injection
- OpenAI_NoCacheControl: plain openai backend never injects cache_control
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Truncation (loop.go, core.go):
- Add ToolResultMaxBytes to agentConfig; default 8192 bytes
- In execOne, after PostTool/PopInject, truncate results that exceed the
limit and append a continuation hint with byte offset
- Error results are never truncated; resultCache now stores the
post-truncation form so cache hits are consistent
Stale read pruning (loop_prune.go):
- pruneStaleReads() is called each iteration before building the
history slice sent to the backend
- Parses execute_code step args to track file_read call-IDs and the
paths touched by file_write/file_edit steps
- When a write is seen, all accumulated read results for that path
(not just the most recent) are removed from the history view
- Inline code steps are left unanalysed (conservative: never prune
what cannot be determined safe to prune)
Also note: compaction (3) was already a full LLM summarisation pass
(CONTEXT CHECKPOINT COMPACTION prompt in session.go); no change needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Maintain a sync.Map (resultCache) across the lifetime of a run() call.
Before executing a read-safe tool (ClassifyTool returns true), check the
cache by key name+"\x00"+args. On hit, emit the cached result and skip
Exec entirely. On miss with a successful Exec, store the result. Error
results are not cached so transient failures are retried.
Tests: HitSkipsExec, DifferentArgsMiss, SerialToolNotCached,
ErrorNotCached (multi-turn sequential).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- loop_parallel_test.go: four tests covering concurrent execution (barrier
pattern), serial tool breaking a batch, cancellation filling all results,
and nil ClassifyTool falling back to serial
- loop.go: emit "tool" events for all cancelled tool calls (early-ctx-err
path in execOne, top-level ctx check, and post-batch fill) so every
dispatched tool call has a visible result regardless of cancellation timing
- anthropic_stream_test.go: five tests covering tool schema cache_control
placement (last tool only, single-tool edge case) and tool result caching
(large gets cache, small does not, last of multiple large results wins)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tools annotated with "# ollie:parallel read" now run concurrently when
the LLM issues multiple such calls in a single round, without needing
an execute_code pipeline.
- tools.ParallelClassifier interface; execute.Server implements it via
IsParallelRead, which reads the tool script and checks the annotation
- BuildAgentEnv wires IsParallelRead → AgentEnv.classifyTool →
agentConfig.ClassifyTool (nil when execute server absent)
- loop.go: consecutive parallel-safe calls are fanned out with a
WaitGroup; results are collected in submission order; cancellation
fills remaining calls with interrupted results; serial tools and
batches of one skip the goroutine overhead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tool schema caching:
- Mark the last tool with cache_control:ephemeral before each Anthropic
request; Anthropic caches all preceding tools as a prefix
- Same for OpenRouter+Claude via openAITool.CacheControl extension
Large tool result caching (Anthropic only):
- In buildAnthropicMessages, scan backwards through the encoded output
and mark the last tool_result block ≥4096 bytes with cache_control
- 4096 bytes ≈ 1024 tokens, Anthropic's minimum cache entry size
- OpenAI wire format has no block structure for tool results; skip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When OpenRouter routes to a Claude model, encode the system message as
a content-block array with cache_control:ephemeral instead of a plain
string. OpenRouter passes this through to Anthropic, enabling the same
90% cache-read discount as the native Anthropic backend.
Detection: b.name=="openrouter" && model contains "claude-".
Encoding: MarshalJSON on openAIMessage outputs content as array when
ContentBlocks is set, preserving the existing *string path for all
other cases with no test changes required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Anthropic system prompt sent as a cacheable content block
(cache_control: ephemeral) so repeated turns reuse the cached prefix
- Parse cache_read_input_tokens and cache_creation_input_tokens from
Anthropic message_start; parse cached_tokens from OpenAI
prompt_tokens_details
- Normalise Usage so CachedInputTokens is always excluded from
InputTokens regardless of provider
- computeCostUSD now takes backend.Usage and applies correct cache
discounts: 10% read / 125% creation for Anthropic, 50% read for OpenAI
- Session tracks TotalCachedInputTokens / TotalCacheCreationTokens;
/usage shows cached token count when non-zero
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>