arcane-os 0.88.1 → 0.90.0

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 (44) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/browser-runtime/ai/browser-decisions.mjs +31 -4
  3. package/browser-runtime/ai/core-local.mjs +105 -25
  4. package/browser-runtime/ai/decision-runtime.mjs +7 -0
  5. package/browser-runtime/ai/decision-worker.mjs +9 -1
  6. package/browser-runtime/core/client.mjs +6 -0
  7. package/browser-runtime/core/contracts.mjs +10 -0
  8. package/docs/reference/ai/browser-decisions.md +89 -7
  9. package/docs/reference/core-client.md +59 -0
  10. package/docs/reference/core-native-packaging.md +62 -10
  11. package/docs/reference/core-repositories.md +104 -8
  12. package/docs/reference/git-text-snapshot.md +16 -1
  13. package/docs/reference/inventory/package-api.json +25 -25
  14. package/docs/reference/inventory/runtime-components.json +1 -1
  15. package/docs/reference/inventory/runtime-modules.json +4 -4
  16. package/docs/reference/local-ai.md +108 -9
  17. package/docs/reference/mcp-stdio.md +76 -1
  18. package/docs/reference/native-app-control.md +17 -5
  19. package/docs/reference/protocols.md +14 -0
  20. package/docs/reference/runtime-modules.md +9 -3
  21. package/docs/reference/sdk-api.md +202 -35
  22. package/package.json +2 -2
  23. package/runtime/arcane/modules/RiskSignalAnalyzer.js +1 -5
  24. package/schemas/arcane-app.schema.json +13 -3
  25. package/schemas/arcane-package.schema.json +10 -1
  26. package/src/app-descriptor.mjs +6 -2
  27. package/src/cli/app-control.mjs +1 -1
  28. package/src/core/hosts/windows/ArcaneHost.cs +273 -5
  29. package/src/core/hosts/windows/ArcaneLauncher.cs +9 -0
  30. package/src/core/hosts/windows/ArcaneWindowControl.cs +7 -7
  31. package/src/core/repositories.mjs +17 -6
  32. package/src/core/services/local-ai.mjs +183 -22
  33. package/src/git-text-snapshot.mjs +4 -2
  34. package/src/installed-sdk-runtime.mjs +2 -0
  35. package/src/local-ai/config.mjs +16 -0
  36. package/src/local-ai/dev.mjs +2 -2
  37. package/src/local-ai/discover.mjs +3 -0
  38. package/src/local-ai/install.mjs +130 -20
  39. package/src/local-ai/llama-execution-windows.ps1 +158 -0
  40. package/src/local-ai/llama-execution.mjs +169 -0
  41. package/src/local-ai/server.mjs +38 -8
  42. package/src/native/portable-layout.mjs +13 -3
  43. package/src/repository.mjs +33 -8
  44. package/src/workspace-runtime.mjs +97 -1
@@ -4,6 +4,7 @@ import Is from 'strong-type';
4
4
  import {ArcaneError,ERROR_CODES,errorRecord,normalizeError,throwIfAborted} from '../errors.mjs';
5
5
  import {createEventQueue} from '../event-queue.mjs';
6
6
  import {runProcess} from '../process.mjs';
7
+ import {resolveLlamaExecution} from './llama-execution.mjs';
7
8
 
8
9
  const is=new Is(false);
9
10
 
@@ -48,17 +49,21 @@ async function probeServer(id,url,signal){
48
49
  return observed;
49
50
  }
50
51
 
51
- export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,onEvent,onState}={}){
52
+ export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,onEvent,onState,onExit,executionDevices,requireManaged=false}={}){
52
53
  if(!['llama.cpp','ollama'].includes(id)){
53
54
  throw new ArcaneError(ERROR_CODES.usage,`Unsupported local AI server: ${String(id)}.`);
54
55
  }
55
56
  const url=localAIServerURL(id,configuration);
56
57
  const endpoint=new URL(url);
58
+ const managedSelection=requireManaged||(id==='llama.cpp'&&(configuration.executionTarget!==undefined
59
+ ||(runtime?.requestedBackend&&runtime.requestedBackend!=='auto')));
57
60
  const controller=new AbortController();
58
61
  const operationSignal=signal?AbortSignal.any([signal,controller.signal]):controller.signal;
59
62
  const events=createEventQueue(onEvent,{onFailure:fail});
60
63
  let current={id,url,state:'starting',available:false,owned:false};
61
64
  let owned=false;
65
+ let external=false;
66
+ let execution;
62
67
  let processTask=null;
63
68
  let processObservation=null;
64
69
  let startupTask=null;
@@ -70,6 +75,8 @@ export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,
70
75
  let terminalFailure=null;
71
76
  let resolveReady;
72
77
  let rejectReady;
78
+ let resolveManagement;
79
+ const management=new Promise(function discoverManagement(resolve){resolveManagement=resolve;});
73
80
  const output={stdout:'',stderr:''};
74
81
  const ready=new Promise(function createReadiness(resolve,reject){
75
82
  resolveReady=resolve;
@@ -80,7 +87,9 @@ export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,
80
87
  void ready.catch(function observeReadinessFailure(){return undefined;});
81
88
 
82
89
  async function publishState(state,error){
83
- current={id,url,state,available:state==='ready',owned,...(error?{error:errorRecord(error)}:{})};
90
+ current={id,url,state,available:state==='ready',owned,
91
+ managed:!external&&Boolean(runtime?.executable),
92
+ ...(id==='llama.cpp'?{execution:execution??null}:{}),...(error?{error:errorRecord(error)}:{})};
84
93
  await onState?.(current);
85
94
  await events.send({
86
95
  type:`local-ai.server.${state}`,
@@ -147,6 +156,8 @@ export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,
147
156
  // endpoint at this actual launch boundary rather than presenting
148
157
  // that earlier installation-selection snapshot as current health.
149
158
  const observed=await probeServer(id,url,operationSignal);
159
+ external=true;
160
+ resolveManagement(false);
150
161
  if(runtime?.requestedVersion&&runtime.requestedVersion!=='latest'){
151
162
  const requested=runtime.requestedVersion.replace(/^v/u,'');
152
163
  if(!is.string(observed.version)||observed.version.replace(/^v/u,'')!==requested){
@@ -158,10 +169,18 @@ export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,
158
169
  throwIfAborted(operationSignal);
159
170
  // An HTTP error is an existing service's state. Only a refused
160
171
  // connection allows starting the selected local executable.
161
- if(!localAIConnectionRefused(error))throw error;
172
+ if(!localAIConnectionRefused(error)){
173
+ external=true;
174
+ resolveManagement(false);
175
+ throw error;
176
+ }
162
177
  if(!localHostname(endpoint.hostname))throw error;
163
178
  }
164
179
  if(existing){
180
+ external=true;
181
+ if(managedSelection){
182
+ throw new ArcaneError('LOCAL_AI_EXTERNAL_SELECTION_UNSUPPORTED','The existing llama.cpp server owns its model and device selection. Use an available listener for SDK-managed model loading.');
183
+ }
165
184
  await publishState('ready');
166
185
  throwIfAborted(operationSignal);
167
186
  settleReady();
@@ -170,6 +189,7 @@ export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,
170
189
  if(!runtime?.executable){
171
190
  throw new ArcaneError(ERROR_CODES.prerequisiteMissing,`No installed ${id} executable was supplied.`);
172
191
  }
192
+ resolveManagement(true);
173
193
  if(endpoint.protocol!=='http:'){
174
194
  throw new ArcaneError(ERROR_CODES.usage,`${id} managed startup requires its direct HTTP listener URL.`);
175
195
  }
@@ -187,12 +207,19 @@ export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,
187
207
  throwIfAborted(operationSignal);
188
208
  const host=endpoint.hostname.replace(/^\[|\]$/gu,'');
189
209
  const port=endpoint.port||'80';
210
+ const selected=id==='llama.cpp'?await resolveLlamaExecution({
211
+ executionTarget:configuration.executionTarget,executionDevices,runtime,signal:operationSignal,onEvent:events.send
212
+ }):null;
213
+ if(selected)execution=selected.execution;
190
214
  const arguments_=id==='ollama'?['serve',...args]:[
191
215
  '--host',host,'--port',port,'--jinja',
216
+ ...(managedSelection?args:[]),
192
217
  ...(configuration.model?['--model',path.resolve(appRoot,configuration.model)]:['--models-dir',modelsDirectory]),
193
- ...args
218
+ ...(!managedSelection?args:[]),
219
+ ...(configuration.alias?['--alias',configuration.alias]:[]),
220
+ ...selected.args
194
221
  ];
195
- const env=id==='ollama'?{OLLAMA_HOST:endpoint.origin,OLLAMA_MODELS:modelsDirectory}:undefined;
222
+ const env=id==='ollama'?{OLLAMA_HOST:endpoint.origin,OLLAMA_MODELS:modelsDirectory}:selected.env;
196
223
  owned=true;
197
224
  await publishState('starting');
198
225
  throwIfAborted(operationSignal);
@@ -215,7 +242,7 @@ export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,
215
242
  return;
216
243
  }
217
244
  await fail(error);
218
- });
245
+ }).finally(async function releaseProcessResources(){await onExit?.();});
219
246
  void processObservation.catch(function observeProcessCallbackFailure(error){
220
247
  current={...current,state:'error',available:false,error:errorRecord(error)};
221
248
  settleReady(error);
@@ -250,7 +277,10 @@ export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,
250
277
  });
251
278
  }
252
279
 
253
- startupTask=start().catch(fail);
280
+ startupTask=start().catch(fail).finally(async function releaseUnstartedResources(){
281
+ resolveManagement(false);
282
+ if(!processTask)await onExit?.();
283
+ });
254
284
  void startupTask.catch(function observeStartupCallbackFailure(error){
255
285
  current={...current,state:'error',available:false,error:errorRecord(error)};
256
286
  settleReady(error);
@@ -258,5 +288,5 @@ export function createLocalAIServer({id,configuration={},runtime,appRoot,signal,
258
288
  operationSignal.addEventListener('abort',observeLifetimeAbort,{once:true});
259
289
  if(operationSignal.aborted)observeLifetimeAbort();
260
290
 
261
- return {ready,url,close,get owned(){return owned;},get current(){return current;}};
291
+ return {ready,management,url,close,get owned(){return owned;},get external(){return external;},get current(){return current;}};
262
292
  }
@@ -155,8 +155,8 @@ export function coreEntrySource(application, version, services, {localAI, runtim
155
155
  const imageSelected = localAI?.runtimes?.some(function selectedImage(requirement) {
156
156
  return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
157
157
  }) ?? false;
158
- const modelAssetsSelected = imageSelected || (localAI?.runtimes?.some(function selectedONNX(requirement) {
159
- return (typeof requirement === 'string' ? requirement : requirement.id) === 'onnx';
158
+ const modelAssetsSelected = imageSelected || (localAI?.runtimes?.some(function selectedProjectedRuntime(requirement) {
159
+ return ['onnx', 'llama.cpp'].includes(typeof requirement === 'string' ? requirement : requirement.id);
160
160
  }) ?? false);
161
161
  const whisperSelected = localAI?.runtimes?.some(function selectedWhisper(requirement) {
162
162
  return (typeof requirement === 'string' ? requirement : requirement.id) === 'whisper.cpp';
@@ -199,13 +199,21 @@ export function coreEntrySource(application, version, services, {localAI, runtim
199
199
  ' register(packagedWeb);');
200
200
  }
201
201
  return [
202
- "import {fileURLToPath} from 'node:url';",
202
+ "import {fileURLToPath, pathToFileURL} from 'node:url';",
203
203
  "import path from 'node:path';",
204
204
  "import {isSea} from 'node:sea';",
205
205
  ...(launchDefaults === undefined ? [] : ["import {mkdir} from 'node:fs/promises';"]),
206
206
  "import {readCoreLaunchContext, startCoreHost, startCoreListener, runSharedCoreHost, startSharedCoreBridge} from 'arcane-os/core/host';",
207
207
  "import {createModelObservationService} from 'arcane-os/core/model-observation';",
208
208
  '',
209
+ "const appEntryFlag = process.argv.indexOf('--arcane-app-entry', 2);",
210
+ 'if (appEntryFlag !== -1) {',
211
+ ' const appEntry = process.argv[appEntryFlag + 1];',
212
+ " if (appEntry === undefined || appEntry === '') throw new TypeError('--arcane-app-entry requires an app-relative module path.');",
213
+ " const appRoot = fileURLToPath(new URL('../app/', import.meta.url));",
214
+ ' await import(pathToFileURL(path.resolve(appRoot, appEntry)).href);',
215
+ '} else {',
216
+ ...[
209
217
  ...(localAI === undefined ? [] : [
210
218
  '// Bundled runtime paths are artifact-root-relative POSIX paths.',
211
219
  'function runtimePath(relative) {',
@@ -366,6 +374,8 @@ export function coreEntrySource(application, version, services, {localAI, runtim
366
374
  ' }',
367
375
  " process.off('SIGINT', closeCore);",
368
376
  " process.off('SIGTERM', closeCore);",
377
+ '}'
378
+ ].map(function indentOrdinaryCore(line) { return line ? ` ${line}` : line; }),
369
379
  '}',
370
380
  ''
371
381
  ].join('\n');
@@ -56,20 +56,33 @@ export async function readGitText(args, {
56
56
  }
57
57
  }
58
58
 
59
- /** Capture a caller-selected operation target without changing its strings. */
59
+ /** Derive a Git argument only when the caller explicitly selects a local base. */
60
+ export function captureRepositoryRemote(remote, remoteBase) {
61
+ if (remoteBase === undefined) return remote;
62
+ if (!is.string(remoteBase) || !remoteBase.isWellFormed() || remoteBase.includes('\0')) {
63
+ throw new TypeError('remoteBase must be text representable as a native filesystem path.');
64
+ }
65
+ return remote === undefined ? undefined : path.resolve(remoteBase, remote);
66
+ }
67
+
68
+ /** Keep original selection text separate from the accepted command argument. */
60
69
  export function captureRepositoryTarget(target) {
61
70
  if (target === undefined) return undefined;
62
- const {remote, ref} = target;
71
+ const {remote, ref, remoteBase} = target;
63
72
  for (const [name, value] of Object.entries({remote, ref})) {
64
73
  if (!is.string(value) || value === '' || !value.isWellFormed() || value.includes('\0')) {
65
74
  throw new TypeError(`target.${name} must be nonempty text representable as a native process argument.`);
66
75
  }
67
76
  }
68
- return {remote, ref};
77
+ return {
78
+ target: {remote, ref, ...(remoteBase === undefined ? {} : {remoteBase})},
79
+ remoteArgument: captureRepositoryRemote(remote, remoteBase)
80
+ };
69
81
  }
70
82
 
71
- export function repositoryPushArguments(target) {
72
- return target === undefined ? ['push'] : ['push', '--no-follow-tags', '--', target.remote, `HEAD:${target.ref}`];
83
+ export function repositoryPushArguments(selection) {
84
+ return selection === undefined ? ['push']
85
+ : ['push', '--no-follow-tags', '--', selection.remoteArgument, `HEAD:${selection.target.ref}`];
73
86
  }
74
87
 
75
88
  /** Observe an existing workspace without initializing or refreshing it. */
@@ -167,6 +180,12 @@ export async function repositoryPull({
167
180
  }={}){
168
181
  throwIfAborted(signal);
169
182
  const selectedTarget=captureRepositoryTarget(target);
183
+ return pullRepositoryTarget(selectedTarget,{workspaceRoot,signal,onEvent,run});
184
+ }
185
+
186
+ /** Internal workspace path: its target was already captured before queueing. */
187
+ export async function pullRepositoryTarget(selectedTarget,{workspaceRoot,signal,onEvent,run=runProcess}){
188
+ throwIfAborted(signal);
170
189
  const before=await repositoryStatus({workspaceRoot,signal,onEvent,run});
171
190
  if(!before.clean){
172
191
  throw new ArcaneError(
@@ -176,14 +195,14 @@ export async function repositoryPull({
176
195
  );
177
196
  }
178
197
  const args=selectedTarget===undefined?['pull','--ff-only']
179
- :['pull','--ff-only','--',selectedTarget.remote,`${selectedTarget.ref}:`];
198
+ :['pull','--ff-only','--',selectedTarget.remoteArgument,`${selectedTarget.target.ref}:`];
180
199
  const result=await git(args,{cwd:workspaceRoot,signal,onEvent,run});
181
200
  return {
182
201
  action:'pull',
183
202
  repositoryRoot:before.repositoryRoot,
184
203
  branch:before.branch,
185
204
  output:result.stdout.trim(),
186
- ...(selectedTarget===undefined?{}:{target:selectedTarget,stdout:result.stdout,stderr:result.stderr})
205
+ ...(selectedTarget===undefined?{}:{target:selectedTarget.target,stdout:result.stdout,stderr:result.stderr})
187
206
  };
188
207
  }
189
208
 
@@ -196,6 +215,12 @@ export async function repositoryPush({
196
215
  }={}){
197
216
  throwIfAborted(signal);
198
217
  const selectedTarget=captureRepositoryTarget(target);
218
+ return pushRepositoryTarget(selectedTarget,{workspaceRoot,signal,onEvent,run});
219
+ }
220
+
221
+ /** Internal workspace path: its target was already captured before queueing. */
222
+ export async function pushRepositoryTarget(selectedTarget,{workspaceRoot,signal,onEvent,run=runProcess}){
223
+ throwIfAborted(signal);
199
224
  const before=await repositoryStatus({workspaceRoot,signal,onEvent,run});
200
225
  if(!before.branch){
201
226
  throw new ArcaneError(
@@ -209,7 +234,7 @@ export async function repositoryPush({
209
234
  repositoryRoot:before.repositoryRoot,
210
235
  branch:before.branch,
211
236
  output:(result.stdout||result.stderr).trim(),
212
- ...(selectedTarget===undefined?{}:{target:selectedTarget,stdout:result.stdout,stderr:result.stderr})
237
+ ...(selectedTarget===undefined?{}:{target:selectedTarget.target,stdout:result.stdout,stderr:result.stderr})
213
238
  };
214
239
  }
215
240
 
@@ -85,15 +85,96 @@ async function removeTemporaryTree(location){
85
85
  await rm(location,{recursive:true,force:true}).catch(()=>{});
86
86
  }
87
87
 
88
+ async function materializeBrowserDecisions(destination, signal, onEvent) {
89
+ const transformers = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/';
90
+ const onnx = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.31.0-dev.20260914-8d85527a0/';
91
+ const files = [
92
+ {name: 'transformers.min.js', url: `${transformers}dist/transformers.min.js`},
93
+ {name: 'ort-wasm-simd-threaded.asyncify.mjs', url: `${onnx}dist/ort-wasm-simd-threaded.asyncify.mjs`},
94
+ {name: 'ort-wasm-simd-threaded.asyncify.wasm', url: `${onnx}dist/ort-wasm-simd-threaded.asyncify.wasm`},
95
+ {name: 'TRANSFORMERS-LICENSE', url: `${transformers}LICENSE`},
96
+ {name: 'ONNX-RUNTIME-LICENSE', url: `${onnx}LICENSE`}
97
+ ];
98
+ const controller = new AbortController();
99
+ function cancelDistribution() {
100
+ controller.abort(signal.reason);
101
+ }
102
+ signal?.addEventListener('abort', cancelDistribution, {once: true});
103
+ if (signal?.aborted) cancelDistribution();
104
+ let completed = 0;
105
+ try {
106
+ throwIfAborted(controller.signal);
107
+ await emit(
108
+ onEvent,
109
+ {type: 'workspace.decisions.started', completed, total: files.length}
110
+ );
111
+ throwIfAborted(controller.signal);
112
+ await mkdir(destination, {recursive: true});
113
+ async function acquireDistributionFile(file) {
114
+ try {
115
+ throwIfAborted(controller.signal);
116
+ const response = await fetch(file.url, {signal: controller.signal});
117
+ const content = new Uint8Array(await response.arrayBuffer());
118
+ if (!response.ok) {
119
+ const error = new Error(`Browser decision distribution returned HTTP ${response.status} ${response.statusText}.`);
120
+ error.code = 'ARCANE_DECISION_DISTRIBUTION_DOWNLOAD_FAILED';
121
+ error.response = {
122
+ url: response.url,
123
+ status: response.status,
124
+ statusText: response.statusText,
125
+ headers: [...response.headers],
126
+ content
127
+ };
128
+ throw error;
129
+ }
130
+ throwIfAborted(controller.signal);
131
+ // Upstream distribution content is opaque, including its own
132
+ // imports and license text. No SDK source rewriting applies.
133
+ await writeFile(path.join(destination, file.name), content);
134
+ throwIfAborted(controller.signal);
135
+ completed += 1;
136
+ await emit(
137
+ onEvent,
138
+ {type: 'workspace.decisions.progress', file: file.name, url: file.url, completed, total: files.length}
139
+ );
140
+ } catch (error) {
141
+ controller.abort(error);
142
+ throw error;
143
+ }
144
+ }
145
+ // One fixed distribution, acquired once for the selected projection;
146
+ // no model/app/architecture multiplier or installation script.
147
+ const outcomes = await Promise.allSettled(files.map(acquireDistributionFile));
148
+ const failures = new Set();
149
+ for (const outcome of outcomes) {
150
+ if (outcome.status === 'rejected') failures.add(outcome.reason);
151
+ }
152
+ if (failures.size === 1) throw [...failures][0];
153
+ if (failures.size > 1) {
154
+ throw new AggregateError([...failures], 'Browser decision distribution acquisition failed.');
155
+ }
156
+ throwIfAborted(controller.signal);
157
+ } finally {
158
+ signal?.removeEventListener('abort', cancelDistribution);
159
+ }
160
+ return {
161
+ transformersVersion: '4.3.0',
162
+ onnxRuntimeVersion: '1.31.0-dev.20260914-8d85527a0',
163
+ files: files.map(function distributionFilename(file) { return file.name; })
164
+ };
165
+ }
166
+
88
167
  export async function materializeWorkspaceRuntimeContent({
89
168
  workspaceRoot,
90
169
  runtimeRoot=path.join(getSdkRoot(),'runtime'),
91
170
  browserRuntimeRoot=getSdkBrowserRuntimeRoot(),
92
171
  sdkVersion=SDK_VERSION,
172
+ browserDecisions=false,
93
173
  signal,
94
174
  onEvent
95
175
  }={}){
96
176
  if(!workspaceRoot)fail('workspaceRoot is required to materialize a workspace runtime.');
177
+ if(!is.boolean(browserDecisions))throw new TypeError('browserDecisions must be a boolean.');
97
178
  throwIfAborted(signal);
98
179
  const workspace=await realDirectory(workspaceRoot,'Workspace root');
99
180
  const runtime=await realDirectory(runtimeRoot,'SDK runtime root');
@@ -109,6 +190,7 @@ export async function materializeWorkspaceRuntimeContent({
109
190
  const backupRoot=path.join(workspace,`${BACKUP_PREFIX}${token}`);
110
191
  let backedUp=false;
111
192
  let promoted=false;
193
+ let decisionDistribution;
112
194
 
113
195
  await mkdir(stagingRoot);
114
196
  try{
@@ -134,6 +216,14 @@ export async function materializeWorkspaceRuntimeContent({
134
216
  );
135
217
  }
136
218
 
219
+ if (browserDecisions) {
220
+ decisionDistribution = await materializeBrowserDecisions(
221
+ path.join(sdkDestination, 'ai', 'decisions-runtime'),
222
+ signal,
223
+ onEvent
224
+ );
225
+ }
226
+
137
227
  throwIfAborted(signal);
138
228
  try{
139
229
  const existing=await lstat(destinationRoot);
@@ -160,7 +250,13 @@ export async function materializeWorkspaceRuntimeContent({
160
250
  return {
161
251
  kind:'arcane-workspace-runtime-content',
162
252
  workspaceRoot:workspace,
163
- runtimeRoot:destinationRoot
253
+ runtimeRoot:destinationRoot,
254
+ ...(decisionDistribution ? {
255
+ browserDecisions: {
256
+ ...decisionDistribution,
257
+ directory: path.join(destinationRoot, 'sdk', 'ai', 'decisions-runtime')
258
+ }
259
+ } : {})
164
260
  };
165
261
  }catch(error){
166
262
  if(promoted)await removeTemporaryTree(destinationRoot);