Skip to content

feat(p10): upstream retry, token cost metering, and diagnostics port - #5

Merged
ROM4n2 merged 2 commits into
masterfrom
feat/p10-upstream-resilience-and-cost
Oct 27, 2026
Merged

ROM4n2 merged 2 commits into
masterfrom
feat/p10-upstream-resilience-and-cost

Conversation

@ROM4n2

@ROM4n2 ROM4n2 commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Closes the production gaps that P9 left open, each traced to a physical code fact rather than prose.

What was actually broken

  1. Recoverable upstream failures were recorded as terminal failures. llm.Client returned *APIError for any non-200. DeepSeek answers 429/503 with Retry-After at quota boundaries and during rolling deploys; the service wrote task.error = "upstream error" — reporting "wait a second" as "service broken" and discarding an agent loop that had already burned 3-4 steps.
  2. Token cost was invisible. llm.Usage was consumed only by the eval harness. Runtime exposed no token metric and persisted nothing, so "how much did today cost" had no answer.
  3. Session window and diagnostics were hardcoded. worker/session.go pinned the history fetch at 100; grep -rn pprof had zero hits, so goroutine/memory problems required a restart to reproduce.

Changes

llm/retry.go — doWithRetry on all four send paths (Chat, Embed, ChatWithTools, ChatWithToolsStream):

  • retry whitelist 429/500/502/503/504; 400/401/403/404/422 never retried (auth and body errors repeat identically and multiply the bill)
  • Retry-After parsed as delta-seconds or HTTP-date, capped by MaxDelay — upstream may ask for 60s, but holding a worker that long costs more than abandoning the retry
  • backoff selects on ctx.Done(), so task timeout, user cancellation and shutdown take effect mid-wait
  • the request is rebuilt per attempt: http.Request.Body is not re-readable
  • streaming retries only before the first delta. A mid-stream break is terminal; retrying would replay text already sent to the client

Token cost, two channels — agent_llm_retries_total, agent_llm_prompt_tokens_total, agent_llm_completion_tokens_total counters, plus prompt_tokens/completion_tokens on store.Task written through a separate TaskStore.RecordUsage. Metering stays out of the terminal-state transition, runs on the failure path too (failed calls still bill), and a metering error never flips a successful task to failed. Old databases gain the columns via an idempotent migration.

Config — session_history_limit (default 100) replaces the hardcoded window; pprof_addr gates a diagnostics listener on its own port and mux, off by default, warning when bound to a non-loopback address (the pprof index enumerates every profile, so the main mux's public / demo route is the wrong place for it).

Architecture gates — pool.go hit 401 lines and pool_test.go hit 810, so cancel handling and the store double moved into sibling files rather than raising the 400/800 budgets.

Verification

Gate Result
gofmt -l . empty
go build ./... pass
go vet ./... pass
go test -race ./... 545 passed / 12 packages / 0 races (P9 baseline 506)
TestArchitecture 2 passed (AST DAG + line budgets, 99 files)
TestSubmitLatencyPercentiles 2000 requests @ 32 workers: 0 non-202, p95 525us, p99 1.05ms, 172k req/s

Deliberate non-goals

No go-retryablehttp (60 lines of logic for a dependency that does not know about the stream-replay invariant), no unbounded retry, no usage-in-trace-only (the buffer evicts), no token fields inside Complete, no retry after a stream break, and no end-to-end p99 requiring a live API key. All recorded in ADR-0021's rejected-alternatives table.

…nostics port

llm: doWithRetry on all four send paths — retry only 429/500/502/503/504,
honour Retry-After (seconds or HTTP-date) capped by MaxDelay, backoff
selects on ctx.Done so timeout/cancel/shutdown take effect mid-wait, and
the request is rebuilt per attempt because http.Request.Body is not
re-readable. Streaming retries only before the first delta: a mid-stream
break is terminal, otherwise already-emitted text would be replayed.
OnRetry callback injects metrics from the composition root (the llm
package's allowed import set is empty in the AST gate).

metrics/store/worker: agent_llm_{retries,prompt_tokens,completion_tokens}
counters plus per-task prompt_tokens/completion_tokens columns written via
a separate TaskStore.RecordUsage — metering stays out of the terminal-state
transition, failed calls still bill, and a metering error never flips a
successful task to failed. Old databases gain the columns through an
idempotent migration.

config/worker: session_history_limit (default 100) replaces the hardcoded
window; pprof_addr gates a diagnostics listener on its own port and mux,
off by default, warning when bound to a non-loopback address.

pool.go and pool_test.go hit the 400/800 line budgets, so cancel handling
and the store double moved into sibling files instead of raising the cap.
…hors

ADR-0021 carries the rejected-alternatives table (no retryablehttp, no
infinite retry, no usage-in-trace-only, no token fields inside Complete,
no pprof on the main mux, no retry after a stream break) so the next
session does not re-litigate them.

README: 14 config keys, agent_llm_* metrics, per-task token fields in the
GET /tasks contract, and the load-probe evidence.
ARCHITECTURE: P1-P10 scope, production capabilities, diagnostics in the
observability subsection, ADR-0021 in the index.
.agent-context: refreshed baseline (360 func Test / 545 RUN / 99 files /
20,851 lines), new retry and metering red lines.
WORKMEMORY: PROJECT_OVERVIEW P10 entry, work.log event, notes file, INDEX row.
@ROM4n2
ROM4n2 merged commit e084ae6 into master Oct 27, 2026
1 check passed
@ROM4n2
ROM4n2 deleted the feat/p10-upstream-resilience-and-cost branch October 27, 2026 13:25
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