Skip to content

agent,lua: a plugin platform for supervision and scheduling plugins - #875

Open
Firaenix wants to merge 2 commits into
tontinton:mainfrom
Firaenix:pr/plugin-platform
Open

agent,lua: a plugin platform for supervision and scheduling plugins#875
Firaenix wants to merge 2 commits into
tontinton:mainfrom
Firaenix:pr/plugin-platform

Conversation

@Firaenix

@Firaenix Firaenix commented Aug 26, 2026

Copy link
Copy Markdown

Builds on #874 (the two commits below it in the branch); review the top commit.

Everything a plugin needs to supervise or route the agent's work without forking a builtin:

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

Reviewer chain: maki.api.register_reviewer installs a chain link that classifies any tool call which would otherwise prompt: ALLOW runs it, DENY blocks it with the reason fed back to the agent, ASK escalates down the chain and finally to 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 flows), receiving the decoded input, derived scopes (for bash: the parsed command segments), cwd, and attempt history. Under yolo an unresolved chain denies with retry guidance (customisable via redirect_guidance) instead of prompting, hardening to a stop instruction after three redirects in a turn, so unattended runs never stall on a question. Reviewers receive the raw input, derived permission scopes, cwd, last user message, and attempt history, fenced as untrusted data with injection attempts named as deny grounds. Calls resolve through the normal provider stack with per-link timeouts, report usage into session cost totals, and emit tool decisions with a dedicated reviewer OTel source. Registration is live and gated behind a new 'reviewers' grant in plugin.toml, since an always-allow reviewer would silently bypass prompting. Outcomes surface through a ToolReviewed autocmd (spend included) and native deny/redirect toasts.

Task slots: the bundled task tool declares task.resolve_model, task.system_prompt, and task.tools extension slots, so a plugin can reroute subagent model choice, steer prompts, or filter tools by wrapping a slot instead of shipping a fork of the tool.

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-platform branch 4 times, most recently from cfd4417 to d41e235 Compare August 26, 2026 15:23
@Firaenix
Firaenix force-pushed the pr/plugin-platform branch 2 times, most recently from 3842913 to bb7d7a9 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.
@Firaenix
Firaenix force-pushed the pr/plugin-platform branch from bb7d7a9 to 657769e 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