@robzilla1738/agentswarm 0.2.0

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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/bin/swarm.js +10 -0
  4. package/dist/agent.js +211 -0
  5. package/dist/cli.js +667 -0
  6. package/dist/config.js +289 -0
  7. package/dist/control.js +96 -0
  8. package/dist/deepseek.js +321 -0
  9. package/dist/executor.js +988 -0
  10. package/dist/hub.js +553 -0
  11. package/dist/journal.js +152 -0
  12. package/dist/prompts.js +232 -0
  13. package/dist/providers.js +151 -0
  14. package/dist/run.js +309 -0
  15. package/dist/sandbox.js +505 -0
  16. package/dist/state.js +230 -0
  17. package/dist/terminal.js +298 -0
  18. package/dist/tools.js +491 -0
  19. package/dist/types.js +26 -0
  20. package/dist/util.js +209 -0
  21. package/dist/webtools.js +205 -0
  22. package/package.json +63 -0
  23. package/ui/out/404/index.html +1 -0
  24. package/ui/out/404.html +1 -0
  25. package/ui/out/_next/static/chunks/255-2aa030c9ba2867e3.js +1 -0
  26. package/ui/out/_next/static/chunks/383-289a866b246b41cc.js +1 -0
  27. package/ui/out/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
  28. package/ui/out/_next/static/chunks/619-ba102abea3e3d0e4.js +1 -0
  29. package/ui/out/_next/static/chunks/677-b37981ba0eca75b2.js +1 -0
  30. package/ui/out/_next/static/chunks/app/_not-found/page-2d0982e372f7be41.js +1 -0
  31. package/ui/out/_next/static/chunks/app/layout-37ad32c5fdb26f29.js +1 -0
  32. package/ui/out/_next/static/chunks/app/page-0c9f35bd4aa8e370.js +1 -0
  33. package/ui/out/_next/static/chunks/app/run/page-13dc41a57e34da71.js +1 -0
  34. package/ui/out/_next/static/chunks/app/settings/page-a1763be7f6de888c.js +1 -0
  35. package/ui/out/_next/static/chunks/framework-2c534e0e662575a2.js +1 -0
  36. package/ui/out/_next/static/chunks/main-app-889ed884f8bc78e3.js +1 -0
  37. package/ui/out/_next/static/chunks/main-eb90ae3b35d2fd16.js +1 -0
  38. package/ui/out/_next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  39. package/ui/out/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  40. package/ui/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  41. package/ui/out/_next/static/chunks/webpack-38639c05c96dbeca.js +1 -0
  42. package/ui/out/_next/static/css/82edaa7a5942f894.css +3 -0
  43. package/ui/out/_next/static/eiQeDU9uBHNsBj0CFkp8M/_buildManifest.js +1 -0
  44. package/ui/out/_next/static/eiQeDU9uBHNsBj0CFkp8M/_ssgManifest.js +1 -0
  45. package/ui/out/_next/static/media/0aa834ed78bf6d07-s.woff2 +0 -0
  46. package/ui/out/_next/static/media/438aa629764e75f3-s.woff2 +0 -0
  47. package/ui/out/_next/static/media/4c9affa5bc8f420e-s.p.woff2 +0 -0
  48. package/ui/out/_next/static/media/51251f8b9793cdb3-s.woff2 +0 -0
  49. package/ui/out/_next/static/media/67957d42bae0796d-s.woff2 +0 -0
  50. package/ui/out/_next/static/media/875ae681bfde4580-s.woff2 +0 -0
  51. package/ui/out/_next/static/media/886030b0b59bc5a7-s.woff2 +0 -0
  52. package/ui/out/_next/static/media/939c4f875ee75fbb-s.woff2 +0 -0
  53. package/ui/out/_next/static/media/bb3ef058b751a6ad-s.p.woff2 +0 -0
  54. package/ui/out/_next/static/media/cc978ac5ee68c2b6-s.woff2 +0 -0
  55. package/ui/out/_next/static/media/e857b654a2caa584-s.woff2 +0 -0
  56. package/ui/out/_next/static/media/f911b923c6adde36-s.woff2 +0 -0
  57. package/ui/out/icon.png +0 -0
  58. package/ui/out/index.html +1 -0
  59. package/ui/out/index.txt +22 -0
  60. package/ui/out/run/index.html +1 -0
  61. package/ui/out/run/index.txt +22 -0
  62. package/ui/out/settings/index.html +1 -0
  63. package/ui/out/settings/index.txt +22 -0
  64. package/ui/out/swarm-mark.png +0 -0
package/dist/config.js ADDED
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SETTABLE_KEYS = exports.SECRET_ENV_KEYS = exports.DEFAULTS = exports.DEFAULT_PRICING = void 0;
37
+ exports.home = home;
38
+ exports.runsDir = runsDir;
39
+ exports.runDir = runDir;
40
+ exports.configPath = configPath;
41
+ exports.loadConfig = loadConfig;
42
+ exports.saveConfig = saveConfig;
43
+ exports.maskKey = maskKey;
44
+ exports.coerceConfigValue = coerceConfigValue;
45
+ const fs = __importStar(require("fs"));
46
+ const os = __importStar(require("os"));
47
+ const path = __importStar(require("path"));
48
+ const providers_1 = require("./providers");
49
+ const util_1 = require("./util");
50
+ exports.DEFAULT_PRICING = {
51
+ // $ per 1M tokens (June 2026, api-docs.deepseek.com/quick_start/pricing)
52
+ "deepseek-v4-flash": { inMiss: 0.14, inHit: 0.0028, out: 0.28 },
53
+ "deepseek-v4-pro": { inMiss: 0.435, inHit: 0.003625, out: 0.87 },
54
+ // Deprecated aliases (map to v4-flash modes until 2026-07-24)
55
+ "deepseek-chat": { inMiss: 0.14, inHit: 0.0028, out: 0.28 },
56
+ "deepseek-reasoner": { inMiss: 0.14, inHit: 0.0028, out: 0.28 },
57
+ // Approximate list prices for other providers — unknown models cost $0 in
58
+ // the UI rather than guessing.
59
+ "gpt-5.1": { inMiss: 1.25, inHit: 0.125, out: 10 },
60
+ "gpt-5.1-mini": { inMiss: 0.25, inHit: 0.025, out: 2 },
61
+ "claude-sonnet-4-6": { inMiss: 3, inHit: 0.3, out: 15 },
62
+ "claude-haiku-4-5": { inMiss: 1, inHit: 0.1, out: 5 },
63
+ "MiniMax-M2.1": { inMiss: 0.3, inHit: 0.03, out: 1.2 },
64
+ "MiniMax-M2": { inMiss: 0.3, inHit: 0.03, out: 1.2 },
65
+ };
66
+ exports.DEFAULTS = {
67
+ provider: "deepseek",
68
+ providers: {},
69
+ apiKey: "",
70
+ baseUrl: providers_1.PROVIDERS.deepseek.baseUrl,
71
+ model: "deepseek-v4-flash",
72
+ conductorModel: "deepseek-v4-flash",
73
+ maxWorkers: 6,
74
+ maxStepsPerTask: 30,
75
+ maxTasks: 48,
76
+ maxTokensPerRun: 12_000_000,
77
+ verification: "normal",
78
+ thinking: true,
79
+ reasoningEffort: "high",
80
+ safeMode: true,
81
+ tinyfishApiKey: "",
82
+ searchBackend: "auto",
83
+ searchkitCmd: "searchkit",
84
+ sandboxRuntime: "host",
85
+ sandboxImage: "node:22-bookworm",
86
+ e2bApiKey: "",
87
+ e2bTemplate: "base",
88
+ modalTokenId: "",
89
+ modalTokenSecret: "",
90
+ vercelToken: "",
91
+ vercelTeamId: "",
92
+ vercelProjectId: "",
93
+ requestTimeoutMs: 900_000,
94
+ idleTimeoutMs: 180_000,
95
+ contextTokenLimit: 120_000,
96
+ maxToolResultChars: 12_000,
97
+ hubPort: 7777,
98
+ uiPort: 7780,
99
+ pricing: exports.DEFAULT_PRICING,
100
+ };
101
+ /**
102
+ * Env vars that must never leak into agent shell commands when they execute
103
+ * directly on the host: every provider key env plus the search/sandbox
104
+ * credentials the engine itself understands.
105
+ */
106
+ exports.SECRET_ENV_KEYS = [
107
+ ...new Set([
108
+ ...Object.values(providers_1.PROVIDERS)
109
+ .map((p) => p.keyEnv)
110
+ .filter((k) => Boolean(k)),
111
+ "TINYFISH_API_KEY",
112
+ "E2B_API_KEY",
113
+ "MODAL_TOKEN_ID",
114
+ "MODAL_TOKEN_SECRET",
115
+ "VERCEL_SANDBOX_TOKEN",
116
+ ]),
117
+ ];
118
+ function home() {
119
+ return process.env.AGENTSWARM_HOME || path.join(os.homedir(), ".agentswarm");
120
+ }
121
+ function runsDir() {
122
+ return path.join(home(), "runs");
123
+ }
124
+ function runDir(id) {
125
+ return path.join(runsDir(), id);
126
+ }
127
+ function configPath() {
128
+ return path.join(home(), "config.json");
129
+ }
130
+ /**
131
+ * Migrate a pre-provider config file in place: flat apiKey/baseUrl belonged
132
+ * to DeepSeek (the only provider that existed).
133
+ */
134
+ function migrate(file) {
135
+ const out = { ...file, providers: { ...(file.providers || {}) } };
136
+ if ((file.apiKey || file.baseUrl) && !out.providers.deepseek) {
137
+ out.providers.deepseek = {
138
+ ...(file.apiKey ? { apiKey: file.apiKey } : {}),
139
+ ...(file.baseUrl && file.baseUrl !== providers_1.PROVIDERS.deepseek.baseUrl ? { baseUrl: file.baseUrl } : {}),
140
+ };
141
+ }
142
+ return out;
143
+ }
144
+ function loadConfig() {
145
+ const file = migrate((0, util_1.readJson)(configPath(), {}));
146
+ const provider = (0, providers_1.isProviderId)(file.provider) ? file.provider : "deepseek";
147
+ const info = providers_1.PROVIDERS[provider];
148
+ const cred = file.providers?.[provider] || {};
149
+ const cfg = {
150
+ ...exports.DEFAULTS,
151
+ ...file,
152
+ provider,
153
+ providers: file.providers || {},
154
+ pricing: { ...exports.DEFAULT_PRICING, ...(file.pricing || {}) },
155
+ apiKey: cred.apiKey || "",
156
+ baseUrl: cred.baseUrl || info.baseUrl,
157
+ };
158
+ // Env overrides: provider-specific key env, plus legacy DEEPSEEK_API_KEY.
159
+ if (info.keyEnv && process.env[info.keyEnv])
160
+ cfg.apiKey = process.env[info.keyEnv];
161
+ if (process.env.TINYFISH_API_KEY)
162
+ cfg.tinyfishApiKey = process.env.TINYFISH_API_KEY;
163
+ if (process.env.E2B_API_KEY)
164
+ cfg.e2bApiKey = process.env.E2B_API_KEY;
165
+ if (process.env.MODAL_TOKEN_ID)
166
+ cfg.modalTokenId = process.env.MODAL_TOKEN_ID;
167
+ if (process.env.MODAL_TOKEN_SECRET)
168
+ cfg.modalTokenSecret = process.env.MODAL_TOKEN_SECRET;
169
+ if (process.env.VERCEL_SANDBOX_TOKEN)
170
+ cfg.vercelToken = process.env.VERCEL_SANDBOX_TOKEN;
171
+ if (process.env.SWARM_HUB_PORT)
172
+ cfg.hubPort = Number(process.env.SWARM_HUB_PORT) || cfg.hubPort;
173
+ return cfg;
174
+ }
175
+ function saveConfig(patch) {
176
+ (0, util_1.ensureDir)(home());
177
+ const current = migrate((0, util_1.readJson)(configPath(), {}));
178
+ const next = { ...current, ...patch };
179
+ // Per-provider creds deep-merge so saving one provider's key keeps the rest.
180
+ if (patch.providers) {
181
+ next.providers = { ...(current.providers || {}) };
182
+ for (const [id, cred] of Object.entries(patch.providers)) {
183
+ if (!(0, providers_1.isProviderId)(id) || !cred)
184
+ continue;
185
+ next.providers[id] = { ...(next.providers[id] || {}), ...cred };
186
+ }
187
+ }
188
+ // Legacy flat fields (CLI `swarm config set apiKey/baseUrl`) target the
189
+ // active provider rather than a dead top-level slot.
190
+ const active = (0, providers_1.isProviderId)(next.provider) ? next.provider : "deepseek";
191
+ for (const k of ["apiKey", "baseUrl"]) {
192
+ if (patch[k] !== undefined) {
193
+ next.providers = next.providers || {};
194
+ next.providers[active] = { ...(next.providers[active] || {}), [k]: patch[k] };
195
+ delete next[k];
196
+ }
197
+ }
198
+ (0, util_1.writeJson)(configPath(), next, 0o600);
199
+ try {
200
+ fs.chmodSync(configPath(), 0o600);
201
+ }
202
+ catch {
203
+ /* best effort */
204
+ }
205
+ return loadConfig();
206
+ }
207
+ function maskKey(key) {
208
+ if (!key)
209
+ return "";
210
+ if (key.length <= 9)
211
+ return key[0] + "…";
212
+ return key.slice(0, 5) + "…" + key.slice(-4);
213
+ }
214
+ /** Settable via `swarm config set <key> <value>` and the UI settings page. */
215
+ exports.SETTABLE_KEYS = [
216
+ "provider",
217
+ "apiKey",
218
+ "baseUrl",
219
+ "model",
220
+ "conductorModel",
221
+ "maxWorkers",
222
+ "maxStepsPerTask",
223
+ "maxTasks",
224
+ "maxTokensPerRun",
225
+ "verification",
226
+ "thinking",
227
+ "reasoningEffort",
228
+ "safeMode",
229
+ "tinyfishApiKey",
230
+ "searchBackend",
231
+ "searchkitCmd",
232
+ "sandboxRuntime",
233
+ "sandboxImage",
234
+ "e2bApiKey",
235
+ "e2bTemplate",
236
+ "modalTokenId",
237
+ "modalTokenSecret",
238
+ "vercelToken",
239
+ "vercelTeamId",
240
+ "vercelProjectId",
241
+ "contextTokenLimit",
242
+ "hubPort",
243
+ "uiPort",
244
+ ];
245
+ /** Allowed ranges for numeric settings (values are clamped, not rejected). */
246
+ const NUM_RANGES = {
247
+ maxWorkers: [1, 32],
248
+ maxStepsPerTask: [3, 200],
249
+ maxTasks: [1, 1000],
250
+ maxTokensPerRun: [50_000, 2_000_000_000],
251
+ contextTokenLimit: [8_000, 900_000],
252
+ hubPort: [0, 65535],
253
+ uiPort: [0, 65535],
254
+ };
255
+ const ENUMS = {
256
+ verification: ["off", "normal", "strict"],
257
+ reasoningEffort: ["low", "medium", "high", "max"],
258
+ searchBackend: ["auto", "tinyfish", "ddg"],
259
+ sandboxRuntime: ["auto", "host", "docker", "e2b", "modal", "vercel"],
260
+ provider: Object.keys(providers_1.PROVIDERS),
261
+ };
262
+ /**
263
+ * Validate + normalize one settable config value (from the CLI or the hub's
264
+ * JSON body). Throws with a human message on invalid input — never lets NaN
265
+ * or a bad enum reach config.json, where it would poison every later run.
266
+ */
267
+ function coerceConfigValue(key, raw) {
268
+ const range = NUM_RANGES[key];
269
+ if (range) {
270
+ const n = Number(raw);
271
+ if (!Number.isFinite(n))
272
+ throw new Error(`${key} must be a number`);
273
+ return Math.min(range[1], Math.max(range[0], Math.round(n)));
274
+ }
275
+ if (key === "thinking" || key === "safeMode") {
276
+ if (typeof raw === "boolean")
277
+ return raw;
278
+ return raw === "true" || raw === "1" || raw === "on";
279
+ }
280
+ const allowed = ENUMS[key];
281
+ if (allowed) {
282
+ const v = String(raw);
283
+ if (!allowed.includes(v))
284
+ throw new Error(`${key} must be one of: ${allowed.join(" | ")}`);
285
+ return v;
286
+ }
287
+ // Secrets, URLs, model names: strip stray whitespace/newlines from pastes.
288
+ return String(raw).trim();
289
+ }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ControlReader = void 0;
37
+ exports.controlFile = controlFile;
38
+ exports.appendControl = appendControl;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const util_1 = require("./util");
42
+ function controlFile(runDirPath) {
43
+ return path.join(runDirPath, "control.jsonl");
44
+ }
45
+ function appendControl(runDirPath, msg) {
46
+ const line = JSON.stringify({ t: Date.now(), ...msg }) + "\n";
47
+ fs.appendFileSync(controlFile(runDirPath), line, "utf8");
48
+ }
49
+ class ControlReader {
50
+ file;
51
+ offset = 0;
52
+ constructor(runDirPath) {
53
+ this.file = controlFile(runDirPath);
54
+ try {
55
+ this.offset = fs.statSync(this.file).size;
56
+ }
57
+ catch {
58
+ this.offset = 0;
59
+ }
60
+ }
61
+ poll() {
62
+ let stat;
63
+ try {
64
+ stat = fs.statSync(this.file);
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ if (stat.size <= this.offset) {
70
+ if (stat.size < this.offset)
71
+ this.offset = 0;
72
+ return [];
73
+ }
74
+ const fd = fs.openSync(this.file, "r");
75
+ try {
76
+ const len = stat.size - this.offset;
77
+ const buf = Buffer.alloc(len);
78
+ fs.readSync(fd, buf, 0, len, this.offset);
79
+ this.offset = stat.size;
80
+ const out = [];
81
+ for (const line of buf.toString("utf8").split("\n")) {
82
+ const s = line.trim();
83
+ if (!s)
84
+ continue;
85
+ const msg = (0, util_1.safeJson)(s);
86
+ if (msg && msg.kind)
87
+ out.push(msg);
88
+ }
89
+ return out;
90
+ }
91
+ finally {
92
+ fs.closeSync(fd);
93
+ }
94
+ }
95
+ }
96
+ exports.ControlReader = ControlReader;
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CancelledError = exports.ApiError = void 0;
4
+ exports.chat = chat;
5
+ exports.isFatalAuthError = isFatalAuthError;
6
+ exports.validateAuth = validateAuth;
7
+ exports.listModels = listModels;
8
+ const providers_1 = require("./providers");
9
+ const util_1 = require("./util");
10
+ function providerOf(cfg) {
11
+ return providers_1.PROVIDERS[cfg.provider] ?? providers_1.PROVIDERS.deepseek;
12
+ }
13
+ class ApiError extends Error {
14
+ status;
15
+ body;
16
+ constructor(status, body) {
17
+ super(`API ${status}: ${body.slice(0, 600)}`);
18
+ this.status = status;
19
+ this.body = body;
20
+ }
21
+ }
22
+ exports.ApiError = ApiError;
23
+ class CancelledError extends Error {
24
+ constructor() {
25
+ super("cancelled");
26
+ }
27
+ }
28
+ exports.CancelledError = CancelledError;
29
+ function retryable(e) {
30
+ if (e instanceof CancelledError)
31
+ return false;
32
+ if (e instanceof ApiError)
33
+ return e.status === 429 || e.status >= 500;
34
+ // Network hiccups, idle timeouts, broken streams.
35
+ return true;
36
+ }
37
+ function normalizeUsage(u) {
38
+ const prompt = u?.prompt_tokens ?? 0;
39
+ const completion = u?.completion_tokens ?? 0;
40
+ const hit = u?.prompt_cache_hit_tokens ?? u?.prompt_tokens_details?.cached_tokens ?? 0;
41
+ const miss = u?.prompt_cache_miss_tokens ?? Math.max(0, prompt - hit);
42
+ return {
43
+ promptTokens: prompt,
44
+ completionTokens: completion,
45
+ cacheHitTokens: hit,
46
+ cacheMissTokens: miss,
47
+ };
48
+ }
49
+ function sanitizeMessages(messages, thinking) {
50
+ return messages.map((m) => {
51
+ if (m.role === "assistant") {
52
+ const out = { role: "assistant", content: m.content ?? "" };
53
+ if (m.tool_calls?.length) {
54
+ out.tool_calls = m.tool_calls;
55
+ // DeepSeek thinking mode REQUIRES reasoning_content on tool-call turns
56
+ // (400 otherwise). In non-thinking mode the field must be absent.
57
+ if (thinking)
58
+ out.reasoning_content = m.reasoning_content ?? "";
59
+ }
60
+ return out;
61
+ }
62
+ if (m.role === "tool") {
63
+ return { role: "tool", content: m.content ?? "", tool_call_id: m.tool_call_id };
64
+ }
65
+ return { role: m.role, content: m.content ?? "" };
66
+ });
67
+ }
68
+ /**
69
+ * One streaming chat-completions call with retries. Streams deltas (text and
70
+ * reasoning), accumulates tool calls, captures usage from the final chunk.
71
+ */
72
+ async function chat(cfg, o) {
73
+ let lastErr;
74
+ const attempts = 4;
75
+ for (let i = 0; i < attempts; i++) {
76
+ try {
77
+ return await chatOnce(cfg, o);
78
+ }
79
+ catch (e) {
80
+ lastErr = e;
81
+ if (!retryable(e) || i === attempts - 1)
82
+ throw e;
83
+ const backoff = [1500, 5000, 15000][i] ?? 15000;
84
+ await (0, util_1.sleep)(backoff + Math.random() * 1000);
85
+ if (o.signal?.aborted)
86
+ throw new CancelledError();
87
+ }
88
+ }
89
+ throw lastErr;
90
+ }
91
+ async function chatOnce(cfg, o) {
92
+ const provider = providerOf(cfg);
93
+ if (!cfg.apiKey && provider.keyRequired) {
94
+ throw new ApiError(401, `No ${provider.label} API key configured. Run \`swarm config set apiKey ...\` or open Settings in the UI.`);
95
+ }
96
+ if (o.signal?.aborted)
97
+ throw new CancelledError();
98
+ // DeepSeek's thinking protocol (the `thinking` body field + reasoning_content
99
+ // echo) is provider-specific; unknown body params are hard 400s elsewhere.
100
+ const dsThinking = provider.deepseekThinking;
101
+ const body = {
102
+ model: o.model,
103
+ messages: sanitizeMessages(o.messages, o.thinking && dsThinking),
104
+ stream: true,
105
+ stream_options: { include_usage: true },
106
+ [provider.maxTokensParam]: o.maxTokens ?? 16384,
107
+ };
108
+ if (dsThinking)
109
+ body.thinking = { type: o.thinking ? "enabled" : "disabled" };
110
+ const effort = o.thinking ? (0, providers_1.mapEffort)(o.reasoningEffort, provider) : undefined;
111
+ if (effort)
112
+ body.reasoning_effort = effort;
113
+ if (o.tools?.length) {
114
+ body.tools = o.tools.map((t) => ({
115
+ type: "function",
116
+ function: { name: t.name, description: t.description, parameters: t.parameters },
117
+ }));
118
+ if (o.toolChoice === "none" ||
119
+ o.toolChoice === "auto" ||
120
+ o.toolChoice === "required" ||
121
+ o.toolChoice === undefined) {
122
+ body.tool_choice = o.toolChoice ?? "auto";
123
+ }
124
+ else {
125
+ body.tool_choice = { type: "function", function: { name: o.toolChoice } };
126
+ }
127
+ }
128
+ const ac = new AbortController();
129
+ const onOuterAbort = () => ac.abort();
130
+ o.signal?.addEventListener("abort", onOuterAbort, { once: true });
131
+ let lastActivity = Date.now();
132
+ const idleTimer = setInterval(() => {
133
+ if (Date.now() - lastActivity > cfg.idleTimeoutMs)
134
+ ac.abort();
135
+ }, 5000);
136
+ const hardTimer = setTimeout(() => ac.abort(), cfg.requestTimeoutMs);
137
+ try {
138
+ const base = cfg.baseUrl.replace(/\/+$/, "");
139
+ const headers = { "content-type": "application/json" };
140
+ if (cfg.apiKey)
141
+ headers.authorization = `Bearer ${cfg.apiKey}`;
142
+ if (cfg.provider === "openrouter")
143
+ headers["x-title"] = "agentswarm";
144
+ const res = await fetch(`${base}/chat/completions`, {
145
+ method: "POST",
146
+ headers,
147
+ body: JSON.stringify(body),
148
+ signal: ac.signal,
149
+ });
150
+ if (!res.ok) {
151
+ const text = await res.text().catch(() => "");
152
+ throw new ApiError(res.status, text);
153
+ }
154
+ if (!res.body)
155
+ throw new ApiError(0, "empty response body");
156
+ const reader = res.body.getReader();
157
+ const decoder = new TextDecoder();
158
+ let buf = "";
159
+ let content = "";
160
+ let reasoning = "";
161
+ let finishReason = "stop";
162
+ let usage = { promptTokens: 0, completionTokens: 0, cacheHitTokens: 0, cacheMissTokens: 0 };
163
+ const calls = new Map();
164
+ const handleLine = (line) => {
165
+ const s = line.trim();
166
+ if (!s.startsWith("data:"))
167
+ return;
168
+ const payload = s.slice(5).trim();
169
+ if (!payload || payload === "[DONE]")
170
+ return;
171
+ let chunk;
172
+ try {
173
+ chunk = JSON.parse(payload);
174
+ }
175
+ catch {
176
+ return;
177
+ }
178
+ if (chunk.usage)
179
+ usage = normalizeUsage(chunk.usage);
180
+ const choice = chunk.choices?.[0];
181
+ if (!choice)
182
+ return;
183
+ if (choice.finish_reason)
184
+ finishReason = choice.finish_reason;
185
+ const delta = choice.delta || {};
186
+ // reasoning_content: DeepSeek / MiniMax. reasoning: OpenRouter / LM Studio.
187
+ const think = typeof delta.reasoning_content === "string" && delta.reasoning_content
188
+ ? delta.reasoning_content
189
+ : typeof delta.reasoning === "string" && delta.reasoning
190
+ ? delta.reasoning
191
+ : "";
192
+ if (think) {
193
+ reasoning += think;
194
+ o.onDelta?.({ think });
195
+ }
196
+ if (typeof delta.content === "string" && delta.content) {
197
+ content += delta.content;
198
+ o.onDelta?.({ text: delta.content });
199
+ }
200
+ if (Array.isArray(delta.tool_calls)) {
201
+ for (const tc of delta.tool_calls) {
202
+ const idx = tc.index ?? 0;
203
+ const cur = calls.get(idx) ?? { id: "", name: "", args: "" };
204
+ if (tc.id)
205
+ cur.id = tc.id;
206
+ if (tc.function?.name)
207
+ cur.name = tc.function.name;
208
+ if (tc.function?.arguments)
209
+ cur.args += tc.function.arguments;
210
+ calls.set(idx, cur);
211
+ }
212
+ }
213
+ };
214
+ for (;;) {
215
+ const { done, value } = await reader.read();
216
+ if (done)
217
+ break;
218
+ lastActivity = Date.now();
219
+ buf += decoder.decode(value, { stream: true });
220
+ let nl;
221
+ while ((nl = buf.indexOf("\n")) >= 0) {
222
+ handleLine(buf.slice(0, nl));
223
+ buf = buf.slice(nl + 1);
224
+ }
225
+ }
226
+ if (buf)
227
+ handleLine(buf);
228
+ const toolCalls = [...calls.entries()]
229
+ .sort((a, b) => a[0] - b[0])
230
+ .map(([i, c]) => ({
231
+ id: c.id || `call_${i}`,
232
+ type: "function",
233
+ function: { name: c.name, arguments: c.args || "{}" },
234
+ }))
235
+ .filter((c) => c.function.name);
236
+ if (!usage.promptTokens && !usage.completionTokens) {
237
+ // Usage chunk missing — estimate so budgets still move.
238
+ const inChars = o.messages.reduce((n, m) => n + (m.content?.length ?? 0), 0);
239
+ const outChars = content.length + reasoning.length;
240
+ usage = {
241
+ promptTokens: Math.ceil(inChars / 3.5),
242
+ completionTokens: Math.ceil(outChars / 3.5),
243
+ cacheHitTokens: 0,
244
+ cacheMissTokens: Math.ceil(inChars / 3.5),
245
+ };
246
+ }
247
+ return { content, reasoning, toolCalls, finishReason, usage };
248
+ }
249
+ catch (e) {
250
+ if (o.signal?.aborted)
251
+ throw new CancelledError();
252
+ throw e;
253
+ }
254
+ finally {
255
+ clearInterval(idleTimer);
256
+ clearTimeout(hardTimer);
257
+ o.signal?.removeEventListener("abort", onOuterAbort);
258
+ }
259
+ }
260
+ function isFatalAuthError(e) {
261
+ if (e instanceof ApiError)
262
+ return e.status === 401 || e.status === 403;
263
+ if (e instanceof Error)
264
+ return /no \S+ api key/i.test(e.message);
265
+ return false;
266
+ }
267
+ /**
268
+ * Cheap auth preflight. Hits /models (no generation cost) and classifies the
269
+ * result so callers can give the operator an instant, clear error instead of a
270
+ * phantom run. Returns "ok" on success, "invalid" only on explicit 401/403,
271
+ * and "unknown" for anything else (never block on transient/unsupported).
272
+ */
273
+ async function validateAuth(cfg) {
274
+ const provider = providerOf(cfg);
275
+ if (!cfg.apiKey && provider.keyRequired) {
276
+ return { status: "invalid", message: `No ${provider.label} API key configured.` };
277
+ }
278
+ try {
279
+ const base = cfg.baseUrl.replace(/\/+$/, "");
280
+ const headers = {};
281
+ if (cfg.apiKey)
282
+ headers.authorization = `Bearer ${cfg.apiKey}`;
283
+ const res = await fetch(`${base}/models`, {
284
+ headers,
285
+ signal: AbortSignal.timeout(12000),
286
+ });
287
+ if (res.ok)
288
+ return { status: "ok" };
289
+ const body = await res.text().catch(() => "");
290
+ if (res.status === 401 || res.status === 403) {
291
+ return { status: "invalid", message: extractApiMessage(body) || "API key rejected (401)." };
292
+ }
293
+ return { status: "unknown", message: `HTTP ${res.status}` };
294
+ }
295
+ catch (e) {
296
+ return { status: "unknown", message: (0, util_1.errMsg)(e) };
297
+ }
298
+ }
299
+ function extractApiMessage(body) {
300
+ try {
301
+ const j = JSON.parse(body);
302
+ return j?.error?.message || j?.message || "";
303
+ }
304
+ catch {
305
+ return body.slice(0, 200);
306
+ }
307
+ }
308
+ async function listModels(cfg) {
309
+ const base = cfg.baseUrl.replace(/\/+$/, "");
310
+ const headers = {};
311
+ if (cfg.apiKey)
312
+ headers.authorization = `Bearer ${cfg.apiKey}`;
313
+ const res = await fetch(`${base}/models`, {
314
+ headers,
315
+ signal: AbortSignal.timeout(15000),
316
+ });
317
+ if (!res.ok)
318
+ throw new ApiError(res.status, await res.text().catch(() => ""));
319
+ const data = await res.json();
320
+ return (data.data || []).map((m) => m.id).filter(Boolean);
321
+ }