@mlx-node/agent 0.0.8 → 0.0.10

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 (49) hide show
  1. package/dist/catalog.d.ts +15 -0
  2. package/dist/catalog.d.ts.map +1 -1
  3. package/dist/catalog.js +15 -0
  4. package/dist/cold-tier.d.ts +99 -0
  5. package/dist/cold-tier.d.ts.map +1 -0
  6. package/dist/cold-tier.js +155 -0
  7. package/dist/extensions/local-image-input.d.ts +24 -0
  8. package/dist/extensions/local-image-input.d.ts.map +1 -0
  9. package/dist/extensions/local-image-input.js +114 -0
  10. package/dist/extensions/subagent.d.ts +20 -1
  11. package/dist/extensions/subagent.d.ts.map +1 -1
  12. package/dist/extensions/subagent.js +46 -6
  13. package/dist/paths.d.ts +13 -0
  14. package/dist/paths.d.ts.map +1 -0
  15. package/dist/paths.js +18 -0
  16. package/dist/provider/chat-config.d.ts +1 -1
  17. package/dist/provider/chat-config.d.ts.map +1 -1
  18. package/dist/provider/chat-config.js +15 -3
  19. package/dist/provider/events.d.ts +9 -0
  20. package/dist/provider/events.d.ts.map +1 -1
  21. package/dist/provider/events.js +15 -0
  22. package/dist/provider/index.d.ts +12 -1
  23. package/dist/provider/index.d.ts.map +1 -1
  24. package/dist/provider/index.js +153 -7
  25. package/dist/provider/metrics-trace.d.ts +274 -0
  26. package/dist/provider/metrics-trace.d.ts.map +1 -0
  27. package/dist/provider/metrics-trace.js +174 -0
  28. package/dist/provider/mlx-identity.d.ts +16 -0
  29. package/dist/provider/mlx-identity.d.ts.map +1 -0
  30. package/dist/provider/mlx-identity.js +15 -0
  31. package/dist/provider/model-host.d.ts +45 -3
  32. package/dist/provider/model-host.d.ts.map +1 -1
  33. package/dist/provider/model-host.js +34 -2
  34. package/dist/provider/model-registry-filter.d.ts +74 -18
  35. package/dist/provider/model-registry-filter.d.ts.map +1 -1
  36. package/dist/provider/model-registry-filter.js +229 -38
  37. package/dist/provider/models.d.ts +2 -3
  38. package/dist/provider/models.d.ts.map +1 -1
  39. package/dist/provider/models.js +46 -20
  40. package/dist/provider/stream-adapter.d.ts +37 -4
  41. package/dist/provider/stream-adapter.d.ts.map +1 -1
  42. package/dist/provider/stream-adapter.js +82 -7
  43. package/dist/provider/warm-reuse.d.ts +11 -8
  44. package/dist/provider/warm-reuse.d.ts.map +1 -1
  45. package/dist/provider/warm-reuse.js +14 -7
  46. package/dist/run-agent.d.ts +12 -3
  47. package/dist/run-agent.d.ts.map +1 -1
  48. package/dist/run-agent.js +31 -5
  49. package/package.json +10 -5
@@ -54,6 +54,9 @@ const THINKING_LEVEL_TO_EFFORT = {
54
54
  xhigh: 'high',
55
55
  max: 'high',
56
56
  };
57
+ function isPositiveSafeInteger(value) {
58
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
59
+ }
57
60
  /**
58
61
  * Resolve Pi's thinking level once for both native config and persisted replay
59
62
  * provenance. Keeping these values together prevents a low/minimal turn from
@@ -67,7 +70,7 @@ export function resolveReasoningMode(reasoning) {
67
70
  thinkingEnabled: reasoningEffort === 'medium' || reasoningEffort === 'high',
68
71
  };
69
72
  }
70
- export function buildChatConfig(modelType, options, tools, rootCacheOwnerId, resolvedReasoning = resolveReasoningMode(options?.reasoning)) {
73
+ export function buildChatConfig(modelType, options, tools, rootCacheOwnerId, resolvedReasoning = resolveReasoningMode(options?.reasoning), modelMaxTokens) {
71
74
  const preset = launchPresetFor(modelType);
72
75
  if (!preset) {
73
76
  const known = [...new Set([...Object.keys(LAUNCH_PRESETS), ...Object.keys(AGENT_LAUNCH_PRESETS)])].join(', ');
@@ -92,8 +95,17 @@ export function buildChatConfig(modelType, options, tools, rootCacheOwnerId, res
92
95
  // which branch the bounded GDN sidecar store protects from child eviction.
93
96
  if (rootCacheOwnerId !== undefined)
94
97
  config.cacheRootOwnerId = rootCacheOwnerId;
95
- if (options?.maxTokens !== undefined)
96
- config.maxNewTokens = options.maxTokens;
98
+ const explicitMaxTokens = options?.maxTokens;
99
+ if (isPositiveSafeInteger(explicitMaxTokens)) {
100
+ // A valid per-call provider option is the topmost layer.
101
+ config.maxNewTokens = explicitMaxTokens;
102
+ }
103
+ else if (isPositiveSafeInteger(modelMaxTokens)) {
104
+ // Normal Pi agent turns omit SimpleStreamOptions.maxTokens. Honor the
105
+ // composed Model metadata (including models.json modelOverrides) without
106
+ // allowing malformed/hostile metadata to replace the family preset.
107
+ config.maxNewTokens = modelMaxTokens;
108
+ }
97
109
  if (options?.temperature !== undefined)
98
110
  config.temperature = options.temperature;
99
111
  if (tools && tools.length > 0)
@@ -21,6 +21,15 @@ import type { ChatStreamDelta, ChatStreamFinal, PerformanceMetrics } from '@mlx-
21
21
  export declare function emptyUsage(): Usage;
22
22
  export declare class TurnEmitter {
23
23
  private readonly onPerformance?;
24
+ /**
25
+ * Correlation id for this turn. Minted here, stamped on the partial message
26
+ * as the custom `mlxTraceId` field (survives pi's JSONL round-trip like
27
+ * `mlxThinkingEnabled`), and surfaced so the stream adapter can join a
28
+ * `MetricsTrace` record to the persisted pi message. The pi entry id is not
29
+ * knowable in-turn (pi emits `message_end` before persisting), so this
30
+ * minted id is the durable join key.
31
+ */
32
+ readonly traceId: string;
24
33
  private readonly stream;
25
34
  private readonly partial;
26
35
  private readonly textBuffer;
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/provider/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,GAAG,EACH,gBAAgB,EAChB,2BAA2B,EAC3B,KAAK,EAIL,KAAK,EACN,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAkB,MAAM,cAAc,CAAC;AAMzG;;;GAGG;AACH,wBAAgB,UAAU,IAAI,KAAK,CASlC;AA2CD,qBAAa,WAAW;IAkBpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAjBjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IACrD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA4B;IAC3D;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB,CAAM;IACtC,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,QAAQ,CAAS;IAEzB,YACE,MAAM,EAAE,2BAA2B,EACnC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACA,aAAa,CAAC,GAAE,CAAC,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,kBAAkB,KAAK,IAAI,aAAA,EACrG,eAAe,CAAC,EAAE,OAAO,EAuB1B;IAED,OAAO,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAqBpC;IAED,OAAO,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CA8CpC;IAED;;;;;OAKG;IACH,SAAS,IAAI,IAAI,CAGhB;IAED;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAG1B;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAqBvB,OAAO,CAAC,cAAc;IAmBtB,OAAO,CAAC,iBAAiB;IA6BzB,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,UAAU;CAGnB"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/provider/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EACV,GAAG,EACH,gBAAgB,EAChB,2BAA2B,EAC3B,KAAK,EAIL,KAAK,EACN,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAkB,MAAM,cAAc,CAAC;AAMzG;;;GAGG;AACH,wBAAgB,UAAU,IAAI,KAAK,CASlC;AA2CD,qBAAa,WAAW;IA2BpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IA1BjC;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IACrD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA4B;IAC3D;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB,CAAM;IACtC,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,QAAQ,CAAS;IAEzB,YACE,MAAM,EAAE,2BAA2B,EACnC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACA,aAAa,CAAC,GAAE,CAAC,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,kBAAkB,KAAK,IAAI,aAAA,EACrG,eAAe,CAAC,EAAE,OAAO,EA4B1B;IAED,OAAO,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAqBpC;IAED,OAAO,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CA8CpC;IAED;;;;;OAKG;IACH,SAAS,IAAI,IAAI,CAGhB;IAED;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAG1B;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAqBvB,OAAO,CAAC,cAAc;IAmBtB,OAAO,CAAC,iBAAiB;IA6BzB,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,UAAU;CAGnB"}
@@ -12,6 +12,7 @@
12
12
  * - Every method is throw-safe: the pi StreamFn contract does not allow
13
13
  * the emitter to throw, so internal failures route to {@link onError}.
14
14
  */
15
+ import { randomUUID } from 'node:crypto';
15
16
  import { coerceErrorMessage } from './error-coercion.js';
16
17
  import { ReasoningTagBuffer } from './reasoning-tag-buffer.js';
17
18
  import { ToolCallTagBuffer } from './tool-call-buffer.js';
@@ -69,6 +70,15 @@ function toPiToolCall(call) {
69
70
  }
70
71
  export class TurnEmitter {
71
72
  onPerformance;
73
+ /**
74
+ * Correlation id for this turn. Minted here, stamped on the partial message
75
+ * as the custom `mlxTraceId` field (survives pi's JSONL round-trip like
76
+ * `mlxThinkingEnabled`), and surfaced so the stream adapter can join a
77
+ * `MetricsTrace` record to the persisted pi message. The pi entry id is not
78
+ * knowable in-turn (pi emits `message_end` before persisting), so this
79
+ * minted id is the durable join key.
80
+ */
81
+ traceId;
72
82
  stream;
73
83
  partial;
74
84
  textBuffer = new ToolCallTagBuffer();
@@ -85,6 +95,8 @@ export class TurnEmitter {
85
95
  constructor(stream, model, onPerformance, thinkingEnabled) {
86
96
  this.onPerformance = onPerformance;
87
97
  this.stream = stream;
98
+ const traceId = randomUUID();
99
+ this.traceId = traceId;
88
100
  this.partial = {
89
101
  role: 'assistant',
90
102
  content: [],
@@ -104,6 +116,9 @@ export class TurnEmitter {
104
116
  if (thinkingEnabled !== undefined) {
105
117
  this.partial.mlxThinkingEnabled = thinkingEnabled;
106
118
  }
119
+ // Same custom-field mechanism: stamp the trace id so a dashboard can join
120
+ // the durable MetricsTrace record back to this persisted pi message.
121
+ this.partial.mlxTraceId = traceId;
107
122
  stream.push({ type: 'start', partial: this.partial });
108
123
  }
109
124
  onDelta(delta) {
@@ -15,8 +15,19 @@
15
15
  * `ExtensionAPI` value arrives as the factory argument.
16
16
  */
17
17
  import type { InlineExtension } from '@earendil-works/pi-coding-agent';
18
+ import { type ColdCacheStats, type ColdSidecarStats } from '@mlx-node/core';
19
+ import { MetricsTrace } from './metrics-trace.js';
18
20
  import { MlxModelHost } from './model-host.js';
19
21
  import type { MlxModelInfo } from './models.js';
22
+ /** Injectable seams for {@link createMlxProviderExtension} (unit tests). */
23
+ export interface MlxProviderExtensionDeps {
24
+ /** Process-wide cold-tier reader; defaults to the native addon (absent in unit tests). */
25
+ coldStats?: () => ColdCacheStats | undefined;
26
+ /** Process-wide sidecar-counter reader; defaults to the native addon. */
27
+ sidecarStats?: () => ColdSidecarStats | undefined;
28
+ /** Durable per-turn telemetry sink; defaults to a fresh {@link MetricsTrace}. */
29
+ metricsTrace?: MetricsTrace;
30
+ }
20
31
  /**
21
32
  * Build the `mlx-provider` inline extension serving `models`. The host
22
33
  * (one per process — it owns the single GPU-resident model) is created
@@ -24,5 +35,5 @@ import type { MlxModelInfo } from './models.js';
24
35
  * host, but stays lazy about weights: nothing loads until the first
25
36
  * `streamSimple` call.
26
37
  */
27
- export declare function createMlxProviderExtension(models: MlxModelInfo[], host?: MlxModelHost): InlineExtension;
38
+ export declare function createMlxProviderExtension(models: MlxModelInfo[], host?: MlxModelHost, deps?: MlxProviderExtensionDeps): InlineExtension;
28
39
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/provider/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAErF,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,eAAe,CAqCvG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/provider/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACrF,OAAO,EAAoC,KAAK,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAG9G,OAAO,EAAE,YAAY,EAA2B,MAAM,oBAAoB,CAAC;AAE3E,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA4BhD,4EAA4E;AAC5E,MAAM,WAAW,wBAAwB;IACvC,0FAA0F;IAC1F,SAAS,CAAC,EAAE,MAAM,cAAc,GAAG,SAAS,CAAC;IAC7C,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,gBAAgB,GAAG,SAAS,CAAC;IAClD,iFAAiF;IACjF,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,YAAY,EAAE,EACtB,IAAI,CAAC,EAAE,YAAY,EACnB,IAAI,GAAE,wBAA6B,GAClC,eAAe,CA6KjB"}
@@ -14,9 +14,37 @@
14
14
  * at module top level. Only type-only pi imports appear here; the
15
15
  * `ExtensionAPI` value arrives as the factory argument.
16
16
  */
17
+ import { coldCacheStats, coldSidecarStats } from '@mlx-node/core';
18
+ import { canonicalCacheRoot } from '../cold-tier.js';
19
+ import { MetricsTrace } from './metrics-trace.js';
20
+ import { MLX_API, MLX_API_KEY, MLX_BASE_URL, MLX_PROVIDER_ID } from './mlx-identity.js';
17
21
  import { MlxModelHost } from './model-host.js';
18
22
  import { PerformanceStatus } from './performance-status.js';
19
23
  import { makeMlxStreamSimple } from './stream-adapter.js';
24
+ /** Read the process-wide cold-tier snapshot; the native addon may be absent (unit tests). */
25
+ function safeColdStats() {
26
+ try {
27
+ return coldCacheStats();
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ /**
34
+ * Read the process-wide sidecar counters. Separate from {@link safeColdStats}
35
+ * because it reads a different native struct that has no `enabled` gate: the
36
+ * counters live in a plain static, not in the tier, so they are valid even
37
+ * when the tier never opened — which is exactly the case a run with zero
38
+ * sidecars needs distinguished.
39
+ */
40
+ function safeSidecarStats() {
41
+ try {
42
+ return coldSidecarStats();
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
20
48
  /**
21
49
  * Build the `mlx-provider` inline extension serving `models`. The host
22
50
  * (one per process — it owns the single GPU-resident model) is created
@@ -24,27 +52,145 @@ import { makeMlxStreamSimple } from './stream-adapter.js';
24
52
  * host, but stays lazy about weights: nothing loads until the first
25
53
  * `streamSimple` call.
26
54
  */
27
- export function createMlxProviderExtension(models, host) {
55
+ export function createMlxProviderExtension(models, host, deps = {}) {
28
56
  const resolvedHost = host ?? new MlxModelHost(models.map((m) => m.discovered));
29
57
  const performanceStatus = new PerformanceStatus();
58
+ const metricsTrace = deps.metricsTrace ?? new MetricsTrace();
59
+ const readColdStats = deps.coldStats ?? safeColdStats;
60
+ const readSidecarStats = deps.sidecarStats ?? safeSidecarStats;
30
61
  // This closure outlives Pi runtime replacement. Pi creates a replacement
31
62
  // runtime for /new and /resume and reruns inline extension factories; each
32
63
  // new factory's session_start updates the root while child sessions keep
33
- // using this registered stream.
64
+ // using this registered stream. The root id doubles as the cache owner and
65
+ // the metrics-trace root; the JSONL path travels alongside it for metrics.
34
66
  let rootCacheOwnerId;
35
- const streamSimple = makeMlxStreamSimple(resolvedHost, performanceStatus.record, () => rootCacheOwnerId);
67
+ let rootSessionFile;
68
+ // Cold-tier counters are cumulative since the tier opened. Snapshot them at
69
+ // each turn's native start (`onTurnStart`, fired inside the serialized host
70
+ // closure) and diff at the success terminal, so a turn that aborted or
71
+ // errored — which never reaches `onTurnRecord` — can't leak its restores into
72
+ // the next successful turn's delta. Inference is serialized per process (host
73
+ // promise chain), so by the time a turn snapshots, any prior turn has fully
74
+ // drained. The SYNCHRONOUS counters (hits/misses/bytesRestored/enqueued/
75
+ // corruptions/queueDrops/restoreDeclines) are exact per-turn; bytesWritten,
76
+ // evictions and writeErrors advance on the async writer thread, so those
77
+ // deltas are approximate (documented per field on the record).
78
+ //
79
+ // The SIDECAR counters are snapshotted the same way but are exact without
80
+ // exception: every one of them is recorded inside the native turn finalize,
81
+ // on the calling thread, before the turn returns. They also have no `enabled`
82
+ // gate — they live in a plain process static rather than in the tier — so
83
+ // they read honestly on a run where the tier never opened, which is the run
84
+ // that most needs them.
85
+ let turnStartCold = readColdStats();
86
+ let turnStartSidecar = readSidecarStats();
87
+ const onTurnStart = () => {
88
+ turnStartCold = readColdStats();
89
+ turnStartSidecar = readSidecarStats();
90
+ };
91
+ const onTurnRecord = ({ traceId, sessionId, rootSessionId, rootSessionFile, model, final, durationMs, queueMs, resident, }) => {
92
+ const rec = {
93
+ traceId,
94
+ ts: Date.now(),
95
+ sessionId,
96
+ rootSessionId,
97
+ rootSessionFile,
98
+ model,
99
+ durationMs,
100
+ queueMs,
101
+ resident,
102
+ finishReason: final.finishReason,
103
+ promptTokens: final.promptTokens,
104
+ cachedTokens: final.cachedTokens ?? 0,
105
+ outputTokens: final.numTokens,
106
+ reasoningTokens: final.reasoningTokens,
107
+ };
108
+ const perf = final.performance;
109
+ if (perf) {
110
+ rec.ttftMs = perf.ttftMs;
111
+ rec.prefillTps = perf.prefillTokensPerSecond;
112
+ rec.decodeTps = perf.decodeTokensPerSecond;
113
+ rec.mtpCycles = perf.mtpCycles;
114
+ // mlx-vlm-comparable headline accept rate (committed tokens per cycle).
115
+ rec.mtpMeanAccepted = perf.mtpMeanAcceptedTokensTotal;
116
+ }
117
+ const cold = readColdStats();
118
+ if (cold && turnStartCold) {
119
+ rec.coldHits = cold.hits - turnStartCold.hits;
120
+ rec.coldMisses = cold.misses - turnStartCold.misses;
121
+ rec.coldBytesWritten = cold.bytesWritten - turnStartCold.bytesWritten;
122
+ rec.coldBytesRestored = cold.bytesRestored - turnStartCold.bytesRestored;
123
+ rec.coldEnqueued = cold.enqueued - turnStartCold.enqueued;
124
+ rec.coldQueueDrops = cold.queueDrops - turnStartCold.queueDrops;
125
+ rec.coldEvictions = cold.evictions - turnStartCold.evictions;
126
+ rec.coldCorruptions = cold.corruptions - turnStartCold.corruptions;
127
+ rec.coldWriteErrors = cold.writeErrors - turnStartCold.writeErrors;
128
+ rec.coldRestoreDeclines = cold.restoreDeclines - turnStartCold.restoreDeclines;
129
+ // Absolutes, not deltas: an aborted/errored turn never reaches this
130
+ // recorder, so a corruption or a dropped write during one lands in NO
131
+ // delta. The cumulative counter observed by the next successful turn
132
+ // still carries it, which is what makes "corruptions must be 0"
133
+ // checkable at all (`MAX(total) > 0` over any window).
134
+ rec.coldCorruptionsTotal = cold.corruptions;
135
+ rec.coldQueueDropsTotal = cold.queueDrops;
136
+ // Same latch, and the counter that needs it most: a write error is
137
+ // raised on the background writer, so the one covering the LAST turn
138
+ // before a crash lands in no delta at all — and "is my cache root
139
+ // broken?" is a question about ever, not about this turn.
140
+ rec.coldWriteErrorsTotal = cold.writeErrors;
141
+ // Cache IDENTITY comes from the END-of-turn snapshot, never the baseline:
142
+ // the tier opens LAZILY on first use, so on a process's first turn
143
+ // `turnStartCold` is the all-zero default with `enabled: false` and an
144
+ // empty root. Canonicalized here — in the writer — so the dashboard
145
+ // never has to match whatever spelling Rust happened to construct.
146
+ rec.coldEnabled = cold.enabled;
147
+ // ONE emptiness test, applied to the CANONICAL value. Gating on the raw
148
+ // native string used a DIFFERENT test from the one `canonicalCacheRoot`
149
+ // applies (it trims), so a whitespace-only root passed `length > 0` here
150
+ // and canonicalized to `''` — which `MetricsTrace.record` then dropped,
151
+ // leaving a row that says the tier was ON while carrying no root at all.
152
+ if (cold.enabled) {
153
+ const canonicalRoot = canonicalCacheRoot(cold.root);
154
+ if (canonicalRoot.length > 0)
155
+ rec.coldRoot = canonicalRoot;
156
+ }
157
+ }
158
+ // Its own guard, deliberately not folded into the block above: the sidecar
159
+ // reader never consults the tier, so it keeps reporting when `cold` is
160
+ // undefined or the tier failed to open. A run with `coldHits: 0` and no
161
+ // tier is exactly when "did the capture even run?" is the question, and
162
+ // gating these on `cold` would blank them precisely then.
163
+ const sidecar = readSidecarStats();
164
+ if (sidecar && turnStartSidecar) {
165
+ rec.coldSidecarCaptureReached = sidecar.captureReached - turnStartSidecar.captureReached;
166
+ rec.coldSidecarChainEmpty = sidecar.chainEmpty - turnStartSidecar.chainEmpty;
167
+ rec.coldSidecarBoundarySkips = sidecar.boundarySkips - turnStartSidecar.boundarySkips;
168
+ rec.coldSidecarAlreadyPersisted = sidecar.alreadyPersisted - turnStartSidecar.alreadyPersisted;
169
+ rec.coldSidecarEnqueued = sidecar.enqueued - turnStartSidecar.enqueued;
170
+ rec.coldSidecarQueueDrops = sidecar.queueDrops - turnStartSidecar.queueDrops;
171
+ rec.coldSidecarInstalled = sidecar.installed - turnStartSidecar.installed;
172
+ rec.coldSidecarRestoreSuppressed = sidecar.restoreSuppressed - turnStartSidecar.restoreSuppressed;
173
+ }
174
+ metricsTrace.record(rec);
175
+ };
176
+ const streamSimple = makeMlxStreamSimple(resolvedHost, performanceStatus.record, () => rootCacheOwnerId, onTurnRecord, onTurnStart, () => rootSessionFile);
36
177
  return {
37
178
  name: 'mlx-provider',
38
179
  factory: (pi) => {
39
- pi.registerProvider('mlx', {
40
- api: 'mlx',
41
- baseUrl: 'mlx://local',
42
- apiKey: 'mlx-local',
180
+ pi.registerProvider(MLX_PROVIDER_ID, {
181
+ api: MLX_API,
182
+ baseUrl: MLX_BASE_URL,
183
+ apiKey: MLX_API_KEY,
43
184
  streamSimple,
44
185
  models: models.map((m) => m.piModel),
45
186
  });
46
187
  pi.on('session_start', (_event, ctx) => {
47
188
  rootCacheOwnerId = ctx.sessionManager.getSessionId();
189
+ // Snapshot the root JSONL path so a turn submitted under this root is
190
+ // correlated to it at completion — even for subagent turns, which have
191
+ // no session file of their own. `getSessionFile` is optional-chained so
192
+ // a minimal test/mock session manager without it still works.
193
+ rootSessionFile = ctx.sessionManager.getSessionFile?.();
48
194
  });
49
195
  pi.on('message_end', (event, ctx) => {
50
196
  performanceStatus.showMessage(event, ctx);
@@ -0,0 +1,274 @@
1
+ /**
2
+ * `MetricsTrace` — always-on per-turn inference telemetry, appended as JSON
3
+ * Lines to `$HOME/.mlx-node/metrics/traces/<YYYY-MM-DD>-<pid>.jsonl`.
4
+ *
5
+ * This is a durable sink that complements the transient in-memory
6
+ * {@link ./performance-status.ts} WeakMap (which only feeds the live TUI
7
+ * footer). One record is written per successful inference turn so the
8
+ * dashboard can correlate throughput, cache reuse, and cold-tier deltas back
9
+ * to the pi session that produced them via `mlxTraceId`.
10
+ *
11
+ * Contract:
12
+ * - Default-on; the `MLX_AGENT_METRICS` env var set to `0` / `false` / `off`
13
+ * (case-insensitive) is the only kill switch.
14
+ * - `record()` NEVER throws: every field is allowlisted (no free text ever
15
+ * lands on disk) and all fs work is wrapped — telemetry must never break
16
+ * an inference turn.
17
+ */
18
+ /**
19
+ * Per-turn delta of every COUNTER on the native `ColdCacheStats` — the paged
20
+ * K/V block traffic. One entry per native counter field, named
21
+ * `cold` + PascalCase(nativeKey); the three non-counter fields (`enabled`,
22
+ * `root`, `quotaBytes`) describe the tier's identity rather than a turn and are
23
+ * carried separately as {@link MetricsTraceRecord.coldEnabled} /
24
+ * {@link MetricsTraceRecord.coldRoot}.
25
+ *
26
+ * This is the TS half of a cross-language invariant.
27
+ * `__test__/cold-counter-fields.test.ts` derives the same names from
28
+ * `coldCacheStats()` at runtime and demands an exact match, so a counter added
29
+ * natively but not here — or, the failure this list exists for, one quietly
30
+ * dropped from here — is a red test rather than an empty dashboard column.
31
+ */
32
+ export declare const COLD_COUNTER_FIELDS: readonly ['coldHits', 'coldMisses', 'coldEnqueued', 'coldQueueDrops', 'coldBytesWritten', 'coldBytesRestored', 'coldEvictions', 'coldCorruptions', 'coldWriteErrors', 'coldRestoreDeclines'];
33
+ /**
34
+ * Per-turn delta of every counter on the native `ColdSidecarStats` — the
35
+ * recurrent / sliding-window state that lives OUTSIDE the paged pool. Named
36
+ * `coldSidecar` + PascalCase(nativeKey).
37
+ *
38
+ * Deliberately a second, differently-prefixed list rather than a merge: both
39
+ * native structs carry `enqueued` and `queueDrops`, and they count different
40
+ * objects (blocks vs sidecars). Flattening them into one namespace would make
41
+ * two unrelated numbers collide on one column.
42
+ */
43
+ export declare const COLD_SIDECAR_FIELDS: readonly ['coldSidecarCaptureReached', 'coldSidecarChainEmpty', 'coldSidecarBoundarySkips', 'coldSidecarAlreadyPersisted', 'coldSidecarEnqueued', 'coldSidecarQueueDrops', 'coldSidecarInstalled', 'coldSidecarRestoreSuppressed'];
44
+ /** Every cold-tier per-turn delta field, in native-struct order. */
45
+ export declare const COLD_DELTA_FIELDS: readonly ["coldHits", "coldMisses", "coldEnqueued", "coldQueueDrops", "coldBytesWritten", "coldBytesRestored", "coldEvictions", "coldCorruptions", "coldWriteErrors", "coldRestoreDeclines", "coldSidecarCaptureReached", "coldSidecarChainEmpty", "coldSidecarBoundarySkips", "coldSidecarAlreadyPersisted", "coldSidecarEnqueued", "coldSidecarQueueDrops", "coldSidecarInstalled", "coldSidecarRestoreSuppressed"];
46
+ /** A key of {@link COLD_DELTA_FIELDS}; every one is an optional `number`. */
47
+ export type ColdDeltaField = (typeof COLD_DELTA_FIELDS)[number];
48
+ /**
49
+ * One inference turn's telemetry. Every field is a number, a small
50
+ * enumerated string (`finishReason`), or an identifier — never model output
51
+ * text, tool arguments, or prompt content.
52
+ */
53
+ export interface MetricsTraceRecord extends Partial<Record<ColdDeltaField, number>> {
54
+ /** Schema version. */
55
+ v: 1;
56
+ /** Join key minted by `TurnEmitter`; also stamped on the pi message as `mlxTraceId`. */
57
+ traceId: string;
58
+ /** Wall-clock write time (ms since epoch). */
59
+ ts: number;
60
+ /** pi per-request session id (parent vs each subagent differ). */
61
+ sessionId?: string;
62
+ /** Root pi session id the turn was SUBMITTED under (snapshotted at submit). */
63
+ rootSessionId?: string;
64
+ /** Root pi session JSONL file path the turn was SUBMITTED under (snapshotted at submit). */
65
+ rootSessionFile?: string;
66
+ /** Model id served this turn (`mlx/<dir-name>`'s `<dir-name>`). */
67
+ model: string;
68
+ /** Turn duration (ms) bracketing resident selection + prefill + decode. */
69
+ durationMs: number;
70
+ /**
71
+ * Queue + cold-load wait (ms) before native work began this turn — the gap
72
+ * between turn submission and the serialized inference callback firing.
73
+ * Subtract from `durationMs` to isolate execution-only latency.
74
+ */
75
+ queueMs?: number;
76
+ /** `true` when the model was already warm/resident, `false` on a cold load/swap. */
77
+ resident?: boolean;
78
+ /** Native finish reason (`stop` / `length` / `tool_calls` / …). */
79
+ finishReason: string;
80
+ promptTokens: number;
81
+ cachedTokens: number;
82
+ outputTokens: number;
83
+ reasoningTokens: number;
84
+ ttftMs?: number;
85
+ prefillTps?: number;
86
+ decodeTps?: number;
87
+ mtpCycles?: number;
88
+ mtpMeanAccepted?: number;
89
+ /** Cold-tier restore hits accrued this turn (synchronous counter — exact). */
90
+ coldHits?: number;
91
+ /** Cold-tier lookup misses accrued this turn (synchronous counter — exact). */
92
+ coldMisses?: number;
93
+ /**
94
+ * Cold-tier bytes that LANDED this turn — credited natively only after the
95
+ * payload sync, the commit rename and the directory fsync all succeeded, so
96
+ * a write that failed contributes nothing here and one {@link
97
+ * coldWriteErrors} instead.
98
+ *
99
+ * Advances on an async writer thread, so this delta is APPROXIMATE — it may
100
+ * attribute a prior turn's flush to the turn that observes it, and a write
101
+ * still in flight at the end of the turn lands in a later one. That is the
102
+ * only sense in which it lags; it is never an enqueue-time estimate.
103
+ */
104
+ coldBytesWritten?: number;
105
+ /** Cold-tier bytes read back on validated hits this turn (synchronous — exact). */
106
+ coldBytesRestored?: number;
107
+ /**
108
+ * Canonical cold-cache root this turn's tier was operating in, as produced by
109
+ * `canonicalCacheRoot` — the JOIN KEY the dashboard scopes its hit-rate and
110
+ * trend to. Written ONLY when the tier was actually open; a turn that ran
111
+ * with the tier off carries `coldEnabled: false` and NO root, and a record
112
+ * written by a build that predates this field carries neither. Those two
113
+ * states are deliberately distinguishable: the dashboard reports "recorded
114
+ * before cache attribution existed" separately from "the tier was off".
115
+ */
116
+ coldRoot?: string;
117
+ /** Whether the cold tier was open for this turn (`ColdCacheStats.enabled`). */
118
+ coldEnabled?: boolean;
119
+ /**
120
+ * Cold-tier write-queue submissions accrued this turn. Incremented on the
121
+ * calling thread, so this delta is EXACT.
122
+ */
123
+ coldEnqueued?: number;
124
+ /**
125
+ * Cold-tier writes REFUSED at admission this turn because the bounded queue
126
+ * was full. Incremented on the calling thread, so this delta is EXACT.
127
+ *
128
+ * Now admission refusals only: the commit-rename-failure arm used to be
129
+ * counted here too, which put one event under a name describing a different
130
+ * cause. It is a {@link coldWriteErrors} instead, and the two are disjoint —
131
+ * never sum them.
132
+ */
133
+ coldQueueDrops?: number;
134
+ /**
135
+ * Cold-tier objects evicted by quota enforcement this turn. Advances on the
136
+ * background writer thread — APPROXIMATE, same caveat as `coldBytesWritten`.
137
+ */
138
+ coldEvictions?: number;
139
+ /**
140
+ * Cold-tier objects that failed validation on restore this turn. Incremented
141
+ * on the calling thread, so this delta is EXACT. Always accompanied by a
142
+ * miss, i.e. corruptions are a SUBSET of `coldMisses` — never add the two.
143
+ */
144
+ coldCorruptions?: number;
145
+ /**
146
+ * CUMULATIVE corruptions since the tier opened in this process, not a delta.
147
+ * Carried because the acceptance bar is "corruptions must be 0" and a delta
148
+ * alone cannot prove it: a turn that aborts or errors never reaches
149
+ * `record()`, so a corruption during it lands in no delta at all. The next
150
+ * successful turn's absolute total still includes it, so `MAX(total) > 0`
151
+ * over any window is the sound "did this ever happen" latch.
152
+ */
153
+ coldCorruptionsTotal?: number;
154
+ /**
155
+ * CUMULATIVE queue drops since the tier opened in this process, for the same
156
+ * reason as {@link coldCorruptionsTotal}: an errored turn's dropped writes
157
+ * would otherwise be invisible.
158
+ */
159
+ coldQueueDropsTotal?: number;
160
+ /**
161
+ * Cold-tier writes this turn that the queue ACCEPTED and that never reached
162
+ * disk — a read-only, full or unmounted cache root, a quota the object
163
+ * cannot fit, a failed rename, a failed fsync.
164
+ *
165
+ * The native writer is deliberately fail-open (a broken cache root must not
166
+ * change a single emitted token) and it returns its error to nobody, so
167
+ * before this counter existed the entire class was invisible: a turn against
168
+ * an unwritable root reported `coldQueueDrops 0`, `coldCorruptions 0`, an
169
+ * empty stderr and a clean exit. Advances on the background writer thread,
170
+ * so APPROXIMATE per turn in the same way {@link coldBytesWritten} is —
171
+ * non-zero over a window is the signal, not the exact attribution.
172
+ */
173
+ coldWriteErrors?: number;
174
+ /**
175
+ * Restores REFUSED this turn: the tier held candidate blocks and the walk
176
+ * handed back none of them.
177
+ *
178
+ * Neither a hit nor a miss, because both of those count per-block lookups
179
+ * and a refusal happens instead of one — which is why a refused restore
180
+ * reported `coldHits 0, coldMisses 0`, exactly the row a turn that never
181
+ * consulted the tier produces. Incremented on the calling thread, so EXACT.
182
+ * The reason (`no_backed_boundary`, `parent_chain_unavailable`,
183
+ * `restore_short_of_boundary`) and the block geometry go to the
184
+ * `[MLX_TRACE] paged cold_restore_declined` line.
185
+ */
186
+ coldRestoreDeclines?: number;
187
+ /**
188
+ * CUMULATIVE write errors since the tier opened in this process, for the
189
+ * same reason as {@link coldCorruptionsTotal} — and more sharply, since this
190
+ * counter advances on the background writer: the error covering the last
191
+ * turn before a crash reaches no delta at all.
192
+ *
193
+ * No such total exists for {@link coldRestoreDeclines} on purpose. A "did it
194
+ * ever happen" latch is only meaningful for a counter whose healthy value is
195
+ * zero; the first turn of any new prompt legitimately declines, so a latch
196
+ * there would be pinned on from the first minute and mean nothing.
197
+ */
198
+ coldWriteErrorsTotal?: number;
199
+ /**
200
+ * Turns this turn's process spent reaching a family's sidecar capture. Every
201
+ * other `coldSidecar*` counter is a sub-count of this one, so `0` here while
202
+ * blocks were written means the finalize path never called the capture at
203
+ * all — a different bug from a capture that ran and declined.
204
+ */
205
+ coldSidecarCaptureReached?: number;
206
+ /**
207
+ * Sidecar captures that found no whole persisted block to anchor recurrent
208
+ * state under.
209
+ */
210
+ coldSidecarChainEmpty?: number;
211
+ /**
212
+ * Sidecar captures whose chain covered blocks but where no retained
213
+ * checkpoint sat at or below its reach — the "ladder collapsed to its
214
+ * deepest rung" signature.
215
+ */
216
+ coldSidecarBoundarySkips?: number;
217
+ /**
218
+ * Sidecar captures that selected a boundary already on disk. The STEADY
219
+ * STATE of a repeated prompt, not a fault: without it a healthy run
220
+ * (`coldSidecarEnqueued == 0` because the first turn already wrote it) is
221
+ * indistinguishable from a broken one.
222
+ */
223
+ coldSidecarAlreadyPersisted?: number;
224
+ /**
225
+ * Sidecars handed to the writer queue this turn. A SUBSET of
226
+ * {@link coldEnqueued}, which counts every object that took a queue slot —
227
+ * so summing them double-counts each sidecar.
228
+ */
229
+ coldSidecarEnqueued?: number;
230
+ /**
231
+ * Sidecars the writer queue refused because it was full. Likewise a subset
232
+ * of {@link coldQueueDrops}, not a separate population.
233
+ */
234
+ coldSidecarQueueDrops?: number;
235
+ /**
236
+ * Restored sidecars a family INSTALLED as its live per-turn state. The one
237
+ * read-side sidecar counter, and the only thing that separates "restored and
238
+ * used" from "restored, then silently re-derived by a full O(prefix) replay":
239
+ * the replay produces correct state, so `cachedTokens`, `coldHits`,
240
+ * `coldCorruptions` and the output text are all identical either way.
241
+ */
242
+ coldSidecarInstalled?: number;
243
+ /**
244
+ * Restored prefixes a family THREW AWAY this turn, releasing the request and
245
+ * restarting the turn cold (gemma4's large-sliding-restore suppression).
246
+ *
247
+ * A different event from {@link coldRestoreDeclines}: there the walk refused
248
+ * to serve anything, here it served and the family discarded it. This one is
249
+ * therefore preceded by real `coldHits` and `coldBytesRestored`, so the turn
250
+ * reads as successful reuse right up to the point where it recomputes the
251
+ * whole prompt anyway.
252
+ */
253
+ coldSidecarRestoreSuppressed?: number;
254
+ }
255
+ type MetricsTraceInput = Omit<MetricsTraceRecord, 'v'>;
256
+ export declare class MetricsTrace {
257
+ readonly enabled: boolean;
258
+ private readonly dir;
259
+ private readonly now;
260
+ constructor(opts?: {
261
+ dir?: string;
262
+ now?: () => number;
263
+ });
264
+ /** `<dir>/<YYYY-MM-DD>-<pid>.jsonl` — UTC date so rotation is timezone-stable. */
265
+ currentFile(): string;
266
+ /**
267
+ * Append one allowlisted JSON line. Never throws: a broken sink must not
268
+ * surface into the inference path. Excess input properties are dropped — the
269
+ * record is rebuilt field by field so free text can never reach disk.
270
+ */
271
+ record(rec: MetricsTraceInput): void;
272
+ }
273
+ export {};
274
+ //# sourceMappingURL=metrics-trace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics-trace.d.ts","sourceRoot":"","sources":["../../src/provider/metrics-trace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,YAC9B,UAAU,EACV,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,CACb,CAAC;AAEX;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,YAC9B,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,8BAA8B,CACtB,CAAC;AAEX,oEAAoE;AACpE,eAAO,MAAM,iBAAiB,uZAA4D,CAAC;AAE3F,6EAA6E;AAC7E,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,kBAAmB,SAAQ,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;IACjF,sBAAsB;IACtB,CAAC,EAAE,CAAC,CAAC;IACL,wFAAwF;IACxF,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4FAA4F;IAC5F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;;;;;;;OAYG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;;;;;OAUG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;;;;;OAKG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;OASG;IACH,4BAA4B,CAAC,EAAE,MAAM,CAAC;CACvC;AAED,KAAK,iBAAiB,GAAG,IAAI,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;AAcvD,qBAAa,YAAY;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,YAAY,IAAI,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE,EAItD;IAED,kFAAkF;IAClF,WAAW,IAAI,MAAM,CAGpB;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,CAyDnC;CACF"}