@desplega.ai/agent-swarm 1.80.0 → 1.80.2

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 (100) hide show
  1. package/openapi.json +399 -14
  2. package/package.json +3 -1
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/db.ts +1 -1
  5. package/src/be/migrations/064_scripts.sql +39 -0
  6. package/src/be/migrations/065_script_embeddings.sql +7 -0
  7. package/src/be/migrations/066_scripts_args_json_schema.sql +1 -0
  8. package/src/be/scripts/db.ts +417 -0
  9. package/src/be/scripts/embeddings.ts +233 -0
  10. package/src/be/scripts/extract-schema.ts +55 -0
  11. package/src/be/scripts/maintenance.ts +9 -0
  12. package/src/be/scripts/typecheck.ts +199 -0
  13. package/src/cli.tsx +22 -5
  14. package/src/commands/artifact.ts +3 -2
  15. package/src/commands/claude-managed-setup.ts +2 -1
  16. package/src/commands/codex-login.ts +5 -3
  17. package/src/commands/onboard.tsx +2 -1
  18. package/src/commands/runner.ts +153 -20
  19. package/src/commands/setup.tsx +5 -3
  20. package/src/hooks/hook.ts +4 -3
  21. package/src/http/index.ts +40 -29
  22. package/src/http/memory.ts +28 -0
  23. package/src/http/openapi.ts +1 -0
  24. package/src/http/page-proxy.ts +2 -1
  25. package/src/http/route-def.ts +1 -0
  26. package/src/http/schedules.ts +37 -0
  27. package/src/http/scripts.ts +388 -0
  28. package/src/linear/outbound.ts +9 -2
  29. package/src/otel.ts +5 -0
  30. package/src/providers/claude-adapter.ts +23 -1
  31. package/src/providers/types.ts +8 -0
  32. package/src/scripts-runtime/ctx.ts +23 -0
  33. package/src/scripts-runtime/eval-harness.ts +63 -0
  34. package/src/scripts-runtime/executors/native.ts +232 -0
  35. package/src/scripts-runtime/executors/registry.ts +16 -0
  36. package/src/scripts-runtime/executors/types.ts +63 -0
  37. package/src/scripts-runtime/extract-args-schema.ts +69 -0
  38. package/src/scripts-runtime/extract-signature.ts +81 -0
  39. package/src/scripts-runtime/import-allowlist.ts +109 -0
  40. package/src/scripts-runtime/loader.ts +96 -0
  41. package/src/scripts-runtime/redacted.ts +48 -0
  42. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  43. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  44. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  45. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  46. package/src/scripts-runtime/stdlib/index.ts +16 -0
  47. package/src/scripts-runtime/stdlib/table.ts +17 -0
  48. package/src/scripts-runtime/swarm-config.ts +35 -0
  49. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  50. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  51. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  52. package/src/server.ts +12 -0
  53. package/src/tests/api-key.test.ts +33 -0
  54. package/src/tests/codex-login.test.ts +1 -1
  55. package/src/tests/error-tracker.test.ts +44 -0
  56. package/src/tests/linear-outbound-sync.test.ts +109 -0
  57. package/src/tests/mcp-tools.test.ts +69 -0
  58. package/src/tests/rate-limit-event.test.ts +292 -0
  59. package/src/tests/redacted.test.ts +29 -0
  60. package/src/tests/runner-tool-spans.test.ts +268 -0
  61. package/src/tests/script-executor-conformance.test.ts +142 -0
  62. package/src/tests/script-executor-registry.test.ts +17 -0
  63. package/src/tests/scripts-db.test.ts +329 -0
  64. package/src/tests/scripts-embeddings.test.ts +291 -0
  65. package/src/tests/scripts-extract-signature.test.ts +47 -0
  66. package/src/tests/scripts-http.test.ts +403 -0
  67. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  68. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  69. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  70. package/src/tests/scripts-runtime.test.ts +344 -0
  71. package/src/tests/sdk-allowlist.test.ts +59 -0
  72. package/src/tests/secret-scrubber.test.ts +35 -1
  73. package/src/tests/swarm-config.test.ts +38 -0
  74. package/src/tests/tool-annotations.test.ts +2 -2
  75. package/src/tests/tool-call-progress.test.ts +30 -0
  76. package/src/tests/workflow-e2e.test.ts +218 -0
  77. package/src/tests/workflow-executors.test.ts +32 -2
  78. package/src/tests/workflow-input-redaction.test.ts +232 -0
  79. package/src/tests/workflow-swarm-script.test.ts +273 -0
  80. package/src/tools/memory-rate.ts +2 -1
  81. package/src/tools/script-common.ts +88 -0
  82. package/src/tools/script-delete.ts +35 -0
  83. package/src/tools/script-query-types.ts +37 -0
  84. package/src/tools/script-run.ts +43 -0
  85. package/src/tools/script-search.ts +32 -0
  86. package/src/tools/script-upsert.ts +43 -0
  87. package/src/tools/tool-config.ts +7 -0
  88. package/src/types.ts +61 -1
  89. package/src/utils/api-key.ts +28 -0
  90. package/src/utils/error-tracker.ts +58 -0
  91. package/src/utils/page-session.ts +8 -6
  92. package/src/utils/secret-scrubber.ts +22 -1
  93. package/src/workflows/engine.ts +12 -4
  94. package/src/workflows/executors/index.ts +1 -0
  95. package/src/workflows/executors/registry.ts +2 -0
  96. package/src/workflows/executors/script.ts +12 -1
  97. package/src/workflows/executors/swarm-script.ts +170 -0
  98. package/src/workflows/input.ts +65 -0
  99. package/src/workflows/recovery.ts +31 -3
  100. package/src/workflows/resume.ts +43 -5
@@ -0,0 +1,388 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { z } from "zod";
3
+ import { getAgentById } from "../be/db";
4
+ import { createEvent } from "../be/events";
5
+ import { deleteScript, getScript, upsertScriptByName } from "../be/scripts/db";
6
+ import { searchScripts } from "../be/scripts/embeddings";
7
+ import { extractArgsJsonSchema } from "../be/scripts/extract-schema";
8
+ import { SCRIPT_SDK_TYPES, SCRIPT_STDLIB_TYPES, typecheckScript } from "../be/scripts/typecheck";
9
+ import { extractScriptSignature } from "../scripts-runtime/extract-signature";
10
+ import { runScript } from "../scripts-runtime/loader";
11
+ import {
12
+ ScriptFsModeSchema,
13
+ type ScriptRecord,
14
+ type ScriptScope,
15
+ ScriptScopeSchema,
16
+ } from "../types";
17
+ import { scrubObject } from "../utils/secret-scrubber";
18
+ import { route } from "./route-def";
19
+ import { json, jsonError } from "./utils";
20
+
21
+ const scriptNameSchema = z.string().min(1).max(200);
22
+
23
+ const upsertBodySchema = z.object({
24
+ name: scriptNameSchema,
25
+ source: z.string().min(1),
26
+ description: z.string().default(""),
27
+ intent: z.string().default(""),
28
+ scope: ScriptScopeSchema.default("agent"),
29
+ fsMode: ScriptFsModeSchema.default("none"),
30
+ });
31
+
32
+ const runBodySchema = z
33
+ .object({
34
+ name: scriptNameSchema.optional(),
35
+ source: z.string().min(1).optional(),
36
+ args: z.unknown().optional(),
37
+ intent: z.string().default(""),
38
+ scope: ScriptScopeSchema.optional(),
39
+ fsMode: ScriptFsModeSchema.default("none"),
40
+ })
41
+ .refine((body) => Boolean(body.name) !== Boolean(body.source), {
42
+ message: "Provide exactly one of name or source",
43
+ });
44
+
45
+ const searchBodySchema = z.object({
46
+ query: z.string().default(""),
47
+ scope: ScriptScopeSchema.optional(),
48
+ limit: z.number().int().min(1).max(100).default(10),
49
+ });
50
+
51
+ const nameParamsSchema = z.object({ name: scriptNameSchema });
52
+ const scopeQuerySchema = z.object({ scope: ScriptScopeSchema.default("agent") });
53
+ const optionalScopeQuerySchema = z.object({ scope: ScriptScopeSchema.optional() });
54
+
55
+ const upsertRoute = route({
56
+ method: "post",
57
+ path: "/api/scripts/upsert",
58
+ pattern: ["api", "scripts", "upsert"],
59
+ operationId: "scripts_upsert",
60
+ summary: "Create or update a reusable script",
61
+ description: "Explicit script upserts run a TypeScript typecheck before writing.",
62
+ tags: ["Scripts"],
63
+ body: upsertBodySchema,
64
+ responses: {
65
+ 200: { description: "Script upserted" },
66
+ 400: { description: "Validation or typecheck failure" },
67
+ 403: { description: "Global write requires lead agent" },
68
+ },
69
+ });
70
+
71
+ const runRoute = route({
72
+ method: "post",
73
+ path: "/api/scripts/run",
74
+ pattern: ["api", "scripts", "run"],
75
+ operationId: "scripts_run",
76
+ summary: "Run a reusable or inline script",
77
+ description:
78
+ "Inline source skips typecheck and is auto-saved as a scratch script only on success.",
79
+ tags: ["Scripts"],
80
+ body: runBodySchema,
81
+ responses: {
82
+ 200: { description: "Script run completed" },
83
+ 400: { description: "Validation error" },
84
+ 404: { description: "Script not found" },
85
+ 501: { description: "workspace-rw scripts are not supported in v1" },
86
+ },
87
+ });
88
+
89
+ const searchRoute = route({
90
+ method: "post",
91
+ path: "/api/scripts/search",
92
+ pattern: ["api", "scripts", "search"],
93
+ operationId: "scripts_search",
94
+ summary: "Search reusable scripts",
95
+ description: "Phase 3 search is substring-only over script name and metadata.",
96
+ tags: ["Scripts"],
97
+ body: searchBodySchema,
98
+ responses: {
99
+ 200: { description: "Matching scripts" },
100
+ 400: { description: "Validation error" },
101
+ },
102
+ });
103
+
104
+ const deleteRoute = route({
105
+ method: "delete",
106
+ path: "/api/scripts/{name}",
107
+ pattern: ["api", "scripts", null],
108
+ operationId: "scripts_delete",
109
+ summary: "Delete a reusable script",
110
+ tags: ["Scripts"],
111
+ params: nameParamsSchema,
112
+ query: scopeQuerySchema,
113
+ responses: {
114
+ 200: { description: "Delete result" },
115
+ 400: { description: "Validation error" },
116
+ 403: { description: "Global delete requires lead agent" },
117
+ },
118
+ });
119
+
120
+ const typesRoute = route({
121
+ method: "get",
122
+ path: "/api/scripts/{name}/types",
123
+ pattern: ["api", "scripts", null, "types"],
124
+ operationId: "scripts_types",
125
+ summary: "Get script signature and authoring types",
126
+ tags: ["Scripts"],
127
+ params: nameParamsSchema,
128
+ query: optionalScopeQuerySchema,
129
+ responses: {
130
+ 200: { description: "Script signature and type blobs" },
131
+ 404: { description: "Script not found" },
132
+ },
133
+ });
134
+
135
+ function requireAgent(res: ServerResponse, agentId: string | undefined) {
136
+ if (!agentId) {
137
+ jsonError(res, "X-Agent-ID required for scripts API", 400);
138
+ return null;
139
+ }
140
+ const agent = getAgentById(agentId);
141
+ if (!agent) {
142
+ jsonError(res, "Agent not found", 404);
143
+ return null;
144
+ }
145
+ return agent;
146
+ }
147
+
148
+ function signatureJsonFor(source: string): string {
149
+ return JSON.stringify(extractScriptSignature(source));
150
+ }
151
+
152
+ function resolveScript(name: string, agentId: string, scope?: ScriptScope): ScriptRecord | null {
153
+ if (scope === "global") return getScript({ name, scope: "global" });
154
+ if (scope === "agent") return getScript({ name, scope: "agent", scopeId: agentId });
155
+ return (
156
+ getScript({ name, scope: "agent", scopeId: agentId }) ?? getScript({ name, scope: "global" })
157
+ );
158
+ }
159
+
160
+ function scratchSlug(intent: string, source: string): string {
161
+ const base = (intent || "inline-script")
162
+ .toLowerCase()
163
+ .replace(/[^a-z0-9]+/g, "-")
164
+ .replace(/^-+|-+$/g, "")
165
+ .slice(0, 48);
166
+ const hash = new Bun.CryptoHasher("sha256").update(source).digest("hex").slice(0, 8);
167
+ return `scratch-${base || "inline-script"}-${hash}`;
168
+ }
169
+
170
+ function emitGlobalUpsertEvent(args: {
171
+ agentId: string;
172
+ script: ScriptRecord;
173
+ isNew: boolean;
174
+ isPromotion: boolean;
175
+ }) {
176
+ createEvent({
177
+ category: "system",
178
+ event: "script.global_upsert",
179
+ source: "api",
180
+ agentId: args.agentId,
181
+ data: {
182
+ scriptId: args.script.id,
183
+ name: args.script.name,
184
+ version: args.script.version,
185
+ contentHash: args.script.contentHash,
186
+ changedByAgentId: args.agentId,
187
+ isNew: args.isNew,
188
+ isPromotion: args.isPromotion,
189
+ },
190
+ });
191
+ }
192
+
193
+ export async function handleScripts(
194
+ req: IncomingMessage,
195
+ res: ServerResponse,
196
+ pathSegments: string[],
197
+ queryParams: URLSearchParams,
198
+ agentId: string | undefined,
199
+ ): Promise<boolean> {
200
+ if (upsertRoute.match(req.method, pathSegments)) {
201
+ const parsed = await upsertRoute.parse(req, res, pathSegments, queryParams);
202
+ if (!parsed) return true;
203
+ const agent = requireAgent(res, agentId);
204
+ if (!agent) return true;
205
+
206
+ if (parsed.body.scope === "global" && !agent.isLead) {
207
+ jsonError(res, "Global scripts require a lead agent", 403);
208
+ return true;
209
+ }
210
+
211
+ const typecheck = typecheckScript(parsed.body.source);
212
+ if (!typecheck.ok) {
213
+ json(res, { error: "typecheck_failed", diagnostics: typecheck.diagnostics }, 400);
214
+ return true;
215
+ }
216
+
217
+ const existingAgentScript =
218
+ parsed.body.scope === "global"
219
+ ? getScript({ name: parsed.body.name, scope: "agent", scopeId: agent.id })
220
+ : null;
221
+ const argsJsonSchema = await extractArgsJsonSchema(parsed.body.source);
222
+ const result = await upsertScriptByName({
223
+ name: parsed.body.name,
224
+ scope: parsed.body.scope,
225
+ scopeId: parsed.body.scope === "agent" ? agent.id : null,
226
+ source: parsed.body.source,
227
+ description: parsed.body.description,
228
+ intent: parsed.body.intent,
229
+ signatureJson: signatureJsonFor(parsed.body.source),
230
+ argsJsonSchema,
231
+ fsMode: parsed.body.fsMode,
232
+ agentId: agent.id,
233
+ isScratch: false,
234
+ typeChecked: true,
235
+ });
236
+
237
+ if (parsed.body.scope === "global" && !result.contentDeduped) {
238
+ emitGlobalUpsertEvent({
239
+ agentId: agent.id,
240
+ script: result.script,
241
+ isNew: result.isNew,
242
+ isPromotion: Boolean(existingAgentScript),
243
+ });
244
+ }
245
+
246
+ json(res, {
247
+ name: result.script.name,
248
+ version: result.script.version,
249
+ contentDeduped: result.contentDeduped,
250
+ });
251
+ return true;
252
+ }
253
+
254
+ if (runRoute.match(req.method, pathSegments)) {
255
+ const parsed = await runRoute.parse(req, res, pathSegments, queryParams);
256
+ if (!parsed) return true;
257
+ const agent = requireAgent(res, agentId);
258
+ if (!agent) return true;
259
+
260
+ let source = parsed.body.source;
261
+ let fsMode = parsed.body.fsMode;
262
+ if (parsed.body.name) {
263
+ const script = resolveScript(parsed.body.name, agent.id, parsed.body.scope);
264
+ if (!script) {
265
+ jsonError(res, "Script not found", 404);
266
+ return true;
267
+ }
268
+ source = script.source;
269
+ fsMode = script.fsMode;
270
+ }
271
+
272
+ if (fsMode === "workspace-rw") {
273
+ jsonError(res, "workspace-rw scripts are not supported by /api/scripts/run in v1", 501);
274
+ return true;
275
+ }
276
+
277
+ const output = await runScript({
278
+ source: source as string,
279
+ args: parsed.body.args,
280
+ fsMode,
281
+ agentId: agent.id,
282
+ });
283
+
284
+ let autoSaved: { slug: string; reason: string } | undefined;
285
+ if (parsed.body.source && !output.error && output.exitCode === 0) {
286
+ const slug = scratchSlug(parsed.body.intent, parsed.body.source);
287
+ await upsertScriptByName({
288
+ name: slug,
289
+ scope: "agent",
290
+ scopeId: agent.id,
291
+ source: parsed.body.source,
292
+ description: `Scratch script: ${parsed.body.intent || slug}`,
293
+ intent: parsed.body.intent || "Inline script auto-saved after successful run",
294
+ signatureJson: signatureJsonFor(parsed.body.source),
295
+ fsMode: "none",
296
+ agentId: agent.id,
297
+ isScratch: true,
298
+ typeChecked: false,
299
+ changeReason: "Auto-saved successful inline run",
300
+ });
301
+ autoSaved = { slug, reason: "successful_inline_run" };
302
+ }
303
+
304
+ json(
305
+ res,
306
+ scrubObject({
307
+ result: output.result,
308
+ autoSaved,
309
+ truncated: output.truncated,
310
+ durationMs: output.durationMs,
311
+ stdout: output.stdout,
312
+ stderr: output.stderr,
313
+ exitCode: output.exitCode,
314
+ error: output.error,
315
+ }),
316
+ );
317
+ return true;
318
+ }
319
+
320
+ if (searchRoute.match(req.method, pathSegments)) {
321
+ const parsed = await searchRoute.parse(req, res, pathSegments, queryParams);
322
+ if (!parsed) return true;
323
+ const agent = requireAgent(res, agentId);
324
+ if (!agent) return true;
325
+
326
+ const matches = await searchScripts({
327
+ query: parsed.body.query,
328
+ scope: parsed.body.scope,
329
+ scopeId: agent.id,
330
+ limit: parsed.body.limit,
331
+ });
332
+
333
+ json(res, {
334
+ results: matches.map(({ script, score }) => ({
335
+ name: script.name,
336
+ signature: JSON.parse(script.signatureJson),
337
+ argsJsonSchema: script.argsJsonSchema
338
+ ? (JSON.parse(script.argsJsonSchema) as unknown)
339
+ : null,
340
+ description: script.description,
341
+ score,
342
+ })),
343
+ });
344
+ return true;
345
+ }
346
+
347
+ if (typesRoute.match(req.method, pathSegments)) {
348
+ const parsed = await typesRoute.parse(req, res, pathSegments, queryParams);
349
+ if (!parsed) return true;
350
+ const agent = requireAgent(res, agentId);
351
+ if (!agent) return true;
352
+
353
+ const script = resolveScript(parsed.params.name, agent.id, parsed.query.scope);
354
+ if (!script) {
355
+ jsonError(res, "Script not found", 404);
356
+ return true;
357
+ }
358
+ json(res, {
359
+ signature: JSON.parse(script.signatureJson),
360
+ argsJsonSchema: script.argsJsonSchema ? (JSON.parse(script.argsJsonSchema) as unknown) : null,
361
+ sdkTypes: SCRIPT_SDK_TYPES,
362
+ stdlibTypes: SCRIPT_STDLIB_TYPES,
363
+ });
364
+ return true;
365
+ }
366
+
367
+ if (deleteRoute.match(req.method, pathSegments)) {
368
+ const parsed = await deleteRoute.parse(req, res, pathSegments, queryParams);
369
+ if (!parsed) return true;
370
+ const agent = requireAgent(res, agentId);
371
+ if (!agent) return true;
372
+
373
+ if (parsed.query.scope === "global" && !agent.isLead) {
374
+ jsonError(res, "Global scripts require a lead agent", 403);
375
+ return true;
376
+ }
377
+
378
+ const deleted = deleteScript({
379
+ name: parsed.params.name,
380
+ scope: parsed.query.scope,
381
+ scopeId: parsed.query.scope === "agent" ? agent.id : null,
382
+ });
383
+ json(res, { deleted });
384
+ return true;
385
+ }
386
+
387
+ return false;
388
+ }
@@ -49,6 +49,10 @@ async function handleTaskCreated(data: unknown): Promise<void> {
49
49
  );
50
50
  }
51
51
 
52
+ // Cap parameter length to avoid oversized Linear GraphQL payloads. Linear renders this in the
53
+ // AgentSession panel; 2000 chars is plenty for a progress update.
54
+ const PROGRESS_PARAMETER_MAX = 2000;
55
+
52
56
  async function handleTaskProgress(data: unknown): Promise<void> {
53
57
  const { taskId, progress } = data as { taskId: string; progress?: string };
54
58
  if (!taskId || !progress) return;
@@ -56,8 +60,11 @@ async function handleTaskProgress(data: unknown): Promise<void> {
56
60
  const sessionId = taskSessionMap.get(taskId);
57
61
  if (!sessionId) return;
58
62
 
59
- // Use 'action' activity type — Linear renders it as a structured tool invocation card
60
- postAgentSessionAction(sessionId, progress).catch((err) => {
63
+ // Post as `action` activity (renders as a structured card in Linear's AgentSession panel).
64
+ // Per Linear's agentActivityCreate spec, `action` requires BOTH `action` AND `parameter`;
65
+ // the original bug here was passing `progress` as `action` with `parameter` undefined.
66
+ const parameter = progress.slice(0, PROGRESS_PARAMETER_MAX);
67
+ postAgentSessionAction(sessionId, "Progress update", parameter).catch((err) => {
61
68
  console.error(`[Linear Outbound] Failed to post progress action for task ${taskId}:`, err);
62
69
  });
63
70
  }
package/src/otel.ts CHANGED
@@ -48,6 +48,11 @@ export function isOtelEnabled(): boolean {
48
48
  return enabled;
49
49
  }
50
50
 
51
+ export function isPollTracingEnabled(): boolean {
52
+ const v = (process.env.OTEL_TRACE_POLL ?? "").toLowerCase();
53
+ return v === "1" || v === "true" || v === "yes" || v === "on";
54
+ }
55
+
51
56
  export async function initOtel(serviceRole = process.env.AGENT_ROLE || "api"): Promise<void> {
52
57
  if (!enabled || initialized) return;
53
58
  initialized = true;
@@ -458,6 +458,7 @@ class ClaudeSession implements ProviderSession {
458
458
  cost: lastCost,
459
459
  isError: (exitCode ?? 1) !== 0,
460
460
  failureReason,
461
+ rateLimitResetAt: this.errorTracker.getRateLimitResetAt(),
461
462
  };
462
463
  }
463
464
 
@@ -541,8 +542,29 @@ class ClaudeSession implements ProviderSession {
541
542
  // Tool use from assistant messages — emit tool_start for auto-progress
542
543
  if (json.type === "assistant" && json.message) {
543
544
  const message = json.message as {
544
- content?: Array<{ type: string; name?: string; id?: string; input?: unknown }>;
545
+ content?: Array<{
546
+ type: string;
547
+ name?: string;
548
+ id?: string;
549
+ input?: unknown;
550
+ text?: string;
551
+ }>;
545
552
  };
553
+
554
+ // Emit a `message` event BEFORE any tool_start events for this turn.
555
+ // The runner uses this as an "assistant turn boundary" to implicit-close
556
+ // any worker.tool spans left open by the previous turn (the Claude CLI
557
+ // doesn't emit per-tool completion events for harness-side tools like
558
+ // Bash/Read/Edit, so without this boundary their spans would stay open
559
+ // until session shutdown and report inflated duration_ms).
560
+ const text = Array.isArray(message.content)
561
+ ? message.content
562
+ .filter((b) => b.type === "text" && typeof b.text === "string")
563
+ .map((b) => b.text as string)
564
+ .join("")
565
+ : "";
566
+ this.emit({ type: "message", role: "assistant", content: text });
567
+
546
568
  if (message.content) {
547
569
  for (const block of message.content) {
548
570
  if (block.type === "tool_use" && block.name) {
@@ -116,6 +116,14 @@ export interface ProviderResult {
116
116
  errorCategory?: string;
117
117
  /** Human-readable failure reason built from error tracking. */
118
118
  failureReason?: string;
119
+ /**
120
+ * ISO timestamp of the rate limit reset time, parsed from a structured
121
+ * `rate_limit_event` line in the Claude CLI stream. Only set by the Claude
122
+ * adapter when a `status: "rejected"` event is present. Already clamped to
123
+ * [now+60s, now+6h] at the source. The runner uses this as tier-1 of the
124
+ * three-tier cooldown resolver.
125
+ */
126
+ rateLimitResetAt?: string;
119
127
  }
120
128
 
121
129
  /** Behavioral traits that govern prompt assembly and feature gating. */
@@ -0,0 +1,23 @@
1
+ import { stdlib } from "./stdlib";
2
+ import type { SwarmConfig } from "./swarm-config";
3
+ import { createSwarmSdk } from "./swarm-sdk";
4
+
5
+ export type RuntimeCtx = {
6
+ swarm: Record<string, unknown> & { config: SwarmConfig };
7
+ stdlib: typeof stdlib;
8
+ logger: {
9
+ log: (...args: unknown[]) => void;
10
+ warn: (...args: unknown[]) => void;
11
+ error: (...args: unknown[]) => void;
12
+ };
13
+ };
14
+
15
+ export function buildCtx({ swarmConfig }: { swarmConfig: SwarmConfig }): RuntimeCtx {
16
+ const swarm = createSwarmSdk(swarmConfig) as Record<string, unknown> & { config: SwarmConfig };
17
+ swarm.config = swarmConfig;
18
+ return {
19
+ swarm,
20
+ stdlib,
21
+ logger: console,
22
+ };
23
+ }
@@ -0,0 +1,63 @@
1
+ import { buildCtx } from "./ctx";
2
+ import type { SwarmConfigPayload } from "./executors/types";
3
+ import { SwarmConfig } from "./swarm-config";
4
+
5
+ function requiredEnv(name: string): string {
6
+ const value = process.env[name];
7
+ if (!value) throw new Error(`Missing required env ${name}`);
8
+ return value;
9
+ }
10
+
11
+ try {
12
+ const stdin = await Bun.stdin.text();
13
+ if (!stdin.trim()) {
14
+ console.error("Swarm script config payload was empty");
15
+ process.exit(2);
16
+ }
17
+
18
+ const payload = JSON.parse(stdin) as SwarmConfigPayload;
19
+ const swarmConfig = new SwarmConfig(payload);
20
+ const rawArgs = JSON.parse(await Bun.file(requiredEnv("SWARM_SCRIPT_ARGS_FILE")).text());
21
+ // Accept both shapes: callers may pass an already-serialized JSON string.
22
+ const parsedArgs = typeof rawArgs === "string" ? JSON.parse(rawArgs) : rawArgs;
23
+ const ctx = buildCtx({ swarmConfig });
24
+
25
+ const sourceText = await Bun.file(requiredEnv("SWARM_SCRIPT_SOURCE_FILE")).text();
26
+ const userModulePath = `${requiredEnv("SWARM_SCRIPT_TMPDIR")}/user-script.ts`;
27
+ await Bun.write(userModulePath, sourceText);
28
+
29
+ const mod = await import(userModulePath);
30
+ if (typeof mod.default !== "function") {
31
+ throw new Error("Swarm script must export a default function");
32
+ }
33
+
34
+ let validatedArgs = parsedArgs;
35
+ if (mod.argsSchema && typeof mod.argsSchema === "object" && "parse" in mod.argsSchema) {
36
+ try {
37
+ // biome-ignore lint/suspicious/noExplicitAny: argsSchema is a Zod schema at runtime
38
+ validatedArgs = (mod.argsSchema as any).parse(parsedArgs);
39
+ } catch (err) {
40
+ // Format ZodError issues into a readable message
41
+ if (
42
+ err &&
43
+ typeof err === "object" &&
44
+ "issues" in err &&
45
+ Array.isArray((err as { issues: unknown[] }).issues)
46
+ ) {
47
+ const issues = (
48
+ err as { issues: Array<{ path: (string | number)[]; message: string }> }
49
+ ).issues
50
+ .map((i) => ` ${i.path.length ? i.path.join(".") : "(root)"}: ${i.message}`)
51
+ .join("\n");
52
+ throw new Error(`argsSchema validation failed:\n${issues}`);
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+
58
+ const result = await mod.default(validatedArgs, ctx);
59
+ await Bun.write(requiredEnv("SWARM_SCRIPT_RESULT_FILE"), JSON.stringify(result ?? null));
60
+ } catch (error) {
61
+ console.error(error instanceof Error ? error.stack || error.message : String(error));
62
+ process.exit(1);
63
+ }