pi-tuck 1.0.27 → 1.0.28
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 +27 -0
- package/README.md +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +37 -24
- package/dist/src/build-id.d.ts +1 -1
- package/dist/src/build-id.js +1 -1
- package/dist/src/compaction.d.ts +4 -0
- package/dist/src/compaction.js +156 -176
- package/dist/src/diagnostics.d.ts +1 -1
- package/dist/src/request-capacity.d.ts +6 -1
- package/dist/src/request-capacity.js +15 -10
- package/dist/src/run.d.ts +52 -0
- package/dist/src/run.js +89 -0
- package/dist/src/semantic-recall.d.ts +72 -2
- package/dist/src/semantic-recall.js +58 -12
- package/dist/src/status.d.ts +1 -0
- package/dist/src/status.js +2 -1
- package/dist/src/tuck.js +20 -20
- package/index.ts +31 -22
- package/package.json +1 -1
- package/src/build-id.ts +1 -1
- package/src/compaction.ts +137 -159
- package/src/request-capacity.ts +14 -8
- package/src/run.ts +91 -0
- package/src/semantic-recall.ts +46 -14
- package/src/status.ts +3 -1
- package/src/tuck.ts +18 -18
package/dist/src/compaction.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { textOf } from "./tuck/core/content.js";
|
|
2
|
-
import {
|
|
2
|
+
import { PendingCut, StartingPrompt, createRun } from "./run.js";
|
|
3
|
+
import { requestReplyFloor, replyFit, carriesPrompt, thinkingOnlyLength, piContextSafetyMargin, loadPiRequestMath } from "./request-capacity.js";
|
|
3
4
|
import { BUILD_ID } from "./build-id.js";
|
|
4
5
|
import { createSemanticRecall, budgetSemantic } from "./semantic-recall.js";
|
|
5
6
|
import { DIAGNOSTIC_BUILD, IncidentRecorder, attachReceipt, permittedEntries, receiptId, findReceiptEntry } from "./diagnostics.js";
|
|
@@ -11,7 +12,7 @@ import { projectedMessages } from "./omissions.js";
|
|
|
11
12
|
import { applyClips, clipBudgets, clipFor } from "./clip.js";
|
|
12
13
|
import { DEFAULT_CUT_AT, bandOf, chooseCut, conversationTokens, gateCut, incomingEventTokens, incomingTokens, measurableConversationTokens, planningReserve, projectedConversationTokens, systemPart, validCutAt } from "./budget.js";
|
|
13
14
|
import { getCurrentSystemMessage, isContextOverflow, normalizeContext } from "@earendil-works/pi-ai";
|
|
14
|
-
import { calculateContextTokens, convertToLlm, estimateTokens, shouldCompact,
|
|
15
|
+
import { calculateContextTokens, convertToLlm, estimateTokens, shouldCompact, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
15
16
|
import { Type } from "typebox";
|
|
16
17
|
import { Check } from "typebox/value";
|
|
17
18
|
import { MAX_RECALL_CHARACTERS, MAX_RECALL_ENTRY_ID_CHARACTERS, MAX_RECALL_QUERY_CHARACTERS } from "./recall.js";
|
|
@@ -19,7 +20,7 @@ import { tuck, tuckDraft, tuckObservedDraft, tuckRecall } from "./tuck.js";
|
|
|
19
20
|
import { LOADED_VERSION, statusText } from "./status.js";
|
|
20
21
|
import { ImageRecallInput, historyImage, historyAround } from "./history-image.js";
|
|
21
22
|
import { eligibilityKey, eligibilityKeyMoved, memoizedEligibleSources } from "./eligibility-cache.js";
|
|
22
|
-
import { applyHistoryTools,
|
|
23
|
+
import { applyHistoryTools, historyToolDeclarations, historyToolNames, stageHistoryTools } from "./history-tools.js";
|
|
23
24
|
const query = Type.String({ minLength: 1, maxLength: MAX_RECALL_QUERY_CHARACTERS });
|
|
24
25
|
const entryId = Type.String({ minLength: 1, maxLength: MAX_RECALL_ENTRY_ID_CHARACTERS });
|
|
25
26
|
const paging = {
|
|
@@ -43,7 +44,7 @@ const RecallInput = Type.Union([
|
|
|
43
44
|
const SearchInput = Type.Object({ query, priorResults: Type.Optional(Type.Array(Type.Object({ sourceSha: Type.String({ minLength: 64, maxLength: 64 }), read: Type.Object({ entryId, offset: Type.Integer({ minimum: 0 }), limit: Type.Integer({ minimum: 0 }) }, { additionalProperties: false }) }, { additionalProperties: false }), { maxItems: 20 })) }, { additionalProperties: false });
|
|
44
45
|
const reply = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }], details: {} });
|
|
45
46
|
/** The compactor's verdict that this attempt cannot be made: the cut it needs cannot be made, or the request cannot fit. Like every failure but
|
|
46
|
-
* a `CommitFault`, it ends with its attempt (`
|
|
47
|
+
* a `CommitFault`, it ends with its attempt (`resetRun`), and the next attempt plans afresh from the session as it is. */
|
|
47
48
|
class Refusal extends Error {
|
|
48
49
|
}
|
|
49
50
|
/** A cut this compactor supplied that Pi then failed to commit. Pi appends a cut to the session in memory before it writes the file, so the
|
|
@@ -122,11 +123,16 @@ export async function installCompaction(pi, options = {}) {
|
|
|
122
123
|
// Semantic indexing outlives any one attempt, so it checks only that the host still grants access: a compactor failure, which ends with its
|
|
123
124
|
// attempt, must not read as a revocation (that disables semantic recall and deletes its index for the rest of the session).
|
|
124
125
|
// A prompt's cut reaches the requests at once but the session only at the end of the first turn: until then search sees the history as the model does.
|
|
125
|
-
const
|
|
126
|
+
const view = (ctx, threshold = false) => (threshold && run?.expired ? liveCut : run?.cut ?? liveCut).view(ctx);
|
|
127
|
+
const liveCut = new PendingCut();
|
|
128
|
+
const semanticProvider = (ctx) => () => {
|
|
129
|
+
const current = view(ctx);
|
|
130
|
+
return memoizedEligibleSources(ctx, options.recallPolicy, hostAccess, current.pending ? current.keptFromId : undefined);
|
|
131
|
+
};
|
|
126
132
|
const recorder = new IncidentRecorder(options.diagnostics);
|
|
127
133
|
let proposalId;
|
|
128
134
|
const observe = (ctx) => recorder.measureInline(() => {
|
|
129
|
-
if (!active() || failure)
|
|
135
|
+
if (!active() || failure())
|
|
130
136
|
return;
|
|
131
137
|
if (!proposalId)
|
|
132
138
|
return;
|
|
@@ -218,16 +224,18 @@ export async function installCompaction(pi, options = {}) {
|
|
|
218
224
|
ctx.ui.notify(JSON.stringify({ ...semanticStatus(), ...(errors.length ? { errors } : {}) }), errors.length ? "warning" : "info");
|
|
219
225
|
} });
|
|
220
226
|
pi.registerCommand("tuck", { description: "pi-tuck's state in this session: version, cuts, cut point, search", handler: async (_args, ctx) => {
|
|
221
|
-
const
|
|
222
|
-
ctx.ui.notify(statusText({ version: LOADED_VERSION, active: active(), branch: ctx.sessionManager.getBranch(), cutAt: options.cutAt, search, semantic:
|
|
227
|
+
const state = semantic.status();
|
|
228
|
+
ctx.ui.notify(statusText({ version: LOADED_VERSION, active: active(), branch: ctx.sessionManager.getBranch(), cutAt: options.cutAt, search, semantic: state, pending: view(ctx).pending }), "info");
|
|
223
229
|
} });
|
|
224
230
|
// Pi's native start/settled events bracket the run and its post-turn cuts.
|
|
225
|
-
let
|
|
226
|
-
|
|
227
|
-
let
|
|
228
|
-
|
|
229
|
-
const protectedPrompt = (ctx) =>
|
|
230
|
-
|
|
231
|
+
let run;
|
|
232
|
+
// A native append may have mutated memory before its disk write failed. This session fault outlives the run.
|
|
233
|
+
let commitFault;
|
|
234
|
+
const failure = () => commitFault ?? run?.failure;
|
|
235
|
+
const protectedPrompt = (ctx) => run?.prompt?.id(ctx.sessionManager.getBranch());
|
|
236
|
+
const clips = () => run?.clips ?? emptyClips;
|
|
237
|
+
const emptyClips = new Map();
|
|
238
|
+
const resetRun = () => { run = undefined; pending = undefined; };
|
|
231
239
|
const promptRefusal = () => new Refusal("Prompt check failed: the whole starting prompt cannot fit after cutting to the band and room, shrinking the summary and clipping fresh results. Shorten the prompt or choose a model with a larger context window, then retry");
|
|
232
240
|
let estimatedTokens = 0;
|
|
233
241
|
// One serial request, one accounting identity; observations never certify new content. A new identity (the first request after a session
|
|
@@ -274,7 +282,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
274
282
|
pending = undefined;
|
|
275
283
|
const key = identityFor(ctx), message = event.message;
|
|
276
284
|
const overflow = message.stopReason === "error" && isContextOverflow(message);
|
|
277
|
-
if (!paired || !key || paired.identity !== key || !active() || failure || ctx.signal?.aborted || (message.errorMessage && !overflow) ||
|
|
285
|
+
if (!paired || !key || paired.identity !== key || !active() || failure() || ctx.signal?.aborted || (message.errorMessage && !overflow) ||
|
|
278
286
|
message.provider !== ctx.model?.provider || message.model !== ctx.model.id || message.api !== ctx.model.api ||
|
|
279
287
|
(!overflow && !["stop", "toolUse", "length"].includes(message.stopReason)) ||
|
|
280
288
|
!isDeepStrictEqual(convertToLlm(paired.messages), paired.snapshot))
|
|
@@ -304,8 +312,30 @@ export async function installCompaction(pi, options = {}) {
|
|
|
304
312
|
if (input > 0 && input <= ctx.model.contextWindow && Number.isSafeInteger(input) && Number.isFinite(ratio))
|
|
305
313
|
learn(ratio);
|
|
306
314
|
});
|
|
307
|
-
|
|
308
|
-
|
|
315
|
+
// The operator's length-stop notice is decided at settle and is not attempt state: a run reset before then (a tree move inside another
|
|
316
|
+
// extension's agent_settled, a failure) must not erase it. Only the final assistant reply decides it; a successful retry clears it.
|
|
317
|
+
let lengthStop = false;
|
|
318
|
+
pi.on("agent_start", () => { run ??= createRun("admitted"); run.phase = "admitted"; run.thinkingRetryPending = false; lengthStop = false; pending = undefined; });
|
|
319
|
+
pi.on("message_end", event => { if (event.message.role === "assistant")
|
|
320
|
+
lengthStop = event.message.stopReason === "length"; });
|
|
321
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
322
|
+
try {
|
|
323
|
+
if (!lengthStop || !active())
|
|
324
|
+
return;
|
|
325
|
+
lengthStop = false;
|
|
326
|
+
const message = "pi-tuck: the last reply stopped at the model's output limit and may be cut off. Ask the agent to continue.";
|
|
327
|
+
try {
|
|
328
|
+
if (ctx.hasUI === false)
|
|
329
|
+
process.stderr.write(message + "\n");
|
|
330
|
+
else
|
|
331
|
+
ctx.ui.notify(message, "warning");
|
|
332
|
+
}
|
|
333
|
+
catch { /* ended */ }
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
resetRun();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
309
339
|
const active = () => options.active?.() ?? true;
|
|
310
340
|
const settingsFor = () => options.settings ?? SettingsManager.inMemory(pi.getSettings());
|
|
311
341
|
// The one reserve decision; every consumer takes its room from limits. Standalone starts at Pi's native cut point. Once a reply did
|
|
@@ -318,7 +348,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
318
348
|
const budget = host ?? { response: settingsFor().getCompactionSettings(ctx.model).reserveTokens };
|
|
319
349
|
if (!Number.isSafeInteger(budget.response) || budget.response < 0)
|
|
320
350
|
throw new Error("Invalid host request reserve");
|
|
321
|
-
const floor = host ? host.responseExact !== true : ctx.sessionManager.getBranch().
|
|
351
|
+
const floor = host ? host.responseExact !== true : replyFit(ctx.sessionManager.getBranch()).failed;
|
|
322
352
|
return { response: floor ? Math.max(budget.response, ctx.model?.maxTokens ?? 0) : budget.response };
|
|
323
353
|
};
|
|
324
354
|
const inputRoom = (response, ctx) => {
|
|
@@ -336,18 +366,18 @@ export async function installCompaction(pi, options = {}) {
|
|
|
336
366
|
const hardLine = Math.min(Math.max(room, (ctx.model?.contextWindow ?? 0) - piMargin - floor), lastCeiling ?? Infinity);
|
|
337
367
|
return { response, room, hardLine, replyFloor: floor };
|
|
338
368
|
};
|
|
339
|
-
const decisionBand = (ctx, room, system) => {
|
|
340
|
-
const last =
|
|
369
|
+
const decisionBand = (ctx, room, system, current = view(ctx)) => {
|
|
370
|
+
const last = current.session.getBranch().findLast(entry => entry.type === "compaction");
|
|
341
371
|
const after = last?.type === "compaction" ? last.details?.tokensAfter : undefined;
|
|
342
372
|
return bandOf(room, cutAt, options.cutAt === undefined && typeof after === "number" && Number.isFinite(after) && after >= 0
|
|
343
373
|
? { system, tokensAfter: after, factor } : undefined);
|
|
344
374
|
};
|
|
345
375
|
const fits = (tokens, limit, ctx) => corrected(tokens, ctx) < limit;
|
|
346
376
|
// The saved conversation and a cut's quantities are counted by the one function the guard counts with (strict, as the context hook is).
|
|
347
|
-
const measureFor = (ctx) => (messages) => projectedConversationTokens(applyClips(messages, clips), ctx, options.recallPolicy);
|
|
377
|
+
const measureFor = (ctx) => (messages) => projectedConversationTokens(applyClips(messages, clips()), ctx, options.recallPolicy);
|
|
348
378
|
// The planner counts with the same function, except that it does not fail on a summary in another format: a cut can replace one
|
|
349
379
|
// (an operator's /compact repairs a session whose saved summary is foreign). Identical to the strict one for this compactor's summaries.
|
|
350
|
-
const plannerMeasureFor = (ctx) => Object.assign((messages) => measurableConversationTokens(applyClips(messages, clips), ctx, options.recallPolicy), { messageTokens: (message) => conversationTokens(applyClips([message], clips)) });
|
|
380
|
+
const plannerMeasureFor = (ctx) => Object.assign((messages) => measurableConversationTokens(applyClips(messages, clips()), ctx, options.recallPolicy), { messageTokens: (message) => conversationTokens(applyClips([message], clips())) });
|
|
351
381
|
const hostMessages = (ctx) => options.hostMessages?.(ctx) ?? [];
|
|
352
382
|
const toolShapes = (names) => {
|
|
353
383
|
const selected = new Set(names);
|
|
@@ -375,19 +405,19 @@ export async function installCompaction(pi, options = {}) {
|
|
|
375
405
|
const saved = messages.some(message => message.role === "system") ? messages :
|
|
376
406
|
[{ role: "system", content: prompt, toolsAdded: shapes, timestamp: 0 }, ...messages];
|
|
377
407
|
const declared = enabled ? historyToolDeclarations(saved, shapes, true) : saved;
|
|
378
|
-
return applyClips([...host, ...(enabled ? projectedMessages(declared, ctx, options.recallPolicy) : declared), ...incoming], clips);
|
|
408
|
+
return applyClips([...host, ...(enabled ? projectedMessages(declared, ctx, options.recallPolicy) : declared), ...incoming], clips());
|
|
379
409
|
};
|
|
380
410
|
const clampAboveRoom = (decision, ctx, room, target, total, project, replan, deepest) => {
|
|
381
411
|
if (options.requestReserve || !piClamp)
|
|
382
412
|
return decision;
|
|
383
413
|
const lastAttempt = () => {
|
|
384
|
-
const previousClips = new Map(clips);
|
|
414
|
+
const previousClips = new Map(clips());
|
|
385
415
|
const candidate = deepest();
|
|
386
416
|
if (candidate.kind === "cut" && fits(total(candidate), limits(ctx).hardLine, ctx) && requestClamp(project(candidate), ctx).leavesFloor)
|
|
387
417
|
return candidate;
|
|
388
|
-
clips.clear();
|
|
418
|
+
clips().clear();
|
|
389
419
|
for (const [id, clip] of previousClips)
|
|
390
|
-
clips.set(id, clip);
|
|
420
|
+
clips().set(id, clip);
|
|
391
421
|
return { kind: "fault", capacity: true, message: clampMessage };
|
|
392
422
|
};
|
|
393
423
|
for (let retry = 0;; retry++) {
|
|
@@ -412,18 +442,6 @@ export async function installCompaction(pi, options = {}) {
|
|
|
412
442
|
const isNoWork = (message) => ["Already compacted", "Nothing to compact (session too small)"].includes(message?.replace(/^Compaction failed: /, "") ?? "");
|
|
413
443
|
// A missing model is configuration, not capacity.
|
|
414
444
|
const missingModel = () => new Error("No context model for the request budget");
|
|
415
|
-
let promptCut;
|
|
416
|
-
// Where the branch ended when the prompt's cut was planned: the prompt is the entry Pi appends there. A cut that keeps nothing of the conversation
|
|
417
|
-
// before it names no entry to keep from, and read as it stands that drops the prompt too, so the model never sees it.
|
|
418
|
-
let promptCutAt = 0;
|
|
419
|
-
const keptFrom = (cut, ctx) => cut.firstKeptEntryId ||
|
|
420
|
-
ctx.sessionManager.getBranch().slice(promptCutAt).find(entry => entry.type !== "custom" || entry.customType !== "pi-tuck-capacity")?.id || "";
|
|
421
|
-
let failure;
|
|
422
|
-
let refusalReceipted = false;
|
|
423
|
-
let pendingBoundaryCommit = false;
|
|
424
|
-
// Fresh tool results the room cannot hold, by call id: each request of the run carries the clip in their place; the session keeps them
|
|
425
|
-
// whole. A clip ends with its run like any failure would: the next run plans from the session as it is, where the result is no longer fresh.
|
|
426
|
-
const clips = new Map();
|
|
427
445
|
const receipt = (kind, reason, ctx) => {
|
|
428
446
|
let room = null, hardLine = null;
|
|
429
447
|
try {
|
|
@@ -436,51 +454,43 @@ export async function installCompaction(pi, options = {}) {
|
|
|
436
454
|
recorder.stop("native-attempt-failed");
|
|
437
455
|
// Every refusal says why, once per attempt: a capacity error with what the user can change, a cut Pi failed to commit with what that
|
|
438
456
|
// means for the session, anything else as it is (an attempt that ended with no word looked like a hang).
|
|
439
|
-
const notify = !
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
457
|
+
const previous = failure(), notify = !previous;
|
|
458
|
+
const fault = previous instanceof CommitFault ? previous : error instanceof Error ? error : new Error(String(error));
|
|
459
|
+
const receipted = run?.refusalReceipted;
|
|
460
|
+
resetRun();
|
|
461
|
+
// Keep only a terminal attempt record until Pi settles: its fresh abort controller must still see the refusal.
|
|
462
|
+
run = createRun("failed");
|
|
463
|
+
run.failure = fault;
|
|
464
|
+
run.refusalReceipted = true;
|
|
465
|
+
if (fault instanceof CommitFault)
|
|
466
|
+
commitFault = fault;
|
|
446
467
|
try {
|
|
447
|
-
if (!
|
|
448
|
-
|
|
449
|
-
receipt("refusal", failure.message, ctx);
|
|
450
|
-
}
|
|
468
|
+
if (!receipted)
|
|
469
|
+
receipt("refusal", fault.message, ctx);
|
|
451
470
|
options.fail?.(error);
|
|
452
471
|
}
|
|
453
472
|
finally {
|
|
454
473
|
try {
|
|
455
474
|
if (notify)
|
|
456
|
-
ctx.ui.notify(
|
|
457
|
-
? `pi-tuck: ${
|
|
458
|
-
: `pi-tuck could not plan this request (${
|
|
475
|
+
ctx.ui.notify(fault instanceof CapacityError ? refusalNotice(fault.message, factor) : fault instanceof CommitFault
|
|
476
|
+
? `pi-tuck: ${fault.message}; the session holds a cut its file may lack, so start a new session.`
|
|
477
|
+
: `pi-tuck could not plan this request (${fault.message}); the next prompt is planned afresh.`, "error");
|
|
459
478
|
}
|
|
460
479
|
finally {
|
|
461
480
|
ctx.abort();
|
|
462
481
|
}
|
|
463
482
|
}
|
|
464
483
|
};
|
|
465
|
-
// The one place a failure is dropped: when its attempt has settled, when a prompt arrives, and when the operator asks for a cut. A run that no
|
|
466
|
-
// input started (a custom message) has only the first of these. No failure but a CommitFault outlives its attempt.
|
|
467
|
-
const forgetFailure = () => { refusalReceipted = false; if (!(failure instanceof CommitFault))
|
|
468
|
-
failure = undefined; };
|
|
469
484
|
const access = () => {
|
|
470
|
-
if (failure)
|
|
471
|
-
throw failure;
|
|
485
|
+
if (failure())
|
|
486
|
+
throw failure();
|
|
472
487
|
hostAccess();
|
|
473
488
|
};
|
|
474
|
-
// Abort alone is insufficient: Pi starts a fresh abort controller after
|
|
475
|
-
// before_agent_start. Keep this attempt blocked, then recheck on new idle input.
|
|
476
|
-
// "decided": `input` has decided for the run it starts, so that run's before_agent_start does not decide again. "starting": a run is
|
|
477
|
-
// between before_agent_start and agent_start, where Pi 1.1 still reports idle but a message joins that run.
|
|
478
|
-
let phase = "idle";
|
|
479
489
|
pi.on("input", async (event, ctx) => {
|
|
480
|
-
if (!ctx.isIdle() || phase === "starting")
|
|
481
|
-
return; //
|
|
482
|
-
|
|
483
|
-
|
|
490
|
+
if (!ctx.isIdle() || run?.phase === "starting")
|
|
491
|
+
return; // steering joins the existing run
|
|
492
|
+
run = createRun("decided");
|
|
493
|
+
const decision = run;
|
|
484
494
|
try {
|
|
485
495
|
syncHistoryTools(ctx);
|
|
486
496
|
await preflight(ctx, incomingTokens(event.text, event.images), [{ role: "user", content: [{ type: "text", text: event.text }, ...(event.images ?? [])], timestamp: 0 }], ctx.getSystemPrompt(), pi.getActiveTools());
|
|
@@ -489,6 +499,10 @@ export async function installCompaction(pi, options = {}) {
|
|
|
489
499
|
catch (error) {
|
|
490
500
|
fail(error, ctx);
|
|
491
501
|
}
|
|
502
|
+
// Pi normally reaches its pre-run threshold check within microtasks. Auth I/O or a later extension's input I/O can make a
|
|
503
|
+
// real prompt expire too; safely fall back to carrying its cut in the request and saving it at the first boundary.
|
|
504
|
+
setImmediate(() => { if (run === decision && !decision.claimed)
|
|
505
|
+
decision.expired = true; });
|
|
492
506
|
});
|
|
493
507
|
pi.registerTool({
|
|
494
508
|
name: "tuck_recall", label: "pi-tuck history", parameters: RecallInput,
|
|
@@ -566,28 +580,30 @@ export async function installCompaction(pi, options = {}) {
|
|
|
566
580
|
}
|
|
567
581
|
// Pi checks its own threshold after a prompt's input and before the run starts: the prompt's cut, planned with the prompt counted,
|
|
568
582
|
// is the answer, and it is saved before the first request.
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
583
|
+
if (run?.phase === "decided" && !run.expired && event.reason === "threshold") {
|
|
584
|
+
run.claimed = true;
|
|
585
|
+
if (view(ctx).pending) {
|
|
586
|
+
access();
|
|
587
|
+
const current = view(ctx), { type: _, ...compaction } = run.cut.take();
|
|
588
|
+
return { compaction: { ...compaction, firstKeptEntryId: current.keptFromId } };
|
|
589
|
+
}
|
|
574
590
|
}
|
|
575
591
|
const operatorManual = event.reason === "manual";
|
|
576
|
-
if (operatorManual &&
|
|
577
|
-
|
|
592
|
+
if (operatorManual && run?.phase !== "admitted")
|
|
593
|
+
resetRun();
|
|
578
594
|
access();
|
|
579
595
|
// 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
|
|
580
596
|
// plain /compact makes and say the text was not used, rather than cancel (which Pi shows as an error, with no cut).
|
|
581
597
|
if (event.customInstructions?.trim())
|
|
582
598
|
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");
|
|
583
599
|
const { response, room } = limits(ctx);
|
|
584
|
-
const retain = operatorManual ||
|
|
600
|
+
const retain = operatorManual || run?.phase === "admitted";
|
|
585
601
|
// A cut Pi asks for (an operator's /compact, Pi's threshold or overflow recovery) charges what the request must carry whatever is
|
|
586
602
|
// cut: the system part and the host input. A prompt's cut is pi-tuck's own and never comes here.
|
|
587
603
|
const saved = ctx.sessionManager.buildSessionProjection().messages;
|
|
588
604
|
const hostInput = hostMessages(ctx), host = conversationTokens(hostInput);
|
|
589
605
|
const system = requestSystemPart(ctx.getSystemPrompt(), pi.getActiveTools(), saved, ctx.getSystemPrompt());
|
|
590
|
-
const mandatory = system + host, band = decisionBand(ctx, room, system);
|
|
606
|
+
const mandatory = system + host, band = decisionBand(ctx, room, system, view(ctx, event.reason === "threshold"));
|
|
591
607
|
const before = plannerMeasureFor(ctx)(saved);
|
|
592
608
|
observeBudget(before + mandatory, ctx);
|
|
593
609
|
// Pi's own usage threshold (reason "threshold") counts the last reply's output tokens, reasoning included, plus the messages after
|
|
@@ -610,9 +626,9 @@ export async function installCompaction(pi, options = {}) {
|
|
|
610
626
|
const reserve = planReserve(target, targetRoom, targetBand, mandatory, ctx);
|
|
611
627
|
// Every cut asked while no run is admitted (an operator's cut) is planned for a run that has not begun: the session has moved on
|
|
612
628
|
// from any run that ended. A retry is inside its run.
|
|
613
|
-
const draft = tuckDraft(tuck, event, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, retain, operatorManual, operatorManual ||
|
|
629
|
+
const draft = tuckDraft(tuck, event, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, retain, operatorManual, operatorManual || run?.phase !== "admitted", run?.phase === "admitted" ? protectedPrompt(ctx) : undefined);
|
|
614
630
|
return draft.compaction ? { draft: true, after: draft.after, mode: draft.mode, compaction: draft.compaction, reserve,
|
|
615
|
-
messages:
|
|
631
|
+
messages: plannedView(ctx, { type: "compaction", ...draft.compaction, tokensBefore: before }).session.buildSessionProjection().messages }
|
|
616
632
|
: { draft: false, capacityRejected: Boolean(draft.capacityRejected), reason: draft.unavailable };
|
|
617
633
|
},
|
|
618
634
|
});
|
|
@@ -621,7 +637,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
621
637
|
const { hardLine } = limits(ctx);
|
|
622
638
|
if (!operatorManual && hardLine > room && decision.kind === "fault") {
|
|
623
639
|
decision = fits(before + mandatory, hardLine, ctx) ? { kind: "none" } : decide(hardLine, hardLine);
|
|
624
|
-
decision = clampAboveRoom(decision, ctx, room, hardLine, candidate => (candidate.kind === "cut" ? candidate.plan.after : before) + mandatory, candidate => projectedRequest(candidate.kind === "cut" ? candidate.plan.messages : saved, ctx, hostInput, [], ctx.getSystemPrompt(), pi.getActiveTools(), candidate.kind === "cut"), target => target <= room ? decide() : decide(target, target), () => deepestDecision(ctx, operatorManual ||
|
|
640
|
+
decision = clampAboveRoom(decision, ctx, room, hardLine, candidate => (candidate.kind === "cut" ? candidate.plan.after : before) + mandatory, candidate => projectedRequest(candidate.kind === "cut" ? candidate.plan.messages : saved, ctx, hostInput, [], ctx.getSystemPrompt(), pi.getActiveTools(), candidate.kind === "cut"), target => target <= room ? decide() : decide(target, target), () => deepestDecision(ctx, operatorManual || run?.phase !== "admitted", retain));
|
|
625
641
|
}
|
|
626
642
|
if (decision.kind === "cut") {
|
|
627
643
|
return { compaction: captureDraft(decision.plan.compaction, ctx, decision.plan.reserve, event.reason, event.preparation.tokensBefore, retain, operatorManual, event.preparation.previousSummary, { response, host, mandatory, target: decision.target, margin: room - band }) };
|
|
@@ -647,49 +663,23 @@ export async function installCompaction(pi, options = {}) {
|
|
|
647
663
|
}
|
|
648
664
|
});
|
|
649
665
|
pi.on("session_before_tree", (event) => event.preparation.userWantsSummary ? { cancel: true } : undefined);
|
|
650
|
-
pi.on("session_start", () => {
|
|
651
|
-
const historyToolsEnabled = (ctx) =>
|
|
666
|
+
pi.on("session_start", () => { resetRun(); recorder.stop("session-transition"); proposalId = undefined; commitFault = undefined; estimatedTokens = 0; identity = ""; factor = 1; });
|
|
667
|
+
const historyToolsEnabled = (ctx) => view(ctx).hasTuckCut;
|
|
652
668
|
const syncHistoryTools = (ctx) => { if (active())
|
|
653
669
|
applyHistoryTools(pi, historyToolsEnabled(ctx)); };
|
|
654
670
|
pi.on("session_start", (_event, ctx) => syncHistoryTools(ctx));
|
|
655
671
|
// A plan decided on `input` is for the branch it was decided on: moving in the tree leaves it planned for another branch, whose kept entry
|
|
656
672
|
// may not be on this one.
|
|
657
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
658
|
-
pi.on("session_compact", (_event, ctx) => { if (
|
|
673
|
+
pi.on("session_tree", (_event, ctx) => { resetRun(); syncHistoryTools(ctx); });
|
|
674
|
+
pi.on("session_compact", (_event, ctx) => { if (run?.phase !== "admitted")
|
|
659
675
|
syncHistoryTools(ctx); });
|
|
660
676
|
pi.on("turn_start", (_event, ctx) => syncHistoryTools(ctx));
|
|
661
677
|
pi.on("agent_settled", (_event, ctx) => syncHistoryTools(ctx));
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
pi.on("agent_start", () => { lengthStop = false; });
|
|
667
|
-
pi.on("message_end", event => { if (event.message.role === "assistant")
|
|
668
|
-
lengthStop = event.message.stopReason === "length"; });
|
|
669
|
-
pi.on("agent_settled", (_event, ctx) => {
|
|
670
|
-
if (!lengthStop || !active())
|
|
671
|
-
return;
|
|
672
|
-
lengthStop = false;
|
|
673
|
-
const message = "pi-tuck: the last reply stopped at the model's output limit and may be cut off. Ask the agent to continue.";
|
|
674
|
-
try {
|
|
675
|
-
if (ctx.hasUI === false)
|
|
676
|
-
process.stderr.write(message + "\n");
|
|
677
|
-
else
|
|
678
|
-
ctx.ui.notify(message, "warning");
|
|
679
|
-
}
|
|
680
|
-
catch { /* ended */ } // with no UI (pi -p), stderr is the operator's
|
|
681
|
-
});
|
|
682
|
-
// 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.
|
|
683
|
-
const previewCut = (ctx, cut) => {
|
|
684
|
-
const header = ctx.sessionManager.getHeader();
|
|
685
|
-
if (!header)
|
|
686
|
-
throw new Error("Missing native session header for the prompt's cut");
|
|
687
|
-
const preview = SessionManager.inMemory(ctx.cwd, undefined, [header, ...ctx.sessionManager.getBranch()]);
|
|
688
|
-
preview.appendCompaction(cut.summary, cut.firstKeptEntryId, 0, cut.details, true);
|
|
689
|
-
return preview;
|
|
678
|
+
const plannedView = (ctx, cut) => {
|
|
679
|
+
const pending = new PendingCut();
|
|
680
|
+
pending.plan(cut, ctx.sessionManager.getBranch().length);
|
|
681
|
+
return pending.view(ctx);
|
|
690
682
|
};
|
|
691
|
-
const cutSession = (ctx, cut) => previewCut(ctx, { ...cut, firstKeptEntryId: keptFrom(cut, ctx) });
|
|
692
|
-
const withCut = (ctx, cut) => cutSession(ctx, cut).buildSessionProjection().messages;
|
|
693
683
|
// The shared final planner: preserve the compiler's prompt/protocol floor and spend nothing on optional history. Fresh text results
|
|
694
684
|
// get the smallest existing head/tail + recall-locator representation; their originals remain in the session, as for ordinary clips.
|
|
695
685
|
const deepestDecision = (ctx, newPrompt, retain) => {
|
|
@@ -712,7 +702,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
712
702
|
low = middle + 1;
|
|
713
703
|
}
|
|
714
704
|
for (const clip of clipFor([result], entryIdOf, low))
|
|
715
|
-
clips.set(clip.toolCallId, clip);
|
|
705
|
+
clips().set(clip.toolCallId, clip);
|
|
716
706
|
}
|
|
717
707
|
}
|
|
718
708
|
const before = measureFor(ctx)(saved);
|
|
@@ -721,7 +711,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
721
711
|
protectedPromptEntryId: newPrompt ? undefined : protectedPrompt(ctx), deepest: true }, ctx, 0, plannerMeasureFor(ctx), options.recallPolicy, retain);
|
|
722
712
|
if (!draft.compaction)
|
|
723
713
|
return { kind: "fault", capacity: Boolean(draft.capacityRejected), message: draft.unavailable };
|
|
724
|
-
const messages =
|
|
714
|
+
const messages = plannedView(ctx, { type: "compaction", ...draft.compaction }).session.buildSessionProjection().messages;
|
|
725
715
|
assertNativeToolRounds(messages);
|
|
726
716
|
return { kind: "cut", target: "room", plan: { draft: true, after: measureFor(ctx)(messages), mode: draft.mode,
|
|
727
717
|
compaction: draft.compaction, reserve: 0, messages } };
|
|
@@ -731,8 +721,8 @@ export async function installCompaction(pi, options = {}) {
|
|
|
731
721
|
// keepRecentTokens, and where those are the whole conversation it has nothing to cut, so every later prompt was refused for good (t06 at
|
|
732
722
|
// Pi's defaults on a 32k window). Each request of the run carries the cut, and the run's first boundary saves it.
|
|
733
723
|
const preflight = (ctx, incomingPart, incomingMessages, observedPrompt, tools, hostSnapshot) => {
|
|
734
|
-
|
|
735
|
-
|
|
724
|
+
run.cut.clear();
|
|
725
|
+
const branchLength = ctx.sessionManager.getBranch().length;
|
|
736
726
|
if (!active())
|
|
737
727
|
return;
|
|
738
728
|
access();
|
|
@@ -767,7 +757,9 @@ export async function installCompaction(pi, options = {}) {
|
|
|
767
757
|
const draft = tuckObservedDraft(tuck, { branchEntries: ctx.sessionManager.getBranch(), tokensBefore: before, previousSummary, newPrompt: true, reRenderSummary: !opportunistic }, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, false);
|
|
768
758
|
if (!draft.compaction)
|
|
769
759
|
return { draft: false, capacityRejected: Boolean(draft.capacityRejected), reason: draft.unavailable };
|
|
770
|
-
const
|
|
760
|
+
const candidate = new PendingCut();
|
|
761
|
+
candidate.plan({ type: "compaction", ...draft.compaction, tokensBefore: before }, branchLength);
|
|
762
|
+
const messages = candidate.view(ctx).session.buildSessionProjection().messages;
|
|
771
763
|
assertNativeToolRounds(messages);
|
|
772
764
|
return { draft: true, after: measure(messages), mode: draft.mode, compaction: draft.compaction, reserve, messages };
|
|
773
765
|
},
|
|
@@ -787,16 +779,19 @@ export async function installCompaction(pi, options = {}) {
|
|
|
787
779
|
if (decision.kind === "fault")
|
|
788
780
|
throw new (decision.capacity ? CapacityError : Refusal)(decision.message);
|
|
789
781
|
const compaction = captureDraft(decision.plan.compaction, ctx, decision.plan.reserve, "prompt", before, false, false, previousSummary, { response, host, mandatory, target: decision.target, margin: room - band });
|
|
790
|
-
|
|
782
|
+
run.cut.plan({ type: "compaction", summary: compaction.summary, firstKeptEntryId: compaction.firstKeptEntryId, tokensBefore: before, details: compaction.details }, branchLength);
|
|
791
783
|
};
|
|
792
784
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
793
785
|
// A decided plan is for the prompt `input` saw. A run that a custom message starts is not that run (another extension handled the
|
|
794
786
|
// input, so no run started for it): plan afresh.
|
|
795
|
-
const prompted = phase === "decided" && event.messageType !== "custom";
|
|
796
|
-
phase
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
787
|
+
const prompted = run?.phase === "decided" && event.messageType !== "custom";
|
|
788
|
+
if (!prompted && run?.phase !== "failed")
|
|
789
|
+
run = createRun("starting");
|
|
790
|
+
run ??= createRun("starting");
|
|
791
|
+
run.claimed = true;
|
|
792
|
+
run.phase = "starting";
|
|
793
|
+
const prompt = event.messageType === "custom" ? textOf(event.message.content) : event.prompt;
|
|
794
|
+
run.prompt = prompt ? new StartingPrompt(prompt, ctx.sessionManager.getBranch().length, event.messageType) : undefined;
|
|
800
795
|
try {
|
|
801
796
|
syncHistoryTools(ctx);
|
|
802
797
|
if (prompted)
|
|
@@ -821,7 +816,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
821
816
|
// handlers have run. Pi's prepareRequest has not yet snapshotted executable tools. A first cut discovered in context_with_system
|
|
822
817
|
// would be too late for that snapshot, even if its declarations were repaired there, so make the late prompt decision here.
|
|
823
818
|
pi.on("message_end", (event, ctx) => {
|
|
824
|
-
if (event.message.role !== "user" || !active() ||
|
|
819
|
+
if (event.message.role !== "user" || !active() || run?.phase !== "admitted" || view(ctx).pending)
|
|
825
820
|
return;
|
|
826
821
|
try {
|
|
827
822
|
access();
|
|
@@ -914,12 +909,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
914
909
|
}
|
|
915
910
|
return { draft: false, capacityRejected: Boolean(draft.capacityRejected), reason: draft.unavailable };
|
|
916
911
|
}
|
|
917
|
-
const
|
|
918
|
-
if (!header)
|
|
919
|
-
throw new Error("Missing native session header at tool-loop boundary");
|
|
920
|
-
const preview = SessionManager.inMemory(ctx.cwd, undefined, [header, ...ctx.sessionManager.getBranch()]);
|
|
921
|
-
preview.appendCompaction(draft.compaction.summary, draft.compaction.firstKeptEntryId, before, draft.compaction.details, true);
|
|
922
|
-
const messages = preview.buildSessionProjection().messages;
|
|
912
|
+
const messages = plannedView(ctx, { type: "compaction", ...draft.compaction, tokensBefore: before }).session.buildSessionProjection().messages;
|
|
923
913
|
assertNativeToolRounds(messages);
|
|
924
914
|
previews.push(messages);
|
|
925
915
|
return { draft: true, after: measure(messages), mode: draft.mode, compaction: draft.compaction, reserve, messages };
|
|
@@ -944,11 +934,11 @@ export async function installCompaction(pi, options = {}) {
|
|
|
944
934
|
if (!round.length)
|
|
945
935
|
continue;
|
|
946
936
|
for (const clip of round)
|
|
947
|
-
clips.set(clip.toolCallId, clip);
|
|
937
|
+
run.clips.set(clip.toolCallId, clip);
|
|
948
938
|
const clippedDecision = decide(clipRoom, targetBand, required);
|
|
949
939
|
if (clippedDecision.kind === "fault")
|
|
950
940
|
for (const clip of round)
|
|
951
|
-
clips.delete(clip.toolCallId);
|
|
941
|
+
run.clips.delete(clip.toolCallId);
|
|
952
942
|
else {
|
|
953
943
|
decision = clippedDecision;
|
|
954
944
|
break;
|
|
@@ -986,15 +976,15 @@ export async function installCompaction(pi, options = {}) {
|
|
|
986
976
|
firstKeptEntryId: compaction.firstKeptEntryId, details: compaction.details }] };
|
|
987
977
|
};
|
|
988
978
|
const atBoundary = (event, ctx, reason, decide) => {
|
|
989
|
-
|
|
979
|
+
const current = view(ctx);
|
|
980
|
+
if (!current.pending)
|
|
990
981
|
return decide ? boundaryCut(event, ctx, reason) : undefined;
|
|
991
|
-
const entry =
|
|
992
|
-
|
|
993
|
-
const preview = cutSession(ctx, entry), view = Object.create(ctx, { sessionManager: { value: preview } });
|
|
982
|
+
const entry = run.cut.take();
|
|
983
|
+
const preview = current.session, context = Object.create(ctx, { sessionManager: { value: preview } });
|
|
994
984
|
const more = decide && !event.entries.length
|
|
995
|
-
? boundaryCut({ ...event, context: { ...event.context, contextMessages: preview.buildSessionProjection().messages } },
|
|
985
|
+
? boundaryCut({ ...event, context: { ...event.context, contextMessages: preview.buildSessionProjection().messages } }, context, reason) : undefined;
|
|
996
986
|
const { tokensBefore: _, ...cut } = entry;
|
|
997
|
-
const saved = { ...cut, firstKeptEntryId:
|
|
987
|
+
const saved = { ...cut, firstKeptEntryId: current.keptFromId };
|
|
998
988
|
// As for a cut Pi makes (session_compact): the history the cut leaves behind is indexed, once Pi has committed the entries.
|
|
999
989
|
setImmediate(() => { semantic.indexAtCut(semanticProvider(ctx)); });
|
|
1000
990
|
return { entries: [...event.entries, saved, ...(more?.entries ?? [])] };
|
|
@@ -1004,37 +994,24 @@ export async function installCompaction(pi, options = {}) {
|
|
|
1004
994
|
// Pi's later native length recovery from retrying it independently. The stored message and its output usage stay on the branch.
|
|
1005
995
|
const entries = [...(atBoundary(event, ctx, "run-end", false)?.entries ?? event.entries),
|
|
1006
996
|
{ type: "context_edit", targetId: event.messageEntryId, replacement: null }];
|
|
1007
|
-
if (thinkingRetryPending)
|
|
997
|
+
if (run.thinkingRetryPending)
|
|
1008
998
|
return { entries, continue: false };
|
|
1009
|
-
const
|
|
1010
|
-
if (!header)
|
|
1011
|
-
throw new Error("Missing native session header for the thinking-only retry");
|
|
1012
|
-
const preview = SessionManager.inMemory(ctx.cwd, undefined, [header, ...ctx.sessionManager.getBranch()]);
|
|
1013
|
-
for (const entry of entries) {
|
|
1014
|
-
if (entry.type === "context_edit")
|
|
1015
|
-
preview.appendContextEdit(entry.targetId, entry.replacement);
|
|
1016
|
-
else if (entry.type === "compaction")
|
|
1017
|
-
preview.appendCompaction(entry.summary, entry.firstKeptEntryId, 0, entry.details, true, entry.usage);
|
|
1018
|
-
else if (entry.type === "custom")
|
|
1019
|
-
preview.appendCustomEntry(entry.customType, entry.data);
|
|
1020
|
-
else
|
|
1021
|
-
preview.appendCustomMessageEntry(entry.customType, entry.content, entry.display, entry.details);
|
|
1022
|
-
}
|
|
999
|
+
const preview = liveCut.view(ctx, entries).session;
|
|
1023
1000
|
const view = Object.create(ctx, { sessionManager: { value: preview } });
|
|
1024
1001
|
const hostInput = hostMessages(ctx), host = conversationTokens(hostInput);
|
|
1025
1002
|
// The deepest cut only when it buys reply room: where Pi's cap on the request as it stands (the stopped reply left out) is already
|
|
1026
1003
|
// as large as the deepest cut's, the turn is retried uncut, and no history is given up for nothing.
|
|
1027
1004
|
const asIs = projectedRequest(preview.buildSessionProjection().messages, view, hostInput, event.context.pendingMessages, ctx.getSystemPrompt(), pi.getActiveTools(), false);
|
|
1028
1005
|
const asIsCheck = requestClamp(asIs, view);
|
|
1029
|
-
const previousClips = new Map(clips);
|
|
1006
|
+
const previousClips = new Map(clips());
|
|
1030
1007
|
const decision = deepestDecision(view, false, true);
|
|
1031
1008
|
if (decision.kind === "cut" && asIsCheck.cap !== undefined && asIsCheck.leavesFloor &&
|
|
1032
1009
|
requestClamp(projectedRequest(decision.plan.messages, view, hostInput, event.context.pendingMessages, ctx.getSystemPrompt(), pi.getActiveTools(), true), view).cap <= asIsCheck.cap) {
|
|
1033
|
-
clips.clear();
|
|
1010
|
+
clips().clear();
|
|
1034
1011
|
for (const [id, clip] of previousClips)
|
|
1035
|
-
clips.set(id, clip);
|
|
1012
|
+
clips().set(id, clip);
|
|
1036
1013
|
const { room, hardLine } = limits(view);
|
|
1037
|
-
thinkingRetryPending = true;
|
|
1014
|
+
run.thinkingRetryPending = true;
|
|
1038
1015
|
return { entries: [...entries, { type: "custom", customType: "pi-tuck-capacity", data: { kind: "retry", stoppedReplyEntryId: event.messageEntryId,
|
|
1039
1016
|
stoppedOutputTokens: event.message.role === "assistant" ? event.message.usage?.output ?? 0 : 0, cut: false,
|
|
1040
1017
|
piEstimateTokens: asIsCheck.estimate, piCap: asIsCheck.cap, room, hardLine, buildId: BUILD_ID } }], continue: true };
|
|
@@ -1042,7 +1019,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
1042
1019
|
if (decision.kind !== "cut")
|
|
1043
1020
|
throw new CapacityError(clampMessage);
|
|
1044
1021
|
const messages = projectedRequest(decision.plan.messages, view, hostInput, event.context.pendingMessages, ctx.getSystemPrompt(), pi.getActiveTools(), true);
|
|
1045
|
-
if (
|
|
1022
|
+
if (run?.prompt && !carriesPrompt(messages, run.prompt.text(ctx.sessionManager.getBranch()), run.prompt.role))
|
|
1046
1023
|
throw promptRefusal();
|
|
1047
1024
|
const system = requestSystemPart(ctx.getSystemPrompt(), pi.getActiveTools(), messages, ctx.getSystemPrompt());
|
|
1048
1025
|
const tokens = system + conversationTokens(messages), check = requestClamp(messages, view);
|
|
@@ -1050,8 +1027,8 @@ export async function installCompaction(pi, options = {}) {
|
|
|
1050
1027
|
if (!fits(tokens, hardLine, view) || check.cap === undefined || !check.leavesFloor)
|
|
1051
1028
|
throw new CapacityError(clampMessage);
|
|
1052
1029
|
const compaction = captureDraft(decision.plan.compaction, view, decision.plan.reserve, "length-retry", tokens, true, false, undefined, { response, host, mandatory: system + host + conversationTokens(event.context.pendingMessages), target: decision.target, margin: room - decisionBand(view, room, system) });
|
|
1053
|
-
thinkingRetryPending = true;
|
|
1054
|
-
pendingBoundaryCommit = true;
|
|
1030
|
+
run.thinkingRetryPending = true;
|
|
1031
|
+
run.pendingBoundaryCommit = true;
|
|
1055
1032
|
return { entries: [...entries, { type: "compaction", summary: compaction.summary, firstKeptEntryId: compaction.firstKeptEntryId || null, details: compaction.details },
|
|
1056
1033
|
{ type: "custom", customType: "pi-tuck-capacity", data: { kind: "retry", stoppedReplyEntryId: event.messageEntryId,
|
|
1057
1034
|
stoppedOutputTokens: event.message.role === "assistant" ? event.message.usage?.output ?? 0 : 0, cut: true,
|
|
@@ -1059,18 +1036,18 @@ export async function installCompaction(pi, options = {}) {
|
|
|
1059
1036
|
};
|
|
1060
1037
|
pi.on("turn_end", (event, ctx) => {
|
|
1061
1038
|
try {
|
|
1062
|
-
if (!active() ||
|
|
1039
|
+
if (!active() || run?.phase !== "admitted")
|
|
1063
1040
|
return;
|
|
1064
|
-
if (thinkingOnlyLength(event.message) && !failure && !ctx.signal?.aborted && settingsFor().getCompactionSettings(ctx.model).enabled)
|
|
1041
|
+
if (thinkingOnlyLength(event.message) && !failure() && !ctx.signal?.aborted && settingsFor().getCompactionSettings(ctx.model).enabled)
|
|
1065
1042
|
return retryThinking(event, ctx);
|
|
1066
|
-
thinkingRetryPending = false;
|
|
1043
|
+
run.thinkingRetryPending = false;
|
|
1067
1044
|
const decide = event.outcome === "completed" && !ctx.signal?.aborted && event.context.canContinue && event.toolResults.length > 0 &&
|
|
1068
1045
|
settingsFor().getCompactionSettings(ctx.model).enabled;
|
|
1069
|
-
if (!
|
|
1046
|
+
if (!view(ctx).pending && !decide)
|
|
1070
1047
|
return;
|
|
1071
1048
|
const result = atBoundary(event, ctx, "tool-loop", decide);
|
|
1072
1049
|
if (result) {
|
|
1073
|
-
pendingBoundaryCommit = true;
|
|
1050
|
+
run.pendingBoundaryCommit = true;
|
|
1074
1051
|
}
|
|
1075
1052
|
return result;
|
|
1076
1053
|
}
|
|
@@ -1091,23 +1068,24 @@ export async function installCompaction(pi, options = {}) {
|
|
|
1091
1068
|
// 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
|
|
1092
1069
|
// the conversation with the cut applied. Pi sends the saved system message apart from the request's messages, so neither side carries it
|
|
1093
1070
|
// here. A request that does not carry the saved conversation as it stands cannot take the cut.
|
|
1094
|
-
const cutRequest = (messages, ctx
|
|
1071
|
+
const cutRequest = (messages, ctx) => {
|
|
1095
1072
|
const conversation = (all) => all.filter(message => message.role !== "system");
|
|
1096
1073
|
const saved = conversation(ctx.sessionManager.buildSessionProjection().messages);
|
|
1097
1074
|
for (let at = 0; at + saved.length <= messages.length; at++)
|
|
1098
1075
|
if (saved.every((message, i) => isDeepStrictEqual(messages[at + i], message)))
|
|
1099
|
-
return [...messages.slice(0, at), ...conversation(
|
|
1076
|
+
return [...messages.slice(0, at), ...conversation(view(ctx).session.buildSessionProjection().messages), ...messages.slice(at + saved.length)];
|
|
1100
1077
|
throw new Error("The prompt's cut cannot be applied: the request does not carry the saved conversation");
|
|
1101
1078
|
};
|
|
1102
1079
|
pi.on("context", (event, ctx) => {
|
|
1103
1080
|
try {
|
|
1104
1081
|
if (!active())
|
|
1105
1082
|
return;
|
|
1083
|
+
run?.prompt?.id(ctx.sessionManager.getBranch());
|
|
1106
1084
|
// Before a cut, Pi owns the whole request, including its object identities and system-message serialization.
|
|
1107
1085
|
if (!historyToolsEnabled(ctx))
|
|
1108
|
-
return clips.size ? { messages: applyClips(event.messages, clips) } : undefined;
|
|
1086
|
+
return clips().size ? { messages: applyClips(event.messages, clips()) } : undefined;
|
|
1109
1087
|
access();
|
|
1110
|
-
return { messages: applyClips(projectedMessages(
|
|
1088
|
+
return { messages: applyClips(projectedMessages(view(ctx).pending ? cutRequest(event.messages, ctx) : event.messages, ctx, options.recallPolicy), clips()) };
|
|
1111
1089
|
}
|
|
1112
1090
|
catch (error) {
|
|
1113
1091
|
try {
|
|
@@ -1138,17 +1116,18 @@ export async function installCompaction(pi, options = {}) {
|
|
|
1138
1116
|
fail(new (event.fromExtension ? CommitFault : Error)(event.errorMessage ?? "Compaction failed"), ctx);
|
|
1139
1117
|
});
|
|
1140
1118
|
pi.on("agent_before_settle", (event, ctx) => {
|
|
1141
|
-
if (pendingBoundaryCommit) {
|
|
1119
|
+
if (run?.pendingBoundaryCommit) {
|
|
1142
1120
|
if (event.outcome === "error")
|
|
1143
1121
|
return void fail(new CommitFault("Native tool-loop boundary draft remained unconfirmed at error settlement"), ctx);
|
|
1144
|
-
|
|
1122
|
+
if (run)
|
|
1123
|
+
run.pendingBoundaryCommit = false;
|
|
1145
1124
|
}
|
|
1146
1125
|
// Pi commits these entries before the run settles.
|
|
1147
1126
|
try {
|
|
1148
|
-
if (!active() ||
|
|
1127
|
+
if (!active() || run?.phase !== "admitted" || failure() || !settingsFor().getCompactionSettings(ctx.model).enabled)
|
|
1149
1128
|
return;
|
|
1150
1129
|
const decide = event.outcome === "completed" && !ctx.signal?.aborted;
|
|
1151
|
-
if (
|
|
1130
|
+
if (view(ctx).pending ? event.outcome === "error" : !decide)
|
|
1152
1131
|
return;
|
|
1153
1132
|
return atBoundary(event, ctx, "run-end", decide);
|
|
1154
1133
|
}
|
|
@@ -1158,7 +1137,8 @@ export async function installCompaction(pi, options = {}) {
|
|
|
1158
1137
|
});
|
|
1159
1138
|
pi.on("context_with_system", (event, ctx) => {
|
|
1160
1139
|
// Reaching the next request boundary proves native commit returned, not durable storage.
|
|
1161
|
-
|
|
1140
|
+
if (run)
|
|
1141
|
+
run.pendingBoundaryCommit = false;
|
|
1162
1142
|
pending = undefined;
|
|
1163
1143
|
try {
|
|
1164
1144
|
if (!active())
|
|
@@ -1177,7 +1157,7 @@ export async function installCompaction(pi, options = {}) {
|
|
|
1177
1157
|
observeBudget(tokens, ctx);
|
|
1178
1158
|
estimatedTokens = corrected(tokens, ctx);
|
|
1179
1159
|
const { room, hardLine } = limits(ctx);
|
|
1180
|
-
if (
|
|
1160
|
+
if (run?.prompt && !carriesPrompt(messages, run.prompt.text(ctx.sessionManager.getBranch()), run.prompt.role))
|
|
1181
1161
|
throw new Refusal("Prompt check failed: the request is missing the whole text of the prompt that started this run. Shorten the prompt or choose a model with a larger context window, then retry");
|
|
1182
1162
|
if (!fits(tokens, hardLine, ctx))
|
|
1183
1163
|
throw new CapacityError("Complete Pi context and response reserve exceed the estimated request budget");
|