@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.
- package/openapi.json +399 -14
- package/package.json +3 -1
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +1 -1
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/migrations/066_scripts_args_json_schema.sql +1 -0
- package/src/be/scripts/db.ts +417 -0
- package/src/be/scripts/embeddings.ts +233 -0
- package/src/be/scripts/extract-schema.ts +55 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +199 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +2 -1
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +153 -20
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/index.ts +40 -29
- package/src/http/memory.ts +28 -0
- package/src/http/openapi.ts +1 -0
- package/src/http/page-proxy.ts +2 -1
- package/src/http/route-def.ts +1 -0
- package/src/http/schedules.ts +37 -0
- package/src/http/scripts.ts +388 -0
- package/src/linear/outbound.ts +9 -2
- package/src/otel.ts +5 -0
- package/src/providers/claude-adapter.ts +23 -1
- package/src/providers/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +63 -0
- package/src/scripts-runtime/executors/native.ts +232 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -0
- package/src/scripts-runtime/extract-args-schema.ts +69 -0
- package/src/scripts-runtime/extract-signature.ts +81 -0
- package/src/scripts-runtime/import-allowlist.ts +109 -0
- package/src/scripts-runtime/loader.ts +96 -0
- package/src/scripts-runtime/redacted.ts +48 -0
- package/src/scripts-runtime/sdk-allowlist.ts +29 -0
- package/src/scripts-runtime/stdlib/fetch.ts +46 -0
- package/src/scripts-runtime/stdlib/glob.ts +8 -0
- package/src/scripts-runtime/stdlib/grep.ts +34 -0
- package/src/scripts-runtime/stdlib/index.ts +16 -0
- package/src/scripts-runtime/stdlib/table.ts +17 -0
- package/src/scripts-runtime/swarm-config.ts +35 -0
- package/src/scripts-runtime/swarm-sdk.ts +197 -0
- package/src/scripts-runtime/types/stdlib.d.ts +104 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
- package/src/server.ts +12 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/error-tracker.test.ts +44 -0
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/rate-limit-event.test.ts +292 -0
- package/src/tests/redacted.test.ts +29 -0
- package/src/tests/runner-tool-spans.test.ts +268 -0
- package/src/tests/script-executor-conformance.test.ts +142 -0
- package/src/tests/script-executor-registry.test.ts +17 -0
- package/src/tests/scripts-db.test.ts +329 -0
- package/src/tests/scripts-embeddings.test.ts +291 -0
- package/src/tests/scripts-extract-signature.test.ts +47 -0
- package/src/tests/scripts-http.test.ts +403 -0
- package/src/tests/scripts-import-allowlist.test.ts +55 -0
- package/src/tests/scripts-mcp-e2e.test.ts +269 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
- package/src/tests/scripts-runtime.test.ts +344 -0
- package/src/tests/sdk-allowlist.test.ts +59 -0
- package/src/tests/secret-scrubber.test.ts +35 -1
- package/src/tests/swarm-config.test.ts +38 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/tool-call-progress.test.ts +30 -0
- package/src/tests/workflow-e2e.test.ts +218 -0
- package/src/tests/workflow-executors.test.ts +32 -2
- package/src/tests/workflow-input-redaction.test.ts +232 -0
- package/src/tests/workflow-swarm-script.test.ts +273 -0
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/script-common.ts +88 -0
- package/src/tools/script-delete.ts +35 -0
- package/src/tools/script-query-types.ts +37 -0
- package/src/tools/script-run.ts +43 -0
- package/src/tools/script-search.ts +32 -0
- package/src/tools/script-upsert.ts +43 -0
- package/src/tools/tool-config.ts +7 -0
- package/src/types.ts +61 -1
- package/src/utils/api-key.ts +28 -0
- package/src/utils/error-tracker.ts +58 -0
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +22 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- 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
|
+
}
|
package/src/linear/outbound.ts
CHANGED
|
@@ -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
|
-
//
|
|
60
|
-
|
|
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<{
|
|
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) {
|
package/src/providers/types.ts
CHANGED
|
@@ -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
|
+
}
|