@d3ara1n/pi-scout 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  Per-turn side agent decision framework for [pi](https://github.com/earendil-works/pi).
4
4
 
5
- Before each conversation turn, a cheap side agent model analyzes the user's prompt and makes routing decisions:
5
+ Before each conversation turn, scout analyzes the user's prompt and makes routing decisions:
6
6
 
7
7
  1. **skill-router** — Selects which skills to activate and injects their full content (replacing pi's default skill metadata list)
8
8
  2. **model-router** — Automatically switches the active model role based on task complexity
9
+ 3. **short-circuit** — Skips the side model entirely on trivial acknowledgments (`好的` / `ok` / `はい`), avoiding the per-turn latency and cost
9
10
 
10
- Both modules can be independently toggled on/off.
11
+ All three modules can be independently toggled on/off.
11
12
 
12
13
  ## Why model-router is disabled by default
13
14
 
@@ -35,6 +36,14 @@ Or add to `settings.json` for persistent enablement:
35
36
  }
36
37
  ```
37
38
 
39
+ ## Short-circuit layer
40
+
41
+ Short-circuit is a cost/latency optimization that lets scout **skip the side model entirely** on trivial acknowledgments. It mirrors OpenHuman's hybrid-gate pattern: cheap signals handle the obvious cases, the side model only handles the ambiguous middle — so there is no quality loss.
42
+
43
+ **Trivial acknowledgment** — a short prompt that is *entirely* an ack (`好的` / `ok` / `はい` / `네`) routes to "no skills, no role change". Matched against a built-in 中/英/日/韓 phrase table. A trivial ack settles every module, so this is safe even with model-router on. Long prompts are never treated as acks even if they start with an ack word, so `好的,那我们重构整个模块` always reaches the side model.
44
+
45
+ Anything that isn't a trivial ack falls through to the side model — that's what the model is for. When short-circuit fires, the status bar shows `✓ scout: (skipped) trivial ack` for transparency.
46
+
38
47
  ## How it works
39
48
 
40
49
  ```
@@ -43,7 +52,10 @@ User sends prompt
43
52
 
44
53
  before_agent_start hook fires
45
54
 
46
- ├─ Side agent (cheap model) analyzes prompt + available skills + current role
55
+ ├─ [short-circuit] Trivial ack? decide instantly, skip the side model
56
+ │ (status shows "✓ scout: (skipped) …")
57
+
58
+ ├─ otherwise → Side agent (cheap model) analyzes prompt + available skills + current role
47
59
  ├─ Returns: { skills: [...], role: "...", reasoning: "..." }
48
60
 
49
61
  ├─ [skill-router] Strips <available_skills> XML, injects selected skill SKILL.md content
@@ -72,7 +84,13 @@ Edit `~/.pi/agent/settings.json`:
72
84
  "maxSelectedSkills": 5,
73
85
  "modules": {
74
86
  "skillRouter": true,
75
- "modelRouter": false
87
+ "modelRouter": false,
88
+ "shortCircuit": true
89
+ },
90
+ "shortCircuit": {
91
+ "trivialAck": true,
92
+ "maxAckLength": 12,
93
+ "ackPhrases": ["收到啦", "will do"]
76
94
  }
77
95
  }
78
96
  }
@@ -85,6 +103,10 @@ Edit `~/.pi/agent/settings.json`:
85
103
  | `maxSelectedSkills` | `5` | Max skills the side agent can select |
86
104
  | `modules.skillRouter` | `true` | Enable/disable skill routing |
87
105
  | `modules.modelRouter` | `false` | Enable/disable model routing (disabled by default to avoid cache inefficiency and extra costs) |
106
+ | `modules.shortCircuit` | `true` | Enable/disable the short-circuit layer |
107
+ | `shortCircuit.trivialAck` | `true` | Enable the trivial-acknowledgment rule |
108
+ | `shortCircuit.maxAckLength` | `12` | Max prompt length (chars) for the trivial-ack rule |
109
+ | `shortCircuit.ackPhrases` | `[]` | Extra ack phrases merged on top of the built-in 中/英/日/韓 table |
88
110
 
89
111
  ## Commands
90
112
 
@@ -93,6 +115,7 @@ Edit `~/.pi/agent/settings.json`:
93
115
  | `/scout` | Show scout status and last decision |
94
116
  | `/scout:skill-router on/off` | Toggle skill-router module |
95
117
  | `/scout:model-router on/off` | Toggle model-router module |
118
+ | `/scout:short-circuit on/off` | Toggle short-circuit module |
96
119
 
97
120
  ## Performance
98
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-scout",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Per-turn side agent decision framework for pi — uses a cheap model to select skills and route models before each conversation turn",
6
6
  "main": "src/index.ts",
package/src/config.ts CHANGED
@@ -21,8 +21,7 @@ function readSettingsFile(filePath: string): any {
21
21
  try {
22
22
  if (!fs.existsSync(filePath)) return {};
23
23
  const content = fs.readFileSync(filePath, "utf-8");
24
- const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
25
- return JSON.parse(stripped);
24
+ return JSON.parse(content);
26
25
  } catch {
27
26
  return {};
28
27
  }
@@ -63,6 +62,14 @@ export function loadScoutConfig(cwd?: string): ScoutConfig {
63
62
  modules: {
64
63
  skillRouter: raw.modules?.skillRouter ?? DEFAULT_CONFIG.modules.skillRouter,
65
64
  modelRouter: raw.modules?.modelRouter ?? DEFAULT_CONFIG.modules.modelRouter,
65
+ shortCircuit: raw.modules?.shortCircuit ?? DEFAULT_CONFIG.modules.shortCircuit,
66
+ },
67
+ shortCircuit: {
68
+ trivialAck: raw.shortCircuit?.trivialAck ?? DEFAULT_CONFIG.shortCircuit.trivialAck,
69
+ maxAckLength: raw.shortCircuit?.maxAckLength ?? DEFAULT_CONFIG.shortCircuit.maxAckLength,
70
+ ackPhrases: Array.isArray(raw.shortCircuit?.ackPhrases)
71
+ ? raw.shortCircuit.ackPhrases.filter((p: any) => typeof p === "string")
72
+ : DEFAULT_CONFIG.shortCircuit.ackPhrases,
66
73
  },
67
74
  };
68
75
  }
package/src/index.ts CHANGED
@@ -20,11 +20,16 @@ import { callSideAgent } from "./side-agent.ts";
20
20
  import { buildScoutSystemPrompt, buildScoutUserMessage } from "./scout-prompt.ts";
21
21
  import { filterSkillsBlock, resetSkillCache } from "./skill-inject.ts";
22
22
  import { switchToRole } from "./model-switch.ts";
23
+ import { evaluateShortCircuit } from "./short-circuit.ts";
23
24
 
24
25
  const STATUS_KEY = "scout";
25
26
 
26
27
  /** Build a one-line status summary from a scout decision. */
27
28
  function formatDecisionStatus(decision: ScoutDecision, theme: any): string {
29
+ if (decision.source === "short-circuit") {
30
+ return theme.fg("success", "✓ scout:") + " " + theme.fg("dim", `(skipped) ${decision.reasoning}`);
31
+ }
32
+
28
33
  const parts: string[] = [];
29
34
 
30
35
  if (decision.skills.length > 0) {
@@ -77,6 +82,7 @@ export default function scoutExtension(pi: ExtensionAPI) {
77
82
  `Modules:`,
78
83
  ` skill-router: ${config.modules.skillRouter ? "on" : "off"}`,
79
84
  ` model-router: ${config.modules.modelRouter ? "on" : "off"}`,
85
+ ` short-circuit: ${config.modules.shortCircuit ? "on" : "off"}`,
80
86
  ];
81
87
 
82
88
  if (lastDecision) {
@@ -125,6 +131,23 @@ export default function scoutExtension(pi: ExtensionAPI) {
125
131
  },
126
132
  });
127
133
 
134
+ // ── /scout:short-circuit on/off ─────────────────────────────────
135
+ pi.registerCommand("scout:short-circuit", {
136
+ description: "Toggle short-circuit module (on/off)",
137
+ handler: async (args, ctx) => {
138
+ const value = (args ?? "").trim().toLowerCase();
139
+ if (value === "on") {
140
+ config.modules.shortCircuit = true;
141
+ ctx.ui.notify("Scout: short-circuit enabled", "info");
142
+ } else if (value === "off") {
143
+ config.modules.shortCircuit = false;
144
+ ctx.ui.notify("Scout: short-circuit disabled", "info");
145
+ } else {
146
+ ctx.ui.notify("Usage: /scout:short-circuit on|off", "info");
147
+ }
148
+ },
149
+ });
150
+
128
151
  // ── list_skills tool ───────────────────────────────────────────
129
152
  let cachedAllSkills: Array<{ name: string; description: string; filePath: string }> = [];
130
153
 
@@ -192,6 +215,52 @@ export default function scoutExtension(pi: ExtensionAPI) {
192
215
 
193
216
  const theme = ctx.ui.theme;
194
217
 
218
+ // Skills available this turn — used by both the short-circuit layer
219
+ // and the side-agent path.
220
+ const skills = event.systemPromptOptions?.skills ?? [];
221
+ if (cachedAllSkills.length === 0 && skills.length > 0) {
222
+ cachedAllSkills = skills.map((s: any) => ({
223
+ name: s.name,
224
+ description: s.description ?? "",
225
+ filePath: s.filePath,
226
+ }));
227
+ }
228
+ const skillEntries = skills.map((s: any) => ({
229
+ name: s.name,
230
+ description: s.description ?? "",
231
+ filePath: s.filePath,
232
+ }));
233
+
234
+ // ── Short-circuit layer ───────────────────────────────────
235
+ // Skip the side model on trivial acknowledgments. A trivial ack means
236
+ // "no skills, don't switch models" — both answers are certain, so this
237
+ // is safe even with model-router on. Runs before model resolution to
238
+ // save that cost too.
239
+ if (config.modules.shortCircuit) {
240
+ const skip = evaluateShortCircuit(event.prompt, config.shortCircuit);
241
+ if (skip) {
242
+ const decision: ScoutDecision = {
243
+ skills: [],
244
+ role: null,
245
+ reasoning: skip.reasoning,
246
+ source: "short-circuit",
247
+ };
248
+ lastDecision = decision;
249
+
250
+ let systemPrompt = event.systemPrompt;
251
+ if (config.modules.skillRouter) {
252
+ systemPrompt = filterSkillsBlock(systemPrompt, [], skillEntries);
253
+ }
254
+
255
+ // Keep prevTurn current so the next turn still has context.
256
+ prevTurn = { userPrompt: event.prompt, assistantSummary: "" };
257
+
258
+ ctx.ui.setStatus(STATUS_KEY, formatDecisionStatus(decision, theme));
259
+ if (systemPrompt !== event.systemPrompt) return { systemPrompt };
260
+ return;
261
+ }
262
+ }
263
+
195
264
  // Show "Scouting..." indicator
196
265
  ctx.ui.setStatus(STATUS_KEY, theme.fg("accent", "◎") + theme.fg("dim", " Scouting..."));
197
266
 
@@ -206,15 +275,7 @@ export default function scoutExtension(pi: ExtensionAPI) {
206
275
  // Update status: resolving
207
276
  ctx.ui.setStatus(STATUS_KEY, theme.fg("accent", "◎") + theme.fg("dim", ` Scouting via ${sideResolved.model.provider}/${sideResolved.model.id}...`));
208
277
 
209
- // 1. Get available skills from systemPromptOptions
210
- const skills = event.systemPromptOptions?.skills ?? [];
211
- if (cachedAllSkills.length === 0 && skills.length > 0) {
212
- cachedAllSkills = skills.map((s: any) => ({
213
- name: s.name,
214
- description: s.description ?? "",
215
- filePath: s.filePath,
216
- }));
217
- }
278
+ // 1. Build the skills list for the side agent prompt
218
279
  const skillsList = skills
219
280
  .map((s: any) => `- ${s.name}: ${s.description ?? "(no description)"}`)
220
281
  .join("\n");
@@ -265,11 +326,7 @@ export default function scoutExtension(pi: ExtensionAPI) {
265
326
 
266
327
  // 6. skill-router: filter skills XML to only selected ones
267
328
  if (config.modules.skillRouter) {
268
- systemPrompt = filterSkillsBlock(
269
- systemPrompt,
270
- decision.skills,
271
- skills.map((s: any) => ({ name: s.name, description: s.description ?? "", filePath: s.filePath })),
272
- );
329
+ systemPrompt = filterSkillsBlock(systemPrompt, decision.skills, skillEntries);
273
330
  }
274
331
 
275
332
  // 7. model-router: switch model if side agent recommends a different role
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Tests for the short-circuit layer.
3
+ * Run: node --test packages/pi-scout/src/short-circuit.test.ts
4
+ */
5
+
6
+ import { test } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import {
9
+ normalizeAckPrompt,
10
+ buildAckSet,
11
+ evaluateShortCircuit,
12
+ } from "./short-circuit.ts";
13
+ import type { ShortCircuitConfig } from "./types.ts";
14
+
15
+ const CFG = (overrides: Partial<ShortCircuitConfig> = {}): ShortCircuitConfig => ({
16
+ trivialAck: true,
17
+ maxAckLength: 12,
18
+ ackPhrases: [],
19
+ ...overrides,
20
+ });
21
+
22
+ // ── normalizeAckPrompt ─────────────────────────────────────────
23
+
24
+ test("normalizeAckPrompt strips trailing CJK/Latin punctuation", () => {
25
+ assert.equal(normalizeAckPrompt("好的。"), "好的");
26
+ assert.equal(normalizeAckPrompt("OK!"), "ok");
27
+ assert.equal(normalizeAckPrompt(" 嗯 "), "嗯");
28
+ assert.equal(normalizeAckPrompt("はい。"), "はい");
29
+ assert.equal(normalizeAckPrompt("네!"), "네");
30
+ });
31
+
32
+ test("normalizeAckPrompt preserves internal punctuation", () => {
33
+ assert.equal(normalizeAckPrompt("好的,那我们继续"), "好的,那我们继续");
34
+ });
35
+
36
+ test("normalizeAckPrompt lowercases", () => {
37
+ assert.equal(normalizeAckPrompt("OK"), "ok");
38
+ assert.equal(normalizeAckPrompt("Sure"), "sure");
39
+ });
40
+
41
+ test("normalizeAckPrompt empty for pure punctuation", () => {
42
+ assert.equal(normalizeAckPrompt("。。。"), "");
43
+ assert.equal(normalizeAckPrompt(" "), "");
44
+ });
45
+
46
+ // ── buildAckSet ────────────────────────────────────────────────
47
+
48
+ test("buildAckSet includes defaults and user extras, normalized+deduped", () => {
49
+ const set = buildAckSet(["收到啦", "OK", " custom "]);
50
+ assert.ok(set.has("好的"));
51
+ assert.ok(set.has("ok"));
52
+ assert.ok(set.has("はい"));
53
+ assert.ok(set.has("네"));
54
+ assert.ok(set.has("收到啦"));
55
+ assert.ok(set.has("custom"));
56
+ });
57
+
58
+ test("buildAckSet ignores empty normalized entries", () => {
59
+ const set = buildAckSet(["。。。", ""]);
60
+ assert.equal(set.size > 0, true);
61
+ assert.ok(!set.has(""));
62
+ });
63
+
64
+ // ── evaluateShortCircuit ───────────────────────────────────────
65
+
66
+ test("trivial ack short-circuits across 中/英/日/韓", () => {
67
+ const cfg = CFG();
68
+ for (const prompt of ["好的", "ok", "はい", "네", "嗯嗯", "sure", "了解"]) {
69
+ const r = evaluateShortCircuit(prompt, cfg);
70
+ assert.ok(r, `expected short-circuit for: ${prompt}`);
71
+ assert.equal(r!.reasoning, "trivial ack");
72
+ }
73
+ });
74
+
75
+ test("trivial ack tolerates trailing punctuation", () => {
76
+ const r = evaluateShortCircuit("好的。", CFG());
77
+ assert.ok(r);
78
+ assert.equal(r!.reasoning, "trivial ack");
79
+ });
80
+
81
+ test("long prompt starting with ack is NOT short-circuited", () => {
82
+ const r = evaluateShortCircuit("好的,那我们重构整个模块吧", CFG());
83
+ assert.equal(r, null);
84
+ });
85
+
86
+ test("prompt longer than maxAckLength is not an ack", () => {
87
+ assert.equal(
88
+ evaluateShortCircuit("understood thank you", CFG({ maxAckLength: 5 })),
89
+ null,
90
+ );
91
+ });
92
+
93
+ test("trivialAck disabled → ack falls through to model", () => {
94
+ const r = evaluateShortCircuit("好的", CFG({ trivialAck: false }));
95
+ assert.equal(r, null);
96
+ });
97
+
98
+ test("user ackPhrases extend the table", () => {
99
+ const r = evaluateShortCircuit("收到啦", CFG({ ackPhrases: ["收到啦"] }));
100
+ assert.ok(r);
101
+ assert.equal(r!.reasoning, "trivial ack");
102
+ });
103
+
104
+ test("empty / punctuation-only prompt is not an ack", () => {
105
+ assert.equal(evaluateShortCircuit("", CFG()), null);
106
+ assert.equal(evaluateShortCircuit("。。。", CFG()), null);
107
+ });
108
+
109
+ test("ordinary prompt falls through to model", () => {
110
+ assert.equal(
111
+ evaluateShortCircuit("帮我看看这个函数有没有性能问题", CFG()),
112
+ null,
113
+ );
114
+ assert.equal(
115
+ evaluateShortCircuit("what is the weather today", CFG()),
116
+ null,
117
+ );
118
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Short-circuit layer — skip the side model on trivial prompts.
3
+ *
4
+ * Borrowed from OpenHuman's hybrid-gate pattern (PR #775): cheap signals
5
+ * short-circuit the obvious cases, the model only handles the ambiguous
6
+ * middle. There is no quality loss — the short-circuit fires only when the
7
+ * routing decision is already certain, everything else falls through to the
8
+ * side model.
9
+ *
10
+ * One rule, multilingual by construction:
11
+ *
12
+ * **Trivial acknowledgment** — a short prompt that is *entirely* an ack
13
+ * ("好的" / "ok" / "はい" / "네"). Matched against a built-in 中/英/日/韓
14
+ * phrase table. A trivial ack means "no skills, don't switch models" — both
15
+ * module answers are certain, so skipping the side model is always safe, even
16
+ * with model-router on. Long prompts are never treated as acks even if they
17
+ * start with an ack word, so "好的,那我们重构整个模块" always reaches the model.
18
+ */
19
+
20
+ import type { ShortCircuitConfig } from "./types.ts";
21
+
22
+ /** Result of a successful short-circuit. */
23
+ export interface ShortCircuitResult {
24
+ /** Short human-readable reason, surfaced in the status bar. */
25
+ reasoning: string;
26
+ }
27
+
28
+ /**
29
+ * Built-in trivial-acknowledgment phrases, 中/英/日/韓.
30
+ *
31
+ * High-precision only — every entry is unambiguously a pure ack. When unsure,
32
+ * prefer omission: a missed ack simply reaches the model, while a false ack
33
+ * would silently drop routing. User-supplied `ackPhrases` are merged on top.
34
+ */
35
+ const DEFAULT_ACK_PHRASES: string[] = [
36
+ // 中文
37
+ "好的", "好", "嗯", "嗯嗯", "嗯呢", "行", "可以", "对", "对的", "是", "是的",
38
+ "继续", "继续吧", "往下", "没问题", "明白", "收到", "了解", "知道了", "晓得",
39
+ "中", "成", "行吧", "可以的", "好的呀", "好嘞", "妥", "妥了", "嗯哼",
40
+ // English
41
+ "ok", "okay", "oki", "okie", "okk", "k", "sure", "yes", "yeah", "yep",
42
+ "yup", "continue", "go", "ahead", "go ahead", "sounds good", "got it",
43
+ "understood", "will do", "agreed", "roger", "proceed", "ack",
44
+ "acknowledged", "fine",
45
+ // 日本語
46
+ "はい", "うん", "おk", "続けて", "了解", "承知", "わかった", "分かった",
47
+ "ええ", "継続", "進めて", "いいよ", "いいです", "オッケー", "おけ",
48
+ // 한국어
49
+ "네", "응", "응응", "계속", "알겠어", "알겠음", "좋아", "그래", "오키",
50
+ ];
51
+
52
+ /**
53
+ * Normalize a prompt/phrase for ack matching: trim, lowercase, strip
54
+ * leading/trailing punctuation. Internal punctuation is preserved so that
55
+ * non-ack content keeps the string out of the ack set.
56
+ * @internal — exported for testing.
57
+ */
58
+ export function normalizeAckPrompt(input: string): string {
59
+ return input
60
+ .trim()
61
+ .toLowerCase()
62
+ .replace(/^[。!?.!?,,、~~…·\s]+/, "")
63
+ .replace(/[。!?.!?,,、~~…·\s]+$/, "");
64
+ }
65
+
66
+ /**
67
+ * Build the full ack-phrase set: built-in defaults plus user extras,
68
+ * all normalized. Deduplicated via Set.
69
+ * @internal — exported for testing.
70
+ */
71
+ export function buildAckSet(extra: string[]): Set<string> {
72
+ const all = [...DEFAULT_ACK_PHRASES, ...extra];
73
+ const set = new Set<string>();
74
+ for (const phrase of all) {
75
+ const n = normalizeAckPrompt(phrase);
76
+ if (n.length > 0) set.add(n);
77
+ }
78
+ return set;
79
+ }
80
+
81
+ /**
82
+ * Evaluate whether a prompt can be short-circuited as a trivial ack.
83
+ * Returns the decision when it can, `null` when the prompt must reach the
84
+ * side model.
85
+ *
86
+ * @param prompt - The user's prompt text
87
+ * @param config - Short-circuit tuning
88
+ */
89
+ export function evaluateShortCircuit(
90
+ prompt: string,
91
+ config: ShortCircuitConfig,
92
+ ): ShortCircuitResult | null {
93
+ if (!config.trivialAck) return null;
94
+
95
+ const normalized = normalizeAckPrompt(prompt);
96
+ if (normalized.length === 0 || normalized.length > config.maxAckLength) {
97
+ return null;
98
+ }
99
+
100
+ const ackSet = buildAckSet(config.ackPhrases);
101
+ return ackSet.has(normalized) ? { reasoning: "trivial ack" } : null;
102
+ }
package/src/side-agent.ts CHANGED
@@ -31,7 +31,7 @@ export async function callSideAgent(
31
31
  systemPrompt: string,
32
32
  userMessage: string,
33
33
  ): Promise<ScoutDecision> {
34
- const fallback: ScoutDecision = { skills: [], role: null, reasoning: "side agent error" };
34
+ const fallback: ScoutDecision = { skills: [], role: null, reasoning: "side agent error", source: "side-agent" };
35
35
 
36
36
  const context: SideAgentContext = {
37
37
  systemPrompt,
@@ -71,7 +71,7 @@ export async function callSideAgent(
71
71
  * Tolerant of markdown wrapping, extra whitespace, etc.
72
72
  */
73
73
  function parseDecision(raw: string): ScoutDecision {
74
- const fallback: ScoutDecision = { skills: [], role: null, reasoning: "parse error" };
74
+ const fallback: ScoutDecision = { skills: [], role: null, reasoning: "parse error", source: "side-agent" };
75
75
 
76
76
  // Strip markdown code fences if present
77
77
  let text = raw.trim();
@@ -92,6 +92,7 @@ function parseDecision(raw: string): ScoutDecision {
92
92
  reasoning: typeof parsed.reasoning === "string"
93
93
  ? parsed.reasoning
94
94
  : "no reasoning provided",
95
+ source: "side-agent",
95
96
  };
96
97
  } catch {
97
98
  console.warn("[pi-scout] Failed to parse side agent response:", raw.slice(0, 200));
package/src/types.ts CHANGED
@@ -14,10 +14,35 @@ export interface ScoutConfig {
14
14
  modules: {
15
15
  skillRouter: boolean;
16
16
  modelRouter: boolean;
17
+ /** Short-circuit layer: skip the side LLM on high-confidence prompts */
18
+ shortCircuit: boolean;
17
19
  };
20
+ /** Tuning for the short-circuit module */
21
+ shortCircuit: ShortCircuitConfig;
18
22
  }
19
23
 
20
- /** Decision returned by the side agent. */
24
+ /**
25
+ * Tuning for the short-circuit module.
26
+ *
27
+ * The short-circuit layer lets scout skip the side model entirely on
28
+ * trivial acknowledgments ("好的" / "ok" / "はい"). A trivial ack means
29
+ * "no skills, don't switch models" — both module answers are certain, so
30
+ * skipping the side model is always safe, even with model-router on.
31
+ * Ambiguous prompts always fall through to the side model, so there is no
32
+ * quality loss on hard cases.
33
+ */
34
+ export interface ShortCircuitConfig {
35
+ /** Enable the trivial-acknowledgment rule */
36
+ trivialAck: boolean;
37
+ /** Max prompt length (chars) for the trivial-ack rule. Longer prompts are
38
+ * never short-circuited as acks even if they begin with an ack word. */
39
+ maxAckLength: number;
40
+ /** Additional ack phrases merged on top of the built-in 中/英/日/韓 table.
41
+ * Provide raw strings; they are normalized (trimmed, lowercased) at runtime. */
42
+ ackPhrases: string[];
43
+ }
44
+
45
+ /** Decision returned by the side agent or the short-circuit layer. */
21
46
  export interface ScoutDecision {
22
47
  /** Selected skill names */
23
48
  skills: string[];
@@ -25,6 +50,8 @@ export interface ScoutDecision {
25
50
  role: string | null;
26
51
  /** Brief reasoning */
27
52
  reasoning: string;
53
+ /** Where the decision came from — controls status-bar presentation */
54
+ source?: "side-agent" | "short-circuit";
28
55
  }
29
56
 
30
57
  export const DEFAULT_CONFIG: ScoutConfig = {
@@ -34,5 +61,11 @@ export const DEFAULT_CONFIG: ScoutConfig = {
34
61
  modules: {
35
62
  skillRouter: true,
36
63
  modelRouter: false,
64
+ shortCircuit: true,
65
+ },
66
+ shortCircuit: {
67
+ trivialAck: true,
68
+ maxAckLength: 12,
69
+ ackPhrases: [],
37
70
  },
38
71
  };