Commit Graph

601 Commits

Author SHA1 Message Date
Levi Neely ba9ab44565 execute: fix test sandbox for plan9 PATH, drop GNU-only bc -q flag
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.
2026-05-02 10:40:00 +02:00
Levi Neely f32a2a6907 prompt: support literal strings in exec prompt arrays; clarify timeout schema
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.
2026-05-02 10:29:01 +02:00
Levi Neely c3f0f93e24 agent: abort on consecutive tool errors
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.
2026-05-01 15:57:20 +02:00
Levi Neely d175bdba32 remove ollie-tui reference 2026-05-01 10:21:30 +02:00
Levi Neely 6646740d67 change tools to hierarchical 2026-04-30 14:39:02 +02:00
Levi Neely e13bf552e7 docs: update hooks and tool scripts list 2026-04-30 09:07:52 +02:00
Levi Neely efe862a830 remove pkg/store: moved to 9p/store 2026-04-30 07:53:35 +02:00
Levi Neely 2d81cc2aa1 pruneStaleReads: keep last read per path as base state
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.
2026-04-29 18:03:49 +02:00
Levi Neely ed3373d3f0 add s/{sid}/context 9P endpoint
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.
2026-04-29 18:03:43 +02:00
Levi Neely 6ce1104f29 fix: prune stale tool calls from assistant messages
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.
2026-04-29 10:13:39 +02:00
Levi Neely 28027d2f2b core: detect text-form tool calls as ToolUnsupportedError
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>
2026-04-28 19:06:49 +02:00
Levi Neely c812f0882e freeloader: fix 404 tool_unsupported detection and FIFO-drain recovery
- 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>
2026-04-28 18:47:34 +02:00
Levi Neely 9ce1e63788 core: allow /model switch while running; detach turnError hook context
- /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>
2026-04-28 17:41:43 +02:00
Levi Neely d350c9f412 core: fix turnError hook wiring — Handled field, correct exit-code semantics
- 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>
2026-04-28 17:27:13 +02:00
Levi Neely 78e5416550 core: freeloader infrastructure — turnError hook, ToolUnsupportedError, prompt.prev, MemoryStore
- 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>
2026-04-28 17:17:25 +02:00
Levi Neely 2c9f853a4a core: sort /models output alphabetically via ListModels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 16:00:22 +02:00
Levi Neely b322ad550e core: sort /models output alphabetically
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 14:38:59 +02:00
Levi Neely 9f1d7a5ec8 core: caching risk-reduction tests
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>
2026-04-28 14:33:29 +02:00
Levi Neely 7eadfee25d core: tool result truncation and stale read pruning
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>
2026-04-28 14:12:00 +02:00
Levi Neely f805dcdb74 core: tool result deduplication for read-safe tools
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>
2026-04-28 13:19:20 +02:00
Levi Neely 905bf4e9a9 core: tests and emission fixes for parallel tool execution and caching
- 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>
2026-04-28 13:13:32 +02:00
Levi Neely bc59f6c40f core: loop-level parallel tool execution for read-safe tools
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>
2026-04-28 13:05:15 +02:00
Levi Neely efac665ffe core: tool schema caching and large tool result caching
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>
2026-04-28 12:22:45 +02:00
Levi Neely ba14c8f07f core: cache system prompt on OpenRouter+Claude via content block encoding
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>
2026-04-28 12:14:46 +02:00
Levi Neely cf6fee36e2 core: Anthropic prompt caching and cache-aware cost accounting
- 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>
2026-04-28 12:01:14 +02:00
Levi Neely 4181d56d54 agent: only report cost when backend provides it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:12:08 +02:00
Levi Neely efb75ddea0 agent: /cost command shows last-turn and session cost
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:45:57 +02:00
Levi Neely f16ba70126 store: newline before any non-assistant event following assistant text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:44:10 +02:00
Levi Neely 370171bf80 agent: context overflow recovery and OLLIE_MAX_SESSION_COST spending cap
On ContextOverflowError (400 with context-length body), restore pre-turn
snapshot, compact, and retry the turn once. Covers runaway context growth
that slips past the proactive 75% threshold.

OLLIE_MAX_SESSION_COST (USD) blocks turn start and interrupts mid-turn
via the usage event handler, which fires between every tool round.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:35:39 +02:00
Levi Neely cf5652a5b1 agent: log user input, reply, and interrupted turns to audit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:23:55 +02:00
Levi Neely 4b96e0cae1 agent: audit logging via OLLIE_AUDIT_LOG=debug
Logger.Sub(tag) derives a child logger with a new tag from the same
writers, with level inherited from the parent but independently
overridable via OLLIE_{TAG}_LOG.

The agent creates an "audit" sub-logger at startup. Key events are
logged at debug level through it:
  audit: call: <tool> <args (truncated)>
  audit: result: <tool> <content (truncated)>
  audit: error: <message>
  audit: turn: start/end with cost and session ID
  audit: compact: messages removed, trigger, session ID

Enable with OLLIE_AUDIT_LOG=debug.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:21:13 +02:00
Levi Neely 59ee25b010 agent: per-turn and session cost tracking in USD
Usage events now carry a 4th field (costUSD) so OpenRouter's reported
cost flows through unchanged. For backends that only report token counts
(Anthropic, direct OpenAI, Ollama), cost is computed from a prefix-keyed
pricing table. The higher-fidelity source always wins.

Per turn:
- turnInputTokens/turnOutputTokens/turnCostUSD reset before each turn
- recordTurnCost() stores LastTurnCostUSD, adds to SessionCostUSD
- "costLast=$X.XXXX" emitted as an info event at turn end

Session file "cost" returns costLast/costSession key=value pairs.
Agent interface gains Cost() string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:14:12 +02:00
Levi Neely 0aa44b4a00 agent: warn once at 60% context use before auto-compact at 75%
Emits "context at N% — will auto-compact at 75%" on the turn that
crosses the warn threshold, then suppresses further warnings until
the next compaction resets the flag.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 17:57:38 +02:00
Levi Neely 625257dea1 agent: retry on transient backend errors and mid-stream drops
Previously only HTTP 429 triggered a retry. Network failures, 5xx
responses, and mid-stream drops all hard-failed the turn.

- Add TransientError for 5xx and network-level failures; streamRequest
  wraps client.Do errors and >=500 responses in it
- Merge pre-stream and mid-stream retry into one loop (maxTransientRetries=3)
  covering rate limits, transient errors, and pure stream drops
- On stream drop with ctx cancelled: record partial state and return
  (existing pause behaviour preserved)
- On stream drop without cancellation: close any open <think> block,
  emit retry countdown, restart ChatStream from the same history

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 17:54:27 +02:00
Levi Neely 9fecf5f60f store: add read-only env file exposing OLLIE_ vars
New session file 'env' (mode 0444) returns OLLIE_SESSION_ID and all
OLLIE_ environment variables from the server process. Frontends can
read it to populate shell environments without depending on core.

Also fix pre-existing race in TestSessionFileStoreWritePrompt.
2026-04-26 22:18:17 +02:00
Levi Neely ed62caced5 agent: serialize Submit with submitMu to prevent command/turn races
Commands like /compact arriving via ctl and prompts arriving via prompt
both call Core.Submit in separate goroutines. handleCommand for mutating
commands (compact, clear, agent, backend) modifies session state without
setting currentAction, so IsRunning() returns false and a concurrent
executeTurn can race.

Add submitMu held during the serialized path of Submit. The fast path
(inject/FIFO push while running) remains lock-free.

Also fix pre-existing race in TestSessionFileStoreWritePrompt: replace
time.Sleep synchronization with a channel signal.
2026-04-26 19:40:06 +02:00
Levi Neely c32ea01f60 agent: interrupt is now pause, not cancel
With mid-turn SaveSession in place, interrupt keeps in-memory session
state matching the on-disk state rather than restoring the pre-turn
snapshot. The model resumes naturally from the saved history on the
next turn.

Remove sessionClean — no longer needed now that all interrupted sessions
are kept rather than conditionally discarded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 19:25:57 +02:00
Levi Neely dc83763c24 agent: save session after each tool round for mid-turn crash recovery
Adds SaveSession callback to agentConfig, called after every state.update()
in the run loop. On restart, the session JSON contains all completed tool
rounds; the model continues naturally from current history.

SIGTERM is already handled in olliesrv via InterruptAll + Shutdown, which
cancels all session contexts and lets the loop exit cleanly after the
current state.update(). Worst-case (SIGKILL) loses one tool round, not
the whole turn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 19:24:28 +02:00
Levi Neely eabe17a1b1 backend: stream extended thinking / reasoning tokens
- Add StreamEvent.Reasoning for incremental thinking deltas
- Add GenerationParams.ThinkingBudget to enable Anthropic extended thinking
- Anthropic: parse thinking_delta events; set Anthropic-Beta header and
  thinking request param when ThinkingBudget > 0
- OpenAI: parse delta.reasoning (o-series) and delta.reasoning_content
  (OpenRouter) fields
- Loop: emit "reasoning" events wrapped in <think>/</think> markers
- FormatEvent: append reasoning content directly to the chat log

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 19:01:27 +02:00
Levi Neely 132174df95 agent: drop error return from Session.update
The function only appends to a slice and can never fail. Removing the
dead error return eliminates the nolint:errcheck suppression on the
interrupted-stream path in loop.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 18:56:58 +02:00
Levi Neely 18b5c2c08c tests: improve pkg/store coverage to 85.3%
Add tests for FormatParams/ParseParams round-trip and error paths,
Session.Interrupt, createSession via NewCore injection, and openEntry/
OpenStore not-found paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 18:55:31 +02:00
Levi Neely 386dc64753 tests: add flock/locking coverage for execute package
Covers detectParallelClass, sanitizeLockName, acquireFlock, classifyStep,
SetLockDir, and runReadBatch — bringing pkg/tools/execute from 70.9% to 92.4%.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 18:50:35 +02:00
Levi Neely c7ad2ca68a backend: require OLLIE_BACKEND/OLLIE_MODEL; no silent ollama default
- NewWithName and newFromEnv now error when OLLIE_BACKEND is unset
  instead of falling back to ollama silently
- createSession reads OLLIE_MODEL as fallback when model= is not given
- Update test to assert error on missing OLLIE_BACKEND

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 18:25:39 +02:00
Levi Neely b54c5c08f9 kiro: implement social token refresh via desktop.kiro.dev 2026-04-26 18:11:21 +02:00
Levi Neely cafeff4a1e remove built-in SYSTEM_PROMPT.md loading
System prompt is now assembled via x/prime in the agent prompt array,
same as all other prompt fragments. Remove expandSystemPrompt and
the agentPrompt field — resolved prompt goes directly into preamble.
2026-04-26 15:10:05 +02:00
Levi Neely d0e1b6f6ef config: make prompt executable (string or []string of commands)
Prompt field in agent JSON now accepts either a string (existing
behavior) or an array of strings. Each array element is executed as a
shell command; their combined stdout becomes the agent prompt.

- Add config.Prompt type with custom JSON unmarshaling
- Rewrite resolvePrompt to handle both modes
- Wire resolvePrompt into BuildAgentEnv
2026-04-26 15:00:33 +02:00
Levi Neely dabae7f72b agent: preserve session on interrupt only when state is clean
Keep session state after an interrupt if the last message is a clean
assistant text response (no dangling tool calls). Roll back to pre-turn
snapshot if the history is inconsistent — mid-tool-call interrupts would
send malformed history to the backend on the next turn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:36:43 +02:00
Levi Neely 554697e259 agent: preserve session state on interrupt, discard only on real error
Interrupting a long turn previously cleared the session (first-turn case)
or rolled back messages (subsequent turns). An interrupt is intentional —
keep whatever state was built so the user can continue.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:35:13 +02:00
Levi Neely c6d6f2f162 backend: recover DeepSeek DSML tool calls leaking into content stream
DeepSeek's native tool-call format uses <|DSML|invoke> tokens. When
OpenRouter fails to convert them to standard tool_calls chunks, they
arrive as raw content text. Detect the marker, buffer the content, and
parse it into ToolCall structs at [DONE] if no structured tool calls
accumulated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:17:29 +02:00
Levi Neely a2e01840b3 store: add s/{sid}/plan — session-scoped markdown task checklist
Session.plan field and SessionFile handler for 9p endpoint s/{id}/plan.
Agents write markdown checklists here via task_add/task_check/task_clear;
peers and parent sessions can read it directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 12:29:58 +02:00