/** * execute — the advisor side-call. Curates the executor's branch (inventory * prefix + tail massaging), invokes the advisor model via completeSimple with * no tools, and returns a structured tool result. Every result branch (success * / abort / error / empty) and the pre-call error paths funnel through * buildAdvisorResult so the envelope is built in exactly one place. */ import type { StopReason, Usage } from "@earendil-works/pi-ai"; import { completeSimple, type Message, type ThinkingLevel } from "@earendil-works/pi-ai"; import { type AgentToolResult, type AgentToolUpdateCallback, buildSessionContext, convertToLlm, type ExtensionAPI, type ExtensionContext, } from "@earendil-works/pi-coding-agent"; import { ensureUserTailForAdvisor, stripInflightAdvisorCall } from "./context.js"; import { getInventoryMessage } from "./inventory.js"; import { ERR_ABORTED_DETAIL, ERR_CALL_ABORTED, ERR_EMPTY_RESPONSE, ERR_EMPTY_RESPONSE_DETAIL, ERR_NO_MODEL, ERR_NO_MODEL_SELECTED, errCallFailed, errCallThrew, errMisconfigured, errNoApiKey, errNoApiKeyDetail, msgConsulting, } from "./messages.js"; import { ADVISOR_SYSTEM_PROMPT } from "./prompt.js"; import { getAdvisorEffort, getAdvisorModel } from "./state.js"; interface AdvisorDetails { advisorModel?: string; effort?: ThinkingLevel; usage?: Usage; stopReason?: StopReason; errorMessage?: string; } // Single result-envelope builder — every executeAdvisor branch and the pre-call // error paths funnel through here. `effort` is snapshotted once at executeAdvisor // entry and threaded through every call so the returned details.effort always // matches the value sent as `reasoning` to completeSimple, even if module-level // state is mutated during the await window. function buildAdvisorResult(opts: { text: string; effort: ThinkingLevel | undefined; advisorLabel?: string; usage?: Usage; stopReason?: StopReason; errorMessage?: string; }): AgentToolResult { const details: AdvisorDetails = { effort: opts.effort }; if (opts.advisorLabel !== undefined) details.advisorModel = opts.advisorLabel; if (opts.usage !== undefined) details.usage = opts.usage; if (opts.stopReason !== undefined) details.stopReason = opts.stopReason; if (opts.errorMessage !== undefined) details.errorMessage = opts.errorMessage; return { content: [{ type: "text", text: opts.text }], details }; } function buildErrorResult( advisorLabel: string | undefined, effort: ThinkingLevel | undefined, userText: string, errorMessage: string, ): AgentToolResult { return buildAdvisorResult({ text: userText, effort, advisorLabel, errorMessage }); } export async function executeAdvisor( ctx: ExtensionContext, pi: ExtensionAPI, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback | undefined, ): Promise> { // Snapshot effort once at entry — every result envelope and the API call // itself use this same value so a concurrent setAdvisorEffort() during the // await window cannot desync details.effort from the `reasoning` actually sent. const effort = getAdvisorEffort(); const advisor = getAdvisorModel(); if (!advisor) { return buildErrorResult(undefined, effort, ERR_NO_MODEL, ERR_NO_MODEL_SELECTED); } const advisorLabel = `${advisor.provider}:${advisor.id}`; const auth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor); if (!auth.ok) { return buildErrorResult(advisorLabel, effort, errMisconfigured(advisorLabel, auth.error), auth.error); } if (!auth.apiKey) { return buildErrorResult(advisorLabel, effort, errNoApiKey(advisorLabel), errNoApiKeyDetail(advisor.provider)); } // Live-read every call — advisor runs mid-turn so any message_end snapshot // is always one turn stale. buildSessionContext() preserves Pi's resolved // LLM context, including compaction summaries and branch summaries, instead // of replaying raw pre-compaction branch messages. convertToLlm is // pass-through for user/assistant/toolResult (messages.js:111-114), so // element refs are stable across calls via the session store. const { messages: sessionMessages } = buildSessionContext( ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId(), ); const branchMessages = ensureUserTailForAdvisor(stripInflightAdvisorCall(convertToLlm(sessionMessages))); const inventoryMessage = getInventoryMessage(pi.getAllTools()); const messages: Message[] = inventoryMessage ? [inventoryMessage, ...branchMessages] : branchMessages; onUpdate?.({ content: [{ type: "text", text: msgConsulting(advisorLabel, effort) }], details: { advisorModel: advisorLabel, effort }, }); try { const response = await completeSimple( advisor, // `tools: []` reaffirms the "never calls tools" contract even when // `messages` contains prior toolCall/toolResult blocks (btw.ts:235). { systemPrompt: ADVISOR_SYSTEM_PROMPT, messages, tools: [] }, { apiKey: auth.apiKey, headers: auth.headers, signal, reasoning: effort }, ); if (response.stopReason === "aborted") { return buildAdvisorResult({ text: ERR_CALL_ABORTED, effort, advisorLabel, usage: response.usage, stopReason: response.stopReason, errorMessage: response.errorMessage ?? ERR_ABORTED_DETAIL, }); } if (response.stopReason === "error") { return buildAdvisorResult({ text: errCallFailed(response.errorMessage), effort, advisorLabel, usage: response.usage, stopReason: response.stopReason, errorMessage: response.errorMessage, }); } const advisorText = response.content .filter((c): c is { type: "text"; text: string } => c.type === "text") .map((c) => c.text) .join("\n") .trim(); if (!advisorText) { return buildAdvisorResult({ text: ERR_EMPTY_RESPONSE, effort, advisorLabel, usage: response.usage, stopReason: response.stopReason, errorMessage: ERR_EMPTY_RESPONSE_DETAIL, }); } return buildAdvisorResult({ text: advisorText, effort, advisorLabel, usage: response.usage, stopReason: response.stopReason, }); } catch (err) { const message = err instanceof Error ? err.message : String(err); return buildErrorResult(advisorLabel, effort, errCallThrew(message), message); } }