arcane-os 0.80.0 → 0.82.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 (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +97 -19
  4. package/browser-runtime/ai/dbopfs-model-parts.mjs +12 -5
  5. package/browser-runtime/ai/dbopfs-model-resources.mjs +2 -5
  6. package/browser-runtime/core/app-control.mjs +439 -0
  7. package/docs/reference/ai/browser-speech.md +11 -0
  8. package/docs/reference/ai/browser-wasm.md +21 -10
  9. package/docs/reference/cli.md +9 -1
  10. package/docs/reference/core-native-packaging.md +14 -0
  11. package/docs/reference/inventory/package-api.json +23 -9
  12. package/docs/reference/inventory/runtime-components.json +2 -1
  13. package/docs/reference/native-app-control.md +147 -0
  14. package/docs/reference/native-decisions.md +74 -16
  15. package/docs/reference/runtime-components.md +20 -0
  16. package/docs/reference/sdk-api.md +49 -6
  17. package/package.json +2 -1
  18. package/runtime/arcane/components/chat.html +140 -18
  19. package/runtime/arcane/modules/AI.js +10 -5
  20. package/src/cli/app-control.mjs +106 -0
  21. package/src/cli/main.mjs +5 -0
  22. package/src/core/app-control.mjs +23 -0
  23. package/src/core/hosts/windows/ArcaneAppControl.cs +525 -0
  24. package/src/core/hosts/windows/ArcaneCoreProcess.cs +2 -63
  25. package/src/core/hosts/windows/ArcaneFrameTransport.cs +80 -0
  26. package/src/core/hosts/windows/ArcaneHost.cs +45 -7
  27. package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
  28. package/src/core/hosts/windows/ArcaneWindowControl.cs +248 -0
  29. package/src/core/services/decisions.mjs +16 -2
  30. package/src/core/shared-host.mjs +7 -91
  31. package/src/core/socket-connection.mjs +109 -0
  32. package/src/local-ai/decision-tokenizer-worker.mjs +1 -1
  33. package/src/local-ai/decision-tokenizer.mjs +2 -2
  34. package/src/local-ai/decisions.mjs +33 -10
@@ -0,0 +1,80 @@
1
+ using System;
2
+ using System.Globalization;
3
+ using System.IO;
4
+ using System.Text;
5
+ using System.Threading.Tasks;
6
+
7
+ namespace Arcane.Core.Hosts.Windows
8
+ {
9
+ /// <summary>The shared native transport framing; callers own stream lifetime and ordered writes.</summary>
10
+ internal static class ArcaneFrameTransport
11
+ {
12
+ private static readonly Encoding Utf8 = new UTF8Encoding(false, true);
13
+ private static readonly Encoding Ascii = Encoding.GetEncoding(
14
+ "us-ascii", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
15
+
16
+ internal static async Task WriteAsync(Stream stream, string json)
17
+ {
18
+ byte[] body = Utf8.GetBytes(json);
19
+ // Length belongs only to the Content-Length transport frame.
20
+ byte[] header = Ascii.GetBytes("Content-Length: " + body.Length.ToString(CultureInfo.InvariantCulture) + "\r\n\r\n");
21
+ await stream.WriteAsync(header, 0, header.Length).ConfigureAwait(false);
22
+ await stream.WriteAsync(body, 0, body.Length).ConfigureAwait(false);
23
+ await stream.FlushAsync().ConfigureAwait(false);
24
+ }
25
+
26
+ internal static async Task<string> ReadAsync(Stream stream)
27
+ {
28
+ using (MemoryStream header = new MemoryStream())
29
+ using (MemoryStream body = new MemoryStream())
30
+ {
31
+ try
32
+ {
33
+ byte[] next = new byte[1];
34
+ byte[] separator = new byte[] { 13, 10, 13, 10 };
35
+ int matched = 0;
36
+ while (matched != separator.Length)
37
+ {
38
+ int read = await stream.ReadAsync(next, 0, 1).ConfigureAwait(false);
39
+ if (read == 0)
40
+ {
41
+ if (header.Length == 0) return null;
42
+ throw new EndOfStreamException("Core transport ended during a frame header.");
43
+ }
44
+ header.WriteByte(next[0]);
45
+ matched = next[0] == separator[matched] ? matched + 1 : next[0] == 13 ? 1 : 0;
46
+ }
47
+ byte[] headerBytes = header.ToArray();
48
+ string headerText = Ascii.GetString(headerBytes, 0, headerBytes.Length - separator.Length);
49
+ long? contentLength = null;
50
+ foreach (string line in headerText.Split(new string[] { "\r\n" }, StringSplitOptions.None))
51
+ {
52
+ if (!line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) continue;
53
+ long length;
54
+ if (contentLength.HasValue || !Int64.TryParse(line.Substring(15).Trim(), NumberStyles.None,
55
+ CultureInfo.InvariantCulture, out length))
56
+ throw new InvalidDataException("Core frame Content-Length is invalid.");
57
+ contentLength = length;
58
+ }
59
+ if (!contentLength.HasValue) throw new InvalidDataException("Core frame Content-Length is missing.");
60
+ long remaining = contentLength.Value;
61
+ byte[] buffer = new byte[8192];
62
+ while (remaining != 0)
63
+ {
64
+ int read = await stream.ReadAsync(buffer, 0, (int)Math.Min(remaining, buffer.Length)).ConfigureAwait(false);
65
+ if (read == 0) throw new EndOfStreamException("Core transport ended during a frame body.");
66
+ body.Write(buffer, 0, read);
67
+ remaining -= read;
68
+ }
69
+ return Utf8.GetString(body.ToArray());
70
+ }
71
+ catch (Exception error)
72
+ {
73
+ error.Data["frameHeader"] = header.ToArray();
74
+ error.Data["frameBody"] = body.ToArray();
75
+ throw;
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
@@ -16,6 +16,7 @@ namespace Arcane.Core.Hosts.Windows
16
16
  /// <summary>Selected by the application launcher; no OS layout is inferred.</summary>
17
17
  public sealed class ArcaneHostOptions
18
18
  {
19
+ public string ApplicationId { get; set; }
19
20
  public string ApplicationRoot { get; set; }
20
21
  public string StartPath { get; set; }
21
22
  public string OriginHost { get; set; }
@@ -23,6 +24,8 @@ namespace Arcane.Core.Hosts.Windows
23
24
  public string Title { get; set; }
24
25
  public string IconPath { get; set; }
25
26
  public string ClassicClientSource { get; set; }
27
+ public string AppControlEndpoint { get; set; }
28
+ public string AppControlSource { get; set; }
26
29
  public string CoreExecutable { get; set; }
27
30
  public string CoreArguments { get; set; }
28
31
  public string CoreWorkingDirectory { get; set; }
@@ -91,7 +94,7 @@ namespace Arcane.Core.Hosts.Windows
91
94
  /// to retired documents; errors stay outside ordinary application content.
92
95
  /// Callbacks must not synchronously wait for Ready, Completion or CloseAsync.
93
96
  /// </summary>
94
- public sealed class ArcaneHostForm : Form
97
+ public sealed partial class ArcaneHostForm : Form
95
98
  {
96
99
  private const string CancelRendererRequests = "{\"protocol\":\"arcane/1\",\"type\":\"control\",\"control\":\"requests.cancelAll\"}";
97
100
  private readonly ArcaneHostOptions options;
@@ -124,6 +127,7 @@ namespace Arcane.Core.Hosts.Windows
124
127
  private bool started;
125
128
  private bool documentReady;
126
129
  private bool deliveryScheduled;
130
+ private bool browserProcessExited;
127
131
  private bool closing;
128
132
  private bool closeAllowed;
129
133
 
@@ -212,6 +216,7 @@ namespace Arcane.Core.Hosts.Windows
212
216
  string profile = Path.GetFullPath(options.ProfileDirectory);
213
217
  Uri origin = new UriBuilder(Uri.UriSchemeHttps, options.OriginHost).Uri;
214
218
  Uri start = new Uri(origin, options.StartPath);
219
+ StartAppControl();
215
220
  iconLoading = LoadApplicationIconAsync();
216
221
  iconLoading.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
217
222
  core = ArcaneCoreProcess.Start(options.CoreExecutable, options.CoreArguments,
@@ -227,7 +232,13 @@ namespace Arcane.Core.Hosts.Windows
227
232
  InstallDocumentBridge(generation);
228
233
  // The selected generator uses replayRuntimeState:true. Both RPC
229
234
  // installation and the shared event owner remain SDK-owned.
230
- injectedScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(options.ClassicClientSource);
235
+ Task scripts = Task.WhenAll(InstallClassicClientAsync(browser), InstallAppControlAsync(browser));
236
+ try { await scripts; }
237
+ catch
238
+ {
239
+ if (scripts.Exception != null) throw scripts.Exception;
240
+ throw;
241
+ }
231
242
  if (closing) return;
232
243
  browser.NavigationStarting += NavigationStarting;
233
244
  browser.NavigationCompleted += NavigationCompleted;
@@ -243,6 +254,11 @@ namespace Arcane.Core.Hosts.Windows
243
254
  finally { initialized.TrySetResult(null); }
244
255
  }
245
256
 
257
+ private async Task InstallClassicClientAsync(CoreWebView2 browser)
258
+ {
259
+ injectedScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(options.ClassicClientSource);
260
+ }
261
+
246
262
  private void NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs args)
247
263
  {
248
264
  if (nextGeneration != 0 && navigationId == args.NavigationId) return;
@@ -551,11 +567,30 @@ namespace Arcane.Core.Hosts.Windows
551
567
  {
552
568
  error = new AggregateException("Reading WebView2 process-failure diagnostics failed.", error, diagnosticError);
553
569
  }
554
- // Only loss of the browser or top-level renderer ends this bridge.
570
+ if (kind == CoreWebView2ProcessFailedKind.BrowserProcessExited)
571
+ {
572
+ // WebView2 has closed this browser. Complete the owned host
573
+ // lifetime without trying to notify its vanished document.
574
+ browserProcessExited = true;
575
+ lock (stateLock)
576
+ {
577
+ if (transportFailure == null) transportFailure = error;
578
+ }
579
+ Report(error);
580
+ ready.TrySetException(error);
581
+ if (bridge != null) bridge.Stop(error);
582
+ if (activeBridge != null) activeBridge.Stop(error);
583
+ BeginClose();
584
+ return;
585
+ }
586
+ // Loss of the top-level renderer ends its bridge.
555
587
  // GPU/utility/plugin helpers recover independently; subframe loss,
556
588
  // unresponsiveness and unspecified failures remain full diagnostics.
557
- if (kind == CoreWebView2ProcessFailedKind.BrowserProcessExited
558
- || kind == CoreWebView2ProcessFailedKind.RenderProcessExited) CoreFailed(error);
589
+ if (kind == CoreWebView2ProcessFailedKind.RenderProcessExited)
590
+ {
591
+ AppControlRendererExited();
592
+ CoreFailed(error);
593
+ }
559
594
  else Report(error);
560
595
  }
561
596
 
@@ -665,7 +700,7 @@ namespace Arcane.Core.Hosts.Windows
665
700
 
666
701
  private void NotifyTransportFailure()
667
702
  {
668
- if (!documentReady || webView.CoreWebView2 == null || transportFailure == null || closing) return;
703
+ if (closing || browserProcessExited || !documentReady || webView.CoreWebView2 == null || transportFailure == null) return;
669
704
  if (failureNotifiedGeneration == generation) return;
670
705
  failureNotifiedGeneration = generation;
671
706
  Task failureNotification = NotifyTransportFailureAsync(transportFailure);
@@ -711,6 +746,7 @@ namespace Arcane.Core.Hosts.Windows
711
746
  if (!started) initialized.TrySetResult(null);
712
747
  if (bridge != null) bridge.Stop(new InvalidOperationException("The application window is closing."));
713
748
  if (activeBridge != null) activeBridge.Stop(new InvalidOperationException("The application window is closing."));
749
+ StopAppControl();
714
750
  shutdown = DrainAndCloseAsync();
715
751
  shutdown.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
716
752
  }
@@ -737,6 +773,7 @@ namespace Arcane.Core.Hosts.Windows
737
773
  catch (Exception error) { ReportTaskFailure(coreLifetime, error); }
738
774
  }
739
775
  }
776
+ await DrainAppControlAsync();
740
777
  // Joining the independent image owner belongs after Core input has
741
778
  // closed; image I/O must never delay accepted service work draining.
742
779
  if (iconLoading != null)
@@ -761,10 +798,11 @@ namespace Arcane.Core.Hosts.Windows
761
798
  ready.TrySetCanceled();
762
799
  try
763
800
  {
764
- if (webView.CoreWebView2 != null)
801
+ if (!browserProcessExited && webView.CoreWebView2 != null)
765
802
  {
766
803
  webView.CoreWebView2.RemoveHostObjectFromScript("arcaneBridge");
767
804
  if (injectedScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(injectedScript);
805
+ if (appControlScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(appControlScript);
768
806
  }
769
807
  }
770
808
  catch (Exception error) { Report(error); }
@@ -85,8 +85,11 @@ namespace Arcane.Core.Hosts.Windows
85
85
  ? Path.GetFullPath((string)stateRoot)
86
86
  : Path.Combine(defaultStateBase, "Arcane", appId);
87
87
  OpenDiagnostics(Path.Combine(profileParent, "Diagnostics"));
88
+ Dictionary<string, object> appControl = launchContext.ContainsKey("appControl")
89
+ ? RequiredRecord(launchContext, "appControl") : null;
88
90
  ArcaneHostOptions options = new ArcaneHostOptions
89
91
  {
92
+ ApplicationId = appId,
90
93
  ApplicationRoot = Path.GetFullPath(Path.Combine(directory, RequiredString(manifest, "webRoot"))),
91
94
  StartPath = RequiredString(manifest, "start"),
92
95
  OriginHost = "arcane.local",
@@ -97,6 +100,8 @@ namespace Arcane.Core.Hosts.Windows
97
100
  Resizable = OptionalWindowResizable(windowOptions),
98
101
  IconPath = String.IsNullOrEmpty(icon) ? null : Path.Combine(directory, icon),
99
102
  ClassicClientSource = File.ReadAllText(Path.Combine(directory, RequiredString(client, "source"))),
103
+ AppControlEndpoint = appControl == null ? null : RequiredString(appControl, "endpoint"),
104
+ AppControlSource = appControl == null ? null : File.ReadAllText(Path.Combine(directory, "arcane-app-control.js")),
100
105
  CoreExecutable = Path.Combine(directory, "runtime", "ArcaneCore.exe"),
101
106
  // Forward the actual host selection separately; Core keeps
102
107
  // every explicit launch-record field unchanged.
@@ -0,0 +1,248 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.IO;
4
+ using System.Threading;
5
+ using System.Threading.Tasks;
6
+ using Microsoft.Web.WebView2.Core;
7
+
8
+ namespace Arcane.Core.Hosts.Windows
9
+ {
10
+ // The form owns UI-thread dispatch and document lifetime. The pipe owner
11
+ // handles only connections and framed requests, never another Core child.
12
+ public sealed partial class ArcaneHostForm
13
+ {
14
+ private const string AppControlDocumentKey = "Symbol.for('arcane-os.app-control.document')";
15
+ private readonly HashSet<Task> appControlDocumentTasks = new HashSet<Task>();
16
+ private ArcaneAppControl appControl;
17
+ private Task appControlClosing;
18
+ private string appControlScript;
19
+ private string appControlDocument;
20
+ private long appControlGeneration;
21
+ private long appControlDocumentEpoch;
22
+ private ulong appControlNavigationId;
23
+ private bool appControlNavigating;
24
+
25
+ private void StartAppControl()
26
+ {
27
+ if (options.AppControlEndpoint == null) return;
28
+ if (String.IsNullOrEmpty(options.AppControlSource))
29
+ throw new ArgumentException("Supply the SDK-generated app-control source for the selected endpoint.");
30
+ appControl = new ArcaneAppControl(options.AppControlEndpoint, DispatchAppControl, DeliverDiagnostic, Report);
31
+ appControl.Completion.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
32
+ }
33
+
34
+ private async Task InstallAppControlAsync(CoreWebView2 browser)
35
+ {
36
+ if (appControl == null) return;
37
+ appControlScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(
38
+ "globalThis[" + AppControlDocumentKey + "] = globalThis.crypto.randomUUID();");
39
+ if (closing) return;
40
+ browser.NavigationStarting += AppControlNavigationStarting;
41
+ browser.NavigationCompleted += AppControlNavigationCompleted;
42
+ }
43
+
44
+ private void StopAppControl()
45
+ {
46
+ if (appControl == null || appControlClosing != null) return;
47
+ appControlClosing = appControl.CloseAsync();
48
+ }
49
+
50
+ private async Task DrainAppControlAsync()
51
+ {
52
+ // Ingress stops in BeginClose. Join only after the existing Core
53
+ // cancellation/close has started, with the UI pump still alive.
54
+ if (appControlClosing != null)
55
+ {
56
+ try { await appControlClosing; }
57
+ catch (Exception error) { ReportTaskFailure(appControlClosing, error); }
58
+ }
59
+ Task documents = Task.WhenAll(appControlDocumentTasks);
60
+ try { await documents; }
61
+ catch (Exception error) { ReportTaskFailure(documents, error); }
62
+ }
63
+
64
+ private void AppControlNavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs args)
65
+ {
66
+ appControlNavigationId = args.NavigationId;
67
+ appControlDocumentEpoch++;
68
+ appControlNavigating = true;
69
+ }
70
+
71
+ private void AppControlNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs args)
72
+ {
73
+ if (closing || browserProcessExited || args.NavigationId != appControlNavigationId) return;
74
+ // A cancelled navigation may leave the old document alive. Read its
75
+ // actual creation marker instead of treating every attempt as new.
76
+ Task task = RefreshAppControlDocumentAsync(args.NavigationId, appControlDocumentEpoch);
77
+ appControlDocumentTasks.Add(task);
78
+ task.ContinueWith(AppControlDocumentSettled, CancellationToken.None,
79
+ TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
80
+ }
81
+
82
+ private void AppControlRendererExited()
83
+ {
84
+ appControlDocumentEpoch++;
85
+ appControlDocument = null;
86
+ appControlNavigating = true;
87
+ }
88
+
89
+ private async Task RefreshAppControlDocumentAsync(ulong selectedNavigation, long selectedEpoch)
90
+ {
91
+ string json = await webView.CoreWebView2.ExecuteScriptAsync("globalThis[" + AppControlDocumentKey + "]");
92
+ if (closing || browserProcessExited || selectedNavigation != appControlNavigationId
93
+ || selectedEpoch != appControlDocumentEpoch) return;
94
+ string current = ArcaneHost.Serializer().DeserializeObject(json) as string;
95
+ if (current == null) throw new InvalidOperationException("The current document has no app-control lifecycle marker.");
96
+ if (current != appControlDocument)
97
+ {
98
+ appControlDocument = current;
99
+ appControlGeneration++;
100
+ }
101
+ appControlNavigating = false;
102
+ }
103
+
104
+ private void AppControlDocumentSettled(Task task)
105
+ {
106
+ appControlDocumentTasks.Remove(task);
107
+ if (task.IsFaulted) ReportTaskFailure(task, task.Exception);
108
+ }
109
+
110
+ private Task<object> DispatchAppControl(string method, Dictionary<string, object> parameters,
111
+ CancellationToken cancellation)
112
+ {
113
+ AppControlRequest request = new AppControlRequest(this, method, parameters, cancellation);
114
+ try
115
+ {
116
+ // The form's owned message pump stays alive through control and
117
+ // Core drain. No foreground window or desktop input is selected.
118
+ BeginInvoke(new Action(request.Run));
119
+ }
120
+ catch (Exception error) { request.Completion.TrySetException(error); }
121
+ return request.Completion.Task;
122
+ }
123
+
124
+ private bool AppControlDocumentReady()
125
+ {
126
+ return !closing && !browserProcessExited && !appControlNavigating
127
+ && appControlDocument != null && webView.CoreWebView2 != null;
128
+ }
129
+
130
+ private Dictionary<string, object> AppControlStatus()
131
+ {
132
+ return new Dictionary<string, object>
133
+ {
134
+ { "app", new Dictionary<string, object> { { "id", options.ApplicationId }, { "name", options.Title } } },
135
+ { "platform", "windows" }, { "host", "webview2" },
136
+ { "url", webView.Source == null ? null : webView.Source.AbsoluteUri },
137
+ { "documentGeneration", appControlGeneration }, { "ready", AppControlDocumentReady() },
138
+ { "window", new Dictionary<string, object>
139
+ { { "title", Text }, { "state", WindowState.ToString() },
140
+ { "width", ClientSize.Width }, { "height", ClientSize.Height } } },
141
+ { "operations", new string[] { "status", "inspect", "capture", "act" } }
142
+ };
143
+ }
144
+
145
+ private async Task<object> ExecuteAppControlAsync(string method, Dictionary<string, object> parameters,
146
+ CancellationToken cancellation)
147
+ {
148
+ cancellation.ThrowIfCancellationRequested();
149
+ if (method == "app.control.status") return AppControlStatus();
150
+ if (method != "app.control.inspect" && method != "app.control.capture" && method != "app.control.act")
151
+ throw new ArgumentException("Unknown app-control method: " + method);
152
+ if (!AppControlDocumentReady()) throw new InvalidOperationException("The app document is not ready for control.");
153
+ long selectedGeneration = appControlGeneration;
154
+ string selectedDocument = appControlDocument;
155
+ string selectedUrl = webView.Source == null ? null : webView.Source.AbsoluteUri;
156
+ object expected;
157
+ if (method == "app.control.act" && !parameters.ContainsKey("documentGeneration"))
158
+ throw new ArgumentException("An action requires the documentGeneration returned by status or inspect.");
159
+ if (parameters.TryGetValue("documentGeneration", out expected)
160
+ && (!(expected is int || expected is long || expected is decimal || expected is double)
161
+ || Convert.ToDouble(expected) != selectedGeneration))
162
+ throw new InvalidOperationException("The selected app document has been replaced.");
163
+
164
+ object result;
165
+ if (method == "app.control.capture")
166
+ {
167
+ using (MemoryStream image = new MemoryStream())
168
+ {
169
+ await webView.CoreWebView2.CapturePreviewAsync(CoreWebView2CapturePreviewImageFormat.Png, image);
170
+ result = new Dictionary<string, object>
171
+ {
172
+ { "mimeType", "image/png" }, { "data", Convert.ToBase64String(image.ToArray()) }
173
+ };
174
+ }
175
+ }
176
+ else
177
+ {
178
+ string operation = method == "app.control.inspect" ? "inspect" : "act";
179
+ // The creation marker is checked inside the executing document.
180
+ // Native generation checks alone cannot stop a queued script
181
+ // from reaching a successor document during navigation.
182
+ string script = "(function(expected,operation,parameters){"
183
+ + "if(globalThis[" + AppControlDocumentKey + "]!==expected)"
184
+ + "return {ok:false,error:{code:'APP_CONTROL_DOCUMENT_REPLACED',message:'The selected app document has been replaced.'}};"
185
+ + "return (" + options.AppControlSource + ")(operation,parameters);})("
186
+ + ArcaneHost.Serializer().Serialize(selectedDocument) + ","
187
+ + ArcaneHost.Serializer().Serialize(operation) + ","
188
+ + ArcaneHost.Serializer().Serialize(parameters) + ")";
189
+ string json = await webView.CoreWebView2.ExecuteScriptAsync(script);
190
+ Dictionary<string, object> envelope = ArcaneHost.Serializer().DeserializeObject(json) as Dictionary<string, object>;
191
+ object ok;
192
+ if (envelope == null || !envelope.TryGetValue("ok", out ok) || !(ok is bool))
193
+ throw new InvalidDataException("The document returned no app-control result.");
194
+ if (!(bool)ok)
195
+ {
196
+ object detail;
197
+ envelope.TryGetValue("error", out detail);
198
+ IOException error = new IOException("The document app-control operation failed.");
199
+ error.Data["documentError"] = detail;
200
+ throw error;
201
+ }
202
+ if (!envelope.TryGetValue("result", out result))
203
+ throw new InvalidDataException("The document omitted the app-control result.");
204
+ }
205
+ // Completion is observed after dispatch even if a caller cancelled.
206
+ // A document change is reported with the actual returned result; it
207
+ // neither retries an action nor claims its effects were reversed.
208
+ if (!AppControlDocumentReady() || appControlGeneration != selectedGeneration || appControlDocument != selectedDocument)
209
+ {
210
+ IOException error = new IOException("The app document changed while the operation was completing.");
211
+ error.Data["result"] = result;
212
+ error.Data["documentGeneration"] = selectedGeneration;
213
+ error.Data["url"] = selectedUrl;
214
+ throw error;
215
+ }
216
+ Dictionary<string, object> record = result as Dictionary<string, object>;
217
+ if (record == null) throw new InvalidDataException("The app-control result must be an object.");
218
+ record["documentGeneration"] = selectedGeneration;
219
+ record["url"] = webView.Source == null ? null : webView.Source.AbsoluteUri;
220
+ return record;
221
+ }
222
+
223
+ private sealed class AppControlRequest
224
+ {
225
+ private readonly ArcaneHostForm window;
226
+ private readonly string method;
227
+ private readonly Dictionary<string, object> parameters;
228
+ private readonly CancellationToken cancellation;
229
+ internal readonly TaskCompletionSource<object> Completion = new TaskCompletionSource<object>();
230
+
231
+ internal AppControlRequest(ArcaneHostForm window, string method,
232
+ Dictionary<string, object> parameters, CancellationToken cancellation)
233
+ {
234
+ this.window = window;
235
+ this.method = method;
236
+ this.parameters = parameters;
237
+ this.cancellation = cancellation;
238
+ }
239
+
240
+ internal async void Run()
241
+ {
242
+ try { Completion.TrySetResult(await window.ExecuteAppControlAsync(method, parameters, cancellation)); }
243
+ catch (OperationCanceledException) { Completion.TrySetCanceled(); }
244
+ catch (Exception error) { Completion.TrySetException(error); }
245
+ }
246
+ }
247
+ }
248
+ }
@@ -10,6 +10,7 @@ function sameExecutionTarget(left, right) {
10
10
 
11
11
  function sameActivation(left, right) {
12
12
  if (!left || !right || !sameExecutionTarget(left.executionTarget, right.executionTarget) || left.assetProjectionId !== right.assetProjectionId) return false;
13
+ if (left.family !== right.family || left.model !== right.model || left.revision !== right.revision || left.dtype !== right.dtype) return false;
13
14
  if (left.resourcePaths === right.resourcePaths) return true;
14
15
  if (!left.resourcePaths || !right.resourcePaths) return false;
15
16
  const entries = Object.entries(left.resourcePaths);
@@ -32,6 +33,10 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
32
33
  let closing;
33
34
  let unsubscribe;
34
35
  let selectedActivation = {
36
+ family: configuration.family === undefined ? 'laya' : configuration.family,
37
+ model: configuration.model === undefined ? 'onnx-community/laya-typed-decisions-ONNX' : configuration.model,
38
+ revision: configuration.revision === undefined ? 'main' : configuration.revision,
39
+ dtype: configuration.dtype === undefined ? 'fp32' : configuration.dtype,
35
40
  executionTarget: configuration.executionTarget,
36
41
  ...(configuration.assetProjectionId == null ? {} : {assetProjectionId: configuration.assetProjectionId, resourcePaths: configuration.resourcePaths && {...configuration.resourcePaths}})
37
42
  };
@@ -40,7 +45,7 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
40
45
 
41
46
  function current() {
42
47
  const snapshot = model?.current() ?? {
43
- family: 'laya', model: configuration.model ?? 'onnx-community/laya-typed-decisions-ONNX',
48
+ family: configuration.family ?? 'laya', model: configuration.model ?? 'onnx-community/laya-typed-decisions-ONNX',
44
49
  revision: configuration.revision ?? 'main', dtype: configuration.dtype ?? 'fp32',
45
50
  state: closing ? 'disposed' : 'unloaded', loaded: false, busy: Boolean(loading),
46
51
  activeRequests: 0, progress: null, error: null, execution: null
@@ -66,12 +71,19 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
66
71
 
67
72
  function operationSignal(signal) { return signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal; }
68
73
 
69
- function load({signal, executionTarget: requestedTarget, assetProjectionId: requestedProjection, resourcePaths: requestedPaths} = {}) {
74
+ function load({
75
+ family: requestedFamily, model: requestedModel, revision: requestedRevision, dtype: requestedDtype,
76
+ signal, executionTarget: requestedTarget, assetProjectionId: requestedProjection, resourcePaths: requestedPaths
77
+ } = {}) {
70
78
  operationSignal(signal).throwIfAborted();
71
79
  if (!context) throw new CoreError({code: 'CORE_NOT_READY', message: 'Register and start the decision service with Core before loading.'});
72
80
  const projection = requestedProjection === undefined ? selectedActivation.assetProjectionId : requestedProjection;
73
81
  const mapping = requestedPaths === undefined ? selectedActivation.resourcePaths : requestedPaths;
74
82
  const selection = {
83
+ family: requestedFamily === undefined ? selectedActivation.family : requestedFamily,
84
+ model: requestedModel === undefined ? selectedActivation.model : requestedModel,
85
+ revision: requestedRevision === undefined ? selectedActivation.revision : requestedRevision,
86
+ dtype: requestedDtype === undefined ? selectedActivation.dtype : requestedDtype,
75
87
  executionTarget: requestedTarget === undefined ? selectedActivation.executionTarget : requestedTarget,
76
88
  ...(projection == null ? {} : {assetProjectionId: projection, resourcePaths: mapping && typeof mapping === 'object' && !Array.isArray(mapping) ? {...mapping} : mapping})
77
89
  };
@@ -116,6 +128,7 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
116
128
  );
117
129
  model = createNativeDecisionModel({
118
130
  ...configuration, paths, onnx: localAI.getONNXRuntime(), modelAssets,
131
+ family: selection.family, model: selection.model, revision: selection.revision, dtype: selection.dtype,
119
132
  executionTarget: selection.executionTarget, assetProjectionId: selection.assetProjectionId,
120
133
  resourcePaths: selection.resourcePaths, signal: lifetime.signal
121
134
  });
@@ -221,6 +234,7 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
221
234
  'decisions.status': current,
222
235
  'decisions.load': function loadRequest(parameters, request) {
223
236
  return load({
237
+ family: parameters?.family, model: parameters?.model, revision: parameters?.revision, dtype: parameters?.dtype,
224
238
  executionTarget: parameters?.executionTarget, assetProjectionId: parameters?.assetProjectionId,
225
239
  resourcePaths: parameters?.resourcePaths, signal: request.signal
226
240
  });