simple-agents-wasm 0.3.6 → 0.3.8

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.8",
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];
@@ -41,6 +38,7 @@ export interface InitOutput {
41
38
  readonly wasmclient_streamEvents: (a: number, b: number, c: number, d: any, e: any, f: number) => any;
42
39
  readonly wasm_bindgen__convert__closures_____invoke__h9f53f643b01d7c8e: (a: number, b: number, c: any) => [number, number];
43
40
  readonly wasm_bindgen__convert__closures_____invoke__h05acb8c479b21d4b: (a: number, b: number, c: any, d: any) => void;
41
+ readonly wasm_bindgen__convert__closures_____invoke__h25554e005bd8517d: (a: number, b: number, c: any) => void;
44
42
  readonly __wbindgen_malloc: (a: number, b: number) => number;
45
43
  readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
46
44
  readonly __wbindgen_exn_store: (a: number) => void;
@@ -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;
@@ -418,26 +409,31 @@ function __wbg_get_imports() {
418
409
  return ret;
419
410
  },
420
411
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
421
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 152, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
412
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 157, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
422
413
  const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h9f53f643b01d7c8e);
423
414
  return ret;
424
415
  },
425
- __wbindgen_cast_0000000000000002: function(arg0) {
416
+ __wbindgen_cast_0000000000000002: function(arg0, arg1) {
417
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 78, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
418
+ const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h25554e005bd8517d);
419
+ return ret;
420
+ },
421
+ __wbindgen_cast_0000000000000003: function(arg0) {
426
422
  // Cast intrinsic for `F64 -> Externref`.
427
423
  const ret = arg0;
428
424
  return ret;
429
425
  },
430
- __wbindgen_cast_0000000000000003: function(arg0) {
426
+ __wbindgen_cast_0000000000000004: function(arg0) {
431
427
  // Cast intrinsic for `I64 -> Externref`.
432
428
  const ret = arg0;
433
429
  return ret;
434
430
  },
435
- __wbindgen_cast_0000000000000004: function(arg0, arg1) {
431
+ __wbindgen_cast_0000000000000005: function(arg0, arg1) {
436
432
  // Cast intrinsic for `Ref(String) -> Externref`.
437
433
  const ret = getStringFromWasm0(arg0, arg1);
438
434
  return ret;
439
435
  },
440
- __wbindgen_cast_0000000000000005: function(arg0) {
436
+ __wbindgen_cast_0000000000000006: function(arg0) {
441
437
  // Cast intrinsic for `U64 -> Externref`.
442
438
  const ret = BigInt.asUintN(64, arg0);
443
439
  return ret;
@@ -458,6 +454,10 @@ function __wbg_get_imports() {
458
454
  };
459
455
  }
460
456
 
457
+ function wasm_bindgen__convert__closures_____invoke__h25554e005bd8517d(arg0, arg1, arg2) {
458
+ wasm.wasm_bindgen__convert__closures_____invoke__h25554e005bd8517d(arg0, arg1, arg2);
459
+ }
460
+
461
461
  function wasm_bindgen__convert__closures_____invoke__h9f53f643b01d7c8e(arg0, arg1, arg2) {
462
462
  const ret = wasm.wasm_bindgen__convert__closures_____invoke__h9f53f643b01d7c8e(arg0, arg1, arg2);
463
463
  if (ret[1]) {
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];
@@ -12,6 +11,7 @@ export const wasmclient_runYamlString: (a: number, b: number, c: number, d: any,
12
11
  export const wasmclient_streamEvents: (a: number, b: number, c: number, d: any, e: any, f: number) => any;
13
12
  export const wasm_bindgen__convert__closures_____invoke__h9f53f643b01d7c8e: (a: number, b: number, c: any) => [number, number];
14
13
  export const wasm_bindgen__convert__closures_____invoke__h05acb8c479b21d4b: (a: number, b: number, c: any, d: any) => void;
14
+ export const wasm_bindgen__convert__closures_____invoke__h25554e005bd8517d: (a: number, b: number, c: any) => void;
15
15
  export const __wbindgen_malloc: (a: number, b: number) => number;
16
16
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
17
17
  export const __wbindgen_exn_store: (a: number) => void;
@@ -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.8"
4
4
  edition = "2021"
5
5
  license = "MIT OR Apache-2.0"
6
6
 
package/rust/src/lib.rs CHANGED
@@ -1,7 +1,9 @@
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;
6
+ use wasm_bindgen::closure::Closure;
5
7
  use wasm_bindgen::prelude::*;
6
8
  use wasm_bindgen::JsCast;
7
9
  use wasm_bindgen_futures::JsFuture;
@@ -86,6 +88,7 @@ struct WorkflowDoc {
86
88
 
87
89
  #[derive(Deserialize, Clone)]
88
90
  struct GraphWorkflowDoc {
91
+ id: Option<String>,
89
92
  model: Option<String>,
90
93
  entry_node: String,
91
94
  nodes: Vec<GraphWorkflowNode>,
@@ -125,6 +128,7 @@ struct GraphLlmCall {
125
128
  temperature: Option<f64>,
126
129
  messages_path: Option<String>,
127
130
  append_prompt_as_user: Option<bool>,
131
+ stream: Option<bool>,
128
132
  }
129
133
 
130
134
  #[derive(Deserialize, Clone)]
@@ -271,6 +275,29 @@ struct WorkflowRunOptions {
271
275
  trace: Option<JsonValue>,
272
276
  #[serde(skip)]
273
277
  functions_js: Option<JsValue>,
278
+ #[serde(skip)]
279
+ on_event_js: Option<Function>,
280
+ /// Injected by JS as `__fetchImpl` so HTTP calls use the same fetch as tests (no global race).
281
+ #[serde(skip)]
282
+ fetch_js: Option<JsValue>,
283
+ }
284
+
285
+ struct FetchOverrideGuard<'a> {
286
+ cell: &'a RefCell<Option<JsValue>>,
287
+ previous: Option<JsValue>,
288
+ }
289
+
290
+ impl<'a> FetchOverrideGuard<'a> {
291
+ fn new(cell: &'a RefCell<Option<JsValue>>, value: Option<JsValue>) -> Self {
292
+ let previous = cell.replace(value);
293
+ Self { cell, previous }
294
+ }
295
+ }
296
+
297
+ impl Drop for FetchOverrideGuard<'_> {
298
+ fn drop(&mut self) {
299
+ self.cell.replace(self.previous.take());
300
+ }
274
301
  }
275
302
 
276
303
  fn js_error(message: impl Into<String>) -> JsValue {
@@ -289,6 +316,24 @@ fn config_error(message: impl Into<String>) -> JsValue {
289
316
  .into()
290
317
  }
291
318
 
319
+ /// Plain objects for JS `Function.call` / stream callbacks. `serde_wasm_bindgen::to_value`
320
+ /// can yield empty externref objects in Node for callback arguments; `JSON.parse` matches browser behavior.
321
+ fn json_value_to_js_plain(value: &JsonValue) -> Result<JsValue, JsValue> {
322
+ let s =
323
+ serde_json::to_string(value).map_err(|_| js_error("failed to serialize value for JS"))?;
324
+ js_sys::JSON::parse(&s).map_err(|_| js_error("failed to parse JSON for JS"))
325
+ }
326
+
327
+ fn emit_workflow_event(on_event: &Option<Function>, event: JsonValue) -> Result<(), JsValue> {
328
+ if let Some(callback) = on_event {
329
+ let event_js = json_value_to_js_plain(&event)?;
330
+ callback
331
+ .call1(&JsValue::NULL, &event_js)
332
+ .map_err(|_| js_error("failed to call workflow stream callback"))?;
333
+ }
334
+ Ok(())
335
+ }
336
+
292
337
  fn now_millis() -> f64 {
293
338
  let global = js_sys::global();
294
339
  let performance = Reflect::get(&global, &JsValue::from_str("performance")).ok();
@@ -463,13 +508,23 @@ fn call_method2_sync(
463
508
  }
464
509
 
465
510
  async fn js_fetch(
511
+ fetch_override: Option<JsValue>,
466
512
  url: &str,
467
513
  body: &JsonValue,
468
514
  headers: &HashMap<String, String>,
469
515
  ) -> Result<JsValue, JsValue> {
470
516
  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"))?;
517
+ let fetch_value = if let Some(f) = fetch_override {
518
+ if f.is_function() {
519
+ f
520
+ } else {
521
+ Reflect::get(&global, &JsValue::from_str("fetch"))
522
+ .map_err(|_| js_error("global fetch is unavailable"))?
523
+ }
524
+ } else {
525
+ Reflect::get(&global, &JsValue::from_str("fetch"))
526
+ .map_err(|_| js_error("global fetch is unavailable"))?
527
+ };
473
528
  let fetch_fn = fetch_value
474
529
  .dyn_into::<Function>()
475
530
  .map_err(|_| js_error("global fetch is not callable"))?;
@@ -985,6 +1040,7 @@ pub struct WasmClient {
985
1040
  base_url: String,
986
1041
  api_key: String,
987
1042
  headers: HashMap<String, String>,
1043
+ fetch_override: RefCell<Option<JsValue>>,
988
1044
  }
989
1045
 
990
1046
  #[wasm_bindgen]
@@ -1013,6 +1069,7 @@ impl WasmClient {
1013
1069
  base_url: normalize_base_url(&base),
1014
1070
  api_key: parsed.api_key,
1015
1071
  headers: parsed.headers.unwrap_or_default(),
1072
+ fetch_override: RefCell::new(None),
1016
1073
  })
1017
1074
  }
1018
1075
 
@@ -1066,7 +1123,9 @@ impl WasmClient {
1066
1123
  .or_insert_with(|| "https://simpleagents.dev".to_string());
1067
1124
  }
1068
1125
 
1126
+ let fetch_override = self.fetch_override.borrow().clone();
1069
1127
  let response = js_fetch(
1128
+ fetch_override,
1070
1129
  &format!("{}/chat/completions", self.base_url),
1071
1130
  &body,
1072
1131
  &headers,
@@ -1235,7 +1294,9 @@ impl WasmClient {
1235
1294
  .or_insert_with(|| "https://simpleagents.dev".to_string());
1236
1295
  }
1237
1296
 
1297
+ let fetch_override = self.fetch_override.borrow().clone();
1238
1298
  let response = js_fetch(
1299
+ fetch_override,
1239
1300
  &format!("{}/chat/completions", self.base_url),
1240
1301
  &body,
1241
1302
  &headers,
@@ -1260,8 +1321,7 @@ impl WasmClient {
1260
1321
  "eventType": "error",
1261
1322
  "error": { "message": message }
1262
1323
  });
1263
- let event_js = serde_wasm_bindgen::to_value(&err_event)
1264
- .map_err(|_| js_error("failed to serialize stream error event"))?;
1324
+ let event_js = json_value_to_js_plain(&err_event)?;
1265
1325
  on_event
1266
1326
  .call1(&JsValue::NULL, &event_js)
1267
1327
  .map_err(|_| js_error("failed to call stream callback"))?;
@@ -1337,8 +1397,7 @@ impl WasmClient {
1337
1397
  "raw": data,
1338
1398
  }
1339
1399
  });
1340
- let event_js = serde_wasm_bindgen::to_value(&delta_event)
1341
- .map_err(|_| js_error("failed to serialize stream delta event"))?;
1400
+ let event_js = json_value_to_js_plain(&delta_event)?;
1342
1401
  on_event
1343
1402
  .call1(&JsValue::NULL, &event_js)
1344
1403
  .map_err(|_| js_error("failed to call stream callback"))?;
@@ -1348,8 +1407,7 @@ impl WasmClient {
1348
1407
  .await?;
1349
1408
 
1350
1409
  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"))?;
1410
+ let done_js = json_value_to_js_plain(&done_event)?;
1353
1411
  on_event
1354
1412
  .call1(&JsValue::NULL, &done_js)
1355
1413
  .map_err(|_| js_error("failed to call stream callback"))?;
@@ -1396,14 +1454,34 @@ impl WasmClient {
1396
1454
  telemetry: None,
1397
1455
  trace: None,
1398
1456
  functions_js: None,
1457
+ on_event_js: None,
1458
+ fetch_js: None,
1399
1459
  };
1400
1460
  if let Some(options_js) = workflow_options {
1401
1461
  options = serde_wasm_bindgen::from_value(options_js.clone())
1402
1462
  .map_err(|_| config_error("invalid workflowOptions object"))?;
1403
1463
  let functions_value = Reflect::get(&options_js, &JsValue::from_str("functions")).ok();
1404
- options.functions_js = functions_value;
1464
+ options.functions_js = functions_value.and_then(|v| {
1465
+ if v.is_undefined() || v.is_null() {
1466
+ None
1467
+ } else {
1468
+ Some(v)
1469
+ }
1470
+ });
1471
+ let on_event_value = Reflect::get(&options_js, &JsValue::from_str("onEvent")).ok();
1472
+ options.on_event_js = on_event_value.and_then(|v| {
1473
+ if v.is_undefined() || v.is_null() {
1474
+ None
1475
+ } else {
1476
+ v.dyn_into::<Function>().ok()
1477
+ }
1478
+ });
1479
+ let fetch_js = Reflect::get(&options_js, &JsValue::from_str("__fetchImpl")).ok();
1480
+ options.fetch_js = fetch_js;
1405
1481
  }
1406
1482
 
1483
+ let _fetch_guard = FetchOverrideGuard::new(&self.fetch_override, options.fetch_js.clone());
1484
+
1407
1485
  if raw_doc.get("entry_node").is_some() && raw_doc.get("nodes").is_some() {
1408
1486
  let graph_doc: GraphWorkflowDoc = serde_json::from_value(raw_doc.clone())
1409
1487
  .map_err(|error| config_error(format!("invalid graph workflow YAML: {error}")))?;
@@ -1445,6 +1523,14 @@ impl WasmClient {
1445
1523
  let total_reasoning_tokens: u64 = 0;
1446
1524
  let mut llm_nodes_without_usage: Vec<String> = Vec::new();
1447
1525
 
1526
+ emit_workflow_event(
1527
+ &options.on_event_js,
1528
+ json!({
1529
+ "event_type": "workflow_started",
1530
+ "workflow_id": graph_doc.id.clone().unwrap_or_else(|| "wasm_workflow".to_string())
1531
+ }),
1532
+ )?;
1533
+
1448
1534
  while !pointer.is_empty() {
1449
1535
  iterations += 1;
1450
1536
  if iterations > 1000 {
@@ -1519,24 +1605,74 @@ impl WasmClient {
1519
1605
  tool_calls: None,
1520
1606
  });
1521
1607
  }
1522
- serde_wasm_bindgen::to_value(&history)
1523
- .map_err(|_| js_error("failed to serialize graph llm messages"))?
1608
+ json_value_to_js_plain(
1609
+ &serde_json::to_value(&history)
1610
+ .map_err(|_| js_error("failed to serialize graph llm messages"))?,
1611
+ )?
1524
1612
  } else {
1525
1613
  JsValue::from_str(&prompt)
1526
1614
  };
1527
1615
 
1528
1616
  let opts = json!({ "temperature": llm.temperature });
1529
- let completion_js = self
1530
- .complete(
1531
- model,
1532
- prompt_js,
1533
- Some(
1534
- serde_wasm_bindgen::to_value(&opts).map_err(|_| {
1535
- js_error("failed to serialize completion options")
1536
- })?,
1537
- ),
1538
- )
1539
- .await?;
1617
+ let completion_js = if llm.stream.unwrap_or(false) && options.on_event_js.is_some() {
1618
+ let node_id = node.id.clone();
1619
+ let step_id = node.id.clone();
1620
+ let workflow_on_event = options
1621
+ .on_event_js
1622
+ .clone()
1623
+ .ok_or_else(|| js_error("missing workflow stream callback"))?;
1624
+ let mapped_stream_cb = Closure::wrap(Box::new(move |stream_event_js: JsValue| {
1625
+ let stream_event: JsonValue =
1626
+ serde_wasm_bindgen::from_value(stream_event_js).unwrap_or(JsonValue::Null);
1627
+ let stream_event_type = stream_event
1628
+ .get("eventType")
1629
+ .and_then(JsonValue::as_str)
1630
+ .unwrap_or_default();
1631
+
1632
+ if stream_event_type == "delta" {
1633
+ if let Some(delta) = stream_event
1634
+ .get("delta")
1635
+ .and_then(|v| v.get("content"))
1636
+ .and_then(JsonValue::as_str)
1637
+ {
1638
+ let workflow_event = json!({
1639
+ "event_type": "node_stream_delta",
1640
+ "node_id": node_id.clone(),
1641
+ "step_id": step_id.clone(),
1642
+ "delta": delta
1643
+ });
1644
+ if let Ok(event_js) = json_value_to_js_plain(&workflow_event) {
1645
+ let _ = workflow_on_event.call1(&JsValue::NULL, &event_js);
1646
+ }
1647
+ }
1648
+ } else if stream_event_type == "done" {
1649
+ let workflow_event = json!({
1650
+ "event_type": "node_stream_snapshot",
1651
+ "node_id": node_id.clone(),
1652
+ "step_id": step_id.clone(),
1653
+ "metadata": { "is_complete": true }
1654
+ });
1655
+ if let Ok(event_js) = json_value_to_js_plain(&workflow_event) {
1656
+ let _ = workflow_on_event.call1(&JsValue::NULL, &event_js);
1657
+ }
1658
+ }
1659
+ }) as Box<dyn FnMut(JsValue)>);
1660
+ let stream_callback_fn: Function =
1661
+ mapped_stream_cb.as_ref().unchecked_ref::<Function>().clone();
1662
+ let result = self
1663
+ .stream_events(
1664
+ model,
1665
+ prompt_js,
1666
+ stream_callback_fn,
1667
+ Some(json_value_to_js_plain(&opts)?),
1668
+ )
1669
+ .await;
1670
+ drop(mapped_stream_cb);
1671
+ result?
1672
+ } else {
1673
+ self.complete(model, prompt_js, Some(json_value_to_js_plain(&opts)?))
1674
+ .await?
1675
+ };
1540
1676
  let completion: JsonValue = serde_wasm_bindgen::from_value(completion_js)
1541
1677
  .map_err(|_| js_error("failed to parse completion result"))?;
1542
1678
 
@@ -1630,17 +1766,23 @@ impl WasmClient {
1630
1766
  .unwrap_or_else(|| handler.clone());
1631
1767
  let functions_js = options.functions_js.clone().ok_or_else(|| {
1632
1768
  config_error(format!(
1633
- "custom_worker node '{}' requires workflowOptions.functions",
1634
- node.id
1769
+ "custom_worker node '{}' requires workflowOptions.functions['{}']",
1770
+ node.id, lookup_key
1635
1771
  ))
1636
1772
  })?;
1637
1773
  let function_value = Reflect::get(&functions_js, &JsValue::from_str(&lookup_key))
1638
1774
  .map_err(|_| {
1639
1775
  config_error(format!(
1640
- "failed to resolve custom worker handler key '{}' from workflowOptions.functions",
1641
- lookup_key
1776
+ "custom_worker node '{}' requires workflowOptions.functions['{}']",
1777
+ node.id, lookup_key
1642
1778
  ))
1643
1779
  })?;
1780
+ if function_value.is_undefined() || function_value.is_null() {
1781
+ return Err(config_error(format!(
1782
+ "custom_worker node '{}' requires workflowOptions.functions['{}']",
1783
+ node.id, lookup_key
1784
+ )));
1785
+ }
1644
1786
  let function = function_value.dyn_into::<Function>().map_err(|_| {
1645
1787
  config_error(format!(
1646
1788
  "custom_worker node '{}' requires workflowOptions.functions['{}']",
@@ -1660,10 +1802,8 @@ impl WasmClient {
1660
1802
  .unwrap_or(JsonValue::Null),
1661
1803
  "nodeId": node.id.clone()
1662
1804
  });
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"))?;
1805
+ let args_js = json_value_to_js_plain(&worker_args)?;
1806
+ let context_js = json_value_to_js_plain(&graph_context)?;
1667
1807
  let worker_call_output = function
1668
1808
  .call2(&JsValue::NULL, &args_js, &context_js)
1669
1809
  .map_err(|_| {
@@ -1741,6 +1881,17 @@ impl WasmClient {
1741
1881
  trace.push(node.id.clone());
1742
1882
  }
1743
1883
 
1884
+ emit_workflow_event(
1885
+ &options.on_event_js,
1886
+ json!({
1887
+ "event_type": "workflow_completed",
1888
+ "workflow_id": raw_doc
1889
+ .get("id")
1890
+ .and_then(JsonValue::as_str)
1891
+ .unwrap_or("wasm_workflow")
1892
+ }),
1893
+ )?;
1894
+
1744
1895
  let total_elapsed_ms = (now_millis() - workflow_started).max(0.0) as u64;
1745
1896
  let terminal_node = trace.last().cloned().unwrap_or_default();
1746
1897
  let workflow_id = raw_doc
@@ -1814,8 +1965,9 @@ impl WasmClient {
1814
1965
  trace_id: None,
1815
1966
  metadata: Some(json!({"nerdstats": nerdstats})),
1816
1967
  };
1817
- return serde_wasm_bindgen::to_value(&result)
1818
- .map_err(|_| js_error("failed to serialize workflow result"));
1968
+ let jv = serde_json::to_value(&result)
1969
+ .map_err(|_| js_error("failed to serialize workflow result"))?;
1970
+ return json_value_to_js_plain(&jv);
1819
1971
  }
1820
1972
 
1821
1973
  let doc: WorkflowDoc = serde_json::from_value(raw_doc)
@@ -1885,11 +2037,7 @@ impl WasmClient {
1885
2037
  .complete(
1886
2038
  model,
1887
2039
  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
- ),
2040
+ Some(json_value_to_js_plain(&opts)?),
1893
2041
  )
1894
2042
  .await?;
1895
2043
  let completion: JsonValue = serde_wasm_bindgen::from_value(completion_js)
@@ -1956,11 +2104,8 @@ impl WasmClient {
1956
2104
  .unwrap_or_else(|| JsonValue::Object(JsonMap::new())),
1957
2105
  &context,
1958
2106
  );
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"))?;
2107
+ let args_js = json_value_to_js_plain(&args_value)?;
2108
+ let context_js = json_value_to_js_plain(&JsonValue::Object(context.clone()))?;
1964
2109
  let call_output = function
1965
2110
  .call2(&JsValue::NULL, &args_js, &context_js)
1966
2111
  .map_err(|_| {
@@ -2017,18 +2162,32 @@ impl WasmClient {
2017
2162
  pointer += 1;
2018
2163
  }
2019
2164
 
2165
+ let trace: Vec<String> = events
2166
+ .iter()
2167
+ .filter(|e| e.status == "completed")
2168
+ .map(|e| e.step_id.clone())
2169
+ .collect();
2170
+ let terminal_node = trace.last().cloned();
2171
+
2172
+ let context_value = JsonValue::Object(context);
2173
+ let outputs_value = context_value
2174
+ .as_object()
2175
+ .cloned()
2176
+ .map(JsonValue::Object)
2177
+ .unwrap_or_else(|| JsonValue::Object(JsonMap::new()));
2178
+
2020
2179
  let result = WorkflowRunResult {
2021
2180
  status: "ok".to_string(),
2022
- context: JsonValue::Object(context),
2023
- output,
2181
+ context: context_value.clone(),
2182
+ output: output.clone(),
2024
2183
  events,
2025
- workflow_id: None,
2184
+ workflow_id: Some("wasm_workflow".to_string()),
2026
2185
  entry_node: None,
2027
2186
  email_text: None,
2028
- trace: None,
2029
- outputs: None,
2030
- terminal_node: None,
2031
- terminal_output: None,
2187
+ trace: Some(trace),
2188
+ outputs: Some(outputs_value),
2189
+ terminal_node,
2190
+ terminal_output: output,
2032
2191
  step_timings: None,
2033
2192
  total_elapsed_ms: None,
2034
2193
  ttft_ms: None,
@@ -2040,8 +2199,9 @@ impl WasmClient {
2040
2199
  trace_id: None,
2041
2200
  metadata: None,
2042
2201
  };
2043
- serde_wasm_bindgen::to_value(&result)
2044
- .map_err(|_| js_error("failed to serialize workflow result"))
2202
+ let jv = serde_json::to_value(&result)
2203
+ .map_err(|_| js_error("failed to serialize workflow result"))?;
2204
+ json_value_to_js_plain(&jv)
2045
2205
  }
2046
2206
 
2047
2207
  #[wasm_bindgen(js_name = runWorkflowYaml)]
@@ -2080,8 +2240,7 @@ impl WasmClient {
2080
2240
  serde_json::json!([])
2081
2241
  };
2082
2242
 
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"))?;
2243
+ let input_js = json_value_to_js_plain(&serde_json::json!({ "messages": messages_value }))?;
2085
2244
 
2086
2245
  self.run_workflow_yaml_string(yaml_text, input_js, options).await
2087
2246
  }
@@ -2091,10 +2250,3 @@ impl WasmClient {
2091
2250
  pub fn supports_rust_wasm() -> bool {
2092
2251
  true
2093
2252
  }
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
- }