Publica agent-switcher: extensión de pi para cambio de agentes estilo opencode
This commit is contained in:
commit
e4b86fc6eb
7 files changed
+1156
No files matched your search
@@ -0,0 +1,582 @@
|
||||
/**
|
||||
* Agent Switcher — cambio de agentes estilo opencode para pi.
|
||||
*
|
||||
* Permite alternar entre agentes (p. ej. `plan` y `build`) con `/agent`,
|
||||
* `/agents` o los atajos de teclado de cada agente. Cada agente puede llevar su
|
||||
* de thinking, allowlist de herramientas, rol de solo lectura y system
|
||||
* prompt, definidos en archivos Markdown con frontmatter:
|
||||
*
|
||||
* ~/.pi/agent/agents/*.md (agentes de usuario)
|
||||
* <cwd>/.pi/agents/*.md (agentes de proyecto, sobrescriben)
|
||||
*
|
||||
* Ver README.md para el esquema completo y defs.ts para la lista de claves.
|
||||
*
|
||||
* Semántica (equivalente al cambio de agentes de opencode):
|
||||
* - El agente activo gobierna: su modelo/thinking se aplican al activarse y
|
||||
* se reafirman en cada `before_agent_start`.
|
||||
* - Si el agente no define `model`, se conserva el modelo de la sesión.
|
||||
* - Un cambio manual de modelo con `/model` queda registrado como override
|
||||
* de sesión para el agente activo y se restaura al volver a él.
|
||||
* - La selección persiste en la sesión y se restaura al reanudarla.
|
||||
*/
|
||||
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import type { Model } from "@earendil-works/pi-ai";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
ExtensionCommandContext,
|
||||
ExtensionContext,
|
||||
ToolCallEvent,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { findAgent, loadAgentDefinitions, type AgentDefinition } from "./defs.ts";
|
||||
import { isReadOnlySafeCommand } from "./policy.ts";
|
||||
|
||||
const ENTRY_TYPE = "agent-switcher";
|
||||
const DEFAULT_AGENT = "build";
|
||||
|
||||
interface OverrideRecord {
|
||||
/** Valor elegido durante la sesión (modelo o thinking). */
|
||||
value: string;
|
||||
/** Valor del archivo en el momento de elegirlo. Si el archivo cambia, el override deja de valer. */
|
||||
base?: string;
|
||||
/** Solo los overrides explícitos (`/agent model`) se aplican; descarta registros de versiones previas. */
|
||||
explicit?: boolean;
|
||||
}
|
||||
|
||||
interface PersistedState {
|
||||
active?: string;
|
||||
/** Overrides de modelo por agente. Solo se aplican los `explicit: true`. */
|
||||
models?: Record<string, OverrideRecord>;
|
||||
/** Overrides de thinking por agente. Solo se aplican los `explicit: true`. */
|
||||
thinking?: Record<string, OverrideRecord>;
|
||||
}
|
||||
|
||||
export default function agentSwitcher(pi: ExtensionAPI) {
|
||||
/** Definiciones cargadas (se recargan en cada session_start). */
|
||||
let defs: AgentDefinition[] = [];
|
||||
/** Nombre del agente activo. */
|
||||
let activeName = DEFAULT_AGENT;
|
||||
/** Herramientas activas antes de la primera restricción (para restaurar). */
|
||||
let toolsBaseline: string[] | undefined;
|
||||
/** Overrides de sesión por agente (solo cambios explícitos del usuario). */
|
||||
const modelOverrides = new Map<string, OverrideRecord>();
|
||||
const thinkingOverrides = new Map<string, OverrideRecord>();
|
||||
/** Cola de aplicación: serializa cambios de modelo/thinking/herramientas para
|
||||
* que pulsaciones rápidas de F5/F6 no se pisen (el último cambio gana). */
|
||||
let applyChain: Promise<unknown> = Promise.resolve();
|
||||
/** Último error de modelo notificado (para no repetirlo en cada run). */
|
||||
let lastError = "";
|
||||
|
||||
pi.registerFlag("agent", {
|
||||
description: "Agente activo al arrancar (agent-switcher)",
|
||||
type: "string",
|
||||
});
|
||||
|
||||
function current(): AgentDefinition | undefined {
|
||||
if (defs.length === 0) defs = loadAgentDefinitions(process.cwd());
|
||||
return findAgent(defs, activeName);
|
||||
}
|
||||
|
||||
/** El override vale solo mientras el archivo del agente no cambie. */
|
||||
function effectiveOverride(
|
||||
override: OverrideRecord | undefined,
|
||||
base: string | undefined,
|
||||
): string | undefined {
|
||||
if (!override || override.base !== base) return undefined;
|
||||
return override.value;
|
||||
}
|
||||
|
||||
function effectiveRef(agent: AgentDefinition): string | undefined {
|
||||
return effectiveOverride(modelOverrides.get(agent.name), agent.model) ?? agent.model;
|
||||
}
|
||||
|
||||
function effectiveThinking(agent: AgentDefinition): string | undefined {
|
||||
return effectiveOverride(thinkingOverrides.get(agent.name), agent.thinking) ?? agent.thinking;
|
||||
}
|
||||
|
||||
function modelRef(model: Model<any> | undefined): string | undefined {
|
||||
return model ? `${model.provider}/${model.id}` : undefined;
|
||||
}
|
||||
|
||||
function notify(ctx: ExtensionContext, text: string, kind: "info" | "warning" | "error" = "info"): void {
|
||||
if (ctx.hasUI) ctx.ui.notify(text, kind);
|
||||
}
|
||||
|
||||
/** Resuelve "provider/id" o un "id" único contra el registro de modelos. */
|
||||
function resolveModel(
|
||||
ctx: ExtensionContext,
|
||||
ref: string,
|
||||
): { model?: Model<any>; error?: string } {
|
||||
let all: Model<any>[];
|
||||
try {
|
||||
all = ctx.modelRegistry.getAll();
|
||||
} catch {
|
||||
return { error: `no se pudo leer el registro de modelos` };
|
||||
}
|
||||
const slash = ref.indexOf("/");
|
||||
if (slash > 0) {
|
||||
const provider = ref.slice(0, slash);
|
||||
const id = ref.slice(slash + 1);
|
||||
const model = all.find((m) => m.provider === provider && m.id === id);
|
||||
return model ? { model } : { error: `modelo "${ref}" no encontrado` };
|
||||
}
|
||||
const matches = all.filter((m) => m.id === ref);
|
||||
if (matches.length === 1) return { model: matches[0] };
|
||||
if (matches.length > 1) {
|
||||
const providers = [...new Set(matches.map((m) => m.provider))].join(", ");
|
||||
return { error: `"${ref}" es ambiguo entre: ${providers}. Usa "provider/id"` };
|
||||
}
|
||||
return { error: `modelo "${ref}" no encontrado` };
|
||||
}
|
||||
|
||||
/** Restaura o restringe las herramientas según el agente. */
|
||||
function applyTools(agent: AgentDefinition): string[] {
|
||||
const registered = new Set(pi.getAllTools().map((t) => t.name));
|
||||
if (agent.tools && agent.tools.length > 0) {
|
||||
if (toolsBaseline === undefined) toolsBaseline = pi.getActiveTools();
|
||||
const list = agent.tools.filter((n) => registered.has(n));
|
||||
if (list.length > 0) pi.setActiveTools(list);
|
||||
return agent.tools.filter((n) => !registered.has(n));
|
||||
}
|
||||
if (toolsBaseline !== undefined) {
|
||||
pi.setActiveTools(toolsBaseline);
|
||||
toolsBaseline = undefined;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Encola una operación de aplicación para serializarla con el resto. */
|
||||
function enqueue<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const run = applyChain.then(fn, fn);
|
||||
applyChain = run.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
);
|
||||
return run;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica modelo, thinking y herramientas del agente de forma idempotente.
|
||||
* Las aplicaciones se serializan: si se pulsa F5 y F6 en ráfaga, cada una
|
||||
* termina por orden y el último cambio gana (antes la segunda se saltaba la
|
||||
* aplicación y los mensajes se cruzaban con el agente anterior).
|
||||
* `announce` controla las notificaciones (solo en cambios explícitos).
|
||||
*/
|
||||
async function ensureApplied(
|
||||
agent: AgentDefinition,
|
||||
ctx: ExtensionContext,
|
||||
announce: boolean,
|
||||
): Promise<void> {
|
||||
return enqueue(() => applyAgentPolicy(agent, ctx, announce));
|
||||
}
|
||||
|
||||
async function applyAgentPolicy(
|
||||
agent: AgentDefinition,
|
||||
ctx: ExtensionContext,
|
||||
announce: boolean,
|
||||
): Promise<void> {
|
||||
let appliedRef: string | undefined;
|
||||
{
|
||||
// Modelo: override de sesión > modelo del agente > modelo actual.
|
||||
const ref = effectiveRef(agent);
|
||||
if (ref) {
|
||||
const { model, error } = resolveModel(ctx, ref);
|
||||
if (!model) {
|
||||
if (announce || error !== lastError) {
|
||||
notify(ctx, `agent-switcher (${agent.name}): ${error}`, "warning");
|
||||
lastError = error ?? "";
|
||||
}
|
||||
} else {
|
||||
lastError = "";
|
||||
appliedRef = modelRef(model);
|
||||
const differs = ctx.model?.provider !== model.provider || ctx.model?.id !== model.id;
|
||||
if (differs) {
|
||||
try {
|
||||
const ok = await pi.setModel(model);
|
||||
if (!ok && announce) {
|
||||
notify(
|
||||
ctx,
|
||||
`agent-switcher: sin autenticación para ${model.provider}/${model.id}`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
if (announce) {
|
||||
notify(ctx, `agent-switcher: no se pudo cambiar a ${model.provider}/${model.id}`, "warning");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thinking: override de sesión > thinking del agente.
|
||||
const thinking = effectiveThinking(agent);
|
||||
if (thinking) pi.setThinkingLevel(thinking as ThinkingLevel);
|
||||
|
||||
// Herramientas.
|
||||
const unknown = applyTools(agent);
|
||||
if (unknown.length > 0 && announce) {
|
||||
notify(ctx, `agent-switcher: herramientas no registradas ignoradas: ${unknown.join(", ")}`, "warning");
|
||||
}
|
||||
|
||||
if (announce && agent.name === activeName) {
|
||||
const parts = [`Agente activo: ${agent.name}`];
|
||||
const shown = appliedRef ?? modelRef(ctx.model);
|
||||
if (shown) parts.push(`modelo ${shown}`);
|
||||
if (agent.tools?.length) parts.push(`tools ${agent.tools.join(", ")}`);
|
||||
if (agent.acceptanceRole === "read-only") parts.push("solo lectura");
|
||||
notify(ctx, parts.join(" · "), "info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function persist(): void {
|
||||
const state: PersistedState = {
|
||||
active: activeName,
|
||||
models: Object.fromEntries(modelOverrides),
|
||||
thinking: Object.fromEntries(thinkingOverrides),
|
||||
};
|
||||
pi.appendEntry(ENTRY_TYPE, state);
|
||||
}
|
||||
|
||||
function defaultAgentName(): string {
|
||||
if (findAgent(defs, DEFAULT_AGENT)) return DEFAULT_AGENT;
|
||||
return (defs.find((d) => d.advertise) ?? defs[0])?.name ?? DEFAULT_AGENT;
|
||||
}
|
||||
|
||||
/** Cambia el agente activo y aplica su política. */
|
||||
async function switchTo(
|
||||
agent: AgentDefinition,
|
||||
ctx: ExtensionContext,
|
||||
announce = true,
|
||||
): Promise<void> {
|
||||
const wasActive = agent.name === activeName;
|
||||
activeName = agent.name;
|
||||
await ensureApplied(agent, ctx, announce);
|
||||
persist();
|
||||
if (announce && wasActive) {
|
||||
notify(ctx, `agent-switcher: "${agent.name}" ya estaba activo`, "info");
|
||||
}
|
||||
}
|
||||
|
||||
function agentLines(ctx: ExtensionContext): string[] {
|
||||
return defs.map((d) => {
|
||||
const marker = d.name === activeName ? "●" : "○";
|
||||
const override = effectiveOverride(modelOverrides.get(d.name), d.model);
|
||||
let ref = override ?? d.model ?? "—";
|
||||
if (d.name === activeName && ref === "—") ref = modelRef(ctx.model) ?? "—";
|
||||
if (override) ref += " (sesión)";
|
||||
const flags = [
|
||||
d.acceptanceRole === "read-only" ? "solo lectura" : "",
|
||||
d.advertise ? "" : "oculto",
|
||||
].filter(Boolean);
|
||||
const head = `${marker} ${d.name}${d.aliases.length ? ` (${d.aliases.join(", ")})` : ""} — ${ref}${
|
||||
flags.length ? ` · ${flags.join(" · ")}` : ""
|
||||
}`;
|
||||
return d.description ? `${head}\n ${d.description}` : head;
|
||||
});
|
||||
}
|
||||
|
||||
async function commandAgent(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
||||
if (defs.length === 0) defs = loadAgentDefinitions(ctx.cwd);
|
||||
const trimmed = args.trim();
|
||||
|
||||
// /agent → selector (o listado si no hay UI).
|
||||
if (!trimmed) {
|
||||
const visible = defs.filter((d) => d.advertise);
|
||||
if (!ctx.hasUI || visible.length === 0) {
|
||||
notify(ctx, `Agentes:\n${agentLines(ctx).join("\n")}`, "info");
|
||||
return;
|
||||
}
|
||||
const labels = visible.map(
|
||||
(d) =>
|
||||
`${d.name === activeName ? "●" : "○"} ${d.name}${
|
||||
d.aliases.length ? ` (${d.aliases.join(", ")})` : ""
|
||||
} — ${d.description}${effectiveRef(d) ? ` [${effectiveRef(d)}]` : ""}`,
|
||||
);
|
||||
const chosen = await ctx.ui.select("Cambiar de agente", labels);
|
||||
if (!chosen) return;
|
||||
const idx = labels.indexOf(chosen);
|
||||
const def = visible[idx];
|
||||
if (def) await switchTo(def, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
const [sub, ...restWords] = trimmed.split(/\s+/);
|
||||
const rest = restWords.join(" ");
|
||||
const subLower = sub.toLowerCase();
|
||||
|
||||
if (subLower === "list" || subLower === "ls") {
|
||||
notify(ctx, `Agentes:\n${agentLines(ctx).join("\n")}`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
if (subLower === "help") {
|
||||
notify(
|
||||
ctx,
|
||||
[
|
||||
"Uso:",
|
||||
" /agent selector de agentes",
|
||||
" /agent <nombre> cambiar de agente",
|
||||
" /agent list listar agentes y su estado",
|
||||
" /agent model modelo efectivo del agente activo",
|
||||
" /agent model <ref> fijar modelo del agente (esta sesión)",
|
||||
" /agent model reset quitar el override y usar el modelo del archivo",
|
||||
" /agents alias de /agent list",
|
||||
`Atajos de teclado: ver "shortcut:" en el frontmatter de cada agente (F5/F6 por defecto).`,
|
||||
].join("\n"),
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subLower === "model") {
|
||||
const agent = current();
|
||||
if (!agent) {
|
||||
notify(ctx, "agent-switcher: no hay ningún agente activo", "warning");
|
||||
return;
|
||||
}
|
||||
if (!rest) {
|
||||
const override = effectiveOverride(modelOverrides.get(agent.name), agent.model);
|
||||
notify(
|
||||
ctx,
|
||||
override
|
||||
? `${agent.name} → ${override} (override de sesión; /agent model reset para volver a ${agent.model ?? "el modelo de la sesión"})`
|
||||
: agent.model
|
||||
? `${agent.name} → ${agent.model} (del archivo)`
|
||||
: `${agent.name} no define modelo; usa el de la sesión (${modelRef(ctx.model) ?? "ninguno"})`,
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (["reset", "clear", "borrar", "-"].includes(rest.toLowerCase())) {
|
||||
modelOverrides.delete(agent.name);
|
||||
await ensureApplied(agent, ctx, true);
|
||||
persist();
|
||||
notify(
|
||||
ctx,
|
||||
`Override de ${agent.name} eliminado → ${agent.model ?? "modelo de la sesión"}`,
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { model, error } = resolveModel(ctx, rest);
|
||||
if (!model) {
|
||||
notify(ctx, `agent-switcher: ${error}`, "warning");
|
||||
return;
|
||||
}
|
||||
modelOverrides.set(agent.name, {
|
||||
value: `${model.provider}/${model.id}`,
|
||||
base: agent.model,
|
||||
explicit: true,
|
||||
});
|
||||
let ok = false;
|
||||
try {
|
||||
ok = await enqueue(() => pi.setModel(model));
|
||||
} catch {
|
||||
ok = false;
|
||||
}
|
||||
if (!ok) {
|
||||
modelOverrides.delete(agent.name);
|
||||
notify(
|
||||
ctx,
|
||||
`agent-switcher: no se pudo cambiar a ${model.provider}/${model.id} (¿sin autenticación?)`,
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
persist();
|
||||
notify(ctx, `Modelo de ${agent.name} (sesión) → ${model.provider}/${model.id}`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
const target = findAgent(defs, subLower);
|
||||
if (!target) {
|
||||
notify(
|
||||
ctx,
|
||||
`agent-switcher: agente desconocido "${sub}". Disponibles: ${defs
|
||||
.map((d) => d.name)
|
||||
.join(", ")}\n/agent list para más detalle, /agent para el selector.`,
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await switchTo(target, ctx);
|
||||
}
|
||||
|
||||
pi.registerCommand("agent", {
|
||||
description: "Cambiar de agente (estilo opencode): /agent [nombre|model <ref>|list|help]",
|
||||
getArgumentCompletions: (prefix: string) => {
|
||||
const options = ["list", "model", "help"];
|
||||
for (const d of defs) {
|
||||
options.push(d.name, ...d.aliases);
|
||||
}
|
||||
const p = prefix.toLowerCase();
|
||||
const seen = new Set<string>();
|
||||
return options
|
||||
.filter((v) => v.startsWith(p) && !seen.has(v))
|
||||
.map((v) => {
|
||||
seen.add(v);
|
||||
const d = findAgent(defs, v);
|
||||
return { value: v, label: v, description: d?.description };
|
||||
});
|
||||
},
|
||||
handler: async (args, ctx) => {
|
||||
await commandAgent(args, ctx);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("agents", {
|
||||
description: "Listar agentes disponibles y su estado",
|
||||
handler: async (_args, ctx) => {
|
||||
await commandAgent("list", ctx);
|
||||
},
|
||||
});
|
||||
|
||||
// Atajos directos por agente (frontmatter `shortcut:`, p. ej. f5, f6).
|
||||
// Se registran al cargar la extensión para que pi los recoja al construir
|
||||
// los keybindings; tras editar un .md usa /reload para recargarlos.
|
||||
{
|
||||
if (defs.length === 0) defs = loadAgentDefinitions(process.cwd());
|
||||
const claimed = new Set<string>();
|
||||
for (const agent of defs) {
|
||||
for (const key of agent.shortcuts) {
|
||||
if (claimed.has(key)) continue;
|
||||
claimed.add(key);
|
||||
try {
|
||||
pi.registerShortcut(key as Parameters<typeof pi.registerShortcut>[0], {
|
||||
description: `Cambiar al agente ${agent.name}`,
|
||||
handler: async (ctx) => {
|
||||
const target = findAgent(defs, agent.name);
|
||||
if (target) await switchTo(target, ctx);
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// Atajo inválido o en conflicto: no debe romper la carga.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nota de diseño: los cambios de modelo de /model, Ctrl+P o de otras
|
||||
// extensiones NO se convierten en overrides (un evento tardío se atribuía
|
||||
// al agente equivocado e intercambiaba los modelos). Solo `/agent model`
|
||||
// crea overrides; el modelo del agente se reafirma en cada run.
|
||||
|
||||
// Defensa en profundidad: la allowlist de herramientas y el modo
|
||||
// solo lectura también se aplican a nivel de llamada.
|
||||
pi.on("tool_call", async (event: ToolCallEvent) => {
|
||||
const agent = current();
|
||||
if (!agent) return;
|
||||
|
||||
if (agent.tools && agent.tools.length > 0 && !agent.tools.includes(event.toolName)) {
|
||||
return {
|
||||
block: true,
|
||||
reason: `Agente "${agent.name}": la herramienta "${event.toolName}" no está en su lista permitida (${agent.tools.join(", ")}).`,
|
||||
};
|
||||
}
|
||||
|
||||
if (agent.acceptanceRole === "read-only") {
|
||||
if (event.toolName === "edit" || event.toolName === "write") {
|
||||
return {
|
||||
block: true,
|
||||
reason: `Agente "${agent.name}": modo solo lectura; "${event.toolName}" está bloqueado. Cambia de agente con /agent para implementar.`,
|
||||
};
|
||||
}
|
||||
if (event.toolName === "bash" || event.toolName === "powershell") {
|
||||
const input = event.input as { command?: unknown };
|
||||
const command = typeof input?.command === "string" ? input.command : "";
|
||||
if (!isReadOnlySafeCommand(command)) {
|
||||
return {
|
||||
block: true,
|
||||
reason: `Agente "${agent.name}": modo solo lectura; el comando no está en la lista de solo lectura.\nComando: ${command}\nUsa /agent para cambiar a un agente con permisos de escritura.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// Aplicar la política del agente activo en cada run: prompt, modelo,
|
||||
// thinking y herramientas (idempotente).
|
||||
pi.on("before_agent_start", async (event, ctx) => {
|
||||
const agent = current();
|
||||
if (!agent) return;
|
||||
|
||||
const opts = event.systemPromptOptions;
|
||||
if (agent.systemPrompt) {
|
||||
if (agent.systemPromptMode === "replace") {
|
||||
opts.customPrompt = agent.systemPrompt;
|
||||
} else if (!opts.appendSystemPrompt.includes(agent.systemPrompt)) {
|
||||
opts.appendSystemPrompt = opts.appendSystemPrompt
|
||||
? `${opts.appendSystemPrompt}\n\n${agent.systemPrompt}`
|
||||
: agent.systemPrompt;
|
||||
}
|
||||
}
|
||||
if (!agent.inheritProjectContext) opts.contextFiles = [];
|
||||
if (!agent.inheritSkills) opts.skills = [];
|
||||
if (agent.tools?.length) {
|
||||
const registered = new Set(pi.getAllTools().map((t) => t.name));
|
||||
opts.selectedTools = agent.tools.filter((n) => registered.has(n));
|
||||
}
|
||||
|
||||
await ensureApplied(agent, ctx, false);
|
||||
});
|
||||
|
||||
// Cargar definiciones y restaurar el agente de la sesión.
|
||||
pi.on("session_start", async (event, ctx) => {
|
||||
defs = loadAgentDefinitions(ctx.cwd);
|
||||
modelOverrides.clear();
|
||||
thinkingOverrides.clear();
|
||||
// Restaurar antes de soltar el baseline: si la sesión/recarga arranca con
|
||||
// un agente de solo lectura activo, soltarlo sin restaurar perdería la
|
||||
// lista base y build ya no podría recuperar sus herramientas.
|
||||
if (toolsBaseline !== undefined) {
|
||||
pi.setActiveTools(toolsBaseline);
|
||||
toolsBaseline = undefined;
|
||||
}
|
||||
lastError = "";
|
||||
|
||||
const entries = ctx.sessionManager.getEntries() as Array<{
|
||||
type?: string;
|
||||
customType?: string;
|
||||
data?: PersistedState;
|
||||
}>;
|
||||
const last = [...entries]
|
||||
.reverse()
|
||||
.find((e) => e.type === "custom" && e.customType === ENTRY_TYPE && e.data);
|
||||
|
||||
// Solo se aplican overrides explícitos ({value, base, explicit:true});
|
||||
// los strings legacy y los registros de versiones previas (autograbados)
|
||||
// se ignoran: pueden estar contaminados y sombrear el modelo del archivo.
|
||||
if (last?.data?.models) {
|
||||
for (const [name, o] of Object.entries(last.data.models)) {
|
||||
if (o && typeof o === "object" && o.explicit === true && typeof o.value === "string") {
|
||||
modelOverrides.set(name, o);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (last?.data?.thinking) {
|
||||
for (const [name, o] of Object.entries(last.data.thinking)) {
|
||||
if (o && typeof o === "object" && o.explicit === true && typeof o.value === "string") {
|
||||
thinkingOverrides.set(name, o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prioridad: flag --agent > agente persistido > agente por defecto.
|
||||
const flagAgent = pi.getFlag("agent");
|
||||
const persisted = last?.data?.active;
|
||||
if (typeof flagAgent === "string" && findAgent(defs, flagAgent)) {
|
||||
activeName = findAgent(defs, flagAgent)!.name;
|
||||
} else if (persisted && findAgent(defs, persisted)) {
|
||||
activeName = findAgent(defs, persisted)!.name;
|
||||
} else {
|
||||
activeName = defaultAgentName();
|
||||
}
|
||||
|
||||
const agent = current();
|
||||
if (agent) await ensureApplied(agent, ctx, event.reason === "startup" || event.reason === "new");
|
||||
persist();
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user