pi-fold 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -28
- package/extensions/active-context.ts +1011 -444
- package/extensions/index.js +16 -3
- package/extensions/lib/curation.ts +56 -60
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +26 -42
- package/extensions/lib/instrumentation.ts +10 -3
- package/extensions/lib/measurement.ts +6 -12
- package/extensions/lib/persistence.ts +62 -38
- package/extensions/lib/policy.ts +171 -89
- package/extensions/lib/scheduling.ts +341 -108
- package/extensions/lib/selection.ts +6 -78
- package/extensions/lib/tool-surface.ts +29 -33
- package/extensions/lib/transcript.ts +17 -60
- package/extensions/settings.ts +481 -0
- package/package.json +4 -1
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
orderedRoots,
|
|
17
17
|
refsInOrder,
|
|
18
18
|
refsProtected,
|
|
19
|
-
toolRefsProtected,
|
|
20
19
|
visibleCollapsedRoots,
|
|
21
20
|
} from "./measurement.ts";
|
|
22
21
|
import {
|
|
@@ -28,7 +27,6 @@ import {
|
|
|
28
27
|
import {
|
|
29
28
|
ACTIVE_CONTEXT_POLICY,
|
|
30
29
|
DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
31
|
-
MAX_FOLD_SPAN_CHARS,
|
|
32
30
|
} from "./policy.ts";
|
|
33
31
|
import type {
|
|
34
32
|
ActiveContextSnapshot,
|
|
@@ -42,7 +40,6 @@ import type {
|
|
|
42
40
|
} from "./policy.ts";
|
|
43
41
|
import {
|
|
44
42
|
chapterSegments,
|
|
45
|
-
currentTurnBoundary,
|
|
46
43
|
scanTurnToolBatches,
|
|
47
44
|
structurallyClosedChapterUnits,
|
|
48
45
|
terminalAssistant,
|
|
@@ -503,9 +500,7 @@ export function pinnedChildFold(
|
|
|
503
500
|
const child = byId.get(part.foldId);
|
|
504
501
|
if (!child) continue;
|
|
505
502
|
const refs = flattenFoldRefs(child, state);
|
|
506
|
-
const pinned =
|
|
507
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
508
|
-
: refsProtected(refs, state, snapshot);
|
|
503
|
+
const pinned = refsProtected(refs, state, snapshot);
|
|
509
504
|
if (pinned) return child;
|
|
510
505
|
}
|
|
511
506
|
return null;
|
|
@@ -513,7 +508,7 @@ export function pinnedChildFold(
|
|
|
513
508
|
|
|
514
509
|
export function pinnedNestingRefusal(pinned: ActiveFold, toolName: string): string {
|
|
515
510
|
return `fold refused: ${pinned.id} is pinned, and nesting it would hide context you asked to keep. ` +
|
|
516
|
-
`Release it with ${toolName} {"action":"
|
|
511
|
+
`Release it with ${toolName} {"action":"unpin","ids":["${pinned.id}"]} first, ` +
|
|
517
512
|
"or name a span that stops at its boundary.";
|
|
518
513
|
}
|
|
519
514
|
|
|
@@ -676,7 +671,6 @@ function consolidationCandidate(
|
|
|
676
671
|
if (refsProtected(raw, state, snapshot)) return null;
|
|
677
672
|
if (raw.some((ref) => claims.refKeys.has(objectRefKey(ref)))) return null;
|
|
678
673
|
const sourceRefs = candidateSourceRefs(parts, state);
|
|
679
|
-
if (sourceRefs.length > snapshot.policy.maxFoldSourceRefs) return null;
|
|
680
674
|
return { kind: "consolidation", parts, sourceRefs };
|
|
681
675
|
}
|
|
682
676
|
|
|
@@ -735,9 +729,8 @@ export function automaticToolBatches(
|
|
|
735
729
|
refs.some((ref) => !ref || ref.role !== "toolResult")) continue;
|
|
736
730
|
const exactRefs = refs as EvidenceRef[];
|
|
737
731
|
const size = group.reduce((total, { item }) => total + bytes(item.message), 0);
|
|
738
|
-
if (exactRefs
|
|
739
|
-
|
|
740
|
-
size < snapshot.policy.minToolChars) continue;
|
|
732
|
+
if (refsProtected(exactRefs, state, snapshot) || refsInOrder(snapshot, exactRefs) === null ||
|
|
733
|
+
size < snapshot.thresholds.minFoldChars) continue;
|
|
741
734
|
batches.push({ refs: exactRefs, indices: group.map(({ item }) => item.index), bytes: size });
|
|
742
735
|
}
|
|
743
736
|
return batches;
|
|
@@ -770,9 +763,7 @@ export function selectAutomaticRefold(
|
|
|
770
763
|
if (claimedFoldIds.has(id)) return [];
|
|
771
764
|
const fold = state.folds.find((item) => item.id === id);
|
|
772
765
|
const interval = fold ? foldInterval(fold, state, snapshot) : null;
|
|
773
|
-
const protectedSource = fold && (fold
|
|
774
|
-
? toolRefsProtected(flattenFoldRefs(fold, state), state, snapshot)
|
|
775
|
-
: refsProtected(flattenFoldRefs(fold, state), state, snapshot));
|
|
766
|
+
const protectedSource = fold && (refsProtected(flattenFoldRefs(fold, state), state, snapshot));
|
|
776
767
|
return fold && interval && !protectedSource && !state.leases[id]
|
|
777
768
|
? [{ id, ...interval }]
|
|
778
769
|
: [];
|
|
@@ -805,47 +796,9 @@ export function resolveFoldInputIds(
|
|
|
805
796
|
export function chapterRangeIsUnitAligned(snapshot: ActiveContextSnapshot, start: number, end: number): boolean {
|
|
806
797
|
const units = chapterUnits(snapshot).filter((unit) => unit.end > start && unit.start <= end);
|
|
807
798
|
if (!units.length || units[0].start !== start || units.at(-1)!.end !== end + 1) return false;
|
|
808
|
-
if (new Set(units.map((unit) => unit.turnStart)).size > snapshot.policy.maxChapterTurns) return false;
|
|
809
799
|
return units.every((unit, index) => index === 0 || unit.start === units[index - 1].end);
|
|
810
800
|
}
|
|
811
801
|
|
|
812
|
-
export function splitCandidateBySize(
|
|
813
|
-
snapshot: ActiveContextSnapshot,
|
|
814
|
-
state: ActiveContextState,
|
|
815
|
-
candidate: FoldCandidate,
|
|
816
|
-
maxChars: number = MAX_FOLD_SPAN_CHARS,
|
|
817
|
-
): FoldCandidate[] {
|
|
818
|
-
if (candidate.kind !== "chapter") return [candidate];
|
|
819
|
-
const refs = candidate.sourceRefs.length ? candidate.sourceRefs : candidateSourceRefs(candidate.parts, state);
|
|
820
|
-
const indices = refs.map((ref) => exactMapped(snapshot, ref)?.index ?? -1);
|
|
821
|
-
if (!indices.length || indices.some((index) => index < 0)) return [candidate];
|
|
822
|
-
const start = Math.min(...indices);
|
|
823
|
-
const end = Math.max(...indices);
|
|
824
|
-
if (spanBytes(snapshot, start, end + 1) <= maxChars) return [candidate];
|
|
825
|
-
const units = chapterUnits(snapshot).filter((unit) => unit.start >= start && unit.end <= end + 1);
|
|
826
|
-
if (units.length < 2) return [candidate];
|
|
827
|
-
const split: FoldCandidate[] = [];
|
|
828
|
-
const emit = (from: number, to: number): boolean => {
|
|
829
|
-
const parts = partsForRange(snapshot, state, from, to, new Set<FoldKind>(["tool-result"]));
|
|
830
|
-
if (!parts) return false;
|
|
831
|
-
split.push({ kind: "chapter", parts, sourceRefs: candidateSourceRefs(parts, state) });
|
|
832
|
-
return true;
|
|
833
|
-
};
|
|
834
|
-
let runStart = units[0].start;
|
|
835
|
-
let runBytes = 0;
|
|
836
|
-
for (const unit of units) {
|
|
837
|
-
const size = spanBytes(snapshot, unit.start, unit.end);
|
|
838
|
-
if (runBytes > 0 && runBytes + size > maxChars) {
|
|
839
|
-
if (!emit(runStart, unit.start - 1)) return [candidate];
|
|
840
|
-
runStart = unit.start;
|
|
841
|
-
runBytes = 0;
|
|
842
|
-
}
|
|
843
|
-
runBytes += size;
|
|
844
|
-
}
|
|
845
|
-
if (runBytes > 0 && !emit(runStart, units.at(-1)!.end - 1)) return [candidate];
|
|
846
|
-
return split.length ? split : [candidate];
|
|
847
|
-
}
|
|
848
|
-
|
|
849
802
|
export function candidateSpanChars(
|
|
850
803
|
snapshot: ActiveContextSnapshot,
|
|
851
804
|
state: ActiveContextState,
|
|
@@ -986,11 +939,6 @@ export function snapSpanIds(
|
|
|
986
939
|
`${absorb ? "end" : "near"} boundary`);
|
|
987
940
|
}
|
|
988
941
|
}
|
|
989
|
-
const boundary = currentTurnBoundary(snapshot);
|
|
990
|
-
if (boundary >= 0 && end > boundary) {
|
|
991
|
-
end = boundary;
|
|
992
|
-
note("span reached into the turn still in flight; corrected back to the last closed turn");
|
|
993
|
-
}
|
|
994
942
|
const startUnit = units.find((unit) => start >= unit.start && start < unit.end);
|
|
995
943
|
if (startUnit && startUnit.start !== start) {
|
|
996
944
|
start = startUnit.start;
|
|
@@ -1001,23 +949,6 @@ export function snapSpanIds(
|
|
|
1001
949
|
end = endUnit.end - 1;
|
|
1002
950
|
note("span ended mid-unit; corrected to the end of its closed user/assistant/tool unit");
|
|
1003
951
|
}
|
|
1004
|
-
const covered = units.filter((unit) => unit.start >= start && unit.end <= end + 1);
|
|
1005
|
-
if (covered.length) {
|
|
1006
|
-
const turns: number[] = [];
|
|
1007
|
-
let clamped = end;
|
|
1008
|
-
for (const unit of covered) {
|
|
1009
|
-
if (!turns.includes(unit.turnStart)) {
|
|
1010
|
-
if (turns.length >= snapshot.policy.maxChapterTurns) break;
|
|
1011
|
-
turns.push(unit.turnStart);
|
|
1012
|
-
}
|
|
1013
|
-
clamped = unit.end - 1;
|
|
1014
|
-
}
|
|
1015
|
-
if (clamped < end) {
|
|
1016
|
-
end = clamped;
|
|
1017
|
-
note(`span covered more than the ${snapshot.policy.maxChapterTurns}-turn chapter limit; ` +
|
|
1018
|
-
"corrected to the last turn that fits");
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
952
|
if (start === passStart && end === passEnd) break;
|
|
1022
953
|
}
|
|
1023
954
|
if (!corrections.length || end < start ||
|
|
@@ -1067,14 +998,11 @@ export function manualFoldCandidate(
|
|
|
1067
998
|
options: { allowProtected?: boolean } = {},
|
|
1068
999
|
): FoldCandidate {
|
|
1069
1000
|
const blockedTool = (refs: EvidenceRef[]): boolean =>
|
|
1070
|
-
!options.allowProtected &&
|
|
1001
|
+
!options.allowProtected && refsProtected(refs, state, snapshot);
|
|
1071
1002
|
const blocked = (refs: EvidenceRef[]): boolean =>
|
|
1072
1003
|
!options.allowProtected && refsProtected(refs, state, snapshot);
|
|
1073
1004
|
const owners = directFoldOwners(state.folds);
|
|
1074
1005
|
const bounded = (candidate: FoldCandidate): FoldCandidate => {
|
|
1075
|
-
if (candidate.sourceRefs.length > snapshot.policy.maxFoldSourceRefs) {
|
|
1076
|
-
throw new Error(`Folds may include at most ${snapshot.policy.maxFoldSourceRefs} exact source references`);
|
|
1077
|
-
}
|
|
1078
1006
|
for (const part of candidate.parts) {
|
|
1079
1007
|
if (part.kind !== "raw") continue;
|
|
1080
1008
|
const ownerId = owners.get(objectRefKey(part.ref));
|
|
@@ -29,16 +29,16 @@ export function buildActiveContextTool(input: {
|
|
|
29
29
|
handler: ToolHandler;
|
|
30
30
|
}) {
|
|
31
31
|
const peekGuidance = input.allowedActions.includes("peek")
|
|
32
|
-
? ` Peek is the point read: it returns one fold's index view -- every nested fold's brief in full, plus the head and tail of that fold's own span with the omitted middle stated -- for any fold id, ancestors still collapsed, changing nothing. It serves ONE level: raw entries come back exactly, folds nested inside stay placeheld, and peeking a child id you see there returns that child's own bytes, so anything folded is one hop away at any depth. It returns at most ${input.defaultPeekBytes} bytes unless you widen it with bytes, and it
|
|
32
|
+
? ` Peek is the point read: it returns one fold's index view -- every nested fold's brief in full, plus the head and tail of that fold's own span with the omitted middle stated -- for any fold id, ancestors still collapsed, changing nothing. It serves ONE level: raw entries come back exactly, folds nested inside stay placeheld, and peeking a child id you see there returns that child's own bytes, so anything folded is one hop away at any depth. It returns at most ${input.defaultPeekBytes} bytes unless you widen it with bytes, and it is EPHEMERAL by default: the bytes ride only until your next message, your reply then the surviving trace, so extract what your current task needs from them in that reply. Peeking again is lossless and costs one append, so a read you turn out to need twice is cheap; pass ephemeral false when you need the bytes standing across several turns. Expand is the durable in-place restoration.`
|
|
33
33
|
: "";
|
|
34
|
-
const correctionGuidance = input.allowedActions.includes("
|
|
35
|
-
? " Curation is correctable:
|
|
34
|
+
const correctionGuidance = input.allowedActions.includes("reboundary")
|
|
35
|
+
? " Curation is correctable: reboundary with ids re-cuts a span into exactly one fold, which merges adjacent folds when the span covers several and splits one when the span sits inside it. Reboundary with a single id returns that fold's span to raw. A brief is written when you mark the span; a standing fold's brief is not rewritten, because rewriting it rewrites the projection at that fold's placeholder and costs the whole prefix cache from there on."
|
|
36
36
|
: "";
|
|
37
|
-
const pinGuidance = input.allowedActions.includes("
|
|
38
|
-
? "
|
|
37
|
+
const pinGuidance = input.allowedActions.includes("pin")
|
|
38
|
+
? " Pin with ids holds folds or entries against automatic folding, consolidation and refolding until unpin releases them, so an expanded span you still need stays expanded. Pin reveals nothing on its own: a folded span stays folded, just held. Pinning moves no context bytes, and fold receipts report how much mass you have pinned."
|
|
39
39
|
: "";
|
|
40
|
-
const epochGuidance = input.allowedActions.includes("
|
|
41
|
-
? "
|
|
40
|
+
const epochGuidance = input.allowedActions.includes("brief")
|
|
41
|
+
? " THE RUNTIME CUTS THE FOLDS AND YOU ANNOTATE THEM. As raw material piles up it is cut into pending folds behind you, stalest first. A pending fold moves NO context bytes: you keep seeing the raw material exactly as it is, and the fold reaches your window only when a commit applies it. You are told when folds are cut and which ones carry no brief yet. Answer with brief, one id and one sentence: you know why the span mattered and what you will want back from it, and the deterministic brief that fills the gap reads the span alone and does not. Writing a brief on a pending fold is FREE, because the fold is not in your window yet; briefing a standing fold is refused, because its placeholder is in your window and rewriting it rewrites your context from that point on. reboundary re-cuts a span the runtime got wrong, unmark withdraws a cut you do not want taken, and when to commit stays the runtime's to decide."
|
|
42
42
|
: "";
|
|
43
43
|
const recoveryGuidance = input.allowedActions.includes("expand")
|
|
44
44
|
? " A fold brief is an index entry, not the source: when a question or task depends on material that is folded, peek or expand that fold and answer from the exact bytes, never from memory of them. Recalling folded detail without reopening it is how specifics get misremembered."
|
|
@@ -48,8 +48,8 @@ export function buildActiveContextTool(input: {
|
|
|
48
48
|
name: input.name,
|
|
49
49
|
label: input.label,
|
|
50
50
|
description: input.fullSurface
|
|
51
|
-
? `Page, peek,
|
|
52
|
-
: `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance}
|
|
51
|
+
? `Page, peek, brief, expand, refold, pin, or re-cut exact Pi active-context evidence.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance} Mutations persist immediately and affect the next model call inside the same continuing turn; no turn boundary is required. Supplied fold briefs have a hard ${input.maxBriefChars}-character maximum.`
|
|
52
|
+
: `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance} Supplied fold briefs have a hard ${input.maxBriefChars}-character maximum.`,
|
|
53
53
|
parameters: {
|
|
54
54
|
type: "object",
|
|
55
55
|
additionalProperties: false,
|
|
@@ -58,35 +58,24 @@ export function buildActiveContextTool(input: {
|
|
|
58
58
|
action: { type: "string", enum: input.allowedActions },
|
|
59
59
|
ids: { type: "array", minItems: 1, maxItems: 64, items: { type: "string", minLength: 1 } },
|
|
60
60
|
id: { type: "string", minLength: 1 },
|
|
61
|
-
marks: {
|
|
62
|
-
type: "array",
|
|
63
|
-
minItems: 1,
|
|
64
|
-
maxItems: 64,
|
|
65
|
-
description: "Fold only: several spans with their own briefs in one call, which is the shape to prefer. An invalid or overlapping span is corrected to the nearest valid edge and the correction is reported back.",
|
|
66
|
-
items: {
|
|
67
|
-
type: "object",
|
|
68
|
-
additionalProperties: false,
|
|
69
|
-
required: ["ids"],
|
|
70
|
-
properties: {
|
|
71
|
-
ids: { type: "array", minItems: 1, maxItems: 64, items: { type: "string", minLength: 1 } },
|
|
72
|
-
brief: {
|
|
73
|
-
type: "string",
|
|
74
|
-
minLength: 1,
|
|
75
|
-
maxLength: input.maxBriefChars,
|
|
76
|
-
description: "Write one for every span you mark. You know why the span mattered and " +
|
|
77
|
-
"what you will want back from it; the deterministic brief that fills the gap " +
|
|
78
|
-
"reads the span alone and does not.",
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
61
|
brief: {
|
|
84
62
|
type: "string",
|
|
85
63
|
minLength: 1,
|
|
86
64
|
maxLength: input.maxBriefChars,
|
|
87
|
-
description:
|
|
65
|
+
description: "Brief only: what this pending fold covers and what you will want " +
|
|
66
|
+
`back from it, at most ${input.maxBriefChars} characters. Free to write, ` +
|
|
67
|
+
"because the fold it names is not in your window yet.",
|
|
88
68
|
},
|
|
89
69
|
offset: { type: "integer", minimum: 0 },
|
|
70
|
+
ephemeral: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "Peek only. Default TRUE: the returned bytes ride your context " +
|
|
73
|
+
"exactly until your next message, then their place holds a one-line " +
|
|
74
|
+
"placeholder and your reply is the surviving trace, so extract what you need " +
|
|
75
|
+
"into that reply. Peeking again is lossless. Pass false to keep the result " +
|
|
76
|
+
"standing like any other tool result, which is what you want when the bytes " +
|
|
77
|
+
"have to survive several turns of work.",
|
|
78
|
+
},
|
|
90
79
|
bytes: {
|
|
91
80
|
type: "integer",
|
|
92
81
|
minimum: input.minPeekSliceBytes,
|
|
@@ -103,8 +92,10 @@ export function buildActiveContextTool(input: {
|
|
|
103
92
|
export function buildActiveContextCommands(input: {
|
|
104
93
|
statusName: string;
|
|
105
94
|
foldName: string;
|
|
95
|
+
editorName: string;
|
|
106
96
|
statusHandler: CommandHandler;
|
|
107
97
|
foldHandler: CommandHandler;
|
|
98
|
+
editorHandler: CommandHandler;
|
|
108
99
|
}) {
|
|
109
100
|
return [
|
|
110
101
|
{
|
|
@@ -114,9 +105,14 @@ export function buildActiveContextCommands(input: {
|
|
|
114
105
|
},
|
|
115
106
|
{
|
|
116
107
|
name: input.foldName,
|
|
117
|
-
description: "
|
|
108
|
+
description: "Commit every staged fold in one epoch; with explicit ids, folds exactly that span now; " +
|
|
118
109
|
"works without a main-model request",
|
|
119
110
|
handler: input.foldHandler,
|
|
120
111
|
},
|
|
112
|
+
{
|
|
113
|
+
name: input.editorName,
|
|
114
|
+
description: "Open the context window editor: occupancy, fold roots and briefs, staged marks, pins",
|
|
115
|
+
handler: input.editorHandler,
|
|
116
|
+
},
|
|
121
117
|
];
|
|
122
118
|
}
|
|
@@ -3,12 +3,10 @@ import {
|
|
|
3
3
|
evidenceRef,
|
|
4
4
|
evidenceValue,
|
|
5
5
|
isPlainRecord,
|
|
6
|
-
objectRefKey,
|
|
7
6
|
sha256Text,
|
|
8
7
|
stableStringify,
|
|
9
8
|
} from "../json.ts";
|
|
10
9
|
import {
|
|
11
|
-
bytes,
|
|
12
10
|
clone,
|
|
13
11
|
messageRole,
|
|
14
12
|
ownValue,
|
|
@@ -24,7 +22,6 @@ import {
|
|
|
24
22
|
DEFAULT_THRESHOLDS,
|
|
25
23
|
PEEK_READ_ONLY_CONTEXT_ACTIONS,
|
|
26
24
|
servingBudgetTokens,
|
|
27
|
-
zoneBytes,
|
|
28
25
|
} from "./policy.ts";
|
|
29
26
|
import type {
|
|
30
27
|
ActiveContextSnapshot,
|
|
@@ -70,7 +67,13 @@ export function leadingCompactionContinuation(messages: unknown[]): CompleteTurn
|
|
|
70
67
|
|
|
71
68
|
export const READ_ONLY_CONTEXT_ACTION_ARGUMENTS: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
72
69
|
status: Object.freeze(["action", "detail", "offset", "limit"]),
|
|
73
|
-
|
|
70
|
+
// offset and bytes are the tool surface's own narrowing parameters: a paged or
|
|
71
|
+
// narrowed peek is as byte-inert as a bare one, and excluding them made the
|
|
72
|
+
// read built to SAVE window mass linger as raw mass past the reclaimer.
|
|
73
|
+
// ephemeral joins them for the same reason: it changes when the RESULT leaves
|
|
74
|
+
// the projection, never what the call moves, and a call the reclaimer cannot
|
|
75
|
+
// claim would linger as exactly the mass the parameter exists to shed.
|
|
76
|
+
peek: Object.freeze(["action", "id", "offset", "bytes", "ephemeral"]),
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
export function isAutoFoldableToolCall(
|
|
@@ -317,7 +320,14 @@ export function unsafeChapterIndices(messages: unknown[]): Set<number> {
|
|
|
317
320
|
return unsafe;
|
|
318
321
|
}
|
|
319
322
|
|
|
320
|
-
|
|
323
|
+
/**
|
|
324
|
+
* The first index of an unfinished user turn, or the end of the transcript when the last
|
|
325
|
+
* turn completed. Folding into an unfinished turn leaves a user message whose tool calls are
|
|
326
|
+
* unanswered, which is malformed for every provider, so this is a validity boundary rather
|
|
327
|
+
* than a curation preference. The fresh-tail byte walk that used to sit here is deleted:
|
|
328
|
+
* nothing decided on it, and stale-first ordering already leaves recent material last.
|
|
329
|
+
*/
|
|
330
|
+
export function freshBoundary(messages: unknown[], turns: CompleteTurn[]): number {
|
|
321
331
|
if (!messages.length) return 0;
|
|
322
332
|
let unfinishedUser = messages.length;
|
|
323
333
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
@@ -329,36 +339,7 @@ export function freshBoundary(messages: unknown[], turns: CompleteTurn[], freshB
|
|
|
329
339
|
unfinishedUser = complete ? messages.length : index;
|
|
330
340
|
break;
|
|
331
341
|
}
|
|
332
|
-
|
|
333
|
-
let boundary = Math.min(messages.length, unfinishedUser);
|
|
334
|
-
while (boundary > 0) {
|
|
335
|
-
const suffix: unknown[] = [];
|
|
336
|
-
for (let index = boundary; index < messages.length; index += 1) suffix.push(messages[index]);
|
|
337
|
-
if (bytes(suffix) >= freshBytes) break;
|
|
338
|
-
let previous: CompleteTurn | null = null;
|
|
339
|
-
for (let turnIndex = 0; turnIndex < turns.length; turnIndex += 1) {
|
|
340
|
-
if (turns[turnIndex].start < boundary) previous = turns[turnIndex];
|
|
341
|
-
}
|
|
342
|
-
if (!previous) { boundary = leading?.end ?? 0; break; }
|
|
343
|
-
boundary = previous.start;
|
|
344
|
-
}
|
|
345
|
-
return boundary;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
export function toolFreshIndices(
|
|
349
|
-
messages: unknown[],
|
|
350
|
-
_turns: CompleteTurn[],
|
|
351
|
-
freshBytes: number,
|
|
352
|
-
): Set<number> {
|
|
353
|
-
const protectedIndices = new Set<number>();
|
|
354
|
-
let tailBytes = 0;
|
|
355
|
-
let boundary = messages.length;
|
|
356
|
-
while (boundary > 0 && tailBytes < freshBytes) {
|
|
357
|
-
boundary -= 1;
|
|
358
|
-
tailBytes += bytes(messages[boundary]);
|
|
359
|
-
}
|
|
360
|
-
for (let index = boundary; index < messages.length; index += 1) protectedIndices.add(index);
|
|
361
|
-
return protectedIndices;
|
|
342
|
+
return Math.min(messages.length, unfinishedUser);
|
|
362
343
|
}
|
|
363
344
|
|
|
364
345
|
type EntryEvidence = { message: unknown; ref: BranchObject["ref"] };
|
|
@@ -486,17 +467,13 @@ export function mapActiveContext(input: {
|
|
|
486
467
|
const budgetTokens = input.netBudget === true
|
|
487
468
|
? (reportedContextWindow ?? DEFAULT_CONTEXT_WINDOW)
|
|
488
469
|
: servingBudgetTokens(reportedContextWindow ?? DEFAULT_CONTEXT_WINDOW);
|
|
489
|
-
const freshBytes = zoneBytes(thresholds.freshTail, budgetTokens);
|
|
490
470
|
const turns = completeTurns(input.eventMessages);
|
|
491
|
-
const boundary = freshBoundary(input.eventMessages, turns
|
|
471
|
+
const boundary = freshBoundary(input.eventMessages, turns);
|
|
492
472
|
const protectedIndices = unsafeChapterIndices(input.eventMessages);
|
|
493
|
-
const toolProtectedIndices = toolFreshIndices(input.eventMessages, turns, freshBytes);
|
|
494
|
-
for (const index of toolProtectedIndices) protectedIndices.add(index);
|
|
495
473
|
for (let mappedIndex = 0; mappedIndex < mapped.length; mappedIndex += 1) {
|
|
496
474
|
const item = mapped[mappedIndex];
|
|
497
475
|
if (item.ref) continue;
|
|
498
476
|
protectedIndices.add(item.index);
|
|
499
|
-
toolProtectedIndices.add(item.index);
|
|
500
477
|
}
|
|
501
478
|
const snapshot: ActiveContextSnapshot = {
|
|
502
479
|
sessionId: input.sessionId,
|
|
@@ -508,7 +485,6 @@ export function mapActiveContext(input: {
|
|
|
508
485
|
thresholds,
|
|
509
486
|
budgetTokens,
|
|
510
487
|
protectedIndices,
|
|
511
|
-
toolProtectedIndices,
|
|
512
488
|
policy,
|
|
513
489
|
toolName: input.toolName ?? DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
514
490
|
brandNoun: input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
@@ -520,22 +496,3 @@ export function mapActiveContext(input: {
|
|
|
520
496
|
};
|
|
521
497
|
return snapshot;
|
|
522
498
|
}
|
|
523
|
-
|
|
524
|
-
export function currentTurnBoundary(snapshot: Pick<ActiveContextSnapshot, "messages">): number {
|
|
525
|
-
let boundary = -1;
|
|
526
|
-
for (let index = 0; index < snapshot.messages.length; index += 1) {
|
|
527
|
-
if (terminalAssistant(snapshot.messages[index])) boundary = index;
|
|
528
|
-
}
|
|
529
|
-
return boundary;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
export function currentTurnRefKeys(snapshot: ActiveContextSnapshot): Set<string> {
|
|
533
|
-
const boundary = currentTurnBoundary(snapshot);
|
|
534
|
-
const keys = new Set<string>();
|
|
535
|
-
for (const item of snapshot.mapped) {
|
|
536
|
-
if (item.index <= boundary || !item.ref) continue;
|
|
537
|
-
if (messageRole(item.message) !== "toolResult") continue;
|
|
538
|
-
keys.add(objectRefKey(item.ref));
|
|
539
|
-
}
|
|
540
|
-
return keys;
|
|
541
|
-
}
|