Commit Graph

68 Commits

Author SHA1 Message Date
ollie 2c36c023aa execute_code -> shell: simplify to single bash command, remove steps/parallel/detach/language 2026-07-28 23:03:31 +02:00
ollie 6bd77276ee remove call_tool and pipe implementations 2026-07-28 22:51:47 +02:00
ollie af9a22b1db response IDs and structured reactions with exact targeting 2026-07-27 19:12:12 +02:00
Levi Neely 273a31c5f1 emit post-stream suffixes to chat output
When tool results are streamed, suffixes appended after execution
(truncation hints, user-interruptions, PostTool context) were only
written to the stored message history but never emitted to the chat
stream. This meant they never appeared in D-Bus signals or the chat
file.

Accumulate suffixes explicitly and emit them as a final event when
streaming was active, guaranteeing they appear at the end of the
tool output in all output paths.
2026-07-27 13:07:41 +02:00
Levi Neely 75f11a7dc9 enforce 128KB tool result ceiling unconditionally
The tool result truncation had two bypasses:

1. Error results were exempt (!isErr condition) — a command returning
   output in its error message could flood the context with megabytes
   of untruncated data.

2. The streaming callback emitted chunks to the event handler (and
   thus D-Bus signals) with no size check, flooding the bus even
   when the final stored result would be truncated.

Fix:
- Remove the configurable ToolResultMaxBytes field; use the constant
  defaultToolResultMaxBytes (128KB) unconditionally for all results
  regardless of success/error status.
- Cap the streaming callback at 128KB so chunks stop being emitted
  once the ceiling is reached.
- Cap output embedded in execute server error messages to 8KB as
  defense in depth.
2026-07-27 13:02:45 +02:00
ollie c2688a1d3d agent: cross-turn tool result cache
Move the read-safe tool result cache from a per-turn local variable
to the agent struct, persisting it across turns. This eliminates
redundant calls to file_read, lsp_*, and memory_recall when the
underlying data has not changed.

The cache is invalidated (cleared entirely) when file_write or
file_edit executes, ensuring stale results are never returned
after mutations.
2026-07-21 12:04:35 +02:00
ollie 9c1dd58474 streaming: tool output streams into [tool:] block, no new roles 2026-07-19 21:36:55 +02:00
ollie 805933453f streaming: tool output streams into tool block directly 2026-07-19 21:25:42 +02:00
ollie 5fc7bd816e streaming: emit partial tool output via context callback 2026-07-19 21:06:13 +02:00
ollie 6f35059d83 agent/hooks: add run/fail summary to HookResult
Track Total, Succeeded, Failed counts and FailedCmds in HookResult.
Add Summary() method producing "(N of X hooks run) (K of X failed: cmd)".
Emit hook summary as info event at all lifecycle hook call sites and
on preTool/postTool failure.
2026-07-19 10:25:09 +02:00
ollie a50149d075 fix: send profileArn for social accounts, improve error messages
- Remove IsSocial guard from sqliteKiroAuth.ProfileARN(); the Kiro API
  now requires profileArn on all requests including personal accounts
- Add OLLIE_KIRO_PROFILE_ARN env var for static token auth
- Add repeated error detection (3x identical error triggers corrective nudge)
- Include expected format in tool dispatch error messages
2026-07-11 16:08:44 +02:00
Levi Neely 1dbc75cecc agent: replace cold truncation with real LLM summarization
- store all tool results verbatim at update time
- stripColdResults now calls LLM to summarize, skips hot tail
- stripCold interface takes ctx+backend
- remove stripColdResults from RestoreSession
2026-06-01 09:45:27 +02:00
ollie cb1551fe67 fix compaction model to deepseek-v4-flash; accept error stop reason in loop 2026-05-30 10:46:12 +02:00
ollie bc7a536ced fix: round-trip reasoning_content for DeepSeek thinking models
DeepSeek requires reasoning_content to be echoed back on assistant
messages. Store reasoning from stream events on Message and serialize
it as reasoning_content in the OpenAI wire format.
2026-05-28 19:04:03 +02:00
ollie 10cbded305 token efficiency: fix tier classification, expand cold tier, add usage log and compaction model override
- Fix dead-code bug: ClassifyTier now parses inner tool names from
  call_tool/pipe arguments via ResultTierArgs
- Add file_grep, file_glob, lsp_symbols, lsp_completion, lsp_diagnostics,
  reasoning_think to cold tier
- Add append-only JSONL usage log (~/.local/share/ollie/usage.jsonl)
- Add compaction model override: config > OLLIE_COMPACTION_MODEL env >
  per-backend default (haiku/gpt-4o-mini/flash)
- Expose CompactionModel on Core interface
2026-05-28 18:44:30 +02:00
ollie 02df4b8dd5 agent: plumb image ContentBlocks from tool results through to backend messages
toolExecutor now returns (string, []ContentBlock, error).
extractToolResult parses image blocks from structured tool output.
Session.update populates ContentBlocks on tool messages.
file_read detects PNG/JPEG/GIF/WEBP and returns base64-encoded image blocks.
2026-05-25 18:02:21 +02:00
Levi Neely 19877f0a06 agent/loop: send full compacted history instead of recent-only tail
After compaction, the loop was discarding cold+warm zones and only
sending the last 8 messages to the backend. This caused the agent to
lose context after manual or auto compaction.

Remove the recentHistory path entirely: state.history() already
returns the compacted (small) history post-compact. The TaskState
render block remains as additional steering injected after the system
prompt.

Removed: StepContext struct, recentHistory from state interface and
Session.
2026-05-23 13:16:41 +02:00
Levi Neely 9d6800a51c agent: parse text-based tool calls instead of failing
Models that don't support native function calling emit tool calls as
plain text. Instead of treating this as ToolUnsupportedError, parse the
text and execute the tool calls normally.

Handles both direct JSON and shorthand (key=value) formats with relaxed
JSON support (unquoted keys).
2026-05-23 11:46:49 +02:00
Levi Neely 93c0b374d5 truncation: avoid mid-rune cut with ToValidUTF8 2026-05-17 12:59:49 +02:00
Levi Neely 7237980ff5 truncation: raise hard ceiling to 128KB, include tool name in message 2026-05-17 12:51:43 +02:00
Levi Neely 011843a59d loop: per-step budgets — replan gate (8 rounds) and stall detection (5 rounds) 2026-05-17 12:13:26 +02:00
Levi Neely 8be8b9c42d task state: read PlanStep from plan file (ground truth) with text fallback 2026-05-17 11:09:44 +02:00
Levi Neely 837c95c483 state: expose estimateTokens/stripCold on interface; remove type assertion 2026-05-17 11:06:45 +02:00
Levi Neely d1ac24fb1d loop: auto-infer TaskState from actual tool results each round 2026-05-17 11:04:55 +02:00
Levi Neely 1cf171adae loop: proactive cold stripping at 50% context budget before backend call 2026-05-17 11:01:21 +02:00
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