pi-goal-x 0.31.5 → 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 +5 -1
- 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-events.ts +42 -76
- package/extensions/goal-prompt-cache.ts +49 -0
- package/extensions/goal-scheduler.ts +0 -6
- package/extensions/prompts/goal-prompts.ts +4 -4
- 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>
|
|
@@ -168,3 +168,7 @@ Existing producers that directly send `triggerTurn`/`followUp` messages still ru
|
|
|
168
168
|
## License
|
|
169
169
|
|
|
170
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.
|
|
@@ -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;
|
|
@@ -183,14 +181,10 @@ export class GoalScheduler {
|
|
|
183
181
|
} catch (error) { return { content: [{ type: "text", text: `Scheduling decision NOT saved: ${error instanceof Error ? error.message : String(error)}` }], details: { error: true }, terminate: false }; }
|
|
184
182
|
}
|
|
185
183
|
|
|
186
|
-
prepare(): void { this.prepared = true; }
|
|
187
|
-
needsLiveContext(): boolean { return this.liveContext; }
|
|
188
|
-
|
|
189
184
|
/** One logical run can contain multiple agent_start events during native retry/compaction. */
|
|
190
185
|
begin(ctx: ExtensionContext): void {
|
|
191
186
|
this.ctx = ctx;
|
|
192
187
|
if (this.inRun) { this.core.runningGoalId = this.core.state.goal?.id ?? null; return; }
|
|
193
|
-
this.liveContext = !this.prepared; this.prepared = false;
|
|
194
188
|
this.inRun = true; this.declared = false; this.denied = false;
|
|
195
189
|
this.runGoalId = this.core.state.goal?.id;
|
|
196
190
|
this.cancelTimer(); this.core.runtime.clearContinuationTimer();
|
|
@@ -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
|
|
|
@@ -265,8 +265,8 @@ export function objectiveEditedPrompt(goal: GoalRecord): string {
|
|
|
265
265
|
* Deprecated compatibility wrapper (issue #30). The full continuation prompt
|
|
266
266
|
* was the defect: every auto-continue turn persisted the whole objective/task/
|
|
267
267
|
* contract/policy block as a custom session message, growing sessions by
|
|
268
|
-
* ~6.4K chars per turn. The authoritative state is now injected
|
|
269
|
-
* 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.
|
|
270
270
|
*
|
|
271
271
|
* Kept for one minor release so external call sites migrate explicitly.
|
|
272
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",
|