simple-agents-wasm 0.2.32 → 0.2.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +24 -195
- package/package.json +2 -1
- package/pkg/simple_agents_wasm_bg.wasm +0 -0
- package/runtime/errors.js +7 -0
- package/runtime/rust-runtime.js +18 -0
- package/runtime/stream.js +136 -0
- package/rust/Cargo.toml +1 -1
package/index.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { parse as parseYaml } from "yaml";
|
|
2
|
+
import { configError, runtimeError } from "./runtime/errors.js";
|
|
3
|
+
import {
|
|
4
|
+
applyDeltaToAggregate,
|
|
5
|
+
createStreamAggregator,
|
|
6
|
+
createStreamEventBridge,
|
|
7
|
+
iterateSse,
|
|
8
|
+
parseSseEventBlock
|
|
9
|
+
} from "./runtime/stream.js";
|
|
10
|
+
import { loadRustModule } from "./runtime/rust-runtime.js";
|
|
2
11
|
|
|
3
12
|
const DEFAULT_BASE_URLS = {
|
|
4
13
|
openai: "https://api.openai.com/v1",
|
|
5
14
|
openrouter: "https://openrouter.ai/api/v1"
|
|
6
15
|
};
|
|
7
16
|
|
|
8
|
-
function configError(message) {
|
|
9
|
-
return new Error(`simple-agents-wasm config error: ${message}`);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function runtimeError(message) {
|
|
13
|
-
return new Error(`simple-agents-wasm runtime error: ${message}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
17
|
function toMessages(promptOrMessages) {
|
|
17
18
|
if (typeof promptOrMessages === "string") {
|
|
18
19
|
const content = promptOrMessages.trim();
|
|
@@ -62,62 +63,6 @@ function toToolCalls(toolCalls) {
|
|
|
62
63
|
}));
|
|
63
64
|
}
|
|
64
65
|
|
|
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
66
|
|
|
122
67
|
function assertWorkflowResultShape(result) {
|
|
123
68
|
if (result === null || typeof result !== "object") {
|
|
@@ -180,36 +125,17 @@ function finiteNumberOrNull(value) {
|
|
|
180
125
|
}
|
|
181
126
|
|
|
182
127
|
function buildStepDetail(step) {
|
|
183
|
-
|
|
184
|
-
elapsed_ms: step.elapsedMs,
|
|
128
|
+
return {
|
|
185
129
|
node_id: step.nodeId,
|
|
186
|
-
node_kind: step.nodeKind
|
|
130
|
+
node_kind: step.nodeKind,
|
|
131
|
+
model_name: step.modelName ?? null,
|
|
132
|
+
elapsed_ms: step.elapsedMs,
|
|
133
|
+
prompt_tokens: finiteNumberOrNull(step.promptTokens),
|
|
134
|
+
completion_tokens: finiteNumberOrNull(step.completionTokens),
|
|
135
|
+
total_tokens: finiteNumberOrNull(step.totalTokens),
|
|
136
|
+
reasoning_tokens: 0,
|
|
137
|
+
tokens_per_second: finiteNumberOrNull(step.tokensPerSecond)
|
|
187
138
|
};
|
|
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
139
|
}
|
|
214
140
|
|
|
215
141
|
function buildWorkflowNerdstats(summary) {
|
|
@@ -605,10 +531,6 @@ function llmOutputSchema(node) {
|
|
|
605
531
|
};
|
|
606
532
|
}
|
|
607
533
|
|
|
608
|
-
function normalizeSseChunk(chunk) {
|
|
609
|
-
return chunk.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
610
|
-
}
|
|
611
|
-
|
|
612
534
|
function evaluateSwitchCondition(condition, context) {
|
|
613
535
|
if (typeof condition !== "string") {
|
|
614
536
|
return false;
|
|
@@ -629,66 +551,6 @@ function evaluateSwitchCondition(condition, context) {
|
|
|
629
551
|
return false;
|
|
630
552
|
}
|
|
631
553
|
|
|
632
|
-
function parseSseEventBlock(block) {
|
|
633
|
-
const lines = block.split("\n");
|
|
634
|
-
const dataLines = [];
|
|
635
|
-
for (const line of lines) {
|
|
636
|
-
if (line.startsWith("data:")) {
|
|
637
|
-
dataLines.push(line.slice(5).trimStart());
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (dataLines.length === 0) {
|
|
642
|
-
return null;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const payload = dataLines.join("\n");
|
|
646
|
-
if (payload === "[DONE]") {
|
|
647
|
-
return { done: true };
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
try {
|
|
651
|
-
return { done: false, json: JSON.parse(payload), raw: payload };
|
|
652
|
-
} catch {
|
|
653
|
-
return { done: false, raw: payload };
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
async function* iterateSse(response) {
|
|
658
|
-
if (!response.body) {
|
|
659
|
-
throw runtimeError("stream response had no body");
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const reader = response.body.getReader();
|
|
663
|
-
const decoder = new TextDecoder();
|
|
664
|
-
let buffer = "";
|
|
665
|
-
|
|
666
|
-
while (true) {
|
|
667
|
-
const { value, done } = await reader.read();
|
|
668
|
-
if (done) {
|
|
669
|
-
break;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
buffer += normalizeSseChunk(decoder.decode(value, { stream: true }));
|
|
673
|
-
let delimiterIndex = buffer.indexOf("\n\n");
|
|
674
|
-
while (delimiterIndex !== -1) {
|
|
675
|
-
const block = buffer.slice(0, delimiterIndex).trim();
|
|
676
|
-
buffer = buffer.slice(delimiterIndex + 2);
|
|
677
|
-
if (block.length > 0) {
|
|
678
|
-
yield block;
|
|
679
|
-
}
|
|
680
|
-
delimiterIndex = buffer.indexOf("\n\n");
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
buffer += normalizeSseChunk(decoder.decode());
|
|
685
|
-
|
|
686
|
-
const trailing = buffer.trim();
|
|
687
|
-
if (trailing.length > 0) {
|
|
688
|
-
yield trailing;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
554
|
class BrowserJsClient {
|
|
693
555
|
constructor(provider, config) {
|
|
694
556
|
if (provider !== "openai" && provider !== "openrouter") {
|
|
@@ -829,10 +691,7 @@ class BrowserJsClient {
|
|
|
829
691
|
}
|
|
830
692
|
|
|
831
693
|
const started = performance.now();
|
|
832
|
-
|
|
833
|
-
let responseModel = model;
|
|
834
|
-
let aggregate = "";
|
|
835
|
-
let finishReason;
|
|
694
|
+
const aggregateState = createStreamAggregator(model);
|
|
836
695
|
let usage = {
|
|
837
696
|
promptTokens: 0,
|
|
838
697
|
completionTokens: 0,
|
|
@@ -867,18 +726,7 @@ class BrowserJsClient {
|
|
|
867
726
|
raw: parsed.raw
|
|
868
727
|
};
|
|
869
728
|
|
|
870
|
-
|
|
871
|
-
responseId = delta.id;
|
|
872
|
-
}
|
|
873
|
-
if (delta.model) {
|
|
874
|
-
responseModel = delta.model;
|
|
875
|
-
}
|
|
876
|
-
if (delta.content) {
|
|
877
|
-
aggregate += delta.content;
|
|
878
|
-
}
|
|
879
|
-
if (delta.finishReason) {
|
|
880
|
-
finishReason = delta.finishReason;
|
|
881
|
-
}
|
|
729
|
+
applyDeltaToAggregate(aggregateState, delta);
|
|
882
730
|
if (chunk?.usage && typeof chunk.usage === "object") {
|
|
883
731
|
usage = toUsage(chunk.usage);
|
|
884
732
|
usageAvailable = true;
|
|
@@ -897,11 +745,11 @@ class BrowserJsClient {
|
|
|
897
745
|
const latencyMs = Math.max(0, Math.round(performance.now() - started));
|
|
898
746
|
|
|
899
747
|
return {
|
|
900
|
-
id: responseId,
|
|
901
|
-
model: responseModel,
|
|
748
|
+
id: aggregateState.responseId,
|
|
749
|
+
model: aggregateState.responseModel,
|
|
902
750
|
role: "assistant",
|
|
903
|
-
content: aggregate,
|
|
904
|
-
finishReason,
|
|
751
|
+
content: aggregateState.aggregate,
|
|
752
|
+
finishReason: aggregateState.finishReason,
|
|
905
753
|
usage: {
|
|
906
754
|
...usage
|
|
907
755
|
},
|
|
@@ -1323,25 +1171,6 @@ class BrowserJsClient {
|
|
|
1323
1171
|
}
|
|
1324
1172
|
}
|
|
1325
1173
|
|
|
1326
|
-
let rustModulePromise;
|
|
1327
|
-
|
|
1328
|
-
async function loadRustModule() {
|
|
1329
|
-
if (!rustModulePromise) {
|
|
1330
|
-
rustModulePromise = (async () => {
|
|
1331
|
-
try {
|
|
1332
|
-
const moduleValue = await import("./pkg/simple_agents_wasm.js");
|
|
1333
|
-
const wasmUrl = new URL("./pkg/simple_agents_wasm_bg.wasm", import.meta.url);
|
|
1334
|
-
await moduleValue.default({ module_or_path: wasmUrl });
|
|
1335
|
-
return moduleValue;
|
|
1336
|
-
} catch {
|
|
1337
|
-
return null;
|
|
1338
|
-
}
|
|
1339
|
-
})();
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
return rustModulePromise;
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
1174
|
export class Client {
|
|
1346
1175
|
constructor(provider, config) {
|
|
1347
1176
|
this.fallbackClient = new BrowserJsClient(provider, config);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-agents-wasm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.33",
|
|
4
4
|
"description": "Browser-compatible SimpleAgents client for OpenAI-compatible providers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"index.js",
|
|
18
18
|
"index.d.ts",
|
|
19
|
+
"runtime",
|
|
19
20
|
"README.md",
|
|
20
21
|
"pkg",
|
|
21
22
|
"rust/Cargo.toml",
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
let rustModulePromise;
|
|
2
|
+
|
|
3
|
+
export async function loadRustModule() {
|
|
4
|
+
if (!rustModulePromise) {
|
|
5
|
+
rustModulePromise = (async () => {
|
|
6
|
+
try {
|
|
7
|
+
const moduleValue = await import("../pkg/simple_agents_wasm.js");
|
|
8
|
+
const wasmUrl = new URL("../pkg/simple_agents_wasm_bg.wasm", import.meta.url);
|
|
9
|
+
await moduleValue.default({ module_or_path: wasmUrl });
|
|
10
|
+
return moduleValue;
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
})();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return rustModulePromise;
|
|
18
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { runtimeError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export function createStreamAggregator(model) {
|
|
4
|
+
return {
|
|
5
|
+
responseId: "",
|
|
6
|
+
responseModel: model,
|
|
7
|
+
aggregate: "",
|
|
8
|
+
finishReason: undefined
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function applyDeltaToAggregate(state, delta) {
|
|
13
|
+
if (!state || !delta) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!state.responseId && delta.id) {
|
|
18
|
+
state.responseId = delta.id;
|
|
19
|
+
}
|
|
20
|
+
if (delta.model) {
|
|
21
|
+
state.responseModel = delta.model;
|
|
22
|
+
}
|
|
23
|
+
if (delta.content) {
|
|
24
|
+
state.aggregate += delta.content;
|
|
25
|
+
}
|
|
26
|
+
if (delta.finishReason) {
|
|
27
|
+
state.finishReason = delta.finishReason;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createStreamEventBridge(model, onChunk) {
|
|
32
|
+
const aggregateState = createStreamAggregator(model);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
onEvent(event) {
|
|
36
|
+
if (event.eventType === "delta") {
|
|
37
|
+
const delta = event.delta;
|
|
38
|
+
if (!delta) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
applyDeltaToAggregate(aggregateState, delta);
|
|
43
|
+
|
|
44
|
+
onChunk({
|
|
45
|
+
id: delta.id,
|
|
46
|
+
model: delta.model,
|
|
47
|
+
content: delta.content,
|
|
48
|
+
finishReason: delta.finishReason,
|
|
49
|
+
raw: delta.raw
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (event.eventType === "error") {
|
|
54
|
+
onChunk({
|
|
55
|
+
id: aggregateState.responseId || "error",
|
|
56
|
+
model: aggregateState.responseModel,
|
|
57
|
+
error: event.error?.message ?? "stream error"
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
mergeResult(result, started) {
|
|
62
|
+
return {
|
|
63
|
+
...result,
|
|
64
|
+
id: result.id || aggregateState.responseId,
|
|
65
|
+
model: result.model || aggregateState.responseModel,
|
|
66
|
+
content: result.content ?? aggregateState.aggregate,
|
|
67
|
+
finishReason: result.finishReason ?? aggregateState.finishReason,
|
|
68
|
+
latencyMs: Math.max(0, Math.round(performance.now() - started))
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeSseChunk(chunk) {
|
|
75
|
+
return chunk.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseSseEventBlock(block) {
|
|
79
|
+
const lines = block.split("\n");
|
|
80
|
+
const dataLines = [];
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
if (line.startsWith("data:")) {
|
|
83
|
+
dataLines.push(line.slice(5).trimStart());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (dataLines.length === 0) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const payload = dataLines.join("\n");
|
|
92
|
+
if (payload === "[DONE]") {
|
|
93
|
+
return { done: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
return { done: false, json: JSON.parse(payload), raw: payload };
|
|
98
|
+
} catch {
|
|
99
|
+
return { done: false, raw: payload };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function* iterateSse(response) {
|
|
104
|
+
if (!response.body) {
|
|
105
|
+
throw runtimeError("stream response had no body");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const reader = response.body.getReader();
|
|
109
|
+
const decoder = new TextDecoder();
|
|
110
|
+
let buffer = "";
|
|
111
|
+
|
|
112
|
+
while (true) {
|
|
113
|
+
const { value, done } = await reader.read();
|
|
114
|
+
if (done) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
buffer += normalizeSseChunk(decoder.decode(value, { stream: true }));
|
|
119
|
+
let delimiterIndex = buffer.indexOf("\n\n");
|
|
120
|
+
while (delimiterIndex !== -1) {
|
|
121
|
+
const block = buffer.slice(0, delimiterIndex).trim();
|
|
122
|
+
buffer = buffer.slice(delimiterIndex + 2);
|
|
123
|
+
if (block.length > 0) {
|
|
124
|
+
yield block;
|
|
125
|
+
}
|
|
126
|
+
delimiterIndex = buffer.indexOf("\n\n");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
buffer += normalizeSseChunk(decoder.decode());
|
|
131
|
+
|
|
132
|
+
const trailing = buffer.trim();
|
|
133
|
+
if (trailing.length > 0) {
|
|
134
|
+
yield trailing;
|
|
135
|
+
}
|
|
136
|
+
}
|