@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
@@ -0,0 +1,174 @@
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
+ import { appendFileSync, mkdirSync } from 'node:fs';
19
+ import { dirname, join } from 'node:path';
20
+ import { metricsTraceDir } from '../paths.js';
21
+ /**
22
+ * Per-turn delta of every COUNTER on the native `ColdCacheStats` — the paged
23
+ * K/V block traffic. One entry per native counter field, named
24
+ * `cold` + PascalCase(nativeKey); the three non-counter fields (`enabled`,
25
+ * `root`, `quotaBytes`) describe the tier's identity rather than a turn and are
26
+ * carried separately as {@link MetricsTraceRecord.coldEnabled} /
27
+ * {@link MetricsTraceRecord.coldRoot}.
28
+ *
29
+ * This is the TS half of a cross-language invariant.
30
+ * `__test__/cold-counter-fields.test.ts` derives the same names from
31
+ * `coldCacheStats()` at runtime and demands an exact match, so a counter added
32
+ * natively but not here — or, the failure this list exists for, one quietly
33
+ * dropped from here — is a red test rather than an empty dashboard column.
34
+ */
35
+ export const COLD_COUNTER_FIELDS = [
36
+ 'coldHits',
37
+ 'coldMisses',
38
+ 'coldEnqueued',
39
+ 'coldQueueDrops',
40
+ 'coldBytesWritten',
41
+ 'coldBytesRestored',
42
+ 'coldEvictions',
43
+ 'coldCorruptions',
44
+ 'coldWriteErrors',
45
+ 'coldRestoreDeclines',
46
+ ];
47
+ /**
48
+ * Per-turn delta of every counter on the native `ColdSidecarStats` — the
49
+ * recurrent / sliding-window state that lives OUTSIDE the paged pool. Named
50
+ * `coldSidecar` + PascalCase(nativeKey).
51
+ *
52
+ * Deliberately a second, differently-prefixed list rather than a merge: both
53
+ * native structs carry `enqueued` and `queueDrops`, and they count different
54
+ * objects (blocks vs sidecars). Flattening them into one namespace would make
55
+ * two unrelated numbers collide on one column.
56
+ */
57
+ export const COLD_SIDECAR_FIELDS = [
58
+ 'coldSidecarCaptureReached',
59
+ 'coldSidecarChainEmpty',
60
+ 'coldSidecarBoundarySkips',
61
+ 'coldSidecarAlreadyPersisted',
62
+ 'coldSidecarEnqueued',
63
+ 'coldSidecarQueueDrops',
64
+ 'coldSidecarInstalled',
65
+ 'coldSidecarRestoreSuppressed',
66
+ ];
67
+ /** Every cold-tier per-turn delta field, in native-struct order. */
68
+ export const COLD_DELTA_FIELDS = [...COLD_COUNTER_FIELDS, ...COLD_SIDECAR_FIELDS];
69
+ function envDisabled() {
70
+ const raw = process.env.MLX_AGENT_METRICS;
71
+ if (raw === undefined)
72
+ return false;
73
+ const normalized = raw.trim().toLowerCase();
74
+ return normalized === '0' || normalized === 'false' || normalized === 'off';
75
+ }
76
+ /** A finite number, or `undefined` if the value is absent / non-finite. */
77
+ function finite(value) {
78
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
79
+ }
80
+ export class MetricsTrace {
81
+ enabled;
82
+ dir;
83
+ now;
84
+ constructor(opts) {
85
+ this.enabled = !envDisabled();
86
+ this.dir = opts?.dir ?? metricsTraceDir();
87
+ this.now = opts?.now ?? Date.now;
88
+ }
89
+ /** `<dir>/<YYYY-MM-DD>-<pid>.jsonl` — UTC date so rotation is timezone-stable. */
90
+ currentFile() {
91
+ const date = new Date(this.now()).toISOString().slice(0, 10);
92
+ return join(this.dir, `${date}-${process.pid}.jsonl`);
93
+ }
94
+ /**
95
+ * Append one allowlisted JSON line. Never throws: a broken sink must not
96
+ * surface into the inference path. Excess input properties are dropped — the
97
+ * record is rebuilt field by field so free text can never reach disk.
98
+ */
99
+ record(rec) {
100
+ if (!this.enabled)
101
+ return;
102
+ try {
103
+ const out = {
104
+ v: 1,
105
+ traceId: rec.traceId,
106
+ ts: rec.ts,
107
+ model: rec.model,
108
+ durationMs: rec.durationMs,
109
+ finishReason: rec.finishReason,
110
+ promptTokens: rec.promptTokens,
111
+ cachedTokens: rec.cachedTokens,
112
+ outputTokens: rec.outputTokens,
113
+ reasoningTokens: rec.reasoningTokens,
114
+ };
115
+ if (rec.sessionId !== undefined)
116
+ out.sessionId = rec.sessionId;
117
+ if (rec.rootSessionId !== undefined)
118
+ out.rootSessionId = rec.rootSessionId;
119
+ if (rec.rootSessionFile !== undefined)
120
+ out.rootSessionFile = rec.rootSessionFile;
121
+ const queueMs = finite(rec.queueMs);
122
+ if (queueMs !== undefined)
123
+ out.queueMs = queueMs;
124
+ if (typeof rec.resident === 'boolean')
125
+ out.resident = rec.resident;
126
+ const ttftMs = finite(rec.ttftMs);
127
+ if (ttftMs !== undefined)
128
+ out.ttftMs = ttftMs;
129
+ const prefillTps = finite(rec.prefillTps);
130
+ if (prefillTps !== undefined)
131
+ out.prefillTps = prefillTps;
132
+ const decodeTps = finite(rec.decodeTps);
133
+ if (decodeTps !== undefined)
134
+ out.decodeTps = decodeTps;
135
+ const mtpCycles = finite(rec.mtpCycles);
136
+ if (mtpCycles !== undefined)
137
+ out.mtpCycles = mtpCycles;
138
+ const mtpMeanAccepted = finite(rec.mtpMeanAccepted);
139
+ if (mtpMeanAccepted !== undefined)
140
+ out.mtpMeanAccepted = mtpMeanAccepted;
141
+ // Every cold-tier delta comes off ONE list, so a counter can only be
142
+ // dropped from the JSONL by being dropped from `COLD_DELTA_FIELDS` —
143
+ // which `__test__/cold-counter-fields.test.ts` pins to the native structs.
144
+ // Spelling them out here is what let four `coldCacheStats()` counters sit
145
+ // unwritten for the life of the feature.
146
+ for (const key of COLD_DELTA_FIELDS) {
147
+ const value = finite(rec[key]);
148
+ if (value !== undefined)
149
+ out[key] = value;
150
+ }
151
+ // A cache identity is only meaningful for a tier that was actually open;
152
+ // an empty root would create a bucket no dashboard root can ever match.
153
+ if (typeof rec.coldRoot === 'string' && rec.coldRoot.length > 0)
154
+ out.coldRoot = rec.coldRoot;
155
+ if (typeof rec.coldEnabled === 'boolean')
156
+ out.coldEnabled = rec.coldEnabled;
157
+ const coldCorruptionsTotal = finite(rec.coldCorruptionsTotal);
158
+ if (coldCorruptionsTotal !== undefined)
159
+ out.coldCorruptionsTotal = coldCorruptionsTotal;
160
+ const coldQueueDropsTotal = finite(rec.coldQueueDropsTotal);
161
+ if (coldQueueDropsTotal !== undefined)
162
+ out.coldQueueDropsTotal = coldQueueDropsTotal;
163
+ const coldWriteErrorsTotal = finite(rec.coldWriteErrorsTotal);
164
+ if (coldWriteErrorsTotal !== undefined)
165
+ out.coldWriteErrorsTotal = coldWriteErrorsTotal;
166
+ const file = this.currentFile();
167
+ mkdirSync(dirname(file), { recursive: true });
168
+ appendFileSync(file, `${JSON.stringify(out)}\n`);
169
+ }
170
+ catch {
171
+ // Telemetry is best-effort; a broken sink must never break inference.
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Identity of the single in-process `mlx` provider, shared between the provider
3
+ * registration ({@link ../provider/index.ts}) and the mlx-only policy adapter
4
+ * ({@link ./model-registry-filter.ts}) so the two can never drift. In particular
5
+ * the adapter pins mlx auth to {@link MLX_API_KEY}/{@link MLX_BASE_URL}, which
6
+ * MUST match what `registerProvider('mlx', …)` sets.
7
+ */
8
+ /** Reserved provider id for the local mlx provider. */
9
+ export declare const MLX_PROVIDER_ID = "mlx";
10
+ /** Provider `api` tag for local mlx models. */
11
+ export declare const MLX_API = "mlx";
12
+ /** Local (non-network) base URL for the mlx provider. */
13
+ export declare const MLX_BASE_URL = "mlx://local";
14
+ /** Literal marker apiKey — flags the provider configured; never a real key. */
15
+ export declare const MLX_API_KEY = "mlx-local";
16
+ //# sourceMappingURL=mlx-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mlx-identity.d.ts","sourceRoot":"","sources":["../../src/provider/mlx-identity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,uDAAuD;AACvD,eAAO,MAAM,eAAe,QAAQ,CAAC;AACrC,+CAA+C;AAC/C,eAAO,MAAM,OAAO,QAAQ,CAAC;AAC7B,yDAAyD;AACzD,eAAO,MAAM,YAAY,gBAAgB,CAAC;AAC1C,+EAA+E;AAC/E,eAAO,MAAM,WAAW,cAAc,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Identity of the single in-process `mlx` provider, shared between the provider
3
+ * registration ({@link ../provider/index.ts}) and the mlx-only policy adapter
4
+ * ({@link ./model-registry-filter.ts}) so the two can never drift. In particular
5
+ * the adapter pins mlx auth to {@link MLX_API_KEY}/{@link MLX_BASE_URL}, which
6
+ * MUST match what `registerProvider('mlx', …)` sets.
7
+ */
8
+ /** Reserved provider id for the local mlx provider. */
9
+ export const MLX_PROVIDER_ID = 'mlx';
10
+ /** Provider `api` tag for local mlx models. */
11
+ export const MLX_API = 'mlx';
12
+ /** Local (non-network) base URL for the mlx provider. */
13
+ export const MLX_BASE_URL = 'mlx://local';
14
+ /** Literal marker apiKey — flags the provider configured; never a real key. */
15
+ export const MLX_API_KEY = 'mlx-local';
@@ -13,16 +13,43 @@
13
13
  * overlapping native activity on the compiled-path globals).
14
14
  */
15
15
  import { ChatSession, loadModel } from '@mlx-node/lm';
16
+ import { COLD_TIER_RESTORE_FAMILIES } from '../cold-tier.js';
16
17
  import type { DiscoveredModelLike } from '../types.js';
18
+ /**
19
+ * Re-exported so the HOST consults exactly the symbol the drift guard and the
20
+ * `--no-persist-cache` help text consult. The definition lives in the
21
+ * native-free `../cold-tier.js` leaf (reachable off-package through the
22
+ * `@mlx-node/agent/catalog` subpath, which re-exports it)
23
+ * because this module value-imports `@mlx-node/lm`, which loads the native
24
+ * addon — the dashboard and the CLI help path must be able to read the list
25
+ * without that.
26
+ */
27
+ export { COLD_TIER_RESTORE_FAMILIES };
28
+ /** Per-load policy handed to {@link MlxModelHostOptions.resolveModelPathFn}. */
29
+ export interface ModelLoadPolicy {
30
+ /**
31
+ * Authoritative cold-tier directive for the config overlay
32
+ * (`persist_paged_cache` in the cloned config.json). Present ONLY for loads
33
+ * of a {@link COLD_TIER_RESTORE_FAMILIES} family, carrying the resolved
34
+ * {@link MlxModelHostOptions.persistPagedCache} value as an EXPLICIT
35
+ * boolean: `true` enables the SSD cold tier, `false` authoritatively
36
+ * disables it — overriding any `persist_paged_cache` the checkpoint's own
37
+ * config.json hard-codes, so `mlx agent --no-persist-cache` truly wins.
38
+ * Every other family receives no policy at all, so the overlay never touches
39
+ * the field for them.
40
+ */
41
+ persistPagedCache: boolean;
42
+ }
17
43
  export interface MlxModelHostOptions {
18
44
  /** Injectable model loader so tests can stub native loading. */
19
45
  loadModelFn?: typeof loadModel;
20
46
  /**
21
47
  * Optional load-path policy. `mlx agent` uses this to point the loader at
22
48
  * an ephemeral config overlay with block-paged attention enabled while
23
- * leaving the checkpoint directory untouched.
49
+ * leaving the checkpoint directory untouched. The optional per-load policy
50
+ * carries the qwen3 cold-tier opt-in (see {@link ModelLoadPolicy}).
24
51
  */
25
- resolveModelPathFn?: (model: DiscoveredModelLike) => Promise<string>;
52
+ resolveModelPathFn?: (model: DiscoveredModelLike, policy?: ModelLoadPolicy) => Promise<string>;
26
53
  /**
27
54
  * Reject a loaded model unless its native paged-cache adapter is active.
28
55
  * The agent entrypoint enables this so a model/platform incompatibility
@@ -32,12 +59,21 @@ export interface MlxModelHostOptions {
32
59
  * executor is currently flat-cache-only.
33
60
  */
34
61
  requirePagedCache?: boolean;
62
+ /**
63
+ * Enable the cold tier (persisted paged prefix blocks) by default. `mlx
64
+ * agent` turns this on; `mlx agent --no-persist-cache` sets it false.
65
+ * Applied only to loads of a {@link COLD_TIER_RESTORE_FAMILIES} family —
66
+ * every other family keeps per-layer state outside the paged pool, so its
67
+ * prefix cannot be restored soundly. Defaults true.
68
+ */
69
+ persistPagedCache?: boolean;
35
70
  }
36
71
  export declare class MlxModelHost {
37
72
  private readonly byName;
38
73
  private readonly loadModelFn;
39
74
  private readonly resolveModelPathFn;
40
75
  private readonly requirePagedCache;
76
+ private readonly persistPagedCache;
41
77
  private resident;
42
78
  private chain;
43
79
  constructor(models: DiscoveredModelLike[], opts?: MlxModelHostOptions);
@@ -57,13 +93,19 @@ export declare class MlxModelHost {
57
93
  * use the resident session; there is deliberately no method that
58
94
  * returns a session outside the serialized section.
59
95
  *
96
+ * `fn` also receives a `resident` boolean: `true` when the turn reused
97
+ * the already-loaded model (warm — no load happened), `false` when it had
98
+ * to load or swap the checkpoint first. This is the same warm/cold
99
+ * distinction the branch below already makes; surfacing it lets a metrics
100
+ * consumer separate queue wait from cold-load time without another channel.
101
+ *
60
102
  * Swaps drop the old session + model refs BEFORE loading the new
61
103
  * checkpoint so GC + native destructors can reclaim the old weights
62
104
  * during the load. A load failure leaves no resident (next call
63
105
  * retries); a failure thrown by `fn` rejects only this call's promise
64
106
  * and keeps the resident loaded for later callers.
65
107
  */
66
- runWithResident<T>(modelId: string, fn: (session: ChatSession) => Promise<T>): Promise<T>;
108
+ runWithResident<T>(modelId: string, fn: (session: ChatSession, resident: boolean) => Promise<T>): Promise<T>;
67
109
  /**
68
110
  * Flag the current resident as post-error so the next turn does a full
69
111
  * reset instead of a warm reuse. No-op unless `modelId` is the live
@@ -1 +1 @@
1
- {"version":3,"file":"model-host.d.ts","sourceRoot":"","sources":["../../src/provider/model-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAA4B,MAAM,cAAc,CAAC;AAEhF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,SAAS,CAAC;IAC/B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrE;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAmBD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;IAC/C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAkD;IACrF,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,KAAK,CAAuC;IAEpD,YAAY,MAAM,EAAE,mBAAmB,EAAE,EAAE,IAAI,GAAE,mBAAwB,EAKxE;IAED,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED;;;;;OAKG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAE1D;IAED;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAmCxF;IAED;;;;;OAKG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIvC;IAED;;;;;OAKG;IACH,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO7C;IAED;;;;OAIG;IACH,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIxC;IAED;;;;OAIG;IACH,OAAO,CAAC,aAAa;CAQtB"}
1
+ {"version":3,"file":"model-host.d.ts","sourceRoot":"","sources":["../../src/provider/model-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAA4B,MAAM,cAAc,CAAC;AAEhF,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD;;;;;;;;GAQG;AACH,OAAO,EAAE,0BAA0B,EAAE,CAAC;AAEtC,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;;OAUG;IACH,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/F;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAmBD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;IAC/C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA4E;IAC/G,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,KAAK,CAAuC;IAEpD,YAAY,MAAM,EAAE,mBAAmB,EAAE,EAAE,IAAI,GAAE,mBAAwB,EAMxE;IAED,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED;;;;;OAKG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAE1D;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAgD3G;IAED;;;;;OAKG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIvC;IAED;;;;;OAKG;IACH,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO7C;IAED;;;;OAIG;IACH,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIxC;IAED;;;;OAIG;IACH,OAAO,CAAC,aAAa;CAQtB"}
@@ -13,11 +13,23 @@
13
13
  * overlapping native activity on the compiled-path globals).
14
14
  */
15
15
  import { ChatSession, loadModel } from '@mlx-node/lm';
16
+ import { COLD_TIER_RESTORE_FAMILIES } from '../cold-tier.js';
17
+ /**
18
+ * Re-exported so the HOST consults exactly the symbol the drift guard and the
19
+ * `--no-persist-cache` help text consult. The definition lives in the
20
+ * native-free `../cold-tier.js` leaf (reachable off-package through the
21
+ * `@mlx-node/agent/catalog` subpath, which re-exports it)
22
+ * because this module value-imports `@mlx-node/lm`, which loads the native
23
+ * addon — the dashboard and the CLI help path must be able to read the list
24
+ * without that.
25
+ */
26
+ export { COLD_TIER_RESTORE_FAMILIES };
16
27
  export class MlxModelHost {
17
28
  byName = new Map();
18
29
  loadModelFn;
19
30
  resolveModelPathFn;
20
31
  requirePagedCache;
32
+ persistPagedCache;
21
33
  resident = null;
22
34
  chain = Promise.resolve();
23
35
  constructor(models, opts = {}) {
@@ -26,6 +38,7 @@ export class MlxModelHost {
26
38
  this.loadModelFn = opts.loadModelFn ?? loadModel;
27
39
  this.resolveModelPathFn = opts.resolveModelPathFn ?? (async (model) => model.path);
28
40
  this.requirePagedCache = opts.requirePagedCache ?? false;
41
+ this.persistPagedCache = opts.persistPagedCache ?? true;
29
42
  }
30
43
  get residentId() {
31
44
  return this.resident?.id ?? null;
@@ -47,6 +60,12 @@ export class MlxModelHost {
47
60
  * use the resident session; there is deliberately no method that
48
61
  * returns a session outside the serialized section.
49
62
  *
63
+ * `fn` also receives a `resident` boolean: `true` when the turn reused
64
+ * the already-loaded model (warm — no load happened), `false` when it had
65
+ * to load or swap the checkpoint first. This is the same warm/cold
66
+ * distinction the branch below already makes; surfacing it lets a metrics
67
+ * consumer separate queue wait from cold-load time without another channel.
68
+ *
50
69
  * Swaps drop the old session + model refs BEFORE loading the new
51
70
  * checkpoint so GC + native destructors can reclaim the old weights
52
71
  * during the load. A load failure leaves no resident (next call
@@ -61,12 +80,25 @@ export class MlxModelHost {
61
80
  }
62
81
  return this.runSerialized(async () => {
63
82
  let session;
83
+ // `true` when this turn reuses the loaded resident (warm), `false` when
84
+ // it loaded/swapped the checkpoint first (cold).
85
+ let resident;
64
86
  if (this.resident?.id === modelId) {
65
87
  session = this.resident.session;
88
+ resident = true;
66
89
  }
67
90
  else {
91
+ resident = false;
68
92
  this.resident = null;
69
- const resolvedPath = await this.resolveModelPathFn(entry);
93
+ // Only a COLD_TIER_RESTORE_FAMILIES family has a sound paged cold
94
+ // restore. Hand it an EXPLICIT tri-state directive so the overlay can
95
+ // authoritatively set the flag either way (default-on, or
96
+ // `--no-persist-cache` off — overriding any value in the checkpoint's
97
+ // config.json). Every other family gets no policy at all, so the
98
+ // overlay never touches the field for them.
99
+ const resolvedPath = COLD_TIER_RESTORE_FAMILIES.has(entry.modelType)
100
+ ? await this.resolveModelPathFn(entry, { persistPagedCache: this.persistPagedCache })
101
+ : await this.resolveModelPathFn(entry);
70
102
  const model = await this.loadModelFn(resolvedPath);
71
103
  const sessionModel = model;
72
104
  const gemmaDraftActive = entry.modelType === 'gemma4' && sessionModel.hasMtpWeights?.() === true;
@@ -83,7 +115,7 @@ export class MlxModelHost {
83
115
  session = new ChatSession(sessionModel);
84
116
  this.resident = { id: modelId, session, model, dirty: false };
85
117
  }
86
- return await fn(session);
118
+ return await fn(session, resident);
87
119
  });
88
120
  }
89
121
  /**
@@ -1,36 +1,92 @@
1
1
  /**
2
- * Process-local policy adapter for pi's public ModelRegistry.
2
+ * Process-local policy adapter for pi's canonical `ModelRuntime`.
3
3
  *
4
- * `mlx agent` is an offline/local product, but pi's registry also contains
5
- * every authenticated built-in cloud provider. CLI `--models mlx/*` only sets
6
- * the initial selector scope: Tab, `/models`, RPC enumeration, explicit model
7
- * resolution, and restored sessions still consult the registry's unscoped
8
- * reads. Filter those reads at their shared boundary before pi constructs its
9
- * runtime so every path sees only the exact local models this process serves.
4
+ * `mlx agent` is an offline/local product, but pi's runtime also composes every
5
+ * built-in cloud provider. CLI `--models mlx/*` only sets the initial selector
6
+ * scope: Tab, `/models`, RPC enumeration, explicit model resolution, and
7
+ * restored sessions all read the runtime's unscoped catalog/availability
8
+ * directly (the `ModelRegistry` facade handed to extensions delegates to the
9
+ * same runtime). Filter those reads at their shared boundary the runtime
10
+ * prototype — so every path sees only the exact local models this process
11
+ * serves. Patching the runtime (not the extension-only facade) is what keeps
12
+ * the mlx-only guarantee across the selector / listing / resolution paths.
13
+ *
14
+ * The guarantee is an ALLOWLIST across three surfaces, all keyed on the `mlx`
15
+ * provider id:
16
+ * 1. Model reads (`getModels`/`getAvailable*`/`getModel`) — exact local-model
17
+ * identity (`api === 'mlx' && baseUrl === 'mlx://local'`).
18
+ * 2. Provider/auth reads (`getProviders`/`getProvider`/`checkAuth`/`getAuth`/
19
+ * `isUsingOAuth`/`hasConfiguredAuth`/`listCredentials`/`getProviderAuthStatus`)
20
+ * — never surface, report configured, resolve auth for, or enumerate a
21
+ * credential of any non-`mlx` provider. `getAuth` is the pivotal one: pi's
22
+ * `/login`, `/logout`, and the built-in `/llama` command all resolve auth
23
+ * through it, and its OAuth/`LLAMA_BASE_URL` `fetch` consults neither
24
+ * `PI_OFFLINE` nor any allow-network flag; returning `undefined` for non-mlx
25
+ * makes those commands fail before any network or second-model-host load.
26
+ * 3. Auth mutation / network (`login` rejected for non-mlx; `refresh` forced to
27
+ * `allowNetwork: false` so an explicit `allowNetwork: true` — e.g. pi's
28
+ * `update --models` package command — can never override offline mode).
29
+ *
30
+ * The `mlx` provider is registered with a literal apiKey and never needs
31
+ * `/login`; streaming still works because `prepareRequest` calls `getAuth(model)`
32
+ * with `model.provider === 'mlx'`, which passes through.
33
+ *
34
+ * Surfaces 1-3 patch the runtime's PUBLIC facade, but pi's own internals read the
35
+ * composed provider map `this.models` (pi-ai `ModelsImpl`) directly — a boundary
36
+ * the facade patches cannot reach (e.g. `refresh` resolving a command-backed
37
+ * cloud credential). So there is a 4th, structural surface: the `recomposeProvider`
38
+ * choke composes ONLY `mlx` into `this.models`, making every internal read
39
+ * mlx-only by construction. It requires installation before the runtime is
40
+ * constructed (which `runAgent` guarantees).
10
41
  *
11
42
  * Keep this adapter isolated: once pi exposes a first-class provider allowlist
12
43
  * in `MainOptions`, this file can be replaced by that option without touching
13
44
  * the provider or CLI layers.
14
45
  */
15
- interface RegistryModel {
46
+ interface RuntimeModel {
16
47
  provider: string;
17
48
  id: string;
18
49
  api: string;
19
50
  baseUrl: string;
20
51
  }
21
- export interface FilterableModelRegistry<TModel extends RegistryModel = RegistryModel> {
22
- getAll(): TModel[];
23
- getAvailable(): TModel[];
24
- find(provider: string, modelId: string): TModel | undefined;
25
- hasConfiguredAuth(model: TModel): boolean;
52
+ /** Minimal shape of a pi `Provider` — only the id is needed to gate on provider. */
53
+ interface RuntimeProvider {
54
+ id: string;
55
+ }
56
+ /** Minimal shape of a pi `CredentialInfo` — only the provider id is needed. */
57
+ interface RuntimeCredential {
58
+ providerId: string;
59
+ }
60
+ /** Minimal shape of a pi `AuthStatus` — only the configured flag is asserted. */
61
+ interface RuntimeAuthStatus {
62
+ configured: boolean;
63
+ }
64
+ export interface FilterableModelRuntime<TModel extends RuntimeModel = RuntimeModel> {
65
+ getModels(providerId?: string): readonly TModel[];
66
+ getAvailableSnapshot(): readonly TModel[];
67
+ getAvailable(providerId?: string): Promise<readonly TModel[]>;
68
+ getModel(provider: string, modelId: string): TModel | undefined;
69
+ getProvider(providerId: string): RuntimeProvider | undefined;
70
+ hasConfiguredAuth(providerId: string): boolean;
71
+ checkAuth(providerId: string): Promise<unknown>;
72
+ isUsingOAuth(providerId: string): boolean;
73
+ getProviders(): readonly RuntimeProvider[];
74
+ listCredentials(): Promise<readonly RuntimeCredential[]>;
75
+ getProviderAuthStatus(providerId: string): RuntimeAuthStatus;
76
+ login(providerId: string, type: unknown, interaction: unknown): Promise<unknown>;
77
+ refresh(options?: {
78
+ allowNetwork?: boolean;
79
+ force?: boolean;
80
+ signal?: unknown;
81
+ }): Promise<unknown>;
26
82
  }
27
- export interface FilterableModelRegistryConstructor<TModel extends RegistryModel = RegistryModel> {
28
- prototype: FilterableModelRegistry<TModel>;
83
+ export interface FilterableModelRuntimeConstructor<TModel extends RuntimeModel = RuntimeModel> {
84
+ prototype: FilterableModelRuntime<TModel>;
29
85
  }
30
86
  /**
31
- * Install an exact local-model read policy for one `runAgent()` lifetime.
32
- * Returns an idempotent restore callback.
87
+ * Install an exact local-model / mlx-only-provider policy for one `runAgent()`
88
+ * lifetime. Returns an idempotent restore callback.
33
89
  */
34
- export declare function installMlxOnlyModelRegistryFilter<TModel extends RegistryModel>(Registry: FilterableModelRegistryConstructor<TModel>, modelIds: Iterable<string>): () => void;
90
+ export declare function installMlxOnlyModelRegistryFilter<TModel extends RuntimeModel>(Runtime: FilterableModelRuntimeConstructor<TModel>, modelIds: Iterable<string>): () => void;
35
91
  export {};
36
92
  //# sourceMappingURL=model-registry-filter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-registry-filter.d.ts","sourceRoot":"","sources":["../../src/provider/model-registry-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB,CAAC,MAAM,SAAS,aAAa,GAAG,aAAa;IACnF,MAAM,IAAI,MAAM,EAAE,CAAC;IACnB,YAAY,IAAI,MAAM,EAAE,CAAC;IACzB,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC5D,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3C;AAED,MAAM,WAAW,kCAAkC,CAAC,MAAM,SAAS,aAAa,GAAG,aAAa;IAC9F,SAAS,EAAE,uBAAuB,CAAC,MAAM,CAAC,CAAC;CAC5C;AAcD;;;GAGG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,SAAS,aAAa,EAC5E,QAAQ,EAAE,kCAAkC,CAAC,MAAM,CAAC,EACpD,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,GACzB,MAAM,IAAI,CA0DZ"}
1
+ {"version":3,"file":"model-registry-filter.d.ts","sourceRoot":"","sources":["../../src/provider/model-registry-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAIH,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,oFAAoF;AACpF,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,+EAA+E;AAC/E,UAAU,iBAAiB;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,iFAAiF;AACjF,UAAU,iBAAiB;IACzB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB,CAAC,MAAM,SAAS,YAAY,GAAG,YAAY;IAChF,SAAS,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAClD,oBAAoB,IAAI,SAAS,MAAM,EAAE,CAAC;IAC1C,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAChE,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IAC7D,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/C,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1C,YAAY,IAAI,SAAS,eAAe,EAAE,CAAC;IAC3C,eAAe,IAAI,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAC;IACzD,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,CAAC;IAC7D,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjF,OAAO,CAAC,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpG;AAED,MAAM,WAAW,iCAAiC,CAAC,MAAM,SAAS,YAAY,GAAG,YAAY;IAC3F,SAAS,EAAE,sBAAsB,CAAC,MAAM,CAAC,CAAC;CAC3C;AAqCD;;;GAGG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,SAAS,YAAY,EAC3E,OAAO,EAAE,iCAAiC,CAAC,MAAM,CAAC,EAClD,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,GACzB,MAAM,IAAI,CA2MZ"}