@llblab/pi-actors 0.49.2 → 0.50.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/BACKLOG.md +0 -10
- package/CHANGELOG.md +10 -1
- package/README.md +19 -3
- package/dist/lib/inspector-overlay.d.ts +3 -0
- package/dist/lib/inspector-overlay.js +120 -38
- package/dist/lib/limits.d.ts +1 -0
- package/dist/lib/limits.js +1 -0
- package/dist/lib/runs-artifacts.js +24 -4
- package/dist/lib/session-evidence.d.ts +1 -0
- package/dist/lib/session-evidence.js +3 -2
- package/dist/lib/state-readers.d.ts +5 -1
- package/dist/lib/state-readers.js +30 -3
- package/dist/lib/tools-access.d.ts +1 -0
- package/dist/lib/tools-access.js +13 -11
- package/dist/lib/tools-inspect.js +5 -5
- package/dist/lib/tools-message.d.ts +1 -0
- package/dist/lib/tools-message.js +3 -1
- package/docs/README.md +1 -0
- package/docs/actor-inspector.md +21 -5
- package/docs/async-runs.md +16 -4
- package/docs/command-templates.md +5 -4
- package/docs/inspection.md +83 -0
- package/docs/recipe-library.md +1 -1
- package/docs/template-recipes.md +225 -66
- package/docs/tool-registry.md +24 -3
- package/lib/inspector-overlay.ts +161 -33
- package/lib/limits.ts +1 -0
- package/lib/runs-artifacts.ts +34 -4
- package/lib/session-evidence.ts +7 -2
- package/lib/state-readers.ts +45 -3
- package/lib/tools-access.ts +26 -12
- package/lib/tools-inspect.ts +9 -5
- package/lib/tools-message.ts +8 -1
- package/package.json +1 -1
package/lib/inspector-overlay.ts
CHANGED
|
@@ -10,6 +10,7 @@ import * as path from "node:path";
|
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import {
|
|
12
12
|
matchesKey,
|
|
13
|
+
sliceByColumn,
|
|
13
14
|
truncateToWidth,
|
|
14
15
|
visibleWidth,
|
|
15
16
|
type TUI,
|
|
@@ -30,6 +31,11 @@ export type ActorInspectorTab = "recipe" | "trace" | "control";
|
|
|
30
31
|
type InspectorFocus = "runs" | "tabs" | "document" | "list" | "detail" | "select";
|
|
31
32
|
type SelectorMode = "run" | "source";
|
|
32
33
|
|
|
34
|
+
interface SelectorOption {
|
|
35
|
+
content: string;
|
|
36
|
+
preserveColors?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
export interface ActorInspectorActionResult {
|
|
34
40
|
ok: boolean;
|
|
35
41
|
message: string;
|
|
@@ -73,6 +79,7 @@ export class ActorInspectorOverlay {
|
|
|
73
79
|
private readonly theme: Theme;
|
|
74
80
|
private readonly tui: TUI;
|
|
75
81
|
private readonly refreshTimer: NodeJS.Timeout;
|
|
82
|
+
private contentSelectedRows = new Set<number>();
|
|
76
83
|
private contentStripeIndices: number[] = [];
|
|
77
84
|
private documentScroll = 0;
|
|
78
85
|
private detailScroll = 0;
|
|
@@ -244,10 +251,11 @@ export class ActorInspectorOverlay {
|
|
|
244
251
|
if (this.killConfirmation) return this.renderKillDialog(innerWidth);
|
|
245
252
|
|
|
246
253
|
const lines = [this.border("╭", " Actor Inspector ", "╮", innerWidth)];
|
|
247
|
-
lines.push(this.row(this.renderRunControl(run, runs.length - this.runIndex), innerWidth));
|
|
254
|
+
lines.push(this.row(this.renderRunControl(run, runs.length - this.runIndex - 1), innerWidth));
|
|
248
255
|
lines.push(this.row(this.renderTabs(run), innerWidth));
|
|
249
256
|
lines.push(this.border("├", "", "┤", innerWidth));
|
|
250
257
|
|
|
258
|
+
this.contentSelectedRows.clear();
|
|
251
259
|
this.contentStripeIndices = [];
|
|
252
260
|
let content = this.renderContent(run, innerWidth);
|
|
253
261
|
if (this.feedback) {
|
|
@@ -257,6 +265,9 @@ export class ActorInspectorOverlay {
|
|
|
257
265
|
? "error"
|
|
258
266
|
: "warning";
|
|
259
267
|
content = [this.theme.fg(color, ` ${this.feedback.message}`), ...content];
|
|
268
|
+
this.contentSelectedRows = new Set(
|
|
269
|
+
[...this.contentSelectedRows].map((index) => index + 1),
|
|
270
|
+
);
|
|
260
271
|
this.contentStripeIndices = [0, ...this.contentStripeIndices];
|
|
261
272
|
}
|
|
262
273
|
const viewportRows = this.contentViewportRows();
|
|
@@ -266,9 +277,27 @@ export class ActorInspectorOverlay {
|
|
|
266
277
|
content[index] ?? "",
|
|
267
278
|
innerWidth,
|
|
268
279
|
this.contentStripeIndices[index] ?? index,
|
|
280
|
+
this.contentSelectedRows.has(index),
|
|
269
281
|
));
|
|
270
282
|
}
|
|
271
283
|
lines.push(this.footerBorder(this.renderKeyHints(), innerWidth));
|
|
284
|
+
if (this.focus === "select") {
|
|
285
|
+
const runSelector = this.selectorMode === "run";
|
|
286
|
+
const startRow = runSelector ? 2 : 3;
|
|
287
|
+
const horizontalOffset = runSelector ? 4 : 12;
|
|
288
|
+
const availableRows = Math.max(3, lines.length - startRow - 1);
|
|
289
|
+
const menu = this.renderSelector(run, availableRows);
|
|
290
|
+
for (const [offset, menuLine] of menu.entries()) {
|
|
291
|
+
const target = startRow + offset;
|
|
292
|
+
if (target >= lines.length - 1) break;
|
|
293
|
+
lines[target] = this.compositeMenuLine(
|
|
294
|
+
lines[target]!,
|
|
295
|
+
menuLine,
|
|
296
|
+
innerWidth,
|
|
297
|
+
horizontalOffset,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
272
301
|
return lines;
|
|
273
302
|
}
|
|
274
303
|
|
|
@@ -428,10 +457,10 @@ export class ActorInspectorOverlay {
|
|
|
428
457
|
private renderKillDialog(innerWidth: number): string[] {
|
|
429
458
|
const confirmation = this.killConfirmation!;
|
|
430
459
|
const cancel = this.killDialogChoice === "cancel"
|
|
431
|
-
? this.theme.bg("
|
|
460
|
+
? this.theme.bg("selectedBg", this.theme.fg("accent", " Cancel "))
|
|
432
461
|
: this.theme.fg("muted", " Cancel ");
|
|
433
462
|
const kill = this.killDialogChoice === "kill"
|
|
434
|
-
? this.theme.bg("
|
|
463
|
+
? this.theme.bg("selectedBg", this.theme.fg("error", " Kill actor "))
|
|
435
464
|
: this.theme.fg("error", " Kill actor ");
|
|
436
465
|
const body = [
|
|
437
466
|
"",
|
|
@@ -464,23 +493,32 @@ export class ActorInspectorOverlay {
|
|
|
464
493
|
const suffix = active ? this.theme.fg("accent", " → ") : " ";
|
|
465
494
|
if (!run) {
|
|
466
495
|
const empty = this.theme.fg("muted", `${prefix}Run: none${suffix}`);
|
|
467
|
-
return
|
|
496
|
+
return active ? this.theme.bg("selectedBg", empty) : empty;
|
|
468
497
|
}
|
|
469
498
|
const value = `${prefix}${this.theme.fg("muted", "Run: ")}${this.theme.fg("text", `#${sequence}`)} ${this.theme.fg("accent", run.run)} ${this.theme.fg(this.statusColor(run.status), run.status)}${suffix}`;
|
|
470
|
-
return
|
|
499
|
+
return active ? this.theme.bg("selectedBg", value) : value;
|
|
471
500
|
}
|
|
472
501
|
|
|
473
502
|
private renderTabs(run?: ActorInspector.ActorInspectorRunItem): string {
|
|
474
503
|
const source = this.traceSource(run);
|
|
475
504
|
return TABS.map((tab, index) => {
|
|
476
505
|
const base = `${tab[0]!.toUpperCase()}${tab.slice(1)}`;
|
|
477
|
-
const label = tab === "trace" && source !== "all" ? `${base}
|
|
506
|
+
const label = tab === "trace" && source !== "all" ? `${base}: ${source}` : base;
|
|
478
507
|
const selected = index === this.tabIndex;
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
508
|
+
const bracketed = selected && this.focus !== "runs";
|
|
509
|
+
if (!selected) {
|
|
510
|
+
const display = bracketed ? `[ ${label} ]` : ` ${label} `;
|
|
511
|
+
return this.theme.fg("muted", ` ${display} `);
|
|
512
|
+
}
|
|
513
|
+
const coloredLabel = tab === "trace" && source !== "all"
|
|
514
|
+
? `${this.theme.fg("accent", `${base}:`)} ${this.theme.fg("text", source)}`
|
|
515
|
+
: this.theme.fg("accent", label);
|
|
516
|
+
const colored = bracketed
|
|
517
|
+
? ` ${this.theme.fg("accent", "[ ")}${coloredLabel}${this.theme.fg("accent", " ]")} `
|
|
518
|
+
: ` ${coloredLabel} `;
|
|
519
|
+
const active = this.focus === "tabs"
|
|
520
|
+
|| (this.focus === "select" && this.selectorMode === "source");
|
|
521
|
+
return active ? this.theme.bg("selectedBg", colored) : colored;
|
|
484
522
|
}).join(" ");
|
|
485
523
|
}
|
|
486
524
|
|
|
@@ -489,7 +527,6 @@ export class ActorInspectorOverlay {
|
|
|
489
527
|
width: number,
|
|
490
528
|
): string[] {
|
|
491
529
|
if (!run) return [this.theme.fg("muted", " No owned actor runs")];
|
|
492
|
-
if (this.focus === "select") return this.renderSelector(run);
|
|
493
530
|
if (this.focus === "detail") return this.renderTraceDetail(width);
|
|
494
531
|
if (this.tab === "trace") return this.renderTraceList(run);
|
|
495
532
|
const stateDir = path.join(this.stateRoot, run.run);
|
|
@@ -509,31 +546,64 @@ export class ActorInspectorOverlay {
|
|
|
509
546
|
);
|
|
510
547
|
}
|
|
511
548
|
|
|
512
|
-
private renderSelector(
|
|
549
|
+
private renderSelector(
|
|
550
|
+
run: ActorInspector.ActorInspectorRunItem,
|
|
551
|
+
availableRows: number,
|
|
552
|
+
): string[] {
|
|
513
553
|
const options = this.selectorMode === "run"
|
|
514
|
-
? this.
|
|
515
|
-
: this.traceSources(run).map((source) =>
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
554
|
+
? this.runSelectorOptions()
|
|
555
|
+
: this.traceSources(run).map((source) => ({
|
|
556
|
+
content: `${this.theme.fg("accent", "Trace:")} ${this.theme.fg("text", source)}`,
|
|
557
|
+
preserveColors: true,
|
|
558
|
+
}));
|
|
559
|
+
return this.renderMenuBox(options, this.selectorIndex, availableRows);
|
|
519
560
|
}
|
|
520
561
|
|
|
521
|
-
private
|
|
562
|
+
private runSelectorOptions(): SelectorOption[] {
|
|
563
|
+
const runs = this.runs();
|
|
564
|
+
const sequenceWidth = Math.max(
|
|
565
|
+
2,
|
|
566
|
+
...runs.map((_item, index) => `#${runs.length - index - 1}`.length),
|
|
567
|
+
);
|
|
568
|
+
const runWidth = Math.max(1, ...runs.map((item) => visibleWidth(item.run)));
|
|
569
|
+
return runs.map((item, index) => ({
|
|
570
|
+
content: [
|
|
571
|
+
this.theme.fg("text", this.fit(`#${runs.length - index - 1}`, sequenceWidth)),
|
|
572
|
+
this.theme.fg("accent", this.fit(item.run, runWidth)),
|
|
573
|
+
this.theme.fg(this.statusColor(item.status), item.status),
|
|
574
|
+
].join(" "),
|
|
575
|
+
preserveColors: true,
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private renderMenuBox(
|
|
580
|
+
options: SelectorOption[],
|
|
581
|
+
focusedIndex: number,
|
|
582
|
+
availableRows: number,
|
|
583
|
+
): string[] {
|
|
522
584
|
if (options.length === 0) return [this.theme.fg("muted", " No options")];
|
|
523
|
-
const visibleLimit = Math.max(
|
|
585
|
+
const visibleLimit = Math.max(
|
|
586
|
+
1,
|
|
587
|
+
Math.min(availableRows - 2, options.length),
|
|
588
|
+
);
|
|
524
589
|
const maxStart = Math.max(0, options.length - visibleLimit);
|
|
525
590
|
const start = Math.max(0, Math.min(focusedIndex - Math.floor(visibleLimit / 2), maxStart));
|
|
526
591
|
const visible = options.slice(start, start + visibleLimit);
|
|
527
|
-
const width = Math.max(8, ...options.map((option) => visibleWidth(option) + 4));
|
|
592
|
+
const width = Math.max(8, ...options.map((option) => visibleWidth(option.content) + 4));
|
|
528
593
|
const border = (left: string, right: string, marker = "") =>
|
|
529
594
|
this.theme.fg("borderAccent", `${left}${marker}${"─".repeat(Math.max(0, width - visibleWidth(marker)))}${right}`);
|
|
530
595
|
return [
|
|
531
596
|
border("╭", "╮", start > 0 ? "↑" : ""),
|
|
532
597
|
...visible.map((option, offset) => {
|
|
533
598
|
const index = start + offset;
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
599
|
+
const marker = index === focusedIndex
|
|
600
|
+
? this.theme.fg("accent", " ▶ ")
|
|
601
|
+
: " ";
|
|
602
|
+
const row = this.fit(`${marker}${option.content}`, width);
|
|
603
|
+
const colored = index === focusedIndex && !option.preserveColors
|
|
604
|
+
? this.theme.fg("accent", row)
|
|
605
|
+
: row;
|
|
606
|
+
const styled = index === focusedIndex ? this.theme.bg("selectedBg", colored) : colored;
|
|
537
607
|
return `${this.theme.fg("borderAccent", "│")}${styled}${this.theme.fg("borderAccent", "│")}`;
|
|
538
608
|
}),
|
|
539
609
|
border("╰", "╯", start + visible.length < options.length ? "↓" : ""),
|
|
@@ -562,6 +632,14 @@ export class ActorInspectorOverlay {
|
|
|
562
632
|
this.contentStripeIndices = items
|
|
563
633
|
.slice(start, start + viewportRows)
|
|
564
634
|
.map((_item, offset) => start + offset);
|
|
635
|
+
const selectedViewportRow = banner.length + this.rowIndex - start;
|
|
636
|
+
if (
|
|
637
|
+
this.focus === "list" &&
|
|
638
|
+
selectedViewportRow >= banner.length &&
|
|
639
|
+
selectedViewportRow < viewportRows
|
|
640
|
+
) {
|
|
641
|
+
this.contentSelectedRows.add(selectedViewportRow);
|
|
642
|
+
}
|
|
565
643
|
return [...banner, ...items.slice(start, start + viewportRows - banner.length).map((item, offset) => {
|
|
566
644
|
const index = start + offset;
|
|
567
645
|
const detail = item.detail && typeof item.detail === "object"
|
|
@@ -575,7 +653,7 @@ export class ActorInspectorOverlay {
|
|
|
575
653
|
const prefix = this.focus === "list" && index === this.rowIndex
|
|
576
654
|
? this.theme.fg("accent", " ▶ ")
|
|
577
655
|
: " ";
|
|
578
|
-
const row = `${prefix}${this.theme.fg("text", `#${items.length - index}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
|
|
656
|
+
const row = `${prefix}${this.theme.fg("text", `#${items.length - index - 1}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
|
|
579
657
|
return this.focus === "list" && index === this.rowIndex
|
|
580
658
|
? this.theme.fg("accent", row)
|
|
581
659
|
: row;
|
|
@@ -734,11 +812,24 @@ export class ActorInspectorOverlay {
|
|
|
734
812
|
return [
|
|
735
813
|
`${indent}[`,
|
|
736
814
|
...value.flatMap((item, index) => {
|
|
737
|
-
const
|
|
815
|
+
const itemDepth = depth + 1;
|
|
816
|
+
const itemIndent = " ".repeat(itemDepth);
|
|
817
|
+
const marker = `#${index}`;
|
|
738
818
|
const itemInline = this.inlineDocumentValue(item);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
819
|
+
let lines: string[];
|
|
820
|
+
if (itemInline !== undefined) {
|
|
821
|
+
lines = [`${itemIndent}${marker}: ${itemInline}`];
|
|
822
|
+
} else if (item && typeof item === "object") {
|
|
823
|
+
const nested = this.document(item, itemDepth);
|
|
824
|
+
lines = [
|
|
825
|
+
`${itemIndent}${marker}: ${nested[0]!.slice(itemIndent.length)}`,
|
|
826
|
+
...nested.slice(1),
|
|
827
|
+
];
|
|
828
|
+
} else {
|
|
829
|
+
lines = this.labeledScalarLines(marker, item, itemDepth);
|
|
830
|
+
}
|
|
831
|
+
if (index < value.length - 1) lines[lines.length - 1] += ",";
|
|
832
|
+
return lines;
|
|
742
833
|
}),
|
|
743
834
|
`${indent}]`,
|
|
744
835
|
];
|
|
@@ -792,9 +883,13 @@ export class ActorInspectorOverlay {
|
|
|
792
883
|
private styleDocumentLine(line: string): string {
|
|
793
884
|
const indent = line.match(/^ */u)?.[0] ?? "";
|
|
794
885
|
const content = line.slice(indent.length);
|
|
795
|
-
if (
|
|
886
|
+
if (content.endsWith(":")) {
|
|
796
887
|
return `${indent}${this.theme.fg("accent", content)}`;
|
|
797
888
|
}
|
|
889
|
+
const indexed = /^(#\d+:)(.*)$/u.exec(content);
|
|
890
|
+
if (indexed) {
|
|
891
|
+
return `${indent}${this.theme.fg("accent", indexed[1])}${this.theme.fg("text", indexed[2])}`;
|
|
892
|
+
}
|
|
798
893
|
const labeled = /^([^:]+:)(.*)$/u.exec(content);
|
|
799
894
|
if (labeled) {
|
|
800
895
|
return `${indent}${this.theme.fg("muted", labeled[1])}${this.theme.fg("text", labeled[2])}`;
|
|
@@ -868,13 +963,46 @@ export class ActorInspectorOverlay {
|
|
|
868
963
|
return `${this.theme.fg("borderAccent", `╰${prefix}`)}${content}${this.theme.fg("borderAccent", `${suffix}${fill}╯`)}`;
|
|
869
964
|
}
|
|
870
965
|
|
|
966
|
+
private compositeMenuLine(
|
|
967
|
+
baseLine: string,
|
|
968
|
+
menuLine: string,
|
|
969
|
+
innerWidth: number,
|
|
970
|
+
horizontalOffset: number,
|
|
971
|
+
): string {
|
|
972
|
+
const offset = Math.max(0, Math.min(horizontalOffset, innerWidth));
|
|
973
|
+
const menuWidth = Math.min(innerWidth - offset, visibleWidth(menuLine));
|
|
974
|
+
const baseInner = sliceByColumn(baseLine, 1, innerWidth, true);
|
|
975
|
+
const prefix = sliceByColumn(baseInner, 0, offset, true);
|
|
976
|
+
const suffixStart = offset + menuWidth;
|
|
977
|
+
const suffix = sliceByColumn(
|
|
978
|
+
baseInner,
|
|
979
|
+
suffixStart,
|
|
980
|
+
Math.max(0, innerWidth - suffixStart),
|
|
981
|
+
true,
|
|
982
|
+
);
|
|
983
|
+
const remainder = this.fit(suffix, innerWidth - suffixStart);
|
|
984
|
+
return `${this.theme.fg("borderAccent", "│")}${this.fit(prefix, offset)}${truncateToWidth(menuLine, menuWidth, "")}${remainder}${this.theme.fg("borderAccent", "│")}`;
|
|
985
|
+
}
|
|
986
|
+
|
|
871
987
|
private stripeBackground(content: string, index: number): string {
|
|
872
988
|
return index % 2 === 0 ? content : this.theme.bg("customMessageBg", content);
|
|
873
989
|
}
|
|
874
990
|
|
|
875
|
-
private stripedRow(
|
|
876
|
-
|
|
877
|
-
|
|
991
|
+
private stripedRow(
|
|
992
|
+
content: string,
|
|
993
|
+
width: number,
|
|
994
|
+
index: number,
|
|
995
|
+
selected: boolean,
|
|
996
|
+
): string {
|
|
997
|
+
if (!selected) {
|
|
998
|
+
const fitted = this.fit(content, width);
|
|
999
|
+
return `${this.theme.fg("borderAccent", "│")}${this.stripeBackground(fitted, index)}${this.theme.fg("borderAccent", "│")}`;
|
|
1000
|
+
}
|
|
1001
|
+
const selectedContent = truncateToWidth(`${content} `, width, "");
|
|
1002
|
+
const remainder = " ".repeat(
|
|
1003
|
+
Math.max(0, width - visibleWidth(selectedContent)),
|
|
1004
|
+
);
|
|
1005
|
+
return `${this.theme.fg("borderAccent", "│")}${this.theme.bg("selectedBg", selectedContent)}${this.stripeBackground(remainder, index)}${this.theme.fg("borderAccent", "│")}`;
|
|
878
1006
|
}
|
|
879
1007
|
|
|
880
1008
|
private row(content: string, width: number): string {
|
package/lib/limits.ts
CHANGED
|
@@ -12,6 +12,7 @@ export const CONTROL_INPUT_MAX_BYTES = 380;
|
|
|
12
12
|
export const CONTROL_WIRE_MAX_BYTES = 512;
|
|
13
13
|
export const INSPECTOR_BODY_PREVIEW_CHARS = 320;
|
|
14
14
|
export const DOCTOR_ACTION_PREVIEW_CHARS = 72;
|
|
15
|
+
export const SESSION_EVIDENCE_MAX_BYTES = 4 * 1024 * 1024;
|
|
15
16
|
export const SESSION_EVIDENCE_MAX_TURNS = 100;
|
|
16
17
|
export const SESSION_EVIDENCE_TEXT_CHARS = 4_000;
|
|
17
18
|
export const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
|
package/lib/runs-artifacts.ts
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
closeSync,
|
|
9
|
+
fstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readSync,
|
|
12
|
+
} from "node:fs";
|
|
8
13
|
|
|
9
14
|
import { substituteCommandTemplateToken } from "./command-templates.ts";
|
|
10
15
|
|
|
@@ -22,6 +27,31 @@ export interface RunArtifactManifestEntry {
|
|
|
22
27
|
size?: number;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
function hashArtifactFile(path: string): { sha256: string; size: number } {
|
|
31
|
+
const fd = openSync(path, "r");
|
|
32
|
+
try {
|
|
33
|
+
const size = fstatSync(fd).size;
|
|
34
|
+
const hash = createHash("sha256");
|
|
35
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
36
|
+
let position = 0;
|
|
37
|
+
while (position < size) {
|
|
38
|
+
const bytesRead = readSync(
|
|
39
|
+
fd,
|
|
40
|
+
chunk,
|
|
41
|
+
0,
|
|
42
|
+
Math.min(chunk.byteLength, size - position),
|
|
43
|
+
position,
|
|
44
|
+
);
|
|
45
|
+
if (bytesRead === 0) break;
|
|
46
|
+
hash.update(chunk.subarray(0, bytesRead));
|
|
47
|
+
position += bytesRead;
|
|
48
|
+
}
|
|
49
|
+
return { sha256: hash.digest("hex"), size: position };
|
|
50
|
+
} finally {
|
|
51
|
+
closeSync(fd);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
export function resolveArtifactPaths(
|
|
26
56
|
artifacts: Record<string, RunArtifactDeclaration> | undefined,
|
|
27
57
|
values: Record<string, unknown>,
|
|
@@ -64,7 +94,7 @@ export function resolveArtifactManifest(
|
|
|
64
94
|
typeof artifact === "string" ? { path: artifact } : artifact;
|
|
65
95
|
if (!declaration?.path) continue;
|
|
66
96
|
try {
|
|
67
|
-
const
|
|
97
|
+
const hashed = hashArtifactFile(declaration.path);
|
|
68
98
|
manifest[name] = {
|
|
69
99
|
exists: true,
|
|
70
100
|
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
@@ -75,8 +105,8 @@ export function resolveArtifactManifest(
|
|
|
75
105
|
...(declaration.required !== undefined
|
|
76
106
|
? { required: declaration.required }
|
|
77
107
|
: {}),
|
|
78
|
-
sha256:
|
|
79
|
-
size:
|
|
108
|
+
sha256: hashed.sha256,
|
|
109
|
+
size: hashed.size,
|
|
80
110
|
};
|
|
81
111
|
} catch {
|
|
82
112
|
manifest[name] = {
|
package/lib/session-evidence.ts
CHANGED
|
@@ -55,6 +55,7 @@ export interface SessionEvidence {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export interface SessionEvidenceReadOptions {
|
|
58
|
+
maxBytes?: number;
|
|
58
59
|
maxTextChars?: number;
|
|
59
60
|
maxToolCalls?: number;
|
|
60
61
|
maxTurns?: number;
|
|
@@ -195,6 +196,10 @@ export function readSessionEvidence(
|
|
|
195
196
|
path: string,
|
|
196
197
|
options: SessionEvidenceReadOptions = {},
|
|
197
198
|
): SessionEvidence {
|
|
199
|
+
const maxBytes = Math.max(
|
|
200
|
+
1,
|
|
201
|
+
options.maxBytes ?? Limits.SESSION_EVIDENCE_MAX_BYTES,
|
|
202
|
+
);
|
|
198
203
|
const maxTextChars = Math.max(
|
|
199
204
|
1,
|
|
200
205
|
options.maxTextChars ?? Limits.SESSION_EVIDENCE_TEXT_CHARS,
|
|
@@ -207,7 +212,7 @@ export function readSessionEvidence(
|
|
|
207
212
|
1,
|
|
208
213
|
options.maxTurns ?? Limits.SESSION_EVIDENCE_MAX_TURNS,
|
|
209
214
|
);
|
|
210
|
-
const read = readJsonlFileResilient<SessionEntry>(path);
|
|
215
|
+
const read = readJsonlFileResilient<SessionEntry>(path, { maxBytes });
|
|
211
216
|
const diagnostics = [...read.diagnostics];
|
|
212
217
|
const header = read.records.find((entry) => entry.type === "session");
|
|
213
218
|
const branch = activeBranch(read.records, path, diagnostics);
|
|
@@ -296,7 +301,7 @@ export function readSessionEvidence(
|
|
|
296
301
|
path,
|
|
297
302
|
...(header ? { session: asRecord(header) } : {}),
|
|
298
303
|
totalTurns: turns.length,
|
|
299
|
-
truncated: turns.length > visibleTurns.length,
|
|
304
|
+
truncated: read.truncated === true || turns.length > visibleTurns.length,
|
|
300
305
|
turns: visibleTurns,
|
|
301
306
|
};
|
|
302
307
|
}
|
package/lib/state-readers.ts
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* Owns best-effort JSON and JSONL parsing helpers for operator-facing state reads.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
closeSync,
|
|
9
|
+
fstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readSync,
|
|
13
|
+
} from "node:fs";
|
|
8
14
|
|
|
9
15
|
export interface StateReadDiagnostic {
|
|
10
16
|
line?: number;
|
|
@@ -20,6 +26,11 @@ export interface JsonReadResult<T> {
|
|
|
20
26
|
export interface JsonlReadResult<T> {
|
|
21
27
|
diagnostics: StateReadDiagnostic[];
|
|
22
28
|
records: T[];
|
|
29
|
+
truncated?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface JsonlReadOptions {
|
|
33
|
+
maxBytes?: number;
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
function isEnoent(error: unknown): boolean {
|
|
@@ -30,6 +41,31 @@ function diagnosticMessage(error: unknown): string {
|
|
|
30
41
|
return error instanceof Error ? error.message : String(error);
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
function readTextFileCapped(path: string, maxBytes: number | undefined): string {
|
|
45
|
+
if (maxBytes === undefined) return readFileSync(path, "utf8");
|
|
46
|
+
const limit = Math.max(0, Math.floor(maxBytes));
|
|
47
|
+
const fd = openSync(path, "r");
|
|
48
|
+
try {
|
|
49
|
+
const size = fstatSync(fd).size;
|
|
50
|
+
if (size > limit) {
|
|
51
|
+
throw Object.assign(
|
|
52
|
+
new Error(`file exceeds bounded read limit (${size} > ${limit} bytes)`),
|
|
53
|
+
{ code: "EFBIG" },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const content = Buffer.allocUnsafe(size);
|
|
57
|
+
let offset = 0;
|
|
58
|
+
while (offset < size) {
|
|
59
|
+
const bytesRead = readSync(fd, content, offset, size - offset, offset);
|
|
60
|
+
if (bytesRead === 0) break;
|
|
61
|
+
offset += bytesRead;
|
|
62
|
+
}
|
|
63
|
+
return content.subarray(0, offset).toString("utf8");
|
|
64
|
+
} finally {
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
33
69
|
export function readJsonFileResilient<T>(path: string, fallback: T): JsonReadResult<T> {
|
|
34
70
|
try {
|
|
35
71
|
return {
|
|
@@ -50,11 +86,14 @@ export function readJsonFileResilient<T>(path: string, fallback: T): JsonReadRes
|
|
|
50
86
|
}
|
|
51
87
|
}
|
|
52
88
|
|
|
53
|
-
export function readJsonlFileResilient<T>(
|
|
89
|
+
export function readJsonlFileResilient<T>(
|
|
90
|
+
path: string,
|
|
91
|
+
options: JsonlReadOptions = {},
|
|
92
|
+
): JsonlReadResult<T> {
|
|
54
93
|
try {
|
|
55
94
|
const records: T[] = [];
|
|
56
95
|
const diagnostics: StateReadDiagnostic[] = [];
|
|
57
|
-
const lines =
|
|
96
|
+
const lines = readTextFileCapped(path, options.maxBytes).split("\n");
|
|
58
97
|
for (const [index, line] of lines.entries()) {
|
|
59
98
|
if (!line.trim()) continue;
|
|
60
99
|
try {
|
|
@@ -78,6 +117,9 @@ export function readJsonlFileResilient<T>(path: string): JsonlReadResult<T> {
|
|
|
78
117
|
},
|
|
79
118
|
],
|
|
80
119
|
records: [],
|
|
120
|
+
...((error as NodeJS.ErrnoException).code === "EFBIG"
|
|
121
|
+
? { truncated: true }
|
|
122
|
+
: {}),
|
|
81
123
|
};
|
|
82
124
|
}
|
|
83
125
|
}
|
package/lib/tools-access.ts
CHANGED
|
@@ -17,8 +17,11 @@ export function getContextSessionId(ctx: unknown): string | undefined {
|
|
|
17
17
|
export function requireContextSessionId(ctx: unknown, actor: string): string {
|
|
18
18
|
const sessionId = getContextSessionId(ctx);
|
|
19
19
|
if (!sessionId) {
|
|
20
|
-
throw
|
|
21
|
-
|
|
20
|
+
throw Object.assign(
|
|
21
|
+
new Error(
|
|
22
|
+
`${actor} reason=session_unavailable requires a current coordinator session; retry from an active coordinator session.`,
|
|
23
|
+
),
|
|
24
|
+
{ reason: "session_unavailable" },
|
|
22
25
|
);
|
|
23
26
|
}
|
|
24
27
|
return sessionId;
|
|
@@ -33,16 +36,13 @@ export function sessionMismatchError(input: {
|
|
|
33
36
|
const ownerSession = input.expectedSession ?? "none";
|
|
34
37
|
const currentSession = input.currentSession ?? "none";
|
|
35
38
|
const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
|
|
36
|
-
const hintTarget = input.expectedSession
|
|
37
|
-
? `session:${input.expectedSession}`
|
|
38
|
-
: "session:all";
|
|
39
39
|
return Object.assign(
|
|
40
40
|
new Error(
|
|
41
|
-
`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=
|
|
41
|
+
`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_runtime_runs`,
|
|
42
42
|
),
|
|
43
43
|
{
|
|
44
44
|
current_session: input.currentSession,
|
|
45
|
-
hint:
|
|
45
|
+
hint: "inspect target=runtime view=runs",
|
|
46
46
|
owner_session: input.expectedSession,
|
|
47
47
|
reason: "session_mismatch",
|
|
48
48
|
run: input.run,
|
|
@@ -51,18 +51,32 @@ export function sessionMismatchError(input: {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export function
|
|
54
|
+
export function assertRunStatusAccessibleToContext(
|
|
55
55
|
runId: string,
|
|
56
|
+
status: Record<string, unknown>,
|
|
56
57
|
ctx: unknown,
|
|
57
58
|
): Record<string, unknown> {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
59
|
+
const sessionId = requireContextSessionId(ctx, `run:${runId}`);
|
|
60
|
+
const ownerId = typeof status.ownerId === "string" && status.ownerId
|
|
61
|
+
? status.ownerId
|
|
62
|
+
: undefined;
|
|
63
|
+
if (ownerId !== sessionId) {
|
|
61
64
|
throw sessionMismatchError({
|
|
62
65
|
currentSession: sessionId,
|
|
63
|
-
expectedSession:
|
|
66
|
+
expectedSession: ownerId,
|
|
64
67
|
run: runId,
|
|
65
68
|
});
|
|
66
69
|
}
|
|
67
70
|
return status;
|
|
68
71
|
}
|
|
72
|
+
|
|
73
|
+
export function assertRunAccessibleToContext(
|
|
74
|
+
runId: string,
|
|
75
|
+
ctx: unknown,
|
|
76
|
+
): Record<string, unknown> {
|
|
77
|
+
return assertRunStatusAccessibleToContext(
|
|
78
|
+
runId,
|
|
79
|
+
AsyncRuns.getRunStatus(runId),
|
|
80
|
+
ctx,
|
|
81
|
+
);
|
|
82
|
+
}
|
package/lib/tools-inspect.ts
CHANGED
|
@@ -42,7 +42,7 @@ export interface InspectToolDeps<TContext = unknown> {
|
|
|
42
42
|
|
|
43
43
|
function runtimeStatus(): Record<string, unknown> {
|
|
44
44
|
return {
|
|
45
|
-
automatic_review:
|
|
45
|
+
automatic_review: Paths.isAutomaticRecipeReviewEnabled(),
|
|
46
46
|
run_root: Paths.getRunStateRoot(),
|
|
47
47
|
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
48
48
|
version: RuntimeIdentity.getPackageVersion(),
|
|
@@ -54,7 +54,7 @@ function runtimeRuns(
|
|
|
54
54
|
deps: InspectToolDeps,
|
|
55
55
|
status: string | undefined,
|
|
56
56
|
): Record<string, unknown> {
|
|
57
|
-
const session = ToolsAccess.
|
|
57
|
+
const session = ToolsAccess.requireContextSessionId(ctx, "runtime Run inventory");
|
|
58
58
|
const listed = deps.listRuns
|
|
59
59
|
? deps.listRuns()
|
|
60
60
|
: AsyncRuns.listRuns(undefined, status);
|
|
@@ -68,8 +68,8 @@ function runtimeRuns(
|
|
|
68
68
|
return run;
|
|
69
69
|
}
|
|
70
70
|
})
|
|
71
|
-
.filter((run) =>
|
|
72
|
-
return {
|
|
71
|
+
.filter((run) => run.ownerId === session);
|
|
72
|
+
return { owner_session: session, runs };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function runtimeTriage(
|
|
@@ -463,7 +463,11 @@ function inspectRun(
|
|
|
463
463
|
throw new Error("inspect run:<id> supports view=recipe, view=trace, or view=control.");
|
|
464
464
|
}
|
|
465
465
|
const status = deps.getRunStatus
|
|
466
|
-
?
|
|
466
|
+
? ToolsAccess.assertRunStatusAccessibleToContext(
|
|
467
|
+
run,
|
|
468
|
+
deps.getRunStatus(run),
|
|
469
|
+
ctx,
|
|
470
|
+
)
|
|
467
471
|
: ToolsAccess.assertRunAccessibleToContext(run, ctx);
|
|
468
472
|
const stateDir = String(status.state_dir);
|
|
469
473
|
if (view === "recipe") {
|
package/lib/tools-message.ts
CHANGED
|
@@ -46,6 +46,7 @@ function runNextActions(run: string, result: Record<string, unknown>): string[]
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export interface ControlToolDeps {
|
|
49
|
+
getRunStatus?: (run: string) => Record<string, unknown>;
|
|
49
50
|
handleRuntimeControl?: (
|
|
50
51
|
action: string,
|
|
51
52
|
input: unknown,
|
|
@@ -94,7 +95,13 @@ export function createControlToolDefinition<TContext = unknown>(
|
|
|
94
95
|
result = deps.handleRuntimeControl(request.action, request.input);
|
|
95
96
|
} else {
|
|
96
97
|
const run = request.target.slice(4);
|
|
97
|
-
const status =
|
|
98
|
+
const status = deps.getRunStatus
|
|
99
|
+
? ToolsAccess.assertRunStatusAccessibleToContext(
|
|
100
|
+
run,
|
|
101
|
+
deps.getRunStatus(run),
|
|
102
|
+
ctx,
|
|
103
|
+
)
|
|
104
|
+
: ToolsAccess.assertRunAccessibleToContext(run, ctx);
|
|
98
105
|
const runInstanceId =
|
|
99
106
|
typeof status.run_instance_id === "string"
|
|
100
107
|
? status.run_instance_id
|