@llblab/pi-actors 0.33.0 → 0.34.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 +22 -11
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +22 -1
- package/dist/index.js +10 -10
- package/dist/lib/async-runs.d.ts +23 -75
- package/dist/lib/async-runs.js +49 -801
- package/dist/lib/command-templates.d.ts +3 -3
- package/dist/lib/command-templates.js +3 -3
- package/dist/lib/config.d.ts +2 -2
- package/dist/lib/config.js +5 -5
- package/dist/lib/{output.d.ts → execution-output.d.ts} +3 -3
- package/dist/lib/{output.js → execution-output.js} +3 -3
- package/dist/lib/execution.js +1 -1
- package/dist/lib/{actor-inspector-tui.d.ts → inspector.d.ts} +3 -2
- package/dist/lib/{actor-inspector-tui.js → inspector.js} +3 -2
- package/dist/lib/mailbox-loop.d.ts +3 -3
- package/dist/lib/mailbox-loop.js +3 -3
- package/dist/lib/{actor-messages.d.ts → messages.d.ts} +3 -3
- package/dist/lib/{actor-messages.js → messages.js} +3 -3
- package/dist/lib/{actor-recipe-context.d.ts → recipes-context.d.ts} +3 -2
- package/dist/lib/{actor-recipe-context.js → recipes-context.js} +2 -1
- package/dist/lib/{recipe-discovery.d.ts → recipes-discovery.d.ts} +8 -8
- package/dist/lib/{recipe-discovery.js → recipes-discovery.js} +6 -6
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +22 -22
- package/dist/lib/{actor-rooms.d.ts → rooms.d.ts} +4 -4
- package/dist/lib/{actor-rooms.js → rooms.js} +3 -3
- package/dist/lib/runs-artifacts.d.ts +21 -0
- package/dist/lib/runs-artifacts.js +68 -0
- package/dist/lib/runs-control.d.ts +13 -0
- package/dist/lib/runs-control.js +59 -0
- package/dist/lib/runs-identity.d.ts +5 -0
- package/dist/lib/runs-identity.js +11 -0
- package/dist/lib/runs-index.d.ts +18 -0
- package/dist/lib/runs-index.js +119 -0
- package/dist/lib/runs-mailbox.d.ts +25 -0
- package/dist/lib/runs-mailbox.js +146 -0
- package/dist/lib/runs-messages.d.ts +15 -0
- package/dist/lib/runs-messages.js +179 -0
- package/dist/lib/runs-outbox.d.ts +41 -0
- package/dist/lib/runs-outbox.js +87 -0
- package/dist/lib/runs-process.d.ts +7 -0
- package/dist/lib/runs-process.js +36 -0
- package/dist/lib/runs-retention.d.ts +8 -0
- package/dist/lib/runs-retention.js +56 -0
- package/dist/lib/runs-start.d.ts +9 -0
- package/dist/lib/runs-start.js +67 -0
- package/dist/lib/runs-status.d.ts +8 -0
- package/dist/lib/runs-status.js +60 -0
- package/dist/lib/runtime.d.ts +2 -2
- package/dist/lib/runtime.js +5 -5
- package/dist/lib/schema.d.ts +9 -0
- package/dist/lib/schema.js +40 -0
- package/dist/lib/tools-access.d.ts +19 -0
- package/dist/lib/tools-access.js +44 -0
- package/dist/lib/tools-inspect.d.ts +11 -0
- package/dist/lib/tools-inspect.js +534 -0
- package/dist/lib/tools-local.d.ts +14 -0
- package/dist/lib/tools-local.js +156 -0
- package/dist/lib/tools-mailbox.d.ts +8 -0
- package/dist/lib/tools-mailbox.js +48 -0
- package/dist/lib/tools-message.d.ts +9 -0
- package/dist/lib/tools-message.js +346 -0
- package/dist/lib/tools-register.d.ts +18 -0
- package/dist/lib/tools-register.js +40 -0
- package/dist/lib/tools-response.d.ts +21 -0
- package/dist/lib/tools-response.js +219 -0
- package/dist/lib/tools-spawn.d.ts +12 -0
- package/dist/lib/tools-spawn.js +159 -0
- package/dist/lib/tools.d.ts +8 -39
- package/dist/lib/tools.js +11 -1490
- package/dist/recipes/utility-coordinator-lock-snapshot.json +1 -1
- package/dist/scripts/actor-worker.mjs +186 -7
- package/dist/scripts/async-runner.mjs +171 -8
- package/dist/scripts/build-dist.mjs +1 -1
- package/dist/scripts/conformance.mjs +3 -3
- package/dist/scripts/coordinator.mjs +733 -11
- package/dist/scripts/locker.mjs +345 -16
- package/dist/scripts/recipe-utils.mjs +478 -14
- package/dist/scripts/validate-recipe.mjs +110 -10
- package/dist/skills/actors/SKILL.md +1 -1
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/recipe-library.md +1 -1
- package/docs/template-recipes.md +1 -1
- package/index.ts +11 -18
- package/lib/async-runs.ts +124 -1016
- package/lib/command-templates.ts +3 -3
- package/lib/config.ts +7 -7
- package/lib/{output.ts → execution-output.ts} +3 -3
- package/lib/execution.ts +5 -1
- package/lib/{actor-inspector-tui.ts → inspector.ts} +4 -3
- package/lib/mailbox-loop.ts +3 -3
- package/lib/{actor-messages.ts → messages.ts} +3 -3
- package/lib/{actor-recipe-context.ts → recipes-context.ts} +3 -2
- package/lib/{recipe-discovery.ts → recipes-discovery.ts} +19 -19
- package/lib/{recipe-usage.ts → recipes-usage.ts} +2 -2
- package/lib/registry.ts +37 -24
- package/lib/{actor-rooms.ts → rooms.ts} +4 -4
- package/lib/runs-artifacts.ts +96 -0
- package/lib/runs-control.ts +85 -0
- package/lib/runs-identity.ts +14 -0
- package/lib/runs-index.ts +156 -0
- package/lib/runs-mailbox.ts +208 -0
- package/lib/runs-messages.ts +252 -0
- package/lib/runs-outbox.ts +144 -0
- package/lib/runs-process.ts +51 -0
- package/lib/runs-retention.ts +77 -0
- package/lib/runs-start.ts +98 -0
- package/lib/runs-status.ts +78 -0
- package/lib/runtime.ts +6 -6
- package/lib/schema.ts +55 -0
- package/lib/tools-access.ts +68 -0
- package/lib/tools-inspect.ts +735 -0
- package/lib/tools-local.ts +234 -0
- package/lib/tools-mailbox.ts +56 -0
- package/lib/tools-message.ts +483 -0
- package/lib/tools-register.ts +64 -0
- package/lib/tools-response.ts +267 -0
- package/lib/tools-spawn.ts +240 -0
- package/lib/tools.ts +20 -2040
- package/package.json +1 -1
- package/recipes/utility-coordinator-lock-snapshot.json +1 -1
- package/scripts/actor-worker.mjs +186 -7
- package/scripts/async-runner.mjs +171 -8
- package/scripts/build-dist.mjs +1 -1
- package/scripts/conformance.mjs +3 -3
- package/scripts/coordinator.mjs +733 -11
- package/scripts/locker.mjs +345 -16
- package/scripts/recipe-utils.mjs +478 -14
- package/scripts/validate-recipe.mjs +110 -10
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
- package/dist/lib/actor-worker.d.ts +0 -16
- package/dist/lib/actor-worker.js +0 -134
- package/dist/lib/async-runner.d.ts +0 -5
- package/dist/lib/async-runner.js +0 -134
- package/dist/lib/coordinator.d.ts +0 -5
- package/dist/lib/coordinator.js +0 -660
- package/dist/lib/locker.d.ts +0 -5
- package/dist/lib/locker.js +0 -310
- package/dist/lib/recipe-utils.d.ts +0 -5
- package/dist/lib/recipe-utils.js +0 -385
- package/dist/lib/validate-recipe.d.ts +0 -6
- package/dist/lib/validate-recipe.js +0 -104
- package/lib/actor-worker.ts +0 -174
- package/lib/async-runner.ts +0 -173
- package/lib/coordinator.ts +0 -742
- package/lib/locker.ts +0 -340
- package/lib/recipe-utils.ts +0 -486
- package/lib/validate-recipe.ts +0 -110
- /package/dist/lib/{recipe-references.d.ts → recipes-references.d.ts} +0 -0
- /package/dist/lib/{recipe-references.js → recipes-references.js} +0 -0
- /package/dist/lib/{recipe-usage.d.ts → recipes-usage.d.ts} +0 -0
- /package/dist/lib/{recipe-usage.js → recipes-usage.js} +0 -0
- /package/lib/{recipe-references.ts → recipes-references.ts} +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run outbox events.
|
|
3
|
+
* Owns outbox event normalization, parsing, and append payload formatting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type RunOutboxDelivery = "log" | "notify" | "followup";
|
|
7
|
+
export type RunOutboxLevel = "info" | "warning" | "error";
|
|
8
|
+
|
|
9
|
+
export interface RunOutboxEvent {
|
|
10
|
+
body?: unknown;
|
|
11
|
+
correlation_id?: string;
|
|
12
|
+
data?: unknown;
|
|
13
|
+
delivery: RunOutboxDelivery;
|
|
14
|
+
event: string;
|
|
15
|
+
from?: string;
|
|
16
|
+
id: string;
|
|
17
|
+
level: RunOutboxLevel;
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
reply_to?: string;
|
|
20
|
+
run: string;
|
|
21
|
+
state_dir: string;
|
|
22
|
+
summary: string;
|
|
23
|
+
to?: string;
|
|
24
|
+
ts: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeRunOutboxDelivery(value: unknown): RunOutboxDelivery {
|
|
29
|
+
return value === "notify" || value === "followup" ? value : "log";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeRunOutboxLevel(value: unknown): RunOutboxLevel {
|
|
33
|
+
return value === "warning" || value === "error" ? value : "info";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeRunOutboxEvent(
|
|
37
|
+
raw: unknown,
|
|
38
|
+
run: string,
|
|
39
|
+
stateDir: string,
|
|
40
|
+
index: number,
|
|
41
|
+
): RunOutboxEvent | undefined {
|
|
42
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
43
|
+
const record = raw as Record<string, unknown>;
|
|
44
|
+
const event =
|
|
45
|
+
typeof record.event === "string" && record.event.trim()
|
|
46
|
+
? record.event.trim()
|
|
47
|
+
: "run.event";
|
|
48
|
+
const summary =
|
|
49
|
+
typeof record.summary === "string" && record.summary.trim()
|
|
50
|
+
? record.summary.trim()
|
|
51
|
+
: event;
|
|
52
|
+
const ts =
|
|
53
|
+
typeof record.ts === "string" && record.ts.trim()
|
|
54
|
+
? record.ts.trim()
|
|
55
|
+
: new Date(0).toISOString();
|
|
56
|
+
const id =
|
|
57
|
+
typeof record.id === "string" && record.id.trim()
|
|
58
|
+
? record.id.trim()
|
|
59
|
+
: `${run}:${index}`;
|
|
60
|
+
return {
|
|
61
|
+
...(record.body !== undefined ? { body: record.body } : {}),
|
|
62
|
+
...(typeof record.correlation_id === "string"
|
|
63
|
+
? { correlation_id: record.correlation_id }
|
|
64
|
+
: {}),
|
|
65
|
+
...(record.data !== undefined ? { data: record.data } : {}),
|
|
66
|
+
delivery: normalizeRunOutboxDelivery(record.delivery),
|
|
67
|
+
...(record.metadata &&
|
|
68
|
+
typeof record.metadata === "object" &&
|
|
69
|
+
!Array.isArray(record.metadata)
|
|
70
|
+
? { metadata: record.metadata as Record<string, unknown> }
|
|
71
|
+
: {}),
|
|
72
|
+
event,
|
|
73
|
+
...(typeof record.from === "string" ? { from: record.from } : {}),
|
|
74
|
+
id,
|
|
75
|
+
level: normalizeRunOutboxLevel(record.level),
|
|
76
|
+
...(typeof record.reply_to === "string"
|
|
77
|
+
? { reply_to: record.reply_to }
|
|
78
|
+
: {}),
|
|
79
|
+
run,
|
|
80
|
+
state_dir: stateDir,
|
|
81
|
+
summary,
|
|
82
|
+
...(typeof record.to === "string" ? { to: record.to } : {}),
|
|
83
|
+
ts,
|
|
84
|
+
...(typeof record.type === "string" ? { type: record.type } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseRunOutboxEventLine(
|
|
89
|
+
line: string,
|
|
90
|
+
run: string,
|
|
91
|
+
stateDir: string,
|
|
92
|
+
index: number,
|
|
93
|
+
): RunOutboxEvent | undefined {
|
|
94
|
+
try {
|
|
95
|
+
return normalizeRunOutboxEvent(JSON.parse(line), run, stateDir, index);
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildRunOutboxEventPayload(
|
|
102
|
+
run: string,
|
|
103
|
+
event: {
|
|
104
|
+
body?: unknown;
|
|
105
|
+
correlation_id?: string;
|
|
106
|
+
data?: unknown;
|
|
107
|
+
delivery?: string;
|
|
108
|
+
event?: string;
|
|
109
|
+
from?: string;
|
|
110
|
+
level?: string;
|
|
111
|
+
metadata?: Record<string, unknown>;
|
|
112
|
+
reply_to?: string;
|
|
113
|
+
summary?: string;
|
|
114
|
+
to?: string;
|
|
115
|
+
type?: string;
|
|
116
|
+
},
|
|
117
|
+
): Record<string, unknown> {
|
|
118
|
+
const type = event.type || event.event || "run.message";
|
|
119
|
+
const to = event.to || "coordinator";
|
|
120
|
+
const metadata = event.metadata ?? {};
|
|
121
|
+
const requiresResponse = metadata.requires_response === true;
|
|
122
|
+
return {
|
|
123
|
+
...(event.body !== undefined ? { body: event.body } : {}),
|
|
124
|
+
...(event.correlation_id ? { correlation_id: event.correlation_id } : {}),
|
|
125
|
+
...(event.data !== undefined ? { data: event.data } : {}),
|
|
126
|
+
delivery: normalizeRunOutboxDelivery(
|
|
127
|
+
event.delivery ??
|
|
128
|
+
(requiresResponse
|
|
129
|
+
? "followup"
|
|
130
|
+
: to === "coordinator"
|
|
131
|
+
? "notify"
|
|
132
|
+
: "log"),
|
|
133
|
+
),
|
|
134
|
+
event: type,
|
|
135
|
+
from: event.from || `run:${run}`,
|
|
136
|
+
level: normalizeRunOutboxLevel(event.level),
|
|
137
|
+
...(event.metadata ? { metadata: event.metadata } : {}),
|
|
138
|
+
...(event.reply_to ? { reply_to: event.reply_to } : {}),
|
|
139
|
+
summary: event.summary || type,
|
|
140
|
+
to,
|
|
141
|
+
ts: new Date().toISOString(),
|
|
142
|
+
type,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run process identity helpers.
|
|
3
|
+
* Owns liveness checks and Linux runner identity matching for run-owned processes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, readlinkSync } from "node:fs";
|
|
7
|
+
import { platform } from "node:os";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
export function isAlive(pid: number): boolean {
|
|
11
|
+
try {
|
|
12
|
+
process.kill(pid, 0);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function pidMatchesRun(
|
|
20
|
+
pid: number,
|
|
21
|
+
cwd: string,
|
|
22
|
+
stateDir: string,
|
|
23
|
+
runnerPath: string,
|
|
24
|
+
): boolean {
|
|
25
|
+
if (platform() !== "linux" || !existsSync(`/proc/${pid}`))
|
|
26
|
+
return isAlive(pid);
|
|
27
|
+
try {
|
|
28
|
+
const procCwd = readlinkSync(`/proc/${pid}/cwd`);
|
|
29
|
+
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
30
|
+
return (
|
|
31
|
+
procCwd === resolve(cwd) &&
|
|
32
|
+
cmdline.includes(runnerPath) &&
|
|
33
|
+
cmdline.includes(stateDir)
|
|
34
|
+
);
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isWithinRunnerIdentityGrace(
|
|
41
|
+
meta: Record<string, unknown>,
|
|
42
|
+
graceMs: number,
|
|
43
|
+
): boolean {
|
|
44
|
+
const createdAt =
|
|
45
|
+
typeof meta.createdAt === "string" ? Date.parse(meta.createdAt) : NaN;
|
|
46
|
+
return (
|
|
47
|
+
Number.isFinite(createdAt) &&
|
|
48
|
+
Date.now() - createdAt >= 0 &&
|
|
49
|
+
Date.now() - createdAt <= graceMs
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run retention operations.
|
|
3
|
+
* Owns terminal-run archive and prune filesystem behavior.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { cpSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { writeJsonAtomic } from "./file-state.ts";
|
|
10
|
+
import {
|
|
11
|
+
resolveArtifactManifest,
|
|
12
|
+
type RunArtifactDeclaration,
|
|
13
|
+
} from "./runs-artifacts.ts";
|
|
14
|
+
import { safeRunId } from "./runs-identity.ts";
|
|
15
|
+
|
|
16
|
+
function archivePathFor(run: string, stateDir: string): string {
|
|
17
|
+
const archiveRoot = join(dirname(stateDir), "archived");
|
|
18
|
+
mkdirSync(archiveRoot, { recursive: true });
|
|
19
|
+
return join(
|
|
20
|
+
archiveRoot,
|
|
21
|
+
`${safeRunId(run)}-${new Date().toISOString().replace(/[:.]/g, "-")}`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function archiveTerminalRun(
|
|
26
|
+
status: Record<string, unknown>,
|
|
27
|
+
): Record<string, unknown> {
|
|
28
|
+
const stateDir = String(status.state_dir);
|
|
29
|
+
const run = String(status.run ?? basename(stateDir));
|
|
30
|
+
const archiveDir = archivePathFor(run, stateDir);
|
|
31
|
+
renameSync(stateDir, archiveDir);
|
|
32
|
+
mkdirSync(stateDir, { recursive: true });
|
|
33
|
+
const tombstone = {
|
|
34
|
+
archived: true,
|
|
35
|
+
archive_dir: archiveDir,
|
|
36
|
+
original_state_dir: stateDir,
|
|
37
|
+
run,
|
|
38
|
+
status: status.status,
|
|
39
|
+
ts: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
writeJsonAtomic(join(stateDir, "archive-tombstone.json"), tombstone);
|
|
42
|
+
return tombstone;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function pruneTerminalRun(
|
|
46
|
+
status: Record<string, unknown>,
|
|
47
|
+
options: { preserveArtifacts?: boolean } = {},
|
|
48
|
+
): Record<string, unknown> {
|
|
49
|
+
const stateDir = String(status.state_dir);
|
|
50
|
+
const run = String(status.run ?? basename(stateDir));
|
|
51
|
+
const manifest = resolveArtifactManifest(
|
|
52
|
+
status.artifacts as Record<string, RunArtifactDeclaration> | undefined,
|
|
53
|
+
);
|
|
54
|
+
const preserved: Record<string, string> = {};
|
|
55
|
+
if (options.preserveArtifacts && manifest) {
|
|
56
|
+
const preserveRoot = join(
|
|
57
|
+
dirname(stateDir),
|
|
58
|
+
"preserved-artifacts",
|
|
59
|
+
safeRunId(run),
|
|
60
|
+
);
|
|
61
|
+
mkdirSync(preserveRoot, { recursive: true });
|
|
62
|
+
for (const [name, artifact] of Object.entries(manifest)) {
|
|
63
|
+
if (!artifact.exists) continue;
|
|
64
|
+
const target = join(preserveRoot, basename(artifact.path));
|
|
65
|
+
cpSync(artifact.path, target, { force: true });
|
|
66
|
+
preserved[name] = target;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
70
|
+
return {
|
|
71
|
+
pruned: true,
|
|
72
|
+
preserved_artifacts: preserved,
|
|
73
|
+
run,
|
|
74
|
+
state_dir: stateDir,
|
|
75
|
+
ts: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run start guards.
|
|
3
|
+
* Owns active-run reuse checks, start lock acquisition, and safe state-dir
|
|
4
|
+
* preparation before a new runner process is spawned.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import { isAlive, pidMatchesRun } from "./runs-process.ts";
|
|
11
|
+
|
|
12
|
+
const START_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
export type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
|
|
15
|
+
|
|
16
|
+
export function assertNoActiveRunState(
|
|
17
|
+
stateDir: string,
|
|
18
|
+
readJson: RunJsonReader,
|
|
19
|
+
runnerPath: string,
|
|
20
|
+
): void {
|
|
21
|
+
const meta = readJson(join(stateDir, "run.json"));
|
|
22
|
+
if (!meta) return;
|
|
23
|
+
const pid = Number(meta.pid || 0);
|
|
24
|
+
const cwd = String(meta.cwd ?? "");
|
|
25
|
+
if (!pid || !isAlive(pid) || !pidMatchesRun(pid, cwd, stateDir, runnerPath))
|
|
26
|
+
return;
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function acquireStateStartLock(stateDir: string): () => void {
|
|
33
|
+
const lockDir = join(stateDir, ".start.lock");
|
|
34
|
+
try {
|
|
35
|
+
mkdirSync(lockDir);
|
|
36
|
+
writeFileSync(
|
|
37
|
+
join(lockDir, "owner.json"),
|
|
38
|
+
`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`,
|
|
39
|
+
"utf8",
|
|
40
|
+
);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
try {
|
|
43
|
+
const stat = statSync(lockDir);
|
|
44
|
+
if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS) {
|
|
45
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
46
|
+
mkdirSync(lockDir);
|
|
47
|
+
writeFileSync(
|
|
48
|
+
join(lockDir, "owner.json"),
|
|
49
|
+
`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), recovered: true })}\n`,
|
|
50
|
+
"utf8",
|
|
51
|
+
);
|
|
52
|
+
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Keep the original lock acquisition error below.
|
|
56
|
+
}
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Run state is already being started: ${stateDir}. Retry after the current start finishes.`,
|
|
59
|
+
{ cause: error },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function prepareStateDirForStart(
|
|
66
|
+
stateDir: string,
|
|
67
|
+
readJson: RunJsonReader,
|
|
68
|
+
runnerPath: string,
|
|
69
|
+
): void {
|
|
70
|
+
const existing = readJson(join(stateDir, "run.json"));
|
|
71
|
+
const existingPid = Number(existing?.pid || 0);
|
|
72
|
+
const existingCwd =
|
|
73
|
+
typeof existing?.cwd === "string" ? existing.cwd : undefined;
|
|
74
|
+
const existingResult = readJson(join(stateDir, "result.json"));
|
|
75
|
+
if (
|
|
76
|
+
!existingResult &&
|
|
77
|
+
existingPid &&
|
|
78
|
+
existingCwd &&
|
|
79
|
+
isAlive(existingPid) &&
|
|
80
|
+
pidMatchesRun(existingPid, existingCwd, stateDir, runnerPath)
|
|
81
|
+
) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Run state already has an active owned process: ${String(existing?.run ?? stateDir)}. Stop it before restarting.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
for (const file of [
|
|
87
|
+
"events.jsonl",
|
|
88
|
+
"inbox.jsonl",
|
|
89
|
+
"outbox.jsonl",
|
|
90
|
+
"progress.json",
|
|
91
|
+
"result.json",
|
|
92
|
+
"stderr.log",
|
|
93
|
+
"stdout.log",
|
|
94
|
+
"terminal-handled.json",
|
|
95
|
+
]) {
|
|
96
|
+
rmSync(join(stateDir, file), { force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async run status and log readers.
|
|
3
|
+
* Owns status derivation from run state files plus bounded log/event tails.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import type { AsyncRunStatus } from "./async-runs.ts";
|
|
10
|
+
import { isAlive, isWithinRunnerIdentityGrace, pidMatchesRun } from "./runs-process.ts";
|
|
11
|
+
import { readJsonlFileResilient } from "./state-readers.ts";
|
|
12
|
+
|
|
13
|
+
export type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
|
|
14
|
+
|
|
15
|
+
function getInterruptedRunStatus(
|
|
16
|
+
stateDir: string,
|
|
17
|
+
): "cancelled" | "killed" | undefined {
|
|
18
|
+
const events = readJsonlFileResilient<Record<string, unknown>>(
|
|
19
|
+
join(stateDir, "events.jsonl"),
|
|
20
|
+
).records.slice(-200);
|
|
21
|
+
for (const event of events.reverse()) {
|
|
22
|
+
if (event.event === "run.kill") return "killed";
|
|
23
|
+
if (event.event === "run.cancel") return "cancelled";
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildRunStatus(
|
|
29
|
+
stateDir: string,
|
|
30
|
+
runOrDir: string,
|
|
31
|
+
meta: Record<string, unknown>,
|
|
32
|
+
readJson: RunJsonReader,
|
|
33
|
+
runnerPath: string,
|
|
34
|
+
runnerIdentityGraceMs: number,
|
|
35
|
+
): Record<string, unknown> {
|
|
36
|
+
const result = readJson(join(stateDir, "result.json"));
|
|
37
|
+
const pid = Number(meta.pid || 0);
|
|
38
|
+
const aliveOwnedRunner = Boolean(
|
|
39
|
+
pid &&
|
|
40
|
+
isAlive(pid) &&
|
|
41
|
+
(!Array.isArray(meta.argv) ||
|
|
42
|
+
pidMatchesRun(pid, String(meta.cwd ?? ""), stateDir, runnerPath) ||
|
|
43
|
+
isWithinRunnerIdentityGrace(meta, runnerIdentityGraceMs)),
|
|
44
|
+
);
|
|
45
|
+
const status: AsyncRunStatus = result
|
|
46
|
+
? Number(result.code ?? 0) === 0
|
|
47
|
+
? "done"
|
|
48
|
+
: "failed"
|
|
49
|
+
: aliveOwnedRunner
|
|
50
|
+
? "running"
|
|
51
|
+
: (getInterruptedRunStatus(stateDir) ?? "exited");
|
|
52
|
+
const terminalHandled = readJson(join(stateDir, "terminal-handled.json"));
|
|
53
|
+
return {
|
|
54
|
+
...meta,
|
|
55
|
+
eventsFile: join(stateDir, "events.jsonl"),
|
|
56
|
+
inboxFile: join(stateDir, "inbox.jsonl"),
|
|
57
|
+
outboxFile: join(stateDir, "outbox.jsonl"),
|
|
58
|
+
progress: readJson(join(stateDir, "progress.json")) || null,
|
|
59
|
+
result: result || null,
|
|
60
|
+
...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
|
|
61
|
+
state_dir: String(meta.state_dir ?? stateDir),
|
|
62
|
+
stderrLog: join(stateDir, "stderr.log"),
|
|
63
|
+
stdoutLog: join(stateDir, "stdout.log"),
|
|
64
|
+
status,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function tailFile(path: string, lines: number): string {
|
|
69
|
+
if (!existsSync(path)) return "";
|
|
70
|
+
const content = readFileSync(path, "utf8").trimEnd();
|
|
71
|
+
if (!content) return "";
|
|
72
|
+
return content.split("\n").slice(-lines).join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function tailLines(path: string, lines: number): string[] {
|
|
76
|
+
const content = tailFile(path, lines);
|
|
77
|
+
return content ? content.split("\n") : [];
|
|
78
|
+
}
|
package/lib/runtime.ts
CHANGED
|
@@ -9,8 +9,8 @@ import { existsSync, watch, type FSWatcher } from "node:fs";
|
|
|
9
9
|
import * as Config from "./config.ts";
|
|
10
10
|
import type { RegisteredToolExec } from "./execution.ts";
|
|
11
11
|
import * as Paths from "./paths.ts";
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
12
|
+
import * as RecipesDiscovery from "./recipes-discovery.ts";
|
|
13
|
+
import * as ToolsLocal from "./tools-local.ts";
|
|
14
14
|
|
|
15
15
|
export interface RuntimeContext {
|
|
16
16
|
hasUI: boolean;
|
|
@@ -31,7 +31,7 @@ export interface ToolRegistryRuntimeDeps {
|
|
|
31
31
|
getActiveTools?: () => string[];
|
|
32
32
|
getAllTools: () => ToolInfoLike[];
|
|
33
33
|
registerTool: (
|
|
34
|
-
definition: ReturnType<typeof
|
|
34
|
+
definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>,
|
|
35
35
|
) => void;
|
|
36
36
|
reservedToolNames: Set<string>;
|
|
37
37
|
setActiveTools?: (toolNames: string[]) => void;
|
|
@@ -100,7 +100,7 @@ export function createAutoToolsRuntime(
|
|
|
100
100
|
function registerRuntimeTool(cfg: Config.RegisteredTool) {
|
|
101
101
|
const fingerprint = getToolFingerprint(cfg);
|
|
102
102
|
if (runtimeToolFingerprints.get(cfg.name) === fingerprint) return;
|
|
103
|
-
deps.registerTool(
|
|
103
|
+
deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
|
|
104
104
|
runtimeTools.add(cfg.name);
|
|
105
105
|
runtimeToolFingerprints.set(cfg.name, fingerprint);
|
|
106
106
|
}
|
|
@@ -148,7 +148,7 @@ export function createAutoToolsRuntime(
|
|
|
148
148
|
const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
|
|
149
149
|
const packagedRecipeRoot =
|
|
150
150
|
deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot();
|
|
151
|
-
const discovered =
|
|
151
|
+
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
152
152
|
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
153
153
|
{ root: packagedRecipeRoot },
|
|
154
154
|
]);
|
|
@@ -156,7 +156,7 @@ export function createAutoToolsRuntime(
|
|
|
156
156
|
tools.clear();
|
|
157
157
|
for (const entry of discovered.active.values()) {
|
|
158
158
|
try {
|
|
159
|
-
const cfg =
|
|
159
|
+
const cfg = RecipesDiscovery.toRegisteredTool(entry);
|
|
160
160
|
if (cfg) tools.set(cfg.name, cfg);
|
|
161
161
|
} catch (error) {
|
|
162
162
|
warnings.push(
|
package/lib/schema.ts
CHANGED
|
@@ -30,6 +30,61 @@ export interface ToolArgSpec {
|
|
|
30
30
|
error?: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export type JsonSchema = Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
export function stringSchema(description: string): JsonSchema {
|
|
36
|
+
return { description, type: "string" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function typedArgSchema(
|
|
40
|
+
description: string,
|
|
41
|
+
type: ToolArgType | undefined,
|
|
42
|
+
): JsonSchema {
|
|
43
|
+
if (!type) return stringSchema(description);
|
|
44
|
+
switch (type.kind) {
|
|
45
|
+
case "int":
|
|
46
|
+
return { description, type: "integer" };
|
|
47
|
+
case "number":
|
|
48
|
+
return { description, type: "number" };
|
|
49
|
+
case "bool":
|
|
50
|
+
return { description, type: "boolean" };
|
|
51
|
+
case "array":
|
|
52
|
+
return { description, items: {}, type: "array" };
|
|
53
|
+
case "enum":
|
|
54
|
+
return { description, enum: type.values, type: "string" };
|
|
55
|
+
case "path":
|
|
56
|
+
case "string":
|
|
57
|
+
return stringSchema(description);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function booleanSchema(description: string): JsonSchema {
|
|
62
|
+
return { description, type: "boolean" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function nullSchema(description: string): JsonSchema {
|
|
66
|
+
return { description, type: "null" };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function arraySchema(description: string): JsonSchema {
|
|
70
|
+
return { description, items: {}, type: "array" };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function unionSchema(anyOf: JsonSchema[]): JsonSchema {
|
|
74
|
+
return { anyOf };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function objectSchema(
|
|
78
|
+
properties: Record<string, JsonSchema>,
|
|
79
|
+
required: string[] = [],
|
|
80
|
+
): JsonSchema {
|
|
81
|
+
return { additionalProperties: false, properties, required, type: "object" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function looseObjectSchema(description: string): JsonSchema {
|
|
85
|
+
return { additionalProperties: true, description, type: "object" };
|
|
86
|
+
}
|
|
87
|
+
|
|
33
88
|
function mergeUnique(items: string[]): string[] {
|
|
34
89
|
return [...new Set(items.filter(Boolean))];
|
|
35
90
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public tool access and ownership checks
|
|
3
|
+
* Zones: session ownership, run visibility guards, tool authorization errors
|
|
4
|
+
* Owns consistent session-mismatch diagnostics for public tool execution paths
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as AsyncRuns from "./async-runs.ts";
|
|
8
|
+
|
|
9
|
+
export interface SessionContext {
|
|
10
|
+
sessionManager?: { getSessionId?: () => string };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getContextSessionId(ctx: unknown): string | undefined {
|
|
14
|
+
return (ctx as SessionContext | undefined)?.sessionManager?.getSessionId?.();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function requireContextSessionId(ctx: unknown, actor: string): string {
|
|
18
|
+
const sessionId = getContextSessionId(ctx);
|
|
19
|
+
if (!sessionId) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`${actor} requires a current coordinator session; use session:<id> or session:all for explicit session inventory.`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return sessionId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sessionMismatchError(input: {
|
|
28
|
+
currentSession?: string;
|
|
29
|
+
expectedSession?: string;
|
|
30
|
+
run?: string;
|
|
31
|
+
target?: string;
|
|
32
|
+
}): Error {
|
|
33
|
+
const ownerSession = input.expectedSession ?? "none";
|
|
34
|
+
const currentSession = input.currentSession ?? "none";
|
|
35
|
+
const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
|
|
36
|
+
const hintTarget = input.expectedSession
|
|
37
|
+
? `session:${input.expectedSession}`
|
|
38
|
+
: "session:all";
|
|
39
|
+
return Object.assign(
|
|
40
|
+
new Error(
|
|
41
|
+
`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_session:${input.expectedSession ?? "all"}`,
|
|
42
|
+
),
|
|
43
|
+
{
|
|
44
|
+
current_session: input.currentSession,
|
|
45
|
+
hint: `inspect target=${hintTarget} view=status`,
|
|
46
|
+
owner_session: input.expectedSession,
|
|
47
|
+
reason: "session_mismatch",
|
|
48
|
+
run: input.run,
|
|
49
|
+
target: input.target,
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function assertRunAccessibleToContext(
|
|
55
|
+
runId: string,
|
|
56
|
+
ctx: unknown,
|
|
57
|
+
): Record<string, unknown> {
|
|
58
|
+
const status = AsyncRuns.getRunStatus(runId);
|
|
59
|
+
const sessionId = getContextSessionId(ctx);
|
|
60
|
+
if (sessionId && status.ownerId && status.ownerId !== sessionId) {
|
|
61
|
+
throw sessionMismatchError({
|
|
62
|
+
currentSession: sessionId,
|
|
63
|
+
expectedSession: String(status.ownerId),
|
|
64
|
+
run: runId,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return status;
|
|
68
|
+
}
|