@llblab/pi-actors 0.41.1 → 0.42.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.
Files changed (147) hide show
  1. package/AGENTS.md +11 -6
  2. package/BACKLOG.md +2 -62
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +22 -4
  5. package/dist/index.js +25 -121
  6. package/dist/lib/async-runs.d.ts +25 -5
  7. package/dist/lib/async-runs.js +136 -47
  8. package/dist/lib/automatic-review-runtime.d.ts +18 -0
  9. package/dist/lib/automatic-review-runtime.js +96 -0
  10. package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  11. package/dist/lib/draft-consolidation-transaction.js +610 -0
  12. package/dist/lib/draft-consolidation.d.ts +35 -0
  13. package/dist/lib/draft-consolidation.js +126 -0
  14. package/dist/lib/draft-review.d.ts +56 -0
  15. package/dist/lib/draft-review.js +254 -0
  16. package/dist/lib/draft-sleep.d.ts +65 -0
  17. package/dist/lib/draft-sleep.js +468 -0
  18. package/dist/lib/file-state.d.ts +8 -1
  19. package/dist/lib/file-state.js +115 -18
  20. package/dist/lib/inspector-actions.d.ts +16 -0
  21. package/dist/lib/inspector-actions.js +57 -0
  22. package/dist/lib/inspector-command.d.ts +7 -0
  23. package/dist/lib/inspector-command.js +37 -0
  24. package/dist/lib/inspector-overlay.d.ts +20 -3
  25. package/dist/lib/inspector-overlay.js +275 -74
  26. package/dist/lib/inspector.d.ts +9 -0
  27. package/dist/lib/inspector.js +48 -0
  28. package/dist/lib/observability.d.ts +1 -0
  29. package/dist/lib/observability.js +11 -1
  30. package/dist/lib/paths.d.ts +7 -0
  31. package/dist/lib/paths.js +28 -0
  32. package/dist/lib/recipes-discovery.js +32 -24
  33. package/dist/lib/recipes-usage.d.ts +18 -6
  34. package/dist/lib/recipes-usage.js +445 -34
  35. package/dist/lib/review-control.d.ts +14 -0
  36. package/dist/lib/review-control.js +111 -0
  37. package/dist/lib/review-diagnostics.d.ts +11 -0
  38. package/dist/lib/review-diagnostics.js +148 -0
  39. package/dist/lib/review-projection.d.ts +14 -0
  40. package/dist/lib/review-projection.js +170 -0
  41. package/dist/lib/run-ui-runtime.d.ts +18 -0
  42. package/dist/lib/run-ui-runtime.js +123 -0
  43. package/dist/lib/runs-artifacts.d.ts +1 -1
  44. package/dist/lib/runs-artifacts.js +1 -1
  45. package/dist/lib/runs-control.d.ts +8 -2
  46. package/dist/lib/runs-control.js +23 -6
  47. package/dist/lib/runs-identity.d.ts +1 -1
  48. package/dist/lib/runs-identity.js +1 -1
  49. package/dist/lib/runs-index.d.ts +11 -2
  50. package/dist/lib/runs-index.js +46 -23
  51. package/dist/lib/runs-mailbox.d.ts +1 -1
  52. package/dist/lib/runs-mailbox.js +1 -1
  53. package/dist/lib/runs-messages.d.ts +1 -1
  54. package/dist/lib/runs-messages.js +1 -1
  55. package/dist/lib/runs-outbox.d.ts +1 -1
  56. package/dist/lib/runs-outbox.js +1 -1
  57. package/dist/lib/runs-ownership.d.ts +1 -1
  58. package/dist/lib/runs-ownership.js +1 -1
  59. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  60. package/dist/lib/runs-parent-teardown.js +172 -0
  61. package/dist/lib/runs-process.d.ts +1 -1
  62. package/dist/lib/runs-process.js +1 -1
  63. package/dist/lib/runs-retention.d.ts +1 -1
  64. package/dist/lib/runs-retention.js +1 -1
  65. package/dist/lib/runs-start.d.ts +5 -3
  66. package/dist/lib/runs-start.js +6 -48
  67. package/dist/lib/runs-status.d.ts +5 -3
  68. package/dist/lib/runs-status.js +4 -6
  69. package/dist/lib/runtime.d.ts +7 -1
  70. package/dist/lib/runtime.js +32 -18
  71. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  72. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  73. package/dist/lib/tool-review-lineage.d.ts +24 -0
  74. package/dist/lib/tool-review-lineage.js +98 -0
  75. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  76. package/dist/lib/tool-review-scheduler.js +494 -0
  77. package/dist/lib/tool-review-transaction.d.ts +50 -0
  78. package/dist/lib/tool-review-transaction.js +362 -0
  79. package/dist/lib/tool-review.d.ts +56 -0
  80. package/dist/lib/tool-review.js +197 -0
  81. package/dist/lib/tools-inspect.js +26 -3
  82. package/dist/lib/tools-local.js +4 -2
  83. package/dist/lib/tools-message.d.ts +1 -0
  84. package/dist/lib/tools-message.js +29 -17
  85. package/dist/lib/tools-spawn.js +4 -1
  86. package/dist/lib/tools.d.ts +1 -0
  87. package/dist/lib/tools.js +1 -0
  88. package/dist/recipes/draft-review.json +24 -0
  89. package/dist/recipes/tool-review.json +24 -0
  90. package/dist/scripts/release-gates.mjs +165 -0
  91. package/dist/skills/actors/SKILL.md +9 -8
  92. package/dist/skills/swarm/SKILL.md +1 -1
  93. package/docs/actor-inspector.md +20 -11
  94. package/docs/async-runs.md +9 -1
  95. package/docs/recipe-library.md +7 -3
  96. package/docs/template-recipes.md +4 -11
  97. package/docs/tool-registry.md +11 -4
  98. package/index.ts +27 -142
  99. package/lib/async-runs.ts +218 -62
  100. package/lib/automatic-review-runtime.ts +135 -0
  101. package/lib/draft-consolidation-transaction.ts +821 -0
  102. package/lib/draft-consolidation.ts +181 -0
  103. package/lib/draft-review.ts +325 -0
  104. package/lib/draft-sleep.ts +576 -0
  105. package/lib/file-state.ts +143 -19
  106. package/lib/inspector-actions.ts +79 -0
  107. package/lib/inspector-command.ts +54 -0
  108. package/lib/inspector-overlay.ts +325 -91
  109. package/lib/inspector.ts +72 -0
  110. package/lib/observability.ts +14 -1
  111. package/lib/paths.ts +43 -0
  112. package/lib/recipes-discovery.ts +34 -26
  113. package/lib/recipes-usage.ts +569 -40
  114. package/lib/review-control.ts +137 -0
  115. package/lib/review-diagnostics.ts +164 -0
  116. package/lib/review-projection.ts +200 -0
  117. package/lib/run-ui-runtime.ts +153 -0
  118. package/lib/runs-artifacts.ts +1 -1
  119. package/lib/runs-control.ts +49 -5
  120. package/lib/runs-identity.ts +1 -1
  121. package/lib/runs-index.ts +57 -21
  122. package/lib/runs-mailbox.ts +1 -1
  123. package/lib/runs-messages.ts +1 -1
  124. package/lib/runs-outbox.ts +1 -1
  125. package/lib/runs-ownership.ts +1 -1
  126. package/lib/runs-parent-teardown.ts +257 -0
  127. package/lib/runs-process.ts +1 -1
  128. package/lib/runs-retention.ts +1 -1
  129. package/lib/runs-start.ts +12 -68
  130. package/lib/runs-status.ts +12 -8
  131. package/lib/runtime.ts +34 -17
  132. package/lib/tool-review-lineage-transaction.ts +881 -0
  133. package/lib/tool-review-lineage.ts +145 -0
  134. package/lib/tool-review-scheduler.ts +635 -0
  135. package/lib/tool-review-transaction.ts +563 -0
  136. package/lib/tool-review.ts +270 -0
  137. package/lib/tools-inspect.ts +33 -3
  138. package/lib/tools-local.ts +8 -2
  139. package/lib/tools-message.ts +45 -30
  140. package/lib/tools-spawn.ts +8 -1
  141. package/lib/tools.ts +5 -0
  142. package/package.json +3 -2
  143. package/recipes/draft-review.json +24 -0
  144. package/recipes/tool-review.json +24 -0
  145. package/scripts/release-gates.mjs +165 -0
  146. package/skills/actors/SKILL.md +9 -8
  147. package/skills/swarm/SKILL.md +1 -1
@@ -5,12 +5,19 @@
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import { stripVTControlCharacters } from "node:util";
8
- import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
8
+ import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
9
9
  import * as Inspector from "./inspector.js";
10
+ const INSPECTOR_TABS = [
11
+ "recipe",
12
+ "communications",
13
+ "turns",
14
+ ];
10
15
  const DETAIL_SECTION_BREAK = { sectionBreak: true };
11
16
  export class ActorInspectorOverlay {
12
17
  done;
18
+ killRun;
13
19
  ownerId;
20
+ readTurns;
14
21
  stateRoot;
15
22
  theme;
16
23
  tui;
@@ -24,23 +31,32 @@ export class ActorInspectorOverlay {
24
31
  detailOpen = false;
25
32
  detailScroll = 0;
26
33
  detailTurn;
27
- detailView = "evidence";
34
+ feedback;
35
+ killConfirmation;
28
36
  readKeys = new Set();
29
37
  focus = "tabs";
30
38
  filterControlIndex = 0;
31
39
  menuLevel = "filter";
32
40
  selectorIndex = 0;
41
+ recipeScroll = 0;
33
42
  rowIndex = 0;
34
43
  selectedRun;
35
44
  subagentIndex = 0;
36
- tab = "communications";
45
+ tab = "recipe";
46
+ turnCache;
37
47
  constructor(options) {
38
48
  this.done = options.done;
49
+ this.killRun = options.killRun;
39
50
  this.ownerId = options.ownerId;
51
+ this.readTurns = options.readTurns ?? Inspector.readActorInspectorTurns;
40
52
  this.stateRoot = options.stateRoot;
41
53
  this.theme = options.theme;
42
54
  this.tui = options.tui;
43
- this.refreshTimer = setInterval(() => this.tui.requestRender(), 1000);
55
+ this.refreshTimer = setInterval(() => {
56
+ if (this.focus !== "list" && this.focus !== "detail")
57
+ this.turnCache = undefined;
58
+ this.tui.requestRender();
59
+ }, 1000);
44
60
  this.refreshTimer.unref?.();
45
61
  }
46
62
  handleInput(data) {
@@ -50,6 +66,22 @@ export class ActorInspectorOverlay {
50
66
  this.done();
51
67
  return;
52
68
  }
69
+ if (this.killConfirmation) {
70
+ if (matchesKey(data, "escape") || data.toLowerCase() === "n") {
71
+ this.feedback = { ok: false, message: "Kill cancelled." };
72
+ this.killConfirmation = undefined;
73
+ }
74
+ else if (matchesKey(data, "return") || data.toLowerCase() === "y") {
75
+ this.confirmKill();
76
+ }
77
+ this.tui.requestRender();
78
+ return;
79
+ }
80
+ if (data.toLowerCase() === "k") {
81
+ this.requestKill(runs);
82
+ this.tui.requestRender();
83
+ return;
84
+ }
53
85
  if (matchesKey(data, "escape")) {
54
86
  if (this.focus === "select") {
55
87
  if (this.menuLevel === "value") {
@@ -61,6 +93,8 @@ export class ActorInspectorOverlay {
61
93
  }
62
94
  else if (this.focus === "detail")
63
95
  this.backDetail();
96
+ else if (this.focus === "recipe")
97
+ this.focus = "tabs";
64
98
  else
65
99
  this.done();
66
100
  this.tui.requestRender();
@@ -80,19 +114,35 @@ export class ActorInspectorOverlay {
80
114
  this.tui.requestRender();
81
115
  return;
82
116
  }
117
+ if (this.focus === "recipe") {
118
+ if (matchesKey(data, "up")) {
119
+ if (this.recipeScroll === 0)
120
+ this.focus = "tabs";
121
+ else
122
+ this.recipeScroll -= 1;
123
+ }
124
+ else if (matchesKey(data, "down"))
125
+ this.recipeScroll += 1;
126
+ else if (matchesKey(data, "pageUp"))
127
+ this.recipeScroll = Math.max(0, this.recipeScroll - this.contentViewportRows());
128
+ else if (matchesKey(data, "pageDown"))
129
+ this.recipeScroll += this.contentViewportRows();
130
+ else if (matchesKey(data, "left"))
131
+ this.focus = "tabs";
132
+ this.tui.requestRender();
133
+ return;
134
+ }
83
135
  if (this.focus === "detail") {
84
136
  if (matchesKey(data, "up"))
85
137
  this.detailScroll = Math.max(0, this.detailScroll - 1);
86
138
  else if (matchesKey(data, "down"))
87
139
  this.detailScroll += 1;
140
+ else if (matchesKey(data, "pageUp"))
141
+ this.detailScroll = Math.max(0, this.detailScroll - this.contentViewportRows());
142
+ else if (matchesKey(data, "pageDown"))
143
+ this.detailScroll += this.contentViewportRows();
88
144
  else if (matchesKey(data, "left"))
89
145
  this.backDetail();
90
- else if ((matchesKey(data, "right") || matchesKey(data, "return")) &&
91
- this.detailView === "evidence" &&
92
- this.tab === "turns") {
93
- this.detailView = "transcript";
94
- this.detailScroll = 0;
95
- }
96
146
  this.tui.requestRender();
97
147
  return;
98
148
  }
@@ -108,12 +158,18 @@ export class ActorInspectorOverlay {
108
158
  }
109
159
  else if (this.focus === "tabs") {
110
160
  if (matchesKey(data, "left") || matchesKey(data, "right")) {
111
- this.tab = this.tab === "communications" ? "turns" : "communications";
161
+ const current = INSPECTOR_TABS.indexOf(this.tab);
162
+ const direction = matchesKey(data, "right") ? 1 : -1;
163
+ this.tab = INSPECTOR_TABS[(current + direction + INSPECTOR_TABS.length) % INSPECTOR_TABS.length];
112
164
  this.filterControlIndex = 0;
165
+ this.recipeScroll = 0;
113
166
  this.rowIndex = 0;
114
167
  }
115
168
  else if (matchesKey(data, "up"))
116
169
  this.focus = "runs";
170
+ else if ((matchesKey(data, "down") || matchesKey(data, "return")) &&
171
+ this.tab === "recipe")
172
+ this.focus = "recipe";
117
173
  else if (matchesKey(data, "down") && this.listItemCount() > 0)
118
174
  this.focus = "list";
119
175
  else if (matchesKey(data, "return"))
@@ -121,7 +177,13 @@ export class ActorInspectorOverlay {
121
177
  }
122
178
  else if (this.focus === "list") {
123
179
  const count = this.tab === "communications" ? this.communicationPreviews().length : this.turnItems().length;
124
- if (matchesKey(data, "up")) {
180
+ if (matchesKey(data, "left"))
181
+ this.focus = "tabs";
182
+ else if (matchesKey(data, "pageUp"))
183
+ this.rowIndex = Math.max(0, this.rowIndex - this.contentViewportRows());
184
+ else if (matchesKey(data, "pageDown"))
185
+ this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + this.contentViewportRows());
186
+ else if (matchesKey(data, "up")) {
125
187
  if (this.rowIndex === 0)
126
188
  this.focus = "tabs";
127
189
  else
@@ -167,11 +229,23 @@ export class ActorInspectorOverlay {
167
229
  else
168
230
  lines.push(this.border("├", "", "┤", innerWidth));
169
231
  this.contentStripeIndices = [];
170
- const content = this.detailOpen
171
- ? this.renderDetail(innerWidth)
172
- : this.selectedRun
173
- ? this.renderTimeline(innerWidth)
174
- : this.renderRunSelector(innerWidth);
232
+ const baseContent = this.killConfirmation
233
+ ? this.renderKillConfirmation()
234
+ : this.detailOpen
235
+ ? this.renderDetail(innerWidth)
236
+ : this.selectedRun
237
+ ? this.renderTimeline(innerWidth)
238
+ : this.renderRunSelector(innerWidth);
239
+ const content = this.feedback && !this.killConfirmation
240
+ ? [
241
+ this.theme.fg(this.feedback.ok
242
+ ? "success"
243
+ : /^Kill (failed|rejected):/.test(this.feedback.message)
244
+ ? "error"
245
+ : "warning", ` ${this.feedback.message}`),
246
+ ...baseContent.slice(0, Math.max(0, this.contentViewportRows() - 1)),
247
+ ]
248
+ : baseContent;
175
249
  const contentViewportRows = this.contentViewportRows();
176
250
  const selectorRows = selector.slice(1);
177
251
  for (let index = 0; index < Math.max(content.length, contentViewportRows); index += 1) {
@@ -215,7 +289,9 @@ export class ActorInspectorOverlay {
215
289
  }
216
290
  selectRun(run, index) {
217
291
  this.selectedRun = run;
292
+ this.turnCache = undefined;
218
293
  this.runIndex = index;
294
+ this.recipeScroll = 0;
219
295
  this.rowIndex = 0;
220
296
  this.subagentIndex = 0;
221
297
  this.communicationFrom = "all";
@@ -229,7 +305,67 @@ export class ActorInspectorOverlay {
229
305
  if (next)
230
306
  this.selectRun(next.run, nextIndex);
231
307
  }
308
+ requestKill(runs) {
309
+ this.feedback = undefined;
310
+ if (this.focus !== "runs") {
311
+ this.feedback = { ok: false, message: "Focus Run before using Kill." };
312
+ return;
313
+ }
314
+ const run = runs.find((item) => item.run === this.selectedRun);
315
+ if (!run) {
316
+ this.feedback = { ok: false, message: "Kill unavailable: no owned run." };
317
+ return;
318
+ }
319
+ if (!this.killRun) {
320
+ this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
321
+ return;
322
+ }
323
+ if (run.status !== "running") {
324
+ this.feedback = {
325
+ ok: false,
326
+ message: `Kill unavailable: run is ${run.status}.`,
327
+ };
328
+ return;
329
+ }
330
+ if (!run.runInstanceId) {
331
+ this.feedback = {
332
+ ok: false,
333
+ message: "Kill unavailable: run generation unavailable.",
334
+ };
335
+ return;
336
+ }
337
+ this.killConfirmation = {
338
+ run: run.run,
339
+ runInstanceId: run.runInstanceId,
340
+ };
341
+ }
342
+ confirmKill() {
343
+ const confirmation = this.killConfirmation;
344
+ this.killConfirmation = undefined;
345
+ if (!confirmation || !this.killRun) {
346
+ this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
347
+ return;
348
+ }
349
+ try {
350
+ this.feedback = this.killRun(confirmation.run, confirmation.runInstanceId);
351
+ }
352
+ catch (error) {
353
+ const message = error instanceof Error ? error.message : String(error);
354
+ this.feedback = {
355
+ ok: false,
356
+ message: `Kill failed: ${message.replaceAll(/\s+/g, " ").slice(0, 160)}.`,
357
+ };
358
+ }
359
+ }
360
+ renderKillConfirmation() {
361
+ return [
362
+ this.theme.fg("error", ` Kill running actor run:${this.killConfirmation?.run}?`),
363
+ this.theme.fg("muted", " This sends canonical control.kill and cannot be undone."),
364
+ ];
365
+ }
232
366
  listItemCount() {
367
+ if (this.tab === "recipe")
368
+ return 0;
233
369
  return this.tab === "communications"
234
370
  ? this.communicationPreviews().length
235
371
  : this.turnItems().length;
@@ -239,24 +375,35 @@ export class ActorInspectorOverlay {
239
375
  const runControl = stripVTControlCharacters(this.renderRunControl());
240
376
  return Math.max(0, runControl.indexOf("Run:") - 2);
241
377
  }
242
- const label = this.tab === "communications" ? "Messages" : "Turns";
378
+ const label = this.tab === "communications"
379
+ ? "Messages"
380
+ : this.tab === "turns"
381
+ ? "Turns"
382
+ : "Recipe";
243
383
  const tabs = stripVTControlCharacters(this.renderTabs());
244
384
  return Math.max(0, tabs.indexOf(label) - 2);
245
385
  }
246
386
  renderKeyHints() {
247
387
  const hint = (keys, description) => `${this.theme.fg("accent", keys)}${this.theme.fg("dim", ` ${description}`)}`;
388
+ if (this.killConfirmation)
389
+ return ` ${hint("y/enter", "confirm kill")} ${hint("n/esc", "cancel")}`;
248
390
  if (this.focus === "select")
249
391
  return this.menuLevel === "value"
250
392
  ? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
251
393
  : ` ${hint("↑↓", "option")} ${hint("→/enter", "open")} ${hint("←/esc", "back")}`;
394
+ if (this.focus === "recipe")
395
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("↑ at top", "tabs")} ${hint("←/esc", "tabs")}`;
252
396
  if (this.focus === "detail")
253
- return this.tab === "turns" && this.detailView === "evidence"
254
- ? ` ${hint("↑↓", "scroll")} ${hint("→/enter", "readable")} ${hint("←/esc", "back")}`
255
- : ` ${hint("↑↓", "scroll")} ${hint("←/esc", this.detailView === "transcript" ? "evidence" : "back")}`;
397
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("←/esc", "back")}`;
256
398
  if (this.focus === "list")
257
- return ` ${hint("↑↓", "row")} ${hint("→/enter", "open")} ${hint("esc", "close")}`;
258
- if (this.focus === "runs")
259
- return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")} ${hint("esc", "close")}`;
399
+ return ` ${hint("↑↓/pgup/pgdn", "row")} ${hint("→/enter", "open")} ${hint("←", "tabs")} ${hint("esc", "close")}`;
400
+ if (this.focus === "runs") {
401
+ const run = this.runs().find((item) => item.run === this.selectedRun);
402
+ const killHint = this.killRun && run?.status === "running" && run.runInstanceId
403
+ ? ` ${hint("k", "kill")}`
404
+ : "";
405
+ return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")}${killHint} ${hint("esc", "close")}`;
406
+ }
260
407
  return ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
261
408
  }
262
409
  renderRunControl() {
@@ -306,7 +453,7 @@ export class ActorInspectorOverlay {
306
453
  const subagent = this.subagents()[this.subagentIndex] ?? "all";
307
454
  const messagesLabel = `Messages${messageFilters.length > 0 ? ` (${messageFilters.join(", ")})` : ""}`;
308
455
  const turnsLabel = `Turns${subagent === "all" ? "" : ` (${subagent})`}`;
309
- return `${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
456
+ return `${tab("recipe", "Recipe")} ${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
310
457
  }
311
458
  renderRunSelector(width) {
312
459
  const runs = this.runs();
@@ -331,6 +478,8 @@ export class ActorInspectorOverlay {
331
478
  this.selectedRun = undefined;
332
479
  return [this.theme.fg("warning", " Selected run is no longer owned by this session")];
333
480
  }
481
+ if (this.tab === "recipe")
482
+ return this.renderRecipe(width);
334
483
  const rows = this.tab === "communications"
335
484
  ? this.communicationRows()
336
485
  : this.turnRows();
@@ -363,6 +512,28 @@ export class ActorInspectorOverlay {
363
512
  : ` ${row}`;
364
513
  });
365
514
  }
515
+ renderRecipe(width) {
516
+ if (!this.selectedRun)
517
+ return [];
518
+ const recipe = Inspector.readActorInspectorRecipe(path.join(this.stateRoot, this.selectedRun));
519
+ const logicalLines = [
520
+ ...this.detailSection("Identity", this.readableValueLines(recipe.identity, " ")),
521
+ ...(recipe.definition
522
+ ? this.detailSection("Recipe", this.readableValueLines(recipe.definition, " "))
523
+ : []),
524
+ ...this.detailSection("Launch", this.readableValueLines(recipe.launch, " ")),
525
+ ...(recipe.composition.length > 0
526
+ ? this.detailSection("Composition", this.readableValueLines(recipe.composition, " "))
527
+ : []),
528
+ ...this.detailSection("Diagnostics", recipe.diagnostics),
529
+ ];
530
+ const wrapped = this.wrapDetailLines(logicalLines.length > 0 ? logicalLines : [" No persisted recipe evidence"], width);
531
+ const viewportRows = this.contentViewportRows();
532
+ const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
533
+ this.recipeScroll = Math.min(this.recipeScroll, maxStart);
534
+ this.contentStripeIndices = wrapped.stripeIndices.slice(this.recipeScroll, this.recipeScroll + viewportRows);
535
+ return wrapped.lines.slice(this.recipeScroll, this.recipeScroll + viewportRows);
536
+ }
366
537
  communicationPreviews() {
367
538
  if (!this.selectedRun)
368
539
  return [];
@@ -419,16 +590,18 @@ export class ActorInspectorOverlay {
419
590
  ["inbox", preview.inbox_status],
420
591
  ["timestamp", preview.timestamp],
421
592
  ];
422
- return fields
593
+ const lines = fields
423
594
  .filter(([, value]) => value !== undefined)
424
595
  .map(([label, value]) => truncateToWidth(` ${label.padEnd(12, " ")} ${typeof value === "string" ? value : JSON.stringify(value)}`, width, "…"));
596
+ const viewportRows = this.contentViewportRows();
597
+ const maxStart = Math.max(0, lines.length - viewportRows);
598
+ this.detailScroll = Math.min(this.detailScroll, maxStart);
599
+ return lines.slice(this.detailScroll, this.detailScroll + viewportRows);
425
600
  }
426
601
  const turn = this.detailTurn;
427
602
  if (!turn)
428
603
  return [this.theme.fg("warning", " Turn row is no longer available")];
429
- const logicalLines = this.detailView === "transcript"
430
- ? this.turnTranscriptLines(turn)
431
- : this.turnEvidenceLines(turn);
604
+ const logicalLines = this.turnEvidenceLines(turn);
432
605
  const wrapped = this.wrapDetailLines(logicalLines, width);
433
606
  const viewportRows = this.contentViewportRows();
434
607
  const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
@@ -452,9 +625,31 @@ export class ActorInspectorOverlay {
452
625
  const leading = physicalLine.match(/^ */)?.[0] ?? "";
453
626
  const content = physicalLine.slice(leading.length);
454
627
  const contentWidth = Math.max(1, width - visibleWidth(leading));
455
- const wrapped = visibleWidth(physicalLine) <= width
456
- ? [physicalLine]
457
- : wrapTextWithAnsi(content, contentWidth).map((visualLine) => `${leading}${visualLine}`);
628
+ const labeled = /^(\s*(?:-\s+)?[^:]+:\s+)(\S.*)$/u.exec(physicalLine);
629
+ let wrapped;
630
+ if (visibleWidth(physicalLine) > width &&
631
+ labeled &&
632
+ visibleWidth(labeled[1]) < width) {
633
+ const prefix = labeled[1];
634
+ const value = labeled[2];
635
+ const firstWidth = Math.max(1, width - visibleWidth(prefix));
636
+ const first = this.wrapPreferred(value, firstWidth)[0] ??
637
+ this.takeVisiblePrefix(value, firstWidth);
638
+ const remainder = value.slice(first.length).trimStart();
639
+ const continuation = `${leading} `;
640
+ const continuationWidth = Math.max(1, width - visibleWidth(continuation));
641
+ wrapped = [
642
+ `${prefix}${first}`,
643
+ ...(remainder
644
+ ? this.wrapPreferred(remainder, continuationWidth).map((visualLine) => `${continuation}${visualLine}`)
645
+ : []),
646
+ ];
647
+ }
648
+ else {
649
+ wrapped = visibleWidth(physicalLine) <= width
650
+ ? [physicalLine]
651
+ : this.wrapPreferred(content, contentWidth).map((visualLine) => `${leading}${visualLine}`);
652
+ }
458
653
  for (const visualLine of wrapped) {
459
654
  if (!visualLine.trim())
460
655
  continue;
@@ -465,6 +660,40 @@ export class ActorInspectorOverlay {
465
660
  }
466
661
  return { lines: wrappedLines, stripeIndices };
467
662
  }
663
+ wrapPreferred(content, width) {
664
+ const lines = [];
665
+ let remaining = stripVTControlCharacters(content).trimEnd();
666
+ while (remaining && visibleWidth(remaining) > width) {
667
+ const candidate = this.takeVisiblePrefix(remaining, width);
668
+ let splitAt = -1;
669
+ let trimBoundary = false;
670
+ for (let index = 1; index < candidate.length; index += 1) {
671
+ if (/\s/u.test(candidate[index])) {
672
+ splitAt = index + 1;
673
+ trimBoundary = true;
674
+ }
675
+ else if (/[-+=/_,.:]/u.test(candidate[index])) {
676
+ splitAt = index;
677
+ trimBoundary = false;
678
+ }
679
+ }
680
+ if (splitAt <= 0)
681
+ splitAt = candidate.length;
682
+ const head = remaining.slice(0, splitAt).trimEnd();
683
+ if (!head) {
684
+ lines.push(candidate);
685
+ remaining = remaining.slice(candidate.length);
686
+ continue;
687
+ }
688
+ lines.push(head);
689
+ remaining = remaining.slice(splitAt);
690
+ if (trimBoundary)
691
+ remaining = remaining.trimStart();
692
+ }
693
+ if (remaining)
694
+ lines.push(remaining);
695
+ return lines;
696
+ }
468
697
  detailSection(title, content) {
469
698
  if (content.length === 0)
470
699
  return [];
@@ -508,9 +737,10 @@ export class ActorInspectorOverlay {
508
737
  }
509
738
  if (!item || typeof item !== "object")
510
739
  return [`${indent}- ${String(item)}`];
740
+ const [first = "", ...rest] = this.readableValueLines(item, `${indent} `, depth + 1);
511
741
  return [
512
- `${indent}-`,
513
- ...this.readableValueLines(item, `${indent} `, depth + 1),
742
+ `${indent}- ${first.trimStart()}`,
743
+ ...rest,
514
744
  ];
515
745
  });
516
746
  }
@@ -543,7 +773,7 @@ export class ActorInspectorOverlay {
543
773
  : "";
544
774
  lines.push(` Subagent ${subagent}${stage}`, DETAIL_SECTION_BREAK);
545
775
  if (turn.userText)
546
- lines.push(...this.detailSection("User", [turn.userText]));
776
+ lines.push(...this.detailSection("User", [this.readablePromptText(turn.userText)]));
547
777
  if (turn.thinking)
548
778
  lines.push(...this.detailSection("Thinking", [turn.thinking]));
549
779
  if (turn.assistantText)
@@ -586,12 +816,6 @@ export class ActorInspectorOverlay {
586
816
  ` ${turn.promptFile}${turn.promptBytes ? ` (${turn.promptBytes} bytes)` : ""}`,
587
817
  ]
588
818
  : []),
589
- ...(turn.recipeContext === undefined
590
- ? []
591
- : [
592
- "Recipe Context",
593
- ...this.readableValueLines(turn.recipeContext, " "),
594
- ]),
595
819
  ];
596
820
  lines.push(...this.detailSection("Provenance", provenance));
597
821
  return lines.length > 0 ? lines : [" No persisted turn evidence"];
@@ -600,34 +824,18 @@ export class ActorInspectorOverlay {
600
824
  const wrapped = /^\s*<file\b[^>]*>\s*([\s\S]*?)\s*<\/file>\s*$/i.exec(text);
601
825
  return wrapped?.[1]?.trim() || text;
602
826
  }
603
- turnTranscriptLines(turn) {
604
- const lines = [];
605
- if (turn.userText)
606
- lines.push(...this.detailSection("User", [this.readablePromptText(turn.userText)]));
607
- if (turn.thinking)
608
- lines.push(...this.detailSection("Thinking", [turn.thinking]));
609
- if (turn.assistantText)
610
- lines.push(...this.detailSection("Assistant", [turn.assistantText]));
611
- for (const tool of turn.toolCalls) {
612
- const content = [
613
- ...(tool.arguments === undefined
614
- ? []
615
- : ["Input", ...this.readableValueLines(tool.arguments, " ")]),
616
- ...(tool.result === undefined
617
- ? []
618
- : ["Result", ...this.readableValueLines(tool.result, " ")]),
619
- ];
620
- lines.push(...this.detailSection(`Tool (${tool.name}${tool.resultError ? ", Error" : ""})`, content));
621
- }
622
- if (turn.error)
623
- lines.push(...this.detailSection("Error", [turn.error]));
624
- return lines.length > 0 ? lines : [" No readable turn content"];
625
- }
626
- turnItems() {
827
+ allTurnItems() {
627
828
  if (!this.selectedRun)
628
829
  return [];
830
+ if (this.turnCache?.run === this.selectedRun)
831
+ return this.turnCache.turns;
832
+ const turns = this.readTurns(path.join(this.stateRoot, this.selectedRun));
833
+ this.turnCache = { run: this.selectedRun, turns };
834
+ return turns;
835
+ }
836
+ turnItems() {
629
837
  const selectedSubagent = this.subagents()[this.subagentIndex];
630
- return Inspector.readActorInspectorTurns(path.join(this.stateRoot, this.selectedRun))
838
+ return this.allTurnItems()
631
839
  .filter((turn) => !selectedSubagent ||
632
840
  selectedSubagent === "all" ||
633
841
  turn.commandId === selectedSubagent)
@@ -662,7 +870,7 @@ export class ActorInspectorOverlay {
662
870
  subagents() {
663
871
  if (!this.selectedRun || this.tab !== "turns")
664
872
  return ["all"];
665
- const ids = Inspector.readActorInspectorTurns(path.join(this.stateRoot, this.selectedRun)).map((turn) => turn.commandId);
873
+ const ids = this.allTurnItems().map((turn) => turn.commandId);
666
874
  return ["all", ...new Set(ids)];
667
875
  }
668
876
  valueOptions() {
@@ -795,17 +1003,11 @@ export class ActorInspectorOverlay {
795
1003
  else
796
1004
  this.detailTurn = this.turnItems()[this.rowIndex];
797
1005
  this.detailScroll = 0;
798
- this.detailView = "evidence";
799
1006
  this.detailOpen = Boolean(this.detailCommunication || this.detailTurn);
800
1007
  if (this.detailOpen)
801
1008
  this.focus = "detail";
802
1009
  }
803
1010
  backDetail() {
804
- if (this.detailView === "transcript") {
805
- this.detailView = "evidence";
806
- this.detailScroll = 0;
807
- return;
808
- }
809
1011
  this.closeDetail();
810
1012
  }
811
1013
  closeDetail() {
@@ -813,7 +1015,6 @@ export class ActorInspectorOverlay {
813
1015
  this.detailCommunication = undefined;
814
1016
  this.detailTurn = undefined;
815
1017
  this.detailScroll = 0;
816
- this.detailView = "evidence";
817
1018
  this.focus = "list";
818
1019
  }
819
1020
  border(left, title, right, width) {
@@ -55,10 +55,19 @@ export interface ActorInspectorPreviewReadOptions {
55
55
  }
56
56
  export interface ActorInspectorRunItem {
57
57
  run: string;
58
+ runInstanceId?: string;
58
59
  status: string;
59
60
  updatedAt?: string;
60
61
  }
61
62
  export declare function readActorInspectorRuns(stateRoot: string, ownerId: string): ActorInspectorRunItem[];
63
+ export interface ActorInspectorRecipeView {
64
+ composition: Array<Record<string, unknown>>;
65
+ definition?: Record<string, unknown>;
66
+ diagnostics: string[];
67
+ identity: Record<string, unknown>;
68
+ launch: Record<string, unknown>;
69
+ }
70
+ export declare function readActorInspectorRecipe(stateDir: string): ActorInspectorRecipeView;
62
71
  export interface ActorInspectorTurnItem extends SessionEvidence.SessionEvidenceTurn {
63
72
  commandId: string;
64
73
  diagnostics: string[];
@@ -19,10 +19,14 @@ export function readActorInspectorRuns(stateRoot, ownerId) {
19
19
  const stateDir = path.join(stateRoot, entry.name);
20
20
  if (!matchesOwner(stateDir, ownerId))
21
21
  return [];
22
+ const runMeta = readJsonFileResilient(path.join(stateDir, "run.json"), {}).value;
22
23
  const progress = readJsonFileResilient(path.join(stateDir, "progress.json"), {}).value;
23
24
  const result = readJsonFileResilient(path.join(stateDir, "result.json"), {}).value;
24
25
  return [{
25
26
  run: entry.name,
27
+ ...(typeof runMeta.run_instance_id === "string"
28
+ ? { runInstanceId: runMeta.run_instance_id }
29
+ : {}),
26
30
  status: String(progress.phase ?? (Object.keys(result).length ? "terminal" : "unknown")),
27
31
  ...(typeof progress.updatedAt === "string"
28
32
  ? { updatedAt: progress.updatedAt }
@@ -42,6 +46,50 @@ export function readActorInspectorRuns(stateRoot, ownerId) {
42
46
  return [];
43
47
  }
44
48
  }
49
+ export function readActorInspectorRecipe(stateDir) {
50
+ const result = readJsonFileResilient(path.join(stateDir, "run.json"), {});
51
+ const meta = result.value;
52
+ const contexts = Array.isArray(meta.recipe_context_records)
53
+ ? meta.recipe_context_records
54
+ .filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)))
55
+ : [];
56
+ const primary = contexts.find((entry) => entry.depth === 0) ?? contexts[0];
57
+ const definition = primary?.recipe && typeof primary.recipe === "object" &&
58
+ !Array.isArray(primary.recipe)
59
+ ? primary.recipe
60
+ : undefined;
61
+ const redacted = (value) => SessionEvidence.redactSessionEvidenceValue(value);
62
+ const redactedRecord = (value) => redacted(Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)));
63
+ return {
64
+ composition: contexts.map((entry) => redactedRecord({
65
+ ...(typeof entry.alias === "string" ? { alias: entry.alias } : {}),
66
+ depth: entry.depth,
67
+ file: entry.file,
68
+ import_path: entry.import_path,
69
+ name: entry.name,
70
+ })),
71
+ ...(definition
72
+ ? { definition: redactedRecord(definition) }
73
+ : {}),
74
+ diagnostics: result.diagnostics.map((diagnostic) => diagnostic.message),
75
+ identity: redactedRecord({
76
+ run: meta.run,
77
+ recipe: meta.recipe,
78
+ recipe_file: meta.recipe_file,
79
+ launch_source: meta.launch_source,
80
+ }),
81
+ launch: redactedRecord({
82
+ cwd: meta.cwd,
83
+ template: meta.template,
84
+ values: meta.values,
85
+ model_policy: meta.model_policy,
86
+ mailbox: meta.mailbox,
87
+ artifacts: meta.artifacts,
88
+ notification_policy: meta.notification_policy,
89
+ retire_when: meta.retire_when,
90
+ }),
91
+ };
92
+ }
45
93
  function ownedSessionPath(stateDir, sessionFile) {
46
94
  if (path.isAbsolute(sessionFile))
47
95
  return undefined;
@@ -17,6 +17,7 @@ export interface RunObservation {
17
17
  artifacts?: Record<string, string>;
18
18
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
19
19
  modelPolicy?: Record<string, unknown>;
20
+ notificationPolicy?: "normal" | "silent";
20
21
  recipeFile?: string;
21
22
  terminalHandled?: boolean;
22
23
  retireWhen?: string;