@kal-elsam/kairo-runtime 0.7.0 → 0.9.0
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 +43 -11
- package/global-template/components/catalog.json +4 -1
- package/global-template/components/orchestrator/extensions/pi/kairo-minion.js +604 -0
- package/package.json +1 -1
- package/scripts/cockpit-smoke.mjs +4 -4
- package/src/cli.js +34 -2
- package/src/global/adapters/pi.js +1 -1
- package/src/global/global-doctor.js +2 -0
- package/src/global/ink/cockpit/primitives.js +96 -31
- package/src/global/ink/cockpit-alerts.js +36 -0
- package/src/global/ink/cockpit-changes.js +59 -35
- package/src/global/ink/cockpit-control-center.js +129 -51
- package/src/global/ink/cockpit-controller.js +58 -11
- package/src/global/ink/cockpit-focus.js +4 -2
- package/src/global/ink/cockpit-models.js +89 -47
- package/src/global/ink/cockpit-palette.js +98 -0
- package/src/global/ink/cockpit-path-label.js +19 -0
- package/src/global/ink/cockpit-recovery.js +78 -15
- package/src/global/ink/cockpit-reviews.js +14 -10
- package/src/global/ink/cockpit-runs.js +13 -4
- package/src/global/ink/cockpit-settings.js +194 -0
- package/src/global/ink/cockpit-views.js +88 -57
- package/src/global/ink/orchestrator-app.js +137 -46
- package/src/global/ink/orchestrator-state.js +24 -14
- package/src/global/ink/use-orchestrator-data.js +58 -0
- package/src/global/paths.js +3 -0
- package/src/global/runtime/alerts/alert-store.js +216 -0
- package/src/global/runtime/alerts/alert-types.js +59 -0
- package/src/global/runtime/alerts/alert-validate.js +117 -0
- package/src/global/runtime/execution-adapters/pi.js +12 -2
- package/src/global/runtime/monitor/monitor-cli.js +62 -0
- package/src/global/runtime/monitor/monitor-platform.js +95 -0
- package/src/global/runtime/monitor/monitor.js +249 -0
- package/src/global/runtime/orchestration/index.js +23 -0
- package/src/global/runtime/orchestration/orch-receipts.js +234 -0
- package/src/global/runtime/orchestration/orch-types.js +173 -0
- package/src/global/runtime/orchestration/orch-validate.js +63 -0
- package/src/global/runtime/run-cli.js +1 -0
- package/src/global/runtime/run-manager.js +59 -4
- package/src/global/runtime/run-strategy.js +71 -0
- package/src/global/runtime/run-supervisor.js +22 -2
- package/src/global/runtime/run-types.js +5 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
export const RUN_STRATEGIES = Object.freeze({ DIRECT: "direct", ORCHESTRATED: "orchestrated" });
|
|
3
|
+
export const DAG_NODE_STATES = Object.freeze({
|
|
4
|
+
PENDING: "pending", READY: "ready", RUNNING: "running", COMPACTING: "compacting",
|
|
5
|
+
COMPLETED: "completed", FAILED: "failed", CANCELLED: "cancelled", BLOCKED: "blocked"
|
|
6
|
+
});
|
|
7
|
+
export const DAG_TERMINAL_STATES = new Set([
|
|
8
|
+
DAG_NODE_STATES.COMPLETED, DAG_NODE_STATES.FAILED, DAG_NODE_STATES.CANCELLED
|
|
9
|
+
]);
|
|
10
|
+
export const ORCH_LIMITS = Object.freeze({
|
|
11
|
+
MAX_DEPTH: 1, DEFAULT_CONCURRENCY: 2, MAX_ATTEMPTS: 2, COMPACT_RATIO: 0.7, STOP_RATIO: 0.9
|
|
12
|
+
});
|
|
13
|
+
export const ORCH_ERROR_CODES = Object.freeze({
|
|
14
|
+
INVALID_STRATEGY: "invalid_strategy", INVALID_DEPTH: "invalid_depth",
|
|
15
|
+
INVALID_LINEAGE: "invalid_lineage", INVALID_HANDOFF: "invalid_handoff",
|
|
16
|
+
INVALID_NODE: "invalid_node", LIMIT_EXCEEDED: "limit_exceeded",
|
|
17
|
+
FORBIDDEN_FIELD: "forbidden_field", RECEIPT_EXISTS: "receipt_exists"
|
|
18
|
+
});
|
|
19
|
+
export class OrchContractError extends Error {
|
|
20
|
+
constructor(message, { code, details = null } = {}) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "OrchContractError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.details = details;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function createTaskId() {
|
|
28
|
+
return `task_${randomBytes(8).toString("hex")}`;
|
|
29
|
+
}
|
|
30
|
+
export function isTerminalDagState(state) {
|
|
31
|
+
return DAG_TERMINAL_STATES.has(state);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertDepth(depth) {
|
|
35
|
+
const d = Number(depth);
|
|
36
|
+
if (!Number.isInteger(d) || d < 0) {
|
|
37
|
+
throw new OrchContractError(`Invalid depth "${depth}".`, {
|
|
38
|
+
code: ORCH_ERROR_CODES.INVALID_DEPTH, details: { depth }
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (d > ORCH_LIMITS.MAX_DEPTH) {
|
|
42
|
+
throw new OrchContractError(`Orchestration depth ${d} exceeds max ${ORCH_LIMITS.MAX_DEPTH}.`, {
|
|
43
|
+
code: ORCH_ERROR_CODES.INVALID_DEPTH, details: { depth: d }
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return d;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertParentForDepth(depth, parentId, { rootMsg, minionMsg, code }) {
|
|
50
|
+
if (depth === 0 && parentId != null) {
|
|
51
|
+
throw new OrchContractError(rootMsg, { code });
|
|
52
|
+
}
|
|
53
|
+
if (depth > 0 && !parentId) {
|
|
54
|
+
throw new OrchContractError(minionMsg, { code });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function normalizeRunStrategy(value = RUN_STRATEGIES.DIRECT) {
|
|
59
|
+
const strategy = String(value ?? RUN_STRATEGIES.DIRECT).trim().toLowerCase();
|
|
60
|
+
if (!Object.values(RUN_STRATEGIES).includes(strategy)) {
|
|
61
|
+
throw new OrchContractError(`Invalid run strategy "${value}". Use direct or orchestrated.`, {
|
|
62
|
+
code: ORCH_ERROR_CODES.INVALID_STRATEGY, details: { value }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return strategy;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Lineage: max depth 1 (root=0, minion=1). */
|
|
69
|
+
export function createOrchLineage({ rootRunId, parentRunId = null, taskId = null, depth = 0 } = {}) {
|
|
70
|
+
if (typeof rootRunId !== "string" || !rootRunId) {
|
|
71
|
+
throw new OrchContractError("rootRunId is required.", { code: ORCH_ERROR_CODES.INVALID_LINEAGE });
|
|
72
|
+
}
|
|
73
|
+
const d = assertDepth(depth);
|
|
74
|
+
assertParentForDepth(d, parentRunId, {
|
|
75
|
+
rootMsg: "Root nodes must not set parentRunId.",
|
|
76
|
+
minionMsg: "Minion nodes require parentRunId.",
|
|
77
|
+
code: ORCH_ERROR_CODES.INVALID_LINEAGE
|
|
78
|
+
});
|
|
79
|
+
return { rootRunId, parentRunId: parentRunId ?? null, taskId: taskId ?? createTaskId(), depth: d };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createBudgetUsage({
|
|
83
|
+
contextTokens = 0, contextLimit = 0,
|
|
84
|
+
compactRatio = ORCH_LIMITS.COMPACT_RATIO, stopRatio = ORCH_LIMITS.STOP_RATIO
|
|
85
|
+
} = {}) {
|
|
86
|
+
const tokens = Math.max(0, Number(contextTokens) || 0);
|
|
87
|
+
const limit = Math.max(0, Number(contextLimit) || 0);
|
|
88
|
+
const ratio = limit > 0 ? tokens / limit : 0;
|
|
89
|
+
return {
|
|
90
|
+
contextTokens: tokens, contextLimit: limit, ratio,
|
|
91
|
+
shouldCompact: limit > 0 && ratio >= compactRatio && ratio < stopRatio,
|
|
92
|
+
shouldStop: limit > 0 && ratio >= stopRatio
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createDagNode({
|
|
97
|
+
taskId = null, runId = null, parentTaskId = null, depth = 0,
|
|
98
|
+
state = DAG_NODE_STATES.PENDING, dependsOn = [], attempt = 0,
|
|
99
|
+
objectiveDigest = null, budget = null, resultDigest = null, error = null
|
|
100
|
+
} = {}) {
|
|
101
|
+
if (!Object.values(DAG_NODE_STATES).includes(state)) {
|
|
102
|
+
throw new OrchContractError(`Invalid DAG node state "${state}".`, {
|
|
103
|
+
code: ORCH_ERROR_CODES.INVALID_NODE, details: { state }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const d = assertDepth(depth);
|
|
107
|
+
assertParentForDepth(d, parentTaskId, {
|
|
108
|
+
rootMsg: "Root nodes must not set parentTaskId.",
|
|
109
|
+
minionMsg: "Minion nodes require parentTaskId.",
|
|
110
|
+
code: ORCH_ERROR_CODES.INVALID_NODE
|
|
111
|
+
});
|
|
112
|
+
const attempts = Number(attempt) || 0;
|
|
113
|
+
if (attempts < 0 || attempts > ORCH_LIMITS.MAX_ATTEMPTS) {
|
|
114
|
+
throw new OrchContractError(`Attempt ${attempts} outside 0..${ORCH_LIMITS.MAX_ATTEMPTS}.`, {
|
|
115
|
+
code: ORCH_ERROR_CODES.LIMIT_EXCEEDED, details: { attempt: attempts }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const deps = [...new Set((dependsOn ?? []).map((id) => {
|
|
119
|
+
if (typeof id !== "string" || !id) {
|
|
120
|
+
throw new OrchContractError("Dependency taskId is required.", { code: ORCH_ERROR_CODES.INVALID_NODE });
|
|
121
|
+
}
|
|
122
|
+
return id;
|
|
123
|
+
}))];
|
|
124
|
+
return {
|
|
125
|
+
taskId: taskId ?? createTaskId(), runId: runId ?? null, parentTaskId: parentTaskId ?? null,
|
|
126
|
+
depth: d, state, dependsOn: deps, attempt: attempts,
|
|
127
|
+
objectiveDigest, budget: budget ?? null, resultDigest, error: error ?? null
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createMinionBrief({
|
|
132
|
+
objective, constraints = [], admittedPaths = [], exitCriteria = [],
|
|
133
|
+
parentTaskId = null, taskId = null
|
|
134
|
+
} = {}) {
|
|
135
|
+
if (typeof objective !== "string" || !objective.trim()) {
|
|
136
|
+
throw new OrchContractError("Minion brief requires a non-empty objective.", {
|
|
137
|
+
code: ORCH_ERROR_CODES.INVALID_HANDOFF
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
taskId: taskId ?? createTaskId(), parentTaskId: parentTaskId ?? null,
|
|
142
|
+
objective: objective.trim(), constraints: (constraints ?? []).map(String),
|
|
143
|
+
admittedPaths: (admittedPaths ?? []).map(String), exitCriteria: (exitCriteria ?? []).map(String)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function createMinionResult({
|
|
148
|
+
taskId = null, summary, decisions = [], files = [], risks = [],
|
|
149
|
+
evidence = [], usage = null, compact = false
|
|
150
|
+
} = {}) {
|
|
151
|
+
if (typeof taskId !== "string" || !taskId) {
|
|
152
|
+
throw new OrchContractError("Minion result requires taskId.", { code: ORCH_ERROR_CODES.INVALID_HANDOFF });
|
|
153
|
+
}
|
|
154
|
+
if (typeof summary !== "string" || !summary.trim()) {
|
|
155
|
+
throw new OrchContractError("Minion result requires a non-empty summary.", {
|
|
156
|
+
code: ORCH_ERROR_CODES.INVALID_HANDOFF
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
taskId, summary: summary.trim(),
|
|
161
|
+
decisions: (decisions ?? []).map(String), files: (files ?? []).map(String),
|
|
162
|
+
risks: (risks ?? []).map(String), evidence: (evidence ?? []).map(String),
|
|
163
|
+
usage: {
|
|
164
|
+
inputTokens: usage?.inputTokens ?? null, outputTokens: usage?.outputTokens ?? null,
|
|
165
|
+
totalTokens: usage?.totalTokens ?? null, cost: usage?.cost ?? null
|
|
166
|
+
},
|
|
167
|
+
compact: Boolean(compact)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function digestAllowlisted(value) {
|
|
172
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
|
|
173
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ORCH_ERROR_CODES, OrchContractError, createBudgetUsage, createDagNode,
|
|
3
|
+
createMinionResult, createOrchLineage, normalizeRunStrategy
|
|
4
|
+
} from "./orch-types.js";
|
|
5
|
+
export const FORBIDDEN_KEYS = new Set([
|
|
6
|
+
"prompt", "diff", "transcript", "raw", "rawOutput", "stdout", "stderr", "output",
|
|
7
|
+
"message", "messages", "content", "secret", "secrets", "token", "apiKey",
|
|
8
|
+
"conversation", "history", "toolArgs", "arguments", "objective"
|
|
9
|
+
]);
|
|
10
|
+
export function walkForbiddenKeys(value, path = "") {
|
|
11
|
+
if (!value || typeof value !== "object") return;
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
value.forEach((item, i) => walkForbiddenKeys(item, `${path}[${i}]`));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
for (const [key, child] of Object.entries(value)) {
|
|
17
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
18
|
+
throw new OrchContractError(`Forbidden field "${path}${key}" in orchestration receipt.`, {
|
|
19
|
+
code: ORCH_ERROR_CODES.FORBIDDEN_FIELD, details: { key, path }
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
walkForbiddenKeys(child, `${path}${key}.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function assertOrchReceiptSecretFree(receipt) {
|
|
26
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
27
|
+
throw new OrchContractError("Invalid orchestration receipt: expected object.", {
|
|
28
|
+
code: ORCH_ERROR_CODES.FORBIDDEN_FIELD
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
walkForbiddenKeys(receipt);
|
|
32
|
+
if (receipt.version !== 1) {
|
|
33
|
+
throw new OrchContractError("Orchestration receipt version must be 1.", {
|
|
34
|
+
code: ORCH_ERROR_CODES.FORBIDDEN_FIELD
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
normalizeRunStrategy(receipt.strategy);
|
|
38
|
+
if (typeof receipt.rootRunId !== "string" || !receipt.rootRunId) {
|
|
39
|
+
throw new OrchContractError("rootRunId is required on receipt.", {
|
|
40
|
+
code: ORCH_ERROR_CODES.INVALID_LINEAGE
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
createOrchLineage(receipt.lineage);
|
|
44
|
+
if (receipt.lineage.rootRunId !== receipt.rootRunId) {
|
|
45
|
+
throw new OrchContractError("lineage.rootRunId must match receipt.rootRunId.", {
|
|
46
|
+
code: ORCH_ERROR_CODES.INVALID_LINEAGE
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(receipt.nodes) || !Array.isArray(receipt.results)) {
|
|
50
|
+
throw new OrchContractError("Receipt requires nodes[] and results[].", {
|
|
51
|
+
code: ORCH_ERROR_CODES.INVALID_NODE
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
for (const node of receipt.nodes) {
|
|
55
|
+
createDagNode(node);
|
|
56
|
+
if (node.budget) createBudgetUsage(node.budget);
|
|
57
|
+
}
|
|
58
|
+
for (const result of receipt.results) createMinionResult(result);
|
|
59
|
+
if (typeof receipt.createdAt !== "string" || !receipt.createdAt) {
|
|
60
|
+
throw new OrchContractError("createdAt is required.", { code: ORCH_ERROR_CODES.FORBIDDEN_FIELD });
|
|
61
|
+
}
|
|
62
|
+
return receipt;
|
|
63
|
+
}
|
|
@@ -45,6 +45,7 @@ export async function runGlobalRun(options, packageManifest, { startRunImpl = st
|
|
|
45
45
|
captureTranscript,
|
|
46
46
|
cliVersion: packageManifest.version,
|
|
47
47
|
profile: profileResolved,
|
|
48
|
+
strategy: options.strategy ?? "direct",
|
|
48
49
|
follow: options.follow,
|
|
49
50
|
timeoutMs: options.timeoutMs,
|
|
50
51
|
wait: options.wait !== false
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
appendRunEvent,
|
|
6
6
|
appendRunStartedEvent,
|
|
7
7
|
createRunRecord,
|
|
8
|
+
listRunRecords,
|
|
8
9
|
readRunState,
|
|
9
10
|
reconcileActiveRuns,
|
|
10
11
|
writeRunState
|
|
@@ -22,6 +23,20 @@ import {
|
|
|
22
23
|
readSupervisorLockForRun,
|
|
23
24
|
supervisePreparedRun
|
|
24
25
|
} from "./run-supervisor.js";
|
|
26
|
+
import {
|
|
27
|
+
assertManagedMinionExtension,
|
|
28
|
+
assertOrchestratedAgent,
|
|
29
|
+
createRootRunLineage,
|
|
30
|
+
normalizeRunStrategy,
|
|
31
|
+
RUN_STRATEGIES
|
|
32
|
+
} from "./run-strategy.js";
|
|
33
|
+
import {
|
|
34
|
+
DAG_NODE_STATES,
|
|
35
|
+
createDagNode,
|
|
36
|
+
createOrchState,
|
|
37
|
+
reconcileOrchState,
|
|
38
|
+
saveOrchState
|
|
39
|
+
} from "./orchestration/index.js";
|
|
25
40
|
|
|
26
41
|
const activeProcesses = new Map();
|
|
27
42
|
const cancelledRuns = new Set();
|
|
@@ -55,6 +70,19 @@ export async function recoverRuns(homeDir) {
|
|
|
55
70
|
exceptRunIds: listActiveRunIds(),
|
|
56
71
|
isRunAliveImpl: isRunSupervisedAlive
|
|
57
72
|
});
|
|
73
|
+
for (const run of await listRunRecords(homeDir)) {
|
|
74
|
+
if (normalizeRunStrategy(run.strategy ?? RUN_STRATEGIES.DIRECT) !== RUN_STRATEGIES.ORCHESTRATED) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (await isRunSupervisedAlive(homeDir, run)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await reconcileOrchState(run.runId, { homeDir });
|
|
82
|
+
} catch {
|
|
83
|
+
// Fail closed per root: never invent receipt evidence from corrupt state.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
58
86
|
return interrupted;
|
|
59
87
|
}
|
|
60
88
|
|
|
@@ -67,8 +95,10 @@ async function prepareRun({
|
|
|
67
95
|
permissions = [],
|
|
68
96
|
captureTranscript = false,
|
|
69
97
|
cliVersion,
|
|
70
|
-
profile = null
|
|
98
|
+
profile = null,
|
|
99
|
+
strategy = "direct"
|
|
71
100
|
}) {
|
|
101
|
+
const normalizedStrategy = assertOrchestratedAgent(agentId, strategy);
|
|
72
102
|
const adapter = resolveExecutionAdapter(agentId);
|
|
73
103
|
const availability = adapter.availability({ cwd });
|
|
74
104
|
|
|
@@ -83,7 +113,12 @@ async function prepareRun({
|
|
|
83
113
|
);
|
|
84
114
|
}
|
|
85
115
|
|
|
116
|
+
if (normalizedStrategy === RUN_STRATEGIES.ORCHESTRATED) {
|
|
117
|
+
await assertManagedMinionExtension(homeDir);
|
|
118
|
+
}
|
|
119
|
+
|
|
86
120
|
const runId = createRunId();
|
|
121
|
+
const lineage = createRootRunLineage(runId);
|
|
87
122
|
const metadata = createRunMetadata({
|
|
88
123
|
runId,
|
|
89
124
|
agentId,
|
|
@@ -94,7 +129,9 @@ async function prepareRun({
|
|
|
94
129
|
permissions,
|
|
95
130
|
captureTranscript,
|
|
96
131
|
cliVersion,
|
|
97
|
-
profileSources: profile?.sources ?? null
|
|
132
|
+
profileSources: profile?.sources ?? null,
|
|
133
|
+
strategy: normalizedStrategy,
|
|
134
|
+
lineage
|
|
98
135
|
});
|
|
99
136
|
|
|
100
137
|
await createRunRecord(homeDir, metadata);
|
|
@@ -107,9 +144,25 @@ async function prepareRun({
|
|
|
107
144
|
permissions,
|
|
108
145
|
captureTranscript,
|
|
109
146
|
cliVersion,
|
|
110
|
-
profile: profile?.profile ?? null
|
|
147
|
+
profile: profile?.profile ?? null,
|
|
148
|
+
strategy: normalizedStrategy
|
|
111
149
|
});
|
|
112
150
|
|
|
151
|
+
if (normalizedStrategy === RUN_STRATEGIES.ORCHESTRATED) {
|
|
152
|
+
await saveOrchState(createOrchState({
|
|
153
|
+
rootRunId: runId,
|
|
154
|
+
strategy: normalizedStrategy,
|
|
155
|
+
lineage,
|
|
156
|
+
nodes: [createDagNode({
|
|
157
|
+
taskId: lineage.taskId,
|
|
158
|
+
runId,
|
|
159
|
+
depth: 0,
|
|
160
|
+
state: DAG_NODE_STATES.RUNNING
|
|
161
|
+
})],
|
|
162
|
+
cliVersion
|
|
163
|
+
}), { homeDir });
|
|
164
|
+
}
|
|
165
|
+
|
|
113
166
|
return { runId, metadata };
|
|
114
167
|
}
|
|
115
168
|
|
|
@@ -123,6 +176,7 @@ export async function startRun({
|
|
|
123
176
|
captureTranscript = false,
|
|
124
177
|
cliVersion,
|
|
125
178
|
profile = null,
|
|
179
|
+
strategy = "direct",
|
|
126
180
|
follow = false,
|
|
127
181
|
timeoutMs = null,
|
|
128
182
|
wait = true,
|
|
@@ -138,7 +192,8 @@ export async function startRun({
|
|
|
138
192
|
permissions,
|
|
139
193
|
captureTranscript,
|
|
140
194
|
cliVersion,
|
|
141
|
-
profile
|
|
195
|
+
profile,
|
|
196
|
+
strategy: normalizeRunStrategy(strategy)
|
|
142
197
|
});
|
|
143
198
|
|
|
144
199
|
if (!wait) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
RUN_STRATEGIES,
|
|
6
|
+
normalizeRunStrategy,
|
|
7
|
+
createOrchLineage,
|
|
8
|
+
resolveKairoMinionExtensionPath
|
|
9
|
+
} from "./orchestration/index.js";
|
|
10
|
+
|
|
11
|
+
export { RUN_STRATEGIES, normalizeRunStrategy };
|
|
12
|
+
|
|
13
|
+
const ORCH_MODULE_PATH = join(dirname(fileURLToPath(import.meta.url)), "orchestration", "index.js");
|
|
14
|
+
|
|
15
|
+
export const ORCH_RUNTIME_ENV = Object.freeze({
|
|
16
|
+
HOME: "KAIRO_ORCH_HOME",
|
|
17
|
+
ROOT_RUN_ID: "KAIRO_ORCH_ROOT_RUN_ID",
|
|
18
|
+
ROOT_TASK_ID: "KAIRO_ORCH_ROOT_TASK_ID",
|
|
19
|
+
CLI_VERSION: "KAIRO_ORCH_CLI_VERSION",
|
|
20
|
+
MODULE: "KAIRO_ORCH_MODULE"
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** Reject orchestrated for non-Pi before any run I/O. */
|
|
24
|
+
export function assertOrchestratedAgent(agentId, strategy) {
|
|
25
|
+
const normalized = normalizeRunStrategy(strategy);
|
|
26
|
+
if (normalized === RUN_STRATEGIES.ORCHESTRATED && agentId !== "pi") {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Strategy "orchestrated" requires agent "pi" (got "${agentId}").`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Fail closed when managed extension is missing or not a regular file. */
|
|
35
|
+
export async function assertManagedMinionExtension(homeDir) {
|
|
36
|
+
const extensionPath = resolveKairoMinionExtensionPath(homeDir);
|
|
37
|
+
let info;
|
|
38
|
+
try {
|
|
39
|
+
info = await stat(extensionPath);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(`Managed Kairo minion extension missing: ${extensionPath}`);
|
|
42
|
+
}
|
|
43
|
+
if (!info.isFile()) {
|
|
44
|
+
throw new Error(`Managed Kairo minion extension is not a regular file: ${extensionPath}`);
|
|
45
|
+
}
|
|
46
|
+
return extensionPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createRootRunLineage(runId) {
|
|
50
|
+
return createOrchLineage({ rootRunId: runId, parentRunId: null, depth: 0 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Supervisor derives extension path from homeDir only — never from CLI/handoff. */
|
|
54
|
+
export function resolveOrchestratedExtensionPath(homeDir, strategy) {
|
|
55
|
+
if (normalizeRunStrategy(strategy) !== RUN_STRATEGIES.ORCHESTRATED) return null;
|
|
56
|
+
return resolveKairoMinionExtensionPath(homeDir);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildOrchestratedRuntimeEnv({
|
|
60
|
+
homeDir, rootRunId, rootTaskId, cliVersion = null,
|
|
61
|
+
strategy = RUN_STRATEGIES.DIRECT, baseEnv = process.env
|
|
62
|
+
} = {}) {
|
|
63
|
+
const env = { ...baseEnv };
|
|
64
|
+
if (normalizeRunStrategy(strategy) !== RUN_STRATEGIES.ORCHESTRATED) return env;
|
|
65
|
+
env[ORCH_RUNTIME_ENV.HOME] = homeDir;
|
|
66
|
+
env[ORCH_RUNTIME_ENV.ROOT_RUN_ID] = rootRunId;
|
|
67
|
+
env[ORCH_RUNTIME_ENV.ROOT_TASK_ID] = rootTaskId;
|
|
68
|
+
env[ORCH_RUNTIME_ENV.CLI_VERSION] = cliVersion == null ? "" : String(cliVersion);
|
|
69
|
+
env[ORCH_RUNTIME_ENV.MODULE] = ORCH_MODULE_PATH;
|
|
70
|
+
return env;
|
|
71
|
+
}
|
|
@@ -18,6 +18,12 @@ import { consumeRunHandoff } from "./run-handoff.js";
|
|
|
18
18
|
import { isRunCancelRequested } from "./run-cancel-signal.js";
|
|
19
19
|
import { readSupervisorLock, touchSupervisorLock, writeSupervisorLock } from "./run-supervisor-lock.js";
|
|
20
20
|
import { shouldPersistTranscript } from "./run-redact.js";
|
|
21
|
+
import {
|
|
22
|
+
buildOrchestratedRuntimeEnv,
|
|
23
|
+
normalizeRunStrategy,
|
|
24
|
+
resolveOrchestratedExtensionPath
|
|
25
|
+
} from "./run-strategy.js";
|
|
26
|
+
import { finalizeOrchState, RUN_STRATEGIES } from "./orchestration/index.js";
|
|
21
27
|
|
|
22
28
|
async function shouldPreserveCancelledState(homeDir, runId) {
|
|
23
29
|
const fresh = await readRunState(homeDir, runId);
|
|
@@ -66,12 +72,16 @@ export async function supervisePreparedRun({
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
const captureTranscript = handoff.captureTranscript === true;
|
|
75
|
+
const strategy = normalizeRunStrategy(handoff.strategy ?? metadata.strategy ?? "direct");
|
|
76
|
+
const extensionPath = resolveOrchestratedExtensionPath(homeDir, strategy);
|
|
69
77
|
const launch = adapter.buildLaunch({
|
|
70
78
|
task: handoff.task,
|
|
71
79
|
cwd: handoff.cwd,
|
|
72
80
|
model: handoff.model,
|
|
73
81
|
permissions: handoff.permissions ?? [],
|
|
74
|
-
profile: handoff.profile ?? null
|
|
82
|
+
profile: handoff.profile ?? null,
|
|
83
|
+
strategy,
|
|
84
|
+
extensionPath
|
|
75
85
|
});
|
|
76
86
|
|
|
77
87
|
metadata = {
|
|
@@ -90,7 +100,14 @@ export async function supervisePreparedRun({
|
|
|
90
100
|
|
|
91
101
|
const child = spawnImpl(launch.command, launch.args, {
|
|
92
102
|
cwd: launch.cwd,
|
|
93
|
-
env:
|
|
103
|
+
env: buildOrchestratedRuntimeEnv({
|
|
104
|
+
homeDir,
|
|
105
|
+
rootRunId: runId,
|
|
106
|
+
rootTaskId: metadata.lineage?.taskId,
|
|
107
|
+
cliVersion: metadata.cliVersion,
|
|
108
|
+
strategy,
|
|
109
|
+
baseEnv: launch.env ?? process.env
|
|
110
|
+
}),
|
|
94
111
|
stdio: ["ignore", "pipe", "pipe"]
|
|
95
112
|
});
|
|
96
113
|
activeProcesses?.set(runId, child);
|
|
@@ -244,6 +261,9 @@ export async function supervisePreparedRun({
|
|
|
244
261
|
type: failed ? "run.failed" : "run.completed",
|
|
245
262
|
data: { exitCode }
|
|
246
263
|
}), { captureTranscript: shouldPersistTranscript(captureTranscript) });
|
|
264
|
+
if (!failed && strategy === RUN_STRATEGIES.ORCHESTRATED) {
|
|
265
|
+
await finalizeOrchState(runId, { homeDir, recovered: false });
|
|
266
|
+
}
|
|
247
267
|
resolve(metadata);
|
|
248
268
|
});
|
|
249
269
|
} catch (error) {
|
|
@@ -83,7 +83,9 @@ export function createRunMetadata({
|
|
|
83
83
|
permissions = [],
|
|
84
84
|
captureTranscript = false,
|
|
85
85
|
cliVersion,
|
|
86
|
-
profileSources = null
|
|
86
|
+
profileSources = null,
|
|
87
|
+
strategy = "direct",
|
|
88
|
+
lineage = null
|
|
87
89
|
}) {
|
|
88
90
|
const { taskDigest, taskLength } = createTaskFingerprint(task);
|
|
89
91
|
const now = new Date().toISOString();
|
|
@@ -100,6 +102,8 @@ export function createRunMetadata({
|
|
|
100
102
|
captureTranscript,
|
|
101
103
|
cliVersion,
|
|
102
104
|
profileSources,
|
|
105
|
+
strategy,
|
|
106
|
+
lineage,
|
|
103
107
|
state: RUN_STATES.PENDING,
|
|
104
108
|
pid: null,
|
|
105
109
|
supervisorPid: null,
|