# ATLAS API Reference API endpoints for each ATLAS service. All services communicate over HTTP/JSON. Streaming endpoints use Server-Sent Events (SSE). > **Ports listed are defaults.** All are configurable via environment variables (see [CONFIGURATION.md](CONFIGURATION.md)). The HTTP contracts below are deployment-mode-agnostic — only the host:port differs. Quick reference: > > | Service | Docker Compose (host=container) | Bare metal (env-configured) | K3s NodePort (`atlas.conf.example`) | > |---|---|---|---| > | atlas-proxy | `8090` | `ATLAS_PROXY_PORT` (default 8090) | `ATLAS_PROXY_NODEPORT` (default 30080) | > | v3-service | `8070` | `ATLAS_V3_PORT` (default 8070) | `ATLAS_V3_NODEPORT` (default 30070) | > | geometric-lens | `8099` | `ATLAS_LENS_PORT` (default 8099) | `ATLAS_LENS_NODEPORT` (default 31144) | > | sandbox | host `30820` → container `8020` | `ATLAS_SANDBOX_PORT` (default 8020) | `ATLAS_SANDBOX_NODEPORT` (default 30820) | > | llama-server | `8080` | `ATLAS_LLAMA_PORT` (default 8080) | `ATLAS_LLAMA_NODEPORT` (default 32735) | > > The K3s manifests are rendered from `templates/*.yaml.tmpl` into `$K8S_DIR/manifests/` by `scripts/install.sh` at install time. See [SETUP.md Method 3](SETUP.md) for the full K3s deployment. --- ## atlas-proxy (Port 8090) The main entry point. Wraps llama-server with an agent loop, grammar-constrained tool calls, Lens scoring, and sandbox verification. **This is the public client surface.** The canonical client is [atlas-tui](CLI.md), but the contract below is stable and other front-ends (web UIs, editor plugins, CI bots, custom CLIs) can use it directly. **Public client API** — the endpoints a front-end drives. The three a minimal client must implement are `/v1/agent`, `/cancel`, and `/v1/permission`; the rest are optional enrichments. | Endpoint | Method | Purpose | |----------|--------|---------| | `/v1/agent` | POST | Send a user message, stream back a turn (tool calls, results, tokens, completion) as SSE | | `/cancel` | POST | Abort an in-flight `/v1/agent` turn by `session_id` | | `/v1/permission` | POST | Answer a `permission_request` (approve/deny a destructive tool call mid-turn) | | `/events` | GET | Subscribe to a global typed-envelope event broker — same events the TUI's pipeline pane uses | | `/v1/calibration/status` | GET | Lens + ASA compat verdict for the loaded model — what the TUI's Pipeline pane badge reads on startup | | `/feedback` | POST | Record a pass's human verdict (per-file accept/deny and/or pass-level thumbs) as weighted lens training samples | | `/v1/lens/training-status` | GET | Collected lens-sample counts for the loaded model plus a retrain-available flag | **OpenAI compatibility:** | Endpoint | Method | Purpose | |----------|--------|---------| | `/v1/chat/completions` | POST | OpenAI-compatible chat completions — a direct passthrough to llama-server for SDK compatibility | | `/v1/models` | GET | List available models (OpenAI-compatible). `/models` (no `/v1/` prefix) is an alias. | **Diagnostics:** | Endpoint | Method | Purpose | |----------|--------|---------| | `/health` | GET | Liveness — always 200, `status` reports `"ok"` or `"degraded"` | | `/ready` | GET | Readiness probe — 200 only when inference, lens scoring (`lens/ready`), the sandbox, and v3-service are all healthy; 503 otherwise. Use this for load-balancer / orchestrator health checks; use `/health` for informational status. | | `/version` | GET | API version, SSE protocol version, and the full error-code set — see [Versioning and error codes](#versioning-and-error-codes) | **Catch-all:** any unmatched path is proxied directly to llama-server. --- ### POST /v1/agent Tool-based agent endpoint. Sends a user message, runs the agent loop (LLM → tool call → tool result → repeat) until the model emits `done` or hits the turn cap, and streams every step back as SSE. **Request:** ```json { "message": "Add a snake game in Python and verify it runs", "working_dir": "/home/me/projects/snake", "mode": "default", "session_id": "tui-7f3a2c1b" } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | `message` | string | (required) | The user's request | | `working_dir` | string | `"."` | Host-side working directory. Inside the proxy container this is overridden to `ATLAS_WORKSPACE_DIR` (the bind-mount target — `/workspace` by default). The startup wrapper aligns the bind mount to the user's cwd, so writes land in the right place. | | `mode` | string | `"default"` | Permission mode: `"default"` (prompt for destructive ops), `"accept-edits"` (auto-approve `write_file`/`edit_file`/`structural_edit`/`move_file`, prompt for delete/run), `"yolo"` (auto-approve everything) | | `session_id` | string | `""` | Required for `/cancel` and for the interactive permission prompt (`/v1/permission`). The proxy keys the cancel handle and pending permission requests by this id while the turn is running. **Without a session_id, destructive tool calls in `default`/`accept-edits` mode are denied** (there is no channel to answer the prompt) — unattended clients use `mode:"yolo"` or pre-approve tools via `session_allowed_tools`. | | `history` | array | `[]` | Optional. Prior-turn `{role, content}` messages (`"user"` / `"assistant"`) the client wants replayed into the conversation before the new message. Capped at the most recent 40 entries. Omit for a single-turn request. | | `session_allowed_tools` | array | `[]` | Optional. Tool names the user has approved for the whole session (e.g. from an "allow for session" choice). The proxy skips the interactive permission prompt for these. The client re-sends the current list on each turn. | | `bypass_v3` | bool | `false` | Optional. Disables V3 orchestration for the turn. Used by the TUI's `/demo` split-pane baseline. | | `disable_fresh_slot` | bool | `false` | Optional. Keeps the pre-warmed KV-cache prefix instead of requesting a fresh slot. Used by `/demo`. | | `sandbox_subdir` | string | `""` | Optional. Confines the turn to a subdirectory of the workspace (a bare directory name — anything with path separators or traversal is ignored). `/demo` uses one per pane so concurrent sessions don't clobber each other's files. | **Response:** `text/event-stream` of `data: {...}\n\n` lines. The proxy flushes a `: connected\n\n` SSE comment on connect so clients see HTTP/200 immediately, then emits typed events for the duration of the turn, terminated by `data: [DONE]\n\n`. #### Event types on `/v1/agent` Every event has the shape `{"type":"","data":{...}}`. Types in emission order for a typical turn: | Type | When | Payload | |------|------|---------| | `turn_start` | At the start of every agent loop iteration | `turn` (int), `messages` (int), `trimmed` (bool, true if conversation history was trimmed for context window) | | `llm_call_start` | Before each LLM round-trip | `turn`, `messages`, `prompt_tokens` (estimated, chars/4) | | `llm_prompt_progress` | Every ~100 ms while llama-server is in prompt-eval (before any decoded token). Real prompt-eval counters come from llama-server's `/slots` endpoint; when `/slots` returns 404/501 the poller keeps emitting with `processed=0` and the chars/4 estimate as `total` — the elapsed timer is still the useful signal. | `processed` (int), `total` (int), `pct` (0–1 float), `elapsed_ms` (int). Stops as soon as `llm_first_token` fires. | | `llm_first_token` | First streamed delta from llama-server | `prompt_ms` (time-to-first-token in milliseconds) | | `llm_token` | Each streamed delta | `text` (the delta string — typically a token or two) | | `llm_call_end` | LLM call finished | `turn`, `tokens` (this call), `total_tokens` (cumulative for the turn), `ms`, `chars`. On error: `error` is added, `chars` is absent, and `tokens` is `0`. | | `tool_call` | Model emitted a `{"type":"tool_call",...}` JSON | `name` (string), `args` (raw JSON), `turn` | | `permission_request` | A destructive tool call is awaiting approval in `default`/`accept-edits` mode. The turn pauses until the client answers via `POST /v1/permission` (or the client disconnects/cancels, or the fail-safe timeout denies). | `tool_name` (string), `args` (raw JSON), `message` (human-readable description), `tool_call_id` (string — echo back on `/v1/permission`) | | `permission_denied` | The pending tool call was denied (by the client, a disconnect/cancel, or the timeout) | `tool` (the tool name) | | `tool_result` | Tool finished executing | `tool`, `success` (bool), `data` (raw JSON), `error` (string), `elapsed` (Go duration string, e.g. `"245ms"`) | | `text` | Model emitted a `{"type":"text","content":"..."}` JSON (conversational reply) | `content` (string) | | `v3_progress` | V3 pipeline stage that doesn't have a dedicated typed event yet (fallback) | `message` (string) — humanized stage label | | `v3_llm_start` / `v3_llm_end` | V3's internal LLMAdapter started / finished a call (planner, candidate generation, repair, etc.) | `detail` (string), `call` (int), `tokens` (int, on `llm_end`), `elapsed_ms` (int, on `llm_end`), `max_tokens`, `temperature` (on `llm_start`) | | `v3_token` | V3's internal LLM streamed a token | `text` (delta string) | | `v3_reasoning_token` | V3's internal LLM streamed a `reasoning_content` delta. Separate from `reasoning_token` so it targets the V3 streaming row rather than the agent's LLM row. | `text` (delta string) | | `v3_phase` | V3 phase transition (`phase1`, `phase2`, `phase2_allocated`) | `stage`, `detail`, plus the CxGx allocation on `phase2_allocated`: `k` (candidate count, never below 3), `tier`, `base_tier` (the tier C(x) picked before escalation), `gx_escalation` (int, 0/1/2 tiers added by G(x)), `capped_from` (string, the tier before the wall-clock cap lowered it; empty when uncapped), `reason` (`gated`\|`budget_capped`\|`uncalibrated`) | | `v3_plansearch` | PlanSearch step (`plansearch`, `plansearch_done`, `plansearch_error`) | `stage`, `detail`, `plans` (int), `candidates` (int, on `_done`), `tokens` (int, on `_done`) | | `v3_divsampling` | DivSampling step (`divsampling`, `divsampling_done`, `divsampling_error`) | `stage`, `detail`, `slots` (int), `total` (int, on `_done`) | | `v3_sandbox` | Per-candidate sandbox test (`sandbox_test`, `sandbox_pass`, `sandbox_fail`, `sandbox_done`) | `stage`, `detail`, `index` (int), `elapsed_ms` (int), `energy` (float, on `_pass`), `stderr` (string, first 120 chars on `_fail`), `passed` / `total` (on `_done`) | | `v3_select` | Candidate selection (`selected`) | `stage`, `detail`, `index` (int), `energy` (float) | | `v3_lens_per_step` | Per-token lens scoring of a generated candidate. Fires once per candidate. | `stage`, `detail`, `index` (int, candidate index), `source` (`plansearch`\|`divsampling`), `first_off_rails_idx` (int, -1 if none), `gx_score_min` (float), `gx_score_mean` (float), `cx_norm_max` (float), `n_tokens` (int) | | `v3_lens_veto` | A sandbox-passing candidate was rejected because its `gx_min` fell below the model's severe-quality threshold. The candidate is marked failed and re-enters the Phase-3 repair pool; the energy fallback never returns it. Absent when the Lens is uncalibrated. | `stage`, `detail`, `index` (int, candidate index), `gx_score_min` (float), `first_off_rails_idx` (int, -1 if none) | | `v3_structural_veto` | A sandbox-passing candidate was rejected because tree-sitter found direct-identifier calls resolving to no local def, import, builtin, or project symbol. The candidate is marked failed and re-enters the Phase-3 repair pool; the energy fallback never returns it. | `stage`, `detail`, `index` (int, candidate index), `n_unresolved` (int), `unresolved_calls` (string[], up to 5), `n_calls_total` (int) | | `v3_call_chain_context` | Phase-3 repair injected a call-chain context block for the failing function. Informational. | `stage`, `detail`, `function` (string — the failing function name) | | `symbol_index_injected` | Turn-zero auto-injection of function/class snippets for symbols named in the user message. | `matched` (string[] — matched symbol names), `n_files` (int — project files scanned), `skipped` (int — symbols that didn't resolve) | | `pattern_context_injected` | Turn-zero injection of the lens pattern-cache reader's results (`POST /internal/patterns/context`): lessons from previous sessions whose pattern type matches the task, injected as one `[system note]` block. Absent when the lens is unreachable or returns nothing (fail-soft). | `count` (int — patterns injected, ≤3), `types` (string[] — the injected patterns' types) | | `agent_lens_score` | Lens scored a `write_file` or `edit_file` tool call's content. Fires per write/edit before tool execution. | `tool` (`write_file`\|`edit_file`), `turn` (int), `n_tokens` (int), `first_off_rails_idx` (int, -1 if none), `gx_score_min` (float), `gx_score_mean` (float), `latency_ms` (float) | | `agent_lens_intervention` | Lens detected consecutive low-quality writes against the model's `low`/`severe` thresholds and queued a corrective for the next LLM call. Absent when calibration is missing. | `turn` (int), `tool` (string), `reason` (string — the corrective injected into ctx.Messages) | | `agent_repeat_intervention` | Proxy saw the same `(tool_name, args)` signature ≥3× in the last 8 turns and queued a corrective. | `turn` (int), `tool` (string), `reason` (string — the corrective injected into ctx.Messages) | | `agent_reasoning_intervention` | Proxy saw the model's `reasoning_content` open with the same prefix for ≥3 consecutive turns and queued a corrective. | `turn` (int), `consecutive` (int — how many turns the snippet repeated), `snippet` (string — the normalized opening that triggered), `reason` (string — corrective injected into ctx.Messages) | | `reasoning_token` | One delta from a model's optional `reasoning_content` stream during SSE chat completion. These tokens are forwarded to the TUI for live display. Distinct from `llm_token` (which carries the JSON tool-call content destined for parse). | `text` (string — single delta of reasoning prose) | | `reasoning_budget_cut` | The `reasoning_content` stream exceeded the reasoning budget with no content emitted — the proxy cuts the stream and re-prompts. | `reasoning_chars` (int — reasoning chars accumulated when cut) | | `content_loop_cut` | The proxy detected a verbatim repeating tail in the content stream (the model restating itself in a loop) and cut the stream. | `chars` (int — content chars accumulated when cut) | | `v3_repair` | Phase 3 repair strategy (`phase3`, `pr_cot*`, `refinement*`, `fallback`, `fallback_all_vetoed`) | `stage`, `detail`, `strategy` (string: `pr_cot` / `refinement`), `failing` (int), `iterations` (int, on `refinement_pass`), `tokens` (int, on `_pass`) | | `v3_probe` | Probe phase events (`probe`, `probe_light`, `probe_error`, `probe_retry`, `probe_failed`, `probe_scored`, `probe_sandbox`, `probe_pass`) | `stage`, `detail` | | `v3_self_test` | Self-test generation/verify events (`self_test_gen`, `self_test_done`, `self_test_error`, `self_test_skip`, `self_test_verify`) | `stage`, `detail` | | `v3_plan` | Plan-pipeline progress (`plan_start`, `plan_candidate`, `plan_candidate_scored`, `plan_candidate_unparseable`, `plan_candidate_error`, `plan_selected`, `plan_failed`). Per-token `token`/`llm_start`/`llm_end` events are filtered out at the proxy. | `stage`, `detail`, `index` (int, per-candidate), `score` (float, on `_scored`/`_selected`), `revision` (int, set when fired during a revise) | | `plan_loaded` | A winning plan has been generated. Fires once after initial generation and again after each revision. Carries the full step list. | `steps` (array of `{id, action, target, why}`), `verify_step` (string id), `rationale` (string), `winning_score` (float), `revision` (int — 0 for initial plan, 1+ for revisions) | | `plan_adherence` | Emitted after each tool call, indicating whether the call satisfied an outstanding plan step. Off-plan calls (`matched=false`, no `neutral`) accumulate into the off-streak counter that drives auto-revise. | On match: `matched=true`, `step_index`, `step_id`, `step_action`, `satisfied` (steps satisfied so far), `total`. On miss: `matched=false`, `tool`, `off_streak` (consecutive off-plan calls), `satisfied`, `total`. Recon tools (`read_file`, `list_directory`, `find_file`, `search_files`) emit the miss shape plus `neutral=true` — they don't satisfy steps but leave `off_streak` unchanged. | | `plan_revise` | The off-streak crossed `planAutoReviseThreshold` (5) — a fresh plan is being generated. The next `plan_loaded` (with `revision>0`) supersedes the prior plan; `Satisfied` flags reset. | `reason` (string), `revision` (int, 1-indexed) | | `done` | Agent loop ended cleanly | `summary` (string — empty for a `text`-shaped turn) | | `error` | LLM/parse/turn-cap error | `error` (string) | After the final event the server writes the SSE sentinel `data: [DONE]\n\n` and closes the response. > A minimal chat client needs only `text`, `tool_call`, `tool_result`, `done`, `error`. The `llm_*` and `v3_*` events report model activity during the turn; ignore them if your UI doesn't render progress. #### Stream parsing example (Python) ```python import json, requests with requests.post( "http://localhost:8090/v1/agent", json={"message": "fix the bug in app.py", "session_id": "client-1"}, stream=True, timeout=(10, None), # connect timeout 10s, no read timeout — turns can be long ) as r: r.raise_for_status() for line in r.iter_lines(decode_unicode=True): if not line or not line.startswith("data: "): continue body = line[6:] if body == "[DONE]": break evt = json.loads(body) t, d = evt["type"], evt["data"] if t == "tool_call": print(f"→ {d['name']}({d.get('args', {})})") elif t == "tool_result": print(f" {'OK' if d['success'] else 'FAIL'} {d.get('elapsed', '')}") elif t == "text": print(d["content"]) elif t == "done": print(f"✓ {d.get('summary', '')}") elif t == "error": print(f"✗ {d['error']}") ``` Full streaming chat with token-level rendering is roughly +20 lines (buffer `llm_token` deltas, flush on `llm_call_end`). --- ### POST /cancel Abort an in-flight `/v1/agent` turn. Idempotent — repeated calls for the same session return 404. **Request:** ```json {"session_id": "tui-7f3a2c1b"} ``` **Response (200):** ```json {"cancelled": true} ``` **Response (404):** ```json {"cancelled": false} ``` When cancelled, the agent loop exits via `context.Canceled`, the SSE stream emits its trailing `[DONE]`, and the connection closes cleanly. Any in-flight LLM call to llama-server is also aborted via the cascading request context. The TUI uses this on `Esc` mid-turn — see [CLI.md → Cancelling a turn](CLI.md). --- ### POST /v1/permission Answer a `permission_request` event. In `default` and `accept-edits` mode the agent loop pauses on a destructive tool call and emits `permission_request`; the turn stays blocked until this endpoint delivers a decision, the client disconnects/cancels, or a fail-safe timeout denies (`ATLAS_PERMISSION_TIMEOUT_SEC`, default 600s). Idempotent — a decision for an unknown or already-resolved request returns 404. **Request:** ```json {"session_id": "tui-7f3a2c1b", "tool_call_id": "call_3", "decision": "allow", "scope": "once"} ``` | Field | Type | Description | |-------|------|-------------| | `session_id` | string | The `session_id` of the paused `/v1/agent` turn | | `tool_call_id` | string | The `tool_call_id` from the `permission_request` event | | `decision` | string | `"allow"` or `"deny"` (anything other than `"allow"` denies) | | `scope` | string | `"once"` (this call only) or `"session"`. `"session"` additionally skips re-prompting for the same tool for the rest of the turn; the client typically also adds the tool to `session_allowed_tools` on subsequent turns. | **Response (200):** `{"delivered": true}` — the blocked turn was signaled. **Response (404):** `{"delivered": false}` — no matching pending request (already resolved, cancelled, or timed out). The correlation key is `session_id` + `tool_call_id`, so multiple destructive calls within one turn (`call_0`, `call_1`, …) are answered independently. See [CLI.md → Permission modes](CLI.md). --- ### GET /events Subscribe to the **global typed-envelope broker**. Unlike `/v1/agent` (per-request stream of one turn), `/events` is a long-lived pub/sub feed of structured envelopes from across the proxy: agent loop boundaries, tool calls, V3 stage transitions, metrics. Multiple clients can subscribe simultaneously; slow consumers drop events rather than blocking producers. **Envelope wire format** (matches `atlas/events.py` exactly). A `stage_start` example (`duration_ms` is only set on `stage_end` / `tool_result` and the final `done`): ```json { "event_id": "evt_a1b2c3d4", "timestamp": 1714617823.412, "type": "stage_start", "stage": "llm", "payload": {"turn": 1, "messages": 3} } ``` **Event types:** `stage_start`, `stage_end`, `tool_call`, `tool_result`, `metric`, `error`, `done`. See [PROTOCOL.md](PROTOCOL.md) for the full per-type payload contracts and the `{stage, detail}` → envelope translation atlas-proxy applies to v3-service SSE. **Transport:** SSE. Each line is `data: \n\n`. The server sends `: connected\n\n` immediately on subscribe and a `: heartbeat\n\n` comment every 15 s during quiet stretches to keep proxies/load-balancers from idling out the connection. **Example:** ```bash curl -N http://localhost:8090/events ``` Use `/events` when you want a global observability feed (a TUI pipeline pane, a metrics scraper, a debug log viewer). Use `/v1/agent` when you want to drive a specific user turn. --- ### GET /v1/calibration/status Returns the proxy's view of whether the loaded model has compatible Geometric Lens artifacts (the verdict distilled from the lens service's `/health` payload) and whether an ASA control vector is in play. The TUI hits this on startup to render the badge next to the Pipeline pane title. **Response shape:** ```json { "lens": { "verdict": "supported", "cost_field_loaded": true, "cost_field_dim": 4096, "embed_dim": 4096, "gx_loaded": true, "cx_calibrated": true, "gx_calibrated": true, "hint": "ready" }, "asa": { "verdict": "missing", "vector_path": "/models/ast_edit_steering.gguf", "vector_present": false, "hint": "no control vector at /models/ast_edit_steering.gguf — build one via `atlas asa build`" } } ``` `cost_field_dim` / `embed_dim` reflect the loaded model's hidden dimension — the values differ per model. The payload also carries a `dimensions` array — the seven status dimensions the TUI and `atlas doctor` render, each `{name, status, detail}`: ```json { "dimensions": [ {"name": "model_runtime", "status": "supported", "detail": "model served and reachable"}, {"name": "direct_agent", "status": "supported", "detail": "model-agnostic; independent of lens/ASA state"}, {"name": "lens_identity", "status": "supported", "detail": "cost field matches the served model's dimension"}, {"name": "lens_scoring", "status": "supported", "detail": "C(x) + G(x) scoring available"}, {"name": "lens_calibration", "status": "calibrated", "detail": "per-model normalization + thresholds loaded"}, {"name": "lens_intervention", "status": "active", "detail": "threshold interventions enabled"}, {"name": "asa", "status": "unverified", "detail": "control vector present without a matching model marker; run `atlas asa build`"} ] } ``` **Verdict values:** - **Lens:** `supported` | `no-artifacts` | `incomplete-artifacts` | `uncalibrated` | `dim-mismatch` | `unreachable`. `incomplete-artifacts` means C(x) loaded but G(x) artifacts are missing; `uncalibrated` means weights loaded without the model's calibration files (`cx_normalization.json` / `gx_thresholds.json`) — both point at `atlas lens build`. - **ASA:** `supported` | `missing` | `unverified` | `incompatible`. `incompatible` means the control vector on disk is marked for a different model than the one selected. **Use:** ```bash curl http://localhost:8090/v1/calibration/status | jq . ``` **Cache:** none — every call re-probes the lens service. Cost is ~50–200 ms (one HTTP round-trip to `lens/health`). TUI calls once at startup; CI / monitoring should poll no faster than every few seconds. --- ### POST /feedback Records a human verdict on the most recent pass for a session as weighted lens training samples (`proxy/lens.go`). The TUI's `/good`, `/bad`, and per-file accept/deny review flow post here. Per-file verdicts take precedence; when a file carries no verdict, the pass-level thumbs labels it coarsely (with lower weight). A denial is recorded as a confident negative regardless of the pass thumbs. **Request:** ```json { "session_id": "tui-7f3a2c1b", "thumbs": "up", "files": [ {"path": "app.py", "verdict": "accept"}, {"path": "utils.py", "verdict": "deny"} ] } ``` | Field | Type | Description | |-------|------|-------------| | `session_id` | string | The session whose pending pass is being rated. One pending pass per session — rating consumes it. | | `thumbs` | string | `"up"` \| `"down"` \| `""` — pass-level verdict, applied to files without a per-file verdict | | `files[].verdict` | string | `"accept"` \| `"deny"` — per-file verdict (review mode) | **Response (200):** ```json {"recorded": 2, "good": 143, "bad": 27} ``` When there is no pending pass for the session, the response is `{"recorded": 0, "note": "no pending pass for that session"}`. Samples land in the per-model training corpus that `atlas lens retrain` consumes. --- ### GET /v1/lens/training-status Reports the collected-sample counts for the loaded model and whether a retrain is worth offering. The TUI polls this to show the "retrain available" banner. ```bash curl http://localhost:8090/v1/lens/training-status ``` ```json { "model": "local-model", "good": 1650, "bad": 420, "total": 2070, "threshold": 2000, "retrain_available": true, "command": "atlas lens retrain" } ``` `retrain_available` is true when `total >= threshold` **and** the minority class holds at least 25% of the threshold (so the corpus isn't all-positive or all-negative). The threshold defaults to 2000 and is overridable via `ATLAS_LENS_RETRAIN_MIN`. --- ### POST /v1/chat/completions (passthrough) OpenAI-compatible chat completions, kept for SDK compatibility. The proxy forwards these requests to llama-server otherwise unmodified, but guarantees an explicit completion bound: `max_tokens` (or `n_predict` on `/completion`, `/completions`, `/infill`) is clamped to `ATLAS_MAX_COMPLETION_TOKENS` (default 8192) when missing, non-positive, or above that ceiling. A client that omits `max_tokens` therefore gets 8192, not the server default — **no agent loop, no tool calls, no V3 pipeline runs on this endpoint**. The response shape and streaming format is whatever llama-server returns natively. **For agent turns and tool calls, use `/v1/agent`.** It carries the full structured event stream (tool calls, V3 progress, permission requests). **Request:** ```json { "model": "local-model", "messages": [ {"role": "user", "content": "Create a Python hello world script"} ], "max_tokens": 4096, "temperature": 0.3, "stream": true } ``` **Response:** llama-server's native OpenAI-compatible shape — `chat.completion.chunk` SSE deltas when `stream: true`, a single `chat.completion` object otherwise. The proxy adds nothing else to the payload. > **Note:** `/models` (no `/v1/` prefix) is an alias for `/v1/models`. Any unmatched path is proxied directly to llama-server. --- ### Tools available to the agent loop Defined in `proxy/tools.go`. Used by the model when responding `{"type":"tool_call","name":"","args":{...}}`. | Tool | Purpose | |------|---------| | `read_file` | Read a file and return its contents with line numbers | | `outline_file` | Symbol outline of a file (functions/classes with line ranges and call edges, via tree-sitter). Cheaper than `read_file` for orienting in a large file. | | `write_file` | Create a new file. **Rejected for any existing file >5 lines** (`proxy/agent.go`) — use `structural_edit` (whole function/class/element rewrite) or `edit_file` (≤10-line surgical change). Two exemptions: corrupted-looking files (prose preamble, stray markdown fences), so a self-heal full-replace is allowed there; and files the session itself created, so the agent can rewrite its own drafts. | | `edit_file` | Apply targeted `old_str`/`new_str` edits to an existing file. Routes through V3 verification at tier 2+. A `.py` edit that introduces an unresolved direct call (would-be `NameError`) is refused by the structural gate — the error names the call and the file is not modified. The wrong tool for >10 lines of change — switch to `structural_edit`. | | `insert_after` | Insert lines into a file after line `line` (1-based, as printed by `read_file`; `0` inserts at the top). Only the new text is supplied — there is no anchor to reproduce, which is what makes it the right tool for ADDING code rather than changing it. Requires the file to have been read, so the line numbers are current. Same syntax, structural and embedded-script gates as `edit_file`. | | `structural_edit` | Surgical replacement of one whole named block (function, class, or HTML element). Selectors v1: Python `function:NAME` / `class:NAME` (decorator-aware), HTML `` (top-level; `