@d3ara1n/pi-scout 1.0.2 → 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/README.md +27 -4
- package/package.json +1 -1
- package/src/config.ts +36 -40
- package/src/index.ts +332 -264
- package/src/model-switch.ts +17 -17
- package/src/scout-prompt.ts +64 -60
- package/src/short-circuit.test.ts +105 -0
- package/src/short-circuit.ts +170 -0
- package/src/side-agent.ts +65 -65
- package/src/skill-inject.ts +41 -38
- package/src/types.ts +34 -1
package/src/index.ts
CHANGED
|
@@ -20,276 +20,344 @@ import { callSideAgent } from "./side-agent.ts";
|
|
|
20
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
|
+
import { evaluateShortCircuit } from "./short-circuit.ts";
|
|
23
24
|
|
|
24
25
|
const STATUS_KEY = "scout";
|
|
25
26
|
|
|
26
27
|
/** Build a one-line status summary from a scout decision. */
|
|
27
28
|
function formatDecisionStatus(decision: ScoutDecision, theme: any): string {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
if (decision.source === "short-circuit") {
|
|
30
|
+
return (
|
|
31
|
+
theme.fg("success", "✓ scout:") + " " + theme.fg("dim", `(skipped) ${decision.reasoning}`)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parts: string[] = [];
|
|
36
|
+
|
|
37
|
+
if (decision.skills.length > 0) {
|
|
38
|
+
const names =
|
|
39
|
+
decision.skills.length <= 3
|
|
40
|
+
? decision.skills.join(", ")
|
|
41
|
+
: `${decision.skills.slice(0, 2).join(", ")} +${decision.skills.length - 2}`;
|
|
42
|
+
parts.push(theme.fg("accent", `skills: ${names}`));
|
|
43
|
+
}
|
|
44
|
+
if (decision.role) {
|
|
45
|
+
parts.push(theme.fg("warning", `→ ${decision.role}`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (parts.length === 0) {
|
|
49
|
+
return theme.fg("dim", "✓ scout: no changes");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return theme.fg("success", "✓ scout:") + " " + parts.join(" | ");
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
export default function scoutExtension(pi: ExtensionAPI) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
56
|
+
let config: ScoutConfig = DEFAULT_CONFIG;
|
|
57
|
+
let lastDecision: ScoutDecision | undefined;
|
|
58
|
+
|
|
59
|
+
/** Cache of previous turn context for better routing decisions. */
|
|
60
|
+
let prevTurn: { userPrompt: string; assistantSummary: string } | undefined;
|
|
61
|
+
|
|
62
|
+
function tryGetRolesApi(ctx: ExtensionContext): ModelRolesAPI | undefined {
|
|
63
|
+
try {
|
|
64
|
+
return getModelRolesAPI();
|
|
65
|
+
} catch {
|
|
66
|
+
ctx.ui.notify(
|
|
67
|
+
"pi-model-roles not loaded. Ensure @d3ara1n/pi-model-roles is in extensions and restart.",
|
|
68
|
+
"error",
|
|
69
|
+
);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── /scout — show status ────────────────────────────────────────
|
|
75
|
+
pi.registerCommand("scout", {
|
|
76
|
+
description: "Show scout status and last decision",
|
|
77
|
+
handler: async (_args, ctx) => {
|
|
78
|
+
const rolesApi = tryGetRolesApi(ctx);
|
|
79
|
+
const sideRole = rolesApi?.getRole(config.sideAgentRole);
|
|
80
|
+
const lines = [
|
|
81
|
+
`Scout: ${config.enabled ? "enabled" : "disabled"}`,
|
|
82
|
+
`Side agent role: ${config.sideAgentRole} (${sideRole?.model ?? "current model"})`,
|
|
83
|
+
``,
|
|
84
|
+
`Modules:`,
|
|
85
|
+
` skill-router: ${config.modules.skillRouter ? "on" : "off"}`,
|
|
86
|
+
` model-router: ${config.modules.modelRouter ? "on" : "off"}`,
|
|
87
|
+
` short-circuit: ${config.modules.shortCircuit ? "on" : "off"}`,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
if (lastDecision) {
|
|
91
|
+
lines.push(``);
|
|
92
|
+
lines.push(`Last decision:`);
|
|
93
|
+
lines.push(
|
|
94
|
+
` skills: ${lastDecision.skills.length > 0 ? lastDecision.skills.join(", ") : "(none)"}`,
|
|
95
|
+
);
|
|
96
|
+
lines.push(` role: ${lastDecision.role ?? "(no change)"}`);
|
|
97
|
+
lines.push(` reasoning: ${lastDecision.reasoning}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── /scout:skill-router on/off ──────────────────────────────────
|
|
105
|
+
pi.registerCommand("scout:skill-router", {
|
|
106
|
+
description: "Toggle skill-router module (on/off)",
|
|
107
|
+
handler: async (args, ctx) => {
|
|
108
|
+
const value = (args ?? "").trim().toLowerCase();
|
|
109
|
+
if (value === "on") {
|
|
110
|
+
config.modules.skillRouter = true;
|
|
111
|
+
ctx.ui.notify("Scout: skill-router enabled", "info");
|
|
112
|
+
} else if (value === "off") {
|
|
113
|
+
config.modules.skillRouter = false;
|
|
114
|
+
ctx.ui.notify("Scout: skill-router disabled", "info");
|
|
115
|
+
} else {
|
|
116
|
+
ctx.ui.notify("Usage: /scout:skill-router on|off", "info");
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── /scout:model-router on/off ──────────────────────────────────
|
|
122
|
+
pi.registerCommand("scout:model-router", {
|
|
123
|
+
description: "Toggle model-router module (on/off)",
|
|
124
|
+
handler: async (args, ctx) => {
|
|
125
|
+
const value = (args ?? "").trim().toLowerCase();
|
|
126
|
+
if (value === "on") {
|
|
127
|
+
config.modules.modelRouter = true;
|
|
128
|
+
ctx.ui.notify("Scout: model-router enabled", "info");
|
|
129
|
+
} else if (value === "off") {
|
|
130
|
+
config.modules.modelRouter = false;
|
|
131
|
+
ctx.ui.notify("Scout: model-router disabled", "info");
|
|
132
|
+
} else {
|
|
133
|
+
ctx.ui.notify("Usage: /scout:model-router on|off", "info");
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── /scout:short-circuit on/off ─────────────────────────────────
|
|
139
|
+
pi.registerCommand("scout:short-circuit", {
|
|
140
|
+
description: "Toggle short-circuit module (on/off)",
|
|
141
|
+
handler: async (args, ctx) => {
|
|
142
|
+
const value = (args ?? "").trim().toLowerCase();
|
|
143
|
+
if (value === "on") {
|
|
144
|
+
config.modules.shortCircuit = true;
|
|
145
|
+
ctx.ui.notify("Scout: short-circuit enabled", "info");
|
|
146
|
+
} else if (value === "off") {
|
|
147
|
+
config.modules.shortCircuit = false;
|
|
148
|
+
ctx.ui.notify("Scout: short-circuit disabled", "info");
|
|
149
|
+
} else {
|
|
150
|
+
ctx.ui.notify("Usage: /scout:short-circuit on|off", "info");
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── list_skills tool ───────────────────────────────────────────
|
|
156
|
+
let cachedAllSkills: Array<{ name: string; description: string; filePath: string }> = [];
|
|
157
|
+
|
|
158
|
+
pi.registerTool({
|
|
159
|
+
name: "list_skills",
|
|
160
|
+
label: "List all skills",
|
|
161
|
+
description:
|
|
162
|
+
"List all available skills with name and description. Use this when the user asks what skills are installed or you need to discover skills beyond those currently active.",
|
|
163
|
+
parameters: { type: "object", properties: {}, required: [] } as any,
|
|
164
|
+
async execute() {
|
|
165
|
+
if (cachedAllSkills.length === 0) {
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text", text: "No skills available." }],
|
|
168
|
+
details: undefined as any,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const lines = cachedAllSkills.map((s) => `- **${s.name}**: ${s.description}`);
|
|
172
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: undefined as any };
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── session_start: load config ──────────────────────────────────
|
|
177
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
178
|
+
config = loadScoutConfig(ctx.cwd);
|
|
179
|
+
resetSkillCache();
|
|
180
|
+
prevTurn = undefined;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ── turn_end: cache assistant response for next turn's context ──
|
|
184
|
+
pi.on("turn_end", async (event) => {
|
|
185
|
+
const msg = event.message;
|
|
186
|
+
if (msg.role !== "assistant") return;
|
|
187
|
+
|
|
188
|
+
// Extract text content, skip thinking blocks and tool calls
|
|
189
|
+
const textParts: string[] = [];
|
|
190
|
+
for (const block of msg.content) {
|
|
191
|
+
if ("type" in block && block.type === "text" && "text" in block) {
|
|
192
|
+
textParts.push(block.text);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const fullText = textParts.join("");
|
|
196
|
+
if (!fullText.trim()) return;
|
|
197
|
+
|
|
198
|
+
// Truncate to avoid bloating the side agent context
|
|
199
|
+
const MAX_SUMMARY = 500;
|
|
200
|
+
const assistantSummary =
|
|
201
|
+
fullText.length > MAX_SUMMARY ? fullText.slice(0, MAX_SUMMARY) + "..." : fullText;
|
|
202
|
+
|
|
203
|
+
// before_agent_start already created/updated prevTurn with the user prompt.
|
|
204
|
+
// We just fill in the assistant summary here.
|
|
205
|
+
if (prevTurn) {
|
|
206
|
+
prevTurn.assistantSummary = assistantSummary;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── before_agent_start: core scout logic ────────────────────────
|
|
211
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
212
|
+
if (!config.enabled) return;
|
|
213
|
+
if (!config.modules.skillRouter && !config.modules.modelRouter) return;
|
|
214
|
+
|
|
215
|
+
let rolesApi: ModelRolesAPI;
|
|
216
|
+
try {
|
|
217
|
+
rolesApi = getModelRolesAPI();
|
|
218
|
+
} catch {
|
|
219
|
+
console.warn("[pi-scout] pi-model-roles not initialized — skipping scout");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const theme = ctx.ui.theme;
|
|
224
|
+
|
|
225
|
+
// Skills available this turn — used by both the short-circuit layer
|
|
226
|
+
// and the side-agent path.
|
|
227
|
+
const skills = event.systemPromptOptions?.skills ?? [];
|
|
228
|
+
if (cachedAllSkills.length === 0 && skills.length > 0) {
|
|
229
|
+
cachedAllSkills = skills.map((s: any) => ({
|
|
230
|
+
name: s.name,
|
|
231
|
+
description: s.description ?? "",
|
|
232
|
+
filePath: s.filePath,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
const skillEntries = skills.map((s: any) => ({
|
|
236
|
+
name: s.name,
|
|
237
|
+
description: s.description ?? "",
|
|
238
|
+
filePath: s.filePath,
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
// ── Short-circuit layer ───────────────────────────────────
|
|
242
|
+
// Skip the side model on trivial acknowledgments. A trivial ack means
|
|
243
|
+
// "no skills, don't switch models" — both answers are certain, so this
|
|
244
|
+
// is safe even with model-router on. Runs before model resolution to
|
|
245
|
+
// save that cost too.
|
|
246
|
+
if (config.modules.shortCircuit) {
|
|
247
|
+
const skip = evaluateShortCircuit(event.prompt, config.shortCircuit);
|
|
248
|
+
if (skip) {
|
|
249
|
+
const decision: ScoutDecision = {
|
|
250
|
+
skills: [],
|
|
251
|
+
role: null,
|
|
252
|
+
reasoning: skip.reasoning,
|
|
253
|
+
source: "short-circuit",
|
|
254
|
+
};
|
|
255
|
+
lastDecision = decision;
|
|
256
|
+
|
|
257
|
+
let systemPrompt = event.systemPrompt;
|
|
258
|
+
if (config.modules.skillRouter) {
|
|
259
|
+
systemPrompt = filterSkillsBlock(systemPrompt, [], skillEntries);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Keep prevTurn current so the next turn still has context.
|
|
263
|
+
prevTurn = { userPrompt: event.prompt, assistantSummary: "" };
|
|
264
|
+
|
|
265
|
+
ctx.ui.setStatus(STATUS_KEY, formatDecisionStatus(decision, theme));
|
|
266
|
+
if (systemPrompt !== event.systemPrompt) return { systemPrompt };
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Show "Scouting..." indicator
|
|
272
|
+
ctx.ui.setStatus(STATUS_KEY, theme.fg("accent", "◎") + theme.fg("dim", " Scouting..."));
|
|
273
|
+
|
|
274
|
+
// Resolve side agent model (sync — auth is resolved inside complete())
|
|
275
|
+
const sideResolved = rolesApi.resolveRole(config.sideAgentRole);
|
|
276
|
+
if (!sideResolved.model) {
|
|
277
|
+
ctx.ui.setStatus(STATUS_KEY, theme.fg("warning", "◎ scout: side model unavailable"));
|
|
278
|
+
console.warn(`[pi-scout] Side agent role "${config.sideAgentRole}" not available — skipping`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Update status: resolving
|
|
283
|
+
ctx.ui.setStatus(
|
|
284
|
+
STATUS_KEY,
|
|
285
|
+
theme.fg("accent", "◎") +
|
|
286
|
+
theme.fg("dim", ` Scouting via ${sideResolved.model.provider}/${sideResolved.model.id}...`),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// 1. Build the skills list for the side agent prompt
|
|
290
|
+
const skillsList = skills
|
|
291
|
+
.map((s: any) => `- ${s.name}: ${s.description ?? "(no description)"}`)
|
|
292
|
+
.join("\n");
|
|
293
|
+
|
|
294
|
+
// 2. Determine current role (use getCurrentRole: it recognizes the
|
|
295
|
+
// default role even when model=null, so the router has a real
|
|
296
|
+
// baseline instead of an opaque "unknown".)
|
|
297
|
+
const currentModel = ctx.model;
|
|
298
|
+
const currentRole = currentModel
|
|
299
|
+
? (rolesApi.getCurrentRole(`${currentModel.provider}/${currentModel.id}`) ?? "unknown")
|
|
300
|
+
: "unknown";
|
|
301
|
+
|
|
302
|
+
// 3. Build roles list
|
|
303
|
+
const visibleRoles = rolesApi.getVisibleRoles();
|
|
304
|
+
const rolesList = Object.entries(visibleRoles)
|
|
305
|
+
.map(
|
|
306
|
+
([name, cfg]: [string, any]) =>
|
|
307
|
+
`- ${name}: ${cfg.description ?? "(no description)"}${cfg.model ? ` (model: ${cfg.model})` : " (current model)"}`,
|
|
308
|
+
)
|
|
309
|
+
.join("\n");
|
|
310
|
+
|
|
311
|
+
// 4. Build user message with conversation context
|
|
312
|
+
const prevTurnContext = prevTurn?.assistantSummary
|
|
313
|
+
? { userPrompt: prevTurn.userPrompt, assistantSummary: prevTurn.assistantSummary }
|
|
314
|
+
: undefined;
|
|
315
|
+
const userMessage = buildScoutUserMessage(event.prompt, currentRole, prevTurnContext);
|
|
316
|
+
|
|
317
|
+
// Reset prevTurn for the current turn — user prompt now, assistant filled by turn_end
|
|
318
|
+
prevTurn = { userPrompt: event.prompt, assistantSummary: "" };
|
|
319
|
+
|
|
320
|
+
// 5. Call side agent
|
|
321
|
+
const scoutSystemPrompt = buildScoutSystemPrompt(config, skillsList, rolesList);
|
|
322
|
+
const decision = await callSideAgent(
|
|
323
|
+
rolesApi,
|
|
324
|
+
config.sideAgentRole,
|
|
325
|
+
scoutSystemPrompt,
|
|
326
|
+
userMessage,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// Enforce module toggles at the application layer, regardless of what
|
|
330
|
+
// the side agent returned. A disabled module's decision field is zeroed
|
|
331
|
+
// so the status bar, /scout command, and apply logic all see clean
|
|
332
|
+
// values (no misleading "→ fast" when model-router is off).
|
|
333
|
+
if (!config.modules.modelRouter) decision.role = null;
|
|
334
|
+
if (!config.modules.skillRouter) decision.skills = [];
|
|
335
|
+
lastDecision = decision;
|
|
336
|
+
|
|
337
|
+
let systemPrompt = event.systemPrompt;
|
|
338
|
+
|
|
339
|
+
// 6. skill-router: filter skills XML to only selected ones
|
|
340
|
+
if (config.modules.skillRouter) {
|
|
341
|
+
systemPrompt = filterSkillsBlock(systemPrompt, decision.skills, skillEntries);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 7. model-router: switch model if side agent recommends a different role
|
|
345
|
+
if (config.modules.modelRouter && decision.role && decision.role !== currentRole) {
|
|
346
|
+
const switched = await switchToRole(pi, decision.role, rolesApi);
|
|
347
|
+
if (switched) {
|
|
348
|
+
const newModel = await rolesApi.resolveRoleAsync(decision.role);
|
|
349
|
+
if (newModel?.model) {
|
|
350
|
+
systemPrompt += `\n\n<current_model>${newModel.model.provider}/${newModel.model.id} (role: ${decision.role})</current_model>`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 8. Show result in status bar
|
|
356
|
+
ctx.ui.setStatus(STATUS_KEY, formatDecisionStatus(decision, theme));
|
|
357
|
+
|
|
358
|
+
// 9. Return modified system prompt
|
|
359
|
+
if (systemPrompt !== event.systemPrompt) {
|
|
360
|
+
return { systemPrompt };
|
|
361
|
+
}
|
|
362
|
+
});
|
|
295
363
|
}
|