summaryrefslogtreecommitdiff
path: root/research
diff options
context:
space:
mode:
Diffstat (limited to 'research')
-rw-r--r--research/notes.md393
1 files changed, 393 insertions, 0 deletions
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 <id> Resume the conversation whose id begins with <id>.
+panto sessions List saved sessions for this directory.
+panto auth status Show configured auth sessions and login state.
+panto auth login <name> Log in to an OAuth auth session (device flow).
+panto auth logout <name> 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:
+ `<short-id> <created> <message-count> messages`. short-id = first 8 hex chars of the session
+ UUIDv7. created trimmed to `YYYY-MM-DD HH:MM`. "no sessions for <cwd>" 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 <name>)"
+ - login <name>: OAuth device flow. Prints: "To authorize, open this URL in a browser: <uri>" and
+ "enter the code: <user_code>". Runs secondary token exchange now if configured. api_key sessions:
+ "'<name>' is an api_key session; nothing to log in to".
+ - logout <name>: deletes the stored token set ("logged out of '<name>'" / "no stored token for '<name>'").
+
+## 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.<name>] define providers
+- [defaults] model = "<provider>:<alias>" 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.<n>] 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.<name>] (networked provider; transport only)
+- style (required) — one of: openai_chat | anthropic_messages | openai_responses
+- base_url (required) — string
+- auth (required) — names the [auth.<n>] 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.<name>.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.<name>] (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>:<alias>" — 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 = <int > 0> — kept-suffix token budget (omitted ⇒ libpanto default).
+- model = "<provider>:<alias>" — 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: [<provider>.<alias>] — <provider> matches a [providers.<name>] in config.toml;
+<alias> is the short name referenced as <provider>:<alias> (e.g. anthropic:sonnet).
+
+Keys:
+- model = <string> wire model id sent to the API; defaults to <alias> if omitted.
+- max_tokens = <int> 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 = <float>
+ output = <float>
+ cache_read = <float>
+ cache_write = <float>
+- openai_chat ONLY:
+ reasoning = <string> default | off | minimal | low | medium | high (default: default)
+- anthropic_messages ONLY:
+ api_version = <string> Anthropic-Version header; default "2023-06-01"
+ thinking = <string> disabled | enabled | adaptive (default: disabled)
+ effort = <string> low | medium | high | xhigh | max (default: medium)
+ — only used when thinking = "adaptive"
+ thinking_budget_tokens = <int> max reasoning tokens for thinking="enabled"; default 32000;
+ null ⇒ max_tokens − 1; ignored when adaptive/disabled
+ thinking_interleaved = <bool> 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 <name>` -> 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+<key> -> decoded (ESC-prefixed alt forms)
+
+Control:
+- Ctrl+C -> interrupt / quit
+- Ctrl+D -> EOF / exit
+- Esc -> escape
+- Ctrl+<letter> 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 <id>` → resume session whose id has prefix <id>.
+- 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> (<n> 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.<field> -> 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.