@kontextmind/kxm 0.7.150 → 0.7.151
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +14 -0
- package/docs/architecture/agent-path.md +5 -0
- package/docs/operations/troubleshooting.md +1 -0
- package/docs/reference/cli-reference.md +4 -4
- package/docs/reference/config-reference.md +17 -4
- package/docs/reference/harness-routing.md +7 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +153 -60
- package/plugins/kxm/dist/core.js +22 -3
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +676 -130
- package/plugins/kxm/dist/runtime.js +720 -164
- package/plugins/kxm/dist/server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/system.ts +4 -1
- package/plugins/kxm/src/engine-fold.ts +1 -0
- package/plugins/kxm/src/engine.ts +264 -22
- package/plugins/kxm/src/improve-sources.ts +17 -3
- package/plugins/kxm/src/improve.ts +14 -0
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/policy-draft.mjs +2 -2
- package/plugins/kxm/src/role.ts +1 -1
- package/plugins/kxm/src/route-fallback.ts +504 -0
- package/plugins/kxm/src/route-switch.ts +49 -0
- package/plugins/kxm/src/routing.ts +24 -1
- package/schemas/common.schema.json +2 -1
- package/schemas/role.schema.json +1 -1
- package/schemas/run-event.schema.json +20 -1
|
@@ -14992,7 +14992,7 @@ var TOOL_KEYS = Object.freeze(["preset", "allow", "deny"]);
|
|
|
14992
14992
|
var TEMPLATE_KEYS = Object.freeze(["template", "schema"]);
|
|
14993
14993
|
var POLICY_KEYS = Object.freeze(["vendorIndependenceRequired", "maxTransitions", "requiresGateVerification", "fallback"]);
|
|
14994
14994
|
var FALLBACK_KEYS = Object.freeze(["onError", "maxSwitches", "revert"]);
|
|
14995
|
-
var FALLBACK_ERRORS = Object.freeze(["rate_limit", "transport", "provider_unavailable"]);
|
|
14995
|
+
var FALLBACK_ERRORS = Object.freeze(["rate_limit", "transport", "provider_unavailable", "context_overflow"]);
|
|
14996
14996
|
var FALLBACK_REVERT = Object.freeze(["next_run", "never"]);
|
|
14997
14997
|
var ROSTER_ENTRY_KEYS = Object.freeze(["route", "effort", "mode"]);
|
|
14998
14998
|
var CRITIC_PURPOSES = Object.freeze(["reviewer-arch", "reviewer-cli"]);
|
package/plugins/kxm/package.json
CHANGED
|
@@ -597,6 +597,7 @@ export async function cmdImprove(runtime: Runtime, options: { file?: string | un
|
|
|
597
597
|
promotionPolicy: config.improvement.promotionPolicy,
|
|
598
598
|
autoThreshold: config.improvement.autoThreshold,
|
|
599
599
|
halfLifeDays: config.improvement.telemetryHalfLifeDays,
|
|
600
|
+
routeSwitches: loaded.routeSwitches,
|
|
600
601
|
});
|
|
601
602
|
|
|
602
603
|
const reportDir = join(runtime.dirs.assets, "improvements");
|
|
@@ -923,6 +924,7 @@ export async function cmdRoutingReport(
|
|
|
923
924
|
let file: string;
|
|
924
925
|
let records: Array<RoutingRecord | RoutingRecordV2>;
|
|
925
926
|
let sources: RoutingSourceSummary[] | undefined;
|
|
927
|
+
let routeSwitches: LoadedRoutingSources["routeSwitches"] = [];
|
|
926
928
|
if (options.file !== undefined) {
|
|
927
929
|
file = options.file;
|
|
928
930
|
records = readRoutingRecords(file).map((entry) => entry.routing);
|
|
@@ -933,6 +935,7 @@ export async function cmdRoutingReport(
|
|
|
933
935
|
file = telemetryPath(runtime.dirs.logs);
|
|
934
936
|
records = loaded.records;
|
|
935
937
|
sources = loaded.sources;
|
|
938
|
+
routeSwitches = loaded.routeSwitches;
|
|
936
939
|
}
|
|
937
940
|
const includeEquivalentListCost = Boolean(options.equivalentListCost || options.listPrices);
|
|
938
941
|
|
|
@@ -946,7 +949,7 @@ export async function cmdRoutingReport(
|
|
|
946
949
|
}
|
|
947
950
|
}
|
|
948
951
|
|
|
949
|
-
const report = generateRoutingReport(records, { catalog, includeEquivalentListCost });
|
|
952
|
+
const report = generateRoutingReport(records, { catalog, includeEquivalentListCost, routeSwitches });
|
|
950
953
|
|
|
951
954
|
if (records.length === 0) {
|
|
952
955
|
print(runtime.io, runtime.json, { ok: true, command: "routing report", file, ...(sources ? { sources } : {}), configurations: [], report }, "no routing records in telemetry");
|
|
@@ -555,6 +555,7 @@ export function foldKxmRunState(
|
|
|
555
555
|
foldEffectUncertain(state, plan!, event);
|
|
556
556
|
break;
|
|
557
557
|
case "routing.attempt.recorded":
|
|
558
|
+
case "routing.route_switched":
|
|
558
559
|
break;
|
|
559
560
|
default:
|
|
560
561
|
throw runtimeError("run_events_illegal", run.runId, `event type ${event.eventType} is not legal in this engine slice`);
|
|
@@ -3,6 +3,17 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import { listRoleBindings, loadRoutePolicy } from "./routes.ts";
|
|
6
|
+
import {
|
|
7
|
+
carryForwardPrompt,
|
|
8
|
+
classifyRouteFailure,
|
|
9
|
+
decideRouteSwitch,
|
|
10
|
+
loadAgentFallback,
|
|
11
|
+
mergeSessionCarry,
|
|
12
|
+
parseSessionCarry,
|
|
13
|
+
type FallbackCandidate,
|
|
14
|
+
type FallbackPolicy,
|
|
15
|
+
type RouteSessionCarry,
|
|
16
|
+
} from "./route-fallback.ts";
|
|
6
17
|
import { findYamlBasename } from "./workforce-names.mjs";
|
|
7
18
|
import { applyAuthoringWitness, captureWorktreeWitness } from "./worktree-witness.ts";
|
|
8
19
|
import {
|
|
@@ -878,10 +889,58 @@ export function verifyKxmAttemptCapability(
|
|
|
878
889
|
};
|
|
879
890
|
}
|
|
880
891
|
|
|
892
|
+
interface ProducerFailureObservation {
|
|
893
|
+
classified: ReturnType<typeof classifyRouteFailure>;
|
|
894
|
+
sessionCarry?: RouteSessionCarry;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
interface AttemptFallbackState {
|
|
898
|
+
policy: FallbackPolicy;
|
|
899
|
+
chain: FallbackCandidate[];
|
|
900
|
+
index: number;
|
|
901
|
+
switches: number;
|
|
902
|
+
basePrompt: string;
|
|
903
|
+
stickyRouteId?: string;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const attemptFallback = new Map<string, AttemptFallbackState>();
|
|
907
|
+
const runStickyRoute = new Map<string, string>();
|
|
908
|
+
|
|
909
|
+
function observeProducerFailure(error: unknown, capability: string | undefined): { code: string; failure: ProducerFailureObservation } {
|
|
910
|
+
// Never throw while classifying a throw: a producer can reject with anything
|
|
911
|
+
// (Object.create(null) defeats String(), an Error whose message is not a string
|
|
912
|
+
// defeats split()), and a throwing classifier would reroute a producer_rejected
|
|
913
|
+
// settle into the execution-error path.
|
|
914
|
+
try {
|
|
915
|
+
const raw = error instanceof Error ? error.message : error;
|
|
916
|
+
const message = typeof raw === "string" ? raw : "";
|
|
917
|
+
let text = message;
|
|
918
|
+
if (capability) text = text.split(capability).join("[redacted]");
|
|
919
|
+
text = text.replace(/kxmcap_[A-Za-z0-9_-]+/g, "[redacted]");
|
|
920
|
+
const match = /^[a-z][a-z0-9]*(?:_[a-z0-9]+){1,3}/.exec(text);
|
|
921
|
+
const extracted = match?.[0] ?? "";
|
|
922
|
+
const code = extracted.length > 0 && extracted.length <= 64 ? extracted : "producer_error";
|
|
923
|
+
const record = error && typeof error === "object" ? error as Record<string, unknown> : undefined;
|
|
924
|
+
const statusValue = record?.statusCode ?? record?.status ?? record?.httpStatus;
|
|
925
|
+
const httpStatus = typeof statusValue === "number" && Number.isInteger(statusValue) ? statusValue : undefined;
|
|
926
|
+
const name = error instanceof Error ? error.name : undefined;
|
|
927
|
+
const sessionCarry = parseSessionCarry(record?.sessionCarry);
|
|
928
|
+
return {
|
|
929
|
+
code,
|
|
930
|
+
failure: {
|
|
931
|
+
classified: classifyRouteFailure({ code, text: text.slice(0, 500), httpStatus, name }),
|
|
932
|
+
...(sessionCarry ? { sessionCarry } : {}),
|
|
933
|
+
},
|
|
934
|
+
};
|
|
935
|
+
} catch {
|
|
936
|
+
return { code: "producer_error", failure: { classified: classifyRouteFailure({ code: "producer_error" }) } };
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
881
940
|
function invokeProducer(
|
|
882
941
|
producer: KxmProducer,
|
|
883
942
|
request: KxmProducerRequest,
|
|
884
|
-
): Promise<{ result?: KxmProducerResult; error: boolean; errorCode?: string }> {
|
|
943
|
+
): Promise<{ result?: KxmProducerResult; error: boolean; errorCode?: string; failure?: ProducerFailureObservation }> {
|
|
885
944
|
// The reason a producer refused is the single most useful fact about a failed attempt,
|
|
886
945
|
// and it used to be discarded here: a live drive that could not authenticate read in the
|
|
887
946
|
// event log exactly like a simulation that ran fine. But the message is producer-
|
|
@@ -890,36 +949,184 @@ function invokeProducer(
|
|
|
890
949
|
// So: redact the attempt capability and anything capability-shaped, then keep only a
|
|
891
950
|
// leading machine-code identifier (lowercase segments joined by underscores). Anything
|
|
892
951
|
// else — prose, free text, a redacted-to-empty message — records as producer_error.
|
|
893
|
-
const reason = (error: unknown): string => {
|
|
894
|
-
// Never throw while classifying a throw: a producer can reject with anything
|
|
895
|
-
// (Object.create(null) defeats String(), an Error whose message is not a string
|
|
896
|
-
// defeats split()), and a throwing classifier would reroute a producer_rejected
|
|
897
|
-
// settle into the execution-error path — the exact invisibility this exists to fix.
|
|
898
|
-
try {
|
|
899
|
-
const raw = error instanceof Error ? error.message : error;
|
|
900
|
-
const message = typeof raw === "string" ? raw : "";
|
|
901
|
-
let text = message;
|
|
902
|
-
if (request.capability) text = text.split(request.capability).join("[redacted]");
|
|
903
|
-
text = text.replace(/kxmcap_[A-Za-z0-9_-]+/g, "[redacted]");
|
|
904
|
-
const match = /^[a-z][a-z0-9]*(?:_[a-z0-9]+){1,3}/.exec(text);
|
|
905
|
-
const code = match?.[0] ?? "";
|
|
906
|
-
return code.length > 0 && code.length <= 64 ? code : "producer_error";
|
|
907
|
-
} catch {
|
|
908
|
-
return "producer_error";
|
|
909
|
-
}
|
|
910
|
-
};
|
|
911
952
|
let pending: Promise<KxmProducerResult>;
|
|
912
953
|
try {
|
|
913
954
|
pending = Promise.resolve(producer.produce(request));
|
|
914
955
|
} catch (error) {
|
|
915
|
-
|
|
956
|
+
const observed = observeProducerFailure(error, request.capability);
|
|
957
|
+
return Promise.resolve({ error: true, errorCode: observed.code, failure: observed.failure });
|
|
916
958
|
}
|
|
917
959
|
return pending.then(
|
|
918
960
|
(result) => ({ result, error: false }),
|
|
919
|
-
(error) =>
|
|
961
|
+
(error) => {
|
|
962
|
+
const observed = observeProducerFailure(error, request.capability);
|
|
963
|
+
return { error: true, errorCode: observed.code, failure: observed.failure };
|
|
964
|
+
},
|
|
920
965
|
);
|
|
921
966
|
}
|
|
922
967
|
|
|
968
|
+
function stickyRouteFromEvents(context: KxmRuntimeContext, runId: string): string | undefined {
|
|
969
|
+
const remembered = runStickyRoute.get(runId);
|
|
970
|
+
if (remembered) return remembered;
|
|
971
|
+
let found: string | undefined;
|
|
972
|
+
for (const event of context.eventStore.events(runId, 0, 100000)) {
|
|
973
|
+
if (event.eventType !== "routing.attempt.recorded") continue;
|
|
974
|
+
const routing = (event.payload as { routing?: { providerMetadata?: { fallbackRoute?: unknown } } }).routing;
|
|
975
|
+
const route = routing?.providerMetadata?.fallbackRoute;
|
|
976
|
+
if (typeof route === "string" && route.length > 0) found = route;
|
|
977
|
+
}
|
|
978
|
+
return found;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function stickyWalkRoute(
|
|
982
|
+
context: KxmRuntimeContext,
|
|
983
|
+
step: KxmCompiledStep,
|
|
984
|
+
agentId: string,
|
|
985
|
+
runId: string,
|
|
986
|
+
liveWrite: boolean,
|
|
987
|
+
): ResolvedProducerRoute | undefined {
|
|
988
|
+
const loaded = loadAgentFallback(context.projectRoot, agentId, { liveWrite });
|
|
989
|
+
if (!loaded || loaded.policy.revert !== "never") return undefined;
|
|
990
|
+
const sticky = stickyRouteFromEvents(context, runId);
|
|
991
|
+
if (!sticky) return undefined;
|
|
992
|
+
const candidate = loaded.chain.find((item) => item.routeId === sticky);
|
|
993
|
+
if (!candidate) return undefined;
|
|
994
|
+
return candidateToRoute(candidate, loaded.roleId);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function candidateToRoute(candidate: FallbackCandidate, role: string): ResolvedProducerRoute {
|
|
998
|
+
return {
|
|
999
|
+
provider: candidate.provider,
|
|
1000
|
+
model: candidate.model,
|
|
1001
|
+
selector: candidate.selector,
|
|
1002
|
+
harness: candidate.harness,
|
|
1003
|
+
routeId: candidate.routeId,
|
|
1004
|
+
role,
|
|
1005
|
+
permissions: [...candidate.permissions],
|
|
1006
|
+
...(candidate.effort ? { effort: candidate.effort } : {}),
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function rememberAttemptFallback(
|
|
1011
|
+
attemptId: string,
|
|
1012
|
+
loaded: { policy: FallbackPolicy; chain: FallbackCandidate[] },
|
|
1013
|
+
index: number,
|
|
1014
|
+
basePrompt: string,
|
|
1015
|
+
): void {
|
|
1016
|
+
if (loaded.chain.length < 2 || loaded.policy.onError.length === 0 || loaded.policy.maxSwitches <= 0) return;
|
|
1017
|
+
if (index < 0 || index >= loaded.chain.length) return;
|
|
1018
|
+
attemptFallback.set(attemptId, {
|
|
1019
|
+
policy: loaded.policy,
|
|
1020
|
+
chain: loaded.chain,
|
|
1021
|
+
index,
|
|
1022
|
+
switches: 0,
|
|
1023
|
+
basePrompt,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function appendRouteSwitch(
|
|
1028
|
+
context: KxmRuntimeContext,
|
|
1029
|
+
member: PreparedDispatch,
|
|
1030
|
+
routeSwitch: { from: string; to: string; reason: string; effort?: string },
|
|
1031
|
+
): void {
|
|
1032
|
+
const run = requireRun(context, member.run.runId);
|
|
1033
|
+
const now = new Date().toISOString();
|
|
1034
|
+
const sequence = context.eventStore.nextSequence(run.runId);
|
|
1035
|
+
const event: KxmRunEvent = {
|
|
1036
|
+
...kxmEventBase(context, run, now, kxmMonotonicNs()),
|
|
1037
|
+
eventId: newKxmEventId(),
|
|
1038
|
+
eventType: "routing.route_switched",
|
|
1039
|
+
sequence,
|
|
1040
|
+
payload: {
|
|
1041
|
+
attemptId: member.attemptId,
|
|
1042
|
+
stepId: member.stepId,
|
|
1043
|
+
routeSwitch: {
|
|
1044
|
+
from: routeSwitch.from,
|
|
1045
|
+
to: routeSwitch.to,
|
|
1046
|
+
reason: routeSwitch.reason,
|
|
1047
|
+
...(routeSwitch.effort ? { effort: routeSwitch.effort } : {}),
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
};
|
|
1051
|
+
context.eventStore.appendEvent(event);
|
|
1052
|
+
const next = foldStoredKxmRun(context, run);
|
|
1053
|
+
persistKxmRunState(context, run.runId, next, sequence);
|
|
1054
|
+
try {
|
|
1055
|
+
context.logger?.({
|
|
1056
|
+
event: "route_switch",
|
|
1057
|
+
runId: run.runId,
|
|
1058
|
+
attemptId: member.attemptId,
|
|
1059
|
+
stepId: member.stepId,
|
|
1060
|
+
from: routeSwitch.from,
|
|
1061
|
+
to: routeSwitch.to,
|
|
1062
|
+
reason: routeSwitch.reason,
|
|
1063
|
+
...(routeSwitch.effort ? { effort: routeSwitch.effort } : {}),
|
|
1064
|
+
});
|
|
1065
|
+
} catch {
|
|
1066
|
+
// Logging is best effort and never fails a switch.
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function produceWithRouteFallback(
|
|
1071
|
+
context: KxmRuntimeContext,
|
|
1072
|
+
member: PreparedDispatch,
|
|
1073
|
+
producer: KxmProducer,
|
|
1074
|
+
): Promise<{ result?: KxmProducerResult; error: boolean; errorCode?: string }> {
|
|
1075
|
+
const session = attemptFallback.get(member.attemptId);
|
|
1076
|
+
let request = member.request;
|
|
1077
|
+
let produced = await invokeProducer(producer, request);
|
|
1078
|
+
if (!session) return producerOutcome(produced);
|
|
1079
|
+
let carry: RouteSessionCarry | undefined;
|
|
1080
|
+
while (produced.error) {
|
|
1081
|
+
const current = session.chain[session.index];
|
|
1082
|
+
if (!current) break;
|
|
1083
|
+
const failure = produced.failure?.classified ?? classifyRouteFailure({ code: produced.errorCode, text: produced.errorCode });
|
|
1084
|
+
const decision = decideRouteSwitch({
|
|
1085
|
+
policy: session.policy,
|
|
1086
|
+
chain: session.chain,
|
|
1087
|
+
currentRouteId: current.routeId,
|
|
1088
|
+
switchesUsed: session.switches,
|
|
1089
|
+
failure,
|
|
1090
|
+
...(current.effort || member.request.thinking ? { currentEffort: current.effort ?? member.request.thinking } : {}),
|
|
1091
|
+
});
|
|
1092
|
+
if (decision.action !== "switch") break;
|
|
1093
|
+
appendRouteSwitch(context, member, {
|
|
1094
|
+
from: current.routeId,
|
|
1095
|
+
to: decision.to.routeId,
|
|
1096
|
+
reason: decision.reason,
|
|
1097
|
+
...(decision.effort ? { effort: decision.effort } : {}),
|
|
1098
|
+
});
|
|
1099
|
+
session.switches += 1;
|
|
1100
|
+
session.index = session.chain.findIndex((candidate) => candidate.routeId === decision.to.routeId);
|
|
1101
|
+
carry = mergeSessionCarry(carry, produced.failure?.sessionCarry);
|
|
1102
|
+
request = {
|
|
1103
|
+
...member.request,
|
|
1104
|
+
prompt: carryForwardPrompt(session.basePrompt, carry),
|
|
1105
|
+
model: decision.to.model,
|
|
1106
|
+
provider: decision.to.provider,
|
|
1107
|
+
harness: decision.to.harness,
|
|
1108
|
+
...(decision.effort ? { thinking: decision.effort } : {}),
|
|
1109
|
+
};
|
|
1110
|
+
produced = await invokeProducer(producer, request);
|
|
1111
|
+
}
|
|
1112
|
+
if (!produced.error && session.switches > 0 && session.policy.revert === "never") {
|
|
1113
|
+
const route = session.chain[session.index];
|
|
1114
|
+
if (route) {
|
|
1115
|
+
session.stickyRouteId = route.routeId;
|
|
1116
|
+
runStickyRoute.set(member.run.runId, route.routeId);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return producerOutcome(produced);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function producerOutcome(produced: { result?: KxmProducerResult; error: boolean; errorCode?: string }): { result?: KxmProducerResult; error: boolean; errorCode?: string } {
|
|
1123
|
+
return {
|
|
1124
|
+
error: produced.error,
|
|
1125
|
+
...(produced.result !== undefined ? { result: produced.result } : {}),
|
|
1126
|
+
...(produced.errorCode !== undefined ? { errorCode: produced.errorCode } : {}),
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
923
1130
|
export interface KxmPreparedGateDispatch {
|
|
924
1131
|
readonly run: KxmRunRecord;
|
|
925
1132
|
readonly envelope: KxmRunPlanEnvelope;
|
|
@@ -1748,6 +1955,14 @@ function prepareDispatch(
|
|
|
1748
1955
|
return { kind: "return", state, handoff: { ...routeResult.error, stepId } };
|
|
1749
1956
|
}
|
|
1750
1957
|
resolvedRoute = routeResult;
|
|
1958
|
+
const stickyRoute = stickyWalkRoute(
|
|
1959
|
+
context,
|
|
1960
|
+
step,
|
|
1961
|
+
agentId,
|
|
1962
|
+
run.runId,
|
|
1963
|
+
Object.values(step.repositories).some((access) => access === "write"),
|
|
1964
|
+
);
|
|
1965
|
+
if (stickyRoute && stickyRoute.routeId !== resolvedRoute.routeId) resolvedRoute = stickyRoute;
|
|
1751
1966
|
const writeRefusal = unsupportedLiveWrite(
|
|
1752
1967
|
context.projectRoot,
|
|
1753
1968
|
step,
|
|
@@ -1885,6 +2100,27 @@ function birthMember(
|
|
|
1885
2100
|
}
|
|
1886
2101
|
resolvedRoute = routeResult;
|
|
1887
2102
|
}
|
|
2103
|
+
const liveProducer = Boolean(input.producerId && input.producerId !== "driver-simulated");
|
|
2104
|
+
const liveWrite = liveProducer && Object.values(input.step.repositories).some((access) => access === "write");
|
|
2105
|
+
const loadedFallback = loadAgentFallback(context.projectRoot, agentId, { liveWrite });
|
|
2106
|
+
let fallbackIndex = -1;
|
|
2107
|
+
if (loadedFallback && loadedFallback.chain.length > 1 && loadedFallback.policy.maxSwitches > 0) {
|
|
2108
|
+
const sticky = loadedFallback.policy.revert === "never" ? stickyRouteFromEvents(context, run.runId) : undefined;
|
|
2109
|
+
const stickyIndex = sticky ? loadedFallback.chain.findIndex((candidate) => candidate.routeId === sticky) : -1;
|
|
2110
|
+
if (!resolvedRoute) {
|
|
2111
|
+
fallbackIndex = stickyIndex > 0 ? stickyIndex : 0;
|
|
2112
|
+
const start = loadedFallback.chain[fallbackIndex];
|
|
2113
|
+
if (start) resolvedRoute = candidateToRoute(start, loadedFallback.roleId);
|
|
2114
|
+
} else if (stickyIndex > 0 && resolvedRoute.routeId !== loadedFallback.chain[stickyIndex]?.routeId) {
|
|
2115
|
+
const start = loadedFallback.chain[stickyIndex];
|
|
2116
|
+
if (start) {
|
|
2117
|
+
resolvedRoute = candidateToRoute(start, loadedFallback.roleId);
|
|
2118
|
+
fallbackIndex = stickyIndex;
|
|
2119
|
+
}
|
|
2120
|
+
} else {
|
|
2121
|
+
fallbackIndex = loadedFallback.chain.findIndex((candidate) => candidate.routeId === resolvedRoute?.routeId);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
1888
2124
|
const dispatchContext = assembleDispatchContext(input.dispatchSources, {
|
|
1889
2125
|
projectId: run.projectId,
|
|
1890
2126
|
runId: run.runId,
|
|
@@ -2024,6 +2260,9 @@ function birthMember(
|
|
|
2024
2260
|
// Logging is best effort and never fails a birth.
|
|
2025
2261
|
}
|
|
2026
2262
|
}
|
|
2263
|
+
if (loadedFallback && fallbackIndex >= 0) {
|
|
2264
|
+
rememberAttemptFallback(attemptId, loadedFallback, fallbackIndex, member.request.prompt ?? "");
|
|
2265
|
+
}
|
|
2027
2266
|
kxmPanelDispatchSeams.afterBirth?.(member);
|
|
2028
2267
|
return member;
|
|
2029
2268
|
}
|
|
@@ -2172,7 +2411,7 @@ async function drivePanel(
|
|
|
2172
2411
|
const live = member.producerId !== "driver-simulated";
|
|
2173
2412
|
const writes = Object.values(member.step.repositories).some((access) => access === "write");
|
|
2174
2413
|
const before = live ? captureWorktreeWitness(context.projectRoot) : undefined;
|
|
2175
|
-
const produced = await
|
|
2414
|
+
const produced = await produceWithRouteFallback(context, member, producer);
|
|
2176
2415
|
if (live && produced.result && before) {
|
|
2177
2416
|
const after = captureWorktreeWitness(context.projectRoot);
|
|
2178
2417
|
return {
|
|
@@ -2384,6 +2623,8 @@ function engineRoutingMetadata(
|
|
|
2384
2623
|
// Already `sha256:<hex>` from acceptance; the prompt text is never read here.
|
|
2385
2624
|
metadata.objectiveSha256 = dispatch.run.promptSha256;
|
|
2386
2625
|
metadata.stepWrites = Object.values(dispatch.step.repositories).some((access) => access === "write");
|
|
2626
|
+
const stickyRouteId = attemptFallback.get(dispatch.attemptId)?.stickyRouteId;
|
|
2627
|
+
if (stickyRouteId && Object.keys(metadata).length < MAX_PROVIDER_METADATA_FIELDS) metadata.fallbackRoute = stickyRouteId;
|
|
2387
2628
|
return metadata;
|
|
2388
2629
|
}
|
|
2389
2630
|
|
|
@@ -2555,6 +2796,7 @@ function settleMember(
|
|
|
2555
2796
|
}
|
|
2556
2797
|
}
|
|
2557
2798
|
context.eventStore.settleCapability(dispatch.attemptId, "settled");
|
|
2799
|
+
attemptFallback.delete(dispatch.attemptId);
|
|
2558
2800
|
for (const event of events) context.eventStore.appendEvent(event);
|
|
2559
2801
|
const next = foldStoredKxmRun(context, run);
|
|
2560
2802
|
persistKxmRunState(context, run.runId, next, events[events.length - 1]!.sequence);
|
|
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { DatabaseSync } from "./sqlite.ts";
|
|
4
4
|
import { discoverKxmProjectRoot } from "./project-config.ts";
|
|
5
5
|
import { kxmProjectRunEventsPath } from "./runtime-store.ts";
|
|
6
|
+
import { parseRouteSwitchRecord, type RouteSwitchRecord } from "./route-switch.ts";
|
|
6
7
|
import { parseRoutingRecordV2, ROUTING_RECORD_V2_SCHEMA, type RoutingRecord, type RoutingRecordV2 } from "./routing.ts";
|
|
7
8
|
import { readRoutingRecords, telemetryPath } from "./telemetry.ts";
|
|
8
9
|
|
|
@@ -30,16 +31,18 @@ export interface RoutingSourceSummary {
|
|
|
30
31
|
export interface EngineRoutingRead {
|
|
31
32
|
records: RoutingRecordV2[];
|
|
32
33
|
source: RoutingSourceSummary;
|
|
34
|
+
routeSwitches: RouteSwitchRecord[];
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export interface LoadedRoutingSources {
|
|
36
38
|
projectRoot?: string;
|
|
37
39
|
records: Array<RoutingRecord | RoutingRecordV2>;
|
|
38
40
|
sources: RoutingSourceSummary[];
|
|
41
|
+
routeSwitches: RouteSwitchRecord[];
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
const ENGINE_EVENTS_SQL = "SELECT run_id, sequence, event_type, payload FROM events"
|
|
42
|
-
+ " WHERE event_type IN ('routing.attempt.recorded','step.entered','run.status_changed')"
|
|
45
|
+
+ " WHERE event_type IN ('routing.attempt.recorded','routing.route_switched','step.entered','run.status_changed')"
|
|
43
46
|
+ " ORDER BY run_id, sequence";
|
|
44
47
|
|
|
45
48
|
/** The simulated producer's harness label; its attempts measure nothing. */
|
|
@@ -81,7 +84,7 @@ export function readEngineRoutingRecords(path: string): EngineRoutingRead {
|
|
|
81
84
|
undecided: 0,
|
|
82
85
|
duplicatesDropped: 0,
|
|
83
86
|
};
|
|
84
|
-
if (!existsSync(path)) return { records: [], source };
|
|
87
|
+
if (!existsSync(path)) return { records: [], source, routeSwitches: [] };
|
|
85
88
|
source.exists = true;
|
|
86
89
|
|
|
87
90
|
let rows: Array<Record<string, unknown>>;
|
|
@@ -102,6 +105,7 @@ export function readEngineRoutingRecords(path: string): EngineRoutingRead {
|
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
const runs = new Map<string, RunLog>();
|
|
108
|
+
const routeSwitches: RouteSwitchRecord[] = [];
|
|
105
109
|
for (const row of rows) {
|
|
106
110
|
const runId = String(row.run_id);
|
|
107
111
|
const sequence = Number(row.sequence);
|
|
@@ -114,6 +118,14 @@ export function readEngineRoutingRecords(path: string): EngineRoutingRead {
|
|
|
114
118
|
}
|
|
115
119
|
if (eventType === "routing.attempt.recorded") {
|
|
116
120
|
run.routing.push({ sequence, payload });
|
|
121
|
+
} else if (eventType === "routing.route_switched") {
|
|
122
|
+
const parsed = parsePayload(payload);
|
|
123
|
+
const record = parseRouteSwitchRecord(parsed, {
|
|
124
|
+
attemptId: typeof parsed?.attemptId === "string" ? parsed.attemptId : undefined,
|
|
125
|
+
stepId: typeof parsed?.stepId === "string" ? parsed.stepId : undefined,
|
|
126
|
+
runId,
|
|
127
|
+
});
|
|
128
|
+
if (record) routeSwitches.push(record);
|
|
117
129
|
} else if (eventType === "step.entered") {
|
|
118
130
|
const stepId = parsePayload(payload)?.stepId;
|
|
119
131
|
if (typeof stepId === "string") run.lastEntered.set(stepId, sequence);
|
|
@@ -158,6 +170,7 @@ export function readEngineRoutingRecords(path: string): EngineRoutingRead {
|
|
|
158
170
|
}
|
|
159
171
|
return {
|
|
160
172
|
records,
|
|
173
|
+
routeSwitches,
|
|
161
174
|
source: { ...source, records: records.length, skippedInvalid, excludedSimulated, undecided },
|
|
162
175
|
};
|
|
163
176
|
}
|
|
@@ -189,7 +202,7 @@ export function loadRoutingSources(options: {
|
|
|
189
202
|
file?: string | undefined;
|
|
190
203
|
}): LoadedRoutingSources {
|
|
191
204
|
const projectRoot = discoverKxmProjectRoot(options.cwd);
|
|
192
|
-
const reads: Array<{ records: Array<RoutingRecord | RoutingRecordV2>; source: RoutingSourceSummary }> = [];
|
|
205
|
+
const reads: Array<{ records: Array<RoutingRecord | RoutingRecordV2>; source: RoutingSourceSummary; routeSwitches?: RouteSwitchRecord[] }> = [];
|
|
193
206
|
if (options.file !== undefined) {
|
|
194
207
|
reads.push(readJsonlSource("file", resolve(options.cwd, options.file)));
|
|
195
208
|
} else {
|
|
@@ -219,5 +232,6 @@ export function loadRoutingSources(options: {
|
|
|
219
232
|
...(projectRoot !== undefined ? { projectRoot } : {}),
|
|
220
233
|
records,
|
|
221
234
|
sources: reads.map((read) => read.source),
|
|
235
|
+
routeSwitches: reads.flatMap((read) => read.routeSwitches ?? []),
|
|
222
236
|
};
|
|
223
237
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import type { RouteSwitchRecord } from "./route-switch.ts";
|
|
4
5
|
import { computeDecayedWeight, type RoutingRecord, type RoutingRecordV2 } from "./routing.ts";
|
|
5
6
|
import { nowIso } from "./protocol.ts";
|
|
6
7
|
import { compareCodeUnitIds } from "./relevance.ts";
|
|
@@ -79,6 +80,7 @@ export interface ImprovementReport {
|
|
|
79
80
|
candidates: ImprovementCandidate[];
|
|
80
81
|
promotionPolicy: string;
|
|
81
82
|
promotion: ImprovementPromotionEntry[];
|
|
83
|
+
routeSwitches?: RouteSwitchRecord[] | undefined;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
export function classifyCandidateKind(stepId: string, agentRole: string): CandidateKind {
|
|
@@ -426,6 +428,7 @@ export interface BuildImprovementReportOptions extends GroupRoutingOptions {
|
|
|
426
428
|
/** improvement.promotionPolicy; reported as readiness only. */
|
|
427
429
|
promotionPolicy?: string | undefined;
|
|
428
430
|
autoThreshold?: PromotionAutoThreshold | undefined;
|
|
431
|
+
routeSwitches?: readonly RouteSwitchRecord[] | undefined;
|
|
429
432
|
}
|
|
430
433
|
|
|
431
434
|
export function buildImprovementReport(
|
|
@@ -498,6 +501,7 @@ export function buildImprovementReport(
|
|
|
498
501
|
});
|
|
499
502
|
}
|
|
500
503
|
|
|
504
|
+
const routeSwitches = options.routeSwitches?.filter((item) => item.from && item.to) ?? [];
|
|
501
505
|
return {
|
|
502
506
|
schema: IMPROVEMENT_REPORT_SCHEMA,
|
|
503
507
|
createdAt: nowIso(),
|
|
@@ -507,6 +511,7 @@ export function buildImprovementReport(
|
|
|
507
511
|
candidates,
|
|
508
512
|
promotionPolicy,
|
|
509
513
|
promotion,
|
|
514
|
+
...(routeSwitches.length > 0 ? { routeSwitches: [...routeSwitches] } : {}),
|
|
510
515
|
};
|
|
511
516
|
}
|
|
512
517
|
|
|
@@ -547,6 +552,15 @@ export function formatImprovementReport(report: ImprovementReport): string {
|
|
|
547
552
|
lines.push(`${wf} ${step} ${role} ${prompt} ${rec} ${runs} ${asks} ${weighted} ${cost} ${lat} ${accepted} ${rework} ${cand}`);
|
|
548
553
|
}
|
|
549
554
|
|
|
555
|
+
if (report.routeSwitches && report.routeSwitches.length > 0) {
|
|
556
|
+
lines.push("");
|
|
557
|
+
lines.push(`Route switches (${report.routeSwitches.length}):`);
|
|
558
|
+
for (const item of report.routeSwitches) {
|
|
559
|
+
const effort = item.effort ? ` effort ${item.effort}` : "";
|
|
560
|
+
lines.push(` - ${item.from} -> ${item.to} (${item.reason}${effort}) on ${item.stepId}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
550
564
|
if (report.candidates.length > 0) {
|
|
551
565
|
const readiness = new Map(report.promotion.map((entry) => [entry.candidateId, entry]));
|
|
552
566
|
lines.push("");
|
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.151";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -33,7 +33,7 @@ const TOOL_KEYS = Object.freeze(["preset", "allow", "deny"]);
|
|
|
33
33
|
const TEMPLATE_KEYS = Object.freeze(["template", "schema"]);
|
|
34
34
|
const POLICY_KEYS = Object.freeze(["vendorIndependenceRequired", "maxTransitions", "requiresGateVerification", "fallback"]);
|
|
35
35
|
const FALLBACK_KEYS = Object.freeze(["onError", "maxSwitches", "revert"]);
|
|
36
|
-
const FALLBACK_ERRORS = Object.freeze(["rate_limit", "transport", "provider_unavailable"]);
|
|
36
|
+
const FALLBACK_ERRORS = Object.freeze(["rate_limit", "transport", "provider_unavailable", "context_overflow"]);
|
|
37
37
|
const FALLBACK_REVERT = Object.freeze(["next_run", "never"]);
|
|
38
38
|
const ROSTER_ENTRY_KEYS = Object.freeze(["route", "effort", "mode"]);
|
|
39
39
|
const CRITIC_PURPOSES = Object.freeze(["reviewer-arch", "reviewer-cli"]);
|
|
@@ -398,7 +398,7 @@ function validateRoleShape(document, id, label, issues) {
|
|
|
398
398
|
if (fallback.onError !== undefined) {
|
|
399
399
|
if (!Array.isArray(fallback.onError) || new Set(fallback.onError).size !== fallback.onError.length
|
|
400
400
|
|| fallback.onError.some((item) => !FALLBACK_ERRORS.includes(item))) {
|
|
401
|
-
issues.push(issue("schema", "schema_enum", path, "onError must be unique rate_limit, transport, or
|
|
401
|
+
issues.push(issue("schema", "schema_enum", path, "onError must be unique rate_limit, transport, provider_unavailable, or context_overflow values"));
|
|
402
402
|
}
|
|
403
403
|
}
|
|
404
404
|
if (fallback.maxSwitches !== undefined && (!Number.isInteger(fallback.maxSwitches) || fallback.maxSwitches < 0)) {
|
package/plugins/kxm/src/role.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface KxmTemplateContract {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface KxmRoleFallback {
|
|
35
|
-
onError?: Array<"rate_limit" | "transport" | "provider_unavailable"> | undefined;
|
|
35
|
+
onError?: Array<"rate_limit" | "transport" | "provider_unavailable" | "context_overflow"> | undefined;
|
|
36
36
|
maxSwitches?: number | undefined;
|
|
37
37
|
revert?: "next_run" | "never" | undefined;
|
|
38
38
|
}
|