@kal-elsam/kairo-runtime 0.2.1 → 0.2.2

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.
@@ -0,0 +1,37 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { runPaths } from "../paths.js";
4
+
5
+ function lockPath(homeDir, runId) {
6
+ return `${runPaths(homeDir, runId).runDir}/supervisor.lock.json`;
7
+ }
8
+
9
+ export async function writeSupervisorLock(homeDir, runId, lock) {
10
+ const { runDir } = runPaths(homeDir, runId);
11
+ await mkdir(runDir, { recursive: true });
12
+ await writeFile(lockPath(homeDir, runId), `${JSON.stringify(lock, null, 2)}\n`, "utf8");
13
+ }
14
+
15
+ export async function readSupervisorLock(homeDir, runId) {
16
+ const path = lockPath(homeDir, runId);
17
+ if (!existsSync(path)) return null;
18
+
19
+ try {
20
+ return JSON.parse(await readFile(path, "utf8"));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ export async function touchSupervisorLock(homeDir, runId, fields = {}) {
27
+ const current = await readSupervisorLock(homeDir, runId);
28
+ if (!current) return null;
29
+
30
+ const next = {
31
+ ...current,
32
+ ...fields,
33
+ lastHeartbeat: new Date().toISOString()
34
+ };
35
+ await writeSupervisorLock(homeDir, runId, next);
36
+ return next;
37
+ }
@@ -0,0 +1,12 @@
1
+ import { supervisePreparedRun } from "./run-supervisor.js";
2
+ import { deleteRunHandoff } from "./run-handoff.js";
3
+
4
+ const homeDir = process.env.KAIRO_SUPERVISOR_HOME;
5
+ const runId = process.env.KAIRO_SUPERVISOR_RUN_ID;
6
+
7
+ if (homeDir && runId) {
8
+ supervisePreparedRun({ homeDir, runId }).catch(async () => {
9
+ await deleteRunHandoff(homeDir, runId).catch(() => {});
10
+ process.exitCode = 1;
11
+ });
12
+ }
@@ -0,0 +1,289 @@
1
+ import { spawn } from "node:child_process";
2
+ import process from "node:process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { RUN_STATES } from "./run-types.js";
5
+ import {
6
+ applyEventToMetadata,
7
+ createRunEvent,
8
+ normalizeAdapterEvent,
9
+ transitionRunState
10
+ } from "./run-events.js";
11
+ import {
12
+ appendRunEvent,
13
+ readRunState,
14
+ writeRunState
15
+ } from "./run-store.js";
16
+ import { resolveExecutionAdapter } from "./execution-adapters/index.js";
17
+ import { consumeRunHandoff } from "./run-handoff.js";
18
+ import { isRunCancelRequested } from "./run-cancel-signal.js";
19
+ import { readSupervisorLock, touchSupervisorLock, writeSupervisorLock } from "./run-supervisor-lock.js";
20
+ import { shouldPersistTranscript } from "./run-redact.js";
21
+
22
+ async function shouldPreserveCancelledState(homeDir, runId) {
23
+ const fresh = await readRunState(homeDir, runId);
24
+ if (fresh?.state === RUN_STATES.CANCELLED) {
25
+ return fresh;
26
+ }
27
+ if (await isRunCancelRequested(homeDir, runId)) {
28
+ return fresh ?? null;
29
+ }
30
+ return null;
31
+ }
32
+
33
+ const workerPath = fileURLToPath(new URL("./run-supervisor-worker.js", import.meta.url));
34
+
35
+ export function spawnDetachedSupervisor({ homeDir, runId, spawnImpl = spawn }) {
36
+ const child = spawnImpl(process.execPath, [workerPath], {
37
+ env: {
38
+ ...process.env,
39
+ KAIRO_SUPERVISOR_HOME: homeDir,
40
+ KAIRO_SUPERVISOR_RUN_ID: runId
41
+ },
42
+ detached: true,
43
+ stdio: "ignore"
44
+ });
45
+ child.unref();
46
+ return child.pid ?? null;
47
+ }
48
+
49
+ export const forkDetachedSupervisor = spawnDetachedSupervisor;
50
+
51
+ export async function supervisePreparedRun({
52
+ homeDir,
53
+ runId,
54
+ follow = false,
55
+ timeoutMs = null,
56
+ spawnImpl = spawn,
57
+ cancelledRuns = null,
58
+ activeProcesses = null
59
+ }) {
60
+ const handoff = await consumeRunHandoff(homeDir, runId);
61
+ const adapter = resolveExecutionAdapter(handoff.agentId);
62
+ let metadata = await readRunState(homeDir, runId);
63
+
64
+ if (!metadata) {
65
+ throw new Error(`Run "${runId}" not found.`);
66
+ }
67
+
68
+ const captureTranscript = handoff.captureTranscript === true;
69
+ const launch = adapter.buildLaunch({
70
+ task: handoff.task,
71
+ cwd: handoff.cwd,
72
+ model: handoff.model,
73
+ permissions: handoff.permissions ?? [],
74
+ profile: handoff.profile ?? null
75
+ });
76
+
77
+ metadata = {
78
+ ...metadata,
79
+ state: RUN_STATES.RUNNING,
80
+ supervisorPid: process.pid,
81
+ updatedAt: new Date().toISOString()
82
+ };
83
+ await writeRunState(homeDir, metadata);
84
+ await writeSupervisorLock(homeDir, runId, {
85
+ supervisorPid: process.pid,
86
+ agentPid: null,
87
+ startedAt: new Date().toISOString(),
88
+ lastHeartbeat: new Date().toISOString()
89
+ });
90
+
91
+ const child = spawnImpl(launch.command, launch.args, {
92
+ cwd: launch.cwd,
93
+ env: launch.env,
94
+ stdio: ["ignore", "pipe", "pipe"]
95
+ });
96
+ activeProcesses?.set(runId, child);
97
+
98
+ let stdoutBuffer = "";
99
+ let stderrBuffer = "";
100
+ let timeoutHandle = null;
101
+ let processing = Promise.resolve();
102
+ let stateWrites = Promise.resolve();
103
+
104
+ const enqueue = (work) => {
105
+ processing = processing.then(work);
106
+ };
107
+
108
+ const serializeStateWrite = (work) => {
109
+ stateWrites = stateWrites.then(work);
110
+ return stateWrites;
111
+ };
112
+
113
+ const persistEvent = async (event) => {
114
+ await serializeStateWrite(async () => {
115
+ metadata = applyEventToMetadata(metadata, event);
116
+ await appendRunEvent(homeDir, event, { captureTranscript: shouldPersistTranscript(captureTranscript) });
117
+ await writeRunState(homeDir, metadata);
118
+ });
119
+ };
120
+
121
+ const handleLine = async (line, stream) => {
122
+ const structured = adapter.parseEventLine(line, { runId, cwd: handoff.cwd });
123
+ const normalized = structured
124
+ ? normalizeAdapterEvent(adapter.id, structured, { captureTranscript })
125
+ : normalizeAdapterEvent(adapter.id, line, { captureTranscript });
126
+
127
+ if (!normalized) return;
128
+
129
+ const event = {
130
+ ...normalized,
131
+ runId,
132
+ timestamp: normalized.timestamp ?? new Date().toISOString()
133
+ };
134
+
135
+ if (captureTranscript && (event.type === "agent.assistant" || event.type === "agent.result")) {
136
+ await persistEvent({
137
+ ...createRunEvent({
138
+ runId,
139
+ type: "run.transcript",
140
+ data: event.data,
141
+ captureTranscript: true
142
+ }),
143
+ runId,
144
+ timestamp: event.timestamp
145
+ });
146
+ }
147
+
148
+ await persistEvent(event);
149
+
150
+ if (follow && stream === "stdout") {
151
+ process.stdout.write(`${line}\n`);
152
+ }
153
+ if (follow && stream === "stderr") {
154
+ process.stderr.write(`${line}\n`);
155
+ }
156
+ };
157
+
158
+ const flushBuffer = async (buffer, stream) => {
159
+ const lines = buffer.split("\n");
160
+ const remainder = lines.pop() ?? "";
161
+ for (const line of lines) {
162
+ await handleLine(line, stream);
163
+ }
164
+ return remainder;
165
+ };
166
+
167
+ const completion = new Promise((resolve, reject) => {
168
+ child.on("error", async (error) => {
169
+ if (timeoutHandle) clearTimeout(timeoutHandle);
170
+ activeProcesses?.delete(runId);
171
+
172
+ await serializeStateWrite(async () => {
173
+ const preserved = await shouldPreserveCancelledState(homeDir, runId);
174
+ if (preserved) {
175
+ cancelledRuns?.delete(runId);
176
+ resolve(preserved.state === RUN_STATES.CANCELLED
177
+ ? preserved
178
+ : { ...preserved, state: RUN_STATES.CANCELLED });
179
+ return;
180
+ }
181
+
182
+ metadata = transitionRunState(metadata, RUN_STATES.FAILED, {
183
+ error: error.message
184
+ });
185
+ await writeRunState(homeDir, metadata);
186
+ await appendRunEvent(homeDir, createRunEvent({
187
+ runId,
188
+ type: "run.failed",
189
+ data: { error: error.message }
190
+ }), { captureTranscript: shouldPersistTranscript(captureTranscript) });
191
+ });
192
+ reject(error);
193
+ });
194
+
195
+ child.on("close", async (exitCode) => {
196
+ try {
197
+ if (timeoutHandle) clearTimeout(timeoutHandle);
198
+ activeProcesses?.delete(runId);
199
+
200
+ await processing;
201
+ await stateWrites;
202
+
203
+ if (stdoutBuffer.trim()) {
204
+ await handleLine(stdoutBuffer.trim(), "stdout");
205
+ stdoutBuffer = "";
206
+ }
207
+ if (stderrBuffer.trim()) {
208
+ await handleLine(stderrBuffer.trim(), "stderr");
209
+ stderrBuffer = "";
210
+ }
211
+
212
+ await processing;
213
+ await stateWrites;
214
+
215
+ await serializeStateWrite(async () => {
216
+ const preserved = await shouldPreserveCancelledState(homeDir, runId);
217
+ if (preserved) {
218
+ cancelledRuns?.delete(runId);
219
+ resolve(preserved.state === RUN_STATES.CANCELLED
220
+ ? preserved
221
+ : { ...preserved, state: RUN_STATES.CANCELLED });
222
+ return;
223
+ }
224
+
225
+ if (cancelledRuns?.has(runId)) {
226
+ cancelledRuns.delete(runId);
227
+ try {
228
+ resolve(await readRunState(homeDir, runId));
229
+ } catch {
230
+ resolve({ ...metadata, state: RUN_STATES.CANCELLED });
231
+ }
232
+ return;
233
+ }
234
+
235
+ const failed = exitCode !== 0;
236
+ const nextState = failed ? RUN_STATES.FAILED : RUN_STATES.COMPLETED;
237
+ metadata = transitionRunState(metadata, nextState, {
238
+ exitCode,
239
+ error: failed ? `Process exited with code ${exitCode}` : null
240
+ });
241
+ await writeRunState(homeDir, metadata);
242
+ await appendRunEvent(homeDir, createRunEvent({
243
+ runId,
244
+ type: failed ? "run.failed" : "run.completed",
245
+ data: { exitCode }
246
+ }), { captureTranscript: shouldPersistTranscript(captureTranscript) });
247
+ resolve(metadata);
248
+ });
249
+ } catch (error) {
250
+ reject(error);
251
+ }
252
+ });
253
+ });
254
+
255
+ child.stdout.on("data", (chunk) => {
256
+ enqueue(async () => {
257
+ stdoutBuffer += chunk.toString();
258
+ stdoutBuffer = await flushBuffer(stdoutBuffer, "stdout");
259
+ });
260
+ });
261
+
262
+ child.stderr.on("data", (chunk) => {
263
+ enqueue(async () => {
264
+ stderrBuffer += chunk.toString();
265
+ stderrBuffer = await flushBuffer(stderrBuffer, "stderr");
266
+ });
267
+ });
268
+
269
+ if (timeoutMs != null && timeoutMs > 0) {
270
+ timeoutHandle = setTimeout(() => {
271
+ child.kill("SIGTERM");
272
+ }, timeoutMs);
273
+ }
274
+
275
+ void serializeStateWrite(async () => {
276
+ metadata = {
277
+ ...metadata,
278
+ pid: child.pid ?? null
279
+ };
280
+ await writeRunState(homeDir, metadata);
281
+ await touchSupervisorLock(homeDir, runId, { agentPid: child.pid ?? null });
282
+ });
283
+
284
+ return completion;
285
+ }
286
+
287
+ export async function readSupervisorLockForRun(homeDir, runId) {
288
+ return readSupervisorLock(homeDir, runId);
289
+ }
@@ -0,0 +1,117 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export const RUN_STATES = {
4
+ PENDING: "pending",
5
+ STARTING: "starting",
6
+ RUNNING: "running",
7
+ COMPLETED: "completed",
8
+ FAILED: "failed",
9
+ CANCELLED: "cancelled",
10
+ INTERRUPTED: "interrupted"
11
+ };
12
+
13
+ export const STARTING_GRACE_MS = 30_000;
14
+
15
+ export const RUN_EVENT_TYPES = {
16
+ RUN_STARTED: "run.started",
17
+ RUN_COMPLETED: "run.completed",
18
+ RUN_FAILED: "run.failed",
19
+ RUN_CANCELLED: "run.cancelled",
20
+ STDOUT: "process.stdout",
21
+ STDERR: "process.stderr",
22
+ TOOL_CALL: "agent.tool_call",
23
+ TOOL_RESULT: "agent.tool_result",
24
+ ASSISTANT: "agent.assistant",
25
+ TOKEN_USAGE: "agent.token_usage",
26
+ DIFF_SUMMARY: "agent.diff_summary",
27
+ SYSTEM: "agent.system",
28
+ RESULT: "agent.result",
29
+ TRANSCRIPT: "run.transcript"
30
+ };
31
+
32
+ export const TERMINAL_RUN_STATES = new Set([
33
+ RUN_STATES.COMPLETED,
34
+ RUN_STATES.FAILED,
35
+ RUN_STATES.CANCELLED,
36
+ RUN_STATES.INTERRUPTED
37
+ ]);
38
+
39
+ export const ACTIVE_RUN_STATES = new Set([
40
+ RUN_STATES.PENDING,
41
+ RUN_STATES.STARTING,
42
+ RUN_STATES.RUNNING
43
+ ]);
44
+
45
+ export function isTerminalRunState(state) {
46
+ return TERMINAL_RUN_STATES.has(state);
47
+ }
48
+
49
+ export function isActiveRunState(state) {
50
+ return state === RUN_STATES.PENDING
51
+ || state === RUN_STATES.STARTING
52
+ || state === RUN_STATES.RUNNING;
53
+ }
54
+
55
+ export function createRunId() {
56
+ const timestamp = Date.now().toString(36);
57
+ const random = Math.random().toString(36).slice(2, 8);
58
+ return `run_${timestamp}_${random}`;
59
+ }
60
+
61
+ export function createTaskFingerprint(task) {
62
+ const normalized = String(task ?? "").replace(/\s+/g, " ").trim();
63
+ const digest = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
64
+ return {
65
+ taskDigest: digest,
66
+ taskLength: normalized.length
67
+ };
68
+ }
69
+
70
+ export function formatTaskLabel(metadata) {
71
+ const digest = metadata?.taskDigest ?? "unknown";
72
+ const length = metadata?.taskLength ?? 0;
73
+ return `task:${digest} (${length} chars, content not stored)`;
74
+ }
75
+
76
+ export function createRunMetadata({
77
+ runId,
78
+ agentId,
79
+ provider,
80
+ model = null,
81
+ task,
82
+ cwd,
83
+ permissions = [],
84
+ captureTranscript = false,
85
+ cliVersion,
86
+ profileSources = null
87
+ }) {
88
+ const { taskDigest, taskLength } = createTaskFingerprint(task);
89
+ const now = new Date().toISOString();
90
+
91
+ return {
92
+ runId,
93
+ agentId,
94
+ provider,
95
+ model,
96
+ taskDigest,
97
+ taskLength,
98
+ cwd,
99
+ permissions,
100
+ captureTranscript,
101
+ cliVersion,
102
+ profileSources,
103
+ state: RUN_STATES.PENDING,
104
+ pid: null,
105
+ supervisorPid: null,
106
+ exitCode: null,
107
+ startedAt: now,
108
+ updatedAt: now,
109
+ completedAt: null,
110
+ tools: [],
111
+ commands: [],
112
+ tokenUsage: null,
113
+ cost: null,
114
+ diffSummary: null,
115
+ error: null
116
+ };
117
+ }