@kal-elsam/kairo-runtime 0.2.1 → 0.2.3
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/README.md +13 -2
- package/package.json +1 -1
- package/scripts/runtime-mvp-smoke.sh +152 -0
- package/src/cli.js +97 -9
- package/src/global/brand/index.js +10 -0
- package/src/global/dashboard-guidance.js +66 -0
- package/src/global/initial-experience.js +34 -0
- package/src/global/ink/orchestrator-app.js +372 -82
- package/src/global/ink/orchestrator-state.js +196 -65
- package/src/global/ink/run-orchestrator-ink.js +2 -0
- package/src/global/ink/run-setup-ink.js +2 -0
- package/src/global/ink/setup-app.js +8 -4
- package/src/global/ink/setup-state.js +24 -2
- package/src/global/orchestrator.js +46 -42
- package/src/global/paths.js +13 -0
- package/src/global/profile.js +17 -2
- package/src/global/runtime/execution-adapters/claude.js +65 -0
- package/src/global/runtime/execution-adapters/codex.js +78 -0
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +92 -0
- package/src/global/runtime/execution-adapters/cursor.js +104 -0
- package/src/global/runtime/execution-adapters/index.js +36 -0
- package/src/global/runtime/execution-adapters/opencode.js +38 -0
- package/src/global/runtime/run-cancel-signal.js +29 -0
- package/src/global/runtime/run-cli.js +221 -0
- package/src/global/runtime/run-events.js +144 -0
- package/src/global/runtime/run-handoff.js +71 -0
- package/src/global/runtime/run-liveness.js +28 -0
- package/src/global/runtime/run-manager.js +271 -0
- package/src/global/runtime/run-profile.js +93 -0
- package/src/global/runtime/run-redact.js +66 -0
- package/src/global/runtime/run-starting.js +13 -0
- package/src/global/runtime/run-store.js +159 -0
- package/src/global/runtime/run-supervisor-lock.js +37 -0
- package/src/global/runtime/run-supervisor-worker.js +12 -0
- package/src/global/runtime/run-supervisor.js +289 -0
- package/src/global/runtime/run-types.js +117 -0
- package/src/global/setup.js +13 -5
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { harnessHomePaths, runPaths } from "../paths.js";
|
|
4
|
+
import { RUN_STATES, isActiveRunState } from "./run-types.js";
|
|
5
|
+
import { createRunEvent } from "./run-events.js";
|
|
6
|
+
|
|
7
|
+
const writeLocks = new Map();
|
|
8
|
+
|
|
9
|
+
export function getRunsDir(homeDir) {
|
|
10
|
+
return harnessHomePaths(homeDir).runsDir;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function createRunRecord(homeDir, metadata) {
|
|
14
|
+
const { runDir, statePath } = runPaths(homeDir, metadata.runId);
|
|
15
|
+
await mkdir(runDir, { recursive: true });
|
|
16
|
+
await writeFile(statePath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
17
|
+
return metadata;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function readRunState(homeDir, runId) {
|
|
21
|
+
const { statePath } = runPaths(homeDir, runId);
|
|
22
|
+
if (!existsSync(statePath)) return null;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(await readFile(statePath, "utf8"));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(`Invalid run state at ${statePath}: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function writeRunState(homeDir, metadata) {
|
|
32
|
+
const key = metadata.runId;
|
|
33
|
+
const previous = writeLocks.get(key) ?? Promise.resolve();
|
|
34
|
+
const next = previous.then(async () => {
|
|
35
|
+
const { statePath, runDir } = runPaths(homeDir, metadata.runId);
|
|
36
|
+
await mkdir(runDir, { recursive: true });
|
|
37
|
+
await writeFile(statePath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
38
|
+
return metadata;
|
|
39
|
+
});
|
|
40
|
+
writeLocks.set(key, next.catch(() => {}));
|
|
41
|
+
return next;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function appendRunEvent(homeDir, event, { captureTranscript = false } = {}) {
|
|
45
|
+
const { eventsPath, transcriptPath } = runPaths(homeDir, event.runId);
|
|
46
|
+
await mkdir(runPaths(homeDir, event.runId).runDir, { recursive: true });
|
|
47
|
+
|
|
48
|
+
const line = `${JSON.stringify(event)}\n`;
|
|
49
|
+
await appendFile(eventsPath, line, "utf8");
|
|
50
|
+
|
|
51
|
+
if (captureTranscript && event.type === "run.transcript") {
|
|
52
|
+
await appendFile(transcriptPath, line, "utf8");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return event;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function appendRunStartedEvent(homeDir, metadata) {
|
|
59
|
+
const event = createRunEvent({
|
|
60
|
+
runId: metadata.runId,
|
|
61
|
+
type: "run.started",
|
|
62
|
+
data: {
|
|
63
|
+
agentId: metadata.agentId,
|
|
64
|
+
provider: metadata.provider,
|
|
65
|
+
model: metadata.model,
|
|
66
|
+
cwd: metadata.cwd,
|
|
67
|
+
permissions: metadata.permissions,
|
|
68
|
+
taskDigest: metadata.taskDigest,
|
|
69
|
+
taskLength: metadata.taskLength
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await appendRunEvent(homeDir, event);
|
|
74
|
+
return event;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function readRunEvents(homeDir, runId, { limit = null } = {}) {
|
|
78
|
+
const { eventsPath } = runPaths(homeDir, runId);
|
|
79
|
+
if (!existsSync(eventsPath)) return [];
|
|
80
|
+
|
|
81
|
+
const content = await readFile(eventsPath, "utf8");
|
|
82
|
+
const events = content
|
|
83
|
+
.split("\n")
|
|
84
|
+
.filter((line) => line.trim().length > 0)
|
|
85
|
+
.map((line, index) => {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(line);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { parseError: true, line: index + 1, message: error.message };
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (limit != null && limit > 0) {
|
|
94
|
+
return events.slice(-limit);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return events;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function listRunRecords(homeDir, { limit = null, activeOnly = false } = {}) {
|
|
101
|
+
const runsDir = getRunsDir(homeDir);
|
|
102
|
+
if (!existsSync(runsDir)) return [];
|
|
103
|
+
|
|
104
|
+
const entries = await readdir(runsDir, { withFileTypes: true });
|
|
105
|
+
const runs = [];
|
|
106
|
+
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (!entry.isDirectory()) continue;
|
|
109
|
+
const state = await readRunState(homeDir, entry.name);
|
|
110
|
+
if (!state) continue;
|
|
111
|
+
if (activeOnly && !isActiveRunState(state.state)) continue;
|
|
112
|
+
runs.push(state);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
runs.sort((left, right) => String(right.startedAt).localeCompare(String(left.startedAt)));
|
|
116
|
+
|
|
117
|
+
if (limit != null && limit > 0) {
|
|
118
|
+
return runs.slice(0, limit);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return runs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function markInterruptedRuns(homeDir, { exceptRunIds = [] } = {}) {
|
|
125
|
+
const except = new Set(exceptRunIds);
|
|
126
|
+
return reconcileActiveRuns(homeDir, {
|
|
127
|
+
isRunAliveImpl: async (_dir, run) => except.has(run.runId)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function reconcileActiveRuns(homeDir, { isRunAliveImpl } = {}) {
|
|
132
|
+
const activeRuns = await listRunRecords(homeDir, { activeOnly: true });
|
|
133
|
+
const interrupted = [];
|
|
134
|
+
|
|
135
|
+
for (const run of activeRuns) {
|
|
136
|
+
if (isRunAliveImpl) {
|
|
137
|
+
const alive = await isRunAliveImpl(homeDir, run);
|
|
138
|
+
if (alive) continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const next = {
|
|
142
|
+
...run,
|
|
143
|
+
state: RUN_STATES.INTERRUPTED,
|
|
144
|
+
updatedAt: new Date().toISOString(),
|
|
145
|
+
completedAt: new Date().toISOString(),
|
|
146
|
+
error: run.error ?? "Run interrupted (supervisor no longer alive)."
|
|
147
|
+
};
|
|
148
|
+
await writeRunState(homeDir, next);
|
|
149
|
+
const event = createRunEvent({
|
|
150
|
+
runId: run.runId,
|
|
151
|
+
type: "run.failed",
|
|
152
|
+
data: { reason: "interrupted", previousState: run.state }
|
|
153
|
+
});
|
|
154
|
+
await appendRunEvent(homeDir, event);
|
|
155
|
+
interrupted.push(next);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return interrupted;
|
|
159
|
+
}
|
|
@@ -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
|
+
}
|
package/src/global/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
-
import { BRAND } from "./brand/index.js";
|
|
2
|
+
import { BRAND, ONBOARDING_COPY } from "./brand/index.js";
|
|
3
3
|
import { installGlobalHarness } from "./global-installer.js";
|
|
4
4
|
import {
|
|
5
5
|
assertExplicitApplyConsent,
|
|
@@ -59,6 +59,7 @@ export async function runHarnessSetup({
|
|
|
59
59
|
confirmExplicit = false,
|
|
60
60
|
json = false,
|
|
61
61
|
simple = false,
|
|
62
|
+
onboarding = false,
|
|
62
63
|
interactive = Boolean(input.isTTY && output.isTTY),
|
|
63
64
|
createPrompt = createReadlinePrompt,
|
|
64
65
|
runSetupInkImpl = defaultRunSetupInk,
|
|
@@ -76,7 +77,7 @@ export async function runHarnessSetup({
|
|
|
76
77
|
let usedInk = false;
|
|
77
78
|
|
|
78
79
|
if (!useInk && !useWizard) {
|
|
79
|
-
printSetupIntro({ homeDir });
|
|
80
|
+
printSetupIntro({ homeDir, onboarding });
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
const setupUiArgs = {
|
|
@@ -86,6 +87,7 @@ export async function runHarnessSetup({
|
|
|
86
87
|
packageName,
|
|
87
88
|
cliVersion,
|
|
88
89
|
dryRun,
|
|
90
|
+
onboarding,
|
|
89
91
|
preflight,
|
|
90
92
|
yes,
|
|
91
93
|
confirm,
|
|
@@ -254,11 +256,17 @@ export async function runHarnessSetup({
|
|
|
254
256
|
return { cancelled: false, result, usedWizard, usedInk };
|
|
255
257
|
}
|
|
256
258
|
|
|
257
|
-
function printSetupIntro({ homeDir }) {
|
|
259
|
+
function printSetupIntro({ homeDir, onboarding = false }) {
|
|
258
260
|
const detected = detectInstalledAdapters({ homeDir });
|
|
259
261
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
+
if (onboarding) {
|
|
263
|
+
console.log(ONBOARDING_COPY.welcomeTitle);
|
|
264
|
+
console.log(ONBOARDING_COPY.purpose);
|
|
265
|
+
console.log(ONBOARDING_COPY.safety);
|
|
266
|
+
} else {
|
|
267
|
+
console.log(`${BRAND.displayName} setup — local AI ecosystem configurator`);
|
|
268
|
+
console.log("Configures and coordinates local agents. Does not install the AI apps themselves.");
|
|
269
|
+
}
|
|
262
270
|
console.log("");
|
|
263
271
|
console.log(`Detected agents: ${detected.join(", ") || "none"}`);
|
|
264
272
|
console.log(`Supported agents: ${GLOBAL_AGENT_IDS.join(", ")}`);
|