@mlx-node/agent 0.0.12 → 0.0.15

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 (61) hide show
  1. package/dist/catalog.d.ts +10 -1
  2. package/dist/catalog.d.ts.map +1 -1
  3. package/dist/catalog.js +11 -2
  4. package/dist/delegate.d.ts +29 -0
  5. package/dist/delegate.d.ts.map +1 -0
  6. package/dist/delegate.js +106 -0
  7. package/dist/extensions/delegation.d.ts +15 -0
  8. package/dist/extensions/delegation.d.ts.map +1 -0
  9. package/dist/extensions/delegation.js +93 -0
  10. package/dist/paths.d.ts +6 -0
  11. package/dist/paths.d.ts.map +1 -1
  12. package/dist/paths.js +16 -0
  13. package/dist/provider/chat-config.d.ts +6 -5
  14. package/dist/provider/chat-config.d.ts.map +1 -1
  15. package/dist/provider/chat-config.js +21 -7
  16. package/dist/provider/index.d.ts.map +1 -1
  17. package/dist/provider/index.js +8 -1
  18. package/dist/provider/model-host.d.ts +1 -1
  19. package/dist/provider/model-host.d.ts.map +1 -1
  20. package/dist/provider/model-host.js +25 -7
  21. package/dist/provider/models.d.ts +3 -14
  22. package/dist/provider/models.d.ts.map +1 -1
  23. package/dist/provider/models.js +17 -239
  24. package/dist/provider/stream-adapter.d.ts +2 -2
  25. package/dist/provider/stream-adapter.d.ts.map +1 -1
  26. package/dist/provider/stream-adapter.js +8 -5
  27. package/dist/run-agent.d.ts +4 -0
  28. package/dist/run-agent.d.ts.map +1 -1
  29. package/dist/run-agent.js +8 -2
  30. package/dist/types.d.ts +1 -1
  31. package/dist/types.d.ts.map +1 -1
  32. package/package.json +23 -5
  33. package/src/catalog.ts +194 -0
  34. package/src/cold-tier.ts +152 -0
  35. package/src/delegate.ts +136 -0
  36. package/src/extensions/approval-detail.ts +57 -0
  37. package/src/extensions/delegation.ts +109 -0
  38. package/src/extensions/local-image-input.ts +132 -0
  39. package/src/extensions/permission-gate.ts +347 -0
  40. package/src/extensions/subagent.ts +743 -0
  41. package/src/extensions/terminal-title.ts +53 -0
  42. package/src/extensions/trace-notice.ts +37 -0
  43. package/src/index.ts +23 -0
  44. package/src/paths.ts +36 -0
  45. package/src/provider/chat-config.ts +132 -0
  46. package/src/provider/convert-messages.ts +273 -0
  47. package/src/provider/error-coercion.ts +36 -0
  48. package/src/provider/events.ts +341 -0
  49. package/src/provider/index.ts +255 -0
  50. package/src/provider/metrics-trace.ts +380 -0
  51. package/src/provider/mlx-identity.ts +16 -0
  52. package/src/provider/model-host.ts +276 -0
  53. package/src/provider/model-registry-filter.ts +336 -0
  54. package/src/provider/models.ts +48 -0
  55. package/src/provider/performance-status.ts +112 -0
  56. package/src/provider/reasoning-tag-buffer.ts +67 -0
  57. package/src/provider/stream-adapter.ts +515 -0
  58. package/src/provider/tool-call-buffer.ts +82 -0
  59. package/src/provider/warm-reuse.ts +125 -0
  60. package/src/run-agent.ts +178 -0
  61. package/src/types.ts +10 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Agent-private `ChatSession` warm-reuse helper.
3
+ *
4
+ * Port of `packages/server/src/chat-session-warm-reuse.ts` — that module
5
+ * is deliberately kept off the server's export map, so the agent package
6
+ * carries its own copy with identical runtime behavior. Keep the two in
7
+ * sync.
8
+ *
9
+ * Why this helper exists at all: `ChatSession.reset()` is the safe
10
+ * public wipe — it always calls `model.resetCaches()` because the
11
+ * underlying `SessionCapableModel` may be shared across session
12
+ * lifetimes. The agent provider bridge, however, owns exactly one
13
+ * session per model process and replays pi's full message history on
14
+ * every LLM call, so the native KV cache always belongs to the chain
15
+ * being replayed. A JS-state-only reset that preserves the native
16
+ * cache is correct there: the next `primeHistory()` +
17
+ * `startFromHistoryStream()` lets the native prefix verifier recover
18
+ * the reused prefix and skip the corresponding re-prefill.
19
+ *
20
+ * Fields accessed: `inFlight`, `history`, `lastImagesKey`, `lastAudioKey`, `turnCount`,
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
29
+ * `packages/agent/__test__/warm-reuse.test.ts` checks every name in
30
+ * {@link WARM_REUSE_TOUCHED_FIELDS} against a real `ChatSession`
31
+ * instance.
32
+ */
33
+
34
+ import type { ChatConfig, ChatSession, SessionCapableModel } from '@mlx-node/lm';
35
+
36
+ /**
37
+ * Private structural view of the `ChatSession` JS-side state that the
38
+ * warm-reuse helper needs to wipe. Mirrors the internal state
39
+ * documented on `ChatSession` itself — field names are load-bearing:
40
+ * they must byte-match the concrete class's private fields or the
41
+ * cast-based mutation below silently no-ops.
42
+ */
43
+ interface ChatSessionWarmReuseInternals {
44
+ inFlight: boolean;
45
+ history: unknown[];
46
+ lastImagesKey: string | null;
47
+ lastAudioKey: string | null;
48
+ turnCount: number;
49
+ unresolvedOkToolCallCount: number | null;
50
+ needsFullReplay: boolean;
51
+ defaultConfig?: ChatConfig;
52
+ activeTools: ChatConfig['tools'];
53
+ }
54
+
55
+ /**
56
+ * `Record<keyof ..., true>` forces this map to list EXACTLY the fields of
57
+ * {@link ChatSessionWarmReuseInternals}: adding/removing/renaming a field
58
+ * in the interface without updating the map is a compile error, so the
59
+ * runtime list below can never drift from the fields the helper touches.
60
+ */
61
+ const WARM_REUSE_TOUCHED_FIELD_SET: Record<keyof ChatSessionWarmReuseInternals, true> = {
62
+ inFlight: true,
63
+ history: true,
64
+ lastImagesKey: true,
65
+ lastAudioKey: true,
66
+ turnCount: true,
67
+ unresolvedOkToolCallCount: true,
68
+ needsFullReplay: true,
69
+ defaultConfig: true,
70
+ activeTools: true,
71
+ };
72
+
73
+ /**
74
+ * Runtime list of the `ChatSession` private field names this module
75
+ * reads or writes — exported solely so the drift test can assert each
76
+ * one still exists on a real `ChatSession` instance.
77
+ */
78
+ export const WARM_REUSE_TOUCHED_FIELDS = Object.keys(WARM_REUSE_TOUCHED_FIELD_SET) as ReadonlyArray<
79
+ keyof ChatSessionWarmReuseInternals
80
+ >;
81
+
82
+ /**
83
+ * JS-state-only reset that DELIBERATELY preserves the underlying
84
+ * model's native KV cache and `cached_token_history`.
85
+ *
86
+ * @internal agent-private — used only by the provider bridge's
87
+ * per-call warm replay (`resetPreservingNativeCacheForWarmReuse` →
88
+ * `primeHistory` → `startFromHistoryStream`). Never export from this
89
+ * package's `index.ts`.
90
+ *
91
+ * Wipes ONLY the JS-side session state (history array, image key, turn
92
+ * counter, tool-call fan-out guard). With this function, the JS session
93
+ * is fresh enough for `ChatSession.primeHistory()` (which requires
94
+ * `turnCount === 0`) while the native prefix verifier can still recover
95
+ * the reused prefix on the next `chatSessionStart` and skip the
96
+ * corresponding re-prefill.
97
+ */
98
+ export async function resetPreservingNativeCacheForWarmReuse<M extends SessionCapableModel>(
99
+ session: ChatSession<M>,
100
+ ): Promise<void> {
101
+ // TypeScript `private` fields are only compile-time checks; at
102
+ // runtime they are ordinary properties. The cast through
103
+ // `ChatSessionWarmReuseInternals` preserves full static typing for
104
+ // this helper's mutations while bypassing the `private` gate — which
105
+ // is correct here because this helper is the designated agent-side
106
+ // friend accessor. The cast is funneled through `unknown` because TS
107
+ // correctly rejects a direct `ChatSession → Internals` cast when the
108
+ // concrete class has other non-internals fields.
109
+ const internals = session as unknown as ChatSessionWarmReuseInternals;
110
+ if (internals.inFlight) {
111
+ throw new Error(
112
+ 'ChatSession: cannot resetPreservingNativeCacheForWarmReuse() while a send() is in flight; await the previous call first',
113
+ );
114
+ }
115
+ internals.history = [];
116
+ internals.lastImagesKey = null;
117
+ internals.lastAudioKey = null;
118
+ internals.turnCount = 0;
119
+ internals.unresolvedOkToolCallCount = null;
120
+ internals.needsFullReplay = false;
121
+ // Tools are conversation state. A provider replay can switch to an
122
+ // unrelated history, so restore constructor defaults exactly like
123
+ // ChatSession.reset() instead of leaking the prior committed overlay.
124
+ internals.activeTools = internals.defaultConfig?.tools;
125
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * `runAgent` — the boot shell that hands control to pi's `main()` with
3
+ * the mlx provider, permission gate, local subagents, and terminal branding installed.
4
+ *
5
+ * Spike-proven boot contract:
6
+ * - The env vars below must be set BEFORE any runtime import of
7
+ * `@earendil-works/pi-coding-agent` (pi reads its config env at
8
+ * import/call time) — hence the dynamic import and the type-only
9
+ * top-level pi import here.
10
+ * - pi's `main()` RETURNS on the happy path but `process.exit()`s on
11
+ * help/error/package-command paths, so nothing critical may run after
12
+ * `await main()`.
13
+ * - In print/json mode pi takes over stdout (our writes are rerouted to
14
+ * stderr) and reads non-TTY stdin to EOF as prompt input — this
15
+ * wrapper must never consume or hold stdin. Paged-config temp cleanup in
16
+ * `finally` is best-effort for normal returns/rejections, never correctness-
17
+ * critical on pi's hard `process.exit()` paths.
18
+ */
19
+
20
+ import { homedir } from 'node:os';
21
+ import { join } from 'node:path';
22
+
23
+ import type { InlineExtension } from '@earendil-works/pi-coding-agent';
24
+ import { coldCacheDrain } from '@mlx-node/core';
25
+ import { PagedConfigOverrideManager } from '@mlx-node/lm';
26
+
27
+ import { createDelegationExtension } from './extensions/delegation.js';
28
+ import { createLocalImageInputExtension } from './extensions/local-image-input.js';
29
+ import { createPermissionGateExtension } from './extensions/permission-gate.js';
30
+ import { createSubagentExtension } from './extensions/subagent.js';
31
+ import { createTerminalTitleExtension } from './extensions/terminal-title.js';
32
+ import { createTraceNoticeExtension } from './extensions/trace-notice.js';
33
+ import { createMlxProviderExtension } from './provider/index.js';
34
+ import { MlxModelHost } from './provider/model-host.js';
35
+ import {
36
+ type FilterableModelRuntimeConstructor,
37
+ installMlxOnlyModelRegistryFilter,
38
+ } from './provider/model-registry-filter.js';
39
+ import type { MlxModelInfo } from './provider/models.js';
40
+
41
+ /** Shape of pi's `main(argv, { extensionFactories })` — also the test seam. */
42
+ export type RunAgentMain = (args: string[], opts: { extensionFactories: InlineExtension[] }) => Promise<void>;
43
+
44
+ export interface RunAgentPi {
45
+ main: RunAgentMain;
46
+ ModelRuntime: FilterableModelRuntimeConstructor;
47
+ }
48
+
49
+ /** @internal Narrow lifecycle seam for the agent's paged config overlays. */
50
+ export interface AgentPagedConfigOverrides {
51
+ resolve(modelPath: string, modelType?: string, persistPagedCache?: boolean): Promise<string>;
52
+ cleanup(): Promise<void>;
53
+ }
54
+
55
+ export interface RunAgentOptions {
56
+ /** Focused worker tools/permissions; inference and session defaults remain shared. */
57
+ mode?: 'delegate';
58
+ /** Explicit approval supplied by the outer caller, captured before the worker starts. */
59
+ delegateCallerApproved?: boolean;
60
+ /** Resolved models directory (context for callers/diagnostics — discovery already ran). */
61
+ modelsDir: string;
62
+ /** Discovered models to serve through the in-process `mlx` provider. */
63
+ models: MlxModelInfo[];
64
+ /** Passthrough args handed to pi's `main()` verbatim. */
65
+ argv: string[];
66
+ /** Native inference-log path to surface after Pi takes over the TUI. */
67
+ traceLogFile?: string;
68
+ /**
69
+ * Enable the SSD cold tier by default (the agent's default; the CLI sets it
70
+ * false for `--no-persist-cache`). Forwarded to {@link MlxModelHost}, which
71
+ * applies this ONE value to every load whose family is in
72
+ * `COLD_TIER_RESTORE_FAMILIES` — not to qwen3 alone. Families off that list
73
+ * are handed no policy because they can never persist, not because this flag
74
+ * spares them. `undefined` keeps the host's on-by-default behavior.
75
+ */
76
+ persistPagedCache?: boolean;
77
+ /** @internal Test seam; when set, the pi dynamic import is skipped entirely. */
78
+ piImpl?: RunAgentPi;
79
+ /** @internal Test seam for paged model-path resolution and cleanup. */
80
+ pagedConfigOverrides?: AgentPagedConfigOverrides;
81
+ }
82
+
83
+ /** @internal Exact opt-in parser kept separate so non-`1` values stay disabled. */
84
+ export function agentGemmaDraftEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
85
+ return env.MLX_AGENT_ENABLE_GEMMA_DRAFT === '1';
86
+ }
87
+
88
+ /**
89
+ * Seed the pi/mlx environment (never clobbering user-set values) and run
90
+ * pi's `main()` with the mlx inline extensions. May not return: pi
91
+ * `process.exit()`s on help/error paths.
92
+ *
93
+ * This is a dedicated-process entrypoint: pi owns stdin/stdout, signal
94
+ * handlers, and process exit while `main()` runs. Do not run it concurrently
95
+ * with another pi SDK/runtime in the same process. The temporary registry
96
+ * policy is restored when `main()` returns or rejects.
97
+ */
98
+ export async function runAgent(opts: RunAgentOptions): Promise<void> {
99
+ process.env.PI_CODING_AGENT_DIR ??= join(homedir(), '.mlx-node', 'agent');
100
+ process.env.PI_SKIP_VERSION_CHECK ??= '1';
101
+ // Mirrors `mlx launch claude`: chunked paged prefill keeps long-prompt
102
+ // TTFT bounded on the default paged path.
103
+ process.env.MLX_PAGED_PREFILL_CHUNK_SIZE ??= '2048';
104
+ // Hard offline invariant — NOT a user-overridable default, hence `=` not `??=`.
105
+ // `mlx agent` is local-only: no cloud provider may ever be contacted. pi 0.81.1's
106
+ // interactive and RPC startup call `ModelRuntime.refresh()`, which when PI_OFFLINE
107
+ // is unset fetches remote provider catalogs from pi.dev and can refresh a persisted
108
+ // cloud credential — a network path the mlx-only prototype filter does NOT cover
109
+ // (it patches the read methods, not `refresh`). Forcing PI_OFFLINE=1 here, before pi
110
+ // is imported below, also pins every ModelRuntime in this process to `allowNetwork`
111
+ // off, so no ambient/prior cloud credential can leak outbound traffic.
112
+ process.env.PI_OFFLINE = '1';
113
+
114
+ // Force every paged-capable agent family through an isolated config clone.
115
+ // This includes quantized LFM2 (whose standalone default is deliberately
116
+ // flat) and Qwen3.5 dense/MoE (whose text-only defaults are flat). Gemma4's
117
+ // paged overlay intentionally hides an embedded draft/ directory: the
118
+ // current speculative executor is flat-cache-only and can regress quantized
119
+ // agent workloads. Users may explicitly opt back into that native behavior.
120
+ const preserveEmbeddedGemmaDraft = agentGemmaDraftEnabled();
121
+ const pagedConfigOverrides =
122
+ opts.pagedConfigOverrides ?? new PagedConfigOverrideManager({ preserveEmbeddedGemmaDraft });
123
+ const modelHost = new MlxModelHost(
124
+ opts.models.map((model) => model.discovered),
125
+ {
126
+ resolveModelPathFn: (model, policy) =>
127
+ pagedConfigOverrides.resolve(model.path, model.modelType, policy?.persistPagedCache),
128
+ requirePagedCache: true,
129
+ persistPagedCache: opts.persistPagedCache,
130
+ },
131
+ );
132
+
133
+ // Keep the pi import strictly behind the seam. The seam carries BOTH main and
134
+ // the ModelRuntime class, so tests and production exercise the same policy
135
+ // installation/lifecycle instead of being able to bypass it accidentally. The
136
+ // filter patches the runtime prototype (not the extension-only ModelRegistry
137
+ // facade), which is where the selector / listing / resolution paths read.
138
+ const pi: RunAgentPi = opts.piImpl ?? (await import('@earendil-works/pi-coding-agent'));
139
+ const restoreModelRegistry = installMlxOnlyModelRegistryFilter(
140
+ pi.ModelRuntime,
141
+ opts.models.map((model) => model.discovered.name),
142
+ );
143
+ const subagentsEnabled =
144
+ opts.mode !== 'delegate' &&
145
+ opts.models.length > 0 &&
146
+ !opts.argv.includes('--no-extensions') &&
147
+ !opts.argv.includes('-ne');
148
+ try {
149
+ await pi.main(opts.argv, {
150
+ extensionFactories: [
151
+ createMlxProviderExtension(opts.models, modelHost),
152
+ createLocalImageInputExtension(),
153
+ opts.mode === 'delegate'
154
+ ? createDelegationExtension({ callerApproved: opts.delegateCallerApproved })
155
+ : createPermissionGateExtension(),
156
+ ...(subagentsEnabled ? [createSubagentExtension()] : []),
157
+ ...(opts.traceLogFile !== undefined ? [createTraceNoticeExtension(opts.traceLogFile)] : []),
158
+ createTerminalTitleExtension(),
159
+ ],
160
+ });
161
+ } finally {
162
+ try {
163
+ restoreModelRegistry();
164
+ } finally {
165
+ await pagedConfigOverrides.cleanup();
166
+ // `mlx agent -p` is one-shot: flush any accepted cold-tier prefix blocks
167
+ // to disk before the process exits, otherwise a prompt's just-persisted
168
+ // KV could still be queued/mid-write when we return. No-op when the tier
169
+ // was never opened, bounded so a stuck fsync can't hang exit, and never
170
+ // allowed to throw out of cleanup (best-effort durability).
171
+ try {
172
+ coldCacheDrain(5000);
173
+ } catch {
174
+ // Best-effort: a drain failure must never mask the real exit path.
175
+ }
176
+ }
177
+ }
178
+ }
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { ModelType } from '@mlx-node/lm';
2
+
3
+ /** Structural mirror of the CLI's DiscoveredModel — avoids a cli↔agent dependency cycle. */
4
+ export interface DiscoveredModelLike {
5
+ name: string;
6
+ path: string;
7
+ modelType: ModelType;
8
+ /** External speculative drafter. When supplied, forwarded unchanged for loader validation. */
9
+ draftModelPath?: string;
10
+ }