simple-agents-wasm 0.2.31 → 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 +376 -119
- 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 +202 -7
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) => {
|
|
@@ -613,63 +778,17 @@ class BrowserJsClient {
|
|
|
613
778
|
throw configError("onChunk callback is required");
|
|
614
779
|
}
|
|
615
780
|
|
|
616
|
-
let aggregate = "";
|
|
617
|
-
let finalId = "";
|
|
618
|
-
let finalModel = model;
|
|
619
|
-
let finalFinishReason;
|
|
620
781
|
const started = performance.now();
|
|
782
|
+
const streamBridge = createStreamEventBridge(model, onChunk);
|
|
621
783
|
|
|
622
784
|
const result = await this.streamEvents(
|
|
623
785
|
model,
|
|
624
786
|
promptOrMessages,
|
|
625
|
-
(event) =>
|
|
626
|
-
if (event.eventType === "delta") {
|
|
627
|
-
const delta = event.delta;
|
|
628
|
-
if (!delta) {
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if (delta.id && finalId.length === 0) {
|
|
633
|
-
finalId = delta.id;
|
|
634
|
-
}
|
|
635
|
-
if (delta.model) {
|
|
636
|
-
finalModel = delta.model;
|
|
637
|
-
}
|
|
638
|
-
if (delta.content) {
|
|
639
|
-
aggregate += delta.content;
|
|
640
|
-
}
|
|
641
|
-
if (delta.finishReason) {
|
|
642
|
-
finalFinishReason = delta.finishReason;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
onChunk({
|
|
646
|
-
id: delta.id,
|
|
647
|
-
model: delta.model,
|
|
648
|
-
content: delta.content,
|
|
649
|
-
finishReason: delta.finishReason,
|
|
650
|
-
raw: delta.raw
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
if (event.eventType === "error") {
|
|
655
|
-
onChunk({
|
|
656
|
-
id: finalId || "error",
|
|
657
|
-
model: finalModel,
|
|
658
|
-
error: event.error?.message ?? "stream error"
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
},
|
|
787
|
+
(event) => streamBridge.onEvent(event),
|
|
662
788
|
options
|
|
663
789
|
);
|
|
664
790
|
|
|
665
|
-
return
|
|
666
|
-
...result,
|
|
667
|
-
id: result.id || finalId,
|
|
668
|
-
model: result.model || finalModel,
|
|
669
|
-
content: result.content ?? aggregate,
|
|
670
|
-
finishReason: result.finishReason ?? finalFinishReason,
|
|
671
|
-
latencyMs: Math.max(0, Math.round(performance.now() - started))
|
|
672
|
-
};
|
|
791
|
+
return streamBridge.mergeResult(result, started);
|
|
673
792
|
}
|
|
674
793
|
|
|
675
794
|
async streamEvents(model, promptOrMessages, onEvent, options = {}) {
|
|
@@ -694,7 +813,10 @@ class BrowserJsClient {
|
|
|
694
813
|
max_tokens: options.maxTokens,
|
|
695
814
|
temperature: options.temperature,
|
|
696
815
|
top_p: options.topP,
|
|
697
|
-
stream: true
|
|
816
|
+
stream: true,
|
|
817
|
+
stream_options: {
|
|
818
|
+
include_usage: true
|
|
819
|
+
}
|
|
698
820
|
})
|
|
699
821
|
});
|
|
700
822
|
|
|
@@ -711,6 +833,12 @@ class BrowserJsClient {
|
|
|
711
833
|
let responseModel = model;
|
|
712
834
|
let aggregate = "";
|
|
713
835
|
let finishReason;
|
|
836
|
+
let usage = {
|
|
837
|
+
promptTokens: 0,
|
|
838
|
+
completionTokens: 0,
|
|
839
|
+
totalTokens: 0
|
|
840
|
+
};
|
|
841
|
+
let usageAvailable = false;
|
|
714
842
|
|
|
715
843
|
try {
|
|
716
844
|
for await (const block of iterateSse(response)) {
|
|
@@ -751,6 +879,10 @@ class BrowserJsClient {
|
|
|
751
879
|
if (delta.finishReason) {
|
|
752
880
|
finishReason = delta.finishReason;
|
|
753
881
|
}
|
|
882
|
+
if (chunk?.usage && typeof chunk.usage === "object") {
|
|
883
|
+
usage = toUsage(chunk.usage);
|
|
884
|
+
usageAvailable = true;
|
|
885
|
+
}
|
|
754
886
|
|
|
755
887
|
onEvent({ eventType: "delta", delta });
|
|
756
888
|
}
|
|
@@ -771,11 +903,9 @@ class BrowserJsClient {
|
|
|
771
903
|
content: aggregate,
|
|
772
904
|
finishReason,
|
|
773
905
|
usage: {
|
|
774
|
-
|
|
775
|
-
completionTokens: 0,
|
|
776
|
-
totalTokens: 0
|
|
906
|
+
...usage
|
|
777
907
|
},
|
|
778
|
-
usageAvailable
|
|
908
|
+
usageAvailable,
|
|
779
909
|
latencyMs,
|
|
780
910
|
raw: undefined,
|
|
781
911
|
healed: undefined,
|
|
@@ -826,9 +956,17 @@ class BrowserJsClient {
|
|
|
826
956
|
nodes: {}
|
|
827
957
|
};
|
|
828
958
|
|
|
959
|
+
const workflowStarted = performance.now();
|
|
960
|
+
let workflowTtftMs = 0;
|
|
829
961
|
let pointer = doc.entry_node;
|
|
830
962
|
let output;
|
|
831
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 = [];
|
|
832
970
|
|
|
833
971
|
while (typeof pointer === "string" && pointer.length > 0) {
|
|
834
972
|
iterations += 1;
|
|
@@ -843,6 +981,11 @@ class BrowserJsClient {
|
|
|
843
981
|
|
|
844
982
|
const nodeType = node.node_type ?? {};
|
|
845
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;
|
|
846
989
|
events.push({ stepId: node.id, stepType: nodeTypeName, status: "started" });
|
|
847
990
|
|
|
848
991
|
if (nodeType.llm_call) {
|
|
@@ -874,10 +1017,57 @@ class BrowserJsClient {
|
|
|
874
1017
|
promptOrMessages = history;
|
|
875
1018
|
}
|
|
876
1019
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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);
|
|
881
1071
|
const validationError = schemaValidationError(llmOutputSchema(node), parsedOutput);
|
|
882
1072
|
if (validationError !== null) {
|
|
883
1073
|
throw runtimeError(
|
|
@@ -886,7 +1076,7 @@ class BrowserJsClient {
|
|
|
886
1076
|
}
|
|
887
1077
|
graphContext.nodes[node.id] = {
|
|
888
1078
|
output: parsedOutput,
|
|
889
|
-
raw:
|
|
1079
|
+
raw: rawContent
|
|
890
1080
|
};
|
|
891
1081
|
output = parsedOutput;
|
|
892
1082
|
|
|
@@ -901,15 +1091,22 @@ class BrowserJsClient {
|
|
|
901
1091
|
pointer = matched?.target ?? switchSpec.default ?? "";
|
|
902
1092
|
} else if (nodeType.custom_worker) {
|
|
903
1093
|
const handler = nodeType.custom_worker.handler ?? "custom_worker";
|
|
904
|
-
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];
|
|
905
1100
|
if (typeof fn !== "function") {
|
|
906
1101
|
throw runtimeError(
|
|
907
|
-
`custom_worker node '${node.id}' requires workflowOptions.functions['${
|
|
1102
|
+
`custom_worker node '${node.id}' requires workflowOptions.functions['${lookupKey}']`
|
|
908
1103
|
);
|
|
909
1104
|
}
|
|
910
1105
|
const workerOutput = await fn(
|
|
911
1106
|
{
|
|
912
1107
|
handler,
|
|
1108
|
+
handler_file: handlerFile,
|
|
1109
|
+
handler_lookup_key: lookupKey,
|
|
913
1110
|
payload: interpolatePathValue(node.config?.payload ?? null, graphContext),
|
|
914
1111
|
nodeId: node.id
|
|
915
1112
|
},
|
|
@@ -926,13 +1123,92 @@ class BrowserJsClient {
|
|
|
926
1123
|
}
|
|
927
1124
|
|
|
928
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
|
+
}
|
|
929
1187
|
}
|
|
930
1188
|
|
|
931
1189
|
return {
|
|
932
|
-
|
|
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,
|
|
933
1210
|
context: graphContext,
|
|
934
|
-
|
|
935
|
-
events
|
|
1211
|
+
status: "ok"
|
|
936
1212
|
};
|
|
937
1213
|
}
|
|
938
1214
|
|
|
@@ -1021,10 +1297,22 @@ class BrowserJsClient {
|
|
|
1021
1297
|
}
|
|
1022
1298
|
|
|
1023
1299
|
return {
|
|
1024
|
-
|
|
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,
|
|
1025
1314
|
context,
|
|
1026
|
-
|
|
1027
|
-
events
|
|
1315
|
+
status: "ok"
|
|
1028
1316
|
};
|
|
1029
1317
|
}
|
|
1030
1318
|
|
|
@@ -1043,7 +1331,7 @@ async function loadRustModule() {
|
|
|
1043
1331
|
try {
|
|
1044
1332
|
const moduleValue = await import("./pkg/simple_agents_wasm.js");
|
|
1045
1333
|
const wasmUrl = new URL("./pkg/simple_agents_wasm_bg.wasm", import.meta.url);
|
|
1046
|
-
await moduleValue.default(wasmUrl);
|
|
1334
|
+
await moduleValue.default({ module_or_path: wasmUrl });
|
|
1047
1335
|
return moduleValue;
|
|
1048
1336
|
} catch {
|
|
1049
1337
|
return null;
|
|
@@ -1108,56 +1396,17 @@ export class Client {
|
|
|
1108
1396
|
async stream(model, promptOrMessages, onChunk, options = {}) {
|
|
1109
1397
|
const rust = await this.ensureBackend();
|
|
1110
1398
|
if (rust) {
|
|
1111
|
-
let aggregate = "";
|
|
1112
|
-
let finalId = "";
|
|
1113
|
-
let finalModel = model;
|
|
1114
|
-
let finalFinishReason;
|
|
1115
1399
|
const started = performance.now();
|
|
1400
|
+
const streamBridge = createStreamEventBridge(model, onChunk);
|
|
1116
1401
|
|
|
1117
|
-
const result = await rust.streamEvents(
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (!finalId && delta.id) {
|
|
1124
|
-
finalId = delta.id;
|
|
1125
|
-
}
|
|
1126
|
-
if (delta.model) {
|
|
1127
|
-
finalModel = delta.model;
|
|
1128
|
-
}
|
|
1129
|
-
if (delta.content) {
|
|
1130
|
-
aggregate += delta.content;
|
|
1131
|
-
}
|
|
1132
|
-
if (delta.finishReason) {
|
|
1133
|
-
finalFinishReason = delta.finishReason;
|
|
1134
|
-
}
|
|
1135
|
-
onChunk({
|
|
1136
|
-
id: delta.id,
|
|
1137
|
-
model: delta.model,
|
|
1138
|
-
content: delta.content,
|
|
1139
|
-
finishReason: delta.finishReason,
|
|
1140
|
-
raw: delta.raw
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
if (event.eventType === "error") {
|
|
1145
|
-
onChunk({
|
|
1146
|
-
id: finalId || "error",
|
|
1147
|
-
model: finalModel,
|
|
1148
|
-
error: event.error?.message ?? "stream error"
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
}, options);
|
|
1402
|
+
const result = await rust.streamEvents(
|
|
1403
|
+
model,
|
|
1404
|
+
promptOrMessages,
|
|
1405
|
+
(event) => streamBridge.onEvent(event),
|
|
1406
|
+
options
|
|
1407
|
+
);
|
|
1152
1408
|
|
|
1153
|
-
return
|
|
1154
|
-
...result,
|
|
1155
|
-
id: result.id || finalId,
|
|
1156
|
-
model: result.model || finalModel,
|
|
1157
|
-
content: result.content ?? aggregate,
|
|
1158
|
-
finishReason: result.finishReason ?? finalFinishReason,
|
|
1159
|
-
latencyMs: Math.max(0, Math.round(performance.now() - started))
|
|
1160
|
-
};
|
|
1409
|
+
return streamBridge.mergeResult(result, started);
|
|
1161
1410
|
}
|
|
1162
1411
|
|
|
1163
1412
|
return this.fallbackClient.stream(model, promptOrMessages, onChunk, options);
|
|
@@ -1174,17 +1423,25 @@ export class Client {
|
|
|
1174
1423
|
async runWorkflowYamlString(yamlText, workflowInput, workflowOptions) {
|
|
1175
1424
|
const rust = await this.ensureBackend();
|
|
1176
1425
|
if (rust) {
|
|
1177
|
-
|
|
1426
|
+
const result = await rust.runWorkflowYamlString(yamlText, workflowInput, workflowOptions);
|
|
1427
|
+
return assertWorkflowResultShape(normalizeWorkflowResult(result));
|
|
1178
1428
|
}
|
|
1179
|
-
|
|
1429
|
+
const result = await this.fallbackClient.runWorkflowYamlString(
|
|
1430
|
+
yamlText,
|
|
1431
|
+
workflowInput,
|
|
1432
|
+
workflowOptions
|
|
1433
|
+
);
|
|
1434
|
+
return assertWorkflowResultShape(normalizeWorkflowResult(result));
|
|
1180
1435
|
}
|
|
1181
1436
|
|
|
1182
1437
|
async runWorkflowYaml(workflowPath, workflowInput) {
|
|
1183
1438
|
const rust = await this.ensureBackend();
|
|
1184
1439
|
if (rust) {
|
|
1185
|
-
|
|
1440
|
+
const result = await rust.runWorkflowYaml(workflowPath, workflowInput);
|
|
1441
|
+
return assertWorkflowResultShape(normalizeWorkflowResult(result));
|
|
1186
1442
|
}
|
|
1187
|
-
|
|
1443
|
+
const result = await this.fallbackClient.runWorkflowYaml(workflowPath, workflowInput);
|
|
1444
|
+
return assertWorkflowResultShape(normalizeWorkflowResult(result));
|
|
1188
1445
|
}
|
|
1189
1446
|
}
|
|
1190
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)]
|
|
@@ -1316,7 +1351,7 @@ impl WasmClient {
|
|
|
1316
1351
|
}
|
|
1317
1352
|
|
|
1318
1353
|
if raw_doc.get("entry_node").is_some() && raw_doc.get("nodes").is_some() {
|
|
1319
|
-
let graph_doc: GraphWorkflowDoc = serde_json::from_value(raw_doc)
|
|
1354
|
+
let graph_doc: GraphWorkflowDoc = serde_json::from_value(raw_doc.clone())
|
|
1320
1355
|
.map_err(|error| config_error(format!("invalid graph workflow YAML: {error}")))?;
|
|
1321
1356
|
|
|
1322
1357
|
let mut node_by_id: HashMap<String, GraphWorkflowNode> = HashMap::new();
|
|
@@ -1342,10 +1377,19 @@ impl WasmClient {
|
|
|
1342
1377
|
"nodes": JsonValue::Object(JsonMap::new())
|
|
1343
1378
|
});
|
|
1344
1379
|
|
|
1380
|
+
let workflow_started = now_millis();
|
|
1345
1381
|
let mut events = Vec::new();
|
|
1346
1382
|
let mut output: Option<JsonValue> = None;
|
|
1347
1383
|
let mut pointer = graph_doc.entry_node.clone();
|
|
1348
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();
|
|
1349
1393
|
|
|
1350
1394
|
while !pointer.is_empty() {
|
|
1351
1395
|
iterations += 1;
|
|
@@ -1373,6 +1417,12 @@ impl WasmClient {
|
|
|
1373
1417
|
status: "started".to_string(),
|
|
1374
1418
|
});
|
|
1375
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
|
+
|
|
1376
1426
|
if let Some(llm) = node.node_type.llm_call.as_ref() {
|
|
1377
1427
|
let model = llm
|
|
1378
1428
|
.model
|
|
@@ -1436,6 +1486,32 @@ impl WasmClient {
|
|
|
1436
1486
|
let completion: JsonValue = serde_wasm_bindgen::from_value(completion_js)
|
|
1437
1487
|
.map_err(|_| js_error("failed to parse completion result"))?;
|
|
1438
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
|
+
|
|
1439
1515
|
let raw_content = completion
|
|
1440
1516
|
.get("content")
|
|
1441
1517
|
.and_then(JsonValue::as_str)
|
|
@@ -1493,28 +1569,35 @@ impl WasmClient {
|
|
|
1493
1569
|
.handler
|
|
1494
1570
|
.clone()
|
|
1495
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());
|
|
1496
1577
|
let functions_js = options.functions_js.clone().ok_or_else(|| {
|
|
1497
1578
|
config_error(format!(
|
|
1498
1579
|
"custom_worker node '{}' requires workflowOptions.functions",
|
|
1499
1580
|
node.id
|
|
1500
1581
|
))
|
|
1501
1582
|
})?;
|
|
1502
|
-
let function_value = Reflect::get(&functions_js, &JsValue::from_str(&
|
|
1583
|
+
let function_value = Reflect::get(&functions_js, &JsValue::from_str(&lookup_key))
|
|
1503
1584
|
.map_err(|_| {
|
|
1504
1585
|
config_error(format!(
|
|
1505
|
-
"failed to resolve custom worker handler '{}' from workflowOptions.functions",
|
|
1506
|
-
|
|
1586
|
+
"failed to resolve custom worker handler key '{}' from workflowOptions.functions",
|
|
1587
|
+
lookup_key
|
|
1507
1588
|
))
|
|
1508
1589
|
})?;
|
|
1509
1590
|
let function = function_value.dyn_into::<Function>().map_err(|_| {
|
|
1510
1591
|
config_error(format!(
|
|
1511
1592
|
"custom_worker node '{}' requires workflowOptions.functions['{}']",
|
|
1512
|
-
node.id,
|
|
1593
|
+
node.id, lookup_key
|
|
1513
1594
|
))
|
|
1514
1595
|
})?;
|
|
1515
1596
|
|
|
1516
1597
|
let worker_args = json!({
|
|
1517
1598
|
"handler": handler,
|
|
1599
|
+
"handler_file": custom_worker.handler_file,
|
|
1600
|
+
"handler_lookup_key": lookup_key,
|
|
1518
1601
|
"payload": node
|
|
1519
1602
|
.config
|
|
1520
1603
|
.as_ref()
|
|
@@ -1570,17 +1653,112 @@ impl WasmClient {
|
|
|
1570
1653
|
}
|
|
1571
1654
|
|
|
1572
1655
|
events.push(WorkflowRunEvent {
|
|
1573
|
-
step_id: node.id,
|
|
1656
|
+
step_id: node.id.clone(),
|
|
1574
1657
|
step_type: step_type.to_string(),
|
|
1575
1658
|
status: "completed".to_string(),
|
|
1576
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());
|
|
1577
1688
|
}
|
|
1578
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
|
+
|
|
1579
1734
|
let result = WorkflowRunResult {
|
|
1580
1735
|
status: "ok".to_string(),
|
|
1581
1736
|
context: graph_context,
|
|
1582
|
-
output,
|
|
1737
|
+
output: output.clone(),
|
|
1583
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})),
|
|
1584
1762
|
};
|
|
1585
1763
|
return serde_wasm_bindgen::to_value(&result)
|
|
1586
1764
|
.map_err(|_| js_error("failed to serialize workflow result"));
|
|
@@ -1790,6 +1968,23 @@ impl WasmClient {
|
|
|
1790
1968
|
context: JsonValue::Object(context),
|
|
1791
1969
|
output,
|
|
1792
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,
|
|
1793
1988
|
};
|
|
1794
1989
|
serde_wasm_bindgen::to_value(&result)
|
|
1795
1990
|
.map_err(|_| js_error("failed to serialize workflow result"))
|