@fiale-plus/pi-rogue 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/README.md +50 -0
- package/node_modules/@fiale-plus/pi-core/README.md +13 -0
- package/node_modules/@fiale-plus/pi-core/package.json +25 -0
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +109 -0
- package/node_modules/@fiale-plus/pi-core/src/index.ts +5 -0
- package/node_modules/@fiale-plus/pi-core/src/paths.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.test.ts +129 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.ts +97 -0
- package/node_modules/@fiale-plus/pi-core/src/storage.ts +39 -0
- package/node_modules/@fiale-plus/pi-core/src/text.test.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/text.ts +14 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +19 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +248 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate.test.ts +66 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +364 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1677 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +63 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +512 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +126 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +580 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +227 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +53 -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 +749 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +818 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/file.ts +191 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +302 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +369 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +122 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +561 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +102 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
- package/package.json +51 -0
- 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 +27 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext, ToolResultEvent } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { ContextArtifact } from "@fiale-plus/pi-core";
|
|
10
|
+
import { createFileContextBroker } from "./file.js";
|
|
11
|
+
import { createInMemoryContextBroker } from "./index.js";
|
|
12
|
+
|
|
13
|
+
export interface ContextBrokerBetaOptions {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
maxRecords?: number;
|
|
16
|
+
maxBytes?: number;
|
|
17
|
+
globalMaxRecords?: number;
|
|
18
|
+
globalMaxBytes?: number;
|
|
19
|
+
briefBytes?: number;
|
|
20
|
+
lookupBytes?: number;
|
|
21
|
+
searchBytes?: number;
|
|
22
|
+
rewriteThresholdBytes?: number;
|
|
23
|
+
durable?: boolean;
|
|
24
|
+
storeDir?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type UiLike = { notify(message: string, type?: "info" | "warning" | "error"): void; setStatus?(key: string, text: string | undefined): void };
|
|
28
|
+
type SessionContextLike = Pick<ExtensionContext, "cwd" | "sessionManager"> & { ui: UiLike };
|
|
29
|
+
|
|
30
|
+
const DEFAULT_BRIEF_BYTES = 1_800;
|
|
31
|
+
const DEFAULT_LOOKUP_BYTES = 12_000;
|
|
32
|
+
const DEFAULT_SEARCH_BYTES = 2_000;
|
|
33
|
+
const DEFAULT_REWRITE_THRESHOLD_BYTES = 8 * 1024;
|
|
34
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
35
|
+
const ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
36
|
+
|
|
37
|
+
function envFlag(name: string): boolean {
|
|
38
|
+
return ENABLED_VALUES.has(String(process.env[name] ?? "").trim().toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function envNonNegativeInt(name: string): number | undefined {
|
|
42
|
+
const raw = process.env[name];
|
|
43
|
+
if (!raw) return undefined;
|
|
44
|
+
const value = Number.parseInt(raw, 10);
|
|
45
|
+
if (!Number.isFinite(value) || value < 0) return undefined;
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isEnvEnabled(): boolean {
|
|
50
|
+
return envFlag("PI_CONTEXT_BROKER_ENABLED");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isSensitiveKey(key: string): boolean {
|
|
54
|
+
return /(?:api[_-]?key|token|secret|password|credential)/i.test(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function redactSecrets(text: string): string {
|
|
58
|
+
return text
|
|
59
|
+
.replace(/\b(sk-[A-Za-z0-9_-]{12,})\b/g, "[REDACTED_API_KEY]")
|
|
60
|
+
.replace(/\b(gh[pousr]_[A-Za-z0-9_]{12,})\b/g, "[REDACTED_GITHUB_TOKEN]")
|
|
61
|
+
.replace(/([\"']?(?:api[_-]?key|token|secret|password|credential)[\w.-]*[\"']?\s*[:=]\s*[\"']?)([^\s'\",;}]+)/gi, "$1[REDACTED]");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sanitizeValue(value: unknown, depth = 0): unknown {
|
|
65
|
+
if (typeof value === "string") return redactSecrets(value);
|
|
66
|
+
if (value == null || typeof value !== "object") return value;
|
|
67
|
+
if (depth > 6) return "[MAX_DEPTH]";
|
|
68
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, depth + 1));
|
|
69
|
+
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
|
70
|
+
key,
|
|
71
|
+
isSensitiveKey(key) ? "[REDACTED]" : sanitizeValue(item, depth + 1),
|
|
72
|
+
]));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toText(value: unknown): string {
|
|
76
|
+
if (typeof value === "string") return redactSecrets(value);
|
|
77
|
+
try {
|
|
78
|
+
return redactSecrets(JSON.stringify(value, null, 2));
|
|
79
|
+
} catch {
|
|
80
|
+
return redactSecrets(String(value ?? ""));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sanitizeForPrompt(text: string): string {
|
|
85
|
+
return String(text).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isHostilePayload(payload: string): boolean {
|
|
89
|
+
return hasHostileText(payload);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasHostileText(text: string): boolean {
|
|
93
|
+
let suspicious = 0;
|
|
94
|
+
let scanned = 0;
|
|
95
|
+
for (const char of text.slice(0, 4096)) {
|
|
96
|
+
const code = char.codePointAt(0) ?? 0;
|
|
97
|
+
scanned += 1;
|
|
98
|
+
if (
|
|
99
|
+
code === 0x00
|
|
100
|
+
|| (code >= 0x01 && code <= 0x08)
|
|
101
|
+
|| (code >= 0x0E && code <= 0x1F)
|
|
102
|
+
|| (code >= 0x7F && code <= 0x9F)
|
|
103
|
+
) {
|
|
104
|
+
suspicious += 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (scanned < 12) return suspicious > 0;
|
|
108
|
+
return suspicious / scanned >= 0.05;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasHostileValue(value: unknown): boolean {
|
|
112
|
+
if (typeof value === "string") return hasHostileText(value);
|
|
113
|
+
if (Array.isArray(value)) return value.some(hasHostileValue);
|
|
114
|
+
if (value && typeof value === "object") {
|
|
115
|
+
return Object.values(value as Record<string, unknown>).some((entry) => hasHostileValue(entry));
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderLookupOutput(item: ContextArtifact, payloadLimit: number): string {
|
|
121
|
+
const isBinary = item.tags.includes("hostile") || item.tags.includes("binary");
|
|
122
|
+
const payloadLines = isBinary
|
|
123
|
+
? [
|
|
124
|
+
"payload:",
|
|
125
|
+
"[payload intentionally omitted from prompt for safety; use /context export",
|
|
126
|
+
sanitizeForPrompt(item.handle),
|
|
127
|
+
"for full content]",
|
|
128
|
+
]
|
|
129
|
+
: [
|
|
130
|
+
"payload:",
|
|
131
|
+
truncateUtf8(sanitizeForPrompt(item.payload), payloadLimit),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
return [
|
|
135
|
+
sanitizeForPrompt(item.handle),
|
|
136
|
+
`tier=${item.tier} kind=${item.kind} bytes=${item.bytes}`,
|
|
137
|
+
`summary=${sanitizeForPrompt(item.summary)}`,
|
|
138
|
+
...payloadLines,
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function truncateUtf8(text: string, maxBytes: number): string {
|
|
143
|
+
const limit = Math.max(0, Math.floor(maxBytes));
|
|
144
|
+
const totalBytes = Buffer.byteLength(text, "utf8");
|
|
145
|
+
if (totalBytes <= limit) return text;
|
|
146
|
+
if (limit === 0) return "";
|
|
147
|
+
|
|
148
|
+
let omittedBytes = totalBytes;
|
|
149
|
+
let result = "";
|
|
150
|
+
let marker = "…";
|
|
151
|
+
|
|
152
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
153
|
+
const verboseMarker = `\n[truncated: omitted ${omittedBytes} bytes]`;
|
|
154
|
+
marker = Buffer.byteLength(verboseMarker, "utf8") < limit ? verboseMarker : "…";
|
|
155
|
+
const contentLimit = Math.max(0, limit - Buffer.byteLength(marker, "utf8"));
|
|
156
|
+
let used = 0;
|
|
157
|
+
let prefix = "";
|
|
158
|
+
|
|
159
|
+
for (const char of text) {
|
|
160
|
+
const bytes = Buffer.byteLength(char, "utf8");
|
|
161
|
+
if (used + bytes > contentLimit) break;
|
|
162
|
+
prefix += char;
|
|
163
|
+
used += bytes;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
result = prefix;
|
|
167
|
+
const nextOmittedBytes = totalBytes - used;
|
|
168
|
+
if (nextOmittedBytes === omittedBytes) break;
|
|
169
|
+
omittedBytes = nextOmittedBytes;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return `${result}${marker}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function compact(value: string, max = 120): string {
|
|
176
|
+
return truncateUtf8(value.replace(/\s+/g, " ").trim(), max);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function stableHash(value: string): string {
|
|
180
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function sessionIdFor(ctx: Partial<SessionContextLike>): string {
|
|
184
|
+
const file = ctx.sessionManager?.getSessionFile?.();
|
|
185
|
+
return file || ctx.cwd || process.cwd();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function messageTimestamp(entry: any): number | undefined {
|
|
189
|
+
const value = entry?.message?.timestamp ?? entry?.timestamp;
|
|
190
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
191
|
+
const parsed = Date.parse(String(value ?? ""));
|
|
192
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function contentText(content: unknown): string {
|
|
196
|
+
if (Array.isArray(content)) {
|
|
197
|
+
return content.map((block) => block?.type === "text" ? block.text : toText(block)).join("\n");
|
|
198
|
+
}
|
|
199
|
+
return toText(content);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function toolPayload(event: { toolName: string; input?: unknown; content?: unknown; details?: unknown; isError?: boolean }): string {
|
|
203
|
+
return [
|
|
204
|
+
`tool=${event.toolName}`,
|
|
205
|
+
`isError=${Boolean(event.isError)}`,
|
|
206
|
+
"input:",
|
|
207
|
+
toText(event.input),
|
|
208
|
+
"content:",
|
|
209
|
+
toText(event.content),
|
|
210
|
+
"details:",
|
|
211
|
+
toText(event.details),
|
|
212
|
+
].join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function textResult(text: string): AgentToolResult<unknown> {
|
|
216
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function brokerPlaceholder(artifact: ContextArtifact): string {
|
|
220
|
+
return [
|
|
221
|
+
`Context broker artifact: ${artifact.handle}`,
|
|
222
|
+
`Summary: ${artifact.summary}`,
|
|
223
|
+
`Payload bytes: ${artifact.bytes}`,
|
|
224
|
+
"Raw payload omitted from prompt. Use /context lookup <handle> if exact evidence is needed.",
|
|
225
|
+
].join("\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function contextLookupHistoryPlaceholder(): string {
|
|
229
|
+
return [
|
|
230
|
+
"Context lookup result omitted from prompt.",
|
|
231
|
+
"Prior context_lookup evidence is terminal and is not re-brokered.",
|
|
232
|
+
"Run context_lookup again with a focused handle/filter only if exact evidence is still needed.",
|
|
233
|
+
].join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function prunedPayloadPlaceholder(hostile = false): string {
|
|
237
|
+
return [
|
|
238
|
+
"Context broker artifact pruned before prompt assembly.",
|
|
239
|
+
hostile ? "Raw hostile/binary payload omitted from prompt for safety." : "Raw payload omitted from prompt to avoid restoring pruned broker evidence.",
|
|
240
|
+
"Re-run the originating command or use a retained ctx:// handle if exact evidence is still needed.",
|
|
241
|
+
].join("\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function summarizeTool(event: { toolName: string; input?: any; isError?: boolean }, bytes: number, hostile = false): string {
|
|
245
|
+
const command = event.toolName === "bash" ? event.input?.command : undefined;
|
|
246
|
+
const path = event.input?.path;
|
|
247
|
+
const target = command ? ` command=${compact(String(command), 120)}` : path ? ` path=${path}` : "";
|
|
248
|
+
const marker = hostile ? "; payload marked hostile; use /context export for full content" : "";
|
|
249
|
+
return `${event.isError ? "failed" : "completed"} ${event.toolName}${target}; payload=${bytes} bytes${marker}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const NON_BROKERED_TOOL_NAMES = new Set(["context_lookup"]);
|
|
253
|
+
|
|
254
|
+
function shouldBrokerToolName(toolName: string): boolean {
|
|
255
|
+
return !NON_BROKERED_TOOL_NAMES.has(toolName);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function ttlFromNowFor(createdAt: number | undefined): number | undefined {
|
|
259
|
+
if (typeof createdAt !== "number" || !Number.isFinite(createdAt)) return undefined;
|
|
260
|
+
return Math.max(DEFAULT_TTL_MS, Date.now() - createdAt + DEFAULT_TTL_MS);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
function isNeverBrokeredToolName(toolName: unknown): boolean {
|
|
265
|
+
return toolName === "advisor";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isNeverBrokeredToolMessage(message: unknown): boolean {
|
|
269
|
+
if (message == null || typeof message !== "object") return false;
|
|
270
|
+
const maybe = message as { toolName?: unknown };
|
|
271
|
+
return isNeverBrokeredToolName(maybe.toolName);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrokerBetaOptions = {}): Promise<void> {
|
|
275
|
+
const p = pi as any;
|
|
276
|
+
if (p.__piRogueContextBrokerBetaRegistered) return;
|
|
277
|
+
p.__piRogueContextBrokerBetaRegistered = true;
|
|
278
|
+
|
|
279
|
+
const briefBytes = options.briefBytes ?? DEFAULT_BRIEF_BYTES;
|
|
280
|
+
const lookupBytes = options.lookupBytes ?? DEFAULT_LOOKUP_BYTES;
|
|
281
|
+
const searchBytes = options.searchBytes ?? DEFAULT_SEARCH_BYTES;
|
|
282
|
+
const rewriteThresholdBytes =
|
|
283
|
+
options.rewriteThresholdBytes
|
|
284
|
+
?? envNonNegativeInt("PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES")
|
|
285
|
+
?? DEFAULT_REWRITE_THRESHOLD_BYTES;
|
|
286
|
+
const brokerOptions = {
|
|
287
|
+
maxRecords: options.maxRecords ?? 64,
|
|
288
|
+
maxBytes: options.maxBytes ?? 8 * 1024 * 1024,
|
|
289
|
+
globalMaxRecords: options.globalMaxRecords ?? envNonNegativeInt("PI_CONTEXT_BROKER_GLOBAL_MAX_RECORDS"),
|
|
290
|
+
globalMaxBytes: options.globalMaxBytes ?? envNonNegativeInt("PI_CONTEXT_BROKER_GLOBAL_MAX_BYTES"),
|
|
291
|
+
briefBytes,
|
|
292
|
+
};
|
|
293
|
+
const durable = options.durable ?? (envFlag("PI_CONTEXT_BROKER_DURABLE") || Boolean(options.storeDir ?? process.env.PI_CONTEXT_BROKER_STORE_DIR));
|
|
294
|
+
const durableBackend = String(process.env.PI_CONTEXT_BROKER_BACKEND ?? "sqlite").trim().toLowerCase();
|
|
295
|
+
const broker = durable
|
|
296
|
+
? durableBackend === "jsonl"
|
|
297
|
+
? createFileContextBroker({ ...brokerOptions, dir: options.storeDir ?? process.env.PI_CONTEXT_BROKER_STORE_DIR })
|
|
298
|
+
: (await import("./sqlite.js")).createSqliteContextBroker({ ...brokerOptions, dir: options.storeDir ?? process.env.PI_CONTEXT_BROKER_STORE_DIR })
|
|
299
|
+
: createInMemoryContextBroker(brokerOptions);
|
|
300
|
+
const seenSourceIds = new Set<string>();
|
|
301
|
+
const sourceHandles = new Map<string, string>();
|
|
302
|
+
let activeSessionId = process.cwd();
|
|
303
|
+
const routingTelemetry = {
|
|
304
|
+
contextHookCalls: 0,
|
|
305
|
+
contextHookToolResults: 0,
|
|
306
|
+
contextHookToolResultRewrites: 0,
|
|
307
|
+
contextHookToolResultHostile: 0,
|
|
308
|
+
contextHookBash: 0,
|
|
309
|
+
contextHookBashRewrites: 0,
|
|
310
|
+
contextHookBashHostile: 0,
|
|
311
|
+
toolResultEvents: 0,
|
|
312
|
+
toolResultArtifacts: 0,
|
|
313
|
+
backfillScans: 0,
|
|
314
|
+
backfillAdded: 0,
|
|
315
|
+
backfillErrors: 0,
|
|
316
|
+
toolLookupCalls: 0,
|
|
317
|
+
toolLookupExactCalls: 0,
|
|
318
|
+
toolLookupTextCalls: 0,
|
|
319
|
+
toolLookupHits: 0,
|
|
320
|
+
toolLookupMisses: 0,
|
|
321
|
+
commandLookupCalls: 0,
|
|
322
|
+
commandLookupExactCalls: 0,
|
|
323
|
+
commandLookupTextCalls: 0,
|
|
324
|
+
commandLookupHits: 0,
|
|
325
|
+
commandLookupMisses: 0,
|
|
326
|
+
exportCalls: 0,
|
|
327
|
+
pinCalls: 0,
|
|
328
|
+
statusCalls: 0,
|
|
329
|
+
pruneCalls: 0,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
function formatRoutingTelemetry(): string {
|
|
333
|
+
const line = [
|
|
334
|
+
`contextHook calls=${routingTelemetry.contextHookCalls}`,
|
|
335
|
+
`toolResults seen=${routingTelemetry.contextHookToolResults} rewritten=${routingTelemetry.contextHookToolResultRewrites} hostile=${routingTelemetry.contextHookToolResultHostile}`,
|
|
336
|
+
`bash seen=${routingTelemetry.contextHookBash} rewritten=${routingTelemetry.contextHookBashRewrites} hostile=${routingTelemetry.contextHookBashHostile}`,
|
|
337
|
+
`lookups tool(calls=${routingTelemetry.toolLookupCalls}, hits=${routingTelemetry.toolLookupHits}, misses=${routingTelemetry.toolLookupMisses})`,
|
|
338
|
+
`lookups slash(calls=${routingTelemetry.commandLookupCalls}, hits=${routingTelemetry.commandLookupHits}, misses=${routingTelemetry.commandLookupMisses})`,
|
|
339
|
+
`exports=${routingTelemetry.exportCalls}`,
|
|
340
|
+
`pins=${routingTelemetry.pinCalls}`,
|
|
341
|
+
`pruneCalls=${routingTelemetry.pruneCalls}`,
|
|
342
|
+
`backfill scans=${routingTelemetry.backfillScans} added=${routingTelemetry.backfillAdded} errors=${routingTelemetry.backfillErrors}`,
|
|
343
|
+
];
|
|
344
|
+
return `Context broker routing telemetry: ${line.join(", ")}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function publishToolArtifact(event: {
|
|
348
|
+
toolName: string;
|
|
349
|
+
input?: any;
|
|
350
|
+
content?: unknown;
|
|
351
|
+
details?: unknown;
|
|
352
|
+
isError?: boolean;
|
|
353
|
+
sourceId?: string;
|
|
354
|
+
createdAt?: number;
|
|
355
|
+
ttlMs?: number;
|
|
356
|
+
}): ContextArtifact | null {
|
|
357
|
+
if (!shouldBrokerToolName(event.toolName)) return null;
|
|
358
|
+
|
|
359
|
+
if (event.sourceId) {
|
|
360
|
+
const existingHandle = sourceHandles.get(event.sourceId);
|
|
361
|
+
if (existingHandle) {
|
|
362
|
+
const existing = broker.lookup({ handle: existingHandle })[0];
|
|
363
|
+
if (existing) return existing;
|
|
364
|
+
sourceHandles.delete(event.sourceId);
|
|
365
|
+
seenSourceIds.delete(event.sourceId);
|
|
366
|
+
}
|
|
367
|
+
if (seenSourceIds.has(event.sourceId)) seenSourceIds.delete(event.sourceId);
|
|
368
|
+
seenSourceIds.add(event.sourceId);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const sanitizedEvent = {
|
|
372
|
+
...event,
|
|
373
|
+
input: sanitizeValue(event.input) as any,
|
|
374
|
+
content: sanitizeValue(event.content),
|
|
375
|
+
details: sanitizeValue(event.details),
|
|
376
|
+
};
|
|
377
|
+
const payload = toolPayload(sanitizedEvent);
|
|
378
|
+
const bytes = Buffer.byteLength(payload, "utf8");
|
|
379
|
+
const hostilePayload = isHostilePayload(payload) || hasHostileValue(sanitizedEvent);
|
|
380
|
+
const artifact = broker.publish({
|
|
381
|
+
sessionId: activeSessionId,
|
|
382
|
+
kind: "tool_output",
|
|
383
|
+
payload,
|
|
384
|
+
summary: summarizeTool(sanitizedEvent, bytes, hostilePayload),
|
|
385
|
+
tags: [
|
|
386
|
+
event.toolName,
|
|
387
|
+
event.isError ? "error" : "ok",
|
|
388
|
+
event.sourceId ? "session-backfill" : "live",
|
|
389
|
+
...(hostilePayload ? ["hostile", "binary"] : []),
|
|
390
|
+
],
|
|
391
|
+
command: event.toolName === "bash" && typeof sanitizedEvent.input?.command === "string" ? sanitizedEvent.input.command : undefined,
|
|
392
|
+
paths: typeof sanitizedEvent.input?.path === "string" ? [sanitizedEvent.input.path] : [],
|
|
393
|
+
ttlMs: event.ttlMs ?? DEFAULT_TTL_MS,
|
|
394
|
+
parentIds: event.sourceId ? [event.sourceId] : [],
|
|
395
|
+
createdAt: event.createdAt,
|
|
396
|
+
});
|
|
397
|
+
if (artifact) routingTelemetry.toolResultArtifacts += 1;
|
|
398
|
+
if (event.sourceId) sourceHandles.set(event.sourceId, artifact.handle);
|
|
399
|
+
return artifact;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function collectToolInputs(entries: any[]): Map<string, { toolName?: string; input?: unknown }> {
|
|
403
|
+
const toolInputs = new Map<string, { toolName?: string; input?: unknown }>();
|
|
404
|
+
for (const entry of entries) {
|
|
405
|
+
const message = entry?.type === "message" ? entry.message : entry;
|
|
406
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
407
|
+
for (const block of message.content) {
|
|
408
|
+
if (block?.type === "toolCall" && typeof block.id === "string") {
|
|
409
|
+
toolInputs.set(block.id, { toolName: typeof block.name === "string" ? block.name : undefined, input: block.arguments });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return toolInputs;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function backfillSessionArtifacts(ctx: Partial<SessionContextLike>): { added: number; scanned: number; errors: number } {
|
|
417
|
+
activeSessionId = sessionIdFor(ctx);
|
|
418
|
+
let entries: any[] = [];
|
|
419
|
+
try {
|
|
420
|
+
entries = ctx.sessionManager?.getBranch?.() ?? [];
|
|
421
|
+
} catch {
|
|
422
|
+
return { added: 0, scanned: 0, errors: 1 };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const toolInputs = collectToolInputs(entries);
|
|
426
|
+
|
|
427
|
+
let added = 0;
|
|
428
|
+
let scanned = 0;
|
|
429
|
+
let errors = 0;
|
|
430
|
+
|
|
431
|
+
for (const entry of entries) {
|
|
432
|
+
try {
|
|
433
|
+
const entryId = typeof entry?.id === "string" ? entry.id : undefined;
|
|
434
|
+
const createdAt = messageTimestamp(entry);
|
|
435
|
+
|
|
436
|
+
if (entry?.type === "message" && entry.message?.role === "toolResult") {
|
|
437
|
+
scanned += 1;
|
|
438
|
+
routingTelemetry.backfillScans += 1;
|
|
439
|
+
const sourceId = typeof entry.message.toolCallId === "string" ? entry.message.toolCallId : entryId;
|
|
440
|
+
const toolInput = sourceId ? toolInputs.get(sourceId) : undefined;
|
|
441
|
+
const alreadySeen = sourceId ? seenSourceIds.has(sourceId) || sourceHandles.has(sourceId) : false;
|
|
442
|
+
if (publishToolArtifact({
|
|
443
|
+
toolName: String(entry.message.toolName ?? toolInput?.toolName ?? "tool"),
|
|
444
|
+
input: entry.message.input ?? toolInput?.input,
|
|
445
|
+
content: entry.message.content,
|
|
446
|
+
details: entry.message.details,
|
|
447
|
+
isError: Boolean(entry.message.isError),
|
|
448
|
+
sourceId,
|
|
449
|
+
createdAt,
|
|
450
|
+
ttlMs: ttlFromNowFor(createdAt),
|
|
451
|
+
}) && !alreadySeen) {
|
|
452
|
+
added += 1;
|
|
453
|
+
routingTelemetry.backfillAdded += 1;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (entry?.type === "message" && entry.message?.role === "bashExecution") {
|
|
458
|
+
if (entry.message.excludeFromContext === true) continue;
|
|
459
|
+
scanned += 1;
|
|
460
|
+
routingTelemetry.backfillScans += 1;
|
|
461
|
+
const sourceId = entryId;
|
|
462
|
+
const alreadySeen = sourceId ? seenSourceIds.has(sourceId) || sourceHandles.has(sourceId) : false;
|
|
463
|
+
if (publishToolArtifact({
|
|
464
|
+
toolName: "bash",
|
|
465
|
+
input: { command: entry.message.command },
|
|
466
|
+
content: entry.message.output,
|
|
467
|
+
details: {
|
|
468
|
+
exitCode: entry.message.exitCode,
|
|
469
|
+
cancelled: entry.message.cancelled,
|
|
470
|
+
truncated: entry.message.truncated,
|
|
471
|
+
fullOutputPath: entry.message.fullOutputPath,
|
|
472
|
+
},
|
|
473
|
+
isError: typeof entry.message.exitCode === "number" ? entry.message.exitCode !== 0 : Boolean(entry.message.cancelled),
|
|
474
|
+
sourceId,
|
|
475
|
+
createdAt,
|
|
476
|
+
ttlMs: ttlFromNowFor(createdAt),
|
|
477
|
+
}) && !alreadySeen) {
|
|
478
|
+
added += 1;
|
|
479
|
+
routingTelemetry.backfillAdded += 1;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
errors += 1;
|
|
484
|
+
routingTelemetry.backfillErrors += 1;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return { added, scanned, errors };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function currentBrief(): string {
|
|
492
|
+
return broker.renderBrief({ sessionId: activeSessionId, budgetBytes: briefBytes });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
p.__piRogueContextBroker = {
|
|
496
|
+
renderBrief: currentBrief,
|
|
497
|
+
lookup: broker.lookup,
|
|
498
|
+
status: broker.status,
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const contextActions: AutocompleteItem[] = [
|
|
502
|
+
{ value: "status", label: "status", description: "Show broker record, byte, and pinned counts" },
|
|
503
|
+
{ value: "brief", label: "brief", description: "Show the bounded broker brief" },
|
|
504
|
+
{ value: "lookup ", label: "lookup", description: "Lookup by ctx:// handle or current-session text" },
|
|
505
|
+
{ value: "pin ", label: "pin", description: "Pin an artifact by ctx:// handle or id" },
|
|
506
|
+
{ value: "export ", label: "export", description: "Export full payload for a ctx:// handle or id" },
|
|
507
|
+
{ value: "prune", label: "prune", description: "Run TTL/cap pruning now" },
|
|
508
|
+
];
|
|
509
|
+
|
|
510
|
+
function artifactCompletions(action: "lookup" | "pin" | "export", query: string): AutocompleteItem[] {
|
|
511
|
+
const needle = query.trim().toLowerCase();
|
|
512
|
+
return broker.lookup({ sessionId: activeSessionId, limit: 10 })
|
|
513
|
+
.filter((artifact) => {
|
|
514
|
+
if (!needle) return true;
|
|
515
|
+
return artifact.handle.toLowerCase().includes(needle)
|
|
516
|
+
|| artifact.summary.toLowerCase().includes(needle)
|
|
517
|
+
|| artifact.kind.toLowerCase().includes(needle)
|
|
518
|
+
|| artifact.tags.join(" ").toLowerCase().includes(needle)
|
|
519
|
+
|| artifact.paths.join(" ").toLowerCase().includes(needle);
|
|
520
|
+
})
|
|
521
|
+
.map((artifact) => ({
|
|
522
|
+
value: `${action} ${artifact.handle}`,
|
|
523
|
+
label: `${action} ${artifact.kind}`,
|
|
524
|
+
description: `${artifact.pinned ? "pinned; " : ""}${artifact.summary}`,
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function contextArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
|
|
529
|
+
const prefix = argumentPrefix.trimStart();
|
|
530
|
+
const [action = "", ...restParts] = prefix.split(/\s+/);
|
|
531
|
+
const hasActionSeparator = /\s/.test(prefix);
|
|
532
|
+
|
|
533
|
+
if (!action || !hasActionSeparator) {
|
|
534
|
+
const items = contextActions.filter((item) => item.value.trim().startsWith(action));
|
|
535
|
+
return items.length ? items : contextActions;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (action === "lookup" || action === "pin" || action === "export") {
|
|
539
|
+
const items = artifactCompletions(action, restParts.join(" "));
|
|
540
|
+
return items.length ? items : null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
547
|
+
const { added, scanned, errors } = backfillSessionArtifacts(ctx);
|
|
548
|
+
ctx.ui.setStatus?.("context-broker", "ctx:on");
|
|
549
|
+
ctx.ui.notify(
|
|
550
|
+
`Context broker enabled. Backfilled ${added}/${scanned} current-branch tool artifacts${errors ? ` (${errors} malformed skipped)` : ""}. Use /context status or /context brief.`,
|
|
551
|
+
errors ? "warning" : "info",
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
556
|
+
activeSessionId = sessionIdFor(ctx);
|
|
557
|
+
const before = broker.status();
|
|
558
|
+
const after = broker.purge({ sessionId: activeSessionId, keepPinned: true });
|
|
559
|
+
seenSourceIds.clear();
|
|
560
|
+
sourceHandles.clear();
|
|
561
|
+
const removed = before.records - after.records;
|
|
562
|
+
if (removed > 0) ctx.ui.notify(`Context broker compact cleanup purged ${removed} unpinned artifact${removed === 1 ? "" : "s"}; pinned artifacts retained.`, "info");
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
pi.on("tool_result", async (event: ToolResultEvent, ctx) => {
|
|
566
|
+
activeSessionId = sessionIdFor(ctx);
|
|
567
|
+
routingTelemetry.toolResultEvents += 1;
|
|
568
|
+
publishToolArtifact({ ...event, sourceId: event.toolCallId });
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
pi.on("context", async (event, ctx) => {
|
|
572
|
+
activeSessionId = sessionIdFor(ctx);
|
|
573
|
+
routingTelemetry.contextHookCalls += 1;
|
|
574
|
+
const toolInputs = collectToolInputs(event.messages);
|
|
575
|
+
const drafts = event.messages.map((message: any): { original: any; replacement?: any; artifact?: ContextArtifact; rewrite?: (artifact: ContextArtifact) => any; safeFallback?: any } => {
|
|
576
|
+
if (message?.role === "toolResult") {
|
|
577
|
+
routingTelemetry.contextHookToolResults += 1;
|
|
578
|
+
const raw = contentText(message.content);
|
|
579
|
+
const toolInput = typeof message.toolCallId === "string" ? toolInputs.get(message.toolCallId) : undefined;
|
|
580
|
+
const toolName = String(message.toolName ?? toolInput?.toolName ?? "tool");
|
|
581
|
+
const hostile = hasHostileText(raw) || hasHostileValue(message.content);
|
|
582
|
+
if (hostile) routingTelemetry.contextHookToolResultHostile += 1;
|
|
583
|
+
const shouldRewrite = Buffer.byteLength(raw, "utf8") > rewriteThresholdBytes || hostile;
|
|
584
|
+
if (!shouldRewrite) return { original: message };
|
|
585
|
+
if (!shouldBrokerToolName(toolName)) {
|
|
586
|
+
return { original: message, replacement: { ...message, content: [{ type: "text", text: contextLookupHistoryPlaceholder() }] } };
|
|
587
|
+
}
|
|
588
|
+
const artifact = publishToolArtifact({
|
|
589
|
+
toolName,
|
|
590
|
+
input: message.input ?? toolInput?.input,
|
|
591
|
+
content: message.content,
|
|
592
|
+
details: message.details,
|
|
593
|
+
isError: Boolean(message.isError),
|
|
594
|
+
sourceId: typeof message.toolCallId === "string" ? message.toolCallId : undefined,
|
|
595
|
+
createdAt: typeof message.timestamp === "number" ? message.timestamp : undefined,
|
|
596
|
+
ttlMs: ttlFromNowFor(typeof message.timestamp === "number" ? message.timestamp : undefined),
|
|
597
|
+
});
|
|
598
|
+
if (!artifact) return { original: message };
|
|
599
|
+
routingTelemetry.contextHookToolResultRewrites += 1;
|
|
600
|
+
return {
|
|
601
|
+
original: message,
|
|
602
|
+
artifact,
|
|
603
|
+
rewrite: (live) => ({ ...message, content: [{ type: "text", text: brokerPlaceholder(live) }] }),
|
|
604
|
+
safeFallback: { ...message, content: [{ type: "text", text: prunedPayloadPlaceholder(hostile) }] },
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (message?.role === "bashExecution" && message.excludeFromContext !== true) {
|
|
609
|
+
routingTelemetry.contextHookBash += 1;
|
|
610
|
+
const raw = String(message.output ?? "");
|
|
611
|
+
const hostile = hasHostileText(raw) || hasHostileValue(message.output);
|
|
612
|
+
if (hostile) routingTelemetry.contextHookBashHostile += 1;
|
|
613
|
+
const shouldRewrite = Buffer.byteLength(raw, "utf8") > rewriteThresholdBytes || hostile;
|
|
614
|
+
if (!shouldRewrite) return { original: message };
|
|
615
|
+
const sourceId = typeof message.timestamp === "number"
|
|
616
|
+
? `bash:${message.timestamp}:${stableHash([message.command ?? "", raw, message.exitCode ?? "", message.cancelled ?? ""].join("\n"))}`
|
|
617
|
+
: `bash:${stableHash([message.command ?? "", raw, message.exitCode ?? "", message.cancelled ?? ""].join("\n"))}`;
|
|
618
|
+
const artifact = publishToolArtifact({
|
|
619
|
+
toolName: "bash",
|
|
620
|
+
input: { command: message.command },
|
|
621
|
+
content: message.output,
|
|
622
|
+
details: {
|
|
623
|
+
exitCode: message.exitCode,
|
|
624
|
+
cancelled: message.cancelled,
|
|
625
|
+
truncated: message.truncated,
|
|
626
|
+
fullOutputPath: message.fullOutputPath,
|
|
627
|
+
},
|
|
628
|
+
isError: typeof message.exitCode === "number" ? message.exitCode !== 0 : Boolean(message.cancelled),
|
|
629
|
+
sourceId,
|
|
630
|
+
createdAt: typeof message.timestamp === "number" ? message.timestamp : undefined,
|
|
631
|
+
ttlMs: ttlFromNowFor(typeof message.timestamp === "number" ? message.timestamp : undefined),
|
|
632
|
+
});
|
|
633
|
+
if (!artifact) return { original: message };
|
|
634
|
+
routingTelemetry.contextHookBashRewrites += 1;
|
|
635
|
+
return {
|
|
636
|
+
original: message,
|
|
637
|
+
artifact,
|
|
638
|
+
rewrite: (live) => ({ ...message, output: brokerPlaceholder(live), truncated: true }),
|
|
639
|
+
safeFallback: { ...message, output: prunedPayloadPlaceholder(hostile), truncated: true },
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return { original: message };
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
let changed = false;
|
|
647
|
+
const messages = drafts.map((draft) => {
|
|
648
|
+
if (draft.replacement) {
|
|
649
|
+
changed = true;
|
|
650
|
+
return draft.replacement;
|
|
651
|
+
}
|
|
652
|
+
if (!draft.artifact || !draft.rewrite) return draft.original;
|
|
653
|
+
const live = broker.lookup({ handle: draft.artifact.handle })[0];
|
|
654
|
+
if (!live) {
|
|
655
|
+
for (const parentId of draft.artifact.parentIds) sourceHandles.delete(parentId);
|
|
656
|
+
if (draft.safeFallback) {
|
|
657
|
+
changed = true;
|
|
658
|
+
return draft.safeFallback;
|
|
659
|
+
}
|
|
660
|
+
return draft.original;
|
|
661
|
+
}
|
|
662
|
+
changed = true;
|
|
663
|
+
return draft.rewrite(live);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
return changed ? { messages } : undefined;
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
pi.on("before_agent_start", async (event) => {
|
|
670
|
+
const brief = currentBrief();
|
|
671
|
+
if (!brief.includes("ctx://")) return;
|
|
672
|
+
return {
|
|
673
|
+
systemPrompt: [
|
|
674
|
+
event.systemPrompt,
|
|
675
|
+
brief,
|
|
676
|
+
"Context broker rule: use /context lookup <handle> for exact evidence when a broker handle is relevant. Broker briefs are bounded summaries and never raw payload dumps.",
|
|
677
|
+
].join("\n\n"),
|
|
678
|
+
};
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
pi.registerTool({
|
|
682
|
+
name: "context_lookup",
|
|
683
|
+
label: "Context Lookup",
|
|
684
|
+
description: "Lookup exact or searchable context broker artifacts by handle, current-session text, path, tag, kind, or tier.",
|
|
685
|
+
promptSnippet: "context_lookup: retrieve context broker artifacts by ctx:// handle or focused filters before asking the user to repeat prior tool output.",
|
|
686
|
+
promptGuidelines: [
|
|
687
|
+
"Use context_lookup when a ctx:// handle is relevant and exact evidence is needed.",
|
|
688
|
+
"Do not paste large raw broker payloads unless the user explicitly asks; summarize and cite handles instead.",
|
|
689
|
+
],
|
|
690
|
+
parameters: Type.Object({
|
|
691
|
+
handle: Type.Optional(Type.String({ description: "Exact ctx:// handle to retrieve" })),
|
|
692
|
+
text: Type.Optional(Type.String({ description: "Current-session text search over broker summaries and indexed payload text" })),
|
|
693
|
+
path: Type.Optional(Type.String({ description: "File or directory path filter" })),
|
|
694
|
+
tag: Type.Optional(Type.String({ description: "Artifact tag filter" })),
|
|
695
|
+
kind: Type.Optional(Type.String({ enum: ["tool_output", "diff", "file_snapshot", "subagent_result", "advisor_brief", "memory_note"] })),
|
|
696
|
+
tier: Type.Optional(Type.String({ enum: ["hot", "warm", "cold"] })),
|
|
697
|
+
limit: Type.Optional(Type.Number({ minimum: 1, maximum: 10, description: "Maximum artifacts to return" })),
|
|
698
|
+
}),
|
|
699
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
700
|
+
activeSessionId = sessionIdFor(ctx);
|
|
701
|
+
routingTelemetry.toolLookupCalls += 1;
|
|
702
|
+
const p = params as { handle?: string; text?: string; path?: string; tag?: string; kind?: any; tier?: any; limit?: number };
|
|
703
|
+
const exact = typeof p.handle === "string" && p.handle.startsWith("ctx://");
|
|
704
|
+
routingTelemetry.toolLookupExactCalls += exact ? 1 : 0;
|
|
705
|
+
routingTelemetry.toolLookupTextCalls += exact ? 0 : 1;
|
|
706
|
+
const focused = exact || Boolean(p.text?.trim() || p.path?.trim() || p.tag?.trim() || p.kind || p.tier);
|
|
707
|
+
if (!focused) {
|
|
708
|
+
return textResult("context_lookup requires a focused filter: handle, text, path, tag, kind, or tier. Empty lookups are refused to avoid dumping brokered payloads into the prompt.");
|
|
709
|
+
}
|
|
710
|
+
const results = broker.lookup({
|
|
711
|
+
handle: exact ? p.handle : undefined,
|
|
712
|
+
sessionId: exact ? undefined : activeSessionId,
|
|
713
|
+
text: exact ? undefined : p.text,
|
|
714
|
+
path: p.path,
|
|
715
|
+
tag: p.tag,
|
|
716
|
+
kind: p.kind,
|
|
717
|
+
tier: p.tier,
|
|
718
|
+
limit: Math.min(10, Math.max(1, Math.floor(p.limit ?? (exact ? 1 : 5)))),
|
|
719
|
+
});
|
|
720
|
+
if (!results.length) {
|
|
721
|
+
routingTelemetry.toolLookupMisses += 1;
|
|
722
|
+
return textResult("No context artifacts matched. Missing or expired handles should be reported explicitly.");
|
|
723
|
+
}
|
|
724
|
+
routingTelemetry.toolLookupHits += 1;
|
|
725
|
+
return textResult(results.map((item) => renderLookupOutput(item, exact ? lookupBytes : searchBytes)).join("\n\n---\n\n"));
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
pi.registerCommand("context", {
|
|
730
|
+
description: "Inspect the context broker: status | brief | lookup <handle-or-text> | pin <handle-or-id> | export <handle-or-id> | prune",
|
|
731
|
+
getArgumentCompletions: contextArgumentCompletions,
|
|
732
|
+
handler: async (args, ctx) => {
|
|
733
|
+
activeSessionId = sessionIdFor(ctx);
|
|
734
|
+
const [action = "status", ...rest] = String(args || "").trim().split(/\s+/).filter(Boolean);
|
|
735
|
+
const query = rest.join(" ");
|
|
736
|
+
|
|
737
|
+
if (action === "status") {
|
|
738
|
+
routingTelemetry.statusCalls += 1;
|
|
739
|
+
const status = broker.status();
|
|
740
|
+
ctx.ui.notify(
|
|
741
|
+
`Context broker: enabled, session=${activeSessionId}, records=${status.records}, bytes=${status.bytes}/${status.maxBytes}, tiers=hot:${status.hotRecords}/${status.hotBytes} warm:${status.warmRecords}/${status.warmBytes} cold:${status.coldRecords}/${status.coldBytes}, pinned=${status.pinnedRecords}/${status.pinnedBytes} bytes`,
|
|
742
|
+
"info",
|
|
743
|
+
);
|
|
744
|
+
ctx.ui.notify(formatRoutingTelemetry(), "info");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (action === "brief") {
|
|
749
|
+
ctx.ui.notify(currentBrief(), "info");
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (action === "lookup") {
|
|
754
|
+
if (!query) {
|
|
755
|
+
ctx.ui.notify("Usage: /context lookup <ctx://handle-or-text>", "warning");
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
routingTelemetry.commandLookupCalls += 1;
|
|
759
|
+
const exact = query.startsWith("ctx://");
|
|
760
|
+
routingTelemetry.commandLookupExactCalls += exact ? 1 : 0;
|
|
761
|
+
routingTelemetry.commandLookupTextCalls += exact ? 0 : 1;
|
|
762
|
+
const results = broker.lookup(exact ? { handle: query } : { sessionId: activeSessionId, text: query, limit: 5 });
|
|
763
|
+
if (results.length) {
|
|
764
|
+
routingTelemetry.commandLookupHits += 1;
|
|
765
|
+
} else {
|
|
766
|
+
routingTelemetry.commandLookupMisses += 1;
|
|
767
|
+
}
|
|
768
|
+
ctx.ui.notify(results.length ? results.map((item) => renderLookupOutput(item, exact ? lookupBytes : searchBytes)).join("\n\n---\n\n") : "No context artifacts matched.", "info");
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (action === "pin") {
|
|
773
|
+
if (!query) {
|
|
774
|
+
ctx.ui.notify("Usage: /context pin <ctx://handle-or-id>", "warning");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const pinned = broker.pin(query, true);
|
|
778
|
+
routingTelemetry.pinCalls += 1;
|
|
779
|
+
ctx.ui.notify(pinned ? `Pinned ${pinned.handle}` : "No artifact matched that handle/id.", pinned ? "info" : "warning");
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (action === "export") {
|
|
784
|
+
if (!query) {
|
|
785
|
+
ctx.ui.notify("Usage: /context export <ctx://handle-or-id>", "warning");
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const exact = query.startsWith("ctx://");
|
|
790
|
+
const artifact = exact ? broker.lookup({ handle: query })[0] : broker.lookup({ id: query })[0];
|
|
791
|
+
if (!artifact) {
|
|
792
|
+
ctx.ui.notify("No artifact matched that handle-or-id.", "warning");
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const exportDir = mkdtempSync(join(tmpdir(), "pi-context-broker-export-"));
|
|
797
|
+
const exportPath = join(exportDir, `${artifact.id}.txt`);
|
|
798
|
+
writeFileSync(exportPath, artifact.payload, "utf8");
|
|
799
|
+
routingTelemetry.exportCalls += 1;
|
|
800
|
+
ctx.ui.notify(`Exported full payload for ${sanitizeForPrompt(artifact.handle)} (${artifact.bytes} bytes) to ${exportPath}`, "info");
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (action === "prune") {
|
|
805
|
+
routingTelemetry.pruneCalls += 1;
|
|
806
|
+
const status = broker.prune();
|
|
807
|
+
ctx.ui.notify(`Pruned. ${status.records} records, ${status.bytes} bytes remain.`, "info");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
ctx.ui.notify("Usage: /context status | brief | lookup <handle-or-text> | pin <handle-or-id> | export <handle-or-id> | prune", "warning");
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export function shouldEnableContextBrokerBeta(options: ContextBrokerBetaOptions = {}): boolean {
|
|
817
|
+
return Boolean(options.enabled ?? isEnvEnabled());
|
|
818
|
+
}
|