context + repl + config: per-class system_prompt override (closes #86)
Small local models follow precise structured instructions better than
natural language. Per-routing-class system_prompt override gives them
tighter instructions for THAT request while preserving ambient context.
Changes:
- Context:to_messages(opts) — opts.system_prompt_override REPLACES
the base system_prompt for THIS render only (state unchanged).
Dynamic blocks ([background], [project], [earlier summary], NORRIS
suffix) still compose on top. opts is optional; nil-safe for old
callers.
- repl.lua ask_ai — captures req_class from router.classify_model
(already returned by Phase 5; previously discarded after the
status line). Looks up config.routing.system_prompts[req_class];
passes as opts.system_prompt_override to ctx:to_messages each
iteration of the tool-sub-loop.
- Gating: override fires only when routing.auto is on (no class ->
no override). If system_prompts[class] absent for a class, fall
through to the default system_prompt (no surprise).
- Norris unaffected: safety.norris_step builds its own messages
array; doesn't go through this path.
- config.lua gains a commented-out example showing routing.system_
prompts with the code/default examples from the FR body.
Smoke verified:
- 12-case context.lua unit test: opts nil/absent/present, override
replaces base, dynamic blocks still compose, state unchanged
after call, Norris-mode coexistence (suffix still present;
background still suppressed).
- E2E against cloud broker with routing.system_prompts.code set:
triple-backtick prompt -> code class -> override fires; model
emits terse code-only output. Non-code prompt -> default class
-> no override -> normal verbose-ish reply.
Regression: test_safety 87/87, test_router_model 31/31, repl loads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+20
@@ -240,6 +240,26 @@ return {
|
|||||||
-- -- Toggle at runtime with :fallback on / :fallback off.
|
-- -- Toggle at runtime with :fallback on / :fallback off.
|
||||||
-- fallback = false, -- default off (cost-safety)
|
-- fallback = false, -- default off (cost-safety)
|
||||||
-- fallback_model = "cloud",
|
-- fallback_model = "cloud",
|
||||||
|
--
|
||||||
|
-- -- Issue #86: per-class system_prompt override. When the
|
||||||
|
-- -- classified request falls into a class with an entry here,
|
||||||
|
-- -- the BASE system_prompt is REPLACED for that one request
|
||||||
|
-- -- (dynamic blocks — [background], [project], [earlier
|
||||||
|
-- -- summary], NORRIS suffix — still compose on top). Mostly
|
||||||
|
-- -- useful for tightening small local models' instruction
|
||||||
|
-- -- adherence. Default {} (no override).
|
||||||
|
-- system_prompts = {
|
||||||
|
-- code = [[You are a code assistant. Rules:
|
||||||
|
-- 1. Output ONLY the requested code or command.
|
||||||
|
-- 2. No prose explanation unless explicitly asked.
|
||||||
|
-- 3. Wrap shell commands in CMD: prefix.
|
||||||
|
-- 4. Max response: 200 tokens.]],
|
||||||
|
-- default = [[You are a shell assistant.
|
||||||
|
-- Output shell commands as: CMD: <command>
|
||||||
|
-- Output answers as single short sentences.
|
||||||
|
-- Do not ask clarifying questions.]],
|
||||||
|
-- -- reasoning routes to cloud; no override usually needed
|
||||||
|
-- },
|
||||||
-- },
|
-- },
|
||||||
|
|
||||||
-- ── Phase 5 context summarization on sliding-window eviction.
|
-- ── Phase 5 context summarization on sliding-window eviction.
|
||||||
|
|||||||
+10
-2
@@ -228,8 +228,16 @@ The user will be prompted to confirm destructive actions; expect their
|
|||||||
verdict in the next turn as a synthesized "[aish] ... skipped by user"
|
verdict in the next turn as a synthesized "[aish] ... skipped by user"
|
||||||
message if they declined.]]
|
message if they declined.]]
|
||||||
|
|
||||||
function Context:to_messages()
|
function Context:to_messages(opts)
|
||||||
local sys_content = self.system_prompt
|
-- Phase 10 (#86): per-call system_prompt_override. Replaces the
|
||||||
|
-- BASE system_prompt for THIS render only (state unchanged); the
|
||||||
|
-- dynamic blocks ([background], [project], [earlier summary],
|
||||||
|
-- NORRIS suffix) still compose on top. Used by ask_ai's routing
|
||||||
|
-- path when cfg.routing.system_prompts[class] is set — gives
|
||||||
|
-- small local models tighter instructions while preserving
|
||||||
|
-- ambient memory/project context.
|
||||||
|
local sys_content = (opts and opts.system_prompt_override)
|
||||||
|
or self.system_prompt
|
||||||
-- Phase 4 [background] memory block + Phase 6 [project] file-tree
|
-- Phase 4 [background] memory block + Phase 6 [project] file-tree
|
||||||
-- block + Phase 5 [earlier summary] block. All suppressed during
|
-- block + Phase 5 [earlier summary] block. All suppressed during
|
||||||
-- Norris (R-C1 / R-C4 — avoid redundant tokens per planning
|
-- Norris (R-C1 / R-C4 — avoid redundant tokens per planning
|
||||||
|
|||||||
@@ -978,13 +978,27 @@ function M.run(config)
|
|||||||
-- tool-sub-loop; active_name/active_cfg are NOT mutated so the
|
-- tool-sub-loop; active_name/active_cfg are NOT mutated so the
|
||||||
-- user's :model selection survives the request.
|
-- user's :model selection survives the request.
|
||||||
local req_name, req_cfg = active_name, active_cfg
|
local req_name, req_cfg = active_name, active_cfg
|
||||||
|
local req_class
|
||||||
if config.routing and config.routing.auto then
|
if config.routing and config.routing.auto then
|
||||||
local routed, class = router.classify_model(text, config)
|
local routed, class = router.classify_model(text, config)
|
||||||
|
req_class = class
|
||||||
if routed and config.models[routed] and routed ~= active_name then
|
if routed and config.models[routed] and routed ~= active_name then
|
||||||
renderer.status(("routed to %s (%s class)"):format(routed, class))
|
renderer.status(("routed to %s (%s class)"):format(routed, class))
|
||||||
req_name, req_cfg = routed, config.models[routed]
|
req_name, req_cfg = routed, config.models[routed]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- Phase 10 (#86): per-class system_prompt override. Tightens
|
||||||
|
-- instruction adherence on small local models. Lookup is
|
||||||
|
-- gated on auto-routing being on (no class -> no override).
|
||||||
|
-- When the override is set, ctx:to_messages REPLACES the base
|
||||||
|
-- system_prompt for THIS render only; dynamic blocks
|
||||||
|
-- ([background], [project], [earlier summary], NORRIS suffix)
|
||||||
|
-- still compose on top. Norris path is unaffected — it builds
|
||||||
|
-- its own messages array inside safety.norris_step.
|
||||||
|
local sys_override = config.routing
|
||||||
|
and config.routing.system_prompts
|
||||||
|
and req_class
|
||||||
|
and config.routing.system_prompts[req_class]
|
||||||
|
|
||||||
local depth = 0
|
local depth = 0
|
||||||
local final_resp = ""
|
local final_resp = ""
|
||||||
@@ -994,7 +1008,9 @@ function M.run(config)
|
|||||||
local text_parts = {}
|
local text_parts = {}
|
||||||
local tool_calls_seen = {}
|
local tool_calls_seen = {}
|
||||||
local redact_mode = secrets_mode_for(req_cfg)
|
local redact_mode = secrets_mode_for(req_cfg)
|
||||||
local scrubbed_msgs = scrub_messages(ctx:to_messages(), redact_mode)
|
local scrubbed_msgs = scrub_messages(
|
||||||
|
ctx:to_messages({ system_prompt_override = sys_override }),
|
||||||
|
redact_mode)
|
||||||
-- Streaming rehydrator wraps the on_delta so the user sees real
|
-- Streaming rehydrator wraps the on_delta so the user sees real
|
||||||
-- values; text_parts accumulates the REHYDRATED chunks so
|
-- values; text_parts accumulates the REHYDRATED chunks so
|
||||||
-- final_resp (used for CMD: / DELEGATE: extraction) is plain.
|
-- final_resp (used for CMD: / DELEGATE: extraction) is plain.
|
||||||
|
|||||||
Reference in New Issue
Block a user