repl: sub-broker delegation via DELEGATE: marker (closes #6)
Cost and context-window control: a "heavy" preset's model can offload
work to a cheaper preset without spending its own tokens on the result.
Example: deep model is mid-conversation and asks fast to summarize a
20k-line build log; the summary comes back as exec-output for the
next turn, deep stays small.
Marker syntax: DELEGATE: <preset> "<prompt>"
(Single or double quotes; one DELEGATE per line; lines without the
quoted shape are dropped — let the user write about delegation in
prose without accidental dispatch.)
Dispatch flow (mirrors CMD: / CMD&: extraction):
1. ask_ai's stream completes
2. extract_delegate_lines walks the final response
3. For each {preset, prompt}: broker.chat(config.models[preset], ...)
synchronously; result is appended via ctx:append_exec_output as
"[delegate <preset>]: <result>"
4. The model sees the delegate result on its next turn
Implementation choice — marker over tool: option 1 from the issue
("inline delegate marker") works with any model regardless of
tool_calls support. Option 2 (aish_delegate as a tool dispatched in
the existing Phase 2 sub-loop) is the better UX for capable models
since it returns the result mid-turn — filed as follow-up if needed.
Meta surface:
:delegate <preset> <prompt> one-shot direct invocation (useful for
testing without depending on the model
emitting DELEGATE:, and as a manual
"ask <preset> something" verb)
Scope:
- Plan mode: emits "PLAN: DELEGATE <preset> <prompt>" without dispatch
- Norris: not extended; the planner's model anchor would conflict with
mid-plan switching (R-C3-adjacent risk)
- No self-delegation guard: each DELEGATE is a separate broker call,
not recursive; a delegate result reaching the next turn could
contain another DELEGATE but that's bounded by max_tool_depth-style
iteration cap on the parent
- No cost prompt: configuring a paid cloud preset already implies
consent to spend on it
- Unknown preset → error status + exec-output note "[delegate X failed:
unknown preset]"
Extractor unit-tested with 8 cases (single-quote, double-quote, multi-
line prose, empty prompt, no-quotes, case-sensitive, wrong prefix).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -147,4 +147,23 @@ function M.extract_cmd_bg_lines(text)
|
|||||||
return cmds
|
return cmds
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Issue #6: `DELEGATE: <preset> "<prompt>"` lines. Parses each into
|
||||||
|
-- (preset, prompt) — quotes around the prompt are required so the
|
||||||
|
-- parser can find the boundary unambiguously (the prompt may contain
|
||||||
|
-- arbitrary punctuation otherwise). Lines that don't match the
|
||||||
|
-- quoted shape are silently dropped (rendered as text to the user).
|
||||||
|
function M.extract_delegate_lines(text)
|
||||||
|
local out = {}
|
||||||
|
for line in (text or ""):gmatch("[^\n]+") do
|
||||||
|
local preset, prompt = line:match([[^DELEGATE: (%S+)%s+"(.+)"%s*$]])
|
||||||
|
if not preset then
|
||||||
|
preset, prompt = line:match([[^DELEGATE: (%S+)%s+'(.+)'%s*$]])
|
||||||
|
end
|
||||||
|
if preset and prompt and prompt:match("%S") then
|
||||||
|
out[#out + 1] = { preset = preset, prompt = prompt }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ Meta commands:
|
|||||||
:bg-list list background jobs (issued via CMD&: or :bg-spawn)
|
:bg-list list background jobs (issued via CMD&: or :bg-spawn)
|
||||||
:bg-output <id> dump the log of a background job
|
:bg-output <id> dump the log of a background job
|
||||||
:bg-kill <id> SIGTERM a background job
|
:bg-kill <id> SIGTERM a background job
|
||||||
|
:delegate <p> <prompt> one-shot sub-broker call to preset <p>; prints reply
|
||||||
:help this message
|
:help this message
|
||||||
]]
|
]]
|
||||||
|
|
||||||
@@ -809,6 +810,37 @@ function M.run(config)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Issue #6: DELEGATE: <preset> "<prompt>" — sub-broker call against
|
||||||
|
-- a different model preset. Result is fed back as exec-output so the
|
||||||
|
-- model sees it on the next turn. Synchronous (blocks the current
|
||||||
|
-- ask_ai return until each delegate resolves). Cost note: a DELEGATE
|
||||||
|
-- to a paid cloud preset spends API tokens silently — the user has
|
||||||
|
-- already opted in by configuring the preset.
|
||||||
|
for _, d in ipairs(executor.extract_delegate_lines(final_resp)) do
|
||||||
|
local sub_cfg = config.models[d.preset]
|
||||||
|
if plan_mode then
|
||||||
|
renderer.status(("PLAN: DELEGATE %s \"%s\""):format(d.preset, d.prompt))
|
||||||
|
ctx:append_exec_output(
|
||||||
|
("[plan] would delegate to %s: %s"):format(d.preset, d.prompt))
|
||||||
|
elseif not sub_cfg then
|
||||||
|
renderer.status(("DELEGATE: unknown preset '%s'"):format(d.preset))
|
||||||
|
ctx:append_exec_output(
|
||||||
|
("[delegate %s failed: unknown preset]"):format(d.preset))
|
||||||
|
else
|
||||||
|
renderer.status(("DELEGATE -> %s: %s"):format(d.preset, d.prompt))
|
||||||
|
local sub_msgs = { { role = "user", content = d.prompt } }
|
||||||
|
local sub_text, sub_err = broker.chat(sub_cfg, sub_msgs)
|
||||||
|
if not sub_text then
|
||||||
|
renderer.status(("delegate %s failed: %s"):format(d.preset, tostring(sub_err)))
|
||||||
|
ctx:append_exec_output(
|
||||||
|
("[delegate %s failed: %s]"):format(d.preset, tostring(sub_err)))
|
||||||
|
else
|
||||||
|
ctx:append_exec_output(
|
||||||
|
("[delegate %s]: %s"):format(d.preset, sub_text))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function shutdown_session()
|
local function shutdown_session()
|
||||||
@@ -1658,6 +1690,29 @@ function M.run(config)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
meta.delegate = function(args)
|
||||||
|
local preset, prompt = args:match("^%s*(%S+)%s+(.+)$")
|
||||||
|
if not preset then
|
||||||
|
renderer.status("usage: :delegate <preset> <prompt>"); return
|
||||||
|
end
|
||||||
|
prompt = prompt:gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
prompt = prompt:match([[^"(.*)"$]]) or prompt:match([[^'(.*)'$]]) or prompt
|
||||||
|
if prompt == "" then renderer.status("usage: :delegate <preset> <prompt>"); return end
|
||||||
|
local sub_cfg = config.models[preset]
|
||||||
|
if not sub_cfg then
|
||||||
|
renderer.status(("unknown preset: %s"):format(preset)); return
|
||||||
|
end
|
||||||
|
renderer.status(("DELEGATE -> %s: %s"):format(preset, prompt))
|
||||||
|
local sub_msgs = { { role = "user", content = prompt } }
|
||||||
|
local sub_text, sub_err = broker.chat(sub_cfg, sub_msgs)
|
||||||
|
if not sub_text then
|
||||||
|
renderer.status(("delegate %s failed: %s"):format(preset, tostring(sub_err)))
|
||||||
|
else
|
||||||
|
io.write(sub_text)
|
||||||
|
if not sub_text:match("\n$") then io.write("\n") end
|
||||||
|
ctx:append_exec_output(("[delegate %s]: %s"):format(preset, sub_text))
|
||||||
|
end
|
||||||
|
end
|
||||||
meta["bg-spawn"] = function(args)
|
meta["bg-spawn"] = function(args)
|
||||||
local cmd = (args or ""):match("^%s*(.-)%s*$")
|
local cmd = (args or ""):match("^%s*(.-)%s*$")
|
||||||
if cmd == "" then renderer.status("usage: :bg-spawn <cmd>"); return end
|
if cmd == "" then renderer.status("usage: :bg-spawn <cmd>"); return end
|
||||||
|
|||||||
Reference in New Issue
Block a user