Skip to content

fix: friendly error when --model is missing - #1

Closed
reno wants to merge 26 commits into
developfrom
worktree-staged-chasing-hare
Closed

reno wants to merge 26 commits into
developfrom
worktree-staged-chasing-hare

Conversation

@reno

@reno reno commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • config.Load now rejects an empty --model with a clear ErrModelRequired error naming the flag, instead of letting it reach a provider adapter and fail as an opaque transport error (404 from Anthropic, a failed connection attempt to Ollama).
  • Updated TestChatProviderEnvFallback / TestChatFlagOverridesEnv to pass --model now that it's required; added TestChatMissingModelIsClearError and a config-level empty model case.

Test plan

  • go test ./...
  • gofmt -l . / go vet ./...
  • golangci-lint run

https://claude.ai/code/session_014QmC7oUExcrEMLRtQK75RV

reno and others added 26 commits August 26, 2026 21:17
Approval lives in the agent loop, not the tool, per CLAUDE.md invariant 5:
tools declare NeedsApproval via an optional interface, the loop consults an
injected Approver before executing, and a denial becomes a normal
IsError ToolResult so the loop continues.
Atomic temp+rename write with a line-diff Preview surfaced to the loop's
approval prompt; wires the --allow-write flag CLAUDE.md's CLI skeleton
task deferred to this phase.
StreamEvents centralizes the close-exactly-once/exit-on-cancel contract in
one place so 6.2/6.3's adapters don't each reimplement it; CollectStream is
the shared consumer-side counterpart that reassembles a canonical Response
from an event stream, reused by both adapters' equivalence tests.
Accumulates input_json_delta fragments as raw text per content-block index
and unmarshals once at content_block_stop, never parsing a partial chunk.
Verified against a 7-chunk fixture that splits a tool argument's \u escape
mid-character, asserting byte-identical reassembly and Response-level
equivalence with the non-streaming path.
Unlike Anthropic, Ollama doesn't fragment a tool call's arguments across
lines, so each line's tool_calls become an immediate ToolUseStart/
ToolUseDone pair rather than accumulated deltas. Reuses the existing
string-vs-object argument normalization per line.
One shared suite (canonical round-trip, tool call, error surfaces as an
error, cancellation, stream/non-stream equivalence) driven by a small
Harness each adapter supplies; the suite itself never imports a provider
SDK, so a third provider only needs its own Harness.
Renderer consumes a provider's llm.Event stream and returns the assembled
Response, mirroring llm.CollectStream but with the side effect of printing
as text arrives. PlainRenderer is the default, non-interactive
implementation, plus an optional ToolStatusReporter hook renderers can
implement to learn about tool runs the loop performs after a round's
events finish.

Agent gains RunStream, sharing round bookkeeping (guards, repetition,
history) with Run via a new runRound helper so the two entry points can't
drift on policy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
Model implements the interactive TUI's state machine (idle / streaming /
tool running / approval modal) over bubbles' viewport, textarea, and
spinner, with glamour rendering finished assistant text. TUIRenderer and
TUIApprover bridge the agent loop's Renderer/Approver contracts to a
running *tea.Program via Send, keeping Model itself free of any agent or
provider dependency. docs/tui-check.md is the manual walkthrough 7.2's AC
calls for; it can't be automated.

runChat is no longer a placeholder: it resolves a real provider via
llm.New, builds a tool registry (read_file/list_dir, write_file behind
--allow-write — run_command is left out, since it needs a binary allowlist
config has no flag for yet), and drives either the plain or TUI path
through Agent.RunStream. It's now a package var so cmd/pico's tests can
substitute a stub instead of exercising a real provider.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
Agent records each completed turn's total Usage (summed across that turn's
provider rounds, since a turn can span several tool-calling rounds) and
exposes TurnUsages/CumulativeUsage. The Ollama adapter estimates
InputTokens/OutputTokens from character length whenever a response's real
counts come back 0, in both Chat and Stream, so cumulative tracking keeps
making progress against a version/model that omits them.

cmd/pico gains a small slash-command dispatcher in the plain REPL; /usage
is its first command, printing cumulative and last-turn totals. Split out
runREPL so it's testable without a real terminal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
history.CompactionBoundary finds a safe cut point by construction: it
always lands at the start of a genuine user turn (one carrying a Text
block), and a new turn can only start once every earlier ToolUse has been
answered (CLAUDE.md invariant 3) — so the boundary can never split a pair.
History.Compact replaces everything before it with one synthetic assistant
message and refuses to commit a result that fails Validate(), as a
backstop rather than the primary safety mechanism.

Agent.maybeCompact is the trigger policy: once EstimateTokens(history)
crosses a configured fraction of the context window, it asks the provider
to summarize the elided turns (prompt pinned by a golden test) and calls
Compact. It's best-effort — a failed summarization call just skips
compaction for this turn rather than failing it. cmd/pico wires a default
policy (Ollama's num_ctx, or a constant for Anthropic since config has no
per-model window size yet) into both the plain and TUI paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
History gains LoadInto (replace a shared *History's messages in place,
validating before committing so a corrupt file never leaves it partially
loaded) and Reset. cmd/pico's new session type resolves a sessions
directory and a named session's file path, used both at startup
(--session resumes an existing file or starts a fresh one) and by the
plain REPL's /new, /save, and /load commands. History auto-saves after
every turn in both the plain REPL and the TUI driver, so killing the
process mid-session and resuming replays the full prior context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
recordutil.Recorder wraps an http.RoundTripper to capture a live
request/response pair without altering how it's sent; Scrub redacts exact
secret values plus any sk-shaped literal. Each adapter gets a
TestRecordLiveExchange, skipped unless RECORD=1 and real credentials are
present, that drives one real Chat call and writes the scrubbed exchange
to testdata/golden/live_exchange — never touching the network in normal
`make test` runs. TestGoldenFixturesContainNoSecrets statically greps
every checked-in fixture for sk- and the live API key env var.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
config.LogLevel is now a validated type (ErrUnknownLogLevel joins
ErrUnknownProvider/ErrUnknownToolsMode), and cmd/pico's setupLogging
points the default slog logger at stderr with that severity, so piped
chat output on stdout stays clean regardless of level.

recordutil gains LogJSON/LogBytes: a debug-only, level-gated dump that
scrubs and truncates a request/response before it ever reaches slog.
Both adapters call it around their Chat and Stream exchanges. Anthropic's
Provider keeps its API key only for exact-value redaction — the SDK never
puts it in the request body, only a header, so recordutil's generic
sk-shaped pattern already covers the realistic case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
lint-test runs make lint, make test, and a separate go test -race pass on
Go 1.26 and 1.27 (the floor ollama/ollama/api's own go.mod raised it to in
phase 3.1). build cross-compiles for linux/darwin amd64/arm64 with
CGO_ENABLED=0 — verified locally for all four combinations before adding
this. Neither job needs any secret: the suite is offline by construction,
and 9.1's RECORD=1-gated live tests stay skipped without it set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
Covers what the project is, install/quickstart for both backends, the
full flag table, the plain REPL's slash commands vs the TUI, the safety
model, a mermaid diagram of the agent loop, and an honest section on what
building against a small local model actually exposed (invented tool
names, repeated calls, string-vs-object arguments, the UTF-8/JSON decoder
gap found while designing 6.2's SSE fixture).

Writing this surfaced two real gaps that weren't visible before: --tools
and --stream are parsed and validated but never read by cmd/pico, so
selecting prompted-tools mode or turning off streaming currently does
nothing. Documented under Known Limitations alongside the run_command and
TUI-slash-command gaps already noted in earlier commits, rather than
silently fixed under this task's banner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
resolveProvider wraps the resolved provider with prompted.Wrap when
--tools=prompted, rejecting that combined with --tui up front (a clear
startup error) rather than letting it fail confusingly on the first turn
— prompted.Provider.Stream always errors, and the TUI always drives the
agent through RunStream.

newTurnRunner picks Run or RunStream per --stream, printing Run's result
itself since PlainRenderer only ever consumes a stream. --tools=prompted
forces non-streaming regardless of --stream, since the same Stream
limitation applies there too; a log line says so when it overrides an
explicit --stream=true.

Updates the README's Known limitations section to drop the two entries
this closes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
config.AllowCommands carries a comma-separated binary allowlist from the
new --allow-commands flag. buildRegistry registers run_command only when
it's non-empty, using the same defaultToolTimeout the agent loop already
applies to every other tool — an empty allowlist would leave the tool
present but unconditionally useless, so it stays unregistered instead.

Updates the README: documents the flag, drops the now-closed
run_command entry from Known limitations, and fixes a stale cross-
reference to it left over from the --tools/--stream wiring commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
ui gains CommandOutput/commandOutputMsg: a way to append text to the
transcript that, unlike a turn, never touches the spinner/streaming state
machine (cmd/pico's driver sends it without ever sending TurnStarted).

cmd/pico's TUI driver goroutine now recognizes a leading slash the same
way the plain REPL's runREPL does, via the same slashCommand/handleCommand
pair — /usage, /new, /save, /load work identically in both. Since the TUI
never echoes what was typed, runTUICommand prefixes the output with the
command line itself so it's clear what ran. Split out from the driver
goroutine so it's testable without a running *tea.Program, mirroring why
runREPL was split out from runPlainChat.

Updates README (both surfaces now documented as understanding the same
commands; the "TUI doesn't parse slash commands" Known Limitation is
gone) and docs/tui-check.md with a manual check for the four commands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
config.ContextWindow carries a new --context-window flag (default
200,000, same value the hardcoded constant used) into
compactionPolicy for every provider except Ollama, which keeps using
NumCtx — that's already an explicit, required value, so compaction
reuses it rather than tracking a second number that could drift from it.

Removes the README's now-empty Known Limitations section: this was the
last entry in it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1KJRVUrFgkDupk6NzkHKq
config.Load now validates Model is non-empty, failing fast with a message
that names --model instead of letting an empty model reach a provider
adapter and surface as an opaque transport error (404 from Anthropic, a
failed local connection to Ollama).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QmC7oUExcrEMLRtQK75RV
@reno reno closed this Aug 29, 2026
@reno
reno deleted the worktree-staged-chasing-hare branch August 30, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant