@hypercarrier/rarebit 0.1.0-alpha.6 → 0.2.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/CHANGELOG.md +13 -3
- package/README.md +72 -19
- package/RUNBOOK.md +16 -9
- package/docs/examples/rarebit.settings.json +19 -0
- package/package.json +9 -2
- package/scripts/e2e-recap-pi.mjs +456 -0
- package/scripts/e2e-settings-pi.mjs +267 -0
- package/scripts/e2e-v020-pi.mjs +269 -0
- package/scripts/verify-package.mjs +14 -8
- package/src/extension.mjs +183 -13
- package/src/lifecycle.mjs +32 -7
- package/src/rarebit-cli.mjs +2 -0
- package/src/rarebit-command.mjs +21 -1
- package/src/rarebit-core.mjs +87 -9
- package/src/rarebit-recap.mjs +422 -0
- package/src/rarebit-service.mjs +60 -7
- package/src/rarebit-settings-ui.mjs +479 -0
- package/src/rarebit-settings.mjs +90 -7
- package/src/types.d.ts +60 -3
- package/test/lifecycle-contract-independent.test.mjs +2 -0
- package/test/package-artifact-identity.test.mjs +54 -0
- package/test/rarebit-command.test.mjs +8 -4
- package/test/rarebit-recap.test.mjs +513 -0
- package/test/rarebit-settings-ui.test.mjs +388 -0
- package/test/rarebit-v020.test.mjs +294 -0
- package/test/summary-attention-independent.test.mjs +1 -1
package/src/extension.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./rarebit-service.mjs";
|
|
14
14
|
import {
|
|
15
15
|
DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
16
|
+
normalizeRarebitSummaryPrompt,
|
|
16
17
|
selectRarebits,
|
|
17
18
|
sha256,
|
|
18
19
|
} from "./rarebit-core.mjs";
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
getRarebitArgumentCompletions,
|
|
24
25
|
parseRarebitCommand,
|
|
25
26
|
rarebitCommandDescription,
|
|
27
|
+
rarebitCommandUsage,
|
|
26
28
|
} from "./rarebit-command.mjs";
|
|
27
29
|
import {
|
|
28
30
|
buildRarebitForkPlan,
|
|
@@ -39,6 +41,12 @@ import {
|
|
|
39
41
|
createDetachedMaterializer,
|
|
40
42
|
registerRarebitLifecycle,
|
|
41
43
|
} from "./lifecycle.mjs";
|
|
44
|
+
import { createRarebitRecapController } from "./rarebit-recap.mjs";
|
|
45
|
+
import {
|
|
46
|
+
openRarebitPalette,
|
|
47
|
+
openRarebitSettings,
|
|
48
|
+
rarebitPaletteCommand,
|
|
49
|
+
} from "./rarebit-settings-ui.mjs";
|
|
42
50
|
import {
|
|
43
51
|
automaticSummaryInhibitionIdentity,
|
|
44
52
|
queryAutomaticSummaryPolicy,
|
|
@@ -104,6 +112,10 @@ export async function readConfiguredRarebitSettings({
|
|
|
104
112
|
},
|
|
105
113
|
summaryPolicy: resolved.summaryPolicy,
|
|
106
114
|
autoTitle: resolved.autoTitle,
|
|
115
|
+
maxInputTokens: resolved.maxInputTokens,
|
|
116
|
+
summaryPrompt: resolved.summaryPrompt,
|
|
117
|
+
diagnostics: resolved.diagnostics,
|
|
118
|
+
recap: resolved.recap,
|
|
107
119
|
reserveTokens: resolveReserveTokens(global.value, project.value, model),
|
|
108
120
|
};
|
|
109
121
|
}
|
|
@@ -121,6 +133,10 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
121
133
|
});
|
|
122
134
|
const activityReporter =
|
|
123
135
|
explicit.activityReporter ?? createHerdrActivityReporter();
|
|
136
|
+
const recapController = createRarebitRecapController({
|
|
137
|
+
delayMs: explicit.recap?.delayMs,
|
|
138
|
+
timezone: explicit.recap?.timezone,
|
|
139
|
+
});
|
|
124
140
|
const projectActivity = (ctx) => {
|
|
125
141
|
const entries = ctx?.sessionManager?.getBranch?.() ?? [];
|
|
126
142
|
return projectRarebitSessionActivity({
|
|
@@ -155,10 +171,11 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
155
171
|
? `${name} tokens: ${value}`
|
|
156
172
|
: `${name} tokens: unavailable`;
|
|
157
173
|
|
|
158
|
-
const notifyOutcome = (ctx, result, triggered) => {
|
|
159
|
-
if (
|
|
174
|
+
const notifyOutcome = (ctx, result, triggered, diagnostics = {}) => {
|
|
175
|
+
if (result?.duplicate || result?.inFlight) return;
|
|
160
176
|
switch (result?.record?.status) {
|
|
161
177
|
case "ok":
|
|
178
|
+
if (!triggered || diagnostics.summaryUpdated !== true) return;
|
|
162
179
|
notify(
|
|
163
180
|
ctx,
|
|
164
181
|
`Rarebit Summary updated (${tokenLabel("input", result.record.synthesis?.usage?.inputTokens)} · ${tokenLabel("output", result.record.synthesis?.usage?.outputTokens)} · model ${resultModelLabel(result)})`,
|
|
@@ -221,6 +238,16 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
221
238
|
...injected,
|
|
222
239
|
...explicit,
|
|
223
240
|
summaryPolicy,
|
|
241
|
+
maxInputTokens: explicit.maxInputTokens ?? injected.maxInputTokens,
|
|
242
|
+
summaryPrompt: explicit.summaryPrompt ?? injected.summaryPrompt,
|
|
243
|
+
diagnostics: {
|
|
244
|
+
...(injected.diagnostics ?? {}),
|
|
245
|
+
...(explicit.diagnostics ?? {}),
|
|
246
|
+
},
|
|
247
|
+
recap: {
|
|
248
|
+
...(injected.recap ?? {}),
|
|
249
|
+
...(explicit.recap ?? {}),
|
|
250
|
+
},
|
|
224
251
|
piAi: explicit.piAi ?? {
|
|
225
252
|
complete: async (...args) => (await import("@earendil-works/pi-ai/compat")).complete(...args),
|
|
226
253
|
},
|
|
@@ -228,10 +255,23 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
228
255
|
};
|
|
229
256
|
|
|
230
257
|
const materialize = async (ctx, { force = false } = {}) => {
|
|
258
|
+
const recapToken = recapController.captureMaterialization(ctx);
|
|
231
259
|
let synthesisTriggered = false;
|
|
232
260
|
const effective = await loadEffective(ctx);
|
|
261
|
+
let summaryPrompt;
|
|
262
|
+
try {
|
|
263
|
+
summaryPrompt = normalizeRarebitSummaryPrompt(effective.summaryPrompt).guidance;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
notify(
|
|
266
|
+
ctx,
|
|
267
|
+
`Invalid rarebit.summary_prompt: ${error?.message ?? error}`,
|
|
268
|
+
"error",
|
|
269
|
+
);
|
|
270
|
+
return { record: { status: "invalid_configuration" } };
|
|
271
|
+
}
|
|
233
272
|
const result = await processRarebitSummary(ctx, {
|
|
234
273
|
...effective,
|
|
274
|
+
summaryPrompt,
|
|
235
275
|
forceSynthesis: force,
|
|
236
276
|
lifecycleBoundary: force ? "manual" : ctx?.lifecycleBoundary,
|
|
237
277
|
queryAutomaticSummaryPolicy:
|
|
@@ -239,14 +279,26 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
239
279
|
onSynthesisTriggered: (detail) => {
|
|
240
280
|
synthesisTriggered = true;
|
|
241
281
|
const count = detail.rarebitCount ?? detail.rarebitCount;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
282
|
+
if (effective.diagnostics?.summaryTriggered === true)
|
|
283
|
+
notify(
|
|
284
|
+
ctx,
|
|
285
|
+
`Rarebit Summary triggered (${count} Rarebits · input tokens: ~${detail.estimatedInputTokens} estimated (chars/4) · model ${modelLabel(detail.model)})`,
|
|
286
|
+
"info",
|
|
287
|
+
);
|
|
247
288
|
},
|
|
248
289
|
});
|
|
249
|
-
notifyOutcome(ctx, result, synthesisTriggered);
|
|
290
|
+
notifyOutcome(ctx, result, synthesisTriggered, effective.diagnostics);
|
|
291
|
+
recapController.offer({
|
|
292
|
+
ctx,
|
|
293
|
+
result,
|
|
294
|
+
token: recapToken,
|
|
295
|
+
recapEnabled: effective.recap?.enabled !== false,
|
|
296
|
+
recapDelayMs: effective.recap?.delayMs,
|
|
297
|
+
recapTimezone: effective.recap?.timezone,
|
|
298
|
+
sessionRoot: effective.sessionRoot,
|
|
299
|
+
rarebitRoot: effective.rarebitRoot,
|
|
300
|
+
allowExternalSession: effective.allowExternalSession === true,
|
|
301
|
+
});
|
|
250
302
|
return result;
|
|
251
303
|
};
|
|
252
304
|
const schedule = createDetachedMaterializer(materialize, (_error, ctx) => {
|
|
@@ -354,20 +406,33 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
354
406
|
);
|
|
355
407
|
|
|
356
408
|
registerRarebitLifecycle(pi, schedule, {
|
|
357
|
-
onSessionStart: (ctx) => {
|
|
409
|
+
onSessionStart: (ctx, generation) => {
|
|
410
|
+
recapController.invalidate(ctx, generation);
|
|
358
411
|
activeSession = identityFrom(ctx);
|
|
359
412
|
activityReporter.start(projectActivity(ctx));
|
|
360
413
|
},
|
|
361
|
-
|
|
414
|
+
onAgentStart: (ctx, generation) => {
|
|
415
|
+
recapController.invalidate(ctx, generation);
|
|
416
|
+
},
|
|
417
|
+
onTurnStart: (ctx, generation) => {
|
|
418
|
+
recapController.invalidate(ctx, generation);
|
|
419
|
+
},
|
|
420
|
+
onInputSent: (ctx, generation) => {
|
|
421
|
+
recapController.invalidate(ctx, generation);
|
|
422
|
+
},
|
|
423
|
+
onSessionTree: (ctx, _event, generation) => {
|
|
424
|
+
recapController.invalidate(ctx, generation);
|
|
362
425
|
activityReporter.update(projectActivity(ctx));
|
|
363
426
|
},
|
|
364
427
|
onSelectedUserPersisted: (ctx) => {
|
|
365
428
|
activityReporter.update(projectActivity(ctx));
|
|
366
429
|
},
|
|
367
430
|
onAgentSettled: (ctx) => {
|
|
431
|
+
recapController.updateContext(ctx);
|
|
368
432
|
activityReporter.update(projectActivity(ctx));
|
|
369
433
|
},
|
|
370
|
-
onSessionShutdown: () => {
|
|
434
|
+
onSessionShutdown: (ctx, generation) => {
|
|
435
|
+
recapController.invalidate(ctx, generation);
|
|
371
436
|
activeSession = undefined;
|
|
372
437
|
activityReporter.stop();
|
|
373
438
|
},
|
|
@@ -445,7 +510,35 @@ ${fence(requestText)}`;
|
|
|
445
510
|
description: rarebitCommandDescription(),
|
|
446
511
|
getArgumentCompletions: getRarebitArgumentCompletions,
|
|
447
512
|
handler: async (args, ctx) => {
|
|
448
|
-
|
|
513
|
+
let commandInput = String(args ?? "");
|
|
514
|
+
const trimmedCommandInput = commandInput.trim();
|
|
515
|
+
if (!trimmedCommandInput || trimmedCommandInput === "menu") {
|
|
516
|
+
const currentUiState = () => ({
|
|
517
|
+
...identityFrom(ctx),
|
|
518
|
+
branchLeafId: ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null,
|
|
519
|
+
cwd: ctx?.cwd ?? process.cwd(),
|
|
520
|
+
projectTrusted: ctx?.isProjectTrusted?.() === true,
|
|
521
|
+
});
|
|
522
|
+
const initial = currentUiState();
|
|
523
|
+
const selection = await openRarebitPalette(ctx, {
|
|
524
|
+
isCurrent: () => {
|
|
525
|
+
const current = currentUiState();
|
|
526
|
+
return current.sessionId === initial.sessionId &&
|
|
527
|
+
current.sessionFile === initial.sessionFile &&
|
|
528
|
+
current.branchLeafId === initial.branchLeafId &&
|
|
529
|
+
current.cwd === initial.cwd &&
|
|
530
|
+
current.projectTrusted === initial.projectTrusted;
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
commandInput = rarebitPaletteCommand(selection) ?? "";
|
|
534
|
+
if (!commandInput) return;
|
|
535
|
+
if (selection?.id === "recall") {
|
|
536
|
+
const prompt = await ctx.ui.input?.("Rarebit recall prompt", "");
|
|
537
|
+
if (prompt === undefined || !String(prompt).trim()) return;
|
|
538
|
+
commandInput = `recall ${prompt}`;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const command = parseRarebitCommand(commandInput);
|
|
449
542
|
if (!command.ok) {
|
|
450
543
|
notify(
|
|
451
544
|
ctx,
|
|
@@ -457,6 +550,61 @@ ${fence(requestText)}`;
|
|
|
457
550
|
return;
|
|
458
551
|
}
|
|
459
552
|
const { subcommand, arguments: rest } = command;
|
|
553
|
+
if (subcommand === "help") {
|
|
554
|
+
notify(
|
|
555
|
+
ctx,
|
|
556
|
+
[
|
|
557
|
+
rarebitCommandUsage(),
|
|
558
|
+
...(commandInput.trim().split(/\s+/).length === 1
|
|
559
|
+
? ["Use /rarebit to open the interactive command palette."]
|
|
560
|
+
: []),
|
|
561
|
+
].join("\n"),
|
|
562
|
+
"info",
|
|
563
|
+
);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (subcommand === "settings") {
|
|
567
|
+
const currentUiState = () => ({
|
|
568
|
+
...identityFrom(ctx),
|
|
569
|
+
branchLeafId: ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null,
|
|
570
|
+
cwd: ctx?.cwd ?? process.cwd(),
|
|
571
|
+
projectTrusted: ctx?.isProjectTrusted?.() === true,
|
|
572
|
+
});
|
|
573
|
+
const initial = currentUiState();
|
|
574
|
+
await openRarebitSettings(ctx, {
|
|
575
|
+
initialScope: rest[0] === "project" ? "project" : "global",
|
|
576
|
+
projectTrusted: initial.projectTrusted,
|
|
577
|
+
isCurrent: () => {
|
|
578
|
+
const current = currentUiState();
|
|
579
|
+
return current.sessionId === initial.sessionId &&
|
|
580
|
+
current.sessionFile === initial.sessionFile &&
|
|
581
|
+
current.branchLeafId === initial.branchLeafId &&
|
|
582
|
+
current.cwd === initial.cwd &&
|
|
583
|
+
current.projectTrusted === initial.projectTrusted;
|
|
584
|
+
},
|
|
585
|
+
onSaved: async ({ namespace, document }) => {
|
|
586
|
+
const previous = document?.namespace ?? {};
|
|
587
|
+
const changed = (path) => {
|
|
588
|
+
const read = (value) => path.reduce((current, key) => current?.[key], value);
|
|
589
|
+
return JSON.stringify(read(previous)) !== JSON.stringify(read(namespace));
|
|
590
|
+
};
|
|
591
|
+
if (changed(["max_rarebit_ratio"])) {
|
|
592
|
+
const next = { ...policyOverrides };
|
|
593
|
+
delete next.maxRarebitRatio;
|
|
594
|
+
policyOverrides = next;
|
|
595
|
+
}
|
|
596
|
+
if (changed(["min_total_length"])) {
|
|
597
|
+
const next = { ...policyOverrides };
|
|
598
|
+
delete next.minTotalLength;
|
|
599
|
+
policyOverrides = next;
|
|
600
|
+
}
|
|
601
|
+
if (changed(["auto_title"])) autoTitleOverride = undefined;
|
|
602
|
+
const effective = await loadEffective(ctx);
|
|
603
|
+
if (effective.recap?.enabled === false) recapController.invalidate(ctx);
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
460
608
|
if (subcommand === "fork") {
|
|
461
609
|
if (typeof ctx?.switchSession !== "function") {
|
|
462
610
|
notify(ctx, "Rarebit fork is unavailable in this Pi runtime", "error");
|
|
@@ -580,7 +728,7 @@ ${fence(requestText)}`;
|
|
|
580
728
|
: "settings_or_default";
|
|
581
729
|
notify(
|
|
582
730
|
ctx,
|
|
583
|
-
`Rarebit ${subcommand}: auto-title=${autoTitleOverride ?? effective.autoTitle ?? true}; max_rarebit_ratio=${policy.maxRarebitRatio} (${ratioSource}); min_total_length=${policy.minTotalLength} estimated tokens via ceil(chars/4) (${lengthSource}); measurement=${policy.measurementVersion}; model=${modelLabel(effective.model)} from ${effective.modelProvenance?.settingsKey ?? "explicit config"}${policyStatus}`,
|
|
731
|
+
`Rarebit ${subcommand}: auto-title=${autoTitleOverride ?? effective.autoTitle ?? true}; recap=${effective.recap?.enabled !== false ? "on" : "off"} after ${effective.recap?.delayMs ?? 60000}ms (${effective.recap?.timezone ?? "host"}); diagnostics=triggered:${effective.diagnostics?.summaryTriggered === true ? "on" : "off"},updated:${effective.diagnostics?.summaryUpdated === true ? "on" : "off"}; max_input_tokens=${effective.maxInputTokens ?? 64000}; max_rarebit_ratio=${policy.maxRarebitRatio} (${ratioSource}); min_total_length=${policy.minTotalLength} estimated tokens via ceil(chars/4) (${lengthSource}); measurement=${policy.measurementVersion}; model=${modelLabel(effective.model)} from ${effective.modelProvenance?.settingsKey ?? "explicit config"}${policyStatus}`,
|
|
584
732
|
"info",
|
|
585
733
|
);
|
|
586
734
|
return;
|
|
@@ -610,6 +758,28 @@ ${fence(requestText)}`;
|
|
|
610
758
|
);
|
|
611
759
|
return;
|
|
612
760
|
}
|
|
761
|
+
if (subcommand === "recap") {
|
|
762
|
+
if (effective.recap?.enabled === false) {
|
|
763
|
+
recapController.invalidate(ctx);
|
|
764
|
+
notify(ctx, "Rarebit Recap is disabled in settings", "info");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const recap = await recapController.showExisting(ctx, {
|
|
768
|
+
sessionRoot: effective.sessionRoot,
|
|
769
|
+
rarebitRoot: effective.rarebitRoot,
|
|
770
|
+
recapTimezone: effective.recap?.timezone,
|
|
771
|
+
allowExternalSession: effective.allowExternalSession === true,
|
|
772
|
+
});
|
|
773
|
+
if (!recap.shown)
|
|
774
|
+
notify(
|
|
775
|
+
ctx,
|
|
776
|
+
recap.reason === "no_current_summary"
|
|
777
|
+
? "Rarebit has no current Summary for this Session branch"
|
|
778
|
+
: "Rarebit Recap is unavailable in this UI",
|
|
779
|
+
"warning",
|
|
780
|
+
);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
613
783
|
if (subcommand === "title") {
|
|
614
784
|
void generateTitle(ctx, { manual: true }).catch(() =>
|
|
615
785
|
notify(ctx, "Rarebit title generation failed", "error"),
|
package/src/lifecycle.mjs
CHANGED
|
@@ -16,7 +16,12 @@ function messageText(event) {
|
|
|
16
16
|
.join("\n");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function snapshotMaterializationContext(
|
|
19
|
+
function snapshotMaterializationContext(
|
|
20
|
+
ctx,
|
|
21
|
+
mayNotify,
|
|
22
|
+
lifecycleBoundary,
|
|
23
|
+
rarebitLifecycleGeneration,
|
|
24
|
+
) {
|
|
20
25
|
const sessionManager = ctx?.sessionManager;
|
|
21
26
|
const header = sessionManager?.getHeader?.();
|
|
22
27
|
const branch = sessionManager?.getBranch?.();
|
|
@@ -39,6 +44,7 @@ function snapshotMaterializationContext(ctx, mayNotify, lifecycleBoundary) {
|
|
|
39
44
|
modelRegistry: ctx?.modelRegistry,
|
|
40
45
|
sessionId: ctx?.sessionId,
|
|
41
46
|
lifecycleBoundary,
|
|
47
|
+
rarebitLifecycleGeneration,
|
|
42
48
|
isProjectTrusted: () => projectTrusted,
|
|
43
49
|
sessionManager: {
|
|
44
50
|
getHeader: () => header,
|
|
@@ -126,6 +132,7 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
|
|
|
126
132
|
let ownerMessageSeen = false;
|
|
127
133
|
let sessionGeneration = 0;
|
|
128
134
|
let sessionLive = false;
|
|
135
|
+
let rarebitLifecycleGeneration = 0;
|
|
129
136
|
const MAX_PENDING_INPUT_ORIGINS = 256;
|
|
130
137
|
|
|
131
138
|
const clearInputOrigins = () => {
|
|
@@ -169,7 +176,8 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
|
|
|
169
176
|
};
|
|
170
177
|
|
|
171
178
|
pi.on("session_start", (_event, ctx) => {
|
|
172
|
-
|
|
179
|
+
rarebitLifecycleGeneration += 1;
|
|
180
|
+
options.onSessionStart?.(ctx, rarebitLifecycleGeneration);
|
|
173
181
|
clearInputOrigins();
|
|
174
182
|
persistedUserInputAwaitingProvider = false;
|
|
175
183
|
selectedUserMessageAwaitingProvider = false;
|
|
@@ -185,13 +193,26 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
|
|
|
185
193
|
// the selected Session evidence.
|
|
186
194
|
});
|
|
187
195
|
|
|
188
|
-
pi.on("agent_start", () => {
|
|
196
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
197
|
+
rarebitLifecycleGeneration += 1;
|
|
198
|
+
options.onAgentStart?.(ctx, rarebitLifecycleGeneration);
|
|
189
199
|
// A retry, compaction pass, or queued continuation starts a new agent run;
|
|
190
200
|
// an earlier stop is not the fully settled branch.
|
|
191
201
|
normalStopAwaitingSettlement = false;
|
|
192
202
|
});
|
|
193
203
|
|
|
194
|
-
pi.on("
|
|
204
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
205
|
+
rarebitLifecycleGeneration += 1;
|
|
206
|
+
options.onTurnStart?.(ctx, rarebitLifecycleGeneration);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
pi.on("input", (event, ctx) => {
|
|
210
|
+
rarebitLifecycleGeneration += 1;
|
|
211
|
+
try {
|
|
212
|
+
options.onInputSent?.(ctx, rarebitLifecycleGeneration);
|
|
213
|
+
} catch {
|
|
214
|
+
// Human-facing cancellation is optional and must not alter input delivery.
|
|
215
|
+
}
|
|
195
216
|
const bucket =
|
|
196
217
|
event?.streamingBehavior === "steer"
|
|
197
218
|
? "steer"
|
|
@@ -235,8 +256,9 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
|
|
|
235
256
|
});
|
|
236
257
|
|
|
237
258
|
pi.on("session_tree", (event, ctx) => {
|
|
259
|
+
rarebitLifecycleGeneration += 1;
|
|
238
260
|
try {
|
|
239
|
-
options.onSessionTree?.(ctx, event);
|
|
261
|
+
options.onSessionTree?.(ctx, event, rarebitLifecycleGeneration);
|
|
240
262
|
} catch {
|
|
241
263
|
// Tree recency is an optional projection, not materialization truth.
|
|
242
264
|
}
|
|
@@ -261,6 +283,7 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
|
|
|
261
283
|
ctx,
|
|
262
284
|
() => sessionLive && sessionGeneration === generation,
|
|
263
285
|
pendingLifecycleBoundary,
|
|
286
|
+
rarebitLifecycleGeneration,
|
|
264
287
|
);
|
|
265
288
|
pendingLifecycleBoundary = "owner_request";
|
|
266
289
|
schedule(snapshot);
|
|
@@ -306,17 +329,19 @@ export function registerRarebitLifecycle(pi, schedule, options = {}) {
|
|
|
306
329
|
ctx,
|
|
307
330
|
() => sessionLive && sessionGeneration === generation,
|
|
308
331
|
"agent_settled",
|
|
332
|
+
rarebitLifecycleGeneration,
|
|
309
333
|
),
|
|
310
334
|
);
|
|
311
335
|
});
|
|
312
336
|
|
|
313
|
-
pi.on("session_shutdown", () => {
|
|
337
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
338
|
+
rarebitLifecycleGeneration += 1;
|
|
314
339
|
clearInputOrigins();
|
|
315
340
|
persistedUserInputAwaitingProvider = false;
|
|
316
341
|
selectedUserMessageAwaitingProvider = false;
|
|
317
342
|
firstOwnerInputAwaitingProvider = undefined;
|
|
318
343
|
normalStopAwaitingSettlement = false;
|
|
319
344
|
sessionLive = false;
|
|
320
|
-
options.onSessionShutdown?.();
|
|
345
|
+
options.onSessionShutdown?.(ctx, rarebitLifecycleGeneration);
|
|
321
346
|
});
|
|
322
347
|
}
|
package/src/rarebit-cli.mjs
CHANGED
|
@@ -205,6 +205,7 @@ export async function readRarebitCliSettings({
|
|
|
205
205
|
rawRefs: [globalPath, projectPath],
|
|
206
206
|
},
|
|
207
207
|
summaryPolicy: resolved.summaryPolicy,
|
|
208
|
+
summaryPrompt: resolved.summaryPrompt,
|
|
208
209
|
autoTitle: resolved.autoTitle,
|
|
209
210
|
};
|
|
210
211
|
}
|
|
@@ -351,6 +352,7 @@ export async function runRarebitCli(options, dependencies = {}) {
|
|
|
351
352
|
model: runtime.model,
|
|
352
353
|
modelProvenance: runtime.modelProvenance,
|
|
353
354
|
summaryPolicy: runtime.summaryPolicy,
|
|
355
|
+
summaryPrompt: runtime.summaryPrompt,
|
|
354
356
|
forceSynthesis: options.force === true,
|
|
355
357
|
allowExternalSession: true,
|
|
356
358
|
rarebitRoot: dependencies.rarebitRoot,
|
package/src/rarebit-command.mjs
CHANGED
|
@@ -17,8 +17,23 @@ const rest = (usage) => ({
|
|
|
17
17
|
|
|
18
18
|
export const RAREBIT_COMMAND_GRAMMAR = Object.freeze({
|
|
19
19
|
name: "rarebit",
|
|
20
|
-
defaultSubcommand: "
|
|
20
|
+
defaultSubcommand: "menu",
|
|
21
21
|
subcommands: Object.freeze([
|
|
22
|
+
{
|
|
23
|
+
name: "menu",
|
|
24
|
+
description: "Open the interactive Rarebit command palette",
|
|
25
|
+
forms: [[]],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "settings",
|
|
29
|
+
description: "Open interactive Rarebit settings",
|
|
30
|
+
forms: [[], [literal("global", "Edit global Pi settings")], [literal("project", "Edit trusted project settings")]],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "help",
|
|
34
|
+
description: "Show Rarebit command help",
|
|
35
|
+
forms: [[]],
|
|
36
|
+
},
|
|
22
37
|
{
|
|
23
38
|
name: "status",
|
|
24
39
|
description: "Show effective Rarebit configuration",
|
|
@@ -73,6 +88,11 @@ export const RAREBIT_COMMAND_GRAMMAR = Object.freeze({
|
|
|
73
88
|
usageNote: "use Pi's native /name for a literal title",
|
|
74
89
|
forms: [[]],
|
|
75
90
|
},
|
|
91
|
+
{
|
|
92
|
+
name: "recap",
|
|
93
|
+
description: "Show the current Recap in the TUI",
|
|
94
|
+
forms: [[]],
|
|
95
|
+
},
|
|
76
96
|
{
|
|
77
97
|
name: "summarize",
|
|
78
98
|
description: "Force a Rarebit Summary materialization",
|
package/src/rarebit-core.mjs
CHANGED
|
@@ -7,6 +7,16 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
export const RAREBIT_SELECTOR_VERSION = "rarebit-selector-v1";
|
|
8
8
|
export const RAREBIT_MEASUREMENT_VERSION = "rarebit-prose-chars-div4-v1";
|
|
9
9
|
export const RAREBIT_SUMMARY_PROMPT_VERSION = "rarebit-summary-v6";
|
|
10
|
+
export const RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION =
|
|
11
|
+
"rarebit-summary-prompt-v1";
|
|
12
|
+
// User guidance is a UTF-16 string in JavaScript. Bound it before any Summary
|
|
13
|
+
// provider work so a malformed settings value cannot create an unbounded job.
|
|
14
|
+
export const MAX_RAREBIT_SUMMARY_PROMPT_CHARS = 64_000;
|
|
15
|
+
// Receipts have a 48 KiB protocol limit. Keep the configurable Summary field
|
|
16
|
+
// well below that limit even when the model returns multi-byte text.
|
|
17
|
+
export const MAX_RAREBIT_SUMMARY_CHARS = 8_000;
|
|
18
|
+
export const DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE =
|
|
19
|
+
"The summary is free-form prose. State when evidence is uncertain, confusing, contradictory, or importantly missing instead of inventing a coherent account.";
|
|
10
20
|
export const RAREBIT_TITLE_PROMPT_VERSION = "rarebit-title-v1";
|
|
11
21
|
export const RAREBIT_JOB_IDENTITY_VERSION = "rarebit-job-identity-v2";
|
|
12
22
|
|
|
@@ -274,12 +284,50 @@ function semanticMessages(selection) {
|
|
|
274
284
|
}));
|
|
275
285
|
}
|
|
276
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Resolve the bounded Summary style contract. The setting is a scalar prompt
|
|
289
|
+
* fragment. The fixed evidence and status contract remains in this module.
|
|
290
|
+
* The normalized identity is used by the service to prevent a changed prompt
|
|
291
|
+
* from reusing an older receipt.
|
|
292
|
+
*/
|
|
293
|
+
export function normalizeRarebitSummaryPrompt(value) {
|
|
294
|
+
if (value === undefined) {
|
|
295
|
+
return {
|
|
296
|
+
guidance: DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE,
|
|
297
|
+
promptIdentity: null,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (typeof value !== "string")
|
|
301
|
+
throw new TypeError("summary_prompt must be a string");
|
|
302
|
+
const guidance = value.replace(/\r\n?/g, "\n").trim();
|
|
303
|
+
if (!guidance) throw new TypeError("summary_prompt must not be blank");
|
|
304
|
+
if (guidance.length > MAX_RAREBIT_SUMMARY_PROMPT_CHARS)
|
|
305
|
+
throw new RangeError(
|
|
306
|
+
`summary_prompt must not exceed ${MAX_RAREBIT_SUMMARY_PROMPT_CHARS} UTF-16 characters`,
|
|
307
|
+
);
|
|
308
|
+
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/.test(guidance))
|
|
309
|
+
throw new TypeError("summary_prompt contains unsafe control characters");
|
|
310
|
+
return {
|
|
311
|
+
guidance,
|
|
312
|
+
promptIdentity:
|
|
313
|
+
guidance === DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE
|
|
314
|
+
? null
|
|
315
|
+
: sha256({
|
|
316
|
+
version: RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION,
|
|
317
|
+
guidance,
|
|
318
|
+
}),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
277
322
|
function summaryPromptParts(messages, {
|
|
278
323
|
lifecycleBoundary,
|
|
279
324
|
omittedMessageCount = 0,
|
|
280
325
|
omittedTextChars = 0,
|
|
326
|
+
summaryPrompt,
|
|
281
327
|
} = {}) {
|
|
282
328
|
const ownerRequest = lifecycleBoundary === "owner_request";
|
|
329
|
+
const resolvedPrompt = normalizeRarebitSummaryPrompt(summaryPrompt);
|
|
330
|
+
const styleLine = resolvedPrompt.guidance;
|
|
283
331
|
const omission =
|
|
284
332
|
omittedMessageCount > 0 || omittedTextChars > 0
|
|
285
333
|
? {
|
|
@@ -294,7 +342,7 @@ function summaryPromptParts(messages, {
|
|
|
294
342
|
"You are the HyperCarrier Rarebit summarizer.",
|
|
295
343
|
"Summarize only what is explicitly stated in the ordered Rarebit evidence stream below.",
|
|
296
344
|
"If an omission record appears, it is authoritative: earlier evidence is unavailable to you. Do not infer what was trimmed, and state when important missing evidence makes the account uncertain or confusing.",
|
|
297
|
-
|
|
345
|
+
styleLine,
|
|
298
346
|
"Identify every active, non-superseded request visible in the supplied evidence, not only the last turn. Explicit cancellation, replacement, supersession, and later resolution make an earlier request inactive.",
|
|
299
347
|
"Selected evidence contains only user and assistant message prose at Rarebit continuation or stop boundaries. Tool-call inputs, tool results, hidden reasoning, and transport records are deliberately absent.",
|
|
300
348
|
"Absence of a tool transcript is unobservable and must never by itself imply that work was not performed.",
|
|
@@ -328,27 +376,32 @@ export function composeRarebitSummaryDerivationInput(
|
|
|
328
376
|
promptVersion = RAREBIT_SUMMARY_PROMPT_VERSION,
|
|
329
377
|
lifecycleBoundary = "manual",
|
|
330
378
|
maxPromptChars = Number.MAX_SAFE_INTEGER,
|
|
379
|
+
summaryPrompt,
|
|
331
380
|
} = {},
|
|
332
381
|
) {
|
|
333
|
-
void promptVersion;
|
|
334
382
|
if (
|
|
335
383
|
!RAREBIT_SUMMARY_WRITABLE_LIFECYCLE_BOUNDARIES.includes(lifecycleBoundary)
|
|
336
384
|
)
|
|
337
385
|
throw new TypeError("Unsupported Summary lifecycle boundary");
|
|
338
386
|
if (!Number.isInteger(maxPromptChars) || maxPromptChars < 1)
|
|
339
387
|
throw new RangeError("maxPromptChars must be a positive integer");
|
|
388
|
+
const resolvedPrompt = normalizeRarebitSummaryPrompt(summaryPrompt);
|
|
340
389
|
|
|
341
390
|
const allMessages = semanticMessages(selection);
|
|
342
391
|
let messages = allMessages.slice();
|
|
343
392
|
let omittedMessageCount = 0;
|
|
344
393
|
let omittedTextChars = 0;
|
|
345
|
-
let prompt = renderSummaryPrompt(messages, {
|
|
394
|
+
let prompt = renderSummaryPrompt(messages, {
|
|
395
|
+
lifecycleBoundary,
|
|
396
|
+
summaryPrompt,
|
|
397
|
+
});
|
|
346
398
|
while (prompt.length > maxPromptChars && messages.length > 1) {
|
|
347
399
|
messages.shift();
|
|
348
400
|
omittedMessageCount += 1;
|
|
349
401
|
prompt = renderSummaryPrompt(messages, {
|
|
350
402
|
lifecycleBoundary,
|
|
351
403
|
omittedMessageCount,
|
|
404
|
+
summaryPrompt,
|
|
352
405
|
});
|
|
353
406
|
}
|
|
354
407
|
if (prompt.length > maxPromptChars && messages.length === 1) {
|
|
@@ -363,6 +416,7 @@ export function composeRarebitSummaryDerivationInput(
|
|
|
363
416
|
lifecycleBoundary,
|
|
364
417
|
omittedMessageCount,
|
|
365
418
|
omittedTextChars: removed,
|
|
419
|
+
summaryPrompt,
|
|
366
420
|
});
|
|
367
421
|
if (candidatePrompt.length <= maxPromptChars) {
|
|
368
422
|
best = { messages: candidate, prompt: candidatePrompt, removed };
|
|
@@ -377,6 +431,8 @@ export function composeRarebitSummaryDerivationInput(
|
|
|
377
431
|
}
|
|
378
432
|
return {
|
|
379
433
|
prompt,
|
|
434
|
+
promptIdentity: resolvedPrompt.promptIdentity,
|
|
435
|
+
promptVersion,
|
|
380
436
|
coverage: {
|
|
381
437
|
totalMessageCount: allMessages.length,
|
|
382
438
|
includedMessageCount: messages.length,
|
|
@@ -425,17 +481,35 @@ function compactLine(value) {
|
|
|
425
481
|
.trim();
|
|
426
482
|
}
|
|
427
483
|
|
|
428
|
-
|
|
484
|
+
function preserveProseLayout(value) {
|
|
485
|
+
return String(value ?? "")
|
|
486
|
+
.replace(/\r\n?/g, "\n")
|
|
487
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "")
|
|
488
|
+
.replace(/[\u2028\u2029]/g, "\n")
|
|
489
|
+
.trim();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function normalizeRarebitSummary(
|
|
493
|
+
value,
|
|
494
|
+
{ maxChars = MAX_RAREBIT_SUMMARY_CHARS } = {},
|
|
495
|
+
) {
|
|
429
496
|
if (!Number.isInteger(maxChars) || maxChars < 1)
|
|
430
497
|
throw new RangeError("maxChars must be a positive integer");
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
498
|
+
if (maxChars > MAX_RAREBIT_SUMMARY_CHARS)
|
|
499
|
+
throw new RangeError(
|
|
500
|
+
`maxChars must not exceed ${MAX_RAREBIT_SUMMARY_CHARS}`,
|
|
501
|
+
);
|
|
502
|
+
const normalized = preserveProseLayout(value);
|
|
503
|
+
if (!normalized) throw new Error("Summary model returned no usable summary");
|
|
504
|
+
if (normalized.length <= maxChars) return normalized;
|
|
505
|
+
throw new RangeError(
|
|
506
|
+
`Summary model returned ${normalized.length} characters; configured limit is ${maxChars}`,
|
|
507
|
+
);
|
|
434
508
|
}
|
|
435
509
|
|
|
436
510
|
export function normalizeRarebitSummarySynthesis(
|
|
437
511
|
value,
|
|
438
|
-
{ maxChars =
|
|
512
|
+
{ maxChars = MAX_RAREBIT_SUMMARY_CHARS } = {},
|
|
439
513
|
) {
|
|
440
514
|
let synthesis;
|
|
441
515
|
try {
|
|
@@ -456,7 +530,9 @@ export function normalizeRarebitSummarySynthesis(
|
|
|
456
530
|
if (!RAREBIT_SESSION_STATUS_REASONS[status].includes(reason))
|
|
457
531
|
throw new TypeError("Summary model response has an illegal statusReason");
|
|
458
532
|
return {
|
|
459
|
-
summary: normalizeRarebitSummary(synthesis.summary, {
|
|
533
|
+
summary: normalizeRarebitSummary(synthesis.summary, {
|
|
534
|
+
maxChars,
|
|
535
|
+
}),
|
|
460
536
|
sessionStatus: status,
|
|
461
537
|
statusReason: reason,
|
|
462
538
|
};
|
|
@@ -501,6 +577,7 @@ export function rarebitJobIdentity({
|
|
|
501
577
|
selection,
|
|
502
578
|
policy = null,
|
|
503
579
|
promptVersion,
|
|
580
|
+
promptIdentity = null,
|
|
504
581
|
model = null,
|
|
505
582
|
} = {}) {
|
|
506
583
|
if (!new Set(["summary", "title"]).has(operation))
|
|
@@ -520,6 +597,7 @@ export function rarebitJobIdentity({
|
|
|
520
597
|
selectionManifestHash: selection.manifestHash,
|
|
521
598
|
policy: policy === null ? null : normalizeRarebitSummaryPolicy(policy),
|
|
522
599
|
promptVersion: promptVersion ?? null,
|
|
600
|
+
promptIdentity: promptIdentity ?? null,
|
|
523
601
|
model,
|
|
524
602
|
});
|
|
525
603
|
}
|