@juicesharp/rpiv-advisor 0.6.1 → 0.7.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/advisor.ts +104 -15
- package/package.json +1 -1
- package/prompts/advisor-system.txt +1 -1
package/advisor.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
type ExtensionAPI,
|
|
27
27
|
type ExtensionContext,
|
|
28
28
|
type SessionEntry,
|
|
29
|
-
|
|
29
|
+
type ToolInfo,
|
|
30
30
|
} from "@mariozechner/pi-coding-agent";
|
|
31
31
|
import type { SelectItem } from "@mariozechner/pi-tui";
|
|
32
32
|
import { Type } from "@sinclair/typebox";
|
|
@@ -136,6 +136,97 @@ export const ADVISOR_SYSTEM_PROMPT = readFileSync(
|
|
|
136
136
|
"utf-8",
|
|
137
137
|
).trimEnd();
|
|
138
138
|
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Inventory state + serializer — stable tool-inventory Message for cache parity
|
|
141
|
+
//
|
|
142
|
+
// globalThis-keyed to survive module re-import on /new, /fork, /resume (mirrors
|
|
143
|
+
// rpiv-btw/btw.ts:37, 87-98). Single-slot cache — the Pi tool registry is
|
|
144
|
+
// process-scoped, so per-session keying would be redundant. Cache invalidates
|
|
145
|
+
// only when the set of registered tool names changes.
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
const ADVISOR_STATE_KEY = Symbol.for("rpiv-advisor");
|
|
149
|
+
|
|
150
|
+
interface AdvisorState {
|
|
151
|
+
inventorySignature?: string;
|
|
152
|
+
inventoryMessage?: Message;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getAdvisorRuntimeState(): AdvisorState {
|
|
156
|
+
const g = globalThis as unknown as { [k: symbol]: AdvisorState | undefined };
|
|
157
|
+
let state = g[ADVISOR_STATE_KEY];
|
|
158
|
+
if (!state) {
|
|
159
|
+
state = {};
|
|
160
|
+
g[ADVISOR_STATE_KEY] = state;
|
|
161
|
+
}
|
|
162
|
+
return state;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Recursive key-sorted JSON serializer — matches JSON.stringify semantics
|
|
166
|
+
// (drops `undefined` in objects, emits `null` for `undefined` in arrays) but
|
|
167
|
+
// guarantees stable key ordering across V8 insertion-order variation. Required
|
|
168
|
+
// because nested TypeBox schemas may be authored in any order, and prompt
|
|
169
|
+
// caching is byte-sensitive.
|
|
170
|
+
function stableStringify(value: unknown): string {
|
|
171
|
+
if (value === null || typeof value !== "object") {
|
|
172
|
+
return JSON.stringify(value);
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
return `[${value.map((v) => (v === undefined ? "null" : stableStringify(v))).join(",")}]`;
|
|
176
|
+
}
|
|
177
|
+
const obj = value as Record<string, unknown>;
|
|
178
|
+
const entries: string[] = [];
|
|
179
|
+
for (const k of Object.keys(obj).sort()) {
|
|
180
|
+
const v = obj[k];
|
|
181
|
+
if (v === undefined) continue;
|
|
182
|
+
entries.push(`${JSON.stringify(k)}:${stableStringify(v)}`);
|
|
183
|
+
}
|
|
184
|
+
return `{${entries.join(",")}}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildInventoryBlock(tools: ToolInfo[]): string {
|
|
188
|
+
// Omit `sourceInfo` — its `path` field is install-location-dependent and
|
|
189
|
+
// would bust cache parity across machines/reinstalls.
|
|
190
|
+
return tools
|
|
191
|
+
.map((t) => `### ${t.name}\n${t.description}\n\nParameters: ${stableStringify(t.parameters)}`)
|
|
192
|
+
.join("\n\n---\n\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Strip the executor's in-flight advisor() toolCall from the tail assistant
|
|
196
|
+
// message. That call is what invoked *us* — there is no matching toolResult
|
|
197
|
+
// yet, and providers (Anthropic, GLM/zai, OpenAI) reject payloads with orphan
|
|
198
|
+
// toolCalls. Name-targeted to leave any other trailing toolCalls visible.
|
|
199
|
+
function stripInflightAdvisorCall(messages: Message[]): Message[] {
|
|
200
|
+
if (messages.length === 0) return messages;
|
|
201
|
+
const last = messages[messages.length - 1];
|
|
202
|
+
if (last.role !== "assistant") return messages;
|
|
203
|
+
const filtered = last.content.filter((c) => !(c.type === "toolCall" && c.name === ADVISOR_TOOL_NAME));
|
|
204
|
+
if (filtered.length === last.content.length) return messages;
|
|
205
|
+
if (filtered.length === 0) return messages.slice(0, -1);
|
|
206
|
+
return [...messages.slice(0, -1), { ...last, content: filtered }];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Returns `undefined` when the registry is empty (no extensions loaded) so
|
|
210
|
+
// callers can skip prepending an empty block that would still cost a cache unit.
|
|
211
|
+
export function getInventoryMessage(tools: ToolInfo[]): Message | undefined {
|
|
212
|
+
if (tools.length === 0) return undefined;
|
|
213
|
+
const sorted = [...tools].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
214
|
+
const signature = sorted.map((t) => t.name).join("|");
|
|
215
|
+
const state = getAdvisorRuntimeState();
|
|
216
|
+
if (state.inventorySignature === signature && state.inventoryMessage) {
|
|
217
|
+
return state.inventoryMessage;
|
|
218
|
+
}
|
|
219
|
+
const text = `## Available Executor Tools\n\n${buildInventoryBlock(sorted)}`;
|
|
220
|
+
const message: Message = {
|
|
221
|
+
role: "user",
|
|
222
|
+
content: [{ type: "text", text }],
|
|
223
|
+
timestamp: Date.now(),
|
|
224
|
+
};
|
|
225
|
+
state.inventorySignature = signature;
|
|
226
|
+
state.inventoryMessage = message;
|
|
227
|
+
return message;
|
|
228
|
+
}
|
|
229
|
+
|
|
139
230
|
// ---------------------------------------------------------------------------
|
|
140
231
|
// Module state — in-memory, resets each session
|
|
141
232
|
// ---------------------------------------------------------------------------
|
|
@@ -219,6 +310,7 @@ function buildErrorResult(
|
|
|
219
310
|
|
|
220
311
|
async function executeAdvisor(
|
|
221
312
|
ctx: ExtensionContext,
|
|
313
|
+
pi: ExtensionAPI,
|
|
222
314
|
signal: AbortSignal | undefined,
|
|
223
315
|
onUpdate: AgentToolUpdateCallback<AdvisorDetails> | undefined,
|
|
224
316
|
): Promise<AgentToolResult<AdvisorDetails>> {
|
|
@@ -237,22 +329,17 @@ async function executeAdvisor(
|
|
|
237
329
|
return buildErrorResult(advisorLabel, errNoApiKey(advisorLabel), errNoApiKeyDetail(advisor.provider));
|
|
238
330
|
}
|
|
239
331
|
|
|
332
|
+
// Live-read every call — advisor runs mid-turn so any message_end snapshot
|
|
333
|
+
// is always one turn stale. convertToLlm is pass-through for user/assistant/
|
|
334
|
+
// toolResult (messages.js:111-114), so element refs are stable across calls
|
|
335
|
+
// via the session store — content-stable output without a snapshot layer.
|
|
240
336
|
const branch = ctx.sessionManager.getBranch();
|
|
241
337
|
const agentMessages = branch
|
|
242
338
|
.filter((e): e is SessionEntry & { type: "message" } => e.type === "message")
|
|
243
339
|
.map((e) => e.message);
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
role: "user",
|
|
248
|
-
content: [
|
|
249
|
-
{
|
|
250
|
-
type: "text",
|
|
251
|
-
text: `## Conversation So Far\n\n${conversationText}`,
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
timestamp: Date.now(),
|
|
255
|
-
};
|
|
340
|
+
const branchMessages = stripInflightAdvisorCall(convertToLlm(agentMessages));
|
|
341
|
+
const inventoryMessage = getInventoryMessage(pi.getAllTools());
|
|
342
|
+
const messages: Message[] = inventoryMessage ? [inventoryMessage, ...branchMessages] : branchMessages;
|
|
256
343
|
|
|
257
344
|
onUpdate?.({
|
|
258
345
|
content: [{ type: "text", text: msgConsulting(advisorLabel, effort) }],
|
|
@@ -262,7 +349,9 @@ async function executeAdvisor(
|
|
|
262
349
|
try {
|
|
263
350
|
const response = await completeSimple(
|
|
264
351
|
advisor,
|
|
265
|
-
|
|
352
|
+
// `tools: []` reaffirms the "never calls tools" contract even when
|
|
353
|
+
// `messages` contains prior toolCall/toolResult blocks (btw.ts:235).
|
|
354
|
+
{ systemPrompt: ADVISOR_SYSTEM_PROMPT, messages, tools: [] },
|
|
266
355
|
{ apiKey: auth.apiKey, headers: auth.headers, signal, reasoning: effort },
|
|
267
356
|
);
|
|
268
357
|
|
|
@@ -362,7 +451,7 @@ export function registerAdvisorTool(pi: ExtensionAPI): void {
|
|
|
362
451
|
parameters: AdvisorParams,
|
|
363
452
|
|
|
364
453
|
async execute(_toolCallId, _params, signal, onUpdate, ctx) {
|
|
365
|
-
return executeAdvisor(ctx, signal, onUpdate);
|
|
454
|
+
return executeAdvisor(ctx, pi, signal, onUpdate);
|
|
366
455
|
},
|
|
367
456
|
});
|
|
368
457
|
}
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
You are an advisor model in an advisor-strategy pattern. An executor model is running a task end-to-end — calling tools, reading results, iterating toward a solution. When the executor hits a decision it cannot reasonably solve alone, it consults you for guidance.
|
|
1
|
+
You are an advisor model in an advisor-strategy pattern. An executor model is running a task end-to-end — calling tools, reading results, iterating toward a solution. When the executor hits a decision it cannot reasonably solve alone, it consults you for guidance. The executor's full tool inventory is prepended before the conversation so you can judge tool-choice correctness.
|
|
2
2
|
|
|
3
3
|
You read the shared conversation context and return ONE of:
|
|
4
4
|
- a plan (concrete next steps the executor should take),
|