From cdf4e866793efd87b55419950ce6db6073b03c10 Mon Sep 17 00:00:00 2001 From: Markus Fritsche Date: Sat, 16 May 2026 21:29:09 +0000 Subject: [PATCH] repl: sub-broker delegation via DELEGATE: marker (closes #6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: "" (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 ]: " 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 one-shot direct invocation (useful for testing without depending on the model emitting DELEGATE:, and as a manual "ask something" verb) Scope: - Plan mode: emits "PLAN: DELEGATE " 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) --- executor.lua | 19 ++++++++++++++++++ repl.lua | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/executor.lua b/executor.lua index f31aa9f..10c6b28 100644 --- a/executor.lua +++ b/executor.lua @@ -147,4 +147,23 @@ function M.extract_cmd_bg_lines(text) return cmds end +-- Issue #6: `DELEGATE: ""` 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 diff --git a/repl.lua b/repl.lua index 90da735..45e4ae4 100644 --- a/repl.lua +++ b/repl.lua @@ -146,6 +146,7 @@ Meta commands: :bg-list list background jobs (issued via CMD&: or :bg-spawn) :bg-output dump the log of a background job :bg-kill SIGTERM a background job + :delegate

one-shot sub-broker call to preset

; prints reply :help this message ]] @@ -809,6 +810,37 @@ function M.run(config) end end end + + -- Issue #6: DELEGATE: "" — 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 local function shutdown_session() @@ -1658,6 +1690,29 @@ function M.run(config) end end + meta.delegate = function(args) + local preset, prompt = args:match("^%s*(%S+)%s+(.+)$") + if not preset then + renderer.status("usage: :delegate "); return + end + prompt = prompt:gsub("^%s+", ""):gsub("%s+$", "") + prompt = prompt:match([[^"(.*)"$]]) or prompt:match([[^'(.*)'$]]) or prompt + if prompt == "" then renderer.status("usage: :delegate "); 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) local cmd = (args or ""):match("^%s*(.-)%s*$") if cmd == "" then renderer.status("usage: :bg-spawn "); return end