pi-tuck 1.0.17 → 1.0.19
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/CHANGELOG.md +72 -0
- package/README.md +24 -7
- package/dist/index.js +7 -2
- package/dist/src/budget.d.ts +14 -17
- package/dist/src/budget.js +22 -29
- package/dist/src/build-id.d.ts +1 -1
- package/dist/src/build-id.js +1 -1
- package/dist/src/compaction.js +118 -40
- package/dist/src/diagnostics.d.ts +1 -1
- package/dist/src/history-tools.d.ts +16 -0
- package/dist/src/history-tools.js +59 -0
- package/dist/src/ledger.d.ts +0 -2
- package/dist/src/ledger.js +0 -2
- package/dist/src/omissions.d.ts +1 -1
- package/dist/src/omissions.js +88 -6
- package/dist/src/status.d.ts +19 -0
- package/dist/src/status.js +36 -0
- package/dist/src/tuck/core/brief.d.ts +6 -3
- package/dist/src/tuck/core/brief.js +27 -9
- package/dist/src/tuck/core/filter-noise.d.ts +1 -1
- package/dist/src/tuck/core/filter-noise.js +1 -3
- package/dist/src/tuck/core/normalize.d.ts +2 -5
- package/dist/src/tuck/core/normalize.js +15 -33
- package/dist/src/tuck/core/shrink.d.ts +6 -14
- package/dist/src/tuck/core/shrink.js +162 -102
- package/dist/src/tuck/core/summarize.d.ts +3 -14
- package/dist/src/tuck/core/summarize.js +9 -20
- package/dist/src/tuck/core/transcript.d.ts +5 -0
- package/dist/src/tuck/core/transcript.js +27 -4
- package/dist/src/tuck/types.d.ts +1 -1
- package/dist/src/tuck.d.ts +9 -17
- package/dist/src/tuck.js +384 -212
- package/index.ts +4 -2
- package/package.json +1 -1
- package/src/budget.ts +24 -26
- package/src/build-id.ts +1 -1
- package/src/compaction.ts +107 -38
- package/src/history-tools.ts +57 -0
- package/src/ledger.ts +0 -2
- package/src/omissions.ts +64 -7
- package/src/status.ts +43 -0
- package/src/tuck/core/brief.ts +25 -10
- package/src/tuck/core/filter-noise.ts +1 -3
- package/src/tuck/core/normalize.ts +11 -27
- package/src/tuck/core/shrink.ts +141 -91
- package/src/tuck/core/summarize.ts +9 -24
- package/src/tuck/core/transcript.ts +26 -4
- package/src/tuck/types.ts +3 -3
- package/src/tuck.ts +336 -185
package/dist/src/compaction.js
CHANGED
|
@@ -6,15 +6,17 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
6
6
|
import { assertNativeToolRounds } from "./protocol.js";
|
|
7
7
|
import { projectedMessages } from "./omissions.js";
|
|
8
8
|
import { applyClips, clipBudgets, clipFor } from "./clip.js";
|
|
9
|
-
import { DEFAULT_CUT_AT, bandOf, chooseCut, conversationTokens,
|
|
10
|
-
import { isContextOverflow } from "@earendil-works/pi-ai";
|
|
11
|
-
import { convertToLlm, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { DEFAULT_CUT_AT, bandOf, chooseCut, conversationTokens, gateCut, incomingEventTokens, incomingTokens, measurableConversationTokens, planningReserve, projectedConversationTokens, systemPart, validCutAt } from "./budget.js";
|
|
10
|
+
import { getCurrentSystemMessage, isContextOverflow } from "@earendil-works/pi-ai";
|
|
11
|
+
import { calculateContextTokens, convertToLlm, estimateTokens, shouldCompact, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Type } from "typebox";
|
|
13
13
|
import { Check } from "typebox/value";
|
|
14
14
|
import { MAX_RECALL_CHARACTERS, MAX_RECALL_ENTRY_ID_CHARACTERS, MAX_RECALL_QUERY_CHARACTERS } from "./recall.js";
|
|
15
15
|
import { tuck, tuckDraft, tuckObservedDraft, tuckRecall } from "./tuck.js";
|
|
16
|
+
import { LOADED_VERSION, statusText } from "./status.js";
|
|
16
17
|
import { ImageRecallInput, historyImage, historyAround } from "./history-image.js";
|
|
17
18
|
import { eligibilityKey, eligibilityKeyMoved, memoizedEligibleSources } from "./eligibility-cache.js";
|
|
19
|
+
import { applyHistoryTools, hasTuckCut, historyToolDeclarations, historyToolNames, stageHistoryTools } from "./history-tools.js";
|
|
18
20
|
const query = Type.String({ minLength: 1, maxLength: MAX_RECALL_QUERY_CHARACTERS });
|
|
19
21
|
const entryId = Type.String({ minLength: 1, maxLength: MAX_RECALL_ENTRY_ID_CHARACTERS });
|
|
20
22
|
const paging = {
|
|
@@ -208,6 +210,10 @@ export async function installCompaction(pi, options = {}) {
|
|
|
208
210
|
}
|
|
209
211
|
ctx.ui.notify(JSON.stringify({ ...semanticStatus(), ...(errors.length ? { errors } : {}) }), errors.length ? "warning" : "info");
|
|
210
212
|
} });
|
|
213
|
+
pi.registerCommand("tuck", { description: "pi-tuck's state in this session: version, cuts, cut point, search", handler: async (_args, ctx) => {
|
|
214
|
+
const { state, lastError } = semantic.status();
|
|
215
|
+
ctx.ui.notify(statusText({ version: LOADED_VERSION, active: active(), branch: ctx.sessionManager.getBranch(), cutAt: options.cutAt, search, semantic: { state, lastError } }), "info");
|
|
216
|
+
} });
|
|
211
217
|
// Pi's native start/settled events bracket the run and its post-turn cuts.
|
|
212
218
|
let admittedRun = false;
|
|
213
219
|
// One serial request, one accounting identity; observations never certify new content. A new identity (the first request after a session
|
|
@@ -319,12 +325,20 @@ export async function installCompaction(pi, options = {}) {
|
|
|
319
325
|
const measureFor = (ctx) => (messages) => projectedConversationTokens(applyClips(messages, clips), ctx, options.recallPolicy);
|
|
320
326
|
// The planner counts with the same function, except that it does not fail on a summary in another format: a cut can replace one
|
|
321
327
|
// (an operator's /compact repairs a session whose saved summary is foreign). Identical to the strict one for this compactor's summaries.
|
|
322
|
-
const plannerMeasureFor = (ctx) => (messages) => measurableConversationTokens(applyClips(messages, clips), ctx, options.recallPolicy);
|
|
328
|
+
const plannerMeasureFor = (ctx) => Object.assign((messages) => measurableConversationTokens(applyClips(messages, clips), ctx, options.recallPolicy), { messageTokens: (message) => conversationTokens(applyClips([message], clips)) });
|
|
323
329
|
const hostTokens = (ctx) => options.hostMessages ? conversationTokens(options.hostMessages(ctx)) : 0;
|
|
324
330
|
const toolShapes = (names) => {
|
|
325
331
|
const selected = new Set(names);
|
|
326
332
|
return pi.getAllTools().filter(tool => selected.has(tool.name)).map(tool => ({ name: tool.name, description: tool.description, parameters: tool.parameters }));
|
|
327
333
|
};
|
|
334
|
+
// Every decision prices the declaration set a cut must carry, even before the first cut enables our tools on the wire.
|
|
335
|
+
const requestSystemPart = (observedPrompt, names, saved, guardPrompt) => {
|
|
336
|
+
const shapes = toolShapes(historyToolNames(pi, names, true));
|
|
337
|
+
// Keep the guard's observed schemas (Pi may enrich a host tool's schema), and add our future declarations to that same view.
|
|
338
|
+
// Taking max(current guard, future raw shapes) alone could lose the enrichment when the future shapes are larger.
|
|
339
|
+
const future = saved.some(message => message.role === "system") ? historyToolDeclarations(saved, shapes, true) : saved;
|
|
340
|
+
return systemPart(observedPrompt, shapes, future, guardPrompt);
|
|
341
|
+
};
|
|
328
342
|
// Pi reports a manual cut with nothing to do as a failed compaction; the thrown error lacks the event's "Compaction failed: " prefix.
|
|
329
343
|
const isNoWork = (message) => ["Already compacted", "Nothing to compact (session too small)"].includes(message?.replace(/^Compaction failed: /, "") ?? "");
|
|
330
344
|
// A missing model is configuration, not capacity.
|
|
@@ -385,7 +399,9 @@ export async function installCompaction(pi, options = {}) {
|
|
|
385
399
|
forgetFailure();
|
|
386
400
|
phase = "decided";
|
|
387
401
|
try {
|
|
402
|
+
syncHistoryTools(ctx);
|
|
388
403
|
await preflight(ctx, incomingTokens(event.text, event.images), ctx.getSystemPrompt(), pi.getActiveTools());
|
|
404
|
+
syncHistoryTools(ctx);
|
|
389
405
|
}
|
|
390
406
|
catch (error) {
|
|
391
407
|
fail(error, ctx);
|
|
@@ -466,18 +482,17 @@ export async function installCompaction(pi, options = {}) {
|
|
|
466
482
|
if (operatorManual && !admittedRun)
|
|
467
483
|
forgetFailure();
|
|
468
484
|
access();
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
485
|
+
// pi-tuck's summary is built from the session itself, so text after /compact cannot steer it. The operator asked for a cut: make the one
|
|
486
|
+
// plain /compact makes and say the text was not used, rather than cancel (which Pi shows as an error, with no cut).
|
|
487
|
+
if (event.customInstructions?.trim())
|
|
488
|
+
ctx.ui.notify("pi-tuck: /compact ignores the text after it, since pi-tuck builds its summary from the session itself. Cutting as plain /compact does.", "info");
|
|
474
489
|
const { response, room } = limits(ctx);
|
|
475
490
|
const retain = operatorManual || admittedRun;
|
|
476
491
|
// A cut Pi asks for (an operator's /compact, Pi's threshold or overflow recovery) charges what the request must carry whatever is
|
|
477
492
|
// cut: the system part and the host input. A prompt's cut is pi-tuck's own and never comes here.
|
|
478
493
|
const saved = ctx.sessionManager.buildSessionProjection().messages;
|
|
479
494
|
const host = hostTokens(ctx);
|
|
480
|
-
const system =
|
|
495
|
+
const system = requestSystemPart(ctx.getSystemPrompt(), pi.getActiveTools(), saved, ctx.getSystemPrompt());
|
|
481
496
|
const mandatory = system + host, band = decisionBand(ctx, room, system);
|
|
482
497
|
const before = plannerMeasureFor(ctx)(saved);
|
|
483
498
|
// Pi's own usage threshold (reason "threshold") counts the last reply's output tokens, reasoning included, plus the messages after
|
|
@@ -528,6 +543,15 @@ export async function installCompaction(pi, options = {}) {
|
|
|
528
543
|
});
|
|
529
544
|
pi.on("session_before_tree", (event) => event.preparation.userWantsSummary ? { cancel: true } : undefined);
|
|
530
545
|
pi.on("session_start", () => { clips.clear(); promptCut = undefined; recorder.stop("session-transition"); proposalId = undefined; failure = undefined; admittedRun = false; pendingBoundaryCommit = false; identity = ""; factor = 1; pending = undefined; phase = "idle"; });
|
|
546
|
+
const historyToolsEnabled = (ctx) => Boolean(promptCut) || hasTuckCut(ctx.sessionManager.getBranch());
|
|
547
|
+
const syncHistoryTools = (ctx) => { if (active())
|
|
548
|
+
applyHistoryTools(pi, historyToolsEnabled(ctx)); };
|
|
549
|
+
pi.on("session_start", (_event, ctx) => syncHistoryTools(ctx));
|
|
550
|
+
pi.on("session_tree", (_event, ctx) => syncHistoryTools(ctx));
|
|
551
|
+
pi.on("session_compact", (_event, ctx) => { if (!admittedRun)
|
|
552
|
+
syncHistoryTools(ctx); });
|
|
553
|
+
pi.on("turn_start", (_event, ctx) => syncHistoryTools(ctx));
|
|
554
|
+
pi.on("agent_settled", (_event, ctx) => syncHistoryTools(ctx));
|
|
531
555
|
// The session as it will be once a cut that is not saved yet is saved, and its conversation as the next request carries it.
|
|
532
556
|
const cutSession = (ctx, cut) => {
|
|
533
557
|
const header = ctx.sessionManager.getHeader();
|
|
@@ -542,7 +566,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
542
566
|
// custom message starts). The cut is pi-tuck's own, planned as at a tool boundary. It is not Pi's: Pi's cut finder keeps the last
|
|
543
567
|
// keepRecentTokens, and where those are the whole conversation it has nothing to cut, so every later prompt was refused for good (t06 at
|
|
544
568
|
// Pi's defaults on a 32k window). Each request of the run carries the cut, and the run's first boundary saves it.
|
|
545
|
-
const preflight = (ctx, incomingPart, observedPrompt, tools) => {
|
|
569
|
+
const preflight = (ctx, incomingPart, observedPrompt, tools, hostSnapshot) => {
|
|
546
570
|
promptCut = undefined;
|
|
547
571
|
promptCutAt = ctx.sessionManager.getBranch().length;
|
|
548
572
|
if (!active())
|
|
@@ -555,9 +579,9 @@ export async function installCompaction(pi, options = {}) {
|
|
|
555
579
|
const { response, room } = limits(ctx);
|
|
556
580
|
// Public early view only: later handlers, image normalization and tool-loop growth remain the complete-context guard's responsibility.
|
|
557
581
|
const saved = ctx.sessionManager.buildSessionProjection().messages;
|
|
558
|
-
const system =
|
|
582
|
+
const system = requestSystemPart(observedPrompt, tools, saved, ctx.getSystemPrompt());
|
|
559
583
|
const band = decisionBand(ctx, room, system);
|
|
560
|
-
const host = hostTokens(ctx), measure = measureFor(ctx), before = measure(saved);
|
|
584
|
+
const host = hostSnapshot ?? hostTokens(ctx), measure = measureFor(ctx), before = measure(saved);
|
|
561
585
|
const incoming = incomingPart + system, mandatory = incoming + host;
|
|
562
586
|
if (!fits(incoming, room, ctx))
|
|
563
587
|
throw new CapacityError("Incoming Pi prompt and response reserve cannot fit the estimated request budget");
|
|
@@ -594,10 +618,43 @@ export async function installCompaction(pi, options = {}) {
|
|
|
594
618
|
const prompted = phase === "decided";
|
|
595
619
|
phase = "starting";
|
|
596
620
|
try {
|
|
621
|
+
syncHistoryTools(ctx);
|
|
597
622
|
if (prompted)
|
|
598
623
|
access(); // decided on input; a failure latched there still stops the run
|
|
599
624
|
else
|
|
600
|
-
await preflight(ctx, incomingEventTokens(event), event.systemPrompt, event.systemPromptOptions.selectedTools);
|
|
625
|
+
await preflight(ctx, incomingEventTokens(event), event.systemPrompt, historyToolNames(pi, event.systemPromptOptions.selectedTools, historyToolsEnabled(ctx)));
|
|
626
|
+
// Pi declares tool changes before turn_start, then snapshots executable tools in prepareRequest. Introduce our tools in
|
|
627
|
+
// the request projection: recording an activation message here would shift every later session-global recall index.
|
|
628
|
+
if (historyToolsEnabled(ctx))
|
|
629
|
+
stageHistoryTools(pi, ctx.sessionManager.buildSessionProjection().messages);
|
|
630
|
+
else
|
|
631
|
+
applyHistoryTools(pi, false);
|
|
632
|
+
const staged = pi.getActiveTools();
|
|
633
|
+
event.systemPromptOptions.selectedTools = historyToolNames(pi, event.systemPromptOptions.selectedTools, true).filter(name => name !== "tuck_recall" && name !== "tuck_search" || staged.includes(name));
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
fail(error, ctx);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
// The incoming user message is about to be appended (including steering inside a run), and later before_agent_start/turn_start
|
|
640
|
+
// handlers have run. Pi's prepareRequest has not yet snapshotted executable tools. A first cut discovered in context_with_system
|
|
641
|
+
// would be too late for that snapshot, even if its declarations were repaired there, so make the late prompt decision here.
|
|
642
|
+
pi.on("message_end", (event, ctx) => {
|
|
643
|
+
if (event.message.role !== "user" || !active() || !admittedRun || promptCut)
|
|
644
|
+
return;
|
|
645
|
+
try {
|
|
646
|
+
access();
|
|
647
|
+
if (!ctx.model)
|
|
648
|
+
throw missingModel();
|
|
649
|
+
const message = event.message;
|
|
650
|
+
const incoming = incomingTokens(typeof message.content === "string" ? message.content : message.content.flatMap(b => b.type === "text" ? [b.text] : []).join("\n"), typeof message.content === "string" ? [] : message.content.filter((b) => b.type === "image"));
|
|
651
|
+
const saved = ctx.sessionManager.buildSessionProjection().messages;
|
|
652
|
+
const host = hostTokens(ctx);
|
|
653
|
+
const system = requestSystemPart(ctx.getSystemPrompt(), pi.getActiveTools(), saved, ctx.getSystemPrompt());
|
|
654
|
+
if (fits(conversationTokens(saved) + system + incoming + host, limits(ctx).room, ctx))
|
|
655
|
+
return;
|
|
656
|
+
preflight(ctx, incoming, ctx.getSystemPrompt(), pi.getActiveTools(), host);
|
|
657
|
+
syncHistoryTools(ctx);
|
|
601
658
|
}
|
|
602
659
|
catch (error) {
|
|
603
660
|
fail(error, ctx);
|
|
@@ -620,11 +677,28 @@ export async function installCompaction(pi, options = {}) {
|
|
|
620
677
|
// One snapshot per decision. Messages queued for the next request (steering, follow-ups) are in that request but not in the
|
|
621
678
|
// saved conversation. Pi's peek returns the steering queue, else the follow-up queue; a follow-up is not in the very next
|
|
622
679
|
// request while tools still run, so it can be counted early: conservative, and nothing else would cut at that later boundary.
|
|
623
|
-
const system =
|
|
680
|
+
const system = requestSystemPart(ctx.getSystemPrompt(), pi.getActiveTools(), saved, ctx.getSystemPrompt());
|
|
624
681
|
const band = decisionBand(ctx, room, system);
|
|
625
682
|
const host = hostTokens(ctx);
|
|
626
683
|
const queued = event.context.pendingMessages.length ? conversationTokens(event.context.pendingMessages) : 0;
|
|
627
684
|
const mandatory = system + host + queued;
|
|
685
|
+
// Without usable provider usage, Pi's own threshold uses its unwrapped message estimate as the compiler's density input.
|
|
686
|
+
// Our declarations are request-only, so Pi can miss that threshold. Preserve the same density input at settlement; every
|
|
687
|
+
// budget decision below still uses `system` and the complete conversation measure, including the summary envelope.
|
|
688
|
+
let densityTokens;
|
|
689
|
+
const lastAssistant = saved.findLast(message => message.role === "assistant");
|
|
690
|
+
const lastCut = ctx.sessionManager.getBranch().findLast(entry => entry.type === "compaction");
|
|
691
|
+
if (reason === "run-end" && lastAssistant && (!lastCut || lastAssistant.timestamp > Date.parse(lastCut.timestamp)) &&
|
|
692
|
+
!saved.some(message => message.role === "assistant" &&
|
|
693
|
+
!["error", "aborted"].includes(message.stopReason) && message.usage && calculateContextTokens(message.usage) > 0)) {
|
|
694
|
+
const forecast = historyToolDeclarations(saved, toolShapes(historyToolNames(pi, pi.getActiveTools(), true)), true);
|
|
695
|
+
if (forecast !== saved) {
|
|
696
|
+
const head = getCurrentSystemMessage(convertToLlm(forecast));
|
|
697
|
+
const native = (head ? estimateTokens(head) : 0) + saved.reduce((sum, message) => sum + (message.role === "system" ? 0 : estimateTokens(message)), 0);
|
|
698
|
+
if (shouldCompact(native, ctx.model.contextWindow, settingsFor().getCompactionSettings(ctx.model)))
|
|
699
|
+
densityTokens = native;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
628
702
|
const previousSummary = saved.find(message => message.role === "compactionSummary")?.summary;
|
|
629
703
|
// The cut previews this decision has built, in the order it built them. The clip retry below measures them for what a cut leaves the
|
|
630
704
|
// request besides the fresh results. Reset per decision, so a retry sizes itself against the cut it is retrying, not an earlier attempt's.
|
|
@@ -634,18 +708,22 @@ export async function installCompaction(pi, options = {}) {
|
|
|
634
708
|
previews = [];
|
|
635
709
|
floorKept = undefined;
|
|
636
710
|
const before = measure(saved);
|
|
637
|
-
|
|
638
|
-
|
|
711
|
+
const gate = gateCut({ total: before + mandatory, mandatory, room, band, fits: (tokens, limit) => fits(tokens, limit, ctx) });
|
|
712
|
+
if (gate.kind !== "plan")
|
|
713
|
+
return gate;
|
|
714
|
+
return chooseCut({
|
|
715
|
+
opportunistic: gate.opportunistic, mandatory, before, room, band, fits: (tokens, limit) => fits(tokens, limit, ctx), usefulOnly: options.cutAt === undefined,
|
|
639
716
|
unreduced: "Tool-loop preflight compaction did not reduce context within the estimated request budget",
|
|
640
717
|
plan: (target) => {
|
|
641
718
|
const reserve = planReserve(target, room, band, mandatory, ctx);
|
|
642
|
-
const draft = tuckObservedDraft(tuck, { branchEntries: ctx.sessionManager.getBranch(), tokensBefore: before, previousSummary }, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, true);
|
|
719
|
+
const draft = tuckObservedDraft(tuck, { branchEntries: ctx.sessionManager.getBranch(), tokensBefore: densityTokens ?? before, previousSummary }, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, true);
|
|
643
720
|
if (!draft.compaction) {
|
|
644
721
|
// The planner's own floor, so a clip's second budget is measured against what the smallest cut really keeps.
|
|
645
722
|
if (draft.floorMessages && event.toolResults) {
|
|
646
723
|
const fresh = new Set(event.toolResults.map(result => result.toolCallId));
|
|
647
724
|
const kept = measure(draft.floorMessages.filter(message => !(message.role === "toolResult" && fresh.has(message.toolCallId))));
|
|
648
|
-
|
|
725
|
+
// Both targets must leave their protected openings beside the clip.
|
|
726
|
+
floorKept = Math.max(floorKept ?? kept, kept);
|
|
649
727
|
}
|
|
650
728
|
return { draft: false, capacityRejected: Boolean(draft.capacityRejected), reason: draft.unavailable };
|
|
651
729
|
}
|
|
@@ -671,7 +749,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
671
749
|
const branch = ctx.sessionManager.getBranch();
|
|
672
750
|
const entryIdOf = (toolCallId) => branch.findLast(entry => entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolCallId === toolCallId)?.id;
|
|
673
751
|
const fresh = new Set(event.toolResults.map(result => result.toolCallId));
|
|
674
|
-
const kept = previews.length ? Math.
|
|
752
|
+
const kept = previews.length ? Math.max(...previews.map(preview => measure(preview.filter(message => !(message.role === "toolResult" && fresh.has(message.toolCallId)))))) : floorKept;
|
|
675
753
|
for (const budget of clipBudgets(room, factor, mandatory, kept)) {
|
|
676
754
|
const round = clipFor(event.toolResults, entryIdOf, budget);
|
|
677
755
|
if (!round.length)
|
|
@@ -720,13 +798,24 @@ export async function installCompaction(pi, options = {}) {
|
|
|
720
798
|
if (!promptCut && !decide)
|
|
721
799
|
return;
|
|
722
800
|
const result = atBoundary(event, ctx, "tool-loop", decide);
|
|
723
|
-
if (result)
|
|
801
|
+
if (result) {
|
|
724
802
|
pendingBoundaryCommit = true;
|
|
803
|
+
}
|
|
725
804
|
return result;
|
|
726
805
|
}
|
|
727
806
|
catch (error) {
|
|
728
807
|
fail(error, ctx);
|
|
729
808
|
}
|
|
809
|
+
finally {
|
|
810
|
+
// Keep our part of Pi's next declaration snapshot unchanged; turn_start restores the branch-derived executable set before
|
|
811
|
+
// prepareRequest. This avoids an extra persisted system message at the first cut and on later rounds/runs.
|
|
812
|
+
if (active()) {
|
|
813
|
+
if (historyToolsEnabled(ctx))
|
|
814
|
+
stageHistoryTools(pi, ctx.sessionManager.buildSessionProjection().messages);
|
|
815
|
+
else
|
|
816
|
+
applyHistoryTools(pi, false);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
730
819
|
});
|
|
731
820
|
// A request of a run whose prompt's cut is not saved yet: the saved conversation in it, which other handlers may surround, is replaced by
|
|
732
821
|
// the conversation with the cut applied. Pi sends the saved system message apart from the request's messages, so neither side carries it
|
|
@@ -743,6 +832,9 @@ export async function installCompaction(pi, options = {}) {
|
|
|
743
832
|
try {
|
|
744
833
|
if (!active())
|
|
745
834
|
return;
|
|
835
|
+
// Before a cut, Pi owns the whole request, including its object identities and system-message serialization.
|
|
836
|
+
if (!historyToolsEnabled(ctx))
|
|
837
|
+
return clips.size ? { messages: applyClips(event.messages, clips) } : undefined;
|
|
746
838
|
access();
|
|
747
839
|
return { messages: applyClips(projectedMessages(promptCut ? cutRequest(event.messages, ctx, promptCut) : event.messages, ctx, options.recallPolicy), clips) };
|
|
748
840
|
}
|
|
@@ -751,7 +843,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
751
843
|
fail(error, ctx);
|
|
752
844
|
}
|
|
753
845
|
finally {
|
|
754
|
-
return { messages: [] };
|
|
846
|
+
return historyToolsEnabled(ctx) ? { messages: [] } : undefined;
|
|
755
847
|
}
|
|
756
848
|
}
|
|
757
849
|
});
|
|
@@ -807,24 +899,10 @@ export async function installCompaction(pi, options = {}) {
|
|
|
807
899
|
throw missingModel();
|
|
808
900
|
// The complete request, counted after Pi's conversion (summary/custom/bash envelopes), with the effective system once.
|
|
809
901
|
// Pi applies a forced prompt after this hook; the public prompt already observes it.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (!fits(tokens, limits(ctx).room, ctx) && !promptCut) {
|
|
815
|
-
const branch = ctx.sessionManager.getBranch(), last = branch.at(-1);
|
|
816
|
-
if (last?.type === "message" && last.message.role === "user") {
|
|
817
|
-
const header = ctx.sessionManager.getHeader();
|
|
818
|
-
const before = header && SessionManager.inMemory(ctx.cwd, undefined, [header, ...branch.slice(0, -1)]);
|
|
819
|
-
if (before) {
|
|
820
|
-
preflight(Object.create(ctx, { sessionManager: { value: before } }), incomingTokens(typeof last.message.content === "string" ? last.message.content : last.message.content.flatMap(b => b.type === "text" ? [b.text] : []).join("\n"), typeof last.message.content === "string" ? [] : last.message.content.filter((b) => b.type === "image")), ctx.getSystemPrompt(), pi.getActiveTools());
|
|
821
|
-
if (promptCut) {
|
|
822
|
-
messages = cutRequest(event.messages, ctx, promptCut);
|
|
823
|
-
tokens = requestTokens(messages, ctx.getSystemPrompt());
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
902
|
+
syncHistoryTools(ctx);
|
|
903
|
+
const messages = historyToolsEnabled(ctx) ? historyToolDeclarations(event.messages, toolShapes(pi.getActiveTools()), true) : event.messages;
|
|
904
|
+
const system = requestSystemPart(ctx.getSystemPrompt(), pi.getActiveTools(), messages, ctx.getSystemPrompt());
|
|
905
|
+
const tokens = system + conversationTokens(messages);
|
|
828
906
|
if (!fits(tokens, limits(ctx).room, ctx))
|
|
829
907
|
throw new CapacityError("Complete Pi context and response reserve exceed the estimated request budget");
|
|
830
908
|
if (recorder.enabled && !recorder.waitingForCut)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RecallPolicy } from './recall.js';
|
|
2
2
|
import type { SessionEntry } from '@earendil-works/pi-coding-agent';
|
|
3
|
-
export declare const DIAGNOSTIC_BUILD = "
|
|
3
|
+
export declare const DIAGNOSTIC_BUILD = "3bbefa84bf1600296d12131a405f9feaa8fb1ea26ca1a85f5292b40a0a57333f";
|
|
4
4
|
export interface Receipt {
|
|
5
5
|
id: string;
|
|
6
6
|
build: string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Messages, ToolShape } from "./budget.js";
|
|
3
|
+
/** Native Pi compactions do not make pi-tuck history available. No session-wide activation state. */
|
|
4
|
+
export declare const hasTuckCut: (branch: readonly {
|
|
5
|
+
type: string;
|
|
6
|
+
details?: unknown;
|
|
7
|
+
}[]) => boolean;
|
|
8
|
+
/** Preserve the host's loadout and its order; only our registered tools may change. */
|
|
9
|
+
export declare function historyToolNames(pi: ExtensionAPI, names: readonly string[], enabled: boolean): string[];
|
|
10
|
+
export declare function applyHistoryTools(pi: ExtensionAPI, enabled: boolean): void;
|
|
11
|
+
/** Match only our part of Pi's saved declaration snapshot, so its loop records no activation/removal message. Legacy sessions can
|
|
12
|
+
* already declare our tools; new sessions declare them only in the request projection. Executable tools are restored at turn_start. */
|
|
13
|
+
export declare function stageHistoryTools(pi: ExtensionAPI, messages: Messages): string[];
|
|
14
|
+
/** Pi has already declared the request's tools before queued user messages are appended. A prompt cut planned on such a message needs its declarations
|
|
15
|
+
* in that very request. Change only our declarations, leaving all other system content and tools in their original places. */
|
|
16
|
+
export declare function historyToolDeclarations(messages: Messages, tools: readonly ToolShape[], enabled: boolean): Messages;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getCurrentTools } from "@earendil-works/pi-ai";
|
|
2
|
+
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
const own = (name) => name === "tuck_recall" || name === "tuck_search";
|
|
4
|
+
/** Native Pi compactions do not make pi-tuck history available. No session-wide activation state. */
|
|
5
|
+
export const hasTuckCut = (branch) => branch.some(entry => entry.type === "compaction" && entry.details?.compactor === "pi-tuck");
|
|
6
|
+
/** Preserve the host's loadout and its order; only our registered tools may change. */
|
|
7
|
+
export function historyToolNames(pi, names, enabled) {
|
|
8
|
+
if (!enabled)
|
|
9
|
+
return names.filter(name => !own(name));
|
|
10
|
+
const registered = new Set(pi.getAllTools().map(tool => tool.name));
|
|
11
|
+
return [...names, ...["tuck_recall", "tuck_search"].filter(name => registered.has(name) && !names.includes(name))];
|
|
12
|
+
}
|
|
13
|
+
export function applyHistoryTools(pi, enabled) {
|
|
14
|
+
const current = pi.getActiveTools(), next = historyToolNames(pi, current, enabled);
|
|
15
|
+
if (current.length !== next.length || current.some((name, index) => name !== next[index]))
|
|
16
|
+
pi.setActiveTools(next);
|
|
17
|
+
}
|
|
18
|
+
/** Match only our part of Pi's saved declaration snapshot, so its loop records no activation/removal message. Legacy sessions can
|
|
19
|
+
* already declare our tools; new sessions declare them only in the request projection. Executable tools are restored at turn_start. */
|
|
20
|
+
export function stageHistoryTools(pi, messages) {
|
|
21
|
+
const declared = new Set(getCurrentTools(convertToLlm(messages)).map(tool => tool.name));
|
|
22
|
+
const current = pi.getActiveTools();
|
|
23
|
+
const next = historyToolNames(pi, current, true).filter(name => !own(name) || declared.has(name));
|
|
24
|
+
if (current.length !== next.length || current.some((name, index) => name !== next[index]))
|
|
25
|
+
pi.setActiveTools(next);
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
/** Pi has already declared the request's tools before queued user messages are appended. A prompt cut planned on such a message needs its declarations
|
|
29
|
+
* in that very request. Change only our declarations, leaving all other system content and tools in their original places. */
|
|
30
|
+
export function historyToolDeclarations(messages, tools, enabled) {
|
|
31
|
+
let changed = false;
|
|
32
|
+
const filtered = messages.map(message => {
|
|
33
|
+
if (message.role !== "system")
|
|
34
|
+
return message;
|
|
35
|
+
const added = message.toolsAdded?.filter(tool => !own(tool.name));
|
|
36
|
+
const removed = message.toolsRemoved?.filter(tool => !own(tool.name));
|
|
37
|
+
if (added?.length === message.toolsAdded?.length && removed?.length === message.toolsRemoved?.length)
|
|
38
|
+
return message;
|
|
39
|
+
changed = true;
|
|
40
|
+
const { toolsAdded: _, toolsRemoved: __, ...rest } = message;
|
|
41
|
+
return { ...rest, ...(added?.length ? { toolsAdded: added } : {}), ...(removed?.length ? { toolsRemoved: removed } : {}) };
|
|
42
|
+
});
|
|
43
|
+
// Leave an already-correct transcript byte-identical, including the original placement of our declarations.
|
|
44
|
+
const declared = getCurrentTools(convertToLlm(messages));
|
|
45
|
+
const wanted = enabled ? tools.filter(tool => own(tool.name)) : [];
|
|
46
|
+
if (enabled && wanted.every(tool => declared.some(current => own(current.name) && current.name === tool.name)) &&
|
|
47
|
+
declared.filter(tool => own(tool.name)).length === wanted.length)
|
|
48
|
+
return messages;
|
|
49
|
+
if (!enabled || !wanted.length)
|
|
50
|
+
return changed ? filtered : messages;
|
|
51
|
+
const at = filtered.findLastIndex(message => message.role === "system");
|
|
52
|
+
if (at < 0)
|
|
53
|
+
throw new Error("Missing Pi system message for history tool declarations");
|
|
54
|
+
const system = filtered[at];
|
|
55
|
+
if (system.role !== "system")
|
|
56
|
+
throw new Error("Missing Pi system message");
|
|
57
|
+
filtered[at] = { ...system, toolsAdded: [...(system.toolsAdded ?? []), ...wanted] };
|
|
58
|
+
return filtered;
|
|
59
|
+
}
|
package/dist/src/ledger.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { TranscriptRecord } from "./tuck/core/transcript.js";
|
|
3
|
-
/** The initiating prompt is quoted whole up to this many characters; the rest stays readable through tuck_recall. */
|
|
4
|
-
export declare const MAX_TASK_CONTRACT_CHARACTERS = 12000;
|
|
5
3
|
/** Owner default; older distinct paths are counted, not listed. */
|
|
6
4
|
export declare const MAX_LEDGER_LINES = 60;
|
|
7
5
|
export declare const LEDGER_HEADING = "Files touched by read, write and edit before this summary, newest first, with the last operation:";
|
package/dist/src/ledger.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { isLookupTool } from "./tuck/core/recall-operation.js";
|
|
2
2
|
import { isDiscardedResponse } from "./protocol.js";
|
|
3
3
|
import { extractPath } from "./tuck/core/tool-args.js";
|
|
4
|
-
/** The initiating prompt is quoted whole up to this many characters; the rest stays readable through tuck_recall. */
|
|
5
|
-
export const MAX_TASK_CONTRACT_CHARACTERS = 12000;
|
|
6
4
|
/** Owner default; older distinct paths are counted, not listed. */
|
|
7
5
|
export const MAX_LEDGER_LINES = 60;
|
|
8
6
|
const LEDGER_TITLE = "Files";
|
package/dist/src/omissions.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type RecallPolicy } from './recall.js';
|
|
|
3
3
|
/** Native branch edits supply exclusion; original global indices supply provenance. */
|
|
4
4
|
export declare function omittedSourceIndices(entries: readonly SessionEntry[], branch: readonly SessionEntry[]): ReadonlySet<number>;
|
|
5
5
|
/** Preserve surviving record bytes/order, and the summary's format (a v1 summary stays v1). No text matching or semantic inference. */
|
|
6
|
-
export declare function filterOmittedSummary(summary: string, omitted: ReadonlySet<number>): string;
|
|
6
|
+
export declare function filterOmittedSummary(summary: string, omitted: ReadonlySet<number>, sources?: readonly SessionEntry[], rangeOmitted?: ReadonlySet<number>): string;
|
|
7
7
|
/** Custom quotes carry native entry identity; no text matching or legacy fallback. */
|
|
8
8
|
export declare function filterCustomQuotes(summary: string, entries: readonly SessionEntry[], branch: readonly SessionEntry[], policy?: RecallPolicy): string;
|
|
9
9
|
type ProjectedMessages = ReturnType<ExtensionContext['sessionManager']['buildSessionProjection']>['messages'];
|
package/dist/src/omissions.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { buildGlobalIndexById } from './tuck/core/global-indices.js';
|
|
2
|
-
import { parseRecords, splitSummary, transcriptHeader } from './tuck/core/transcript.js';
|
|
2
|
+
import { parseRecords, serializeRecord, splitSummary, transcriptHeader } from './tuck/core/transcript.js';
|
|
3
|
+
import { textOf } from "./tuck/core/content.js";
|
|
4
|
+
import { isDiscardedResponse } from "./protocol.js";
|
|
3
5
|
import { createRecall } from './recall.js';
|
|
4
6
|
/** Native branch edits supply exclusion; original global indices supply provenance. */
|
|
5
7
|
export function omittedSourceIndices(entries, branch) {
|
|
6
|
-
const indices = buildGlobalIndexById(entries);
|
|
7
8
|
const edits = new Set();
|
|
8
9
|
for (const entry of branch)
|
|
9
10
|
if (entry.type === 'context_edit') {
|
|
@@ -11,10 +12,13 @@ export function omittedSourceIndices(entries, branch) {
|
|
|
11
12
|
throw new Error('pi-tuck does not support replacement context edits');
|
|
12
13
|
edits.add(entry.targetId);
|
|
13
14
|
}
|
|
15
|
+
if (!edits.size)
|
|
16
|
+
return new Set();
|
|
17
|
+
const indices = buildGlobalIndexById(entries);
|
|
14
18
|
return new Set([...edits].flatMap(id => indices.has(id) ? [indices.get(id)] : []));
|
|
15
19
|
}
|
|
16
20
|
/** Preserve surviving record bytes/order, and the summary's format (a v1 summary stays v1). No text matching or semantic inference. */
|
|
17
|
-
export function filterOmittedSummary(summary, omitted) {
|
|
21
|
+
export function filterOmittedSummary(summary, omitted, sources, rangeOmitted = omitted) {
|
|
18
22
|
if (!omitted.size)
|
|
19
23
|
return summary;
|
|
20
24
|
const split = splitSummary(summary);
|
|
@@ -22,7 +26,71 @@ export function filterOmittedSummary(summary, omitted) {
|
|
|
22
26
|
throw new Error('Cannot apply native source omission to an unsupported compaction summary');
|
|
23
27
|
const { body, closing, version } = split;
|
|
24
28
|
const records = parseRecords(body, version), lines = body ? body.split('\n') : [];
|
|
25
|
-
|
|
29
|
+
// A broad range does not identify the role of an intervening source. Without
|
|
30
|
+
// original sources there is no evidence that an edit removes a counted unit.
|
|
31
|
+
const indices = sources && rangeOmitted.size && records.some(record => record.sourceRange) ? buildGlobalIndexById(sources) : undefined;
|
|
32
|
+
const sourceUnits = new Map();
|
|
33
|
+
const shownBySource = new Map();
|
|
34
|
+
for (const record of records)
|
|
35
|
+
if (record.kind === 'excerpt' && record.sourceIndex !== undefined && !record.span) {
|
|
36
|
+
const key = `${record.role}:${record.sourceIndex}`, shown = shownBySource.get(key) ?? [];
|
|
37
|
+
shown.push(record.text);
|
|
38
|
+
shownBySource.set(key, shown);
|
|
39
|
+
}
|
|
40
|
+
if (indices && sources)
|
|
41
|
+
for (const entry of sources) {
|
|
42
|
+
if (entry.type !== 'message')
|
|
43
|
+
continue;
|
|
44
|
+
const index = indices.get(entry.id);
|
|
45
|
+
if (index === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
const message = entry.message;
|
|
48
|
+
const shown = shownBySource.get(`${message.role}:${index}`) ?? [];
|
|
49
|
+
if (message.role === 'user') {
|
|
50
|
+
const text = textOf(message.content);
|
|
51
|
+
sourceUnits.set(index, { role: 'user', missing: Number(Boolean(text.length) && !shown.includes(text)) });
|
|
52
|
+
}
|
|
53
|
+
else if (message.role === 'assistant' && !isDiscardedResponse(message)) {
|
|
54
|
+
const available = [...shown];
|
|
55
|
+
let missing = 0;
|
|
56
|
+
for (const part of message.content)
|
|
57
|
+
if (part.type === 'text' && part.text.trim()) {
|
|
58
|
+
const at = available.indexOf(part.text);
|
|
59
|
+
if (at < 0)
|
|
60
|
+
missing++;
|
|
61
|
+
else
|
|
62
|
+
available.splice(at, 1);
|
|
63
|
+
}
|
|
64
|
+
sourceUnits.set(index, { role: 'assistant', missing });
|
|
65
|
+
}
|
|
66
|
+
else if (message.role === 'toolResult')
|
|
67
|
+
sourceUnits.set(index, { role: 'toolResult', missing: 1 });
|
|
68
|
+
}
|
|
69
|
+
const retained = records.flatMap((record, at) => {
|
|
70
|
+
if ((record.sourceIndex !== undefined && omitted.has(record.sourceIndex)) || record.sourceIndices?.some(index => omitted.has(index)))
|
|
71
|
+
return [];
|
|
72
|
+
if (!record.sourceRange || !indices || record.count === undefined)
|
|
73
|
+
return [lines[at]];
|
|
74
|
+
const range = record.sourceRange;
|
|
75
|
+
let removed = 0;
|
|
76
|
+
const remaining = [];
|
|
77
|
+
for (const [index, unit] of sourceUnits) {
|
|
78
|
+
if (index < range.start || index > range.end || unit.role !== record.role || !unit.missing)
|
|
79
|
+
continue;
|
|
80
|
+
if (rangeOmitted.has(index))
|
|
81
|
+
removed += unit.missing;
|
|
82
|
+
else if (!omitted.has(index))
|
|
83
|
+
remaining.push(index);
|
|
84
|
+
}
|
|
85
|
+
if (!removed)
|
|
86
|
+
return [lines[at]];
|
|
87
|
+
const count = record.count - removed;
|
|
88
|
+
if (count <= 0)
|
|
89
|
+
return [];
|
|
90
|
+
const sourceRange = remaining.length ? { start: Math.min(...remaining), end: Math.max(...remaining) } : range;
|
|
91
|
+
const adjusted = { ...record, count, sourceRange };
|
|
92
|
+
return [version === 1 ? JSON.stringify(adjusted) : serializeRecord(adjusted)];
|
|
93
|
+
});
|
|
26
94
|
return transcriptHeader(version) + '\n' + retained.join('\n') + closing;
|
|
27
95
|
}
|
|
28
96
|
/** Custom quotes carry native entry identity; no text matching or legacy fallback. */
|
|
@@ -31,6 +99,9 @@ export function filterCustomQuotes(summary, entries, branch, policy = {}) {
|
|
|
31
99
|
if (!split)
|
|
32
100
|
throw new Error('Cannot resolve custom sources in an unsupported compaction summary');
|
|
33
101
|
const { body, closing, version } = split;
|
|
102
|
+
const records = parseRecords(body, version);
|
|
103
|
+
if (!records.some(record => record.role === "custom"))
|
|
104
|
+
return summary;
|
|
34
105
|
const omitted = new Set(branch.flatMap(entry => entry.type === 'context_edit' && entry.replacement === null ? [entry.targetId] : []));
|
|
35
106
|
const eligible = createRecall(policy).isHistorySource, indices = buildGlobalIndexById(entries);
|
|
36
107
|
const globalCounts = new Map(), branchSources = new Map();
|
|
@@ -42,7 +113,6 @@ export function filterCustomQuotes(summary, entries, branch, policy = {}) {
|
|
|
42
113
|
branchSources.set(entry.id, found);
|
|
43
114
|
}
|
|
44
115
|
const lines = body ? body.split('\n') : [];
|
|
45
|
-
const records = parseRecords(body, version);
|
|
46
116
|
const retained = records.flatMap((record, i) => {
|
|
47
117
|
if (record.role !== 'custom')
|
|
48
118
|
return [lines[i]];
|
|
@@ -62,8 +132,20 @@ export function filterCustomQuotes(summary, entries, branch, policy = {}) {
|
|
|
62
132
|
function projectMessages(messages, ctx, policy, allowForeign) {
|
|
63
133
|
const entries = ctx.sessionManager.getEntries(), branch = ctx.sessionManager.getBranch();
|
|
64
134
|
const omitted = omittedSourceIndices(entries, branch);
|
|
135
|
+
const lastCut = branch.findLastIndex(entry => entry.type === 'compaction');
|
|
136
|
+
const cut = branch[lastCut];
|
|
137
|
+
const afterCut = new Set();
|
|
138
|
+
if (omitted.size && cut?.type === 'compaction') {
|
|
139
|
+
const indices = buildGlobalIndexById(entries);
|
|
140
|
+
for (const entry of branch.slice(lastCut + 1))
|
|
141
|
+
if (entry.type === 'context_edit' && entry.replacement === null) {
|
|
142
|
+
const index = indices.get(entry.targetId);
|
|
143
|
+
if (index !== undefined)
|
|
144
|
+
afterCut.add(index);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
65
147
|
return messages.map(message => message.role === 'compactionSummary' && (!allowForeign || splitSummary(message.summary) !== undefined)
|
|
66
|
-
? { ...message, summary: filterCustomQuotes(filterOmittedSummary(message.summary, omitted), entries, branch, policy) } : message);
|
|
148
|
+
? { ...message, summary: filterCustomQuotes(filterOmittedSummary(message.summary, omitted, entries, cut?.type === 'compaction' && cut.summary === message.summary ? afterCut : new Set()), entries, branch, policy) } : message);
|
|
67
149
|
}
|
|
68
150
|
export function projectedMessages(messages, ctx, policy) {
|
|
69
151
|
return projectMessages(messages, ctx, policy, false);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SemanticState } from "./semantic-recall.js";
|
|
2
|
+
/** The version of the package this code was loaded from: the open session keeps it until Pi restarts, whatever is installed since. */
|
|
3
|
+
export declare const LOADED_VERSION: string;
|
|
4
|
+
/** `/tuck`: what an operator needs to know in words, from what is already recorded. It reads; it changes nothing the model sees. */
|
|
5
|
+
export declare function statusText(input: {
|
|
6
|
+
version: string;
|
|
7
|
+
active: boolean;
|
|
8
|
+
branch: readonly {
|
|
9
|
+
type: string;
|
|
10
|
+
tokensBefore?: unknown;
|
|
11
|
+
details?: unknown;
|
|
12
|
+
}[];
|
|
13
|
+
cutAt: number | undefined;
|
|
14
|
+
search: boolean;
|
|
15
|
+
semantic: {
|
|
16
|
+
state: SemanticState;
|
|
17
|
+
lastError: string | null;
|
|
18
|
+
};
|
|
19
|
+
}): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
/** The version of the package this code was loaded from: the open session keeps it until Pi restarts, whatever is installed since. */
|
|
4
|
+
export const LOADED_VERSION = (() => {
|
|
5
|
+
try {
|
|
6
|
+
const path = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "../package.json" : "../../package.json", import.meta.url));
|
|
7
|
+
const version = JSON.parse(readFileSync(path, "utf8")).version;
|
|
8
|
+
return typeof version === "string" ? version : "unknown";
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return "unknown";
|
|
12
|
+
}
|
|
13
|
+
})();
|
|
14
|
+
/** `/tuck`: what an operator needs to know in words, from what is already recorded. It reads; it changes nothing the model sees. */
|
|
15
|
+
export function statusText(input) {
|
|
16
|
+
if (!input.active)
|
|
17
|
+
return `pi-tuck ${input.version} is loaded but inactive in this host; Pi's own compaction runs.`;
|
|
18
|
+
const cuts = input.branch.filter(e => e.type === "compaction"), ours = cuts.filter(e => e.details?.compactor === "pi-tuck");
|
|
19
|
+
const last = ours.at(-1), before = last?.tokensBefore, after = last?.details?.tokensAfter;
|
|
20
|
+
const count = (n) => n.toLocaleString("en-US");
|
|
21
|
+
const cutLine = ours.length === 0 ? "Cuts: none yet in this session. The recall tools appear after the first cut."
|
|
22
|
+
: `Cuts: ${ours.length} by pi-tuck in this session` + (typeof before === "number" && typeof after === "number" ? `, the last from ${count(before)} to ${count(after)} tokens.` : ".");
|
|
23
|
+
const native = cuts.length - ours.length;
|
|
24
|
+
const searchLine = !input.search ? "Search: by keyword (search by meaning needs macOS on Apple silicon)."
|
|
25
|
+
: input.semantic.state === "ready" || input.semantic.state === "waiting" ? "Search: by meaning and keyword."
|
|
26
|
+
: input.semantic.state === "preparing" ? "Search: by meaning and keyword; indexing history in the background."
|
|
27
|
+
: `Search: by keyword; ${lower(input.semantic.lastError ?? "semantic recall is off in this session (/tuck-semantic on)")}.`.replace(/\.\.$/, ".");
|
|
28
|
+
return [
|
|
29
|
+
`pi-tuck ${input.version} (the version this session loaded; restart Pi after an update).`,
|
|
30
|
+
cutLine,
|
|
31
|
+
...(native > 0 ? [`Pi's own compaction cut ${native} time${native === 1 ? "" : "s"} in this session.`] : []),
|
|
32
|
+
input.cutAt === undefined ? "Cut point: adapts by itself." : `Cut point: fixed at cutAt ${input.cutAt} of the input room. Remove cutAt from pi-tuck.json to let it adapt.`,
|
|
33
|
+
searchLine,
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
const lower = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|