pi-fold 0.1.0 → 1.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 +97 -7
- package/extensions/active-context.ts +2252 -182
- package/extensions/lib/advisory.ts +171 -14
- package/extensions/lib/canonical.ts +20 -0
- package/extensions/lib/curation.ts +437 -0
- package/extensions/lib/folding.ts +468 -22
- package/extensions/lib/instrumentation.ts +333 -0
- package/extensions/lib/measurement.ts +111 -0
- package/extensions/lib/persistence.ts +158 -2
- package/extensions/lib/policy.ts +380 -4
- package/extensions/lib/scheduling.ts +923 -0
- package/extensions/lib/selection.ts +437 -12
- package/extensions/lib/surfacing.ts +349 -0
- package/extensions/lib/tool-surface.ts +52 -3
- package/extensions/lib/transcript.ts +58 -4
- package/package.json +2 -2
|
@@ -9,8 +9,15 @@ import type {
|
|
|
9
9
|
ActiveContextSnapshot,
|
|
10
10
|
ActiveContextState,
|
|
11
11
|
AdvisoryMilestone,
|
|
12
|
+
GuidanceProfile,
|
|
13
|
+
SurfacingSuggestion,
|
|
14
|
+
} from "./policy.ts";
|
|
15
|
+
import {
|
|
16
|
+
contextBrand,
|
|
17
|
+
DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
18
|
+
DEFAULT_GUIDANCE_PROFILE,
|
|
19
|
+
GUIDANCE_PROFILES,
|
|
12
20
|
} from "./policy.ts";
|
|
13
|
-
import { contextBrand, DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN } from "./policy.ts";
|
|
14
21
|
|
|
15
22
|
export { ADVISORY_BUDGETS };
|
|
16
23
|
|
|
@@ -35,18 +42,41 @@ export function clearArmedAdvisory(state: ActiveContextState): ActiveContextStat
|
|
|
35
42
|
return { ...state, advisory };
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
export function
|
|
45
|
+
export function normalizeGuidanceProfile(guidance: unknown = DEFAULT_GUIDANCE_PROFILE): GuidanceProfile {
|
|
46
|
+
if (!GUIDANCE_PROFILES.includes(guidance as GuidanceProfile)) {
|
|
47
|
+
throw new Error('guidance must be "pressure", "curation", or "minimal"');
|
|
48
|
+
}
|
|
49
|
+
return guidance as GuidanceProfile;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function guidanceRungs(
|
|
39
53
|
snapshot: Pick<ActiveContextSnapshot, "policy" | "contextWindow">,
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
guidance: GuidanceProfile,
|
|
55
|
+
): AdvisorySchedule["rungs"] {
|
|
56
|
+
const urgent = {
|
|
57
|
+
milestone: "urgent" as const,
|
|
58
|
+
threshold: hardFenceRatio(snapshot) - 0.03,
|
|
59
|
+
budget: ADVISORY_BUDGETS.urgent,
|
|
60
|
+
};
|
|
61
|
+
if (guidance === "minimal") return [urgent];
|
|
62
|
+
return [
|
|
63
|
+
...(guidance === "curation"
|
|
64
|
+
? [{ milestone: "orientation" as const, threshold: 0.25, budget: ADVISORY_BUDGETS.orientation }]
|
|
65
|
+
: []),
|
|
42
66
|
{ milestone: "notice" as const, threshold: 0.50, budget: ADVISORY_BUDGETS.notice },
|
|
43
67
|
{ milestone: "tools" as const, threshold: snapshot.policy.toolFoldRatio - 0.04,
|
|
44
68
|
budget: ADVISORY_BUDGETS.tools },
|
|
45
69
|
{ milestone: "chapters" as const, threshold: snapshot.policy.prepareRatio - 0.05,
|
|
46
70
|
budget: ADVISORY_BUDGETS.chapters },
|
|
47
|
-
|
|
48
|
-
budget: ADVISORY_BUDGETS.urgent },
|
|
71
|
+
urgent,
|
|
49
72
|
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function advisorySchedule(
|
|
76
|
+
snapshot: Pick<ActiveContextSnapshot, "policy" | "contextWindow">,
|
|
77
|
+
guidance: GuidanceProfile = DEFAULT_GUIDANCE_PROFILE,
|
|
78
|
+
): AdvisorySchedule {
|
|
79
|
+
const rungs = guidanceRungs(snapshot, guidance);
|
|
50
80
|
for (let index = rungs.length - 2; index >= 0; index -= 1) {
|
|
51
81
|
rungs[index].threshold = Math.min(rungs[index].threshold, rungs[index + 1].threshold - 0.02);
|
|
52
82
|
}
|
|
@@ -57,12 +87,36 @@ export function advisorySchedule(
|
|
|
57
87
|
};
|
|
58
88
|
}
|
|
59
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Record that an armed advisory actually REACHED the agent.
|
|
92
|
+
*
|
|
93
|
+
* The budget used to be spent when a milestone armed. Any automatic fold in the same
|
|
94
|
+
* context pass then cleared the arm before the projection was built, so a working
|
|
95
|
+
* ladder drained every milestone budget without ever emitting a word. Measured
|
|
96
|
+
* 2026-08-06 on an identical pressure schedule: an idle-ladder session delivered 7
|
|
97
|
+
* advisories, and a tool-using session delivered 0 while reporting notice, tools and
|
|
98
|
+
* chapters each "delivered" once. All four instrumented runs were the second case.
|
|
99
|
+
*/
|
|
100
|
+
export function recordAdvisoryDelivery(
|
|
101
|
+
state: ActiveContextState,
|
|
102
|
+
milestone: AdvisoryMilestone,
|
|
103
|
+
): ActiveContextState {
|
|
104
|
+
const current = advisoryState(state);
|
|
105
|
+
const delivered = { ...current.delivered };
|
|
106
|
+
delivered[milestone] = (delivered[milestone] ?? 0) + 1;
|
|
107
|
+
const advisory = { ...current, delivered };
|
|
108
|
+
delete advisory.armed;
|
|
109
|
+
return { ...state, advisory };
|
|
110
|
+
}
|
|
111
|
+
|
|
60
112
|
export function updateAdvisoryMilestone(
|
|
61
113
|
currentState: ActiveContextState,
|
|
62
114
|
ratio: number,
|
|
63
115
|
schedule: AdvisorySchedule,
|
|
64
116
|
scheduleChanged: boolean,
|
|
65
117
|
scheduleKey: string,
|
|
118
|
+
/** Spend the milestone budget on DELIVERY rather than on arming. */
|
|
119
|
+
countOnDelivery = false,
|
|
66
120
|
): { state: ActiveContextState; milestone: AdvisoryMilestone | null } {
|
|
67
121
|
const current = advisoryState(currentState);
|
|
68
122
|
if (scheduleChanged) {
|
|
@@ -78,12 +132,28 @@ export function updateAdvisoryMilestone(
|
|
|
78
132
|
highWater = Math.min(highWater, index > 0 ? schedule.rungs[index - 1].threshold : 0);
|
|
79
133
|
}
|
|
80
134
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
135
|
+
let selected: AdvisorySchedule["rungs"][number] | null;
|
|
136
|
+
if (countOnDelivery) {
|
|
137
|
+
// Only ever the highest rung the current pressure has reached, so a plateau does
|
|
138
|
+
// not replay a descending cascade of stale milestones. A rung that has never been
|
|
139
|
+
// DELIVERED stays armable even after the high-water mark has passed it: the old
|
|
140
|
+
// ratchet locked out every rung that armed once and was cleared before it spoke.
|
|
141
|
+
const reached = schedule.rungs.filter((rung) => ratio >= rung.threshold).at(-1) ?? null;
|
|
142
|
+
const deliveries = reached ? current.delivered[reached.milestone] ?? 0 : 0;
|
|
143
|
+
selected = reached && deliveries < reached.budget &&
|
|
144
|
+
(highWater < reached.threshold || deliveries === 0)
|
|
145
|
+
? reached
|
|
146
|
+
: null;
|
|
147
|
+
} else {
|
|
148
|
+
const crossed = schedule.rungs.filter((rung) =>
|
|
149
|
+
highWater < rung.threshold && ratio >= rung.threshold &&
|
|
150
|
+
(current.delivered[rung.milestone] ?? 0) < rung.budget);
|
|
151
|
+
selected = crossed.at(-1) ?? null;
|
|
152
|
+
}
|
|
85
153
|
const delivered = { ...current.delivered };
|
|
86
|
-
if (selected
|
|
154
|
+
if (selected && !countOnDelivery) {
|
|
155
|
+
delivered[selected.milestone] = (delivered[selected.milestone] ?? 0) + 1;
|
|
156
|
+
}
|
|
87
157
|
const armed = selected
|
|
88
158
|
? { milestone: selected.milestone, threshold: selected.threshold, scheduleKey }
|
|
89
159
|
: current.armed;
|
|
@@ -100,15 +170,52 @@ export function updateAdvisoryMilestone(
|
|
|
100
170
|
};
|
|
101
171
|
}
|
|
102
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Wording rule for every dosage below: report what is HAPPENING and name the actions
|
|
175
|
+
* that change it. The earlier phrasing asked the agent to fold proactively, which is
|
|
176
|
+
* advice it can only act on by interrupting its own task; the runtime folds either way,
|
|
177
|
+
* so the honest thing to say is what it will do and what reacting buys.
|
|
178
|
+
*/
|
|
179
|
+
function curationText(milestone: AdvisoryMilestone, percent: number, toolName: string): string {
|
|
180
|
+
if (milestone === "orientation") {
|
|
181
|
+
return "Collapsed folds are a browsable index of the work behind you, and their briefs sit in the cached " +
|
|
182
|
+
`prefix of the window, so paging that index costs almost nothing. Page it with ${toolName} ` +
|
|
183
|
+
`{"action":"status"} and expand what the current task needs with ${toolName} ` +
|
|
184
|
+
'{"action":"expand","id":"<fold-id>"}.';
|
|
185
|
+
}
|
|
186
|
+
if (milestone === "notice") {
|
|
187
|
+
return `Context is ${percent}% full, and the ladder is folding stale spans as it fills. Spans you fold ` +
|
|
188
|
+
`yourself carry your brief instead of a generated one: ${toolName} ` +
|
|
189
|
+
'{"action":"fold","marks":[{"ids":["<start>","<end>"],"brief":"<factual brief>"}]} batches several at ' +
|
|
190
|
+
`once, and ${toolName} {"action":"protect","ids":["<entry-id>"]} keeps what must stay raw out of every ` +
|
|
191
|
+
"fold. Continuing the task is the default; nothing here needs a reply.";
|
|
192
|
+
}
|
|
193
|
+
if (milestone === "tools") {
|
|
194
|
+
return "Completed read-only tool batches are what the ladder reclaims first, and their endpoint ids are in " +
|
|
195
|
+
"the live advisory. A batch it folds keeps a generated brief; one you fold keeps yours. Either way the " +
|
|
196
|
+
`exact entries come back with ${toolName} {"action":"expand","id":"<fold-id>"}, and a brief that does ` +
|
|
197
|
+
`not describe what you needed is corrected with ${toolName} ` +
|
|
198
|
+
'{"action":"rebrief","id":"<fold-id>","brief":"<factual brief>"}.';
|
|
199
|
+
}
|
|
200
|
+
return "Adjacent folds of finished work are the next thing the ladder consolidates. Handing two or more to " +
|
|
201
|
+
`${toolName} {"action":"fold","ids":["<fold-id>","<fold-id>"],"brief":"<factual brief>"} nests them under ` +
|
|
202
|
+
"a brief you wrote, leaving the oldest material deepest and still exactly recoverable; a boundary that " +
|
|
203
|
+
`came out wrong is re-cut with ${toolName} {"action":"reboundary","ids":["<start>","<end>"]}.`;
|
|
204
|
+
}
|
|
205
|
+
|
|
103
206
|
export function milestoneText(
|
|
104
207
|
milestone: AdvisoryMilestone,
|
|
105
208
|
sessionId: string,
|
|
106
209
|
threshold: number,
|
|
107
210
|
toolName: string,
|
|
108
211
|
brandNoun = DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
212
|
+
guidance: GuidanceProfile = DEFAULT_GUIDANCE_PROFILE,
|
|
109
213
|
): string {
|
|
110
214
|
const percent = Math.round(threshold * 100);
|
|
111
215
|
const prefix = `[${contextBrand(brandNoun)} milestone ${milestone}; session ${sessionId.slice(0, 16)}]`;
|
|
216
|
+
if (guidance === "curation" && milestone !== "urgent") {
|
|
217
|
+
return `${prefix} ${curationText(milestone, percent, toolName)}`;
|
|
218
|
+
}
|
|
112
219
|
if (milestone === "notice") {
|
|
113
220
|
return `${prefix} Context pressure has crossed ${percent}%. Automatic folding is available. ` +
|
|
114
221
|
`Inspect candidates exactly with ${toolName} {"action":"status"}.`;
|
|
@@ -119,13 +226,55 @@ export function milestoneText(
|
|
|
119
226
|
}
|
|
120
227
|
if (milestone === "chapters") {
|
|
121
228
|
return `${prefix} The chapter preparation rung begins at ${percent}%. ` +
|
|
122
|
-
`
|
|
123
|
-
|
|
229
|
+
`Closed chapters fold from the eligibleChapter endpoints in ${toolName} {"action":"status"}; ` +
|
|
230
|
+
`${toolName} {"action":"fold","ids":["<start>","<end>"],"brief":"<factual brief>"} folds one with ` +
|
|
231
|
+
"your own brief instead of a generated one.";
|
|
124
232
|
}
|
|
125
233
|
return `${prefix} The hard context fence is near. The next automatic action is a committed chapter fold ` +
|
|
126
234
|
"or the provider request is aborted before transmission.";
|
|
127
235
|
}
|
|
128
236
|
|
|
237
|
+
/**
|
|
238
|
+
* The surfacing carrier. It rides the same ephemeral tail channel as the live
|
|
239
|
+
* advisory: appended after the stable prefix, never durable, never a mutation of
|
|
240
|
+
* the projection, and cleared whenever the next context event is built.
|
|
241
|
+
*/
|
|
242
|
+
export function surfacingText(input: {
|
|
243
|
+
suggestions: readonly SurfacingSuggestion[];
|
|
244
|
+
brandNoun?: string;
|
|
245
|
+
}): string | null {
|
|
246
|
+
if (!input.suggestions.length) return null;
|
|
247
|
+
const brand = contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
|
|
248
|
+
const lines = input.suggestions.map((suggestion, index) => [
|
|
249
|
+
`${index + 1}. ${suggestion.id}${suggestion.depth ? ` (depth ${suggestion.depth})` : ""}: ${suggestion.text}`,
|
|
250
|
+
...(suggestion.alternateRoute ? [` peek (ephemeral read): ${suggestion.alternateRoute}`] : []),
|
|
251
|
+
` expand (durable restore): ${suggestion.route}`,
|
|
252
|
+
].join("\n"));
|
|
253
|
+
return boundReceiptText(
|
|
254
|
+
[
|
|
255
|
+
`[${brand} suggestions] ${input.suggestions.length} collapsed span(s) look relevant to the current ` +
|
|
256
|
+
"task. Peek reads one back ephemerally at any depth; expand restores it in place, outermost first. " +
|
|
257
|
+
"Ignoring them is a valid answer.",
|
|
258
|
+
...lines,
|
|
259
|
+
].join("\n"),
|
|
260
|
+
2_048,
|
|
261
|
+
`[${brand} suggestions] Relevant collapsed spans are unavailable this pass.`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface AdvisoryCuration {
|
|
266
|
+
/** Room left before the truthful fence, in tokens. */
|
|
267
|
+
headroomTokens: number | null;
|
|
268
|
+
budgetTokens: number;
|
|
269
|
+
pendingMarks: number;
|
|
270
|
+
eligibleMarks: number;
|
|
271
|
+
/** What the pending marks would free, in TOKENS. A percentage is not actionable. */
|
|
272
|
+
freedTokens: number;
|
|
273
|
+
eligibleFreedTokens: number;
|
|
274
|
+
/** The share of what the request currently pays for that no pending decision reclaims. */
|
|
275
|
+
unmarkedShare: number;
|
|
276
|
+
}
|
|
277
|
+
|
|
129
278
|
export function liveAdvisoryText(input: {
|
|
130
279
|
milestone: AdvisoryMilestone;
|
|
131
280
|
ratio: number;
|
|
@@ -133,6 +282,7 @@ export function liveAdvisoryText(input: {
|
|
|
133
282
|
chapterEndpoints: string[];
|
|
134
283
|
remediationCount: number;
|
|
135
284
|
brandNoun?: string;
|
|
285
|
+
curation?: AdvisoryCuration | null;
|
|
136
286
|
}): string {
|
|
137
287
|
const tools = input.toolEndpoints.length
|
|
138
288
|
? input.toolEndpoints.slice(0, 3).join(", ")
|
|
@@ -140,9 +290,16 @@ export function liveAdvisoryText(input: {
|
|
|
140
290
|
const chapter = input.chapterEndpoints.length
|
|
141
291
|
? `${input.chapterEndpoints[0]}..${input.chapterEndpoints.at(-1)}`
|
|
142
292
|
: "none";
|
|
293
|
+
const curation = input.curation
|
|
294
|
+
? `headroom ${input.curation.headroomTokens ?? "unmeasured"} of ${input.curation.budgetTokens} tokens; ` +
|
|
295
|
+
`unmarked share ${Math.round(input.curation.unmarkedShare * 100)}%; ` +
|
|
296
|
+
`${input.curation.pendingMarks} pending mark(s), ${input.curation.eligibleMarks} eligible now, ` +
|
|
297
|
+
`together freeing about ${input.curation.eligibleFreedTokens} tokens of the ` +
|
|
298
|
+
`${input.curation.freedTokens} marked; `
|
|
299
|
+
: "";
|
|
143
300
|
return boundReceiptText(
|
|
144
301
|
`[${contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN)} advisory] ` +
|
|
145
|
-
`pressure ${Math.round(input.ratio * 100)}%; milestone ${input.milestone}; ` +
|
|
302
|
+
`pressure ${Math.round(input.ratio * 100)}%; milestone ${input.milestone}; ${curation}` +
|
|
146
303
|
`eligible read-only batch endpoints: ${tools}; eligibleChapter endpoints: ${chapter}; ` +
|
|
147
304
|
`session milestone count: ${input.remediationCount}.`,
|
|
148
305
|
2_048,
|
|
@@ -15,6 +15,26 @@ export function bytes(value: unknown): number {
|
|
|
15
15
|
return Buffer.byteLength(typeof value === "string" ? value : stableStringify(value), "utf8");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export function boundedUtf8(text: string, maximumBytes: number): string {
|
|
19
|
+
const buffer = Buffer.from(text, "utf8");
|
|
20
|
+
if (buffer.length <= maximumBytes) return text;
|
|
21
|
+
// A multi-byte sequence cut by the bound decodes to one trailing replacement
|
|
22
|
+
// character; dropping it keeps the slice an exact prefix of the source.
|
|
23
|
+
return buffer.subarray(0, maximumBytes).toString("utf8").replace(/\uFFFD$/, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The suffix of `text` beginning at a byte offset. A multi-byte sequence cut by the
|
|
28
|
+
* offset decodes to one leading replacement character; dropping it keeps the slice an
|
|
29
|
+
* exact suffix of the source, which is what makes a paged peek losslessly rejoinable.
|
|
30
|
+
*/
|
|
31
|
+
export function utf8Slice(text: string, offsetBytes: number): string {
|
|
32
|
+
if (offsetBytes <= 0) return text;
|
|
33
|
+
const buffer = Buffer.from(text, "utf8");
|
|
34
|
+
if (offsetBytes >= buffer.length) return "";
|
|
35
|
+
return buffer.subarray(offsetBytes).toString("utf8").replace(/^�/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
export function ownValue(value: unknown, key: string): unknown {
|
|
19
39
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
20
40
|
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|