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/index.js CHANGED
@@ -1,35 +1,9 @@
1
- import { parse as parseYaml } from "yaml";
2
1
  import { configError, runtimeError } from "./runtime/errors.js";
3
2
  import {
4
- applyDeltaToAggregate,
5
- createStreamAggregator,
6
3
  createStreamEventBridge,
7
- iterateSse,
8
- parseSseEventBlock
9
4
  } from "./runtime/stream.js";
10
5
  import { loadRustModule } from "./runtime/rust-runtime.js";
11
6
 
12
- const DEFAULT_BASE_URLS = {
13
- openai: "https://api.openai.com/v1",
14
- openrouter: "https://openrouter.ai/api/v1"
15
- };
16
-
17
- function toMessages(promptOrMessages) {
18
- if (typeof promptOrMessages === "string") {
19
- const content = promptOrMessages.trim();
20
- if (content.length === 0) {
21
- throw configError("prompt cannot be empty");
22
- }
23
- return [{ role: "user", content }];
24
- }
25
-
26
- if (!Array.isArray(promptOrMessages) || promptOrMessages.length === 0) {
27
- throw configError("messages must be a non-empty array");
28
- }
29
-
30
- return promptOrMessages;
31
- }
32
-
33
7
  function buildWorkflowInputFromExecutionRequest(request) {
34
8
  if (!request || typeof request !== "object") {
35
9
  throw configError("workflow request must be an object");
@@ -40,6 +14,7 @@ function buildWorkflowInputFromExecutionRequest(request) {
40
14
  if (!Array.isArray(request.messages) || request.messages.length === 0) {
41
15
  throw configError("messages must be a non-empty array");
42
16
  }
17
+
43
18
  const input = request.input && typeof request.input === "object" ? { ...request.input } : {};
44
19
  input.messages = request.messages;
45
20
  if (request.context && typeof request.context === "object") {
@@ -67,40 +42,6 @@ function buildWorkflowOptionsFromExecutionRequest(request, onEvent) {
67
42
  return options;
68
43
  }
69
44
 
70
- function toUsage(usage) {
71
- if (!usage || typeof usage !== "object") {
72
- return {
73
- promptTokens: 0,
74
- completionTokens: 0,
75
- totalTokens: 0
76
- };
77
- }
78
-
79
- return {
80
- promptTokens: usage.prompt_tokens ?? usage.promptTokens ?? 0,
81
- completionTokens: usage.completion_tokens ?? usage.completionTokens ?? 0,
82
- totalTokens: usage.total_tokens ?? usage.totalTokens ?? 0
83
- };
84
- }
85
-
86
- function toToolCalls(toolCalls) {
87
- if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
88
- return undefined;
89
- }
90
-
91
- return toolCalls
92
- .filter((call) => call && typeof call === "object")
93
- .map((call) => ({
94
- id: call.id ?? "",
95
- toolType: call.type ?? call.toolType ?? "function",
96
- function: {
97
- name: call.function?.name ?? "",
98
- arguments: call.function?.arguments ?? ""
99
- }
100
- }));
101
- }
102
-
103
-
104
45
  function assertWorkflowResultShape(result) {
105
46
  if (result === null || typeof result !== "object") {
106
47
  throw runtimeError(
@@ -135,8 +76,8 @@ function normalizeWorkflowResult(result) {
135
76
  : context;
136
77
  const trace = Array.isArray(result.events)
137
78
  ? result.events
138
- .filter((event) => event && event.status === "completed" && typeof event.stepId === "string")
139
- .map((event) => event.stepId)
79
+ .filter((event) => event && event.status === "completed" && typeof event.stepId === "string")
80
+ .map((event) => event.stepId)
140
81
  : [];
141
82
  const terminalNode = trace.at(-1) ?? "";
142
83
 
@@ -146,6 +87,7 @@ function normalizeWorkflowResult(result) {
146
87
  email_text: typeof context?.input?.email_text === "string" ? context.input.email_text : "",
147
88
  trace,
148
89
  outputs: nodeOutputs,
90
+ context,
149
91
  terminal_node: typeof result.terminal_node === "string" ? result.terminal_node : terminalNode,
150
92
  terminal_output: result.output,
151
93
  events: Array.isArray(result.events) ? result.events : [],
@@ -153,1068 +95,23 @@ function normalizeWorkflowResult(result) {
153
95
  };
154
96
  }
155
97
 
156
- function normalizeBaseUrl(baseUrl) {
157
- return baseUrl.replace(/\/$/, "");
158
- }
159
-
160
- function finiteNumberOrNull(value) {
161
- return Number.isFinite(value) ? value : null;
162
- }
163
-
164
- function buildStepDetail(step) {
165
- return {
166
- node_id: step.nodeId,
167
- node_kind: step.nodeKind,
168
- model_name: step.modelName ?? null,
169
- elapsed_ms: step.elapsedMs,
170
- prompt_tokens: finiteNumberOrNull(step.promptTokens),
171
- completion_tokens: finiteNumberOrNull(step.completionTokens),
172
- total_tokens: finiteNumberOrNull(step.totalTokens),
173
- reasoning_tokens: 0,
174
- tokens_per_second: finiteNumberOrNull(step.tokensPerSecond)
175
- };
176
- }
177
-
178
- function buildWorkflowNerdstats(summary) {
179
- return {
180
- workflow_id: summary.workflowId,
181
- terminal_node: summary.terminalNode,
182
- total_elapsed_ms: summary.totalElapsedMs,
183
- ttft_ms: summary.ttftMs,
184
- step_details: summary.stepDetails,
185
- total_input_tokens: summary.totalInputTokens,
186
- total_output_tokens: summary.totalOutputTokens,
187
- total_tokens: summary.totalTokens,
188
- total_reasoning_tokens: summary.totalReasoningTokens,
189
- tokens_per_second: summary.tokensPerSecond,
190
- trace_id: summary.traceId,
191
- token_metrics_available: summary.tokenMetricsAvailable,
192
- token_metrics_source: summary.tokenMetricsSource,
193
- llm_nodes_without_usage: summary.llmNodesWithoutUsage
194
- };
195
- }
196
-
197
- function interpolate(value, context) {
198
- if (typeof value === "string") {
199
- return value.replace(/{{\s*([^}]+)\s*}}/g, (_, key) => {
200
- const token = String(key).trim();
201
- const resolved = context[token];
202
- if (resolved === null || resolved === undefined) {
203
- return "";
204
- }
205
- if (typeof resolved === "string") {
206
- return resolved;
207
- }
208
- return JSON.stringify(resolved);
209
- });
210
- }
211
-
212
- if (Array.isArray(value)) {
213
- return value.map((entry) => interpolate(entry, context));
214
- }
215
-
216
- if (value !== null && value !== undefined && typeof value === "object") {
217
- const output = {};
218
- for (const [key, nested] of Object.entries(value)) {
219
- output[key] = interpolate(nested, context);
220
- }
221
- return output;
222
- }
223
-
224
- return value;
225
- }
226
-
227
- function evaluateCondition(condition, context) {
228
- if (!condition || typeof condition !== "object") {
229
- return false;
230
- }
231
-
232
- const left = interpolate(condition.left, context);
233
- const right = interpolate(condition.right, context);
234
-
235
- if (condition.operator === "eq") {
236
- return left === right;
237
- }
238
- if (condition.operator === "ne") {
239
- return left !== right;
240
- }
241
- if (condition.operator === "contains") {
242
- return String(left).includes(String(right));
243
- }
244
-
245
- return false;
246
- }
247
-
248
- function parseWorkflow(yamlText) {
249
- if (typeof yamlText !== "string" || yamlText.trim().length === 0) {
250
- throw configError("yamlText must be a non-empty string");
251
- }
252
-
253
- const parsed = parseYaml(yamlText);
254
- if (!parsed || typeof parsed !== "object") {
255
- throw configError("workflow YAML must parse to an object");
256
- }
257
-
258
- if (!Array.isArray(parsed.steps) && !isGraphWorkflow(parsed)) {
259
- throw configError(
260
- "workflow YAML must contain either a steps array or graph fields (entry_node + nodes)"
261
- );
262
- }
263
-
264
- return parsed;
265
- }
266
-
267
- function isGraphWorkflow(doc) {
268
- return (
269
- doc &&
270
- typeof doc === "object" &&
271
- typeof doc.entry_node === "string" &&
272
- Array.isArray(doc.nodes)
273
- );
274
- }
275
-
276
- function getPathValue(source, path) {
277
- if (!source || typeof source !== "object") {
278
- return undefined;
279
- }
280
-
281
- const normalized = String(path).trim().replace(/^\$\./, "");
282
- const tokens = normalized.split(".").filter((token) => token.length > 0);
283
- let current = source;
284
- for (const token of tokens) {
285
- if (!current || typeof current !== "object") {
286
- return undefined;
287
- }
288
- current = current[token];
289
- }
290
- return current;
291
- }
292
-
293
- function interpolatePathTemplate(template, context) {
294
- if (typeof template !== "string") {
295
- return "";
296
- }
297
-
298
- return template.replace(/{{\s*([^}]+)\s*}}/g, (_, token) => {
299
- const resolved = getPathValue(context, token);
300
- if (resolved === null || resolved === undefined) {
301
- return "";
302
- }
303
- if (typeof resolved === "string") {
304
- return resolved;
305
- }
306
- return JSON.stringify(resolved);
307
- });
308
- }
309
-
310
- function interpolatePathValue(value, context) {
311
- if (typeof value === "string") {
312
- return value.replace(/{{\s*([^}]+)\s*}}/g, (_, token) => {
313
- const resolved = getPathValue(context, token);
314
- if (resolved === null || resolved === undefined) {
315
- return "";
316
- }
317
- if (typeof resolved === "string") {
318
- return resolved;
319
- }
320
- return JSON.stringify(resolved);
321
- });
322
- }
323
-
324
- if (Array.isArray(value)) {
325
- return value.map((entry) => interpolatePathValue(entry, context));
326
- }
327
-
328
- if (value !== null && value !== undefined && typeof value === "object") {
329
- const output = {};
330
- for (const [key, nested] of Object.entries(value)) {
331
- output[key] = interpolatePathValue(nested, context);
332
- }
333
- return output;
334
- }
335
-
336
- return value;
337
- }
338
-
339
- function maybeParseJson(text) {
340
- if (typeof text !== "string") {
341
- return text;
342
- }
343
-
344
- try {
345
- return JSON.parse(text);
346
- } catch {
347
- const start = text.indexOf("{");
348
- const end = text.lastIndexOf("}");
349
- if (start !== -1 && end !== -1 && end > start) {
350
- const candidate = text.slice(start, end + 1);
351
- try {
352
- return JSON.parse(candidate);
353
- } catch {
354
- return text;
355
- }
356
- }
357
- return text;
358
- }
359
- }
360
-
361
- function matchesJsonSchemaType(expectedType, value) {
362
- if (expectedType === "null") {
363
- return value === null;
364
- }
365
- if (expectedType === "array") {
366
- return Array.isArray(value);
367
- }
368
- if (expectedType === "object") {
369
- return value !== null && typeof value === "object" && !Array.isArray(value);
370
- }
371
- if (expectedType === "integer") {
372
- return typeof value === "number" && Number.isInteger(value);
373
- }
374
- return typeof value === expectedType;
375
- }
376
-
377
- function jsonValueType(value) {
378
- if (value === null) {
379
- return "null";
380
- }
381
- if (Array.isArray(value)) {
382
- return "array";
383
- }
384
- return typeof value;
385
- }
386
-
387
- function equalJsonValue(left, right) {
388
- if (left === right) {
389
- return true;
390
- }
391
- if (
392
- left === null ||
393
- right === null ||
394
- left === undefined ||
395
- right === undefined ||
396
- typeof left !== "object" ||
397
- typeof right !== "object"
398
- ) {
399
- return false;
400
- }
401
-
402
- try {
403
- return JSON.stringify(left) === JSON.stringify(right);
404
- } catch {
405
- return false;
406
- }
407
- }
408
-
409
- function schemaValidationError(schema, value, path = "$") {
410
- if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
411
- return null;
412
- }
413
-
414
- if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
415
- const anyOfErrors = schema.anyOf
416
- .map((nested) => schemaValidationError(nested, value, path))
417
- .filter((entry) => typeof entry === "string");
418
- if (anyOfErrors.length === schema.anyOf.length) {
419
- return `${path} did not satisfy anyOf`;
420
- }
421
- }
422
-
423
- if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
424
- let matched = 0;
425
- for (const nested of schema.oneOf) {
426
- if (schemaValidationError(nested, value, path) === null) {
427
- matched += 1;
428
- }
429
- }
430
- if (matched !== 1) {
431
- return `${path} must satisfy exactly one oneOf schema`;
432
- }
433
- }
434
-
435
- if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
436
- for (const nested of schema.allOf) {
437
- const error = schemaValidationError(nested, value, path);
438
- if (error !== null) {
439
- return error;
440
- }
441
- }
442
- }
443
-
444
- if (Array.isArray(schema.enum) && schema.enum.length > 0) {
445
- const matched = schema.enum.some((candidate) => equalJsonValue(candidate, value));
446
- if (!matched) {
447
- return `${path} must be one of enum values`;
448
- }
449
- }
450
-
451
- if (schema.type !== undefined) {
452
- const expectedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
453
- const matchedType = expectedTypes.some((expectedType) => {
454
- return typeof expectedType === "string" && matchesJsonSchemaType(expectedType, value);
455
- });
456
- if (!matchedType) {
457
- return `${path} expected type ${expectedTypes.join(" | ")}, got ${jsonValueType(value)}`;
458
- }
459
- }
460
-
461
- if (typeof value === "string") {
462
- if (typeof schema.minLength === "number" && value.length < schema.minLength) {
463
- return `${path} must have minLength ${schema.minLength}`;
464
- }
465
- if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
466
- return `${path} must have maxLength ${schema.maxLength}`;
467
- }
468
- if (typeof schema.pattern === "string") {
469
- const pattern = new RegExp(schema.pattern);
470
- if (!pattern.test(value)) {
471
- return `${path} must match pattern ${schema.pattern}`;
472
- }
473
- }
474
- }
475
-
476
- if (typeof value === "number") {
477
- if (typeof schema.minimum === "number" && value < schema.minimum) {
478
- return `${path} must be >= ${schema.minimum}`;
479
- }
480
- if (typeof schema.maximum === "number" && value > schema.maximum) {
481
- return `${path} must be <= ${schema.maximum}`;
482
- }
483
- }
484
-
485
- if (Array.isArray(value)) {
486
- if (typeof schema.minItems === "number" && value.length < schema.minItems) {
487
- return `${path} must have at least ${schema.minItems} items`;
488
- }
489
- if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
490
- return `${path} must have at most ${schema.maxItems} items`;
491
- }
492
- if (schema.items !== undefined) {
493
- for (let index = 0; index < value.length; index += 1) {
494
- const error = schemaValidationError(schema.items, value[index], `${path}[${index}]`);
495
- if (error !== null) {
496
- return error;
497
- }
498
- }
499
- }
500
- }
501
-
502
- const isObjectValue = value !== null && typeof value === "object" && !Array.isArray(value);
503
- if (isObjectValue) {
504
- const properties =
505
- schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties)
506
- ? schema.properties
507
- : {};
508
-
509
- if (Array.isArray(schema.required)) {
510
- for (const key of schema.required) {
511
- if (typeof key !== "string") {
512
- continue;
513
- }
514
- if (!(key in value)) {
515
- return `${path}.${key} is required`;
516
- }
517
- }
518
- }
519
-
520
- for (const [key, propertySchema] of Object.entries(properties)) {
521
- if (!(key in value)) {
522
- continue;
523
- }
524
- const error = schemaValidationError(propertySchema, value[key], `${path}.${key}`);
525
- if (error !== null) {
526
- return error;
527
- }
528
- }
529
-
530
- const knownKeys = new Set(Object.keys(properties));
531
- if (schema.additionalProperties === false) {
532
- for (const key of Object.keys(value)) {
533
- if (!knownKeys.has(key)) {
534
- return `${path}.${key} is not allowed`;
535
- }
536
- }
537
- } else if (
538
- schema.additionalProperties !== undefined &&
539
- schema.additionalProperties !== true
540
- ) {
541
- for (const key of Object.keys(value)) {
542
- if (knownKeys.has(key)) {
543
- continue;
544
- }
545
- const error = schemaValidationError(
546
- schema.additionalProperties,
547
- value[key],
548
- `${path}.${key}`
549
- );
550
- if (error !== null) {
551
- return error;
552
- }
553
- }
554
- }
555
- }
556
-
557
- return null;
558
- }
559
-
560
- function llmOutputSchema(node) {
561
- const schema = node?.config?.output_schema;
562
- if (schema && typeof schema === "object" && !Array.isArray(schema)) {
563
- return schema;
564
- }
565
- return {
566
- type: "object",
567
- additionalProperties: true
568
- };
569
- }
570
-
571
- function evaluateSwitchCondition(condition, context) {
572
- if (typeof condition !== "string") {
573
- return false;
574
- }
575
-
576
- const eq = condition.match(/^\$\.([A-Za-z0-9_.]+)\s*==\s*"([\s\S]*)"$/);
577
- if (eq) {
578
- const left = getPathValue(context, eq[1]);
579
- return String(left ?? "") === eq[2];
580
- }
581
-
582
- const ne = condition.match(/^\$\.([A-Za-z0-9_.]+)\s*!=\s*"([\s\S]*)"$/);
583
- if (ne) {
584
- const left = getPathValue(context, ne[1]);
585
- return String(left ?? "") !== ne[2];
586
- }
587
-
588
- return false;
589
- }
590
-
591
- class BrowserJsClient {
98
+ export class Client {
592
99
  constructor(provider, config) {
593
100
  if (provider !== "openai" && provider !== "openrouter") {
594
101
  throw configError("provider must be 'openai' or 'openrouter' in wasm mode");
595
102
  }
596
-
597
103
  if (!config || typeof config !== "object") {
598
104
  throw configError("config object is required");
599
105
  }
600
-
601
106
  if (typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
602
107
  throw configError("config.apiKey is required");
603
108
  }
604
109
 
605
- this.provider = provider;
606
- this.baseUrl = normalizeBaseUrl(config.baseUrl ?? DEFAULT_BASE_URLS[provider] ?? "");
607
- this.apiKey = config.apiKey;
608
- this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
609
- this.headers = config.headers ?? {};
610
-
611
- if (typeof this.fetchImpl !== "function") {
612
- throw configError("fetch implementation is required");
613
- }
614
- if (!this.baseUrl) {
615
- throw configError("baseUrl is required");
616
- }
617
- }
618
-
619
- async complete(model, promptOrMessages, options = {}) {
620
- if (typeof model !== "string" || model.trim() === "") {
621
- throw configError("model cannot be empty");
622
- }
623
-
624
- const mode = options.mode ?? "standard";
625
- if (mode === "healed_json" || mode === "schema") {
626
- throw runtimeError(
627
- "healed_json and schema modes are not supported in simple-agents-wasm yet"
628
- );
629
- }
630
-
631
- const started = performance.now();
632
- const messages = toMessages(promptOrMessages);
633
- const response = await this.fetchImpl(`${this.baseUrl}/chat/completions`, {
634
- method: "POST",
635
- headers: {
636
- "Content-Type": "application/json",
637
- Authorization: `Bearer ${this.apiKey}`,
638
- ...this.headers
639
- },
640
- body: JSON.stringify({
641
- model,
642
- messages,
643
- max_tokens: options.maxTokens,
644
- temperature: options.temperature,
645
- top_p: options.topP,
646
- stream: false
647
- })
648
- });
649
-
650
- if (!response.ok) {
651
- const body = await response.text();
652
- throw runtimeError(`request failed (${response.status}): ${body.slice(0, 500)}`);
653
- }
654
-
655
- const data = await response.json();
656
- const choice = data?.choices?.[0];
657
- const latencyMs = Math.max(0, Math.round(performance.now() - started));
658
-
659
- return {
660
- id: data?.id ?? "",
661
- model: data?.model ?? model,
662
- role: choice?.message?.role ?? "assistant",
663
- content: choice?.message?.content,
664
- toolCalls: toToolCalls(choice?.message?.tool_calls),
665
- finishReason: choice?.finish_reason,
666
- usage: toUsage(data?.usage),
667
- usageAvailable: Boolean(data?.usage),
668
- latencyMs,
669
- raw: JSON.stringify(data),
670
- healed: undefined,
671
- coerced: undefined
672
- };
673
- }
674
-
675
- async stream(model, promptOrMessages, onChunk, options = {}) {
676
- if (typeof onChunk !== "function") {
677
- throw configError("onChunk callback is required");
678
- }
679
-
680
- const started = performance.now();
681
- const streamBridge = createStreamEventBridge(model, onChunk);
682
-
683
- const result = await this.streamEvents(
684
- model,
685
- promptOrMessages,
686
- (event) => streamBridge.onEvent(event),
687
- options
688
- );
689
-
690
- return streamBridge.mergeResult(result, started);
691
- }
692
-
693
- async streamEvents(model, promptOrMessages, onEvent, options = {}) {
694
- if (typeof model !== "string" || model.trim() === "") {
695
- throw configError("model cannot be empty");
696
- }
697
- if (typeof onEvent !== "function") {
698
- throw configError("onEvent callback is required");
699
- }
700
-
701
- const messages = toMessages(promptOrMessages);
702
- const response = await this.fetchImpl(`${this.baseUrl}/chat/completions`, {
703
- method: "POST",
704
- headers: {
705
- "Content-Type": "application/json",
706
- Authorization: `Bearer ${this.apiKey}`,
707
- ...this.headers
708
- },
709
- body: JSON.stringify({
710
- model,
711
- messages,
712
- max_tokens: options.maxTokens,
713
- temperature: options.temperature,
714
- top_p: options.topP,
715
- stream: true,
716
- stream_options: {
717
- include_usage: true
718
- }
719
- })
720
- });
721
-
722
- if (!response.ok) {
723
- const body = await response.text();
724
- const message = `request failed (${response.status}): ${body.slice(0, 500)}`;
725
- const errorEvent = { eventType: "error", error: { message } };
726
- onEvent(errorEvent);
727
- throw runtimeError(message);
728
- }
729
-
730
- const started = performance.now();
731
- const aggregateState = createStreamAggregator(model);
732
- let usage = {
733
- promptTokens: 0,
734
- completionTokens: 0,
735
- totalTokens: 0
736
- };
737
- let usageAvailable = false;
738
-
739
- try {
740
- for await (const block of iterateSse(response)) {
741
- const parsed = parseSseEventBlock(block);
742
- if (!parsed) {
743
- continue;
744
- }
745
-
746
- if (parsed.done) {
747
- break;
748
- }
749
-
750
- if (!parsed.json) {
751
- continue;
752
- }
753
-
754
- const chunk = parsed.json;
755
- const choice = chunk?.choices?.[0];
756
- const delta = {
757
- id: chunk?.id ?? "",
758
- model: chunk?.model ?? model,
759
- index: choice?.index ?? 0,
760
- role: choice?.delta?.role,
761
- content: choice?.delta?.content,
762
- finishReason: choice?.finish_reason,
763
- raw: parsed.raw
764
- };
765
-
766
- applyDeltaToAggregate(aggregateState, delta);
767
- if (chunk?.usage && typeof chunk.usage === "object") {
768
- usage = toUsage(chunk.usage);
769
- usageAvailable = true;
770
- }
771
-
772
- onEvent({ eventType: "delta", delta });
773
- }
774
-
775
- onEvent({ eventType: "done" });
776
- } catch (error) {
777
- const message = error instanceof Error ? error.message : "stream parsing failed";
778
- onEvent({ eventType: "error", error: { message } });
779
- throw runtimeError(message);
780
- }
781
-
782
- const latencyMs = Math.max(0, Math.round(performance.now() - started));
783
-
784
- return {
785
- id: aggregateState.responseId,
786
- model: aggregateState.responseModel,
787
- role: "assistant",
788
- content: aggregateState.aggregate,
789
- finishReason: aggregateState.finishReason,
790
- usage: {
791
- ...usage
792
- },
793
- usageAvailable,
794
- latencyMs,
795
- raw: undefined,
796
- healed: undefined,
797
- coerced: undefined
798
- };
799
- }
800
-
801
- async runWorkflowYamlString() {
802
- const yamlText = arguments[0];
803
- const workflowInput = arguments[1] ?? {};
804
- const workflowOptions = arguments[2] ?? {};
805
-
806
- const doc = parseWorkflow(yamlText);
807
- if (workflowInput === null || typeof workflowInput !== "object") {
808
- throw configError("workflowInput must be an object");
809
- }
810
-
811
- const context = { ...workflowInput };
812
- const events = [];
813
- const functions =
814
- workflowOptions && typeof workflowOptions.functions === "object"
815
- ? workflowOptions.functions
816
- : {};
817
-
818
- if (isGraphWorkflow(doc)) {
819
- const nodeById = new Map();
820
- for (const node of doc.nodes) {
821
- if (!node || typeof node !== "object" || typeof node.id !== "string") {
822
- throw configError("workflow node is invalid or missing id");
823
- }
824
- nodeById.set(node.id, node);
825
- }
826
-
827
- const edgeMap = new Map();
828
- if (Array.isArray(doc.edges)) {
829
- for (const edge of doc.edges) {
830
- if (!edge || typeof edge.from !== "string" || typeof edge.to !== "string") {
831
- continue;
832
- }
833
- const existing = edgeMap.get(edge.from) ?? [];
834
- existing.push(edge.to);
835
- edgeMap.set(edge.from, existing);
836
- }
837
- }
838
-
839
- const graphContext = {
840
- input: workflowInput,
841
- nodes: {}
842
- };
843
-
844
- const workflowStarted = performance.now();
845
- let workflowTtftMs = 0;
846
- let pointer = doc.entry_node;
847
- let output;
848
- let iterations = 0;
849
- const stepDetails = [];
850
- let totalInputTokens = 0;
851
- let totalOutputTokens = 0;
852
- let totalTokens = 0;
853
- const totalReasoningTokens = 0;
854
- const llmNodesWithoutUsage = [];
855
-
856
- while (typeof pointer === "string" && pointer.length > 0) {
857
- iterations += 1;
858
- if (iterations > 1000) {
859
- throw runtimeError("workflow exceeded maximum step iterations");
860
- }
861
-
862
- const node = nodeById.get(pointer);
863
- if (!node) {
864
- throw configError(`workflow references unknown node '${pointer}'`);
865
- }
866
-
867
- const nodeType = node.node_type ?? {};
868
- const nodeTypeName = Object.keys(nodeType)[0] ?? "unknown";
869
- const nodeStarted = performance.now();
870
- let stepModelName;
871
- let stepPromptTokens;
872
- let stepCompletionTokens;
873
- let stepTotalTokens;
874
- events.push({ stepId: node.id, stepType: nodeTypeName, status: "started" });
875
-
876
- if (nodeType.llm_call) {
877
- const llm = nodeType.llm_call;
878
- const model = llm.model ?? doc.model ?? workflowInput.model;
879
- if (typeof model !== "string" || model.trim().length === 0) {
880
- throw configError(`llm_call node '${node.id}' requires node_type.llm_call.model`);
881
- }
882
-
883
- const prompt = interpolatePathTemplate(node.config?.prompt ?? "", graphContext);
884
- let promptOrMessages = prompt;
885
- if (llm.messages_path === "input.messages") {
886
- const source = getPathValue(graphContext, llm.messages_path);
887
- const history = Array.isArray(source)
888
- ? source
889
- .filter((message) => {
890
- return (
891
- message &&
892
- typeof message === "object" &&
893
- typeof message.role === "string" &&
894
- typeof message.content === "string"
895
- );
896
- })
897
- .map((message) => ({ role: message.role, content: message.content }))
898
- : [];
899
- if (llm.append_prompt_as_user !== false) {
900
- history.push({ role: "user", content: prompt });
901
- }
902
- promptOrMessages = history;
903
- }
904
-
905
- let rawContent = "";
906
- let completion;
907
- if (llm.stream === true) {
908
- completion = await this.streamEvents(
909
- model,
910
- promptOrMessages,
911
- (event) => {
912
- if (event && event.eventType === "delta" && typeof event.delta?.content === "string") {
913
- if (workflowTtftMs === 0) {
914
- const measured = Math.max(0, Math.round(performance.now() - workflowStarted));
915
- workflowTtftMs = measured === 0 ? 1 : measured;
916
- }
917
- rawContent += event.delta.content;
918
- if (typeof workflowOptions?.onEvent === "function") {
919
- workflowOptions.onEvent({
920
- eventType: "node_stream_delta",
921
- nodeId: node.id,
922
- delta: event.delta.content,
923
- model: event.delta.model ?? model
924
- });
925
- }
926
- }
927
- },
928
- {
929
- temperature: llm.temperature
930
- }
931
- );
932
- rawContent = completion.content ?? rawContent;
933
- } else {
934
- completion = await this.complete(model, promptOrMessages, {
935
- temperature: llm.temperature
936
- });
937
- rawContent = completion.content ?? "";
938
- }
939
-
940
- stepModelName = completion?.model ?? model;
941
- if (completion?.usageAvailable === true && completion?.usage && typeof completion.usage === "object") {
942
- const usage = completion.usage;
943
- stepPromptTokens = Number.isFinite(usage.promptTokens) ? usage.promptTokens : 0;
944
- stepCompletionTokens = Number.isFinite(usage.completionTokens)
945
- ? usage.completionTokens
946
- : 0;
947
- stepTotalTokens = Number.isFinite(usage.totalTokens) ? usage.totalTokens : 0;
948
- totalInputTokens += stepPromptTokens;
949
- totalOutputTokens += stepCompletionTokens;
950
- totalTokens += stepTotalTokens;
951
- } else {
952
- llmNodesWithoutUsage.push(node.id);
953
- }
954
-
955
- const parsedOutput = maybeParseJson(rawContent);
956
- const validationError = schemaValidationError(llmOutputSchema(node), parsedOutput);
957
- if (validationError !== null) {
958
- throw runtimeError(
959
- `llm_call node '${node.id}' output failed schema validation: ${validationError}`
960
- );
961
- }
962
- graphContext.nodes[node.id] = {
963
- output: parsedOutput,
964
- raw: rawContent
965
- };
966
- output = parsedOutput;
967
-
968
- const nextTargets = edgeMap.get(node.id) ?? [];
969
- pointer = nextTargets[0] ?? "";
970
- } else if (nodeType.switch) {
971
- const switchSpec = nodeType.switch;
972
- const branches = Array.isArray(switchSpec.branches) ? switchSpec.branches : [];
973
- const matched = branches.find((branch) =>
974
- evaluateSwitchCondition(branch?.condition, graphContext)
975
- );
976
- pointer = matched?.target ?? switchSpec.default ?? "";
977
- } else if (nodeType.custom_worker) {
978
- const handler = nodeType.custom_worker.handler ?? "custom_worker";
979
- const handlerFile = nodeType.custom_worker.handler_file;
980
- const lookupKey =
981
- typeof handlerFile === "string" && handlerFile.length > 0
982
- ? `${handlerFile}#${handler}`
983
- : handler;
984
- const fn = functions[lookupKey];
985
- if (typeof fn !== "function") {
986
- throw runtimeError(
987
- `custom_worker node '${node.id}' requires workflowOptions.functions['${lookupKey}']`
988
- );
989
- }
990
- const workerOutput = await fn(
991
- {
992
- handler,
993
- handler_file: handlerFile,
994
- handler_lookup_key: lookupKey,
995
- payload: interpolatePathValue(node.config?.payload ?? null, graphContext),
996
- nodeId: node.id
997
- },
998
- graphContext
999
- );
1000
- graphContext.nodes[node.id] = {
1001
- output: workerOutput
1002
- };
1003
- output = workerOutput;
1004
- const nextTargets = edgeMap.get(node.id) ?? [];
1005
- pointer = nextTargets[0] ?? "";
1006
- } else {
1007
- throw configError(`unsupported node_type in simple-agents-wasm graph workflow`);
1008
- }
1009
-
1010
- events.push({ stepId: node.id, stepType: nodeTypeName, status: "completed" });
1011
- const elapsedMs = Math.max(0, Math.round(performance.now() - nodeStarted));
1012
- const stepTokensPerSecond =
1013
- Number.isFinite(stepCompletionTokens) && elapsedMs > 0
1014
- ? Math.round((stepCompletionTokens / (elapsedMs / 1000)) * 100) / 100
1015
- : null;
1016
- stepDetails.push(
1017
- buildStepDetail({
1018
- nodeId: node.id,
1019
- nodeKind: nodeTypeName,
1020
- modelName: stepModelName,
1021
- elapsedMs,
1022
- promptTokens: stepPromptTokens,
1023
- completionTokens: stepCompletionTokens,
1024
- totalTokens: stepTotalTokens,
1025
- tokensPerSecond: stepTokensPerSecond
1026
- })
1027
- );
1028
- }
1029
-
1030
- const trace = events
1031
- .filter((event) => event && event.status === "completed")
1032
- .map((event) => event.stepId);
1033
- const terminalNode = trace.at(-1) ?? "";
1034
- const totalElapsedMs = Math.max(0, Math.round(performance.now() - workflowStarted));
1035
- const tokenMetricsAvailable = llmNodesWithoutUsage.length === 0;
1036
- const overallTokensPerSecond =
1037
- totalElapsedMs > 0 ? Math.round((totalOutputTokens / (totalElapsedMs / 1000)) * 100) / 100 : 0;
1038
- const workflowId =
1039
- typeof doc.id === "string" && doc.id.length > 0 ? doc.id : "browser_js_workflow";
1040
- const nerdstats = buildWorkflowNerdstats({
1041
- workflowId,
1042
- terminalNode,
1043
- totalElapsedMs,
1044
- ttftMs: workflowTtftMs,
1045
- stepDetails,
1046
- totalInputTokens,
1047
- totalOutputTokens,
1048
- totalTokens,
1049
- totalReasoningTokens,
1050
- tokensPerSecond: overallTokensPerSecond,
1051
- traceId: "",
1052
- tokenMetricsAvailable,
1053
- tokenMetricsSource: tokenMetricsAvailable ? "provider_usage" : "unavailable",
1054
- llmNodesWithoutUsage
1055
- });
1056
- if (typeof workflowOptions?.onEvent === "function") {
1057
- workflowOptions.onEvent({
1058
- event_type: "workflow_completed",
1059
- metadata: {
1060
- nerdstats
1061
- }
1062
- });
1063
- }
1064
-
1065
- const outputs = {};
1066
- for (const [nodeId, nodeValue] of Object.entries(graphContext.nodes ?? {})) {
1067
- if (nodeValue && typeof nodeValue === "object" && "output" in nodeValue) {
1068
- outputs[nodeId] = nodeValue.output;
1069
- } else {
1070
- outputs[nodeId] = nodeValue;
1071
- }
1072
- }
1073
-
1074
- return {
1075
- workflow_id: workflowId,
1076
- entry_node: doc.entry_node,
1077
- email_text: typeof graphContext.input?.email_text === "string" ? graphContext.input.email_text : "",
1078
- trace,
1079
- outputs,
1080
- terminal_node: terminalNode,
1081
- terminal_output: output,
1082
- step_timings: stepDetails,
1083
- total_elapsed_ms: totalElapsedMs,
1084
- ttft_ms: workflowTtftMs,
1085
- total_input_tokens: totalInputTokens,
1086
- total_output_tokens: totalOutputTokens,
1087
- total_tokens: totalTokens,
1088
- total_reasoning_tokens: totalReasoningTokens,
1089
- tokens_per_second: overallTokensPerSecond,
1090
- trace_id: "",
1091
- metadata: {
1092
- nerdstats
1093
- },
1094
- events,
1095
- context: graphContext,
1096
- status: "ok"
1097
- };
1098
- }
1099
-
1100
- const indexById = new Map();
1101
- doc.steps.forEach((step, index) => {
1102
- if (!step || typeof step !== "object") {
1103
- throw configError(`workflow step at index ${index} must be an object`);
1104
- }
1105
- if (!step.id || !step.type) {
1106
- throw configError(`workflow step at index ${index} requires id and type`);
1107
- }
1108
- indexById.set(step.id, index);
1109
- });
1110
-
1111
- let pointer = 0;
1112
- let output;
1113
- let iterations = 0;
1114
-
1115
- while (pointer < doc.steps.length) {
1116
- iterations += 1;
1117
- if (iterations > 1000) {
1118
- throw runtimeError("workflow exceeded maximum step iterations");
1119
- }
1120
-
1121
- const step = doc.steps[pointer];
1122
- events.push({ stepId: step.id, stepType: step.type, status: "started" });
1123
-
1124
- if (step.type === "set") {
1125
- if (typeof step.key !== "string" || step.key.length === 0) {
1126
- throw configError(`set step '${step.id}' requires key`);
1127
- }
1128
- context[step.key] = interpolate(step.value, context);
1129
- } else if (step.type === "llm_call") {
1130
- const prompt = String(interpolate(step.prompt ?? "", context));
1131
- const model = step.model ?? doc.model ?? context.model;
1132
- if (typeof model !== "string" || model.trim().length === 0) {
1133
- throw configError(`llm_call step '${step.id}' requires a model (step.model, workflow model, or workflowInput.model)`);
1134
- }
1135
- const completion = await this.complete(model, prompt, {
1136
- temperature: step.temperature
1137
- });
1138
- context[step.id] = completion.content ?? "";
1139
- } else if (step.type === "if") {
1140
- const matched = evaluateCondition(step.condition, context);
1141
- const targetId = matched ? step.then : step.else;
1142
- if (targetId) {
1143
- const jumpTo = indexById.get(targetId);
1144
- if (jumpTo === undefined) {
1145
- throw configError(`if step '${step.id}' points to unknown step '${targetId}'`);
1146
- }
1147
- events.push({ stepId: step.id, stepType: step.type, status: "completed" });
1148
- pointer = jumpTo;
1149
- continue;
1150
- }
1151
- } else if (step.type === "call_function") {
1152
- if (typeof step.function !== "string" || step.function.length === 0) {
1153
- throw configError(`call_function step '${step.id}' requires function`);
1154
- }
1155
-
1156
- const fn = functions[step.function];
1157
- if (typeof fn !== "function") {
1158
- throw configError(`call_function step '${step.id}' references unknown function '${step.function}'`);
1159
- }
1160
-
1161
- const args = interpolate(step.args ?? {}, context);
1162
- context[step.id] = await fn(args, context);
1163
- } else if (step.type === "output") {
1164
- output = interpolate(step.text ?? step.value ?? "", context);
1165
- context[step.id] = output;
1166
- } else {
1167
- throw configError(`unsupported step type '${step.type}' in simple-agents-wasm`);
1168
- }
1169
-
1170
- events.push({ stepId: step.id, stepType: step.type, status: "completed" });
1171
-
1172
- if (step.next) {
1173
- const jumpTo = indexById.get(step.next);
1174
- if (jumpTo === undefined) {
1175
- throw configError(`step '${step.id}' points to unknown next step '${step.next}'`);
1176
- }
1177
- pointer = jumpTo;
1178
- continue;
1179
- }
1180
-
1181
- pointer += 1;
1182
- }
1183
-
1184
- return {
1185
- workflow_id:
1186
- typeof doc.id === "string" && doc.id.length > 0 ? doc.id : "browser_js_workflow",
1187
- entry_node: typeof doc.steps?.[0]?.id === "string" ? doc.steps[0].id : "",
1188
- email_text: typeof workflowInput?.email_text === "string" ? workflowInput.email_text : "",
1189
- trace: events
1190
- .filter((event) => event && event.status === "completed")
1191
- .map((event) => event.stepId),
1192
- outputs: { ...context },
1193
- terminal_node: events
1194
- .filter((event) => event && event.status === "completed")
1195
- .map((event) => event.stepId)
1196
- .at(-1) ?? "",
1197
- terminal_output: output,
1198
- events,
1199
- context,
1200
- status: "ok"
1201
- };
1202
- }
1203
-
1204
- async runWorkflowYaml(workflowPath) {
1205
- throw runtimeError(
1206
- `workflow file paths are not supported in browser runtime: ${workflowPath}`
1207
- );
1208
- }
1209
- }
1210
-
1211
- export class Client {
1212
- constructor(provider, config) {
1213
- this.fallbackClient = new BrowserJsClient(provider, config);
1214
110
  this.provider = provider;
1215
111
  this.config = config;
1216
112
  this.rustClient = null;
1217
113
  this.readyPromise = null;
114
+ this.fetchOverrideQueue = Promise.resolve();
1218
115
  }
1219
116
 
1220
117
  async ensureBackend() {
@@ -1226,88 +123,94 @@ export class Client {
1226
123
  }
1227
124
 
1228
125
  this.readyPromise = (async () => {
1229
- if (this.config.fetchImpl && this.config.fetchImpl !== globalThis.fetch) {
1230
- return null;
1231
- }
1232
-
1233
126
  const moduleValue = await loadRustModule();
1234
127
  if (!moduleValue || typeof moduleValue.WasmClient !== "function") {
1235
- return null;
128
+ throw runtimeError("Rust WASM backend is unavailable");
1236
129
  }
130
+ return new moduleValue.WasmClient(this.provider, {
131
+ apiKey: this.config.apiKey,
132
+ baseUrl: this.config.baseUrl,
133
+ headers: this.config.headers
134
+ });
135
+ })();
136
+
137
+ try {
138
+ this.rustClient = await this.readyPromise;
139
+ return this.rustClient;
140
+ } catch (error) {
141
+ this.readyPromise = null;
142
+ throw error;
143
+ }
144
+ }
1237
145
 
146
+ async withFetchOverride(operation) {
147
+ const customFetch = this.config.fetchImpl;
148
+ if (typeof customFetch !== "function" || customFetch === globalThis.fetch) {
149
+ return operation();
150
+ }
151
+
152
+ const run = async () => {
153
+ const previousFetch = globalThis.fetch;
154
+ globalThis.fetch = customFetch;
1238
155
  try {
1239
- const client = new moduleValue.WasmClient(this.provider, {
1240
- apiKey: this.config.apiKey,
1241
- baseUrl: this.config.baseUrl,
1242
- headers: this.config.headers
1243
- });
1244
- this.rustClient = client;
1245
- return client;
1246
- } catch {
1247
- return null;
156
+ return await operation();
157
+ } finally {
158
+ globalThis.fetch = previousFetch;
1248
159
  }
1249
- })();
160
+ };
1250
161
 
1251
- return this.readyPromise;
162
+ this.fetchOverrideQueue = this.fetchOverrideQueue.then(run, run);
163
+ return this.fetchOverrideQueue;
1252
164
  }
1253
165
 
1254
166
  async complete(model, promptOrMessages, options = {}) {
1255
167
  const rust = await this.ensureBackend();
1256
- if (rust) {
1257
- return rust.complete(model, promptOrMessages, options);
1258
- }
1259
- return this.fallbackClient.complete(model, promptOrMessages, options);
168
+ return this.withFetchOverride(async () => rust.complete(model, promptOrMessages, options));
1260
169
  }
1261
170
 
1262
171
  async stream(model, promptOrMessages, onChunk, options = {}) {
1263
172
  const rust = await this.ensureBackend();
1264
- if (rust) {
173
+ return this.withFetchOverride(async () => {
1265
174
  const started = performance.now();
1266
175
  const streamBridge = createStreamEventBridge(model, onChunk);
1267
-
1268
176
  const result = await rust.streamEvents(
1269
177
  model,
1270
178
  promptOrMessages,
1271
179
  (event) => streamBridge.onEvent(event),
1272
180
  options
1273
181
  );
1274
-
1275
182
  return streamBridge.mergeResult(result, started);
1276
- }
1277
-
1278
- return this.fallbackClient.stream(model, promptOrMessages, onChunk, options);
183
+ });
1279
184
  }
1280
185
 
1281
186
  async streamEvents(model, promptOrMessages, onEvent, options = {}) {
1282
187
  const rust = await this.ensureBackend();
1283
- if (rust) {
1284
- return rust.streamEvents(model, promptOrMessages, onEvent, options);
1285
- }
1286
- return this.fallbackClient.streamEvents(model, promptOrMessages, onEvent, options);
188
+ return this.withFetchOverride(async () =>
189
+ rust.streamEvents(model, promptOrMessages, onEvent, options)
190
+ );
1287
191
  }
1288
192
 
1289
193
  async runWorkflowYamlString(yamlText, workflowInput, workflowOptions) {
1290
194
  const rust = await this.ensureBackend();
1291
- if (rust) {
1292
- const result = await rust.runWorkflowYamlString(yamlText, workflowInput, workflowOptions);
195
+ return this.withFetchOverride(async () => {
196
+ let mergedOptions = workflowOptions;
197
+ if (typeof this.config.fetchImpl === "function") {
198
+ mergedOptions =
199
+ workflowOptions && typeof workflowOptions === "object"
200
+ ? { ...workflowOptions, __fetchImpl: this.config.fetchImpl }
201
+ : { __fetchImpl: this.config.fetchImpl };
202
+ }
203
+ const result = await rust.runWorkflowYamlString(yamlText, workflowInput, mergedOptions);
1293
204
  return assertWorkflowResultShape(normalizeWorkflowResult(result));
1294
- }
1295
- const result = await this.fallbackClient.runWorkflowYamlString(
1296
- yamlText,
1297
- workflowInput,
1298
- workflowOptions
1299
- );
1300
- return assertWorkflowResultShape(normalizeWorkflowResult(result));
205
+ });
1301
206
  }
1302
207
 
1303
208
  async runWorkflowYaml(workflowPath, workflowInput) {
1304
209
  const rust = await this.ensureBackend();
1305
- if (rust) {
210
+ return this.withFetchOverride(async () => {
1306
211
  const result = await rust.runWorkflowYaml(workflowPath, workflowInput);
1307
212
  return assertWorkflowResultShape(normalizeWorkflowResult(result));
1308
- }
1309
- const result = await this.fallbackClient.runWorkflowYaml(workflowPath, workflowInput);
1310
- return assertWorkflowResultShape(normalizeWorkflowResult(result));
213
+ });
1311
214
  }
1312
215
 
1313
216
  async run(request) {
@@ -1320,7 +223,6 @@ export class Client {
1320
223
  return this.run(request);
1321
224
  }
1322
225
 
1323
- // Workflow-streaming surface (completion streaming already uses `stream`).
1324
226
  async streamWorkflow(request, onEvent) {
1325
227
  const workflowInput = buildWorkflowInputFromExecutionRequest(request);
1326
228
  const workflowOptions = buildWorkflowOptionsFromExecutionRequest(request, onEvent);
@@ -1329,12 +231,11 @@ export class Client {
1329
231
  }
1330
232
 
1331
233
  export async function hasRustBackend() {
1332
- const moduleValue = await loadRustModule();
1333
- if (!moduleValue || typeof moduleValue.supportsRustWasm !== "function") {
1334
- return false;
1335
- }
1336
-
1337
234
  try {
235
+ const moduleValue = await loadRustModule();
236
+ if (!moduleValue || typeof moduleValue.supportsRustWasm !== "function") {
237
+ return false;
238
+ }
1338
239
  return Boolean(moduleValue.supportsRustWasm());
1339
240
  } catch {
1340
241
  return false;