@fiale-plus/pi-rogue-bundle 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/node_modules/@fiale-plus/pi-core/README.md +6 -5
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +20 -227
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +26 -7
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +17 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +44 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/package.json +31 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +480 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +573 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/file.ts +165 -0
- package/node_modules/@fiale-plus/{pi-core/src/context-broker.test.ts → pi-rogue-context-broker/src/index.test.ts} +61 -1
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +324 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +78 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +500 -0
- package/package.json +11 -3
- package/src/context-broker-file.ts +1 -0
- package/src/context-broker-sqlite.ts +1 -0
- package/src/context-broker.ts +1 -0
- package/src/extension.test.ts +68 -0
- package/src/extension.ts +14 -3
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
It stitches together (and bundles for a true single-package install):
|
|
6
6
|
|
|
7
|
+
- `@fiale-plus/pi-core` (shared contracts/helpers)
|
|
7
8
|
- `@fiale-plus/pi-rogue-advisor` (logic; direct releases paused)
|
|
9
|
+
- `@fiale-plus/pi-rogue-context-broker` (beta context-broker runtime; disabled by default)
|
|
8
10
|
- `@fiale-plus/pi-rogue-orchestration` (logic; direct releases paused)
|
|
9
11
|
|
|
10
12
|
Direct installs of the advisor/orchestration packages are paused (marked private). All users and future releases go through the bundle. See `docs/release.md` and root `AGENTS.md` / `README.md` for the release policy.
|
|
@@ -26,12 +28,17 @@ npm install
|
|
|
26
28
|
## Scope boundaries
|
|
27
29
|
|
|
28
30
|
- **Lab / internal helpers are excluded from this bundle.**
|
|
31
|
+
- The beta context-broker runtime is bundled for opt-in experiments but is not registered/enabled by default.
|
|
32
|
+
- Opt-in consumers can import the runtime through the bundle subpath: `@fiale-plus/pi-rogue-bundle/context-broker`.
|
|
33
|
+
- Set `PI_CONTEXT_BROKER_ENABLED=true` before starting Pi to register the beta `/context` command surface and prompt-load rewriting.
|
|
34
|
+
- Optional durable broker storage can be enabled with `PI_CONTEXT_BROKER_DURABLE=true` or `PI_CONTEXT_BROKER_STORE_DIR=/path/to/store`; it defaults to SQLite/FTS and supports `PI_CONTEXT_BROKER_BACKEND=jsonl` for the legacy JSONL/blob backend.
|
|
29
35
|
- `@fiale-plus/pi-rogue-bundle` is the only published surface for the logic.
|
|
30
36
|
- Internal helper packages (`@fiale-plus/pi-rogue-guardrails`, `@fiale-plus/pi-rogue-brain`, `@fiale-plus/pi-rogue-repo-arch`) are maintained separately in the lab section and not published.
|
|
31
37
|
|
|
32
38
|
## Command surface
|
|
33
39
|
|
|
34
|
-
- `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab` plus status/config/command paths (all provided via the bundle).
|
|
40
|
+
- Default: `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab` plus status/config/command paths (all provided via the bundle).
|
|
41
|
+
- Opt-in beta: `PI_CONTEXT_BROKER_ENABLED=true` adds `/context status`, `/context brief`, `/context lookup <handle|text>`, `/context pin <handle>`, and `/context prune` with autocomplete.
|
|
35
42
|
|
|
36
43
|
## Status
|
|
37
44
|
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Shared helpers for the Pi-Rogue workspace.
|
|
4
4
|
|
|
5
|
-
Includes
|
|
5
|
+
Includes shared bounded context broker contracts:
|
|
6
6
|
|
|
7
|
-
- `
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
7
|
+
- `BoundedContextBroker`
|
|
8
|
+
- `ContextArtifact` / `ContextArtifactInput`
|
|
9
|
+
- lookup, retention, and status type definitions
|
|
10
|
+
|
|
11
|
+
The executable in-memory implementation lives in `@fiale-plus/pi-rogue-context-broker`.
|
|
11
12
|
|
|
12
13
|
Install locally from this repo root: `npm install`
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { safeName } from "./text.js";
|
|
3
|
-
|
|
4
1
|
export type ContextArtifactKind =
|
|
5
2
|
| "tool_output"
|
|
6
3
|
| "diff"
|
|
@@ -9,6 +6,8 @@ export type ContextArtifactKind =
|
|
|
9
6
|
| "advisor_brief"
|
|
10
7
|
| "memory_note";
|
|
11
8
|
|
|
9
|
+
export type ContextArtifactTier = "hot" | "warm" | "cold";
|
|
10
|
+
|
|
12
11
|
export interface ContextArtifactInput {
|
|
13
12
|
sessionId: string;
|
|
14
13
|
kind: ContextArtifactKind;
|
|
@@ -18,6 +17,7 @@ export interface ContextArtifactInput {
|
|
|
18
17
|
paths?: string[];
|
|
19
18
|
command?: string;
|
|
20
19
|
branch?: string;
|
|
20
|
+
tier?: ContextArtifactTier;
|
|
21
21
|
ttlMs?: number;
|
|
22
22
|
pinned?: boolean;
|
|
23
23
|
parentIds?: string[];
|
|
@@ -39,6 +39,7 @@ export interface ContextArtifact {
|
|
|
39
39
|
paths: string[];
|
|
40
40
|
command?: string;
|
|
41
41
|
branch?: string;
|
|
42
|
+
tier: ContextArtifactTier;
|
|
42
43
|
expiresAt?: number;
|
|
43
44
|
pinned: boolean;
|
|
44
45
|
parentIds: string[];
|
|
@@ -53,6 +54,7 @@ export interface ContextLookupQuery {
|
|
|
53
54
|
path?: string;
|
|
54
55
|
commandPrefix?: string;
|
|
55
56
|
branch?: string;
|
|
57
|
+
tier?: ContextArtifactTier;
|
|
56
58
|
text?: string;
|
|
57
59
|
limit?: number;
|
|
58
60
|
}
|
|
@@ -62,6 +64,12 @@ export interface ContextBrokerStatus {
|
|
|
62
64
|
bytes: number;
|
|
63
65
|
pinnedRecords: number;
|
|
64
66
|
pinnedBytes: number;
|
|
67
|
+
hotRecords: number;
|
|
68
|
+
hotBytes: number;
|
|
69
|
+
warmRecords: number;
|
|
70
|
+
warmBytes: number;
|
|
71
|
+
coldRecords: number;
|
|
72
|
+
coldBytes: number;
|
|
65
73
|
maxRecords: number;
|
|
66
74
|
maxBytes: number;
|
|
67
75
|
}
|
|
@@ -70,6 +78,15 @@ export interface ContextBrokerOptions {
|
|
|
70
78
|
maxRecords?: number;
|
|
71
79
|
maxBytes?: number;
|
|
72
80
|
defaultTtlMs?: number;
|
|
81
|
+
hotTtlMs?: number;
|
|
82
|
+
warmTtlMs?: number;
|
|
83
|
+
coldTtlMs?: number;
|
|
84
|
+
hotMaxRecords?: number;
|
|
85
|
+
warmMaxRecords?: number;
|
|
86
|
+
coldMaxRecords?: number;
|
|
87
|
+
hotMaxBytes?: number;
|
|
88
|
+
warmMaxBytes?: number;
|
|
89
|
+
coldMaxBytes?: number;
|
|
73
90
|
summaryBytes?: number;
|
|
74
91
|
briefBytes?: number;
|
|
75
92
|
}
|
|
@@ -82,227 +99,3 @@ export interface BoundedContextBroker {
|
|
|
82
99
|
status(): ContextBrokerStatus;
|
|
83
100
|
renderBrief(query?: ContextLookupQuery & { budgetBytes?: number }): string;
|
|
84
101
|
}
|
|
85
|
-
|
|
86
|
-
const DEFAULT_MAX_RECORDS = 256;
|
|
87
|
-
const DEFAULT_MAX_BYTES = 128 * 1024 * 1024;
|
|
88
|
-
const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
89
|
-
const DEFAULT_SUMMARY_BYTES = 320;
|
|
90
|
-
const DEFAULT_BRIEF_BYTES = 2_000;
|
|
91
|
-
|
|
92
|
-
function normalizeList(values: string[] | undefined): string[] {
|
|
93
|
-
return [...new Set((values ?? []).map((value) => String(value || "").trim()).filter(Boolean))];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function payloadText(payload: string | Buffer): string {
|
|
97
|
-
return Buffer.isBuffer(payload) ? payload.toString("utf8") : String(payload ?? "");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function payloadBytes(payload: string | Buffer): number {
|
|
101
|
-
return Buffer.isBuffer(payload) ? payload.length : Buffer.byteLength(String(payload ?? ""), "utf8");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function hashPayload(payload: string | Buffer): string {
|
|
105
|
-
return createHash("sha256").update(Buffer.isBuffer(payload) ? payload : String(payload)).digest("hex");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function normalizeNeedle(value: string | undefined): string {
|
|
109
|
-
return String(value ?? "").trim().toLowerCase();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function truncateUtf8(text: string, maxBytes: number): string {
|
|
113
|
-
const limit = Math.max(0, Math.floor(maxBytes));
|
|
114
|
-
if (Buffer.byteLength(text, "utf8") <= limit) return text;
|
|
115
|
-
if (limit === 0) return "";
|
|
116
|
-
|
|
117
|
-
const ellipsis = "…";
|
|
118
|
-
const ellipsisBytes = Buffer.byteLength(ellipsis, "utf8");
|
|
119
|
-
const contentLimit = Math.max(0, limit - ellipsisBytes);
|
|
120
|
-
let used = 0;
|
|
121
|
-
let result = "";
|
|
122
|
-
|
|
123
|
-
for (const char of text) {
|
|
124
|
-
const bytes = Buffer.byteLength(char, "utf8");
|
|
125
|
-
if (used + bytes > contentLimit) break;
|
|
126
|
-
result += char;
|
|
127
|
-
used += bytes;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (Buffer.byteLength(result + ellipsis, "utf8") <= limit) return result + ellipsis;
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function summarizeArtifact(summary: string | undefined, kind: ContextArtifactKind, bytes: number, sha256: string, maxBytes: number): string {
|
|
135
|
-
const cleaned = String(summary ?? "").replace(/\s+/g, " ").trim();
|
|
136
|
-
if (cleaned) return truncateUtf8(cleaned, maxBytes);
|
|
137
|
-
return truncateUtf8(`[${kind} payload stored externally; ${bytes} bytes; sha256=${sha256.slice(0, 16)}]`, maxBytes);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function artifactMatches(artifact: ContextArtifact, query: ContextLookupQuery): boolean {
|
|
141
|
-
if (query.id && artifact.id !== query.id) return false;
|
|
142
|
-
if (query.handle && artifact.handle !== query.handle) return false;
|
|
143
|
-
if (query.sessionId && artifact.sessionId !== query.sessionId) return false;
|
|
144
|
-
if (query.kind && artifact.kind !== query.kind) return false;
|
|
145
|
-
if (query.branch && artifact.branch !== query.branch) return false;
|
|
146
|
-
if (query.tag && !artifact.tags.includes(query.tag)) return false;
|
|
147
|
-
if (query.path) {
|
|
148
|
-
const queryPath = query.path.replace(/\/$/, "");
|
|
149
|
-
if (!artifact.paths.some((path) => path === query.path || path.startsWith(`${queryPath}/`))) return false;
|
|
150
|
-
}
|
|
151
|
-
if (query.commandPrefix && !artifact.command?.startsWith(query.commandPrefix)) return false;
|
|
152
|
-
|
|
153
|
-
const text = normalizeNeedle(query.text);
|
|
154
|
-
if (text) {
|
|
155
|
-
const haystack = [artifact.summary, artifact.payload, artifact.command, artifact.tags.join(" "), artifact.paths.join(" ")]
|
|
156
|
-
.join("\n")
|
|
157
|
-
.toLowerCase();
|
|
158
|
-
if (!haystack.includes(text)) return false;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function createInMemoryContextBroker(options: ContextBrokerOptions = {}): BoundedContextBroker {
|
|
165
|
-
const maxRecords = Math.max(1, Math.floor(options.maxRecords ?? DEFAULT_MAX_RECORDS));
|
|
166
|
-
const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? DEFAULT_MAX_BYTES));
|
|
167
|
-
const defaultTtlMs = Math.max(0, Math.floor(options.defaultTtlMs ?? DEFAULT_TTL_MS));
|
|
168
|
-
const summaryBytes = Math.max(16, Math.floor(options.summaryBytes ?? DEFAULT_SUMMARY_BYTES));
|
|
169
|
-
const defaultBriefBytes = Math.max(64, Math.floor(options.briefBytes ?? DEFAULT_BRIEF_BYTES));
|
|
170
|
-
let artifacts: Array<ContextArtifact & { sequence: number }> = [];
|
|
171
|
-
let sequence = 0;
|
|
172
|
-
|
|
173
|
-
function currentStatus(): ContextBrokerStatus {
|
|
174
|
-
const bytes = artifacts.reduce((sum, artifact) => sum + artifact.bytes, 0);
|
|
175
|
-
const pinned = artifacts.filter((artifact) => artifact.pinned);
|
|
176
|
-
return {
|
|
177
|
-
records: artifacts.length,
|
|
178
|
-
bytes,
|
|
179
|
-
pinnedRecords: pinned.length,
|
|
180
|
-
pinnedBytes: pinned.reduce((sum, artifact) => sum + artifact.bytes, 0),
|
|
181
|
-
maxRecords,
|
|
182
|
-
maxBytes,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function dropExpired(now = Date.now(), protectedIds = new Set<string>()): void {
|
|
187
|
-
artifacts = artifacts.filter(
|
|
188
|
-
(artifact) => artifact.pinned || protectedIds.has(artifact.id) || !artifact.expiresAt || artifact.expiresAt > now,
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function oldestRemovable(sessionId: string, protectedIds: Set<string>): { artifact: ContextArtifact & { sequence: number }; index: number } | undefined {
|
|
193
|
-
return artifacts
|
|
194
|
-
.map((artifact, index) => ({ artifact, index }))
|
|
195
|
-
.filter(({ artifact }) => artifact.sessionId === sessionId && !artifact.pinned && !protectedIds.has(artifact.id))
|
|
196
|
-
.sort((a, b) => {
|
|
197
|
-
if (a.artifact.createdAt !== b.artifact.createdAt) return a.artifact.createdAt - b.artifact.createdAt;
|
|
198
|
-
return a.artifact.sequence - b.artifact.sequence;
|
|
199
|
-
})[0];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function sessionWithinCaps(sessionId: string): boolean {
|
|
203
|
-
const sessionArtifacts = artifacts.filter((artifact) => artifact.sessionId === sessionId);
|
|
204
|
-
return sessionArtifacts.length <= maxRecords && sessionArtifacts.reduce((sum, artifact) => sum + artifact.bytes, 0) <= maxBytes;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function prune(now = Date.now(), protectedIds = new Set<string>()): ContextBrokerStatus {
|
|
208
|
-
dropExpired(now, protectedIds);
|
|
209
|
-
|
|
210
|
-
for (const sessionId of new Set(artifacts.map((artifact) => artifact.sessionId))) {
|
|
211
|
-
while (!sessionWithinCaps(sessionId)) {
|
|
212
|
-
const candidate = oldestRemovable(sessionId, protectedIds);
|
|
213
|
-
if (!candidate) break;
|
|
214
|
-
artifacts.splice(candidate.index, 1);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return currentStatus();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function status(): ContextBrokerStatus {
|
|
222
|
-
dropExpired();
|
|
223
|
-
return currentStatus();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function publish(input: ContextArtifactInput): ContextArtifact {
|
|
227
|
-
const now = input.createdAt ?? Date.now();
|
|
228
|
-
const payload = payloadText(input.payload);
|
|
229
|
-
const sha256 = hashPayload(input.payload);
|
|
230
|
-
const bytes = payloadBytes(input.payload);
|
|
231
|
-
const artifactSequence = ++sequence;
|
|
232
|
-
const id = `ctx-${now.toString(36)}-${String(artifactSequence).padStart(4, "0")}-${sha256.slice(0, 12)}`;
|
|
233
|
-
const session = safeName(input.sessionId || "session");
|
|
234
|
-
const kind = input.kind;
|
|
235
|
-
const handle = `ctx://session/${session}/${kind}/${sha256.slice(0, 16)}/${id}`;
|
|
236
|
-
const ttlMs = input.ttlMs ?? defaultTtlMs;
|
|
237
|
-
|
|
238
|
-
const artifact: ContextArtifact & { sequence: number } = {
|
|
239
|
-
id,
|
|
240
|
-
handle,
|
|
241
|
-
sessionId: input.sessionId,
|
|
242
|
-
kind,
|
|
243
|
-
createdAt: now,
|
|
244
|
-
updatedAt: now,
|
|
245
|
-
bytes,
|
|
246
|
-
sha256,
|
|
247
|
-
payload,
|
|
248
|
-
summary: summarizeArtifact(input.summary, kind, bytes, sha256, summaryBytes),
|
|
249
|
-
tags: normalizeList(input.tags),
|
|
250
|
-
paths: normalizeList(input.paths),
|
|
251
|
-
command: input.command?.trim() || undefined,
|
|
252
|
-
branch: input.branch?.trim() || undefined,
|
|
253
|
-
expiresAt: ttlMs > 0 ? now + ttlMs : undefined,
|
|
254
|
-
pinned: Boolean(input.pinned),
|
|
255
|
-
parentIds: normalizeList(input.parentIds),
|
|
256
|
-
sequence: artifactSequence,
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
artifacts = [artifact, ...artifacts];
|
|
260
|
-
prune(now, new Set([artifact.id]));
|
|
261
|
-
return artifact;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function lookup(query: ContextLookupQuery = {}): ContextArtifact[] {
|
|
265
|
-
dropExpired();
|
|
266
|
-
const limit = Math.max(1, Math.floor(query.limit ?? (artifacts.length || 1)));
|
|
267
|
-
return artifacts
|
|
268
|
-
.filter((artifact) => artifactMatches(artifact, query))
|
|
269
|
-
.sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.createdAt - a.createdAt || b.sequence - a.sequence)
|
|
270
|
-
.slice(0, limit);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function pin(idOrHandle: string, pinned = true): ContextArtifact | null {
|
|
274
|
-
dropExpired();
|
|
275
|
-
const artifact = artifacts.find((candidate) => candidate.id === idOrHandle || candidate.handle === idOrHandle) ?? null;
|
|
276
|
-
if (!artifact) return null;
|
|
277
|
-
artifact.pinned = pinned;
|
|
278
|
-
artifact.updatedAt = Date.now();
|
|
279
|
-
prune();
|
|
280
|
-
return artifacts.find((candidate) => candidate.id === artifact.id) ?? null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function renderBrief(query: ContextLookupQuery & { budgetBytes?: number } = {}): string {
|
|
284
|
-
const budget = Math.max(64, Math.floor(query.budgetBytes ?? defaultBriefBytes));
|
|
285
|
-
const lines = [
|
|
286
|
-
"## Context Broker",
|
|
287
|
-
`Budget: ${budget} bytes`,
|
|
288
|
-
...lookup({ ...query, limit: query.limit ?? 8 }).map((artifact) => {
|
|
289
|
-
const pin = artifact.pinned ? " pinned" : "";
|
|
290
|
-
const path = artifact.paths.length ? ` paths=${artifact.paths.slice(0, 3).join(",")}` : "";
|
|
291
|
-
const tags = artifact.tags.length ? ` tags=${artifact.tags.slice(0, 3).join(",")}` : "";
|
|
292
|
-
return `- ${artifact.handle} kind=${artifact.kind}${pin}${path}${tags} summary="${artifact.summary}"`;
|
|
293
|
-
}),
|
|
294
|
-
"Lookup: use broker lookup by handle/path/tag/kind/session before replaying raw payloads.",
|
|
295
|
-
];
|
|
296
|
-
|
|
297
|
-
return truncateUtf8(lines.join("\n"), budget);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
publish,
|
|
302
|
-
lookup,
|
|
303
|
-
pin,
|
|
304
|
-
prune,
|
|
305
|
-
status,
|
|
306
|
-
renderBrief,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
@@ -283,6 +283,15 @@ function brief(s: SessionState): string {
|
|
|
283
283
|
return lines.join("\n").slice(0, 1200);
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
function contextBrokerBrief(pi: ExtensionAPI): string {
|
|
287
|
+
try {
|
|
288
|
+
const text = (pi as any).__piRogueContextBroker?.renderBrief?.();
|
|
289
|
+
return typeof text === "string" && text.includes("ctx://") ? sanitizeAdvisorText(text).slice(0, 2400) : "";
|
|
290
|
+
} catch {
|
|
291
|
+
return "";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
286
295
|
const CLIPBOARD_IMAGE_PATH_RE = /(?:\/(?:private\/)?var\/folders\/[^\s"'`<>]+\/T|\/(?:tmp|var\/tmp))\/clipboard-\d{4}-\d{2}-\d{2}-[A-Za-z0-9-]+\.(?:png|jpe?g|gif|webp)\b/g;
|
|
287
296
|
|
|
288
297
|
export function sanitizeAdvisorText(text: unknown): string {
|
|
@@ -881,12 +890,18 @@ async function askAdvisor(pi: ExtensionAPI, ctx: any, question: string, scope: s
|
|
|
881
890
|
const state = loadState();
|
|
882
891
|
if (!question.trim()) return { text: "Ask a question.", error: "empty" };
|
|
883
892
|
|
|
884
|
-
const
|
|
893
|
+
const brokerBrief = includeWork ? contextBrokerBrief(pi) : "";
|
|
894
|
+
const ck = hash("adv", config.model ?? "auto", squish(question, 300), includeWork ? brief(state) : "", brokerBrief);
|
|
885
895
|
const cache = loadCache();
|
|
886
896
|
if (cache[ck]) { state.cacheHits++; saveState(state); return { text: cache[ck], cached: true }; }
|
|
887
897
|
|
|
888
898
|
const msgs = [
|
|
889
|
-
{ role: "user", content: [
|
|
899
|
+
{ role: "user", content: [
|
|
900
|
+
`Question: ${question}`,
|
|
901
|
+
scope ? `Scope: ${scope}` : "",
|
|
902
|
+
includeWork && brief(state) ? `Session:\n${brief(state)}` : "",
|
|
903
|
+
brokerBrief ? `Context broker brief:\n${brokerBrief}` : "",
|
|
904
|
+
].filter(Boolean).join("\n"), timestamp: new Date().toISOString() },
|
|
890
905
|
] as any[];
|
|
891
906
|
|
|
892
907
|
const completed = await completeWithModelFallback(ctx, config, ADVISOR_SYSTEM, msgs, { maxTokens: 600, reasoning: "medium" as ThinkingLevel });
|
|
@@ -984,7 +999,8 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
|
|
|
984
999
|
}
|
|
985
1000
|
|
|
986
1001
|
const b = brief(state);
|
|
987
|
-
|
|
1002
|
+
const brokerBrief = contextBrokerBrief(pi);
|
|
1003
|
+
if (!b && !brokerBrief) {
|
|
988
1004
|
finalDecision = "defer";
|
|
989
1005
|
finalReason = "missing brief context";
|
|
990
1006
|
markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
|
|
@@ -993,7 +1009,7 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
|
|
|
993
1009
|
return;
|
|
994
1010
|
}
|
|
995
1011
|
|
|
996
|
-
const rk = hash("rev", trigger, b, delta, String(meta.fileChanged), String(meta.failed), String(meta.isAgentEnd), String(reviewRoute.label), signature);
|
|
1012
|
+
const rk = hash("rev", trigger, b, brokerBrief, delta, String(meta.fileChanged), String(meta.failed), String(meta.isAgentEnd), String(reviewRoute.label), signature);
|
|
997
1013
|
const cache = loadCache();
|
|
998
1014
|
if (cache[rk]) {
|
|
999
1015
|
finalDecision = "defer";
|
|
@@ -1011,7 +1027,8 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
|
|
|
1011
1027
|
`Delta: ${delta || "(none)"}`,
|
|
1012
1028
|
`Files: ${meta.fileChanged} Errors: ${meta.failed}`,
|
|
1013
1029
|
`Route: ${summarizeRoute(reviewRoute)}`,
|
|
1014
|
-
`Brief:\n${b}
|
|
1030
|
+
b ? `Brief:\n${b}` : "",
|
|
1031
|
+
brokerBrief ? `Context broker brief:\n${brokerBrief}` : "",
|
|
1015
1032
|
].join("\n"), timestamp: new Date().toISOString() },
|
|
1016
1033
|
] as any[];
|
|
1017
1034
|
const completed = await completeWithModelFallback(ctx, config, REVIEW_SYSTEM, msgs, { maxTokens: 400, reasoning: "low" as ThinkingLevel });
|
|
@@ -1140,13 +1157,14 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1140
1157
|
const prompt = typeof event.prompt === "string" && event.prompt.trim() ? squish(event.prompt, 1000) : "";
|
|
1141
1158
|
if (prompt) state.lastTask = prompt;
|
|
1142
1159
|
const briefText = brief(state);
|
|
1160
|
+
const brokerBrief = contextBrokerBrief(pi);
|
|
1143
1161
|
const intent = prompt ? classifyIntent(prompt) : "";
|
|
1144
1162
|
const mode = prompt ? classifyMode(prompt) : "";
|
|
1145
1163
|
const intentTag = intent ? `Intent: ${intent}` : "";
|
|
1146
1164
|
const modeTag = mode ? `Mode: ${mode}` : "";
|
|
1147
1165
|
// Enrich preflight text with session context so the binary gate has more signal
|
|
1148
|
-
const enrichedText = [prompt, event.systemPrompt || "", briefText ? `Brief: ${briefText}` : "", intentTag, modeTag].filter(Boolean).join(" ");
|
|
1149
|
-
const routeInput: AdvisorRouteInput = { phase: "preflight", text: enrichedText || prompt || event.systemPrompt || briefText || intentTag || modeTag || "", brief: briefText };
|
|
1166
|
+
const enrichedText = [prompt, event.systemPrompt || "", briefText ? `Brief: ${briefText}` : "", brokerBrief ? `Context broker: ${brokerBrief}` : "", intentTag, modeTag].filter(Boolean).join(" ");
|
|
1167
|
+
const routeInput: AdvisorRouteInput = { phase: "preflight", text: enrichedText || prompt || event.systemPrompt || briefText || brokerBrief || intentTag || modeTag || "", brief: [briefText, brokerBrief].filter(Boolean).join("\n\n") };
|
|
1150
1168
|
|
|
1151
1169
|
// Binary gate model — fast local classifier for continue/escalate decisions
|
|
1152
1170
|
const gatePrediction = binaryGatePredict(routeInput.text);
|
|
@@ -1191,6 +1209,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1191
1209
|
note,
|
|
1192
1210
|
controlTag,
|
|
1193
1211
|
briefText ? `Brief (cache-aware):\n${briefText}` : "",
|
|
1212
|
+
brokerBrief ? `Context broker brief (lookup-first):\n${brokerBrief}` : "",
|
|
1194
1213
|
].filter(Boolean).join("\n\n"),
|
|
1195
1214
|
};
|
|
1196
1215
|
});
|
|
@@ -93,6 +93,7 @@ describe("advisor two-agent convergence", () => {
|
|
|
93
93
|
let messageRenderers: MessageRendererMap;
|
|
94
94
|
let sendMessageMock: ReturnType<typeof vi.fn>;
|
|
95
95
|
let completeSimpleMock: ReturnType<typeof vi.fn>;
|
|
96
|
+
let piMock: any;
|
|
96
97
|
let priorState: string | null = null;
|
|
97
98
|
let priorConfig: string | null = null;
|
|
98
99
|
let priorCache: string | null = null;
|
|
@@ -107,6 +108,7 @@ describe("advisor two-agent convergence", () => {
|
|
|
107
108
|
commands = setup.commands;
|
|
108
109
|
messageRenderers = setup.messageRenderers;
|
|
109
110
|
sendMessageMock = setup.sendMessage;
|
|
111
|
+
piMock = setup.pi;
|
|
110
112
|
|
|
111
113
|
mkdirSync(dirname(ADVISOR_STATE_PATH), { recursive: true });
|
|
112
114
|
writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify({ mode: "auto", review: "light", checkins: "off", checkinIntervalMinutes: 30 }, null, 2), "utf8");
|
|
@@ -377,6 +379,21 @@ describe("advisor two-agent convergence", () => {
|
|
|
377
379
|
);
|
|
378
380
|
});
|
|
379
381
|
|
|
382
|
+
it("includes broker briefs in manual advisor context when available", async () => {
|
|
383
|
+
expect(commands.advisor).toBeTruthy();
|
|
384
|
+
piMock.__piRogueContextBroker = {
|
|
385
|
+
renderBrief: () => "## Context Broker\nHot:\n- ctx://session/s/tool_output/abc/ctx-1 summary=\"npm test passed\"",
|
|
386
|
+
};
|
|
387
|
+
completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "Use the broker handle as evidence." }] });
|
|
388
|
+
|
|
389
|
+
await commands.advisor.handler("should we use broker context", ctx);
|
|
390
|
+
|
|
391
|
+
const messages = completeSimpleMock.mock.calls.at(-1)?.[1]?.messages;
|
|
392
|
+
const promptText = JSON.stringify(messages ?? completeSimpleMock.mock.calls.at(-1));
|
|
393
|
+
expect(promptText).toContain("Context broker brief");
|
|
394
|
+
expect(promptText).toContain("ctx://session/s/tool_output/abc/ctx-1");
|
|
395
|
+
});
|
|
396
|
+
|
|
380
397
|
it("does not re-run advisory review on repeated agent-end material snapshots", async () => {
|
|
381
398
|
const preflight = handlers.before_agent_start;
|
|
382
399
|
const agentEnd = handlers.agent_end;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Pi-Rogue Context Broker
|
|
2
|
+
|
|
3
|
+
Beta context broker runtime for Pi-Rogue.
|
|
4
|
+
|
|
5
|
+
This package contains the executable in-memory bounded broker implementation:
|
|
6
|
+
|
|
7
|
+
- `createInMemoryContextBroker()` stores artifacts behind stable `ctx://...` handles.
|
|
8
|
+
- Lookups support handle, session, kind, tag, path, command prefix, branch, tier, and text filters.
|
|
9
|
+
- Omitted summaries become metadata-only placeholders, keeping raw payloads out of prompt briefs by default.
|
|
10
|
+
- Artifacts are classified as hot/warm/cold on publish; prompt briefs render hot first, warm second, and exclude cold unless explicitly queried.
|
|
11
|
+
- Pruning enforces per-session record/byte caps, tier-specific record/byte caps, TTL expiry on reads, and pinned-artifact retention.
|
|
12
|
+
|
|
13
|
+
It is intentionally disabled by default in the bundle.
|
|
14
|
+
|
|
15
|
+
## Opt-in beta extension
|
|
16
|
+
|
|
17
|
+
Set `PI_CONTEXT_BROKER_ENABLED=true` before starting Pi with the bundle installed to enable the beta extension:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
PI_CONTEXT_BROKER_ENABLED=true pi
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
When enabled, the bundle registers a `context_lookup` LLM tool plus `/context` commands:
|
|
24
|
+
|
|
25
|
+
- `/context status` — enabled state, record/byte counts, pinned counts.
|
|
26
|
+
- `/context brief` — bounded prompt-safe broker brief with handles and summaries.
|
|
27
|
+
- `/context lookup <handle|text>` — exact handle rehydration or current-session text search.
|
|
28
|
+
- `/context pin <handle>` — protect an artifact from normal TTL/cap pruning.
|
|
29
|
+
- `/context prune` — run TTL/cap pruning immediately.
|
|
30
|
+
|
|
31
|
+
The command includes autocomplete for subcommands and known artifact handles. Exact handle lookup returns clipped payload text; text search returns a smaller clipped excerpt, and truncation is marked explicitly.
|
|
32
|
+
|
|
33
|
+
Optional durability is available with `PI_CONTEXT_BROKER_DURABLE=true` or `PI_CONTEXT_BROKER_STORE_DIR=/path/to/store`. Durable mode now defaults to SQLite (`artifacts.sqlite`) with an FTS index for text lookup, so exact handles, tier, and pin state survive restarts without replay reconstruction. Set `PI_CONTEXT_BROKER_BACKEND=jsonl` to use the legacy JSONL/blob backend.
|
|
34
|
+
|
|
35
|
+
## Session behavior and limits
|
|
36
|
+
|
|
37
|
+
- On session start/reload, the beta backfills the current Pi session branch from `toolResult` and prompt-visible `bashExecution` entries.
|
|
38
|
+
- Backfill is idempotent by session entry id, skips malformed entries instead of failing the session, and honors Pi's `excludeFromContext` bash entries.
|
|
39
|
+
- Without durable mode, restarting Pi loses broker state until the current branch is backfilled again.
|
|
40
|
+
- Prompt integration injects a bounded, tier-aware broker brief and lookup guidance; the LLM also gets a `context_lookup` tool for exact handle dereferencing.
|
|
41
|
+
- The `context` hook rewrites large `toolResult` and prompt-visible `bashExecution` payloads in the LLM-bound message copy to broker handles and summaries, reducing prompt load while preserving exact `/context lookup` rehydration.
|
|
42
|
+
- Pi `excludeFromContext` bash entries are not backfilled or rewritten into broker prompts.
|
|
43
|
+
- Basic secret redaction runs before broker storage and display for common token/password/API-key patterns.
|
|
44
|
+
- Rollback is immediate: unset `PI_CONTEXT_BROKER_ENABLED` and `/reload` or restart Pi. Disable durable writes by unsetting `PI_CONTEXT_BROKER_DURABLE` and `PI_CONTEXT_BROKER_STORE_DIR`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fiale-plus/pi-rogue-context-broker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Beta context broker runtime for Pi-Rogue. In-memory bounded broker implementation behind explicit opt-in.",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"check": "tsc -p ../../tsconfig.json --noEmit",
|
|
13
|
+
"test": "cd ../.. && vitest run packages/context-broker/src/*.test.ts"
|
|
14
|
+
},
|
|
15
|
+
"main": "./src/index.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./src/index.ts",
|
|
18
|
+
"./extension": "./src/extension.ts",
|
|
19
|
+
"./file": "./src/file.ts",
|
|
20
|
+
"./sqlite": "./src/sqlite.ts"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@fiale-plus/pi-core": "^0.1.0",
|
|
24
|
+
"typebox": "^1.1.24"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"README.md",
|
|
29
|
+
"package.json"
|
|
30
|
+
]
|
|
31
|
+
}
|