pi-fold 0.0.1 → 0.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/LICENSE +21 -0
- package/README.md +76 -11
- package/extensions/active-context.ts +1990 -0
- package/extensions/evidence.js +673 -0
- package/extensions/index.js +32 -0
- package/extensions/json.ts +228 -0
- package/extensions/lib/advisory.ts +152 -0
- package/extensions/lib/canonical.ts +163 -0
- package/extensions/lib/folding.ts +1012 -0
- package/extensions/lib/measurement.ts +429 -0
- package/extensions/lib/persistence.ts +773 -0
- package/extensions/lib/policy.ts +233 -0
- package/extensions/lib/selection.ts +501 -0
- package/extensions/lib/tool-surface.ts +66 -0
- package/extensions/lib/transcript.ts +479 -0
- package/extensions/summarizer.js +95 -0
- package/package.json +48 -11
- package/index.js +0 -4
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import type { EvidenceRef } from "../json.ts";
|
|
2
|
+
import {
|
|
3
|
+
denseOwnArrayValues,
|
|
4
|
+
objectRefKey,
|
|
5
|
+
} from "../json.ts";
|
|
6
|
+
import {
|
|
7
|
+
bytes,
|
|
8
|
+
contentText,
|
|
9
|
+
messageRole,
|
|
10
|
+
ownValue,
|
|
11
|
+
usefulBrief,
|
|
12
|
+
} from "./canonical.ts";
|
|
13
|
+
import {
|
|
14
|
+
exactMapped,
|
|
15
|
+
foldInterval,
|
|
16
|
+
orderedRoots,
|
|
17
|
+
refsInOrder,
|
|
18
|
+
refsProtected,
|
|
19
|
+
toolFoldCadence,
|
|
20
|
+
toolRefsProtected,
|
|
21
|
+
visibleCollapsedRoots,
|
|
22
|
+
} from "./measurement.ts";
|
|
23
|
+
import {
|
|
24
|
+
flattenFoldRefs,
|
|
25
|
+
foldMap,
|
|
26
|
+
} from "./persistence.ts";
|
|
27
|
+
import {
|
|
28
|
+
ACTIVE_CONTEXT_POLICY,
|
|
29
|
+
CONSOLIDATION_WIDTH_THRESHOLD,
|
|
30
|
+
DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
31
|
+
} from "./policy.ts";
|
|
32
|
+
import type {
|
|
33
|
+
ActiveContextSnapshot,
|
|
34
|
+
ActiveContextState,
|
|
35
|
+
ActiveFold,
|
|
36
|
+
CompleteTurn,
|
|
37
|
+
FoldCandidate,
|
|
38
|
+
FoldKind,
|
|
39
|
+
FoldPart,
|
|
40
|
+
MappedMessage,
|
|
41
|
+
} from "./policy.ts";
|
|
42
|
+
import {
|
|
43
|
+
chapterSegments,
|
|
44
|
+
scanTurnToolBatches,
|
|
45
|
+
structurallyClosedChapterUnits,
|
|
46
|
+
terminalAssistant,
|
|
47
|
+
} from "./transcript.ts";
|
|
48
|
+
import type { ChapterUnit } from "./transcript.ts";
|
|
49
|
+
|
|
50
|
+
export interface ResultCall {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
batch: string[];
|
|
54
|
+
assistantIndex: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const RESULT_CALL_INDEXES = new WeakMap<ActiveContextSnapshot, {
|
|
58
|
+
strict?: Map<number, ResultCall>;
|
|
59
|
+
consumed?: Map<number, ResultCall>;
|
|
60
|
+
}>();
|
|
61
|
+
|
|
62
|
+
export function resultCallIndex(snapshot: ActiveContextSnapshot, allowConsumedIncomplete: boolean): Map<number, ResultCall> {
|
|
63
|
+
const cached = RESULT_CALL_INDEXES.get(snapshot) ?? {};
|
|
64
|
+
const key = allowConsumedIncomplete ? "consumed" : "strict";
|
|
65
|
+
if (cached[key]) return cached[key]!;
|
|
66
|
+
const indexed = new Map<number, ResultCall>();
|
|
67
|
+
const segments: Array<{ turn: CompleteTurn; complete: boolean }> = snapshot.completeTurns.map((turn) => ({
|
|
68
|
+
turn,
|
|
69
|
+
complete: true,
|
|
70
|
+
}));
|
|
71
|
+
if (allowConsumedIncomplete) {
|
|
72
|
+
const completeStarts = new Set(snapshot.completeTurns.map((turn) => turn.start));
|
|
73
|
+
const starts = snapshot.messages.flatMap((message, index) => messageRole(message) === "user" ? [index] : []);
|
|
74
|
+
for (let cursor = 0; cursor < starts.length; cursor += 1) {
|
|
75
|
+
if (completeStarts.has(starts[cursor])) continue;
|
|
76
|
+
segments.push({
|
|
77
|
+
turn: { start: starts[cursor], end: starts[cursor + 1] ?? snapshot.messages.length },
|
|
78
|
+
complete: false,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
segments.sort((left, right) => left.turn.start - right.turn.start);
|
|
83
|
+
for (const { turn, complete } of segments) {
|
|
84
|
+
const validated = scanTurnToolBatches(
|
|
85
|
+
snapshot.messages,
|
|
86
|
+
turn,
|
|
87
|
+
allowConsumedIncomplete,
|
|
88
|
+
snapshot.toolName,
|
|
89
|
+
snapshot.readOnlyTools,
|
|
90
|
+
);
|
|
91
|
+
if (!validated) continue;
|
|
92
|
+
const batches = new Map<number, string[]>();
|
|
93
|
+
for (const call of validated.calls) {
|
|
94
|
+
const batch = batches.get(call.assistantIndex) ?? [];
|
|
95
|
+
batch.push(call.id);
|
|
96
|
+
batches.set(call.assistantIndex, batch);
|
|
97
|
+
}
|
|
98
|
+
for (const call of validated.calls) {
|
|
99
|
+
if (allowConsumedIncomplete) {
|
|
100
|
+
let laterGenerations = 0;
|
|
101
|
+
for (let index = call.resultIndex + 1; index < turn.end; index += 1) {
|
|
102
|
+
const message = snapshot.messages[index];
|
|
103
|
+
if (messageRole(message) !== "assistant") continue;
|
|
104
|
+
if (ownValue(message, "stopReason") === "toolUse") laterGenerations += 1;
|
|
105
|
+
else if (complete && terminalAssistant(message)) laterGenerations = Math.max(laterGenerations, 1);
|
|
106
|
+
}
|
|
107
|
+
if ((complete && laterGenerations < 1) || (!complete && laterGenerations < 2)) continue;
|
|
108
|
+
}
|
|
109
|
+
indexed.set(call.resultIndex, {
|
|
110
|
+
id: call.id,
|
|
111
|
+
name: call.name,
|
|
112
|
+
assistantIndex: call.assistantIndex,
|
|
113
|
+
batch: batches.get(call.assistantIndex)!,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
cached[key] = indexed;
|
|
118
|
+
RESULT_CALL_INDEXES.set(snapshot, cached);
|
|
119
|
+
return indexed;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function resultCall(
|
|
123
|
+
snapshot: ActiveContextSnapshot,
|
|
124
|
+
resultIndex: number,
|
|
125
|
+
allowConsumedIncomplete = false,
|
|
126
|
+
): ResultCall | null {
|
|
127
|
+
return resultCallIndex(snapshot, allowConsumedIncomplete).get(resultIndex) ?? null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const TOOL_CALL_ARGUMENTS = new WeakMap<ActiveContextSnapshot, Map<number, Map<string, unknown>>>();
|
|
131
|
+
|
|
132
|
+
export function toolCallArguments(snapshot: ActiveContextSnapshot, assistantIndex: number, id: string): unknown {
|
|
133
|
+
let assistants = TOOL_CALL_ARGUMENTS.get(snapshot);
|
|
134
|
+
if (!assistants) {
|
|
135
|
+
assistants = new Map();
|
|
136
|
+
TOOL_CALL_ARGUMENTS.set(snapshot, assistants);
|
|
137
|
+
}
|
|
138
|
+
let calls = assistants.get(assistantIndex);
|
|
139
|
+
if (!calls) {
|
|
140
|
+
calls = new Map();
|
|
141
|
+
const assistant = snapshot.messages[assistantIndex];
|
|
142
|
+
for (const part of denseOwnArrayValues(ownValue(assistant, "content")) ?? []) {
|
|
143
|
+
if (ownValue(part, "type") !== "toolCall") continue;
|
|
144
|
+
const callId = ownValue(part, "id");
|
|
145
|
+
if (typeof callId === "string" && callId) calls.set(callId, ownValue(part, "arguments"));
|
|
146
|
+
}
|
|
147
|
+
assistants.set(assistantIndex, calls);
|
|
148
|
+
}
|
|
149
|
+
return calls.get(id);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function automaticToolBrief(snapshot: ActiveContextSnapshot, candidate: FoldCandidate): string {
|
|
153
|
+
const refs = candidate.kind === "tool-result" && candidate.parts.every((part) => part.kind === "raw")
|
|
154
|
+
? candidate.parts.map((part) => (part as Extract<FoldPart, { kind: "raw" }>).ref)
|
|
155
|
+
: [];
|
|
156
|
+
const calls = refs.map((ref) => {
|
|
157
|
+
const item = exactMapped(snapshot, ref);
|
|
158
|
+
const call = item ? resultCall(snapshot, item.index, true) : null;
|
|
159
|
+
if (!call) throw new Error("Automatic tool brief lost its validated call");
|
|
160
|
+
return call;
|
|
161
|
+
});
|
|
162
|
+
if (!calls.length || new Set(calls.map((call) => call.assistantIndex)).size !== 1) {
|
|
163
|
+
throw new Error("Automatic tool brief crossed a validated assistant batch");
|
|
164
|
+
}
|
|
165
|
+
const first = calls[0];
|
|
166
|
+
const factualBriefValue = (value: string): string => value
|
|
167
|
+
.replace(new RegExp(snapshot.toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), "active-context service")
|
|
168
|
+
.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
169
|
+
.replace(/\s+/g, " ")
|
|
170
|
+
.trim()
|
|
171
|
+
.slice(0, 120);
|
|
172
|
+
const factualToolName = (name: string): string => name.toLowerCase() === snapshot.toolName.toLowerCase()
|
|
173
|
+
? "active-context status inspection"
|
|
174
|
+
: factualBriefValue(name);
|
|
175
|
+
const args = toolCallArguments(snapshot, first.assistantIndex, first.id);
|
|
176
|
+
const targets: string[] = [];
|
|
177
|
+
for (const key of ["path", "address", "query", "url", "action", "id"]) {
|
|
178
|
+
const value = ownValue(args, key);
|
|
179
|
+
if (typeof value === "string" && value.trim()) targets.push(`${key}=${factualBriefValue(value)}`);
|
|
180
|
+
}
|
|
181
|
+
const target = targets.length ? ` for ${targets.join(", ")}` : "";
|
|
182
|
+
if (calls.length === 1) {
|
|
183
|
+
return `Completed read-only ${factualToolName(first.name)}${target}; its exact stale output remains recoverable from this fold.`;
|
|
184
|
+
}
|
|
185
|
+
const names = [...new Set(calls.map((call) => factualToolName(call.name)))].sort().join("/");
|
|
186
|
+
return `Completed one read-only ${names} batch with ${calls.length} exact results${target}; every stale output remains recoverable from this fold.`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function deterministicConsolidationBrief(candidate: FoldCandidate, state: ActiveContextState): string {
|
|
190
|
+
const byId = foldMap(state);
|
|
191
|
+
const subjects = candidate.parts.flatMap((part) => {
|
|
192
|
+
if (part.kind !== "fold") return [];
|
|
193
|
+
const child = byId.get(part.foldId);
|
|
194
|
+
return child ? [child.brief.replace(/\s+/g, " ").trim()] : [];
|
|
195
|
+
});
|
|
196
|
+
const text = `Grouped completed context covering: ${subjects.join(" ")}`.replace(/\s+/g, " ").trim();
|
|
197
|
+
if (text.length <= ACTIVE_CONTEXT_POLICY.maxBriefChars) return text;
|
|
198
|
+
let bounded = text.slice(0, ACTIVE_CONTEXT_POLICY.maxBriefChars - 1).trimEnd();
|
|
199
|
+
const finalCode = bounded.charCodeAt(bounded.length - 1);
|
|
200
|
+
if (finalCode >= 0xd800 && finalCode <= 0xdbff) bounded = bounded.slice(0, -1);
|
|
201
|
+
return `${bounded}.`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function partsForRange(
|
|
205
|
+
snapshot: ActiveContextSnapshot,
|
|
206
|
+
state: ActiveContextState,
|
|
207
|
+
start: number,
|
|
208
|
+
end: number,
|
|
209
|
+
allowedChildKinds: ReadonlySet<FoldKind>,
|
|
210
|
+
): FoldPart[] | null {
|
|
211
|
+
const roots = orderedRoots(state, snapshot);
|
|
212
|
+
for (const root of roots) {
|
|
213
|
+
const overlaps = root.start <= end && start <= root.end;
|
|
214
|
+
if (!overlaps) continue;
|
|
215
|
+
if (root.start < start || root.end > end || !allowedChildKinds.has(root.fold.kind)) return null;
|
|
216
|
+
}
|
|
217
|
+
const childAt = new Map(roots
|
|
218
|
+
.filter((root) => root.start >= start && root.end <= end)
|
|
219
|
+
.map((root) => [root.start, root]));
|
|
220
|
+
const parts: FoldPart[] = [];
|
|
221
|
+
for (let index = start; index <= end;) {
|
|
222
|
+
const child = childAt.get(index);
|
|
223
|
+
if (child) {
|
|
224
|
+
parts.push({ kind: "fold", foldId: child.fold.id });
|
|
225
|
+
index = child.end + 1;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const ref = snapshot.mapped[index]?.ref;
|
|
229
|
+
if (!ref) return null;
|
|
230
|
+
parts.push({ kind: "raw", ref });
|
|
231
|
+
index += 1;
|
|
232
|
+
}
|
|
233
|
+
return parts;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function candidateSourceRefs(parts: FoldPart[], state: ActiveContextState): EvidenceRef[] {
|
|
237
|
+
const byId = foldMap(state);
|
|
238
|
+
return parts.flatMap((part) => {
|
|
239
|
+
if (part.kind === "raw") return [part.ref];
|
|
240
|
+
const child = byId.get(part.foldId);
|
|
241
|
+
if (!child) throw new Error(`Missing candidate child ${part.foldId}`);
|
|
242
|
+
return flattenFoldRefs(child, state);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function chapterUnits(snapshot: ActiveContextSnapshot): ChapterUnit[] {
|
|
247
|
+
return chapterSegments(snapshot.messages)
|
|
248
|
+
.flatMap((segment) => structurallyClosedChapterUnits(snapshot.messages, segment));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function selectAutomaticConsolidation(
|
|
252
|
+
snapshot: ActiveContextSnapshot,
|
|
253
|
+
state: ActiveContextState,
|
|
254
|
+
ratio: number,
|
|
255
|
+
): FoldCandidate | null {
|
|
256
|
+
const visibleRoots = visibleCollapsedRoots(state, snapshot);
|
|
257
|
+
const widthEligible = visibleRoots.length > CONSOLIDATION_WIDTH_THRESHOLD;
|
|
258
|
+
const pressureEligible = Number.isFinite(ratio) && ratio >= snapshot.policy.consolidationRatio;
|
|
259
|
+
if (!widthEligible && !pressureEligible) return null;
|
|
260
|
+
const roots = visibleRoots.filter(({ fold }) =>
|
|
261
|
+
(fold.kind === "chapter" || fold.kind === "consolidation") &&
|
|
262
|
+
!refsProtected(flattenFoldRefs(fold, state), state, snapshot));
|
|
263
|
+
const candidateFor = (selected: typeof roots): FoldCandidate | null => {
|
|
264
|
+
const parts: FoldPart[] = selected.map(({ fold }) => ({ kind: "fold", foldId: fold.id }));
|
|
265
|
+
const sourceRefs = candidateSourceRefs(parts, state);
|
|
266
|
+
return selected.length >= 2 && sourceRefs.length <= snapshot.policy.maxFoldSourceRefs
|
|
267
|
+
? { kind: "consolidation", parts, sourceRefs }
|
|
268
|
+
: null;
|
|
269
|
+
};
|
|
270
|
+
if (widthEligible && !pressureEligible) {
|
|
271
|
+
const oldest = roots.slice(0, snapshot.policy.consolidationChildren);
|
|
272
|
+
let run: typeof roots = [];
|
|
273
|
+
for (const root of oldest) {
|
|
274
|
+
if (!run.length || root.start === run.at(-1)!.end + 1) {
|
|
275
|
+
run.push(root);
|
|
276
|
+
} else {
|
|
277
|
+
if (run.length >= 2) return candidateFor(run);
|
|
278
|
+
run = [root];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return run.length >= 2 ? candidateFor(run) : null;
|
|
282
|
+
}
|
|
283
|
+
let run: typeof roots = [];
|
|
284
|
+
const finish = (): FoldCandidate | null => {
|
|
285
|
+
if (run.length < snapshot.policy.consolidationChildren) return null;
|
|
286
|
+
const selected = run.slice(0, snapshot.policy.maxConsolidationChildren);
|
|
287
|
+
const parts: FoldPart[] = selected.map(({ fold }) => ({ kind: "fold", foldId: fold.id }));
|
|
288
|
+
const sourceRefs = candidateSourceRefs(parts, state);
|
|
289
|
+
return sourceRefs.length <= snapshot.policy.maxFoldSourceRefs
|
|
290
|
+
? { kind: "consolidation", parts, sourceRefs }
|
|
291
|
+
: null;
|
|
292
|
+
};
|
|
293
|
+
for (const root of roots) {
|
|
294
|
+
if (!run.length || root.start === run.at(-1)!.end + 1) run.push(root);
|
|
295
|
+
else {
|
|
296
|
+
const candidate = finish();
|
|
297
|
+
if (candidate) return candidate;
|
|
298
|
+
run = [root];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return finish();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function selectAutomaticToolBatch(
|
|
305
|
+
snapshot: ActiveContextSnapshot,
|
|
306
|
+
state: ActiveContextState,
|
|
307
|
+
ratio: number,
|
|
308
|
+
): FoldCandidate[] {
|
|
309
|
+
if (!Number.isFinite(ratio) || ratio < snapshot.policy.toolFoldRatio) return [];
|
|
310
|
+
const owned = new Set(state.folds.flatMap((fold) => fold.parts.flatMap((part) =>
|
|
311
|
+
part.kind === "raw" ? [objectRefKey(part.ref)] : [])));
|
|
312
|
+
const groups = new Map<number, Array<{ item: MappedMessage; call: NonNullable<ReturnType<typeof resultCall>> }>>();
|
|
313
|
+
for (const item of snapshot.mapped) {
|
|
314
|
+
if (messageRole(item.message) !== "toolResult") continue;
|
|
315
|
+
const call = resultCall(snapshot, item.index, true);
|
|
316
|
+
if (!call) continue;
|
|
317
|
+
const group = groups.get(call.assistantIndex) ?? [];
|
|
318
|
+
group.push({ item, call });
|
|
319
|
+
groups.set(call.assistantIndex, group);
|
|
320
|
+
}
|
|
321
|
+
for (const group of groups.values()) {
|
|
322
|
+
const expected = group[0].call.batch;
|
|
323
|
+
const ids = new Set(group.map(({ call }) => call.id));
|
|
324
|
+
const refs = group.map(({ item }) => item.ref);
|
|
325
|
+
if (ids.size !== expected.length || expected.some((id) => !ids.has(id)) ||
|
|
326
|
+
refs.some((ref) => !ref || ref.role !== "toolResult")) continue;
|
|
327
|
+
const exactRefs = refs as EvidenceRef[];
|
|
328
|
+
if (exactRefs.length > snapshot.policy.maxFoldSourceRefs ||
|
|
329
|
+
exactRefs.some((ref) => owned.has(objectRefKey(ref))) ||
|
|
330
|
+
toolRefsProtected(exactRefs, state, snapshot) || refsInOrder(snapshot, exactRefs) === null ||
|
|
331
|
+
group.reduce((total, { item }) => total + bytes(item.message), 0) < snapshot.policy.minToolChars) continue;
|
|
332
|
+
return [{
|
|
333
|
+
kind: "tool-result",
|
|
334
|
+
parts: exactRefs.map((ref) => ({ kind: "raw", ref })),
|
|
335
|
+
sourceRefs: exactRefs,
|
|
336
|
+
}];
|
|
337
|
+
}
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function selectAutomaticRefold(
|
|
342
|
+
snapshot: ActiveContextSnapshot,
|
|
343
|
+
state: ActiveContextState,
|
|
344
|
+
ratio: number,
|
|
345
|
+
): string | null {
|
|
346
|
+
if (!Number.isFinite(ratio) || ratio < snapshot.policy.refoldRatio) return null;
|
|
347
|
+
const candidates = state.expanded.flatMap((id) => {
|
|
348
|
+
const fold = state.folds.find((item) => item.id === id);
|
|
349
|
+
const interval = fold ? foldInterval(fold, state, snapshot) : null;
|
|
350
|
+
const protectedSource = fold && (fold.kind === "tool-result"
|
|
351
|
+
? toolRefsProtected(flattenFoldRefs(fold, state), state, snapshot)
|
|
352
|
+
: refsProtected(flattenFoldRefs(fold, state), state, snapshot));
|
|
353
|
+
return fold && interval && !protectedSource && !state.leases[id] ? [{ id, ...interval }] : [];
|
|
354
|
+
}).sort((left, right) => left.end - right.end || (left.end - left.start) - (right.end - right.start));
|
|
355
|
+
return candidates[0]?.id ?? null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function selectAutomaticToolForRung(
|
|
359
|
+
snapshot: ActiveContextSnapshot,
|
|
360
|
+
state: ActiveContextState,
|
|
361
|
+
ratio: number,
|
|
362
|
+
cadenceWaived = false,
|
|
363
|
+
): FoldCandidate | null {
|
|
364
|
+
const cadenceSatisfied = state.tokensSinceToolFold >= toolFoldCadence(snapshot.contextWindow);
|
|
365
|
+
if (!cadenceWaived && (!Number.isFinite(ratio) ||
|
|
366
|
+
(ratio < snapshot.policy.toolFoldRatio && !cadenceSatisfied))) return null;
|
|
367
|
+
return selectAutomaticToolBatch(snapshot, state, 1)[0] ?? null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function resolveFoldInputIds(
|
|
371
|
+
snapshot: ActiveContextSnapshot,
|
|
372
|
+
state: ActiveContextState,
|
|
373
|
+
ids: string[],
|
|
374
|
+
): Array<{ start: number; end: number; fold?: ActiveFold }> {
|
|
375
|
+
if (!ids.length || ids.length > 64 || ids.some((id) => !id)) throw new Error("fold requires 1-64 nonempty ids");
|
|
376
|
+
const values = ids.map((id) => {
|
|
377
|
+
const fold = state.folds.find((item) => item.id === id);
|
|
378
|
+
if (fold) {
|
|
379
|
+
if (fold.parentId !== null) throw new Error(`Fold ${id} is already nested under ${fold.parentId}`);
|
|
380
|
+
const interval = foldInterval(fold, state, snapshot);
|
|
381
|
+
if (!interval) throw new Error(`Fold ${id} is not active in the Pi context event`);
|
|
382
|
+
return { ...interval, fold };
|
|
383
|
+
}
|
|
384
|
+
const item = snapshot.mapped.find((candidate) => candidate.ref?.entryId === id);
|
|
385
|
+
if (!item?.ref) throw new Error(`Unknown active-context source ${id}`);
|
|
386
|
+
return { start: item.index, end: item.index };
|
|
387
|
+
});
|
|
388
|
+
values.sort((left, right) => left.start - right.start);
|
|
389
|
+
return values;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function chapterRangeIsUnitAligned(snapshot: ActiveContextSnapshot, start: number, end: number): boolean {
|
|
393
|
+
const units = chapterUnits(snapshot).filter((unit) => unit.end > start && unit.start <= end);
|
|
394
|
+
if (!units.length || units[0].start !== start || units.at(-1)!.end !== end + 1) return false;
|
|
395
|
+
if (new Set(units.map((unit) => unit.turnStart)).size > snapshot.policy.maxChapterTurns) return false;
|
|
396
|
+
return units.every((unit, index) => index === 0 || unit.start === units[index - 1].end);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function manualFoldCandidate(
|
|
400
|
+
snapshot: ActiveContextSnapshot,
|
|
401
|
+
state: ActiveContextState,
|
|
402
|
+
ids: string[],
|
|
403
|
+
): FoldCandidate {
|
|
404
|
+
const bounded = (candidate: FoldCandidate): FoldCandidate => {
|
|
405
|
+
if (candidate.sourceRefs.length > snapshot.policy.maxFoldSourceRefs) {
|
|
406
|
+
throw new Error(`Folds may include at most ${snapshot.policy.maxFoldSourceRefs} exact source references`);
|
|
407
|
+
}
|
|
408
|
+
return candidate;
|
|
409
|
+
};
|
|
410
|
+
const selected = resolveFoldInputIds(snapshot, state, ids);
|
|
411
|
+
const start = selected[0].start;
|
|
412
|
+
const end = selected.at(-1)!.end;
|
|
413
|
+
const one = selected.length === 1 ? selected[0] : null;
|
|
414
|
+
if (selected.every((item) => !item.fold && snapshot.mapped[item.start].ref?.role === "toolResult")) {
|
|
415
|
+
const refs = selected.map((item) => snapshot.mapped[item.start].ref!);
|
|
416
|
+
const calls = selected.map((item) => resultCall(snapshot, item.start, true));
|
|
417
|
+
const first = calls[0];
|
|
418
|
+
const completeBatch = first && calls.every((call) => call && call.assistantIndex === first.assistantIndex) &&
|
|
419
|
+
calls.length === first.batch.length &&
|
|
420
|
+
new Set(calls.map((call) => call!.id)).size === first.batch.length &&
|
|
421
|
+
first.batch.every((id) => calls.some((call) => call!.id === id));
|
|
422
|
+
if (completeBatch && !toolRefsProtected(refs, state, snapshot)) {
|
|
423
|
+
return bounded({ kind: "tool-result", parts: refs.map((ref) => ({ kind: "raw", ref })), sourceRefs: refs });
|
|
424
|
+
}
|
|
425
|
+
if (one && first && first.batch.length === 1 && !toolRefsProtected(refs, state, snapshot)) {
|
|
426
|
+
return bounded({ kind: "tool-result", parts: [{ kind: "raw", ref: refs[0] }], sourceRefs: refs });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const exactFolds = selected.every((item) => item.fold && item.fold.kind !== "tool-result") &&
|
|
430
|
+
selected.every((item, index) => index === 0 || item.start === selected[index - 1].end + 1);
|
|
431
|
+
if (exactFolds && selected.length >= 2) {
|
|
432
|
+
const parts: FoldPart[] = selected.map((item) => ({ kind: "fold", foldId: item.fold!.id }));
|
|
433
|
+
const refs = candidateSourceRefs(parts, state);
|
|
434
|
+
if (refsProtected(refs, state, snapshot)) throw new Error("Manual consolidation contains protected evidence");
|
|
435
|
+
return bounded({ kind: "consolidation", parts, sourceRefs: refs });
|
|
436
|
+
}
|
|
437
|
+
if (!chapterRangeIsUnitAligned(snapshot, start, end)) {
|
|
438
|
+
throw new Error("Chapter folds must align to a contiguous structurally closed user/assistant/tool-batch range");
|
|
439
|
+
}
|
|
440
|
+
const parts = partsForRange(snapshot, state, start, end, new Set<FoldKind>(["tool-result"]));
|
|
441
|
+
if (!parts) throw new Error("Chapter fold would partially overlap or swallow an existing chapter");
|
|
442
|
+
const refs = candidateSourceRefs(parts, state);
|
|
443
|
+
if (refsProtected(refs, state, snapshot)) throw new Error("Manual chapter contains fresh, unfinished, unmatched, unmapped, or protected evidence");
|
|
444
|
+
return bounded({ kind: "chapter", parts, sourceRefs: refs });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function oneLine(value: string, maximum: number): string {
|
|
448
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
449
|
+
if (text.length <= maximum) return text;
|
|
450
|
+
let bounded = text.slice(0, maximum).trimEnd();
|
|
451
|
+
const finalCode = bounded.charCodeAt(bounded.length - 1);
|
|
452
|
+
if (finalCode >= 0xd800 && finalCode <= 0xdbff) bounded = bounded.slice(0, -1);
|
|
453
|
+
return bounded;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function deterministicChapterBrief(
|
|
457
|
+
refs: EvidenceRef[],
|
|
458
|
+
messages: unknown[],
|
|
459
|
+
toolName = DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
460
|
+
): string {
|
|
461
|
+
if (refs.length !== messages.length || !refs.length) {
|
|
462
|
+
throw new Error("Deterministic chapter brief requires aligned exact evidence");
|
|
463
|
+
}
|
|
464
|
+
const firstUser = messages.find((message) => messageRole(message) === "user");
|
|
465
|
+
const firstAssistant = messages.find((message) =>
|
|
466
|
+
messageRole(message) === "assistant" && contentText(message).trim());
|
|
467
|
+
const toolCounts = new Map<string, number>();
|
|
468
|
+
for (const message of messages) {
|
|
469
|
+
if (messageRole(message) !== "assistant") continue;
|
|
470
|
+
for (const part of denseOwnArrayValues(ownValue(message, "content")) ?? []) {
|
|
471
|
+
if (ownValue(part, "type") !== "toolCall") continue;
|
|
472
|
+
const name = ownValue(part, "name");
|
|
473
|
+
if (typeof name === "string" && name) toolCounts.set(name, (toolCounts.get(name) ?? 0) + 1);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const ask = oneLine(firstUser ? contentText(firstUser) : "No user ask in this span", 90);
|
|
477
|
+
const assistant = oneLine(firstAssistant ? contentText(firstAssistant).split(/\r?\n/)[0] :
|
|
478
|
+
"No assistant text in this span", 110);
|
|
479
|
+
const tools = oneLine([...toolCounts.entries()]
|
|
480
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
481
|
+
.map(([name, count]) => `${count}×${name}`)
|
|
482
|
+
.join(" ") || "no tools", 500);
|
|
483
|
+
const escapeName = (name: string) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
484
|
+
const composed = `User: ${ask} · Tools: ${tools} · Assistant: ${assistant}`
|
|
485
|
+
.replace(new RegExp(escapeName(toolName), "gi"), "active-context service");
|
|
486
|
+
if (usefulBrief(composed, ACTIVE_CONTEXT_POLICY.maxBriefChars, toolName)) return composed;
|
|
487
|
+
// Constant floor-of-the-floor: provably passes usefulBrief (no tool name, no structural pattern).
|
|
488
|
+
return `Folded ${refs.length} exact messages from this span's complete turns.`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function deterministicChapterCandidateBrief(
|
|
492
|
+
snapshot: ActiveContextSnapshot,
|
|
493
|
+
candidate: FoldCandidate,
|
|
494
|
+
): string {
|
|
495
|
+
const messages = candidate.sourceRefs.map((ref) => {
|
|
496
|
+
const item = exactMapped(snapshot, ref);
|
|
497
|
+
if (!item) throw new Error(`Exact Pi evidence drift for ${ref.entryId}`);
|
|
498
|
+
return item.message;
|
|
499
|
+
});
|
|
500
|
+
return deterministicChapterBrief(candidate.sourceRefs, messages, snapshot.toolName);
|
|
501
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
type ToolHandler = (
|
|
2
|
+
toolCallId: string,
|
|
3
|
+
params: Record<string, unknown>,
|
|
4
|
+
signal: AbortSignal,
|
|
5
|
+
onUpdate: unknown,
|
|
6
|
+
ctx: any,
|
|
7
|
+
) => Promise<unknown>;
|
|
8
|
+
|
|
9
|
+
type CommandHandler = (args: string, ctx: any) => Promise<void> | void;
|
|
10
|
+
|
|
11
|
+
export function buildActiveContextTool(input: {
|
|
12
|
+
name: string;
|
|
13
|
+
label: string;
|
|
14
|
+
allowedActions: readonly string[];
|
|
15
|
+
fullSurface: boolean;
|
|
16
|
+
maxBriefChars: number;
|
|
17
|
+
handler: ToolHandler;
|
|
18
|
+
}) {
|
|
19
|
+
return {
|
|
20
|
+
name: input.name,
|
|
21
|
+
label: input.label,
|
|
22
|
+
description: input.fullSurface
|
|
23
|
+
? "Page, fold, expand, refold, or protect exact Pi active-context evidence. 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 1200-character maximum."
|
|
24
|
+
: `Use only the configured active-context actions: ${input.allowedActions.join(", ")}. Call fold only by copying the exact eligibleChapter.action returned by status; if status has no eligibleChapter, continue the task without folding. Supplied fold briefs have a hard 1200-character maximum.`,
|
|
25
|
+
parameters: {
|
|
26
|
+
type: "object",
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
required: ["action"],
|
|
29
|
+
properties: {
|
|
30
|
+
action: { type: "string", enum: input.allowedActions },
|
|
31
|
+
ids: { type: "array", minItems: 1, maxItems: 64, items: { type: "string", minLength: 1 } },
|
|
32
|
+
id: { type: "string", minLength: 1 },
|
|
33
|
+
brief: {
|
|
34
|
+
type: "string",
|
|
35
|
+
minLength: 1,
|
|
36
|
+
maxLength: input.maxBriefChars,
|
|
37
|
+
description: "Factual fold brief; keep it at most 1000 characters to stay below the hard 1200-character limit.",
|
|
38
|
+
},
|
|
39
|
+
offset: { type: "integer", minimum: 0 },
|
|
40
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
41
|
+
detail: { type: "string", enum: ["fold_candidates"] },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
execute: input.handler,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildActiveContextCommands(input: {
|
|
49
|
+
statusName: string;
|
|
50
|
+
foldName: string;
|
|
51
|
+
statusHandler: CommandHandler;
|
|
52
|
+
foldHandler: CommandHandler;
|
|
53
|
+
}) {
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
name: input.statusName,
|
|
57
|
+
description: "Show active-context fold roots and paging state",
|
|
58
|
+
handler: input.statusHandler,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: input.foldName,
|
|
62
|
+
description: "Losslessly fold a stale context span; works without a main-model request",
|
|
63
|
+
handler: input.foldHandler,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
}
|