Commit Graph

43 Commits

Author SHA1 Message Date
Levi Neely 779b99fd9b tool results: tier classification (hot/warm/cold) with immediate cold summarization 2026-05-17 10:59:17 +02:00
Levi Neely da36db8f2b loop: introduce StepContext — per-turn context object assembled once 2026-05-17 10:54:10 +02:00
Levi Neely fa88c89369 compact: tiered compaction — cold (summary), warm (decision index), hot (verbatim tail) 2026-05-17 10:52:48 +02:00
Levi Neely b5b2ee43ec loop: replace plan file re-injection with structured TaskState re-injection 2026-05-17 10:14:44 +02:00
Levi Neely 283a2e24d8 state: extend interface with recentHistory/taskState; loop uses selective context 2026-05-17 10:10:33 +02:00
Levi Neely b775e5df7a agent: add TaskState — persisted structured overlay injected at each turn 2026-05-17 10:06:21 +02:00
Levi Neely 22d72f143d fix: wire allowExecutors/allowTools in BuildAgentEnv; add missing IncrToolCallCount field 2026-05-17 00:05:34 +02:00
Levi Neely 4fdf5e042d loop: re-surface plan file every 10 tool rounds; nudge includes plan reminder 2026-05-15 19:48:13 +00:00
Levi Neely c20a5203ca agent: add MaxSteps soft guardrail to agentConfig and loop 2026-05-15 19:25:10 +00:00
Levi Neely e6233626bc speed up agent tests
- 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.
2026-05-04 11:58:01 +02:00
Levi Neely 6422c365b7 agent: deduplicate identical tool calls within parallel batches
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.
2026-05-03 21:27:46 +02:00
Levi Neely 3c5d92b461 agent: roll back session on interrupt, clean error message
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.
2026-05-03 21:10:28 +02:00
Levi Neely b511626e4f remove stale-read pruning from context management 2026-05-02 13:50:40 +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 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 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 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 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 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 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 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 ac238f6903 agent: roll back session on turn failure
Snapshot session state before each turn. On any error from run(),
restore to the pre-turn snapshot so the session is clean for the next
attempt. Remove recordInterruption — those messages were appended to
state that gets rolled back anyway, making them dead code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 07:30:59 +02:00
Levi Neely 0fba869ab7 agent: add preTool and postTool hooks
preTool fires before each tool execution; exit 2 blocks the call and
returns the hook's stderr as the tool error result. postTool fires after;
exit 0 appends context to the result, exit 2 replaces it entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 07:19:35 +02:00
Levi Neely 51922b78fc agent: mid-turn compaction and runCompact consolidation
Extract runCompact() to handle the full compact cycle (pre-hook, compact,
spawn-context re-injection, post-hook) consistently across all three call
sites. Wire AutoCompact into agentConfig so loop.go triggers it after each
tool round, not just before turns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:51:53 +02:00
Levi Neely 77dd5c6168 agent: refactor prompt architecture and session lifecycle
- Rename loopConfig→agentConfig, agentCore→agent, loopcfg→cfg, systemPrompt→preamble
  for clarity: the struct represents agent runtime config, the field is its config,
  and preamble accurately describes the compiled system+agent prompt sent as system role

- Separate preamble from agent context: preamble now contains only SYSTEM_PROMPT.md;
  agent prompt and agentSpawn hook output are injected into session history at
  context refresh points instead of being bolted onto the system role

- Replace fireAgentSpawn (mutated preamble once, guarded by agentSpawnFired flag)
  with spawnContext (assembles agent prompt + hook output, returns string for caller
  to inject); remove agentSpawnFired entirely — session==nil is the sufficient guard

- Three refresh points inject spawn context into session history:
  session start, post-clear (session=nil triggers same path), post-compaction
  (auto and manual)

- Move all preTurn tool primes (tools-file, tools-reasoning, tools-memory,
  tools-subagent, tools-elevate) to agentSpawn in all agent configs so they are
  injected at session refresh points alongside bd prime, not repeated every turn

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:38:18 +02:00
Levi Neely c6f6e7feeb agent: testability, dead code removal, coverage to 83%
- Hook timeout configurable via hookTimeout var (unlocks <-timeout branch)
- newBackend injection into agentCore (unlocks /backend switch in tests)
- Remove dead code: WatchSignals, Prompt(), Goal(), isComplete/markComplete,
  contextStatsString, extractMCPText, DefaultAgentsDir, DefaultSessionsDir,
  Dispatcher() on AgentEnv
- Signal handling moved to process entry points; core has no OS signal knowledge
- Tests: extractMCPResult, mcpToolsToBackend, RestoreSession, SetEnv,
  BuildAgentEnv, firstSentence, hook timeout, backend injection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 12:28:29 +02:00
Levi Neely cf604bd38f usage: track responses, estimate tokens when backend doesn't report 2026-04-13 11:03:39 +02:00
Levi Neely 345ec744d0 remove dead code 2026-04-12 20:05:52 +02:00
Levi Neely ae82e0eff0 agent: inject mid-turn prompts at tool boundaries instead of queueing
- Submit routes to Inject when a turn is already running, keeping the
  routing decision in core rather than the frontend or 9p layer
- Inject no longer cancels the current action; the pending prompt is
  stored and consumed at the next tool result boundary (success or
  failure), matching goq interrupt semantics
- If an inject is already pending, fall back to the FIFO so nothing is
  lost; /irw (interrupt-rewrite) explicitly overwrites the pending inject
- PopInject is now called after every tool result, not just the last one
- Injected prompts are echoed to the chat via the running turn's output

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 22:25:37 +02:00
Levi Neely e2d973fe3f Remove agent stalled feature
Remove MaxSteps limit and associated stalled event emission.
The agent will now run indefinitely until completion or interruption,
eliminating the annoying 'agent stalled' messages.
2026-04-11 15:51:39 +02:00
Levi Neely 2f68cc1f31 agent: add usage tracking and per-turn token reporting
Emit usage events from the loop when backends report token counts.
Session tracks cumulative input/output tokens and request count.
2026-04-10 20:29:49 +02:00
Levi Neely a54e5946bb agent: add interrupt injection, prompt FIFO, and queue drain
- Inject(prompt) cancels the running tool and injects the message into
  the conversation as <system-user-interruption>
- Queue/PopQueue FIFO for deferred prompt execution
- Submit() drains FIFO after each turn
- IsRunning() for frontends to choose Submit vs Inject
- TUI: typing during a turn calls Inject; /q queues
- 9P: prompt interrupts if busy; prompt.fifo is push/pop FIFO
2026-04-10 20:11:12 +02:00
Levi Neely 105b56d746 agent: replace rollback with proper interruption recording
Instead of discarding history on interruption, record what happened:
- Partial assistant content and tool calls are preserved
- Skipped tool calls get synthetic cancelled results
- Phase-specific notes (stream/tools/request) tell the model what occurred
- Side effects from already-executed tools are no longer hidden from history
2026-04-10 19:39:02 +02:00
Levi Neely f87699ac7d Unify tool servers: dissolve builtin/MCP distinction
All tool servers now implement tools.Server equally. tools.MCPExecutor →
tools.Dispatcher (unexported dispatcher), tools.Executor → tools.Dispatcher
interface, tools.NewMCPServer → tools.NewServer, execute.Executor →
execute.Server, file.Server added. BuildAgentEnv takes a pre-configured
Dispatcher; caller registers execute and file servers before calling.
builtinServer collapsed — no special "builtin" concept remains.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 23:51:10 +02:00
Levi Neely 97b85e5163 refactor: promote backend/config to pkg/, extract TUI to ollie-tui
Move internal/backend and internal/config to pkg/ so external consumers
can import them. Remove cmd/ollie and internal/tui — they now live in
the separate ollie-tui repo which imports ollie as a library.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 22:05:35 +02:00
Levi Neely ce329bfcc7 backend: internalize model state; remove model param from ChatStream
Each backend now owns its active model via Model()/SetModel(). ChatStream
no longer takes a model string — callers use SetModel to configure it.
AgentCoreConfig.ModelName sets the backend model at construction time via
SetModel, then the field can be dropped from the live agentCore struct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:42:30 +02:00
Levi Neely f280ef17db Make pkg/agent internals private; move app-level utils to cmd/ollie
Everything not part of the agent<->frontend contract is now unexported:
- contextConfig/Builder/Stats, loopConfig, toolExecutor, confirmFn,
  run, state, toolResult — internal loop/session machinery
- buildFirstPrompt, systemPrompt, builtinTools — impl details
- agentCore struct (NewAgentCore returns Core)
- Session methods (saveTo, history, rollback, compact, etc.)
- AgentEnv internal fields (mcpExec, exec, confirm, tools, etc.)

App-level utilities moved from pkg/agent to cmd/ollie:
- resolveBackendName, defaultModelForBackend, newSessionID,
  agentConfigPath

RestoreSession now takes int (ctxOverhead) directly, eliminating
the need for contextConfig to be public.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:31:02 +02:00