lardon3d/.pi/lardon-profiles.ts
2026-08-24 14:44:29 +02:00

314 lines
6.8 KiB
TypeScript

import type {
ExtensionAPI,
ExtensionContext,
} from "@earendil-works/pi-coding-agent";
type ProfileName =
| "local"
| "ctx"
| "review"
| "mem"
| "full"
| "codex";
interface Profile {
provider: string;
model: string;
thinking: "off" | "low";
tools: string[];
}
const profiles: Record<ProfileName, Profile> = {
/*
* Default mode.
* Qwen3 local, read-only, enough for normal repository exploration.
*/
local: {
provider: "local-lardon",
model: "lardon-local",
thinking: "off",
tools: [
"read",
"grep",
"find",
"ls",
],
},
review: {
provider: "local-lardon",
model: "lardon-local",
thinking: "off",
tools: [
"read",
"grep",
"find",
"ls",
"lsp_diagnostics",
],
},
/*
* Local + context-mode through the compact MCP proxy.
* No direct ctx_* tools are exposed.
*/
ctx: {
provider: "local-lardon",
model: "lardon-local",
thinking: "off",
tools: [
"read",
"grep",
"find",
"ls",
"lsp_diagnostics",
"subagent",
"mcp",
],
},
/*
* Local + Hermes search.
* memory_search is intentionally not enabled in the default profile
* because small local models tend to invoke it too eagerly.
*/
mem: {
provider: "local-lardon",
model: "lardon-local",
thinking: "off",
tools: [
"read",
"grep",
"find",
"ls",
"memory_search",
],
},
/*
* Local with both optional facilities.
* Use only when a task genuinely needs memory + context-mode.
*/
full: {
provider: "local-lardon",
model: "lardon-local",
thinking: "off",
tools: [
"read",
"grep",
"find",
"ls",
"lsp_diagnostics",
"subagent",
"mcp",
"memory_search",
],
},
/*
* Explicit quota-consuming mode.
*
* Codex is NEVER selected automatically.
* Subagents remain constrained separately by pi-subagents'
* local-only modelScope.
*/
codex: {
provider: "openai-codex",
model: "gpt-5.6-sol",
thinking: "low",
tools: [
"read",
"grep",
"find",
"ls",
"bash",
"edit",
"write",
"lsp_diagnostics",
"subagent",
"mcp",
"memory_search",
],
},
};
export default function lardonProfiles(pi: ExtensionAPI) {
let activeProfile: ProfileName = "local";
function validTools(requested: string[], ctx: ExtensionContext): string[] {
const available = new Set(pi.getAllTools().map((tool) => tool.name));
const valid = requested.filter((name) => available.has(name));
const missing = requested.filter((name) => !available.has(name));
if (missing.length > 0) {
ctx.ui.notify(
`Lardon profile: unavailable tools ignored: ${missing.join(", ")}`,
"warning",
);
}
return valid;
}
async function applyProfile(
name: ProfileName,
ctx: ExtensionContext,
notify = true,
): Promise<boolean> {
const profile = profiles[name];
const model = ctx.modelRegistry.find(
profile.provider,
profile.model,
);
if (!model) {
ctx.ui.notify(
`Lardon profile "${name}": model not found: ${profile.provider}/${profile.model}`,
"error",
);
return false;
}
const switched = await pi.setModel(model);
if (!switched) {
ctx.ui.notify(
`Lardon profile "${name}": model authentication unavailable`,
"error",
);
return false;
}
pi.setThinkingLevel(profile.thinking);
const tools = validTools(profile.tools, ctx);
pi.setActiveTools(tools);
activeProfile = name;
ctx.ui.setStatus(
"lardon-profile",
`Lardon:${name}`,
);
if (notify) {
ctx.ui.notify(
`Profile ${name}${profile.provider}/${profile.model} · thinking ${profile.thinking} · ${tools.length} tools`,
name === "codex" ? "warning" : "info",
);
}
return true;
}
pi.registerCommand("local", {
description: "Qwen local read-only — default Lardon3D profile",
handler: async (_args, ctx) => {
await applyProfile("local", ctx);
},
});
pi.registerCommand("ctx", {
description: "Qwen local + compact context-mode MCP proxy",
handler: async (_args, ctx) => {
await applyProfile("ctx", ctx);
},
});
pi.registerCommand("mem", {
description: "Qwen local + Hermes memory search",
handler: async (_args, ctx) => {
await applyProfile("mem", ctx);
},
});
pi.registerCommand("full", {
description: "Qwen local + context-mode + memory search",
handler: async (_args, ctx) => {
await applyProfile("full", ctx);
},
});
pi.registerCommand("codex", {
description: "Explicitly switch to Codex Sol-low and enable coding tools",
handler: async (_args, ctx) => {
if (ctx.mode === "tui") {
const confirmed = await ctx.ui.confirm(
"Use Codex quota?",
"Switch this session to openai-codex/gpt-5.6-sol (thinking: low)?",
);
if (!confirmed) {
ctx.ui.notify("Codex switch cancelled", "info");
return;
}
}
await applyProfile("codex", ctx);
},
});
pi.registerCommand("profile", {
description: "Show current Lardon3D profile and active tools",
handler: async (_args, ctx) => {
const profile = profiles[activeProfile];
ctx.ui.notify(
[
`Profile: ${activeProfile}`,
`Model: ${profile.provider}/${profile.model}`,
`Thinking: ${profile.thinking}`,
`Tools: ${pi.getActiveTools().join(", ")}`,
].join("\n"),
"info",
);
},
});
pi.registerCommand("review", {
description: "Qwen local + subagents de revue/reconnaissance",
handler: async (_args, ctx) => {
await applyProfile("review", ctx);
},
});
/*
* Fail closed.
*
* Every newly started Pi process begins LOCAL even if the previous
* session ended while Codex was active.
*
* Codex can only be entered again with an explicit /codex.
*/
/*
* Other extensions may register/reactivate their own tools after
* session_start (for example mcp and subagent_supervisor).
*
* Re-assert the selected Lardon profile immediately before every
* user-triggered agent run so the model sees exactly the intended
* tool allowlist.
*/
pi.on("before_agent_start", async (_event, ctx) => {
const profile = profiles[activeProfile];
const available = new Set(
pi.getAllTools().map((tool) => tool.name),
);
const tools = profile.tools.filter(
(name) => available.has(name),
);
pi.setActiveTools(tools);
ctx.ui.setStatus(
"lardon-profile",
`Lardon:${activeProfile}`,
);
});
pi.on("session_start", async (_event, ctx) => {
await applyProfile("local", ctx, false);
});
}