pi-fold 0.0.1 → 0.1.1
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 +135 -11
- package/extensions/active-context.ts +2157 -0
- package/extensions/evidence.js +673 -0
- package/extensions/index.js +32 -0
- package/extensions/json.ts +228 -0
- package/extensions/lib/advisory.ts +238 -0
- package/extensions/lib/canonical.ts +171 -0
- package/extensions/lib/folding.ts +1091 -0
- package/extensions/lib/measurement.ts +429 -0
- package/extensions/lib/persistence.ts +814 -0
- package/extensions/lib/policy.ts +296 -0
- package/extensions/lib/selection.ts +501 -0
- package/extensions/lib/surfacing.ts +332 -0
- package/extensions/lib/tool-surface.ts +69 -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,332 @@
|
|
|
1
|
+
import { stableStringify } from "../json.ts";
|
|
2
|
+
import {
|
|
3
|
+
clone,
|
|
4
|
+
contentText,
|
|
5
|
+
messageRole,
|
|
6
|
+
} from "./canonical.ts";
|
|
7
|
+
import {
|
|
8
|
+
foldDepth,
|
|
9
|
+
orderedFoldTree,
|
|
10
|
+
} from "./folding.ts";
|
|
11
|
+
import {
|
|
12
|
+
foldInterval,
|
|
13
|
+
refsProtected,
|
|
14
|
+
toolRefsProtected,
|
|
15
|
+
} from "./measurement.ts";
|
|
16
|
+
import { flattenFoldRefs } from "./persistence.ts";
|
|
17
|
+
import {
|
|
18
|
+
SURFACING_CHAR_BUDGET,
|
|
19
|
+
SURFACING_COOLDOWN_ORDINALS,
|
|
20
|
+
SURFACING_DEPTH_WEIGHT,
|
|
21
|
+
SURFACING_LEXICAL_WEIGHT,
|
|
22
|
+
SURFACING_MAX_DEPTH,
|
|
23
|
+
SURFACING_MAX_LOG_RECORDS,
|
|
24
|
+
SURFACING_MAX_TASK_CHARS,
|
|
25
|
+
SURFACING_MAX_TEXT_CHARS,
|
|
26
|
+
SURFACING_MIN_SCORE,
|
|
27
|
+
SURFACING_OUTCOME_WINDOW_ORDINALS,
|
|
28
|
+
SURFACING_RECENCY_WEIGHT,
|
|
29
|
+
SURFACING_RECENT_TASK_SPANS,
|
|
30
|
+
SURFACING_SOURCE_ID,
|
|
31
|
+
SURFACING_TOP_K,
|
|
32
|
+
} from "./policy.ts";
|
|
33
|
+
import type {
|
|
34
|
+
ActiveContextSnapshot,
|
|
35
|
+
ActiveContextState,
|
|
36
|
+
SuggestionSource,
|
|
37
|
+
SuggestionSourceInput,
|
|
38
|
+
SurfacingCandidate,
|
|
39
|
+
SurfacingRecord,
|
|
40
|
+
SurfacingSuggestion,
|
|
41
|
+
} from "./policy.ts";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Deterministic, model-free surfacing. Every input is either the transcript's own
|
|
45
|
+
* ordering or the durable log: no wall clock, no randomness, no embedding call, so
|
|
46
|
+
* the same session state always produces the same slate in the same order.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
const SURFACING_STOPWORDS: ReadonlySet<string> = new Set([
|
|
50
|
+
"about", "after", "again", "against", "already", "also", "another", "because", "been",
|
|
51
|
+
"before", "being", "between", "both", "came", "come", "could", "does", "doing", "done",
|
|
52
|
+
"down", "during", "each", "either", "else", "even", "ever", "every", "exact", "exactly",
|
|
53
|
+
"first", "from", "have", "having", "here", "into", "just", "keep", "kept", "like", "made",
|
|
54
|
+
"make", "many", "more", "most", "much", "must", "need", "next", "none", "only", "other",
|
|
55
|
+
"over", "same", "should", "since", "some", "such", "take", "than", "that", "their", "them",
|
|
56
|
+
"then", "there", "these", "they", "this", "those", "through", "under", "until", "upon",
|
|
57
|
+
"very", "want", "were", "what", "when", "where", "which", "while", "will", "with", "would",
|
|
58
|
+
"your",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export function surfacingTokens(value: string): string[] {
|
|
62
|
+
const tokens: string[] = [];
|
|
63
|
+
const seen = new Set<string>();
|
|
64
|
+
for (const raw of value.toLowerCase().split(/[^a-z0-9_]+/)) {
|
|
65
|
+
const token = raw.replace(/^_+|_+$/g, "");
|
|
66
|
+
if (token.length < 4 || SURFACING_STOPWORDS.has(token) || seen.has(token)) continue;
|
|
67
|
+
seen.add(token);
|
|
68
|
+
tokens.push(token);
|
|
69
|
+
}
|
|
70
|
+
return tokens;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The most recent user/assistant text spans: what the session is working on right now. */
|
|
74
|
+
export function recentTaskText(
|
|
75
|
+
snapshot: Pick<ActiveContextSnapshot, "messages">,
|
|
76
|
+
spans = SURFACING_RECENT_TASK_SPANS,
|
|
77
|
+
): string[] {
|
|
78
|
+
const collected: string[] = [];
|
|
79
|
+
let chars = 0;
|
|
80
|
+
for (let index = snapshot.messages.length - 1; index >= 0 && collected.length < spans; index -= 1) {
|
|
81
|
+
const message = snapshot.messages[index];
|
|
82
|
+
const role = messageRole(message);
|
|
83
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
84
|
+
const text = contentText(message).trim();
|
|
85
|
+
if (!text) continue;
|
|
86
|
+
const bounded = text.slice(0, SURFACING_MAX_TASK_CHARS - chars);
|
|
87
|
+
if (!bounded) break;
|
|
88
|
+
collected.push(bounded);
|
|
89
|
+
chars += bounded.length;
|
|
90
|
+
if (chars >= SURFACING_MAX_TASK_CHARS) break;
|
|
91
|
+
}
|
|
92
|
+
return collected.reverse();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function taskTokenSet(
|
|
96
|
+
snapshot: Pick<ActiveContextSnapshot, "messages">,
|
|
97
|
+
spans = SURFACING_RECENT_TASK_SPANS,
|
|
98
|
+
): ReadonlySet<string> {
|
|
99
|
+
return new Set(surfacingTokens(recentTaskText(snapshot, spans).join(" ")));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Share of the candidate's own vocabulary the recent task text also uses. */
|
|
103
|
+
export function lexicalOverlap(candidateText: string, taskTokens: ReadonlySet<string>): number {
|
|
104
|
+
const tokens = surfacingTokens(candidateText);
|
|
105
|
+
if (!tokens.length || !taskTokens.size) return 0;
|
|
106
|
+
let matched = 0;
|
|
107
|
+
for (const token of tokens) if (taskTokens.has(token)) matched += 1;
|
|
108
|
+
return matched / tokens.length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function roundedScore(value: number): number {
|
|
112
|
+
return Math.round(Math.max(0, Math.min(1, value)) * 10_000) / 10_000;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function scoreSurfacingCandidate(
|
|
116
|
+
candidate: SurfacingCandidate,
|
|
117
|
+
taskTokens: ReadonlySet<string>,
|
|
118
|
+
positionCeiling: number,
|
|
119
|
+
): number {
|
|
120
|
+
const lexical = lexicalOverlap(candidate.text, taskTokens);
|
|
121
|
+
const position = candidate.position ?? 0;
|
|
122
|
+
const recency = positionCeiling > 0 ? Math.max(0, Math.min(1, position / positionCeiling)) : 0;
|
|
123
|
+
const depth = Math.min(Math.max(0, candidate.depth ?? 0), SURFACING_MAX_DEPTH) / SURFACING_MAX_DEPTH;
|
|
124
|
+
return roundedScore(
|
|
125
|
+
SURFACING_LEXICAL_WEIGHT * lexical +
|
|
126
|
+
SURFACING_RECENCY_WEIGHT * recency +
|
|
127
|
+
SURFACING_DEPTH_WEIGHT * depth,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Ids suppressed by hysteresis: suggested recently, not acted on, still inside the cooldown. */
|
|
132
|
+
export function cooledDownIds(
|
|
133
|
+
log: readonly SurfacingRecord[],
|
|
134
|
+
ordinal: number,
|
|
135
|
+
cooldown = SURFACING_COOLDOWN_ORDINALS,
|
|
136
|
+
): Set<string> {
|
|
137
|
+
const suppressed = new Set<string>();
|
|
138
|
+
for (const record of log) {
|
|
139
|
+
if (record.outcome === "accept" || record.ordinal === ordinal) continue;
|
|
140
|
+
if (ordinal < record.ordinal + cooldown) suppressed.add(surfacingKey(record.source, record.id));
|
|
141
|
+
}
|
|
142
|
+
return suppressed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function surfacingKey(source: string, id: string): string {
|
|
146
|
+
return `${source}::${id}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function rankSurfacingCandidates(input: {
|
|
150
|
+
candidates: readonly SurfacingCandidate[];
|
|
151
|
+
taskTokens: ReadonlySet<string>;
|
|
152
|
+
positionCeiling: number;
|
|
153
|
+
suppressed?: ReadonlySet<string>;
|
|
154
|
+
topK?: number;
|
|
155
|
+
minimumScore?: number;
|
|
156
|
+
charBudget?: number;
|
|
157
|
+
}): SurfacingSuggestion[] {
|
|
158
|
+
const topK = input.topK ?? SURFACING_TOP_K;
|
|
159
|
+
const minimumScore = input.minimumScore ?? SURFACING_MIN_SCORE;
|
|
160
|
+
const charBudget = input.charBudget ?? SURFACING_CHAR_BUDGET;
|
|
161
|
+
const scored = input.candidates
|
|
162
|
+
.map((candidate) => ({
|
|
163
|
+
...candidate,
|
|
164
|
+
text: candidate.text.replace(/\s+/g, " ").trim().slice(0, SURFACING_MAX_TEXT_CHARS),
|
|
165
|
+
score: scoreSurfacingCandidate(candidate, input.taskTokens, input.positionCeiling),
|
|
166
|
+
}))
|
|
167
|
+
.filter((candidate) => candidate.text && candidate.score >= minimumScore &&
|
|
168
|
+
!input.suppressed?.has(surfacingKey(candidate.source, candidate.id)))
|
|
169
|
+
// Ties break on source then id so the slate never depends on candidate arrival order.
|
|
170
|
+
.sort((left, right) => right.score - left.score ||
|
|
171
|
+
left.source.localeCompare(right.source) || left.id.localeCompare(right.id));
|
|
172
|
+
const selected: SurfacingSuggestion[] = [];
|
|
173
|
+
let used = 0;
|
|
174
|
+
for (const candidate of scored) {
|
|
175
|
+
if (selected.length >= topK) break;
|
|
176
|
+
const cost = candidate.text.length + candidate.id.length + candidate.route.length +
|
|
177
|
+
(candidate.alternateRoute?.length ?? 0);
|
|
178
|
+
if (used + cost > charBudget) continue;
|
|
179
|
+
used += cost;
|
|
180
|
+
selected.push(candidate);
|
|
181
|
+
}
|
|
182
|
+
return selected;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The session's monotonic transcript ordinal; recency and cooldown are measured in these. */
|
|
186
|
+
export function surfacingOrdinal(snapshot: Pick<ActiveContextSnapshot, "mapped">): number {
|
|
187
|
+
return snapshot.mapped.length;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function surfacingLog(state: Pick<ActiveContextState, "surfacing">): SurfacingRecord[] {
|
|
191
|
+
return state.surfacing ? clone(state.surfacing) : [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function withSurfacingLog(
|
|
195
|
+
state: ActiveContextState,
|
|
196
|
+
log: readonly SurfacingRecord[],
|
|
197
|
+
): ActiveContextState {
|
|
198
|
+
const bounded = log.slice(Math.max(0, log.length - SURFACING_MAX_LOG_RECORDS));
|
|
199
|
+
const next = { ...state };
|
|
200
|
+
if (bounded.length) next.surfacing = clone(bounded) as SurfacingRecord[];
|
|
201
|
+
else delete next.surfacing;
|
|
202
|
+
return next;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A shown suggestion the agent never peeked or expanded inside the outcome window is a reject. */
|
|
206
|
+
export function resolvedSurfacingLog(
|
|
207
|
+
log: readonly SurfacingRecord[],
|
|
208
|
+
ordinal: number,
|
|
209
|
+
outcomeWindow = SURFACING_OUTCOME_WINDOW_ORDINALS,
|
|
210
|
+
): SurfacingRecord[] {
|
|
211
|
+
return log.map((record) => record.outcome === "shown" && ordinal > record.ordinal + outcomeWindow
|
|
212
|
+
? { ...record, outcome: "reject" as const }
|
|
213
|
+
: { ...record });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Marks a suggestion accepted when the agent peeks or expands it inside the outcome window. */
|
|
217
|
+
export function acceptSurfacingSuggestion(
|
|
218
|
+
state: ActiveContextState,
|
|
219
|
+
id: string,
|
|
220
|
+
ordinal: number,
|
|
221
|
+
outcomeWindow = SURFACING_OUTCOME_WINDOW_ORDINALS,
|
|
222
|
+
): ActiveContextState {
|
|
223
|
+
const log = surfacingLog(state);
|
|
224
|
+
let changed = false;
|
|
225
|
+
const next = log.map((record) => {
|
|
226
|
+
if (record.id !== id || record.outcome !== "shown" || ordinal > record.ordinal + outcomeWindow) return record;
|
|
227
|
+
changed = true;
|
|
228
|
+
return { ...record, outcome: "accept" as const };
|
|
229
|
+
});
|
|
230
|
+
return changed ? withSurfacingLog(state, next) : state;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The built-in source: every currently collapsed fold, nested ones included. */
|
|
234
|
+
export function foldBriefCandidates(input: SuggestionSourceInput): SurfacingCandidate[] {
|
|
235
|
+
const { state, snapshot, toolName } = input;
|
|
236
|
+
const collapsed = orderedFoldTree(state, snapshot).filter((fold) => !state.expanded.includes(fold.id));
|
|
237
|
+
return collapsed.flatMap((fold) => {
|
|
238
|
+
const interval = foldInterval(fold, state, snapshot);
|
|
239
|
+
if (!interval) return [];
|
|
240
|
+
const refs = flattenFoldRefs(fold, state);
|
|
241
|
+
const blocked = fold.kind === "tool-result"
|
|
242
|
+
? toolRefsProtected(refs, state, snapshot)
|
|
243
|
+
: refsProtected(refs, state, snapshot);
|
|
244
|
+
if (blocked) return [];
|
|
245
|
+
return [{
|
|
246
|
+
source: SURFACING_SOURCE_ID,
|
|
247
|
+
id: fold.id,
|
|
248
|
+
text: fold.brief,
|
|
249
|
+
route: `${toolName} {"action":"expand","id":"${fold.id}"}`,
|
|
250
|
+
alternateRoute: `${toolName} {"action":"peek","id":"${fold.id}"}`,
|
|
251
|
+
position: interval.end,
|
|
252
|
+
depth: foldDepth(state, fold),
|
|
253
|
+
}];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export const FOLD_BRIEF_SUGGESTION_SOURCE: SuggestionSource = Object.freeze({
|
|
258
|
+
id: SURFACING_SOURCE_ID,
|
|
259
|
+
candidates: foldBriefCandidates,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
export function collectSuggestionCandidates(
|
|
263
|
+
sources: readonly SuggestionSource[],
|
|
264
|
+
input: SuggestionSourceInput,
|
|
265
|
+
): SurfacingCandidate[] {
|
|
266
|
+
return sources.flatMap((source) => {
|
|
267
|
+
let produced: unknown;
|
|
268
|
+
// One faulty external source may never take the carrier, the projection, or the
|
|
269
|
+
// ladder down with it; it simply contributes nothing this pass.
|
|
270
|
+
try { produced = source.candidates(input); }
|
|
271
|
+
catch { return []; }
|
|
272
|
+
if (!Array.isArray(produced)) return [];
|
|
273
|
+
return produced.flatMap((candidate) => validSurfacingCandidate(candidate, source.id)
|
|
274
|
+
? [{ ...candidate, source: source.id }]
|
|
275
|
+
: []);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function validSurfacingCandidate(value: unknown, sourceId: string): value is SurfacingCandidate {
|
|
280
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
281
|
+
const candidate = value as Partial<SurfacingCandidate>;
|
|
282
|
+
return Boolean(sourceId) && typeof candidate.id === "string" && Boolean(candidate.id) &&
|
|
283
|
+
typeof candidate.text === "string" && Boolean(candidate.text.trim()) &&
|
|
284
|
+
typeof candidate.route === "string" && Boolean(candidate.route) &&
|
|
285
|
+
(candidate.alternateRoute === undefined || typeof candidate.alternateRoute === "string") &&
|
|
286
|
+
(candidate.position === undefined || Number.isFinite(candidate.position)) &&
|
|
287
|
+
(candidate.depth === undefined || Number.isFinite(candidate.depth));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* One pass of the selector: resolve outstanding outcomes, rank what is eligible now,
|
|
292
|
+
* and log what will be shown. Pure: the caller owns persistence and rendering.
|
|
293
|
+
*/
|
|
294
|
+
export function updateSurfacing(input: {
|
|
295
|
+
state: ActiveContextState;
|
|
296
|
+
snapshot: ActiveContextSnapshot;
|
|
297
|
+
sources: readonly SuggestionSource[];
|
|
298
|
+
toolName: string;
|
|
299
|
+
}): { state: ActiveContextState; suggestions: SurfacingSuggestion[] } {
|
|
300
|
+
const { state, snapshot } = input;
|
|
301
|
+
const ordinal = surfacingOrdinal(snapshot);
|
|
302
|
+
const resolved = resolvedSurfacingLog(surfacingLog(state), ordinal);
|
|
303
|
+
const shownAtOrdinal = new Set(resolved
|
|
304
|
+
.filter((record) => record.ordinal === ordinal)
|
|
305
|
+
.map((record) => surfacingKey(record.source, record.id)));
|
|
306
|
+
const suggestions = rankSurfacingCandidates({
|
|
307
|
+
candidates: collectSuggestionCandidates(input.sources, {
|
|
308
|
+
state,
|
|
309
|
+
snapshot,
|
|
310
|
+
toolName: input.toolName,
|
|
311
|
+
}),
|
|
312
|
+
taskTokens: taskTokenSet(snapshot),
|
|
313
|
+
positionCeiling: Math.max(0, snapshot.mapped.length - 1),
|
|
314
|
+
suppressed: cooledDownIds(resolved, ordinal),
|
|
315
|
+
});
|
|
316
|
+
// Re-projecting the same ordinal re-renders the same slate; it is one showing,
|
|
317
|
+
// so it neither duplicates a log record nor trips its own cooldown.
|
|
318
|
+
const appended = suggestions.flatMap((suggestion) =>
|
|
319
|
+
shownAtOrdinal.has(surfacingKey(suggestion.source, suggestion.id))
|
|
320
|
+
? []
|
|
321
|
+
: [{
|
|
322
|
+
source: suggestion.source,
|
|
323
|
+
id: suggestion.id,
|
|
324
|
+
score: suggestion.score,
|
|
325
|
+
ordinal,
|
|
326
|
+
outcome: "shown" as const,
|
|
327
|
+
}]);
|
|
328
|
+
const log = [...resolved, ...appended];
|
|
329
|
+
const next = withSurfacingLog(state, log);
|
|
330
|
+
const unchanged = stableStringify(state.surfacing ?? null) === stableStringify(next.surfacing ?? null);
|
|
331
|
+
return { state: unchanged ? state : next, suggestions };
|
|
332
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
const peekGuidance = input.allowedActions.includes("peek")
|
|
20
|
+
? " Peek is the ephemeral point read: it returns one fold's exact source at any depth, ancestors still collapsed, and changes nothing, so the content arrives as a tool result that can later fold away as a completed read batch; expand is the durable in-place restoration."
|
|
21
|
+
: "";
|
|
22
|
+
return {
|
|
23
|
+
name: input.name,
|
|
24
|
+
label: input.label,
|
|
25
|
+
description: input.fullSurface
|
|
26
|
+
? `Page, peek, fold, expand, refold, or protect exact Pi active-context evidence.${peekGuidance} 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.`
|
|
27
|
+
: `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance} 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.`,
|
|
28
|
+
parameters: {
|
|
29
|
+
type: "object",
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
required: ["action"],
|
|
32
|
+
properties: {
|
|
33
|
+
action: { type: "string", enum: input.allowedActions },
|
|
34
|
+
ids: { type: "array", minItems: 1, maxItems: 64, items: { type: "string", minLength: 1 } },
|
|
35
|
+
id: { type: "string", minLength: 1 },
|
|
36
|
+
brief: {
|
|
37
|
+
type: "string",
|
|
38
|
+
minLength: 1,
|
|
39
|
+
maxLength: input.maxBriefChars,
|
|
40
|
+
description: "Factual fold brief; keep it at most 1000 characters to stay below the hard 1200-character limit.",
|
|
41
|
+
},
|
|
42
|
+
offset: { type: "integer", minimum: 0 },
|
|
43
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
44
|
+
detail: { type: "string", enum: ["fold_candidates", "tree"] },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
execute: input.handler,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildActiveContextCommands(input: {
|
|
52
|
+
statusName: string;
|
|
53
|
+
foldName: string;
|
|
54
|
+
statusHandler: CommandHandler;
|
|
55
|
+
foldHandler: CommandHandler;
|
|
56
|
+
}) {
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
name: input.statusName,
|
|
60
|
+
description: "Show active-context fold roots and paging state",
|
|
61
|
+
handler: input.statusHandler,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: input.foldName,
|
|
65
|
+
description: "Losslessly fold a stale context span; works without a main-model request",
|
|
66
|
+
handler: input.foldHandler,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|