simple-agents-wasm 0.3.6 → 0.3.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-agents-wasm",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Browser-compatible SimpleAgents client for OpenAI-compatible providers",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -24,15 +24,12 @@ export class WasmClient {
24
24
 
25
25
  export function supportsRustWasm(): boolean;
26
26
 
27
- export function toJsArray(value: any): Array<any>;
28
-
29
27
  export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
30
28
 
31
29
  export interface InitOutput {
32
30
  readonly memory: WebAssembly.Memory;
33
31
  readonly __wbg_wasmclient_free: (a: number, b: number) => void;
34
32
  readonly supportsRustWasm: () => number;
35
- readonly toJsArray: (a: any) => any;
36
33
  readonly wasmclient_complete: (a: number, b: number, c: number, d: any, e: number) => any;
37
34
  readonly wasmclient_new: (a: number, b: number, c: any) => [number, number, number];
38
35
  readonly wasmclient_runWorkflowYaml: (a: number, b: number, c: number, d: any) => [number, number, number];
@@ -108,15 +108,6 @@ export function supportsRustWasm() {
108
108
  return ret !== 0;
109
109
  }
110
110
 
111
- /**
112
- * @param {any} value
113
- * @returns {Array<any>}
114
- */
115
- export function toJsArray(value) {
116
- const ret = wasm.toJsArray(value);
117
- return ret;
118
- }
119
-
120
111
  function __wbg_get_imports() {
121
112
  const import0 = {
122
113
  __proto__: null,
@@ -357,13 +348,13 @@ function __wbg_get_imports() {
357
348
  const ret = arg0.next();
358
349
  return ret;
359
350
  }, arguments); },
351
+ __wbg_parse_545d11396395fbbd: function() { return handleError(function (arg0, arg1) {
352
+ const ret = JSON.parse(getStringFromWasm0(arg0, arg1));
353
+ return ret;
354
+ }, arguments); },
360
355
  __wbg_prototypesetcall_3e05eb9545565046: function(arg0, arg1, arg2) {
361
356
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
362
357
  },
363
- __wbg_push_6bdbc990be5ac37b: function(arg0, arg1) {
364
- const ret = arg0.push(arg1);
365
- return ret;
366
- },
367
358
  __wbg_queueMicrotask_abaf92f0bd4e80a4: function(arg0) {
368
359
  const ret = arg0.queueMicrotask;
369
360
  return ret;
Binary file
@@ -3,7 +3,6 @@
3
3
  export const memory: WebAssembly.Memory;
4
4
  export const __wbg_wasmclient_free: (a: number, b: number) => void;
5
5
  export const supportsRustWasm: () => number;
6
- export const toJsArray: (a: any) => any;
7
6
  export const wasmclient_complete: (a: number, b: number, c: number, d: any, e: number) => any;
8
7
  export const wasmclient_new: (a: number, b: number, c: any) => [number, number, number];
9
8
  export const wasmclient_runWorkflowYaml: (a: number, b: number, c: number, d: any) => [number, number, number];
@@ -1,16 +1,34 @@
1
1
  let rustModulePromise;
2
2
 
3
+ function isNodeRuntime() {
4
+ return typeof process !== "undefined" && Boolean(process.versions?.node);
5
+ }
6
+
3
7
  export async function loadRustModule() {
4
8
  if (!rustModulePromise) {
5
9
  rustModulePromise = (async () => {
6
10
  try {
7
11
  const moduleValue = await import("../pkg/simple_agents_wasm.js");
8
12
  const wasmUrl = new URL("../pkg/simple_agents_wasm_bg.wasm", import.meta.url);
9
- await moduleValue.default({ module_or_path: wasmUrl });
13
+ let initArg;
14
+ if (isNodeRuntime()) {
15
+ const [{ readFile }, { fileURLToPath }] = await Promise.all([
16
+ import("node:fs/promises"),
17
+ import("node:url")
18
+ ]);
19
+ initArg = { module_or_path: await readFile(fileURLToPath(wasmUrl)) };
20
+ } else {
21
+ initArg = { module_or_path: wasmUrl };
22
+ }
23
+ await moduleValue.default(initArg);
10
24
  return moduleValue;
11
25
  } catch (error) {
12
- console.debug("[simple-agents-wasm] Rust module unavailable, falling back to JS runtime", error);
13
- return null;
26
+ const reason = error instanceof Error ? error.message : String(error);
27
+ throw new Error(
28
+ `[simple-agents-wasm] Failed to load Rust WASM backend. ` +
29
+ `Build artifacts are required (run "npm run build" in bindings/wasm/simple-agents-wasm). ` +
30
+ `Original error: ${reason}`
31
+ );
14
32
  }
15
33
  })();
16
34
  }
package/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "simple-agents-wasm-rust"
3
- version = "0.3.6"
3
+ version = "0.3.7"
4
4
  edition = "2021"
5
5
  license = "MIT OR Apache-2.0"
6
6
 
package/rust/src/lib.rs CHANGED
@@ -1,6 +1,7 @@
1
1
  use js_sys::{Array, Function, Object, Promise, Reflect};
2
2
  use serde::{Deserialize, Serialize};
3
3
  use serde_json::{json, Map as JsonMap, Value as JsonValue};
4
+ use std::cell::RefCell;
4
5
  use std::collections::HashMap;
5
6
  use wasm_bindgen::prelude::*;
6
7
  use wasm_bindgen::JsCast;
@@ -271,6 +272,27 @@ struct WorkflowRunOptions {
271
272
  trace: Option<JsonValue>,
272
273
  #[serde(skip)]
273
274
  functions_js: Option<JsValue>,
275
+ /// Injected by JS as `__fetchImpl` so HTTP calls use the same fetch as tests (no global race).
276
+ #[serde(skip)]
277
+ fetch_js: Option<JsValue>,
278
+ }
279
+
280
+ struct FetchOverrideGuard<'a> {
281
+ cell: &'a RefCell<Option<JsValue>>,
282
+ previous: Option<JsValue>,
283
+ }
284
+
285
+ impl<'a> FetchOverrideGuard<'a> {
286
+ fn new(cell: &'a RefCell<Option<JsValue>>, value: Option<JsValue>) -> Self {
287
+ let previous = cell.replace(value);
288
+ Self { cell, previous }
289
+ }
290
+ }
291
+
292
+ impl Drop for FetchOverrideGuard<'_> {
293
+ fn drop(&mut self) {
294
+ self.cell.replace(self.previous.take());
295
+ }
274
296
  }
275
297
 
276
298
  fn js_error(message: impl Into<String>) -> JsValue {
@@ -289,6 +311,14 @@ fn config_error(message: impl Into<String>) -> JsValue {
289
311
  .into()
290
312
  }
291
313
 
314
+ /// Plain objects for JS `Function.call` / stream callbacks. `serde_wasm_bindgen::to_value`
315
+ /// can yield empty externref objects in Node for callback arguments; `JSON.parse` matches browser behavior.
316
+ fn json_value_to_js_plain(value: &JsonValue) -> Result<JsValue, JsValue> {
317
+ let s =
318
+ serde_json::to_string(value).map_err(|_| js_error("failed to serialize value for JS"))?;
319
+ js_sys::JSON::parse(&s).map_err(|_| js_error("failed to parse JSON for JS"))
320
+ }
321
+
292
322
  fn now_millis() -> f64 {
293
323
  let global = js_sys::global();
294
324
  let performance = Reflect::get(&global, &JsValue::from_str("performance")).ok();
@@ -463,13 +493,23 @@ fn call_method2_sync(
463
493
  }
464
494
 
465
495
  async fn js_fetch(
496
+ fetch_override: Option<JsValue>,
466
497
  url: &str,
467
498
  body: &JsonValue,
468
499
  headers: &HashMap<String, String>,
469
500
  ) -> Result<JsValue, JsValue> {
470
501
  let global = js_sys::global();
471
- let fetch_value = Reflect::get(&global, &JsValue::from_str("fetch"))
472
- .map_err(|_| js_error("global fetch is unavailable"))?;
502
+ let fetch_value = if let Some(f) = fetch_override {
503
+ if f.is_function() {
504
+ f
505
+ } else {
506
+ Reflect::get(&global, &JsValue::from_str("fetch"))
507
+ .map_err(|_| js_error("global fetch is unavailable"))?
508
+ }
509
+ } else {
510
+ Reflect::get(&global, &JsValue::from_str("fetch"))
511
+ .map_err(|_| js_error("global fetch is unavailable"))?
512
+ };
473
513
  let fetch_fn = fetch_value
474
514
  .dyn_into::<Function>()
475
515
  .map_err(|_| js_error("global fetch is not callable"))?;
@@ -985,6 +1025,7 @@ pub struct WasmClient {
985
1025
  base_url: String,
986
1026
  api_key: String,
987
1027
  headers: HashMap<String, String>,
1028
+ fetch_override: RefCell<Option<JsValue>>,
988
1029
  }
989
1030
 
990
1031
  #[wasm_bindgen]
@@ -1013,6 +1054,7 @@ impl WasmClient {
1013
1054
  base_url: normalize_base_url(&base),
1014
1055
  api_key: parsed.api_key,
1015
1056
  headers: parsed.headers.unwrap_or_default(),
1057
+ fetch_override: RefCell::new(None),
1016
1058
  })
1017
1059
  }
1018
1060
 
@@ -1066,7 +1108,9 @@ impl WasmClient {
1066
1108
  .or_insert_with(|| "https://simpleagents.dev".to_string());
1067
1109
  }
1068
1110
 
1111
+ let fetch_override = self.fetch_override.borrow().clone();
1069
1112
  let response = js_fetch(
1113
+ fetch_override,
1070
1114
  &format!("{}/chat/completions", self.base_url),
1071
1115
  &body,
1072
1116
  &headers,
@@ -1235,7 +1279,9 @@ impl WasmClient {
1235
1279
  .or_insert_with(|| "https://simpleagents.dev".to_string());
1236
1280
  }
1237
1281
 
1282
+ let fetch_override = self.fetch_override.borrow().clone();
1238
1283
  let response = js_fetch(
1284
+ fetch_override,
1239
1285
  &format!("{}/chat/completions", self.base_url),
1240
1286
  &body,
1241
1287
  &headers,
@@ -1260,8 +1306,7 @@ impl WasmClient {
1260
1306
  "eventType": "error",
1261
1307
  "error": { "message": message }
1262
1308
  });
1263
- let event_js = serde_wasm_bindgen::to_value(&err_event)
1264
- .map_err(|_| js_error("failed to serialize stream error event"))?;
1309
+ let event_js = json_value_to_js_plain(&err_event)?;
1265
1310
  on_event
1266
1311
  .call1(&JsValue::NULL, &event_js)
1267
1312
  .map_err(|_| js_error("failed to call stream callback"))?;
@@ -1337,8 +1382,7 @@ impl WasmClient {
1337
1382
  "raw": data,
1338
1383
  }
1339
1384
  });
1340
- let event_js = serde_wasm_bindgen::to_value(&delta_event)
1341
- .map_err(|_| js_error("failed to serialize stream delta event"))?;
1385
+ let event_js = json_value_to_js_plain(&delta_event)?;
1342
1386
  on_event
1343
1387
  .call1(&JsValue::NULL, &event_js)
1344
1388
  .map_err(|_| js_error("failed to call stream callback"))?;
@@ -1348,8 +1392,7 @@ impl WasmClient {
1348
1392
  .await?;
1349
1393
 
1350
1394
  let done_event = json!({ "eventType": "done" });
1351
- let done_js = serde_wasm_bindgen::to_value(&done_event)
1352
- .map_err(|_| js_error("failed to serialize stream done event"))?;
1395
+ let done_js = json_value_to_js_plain(&done_event)?;
1353
1396
  on_event
1354
1397
  .call1(&JsValue::NULL, &done_js)
1355
1398
  .map_err(|_| js_error("failed to call stream callback"))?;
@@ -1396,14 +1439,25 @@ impl WasmClient {
1396
1439
  telemetry: None,
1397
1440
  trace: None,
1398
1441
  functions_js: None,
1442
+ fetch_js: None,
1399
1443
  };
1400
1444
  if let Some(options_js) = workflow_options {
1401
1445
  options = serde_wasm_bindgen::from_value(options_js.clone())
1402
1446
  .map_err(|_| config_error("invalid workflowOptions object"))?;
1403
1447
  let functions_value = Reflect::get(&options_js, &JsValue::from_str("functions")).ok();
1404
- options.functions_js = functions_value;
1448
+ options.functions_js = functions_value.and_then(|v| {
1449
+ if v.is_undefined() || v.is_null() {
1450
+ None
1451
+ } else {
1452
+ Some(v)
1453
+ }
1454
+ });
1455
+ let fetch_js = Reflect::get(&options_js, &JsValue::from_str("__fetchImpl")).ok();
1456
+ options.fetch_js = fetch_js;
1405
1457
  }
1406
1458
 
1459
+ let _fetch_guard = FetchOverrideGuard::new(&self.fetch_override, options.fetch_js.clone());
1460
+
1407
1461
  if raw_doc.get("entry_node").is_some() && raw_doc.get("nodes").is_some() {
1408
1462
  let graph_doc: GraphWorkflowDoc = serde_json::from_value(raw_doc.clone())
1409
1463
  .map_err(|error| config_error(format!("invalid graph workflow YAML: {error}")))?;
@@ -1519,8 +1573,10 @@ impl WasmClient {
1519
1573
  tool_calls: None,
1520
1574
  });
1521
1575
  }
1522
- serde_wasm_bindgen::to_value(&history)
1523
- .map_err(|_| js_error("failed to serialize graph llm messages"))?
1576
+ json_value_to_js_plain(
1577
+ &serde_json::to_value(&history)
1578
+ .map_err(|_| js_error("failed to serialize graph llm messages"))?,
1579
+ )?
1524
1580
  } else {
1525
1581
  JsValue::from_str(&prompt)
1526
1582
  };
@@ -1530,11 +1586,7 @@ impl WasmClient {
1530
1586
  .complete(
1531
1587
  model,
1532
1588
  prompt_js,
1533
- Some(
1534
- serde_wasm_bindgen::to_value(&opts).map_err(|_| {
1535
- js_error("failed to serialize completion options")
1536
- })?,
1537
- ),
1589
+ Some(json_value_to_js_plain(&opts)?),
1538
1590
  )
1539
1591
  .await?;
1540
1592
  let completion: JsonValue = serde_wasm_bindgen::from_value(completion_js)
@@ -1630,17 +1682,23 @@ impl WasmClient {
1630
1682
  .unwrap_or_else(|| handler.clone());
1631
1683
  let functions_js = options.functions_js.clone().ok_or_else(|| {
1632
1684
  config_error(format!(
1633
- "custom_worker node '{}' requires workflowOptions.functions",
1634
- node.id
1685
+ "custom_worker node '{}' requires workflowOptions.functions['{}']",
1686
+ node.id, lookup_key
1635
1687
  ))
1636
1688
  })?;
1637
1689
  let function_value = Reflect::get(&functions_js, &JsValue::from_str(&lookup_key))
1638
1690
  .map_err(|_| {
1639
1691
  config_error(format!(
1640
- "failed to resolve custom worker handler key '{}' from workflowOptions.functions",
1641
- lookup_key
1692
+ "custom_worker node '{}' requires workflowOptions.functions['{}']",
1693
+ node.id, lookup_key
1642
1694
  ))
1643
1695
  })?;
1696
+ if function_value.is_undefined() || function_value.is_null() {
1697
+ return Err(config_error(format!(
1698
+ "custom_worker node '{}' requires workflowOptions.functions['{}']",
1699
+ node.id, lookup_key
1700
+ )));
1701
+ }
1644
1702
  let function = function_value.dyn_into::<Function>().map_err(|_| {
1645
1703
  config_error(format!(
1646
1704
  "custom_worker node '{}' requires workflowOptions.functions['{}']",
@@ -1660,10 +1718,8 @@ impl WasmClient {
1660
1718
  .unwrap_or(JsonValue::Null),
1661
1719
  "nodeId": node.id.clone()
1662
1720
  });
1663
- let args_js = serde_wasm_bindgen::to_value(&worker_args)
1664
- .map_err(|_| js_error("failed to serialize custom_worker args"))?;
1665
- let context_js = serde_wasm_bindgen::to_value(&graph_context)
1666
- .map_err(|_| js_error("failed to serialize graph context"))?;
1721
+ let args_js = json_value_to_js_plain(&worker_args)?;
1722
+ let context_js = json_value_to_js_plain(&graph_context)?;
1667
1723
  let worker_call_output = function
1668
1724
  .call2(&JsValue::NULL, &args_js, &context_js)
1669
1725
  .map_err(|_| {
@@ -1814,8 +1870,9 @@ impl WasmClient {
1814
1870
  trace_id: None,
1815
1871
  metadata: Some(json!({"nerdstats": nerdstats})),
1816
1872
  };
1817
- return serde_wasm_bindgen::to_value(&result)
1818
- .map_err(|_| js_error("failed to serialize workflow result"));
1873
+ let jv = serde_json::to_value(&result)
1874
+ .map_err(|_| js_error("failed to serialize workflow result"))?;
1875
+ return json_value_to_js_plain(&jv);
1819
1876
  }
1820
1877
 
1821
1878
  let doc: WorkflowDoc = serde_json::from_value(raw_doc)
@@ -1885,11 +1942,7 @@ impl WasmClient {
1885
1942
  .complete(
1886
1943
  model,
1887
1944
  JsValue::from_str(&prompt),
1888
- Some(
1889
- serde_wasm_bindgen::to_value(&opts).map_err(|_| {
1890
- js_error("failed to serialize completion options")
1891
- })?,
1892
- ),
1945
+ Some(json_value_to_js_plain(&opts)?),
1893
1946
  )
1894
1947
  .await?;
1895
1948
  let completion: JsonValue = serde_wasm_bindgen::from_value(completion_js)
@@ -1956,11 +2009,8 @@ impl WasmClient {
1956
2009
  .unwrap_or_else(|| JsonValue::Object(JsonMap::new())),
1957
2010
  &context,
1958
2011
  );
1959
- let args_js = serde_wasm_bindgen::to_value(&args_value)
1960
- .map_err(|_| js_error("failed to serialize call_function args"))?;
1961
- let context_js =
1962
- serde_wasm_bindgen::to_value(&JsonValue::Object(context.clone()))
1963
- .map_err(|_| js_error("failed to serialize workflow context"))?;
2012
+ let args_js = json_value_to_js_plain(&args_value)?;
2013
+ let context_js = json_value_to_js_plain(&JsonValue::Object(context.clone()))?;
1964
2014
  let call_output = function
1965
2015
  .call2(&JsValue::NULL, &args_js, &context_js)
1966
2016
  .map_err(|_| {
@@ -2017,18 +2067,32 @@ impl WasmClient {
2017
2067
  pointer += 1;
2018
2068
  }
2019
2069
 
2070
+ let trace: Vec<String> = events
2071
+ .iter()
2072
+ .filter(|e| e.status == "completed")
2073
+ .map(|e| e.step_id.clone())
2074
+ .collect();
2075
+ let terminal_node = trace.last().cloned();
2076
+
2077
+ let context_value = JsonValue::Object(context);
2078
+ let outputs_value = context_value
2079
+ .as_object()
2080
+ .cloned()
2081
+ .map(JsonValue::Object)
2082
+ .unwrap_or_else(|| JsonValue::Object(JsonMap::new()));
2083
+
2020
2084
  let result = WorkflowRunResult {
2021
2085
  status: "ok".to_string(),
2022
- context: JsonValue::Object(context),
2023
- output,
2086
+ context: context_value.clone(),
2087
+ output: output.clone(),
2024
2088
  events,
2025
- workflow_id: None,
2089
+ workflow_id: Some("wasm_workflow".to_string()),
2026
2090
  entry_node: None,
2027
2091
  email_text: None,
2028
- trace: None,
2029
- outputs: None,
2030
- terminal_node: None,
2031
- terminal_output: None,
2092
+ trace: Some(trace),
2093
+ outputs: Some(outputs_value),
2094
+ terminal_node,
2095
+ terminal_output: output,
2032
2096
  step_timings: None,
2033
2097
  total_elapsed_ms: None,
2034
2098
  ttft_ms: None,
@@ -2040,8 +2104,9 @@ impl WasmClient {
2040
2104
  trace_id: None,
2041
2105
  metadata: None,
2042
2106
  };
2043
- serde_wasm_bindgen::to_value(&result)
2044
- .map_err(|_| js_error("failed to serialize workflow result"))
2107
+ let jv = serde_json::to_value(&result)
2108
+ .map_err(|_| js_error("failed to serialize workflow result"))?;
2109
+ json_value_to_js_plain(&jv)
2045
2110
  }
2046
2111
 
2047
2112
  #[wasm_bindgen(js_name = runWorkflowYaml)]
@@ -2080,8 +2145,7 @@ impl WasmClient {
2080
2145
  serde_json::json!([])
2081
2146
  };
2082
2147
 
2083
- let input_js = serde_wasm_bindgen::to_value(&serde_json::json!({ "messages": messages_value }))
2084
- .map_err(|_| js_error("failed to serialize messages input"))?;
2148
+ let input_js = json_value_to_js_plain(&serde_json::json!({ "messages": messages_value }))?;
2085
2149
 
2086
2150
  self.run_workflow_yaml_string(yaml_text, input_js, options).await
2087
2151
  }
@@ -2091,10 +2155,3 @@ impl WasmClient {
2091
2155
  pub fn supports_rust_wasm() -> bool {
2092
2156
  true
2093
2157
  }
2094
-
2095
- #[wasm_bindgen(js_name = toJsArray)]
2096
- pub fn to_js_array(value: JsValue) -> Array {
2097
- let arr = Array::new();
2098
- arr.push(&value);
2099
- arr
2100
- }