Agents
@frites/agents (packages/agents) is frites's adapter layer over the headless coding CLIs. It knows how to invoke each backend, stream and parse its events, normalize token usage and cost across providers, scrub the child environment for recursion safety, and reap a stalled child. Its only dependency is @frites/core, whose structural interfaces it satisfies so the engine never spawns a process itself.
For how runners fit into the overall execution model, see Agents and runners.
Exports
packages/agents/src/index.ts re-exports:
runner.js
CliRunnerDef, makeRunAgent, RunAccumulator: the engine-path RunAgentFn factory
completion.js
runCompletion, parseClaudeLine, parseCodexLine, ChildEvent, CompletionResult, StreamAcc: the answer-council path
backend-errors.js
classifyBackendFailure, ModelBackendError, backendFailureFrom: normalized rate/usage/auth/context failure metadata
backend-policy.js
BackendSuppressionController: provider suppression and alternate-provider selection after retryable backend failures
claude.js / codex.js
claudeRunner and codexRunner CliRunnerDefs
env-sandbox.js
buildChildEnv, assertDepth, currentDepth: the recursion + secret boundary
timeout.js
startIdleTimeout: the idle/hard reaper
pricing.js
Re-exports estimateCostUsd, pricingFor, UsageTokens from @frites/core (back-compat)
defaultRunners is the shipped list: [claudeRunner, codexRunner].
Backend suppression and retry policy
Backend failures are classified first, then the coordinator decides what to do with them. Provider/account-scoped failures (usage-limit, rate-limit, quota-exceeded, auth, and short backend overloads) suppress that provider kind (claude-cli or codex-cli) for later calls. Reset timestamps or retry-after values win when the backend provides them; otherwise frites uses conservative TTLs: five hours for usage limits, one hour for quota, ten minutes for auth, five minutes for rate limits, and one minute for overloads. Prompt-shape failures such as context-length, cancellations, and unknown exits are not suppressed.
The gateway can retry the same logical child or synthesizer call through another configured unsuppressed provider. It does not retry a final-answer synthesizer after answer text has already streamed to the client, because those tokens cannot be retracted. Background/utility turns (which pin a small, cheap model) are the exception: they are not failed over to the full-price default agents — a suppressed cheap provider simply fails the cheap turn rather than silently escalating it to a premium council agent. Worktree mode records the same suppressions and uses them to avoid suppressed providers on later stages such as synthesis, but it does not automatically rerun a failed child in the same worktree: a backend can fail after partial edits, and retrying a different provider on top of those edits would blur candidate ownership.
Runners (runner.ts)
A CliRunnerDef describes one CLI backend: its kind, its command, a buildArgv(spec, ctx), and an onLine(line, emit, acc) parser. makeRunAgent({ runners, config, passApiKeys }) indexes the runners by kind and returns the RunAgentFn the engine calls.
Before spawning, makeRunAgent asserts the recursion depth, builds the scrubbed child env, and applies config defaults onto the spec so a per-child budget/timeout/reasoning value always takes effect even when the spec omits it:
maxBudgetUsd←config.perChildBudgetUsdtimeoutMs←config.perChildTimeoutMs(idle)hardTimeoutMs←config.perChildHardTimeoutMs(absolute, off when unset)reasoningEffort←config.codexReasoningEffort(codex only; claude ignores it)
spawnAndStream spawns the CLI detached (its own process group, so it can be tree-killed via process.kill(-pid, …)), pipes the prompt over stdin and closes it (the EOF is what stops the child waiting for input; a real transcript would exceed ARG_MAX and trip spawn E2BIG if passed as argv), buffers stdout into newline-delimited lines for onLine, writes a combined log to a temp file, and resolves an AgentRunOutput with a status of succeeded, errored, or timed-out. On nonzero backend exits it classifies common rate-limit, usage-limit, auth, context-length, quota, and overload failures into backendFailure metadata while preserving the raw temp log. On idle timeout or abort it sends SIGTERM then escalates to SIGKILL after a 3000ms grace.
Claude runner (claude.ts)
Headless Claude Code, invoked as claude -p --output-format stream-json --verbose --permission-mode bypassPermissions --strict-mcp-config --setting-sources project, with --model and --max-budget-usd appended from the spec. It reuses the machine's subscription OAuth (keychain), so no API key is needed. --strict-mcp-config plus --setting-sources project keep the child from auto-loading frites's own MCP (a recursion guard). The onLine parser emits progress for tool uses, captures the assistant text/result as the summary, reads total_cost_usd as the authoritative cost, and sums Anthropic's disjoint input categories (fresh + cache-read + cache-creation) into the normalized input total; output_tokens already includes thinking, so no reasoning fold is needed.
Codex runner (codex.ts)
Headless Codex, invoked as codex exec --ignore-user-config --json --skip-git-repo-check -s workspace-write -C <cwd> -c approval_policy="never", then -c model_reasoning_effort="<v>" (when set), -m <model> (when set), and - (read prompt from stdin). It reuses the machine's ChatGPT sign-in (~/.codex/auth.json); approval is set via -c approval_policy="never" because the --ask-for-approval flag exits 2, and the workspace-write sandbox lets it edit within the worktree. --ignore-user-config prevents loading config.toml (which could route to the gateway and recurse). The NDJSON schema drifts between versions, so the parser is defensive: it pattern-matches event types for progress, captures the latest message as the summary, passes input_tokens through (codex's value is already the inclusive total, with cached as a subset), and folds reasoning_output_tokens into output_tokens so the total is comparable with claude. cost_usd is honored when present (the API-key path); the ChatGPT backend usually omits it.
model_reasoning_effort="minimal"is not safe on the stock codex model. It 400s because it is incompatible with the built-inweb_search/image_gentools. frites shipshighas the default, so uselow/medium/high.
Completions (completion.ts)
runCompletion(kind, prompt, opts) is the answer-only path used by the answer council: a single agent runs read-only (no worktree, no editing) and returns its text plus normalized cost/tokens, streaming ChildEvents (start/text/reasoning/tool/usage) live via opts.onEvent.
Claude runs with
--output-format stream-json --verbose --include-partial-messagesfor token-level deltas,--strict-mcp-config,--setting-sources project(neveruser, which could setANTHROPIC_BASE_URLto the gateway and fork-bomb), and--disallowedTools Edit Write NotebookEditas a read-only guard.Codex runs with
-s read-onlyand-o <file>(a final-message fallback written outside the repo if the event stream yields noagent_message), matching the execute path's reasoning depth.
It runs in the caller's real repo when opts.cwd is a valid absolute path (so reads actually work), otherwise in a temp scratch dir; only scratch dirs frites creates are cleaned up. parseClaudeLine and parseCodexLine are pure, fixture-tested per-line parsers shared by this path, handling both codex's newer thread/turn/item events and the legacy msg-wrapped shape.
Environment sandbox (env-sandbox.ts)
The child environment is built by allowlist, never by copying process.env. This is the recursion guard and secret-minimization boundary for full-auto agents.
buildChildEnvcopies only theALLOWLISTvars (HOME,PATH, locale,CODEX_HOME,CLAUDE_CODE_OAUTH_TOKEN, the XDG dirs, …), optionally passesANTHROPIC_API_KEY/OPENAI_API_KEYwhenpassApiKeysis set, then (as defense in depth) deletes every base-URL var inSCRUB_EXACT(ANTHROPIC_BASE_URL,ANTHROPIC_API_URL,OPENAI_BASE_URL,OPENAI_API_BASE,CODEX_BASE_URL) so a child can never be pointed back at frites. It then setsFRITES_DEPTH = depth + 1andFRITES_CHILD = 1.currentDepthreadsFRITES_DEPTHfrom the env;assertDepth(depth, maxDepth)throws the recursion-fuse error whendepth >= maxDepth.
Timeouts (timeout.ts)
startIdleTimeout({ idleMs, hardMs, onFire }) reaps a child that has gone silent, not one that is merely slow. touch() (called on every chunk of child output) resets the idle countdown, so a child that keeps streaming runs as long as it stays productive. Only a genuine deadlock, stalled read, or output-less spin trips it. hardMs is an optional non-resetting absolute ceiling for the pathological "spinning forever while still dribbling bytes" case (off when undefined/0). onFire runs at most once, with whichever timer tripped first, and touch() is inert afterward. Both the runner and completion paths drive their reaping through this controller.
Pricing (pricing.ts)
A thin back-compat re-export of estimateCostUsd, pricingFor, and UsageTokens from @frites/core, so the engine path and the answer-council path estimate child spend identically from one source of truth. See Cost telemetry and Pricing.
Last updated