@llblab/pi-actors 0.43.1 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -6
- package/BACKLOG.md +1 -189
- package/CHANGELOG.md +419 -402
- package/README.md +8 -6
- package/dist/index.js +7 -3
- package/dist/lib/async-runs.d.ts +2 -0
- package/dist/lib/async-runs.js +41 -34
- package/dist/lib/config.js +1 -0
- package/dist/lib/file-state.d.ts +3 -1
- package/dist/lib/file-state.js +25 -7
- package/dist/lib/inspector-overlay.js +46 -30
- package/dist/lib/inspector.js +8 -2
- package/dist/lib/limits.d.ts +10 -0
- package/dist/lib/limits.js +10 -0
- package/dist/lib/observability.d.ts +4 -2
- package/dist/lib/observability.js +43 -36
- package/dist/lib/recipes-discovery.js +20 -3
- package/dist/lib/recipes-references.d.ts +10 -0
- package/dist/lib/recipes-references.js +168 -5
- package/dist/lib/run-evidence-policy.d.ts +95 -0
- package/dist/lib/run-evidence-policy.js +177 -0
- package/dist/lib/run-ui-runtime.js +2 -0
- package/dist/lib/runs-controls.d.ts +7 -5
- package/dist/lib/runs-controls.js +186 -49
- package/dist/lib/runs-retention.js +27 -14
- package/dist/lib/runs-trace.d.ts +25 -1
- package/dist/lib/runs-trace.js +410 -21
- package/dist/lib/runtime-triage.js +6 -22
- package/dist/lib/schema.d.ts +3 -0
- package/dist/lib/schema.js +34 -1
- package/dist/lib/tools-inspect.js +58 -14
- package/dist/lib/tools-local.js +27 -10
- package/dist/lib/tools-spawn.js +2 -2
- package/dist/lib/trace-projection.js +90 -44
- package/dist/scripts/conformance.mjs +5 -0
- package/dist/scripts/locker.mjs +31 -74
- package/dist/scripts/music-player.mjs +41 -129
- package/dist/scripts/release-gates.mjs +26 -4
- package/dist/skills/actors/SKILL.md +9 -8
- package/dist/skills/swarm/SKILL.md +2 -2
- package/docs/README.md +0 -1
- package/docs/actor-inspector.md +3 -2
- package/docs/async-runs.md +10 -8
- package/docs/command-templates.md +2 -2
- package/docs/recipe-library.md +4 -2
- package/docs/template-recipes.md +10 -4
- package/docs/tool-registry.md +1 -1
- package/index.ts +9 -3
- package/lib/async-runs.ts +51 -54
- package/lib/config.ts +4 -0
- package/lib/file-state.ts +28 -7
- package/lib/inspector-overlay.ts +29 -17
- package/lib/inspector.ts +16 -2
- package/lib/limits.ts +10 -0
- package/lib/observability.ts +55 -57
- package/lib/recipes-discovery.ts +23 -3
- package/lib/recipes-references.ts +227 -7
- package/lib/run-evidence-policy.ts +242 -0
- package/lib/run-ui-runtime.ts +2 -0
- package/lib/runs-controls.ts +177 -108
- package/lib/runs-retention.ts +28 -20
- package/lib/runs-trace.ts +496 -21
- package/lib/runtime-triage.ts +11 -25
- package/lib/schema.ts +53 -1
- package/lib/tools-inspect.ts +56 -15
- package/lib/tools-local.ts +49 -19
- package/lib/tools-spawn.ts +2 -2
- package/lib/trace-projection.ts +137 -75
- package/package.json +1 -1
- package/scripts/conformance.mjs +5 -0
- package/scripts/locker.mjs +31 -74
- package/scripts/music-player.mjs +41 -129
- package/scripts/release-gates.mjs +26 -4
- package/skills/actors/SKILL.md +9 -8
- package/skills/swarm/SKILL.md +2 -2
- package/dist/lib/runtime-notifier.d.ts +0 -48
- package/dist/lib/runtime-notifier.js +0 -138
- package/docs/0.43-baseline.md +0 -39
- package/lib/runtime-notifier.ts +0 -211
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
* Zones: Run Recipe/Trace/Control views and runtime/recipe/tool diagnostics
|
|
4
4
|
* Owns exact inspect target/view dispatch; source projection stays in domain modules.
|
|
5
5
|
*/
|
|
6
|
+
import { statSync } from "node:fs";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import * as AsyncRuns from "./async-runs.js";
|
|
8
9
|
import * as ControlProjection from "./control-projection.js";
|
|
9
10
|
import * as Inspector from "./inspector.js";
|
|
10
11
|
import * as Limits from "./limits.js";
|
|
12
|
+
import { computeRunControlCapacity } from "./run-evidence-policy.js";
|
|
11
13
|
import * as Paths from "./paths.js";
|
|
12
14
|
import * as RecipesDiscovery from "./recipes-discovery.js";
|
|
15
|
+
import * as RecipesReferences from "./recipes-references.js";
|
|
13
16
|
import * as ReviewDiagnostics from "./review-diagnostics.js";
|
|
14
17
|
import * as RunsControls from "./runs-controls.js";
|
|
15
18
|
import * as RunControlDelivery from "./runs-control-delivery.js";
|
|
@@ -55,7 +58,9 @@ function runtimeTriage(ctx, deps) {
|
|
|
55
58
|
const failed = runs.filter((run) => run.status === "failed");
|
|
56
59
|
const pendingControls = [];
|
|
57
60
|
const staleControls = [];
|
|
61
|
+
const backpressuredRuns = [];
|
|
58
62
|
const controlDiagnostics = [];
|
|
63
|
+
const traceDiagnostics = [];
|
|
59
64
|
const nowMs = Date.now();
|
|
60
65
|
for (const run of runs) {
|
|
61
66
|
const stateDir = typeof run.state_dir === "string" ? run.state_dir : undefined;
|
|
@@ -63,13 +68,19 @@ function runtimeTriage(ctx, deps) {
|
|
|
63
68
|
continue;
|
|
64
69
|
const runId = String(run.run ?? "unknown");
|
|
65
70
|
const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
|
|
71
|
+
const capacity = computeRunControlCapacity(journal.records);
|
|
66
72
|
controlDiagnostics.push(...journal.diagnostics.map((diagnostic) => ({
|
|
67
73
|
...(diagnostic.line === undefined ? {} : { line: diagnostic.line }),
|
|
68
|
-
reason: diagnostic.line === undefined
|
|
69
|
-
? "unreadable_control_journal"
|
|
70
|
-
: "invalid_control_json",
|
|
71
|
-
run: runId,
|
|
74
|
+
reason: diagnostic.line === undefined ? "unreadable_control_journal" : "invalid_control_json", run: runId,
|
|
72
75
|
})));
|
|
76
|
+
if (capacity.backpressured)
|
|
77
|
+
backpressuredRuns.push({
|
|
78
|
+
run: runId, pending: capacity.pending, pending_limit: capacity.limit,
|
|
79
|
+
journal_bytes: statSync(RunsControls.runControlsFile(stateDir)).size,
|
|
80
|
+
});
|
|
81
|
+
const traceSummary = RunsTrace.summarizeRunTraceJournal(RunsTrace.readRunTraceJournal(stateDir));
|
|
82
|
+
if (!traceSummary.history_complete)
|
|
83
|
+
traceDiagnostics.push({ run: runId, ...traceSummary });
|
|
73
84
|
for (const control of journal.records) {
|
|
74
85
|
const classified = RuntimeTriage.classifyRuntimeControl({
|
|
75
86
|
run: runId,
|
|
@@ -114,12 +125,13 @@ function runtimeTriage(ctx, deps) {
|
|
|
114
125
|
pending_controls: boundedPendingControls,
|
|
115
126
|
stale_control_count: staleControls.length,
|
|
116
127
|
stale_controls: boundedStaleControls,
|
|
117
|
-
|
|
118
|
-
control_diagnostics: controlDiagnostics.slice(0, 40),
|
|
128
|
+
backpressured_run_count: backpressuredRuns.length, backpressured_runs: backpressuredRuns.slice(0, 40),
|
|
129
|
+
control_diagnostic_count: controlDiagnostics.length, control_diagnostics: controlDiagnostics.slice(0, 40),
|
|
130
|
+
trace_diagnostic_count: traceDiagnostics.length, trace_diagnostics: traceDiagnostics.slice(0, 40),
|
|
119
131
|
attention_events: attentionEvents.slice(-40).reverse(),
|
|
120
132
|
next_actions: [
|
|
121
133
|
...(failed.length ? ["inspect target=runtime view=runs status=failed"] : []),
|
|
122
|
-
...staleControls.slice(0, 3).map((entry) => `inspect target=run:${entry.run} view=control`),
|
|
134
|
+
...new Set([...staleControls, ...backpressuredRuns].slice(0, 3).map((entry) => `inspect target=run:${entry.run} view=control`)),
|
|
123
135
|
...(attentionEvents.length
|
|
124
136
|
? attentionEvents.slice(-3).map((entry) => `inspect target=run:${String(entry.run)} view=trace source=runtime`)
|
|
125
137
|
: []),
|
|
@@ -131,7 +143,7 @@ function compactRuntime(view, details) {
|
|
|
131
143
|
return `\nruntime runs=${details.runs?.length ?? 0}`;
|
|
132
144
|
}
|
|
133
145
|
if (view === "triage") {
|
|
134
|
-
return `\nruntime failed=${details.failed_runs?.length ?? 0} pending_controls=${Number(details.pending_control_count ?? 0)} stale_controls=${Number(details.stale_control_count ?? 0)} attention=${details.attention_events?.length ?? 0}`;
|
|
146
|
+
return `\nruntime failed=${details.failed_runs?.length ?? 0} pending_controls=${Number(details.pending_control_count ?? 0)} stale_controls=${Number(details.stale_control_count ?? 0)} backpressured=${Number(details.backpressured_run_count ?? 0)} incomplete_trace=${Number(details.trace_diagnostic_count ?? 0)} attention=${details.attention_events?.length ?? 0}`;
|
|
135
147
|
}
|
|
136
148
|
return `\nruntime version=${String(details.version)} state_schema=${String(details.state_schema)} automatic_review=${String(details.automatic_review)}`;
|
|
137
149
|
}
|
|
@@ -161,8 +173,17 @@ function inspectRecipes(view, deps) {
|
|
|
161
173
|
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
162
174
|
{ root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
|
|
163
175
|
]);
|
|
176
|
+
const skillRecipeNamespaces = RecipesReferences.getActiveSkillRecipeNamespaces();
|
|
164
177
|
const summary = {
|
|
165
178
|
...RecipesDiscovery.summarizeDiscovery(discovered),
|
|
179
|
+
skill_recipe_namespaces: skillRecipeNamespaces,
|
|
180
|
+
skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
|
|
181
|
+
.filter(([, roots]) => roots.length > 1)
|
|
182
|
+
.map(([name, roots]) => ({
|
|
183
|
+
name,
|
|
184
|
+
roots,
|
|
185
|
+
error: `Ambiguous active Skill Recipe namespace ${name}`,
|
|
186
|
+
})),
|
|
166
187
|
drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
|
|
167
188
|
};
|
|
168
189
|
return {
|
|
@@ -199,8 +220,8 @@ function inspectRun(run, view, input, ctx, deps) {
|
|
|
199
220
|
throw new Error(`unsupported Trace source: ${source}`);
|
|
200
221
|
}
|
|
201
222
|
return {
|
|
202
|
-
run,
|
|
203
|
-
|
|
223
|
+
run, source,
|
|
224
|
+
summary: RunsTrace.summarizeRunTraceJournal(RunsTrace.readRunTraceJournal(stateDir)),
|
|
204
225
|
items: TraceProjection.projectRunTrace(stateDir, {
|
|
205
226
|
artifacts: status.artifacts,
|
|
206
227
|
limit: Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
|
|
@@ -209,19 +230,42 @@ function inspectRun(run, view, input, ctx, deps) {
|
|
|
209
230
|
};
|
|
210
231
|
}
|
|
211
232
|
const runInstanceId = String(status.run_instance_id ?? "");
|
|
233
|
+
const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
|
|
234
|
+
const capacity = computeRunControlCapacity(journal.records);
|
|
235
|
+
const structuralDiagnostics = journal.records.flatMap((value) => {
|
|
236
|
+
const classified = RuntimeTriage.classifyRuntimeControl({ run,
|
|
237
|
+
runInstanceId: runInstanceId || undefined, status: String(status.status ?? "unknown") }, value, Date.now());
|
|
238
|
+
return classified.diagnostic ? [classified.diagnostic.reason] : [];
|
|
239
|
+
});
|
|
240
|
+
let journalBytes = 0;
|
|
241
|
+
try {
|
|
242
|
+
journalBytes = statSync(RunsControls.runControlsFile(stateDir)).size;
|
|
243
|
+
}
|
|
244
|
+
catch { }
|
|
245
|
+
const diagnostics = [...journal.diagnostics.map((item) => ({
|
|
246
|
+
...(item.line === undefined ? {} : { line: item.line }),
|
|
247
|
+
reason: item.line === undefined ? "unreadable_control_journal" : "invalid_control_json",
|
|
248
|
+
})), ...structuralDiagnostics.map((reason) => ({ reason }))].slice(0, 40);
|
|
249
|
+
const nowMs = Date.now();
|
|
250
|
+
const stalePending = journal.records.reduce((count, control) => count + Number(Boolean(RuntimeTriage.classifyRuntimeControl({ run, runInstanceId: runInstanceId || undefined,
|
|
251
|
+
status: String(status.status ?? "unknown") }, control, nowMs).stale)), 0);
|
|
212
252
|
return {
|
|
213
253
|
run,
|
|
214
254
|
run_instance_id: runInstanceId,
|
|
215
255
|
status: status.status,
|
|
256
|
+
pending: capacity.pending, pending_limit: capacity.limit,
|
|
257
|
+
available: capacity.available, backpressured: capacity.backpressured,
|
|
258
|
+
journal_bytes: journalBytes, journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
|
|
259
|
+
...(capacity.oldest_pending_at ? { oldest_pending_at: capacity.oldest_pending_at } : {}),
|
|
260
|
+
stale_pending: stalePending,
|
|
261
|
+
diagnostics,
|
|
216
262
|
actor_actions: Array.isArray(status.control) ? status.control : [],
|
|
217
263
|
runtime_actions: status.status === "running" ? ["kill"] : ["archive", "prune"],
|
|
218
264
|
endpoint: runInstanceId
|
|
219
265
|
? RunControlDelivery.readRunControlEndpoint(stateDir, runInstanceId)
|
|
220
266
|
: undefined,
|
|
221
|
-
recent_controls:
|
|
222
|
-
.
|
|
223
|
-
.reverse()
|
|
224
|
-
.map(ControlProjection.projectRunControl),
|
|
267
|
+
recent_controls: journal.records.slice(-Math.max(1, Number(input.lines || 20))).reverse()
|
|
268
|
+
.map((control) => ControlProjection.projectRunControl(control)),
|
|
225
269
|
};
|
|
226
270
|
}
|
|
227
271
|
function inspectTool(name, view, deps) {
|
package/dist/lib/tools-local.js
CHANGED
|
@@ -45,11 +45,16 @@ function shouldAddRuntimeToolUsageHint(error) {
|
|
|
45
45
|
}
|
|
46
46
|
function formatRuntimeToolUsageHint(cfg, required, includeRunId) {
|
|
47
47
|
const optional = cfg.args.filter((arg) => !required.includes(arg));
|
|
48
|
+
const exampleDefaults = {
|
|
49
|
+
...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
|
|
50
|
+
...cfg.defaults,
|
|
51
|
+
...(cfg.recipe?.values ?? {}),
|
|
52
|
+
};
|
|
48
53
|
const example = {};
|
|
49
54
|
for (const arg of required)
|
|
50
|
-
example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg],
|
|
55
|
+
example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
|
|
51
56
|
for (const arg of optional)
|
|
52
|
-
example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg],
|
|
57
|
+
example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
|
|
53
58
|
if (includeRunId)
|
|
54
59
|
example.run_id = `${cfg.name}-1`;
|
|
55
60
|
const lines = [
|
|
@@ -62,6 +67,21 @@ function formatRuntimeToolUsageHint(cfg, required, includeRunId) {
|
|
|
62
67
|
lines.push(`Optional: ${[...optional, ...(includeRunId ? ["run_id"] : [])].join(", ")}`);
|
|
63
68
|
return lines.join("\n");
|
|
64
69
|
}
|
|
70
|
+
function resolveRecipeToolValues(cfg, callerValues, ctx) {
|
|
71
|
+
const recipeRuntimeValues = {
|
|
72
|
+
...(cfg.recipe?.recipe_dir
|
|
73
|
+
? { recipe_dir: cfg.recipe.recipe_dir }
|
|
74
|
+
: {}),
|
|
75
|
+
...(cfg.recipe?.skill_dir ? { skill_dir: cfg.recipe.skill_dir } : {}),
|
|
76
|
+
};
|
|
77
|
+
return Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues({
|
|
78
|
+
...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
|
|
79
|
+
...cfg.defaults,
|
|
80
|
+
...(cfg.recipe?.values ?? {}),
|
|
81
|
+
...callerValues,
|
|
82
|
+
...recipeRuntimeValues,
|
|
83
|
+
}, ctx), cfg.argTypes);
|
|
84
|
+
}
|
|
65
85
|
function formatRuntimeToolArgumentError(cfg, error, required, includeRunId) {
|
|
66
86
|
const message = error instanceof Error ? error.message : String(error);
|
|
67
87
|
if (!shouldAddRuntimeToolUsageHint(error))
|
|
@@ -87,8 +107,10 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
87
107
|
defaults: cfg.defaults,
|
|
88
108
|
template: requiredTemplate,
|
|
89
109
|
};
|
|
110
|
+
const recipeInlineDefaults = Schema.getExplicitToolArgDefaults(cfg.recipe?.args);
|
|
90
111
|
const requiredArgs = isRecipe && cfg.storedArgs !== undefined
|
|
91
|
-
? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)
|
|
112
|
+
? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg) &&
|
|
113
|
+
!Object.hasOwn(recipeInlineDefaults, arg)))
|
|
92
114
|
: RecipesReferences.isRecipeReference(cfg.template) && !recipeTemplate
|
|
93
115
|
? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
|
|
94
116
|
: Schema.getRequiredToolArgNames(requiredTemplateConfig);
|
|
@@ -137,7 +159,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
137
159
|
run_id: runId,
|
|
138
160
|
tool: cfg.name,
|
|
139
161
|
policy_values: ModelContext.withCurrentModelValues({ ...(cfg.recipe?.values ?? {}), ...values }, ctx),
|
|
140
|
-
values:
|
|
162
|
+
values: resolveRecipeToolValues(cfg, values, ctx),
|
|
141
163
|
}, ctx.cwd);
|
|
142
164
|
return {
|
|
143
165
|
content: [
|
|
@@ -150,12 +172,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
150
172
|
};
|
|
151
173
|
}
|
|
152
174
|
if (isRecipe && recipeTemplate) {
|
|
153
|
-
|
|
154
|
-
...(cfg.recipe?.values ?? {}),
|
|
155
|
-
...cfg.defaults,
|
|
156
|
-
...params,
|
|
157
|
-
}, ctx);
|
|
158
|
-
return await Execution.executeRegisteredTool({ ...cfg, template: recipeTemplate }, Schema.normalizeRuntimeValues(paramsWithDefaults, cfg.argTypes), exec, ctx.cwd, signal);
|
|
175
|
+
return await Execution.executeRegisteredTool({ ...cfg, template: recipeTemplate }, resolveRecipeToolValues(cfg, params, ctx), exec, ctx.cwd, signal);
|
|
159
176
|
}
|
|
160
177
|
return await Execution.executeRegisteredTool(cfg, Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues(params, ctx), cfg.argTypes), exec, ctx.cwd, signal);
|
|
161
178
|
}
|
package/dist/lib/tools-spawn.js
CHANGED
|
@@ -90,8 +90,8 @@ export function createSpawnToolDefinition() {
|
|
|
90
90
|
parameters: Schema.objectSchema({
|
|
91
91
|
artifacts: Schema.looseObjectSchema("Optional named artifact paths for the spawned Run."),
|
|
92
92
|
as: Schema.stringSchema("Optional Run identity in exact run:<id> form."),
|
|
93
|
-
file: Schema.stringSchema("Optional template
|
|
94
|
-
recipe: Schema.stringSchema("Alias for file;
|
|
93
|
+
file: Schema.stringSchema("Optional template Recipe reference. Supports paths, bare user/adjacent/std names, std:<name>, and skill:<skill>/<recipe>."),
|
|
94
|
+
recipe: Schema.stringSchema("Alias for file; qualified Skill/std or ordinary Recipe reference to spawn."),
|
|
95
95
|
template: Schema.unionSchema([
|
|
96
96
|
Schema.stringSchema("Inline command template string"),
|
|
97
97
|
Schema.arraySchema("Inline command-template sequence or parallel tree"),
|
|
@@ -9,11 +9,21 @@ import * as ControlProjection from "./control-projection.js";
|
|
|
9
9
|
import * as ExecutionSessions from "./execution-sessions.js";
|
|
10
10
|
import * as Limits from "./limits.js";
|
|
11
11
|
import { resolveArtifactManifest, } from "./runs-artifacts.js";
|
|
12
|
+
import * as RunsControls from "./runs-controls.js";
|
|
12
13
|
import { tailFile } from "./runs-status.js";
|
|
14
|
+
import * as RunsTrace from "./runs-trace.js";
|
|
13
15
|
import * as SessionEvidence from "./session-evidence.js";
|
|
14
|
-
import { readJsonFileResilient,
|
|
16
|
+
import { readJsonFileResilient, } from "./state-readers.js";
|
|
15
17
|
const LIFECYCLE_PREFIXES = ["run.", "command."];
|
|
16
18
|
const SOURCE_LIMIT = 100;
|
|
19
|
+
const SOURCE_RANK = {
|
|
20
|
+
lifecycle: 6,
|
|
21
|
+
control: 5,
|
|
22
|
+
process: 4,
|
|
23
|
+
agent: 3,
|
|
24
|
+
artifact: 2,
|
|
25
|
+
runtime: 1,
|
|
26
|
+
};
|
|
17
27
|
function redact(value) {
|
|
18
28
|
return SessionEvidence.redactSessionEvidenceValue(value);
|
|
19
29
|
}
|
|
@@ -22,6 +32,21 @@ function sourceForEvent(event) {
|
|
|
22
32
|
? "lifecycle"
|
|
23
33
|
: "runtime";
|
|
24
34
|
}
|
|
35
|
+
function ordered(item, sourceKey, ordinal) {
|
|
36
|
+
return { item, ordinal, sourceKey };
|
|
37
|
+
}
|
|
38
|
+
function compareTraceItems(left, right) {
|
|
39
|
+
const timestamp = right.item.ts.localeCompare(left.item.ts);
|
|
40
|
+
if (timestamp)
|
|
41
|
+
return timestamp;
|
|
42
|
+
if (left.sourceKey === right.sourceKey) {
|
|
43
|
+
const ordinal = right.ordinal - left.ordinal;
|
|
44
|
+
if (ordinal)
|
|
45
|
+
return ordinal;
|
|
46
|
+
}
|
|
47
|
+
return SOURCE_RANK[right.item.source] - SOURCE_RANK[left.item.source] ||
|
|
48
|
+
right.item.id.localeCompare(left.item.id);
|
|
49
|
+
}
|
|
25
50
|
function latestControlTimestamp(control) {
|
|
26
51
|
return (control.failed_at ??
|
|
27
52
|
control.handled_at ??
|
|
@@ -57,39 +82,57 @@ function controlDiagnosticItems(diagnostics) {
|
|
|
57
82
|
}));
|
|
58
83
|
}
|
|
59
84
|
function traceEventItems(stateDir) {
|
|
60
|
-
const read =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
const read = RunsTrace.readRunTraceJournal(stateDir);
|
|
86
|
+
const retained = read.events.slice(-SOURCE_LIMIT);
|
|
87
|
+
const projected = retained.map(({ event, ordinal }) => ordered({
|
|
88
|
+
id: event.id,
|
|
89
|
+
ts: event.ts,
|
|
90
|
+
source: sourceForEvent(event),
|
|
91
|
+
kind: event.kind,
|
|
92
|
+
summary: event.summary ?? event.kind,
|
|
93
|
+
...(event.level ? { level: event.level } : {}),
|
|
94
|
+
detail: redact({
|
|
95
|
+
...(event.attention ? { attention: event.attention } : {}),
|
|
96
|
+
...(event.data !== undefined ? { data: event.data } : {}),
|
|
97
|
+
}),
|
|
98
|
+
}, "trace", ordinal));
|
|
99
|
+
const diagnostics = diagnosticItems(read.diagnostics).map((item, index) => ordered(item, "trace", read.events.length + index + 1));
|
|
100
|
+
const hasMarker = read.events.some(({ event }) => event.kind === "runtime.trace_compacted");
|
|
101
|
+
if (read.omittedPrefixBytes > 0 && !hasMarker) {
|
|
102
|
+
diagnostics.push(ordered({
|
|
103
|
+
id: "runtime:trace-history-incomplete",
|
|
104
|
+
ts: new Date(0).toISOString(),
|
|
105
|
+
source: "runtime",
|
|
106
|
+
kind: "runtime.trace_history_incomplete",
|
|
107
|
+
summary: "Trace history is incomplete",
|
|
108
|
+
level: "warning",
|
|
109
|
+
detail: {
|
|
110
|
+
omitted_prefix_bytes: read.omittedPrefixBytes,
|
|
111
|
+
read_bytes: read.readBytes,
|
|
112
|
+
retained_events: read.events.length,
|
|
113
|
+
},
|
|
114
|
+
}, "trace", 0));
|
|
115
|
+
}
|
|
116
|
+
return [...projected, ...diagnostics];
|
|
76
117
|
}
|
|
77
118
|
function controlItems(stateDir) {
|
|
78
|
-
const read =
|
|
119
|
+
const read = RunsControls.readRunControlJournalFromStateDir(stateDir);
|
|
120
|
+
const start = Math.max(0, read.records.length - SOURCE_LIMIT);
|
|
121
|
+
const projected = read.records.slice(start).map((control, index) => {
|
|
122
|
+
const value = ControlProjection.projectRunControl(control);
|
|
123
|
+
return ordered({
|
|
124
|
+
id: `control:${value.id}`,
|
|
125
|
+
ts: latestControlTimestamp(value),
|
|
126
|
+
source: "control",
|
|
127
|
+
kind: `control.${value.status}`,
|
|
128
|
+
summary: `${value.action} ${value.status}`,
|
|
129
|
+
...(value.status === "failed" ? { level: "error" } : {}),
|
|
130
|
+
detail: value,
|
|
131
|
+
}, "control", start + index + 1);
|
|
132
|
+
});
|
|
79
133
|
return [
|
|
80
|
-
...
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
id: `control:${projected.id}`,
|
|
84
|
-
ts: latestControlTimestamp(projected),
|
|
85
|
-
source: "control",
|
|
86
|
-
kind: `control.${projected.status}`,
|
|
87
|
-
summary: `${projected.action} ${projected.status}`,
|
|
88
|
-
...(projected.status === "failed" ? { level: "error" } : {}),
|
|
89
|
-
detail: projected,
|
|
90
|
-
};
|
|
91
|
-
}),
|
|
92
|
-
...controlDiagnosticItems(read.diagnostics),
|
|
134
|
+
...projected,
|
|
135
|
+
...controlDiagnosticItems(read.diagnostics).map((item, index) => ordered(item, "control", read.records.length + index + 1)),
|
|
93
136
|
];
|
|
94
137
|
}
|
|
95
138
|
function agentItems(stateDir) {
|
|
@@ -98,7 +141,7 @@ function agentItems(stateDir) {
|
|
|
98
141
|
.map((turn) => {
|
|
99
142
|
const text = turn.assistantText ?? turn.userText ?? "Agent turn";
|
|
100
143
|
const summary = text.replaceAll(/\s+/g, " ").trim().slice(0, 160);
|
|
101
|
-
return {
|
|
144
|
+
return ordered({
|
|
102
145
|
id: `agent:${turn.commandId}:${turn.sessionFile}:${turn.index}`,
|
|
103
146
|
ts: turn.timestamp ?? new Date(0).toISOString(),
|
|
104
147
|
source: "agent",
|
|
@@ -106,7 +149,7 @@ function agentItems(stateDir) {
|
|
|
106
149
|
summary: summary || "Agent turn",
|
|
107
150
|
...(turn.error ? { level: "error" } : {}),
|
|
108
151
|
detail: redact(turn),
|
|
109
|
-
};
|
|
152
|
+
}, "agent", turn.index);
|
|
110
153
|
});
|
|
111
154
|
}
|
|
112
155
|
function processItems(stateDir) {
|
|
@@ -118,7 +161,7 @@ function processItems(stateDir) {
|
|
|
118
161
|
const tail = tailFile(path, Limits.DEFAULT_INSPECT_LINES);
|
|
119
162
|
if (!tail && stat.size === 0)
|
|
120
163
|
return [];
|
|
121
|
-
return [{
|
|
164
|
+
return [ordered({
|
|
122
165
|
id: `process:${stream}`,
|
|
123
166
|
ts: stat.mtime.toISOString(),
|
|
124
167
|
source: "process",
|
|
@@ -126,17 +169,18 @@ function processItems(stateDir) {
|
|
|
126
169
|
summary: `${stream} (${stat.size} bytes)`,
|
|
127
170
|
...(stream === "stderr" ? { level: "warning" } : {}),
|
|
128
171
|
detail: redact({ bytes: stat.size, path, tail }),
|
|
129
|
-
}];
|
|
172
|
+
}, `process:${stream}`, 1)];
|
|
130
173
|
});
|
|
131
174
|
}
|
|
132
175
|
function resultItems(stateDir) {
|
|
133
176
|
const path = join(stateDir, "result.json");
|
|
134
177
|
const read = readJsonFileResilient(path, {});
|
|
135
|
-
if (Object.keys(read.value).length === 0)
|
|
136
|
-
return diagnosticItems(read.diagnostics);
|
|
178
|
+
if (Object.keys(read.value).length === 0) {
|
|
179
|
+
return diagnosticItems(read.diagnostics).map((item, index) => ordered(item, "process:result", index));
|
|
180
|
+
}
|
|
137
181
|
const stat = statSync(path);
|
|
138
182
|
const failed = Number(read.value.code ?? 0) !== 0;
|
|
139
|
-
return [{
|
|
183
|
+
return [ordered({
|
|
140
184
|
id: "process:result",
|
|
141
185
|
ts: typeof read.value.completedAt === "string"
|
|
142
186
|
? read.value.completedAt
|
|
@@ -146,13 +190,13 @@ function resultItems(stateDir) {
|
|
|
146
190
|
summary: failed ? "Run process failed" : "Run process completed",
|
|
147
191
|
...(failed ? { level: "error" } : {}),
|
|
148
192
|
detail: redact(read.value),
|
|
149
|
-
}, ...diagnosticItems(read.diagnostics)];
|
|
193
|
+
}, "process:result", 1), ...diagnosticItems(read.diagnostics).map((item, index) => ordered(item, "process:result", index + 2))];
|
|
150
194
|
}
|
|
151
195
|
function artifactItems(stateDir, declarations) {
|
|
152
196
|
const manifest = resolveArtifactManifest(declarations);
|
|
153
197
|
if (!manifest)
|
|
154
198
|
return [];
|
|
155
|
-
return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(([name, artifact]) => ({
|
|
199
|
+
return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(([name, artifact], index) => ordered({
|
|
156
200
|
id: `artifact:${name}`,
|
|
157
201
|
ts: artifact.exists && existsSync(artifact.path)
|
|
158
202
|
? statSync(artifact.path).mtime.toISOString()
|
|
@@ -167,7 +211,7 @@ function artifactItems(stateDir, declarations) {
|
|
|
167
211
|
required: artifact.required,
|
|
168
212
|
...(artifact.size !== undefined ? { size: artifact.size } : {}),
|
|
169
213
|
}),
|
|
170
|
-
}));
|
|
214
|
+
}, "artifact", index + 1));
|
|
171
215
|
}
|
|
172
216
|
export function projectRunTrace(stateDir, options = {}) {
|
|
173
217
|
const source = options.source ?? "all";
|
|
@@ -179,7 +223,9 @@ export function projectRunTrace(stateDir, options = {}) {
|
|
|
179
223
|
...resultItems(stateDir),
|
|
180
224
|
...artifactItems(stateDir, options.artifacts),
|
|
181
225
|
]
|
|
182
|
-
.filter((item) => source === "all" || item.source === source)
|
|
183
|
-
.sort(
|
|
184
|
-
return items
|
|
226
|
+
.filter(({ item }) => source === "all" || item.source === source)
|
|
227
|
+
.sort(compareTraceItems);
|
|
228
|
+
return items
|
|
229
|
+
.slice(0, Math.max(1, Math.min(options.limit ?? 100, 200)))
|
|
230
|
+
.map(({ item }) => item);
|
|
185
231
|
}
|
|
@@ -15,6 +15,11 @@ const conformanceSuites = [
|
|
|
15
15
|
"tests/control.test.ts",
|
|
16
16
|
"tests/runs-controls.test.ts",
|
|
17
17
|
"tests/runs-trace.test.ts",
|
|
18
|
+
"tests/observability.test.ts",
|
|
19
|
+
"tests/async-run-control.test.ts",
|
|
20
|
+
"tests/run-kernel-dogfood.test.ts",
|
|
21
|
+
"tests/installed.scripts.test.ts",
|
|
22
|
+
"tests/inspector-overlay.test.ts",
|
|
18
23
|
"tests/tools-inspect-kernel.test.ts",
|
|
19
24
|
"tests/recipes-discovery.test.ts",
|
|
20
25
|
"tests/review-swarm-dogfood.test.ts",
|
package/dist/scripts/locker.mjs
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import {
|
|
14
|
-
appendFileSync,
|
|
15
14
|
existsSync,
|
|
16
15
|
mkdirSync,
|
|
17
16
|
readFileSync,
|
|
@@ -32,8 +31,12 @@ async function importRuntimeModule(name) {
|
|
|
32
31
|
return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
|
|
36
|
-
|
|
34
|
+
const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } = await importRuntimeModule("file-state");
|
|
35
|
+
const { readJsonlFileResilient } = await importRuntimeModule("state-readers");
|
|
36
|
+
const LOCKER_JOURNAL_MAX_RECORDS = 512;
|
|
37
|
+
const LOCKER_JOURNAL_MAX_BYTES = 1024 * 1024;
|
|
38
|
+
const { claimRunControlByIdInStateDir, updateRunControlStatusInStateDir } =
|
|
39
|
+
await importRuntimeModule("runs-controls");
|
|
37
40
|
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
38
41
|
|
|
39
42
|
function parseArgs(argv) {
|
|
@@ -59,7 +62,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
59
62
|
const queuePath = join(stateDir, "queue.json");
|
|
60
63
|
const locksPath = join(stateDir, "locks.json");
|
|
61
64
|
const journalPath = join(stateDir, "journal.jsonl");
|
|
62
|
-
const controlsPath = join(stateDir, "controls.jsonl");
|
|
63
65
|
const controlPath = join(stateDir, "control.fifo");
|
|
64
66
|
let runInstanceId;
|
|
65
67
|
mkdirSync(stateDir, { recursive: true });
|
|
@@ -96,10 +98,22 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
function journal(event, data = {}) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const release = acquireFileMutationLock(journalPath);
|
|
102
|
+
try {
|
|
103
|
+
const records = [...readJsonlFileResilient(journalPath).records,
|
|
104
|
+
{ event, ts: new Date().toISOString(), ...data }]
|
|
105
|
+
.slice(-LOCKER_JOURNAL_MAX_RECORDS);
|
|
106
|
+
let content = encodeJournal(records);
|
|
107
|
+
while (records.length && Buffer.byteLength(content) > LOCKER_JOURNAL_MAX_BYTES) {
|
|
108
|
+
records.shift();
|
|
109
|
+
content = encodeJournal(records);
|
|
110
|
+
}
|
|
111
|
+
writeTextAtomic(journalPath, content);
|
|
112
|
+
} finally { release(); }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function encodeJournal(records) {
|
|
116
|
+
return records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "";
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
function emitTrace(kind, summary, data = {}, level = "info") {
|
|
@@ -147,69 +161,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
147
161
|
}
|
|
148
162
|
}
|
|
149
163
|
|
|
150
|
-
function acquireControlsLock() {
|
|
151
|
-
return acquireFileMutationLock(controlsPath);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function readControls() {
|
|
155
|
-
if (!existsSync(controlsPath)) return [];
|
|
156
|
-
return readFileSync(controlsPath, "utf8")
|
|
157
|
-
.split("\n")
|
|
158
|
-
.filter((line) => line.trim())
|
|
159
|
-
.map((line) => {
|
|
160
|
-
try {
|
|
161
|
-
return JSON.parse(line);
|
|
162
|
-
} catch {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
.filter(Boolean);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function writeControls(controls) {
|
|
170
|
-
writeTextAtomic(
|
|
171
|
-
controlsPath,
|
|
172
|
-
controls.length
|
|
173
|
-
? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
|
|
174
|
-
: "",
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function claimControl(id) {
|
|
179
|
-
const release = acquireControlsLock();
|
|
180
|
-
try {
|
|
181
|
-
const controls = readControls();
|
|
182
|
-
const control = controls.find((item) => item.id === id);
|
|
183
|
-
if (
|
|
184
|
-
!control ||
|
|
185
|
-
control.run_instance_id !== runInstanceId ||
|
|
186
|
-
(control.status !== "queued" && control.status !== "delivered")
|
|
187
|
-
) {
|
|
188
|
-
return undefined;
|
|
189
|
-
}
|
|
190
|
-
control.claimed_at = new Date().toISOString();
|
|
191
|
-
control.status = "claimed";
|
|
192
|
-
writeControls(controls);
|
|
193
|
-
return { ...control };
|
|
194
|
-
} finally {
|
|
195
|
-
release();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function finalizeControl(id, status, error) {
|
|
200
|
-
const release = acquireControlsLock();
|
|
201
|
-
try {
|
|
202
|
-
const controls = readControls();
|
|
203
|
-
const control = controls.find((item) => item.id === id);
|
|
204
|
-
if (!control || control.status !== "claimed") return;
|
|
205
|
-
control.status = status;
|
|
206
|
-
control[`${status}_at`] = new Date().toISOString();
|
|
207
|
-
if (error) control.error = error;
|
|
208
|
-
writeControls(controls);
|
|
209
|
-
} finally {
|
|
210
|
-
release();
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
164
|
|
|
214
165
|
function tailJournal(count) {
|
|
215
166
|
if (!existsSync(journalPath)) return [];
|
|
@@ -397,7 +348,9 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
397
348
|
function handleLine(line) {
|
|
398
349
|
const control = normalizeControl(line);
|
|
399
350
|
if (!control) return false;
|
|
400
|
-
const claimed =
|
|
351
|
+
const claimed = claimRunControlByIdInStateDir(
|
|
352
|
+
stateDir, runInstanceId, control.id,
|
|
353
|
+
);
|
|
401
354
|
if (!claimed) {
|
|
402
355
|
emitTrace(
|
|
403
356
|
"lock.control_rejected",
|
|
@@ -409,11 +362,15 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
409
362
|
}
|
|
410
363
|
try {
|
|
411
364
|
const stopping = handle(claimed) === true;
|
|
412
|
-
|
|
365
|
+
updateRunControlStatusInStateDir(
|
|
366
|
+
stateDir, claimed.id, "handled", {}, ["claimed"],
|
|
367
|
+
);
|
|
413
368
|
return stopping;
|
|
414
369
|
} catch (error) {
|
|
415
370
|
const text = error instanceof Error ? error.message : String(error);
|
|
416
|
-
|
|
371
|
+
updateRunControlStatusInStateDir(
|
|
372
|
+
stateDir, claimed.id, "failed", { error: text }, ["claimed"],
|
|
373
|
+
);
|
|
417
374
|
journal("lock.error", { error: text });
|
|
418
375
|
emitTrace("lock.error", text, { error: text }, "error");
|
|
419
376
|
return false;
|