pi-fold 2.0.0 → 3.1.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 +79 -191
- package/extensions/active-context.ts +1332 -526
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +17 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +94 -61
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +207 -59
- package/extensions/lib/instrumentation.ts +10 -9
- package/extensions/lib/measurement.ts +43 -16
- package/extensions/lib/persistence.ts +273 -46
- package/extensions/lib/policy.ts +251 -95
- package/extensions/lib/scheduling.ts +446 -126
- package/extensions/lib/selection.ts +117 -116
- package/extensions/lib/tool-surface.ts +60 -33
- package/extensions/lib/transcript.ts +82 -74
- package/extensions/settings.ts +456 -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,9 +40,9 @@ import type {
|
|
|
42
40
|
} from "./policy.ts";
|
|
43
41
|
import {
|
|
44
42
|
chapterSegments,
|
|
45
|
-
currentTurnBoundary,
|
|
46
43
|
scanTurnToolBatches,
|
|
47
44
|
structurallyClosedChapterUnits,
|
|
45
|
+
deadAssistant,
|
|
48
46
|
terminalAssistant,
|
|
49
47
|
} from "./transcript.ts";
|
|
50
48
|
import type { ChapterUnit } from "./transcript.ts";
|
|
@@ -105,7 +103,14 @@ export function resultCallIndex(snapshot: ActiveContextSnapshot, allowConsumedIn
|
|
|
105
103
|
const message = snapshot.messages[index];
|
|
106
104
|
if (messageRole(message) !== "assistant") continue;
|
|
107
105
|
if (ownValue(message, "stopReason") === "toolUse") laterGenerations += 1;
|
|
108
|
-
|
|
106
|
+
// A dead turn's own terminal grants the point a finished turn's does: the turn
|
|
107
|
+
// is only in completeTurns because a user message already stands after it, so
|
|
108
|
+
// no later response will ever consume this batch, and demanding one anyway is
|
|
109
|
+
// the hold that deadlocked sol-20260826-full2 (the 423KB peek copy behind an
|
|
110
|
+
// errored assistant, ineligible for thirty-five aborting passes).
|
|
111
|
+
else if (complete && (terminalAssistant(message) || deadAssistant(message))) {
|
|
112
|
+
laterGenerations = Math.max(laterGenerations, 1);
|
|
113
|
+
}
|
|
109
114
|
}
|
|
110
115
|
if ((complete && laterGenerations < 1) || (!complete && laterGenerations < 2)) continue;
|
|
111
116
|
}
|
|
@@ -503,9 +508,7 @@ export function pinnedChildFold(
|
|
|
503
508
|
const child = byId.get(part.foldId);
|
|
504
509
|
if (!child) continue;
|
|
505
510
|
const refs = flattenFoldRefs(child, state);
|
|
506
|
-
const pinned =
|
|
507
|
-
? toolRefsProtected(refs, state, snapshot)
|
|
508
|
-
: refsProtected(refs, state, snapshot);
|
|
511
|
+
const pinned = refsProtected(refs, state, snapshot);
|
|
509
512
|
if (pinned) return child;
|
|
510
513
|
}
|
|
511
514
|
return null;
|
|
@@ -513,7 +516,7 @@ export function pinnedChildFold(
|
|
|
513
516
|
|
|
514
517
|
export function pinnedNestingRefusal(pinned: ActiveFold, toolName: string): string {
|
|
515
518
|
return `fold refused: ${pinned.id} is pinned, and nesting it would hide context you asked to keep. ` +
|
|
516
|
-
`Release it with ${toolName} {"action":"
|
|
519
|
+
`Release it with ${toolName} {"action":"unpin","ids":["${pinned.id}"]} first, ` +
|
|
517
520
|
"or name a span that stops at its boundary.";
|
|
518
521
|
}
|
|
519
522
|
|
|
@@ -676,7 +679,6 @@ function consolidationCandidate(
|
|
|
676
679
|
if (refsProtected(raw, state, snapshot)) return null;
|
|
677
680
|
if (raw.some((ref) => claims.refKeys.has(objectRefKey(ref)))) return null;
|
|
678
681
|
const sourceRefs = candidateSourceRefs(parts, state);
|
|
679
|
-
if (sourceRefs.length > snapshot.policy.maxFoldSourceRefs) return null;
|
|
680
682
|
return { kind: "consolidation", parts, sourceRefs };
|
|
681
683
|
}
|
|
682
684
|
|
|
@@ -734,15 +736,47 @@ export function automaticToolBatches(
|
|
|
734
736
|
if (ids.size !== expected.length || expected.some((id) => !ids.has(id)) ||
|
|
735
737
|
refs.some((ref) => !ref || ref.role !== "toolResult")) continue;
|
|
736
738
|
const exactRefs = refs as EvidenceRef[];
|
|
737
|
-
const size = group.reduce((total, { item }) => total +
|
|
738
|
-
if (exactRefs
|
|
739
|
-
|
|
740
|
-
size < snapshot.policy.minToolChars) continue;
|
|
739
|
+
const size = group.reduce((total, { item }) => total + messageBytes(item.message), 0);
|
|
740
|
+
if (refsProtected(exactRefs, state, snapshot) || refsInOrder(snapshot, exactRefs) === null ||
|
|
741
|
+
size < snapshot.thresholds.minFoldChars) continue;
|
|
741
742
|
batches.push({ refs: exactRefs, indices: group.map(({ item }) => item.index), bytes: size });
|
|
742
743
|
}
|
|
743
744
|
return batches;
|
|
744
745
|
}
|
|
745
746
|
|
|
747
|
+
/**
|
|
748
|
+
* The serialized size of ONE transcript message, derived once per message object.
|
|
749
|
+
*
|
|
750
|
+
* `selectAutomaticSpan` tries the batch lane first, so this runs up to
|
|
751
|
+
* MAX_FRONTIER_CUTS_PER_PASS times per context event from `frontierMarks`, once per staged
|
|
752
|
+
* mark in a fill, and again from `unmarkedRemainder`. Everything else in
|
|
753
|
+
* `automaticToolBatches` was already memoized; this was not, and it ran BEFORE the
|
|
754
|
+
* eligibility filter, on every complete group including ones that go on to be refused.
|
|
755
|
+
*
|
|
756
|
+
* IT IS KEYED ON THE MESSAGE OBJECT, which is only useful because of where the object comes
|
|
757
|
+
* from: `snapshot.mapped[i].message` is `input.eventMessages[index]` VERBATIM
|
|
758
|
+
* (transcript.ts:494-508), so it is the same object across every snapshot derived from the
|
|
759
|
+
* same session, and the memo hits. `snapshot.messages` is `clone(input.eventMessages)`
|
|
760
|
+
* (transcript.ts:531), fresh objects on every derivation, so a caller reading THAT would
|
|
761
|
+
* never hit and should not be routed here.
|
|
762
|
+
*
|
|
763
|
+
* Deliberately NOT a wrapper on `bytes()`, which is also called on transient arrays
|
|
764
|
+
* (folding.ts, scheduling.ts) whose keys could never hit and would only grow the map.
|
|
765
|
+
*
|
|
766
|
+
* The accepted cost is gate 121's: a message mutated in place keeps its first size.
|
|
767
|
+
*/
|
|
768
|
+
const MESSAGE_BYTES = new WeakMap<object, number>();
|
|
769
|
+
|
|
770
|
+
export function messageBytes(message: unknown): number {
|
|
771
|
+
const key = message && typeof message === "object" ? message as object : null;
|
|
772
|
+
if (!key) return bytes(message);
|
|
773
|
+
const memoized = MESSAGE_BYTES.get(key);
|
|
774
|
+
if (memoized !== undefined) return memoized;
|
|
775
|
+
const size = bytes(message);
|
|
776
|
+
MESSAGE_BYTES.set(key, size);
|
|
777
|
+
return size;
|
|
778
|
+
}
|
|
779
|
+
|
|
746
780
|
export function selectAutomaticToolBatch(
|
|
747
781
|
snapshot: ActiveContextSnapshot,
|
|
748
782
|
state: ActiveContextState,
|
|
@@ -770,9 +804,7 @@ export function selectAutomaticRefold(
|
|
|
770
804
|
if (claimedFoldIds.has(id)) return [];
|
|
771
805
|
const fold = state.folds.find((item) => item.id === id);
|
|
772
806
|
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));
|
|
807
|
+
const protectedSource = fold && (refsProtected(flattenFoldRefs(fold, state), state, snapshot));
|
|
776
808
|
return fold && interval && !protectedSource && !state.leases[id]
|
|
777
809
|
? [{ id, ...interval }]
|
|
778
810
|
: [];
|
|
@@ -805,47 +837,9 @@ export function resolveFoldInputIds(
|
|
|
805
837
|
export function chapterRangeIsUnitAligned(snapshot: ActiveContextSnapshot, start: number, end: number): boolean {
|
|
806
838
|
const units = chapterUnits(snapshot).filter((unit) => unit.end > start && unit.start <= end);
|
|
807
839
|
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
840
|
return units.every((unit, index) => index === 0 || unit.start === units[index - 1].end);
|
|
810
841
|
}
|
|
811
842
|
|
|
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
843
|
export function candidateSpanChars(
|
|
850
844
|
snapshot: ActiveContextSnapshot,
|
|
851
845
|
state: ActiveContextState,
|
|
@@ -986,11 +980,6 @@ export function snapSpanIds(
|
|
|
986
980
|
`${absorb ? "end" : "near"} boundary`);
|
|
987
981
|
}
|
|
988
982
|
}
|
|
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
983
|
const startUnit = units.find((unit) => start >= unit.start && start < unit.end);
|
|
995
984
|
if (startUnit && startUnit.start !== start) {
|
|
996
985
|
start = startUnit.start;
|
|
@@ -1001,23 +990,6 @@ export function snapSpanIds(
|
|
|
1001
990
|
end = endUnit.end - 1;
|
|
1002
991
|
note("span ended mid-unit; corrected to the end of its closed user/assistant/tool unit");
|
|
1003
992
|
}
|
|
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
993
|
if (start === passStart && end === passEnd) break;
|
|
1022
994
|
}
|
|
1023
995
|
if (!corrections.length || end < start ||
|
|
@@ -1066,15 +1038,10 @@ export function manualFoldCandidate(
|
|
|
1066
1038
|
ids: string[],
|
|
1067
1039
|
options: { allowProtected?: boolean } = {},
|
|
1068
1040
|
): FoldCandidate {
|
|
1069
|
-
const blockedTool = (refs: EvidenceRef[]): boolean =>
|
|
1070
|
-
!options.allowProtected && toolRefsProtected(refs, state, snapshot);
|
|
1071
1041
|
const blocked = (refs: EvidenceRef[]): boolean =>
|
|
1072
1042
|
!options.allowProtected && refsProtected(refs, state, snapshot);
|
|
1073
1043
|
const owners = directFoldOwners(state.folds);
|
|
1074
1044
|
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
1045
|
for (const part of candidate.parts) {
|
|
1079
1046
|
if (part.kind !== "raw") continue;
|
|
1080
1047
|
const ownerId = owners.get(objectRefKey(part.ref));
|
|
@@ -1098,10 +1065,10 @@ export function manualFoldCandidate(
|
|
|
1098
1065
|
calls.length === first.batch.length &&
|
|
1099
1066
|
new Set(calls.map((call) => call!.id)).size === first.batch.length &&
|
|
1100
1067
|
first.batch.every((id) => calls.some((call) => call!.id === id));
|
|
1101
|
-
if (completeBatch && !
|
|
1068
|
+
if (completeBatch && !blocked(refs)) {
|
|
1102
1069
|
return bounded({ kind: "tool-result", parts: refs.map((ref) => ({ kind: "raw", ref })), sourceRefs: refs });
|
|
1103
1070
|
}
|
|
1104
|
-
if (one && first && first.batch.length === 1 && !
|
|
1071
|
+
if (one && first && first.batch.length === 1 && !blocked(refs)) {
|
|
1105
1072
|
return bounded({ kind: "tool-result", parts: [{ kind: "raw", ref: refs[0] }], sourceRefs: refs });
|
|
1106
1073
|
}
|
|
1107
1074
|
}
|
|
@@ -1133,6 +1100,26 @@ export function manualFoldCandidate(
|
|
|
1133
1100
|
return bounded({ kind: "chapter", parts, sourceRefs: refs });
|
|
1134
1101
|
}
|
|
1135
1102
|
|
|
1103
|
+
// THE TOOL-CLIP VIEW HEAD (2026-08-24). A clipped result keeps its opening verbatim,
|
|
1104
|
+
// not collapsed: the view stays a readable result head in place, unlike a brief's
|
|
1105
|
+
// one-line quote. The paragraph rule is gate 134's: an opening block terminated by a
|
|
1106
|
+
// blank line is kept whole up to the cap; a result that is bulk from line one keeps its
|
|
1107
|
+
// first cap's worth. The caller states the cut, per gate 136: the marker rides beside
|
|
1108
|
+
// the count of what it hides, never silently.
|
|
1109
|
+
export function toolClipHead(text: string, cap: number): string {
|
|
1110
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
1111
|
+
const kept: string[] = [];
|
|
1112
|
+
for (const line of lines) {
|
|
1113
|
+
if (line.trim()) kept.push(line);
|
|
1114
|
+
else if (kept.length) break;
|
|
1115
|
+
}
|
|
1116
|
+
const paragraph = kept.join("\n");
|
|
1117
|
+
const head = paragraph.length > 0 && paragraph.length <= cap
|
|
1118
|
+
? paragraph
|
|
1119
|
+
: String(text ?? "").slice(0, cap);
|
|
1120
|
+
return head;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1136
1123
|
export function oneLine(value: string, maximum: number): string {
|
|
1137
1124
|
return boundedSubject(value.replace(/\s+/g, " ").trim(), maximum);
|
|
1138
1125
|
}
|
|
@@ -1145,44 +1132,58 @@ export function deterministicChapterBrief(
|
|
|
1145
1132
|
if (refs.length !== messages.length || !refs.length) {
|
|
1146
1133
|
throw new Error("Deterministic chapter brief requires aligned exact evidence");
|
|
1147
1134
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1135
|
+
// EVERY ENTRY IDENTIFIED IN SPAN ORDER (Shane 2026-08-24). The count-style brief
|
|
1136
|
+
// ("User: ... \u00b7 Tools: 3\u00d7bash \u00b7 Assistant: No assistant text in this span")
|
|
1137
|
+
// collapsed to the same anonymous line on every residue span of the pull workload:
|
|
1138
|
+
// sol-20260823-live rep 3 committed 188 byte-identical placeholders of that shape and
|
|
1139
|
+
// the agent re-peeked 76 folds 209 times to learn what each one held. The span's own
|
|
1140
|
+
// calls are the index: each call named with its arguments and its result's opening
|
|
1141
|
+
// line, an errored result marked, the user's ask and every assistant note seated in
|
|
1142
|
+
// span order, and what the cap refuses COUNTED rather than dropped (the gate-135 law
|
|
1143
|
+
// carried forward: a value recorded once in an assistant note between two batches must
|
|
1144
|
+
// reach the brief). Cuts run through oneLine, so "..." always means the exact source
|
|
1145
|
+
// continues.
|
|
1146
|
+
const callNames = new Map<string, string>();
|
|
1147
|
+
const pieces: string[] = [];
|
|
1150
1148
|
for (const message of messages) {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1149
|
+
const role = messageRole(message);
|
|
1150
|
+
if (role === "user") {
|
|
1151
|
+
const ask = contentText(message);
|
|
1152
|
+
if (ask.trim()) pieces.push(`User asked: ${oneLine(ask, 120)}`);
|
|
1153
|
+
} else if (role === "assistant") {
|
|
1154
|
+
for (const part of denseOwnArrayValues(ownValue(message, "content")) ?? []) {
|
|
1155
|
+
if (ownValue(part, "type") !== "toolCall") continue;
|
|
1156
|
+
const id = ownValue(part, "id");
|
|
1157
|
+
const name = ownValue(part, "name");
|
|
1158
|
+
if (typeof id !== "string" || !id || typeof name !== "string" || !name) continue;
|
|
1159
|
+
let args = "{}";
|
|
1160
|
+
try {
|
|
1161
|
+
args = JSON.stringify(ownValue(part, "arguments") ?? {}) ?? "{}";
|
|
1162
|
+
} catch {
|
|
1163
|
+
args = "{unserializable}";
|
|
1164
|
+
}
|
|
1165
|
+
callNames.set(id, `${name}(${oneLine(args, 80)})`);
|
|
1166
|
+
}
|
|
1167
|
+
const note = assistantNoteText(message);
|
|
1168
|
+
if (note) pieces.push(`Noted: ${boundedSubject(note, CHAPTER_NOTE_CHARS)}`);
|
|
1169
|
+
} else if (role === "toolResult") {
|
|
1170
|
+
const id = ownValue(message, "toolCallId");
|
|
1171
|
+
const name = ownValue(message, "toolName");
|
|
1172
|
+
const head = (typeof id === "string" ? callNames.get(id) : undefined) ??
|
|
1173
|
+
(typeof name === "string" && name ? `${name}(...)` : "tool(...)");
|
|
1174
|
+
const errored = ownValue(message, "isError") === true ? " [errored]" : "";
|
|
1175
|
+
pieces.push(`${head}${errored} \u2192 "${oneLine(contentText(message), 90)}"`);
|
|
1156
1176
|
}
|
|
1157
1177
|
}
|
|
1158
|
-
|
|
1159
|
-
// (Shane 2026-08-14). A chapter is the first fold that hides its span, and a
|
|
1160
|
-
// value the agent recorded once on a later turn died with the old single
|
|
1161
|
-
// line: sol-20260814-traps rep 2 lost trace-a-02 and trace-d-05 to exactly
|
|
1162
|
-
// that shape, an assistant message between two tool batches whose words
|
|
1163
|
-
// reached zero briefs. Each note is the message's leading paragraph, capped
|
|
1164
|
-
// per note, seated whole in span order until the brief's own bound refuses
|
|
1165
|
-
// the next one, with the count of what did not seat stated rather than cut.
|
|
1166
|
-
const notes = messages.flatMap((message) => {
|
|
1167
|
-
if (messageRole(message) !== "assistant") return [];
|
|
1168
|
-
const note = assistantNoteText(message);
|
|
1169
|
-
return note ? [boundedSubject(note, CHAPTER_NOTE_CHARS)] : [];
|
|
1170
|
-
});
|
|
1171
|
-
const ask = oneLine(firstUser ? contentText(firstUser) : "No user ask in this span", 90);
|
|
1172
|
-
const tools = oneLine([...toolCounts.entries()]
|
|
1173
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
1174
|
-
.map(([name, count]) => `${count}×${name}`)
|
|
1175
|
-
.join(" ") || "no tools", 500);
|
|
1176
|
-
const separator = " | ";
|
|
1178
|
+
const separator = " \u00b7 ";
|
|
1177
1179
|
const compose = (kept: number): string => {
|
|
1178
|
-
const omitted =
|
|
1179
|
-
const shown =
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
return `User: ${ask} · Tools: ${tools} · Assistant: ${shown}${tail}`;
|
|
1180
|
+
const omitted = pieces.length - kept;
|
|
1181
|
+
const shown = pieces.slice(0, kept).join(separator);
|
|
1182
|
+
const tail = omitted ? `${kept ? separator : ""}${omitted} more entries in this span` : "";
|
|
1183
|
+
return `${shown}${tail}`;
|
|
1183
1184
|
};
|
|
1184
|
-
let composed = compose(
|
|
1185
|
-
for (let kept =
|
|
1185
|
+
let composed = compose(pieces.length);
|
|
1186
|
+
for (let kept = pieces.length; kept > 0 && composed.length > ACTIVE_CONTEXT_POLICY.maxBriefChars; kept -= 1) {
|
|
1186
1187
|
composed = compose(kept - 1);
|
|
1187
1188
|
}
|
|
1188
1189
|
const escapeName = (name: string) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* nothing: the next model call that reads a result, a natural boundary in the work.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { MEMORY_BODY_CHARS, MEMORY_KEY_CHARS, MEMORY_KEYS_MAX } from "./policy.ts";
|
|
11
|
+
|
|
10
12
|
type ToolHandler = (
|
|
11
13
|
toolCallId: string,
|
|
12
14
|
params: Record<string, unknown>,
|
|
@@ -29,27 +31,30 @@ export function buildActiveContextTool(input: {
|
|
|
29
31
|
handler: ToolHandler;
|
|
30
32
|
}) {
|
|
31
33
|
const peekGuidance = input.allowedActions.includes("peek")
|
|
32
|
-
? ` Peek is the point read: it returns one fold's index view --
|
|
34
|
+
? ` Peek is the point read: it returns one fold's index view -- nested folds' briefs seated shallowest first inside the byte budget with the omitted rows counted, 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
35
|
: "";
|
|
34
|
-
const correctionGuidance = input.allowedActions.includes("
|
|
35
|
-
? " Curation is correctable:
|
|
36
|
+
const correctionGuidance = input.allowedActions.includes("reboundary")
|
|
37
|
+
? " 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
38
|
: "";
|
|
37
|
-
const pinGuidance = input.allowedActions.includes("
|
|
38
|
-
? "
|
|
39
|
+
const pinGuidance = input.allowedActions.includes("pin")
|
|
40
|
+
? " 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
41
|
: "";
|
|
40
|
-
const epochGuidance = input.allowedActions.includes("
|
|
41
|
-
? "
|
|
42
|
+
const epochGuidance = input.allowedActions.includes("brief")
|
|
43
|
+
? " 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
44
|
: "";
|
|
43
45
|
const recoveryGuidance = input.allowedActions.includes("expand")
|
|
44
46
|
? " 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."
|
|
45
47
|
: "";
|
|
48
|
+
const memoryGuidance = input.allowedActions.includes("remember")
|
|
49
|
+
? ` WORKING MEMORY: a session-scoped dictionary you maintain beside the fold index. remember with key and body writes or updates an entry (${MEMORY_KEY_CHARS}-character keys, ${MEMORY_BODY_CHARS}-character bodies, ${MEMORY_KEYS_MAX} entries); an empty body removes it; recall with keys reads bodies back, or everything when keys is omitted. The bodies live outside your window: only a one-glance table of contents rides your context, refreshed at each commit, so writing an entry costs your window almost nothing. Keep entries current as the work moves: when a fact an entry records changes, or new information lands that belongs in one, update it then rather than at the end.`
|
|
50
|
+
: "";
|
|
46
51
|
const standingGuidance = " Folding is automatic, lossless and recoverable: the runtime folds stale spans on its own when the window needs room, nothing is ever discarded, and every folded span keeps its exact source. You are never asked to make room and no fold is ever announced in advance. Work normally and let it happen.";
|
|
47
52
|
return {
|
|
48
53
|
name: input.name,
|
|
49
54
|
label: input.label,
|
|
50
55
|
description: input.fullSurface
|
|
51
|
-
? `Page, peek,
|
|
52
|
-
: `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance}
|
|
56
|
+
? `Page, peek, brief, expand, refold, pin, or re-cut exact Pi active-context evidence.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${memoryGuidance}${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.`
|
|
57
|
+
: `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${memoryGuidance}${standingGuidance} Supplied fold briefs have a hard ${input.maxBriefChars}-character maximum.`,
|
|
53
58
|
parameters: {
|
|
54
59
|
type: "object",
|
|
55
60
|
additionalProperties: false,
|
|
@@ -58,35 +63,25 @@ export function buildActiveContextTool(input: {
|
|
|
58
63
|
action: { type: "string", enum: input.allowedActions },
|
|
59
64
|
ids: { type: "array", minItems: 1, maxItems: 64, items: { type: "string", minLength: 1 } },
|
|
60
65
|
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
66
|
brief: {
|
|
84
67
|
type: "string",
|
|
85
68
|
minLength: 1,
|
|
86
69
|
maxLength: input.maxBriefChars,
|
|
87
|
-
description:
|
|
70
|
+
description: "Brief only: what you will want back from this pending fold that " +
|
|
71
|
+
"its automatic brief cannot know. Your sentence is kept BESIDE the automatic " +
|
|
72
|
+
`brief, never instead of it, at most ${input.maxBriefChars} characters. Free ` +
|
|
73
|
+
"to write, because the fold it names is not in your window yet.",
|
|
88
74
|
},
|
|
89
75
|
offset: { type: "integer", minimum: 0 },
|
|
76
|
+
ephemeral: {
|
|
77
|
+
type: "boolean",
|
|
78
|
+
description: "Peek only. Default TRUE: the returned bytes ride your context " +
|
|
79
|
+
"exactly until your next message, then their place holds a one-line " +
|
|
80
|
+
"placeholder and your reply is the surviving trace, so extract what you need " +
|
|
81
|
+
"into that reply. Peeking again is lossless. Pass false to keep the result " +
|
|
82
|
+
"standing like any other tool result, which is what you want when the bytes " +
|
|
83
|
+
"have to survive several turns of work.",
|
|
84
|
+
},
|
|
90
85
|
bytes: {
|
|
91
86
|
type: "integer",
|
|
92
87
|
minimum: input.minPeekSliceBytes,
|
|
@@ -94,6 +89,31 @@ export function buildActiveContextTool(input: {
|
|
|
94
89
|
},
|
|
95
90
|
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
96
91
|
detail: { type: "string", enum: [...input.statusDetails] },
|
|
92
|
+
...(input.allowedActions.includes("remember")
|
|
93
|
+
? {
|
|
94
|
+
key: {
|
|
95
|
+
type: "string",
|
|
96
|
+
minLength: 1,
|
|
97
|
+
maxLength: MEMORY_KEY_CHARS,
|
|
98
|
+
description: "remember only: the working-memory entry to write, update or " +
|
|
99
|
+
"(with an empty body) remove.",
|
|
100
|
+
},
|
|
101
|
+
body: {
|
|
102
|
+
type: "string",
|
|
103
|
+
maxLength: MEMORY_BODY_CHARS,
|
|
104
|
+
description: "remember only: the entry's full text, replacing what the key " +
|
|
105
|
+
"held. Empty removes the entry.",
|
|
106
|
+
},
|
|
107
|
+
keys: {
|
|
108
|
+
type: "array",
|
|
109
|
+
minItems: 1,
|
|
110
|
+
maxItems: MEMORY_KEYS_MAX,
|
|
111
|
+
items: { type: "string", minLength: 1 },
|
|
112
|
+
description: "recall only: the working-memory entries to read. Omit to read " +
|
|
113
|
+
"every entry.",
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
97
117
|
},
|
|
98
118
|
},
|
|
99
119
|
execute: input.handler,
|
|
@@ -103,8 +123,10 @@ export function buildActiveContextTool(input: {
|
|
|
103
123
|
export function buildActiveContextCommands(input: {
|
|
104
124
|
statusName: string;
|
|
105
125
|
foldName: string;
|
|
126
|
+
editorName: string;
|
|
106
127
|
statusHandler: CommandHandler;
|
|
107
128
|
foldHandler: CommandHandler;
|
|
129
|
+
editorHandler: CommandHandler;
|
|
108
130
|
}) {
|
|
109
131
|
return [
|
|
110
132
|
{
|
|
@@ -114,9 +136,14 @@ export function buildActiveContextCommands(input: {
|
|
|
114
136
|
},
|
|
115
137
|
{
|
|
116
138
|
name: input.foldName,
|
|
117
|
-
description: "
|
|
139
|
+
description: "Commit every staged fold in one epoch; with explicit ids, folds exactly that span now; " +
|
|
118
140
|
"works without a main-model request",
|
|
119
141
|
handler: input.foldHandler,
|
|
120
142
|
},
|
|
143
|
+
{
|
|
144
|
+
name: input.editorName,
|
|
145
|
+
description: "Open the context window editor: occupancy, fold roots and briefs, staged marks, pins",
|
|
146
|
+
handler: input.editorHandler,
|
|
147
|
+
},
|
|
121
148
|
];
|
|
122
149
|
}
|