Commit Graph

151 Commits

Author SHA1 Message Date
Levi Neely b775e5df7a agent: add TaskState — persisted structured overlay injected at each turn 2026-05-17 10:06:21 +02:00
Levi Neely 45959cb79d agent: apply config backend/model when switching agents via /agent 2026-05-17 00:19:55 +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 3867d52689 add generation params: top_p, top_k, min_p, top_a, repetition_penalty, max_completion_tokens, response_format, reasoning_effort, include_reasoning, stop, verbosity 2026-05-16 23:54:04 +02:00
Levi Neely 6dfae368d0 fix: wire PlanFile, add missing rate-limit constants, simplify test helpers, fix regex
- Wire PlanFile from $OLLIE/s/{id}/plan into agentConfig so plan
  re-injection is no longer dead code
- Add missing failureWindow/maxFailures/blockDuration constants
- Replace hand-rolled contains() with strings.Contains
- Fix rm -rf .. regex to match end-of-string and whitespace
- Add TestPlanReinjection covering periodic plan re-surfacing
- Add loop_maxsteps_helpers_test.go with mockResponse/newState/sequentialStream
2026-05-16 23:54:04 +02:00
Levi Neely 2101a2c023 agent: add MaxSteps override in AgentCoreConfig and /maxsteps command
- AgentCoreConfig.MaxSteps: when non-zero, overrides the value loaded
  from the agent JSON (cfg.Env.maxSteps). Zero means "use the agent
  JSON value, or unlimited if absent". Callers (9p frontend, httpgw,
  CLI) can now inject a per-session limit at construction time without
  touching any agent config file.

- /maxsteps [n] command: shows the current limit (0 = unlimited) or
  sets it live during a session. Follows the same pattern as /model.
  Added to /help listing.
2026-05-16 18:56:39 +00:00
Levi Neely d8467a4f16 agent: add ToolCallCount to Core interface; wire tcct accumulator
Adds a monotonically increasing tool-call counter (tcct) to the agent.

- atomic.Int64 toolCallCount on the agent struct
- ToolCallCount() int64 on the Core interface
- IncrToolCallCount func() int64 on agentConfig, wired in executeTurn
- Incremented in execOne after the pre-tool hook returns (blocked calls
  don't count) and before cfg.Exec is called (timeout/error still counts)

The counter never resets. Consumers use modulo arithmetic:
  tcct % 10 == 0  ->  fires every 10 actual executions

The primary consumer is ollie-9p, which exposes tcct as a read-only
9P file that pre_tool / post_tool hooks can inspect via cat.
2026-05-15 20:16:43 +00:00
Levi Neely 0d9405f9bc core: read MaxSteps from config into agentConfig 2026-05-15 19:33:44 +00:00
Levi Neely b6308f1a13 agent: log prompt resolution errors to stderr 2026-05-11 14:13:44 +02:00
Levi Neely a2b6adb6cf env: add OLLIE_AGENTS_PATH with colon-separated multi-dir resolution 2026-05-08 13:01:04 +02:00
Levi Neely 0041fc77ec add OLLIE_PROMPTS_PATH env var and PromptsDirs helper 2026-05-08 11:59:16 +02:00
Levi Neely f9b2b50274 agent: preserve completed work on interrupt/error
- 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
2026-05-04 10:23:56 +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 c069deccca config: add tools bool to disable tool collection per agent
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.
2026-05-03 14:58:58 +02:00
Levi Neely 1b04d6a75b agent: fix race between setState and currentAction in Submit
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>
2026-05-03 13:30:12 +02:00
Levi Neely b511626e4f remove stale-read pruning from context management 2026-05-02 13:50:40 +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 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 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 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 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 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 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 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 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 6abf0cdb3d execute: parallel dispatch via flock; agent: preTool/postTool hooks, turn rollback, mid-turn compact
execute_code auto-batches consecutive tool steps annotated '# ollie:parallel read'
into goroutine fan-outs. Write/global steps serialize. LOCK_SH/LOCK_EX via
syscall.Flock in a per-session lock dir for cross-process safety. Tool scripts
declare their own parallelism profile; no tool names hardcoded in Go.

- flock.go: lockClass, classifyStep (annotation-driven), acquireFlock
- tools.go: LockDirSetter interface
- server.go: lockDir field, SetLockDir method
- pipe.go: runReadBatch with shared-lock goroutine fan-out
- code.go: auto-batching pipeline loop
- core.go: pushLockDir wires session tmpdir to execute server at init;
  preTool/postTool hook callbacks in agentConfig; AutoCompact after each
  tool round; session snapshot/restore on turn failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 09:00:29 +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 aa890502cd agent: inject postTurn context into session history
Non-blocked postTurn hook output is now appended to session history so
the model sees it at the start of the next turn without triggering
autonomous continuation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 23:53:00 +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 a4bb7608c7 expand ~ in cwd across agent core, hooks, and execute server
- Add paths.ExpandHome() helper for tilde expansion
- Apply in NewAgentCore, SetCWD (agent + execute server), and hook runner
- Fixes 'chdir ~/path: no such file or directory' errors
2026-04-24 20:10:42 +02:00
Levi Neely ccbb5168b7 fix: statewait blocking and bfg composability
- Report non-zero size for wait files so FUSE clients read them
- Return current state on timeout instead of nil
- Set ChatOffset after user message so offset points past it
- Comment out hooksRan info events (TODO: route to debug file)
2026-04-24 14:00:27 +02:00
Levi Neely 197c3664c6 fix: resolve ${PWD} in system prompt from session cwd, not process env
expandSystemPrompt fell through to os.Getenv("PWD") which always
returned the process startup directory. Map "PWD" explicitly to the
cwd parameter so the system prompt reflects the session's working
directory.
2026-04-22 12:21:57 +02:00
Levi Neely 2264f3f11d agent: fix backend race, split commands.go, use OLLIE_TMP_PATH
- Add backend.NewWithName(name) to avoid os.Setenv race on OLLIE_BACKEND
- Extract handleCommand into commands.go (core.go 1264 -> 859 lines)
- Replace hardcoded /tmp/ollie with OLLIE_TMP_PATH (guaranteed by env.Ensure)
2026-04-21 21:44:39 +02:00
Levi Neely dee0fda330 agent: emit hook timeout/start-failure warnings as info events
- Add Warning field to HookResult
- runHookCmd sets Warning on timeout and start failure
- Hooks.Run collects warnings across commands
- All hook call sites emit warnings via infoEvent
- Test verifies timeout warning is surfaced
2026-04-21 21:20:55 +02:00
Levi Neely 4710e3239f agent: simplify AgentConfigPath to plain concatenation, remove dead config.json fallback 2026-04-21 16:06:07 +02:00
Levi Neely a4d4744eae agent: inline loadSystemPrompt into BuildAgentEnv 2026-04-21 15:58:17 +02:00