simple-agents-wasm 0.2.28 → 0.2.31

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/README.md CHANGED
@@ -47,5 +47,8 @@ console.log(result.content);
47
47
  - Browser mode still depends on provider CORS support.
48
48
  - `healed_json` and `schema` completion modes are not supported yet.
49
49
  - `runWorkflowYaml(workflowPath, ...)` is not supported in browser runtime.
50
- - `runWorkflowYamlString(...)` supports string-based workflow execution only.
50
+ - `runWorkflowYamlString(...)` supports string-based workflow execution for:
51
+ - step workflows (`steps` DSL)
52
+ - graph workflows (`entry_node` + `nodes` + `edges`) with `llm_call`, `switch`, and `custom_worker`.
53
+ - Graph `custom_worker` nodes call `workflowOptions.functions[handler]` and throw a runtime error when the handler is missing.
51
54
  - Use `hasRustBackend()` to check whether Rust wasm backend was loaded.
package/index.d.ts CHANGED
@@ -105,7 +105,10 @@ export interface WorkflowRunOptions {
105
105
  trace?: Record<string, unknown>;
106
106
  functions?: Record<
107
107
  string,
108
- (args: Record<string, unknown>, context: Record<string, unknown>) => unknown
108
+ (
109
+ args: Record<string, unknown>,
110
+ context: Record<string, unknown>
111
+ ) => unknown | Promise<unknown>
109
112
  >;
110
113
  }
111
114
 
package/index.js CHANGED
@@ -127,13 +127,343 @@ function parseWorkflow(yamlText) {
127
127
  throw configError("workflow YAML must parse to an object");
128
128
  }
129
129
 
130
- if (!Array.isArray(parsed.steps)) {
131
- throw configError("workflow YAML must contain a steps array");
130
+ if (!Array.isArray(parsed.steps) && !isGraphWorkflow(parsed)) {
131
+ throw configError(
132
+ "workflow YAML must contain either a steps array or graph fields (entry_node + nodes)"
133
+ );
132
134
  }
133
135
 
134
136
  return parsed;
135
137
  }
136
138
 
139
+ function isGraphWorkflow(doc) {
140
+ return (
141
+ doc &&
142
+ typeof doc === "object" &&
143
+ typeof doc.entry_node === "string" &&
144
+ Array.isArray(doc.nodes)
145
+ );
146
+ }
147
+
148
+ function getPathValue(source, path) {
149
+ if (!source || typeof source !== "object") {
150
+ return undefined;
151
+ }
152
+
153
+ const normalized = String(path).trim().replace(/^\$\./, "");
154
+ const tokens = normalized.split(".").filter((token) => token.length > 0);
155
+ let current = source;
156
+ for (const token of tokens) {
157
+ if (!current || typeof current !== "object") {
158
+ return undefined;
159
+ }
160
+ current = current[token];
161
+ }
162
+ return current;
163
+ }
164
+
165
+ function interpolatePathTemplate(template, context) {
166
+ if (typeof template !== "string") {
167
+ return "";
168
+ }
169
+
170
+ return template.replace(/{{\s*([^}]+)\s*}}/g, (_, token) => {
171
+ const resolved = getPathValue(context, token);
172
+ if (resolved === null || resolved === undefined) {
173
+ return "";
174
+ }
175
+ if (typeof resolved === "string") {
176
+ return resolved;
177
+ }
178
+ return JSON.stringify(resolved);
179
+ });
180
+ }
181
+
182
+ function interpolatePathValue(value, context) {
183
+ if (typeof value === "string") {
184
+ return value.replace(/{{\s*([^}]+)\s*}}/g, (_, token) => {
185
+ const resolved = getPathValue(context, token);
186
+ if (resolved === null || resolved === undefined) {
187
+ return "";
188
+ }
189
+ if (typeof resolved === "string") {
190
+ return resolved;
191
+ }
192
+ return JSON.stringify(resolved);
193
+ });
194
+ }
195
+
196
+ if (Array.isArray(value)) {
197
+ return value.map((entry) => interpolatePathValue(entry, context));
198
+ }
199
+
200
+ if (value !== null && value !== undefined && typeof value === "object") {
201
+ const output = {};
202
+ for (const [key, nested] of Object.entries(value)) {
203
+ output[key] = interpolatePathValue(nested, context);
204
+ }
205
+ return output;
206
+ }
207
+
208
+ return value;
209
+ }
210
+
211
+ function maybeParseJson(text) {
212
+ if (typeof text !== "string") {
213
+ return text;
214
+ }
215
+
216
+ try {
217
+ return JSON.parse(text);
218
+ } catch {
219
+ const start = text.indexOf("{");
220
+ const end = text.lastIndexOf("}");
221
+ if (start !== -1 && end !== -1 && end > start) {
222
+ const candidate = text.slice(start, end + 1);
223
+ try {
224
+ return JSON.parse(candidate);
225
+ } catch {
226
+ return text;
227
+ }
228
+ }
229
+ return text;
230
+ }
231
+ }
232
+
233
+ function matchesJsonSchemaType(expectedType, value) {
234
+ if (expectedType === "null") {
235
+ return value === null;
236
+ }
237
+ if (expectedType === "array") {
238
+ return Array.isArray(value);
239
+ }
240
+ if (expectedType === "object") {
241
+ return value !== null && typeof value === "object" && !Array.isArray(value);
242
+ }
243
+ if (expectedType === "integer") {
244
+ return typeof value === "number" && Number.isInteger(value);
245
+ }
246
+ return typeof value === expectedType;
247
+ }
248
+
249
+ function jsonValueType(value) {
250
+ if (value === null) {
251
+ return "null";
252
+ }
253
+ if (Array.isArray(value)) {
254
+ return "array";
255
+ }
256
+ return typeof value;
257
+ }
258
+
259
+ function equalJsonValue(left, right) {
260
+ if (left === right) {
261
+ return true;
262
+ }
263
+ if (
264
+ left === null ||
265
+ right === null ||
266
+ left === undefined ||
267
+ right === undefined ||
268
+ typeof left !== "object" ||
269
+ typeof right !== "object"
270
+ ) {
271
+ return false;
272
+ }
273
+
274
+ try {
275
+ return JSON.stringify(left) === JSON.stringify(right);
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
280
+
281
+ function schemaValidationError(schema, value, path = "$") {
282
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
283
+ return null;
284
+ }
285
+
286
+ if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
287
+ const anyOfErrors = schema.anyOf
288
+ .map((nested) => schemaValidationError(nested, value, path))
289
+ .filter((entry) => typeof entry === "string");
290
+ if (anyOfErrors.length === schema.anyOf.length) {
291
+ return `${path} did not satisfy anyOf`;
292
+ }
293
+ }
294
+
295
+ if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
296
+ let matched = 0;
297
+ for (const nested of schema.oneOf) {
298
+ if (schemaValidationError(nested, value, path) === null) {
299
+ matched += 1;
300
+ }
301
+ }
302
+ if (matched !== 1) {
303
+ return `${path} must satisfy exactly one oneOf schema`;
304
+ }
305
+ }
306
+
307
+ if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
308
+ for (const nested of schema.allOf) {
309
+ const error = schemaValidationError(nested, value, path);
310
+ if (error !== null) {
311
+ return error;
312
+ }
313
+ }
314
+ }
315
+
316
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
317
+ const matched = schema.enum.some((candidate) => equalJsonValue(candidate, value));
318
+ if (!matched) {
319
+ return `${path} must be one of enum values`;
320
+ }
321
+ }
322
+
323
+ if (schema.type !== undefined) {
324
+ const expectedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
325
+ const matchedType = expectedTypes.some((expectedType) => {
326
+ return typeof expectedType === "string" && matchesJsonSchemaType(expectedType, value);
327
+ });
328
+ if (!matchedType) {
329
+ return `${path} expected type ${expectedTypes.join(" | ")}, got ${jsonValueType(value)}`;
330
+ }
331
+ }
332
+
333
+ if (typeof value === "string") {
334
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) {
335
+ return `${path} must have minLength ${schema.minLength}`;
336
+ }
337
+ if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
338
+ return `${path} must have maxLength ${schema.maxLength}`;
339
+ }
340
+ if (typeof schema.pattern === "string") {
341
+ const pattern = new RegExp(schema.pattern);
342
+ if (!pattern.test(value)) {
343
+ return `${path} must match pattern ${schema.pattern}`;
344
+ }
345
+ }
346
+ }
347
+
348
+ if (typeof value === "number") {
349
+ if (typeof schema.minimum === "number" && value < schema.minimum) {
350
+ return `${path} must be >= ${schema.minimum}`;
351
+ }
352
+ if (typeof schema.maximum === "number" && value > schema.maximum) {
353
+ return `${path} must be <= ${schema.maximum}`;
354
+ }
355
+ }
356
+
357
+ if (Array.isArray(value)) {
358
+ if (typeof schema.minItems === "number" && value.length < schema.minItems) {
359
+ return `${path} must have at least ${schema.minItems} items`;
360
+ }
361
+ if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
362
+ return `${path} must have at most ${schema.maxItems} items`;
363
+ }
364
+ if (schema.items !== undefined) {
365
+ for (let index = 0; index < value.length; index += 1) {
366
+ const error = schemaValidationError(schema.items, value[index], `${path}[${index}]`);
367
+ if (error !== null) {
368
+ return error;
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ const isObjectValue = value !== null && typeof value === "object" && !Array.isArray(value);
375
+ if (isObjectValue) {
376
+ const properties =
377
+ schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties)
378
+ ? schema.properties
379
+ : {};
380
+
381
+ if (Array.isArray(schema.required)) {
382
+ for (const key of schema.required) {
383
+ if (typeof key !== "string") {
384
+ continue;
385
+ }
386
+ if (!(key in value)) {
387
+ return `${path}.${key} is required`;
388
+ }
389
+ }
390
+ }
391
+
392
+ for (const [key, propertySchema] of Object.entries(properties)) {
393
+ if (!(key in value)) {
394
+ continue;
395
+ }
396
+ const error = schemaValidationError(propertySchema, value[key], `${path}.${key}`);
397
+ if (error !== null) {
398
+ return error;
399
+ }
400
+ }
401
+
402
+ const knownKeys = new Set(Object.keys(properties));
403
+ if (schema.additionalProperties === false) {
404
+ for (const key of Object.keys(value)) {
405
+ if (!knownKeys.has(key)) {
406
+ return `${path}.${key} is not allowed`;
407
+ }
408
+ }
409
+ } else if (
410
+ schema.additionalProperties !== undefined &&
411
+ schema.additionalProperties !== true
412
+ ) {
413
+ for (const key of Object.keys(value)) {
414
+ if (knownKeys.has(key)) {
415
+ continue;
416
+ }
417
+ const error = schemaValidationError(
418
+ schema.additionalProperties,
419
+ value[key],
420
+ `${path}.${key}`
421
+ );
422
+ if (error !== null) {
423
+ return error;
424
+ }
425
+ }
426
+ }
427
+ }
428
+
429
+ return null;
430
+ }
431
+
432
+ function llmOutputSchema(node) {
433
+ const schema = node?.config?.output_schema;
434
+ if (schema && typeof schema === "object" && !Array.isArray(schema)) {
435
+ return schema;
436
+ }
437
+ return {
438
+ type: "object",
439
+ additionalProperties: true
440
+ };
441
+ }
442
+
443
+ function normalizeSseChunk(chunk) {
444
+ return chunk.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
445
+ }
446
+
447
+ function evaluateSwitchCondition(condition, context) {
448
+ if (typeof condition !== "string") {
449
+ return false;
450
+ }
451
+
452
+ const eq = condition.match(/^\$\.([A-Za-z0-9_.]+)\s*==\s*"([\s\S]*)"$/);
453
+ if (eq) {
454
+ const left = getPathValue(context, eq[1]);
455
+ return String(left ?? "") === eq[2];
456
+ }
457
+
458
+ const ne = condition.match(/^\$\.([A-Za-z0-9_.]+)\s*!=\s*"([\s\S]*)"$/);
459
+ if (ne) {
460
+ const left = getPathValue(context, ne[1]);
461
+ return String(left ?? "") !== ne[2];
462
+ }
463
+
464
+ return false;
465
+ }
466
+
137
467
  function parseSseEventBlock(block) {
138
468
  const lines = block.split("\n");
139
469
  const dataLines = [];
@@ -174,7 +504,7 @@ async function* iterateSse(response) {
174
504
  break;
175
505
  }
176
506
 
177
- buffer += decoder.decode(value, { stream: true });
507
+ buffer += normalizeSseChunk(decoder.decode(value, { stream: true }));
178
508
  let delimiterIndex = buffer.indexOf("\n\n");
179
509
  while (delimiterIndex !== -1) {
180
510
  const block = buffer.slice(0, delimiterIndex).trim();
@@ -186,6 +516,8 @@ async function* iterateSse(response) {
186
516
  }
187
517
  }
188
518
 
519
+ buffer += normalizeSseChunk(decoder.decode());
520
+
189
521
  const trailing = buffer.trim();
190
522
  if (trailing.length > 0) {
191
523
  yield trailing;
@@ -468,6 +800,142 @@ class BrowserJsClient {
468
800
  ? workflowOptions.functions
469
801
  : {};
470
802
 
803
+ if (isGraphWorkflow(doc)) {
804
+ const nodeById = new Map();
805
+ for (const node of doc.nodes) {
806
+ if (!node || typeof node !== "object" || typeof node.id !== "string") {
807
+ throw configError("workflow node is invalid or missing id");
808
+ }
809
+ nodeById.set(node.id, node);
810
+ }
811
+
812
+ const edgeMap = new Map();
813
+ if (Array.isArray(doc.edges)) {
814
+ for (const edge of doc.edges) {
815
+ if (!edge || typeof edge.from !== "string" || typeof edge.to !== "string") {
816
+ continue;
817
+ }
818
+ const existing = edgeMap.get(edge.from) ?? [];
819
+ existing.push(edge.to);
820
+ edgeMap.set(edge.from, existing);
821
+ }
822
+ }
823
+
824
+ const graphContext = {
825
+ input: workflowInput,
826
+ nodes: {}
827
+ };
828
+
829
+ let pointer = doc.entry_node;
830
+ let output;
831
+ let iterations = 0;
832
+
833
+ while (typeof pointer === "string" && pointer.length > 0) {
834
+ iterations += 1;
835
+ if (iterations > 1000) {
836
+ throw runtimeError("workflow exceeded maximum step iterations");
837
+ }
838
+
839
+ const node = nodeById.get(pointer);
840
+ if (!node) {
841
+ throw configError(`workflow references unknown node '${pointer}'`);
842
+ }
843
+
844
+ const nodeType = node.node_type ?? {};
845
+ const nodeTypeName = Object.keys(nodeType)[0] ?? "unknown";
846
+ events.push({ stepId: node.id, stepType: nodeTypeName, status: "started" });
847
+
848
+ if (nodeType.llm_call) {
849
+ const llm = nodeType.llm_call;
850
+ const model = llm.model ?? doc.model ?? workflowInput.model;
851
+ if (typeof model !== "string" || model.trim().length === 0) {
852
+ throw configError(`llm_call node '${node.id}' requires node_type.llm_call.model`);
853
+ }
854
+
855
+ const prompt = interpolatePathTemplate(node.config?.prompt ?? "", graphContext);
856
+ let promptOrMessages = prompt;
857
+ if (llm.messages_path === "input.messages") {
858
+ const source = getPathValue(graphContext, llm.messages_path);
859
+ const history = Array.isArray(source)
860
+ ? source
861
+ .filter((message) => {
862
+ return (
863
+ message &&
864
+ typeof message === "object" &&
865
+ typeof message.role === "string" &&
866
+ typeof message.content === "string"
867
+ );
868
+ })
869
+ .map((message) => ({ role: message.role, content: message.content }))
870
+ : [];
871
+ if (llm.append_prompt_as_user !== false) {
872
+ history.push({ role: "user", content: prompt });
873
+ }
874
+ promptOrMessages = history;
875
+ }
876
+
877
+ const completion = await this.complete(model, promptOrMessages, {
878
+ temperature: llm.temperature
879
+ });
880
+ const parsedOutput = maybeParseJson(completion.content ?? "");
881
+ const validationError = schemaValidationError(llmOutputSchema(node), parsedOutput);
882
+ if (validationError !== null) {
883
+ throw runtimeError(
884
+ `llm_call node '${node.id}' output failed schema validation: ${validationError}`
885
+ );
886
+ }
887
+ graphContext.nodes[node.id] = {
888
+ output: parsedOutput,
889
+ raw: completion.content ?? ""
890
+ };
891
+ output = parsedOutput;
892
+
893
+ const nextTargets = edgeMap.get(node.id) ?? [];
894
+ pointer = nextTargets[0] ?? "";
895
+ } else if (nodeType.switch) {
896
+ const switchSpec = nodeType.switch;
897
+ const branches = Array.isArray(switchSpec.branches) ? switchSpec.branches : [];
898
+ const matched = branches.find((branch) =>
899
+ evaluateSwitchCondition(branch?.condition, graphContext)
900
+ );
901
+ pointer = matched?.target ?? switchSpec.default ?? "";
902
+ } else if (nodeType.custom_worker) {
903
+ const handler = nodeType.custom_worker.handler ?? "custom_worker";
904
+ const fn = functions[handler];
905
+ if (typeof fn !== "function") {
906
+ throw runtimeError(
907
+ `custom_worker node '${node.id}' requires workflowOptions.functions['${handler}']`
908
+ );
909
+ }
910
+ const workerOutput = await fn(
911
+ {
912
+ handler,
913
+ payload: interpolatePathValue(node.config?.payload ?? null, graphContext),
914
+ nodeId: node.id
915
+ },
916
+ graphContext
917
+ );
918
+ graphContext.nodes[node.id] = {
919
+ output: workerOutput
920
+ };
921
+ output = workerOutput;
922
+ const nextTargets = edgeMap.get(node.id) ?? [];
923
+ pointer = nextTargets[0] ?? "";
924
+ } else {
925
+ throw configError(`unsupported node_type in simple-agents-wasm graph workflow`);
926
+ }
927
+
928
+ events.push({ stepId: node.id, stepType: nodeTypeName, status: "completed" });
929
+ }
930
+
931
+ return {
932
+ status: "ok",
933
+ context: graphContext,
934
+ output,
935
+ events
936
+ };
937
+ }
938
+
471
939
  const indexById = new Map();
472
940
  doc.steps.forEach((step, index) => {
473
941
  if (!step || typeof step !== "object") {
@@ -530,7 +998,7 @@ class BrowserJsClient {
530
998
  }
531
999
 
532
1000
  const args = interpolate(step.args ?? {}, context);
533
- context[step.id] = fn(args, context);
1001
+ context[step.id] = await fn(args, context);
534
1002
  } else if (step.type === "output") {
535
1003
  output = interpolate(step.text ?? step.value ?? "", context);
536
1004
  context[step.id] = output;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-agents-wasm",
3
- "version": "0.2.28",
3
+ "version": "0.2.31",
4
4
  "description": "Browser-compatible SimpleAgents client for OpenAI-compatible providers",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -138,6 +138,10 @@ function __wbg_get_imports() {
138
138
  const ret = typeof(arg0) === 'function';
139
139
  return ret;
140
140
  },
141
+ __wbg___wbindgen_is_null_0b605fc6b167c56f: function(arg0) {
142
+ const ret = arg0 === null;
143
+ return ret;
144
+ },
141
145
  __wbg___wbindgen_is_object_781bc9f159099513: function(arg0) {
142
146
  const val = arg0;
143
147
  const ret = typeof(val) === 'object' && val !== null;
@@ -191,6 +195,10 @@ function __wbg_get_imports() {
191
195
  const ret = arg0.call(arg1);
192
196
  return ret;
193
197
  }, arguments); },
198
+ __wbg_construct_526a6dedb187eba9: function() { return handleError(function (arg0, arg1) {
199
+ const ret = Reflect.construct(arg0, arg1);
200
+ return ret;
201
+ }, arguments); },
194
202
  __wbg_done_08ce71ee07e3bd17: function(arg0) {
195
203
  const ret = arg0.done;
196
204
  return ret;
@@ -386,7 +394,7 @@ function __wbg_get_imports() {
386
394
  return ret;
387
395
  },
388
396
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
389
- // Cast intrinsic for `Closure(Closure { dtor_idx: 85, function: Function { arguments: [Externref], shim_idx: 86, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
397
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 106, function: Function { arguments: [Externref], shim_idx: 107, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
390
398
  const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h5b569a9b0c99a6ce, wasm_bindgen__convert__closures_____invoke__h26e23bd7929d5711);
391
399
  return ret;
392
400
  },
Binary file
package/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "simple-agents-wasm-rust"
3
- version = "0.2.28"
3
+ version = "0.2.31"
4
4
  edition = "2021"
5
5
  license = "MIT OR Apache-2.0"
6
6