pi-goal-x 0.31.4 → 0.31.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.
- package/README.md +14 -5
- package/assets/badge-dark.svg +2 -2
- package/assets/badge-light.svg +2 -2
- package/docs/architecture.md +3 -1
- package/docs/prompt-caching.md +21 -0
- package/extensions/goal-commands.ts +2 -1
- package/extensions/goal-core-tools.ts +2 -2
- package/extensions/goal-events.ts +42 -76
- package/extensions/goal-prompt-cache.ts +49 -0
- package/extensions/goal-scheduler.ts +20 -8
- package/extensions/goal-settings.ts +11 -0
- package/extensions/prompts/goal-prompts.ts +9 -8
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<a href="https://pi.dev/packages?type=extension" target="_blank" rel="noopener noreferrer">
|
|
7
7
|
<picture>
|
|
8
8
|
<source media="(prefers-color-scheme: dark)" srcset="assets/badge-dark.svg">
|
|
9
|
-
<img src="assets/badge-light.svg" alt="TOP 0.3% of Pi coding agent extensions: #7 of 3,
|
|
9
|
+
<img src="assets/badge-light.svg" alt="TOP 0.3% of Pi coding agent extensions: #7 of 3,318 by downloads · Sep 17, 2026 (best recorded rank)" width="480">
|
|
10
10
|
</picture>
|
|
11
11
|
</a>
|
|
12
12
|
</div>
|
|
@@ -115,7 +115,8 @@ Open `/goal-settings` to change these options. You can save defaults for all pro
|
|
|
115
115
|
|
|
116
116
|
| Setting | What it controls |
|
|
117
117
|
| --- | --- |
|
|
118
|
-
|
|
|
118
|
+
| Explicit execution contracts (`strictExecutionContract`) | Opt-in ready/wait protocol with one missing-decision repair, then pause. Defaults to `false`: successful executions continue automatically. |
|
|
119
|
+
| Autonomous run allowance (`maxAutonomousRuns`) | Positive whole number of extension-started runs per creation or `/goal-resume` period. **Unset means unlimited; zero disables automatic continuation.** Settings edits change the limit without resetting usage. |
|
|
119
120
|
| Task tracking (`disableTasks`) | Turn task lists on or off. Set to `true` to disable them. |
|
|
120
121
|
| Subtask depth (`subtaskDepth`) | Limit how many levels of subtasks the agent can create. |
|
|
121
122
|
| Completion requirements (`disableContracts`) | Turn explicit goal and task completion requirements on or off. Set to `true` to disable them. |
|
|
@@ -123,9 +124,11 @@ Open `/goal-settings` to change these options. You can save defaults for all pro
|
|
|
123
124
|
| Auditor provider, model, and thinking level | Choose which model reviews completed work and its reasoning effort. |
|
|
124
125
|
|
|
125
126
|
|
|
126
|
-
###
|
|
127
|
+
### Automatic continuation and optional execution contracts
|
|
127
128
|
|
|
128
|
-
|
|
129
|
+
Active goals continue automatically after successful executions, including reasoning-only responses and final-task verification. No tool call, task update, scheduling declaration, or cooldown is required. Unproductive loops remain possible; optional run limits and token budgets still apply.
|
|
130
|
+
|
|
131
|
+
Enable `strictExecutionContract: true` in `/goal-settings` or your global/project settings to require explicit ready/wait decisions. In that mode, a missing decision permits one repair prompt within the remaining allowance, then pauses. This is a user preference; agents should not enable it merely to continue.
|
|
129
132
|
|
|
130
133
|
Set an appropriate allowance in `/goal-settings`, or in `.pi/pi-goal-x-settings.json`:
|
|
131
134
|
|
|
@@ -133,7 +136,9 @@ Set an appropriate allowance in `/goal-settings`, or in `.pi/pi-goal-x-settings.
|
|
|
133
136
|
{ "maxAutonomousRuns": 20 }
|
|
134
137
|
```
|
|
135
138
|
|
|
136
|
-
Agents may edit this setting. Changing it does not replenish consumed runs; explicit `/goal-resume` renews the period and continues now, including from a waiting goal.
|
|
139
|
+
Agents may edit this setting. Changing it does not replenish consumed runs; explicit `/goal-resume` renews the period and continues now, including from a waiting goal. No configured allowance is required unless the effective limit is zero, which disables resume. Tool calls within a run are not separate runs. Existing token budgets still apply.
|
|
140
|
+
|
|
141
|
+
Explicit `ready` is optional in default mode. New `wait` declarations require strict mode; otherwise they return a non-terminating error. Previously saved waits retain their deadline, checks, and repair rules when upgrading or disabling strict mode, and may be re-declared with the same identity. Mode changes never resume a paused goal or renew consumed runs.
|
|
137
142
|
|
|
138
143
|
```js
|
|
139
144
|
update_goal({ continuation: { kind: "ready", next_action: "Verify the build artifacts" } })
|
|
@@ -163,3 +168,7 @@ Existing producers that directly send `triggerTurn`/`followUp` messages still ru
|
|
|
163
168
|
## License
|
|
164
169
|
|
|
165
170
|
MIT
|
|
171
|
+
|
|
172
|
+
### Prompt caching
|
|
173
|
+
|
|
174
|
+
Goal state is refreshed at the request tail while the system prompt and conversation prefix stay stable. Pi retains control of provider cache settings. See [prompt caching](docs/prompt-caching.md) for explicit-cache handling, validation, and cache invalidation boundaries.
|
package/assets/badge-dark.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.3% of Pi coding agent extensions: #7 of 3,
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.3% of Pi coding agent extensions: #7 of 3,318 by downloads · Sep 17, 2026 (best recorded rank)">
|
|
2
2
|
<!-- Dark mode -->
|
|
3
3
|
<style>
|
|
4
4
|
.frame { fill: #14181f; stroke: #2a3040; stroke-width: 1; }
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
<text class="rank" x="18" y="34">TOP 0.3%</text>
|
|
14
14
|
<line class="divider" x1="128" y1="13" x2="128" y2="41"/>
|
|
15
15
|
<text class="label" x="143" y="25">of Pi coding agent extensions</text>
|
|
16
|
-
<text class="meta" x="143" y="41">#7 of 3,
|
|
16
|
+
<text class="meta" x="143" y="41">#7 of 3,318 by downloads · Sep 17, 2026</text>
|
|
17
17
|
</a>
|
|
18
18
|
</svg>
|
package/assets/badge-light.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.3% of Pi coding agent extensions: #7 of 3,
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.3% of Pi coding agent extensions: #7 of 3,318 by downloads · Sep 17, 2026 (best recorded rank)">
|
|
2
2
|
<!-- Light mode -->
|
|
3
3
|
<style>
|
|
4
4
|
.frame { fill: #f6f8fa; stroke: #d0d7de; stroke-width: 1; }
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
<text class="rank" x="18" y="34">TOP 0.3%</text>
|
|
14
14
|
<line class="divider" x1="128" y1="13" x2="128" y2="41"/>
|
|
15
15
|
<text class="label" x="143" y="25">of Pi coding agent extensions</text>
|
|
16
|
-
<text class="meta" x="143" y="41">#7 of 3,
|
|
16
|
+
<text class="meta" x="143" y="41">#7 of 3,318 by downloads · Sep 17, 2026</text>
|
|
17
17
|
</a>
|
|
18
18
|
</svg>
|
package/docs/architecture.md
CHANGED
|
@@ -19,7 +19,7 @@ handlers from their dedicated modules:
|
|
|
19
19
|
| `goal-task-tools.ts` | `set_goal_tasks` / `update_goal_task` executors plus flat parent-linked conversion, id-stable merge, `countTasks` |
|
|
20
20
|
| `goal-task-confirmation.ts` | Task-only result boundary (`{decision}`, no auditor toggle) with neutral Confirm task list / Keep current tasks labels |
|
|
21
21
|
| `goal-commands.ts` | The curated fourteen-command palette and its handlers |
|
|
22
|
-
| `goal-events.ts` |
|
|
22
|
+
| `goal-events.ts` | Lifecycle and provider event handlers (`context`, `before_provider_request`, `turn_start`, `tool_call`, `tool_execution_end`, `turn_end`, `message_end`, `session_start`, `session_before_compact`, `session_compact`, `session_tree`, `before_agent_start`, `agent_end`, `agent_settled`, `session_shutdown`) |
|
|
23
23
|
| `goal-widget.ts` | Terminal input keybindings (Esc pause / abort-audit, Ctrl+Shift+T overlay) and the hidden debug helpers |
|
|
24
24
|
| `goal-format.ts` | Pure formatting/message-introspection helpers and renderers |
|
|
25
25
|
| `goal-service.ts` | `GoalService` — the sole mutation boundary: ordered reconcile → id/focus-revision validation → clone-mutate → write/archive → ledger → memory commit → returned effects |
|
|
@@ -341,3 +341,5 @@ operable; `/goal-clear` confirms; task confirmation uses neutral labels;
|
|
|
341
341
|
completion commits are failure-checked; and cross-process mutations are
|
|
342
342
|
serialized with persisted revisions plus per-goal filesystem locks that
|
|
343
343
|
return typed conflicts to stale writers instead of overwriting blindly.
|
|
344
|
+
|
|
345
|
+
Prompt layout and explicit provider cache handling are documented in [prompt caching](prompt-caching.md). Live goal state is constructed in the context hook; before_agent_start performs lifecycle preflight without modifying the system prompt.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Prompt caching
|
|
2
|
+
|
|
3
|
+
pi-goal-x preserves the provider's reusable prompt prefix during normal user turns, autonomous checkpoints, and tool loops. No extra extension setting is required. Pi's provider configuration still determines whether caching is enabled, its retention period, and model support.
|
|
4
|
+
|
|
5
|
+
The request layout is:
|
|
6
|
+
|
|
7
|
+
1. Host system prompt and tool schemas.
|
|
8
|
+
2. Conversation history, with each goal checkpoint normalized to a tiny, stable trigger in its original position.
|
|
9
|
+
3. One request-only goal-state message containing the current objective, tasks, lifecycle rules, usage, budget, scheduling state, and applicable recovery/auditor reminders.
|
|
10
|
+
|
|
11
|
+
Changing goal state never rewrites the system prompt or historical messages. The full goal-state message is not persisted into the session. Old checkpoints remain until Pi compacts history; retaining small markers avoids shifting the prefix on every continuation. Display-only auditor messages are consistently excluded, and tool calls stay adjacent to their results.
|
|
12
|
+
|
|
13
|
+
For Anthropic-style explicit caching, the extension moves Pi's existing cache_control breakpoint from the transient state message onto the preceding cacheable history block. The Bedrock equivalent moves the cachePoint. Both preserve the configured TTL and do not add breakpoints. Payloads without an existing explicit marker—including caching disabled and implicit-cache providers—are unchanged. The extension does not set cache keys, enable paid extended retention, or replace provider defaults.
|
|
14
|
+
|
|
15
|
+
Execution tools already use an idempotent profile: goal progress, focus, and lifecycle changes do not change schemas. Explicit switches into/out of drafting and changes to disableTasks can change tools and legitimately rebuild the cache. Drafting messages are appended to conversation history. Isolated auditor and Oracle sessions already use fixed instructions/tool profiles and place goal-specific evidence in their user messages; their tool loops retain history. They have separate sessions and use Pi's cache defaults. Opting these sessions into project resources also opts into any behavior from those resources.
|
|
16
|
+
|
|
17
|
+
Cache reuse can still be interrupted by Pi compaction, branch/session changes, model/provider switches, other extensions changing earlier content, changed tool schemas, provider expiry, minimum-length or breakpoint-lookback limits, or routing. The extension cannot guarantee a hit rate or a particular cost reduction. The reported issue used Pi 0.85.1; this change is validated against the repository's supported installed SDK, without expanding its peer dependency range.
|
|
18
|
+
|
|
19
|
+
Validation includes consecutive request-prefix regressions; actual Anthropic, Anthropic-compatible Chat Completions, OpenAI Chat Completions and Responses serialization with short/long/disabled retention; Bedrock payload-shape checks; lifecycle/tool-pair safety tests; and full SDK parent/auditor/Oracle payload captures. These tests stop before HTTP dispatch. They verify request construction, not live provider hit rates.
|
|
20
|
+
|
|
21
|
+
Provider references: [Anthropic prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) describes exact-prefix matching and explicit breakpoints; [DeepSeek context caching](https://api-docs.deepseek.com/guides/kv_cache/) describes its automatic prefix cache and usage counters.
|
|
@@ -370,6 +370,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
370
370
|
{ key: "autoSelectSingleGoal", label: "autoSelectSingleGoal", section: "Goal behavior", kind: "boolean" },
|
|
371
371
|
{ key: "hideUnfocusedBanner", label: "hideUnfocusedBanner", section: "Goal behavior", kind: "boolean" },
|
|
372
372
|
{ key: "disableContracts", label: "disableContracts", section: "Goal behavior", kind: "boolean" },
|
|
373
|
+
{ key: "strictExecutionContract", label: "explicit execution contracts (opt-in)", section: "Goal behavior", kind: "boolean" },
|
|
373
374
|
{ key: "maxAutonomousRuns", label: "autonomous run allowance", section: "Goal behavior", kind: "positiveInteger" },
|
|
374
375
|
{ key: "stallTimeoutMinutes", label: "stall timeout (minutes)", section: "Goal behavior", kind: "positiveInteger" },
|
|
375
376
|
{ key: "objectiveMaxChars", label: "max objective length (0 = none)", section: "Goal behavior", kind: "positiveInteger" },
|
|
@@ -389,7 +390,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
389
390
|
];
|
|
390
391
|
|
|
391
392
|
function settingsValue(config: GoalSettings, key: keyof GoalSettings | string): string {
|
|
392
|
-
if (key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner") {
|
|
393
|
+
if (key === "strictExecutionContract" || key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner") {
|
|
393
394
|
return config[key] === true ? "true" : "false";
|
|
394
395
|
}
|
|
395
396
|
if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
|
|
@@ -509,8 +509,8 @@ async function runGoalAgentPauseFlow(ctx: ExtensionContext, reason: string | und
|
|
|
509
509
|
pi.registerTool(defineTool({
|
|
510
510
|
name: "update_goal",
|
|
511
511
|
label: "Update Goal",
|
|
512
|
-
description: "
|
|
513
|
-
promptSnippet: "
|
|
512
|
+
description: "Update lifecycle status or optionally save continuation. New waits require strictExecutionContract opt-in. Wait: future ISO deadline; reuse wait_id/deadline without polling on recheck. Completion is audited.",
|
|
513
|
+
promptSnippet: "Complete, block, pause, or optionally save a scheduling decision.",
|
|
514
514
|
promptGuidelines: [],
|
|
515
515
|
parameters: Type.Object({
|
|
516
516
|
status: Type.Optional(StringEnum(["complete", "blocked", "paused"] as const, { description: "Exclusive with continuation." })),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cacheGoalHistory } from "./goal-prompt-cache.ts";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import {
|
|
4
4
|
GOAL_EVENT_ENTRY,
|
|
@@ -34,45 +34,21 @@ import type { GoalCore } from "./goal-state.ts";
|
|
|
34
34
|
import { filterGoalSessionContext } from "./goal-session-safety.ts";
|
|
35
35
|
import type { GoalMutationOutcome } from "./goal-service.ts";
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* Issue #30: provider-context checkpoint compaction (pure helper).
|
|
39
|
-
*
|
|
40
|
-
* Every historical checkpoint message is redundant: its authoritative state is
|
|
41
|
-
* reconstructed from goal storage and injected once per turn by
|
|
42
|
-
* before_agent_start. Normal provider requests therefore retain at most ONE
|
|
43
|
-
* checkpoint marker — the latest — rewritten to the tiny bounded v2 trigger
|
|
44
|
-
* content. Keeping one user-role turn-start marker avoids provider edge cases
|
|
45
|
-
* where removing it would leave the request ending on an assistant or tool
|
|
46
|
-
* result. Audit events, user messages, assistant messages, and tool results
|
|
47
|
-
* pass through untouched.
|
|
48
|
-
*/
|
|
37
|
+
/** Normalize checkpoints independently: deleting old markers shifts the cached prefix. */
|
|
49
38
|
export function compactGoalCheckpointContext(
|
|
50
|
-
|
|
51
|
-
|
|
39
|
+
messages: readonly unknown[],
|
|
40
|
+
_currentGoal: GoalRecord | null,
|
|
52
41
|
): unknown[] | null {
|
|
53
|
-
let
|
|
54
|
-
let checkpointGoalId: string | null = null;
|
|
55
|
-
const checkpoints: number[] = [];
|
|
56
|
-
// Parse each message once; retain ordinary messages and rewrite only the last marker.
|
|
42
|
+
let output: unknown[] | null = null;
|
|
57
43
|
for (let i = 0; i < messages.length; i++) {
|
|
58
|
-
const
|
|
59
|
-
|
|
44
|
+
const message = messages[i] as { customType?: string; details?: unknown; content?: unknown };
|
|
45
|
+
const id = goalEventMessageId(message);
|
|
46
|
+
if (id === null) { output?.push(message); continue; }
|
|
47
|
+
output ??= messages.slice(0, i);
|
|
48
|
+
output.push({ ...message, content: checkpointTriggerPrompt(id), display: false,
|
|
49
|
+
details: { version: 2, kind: "checkpoint", goalId: id } });
|
|
60
50
|
}
|
|
61
|
-
|
|
62
|
-
const output: unknown[] = [];
|
|
63
|
-
let start = 0;
|
|
64
|
-
for (const index of checkpoints) {
|
|
65
|
-
for (let i = start; i < index; i++) output.push(messages[i]);
|
|
66
|
-
start = index + 1;
|
|
67
|
-
}
|
|
68
|
-
const message = messages[lastCheckpointIndex] as Record<string, unknown>;
|
|
69
|
-
output.push({...message, content: checkpointTriggerPrompt(checkpointGoalId), display: false, details: {
|
|
70
|
-
version: 2,
|
|
71
|
-
kind: currentGoal?.id === checkpointGoalId && currentGoal?.status === "active" ? "checkpoint" : "stale",
|
|
72
|
-
goalId: checkpointGoalId, currentGoalId: currentGoal?.id ?? null, currentStatus: currentGoal?.status ?? null,
|
|
73
|
-
}});
|
|
74
|
-
for (let i = start; i < messages.length; i++) output.push(messages[i]);
|
|
75
|
-
return output;
|
|
51
|
+
return output;
|
|
76
52
|
}
|
|
77
53
|
|
|
78
54
|
/**
|
|
@@ -84,22 +60,18 @@ export function compactGoalCheckpointContext(
|
|
|
84
60
|
*/
|
|
85
61
|
export function registerGoalEvents(core: GoalCore): void {
|
|
86
62
|
const { pi } = core;
|
|
63
|
+
let liveContent: string | undefined;
|
|
64
|
+
pi.on("before_provider_request", event => cacheGoalHistory(event.payload, liveContent));
|
|
87
65
|
let continuationAfterSettleFor: string | null = null;
|
|
88
66
|
let networkErrorRecoveryAfterSettleFor: string | null = null;
|
|
89
67
|
|
|
90
68
|
pi.on("context", async (event, ctx) => {
|
|
91
69
|
const filtered = filterGoalSessionContext(event.messages);
|
|
92
70
|
const messages = compactGoalCheckpointContext(filtered ?? event.messages, core.state.goal) ?? filtered;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
// state, but do not repeat policy/objective already in the inherited prompt.
|
|
98
|
-
const content = ctx.getSystemPrompt?.().includes(`[PI GOAL ACTIVE goalId=${goal.id}]`)
|
|
99
|
-
? `[CURRENT EXECUTION STATE goalId=${goal.id}]\nStatus: ${goal.status}; autoContinue: ${goal.autoContinue}.\n${schedulerSummary(goal.scheduler, settings.maxAutonomousRuns)}`
|
|
100
|
-
: goalPrompt(goal, settings);
|
|
101
|
-
const live = { role: "custom" as const, customType: "pi-goal-live-context", content, display: false, timestamp: Date.now() };
|
|
102
|
-
return { messages: [live, ...(messages ?? event.messages)] as typeof event.messages };
|
|
71
|
+
const content = liveContent = currentGoalContext(ctx);
|
|
72
|
+
if (content) {
|
|
73
|
+
const live = { role: "custom" as const, customType: "pi-goal-live-context", content, display: false, timestamp: 0 };
|
|
74
|
+
return { messages: [...(messages ?? event.messages), live] as typeof event.messages };
|
|
103
75
|
}
|
|
104
76
|
return messages === null ? undefined : { messages: messages as typeof event.messages };
|
|
105
77
|
});
|
|
@@ -343,16 +315,9 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
343
315
|
});
|
|
344
316
|
|
|
345
317
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
346
|
-
core.scheduler.prepare();
|
|
347
318
|
core.advanceTurnSeq();
|
|
348
319
|
if (!hasActiveDraft(core)) core.installGoalToolProfile(!loadGoalSettings(ctx.cwd).disableTasks);
|
|
349
|
-
const currentSystemPrompt = () => ctx.getSystemPrompt?.() || event.systemPrompt;
|
|
350
320
|
const incomingGoalId = extractGoalIdFromInjectedMessage(event.prompt ?? "");
|
|
351
|
-
// Several prompt enrichments may need the same ledger snapshot. Keep one
|
|
352
|
-
// local read for this hook instead of repeatedly traversing the cached
|
|
353
|
-
// ledger when rejection and post-compaction steering overlap.
|
|
354
|
-
let promptLedger: ReturnType<typeof readGoalLedger> | undefined;
|
|
355
|
-
const getPromptLedger = () => promptLedger ??= { events: core.state.goal ? goalRuntimeEvents(ctx, core.state.goal.id) : [], malformed: 0 };
|
|
356
321
|
|
|
357
322
|
// If this turn was triggered by a hidden goal checkpoint that no longer
|
|
358
323
|
// matches the active goal, abort the whole turn instead of letting the
|
|
@@ -371,9 +336,7 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
371
336
|
ctx.abort?.();
|
|
372
337
|
} catch {}
|
|
373
338
|
core.updateUI(ctx);
|
|
374
|
-
return
|
|
375
|
-
systemPrompt: `${currentSystemPrompt()}\n\n${staleContinuationPrompt(incomingGoalId, core.state.goal)}`,
|
|
376
|
-
};
|
|
339
|
+
return;
|
|
377
340
|
}
|
|
378
341
|
core.runtime.setCheckpoint(null);
|
|
379
342
|
} else {
|
|
@@ -385,22 +348,29 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
385
348
|
networkErrorRecoveryAfterSettleFor = null;
|
|
386
349
|
}
|
|
387
350
|
|
|
351
|
+
core.reconcileFocusedGoalFromDisk(ctx);
|
|
352
|
+
core.runningGoalId = core.state.goal?.status === "active" ? core.state.goal.id : null;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
/** Request-only state: no live counters, focus, or reminders enter the system prefix. */
|
|
356
|
+
function currentGoalContext(ctx: ExtensionContext): string | undefined {
|
|
357
|
+
const checkpoint = core.runtime.getCheckpointGoalId();
|
|
358
|
+
if (checkpoint !== null && !core.isActionableContinuationGoal(checkpoint)) {
|
|
359
|
+
return staleContinuationPrompt(checkpoint, core.state.goal);
|
|
360
|
+
}
|
|
361
|
+
// Several prompt enrichments may need the same ledger snapshot. Keep one
|
|
362
|
+
// local read for this hook instead of repeatedly traversing the cached
|
|
363
|
+
// ledger when rejection and post-compaction steering overlap.
|
|
364
|
+
let promptLedger: ReturnType<typeof readGoalLedger> | undefined;
|
|
365
|
+
const getPromptLedger = () => promptLedger ??= { events: core.state.goal ? goalRuntimeEvents(ctx, core.state.goal.id) : [], malformed: 0 };
|
|
366
|
+
|
|
388
367
|
if (!core.state.goal) {
|
|
389
|
-
core.runningGoalId = null;
|
|
390
368
|
const openCount = otherOpenGoalCount(core.goalsById, null);
|
|
391
369
|
if (openCount > 0) {
|
|
392
|
-
return
|
|
370
|
+
return unfocusedOpenGoalsPrompt(openCount);
|
|
393
371
|
}
|
|
394
372
|
return;
|
|
395
373
|
}
|
|
396
|
-
core.reconcileFocusedGoalFromDisk(ctx);
|
|
397
|
-
if (!core.state.goal) {
|
|
398
|
-
core.runningGoalId = null;
|
|
399
|
-
const openCount = otherOpenGoalCount(core.goalsById, null);
|
|
400
|
-
if (openCount > 0) return { systemPrompt: `${currentSystemPrompt()}\n\n${unfocusedOpenGoalsPrompt(openCount)}` };
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
core.runningGoalId = core.state.goal.status === "active" ? core.state.goal.id : null;
|
|
404
374
|
if (core.state.goal.status === "complete") return;
|
|
405
375
|
if (core.state.goal.status === "paused") {
|
|
406
376
|
const current = core.state.goal;
|
|
@@ -421,9 +391,7 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
421
391
|
} catch {
|
|
422
392
|
// Ledger read failure should not break the prompt
|
|
423
393
|
}
|
|
424
|
-
return {
|
|
425
|
-
systemPrompt: `${currentSystemPrompt()}\n\n[PI GOAL PAUSED goalId=${current.id}]\n${untrustedObjectiveBlock(current)}${pauseExtras.join("\n")}${auditorExtra}\n\nThe goal is paused. Do not autonomously continue substantive work unless the user resumes it with /goal-resume. If the user explicitly asks to finish the paused goal and the objective is already satisfied based on available evidence, you may call update_goal({status: "complete"}). To abandon a goal, the user runs /goal-clear. Do not report the goal blocked in response to a pause.`,
|
|
426
|
-
};
|
|
394
|
+
return `[PI GOAL PAUSED goalId=${current.id}]\n${untrustedObjectiveBlock(current)}${pauseExtras.join("\n")}${auditorExtra}\n\nThe goal is paused. Do not autonomously continue substantive work unless the user resumes it with /goal-resume. If the user explicitly asks to finish the paused goal and the objective is already satisfied based on available evidence, you may call update_goal({status: "complete"}). To abandon a goal, the user runs /goal-clear. Do not report the goal blocked in response to a pause.`;
|
|
427
395
|
}
|
|
428
396
|
// Token-budget-limited goals get one-time wrap-up steering: summarize,
|
|
429
397
|
// do not start new substantive work, never claim completion unless real.
|
|
@@ -440,13 +408,11 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
440
408
|
const reminder = core.runtime.consumePostBudgetReminder()
|
|
441
409
|
? `\n\n[TOKEN BUDGET REACHED goalId=${limitedGoal.id}]\nThe goal's token budget has been reached${budgetText ? ` (${budgetText}${balanceText})` : ""}. Wrap up the current work in one final response: summarize what was accomplished and what remains, do not start new substantive work, and do not claim the goal is complete unless it actually is. To continue, the user must raise or remove the budget and resume the goal.`
|
|
442
410
|
: "";
|
|
443
|
-
return {
|
|
444
|
-
systemPrompt: `${currentSystemPrompt()}\n\n[PI GOAL BUDGET LIMITED goalId=${limitedGoal.id}]\n${untrustedObjectiveBlock(limitedGoal)}${budgetText ? `\n${budgetText}` : ""}${reminder}`,
|
|
445
|
-
};
|
|
411
|
+
return `[PI GOAL BUDGET LIMITED goalId=${limitedGoal.id}]\n${untrustedObjectiveBlock(limitedGoal)}${budgetText ? `\n${budgetText}` : ""}${reminder}`;
|
|
446
412
|
}
|
|
447
413
|
if (core.state.goal.status === "blocked") {
|
|
448
414
|
const blocked = core.state.goal;
|
|
449
|
-
return
|
|
415
|
+
return `[PI GOAL BLOCKED goalId=${blocked.id}]\n${untrustedObjectiveBlock(blocked)}\nBlocker: ${blocked.pauseReason ?? "unspecified"}\nThe goal is blocked; the user must run /goal-resume before goal work continues.`;
|
|
450
416
|
}
|
|
451
417
|
const activeGoal = core.state.goal;
|
|
452
418
|
const settings = loadGoalSettings(ctx.cwd);
|
|
@@ -466,7 +432,7 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
466
432
|
}
|
|
467
433
|
if (core.runtime.isPostCompactReminderPending() && shouldInjectPostCompactReminder({ pending: true, goal: activeGoal })) {
|
|
468
434
|
core.runtime.clearPostCompactReminder();
|
|
469
|
-
// PR E §62: post-compaction DELTA — the
|
|
435
|
+
// PR E §62: post-compaction DELTA — the current goal block already
|
|
470
436
|
// carries objective/policy/task gate/contract; inject only what
|
|
471
437
|
// compaction may have lost. Falls back to a generic note on ledger
|
|
472
438
|
// read failure.
|
|
@@ -479,8 +445,8 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
479
445
|
prompt = `${prompt}\n\n[POST-COMPACTION RESYNC goalId=${core.state.goal.id}]\nThe conversation was just compacted. Re-read the objective and continue from the actual artifacts/state; do not rely on memory of the prior chat.`;
|
|
480
446
|
}
|
|
481
447
|
}
|
|
482
|
-
return
|
|
483
|
-
}
|
|
448
|
+
return prompt;
|
|
449
|
+
}
|
|
484
450
|
|
|
485
451
|
pi.on("agent_end", async (event, ctx) => {
|
|
486
452
|
const endedGoalId = core.runningGoalId;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { asRecord } from "./goal-record.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pi marks the final user block for explicit caching. Our request-only state
|
|
5
|
+
* never enters history, so that block cannot be reused on the next request.
|
|
6
|
+
* Move that existing breakpoint to the preceding cacheable history block.
|
|
7
|
+
* Implicit caches and cacheRetention=none have no marker and are untouched.
|
|
8
|
+
* Reuse the provider's TTL and marker count rather than enabling caching here.
|
|
9
|
+
*/
|
|
10
|
+
export function cacheGoalHistory(payload: unknown, liveContent: string | undefined): unknown {
|
|
11
|
+
const root = asRecord(payload);
|
|
12
|
+
if (!liveContent || !Array.isArray(root?.messages)) return undefined;
|
|
13
|
+
const messages = root.messages;
|
|
14
|
+
const last = asRecord(messages.at(-1));
|
|
15
|
+
if (last?.role !== "user" || !Array.isArray(last.content)) return undefined;
|
|
16
|
+
// Bedrock represents its breakpoint as a separate content block.
|
|
17
|
+
const bedrockPoint = asRecord(last.content.at(-1));
|
|
18
|
+
if (asRecord(bedrockPoint?.cachePoint) && asRecord(last.content.at(-2))?.text === liveContent) {
|
|
19
|
+
for (let i = messages.length - 2; i >= 0; i--) {
|
|
20
|
+
const previous = asRecord(messages[i]);
|
|
21
|
+
if (!Array.isArray(previous?.content) || previous.content.length === 0) continue;
|
|
22
|
+
if (!previous.content.some(block => asRecord(block)?.cachePoint)) previous.content.push(bedrockPoint);
|
|
23
|
+
last.content.pop();
|
|
24
|
+
return payload;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const tail = asRecord(last.content.at(-1));
|
|
29
|
+
if (tail?.type !== "text" || tail.text !== liveContent || !asRecord(tail.cache_control)) return undefined;
|
|
30
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
31
|
+
const message = asRecord(messages[i]);
|
|
32
|
+
if (!message || !["user", "assistant", "tool"].includes(String(message.role))) continue;
|
|
33
|
+
if (typeof message.content === "string" && message.content.length > 0) {
|
|
34
|
+
message.content = [{ type: "text", text: message.content, cache_control: tail.cache_control }];
|
|
35
|
+
delete tail.cache_control;
|
|
36
|
+
return payload;
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(message.content)) continue;
|
|
39
|
+
for (let j = message.content.length - 1; j >= 0; j--) {
|
|
40
|
+
const block = asRecord(message.content[j]);
|
|
41
|
+
// Thinking blocks cannot carry cache_control. Never split/reorder tool pairs.
|
|
42
|
+
if (!block || block === tail || !["text", "image", "tool_use", "tool_result"].includes(String(block.type))) continue;
|
|
43
|
+
block.cache_control ??= tail.cache_control;
|
|
44
|
+
delete tail.cache_control;
|
|
45
|
+
return payload;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
@@ -11,8 +11,6 @@ export class GoalScheduler {
|
|
|
11
11
|
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
12
12
|
private ctx: ExtensionContext | undefined;
|
|
13
13
|
private inRun = false;
|
|
14
|
-
private prepared = false;
|
|
15
|
-
private liveContext = false;
|
|
16
14
|
private armedGeneration: string | undefined;
|
|
17
15
|
private declared = false;
|
|
18
16
|
private runGoalId: string | undefined;
|
|
@@ -23,6 +21,13 @@ export class GoalScheduler {
|
|
|
23
21
|
|
|
24
22
|
private owner(ctx: ExtensionContext): string { return ctx.sessionManager.getSessionId() || "unknown-session"; }
|
|
25
23
|
private limit(ctx: ExtensionContext): number | undefined { return loadGoalSettings(ctx.cwd).maxAutonomousRuns; }
|
|
24
|
+
private strict(ctx: ExtensionContext, s: GoalSchedulerState): boolean {
|
|
25
|
+
return loadGoalSettings(ctx.cwd).strictExecutionContract === true || !!s.wait;
|
|
26
|
+
}
|
|
27
|
+
private implicitReady(s: GoalSchedulerState): void {
|
|
28
|
+
s.phase = "ready"; s.dispatch = undefined; s.generation = randomUUID();
|
|
29
|
+
s.decision = { kind: "ready", nextAction: "Continue pursuing the goal, then verify and complete it when satisfied.", purpose: "ready" };
|
|
30
|
+
}
|
|
26
31
|
private available(ctx: ExtensionContext, s: GoalSchedulerState): boolean {
|
|
27
32
|
const limit = this.limit(ctx);
|
|
28
33
|
return limit === undefined || s.used < limit;
|
|
@@ -148,6 +153,7 @@ export class GoalScheduler {
|
|
|
148
153
|
if (typeof input.next_action !== "string" || !input.next_action.trim() || input.next_action.length > 2000) throw new Error("ready requires a nonempty next_action (at most 2000 characters).");
|
|
149
154
|
s.phase = "ready"; s.decision = { kind: "ready", nextAction: input.next_action.trim(), purpose: "ready" }; s.wait = undefined;
|
|
150
155
|
} else if (input.kind === "wait") {
|
|
156
|
+
if (!this.strict(ctx, s)) throw new Error("New waits require strictExecutionContract=true in /goal-settings. This is a user preference; continue pursuing the goal without a wait unless the user opts in.");
|
|
151
157
|
if (typeof input.reason !== "string" || !input.reason.trim() || input.reason.length > 2000) throw new Error("wait requires a nonempty reason (at most 2000 characters).");
|
|
152
158
|
const deadline = Date.parse(input.deadline);
|
|
153
159
|
if (!/T.*(?:Z|[+-]\d\d:\d\d)$/.test(input.deadline) || !Number.isSafeInteger(deadline) || deadline <= Date.now()) throw new Error("wait requires a future ISO deadline with a timezone.");
|
|
@@ -175,14 +181,10 @@ export class GoalScheduler {
|
|
|
175
181
|
} catch (error) { return { content: [{ type: "text", text: `Scheduling decision NOT saved: ${error instanceof Error ? error.message : String(error)}` }], details: { error: true }, terminate: false }; }
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
prepare(): void { this.prepared = true; }
|
|
179
|
-
needsLiveContext(): boolean { return this.liveContext; }
|
|
180
|
-
|
|
181
184
|
/** One logical run can contain multiple agent_start events during native retry/compaction. */
|
|
182
185
|
begin(ctx: ExtensionContext): void {
|
|
183
186
|
this.ctx = ctx;
|
|
184
187
|
if (this.inRun) { this.core.runningGoalId = this.core.state.goal?.id ?? null; return; }
|
|
185
|
-
this.liveContext = !this.prepared; this.prepared = false;
|
|
186
188
|
this.inRun = true; this.declared = false; this.denied = false;
|
|
187
189
|
this.runGoalId = this.core.state.goal?.id;
|
|
188
190
|
this.cancelTimer(); this.core.runtime.clearContinuationTimer();
|
|
@@ -230,11 +232,16 @@ export class GoalScheduler {
|
|
|
230
232
|
settled(ctx: ExtensionContext, successful = true): void {
|
|
231
233
|
this.inRun = false;
|
|
232
234
|
if (this.denied || (this.runGoalId && this.core.state.goal?.id !== this.runGoalId)) return;
|
|
235
|
+
invalidateGoalSettingsCache();
|
|
233
236
|
this.safe(ctx, () => {
|
|
234
237
|
const goal = this.core.state.goal;
|
|
235
238
|
if (!goal || goal.status !== "active" || !goal.autoContinue || !successful) return;
|
|
236
239
|
if (this.declared && goal.scheduler?.decision) { this.schedule(ctx); return; }
|
|
237
240
|
const s = this.state(ctx, goal);
|
|
241
|
+
if (!this.strict(ctx, s)) {
|
|
242
|
+
this.update(ctx, state => this.implicitReady(state));
|
|
243
|
+
this.schedule(ctx); return;
|
|
244
|
+
}
|
|
238
245
|
if (s.repairUsed || !this.available(ctx, s)) {
|
|
239
246
|
this.pause(ctx, !this.available(ctx, s) ? this.allowanceReason(ctx) : "No execution disposition after the contract-repair prompt. Use /goal-resume to continue.");
|
|
240
247
|
return;
|
|
@@ -248,7 +255,7 @@ export class GoalScheduler {
|
|
|
248
255
|
}
|
|
249
256
|
recover(ctx: ExtensionContext): void {
|
|
250
257
|
this.safe(ctx, () => {
|
|
251
|
-
this.update(ctx, s => { s.phase = "ready"; s.dispatch = undefined; s.decision = { kind: "ready", nextAction: "Retry after the provider error, then declare an execution disposition.", purpose: "recovery" }; });
|
|
258
|
+
this.update(ctx, s => { s.phase = "ready"; s.dispatch = undefined; s.decision = { kind: "ready", nextAction: this.strict(ctx, s) ? "Retry after the provider error, then declare an execution disposition." : "Retry after the provider error and continue pursuing the goal.", purpose: "recovery" }; });
|
|
252
259
|
this.schedule(ctx);
|
|
253
260
|
});
|
|
254
261
|
}
|
|
@@ -264,11 +271,15 @@ export class GoalScheduler {
|
|
|
264
271
|
}
|
|
265
272
|
schedule(ctx: ExtensionContext): void {
|
|
266
273
|
if (this.inRun) return;
|
|
274
|
+
invalidateGoalSettingsCache();
|
|
267
275
|
this.safe(ctx, () => {
|
|
268
276
|
this.core.reconcileFocusedGoalFromDisk(ctx);
|
|
269
277
|
const g = this.core.state.goal;
|
|
270
278
|
if (!g || g.status !== "active" || !g.autoContinue || !g.scheduler) return;
|
|
271
|
-
|
|
279
|
+
let s = this.state(ctx, g);
|
|
280
|
+
if (s.phase === "ready" && s.decision?.kind === "ready" && s.decision.purpose === "repair" && !this.strict(ctx, s)) {
|
|
281
|
+
s = this.update(ctx, state => this.implicitReady(state)).scheduler!;
|
|
282
|
+
}
|
|
272
283
|
if (!["ready", "waiting"].includes(s.phase)) return;
|
|
273
284
|
if (budgetReached(g)) { this.pause(ctx, "Goal token budget exhausted."); return; }
|
|
274
285
|
if (!this.available(ctx, s)) { this.pause(ctx, this.allowanceReason(ctx)); return; }
|
|
@@ -299,6 +310,7 @@ export class GoalScheduler {
|
|
|
299
310
|
if (s.generation !== generation) throw new Error("Scheduling generation changed.");
|
|
300
311
|
if (g.status !== "active" || !g.autoContinue || budgetReached(g) || !this.available(ctx, s)) throw new Error(this.allowanceReason(ctx));
|
|
301
312
|
if (s.wait && Date.now() >= s.wait.deadline) throw new Error("Wait deadline reached.");
|
|
313
|
+
if (s.phase === "ready" && s.decision?.kind === "ready" && s.decision.purpose === "repair" && !this.strict(ctx, s)) this.implicitReady(s);
|
|
302
314
|
let kind = s.phase === "ready" && s.decision?.kind === "ready" ? s.decision.purpose : undefined;
|
|
303
315
|
if (s.phase === "waiting" && s.wait) {
|
|
304
316
|
if (s.wait.signalled) kind = "wake";
|
|
@@ -104,6 +104,7 @@ export interface GoalSettingsResolvedShape {
|
|
|
104
104
|
stallTimeoutMinutes?: number;
|
|
105
105
|
/** Optional extension-run limit per creation/resume; zero disables, absent inherits (default unlimited). */
|
|
106
106
|
maxAutonomousRuns?: number;
|
|
107
|
+
strictExecutionContract?: boolean;
|
|
107
108
|
/**
|
|
108
109
|
* Maximum objective length in characters (0/unset = no limit, the
|
|
109
110
|
* default; >0 caps objectives in create_goal, propose_goal_draft, and
|
|
@@ -331,6 +332,7 @@ const ALLOWED_SETTINGS_KEYS = new Set([
|
|
|
331
332
|
"auditorProjectResources",
|
|
332
333
|
"stallTimeoutMinutes",
|
|
333
334
|
"maxAutonomousRuns",
|
|
335
|
+
"strictExecutionContract",
|
|
334
336
|
"objectiveMaxChars",
|
|
335
337
|
"keybindings",
|
|
336
338
|
"hideUnfocusedBanner",
|
|
@@ -380,6 +382,7 @@ export function parseSettingsLayer(
|
|
|
380
382
|
|
|
381
383
|
for (const [key, value] of Object.entries(record)) {
|
|
382
384
|
switch (key) {
|
|
385
|
+
case "strictExecutionContract":
|
|
383
386
|
case "disableTasks":
|
|
384
387
|
case "disableContracts":
|
|
385
388
|
case "disabled":
|
|
@@ -790,6 +793,11 @@ function resolvedSettingsSnapshot(cwd: string, env: NodeJS.ProcessEnv): Settings
|
|
|
790
793
|
globalValue: global.layer.oracle?.maxFailedAttemptsPerBlocker,
|
|
791
794
|
defaultValue: 2,
|
|
792
795
|
}));
|
|
796
|
+
const strictExecutionContract = track("strictExecutionContract", resolveLeaf<boolean>({
|
|
797
|
+
projectValue: project.layer.strictExecutionContract,
|
|
798
|
+
globalValue: global.layer.strictExecutionContract,
|
|
799
|
+
defaultValue: false,
|
|
800
|
+
}));
|
|
793
801
|
const maxAutonomousRuns = track("maxAutonomousRuns", resolveLeaf<number | undefined>({
|
|
794
802
|
projectValue: project.layer.maxAutonomousRuns,
|
|
795
803
|
globalValue: global.layer.maxAutonomousRuns,
|
|
@@ -855,6 +863,7 @@ function resolvedSettingsSnapshot(cwd: string, env: NodeJS.ProcessEnv): Settings
|
|
|
855
863
|
hideUnfocusedBanner,
|
|
856
864
|
stallTimeoutMinutes,
|
|
857
865
|
maxAutonomousRuns,
|
|
866
|
+
strictExecutionContract,
|
|
858
867
|
objectiveMaxChars,
|
|
859
868
|
keybindings,
|
|
860
869
|
networkRecovery: {
|
|
@@ -1204,6 +1213,7 @@ function buildPersistedLayer(settings: GoalSettings): Record<string, unknown> {
|
|
|
1204
1213
|
if (so.maxFailedAttemptsPerBlocker !== undefined) o.maxFailedAttemptsPerBlocker = so.maxFailedAttemptsPerBlocker;
|
|
1205
1214
|
if (Object.keys(o).length > 0) persisted.oracle = o;
|
|
1206
1215
|
}
|
|
1216
|
+
if (settings.strictExecutionContract !== undefined) persisted.strictExecutionContract = settings.strictExecutionContract;
|
|
1207
1217
|
if (settings.maxAutonomousRuns !== undefined) persisted.maxAutonomousRuns = settings.maxAutonomousRuns;
|
|
1208
1218
|
if (settings.stallTimeoutMinutes !== undefined) persisted.stallTimeoutMinutes = settings.stallTimeoutMinutes;
|
|
1209
1219
|
if (settings.objectiveMaxChars !== undefined) persisted.objectiveMaxChars = settings.objectiveMaxChars;
|
|
@@ -1248,6 +1258,7 @@ export function effectiveSettingsReport(cwd: string, env: NodeJS.ProcessEnv = pr
|
|
|
1248
1258
|
{ key: "thinkingLevel", label: "thinking_level", format: () => snapshot.value.thinkingLevel ?? "(default)" },
|
|
1249
1259
|
{ key: "auditorProjectResources", label: "auditor project resources", format: () => String(snapshot.value.auditorProjectResources) },
|
|
1250
1260
|
{ key: "hideUnfocusedBanner", label: "hide unfocused banner", format: () => String(snapshot.value.hideUnfocusedBanner) },
|
|
1261
|
+
{ key: "strictExecutionContract", label: "explicit execution contracts (opt-in)", format: () => String(snapshot.value.strictExecutionContract) },
|
|
1251
1262
|
{ key: "maxAutonomousRuns", label: "autonomous run allowance", format: () => snapshot.value.maxAutonomousRuns === 0 ? "0 (disabled)" : String(snapshot.value.maxAutonomousRuns ?? "unlimited (default)") },
|
|
1252
1263
|
{ key: "stallTimeoutMinutes", label: "stall timeout (minutes)", format: () => String(snapshot.value.stallTimeoutMinutes) },
|
|
1253
1264
|
{ key: "objectiveMaxChars", label: "max objective length (0 = none)", format: () => String(snapshot.value.objectiveMaxChars) },
|
|
@@ -11,8 +11,8 @@ export const MAX_PROMPT_FRAGMENT_CHARS = 10_000;
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Issue #30: a persisted continuation checkpoint is a tiny trigger record, not
|
|
14
|
-
* a full prompt. The authoritative goal state is injected
|
|
15
|
-
*
|
|
14
|
+
* a full prompt. The authoritative goal state is injected at the request tail by
|
|
15
|
+
* the context hook; the persisted marker only needs to carry the goal id.
|
|
16
16
|
*/
|
|
17
17
|
export const CHECKPOINT_TRIGGER_MAX_CHARS = 160;
|
|
18
18
|
|
|
@@ -183,11 +183,12 @@ export function sisyphusDisciplineBlock(goal: GoalRecord): string {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/** Shared outcome/blocker policy for active goals (bounded). */
|
|
186
|
-
function lifecyclePolicyBlock(autonomous: boolean): string {
|
|
186
|
+
function lifecyclePolicyBlock(autonomous: boolean, strict: boolean): string {
|
|
187
187
|
return [
|
|
188
188
|
"[OUTCOMES]",
|
|
189
189
|
'- Automatic runs default to unlimited. maxAutonomousRuns in .pi/pi-goal-x-settings.json caps runs; 0 disables (agents may set it). Only creation or user /goal-resume renews usage.',
|
|
190
|
-
...(autonomous ? ['- End execution with update_goal: ready for runnable work, wait for an external condition, or a status below. Saved decisions terminate; further work invalidates them. Missing decisions allow one repair. Never busy-poll.'] : []),
|
|
190
|
+
...(autonomous && strict ? ['- End execution with update_goal: ready for runnable work, wait for an external condition, or a status below. Saved decisions terminate; further work invalidates them. Missing decisions allow one repair. Never busy-poll.'] : []),
|
|
191
|
+
...(autonomous && !strict ? ['- Continue pursuing the goal automatically after each execution; no scheduling declaration is required. Optional ready saves a next action. New waits require user opt-in to strictExecutionContract; do not enable it merely to continue.'] : []),
|
|
191
192
|
'- update_goal({status: "complete"}) only when every requirement is satisfied; the independent completion auditor checks actual evidence. Approval archives; rejection requires rework.',
|
|
192
193
|
'- update_goal({status: "blocked"}) only after the SAME blocker recurs on three consecutive goal turns; keep trying concrete steps before then.',
|
|
193
194
|
'- update_goal({status: "paused", reason: "…"}) pauses immediately. User controls: /goal-pause, /goal-resume, /goal-clear.',
|
|
@@ -207,7 +208,7 @@ let promptCacheChars = 0;
|
|
|
207
208
|
function cachedPrompt(goal: GoalRecord, settings: GoalSettings | undefined, kind: "goal" | "continuation", build: () => string): string {
|
|
208
209
|
const key = [kind, goal.id, goal.status, goal.autoContinue, goal.sisyphus, goal.objective,
|
|
209
210
|
goal.verificationContract, settings?.disableTasks ? undefined : taskIndex(goal.taskList?.tasks),
|
|
210
|
-
goal.taskList?.blockCompletion, promptProfile(), goal.currentTaskId, settings?.disableTasks, settings?.disableContracts, settings?.maxAutonomousRuns !== 0];
|
|
211
|
+
goal.taskList?.blockCompletion, promptProfile(), goal.currentTaskId, settings?.disableTasks, settings?.disableContracts, settings?.maxAutonomousRuns !== 0, settings?.strictExecutionContract === true, !!goal.scheduler?.wait];
|
|
211
212
|
for (let i = promptFragmentCache.length - 1; i >= 0; i--) {
|
|
212
213
|
const entry = promptFragmentCache[i]!;
|
|
213
214
|
if (key.every((part, j) => part === entry.key[j])) return entry.value;
|
|
@@ -238,7 +239,7 @@ function buildGoalPrompt(goal: GoalRecord, settings?: GoalSettings): string {
|
|
|
238
239
|
// Bound individual data fields so essential rules can never be sliced off.
|
|
239
240
|
return [
|
|
240
241
|
`[PI GOAL ACTIVE goalId=${goal.id}]`,
|
|
241
|
-
lifecyclePolicyBlock(settings?.maxAutonomousRuns !== 0), sisyphusDisciplineBlock(goal),
|
|
242
|
+
lifecyclePolicyBlock(settings?.maxAutonomousRuns !== 0, settings?.strictExecutionContract === true || !!goal.scheduler?.wait), sisyphusDisciplineBlock(goal),
|
|
242
243
|
`Status: ${statusLabel(goal)}\nMode: ${goal.sisyphus ? "sisyphus" : "regular"}`,
|
|
243
244
|
untrustedObjectiveBlock(goal), taskListBlock(goal, settings), verificationContractBlock(goal, settings),
|
|
244
245
|
].filter(Boolean).join("\n\n");
|
|
@@ -264,8 +265,8 @@ export function objectiveEditedPrompt(goal: GoalRecord): string {
|
|
|
264
265
|
* Deprecated compatibility wrapper (issue #30). The full continuation prompt
|
|
265
266
|
* was the defect: every auto-continue turn persisted the whole objective/task/
|
|
266
267
|
* contract/policy block as a custom session message, growing sessions by
|
|
267
|
-
* ~6.4K chars per turn. The authoritative state is now injected
|
|
268
|
-
* by
|
|
268
|
+
* ~6.4K chars per turn. The authoritative state is now injected at the request tail
|
|
269
|
+
* by the context hook; the persisted follow-up is only a tiny trigger.
|
|
269
270
|
*
|
|
270
271
|
* Kept for one minor release so external call sites migrate explicitly.
|
|
271
272
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.6",
|
|
4
4
|
"description": "Adds /goal to pi: conversational goal planning, flexible or ordered goals, persistent progress, and an independent completion auditor.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "pi-goal-x contributors",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"docs/agent-flow-design.md",
|
|
35
35
|
"docs/agentic-runtime-prd.md",
|
|
36
36
|
"docs/architecture.md",
|
|
37
|
+
"docs/prompt-caching.md",
|
|
37
38
|
"docs/pi-autoresearch-survey.md",
|
|
38
39
|
"docs/pi-goal-x-unified-dashboard-plan.md",
|
|
39
40
|
"docs/unified-dashboard.md",
|