@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/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 using pi-ai's complete() function
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 { complete } from "@earendil-works/pi-ai";
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
- systemPrompt?: string;
14
- messages: Array<{ role: "user"; content: string; timestamp: number }>;
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 sideModel - The Model instance to use (from pi-model-roles "side" role)
21
- * @param apiKey - API key for the side model
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
- sideModel: any,
29
- apiKey: string | undefined,
30
- headers: Record<string, string> | undefined,
31
- systemPrompt: string,
32
- userMessage: string,
27
+ rolesApi: ModelRolesAPI,
28
+ roleName: string,
29
+ systemPrompt: string,
30
+ userMessage: string,
33
31
  ): Promise<ScoutDecision> {
34
- const fallback: ScoutDecision = { skills: [], role: null, reasoning: "side agent error", source: "side-agent" };
32
+ const fallback: ScoutDecision = {
33
+ skills: [],
34
+ role: null,
35
+ reasoning: "side agent error",
36
+ source: "side-agent",
37
+ };
35
38
 
36
- const context: SideAgentContext = {
37
- systemPrompt,
38
- messages: [
39
- {
40
- role: "user",
41
- content: userMessage,
42
- timestamp: Date.now(),
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
- const options: Record<string, any> = {
48
- maxTokens: 256,
49
- cacheRetention: "short",
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
- if (apiKey) options.apiKey = apiKey;
53
- if (headers) options.headers = headers;
54
-
55
- try {
56
- const result = await complete(sideModel, context, options);
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
- const fallback: ScoutDecision = { skills: [], role: null, reasoning: "parse error", source: "side-agent" };
72
+ const fallback: ScoutDecision = {
73
+ skills: [],
74
+ role: null,
75
+ reasoning: "parse error",
76
+ source: "side-agent",
77
+ };
75
78
 
76
- // Strip markdown code fences if present
77
- let text = raw.trim();
78
- if (text.startsWith("```")) {
79
- text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
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
- try {
83
- const parsed = JSON.parse(text);
85
+ try {
86
+ const parsed = JSON.parse(text);
84
87
 
85
- return {
86
- skills: Array.isArray(parsed.skills)
87
- ? parsed.skills.filter((s: any) => typeof s === "string")
88
- : [],
89
- role: typeof parsed.role === "string" && parsed.role !== "null"
90
- ? parsed.role
91
- : null,
92
- reasoning: typeof parsed.reasoning === "string"
93
- ? parsed.reasoning
94
- : "no reasoning provided",
95
- source: "side-agent",
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
  }
@@ -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 = /\n\nThe following skills provide specialized instructions[\s\S]*?<\/available_skills>/;
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
- shownSkills = new Set();
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
- systemPrompt: string,
37
- selectedSkills: string[],
38
- allSkills: Array<{ name: string; description: string; filePath: string }>,
37
+ systemPrompt: string,
38
+ selectedSkills: string[],
39
+ allSkills: Array<{ name: string; description: string; filePath: string }>,
39
40
  ): string {
40
- if (selectedSkills.length === 0) {
41
- return systemPrompt.replace(SKILLS_SECTION_RE, "");
42
- }
41
+ if (selectedSkills.length === 0) {
42
+ return systemPrompt.replace(SKILLS_SECTION_RE, "");
43
+ }
43
44
 
44
- const skillMap = new Map(allSkills.map((s) => [s.name, s]));
45
- const entries: string[] = [];
46
- const newlyShown: string[] = [];
45
+ const skillMap = new Map(allSkills.map((s) => [s.name, s]));
46
+ const entries: string[] = [];
47
+ const newlyShown: string[] = [];
47
48
 
48
- for (const name of selectedSkills) {
49
- const skill = skillMap.get(name);
50
- if (!skill) continue;
49
+ for (const name of selectedSkills) {
50
+ const skill = skillMap.get(name);
51
+ if (!skill) continue;
51
52
 
52
- if (shownSkills.has(name)) {
53
- // Already introduced — compact form
54
- entries.push(` <skill name="${esc(skill.name)}" location="${esc(skill.filePath)}" />`);
55
- } else {
56
- // First time — include description
57
- entries.push(` <skill name="${esc(skill.name)}" location="${esc(skill.filePath)}">${esc(skill.description)}</skill>`);
58
- newlyShown.push(name);
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
- if (entries.length === 0) {
63
- return systemPrompt.replace(SKILLS_SECTION_RE, "");
64
- }
65
+ if (entries.length === 0) {
66
+ return systemPrompt.replace(SKILLS_SECTION_RE, "");
67
+ }
65
68
 
66
- // Update cache
67
- for (const name of newlyShown) {
68
- shownSkills.add(name);
69
- }
69
+ // Update cache
70
+ for (const name of newlyShown) {
71
+ shownSkills.add(name);
72
+ }
70
73
 
71
- const compact = `\n\nActive skills (use \`read\` to load a skill's file):\n<available_skills>\n${entries.join("\n")}\n</available_skills>`;
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
- return systemPrompt.replace(SKILLS_SECTION_RE, compact);
76
+ return systemPrompt.replace(SKILLS_SECTION_RE, compact);
74
77
  }
75
78
 
76
79
  function esc(str: string): string {
77
- return str
78
- .replace(/&/g, "&amp;")
79
- .replace(/</g, "&lt;")
80
- .replace(/>/g, "&gt;")
81
- .replace(/"/g, "&quot;")
82
- .replace(/'/g, "&apos;");
80
+ return str
81
+ .replace(/&/g, "&amp;")
82
+ .replace(/</g, "&lt;")
83
+ .replace(/>/g, "&gt;")
84
+ .replace(/"/g, "&quot;")
85
+ .replace(/'/g, "&apos;");
83
86
  }