@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.
- package/dist/catalog.d.ts +10 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +11 -2
- package/dist/delegate.d.ts +29 -0
- package/dist/delegate.d.ts.map +1 -0
- package/dist/delegate.js +106 -0
- package/dist/extensions/delegation.d.ts +15 -0
- package/dist/extensions/delegation.d.ts.map +1 -0
- package/dist/extensions/delegation.js +93 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +16 -0
- package/dist/provider/chat-config.d.ts +6 -5
- package/dist/provider/chat-config.d.ts.map +1 -1
- package/dist/provider/chat-config.js +21 -7
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +8 -1
- package/dist/provider/model-host.d.ts +1 -1
- package/dist/provider/model-host.d.ts.map +1 -1
- package/dist/provider/model-host.js +25 -7
- package/dist/provider/models.d.ts +3 -14
- package/dist/provider/models.d.ts.map +1 -1
- package/dist/provider/models.js +17 -239
- package/dist/provider/stream-adapter.d.ts +2 -2
- package/dist/provider/stream-adapter.d.ts.map +1 -1
- package/dist/provider/stream-adapter.js +8 -5
- package/dist/run-agent.d.ts +4 -0
- package/dist/run-agent.d.ts.map +1 -1
- package/dist/run-agent.js +8 -2
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +23 -5
- package/src/catalog.ts +194 -0
- package/src/cold-tier.ts +152 -0
- package/src/delegate.ts +136 -0
- package/src/extensions/approval-detail.ts +57 -0
- package/src/extensions/delegation.ts +109 -0
- package/src/extensions/local-image-input.ts +132 -0
- package/src/extensions/permission-gate.ts +347 -0
- package/src/extensions/subagent.ts +743 -0
- package/src/extensions/terminal-title.ts +53 -0
- package/src/extensions/trace-notice.ts +37 -0
- package/src/index.ts +23 -0
- package/src/paths.ts +36 -0
- package/src/provider/chat-config.ts +132 -0
- package/src/provider/convert-messages.ts +273 -0
- package/src/provider/error-coercion.ts +36 -0
- package/src/provider/events.ts +341 -0
- package/src/provider/index.ts +255 -0
- package/src/provider/metrics-trace.ts +380 -0
- package/src/provider/mlx-identity.ts +16 -0
- package/src/provider/model-host.ts +276 -0
- package/src/provider/model-registry-filter.ts +336 -0
- package/src/provider/models.ts +48 -0
- package/src/provider/performance-status.ts +112 -0
- package/src/provider/reasoning-tag-buffer.ts +67 -0
- package/src/provider/stream-adapter.ts +515 -0
- package/src/provider/tool-call-buffer.ts +82 -0
- package/src/provider/warm-reuse.ts +125 -0
- package/src/run-agent.ts +178 -0
- 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
|
+
}
|
package/src/run-agent.ts
ADDED
|
@@ -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
|
+
}
|