pi-goal 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export type GoalStatus = "active" | "paused" | "budget_limited" | "complete";
|
|
2
|
+
|
|
3
|
+
export type GoalState = {
|
|
4
|
+
version: 1;
|
|
5
|
+
id: string;
|
|
6
|
+
objective: string;
|
|
7
|
+
status: GoalStatus;
|
|
8
|
+
tokenBudget: number | null;
|
|
9
|
+
tokensUsed: number;
|
|
10
|
+
timeUsedSeconds: number;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
updatedAt: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type GoalEventKind = "active" | "continuation" | "paused" | "resumed" | "cleared" | "budget_limited" | "complete";
|
|
16
|
+
|
|
17
|
+
export function parseTokenBudget(input: string): { objective: string; tokenBudget: number | null; error?: string } {
|
|
18
|
+
const match = input.match(/(?:^|\s)--tokens(?:=|\s+)(\S+\s*[kKmM]?)(?:\s|$)/);
|
|
19
|
+
if (!match) return { objective: input.trim(), tokenBudget: null };
|
|
20
|
+
|
|
21
|
+
const raw = match[1].replace(/\s+/g, "");
|
|
22
|
+
const suffix = raw.slice(-1).toLowerCase();
|
|
23
|
+
const numeric = suffix === "k" || suffix === "m" ? raw.slice(0, -1) : raw;
|
|
24
|
+
const value = Number(numeric);
|
|
25
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
26
|
+
return { objective: input.trim(), tokenBudget: null, error: "Token budget must be positive." };
|
|
27
|
+
}
|
|
28
|
+
const multiplier = suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : 1;
|
|
29
|
+
const tokenBudget = Math.round(value * multiplier);
|
|
30
|
+
const objective = (input.slice(0, match.index) + " " + input.slice((match.index ?? 0) + match[0].length)).trim();
|
|
31
|
+
return { objective, tokenBudget };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeTokenBudget(value: unknown): { tokenBudget: number | null; error?: string } {
|
|
35
|
+
if (value == null) return { tokenBudget: null };
|
|
36
|
+
const tokenBudget = Math.round(Number(value));
|
|
37
|
+
if (!Number.isFinite(tokenBudget) || tokenBudget <= 0) {
|
|
38
|
+
return { tokenBudget: null, error: "tokenBudget must be a positive number when provided." };
|
|
39
|
+
}
|
|
40
|
+
return { tokenBudget };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatTokens(value: number): string {
|
|
44
|
+
if (value >= 1_000_000) return `${Math.round(value / 100_000) / 10}M`;
|
|
45
|
+
if (value >= 1_000) return `${Math.round(value / 100) / 10}K`;
|
|
46
|
+
return String(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatElapsed(seconds: number): string {
|
|
50
|
+
if (seconds < 60) return `${seconds}s`;
|
|
51
|
+
const minutes = Math.floor(seconds / 60);
|
|
52
|
+
if (minutes < 60) return `${minutes}m`;
|
|
53
|
+
const hours = Math.floor(minutes / 60);
|
|
54
|
+
const remMinutes = minutes % 60;
|
|
55
|
+
return remMinutes ? `${hours}h ${remMinutes}m` : `${hours}h`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function statusLine(state: GoalState | null): string | undefined {
|
|
59
|
+
if (!state) return undefined;
|
|
60
|
+
const budget = state.tokenBudget ? ` (${formatTokens(state.tokensUsed)} / ${formatTokens(state.tokenBudget)})` : ` (${formatElapsed(state.timeUsedSeconds)})`;
|
|
61
|
+
if (state.status === "active") return `Pursuing goal${budget}`;
|
|
62
|
+
if (state.status === "paused") return "Goal paused (/goal resume)";
|
|
63
|
+
if (state.status === "budget_limited") return state.tokenBudget ? `Goal unmet${budget}` : "Goal abandoned";
|
|
64
|
+
return `Goal achieved${budget}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function goalUsage(state: GoalState): string {
|
|
68
|
+
if (state.tokenBudget != null) return `${formatTokens(state.tokensUsed)} / ${formatTokens(state.tokenBudget)} tokens`;
|
|
69
|
+
return formatElapsed(state.timeUsedSeconds);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function truncateObjective(objective: string, max = 96): string {
|
|
73
|
+
const singleLine = objective.replace(/\s+/g, " ").trim();
|
|
74
|
+
return singleLine.length > max ? `${singleLine.slice(0, max - 1)}…` : singleLine;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function goalEventStatus(kind: GoalEventKind): string {
|
|
78
|
+
const labels: Record<GoalEventKind, string> = {
|
|
79
|
+
active: "active",
|
|
80
|
+
continuation: "continuing",
|
|
81
|
+
paused: "paused",
|
|
82
|
+
resumed: "resumed",
|
|
83
|
+
cleared: "cleared",
|
|
84
|
+
budget_limited: "budget reached",
|
|
85
|
+
complete: "achieved",
|
|
86
|
+
};
|
|
87
|
+
return labels[kind];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createGoalState(objective: string, tokenBudget: number | null, now = Date.now(), random = Math.random()): GoalState {
|
|
91
|
+
return {
|
|
92
|
+
version: 1,
|
|
93
|
+
id: `${now}-${random.toString(16).slice(2)}`,
|
|
94
|
+
objective,
|
|
95
|
+
status: "active",
|
|
96
|
+
tokenBudget,
|
|
97
|
+
tokensUsed: 0,
|
|
98
|
+
timeUsedSeconds: 0,
|
|
99
|
+
createdAt: now,
|
|
100
|
+
updatedAt: now,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function accountGoalTurn(state: GoalState, tokenDelta: number, elapsedSeconds: number, now = Date.now()): GoalState {
|
|
105
|
+
let next: GoalState = {
|
|
106
|
+
...state,
|
|
107
|
+
tokensUsed: state.tokensUsed + Math.max(0, tokenDelta),
|
|
108
|
+
timeUsedSeconds: state.timeUsedSeconds + Math.max(0, elapsedSeconds),
|
|
109
|
+
updatedAt: now,
|
|
110
|
+
};
|
|
111
|
+
if (next.status === "active" && next.tokenBudget != null && next.tokensUsed >= next.tokenBudget) {
|
|
112
|
+
next = { ...next, status: "budget_limited" };
|
|
113
|
+
}
|
|
114
|
+
return next;
|
|
115
|
+
}
|
|
@@ -1,102 +1,29 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Box, Spacer, Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
accountGoalTurn,
|
|
5
|
+
createGoalState,
|
|
6
|
+
goalEventStatus,
|
|
7
|
+
goalUsage,
|
|
8
|
+
parseTokenBudget,
|
|
9
|
+
statusLine,
|
|
10
|
+
truncateObjective,
|
|
11
|
+
type GoalEventKind,
|
|
12
|
+
type GoalState,
|
|
13
|
+
type GoalStatus,
|
|
14
|
+
normalizeTokenBudget,
|
|
15
|
+
} from "./goal-state";
|
|
16
|
+
import { tokenDeltaFromUsage, type UsageSnapshot } from "./usage";
|
|
3
17
|
|
|
4
18
|
const CUSTOM_TYPE = "pi-goal";
|
|
5
19
|
const EVENT_TYPE = "pi-goal-event";
|
|
6
20
|
|
|
7
|
-
type GoalStatus = "active" | "paused" | "budget_limited" | "complete";
|
|
8
|
-
|
|
9
|
-
type GoalState = {
|
|
10
|
-
version: 1;
|
|
11
|
-
id: string;
|
|
12
|
-
objective: string;
|
|
13
|
-
status: GoalStatus;
|
|
14
|
-
tokenBudget: number | null;
|
|
15
|
-
tokensUsed: number;
|
|
16
|
-
timeUsedSeconds: number;
|
|
17
|
-
createdAt: number;
|
|
18
|
-
updatedAt: number;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type GoalEventKind = "active" | "continuation" | "paused" | "resumed" | "cleared" | "budget_limited" | "complete";
|
|
22
|
-
|
|
23
21
|
let goal: GoalState | null = null;
|
|
24
22
|
let statusBarEnabled = true;
|
|
25
23
|
let activeTurnStartedAt: number | null = null;
|
|
24
|
+
let activeGoalThisTurnId: string | null = null;
|
|
26
25
|
let continuationQueued = false;
|
|
27
26
|
|
|
28
|
-
function parseTokenBudget(input: string): { objective: string; tokenBudget: number | null; error?: string } {
|
|
29
|
-
const match = input.match(/(?:^|\s)--tokens(?:=|\s+)([0-9]+(?:\.[0-9]+)?\s*[kKmM]?)(?:\s|$)/);
|
|
30
|
-
if (!match) return { objective: input.trim(), tokenBudget: null };
|
|
31
|
-
|
|
32
|
-
const raw = match[1].replace(/\s+/g, "");
|
|
33
|
-
const suffix = raw.slice(-1).toLowerCase();
|
|
34
|
-
const numeric = suffix === "k" || suffix === "m" ? raw.slice(0, -1) : raw;
|
|
35
|
-
const value = Number(numeric);
|
|
36
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
37
|
-
return { objective: input.trim(), tokenBudget: null, error: "Token budget must be positive." };
|
|
38
|
-
}
|
|
39
|
-
const multiplier = suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : 1;
|
|
40
|
-
const tokenBudget = Math.round(value * multiplier);
|
|
41
|
-
const objective = (input.slice(0, match.index) + " " + input.slice((match.index ?? 0) + match[0].length)).trim();
|
|
42
|
-
return { objective, tokenBudget };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function formatTokens(value: number): string {
|
|
46
|
-
if (value >= 1_000_000) return `${Math.round(value / 100_000) / 10}M`;
|
|
47
|
-
if (value >= 1_000) return `${Math.round(value / 100) / 10}K`;
|
|
48
|
-
return String(value);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function formatElapsed(seconds: number): string {
|
|
52
|
-
if (seconds < 60) return `${seconds}s`;
|
|
53
|
-
const minutes = Math.floor(seconds / 60);
|
|
54
|
-
if (minutes < 60) return `${minutes}m`;
|
|
55
|
-
const hours = Math.floor(minutes / 60);
|
|
56
|
-
const remMinutes = minutes % 60;
|
|
57
|
-
return remMinutes ? `${hours}h ${remMinutes}m` : `${hours}h`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function statusLine(state: GoalState | null): string | undefined {
|
|
61
|
-
if (!state) return undefined;
|
|
62
|
-
const budget = state.tokenBudget ? ` (${formatTokens(state.tokensUsed)} / ${formatTokens(state.tokenBudget)})` : ` (${formatElapsed(state.timeUsedSeconds)})`;
|
|
63
|
-
if (state.status === "active") return `Pursuing goal${budget}`;
|
|
64
|
-
if (state.status === "paused") return "Goal paused (/goal resume)";
|
|
65
|
-
if (state.status === "budget_limited") return state.tokenBudget ? `Goal unmet${budget}` : "Goal abandoned";
|
|
66
|
-
return `Goal achieved${budget}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function goalUsage(state: GoalState): string {
|
|
70
|
-
if (state.tokenBudget != null) return `${formatTokens(state.tokensUsed)} / ${formatTokens(state.tokenBudget)} tokens`;
|
|
71
|
-
return formatElapsed(state.timeUsedSeconds);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
type UsageSnapshot = { totalTokens?: number; input?: number; output?: number } | null | undefined;
|
|
75
|
-
|
|
76
|
-
function tokenDeltaFromUsage(usage: UsageSnapshot): number {
|
|
77
|
-
if (!usage) return 0;
|
|
78
|
-
if (typeof usage.totalTokens === "number") return Math.max(0, usage.totalTokens);
|
|
79
|
-
return Math.max(0, (Number(usage.input) || 0) + (Number(usage.output) || 0));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function truncateObjective(objective: string, max = 96): string {
|
|
83
|
-
const singleLine = objective.replace(/\s+/g, " ").trim();
|
|
84
|
-
return singleLine.length > max ? `${singleLine.slice(0, max - 1)}…` : singleLine;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function goalEventStatus(kind: GoalEventKind): string {
|
|
88
|
-
const labels: Record<GoalEventKind, string> = {
|
|
89
|
-
active: "active",
|
|
90
|
-
continuation: "continuing",
|
|
91
|
-
paused: "paused",
|
|
92
|
-
resumed: "resumed",
|
|
93
|
-
cleared: "cleared",
|
|
94
|
-
budget_limited: "budget reached",
|
|
95
|
-
complete: "achieved",
|
|
96
|
-
};
|
|
97
|
-
return labels[kind];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
27
|
// The `content` field is what the LLM sees in the conversation history.
|
|
101
28
|
// Every goal event MUST carry actionable text — never a cryptic marker.
|
|
102
29
|
// The TUI renderer collapses long bodies down to a compact badge for humans.
|
|
@@ -160,20 +87,24 @@ function updateStatusBar(ctx: ExtensionContext) {
|
|
|
160
87
|
ctx.ui.setStatus(CUSTOM_TYPE, statusBarEnabled ? statusLine(goal) ?? "" : "");
|
|
161
88
|
}
|
|
162
89
|
|
|
163
|
-
const
|
|
90
|
+
const ACTIVE_GOAL_TOOL_NAMES = ["get_goal", "update_goal"];
|
|
164
91
|
|
|
165
|
-
// Expose
|
|
166
|
-
//
|
|
167
|
-
//
|
|
92
|
+
// Expose read/update tools to the LLM only while a goal is actively being pursued.
|
|
93
|
+
// Keep create_goal available so the model can start a goal when explicitly asked,
|
|
94
|
+
// but rely on its tool contract to reject inferred goals and existing goals.
|
|
168
95
|
function syncGoalTools(pi: ExtensionAPI) {
|
|
169
|
-
const
|
|
96
|
+
const wantActiveTools = goal?.status === "active";
|
|
170
97
|
const active = new Set(pi.getActiveTools());
|
|
171
|
-
|
|
98
|
+
active.add("create_goal");
|
|
99
|
+
for (const name of ACTIVE_GOAL_TOOL_NAMES) (wantActiveTools ? active.add(name) : active.delete(name));
|
|
172
100
|
pi.setActiveTools(Array.from(active));
|
|
173
101
|
}
|
|
174
102
|
|
|
175
103
|
function persist(pi: ExtensionAPI, ctx: ExtensionContext, next: GoalState | null) {
|
|
176
104
|
goal = next;
|
|
105
|
+
if (next?.status !== "active") {
|
|
106
|
+
continuationQueued = false;
|
|
107
|
+
}
|
|
177
108
|
pi.appendEntry(CUSTOM_TYPE, { goal: next, statusBarEnabled });
|
|
178
109
|
updateStatusBar(ctx);
|
|
179
110
|
syncGoalTools(pi);
|
|
@@ -212,7 +143,7 @@ Before deciding that the goal is achieved, perform a completion audit against th
|
|
|
212
143
|
- Identify any missing, incomplete, weakly verified, or uncovered requirement.
|
|
213
144
|
- Treat uncertainty as not achieved; do more verification or continue the work.
|
|
214
145
|
|
|
215
|
-
Do not rely on intent, partial progress, elapsed effort, memory of earlier work, or a plausible final answer as proof of completion. Only mark the goal achieved when the audit shows that the objective has actually been achieved and no required work remains. If any requirement is missing, incomplete, or unverified, keep working instead of marking the goal complete. If the objective is achieved, call update_goal with status \"complete\".
|
|
146
|
+
Do not rely on intent, partial progress, elapsed effort, memory of earlier work, or a plausible final answer as proof of completion. Only mark the goal achieved when the audit shows that the objective has actually been achieved and no required work remains. If any requirement is missing, incomplete, or unverified, keep working instead of marking the goal complete. If the objective is achieved, call update_goal with status \"complete\" so usage accounting is preserved.
|
|
216
147
|
|
|
217
148
|
Do not call update_goal unless the goal is complete. Do not mark a goal complete merely because the budget is nearly exhausted or because you are stopping work.`;
|
|
218
149
|
}
|
|
@@ -287,10 +218,64 @@ export default function piGoal(pi: ExtensionAPI) {
|
|
|
287
218
|
},
|
|
288
219
|
});
|
|
289
220
|
|
|
221
|
+
pi.registerTool({
|
|
222
|
+
name: "create_goal",
|
|
223
|
+
label: "Create Goal",
|
|
224
|
+
description: "Create a new active thread goal only when explicitly requested. A goal must be a durable, evidence-checkable work contract: outcome, verification surface, constraints, boundaries, iteration policy, and blocked stop condition. Fails if a goal already exists.",
|
|
225
|
+
promptSnippet: "Create a pi-goal objective only when the user explicitly requests goal mode",
|
|
226
|
+
promptGuidelines: [
|
|
227
|
+
"Use create_goal only when the user explicitly asks to set/start/follow a goal, or system/developer instructions require a goal.",
|
|
228
|
+
"Do not infer goals from ordinary coding tasks or one-off prompts.",
|
|
229
|
+
"Before creating a goal, turn the request into a concrete objective with: outcome, verification surface, constraints, boundaries, iteration policy, and blocked stop condition.",
|
|
230
|
+
"Use this objective shape when possible: <desired end state>, verified by <specific evidence>, while preserving <constraints>. Use <allowed scope/tools> and avoid <forbidden scope>. Between iterations, <how to choose the next action and what to re-check>. If blocked or no defensible path remains, stop with <evidence gathered, attempted paths, blocker, and next input needed>.",
|
|
231
|
+
"Prefer a self-contained objective that survives continuation turns and context compaction.",
|
|
232
|
+
"Do not create vague goals like 'improve this' or 'finish the feature'; ask a clarifying question if missing success criteria or boundaries materially affect the contract.",
|
|
233
|
+
"Set tokenBudget only when the user explicitly requested a token budget.",
|
|
234
|
+
],
|
|
235
|
+
parameters: {
|
|
236
|
+
type: "object",
|
|
237
|
+
properties: {
|
|
238
|
+
objective: {
|
|
239
|
+
type: "string",
|
|
240
|
+
description: "The concrete objective to pursue as an active thread goal.",
|
|
241
|
+
},
|
|
242
|
+
tokenBudget: {
|
|
243
|
+
type: "number",
|
|
244
|
+
description: "Optional positive token budget for the goal, only when explicitly requested.",
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
required: ["objective"],
|
|
248
|
+
additionalProperties: false,
|
|
249
|
+
} as any,
|
|
250
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
251
|
+
if (goal) {
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: "text", text: "Cannot create a new goal because this thread already has a goal. Use update_goal only when the existing goal is complete, or ask the user to clear/replace it." }],
|
|
254
|
+
isError: true,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const objective = typeof params.objective === "string" ? params.objective.trim() : "";
|
|
258
|
+
if (!objective) {
|
|
259
|
+
return { content: [{ type: "text", text: "objective is required." }], isError: true };
|
|
260
|
+
}
|
|
261
|
+
const parsedBudget = normalizeTokenBudget(params.tokenBudget);
|
|
262
|
+
if (parsedBudget.error) {
|
|
263
|
+
return { content: [{ type: "text", text: parsedBudget.error }], isError: true };
|
|
264
|
+
}
|
|
265
|
+
const next = createGoalState(objective, parsedBudget.tokenBudget);
|
|
266
|
+
persist(pi, ctx, next);
|
|
267
|
+
emitGoalEvent(pi, "active", next, { triggerTurn: ctx.isIdle() });
|
|
268
|
+
return {
|
|
269
|
+
content: [{ type: "text", text: JSON.stringify({ goal: next, remainingTokens: next.tokenBudget }, null, 2) }],
|
|
270
|
+
details: { goal: next },
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
290
275
|
pi.registerTool({
|
|
291
276
|
name: "update_goal",
|
|
292
277
|
label: "Update Goal",
|
|
293
|
-
description: "Mark the current thread goal complete. This tool only accepts status=complete.",
|
|
278
|
+
description: "Mark the current thread goal complete. This tool only accepts status=complete and final turn usage is accounted by the runtime.",
|
|
294
279
|
promptSnippet: "Mark the current goal complete after a strict completion audit",
|
|
295
280
|
promptGuidelines: [
|
|
296
281
|
"Use update_goal only when the current pi-goal objective is fully achieved and verified against concrete evidence.",
|
|
@@ -388,17 +373,7 @@ export default function piGoal(pi: ExtensionAPI) {
|
|
|
388
373
|
const ok = await ctx.ui.confirm("Replace goal?", `Current: ${goal.objective}\n\nNew: ${parsed.objective}`);
|
|
389
374
|
if (!ok) return;
|
|
390
375
|
}
|
|
391
|
-
const next
|
|
392
|
-
version: 1,
|
|
393
|
-
id: `${now}-${Math.random().toString(16).slice(2)}`,
|
|
394
|
-
objective: parsed.objective,
|
|
395
|
-
status: "active",
|
|
396
|
-
tokenBudget: parsed.tokenBudget,
|
|
397
|
-
tokensUsed: 0,
|
|
398
|
-
timeUsedSeconds: 0,
|
|
399
|
-
createdAt: now,
|
|
400
|
-
updatedAt: now,
|
|
401
|
-
};
|
|
376
|
+
const next = createGoalState(parsed.objective, parsed.tokenBudget, now);
|
|
402
377
|
persist(pi, ctx, next);
|
|
403
378
|
emitGoalEvent(pi, "active", next, { triggerTurn: ctx.isIdle() });
|
|
404
379
|
},
|
|
@@ -410,7 +385,8 @@ export default function piGoal(pi: ExtensionAPI) {
|
|
|
410
385
|
statusBarEnabled = restored.statusBarEnabled;
|
|
411
386
|
continuationQueued = false;
|
|
412
387
|
activeTurnStartedAt = null;
|
|
413
|
-
|
|
388
|
+
activeGoalThisTurnId = null;
|
|
389
|
+
// Keep create_goal available, and hide read/update tools unless there is an active goal to pursue.
|
|
414
390
|
syncGoalTools(pi);
|
|
415
391
|
if (goal?.status === "active" && event.reason === "reload") {
|
|
416
392
|
// Reload pauses an active goal so it does not silently resume.
|
|
@@ -438,22 +414,20 @@ export default function piGoal(pi: ExtensionAPI) {
|
|
|
438
414
|
|
|
439
415
|
pi.on("turn_start", (_event, _ctx) => {
|
|
440
416
|
activeTurnStartedAt = Date.now();
|
|
417
|
+
activeGoalThisTurnId = goal?.status === "active" ? goal.id : null;
|
|
441
418
|
});
|
|
442
419
|
|
|
443
420
|
pi.on("turn_end", (event, ctx) => {
|
|
444
|
-
if (!goal ||
|
|
421
|
+
if (!goal || activeGoalThisTurnId !== goal.id) {
|
|
422
|
+
activeTurnStartedAt = null;
|
|
423
|
+
activeGoalThisTurnId = null;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
445
426
|
const elapsed = activeTurnStartedAt ? Math.max(0, Math.round((Date.now() - activeTurnStartedAt) / 1000)) : 0;
|
|
446
427
|
activeTurnStartedAt = null;
|
|
428
|
+
activeGoalThisTurnId = null;
|
|
447
429
|
const tokenDelta = tokenDeltaFromUsage((event.message as { usage?: UsageSnapshot } | undefined)?.usage);
|
|
448
|
-
|
|
449
|
-
...goal,
|
|
450
|
-
tokensUsed: goal.tokensUsed + tokenDelta,
|
|
451
|
-
timeUsedSeconds: goal.timeUsedSeconds + elapsed,
|
|
452
|
-
updatedAt: Date.now(),
|
|
453
|
-
};
|
|
454
|
-
if (next.tokenBudget != null && next.tokensUsed >= next.tokenBudget) {
|
|
455
|
-
next = { ...next, status: "budget_limited" };
|
|
456
|
-
}
|
|
430
|
+
const next = accountGoalTurn(goal, tokenDelta, elapsed);
|
|
457
431
|
persist(pi, ctx, next);
|
|
458
432
|
if (next.status === "budget_limited") {
|
|
459
433
|
emitGoalEvent(pi, "budget_limited", next, { triggerTurn: true, deliverAs: "followUp" });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type UsageSnapshot = {
|
|
2
|
+
totalTokens?: number;
|
|
3
|
+
input?: number;
|
|
4
|
+
output?: number;
|
|
5
|
+
cacheRead?: number;
|
|
6
|
+
cacheWrite?: number;
|
|
7
|
+
} | null | undefined;
|
|
8
|
+
|
|
9
|
+
export function tokenDeltaFromUsage(usage: UsageSnapshot): number {
|
|
10
|
+
if (!usage) return 0;
|
|
11
|
+
if (typeof usage.totalTokens === "number") return Math.max(0, usage.totalTokens);
|
|
12
|
+
const input = Number(usage.input) || 0;
|
|
13
|
+
const output = Number(usage.output) || 0;
|
|
14
|
+
const cacheRead = Number(usage.cacheRead) || 0;
|
|
15
|
+
const cacheWrite = Number(usage.cacheWrite) || 0;
|
|
16
|
+
return Math.max(0, input + output + cacheRead + cacheWrite);
|
|
17
|
+
}
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Persistent autonomous goals for [pi](https://github.com/badlogic/pi-mono).
|
|
6
6
|
|
|
7
|
-
`pi-goal` adds a `/goal` command and goal tools so Pi can keep working toward a long-running objective until the goal is complete, paused, cleared, or token-budget-limited.
|
|
7
|
+
`pi-goal` adds a `/goal` command and goal tools so Pi can keep working toward a long-running, thread-scoped objective until the goal is complete, paused, cleared, or token-budget-limited.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -23,6 +23,7 @@ pi install git:github.com/Michaelliv/pi-goal
|
|
|
23
23
|
```text
|
|
24
24
|
/goal improve benchmark coverage until the suite has strong evidence
|
|
25
25
|
/goal --tokens 50k finish the migration and verify tests
|
|
26
|
+
/goal
|
|
26
27
|
/goal status
|
|
27
28
|
/goal pause
|
|
28
29
|
/goal resume
|
|
@@ -36,12 +37,14 @@ The same Pi agent keeps running normal turns in the same session context until i
|
|
|
36
37
|
|
|
37
38
|
## What it adds
|
|
38
39
|
|
|
40
|
+
- `pi-goal-writer` skill: draft and review strong `/goal` objectives with evidence-based success criteria
|
|
39
41
|
- `/goal [--tokens 50k] <objective>`: set or replace a goal
|
|
40
|
-
- `/goal status`: show the current goal
|
|
42
|
+
- `/goal` or `/goal status`: show the current goal
|
|
41
43
|
- `/goal pause`: stop autonomous continuation without deleting the goal
|
|
42
44
|
- `/goal resume`: reactivate a paused goal
|
|
43
45
|
- `/goal clear`: remove the goal
|
|
44
46
|
- `/goal statusbar on|off`: show or hide the footer status line
|
|
47
|
+
- `create_goal` tool: model can create a goal only when explicitly requested and only if no goal exists
|
|
45
48
|
- `get_goal` tool: read current goal state
|
|
46
49
|
- `update_goal` tool: model can only mark the goal `complete`
|
|
47
50
|
- `get_goal` and `update_goal` are only exposed to the model while a goal is `active`; paused, cleared, complete, and budget-limited goals hide them so unrelated sessions are not tempted to call them
|
|
@@ -62,7 +65,7 @@ The same Pi agent keeps running normal turns in the same session context until i
|
|
|
62
65
|
|
|
63
66
|
## Completion behavior
|
|
64
67
|
|
|
65
|
-
The model is instructed to audit completion against real evidence before calling `update_goal`. The `update_goal` tool deliberately accepts only `status: "complete"`; pausing, resuming, clearing, and budget limiting are controlled by the user or extension runtime.
|
|
68
|
+
The model is instructed to audit completion against real evidence before calling `update_goal`. The `update_goal` tool deliberately accepts only `status: "complete"`; pausing, resuming, clearing, and budget limiting are controlled by the user or extension runtime. The final turn is still accounted even when the model completes the goal mid-turn.
|
|
66
69
|
|
|
67
70
|
## State
|
|
68
71
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Persistent autonomous goals for pi — /goal loops until complete, paused, or budget-limited",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"keywords": [
|
|
@@ -12,20 +12,28 @@
|
|
|
12
12
|
],
|
|
13
13
|
"files": [
|
|
14
14
|
".pi/",
|
|
15
|
+
"skills/",
|
|
15
16
|
"README.md",
|
|
16
17
|
"LICENSE"
|
|
17
18
|
],
|
|
18
19
|
"pi": {
|
|
19
20
|
"extensions": [
|
|
20
21
|
".pi/extensions/pi-goal"
|
|
22
|
+
],
|
|
23
|
+
"skills": [
|
|
24
|
+
"skills"
|
|
21
25
|
]
|
|
22
26
|
},
|
|
23
27
|
"peerDependencies": {
|
|
24
28
|
"@mariozechner/pi-coding-agent": "*",
|
|
25
29
|
"@mariozechner/pi-tui": "*"
|
|
26
30
|
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"jiti": "^2.7.0"
|
|
33
|
+
},
|
|
27
34
|
"scripts": {
|
|
28
35
|
"check": "pi --no-extensions -e ./.pi/extensions/pi-goal/index.ts --list-models __pi_goal_load_check__",
|
|
36
|
+
"test": "node --test test/**/*.test.cjs",
|
|
29
37
|
"pack:dry": "npm pack --dry-run"
|
|
30
38
|
},
|
|
31
39
|
"author": "michaelliv",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pi-goal-writer
|
|
3
|
+
description: Drafts and reviews strong /goal objectives for Pi pi-goal and compatible goal-mode agents. Use when the user asks to write, improve, audit, or meta-prompt a long-running agent goal with clear success criteria, verification, constraints, iteration policy, and blocked stop conditions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pi Goal Writer
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Write `/goal` prompts that are fit for persistent autonomous work. A goal is not a bigger ordinary prompt; it is a completion contract. The agent will keep using it to decide what to do next and whether it can honestly stop, so the goal must define the desired end state, the evidence that proves it, the constraints that must remain true, and when to stop as blocked instead of drifting.
|
|
11
|
+
|
|
12
|
+
Use this skill for Pi `pi-goal` first. The same goal-writing principles also apply to Codex Goal mode and compatible `/goal` workflows.
|
|
13
|
+
|
|
14
|
+
## Core rule
|
|
15
|
+
|
|
16
|
+
Never produce a vague goal such as “make this better,” “finish the feature,” or “improve the codebase.” Turn the user’s rough intent into a goal with auditable completion criteria.
|
|
17
|
+
|
|
18
|
+
A strong goal includes six parts:
|
|
19
|
+
|
|
20
|
+
1. **Outcome** — what must be true when the work is done.
|
|
21
|
+
2. **Verification surface** — tests, commands, benchmark output, report, artifact, diff audit, PR state, screenshots, logs, or other concrete evidence.
|
|
22
|
+
3. **Constraints** — what must not regress or be changed.
|
|
23
|
+
4. **Boundaries** — files, directories, tools, systems, data sources, or permissions the agent may or may not use.
|
|
24
|
+
5. **Iteration policy** — how the agent should choose the next action after each attempt.
|
|
25
|
+
6. **Blocked stop condition** — when the agent should stop honestly, with evidence and the next needed input, instead of continuing blindly.
|
|
26
|
+
|
|
27
|
+
## Workflow
|
|
28
|
+
|
|
29
|
+
1. Default to Pi `pi-goal`. Write a Pi-compatible `/goal` command unless the user explicitly asks for another harness. The goal body can usually be reused in Codex Goal mode; Pi also supports optional token budgets such as `/goal --tokens 50k ...`.
|
|
30
|
+
2. Gather context before drafting when the task depends on a repository, issue, test suite, benchmark, PR, design, or external documentation. Read the relevant files or sources instead of inventing the verification surface.
|
|
31
|
+
3. Ask at most three clarifying questions only when missing information changes the goal contract. Prefer making safe assumptions explicit when the user is trying to move quickly.
|
|
32
|
+
4. Draft the goal as a single pasteable command, then include a short rationale or checklist showing how the six parts are covered.
|
|
33
|
+
5. For high-stakes or ambiguous work, provide two options: a narrower goal that is safer to execute and a broader goal that delegates more discovery to the agent. Recommend one.
|
|
34
|
+
|
|
35
|
+
## Goal template
|
|
36
|
+
|
|
37
|
+
Use this shape unless the user asks for a different format:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
/goal <desired end state>, verified by <specific evidence>, while preserving <constraints>. Use <allowed inputs/tools/scope> and avoid <forbidden scope>. Between iterations, <how to choose the next best action and what to re-check>. If blocked or no defensible path remains, stop with <evidence gathered, attempted paths, blocker, and next input needed>.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For Pi token budgets:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
/goal --tokens 50k <same goal contract>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Writing standards
|
|
50
|
+
|
|
51
|
+
Make the goal self-contained. It should survive context compaction and continuation turns. Include exact command names when known, but do not invent commands. Say “run the relevant project checks identified in AGENTS.md/package scripts” only when exact commands are unknown and the agent can inspect them.
|
|
52
|
+
|
|
53
|
+
Make completion evidence-based. The goal should require the agent to inspect real artifacts before declaring success: files changed, tests passed, benchmark numbers, rendered screenshots, logs, PR checks, or a written audit. Do not let “tests pass” be the only evidence unless the tests actually cover every requirement.
|
|
54
|
+
|
|
55
|
+
Bound the scope. Name excluded directories or behaviors when important, such as “do not rewrite CLI user-facing output,” “do not change public API behavior,” or “do not touch generated files except via the generator.”
|
|
56
|
+
|
|
57
|
+
Preserve honesty under uncertainty. If evidence may be unavailable, require a final report that separates confirmed findings, approximate/proxy evidence, blocked claims, and remaining uncertainty.
|
|
58
|
+
|
|
59
|
+
Prefer concrete stop language: “If blocked, stop with the exact blocker and what would unlock progress.” Avoid weak endings like “do your best.”
|
|
60
|
+
|
|
61
|
+
## Review checklist
|
|
62
|
+
|
|
63
|
+
Before returning a goal, verify it answers:
|
|
64
|
+
|
|
65
|
+
- Can the agent tell when it is done?
|
|
66
|
+
- Can the user independently audit that completion claim?
|
|
67
|
+
- Are regressions and forbidden approaches named?
|
|
68
|
+
- Does the goal allow iteration without inviting unlimited drift?
|
|
69
|
+
- Does it define what to do when tests, credentials, network, data, or product decisions block progress?
|
|
70
|
+
- Is it pasteable as one `/goal` command?
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
Weak:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
/goal improve logging
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Strong:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
/goal Implement structured runtime logging, verified by targeted logger tests, the full project check/type/test suite, and final audits showing no production console.* calls outside approved CLI/UI/logger-sink exceptions. Preserve existing operator-visible console behavior, avoid logging secrets or credentials, and keep the logger generic rather than error-only. Between iterations, inspect the diff and audit remaining catch paths before deciding the next change. If blocked, stop with the unverified requirement, evidence gathered, and the next input needed.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Weak:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
/goal fix flaky checkout test
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Strong:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
/goal Diagnose and either fix or conclusively characterize the flaky checkout test, verified by a reliable local reproduction or an evidence-backed failure analysis plus the relevant test command passing when a fix is made. Preserve public checkout behavior and avoid broad timing hacks unless the evidence shows timing is the root cause. Between iterations, record the hypothesis tested, command output, and next most likely cause. If the flake cannot be reproduced or no safe fix remains, stop with attempted reproductions, logs, suspected causes, and the missing evidence needed.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Weak:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
/goal reproduce this paper
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Strong:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
/goal Produce the strongest evidence-backed reproduction of the paper using available local resources, verified by a final claim-by-claim report and any generated artifacts or runnable checks. Attempt the headline results where feasible, label approximate reconstructions separately from exact reproductions, and do not overclaim missing seeds, checkpoints, datasets, or implementation details. Between iterations, map claims to available evidence and prioritize the highest-value verifiable claim. If exact reproduction is blocked, stop with confirmed claims, proxy evidence, blocked claims, and the specific missing materials.
|
|
108
|
+
```
|