@mlx-node/agent 0.0.7 → 0.0.9

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/events.d.ts +9 -0
  17. package/dist/provider/events.d.ts.map +1 -1
  18. package/dist/provider/events.js +15 -0
  19. package/dist/provider/index.d.ts +12 -1
  20. package/dist/provider/index.d.ts.map +1 -1
  21. package/dist/provider/index.js +153 -7
  22. package/dist/provider/metrics-trace.d.ts +274 -0
  23. package/dist/provider/metrics-trace.d.ts.map +1 -0
  24. package/dist/provider/metrics-trace.js +174 -0
  25. package/dist/provider/mlx-identity.d.ts +16 -0
  26. package/dist/provider/mlx-identity.d.ts.map +1 -0
  27. package/dist/provider/mlx-identity.js +15 -0
  28. package/dist/provider/model-host.d.ts +45 -3
  29. package/dist/provider/model-host.d.ts.map +1 -1
  30. package/dist/provider/model-host.js +34 -2
  31. package/dist/provider/model-registry-filter.d.ts +74 -18
  32. package/dist/provider/model-registry-filter.d.ts.map +1 -1
  33. package/dist/provider/model-registry-filter.js +229 -38
  34. package/dist/provider/models.d.ts +2 -3
  35. package/dist/provider/models.d.ts.map +1 -1
  36. package/dist/provider/models.js +46 -20
  37. package/dist/provider/stream-adapter.d.ts +37 -4
  38. package/dist/provider/stream-adapter.d.ts.map +1 -1
  39. package/dist/provider/stream-adapter.js +58 -3
  40. package/dist/provider/warm-reuse.d.ts +11 -8
  41. package/dist/provider/warm-reuse.d.ts.map +1 -1
  42. package/dist/provider/warm-reuse.js +14 -7
  43. package/dist/run-agent.d.ts +12 -3
  44. package/dist/run-agent.d.ts.map +1 -1
  45. package/dist/run-agent.js +31 -5
  46. package/package.json +10 -5
  47. package/dist/provider/inference-trace.d.ts +0 -58
  48. package/dist/provider/inference-trace.d.ts.map +0 -1
  49. package/dist/provider/inference-trace.js +0 -205
@@ -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`. These are TypeScript `private` fields on
22
- * `ChatSession` (compile-time only) — at runtime they are ordinary
23
- * properties. The cast through {@link ChatSessionWarmReuseInternals}
24
- * gives this helper a typed view of the instance without relaxing the
25
- * class's `private` declarations. The field names MUST stay in sync
26
- * with `packages/lm/src/chat-session.ts`; a mismatch would silently
27
- * skip the intended state wipe — the drift test in
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAErE;;;;;;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;CAC1B;AAkBD;;;;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,CAqBf"}
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`. These are TypeScript `private` fields on
22
- * `ChatSession` (compile-time only) — at runtime they are ordinary
23
- * properties. The cast through {@link ChatSessionWarmReuseInternals}
24
- * gives this helper a typed view of the instance without relaxing the
25
- * class's `private` declarations. The field names MUST stay in sync
26
- * with `packages/lm/src/chat-session.ts`; a mismatch would silently
27
- * skip the intended state wipe — the drift test in
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
  }
@@ -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 FilterableModelRegistryConstructor } from './provider/model-registry-filter.js';
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
- ModelRegistry: FilterableModelRegistryConstructor;
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. */
@@ -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;AASvE,OAAO,EACL,KAAK,kCAAkC,EAExC,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,aAAa,EAAE,kCAAkC,CAAC;CACnD;AAED,6EAA6E;AAC7E,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,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,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,CAmDnE"}
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
- // and its registry class, so tests and production exercise the same policy
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.ModelRegistry, opts.models.map((model) => model.discovered.name));
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.7",
3
+ "version": "0.0.9",
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.80.6",
31
- "@earendil-works/pi-coding-agent": "0.80.6",
32
- "@mlx-node/lm": "0.0.7",
33
- "@mlx-node/server": "0.0.7",
34
+ "@earendil-works/pi-ai": "0.81.1",
35
+ "@earendil-works/pi-coding-agent": "0.81.1",
36
+ "@mlx-node/core": "0.0.9",
37
+ "@mlx-node/lm": "0.0.9",
38
+ "@mlx-node/server": "0.0.9",
34
39
  "typebox": "1.3.6"
35
40
  },
36
41
  "devDependencies": {
@@ -1,58 +0,0 @@
1
- /**
2
- * Privacy-safe, best-effort inference tracing for the in-process mlx agent.
3
- *
4
- * The trace is deliberately metadata-only. Callers can only supply fields from
5
- * the allowlist below; prompt/output text, tool payloads, paths, token ids, and
6
- * error messages have no serialization path. Tracing is enabled only when
7
- * `MLX_INFERENCE_TRACE` is explicitly truthy and
8
- * `MLX_INFERENCE_TRACE_FILE` is non-empty.
9
- *
10
- * Writes are synchronous because this is a bounded, high-level lifecycle trace
11
- * (roughly a dozen records per request), not a token/layer trace. Every write is
12
- * swallowed on failure so observability can never change inference behavior.
13
- */
14
- export type InferenceTraceEvent = 'request.created' | 'host.queue.enter' | 'host.start' | 'host.resident' | 'host.load.start' | 'host.load.end' | 'host.callback.end' | 'session.reset' | 'session.prime' | 'session.config' | 'native.stream.start' | 'native.stream.first_delta' | 'request.terminal';
15
- export type InferenceTraceStatus = 'ok' | 'error' | 'aborted';
16
- export interface InferenceTraceFields {
17
- status?: InferenceTraceStatus;
18
- resident?: 'hit' | 'load' | 'swap';
19
- resetMode?: 'preserve' | 'full';
20
- finishReason?: 'stop' | 'length' | 'cancelled' | 'error' | 'other';
21
- reasoningEffort?: 'none' | 'low' | 'medium' | 'high';
22
- modelType?: 'gemma4' | 'qwen3' | 'qwen3_5' | 'qwen3_5_moe' | 'lfm2' | 'lfm2_moe' | 'other';
23
- streaming?: boolean;
24
- reportPerformance?: boolean;
25
- isReasoning?: boolean;
26
- durationMs?: number;
27
- queueMs?: number;
28
- messageCount?: number;
29
- toolCount?: number;
30
- maxNewTokens?: number;
31
- promptTokens?: number;
32
- cachedTokens?: number;
33
- generatedTokens?: number;
34
- reasoningTokens?: number;
35
- ttftMs?: number;
36
- prefillTokensPerSecond?: number;
37
- decodeTokensPerSecond?: number;
38
- mtpMeanAcceptedTokens?: number;
39
- mtpMeanAcceptedTokensTotal?: number;
40
- mtpCycles?: number;
41
- mtpMeanDepth?: number;
42
- mtpAcceptanceByPosition?: readonly number[];
43
- }
44
- export interface InferenceTraceSink {
45
- write(line: string): void;
46
- }
47
- export declare class InferenceTrace {
48
- private readonly sink;
49
- readonly requestId: string;
50
- private terminalWritten;
51
- constructor(sink?: InferenceTraceSink | undefined);
52
- emit(event: InferenceTraceEvent, fields?: InferenceTraceFields): void;
53
- terminal(status: InferenceTraceStatus, fields?: InferenceTraceFields): void;
54
- }
55
- export declare function createInferenceTrace(): InferenceTrace;
56
- export declare function classifyFinishReason(value: string): NonNullable<InferenceTraceFields['finishReason']>;
57
- export declare function classifyModelType(value: string): NonNullable<InferenceTraceFields['modelType']>;
58
- //# sourceMappingURL=inference-trace.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"inference-trace.d.ts","sourceRoot":"","sources":["../../src/provider/inference-trace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,MAAM,MAAM,mBAAmB,GAC3B,iBAAiB,GACjB,kBAAkB,GAClB,YAAY,GACZ,eAAe,GACf,iBAAiB,GACjB,eAAe,GACf,mBAAmB,GACnB,eAAe,GACf,eAAe,GACf,gBAAgB,GAChB,qBAAqB,GACrB,2BAA2B,GAC3B,kBAAkB,CAAC;AAEvB,MAAM,MAAM,oBAAoB,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,CAAC;AAE9D,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,QAAQ,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;IACnC,SAAS,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,CAAC;IACnE,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrD,SAAS,CAAC,EACN,QAAQ,GACR,OAAO,GACP,SAAS,GACT,aAAa,GACb,MAAM,GACN,UAAU,GACV,OAAO,CAAC;IACZ,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AA+ID,qBAAa,cAAc;IAIb,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,QAAQ,CAAC,SAAS,SAAmB;IACrC,OAAO,CAAC,eAAe,CAAS;IAEhC,YAA6B,IAAI,GAAE,kBAAkB,GAAG,SAAyB,EAAI;IAErF,IAAI,CAAC,KAAK,EAAE,mBAAmB,EAAE,MAAM,GAAE,oBAAyB,GAAG,IAAI,CAiBxE;IAED,QAAQ,CAAC,MAAM,EAAE,oBAAoB,EAAE,MAAM,GAAE,oBAAyB,GAAG,IAAI,CAI9E;CACF;AAED,wBAAgB,oBAAoB,IAAI,cAAc,CAErD;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,oBAAoB,CAAC,cAAc,CAAC,CAAC,CAUrG;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,oBAAoB,CAAC,WAAW,CAAC,CAAC,CAY/F"}
@@ -1,205 +0,0 @@
1
- /**
2
- * Privacy-safe, best-effort inference tracing for the in-process mlx agent.
3
- *
4
- * The trace is deliberately metadata-only. Callers can only supply fields from
5
- * the allowlist below; prompt/output text, tool payloads, paths, token ids, and
6
- * error messages have no serialization path. Tracing is enabled only when
7
- * `MLX_INFERENCE_TRACE` is explicitly truthy and
8
- * `MLX_INFERENCE_TRACE_FILE` is non-empty.
9
- *
10
- * Writes are synchronous because this is a bounded, high-level lifecycle trace
11
- * (roughly a dozen records per request), not a token/layer trace. Every write is
12
- * swallowed on failure so observability can never change inference behavior.
13
- */
14
- import { appendFileSync } from 'node:fs';
15
- const EVENTS = new Set([
16
- 'request.created',
17
- 'host.queue.enter',
18
- 'host.start',
19
- 'host.resident',
20
- 'host.load.start',
21
- 'host.load.end',
22
- 'host.callback.end',
23
- 'session.reset',
24
- 'session.prime',
25
- 'session.config',
26
- 'native.stream.start',
27
- 'native.stream.first_delta',
28
- 'request.terminal',
29
- ]);
30
- const STATUS = new Set(['ok', 'error', 'aborted']);
31
- const RESIDENT = new Set(['hit', 'load', 'swap']);
32
- const RESET_MODE = new Set(['preserve', 'full']);
33
- const FINISH_REASON = new Set([
34
- 'stop',
35
- 'length',
36
- 'cancelled',
37
- 'error',
38
- 'other',
39
- ]);
40
- const REASONING_EFFORT = new Set([
41
- 'none',
42
- 'low',
43
- 'medium',
44
- 'high',
45
- ]);
46
- const MODEL_TYPE = new Set([
47
- 'gemma4',
48
- 'qwen3',
49
- 'qwen3_5',
50
- 'qwen3_5_moe',
51
- 'lfm2',
52
- 'lfm2_moe',
53
- 'other',
54
- ]);
55
- let requestSequence = 0;
56
- let eventSequence = 0;
57
- function envFlagEnabled(value) {
58
- if (value == null)
59
- return false;
60
- return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
61
- }
62
- function defaultSink() {
63
- if (!envFlagEnabled(process.env.MLX_INFERENCE_TRACE))
64
- return undefined;
65
- const path = process.env.MLX_INFERENCE_TRACE_FILE?.trim();
66
- if (!path)
67
- return undefined;
68
- return {
69
- write(line) {
70
- appendFileSync(path, line, { encoding: 'utf8', flag: 'a', mode: 0o600 });
71
- },
72
- };
73
- }
74
- function nextRequestId() {
75
- requestSequence += 1;
76
- return `agent-${process.pid.toString(36)}-${requestSequence.toString(36)}`;
77
- }
78
- function nonNegativeFinite(value) {
79
- return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
80
- }
81
- function nonNegativeInteger(value) {
82
- const n = nonNegativeFinite(value);
83
- return n !== undefined && Number.isSafeInteger(n) ? n : undefined;
84
- }
85
- function copyNumber(out, fields, key, integer) {
86
- const value = integer ? nonNegativeInteger(fields[key]) : nonNegativeFinite(fields[key]);
87
- if (value !== undefined)
88
- out[key] = value;
89
- }
90
- /** Runtime allowlist: protects the file even if an untyped caller supplies extras. */
91
- function sanitizeFields(fields) {
92
- const out = {};
93
- if (fields.status !== undefined && STATUS.has(fields.status))
94
- out.status = fields.status;
95
- if (fields.resident !== undefined && RESIDENT.has(fields.resident))
96
- out.resident = fields.resident;
97
- if (fields.resetMode !== undefined && RESET_MODE.has(fields.resetMode))
98
- out.resetMode = fields.resetMode;
99
- if (fields.finishReason !== undefined && FINISH_REASON.has(fields.finishReason)) {
100
- out.finishReason = fields.finishReason;
101
- }
102
- if (fields.reasoningEffort !== undefined && REASONING_EFFORT.has(fields.reasoningEffort)) {
103
- out.reasoningEffort = fields.reasoningEffort;
104
- }
105
- if (fields.modelType !== undefined && MODEL_TYPE.has(fields.modelType))
106
- out.modelType = fields.modelType;
107
- for (const key of ['streaming', 'reportPerformance', 'isReasoning']) {
108
- if (typeof fields[key] === 'boolean')
109
- out[key] = fields[key];
110
- }
111
- for (const key of [
112
- 'durationMs',
113
- 'queueMs',
114
- 'ttftMs',
115
- 'prefillTokensPerSecond',
116
- 'decodeTokensPerSecond',
117
- 'mtpMeanAcceptedTokens',
118
- 'mtpMeanAcceptedTokensTotal',
119
- 'mtpMeanDepth',
120
- ]) {
121
- copyNumber(out, fields, key, false);
122
- }
123
- for (const key of [
124
- 'messageCount',
125
- 'toolCount',
126
- 'maxNewTokens',
127
- 'promptTokens',
128
- 'cachedTokens',
129
- 'generatedTokens',
130
- 'reasoningTokens',
131
- 'mtpCycles',
132
- ]) {
133
- copyNumber(out, fields, key, true);
134
- }
135
- if (Array.isArray(fields.mtpAcceptanceByPosition)) {
136
- const values = fields.mtpAcceptanceByPosition
137
- .slice(0, 16)
138
- .map(nonNegativeFinite)
139
- .filter((value) => value !== undefined);
140
- if (values.length > 0)
141
- out.mtpAcceptanceByPosition = values;
142
- }
143
- return out;
144
- }
145
- export class InferenceTrace {
146
- sink;
147
- requestId = nextRequestId();
148
- terminalWritten = false;
149
- constructor(sink = defaultSink()) {
150
- this.sink = sink;
151
- }
152
- emit(event, fields = {}) {
153
- if (!this.sink || !EVENTS.has(event))
154
- return;
155
- try {
156
- eventSequence += 1;
157
- const record = {
158
- timestamp: new Date().toISOString(),
159
- pid: process.pid,
160
- seq: eventSequence,
161
- requestId: this.requestId,
162
- component: 'agent',
163
- event,
164
- ...sanitizeFields(fields),
165
- };
166
- this.sink.write(`${JSON.stringify(record)}\n`);
167
- }
168
- catch {
169
- // Diagnostics are best-effort and must never affect inference.
170
- }
171
- }
172
- terminal(status, fields = {}) {
173
- if (this.terminalWritten)
174
- return;
175
- this.terminalWritten = true;
176
- this.emit('request.terminal', { ...fields, status });
177
- }
178
- }
179
- export function createInferenceTrace() {
180
- return new InferenceTrace();
181
- }
182
- export function classifyFinishReason(value) {
183
- switch (value) {
184
- case 'stop':
185
- case 'length':
186
- case 'cancelled':
187
- case 'error':
188
- return value;
189
- default:
190
- return 'other';
191
- }
192
- }
193
- export function classifyModelType(value) {
194
- switch (value) {
195
- case 'gemma4':
196
- case 'qwen3':
197
- case 'qwen3_5':
198
- case 'qwen3_5_moe':
199
- case 'lfm2':
200
- case 'lfm2_moe':
201
- return value;
202
- default:
203
- return 'other';
204
- }
205
- }