simple-agents-wasm 0.2.30 → 0.2.32
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.d.ts +1 -0
- package/index.js +406 -120
- package/package.json +1 -1
- package/pkg/simple_agents_wasm.d.ts +3 -3
- package/pkg/simple_agents_wasm.js +6 -6
- package/pkg/simple_agents_wasm_bg.wasm +0 -0
- package/pkg/simple_agents_wasm_bg.wasm.d.ts +3 -3
- package/rust/Cargo.toml +1 -1
- package/rust/src/lib.rs +222 -8
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -62,10 +62,175 @@ function toToolCalls(toolCalls) {
|
|
|
62
62
|
}));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function createStreamEventBridge(model, onChunk) {
|
|
66
|
+
let aggregate = "";
|
|
67
|
+
let finalId = "";
|
|
68
|
+
let finalModel = model;
|
|
69
|
+
let finalFinishReason;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
onEvent(event) {
|
|
73
|
+
if (event.eventType === "delta") {
|
|
74
|
+
const delta = event.delta;
|
|
75
|
+
if (!delta) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!finalId && delta.id) {
|
|
80
|
+
finalId = delta.id;
|
|
81
|
+
}
|
|
82
|
+
if (delta.model) {
|
|
83
|
+
finalModel = delta.model;
|
|
84
|
+
}
|
|
85
|
+
if (delta.content) {
|
|
86
|
+
aggregate += delta.content;
|
|
87
|
+
}
|
|
88
|
+
if (delta.finishReason) {
|
|
89
|
+
finalFinishReason = delta.finishReason;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onChunk({
|
|
93
|
+
id: delta.id,
|
|
94
|
+
model: delta.model,
|
|
95
|
+
content: delta.content,
|
|
96
|
+
finishReason: delta.finishReason,
|
|
97
|
+
raw: delta.raw
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (event.eventType === "error") {
|
|
102
|
+
onChunk({
|
|
103
|
+
id: finalId || "error",
|
|
104
|
+
model: finalModel,
|
|
105
|
+
error: event.error?.message ?? "stream error"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
mergeResult(result, started) {
|
|
110
|
+
return {
|
|
111
|
+
...result,
|
|
112
|
+
id: result.id || finalId,
|
|
113
|
+
model: result.model || finalModel,
|
|
114
|
+
content: result.content ?? aggregate,
|
|
115
|
+
finishReason: result.finishReason ?? finalFinishReason,
|
|
116
|
+
latencyMs: Math.max(0, Math.round(performance.now() - started))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function assertWorkflowResultShape(result) {
|
|
123
|
+
if (result === null || typeof result !== "object") {
|
|
124
|
+
throw runtimeError(
|
|
125
|
+
"workflow result contract mismatch: expected an object with workflow_id and outputs"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!("workflow_id" in result) || !("outputs" in result)) {
|
|
130
|
+
throw runtimeError(
|
|
131
|
+
"workflow result contract mismatch: expected keys 'workflow_id' and 'outputs'"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeWorkflowResult(result) {
|
|
139
|
+
if (result === null || typeof result !== "object") {
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
if ("workflow_id" in result && "outputs" in result) {
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
if (!("context" in result) || !result.context || typeof result.context !== "object") {
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const context = result.context;
|
|
150
|
+
const nodeOutputs =
|
|
151
|
+
context && typeof context === "object" && context.nodes && typeof context.nodes === "object"
|
|
152
|
+
? context.nodes
|
|
153
|
+
: context;
|
|
154
|
+
const trace = Array.isArray(result.events)
|
|
155
|
+
? result.events
|
|
156
|
+
.filter((event) => event && event.status === "completed" && typeof event.stepId === "string")
|
|
157
|
+
.map((event) => event.stepId)
|
|
158
|
+
: [];
|
|
159
|
+
const terminalNode = trace.at(-1) ?? "";
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
workflow_id: typeof result.workflow_id === "string" ? result.workflow_id : "wasm_workflow",
|
|
163
|
+
entry_node: typeof result.entry_node === "string" ? result.entry_node : trace[0] ?? "",
|
|
164
|
+
email_text: typeof context?.input?.email_text === "string" ? context.input.email_text : "",
|
|
165
|
+
trace,
|
|
166
|
+
outputs: nodeOutputs,
|
|
167
|
+
terminal_node: typeof result.terminal_node === "string" ? result.terminal_node : terminalNode,
|
|
168
|
+
terminal_output: result.output,
|
|
169
|
+
events: Array.isArray(result.events) ? result.events : [],
|
|
170
|
+
status: typeof result.status === "string" ? result.status : "ok"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
65
174
|
function normalizeBaseUrl(baseUrl) {
|
|
66
175
|
return baseUrl.replace(/\/$/, "");
|
|
67
176
|
}
|
|
68
177
|
|
|
178
|
+
function finiteNumberOrNull(value) {
|
|
179
|
+
return Number.isFinite(value) ? value : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildStepDetail(step) {
|
|
183
|
+
const detail = {
|
|
184
|
+
elapsed_ms: step.elapsedMs,
|
|
185
|
+
node_id: step.nodeId,
|
|
186
|
+
node_kind: step.nodeKind
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (step.nodeKind === "llm_call") {
|
|
190
|
+
if (typeof step.modelName === "string") {
|
|
191
|
+
detail.model_name = step.modelName;
|
|
192
|
+
}
|
|
193
|
+
const promptTokens = finiteNumberOrNull(step.promptTokens);
|
|
194
|
+
if (promptTokens !== null) {
|
|
195
|
+
detail.prompt_tokens = promptTokens;
|
|
196
|
+
}
|
|
197
|
+
const completionTokens = finiteNumberOrNull(step.completionTokens);
|
|
198
|
+
if (completionTokens !== null) {
|
|
199
|
+
detail.completion_tokens = completionTokens;
|
|
200
|
+
}
|
|
201
|
+
const totalTokens = finiteNumberOrNull(step.totalTokens);
|
|
202
|
+
if (totalTokens !== null) {
|
|
203
|
+
detail.total_tokens = totalTokens;
|
|
204
|
+
}
|
|
205
|
+
detail.reasoning_tokens = 0;
|
|
206
|
+
const tokensPerSecond = finiteNumberOrNull(step.tokensPerSecond);
|
|
207
|
+
if (tokensPerSecond !== null) {
|
|
208
|
+
detail.tokens_per_second = tokensPerSecond;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return detail;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildWorkflowNerdstats(summary) {
|
|
216
|
+
return {
|
|
217
|
+
workflow_id: summary.workflowId,
|
|
218
|
+
terminal_node: summary.terminalNode,
|
|
219
|
+
total_elapsed_ms: summary.totalElapsedMs,
|
|
220
|
+
ttft_ms: summary.ttftMs,
|
|
221
|
+
step_details: summary.stepDetails,
|
|
222
|
+
total_input_tokens: summary.totalInputTokens,
|
|
223
|
+
total_output_tokens: summary.totalOutputTokens,
|
|
224
|
+
total_tokens: summary.totalTokens,
|
|
225
|
+
total_reasoning_tokens: summary.totalReasoningTokens,
|
|
226
|
+
tokens_per_second: summary.tokensPerSecond,
|
|
227
|
+
trace_id: summary.traceId,
|
|
228
|
+
token_metrics_available: summary.tokenMetricsAvailable,
|
|
229
|
+
token_metrics_source: summary.tokenMetricsSource,
|
|
230
|
+
llm_nodes_without_usage: summary.llmNodesWithoutUsage
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
69
234
|
function interpolate(value, context) {
|
|
70
235
|
if (typeof value === "string") {
|
|
71
236
|
return value.replace(/{{\s*([^}]+)\s*}}/g, (_, key) => {
|
|
@@ -179,6 +344,35 @@ function interpolatePathTemplate(template, context) {
|
|
|
179
344
|
});
|
|
180
345
|
}
|
|
181
346
|
|
|
347
|
+
function interpolatePathValue(value, context) {
|
|
348
|
+
if (typeof value === "string") {
|
|
349
|
+
return value.replace(/{{\s*([^}]+)\s*}}/g, (_, token) => {
|
|
350
|
+
const resolved = getPathValue(context, token);
|
|
351
|
+
if (resolved === null || resolved === undefined) {
|
|
352
|
+
return "";
|
|
353
|
+
}
|
|
354
|
+
if (typeof resolved === "string") {
|
|
355
|
+
return resolved;
|
|
356
|
+
}
|
|
357
|
+
return JSON.stringify(resolved);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (Array.isArray(value)) {
|
|
362
|
+
return value.map((entry) => interpolatePathValue(entry, context));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (value !== null && value !== undefined && typeof value === "object") {
|
|
366
|
+
const output = {};
|
|
367
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
368
|
+
output[key] = interpolatePathValue(nested, context);
|
|
369
|
+
}
|
|
370
|
+
return output;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
|
|
182
376
|
function maybeParseJson(text) {
|
|
183
377
|
if (typeof text !== "string") {
|
|
184
378
|
return text;
|
|
@@ -584,63 +778,17 @@ class BrowserJsClient {
|
|
|
584
778
|
throw configError("onChunk callback is required");
|
|
585
779
|
}
|
|
586
780
|
|
|
587
|
-
let aggregate = "";
|
|
588
|
-
let finalId = "";
|
|
589
|
-
let finalModel = model;
|
|
590
|
-
let finalFinishReason;
|
|
591
781
|
const started = performance.now();
|
|
782
|
+
const streamBridge = createStreamEventBridge(model, onChunk);
|
|
592
783
|
|
|
593
784
|
const result = await this.streamEvents(
|
|
594
785
|
model,
|
|
595
786
|
promptOrMessages,
|
|
596
|
-
(event) =>
|
|
597
|
-
if (event.eventType === "delta") {
|
|
598
|
-
const delta = event.delta;
|
|
599
|
-
if (!delta) {
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (delta.id && finalId.length === 0) {
|
|
604
|
-
finalId = delta.id;
|
|
605
|
-
}
|
|
606
|
-
if (delta.model) {
|
|
607
|
-
finalModel = delta.model;
|
|
608
|
-
}
|
|
609
|
-
if (delta.content) {
|
|
610
|
-
aggregate += delta.content;
|
|
611
|
-
}
|
|
612
|
-
if (delta.finishReason) {
|
|
613
|
-
finalFinishReason = delta.finishReason;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
onChunk({
|
|
617
|
-
id: delta.id,
|
|
618
|
-
model: delta.model,
|
|
619
|
-
content: delta.content,
|
|
620
|
-
finishReason: delta.finishReason,
|
|
621
|
-
raw: delta.raw
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (event.eventType === "error") {
|
|
626
|
-
onChunk({
|
|
627
|
-
id: finalId || "error",
|
|
628
|
-
model: finalModel,
|
|
629
|
-
error: event.error?.message ?? "stream error"
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
},
|
|
787
|
+
(event) => streamBridge.onEvent(event),
|
|
633
788
|
options
|
|
634
789
|
);
|
|
635
790
|
|
|
636
|
-
return
|
|
637
|
-
...result,
|
|
638
|
-
id: result.id || finalId,
|
|
639
|
-
model: result.model || finalModel,
|
|
640
|
-
content: result.content ?? aggregate,
|
|
641
|
-
finishReason: result.finishReason ?? finalFinishReason,
|
|
642
|
-
latencyMs: Math.max(0, Math.round(performance.now() - started))
|
|
643
|
-
};
|
|
791
|
+
return streamBridge.mergeResult(result, started);
|
|
644
792
|
}
|
|
645
793
|
|
|
646
794
|
async streamEvents(model, promptOrMessages, onEvent, options = {}) {
|
|
@@ -665,7 +813,10 @@ class BrowserJsClient {
|
|
|
665
813
|
max_tokens: options.maxTokens,
|
|
666
814
|
temperature: options.temperature,
|
|
667
815
|
top_p: options.topP,
|
|
668
|
-
stream: true
|
|
816
|
+
stream: true,
|
|
817
|
+
stream_options: {
|
|
818
|
+
include_usage: true
|
|
819
|
+
}
|
|
669
820
|
})
|
|
670
821
|
});
|
|
671
822
|
|
|
@@ -682,6 +833,12 @@ class BrowserJsClient {
|
|
|
682
833
|
let responseModel = model;
|
|
683
834
|
let aggregate = "";
|
|
684
835
|
let finishReason;
|
|
836
|
+
let usage = {
|
|
837
|
+
promptTokens: 0,
|
|
838
|
+
completionTokens: 0,
|
|
839
|
+
totalTokens: 0
|
|
840
|
+
};
|
|
841
|
+
let usageAvailable = false;
|
|
685
842
|
|
|
686
843
|
try {
|
|
687
844
|
for await (const block of iterateSse(response)) {
|
|
@@ -722,6 +879,10 @@ class BrowserJsClient {
|
|
|
722
879
|
if (delta.finishReason) {
|
|
723
880
|
finishReason = delta.finishReason;
|
|
724
881
|
}
|
|
882
|
+
if (chunk?.usage && typeof chunk.usage === "object") {
|
|
883
|
+
usage = toUsage(chunk.usage);
|
|
884
|
+
usageAvailable = true;
|
|
885
|
+
}
|
|
725
886
|
|
|
726
887
|
onEvent({ eventType: "delta", delta });
|
|
727
888
|
}
|
|
@@ -742,11 +903,9 @@ class BrowserJsClient {
|
|
|
742
903
|
content: aggregate,
|
|
743
904
|
finishReason,
|
|
744
905
|
usage: {
|
|
745
|
-
|
|
746
|
-
completionTokens: 0,
|
|
747
|
-
totalTokens: 0
|
|
906
|
+
...usage
|
|
748
907
|
},
|
|
749
|
-
usageAvailable
|
|
908
|
+
usageAvailable,
|
|
750
909
|
latencyMs,
|
|
751
910
|
raw: undefined,
|
|
752
911
|
healed: undefined,
|
|
@@ -797,9 +956,17 @@ class BrowserJsClient {
|
|
|
797
956
|
nodes: {}
|
|
798
957
|
};
|
|
799
958
|
|
|
959
|
+
const workflowStarted = performance.now();
|
|
960
|
+
let workflowTtftMs = 0;
|
|
800
961
|
let pointer = doc.entry_node;
|
|
801
962
|
let output;
|
|
802
963
|
let iterations = 0;
|
|
964
|
+
const stepDetails = [];
|
|
965
|
+
let totalInputTokens = 0;
|
|
966
|
+
let totalOutputTokens = 0;
|
|
967
|
+
let totalTokens = 0;
|
|
968
|
+
const totalReasoningTokens = 0;
|
|
969
|
+
const llmNodesWithoutUsage = [];
|
|
803
970
|
|
|
804
971
|
while (typeof pointer === "string" && pointer.length > 0) {
|
|
805
972
|
iterations += 1;
|
|
@@ -814,6 +981,11 @@ class BrowserJsClient {
|
|
|
814
981
|
|
|
815
982
|
const nodeType = node.node_type ?? {};
|
|
816
983
|
const nodeTypeName = Object.keys(nodeType)[0] ?? "unknown";
|
|
984
|
+
const nodeStarted = performance.now();
|
|
985
|
+
let stepModelName;
|
|
986
|
+
let stepPromptTokens;
|
|
987
|
+
let stepCompletionTokens;
|
|
988
|
+
let stepTotalTokens;
|
|
817
989
|
events.push({ stepId: node.id, stepType: nodeTypeName, status: "started" });
|
|
818
990
|
|
|
819
991
|
if (nodeType.llm_call) {
|
|
@@ -845,10 +1017,57 @@ class BrowserJsClient {
|
|
|
845
1017
|
promptOrMessages = history;
|
|
846
1018
|
}
|
|
847
1019
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1020
|
+
let rawContent = "";
|
|
1021
|
+
let completion;
|
|
1022
|
+
if (llm.stream === true) {
|
|
1023
|
+
completion = await this.streamEvents(
|
|
1024
|
+
model,
|
|
1025
|
+
promptOrMessages,
|
|
1026
|
+
(event) => {
|
|
1027
|
+
if (event && event.eventType === "delta" && typeof event.delta?.content === "string") {
|
|
1028
|
+
if (workflowTtftMs === 0) {
|
|
1029
|
+
const measured = Math.max(0, Math.round(performance.now() - workflowStarted));
|
|
1030
|
+
workflowTtftMs = measured === 0 ? 1 : measured;
|
|
1031
|
+
}
|
|
1032
|
+
rawContent += event.delta.content;
|
|
1033
|
+
if (typeof workflowOptions?.onEvent === "function") {
|
|
1034
|
+
workflowOptions.onEvent({
|
|
1035
|
+
eventType: "node_stream_delta",
|
|
1036
|
+
nodeId: node.id,
|
|
1037
|
+
delta: event.delta.content,
|
|
1038
|
+
model: event.delta.model ?? model
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
temperature: llm.temperature
|
|
1045
|
+
}
|
|
1046
|
+
);
|
|
1047
|
+
rawContent = completion.content ?? rawContent;
|
|
1048
|
+
} else {
|
|
1049
|
+
completion = await this.complete(model, promptOrMessages, {
|
|
1050
|
+
temperature: llm.temperature
|
|
1051
|
+
});
|
|
1052
|
+
rawContent = completion.content ?? "";
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
stepModelName = completion?.model ?? model;
|
|
1056
|
+
if (completion?.usageAvailable === true && completion?.usage && typeof completion.usage === "object") {
|
|
1057
|
+
const usage = completion.usage;
|
|
1058
|
+
stepPromptTokens = Number.isFinite(usage.promptTokens) ? usage.promptTokens : 0;
|
|
1059
|
+
stepCompletionTokens = Number.isFinite(usage.completionTokens)
|
|
1060
|
+
? usage.completionTokens
|
|
1061
|
+
: 0;
|
|
1062
|
+
stepTotalTokens = Number.isFinite(usage.totalTokens) ? usage.totalTokens : 0;
|
|
1063
|
+
totalInputTokens += stepPromptTokens;
|
|
1064
|
+
totalOutputTokens += stepCompletionTokens;
|
|
1065
|
+
totalTokens += stepTotalTokens;
|
|
1066
|
+
} else {
|
|
1067
|
+
llmNodesWithoutUsage.push(node.id);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const parsedOutput = maybeParseJson(rawContent);
|
|
852
1071
|
const validationError = schemaValidationError(llmOutputSchema(node), parsedOutput);
|
|
853
1072
|
if (validationError !== null) {
|
|
854
1073
|
throw runtimeError(
|
|
@@ -857,7 +1076,7 @@ class BrowserJsClient {
|
|
|
857
1076
|
}
|
|
858
1077
|
graphContext.nodes[node.id] = {
|
|
859
1078
|
output: parsedOutput,
|
|
860
|
-
raw:
|
|
1079
|
+
raw: rawContent
|
|
861
1080
|
};
|
|
862
1081
|
output = parsedOutput;
|
|
863
1082
|
|
|
@@ -872,16 +1091,23 @@ class BrowserJsClient {
|
|
|
872
1091
|
pointer = matched?.target ?? switchSpec.default ?? "";
|
|
873
1092
|
} else if (nodeType.custom_worker) {
|
|
874
1093
|
const handler = nodeType.custom_worker.handler ?? "custom_worker";
|
|
875
|
-
const
|
|
1094
|
+
const handlerFile = nodeType.custom_worker.handler_file;
|
|
1095
|
+
const lookupKey =
|
|
1096
|
+
typeof handlerFile === "string" && handlerFile.length > 0
|
|
1097
|
+
? `${handlerFile}#${handler}`
|
|
1098
|
+
: handler;
|
|
1099
|
+
const fn = functions[lookupKey];
|
|
876
1100
|
if (typeof fn !== "function") {
|
|
877
1101
|
throw runtimeError(
|
|
878
|
-
`custom_worker node '${node.id}' requires workflowOptions.functions['${
|
|
1102
|
+
`custom_worker node '${node.id}' requires workflowOptions.functions['${lookupKey}']`
|
|
879
1103
|
);
|
|
880
1104
|
}
|
|
881
1105
|
const workerOutput = await fn(
|
|
882
1106
|
{
|
|
883
1107
|
handler,
|
|
884
|
-
|
|
1108
|
+
handler_file: handlerFile,
|
|
1109
|
+
handler_lookup_key: lookupKey,
|
|
1110
|
+
payload: interpolatePathValue(node.config?.payload ?? null, graphContext),
|
|
885
1111
|
nodeId: node.id
|
|
886
1112
|
},
|
|
887
1113
|
graphContext
|
|
@@ -897,13 +1123,92 @@ class BrowserJsClient {
|
|
|
897
1123
|
}
|
|
898
1124
|
|
|
899
1125
|
events.push({ stepId: node.id, stepType: nodeTypeName, status: "completed" });
|
|
1126
|
+
const elapsedMs = Math.max(0, Math.round(performance.now() - nodeStarted));
|
|
1127
|
+
const stepTokensPerSecond =
|
|
1128
|
+
Number.isFinite(stepCompletionTokens) && elapsedMs > 0
|
|
1129
|
+
? Math.round((stepCompletionTokens / (elapsedMs / 1000)) * 100) / 100
|
|
1130
|
+
: null;
|
|
1131
|
+
stepDetails.push(
|
|
1132
|
+
buildStepDetail({
|
|
1133
|
+
nodeId: node.id,
|
|
1134
|
+
nodeKind: nodeTypeName,
|
|
1135
|
+
modelName: stepModelName,
|
|
1136
|
+
elapsedMs,
|
|
1137
|
+
promptTokens: stepPromptTokens,
|
|
1138
|
+
completionTokens: stepCompletionTokens,
|
|
1139
|
+
totalTokens: stepTotalTokens,
|
|
1140
|
+
tokensPerSecond: stepTokensPerSecond
|
|
1141
|
+
})
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const trace = events
|
|
1146
|
+
.filter((event) => event && event.status === "completed")
|
|
1147
|
+
.map((event) => event.stepId);
|
|
1148
|
+
const terminalNode = trace.at(-1) ?? "";
|
|
1149
|
+
const totalElapsedMs = Math.max(0, Math.round(performance.now() - workflowStarted));
|
|
1150
|
+
const tokenMetricsAvailable = llmNodesWithoutUsage.length === 0;
|
|
1151
|
+
const overallTokensPerSecond =
|
|
1152
|
+
totalElapsedMs > 0 ? Math.round((totalOutputTokens / (totalElapsedMs / 1000)) * 100) / 100 : 0;
|
|
1153
|
+
const workflowId =
|
|
1154
|
+
typeof doc.id === "string" && doc.id.length > 0 ? doc.id : "browser_js_workflow";
|
|
1155
|
+
const nerdstats = buildWorkflowNerdstats({
|
|
1156
|
+
workflowId,
|
|
1157
|
+
terminalNode,
|
|
1158
|
+
totalElapsedMs,
|
|
1159
|
+
ttftMs: workflowTtftMs,
|
|
1160
|
+
stepDetails,
|
|
1161
|
+
totalInputTokens,
|
|
1162
|
+
totalOutputTokens,
|
|
1163
|
+
totalTokens,
|
|
1164
|
+
totalReasoningTokens,
|
|
1165
|
+
tokensPerSecond: overallTokensPerSecond,
|
|
1166
|
+
traceId: "",
|
|
1167
|
+
tokenMetricsAvailable,
|
|
1168
|
+
tokenMetricsSource: tokenMetricsAvailable ? "provider_usage" : "unavailable",
|
|
1169
|
+
llmNodesWithoutUsage
|
|
1170
|
+
});
|
|
1171
|
+
if (typeof workflowOptions?.onEvent === "function") {
|
|
1172
|
+
workflowOptions.onEvent({
|
|
1173
|
+
event_type: "workflow_completed",
|
|
1174
|
+
metadata: {
|
|
1175
|
+
nerdstats
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const outputs = {};
|
|
1181
|
+
for (const [nodeId, nodeValue] of Object.entries(graphContext.nodes ?? {})) {
|
|
1182
|
+
if (nodeValue && typeof nodeValue === "object" && "output" in nodeValue) {
|
|
1183
|
+
outputs[nodeId] = nodeValue.output;
|
|
1184
|
+
} else {
|
|
1185
|
+
outputs[nodeId] = nodeValue;
|
|
1186
|
+
}
|
|
900
1187
|
}
|
|
901
1188
|
|
|
902
1189
|
return {
|
|
903
|
-
|
|
1190
|
+
workflow_id: workflowId,
|
|
1191
|
+
entry_node: doc.entry_node,
|
|
1192
|
+
email_text: typeof graphContext.input?.email_text === "string" ? graphContext.input.email_text : "",
|
|
1193
|
+
trace,
|
|
1194
|
+
outputs,
|
|
1195
|
+
terminal_node: terminalNode,
|
|
1196
|
+
terminal_output: output,
|
|
1197
|
+
step_timings: stepDetails,
|
|
1198
|
+
total_elapsed_ms: totalElapsedMs,
|
|
1199
|
+
ttft_ms: workflowTtftMs,
|
|
1200
|
+
total_input_tokens: totalInputTokens,
|
|
1201
|
+
total_output_tokens: totalOutputTokens,
|
|
1202
|
+
total_tokens: totalTokens,
|
|
1203
|
+
total_reasoning_tokens: totalReasoningTokens,
|
|
1204
|
+
tokens_per_second: overallTokensPerSecond,
|
|
1205
|
+
trace_id: "",
|
|
1206
|
+
metadata: {
|
|
1207
|
+
nerdstats
|
|
1208
|
+
},
|
|
1209
|
+
events,
|
|
904
1210
|
context: graphContext,
|
|
905
|
-
|
|
906
|
-
events
|
|
1211
|
+
status: "ok"
|
|
907
1212
|
};
|
|
908
1213
|
}
|
|
909
1214
|
|
|
@@ -992,10 +1297,22 @@ class BrowserJsClient {
|
|
|
992
1297
|
}
|
|
993
1298
|
|
|
994
1299
|
return {
|
|
995
|
-
|
|
1300
|
+
workflow_id:
|
|
1301
|
+
typeof doc.id === "string" && doc.id.length > 0 ? doc.id : "browser_js_workflow",
|
|
1302
|
+
entry_node: typeof doc.steps?.[0]?.id === "string" ? doc.steps[0].id : "",
|
|
1303
|
+
email_text: typeof workflowInput?.email_text === "string" ? workflowInput.email_text : "",
|
|
1304
|
+
trace: events
|
|
1305
|
+
.filter((event) => event && event.status === "completed")
|
|
1306
|
+
.map((event) => event.stepId),
|
|
1307
|
+
outputs: { ...context },
|
|
1308
|
+
terminal_node: events
|
|
1309
|
+
.filter((event) => event && event.status === "completed")
|
|
1310
|
+
.map((event) => event.stepId)
|
|
1311
|
+
.at(-1) ?? "",
|
|
1312
|
+
terminal_output: output,
|
|
1313
|
+
events,
|
|
996
1314
|
context,
|
|
997
|
-
|
|
998
|
-
events
|
|
1315
|
+
status: "ok"
|
|
999
1316
|
};
|
|
1000
1317
|
}
|
|
1001
1318
|
|
|
@@ -1014,7 +1331,7 @@ async function loadRustModule() {
|
|
|
1014
1331
|
try {
|
|
1015
1332
|
const moduleValue = await import("./pkg/simple_agents_wasm.js");
|
|
1016
1333
|
const wasmUrl = new URL("./pkg/simple_agents_wasm_bg.wasm", import.meta.url);
|
|
1017
|
-
await moduleValue.default(wasmUrl);
|
|
1334
|
+
await moduleValue.default({ module_or_path: wasmUrl });
|
|
1018
1335
|
return moduleValue;
|
|
1019
1336
|
} catch {
|
|
1020
1337
|
return null;
|
|
@@ -1079,56 +1396,17 @@ export class Client {
|
|
|
1079
1396
|
async stream(model, promptOrMessages, onChunk, options = {}) {
|
|
1080
1397
|
const rust = await this.ensureBackend();
|
|
1081
1398
|
if (rust) {
|
|
1082
|
-
let aggregate = "";
|
|
1083
|
-
let finalId = "";
|
|
1084
|
-
let finalModel = model;
|
|
1085
|
-
let finalFinishReason;
|
|
1086
1399
|
const started = performance.now();
|
|
1400
|
+
const streamBridge = createStreamEventBridge(model, onChunk);
|
|
1087
1401
|
|
|
1088
|
-
const result = await rust.streamEvents(
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (!finalId && delta.id) {
|
|
1095
|
-
finalId = delta.id;
|
|
1096
|
-
}
|
|
1097
|
-
if (delta.model) {
|
|
1098
|
-
finalModel = delta.model;
|
|
1099
|
-
}
|
|
1100
|
-
if (delta.content) {
|
|
1101
|
-
aggregate += delta.content;
|
|
1102
|
-
}
|
|
1103
|
-
if (delta.finishReason) {
|
|
1104
|
-
finalFinishReason = delta.finishReason;
|
|
1105
|
-
}
|
|
1106
|
-
onChunk({
|
|
1107
|
-
id: delta.id,
|
|
1108
|
-
model: delta.model,
|
|
1109
|
-
content: delta.content,
|
|
1110
|
-
finishReason: delta.finishReason,
|
|
1111
|
-
raw: delta.raw
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
if (event.eventType === "error") {
|
|
1116
|
-
onChunk({
|
|
1117
|
-
id: finalId || "error",
|
|
1118
|
-
model: finalModel,
|
|
1119
|
-
error: event.error?.message ?? "stream error"
|
|
1120
|
-
});
|
|
1121
|
-
}
|
|
1122
|
-
}, options);
|
|
1402
|
+
const result = await rust.streamEvents(
|
|
1403
|
+
model,
|
|
1404
|
+
promptOrMessages,
|
|
1405
|
+
(event) => streamBridge.onEvent(event),
|
|
1406
|
+
options
|
|
1407
|
+
);
|
|
1123
1408
|
|
|
1124
|
-
return
|
|
1125
|
-
...result,
|
|
1126
|
-
id: result.id || finalId,
|
|
1127
|
-
model: result.model || finalModel,
|
|
1128
|
-
content: result.content ?? aggregate,
|
|
1129
|
-
finishReason: result.finishReason ?? finalFinishReason,
|
|
1130
|
-
latencyMs: Math.max(0, Math.round(performance.now() - started))
|
|
1131
|
-
};
|
|
1409
|
+
return streamBridge.mergeResult(result, started);
|
|
1132
1410
|
}
|
|
1133
1411
|
|
|
1134
1412
|
return this.fallbackClient.stream(model, promptOrMessages, onChunk, options);
|
|
@@ -1145,17 +1423,25 @@ export class Client {
|
|
|
1145
1423
|
async runWorkflowYamlString(yamlText, workflowInput, workflowOptions) {
|
|
1146
1424
|
const rust = await this.ensureBackend();
|
|
1147
1425
|
if (rust) {
|
|
1148
|
-
|
|
1426
|
+
const result = await rust.runWorkflowYamlString(yamlText, workflowInput, workflowOptions);
|
|
1427
|
+
return assertWorkflowResultShape(normalizeWorkflowResult(result));
|
|
1149
1428
|
}
|
|
1150
|
-
|
|
1429
|
+
const result = await this.fallbackClient.runWorkflowYamlString(
|
|
1430
|
+
yamlText,
|
|
1431
|
+
workflowInput,
|
|
1432
|
+
workflowOptions
|
|
1433
|
+
);
|
|
1434
|
+
return assertWorkflowResultShape(normalizeWorkflowResult(result));
|
|
1151
1435
|
}
|
|
1152
1436
|
|
|
1153
1437
|
async runWorkflowYaml(workflowPath, workflowInput) {
|
|
1154
1438
|
const rust = await this.ensureBackend();
|
|
1155
1439
|
if (rust) {
|
|
1156
|
-
|
|
1440
|
+
const result = await rust.runWorkflowYaml(workflowPath, workflowInput);
|
|
1441
|
+
return assertWorkflowResultShape(normalizeWorkflowResult(result));
|
|
1157
1442
|
}
|
|
1158
|
-
|
|
1443
|
+
const result = await this.fallbackClient.runWorkflowYaml(workflowPath, workflowInput);
|
|
1444
|
+
return assertWorkflowResultShape(normalizeWorkflowResult(result));
|
|
1159
1445
|
}
|
|
1160
1446
|
}
|
|
1161
1447
|
|
package/package.json
CHANGED
|
@@ -27,9 +27,9 @@ export interface InitOutput {
|
|
|
27
27
|
readonly wasmclient_runWorkflowYaml: (a: number, b: number, c: number, d: any) => [number, number, number];
|
|
28
28
|
readonly wasmclient_runWorkflowYamlString: (a: number, b: number, c: number, d: any, e: number) => any;
|
|
29
29
|
readonly wasmclient_streamEvents: (a: number, b: number, c: number, d: any, e: any, f: number) => any;
|
|
30
|
-
readonly
|
|
31
|
-
readonly
|
|
32
|
-
readonly
|
|
30
|
+
readonly wasm_bindgen__closure__destroy__haab24713ba6d07cd: (a: number, b: number) => void;
|
|
31
|
+
readonly wasm_bindgen__convert__closures_____invoke__h0653cfab9850c2f3: (a: number, b: number, c: any) => [number, number];
|
|
32
|
+
readonly wasm_bindgen__convert__closures_____invoke__h133df7605c346924: (a: number, b: number, c: any, d: any) => void;
|
|
33
33
|
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
|
34
34
|
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
35
35
|
readonly __wbindgen_exn_store: (a: number) => void;
|
|
@@ -314,7 +314,7 @@ function __wbg_get_imports() {
|
|
|
314
314
|
const a = state0.a;
|
|
315
315
|
state0.a = 0;
|
|
316
316
|
try {
|
|
317
|
-
return
|
|
317
|
+
return wasm_bindgen__convert__closures_____invoke__h133df7605c346924(a, state0.b, arg0, arg1);
|
|
318
318
|
} finally {
|
|
319
319
|
state0.a = a;
|
|
320
320
|
}
|
|
@@ -395,7 +395,7 @@ function __wbg_get_imports() {
|
|
|
395
395
|
},
|
|
396
396
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
|
397
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`.
|
|
398
|
-
const ret = makeMutClosure(arg0, arg1, wasm.
|
|
398
|
+
const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__haab24713ba6d07cd, wasm_bindgen__convert__closures_____invoke__h0653cfab9850c2f3);
|
|
399
399
|
return ret;
|
|
400
400
|
},
|
|
401
401
|
__wbindgen_cast_0000000000000002: function(arg0) {
|
|
@@ -434,15 +434,15 @@ function __wbg_get_imports() {
|
|
|
434
434
|
};
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
function
|
|
438
|
-
const ret = wasm.
|
|
437
|
+
function wasm_bindgen__convert__closures_____invoke__h0653cfab9850c2f3(arg0, arg1, arg2) {
|
|
438
|
+
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h0653cfab9850c2f3(arg0, arg1, arg2);
|
|
439
439
|
if (ret[1]) {
|
|
440
440
|
throw takeFromExternrefTable0(ret[0]);
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
function
|
|
445
|
-
wasm.
|
|
444
|
+
function wasm_bindgen__convert__closures_____invoke__h133df7605c346924(arg0, arg1, arg2, arg3) {
|
|
445
|
+
wasm.wasm_bindgen__convert__closures_____invoke__h133df7605c346924(arg0, arg1, arg2, arg3);
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
const WasmClientFinalization = (typeof FinalizationRegistry === 'undefined')
|
|
Binary file
|
|
@@ -9,9 +9,9 @@ export const wasmclient_new: (a: number, b: number, c: any) => [number, number,
|
|
|
9
9
|
export const wasmclient_runWorkflowYaml: (a: number, b: number, c: number, d: any) => [number, number, number];
|
|
10
10
|
export const wasmclient_runWorkflowYamlString: (a: number, b: number, c: number, d: any, e: number) => any;
|
|
11
11
|
export const wasmclient_streamEvents: (a: number, b: number, c: number, d: any, e: any, f: number) => any;
|
|
12
|
-
export const
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
12
|
+
export const wasm_bindgen__closure__destroy__haab24713ba6d07cd: (a: number, b: number) => void;
|
|
13
|
+
export const wasm_bindgen__convert__closures_____invoke__h0653cfab9850c2f3: (a: number, b: number, c: any) => [number, number];
|
|
14
|
+
export const wasm_bindgen__convert__closures_____invoke__h133df7605c346924: (a: number, b: number, c: any, d: any) => void;
|
|
15
15
|
export const __wbindgen_malloc: (a: number, b: number) => number;
|
|
16
16
|
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
17
17
|
export const __wbindgen_exn_store: (a: number) => void;
|
package/rust/Cargo.toml
CHANGED
package/rust/src/lib.rs
CHANGED
|
@@ -140,6 +140,7 @@ struct GraphSwitchBranch {
|
|
|
140
140
|
#[derive(Deserialize, Clone)]
|
|
141
141
|
struct GraphCustomWorker {
|
|
142
142
|
handler: Option<String>,
|
|
143
|
+
handler_file: Option<String>,
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
#[derive(Deserialize, Clone)]
|
|
@@ -223,6 +224,40 @@ struct WorkflowRunResult {
|
|
|
223
224
|
context: JsonValue,
|
|
224
225
|
output: Option<JsonValue>,
|
|
225
226
|
events: Vec<WorkflowRunEvent>,
|
|
227
|
+
#[serde(rename = "workflow_id", skip_serializing_if = "Option::is_none")]
|
|
228
|
+
workflow_id: Option<String>,
|
|
229
|
+
#[serde(rename = "entry_node", skip_serializing_if = "Option::is_none")]
|
|
230
|
+
entry_node: Option<String>,
|
|
231
|
+
#[serde(rename = "email_text", skip_serializing_if = "Option::is_none")]
|
|
232
|
+
email_text: Option<String>,
|
|
233
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
234
|
+
trace: Option<Vec<String>>,
|
|
235
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
236
|
+
outputs: Option<JsonValue>,
|
|
237
|
+
#[serde(rename = "terminal_node", skip_serializing_if = "Option::is_none")]
|
|
238
|
+
terminal_node: Option<String>,
|
|
239
|
+
#[serde(rename = "terminal_output", skip_serializing_if = "Option::is_none")]
|
|
240
|
+
terminal_output: Option<JsonValue>,
|
|
241
|
+
#[serde(rename = "step_timings", skip_serializing_if = "Option::is_none")]
|
|
242
|
+
step_timings: Option<JsonValue>,
|
|
243
|
+
#[serde(rename = "total_elapsed_ms", skip_serializing_if = "Option::is_none")]
|
|
244
|
+
total_elapsed_ms: Option<u64>,
|
|
245
|
+
#[serde(rename = "ttft_ms", skip_serializing_if = "Option::is_none")]
|
|
246
|
+
ttft_ms: Option<u64>,
|
|
247
|
+
#[serde(rename = "total_input_tokens", skip_serializing_if = "Option::is_none")]
|
|
248
|
+
total_input_tokens: Option<u64>,
|
|
249
|
+
#[serde(rename = "total_output_tokens", skip_serializing_if = "Option::is_none")]
|
|
250
|
+
total_output_tokens: Option<u64>,
|
|
251
|
+
#[serde(rename = "total_tokens", skip_serializing_if = "Option::is_none")]
|
|
252
|
+
total_tokens: Option<u64>,
|
|
253
|
+
#[serde(rename = "total_reasoning_tokens", skip_serializing_if = "Option::is_none")]
|
|
254
|
+
total_reasoning_tokens: Option<u64>,
|
|
255
|
+
#[serde(rename = "tokens_per_second", skip_serializing_if = "Option::is_none")]
|
|
256
|
+
tokens_per_second: Option<f64>,
|
|
257
|
+
#[serde(rename = "trace_id", skip_serializing_if = "Option::is_none")]
|
|
258
|
+
trace_id: Option<String>,
|
|
259
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
260
|
+
metadata: Option<JsonValue>,
|
|
226
261
|
}
|
|
227
262
|
|
|
228
263
|
#[derive(Deserialize)]
|
|
@@ -538,6 +573,24 @@ fn interpolate_graph_prompt(input: &str, context: &JsonValue) -> String {
|
|
|
538
573
|
output
|
|
539
574
|
}
|
|
540
575
|
|
|
576
|
+
fn interpolate_graph_json(value: &JsonValue, context: &JsonValue) -> JsonValue {
|
|
577
|
+
match value {
|
|
578
|
+
JsonValue::String(s) => JsonValue::String(interpolate_graph_prompt(s, context)),
|
|
579
|
+
JsonValue::Array(items) => JsonValue::Array(
|
|
580
|
+
items
|
|
581
|
+
.iter()
|
|
582
|
+
.map(|item| interpolate_graph_json(item, context))
|
|
583
|
+
.collect(),
|
|
584
|
+
),
|
|
585
|
+
JsonValue::Object(map) => JsonValue::Object(
|
|
586
|
+
map.iter()
|
|
587
|
+
.map(|(key, value)| (key.clone(), interpolate_graph_json(value, context)))
|
|
588
|
+
.collect(),
|
|
589
|
+
),
|
|
590
|
+
_ => value.clone(),
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
541
594
|
fn parse_json_from_text(value: &str) -> JsonValue {
|
|
542
595
|
if let Ok(parsed) = serde_json::from_str::<JsonValue>(value) {
|
|
543
596
|
return parsed;
|
|
@@ -1298,7 +1351,7 @@ impl WasmClient {
|
|
|
1298
1351
|
}
|
|
1299
1352
|
|
|
1300
1353
|
if raw_doc.get("entry_node").is_some() && raw_doc.get("nodes").is_some() {
|
|
1301
|
-
let graph_doc: GraphWorkflowDoc = serde_json::from_value(raw_doc)
|
|
1354
|
+
let graph_doc: GraphWorkflowDoc = serde_json::from_value(raw_doc.clone())
|
|
1302
1355
|
.map_err(|error| config_error(format!("invalid graph workflow YAML: {error}")))?;
|
|
1303
1356
|
|
|
1304
1357
|
let mut node_by_id: HashMap<String, GraphWorkflowNode> = HashMap::new();
|
|
@@ -1324,10 +1377,19 @@ impl WasmClient {
|
|
|
1324
1377
|
"nodes": JsonValue::Object(JsonMap::new())
|
|
1325
1378
|
});
|
|
1326
1379
|
|
|
1380
|
+
let workflow_started = now_millis();
|
|
1327
1381
|
let mut events = Vec::new();
|
|
1328
1382
|
let mut output: Option<JsonValue> = None;
|
|
1329
1383
|
let mut pointer = graph_doc.entry_node.clone();
|
|
1330
1384
|
let mut iterations = 0usize;
|
|
1385
|
+
let mut workflow_ttft_ms: Option<u64> = None;
|
|
1386
|
+
let mut trace: Vec<String> = Vec::new();
|
|
1387
|
+
let mut step_timings: Vec<JsonValue> = Vec::new();
|
|
1388
|
+
let mut total_input_tokens: u64 = 0;
|
|
1389
|
+
let mut total_output_tokens: u64 = 0;
|
|
1390
|
+
let mut total_tokens: u64 = 0;
|
|
1391
|
+
let total_reasoning_tokens: u64 = 0;
|
|
1392
|
+
let mut llm_nodes_without_usage: Vec<String> = Vec::new();
|
|
1331
1393
|
|
|
1332
1394
|
while !pointer.is_empty() {
|
|
1333
1395
|
iterations += 1;
|
|
@@ -1355,6 +1417,12 @@ impl WasmClient {
|
|
|
1355
1417
|
status: "started".to_string(),
|
|
1356
1418
|
});
|
|
1357
1419
|
|
|
1420
|
+
let node_started = now_millis();
|
|
1421
|
+
let mut model_name: Option<String> = None;
|
|
1422
|
+
let mut prompt_tokens: Option<u64> = None;
|
|
1423
|
+
let mut completion_tokens: Option<u64> = None;
|
|
1424
|
+
let mut total_node_tokens: Option<u64> = None;
|
|
1425
|
+
|
|
1358
1426
|
if let Some(llm) = node.node_type.llm_call.as_ref() {
|
|
1359
1427
|
let model = llm
|
|
1360
1428
|
.model
|
|
@@ -1418,6 +1486,32 @@ impl WasmClient {
|
|
|
1418
1486
|
let completion: JsonValue = serde_wasm_bindgen::from_value(completion_js)
|
|
1419
1487
|
.map_err(|_| js_error("failed to parse completion result"))?;
|
|
1420
1488
|
|
|
1489
|
+
if workflow_ttft_ms.is_none() {
|
|
1490
|
+
let measured = (now_millis() - workflow_started).max(0.0) as u64;
|
|
1491
|
+
workflow_ttft_ms = Some(if measured == 0 { 1 } else { measured });
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
model_name = completion
|
|
1495
|
+
.get("model")
|
|
1496
|
+
.and_then(JsonValue::as_str)
|
|
1497
|
+
.map(ToString::to_string);
|
|
1498
|
+
let usage = completion.get("usage");
|
|
1499
|
+
let usage_available = completion
|
|
1500
|
+
.get("usage_available")
|
|
1501
|
+
.and_then(JsonValue::as_bool)
|
|
1502
|
+
.unwrap_or(false);
|
|
1503
|
+
if usage_available {
|
|
1504
|
+
prompt_tokens = usage
|
|
1505
|
+
.and_then(|value| value.get("prompt_tokens"))
|
|
1506
|
+
.and_then(JsonValue::as_u64);
|
|
1507
|
+
completion_tokens = usage
|
|
1508
|
+
.and_then(|value| value.get("completion_tokens"))
|
|
1509
|
+
.and_then(JsonValue::as_u64);
|
|
1510
|
+
total_node_tokens = usage
|
|
1511
|
+
.and_then(|value| value.get("total_tokens"))
|
|
1512
|
+
.and_then(JsonValue::as_u64);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1421
1515
|
let raw_content = completion
|
|
1422
1516
|
.get("content")
|
|
1423
1517
|
.and_then(JsonValue::as_str)
|
|
@@ -1475,32 +1569,40 @@ impl WasmClient {
|
|
|
1475
1569
|
.handler
|
|
1476
1570
|
.clone()
|
|
1477
1571
|
.unwrap_or_else(|| "custom_worker".to_string());
|
|
1572
|
+
let lookup_key = custom_worker
|
|
1573
|
+
.handler_file
|
|
1574
|
+
.as_deref()
|
|
1575
|
+
.map(|file| format!("{}#{}", file, handler.as_str()))
|
|
1576
|
+
.unwrap_or_else(|| handler.clone());
|
|
1478
1577
|
let functions_js = options.functions_js.clone().ok_or_else(|| {
|
|
1479
1578
|
config_error(format!(
|
|
1480
1579
|
"custom_worker node '{}' requires workflowOptions.functions",
|
|
1481
1580
|
node.id
|
|
1482
1581
|
))
|
|
1483
1582
|
})?;
|
|
1484
|
-
let function_value = Reflect::get(&functions_js, &JsValue::from_str(&
|
|
1583
|
+
let function_value = Reflect::get(&functions_js, &JsValue::from_str(&lookup_key))
|
|
1485
1584
|
.map_err(|_| {
|
|
1486
1585
|
config_error(format!(
|
|
1487
|
-
"failed to resolve custom worker handler '{}' from workflowOptions.functions",
|
|
1488
|
-
|
|
1586
|
+
"failed to resolve custom worker handler key '{}' from workflowOptions.functions",
|
|
1587
|
+
lookup_key
|
|
1489
1588
|
))
|
|
1490
1589
|
})?;
|
|
1491
1590
|
let function = function_value.dyn_into::<Function>().map_err(|_| {
|
|
1492
1591
|
config_error(format!(
|
|
1493
1592
|
"custom_worker node '{}' requires workflowOptions.functions['{}']",
|
|
1494
|
-
node.id,
|
|
1593
|
+
node.id, lookup_key
|
|
1495
1594
|
))
|
|
1496
1595
|
})?;
|
|
1497
1596
|
|
|
1498
1597
|
let worker_args = json!({
|
|
1499
1598
|
"handler": handler,
|
|
1599
|
+
"handler_file": custom_worker.handler_file,
|
|
1600
|
+
"handler_lookup_key": lookup_key,
|
|
1500
1601
|
"payload": node
|
|
1501
1602
|
.config
|
|
1502
1603
|
.as_ref()
|
|
1503
|
-
.and_then(|config| config.payload.
|
|
1604
|
+
.and_then(|config| config.payload.as_ref())
|
|
1605
|
+
.map(|payload| interpolate_graph_json(payload, &graph_context))
|
|
1504
1606
|
.unwrap_or(JsonValue::Null),
|
|
1505
1607
|
"nodeId": node.id.clone()
|
|
1506
1608
|
});
|
|
@@ -1551,17 +1653,112 @@ impl WasmClient {
|
|
|
1551
1653
|
}
|
|
1552
1654
|
|
|
1553
1655
|
events.push(WorkflowRunEvent {
|
|
1554
|
-
step_id: node.id,
|
|
1656
|
+
step_id: node.id.clone(),
|
|
1555
1657
|
step_type: step_type.to_string(),
|
|
1556
1658
|
status: "completed".to_string(),
|
|
1557
1659
|
});
|
|
1660
|
+
let elapsed_ms = (now_millis() - node_started).max(0.0) as u64;
|
|
1661
|
+
if step_type == "llm_call" {
|
|
1662
|
+
if prompt_tokens.is_none() || completion_tokens.is_none() || total_node_tokens.is_none() {
|
|
1663
|
+
llm_nodes_without_usage.push(node.id.clone());
|
|
1664
|
+
}
|
|
1665
|
+
total_input_tokens += prompt_tokens.unwrap_or(0);
|
|
1666
|
+
total_output_tokens += completion_tokens.unwrap_or(0);
|
|
1667
|
+
total_tokens += total_node_tokens.unwrap_or(0);
|
|
1668
|
+
}
|
|
1669
|
+
let step_tokens_per_second = completion_tokens.map(|tokens| {
|
|
1670
|
+
if elapsed_ms == 0 {
|
|
1671
|
+
tokens as f64
|
|
1672
|
+
} else {
|
|
1673
|
+
((tokens as f64 / (elapsed_ms as f64 / 1000.0)) * 100.0).round() / 100.0
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
step_timings.push(json!({
|
|
1677
|
+
"node_id": node.id.clone(),
|
|
1678
|
+
"node_kind": step_type,
|
|
1679
|
+
"model_name": model_name,
|
|
1680
|
+
"elapsed_ms": elapsed_ms,
|
|
1681
|
+
"prompt_tokens": prompt_tokens,
|
|
1682
|
+
"completion_tokens": completion_tokens,
|
|
1683
|
+
"total_tokens": total_node_tokens,
|
|
1684
|
+
"reasoning_tokens": 0,
|
|
1685
|
+
"tokens_per_second": step_tokens_per_second,
|
|
1686
|
+
}));
|
|
1687
|
+
trace.push(node.id.clone());
|
|
1558
1688
|
}
|
|
1559
1689
|
|
|
1690
|
+
let total_elapsed_ms = (now_millis() - workflow_started).max(0.0) as u64;
|
|
1691
|
+
let terminal_node = trace.last().cloned().unwrap_or_default();
|
|
1692
|
+
let workflow_id = raw_doc
|
|
1693
|
+
.get("id")
|
|
1694
|
+
.and_then(JsonValue::as_str)
|
|
1695
|
+
.unwrap_or("wasm_workflow")
|
|
1696
|
+
.to_string();
|
|
1697
|
+
let email_text = graph_context
|
|
1698
|
+
.get("input")
|
|
1699
|
+
.and_then(|input| input.get("email_text"))
|
|
1700
|
+
.and_then(JsonValue::as_str)
|
|
1701
|
+
.unwrap_or_default()
|
|
1702
|
+
.to_string();
|
|
1703
|
+
let outputs = graph_context
|
|
1704
|
+
.get("nodes")
|
|
1705
|
+
.cloned()
|
|
1706
|
+
.unwrap_or(JsonValue::Object(JsonMap::new()));
|
|
1707
|
+
let token_metrics_available = llm_nodes_without_usage.is_empty();
|
|
1708
|
+
let overall_tokens_per_second = if total_elapsed_ms == 0 {
|
|
1709
|
+
Some(total_output_tokens as f64)
|
|
1710
|
+
} else {
|
|
1711
|
+
Some(
|
|
1712
|
+
((total_output_tokens as f64 / (total_elapsed_ms as f64 / 1000.0)) * 100.0)
|
|
1713
|
+
.round()
|
|
1714
|
+
/ 100.0,
|
|
1715
|
+
)
|
|
1716
|
+
};
|
|
1717
|
+
let nerdstats = json!({
|
|
1718
|
+
"workflow_id": workflow_id,
|
|
1719
|
+
"terminal_node": terminal_node,
|
|
1720
|
+
"total_elapsed_ms": total_elapsed_ms,
|
|
1721
|
+
"ttft_ms": workflow_ttft_ms.unwrap_or(0),
|
|
1722
|
+
"step_details": step_timings,
|
|
1723
|
+
"total_input_tokens": total_input_tokens,
|
|
1724
|
+
"total_output_tokens": total_output_tokens,
|
|
1725
|
+
"total_tokens": total_tokens,
|
|
1726
|
+
"total_reasoning_tokens": total_reasoning_tokens,
|
|
1727
|
+
"tokens_per_second": overall_tokens_per_second,
|
|
1728
|
+
"trace_id": JsonValue::Null,
|
|
1729
|
+
"token_metrics_available": token_metrics_available,
|
|
1730
|
+
"token_metrics_source": if token_metrics_available { "provider_usage" } else { "unavailable" },
|
|
1731
|
+
"llm_nodes_without_usage": llm_nodes_without_usage,
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1560
1734
|
let result = WorkflowRunResult {
|
|
1561
1735
|
status: "ok".to_string(),
|
|
1562
1736
|
context: graph_context,
|
|
1563
|
-
output,
|
|
1737
|
+
output: output.clone(),
|
|
1564
1738
|
events,
|
|
1739
|
+
workflow_id: Some(
|
|
1740
|
+
raw_doc
|
|
1741
|
+
.get("id")
|
|
1742
|
+
.and_then(JsonValue::as_str)
|
|
1743
|
+
.unwrap_or("wasm_workflow")
|
|
1744
|
+
.to_string(),
|
|
1745
|
+
),
|
|
1746
|
+
entry_node: Some(graph_doc.entry_node),
|
|
1747
|
+
email_text: Some(email_text),
|
|
1748
|
+
trace: Some(trace),
|
|
1749
|
+
outputs: Some(outputs),
|
|
1750
|
+
terminal_node: Some(terminal_node),
|
|
1751
|
+
terminal_output: output,
|
|
1752
|
+
step_timings: Some(nerdstats.get("step_details").cloned().unwrap_or(JsonValue::Array(vec![]))),
|
|
1753
|
+
total_elapsed_ms: Some(total_elapsed_ms),
|
|
1754
|
+
ttft_ms: Some(workflow_ttft_ms.unwrap_or(0)),
|
|
1755
|
+
total_input_tokens: Some(total_input_tokens),
|
|
1756
|
+
total_output_tokens: Some(total_output_tokens),
|
|
1757
|
+
total_tokens: Some(total_tokens),
|
|
1758
|
+
total_reasoning_tokens: Some(total_reasoning_tokens),
|
|
1759
|
+
tokens_per_second: overall_tokens_per_second,
|
|
1760
|
+
trace_id: None,
|
|
1761
|
+
metadata: Some(json!({"nerdstats": nerdstats})),
|
|
1565
1762
|
};
|
|
1566
1763
|
return serde_wasm_bindgen::to_value(&result)
|
|
1567
1764
|
.map_err(|_| js_error("failed to serialize workflow result"));
|
|
@@ -1771,6 +1968,23 @@ impl WasmClient {
|
|
|
1771
1968
|
context: JsonValue::Object(context),
|
|
1772
1969
|
output,
|
|
1773
1970
|
events,
|
|
1971
|
+
workflow_id: None,
|
|
1972
|
+
entry_node: None,
|
|
1973
|
+
email_text: None,
|
|
1974
|
+
trace: None,
|
|
1975
|
+
outputs: None,
|
|
1976
|
+
terminal_node: None,
|
|
1977
|
+
terminal_output: None,
|
|
1978
|
+
step_timings: None,
|
|
1979
|
+
total_elapsed_ms: None,
|
|
1980
|
+
ttft_ms: None,
|
|
1981
|
+
total_input_tokens: None,
|
|
1982
|
+
total_output_tokens: None,
|
|
1983
|
+
total_tokens: None,
|
|
1984
|
+
total_reasoning_tokens: None,
|
|
1985
|
+
tokens_per_second: None,
|
|
1986
|
+
trace_id: None,
|
|
1987
|
+
metadata: None,
|
|
1774
1988
|
};
|
|
1775
1989
|
serde_wasm_bindgen::to_value(&result)
|
|
1776
1990
|
.map_err(|_| js_error("failed to serialize workflow result"))
|