feat(ext): wire index.ts for goal-driven-loop extension
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user