@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.
- package/package.json +1 -1
- package/src/config.ts +36 -48
- package/src/index.ts +331 -320
- package/src/model-switch.ts +17 -17
- package/src/scout-prompt.ts +64 -60
- package/src/short-circuit.test.ts +46 -59
- package/src/short-circuit.ts +105 -37
- package/src/side-agent.ts +65 -66
- package/src/skill-inject.ts +41 -38
package/src/model-switch.ts
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
pi: ExtensionAPI,
|
|
16
|
+
roleName: string,
|
|
17
|
+
rolesApi: ModelRolesAPI,
|
|
18
18
|
): Promise<boolean> {
|
|
19
|
-
|
|
19
|
+
const resolved = await rolesApi.resolveRoleAsync(roleName);
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
if (resolved.config.thinking) {
|
|
33
|
+
pi.setThinkingLevel(resolved.config.thinking);
|
|
34
|
+
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
return true;
|
|
37
37
|
}
|
package/src/scout-prompt.ts
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
userPrompt: string,
|
|
21
|
+
currentRole: string,
|
|
22
|
+
prevTurn?: PrevTurnContext,
|
|
23
23
|
): string {
|
|
24
|
-
|
|
24
|
+
const parts: string[] = [];
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
parts.push(`Current role: ${currentRole}`);
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
parts.push(``);
|
|
40
|
+
parts.push(`## Current User Prompt`);
|
|
41
|
+
parts.push(userPrompt);
|
|
42
42
|
|
|
43
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
config: ScoutConfig,
|
|
56
|
+
skillsList: string,
|
|
57
|
+
rolesList: string,
|
|
58
58
|
): string {
|
|
59
|
-
|
|
59
|
+
const parts: string[] = [];
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
if (config.modules.modelRouter) {
|
|
97
|
+
parts.push(``);
|
|
98
|
+
parts.push(`## Available Roles`);
|
|
99
|
+
parts.push(rolesList);
|
|
100
|
+
}
|
|
97
101
|
|
|
98
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
29
|
+
assert.equal(normalizeAckPrompt("好的,那我们继续"), "好的,那我们继续");
|
|
34
30
|
});
|
|
35
31
|
|
|
36
32
|
test("normalizeAckPrompt lowercases", () => {
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
});
|
package/src/short-circuit.ts
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
91
|
-
|
|
158
|
+
prompt: string,
|
|
159
|
+
config: ShortCircuitConfig,
|
|
92
160
|
): ShortCircuitResult | null {
|
|
93
|
-
|
|
161
|
+
if (!config.trivialAck) return null;
|
|
94
162
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
163
|
+
const normalized = normalizeAckPrompt(prompt);
|
|
164
|
+
if (normalized.length === 0 || normalized.length > config.maxAckLength) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
99
167
|
|
|
100
|
-
|
|
101
|
-
|
|
168
|
+
const ackSet = buildAckSet(config.ackPhrases);
|
|
169
|
+
return ackSet.has(normalized) ? { reasoning: "trivial ack" } : null;
|
|
102
170
|
}
|