@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/side-agent.ts
CHANGED
|
@@ -1,69 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Side agent invocation logic.
|
|
3
3
|
*
|
|
4
|
-
* Calls the side agent model
|
|
4
|
+
* Calls the side agent via model-roles' complete() (auth resolved internally)
|
|
5
5
|
* and parses the JSON decision response.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import type { ModelRolesAPI } from "@d3ara1n/pi-model-roles";
|
|
9
9
|
import type { ScoutDecision } from "./types.ts";
|
|
10
10
|
|
|
11
11
|
/** Minimal type for side agent context — matches pi-ai's Context interface. */
|
|
12
12
|
interface SideAgentContext {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
systemPrompt?: string;
|
|
14
|
+
messages: Array<{ role: "user"; content: string; timestamp: number }>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Call the side agent and return its decision.
|
|
19
19
|
*
|
|
20
|
-
* @param
|
|
21
|
-
* @param
|
|
22
|
-
* @param headers - Custom headers for the side model
|
|
20
|
+
* @param rolesApi - ModelRolesAPI (provides complete() with auth resolved internally)
|
|
21
|
+
* @param roleName - Role name whose model + auth to use (from scout config)
|
|
23
22
|
* @param systemPrompt - Scout system prompt (includes skills/roles for cache friendliness)
|
|
24
23
|
* @param userMessage - Fully assembled user message (includes context, current role, user prompt)
|
|
25
24
|
* @returns Parsed ScoutDecision, or a safe fallback on error
|
|
26
25
|
*/
|
|
27
26
|
export async function callSideAgent(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
userMessage: string,
|
|
27
|
+
rolesApi: ModelRolesAPI,
|
|
28
|
+
roleName: string,
|
|
29
|
+
systemPrompt: string,
|
|
30
|
+
userMessage: string,
|
|
33
31
|
): Promise<ScoutDecision> {
|
|
34
|
-
|
|
32
|
+
const fallback: ScoutDecision = {
|
|
33
|
+
skills: [],
|
|
34
|
+
role: null,
|
|
35
|
+
reasoning: "side agent error",
|
|
36
|
+
source: "side-agent",
|
|
37
|
+
};
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
const context: SideAgentContext = {
|
|
40
|
+
systemPrompt,
|
|
41
|
+
messages: [
|
|
42
|
+
{
|
|
43
|
+
role: "user",
|
|
44
|
+
content: userMessage,
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
try {
|
|
51
|
+
const result = await rolesApi.complete(roleName, context, {
|
|
52
|
+
cacheRetention: "short",
|
|
53
|
+
});
|
|
54
|
+
const text =
|
|
55
|
+
result.content
|
|
56
|
+
?.filter((block: any) => block.type === "text")
|
|
57
|
+
?.map((block: any) => block.text)
|
|
58
|
+
?.join("") ?? "";
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const text = result.content
|
|
58
|
-
?.filter((block: any) => block.type === "text")
|
|
59
|
-
?.map((block: any) => block.text)
|
|
60
|
-
?.join("") ?? "";
|
|
61
|
-
|
|
62
|
-
return parseDecision(text);
|
|
63
|
-
} catch (err) {
|
|
64
|
-
console.warn("[pi-scout] Side agent call failed:", err);
|
|
65
|
-
return fallback;
|
|
66
|
-
}
|
|
60
|
+
return parseDecision(text);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.warn("[pi-scout] Side agent call failed:", err);
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
/**
|
|
@@ -71,31 +69,32 @@ export async function callSideAgent(
|
|
|
71
69
|
* Tolerant of markdown wrapping, extra whitespace, etc.
|
|
72
70
|
*/
|
|
73
71
|
function parseDecision(raw: string): ScoutDecision {
|
|
74
|
-
|
|
72
|
+
const fallback: ScoutDecision = {
|
|
73
|
+
skills: [],
|
|
74
|
+
role: null,
|
|
75
|
+
reasoning: "parse error",
|
|
76
|
+
source: "side-agent",
|
|
77
|
+
};
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// Strip markdown code fences if present
|
|
80
|
+
let text = raw.trim();
|
|
81
|
+
if (text.startsWith("```")) {
|
|
82
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
83
|
+
}
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(text);
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} catch {
|
|
98
|
-
console.warn("[pi-scout] Failed to parse side agent response:", raw.slice(0, 200));
|
|
99
|
-
return fallback;
|
|
100
|
-
}
|
|
88
|
+
return {
|
|
89
|
+
skills: Array.isArray(parsed.skills)
|
|
90
|
+
? parsed.skills.filter((s: any) => typeof s === "string")
|
|
91
|
+
: [],
|
|
92
|
+
role: typeof parsed.role === "string" && parsed.role !== "null" ? parsed.role : null,
|
|
93
|
+
reasoning: typeof parsed.reasoning === "string" ? parsed.reasoning : "no reasoning provided",
|
|
94
|
+
source: "side-agent",
|
|
95
|
+
};
|
|
96
|
+
} catch {
|
|
97
|
+
console.warn("[pi-scout] Failed to parse side agent response:", raw.slice(0, 200));
|
|
98
|
+
return fallback;
|
|
99
|
+
}
|
|
101
100
|
}
|
package/src/skill-inject.ts
CHANGED
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/** Match pi's entire skills section: intro paragraph + XML block. */
|
|
13
|
-
const SKILLS_SECTION_RE =
|
|
13
|
+
const SKILLS_SECTION_RE =
|
|
14
|
+
/\n\nThe following skills provide specialized instructions[\s\S]*?<\/available_skills>/;
|
|
14
15
|
|
|
15
16
|
/** Track skill names already shown to the LLM in this session. */
|
|
16
17
|
let shownSkills: Set<string> = new Set();
|
|
17
18
|
|
|
18
19
|
/** Reset the cache — called on session_start. */
|
|
19
20
|
export function resetSkillCache(): void {
|
|
20
|
-
|
|
21
|
+
shownSkills = new Set();
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -33,51 +34,53 @@ export function resetSkillCache(): void {
|
|
|
33
34
|
* @returns Modified system prompt
|
|
34
35
|
*/
|
|
35
36
|
export function filterSkillsBlock(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
systemPrompt: string,
|
|
38
|
+
selectedSkills: string[],
|
|
39
|
+
allSkills: Array<{ name: string; description: string; filePath: string }>,
|
|
39
40
|
): string {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
if (selectedSkills.length === 0) {
|
|
42
|
+
return systemPrompt.replace(SKILLS_SECTION_RE, "");
|
|
43
|
+
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const skillMap = new Map(allSkills.map((s) => [s.name, s]));
|
|
46
|
+
const entries: string[] = [];
|
|
47
|
+
const newlyShown: string[] = [];
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
for (const name of selectedSkills) {
|
|
50
|
+
const skill = skillMap.get(name);
|
|
51
|
+
if (!skill) continue;
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
if (shownSkills.has(name)) {
|
|
54
|
+
// Already introduced — compact form
|
|
55
|
+
entries.push(` <skill name="${esc(skill.name)}" location="${esc(skill.filePath)}" />`);
|
|
56
|
+
} else {
|
|
57
|
+
// First time — include description
|
|
58
|
+
entries.push(
|
|
59
|
+
` <skill name="${esc(skill.name)}" location="${esc(skill.filePath)}">${esc(skill.description)}</skill>`,
|
|
60
|
+
);
|
|
61
|
+
newlyShown.push(name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
if (entries.length === 0) {
|
|
66
|
+
return systemPrompt.replace(SKILLS_SECTION_RE, "");
|
|
67
|
+
}
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// Update cache
|
|
70
|
+
for (const name of newlyShown) {
|
|
71
|
+
shownSkills.add(name);
|
|
72
|
+
}
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
const compact = `\n\nActive skills (use \`read\` to load a skill's file):\n<available_skills>\n${entries.join("\n")}\n</available_skills>`;
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
return systemPrompt.replace(SKILLS_SECTION_RE, compact);
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
function esc(str: string): string {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
return str
|
|
81
|
+
.replace(/&/g, "&")
|
|
82
|
+
.replace(/</g, "<")
|
|
83
|
+
.replace(/>/g, ">")
|
|
84
|
+
.replace(/"/g, """)
|
|
85
|
+
.replace(/'/g, "'");
|
|
83
86
|
}
|