@hypercarrier/rarebit 0.1.0-alpha.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/AGENTS.md +14 -0
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/RUNBOOK.md +36 -0
- package/SECURITY.md +11 -0
- package/VISUAL-LANGUAGE.md +89 -0
- package/bin/rarebit.mjs +20 -0
- package/package.json +96 -0
- package/schemas/rarebit.schema.json +618 -0
- package/scripts/e2e-recall-pi083.mjs +199 -0
- package/scripts/e2e-summary-cache.mjs +257 -0
- package/scripts/verify-package.mjs +81 -0
- package/src/automatic-summary-policy.mjs +210 -0
- package/src/extension.mjs +516 -0
- package/src/herdr-activity.mjs +111 -0
- package/src/index.mjs +13 -0
- package/src/lifecycle.mjs +322 -0
- package/src/rarebit-activity.mjs +48 -0
- package/src/rarebit-artifact-state.mjs +839 -0
- package/src/rarebit-cli.mjs +305 -0
- package/src/rarebit-command.mjs +281 -0
- package/src/rarebit-core.mjs +525 -0
- package/src/rarebit-model.mjs +226 -0
- package/src/rarebit-recall.mjs +182 -0
- package/src/rarebit-service.mjs +579 -0
- package/src/rarebit-session.mjs +227 -0
- package/src/rarebit-settings.mjs +79 -0
- package/src/rarebit-store.mjs +771 -0
- package/src/rarebit-visual-language.mjs +110 -0
- package/src/types.d.ts +601 -0
- package/test/automatic-summary-policy.test.mjs +476 -0
- package/test/extension-activity.test.mjs +73 -0
- package/test/fixtures/branch-mixed.json +117 -0
- package/test/fixtures/conformance-corpus.v1.json +306 -0
- package/test/fixtures/truncated-mixed.jsonl +2 -0
- package/test/herdr-activity.test.mjs +93 -0
- package/test/lifecycle-contract-independent.test.mjs +627 -0
- package/test/rarebit-activity.test.mjs +82 -0
- package/test/rarebit-artifact-state.test.mjs +440 -0
- package/test/rarebit-cli.test.mjs +222 -0
- package/test/rarebit-command.test.mjs +127 -0
- package/test/rarebit-core.test.mjs +461 -0
- package/test/rarebit-cross-consumer-conformance.test.mjs +150 -0
- package/test/rarebit-independent-contract.test.mjs +317 -0
- package/test/rarebit-recall.test.mjs +434 -0
- package/test/rarebit-sidecar-protocol.test.mjs +538 -0
- package/test/rarebit-visual-language.test.mjs +75 -0
- package/test/summary-attention-independent.test.mjs +172 -0
- package/test/types-history.ts +13 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
function isUserSubmissionOrigin(event) {
|
|
2
|
+
return event?.source === "interactive" || event?.source === "rpc";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isUserMessage(event) {
|
|
6
|
+
return event?.message?.role === "user";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function messageText(event) {
|
|
10
|
+
const content = event?.message?.content;
|
|
11
|
+
if (typeof content === "string") return content;
|
|
12
|
+
if (!Array.isArray(content)) return "";
|
|
13
|
+
return content
|
|
14
|
+
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
15
|
+
.map((block) => block.text)
|
|
16
|
+
.join("\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function snapshotMaterializationContext(ctx, mayNotify, lifecycleBoundary) {
|
|
20
|
+
const sessionManager = ctx?.sessionManager;
|
|
21
|
+
const header = sessionManager?.getHeader?.();
|
|
22
|
+
const branch = sessionManager?.getBranch?.();
|
|
23
|
+
const branchSnapshot = Array.isArray(branch) ? branch.slice() : [];
|
|
24
|
+
const sessionFile = sessionManager?.getSessionFile?.();
|
|
25
|
+
const projectTrusted = ctx?.isProjectTrusted?.() === true;
|
|
26
|
+
const liveUi = ctx?.ui;
|
|
27
|
+
const ui =
|
|
28
|
+
liveUi && typeof liveUi.notify === "function"
|
|
29
|
+
? {
|
|
30
|
+
notify: (...args) => {
|
|
31
|
+
if (mayNotify()) liveUi.notify(...args);
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
: liveUi;
|
|
35
|
+
return {
|
|
36
|
+
cwd: ctx?.cwd,
|
|
37
|
+
hasUI: ctx?.hasUI,
|
|
38
|
+
ui,
|
|
39
|
+
modelRegistry: ctx?.modelRegistry,
|
|
40
|
+
sessionId: ctx?.sessionId,
|
|
41
|
+
lifecycleBoundary,
|
|
42
|
+
isProjectTrusted: () => projectTrusted,
|
|
43
|
+
sessionManager: {
|
|
44
|
+
getHeader: () => header,
|
|
45
|
+
getBranch: () => branchSnapshot.slice(),
|
|
46
|
+
getSessionFile: () => sessionFile,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run at most one materialization at a time without returning its Promise to
|
|
53
|
+
* Pi's awaited extension hook chain. Triggers that arrive while a run is in
|
|
54
|
+
* flight collapse into one rerun against the latest ExtensionContext.
|
|
55
|
+
*/
|
|
56
|
+
export function createDetachedMaterializer(materialize, onError = () => {}) {
|
|
57
|
+
let running = false;
|
|
58
|
+
let queued = false;
|
|
59
|
+
let latestContext;
|
|
60
|
+
|
|
61
|
+
const drain = async () => {
|
|
62
|
+
try {
|
|
63
|
+
while (queued) {
|
|
64
|
+
const ctx = latestContext;
|
|
65
|
+
queued = false;
|
|
66
|
+
latestContext = undefined;
|
|
67
|
+
try {
|
|
68
|
+
await materialize(ctx);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
try {
|
|
71
|
+
onError(error, ctx);
|
|
72
|
+
} catch {
|
|
73
|
+
// Error reporting is best-effort and must never create an
|
|
74
|
+
// unhandled rejection or re-enter Pi's lifecycle chain.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
running = false;
|
|
80
|
+
// Preserve a trigger that arrived after the loop observed `queued` but
|
|
81
|
+
// before this finally block ran.
|
|
82
|
+
if (queued) kick();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const kick = () => {
|
|
87
|
+
if (running) return;
|
|
88
|
+
running = true;
|
|
89
|
+
// Yield a full event-loop turn. A microtask would let synchronous branch
|
|
90
|
+
// selection run before Pi resumes its provider/TUI foreground pipeline.
|
|
91
|
+
setImmediate(() => {
|
|
92
|
+
void drain();
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (ctx) => {
|
|
97
|
+
latestContext = ctx;
|
|
98
|
+
queued = true;
|
|
99
|
+
kick();
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Register Pi's lifecycle handshake for Rarebit materialization.
|
|
105
|
+
*
|
|
106
|
+
* `input` carries origin but runs before persistence. `message_end(user)`
|
|
107
|
+
* confirms a user record entered the loop, and `before_provider_request`
|
|
108
|
+
* follows Pi's Session append. `agent_end` records whether its terminal
|
|
109
|
+
* assistant stopped normally; `agent_settled` captures the complete branch
|
|
110
|
+
* only after Pi has exhausted retries, compaction, and queued continuations.
|
|
111
|
+
* ESC instead ends in an `aborted` assistant message and never creates a
|
|
112
|
+
* settlement checkpoint.
|
|
113
|
+
* Extension-origin prompts are excluded from the user-submission trigger.
|
|
114
|
+
*/
|
|
115
|
+
export function registerRarebitLifecycle(pi, schedule, options = {}) {
|
|
116
|
+
const inputOrigins = {
|
|
117
|
+
direct: [],
|
|
118
|
+
steer: [],
|
|
119
|
+
followUp: [],
|
|
120
|
+
};
|
|
121
|
+
let persistedUserInputAwaitingProvider = false;
|
|
122
|
+
let selectedUserMessageAwaitingProvider = false;
|
|
123
|
+
let pendingLifecycleBoundary = "owner_request";
|
|
124
|
+
let normalStopAwaitingSettlement = false;
|
|
125
|
+
let firstOwnerInputAwaitingProvider;
|
|
126
|
+
let ownerMessageSeen = false;
|
|
127
|
+
let sessionGeneration = 0;
|
|
128
|
+
let sessionLive = false;
|
|
129
|
+
const MAX_PENDING_INPUT_ORIGINS = 256;
|
|
130
|
+
|
|
131
|
+
const clearInputOrigins = () => {
|
|
132
|
+
for (const queue of Object.values(inputOrigins)) queue.length = 0;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const pendingOriginCount = () =>
|
|
136
|
+
Object.values(inputOrigins).reduce(
|
|
137
|
+
(total, queue) => total + queue.length,
|
|
138
|
+
0,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const pruneOldestOrigin = () => {
|
|
142
|
+
const candidates = Object.values(inputOrigins)
|
|
143
|
+
.filter((queue) => queue.length > 0)
|
|
144
|
+
.sort((left, right) => left[0].sequence - right[0].sequence);
|
|
145
|
+
candidates[0]?.shift();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
let inputSequence = 0;
|
|
149
|
+
const consumeInputOrigin = (text) => {
|
|
150
|
+
// Prefer an exact semantic payload match. If skill/template expansion
|
|
151
|
+
// changed the text, fall back to Pi's delivery order: direct prompt first,
|
|
152
|
+
// then every steer before follow-ups.
|
|
153
|
+
for (const queue of [
|
|
154
|
+
inputOrigins.direct,
|
|
155
|
+
inputOrigins.steer,
|
|
156
|
+
inputOrigins.followUp,
|
|
157
|
+
]) {
|
|
158
|
+
const index = queue.findIndex((entry) => entry.text === text);
|
|
159
|
+
if (index >= 0) return queue.splice(index, 1)[0];
|
|
160
|
+
}
|
|
161
|
+
for (const queue of [
|
|
162
|
+
inputOrigins.direct,
|
|
163
|
+
inputOrigins.steer,
|
|
164
|
+
inputOrigins.followUp,
|
|
165
|
+
]) {
|
|
166
|
+
if (queue.length > 0) return queue.shift();
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
pi.on("session_start", (_event, ctx) => {
|
|
172
|
+
options.onSessionStart?.(ctx);
|
|
173
|
+
clearInputOrigins();
|
|
174
|
+
persistedUserInputAwaitingProvider = false;
|
|
175
|
+
selectedUserMessageAwaitingProvider = false;
|
|
176
|
+
normalStopAwaitingSettlement = false;
|
|
177
|
+
firstOwnerInputAwaitingProvider = undefined;
|
|
178
|
+
ownerMessageSeen = (ctx?.sessionManager?.getBranch?.() ?? []).some(
|
|
179
|
+
(entry) => entry?.type === "message" && entry?.message?.role === "user",
|
|
180
|
+
);
|
|
181
|
+
sessionGeneration += 1;
|
|
182
|
+
sessionLive = true;
|
|
183
|
+
// Session attachment initializes exact-session and title bookkeeping only.
|
|
184
|
+
// It must never derive a Summary: resume/reload changes the adapter, not
|
|
185
|
+
// the selected Session evidence.
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
pi.on("agent_start", () => {
|
|
189
|
+
// A retry, compaction pass, or queued continuation starts a new agent run;
|
|
190
|
+
// an earlier stop is not the fully settled branch.
|
|
191
|
+
normalStopAwaitingSettlement = false;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
pi.on("input", (event) => {
|
|
195
|
+
const bucket =
|
|
196
|
+
event?.streamingBehavior === "steer"
|
|
197
|
+
? "steer"
|
|
198
|
+
: event?.streamingBehavior === "followUp"
|
|
199
|
+
? "followUp"
|
|
200
|
+
: "direct";
|
|
201
|
+
inputOrigins[bucket].push({
|
|
202
|
+
source: event?.source,
|
|
203
|
+
bucket,
|
|
204
|
+
text: typeof event?.text === "string" ? event.text : "",
|
|
205
|
+
sequence: inputSequence,
|
|
206
|
+
});
|
|
207
|
+
inputSequence += 1;
|
|
208
|
+
if (pendingOriginCount() > MAX_PENDING_INPUT_ORIGINS) pruneOldestOrigin();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
pi.on("message_end", (event) => {
|
|
212
|
+
if (!isUserMessage(event)) return;
|
|
213
|
+
// Pi invokes this hook before it appends the entry. The later provider
|
|
214
|
+
// boundary is the first point where an activity consumer may read the
|
|
215
|
+
// new exact active branch without missing this selected user occurrence.
|
|
216
|
+
selectedUserMessageAwaitingProvider = true;
|
|
217
|
+
const origin = consumeInputOrigin(messageText(event));
|
|
218
|
+
if (isUserSubmissionOrigin(origin)) {
|
|
219
|
+
persistedUserInputAwaitingProvider = true;
|
|
220
|
+
pendingLifecycleBoundary = "owner_request";
|
|
221
|
+
if (!ownerMessageSeen && origin.bucket === "direct") {
|
|
222
|
+
ownerMessageSeen = true;
|
|
223
|
+
firstOwnerInputAwaitingProvider = {
|
|
224
|
+
text: messageText(event),
|
|
225
|
+
source: origin.source,
|
|
226
|
+
sourceEntryId:
|
|
227
|
+
typeof event?.message?.id === "string"
|
|
228
|
+
? event.message.id
|
|
229
|
+
: typeof event?.id === "string"
|
|
230
|
+
? event.id
|
|
231
|
+
: null,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
pi.on("session_tree", (event, ctx) => {
|
|
238
|
+
try {
|
|
239
|
+
options.onSessionTree?.(ctx, event);
|
|
240
|
+
} catch {
|
|
241
|
+
// Tree recency is an optional projection, not materialization truth.
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
pi.on("before_provider_request", (_event, ctx) => {
|
|
246
|
+
if (selectedUserMessageAwaitingProvider) {
|
|
247
|
+
selectedUserMessageAwaitingProvider = false;
|
|
248
|
+
try {
|
|
249
|
+
options.onSelectedUserPersisted?.(ctx);
|
|
250
|
+
} catch {
|
|
251
|
+
// Activity/UI projections are optional and must not alter provider I/O.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!persistedUserInputAwaitingProvider) return;
|
|
255
|
+
persistedUserInputAwaitingProvider = false;
|
|
256
|
+
// Pi persists message_end(user) before this provider boundary. Snapshot
|
|
257
|
+
// the evidence coordinates now so a later Session switch cannot retarget
|
|
258
|
+
// an already detached materialization.
|
|
259
|
+
const generation = sessionGeneration;
|
|
260
|
+
const snapshot = snapshotMaterializationContext(
|
|
261
|
+
ctx,
|
|
262
|
+
() => sessionLive && sessionGeneration === generation,
|
|
263
|
+
pendingLifecycleBoundary,
|
|
264
|
+
);
|
|
265
|
+
pendingLifecycleBoundary = "owner_request";
|
|
266
|
+
schedule(snapshot);
|
|
267
|
+
if (firstOwnerInputAwaitingProvider) {
|
|
268
|
+
const ownerMessage = firstOwnerInputAwaitingProvider;
|
|
269
|
+
firstOwnerInputAwaitingProvider = undefined;
|
|
270
|
+
try {
|
|
271
|
+
options.onFirstPersistedOwnerMessage?.(snapshot, ownerMessage);
|
|
272
|
+
} catch {
|
|
273
|
+
// Auto-title is a detached human-facing projection. Failure must not
|
|
274
|
+
// enter Pi's provider path or change summary settlement.
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
pi.on("agent_end", (event) => {
|
|
280
|
+
// Record the latest run outcome, but do not project yet: Pi may still
|
|
281
|
+
// retry, compact, or drain a continuation before `agent_settled`.
|
|
282
|
+
const terminalAssistant = [...(event?.messages ?? [])]
|
|
283
|
+
.reverse()
|
|
284
|
+
.find((message) => message?.role === "assistant");
|
|
285
|
+
normalStopAwaitingSettlement = terminalAssistant?.stopReason === "stop";
|
|
286
|
+
if (terminalAssistant?.stopReason === "aborted") {
|
|
287
|
+
clearInputOrigins();
|
|
288
|
+
persistedUserInputAwaitingProvider = false;
|
|
289
|
+
selectedUserMessageAwaitingProvider = false;
|
|
290
|
+
firstOwnerInputAwaitingProvider = undefined;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
295
|
+
try {
|
|
296
|
+
options.onAgentSettled?.(ctx);
|
|
297
|
+
} catch {
|
|
298
|
+
// Activity/UI projections are optional and must not alter settlement.
|
|
299
|
+
}
|
|
300
|
+
if (!normalStopAwaitingSettlement) return;
|
|
301
|
+
normalStopAwaitingSettlement = false;
|
|
302
|
+
|
|
303
|
+
const generation = sessionGeneration;
|
|
304
|
+
schedule(
|
|
305
|
+
snapshotMaterializationContext(
|
|
306
|
+
ctx,
|
|
307
|
+
() => sessionLive && sessionGeneration === generation,
|
|
308
|
+
"agent_settled",
|
|
309
|
+
),
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
pi.on("session_shutdown", () => {
|
|
314
|
+
clearInputOrigins();
|
|
315
|
+
persistedUserInputAwaitingProvider = false;
|
|
316
|
+
selectedUserMessageAwaitingProvider = false;
|
|
317
|
+
firstOwnerInputAwaitingProvider = undefined;
|
|
318
|
+
normalStopAwaitingSettlement = false;
|
|
319
|
+
sessionLive = false;
|
|
320
|
+
options.onSessionShutdown?.();
|
|
321
|
+
});
|
|
322
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Native Rarebit selection is the source of these recency coordinates. This
|
|
2
|
+
// module deliberately has no clock or renderer: consumers choose how (and
|
|
3
|
+
// whether) to project an age from an exact active-branch snapshot.
|
|
4
|
+
|
|
5
|
+
function latest(occurrences, matches) {
|
|
6
|
+
for (let index = occurrences.length - 1; index >= 0; index -= 1) {
|
|
7
|
+
const occurrence = occurrences[index];
|
|
8
|
+
if (!matches(occurrence)) continue;
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
sourceEntryId: occurrence.sourceEntryId ?? null,
|
|
11
|
+
timestamp:
|
|
12
|
+
typeof occurrence.timestamp === "string" ? occurrence.timestamp : null,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Projects the latest selected user-role and normal assistant-stop evidence.
|
|
20
|
+
* "Latest" is active-branch order, not a clock-sorted guess. A missing or
|
|
21
|
+
* invalid native timestamp stays unavailable to a renderer; callers must not
|
|
22
|
+
* substitute materialization time, mtime, or an earlier occurrence.
|
|
23
|
+
*/
|
|
24
|
+
export function projectRarebitSessionActivity(selection) {
|
|
25
|
+
const occurrences = Array.isArray(selection?.occurrences)
|
|
26
|
+
? selection.occurrences
|
|
27
|
+
: [];
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
schemaVersion: 1,
|
|
30
|
+
selectorVersion:
|
|
31
|
+
typeof selection?.manifest?.selectorVersion === "string"
|
|
32
|
+
? selection.manifest.selectorVersion
|
|
33
|
+
: null,
|
|
34
|
+
selectionManifestHash:
|
|
35
|
+
typeof selection?.manifestHash === "string"
|
|
36
|
+
? selection.manifestHash
|
|
37
|
+
: null,
|
|
38
|
+
latestUser: latest(
|
|
39
|
+
occurrences,
|
|
40
|
+
(occurrence) => occurrence?.role === "user",
|
|
41
|
+
),
|
|
42
|
+
latestAgentStop: latest(
|
|
43
|
+
occurrences,
|
|
44
|
+
(occurrence) =>
|
|
45
|
+
occurrence?.role === "assistant" && occurrence?.outcome === "stop",
|
|
46
|
+
),
|
|
47
|
+
});
|
|
48
|
+
}
|