@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.
- package/dist/catalog.d.ts +15 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +15 -0
- package/dist/cold-tier.d.ts +99 -0
- package/dist/cold-tier.d.ts.map +1 -0
- package/dist/cold-tier.js +155 -0
- package/dist/extensions/local-image-input.d.ts +24 -0
- package/dist/extensions/local-image-input.d.ts.map +1 -0
- package/dist/extensions/local-image-input.js +114 -0
- package/dist/extensions/subagent.d.ts +20 -1
- package/dist/extensions/subagent.d.ts.map +1 -1
- package/dist/extensions/subagent.js +46 -6
- package/dist/paths.d.ts +13 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +18 -0
- package/dist/provider/chat-config.d.ts +1 -1
- package/dist/provider/chat-config.d.ts.map +1 -1
- package/dist/provider/chat-config.js +15 -3
- package/dist/provider/events.d.ts +9 -0
- package/dist/provider/events.d.ts.map +1 -1
- package/dist/provider/events.js +15 -0
- package/dist/provider/index.d.ts +12 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +153 -7
- package/dist/provider/metrics-trace.d.ts +274 -0
- package/dist/provider/metrics-trace.d.ts.map +1 -0
- package/dist/provider/metrics-trace.js +174 -0
- package/dist/provider/mlx-identity.d.ts +16 -0
- package/dist/provider/mlx-identity.d.ts.map +1 -0
- package/dist/provider/mlx-identity.js +15 -0
- package/dist/provider/model-host.d.ts +45 -3
- package/dist/provider/model-host.d.ts.map +1 -1
- package/dist/provider/model-host.js +34 -2
- package/dist/provider/model-registry-filter.d.ts +74 -18
- package/dist/provider/model-registry-filter.d.ts.map +1 -1
- package/dist/provider/model-registry-filter.js +229 -38
- package/dist/provider/models.d.ts +2 -3
- package/dist/provider/models.d.ts.map +1 -1
- package/dist/provider/models.js +46 -20
- package/dist/provider/stream-adapter.d.ts +37 -4
- package/dist/provider/stream-adapter.d.ts.map +1 -1
- package/dist/provider/stream-adapter.js +82 -7
- package/dist/provider/warm-reuse.d.ts +11 -8
- package/dist/provider/warm-reuse.d.ts.map +1 -1
- package/dist/provider/warm-reuse.js +14 -7
- package/dist/run-agent.d.ts +12 -3
- package/dist/run-agent.d.ts.map +1 -1
- package/dist/run-agent.js +31 -5
- package/package.json +10 -5
|
@@ -51,6 +51,16 @@ function safeString(read, fallback) {
|
|
|
51
51
|
return fallback;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
/** Read numeric model metadata without trusting a user-configurable getter. */
|
|
55
|
+
function readPositiveSafeInteger(read) {
|
|
56
|
+
try {
|
|
57
|
+
const value = read();
|
|
58
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
54
64
|
/**
|
|
55
65
|
* Publish the native model's load-time physical context limit onto pi's shared
|
|
56
66
|
* model object. The parent session and every in-process subagent resolve this
|
|
@@ -62,13 +72,18 @@ function safeString(read, fallback) {
|
|
|
62
72
|
* discovery-time limit here, and never let metadata synchronization break an
|
|
63
73
|
* otherwise valid inference turn.
|
|
64
74
|
*/
|
|
65
|
-
function publishEffectiveContextWindow(model, session) {
|
|
75
|
+
function publishEffectiveContextWindow(model, session, configuredModelMaxTokens) {
|
|
66
76
|
try {
|
|
67
77
|
const effective = Math.floor(session.contextLimits()?.effectiveWindowTokens ?? 0);
|
|
68
78
|
if (!Number.isSafeInteger(effective) || effective <= 0)
|
|
69
79
|
return;
|
|
70
80
|
model.contextWindow = Math.min(model.contextWindow, effective);
|
|
71
|
-
|
|
81
|
+
// Use the already-validated snapshot. Reading the shared metadata here
|
|
82
|
+
// would let Math.min coerce invalid user configuration into a valid value
|
|
83
|
+
// that a later turn would then trust.
|
|
84
|
+
if (configuredModelMaxTokens !== undefined) {
|
|
85
|
+
model.maxTokens = Math.min(configuredModelMaxTokens, model.contextWindow);
|
|
86
|
+
}
|
|
72
87
|
}
|
|
73
88
|
catch {
|
|
74
89
|
// Exact native preflight still protects capacity; keep serving the turn.
|
|
@@ -125,7 +140,7 @@ function failsafeMessage(model, reason, message) {
|
|
|
125
140
|
timestamp: Date.now(),
|
|
126
141
|
};
|
|
127
142
|
}
|
|
128
|
-
export function makeMlxStreamSimple(host, onPerformance, resolveRootCacheOwner) {
|
|
143
|
+
export function makeMlxStreamSimple(host, onPerformance, resolveRootCacheOwner, onTurnRecord, onTurnStart, resolveRootSessionFile) {
|
|
129
144
|
return (model, context, options) => {
|
|
130
145
|
const stream = createAssistantMessageEventStream();
|
|
131
146
|
/**
|
|
@@ -138,6 +153,7 @@ export function makeMlxStreamSimple(host, onPerformance, resolveRootCacheOwner)
|
|
|
138
153
|
let emitter;
|
|
139
154
|
let signal;
|
|
140
155
|
let rootCacheOwnerId;
|
|
156
|
+
let rootSessionFile;
|
|
141
157
|
let resolvedReasoning;
|
|
142
158
|
let detachAbort;
|
|
143
159
|
/**
|
|
@@ -208,8 +224,11 @@ export function makeMlxStreamSimple(host, onPerformance, resolveRootCacheOwner)
|
|
|
208
224
|
signal = options?.signal;
|
|
209
225
|
// Snapshot the top-level owner before this request can queue behind
|
|
210
226
|
// another inference. A later /new or /resume must not relabel an older
|
|
211
|
-
// request that was already submitted under the previous root.
|
|
227
|
+
// request that was already submitted under the previous root. The metrics
|
|
228
|
+
// root id reuses this same session_start value; only the JSONL path is a
|
|
229
|
+
// separate snapshot, taken here so it can never drift from the id.
|
|
212
230
|
rootCacheOwnerId = resolveRootCacheOwner?.();
|
|
231
|
+
rootSessionFile = resolveRootSessionFile?.();
|
|
213
232
|
// Snapshot once: the native config and the replay provenance must describe
|
|
214
233
|
// the same resolved template mode. Presence alone is wrong for Pi's
|
|
215
234
|
// minimal/low levels, both of which resolve to disabled thinking.
|
|
@@ -238,13 +257,42 @@ export function makeMlxStreamSimple(host, onPerformance, resolveRootCacheOwner)
|
|
|
238
257
|
}
|
|
239
258
|
void (async () => {
|
|
240
259
|
let sawNativeFinal = false;
|
|
241
|
-
|
|
260
|
+
// Bracket the whole serialized turn (queue wait + resident selection +
|
|
261
|
+
// prefill + decode) so a MetricsTrace record can report wall-clock
|
|
262
|
+
// duration. Read at the terminal only on the success path below.
|
|
263
|
+
const turnStartedAt = Date.now();
|
|
264
|
+
await host.runWithResident(model.id, async (session, resident) => {
|
|
265
|
+
// Callback entry: the queue wait + any cold model load/swap have now
|
|
266
|
+
// resolved, but no native work (prime/prefill/decode) has started. This
|
|
267
|
+
// is the seam that separates queue+load latency from execution time —
|
|
268
|
+
// `durationMs` (read at the terminal) still brackets the whole turn.
|
|
269
|
+
const execStartedAt = Date.now();
|
|
270
|
+
const queueMs = Math.max(0, execStartedAt - turnStartedAt);
|
|
242
271
|
// Terminated while queued behind earlier inference/loading (or
|
|
243
272
|
// between stages below): the terminal already went out — skip ALL
|
|
244
273
|
// session work (no warm-reset, no prime, no stream).
|
|
245
274
|
if (terminated)
|
|
246
275
|
return;
|
|
247
|
-
|
|
276
|
+
// Serialized-turn start: fire before any native work so a metrics
|
|
277
|
+
// consumer can snapshot process-wide cold-tier counters here and diff
|
|
278
|
+
// them at the success terminal. Because turns are serialized, a prior
|
|
279
|
+
// turn that aborted/errored has already fully drained by now, so its
|
|
280
|
+
// activity is baked into this snapshot and never attributed to this
|
|
281
|
+
// turn's delta. Best-effort — a throwing hook must not break inference.
|
|
282
|
+
if (onTurnStart) {
|
|
283
|
+
try {
|
|
284
|
+
onTurnStart();
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Telemetry snapshot is best-effort; never break inference.
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Snapshot the composed budget before publishing native context limits.
|
|
291
|
+
// The advisory publisher must not read this raw value itself:
|
|
292
|
+
// Math.min can coerce hostile metadata (for example "512" or Infinity)
|
|
293
|
+
// into a finite number that a later turn would then trust.
|
|
294
|
+
const configuredModelMaxTokens = readPositiveSafeInteger(() => model.maxTokens);
|
|
295
|
+
publishEffectiveContextWindow(model, session, configuredModelMaxTokens);
|
|
248
296
|
const supportsImages = publishImageCapability(model, session);
|
|
249
297
|
const discovered = host.modelInfo(model.id);
|
|
250
298
|
if (!discovered) {
|
|
@@ -271,7 +319,7 @@ export function makeMlxStreamSimple(host, onPerformance, resolveRootCacheOwner)
|
|
|
271
319
|
return;
|
|
272
320
|
try {
|
|
273
321
|
session.primeHistory(contextToChatMessages(context, supportsImages));
|
|
274
|
-
const config = buildChatConfig(discovered.modelType, options, toolsToDefinitions(context.tools), rootCacheOwnerId, resolvedReasoning);
|
|
322
|
+
const config = buildChatConfig(discovered.modelType, options, toolsToDefinitions(context.tools), rootCacheOwnerId, resolvedReasoning, configuredModelMaxTokens);
|
|
275
323
|
for await (const event of session.startFromHistoryStream(config, signal)) {
|
|
276
324
|
if (event.done) {
|
|
277
325
|
if (event.finishReason === 'error') {
|
|
@@ -308,6 +356,33 @@ export function makeMlxStreamSimple(host, onPerformance, resolveRootCacheOwner)
|
|
|
308
356
|
detachAbort = undefined;
|
|
309
357
|
try {
|
|
310
358
|
turn.onFinal(event);
|
|
359
|
+
// Durable per-turn telemetry: only a successful final gets
|
|
360
|
+
// here. Guarded independently of onFinal so a throwing
|
|
361
|
+
// recorder cannot derail the (already-emitted) terminal.
|
|
362
|
+
if (onTurnRecord) {
|
|
363
|
+
try {
|
|
364
|
+
onTurnRecord({
|
|
365
|
+
traceId: turn.traceId,
|
|
366
|
+
sessionId: options?.sessionId,
|
|
367
|
+
// Root correlation is the SUBMIT-time snapshot, not the
|
|
368
|
+
// live root, so a /new or /resume that landed while this
|
|
369
|
+
// turn ran can't reattribute it (id == cacheOwner root).
|
|
370
|
+
rootSessionId: rootCacheOwnerId,
|
|
371
|
+
rootSessionFile,
|
|
372
|
+
model: model.id,
|
|
373
|
+
final: event,
|
|
374
|
+
durationMs: Math.max(0, Date.now() - turnStartedAt),
|
|
375
|
+
// Queue+load wait captured at callback entry, and the
|
|
376
|
+
// warm/cold distinction from `runWithResident` — so the
|
|
377
|
+
// record can attribute wait vs execution latency.
|
|
378
|
+
queueMs,
|
|
379
|
+
resident,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// Telemetry is best-effort; never break inference.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
311
386
|
}
|
|
312
387
|
catch (err) {
|
|
313
388
|
pushFailsafeTerminal('error', coerceErrorMessage(err));
|
|
@@ -18,18 +18,19 @@
|
|
|
18
18
|
* the reused prefix and skip the corresponding re-prefill.
|
|
19
19
|
*
|
|
20
20
|
* Fields accessed: `inFlight`, `history`, `lastImagesKey`, `lastAudioKey`, `turnCount`,
|
|
21
|
-
* `unresolvedOkToolCallCount`, `needsFullReplay
|
|
22
|
-
* `ChatSession` (compile-time
|
|
23
|
-
* properties. The cast through
|
|
24
|
-
* gives this helper a typed view
|
|
25
|
-
* class's `private` declarations.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
21
|
+
* `unresolvedOkToolCallCount`, `needsFullReplay`, `defaultConfig`, `activeTools`.
|
|
22
|
+
* These are TypeScript `private` fields on `ChatSession` (compile-time
|
|
23
|
+
* only) — at runtime they are ordinary properties. The cast through
|
|
24
|
+
* {@link ChatSessionWarmReuseInternals} gives this helper a typed view
|
|
25
|
+
* of the instance without relaxing the class's `private` declarations.
|
|
26
|
+
* The field names MUST stay in sync with
|
|
27
|
+
* `packages/lm/src/chat-session.ts`; a mismatch would silently skip
|
|
28
|
+
* the intended state wipe — the drift test in
|
|
28
29
|
* `packages/agent/__test__/warm-reuse.test.ts` checks every name in
|
|
29
30
|
* {@link WARM_REUSE_TOUCHED_FIELDS} against a real `ChatSession`
|
|
30
31
|
* instance.
|
|
31
32
|
*/
|
|
32
|
-
import type { ChatSession, SessionCapableModel } from '@mlx-node/lm';
|
|
33
|
+
import type { ChatConfig, ChatSession, SessionCapableModel } from '@mlx-node/lm';
|
|
33
34
|
/**
|
|
34
35
|
* Private structural view of the `ChatSession` JS-side state that the
|
|
35
36
|
* warm-reuse helper needs to wipe. Mirrors the internal state
|
|
@@ -45,6 +46,8 @@ interface ChatSessionWarmReuseInternals {
|
|
|
45
46
|
turnCount: number;
|
|
46
47
|
unresolvedOkToolCallCount: number | null;
|
|
47
48
|
needsFullReplay: boolean;
|
|
49
|
+
defaultConfig?: ChatConfig;
|
|
50
|
+
activeTools: ChatConfig['tools'];
|
|
48
51
|
}
|
|
49
52
|
/**
|
|
50
53
|
* Runtime list of the `ChatSession` private field names this module
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"warm-reuse.d.ts","sourceRoot":"","sources":["../../src/provider/warm-reuse.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"warm-reuse.d.ts","sourceRoot":"","sources":["../../src/provider/warm-reuse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEjF;;;;;;GAMG;AACH,UAAU,6BAA6B;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,WAAW,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;CAClC;AAoBD;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAgD,aAAa,CACjG,MAAM,6BAA6B,CACpC,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,sCAAsC,CAAC,CAAC,SAAS,mBAAmB,EACxF,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,GACtB,OAAO,CAAC,IAAI,CAAC,CAyBf"}
|
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
* the reused prefix and skip the corresponding re-prefill.
|
|
19
19
|
*
|
|
20
20
|
* Fields accessed: `inFlight`, `history`, `lastImagesKey`, `lastAudioKey`, `turnCount`,
|
|
21
|
-
* `unresolvedOkToolCallCount`, `needsFullReplay
|
|
22
|
-
* `ChatSession` (compile-time
|
|
23
|
-
* properties. The cast through
|
|
24
|
-
* gives this helper a typed view
|
|
25
|
-
* class's `private` declarations.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
21
|
+
* `unresolvedOkToolCallCount`, `needsFullReplay`, `defaultConfig`, `activeTools`.
|
|
22
|
+
* These are TypeScript `private` fields on `ChatSession` (compile-time
|
|
23
|
+
* only) — at runtime they are ordinary properties. The cast through
|
|
24
|
+
* {@link ChatSessionWarmReuseInternals} gives this helper a typed view
|
|
25
|
+
* of the instance without relaxing the class's `private` declarations.
|
|
26
|
+
* The field names MUST stay in sync with
|
|
27
|
+
* `packages/lm/src/chat-session.ts`; a mismatch would silently skip
|
|
28
|
+
* the intended state wipe — the drift test in
|
|
28
29
|
* `packages/agent/__test__/warm-reuse.test.ts` checks every name in
|
|
29
30
|
* {@link WARM_REUSE_TOUCHED_FIELDS} against a real `ChatSession`
|
|
30
31
|
* instance.
|
|
@@ -43,6 +44,8 @@ const WARM_REUSE_TOUCHED_FIELD_SET = {
|
|
|
43
44
|
turnCount: true,
|
|
44
45
|
unresolvedOkToolCallCount: true,
|
|
45
46
|
needsFullReplay: true,
|
|
47
|
+
defaultConfig: true,
|
|
48
|
+
activeTools: true,
|
|
46
49
|
};
|
|
47
50
|
/**
|
|
48
51
|
* Runtime list of the `ChatSession` private field names this module
|
|
@@ -85,4 +88,8 @@ export async function resetPreservingNativeCacheForWarmReuse(session) {
|
|
|
85
88
|
internals.turnCount = 0;
|
|
86
89
|
internals.unresolvedOkToolCallCount = null;
|
|
87
90
|
internals.needsFullReplay = false;
|
|
91
|
+
// Tools are conversation state. A provider replay can switch to an
|
|
92
|
+
// unrelated history, so restore constructor defaults exactly like
|
|
93
|
+
// ChatSession.reset() instead of leaking the prior committed overlay.
|
|
94
|
+
internals.activeTools = internals.defaultConfig?.tools;
|
|
88
95
|
}
|
package/dist/run-agent.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* critical on pi's hard `process.exit()` paths.
|
|
18
18
|
*/
|
|
19
19
|
import type { InlineExtension } from '@earendil-works/pi-coding-agent';
|
|
20
|
-
import { type
|
|
20
|
+
import { type FilterableModelRuntimeConstructor } from './provider/model-registry-filter.js';
|
|
21
21
|
import type { MlxModelInfo } from './provider/models.js';
|
|
22
22
|
/** Shape of pi's `main(argv, { extensionFactories })` — also the test seam. */
|
|
23
23
|
export type RunAgentMain = (args: string[], opts: {
|
|
@@ -25,11 +25,11 @@ export type RunAgentMain = (args: string[], opts: {
|
|
|
25
25
|
}) => Promise<void>;
|
|
26
26
|
export interface RunAgentPi {
|
|
27
27
|
main: RunAgentMain;
|
|
28
|
-
|
|
28
|
+
ModelRuntime: FilterableModelRuntimeConstructor;
|
|
29
29
|
}
|
|
30
30
|
/** @internal Narrow lifecycle seam for the agent's paged config overlays. */
|
|
31
31
|
export interface AgentPagedConfigOverrides {
|
|
32
|
-
resolve(modelPath: string, modelType?: string): Promise<string>;
|
|
32
|
+
resolve(modelPath: string, modelType?: string, persistPagedCache?: boolean): Promise<string>;
|
|
33
33
|
cleanup(): Promise<void>;
|
|
34
34
|
}
|
|
35
35
|
export interface RunAgentOptions {
|
|
@@ -41,6 +41,15 @@ export interface RunAgentOptions {
|
|
|
41
41
|
argv: string[];
|
|
42
42
|
/** Native inference-log path to surface after Pi takes over the TUI. */
|
|
43
43
|
traceLogFile?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Enable the SSD cold tier by default (the agent's default; the CLI sets it
|
|
46
|
+
* false for `--no-persist-cache`). Forwarded to {@link MlxModelHost}, which
|
|
47
|
+
* applies this ONE value to every load whose family is in
|
|
48
|
+
* `COLD_TIER_RESTORE_FAMILIES` — not to qwen3 alone. Families off that list
|
|
49
|
+
* are handed no policy because they can never persist, not because this flag
|
|
50
|
+
* spares them. `undefined` keeps the host's on-by-default behavior.
|
|
51
|
+
*/
|
|
52
|
+
persistPagedCache?: boolean;
|
|
44
53
|
/** @internal Test seam; when set, the pi dynamic import is skipped entirely. */
|
|
45
54
|
piImpl?: RunAgentPi;
|
|
46
55
|
/** @internal Test seam for paged model-path resolution and cleanup. */
|
package/dist/run-agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-agent.d.ts","sourceRoot":"","sources":["../src/run-agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"run-agent.d.ts","sourceRoot":"","sources":["../src/run-agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAWvE,OAAO,EACL,KAAK,iCAAiC,EAEvC,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,+EAA+E;AAC/E,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE;IAAE,kBAAkB,EAAE,eAAe,EAAE,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE9G,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,YAAY,CAAC;IACnB,YAAY,EAAE,iCAAiC,CAAC;CACjD;AAED,6EAA6E;AAC7E,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,2FAA2F;IAC3F,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,yDAAyD;IACzD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,yBAAyB,CAAC;CAClD;AAED,mFAAmF;AACnF,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEpF;AAED;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA2EnE"}
|
package/dist/run-agent.js
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
|
+
import { coldCacheDrain } from '@mlx-node/core';
|
|
21
22
|
import { PagedConfigOverrideManager } from '@mlx-node/lm';
|
|
23
|
+
import { createLocalImageInputExtension } from './extensions/local-image-input.js';
|
|
22
24
|
import { createPermissionGateExtension } from './extensions/permission-gate.js';
|
|
23
25
|
import { createSubagentExtension } from './extensions/subagent.js';
|
|
24
26
|
import { createTerminalTitleExtension } from './extensions/terminal-title.js';
|
|
@@ -46,6 +48,15 @@ export async function runAgent(opts) {
|
|
|
46
48
|
// Mirrors `mlx launch claude`: chunked paged prefill keeps long-prompt
|
|
47
49
|
// TTFT bounded on the default paged path.
|
|
48
50
|
process.env.MLX_PAGED_PREFILL_CHUNK_SIZE ??= '2048';
|
|
51
|
+
// Hard offline invariant — NOT a user-overridable default, hence `=` not `??=`.
|
|
52
|
+
// `mlx agent` is local-only: no cloud provider may ever be contacted. pi 0.81.1's
|
|
53
|
+
// interactive and RPC startup call `ModelRuntime.refresh()`, which when PI_OFFLINE
|
|
54
|
+
// is unset fetches remote provider catalogs from pi.dev and can refresh a persisted
|
|
55
|
+
// cloud credential — a network path the mlx-only prototype filter does NOT cover
|
|
56
|
+
// (it patches the read methods, not `refresh`). Forcing PI_OFFLINE=1 here, before pi
|
|
57
|
+
// is imported below, also pins every ModelRuntime in this process to `allowNetwork`
|
|
58
|
+
// off, so no ambient/prior cloud credential can leak outbound traffic.
|
|
59
|
+
process.env.PI_OFFLINE = '1';
|
|
49
60
|
// Force every paged-capable agent family through an isolated config clone.
|
|
50
61
|
// This includes quantized LFM2 (whose standalone default is deliberately
|
|
51
62
|
// flat) and Qwen3.5 dense/MoE (whose text-only defaults are flat). Gemma4's
|
|
@@ -55,19 +66,23 @@ export async function runAgent(opts) {
|
|
|
55
66
|
const preserveEmbeddedGemmaDraft = agentGemmaDraftEnabled();
|
|
56
67
|
const pagedConfigOverrides = opts.pagedConfigOverrides ?? new PagedConfigOverrideManager({ preserveEmbeddedGemmaDraft });
|
|
57
68
|
const modelHost = new MlxModelHost(opts.models.map((model) => model.discovered), {
|
|
58
|
-
resolveModelPathFn: (model) => pagedConfigOverrides.resolve(model.path, model.modelType),
|
|
69
|
+
resolveModelPathFn: (model, policy) => pagedConfigOverrides.resolve(model.path, model.modelType, policy?.persistPagedCache),
|
|
59
70
|
requirePagedCache: true,
|
|
71
|
+
persistPagedCache: opts.persistPagedCache,
|
|
60
72
|
});
|
|
61
|
-
// Keep the pi import strictly behind the seam. The seam carries BOTH main
|
|
62
|
-
//
|
|
63
|
-
// installation/lifecycle instead of being able to bypass it accidentally.
|
|
73
|
+
// Keep the pi import strictly behind the seam. The seam carries BOTH main and
|
|
74
|
+
// the ModelRuntime class, so tests and production exercise the same policy
|
|
75
|
+
// installation/lifecycle instead of being able to bypass it accidentally. The
|
|
76
|
+
// filter patches the runtime prototype (not the extension-only ModelRegistry
|
|
77
|
+
// facade), which is where the selector / listing / resolution paths read.
|
|
64
78
|
const pi = opts.piImpl ?? (await import('@earendil-works/pi-coding-agent'));
|
|
65
|
-
const restoreModelRegistry = installMlxOnlyModelRegistryFilter(pi.
|
|
79
|
+
const restoreModelRegistry = installMlxOnlyModelRegistryFilter(pi.ModelRuntime, opts.models.map((model) => model.discovered.name));
|
|
66
80
|
const subagentsEnabled = opts.models.length > 0 && !opts.argv.includes('--no-extensions') && !opts.argv.includes('-ne');
|
|
67
81
|
try {
|
|
68
82
|
await pi.main(opts.argv, {
|
|
69
83
|
extensionFactories: [
|
|
70
84
|
createMlxProviderExtension(opts.models, modelHost),
|
|
85
|
+
createLocalImageInputExtension(),
|
|
71
86
|
createPermissionGateExtension(),
|
|
72
87
|
...(subagentsEnabled ? [createSubagentExtension()] : []),
|
|
73
88
|
...(opts.traceLogFile !== undefined ? [createTraceNoticeExtension(opts.traceLogFile)] : []),
|
|
@@ -81,6 +96,17 @@ export async function runAgent(opts) {
|
|
|
81
96
|
}
|
|
82
97
|
finally {
|
|
83
98
|
await pagedConfigOverrides.cleanup();
|
|
99
|
+
// `mlx agent -p` is one-shot: flush any accepted cold-tier prefix blocks
|
|
100
|
+
// to disk before the process exits, otherwise a prompt's just-persisted
|
|
101
|
+
// KV could still be queued/mid-write when we return. No-op when the tier
|
|
102
|
+
// was never opened, bounded so a stuck fsync can't hang exit, and never
|
|
103
|
+
// allowed to throw out of cleanup (best-effort durability).
|
|
104
|
+
try {
|
|
105
|
+
coldCacheDrain(5000);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Best-effort: a drain failure must never mask the real exit path.
|
|
109
|
+
}
|
|
84
110
|
}
|
|
85
111
|
}
|
|
86
112
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlx-node/agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"homepage": "https://github.com/mlx-node/mlx-node",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/mlx-node/mlx-node/issues"
|
|
@@ -21,16 +21,21 @@
|
|
|
21
21
|
".": {
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./catalog": {
|
|
26
|
+
"types": "./dist/catalog.d.ts",
|
|
27
|
+
"default": "./dist/catalog.js"
|
|
24
28
|
}
|
|
25
29
|
},
|
|
26
30
|
"scripts": {
|
|
27
31
|
"build": "tsc -b"
|
|
28
32
|
},
|
|
29
33
|
"dependencies": {
|
|
30
|
-
"@earendil-works/pi-ai": "0.
|
|
31
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
32
|
-
"@mlx-node/
|
|
33
|
-
"@mlx-node/
|
|
34
|
+
"@earendil-works/pi-ai": "0.81.1",
|
|
35
|
+
"@earendil-works/pi-coding-agent": "0.81.1",
|
|
36
|
+
"@mlx-node/core": "0.0.10",
|
|
37
|
+
"@mlx-node/lm": "0.0.10",
|
|
38
|
+
"@mlx-node/server": "0.0.10",
|
|
34
39
|
"typebox": "1.3.6"
|
|
35
40
|
},
|
|
36
41
|
"devDependencies": {
|