Skip to content

Instantly share code, notes, and snippets.

@ericflo
Created May 11, 2026 17:31
Show Gist options
  • Select an option

  • Save ericflo/dd2998f8bbfdbbf1299e5209cb65082f to your computer and use it in GitHub Desktop.

Select an option

Save ericflo/dd2998f8bbfdbbf1299e5209cb65082f to your computer and use it in GitHub Desktop.
/**
* Pi Advisor Extension
*
* Drop this file into:
* .pi/extensions/advisor.ts
* or:
* ~/.pi/agent/extensions/advisor.ts
*
* Sends the current Pi transcript to a stronger advisor model and returns
* strategic guidance. The advisor does not call tools, edit files, or write
* the final user-facing answer.
*
* Recommended env for a local executor:
* export PI_ADVISOR_PROVIDER=anthropic
* export PI_ADVISOR_MODEL=claude-opus-4-7
* # or:
* export PI_ADVISOR_PROVIDER=openai
* export PI_ADVISOR_MODEL=gpt-5.5
*
* Optional privacy gate:
* export PI_ADVISOR_REQUIRE_ALLOW=1
* export PI_ADVISOR_ALLOWED=1
*
* Optional tuning:
* PI_ADVISOR_MAX_PER_TURN=2
* PI_ADVISOR_MAX_WORDS=600
* PI_ADVISOR_REASONING_EFFORT=high
* PI_ADVISOR_REDACT=1
* PI_ADVISOR_CACHE=short # "none" | "short" | "long"
*
* Disable entirely: set PI_ADVISOR_MODE=off.
*
* Env is read lazily: change a variable and call /reload (no process restart
* needed) to pick up the new value.
*/
import { randomUUID } from "node:crypto";
import { complete, StringEnum, type CacheRetention } from "@mariozechner/pi-ai";
import type {
ExtensionAPI,
ExtensionCommandContext,
ExtensionContext,
} from "@mariozechner/pi-coding-agent";
import {
convertToLlm,
serializeConversation,
} from "@mariozechner/pi-coding-agent";
import { Type } from "typebox";
const TOOL_NAME = "advisor";
const ADVISOR_CUSTOM_MESSAGE_TYPE = "advisor-advice";
// Stable id for this extension instance. Used as a fallback cache-affinity
// hint when the session manager does not expose a session id.
const FALLBACK_SESSION_ID = `advisor-ext-${randomUUID()}`;
type AdvisorStage =
| "orientation"
| "approach"
| "stuck"
| "final_review"
| "manual";
type AdvisorParams = {
stage?: AdvisorStage;
question?: string;
provider?: string;
model?: string;
max_words?: number;
context?: string;
include_transcript?: boolean;
};
type AdvisorRunResult = {
ok: boolean;
text: string;
provider: string;
model: string;
promptChars: number;
transcriptChars: number;
callIndex: number;
turnCallIndex: number;
error?: string;
};
// Minimal shape for session branch entries we actually read.
interface BranchMessage {
role?: string;
toolName?: string;
content?: unknown;
details?: {
advisor?: {
callIndex?: unknown;
};
};
customType?: string;
}
interface BranchEntry {
type?: string;
message?: BranchMessage;
}
function env(name: string, fallback = ""): string {
const value = process.env[name];
return value === undefined || value === "" ? fallback : value;
}
function intEnv(name: string, fallback: number): number {
const raw = env(name);
if (!raw) return fallback;
const parsed = Number(raw);
return Number.isFinite(parsed) ? Math.trunc(parsed) : fallback;
}
function boolEnv(name: string, fallback = false): boolean {
const raw = env(name);
if (!raw) return fallback;
return ["1", "true", "yes", "y", "on"].includes(raw.toLowerCase());
}
function isOffMode(): boolean {
const mode = env("PI_ADVISOR_MODE", "").trim().toLowerCase();
return ["off", "false", "0", "disabled", "disable"].includes(mode);
}
/**
* Runtime config resolved on each call. Lets users change env vars and
* /reload without restarting the process.
*/
interface AdvisorConfig {
provider: string;
model: string;
requireAllow: boolean;
allowed: boolean;
redact: boolean;
maxPerTurn: number;
defaultMaxWords: number;
reasoningEffort: string;
cacheRetention: CacheRetention;
}
function getConfig(): AdvisorConfig {
const requireAllow = boolEnv("PI_ADVISOR_REQUIRE_ALLOW", false);
const cacheRaw = env("PI_ADVISOR_CACHE", "short").toLowerCase();
const cacheRetention: CacheRetention =
cacheRaw === "none" || cacheRaw === "long" ? cacheRaw : "short";
return {
provider: env("PI_ADVISOR_PROVIDER", "anthropic"),
model: env("PI_ADVISOR_MODEL", "claude-opus-4-7"),
requireAllow,
allowed: boolEnv("PI_ADVISOR_ALLOWED", !requireAllow),
redact: boolEnv("PI_ADVISOR_REDACT", true),
maxPerTurn: intEnv("PI_ADVISOR_MAX_PER_TURN", 2),
defaultMaxWords: intEnv("PI_ADVISOR_MAX_WORDS", 600),
reasoningEffort: env("PI_ADVISOR_REASONING_EFFORT", "high"),
cacheRetention,
};
}
function clampWords(value: unknown, fallback: number): number {
const n =
typeof value === "number" && Number.isFinite(value) ? value : fallback;
return Math.max(80, Math.min(Math.trunc(n), 1_200));
}
function redactSecrets(input: string, enabled: boolean): string {
if (!enabled) return input;
return (
input
// Known-prefix provider keys (specific → generic).
.replace(/sk-ant-[A-Za-z0-9_-]{16,}/g, "[REDACTED_ANTHROPIC_KEY]")
.replace(/sk-proj-[A-Za-z0-9_-]{16,}/g, "[REDACTED_OPENAI_PROJECT_KEY]")
// Tight fallback for other OpenAI-style keys: require a word boundary and
// long alnum tail (no dashes/underscores) so we don't eat identifiers
// like "sk-learn" or "sk-my-branch-name".
.replace(/\bsk-[A-Za-z0-9]{32,}\b/g, "[REDACTED_API_KEY]")
.replace(/AIza[0-9A-Za-z_-]{20,}/g, "[REDACTED_GOOGLE_API_KEY]")
.replace(/ghp_[0-9A-Za-z_]{20,}/g, "[REDACTED_GITHUB_TOKEN]")
.replace(/github_pat_[0-9A-Za-z_]{20,}/g, "[REDACTED_GITHUB_TOKEN]")
.replace(/xox[baprs]-[0-9A-Za-z-]{20,}/g, "[REDACTED_SLACK_TOKEN]")
.replace(
/(OPENAI_API_KEY|ANTHROPIC_API_KEY|GOOGLE_API_KEY|GEMINI_API_KEY|GITHUB_TOKEN|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN)\s*=\s*["']?[^"'\s]+["']?/g,
"$1=[REDACTED]",
)
.replace(
/Authorization:\s*Bearer\s+[^\s"'`]+/gi,
"Authorization: Bearer [REDACTED]",
)
.replace(
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
"[REDACTED_PRIVATE_KEY]",
)
);
}
function contentToText(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
const parts: string[] = [];
for (const part of content) {
if (!part || typeof part !== "object") continue;
const block = part as {
type?: string;
text?: string;
name?: string;
arguments?: unknown;
content?: unknown;
};
if (block.type === "text" && typeof block.text === "string") {
parts.push(block.text);
} else if (block.type === "toolCall" && typeof block.name === "string") {
parts.push(
`Tool call: ${block.name} ${JSON.stringify(block.arguments ?? {})}`,
);
} else if (typeof block.text === "string") {
parts.push(block.text);
} else if (block.content) {
const nested = contentToText(block.content);
if (nested) parts.push(nested);
}
}
return parts.join("\n");
}
function fallbackSerializeBranch(entries: BranchEntry[]): string {
const sections: string[] = [];
for (const entry of entries) {
if (entry?.type !== "message" || !entry.message) continue;
const role = entry.message.role ?? "unknown";
const text = contentToText(entry.message.content).trim();
const toolName = entry.message.toolName ? ` ${entry.message.toolName}` : "";
if (text) {
sections.push(`${role}${toolName}:\n${text}`);
}
}
return sections.join("\n\n");
}
function getBranchEntries(
ctx: ExtensionContext | ExtensionCommandContext,
): BranchEntry[] {
return (ctx.sessionManager.getBranch?.() ?? []) as BranchEntry[];
}
function getBranchMessages(
ctx: ExtensionContext | ExtensionCommandContext,
): BranchMessage[] {
return getBranchEntries(ctx)
.filter((entry) => entry?.type === "message" && entry.message)
.map((entry) => entry.message as BranchMessage);
}
function buildTranscript(
ctx: ExtensionContext | ExtensionCommandContext,
redact: boolean,
): string {
let serialized = "";
try {
// convertToLlm expects pi's internal message shape; our BranchMessage is
// a read-only subset, so a structural cast is safe here.
serialized = serializeConversation(
convertToLlm(getBranchMessages(ctx) as never),
);
} catch {
serialized = fallbackSerializeBranch(getBranchEntries(ctx));
}
return redactSecrets(serialized, redact);
}
function activeToolSummary(pi: ExtensionAPI): string {
// getActiveTools() returns an array of tool name strings.
const tools = pi
.getActiveTools()
.filter((name) => name !== TOOL_NAME)
.map((name) => `- ${name}`)
.join("\n");
return tools || "- No active non-advisor tools detected.";
}
function executorName(ctx: ExtensionContext | ExtensionCommandContext): string {
const model = (ctx as unknown as { model?: { provider?: string; id?: string; name?: string } }).model;
if (!model) return "unknown";
return `${model.provider ?? "unknown"}/${model.id ?? model.name ?? "unknown"}`;
}
function getSessionIdForCache(
ctx: ExtensionContext | ExtensionCommandContext,
): string {
try {
const sid = ctx.sessionManager.getSessionId?.();
if (typeof sid === "string" && sid.length > 0) return sid;
} catch {
// fall through
}
return FALLBACK_SESSION_ID;
}
function buildAdvisorPrompt(
pi: ExtensionAPI,
ctx: ExtensionContext | ExtensionCommandContext,
params: AdvisorParams,
transcript: string,
cfg: AdvisorConfig,
): string {
const stage = params.stage ?? "approach";
const question =
params.question?.trim() ||
"Review the current task state and advise the executor on the best next steps.";
const maxWords = clampWords(params.max_words, cfg.defaultMaxWords);
const includeTranscript = params.include_transcript !== false;
const curatedContext = redactSecrets(
(params.context ?? "").trim(),
cfg.redact,
);
let systemPrompt = "";
try {
systemPrompt =
(ctx as unknown as { getSystemPrompt?: () => string }).getSystemPrompt?.() ?? "";
} catch {
systemPrompt = "";
}
systemPrompt = redactSecrets(systemPrompt, cfg.redact);
const cwd =
(ctx as unknown as { cwd?: string }).cwd ?? "(unknown)";
const contextSection = curatedContext
? `\n\nThe executor selected the following as most relevant. Treat it as primary signal; the conversation transcript (if present) is supporting context.\n<executor_curated_context>\n${curatedContext}\n</executor_curated_context>`
: "";
const transcriptSection = includeTranscript
? `\n\nConversation transcript:\n<conversation>\n${transcript}\n</conversation>`
: "";
return `
You are the ADVISOR model for a Pi coding-agent session.
The executor is: ${executorName(ctx)}.
The executor may be a smaller local model. You are not the executor.
You do not call tools, edit files, run commands, or write the final user-facing answer.
Your job is to give strategic guidance that the executor can apply with its own tools.
Current working directory:
${cwd}
Executor tools available:
${activeToolSummary(pi)}
Advisor request stage:
${stage}
Executor's specific question:
${question}
Rules:
- Base your advice on the transcript. Do not invent files, APIs, test results, or requirements.
- Be concrete: name files, functions, commands, and tests only when the transcript supports them.
- Prefer minimal, reversible changes and verification over speculative rewrites.
- If the executor is stuck, diagnose the likely failure pattern and propose a different next step.
- If this is final_review, identify missing verification and whether it is safe to declare done.
- If evidence conflicts with your preferred plan, call out the conflict explicitly.
- Keep the response under ${maxWords} words.
- Use exactly this structure:
1. Situation
2. Recommended plan
3. Risks / things to verify
4. Done criteria or stop signal
Pi system prompt excerpt:
<system_prompt>
${systemPrompt}
</system_prompt>${contextSection}${transcriptSection}
`.trim();
}
interface CompletionInfo {
text: string;
stopReason?: string;
errorMessage?: string;
outputTokens?: number;
reasoningTokens?: number;
}
function summarizeCompletion(response: unknown): CompletionInfo {
const r = response as
| {
content?: unknown;
stopReason?: string;
errorMessage?: string;
usage?: { output?: number; reasoning?: number };
}
| undefined;
const content = Array.isArray(r?.content) ? r!.content : [];
const text = content
.filter(
(part: unknown): part is { type: string; text: string } =>
typeof part === "object" &&
part !== null &&
(part as { type?: unknown }).type === "text" &&
typeof (part as { text?: unknown }).text === "string",
)
.map((part) => part.text)
.join("\n")
.trim();
return {
text,
stopReason: r?.stopReason,
errorMessage: r?.errorMessage,
outputTokens: r?.usage?.output,
reasoningTokens: r?.usage?.reasoning,
};
}
const ADVISOR_GUIDANCE_TEXT = `
Advisor tool guidance:
You have an \`advisor\` tool backed by a stronger external model.
Call advisor when:
- You have completed initial orientation reads on a complex task, but before committing to a non-trivial implementation approach.
- You are stuck after repeated command/test failures, contradictory evidence, or non-converging edits.
- You are considering a major change of approach.
- You believe a complex task is complete and want final review before declaring done.
Do not call advisor for simple one-step tasks or when the next action is obvious.
Do not use advisor as a substitute for reading the relevant files or running tests.
Treat advisor advice as strategic guidance, not ground truth. If local evidence conflicts with the advice, investigate and reconcile the conflict explicitly.
`.trim();
export default function advisorExtension(pi: ExtensionAPI) {
if (isOffMode()) {
return;
}
let callsThisTurn = 0;
let callsThisSession = 0;
function readCallIndex(message: BranchMessage | undefined): number {
const raw = message?.details?.advisor?.callIndex;
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
return Math.trunc(raw);
}
return 0;
}
function restoreAdvisorCount(
ctx: ExtensionContext | ExtensionCommandContext,
) {
callsThisTurn = 0;
callsThisSession = 0;
for (const entry of getBranchEntries(ctx)) {
const message = entry?.message;
if (entry?.type !== "message" || !message) continue;
const isAdvisorToolResult =
message.role === "toolResult" && message.toolName === TOOL_NAME;
const isAdvisorCustom =
message.role === "custom" &&
message.customType === ADVISOR_CUSTOM_MESSAGE_TYPE;
if (!isAdvisorToolResult && !isAdvisorCustom) continue;
const n = readCallIndex(message);
if (n > callsThisSession) {
callsThisSession = n;
}
}
}
async function runAdvisor(
ctx: ExtensionContext | ExtensionCommandContext,
params: AdvisorParams,
signal?: AbortSignal,
): Promise<AdvisorRunResult> {
const cfg = getConfig();
const provider = params.provider?.trim() || cfg.provider;
const modelId = params.model?.trim() || cfg.model;
const base = {
provider,
model: modelId,
promptChars: 0,
transcriptChars: 0,
callIndex: callsThisSession,
turnCallIndex: callsThisTurn,
};
if (!cfg.allowed) {
return {
...base,
ok: false,
text: "Advisor is disabled by privacy gate. Set PI_ADVISOR_ALLOWED=1, or unset PI_ADVISOR_REQUIRE_ALLOW, to allow sending transcript context to the advisor provider.",
error: "privacy_gate",
};
}
if (callsThisTurn >= cfg.maxPerTurn) {
return {
...base,
ok: false,
text: `Advisor per-turn cap reached (${cfg.maxPerTurn}). Continue with the best current plan unless the user explicitly asks for more advisor calls.`,
error: "per_turn_cap",
};
}
const model = ctx.modelRegistry.find(provider, modelId);
if (!model) {
return {
...base,
ok: false,
text: `Advisor model not found in Pi model registry: ${provider}/${modelId}. Use /model to check available models or set PI_ADVISOR_PROVIDER and PI_ADVISOR_MODEL.`,
error: "model_not_found",
};
}
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
if (!auth.ok) {
return {
...base,
ok: false,
text: `Advisor auth failed for ${provider}/${modelId}: ${auth.error}`,
error: "auth_failed",
};
}
if (!auth.apiKey) {
return {
...base,
ok: false,
text: `No API key available for advisor model ${provider}/${modelId}. Set the provider API key, authenticate in Pi, or choose another advisor model.`,
error: "missing_api_key",
};
}
const includeTranscript = params.include_transcript !== false;
const hasContext = (params.context ?? "").trim().length > 0;
if (!includeTranscript && !hasContext) {
return {
...base,
ok: false,
text: "Advisor called with include_transcript=false but no `context` was provided. Either keep include_transcript at its default (true) or pass a non-empty `context` string with the curated information you want the advisor to consider.",
error: "missing_context",
};
}
const transcript = includeTranscript ? buildTranscript(ctx, cfg.redact) : "";
const prompt = buildAdvisorPrompt(pi, ctx, params, transcript, cfg);
callsThisTurn += 1;
callsThisSession += 1;
const messages = [
{
role: "user" as const,
content: [{ type: "text" as const, text: prompt }],
timestamp: Date.now(),
},
];
// Scope the cache-affinity key per advisor provider/model so switching
// advisors mid-session doesn't cause misses or cross-model collisions.
const baseSessionId = getSessionIdForCache(ctx);
const cacheSessionId = `${baseSessionId}:${provider}:${modelId}`;
const options: Record<string, unknown> = {
apiKey: auth.apiKey,
headers: auth.headers,
cacheRetention: cfg.cacheRetention,
sessionId: cacheSessionId,
signal,
};
if (provider.toLowerCase().includes("openai") && cfg.reasoningEffort) {
options.reasoningEffort = cfg.reasoningEffort;
}
try {
const response = await complete(
model,
{ messages },
options as Parameters<typeof complete>[2],
);
const info = summarizeCompletion(response);
if (info.text) {
return {
ok: true,
text: info.text,
provider,
model: model.id,
promptChars: prompt.length,
transcriptChars: transcript.length,
callIndex: callsThisSession,
turnCallIndex: callsThisTurn,
};
}
// Empty completion — surface what we know so failures are diagnosable.
const detail = [
info.errorMessage ? `error: ${info.errorMessage}` : null,
info.stopReason ? `stopReason: ${info.stopReason}` : null,
typeof info.outputTokens === "number"
? `outputTokens: ${info.outputTokens}`
: null,
typeof info.reasoningTokens === "number"
? `reasoningTokens: ${info.reasoningTokens}`
: null,
]
.filter(Boolean)
.join(", ");
return {
ok: false,
text: `Advisor returned no text.${detail ? ` (${detail})` : ""} Continue with the current best plan and verify with tests.`,
provider,
model: model.id,
promptChars: prompt.length,
transcriptChars: transcript.length,
callIndex: callsThisSession,
turnCallIndex: callsThisTurn,
error: info.errorMessage ?? info.stopReason ?? "empty_response",
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
ok: false,
text: `Advisor call failed: ${message}. Continue without advisor and verify locally.`,
provider,
model: model.id,
promptChars: prompt.length,
transcriptChars: transcript.length,
callIndex: callsThisSession,
turnCallIndex: callsThisTurn,
error: message,
};
}
}
function formatAdvisorResult(result: AdvisorRunResult): string {
const status = result.ok ? "Advisor advice" : "Advisor unavailable";
return `${status} from ${result.provider}/${result.model}:\n\n${result.text}`;
}
pi.on("session_start", async (_event, ctx) => {
restoreAdvisorCount(ctx);
if (ctx.hasUI) {
const cfg = getConfig();
ctx.ui.notify(
`Advisor extension loaded: ${cfg.provider}/${cfg.model}`,
"info",
);
}
});
pi.on("agent_start", async () => {
callsThisTurn = 0;
});
pi.on("before_agent_start", async (event) => ({
systemPrompt: `${event.systemPrompt}\n\n${ADVISOR_GUIDANCE_TEXT}`,
}));
pi.registerTool({
name: TOOL_NAME,
label: "Advisor",
description:
"Ask a stronger advisor model for strategic guidance. The advisor sees the current Pi conversation transcript and returns a compact plan, critique, or final review. It does not edit files or run tools.",
promptSnippet:
"Ask a stronger advisor model for complex planning, stuck states, approach changes, or final review.",
promptGuidelines: [
"Use advisor after orientation reads on complex coding tasks, before committing to a non-trivial implementation approach.",
"Use advisor when stuck after repeated command/test failures, contradictory evidence, or non-converging edits.",
"Use advisor before declaring a complex task complete, especially when verification is incomplete or risky.",
"Do not use advisor for simple one-step tasks or as a substitute for reading files and running tests.",
],
parameters: Type.Object({
stage: Type.Optional(
StringEnum(
["orientation", "approach", "stuck", "final_review"] as const,
{
description: "Why advisor is being called right now.",
},
),
),
question: Type.Optional(
Type.String({
description:
"Specific question or decision for the advisor. Include your hypothesis, errors, or proposed plan.",
}),
),
provider: Type.Optional(
Type.String({
description:
"Optional advisor provider override, for example openai or anthropic. Defaults to PI_ADVISOR_PROVIDER.",
}),
),
model: Type.Optional(
Type.String({
description:
"Optional advisor model override, for example claude-opus-4-7 or gpt-5.5. Defaults to PI_ADVISOR_MODEL.",
}),
),
max_words: Type.Optional(
Type.Number({
description:
"Approximate maximum advice length. Default PI_ADVISOR_MAX_WORDS, capped at 1200.",
}),
),
context: Type.Optional(
Type.String({
description:
"Optional executor-curated context (relevant code, diffs, errors, prior reasoning). When provided, the advisor treats it as primary signal over the conversation transcript. Useful for focused reviews or to reduce token cost on long sessions.",
}),
),
include_transcript: Type.Optional(
Type.Boolean({
description:
"Whether to also send the full conversation transcript. Defaults to true. Set to false to send only `context` (must be non-empty) for self-contained questions, privacy-sensitive flows, or to avoid noise from long sessions. Combining true with a non-empty `context` is allowed but doubles tokens; prefer false when you already know what matters.",
}),
),
}),
prepareArguments(args) {
if (!args || typeof args !== "object") return {};
return args;
},
async execute(_toolCallId, params: AdvisorParams, signal, onUpdate, ctx) {
const cfg = getConfig();
const displayProvider = params.provider ?? cfg.provider;
const displayModel = params.model ?? cfg.model;
onUpdate?.({
content: [
{
type: "text",
text: `Consulting advisor ${displayProvider}/${displayModel}...`,
},
],
details: {
advisor: {
status: "running",
provider: displayProvider,
model: displayModel,
},
},
});
const result = await runAdvisor(ctx, params, signal);
return {
content: [{ type: "text", text: formatAdvisorResult(result) }],
details: {
advisor: {
ok: result.ok,
provider: result.provider,
model: result.model,
callIndex: result.callIndex,
turnCallIndex: result.turnCallIndex,
maxPerTurn: cfg.maxPerTurn,
promptChars: result.promptChars,
transcriptChars: result.transcriptChars,
error: result.error,
},
},
};
},
});
pi.registerCommand("advise", {
description:
"Manually ask the configured advisor model and inject the advice as steering context",
handler: async (args, ctx) => {
await ctx.waitForIdle();
if (ctx.hasUI) {
ctx.ui.notify("Consulting advisor...", "info");
}
const result = await runAdvisor(ctx, {
stage: "manual",
question:
args.trim() ||
"Review the current work and advise the executor on the best next step.",
});
// Don't steer the executor with cap/privacy/auth error messages — those
// are meta problems for the human, not guidance for the coding agent.
if (!result.ok) {
if (ctx.hasUI) {
ctx.ui.notify(
`Advisor unavailable (${result.error ?? "unknown"}): ${result.text}`,
"warning",
);
}
return;
}
const content = formatAdvisorResult(result);
pi.sendMessage(
{
customType: ADVISOR_CUSTOM_MESSAGE_TYPE,
content,
display: true,
details: {
advisor: {
ok: result.ok,
provider: result.provider,
model: result.model,
callIndex: result.callIndex,
turnCallIndex: result.turnCallIndex,
promptChars: result.promptChars,
transcriptChars: result.transcriptChars,
error: result.error,
},
},
},
{
triggerTurn: true,
deliverAs: "steer",
},
);
},
});
pi.registerCommand("advisor-status", {
description: "Show advisor extension status",
handler: async (_args, ctx) => {
const cfg = getConfig();
ctx.ui.notify(
[
`advisor: ${cfg.provider}/${cfg.model}`,
`allowed: ${cfg.allowed ? "yes" : "no"}`,
`redaction: ${cfg.redact ? "on" : "off"}`,
`cache retention: ${cfg.cacheRetention}`,
`calls this turn / total this session: ${callsThisTurn}/${callsThisSession}`,
`cap per-turn: ${cfg.maxPerTurn} (no session cap)`,
`reasoning effort (openai): ${cfg.reasoningEffort || "(default)"}`,
`default max_words: ${cfg.defaultMaxWords}`,
`config is read lazily; edit env then /reload to apply.`,
].join("\n"),
"info",
);
},
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment