pi-shadow-mind 0.1.19 → 0.1.21

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/dist/index.js CHANGED
@@ -6,11 +6,8 @@ var __export = (target, all) => {
6
6
 
7
7
  // src/runtime.ts
8
8
  import { randomUUID as randomUUID3 } from "node:crypto";
9
- import {
10
- buildSessionContext
11
- } from "@earendil-works/pi-coding-agent";
12
9
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
13
- import { Text } from "@earendil-works/pi-tui";
10
+ import { Text as Text2 } from "@earendil-works/pi-tui";
14
11
 
15
12
  // src/final-response-queue.ts
16
13
  var FinalResponseQueue = class {
@@ -29,6 +26,7 @@ var FinalResponseQueue = class {
29
26
  this.pending.length = 0;
30
27
  }
31
28
  pump() {
29
+ if (this.options.canLaunch && !this.options.canLaunch()) return;
32
30
  while (this.options.activeCount() < this.options.maxParallel() && this.pending.length > 0) {
33
31
  const epoch = this.options.currentEpoch();
34
32
  const activeShadowIds = this.options.activeShadowIds();
@@ -52,6 +50,7 @@ var CompletionReview = class {
52
50
  maxParallel: () => this.host.maxParallel(),
53
51
  activeCount: () => this.host.activeCount(),
54
52
  activeShadowIds: () => this.host.activeShadowIds(),
53
+ canLaunch: () => this.host.canLaunch ? this.host.canLaunch() : true,
55
54
  launch: ({ job, run }) => host.launch(job, run)
56
55
  });
57
56
  }
@@ -78,7 +77,8 @@ var CompletionReview = class {
78
77
  requestId: request.id,
79
78
  epoch: request.epoch,
80
79
  remaining: jobs.length,
81
- reports: []
80
+ reports: [],
81
+ shadowIds: jobs.map((job) => job.shadowId)
82
82
  };
83
83
  this.queue.enqueue(
84
84
  jobs.map((job) => {
@@ -102,7 +102,9 @@ var CompletionReview = class {
102
102
  this.state.remaining -= 1;
103
103
  if (this.state.remaining > 0) return;
104
104
  const reports = this.state.reports;
105
+ const shadowIds = this.state.shadowIds;
105
106
  this.retire(requestId);
107
+ this.host.onReviewCompleted?.(shadowIds);
106
108
  if (reports.length > 0) this.host.deliver(reports);
107
109
  }
108
110
  cancel(request) {
@@ -145,6 +147,12 @@ function isNonEmptyString(value) {
145
147
  function isStringArray(value) {
146
148
  return Array.isArray(value) && value.every((item) => isNonEmptyString(item));
147
149
  }
150
+ function cleanStringArray(value, fallback, name) {
151
+ if (value === void 0) return [...fallback];
152
+ if (!isStringArray(value))
153
+ throw new Error(`${name} must be an array of non-empty strings`);
154
+ return [...new Set(value.map((item) => item.trim()))];
155
+ }
148
156
  function isThinkingLevel(value) {
149
157
  return typeof value === "string" && THINKING_LEVELS.has(value);
150
158
  }
@@ -152,6 +160,7 @@ function isThinkingLevel(value) {
152
160
  // src/config.ts
153
161
  var DEFAULT_CONFIG = {
154
162
  heartbeatProbability: 1 / 3,
163
+ heartbeatTools: [],
155
164
  maxParallelShadows: 2,
156
165
  defaultShadowTimeoutSeconds: 300,
157
166
  headlessDrainTimeoutSeconds: 120,
@@ -164,6 +173,7 @@ function parseConfig(input) {
164
173
  }
165
174
  const value = input;
166
175
  const probability = numberInRange(value.heartbeat_probability, 0, 1, DEFAULT_CONFIG.heartbeatProbability, "heartbeat_probability");
176
+ const heartbeatTools = cleanStringArray(value.heartbeat_tools, DEFAULT_CONFIG.heartbeatTools, "heartbeat_tools");
167
177
  const parallel = positiveInteger(value.max_parallel_shadows, DEFAULT_CONFIG.maxParallelShadows, "max_parallel_shadows");
168
178
  const timeout = positiveNumber(value.default_shadow_timeout_seconds, DEFAULT_CONFIG.defaultShadowTimeoutSeconds, "default_shadow_timeout_seconds");
169
179
  const drainTimeout = positiveNumber(value.headless_drain_timeout_seconds, DEFAULT_CONFIG.headlessDrainTimeoutSeconds, "headless_drain_timeout_seconds");
@@ -176,6 +186,7 @@ function parseConfig(input) {
176
186
  }
177
187
  return {
178
188
  heartbeatProbability: probability,
189
+ heartbeatTools,
179
190
  maxParallelShadows: parallel,
180
191
  defaultShadowTimeoutSeconds: timeout,
181
192
  headlessDrainTimeoutSeconds: drainTimeout,
@@ -188,6 +199,7 @@ function parseConfig(input) {
188
199
  function serializeConfig(config) {
189
200
  return `${JSON.stringify({
190
201
  heartbeat_probability: config.heartbeatProbability,
202
+ ...config.heartbeatTools && config.heartbeatTools.length ? { heartbeat_tools: config.heartbeatTools } : {},
191
203
  max_parallel_shadows: config.maxParallelShadows,
192
204
  default_shadow_timeout_seconds: config.defaultShadowTimeoutSeconds,
193
205
  headless_drain_timeout_seconds: config.headlessDrainTimeoutSeconds,
@@ -6968,7 +6980,7 @@ function parseShadowMarkdown(source, filePath) {
6968
6980
  "activation_probability"
6969
6981
  ),
6970
6982
  trigger: triggerArray(value.trigger, ["heartbeat"], "trigger"),
6971
- activeForModels: stringArray(
6983
+ activeForModels: cleanStringArray(
6972
6984
  value.active_for_models,
6973
6985
  ["*"],
6974
6986
  "active_for_models"
@@ -6979,7 +6991,16 @@ function parseShadowMarkdown(source, filePath) {
6979
6991
  value.timeout_seconds,
6980
6992
  "timeout_seconds"
6981
6993
  ),
6982
- tools: stringArray(value.tools, [], "tools"),
6994
+ finalResponseRounds: optionalNonNegativeInteger(
6995
+ value.final_response_rounds,
6996
+ "final_response_rounds"
6997
+ ),
6998
+ activationTools: cleanStringArray(
6999
+ value.activation_tools,
7000
+ [],
7001
+ "activation_tools"
7002
+ ),
7003
+ tools: cleanStringArray(value.tools, [], "tools"),
6983
7004
  prompt,
6984
7005
  filePath
6985
7006
  };
@@ -7010,15 +7031,15 @@ function optionalPositiveNumber(value, name) {
7010
7031
  throw new Error(`${name} must be positive`);
7011
7032
  return value;
7012
7033
  }
7013
- function stringArray(value, fallback, name) {
7014
- if (value === void 0) return [...fallback];
7015
- if (!isStringArray(value))
7016
- throw new Error(`${name} must be an array of non-empty strings`);
7017
- return [...new Set(value.map((item) => item.trim()))];
7034
+ function optionalNonNegativeInteger(value, name) {
7035
+ if (value === void 0) return void 0;
7036
+ if (!isFiniteNumber(value) || !Number.isInteger(value) || value < 0)
7037
+ throw new Error(`${name} must be a non-negative integer`);
7038
+ return value;
7018
7039
  }
7019
7040
  function triggerArray(value, fallback, name) {
7020
7041
  const values = typeof value === "string" ? [value] : value;
7021
- const items = stringArray(values, fallback, name);
7042
+ const items = cleanStringArray(values, fallback, name);
7022
7043
  const allowed = new Set(SHADOW_TRIGGERS);
7023
7044
  const invalid = items.filter((item) => !allowed.has(item));
7024
7045
  if (invalid.length)
@@ -7104,6 +7125,8 @@ function serializeShadow(shadow) {
7104
7125
  ...shadow.runWithModel ? { run_with_model: shadow.runWithModel } : {},
7105
7126
  ...shadow.thinkingLevel ? { thinking_level: shadow.thinkingLevel } : {},
7106
7127
  ...shadow.timeoutSeconds !== void 0 ? { timeout_seconds: shadow.timeoutSeconds } : {},
7128
+ ...shadow.finalResponseRounds !== void 0 ? { final_response_rounds: shadow.finalResponseRounds } : {},
7129
+ ...shadow.activationTools && shadow.activationTools.length ? { activation_tools: shadow.activationTools } : {},
7107
7130
  tools: shadow.tools ?? []
7108
7131
  };
7109
7132
  return `---
@@ -7114,7 +7137,9 @@ ${(shadow.prompt ?? "").trim()}
7114
7137
  `;
7115
7138
  }
7116
7139
  function describeShadow(shadow) {
7117
- return `${shadow.enabled ? "enabled" : "disabled"} ${shadow.id} (${shadow.name}) p=${shadow.activationProbability} trigger=${shadow.trigger.join(",")} models=${shadow.activeForModels.join(",")} tools=${shadow.tools.join(",") || "default"} file=${basename2(shadow.filePath)}`;
7140
+ const actTools = shadow.activationTools.length ? ` act_tools=${shadow.activationTools.join(",")}` : "";
7141
+ const finalRounds = shadow.finalResponseRounds ? ` final_rounds=${shadow.finalResponseRounds}` : "";
7142
+ return `${shadow.enabled ? "enabled" : "disabled"} ${shadow.id} (${shadow.name}) p=${shadow.activationProbability} trigger=${shadow.trigger.join(",")} models=${shadow.activeForModels.join(",")}${finalRounds}${actTools} tools=${shadow.tools.join(",") || "default"} file=${basename2(shadow.filePath)}`;
7118
7143
  }
7119
7144
  function definedOnly(value) {
7120
7145
  return Object.fromEntries(
@@ -7122,6 +7147,27 @@ function definedOnly(value) {
7122
7147
  );
7123
7148
  }
7124
7149
 
7150
+ // src/final-response-budget.ts
7151
+ var FinalResponseBudget = class {
7152
+ counts = /* @__PURE__ */ new Map();
7153
+ canRun(shadow) {
7154
+ const limit = shadow.finalResponseRounds ?? 0;
7155
+ if (limit <= 0) return true;
7156
+ return this.used(shadow.id) < limit;
7157
+ }
7158
+ commit(shadowIds) {
7159
+ for (const id of shadowIds) {
7160
+ this.counts.set(id, this.used(id) + 1);
7161
+ }
7162
+ }
7163
+ used(shadowId) {
7164
+ return this.counts.get(shadowId) ?? 0;
7165
+ }
7166
+ reset() {
7167
+ this.counts.clear();
7168
+ }
7169
+ };
7170
+
7125
7171
  // src/management-tools.ts
7126
7172
  import { Type } from "typebox";
7127
7173
  var ID = Type.String({ pattern: "^[a-z0-9][a-z0-9_-]*$" });
@@ -7149,6 +7195,8 @@ var SHADOW_FIELDS = {
7149
7195
  run_with_model: Type.Optional(Type.String()),
7150
7196
  thinking_level: Type.Optional(THINKING),
7151
7197
  timeout_seconds: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
7198
+ final_response_rounds: Type.Optional(Type.Integer({ minimum: 0 })),
7199
+ activation_tools: Type.Optional(Type.Array(Type.String())),
7152
7200
  tools: Type.Optional(Type.Array(Type.String())),
7153
7201
  prompt: Type.Optional(Type.String())
7154
7202
  };
@@ -7270,6 +7318,7 @@ function configWriteTool(store, getConfig) {
7270
7318
  heartbeat_probability: Type.Optional(
7271
7319
  Type.Number({ minimum: 0, maximum: 1 })
7272
7320
  ),
7321
+ heartbeat_tools: Type.Optional(Type.Array(Type.String())),
7273
7322
  max_parallel_shadows: Type.Optional(Type.Integer({ minimum: 1 })),
7274
7323
  default_shadow_timeout_seconds: Type.Optional(
7275
7324
  Type.Number({ exclusiveMinimum: 0 })
@@ -7289,6 +7338,7 @@ function configWriteTool(store, getConfig) {
7289
7338
  const raw = params;
7290
7339
  const next = parseConfig({
7291
7340
  heartbeat_probability: raw.heartbeat_probability ?? current.heartbeatProbability,
7341
+ heartbeat_tools: raw.heartbeat_tools ?? current.heartbeatTools,
7292
7342
  max_parallel_shadows: raw.max_parallel_shadows ?? current.maxParallelShadows,
7293
7343
  default_shadow_timeout_seconds: raw.default_shadow_timeout_seconds ?? current.defaultShadowTimeoutSeconds,
7294
7344
  headless_drain_timeout_seconds: raw.headless_drain_timeout_seconds ?? current.headlessDrainTimeoutSeconds,
@@ -7336,6 +7386,8 @@ function toPatch(raw) {
7336
7386
  runWithModel: raw.run_with_model,
7337
7387
  thinkingLevel: raw.thinking_level,
7338
7388
  timeoutSeconds: raw.timeout_seconds,
7389
+ finalResponseRounds: raw.final_response_rounds,
7390
+ activationTools: raw.activation_tools,
7339
7391
  tools: raw.tools,
7340
7392
  prompt: raw.prompt
7341
7393
  };
@@ -7385,6 +7437,147 @@ function formatReportBatch(reports) {
7385
7437
  ${report.content}`).join("\n\n");
7386
7438
  }
7387
7439
 
7440
+ // src/report-history.ts
7441
+ var ReportHistory = class {
7442
+ constructor(limit = 5) {
7443
+ this.limit = limit;
7444
+ if (!Number.isInteger(limit) || limit < 0) throw new Error("Report limit must be a non-negative integer");
7445
+ }
7446
+ reports = [];
7447
+ add(reports, deliveredAt = (/* @__PURE__ */ new Date()).toISOString()) {
7448
+ for (const report of reports) {
7449
+ this.reports = this.reports.filter(({ runId }) => runId !== report.runId);
7450
+ this.reports.push({ ...report, deliveredAt });
7451
+ if (this.reports.length > this.limit) this.reports.shift();
7452
+ }
7453
+ }
7454
+ /** Newest delivery first; callers receive an isolated snapshot. */
7455
+ list() {
7456
+ return this.reports.map((report) => ({ ...report })).reverse();
7457
+ }
7458
+ clear() {
7459
+ this.reports = [];
7460
+ }
7461
+ };
7462
+
7463
+ // src/report-viewer.ts
7464
+ import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
7465
+ var ReportViewer = class {
7466
+ constructor(reports, theme, host) {
7467
+ this.theme = theme;
7468
+ this.host = host;
7469
+ this.body = new Text(reports.map(
7470
+ (report) => `${report.shadowName} \xB7 ${report.deliveredAt}
7471
+ run ${report.runId}
7472
+ ${report.content.replace(/\r\n?/g, "\n")}`
7473
+ ).join("\n\n"), 0, 0);
7474
+ this.actions = [
7475
+ ["escape", () => host.close()],
7476
+ ["up", () => this.scroll(-1)],
7477
+ ["down", () => this.scroll(1)],
7478
+ ["pageUp", () => this.scroll(-this.pageSize)],
7479
+ ["pageDown", () => this.scroll(this.pageSize)],
7480
+ ["home", () => {
7481
+ this.offset = 0;
7482
+ }],
7483
+ ["end", () => {
7484
+ this.offset = this.maxOffset();
7485
+ }]
7486
+ ];
7487
+ }
7488
+ body;
7489
+ offset = 0;
7490
+ contentHeight = 0;
7491
+ pageSize = 1;
7492
+ actions;
7493
+ render(width) {
7494
+ const height = Math.max(1, Math.min(Math.floor(this.host.rows() * 0.8), this.host.rows() - 2));
7495
+ this.pageSize = Math.max(1, height - 2);
7496
+ const lines = this.body.render(Math.max(1, width));
7497
+ this.contentHeight = lines.length;
7498
+ this.offset = Math.min(this.offset, this.maxOffset());
7499
+ const page = lines.slice(this.offset, this.offset + this.pageSize);
7500
+ if (height < 3) return page.slice(0, height).map((line) => truncateToWidth(line, width));
7501
+ return [
7502
+ this.theme.fg("accent", truncateToWidth("Recent Shadow reports \xB7 newest first", width)),
7503
+ ...page,
7504
+ this.theme.fg("dim", truncateToWidth(
7505
+ `Esc close \xB7 \u2191\u2193 PgUp/PgDn Home/End \xB7 ${this.offset + 1}\u2013${Math.min(this.offset + this.pageSize, lines.length)}/${lines.length}`,
7506
+ width
7507
+ ))
7508
+ ];
7509
+ }
7510
+ handleInput(data) {
7511
+ const action = this.actions.find(([key]) => matchesKey(data, key));
7512
+ if (!action) return;
7513
+ action[1]();
7514
+ this.host.requestRender();
7515
+ }
7516
+ invalidate() {
7517
+ this.body.invalidate();
7518
+ }
7519
+ maxOffset() {
7520
+ return Math.max(0, this.contentHeight - this.pageSize);
7521
+ }
7522
+ scroll(lines) {
7523
+ this.offset = Math.max(0, Math.min(this.maxOffset(), this.offset + lines));
7524
+ }
7525
+ };
7526
+
7527
+ // src/report-browser.ts
7528
+ var ReportBrowser = class {
7529
+ history = new ReportHistory();
7530
+ closeViewer;
7531
+ add(reports) {
7532
+ this.history.add(reports);
7533
+ }
7534
+ reset() {
7535
+ this.closeViewer?.();
7536
+ this.history.clear();
7537
+ }
7538
+ async handleCommand(command, ctx) {
7539
+ const [name, ...args] = command.split(/\s+/);
7540
+ if (name !== "reports") return false;
7541
+ if (args.length && args.join(" ") !== "hide") {
7542
+ ctx.ui.notify("Usage: /shadow reports [hide]", "warning");
7543
+ return true;
7544
+ }
7545
+ if (args[0] === "hide" || this.closeViewer) {
7546
+ this.closeViewer?.();
7547
+ return true;
7548
+ }
7549
+ if (ctx.mode !== "tui") {
7550
+ if (ctx.hasUI) ctx.ui.notify("The report viewer requires TUI mode. Delivered reports remain in the conversation.", "warning");
7551
+ return true;
7552
+ }
7553
+ const reports = this.history.list();
7554
+ if (!reports.length) {
7555
+ ctx.ui.notify("No reports delivered in this session yet.", "info");
7556
+ return true;
7557
+ }
7558
+ const view = { close: () => {
7559
+ } };
7560
+ try {
7561
+ await ctx.ui.custom((tui, theme, _keys, done) => {
7562
+ view.close = () => {
7563
+ if (this.closeViewer !== view.close) return;
7564
+ this.closeViewer = void 0;
7565
+ done();
7566
+ };
7567
+ this.closeViewer = view.close;
7568
+ return new ReportViewer(reports, theme, {
7569
+ rows: () => tui.terminal.rows,
7570
+ requestRender: () => tui.requestRender(),
7571
+ close: view.close
7572
+ });
7573
+ }, { overlay: true, overlayOptions: { width: "90%", maxHeight: "80%", margin: 1 } });
7574
+ } finally {
7575
+ if (this.closeViewer === view.close) this.closeViewer = void 0;
7576
+ }
7577
+ return true;
7578
+ }
7579
+ };
7580
+
7388
7581
  // src/random.ts
7389
7582
  function createRandom(seed) {
7390
7583
  if (seed === void 0) return Math.random;
@@ -7398,9 +7591,143 @@ function createRandom(seed) {
7398
7591
  };
7399
7592
  }
7400
7593
 
7594
+ // src/acp-projection.ts
7595
+ import {
7596
+ buildContextEntries,
7597
+ buildSessionContext,
7598
+ sessionEntryToContextMessages
7599
+ } from "@earendil-works/pi-coding-agent";
7600
+
7601
+ // src/acp-state.ts
7602
+ import { readFileSync } from "node:fs";
7603
+ function isRecord(value) {
7604
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7605
+ }
7606
+ function readAcpBlocks(sessionFile) {
7607
+ if (!sessionFile) return [];
7608
+ try {
7609
+ const state = JSON.parse(readFileSync(`${sessionFile}.acp.json`, "utf8"));
7610
+ if (!isRecord(state) || !Array.isArray(state.blocks)) return [];
7611
+ const blocks = [];
7612
+ const seen = /* @__PURE__ */ new Set();
7613
+ for (const block of state.blocks) {
7614
+ if (!isRecord(block)) return [];
7615
+ if (block.active === false) continue;
7616
+ if (block.active !== true || typeof block.blockId !== "string" || !block.blockId || typeof block.summary !== "string" || !block.summary.trim() || !Array.isArray(block.effectiveMessageIds) || !block.effectiveMessageIds.every((id) => typeof id === "string" && id.length > 0) || seen.has(block.blockId)) return [];
7617
+ seen.add(block.blockId);
7618
+ blocks.push({
7619
+ blockId: block.blockId,
7620
+ summary: block.summary,
7621
+ effectiveMessageIds: [...block.effectiveMessageIds]
7622
+ });
7623
+ }
7624
+ return blocks;
7625
+ } catch {
7626
+ return [];
7627
+ }
7628
+ }
7629
+
7630
+ // src/acp-projection.ts
7631
+ function buildShadowSessionContext(entries, leafId, sessionFile) {
7632
+ const context = buildSessionContext(entries, leafId);
7633
+ const blocks = readAcpBlocks(sessionFile);
7634
+ if (!blocks.length) return context;
7635
+ const visible = buildContextEntries(entries, leafId).flatMap(
7636
+ (entry) => sessionEntryToContextMessages(entry).map((message) => ({ id: entry.id, message }))
7637
+ );
7638
+ return { ...context, messages: projectMessages(visible, blocks) };
7639
+ }
7640
+ function toolCalls(message) {
7641
+ return message.role === "assistant" ? message.content.filter((part) => part.type === "toolCall") : [];
7642
+ }
7643
+ function coverageIds({ id, message }) {
7644
+ const calls = toolCalls(message);
7645
+ return calls.length > 1 ? calls.map((call) => `${id}#${call.id}`) : [id];
7646
+ }
7647
+ function summaryMessage(block) {
7648
+ return {
7649
+ role: "compactionSummary",
7650
+ summary: `[ACP block ${block.blockId} \u2014 ${block.effectiveMessageIds.length} folded messages]
7651
+ ${block.summary}`,
7652
+ tokensBefore: 0,
7653
+ timestamp: 0
7654
+ };
7655
+ }
7656
+ function projectMessages(visible, blocks) {
7657
+ const indexById = /* @__PURE__ */ new Map();
7658
+ visible.forEach((item, index) => {
7659
+ for (const id of coverageIds(item)) indexById.set(id, index);
7660
+ });
7661
+ const covered = /* @__PURE__ */ new Set();
7662
+ const anchors = /* @__PURE__ */ new Map();
7663
+ for (const block of blocks) {
7664
+ const positions = block.effectiveMessageIds.flatMap((id) => {
7665
+ const index = indexById.get(id);
7666
+ return index === void 0 ? [] : [index];
7667
+ });
7668
+ if (!positions.length) continue;
7669
+ const first = positions.reduce((left, right) => Math.min(left, right));
7670
+ anchors.set(first, [...anchors.get(first) ?? [], block]);
7671
+ for (const id of block.effectiveMessageIds) covered.add(id);
7672
+ }
7673
+ if (!covered.size) return visible.map(({ message }) => message);
7674
+ const firstUser = visible.findIndex(
7675
+ ({ message }) => message.role === "user" || message.role === "custom"
7676
+ );
7677
+ const messages = [];
7678
+ visible.forEach((item, index) => {
7679
+ for (const block of anchors.get(index) ?? []) messages.push(summaryMessage(block));
7680
+ const retained = index === firstUser ? item.message : retainUncovered(item, covered);
7681
+ if (retained) messages.push(retained);
7682
+ });
7683
+ return removeOrphanedTools(messages);
7684
+ }
7685
+ function retainUncovered(item, covered) {
7686
+ const ids = coverageIds(item);
7687
+ if (!ids.some((id) => covered.has(id))) return item.message;
7688
+ if (ids.every((id) => covered.has(id))) return void 0;
7689
+ const message = item.message;
7690
+ if (message.role !== "assistant") return message;
7691
+ return {
7692
+ ...message,
7693
+ content: message.content.filter(
7694
+ (part) => part.type !== "toolCall" || !covered.has(`${item.id}#${part.id}`)
7695
+ )
7696
+ };
7697
+ }
7698
+ function removeOrphanedTools(messages) {
7699
+ const resultIds = new Set(messages.flatMap(
7700
+ (message) => message.role === "toolResult" ? [message.toolCallId] : []
7701
+ ));
7702
+ const paired = messages.flatMap((message) => {
7703
+ const calls = toolCalls(message);
7704
+ if (!calls.length || message.role !== "assistant") return [message];
7705
+ const retained = calls.filter((call) => call.name === "compress" || resultIds.has(call.id));
7706
+ if (!retained.length) return [];
7707
+ const ids = new Set(retained.map((call) => call.id));
7708
+ return [{ ...message, content: message.content.filter((part) => part.type !== "toolCall" || ids.has(part.id)) }];
7709
+ });
7710
+ const callIds = new Set(paired.flatMap((message) => toolCalls(message).map((call) => call.id)));
7711
+ return paired.filter((message) => message.role !== "toolResult" || callIds.has(message.toolCallId));
7712
+ }
7713
+
7401
7714
  // src/scheduler.ts
7402
- function shouldEvaluateHeartbeat(toolResults) {
7403
- return toolResults.length > 0;
7715
+ function extractToolNames(toolResults) {
7716
+ const names = /* @__PURE__ */ new Set();
7717
+ for (const item of toolResults) {
7718
+ if (!item || typeof item !== "object") continue;
7719
+ const name = item.toolName;
7720
+ if (typeof name === "string" && name.trim().length > 0) {
7721
+ names.add(name.trim());
7722
+ }
7723
+ }
7724
+ return names;
7725
+ }
7726
+ function shouldEvaluateHeartbeat(toolsOrResults, heartbeatTools) {
7727
+ const toolNames = toolsOrResults instanceof Set ? toolsOrResults : extractToolNames(toolsOrResults);
7728
+ if (toolNames.size === 0) return false;
7729
+ if (!heartbeatTools || heartbeatTools.length === 0) return true;
7730
+ return heartbeatTools.some((tool2) => toolNames.has(tool2));
7404
7731
  }
7405
7732
  function shouldEvaluateFinalResponse(messages) {
7406
7733
  const message = messages.at(-1);
@@ -7423,11 +7750,14 @@ function decideHeartbeat(options) {
7423
7750
  activated: [],
7424
7751
  candidates: [],
7425
7752
  modelFiltered: [],
7426
- runningExcluded: []
7753
+ runningExcluded: [],
7754
+ roundFiltered: [],
7755
+ toolFiltered: []
7427
7756
  };
7428
7757
  }
7429
7758
  const modelFiltered = [];
7430
7759
  const runningExcluded = [];
7760
+ const toolFiltered = [];
7431
7761
  const rolls = options.shadows.filter((shadow) => {
7432
7762
  if (!shadow.enabled || !hasTrigger(shadow, "heartbeat")) return false;
7433
7763
  if (options.activeShadowIds.has(shadow.id)) {
@@ -7438,6 +7768,10 @@ function decideHeartbeat(options) {
7438
7768
  modelFiltered.push(shadow.id);
7439
7769
  return false;
7440
7770
  }
7771
+ if (!matchesActivationTools(shadow, options.executedTools)) {
7772
+ toolFiltered.push(shadow.id);
7773
+ return false;
7774
+ }
7441
7775
  return true;
7442
7776
  }).map((shadow) => ({ shadow, roll: random() }));
7443
7777
  const hits = rolls.filter(
@@ -7458,11 +7792,14 @@ function decideHeartbeat(options) {
7458
7792
  selected: selectedIds.has(shadow.id)
7459
7793
  })),
7460
7794
  modelFiltered,
7461
- runningExcluded
7795
+ runningExcluded,
7796
+ roundFiltered: [],
7797
+ toolFiltered
7462
7798
  };
7463
7799
  }
7464
7800
  function decideFinalResponse(options) {
7465
7801
  const modelFiltered = [];
7802
+ const roundFiltered = [];
7466
7803
  const candidates = options.shadows.filter((shadow) => {
7467
7804
  if (!shadow.enabled || !hasTrigger(shadow, "final_response"))
7468
7805
  return false;
@@ -7470,6 +7807,10 @@ function decideFinalResponse(options) {
7470
7807
  modelFiltered.push(shadow.id);
7471
7808
  return false;
7472
7809
  }
7810
+ if (options.canRunFinalResponse && !options.canRunFinalResponse(shadow)) {
7811
+ roundFiltered.push(shadow.id);
7812
+ return false;
7813
+ }
7473
7814
  return true;
7474
7815
  }).map((shadow) => ({ shadow, roll: 0 }));
7475
7816
  return {
@@ -7480,12 +7821,21 @@ function decideFinalResponse(options) {
7480
7821
  selected: true
7481
7822
  })),
7482
7823
  modelFiltered,
7483
- runningExcluded: []
7824
+ runningExcluded: [],
7825
+ roundFiltered
7484
7826
  };
7485
7827
  }
7486
7828
  function matchesModel(shadow, fullModelId) {
7487
7829
  return shadow.activeForModels.includes("*") || shadow.activeForModels.includes(fullModelId);
7488
7830
  }
7831
+ function matchesActivationTools(shadow, executedTools) {
7832
+ if (shadow.activationTools.length === 0) {
7833
+ return true;
7834
+ }
7835
+ if (!executedTools) return false;
7836
+ const executedSet = executedTools instanceof Set ? executedTools : new Set(executedTools);
7837
+ return shadow.activationTools.some((tool2) => executedSet.has(tool2));
7838
+ }
7489
7839
  function hasTrigger(shadow, trigger) {
7490
7840
  return shadow.trigger.includes(trigger);
7491
7841
  }
@@ -7716,7 +8066,7 @@ function serializeTrajectory(messages) {
7716
8066
  continue;
7717
8067
  }
7718
8068
  if (message.role === "compactionSummary" || message.role === "branchSummary") {
7719
- appendText(lines, "SUMMARY", message.content);
8069
+ appendText(lines, "SUMMARY", message.summary ?? message.content);
7720
8070
  }
7721
8071
  }
7722
8072
  return `<main-agent-trajectory>
@@ -8380,16 +8730,20 @@ var ShadowMindRuntime = class {
8380
8730
  runner = new ShadowRunner();
8381
8731
  usageStore = new UsageStore(this.agentDir);
8382
8732
  active = /* @__PURE__ */ new Map();
8733
+ finalResponseBudget = new FinalResponseBudget();
8383
8734
  completionReview = new CompletionReview({
8384
8735
  currentEpoch: () => this.epoch,
8385
8736
  maxParallel: () => this.configStore.current.maxParallelShadows,
8386
8737
  activeCount: () => this.active.size,
8387
8738
  activeShadowIds: () => new Set([...this.active.values()].map(({ shadow }) => shadow.id)),
8739
+ canLaunch: () => !this.batcher.hasPending,
8388
8740
  launch: (pending, review) => this.launchShadow({ ...pending, completionReview: review }),
8389
- deliver: (reports) => void this.deliverReports(reports)
8741
+ deliver: (reports) => void this.deliverReports(reports),
8742
+ onReviewCompleted: (shadowIds) => this.finalResponseBudget.commit(shadowIds)
8390
8743
  });
8391
8744
  recentEvents = [];
8392
8745
  recentRuns = [];
8746
+ reports = new ReportBrowser();
8393
8747
  batcher;
8394
8748
  sessionLifetime = new SessionLifetime();
8395
8749
  epoch = 0;
@@ -8417,8 +8771,10 @@ var ShadowMindRuntime = class {
8417
8771
  this.latestContext = ctx;
8418
8772
  this.modelCalls = 0;
8419
8773
  this.completedWithFinalText = false;
8774
+ this.finalResponseBudget.reset();
8420
8775
  this.sessionUsage = zeroUsage();
8421
8776
  this.recentRuns.length = 0;
8777
+ this.reports.reset();
8422
8778
  await this.configStore.initialize();
8423
8779
  await this.usageStore.initialize();
8424
8780
  this.random = createRandom(this.configStore.current.randomSeed);
@@ -8433,6 +8789,7 @@ var ShadowMindRuntime = class {
8433
8789
  this.latestContext = ctx;
8434
8790
  if (event.source === "extension") return;
8435
8791
  this.completedWithFinalText = false;
8792
+ this.finalResponseBudget.reset();
8436
8793
  this.epoch += 1;
8437
8794
  this.abortAll("new-user-input");
8438
8795
  });
@@ -8442,14 +8799,16 @@ var ShadowMindRuntime = class {
8442
8799
  });
8443
8800
  this.pi.on("turn_end", async (event, ctx) => {
8444
8801
  this.latestContext = ctx;
8445
- if (!shouldEvaluateHeartbeat(event.toolResults)) {
8802
+ const toolResults = event.toolResults ?? [];
8803
+ const executedTools = extractToolNames(toolResults);
8804
+ if (executedTools.size === 0) {
8446
8805
  this.record("heartbeat-skipped", {
8447
8806
  reason: "no-tool-activity",
8448
8807
  modelCalls: this.modelCalls
8449
8808
  });
8450
8809
  return;
8451
8810
  }
8452
- await this.onHeartbeat(ctx);
8811
+ await this.onHeartbeat(ctx, executedTools);
8453
8812
  });
8454
8813
  this.pi.on("agent_end", (event, ctx) => {
8455
8814
  this.latestContext = ctx;
@@ -8481,6 +8840,7 @@ var ShadowMindRuntime = class {
8481
8840
  if (!result.settled) this.active.clear();
8482
8841
  }
8483
8842
  await this.usageStore.flush();
8843
+ this.reports.reset();
8484
8844
  ctx.ui.setStatus("shadow-mind", void 0);
8485
8845
  ctx.ui.setWidget("shadow-mind-panel", void 0);
8486
8846
  this.sessionLifetime.deactivate();
@@ -8504,6 +8864,7 @@ var ShadowMindRuntime = class {
8504
8864
  this.setPaused(!this.paused, ctx);
8505
8865
  return;
8506
8866
  }
8867
+ if (await this.reports.handleCommand(command, ctx)) return;
8507
8868
  if (command === "status") {
8508
8869
  await this.refresh(ctx);
8509
8870
  ctx.ui.notify(
@@ -8534,12 +8895,20 @@ var ShadowMindRuntime = class {
8534
8895
  (message, _options, theme) => {
8535
8896
  const content = typeof message.content === "string" ? message.content : "Shadow report";
8536
8897
  const prefix = theme.fg("accent", "\u{1F419} shadow \xB7 ");
8537
- return new Text(`${prefix}${content}`, 0, 0);
8898
+ return new Text2(`${prefix}${content}`, 0, 0);
8538
8899
  }
8539
8900
  );
8540
8901
  }
8541
- async onHeartbeat(ctx) {
8902
+ async onHeartbeat(ctx, executedTools) {
8542
8903
  const snapshot = await this.refresh(ctx);
8904
+ const config = this.configStore.current;
8905
+ if (!shouldEvaluateHeartbeat(executedTools, config.heartbeatTools)) {
8906
+ this.record("heartbeat-skipped", {
8907
+ reason: "tool-filtered",
8908
+ modelCalls: this.modelCalls
8909
+ });
8910
+ return;
8911
+ }
8543
8912
  if (this.paused || !ctx.model) {
8544
8913
  this.record("heartbeat-skipped", {
8545
8914
  reason: this.paused ? "paused" : "no-model",
@@ -8549,17 +8918,18 @@ var ShadowMindRuntime = class {
8549
8918
  }
8550
8919
  const fullModelId = `${ctx.model.provider}/${ctx.model.id}`;
8551
8920
  const decision = decideHeartbeat({
8552
- heartbeatProbability: this.configStore.current.heartbeatProbability,
8921
+ heartbeatProbability: config.heartbeatProbability,
8553
8922
  availableSlots: Math.max(
8554
8923
  0,
8555
- this.configStore.current.maxParallelShadows - this.active.size
8924
+ config.maxParallelShadows - this.active.size
8556
8925
  ),
8557
8926
  shadows: snapshot.shadows,
8558
8927
  activeShadowIds: new Set(
8559
8928
  [...this.active.values()].map(({ shadow }) => shadow.id)
8560
8929
  ),
8561
8930
  mainModelId: fullModelId,
8562
- random: this.random
8931
+ random: this.random,
8932
+ executedTools
8563
8933
  });
8564
8934
  this.record("heartbeat", {
8565
8935
  modelCalls: this.modelCalls,
@@ -8570,12 +8940,14 @@ var ShadowMindRuntime = class {
8570
8940
  roll
8571
8941
  })),
8572
8942
  ...decision.modelFiltered.length ? { modelFiltered: decision.modelFiltered } : {},
8573
- ...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {}
8943
+ ...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {},
8944
+ ...decision.toolFiltered.length ? { toolFiltered: decision.toolFiltered } : {}
8574
8945
  });
8575
8946
  if (!decision.activated.length) return;
8576
- const context = buildSessionContext(
8947
+ const context = buildShadowSessionContext(
8577
8948
  ctx.sessionManager.getEntries(),
8578
- ctx.sessionManager.getLeafId()
8949
+ ctx.sessionManager.getLeafId(),
8950
+ ctx.sessionManager.getSessionFile()
8579
8951
  );
8580
8952
  const availableTools = new Set(
8581
8953
  this.pi.getAllTools().map((tool2) => tool2.name)
@@ -8592,6 +8964,10 @@ var ShadowMindRuntime = class {
8592
8964
  }
8593
8965
  }
8594
8966
  async onFinalResponse(ctx) {
8967
+ if (this.batcher.hasPending) {
8968
+ await this.batcher.flush();
8969
+ return;
8970
+ }
8595
8971
  const request = this.completionReview.begin(this.epoch);
8596
8972
  const snapshot = await this.refresh(ctx);
8597
8973
  if (!this.completionReview.isCurrent(request)) {
@@ -8609,21 +8985,24 @@ var ShadowMindRuntime = class {
8609
8985
  const fullModelId = `${mainModel.provider}/${mainModel.id}`;
8610
8986
  const decision = decideFinalResponse({
8611
8987
  shadows: snapshot.shadows,
8612
- mainModelId: fullModelId
8988
+ mainModelId: fullModelId,
8989
+ canRunFinalResponse: (shadow) => this.finalResponseBudget.canRun(shadow)
8613
8990
  });
8614
8991
  this.record("final-response", {
8615
8992
  candidates: decision.candidates,
8616
8993
  activated: decision.activated.map(({ shadow }) => shadow.id),
8617
8994
  ...decision.modelFiltered.length ? { modelFiltered: decision.modelFiltered } : {},
8618
- ...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {}
8995
+ ...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {},
8996
+ ...decision.roundFiltered.length ? { roundFiltered: decision.roundFiltered } : {}
8619
8997
  });
8620
8998
  if (!decision.activated.length) {
8621
8999
  this.completionReview.schedule(request, []);
8622
9000
  return;
8623
9001
  }
8624
- const context = buildSessionContext(
9002
+ const context = buildShadowSessionContext(
8625
9003
  ctx.sessionManager.getEntries(),
8626
- ctx.sessionManager.getLeafId()
9004
+ ctx.sessionManager.getLeafId(),
9005
+ ctx.sessionManager.getSessionFile()
8627
9006
  );
8628
9007
  const availableTools = new Set(
8629
9008
  this.pi.getAllTools().map((tool2) => tool2.name)
@@ -8735,7 +9114,7 @@ var ShadowMindRuntime = class {
8735
9114
  count: current.length
8736
9115
  });
8737
9116
  const idle = this.latestContext?.isIdle() ?? true;
8738
- this.sessionLifetime.run(() => {
9117
+ const delivered = this.sessionLifetime.run(() => {
8739
9118
  this.pi.sendMessage(
8740
9119
  {
8741
9120
  customType: "shadow-report",
@@ -8751,6 +9130,7 @@ var ShadowMindRuntime = class {
8751
9130
  { triggerTurn: true, deliverAs: idle ? "followUp" : "steer" }
8752
9131
  );
8753
9132
  });
9133
+ if (delivered) this.reports.add(current);
8754
9134
  }
8755
9135
  async refresh(ctx) {
8756
9136
  const config = await this.configStore.reload();
@@ -8852,7 +9232,7 @@ var ShadowMindRuntime = class {
8852
9232
  const detail = failed ? ` ${event.data?.error ?? event.data?.reason}` : "";
8853
9233
  return `${new Date(event.at).toLocaleTimeString("en-GB", { hour12: false })} ${event.kind}${detail}`;
8854
9234
  }),
8855
- "Shortcut: Alt+S toggle \xB7 Commands: /shadow toggle | pause | resume | status | hide"
9235
+ "Shortcut: Alt+S toggle \xB7 Commands: /shadow toggle | pause | resume | status | hide | reports"
8856
9236
  ];
8857
9237
  }
8858
9238
  };