@d3ara1n/pi-scout 0.1.1 → 1.0.0
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 +32 -6
- package/package.json +2 -1
- package/src/index.ts +54 -13
- package/src/scout-prompt.ts +46 -21
- package/src/side-agent.ts +6 -8
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,32 @@ Before each conversation turn, a cheap side agent model analyzes the user's prom
|
|
|
9
9
|
|
|
10
10
|
Both modules can be independently toggled on/off.
|
|
11
11
|
|
|
12
|
+
## Why model-router is disabled by default
|
|
13
|
+
|
|
14
|
+
Model-router switches the active model role based on task complexity, which can:
|
|
15
|
+
|
|
16
|
+
- **Break prompt caching**: Different models don't share cache, causing cache write costs on each switch
|
|
17
|
+
- **Increase API costs**: Frequent model switching adds ~118% overhead in typical workloads
|
|
18
|
+
- **Reduce performance**: Cache misses mean re-uploading system prompt and tools each time
|
|
19
|
+
|
|
20
|
+
We recommend keeping model-router disabled unless you specifically need it. Enable it via:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
/scout:model-router on # Temporary (current session)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or add to `settings.json` for persistent enablement:
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
{
|
|
30
|
+
"scout": {
|
|
31
|
+
"modules": {
|
|
32
|
+
"modelRouter": true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
12
38
|
## How it works
|
|
13
39
|
|
|
14
40
|
```
|
|
@@ -27,7 +53,7 @@ before_agent_start hook fires
|
|
|
27
53
|
## Requirements
|
|
28
54
|
|
|
29
55
|
- **@d3ara1n/pi-model-roles** must be installed and configured
|
|
30
|
-
- A cheap role must be defined in `modelRoles` configuration or use default(may be much more
|
|
56
|
+
- A cheap role must be defined in `modelRoles` configuration or use the default (may be much more expensive) instead
|
|
31
57
|
|
|
32
58
|
## Installation
|
|
33
59
|
|
|
@@ -47,7 +73,7 @@ Edit `~/.pi/agent/settings.json`:
|
|
|
47
73
|
"maxSelectedSkills": 5,
|
|
48
74
|
"modules": {
|
|
49
75
|
"skillRouter": true,
|
|
50
|
-
"modelRouter":
|
|
76
|
+
"modelRouter": false
|
|
51
77
|
}
|
|
52
78
|
}
|
|
53
79
|
}
|
|
@@ -56,18 +82,18 @@ Edit `~/.pi/agent/settings.json`:
|
|
|
56
82
|
| Field | Default | Description |
|
|
57
83
|
|-------|---------|-------------|
|
|
58
84
|
| `enabled` | `true` | Global on/off |
|
|
59
|
-
| `sideAgentRole` | `"
|
|
85
|
+
| `sideAgentRole` | `"utility"` | pi-model-roles role for the side agent |
|
|
60
86
|
| `maxSelectedSkills` | `5` | Max skills the side agent can select |
|
|
61
87
|
| `modules.skillRouter` | `true` | Enable/disable skill routing |
|
|
62
|
-
| `modules.modelRouter` | `
|
|
88
|
+
| `modules.modelRouter` | `false` | Enable/disable model routing (disabled by default to avoid cache inefficiency and extra costs) |
|
|
63
89
|
|
|
64
90
|
## Commands
|
|
65
91
|
|
|
66
92
|
| Command | Description |
|
|
67
93
|
|---------|-------------|
|
|
68
94
|
| `/scout` | Show scout status and last decision |
|
|
69
|
-
| `/scout
|
|
70
|
-
| `/scout
|
|
95
|
+
| `/scout:skill-router on/off` | Toggle skill-router module |
|
|
96
|
+
| `/scout:model-router on/off` | Toggle model-router module |
|
|
71
97
|
|
|
72
98
|
## Performance
|
|
73
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3ara1n/pi-scout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Per-turn side agent decision framework for pi — uses a cheap model to select skills and route models before each conversation turn",
|
|
5
6
|
"main": "src/index.ts",
|
|
6
7
|
"keywords": [
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { ScoutConfig, ScoutDecision } from "./types.ts";
|
|
|
17
17
|
import { DEFAULT_CONFIG } from "./types.ts";
|
|
18
18
|
import { loadScoutConfig } from "./config.ts";
|
|
19
19
|
import { callSideAgent } from "./side-agent.ts";
|
|
20
|
-
import { buildScoutSystemPrompt } from "./scout-prompt.ts";
|
|
20
|
+
import { buildScoutSystemPrompt, buildScoutUserMessage } from "./scout-prompt.ts";
|
|
21
21
|
import { filterSkillsBlock, resetSkillCache } from "./skill-inject.ts";
|
|
22
22
|
import { switchToRole } from "./model-switch.ts";
|
|
23
23
|
|
|
@@ -48,6 +48,9 @@ export default function scoutExtension(pi: ExtensionAPI) {
|
|
|
48
48
|
let config: ScoutConfig = DEFAULT_CONFIG;
|
|
49
49
|
let lastDecision: ScoutDecision | undefined;
|
|
50
50
|
|
|
51
|
+
/** Cache of previous turn context for better routing decisions. */
|
|
52
|
+
let prevTurn: { userPrompt: string; assistantSummary: string } | undefined;
|
|
53
|
+
|
|
51
54
|
function tryGetRolesApi(ctx: ExtensionContext): ModelRolesAPI | undefined {
|
|
52
55
|
try {
|
|
53
56
|
return getModelRolesAPI();
|
|
@@ -132,10 +135,10 @@ export default function scoutExtension(pi: ExtensionAPI) {
|
|
|
132
135
|
parameters: { type: "object", properties: {}, required: [] } as any,
|
|
133
136
|
async execute() {
|
|
134
137
|
if (cachedAllSkills.length === 0) {
|
|
135
|
-
return { content: [{ type: "text", text: "No skills available." }] };
|
|
138
|
+
return { content: [{ type: "text", text: "No skills available." }], details: undefined as any };
|
|
136
139
|
}
|
|
137
140
|
const lines = cachedAllSkills.map((s) => `- **${s.name}**: ${s.description}`);
|
|
138
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
141
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: undefined as any };
|
|
139
142
|
},
|
|
140
143
|
});
|
|
141
144
|
|
|
@@ -143,11 +146,35 @@ export default function scoutExtension(pi: ExtensionAPI) {
|
|
|
143
146
|
pi.on("session_start", async (_event, ctx) => {
|
|
144
147
|
config = loadScoutConfig(ctx.cwd);
|
|
145
148
|
resetSkillCache();
|
|
149
|
+
prevTurn = undefined;
|
|
146
150
|
});
|
|
147
151
|
|
|
148
|
-
// ──
|
|
149
|
-
pi.on("
|
|
150
|
-
|
|
152
|
+
// ── turn_end: cache assistant response for next turn's context ──
|
|
153
|
+
pi.on("turn_end", async (event) => {
|
|
154
|
+
const msg = event.message;
|
|
155
|
+
if (msg.role !== "assistant") return;
|
|
156
|
+
|
|
157
|
+
// Extract text content, skip thinking blocks and tool calls
|
|
158
|
+
const textParts: string[] = [];
|
|
159
|
+
for (const block of msg.content) {
|
|
160
|
+
if ("type" in block && block.type === "text" && "text" in block) {
|
|
161
|
+
textParts.push(block.text);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const fullText = textParts.join("");
|
|
165
|
+
if (!fullText.trim()) return;
|
|
166
|
+
|
|
167
|
+
// Truncate to avoid bloating the side agent context
|
|
168
|
+
const MAX_SUMMARY = 500;
|
|
169
|
+
const assistantSummary = fullText.length > MAX_SUMMARY
|
|
170
|
+
? fullText.slice(0, MAX_SUMMARY) + "..."
|
|
171
|
+
: fullText;
|
|
172
|
+
|
|
173
|
+
// before_agent_start already created/updated prevTurn with the user prompt.
|
|
174
|
+
// We just fill in the assistant summary here.
|
|
175
|
+
if (prevTurn) {
|
|
176
|
+
prevTurn.assistantSummary = assistantSummary;
|
|
177
|
+
}
|
|
151
178
|
});
|
|
152
179
|
|
|
153
180
|
// ── before_agent_start: core scout logic ────────────────────────
|
|
@@ -204,23 +231,37 @@ export default function scoutExtension(pi: ExtensionAPI) {
|
|
|
204
231
|
.map(([name, cfg]: [string, any]) => `- ${name}: ${cfg.description ?? "(no description)"}${cfg.model ? ` (model: ${cfg.model})` : " (current model)"}`)
|
|
205
232
|
.join("\n");
|
|
206
233
|
|
|
207
|
-
// 4.
|
|
234
|
+
// 4. Build user message with conversation context
|
|
235
|
+
const prevTurnContext = prevTurn?.assistantSummary
|
|
236
|
+
? { userPrompt: prevTurn.userPrompt, assistantSummary: prevTurn.assistantSummary }
|
|
237
|
+
: undefined;
|
|
238
|
+
const userMessage = buildScoutUserMessage(event.prompt, currentRole, prevTurnContext);
|
|
239
|
+
|
|
240
|
+
// Reset prevTurn for the current turn — user prompt now, assistant filled by turn_end
|
|
241
|
+
prevTurn = { userPrompt: event.prompt, assistantSummary: "" };
|
|
242
|
+
|
|
243
|
+
// 5. Call side agent
|
|
208
244
|
const scoutSystemPrompt = buildScoutSystemPrompt(config, skillsList, rolesList);
|
|
209
245
|
const decision = await callSideAgent(
|
|
210
246
|
sideResolved.model,
|
|
211
247
|
sideResolved.apiKey,
|
|
212
248
|
sideResolved.headers,
|
|
213
249
|
scoutSystemPrompt,
|
|
214
|
-
|
|
215
|
-
currentRole,
|
|
250
|
+
userMessage,
|
|
216
251
|
);
|
|
217
252
|
|
|
253
|
+
// Enforce module toggles at the application layer, regardless of what
|
|
254
|
+
// the side agent returned. A disabled module's decision field is zeroed
|
|
255
|
+
// so the status bar, /scout command, and apply logic all see clean
|
|
256
|
+
// values (no misleading "→ fast" when model-router is off).
|
|
257
|
+
if (!config.modules.modelRouter) decision.role = null;
|
|
258
|
+
if (!config.modules.skillRouter) decision.skills = [];
|
|
218
259
|
lastDecision = decision;
|
|
219
260
|
|
|
220
261
|
let systemPrompt = event.systemPrompt;
|
|
221
262
|
let switchedRole: string | undefined;
|
|
222
263
|
|
|
223
|
-
//
|
|
264
|
+
// 6. skill-router: filter skills XML to only selected ones
|
|
224
265
|
if (config.modules.skillRouter) {
|
|
225
266
|
systemPrompt = filterSkillsBlock(
|
|
226
267
|
systemPrompt,
|
|
@@ -229,7 +270,7 @@ export default function scoutExtension(pi: ExtensionAPI) {
|
|
|
229
270
|
);
|
|
230
271
|
}
|
|
231
272
|
|
|
232
|
-
//
|
|
273
|
+
// 7. model-router: switch model if side agent recommends a different role
|
|
233
274
|
if (config.modules.modelRouter && decision.role && decision.role !== currentRole) {
|
|
234
275
|
const switched = await switchToRole(pi, decision.role, rolesApi);
|
|
235
276
|
if (switched) {
|
|
@@ -241,10 +282,10 @@ export default function scoutExtension(pi: ExtensionAPI) {
|
|
|
241
282
|
}
|
|
242
283
|
}
|
|
243
284
|
|
|
244
|
-
//
|
|
285
|
+
// 8. Show result in status bar
|
|
245
286
|
ctx.ui.setStatus(STATUS_KEY, formatDecisionStatus(decision, theme));
|
|
246
287
|
|
|
247
|
-
//
|
|
288
|
+
// 9. Return modified system prompt
|
|
248
289
|
if (systemPrompt !== event.systemPrompt) {
|
|
249
290
|
return { systemPrompt };
|
|
250
291
|
}
|
package/src/scout-prompt.ts
CHANGED
|
@@ -1,24 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Side agent
|
|
2
|
+
* Side agent prompt construction — system prompt and user message.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ScoutConfig } from "./types.ts";
|
|
6
6
|
|
|
7
|
+
/** Previous turn context for better routing decisions. */
|
|
8
|
+
export interface PrevTurnContext {
|
|
9
|
+
userPrompt: string;
|
|
10
|
+
assistantSummary: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Build the user message for the side agent.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
15
|
+
*
|
|
16
|
+
* Includes previous turn context (when available) so the side agent can
|
|
17
|
+
* understand follow-up prompts like "continue", "change that", etc.
|
|
11
18
|
*/
|
|
12
19
|
export function buildScoutUserMessage(
|
|
13
20
|
userPrompt: string,
|
|
14
21
|
currentRole: string,
|
|
22
|
+
prevTurn?: PrevTurnContext,
|
|
15
23
|
): string {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const parts: string[] = [];
|
|
25
|
+
|
|
26
|
+
parts.push(`Current role: ${currentRole}`);
|
|
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
|
+
}
|
|
38
|
+
|
|
39
|
+
parts.push(``);
|
|
40
|
+
parts.push(`## Current User Prompt`);
|
|
41
|
+
parts.push(userPrompt);
|
|
42
|
+
|
|
43
|
+
return parts.join("\n");
|
|
22
44
|
}
|
|
23
45
|
|
|
24
46
|
/**
|
|
@@ -53,22 +75,25 @@ export function buildScoutSystemPrompt(
|
|
|
53
75
|
parts.push(`- "role" should be null if the current role is appropriate.`);
|
|
54
76
|
parts.push(`- Only suggest a role change when the task clearly benefits from a different model.`);
|
|
55
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").`);
|
|
56
79
|
|
|
57
|
-
|
|
58
|
-
|
|
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)");
|
|
59
90
|
}
|
|
60
91
|
|
|
61
|
-
if (
|
|
62
|
-
parts.push(
|
|
92
|
+
if (config.modules.modelRouter) {
|
|
93
|
+
parts.push(``);
|
|
94
|
+
parts.push(`## Available Roles`);
|
|
95
|
+
parts.push(rolesList);
|
|
63
96
|
}
|
|
64
97
|
|
|
65
|
-
parts.push(``);
|
|
66
|
-
parts.push(`## Available Skills`);
|
|
67
|
-
parts.push(skillsList || "(none)");
|
|
68
|
-
|
|
69
|
-
parts.push(``);
|
|
70
|
-
parts.push(`## Available Roles`);
|
|
71
|
-
parts.push(rolesList);
|
|
72
|
-
|
|
73
98
|
return parts.join("\n");
|
|
74
99
|
}
|
package/src/side-agent.ts
CHANGED
|
@@ -7,12 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import { complete } from "@earendil-works/pi-ai";
|
|
9
9
|
import type { ScoutDecision } from "./types.ts";
|
|
10
|
-
import { buildScoutUserMessage } from "./scout-prompt.ts";
|
|
11
10
|
|
|
12
|
-
/** Minimal type for side agent context —
|
|
11
|
+
/** Minimal type for side agent context — matches pi-ai's Context interface. */
|
|
13
12
|
interface SideAgentContext {
|
|
14
13
|
systemPrompt?: string;
|
|
15
|
-
messages: Array<{ role:
|
|
14
|
+
messages: Array<{ role: "user"; content: string; timestamp: number }>;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
/**
|
|
@@ -22,8 +21,7 @@ interface SideAgentContext {
|
|
|
22
21
|
* @param apiKey - API key for the side model
|
|
23
22
|
* @param headers - Custom headers for the side model
|
|
24
23
|
* @param systemPrompt - Scout system prompt (includes skills/roles for cache friendliness)
|
|
25
|
-
* @param
|
|
26
|
-
* @param currentRole - Current active role name
|
|
24
|
+
* @param userMessage - Fully assembled user message (includes context, current role, user prompt)
|
|
27
25
|
* @returns Parsed ScoutDecision, or a safe fallback on error
|
|
28
26
|
*/
|
|
29
27
|
export async function callSideAgent(
|
|
@@ -31,8 +29,7 @@ export async function callSideAgent(
|
|
|
31
29
|
apiKey: string | undefined,
|
|
32
30
|
headers: Record<string, string> | undefined,
|
|
33
31
|
systemPrompt: string,
|
|
34
|
-
|
|
35
|
-
currentRole: string,
|
|
32
|
+
userMessage: string,
|
|
36
33
|
): Promise<ScoutDecision> {
|
|
37
34
|
const fallback: ScoutDecision = { skills: [], role: null, reasoning: "side agent error" };
|
|
38
35
|
|
|
@@ -41,7 +38,8 @@ export async function callSideAgent(
|
|
|
41
38
|
messages: [
|
|
42
39
|
{
|
|
43
40
|
role: "user",
|
|
44
|
-
content:
|
|
41
|
+
content: userMessage,
|
|
42
|
+
timestamp: Date.now(),
|
|
45
43
|
},
|
|
46
44
|
],
|
|
47
45
|
};
|