@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.
- package/AGENTS.md +11 -6
- package/BACKLOG.md +1 -65
- package/CHANGELOG.md +25 -0
- package/README.md +24 -4
- package/dist/index.js +25 -121
- package/dist/lib/async-runs.d.ts +37 -5
- package/dist/lib/async-runs.js +181 -51
- package/dist/lib/automatic-review-runtime.d.ts +18 -0
- package/dist/lib/automatic-review-runtime.js +96 -0
- package/dist/lib/command-templates.js +1 -1
- package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
- package/dist/lib/draft-consolidation-transaction.js +610 -0
- package/dist/lib/draft-consolidation.d.ts +35 -0
- package/dist/lib/draft-consolidation.js +126 -0
- package/dist/lib/draft-review.d.ts +56 -0
- package/dist/lib/draft-review.js +254 -0
- package/dist/lib/draft-sleep.d.ts +65 -0
- package/dist/lib/draft-sleep.js +468 -0
- package/dist/lib/file-state.d.ts +8 -1
- package/dist/lib/file-state.js +115 -18
- package/dist/lib/inspector-actions.d.ts +16 -0
- package/dist/lib/inspector-actions.js +57 -0
- package/dist/lib/inspector-command.d.ts +7 -0
- package/dist/lib/inspector-command.js +37 -0
- package/dist/lib/inspector-overlay.d.ts +23 -3
- package/dist/lib/inspector-overlay.js +327 -70
- package/dist/lib/inspector.d.ts +9 -0
- package/dist/lib/inspector.js +49 -1
- package/dist/lib/observability.d.ts +13 -0
- package/dist/lib/observability.js +160 -6
- package/dist/lib/paths.d.ts +7 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/recipes-discovery.js +32 -24
- package/dist/lib/recipes-usage.d.ts +18 -6
- package/dist/lib/recipes-usage.js +445 -34
- package/dist/lib/review-control.d.ts +14 -0
- package/dist/lib/review-control.js +111 -0
- package/dist/lib/review-diagnostics.d.ts +11 -0
- package/dist/lib/review-diagnostics.js +148 -0
- package/dist/lib/review-projection.d.ts +14 -0
- package/dist/lib/review-projection.js +170 -0
- package/dist/lib/run-ui-runtime.d.ts +18 -0
- package/dist/lib/run-ui-runtime.js +123 -0
- package/dist/lib/runs-artifacts.d.ts +1 -1
- package/dist/lib/runs-artifacts.js +1 -1
- package/dist/lib/runs-control.d.ts +10 -2
- package/dist/lib/runs-control.js +37 -7
- package/dist/lib/runs-identity.d.ts +1 -1
- package/dist/lib/runs-identity.js +1 -1
- package/dist/lib/runs-index.d.ts +11 -2
- package/dist/lib/runs-index.js +46 -23
- package/dist/lib/runs-mailbox.d.ts +1 -1
- package/dist/lib/runs-mailbox.js +1 -1
- package/dist/lib/runs-messages.d.ts +1 -1
- package/dist/lib/runs-messages.js +1 -1
- package/dist/lib/runs-outbox.d.ts +1 -1
- package/dist/lib/runs-outbox.js +1 -1
- package/dist/lib/runs-ownership.d.ts +1 -1
- package/dist/lib/runs-ownership.js +18 -4
- package/dist/lib/runs-parent-teardown.d.ts +51 -0
- package/dist/lib/runs-parent-teardown.js +172 -0
- package/dist/lib/runs-process.d.ts +1 -1
- package/dist/lib/runs-process.js +5 -4
- package/dist/lib/runs-retention.d.ts +1 -1
- package/dist/lib/runs-retention.js +1 -1
- package/dist/lib/runs-start.d.ts +5 -3
- package/dist/lib/runs-start.js +7 -48
- package/dist/lib/runs-status.d.ts +5 -3
- package/dist/lib/runs-status.js +7 -6
- package/dist/lib/runtime.d.ts +7 -1
- package/dist/lib/runtime.js +32 -18
- package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
- package/dist/lib/tool-review-lineage-transaction.js +597 -0
- package/dist/lib/tool-review-lineage.d.ts +24 -0
- package/dist/lib/tool-review-lineage.js +98 -0
- package/dist/lib/tool-review-scheduler.d.ts +80 -0
- package/dist/lib/tool-review-scheduler.js +494 -0
- package/dist/lib/tool-review-transaction.d.ts +50 -0
- package/dist/lib/tool-review-transaction.js +362 -0
- package/dist/lib/tool-review.d.ts +56 -0
- package/dist/lib/tool-review.js +197 -0
- package/dist/lib/tools-inspect.js +28 -4
- package/dist/lib/tools-local.js +21 -4
- package/dist/lib/tools-message.d.ts +1 -0
- package/dist/lib/tools-message.js +29 -17
- package/dist/lib/tools-spawn.js +14 -2
- package/dist/lib/tools.d.ts +1 -0
- package/dist/lib/tools.js +1 -0
- package/dist/recipes/draft-review.json +24 -0
- package/dist/recipes/tool-review.json +24 -0
- package/dist/scripts/async-runner.mjs +9 -9
- package/dist/scripts/build-dist.mjs +6 -1
- package/dist/scripts/conformance.mjs +6 -1
- package/dist/scripts/recipe-utils.mjs +3 -3
- package/dist/scripts/release-gates.mjs +165 -0
- package/dist/skills/actors/SKILL.md +9 -8
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +20 -11
- package/docs/async-runs.md +16 -2
- package/docs/recipe-library.md +7 -3
- package/docs/template-recipes.md +4 -11
- package/docs/tool-registry.md +13 -4
- package/index.ts +27 -142
- package/lib/async-runs.ts +283 -67
- package/lib/automatic-review-runtime.ts +135 -0
- package/lib/command-templates.ts +1 -1
- package/lib/draft-consolidation-transaction.ts +821 -0
- package/lib/draft-consolidation.ts +181 -0
- package/lib/draft-review.ts +325 -0
- package/lib/draft-sleep.ts +576 -0
- package/lib/file-state.ts +143 -19
- package/lib/inspector-actions.ts +79 -0
- package/lib/inspector-command.ts +54 -0
- package/lib/inspector-overlay.ts +377 -91
- package/lib/inspector.ts +73 -1
- package/lib/observability.ts +194 -5
- package/lib/paths.ts +43 -0
- package/lib/recipes-discovery.ts +34 -26
- package/lib/recipes-usage.ts +569 -40
- package/lib/review-control.ts +137 -0
- package/lib/review-diagnostics.ts +164 -0
- package/lib/review-projection.ts +200 -0
- package/lib/run-ui-runtime.ts +153 -0
- package/lib/runs-artifacts.ts +1 -1
- package/lib/runs-control.ts +69 -6
- package/lib/runs-identity.ts +1 -1
- package/lib/runs-index.ts +57 -21
- package/lib/runs-mailbox.ts +1 -1
- package/lib/runs-messages.ts +1 -1
- package/lib/runs-outbox.ts +1 -1
- package/lib/runs-ownership.ts +23 -4
- package/lib/runs-parent-teardown.ts +257 -0
- package/lib/runs-process.ts +5 -4
- package/lib/runs-retention.ts +1 -1
- package/lib/runs-start.ts +13 -68
- package/lib/runs-status.ts +17 -8
- package/lib/runtime.ts +34 -17
- package/lib/tool-review-lineage-transaction.ts +881 -0
- package/lib/tool-review-lineage.ts +145 -0
- package/lib/tool-review-scheduler.ts +635 -0
- package/lib/tool-review-transaction.ts +563 -0
- package/lib/tool-review.ts +270 -0
- package/lib/tools-inspect.ts +35 -4
- package/lib/tools-local.ts +29 -4
- package/lib/tools-message.ts +45 -30
- package/lib/tools-spawn.ts +22 -2
- package/lib/tools.ts +5 -0
- package/package.json +5 -3
- package/recipes/draft-review.json +24 -0
- package/recipes/tool-review.json +24 -0
- package/scripts/async-runner.mjs +9 -9
- package/scripts/build-dist.mjs +6 -1
- package/scripts/conformance.mjs +6 -1
- package/scripts/recipe-utils.mjs +3 -3
- package/scripts/release-gates.mjs +165 -0
- package/skills/actors/SKILL.md +9 -8
- package/skills/swarm/SKILL.md +1 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic active-tool portfolio review contract.
|
|
3
|
+
* Zones: thirty-six-tool reviewer input, evolution decisions, mechanical contract gates
|
|
4
|
+
* Owns strict read-only keep/evolve/replace/demote/merge/split review semantics.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const TOOL_REVIEW_THRESHOLD = 36;
|
|
8
|
+
|
|
9
|
+
export interface ToolReviewAssessment {
|
|
10
|
+
adaptability: number;
|
|
11
|
+
futureUsefulness: number;
|
|
12
|
+
lifetimeCalls: number;
|
|
13
|
+
redundancy: number;
|
|
14
|
+
revisionCalls: number;
|
|
15
|
+
safety: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ToolReviewTool {
|
|
19
|
+
name: string;
|
|
20
|
+
path: string;
|
|
21
|
+
recipe: Record<string, unknown>;
|
|
22
|
+
riskLabels: string[];
|
|
23
|
+
sha256: string;
|
|
24
|
+
usage?: Record<string, unknown>;
|
|
25
|
+
valid: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ToolReviewInput {
|
|
29
|
+
createdAt: string;
|
|
30
|
+
reviewId: string;
|
|
31
|
+
tools: ToolReviewTool[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ToolReviewOutputRecipe {
|
|
35
|
+
name: string;
|
|
36
|
+
recipe: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ToolReviewAction =
|
|
40
|
+
| "demote"
|
|
41
|
+
| "evolve"
|
|
42
|
+
| "keep"
|
|
43
|
+
| "merge"
|
|
44
|
+
| "replace"
|
|
45
|
+
| "split";
|
|
46
|
+
|
|
47
|
+
export interface ToolReviewDecision {
|
|
48
|
+
action: ToolReviewAction;
|
|
49
|
+
assessment: ToolReviewAssessment;
|
|
50
|
+
rationale: string;
|
|
51
|
+
recipe?: Record<string, unknown>;
|
|
52
|
+
sha256: string;
|
|
53
|
+
source: string;
|
|
54
|
+
target?: string;
|
|
55
|
+
outputs?: ToolReviewOutputRecipe[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ToolReviewResult {
|
|
59
|
+
createdAt: string;
|
|
60
|
+
decisions: ToolReviewDecision[];
|
|
61
|
+
reviewId: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ToolReviewValidation {
|
|
65
|
+
errors: string[];
|
|
66
|
+
ok: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const UUID_PATTERN = /^[a-f0-9-]{36}$/u;
|
|
70
|
+
const TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]*$/u;
|
|
71
|
+
|
|
72
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
73
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function canonicalJson(value: unknown): string {
|
|
77
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
78
|
+
if (!isRecord(value)) return JSON.stringify(value);
|
|
79
|
+
return `{${Object.keys(value)
|
|
80
|
+
.sort()
|
|
81
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
82
|
+
.join(",")}}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function finiteScore(value: unknown): boolean {
|
|
86
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function calls(value: unknown): number {
|
|
90
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0
|
|
91
|
+
? value
|
|
92
|
+
: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateAssessment(
|
|
96
|
+
assessment: unknown,
|
|
97
|
+
tool: ToolReviewTool,
|
|
98
|
+
errors: string[],
|
|
99
|
+
): void {
|
|
100
|
+
if (
|
|
101
|
+
!isRecord(assessment) ||
|
|
102
|
+
!finiteScore(assessment.adaptability) ||
|
|
103
|
+
!finiteScore(assessment.futureUsefulness) ||
|
|
104
|
+
!finiteScore(assessment.redundancy) ||
|
|
105
|
+
typeof assessment.safety !== "string" ||
|
|
106
|
+
!assessment.safety.trim() ||
|
|
107
|
+
!Number.isInteger(assessment.lifetimeCalls) ||
|
|
108
|
+
Number(assessment.lifetimeCalls) < 0 ||
|
|
109
|
+
!Number.isInteger(assessment.revisionCalls) ||
|
|
110
|
+
Number(assessment.revisionCalls) < 0
|
|
111
|
+
) {
|
|
112
|
+
errors.push(`invalid tool assessment: ${tool.name}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (assessment.lifetimeCalls !== calls(tool.usage?.lifetime_calls ?? tool.usage?.calls)) {
|
|
116
|
+
errors.push(`tool lifetime calls differ from lineage: ${tool.name}`);
|
|
117
|
+
}
|
|
118
|
+
if (assessment.revisionCalls !== calls(tool.usage?.revision_calls)) {
|
|
119
|
+
errors.push(`tool revision calls differ from lineage: ${tool.name}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function validateToolReviewInput(input: ToolReviewInput): ToolReviewValidation {
|
|
124
|
+
const errors: string[] = [];
|
|
125
|
+
if (!UUID_PATTERN.test(input.reviewId)) errors.push("tool reviewId must be a UUID");
|
|
126
|
+
if (!input.createdAt || Number.isNaN(Date.parse(input.createdAt))) {
|
|
127
|
+
errors.push("tool review createdAt must be an ISO timestamp");
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(input.tools) || input.tools.length !== TOOL_REVIEW_THRESHOLD) {
|
|
130
|
+
errors.push(`tool review input must contain exactly ${TOOL_REVIEW_THRESHOLD} tools`);
|
|
131
|
+
}
|
|
132
|
+
const names = new Set<string>();
|
|
133
|
+
for (const tool of input.tools ?? []) {
|
|
134
|
+
if (
|
|
135
|
+
!tool ||
|
|
136
|
+
!TOOL_NAME_PATTERN.test(tool.name) ||
|
|
137
|
+
typeof tool.path !== "string" ||
|
|
138
|
+
typeof tool.sha256 !== "string" ||
|
|
139
|
+
typeof tool.valid !== "boolean" ||
|
|
140
|
+
!isRecord(tool.recipe) ||
|
|
141
|
+
!Array.isArray(tool.riskLabels) ||
|
|
142
|
+
names.has(tool.name)
|
|
143
|
+
) {
|
|
144
|
+
errors.push(`invalid or duplicate reviewed tool: ${String(tool?.name)}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
names.add(tool.name);
|
|
148
|
+
}
|
|
149
|
+
return { errors, ok: errors.length === 0 };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function validateToolReviewResult(
|
|
153
|
+
input: ToolReviewInput,
|
|
154
|
+
result: ToolReviewResult,
|
|
155
|
+
): ToolReviewValidation {
|
|
156
|
+
const errors = [...validateToolReviewInput(input).errors];
|
|
157
|
+
if (result.reviewId !== input.reviewId) errors.push("tool review identity mismatch");
|
|
158
|
+
if (!result.createdAt || Number.isNaN(Date.parse(result.createdAt))) {
|
|
159
|
+
errors.push("tool review result createdAt must be an ISO timestamp");
|
|
160
|
+
}
|
|
161
|
+
if (!Array.isArray(result.decisions)) {
|
|
162
|
+
errors.push("tool review decisions must be an array");
|
|
163
|
+
return { errors, ok: false };
|
|
164
|
+
}
|
|
165
|
+
const inventory = new Map(input.tools.map((tool) => [tool.name, tool]));
|
|
166
|
+
const activeNames = new Set(inventory.keys());
|
|
167
|
+
const seen = new Set<string>();
|
|
168
|
+
const outputNames = new Set<string>();
|
|
169
|
+
const mergeGroups = new Map<string, Array<{ decision: ToolReviewDecision; recipe: string }>>();
|
|
170
|
+
for (const decision of result.decisions) {
|
|
171
|
+
const tool = inventory.get(decision.source);
|
|
172
|
+
if (!tool) {
|
|
173
|
+
errors.push(`unknown reviewed tool: ${decision.source}`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (seen.has(decision.source)) {
|
|
177
|
+
errors.push(`duplicate tool decision: ${decision.source}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
seen.add(decision.source);
|
|
181
|
+
if (decision.sha256 !== tool.sha256) errors.push(`reviewed tool changed: ${tool.name}`);
|
|
182
|
+
if (!decision.rationale?.trim()) errors.push(`missing tool rationale: ${tool.name}`);
|
|
183
|
+
validateAssessment(decision.assessment, tool, errors);
|
|
184
|
+
if (decision.action === "keep" || decision.action === "demote") {
|
|
185
|
+
if (decision.target || decision.recipe || decision.outputs) {
|
|
186
|
+
errors.push(`${decision.action} declares evolution fields: ${tool.name}`);
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (decision.action === "replace" || decision.action === "split") {
|
|
191
|
+
errors.push(`${decision.action} requires explicit operator-authored recipe mutation: ${tool.name}`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (decision.action === "evolve") {
|
|
195
|
+
if (!decision.target || !TOOL_NAME_PATTERN.test(decision.target)) {
|
|
196
|
+
errors.push(`evolve requires snake_case target: ${tool.name}`);
|
|
197
|
+
}
|
|
198
|
+
if (decision.recipe !== undefined || decision.outputs !== undefined) {
|
|
199
|
+
errors.push(`evolve must not supply executable recipe content: ${tool.name}`);
|
|
200
|
+
}
|
|
201
|
+
if (
|
|
202
|
+
decision.target &&
|
|
203
|
+
activeNames.has(decision.target) &&
|
|
204
|
+
decision.target !== tool.name
|
|
205
|
+
) {
|
|
206
|
+
errors.push(`evolve target collides with active tool: ${decision.target}`);
|
|
207
|
+
}
|
|
208
|
+
if (decision.target && outputNames.has(decision.target)) {
|
|
209
|
+
errors.push(`duplicate evolved target: ${decision.target}`);
|
|
210
|
+
} else if (decision.target) outputNames.add(decision.target);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (decision.action === "merge") {
|
|
214
|
+
if (!decision.target || !TOOL_NAME_PATTERN.test(decision.target)) {
|
|
215
|
+
errors.push(`merge requires snake_case target: ${tool.name}`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (decision.recipe !== undefined || decision.outputs !== undefined) {
|
|
219
|
+
errors.push(`merge must not supply executable recipe content: ${tool.name}`);
|
|
220
|
+
}
|
|
221
|
+
const group = mergeGroups.get(decision.target) ?? [];
|
|
222
|
+
group.push({ decision, recipe: canonicalJson(tool.recipe) });
|
|
223
|
+
mergeGroups.set(decision.target, group);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
errors.push(`invalid tool review action: ${tool.name}`);
|
|
227
|
+
}
|
|
228
|
+
for (const tool of input.tools) {
|
|
229
|
+
if (!seen.has(tool.name)) errors.push(`missing tool decision: ${tool.name}`);
|
|
230
|
+
}
|
|
231
|
+
for (const [target, group] of mergeGroups) {
|
|
232
|
+
if (group.length < 2) errors.push(`merge target requires at least two sources: ${target}`);
|
|
233
|
+
if (
|
|
234
|
+
activeNames.has(target) &&
|
|
235
|
+
!group.some((entry) => entry.decision.source === target)
|
|
236
|
+
) {
|
|
237
|
+
errors.push(`merge target collides with unrelated active tool: ${target}`);
|
|
238
|
+
}
|
|
239
|
+
if (new Set(group.map((entry) => entry.recipe)).size !== 1) {
|
|
240
|
+
errors.push(`merge target recipes differ: ${target}`);
|
|
241
|
+
}
|
|
242
|
+
if (outputNames.has(target)) errors.push(`duplicate evolved target: ${target}`);
|
|
243
|
+
outputNames.add(target);
|
|
244
|
+
}
|
|
245
|
+
return { errors, ok: errors.length === 0 };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function parseToolReviewResult(stdout: string): ToolReviewResult {
|
|
249
|
+
const marker = "TOOL_REVIEW_RESULT";
|
|
250
|
+
const markerIndex = stdout.lastIndexOf(marker);
|
|
251
|
+
if (markerIndex < 0) throw new Error("Tool reviewer output marker is missing.");
|
|
252
|
+
const payload = stdout.slice(markerIndex + marker.length).trim();
|
|
253
|
+
if (!payload.startsWith("{") || !payload.endsWith("}")) {
|
|
254
|
+
throw new Error("Tool reviewer output must end with one JSON object.");
|
|
255
|
+
}
|
|
256
|
+
const value = JSON.parse(payload) as unknown;
|
|
257
|
+
if (!isRecord(value)) throw new Error("Tool reviewer result must be an object.");
|
|
258
|
+
return value as unknown as ToolReviewResult;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function createToolReviewPrompt(inputPath: string): string {
|
|
262
|
+
return [
|
|
263
|
+
`Review the attached immutable active-tool portfolio from ${inputPath}.`,
|
|
264
|
+
"Evaluate all 36 tools independently from lifetime/revision usage, adaptability, redundancy, safety, contract quality, and likely future usefulness.",
|
|
265
|
+
"Choose exactly one action per source: keep, evolve, demote, or merge. There is no quota for any action.",
|
|
266
|
+
"evolve may only rename one unchanged captured recipe; demote moves the unchanged recipe to draft memory; merge may only deduplicate at least two canonically identical captured recipes. Never return recipe or outputs fields. replace and split require explicit operator-authored mutation and are unavailable to automatic review.",
|
|
267
|
+
"Do not mutate, register, move, or delete recipes. You are a read-only selector; the deterministic executor derives exact recipe bytes from captured sources and owns every portfolio mutation.",
|
|
268
|
+
"End stdout with TOOL_REVIEW_RESULT on its own line followed by exactly one JSON object with reviewId, createdAt, and decisions.",
|
|
269
|
+
].join("\n");
|
|
270
|
+
}
|
package/lib/tools-inspect.ts
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
10
11
|
|
|
11
12
|
import * as AsyncRuns from "./async-runs.ts";
|
|
12
13
|
import * as Limits from "./limits.ts";
|
|
13
14
|
import * as Messages from "./messages.ts";
|
|
14
15
|
import * as Paths from "./paths.ts";
|
|
15
16
|
import * as RecipesDiscovery from "./recipes-discovery.ts";
|
|
17
|
+
import * as ReviewDiagnostics from "./review-diagnostics.ts";
|
|
16
18
|
import * as Rooms from "./rooms.ts";
|
|
17
19
|
import * as Schema from "./schema.ts";
|
|
18
20
|
import * as ToolsAccess from "./tools-access.ts";
|
|
@@ -22,6 +24,17 @@ import * as ToolsResponse from "./tools-response.ts";
|
|
|
22
24
|
const asRecord = ToolsResponse.asRecord;
|
|
23
25
|
const maybeJsonText = ToolsResponse.maybeJsonText;
|
|
24
26
|
|
|
27
|
+
function compactAutomaticReviewDiagnostics(
|
|
28
|
+
diagnostics: Record<string, unknown>,
|
|
29
|
+
): string {
|
|
30
|
+
const draft = asRecord(diagnostics.draft_review);
|
|
31
|
+
const tool = asRecord(diagnostics.tool_review);
|
|
32
|
+
return [
|
|
33
|
+
`automatic_reviews draft=${String(draft.phase ?? "idle")} tool=${String(tool.phase ?? "idle")}`,
|
|
34
|
+
`lineages=${String(diagnostics.lineage_count ?? 0)} revision_snapshots=${String(diagnostics.revision_snapshots ?? 0)}`,
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
function compactRunMessages(messages: AsyncRuns.RunOutboxEvent[]): string {
|
|
26
39
|
if (messages.length === 0) return "\n(no actor messages)";
|
|
27
40
|
return `\n${messages
|
|
@@ -257,8 +270,9 @@ function getPiActorsRuntimeStatus(): Record<string, unknown> {
|
|
|
257
270
|
} catch {
|
|
258
271
|
git_commit = undefined;
|
|
259
272
|
}
|
|
260
|
-
const entrypoint =
|
|
273
|
+
const entrypoint = fileURLToPath(import.meta.url);
|
|
261
274
|
return {
|
|
275
|
+
automatic_recipe_review: Paths.isAutomaticRecipeReviewEnabled(),
|
|
262
276
|
entrypoint,
|
|
263
277
|
git_commit,
|
|
264
278
|
mode: entrypoint.includes("/dist/") ? "dist" : "source",
|
|
@@ -271,7 +285,7 @@ function getPiActorsRuntimeStatus(): Record<string, unknown> {
|
|
|
271
285
|
}
|
|
272
286
|
|
|
273
287
|
function compactPiActorsRuntimeStatus(status: Record<string, unknown>): string {
|
|
274
|
-
return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
|
|
288
|
+
return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} automatic_review=${String(status.automatic_recipe_review)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
|
|
275
289
|
}
|
|
276
290
|
|
|
277
291
|
function isStaleClaim(message: AsyncRuns.RunInboxMessage, now: number): boolean {
|
|
@@ -515,12 +529,29 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
515
529
|
view !== "status" &&
|
|
516
530
|
view !== "summary" &&
|
|
517
531
|
view !== "doctor" &&
|
|
518
|
-
view !== "imports"
|
|
532
|
+
view !== "imports" &&
|
|
533
|
+
view !== "reviews"
|
|
519
534
|
) {
|
|
520
535
|
throw new Error(
|
|
521
|
-
"inspect recipes supports view=status, view=summary, view=doctor, or view=
|
|
536
|
+
"inspect recipes supports view=status, view=summary, view=doctor, view=imports, or view=reviews.",
|
|
522
537
|
);
|
|
523
538
|
}
|
|
539
|
+
if (view === "reviews") {
|
|
540
|
+
const diagnostics = ReviewDiagnostics.readAutomaticReviewDiagnostics({
|
|
541
|
+
recipeRoot: deps.recipeRoot ?? Paths.getRecipeRoot(),
|
|
542
|
+
});
|
|
543
|
+
return {
|
|
544
|
+
content: [{
|
|
545
|
+
type: "text" as const,
|
|
546
|
+
text: maybeJsonText(
|
|
547
|
+
diagnostics,
|
|
548
|
+
input.verbose === true,
|
|
549
|
+
compactAutomaticReviewDiagnostics(diagnostics),
|
|
550
|
+
),
|
|
551
|
+
}],
|
|
552
|
+
details: diagnostics,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
524
555
|
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
525
556
|
{
|
|
526
557
|
root: deps.recipeRoot ?? Paths.getRecipeRoot(),
|
package/lib/tools-local.ts
CHANGED
|
@@ -143,6 +143,14 @@ export function createRuntimeToolDefinition(
|
|
|
143
143
|
paramSchema.run_id = Schema.stringSchema(
|
|
144
144
|
"Optional run id override for this async template recipe invocation.",
|
|
145
145
|
);
|
|
146
|
+
if (isAsyncRecipe) {
|
|
147
|
+
paramSchema.correlation_id = Schema.stringSchema(
|
|
148
|
+
"Optional workflow correlation id preserved in terminal follow-up delivery.",
|
|
149
|
+
);
|
|
150
|
+
paramSchema.transport_context = Schema.looseObjectSchema(
|
|
151
|
+
"Optional originating transport route preserved for detached terminal follow-up.",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
146
154
|
return {
|
|
147
155
|
name: cfg.name,
|
|
148
156
|
label: cfg.name,
|
|
@@ -155,18 +163,24 @@ export function createRuntimeToolDefinition(
|
|
|
155
163
|
)
|
|
156
164
|
: Prompts.formatRegisteredToolPromptSnippet(cfg.template),
|
|
157
165
|
async execute(
|
|
158
|
-
|
|
166
|
+
toolCallId: string,
|
|
159
167
|
params: unknown,
|
|
160
168
|
signal: AbortSignal | undefined,
|
|
161
169
|
_onUpdate: unknown,
|
|
162
170
|
ctx: RuntimeToolContext,
|
|
163
171
|
) {
|
|
164
172
|
try {
|
|
165
|
-
if (
|
|
166
|
-
|
|
173
|
+
if (
|
|
174
|
+
cfg.sourcePath &&
|
|
175
|
+
!RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool")
|
|
176
|
+
) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Recipe launch rejected because its source changed during activation: ${cfg.sourcePath}. Reload recipe tools and retry.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
167
181
|
if (isAsyncRecipe) {
|
|
168
182
|
const input = params as Record<string, unknown>;
|
|
169
|
-
const { run_id, ...values } = input;
|
|
183
|
+
const { correlation_id, run_id, transport_context, ...values } = input;
|
|
170
184
|
const base = cfg.recipe ? cfg.recipe : { file: String(cfg.template) };
|
|
171
185
|
const runId =
|
|
172
186
|
typeof run_id === "string" && run_id.trim()
|
|
@@ -176,6 +190,17 @@ export function createRuntimeToolDefinition(
|
|
|
176
190
|
{
|
|
177
191
|
...base,
|
|
178
192
|
launch_source: "tool",
|
|
193
|
+
launch_correlation: {
|
|
194
|
+
...(typeof correlation_id === "string"
|
|
195
|
+
? { correlation_id } : {}),
|
|
196
|
+
tool_call_id: toolCallId,
|
|
197
|
+
},
|
|
198
|
+
...(transport_context &&
|
|
199
|
+
typeof transport_context === "object" &&
|
|
200
|
+
!Array.isArray(transport_context)
|
|
201
|
+
? {
|
|
202
|
+
transport_context: transport_context as Record<string, unknown>,
|
|
203
|
+
} : {}),
|
|
179
204
|
ownerId: getRunOwnerId(ctx),
|
|
180
205
|
run_id: runId,
|
|
181
206
|
tool: cfg.name,
|
package/lib/tools-message.ts
CHANGED
|
@@ -290,6 +290,10 @@ function compactActorMessageResult(
|
|
|
290
290
|
|
|
291
291
|
export interface ActorMessageToolDeps<TContext = unknown> {
|
|
292
292
|
getTool?: (name: string) => any | undefined;
|
|
293
|
+
handleRuntimeMessage?: (
|
|
294
|
+
type: string,
|
|
295
|
+
body: unknown,
|
|
296
|
+
) => Record<string, unknown>;
|
|
293
297
|
}
|
|
294
298
|
|
|
295
299
|
export function createActorMessageToolDefinition<TContext = unknown>(
|
|
@@ -363,7 +367,14 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
363
367
|
]
|
|
364
368
|
: [];
|
|
365
369
|
if (message.type === "control.kill") {
|
|
366
|
-
result =
|
|
370
|
+
result = typeof status.run_instance_id === "string"
|
|
371
|
+
? AsyncRuns.killRun(address.value, {
|
|
372
|
+
...(typeof status.ownerId === "string"
|
|
373
|
+
? { ownerId: status.ownerId }
|
|
374
|
+
: {}),
|
|
375
|
+
runInstanceId: status.run_instance_id,
|
|
376
|
+
})
|
|
377
|
+
: { killed: false, reason: "run generation unavailable" };
|
|
367
378
|
} else if (message.type === "control.archive") {
|
|
368
379
|
result = AsyncRuns.archiveRun(address.value);
|
|
369
380
|
} else if (message.type === "control.prune") {
|
|
@@ -463,36 +474,40 @@ export function createActorMessageToolDefinition<TContext = unknown>(
|
|
|
463
474
|
: {}),
|
|
464
475
|
};
|
|
465
476
|
} else if (address.kind === "tool" && address.value) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
)
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
477
|
+
if (address.value === "pi-actors" && deps.handleRuntimeMessage) {
|
|
478
|
+
result = deps.handleRuntimeMessage(message.type, message.body);
|
|
479
|
+
} else {
|
|
480
|
+
const tool = deps.getTool?.(address.value);
|
|
481
|
+
if (!tool || typeof tool.execute !== "function") {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`tool actor not found or not executable: ${address.value}`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const toolParams = messageBodyToToolParams(message);
|
|
487
|
+
let toolResult: unknown;
|
|
488
|
+
try {
|
|
489
|
+
toolResult = await tool.execute(
|
|
490
|
+
`message:${message.type}`,
|
|
491
|
+
toolParams,
|
|
492
|
+
_signal,
|
|
493
|
+
_onUpdate,
|
|
494
|
+
ctx,
|
|
495
|
+
);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
throw formatToolActorFailure(
|
|
498
|
+
address.value,
|
|
499
|
+
message,
|
|
500
|
+
toolParams,
|
|
501
|
+
error,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
result = {
|
|
505
|
+
invoked: true,
|
|
506
|
+
sent: true,
|
|
507
|
+
tool: address.value,
|
|
508
|
+
tool_result: toolResult,
|
|
509
|
+
};
|
|
489
510
|
}
|
|
490
|
-
result = {
|
|
491
|
-
invoked: true,
|
|
492
|
-
sent: true,
|
|
493
|
-
tool: address.value,
|
|
494
|
-
tool_result: toolResult,
|
|
495
|
-
};
|
|
496
511
|
} else if (address.kind === "coordinator" || address.kind === "session") {
|
|
497
512
|
if (!message.from) {
|
|
498
513
|
throw new Error(`message to ${address.kind} requires from=run:<id>.`);
|
package/lib/tools-spawn.ts
CHANGED
|
@@ -8,10 +8,12 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
10
|
import * as AsyncRuns from "./async-runs.ts";
|
|
11
|
+
import { withFileMutationLock } from "./file-state.ts";
|
|
11
12
|
import * as Messages from "./messages.ts";
|
|
12
13
|
import * as ModelContext from "./model-context.ts";
|
|
13
14
|
import * as Paths from "./paths.ts";
|
|
14
15
|
import * as RecipesDiscovery from "./recipes-discovery.ts";
|
|
16
|
+
import * as RecipesUsage from "./recipes-usage.ts";
|
|
15
17
|
import * as Rooms from "./rooms.ts";
|
|
16
18
|
import * as Schema from "./schema.ts";
|
|
17
19
|
import * as ToolsResponse from "./tools-response.ts";
|
|
@@ -90,7 +92,12 @@ function writeSpawnDraftRecipe(
|
|
|
90
92
|
...(defaults ? { defaults } : {}),
|
|
91
93
|
template: input.template,
|
|
92
94
|
};
|
|
93
|
-
|
|
95
|
+
withFileMutationLock(root, () =>
|
|
96
|
+
withFileMutationLock(path, () =>
|
|
97
|
+
writeFileSync(path, `${JSON.stringify(recipe, null, 2)}\n`, { flag: "wx" }),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
RecipesUsage.recordRecipeLaunch(path, new Date(), "spawn", Paths.getRecipeRoot());
|
|
94
101
|
return path;
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -137,6 +144,9 @@ export function createSpawnToolDefinition<
|
|
|
137
144
|
as: Schema.stringSchema(
|
|
138
145
|
"Optional actor address for the spawned run, e.g. run:<id>.",
|
|
139
146
|
),
|
|
147
|
+
correlation_id: Schema.stringSchema(
|
|
148
|
+
"Optional workflow correlation id preserved in terminal follow-up delivery.",
|
|
149
|
+
),
|
|
140
150
|
file: Schema.stringSchema(
|
|
141
151
|
"Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
|
|
142
152
|
),
|
|
@@ -155,6 +165,9 @@ export function createSpawnToolDefinition<
|
|
|
155
165
|
values: Schema.looseObjectSchema(
|
|
156
166
|
"Runtime placeholder values passed to the actor.",
|
|
157
167
|
),
|
|
168
|
+
transport_context: Schema.looseObjectSchema(
|
|
169
|
+
"Optional originating transport route preserved for detached terminal follow-up, e.g. Telegram chat_id and thread_id.",
|
|
170
|
+
),
|
|
158
171
|
verbose: Schema.booleanSchema(
|
|
159
172
|
"Return full JSON instead of compact text.",
|
|
160
173
|
),
|
|
@@ -162,7 +175,7 @@ export function createSpawnToolDefinition<
|
|
|
162
175
|
[],
|
|
163
176
|
),
|
|
164
177
|
async execute(
|
|
165
|
-
|
|
178
|
+
toolCallId: string,
|
|
166
179
|
params: unknown,
|
|
167
180
|
_signal: AbortSignal | undefined,
|
|
168
181
|
_onUpdate: unknown,
|
|
@@ -189,7 +202,14 @@ export function createSpawnToolDefinition<
|
|
|
189
202
|
{
|
|
190
203
|
file: recipe,
|
|
191
204
|
launch_source: "spawn",
|
|
205
|
+
launch_correlation: {
|
|
206
|
+
...(typeof input.correlation_id === "string"
|
|
207
|
+
? { correlation_id: input.correlation_id } : {}),
|
|
208
|
+
tool_call_id: toolCallId,
|
|
209
|
+
},
|
|
192
210
|
ownerId: getRunOwnerId(ctx),
|
|
211
|
+
...(input.transport_context
|
|
212
|
+
? { transport_context: asRecord(input.transport_context) } : {}),
|
|
193
213
|
run_id: runId,
|
|
194
214
|
...(input.template !== undefined
|
|
195
215
|
? {
|
package/lib/tools.ts
CHANGED
|
@@ -25,6 +25,10 @@ export interface CoreActorToolDefinitionDeps<
|
|
|
25
25
|
configPath: string;
|
|
26
26
|
getActiveTools: () => string[];
|
|
27
27
|
getRuntimeTool: (name: string) => unknown;
|
|
28
|
+
handleRuntimeMessage?: (
|
|
29
|
+
type: string,
|
|
30
|
+
body: unknown,
|
|
31
|
+
) => Record<string, unknown>;
|
|
28
32
|
registryRuntime: Pick<
|
|
29
33
|
RegisterToolRuntimeDeps<TContext>,
|
|
30
34
|
"getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool"
|
|
@@ -71,6 +75,7 @@ export function createCoreActorToolDefinitions<
|
|
|
71
75
|
ToolsSpawn.createSpawnToolDefinition<TContext>(),
|
|
72
76
|
ToolsMessage.createActorMessageToolDefinition<TContext>({
|
|
73
77
|
getTool: (name) => deps.getRuntimeTool(name),
|
|
78
|
+
handleRuntimeMessage: deps.handleRuntimeMessage,
|
|
74
79
|
}),
|
|
75
80
|
ToolsInspect.createInspectToolDefinition<TContext>({
|
|
76
81
|
getTool: (name) => deps.getRuntimeTool(name),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-actors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local Actor Kernel for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"check": "node --experimental-strip-types -e \"await import('./index.ts'); console.log('pi-actors: extension import ok')\"",
|
|
29
29
|
"conformance": "node scripts/conformance.mjs",
|
|
30
|
-
"test": "node --experimental-strip-types --test tests/*.test.ts",
|
|
30
|
+
"test": "node --experimental-strip-types --test --test-concurrency=1 tests/*.test.ts",
|
|
31
31
|
"pack:dry": "npm pack --dry-run",
|
|
32
32
|
"recipes:qa": "node scripts/validate-recipe.mjs recipes --all --qa --summary",
|
|
33
|
-
"validate": "
|
|
33
|
+
"release:validate": "npm run validate && node scripts/release-gates.mjs",
|
|
34
|
+
"validate": "npx tsc --noEmit && npm run recipes:qa && npm run build && npm run check && npm test && npm run conformance && npm run pack:dry",
|
|
35
|
+
"audit:dependencies": "npm audit --audit-level=high --omit=peer",
|
|
34
36
|
"build": "node scripts/build-dist.mjs",
|
|
35
37
|
"prepack": "npm run build"
|
|
36
38
|
},
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"async": true,
|
|
3
|
+
"args": [
|
|
4
|
+
"input_path:path",
|
|
5
|
+
"model:string",
|
|
6
|
+
"thinking:string"
|
|
7
|
+
],
|
|
8
|
+
"defaults": {
|
|
9
|
+
"model": "{current_model}",
|
|
10
|
+
"thinking": "{current_thinking}"
|
|
11
|
+
},
|
|
12
|
+
"description": "Read-only background reviewer for one immutable automatic draft-memory batch.",
|
|
13
|
+
"mailbox": {
|
|
14
|
+
"accepts": [
|
|
15
|
+
"control.kill"
|
|
16
|
+
],
|
|
17
|
+
"emits": [
|
|
18
|
+
"command.done",
|
|
19
|
+
"run.done",
|
|
20
|
+
"run.failed"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"template": "pi -p --model {model} --thinking {thinking} --no-tools @{input_path} Review the attached immutable pi-actors draft batch. Evaluate every draft independently using launch history, universality, flexibility, parameterization, duplication, safety, and likely future usefulness. Treat draft and sha256 fields as batch-local opaque occurrence/content-group identifiers and return them verbatim. Choose exactly one promote or discard decision per draft; there is no selection quota. Promotion requires a unique absent snake_case target and targetSha256 null. Never return recipe content: the executor derives the exact immutable captured source. Invalid or secret-touching drafts must be discarded. Include assessment with launches, universality, flexibility, futureUsefulness, and safety. Do not mutate, move, register, or delete recipes. End stdout with DRAFT_REVIEW_RESULT on its own line followed by exactly one JSON object with batchId, createdAt, and decisions."
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"async": true,
|
|
3
|
+
"args": [
|
|
4
|
+
"input_path:path",
|
|
5
|
+
"model:string",
|
|
6
|
+
"thinking:string"
|
|
7
|
+
],
|
|
8
|
+
"defaults": {
|
|
9
|
+
"model": "{current_model}",
|
|
10
|
+
"thinking": "{current_thinking}"
|
|
11
|
+
},
|
|
12
|
+
"description": "Read-only background reviewer for one immutable 36-tool evolution portfolio.",
|
|
13
|
+
"mailbox": {
|
|
14
|
+
"accepts": [
|
|
15
|
+
"control.kill"
|
|
16
|
+
],
|
|
17
|
+
"emits": [
|
|
18
|
+
"command.done",
|
|
19
|
+
"run.done",
|
|
20
|
+
"run.failed"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"template": "pi -p --model {model} --thinking {thinking} --no-tools @{input_path} Review the attached immutable pi-actors active-tool portfolio. Evaluate all 36 tools independently using lifetime and revision usage, adaptability, redundancy, safety, contract quality, and likely future usefulness. Treat source/name and sha256 fields as batch-local opaque occurrence/content-group identifiers, return them verbatim, and infer exact duplication only from a shared content group. Choose exactly one keep, evolve, demote, or merge decision per tool; there is no action quota. evolve may only rename one unchanged captured recipe, demote moves the unchanged recipe to draft memory, and merge may only deduplicate at least two canonically identical captured recipes. Never return recipe or outputs fields; replace and split require explicit operator-authored mutation and are unavailable to automatic review. Do not mutate, register, move, or delete recipes. End stdout with TOOL_REVIEW_RESULT on its own line followed by exactly one JSON object with reviewId, createdAt, and decisions."
|
|
24
|
+
}
|