@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,144 @@
|
|
|
1
|
+
import { RUN_EVENT_TYPES } from "./run-types.js";
|
|
2
|
+
import { redactObject } from "./run-redact.js";
|
|
3
|
+
|
|
4
|
+
export function createRunEvent({
|
|
5
|
+
runId,
|
|
6
|
+
type,
|
|
7
|
+
source = "kairo",
|
|
8
|
+
data = {},
|
|
9
|
+
captureTranscript = false
|
|
10
|
+
}) {
|
|
11
|
+
return {
|
|
12
|
+
timestamp: new Date().toISOString(),
|
|
13
|
+
runId,
|
|
14
|
+
type,
|
|
15
|
+
source,
|
|
16
|
+
data: sanitizeEventData(data, { captureTranscript })
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sanitizeEventData(data, { captureTranscript = false } = {}) {
|
|
21
|
+
const allowTranscript = captureTranscript === true;
|
|
22
|
+
return redactObject(data, { allowTranscript });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeAdapterEvent(adapterId, raw, { captureTranscript = false } = {}) {
|
|
26
|
+
if (raw == null) return null;
|
|
27
|
+
|
|
28
|
+
if (typeof raw === "object" && raw.type && raw.runId) {
|
|
29
|
+
return {
|
|
30
|
+
...raw,
|
|
31
|
+
data: sanitizeEventData(raw.data ?? {}, { captureTranscript })
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof raw === "object" && raw.type) {
|
|
36
|
+
return mapStructuredEvent(adapterId, raw, { captureTranscript });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof raw === "string") {
|
|
40
|
+
return {
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
type: RUN_EVENT_TYPES.STDOUT,
|
|
43
|
+
source: adapterId,
|
|
44
|
+
data: sanitizeEventData({ line: raw }, { captureTranscript })
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mapStructuredEvent(adapterId, raw, { captureTranscript }) {
|
|
52
|
+
const base = {
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
source: adapterId,
|
|
55
|
+
data: sanitizeEventData(extractEventData(raw), { captureTranscript })
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
switch (raw.type) {
|
|
59
|
+
case "system":
|
|
60
|
+
return { ...base, type: RUN_EVENT_TYPES.SYSTEM };
|
|
61
|
+
case "assistant":
|
|
62
|
+
return { ...base, type: RUN_EVENT_TYPES.ASSISTANT };
|
|
63
|
+
case "tool_call":
|
|
64
|
+
return { ...base, type: RUN_EVENT_TYPES.TOOL_CALL };
|
|
65
|
+
case "tool_result":
|
|
66
|
+
return { ...base, type: RUN_EVENT_TYPES.TOOL_RESULT };
|
|
67
|
+
case "result":
|
|
68
|
+
return { ...base, type: RUN_EVENT_TYPES.RESULT };
|
|
69
|
+
case "token_usage":
|
|
70
|
+
case "usage":
|
|
71
|
+
return { ...base, type: RUN_EVENT_TYPES.TOKEN_USAGE };
|
|
72
|
+
case "diff":
|
|
73
|
+
case "diff_summary":
|
|
74
|
+
return { ...base, type: RUN_EVENT_TYPES.DIFF_SUMMARY };
|
|
75
|
+
default:
|
|
76
|
+
return {
|
|
77
|
+
...base,
|
|
78
|
+
type: RUN_EVENT_TYPES.SYSTEM,
|
|
79
|
+
data: sanitizeEventData({ rawType: raw.type, payload: extractEventData(raw) }, { captureTranscript })
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractEventData(raw) {
|
|
85
|
+
const { type: _type, ...rest } = raw;
|
|
86
|
+
return rest;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function applyEventToMetadata(metadata, event) {
|
|
90
|
+
const next = { ...metadata, updatedAt: event.timestamp ?? new Date().toISOString() };
|
|
91
|
+
|
|
92
|
+
switch (event.type) {
|
|
93
|
+
case RUN_EVENT_TYPES.TOOL_CALL: {
|
|
94
|
+
const tool = event.data?.tool_name ?? event.data?.name ?? event.data?.tool ?? event.data?.toolName ?? "unknown";
|
|
95
|
+
if (!next.tools.includes(tool)) {
|
|
96
|
+
next.tools = [...next.tools, tool];
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case RUN_EVENT_TYPES.STDERR:
|
|
101
|
+
case RUN_EVENT_TYPES.STDOUT: {
|
|
102
|
+
const command = event.data?.command;
|
|
103
|
+
if (command && !next.commands.includes(command)) {
|
|
104
|
+
next.commands = [...next.commands, command];
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case RUN_EVENT_TYPES.TOKEN_USAGE: {
|
|
109
|
+
next.tokenUsage = {
|
|
110
|
+
input: event.data?.inputTokens ?? event.data?.input ?? next.tokenUsage?.input ?? null,
|
|
111
|
+
output: event.data?.outputTokens ?? event.data?.output ?? next.tokenUsage?.output ?? null,
|
|
112
|
+
total: event.data?.totalTokens ?? event.data?.total ?? next.tokenUsage?.total ?? null
|
|
113
|
+
};
|
|
114
|
+
if (event.data?.cost != null) {
|
|
115
|
+
next.cost = event.data.cost;
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case RUN_EVENT_TYPES.DIFF_SUMMARY: {
|
|
120
|
+
next.diffSummary = {
|
|
121
|
+
filesChanged: event.data?.filesChanged ?? event.data?.files ?? null,
|
|
122
|
+
insertions: event.data?.insertions ?? null,
|
|
123
|
+
deletions: event.data?.deletions ?? null
|
|
124
|
+
};
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return next;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function transitionRunState(metadata, nextState, { exitCode = null, error = null } = {}) {
|
|
135
|
+
const now = new Date().toISOString();
|
|
136
|
+
return {
|
|
137
|
+
...metadata,
|
|
138
|
+
state: nextState,
|
|
139
|
+
exitCode: exitCode ?? metadata.exitCode,
|
|
140
|
+
error: error ?? metadata.error,
|
|
141
|
+
updatedAt: now,
|
|
142
|
+
completedAt: now
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { runPaths } from "../paths.js";
|
|
4
|
+
import { getRunsDir, readRunState } from "./run-store.js";
|
|
5
|
+
import { isActiveRunState } from "./run-types.js";
|
|
6
|
+
import { isWithinStartingGrace } from "./run-starting.js";
|
|
7
|
+
import { readSupervisorLock } from "./run-supervisor-lock.js";
|
|
8
|
+
|
|
9
|
+
function handoffPath(homeDir, runId) {
|
|
10
|
+
return `${runPaths(homeDir, runId).runDir}/handoff.json`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function writeRunHandoff(homeDir, runId, payload) {
|
|
14
|
+
const { runDir } = runPaths(homeDir, runId);
|
|
15
|
+
await mkdir(runDir, { recursive: true });
|
|
16
|
+
await writeFile(handoffPath(homeDir, runId), `${JSON.stringify(payload)}\n`, "utf8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function consumeRunHandoff(homeDir, runId) {
|
|
20
|
+
const path = handoffPath(homeDir, runId);
|
|
21
|
+
if (!existsSync(path)) {
|
|
22
|
+
throw new Error(`Missing run handoff for "${runId}".`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const payload = JSON.parse(await readFile(path, "utf8"));
|
|
26
|
+
await rm(path, { force: true });
|
|
27
|
+
return payload;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function deleteRunHandoff(homeDir, runId) {
|
|
31
|
+
await rm(handoffPath(homeDir, runId), { force: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function hasRunHandoff(homeDir, runId) {
|
|
35
|
+
return existsSync(handoffPath(homeDir, runId));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function cleanupStaleHandoffs(homeDir, { exceptRunIds = [], isRunAliveImpl } = {}) {
|
|
39
|
+
const except = new Set(exceptRunIds);
|
|
40
|
+
const runsDir = getRunsDir(homeDir);
|
|
41
|
+
if (!existsSync(runsDir)) return [];
|
|
42
|
+
|
|
43
|
+
const cleaned = [];
|
|
44
|
+
const entries = await readdir(runsDir, { withFileTypes: true });
|
|
45
|
+
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.isDirectory()) continue;
|
|
48
|
+
|
|
49
|
+
const runId = entry.name;
|
|
50
|
+
if (except.has(runId)) continue;
|
|
51
|
+
if (!hasRunHandoff(homeDir, runId)) continue;
|
|
52
|
+
|
|
53
|
+
const state = await readRunState(homeDir, runId);
|
|
54
|
+
const lock = await readSupervisorLock(homeDir, runId);
|
|
55
|
+
|
|
56
|
+
if (isWithinStartingGrace(state, lock)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isRunAliveImpl && state && await isRunAliveImpl(homeDir, state)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!state || !isActiveRunState(state.state)) {
|
|
65
|
+
await deleteRunHandoff(homeDir, runId);
|
|
66
|
+
cleaned.push(runId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return cleaned;
|
|
71
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function isProcessAlive(pid) {
|
|
2
|
+
if (pid == null || pid <= 0) return false;
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
process.kill(pid, 0);
|
|
6
|
+
return true;
|
|
7
|
+
} catch (error) {
|
|
8
|
+
return error && typeof error === "object" && error.code === "EPERM";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function isRunAlive(homeDir, run, { readSupervisorLockImpl } = {}) {
|
|
13
|
+
if (run.pid && isProcessAlive(run.pid)) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (readSupervisorLockImpl) {
|
|
18
|
+
const lock = await readSupervisorLockImpl(homeDir, run.runId);
|
|
19
|
+
if (lock?.agentPid && isProcessAlive(lock.agentPid)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (lock?.supervisorPid && isProcessAlive(lock.supervisorPid)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRunId, createRunMetadata, RUN_STATES } from "./run-types.js";
|
|
3
|
+
import { createRunEvent, transitionRunState } from "./run-events.js";
|
|
4
|
+
import {
|
|
5
|
+
appendRunEvent,
|
|
6
|
+
appendRunStartedEvent,
|
|
7
|
+
createRunRecord,
|
|
8
|
+
readRunState,
|
|
9
|
+
reconcileActiveRuns,
|
|
10
|
+
writeRunState
|
|
11
|
+
} from "./run-store.js";
|
|
12
|
+
import { resolveExecutionAdapter } from "./execution-adapters/index.js";
|
|
13
|
+
import { resolveProfileAgents } from "../profile.js";
|
|
14
|
+
import { resolveRuntimeOptions } from "./run-profile.js";
|
|
15
|
+
import { cleanupStaleHandoffs, deleteRunHandoff, writeRunHandoff } from "./run-handoff.js";
|
|
16
|
+
import { isRunAlive } from "./run-liveness.js";
|
|
17
|
+
import { readSupervisorLock, writeSupervisorLock } from "./run-supervisor-lock.js";
|
|
18
|
+
import { writeCancelSignal } from "./run-cancel-signal.js";
|
|
19
|
+
import { isWithinStartingGrace } from "./run-starting.js";
|
|
20
|
+
import {
|
|
21
|
+
forkDetachedSupervisor,
|
|
22
|
+
readSupervisorLockForRun,
|
|
23
|
+
supervisePreparedRun
|
|
24
|
+
} from "./run-supervisor.js";
|
|
25
|
+
|
|
26
|
+
const activeProcesses = new Map();
|
|
27
|
+
const cancelledRuns = new Set();
|
|
28
|
+
|
|
29
|
+
export function getActiveProcess(runId) {
|
|
30
|
+
return activeProcesses.get(runId) ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function listActiveRunIds() {
|
|
34
|
+
return [...activeProcesses.keys()];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function isRunSupervisedAlive(homeDir, run) {
|
|
38
|
+
if (listActiveRunIds().includes(run.runId)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const lock = await readSupervisorLock(homeDir, run.runId);
|
|
43
|
+
if (isWithinStartingGrace(run, lock)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return isRunAlive(homeDir, run, { readSupervisorLockImpl: readSupervisorLockForRun });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function recoverRuns(homeDir) {
|
|
51
|
+
const interrupted = await reconcileActiveRuns(homeDir, {
|
|
52
|
+
isRunAliveImpl: isRunSupervisedAlive
|
|
53
|
+
});
|
|
54
|
+
await cleanupStaleHandoffs(homeDir, {
|
|
55
|
+
exceptRunIds: listActiveRunIds(),
|
|
56
|
+
isRunAliveImpl: isRunSupervisedAlive
|
|
57
|
+
});
|
|
58
|
+
return interrupted;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function prepareRun({
|
|
62
|
+
homeDir,
|
|
63
|
+
agentId,
|
|
64
|
+
task,
|
|
65
|
+
cwd,
|
|
66
|
+
model = null,
|
|
67
|
+
permissions = [],
|
|
68
|
+
captureTranscript = false,
|
|
69
|
+
cliVersion,
|
|
70
|
+
profile = null
|
|
71
|
+
}) {
|
|
72
|
+
const adapter = resolveExecutionAdapter(agentId);
|
|
73
|
+
const availability = adapter.availability({ cwd });
|
|
74
|
+
|
|
75
|
+
if (!availability.available) {
|
|
76
|
+
throw new Error(availability.reason ?? `${adapter.label} is not available.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!availability.launchable) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
availability.reason
|
|
82
|
+
?? `${adapter.label} is not launchable for auditable Kairo runs in v1.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const runId = createRunId();
|
|
87
|
+
const metadata = createRunMetadata({
|
|
88
|
+
runId,
|
|
89
|
+
agentId,
|
|
90
|
+
provider: adapter.label,
|
|
91
|
+
model,
|
|
92
|
+
task,
|
|
93
|
+
cwd,
|
|
94
|
+
permissions,
|
|
95
|
+
captureTranscript,
|
|
96
|
+
cliVersion,
|
|
97
|
+
profileSources: profile?.sources ?? null
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await createRunRecord(homeDir, metadata);
|
|
101
|
+
await appendRunStartedEvent(homeDir, metadata);
|
|
102
|
+
await writeRunHandoff(homeDir, runId, {
|
|
103
|
+
agentId,
|
|
104
|
+
task,
|
|
105
|
+
cwd,
|
|
106
|
+
model,
|
|
107
|
+
permissions,
|
|
108
|
+
captureTranscript,
|
|
109
|
+
cliVersion,
|
|
110
|
+
profile: profile?.profile ?? null
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return { runId, metadata };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function startRun({
|
|
117
|
+
homeDir,
|
|
118
|
+
agentId,
|
|
119
|
+
task,
|
|
120
|
+
cwd,
|
|
121
|
+
model = null,
|
|
122
|
+
permissions = [],
|
|
123
|
+
captureTranscript = false,
|
|
124
|
+
cliVersion,
|
|
125
|
+
profile = null,
|
|
126
|
+
follow = false,
|
|
127
|
+
timeoutMs = null,
|
|
128
|
+
wait = true,
|
|
129
|
+
spawnImpl = spawn,
|
|
130
|
+
forkDetachedSupervisorImpl = forkDetachedSupervisor
|
|
131
|
+
}) {
|
|
132
|
+
const { runId, metadata } = await prepareRun({
|
|
133
|
+
homeDir,
|
|
134
|
+
agentId,
|
|
135
|
+
task,
|
|
136
|
+
cwd,
|
|
137
|
+
model,
|
|
138
|
+
permissions,
|
|
139
|
+
captureTranscript,
|
|
140
|
+
cliVersion,
|
|
141
|
+
profile
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!wait) {
|
|
145
|
+
const startingAt = new Date().toISOString();
|
|
146
|
+
await writeRunState(homeDir, {
|
|
147
|
+
...metadata,
|
|
148
|
+
state: RUN_STATES.STARTING,
|
|
149
|
+
updatedAt: startingAt
|
|
150
|
+
});
|
|
151
|
+
await writeSupervisorLock(homeDir, runId, {
|
|
152
|
+
startingAt,
|
|
153
|
+
supervisorPid: null,
|
|
154
|
+
agentPid: null,
|
|
155
|
+
lastHeartbeat: startingAt
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
let supervisorPid;
|
|
159
|
+
try {
|
|
160
|
+
supervisorPid = forkDetachedSupervisorImpl({ homeDir, runId });
|
|
161
|
+
if (!supervisorPid) {
|
|
162
|
+
throw new Error("Failed to start detached supervisor.");
|
|
163
|
+
}
|
|
164
|
+
await writeSupervisorLock(homeDir, runId, {
|
|
165
|
+
startingAt,
|
|
166
|
+
supervisorPid,
|
|
167
|
+
agentPid: null,
|
|
168
|
+
startedAt: startingAt,
|
|
169
|
+
lastHeartbeat: startingAt
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
await deleteRunHandoff(homeDir, runId);
|
|
173
|
+
const failed = transitionRunState(
|
|
174
|
+
{ ...metadata, state: RUN_STATES.STARTING },
|
|
175
|
+
RUN_STATES.FAILED,
|
|
176
|
+
{ error: error instanceof Error ? error.message : String(error) }
|
|
177
|
+
);
|
|
178
|
+
await writeRunState(homeDir, failed);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const detachedMetadata = {
|
|
183
|
+
...metadata,
|
|
184
|
+
state: RUN_STATES.STARTING,
|
|
185
|
+
supervisorPid,
|
|
186
|
+
updatedAt: startingAt
|
|
187
|
+
};
|
|
188
|
+
await writeRunState(homeDir, detachedMetadata);
|
|
189
|
+
return {
|
|
190
|
+
runId,
|
|
191
|
+
metadata: detachedMetadata,
|
|
192
|
+
completion: null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const completion = supervisePreparedRun({
|
|
197
|
+
homeDir,
|
|
198
|
+
runId,
|
|
199
|
+
follow,
|
|
200
|
+
timeoutMs,
|
|
201
|
+
spawnImpl,
|
|
202
|
+
cancelledRuns,
|
|
203
|
+
activeProcesses
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
runId,
|
|
208
|
+
metadata,
|
|
209
|
+
completion
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function stopRun(homeDir, runId, { signal = "SIGTERM" } = {}) {
|
|
214
|
+
const state = await readRunState(homeDir, runId);
|
|
215
|
+
if (!state) {
|
|
216
|
+
throw new Error(`Run "${runId}" not found.`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (state.state === RUN_STATES.COMPLETED || state.state === RUN_STATES.FAILED || state.state === RUN_STATES.CANCELLED) {
|
|
220
|
+
return state;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const lock = await readSupervisorLock(homeDir, runId);
|
|
224
|
+
const child = activeProcesses.get(runId);
|
|
225
|
+
|
|
226
|
+
if (child) {
|
|
227
|
+
child.kill(signal);
|
|
228
|
+
} else {
|
|
229
|
+
const targets = [lock?.agentPid, state.pid, lock?.supervisorPid].filter(Boolean);
|
|
230
|
+
for (const pid of targets) {
|
|
231
|
+
try {
|
|
232
|
+
process.kill(pid, signal);
|
|
233
|
+
} catch {
|
|
234
|
+
// Process may already be gone.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await writeCancelSignal(homeDir, runId, {
|
|
240
|
+
requested: true,
|
|
241
|
+
signal,
|
|
242
|
+
requestedAt: new Date().toISOString()
|
|
243
|
+
});
|
|
244
|
+
await deleteRunHandoff(homeDir, runId);
|
|
245
|
+
|
|
246
|
+
const metadata = transitionRunState(state, RUN_STATES.CANCELLED, {
|
|
247
|
+
error: "Run cancelled by user."
|
|
248
|
+
});
|
|
249
|
+
cancelledRuns.add(runId);
|
|
250
|
+
await writeRunState(homeDir, metadata);
|
|
251
|
+
await appendRunEvent(homeDir, createRunEvent({
|
|
252
|
+
runId,
|
|
253
|
+
type: "run.cancelled",
|
|
254
|
+
data: { signal }
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
activeProcesses.delete(runId);
|
|
258
|
+
return metadata;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function resolveRunAgent(profile, requestedAgent, detectedAgentIds = []) {
|
|
262
|
+
const agents = resolveProfileAgents(profile?.profile ?? profile, detectedAgentIds);
|
|
263
|
+
const runtime = resolveRuntimeOptions(profile, { agentId: requestedAgent });
|
|
264
|
+
const agentId = runtime.agentId ?? agents[0] ?? null;
|
|
265
|
+
|
|
266
|
+
if (!agentId) {
|
|
267
|
+
throw new Error("No agent specified and no default agent available.");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { agentId, runtime };
|
|
271
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { resolveProfileAgents } from "../profile.js";
|
|
2
|
+
import { EXECUTION_ADAPTER_IDS } from "./execution-adapters/index.js";
|
|
3
|
+
|
|
4
|
+
export function resolveRuntimeOptions(profileResolved, overrides = {}) {
|
|
5
|
+
const profile = profileResolved?.profile ?? profileResolved ?? {};
|
|
6
|
+
const agentAliases = profile.agentAliases ?? {};
|
|
7
|
+
const modelAliases = profile.modelAliases ?? {};
|
|
8
|
+
|
|
9
|
+
let agentId = overrides.agentId ?? profile.defaultRuntimeAgent ?? null;
|
|
10
|
+
if (agentId && agentAliases[agentId]) {
|
|
11
|
+
agentId = agentAliases[agentId];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let model = overrides.model ?? profile.preferredModel ?? profile.defaultRuntimeModel ?? null;
|
|
15
|
+
if (model && modelAliases[model]) {
|
|
16
|
+
model = modelAliases[model];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const permissions = overrides.permissions
|
|
20
|
+
?? profile.defaultPermissions
|
|
21
|
+
?? [];
|
|
22
|
+
|
|
23
|
+
const captureTranscript = overrides.captureTranscript
|
|
24
|
+
?? profile.captureTranscript
|
|
25
|
+
?? false;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
agentId,
|
|
29
|
+
model,
|
|
30
|
+
permissions: Array.isArray(permissions) ? permissions : [],
|
|
31
|
+
captureTranscript: captureTranscript === true
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateRuntimeProfile(profile) {
|
|
36
|
+
const agentAliases = profile.agentAliases;
|
|
37
|
+
if (agentAliases != null) {
|
|
38
|
+
if (typeof agentAliases !== "object" || Array.isArray(agentAliases)) {
|
|
39
|
+
throw new Error("Profile agentAliases must be an object.");
|
|
40
|
+
}
|
|
41
|
+
for (const target of Object.values(agentAliases)) {
|
|
42
|
+
if (!EXECUTION_ADAPTER_IDS.includes(target)) {
|
|
43
|
+
throw new Error(`Unknown agent alias target "${target}".`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const modelAliases = profile.modelAliases;
|
|
49
|
+
if (modelAliases != null) {
|
|
50
|
+
if (typeof modelAliases !== "object" || Array.isArray(modelAliases)) {
|
|
51
|
+
throw new Error("Profile modelAliases must be an object.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const defaultPermissions = profile.defaultPermissions;
|
|
56
|
+
if (defaultPermissions != null && !Array.isArray(defaultPermissions)) {
|
|
57
|
+
throw new Error("Profile defaultPermissions must be an array.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
profile.defaultRuntimeAgent != null
|
|
62
|
+
&& !EXECUTION_ADAPTER_IDS.includes(profile.defaultRuntimeAgent)
|
|
63
|
+
&& !Object.keys(agentAliases ?? {}).includes(profile.defaultRuntimeAgent)
|
|
64
|
+
) {
|
|
65
|
+
throw new Error(`Unknown defaultRuntimeAgent "${profile.defaultRuntimeAgent}".`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (profile.captureTranscript != null && typeof profile.captureTranscript !== "boolean") {
|
|
69
|
+
throw new Error("Profile captureTranscript must be a boolean.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveAgentFromProfile(profileResolved, requestedAgent) {
|
|
74
|
+
const runtime = resolveRuntimeOptions(profileResolved, { agentId: requestedAgent });
|
|
75
|
+
if (!runtime.agentId) {
|
|
76
|
+
throw new Error(`Unknown or missing agent. Use ${EXECUTION_ADAPTER_IDS.join(", ")}.`);
|
|
77
|
+
}
|
|
78
|
+
if (!EXECUTION_ADAPTER_IDS.includes(runtime.agentId)) {
|
|
79
|
+
throw new Error(`Agent "${runtime.agentId}" is not supported for Kairo runs.`);
|
|
80
|
+
}
|
|
81
|
+
return runtime;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const RUNTIME_PROFILE_KEYS = new Set([
|
|
85
|
+
"agentAliases",
|
|
86
|
+
"modelAliases",
|
|
87
|
+
"defaultPermissions",
|
|
88
|
+
"defaultRuntimeAgent",
|
|
89
|
+
"defaultRuntimeModel",
|
|
90
|
+
"captureTranscript"
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
export const SUPPORTED_RUN_AGENTS = [...EXECUTION_ADAPTER_IDS];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isForbiddenSecretKey, normalizeProfileKey } from "../profile.js";
|
|
2
|
+
|
|
3
|
+
const REDACTED = "[REDACTED]";
|
|
4
|
+
|
|
5
|
+
const SECRET_VALUE_PATTERN = /^(sk-[A-Za-z0-9]|sk-or-|gh[pousr]_|xox[baprs]-|AKIA[0-9A-Z]{16}\b|Bearer\s+\S+|eyJ[A-Za-z0-9_-]+\.)|-----BEGIN [A-Z ]*PRIVATE KEY-----/i;
|
|
6
|
+
|
|
7
|
+
const SENSITIVE_ENV_KEYS = new Set([
|
|
8
|
+
"ANTHROPIC_API_KEY",
|
|
9
|
+
"OPENAI_API_KEY",
|
|
10
|
+
"CURSOR_API_KEY",
|
|
11
|
+
"OPENROUTER_API_KEY",
|
|
12
|
+
"GITHUB_TOKEN",
|
|
13
|
+
"GH_TOKEN",
|
|
14
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
15
|
+
"AWS_ACCESS_KEY_ID"
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function redactString(value) {
|
|
19
|
+
if (typeof value !== "string" || value.length === 0) return value;
|
|
20
|
+
if (SECRET_VALUE_PATTERN.test(value)) return REDACTED;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function redactObject(value, { allowTranscript = false } = {}) {
|
|
25
|
+
if (value == null) return value;
|
|
26
|
+
if (typeof value === "string") return redactString(value);
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((item) => redactObject(item, { allowTranscript }));
|
|
29
|
+
}
|
|
30
|
+
if (typeof value !== "object") return value;
|
|
31
|
+
|
|
32
|
+
const result = {};
|
|
33
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
34
|
+
const normalizedKey = normalizeProfileKey(key);
|
|
35
|
+
|
|
36
|
+
if (!allowTranscript && (normalizedKey === "prompt" || normalizedKey === "response" || normalizedKey === "content")) {
|
|
37
|
+
result[key] = REDACTED;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isForbiddenSecretKey(key) || SENSITIVE_ENV_KEYS.has(key)) {
|
|
42
|
+
result[key] = REDACTED;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
result[key] = redactObject(nested, { allowTranscript });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function redactEnv(env = process.env) {
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const [key, value] of Object.entries(env)) {
|
|
55
|
+
if (SENSITIVE_ENV_KEYS.has(key) || isForbiddenSecretKey(key)) {
|
|
56
|
+
result[key] = REDACTED;
|
|
57
|
+
} else {
|
|
58
|
+
result[key] = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function shouldPersistTranscript(captureTranscript) {
|
|
65
|
+
return captureTranscript === true;
|
|
66
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RUN_STATES, STARTING_GRACE_MS } from "./run-types.js";
|
|
2
|
+
|
|
3
|
+
export function isWithinStartingGrace(run, lock, nowMs = Date.now()) {
|
|
4
|
+
if (run?.state !== RUN_STATES.STARTING && run?.state !== RUN_STATES.PENDING) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const anchor = lock?.startingAt ?? lock?.startedAt ?? run?.startedAt;
|
|
9
|
+
if (!anchor) return false;
|
|
10
|
+
|
|
11
|
+
const elapsed = nowMs - new Date(anchor).getTime();
|
|
12
|
+
return elapsed >= 0 && elapsed < STARTING_GRACE_MS;
|
|
13
|
+
}
|