@hypercarrier/rarebit 0.1.0-alpha.5 → 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 +24 -2
- package/README.md +96 -20
- package/RUNBOOK.md +32 -9
- package/bin/piq.mjs +14 -0
- package/docs/examples/rarebit.settings.json +19 -0
- package/package.json +29 -3
- package/scripts/e2e-fork-pi.mjs +824 -0
- 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 +20 -12
- package/src/extension.mjs +280 -21
- package/src/index.mjs +1 -0
- package/src/lifecycle.mjs +32 -7
- package/src/pi-session.mjs +237 -0
- package/src/piq-cli.mjs +96 -0
- package/src/piq.mjs +265 -0
- package/src/rarebit-activity.mjs +8 -2
- package/src/rarebit-cli.mjs +103 -5
- package/src/rarebit-command.mjs +29 -1
- package/src/rarebit-core.mjs +87 -9
- package/src/rarebit-fork-lineage.mjs +69 -0
- package/src/rarebit-fork.mjs +241 -0
- package/src/rarebit-recap.mjs +422 -0
- package/src/rarebit-service.mjs +60 -7
- package/src/rarebit-session.mjs +12 -160
- package/src/rarebit-settings-ui.mjs +479 -0
- package/src/rarebit-settings.mjs +90 -7
- package/src/types.d.ts +91 -3
- package/test/lifecycle-contract-independent.test.mjs +2 -0
- package/test/package-artifact-identity.test.mjs +54 -0
- package/test/piq.test.mjs +212 -0
- package/test/rarebit-command.test.mjs +11 -4
- package/test/rarebit-fork.test.mjs +124 -0
- 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/rarebit-cli.mjs
CHANGED
|
@@ -12,12 +12,19 @@ import {
|
|
|
12
12
|
queryRarebits,
|
|
13
13
|
readRarebitSession,
|
|
14
14
|
} from "./rarebit-session.mjs";
|
|
15
|
+
import {
|
|
16
|
+
buildRarebitForkPlan,
|
|
17
|
+
createRarebitForkEntries,
|
|
18
|
+
sessionFilenameFor,
|
|
19
|
+
writeRarebitForkFile,
|
|
20
|
+
} from "./rarebit-fork.mjs";
|
|
15
21
|
|
|
16
22
|
export const RAREBIT_CLI_USAGE = `Usage:
|
|
17
23
|
rarebit query --session <exact-path-or-id> --json
|
|
18
24
|
rarebit extract --session <exact-path-or-id> --json
|
|
19
25
|
rarebit summarize --session <exact-path-or-id> [--force] [--model-command <executable> [--model-arg <arg>]] --json
|
|
20
26
|
rarebit title --session <exact-path-or-id> [--date YYYY-MM-DD] [--model-command <executable> [--model-arg <arg>]] --json
|
|
27
|
+
rarebit fork <exact-source-path-or-id> [--max-token-length <tokens>] [--no-launch] [--json]
|
|
21
28
|
|
|
22
29
|
Normal summarize/title resolve rarebit.model from Pi settings. The optional model-command
|
|
23
30
|
adapter receives one prompt on stdin and must write only model text to stdout.
|
|
@@ -33,11 +40,11 @@ function requiredValue(argv, index, flag) {
|
|
|
33
40
|
export function parseRarebitCliArgs(argv) {
|
|
34
41
|
if (argv.includes("--help") || argv.includes("-h")) return { help: true };
|
|
35
42
|
const [command, ...rest] = argv;
|
|
36
|
-
if (!new Set(["query", "extract", "summarize", "title"]).has(command))
|
|
43
|
+
if (!new Set(["query", "extract", "summarize", "title", "fork"]).has(command))
|
|
37
44
|
throw new Error(
|
|
38
45
|
"First argument must be query, extract, summarize, or title",
|
|
39
46
|
);
|
|
40
|
-
const options = { command, modelArgs: [], json: false, force: false };
|
|
47
|
+
const options = { command, modelArgs: [], json: false, force: false, noLaunch: false };
|
|
41
48
|
for (let index = 0; index < rest.length; index += 1) {
|
|
42
49
|
const argument = rest[index];
|
|
43
50
|
switch (argument) {
|
|
@@ -45,6 +52,17 @@ export function parseRarebitCliArgs(argv) {
|
|
|
45
52
|
options.session = requiredValue(rest, index, argument);
|
|
46
53
|
index += 1;
|
|
47
54
|
break;
|
|
55
|
+
case "--max-token-length":
|
|
56
|
+
options.maxTokenLength = Number(requiredValue(rest, index, argument));
|
|
57
|
+
index += 1;
|
|
58
|
+
break;
|
|
59
|
+
case "--model":
|
|
60
|
+
options.model = requiredValue(rest, index, argument);
|
|
61
|
+
index += 1;
|
|
62
|
+
break;
|
|
63
|
+
case "--no-launch":
|
|
64
|
+
options.noLaunch = true;
|
|
65
|
+
break;
|
|
48
66
|
case "--model-command":
|
|
49
67
|
options.modelCommand = requiredValue(rest, index, argument);
|
|
50
68
|
index += 1;
|
|
@@ -64,11 +82,13 @@ export function parseRarebitCliArgs(argv) {
|
|
|
64
82
|
options.force = true;
|
|
65
83
|
break;
|
|
66
84
|
default:
|
|
67
|
-
|
|
85
|
+
if (command === "fork" && !argument.startsWith("--") && !options.session) options.session = argument;
|
|
86
|
+
else throw new Error(`Unknown argument: ${argument}`);
|
|
68
87
|
}
|
|
69
88
|
}
|
|
70
|
-
if (!options.session) throw new Error("--session is required");
|
|
71
|
-
if (!options.
|
|
89
|
+
if (!options.session) throw new Error(command === "fork" ? "fork requires an exact source Session path or ID" : "--session is required");
|
|
90
|
+
if (options.maxTokenLength !== undefined && (!Number.isSafeInteger(options.maxTokenLength) || options.maxTokenLength < 1)) throw new Error("--max-token-length must be a positive integer");
|
|
91
|
+
if (!options.json && command !== "fork") throw new Error("--json is required");
|
|
72
92
|
if (options.force && command !== "summarize")
|
|
73
93
|
throw new Error("--force only applies to summarize");
|
|
74
94
|
return options;
|
|
@@ -185,6 +205,7 @@ export async function readRarebitCliSettings({
|
|
|
185
205
|
rawRefs: [globalPath, projectPath],
|
|
186
206
|
},
|
|
187
207
|
summaryPolicy: resolved.summaryPolicy,
|
|
208
|
+
summaryPrompt: resolved.summaryPrompt,
|
|
188
209
|
autoTitle: resolved.autoTitle,
|
|
189
210
|
};
|
|
190
211
|
}
|
|
@@ -235,6 +256,79 @@ async function modelRuntime(options, dependencies) {
|
|
|
235
256
|
};
|
|
236
257
|
}
|
|
237
258
|
|
|
259
|
+
async function resolveForkTarget({ cwd, explicitModel, agentDir, resolveTarget }) {
|
|
260
|
+
if (resolveTarget) return resolveTarget({ cwd, explicitModel, agentDir });
|
|
261
|
+
const {
|
|
262
|
+
ModelRuntime,
|
|
263
|
+
SessionManager,
|
|
264
|
+
SettingsManager,
|
|
265
|
+
createAgentSession,
|
|
266
|
+
} = await import("@earendil-works/pi-coding-agent");
|
|
267
|
+
const settings = SettingsManager.create(cwd, agentDir);
|
|
268
|
+
const runtime = await ModelRuntime.create({ allowModelNetwork: false });
|
|
269
|
+
let targetModel;
|
|
270
|
+
if (explicitModel) {
|
|
271
|
+
const [provider, ...idParts] = explicitModel.split("/");
|
|
272
|
+
const id = idParts.join("/");
|
|
273
|
+
targetModel = runtime.getModel(provider, id);
|
|
274
|
+
} else {
|
|
275
|
+
const provider = settings.getDefaultProvider();
|
|
276
|
+
const id = settings.getDefaultModel();
|
|
277
|
+
targetModel = provider && id ? runtime.getModel(provider, id) : undefined;
|
|
278
|
+
if (!targetModel) targetModel = (await runtime.getAvailable())[0];
|
|
279
|
+
}
|
|
280
|
+
if (!targetModel?.provider || !targetModel?.id || !Number.isFinite(targetModel.contextWindow))
|
|
281
|
+
throw new Error("Cannot resolve the target Pi model and context window before forking; set Pi's default model or pass --model provider/model");
|
|
282
|
+
const global = settings.getGlobalSettings();
|
|
283
|
+
const project = settings.getProjectSettings();
|
|
284
|
+
const key = `${targetModel.provider}/${targetModel.id}`;
|
|
285
|
+
const overrides = { ...(global.compaction?.modelOverrides ?? {}), ...(project.compaction?.modelOverrides ?? {}) };
|
|
286
|
+
const reserveTokens = overrides[key]?.reserveTokens ?? settings.getCompactionReserveTokens();
|
|
287
|
+
const { session } = await createAgentSession({
|
|
288
|
+
cwd,
|
|
289
|
+
agentDir,
|
|
290
|
+
model: targetModel,
|
|
291
|
+
modelRuntime: runtime,
|
|
292
|
+
settingsManager: settings,
|
|
293
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
294
|
+
});
|
|
295
|
+
try {
|
|
296
|
+
const promptOverheadTokens = Math.ceil(String(session.agent.state.systemPrompt ?? "").length / 4);
|
|
297
|
+
const toolOverheadTokens = Math.ceil(JSON.stringify(session.agent.state.tools ?? []).length / 4);
|
|
298
|
+
return { model: targetModel, reserveTokens, promptOverheadTokens, toolOverheadTokens };
|
|
299
|
+
} finally {
|
|
300
|
+
session.dispose();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function runPiFork({ session, maxTokenLength, cwd = process.cwd(), piCommand = "pi", noLaunch = false, model, agentDir, resolveTarget, spawnProcess = spawn } = {}) {
|
|
305
|
+
const loaded = await readRarebitSession(session);
|
|
306
|
+
const target = await resolveForkTarget({ cwd, explicitModel: model, agentDir, resolveTarget });
|
|
307
|
+
const targetModel = target.model;
|
|
308
|
+
const plan = buildRarebitForkPlan({
|
|
309
|
+
header: loaded.parsed.header,
|
|
310
|
+
sessionFile: loaded.sessionFile,
|
|
311
|
+
cwd,
|
|
312
|
+
branch: loaded.branch,
|
|
313
|
+
targetCwd: cwd,
|
|
314
|
+
maxTokenLength,
|
|
315
|
+
targetModel,
|
|
316
|
+
reserveTokens: target.reserveTokens,
|
|
317
|
+
promptOverheadTokens: target.promptOverheadTokens,
|
|
318
|
+
toolOverheadTokens: target.toolOverheadTokens,
|
|
319
|
+
});
|
|
320
|
+
const { SessionManager } = await import("@earendil-works/pi-coding-agent");
|
|
321
|
+
const destinationManager = SessionManager.create(cwd);
|
|
322
|
+
const destination = sessionFilenameFor({ sessionDir: destinationManager.getSessionDir(), forkCreatedAt: plan.forkCreatedAt, sessionId: destinationManager.getSessionId() });
|
|
323
|
+
const built = createRarebitForkEntries(plan, { sessionId: destinationManager.getSessionId() });
|
|
324
|
+
await writeRarebitForkFile(destination, built);
|
|
325
|
+
const result = { operation: "fork", session: destination, sourceSessionId: loaded.session.id, ...plan.omission, headroom: plan.headroom, launched: false };
|
|
326
|
+
if (noLaunch) return result;
|
|
327
|
+
const child = spawnProcess(piCommand, ["--session", destination], { cwd, stdio: "inherit", shell: false, env: process.env });
|
|
328
|
+
const exit = await new Promise((resolve, reject) => { child.on("error", reject); child.on("close", (code, signal) => resolve({ code, signal })); });
|
|
329
|
+
return { ...result, launched: true, exitCode: exit.code, signal: exit.signal };
|
|
330
|
+
}
|
|
331
|
+
|
|
238
332
|
export async function runRarebitCli(options, dependencies = {}) {
|
|
239
333
|
const runtimeDependencies = {
|
|
240
334
|
readSettings: dependencies.readSettings ?? readRarebitCliSettings,
|
|
@@ -245,6 +339,9 @@ export async function runRarebitCli(options, dependencies = {}) {
|
|
|
245
339
|
};
|
|
246
340
|
if (options.command === "query") return queryRarebits(options.session);
|
|
247
341
|
if (options.command === "extract") return extractRarebits(options.session);
|
|
342
|
+
if (options.command === "fork") {
|
|
343
|
+
return runPiFork({ session: options.session, maxTokenLength: options.maxTokenLength, noLaunch: options.noLaunch, piCommand: dependencies.piCommand, model: options.model, agentDir: dependencies.agentDir, resolveTarget: dependencies.resolveTarget, spawnProcess: dependencies.spawnProcess });
|
|
344
|
+
}
|
|
248
345
|
|
|
249
346
|
const loaded = await readRarebitSession(options.session);
|
|
250
347
|
const runtime = await modelRuntime(options, runtimeDependencies);
|
|
@@ -255,6 +352,7 @@ export async function runRarebitCli(options, dependencies = {}) {
|
|
|
255
352
|
model: runtime.model,
|
|
256
353
|
modelProvenance: runtime.modelProvenance,
|
|
257
354
|
summaryPolicy: runtime.summaryPolicy,
|
|
355
|
+
summaryPrompt: runtime.summaryPrompt,
|
|
258
356
|
forceSynthesis: options.force === true,
|
|
259
357
|
allowExternalSession: true,
|
|
260
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",
|
|
@@ -29,6 +44,14 @@ export const RAREBIT_COMMAND_GRAMMAR = Object.freeze({
|
|
|
29
44
|
description: "Recall active-branch Rarebit messages with a prompt",
|
|
30
45
|
forms: [[rest("<prompt...>")]],
|
|
31
46
|
},
|
|
47
|
+
{
|
|
48
|
+
name: "fork",
|
|
49
|
+
description: "Open a bounded Rarebit fork without starting a model turn",
|
|
50
|
+
forms: [
|
|
51
|
+
[],
|
|
52
|
+
[literal("--max-token-length", "Imported Rarebit token budget"), number("<positive tokens>", (value) => Number.isSafeInteger(value) && value > 0)],
|
|
53
|
+
],
|
|
54
|
+
},
|
|
32
55
|
{
|
|
33
56
|
name: "config",
|
|
34
57
|
description: "Show or override summary policy for this process",
|
|
@@ -65,6 +88,11 @@ export const RAREBIT_COMMAND_GRAMMAR = Object.freeze({
|
|
|
65
88
|
usageNote: "use Pi's native /name for a literal title",
|
|
66
89
|
forms: [[]],
|
|
67
90
|
},
|
|
91
|
+
{
|
|
92
|
+
name: "recap",
|
|
93
|
+
description: "Show the current Recap in the TUI",
|
|
94
|
+
forms: [[]],
|
|
95
|
+
},
|
|
68
96
|
{
|
|
69
97
|
name: "summarize",
|
|
70
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
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const RAREBIT_FORK_LINEAGE_SCHEMA_VERSION = 1;
|
|
2
|
+
export const RAREBIT_FORK_ENTRY_MARKER_VERSION = "rarebit-fork-entry/1";
|
|
3
|
+
export const RAREBIT_FORK_LINEAGE_CUSTOM_TYPE = "rarebit-fork-lineage";
|
|
4
|
+
export const RAREBIT_FORK_OPENING_MARKER = "rarebit-fork-opening-v1";
|
|
5
|
+
|
|
6
|
+
function validMarker(marker) {
|
|
7
|
+
return (
|
|
8
|
+
marker &&
|
|
9
|
+
typeof marker === "object" &&
|
|
10
|
+
marker.version === RAREBIT_FORK_ENTRY_MARKER_VERSION &&
|
|
11
|
+
(marker.kind === "seed" || marker.kind === "import")
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function entriesWithIds(entries) {
|
|
16
|
+
return (Array.isArray(entries) ? entries : []).filter(
|
|
17
|
+
(entry) => typeof entry?.id === "string" && entry.id,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function lineageRecords(entries) {
|
|
22
|
+
return (Array.isArray(entries) ? entries : []).filter(
|
|
23
|
+
(entry) =>
|
|
24
|
+
entry?.type === "custom" &&
|
|
25
|
+
entry.customType === RAREBIT_FORK_LINEAGE_CUSTOM_TYPE &&
|
|
26
|
+
entry.data?.schemaVersion === RAREBIT_FORK_LINEAGE_SCHEMA_VERSION,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Return only destination entry IDs present in the supplied entries. This
|
|
32
|
+
* intentionally does not resolve parentSession or return source ancestor IDs,
|
|
33
|
+
* so bounded head/tail consumers can classify a marker without a full scan.
|
|
34
|
+
*/
|
|
35
|
+
export function getImportedRarebitEntryIds(entries) {
|
|
36
|
+
const supplied = new Set(entriesWithIds(entries).map((entry) => entry.id));
|
|
37
|
+
const ids = new Set();
|
|
38
|
+
for (const entry of entriesWithIds(entries)) {
|
|
39
|
+
if (validMarker(entry.rarebitFork)) ids.add(entry.id);
|
|
40
|
+
}
|
|
41
|
+
for (const record of lineageRecords(entries)) {
|
|
42
|
+
const data = record.data;
|
|
43
|
+
const imported = Array.isArray(data.importedEntryIds)
|
|
44
|
+
? data.importedEntryIds
|
|
45
|
+
: [];
|
|
46
|
+
for (const id of [data.opening?.seedEntryId, ...imported]) {
|
|
47
|
+
if (typeof id === "string" && supplied.has(id)) ids.add(id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return ids;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getRarebitForkSeedEntryIds(entries) {
|
|
54
|
+
const supplied = new Set(entriesWithIds(entries).map((entry) => entry.id));
|
|
55
|
+
const ids = new Set();
|
|
56
|
+
for (const entry of entriesWithIds(entries)) {
|
|
57
|
+
if (validMarker(entry.rarebitFork) && entry.rarebitFork.kind === "seed")
|
|
58
|
+
ids.add(entry.id);
|
|
59
|
+
}
|
|
60
|
+
for (const record of lineageRecords(entries)) {
|
|
61
|
+
const id = record.data.opening?.seedEntryId;
|
|
62
|
+
if (typeof id === "string" && supplied.has(id)) ids.add(id);
|
|
63
|
+
}
|
|
64
|
+
return ids;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getRarebitForkLineageRecords(entries) {
|
|
68
|
+
return lineageRecords(entries).map((entry) => entry.data);
|
|
69
|
+
}
|