@elyracode/coding-agent 0.9.19 → 0.9.20

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +1 -0
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/cli/doctor-cli.d.ts.map +1 -1
  6. package/dist/cli/doctor-cli.js +8 -1
  7. package/dist/cli/doctor-cli.js.map +1 -1
  8. package/dist/config.d.ts +2 -0
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +4 -0
  11. package/dist/config.js.map +1 -1
  12. package/dist/core/model-fallback.d.ts +17 -5
  13. package/dist/core/model-fallback.d.ts.map +1 -1
  14. package/dist/core/model-fallback.js +29 -5
  15. package/dist/core/model-fallback.js.map +1 -1
  16. package/dist/core/routing-ledger.d.ts +83 -0
  17. package/dist/core/routing-ledger.d.ts.map +1 -0
  18. package/dist/core/routing-ledger.js +238 -0
  19. package/dist/core/routing-ledger.js.map +1 -0
  20. package/dist/core/sdk.d.ts.map +1 -1
  21. package/dist/core/sdk.js +36 -2
  22. package/dist/core/sdk.js.map +1 -1
  23. package/dist/core/settings-manager.d.ts +2 -0
  24. package/dist/core/settings-manager.d.ts.map +1 -1
  25. package/dist/core/settings-manager.js +3 -0
  26. package/dist/core/settings-manager.js.map +1 -1
  27. package/dist/core/slash-commands.d.ts.map +1 -1
  28. package/dist/core/slash-commands.js +1 -0
  29. package/dist/core/slash-commands.js.map +1 -1
  30. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  31. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  32. package/dist/modes/interactive/interactive-mode.js +20 -1
  33. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  34. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  35. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  36. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  37. package/examples/extensions/sandbox/package-lock.json +2 -2
  38. package/examples/extensions/sandbox/package.json +1 -1
  39. package/examples/extensions/with-deps/package-lock.json +2 -2
  40. package/examples/extensions/with-deps/package.json +1 -1
  41. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"model-fallback.js","sourceRoot":"","sources":["../../src/core/model-fallback.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Cross-provider failover: the reusable core of Elyra's \"AI sovereignty\"\n * behavior. When a turn fails because a provider is unavailable (overloaded,\n * rate-limited, an auth problem, or a model that vanished — e.g. an\n * export-control suspension), we can re-run the turn on an equivalent model\n * from a *different* provider instead of just surfacing the error.\n *\n * This is only possible because Elyra is provider-agnostic. A single-vendor\n * agent has nowhere to fail over to.\n *\n * Both functions here are pure so they can be unit-tested without real\n * providers, and reused by cross-provider consensus (#1) and per-repo model\n * shopping (#4), which need the same equivalence notion.\n */\n\n/** Minimal model shape the fallback selector needs (Model<Api> satisfies it). */\nexport interface FallbackModelInfo {\n\tprovider: string;\n\tid: string;\n\treasoning: boolean;\n\tinput: readonly string[];\n\tcost: { output: number };\n\tcontextWindow: number;\n}\n\nexport type FailureClass = \"failover\" | \"fatal\";\n\n/** Stable key for de-duping tried models across failover attempts. */\nexport function modelKey(m: { provider: string; id: string }): string {\n\treturn `${m.provider}/${m.id}`;\n}\n\n/** Extract an HTTP-ish status code from an unknown provider error, if present. */\nfunction extractStatus(error: unknown): number | undefined {\n\tif (typeof error !== \"object\" || error === null) return undefined;\n\tconst record = error as Record<string, unknown>;\n\tconst raw = record.status ?? record.statusCode ?? record.code;\n\tif (typeof raw === \"number\") return raw;\n\tif (typeof raw === \"string\" && /^\\d+$/.test(raw)) return Number.parseInt(raw, 10);\n\treturn undefined;\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/** Statuses that mean \"this provider can't serve the request right now\". */\nconst FAILOVER_STATUSES = new Set([401, 403, 404, 408, 409, 425, 429, 500, 502, 503, 504, 529]);\n/** Statuses that will fail identically on any provider — do not fail over. */\nconst FATAL_STATUSES = new Set([400, 413, 422]);\n\n/** Message fragments that indicate a transient/provider-specific failure. */\nconst FAILOVER_PATTERNS = [\n\t\"overloaded\",\n\t\"rate limit\",\n\t\"rate-limit\",\n\t\"capacity\",\n\t\"not available\",\n\t\"temporarily unavailable\",\n\t\"timeout\",\n\t\"timed out\",\n\t\"econnreset\",\n\t\"etimedout\",\n\t\"enotfound\",\n\t\"fetch failed\",\n\t\"socket hang up\",\n\t\"service unavailable\",\n];\n\n/** Message fragments that indicate a request that no provider will accept. */\nconst FATAL_PATTERNS = [\n\t\"context length\",\n\t\"maximum context\",\n\t\"context window\",\n\t\"too many tokens\",\n\t\"prompt is too long\",\n\t\"maximum number of tokens\",\n\t\"invalid request\",\n\t\"unsupported\",\n];\n\n/**\n * Decide whether a failed turn should be retried on a different provider.\n *\n * Conservative on the fatal side: an unrecognized error with no transient\n * signal is treated as fatal (surfaced as today) rather than burning a\n * failover attempt on what is probably a real bug or malformed request.\n */\nexport function classifyProviderFailure(error: unknown): FailureClass {\n\tconst status = extractStatus(error);\n\tif (status !== undefined) {\n\t\tif (FATAL_STATUSES.has(status)) return \"fatal\";\n\t\tif (FAILOVER_STATUSES.has(status)) return \"failover\";\n\t}\n\n\tconst message = errorMessage(error).toLowerCase();\n\tif (FATAL_PATTERNS.some((p) => message.includes(p))) return \"fatal\";\n\tif (FAILOVER_PATTERNS.some((p) => message.includes(p))) return \"failover\";\n\n\treturn \"fatal\";\n}\n\n/** Coarse capability band by output cost ($/million tokens). */\nexport function costBand(outputCost: number): 0 | 1 | 2 {\n\tif (outputCost < 3) return 0;\n\tif (outputCost < 20) return 1;\n\treturn 2;\n}\n\n/**\n * Pick an equivalent model to fail over to, or null when none is suitable.\n *\n * Ranking, best first:\n * 1. A different provider (the whole point — route around the outage).\n * 2. Same capability band as the current model (don't silently downgrade\n * a powerful turn to a fast model, or overpay by jumping up a band).\n * 3. Reasoning capability matching the current model.\n * 4. Closest output cost.\n * 5. Larger context window as a final tiebreak.\n *\n * Image conversations only ever fall over to vision-capable models, so a\n * multimodal session can never break.\n */\nexport function selectFallbackModel<T extends FallbackModelInfo>(\n\tcurrent: FallbackModelInfo,\n\tavailable: readonly T[],\n\talreadyTried: ReadonlySet<string> = new Set(),\n): T | null {\n\tconst needsImage = current.input.includes(\"image\");\n\tconst currentBand = costBand(current.cost.output);\n\n\tconst candidates = available.filter((m) => {\n\t\tif (modelKey(m) === modelKey(current)) return false;\n\t\tif (alreadyTried.has(modelKey(m))) return false;\n\t\tif (needsImage && !m.input.includes(\"image\")) return false;\n\t\treturn true;\n\t});\n\n\tif (candidates.length === 0) return null;\n\n\tconst rank = (m: FallbackModelInfo): number[] => [\n\t\tm.provider === current.provider ? 1 : 0, // different provider first\n\t\tMath.abs(costBand(m.cost.output) - currentBand), // same band preferred\n\t\tm.reasoning === current.reasoning ? 0 : 1, // reasoning match\n\t\tMath.abs(m.cost.output - current.cost.output), // closest cost\n\t\t-m.contextWindow, // larger context wins\n\t];\n\n\tlet best = candidates[0];\n\tlet bestRank = rank(best);\n\tfor (let i = 1; i < candidates.length; i++) {\n\t\tconst r = rank(candidates[i]);\n\t\tif (lexLess(r, bestRank)) {\n\t\t\tbest = candidates[i];\n\t\t\tbestRank = r;\n\t\t}\n\t}\n\treturn best;\n}\n\n/** Lexicographic comparison of two numeric rank tuples: a < b ? */\nfunction lexLess(a: number[], b: number[]): boolean {\n\tfor (let i = 0; i < Math.max(a.length, b.length); i++) {\n\t\tconst av = a[i] ?? 0;\n\t\tconst bv = b[i] ?? 0;\n\t\tif (av !== bv) return av < bv;\n\t}\n\treturn false;\n}\n"]}
1
+ {"version":3,"file":"model-fallback.js","sourceRoot":"","sources":["../../src/core/model-fallback.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Cross-provider failover: the reusable core of Elyra's \"AI sovereignty\"\n * behavior. When a turn fails because a provider is unavailable (overloaded,\n * rate-limited, an auth problem, or a model that vanished — e.g. an\n * export-control suspension), we can re-run the turn on an equivalent model\n * from a *different* provider instead of just surfacing the error.\n *\n * This is only possible because Elyra is provider-agnostic. A single-vendor\n * agent has nowhere to fail over to.\n *\n * Both functions here are pure so they can be unit-tested without real\n * providers, and reused by cross-provider consensus (#1) and per-repo model\n * shopping (#4), which need the same equivalence notion.\n */\n\n/** Minimal model shape the fallback selector needs (Model<Api> satisfies it). */\nexport interface FallbackModelInfo {\n\tprovider: string;\n\tid: string;\n\treasoning: boolean;\n\tinput: readonly string[];\n\tcost: { output: number };\n\tcontextWindow: number;\n}\n\nexport type FailureClass = \"failover\" | \"fatal\";\n\n/** Stable key for de-duping tried models across failover attempts. */\nexport function modelKey(m: { provider: string; id: string }): string {\n\treturn `${m.provider}/${m.id}`;\n}\n\n/** Extract an HTTP-ish status code from an unknown provider error, if present. */\nfunction extractStatus(error: unknown): number | undefined {\n\tif (typeof error !== \"object\" || error === null) return undefined;\n\tconst record = error as Record<string, unknown>;\n\tconst raw = record.status ?? record.statusCode ?? record.code;\n\tif (typeof raw === \"number\") return raw;\n\tif (typeof raw === \"string\" && /^\\d+$/.test(raw)) return Number.parseInt(raw, 10);\n\treturn undefined;\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/** Statuses that mean \"this provider can't serve the request right now\". */\nconst FAILOVER_STATUSES = new Set([401, 403, 404, 408, 409, 425, 429, 500, 502, 503, 504, 529]);\n/** Statuses that will fail identically on any provider — do not fail over. */\nconst FATAL_STATUSES = new Set([400, 413, 422]);\n\n/** Message fragments that indicate a transient/provider-specific failure. */\nconst FAILOVER_PATTERNS = [\n\t\"overloaded\",\n\t\"rate limit\",\n\t\"rate-limit\",\n\t\"capacity\",\n\t\"not available\",\n\t\"temporarily unavailable\",\n\t\"timeout\",\n\t\"timed out\",\n\t\"econnreset\",\n\t\"etimedout\",\n\t\"enotfound\",\n\t\"fetch failed\",\n\t\"socket hang up\",\n\t\"service unavailable\",\n];\n\n/** Message fragments that indicate a request that no provider will accept. */\nconst FATAL_PATTERNS = [\n\t\"context length\",\n\t\"maximum context\",\n\t\"context window\",\n\t\"too many tokens\",\n\t\"prompt is too long\",\n\t\"maximum number of tokens\",\n\t\"invalid request\",\n\t\"unsupported\",\n];\n\n/**\n * Decide whether a failed turn should be retried on a different provider.\n *\n * Conservative on the fatal side: an unrecognized error with no transient\n * signal is treated as fatal (surfaced as today) rather than burning a\n * failover attempt on what is probably a real bug or malformed request.\n */\nexport function classifyProviderFailure(error: unknown): FailureClass {\n\tconst status = extractStatus(error);\n\tif (status !== undefined) {\n\t\tif (FATAL_STATUSES.has(status)) return \"fatal\";\n\t\tif (FAILOVER_STATUSES.has(status)) return \"failover\";\n\t}\n\n\tconst message = errorMessage(error).toLowerCase();\n\tif (FATAL_PATTERNS.some((p) => message.includes(p))) return \"fatal\";\n\tif (FAILOVER_PATTERNS.some((p) => message.includes(p))) return \"failover\";\n\n\treturn \"fatal\";\n}\n\n/** Coarse capability band by output cost ($/million tokens). */\nexport function costBand(outputCost: number): 0 | 1 | 2 {\n\tif (outputCost < 3) return 0;\n\tif (outputCost < 20) return 1;\n\treturn 2;\n}\n\n/**\n * Minimal reliability measurements the fallback selector can consume\n * (structurally satisfied by routing-ledger's ModelOutcomeStats).\n */\nexport interface ModelReliabilityStats {\n\tturns: number;\n\tsuccessRate: number;\n\tfailoversFrom: number;\n}\n\n/** Below this many measured turns a model is never demoted. */\nconst MIN_RELIABILITY_SAMPLE = 5;\n\n/**\n * Coarse demotion bucket from local outcome measurements: 0 = fine (or not\n * enough data), 1 = shaky (success < 80%), 2 = unreliable (success < 50% or\n * we failed over away from it more often than not). Models without\n * sufficient samples rank exactly as before — measurements only ever demote\n * demonstrated problems, never promote on thin evidence.\n */\nfunction reliabilityPenalty(m: FallbackModelInfo, stats?: ReadonlyMap<string, ModelReliabilityStats>): number {\n\tconst s = stats?.get(modelKey(m));\n\tif (!s || s.turns < MIN_RELIABILITY_SAMPLE) return 0;\n\tconst failoverRate = s.failoversFrom / s.turns;\n\tif (s.successRate < 0.5 || failoverRate > 0.5) return 2;\n\tif (s.successRate < 0.8) return 1;\n\treturn 0;\n}\n\n/**\n * Pick an equivalent model to fail over to, or null when none is suitable.\n *\n * Ranking, best first:\n * 1. A different provider (the whole point — route around the outage).\n * 2. Measured reliability (when a local routing ledger is supplied):\n * models that demonstrably failed here are demoted, with a sample-size\n * floor so thin data never changes the ranking.\n * 3. Same capability band as the current model (don't silently downgrade\n * a powerful turn to a fast model, or overpay by jumping up a band).\n * 4. Reasoning capability matching the current model.\n * 5. Closest output cost.\n * 6. Larger context window as a final tiebreak.\n *\n * Image conversations only ever fall over to vision-capable models, so a\n * multimodal session can never break.\n */\nexport function selectFallbackModel<T extends FallbackModelInfo>(\n\tcurrent: FallbackModelInfo,\n\tavailable: readonly T[],\n\talreadyTried: ReadonlySet<string> = new Set(),\n\tstats?: ReadonlyMap<string, ModelReliabilityStats>,\n): T | null {\n\tconst needsImage = current.input.includes(\"image\");\n\tconst currentBand = costBand(current.cost.output);\n\n\tconst candidates = available.filter((m) => {\n\t\tif (modelKey(m) === modelKey(current)) return false;\n\t\tif (alreadyTried.has(modelKey(m))) return false;\n\t\tif (needsImage && !m.input.includes(\"image\")) return false;\n\t\treturn true;\n\t});\n\n\tif (candidates.length === 0) return null;\n\n\tconst rank = (m: FallbackModelInfo): number[] => [\n\t\tm.provider === current.provider ? 1 : 0, // different provider first\n\t\treliabilityPenalty(m, stats), // measured reliability (demotion only)\n\t\tMath.abs(costBand(m.cost.output) - currentBand), // same band preferred\n\t\tm.reasoning === current.reasoning ? 0 : 1, // reasoning match\n\t\tMath.abs(m.cost.output - current.cost.output), // closest cost\n\t\t-m.contextWindow, // larger context wins\n\t];\n\n\tlet best = candidates[0];\n\tlet bestRank = rank(best);\n\tfor (let i = 1; i < candidates.length; i++) {\n\t\tconst r = rank(candidates[i]);\n\t\tif (lexLess(r, bestRank)) {\n\t\t\tbest = candidates[i];\n\t\t\tbestRank = r;\n\t\t}\n\t}\n\treturn best;\n}\n\n/** Lexicographic comparison of two numeric rank tuples: a < b ? */\nfunction lexLess(a: number[], b: number[]): boolean {\n\tfor (let i = 0; i < Math.max(a.length, b.length); i++) {\n\t\tconst av = a[i] ?? 0;\n\t\tconst bv = b[i] ?? 0;\n\t\tif (av !== bv) return av < bv;\n\t}\n\treturn false;\n}\n"]}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Routing ledger: local, append-only outcome measurements per model.
3
+ *
4
+ * Measured routing v2 — instead of ranking failover candidates purely on
5
+ * static heuristics (cost band, reasoning, context window), Elyra records how
6
+ * models actually perform on this machine (turn success, edit failures,
7
+ * retries, failovers away from a model) and uses those measurements to demote
8
+ * demonstrably unreliable candidates.
9
+ *
10
+ * Privacy: only model/provider ids, counters, timings, and cost numbers are
11
+ * stored — never prompts, file paths, or code. The ledger is a single local
12
+ * JSONL file under the agent dir and never leaves disk.
13
+ */
14
+ /** Task classification. Reserved prefixes keep the schema stable when subagents/swarm gain per-role model choice. */
15
+ export type LedgerTaskClass = "main" | "review" | `subagent:${string}` | `swarm:${string}`;
16
+ export interface LedgerTurnEvent {
17
+ type: "turn";
18
+ ts: number;
19
+ provider: string;
20
+ modelId: string;
21
+ taskClass: LedgerTaskClass;
22
+ stopReason: string;
23
+ aborted: boolean;
24
+ error: boolean;
25
+ latencyMs?: number;
26
+ inputTokens: number;
27
+ outputTokens: number;
28
+ cost: number;
29
+ editsAttempted: number;
30
+ editsFailed: number;
31
+ retries: number;
32
+ }
33
+ export interface LedgerFailoverEvent {
34
+ type: "failover";
35
+ ts: number;
36
+ fromProvider: string;
37
+ fromModelId: string;
38
+ toProvider?: string;
39
+ toModelId?: string;
40
+ }
41
+ export type LedgerEvent = LedgerTurnEvent | LedgerFailoverEvent;
42
+ /** Below this many turns a model is never demoted (insufficient evidence). */
43
+ export declare const MIN_RELIABILITY_SAMPLE = 5;
44
+ export declare function appendLedgerEvent(path: string, event: LedgerEvent): void;
45
+ export declare function readLedger(path: string): LedgerEvent[];
46
+ export interface ModelOutcomeStats {
47
+ turns: number;
48
+ /** Fraction of turns that ended without error and without being aborted. */
49
+ successRate: number;
50
+ /** Times a failover moved *away* from this model. */
51
+ failoversFrom: number;
52
+ editsAttempted: number;
53
+ editsFailed: number;
54
+ retries: number;
55
+ totalCost: number;
56
+ avgLatencyMs?: number;
57
+ }
58
+ export declare function computeStats(events: readonly LedgerEvent[], options?: {
59
+ taskClass?: LedgerTaskClass;
60
+ now?: number;
61
+ }): Map<string, ModelOutcomeStats>;
62
+ export declare function loadStats(path: string, taskClass?: LedgerTaskClass): Map<string, ModelOutcomeStats>;
63
+ /** Plain-text stats report shared by `elyra doctor routing` and `/routing-stats`. */
64
+ export declare function formatStatsReport(stats: ReadonlyMap<string, ModelOutcomeStats>): string;
65
+ /**
66
+ * Consumes AgentSession events and appends one ledger turn event per
67
+ * assistant message. Tool results that follow an assistant message are
68
+ * attributed to it; the record is flushed when the next assistant message
69
+ * starts or the run ends.
70
+ */
71
+ export declare class TurnOutcomeRecorder {
72
+ private readonly ledgerPath;
73
+ private readonly taskClass;
74
+ private turnStartTs;
75
+ private pending;
76
+ constructor(ledgerPath: string, taskClass?: LedgerTaskClass);
77
+ /** Handle an AgentSession event. Never throws. */
78
+ handleEvent(rawEvent: unknown): void;
79
+ private process;
80
+ /** Flush the pending turn record to disk, if any. */
81
+ flush(): void;
82
+ }
83
+ //# sourceMappingURL=routing-ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-ledger.d.ts","sourceRoot":"","sources":["../../src/core/routing-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,qHAAqH;AACrH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAAC;AAE3F,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,eAAe,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,mBAAmB,CAAC;AAQhE,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAIxE;AAgBD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAmBtD;AAID,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAMD,wBAAgB,YAAY,CAC3B,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,eAAe,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAsEhC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,eAAe,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAEnG;AAID,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,MAAM,CAwBvF;AAoBD;;;;;GAKG;AACH,qBAAa,mBAAmB;IAK9B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAL3B,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,OAAO,CAA8B;IAE7C,YACkB,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,eAAwB,EACjD;IAEJ,kDAAkD;IAClD,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAMnC;IAED,OAAO,CAAC,OAAO;IAgDf,qDAAqD;IACrD,KAAK,IAAI,IAAI,CASZ;CACD"}
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Routing ledger: local, append-only outcome measurements per model.
3
+ *
4
+ * Measured routing v2 — instead of ranking failover candidates purely on
5
+ * static heuristics (cost band, reasoning, context window), Elyra records how
6
+ * models actually perform on this machine (turn success, edit failures,
7
+ * retries, failovers away from a model) and uses those measurements to demote
8
+ * demonstrably unreliable candidates.
9
+ *
10
+ * Privacy: only model/provider ids, counters, timings, and cost numbers are
11
+ * stored — never prompts, file paths, or code. The ledger is a single local
12
+ * JSONL file under the agent dir and never leaves disk.
13
+ */
14
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
15
+ import { dirname } from "node:path";
16
+ // ── Storage ─────────────────────────────────────────────────────────────────
17
+ /** Rotate when the file grows beyond this size; keep the newest half. */
18
+ const ROTATE_BYTES = 1_048_576;
19
+ /** Only events newer than this window contribute to stats. */
20
+ const STATS_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
21
+ /** Below this many turns a model is never demoted (insufficient evidence). */
22
+ export const MIN_RELIABILITY_SAMPLE = 5;
23
+ export function appendLedgerEvent(path, event) {
24
+ mkdirSync(dirname(path), { recursive: true });
25
+ rotateIfNeeded(path);
26
+ appendFileSync(path, `${JSON.stringify(event)}\n`, "utf-8");
27
+ }
28
+ function rotateIfNeeded(path) {
29
+ try {
30
+ if (!existsSync(path))
31
+ return;
32
+ if (statSync(path).size < ROTATE_BYTES)
33
+ return;
34
+ const lines = readFileSync(path, "utf-8")
35
+ .split("\n")
36
+ .filter((l) => l.length > 0);
37
+ const keep = lines.slice(Math.floor(lines.length / 2));
38
+ writeFileSync(path, keep.length > 0 ? `${keep.join("\n")}\n` : "", "utf-8");
39
+ }
40
+ catch {
41
+ // Rotation is best-effort; a failed rotation must never block recording.
42
+ }
43
+ }
44
+ export function readLedger(path) {
45
+ if (!existsSync(path))
46
+ return [];
47
+ let raw;
48
+ try {
49
+ raw = readFileSync(path, "utf-8");
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ const events = [];
55
+ for (const line of raw.split("\n")) {
56
+ if (!line.trim())
57
+ continue;
58
+ try {
59
+ const parsed = JSON.parse(line);
60
+ if (parsed && (parsed.type === "turn" || parsed.type === "failover"))
61
+ events.push(parsed);
62
+ }
63
+ catch {
64
+ // Skip corrupt lines
65
+ }
66
+ }
67
+ return events;
68
+ }
69
+ function statsKey(provider, modelId) {
70
+ return `${provider}/${modelId}`;
71
+ }
72
+ export function computeStats(events, options) {
73
+ const now = options?.now ?? Date.now();
74
+ const cutoff = now - STATS_WINDOW_MS;
75
+ const acc = new Map();
76
+ const get = (key) => {
77
+ let a = acc.get(key);
78
+ if (!a) {
79
+ a = {
80
+ turns: 0,
81
+ successes: 0,
82
+ failoversFrom: 0,
83
+ editsAttempted: 0,
84
+ editsFailed: 0,
85
+ retries: 0,
86
+ totalCost: 0,
87
+ latencySum: 0,
88
+ latencyCount: 0,
89
+ };
90
+ acc.set(key, a);
91
+ }
92
+ return a;
93
+ };
94
+ for (const event of events) {
95
+ if (event.ts < cutoff)
96
+ continue;
97
+ if (event.type === "turn") {
98
+ if (options?.taskClass && event.taskClass !== options.taskClass)
99
+ continue;
100
+ const a = get(statsKey(event.provider, event.modelId));
101
+ a.turns++;
102
+ if (!event.error && !event.aborted)
103
+ a.successes++;
104
+ a.editsAttempted += event.editsAttempted;
105
+ a.editsFailed += event.editsFailed;
106
+ a.retries += event.retries;
107
+ a.totalCost += event.cost;
108
+ if (typeof event.latencyMs === "number") {
109
+ a.latencySum += event.latencyMs;
110
+ a.latencyCount++;
111
+ }
112
+ }
113
+ else {
114
+ const a = get(statsKey(event.fromProvider, event.fromModelId));
115
+ a.failoversFrom++;
116
+ }
117
+ }
118
+ const result = new Map();
119
+ for (const [key, a] of acc) {
120
+ result.set(key, {
121
+ turns: a.turns,
122
+ successRate: a.turns > 0 ? a.successes / a.turns : 1,
123
+ failoversFrom: a.failoversFrom,
124
+ editsAttempted: a.editsAttempted,
125
+ editsFailed: a.editsFailed,
126
+ retries: a.retries,
127
+ totalCost: a.totalCost,
128
+ avgLatencyMs: a.latencyCount > 0 ? a.latencySum / a.latencyCount : undefined,
129
+ });
130
+ }
131
+ return result;
132
+ }
133
+ export function loadStats(path, taskClass) {
134
+ return computeStats(readLedger(path), taskClass ? { taskClass } : undefined);
135
+ }
136
+ // ── Reporting ───────────────────────────────────────────────────────────────
137
+ /** Plain-text stats report shared by `elyra doctor routing` and `/routing-stats`. */
138
+ export function formatStatsReport(stats) {
139
+ if (stats.size === 0) {
140
+ return "No routing measurements recorded yet. Stats accumulate as you use Elyra.";
141
+ }
142
+ const rows = [...stats.entries()].sort((a, b) => b[1].turns - a[1].turns);
143
+ const lines = [];
144
+ lines.push("Model outcome measurements (last 30 days, local only):");
145
+ lines.push("");
146
+ for (const [key, s] of rows) {
147
+ const success = `${Math.round(s.successRate * 100)}%`;
148
+ const editRate = s.editsAttempted > 0 ? `${s.editsFailed}/${s.editsAttempted} edit failures` : "no edits";
149
+ const latency = s.avgLatencyMs !== undefined ? `${(s.avgLatencyMs / 1000).toFixed(1)}s avg` : "n/a";
150
+ const demoted = s.turns >= MIN_RELIABILITY_SAMPLE && (s.successRate < 0.8 || s.failoversFrom / s.turns > 0.5)
151
+ ? " [demoted in failover ranking]"
152
+ : "";
153
+ lines.push(`- ${key}: ${s.turns} turn${s.turns === 1 ? "" : "s"}, ${success} success, ${editRate}, ` +
154
+ `${s.retries} retr${s.retries === 1 ? "y" : "ies"}, ${s.failoversFrom} failover${s.failoversFrom === 1 ? "" : "s"} away, ` +
155
+ `$${s.totalCost.toFixed(4)} total, ${latency}${demoted}`);
156
+ }
157
+ return lines.join("\n");
158
+ }
159
+ const EDIT_TOOLS = new Set(["edit", "write"]);
160
+ /**
161
+ * Consumes AgentSession events and appends one ledger turn event per
162
+ * assistant message. Tool results that follow an assistant message are
163
+ * attributed to it; the record is flushed when the next assistant message
164
+ * starts or the run ends.
165
+ */
166
+ export class TurnOutcomeRecorder {
167
+ constructor(ledgerPath, taskClass = "main") {
168
+ this.ledgerPath = ledgerPath;
169
+ this.taskClass = taskClass;
170
+ }
171
+ /** Handle an AgentSession event. Never throws. */
172
+ handleEvent(rawEvent) {
173
+ try {
174
+ this.process(rawEvent);
175
+ }
176
+ catch {
177
+ // Recording must never break the session.
178
+ }
179
+ }
180
+ process(event) {
181
+ if (event.type === "turn_start") {
182
+ this.turnStartTs = Date.now();
183
+ return;
184
+ }
185
+ if (event.type === "auto_retry_end" && this.pending && typeof event.attempt === "number") {
186
+ this.pending.retries = Math.max(this.pending.retries, event.attempt);
187
+ return;
188
+ }
189
+ if (event.type === "agent_end") {
190
+ this.flush();
191
+ return;
192
+ }
193
+ if (event.type !== "message_end" || !event.message)
194
+ return;
195
+ const message = event.message;
196
+ if (message.role === "assistant") {
197
+ this.flush();
198
+ const now = Date.now();
199
+ this.pending = {
200
+ type: "turn",
201
+ ts: now,
202
+ provider: message.provider ?? "unknown",
203
+ modelId: message.model ?? "unknown",
204
+ taskClass: this.taskClass,
205
+ stopReason: message.stopReason ?? "unknown",
206
+ aborted: message.stopReason === "aborted",
207
+ error: message.stopReason === "error",
208
+ latencyMs: this.turnStartTs !== undefined ? now - this.turnStartTs : undefined,
209
+ inputTokens: message.usage?.input ?? 0,
210
+ outputTokens: message.usage?.output ?? 0,
211
+ cost: message.usage?.cost?.total ?? 0,
212
+ editsAttempted: 0,
213
+ editsFailed: 0,
214
+ retries: 0,
215
+ };
216
+ return;
217
+ }
218
+ if (message.role === "toolResult" && this.pending && message.toolName && EDIT_TOOLS.has(message.toolName)) {
219
+ this.pending.editsAttempted++;
220
+ if (message.isError)
221
+ this.pending.editsFailed++;
222
+ }
223
+ }
224
+ /** Flush the pending turn record to disk, if any. */
225
+ flush() {
226
+ if (!this.pending)
227
+ return;
228
+ const record = this.pending;
229
+ this.pending = undefined;
230
+ try {
231
+ appendLedgerEvent(this.ledgerPath, record);
232
+ }
233
+ catch {
234
+ // Best-effort persistence.
235
+ }
236
+ }
237
+ }
238
+ //# sourceMappingURL=routing-ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-ledger.js","sourceRoot":"","sources":["../../src/core/routing-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACvG,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoCpC,+EAA+E;AAE/E,yEAAyE;AACzE,MAAM,YAAY,GAAG,SAAS,CAAC;AAC/B,8DAA8D;AAC9D,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACjD,8EAA8E;AAC9E,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,KAAkB;IACjE,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,cAAc,CAAC,IAAI,CAAC,CAAC;IACrB,cAAc,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,cAAc,CAAC,IAAY;IACnC,IAAI,CAAC;QACJ,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO;QAC9B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,YAAY;YAAE,OAAO;QAC/C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC;aACvC,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;QACvD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACR,yEAAyE;IAC1E,CAAC;AACF,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACtC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACJ,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;IACD,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,SAAS;QAC3B,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAgB,CAAC;YAC/C,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3F,CAAC;QAAC,MAAM,CAAC;YACR,qBAAqB;QACtB,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC;AAiBD,SAAS,QAAQ,CAAC,QAAgB,EAAE,OAAe;IAClD,OAAO,GAAG,QAAQ,IAAI,OAAO,EAAE,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,YAAY,CAC3B,MAA8B,EAC9B,OAAuD;IAEvD,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACvC,MAAM,MAAM,GAAG,GAAG,GAAG,eAAe,CAAC;IAarC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAe,CAAC;IACnC,MAAM,GAAG,GAAG,CAAC,GAAW,EAAO,EAAE;QAChC,IAAI,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrB,IAAI,CAAC,CAAC,EAAE,CAAC;YACR,CAAC,GAAG;gBACH,KAAK,EAAE,CAAC;gBACR,SAAS,EAAE,CAAC;gBACZ,aAAa,EAAE,CAAC;gBAChB,cAAc,EAAE,CAAC;gBACjB,WAAW,EAAE,CAAC;gBACd,OAAO,EAAE,CAAC;gBACV,SAAS,EAAE,CAAC;gBACZ,UAAU,EAAE,CAAC;gBACb,YAAY,EAAE,CAAC;aACf,CAAC;YACF,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACjB,CAAC;QACD,OAAO,CAAC,CAAC;IACV,CAAC,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,EAAE,GAAG,MAAM;YAAE,SAAS;QAChC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC3B,IAAI,OAAO,EAAE,SAAS,IAAI,KAAK,CAAC,SAAS,KAAK,OAAO,CAAC,SAAS;gBAAE,SAAS;YAC1E,MAAM,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YACvD,CAAC,CAAC,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO;gBAAE,CAAC,CAAC,SAAS,EAAE,CAAC;YAClD,CAAC,CAAC,cAAc,IAAI,KAAK,CAAC,cAAc,CAAC;YACzC,CAAC,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,CAAC;YACnC,CAAC,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC;YAC3B,CAAC,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC;YAC1B,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;gBACzC,CAAC,CAAC,UAAU,IAAI,KAAK,CAAC,SAAS,CAAC;gBAChC,CAAC,CAAC,YAAY,EAAE,CAAC;YAClB,CAAC;QACF,CAAC;aAAM,CAAC;YACP,MAAM,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;YAC/D,CAAC,CAAC,aAAa,EAAE,CAAC;QACnB,CAAC;IACF,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAA6B,CAAC;IACpD,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;QAC5B,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE;YACf,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,WAAW,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YACpD,aAAa,EAAE,CAAC,CAAC,aAAa;YAC9B,cAAc,EAAE,CAAC,CAAC,cAAc;YAChC,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,YAAY,EAAE,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS;SAC5E,CAAC,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,SAA2B;IAClE,OAAO,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAC9E,CAAC;AAED,+EAA+E;AAE/E,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,KAA6C;IAC9E,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,0EAA0E,CAAC;IACnF,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC1E,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC;IACrE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,GAAG,GAAG,CAAC,GAAG,CAAC;QACtD,MAAM,QAAQ,GAAG,CAAC,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,cAAc,gBAAgB,CAAC,CAAC,CAAC,UAAU,CAAC;QAC1G,MAAM,OAAO,GAAG,CAAC,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QACpG,MAAM,OAAO,GACZ,CAAC,CAAC,KAAK,IAAI,sBAAsB,IAAI,CAAC,CAAC,CAAC,WAAW,GAAG,GAAG,IAAI,CAAC,CAAC,aAAa,GAAG,CAAC,CAAC,KAAK,GAAG,GAAG,CAAC;YAC5F,CAAC,CAAC,iCAAiC;YACnC,CAAC,CAAC,EAAE,CAAC;QACP,KAAK,CAAC,IAAI,CACT,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,OAAO,aAAa,QAAQ,IAAI;YACxF,GAAG,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,aAAa,YAAY,CAAC,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,SAAS;YAC1H,IAAI,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,OAAO,GAAG,OAAO,EAAE,CACzD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAkBD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,OAAO,mBAAmB;IAI/B,YACkB,UAAkB,EAClB,SAAS,GAAoB,MAAM;0BADnC,UAAU;yBACV,SAAS;IACxB,CAAC;IAEJ,kDAAkD;IAClD,WAAW,CAAC,QAAiB;QAC5B,IAAI,CAAC;YACJ,IAAI,CAAC,OAAO,CAAC,QAAyB,CAAC,CAAC;QACzC,CAAC;QAAC,MAAM,CAAC;YACR,0CAA0C;QAC3C,CAAC;IACF,CAAC;IAEO,OAAO,CAAC,KAAoB;QACnC,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YACjC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAC9B,OAAO;QACR,CAAC;QAED,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC1F,IAAI,CAAC,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YACrE,OAAO;QACR,CAAC;QAED,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAChC,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,OAAO;QACR,CAAC;QAED,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,OAAO;QAC3D,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAE9B,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAClC,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,IAAI,CAAC,OAAO,GAAG;gBACd,IAAI,EAAE,MAAM;gBACZ,EAAE,EAAE,GAAG;gBACP,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,SAAS;gBACvC,OAAO,EAAE,OAAO,CAAC,KAAK,IAAI,SAAS;gBACnC,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,SAAS;gBAC3C,OAAO,EAAE,OAAO,CAAC,UAAU,KAAK,SAAS;gBACzC,KAAK,EAAE,OAAO,CAAC,UAAU,KAAK,OAAO;gBACrC,SAAS,EAAE,IAAI,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;gBAC9E,WAAW,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,IAAI,CAAC;gBACtC,YAAY,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;gBACxC,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;gBACrC,cAAc,EAAE,CAAC;gBACjB,WAAW,EAAE,CAAC;gBACd,OAAO,EAAE,CAAC;aACV,CAAC;YACF,OAAO;QACR,CAAC;QAED,IAAI,OAAO,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,QAAQ,IAAI,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3G,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;YAC9B,IAAI,OAAO,CAAC,OAAO;gBAAE,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACjD,CAAC;IACF,CAAC;IAED,qDAAqD;IACrD,KAAK;QACJ,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;QACzB,IAAI,CAAC;YACJ,iBAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACR,2BAA2B;QAC5B,CAAC;IACF,CAAC;CACD","sourcesContent":["/**\n * Routing ledger: local, append-only outcome measurements per model.\n *\n * Measured routing v2 — instead of ranking failover candidates purely on\n * static heuristics (cost band, reasoning, context window), Elyra records how\n * models actually perform on this machine (turn success, edit failures,\n * retries, failovers away from a model) and uses those measurements to demote\n * demonstrably unreliable candidates.\n *\n * Privacy: only model/provider ids, counters, timings, and cost numbers are\n * stored — never prompts, file paths, or code. The ledger is a single local\n * JSONL file under the agent dir and never leaves disk.\n */\n\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\n// ── Event types ─────────────────────────────────────────────────────────────\n\n/** Task classification. Reserved prefixes keep the schema stable when subagents/swarm gain per-role model choice. */\nexport type LedgerTaskClass = \"main\" | \"review\" | `subagent:${string}` | `swarm:${string}`;\n\nexport interface LedgerTurnEvent {\n\ttype: \"turn\";\n\tts: number;\n\tprovider: string;\n\tmodelId: string;\n\ttaskClass: LedgerTaskClass;\n\tstopReason: string;\n\taborted: boolean;\n\terror: boolean;\n\tlatencyMs?: number;\n\tinputTokens: number;\n\toutputTokens: number;\n\tcost: number;\n\teditsAttempted: number;\n\teditsFailed: number;\n\tretries: number;\n}\n\nexport interface LedgerFailoverEvent {\n\ttype: \"failover\";\n\tts: number;\n\tfromProvider: string;\n\tfromModelId: string;\n\ttoProvider?: string;\n\ttoModelId?: string;\n}\n\nexport type LedgerEvent = LedgerTurnEvent | LedgerFailoverEvent;\n\n// ── Storage ─────────────────────────────────────────────────────────────────\n\n/** Rotate when the file grows beyond this size; keep the newest half. */\nconst ROTATE_BYTES = 1_048_576;\n/** Only events newer than this window contribute to stats. */\nconst STATS_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;\n/** Below this many turns a model is never demoted (insufficient evidence). */\nexport const MIN_RELIABILITY_SAMPLE = 5;\n\nexport function appendLedgerEvent(path: string, event: LedgerEvent): void {\n\tmkdirSync(dirname(path), { recursive: true });\n\trotateIfNeeded(path);\n\tappendFileSync(path, `${JSON.stringify(event)}\\n`, \"utf-8\");\n}\n\nfunction rotateIfNeeded(path: string): void {\n\ttry {\n\t\tif (!existsSync(path)) return;\n\t\tif (statSync(path).size < ROTATE_BYTES) return;\n\t\tconst lines = readFileSync(path, \"utf-8\")\n\t\t\t.split(\"\\n\")\n\t\t\t.filter((l) => l.length > 0);\n\t\tconst keep = lines.slice(Math.floor(lines.length / 2));\n\t\twriteFileSync(path, keep.length > 0 ? `${keep.join(\"\\n\")}\\n` : \"\", \"utf-8\");\n\t} catch {\n\t\t// Rotation is best-effort; a failed rotation must never block recording.\n\t}\n}\n\nexport function readLedger(path: string): LedgerEvent[] {\n\tif (!existsSync(path)) return [];\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(path, \"utf-8\");\n\t} catch {\n\t\treturn [];\n\t}\n\tconst events: LedgerEvent[] = [];\n\tfor (const line of raw.split(\"\\n\")) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(line) as LedgerEvent;\n\t\t\tif (parsed && (parsed.type === \"turn\" || parsed.type === \"failover\")) events.push(parsed);\n\t\t} catch {\n\t\t\t// Skip corrupt lines\n\t\t}\n\t}\n\treturn events;\n}\n\n// ── Aggregation ─────────────────────────────────────────────────────────────\n\nexport interface ModelOutcomeStats {\n\tturns: number;\n\t/** Fraction of turns that ended without error and without being aborted. */\n\tsuccessRate: number;\n\t/** Times a failover moved *away* from this model. */\n\tfailoversFrom: number;\n\teditsAttempted: number;\n\teditsFailed: number;\n\tretries: number;\n\ttotalCost: number;\n\tavgLatencyMs?: number;\n}\n\nfunction statsKey(provider: string, modelId: string): string {\n\treturn `${provider}/${modelId}`;\n}\n\nexport function computeStats(\n\tevents: readonly LedgerEvent[],\n\toptions?: { taskClass?: LedgerTaskClass; now?: number },\n): Map<string, ModelOutcomeStats> {\n\tconst now = options?.now ?? Date.now();\n\tconst cutoff = now - STATS_WINDOW_MS;\n\n\tinterface Acc {\n\t\tturns: number;\n\t\tsuccesses: number;\n\t\tfailoversFrom: number;\n\t\teditsAttempted: number;\n\t\teditsFailed: number;\n\t\tretries: number;\n\t\ttotalCost: number;\n\t\tlatencySum: number;\n\t\tlatencyCount: number;\n\t}\n\tconst acc = new Map<string, Acc>();\n\tconst get = (key: string): Acc => {\n\t\tlet a = acc.get(key);\n\t\tif (!a) {\n\t\t\ta = {\n\t\t\t\tturns: 0,\n\t\t\t\tsuccesses: 0,\n\t\t\t\tfailoversFrom: 0,\n\t\t\t\teditsAttempted: 0,\n\t\t\t\teditsFailed: 0,\n\t\t\t\tretries: 0,\n\t\t\t\ttotalCost: 0,\n\t\t\t\tlatencySum: 0,\n\t\t\t\tlatencyCount: 0,\n\t\t\t};\n\t\t\tacc.set(key, a);\n\t\t}\n\t\treturn a;\n\t};\n\n\tfor (const event of events) {\n\t\tif (event.ts < cutoff) continue;\n\t\tif (event.type === \"turn\") {\n\t\t\tif (options?.taskClass && event.taskClass !== options.taskClass) continue;\n\t\t\tconst a = get(statsKey(event.provider, event.modelId));\n\t\t\ta.turns++;\n\t\t\tif (!event.error && !event.aborted) a.successes++;\n\t\t\ta.editsAttempted += event.editsAttempted;\n\t\t\ta.editsFailed += event.editsFailed;\n\t\t\ta.retries += event.retries;\n\t\t\ta.totalCost += event.cost;\n\t\t\tif (typeof event.latencyMs === \"number\") {\n\t\t\t\ta.latencySum += event.latencyMs;\n\t\t\t\ta.latencyCount++;\n\t\t\t}\n\t\t} else {\n\t\t\tconst a = get(statsKey(event.fromProvider, event.fromModelId));\n\t\t\ta.failoversFrom++;\n\t\t}\n\t}\n\n\tconst result = new Map<string, ModelOutcomeStats>();\n\tfor (const [key, a] of acc) {\n\t\tresult.set(key, {\n\t\t\tturns: a.turns,\n\t\t\tsuccessRate: a.turns > 0 ? a.successes / a.turns : 1,\n\t\t\tfailoversFrom: a.failoversFrom,\n\t\t\teditsAttempted: a.editsAttempted,\n\t\t\teditsFailed: a.editsFailed,\n\t\t\tretries: a.retries,\n\t\t\ttotalCost: a.totalCost,\n\t\t\tavgLatencyMs: a.latencyCount > 0 ? a.latencySum / a.latencyCount : undefined,\n\t\t});\n\t}\n\treturn result;\n}\n\nexport function loadStats(path: string, taskClass?: LedgerTaskClass): Map<string, ModelOutcomeStats> {\n\treturn computeStats(readLedger(path), taskClass ? { taskClass } : undefined);\n}\n\n// ── Reporting ───────────────────────────────────────────────────────────────\n\n/** Plain-text stats report shared by `elyra doctor routing` and `/routing-stats`. */\nexport function formatStatsReport(stats: ReadonlyMap<string, ModelOutcomeStats>): string {\n\tif (stats.size === 0) {\n\t\treturn \"No routing measurements recorded yet. Stats accumulate as you use Elyra.\";\n\t}\n\n\tconst rows = [...stats.entries()].sort((a, b) => b[1].turns - a[1].turns);\n\tconst lines: string[] = [];\n\tlines.push(\"Model outcome measurements (last 30 days, local only):\");\n\tlines.push(\"\");\n\tfor (const [key, s] of rows) {\n\t\tconst success = `${Math.round(s.successRate * 100)}%`;\n\t\tconst editRate = s.editsAttempted > 0 ? `${s.editsFailed}/${s.editsAttempted} edit failures` : \"no edits\";\n\t\tconst latency = s.avgLatencyMs !== undefined ? `${(s.avgLatencyMs / 1000).toFixed(1)}s avg` : \"n/a\";\n\t\tconst demoted =\n\t\t\ts.turns >= MIN_RELIABILITY_SAMPLE && (s.successRate < 0.8 || s.failoversFrom / s.turns > 0.5)\n\t\t\t\t? \" [demoted in failover ranking]\"\n\t\t\t\t: \"\";\n\t\tlines.push(\n\t\t\t`- ${key}: ${s.turns} turn${s.turns === 1 ? \"\" : \"s\"}, ${success} success, ${editRate}, ` +\n\t\t\t\t`${s.retries} retr${s.retries === 1 ? \"y\" : \"ies\"}, ${s.failoversFrom} failover${s.failoversFrom === 1 ? \"\" : \"s\"} away, ` +\n\t\t\t\t`$${s.totalCost.toFixed(4)} total, ${latency}${demoted}`,\n\t\t);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n// ── Turn outcome recorder ───────────────────────────────────────────────────\n\ninterface RecorderEvent {\n\ttype: string;\n\tmessage?: {\n\t\trole?: string;\n\t\ttoolName?: string;\n\t\tisError?: boolean;\n\t\tprovider?: string;\n\t\tmodel?: string;\n\t\tstopReason?: string;\n\t\tusage?: { input?: number; output?: number; cost?: { total?: number } };\n\t};\n\tattempt?: number;\n}\n\nconst EDIT_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Consumes AgentSession events and appends one ledger turn event per\n * assistant message. Tool results that follow an assistant message are\n * attributed to it; the record is flushed when the next assistant message\n * starts or the run ends.\n */\nexport class TurnOutcomeRecorder {\n\tprivate turnStartTs: number | undefined;\n\tprivate pending: LedgerTurnEvent | undefined;\n\n\tconstructor(\n\t\tprivate readonly ledgerPath: string,\n\t\tprivate readonly taskClass: LedgerTaskClass = \"main\",\n\t) {}\n\n\t/** Handle an AgentSession event. Never throws. */\n\thandleEvent(rawEvent: unknown): void {\n\t\ttry {\n\t\t\tthis.process(rawEvent as RecorderEvent);\n\t\t} catch {\n\t\t\t// Recording must never break the session.\n\t\t}\n\t}\n\n\tprivate process(event: RecorderEvent): void {\n\t\tif (event.type === \"turn_start\") {\n\t\t\tthis.turnStartTs = Date.now();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.type === \"auto_retry_end\" && this.pending && typeof event.attempt === \"number\") {\n\t\t\tthis.pending.retries = Math.max(this.pending.retries, event.attempt);\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.type === \"agent_end\") {\n\t\t\tthis.flush();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.type !== \"message_end\" || !event.message) return;\n\t\tconst message = event.message;\n\n\t\tif (message.role === \"assistant\") {\n\t\t\tthis.flush();\n\t\t\tconst now = Date.now();\n\t\t\tthis.pending = {\n\t\t\t\ttype: \"turn\",\n\t\t\t\tts: now,\n\t\t\t\tprovider: message.provider ?? \"unknown\",\n\t\t\t\tmodelId: message.model ?? \"unknown\",\n\t\t\t\ttaskClass: this.taskClass,\n\t\t\t\tstopReason: message.stopReason ?? \"unknown\",\n\t\t\t\taborted: message.stopReason === \"aborted\",\n\t\t\t\terror: message.stopReason === \"error\",\n\t\t\t\tlatencyMs: this.turnStartTs !== undefined ? now - this.turnStartTs : undefined,\n\t\t\t\tinputTokens: message.usage?.input ?? 0,\n\t\t\t\toutputTokens: message.usage?.output ?? 0,\n\t\t\t\tcost: message.usage?.cost?.total ?? 0,\n\t\t\t\teditsAttempted: 0,\n\t\t\t\teditsFailed: 0,\n\t\t\t\tretries: 0,\n\t\t\t};\n\t\t\treturn;\n\t\t}\n\n\t\tif (message.role === \"toolResult\" && this.pending && message.toolName && EDIT_TOOLS.has(message.toolName)) {\n\t\t\tthis.pending.editsAttempted++;\n\t\t\tif (message.isError) this.pending.editsFailed++;\n\t\t}\n\t}\n\n\t/** Flush the pending turn record to disk, if any. */\n\tflush(): void {\n\t\tif (!this.pending) return;\n\t\tconst record = this.pending;\n\t\tthis.pending = undefined;\n\t\ttry {\n\t\t\tappendLedgerEvent(this.ledgerPath, record);\n\t\t} catch {\n\t\t\t// Best-effort persistence.\n\t\t}\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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"}
package/dist/core/sdk.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { Agent } from "@elyracode/agent-core";
4
4
  import { clampThinkingLevel, streamSimple } from "@elyracode/ai";
5
- import { getAgentDir } from "../config.js";
5
+ import { getAgentDir, getRoutingLedgerPath } from "../config.js";
6
6
  import { AgentSession } from "./agent-session.js";
7
7
  import { formatNoModelsAvailableMessage } from "./auth-guidance.js";
8
8
  import { AuthStorage } from "./auth-storage.js";
@@ -12,6 +12,7 @@ import { classifyProviderFailure, selectFallbackModel } from "./model-fallback.j
12
12
  import { ModelRegistry } from "./model-registry.js";
13
13
  import { findInitialModel } from "./model-resolver.js";
14
14
  import { DefaultResourceLoader } from "./resource-loader.js";
15
+ import { appendLedgerEvent, loadStats, TurnOutcomeRecorder } from "./routing-ledger.js";
15
16
  import { getDefaultSessionDir, SessionManager } from "./session-manager.js";
16
17
  import { SettingsManager } from "./settings-manager.js";
17
18
  import { time } from "./timings.js";
@@ -238,7 +239,35 @@ export async function createAgentSession(options = {}) {
238
239
  return null;
239
240
  if (classifyProviderFailure(error) !== "failover")
240
241
  return null;
241
- return selectFallbackModel(currentModel, modelRegistry.getAvailable(), tried);
242
+ // Measured routing: demote candidates the local ledger shows to be
243
+ // unreliable on this machine (sample-size floor applies).
244
+ const useLedger = settingsManager.getRoutingLedger();
245
+ let stats;
246
+ if (useLedger) {
247
+ try {
248
+ stats = loadStats(getRoutingLedgerPath(), "main");
249
+ }
250
+ catch {
251
+ stats = undefined;
252
+ }
253
+ }
254
+ const next = selectFallbackModel(currentModel, modelRegistry.getAvailable(), tried, stats);
255
+ if (useLedger) {
256
+ try {
257
+ appendLedgerEvent(getRoutingLedgerPath(), {
258
+ type: "failover",
259
+ ts: Date.now(),
260
+ fromProvider: currentModel.provider,
261
+ fromModelId: currentModel.id,
262
+ toProvider: next?.provider,
263
+ toModelId: next?.id,
264
+ });
265
+ }
266
+ catch {
267
+ // Ledger writes are best-effort.
268
+ }
269
+ }
270
+ return next;
242
271
  },
243
272
  sessionId: sessionManager.getSessionId(),
244
273
  transformContext: async (messages) => {
@@ -308,6 +337,11 @@ export async function createAgentSession(options = {}) {
308
337
  extensionRunnerRef,
309
338
  sessionStartEvent: options.sessionStartEvent,
310
339
  });
340
+ // Measured routing: record per-model turn outcomes to the local ledger.
341
+ if (settingsManager.getRoutingLedger()) {
342
+ const recorder = new TurnOutcomeRecorder(getRoutingLedgerPath(), "main");
343
+ session.subscribe((event) => recorder.handleEvent(event));
344
+ }
311
345
  const extensionsResult = resourceLoader.getExtensions();
312
346
  return {
313
347
  session,