@d3ara1n/pi-scout 1.1.0 → 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.
@@ -12,26 +12,26 @@ import type { ModelRolesAPI } from "@d3ara1n/pi-model-roles";
12
12
  * @returns true if the switch was successful
13
13
  */
14
14
  export async function switchToRole(
15
- pi: ExtensionAPI,
16
- roleName: string,
17
- rolesApi: ModelRolesAPI,
15
+ pi: ExtensionAPI,
16
+ roleName: string,
17
+ rolesApi: ModelRolesAPI,
18
18
  ): Promise<boolean> {
19
- const resolved = await rolesApi.resolveRoleAsync(roleName);
19
+ const resolved = await rolesApi.resolveRoleAsync(roleName);
20
20
 
21
- if (!resolved.model) {
22
- console.warn(`[pi-scout] Role "${roleName}" could not be resolved — model not available`);
23
- return false;
24
- }
21
+ if (!resolved.model) {
22
+ console.warn(`[pi-scout] Role "${roleName}" could not be resolved — model not available`);
23
+ return false;
24
+ }
25
25
 
26
- const success = await pi.setModel(resolved.model);
27
- if (!success) {
28
- console.warn(`[pi-scout] setModel() returned false for role "${roleName}" — no API key?`);
29
- return false;
30
- }
26
+ const success = await pi.setModel(resolved.model);
27
+ if (!success) {
28
+ console.warn(`[pi-scout] setModel() returned false for role "${roleName}" — no API key?`);
29
+ return false;
30
+ }
31
31
 
32
- if (resolved.config.thinking) {
33
- pi.setThinkingLevel(resolved.config.thinking);
34
- }
32
+ if (resolved.config.thinking) {
33
+ pi.setThinkingLevel(resolved.config.thinking);
34
+ }
35
35
 
36
- return true;
36
+ return true;
37
37
  }
@@ -6,8 +6,8 @@ import type { ScoutConfig } from "./types.ts";
6
6
 
7
7
  /** Previous turn context for better routing decisions. */
8
8
  export interface PrevTurnContext {
9
- userPrompt: string;
10
- assistantSummary: string;
9
+ userPrompt: string;
10
+ assistantSummary: string;
11
11
  }
12
12
 
13
13
  /**
@@ -17,30 +17,30 @@ export interface PrevTurnContext {
17
17
  * understand follow-up prompts like "continue", "change that", etc.
18
18
  */
19
19
  export function buildScoutUserMessage(
20
- userPrompt: string,
21
- currentRole: string,
22
- prevTurn?: PrevTurnContext,
20
+ userPrompt: string,
21
+ currentRole: string,
22
+ prevTurn?: PrevTurnContext,
23
23
  ): string {
24
- const parts: string[] = [];
24
+ const parts: string[] = [];
25
25
 
26
- parts.push(`Current role: ${currentRole}`);
26
+ parts.push(`Current role: ${currentRole}`);
27
27
 
28
- if (prevTurn && (prevTurn.userPrompt || prevTurn.assistantSummary)) {
29
- parts.push(``);
30
- parts.push(`## Previous Turn`);
31
- if (prevTurn.userPrompt) {
32
- parts.push(`User: ${prevTurn.userPrompt}`);
33
- }
34
- if (prevTurn.assistantSummary) {
35
- parts.push(`Assistant: ${prevTurn.assistantSummary}`);
36
- }
37
- }
28
+ if (prevTurn && (prevTurn.userPrompt || prevTurn.assistantSummary)) {
29
+ parts.push(``);
30
+ parts.push(`## Previous Turn`);
31
+ if (prevTurn.userPrompt) {
32
+ parts.push(`User: ${prevTurn.userPrompt}`);
33
+ }
34
+ if (prevTurn.assistantSummary) {
35
+ parts.push(`Assistant: ${prevTurn.assistantSummary}`);
36
+ }
37
+ }
38
38
 
39
- parts.push(``);
40
- parts.push(`## Current User Prompt`);
41
- parts.push(userPrompt);
39
+ parts.push(``);
40
+ parts.push(`## Current User Prompt`);
41
+ parts.push(userPrompt);
42
42
 
43
- return parts.join("\n");
43
+ return parts.join("\n");
44
44
  }
45
45
 
46
46
  /**
@@ -52,48 +52,52 @@ export function buildScoutUserMessage(
52
52
  * prompt caching to activate.
53
53
  */
54
54
  export function buildScoutSystemPrompt(
55
- config: ScoutConfig,
56
- skillsList: string,
57
- rolesList: string,
55
+ config: ScoutConfig,
56
+ skillsList: string,
57
+ rolesList: string,
58
58
  ): string {
59
- const parts: string[] = [];
59
+ const parts: string[] = [];
60
60
 
61
- parts.push(`You are a scout. Analyze the user's request and decide which skills and model role to use.`);
62
- parts.push(``);
63
- parts.push(`## Response Format`);
64
- parts.push(`Respond with ONLY a JSON object, no markdown, no explanation outside the JSON:`);
65
- parts.push(`{`);
66
- parts.push(` "skills": ["skill-name-1", "skill-name-2"],`);
67
- parts.push(` "role": "role-name-or-null",`);
68
- parts.push(` "reasoning": "one sentence explanation"`);
69
- parts.push(`}`);
70
- parts.push(``);
71
- parts.push(`## Rules`);
72
- parts.push(`- Select at most ${config.maxSelectedSkills} skills. Select 0 if none are relevant.`);
73
- parts.push(`- Only select skills that will materially help with the task.`);
74
- parts.push(`- If the task is trivial (simple question, acknowledgment), select 0 skills.`);
75
- parts.push(`- "role" should be null if the current role is appropriate.`);
76
- parts.push(`- Only suggest a role change when the task clearly benefits from a different model.`);
77
- parts.push(`- Be conservative: prefer fewer skills and no role change when uncertain.`);
78
- parts.push(`- Use the Previous Turn context to understand follow-up requests (e.g. "continue", "change that", "no, the other one").`);
61
+ parts.push(
62
+ `You are a scout. Analyze the user's request and decide which skills and model role to use.`,
63
+ );
64
+ parts.push(``);
65
+ parts.push(`## Response Format`);
66
+ parts.push(`Respond with ONLY a JSON object, no markdown, no explanation outside the JSON:`);
67
+ parts.push(`{`);
68
+ parts.push(` "skills": ["skill-name-1", "skill-name-2"],`);
69
+ parts.push(` "role": "role-name-or-null",`);
70
+ parts.push(` "reasoning": "one sentence explanation"`);
71
+ parts.push(`}`);
72
+ parts.push(``);
73
+ parts.push(`## Rules`);
74
+ parts.push(`- Select at most ${config.maxSelectedSkills} skills. Select 0 if none are relevant.`);
75
+ parts.push(`- Only select skills that will materially help with the task.`);
76
+ parts.push(`- If the task is trivial (simple question, acknowledgment), select 0 skills.`);
77
+ parts.push(`- "role" should be null if the current role is appropriate.`);
78
+ parts.push(`- Only suggest a role change when the task clearly benefits from a different model.`);
79
+ parts.push(`- Be conservative: prefer fewer skills and no role change when uncertain.`);
80
+ parts.push(
81
+ `- Use the Previous Turn context to understand follow-up requests (e.g. "continue", "change that", "no, the other one").`,
82
+ );
79
83
 
80
- // Stable prefix ends here. Sections below are injected conditionally per
81
- // module toggle: a disabled module contributes no candidates, so the side
82
- // agent has nothing to choose from for it (and the application layer zeros
83
- // out its field regardless). The longer section (skills) comes first so
84
- // toggling the later module (roles) only invalidates the cache tail —
85
- // Anthropic prefix-cache matches the longest common prefix.
86
- if (config.modules.skillRouter) {
87
- parts.push(``);
88
- parts.push(`## Available Skills`);
89
- parts.push(skillsList || "(none)");
90
- }
84
+ // Stable prefix ends here. Sections below are injected conditionally per
85
+ // module toggle: a disabled module contributes no candidates, so the side
86
+ // agent has nothing to choose from for it (and the application layer zeros
87
+ // out its field regardless). The longer section (skills) comes first so
88
+ // toggling the later module (roles) only invalidates the cache tail —
89
+ // Anthropic prefix-cache matches the longest common prefix.
90
+ if (config.modules.skillRouter) {
91
+ parts.push(``);
92
+ parts.push(`## Available Skills`);
93
+ parts.push(skillsList || "(none)");
94
+ }
91
95
 
92
- if (config.modules.modelRouter) {
93
- parts.push(``);
94
- parts.push(`## Available Roles`);
95
- parts.push(rolesList);
96
- }
96
+ if (config.modules.modelRouter) {
97
+ parts.push(``);
98
+ parts.push(`## Available Roles`);
99
+ parts.push(rolesList);
100
+ }
97
101
 
98
- return parts.join("\n");
102
+ return parts.join("\n");
99
103
  }
@@ -5,114 +5,101 @@
5
5
 
6
6
  import { test } from "node:test";
7
7
  import assert from "node:assert/strict";
8
- import {
9
- normalizeAckPrompt,
10
- buildAckSet,
11
- evaluateShortCircuit,
12
- } from "./short-circuit.ts";
8
+ import { normalizeAckPrompt, buildAckSet, evaluateShortCircuit } from "./short-circuit.ts";
13
9
  import type { ShortCircuitConfig } from "./types.ts";
14
10
 
15
11
  const CFG = (overrides: Partial<ShortCircuitConfig> = {}): ShortCircuitConfig => ({
16
- trivialAck: true,
17
- maxAckLength: 12,
18
- ackPhrases: [],
19
- ...overrides,
12
+ trivialAck: true,
13
+ maxAckLength: 12,
14
+ ackPhrases: [],
15
+ ...overrides,
20
16
  });
21
17
 
22
18
  // ── normalizeAckPrompt ─────────────────────────────────────────
23
19
 
24
20
  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("네!"), "네");
21
+ assert.equal(normalizeAckPrompt("好的。"), "好的");
22
+ assert.equal(normalizeAckPrompt("OK!"), "ok");
23
+ assert.equal(normalizeAckPrompt(" 嗯 "), "嗯");
24
+ assert.equal(normalizeAckPrompt("はい。"), "はい");
25
+ assert.equal(normalizeAckPrompt("네!"), "네");
30
26
  });
31
27
 
32
28
  test("normalizeAckPrompt preserves internal punctuation", () => {
33
- assert.equal(normalizeAckPrompt("好的,那我们继续"), "好的,那我们继续");
29
+ assert.equal(normalizeAckPrompt("好的,那我们继续"), "好的,那我们继续");
34
30
  });
35
31
 
36
32
  test("normalizeAckPrompt lowercases", () => {
37
- assert.equal(normalizeAckPrompt("OK"), "ok");
38
- assert.equal(normalizeAckPrompt("Sure"), "sure");
33
+ assert.equal(normalizeAckPrompt("OK"), "ok");
34
+ assert.equal(normalizeAckPrompt("Sure"), "sure");
39
35
  });
40
36
 
41
37
  test("normalizeAckPrompt empty for pure punctuation", () => {
42
- assert.equal(normalizeAckPrompt("。。。"), "");
43
- assert.equal(normalizeAckPrompt(" "), "");
38
+ assert.equal(normalizeAckPrompt("。。。"), "");
39
+ assert.equal(normalizeAckPrompt(" "), "");
44
40
  });
45
41
 
46
42
  // ── buildAckSet ────────────────────────────────────────────────
47
43
 
48
44
  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"));
45
+ const set = buildAckSet(["收到啦", "OK", " custom "]);
46
+ assert.ok(set.has("好的"));
47
+ assert.ok(set.has("ok"));
48
+ assert.ok(set.has("はい"));
49
+ assert.ok(set.has("네"));
50
+ assert.ok(set.has("收到啦"));
51
+ assert.ok(set.has("custom"));
56
52
  });
57
53
 
58
54
  test("buildAckSet ignores empty normalized entries", () => {
59
- const set = buildAckSet(["。。。", ""]);
60
- assert.equal(set.size > 0, true);
61
- assert.ok(!set.has(""));
55
+ const set = buildAckSet(["。。。", ""]);
56
+ assert.equal(set.size > 0, true);
57
+ assert.ok(!set.has(""));
62
58
  });
63
59
 
64
60
  // ── evaluateShortCircuit ───────────────────────────────────────
65
61
 
66
62
  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
- }
63
+ const cfg = CFG();
64
+ for (const prompt of ["好的", "ok", "はい", "네", "嗯嗯", "sure", "了解"]) {
65
+ const r = evaluateShortCircuit(prompt, cfg);
66
+ assert.ok(r, `expected short-circuit for: ${prompt}`);
67
+ assert.equal(r!.reasoning, "trivial ack");
68
+ }
73
69
  });
74
70
 
75
71
  test("trivial ack tolerates trailing punctuation", () => {
76
- const r = evaluateShortCircuit("好的。", CFG());
77
- assert.ok(r);
78
- assert.equal(r!.reasoning, "trivial ack");
72
+ const r = evaluateShortCircuit("好的。", CFG());
73
+ assert.ok(r);
74
+ assert.equal(r!.reasoning, "trivial ack");
79
75
  });
80
76
 
81
77
  test("long prompt starting with ack is NOT short-circuited", () => {
82
- const r = evaluateShortCircuit("好的,那我们重构整个模块吧", CFG());
83
- assert.equal(r, null);
78
+ const r = evaluateShortCircuit("好的,那我们重构整个模块吧", CFG());
79
+ assert.equal(r, null);
84
80
  });
85
81
 
86
82
  test("prompt longer than maxAckLength is not an ack", () => {
87
- assert.equal(
88
- evaluateShortCircuit("understood thank you", CFG({ maxAckLength: 5 })),
89
- null,
90
- );
83
+ assert.equal(evaluateShortCircuit("understood thank you", CFG({ maxAckLength: 5 })), null);
91
84
  });
92
85
 
93
86
  test("trivialAck disabled → ack falls through to model", () => {
94
- const r = evaluateShortCircuit("好的", CFG({ trivialAck: false }));
95
- assert.equal(r, null);
87
+ const r = evaluateShortCircuit("好的", CFG({ trivialAck: false }));
88
+ assert.equal(r, null);
96
89
  });
97
90
 
98
91
  test("user ackPhrases extend the table", () => {
99
- const r = evaluateShortCircuit("收到啦", CFG({ ackPhrases: ["收到啦"] }));
100
- assert.ok(r);
101
- assert.equal(r!.reasoning, "trivial ack");
92
+ const r = evaluateShortCircuit("收到啦", CFG({ ackPhrases: ["收到啦"] }));
93
+ assert.ok(r);
94
+ assert.equal(r!.reasoning, "trivial ack");
102
95
  });
103
96
 
104
97
  test("empty / punctuation-only prompt is not an ack", () => {
105
- assert.equal(evaluateShortCircuit("", CFG()), null);
106
- assert.equal(evaluateShortCircuit("。。。", CFG()), null);
98
+ assert.equal(evaluateShortCircuit("", CFG()), null);
99
+ assert.equal(evaluateShortCircuit("。。。", CFG()), null);
107
100
  });
108
101
 
109
102
  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
- );
103
+ assert.equal(evaluateShortCircuit("帮我看看这个函数有没有性能问题", CFG()), null);
104
+ assert.equal(evaluateShortCircuit("what is the weather today", CFG()), null);
118
105
  });
@@ -21,8 +21,8 @@ import type { ShortCircuitConfig } from "./types.ts";
21
21
 
22
22
  /** Result of a successful short-circuit. */
23
23
  export interface ShortCircuitResult {
24
- /** Short human-readable reason, surfaced in the status bar. */
25
- reasoning: string;
24
+ /** Short human-readable reason, surfaced in the status bar. */
25
+ reasoning: string;
26
26
  }
27
27
 
28
28
  /**
@@ -33,20 +33,88 @@ export interface ShortCircuitResult {
33
33
  * would silently drop routing. User-supplied `ackPhrases` are merged on top.
34
34
  */
35
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
- "", "응", "응응", "계속", "알겠어", "알겠음", "좋아", "그래", "오키",
36
+ // 中文
37
+ "好的",
38
+ "",
39
+ "",
40
+ "嗯嗯",
41
+ "嗯呢",
42
+ "",
43
+ "可以",
44
+ "",
45
+ "对的",
46
+ "",
47
+ "是的",
48
+ "继续",
49
+ "继续吧",
50
+ "往下",
51
+ "没问题",
52
+ "明白",
53
+ "收到",
54
+ "了解",
55
+ "知道了",
56
+ "晓得",
57
+ "中",
58
+ "成",
59
+ "行吧",
60
+ "可以的",
61
+ "好的呀",
62
+ "好嘞",
63
+ "妥",
64
+ "妥了",
65
+ "嗯哼",
66
+ // English
67
+ "ok",
68
+ "okay",
69
+ "oki",
70
+ "okie",
71
+ "okk",
72
+ "k",
73
+ "sure",
74
+ "yes",
75
+ "yeah",
76
+ "yep",
77
+ "yup",
78
+ "continue",
79
+ "go",
80
+ "ahead",
81
+ "go ahead",
82
+ "sounds good",
83
+ "got it",
84
+ "understood",
85
+ "will do",
86
+ "agreed",
87
+ "roger",
88
+ "proceed",
89
+ "ack",
90
+ "acknowledged",
91
+ "fine",
92
+ // 日本語
93
+ "はい",
94
+ "うん",
95
+ "おk",
96
+ "続けて",
97
+ "了解",
98
+ "承知",
99
+ "わかった",
100
+ "分かった",
101
+ "ええ",
102
+ "継続",
103
+ "進めて",
104
+ "いいよ",
105
+ "いいです",
106
+ "オッケー",
107
+ "おけ",
108
+ // 한국어
109
+ "네",
110
+ "응",
111
+ "응응",
112
+ "계속",
113
+ "알겠어",
114
+ "알겠음",
115
+ "좋아",
116
+ "그래",
117
+ "오키",
50
118
  ];
51
119
 
52
120
  /**
@@ -56,11 +124,11 @@ const DEFAULT_ACK_PHRASES: string[] = [
56
124
  * @internal — exported for testing.
57
125
  */
58
126
  export function normalizeAckPrompt(input: string): string {
59
- return input
60
- .trim()
61
- .toLowerCase()
62
- .replace(/^[。!?.!?,,、~~…·\s]+/, "")
63
- .replace(/[。!?.!?,,、~~…·\s]+$/, "");
127
+ return input
128
+ .trim()
129
+ .toLowerCase()
130
+ .replace(/^[。!?.!?,,、~~…·\s]+/, "")
131
+ .replace(/[。!?.!?,,、~~…·\s]+$/, "");
64
132
  }
65
133
 
66
134
  /**
@@ -69,13 +137,13 @@ export function normalizeAckPrompt(input: string): string {
69
137
  * @internal — exported for testing.
70
138
  */
71
139
  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;
140
+ const all = [...DEFAULT_ACK_PHRASES, ...extra];
141
+ const set = new Set<string>();
142
+ for (const phrase of all) {
143
+ const n = normalizeAckPrompt(phrase);
144
+ if (n.length > 0) set.add(n);
145
+ }
146
+ return set;
79
147
  }
80
148
 
81
149
  /**
@@ -87,16 +155,16 @@ export function buildAckSet(extra: string[]): Set<string> {
87
155
  * @param config - Short-circuit tuning
88
156
  */
89
157
  export function evaluateShortCircuit(
90
- prompt: string,
91
- config: ShortCircuitConfig,
158
+ prompt: string,
159
+ config: ShortCircuitConfig,
92
160
  ): ShortCircuitResult | null {
93
- if (!config.trivialAck) return null;
161
+ if (!config.trivialAck) return null;
94
162
 
95
- const normalized = normalizeAckPrompt(prompt);
96
- if (normalized.length === 0 || normalized.length > config.maxAckLength) {
97
- return null;
98
- }
163
+ const normalized = normalizeAckPrompt(prompt);
164
+ if (normalized.length === 0 || normalized.length > config.maxAckLength) {
165
+ return null;
166
+ }
99
167
 
100
- const ackSet = buildAckSet(config.ackPhrases);
101
- return ackSet.has(normalized) ? { reasoning: "trivial ack" } : null;
168
+ const ackSet = buildAckSet(config.ackPhrases);
169
+ return ackSet.has(normalized) ? { reasoning: "trivial ack" } : null;
102
170
  }