Commit Graph

45 Commits

Author SHA1 Message Date
Levi Neely 8acb4a901e agent: add Complete (one-shot ephemeral agent)
TRANSITIONAL: stateless code completion as a package function.
Intent is to unify with Agent struct — a full Agent composes a one-shot.
2026-07-29 22:19:19 +02:00
Levi Neely ec62eb9189 session: remove Interrupt, Queue, Detach, and all remaining agent pass-throughs
Session now has only genuinely session-level methods:
- Agent(), Bus() — access points
- Close(), SetEnv() — lifecycle
- CWD(), SetCWD(), SetSessionID() — filesystem identity
- WaitChange() — field watching for 9P
- SaveSession(), saveSession(), flushSave() — persistence

Everything else (Interrupt, Queue, Detach, Reactions, etc.) lives on Agent.
Callers go through session.Agent() to reach agent operations.
2026-07-29 22:05:46 +02:00
Levi Neely 24b9a0bdd3 session: remove Submit and agent pass-through methods
Session no longer has Submit(), IsRunning(), State(), Reply(), BackendName(),
ModelName(), AgentName(), CtxSz(), Cost(), Usage(), Context(), SystemPrompt(),
GenerationParams(), SetGenerationParams(), CompactionModel(), SetCompactionModel(),
ListModels(), Reactions(), React(), ReactTo(), PopQueue(), ToolCallCount().

Callers access the Agent directly via Session.Agent(). Session retains only
genuinely session-level operations: Agent(), Bus(), Close(), SetEnv(),
Interrupt(), Queue(), InjectSystemEvent(), CWD(), SetCWD(), SetSessionID(),
WaitChange(), persistence, and detach.

Agent.Submit now dispatches slash commands internally (HandleCommand) before
falling through to executeTurn.
2026-07-29 21:57:53 +02:00
Levi Neely 0b3d04c17c session: remove command dispatch from Submit
Session.Submit now forwards directly to Agent.Submit. Slash commands
are agent-level concerns handled by Agent.HandleCommand.

Session-level operations (save, resume, kill, rename, cwd) are ctl
verbs issued via the ctl file — they are NOT slash commands and do
not flow through the prompt path.

This eliminates the artificial session/agent command routing and
makes the prompt path clean: everything in the prompt goes to the agent.
2026-07-29 21:52:56 +02:00
Levi Neely 8e4cdaa4bd session: extract agent/ as separate package
Agent is now in ollie/agent with proper encapsulation:
- Unexported fields, exported methods as the API
- Own constructor (agent.NewAgent)
- Owns: turn execution, history, runtime, hooks, commands, compaction
- Session never reaches into agent internals

Session (ollie/session) is a thin host:
- Owns: persistence, session ID, env, detach delegation
- Delegates all agent operations through exported Agent methods
- handleCommand dispatches to agent.HandleCommand for agent-level commands

Agent-level commands (/model, /backend, /compact, /agent, etc.) live
in agent/commands.go and access internals directly (same package).

Session-level commands (/sessions, /save, /resume, /cwd, /help)
remain in session/commands.go.

Test files temporarily removed pending rewrite against new API.
The fifo_test.go passes as a sanity check.
2026-07-29 21:43:49 +02:00
Levi Neely 6ee949bd7c rename agent/ → session/, absorb session.Session into harness
The agent/ package is now session/ — because a session is the
top-level concept. The package contains:
- harness: implements Core, the session orchestrator
- Agent: the reasoning entity (history, runtime, tools)
- History: conversation accumulator
- Fifo, loop, compaction, hooks, commands

The old session/ package (which just held extracted fields) is
dissolved back into the harness. One package, clean ownership.
2026-07-29 19:13:26 +02:00
Levi Neely 761dd51e24 agent: rename sessionHost → harness, host.go → harness.go
The harness is the Core implementation that wires a session.Session
to an Agent and drives execution. A session has a harness; the
harness runs the agent.
2026-07-29 18:47:41 +02:00
Levi Neely cc9fa0c8cd agent: rename type agent → sessionHost, core.go → host.go
The struct that implements Core is the session host — it owns a
session.Session and an Agent, orchestrating their interaction.
Renaming makes the architecture self-documenting:

  sessionHost (implements Core)
  ├── sess *session.Session  — runtime environment
  └── agent *Agent           — reasoning entity (swappable)
2026-07-29 18:44:13 +02:00
Levi Neely bda9c9cb1d agent: rename reasoning.go → agent.go 2026-07-29 18:42:55 +02:00
Levi Neely 1aebc49d4e agent: rename reasoning → Agent
The inner struct that holds agent-specific state (history, runtime,
backend, tools, hooks) is now called Agent — because that's what it
is. A session owns an Agent; the Agent performs reasoning within
the session's environment.
2026-07-29 18:42:28 +02:00
Levi Neely da4d047aa1 agent: extract reasoning struct (swappable agent state)
Move agent-specific fields into a nested 'reasoning' struct:
- history, runtime, cfg (per-turn params)
- agentName, agentsDir
- baseLayers, promptEnvExtra
- newDispatcher, newBackend
- currentAction, warnedContext, resultCache

The agent struct is now the session host (owns sess + reasoning).
On /agent swap, a new reasoning can be built from the new config
while the session (identity, state, bus, env) remains stable.
2026-07-29 18:35:45 +02:00
Levi Neely 23de2e8267 agent: rename receiver s → a for clarity
The agent struct receiver was historically 's' (from when it was
called 'session'). Now that session is a separate concept, rename
to 'a' to match the type name.
2026-07-29 18:33:21 +02:00
Levi Neely 6396167361 agent: delegate sessionID, cwd, env, uname to session.Session
Continue moving session concerns to the session package:
- sessionID → sess.ID()/SetID()
- cwd → sess.CWD()/SetCWD()
- env/envMu → sess.SetEnv()/Env()
- uname → sess.Uname()

Mutex responsibilities are cleanly split:
- session.Session.mu protects: state, reply, cwd, id
- session.Session.envMu protects: env map
- agent.mu protects: history, runtime (agent-specific state)
2026-07-29 18:31:18 +02:00
Levi Neely 64f3f18573 agent: delegate bus, fifo, state, reply to session.Session
Move session-scoped concerns out of the agent struct:
- Bus() → sess.Bus()
- Queue()/PopQueue() → sess.Queue()/PopQueue()
- State()/setState() → sess.State()/SetState()
- Reply() → sess.Reply()/SetReply()

Remove corresponding fields from agent struct. Agent-level
WaitChange still uses its own changeMu/changeCond since it
observes multiple fields (state, usage, ctxsz, cwd, agent).
2026-07-29 18:28:14 +02:00
Levi Neely 498278665f rename agent.Session → agent.History
The Session type in agent/ is purely a conversation accumulator
(message history, token tracking, compaction state). Rename it to
History to free up 'Session' for the runtime environment concept
in the upcoming agent/session split.
2026-07-29 18:21:34 +02:00
Levi Neely 2ea13cd611 restructure: drop pkg/, split tools/execute into focused packages
- Drop pkg/ prefix (Go anti-pattern): ollie/pkg/X → ollie/X
- Split tools/execute god package:
  - execute/: shell execution, sandboxing, elevation client, remote SSH
  - tools/: interfaces + registry + discovery + schema parsing
  - detach/: background process management (ring buffer, signal)
- Promote internal/sandbox → sandbox/
- Absorb config/ into agent/config.go (agent definition loading)
- Merge remote/ into execute/remote.go (RemoteServer)

All tests pass.
2026-07-29 18:10:25 +02:00
Levi Neely c5b16e739e Restructure: pkg/core interface, internal/ packages, cmd/ollie TUI entry point
- pkg/core: public Core interface (Submit/Prompt/Interrupt) + Event/EventHandler types
- internal/agentcore: AgentCore implementation of Core (session, tools, commands)
- internal/tui: TUI package (readline loop, splitInput, signals, bracketed paste)
- internal/agent,backend,config,exec,mcp,tools: moved from root to internal/
- cmd/ollie: minimal main() that wires agentcore + tui together (~130 lines)
- mkfile: update build target to ./cmd/ollie

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:00:04 +02:00
Levi Neely 868ae09364 Add CodeWhisperer/Kiro backend + backend-aware model defaults
- backend/codewhisperer{,_internal}.go: full Amazon CodeWhisperer
  backend implementation — binary AWS event stream decoding, SQLite
  auth for both enterprise OIDC and personal social/GitHub flows, OIDC
  token refresh, and message encoding to the Kiro wire format
- backend/anthropic.go, copilot.go: new backends wired into New()
- backend/new.go: register anthropic, copilot, kiro/codewhisperer cases
- backend/openai.go: add extraHeaders hook for future use
- agent/loop.go: surface non-standard stop reasons as errors instead of
  silently dropping them
- main.go: defaultModelForBackend() sets a sensible default per backend
  (ollama→qwen3.5:9b, openrouter→deepseek/deepseek-v3.2,
   anthropic→claude-sonnet-4-5, kiro→auto); /backend switch now also
  resets the model to avoid stale foreign model IDs causing
  ValidationException; add -prompt flag for non-interactive batch mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:02:23 +02:00
Levi Neely 939f41013e agent: fix stall state always firing on normal text responses
The "no tools" stall condition (totalToolCalls==0 && hadContent) fired
whenever the bot replied in plain text without calling any tools, which
is the normal completion path. The UI would then see agentStalled before
the done message and never transition back to agentIdle.

Removed the "no tools" stall; only the max-steps limit case is a real
stall worth surfacing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 10:51:10 +02:00
Levi Neely b45f81908e agent: fix compact to proactively summarize older messages
Compact was gated on EvictedMessages(), which only returns messages
that have spilled past the 120k char soft limit. Normal sessions never
hit that threshold, making /compact a permanent no-op.

New approach: compact ALL messages older than the tail window (i.e.
everything computeTailStart puts before the protected tail), regardless
of budget. This makes /compact always useful.

Added OlderMessages(), TailWindow(), and SystemMessages() to
ContextBuilder to support the rewrite cleanly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 10:49:39 +02:00
Levi Neely bc8c6b75b5 session,dedup,compact: persistence, hard errors, summary display, write-then-write
Session persistence:
- Sessions saved to ~/.config/ollie/sessions/{id}.json after each
  completed agent turn and after /compact
- --session <id> flag resumes a saved session, auto-loading its agent
- /sessions command lists saved sessions with agent and goal preview
- /clear and /agent switch generate a new session ID
- Session ID shown at startup

Dedup:
- file_read: overlap check now returns a hard error (was a warning
  prepended to the result); check uses requested range, not actual
  read range, to avoid redundant dispatchFileRead on overlap
- tool calls: duplicate (name, args) now returns a hard error; key
  is only recorded on successful execution

/compact: Compact() now returns the summary text; displayed in the
  UI so the user can verify quality; session saved after compact

Write-then-write:
- After a successful file_write, repopulate fileRanges with the
  written range so a follow-up write to the same region does not
  require a re-read
- Whole-file writes record exact new line count from content;
  range writes record the written [start, end] with totalLines=0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 10:46:01 +02:00
Levi Neely 1c4c3049ac agent: reduce repetition and blind writes with dedup caches and range tracking
- Revised system prompt with concrete prohibitions against verbosity and
  premature stopping
- Added GenerationParams (max_tokens, temperature, frequency/presence penalty)
  threading from agent config through backend ChatStream calls
- Added stall detection: emits "stalled" role on max-steps hit or zero tool
  calls with content, surfaces as "stalled" in status bar
- Added per-session file read range tracking: warns on overlapping re-reads,
  blocks file_write unless the target range was previously read
- Added general tool-call dedup: warns on exact (name, args) repeats for
  non-file tools
- Both caches invalidated on /compact and /clear; file read cache invalidated
  per-path on file_write
- Updated all agent configs with documenting defaults for new generation params

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-06 21:29:20 +02:00
Levi Neely 92db8f88cd fix: null content on tool_calls messages; skip empty tool names; rollback on error 2026-04-05 11:53:36 +02:00
Levi Neely bd06d83a0e remove tool output truncation; raise context limits 2026-04-04 22:34:43 +02:00
Levi Neely 974b3b7461 increase context limits and replace tool output truncation with retry hint 2026-04-04 22:27:19 +02:00
Levi Neely e48617fc27 implement /compact and /clear commands
- /compact: summarizes evicted context messages via LLM call, replaces
  them with a single summary system message
- /clear: resets session and display
- ContextBuilder.EvictedMessages(): returns messages outside bounded window
- Session.Compact(): drives the summarization and history replacement
2026-04-04 21:51:30 +02:00
Levi Neely cd59dce4ca add user confirmation for file_read/file_write; fix display whitespace
- file_read and file_write require explicit y/n confirmation before executing
- New agentConfirming UI state with confirm [y/n] status bar indicator
- y/yes approves, n/no denies, anything else denies and falls through to
  normal prompt handling
- file_read output includes line numbers for precise file_write targeting
- Fix tool output display: remove per-line squashWhitespace (preserves indentation)
- file_write description hints to preserve formatting
2026-04-04 21:19:38 +02:00
Levi Neely 70a50d4ed8 fix interrupt handling: propagate context to subprocess tree
- Pass caller context into Executor.Execute so cancellation reaches
  the subprocess immediately
- Use Setpgid + SIGKILL on process group to kill grandchild processes
- Add context to ToolExecutor signature and thread it through dispatch
- Reset agent state to idle after drainAgent
- Rollback incomplete session turn on interrupt
2026-04-04 18:18:02 +02:00
Levi Neely 078428881c agent: revert auto-nudge — model choice is the right fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:30:57 +02:00
Levi Neely 76c5e696d5 agent: auto-nudge when model narrates intent without acting
When the model emits a text-only turn containing narration phrases
("let me", "i'll", "i will", etc.) with no tool calls, the loop now
injects an ephemeral "Continue. Act now." user message and runs another
step rather than treating the turn as completion. Capped at 2 nudges
per run to prevent infinite loops. UI shows "[nudge: continuing…]" so
the user can see what happened.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:28:11 +02:00
Levi Neely ab575ee385 backend,agent,main: retry on HTTP 429 with live countdown
Return RateLimitError from openai backend on HTTP 429, parsing the
Retry-After header (integer seconds or HTTP-date). The agent loop retries
up to 3 times with exponential backoff (5s/10s/20s) when no header is
given, emitting per-second countdown ticks. The status bar renders
"retry {N}s" during the wait.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:25:05 +02:00
Levi Neely 5aea341255 context: fix budget accounting, tail counting, and output limits
Five improvements for cost/focus efficiency:

1. Budget now accounts for fixed per-request overhead (system prompt +
   tool schemas), which were previously invisible to ContextBuilder and
   could silently exceed intended limits by 30-50%.

2. Tail counting now only counts user and plain-assistant (no tool_calls)
   messages toward TailMessages quota. Processed tool exchanges between
   conversational turns are included but do not crowd out genuine context.
   Trailing in-progress exchanges are always preserved and free.

3. BoundedHistory / BoundedHistoryWithNotice deduplicated into a single
   buildBounded(injectNotice bool) private helper.

4. O(n²) prepend in the greedy inclusion loop replaced with
   append+slices.Reverse.

5. exec.Executor.MaxOutputChars replaces the hardcoded 8000-char const.
   Initialised from OLLIE_TOOL_OUTPUT_CHARS env var, defaults to 8000.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 20:27:16 +02:00
Levi Neely b7d46015d7 agent: simplify loop — Run as function, fix stop condition
- Loop struct and New constructor removed; Run is now a package-level
  function taking (ctx, Config, State)
- Stop condition de-nested: natural stop (no tool calls → MarkComplete
  + break) and step-limit stop (step >= maxSteps-1 → break) are now
  separate, sequential checks instead of a redundant outer/inner pair

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 20:13:14 +02:00
Levi Neely 474eb84e2d agent,backend: drop non-streaming path
Both backends already implement streaming; the non-streaming fallback
was dead code that added complexity.

- Backend interface now requires only ChatStream; the separate
  StreamingBackend interface and Chat method are removed
- Both OpenAIBackend and OllamaBackend lose their Chat methods and the
  stream=bool parameter on their internal doChat helpers
- Loop.Run is simplified: one streaming path, no streamed flag, no
  skippedCalls map, no shouldStop helper, no runStreamStep indirection
- "call" events are now emitted solely in the act phase, not in the
  stream phase, so dedup tracking is no longer needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 20:11:10 +02:00
Levi Neely 45a32ed4e7 agent: fix conversation history corruption on stream interruption
Two bugs that combined to produce unrecoverable 400 errors from backends:

1. loop.go: runStreamStep returned nil on stream-closed-without-done,
   causing Run to commit a partial assistant message (with no ToolCalls)
   to state even when the backend had already sent tool call frames.
   Now returns a real error so state.Update is never called and the
   session history stays clean for a retry.

2. context.go: BoundedHistory and BoundedHistoryWithNotice could evict
   an assistant[tool_calls] message while its paired tool[result] messages
   remained in the tail window, producing a tool message with no preceding
   assistant — rejected by strict backends (OpenAI, DeepInfra) with a 400.
   Fixed by dropping assistant+tool pairs atomically in the hard-limit loop
   and adding a sanitizeHistory pass that strips any remaining orphaned
   tool messages before the history is returned.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:59:45 +02:00
Levi Neely ffc3f07725 agent: emit 'call' events during streaming
In the streaming path all tool calls were being added to skippedCalls,
causing the post-stream Act loop to suppress every 'call' emit. The
'-> tool(args)' lines therefore never appeared in the UI.

Fix: emit the 'call' OutputMsg inside runStreamStep once the stream
reports Done and all tool-call arguments are fully accumulated. The
Act loop still skips re-emitting them (correct), while the UI now
sees one 'call' event per tool invocation.
2026-04-01 13:35:59 +02:00
Levi Neely 84ad7451a7 agent: add Usage field to OutputMsg
OutputMsg.Usage carries the raw backend.Usage value when Role=="usage",
rather than formatting it into Content. main.go can now read em.Usage
directly instead of parsing the formatted string.
2026-04-01 13:28:12 +02:00
Levi Neely 833f9e0a00 agent: add bounded context builder to prevent prompt explosion
Add agent/context.go with a ContextBuilder that enforces a character budget
(soft + hard) over the assembled history, evicting oldest non-system messages
first and replacing large tool outputs with truncated summaries.

Update agent/session.go to use ContextBuilder in History(), so the loop
always sees a bounded context window regardless of how many steps have run.

The budget defaults are:
  - SoftLimit: 24000 chars (~6k tokens at 4 chars/token)
  - HardLimit: 96000 chars (~24k tokens)
  - MaxToolOutputChars: 2000
  - TailMessages: 6 (always keep last N messages verbatim)

All limits are configurable via ContextConfig.
2026-04-01 13:10:56 +02:00
Levi Neely 71ff058b1a Fix: skip [0 0 tokens] when counts are zero 2026-04-01 12:56:35 +02:00
Levi Neely d60a1b9215 Add streaming support for bot output and tool execution
- backend: StreamingBackend interface + StreamEvent type
- backend/ollama: ChatStream with line-by-line JSON-LD decoder
- backend/openai: ChatStream with SSE delta parser + tool arg accumulation
- agent/loop: runStreamStep reads deltas, emits OutputMsg per chunk,
  deduplicates call/tool display between stream and outer loop
- main: agentMsg tea type streams incremental updates to the TUI;
  plain string buf replaces strings.Builder (avoids copy-by-value panic);
  drainAgent cancels in-flight goroutines cleanly on user interrupt
2026-04-01 12:52:58 +02:00
Levi Neely f9ba6c8bce Track and display token usage per request
Add Usage{InputTokens, OutputTokens} to backend.Response. Both
OllamaBackend (prompt_eval_count/eval_count) and OpenAIBackend
(prompt_tokens/completion_tokens) populate it. The loop emits a
'usage' OutputMsg after each Chat call; the UI displays it as
[↑N ↓N tokens].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 00:31:03 +02:00
Levi Neely cf56df2ba0 Show tool invocations in chat; fix planning loop stall
Emit a 'call' message before each tool execution so the UI shows
execute_code(...) with full args before the result. Result lines
are now indented with '= ' to visually pair with the call.

System prompt now explicitly says to call execute_code immediately
after planning, without pausing for confirmation, to prevent smaller
models stalling after the planning step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 00:02:31 +02:00
Levi Neely 07a212884a Add system prompt; delete BeadState
BeadState duplicated what execute_code already provides via tool scripts.
Removed it entirely. Added SystemPrompt field to agent.Config, prepended
as a system message each loop iteration. Built-in prompt covers
execute_code usage, sandbox behavior, and skill discovery.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 23:18:40 +02:00
Levi Neely 25cc7a8c33 Wire agent loop into TUI: replace ad-hoc ollama loop with backend+agent
main.go now uses backend.New() for LLM selection (OLLIE_BACKEND env),
agent.Loop for observe/decide/act/update/terminate, and ollie/exec for
built-in execute_code. Multi-turn sessions persist across user inputs.
MCP tools route through mcpExecutor; unknown tools fall back to built-in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:57:08 +02:00
Levi Neely 54e4f44635 Add agent package: observe/decide/act/update/terminate loop
Implements the core agent loop with two state backends:
- Session: ephemeral in-memory, lives for one process lifetime
- BeadState: bead-backed via 9beads tool scripts (claim/read/complete)

Loop is backend-agnostic; takes a ToolExecutor and emits OutputMsgs.
MaxSteps defaults to 1 for simple prompts; callers set higher for tasks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:53:40 +02:00