pi-fold 1.0.3 → 2.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 +34 -8
- package/extensions/active-context.ts +255 -2331
- package/extensions/evidence.js +0 -10
- package/extensions/index.js +10 -30
- package/extensions/json.ts +3 -29
- package/extensions/lib/canonical.ts +0 -8
- package/extensions/lib/curation.ts +1 -258
- package/extensions/lib/folding.ts +33 -406
- package/extensions/lib/instrumentation.ts +3 -124
- package/extensions/lib/measurement.ts +0 -71
- package/extensions/lib/persistence.ts +216 -197
- package/extensions/lib/policy.ts +29 -558
- package/extensions/lib/rollback.ts +0 -53
- package/extensions/lib/scheduling.ts +36 -367
- package/extensions/lib/selection.ts +206 -312
- package/extensions/lib/tool-surface.ts +5 -18
- package/extensions/lib/transcript.ts +32 -108
- package/package.json +1 -1
- package/extensions/lib/surfacing.ts +0 -604
- package/extensions/summarizer.js +0 -291
package/extensions/summarizer.js
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
const loadPiHost = () => import("@earendil-works/pi-coding-agent");
|
|
2
|
-
|
|
3
|
-
function normalizeSummarizer(summarizer) {
|
|
4
|
-
// "deterministic" was a way to select the FAILURE PATH on purpose. Every summarizer
|
|
5
|
-
// failure already falls back to the deterministic brief, so the value bought nothing a
|
|
6
|
-
// failure does not, while reading as a third generator a deployment had to choose
|
|
7
|
-
// between. The generator stays; the value that named it does not.
|
|
8
|
-
if (summarizer === "deterministic") {
|
|
9
|
-
throw new Error('summarizer has no "deterministic" value: the deterministic brief is the ' +
|
|
10
|
-
"automatic fallback whenever a summarizer fails, not a mode to select");
|
|
11
|
-
}
|
|
12
|
-
if (summarizer === "session") return summarizer;
|
|
13
|
-
if (!summarizer || typeof summarizer !== "object" || Array.isArray(summarizer) ||
|
|
14
|
-
typeof summarizer.provider !== "string" || !summarizer.provider ||
|
|
15
|
-
typeof summarizer.model !== "string" || !summarizer.model ||
|
|
16
|
-
(summarizer.effort !== undefined &&
|
|
17
|
-
(typeof summarizer.effort !== "string" || !summarizer.effort))) {
|
|
18
|
-
throw new Error(
|
|
19
|
-
'summarizer must be "session" or an object with nonempty provider and model strings',
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
provider: summarizer.provider,
|
|
24
|
-
model: summarizer.model,
|
|
25
|
-
...(summarizer.effort === undefined ? {} : { effort: summarizer.effort }),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* The runtime hands the generator two BOUNDED orientation slices beside the span: up to
|
|
31
|
-
* two messages and 12,000 characters either side, hashed into the fold identity. They are
|
|
32
|
-
* labelled distinctly here rather than pasted in, because a model that cannot tell the
|
|
33
|
-
* span from its surroundings briefs the surroundings. An absent slice arrives as the
|
|
34
|
-
* empty-orientation literal, and a label with "[]" under it reads as content.
|
|
35
|
-
*/
|
|
36
|
-
const EMPTY_ORIENTATION = "[]";
|
|
37
|
-
|
|
38
|
-
function orientationBlock(label, text) {
|
|
39
|
-
return typeof text === "string" && text && text !== EMPTY_ORIENTATION
|
|
40
|
-
? `${label}:\n${text}`
|
|
41
|
-
: `${label}: none.`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* A brief does two jobs at once, so the request says both: it summarizes the span, and it
|
|
46
|
-
* tells the agent what expanding or peeking that fold would return, which is the whole
|
|
47
|
-
* basis on which the agent decides to dig back in. Naming the concrete contents is what
|
|
48
|
-
* makes the second job possible: an abstract description of a span is not a statement of
|
|
49
|
-
* what is recoverable from it.
|
|
50
|
-
*/
|
|
51
|
-
/**
|
|
52
|
-
* A GROUP is briefed under one extra rule: every child gets a share.
|
|
53
|
-
*
|
|
54
|
-
* A parent is what the ladder builds when folds pile up, and its brief is the only index
|
|
55
|
-
* of what the group holds. A brief that covers the first two children richly and drops the
|
|
56
|
-
* other eight closes them: the agent has no way to know the eighth is in there, so it never
|
|
57
|
-
* expands to find out. Coverage beats depth at this rung, and the children are numbered in
|
|
58
|
-
* the payload so "every one of them" is a countable instruction rather than a wish.
|
|
59
|
-
*/
|
|
60
|
-
function groupCoverageRule(children) {
|
|
61
|
-
return `The span is a GROUP of ${children} folded children, numbered 1 to ${children} ` +
|
|
62
|
-
"below, each with its own brief and then its contents; folds nested inside a child " +
|
|
63
|
-
"stay folded and appear as placeholders, and a child marked collapsed shows only its " +
|
|
64
|
-
"brief. Your brief is the only index of this group, so give EVERY one of the " +
|
|
65
|
-
`${children} children a share of it: name what each one holds in the order they ` +
|
|
66
|
-
"appear. A child you leave out is a child the agent cannot know to look for. Prefer " +
|
|
67
|
-
"one concrete clause per child over a full account of the first few.";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** The marker a batched answer separates its briefs with. On its own line, so a brief that
|
|
71
|
-
* happens to mention one cannot split itself. */
|
|
72
|
-
const BATCH_MARKER = (ordinal) => `=== BRIEF ${ordinal} ===`;
|
|
73
|
-
const BATCH_MARKER_PATTERN = /^===[ \t]*BRIEF[ \t]+(\d+)[ \t]*===[ \t]*$/;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Many spans, one call.
|
|
77
|
-
*
|
|
78
|
-
* A commit folds a dozen spans at once and the lane used to send a dozen separate requests,
|
|
79
|
-
* two at a time, so most folds never reached a generator at all: rep 2 of sol-20260811
|
|
80
|
-
* committed 87 folds and applied 14 model briefs. Batching is not only cheaper per brief,
|
|
81
|
-
* it is what makes coverage reachable. It also buys something a single-span call cannot
|
|
82
|
-
* have: the spans of one commit are consecutive, so each one is the orientation for the
|
|
83
|
-
* next, and a brief can say this continues the work two spans back because the model can
|
|
84
|
-
* see that it does. Orientation is therefore given ONCE around the whole run rather than
|
|
85
|
-
* per span, which is both smaller and truer.
|
|
86
|
-
*/
|
|
87
|
-
function batchBriefRequestPrompt(request) {
|
|
88
|
-
const spans = request.spans;
|
|
89
|
-
const bodies = spans.map((span, at) => [
|
|
90
|
-
`SPAN ${at + 1} OF ${spans.length}`,
|
|
91
|
-
...(Number.isInteger(span.children) && span.children > 1
|
|
92
|
-
? [groupCoverageRule(span.children)]
|
|
93
|
-
: []),
|
|
94
|
-
span.sourceText,
|
|
95
|
-
].join("\n"));
|
|
96
|
-
return [
|
|
97
|
-
`Write ${spans.length} factual briefs, one for each of the ${spans.length} numbered ` +
|
|
98
|
-
"SPANS below. Each brief covers its own span and nothing else, and each is at most " +
|
|
99
|
-
`${request.maxBriefChars} characters. A brief does two jobs at once: it summarizes what ` +
|
|
100
|
-
"its span contains, and it tells an agent what it would get back by expanding or " +
|
|
101
|
-
"peeking that fold later, so the agent can decide whether to dig back in. Once a span " +
|
|
102
|
-
"is folded its brief is the only visible trace, so name the concrete things inside it: " +
|
|
103
|
-
"files, identifiers, decisions, results, errors. Do not describe a span abstractly. " +
|
|
104
|
-
"The spans are consecutive, so where one continues the work of an earlier one you may " +
|
|
105
|
-
"say so by number; do not let that replace naming what is actually in the span. The " +
|
|
106
|
-
"BEFORE and AFTER sections are orientation only: they say where the whole run of spans " +
|
|
107
|
-
"sits in the larger conversation, and their content belongs to no brief. Use no " +
|
|
108
|
-
"preamble and no Markdown headers.\n\n" +
|
|
109
|
-
`Answer with exactly ${spans.length} sections and nothing else. Begin each section ` +
|
|
110
|
-
`with its marker alone on a line: ${BATCH_MARKER(1)} then the first brief, ` +
|
|
111
|
-
`${BATCH_MARKER(2)} then the second, and so on through ` +
|
|
112
|
-
`${BATCH_MARKER(spans.length)}.`,
|
|
113
|
-
orientationBlock("BEFORE THE SPANS (orientation only, do not brief)", request.beforeText),
|
|
114
|
-
bodies.join("\n\n"),
|
|
115
|
-
orientationBlock("AFTER THE SPANS (orientation only, do not brief)", request.afterText),
|
|
116
|
-
...(typeof request.cure === "string" && request.cure
|
|
117
|
-
? [`YOUR PREVIOUS ANSWER DID NOT MEET THE CRITERIA. ${request.cure}\n` +
|
|
118
|
-
`Write all ${spans.length} briefs again, each under its own marker.`]
|
|
119
|
-
: []),
|
|
120
|
-
].join("\n\n");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Split a batched answer back into one brief per span, by marker.
|
|
125
|
-
*
|
|
126
|
-
* Ordinals rather than fold ids: an id is 24 hex characters the model has no reason to
|
|
127
|
-
* reproduce faithfully, and a single transposed character would attach a brief to the wrong
|
|
128
|
-
* span silently. A position it can count. Anything before the first marker is preamble the
|
|
129
|
-
* request asked for none of, and it is dropped rather than prepended to brief one.
|
|
130
|
-
*/
|
|
131
|
-
function splitBatchedBriefs(text, expected) {
|
|
132
|
-
const briefs = new Array(expected).fill("");
|
|
133
|
-
let at = null;
|
|
134
|
-
let buffer = [];
|
|
135
|
-
const flush = () => {
|
|
136
|
-
if (at !== null && at >= 0 && at < expected) briefs[at] = buffer.join("\n").trim();
|
|
137
|
-
buffer = [];
|
|
138
|
-
};
|
|
139
|
-
for (const line of String(text).split(/\r?\n/)) {
|
|
140
|
-
const marked = BATCH_MARKER_PATTERN.exec(line.trim());
|
|
141
|
-
if (marked) {
|
|
142
|
-
flush();
|
|
143
|
-
at = Number(marked[1]) - 1;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (at !== null) buffer.push(line);
|
|
147
|
-
}
|
|
148
|
-
flush();
|
|
149
|
-
return briefs;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function briefRequestPrompt(request) {
|
|
153
|
-
if (Array.isArray(request.spans)) return batchBriefRequestPrompt(request);
|
|
154
|
-
const children = Number.isInteger(request.children) && request.children > 1
|
|
155
|
-
? request.children
|
|
156
|
-
: 0;
|
|
157
|
-
return [
|
|
158
|
-
`Write a factual brief of at most ${request.maxBriefChars} characters covering the ` +
|
|
159
|
-
"SPAN TO BRIEF below, and nothing else. The brief does two jobs at once: it " +
|
|
160
|
-
"summarizes what the span contains, and it tells an agent what it would get back by " +
|
|
161
|
-
"expanding or peeking this fold later, so the agent can decide whether to dig back " +
|
|
162
|
-
"in. Once the span is folded the brief is its only visible trace, so name the " +
|
|
163
|
-
"concrete things inside it: files, identifiers, decisions, results, errors. Do not " +
|
|
164
|
-
"describe the span abstractly. The BEFORE and AFTER sections are orientation only: " +
|
|
165
|
-
"they say where the span sits in the larger conversation, and their content is not " +
|
|
166
|
-
"part of what you are briefing. Use no preamble and no Markdown headers." +
|
|
167
|
-
(children ? `\n\n${groupCoverageRule(children)}` : ""),
|
|
168
|
-
orientationBlock("BEFORE THE SPAN (orientation only, do not brief)", request.beforeText),
|
|
169
|
-
`SPAN TO BRIEF:\n${request.sourceText}`,
|
|
170
|
-
orientationBlock("AFTER THE SPAN (orientation only, do not brief)", request.afterText),
|
|
171
|
-
// A second ask, and the only reason there is one: the first answer missed a criterion
|
|
172
|
-
// this request stated, and the caller would otherwise have to cut the brief itself.
|
|
173
|
-
...(typeof request.cure === "string" && request.cure
|
|
174
|
-
? [`YOUR PREVIOUS ANSWER DID NOT MEET THE CRITERIA. ${request.cure}\nWrite the brief again.`]
|
|
175
|
-
: []),
|
|
176
|
-
].join("\n\n");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function createSummarizeContextSpan(summarizer = "session", loadHostModule = loadPiHost) {
|
|
180
|
-
const configured = normalizeSummarizer(summarizer);
|
|
181
|
-
if (typeof loadHostModule !== "function") {
|
|
182
|
-
throw new Error("Summarizer host-module loader must be a function");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
let runtimePromise;
|
|
186
|
-
const getRuntime = () => {
|
|
187
|
-
runtimePromise ??= Promise.resolve().then(async () => {
|
|
188
|
-
const host = await loadHostModule();
|
|
189
|
-
if (typeof host?.ModelRuntime?.create !== "function") {
|
|
190
|
-
throw new Error("Pi host module does not export ModelRuntime.create()");
|
|
191
|
-
}
|
|
192
|
-
return host.ModelRuntime.create();
|
|
193
|
-
});
|
|
194
|
-
return runtimePromise;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
return async (request, ctx) => {
|
|
198
|
-
const batch = Array.isArray(request?.spans) ? request.spans : null;
|
|
199
|
-
if (batch && (!batch.length || !batch.every((span) => typeof span?.sourceText === "string"))) {
|
|
200
|
-
throw new Error("Summarizer batch requires a nonempty spans array, each with sourceText");
|
|
201
|
-
}
|
|
202
|
-
if (!batch && typeof request?.sourceText !== "string") {
|
|
203
|
-
throw new Error("Summarizer request requires sourceText and a positive maxBriefChars integer");
|
|
204
|
-
}
|
|
205
|
-
if (!Number.isInteger(request?.maxBriefChars) || request.maxBriefChars < 1) {
|
|
206
|
-
throw new Error("Summarizer request requires sourceText and a positive maxBriefChars integer");
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let model;
|
|
210
|
-
if (configured === "session") {
|
|
211
|
-
if (ctx?.model === undefined) {
|
|
212
|
-
throw new Error("Session summarizer requires an active session model");
|
|
213
|
-
}
|
|
214
|
-
model = ctx.model;
|
|
215
|
-
} else {
|
|
216
|
-
const runtime = await getRuntime();
|
|
217
|
-
model = runtime.getModel(configured.provider, configured.model);
|
|
218
|
-
if (!model) {
|
|
219
|
-
throw new Error(`Summarizer model '${configured.provider}/${configured.model}' was not found`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const runtime = await getRuntime();
|
|
224
|
-
const effort = configured !== "session" && configured.effort !== undefined
|
|
225
|
-
? configured.effort
|
|
226
|
-
: typeof ctx?.thinkingLevel === "string" && ctx.thinkingLevel
|
|
227
|
-
? ctx.thinkingLevel
|
|
228
|
-
: "none";
|
|
229
|
-
const prompt = briefRequestPrompt(request);
|
|
230
|
-
// NO TOKEN CEILING, deliberately (Shane 2026-08-11). A maxTokens value does not tell a
|
|
231
|
-
// model to be brief, it cuts the model off mid-answer, and on a reasoning model the
|
|
232
|
-
// reasoning is drawn from the same budget: the harder the span is to read, the less
|
|
233
|
-
// remains for the brief, until nothing remains and the response carries no text at all.
|
|
234
|
-
// The 2026-08-11 rep ran with 512 and paid for it: 22 percent of calls failed with
|
|
235
|
-
// "Summarizer returned no text", every one of them after two minutes of thinking, and
|
|
236
|
-
// brief length correlated NEGATIVELY with call duration at r = -0.575. The group
|
|
237
|
-
// payloads suffered worst, because the most interesting input provokes the most
|
|
238
|
-
// reasoning. Length is bounded by three things that can explain themselves to the
|
|
239
|
-
// model instead: the limit stated in the request, the cure that hands back an
|
|
240
|
-
// over-long brief with the complaint, and the caller's timeout for a runaway.
|
|
241
|
-
const completionOptions = { signal: request.signal };
|
|
242
|
-
if (model.reasoning && effort !== "none" && effort !== "off") {
|
|
243
|
-
completionOptions.reasoning = effort;
|
|
244
|
-
}
|
|
245
|
-
const response = await runtime.completeSimple(model, {
|
|
246
|
-
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
247
|
-
}, completionOptions);
|
|
248
|
-
const brief = Array.isArray(response?.content)
|
|
249
|
-
? response.content
|
|
250
|
-
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
251
|
-
.map((part) => part.text)
|
|
252
|
-
.join("")
|
|
253
|
-
.trim()
|
|
254
|
-
: "";
|
|
255
|
-
if (!brief) throw new Error("Summarizer returned no text");
|
|
256
|
-
if (typeof model.provider !== "string" || !model.provider ||
|
|
257
|
-
typeof model.id !== "string" || !model.id) {
|
|
258
|
-
throw new Error("Resolved summarizer model lacks provider/id attribution");
|
|
259
|
-
}
|
|
260
|
-
// The generator bills outside the session, so this is the ONLY place its spend is
|
|
261
|
-
// visible. Passed through when the provider reports it and omitted when it does not,
|
|
262
|
-
// never defaulted to zero: an absent number and a free call are different facts.
|
|
263
|
-
const usage = response?.usage && typeof response.usage === "object"
|
|
264
|
-
? {
|
|
265
|
-
...(Number.isFinite(response.usage.input) ? { input: response.usage.input } : {}),
|
|
266
|
-
...(Number.isFinite(response.usage.output) ? { output: response.usage.output } : {}),
|
|
267
|
-
...(Number.isFinite(response.usage.cacheRead) ? { cacheRead: response.usage.cacheRead } : {}),
|
|
268
|
-
...(Number.isFinite(response.usage.totalTokens) ? { totalTokens: response.usage.totalTokens } : {}),
|
|
269
|
-
...(Number.isFinite(response.usage?.cost?.total) ? { costTotal: response.usage.cost.total } : {}),
|
|
270
|
-
}
|
|
271
|
-
: null;
|
|
272
|
-
const attribution = {
|
|
273
|
-
provider: model.provider,
|
|
274
|
-
model: model.id,
|
|
275
|
-
effort,
|
|
276
|
-
toolCalls: 0,
|
|
277
|
-
...(usage && Object.keys(usage).length ? { usage } : {}),
|
|
278
|
-
};
|
|
279
|
-
// A batch answers with one brief per span, split by marker and returned positionally.
|
|
280
|
-
// A span whose marker never appeared comes back empty rather than shifted into a
|
|
281
|
-
// neighbour's place: the caller holds each brief to the same contract either way, so an
|
|
282
|
-
// empty one is a complaint about that span alone and the rest still land.
|
|
283
|
-
if (batch) {
|
|
284
|
-
return {
|
|
285
|
-
briefs: splitBatchedBriefs(brief, batch.length),
|
|
286
|
-
...attribution,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
return { brief, ...attribution };
|
|
290
|
-
};
|
|
291
|
-
}
|