@kal-elsam/kairo-runtime 0.22.1 → 0.23.1
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/CHANGELOG.md +38 -0
- package/package.json +1 -1
- package/src/global/cockpit/app.js +51 -2
- package/src/global/conversation/project-router.js +7 -3
- package/src/global/conversation/service.js +19 -5
- package/src/global/observability/codex-models.js +1 -1
- package/src/global/observability/codex-usage.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,44 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.23.1 — 2026-09-18 (Kairo Runtime)
|
|
9
|
+
|
|
10
|
+
Patch release.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- A plain chat question's action label and any failure message said
|
|
15
|
+
generic "Asking Kairo" throughout the wait, even though one specific
|
|
16
|
+
real adapter (Codex or Claude) is what's actually being asked and
|
|
17
|
+
can actually fail — Kairo is the system, never the one answering.
|
|
18
|
+
Adds `service.planAsk`, a read-only preview of ASK mode's routing
|
|
19
|
+
decision (reusing the same cached probes `submitTask` already uses),
|
|
20
|
+
so the cockpit now names the real provider/model from the start of
|
|
21
|
+
the wait instead of only leaking it incidentally through a raw error
|
|
22
|
+
message on failure.
|
|
23
|
+
|
|
24
|
+
## 0.23.0 — 2026-09-18 (Kairo Runtime)
|
|
25
|
+
|
|
26
|
+
Minor release. First slice of PROJECT TEAM's automatic model-fallback
|
|
27
|
+
cascade.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- When a role's assigned model is unavailable but a real, currently-
|
|
32
|
+
eligible fallback exists, the cockpit no longer stops for an extra
|
|
33
|
+
y/n confirm on the substitution. The human already asked for this
|
|
34
|
+
task to execute now (picked the role, pressed execute) — Kairo runs
|
|
35
|
+
the real fallback automatically and narrates the substitution into
|
|
36
|
+
the transcript, so it stays visible without a confirmation prompt.
|
|
37
|
+
ROUTED, MANUAL_HANDOFF, and WAIT_FOR_PROJECT_TEAM with no real
|
|
38
|
+
alternative are all unchanged — still explicit confirm steps.
|
|
39
|
+
|
|
40
|
+
Not yet in this release: showing which model is currently running for
|
|
41
|
+
a role directly in the PROJECT TEAM view, auto-reverting to the
|
|
42
|
+
originally-assigned model once it becomes eligible again, and
|
|
43
|
+
retrying a run that starts on the assigned model and then fails or
|
|
44
|
+
hangs mid-execution.
|
|
45
|
+
|
|
8
46
|
## 0.22.1 — 2026-09-18 (Kairo Runtime)
|
|
9
47
|
|
|
10
48
|
Patch release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Kal-elSam/harness#readme",
|
|
@@ -189,6 +189,24 @@ export async function runCockpitApp({
|
|
|
189
189
|
// handleListInput — 'x' never calls this without one).
|
|
190
190
|
return runAction("Asking PROJECT TEAM who should execute this", async () => {
|
|
191
191
|
const decision = await service.planExecution({ cwd, taskId, role });
|
|
192
|
+
// WAIT_FOR_PROJECT_TEAM with a real suggested alternative: the
|
|
193
|
+
// human already explicitly asked for this task to execute now
|
|
194
|
+
// (picked the role, pressed execute) — the assigned model just
|
|
195
|
+
// isn't the one that got them there. Kairo runs the real,
|
|
196
|
+
// currently-eligible fallback automatically, with zero extra
|
|
197
|
+
// y/n gate, and narrates the substitution into the transcript
|
|
198
|
+
// so it stays visible even without a confirm prompt. Every
|
|
199
|
+
// other decision (ROUTED, MANUAL_HANDOFF, or WAIT_FOR_PROJECT_TEAM
|
|
200
|
+
// with no real alternative) is unchanged — still an explicit
|
|
201
|
+
// confirm step.
|
|
202
|
+
if (decision.decision === "WAIT_FOR_PROJECT_TEAM" && decision.confirmationTarget?.selection === "suggested-alternative") {
|
|
203
|
+
const blockedLabel = decision.blockedAssignment?.model?.displayName ?? decision.blockedAssignment?.model?.modelId ?? decision.blockedAssignment?.provider ?? "the assigned model";
|
|
204
|
+
const alt = decision.suggestedAlternative;
|
|
205
|
+
const altLabel = alt?.model?.displayName ?? alt?.model?.modelId ?? "unknown model";
|
|
206
|
+
pushTranscript("kairo", `${blockedLabel} is unavailable for ${decision.role} — automatically falling back to ${alt?.provider} · ${altLabel}.`);
|
|
207
|
+
await service.executePlan({ cwd, taskId, confirmationTarget: decision.confirmationTarget });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
192
210
|
view.showExecuteConfirm(taskId, decision);
|
|
193
211
|
// MANUAL_HANDOFF: Kairo can't launch this itself, so the real
|
|
194
212
|
// task text (the exact same one an automatic run would get —
|
|
@@ -230,7 +248,7 @@ export async function runCockpitApp({
|
|
|
230
248
|
});
|
|
231
249
|
}
|
|
232
250
|
|
|
233
|
-
editor.onSubmit = (text) => {
|
|
251
|
+
editor.onSubmit = async (text) => {
|
|
234
252
|
const task = text.trim();
|
|
235
253
|
if (!task) return;
|
|
236
254
|
if (task.startsWith("/")) {
|
|
@@ -417,7 +435,38 @@ export async function runCockpitApp({
|
|
|
417
435
|
// PLAN/AGENT always create a plan (see service.submitTask) — replacing
|
|
418
436
|
// the old isLikelyQuestion guess with what the user explicitly told
|
|
419
437
|
// Kairo they're doing (Shift+Tab / /plan).
|
|
420
|
-
|
|
438
|
+
//
|
|
439
|
+
// Kairo is the system, never the thing actually answering — a real
|
|
440
|
+
// adapter (Codex, Claude, …) always is, and that real name should be
|
|
441
|
+
// visible from the moment the wait starts, not just in a successful
|
|
442
|
+
// result or leaked incidentally through an error message. PLAN/AGENT
|
|
443
|
+
// is deterministically Codex (see submitArchitecture/createPlan), so
|
|
444
|
+
// that label needs no extra call; ASK's real provider depends on
|
|
445
|
+
// current quota/eligibility, so a cheap planAsk preview (the same
|
|
446
|
+
// routing submitTask itself will use, its probes cached — see
|
|
447
|
+
// service.js's planAsk doc) resolves it first. A caller that predates
|
|
448
|
+
// planAsk (or a preview that itself fails) falls back to the old
|
|
449
|
+
// generic label rather than ever blocking the real submit on it.
|
|
450
|
+
const mode = view.workMode;
|
|
451
|
+
let askLabel = "Asking Kairo";
|
|
452
|
+
if (mode === "plan" || mode === "agent") {
|
|
453
|
+
askLabel = "Asking Codex to plan";
|
|
454
|
+
} else {
|
|
455
|
+
try {
|
|
456
|
+
const preview = await service.planAsk?.({ cwd, task });
|
|
457
|
+
if (preview?.decision?.decision === "ROUTED") {
|
|
458
|
+
const provider = preview.decision.provider;
|
|
459
|
+
const providerLabel = provider ? provider.charAt(0).toUpperCase() + provider.slice(1) : null;
|
|
460
|
+
if (providerLabel) {
|
|
461
|
+
askLabel = `Asking ${providerLabel}${preview.decision.model ? ` · ${preview.decision.model}` : ""}`;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
// Best-effort label only — a real routing failure still surfaces
|
|
466
|
+
// through submitTask itself below, never swallowed here.
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return runAction(askLabel, async () => {
|
|
421
470
|
const result = await service.submitTask({ cwd, task, mode: view.workMode });
|
|
422
471
|
if (result.kind === "answer") {
|
|
423
472
|
pushTranscript("kairo", `${result.provider}${result.model ? ` · ${result.model}` : ""}: ${result.answer}`);
|
|
@@ -86,9 +86,13 @@ function blocked(role, strategyFingerprint, why, { blockedAssignment = null, sug
|
|
|
86
86
|
* `suggestedAlternative` set to the strategy's own PERSISTED fallback
|
|
87
87
|
* for this role (see project-strategy.js) when that fallback is
|
|
88
88
|
* itself currently real-eligible and automatically executable —
|
|
89
|
-
* otherwise honestly null.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
89
|
+
* otherwise honestly null. This module never launches anything itself
|
|
90
|
+
* — it only ever decides what WOULD run — but by explicit, deliberate
|
|
91
|
+
* product decision the cockpit caller (app.js's onRequestExecute) does
|
|
92
|
+
* treat a real suggestedAlternative as automatically executable once
|
|
93
|
+
* the human has already asked for this task to run, narrating the
|
|
94
|
+
* substitution into the transcript instead of gating it behind an
|
|
95
|
+
* extra y/n prompt.
|
|
92
96
|
* 5. Otherwise -> ROUTED, with the real provider/model/assignmentSource
|
|
93
97
|
* this role delegates to.
|
|
94
98
|
* @param {object} args
|
|
@@ -651,12 +651,16 @@ export function createConversationService(deps = {}) {
|
|
|
651
651
|
return { ...publicPlan(result.status), reused: result.reused === true, projectRoot };
|
|
652
652
|
},
|
|
653
653
|
/**
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
654
|
+
* Read-only preview of who ASK mode would actually ask right now —
|
|
655
|
+
* never calls a provider. Lets a caller (the cockpit's action label)
|
|
656
|
+
* show the real provider/model BEFORE the potentially slow real call
|
|
657
|
+
* starts, instead of a generic "Asking Kairo" that stays true no
|
|
658
|
+
* matter which real provider ends up answering (or timing out).
|
|
659
|
+
* Cheap to call again right after: the same underlying usage/catalog
|
|
660
|
+
* probes askQuestion itself uses are cached (see readCodexUsageCached
|
|
661
|
+
* etc.), so there's no real duplicate provider I/O.
|
|
658
662
|
*/
|
|
659
|
-
async
|
|
663
|
+
async planAsk({ cwd, task }) {
|
|
660
664
|
const projectRoot = await root(cwd);
|
|
661
665
|
const adapters = inspectAdapters({ cwd: projectRoot });
|
|
662
666
|
let codexUsage = null;
|
|
@@ -672,6 +676,16 @@ export function createConversationService(deps = {}) {
|
|
|
672
676
|
claudeCatalog = readClaudeModelsImpl();
|
|
673
677
|
}
|
|
674
678
|
const decision = routeAsk({ adapters, codexUsage, claudeUsage, catalogs: { codex: codexCatalog, claude: claudeCatalog }, taskText: task });
|
|
679
|
+
return { decision, projectRoot };
|
|
680
|
+
},
|
|
681
|
+
/**
|
|
682
|
+
* Real read-only question -> real answer, via whichever provider is
|
|
683
|
+
* actually available/quota-healthy — no task, no plan, no approval
|
|
684
|
+
* gate. Throws (never returns a fabricated answer) if no provider can
|
|
685
|
+
* answer or the call itself fails.
|
|
686
|
+
*/
|
|
687
|
+
async askQuestion({ cwd, task }) {
|
|
688
|
+
const { decision, projectRoot } = await this.planAsk({ cwd, task });
|
|
675
689
|
if (decision.decision !== "ROUTED") throw new Error(`Cannot answer: ${decision.why}`);
|
|
676
690
|
const result = await askProviderImpl({ provider: decision.provider, question: task, model: decision.model, cwd: projectRoot });
|
|
677
691
|
if (result.status !== "answered") throw new Error(result.error ?? `${decision.provider} gave no answer.`);
|
|
@@ -89,7 +89,7 @@ export async function readCodexModels({
|
|
|
89
89
|
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before model list")); });
|
|
90
90
|
|
|
91
91
|
writeRequest(child, 1, "initialize", {
|
|
92
|
-
clientInfo: { name: "kairo", title: "Kairo", version: "0.
|
|
92
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.23.1" },
|
|
93
93
|
capabilities: {}
|
|
94
94
|
});
|
|
95
95
|
});
|
|
@@ -151,7 +151,7 @@ export async function readCodexUsage({
|
|
|
151
151
|
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before rate limits")); });
|
|
152
152
|
|
|
153
153
|
writeRequest(child, 1, "initialize", {
|
|
154
|
-
clientInfo: { name: "kairo", title: "Kairo", version: "0.
|
|
154
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.23.1" },
|
|
155
155
|
capabilities: {}
|
|
156
156
|
});
|
|
157
157
|
});
|