@llblab/pi-actors 0.22.5 → 0.24.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 +125 -59
- package/BACKLOG.md +88 -403
- package/CHANGELOG.md +39 -0
- package/README.md +14 -1
- package/dist/fixtures/protocol/actor-message-branch.json +13 -0
- package/dist/fixtures/protocol/artifact-manifest.json +9 -0
- package/dist/fixtures/protocol/mailbox-contract.json +15 -0
- package/dist/fixtures/protocol/recipe-summary.json +16 -0
- package/dist/fixtures/protocol/room-message.json +11 -0
- package/dist/fixtures/protocol/room-roster.json +11 -0
- package/dist/fixtures/protocol/run-inbox-message.json +9 -0
- package/dist/fixtures/protocol/run-outbox-event.json +9 -0
- package/dist/fixtures/protocol/run-state.json +10 -0
- package/dist/index.js +18 -2
- package/dist/lib/actor-inspector-tui.d.ts +4 -0
- package/dist/lib/actor-inspector-tui.js +58 -44
- package/dist/lib/actor-rooms.d.ts +16 -0
- package/dist/lib/actor-rooms.js +123 -28
- package/dist/lib/actor-worker.d.ts +13 -0
- package/dist/lib/actor-worker.js +86 -0
- package/dist/lib/async-runner.d.ts +5 -0
- package/dist/lib/async-runner.js +134 -0
- package/dist/lib/async-runs.d.ts +34 -2
- package/dist/lib/async-runs.js +224 -50
- package/dist/lib/build-dist.d.ts +5 -0
- package/dist/lib/build-dist.js +24 -0
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/conformance.d.ts +12 -0
- package/dist/lib/conformance.js +28 -0
- package/dist/lib/coordinator.d.ts +5 -0
- package/dist/lib/coordinator.js +557 -0
- package/dist/lib/limits.d.ts +11 -0
- package/dist/lib/limits.js +11 -0
- package/dist/lib/locker.d.ts +5 -0
- package/dist/lib/locker.js +310 -0
- package/dist/lib/mailbox-loop.d.ts +41 -0
- package/dist/lib/mailbox-loop.js +62 -0
- package/dist/lib/observability.d.ts +2 -2
- package/dist/lib/observability.js +57 -57
- package/dist/lib/output.js +4 -5
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-discovery.js +1 -0
- package/dist/lib/recipe-references.d.ts +11 -2
- package/dist/lib/recipe-references.js +29 -4
- package/dist/lib/recipe-usage.d.ts +2 -1
- package/dist/lib/recipe-usage.js +15 -3
- package/dist/lib/recipe-utils.d.ts +5 -0
- package/dist/lib/recipe-utils.js +385 -0
- package/dist/lib/runtime-notifier.js +3 -7
- package/dist/lib/runtime.js +12 -2
- package/dist/lib/state-readers.d.ts +21 -0
- package/dist/lib/state-readers.js +74 -0
- package/dist/lib/tools.js +198 -39
- package/dist/lib/validate-recipe.d.ts +6 -0
- package/dist/lib/validate-recipe.js +104 -0
- package/dist/recipes/actor-worker.json +35 -0
- package/dist/recipes/coordinator-locker.json +45 -0
- package/dist/recipes/lens-swarm.json +66 -0
- package/dist/recipes/locker.json +45 -0
- package/dist/recipes/music-player.json +38 -0
- package/dist/recipes/pipeline-architect-coordinator.json +95 -0
- package/dist/recipes/pipeline-artifact-bundle.json +100 -0
- package/dist/recipes/pipeline-artifact-report.json +58 -0
- package/dist/recipes/pipeline-artifact-write.json +72 -0
- package/dist/recipes/pipeline-async-run-ops.json +70 -0
- package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
- package/dist/recipes/pipeline-development-tasking.json +81 -0
- package/dist/recipes/pipeline-docs-maintenance.json +80 -0
- package/dist/recipes/pipeline-media-library.json +59 -0
- package/dist/recipes/pipeline-quorum-review.json +79 -0
- package/dist/recipes/pipeline-release-readiness.json +110 -0
- package/dist/recipes/pipeline-release-summary.json +88 -0
- package/dist/recipes/pipeline-repo-health.json +89 -0
- package/dist/recipes/pipeline-research-synthesis.json +94 -0
- package/dist/recipes/pipeline-review-readiness.json +54 -0
- package/dist/recipes/pipeline-room-swarm.json +50 -0
- package/dist/recipes/subagent-artifact.json +32 -0
- package/dist/recipes/subagent-checkpoint.json +33 -0
- package/dist/recipes/subagent-conflict-report.json +32 -0
- package/dist/recipes/subagent-contradiction-map.json +33 -0
- package/dist/recipes/subagent-critic.json +35 -0
- package/dist/recipes/subagent-evidence-map.json +33 -0
- package/dist/recipes/subagent-followup.json +33 -0
- package/dist/recipes/subagent-judge.json +33 -0
- package/dist/recipes/subagent-merge.json +33 -0
- package/dist/recipes/subagent-message.json +34 -0
- package/dist/recipes/subagent-normalize.json +31 -0
- package/dist/recipes/subagent-plan.json +33 -0
- package/dist/recipes/subagent-prompt.json +28 -0
- package/dist/recipes/subagent-quorum.json +43 -0
- package/dist/recipes/subagent-review-coordinator.json +114 -0
- package/dist/recipes/subagent-review.json +37 -0
- package/dist/recipes/subagent-task-card.json +35 -0
- package/dist/recipes/subagent-tools.json +27 -0
- package/dist/recipes/subagent-verify.json +34 -0
- package/dist/recipes/subagents-prompts.json +51 -0
- package/dist/recipes/utility-actor-message.json +23 -0
- package/dist/recipes/utility-artifact-manifest.json +16 -0
- package/dist/recipes/utility-artifact-write.json +16 -0
- package/dist/recipes/utility-changelog-head.json +11 -0
- package/dist/recipes/utility-changelog-section.json +13 -0
- package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
- package/dist/recipes/utility-git-log.json +11 -0
- package/dist/recipes/utility-git-status.json +9 -0
- package/dist/recipes/utility-jsonl-tail.json +10 -0
- package/dist/recipes/utility-markdown-index.json +14 -0
- package/dist/recipes/utility-package-summary.json +11 -0
- package/dist/recipes/utility-playlist-build.json +17 -0
- package/dist/recipes/utility-playlist-scan.json +11 -0
- package/dist/recipes/utility-run-ops-snapshot.json +17 -0
- package/dist/recipes/utility-run-state-files.json +13 -0
- package/dist/recipes/utility-run-summary.json +11 -0
- package/dist/recipes/utility-skill-summary.json +13 -0
- package/dist/recipes/utility-validate-recipe.json +13 -0
- package/dist/recipes/utility-validation-wrapper.json +13 -0
- package/dist/scripts/actor-worker.mjs +31 -0
- package/dist/scripts/async-runner.mjs +31 -0
- package/dist/scripts/build-dist.mjs +25 -0
- package/dist/scripts/conformance.mjs +33 -0
- package/dist/scripts/coordinator.mjs +31 -0
- package/dist/scripts/locker.mjs +33 -0
- package/dist/scripts/music-player.mjs +964 -0
- package/dist/scripts/recipe-utils.mjs +31 -0
- package/dist/scripts/validate-recipe.mjs +34 -0
- package/dist/skills/actors/SKILL.md +375 -0
- package/dist/skills/swarm/SKILL.md +467 -0
- package/dist/skills/swarm/references/development-swarm.md +596 -0
- package/docs/actor-messages.md +2 -2
- package/docs/async-runs.md +13 -1
- package/docs/template-recipes.md +2 -2
- package/docs/tool-registry.md +0 -1
- package/fixtures/protocol/actor-message-branch.json +13 -0
- package/fixtures/protocol/artifact-manifest.json +9 -0
- package/fixtures/protocol/mailbox-contract.json +15 -0
- package/fixtures/protocol/recipe-summary.json +16 -0
- package/fixtures/protocol/room-message.json +11 -0
- package/fixtures/protocol/room-roster.json +11 -0
- package/fixtures/protocol/run-inbox-message.json +9 -0
- package/fixtures/protocol/run-outbox-event.json +9 -0
- package/fixtures/protocol/run-state.json +10 -0
- package/index.ts +21 -0
- package/lib/actor-inspector-tui.ts +138 -59
- package/lib/actor-rooms.ts +241 -60
- package/lib/actor-worker.ts +118 -0
- package/lib/async-runner.ts +173 -0
- package/lib/async-runs.ts +302 -53
- package/lib/build-dist.ts +30 -0
- package/lib/command-templates.ts +5 -5
- package/lib/conformance.ts +46 -0
- package/lib/coordinator.ts +649 -0
- package/lib/limits.ts +12 -0
- package/lib/locker.ts +340 -0
- package/lib/mailbox-loop.ts +148 -0
- package/lib/observability.ts +34 -23
- package/lib/output.ts +4 -6
- package/lib/prompts.ts +1 -1
- package/lib/recipe-discovery.ts +1 -0
- package/lib/recipe-references.ts +57 -6
- package/lib/recipe-usage.ts +31 -4
- package/lib/recipe-utils.ts +486 -0
- package/lib/runtime-notifier.ts +4 -6
- package/lib/runtime.ts +31 -7
- package/lib/state-readers.ts +93 -0
- package/lib/tools.ts +297 -58
- package/lib/validate-recipe.ts +110 -0
- package/package.json +11 -2
- package/recipes/actor-worker.json +35 -0
- package/recipes/pipeline-quorum-review.json +12 -7
- package/scripts/actor-worker.mjs +31 -0
- package/scripts/async-runner.mjs +11 -195
- package/scripts/build-dist.mjs +25 -0
- package/scripts/conformance.mjs +33 -0
- package/scripts/coordinator.mjs +19 -616
- package/scripts/locker.mjs +23 -322
- package/scripts/music-player.mjs +21 -2
- package/scripts/recipe-utils.mjs +20 -467
- package/scripts/validate-recipe.mjs +23 -113
- package/skills/actors/SKILL.md +7 -4
- package/skills/swarm/SKILL.md +3 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detached async-runner entrypoint logic.
|
|
3
|
+
* Zones: async run process, command-template execution telemetry
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { appendRecipeContextToPiArgs } from "./actor-recipe-context.ts";
|
|
10
|
+
import { execCommandTemplate } from "./command-templates.ts";
|
|
11
|
+
import { executeRegisteredTool, type ToolExecOptions } from "./execution.ts";
|
|
12
|
+
import { writeJsonAtomic } from "./file-state.ts";
|
|
13
|
+
|
|
14
|
+
function quoteCommandDetailPart(value: string): string {
|
|
15
|
+
if (value === "") return "''";
|
|
16
|
+
if (/^[A-Za-z0-9_/:=.,@%+\-]+$/.test(value)) return value;
|
|
17
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatCommandDetail(command: string, args: string[]): string {
|
|
21
|
+
return [command, ...args].map(quoteCommandDetailPart).join(" ");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function summarizeCommandDetail(commandDetail: string): string {
|
|
25
|
+
return commandDetail.length > 160
|
|
26
|
+
? `${commandDetail.slice(0, 157)}...`
|
|
27
|
+
: commandDetail;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runAsyncRunner(stateDir = process.argv[2]): Promise<void> {
|
|
31
|
+
if (!stateDir) throw new Error("missing state dir");
|
|
32
|
+
|
|
33
|
+
const runPath = join(stateDir, "run.json");
|
|
34
|
+
const progressPath = join(stateDir, "progress.json");
|
|
35
|
+
const resultPath = join(stateDir, "result.json");
|
|
36
|
+
const eventsPath = join(stateDir, "events.jsonl");
|
|
37
|
+
const outboxPath = join(stateDir, "outbox.jsonl");
|
|
38
|
+
const stdoutPath = join(stateDir, "stdout.log");
|
|
39
|
+
const stderrPath = join(stateDir, "stderr.log");
|
|
40
|
+
const meta = JSON.parse(readFileSync(runPath, "utf8"));
|
|
41
|
+
|
|
42
|
+
function event(name: string, data: Record<string, unknown> = {}): void {
|
|
43
|
+
appendFileSync(
|
|
44
|
+
eventsPath,
|
|
45
|
+
`${JSON.stringify({ event: name, ts: new Date().toISOString(), ...data })}\n`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function outbox(
|
|
50
|
+
name: string,
|
|
51
|
+
summary: string,
|
|
52
|
+
data: Record<string, unknown> = {},
|
|
53
|
+
delivery = "log",
|
|
54
|
+
level = "info",
|
|
55
|
+
): void {
|
|
56
|
+
appendFileSync(
|
|
57
|
+
outboxPath,
|
|
58
|
+
`${JSON.stringify({ body: data, data, delivery, event: name, from: `run:${meta.run}`, level, summary, to: "coordinator", ts: new Date().toISOString(), type: name })}\n`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function progress(phase: string, extra: Record<string, unknown> = {}): void {
|
|
63
|
+
writeJsonAtomic(progressPath, {
|
|
64
|
+
phase,
|
|
65
|
+
updatedAt: new Date().toISOString(),
|
|
66
|
+
...extra,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let activeSubagents = 0;
|
|
71
|
+
let completedSubagents = 0;
|
|
72
|
+
const subagentFailures: Record<string, unknown>[] = [];
|
|
73
|
+
|
|
74
|
+
function getCommandDoneDelivery(result: { code: number }): string {
|
|
75
|
+
return result.code !== 0 || activeSubagents > 0 ? "followup" : "log";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function progressRunning(): void {
|
|
79
|
+
progress("running", {
|
|
80
|
+
activeSubagents,
|
|
81
|
+
completed: completedSubagents,
|
|
82
|
+
failures: subagentFailures,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function observedExec(command: string, args: string[], options?: ToolExecOptions) {
|
|
87
|
+
const commandDetail = formatCommandDetail(command, args);
|
|
88
|
+
const execArgs = appendRecipeContextToPiArgs(
|
|
89
|
+
command,
|
|
90
|
+
args,
|
|
91
|
+
meta.recipe_context_records,
|
|
92
|
+
options?.actorRecipeContext,
|
|
93
|
+
);
|
|
94
|
+
activeSubagents += 1;
|
|
95
|
+
event("command.start", { activeSubagents, command: commandDetail });
|
|
96
|
+
progressRunning();
|
|
97
|
+
const result = await execCommandTemplate(command, execArgs, options);
|
|
98
|
+
activeSubagents = Math.max(0, activeSubagents - 1);
|
|
99
|
+
completedSubagents += 1;
|
|
100
|
+
if (result.code !== 0) {
|
|
101
|
+
subagentFailures.push({
|
|
102
|
+
code: result.code,
|
|
103
|
+
command: commandDetail,
|
|
104
|
+
killed: result.killed,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
event("command.done", {
|
|
108
|
+
activeSubagents,
|
|
109
|
+
code: result.code,
|
|
110
|
+
command: commandDetail,
|
|
111
|
+
killed: result.killed,
|
|
112
|
+
});
|
|
113
|
+
outbox(
|
|
114
|
+
"command.done",
|
|
115
|
+
`Command ${summarizeCommandDetail(commandDetail)} completed with code ${result.code}`,
|
|
116
|
+
{
|
|
117
|
+
activeSubagents,
|
|
118
|
+
...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
|
|
119
|
+
run_files: [stdoutPath, stderrPath, resultPath, eventsPath, outboxPath],
|
|
120
|
+
code: result.code,
|
|
121
|
+
command: commandDetail,
|
|
122
|
+
killed: result.killed,
|
|
123
|
+
},
|
|
124
|
+
getCommandDoneDelivery(result),
|
|
125
|
+
result.code === 0 ? "info" : "error",
|
|
126
|
+
);
|
|
127
|
+
progressRunning();
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
event("run.runner.start", { pid: process.pid });
|
|
133
|
+
progressRunning();
|
|
134
|
+
const result = await executeRegisteredTool(
|
|
135
|
+
{
|
|
136
|
+
name: "run_actor",
|
|
137
|
+
description: "Detached command-template run actor",
|
|
138
|
+
template: meta.template,
|
|
139
|
+
args: [],
|
|
140
|
+
defaults: {},
|
|
141
|
+
},
|
|
142
|
+
meta.values || {},
|
|
143
|
+
observedExec,
|
|
144
|
+
meta.cwd,
|
|
145
|
+
);
|
|
146
|
+
const text = result.content?.[0]?.text || "";
|
|
147
|
+
appendFileSync(stdoutPath, text);
|
|
148
|
+
writeJsonAtomic(resultPath, {
|
|
149
|
+
code: result.details.code,
|
|
150
|
+
command: result.details.command,
|
|
151
|
+
killed: result.details.killed,
|
|
152
|
+
truncated: result.details.truncated,
|
|
153
|
+
completedAt: new Date().toISOString(),
|
|
154
|
+
});
|
|
155
|
+
progress("done", {
|
|
156
|
+
completed: 1,
|
|
157
|
+
failures: result.details.nonCriticalFailures || [],
|
|
158
|
+
});
|
|
159
|
+
event("run.done", { code: result.details.code });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
appendFileSync(stderrPath, `${message}\n`);
|
|
163
|
+
writeJsonAtomic(resultPath, {
|
|
164
|
+
code: 1,
|
|
165
|
+
error: message,
|
|
166
|
+
killed: false,
|
|
167
|
+
completedAt: new Date().toISOString(),
|
|
168
|
+
});
|
|
169
|
+
progress("failed", { completed: 0, failures: [{ message }] });
|
|
170
|
+
event("run.failed", { error: message });
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
package/lib/async-runs.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
9
9
|
import {
|
|
10
10
|
closeSync,
|
|
11
11
|
constants,
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
openSync,
|
|
15
15
|
readdirSync,
|
|
16
16
|
readFileSync,
|
|
17
|
+
cpSync,
|
|
17
18
|
readlinkSync,
|
|
19
|
+
renameSync,
|
|
18
20
|
rmSync,
|
|
19
21
|
statSync,
|
|
20
22
|
writeFileSync,
|
|
@@ -31,6 +33,7 @@ import type {
|
|
|
31
33
|
} from "./command-templates.ts";
|
|
32
34
|
import { substituteCommandTemplateToken } from "./command-templates.ts";
|
|
33
35
|
import { writeJsonAtomic } from "./file-state.ts";
|
|
36
|
+
import { readJsonFileResilient, readJsonlFileResilient } from "./state-readers.ts";
|
|
34
37
|
import * as Paths from "./paths.ts";
|
|
35
38
|
import * as RecipeReferences from "./recipe-references.ts";
|
|
36
39
|
import * as RecipeUsage from "./recipe-usage.ts";
|
|
@@ -65,7 +68,7 @@ export interface AsyncRunStartParams {
|
|
|
65
68
|
timeout?: number | string;
|
|
66
69
|
delay?: number | string;
|
|
67
70
|
output?: string;
|
|
68
|
-
artifacts?: Record<string,
|
|
71
|
+
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
69
72
|
mailbox?: RecipeReferences.TemplateRecipeMailbox;
|
|
70
73
|
retire_when?: "children_terminal";
|
|
71
74
|
retry?: number | string;
|
|
@@ -121,7 +124,7 @@ export interface AsyncRunMeta {
|
|
|
121
124
|
tool?: string;
|
|
122
125
|
template: CommandTemplateValue;
|
|
123
126
|
values: Record<string, unknown>;
|
|
124
|
-
artifacts?: Record<string,
|
|
127
|
+
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
125
128
|
control?: AsyncRunControlEndpoint;
|
|
126
129
|
mailbox?: RecipeReferences.TemplateRecipeMailbox;
|
|
127
130
|
recipe_context_records?: RecipeReferences.TemplateRecipeContextRecord[];
|
|
@@ -160,23 +163,93 @@ function safeRunId(value: string | undefined): string {
|
|
|
160
163
|
return run;
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
export type RunArtifactDeclaration =
|
|
167
|
+
| string
|
|
168
|
+
| { path: string; kind?: string; media_type?: string; required?: boolean };
|
|
169
|
+
|
|
170
|
+
export interface RunArtifactManifestEntry {
|
|
171
|
+
exists: boolean;
|
|
172
|
+
kind?: string;
|
|
173
|
+
media_type?: string;
|
|
174
|
+
path: string;
|
|
175
|
+
required?: boolean;
|
|
176
|
+
sha256?: string;
|
|
177
|
+
size?: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
163
180
|
function resolveArtifactPaths(
|
|
164
|
-
artifacts: Record<string,
|
|
181
|
+
artifacts: Record<string, RunArtifactDeclaration> | undefined,
|
|
165
182
|
values: Record<string, unknown>,
|
|
166
|
-
): Record<string,
|
|
183
|
+
): Record<string, RunArtifactDeclaration> | undefined {
|
|
167
184
|
if (!artifacts) return undefined;
|
|
168
|
-
const resolved: Record<string,
|
|
185
|
+
const resolved: Record<string, RunArtifactDeclaration> = {};
|
|
169
186
|
for (const [key, value] of Object.entries(artifacts)) {
|
|
170
187
|
if (!key.trim()) continue;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
if (typeof value === "string") {
|
|
189
|
+
resolved[key] = substituteCommandTemplateToken(
|
|
190
|
+
value,
|
|
191
|
+
values,
|
|
192
|
+
`recipe artifacts.${key}`,
|
|
193
|
+
);
|
|
194
|
+
} else if (
|
|
195
|
+
value &&
|
|
196
|
+
typeof value === "object" &&
|
|
197
|
+
typeof value.path === "string"
|
|
198
|
+
) {
|
|
199
|
+
resolved[key] = {
|
|
200
|
+
...value,
|
|
201
|
+
path: substituteCommandTemplateToken(
|
|
202
|
+
value.path,
|
|
203
|
+
values,
|
|
204
|
+
`recipe artifacts.${key}.path`,
|
|
205
|
+
),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
176
208
|
}
|
|
177
209
|
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
178
210
|
}
|
|
179
211
|
|
|
212
|
+
export function resolveArtifactManifest(
|
|
213
|
+
artifacts: Record<string, RunArtifactDeclaration> | undefined,
|
|
214
|
+
): Record<string, RunArtifactManifestEntry> | undefined {
|
|
215
|
+
if (!artifacts) return undefined;
|
|
216
|
+
const manifest: Record<string, RunArtifactManifestEntry> = {};
|
|
217
|
+
for (const [name, artifact] of Object.entries(artifacts)) {
|
|
218
|
+
const declaration =
|
|
219
|
+
typeof artifact === "string" ? { path: artifact } : artifact;
|
|
220
|
+
if (!declaration?.path) continue;
|
|
221
|
+
try {
|
|
222
|
+
const content = readFileSync(declaration.path);
|
|
223
|
+
manifest[name] = {
|
|
224
|
+
exists: true,
|
|
225
|
+
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
226
|
+
...(declaration.media_type
|
|
227
|
+
? { media_type: declaration.media_type }
|
|
228
|
+
: {}),
|
|
229
|
+
path: declaration.path,
|
|
230
|
+
...(declaration.required !== undefined
|
|
231
|
+
? { required: declaration.required }
|
|
232
|
+
: {}),
|
|
233
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
234
|
+
size: content.byteLength,
|
|
235
|
+
};
|
|
236
|
+
} catch {
|
|
237
|
+
manifest[name] = {
|
|
238
|
+
exists: false,
|
|
239
|
+
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
240
|
+
...(declaration.media_type
|
|
241
|
+
? { media_type: declaration.media_type }
|
|
242
|
+
: {}),
|
|
243
|
+
path: declaration.path,
|
|
244
|
+
...(declaration.required !== undefined
|
|
245
|
+
? { required: declaration.required }
|
|
246
|
+
: {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return Object.keys(manifest).length > 0 ? manifest : undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
180
253
|
function resolveRunTemplate(params: AsyncRunStartParams): {
|
|
181
254
|
template: CommandTemplateValue;
|
|
182
255
|
} {
|
|
@@ -273,11 +346,10 @@ function resolveStartParams(params: AsyncRunStartParams): AsyncRunStartParams {
|
|
|
273
346
|
}
|
|
274
347
|
|
|
275
348
|
function readJson(path: string): Record<string, unknown> | undefined {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
349
|
+
return readJsonFileResilient<Record<string, unknown> | undefined>(
|
|
350
|
+
path,
|
|
351
|
+
undefined,
|
|
352
|
+
).value;
|
|
281
353
|
}
|
|
282
354
|
|
|
283
355
|
function isAlive(pid: number): boolean {
|
|
@@ -320,16 +392,12 @@ function tailLines(path: string, lines: number): string[] {
|
|
|
320
392
|
function getInterruptedRunStatus(
|
|
321
393
|
stateDir: string,
|
|
322
394
|
): "cancelled" | "killed" | undefined {
|
|
323
|
-
const events =
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (event.event === "run.cancel") return "cancelled";
|
|
330
|
-
} catch {
|
|
331
|
-
// Ignore malformed event lines.
|
|
332
|
-
}
|
|
395
|
+
const events = readJsonlFileResilient<Record<string, unknown>>(
|
|
396
|
+
join(stateDir, "events.jsonl"),
|
|
397
|
+
).records.slice(-200);
|
|
398
|
+
for (const event of events.reverse()) {
|
|
399
|
+
if (event.event === "run.kill") return "killed";
|
|
400
|
+
if (event.event === "run.cancel") return "cancelled";
|
|
333
401
|
}
|
|
334
402
|
return undefined;
|
|
335
403
|
}
|
|
@@ -426,7 +494,11 @@ export function startRun(
|
|
|
426
494
|
? RecipeReferences.buildRecipeContextRecords(recipeFile)
|
|
427
495
|
: undefined;
|
|
428
496
|
if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
|
|
429
|
-
RecipeUsage.recordRecipeLaunch(
|
|
497
|
+
RecipeUsage.recordRecipeLaunch(
|
|
498
|
+
recipeFile,
|
|
499
|
+
new Date(),
|
|
500
|
+
startParams.launch_source === "tool" ? "tool" : "spawn",
|
|
501
|
+
);
|
|
430
502
|
}
|
|
431
503
|
const outFd = openSync(stdout, "a");
|
|
432
504
|
const errFd = openSync(stderr, "a");
|
|
@@ -611,6 +683,16 @@ export function getRunStatus(runOrDir: string): Record<string, unknown> {
|
|
|
611
683
|
};
|
|
612
684
|
}
|
|
613
685
|
|
|
686
|
+
export interface RunStateIndexEntry {
|
|
687
|
+
ownerId?: string;
|
|
688
|
+
recipe?: string;
|
|
689
|
+
run: string;
|
|
690
|
+
state_dir: string;
|
|
691
|
+
status: AsyncRunStatus;
|
|
692
|
+
tool?: string;
|
|
693
|
+
updated_at?: string;
|
|
694
|
+
}
|
|
695
|
+
|
|
614
696
|
function matchesStatusFilter(
|
|
615
697
|
status: unknown,
|
|
616
698
|
filter: string | undefined,
|
|
@@ -621,30 +703,125 @@ function matchesStatusFilter(
|
|
|
621
703
|
return status === filter;
|
|
622
704
|
}
|
|
623
705
|
|
|
706
|
+
export function listRunStateDirs(
|
|
707
|
+
stateRoot = DEFAULT_STATE_ROOT,
|
|
708
|
+
depth = 0,
|
|
709
|
+
seen = new Set<string>(),
|
|
710
|
+
): string[] {
|
|
711
|
+
if (!existsSync(stateRoot) || seen.has(resolve(stateRoot))) return [];
|
|
712
|
+
seen.add(resolve(stateRoot));
|
|
713
|
+
let entries: import("node:fs").Dirent[];
|
|
714
|
+
try {
|
|
715
|
+
entries = readdirSync(stateRoot, { withFileTypes: true });
|
|
716
|
+
} catch {
|
|
717
|
+
return [];
|
|
718
|
+
}
|
|
719
|
+
const result: string[] = [];
|
|
720
|
+
for (const entry of entries) {
|
|
721
|
+
if (!entry.isDirectory()) continue;
|
|
722
|
+
const child = join(stateRoot, entry.name);
|
|
723
|
+
if (existsSync(join(child, "run.json"))) result.push(child);
|
|
724
|
+
if (depth + 1 < 8) result.push(...listRunStateDirs(child, depth + 1, seen));
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function runIndexPath(stateRoot: string): string {
|
|
730
|
+
return join(stateRoot, "index.json");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function indexEntryFromStatus(
|
|
734
|
+
status: Record<string, unknown>,
|
|
735
|
+
): RunStateIndexEntry {
|
|
736
|
+
const progress =
|
|
737
|
+
status.progress && typeof status.progress === "object"
|
|
738
|
+
? (status.progress as Record<string, unknown>)
|
|
739
|
+
: {};
|
|
740
|
+
return {
|
|
741
|
+
...(typeof status.ownerId === "string" ? { ownerId: status.ownerId } : {}),
|
|
742
|
+
...(typeof status.recipe === "string" ? { recipe: status.recipe } : {}),
|
|
743
|
+
run: String(status.run),
|
|
744
|
+
state_dir: String(status.state_dir),
|
|
745
|
+
status: status.status as AsyncRunStatus,
|
|
746
|
+
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
747
|
+
updated_at:
|
|
748
|
+
typeof progress.updatedAt === "string"
|
|
749
|
+
? progress.updatedAt
|
|
750
|
+
: typeof status.createdAt === "string"
|
|
751
|
+
? status.createdAt
|
|
752
|
+
: new Date(0).toISOString(),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export function rebuildRunStateIndex(
|
|
757
|
+
stateRoot = DEFAULT_STATE_ROOT,
|
|
758
|
+
): RunStateIndexEntry[] {
|
|
759
|
+
mkdirSync(stateRoot, { recursive: true });
|
|
760
|
+
const entries = listRunStateDirs(stateRoot)
|
|
761
|
+
.flatMap((stateDir) => {
|
|
762
|
+
try {
|
|
763
|
+
return [indexEntryFromStatus(getRunStatus(stateDir))];
|
|
764
|
+
} catch {
|
|
765
|
+
return [];
|
|
766
|
+
}
|
|
767
|
+
})
|
|
768
|
+
.sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""));
|
|
769
|
+
writeJsonAtomic(runIndexPath(stateRoot), {
|
|
770
|
+
entries,
|
|
771
|
+
rebuilt_at: new Date().toISOString(),
|
|
772
|
+
});
|
|
773
|
+
return entries;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function readRunStateIndex(
|
|
777
|
+
stateRoot = DEFAULT_STATE_ROOT,
|
|
778
|
+
): RunStateIndexEntry[] | undefined {
|
|
779
|
+
const index = readJson(runIndexPath(stateRoot));
|
|
780
|
+
if (!index || typeof index !== "object") return undefined;
|
|
781
|
+
const entries = (index as Record<string, unknown>).entries;
|
|
782
|
+
if (!Array.isArray(entries)) return undefined;
|
|
783
|
+
const valid = entries.filter((entry): entry is RunStateIndexEntry => {
|
|
784
|
+
const record =
|
|
785
|
+
entry && typeof entry === "object"
|
|
786
|
+
? (entry as Record<string, unknown>)
|
|
787
|
+
: {};
|
|
788
|
+
return (
|
|
789
|
+
typeof record.run === "string" &&
|
|
790
|
+
typeof record.state_dir === "string" &&
|
|
791
|
+
typeof record.status === "string"
|
|
792
|
+
);
|
|
793
|
+
});
|
|
794
|
+
if (valid.some((entry) => !existsSync(join(entry.state_dir, "run.json"))))
|
|
795
|
+
return undefined;
|
|
796
|
+
return valid;
|
|
797
|
+
}
|
|
798
|
+
|
|
624
799
|
export function listRuns(
|
|
625
800
|
stateRoot = DEFAULT_STATE_ROOT,
|
|
626
801
|
statusFilter?: string,
|
|
627
802
|
): Array<Record<string, unknown>> {
|
|
628
803
|
if (!existsSync(stateRoot)) return [];
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
641
|
-
...(typeof status.recipe === "string" ? { recipe: status.recipe } : {}),
|
|
642
|
-
});
|
|
643
|
-
} catch {
|
|
644
|
-
// Ignore malformed run dirs.
|
|
645
|
-
}
|
|
804
|
+
const indexed = readRunStateIndex(stateRoot);
|
|
805
|
+
if (indexed) {
|
|
806
|
+
return indexed
|
|
807
|
+
.filter((entry) => matchesStatusFilter(entry.status, statusFilter))
|
|
808
|
+
.map((entry) => ({
|
|
809
|
+
run: entry.run,
|
|
810
|
+
state_dir: entry.state_dir,
|
|
811
|
+
status: entry.status,
|
|
812
|
+
...(entry.tool ? { tool: entry.tool } : {}),
|
|
813
|
+
...(entry.recipe ? { recipe: entry.recipe } : {}),
|
|
814
|
+
}));
|
|
646
815
|
}
|
|
647
|
-
return
|
|
816
|
+
return rebuildRunStateIndex(stateRoot)
|
|
817
|
+
.filter((entry) => matchesStatusFilter(entry.status, statusFilter))
|
|
818
|
+
.map((entry) => ({
|
|
819
|
+
run: entry.run,
|
|
820
|
+
state_dir: entry.state_dir,
|
|
821
|
+
status: entry.status,
|
|
822
|
+
...(entry.tool ? { tool: entry.tool } : {}),
|
|
823
|
+
...(entry.recipe ? { recipe: entry.recipe } : {}),
|
|
824
|
+
}));
|
|
648
825
|
}
|
|
649
826
|
|
|
650
827
|
export function tailRun(runOrDir: string, lines = 40): string {
|
|
@@ -732,13 +909,7 @@ function parseRunInboxLine(line: string): RunInboxMessage | undefined {
|
|
|
732
909
|
}
|
|
733
910
|
|
|
734
911
|
function readRunInboxMessagesFromStateDir(stateDir: string): RunInboxMessage[] {
|
|
735
|
-
|
|
736
|
-
if (!existsSync(file)) return [];
|
|
737
|
-
return readFileSync(file, "utf8")
|
|
738
|
-
.split("\n")
|
|
739
|
-
.filter((line) => line.trim())
|
|
740
|
-
.map(parseRunInboxLine)
|
|
741
|
-
.filter((message): message is RunInboxMessage => Boolean(message));
|
|
912
|
+
return readJsonlFileResilient<RunInboxMessage>(runInboxFile(stateDir)).records;
|
|
742
913
|
}
|
|
743
914
|
|
|
744
915
|
function writeRunInboxMessages(
|
|
@@ -879,12 +1050,19 @@ export function appendRunOutboxEvent(
|
|
|
879
1050
|
const run = String(status.run ?? runOrDir);
|
|
880
1051
|
const type = event.type || event.event || "run.message";
|
|
881
1052
|
const to = event.to || "coordinator";
|
|
1053
|
+
const metadata = event.metadata ?? {};
|
|
1054
|
+
const requiresResponse = metadata.requires_response === true;
|
|
882
1055
|
const payload = {
|
|
883
1056
|
...(event.body !== undefined ? { body: event.body } : {}),
|
|
884
1057
|
...(event.correlation_id ? { correlation_id: event.correlation_id } : {}),
|
|
885
1058
|
...(event.data !== undefined ? { data: event.data } : {}),
|
|
886
1059
|
delivery: normalizeRunOutboxDelivery(
|
|
887
|
-
event.delivery ??
|
|
1060
|
+
event.delivery ??
|
|
1061
|
+
(requiresResponse
|
|
1062
|
+
? "followup"
|
|
1063
|
+
: to === "coordinator"
|
|
1064
|
+
? "notify"
|
|
1065
|
+
: "log"),
|
|
888
1066
|
),
|
|
889
1067
|
event: type,
|
|
890
1068
|
from: event.from || `run:${run}`,
|
|
@@ -1242,6 +1420,77 @@ export function cancelRun(runOrDir: string): Record<string, unknown> {
|
|
|
1242
1420
|
: result;
|
|
1243
1421
|
}
|
|
1244
1422
|
|
|
1423
|
+
function assertTerminalRun(runOrDir: string): Record<string, unknown> {
|
|
1424
|
+
const status = getRunStatus(runOrDir);
|
|
1425
|
+
if (status.status === "running") {
|
|
1426
|
+
throw new Error("Only terminal runs can be archived or pruned.");
|
|
1427
|
+
}
|
|
1428
|
+
return status;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function archivePathFor(run: string, stateDir: string): string {
|
|
1432
|
+
const archiveRoot = join(dirname(stateDir), "archived");
|
|
1433
|
+
mkdirSync(archiveRoot, { recursive: true });
|
|
1434
|
+
return join(
|
|
1435
|
+
archiveRoot,
|
|
1436
|
+
`${safeRunId(run)}-${new Date().toISOString().replace(/[:.]/g, "-")}`,
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
export function archiveRun(runOrDir: string): Record<string, unknown> {
|
|
1441
|
+
const status = assertTerminalRun(runOrDir);
|
|
1442
|
+
const stateDir = String(status.state_dir);
|
|
1443
|
+
const run = String(status.run ?? basename(stateDir));
|
|
1444
|
+
const archiveDir = archivePathFor(run, stateDir);
|
|
1445
|
+
renameSync(stateDir, archiveDir);
|
|
1446
|
+
mkdirSync(stateDir, { recursive: true });
|
|
1447
|
+
const tombstone = {
|
|
1448
|
+
archived: true,
|
|
1449
|
+
archive_dir: archiveDir,
|
|
1450
|
+
original_state_dir: stateDir,
|
|
1451
|
+
run,
|
|
1452
|
+
status: status.status,
|
|
1453
|
+
ts: new Date().toISOString(),
|
|
1454
|
+
};
|
|
1455
|
+
writeJsonAtomic(join(stateDir, "archive-tombstone.json"), tombstone);
|
|
1456
|
+
return tombstone;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
export function pruneRun(
|
|
1460
|
+
runOrDir: string,
|
|
1461
|
+
options: { preserveArtifacts?: boolean } = {},
|
|
1462
|
+
): Record<string, unknown> {
|
|
1463
|
+
const status = assertTerminalRun(runOrDir);
|
|
1464
|
+
const stateDir = String(status.state_dir);
|
|
1465
|
+
const run = String(status.run ?? basename(stateDir));
|
|
1466
|
+
const manifest = resolveArtifactManifest(
|
|
1467
|
+
status.artifacts as Record<string, RunArtifactDeclaration> | undefined,
|
|
1468
|
+
);
|
|
1469
|
+
const preserved: Record<string, string> = {};
|
|
1470
|
+
if (options.preserveArtifacts && manifest) {
|
|
1471
|
+
const preserveRoot = join(
|
|
1472
|
+
dirname(stateDir),
|
|
1473
|
+
"preserved-artifacts",
|
|
1474
|
+
safeRunId(run),
|
|
1475
|
+
);
|
|
1476
|
+
mkdirSync(preserveRoot, { recursive: true });
|
|
1477
|
+
for (const [name, artifact] of Object.entries(manifest)) {
|
|
1478
|
+
if (!artifact.exists) continue;
|
|
1479
|
+
const target = join(preserveRoot, basename(artifact.path));
|
|
1480
|
+
cpSync(artifact.path, target, { force: true });
|
|
1481
|
+
preserved[name] = target;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
1485
|
+
return {
|
|
1486
|
+
pruned: true,
|
|
1487
|
+
preserved_artifacts: preserved,
|
|
1488
|
+
run,
|
|
1489
|
+
state_dir: stateDir,
|
|
1490
|
+
ts: new Date().toISOString(),
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1245
1494
|
export function killRun(runOrDir: string): Record<string, unknown> {
|
|
1246
1495
|
const result = stopRun(runOrDir, "SIGKILL", "run.kill");
|
|
1247
1496
|
return Object.hasOwn(result, "stopped")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dist build pipeline entrypoint logic.
|
|
3
|
+
* Zones: packaging, JavaScript-only distributive tree
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { cpSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
function run(command: string, args: string[]): void {
|
|
11
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
12
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildDist(): void {
|
|
16
|
+
rmSync("dist", { recursive: true, force: true });
|
|
17
|
+
mkdirSync("dist", { recursive: true });
|
|
18
|
+
|
|
19
|
+
run("tsc", ["-p", "tsconfig.build.json"]);
|
|
20
|
+
|
|
21
|
+
for (const dir of ["scripts", "recipes", "fixtures", "skills"]) {
|
|
22
|
+
cpSync(dir, join("dist", dir), { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const builtScripts = readdirSync(join("dist", "scripts"))
|
|
26
|
+
.filter((name) => name.endsWith(".mjs"))
|
|
27
|
+
.map((name) => join("dist", "scripts", name));
|
|
28
|
+
|
|
29
|
+
run(process.execPath, ["--check", ...builtScripts]);
|
|
30
|
+
}
|