@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
package/dist/catalog.d.ts
CHANGED
|
@@ -23,4 +23,19 @@ export interface CatalogEntry {
|
|
|
23
23
|
export declare const MODEL_CATALOG: readonly CatalogEntry[];
|
|
24
24
|
/** Catalog entries the wizard offers (hidden entries filtered out). */
|
|
25
25
|
export declare function visibleCatalog(): CatalogEntry[];
|
|
26
|
+
/**
|
|
27
|
+
* Cold-tier facts, re-exported through this subpath.
|
|
28
|
+
*
|
|
29
|
+
* `@mlx-node/agent/catalog` is the agent package's one NATIVE-FREE entry point:
|
|
30
|
+
* the package root re-exports `provider/index.ts`, which value-imports
|
|
31
|
+
* `@mlx-node/core`. The dashboard is a separate viewer process that must never
|
|
32
|
+
* link the addon (docs/dashboard.md: "no Metal init, instant start"), and
|
|
33
|
+
* `mlx agent --help` must print without loading weights — so both reach the
|
|
34
|
+
* cold-tier allowlist and the cache-root canonicalizer through here.
|
|
35
|
+
*
|
|
36
|
+
* These are RE-EXPORTS. The definitions live in `./cold-tier.ts` and there is
|
|
37
|
+
* exactly one of each; `packages/agent/__test__/cold-tier-families.test.ts`
|
|
38
|
+
* guards the allowlist against the native side.
|
|
39
|
+
*/
|
|
40
|
+
export { COLD_TIER_RESTORE_FAMILIES, canonicalCacheRoot, coldTierRestoreFamilyList } from './cold-tier.js';
|
|
26
41
|
//# sourceMappingURL=catalog.d.ts.map
|
package/dist/catalog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,aAAa,EAAE,SAAS,YAAY,EA+BhD,CAAC;AAEF,uEAAuE;AACvE,wBAAgB,cAAc,IAAI,YAAY,EAAE,CAE/C"}
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,aAAa,EAAE,SAAS,YAAY,EA+BhD,CAAC;AAEF,uEAAuE;AACvE,wBAAgB,cAAc,IAAI,YAAY,EAAE,CAE/C;AAED;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/catalog.js
CHANGED
|
@@ -42,3 +42,18 @@ export const MODEL_CATALOG = [
|
|
|
42
42
|
export function visibleCatalog() {
|
|
43
43
|
return MODEL_CATALOG.filter((entry) => !entry.hidden);
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Cold-tier facts, re-exported through this subpath.
|
|
47
|
+
*
|
|
48
|
+
* `@mlx-node/agent/catalog` is the agent package's one NATIVE-FREE entry point:
|
|
49
|
+
* the package root re-exports `provider/index.ts`, which value-imports
|
|
50
|
+
* `@mlx-node/core`. The dashboard is a separate viewer process that must never
|
|
51
|
+
* link the addon (docs/dashboard.md: "no Metal init, instant start"), and
|
|
52
|
+
* `mlx agent --help` must print without loading weights — so both reach the
|
|
53
|
+
* cold-tier allowlist and the cache-root canonicalizer through here.
|
|
54
|
+
*
|
|
55
|
+
* These are RE-EXPORTS. The definitions live in `./cold-tier.ts` and there is
|
|
56
|
+
* exactly one of each; `packages/agent/__test__/cold-tier-families.test.ts`
|
|
57
|
+
* guards the allowlist against the native side.
|
|
58
|
+
*/
|
|
59
|
+
export { COLD_TIER_RESTORE_FAMILIES, canonicalCacheRoot, coldTierRestoreFamilyList } from './cold-tier.js';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cold-tier facts shared by the agent (writer), the CLI (help text) and the
|
|
3
|
+
* dashboard (reader), kept in a NATIVE-FREE leaf module.
|
|
4
|
+
*
|
|
5
|
+
* `provider/model-host.ts` value-imports `@mlx-node/lm`, which loads the native
|
|
6
|
+
* addon; the dashboard is a separate viewer process that must never link it
|
|
7
|
+
* ("no Metal init, instant start" — docs/dashboard.md), and `mlx agent --help`
|
|
8
|
+
* must print without loading weights. This module therefore has NO runtime
|
|
9
|
+
* imports beyond `node:` builtins, and reaches those consumers through the
|
|
10
|
+
* existing native-free `@mlx-node/agent/catalog` subpath, which re-exports it.
|
|
11
|
+
* There is deliberately no `./cold-tier` entry in `package.json`: a second
|
|
12
|
+
* published subpath would be a second thing to keep native-free, and `catalog`
|
|
13
|
+
* already carries that guarantee.
|
|
14
|
+
*
|
|
15
|
+
* Everything here is a single source of truth. Do not copy the family list or
|
|
16
|
+
* re-implement the root canonicalization at a call site; import them.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Model families whose paged prefix blocks can be restored from the SSD cold
|
|
20
|
+
* tier soundly. The single source of truth on the TypeScript side, mirroring
|
|
21
|
+
* `COLD_RESTORE_FAMILIES` in `crates/mlx-core/src/cold_tier.rs`; the two gate
|
|
22
|
+
* the same decision from opposite ends, and
|
|
23
|
+
* `packages/agent/__test__/cold-tier-families.test.ts` asserts they never
|
|
24
|
+
* drift.
|
|
25
|
+
*
|
|
26
|
+
* A family belongs here only when EVERY piece of per-token state its forward
|
|
27
|
+
* pass carries between turns is reconstructible from the tier — either because
|
|
28
|
+
* it all lives inside the paged pool, or because the part that does not is
|
|
29
|
+
* persisted alongside as a cold-tier sidecar and the restore reconciles down to
|
|
30
|
+
* a boundary that sidecar actually backs:
|
|
31
|
+
*
|
|
32
|
+
* - `qwen3` (dense) sizes its pool over all layers, so the pool holds the
|
|
33
|
+
* complete KV for the prefix and needs no sidecar.
|
|
34
|
+
* - `gemma4` sizes its pool over the full-attention layers only, but persists
|
|
35
|
+
* its out-of-pool sliding-window `RotatingKVCache` state as a
|
|
36
|
+
* `ColdGroup::SlidingWindow` sidecar, and its `ColdSidecarPolicy` makes the
|
|
37
|
+
* native restore walk refuse any boundary a validated sidecar does not back.
|
|
38
|
+
* - `qwen3_5` (dense) sizes its pool over the full-attention layers only, but
|
|
39
|
+
* persists its out-of-pool GDN recurrent state (conv + recurrent) as a
|
|
40
|
+
* `ColdGroup::GdnState` sidecar, and its `ColdSidecarPolicy` reconciles the
|
|
41
|
+
* native restore down to the deepest block-aligned boundary a validated
|
|
42
|
+
* sidecar backs (a recurrent state is valid only at its exact prefix length).
|
|
43
|
+
* - `qwen3_5_moe` keeps the SAME GDN recurrent state outside the pool — same
|
|
44
|
+
* shapes, same dtype, same layer mapping — so it shares the dense family's
|
|
45
|
+
* sidecar codec and `ColdSidecarPolicy` verbatim. Its restart-parity gate has
|
|
46
|
+
* since been run on `Qwen3.6-35b-a3b-UD-Q2_K_XL-mlx`: the restore reconciled
|
|
47
|
+
* onto ladder rung 304 of `[16, 64, 304, 1248]` with `hits=42` and
|
|
48
|
+
* `corruptions=0`, and the text matched a no-persist baseline.
|
|
49
|
+
* - `lfm2` / `lfm2_moe` keep short-conv state outside the pool with no
|
|
50
|
+
* serialization path for it at all, AND drive the uniform native adapter
|
|
51
|
+
* API whose restore branch is already wired to the tier — asking for
|
|
52
|
+
* persistence on their behalf would restore an incomplete prefix silently.
|
|
53
|
+
*
|
|
54
|
+
* Widening this set is a correctness decision, never a perf one, and it is
|
|
55
|
+
* authorized by exactly one thing: the family's native restart-parity gate
|
|
56
|
+
* passing on real weights with `hits > 0` and `corruptions == 0`.
|
|
57
|
+
*
|
|
58
|
+
* `mlx agent --no-persist-cache` applies to EVERY family in this set, not to
|
|
59
|
+
* one of them: it is a SINGLE process-wide boolean handed to every load whose
|
|
60
|
+
* family is on the allowlist (`MlxModelHost.runWithResident` →
|
|
61
|
+
* `resolveModelPathFn(entry, { persistPagedCache })`). Families off the
|
|
62
|
+
* allowlist are handed no policy at all — not because the flag spares them, but
|
|
63
|
+
* because `cold_tier::resolve_persist_cold` refuses to persist them under ANY
|
|
64
|
+
* setting. The help text says so in exactly one place, built from
|
|
65
|
+
* {@link coldTierRestoreFamilyList} in `packages/cli/src/commands/agent/index.ts`
|
|
66
|
+
* and asserted by `packages/cli/__test__/agent-cmd.test.ts`.
|
|
67
|
+
*/
|
|
68
|
+
export declare const COLD_TIER_RESTORE_FAMILIES: ReadonlySet<string>;
|
|
69
|
+
/** Allowlisted families in a stable, human-facing order (help text, API payload). */
|
|
70
|
+
export declare function coldTierRestoreFamilyList(): string[];
|
|
71
|
+
/**
|
|
72
|
+
* Canonical identity for a cold-cache root, used as the JOIN KEY between the
|
|
73
|
+
* agent that wrote a trace and the dashboard that reads it back.
|
|
74
|
+
*
|
|
75
|
+
* The two live in DIFFERENT PROCESSES resolving the root from their own
|
|
76
|
+
* environments: the native tier reports `manager.root().display()` (the path as
|
|
77
|
+
* constructed, never realpath'd) while the dashboard builds its own from
|
|
78
|
+
* `MLX_COLD_CACHE_DIR` or the `~/.mlx-node` default. On macOS `/tmp` is a
|
|
79
|
+
* symlink to `/private/tmp`, so two identical-looking spellings canonicalize
|
|
80
|
+
* differently — a raw string compare is a silent-zero trap, not a loud failure.
|
|
81
|
+
* Both sides MUST route through this one function.
|
|
82
|
+
*
|
|
83
|
+
* Resolution order: expand `~`, make absolute, then `realpath` the DEEPEST
|
|
84
|
+
* EXISTING ANCESTOR and re-attach every missing segment lexically. A tier that
|
|
85
|
+
* has never been opened has no root directory — that is the normal state, not
|
|
86
|
+
* an edge case — and when `MLX_COLD_CACHE_DIR` itself has never been created
|
|
87
|
+
* the parent is missing too. Stopping after one level returned a wholly
|
|
88
|
+
* UNcanonicalized path, so under a symlinked ancestor (`/tmp` → `/private/tmp`
|
|
89
|
+
* on macOS) the reader's key diverged from the writer's and rows from the SAME
|
|
90
|
+
* cache were reported as "a different cache directory". Only a path with no
|
|
91
|
+
* resolvable ancestor at all falls back to the lexical absolute form; this is
|
|
92
|
+
* an identity key, not a security boundary, so it fails OPEN to a stable string
|
|
93
|
+
* rather than to `undefined`.
|
|
94
|
+
*
|
|
95
|
+
* An empty input stays empty: the native struct reports `root: ''` while the
|
|
96
|
+
* tier is disabled, and that must never be recorded as a real cache identity.
|
|
97
|
+
*/
|
|
98
|
+
export declare function canonicalCacheRoot(root: string): string;
|
|
99
|
+
//# sourceMappingURL=cold-tier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cold-tier.d.ts","sourceRoot":"","sources":["../src/cold-tier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAKzD,CAAC;AAEH,qFAAqF;AACrF,wBAAgB,yBAAyB,IAAI,MAAM,EAAE,CAEpD;AA2BD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBvD"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cold-tier facts shared by the agent (writer), the CLI (help text) and the
|
|
3
|
+
* dashboard (reader), kept in a NATIVE-FREE leaf module.
|
|
4
|
+
*
|
|
5
|
+
* `provider/model-host.ts` value-imports `@mlx-node/lm`, which loads the native
|
|
6
|
+
* addon; the dashboard is a separate viewer process that must never link it
|
|
7
|
+
* ("no Metal init, instant start" — docs/dashboard.md), and `mlx agent --help`
|
|
8
|
+
* must print without loading weights. This module therefore has NO runtime
|
|
9
|
+
* imports beyond `node:` builtins, and reaches those consumers through the
|
|
10
|
+
* existing native-free `@mlx-node/agent/catalog` subpath, which re-exports it.
|
|
11
|
+
* There is deliberately no `./cold-tier` entry in `package.json`: a second
|
|
12
|
+
* published subpath would be a second thing to keep native-free, and `catalog`
|
|
13
|
+
* already carries that guarantee.
|
|
14
|
+
*
|
|
15
|
+
* Everything here is a single source of truth. Do not copy the family list or
|
|
16
|
+
* re-implement the root canonicalization at a call site; import them.
|
|
17
|
+
*/
|
|
18
|
+
import { realpathSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
21
|
+
/**
|
|
22
|
+
* Model families whose paged prefix blocks can be restored from the SSD cold
|
|
23
|
+
* tier soundly. The single source of truth on the TypeScript side, mirroring
|
|
24
|
+
* `COLD_RESTORE_FAMILIES` in `crates/mlx-core/src/cold_tier.rs`; the two gate
|
|
25
|
+
* the same decision from opposite ends, and
|
|
26
|
+
* `packages/agent/__test__/cold-tier-families.test.ts` asserts they never
|
|
27
|
+
* drift.
|
|
28
|
+
*
|
|
29
|
+
* A family belongs here only when EVERY piece of per-token state its forward
|
|
30
|
+
* pass carries between turns is reconstructible from the tier — either because
|
|
31
|
+
* it all lives inside the paged pool, or because the part that does not is
|
|
32
|
+
* persisted alongside as a cold-tier sidecar and the restore reconciles down to
|
|
33
|
+
* a boundary that sidecar actually backs:
|
|
34
|
+
*
|
|
35
|
+
* - `qwen3` (dense) sizes its pool over all layers, so the pool holds the
|
|
36
|
+
* complete KV for the prefix and needs no sidecar.
|
|
37
|
+
* - `gemma4` sizes its pool over the full-attention layers only, but persists
|
|
38
|
+
* its out-of-pool sliding-window `RotatingKVCache` state as a
|
|
39
|
+
* `ColdGroup::SlidingWindow` sidecar, and its `ColdSidecarPolicy` makes the
|
|
40
|
+
* native restore walk refuse any boundary a validated sidecar does not back.
|
|
41
|
+
* - `qwen3_5` (dense) sizes its pool over the full-attention layers only, but
|
|
42
|
+
* persists its out-of-pool GDN recurrent state (conv + recurrent) as a
|
|
43
|
+
* `ColdGroup::GdnState` sidecar, and its `ColdSidecarPolicy` reconciles the
|
|
44
|
+
* native restore down to the deepest block-aligned boundary a validated
|
|
45
|
+
* sidecar backs (a recurrent state is valid only at its exact prefix length).
|
|
46
|
+
* - `qwen3_5_moe` keeps the SAME GDN recurrent state outside the pool — same
|
|
47
|
+
* shapes, same dtype, same layer mapping — so it shares the dense family's
|
|
48
|
+
* sidecar codec and `ColdSidecarPolicy` verbatim. Its restart-parity gate has
|
|
49
|
+
* since been run on `Qwen3.6-35b-a3b-UD-Q2_K_XL-mlx`: the restore reconciled
|
|
50
|
+
* onto ladder rung 304 of `[16, 64, 304, 1248]` with `hits=42` and
|
|
51
|
+
* `corruptions=0`, and the text matched a no-persist baseline.
|
|
52
|
+
* - `lfm2` / `lfm2_moe` keep short-conv state outside the pool with no
|
|
53
|
+
* serialization path for it at all, AND drive the uniform native adapter
|
|
54
|
+
* API whose restore branch is already wired to the tier — asking for
|
|
55
|
+
* persistence on their behalf would restore an incomplete prefix silently.
|
|
56
|
+
*
|
|
57
|
+
* Widening this set is a correctness decision, never a perf one, and it is
|
|
58
|
+
* authorized by exactly one thing: the family's native restart-parity gate
|
|
59
|
+
* passing on real weights with `hits > 0` and `corruptions == 0`.
|
|
60
|
+
*
|
|
61
|
+
* `mlx agent --no-persist-cache` applies to EVERY family in this set, not to
|
|
62
|
+
* one of them: it is a SINGLE process-wide boolean handed to every load whose
|
|
63
|
+
* family is on the allowlist (`MlxModelHost.runWithResident` →
|
|
64
|
+
* `resolveModelPathFn(entry, { persistPagedCache })`). Families off the
|
|
65
|
+
* allowlist are handed no policy at all — not because the flag spares them, but
|
|
66
|
+
* because `cold_tier::resolve_persist_cold` refuses to persist them under ANY
|
|
67
|
+
* setting. The help text says so in exactly one place, built from
|
|
68
|
+
* {@link coldTierRestoreFamilyList} in `packages/cli/src/commands/agent/index.ts`
|
|
69
|
+
* and asserted by `packages/cli/__test__/agent-cmd.test.ts`.
|
|
70
|
+
*/
|
|
71
|
+
export const COLD_TIER_RESTORE_FAMILIES = new Set([
|
|
72
|
+
'gemma4',
|
|
73
|
+
'qwen3',
|
|
74
|
+
'qwen3_5',
|
|
75
|
+
'qwen3_5_moe',
|
|
76
|
+
]);
|
|
77
|
+
/** Allowlisted families in a stable, human-facing order (help text, API payload). */
|
|
78
|
+
export function coldTierRestoreFamilyList() {
|
|
79
|
+
return [...COLD_TIER_RESTORE_FAMILIES].sort();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Expand a leading `~` / `~/…` against the current home directory. A bare `~`
|
|
83
|
+
* prefix that is part of a longer name (`~cache`) is left alone — only the
|
|
84
|
+
* shell's own two spellings are expanded.
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* `realpath(3)` rather than Node's JS walk.
|
|
88
|
+
*
|
|
89
|
+
* On a case-insensitive volume — APFS's default, i.e. essentially every Mac —
|
|
90
|
+
* `MLX_COLD_CACHE_DIR=~/CacheDir` and `~/cachedir` name the SAME directory, but
|
|
91
|
+
* `fs.realpathSync` hands back whichever spelling the caller typed. Two join
|
|
92
|
+
* keys for one cache is the F1 symptom inverted: a populated disk scan sitting
|
|
93
|
+
* beside a flat 0/0 trend. `realpathSync.native` folds each existing component
|
|
94
|
+
* to its on-disk spelling, so both env spellings produce one key.
|
|
95
|
+
*/
|
|
96
|
+
function realpathNative(path) {
|
|
97
|
+
return realpathSync.native(path);
|
|
98
|
+
}
|
|
99
|
+
function expandTilde(path) {
|
|
100
|
+
if (path === '~')
|
|
101
|
+
return homedir();
|
|
102
|
+
if (path.startsWith('~/'))
|
|
103
|
+
return join(homedir(), path.slice(2));
|
|
104
|
+
return path;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Canonical identity for a cold-cache root, used as the JOIN KEY between the
|
|
108
|
+
* agent that wrote a trace and the dashboard that reads it back.
|
|
109
|
+
*
|
|
110
|
+
* The two live in DIFFERENT PROCESSES resolving the root from their own
|
|
111
|
+
* environments: the native tier reports `manager.root().display()` (the path as
|
|
112
|
+
* constructed, never realpath'd) while the dashboard builds its own from
|
|
113
|
+
* `MLX_COLD_CACHE_DIR` or the `~/.mlx-node` default. On macOS `/tmp` is a
|
|
114
|
+
* symlink to `/private/tmp`, so two identical-looking spellings canonicalize
|
|
115
|
+
* differently — a raw string compare is a silent-zero trap, not a loud failure.
|
|
116
|
+
* Both sides MUST route through this one function.
|
|
117
|
+
*
|
|
118
|
+
* Resolution order: expand `~`, make absolute, then `realpath` the DEEPEST
|
|
119
|
+
* EXISTING ANCESTOR and re-attach every missing segment lexically. A tier that
|
|
120
|
+
* has never been opened has no root directory — that is the normal state, not
|
|
121
|
+
* an edge case — and when `MLX_COLD_CACHE_DIR` itself has never been created
|
|
122
|
+
* the parent is missing too. Stopping after one level returned a wholly
|
|
123
|
+
* UNcanonicalized path, so under a symlinked ancestor (`/tmp` → `/private/tmp`
|
|
124
|
+
* on macOS) the reader's key diverged from the writer's and rows from the SAME
|
|
125
|
+
* cache were reported as "a different cache directory". Only a path with no
|
|
126
|
+
* resolvable ancestor at all falls back to the lexical absolute form; this is
|
|
127
|
+
* an identity key, not a security boundary, so it fails OPEN to a stable string
|
|
128
|
+
* rather than to `undefined`.
|
|
129
|
+
*
|
|
130
|
+
* An empty input stays empty: the native struct reports `root: ''` while the
|
|
131
|
+
* tier is disabled, and that must never be recorded as a real cache identity.
|
|
132
|
+
*/
|
|
133
|
+
export function canonicalCacheRoot(root) {
|
|
134
|
+
const expanded = expandTilde(root.trim());
|
|
135
|
+
if (expanded.length === 0)
|
|
136
|
+
return '';
|
|
137
|
+
const absolute = resolve(expanded);
|
|
138
|
+
const missing = [];
|
|
139
|
+
let cursor = absolute;
|
|
140
|
+
for (;;) {
|
|
141
|
+
try {
|
|
142
|
+
const resolved = realpathNative(cursor);
|
|
143
|
+
return missing.length === 0 ? resolved : join(resolved, ...missing);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
const parent = dirname(cursor);
|
|
147
|
+
// `dirname` is a fixpoint at the filesystem root, which is the loop's
|
|
148
|
+
// termination condition: nothing above it is left to canonicalize.
|
|
149
|
+
if (parent === cursor)
|
|
150
|
+
return absolute;
|
|
151
|
+
missing.unshift(basename(cursor));
|
|
152
|
+
cursor = parent;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach the local image path inserted by Pi's interactive clipboard handler.
|
|
3
|
+
*
|
|
4
|
+
* Pi 0.81.1 writes a pasted clipboard image to a temporary file and inserts
|
|
5
|
+
* only that absolute path into the editor. AgentSession's `input` event is the
|
|
6
|
+
* last seam where the path can be upgraded to `ImageContent` before the user
|
|
7
|
+
* message is committed. Keep this deliberately narrow:
|
|
8
|
+
*
|
|
9
|
+
* - TUI + interactive input only;
|
|
10
|
+
* - the entire input must be one absolute image path;
|
|
11
|
+
* - only `\ ` shell escapes (the macOS drag/paste shape) are decoded;
|
|
12
|
+
* - extension and magic bytes must both identify a supported image.
|
|
13
|
+
*
|
|
14
|
+
* Every failed check returns `continue`, preserving the original text exactly.
|
|
15
|
+
* Capability is intentionally not checked here: discovery advertises every
|
|
16
|
+
* local model as text-only until its first resident load. The stream adapter's
|
|
17
|
+
* authoritative `session.supportsImages()` decides whether native bytes or the
|
|
18
|
+
* existing text-model placeholder reaches inference.
|
|
19
|
+
*/
|
|
20
|
+
import type { ExtensionContext, InlineExtension, InputEvent, InputEventResult } from '@earendil-works/pi-coding-agent';
|
|
21
|
+
/** @internal Exported for focused tests of Pi's input-transform contract. */
|
|
22
|
+
export declare function attachStandaloneLocalImage(event: InputEvent, ctx: ExtensionContext): Promise<InputEventResult>;
|
|
23
|
+
export declare function createLocalImageInputExtension(): InlineExtension;
|
|
24
|
+
//# sourceMappingURL=local-image-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-image-input.d.ts","sourceRoot":"","sources":["../../src/extensions/local-image-input.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,OAAO,KAAK,EAEV,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,gBAAgB,EACjB,MAAM,iCAAiC,CAAC;AA0CzC,6EAA6E;AAC7E,wBAAsB,0BAA0B,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAkDpH;AAED,wBAAgB,8BAA8B,IAAI,eAAe,CAOhE"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach the local image path inserted by Pi's interactive clipboard handler.
|
|
3
|
+
*
|
|
4
|
+
* Pi 0.81.1 writes a pasted clipboard image to a temporary file and inserts
|
|
5
|
+
* only that absolute path into the editor. AgentSession's `input` event is the
|
|
6
|
+
* last seam where the path can be upgraded to `ImageContent` before the user
|
|
7
|
+
* message is committed. Keep this deliberately narrow:
|
|
8
|
+
*
|
|
9
|
+
* - TUI + interactive input only;
|
|
10
|
+
* - the entire input must be one absolute image path;
|
|
11
|
+
* - only `\ ` shell escapes (the macOS drag/paste shape) are decoded;
|
|
12
|
+
* - extension and magic bytes must both identify a supported image.
|
|
13
|
+
*
|
|
14
|
+
* Every failed check returns `continue`, preserving the original text exactly.
|
|
15
|
+
* Capability is intentionally not checked here: discovery advertises every
|
|
16
|
+
* local model as text-only until its first resident load. The stream adapter's
|
|
17
|
+
* authoritative `session.supportsImages()` decides whether native bytes or the
|
|
18
|
+
* existing text-model placeholder reaches inference.
|
|
19
|
+
*/
|
|
20
|
+
import { open } from 'node:fs/promises';
|
|
21
|
+
import { extname, isAbsolute } from 'node:path';
|
|
22
|
+
const MAX_LOCAL_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
23
|
+
const IMAGE_EXTENSIONS = new Set(['.gif', '.jpeg', '.jpg', '.png', '.webp']);
|
|
24
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
25
|
+
function startsWith(bytes, signature, offset = 0) {
|
|
26
|
+
return (bytes.byteLength >= offset + signature.length && signature.every((byte, index) => bytes[offset + index] === byte));
|
|
27
|
+
}
|
|
28
|
+
function startsWithAscii(bytes, text, offset = 0) {
|
|
29
|
+
return startsWith(bytes, Array.from(text, (character) => character.charCodeAt(0)), offset);
|
|
30
|
+
}
|
|
31
|
+
function detectImageMimeType(bytes) {
|
|
32
|
+
if (startsWith(bytes, [0xff, 0xd8, 0xff]) && bytes[3] !== 0xf7)
|
|
33
|
+
return 'image/jpeg';
|
|
34
|
+
if (startsWith(bytes, PNG_SIGNATURE) && startsWithAscii(bytes, 'IHDR', 12))
|
|
35
|
+
return 'image/png';
|
|
36
|
+
if (startsWithAscii(bytes, 'GIF87a') || startsWithAscii(bytes, 'GIF89a'))
|
|
37
|
+
return 'image/gif';
|
|
38
|
+
if (startsWithAscii(bytes, 'RIFF') && startsWithAscii(bytes, 'WEBP', 8))
|
|
39
|
+
return 'image/webp';
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve only the standalone path shape produced by Pi/macOS paste and drag.
|
|
44
|
+
* This is not a shell parser: no quoting, expansion, commands, or general
|
|
45
|
+
* backslash processing is performed.
|
|
46
|
+
*/
|
|
47
|
+
function standaloneImagePath(text) {
|
|
48
|
+
const trimmed = text.trim();
|
|
49
|
+
if (trimmed.length === 0 || trimmed.includes('\n') || trimmed.includes('\r'))
|
|
50
|
+
return undefined;
|
|
51
|
+
const path = trimmed.replaceAll('\\ ', ' ');
|
|
52
|
+
if (!isAbsolute(path) || !IMAGE_EXTENSIONS.has(extname(path).toLowerCase()))
|
|
53
|
+
return undefined;
|
|
54
|
+
return path;
|
|
55
|
+
}
|
|
56
|
+
/** @internal Exported for focused tests of Pi's input-transform contract. */
|
|
57
|
+
export async function attachStandaloneLocalImage(event, ctx) {
|
|
58
|
+
if (ctx.mode !== 'tui' || event.source !== 'interactive' || event.images?.length) {
|
|
59
|
+
return { action: 'continue' };
|
|
60
|
+
}
|
|
61
|
+
const path = standaloneImagePath(event.text);
|
|
62
|
+
if (path === undefined)
|
|
63
|
+
return { action: 'continue' };
|
|
64
|
+
try {
|
|
65
|
+
// Open once so validation and reading stay bound to the same inode if
|
|
66
|
+
// the pathname is replaced. FileHandle.readFile() is intentionally not
|
|
67
|
+
// used: it may allocate from a file that grew after stat(). The exact,
|
|
68
|
+
// positional loop below caps allocation at the validated size.
|
|
69
|
+
const file = await open(path, 'r');
|
|
70
|
+
try {
|
|
71
|
+
const metadata = await file.stat();
|
|
72
|
+
if (!metadata.isFile() || metadata.size === 0 || metadata.size > MAX_LOCAL_IMAGE_BYTES) {
|
|
73
|
+
return { action: 'continue' };
|
|
74
|
+
}
|
|
75
|
+
const bytes = Buffer.allocUnsafe(metadata.size);
|
|
76
|
+
let offset = 0;
|
|
77
|
+
while (offset < bytes.byteLength) {
|
|
78
|
+
const { bytesRead } = await file.read(bytes, offset, bytes.byteLength - offset, offset);
|
|
79
|
+
if (bytesRead === 0)
|
|
80
|
+
return { action: 'continue' };
|
|
81
|
+
offset += bytesRead;
|
|
82
|
+
}
|
|
83
|
+
// Reject same-inode growth after fstat without ever allocating or
|
|
84
|
+
// reading more than one byte beyond the validated size.
|
|
85
|
+
const overflowProbe = Buffer.allocUnsafe(1);
|
|
86
|
+
if ((await file.read(overflowProbe, 0, 1, metadata.size)).bytesRead !== 0) {
|
|
87
|
+
return { action: 'continue' };
|
|
88
|
+
}
|
|
89
|
+
const mimeType = detectImageMimeType(bytes);
|
|
90
|
+
if (mimeType === undefined)
|
|
91
|
+
return { action: 'continue' };
|
|
92
|
+
return {
|
|
93
|
+
action: 'transform',
|
|
94
|
+
text: event.text,
|
|
95
|
+
images: [{ type: 'image', mimeType, data: bytes.toString('base64') }],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await file.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Missing, unreadable, or racing files remain ordinary user text.
|
|
104
|
+
return { action: 'continue' };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function createLocalImageInputExtension() {
|
|
108
|
+
return {
|
|
109
|
+
name: 'mlx-local-image-input',
|
|
110
|
+
factory: (pi) => {
|
|
111
|
+
pi.on('input', attachStandaloneLocalImage);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -15,6 +15,16 @@
|
|
|
15
15
|
* examples/extensions/subagent (MIT).
|
|
16
16
|
*/
|
|
17
17
|
import type { ExtensionContext, InlineExtension } from '@earendil-works/pi-coding-agent';
|
|
18
|
+
/**
|
|
19
|
+
* Tool loops can overlap; the shared `MlxModelHost` serializes model calls.
|
|
20
|
+
*
|
|
21
|
+
* Exported because it is one half of a cross-language invariant. Every live
|
|
22
|
+
* loop is a distinct native GDN cache owner, so this fleet demands
|
|
23
|
+
* `MAX_CONCURRENCY + 1` checkpoint slots (root session plus one per loop), and
|
|
24
|
+
* the native store supplies `gdnPrefixCheckpointLimit()` of them.
|
|
25
|
+
* `__test__/gdn-checkpoint-capacity.test.ts` holds the two together.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MAX_CONCURRENCY = 4;
|
|
18
28
|
type AgentScope = 'user' | 'project' | 'both';
|
|
19
29
|
type AgentSource = 'builtin' | 'user' | 'project' | 'unknown';
|
|
20
30
|
export type SubagentMode = 'single' | 'parallel' | 'chain';
|
|
@@ -36,7 +46,15 @@ export interface InProcessSubagentSession {
|
|
|
36
46
|
export interface SubagentSessionCreateOptions {
|
|
37
47
|
cwd: string;
|
|
38
48
|
model: NonNullable<ExtensionContext['model']>;
|
|
39
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Parent's registered in-process mlx provider config. Registering it on the
|
|
51
|
+
* subagent's fresh runtime reuses the parent's `streamSimple` closure, which
|
|
52
|
+
* is bound to the single shared `MlxModelHost` — so subagent inference stays
|
|
53
|
+
* serialized on the one GPU-resident model. `ProviderConfigInput` is not
|
|
54
|
+
* exported at the package root, so type it structurally from the facade
|
|
55
|
+
* method's return type (survives a future pi rename of the type).
|
|
56
|
+
*/
|
|
57
|
+
mlxProviderConfig: NonNullable<ReturnType<ExtensionContext['modelRegistry']['getRegisteredProviderConfig']>>;
|
|
40
58
|
tools?: string[];
|
|
41
59
|
systemPrompt: string;
|
|
42
60
|
}
|
|
@@ -77,6 +95,7 @@ export declare function normalizeSubagentMode(input: SubagentRequestShape): {
|
|
|
77
95
|
mode: SubagentMode;
|
|
78
96
|
modeCount: number;
|
|
79
97
|
};
|
|
98
|
+
export declare function createProductionSession(options: SubagentSessionCreateOptions): Promise<InProcessSubagentSession>;
|
|
80
99
|
export declare function createSubagentExtension(options?: SubagentExtensionOptions): InlineExtension;
|
|
81
100
|
export {};
|
|
82
101
|
//# sourceMappingURL=subagent.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent.d.ts","sourceRoot":"","sources":["../../src/extensions/subagent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,OAAO,KAAK,EAAgB,gBAAgB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"subagent.d.ts","sourceRoot":"","sources":["../../src/extensions/subagent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,OAAO,KAAK,EAAgB,gBAAgB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAMvG;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,IAAI,CAAC;AAGjC,KAAK,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAC9C,KAAK,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;AAC9D,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACxC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAiCD,MAAM,WAAW,wBAAwB;IACvC,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC1D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,4BAA4B;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9C;;;;;;;OAOG;IACH,iBAAiB,EAAE,WAAW,CAAC,UAAU,CAAC,gBAAgB,CAAC,eAAe,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,CAAC;IAC7G,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,4BAA4B,KAAK,OAAO,CAAC,wBAAwB,CAAC,CAAC;CAC9F;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAMD;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC7C,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,0BAA0B,GAClC,0BAA0B,CAoB5B;AA6GD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,GAChB;IACD,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC,CASA;AAqCD,UAAU,oBAAoB;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,oBAAoB,GAAG;IAClE,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAQA;AAeD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,wBAAwB,CAAC,CAkDnC;AA0LD,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,wBAA6B,GAAG,eAAe,CAsJ/F"}
|
|
@@ -21,8 +21,16 @@ import { StringEnum } from '@earendil-works/pi-ai';
|
|
|
21
21
|
import { Type } from 'typebox';
|
|
22
22
|
import { sanitizeApprovalDetail } from './approval-detail.js';
|
|
23
23
|
const MAX_PARALLEL_TASKS = 8;
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Tool loops can overlap; the shared `MlxModelHost` serializes model calls.
|
|
26
|
+
*
|
|
27
|
+
* Exported because it is one half of a cross-language invariant. Every live
|
|
28
|
+
* loop is a distinct native GDN cache owner, so this fleet demands
|
|
29
|
+
* `MAX_CONCURRENCY + 1` checkpoint slots (root session plus one per loop), and
|
|
30
|
+
* the native store supplies `gdnPrefixCheckpointLimit()` of them.
|
|
31
|
+
* `__test__/gdn-checkpoint-capacity.test.ts` holds the two together.
|
|
32
|
+
*/
|
|
33
|
+
export const MAX_CONCURRENCY = 4;
|
|
26
34
|
const PER_TASK_OUTPUT_CAP = 50 * 1024;
|
|
27
35
|
const COMPACTION_RESERVE_WINDOW_FRACTION = 0.25;
|
|
28
36
|
const COMPACTION_KEEP_RECENT_WINDOW_FRACTION = 0.5;
|
|
@@ -238,10 +246,10 @@ async function mapWithConcurrencyLimit(items, fn) {
|
|
|
238
246
|
await Promise.all(workers);
|
|
239
247
|
return results;
|
|
240
248
|
}
|
|
241
|
-
async function createProductionSession(options) {
|
|
249
|
+
export async function createProductionSession(options) {
|
|
242
250
|
// `runAgent()` seeds pi's config environment before the extension can execute,
|
|
243
251
|
// so defer the runtime import until now instead of importing pi at module load.
|
|
244
|
-
const { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager } = await import('@earendil-works/pi-coding-agent');
|
|
252
|
+
const { createAgentSession, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager } = await import('@earendil-works/pi-coding-agent');
|
|
245
253
|
const agentDir = getAgentDir();
|
|
246
254
|
const settingsManager = SettingsManager.create(options.cwd, agentDir);
|
|
247
255
|
settingsManager.applyOverrides({
|
|
@@ -259,11 +267,26 @@ async function createProductionSession(options) {
|
|
|
259
267
|
appendSystemPromptOverride: (base) => (options.systemPrompt.trim() ? [...base, options.systemPrompt] : base),
|
|
260
268
|
});
|
|
261
269
|
await resourceLoader.reload();
|
|
270
|
+
// 0.81.1: `createAgentSession` takes a `ModelRuntime`, not a `ModelRegistry`.
|
|
271
|
+
// Build a fresh runtime that carries the parent's in-process mlx provider. The
|
|
272
|
+
// config's `streamSimple` closure is bound to the single shared `MlxModelHost`,
|
|
273
|
+
// so all subagent inference stays serialized on the one GPU-resident model.
|
|
274
|
+
// `create` does no network (`allowModelNetwork:false`, also the default — set
|
|
275
|
+
// explicitly to keep the offline invariant local to this call, independent of
|
|
276
|
+
// the process-wide PI_OFFLINE seed in `runAgent`), and reuses the parent's
|
|
277
|
+
// `authPath`/`modelsPath`; `registerProvider` marks mlx configured (apiKey
|
|
278
|
+
// present) so streaming works immediately, dispatched by provider id.
|
|
279
|
+
const modelRuntime = await ModelRuntime.create({
|
|
280
|
+
authPath: path.join(agentDir, 'auth.json'),
|
|
281
|
+
modelsPath: path.join(agentDir, 'models.json'),
|
|
282
|
+
allowModelNetwork: false,
|
|
283
|
+
});
|
|
284
|
+
modelRuntime.registerProvider('mlx', options.mlxProviderConfig);
|
|
262
285
|
const { session } = await createAgentSession({
|
|
263
286
|
cwd: options.cwd,
|
|
264
287
|
agentDir,
|
|
265
288
|
model: options.model,
|
|
266
|
-
|
|
289
|
+
modelRuntime,
|
|
267
290
|
tools: options.tools,
|
|
268
291
|
resourceLoader,
|
|
269
292
|
sessionManager: SessionManager.inMemory(options.cwd),
|
|
@@ -312,6 +335,23 @@ async function runSingleAgent(options, defaultCwd, agents, agentName, task, cwd,
|
|
|
312
335
|
step,
|
|
313
336
|
};
|
|
314
337
|
}
|
|
338
|
+
// The subagent runs on a fresh runtime (no mlx provider extension), so it must
|
|
339
|
+
// inherit the parent's exact registered mlx config to reuse the shared host.
|
|
340
|
+
// Fail closed if the parent has no 'mlx' provider — never fall back to an
|
|
341
|
+
// unconfigured/cloud provider.
|
|
342
|
+
const mlxProviderConfig = context.modelRegistry.getRegisteredProviderConfig('mlx');
|
|
343
|
+
if (!mlxProviderConfig) {
|
|
344
|
+
return {
|
|
345
|
+
agent: agent.name,
|
|
346
|
+
agentSource: agent.source,
|
|
347
|
+
task,
|
|
348
|
+
exitCode: 1,
|
|
349
|
+
messages: [],
|
|
350
|
+
stderr: `Agent ${agent.name} could not inherit the parent mlx provider (no 'mlx' provider registered).`,
|
|
351
|
+
usage: emptyUsage(),
|
|
352
|
+
step,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
315
355
|
const result = {
|
|
316
356
|
agent: agent.name,
|
|
317
357
|
agentSource: agent.source,
|
|
@@ -342,7 +382,7 @@ async function runSingleAgent(options, defaultCwd, agents, agentName, task, cwd,
|
|
|
342
382
|
session = await (options.createSession ?? createProductionSession)({
|
|
343
383
|
cwd: cwd ?? defaultCwd,
|
|
344
384
|
model,
|
|
345
|
-
|
|
385
|
+
mlxProviderConfig,
|
|
346
386
|
tools: agent.tools,
|
|
347
387
|
systemPrompt: agent.systemPrompt,
|
|
348
388
|
});
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `$HOME/.mlx-node` layout helpers owned by the agent.
|
|
3
|
+
*
|
|
4
|
+
* The agent must not import `@mlx-node/cli` (wrong dependency direction), so
|
|
5
|
+
* the small home-directory layout it needs lives here. Mirrors
|
|
6
|
+
* `resolveMlxNodeHome()` in `@mlx-node/server/host/paths`
|
|
7
|
+
* (`packages/server/src/host/paths.ts`).
|
|
8
|
+
*/
|
|
9
|
+
/** Absolute path to `$HOME/.mlx-node`. */
|
|
10
|
+
export declare function mlxNodeHome(): string;
|
|
11
|
+
/** Directory holding per-process `MetricsTrace` JSONL files. */
|
|
12
|
+
export declare function metricsTraceDir(): string;
|
|
13
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,0CAA0C;AAC1C,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,gEAAgE;AAChE,wBAAgB,eAAe,IAAI,MAAM,CAExC"}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `$HOME/.mlx-node` layout helpers owned by the agent.
|
|
3
|
+
*
|
|
4
|
+
* The agent must not import `@mlx-node/cli` (wrong dependency direction), so
|
|
5
|
+
* the small home-directory layout it needs lives here. Mirrors
|
|
6
|
+
* `resolveMlxNodeHome()` in `@mlx-node/server/host/paths`
|
|
7
|
+
* (`packages/server/src/host/paths.ts`).
|
|
8
|
+
*/
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
/** Absolute path to `$HOME/.mlx-node`. */
|
|
12
|
+
export function mlxNodeHome() {
|
|
13
|
+
return join(homedir(), '.mlx-node');
|
|
14
|
+
}
|
|
15
|
+
/** Directory holding per-process `MetricsTrace` JSONL files. */
|
|
16
|
+
export function metricsTraceDir() {
|
|
17
|
+
return join(mlxNodeHome(), 'metrics', 'traces');
|
|
18
|
+
}
|
|
@@ -30,5 +30,5 @@ export interface ResolvedReasoningMode {
|
|
|
30
30
|
* option was present.
|
|
31
31
|
*/
|
|
32
32
|
export declare function resolveReasoningMode(reasoning: ThinkingLevel | undefined): ResolvedReasoningMode;
|
|
33
|
-
export declare function buildChatConfig(modelType: ModelType, options: SimpleStreamOptions | undefined, tools: ToolDefinition[] | undefined, rootCacheOwnerId?: string, resolvedReasoning?: ResolvedReasoningMode): ChatConfig;
|
|
33
|
+
export declare function buildChatConfig(modelType: ModelType, options: SimpleStreamOptions | undefined, tools: ToolDefinition[] | undefined, rootCacheOwnerId?: string, resolvedReasoning?: ResolvedReasoningMode, modelMaxTokens?: unknown): ChatConfig;
|
|
34
34
|
//# sourceMappingURL=chat-config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-config.d.ts","sourceRoot":"","sources":["../../src/provider/chat-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AA0BrE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,CAE9E;AAgBD,MAAM,WAAW,qBAAqB;IACpC,eAAe,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpD,8EAA8E;IAC9E,eAAe,EAAE,OAAO,CAAC;CAC1B;
|
|
1
|
+
{"version":3,"file":"chat-config.d.ts","sourceRoot":"","sources":["../../src/provider/chat-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AA0BrE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,CAE9E;AAgBD,MAAM,WAAW,qBAAqB;IACpC,eAAe,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpD,8EAA8E;IAC9E,eAAe,EAAE,OAAO,CAAC;CAC1B;AAMD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,aAAa,GAAG,SAAS,GAAG,qBAAqB,CAMhG;AAED,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,mBAAmB,GAAG,SAAS,EACxC,KAAK,EAAE,cAAc,EAAE,GAAG,SAAS,EACnC,gBAAgB,CAAC,EAAE,MAAM,EACzB,iBAAiB,wBAA2C,EAC5D,cAAc,CAAC,EAAE,OAAO,GACvB,UAAU,CAsCZ"}
|