@llblab/pi-actors 0.43.0 → 0.43.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 +12 -6
- package/BACKLOG.md +189 -1
- package/CHANGELOG.md +180 -301
- package/README.md +9 -7
- package/dist/index.js +1 -1
- package/dist/lib/async-runs.d.ts +2 -1
- package/dist/lib/async-runs.js +16 -4
- package/dist/lib/automatic-review-runtime.d.ts +1 -1
- package/dist/lib/automatic-review-runtime.js +5 -5
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/command-templates.js +38 -4
- package/dist/lib/control-projection.d.ts +20 -0
- package/dist/lib/control-projection.js +66 -0
- package/dist/lib/control.d.ts +3 -0
- package/dist/lib/control.js +27 -14
- package/dist/lib/draft-sleep.js +3 -3
- package/dist/lib/file-state.d.ts +1 -0
- package/dist/lib/file-state.js +98 -42
- package/dist/lib/inspector-overlay.d.ts +2 -0
- package/dist/lib/inspector-overlay.js +92 -53
- package/dist/lib/limits.d.ts +5 -3
- package/dist/lib/limits.js +5 -3
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-control.js +6 -2
- package/dist/lib/review-control.d.ts +1 -1
- package/dist/lib/review-control.js +4 -5
- package/dist/lib/runs-control-delivery.d.ts +8 -1
- package/dist/lib/runs-control-delivery.js +38 -15
- package/dist/lib/runs-controls.d.ts +2 -0
- package/dist/lib/runs-controls.js +5 -3
- package/dist/lib/runs-trace.d.ts +2 -2
- package/dist/lib/runs-trace.js +24 -20
- package/dist/lib/runtime-identity.d.ts +7 -0
- package/dist/lib/runtime-identity.js +35 -0
- package/dist/lib/runtime-triage.d.ts +29 -0
- package/dist/lib/runtime-triage.js +76 -0
- package/dist/lib/tool-review-scheduler.js +7 -7
- package/dist/lib/tools-inspect.js +53 -14
- package/dist/lib/tools-message.d.ts +1 -2
- package/dist/lib/tools-message.js +6 -6
- package/dist/lib/tools-response.d.ts +0 -1
- package/dist/lib/tools-response.js +0 -9
- package/dist/lib/tools.d.ts +1 -1
- package/dist/lib/tools.js +1 -1
- package/dist/lib/trace-projection.js +30 -10
- package/dist/scripts/locker.mjs +9 -16
- package/dist/scripts/music-player.mjs +7 -13
- package/dist/scripts/release-gates.mjs +33 -2
- package/dist/scripts/validate-recipe.mjs +5 -4
- package/dist/skills/actors/SKILL.md +12 -7
- package/dist/skills/swarm/SKILL.md +0 -2
- package/docs/0.43-baseline.md +18 -23
- package/docs/README.md +2 -4
- package/docs/actor-inspector.md +5 -5
- package/docs/async-runs.md +2 -2
- package/docs/command-templates.md +6 -116
- package/docs/recipe-library.md +2 -4
- package/docs/releasing.md +28 -0
- package/docs/template-recipes.md +1 -1
- package/docs/tool-registry.md +2 -2
- package/index.ts +1 -1
- package/lib/async-runs.ts +18 -5
- package/lib/automatic-review-runtime.ts +7 -7
- package/lib/command-templates.ts +44 -4
- package/lib/control-projection.ts +105 -0
- package/lib/control.ts +33 -18
- package/lib/draft-sleep.ts +3 -3
- package/lib/file-state.ts +68 -61
- package/lib/inspector-overlay.ts +93 -58
- package/lib/limits.ts +5 -3
- package/lib/prompts.ts +1 -1
- package/lib/recipe-control.ts +9 -2
- package/lib/review-control.ts +4 -5
- package/lib/runs-control-delivery.ts +45 -17
- package/lib/runs-controls.ts +12 -3
- package/lib/runs-trace.ts +23 -19
- package/lib/runtime-identity.ts +39 -0
- package/lib/runtime-triage.ts +120 -0
- package/lib/tool-review-scheduler.ts +7 -7
- package/lib/tools-inspect.ts +60 -16
- package/lib/tools-message.ts +7 -8
- package/lib/tools-response.ts +0 -12
- package/lib/tools.ts +4 -4
- package/lib/trace-projection.ts +33 -10
- package/package.json +1 -1
- package/scripts/locker.mjs +9 -16
- package/scripts/music-player.mjs +7 -13
- package/scripts/release-gates.mjs +33 -2
- package/scripts/validate-recipe.mjs +5 -4
- package/skills/actors/SKILL.md +12 -7
- package/skills/swarm/SKILL.md +0 -2
- package/docs/actors-deep-reference.md +0 -108
- package/docs/component-recipes.md +0 -45
- package/docs/task-first-recipes.md +0 -261
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure runtime Control triage policy.
|
|
3
|
+
* Zones: pending admission, status-age classification, terminal/replacement staleness
|
|
4
|
+
* Owns Control triage decisions; journal reads and owner filtering stay in adapters, while safe fields come from control-projection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as ControlProjection from "./control-projection.ts";
|
|
8
|
+
import type { RunControlRecord, RunControlStatus } from "./runs-controls.ts";
|
|
9
|
+
|
|
10
|
+
export const RUNTIME_CONTROL_STALE_AFTER_MS = 5 * 60 * 1_000;
|
|
11
|
+
|
|
12
|
+
const PENDING_STATUSES = new Set<RunControlStatus>([
|
|
13
|
+
"queued",
|
|
14
|
+
"delivered",
|
|
15
|
+
"claimed",
|
|
16
|
+
]);
|
|
17
|
+
const TERMINAL_STATUSES = new Set<RunControlStatus>(["handled", "failed"]);
|
|
18
|
+
|
|
19
|
+
export interface RuntimeTriageRun {
|
|
20
|
+
run: string;
|
|
21
|
+
runInstanceId?: string;
|
|
22
|
+
status: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RuntimePendingControl
|
|
26
|
+
extends ControlProjection.ProjectedRunControl {
|
|
27
|
+
age_ms: number;
|
|
28
|
+
reason: "age_threshold_reached" | "run_generation_replaced" | "run_generation_unavailable" | "run_terminal" | "within_age_threshold";
|
|
29
|
+
run: string;
|
|
30
|
+
status: "queued" | "delivered" | "claimed";
|
|
31
|
+
status_at: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RuntimeControlDiagnostic {
|
|
35
|
+
reason: "invalid_action" | "invalid_control_id" | "invalid_generation" | "invalid_record" | "invalid_status" | "invalid_status_timestamp";
|
|
36
|
+
run: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RuntimeControlClassification {
|
|
40
|
+
diagnostic?: RuntimeControlDiagnostic;
|
|
41
|
+
pending?: RuntimePendingControl;
|
|
42
|
+
stale?: RuntimePendingControl;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function diagnostic(
|
|
46
|
+
run: string,
|
|
47
|
+
reason: RuntimeControlDiagnostic["reason"],
|
|
48
|
+
): RuntimeControlClassification {
|
|
49
|
+
return { diagnostic: { reason, run } };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function statusTimestamp(
|
|
53
|
+
record: Record<string, unknown>,
|
|
54
|
+
status: RunControlStatus,
|
|
55
|
+
): unknown {
|
|
56
|
+
if (status === "failed") return record.failed_at;
|
|
57
|
+
if (status === "handled") return record.handled_at;
|
|
58
|
+
if (status === "claimed") return record.claimed_at;
|
|
59
|
+
if (status === "delivered") return record.delivered_at;
|
|
60
|
+
return record.queued_at;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function classifyRuntimeControl(
|
|
64
|
+
run: RuntimeTriageRun,
|
|
65
|
+
value: unknown,
|
|
66
|
+
nowMs: number,
|
|
67
|
+
): RuntimeControlClassification {
|
|
68
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
69
|
+
return diagnostic(run.run, "invalid_record");
|
|
70
|
+
}
|
|
71
|
+
const record = value as Record<string, unknown>;
|
|
72
|
+
const status = record.status as RunControlStatus;
|
|
73
|
+
if (!PENDING_STATUSES.has(status) && !TERMINAL_STATUSES.has(status)) {
|
|
74
|
+
return diagnostic(run.run, "invalid_status");
|
|
75
|
+
}
|
|
76
|
+
if (typeof record.id !== "string" || !record.id) {
|
|
77
|
+
return diagnostic(run.run, "invalid_control_id");
|
|
78
|
+
}
|
|
79
|
+
if (typeof record.action !== "string" || !record.action) {
|
|
80
|
+
return diagnostic(run.run, "invalid_action");
|
|
81
|
+
}
|
|
82
|
+
if (typeof record.run_instance_id !== "string" || !record.run_instance_id) {
|
|
83
|
+
return diagnostic(run.run, "invalid_generation");
|
|
84
|
+
}
|
|
85
|
+
const timestamp = statusTimestamp(record, status);
|
|
86
|
+
const timestampMs = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
|
|
87
|
+
if (!Number.isFinite(timestampMs)) {
|
|
88
|
+
return diagnostic(run.run, "invalid_status_timestamp");
|
|
89
|
+
}
|
|
90
|
+
if (TERMINAL_STATUSES.has(status)) return {};
|
|
91
|
+
const ageMs = Math.max(0, Math.floor(nowMs - timestampMs));
|
|
92
|
+
const staleReason: RuntimePendingControl["reason"] | undefined =
|
|
93
|
+
!run.runInstanceId
|
|
94
|
+
? "run_generation_unavailable"
|
|
95
|
+
: record.run_instance_id !== run.runInstanceId
|
|
96
|
+
? "run_generation_replaced"
|
|
97
|
+
: run.status !== "running"
|
|
98
|
+
? "run_terminal"
|
|
99
|
+
: ageMs >= RUNTIME_CONTROL_STALE_AFTER_MS
|
|
100
|
+
? "age_threshold_reached"
|
|
101
|
+
: undefined;
|
|
102
|
+
const projected = ControlProjection.projectRunControl(
|
|
103
|
+
record as unknown as RunControlRecord,
|
|
104
|
+
);
|
|
105
|
+
const pending: RuntimePendingControl = {
|
|
106
|
+
...projected,
|
|
107
|
+
age_ms: ageMs,
|
|
108
|
+
reason: staleReason ?? "within_age_threshold",
|
|
109
|
+
run: run.run,
|
|
110
|
+
status: status as RuntimePendingControl["status"],
|
|
111
|
+
status_at: statusTimestamp(
|
|
112
|
+
projected as unknown as Record<string, unknown>,
|
|
113
|
+
status,
|
|
114
|
+
) as string,
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
pending,
|
|
118
|
+
...(staleReason ? { stale: pending } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -404,7 +404,7 @@ export function applyApprovedToolReviewAtSessionBoundary(
|
|
|
404
404
|
...state,
|
|
405
405
|
failedStage: "approval_state",
|
|
406
406
|
lastError: "approved review path is unavailable",
|
|
407
|
-
nextAction: "message
|
|
407
|
+
nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
|
|
408
408
|
phase: "processing_failed" as const,
|
|
409
409
|
updatedAt: (options.now ?? (() => new Date()))().toISOString(),
|
|
410
410
|
};
|
|
@@ -427,7 +427,7 @@ export function applyApprovedToolReviewAtSessionBoundary(
|
|
|
427
427
|
...state,
|
|
428
428
|
failedStage: "transaction",
|
|
429
429
|
lastError: `tool review transaction remained ${transaction.phase}`,
|
|
430
|
-
nextAction: "message
|
|
430
|
+
nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
|
|
431
431
|
phase: "processing_failed" as const,
|
|
432
432
|
updatedAt: (options.now ?? (() => new Date()))().toISOString(),
|
|
433
433
|
};
|
|
@@ -458,7 +458,7 @@ export function applyApprovedToolReviewAtSessionBoundary(
|
|
|
458
458
|
...pending,
|
|
459
459
|
failedStage: "lineage",
|
|
460
460
|
lastError: error instanceof Error ? error.message : String(error),
|
|
461
|
-
nextAction: "message
|
|
461
|
+
nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
|
|
462
462
|
};
|
|
463
463
|
writeJsonAtomic(statePath, failedPending);
|
|
464
464
|
return { outcome: "lineage_pending", state: failedPending };
|
|
@@ -478,7 +478,7 @@ export function applyApprovedToolReviewAtSessionBoundary(
|
|
|
478
478
|
...state,
|
|
479
479
|
failedStage: "transaction",
|
|
480
480
|
lastError: error instanceof Error ? error.message : String(error),
|
|
481
|
-
nextAction: "message
|
|
481
|
+
nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
|
|
482
482
|
phase: "processing_failed" as const,
|
|
483
483
|
updatedAt: (options.now ?? (() => new Date()))().toISOString(),
|
|
484
484
|
};
|
|
@@ -541,7 +541,7 @@ export function createToolReviewScheduler(
|
|
|
541
541
|
...existing,
|
|
542
542
|
failedStage: processed.stage ?? "review_result",
|
|
543
543
|
lastError: processed.error ?? "automatic tool reviewer failed",
|
|
544
|
-
nextAction: "message
|
|
544
|
+
nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
|
|
545
545
|
phase: processed.outcome === "processing_failed"
|
|
546
546
|
? "processing_failed"
|
|
547
547
|
: "failed",
|
|
@@ -577,7 +577,7 @@ export function createToolReviewScheduler(
|
|
|
577
577
|
attempts: existing.attempts + 1,
|
|
578
578
|
failedStage: "review_launch",
|
|
579
579
|
lastError: error instanceof Error ? error.message : String(error),
|
|
580
|
-
nextAction: "message
|
|
580
|
+
nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
|
|
581
581
|
phase: "failed",
|
|
582
582
|
updatedAt: now().toISOString(),
|
|
583
583
|
} satisfies ToolReviewAdmissionState);
|
|
@@ -612,7 +612,7 @@ export function createToolReviewScheduler(
|
|
|
612
612
|
failedStage: "review_launch",
|
|
613
613
|
inputPath: batch.inputPath,
|
|
614
614
|
lastError: error instanceof Error ? error.message : String(error),
|
|
615
|
-
nextAction: "message
|
|
615
|
+
nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
|
|
616
616
|
phase: "failed",
|
|
617
617
|
reviewerInputPath: batch.reviewerInputPath,
|
|
618
618
|
reviewId: batch.reviewId,
|
package/lib/tools-inspect.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
9
|
import * as AsyncRuns from "./async-runs.ts";
|
|
10
|
+
import * as ControlProjection from "./control-projection.ts";
|
|
10
11
|
import * as Inspector from "./inspector.ts";
|
|
11
12
|
import * as Limits from "./limits.ts";
|
|
12
13
|
import * as Paths from "./paths.ts";
|
|
@@ -15,6 +16,8 @@ import * as ReviewDiagnostics from "./review-diagnostics.ts";
|
|
|
15
16
|
import * as RunsControls from "./runs-controls.ts";
|
|
16
17
|
import * as RunControlDelivery from "./runs-control-delivery.ts";
|
|
17
18
|
import * as RunsTrace from "./runs-trace.ts";
|
|
19
|
+
import * as RuntimeIdentity from "./runtime-identity.ts";
|
|
20
|
+
import * as RuntimeTriage from "./runtime-triage.ts";
|
|
18
21
|
import * as Schema from "./schema.ts";
|
|
19
22
|
import * as ToolsAccess from "./tools-access.ts";
|
|
20
23
|
import * as ToolsResponse from "./tools-response.ts";
|
|
@@ -35,7 +38,8 @@ function runtimeStatus(): Record<string, unknown> {
|
|
|
35
38
|
return {
|
|
36
39
|
automatic_review: process.env.PI_ACTORS_AUTOMATIC_REVIEW !== "off",
|
|
37
40
|
run_root: Paths.getRunStateRoot(),
|
|
38
|
-
|
|
41
|
+
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
42
|
+
version: RuntimeIdentity.getPackageVersion(),
|
|
39
43
|
};
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -69,13 +73,39 @@ function runtimeTriage(
|
|
|
69
73
|
const inventory = runtimeRuns(ctx, deps, undefined);
|
|
70
74
|
const runs = inventory.runs as Array<Record<string, unknown>>;
|
|
71
75
|
const failed = runs.filter((run) => run.status === "failed");
|
|
72
|
-
const
|
|
76
|
+
const pendingControls: RuntimeTriage.RuntimePendingControl[] = [];
|
|
77
|
+
const staleControls: RuntimeTriage.RuntimePendingControl[] = [];
|
|
78
|
+
const controlDiagnostics: Array<Record<string, unknown>> = [];
|
|
79
|
+
const nowMs = Date.now();
|
|
80
|
+
for (const run of runs) {
|
|
73
81
|
const stateDir = typeof run.state_dir === "string" ? run.state_dir : undefined;
|
|
74
|
-
if (!stateDir)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
if (!stateDir) continue;
|
|
83
|
+
const runId = String(run.run ?? "unknown");
|
|
84
|
+
const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
|
|
85
|
+
controlDiagnostics.push(...journal.diagnostics.map((diagnostic) => ({
|
|
86
|
+
...(diagnostic.line === undefined ? {} : { line: diagnostic.line }),
|
|
87
|
+
reason: diagnostic.line === undefined
|
|
88
|
+
? "unreadable_control_journal"
|
|
89
|
+
: "invalid_control_json",
|
|
90
|
+
run: runId,
|
|
91
|
+
})));
|
|
92
|
+
for (const control of journal.records) {
|
|
93
|
+
const classified = RuntimeTriage.classifyRuntimeControl(
|
|
94
|
+
{
|
|
95
|
+
run: runId,
|
|
96
|
+
...(typeof run.run_instance_id === "string"
|
|
97
|
+
? { runInstanceId: run.run_instance_id }
|
|
98
|
+
: {}),
|
|
99
|
+
status: String(run.status ?? "unknown"),
|
|
100
|
+
},
|
|
101
|
+
control,
|
|
102
|
+
nowMs,
|
|
103
|
+
);
|
|
104
|
+
if (classified.pending) pendingControls.push(classified.pending);
|
|
105
|
+
if (classified.stale) staleControls.push(classified.stale);
|
|
106
|
+
if (classified.diagnostic) controlDiagnostics.push({ ...classified.diagnostic });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
79
109
|
const attentionEvents = runs.flatMap((run) => {
|
|
80
110
|
const stateDir = typeof run.state_dir === "string" ? run.state_dir : undefined;
|
|
81
111
|
if (!stateDir) return [];
|
|
@@ -90,18 +120,31 @@ function runtimeTriage(
|
|
|
90
120
|
ts: event.ts,
|
|
91
121
|
}));
|
|
92
122
|
});
|
|
123
|
+
const boundedStaleControls = staleControls.slice(0, 40);
|
|
124
|
+
const staleKeys = new Set(boundedStaleControls.map((control) =>
|
|
125
|
+
`${control.run}\0${control.run_instance_id}\0${control.id}`,
|
|
126
|
+
));
|
|
127
|
+
const boundedPendingControls = [
|
|
128
|
+
...boundedStaleControls,
|
|
129
|
+
...pendingControls.filter((control) =>
|
|
130
|
+
!staleKeys.has(`${control.run}\0${control.run_instance_id}\0${control.id}`),
|
|
131
|
+
),
|
|
132
|
+
].slice(0, 40);
|
|
93
133
|
return {
|
|
94
134
|
...inventory,
|
|
95
135
|
failed_runs: failed,
|
|
96
|
-
|
|
136
|
+
pending_control_count: pendingControls.length,
|
|
137
|
+
pending_controls: boundedPendingControls,
|
|
138
|
+
stale_control_count: staleControls.length,
|
|
139
|
+
stale_controls: boundedStaleControls,
|
|
140
|
+
control_diagnostic_count: controlDiagnostics.length,
|
|
141
|
+
control_diagnostics: controlDiagnostics.slice(0, 40),
|
|
97
142
|
attention_events: attentionEvents.slice(-40).reverse(),
|
|
98
143
|
next_actions: [
|
|
99
144
|
...(failed.length ? ["inspect target=runtime view=runs status=failed"] : []),
|
|
100
|
-
...
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
104
|
-
: []),
|
|
145
|
+
...staleControls.slice(0, 3).map((entry) =>
|
|
146
|
+
`inspect target=run:${entry.run} view=control`,
|
|
147
|
+
),
|
|
105
148
|
...(attentionEvents.length
|
|
106
149
|
? attentionEvents.slice(-3).map((entry) =>
|
|
107
150
|
`inspect target=run:${String(entry.run)} view=trace source=runtime`,
|
|
@@ -116,9 +159,9 @@ function compactRuntime(view: string, details: Record<string, unknown>): string
|
|
|
116
159
|
return `\nruntime runs=${(details.runs as unknown[] | undefined)?.length ?? 0}`;
|
|
117
160
|
}
|
|
118
161
|
if (view === "triage") {
|
|
119
|
-
return `\nruntime failed=${(details.failed_runs as unknown[] | undefined)?.length ?? 0}
|
|
162
|
+
return `\nruntime failed=${(details.failed_runs as unknown[] | undefined)?.length ?? 0} pending_controls=${Number(details.pending_control_count ?? 0)} stale_controls=${Number(details.stale_control_count ?? 0)} attention=${(details.attention_events as unknown[] | undefined)?.length ?? 0}`;
|
|
120
163
|
}
|
|
121
|
-
return `\nruntime version=${String(details.version
|
|
164
|
+
return `\nruntime version=${String(details.version)} state_schema=${String(details.state_schema)} automatic_review=${String(details.automatic_review)}`;
|
|
122
165
|
}
|
|
123
166
|
|
|
124
167
|
function inspectRuntime(
|
|
@@ -232,7 +275,8 @@ function inspectRun(
|
|
|
232
275
|
: undefined,
|
|
233
276
|
recent_controls: RunsControls.readRunControlsFromStateDir(stateDir)
|
|
234
277
|
.slice(-Math.max(1, Number(input.lines || 20)))
|
|
235
|
-
.reverse()
|
|
278
|
+
.reverse()
|
|
279
|
+
.map(ControlProjection.projectRunControl),
|
|
236
280
|
};
|
|
237
281
|
}
|
|
238
282
|
|
package/lib/tools-message.ts
CHANGED
|
@@ -46,8 +46,7 @@ function runNextActions(run: string, result: Record<string, unknown>): string[]
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export interface ControlToolDeps {
|
|
49
|
-
|
|
50
|
-
handleRuntimeMessage?: (
|
|
49
|
+
handleRuntimeControl?: (
|
|
51
50
|
action: string,
|
|
52
51
|
input: unknown,
|
|
53
52
|
) => Record<string, unknown>;
|
|
@@ -64,11 +63,11 @@ export function createControlToolDefinition<TContext = unknown>(
|
|
|
64
63
|
parameters: Schema.objectSchema(
|
|
65
64
|
{
|
|
66
65
|
target: Schema.stringSchema("Run target run:<id>, or literal runtime."),
|
|
67
|
-
action: Schema.stringSchema("Lowercase
|
|
66
|
+
action: Schema.stringSchema("Lowercase ASCII Control action, at most 64 characters."),
|
|
68
67
|
input: Schema.unionSchema([
|
|
69
|
-
Schema.stringSchema("Optional Control input string."),
|
|
70
|
-
Schema.looseObjectSchema("Optional structured Control input."),
|
|
71
|
-
Schema.arraySchema("Optional structured Control input array."),
|
|
68
|
+
Schema.stringSchema("Optional Control input string; serialized JSON must fit 380 bytes."),
|
|
69
|
+
Schema.looseObjectSchema("Optional structured Control input; serialized JSON must fit 380 bytes."),
|
|
70
|
+
Schema.arraySchema("Optional structured Control input array; serialized JSON must fit 380 bytes."),
|
|
72
71
|
]),
|
|
73
72
|
verbose: Schema.booleanSchema("Return full JSON instead of compact text."),
|
|
74
73
|
},
|
|
@@ -89,10 +88,10 @@ export function createControlToolDefinition<TContext = unknown>(
|
|
|
89
88
|
`unsupported runtime Control action: ${request.action}; use review.retry or review.reset`,
|
|
90
89
|
);
|
|
91
90
|
}
|
|
92
|
-
if (!deps.
|
|
91
|
+
if (!deps.handleRuntimeControl) {
|
|
93
92
|
throw new Error("runtime Control is unavailable");
|
|
94
93
|
}
|
|
95
|
-
result = deps.
|
|
94
|
+
result = deps.handleRuntimeControl(request.action, request.input);
|
|
96
95
|
} else {
|
|
97
96
|
const run = request.target.slice(4);
|
|
98
97
|
const status = ToolsAccess.assertRunAccessibleToContext(run, ctx);
|
package/lib/tools-response.ts
CHANGED
|
@@ -133,18 +133,6 @@ export function compactAsyncRunStatus(value: unknown): string {
|
|
|
133
133
|
return `\n${tokens.join(" ")}`;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export function artifactNextActions(
|
|
137
|
-
run: unknown,
|
|
138
|
-
artifacts: Record<string, unknown>,
|
|
139
|
-
): string[] {
|
|
140
|
-
const id = String(run ?? "").trim();
|
|
141
|
-
if (!id || Object.keys(artifacts).length === 0) return [];
|
|
142
|
-
return [
|
|
143
|
-
`inspect target=run:${id} view=artifacts verbose=true`,
|
|
144
|
-
`inspect target=run:${id} view=messages`,
|
|
145
|
-
];
|
|
146
|
-
}
|
|
147
|
-
|
|
148
136
|
export function maybeJsonText(
|
|
149
137
|
value: unknown,
|
|
150
138
|
verbose: boolean | undefined,
|
package/lib/tools.ts
CHANGED
|
@@ -25,9 +25,9 @@ export interface CoreActorToolDefinitionDeps<
|
|
|
25
25
|
configPath: string;
|
|
26
26
|
getActiveTools: () => string[];
|
|
27
27
|
getRuntimeTool: (name: string) => unknown;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
handleRuntimeControl?: (
|
|
29
|
+
action: string,
|
|
30
|
+
input: unknown,
|
|
31
31
|
) => Record<string, unknown>;
|
|
32
32
|
registryRuntime: Pick<
|
|
33
33
|
RegisterToolRuntimeDeps<TContext>,
|
|
@@ -74,7 +74,7 @@ export function createCoreActorToolDefinitions<
|
|
|
74
74
|
}),
|
|
75
75
|
ToolsSpawn.createSpawnToolDefinition<TContext>(),
|
|
76
76
|
ToolsMessage.createControlToolDefinition<TContext>({
|
|
77
|
-
|
|
77
|
+
handleRuntimeControl: deps.handleRuntimeControl,
|
|
78
78
|
}),
|
|
79
79
|
ToolsInspect.createInspectToolDefinition<TContext>({
|
|
80
80
|
getTool: (name) => deps.getRuntimeTool(name),
|
package/lib/trace-projection.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { existsSync, statSync } from "node:fs";
|
|
8
8
|
import { join, relative } from "node:path";
|
|
9
9
|
|
|
10
|
+
import * as ControlProjection from "./control-projection.ts";
|
|
10
11
|
import * as ExecutionSessions from "./execution-sessions.ts";
|
|
11
12
|
import * as Limits from "./limits.ts";
|
|
12
13
|
import {
|
|
@@ -78,6 +79,25 @@ function diagnosticItems(diagnostics: StateReadDiagnostic[]): TraceItem[] {
|
|
|
78
79
|
}));
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
function controlDiagnosticItems(
|
|
83
|
+
diagnostics: StateReadDiagnostic[],
|
|
84
|
+
): TraceItem[] {
|
|
85
|
+
return diagnostics.slice(-SOURCE_LIMIT).map((diagnostic, index) => ({
|
|
86
|
+
id: `control-diagnostic:${diagnostic.line ?? 0}:${index}`,
|
|
87
|
+
ts: new Date(0).toISOString(),
|
|
88
|
+
source: "runtime",
|
|
89
|
+
kind: "state.read_error",
|
|
90
|
+
summary: "Malformed or unreadable Control evidence",
|
|
91
|
+
level: "warning",
|
|
92
|
+
detail: {
|
|
93
|
+
...(diagnostic.line === undefined ? {} : { line: diagnostic.line }),
|
|
94
|
+
reason: diagnostic.line === undefined
|
|
95
|
+
? "unreadable_control_journal"
|
|
96
|
+
: "invalid_control_json",
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
81
101
|
function traceEventItems(stateDir: string): TraceItem[] {
|
|
82
102
|
const read = readJsonlFileResilient<TraceEvent>(join(stateDir, "trace.jsonl"));
|
|
83
103
|
return [
|
|
@@ -102,16 +122,19 @@ function controlItems(stateDir: string): TraceItem[] {
|
|
|
102
122
|
join(stateDir, "controls.jsonl"),
|
|
103
123
|
);
|
|
104
124
|
return [
|
|
105
|
-
...read.records.slice(-SOURCE_LIMIT).map((control) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
...read.records.slice(-SOURCE_LIMIT).map((control) => {
|
|
126
|
+
const projected = ControlProjection.projectRunControl(control);
|
|
127
|
+
return {
|
|
128
|
+
id: `control:${projected.id}`,
|
|
129
|
+
ts: latestControlTimestamp(projected),
|
|
130
|
+
source: "control" as const,
|
|
131
|
+
kind: `control.${projected.status}`,
|
|
132
|
+
summary: `${projected.action} ${projected.status}`,
|
|
133
|
+
...(projected.status === "failed" ? { level: "error" as const } : {}),
|
|
134
|
+
detail: projected,
|
|
135
|
+
};
|
|
136
|
+
}),
|
|
137
|
+
...controlDiagnosticItems(read.diagnostics),
|
|
115
138
|
];
|
|
116
139
|
}
|
|
117
140
|
|
package/package.json
CHANGED
package/scripts/locker.mjs
CHANGED
|
@@ -9,13 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
|
-
import { createHash
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
13
|
import {
|
|
14
14
|
appendFileSync,
|
|
15
15
|
existsSync,
|
|
16
16
|
mkdirSync,
|
|
17
17
|
readFileSync,
|
|
18
|
-
rmSync,
|
|
19
18
|
} from "node:fs";
|
|
20
19
|
import { createServer } from "node:net";
|
|
21
20
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -35,6 +34,7 @@ async function importRuntimeModule(name) {
|
|
|
35
34
|
|
|
36
35
|
const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
|
|
37
36
|
await importRuntimeModule("file-state");
|
|
37
|
+
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
38
38
|
|
|
39
39
|
function parseArgs(argv) {
|
|
40
40
|
const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
|
|
@@ -59,7 +59,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
59
59
|
const queuePath = join(stateDir, "queue.json");
|
|
60
60
|
const locksPath = join(stateDir, "locks.json");
|
|
61
61
|
const journalPath = join(stateDir, "journal.jsonl");
|
|
62
|
-
const tracePath = join(stateDir, "trace.jsonl");
|
|
63
62
|
const controlsPath = join(stateDir, "controls.jsonl");
|
|
64
63
|
const controlPath = join(stateDir, "control.fifo");
|
|
65
64
|
let runInstanceId;
|
|
@@ -104,18 +103,13 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
function emitTrace(kind, summary, data = {}, level = "info") {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
level,
|
|
115
|
-
summary,
|
|
116
|
-
ts: new Date().toISOString(),
|
|
117
|
-
})}\n`,
|
|
118
|
-
);
|
|
106
|
+
appendRunTraceEvent(stateDir, {
|
|
107
|
+
attention: "followup",
|
|
108
|
+
data,
|
|
109
|
+
kind,
|
|
110
|
+
level,
|
|
111
|
+
summary,
|
|
112
|
+
});
|
|
119
113
|
}
|
|
120
114
|
|
|
121
115
|
function now() {
|
|
@@ -454,7 +448,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
454
448
|
}
|
|
455
449
|
|
|
456
450
|
async function serveNamedPipe(endpoint) {
|
|
457
|
-
rmSync(endpoint.path, { force: true });
|
|
458
451
|
let resolveStopped;
|
|
459
452
|
const stopped = new Promise((resolve) => {
|
|
460
453
|
resolveStopped = resolve;
|
package/scripts/music-player.mjs
CHANGED
|
@@ -52,6 +52,7 @@ async function importRuntimeModule(name) {
|
|
|
52
52
|
|
|
53
53
|
const { acquireFileMutationLock, writeTextAtomic } =
|
|
54
54
|
await importRuntimeModule("file-state");
|
|
55
|
+
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
55
56
|
|
|
56
57
|
const AUDIO_EXTENSIONS = new Set([
|
|
57
58
|
".aac",
|
|
@@ -420,18 +421,12 @@ function readText(path) {
|
|
|
420
421
|
}
|
|
421
422
|
|
|
422
423
|
function emitPlayerEvent(ctx, kind, summary, data = {}) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
level: "info",
|
|
430
|
-
summary,
|
|
431
|
-
ts: new Date().toISOString(),
|
|
432
|
-
})}\n`,
|
|
433
|
-
"a",
|
|
434
|
-
);
|
|
424
|
+
appendRunTraceEvent(ctx.stateDir, {
|
|
425
|
+
data,
|
|
426
|
+
kind,
|
|
427
|
+
level: "info",
|
|
428
|
+
summary,
|
|
429
|
+
});
|
|
435
430
|
}
|
|
436
431
|
|
|
437
432
|
function emitTrackEvent(ctx, index, count, track, player) {
|
|
@@ -859,7 +854,6 @@ async function playMain(args) {
|
|
|
859
854
|
const ctx = {
|
|
860
855
|
commandFile: join(stateDir, "command.txt"),
|
|
861
856
|
current: undefined,
|
|
862
|
-
eventFile: join(stateDir, "trace.jsonl"),
|
|
863
857
|
controlsFile: join(stateDir, "controls.jsonl"),
|
|
864
858
|
pidFile: join(stateDir, "current.pid"),
|
|
865
859
|
playerControlFile: join(stateDir, "player-control.txt"),
|
|
@@ -145,13 +145,44 @@ try {
|
|
|
145
145
|
}
|
|
146
146
|
console.log("[release] removed-surface and legacy-fallback allowlists checked");
|
|
147
147
|
|
|
148
|
-
const
|
|
148
|
+
const directTraceWrite = /(?:appendFileSync|writeFileSync|writeText(?:Atomic)?)\s*\(\s*[A-Za-z0-9_.]*?(?:trace|event)(?:Path|File)/iu;
|
|
149
|
+
for (const path of files.filter((candidate) =>
|
|
150
|
+
(candidate.startsWith("lib/") && candidate.endsWith(".ts")) ||
|
|
151
|
+
(candidate.startsWith("scripts/") && candidate.endsWith(".mjs"))
|
|
152
|
+
)) {
|
|
153
|
+
if (path === "lib/runs-trace.ts") continue;
|
|
154
|
+
check(!directTraceWrite.test(stagedText(path) ?? ""), `direct Trace writer outside canonical authority: ${path}`);
|
|
155
|
+
}
|
|
156
|
+
const canonicalTrace = stagedText("lib/runs-trace.ts") ?? "";
|
|
157
|
+
check(/withFileMutationLock\(path/u.test(canonicalTrace), "canonical Trace append lacks mutation lock");
|
|
158
|
+
for (const path of [
|
|
159
|
+
"scripts/async-runner.mjs",
|
|
160
|
+
"scripts/locker.mjs",
|
|
161
|
+
"scripts/music-player.mjs",
|
|
162
|
+
]) {
|
|
163
|
+
const text = stagedText(path) ?? "";
|
|
164
|
+
check(
|
|
165
|
+
text.includes('importRuntimeModule("runs-trace")') && text.includes("appendRunTraceEvent"),
|
|
166
|
+
`first-party Trace writer bypasses canonical runtime module: ${path}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
console.log("[release] canonical Trace writer residue checked");
|
|
170
|
+
|
|
171
|
+
check(
|
|
172
|
+
(stagedText("scripts/validate-recipe.mjs") ?? "").includes(
|
|
173
|
+
"qaReport.diagnostics.length === 0 && qaReport.warnings.length === 0",
|
|
174
|
+
),
|
|
175
|
+
"Recipe QA warnings are not release-blocking",
|
|
176
|
+
);
|
|
177
|
+
console.log("[release] zero-warning Recipe QA gate checked");
|
|
178
|
+
|
|
179
|
+
const baselineShippedLines = 28_853;
|
|
149
180
|
const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
|
|
150
181
|
const shippedLines = files
|
|
151
182
|
.filter((path) => shippedPath.test(path))
|
|
152
183
|
.reduce((total, path) => total + (((stagedText(path) ?? "").match(/\n/gu) ?? []).length + 1), 0);
|
|
153
184
|
check(shippedLines < baselineShippedLines, `shipped lines ${shippedLines} are not below baseline ${baselineShippedLines}`);
|
|
154
|
-
console.log(`[release] shipped lines ${shippedLines} <
|
|
185
|
+
console.log(`[release] shipped lines ${shippedLines} < released baseline ${baselineShippedLines}`);
|
|
155
186
|
|
|
156
187
|
const sources = files.filter((path) => path === "index.ts" || (path.startsWith("lib/") && path.endsWith(".ts")));
|
|
157
188
|
const sourceSet = new Set(sources);
|
|
@@ -29,7 +29,7 @@ export function validateRecipeUsage() {
|
|
|
29
29
|
return `Usage:
|
|
30
30
|
validate-recipe.mjs <recipe-file-or-dir> [--all] [--qa] [--summary]
|
|
31
31
|
|
|
32
|
-
Validates one template recipe file, or all *.json/*.md files in a directory when --all is set. Add --qa for packaged-recipe quality checks. Add --summary for compact CLI output.`;
|
|
32
|
+
Validates one template recipe file, or all *.json/*.md files in a directory when --all is set. Add --qa for packaged-recipe quality checks, where diagnostics and warnings fail validation. Add --summary for compact CLI output.`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function expandPath(value) {
|
|
@@ -120,8 +120,6 @@ function validateHelperPaths(file, config) {
|
|
|
120
120
|
function qaDiagnostics(file, config) {
|
|
121
121
|
const diagnostics = [];
|
|
122
122
|
const warnings = [];
|
|
123
|
-
if (typeof config.description !== "string" || !config.description.trim())
|
|
124
|
-
warnings.push("description: missing or empty");
|
|
125
123
|
if (config.mailbox !== undefined)
|
|
126
124
|
diagnostics.push("recipe.mailbox was removed; use control actions and Trace events");
|
|
127
125
|
diagnostics.push(...validateArtifactDeclarations(config));
|
|
@@ -138,7 +136,7 @@ function qaDiagnostics(file, config) {
|
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
function qaOk(qaReport) {
|
|
141
|
-
return qaReport.diagnostics.length === 0;
|
|
139
|
+
return qaReport.diagnostics.length === 0 && qaReport.warnings.length === 0;
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
function validateFile(file, qa = false) {
|
|
@@ -225,6 +223,9 @@ function summarizeReport(report) {
|
|
|
225
223
|
...(result.qa?.diagnostics?.length
|
|
226
224
|
? { diagnostics: result.qa.diagnostics }
|
|
227
225
|
: {}),
|
|
226
|
+
...(result.qa?.warnings?.length
|
|
227
|
+
? { warnings: result.qa.warnings }
|
|
228
|
+
: {}),
|
|
228
229
|
})),
|
|
229
230
|
};
|
|
230
231
|
}
|