@d3ara1n/pi-scout 1.0.2 → 1.1.1

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.2",
3
+ "version": "1.1.1",
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
@@ -12,56 +12,52 @@ import type { ScoutConfig } from "./types.ts";
12
12
  import { DEFAULT_CONFIG } from "./types.ts";
13
13
 
14
14
  function getAgentDir(): string {
15
- const envDir = process.env.PI_AGENT_DIR;
16
- if (envDir) return envDir;
17
- return path.join(os.homedir(), ".pi", "agent");
15
+ const envDir = process.env.PI_AGENT_DIR;
16
+ if (envDir) return envDir;
17
+ return path.join(os.homedir(), ".pi", "agent");
18
18
  }
19
19
 
20
20
  function readSettingsFile(filePath: string): any {
21
- try {
22
- if (!fs.existsSync(filePath)) return {};
23
- const content = fs.readFileSync(filePath, "utf-8");
24
- return JSON.parse(content);
25
- } catch {
26
- return {};
27
- }
21
+ try {
22
+ const content = fs.readFileSync(filePath, "utf-8");
23
+ return JSON.parse(content);
24
+ } catch {
25
+ return {};
26
+ }
28
27
  }
29
28
 
30
- function merge(target: any, source: any): any {
31
- if (!source || typeof source !== "object") return target;
32
- if (!target || typeof target !== "object") return source;
33
- const result = { ...target };
34
- for (const key of Object.keys(source)) {
35
- if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
36
- result[key] = merge(result[key], source[key]);
37
- } else {
38
- result[key] = source[key];
39
- }
40
- }
41
- return result;
29
+ /** Read the `scout` block from a settings file. */
30
+ function readScout(filePath: string): Record<string, any> | undefined {
31
+ const raw = readSettingsFile(filePath)?.scout;
32
+ return raw && typeof raw === "object" ? raw : undefined;
42
33
  }
43
34
 
44
35
  /**
45
- * Load scout config from merged settings.
36
+ * Load scout config. Project overrides global wholesale; per-field `??
37
+ * DEFAULT` fills any gap. (No field-level merge — project replaces global.)
46
38
  * @param cwd - Project working directory
47
39
  */
48
40
  export function loadScoutConfig(cwd?: string): ScoutConfig {
49
- const globalSettings = readSettingsFile(path.join(getAgentDir(), "settings.json"));
50
- const projectSettings = cwd
51
- ? readSettingsFile(path.join(cwd, ".pi", "settings.json"))
52
- : {};
53
- const settings = merge(globalSettings, projectSettings);
41
+ const globalRaw = readScout(path.join(getAgentDir(), "settings.json"));
42
+ const projectRaw = cwd ? readScout(path.join(cwd, ".pi", "settings.json")) : undefined;
43
+ const raw = projectRaw ?? globalRaw;
44
+ if (!raw) return DEFAULT_CONFIG;
54
45
 
55
- const raw = settings?.scout;
56
- if (!raw) return DEFAULT_CONFIG;
57
-
58
- return {
59
- enabled: raw.enabled ?? DEFAULT_CONFIG.enabled,
60
- sideAgentRole: raw.sideAgentRole ?? DEFAULT_CONFIG.sideAgentRole,
61
- maxSelectedSkills: raw.maxSelectedSkills ?? DEFAULT_CONFIG.maxSelectedSkills,
62
- modules: {
63
- skillRouter: raw.modules?.skillRouter ?? DEFAULT_CONFIG.modules.skillRouter,
64
- modelRouter: raw.modules?.modelRouter ?? DEFAULT_CONFIG.modules.modelRouter,
65
- },
66
- };
46
+ return {
47
+ enabled: raw.enabled ?? DEFAULT_CONFIG.enabled,
48
+ sideAgentRole: raw.sideAgentRole ?? DEFAULT_CONFIG.sideAgentRole,
49
+ maxSelectedSkills: raw.maxSelectedSkills ?? DEFAULT_CONFIG.maxSelectedSkills,
50
+ modules: {
51
+ skillRouter: raw.modules?.skillRouter ?? DEFAULT_CONFIG.modules.skillRouter,
52
+ modelRouter: raw.modules?.modelRouter ?? DEFAULT_CONFIG.modules.modelRouter,
53
+ shortCircuit: raw.modules?.shortCircuit ?? DEFAULT_CONFIG.modules.shortCircuit,
54
+ },
55
+ shortCircuit: {
56
+ trivialAck: raw.shortCircuit?.trivialAck ?? DEFAULT_CONFIG.shortCircuit.trivialAck,
57
+ maxAckLength: raw.shortCircuit?.maxAckLength ?? DEFAULT_CONFIG.shortCircuit.maxAckLength,
58
+ ackPhrases: Array.isArray(raw.shortCircuit?.ackPhrases)
59
+ ? raw.shortCircuit.ackPhrases.filter((p: any) => typeof p === "string")
60
+ : DEFAULT_CONFIG.shortCircuit.ackPhrases,
61
+ },
62
+ };
67
63
  }