Skip to content

lua,ui: turn-boundary observability for supervision plugins - #876

Open
Firaenix wants to merge 3 commits into
tontinton:mainfrom
Firaenix:pr/plugin-observability
Open

lua,ui: turn-boundary observability for supervision plugins#876
Firaenix wants to merge 3 commits into
tontinton:mainfrom
Firaenix:pr/plugin-observability

Conversation

@Firaenix

Copy link
Copy Markdown

Builds on #875 (top commit is this change).

Plugins that own an objective (goal loops, budget watchdogs, context managers) need the turn boundary: TurnEnd now carries reason (finished|cancelled|limit) so an auto-continue plugin can appeal a finished turn, always yield to a cancellation, and back off on limits. TurnComplete, AutoCompacting, and CompactionDone become autocmds exposing per-turn usage, cost, and context pressure. maki.session.queue() reads the pending message count without consuming it, so an injector yields when the human already typed.

Also: maki.ui.action docs list every builtin action, and code_execution child calls no longer have their dispatch deadline silently capped at the script budget when the call carries its own longer timeout input.

Plugins that need a picker rebuild one from scratch: a floating window,
hand-rolled fuzzy matching, their own key handling and section headers.
The result never quite matches the built-in pickers, and every plugin
pays the cost again. Meanwhile the real thing - the ratatui ListPicker
that /model and friends render with - already does filtering, sections,
scrolling, paste, theming and keybinds, and only lacks a way for a
plugin to feed it rows.

`maki.ui.picker(items, opts?)` opens exactly that component, populated
with plugin-supplied rows, and answers when the user decides:

- items: `{ label, detail?, suffix?, section?, highlighted?, data? }`
  per row - the same axes PickerItem gives the native pickers. `data`
  is an opaque JSON value echoed back on selection.
- opts: `title`, `initial` (label to preselect), and `keys` - extra
  keybinds (`{ key = "R", hint = "refresh" }`) rendered in the footer.
  Uppercase by convention, matching the model picker's shift-only rule,
  so lowercase letters keep feeding the fuzzy filter. Pressing one
  answers `{ key = "R" }` so the caller can act and reopen.
- Answers: `{ index, label, data }` on Enter (index is 1-based into
  the caller's array), `{ key }` on a registered key, and
  `(nil, "cancelled")` on dismissal - including when the overlay is
  closed from outside, so the waiting coroutine never parks forever.

Plumbing follows the existing patterns: a `LuaPicker` component wraps
`ListPicker<Entry>` the same way ModelPicker does and joins the overlay
set; the request rides a new `UiAction::Picker` through the same
round-trip `maki.model` uses. ListPicker itself learns one small trick:
`set_footer_line`, a prebuilt footer for pickers whose hints are only
known at open time (the existing `with_footer_builder` is a plain fn
pointer and cannot capture the caller's keys).

With `maki.model.info`/`available`/`set`/`refresh`, a complete model
popup is now: list specs, map to rows with price hints, open the
picker, switch on the answer.
@Firaenix
Firaenix force-pushed the pr/plugin-observability branch 2 times, most recently from a2d31e5 to f20de63 Compare August 27, 2026 18:30
Everything a plugin needs to supervise or route the agent's work without
forking a builtin, in one connected change:

Model catalog. Discovery persists to an on-disk cache and replays at
startup; Anthropic /v1/models metadata (max_input_tokens, max_tokens)
flows into ModelInfo; pricing distinguishes billed cost from list price
for subscription-subsidised providers. Lua reads it all through
maki.model.info(spec) and kicks re-discovery with maki.model.refresh().

Reviewer chain. maki.api.register_reviewer installs a chain link that
classifies any tool call which would otherwise prompt the human: ALLOW
runs it, DENY blocks it with the reason fed back to the agent, ASK
escalates to the next link and finally the human. A link is either a
model judging against policy text, or a plugin handler function that
computes the verdict itself (rulebooks, quotas, external approval
systems), receiving the decoded input, the derived permission scopes
(for bash: the treesitter-parsed command segments), parseability, cwd,
the last user message, and the attempt history for repeated calls.
Model links fence everything as untrusted data with injection attempts
called out as grounds to deny. Under yolo an unresolved chain denies
with retry guidance (customisable per registration via
redirect_guidance) instead of prompting, hardening to a stop
instruction after three redirects in a turn, so unattended runs never
stall. Model calls resolve through the normal provider stack, respect a
per-link timeout, report usage into the session cost pipeline, and emit
OTel tool decisions with a dedicated reviewer source. Registration is
live (same-name upsert, /reload clears) and gated behind a new
'reviewers' grant in plugin.toml, since an always-allow link would
silently bypass prompting. Plugins observe outcomes through the
ToolReviewed autocmd, spend included; the UI folds reviewer usage into
session totals and toasts denials.

Task slots. The bundled task tool declares three extension slots -
task.resolve_model, task.system_prompt, task.tools - so a plugin can
reroute subagent model choice, steer subagent prompts, or filter their
tools by wrapping a slot instead of replacing the tool.
A plugin that owns an objective (goal loops, budget watchdogs, context
managers) needs to act at the turn boundary and know why it arrived
there. TurnEnd now carries reason (finished|cancelled|limit) so an
auto-continue plugin can appeal "finished", always yield to
"cancelled", and back off on limits. TurnComplete, AutoCompacting, and
CompactionDone become autocmds, exposing per-turn usage, cost, and
context pressure, plus the compaction moments a context plugin must
react to. maki.session.queue() reads the pending message count without
consuming it, so an injector can yield when the human already typed.

maki.ui.action's docs now list every builtin action (the parser always
accepted them; the doc stopped at three).

code_execution: a child call's dispatch deadline was silently capped at
the script budget, killing explicit long-running calls (a bash build
with timeout = 1200 died at 30s). The per-call deadline is now
max(script budget, the call's own timeout input).
@Firaenix
Firaenix force-pushed the pr/plugin-observability branch from f20de63 to 51374f4 Compare August 28, 2026 07:07
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