feat(ext): wire index.ts for goal-driven-loop extension

This commit is contained in:
张宗平
2026-06-10 17:35:26 +08:00
parent 60c88217ff
commit b5cabceb72
+137
View File
@@ -0,0 +1,137 @@
/**
* goal-driven-loop extension for Pi.
*
* Wires together trigger detection, state injection, and timed
* auto-defaults to eliminate unnecessary user interaction.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { join } from "node:path";
import { existsSync, unlinkSync } from "node:fs";
import { detectTrigger, isCoarseGrained } from "./trigger-detector.js";
import {
readGoalState,
buildInjectionText,
buildContinuationPrompt,
} from "./state-injector.js";
import {
promptStaleState,
notifyCoarseGrainedDelegation,
notifyStagnation,
shouldNotifyStagnation,
type UIContext,
} from "./ui-prompts.js";
const CONTEXT_TENSION_THRESHOLD = 0.8;
const ABANDON_KEYWORDS = ["abandon", "cancel", "不要了", "放弃", "停"];
export default function (pi: ExtensionAPI) {
// ---- Trigger detection on user input ----
pi.on("input", async (event, ctx) => {
if (event.source === "extension") {
return { action: "continue" };
}
// Check for explicit abandon first
const lowerText = event.text.toLowerCase();
const isAbandon = ABANDON_KEYWORDS.some((kw) =>
lowerText.includes(kw.toLowerCase())
);
if (isAbandon) {
const state = readGoalState(ctx.cwd);
if (state) {
handleAbandon(ctx);
return { action: "continue" };
}
}
// Try to detect a goal-setting trigger
const match = detectTrigger(event.text);
if (!match) {
return { action: "continue" };
}
// Trigger detected — check stale state
const existingState = readGoalState(ctx.cwd);
if (existingState && existingState.status === "in-progress") {
const ui = buildUIContext(ctx);
const choice = await promptStaleState(ui);
if (choice === "abandon") {
handleAbandon(ctx);
return { action: "continue" };
}
if (choice === "replace") {
// Let the skill handle replacement on the next turn
return { action: "continue" };
}
// "continue" or undefined → let the skill resume
}
// Granularity check
if (isCoarseGrained(match.goalText)) {
const ui = buildUIContext(ctx);
notifyCoarseGrainedDelegation(ui, match.goalText);
pi.sendUserMessage(`/skill:comet-open ${match.goalText}`, {
deliverAs: "followUp",
});
return { action: "continue" };
}
// Fine-grained — let the skill handle it
return { action: "continue" };
});
// ---- State injection on every agent start ----
pi.on("before_agent_start", async (event, ctx) => {
const state = readGoalState(ctx.cwd);
if (!state) {
return;
}
// Context tension check
const usage = ctx.getContextUsage();
if (
usage &&
usage.contextWindow > 0 &&
usage.tokens / usage.contextWindow > CONTEXT_TENSION_THRESHOLD
) {
const continuationMsg = buildContinuationPrompt(state);
return {
systemPrompt:
event.systemPrompt +
"\n\n" +
continuationMsg +
"\n\nContext is tight. Finalize .agents/goal/GOAL.md updates before completing this turn.",
};
}
// Inject goal state
const injection = buildInjectionText(state);
return {
systemPrompt: event.systemPrompt + "\n\n" + injection,
};
});
}
function buildUIContext(ctx: any): UIContext {
return {
hasUI: ctx.hasUI,
select: ctx.ui.select.bind(ctx.ui),
notify: ctx.ui.notify.bind(ctx.ui),
confirm: ctx.ui.confirm.bind(ctx.ui),
};
}
function handleAbandon(ctx: any): void {
const goalPath = join(ctx.cwd, ".agents", "goal", "GOAL.md");
try {
if (existsSync(goalPath)) {
unlinkSync(goalPath);
}
} catch {
// best effort
}
if (ctx.hasUI) {
ctx.ui.notify("目标已放弃", "info");
}
}