From c21a4ebd001ca305d862b5390f090f2ef14163cf Mon Sep 17 00:00:00 2001 From: t Date: Tue, 16 Jun 2026 19:28:54 -0600 Subject: base site: claude design plus some tweaks --- research/notes.md | 393 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 research/notes.md (limited to 'research/notes.md') diff --git a/research/notes.md b/research/notes.md new file mode 100644 index 0000000..2cfcadc --- /dev/null +++ b/research/notes.md @@ -0,0 +1,393 @@ +# panto — verified content notes (from repo source) + +Source: https://code.tjp.lol/pantograph.git/ (cgit). Only document IMPLEMENTED features. +Project self-describes as "Early". NOT implemented yet (DO NOT DOCUMENT): server mode, subagents, +MCP, permission systems, AGENTS.md automation, skills, customizable /prompts, native (.so) extensions +(planned "next"; Lua is what ships). Standard tools (read/write/edit/bash) ship as extensions. + +## CLI invocation / subcommands (src/subcommand.zig — printHelp + dispatch, VERBATIM help text) + +panto Start a new conversation. +panto --resume Resume the most recent conversation in this directory. +panto --resume Resume the conversation whose id begins with . +panto sessions List saved sessions for this directory. +panto auth status Show configured auth sessions and login state. +panto auth login Log in to an OAuth auth session (device flow). +panto auth logout Forget a stored OAuth token. +panto bootstrap [--force] Run the luarocks bootstrap and exit. +panto lua [args...] Drop into the embedded Lua interpreter. +panto help | --help | -h Show help. + +Details: +- dispatch routes argv[1]; unknown/absent -> agent REPL. +- `lua`: embedded standalone Lua interpreter (panto's lua.c build) with luarocks runtime bootstrap + completed first, so `require("luarocks.*")` and the configured rocks tree work the same as in the + agent process. argv rewritten so the interpreter sees `lua [...args]` (affects arg[0]). +- `bootstrap`: runs the luarocks runtime bootstrap pipeline only, then exits before any agent loop. + First-run setup on a fresh machine (downloads+compiles batteries, stages headers, materializes + config); also good for CI/scripted installs. Idempotent: re-runs no-op fast. + `--force`: wipe the per-Lua-version tree before reinstalling everything. Equivalent to deleting + `$PANTO_HOME/rocks/lua-X.Y.Z/` by hand then running `panto bootstrap`. +- `sessions`: lists sessions for the current working directory. One per line: + ` messages`. short-id = first 8 hex chars of the session + UUIDv7. created trimmed to `YYYY-MM-DD HH:MM`. "no sessions for " when empty. +- `auth` (default action = status): + - status: per configured auth session. + api_key -> "resolved" OR "unresolved (key/env missing)" + oauth_device -> "logged in (access expires in ~Nm)" OR "not logged in (run: panto auth login )" + - login : OAuth device flow. Prints: "To authorize, open this URL in a browser: " and + "enter the code: ". Runs secondary token exchange now if configured. api_key sessions: + "'' is an api_key session; nothing to log in to". + - logout : deletes the stored token set ("logged out of ''" / "no stored token for ''"). + +## Config files (TOML, merged base -> user -> project) [from help text] +- $XDG_DATA_HOME/panto/config.toml (base; auto-generated) +- $XDG_CONFIG_HOME/panto/config.toml (user) +- ./.panto/config.toml (project) +Schema teaser (NEED config_file.zig for full schema): +- [providers.] define providers +- [defaults] model = ":" pick the default model +- [tools] / [extensions] allow/deny globs gate tools & extensions +- Model aliases (wire name, reasoning, max_tokens, pricing) live in models.toml. + +## Environment variables [from help text] +- OPENAI_API_KEY, ANTHROPIC_API_KEY — consumed by the default providers. +- PANTO_SESSION_DIR — override base sessions dir. Default $XDG_DATA_HOME/panto/sessions + or ~/.local/share/panto/sessions. +- PANTO_HOME — override the runtime/rocks tree location. + +## Slash commands (src/command.zig) +- REPL treats any line beginning with `/` as a slash command (never sent to the model). +- Parse: first whitespace-delimited word after `/` = name; trimmed remainder = args. +- Unknown command -> error.CommandNotFound -> user-facing error message. +- Builtin command seen: `/compact` (registers from its own module; compacts the conversation). +- Lua extensions add commands: a script calls `panto.ext.register_command { name, description, handler }`, + harvested by lua_runtime.zig; main.zig registers each. Lua-backed commands carry a lua_ref. +- Tab-completion: planned (registry has list()). + +## CONFIG FILE — full schema (src/config_file.zig, VERIFIED) + +### Layers — 4 TOML files, merged lowest precedence first (NOTE: help text only lists 3; loader has 4) +1. base — $PANTO_HOME/config.toml OR ${XDG_DATA_HOME:-$HOME/.local/share}/panto/config.toml (auto-generated by bootstrap) +2. user — ${XDG_CONFIG_HOME:-$HOME/.config}/panto/config.toml +3. project — ./.panto/config.toml +4. local — ./.panto/config.local.toml (intended to be git-ignored; personal overrides on top of team project layer) +Missing files are skipped silently. + +### Merge semantics +- Tables merge recursively; scalars AND arrays from a higher layer overwrite WHOLESALE (no array append). + e.g. project `tools.deny = [...]` replaces the array entirely. +- Tables accumulate: a provider defined only at base survives when project adds a different provider. + +### `${...}` substitution (auth values) +- `${env:VAR}` reads the environment; `${sibling}` reads another key in the SAME [auth.] section. +- Unresolved reference (missing env var / sibling) → empty string. e.g. define `domain = "github.com"` then + `device_code_url = "https://${domain}/login/device/code"`. + +### [providers.] (networked provider; transport only) +- style (required) — one of: openai_chat | anthropic_messages | openai_responses +- base_url (required) — string +- auth (required) — names the [auth.] session supplying the credential. + (Clean break: provider-level api_key / api_key_env_var are NO LONGER accepted.) +- prompt_cache (bool, default true) — anthropic_messages ONLY; one advancing cache_control breakpoint per + request. Ignored for openai_chat. +- [providers..extra_headers] — table of string header name=value, merged onto each model request. +- A provider whose api_key auth resolves empty is DROPPED ("export the key or the provider disappears"). + OAuth providers always survive (resolved at turn time / via `panto auth login`). + +### [auth.] (named auth session; `type` inferred if omitted: client_id→oauth_device, key→api_key) +api_key: + - type = "api_key" (optional; inferred from `key`) + - key = "${env:VAR}" (usually) or a literal — resolved eagerly; empty ⇒ session unresolved. +oauth_device: + - type = "oauth_device" (optional; inferred from `client_id`) + - dialect = "token" (default) | "codex" + - client_id (required) + - device_code_url (required) + - token_url (required) + - device_poll_url (required ONLY for dialect="codex") + - verification_url (optional) + - scope (optional) + - token_request_format = "form" (default) [enum panto.TokenRequestFormat] + - redirect_uri (optional) + - account_id_jwt_claim (optional) + - secondary token exchange (flat keys; active when exchange_url present): + exchange_url (enables exchange) + exchange_method (default "GET") + exchange_token_path (default "token") — JSON path to the token + exchange_expires_path (optional) — JSON path to expiry + exchange_base_url_path (optional) — JSON path to a dynamic base_url + - arbitrary sibling keys allowed (e.g. `domain`) for use in ${...} templating. + +### [defaults] +- model = ":" — provider must resolve. Selection precedence: + 1) future --model override, 2) defaults.model, 3) if exactly ONE provider resolved AND it has exactly ONE + alias in models.toml, use that, 4) else error (no model selected). +- Model ref format: exactly one colon, both halves non-empty (e.g. `anthropic:claude-sonnet-4-6`). + +### [tools] / [extensions] — allow/deny glob policy (identical shape) +- allow = [glob,...] deny = [glob,...] +- Empty allow ⇒ allow-all (still subject to deny). Permitted iff matches ≥1 allow (or allow empty) AND no deny. +- Same pattern in BOTH allow and deny (same kind) ⇒ hard error. +- Names are dotted, e.g. std.read, std.write, std.edit, std.shell; globs like `std.*`. + (Standard tools ship as extensions under the `std.` namespace and can be denied individually.) + +### [compaction] +- keep_verbatim = 0> — kept-suffix token budget (omitted ⇒ libpanto default). +- model = ":" — optional override model used for compaction (provider must resolve). + +### Model aliases live in models.toml (resolved separately). Knobs seen in buildProviderConfig: +- common: wire `model` name, `reasoning` (ReasoningEffort, default .default), `max_tokens` (default 64000) +- anthropic_messages extra: api_version (default "2023-06-01"), thinking (default disabled), + effort (default medium), thinking_budget_tokens (default 32000), thinking_interleaved (default false) + -> NEED models_toml.zig for the exact TOML table layout + key names. + +### Model aliases live in models.toml (src/models_toml.zig, VERIFIED) +Path: ${XDG_CONFIG_HOME:-$HOME/.config}/panto/models.toml. Missing file ⇒ empty registries (no error). +A referenced alias with NO entry is NOT an error: the alias is used verbatim as the wire model name, +with default knobs and unknown pricing (zero-config convenience). + +Entry table key: [.] — matches a [providers.] in config.toml; + is the short name referenced as : (e.g. anthropic:sonnet). + +Keys: +- model = wire model id sent to the API; defaults to if omitted. +- max_tokens = per-request OUTPUT token cap; >0 else null (= provider/library default). +- Pricing (all optional, USD per MILLION tokens; omitted = UNKNOWN/null, write `= 0` for known-zero): + input = + output = + cache_read = + cache_write = +- openai_chat ONLY: + reasoning = default | off | minimal | low | medium | high (default: default) +- anthropic_messages ONLY: + api_version = Anthropic-Version header; default "2023-06-01" + thinking = disabled | enabled | adaptive (default: disabled) + effort = low | medium | high | xhigh | max (default: medium) + — only used when thinking = "adaptive" + thinking_budget_tokens = max reasoning tokens for thinking="enabled"; default 32000; + null ⇒ max_tokens − 1; ignored when adaptive/disabled + thinking_interleaved = send interleaved-thinking beta header (default false); + only honoured when thinking = "enabled" + +Example models.toml: + [anthropic.sonnet] + model = "claude-sonnet-4-20250514" + max_tokens = 8192 + thinking = "enabled" + thinking_budget_tokens = 16000 + input = 3.0 + output = 15.0 + cache_read = 0.3 + cache_write = 3.75 + + [anthropic.opus] + model = "claude-opus-4-8" + thinking = "adaptive" + effort = "high" + + [openai.gpt] + model = "gpt-4o" + input = 2.5 + output = 10.0 + +## LUA RUNTIME / LUAROCKS (src/manifest.zig + subcommand.zig, VERIFIED) +- panto embeds a PINNED Lua + luarocks and ships "batteries" (rocks). The ONLY battery auto-installed is + `luv` (libuv event loop) — the single runtime dependency. It drives panto's coroutine scheduler and + gives extension authors one rich async I/O surface. +- Bootstrap reconciles the installed rocks tree against the manifest on EVERY startup: installs missing, + removes stale. The battery list is compiled into the binary; users do NOT configure it. +- Extra rocks are the user's responsibility. Install via: + panto lua -e 'arg[0]="luarocks"; require("luarocks.cmd").run_command(...)' + (A higher-level `panto rocks install` is PLANNED — DO NOT document.) +- Per-Lua-version rocks tree lives at $PANTO_HOME/rocks/lua-X.Y.Z/ (wiped by `panto bootstrap --force`). +- luarocks MAJOR.MINOR.PATCH-REV version strings (e.g. "1.52.1-0") passed straight to luarocks install. + +## EXAMPLES INVENTORY (repo /examples) +- examples/extensions/echo.lua (632) — example panto Lua EXTENSION <-- fetch +- examples/extensions/greet.lua (719) — example panto Lua EXTENSION <-- fetch +- examples/tools/ (dir) — example tools <-- fetch tree +- examples/simple-agent.lua (3110) — libpanto Lua SDK agent (NOT panto-ext; for libpanto, skip-ish) +- examples/simple-agent-go/ — libpanto Go SDK agent (NOT panto-ext) + +## LUA EXTENSION SURFACE — verified from examples +Module: `local panto = require("panto")` (available inside extensions AND in `panto lua`). + +### panto.ext.register_tool { name, description, schema, handler } (examples/extensions/echo.lua) +- name : tool name (string) the model calls. +- description : shown to the model. +- schema : JSON-Schema table — { type="object", properties={...}, required={...} }. +- handler : function(input) -> string. `input` is the decoded arguments table; return a string result. + echo example: handler = function(input) return "echo: " .. input.message end + +### panto.ext.register_command { name, description, handler } (examples/extensions/greet.lua) +- Registers a `/`-slash command in the REPL. name is WITHOUT the leading `/`. +- handler = function(args) ... end. `args` is the trimmed text after the command name ("" if none). + Handlers run SYNCHRONOUSLY and act by side effect (e.g. io.write); the RETURN VALUE IS IGNORED. + greet example: `/greet` or `/greet ` -> io.write("\n[greet] hello, "..who.."!\n") +- A Lua command name colliding with a builtin (e.g. compact) is rejected (DuplicateCommand). + +### Where extensions live (from greet.lua header comment) +- Project: `./.panto/extensions/` +- User: the user config dir's `extensions/` dir (i.e. ${XDG_CONFIG_HOME:-~/.config}/panto/extensions/). +- Drop a `.lua` file there; it's loaded at startup. Gate via [extensions] allow/deny globs in config.toml. + +### Runtime +- The embedded interpreter is the panto `lua.c` standalone (Lua + luarocks). `luv` (libuv) is always + available for async I/O; panto's coroutine scheduler drives the libuv loop. +- `panto lua [args...]` drops into the same interpreter for scripting/experiments; + `panto bootstrap` prepares the rocks tree. +- NOTE: I have register_tool + register_command verified. The file src/lua_event_bridge.zig (event hooks) + is NOT yet read — DO NOT invent event APIs (on_turn, hooks, etc.) until verified. + +## KEYBINDINGS — verified from src/tui_input.zig (the input decoder; P1 scope, "minimal but correct") +The REPL input layer turns raw stdin into keys. Keys it recognises TODAY (document only these): + +Editing / text: +- Printable characters (full UTF-8, multi-byte) -> insert +- Enter (Return) -> submit the message +- Shift+Enter -> insert a newline (terminal-dependent, see below) +- Backspace -> delete char before cursor +- Delete -> delete char under/after cursor +- Tab -> tab +- Bracketed paste -> pasted text inserted literally as one run + (not interpreted key-by-key) + +Cursor / navigation (decoded with modifiers): +- Left / Right -> move cursor by character +- Ctrl+Left / Ctrl+Right -> move by word (the "word-motion path") +- Home / End -> line start / end +- Up / Down -> arrow keys (decoded; move within a multi-line draft) +- Page Up / Page Down -> decoded +- Alt+ -> decoded (ESC-prefixed alt forms) + +Control: +- Ctrl+C -> interrupt / quit +- Ctrl+D -> EOF / exit +- Esc -> escape +- Ctrl+ generally (0x01–0x1a) -> decoded as ctrl+a..z + +Shift+Enter detail (genuinely useful doc): in the bare legacy protocol Enter and Shift+Enter both send `\r` +and are indistinguishable. At startup panto negotiates the **Kitty keyboard protocol** (pushes flags 1|4 = +disambiguate + report-alternates; deliberately NOT report-events) and queries the terminal; if confirmed it +reads Shift+Enter as `CSI 13;2u`. Otherwise it falls back to xterm **modifyOtherKeys mode 2** (tmux/xterm), +which sends `CSI 27;2;13~`. On terminals supporting NEITHER (e.g. macOS Terminal.app) the two stay +identical and Enter submits — there's no newline binding there. (Ghostty maps shift+enter to a bare `\n`.) +Deferred / negotiated-but-not-consumed (DO NOT present as features): key-release events, super/hyper +modifiers, full Kitty disambiguation — modelled for later phases, not active. + +## MAIN / STARTUP / TUI WIRING (src/main.zig, VERIFIED) — lots of new facts + +### Agent-mode flags (the ONLY ones) +- `--resume` → resume most recent session in this cwd. +- `--resume ` → resume session whose id has prefix . +- No flag → new session. Unknown args are tolerated/ignored (warn). +- TUI REQUIRES an interactive tty ("panto's TUI requires an interactive terminal"). + +### Builtin slash commands +- ONLY builtin registered is from compaction.zig → `/compact` (manual compaction). + Everything else is added by Lua extensions. (Do NOT invent /help, /model, etc. as slash commands.) + +### LIVE keybindings added by main.zig (the interactive ones!) +- Ctrl+C / Ctrl+D → clean exit (UserExit). +- Ctrl+M → MODEL selector (runtime model picker overlay). +- Ctrl+R → REASONING-effort selector (runtime reasoning picker overlay). + Both are live-session only: a pick rebuilds the provider config and pushes it to the agent + (setConfig); NOTHING is written back to config.toml. +- Enter submits; Shift+Enter newline (see tui_input notes). Input box + footer + transcript differential render. + +### Lua extension surface — EXPANDED (main.zig wiring, VERIFIED) +- `require('panto')` is wired to the native `panto.so` module PLUS the CLI's `ext` subtable → `panto.ext`. +- Lua runtime is ONE long-lived lua_State; module-global state persists across calls. Registers with the + agent as a single ToolSource named `panto-lua`. +- `luv` (libuv) scheduler is installed BEFORE extensions load, so tool handlers may YIELD (async I/O). +- panto.ext.register_tool {…} → adds a model-callable tool (see echo.lua). +- panto.ext.register_command {…} → adds a `/`-slash command (see greet.lua). +- panto.ext.on(...) → registers a UI EVENT handler into the App's event bus, in registration order. + Extensions can WRAP/REPLACE built-in TUI components this way (e.g. a `tool_details` handler replacing + the default `tool (?)` component). Superseded overrides are released (ref + RenderCache freed). +- panto.ext.emit(...) → a Lua call that drives the SAME event bus. + (NOTE: exact event/kind names + on/emit signatures live in src/lua_event_bridge.zig — fetching now. + Until verified, describe on/emit as the component-override/event surface and cite tool_details as the + one named example from main.zig. Don't enumerate events I haven't seen.) + +### Extension discovery (3 layers; src/extension_loader.discoverAndLoad) +- base = $PANTO_HOME/agent +- user = $XDG_CONFIG_HOME/panto (or $HOME/.config/panto) +- project = ./.panto +- Precedence: project shadows user shadows base. Tool-NAME collisions across surviving entries ABORT startup. +- Gated by [extensions] and [tools] allow/deny policies from config.toml. +- (greet.lua header: drop a .lua under .panto/extensions/ project, or the user config's extensions/ dir.) + +### System prompt — a real config surface (system_prompt.zig, referenced by main.zig) +- The agent's system prompt is sourced by convention from SYSTEM.md / APPEND_SYSTEM.md across the + base/user/project layers (base = $PANTO_HOME/agent, where bootstrap stages the bundled SYSTEM.md). + SYSTEM.md replaces; APPEND_SYSTEM.md appends. Reconciled on resume without rewriting history. +- Compaction system prompt = COMPACTION.md across layers (last wins; built-in default otherwise). + +### Compaction +- Automatic compaction is armed at startup. [compaction].keep_verbatim default = 20000 tokens. +- `/compact` triggers it manually. [compaction].model can override the model used for compaction. + +### Sessions / auth (confirm) +- Sessions are per-cwd; created on demand. Resume by id-prefix or latest. "resumed session <8hex> ( messages)". +- Per-turn auth resolution: api_key is a no-op; oauth_device refreshes/exchanges or runs an interactive + device login before the turn. models.toml path = $XDG_CONFIG_HOME/.config/panto/models.toml. +- Config error messages are friendly (e.g. "defaults.model must look like \"provider:model\"."). + +## panto.ext EVENT SURFACE — VERIFIED (src/lua_event_bridge.zig) + +### panto.ext.on(name, handler) — subscribe a Lua handler to a UI event +- The handler participates in the SAME native EventBus the built-in TUI fires. Registration order matters. +- handler = function(e) ... end. `e` is a bridged EVENT OBJECT, valid ONLY during the handler call + (snapshot any field you need into a local — do NOT close over `e` and read it at render time). +- Event object API: + e.name -> the event name (string) + e. -> read-only payload fields (nil when absent/empty, so you can branch: + `if e.tool_name then ...`) + e:getComponent() -> the current (native default) component, as an OPAQUE passthrough handle + e:setComponent(c) -> replace the component for this boundary. `c` is EITHER that passthrough + handle (pass-through / wrap) OR a Lua component table (replace). +- Payload fields by event family (from pushPayloadField): + tool : index(int), tool_name, id, delta, input, output (lifecycle: tool_delta has delta, + tool_result has output, tool_details…) + thinking : index, delta, text + assistant_text : index, delta, text + user_message : text + session_start : version, cwd, model + compaction : summary + custom : (no structured fields — produced by emit) + (VERIFIED event NAME used in tests: "tool". Lifecycle boundary names tool_delta/tool_result/tool_details + appear in source comments. Present these as the event families; the canonical example is "tool".) + +### A Lua COMPONENT is a table: + { render = function(self, width) -> { "line1", "line2", ... } end, + handleInput = function(self, data) ... end, -- optional + firstLineChanged = ..., -- optional (cache-derived by default) + invalidate = ... } -- optional +- render() MUST return an array of strings; runs SYNCHRONOUSLY (may NOT yield — unlike tool handlers); + each line is truncated to `width` columns; any error yields a safe "[lua component error: …]" fallback + line (the frame never crashes). Empty array {} = zero lines (valid). + +### Three override patterns (from the bridge's own tests): +- pass-through native default: e:setComponent(e:getComponent()) +- replace with a Lua component: e:setComponent({ render = function(self,w) return {"hi"} end }) +- claim-by-name (the canonical extension shape): + panto.ext.on("tool", function(e) + if e.tool_name ~= "skill" then return end -- ignore everything else + local name = e.tool_name -- snapshot at handler time + e:setComponent({ render = function(self, w) return { "SKILL:"..name } end }) + end) + +### panto.ext.emit(name, data) — fire a custom event on the SAME bus +- Runs native AND Lua handlers for `name`. `data` is currently surfaced only as an opaque `.custom` + payload (structured marshalling is future). A component chosen from a bare emit is NOT auto-mounted yet. + +## ALL CONTENT GATHERED — ready to build. (lua_runtime/extension_loader internals not needed beyond above.) + +## KEY MODEL (src/tui_key.zig, VERIFIED) — what the P1 decoder currently handles +Decoded keys the running TUI populates today (the model is bigger for future phases, but ONLY these are live): + printable chars, enter, backspace, arrows (up/down/left/right), home, end, escape, Ctrl+C, Ctrl+D. +Mods modelled: ctrl, alt, shift, super, hyper — but P1 decoder only ever sets ctrl (super/hyper need Kitty +protocol; left false). Key events: press/repeat/release — most terminals only emit .press. +=> The keybindings page must document ONLY the live subset; note richer keys are planned, not shipped. -- cgit v1.3