@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.
- package/package.json +1 -1
- package/scripts/runtime-mvp-smoke.sh +152 -0
- package/src/cli.js +77 -4
- package/src/global/ink/orchestrator-app.js +355 -81
- package/src/global/ink/orchestrator-state.js +196 -65
- package/src/global/orchestrator.js +8 -39
- 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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { isExecutableAvailable } from "../../cli-probe.js";
|
|
2
|
+
|
|
3
|
+
export function createExecutionAdapter({
|
|
4
|
+
id,
|
|
5
|
+
label,
|
|
6
|
+
executable,
|
|
7
|
+
capabilities,
|
|
8
|
+
buildLaunch,
|
|
9
|
+
parseEventLine = null,
|
|
10
|
+
checkAvailability = null,
|
|
11
|
+
launchable = null
|
|
12
|
+
}) {
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
label,
|
|
16
|
+
executable,
|
|
17
|
+
capabilities: {
|
|
18
|
+
structuredEvents: false,
|
|
19
|
+
tokens: false,
|
|
20
|
+
diff: false,
|
|
21
|
+
cancel: true,
|
|
22
|
+
transcript: false,
|
|
23
|
+
...capabilities
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
availability(context = {}) {
|
|
27
|
+
if (checkAvailability) {
|
|
28
|
+
return checkAvailability(context);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const available = isExecutableAvailable(executable, { env: context.env ?? process.env });
|
|
32
|
+
if (!available) {
|
|
33
|
+
return {
|
|
34
|
+
available: false,
|
|
35
|
+
compatible: false,
|
|
36
|
+
launchable: false,
|
|
37
|
+
reason: `${label} CLI "${executable}" is not on PATH.`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!capabilities.structuredEvents) {
|
|
42
|
+
return {
|
|
43
|
+
available: true,
|
|
44
|
+
compatible: false,
|
|
45
|
+
launchable: launchable ?? false,
|
|
46
|
+
reason: `${label} can be launched but does not emit auditable structured events in v1.`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
available: true,
|
|
52
|
+
compatible: true,
|
|
53
|
+
launchable: launchable ?? true,
|
|
54
|
+
reason: null
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
buildLaunch(options) {
|
|
59
|
+
return buildLaunch(options);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
parseEventLine(line, context = {}) {
|
|
63
|
+
if (!parseEventLine) return null;
|
|
64
|
+
return parseEventLine(line, context);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseNdjsonLine(line) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!trimmed) return null;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(trimmed);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildPermissionsArgs(permissions = []) {
|
|
81
|
+
const normalized = new Set(permissions.map((entry) => String(entry).toLowerCase()));
|
|
82
|
+
|
|
83
|
+
if (normalized.has("all") || normalized.has("force")) {
|
|
84
|
+
return ["--force"];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (normalized.has("yolo") || normalized.has("dangerously-skip-permissions")) {
|
|
88
|
+
return ["--dangerously-skip-permissions"];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createExecutionAdapter, parseNdjsonLine, buildPermissionsArgs } from "./create-execution-adapter.js";
|
|
2
|
+
import { isExecutableAvailable } from "../../cli-probe.js";
|
|
3
|
+
|
|
4
|
+
const EXECUTABLE = "cursor-agent";
|
|
5
|
+
|
|
6
|
+
function checkCursorAvailability() {
|
|
7
|
+
const agentAvailable = isExecutableAvailable(EXECUTABLE);
|
|
8
|
+
if (!agentAvailable) {
|
|
9
|
+
const legacy = isExecutableAvailable("cursor");
|
|
10
|
+
if (!legacy) {
|
|
11
|
+
return {
|
|
12
|
+
available: false,
|
|
13
|
+
compatible: false,
|
|
14
|
+
launchable: false,
|
|
15
|
+
reason: 'Cursor agent CLI "cursor-agent" is not on PATH. Install Cursor CLI.'
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
available: true,
|
|
20
|
+
compatible: false,
|
|
21
|
+
launchable: false,
|
|
22
|
+
reason: 'Found "cursor" but Kairo v1 requires "cursor-agent" for auditable non-interactive runs.'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
available: true,
|
|
28
|
+
compatible: true,
|
|
29
|
+
launchable: true,
|
|
30
|
+
reason: null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildCursorLaunch({ task, cwd, model, permissions = [] }) {
|
|
35
|
+
const args = [
|
|
36
|
+
"-p",
|
|
37
|
+
"--output-format",
|
|
38
|
+
"stream-json",
|
|
39
|
+
...buildPermissionsArgs(permissions),
|
|
40
|
+
task
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
if (model) {
|
|
44
|
+
args.unshift("--model", model);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
command: EXECUTABLE,
|
|
49
|
+
args,
|
|
50
|
+
cwd,
|
|
51
|
+
env: process.env
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseCursorEventLine(line) {
|
|
56
|
+
const parsed = parseNdjsonLine(line);
|
|
57
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
58
|
+
|
|
59
|
+
if (parsed.type === "assistant" && parsed.timestamp_ms == null && parsed.model_call_id) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (parsed.type === "assistant" && parsed.timestamp_ms == null && !parsed.model_call_id) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (parsed.type === "tool_call") {
|
|
68
|
+
const toolName = parsed.tool_name ?? parsed.name ?? parsed.tool ?? "unknown";
|
|
69
|
+
return {
|
|
70
|
+
type: "tool_call",
|
|
71
|
+
tool_name: toolName,
|
|
72
|
+
status: parsed.subtype ?? parsed.status ?? "started",
|
|
73
|
+
id: parsed.call_id ?? parsed.id ?? null
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (parsed.type === "result" && parsed.usage) {
|
|
78
|
+
return {
|
|
79
|
+
type: "usage",
|
|
80
|
+
inputTokens: parsed.usage.input_tokens ?? parsed.usage.inputTokens ?? null,
|
|
81
|
+
outputTokens: parsed.usage.output_tokens ?? parsed.usage.outputTokens ?? null,
|
|
82
|
+
totalTokens: parsed.usage.total_tokens ?? parsed.usage.totalTokens ?? null,
|
|
83
|
+
cost: parsed.usage.cost ?? parsed.cost ?? null
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default createExecutionAdapter({
|
|
91
|
+
id: "cursor",
|
|
92
|
+
label: "Cursor",
|
|
93
|
+
executable: EXECUTABLE,
|
|
94
|
+
capabilities: {
|
|
95
|
+
structuredEvents: true,
|
|
96
|
+
tokens: true,
|
|
97
|
+
diff: false,
|
|
98
|
+
cancel: true,
|
|
99
|
+
transcript: true
|
|
100
|
+
},
|
|
101
|
+
checkAvailability: checkCursorAvailability,
|
|
102
|
+
buildLaunch: buildCursorLaunch,
|
|
103
|
+
parseEventLine: parseCursorEventLine
|
|
104
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import cursor from "./cursor.js";
|
|
2
|
+
import codex from "./codex.js";
|
|
3
|
+
import claude from "./claude.js";
|
|
4
|
+
import opencode from "./opencode.js";
|
|
5
|
+
|
|
6
|
+
const EXECUTION_ADAPTERS = [cursor, codex, claude, opencode];
|
|
7
|
+
|
|
8
|
+
export const EXECUTION_ADAPTER_IDS = EXECUTION_ADAPTERS.map((adapter) => adapter.id);
|
|
9
|
+
|
|
10
|
+
export function listExecutionAdapters() {
|
|
11
|
+
return [...EXECUTION_ADAPTERS];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveExecutionAdapter(id) {
|
|
15
|
+
const adapter = EXECUTION_ADAPTERS.find((candidate) => candidate.id === id);
|
|
16
|
+
if (!adapter) {
|
|
17
|
+
throw new Error(`Unknown execution adapter "${id}". Use ${EXECUTION_ADAPTER_IDS.join(", ")}.`);
|
|
18
|
+
}
|
|
19
|
+
return adapter;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function inspectExecutionAdapters(context = {}) {
|
|
23
|
+
return EXECUTION_ADAPTERS.map((adapter) => ({
|
|
24
|
+
id: adapter.id,
|
|
25
|
+
label: adapter.label,
|
|
26
|
+
executable: adapter.executable,
|
|
27
|
+
capabilities: adapter.capabilities,
|
|
28
|
+
...adapter.availability(context)
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function listLaunchableAdapterIds(context = {}) {
|
|
33
|
+
return inspectExecutionAdapters(context)
|
|
34
|
+
.filter((provider) => provider.launchable)
|
|
35
|
+
.map((provider) => provider.id);
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createExecutionAdapter } from "./create-execution-adapter.js";
|
|
2
|
+
|
|
3
|
+
const EXECUTABLE = "opencode";
|
|
4
|
+
|
|
5
|
+
function buildOpencodeLaunch({ task, cwd, model, permissions = [] }) {
|
|
6
|
+
const args = ["run", task];
|
|
7
|
+
|
|
8
|
+
if (model) {
|
|
9
|
+
args.unshift("--model", model);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (permissions.includes("force") || permissions.includes("all")) {
|
|
13
|
+
args.unshift("--force");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
command: EXECUTABLE,
|
|
18
|
+
args,
|
|
19
|
+
cwd,
|
|
20
|
+
env: process.env
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default createExecutionAdapter({
|
|
25
|
+
id: "opencode",
|
|
26
|
+
label: "OpenCode",
|
|
27
|
+
executable: EXECUTABLE,
|
|
28
|
+
launchable: false,
|
|
29
|
+
capabilities: {
|
|
30
|
+
structuredEvents: false,
|
|
31
|
+
tokens: false,
|
|
32
|
+
diff: false,
|
|
33
|
+
cancel: true,
|
|
34
|
+
transcript: false
|
|
35
|
+
},
|
|
36
|
+
buildLaunch: buildOpencodeLaunch,
|
|
37
|
+
parseEventLine: null
|
|
38
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { runPaths } from "../paths.js";
|
|
4
|
+
|
|
5
|
+
function cancelSignalPath(homeDir, runId) {
|
|
6
|
+
return `${runPaths(homeDir, runId).runDir}/cancel.signal.json`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function writeCancelSignal(homeDir, runId, payload) {
|
|
10
|
+
const { runDir } = runPaths(homeDir, runId);
|
|
11
|
+
await mkdir(runDir, { recursive: true });
|
|
12
|
+
await writeFile(cancelSignalPath(homeDir, runId), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function readCancelSignal(homeDir, runId) {
|
|
16
|
+
const path = cancelSignalPath(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 isRunCancelRequested(homeDir, runId) {
|
|
27
|
+
const signal = await readCancelSignal(homeDir, runId);
|
|
28
|
+
return signal?.requested === true;
|
|
29
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { resolveHomeDir } from "../paths.js";
|
|
2
|
+
import { resolveProfile } from "../profile.js";
|
|
3
|
+
import { printJson } from "../json-output.js";
|
|
4
|
+
import { BRAND, commandHeader } from "../brand/index.js";
|
|
5
|
+
import { formatCliCommand } from "../brand/cli.js";
|
|
6
|
+
import { inspectExecutionAdapters } from "./execution-adapters/index.js";
|
|
7
|
+
import {
|
|
8
|
+
listRunRecords,
|
|
9
|
+
readRunEvents,
|
|
10
|
+
readRunState
|
|
11
|
+
} from "./run-store.js";
|
|
12
|
+
import {
|
|
13
|
+
recoverRuns,
|
|
14
|
+
resolveRunAgent,
|
|
15
|
+
startRun,
|
|
16
|
+
stopRun
|
|
17
|
+
} from "./run-manager.js";
|
|
18
|
+
import { isActiveRunState, formatTaskLabel } from "./run-types.js";
|
|
19
|
+
import { resolveAgentFromProfile } from "./run-profile.js";
|
|
20
|
+
|
|
21
|
+
export async function runGlobalRun(options, packageManifest, { startRunImpl = startRun } = {}) {
|
|
22
|
+
const homeDir = resolveHomeDir();
|
|
23
|
+
await recoverRuns(homeDir);
|
|
24
|
+
|
|
25
|
+
if (!options.task) {
|
|
26
|
+
throw new Error(`Missing task. Use: ${formatCliCommand('run --agent <id> --task "..."')}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const profileResolved = await resolveProfile({
|
|
30
|
+
homeDir,
|
|
31
|
+
workspaceRoot: options.cwd
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const runtime = resolveAgentFromProfile(profileResolved, options.agent);
|
|
35
|
+
const permissions = options.permissions ?? runtime.permissions;
|
|
36
|
+
const captureTranscript = options.captureTranscript || runtime.captureTranscript;
|
|
37
|
+
|
|
38
|
+
const { runId, metadata, completion } = await startRunImpl({
|
|
39
|
+
homeDir,
|
|
40
|
+
agentId: runtime.agentId,
|
|
41
|
+
task: options.task,
|
|
42
|
+
cwd: options.cwd,
|
|
43
|
+
model: options.model ?? runtime.model,
|
|
44
|
+
permissions,
|
|
45
|
+
captureTranscript,
|
|
46
|
+
cliVersion: packageManifest.version,
|
|
47
|
+
profile: profileResolved,
|
|
48
|
+
follow: options.follow,
|
|
49
|
+
timeoutMs: options.timeoutMs,
|
|
50
|
+
wait: options.wait !== false
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!options.json) {
|
|
54
|
+
console.log(commandHeader(`run started · ${metadata.agentId} · ${runId}`));
|
|
55
|
+
console.log(`Task: ${formatTaskLabel(metadata)}`);
|
|
56
|
+
console.log(`Cwd: ${metadata.cwd}`);
|
|
57
|
+
if (!options.wait) {
|
|
58
|
+
console.log(`Follow: ${formatCliCommand(`runs show ${runId} --follow`)}`);
|
|
59
|
+
return { runId, metadata };
|
|
60
|
+
}
|
|
61
|
+
} else if (!options.wait) {
|
|
62
|
+
printJson({ runId, metadata });
|
|
63
|
+
return { runId, metadata };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!completion) {
|
|
67
|
+
if (options.json) {
|
|
68
|
+
printJson({ runId, metadata });
|
|
69
|
+
}
|
|
70
|
+
return { runId, metadata };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const final = await completion;
|
|
74
|
+
|
|
75
|
+
if (options.json) {
|
|
76
|
+
printJson({ runId, metadata: final });
|
|
77
|
+
} else {
|
|
78
|
+
console.log(`Run ${final.state} (exit ${final.exitCode ?? "n/a"})`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { runId, metadata: final };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function runGlobalRuns(options, packageManifest) {
|
|
85
|
+
const homeDir = resolveHomeDir();
|
|
86
|
+
await recoverRuns(homeDir);
|
|
87
|
+
|
|
88
|
+
switch (options.runsAction ?? "list") {
|
|
89
|
+
case "list":
|
|
90
|
+
return runRunsList(homeDir, options);
|
|
91
|
+
case "show":
|
|
92
|
+
return runRunsShow(homeDir, options);
|
|
93
|
+
case "stop":
|
|
94
|
+
return runRunsStop(homeDir, options);
|
|
95
|
+
default:
|
|
96
|
+
throw new Error(`Unknown runs action "${options.runsAction}". Use list, show, or stop.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function runRunsList(homeDir, options) {
|
|
101
|
+
const runs = await listRunRecords(homeDir, {
|
|
102
|
+
limit: options.limit,
|
|
103
|
+
activeOnly: options.activeOnly
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (options.json) {
|
|
107
|
+
printJson({ runs, providers: inspectExecutionAdapters({ cwd: options.cwd }) });
|
|
108
|
+
return { runs };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(commandHeader("runs"));
|
|
112
|
+
const active = runs.filter((run) => isActiveRunState(run.state));
|
|
113
|
+
console.log(`Active: ${active.length} · Total shown: ${runs.length}`);
|
|
114
|
+
console.log("");
|
|
115
|
+
|
|
116
|
+
for (const run of runs) {
|
|
117
|
+
console.log(
|
|
118
|
+
` ${run.runId} ${run.state.padEnd(12)} ${run.agentId.padEnd(10)} ${formatTaskLabel(run)}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (runs.length === 0) {
|
|
123
|
+
console.log(` (no runs yet — launch with ${formatCliCommand('run --agent cursor --task "..."')})`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { runs };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function runRunsShow(homeDir, options) {
|
|
130
|
+
if (!options.runId) {
|
|
131
|
+
throw new Error(`Missing run id. Use: ${formatCliCommand("runs show <runId>")}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const metadata = await readRunState(homeDir, options.runId);
|
|
135
|
+
if (!metadata) {
|
|
136
|
+
throw new Error(`Run "${options.runId}" not found.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const events = await readRunEvents(homeDir, options.runId, { limit: options.limit });
|
|
140
|
+
|
|
141
|
+
if (options.json) {
|
|
142
|
+
printJson({ metadata, events });
|
|
143
|
+
return { metadata, events };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(commandHeader(`run ${metadata.runId}`));
|
|
147
|
+
console.log(`Agent: ${metadata.agentId} (${metadata.provider})`);
|
|
148
|
+
console.log(`State: ${metadata.state}`);
|
|
149
|
+
console.log(`Model: ${metadata.model ?? "default"}`);
|
|
150
|
+
console.log(`Cwd: ${metadata.cwd}`);
|
|
151
|
+
console.log(`Started: ${metadata.startedAt}`);
|
|
152
|
+
if (metadata.completedAt) console.log(`Completed: ${metadata.completedAt}`);
|
|
153
|
+
if (metadata.tokenUsage) console.log(`Tokens: ${JSON.stringify(metadata.tokenUsage)}`);
|
|
154
|
+
if (metadata.diffSummary) console.log(`Diff: ${JSON.stringify(metadata.diffSummary)}`);
|
|
155
|
+
if (metadata.error) console.log(`Error: ${metadata.error}`);
|
|
156
|
+
console.log("");
|
|
157
|
+
console.log("Events:");
|
|
158
|
+
|
|
159
|
+
for (const event of events) {
|
|
160
|
+
if (event.parseError) {
|
|
161
|
+
console.log(` [parse error line ${event.line}]`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const summary = summarizeEvent(event);
|
|
165
|
+
console.log(` ${event.timestamp} ${event.type} ${summary}`);
|
|
166
|
+
if (options.follow) {
|
|
167
|
+
// follow is for live runs; show replays events only
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { metadata, events };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function runRunsStop(homeDir, options) {
|
|
175
|
+
if (!options.runId) {
|
|
176
|
+
throw new Error(`Missing run id. Use: ${formatCliCommand("runs stop <runId>")}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const metadata = await stopRun(homeDir, options.runId);
|
|
180
|
+
|
|
181
|
+
if (options.json) {
|
|
182
|
+
printJson({ metadata });
|
|
183
|
+
return { metadata };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log(commandHeader(`run ${metadata.runId} cancelled`));
|
|
187
|
+
console.log(`State: ${metadata.state}`);
|
|
188
|
+
return { metadata };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function summarizeEvent(event) {
|
|
192
|
+
if (event.type === "agent.tool_call") {
|
|
193
|
+
return event.data?.tool_name ?? event.data?.name ?? "tool";
|
|
194
|
+
}
|
|
195
|
+
if (event.type === "process.stdout" || event.type === "process.stderr") {
|
|
196
|
+
const line = event.data?.line ?? "";
|
|
197
|
+
return line.length > 80 ? `${line.slice(0, 79)}…` : line;
|
|
198
|
+
}
|
|
199
|
+
if (event.type === "run.completed" || event.type === "run.failed") {
|
|
200
|
+
return `exit=${event.data?.exitCode ?? "n/a"}`;
|
|
201
|
+
}
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function buildRuntimeDashboardData({ homeDir, workspaceRoot, cliVersion }) {
|
|
206
|
+
await recoverRuns(homeDir);
|
|
207
|
+
const [runs, providers, profileResolved] = await Promise.all([
|
|
208
|
+
listRunRecords(homeDir, { limit: 20 }),
|
|
209
|
+
Promise.resolve(inspectExecutionAdapters({ cwd: workspaceRoot })),
|
|
210
|
+
resolveProfile({ homeDir, workspaceRoot })
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
cliVersion,
|
|
215
|
+
runs,
|
|
216
|
+
activeRuns: runs.filter((run) => isActiveRunState(run.state)),
|
|
217
|
+
recentRuns: runs.filter((run) => !isActiveRunState(run.state)).slice(0, 10),
|
|
218
|
+
providers,
|
|
219
|
+
profile: profileResolved
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -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
|
+
}
|