pi-blackhole 0.3.9 → 0.4.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.
@@ -62,6 +62,14 @@ export interface UnifiedConfig {
62
62
  * "pi-default" — Pi's built-in summarization */
63
63
  compactionEngine: "blackhole" | "pi-default";
64
64
 
65
+ /** Mid-run auto-compaction (turn_end trigger, fires while the agent is still
66
+ * executing tool loops — agent_end alone never fires during long runs).
67
+ * "resume" — compact at threshold and inject a resume message so the agent
68
+ * continues the task (default)
69
+ * "pause" — compact at threshold but stop; user continues manually
70
+ * "off" — only evaluate the threshold when the agent finishes a run */
71
+ midRunCompaction: "resume" | "pause" | "off";
72
+
65
73
  /** How much recent transcript to keep visible after compaction.
66
74
  * "pi-default" — use Pi's firstKeptEntryId (respects Pi's keepRecentTokens)
67
75
  * "minimal" — keep only last user message (current agressive pi-vcc behavior)
@@ -76,6 +84,9 @@ export interface UnifiedConfig {
76
84
  compactAfterTokens: number;
77
85
  /** Observation pool token pressure for full fold. */
78
86
  observationsPoolMaxTokens: number;
87
+ /** Treat every compaction as a full-fold boundary so early reflections/drops
88
+ * survive the first compaction in a fresh session. Default true. */
89
+ fullFoldAlways: boolean;
79
90
  /** Target token budget for the observation pool (dropper aims here).
80
91
  * Optional; defaults to half of observationsPoolMaxTokens when unset.
81
92
  * Must be less than observationsPoolMaxTokens.
@@ -146,11 +157,13 @@ export const DEFAULTS: UnifiedConfig = {
146
157
  compaction: "auto",
147
158
  compactionEngine: "blackhole",
148
159
  tailBehavior: "minimal",
160
+ midRunCompaction: "resume",
149
161
 
150
162
  observeAfterTokens: 15_000,
151
163
  reflectAfterTokens: 25_000,
152
164
  compactAfterTokens: 81_000,
153
165
  observationsPoolMaxTokens: 20_000,
166
+ fullFoldAlways: true,
154
167
  observationsPoolTargetTokens: 10_000,
155
168
  reflectorInputMaxTokens: 80_000,
156
169
  dropperInputMaxTokens: 80_000,
@@ -171,6 +184,7 @@ const THINKING_LEVELS: readonly string[] = ["off", "minimal", "low", "medium", "
171
184
  const COMPACTION_VALUES = ["auto", "manual", "off"] as const;
172
185
  const COMPACTION_ENGINE_VALUES = ["blackhole", "pi-default"] as const;
173
186
  const TAIL_BEHAVIOR_VALUES = ["pi-default", "minimal"] as const;
187
+ const MID_RUN_COMPACTION_VALUES = ["resume", "pause", "off"] as const;
174
188
 
175
189
  function isCompaction(v: unknown): v is "auto" | "manual" | "off" {
176
190
  return typeof v === "string" && (COMPACTION_VALUES as readonly string[]).includes(v);
@@ -181,6 +195,9 @@ function isCompactionEngine(v: unknown): v is "blackhole" | "pi-default" {
181
195
  function isTailBehavior(v: unknown): v is "pi-default" | "minimal" {
182
196
  return typeof v === "string" && (TAIL_BEHAVIOR_VALUES as readonly string[]).includes(v);
183
197
  }
198
+ function isMidRunCompaction(v: unknown): v is "resume" | "pause" | "off" {
199
+ return typeof v === "string" && (MID_RUN_COMPACTION_VALUES as readonly string[]).includes(v);
200
+ }
184
201
 
185
202
  function isRecord(v: unknown): v is Record<string, unknown> {
186
203
  return typeof v === "object" && v !== null;
@@ -230,6 +247,7 @@ function parseConfig(raw: Record<string, unknown>): Partial<UnifiedConfig> {
230
247
  if (isCompaction(raw.compaction)) c.compaction = raw.compaction;
231
248
  if (isCompactionEngine(raw.compactionEngine)) c.compactionEngine = raw.compactionEngine;
232
249
  if (isTailBehavior(raw.tailBehavior)) c.tailBehavior = raw.tailBehavior;
250
+ if (isMidRunCompaction(raw.midRunCompaction)) c.midRunCompaction = raw.midRunCompaction;
233
251
 
234
252
  // Booleans — pi-vcc
235
253
  if (typeof raw.overrideDefaultCompaction === "boolean") c.overrideDefaultCompaction = raw.overrideDefaultCompaction;
@@ -240,6 +258,7 @@ function parseConfig(raw: Record<string, unknown>): Partial<UnifiedConfig> {
240
258
  if (typeof raw.noAutoCompact === "boolean") c.noAutoCompact = raw.noAutoCompact;
241
259
  if (typeof raw.passive === "boolean") c.passive = raw.passive;
242
260
  if (typeof raw.memory === "boolean") c.memory = raw.memory;
261
+ if (typeof raw.fullFoldAlways === "boolean") c.fullFoldAlways = raw.fullFoldAlways;
243
262
  if (typeof raw.debugLog === "boolean") c.debugLog = raw.debugLog;
244
263
 
245
264
  // Numeric fields — use nonNegativeInt for observerPreambleMaxTokens (0 = auto)
@@ -319,35 +338,54 @@ function migrateOldKnobs(parsed: Record<string, unknown>): void {
319
338
 
320
339
  // ── Load and save ────────────────────────────────────────────────────────────
321
340
 
322
- function readJson(path: string): Record<string, unknown> | null {
323
- if (!existsSync(path)) return null;
341
+ function readJson(path: string): { data: Record<string, unknown> | null; error: string | null } {
342
+ if (!existsSync(path)) return { data: null, error: null };
324
343
  try {
325
- return JSON.parse(readFileSync(path, "utf-8"));
326
- } catch {
327
- return null;
344
+ return { data: JSON.parse(readFileSync(path, "utf-8")), error: null };
345
+ } catch (e) {
346
+ const msg = `blackhole: config file at ${path} has invalid JSON: ${(e as Error).message}. Using defaults.`;
347
+ console.warn(msg);
348
+ return { data: null, error: msg };
328
349
  }
329
350
  }
330
351
 
352
+ /** Optional warning callback invoked when the primary config file has invalid JSON.
353
+ * Receives the warning message string. Used by callers with UI access to surface
354
+ * the error via ctx.ui.notify(message, "warning").
355
+ */
356
+ type WarnFn = (message: string) => void;
357
+
331
358
  /**
332
359
  * Load unified configuration from ~/.pi/agent/pi-blackhole/pi-blackhole-config.json.
333
360
  * Falls back to legacy sources if the unified file doesn't exist.
334
361
  */
335
- export function loadUnifiedConfig(cwd: string): UnifiedConfig {
362
+ export function loadUnifiedConfig(cwd: string, onWarn?: WarnFn): UnifiedConfig {
336
363
  const path = configPath();
337
- let raw = readJson(path);
364
+ let raw: Record<string, unknown> | null;
365
+ let primaryError: string | null = null;
366
+ const result = readJson(path);
367
+ raw = result.data;
368
+ primaryError = result.error;
369
+ if (primaryError && onWarn) onWarn(primaryError);
338
370
 
339
371
  // Fallback to legacy sources if unified file doesn't exist
340
372
  if (!raw) {
341
373
  // Try legacy pi-vcc config
342
374
  const piVccPath = join(getAgentDir(), "pi-vcc-config.json");
343
- const piVccRaw = readJson(piVccPath);
375
+ const piVccResult = readJson(piVccPath);
376
+ const piVccRaw = piVccResult.data;
377
+ if (piVccResult.error && onWarn) onWarn(piVccResult.error);
344
378
 
345
379
  // Try legacy om config from settings.json
346
380
  const settingsPath = join(getAgentDir(), "settings.json");
347
- const settingsRaw = readJson(settingsPath);
381
+ const settingsResult = readJson(settingsPath);
382
+ const settingsRaw = settingsResult.data;
383
+ if (settingsResult.error && onWarn) onWarn(settingsResult.error);
348
384
  const omRaw = settingsRaw?.["pi-blackhole"] ?? settingsRaw?.["observational-memory"];
349
385
  const projectSettingsPath = join(cwd, ".pi", "settings.json");
350
- const projectRaw = readJson(projectSettingsPath);
386
+ const projectResult = readJson(projectSettingsPath);
387
+ const projectRaw = projectResult.data;
388
+ if (projectResult.error && onWarn) onWarn(projectResult.error);
351
389
  const projectOmRaw = projectRaw?.["pi-blackhole"] ?? projectRaw?.["observational-memory"];
352
390
 
353
391
  // Merge legacy sources
@@ -449,7 +487,11 @@ export function saveUnifiedConfig(settings: Partial<UnifiedConfig>): boolean {
449
487
  const path = configPath();
450
488
  const dir = dirname(path);
451
489
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
452
- const existing = readJson(path) ?? {};
490
+ const existingResult = readJson(path);
491
+ const existing = existingResult.data ?? {};
492
+ if (existingResult.error) {
493
+ console.warn("blackhole: overwriting corrupt config file at " + path);
494
+ }
453
495
  const next = { ...existing, ...settings };
454
496
  writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`);
455
497
  return true;
@@ -18,41 +18,84 @@ const firstLineOf = (text: string): string => {
18
18
  const cleanMessage = (msg: string): string =>
19
19
  msg.replace(/\\"/g, '"').replace(/\\'/g, "'").trim();
20
20
 
21
+ /** Extract commit hash from git output text (tool_result or bash output). */
22
+ const extractHashFromOutput = (text: string): string | undefined => {
23
+ const bracket = text.match(/\[\S+\s+([0-9a-f]{7,12})\]/);
24
+ if (bracket) return bracket[1];
25
+ const range = text.match(/\b([0-9a-f]{7,12})\.\.([0-9a-f]{7,12})\b/);
26
+ if (range) return range[2];
27
+ const plain = text.match(HASH_RE);
28
+ if (plain) return plain[1];
29
+ return undefined;
30
+ };
31
+
32
+ /** Try to extract a commit message from a git commit command string. */
33
+ const tryExtractMessage = (cmd: string): string | undefined => {
34
+ if (!/\bgit\s+commit\b/.test(cmd)) return undefined;
35
+ const m = cmd.match(COMMIT_MSG_RE);
36
+ if (!m) return undefined;
37
+ const message = firstLineOf(cleanMessage(m[1] ?? m[2] ?? m[3] ?? ""));
38
+ return message || undefined;
39
+ };
40
+
21
41
  /**
22
- * Extract git commits from bash tool calls (`git commit -m "..."`) and pair
23
- * with hash from the immediately following tool_result.
42
+ * Extract git commits from bash tool calls, bash execution messages,
43
+ * and user messages that wrap bash execution output (post-convertToLlm).
44
+ *
45
+ * Handles three block kinds:
46
+ * - tool_call (name: "bash") — agent tool call to the bash tool
47
+ * - bash — pi's internal bashExecution message
48
+ * - user — convertToLlm wraps bashExecution as "Ran `cmd`\n```\noutput\n```"
24
49
  */
25
50
  export const extractCommits = (blocks: NormalizedBlock[]): CommitInfo[] => {
26
51
  const commits: CommitInfo[] = [];
52
+ const addCommit = (hash: string | undefined, message: string) => {
53
+ const key = `${hash ?? ""}::${message}`;
54
+ if (!commits.some((c) => `${c.hash ?? ""}::${c.message}` === key)) {
55
+ commits.push({ hash, message });
56
+ }
57
+ };
27
58
 
28
59
  for (let i = 0; i < blocks.length; i++) {
29
60
  const b = blocks[i];
30
- if (b.kind !== "tool_call" || b.name !== "bash") continue;
31
- const cmd = typeof b.args.command === "string" ? b.args.command : "";
32
- if (!/\bgit\s+commit\b/.test(cmd)) continue;
33
- const m = cmd.match(COMMIT_MSG_RE);
34
- if (!m) continue;
35
- const message = firstLineOf(cleanMessage(m[1] ?? m[2] ?? m[3] ?? ""));
36
- if (!message) continue;
37
61
 
38
- let hash: string | undefined;
39
- // Look at next tool_result for hash
40
- for (let j = i + 1; j < Math.min(blocks.length, i + 3); j++) {
41
- const r = blocks[j];
42
- if (r.kind !== "tool_result") continue;
43
- // Common git commit output: `[branch <hash>] message` or `<branch> <hash>..<hash>`
44
- const bracket = r.text.match(/\[\S+\s+([0-9a-f]{7,12})\]/);
45
- if (bracket) { hash = bracket[1]; break; }
46
- const range = r.text.match(/\b([0-9a-f]{7,12})\.\.([0-9a-f]{7,12})\b/);
47
- if (range) { hash = range[2]; break; }
48
- const plain = r.text.match(HASH_RE);
49
- if (plain) { hash = plain[1]; break; }
62
+ // ── Case 1: tool_call (agent calls bash tool) ──
63
+ if (b.kind === "tool_call" && b.name === "bash") {
64
+ const cmd = (b.args && typeof b.args.command === "string") ? b.args.command : "";
65
+ const message = tryExtractMessage(cmd);
66
+ if (!message) continue;
67
+
68
+ let hash: string | undefined;
69
+ for (let j = i + 1; j < Math.min(blocks.length, i + 3); j++) {
70
+ const r = blocks[j];
71
+ if (r.kind !== "tool_result") continue;
72
+ hash = extractHashFromOutput(r.text);
73
+ if (hash) break;
74
+ }
75
+ addCommit(hash, message);
76
+ continue;
50
77
  }
51
78
 
52
- // Dedup by message+hash
53
- const key = `${hash ?? ""}::${message}`;
54
- if (!commits.some((c) => `${c.hash ?? ""}::${c.message}` === key)) {
55
- commits.push({ hash, message });
79
+ // ── Case 2: bash execution message ──
80
+ if (b.kind === "bash") {
81
+ const message = tryExtractMessage(b.command);
82
+ if (!message) continue;
83
+ const hash = extractHashFromOutput(b.output);
84
+ addCommit(hash, message);
85
+ continue;
86
+ }
87
+
88
+ // ── Case 3: user message wrapping a bash execution (post-convertToLlm) ──
89
+ if (b.kind === "user") {
90
+ // Detect "Ran `git commit -m "..."`" pattern in user text
91
+ const ranCmd = b.text.match(/Ran\s+`((?:[^`\\]|\\.)*)`/);
92
+ if (!ranCmd) continue;
93
+ const message = tryExtractMessage(ranCmd[1]);
94
+ if (!message) continue;
95
+ // Extract output from the code block following the command
96
+ const codeBlock = b.text.match(/```\n([\s\S]*?)```/);
97
+ const hash = codeBlock ? extractHashFromOutput(codeBlock[1]) : undefined;
98
+ addCommit(hash, message);
56
99
  }
57
100
  }
58
101
 
@@ -40,6 +40,11 @@ const isSubstantiveGoal = (text: string): boolean => {
40
40
  return true;
41
41
  };
42
42
 
43
+ const FIRST_MSG_CLIP = 80;
44
+
45
+ const indexSuffix = (sourceIndex?: number): string =>
46
+ sourceIndex != null ? ` (#${sourceIndex})` : "";
47
+
43
48
  // Test scope-change / task intent only on the leading portion of a user block
44
49
  // so that pasted outputs below the actual instruction do not trigger matches.
45
50
  const LEADING_CHARS = 200;
@@ -47,6 +52,7 @@ const LEADING_CHARS = 200;
47
52
  export const extractGoals = (blocks: NormalizedBlock[]): string[] => {
48
53
  const goals: string[] = [];
49
54
  let latestScopeChange: string[] | null = null;
55
+ let latestScopeIndex: number | undefined;
50
56
 
51
57
  for (const b of blocks) {
52
58
  if (b.kind !== "user") continue;
@@ -58,21 +64,26 @@ export const extractGoals = (blocks: NormalizedBlock[]): string[] => {
58
64
  if (lines.length === 0) continue;
59
65
 
60
66
  if (goals.length === 0) {
61
- goals.push(...lines.slice(0, 6));
67
+ goals.push(...lines.slice(0, 6).map((l) => clip(l, FIRST_MSG_CLIP) + indexSuffix(b.sourceIndex)));
62
68
  continue;
63
69
  }
64
70
 
65
71
  const leading = b.text.slice(0, LEADING_CHARS);
66
72
  if (SCOPE_CHANGE_RE.test(leading)) {
67
73
  latestScopeChange = lines.slice(0, 3).map((l) => clip(l, MAX_GOAL_CHARS));
74
+ latestScopeIndex = b.sourceIndex;
68
75
  } else if (TASK_RE.test(leading) && lines[0].length > 15) {
69
76
  latestScopeChange = lines.slice(0, 2).map((l) => clip(l, MAX_GOAL_CHARS));
77
+ latestScopeIndex = b.sourceIndex;
70
78
  }
71
79
  }
72
80
 
73
81
  // Only emit the [Scope change] marker when we actually captured bullets.
74
82
  if (latestScopeChange && latestScopeChange.length > 0) {
75
- goals.push("[Scope change]", ...latestScopeChange);
83
+ goals.push("[Scope change]");
84
+ for (const line of latestScopeChange) {
85
+ goals.push(line + indexSuffix(latestScopeIndex));
86
+ }
76
87
  }
77
88
 
78
89
  return goals.slice(0, 8);
@@ -48,6 +48,37 @@ const formatTokens = (n: number): string => {
48
48
  return String(n);
49
49
  };
50
50
 
51
+ export interface CompactionStats {
52
+ summarized: number;
53
+ kept: number;
54
+ keptTokensEst: number;
55
+ compactAll: boolean;
56
+ totalUserTurns: number;
57
+ keptUserTurns: number;
58
+ requestedKeepUserTurns: number;
59
+ keepUserTurnsExplicit: boolean;
60
+ keepFallbackToCompactAll: boolean;
61
+ smartKeepAdjusted: boolean;
62
+ smartFromKeep: number;
63
+ }
64
+
65
+ /**
66
+ * Format compaction stats for user-visible notification.
67
+ * Example output:
68
+ * blackhole: 6 source entries processed; tail kept 1/4 user turns (~0.5k tok).
69
+ */
70
+ export const formatCompactionStats = (stats: CompactionStats): string => {
71
+ const parts: string[] = [`${stats.summarized} source entries processed`];
72
+ parts.push(`tail kept ${stats.keptUserTurns}/${stats.totalUserTurns} user turns`);
73
+ if (stats.smartKeepAdjusted) {
74
+ parts.push(`smart keep:${stats.smartFromKeep}→${stats.keptUserTurns}`);
75
+ }
76
+ if (stats.keepFallbackToCompactAll) {
77
+ parts.push(`compact-all`);
78
+ }
79
+ return `blackhole: ${parts.join("; ")} (~${formatTokens(stats.keptTokensEst)} tok).`;
80
+ };
81
+
51
82
  const dbg = (debug: boolean, data: Record<string, unknown>) => {
52
83
  if (!debug) return;
53
84
  try { writeFileSync("/tmp/pi-blackhole-debug.json", JSON.stringify(data, null, 2)); } catch {}
@@ -240,7 +271,7 @@ const REASON_MESSAGES: Record<OwnCutCancelReason, string> = {
240
271
  export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime) => {
241
272
  pi.on("session_before_compact", (event, ctx) => {
242
273
  const { preparation, branchEntries, customInstructions } = event;
243
- omRuntime.ensureConfig(ctx.cwd ?? process.cwd());
274
+ omRuntime.ensureConfig(ctx.cwd ?? process.cwd(), (msg) => ctx.ui?.notify?.(msg, "warning"));
244
275
  const trace = (ev: string, d?: Record<string, unknown>) => debugLog(ev, d, omRuntime.config.debugLog === true);
245
276
 
246
277
  trace("before_compact.enter", {
@@ -291,9 +322,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
291
322
  // Determine effective tail behavior for buildOwnCut
292
323
  // Both /blackhole and auto-triggered default to "minimal" (aggressive cut);
293
324
  // users can opt into "pi-default" (gentler) by setting tailBehavior in config.
294
- const effectiveTailBehavior = isPiVcc
295
- ? (omRuntime.config.tailBehavior ?? "minimal")
296
- : (omRuntime.config.tailBehavior ?? "minimal");
325
+ const effectiveTailBehavior = omRuntime.config.tailBehavior ?? "minimal";
297
326
 
298
327
  trace("before_compact.tail_behavior", {
299
328
  effectiveTailBehavior,
@@ -400,10 +429,22 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
400
429
  }, 0);
401
430
  return sum;
402
431
  }, 0);
432
+ const totalUserTurns = (branchEntries as any[]).filter((e: any) => e.type === "message" && e.message?.role === "user").length;
433
+ const keptUserTurns = ownCut.compactAll
434
+ ? 0
435
+ : (branchEntries as any[]).slice(keptIdx).filter((e: any) => e.type === "message" && e.message?.role === "user").length;
403
436
  omRuntime.compactionStats = {
404
437
  summarized: agentMessages.length,
405
438
  kept: keptEntries.length,
406
439
  keptTokensEst: Math.round(keptChars / 4),
440
+ compactAll: ownCut.compactAll,
441
+ totalUserTurns,
442
+ keptUserTurns,
443
+ requestedKeepUserTurns: 1,
444
+ keepUserTurnsExplicit: false,
445
+ keepFallbackToCompactAll: ownCut.compactAll,
446
+ smartKeepAdjusted: false,
447
+ smartFromKeep: 1,
407
448
  };
408
449
 
409
450
  const summary = compile({
@@ -460,7 +501,10 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
460
501
  const projection = buildCompactionProjection(
461
502
  branchEntries as any[],
462
503
  firstKeptEntryId,
463
- { observationsPoolMaxTokens: omRuntime.config.observationsPoolMaxTokens },
504
+ {
505
+ observationsPoolMaxTokens: omRuntime.config.observationsPoolMaxTokens,
506
+ fullFoldAlways: omRuntime.config.fullFoldAlways,
507
+ },
464
508
  );
465
509
  omContent = renderSummary(projection.reflections, projection.observations);
466
510
  omDetails = projection.details;
@@ -488,10 +532,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
488
532
  const sessionId = ctx.sessionManager.getSessionId();
489
533
  setTimeout(() => {
490
534
  try {
491
- ctx?.ui?.notify?.(
492
- `blackhole: ${stats.summarized} source entries processed; tail kept ${stats.kept} (~${formatTokens(stats.keptTokensEst)} tok).`,
493
- "info",
494
- );
535
+ ctx?.ui?.notify?.(formatCompactionStats(stats), "info");
495
536
  notifyMigrationReminder(sessionId, (msg, level) => ctx?.ui?.notify?.(msg, level as any));
496
537
  } catch {}
497
538
  }, 500);
@@ -8,7 +8,7 @@
8
8
  import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@earendil-works/pi-agent-core";
9
9
  import type { Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
10
10
  import { createBridgeStreamFn } from "../../provider-stream.js";
11
- import { streamSimple } from "@earendil-works/pi-ai";
11
+ import { streamSimple } from "@earendil-works/pi-ai/compat";
12
12
  import { Type } from "typebox";
13
13
  import type { Static } from "typebox";
14
14
  import { debugLog } from "../../debug-log.js";
@@ -9,7 +9,7 @@
9
9
  import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@earendil-works/pi-agent-core";
10
10
  import type { Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
11
11
  import { createBridgeStreamFn } from "../../provider-stream.js";
12
- import { streamSimple } from "@earendil-works/pi-ai";
12
+ import { streamSimple } from "@earendil-works/pi-ai/compat";
13
13
  import { Type } from "typebox";
14
14
  import type { Static } from "typebox";
15
15
  import { hashId } from "../../ids.js";
@@ -8,7 +8,7 @@
8
8
  import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@earendil-works/pi-agent-core";
9
9
  import type { Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
10
10
  import { createBridgeStreamFn } from "../../provider-stream.js";
11
- import { streamSimple } from "@earendil-works/pi-ai";
11
+ import { streamSimple } from "@earendil-works/pi-ai/compat";
12
12
  import { Type } from "typebox";
13
13
  import type { Static } from "typebox";
14
14
  import { hashId } from "../../ids.js";