@astrosheep/pi-context 0.5.0 → 0.6.1
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 +3 -2
- package/package.json +1 -1
- package/src/index.ts +40 -25
package/README.md
CHANGED
|
@@ -21,7 +21,8 @@ The extension composes Pi's public `session_before_compact` / `session_compact`
|
|
|
21
21
|
- **`new_context` tool** — the model requests a fresh context window. The extension waits for the current tool turn to end, compacts with a short deterministic reset message (old conversation is excluded from the new provider context but stays in the session), then sends exactly one hidden continuation turn.
|
|
22
22
|
- **`<context_window>` hint** — persisted as a visible custom message at session start and after each window reset (Codex-style: written once into history instead of re-injected per request). It carries the agent name, first/current/previous window IDs, and the 5 most recently updated notes. Within a window the note list goes stale, exactly like Codex's steady-state world-state diffing.
|
|
23
23
|
- **Low-budget guidance** — when remaining context first drops to 16,000 tokens or below, a `<context_window_guidance>` reminder is **persisted once per window** into history (TUI-visible, no extra turn; `sendMessage` safely defers to end of turn mid-stream). The in-flight request additionally gets one transient tail copy so the model sees it immediately. Text is static and appended, so the provider prefix cache survives. The exact remaining figure is one `get_context_remaining` call away. Note: Pi's built-in auto-compaction fires when remaining context falls below `reserveTokens` (default 16,384), so raise the reminder threshold above your `reserveTokens` or the reminder never precedes compaction.
|
|
24
|
-
- **
|
|
24
|
+
- **Auto-compact fallback** — Codex `auto_compact_fallback_prompt` parity, adapted to Pi's trigger points. On the first proactive `threshold` compaction in a window (post-run, agent still streaming), the extension cancels compaction once and steers in a note-taking turn ("write durable state with `notes_write_file` now"); the next threshold trigger performs the real reset and auto-continues, like Codex's mid-turn rollover. The pre-prompt threshold path (idle — cancelling would race the user prompt) and `overflow` recovery (cancelling would abandon Pi's one-shot retry) reset immediately without a fallback turn.
|
|
25
|
+
- **Runtime toggle** — `/pi-context off` disables hint injection, guidance, fallback turns, and reset-style compaction (Pi's default compaction, including `keepRecentTokens`, applies again). `/pi-context on` re-enables; a bare `/pi-context` reports the current state.
|
|
25
26
|
- **History tools** — the model searches pre-reset conversation with case-sensitive literal substring search, exactly like Codex's `history.*` namespace.
|
|
26
27
|
- **Notes tools** — persistent, session-scoped virtual files that survive window resets.
|
|
27
28
|
|
|
@@ -45,7 +46,7 @@ The nine Codex History/Notes actions are flattened because Pi tools have one glo
|
|
|
45
46
|
|
|
46
47
|
`notes_*` stores operation entries in the same append-only Pi session under `pi-context/note`. They are session-scoped, survive JSONL reload, never enter provider context, and use safe relative virtual paths only (no absolute paths, `..`, `.`, empty components, or backslashes). Searches are literal and case-sensitive. `notes_read_file` accepts inclusive 1-based line ranges; negative line numbers count from the last line. Writes are capped at 1,000,000 UTF-8 bytes.
|
|
47
48
|
|
|
48
|
-
Pi has no
|
|
49
|
+
Unlike Codex, the history tools do not advertise `agent_name`: Pi has no cross-agent session routing, so the parameter is omitted from the schemas entirely (strict `additionalProperties: false` still rejects it) instead of costing schema tokens on every request.
|
|
49
50
|
|
|
50
51
|
Two extra controls compose Pi public APIs:
|
|
51
52
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ const STATE_TYPE = "pi-context/state";
|
|
|
6
6
|
const NOTE_TYPE = "pi-context/note";
|
|
7
7
|
const HINT_TYPE = "pi-context/hint";
|
|
8
8
|
const GUIDANCE_TYPE = "pi-context/guidance";
|
|
9
|
+
const FALLBACK_TYPE = "pi-context/fallback";
|
|
9
10
|
const RESET_MARKER_TYPE = "pi-context/reset-marker";
|
|
10
11
|
const CONTINUATION_TYPE = "pi-context/continuation";
|
|
11
12
|
const MAX_NOTE_BYTES = 1_000_000;
|
|
@@ -17,6 +18,10 @@ const REMINDER_THRESHOLD_TOKENS = 16_000;
|
|
|
17
18
|
const RESET_SUMMARY = "Context window reset. Prior session entries remain available only through the pi-context history tools.";
|
|
18
19
|
const CONTINUATION = "This is a fresh context window. Recover only the details needed to continue with history_* and notes_*; then continue the task.";
|
|
19
20
|
|
|
21
|
+
/** Codex auto_compact_fallback_prompt parity: one note-taking chance before an automatic reset. */
|
|
22
|
+
const FALLBACK_PROMPT =
|
|
23
|
+
"Context limit reached. This window is about to be reset. Write durable state with notes_write_file now: task state, decisions, open issues, next steps. Do not start new work. After this turn the window resets automatically; old conversation stays searchable through the history_* tools.";
|
|
24
|
+
|
|
20
25
|
type NoteFile = { text: string; createdAt: number; updatedAt: number };
|
|
21
26
|
type NoteOperation = {
|
|
22
27
|
op: "write" | "append";
|
|
@@ -37,7 +42,6 @@ type HistoryItem = {
|
|
|
37
42
|
type HistoryWindow = { windowId: string; createdAt?: string; items: HistoryItem[] };
|
|
38
43
|
|
|
39
44
|
type HistoryFilter = {
|
|
40
|
-
agent_name?: string | null;
|
|
41
45
|
window_id?: string | null;
|
|
42
46
|
role?: HistoryItem["role"] | null;
|
|
43
47
|
tool_namespace?: string | null;
|
|
@@ -53,12 +57,6 @@ function output(value: unknown, details: unknown = value, terminate = false) {
|
|
|
53
57
|
return { content: [{ type: "text" as const, text: json(value) }], details, terminate };
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
function unsupportedAgent(agentName: string | null | undefined) {
|
|
57
|
-
return agentName !== undefined && agentName !== null
|
|
58
|
-
? { error: "Pi 0.85.1 exposes no cross-agent session routing; agent_name is unsupported and was not aliased to this session." }
|
|
59
|
-
: undefined;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
60
|
function isTextContent(part: unknown): part is TextContent {
|
|
63
61
|
return typeof part === "object" && part !== null && (part as TextContent).type === "text" && typeof (part as TextContent).text === "string";
|
|
64
62
|
}
|
|
@@ -155,9 +153,7 @@ function allItems(ctx: ExtensionContext) {
|
|
|
155
153
|
return historyFromSession(ctx).flatMap((window) => window.items);
|
|
156
154
|
}
|
|
157
155
|
|
|
158
|
-
function filteredItems(ctx: ExtensionContext, params: HistoryFilter): HistoryItem[]
|
|
159
|
-
const agentError = unsupportedAgent(params.agent_name);
|
|
160
|
-
if (agentError) return agentError;
|
|
156
|
+
function filteredItems(ctx: ExtensionContext, params: HistoryFilter): HistoryItem[] {
|
|
161
157
|
let items = allItems(ctx);
|
|
162
158
|
if (typeof params.window_id === "string") items = items.filter((item) => item.windowId === params.window_id);
|
|
163
159
|
if (typeof params.role === "string") items = items.filter((item) => item.role === params.role);
|
|
@@ -277,6 +273,8 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
277
273
|
let rollover: "idle" | "requested" | "compacting" | "continued" = "idle";
|
|
278
274
|
let enabled = true;
|
|
279
275
|
let guidancePersistedInWindow: string | undefined;
|
|
276
|
+
let fallbackSentInWindow: string | undefined;
|
|
277
|
+
let continueAfterFallback = false;
|
|
280
278
|
|
|
281
279
|
/** Persist the context_window hint as a visible message (lands in history and the TUI), Codex-style. */
|
|
282
280
|
const persistHint = (ctx: ExtensionContext) => {
|
|
@@ -312,11 +310,9 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
312
310
|
pi.registerTool(defineTool({
|
|
313
311
|
name: "history_list_windows",
|
|
314
312
|
label: "History list windows",
|
|
315
|
-
description: "List durable Pi session-history windows.
|
|
316
|
-
parameters: Type.Object({ limit: positiveInteger(),
|
|
313
|
+
description: "List durable Pi session-history windows.",
|
|
314
|
+
parameters: Type.Object({ limit: positiveInteger(), recent_first: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
317
315
|
async execute(_id, params, _signal, _update, ctx) {
|
|
318
|
-
const agentError = unsupportedAgent(params.agent_name);
|
|
319
|
-
if (agentError) return output(agentError);
|
|
320
316
|
let windows = historyFromSession(ctx);
|
|
321
317
|
if (params.recent_first) windows = [...windows].reverse();
|
|
322
318
|
const limit = params.limit ?? windows.length;
|
|
@@ -328,10 +324,9 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
328
324
|
name: "history_list_items",
|
|
329
325
|
label: "History list items",
|
|
330
326
|
description: "List durable session items, including items before compaction, using opaque item and window IDs.",
|
|
331
|
-
parameters: Type.Object({ limit: positiveInteger(), recent_first: Type.Optional(Type.Boolean()), tool_namespace: nullableString(), role: Type.Optional(role),
|
|
327
|
+
parameters: Type.Object({ limit: positiveInteger(), recent_first: Type.Optional(Type.Boolean()), tool_namespace: nullableString(), role: Type.Optional(role), tool_name: nullableString(), window_id: nullableString(), max_chars_per_item: positiveInteger() }, { additionalProperties: false }),
|
|
332
328
|
async execute(_id, params, _signal, _update, ctx) {
|
|
333
329
|
const items = filteredItems(ctx, params);
|
|
334
|
-
if ("error" in items) return output(items);
|
|
335
330
|
return output({ items: items.slice(0, params.limit ?? items.length).map((item) => visibleItem(item, params.max_chars_per_item ?? 1200)) });
|
|
336
331
|
},
|
|
337
332
|
}));
|
|
@@ -340,10 +335,8 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
340
335
|
name: "history_read_item",
|
|
341
336
|
label: "History read item",
|
|
342
337
|
description: "Read a bounded character range from one durable session item.",
|
|
343
|
-
parameters: Type.Object({
|
|
338
|
+
parameters: Type.Object({ item_id: Type.String(), offset_chars: Type.Optional(Type.Integer({ minimum: 0 })), limit_chars: positiveInteger(), window_id: Type.String() }, { additionalProperties: false }),
|
|
344
339
|
async execute(_id, params, _signal, _update, ctx) {
|
|
345
|
-
const agentError = unsupportedAgent(params.agent_name);
|
|
346
|
-
if (agentError) return output(agentError);
|
|
347
340
|
const item = allItems(ctx).find((candidate) => candidate.windowId === params.window_id && candidate.itemId === params.item_id);
|
|
348
341
|
if (!item) return output({ error: "unknown item_id or window_id" });
|
|
349
342
|
const chars = Array.from(item.content);
|
|
@@ -357,10 +350,9 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
357
350
|
name: "history_search_contents",
|
|
358
351
|
label: "History search",
|
|
359
352
|
description: "Case-sensitive literal substring search over durable Pi session history; no semantic search.",
|
|
360
|
-
parameters: Type.Object({ limit: positiveInteger(), query: Type.String(), recent_first: Type.Optional(Type.Boolean()), tool_namespace: nullableString(), role: Type.Optional(role),
|
|
353
|
+
parameters: Type.Object({ limit: positiveInteger(), query: Type.String(), recent_first: Type.Optional(Type.Boolean()), tool_namespace: nullableString(), role: Type.Optional(role), tool_name: nullableString(), window_id: nullableString() }, { additionalProperties: false }),
|
|
361
354
|
async execute(_id, params, _signal, _update, ctx) {
|
|
362
355
|
const items = filteredItems(ctx, params);
|
|
363
|
-
if ("error" in items) return output(items);
|
|
364
356
|
const matching = items.filter((item) => item.content.includes(params.query));
|
|
365
357
|
return output({ items: matching.slice(0, params.limit ?? matching.length).map((item) => visibleItem(item)) });
|
|
366
358
|
},
|
|
@@ -490,6 +482,22 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
490
482
|
if (!enabled) return undefined; // Default Pi compaction applies; keepRecentTokens is honored again.
|
|
491
483
|
// Never let an aborted or failed custom reset fall through to Pi's default summary.
|
|
492
484
|
if (event.signal.aborted) return { cancel: true };
|
|
485
|
+
// Codex auto_compact_fallback_prompt parity, adapted to Pi's trigger points:
|
|
486
|
+
// - threshold, post-run (agent still streaming): cancel once per window and steer a
|
|
487
|
+
// note-taking turn in; _runAutoCompaction then returns hasQueuedMessages() and the
|
|
488
|
+
// post-run loop delivers it via agent.continue(). Safe, intended path.
|
|
489
|
+
// - threshold, pre-prompt (idle): cancelling still sends the user prompt with an
|
|
490
|
+
// over-threshold context, and sendMessage would race _runAgentPrompt. Reset instead.
|
|
491
|
+
// - overflow: never cancel; that would abandon Pi's one-shot compact-and-retry recovery.
|
|
492
|
+
if (event.reason === "threshold" && !ctx.isIdle()) {
|
|
493
|
+
const windowId = currentWindowId(ctx);
|
|
494
|
+
if (fallbackSentInWindow !== windowId) {
|
|
495
|
+
fallbackSentInWindow = windowId;
|
|
496
|
+
continueAfterFallback = true;
|
|
497
|
+
pi.sendMessage({ customType: FALLBACK_TYPE, content: FALLBACK_PROMPT, display: true }, { triggerTurn: true });
|
|
498
|
+
return { cancel: true };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
493
501
|
try {
|
|
494
502
|
pi.appendEntry(RESET_MARKER_TYPE, { version: 1, reason: event.reason, requested: rollover === "compacting" });
|
|
495
503
|
const markerId = ctx.sessionManager.getLeafId();
|
|
@@ -501,11 +509,18 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
501
509
|
});
|
|
502
510
|
|
|
503
511
|
pi.on("session_compact", (event, ctx) => {
|
|
504
|
-
if (!enabled)
|
|
512
|
+
if (!enabled) {
|
|
513
|
+
continueAfterFallback = false;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
505
516
|
// Overflow retry is already continued once by Pi core. Sending another turn would duplicate it.
|
|
506
517
|
if (event.willRetry) return;
|
|
507
|
-
|
|
508
|
-
|
|
518
|
+
// Continue after our own rollover, and after a fallback reset (Codex rolls over
|
|
519
|
+
// mid-turn and keeps going). A user's manual /compact gets no continuation.
|
|
520
|
+
const shouldContinue = rollover === "compacting" || continueAfterFallback;
|
|
521
|
+
continueAfterFallback = false;
|
|
522
|
+
if (rollover === "compacting") rollover = "continued";
|
|
523
|
+
if (!shouldContinue) return;
|
|
509
524
|
pi.appendEntry(STATE_TYPE, { version: 1, lastResetEntryId: event.compactionEntry.id });
|
|
510
525
|
persistHint(ctx);
|
|
511
526
|
pi.sendMessage({ customType: CONTINUATION_TYPE, content: CONTINUATION, display: false }, { triggerTurn: true });
|
|
@@ -516,4 +531,4 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
516
531
|
});
|
|
517
532
|
}
|
|
518
533
|
|
|
519
|
-
export const internal = { MAX_NOTE_BYTES, NOTE_TYPE, HINT_TYPE, GUIDANCE_TYPE, RESET_MARKER_TYPE, RESET_SUMMARY, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, GUIDANCE_OPEN_TAG, REMINDER_THRESHOLD_TOKENS, lineRange, assertVirtualPath };
|
|
534
|
+
export const internal = { MAX_NOTE_BYTES, NOTE_TYPE, HINT_TYPE, GUIDANCE_TYPE, FALLBACK_TYPE, FALLBACK_PROMPT, RESET_MARKER_TYPE, RESET_SUMMARY, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, GUIDANCE_OPEN_TAG, REMINDER_THRESHOLD_TOKENS, lineRange, assertVirtualPath };
|