@llblab/pi-actors 0.41.1 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/AGENTS.md +11 -6
  2. package/BACKLOG.md +1 -65
  3. package/CHANGELOG.md +25 -0
  4. package/README.md +24 -4
  5. package/dist/index.js +25 -121
  6. package/dist/lib/async-runs.d.ts +37 -5
  7. package/dist/lib/async-runs.js +181 -51
  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/command-templates.js +1 -1
  11. package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  12. package/dist/lib/draft-consolidation-transaction.js +610 -0
  13. package/dist/lib/draft-consolidation.d.ts +35 -0
  14. package/dist/lib/draft-consolidation.js +126 -0
  15. package/dist/lib/draft-review.d.ts +56 -0
  16. package/dist/lib/draft-review.js +254 -0
  17. package/dist/lib/draft-sleep.d.ts +65 -0
  18. package/dist/lib/draft-sleep.js +468 -0
  19. package/dist/lib/file-state.d.ts +8 -1
  20. package/dist/lib/file-state.js +115 -18
  21. package/dist/lib/inspector-actions.d.ts +16 -0
  22. package/dist/lib/inspector-actions.js +57 -0
  23. package/dist/lib/inspector-command.d.ts +7 -0
  24. package/dist/lib/inspector-command.js +37 -0
  25. package/dist/lib/inspector-overlay.d.ts +23 -3
  26. package/dist/lib/inspector-overlay.js +327 -70
  27. package/dist/lib/inspector.d.ts +9 -0
  28. package/dist/lib/inspector.js +49 -1
  29. package/dist/lib/observability.d.ts +13 -0
  30. package/dist/lib/observability.js +160 -6
  31. package/dist/lib/paths.d.ts +7 -0
  32. package/dist/lib/paths.js +28 -0
  33. package/dist/lib/recipes-discovery.js +32 -24
  34. package/dist/lib/recipes-usage.d.ts +18 -6
  35. package/dist/lib/recipes-usage.js +445 -34
  36. package/dist/lib/review-control.d.ts +14 -0
  37. package/dist/lib/review-control.js +111 -0
  38. package/dist/lib/review-diagnostics.d.ts +11 -0
  39. package/dist/lib/review-diagnostics.js +148 -0
  40. package/dist/lib/review-projection.d.ts +14 -0
  41. package/dist/lib/review-projection.js +170 -0
  42. package/dist/lib/run-ui-runtime.d.ts +18 -0
  43. package/dist/lib/run-ui-runtime.js +123 -0
  44. package/dist/lib/runs-artifacts.d.ts +1 -1
  45. package/dist/lib/runs-artifacts.js +1 -1
  46. package/dist/lib/runs-control.d.ts +10 -2
  47. package/dist/lib/runs-control.js +37 -7
  48. package/dist/lib/runs-identity.d.ts +1 -1
  49. package/dist/lib/runs-identity.js +1 -1
  50. package/dist/lib/runs-index.d.ts +11 -2
  51. package/dist/lib/runs-index.js +46 -23
  52. package/dist/lib/runs-mailbox.d.ts +1 -1
  53. package/dist/lib/runs-mailbox.js +1 -1
  54. package/dist/lib/runs-messages.d.ts +1 -1
  55. package/dist/lib/runs-messages.js +1 -1
  56. package/dist/lib/runs-outbox.d.ts +1 -1
  57. package/dist/lib/runs-outbox.js +1 -1
  58. package/dist/lib/runs-ownership.d.ts +1 -1
  59. package/dist/lib/runs-ownership.js +18 -4
  60. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  61. package/dist/lib/runs-parent-teardown.js +172 -0
  62. package/dist/lib/runs-process.d.ts +1 -1
  63. package/dist/lib/runs-process.js +5 -4
  64. package/dist/lib/runs-retention.d.ts +1 -1
  65. package/dist/lib/runs-retention.js +1 -1
  66. package/dist/lib/runs-start.d.ts +5 -3
  67. package/dist/lib/runs-start.js +7 -48
  68. package/dist/lib/runs-status.d.ts +5 -3
  69. package/dist/lib/runs-status.js +7 -6
  70. package/dist/lib/runtime.d.ts +7 -1
  71. package/dist/lib/runtime.js +32 -18
  72. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  73. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  74. package/dist/lib/tool-review-lineage.d.ts +24 -0
  75. package/dist/lib/tool-review-lineage.js +98 -0
  76. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  77. package/dist/lib/tool-review-scheduler.js +494 -0
  78. package/dist/lib/tool-review-transaction.d.ts +50 -0
  79. package/dist/lib/tool-review-transaction.js +362 -0
  80. package/dist/lib/tool-review.d.ts +56 -0
  81. package/dist/lib/tool-review.js +197 -0
  82. package/dist/lib/tools-inspect.js +28 -4
  83. package/dist/lib/tools-local.js +21 -4
  84. package/dist/lib/tools-message.d.ts +1 -0
  85. package/dist/lib/tools-message.js +29 -17
  86. package/dist/lib/tools-spawn.js +14 -2
  87. package/dist/lib/tools.d.ts +1 -0
  88. package/dist/lib/tools.js +1 -0
  89. package/dist/recipes/draft-review.json +24 -0
  90. package/dist/recipes/tool-review.json +24 -0
  91. package/dist/scripts/async-runner.mjs +9 -9
  92. package/dist/scripts/build-dist.mjs +6 -1
  93. package/dist/scripts/conformance.mjs +6 -1
  94. package/dist/scripts/recipe-utils.mjs +3 -3
  95. package/dist/scripts/release-gates.mjs +165 -0
  96. package/dist/skills/actors/SKILL.md +9 -8
  97. package/dist/skills/swarm/SKILL.md +1 -1
  98. package/docs/actor-inspector.md +20 -11
  99. package/docs/async-runs.md +16 -2
  100. package/docs/recipe-library.md +7 -3
  101. package/docs/template-recipes.md +4 -11
  102. package/docs/tool-registry.md +13 -4
  103. package/index.ts +27 -142
  104. package/lib/async-runs.ts +283 -67
  105. package/lib/automatic-review-runtime.ts +135 -0
  106. package/lib/command-templates.ts +1 -1
  107. package/lib/draft-consolidation-transaction.ts +821 -0
  108. package/lib/draft-consolidation.ts +181 -0
  109. package/lib/draft-review.ts +325 -0
  110. package/lib/draft-sleep.ts +576 -0
  111. package/lib/file-state.ts +143 -19
  112. package/lib/inspector-actions.ts +79 -0
  113. package/lib/inspector-command.ts +54 -0
  114. package/lib/inspector-overlay.ts +377 -91
  115. package/lib/inspector.ts +73 -1
  116. package/lib/observability.ts +194 -5
  117. package/lib/paths.ts +43 -0
  118. package/lib/recipes-discovery.ts +34 -26
  119. package/lib/recipes-usage.ts +569 -40
  120. package/lib/review-control.ts +137 -0
  121. package/lib/review-diagnostics.ts +164 -0
  122. package/lib/review-projection.ts +200 -0
  123. package/lib/run-ui-runtime.ts +153 -0
  124. package/lib/runs-artifacts.ts +1 -1
  125. package/lib/runs-control.ts +69 -6
  126. package/lib/runs-identity.ts +1 -1
  127. package/lib/runs-index.ts +57 -21
  128. package/lib/runs-mailbox.ts +1 -1
  129. package/lib/runs-messages.ts +1 -1
  130. package/lib/runs-outbox.ts +1 -1
  131. package/lib/runs-ownership.ts +23 -4
  132. package/lib/runs-parent-teardown.ts +257 -0
  133. package/lib/runs-process.ts +5 -4
  134. package/lib/runs-retention.ts +1 -1
  135. package/lib/runs-start.ts +13 -68
  136. package/lib/runs-status.ts +17 -8
  137. package/lib/runtime.ts +34 -17
  138. package/lib/tool-review-lineage-transaction.ts +881 -0
  139. package/lib/tool-review-lineage.ts +145 -0
  140. package/lib/tool-review-scheduler.ts +635 -0
  141. package/lib/tool-review-transaction.ts +563 -0
  142. package/lib/tool-review.ts +270 -0
  143. package/lib/tools-inspect.ts +35 -4
  144. package/lib/tools-local.ts +29 -4
  145. package/lib/tools-message.ts +45 -30
  146. package/lib/tools-spawn.ts +22 -2
  147. package/lib/tools.ts +5 -0
  148. package/package.json +5 -3
  149. package/recipes/draft-review.json +24 -0
  150. package/recipes/tool-review.json +24 -0
  151. package/scripts/async-runner.mjs +9 -9
  152. package/scripts/build-dist.mjs +6 -1
  153. package/scripts/conformance.mjs +6 -1
  154. package/scripts/recipe-utils.mjs +3 -3
  155. package/scripts/release-gates.mjs +165 -0
  156. package/skills/actors/SKILL.md +9 -8
  157. package/skills/swarm/SKILL.md +1 -1
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Automatic recipe-review operator control.
3
+ * Owns: explicit retry/reset transitions over durable draft/tool admission state.
4
+ */
5
+
6
+ import { existsSync, readFileSync, rmSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+
9
+ import { withFileMutationLock, writeJsonAtomic } from "./file-state.ts";
10
+ import * as Paths from "./paths.ts";
11
+
12
+ export type AutomaticReviewScope = "draft" | "tool";
13
+ export type AutomaticReviewControlAction = "review.reset" | "review.retry";
14
+
15
+ export interface AutomaticReviewControlOptions {
16
+ draftStatePath?: string;
17
+ scheduleDraft?(): void;
18
+ scheduleTool?(): void;
19
+ toolStatePath?: string;
20
+ }
21
+
22
+ function readState(path: string): Record<string, unknown> | undefined {
23
+ try {
24
+ const value = JSON.parse(readFileSync(path, "utf8"));
25
+ return value && typeof value === "object" && !Array.isArray(value)
26
+ ? value as Record<string, unknown>
27
+ : undefined;
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ function statePath(
34
+ scope: AutomaticReviewScope,
35
+ options: AutomaticReviewControlOptions,
36
+ ): string {
37
+ return scope === "draft"
38
+ ? options.draftStatePath ?? Paths.getDraftSleepStatePath()
39
+ : options.toolStatePath ?? Paths.getToolReviewStatePath();
40
+ }
41
+
42
+ function schedule(
43
+ scope: AutomaticReviewScope,
44
+ options: AutomaticReviewControlOptions,
45
+ ): void {
46
+ if (scope === "draft") options.scheduleDraft?.();
47
+ else options.scheduleTool?.();
48
+ }
49
+
50
+ export function controlAutomaticReview(
51
+ action: AutomaticReviewControlAction,
52
+ scope: AutomaticReviewScope,
53
+ options: AutomaticReviewControlOptions = {},
54
+ ): Record<string, unknown> {
55
+ const path = statePath(scope, options);
56
+ const result = withFileMutationLock(path, () => {
57
+ const state = readState(path);
58
+ if (!state) {
59
+ return { action, changed: false, phase: "idle", reason: "review_state_unavailable", scope };
60
+ }
61
+ const phase = String(state.phase ?? "unknown");
62
+ if (action === "review.reset") {
63
+ if (!["completed", "failed", "processing_failed"].includes(phase)) {
64
+ return { action, changed: false, phase, reason: "review_state_not_terminal", scope };
65
+ }
66
+ if (
67
+ scope === "tool" &&
68
+ (state.approvedPath || state.transactionEvidencePath || state.lineageJournalPath)
69
+ ) {
70
+ return {
71
+ action,
72
+ changed: false,
73
+ phase,
74
+ reason: "review_recovery_evidence_requires_retry",
75
+ scope,
76
+ };
77
+ }
78
+ rmSync(path, { force: true });
79
+ return { action, changed: true, phase: "idle", previous_phase: phase, scope };
80
+ }
81
+ if (!["failed", "processing_failed"].includes(phase)) {
82
+ return { action, changed: false, phase, reason: "review_state_not_retryable", scope };
83
+ }
84
+ const resumeDraftTransaction =
85
+ scope === "draft" &&
86
+ phase === "processing_failed" &&
87
+ typeof state.inputPath === "string" &&
88
+ typeof state.runId === "string" &&
89
+ existsSync(join(dirname(state.inputPath), "journal.json"));
90
+ const nextPhase = resumeDraftTransaction
91
+ ? "launched"
92
+ : scope === "tool" && state.approvedPath
93
+ ? state.transactionEvidencePath
94
+ ? "lineage_pending"
95
+ : "approved"
96
+ : "captured";
97
+ const next = {
98
+ ...state,
99
+ attempts: 0,
100
+ lastError: undefined,
101
+ nextAction: undefined,
102
+ failedStage: undefined,
103
+ phase: nextPhase,
104
+ processingAttempts: 0,
105
+ runId: resumeDraftTransaction ? state.runId : undefined,
106
+ updatedAt: new Date().toISOString(),
107
+ };
108
+ writeJsonAtomic(path, next);
109
+ return {
110
+ action,
111
+ changed: true,
112
+ phase: nextPhase,
113
+ previous_phase: phase,
114
+ scope,
115
+ };
116
+ });
117
+ if (result.changed === true) schedule(scope, options);
118
+ const nextActions = ["inspect target=recipes view=reviews"];
119
+ if (
120
+ action === "review.retry" &&
121
+ scope === "tool" &&
122
+ (result.phase === "approved" || result.phase === "lineage_pending")
123
+ ) nextActions.push("/reload");
124
+ return {
125
+ ...result,
126
+ next_actions: nextActions,
127
+ sent: result.changed === true,
128
+ };
129
+ }
130
+
131
+ export function parseAutomaticReviewScope(body: unknown): AutomaticReviewScope {
132
+ const record = body && typeof body === "object" && !Array.isArray(body)
133
+ ? body as Record<string, unknown>
134
+ : {};
135
+ if (record.scope === "draft" || record.scope === "tool") return record.scope;
136
+ throw new Error("review control requires body.scope=draft or body.scope=tool.");
137
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Automatic recipe-review diagnostics.
3
+ * Zones: bounded draft/tool cycle evidence, lineage summaries, revision snapshot inventory
4
+ * Owns read-only diagnostic projection; schedulers and transactions own mutation.
5
+ */
6
+
7
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
8
+ import { basename, dirname, join } from "node:path";
9
+
10
+ import * as Paths from "./paths.ts";
11
+
12
+ const MAX_FILE_BYTES = 1024 * 1024;
13
+ const MAX_ENTRIES = 100;
14
+
15
+ function readJson(path: string | undefined): Record<string, unknown> | undefined {
16
+ if (!path || !existsSync(path) || statSync(path).size > MAX_FILE_BYTES) return undefined;
17
+ try {
18
+ const value = JSON.parse(readFileSync(path, "utf8"));
19
+ return value && typeof value === "object" && !Array.isArray(value)
20
+ ? value as Record<string, unknown>
21
+ : undefined;
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ }
26
+
27
+ function text(value: unknown, limit = 240): string | undefined {
28
+ if (typeof value !== "string") return undefined;
29
+ return value.length > limit ? `${value.slice(0, limit - 1)}…` : value;
30
+ }
31
+
32
+ function strings(value: unknown, limit = 20): string[] {
33
+ return Array.isArray(value)
34
+ ? value.filter((entry): entry is string => typeof entry === "string").slice(-limit)
35
+ : [];
36
+ }
37
+
38
+ function cycleState(state: Record<string, unknown> | undefined): Record<string, unknown> {
39
+ if (!state) return { phase: "idle" };
40
+ return {
41
+ phase: text(state.phase) ?? "unknown",
42
+ attempts: state.attempts,
43
+ failed_stage: text(state.failedStage),
44
+ last_error: text(state.lastError, 500),
45
+ next_action: text(state.nextAction, 500),
46
+ processing_attempts: state.processingAttempts,
47
+ batch_id: text(state.batchId),
48
+ review_id: text(state.reviewId),
49
+ run_id: text(state.runId),
50
+ sources: Array.isArray(state.sourcePaths) ? state.sourcePaths.length : undefined,
51
+ tools: Array.isArray(state.toolNames) ? state.toolNames.length : undefined,
52
+ updated_at: text(state.updatedAt),
53
+ };
54
+ }
55
+
56
+ function draftEvidence(state: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
57
+ const evidence = readJson(text(state?.evidencePath, 4096));
58
+ if (!evidence) return undefined;
59
+ const decisions = Array.isArray(evidence.decisions)
60
+ ? evidence.decisions as Array<Record<string, unknown>>
61
+ : [];
62
+ const garbage = evidence.garbageCollection as Record<string, unknown> | undefined;
63
+ return {
64
+ actions: Object.fromEntries(
65
+ ["promote", "discard"].map((action) => [
66
+ action,
67
+ decisions.filter((decision) => decision.action === action).length,
68
+ ]),
69
+ ),
70
+ garbage_collection: garbage
71
+ ? { reviewed_sources: garbage.reviewedSources, status: garbage.status }
72
+ : undefined,
73
+ reviewed_sources: decisions.length,
74
+ };
75
+ }
76
+
77
+ function toolEvidence(state: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
78
+ const approved = readJson(text(state?.approvedPath, 4096));
79
+ if (!approved) return undefined;
80
+ const decisions = Array.isArray(approved.decisions)
81
+ ? approved.decisions as Array<Record<string, unknown>>
82
+ : [];
83
+ return {
84
+ actions: Object.fromEntries(
85
+ ["keep", "evolve", "replace", "demote", "merge", "split"].map((action) => [
86
+ action,
87
+ decisions.filter((decision) => decision.action === action).length,
88
+ ]),
89
+ ),
90
+ lineage_phase: readJson(text(state?.lineageJournalPath, 4096))?.phase,
91
+ review_id: text(approved.reviewId),
92
+ sources: Array.isArray(approved.sources) ? approved.sources.length : 0,
93
+ targets: Array.isArray(approved.targets) ? approved.targets.length : 0,
94
+ transaction_phase: readJson(
95
+ join(dirname(text(state?.approvedPath, 4096)!), "journal.json"),
96
+ )?.phase,
97
+ };
98
+ }
99
+
100
+ function lineageSummaries(recipeRoot: string): Record<string, unknown>[] {
101
+ const root = join(recipeRoot, ".usage", "recipes");
102
+ if (!existsSync(root)) return [];
103
+ return readdirSync(root, { withFileTypes: true })
104
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
105
+ .map((entry) => join(root, entry.name))
106
+ .sort()
107
+ .slice(0, MAX_ENTRIES)
108
+ .flatMap((path) => {
109
+ const record = readJson(path);
110
+ if (!record) return [];
111
+ return [{
112
+ current_path: text(record.current_path),
113
+ demoted_at: text(record.demoted_at),
114
+ demoted_from_revision: record.demoted_from_revision,
115
+ lifetime_calls: record.lifetime_calls,
116
+ name: text(record.lineage_name) ?? basename(path, ".json"),
117
+ review_epochs: strings(record.review_epochs, 5),
118
+ revision: record.revision,
119
+ revision_calls: record.revision_calls,
120
+ rollback_of_revision: record.rollback_of_revision,
121
+ }];
122
+ });
123
+ }
124
+
125
+ function snapshotCount(recipeRoot: string): number {
126
+ const root = join(recipeRoot, ".usage", "revisions");
127
+ if (!existsSync(root)) return 0;
128
+ let count = 0;
129
+ for (const lineage of readdirSync(root, { withFileTypes: true }).slice(0, MAX_ENTRIES)) {
130
+ if (!lineage.isDirectory()) continue;
131
+ count += readdirSync(join(root, lineage.name), { withFileTypes: true })
132
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
133
+ .length;
134
+ }
135
+ return count;
136
+ }
137
+
138
+ export interface AutomaticReviewDiagnosticOptions {
139
+ draftStatePath?: string;
140
+ recipeRoot?: string;
141
+ toolStatePath?: string;
142
+ }
143
+
144
+ export function readAutomaticReviewDiagnostics(
145
+ options: AutomaticReviewDiagnosticOptions = {},
146
+ ): Record<string, unknown> {
147
+ const recipeRoot = options.recipeRoot ?? Paths.getRecipeRoot();
148
+ const draftState = readJson(options.draftStatePath ?? Paths.getDraftSleepStatePath());
149
+ const toolState = readJson(options.toolStatePath ?? Paths.getToolReviewStatePath());
150
+ const lineages = lineageSummaries(recipeRoot);
151
+ return {
152
+ draft_review: {
153
+ ...cycleState(draftState),
154
+ evidence: draftEvidence(draftState),
155
+ },
156
+ lineage_count: lineages.length,
157
+ lineages,
158
+ revision_snapshots: snapshotCount(recipeRoot),
159
+ tool_review: {
160
+ ...cycleState(toolState),
161
+ evidence: toolEvidence(toolState),
162
+ },
163
+ };
164
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Automatic recipe-review confidentiality projection.
3
+ * Owns: value-free model inputs derived from trusted immutable draft/tool captures.
4
+ */
5
+
6
+ import type {
7
+ DraftReviewInput,
8
+ DraftReviewResult,
9
+ } from "./draft-review.ts";
10
+ import type {
11
+ ToolReviewInput,
12
+ ToolReviewResult,
13
+ } from "./tool-review.ts";
14
+
15
+ function occurrenceId(prefix: string, index: number): string {
16
+ return `${prefix}_${String(index + 1).padStart(2, "0")}`;
17
+ }
18
+
19
+ export const draftReviewIdentity = (index: number): string => occurrenceId("draft", index);
20
+ export const toolReviewIdentity = (index: number): string => occurrenceId("tool", index);
21
+
22
+ function contentGroups(hashes: string[]): string[] {
23
+ const groups = new Map<string, string>();
24
+ return hashes.map((hash) => {
25
+ let group = groups.get(hash);
26
+ if (!group) {
27
+ group = occurrenceId("content_group", groups.size);
28
+ groups.set(hash, group);
29
+ }
30
+ return group;
31
+ });
32
+ }
33
+
34
+ function draftContentGroups(input: DraftReviewInput): string[] {
35
+ return contentGroups([
36
+ ...input.activeTools.map((tool) => tool.sha256),
37
+ ...input.drafts.map((draft) => draft.sha256),
38
+ ]);
39
+ }
40
+
41
+ export function draftReviewRevision(input: DraftReviewInput, index: number): string {
42
+ return draftContentGroups(input)[input.activeTools.length + index]!;
43
+ }
44
+
45
+ export function toolReviewRevision(input: ToolReviewInput, index: number): string {
46
+ return contentGroups(input.tools.map((tool) => tool.sha256))[index]!;
47
+ }
48
+
49
+ function countArray(value: unknown): number {
50
+ return Array.isArray(value) ? value.length : 0;
51
+ }
52
+
53
+ function countRecord(value: unknown): number {
54
+ return value && typeof value === "object" && !Array.isArray(value)
55
+ ? Object.keys(value).length
56
+ : 0;
57
+ }
58
+
59
+ function templateShape(value: unknown): Record<string, unknown> {
60
+ if (typeof value === "string") return { kind: "command", nodes: 1 };
61
+ if (Array.isArray(value)) {
62
+ return {
63
+ branches: value.length,
64
+ kind: "sequence",
65
+ nodes: 1 + value.reduce((total, child) => total + Number(templateShape(child).nodes ?? 0), 0),
66
+ };
67
+ }
68
+ if (!value || typeof value !== "object") return { kind: "invalid", nodes: 0 };
69
+ const record = value as Record<string, unknown>;
70
+ const children = Array.isArray(record.template)
71
+ ? record.template
72
+ : record.template === undefined
73
+ ? []
74
+ : [record.template];
75
+ return {
76
+ branches: children.length,
77
+ conditional: record.when !== undefined,
78
+ kind: record.parallel === true ? "parallel" : "object",
79
+ repeat: record.repeat !== undefined,
80
+ retry: record.retry !== undefined,
81
+ nodes: 1 + children.reduce(
82
+ (total, child) => total + Number(templateShape(child).nodes ?? 0),
83
+ 0,
84
+ ),
85
+ };
86
+ }
87
+
88
+ function contractShape(recipe: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
89
+ if (!recipe) return undefined;
90
+ const mailbox = recipe.mailbox && typeof recipe.mailbox === "object" && !Array.isArray(recipe.mailbox)
91
+ ? recipe.mailbox as Record<string, unknown>
92
+ : {};
93
+ return {
94
+ argument_count: countArray(recipe.args),
95
+ artifact_count: countRecord(recipe.artifacts),
96
+ async: recipe.async === true,
97
+ default_count: countRecord(recipe.defaults),
98
+ import_count: countRecord(recipe.imports),
99
+ mailbox_accepts_count: countArray(mailbox.accepts),
100
+ mailbox_emits_count: countArray(mailbox.emits),
101
+ template_shape: templateShape(recipe.template),
102
+ };
103
+ }
104
+
105
+ function usageShape(usage: Record<string, unknown> | undefined): Record<string, number> {
106
+ const numeric = (key: string): number => {
107
+ const value = usage?.[key];
108
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
109
+ ? value
110
+ : 0;
111
+ };
112
+ return {
113
+ lifetime_calls: numeric("lifetime_calls") || numeric("calls"),
114
+ revision: numeric("revision"),
115
+ revision_calls: numeric("revision_calls"),
116
+ };
117
+ }
118
+
119
+ export function projectDraftReviewInput(input: DraftReviewInput): Record<string, unknown> {
120
+ return {
121
+ activeTools: input.activeTools.map((_tool, index) => ({
122
+ name: occurrenceId("active_tool", index),
123
+ sha256: draftContentGroups(input)[index],
124
+ })),
125
+ batchId: input.batchId,
126
+ createdAt: input.createdAt,
127
+ drafts: input.drafts.map((draft, index) => ({
128
+ contract: contractShape(draft.recipe),
129
+ draft: draftReviewIdentity(index),
130
+ riskLabels: [...draft.riskLabels],
131
+ sha256: draftReviewRevision(input, index),
132
+ usage: usageShape(draft.usage),
133
+ valid: draft.valid,
134
+ })),
135
+ };
136
+ }
137
+
138
+ export function restoreDraftReviewResult(
139
+ input: DraftReviewInput,
140
+ result: DraftReviewResult,
141
+ ): DraftReviewResult {
142
+ const drafts = new Map(input.drafts.map((draft, index) => [draftReviewIdentity(index), {
143
+ draft,
144
+ revision: draftReviewRevision(input, index),
145
+ }]));
146
+ return {
147
+ ...result,
148
+ decisions: result.decisions.map((decision) => {
149
+ const captured = drafts.get(decision.draft);
150
+ return {
151
+ ...decision,
152
+ draft: captured?.draft.path ?? `unknown:${decision.draft}`,
153
+ sha256: captured && decision.sha256 === captured.revision
154
+ ? captured.draft.sha256
155
+ : `invalid:${decision.sha256}`,
156
+ };
157
+ }),
158
+ };
159
+ }
160
+
161
+ export function projectToolReviewInput(input: ToolReviewInput): Record<string, unknown> {
162
+ return {
163
+ createdAt: input.createdAt,
164
+ reviewId: input.reviewId,
165
+ tools: input.tools.map((tool, index) => ({
166
+ contract: contractShape(tool.recipe),
167
+ name: toolReviewIdentity(index),
168
+ riskLabels: [...tool.riskLabels],
169
+ sha256: toolReviewRevision(input, index),
170
+ usage: usageShape(tool.usage),
171
+ valid: tool.valid,
172
+ })),
173
+ };
174
+ }
175
+
176
+ export function restoreToolReviewResult(
177
+ input: ToolReviewInput,
178
+ result: ToolReviewResult,
179
+ ): ToolReviewResult {
180
+ const tools = new Map(input.tools.map((tool, index) => [toolReviewIdentity(index), {
181
+ revision: toolReviewRevision(input, index),
182
+ tool,
183
+ }]));
184
+ return {
185
+ ...result,
186
+ decisions: result.decisions.map((decision) => {
187
+ const captured = tools.get(decision.source);
188
+ return {
189
+ ...decision,
190
+ sha256: captured && decision.sha256 === captured.revision
191
+ ? captured.tool.sha256
192
+ : `invalid:${decision.sha256}`,
193
+ source: captured?.tool.name ?? `unknown:${decision.source}`,
194
+ ...(decision.target
195
+ ? { target: tools.get(decision.target)?.tool.name ?? decision.target }
196
+ : {}),
197
+ };
198
+ }),
199
+ };
200
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Ambient run observability runtime.
3
+ * Zones: run watcher lifecycle, terminal reconciliation, status animation, shutdown teardown
4
+ * Owns event-driven run UI coordination without owning actor execution semantics.
5
+ */
6
+
7
+ import * as AsyncRuns from "./async-runs.ts";
8
+ import * as Observability from "./observability.ts";
9
+ import * as Paths from "./paths.ts";
10
+ import * as Pi from "./pi.ts";
11
+
12
+ export interface RunUiRuntime {
13
+ close(): void;
14
+ shutdown(eventReason: string, ctx: Pi.ExtensionContext): void;
15
+ start(ctx: Pi.ExtensionContext): void;
16
+ }
17
+
18
+ export interface RunUiRuntimeDeps {
19
+ getActiveContext(): Pi.ExtensionContext | undefined;
20
+ getRunOwnerId(ctx: Pi.ExtensionContext): string;
21
+ onRunEvent(): void;
22
+ pi: Pi.ExtensionAPI;
23
+ }
24
+
25
+ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
26
+ let animationInterval: NodeJS.Timeout | undefined;
27
+ let notifyTimeout: NodeJS.Timeout | undefined;
28
+ let lastWatcherDiagnosticId = 0;
29
+ const observation = Observability.createRunUiObservationState();
30
+ const retirementAttempts = new Set<string>();
31
+ const terminalNotificationsInFlight = new Set<string>();
32
+
33
+ const retireCandidateRuns = (
34
+ ctx: Pi.ExtensionContext,
35
+ summary: Observability.RunSummary,
36
+ ): void => {
37
+ void Observability.executeRunRetirements(summary, {
38
+ attempted: retirementAttempts,
39
+ cancelRun: (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
40
+ notify: (message, level) => ctx.ui.notify(message, level),
41
+ sendStop: (candidate) => AsyncRuns.sendRunMessage(candidate.stateDir, "stop"),
42
+ });
43
+ };
44
+ const update = (
45
+ ctx: Pi.ExtensionContext,
46
+ notify = false,
47
+ terminalOnly = false,
48
+ ): void => {
49
+ const ownerId = deps.getRunOwnerId(ctx);
50
+ const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
51
+ ctx.ui.setStatus(
52
+ "zz-pi-actors-runs",
53
+ snapshot.status ? ctx.ui.theme.fg("dim", snapshot.status) : undefined,
54
+ );
55
+ if (!notify) return;
56
+ const sink = Pi.createNotificationSink(deps.pi, ctx);
57
+ retireCandidateRuns(ctx, snapshot.summary);
58
+ Observability.deliverRunTransitionNotifications(
59
+ snapshot.transitions,
60
+ sink,
61
+ terminalNotificationsInFlight,
62
+ );
63
+ Observability.pruneRunUiObservationState(observation, snapshot);
64
+ if (!terminalOnly) {
65
+ Observability.deliverRunOutboxNotifications(snapshot.outboxEvents, sink);
66
+ }
67
+ };
68
+ const reportDiagnostics = (ctx: Pi.ExtensionContext): void => {
69
+ for (const diagnostic of watcher.getDiagnostics()) {
70
+ if (diagnostic.id <= lastWatcherDiagnosticId) continue;
71
+ lastWatcherDiagnosticId = diagnostic.id;
72
+ ctx.ui.notify(
73
+ diagnostic.message,
74
+ diagnostic.code === "rearmed" ? "info" : "warning",
75
+ );
76
+ }
77
+ };
78
+ const scheduleUpdate = (): void => {
79
+ if (notifyTimeout) clearTimeout(notifyTimeout);
80
+ notifyTimeout = setTimeout(() => {
81
+ const ctx = deps.getActiveContext();
82
+ if (!ctx) return;
83
+ watcher.refresh();
84
+ update(ctx, true);
85
+ deps.onRunEvent();
86
+ reportDiagnostics(ctx);
87
+ }, 50);
88
+ notifyTimeout.unref?.();
89
+ };
90
+ const watcher = Observability.createRunStateWatcher({
91
+ stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
92
+ onChange: scheduleUpdate,
93
+ });
94
+ const reconciliation = Observability.createRunTerminalReconciliationLoop({
95
+ onError: (error) => {
96
+ const ctx = deps.getActiveContext();
97
+ if (!ctx) return;
98
+ const message = error instanceof Error ? error.message : String(error);
99
+ ctx.ui.notify(`Actor terminal reconciliation failed: ${message}`, "error");
100
+ },
101
+ reconcile: () => {
102
+ const ctx = deps.getActiveContext();
103
+ if (!ctx) return;
104
+ Observability.reconcileRunTerminalNotifications({
105
+ inFlight: terminalNotificationsInFlight,
106
+ ownerId: deps.getRunOwnerId(ctx),
107
+ sink: Pi.createNotificationSink(deps.pi, ctx),
108
+ state: observation,
109
+ });
110
+ reportDiagnostics(ctx);
111
+ },
112
+ refreshWatcher: () => watcher.refresh(),
113
+ });
114
+ const close = (): void => {
115
+ watcher.close();
116
+ reconciliation.close();
117
+ if (notifyTimeout) clearTimeout(notifyTimeout);
118
+ notifyTimeout = undefined;
119
+ if (animationInterval) clearInterval(animationInterval);
120
+ animationInterval = undefined;
121
+ };
122
+
123
+ return {
124
+ close,
125
+ shutdown(eventReason, ctx) {
126
+ close();
127
+ const teardown = AsyncRuns.teardownRunsOwnedByParent(
128
+ deps.getRunOwnerId(ctx),
129
+ Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
130
+ { trigger: `session_shutdown:${eventReason}` },
131
+ );
132
+ if (teardown.failed === 0) return;
133
+ try {
134
+ ctx.ui.notify(
135
+ `Actor shutdown teardown: killed=${teardown.killed} failed=${teardown.failed} skipped=${teardown.skipped} discovery_failed=${teardown.discoveryFailed}. Summary: ${teardown.summaryPath ?? "unavailable"}.`,
136
+ "warning",
137
+ );
138
+ } catch {
139
+ /* stale shutdown context */
140
+ }
141
+ },
142
+ start(ctx) {
143
+ close();
144
+ update(ctx, true, true);
145
+ watcher.refresh();
146
+ reconciliation.start();
147
+ animationInterval = setInterval(() => {
148
+ if (deps.getActiveContext() === ctx) update(ctx);
149
+ }, 1000);
150
+ animationInterval.unref?.();
151
+ },
152
+ };
153
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Async run artifact declarations and manifest resolution.
3
- * Owns artifact path template expansion and filesystem-backed artifact metadata.
3
+ * Owns: artifact path template expansion and filesystem-backed artifact metadata.
4
4
  */
5
5
 
6
6
  import { createHash } from "node:crypto";