@llblab/pi-actors 0.33.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/AGENTS.md +22 -11
  2. package/BACKLOG.md +3 -34
  3. package/CHANGELOG.md +14 -0
  4. package/dist/index.js +10 -10
  5. package/dist/lib/async-runs.d.ts +23 -75
  6. package/dist/lib/async-runs.js +49 -801
  7. package/dist/lib/command-templates.d.ts +3 -3
  8. package/dist/lib/command-templates.js +3 -3
  9. package/dist/lib/config.d.ts +2 -2
  10. package/dist/lib/config.js +5 -5
  11. package/dist/lib/{output.d.ts → execution-output.d.ts} +3 -3
  12. package/dist/lib/{output.js → execution-output.js} +3 -3
  13. package/dist/lib/execution.js +1 -1
  14. package/dist/lib/{actor-inspector-tui.d.ts → inspector.d.ts} +3 -2
  15. package/dist/lib/{actor-inspector-tui.js → inspector.js} +3 -2
  16. package/dist/lib/mailbox-loop.d.ts +3 -3
  17. package/dist/lib/mailbox-loop.js +3 -3
  18. package/dist/lib/{actor-messages.d.ts → messages.d.ts} +3 -3
  19. package/dist/lib/{actor-messages.js → messages.js} +3 -3
  20. package/dist/lib/{actor-recipe-context.d.ts → recipes-context.d.ts} +3 -2
  21. package/dist/lib/{actor-recipe-context.js → recipes-context.js} +2 -1
  22. package/dist/lib/{recipe-discovery.d.ts → recipes-discovery.d.ts} +8 -8
  23. package/dist/lib/{recipe-discovery.js → recipes-discovery.js} +6 -6
  24. package/dist/lib/registry.d.ts +1 -1
  25. package/dist/lib/registry.js +22 -22
  26. package/dist/lib/{actor-rooms.d.ts → rooms.d.ts} +4 -4
  27. package/dist/lib/{actor-rooms.js → rooms.js} +3 -3
  28. package/dist/lib/runs-artifacts.d.ts +21 -0
  29. package/dist/lib/runs-artifacts.js +68 -0
  30. package/dist/lib/runs-control.d.ts +13 -0
  31. package/dist/lib/runs-control.js +59 -0
  32. package/dist/lib/runs-identity.d.ts +5 -0
  33. package/dist/lib/runs-identity.js +11 -0
  34. package/dist/lib/runs-index.d.ts +18 -0
  35. package/dist/lib/runs-index.js +119 -0
  36. package/dist/lib/runs-mailbox.d.ts +25 -0
  37. package/dist/lib/runs-mailbox.js +146 -0
  38. package/dist/lib/runs-messages.d.ts +15 -0
  39. package/dist/lib/runs-messages.js +179 -0
  40. package/dist/lib/runs-outbox.d.ts +41 -0
  41. package/dist/lib/runs-outbox.js +87 -0
  42. package/dist/lib/runs-process.d.ts +7 -0
  43. package/dist/lib/runs-process.js +36 -0
  44. package/dist/lib/runs-retention.d.ts +8 -0
  45. package/dist/lib/runs-retention.js +56 -0
  46. package/dist/lib/runs-start.d.ts +9 -0
  47. package/dist/lib/runs-start.js +67 -0
  48. package/dist/lib/runs-status.d.ts +8 -0
  49. package/dist/lib/runs-status.js +60 -0
  50. package/dist/lib/runtime.d.ts +2 -2
  51. package/dist/lib/runtime.js +5 -5
  52. package/dist/lib/schema.d.ts +9 -0
  53. package/dist/lib/schema.js +40 -0
  54. package/dist/lib/tools-access.d.ts +19 -0
  55. package/dist/lib/tools-access.js +44 -0
  56. package/dist/lib/tools-inspect.d.ts +11 -0
  57. package/dist/lib/tools-inspect.js +534 -0
  58. package/dist/lib/tools-local.d.ts +14 -0
  59. package/dist/lib/tools-local.js +156 -0
  60. package/dist/lib/tools-mailbox.d.ts +8 -0
  61. package/dist/lib/tools-mailbox.js +48 -0
  62. package/dist/lib/tools-message.d.ts +9 -0
  63. package/dist/lib/tools-message.js +428 -0
  64. package/dist/lib/tools-register.d.ts +18 -0
  65. package/dist/lib/tools-register.js +40 -0
  66. package/dist/lib/tools-response.d.ts +21 -0
  67. package/dist/lib/tools-response.js +219 -0
  68. package/dist/lib/tools-spawn.d.ts +12 -0
  69. package/dist/lib/tools-spawn.js +159 -0
  70. package/dist/lib/tools.d.ts +8 -39
  71. package/dist/lib/tools.js +11 -1490
  72. package/dist/recipes/utility-coordinator-lock-snapshot.json +1 -1
  73. package/dist/scripts/actor-worker.mjs +190 -7
  74. package/dist/scripts/async-runner.mjs +171 -8
  75. package/dist/scripts/build-dist.mjs +1 -1
  76. package/dist/scripts/conformance.mjs +3 -3
  77. package/dist/scripts/coordinator.mjs +733 -11
  78. package/dist/scripts/locker.mjs +345 -16
  79. package/dist/scripts/recipe-utils.mjs +478 -14
  80. package/dist/scripts/validate-recipe.mjs +110 -10
  81. package/dist/skills/actors/SKILL.md +1 -1
  82. package/dist/skills/swarm/SKILL.md +1 -1
  83. package/docs/recipe-library.md +1 -1
  84. package/docs/template-recipes.md +1 -1
  85. package/index.ts +11 -18
  86. package/lib/async-runs.ts +124 -1016
  87. package/lib/command-templates.ts +3 -3
  88. package/lib/config.ts +7 -7
  89. package/lib/{output.ts → execution-output.ts} +3 -3
  90. package/lib/execution.ts +5 -1
  91. package/lib/{actor-inspector-tui.ts → inspector.ts} +4 -3
  92. package/lib/mailbox-loop.ts +3 -3
  93. package/lib/{actor-messages.ts → messages.ts} +3 -3
  94. package/lib/{actor-recipe-context.ts → recipes-context.ts} +3 -2
  95. package/lib/{recipe-discovery.ts → recipes-discovery.ts} +19 -19
  96. package/lib/{recipe-usage.ts → recipes-usage.ts} +2 -2
  97. package/lib/registry.ts +37 -24
  98. package/lib/{actor-rooms.ts → rooms.ts} +4 -4
  99. package/lib/runs-artifacts.ts +96 -0
  100. package/lib/runs-control.ts +85 -0
  101. package/lib/runs-identity.ts +14 -0
  102. package/lib/runs-index.ts +156 -0
  103. package/lib/runs-mailbox.ts +208 -0
  104. package/lib/runs-messages.ts +252 -0
  105. package/lib/runs-outbox.ts +144 -0
  106. package/lib/runs-process.ts +51 -0
  107. package/lib/runs-retention.ts +77 -0
  108. package/lib/runs-start.ts +98 -0
  109. package/lib/runs-status.ts +78 -0
  110. package/lib/runtime.ts +6 -6
  111. package/lib/schema.ts +55 -0
  112. package/lib/tools-access.ts +68 -0
  113. package/lib/tools-inspect.ts +735 -0
  114. package/lib/tools-local.ts +234 -0
  115. package/lib/tools-mailbox.ts +56 -0
  116. package/lib/tools-message.ts +569 -0
  117. package/lib/tools-register.ts +64 -0
  118. package/lib/tools-response.ts +267 -0
  119. package/lib/tools-spawn.ts +240 -0
  120. package/lib/tools.ts +20 -2040
  121. package/package.json +1 -1
  122. package/recipes/utility-coordinator-lock-snapshot.json +1 -1
  123. package/scripts/actor-worker.mjs +190 -7
  124. package/scripts/async-runner.mjs +171 -8
  125. package/scripts/build-dist.mjs +1 -1
  126. package/scripts/conformance.mjs +3 -3
  127. package/scripts/coordinator.mjs +733 -11
  128. package/scripts/locker.mjs +345 -16
  129. package/scripts/recipe-utils.mjs +478 -14
  130. package/scripts/validate-recipe.mjs +110 -10
  131. package/skills/actors/SKILL.md +1 -1
  132. package/skills/swarm/SKILL.md +1 -1
  133. package/dist/lib/actor-worker.d.ts +0 -16
  134. package/dist/lib/actor-worker.js +0 -134
  135. package/dist/lib/async-runner.d.ts +0 -5
  136. package/dist/lib/async-runner.js +0 -134
  137. package/dist/lib/coordinator.d.ts +0 -5
  138. package/dist/lib/coordinator.js +0 -660
  139. package/dist/lib/locker.d.ts +0 -5
  140. package/dist/lib/locker.js +0 -310
  141. package/dist/lib/recipe-utils.d.ts +0 -5
  142. package/dist/lib/recipe-utils.js +0 -385
  143. package/dist/lib/validate-recipe.d.ts +0 -6
  144. package/dist/lib/validate-recipe.js +0 -104
  145. package/lib/actor-worker.ts +0 -174
  146. package/lib/async-runner.ts +0 -173
  147. package/lib/coordinator.ts +0 -742
  148. package/lib/locker.ts +0 -340
  149. package/lib/recipe-utils.ts +0 -486
  150. package/lib/validate-recipe.ts +0 -110
  151. /package/dist/lib/{recipe-references.d.ts → recipes-references.d.ts} +0 -0
  152. /package/dist/lib/{recipe-references.js → recipes-references.js} +0 -0
  153. /package/dist/lib/{recipe-usage.d.ts → recipes-usage.d.ts} +0 -0
  154. /package/dist/lib/{recipe-usage.js → recipes-usage.js} +0 -0
  155. /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 RecipeDiscovery from "./recipe-discovery.ts";
13
- import * as Tools from "./tools.ts";
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 Tools.createRuntimeToolDefinition>,
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(Tools.createRuntimeToolDefinition(cfg, deps.exec));
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 = RecipeDiscovery.discoverRecipeSources([
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 = RecipeDiscovery.toRegisteredTool(entry);
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
+ }