simple-agents-wasm 0.3.5 → 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/README.md +5 -4
- package/index.d.ts +26 -2
- package/index.js +59 -1158
- package/package.json +5 -1
- package/pkg/simple_agents_wasm.d.ts +0 -3
- package/pkg/simple_agents_wasm.js +4 -13
- package/pkg/simple_agents_wasm_bg.wasm +0 -0
- package/pkg/simple_agents_wasm_bg.wasm.d.ts +0 -1
- package/runtime/rust-runtime.js +21 -3
- package/rust/Cargo.toml +1 -1
- package/rust/src/lib.rs +111 -54
- package/workflow_event.d.ts +35 -0
- package/workflow_stream_printer.d.ts +13 -0
- package/workflow_stream_printer.mjs +28 -0
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1284
|
-
|
|
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
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
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;
|