@desplega.ai/agent-swarm 1.80.0 → 1.80.1
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/scripts/db.ts +391 -0
- package/src/be/scripts/embeddings.ts +231 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +193 -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 +72 -10
- 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 +381 -0
- package/src/linear/outbound.ts +9 -2
- package/src/otel.ts +5 -0
- package/src/providers/claude-adapter.ts +22 -1
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +39 -0
- package/src/scripts-runtime/executors/native.ts +229 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -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/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -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 +350 -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 +289 -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 +60 -1
- package/src/utils/api-key.ts +28 -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
package/src/http/index.ts
CHANGED
|
@@ -14,9 +14,10 @@ import { initGitLab } from "../gitlab";
|
|
|
14
14
|
import { stopHeartbeat } from "../heartbeat";
|
|
15
15
|
import { initJira } from "../jira";
|
|
16
16
|
import { initLinear } from "../linear";
|
|
17
|
-
import { initOtel, startSpan, withRemoteContext } from "../otel";
|
|
17
|
+
import { initOtel, isPollTracingEnabled, startSpan, withRemoteContext } from "../otel";
|
|
18
18
|
import { startSlackApp, stopSlackApp } from "../slack";
|
|
19
19
|
import { initTelemetry, telemetry } from "../telemetry";
|
|
20
|
+
import { getApiKey } from "../utils/api-key";
|
|
20
21
|
import { initWorkflows } from "../workflows";
|
|
21
22
|
import { handleActiveSessions } from "./active-sessions";
|
|
22
23
|
import { handleAgentRegister, handleAgentsRest } from "./agents";
|
|
@@ -45,6 +46,7 @@ import { handlePricing } from "./pricing";
|
|
|
45
46
|
import { handlePromptTemplates } from "./prompt-templates";
|
|
46
47
|
import { handleRepos } from "./repos";
|
|
47
48
|
import { handleSchedules } from "./schedules";
|
|
49
|
+
import { handleScripts } from "./scripts";
|
|
48
50
|
import { handleSessionData } from "./session-data";
|
|
49
51
|
import { handleSessions } from "./sessions";
|
|
50
52
|
import { handleSkills } from "./skills";
|
|
@@ -69,7 +71,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
const port = parseInt(process.env.PORT || process.argv[2] || "3013", 10);
|
|
72
|
-
const apiKey =
|
|
74
|
+
const apiKey = getApiKey();
|
|
73
75
|
|
|
74
76
|
// Use globalThis to persist state across hot reloads
|
|
75
77
|
const globalState = globalThis as typeof globalThis & {
|
|
@@ -116,33 +118,39 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
await withRemoteContext(req.headers as Record<string, unknown>, async () => {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
121
|
+
const reqPath = req.url?.split("?")[0] ?? "";
|
|
122
|
+
const skipSpan = reqPath === "/api/poll" && !isPollTracingEnabled();
|
|
123
|
+
const span = skipSpan
|
|
124
|
+
? null
|
|
125
|
+
: startSpan("http.server", {
|
|
126
|
+
"http.request.method": req.method ?? "",
|
|
127
|
+
"url.path": reqPath,
|
|
128
|
+
"agent.id": req.headers["x-agent-id"] as string | undefined,
|
|
129
|
+
"agentswarm.component": "api",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (span) {
|
|
133
|
+
res.on("finish", () => {
|
|
134
|
+
if (spanEnded) return;
|
|
135
|
+
spanEnded = true;
|
|
136
|
+
span.setAttributes({
|
|
137
|
+
"http.response.status_code": statusCode,
|
|
138
|
+
"agentswarm.http.duration_ms": Math.round((performance.now() - startTime) * 10) / 10,
|
|
139
|
+
});
|
|
140
|
+
if (statusCode >= 500) {
|
|
141
|
+
span.setStatus({ code: 2, message: `HTTP ${statusCode}` });
|
|
142
|
+
}
|
|
143
|
+
span.end();
|
|
132
144
|
});
|
|
133
|
-
if (statusCode >= 500) {
|
|
134
|
-
span.setStatus({ code: 2, message: `HTTP ${statusCode}` });
|
|
135
|
-
}
|
|
136
|
-
span.end();
|
|
137
|
-
});
|
|
138
145
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
res.on("error", (err) => {
|
|
147
|
+
if (spanEnded) return;
|
|
148
|
+
spanEnded = true;
|
|
149
|
+
span.recordException(err);
|
|
150
|
+
span.setStatus({ code: 2, message: err.message });
|
|
151
|
+
span.end();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
146
154
|
|
|
147
155
|
setCorsHeaders(req, res);
|
|
148
156
|
|
|
@@ -180,6 +188,7 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
180
188
|
() => handleDbQuery(req, res, pathSegments, queryParams),
|
|
181
189
|
() => handleRepos(req, res, pathSegments, queryParams),
|
|
182
190
|
() => handleSkills(req, res, pathSegments, queryParams, myAgentId),
|
|
191
|
+
() => handleScripts(req, res, pathSegments, queryParams, myAgentId),
|
|
183
192
|
() => handleMcpServers(req, res, pathSegments, queryParams),
|
|
184
193
|
() => handleMcpOAuth(req, res, pathSegments, queryParams),
|
|
185
194
|
() => handleMemory(req, res, pathSegments, myAgentId),
|
|
@@ -205,8 +214,10 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
205
214
|
res.writeHead(404);
|
|
206
215
|
res.end("Not Found");
|
|
207
216
|
} catch (err) {
|
|
208
|
-
span
|
|
209
|
-
|
|
217
|
+
if (span) {
|
|
218
|
+
span.recordException(err);
|
|
219
|
+
span.setStatus({ code: 2, message: err instanceof Error ? err.message : String(err) });
|
|
220
|
+
}
|
|
210
221
|
const message = err instanceof Error ? err.message : String(err);
|
|
211
222
|
console.error(`[HTTP] ❌ ${req.method} ${req.url} → ${message}`);
|
|
212
223
|
if (!res.headersSent) {
|
package/src/http/memory.ts
CHANGED
|
@@ -124,6 +124,20 @@ const deleteMemoryById = route({
|
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
const getMemoryById = route({
|
|
128
|
+
method: "get",
|
|
129
|
+
path: "/api/memory/{id}",
|
|
130
|
+
pattern: ["api", "memory", null],
|
|
131
|
+
summary: "Get a single memory by ID",
|
|
132
|
+
tags: ["Memory"],
|
|
133
|
+
auth: { apiKey: true, agentId: true },
|
|
134
|
+
params: z.object({ id: z.string().uuid() }),
|
|
135
|
+
responses: {
|
|
136
|
+
200: { description: "Memory details" },
|
|
137
|
+
404: { description: "Memory not found" },
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
127
141
|
// Memory rater v1.5 — worker-facing rating endpoints. Plan:
|
|
128
142
|
// thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-3.md
|
|
129
143
|
//
|
|
@@ -618,5 +632,19 @@ export async function handleMemory(
|
|
|
618
632
|
return true;
|
|
619
633
|
}
|
|
620
634
|
|
|
635
|
+
if (getMemoryById.match(req.method, pathSegments)) {
|
|
636
|
+
const parsed = await getMemoryById.parse(req, res, pathSegments, new URLSearchParams());
|
|
637
|
+
if (!parsed) return true;
|
|
638
|
+
|
|
639
|
+
const memory = getMemoryStore().get(parsed.params.id);
|
|
640
|
+
if (!memory) {
|
|
641
|
+
jsonError(res, "Memory not found", 404);
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
json(res, { memory });
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
|
|
621
649
|
return false;
|
|
622
650
|
}
|
package/src/http/openapi.ts
CHANGED
|
@@ -64,6 +64,7 @@ export function generateOpenApiSpec(opts: OpenApiOptions): string {
|
|
|
64
64
|
registry.registerPath({
|
|
65
65
|
method: routeDef.method,
|
|
66
66
|
path: routeDef.path,
|
|
67
|
+
operationId: routeDef.operationId,
|
|
67
68
|
summary: routeDef.summary,
|
|
68
69
|
description: routeDef.description,
|
|
69
70
|
tags: routeDef.tags,
|
package/src/http/page-proxy.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { getPage } from "../be/db";
|
|
3
|
+
import { getApiKey } from "../utils/api-key";
|
|
3
4
|
import { extractAndVerifyCookie } from "../utils/page-session";
|
|
4
5
|
import { route } from "./route-def";
|
|
5
6
|
import { jsonError } from "./utils";
|
|
@@ -147,7 +148,7 @@ export async function handlePageProxy(req: IncomingMessage, res: ServerResponse)
|
|
|
147
148
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
148
149
|
const targetUrl = `${baseUrl}${rewrittenPath}${queryPart}`;
|
|
149
150
|
|
|
150
|
-
const apiKey =
|
|
151
|
+
const apiKey = getApiKey();
|
|
151
152
|
// `X-Page-Id` is the trust anchor for page-scoped KV: only the page-proxy
|
|
152
153
|
// ever sets it (any external `X-Page-Id` header is dropped because we don't
|
|
153
154
|
// forward the original headers). The KV handler treats this as the highest-
|
package/src/http/route-def.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface RouteDef<
|
|
|
20
20
|
path: string; // OpenAPI-style: "/api/tasks/{id}"
|
|
21
21
|
pattern: readonly (string | null)[]; // matchRoute-style: ["api", "tasks", null]
|
|
22
22
|
exact?: boolean; // default true
|
|
23
|
+
operationId?: string;
|
|
23
24
|
summary: string;
|
|
24
25
|
description?: string;
|
|
25
26
|
tags: string[];
|
package/src/http/schedules.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getDb,
|
|
9
9
|
getScheduledTaskById,
|
|
10
10
|
getScheduledTaskByName,
|
|
11
|
+
getScheduledTasks,
|
|
11
12
|
updateScheduledTask,
|
|
12
13
|
} from "../be/db";
|
|
13
14
|
import { calculateNextRun } from "../scheduler/scheduler";
|
|
@@ -64,6 +65,29 @@ const runScheduleNow = route({
|
|
|
64
65
|
},
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
const listSchedules = route({
|
|
69
|
+
method: "get",
|
|
70
|
+
path: "/api/schedules",
|
|
71
|
+
pattern: ["api", "schedules"],
|
|
72
|
+
summary: "List schedules",
|
|
73
|
+
tags: ["Schedules"],
|
|
74
|
+
query: z.object({
|
|
75
|
+
enabled: z
|
|
76
|
+
.enum(["true", "false"])
|
|
77
|
+
.optional()
|
|
78
|
+
.transform((v) => (v === undefined ? undefined : v === "true")),
|
|
79
|
+
name: z.string().optional(),
|
|
80
|
+
scheduleType: z.enum(["recurring", "one_time"]).optional(),
|
|
81
|
+
hideCompleted: z
|
|
82
|
+
.enum(["true", "false"])
|
|
83
|
+
.optional()
|
|
84
|
+
.transform((v) => (v === undefined ? undefined : v === "true")),
|
|
85
|
+
}),
|
|
86
|
+
responses: {
|
|
87
|
+
200: { description: "List of schedules" },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
67
91
|
const getSchedule = route({
|
|
68
92
|
method: "get",
|
|
69
93
|
path: "/api/schedules/{id}",
|
|
@@ -129,6 +153,19 @@ export async function handleSchedules(
|
|
|
129
153
|
queryParams: URLSearchParams,
|
|
130
154
|
_myAgentId: string | undefined,
|
|
131
155
|
): Promise<boolean> {
|
|
156
|
+
if (listSchedules.match(req.method, pathSegments)) {
|
|
157
|
+
const parsed = await listSchedules.parse(req, res, pathSegments, queryParams);
|
|
158
|
+
if (!parsed) return true;
|
|
159
|
+
const schedules = getScheduledTasks({
|
|
160
|
+
enabled: parsed.query.enabled,
|
|
161
|
+
name: parsed.query.name,
|
|
162
|
+
scheduleType: parsed.query.scheduleType,
|
|
163
|
+
hideCompleted: parsed.query.hideCompleted,
|
|
164
|
+
});
|
|
165
|
+
json(res, { schedules, count: schedules.length });
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
132
169
|
if (createSchedule.match(req.method, pathSegments)) {
|
|
133
170
|
const parsed = await createSchedule.parse(req, res, pathSegments, queryParams);
|
|
134
171
|
if (!parsed) return true;
|
|
@@ -0,0 +1,381 @@
|
|
|
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 { SCRIPT_SDK_TYPES, SCRIPT_STDLIB_TYPES, typecheckScript } from "../be/scripts/typecheck";
|
|
8
|
+
import { extractScriptSignature } from "../scripts-runtime/extract-signature";
|
|
9
|
+
import { runScript } from "../scripts-runtime/loader";
|
|
10
|
+
import {
|
|
11
|
+
ScriptFsModeSchema,
|
|
12
|
+
type ScriptRecord,
|
|
13
|
+
type ScriptScope,
|
|
14
|
+
ScriptScopeSchema,
|
|
15
|
+
} from "../types";
|
|
16
|
+
import { scrubObject } from "../utils/secret-scrubber";
|
|
17
|
+
import { route } from "./route-def";
|
|
18
|
+
import { json, jsonError } from "./utils";
|
|
19
|
+
|
|
20
|
+
const scriptNameSchema = z.string().min(1).max(200);
|
|
21
|
+
|
|
22
|
+
const upsertBodySchema = z.object({
|
|
23
|
+
name: scriptNameSchema,
|
|
24
|
+
source: z.string().min(1),
|
|
25
|
+
description: z.string().default(""),
|
|
26
|
+
intent: z.string().default(""),
|
|
27
|
+
scope: ScriptScopeSchema.default("agent"),
|
|
28
|
+
fsMode: ScriptFsModeSchema.default("none"),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const runBodySchema = z
|
|
32
|
+
.object({
|
|
33
|
+
name: scriptNameSchema.optional(),
|
|
34
|
+
source: z.string().min(1).optional(),
|
|
35
|
+
args: z.unknown().optional(),
|
|
36
|
+
intent: z.string().default(""),
|
|
37
|
+
scope: ScriptScopeSchema.optional(),
|
|
38
|
+
fsMode: ScriptFsModeSchema.default("none"),
|
|
39
|
+
})
|
|
40
|
+
.refine((body) => Boolean(body.name) !== Boolean(body.source), {
|
|
41
|
+
message: "Provide exactly one of name or source",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const searchBodySchema = z.object({
|
|
45
|
+
query: z.string().default(""),
|
|
46
|
+
scope: ScriptScopeSchema.optional(),
|
|
47
|
+
limit: z.number().int().min(1).max(100).default(10),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const nameParamsSchema = z.object({ name: scriptNameSchema });
|
|
51
|
+
const scopeQuerySchema = z.object({ scope: ScriptScopeSchema.default("agent") });
|
|
52
|
+
const optionalScopeQuerySchema = z.object({ scope: ScriptScopeSchema.optional() });
|
|
53
|
+
|
|
54
|
+
const upsertRoute = route({
|
|
55
|
+
method: "post",
|
|
56
|
+
path: "/api/scripts/upsert",
|
|
57
|
+
pattern: ["api", "scripts", "upsert"],
|
|
58
|
+
operationId: "scripts_upsert",
|
|
59
|
+
summary: "Create or update a reusable script",
|
|
60
|
+
description: "Explicit script upserts run a TypeScript typecheck before writing.",
|
|
61
|
+
tags: ["Scripts"],
|
|
62
|
+
body: upsertBodySchema,
|
|
63
|
+
responses: {
|
|
64
|
+
200: { description: "Script upserted" },
|
|
65
|
+
400: { description: "Validation or typecheck failure" },
|
|
66
|
+
403: { description: "Global write requires lead agent" },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const runRoute = route({
|
|
71
|
+
method: "post",
|
|
72
|
+
path: "/api/scripts/run",
|
|
73
|
+
pattern: ["api", "scripts", "run"],
|
|
74
|
+
operationId: "scripts_run",
|
|
75
|
+
summary: "Run a reusable or inline script",
|
|
76
|
+
description:
|
|
77
|
+
"Inline source skips typecheck and is auto-saved as a scratch script only on success.",
|
|
78
|
+
tags: ["Scripts"],
|
|
79
|
+
body: runBodySchema,
|
|
80
|
+
responses: {
|
|
81
|
+
200: { description: "Script run completed" },
|
|
82
|
+
400: { description: "Validation error" },
|
|
83
|
+
404: { description: "Script not found" },
|
|
84
|
+
501: { description: "workspace-rw scripts are not supported in v1" },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const searchRoute = route({
|
|
89
|
+
method: "post",
|
|
90
|
+
path: "/api/scripts/search",
|
|
91
|
+
pattern: ["api", "scripts", "search"],
|
|
92
|
+
operationId: "scripts_search",
|
|
93
|
+
summary: "Search reusable scripts",
|
|
94
|
+
description: "Phase 3 search is substring-only over script name and metadata.",
|
|
95
|
+
tags: ["Scripts"],
|
|
96
|
+
body: searchBodySchema,
|
|
97
|
+
responses: {
|
|
98
|
+
200: { description: "Matching scripts" },
|
|
99
|
+
400: { description: "Validation error" },
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const deleteRoute = route({
|
|
104
|
+
method: "delete",
|
|
105
|
+
path: "/api/scripts/{name}",
|
|
106
|
+
pattern: ["api", "scripts", null],
|
|
107
|
+
operationId: "scripts_delete",
|
|
108
|
+
summary: "Delete a reusable script",
|
|
109
|
+
tags: ["Scripts"],
|
|
110
|
+
params: nameParamsSchema,
|
|
111
|
+
query: scopeQuerySchema,
|
|
112
|
+
responses: {
|
|
113
|
+
200: { description: "Delete result" },
|
|
114
|
+
400: { description: "Validation error" },
|
|
115
|
+
403: { description: "Global delete requires lead agent" },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const typesRoute = route({
|
|
120
|
+
method: "get",
|
|
121
|
+
path: "/api/scripts/{name}/types",
|
|
122
|
+
pattern: ["api", "scripts", null, "types"],
|
|
123
|
+
operationId: "scripts_types",
|
|
124
|
+
summary: "Get script signature and authoring types",
|
|
125
|
+
tags: ["Scripts"],
|
|
126
|
+
params: nameParamsSchema,
|
|
127
|
+
query: optionalScopeQuerySchema,
|
|
128
|
+
responses: {
|
|
129
|
+
200: { description: "Script signature and type blobs" },
|
|
130
|
+
404: { description: "Script not found" },
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function requireAgent(res: ServerResponse, agentId: string | undefined) {
|
|
135
|
+
if (!agentId) {
|
|
136
|
+
jsonError(res, "X-Agent-ID required for scripts API", 400);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const agent = getAgentById(agentId);
|
|
140
|
+
if (!agent) {
|
|
141
|
+
jsonError(res, "Agent not found", 404);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return agent;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function signatureJsonFor(source: string): string {
|
|
148
|
+
return JSON.stringify(extractScriptSignature(source));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveScript(name: string, agentId: string, scope?: ScriptScope): ScriptRecord | null {
|
|
152
|
+
if (scope === "global") return getScript({ name, scope: "global" });
|
|
153
|
+
if (scope === "agent") return getScript({ name, scope: "agent", scopeId: agentId });
|
|
154
|
+
return (
|
|
155
|
+
getScript({ name, scope: "agent", scopeId: agentId }) ?? getScript({ name, scope: "global" })
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function scratchSlug(intent: string, source: string): string {
|
|
160
|
+
const base = (intent || "inline-script")
|
|
161
|
+
.toLowerCase()
|
|
162
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
163
|
+
.replace(/^-+|-+$/g, "")
|
|
164
|
+
.slice(0, 48);
|
|
165
|
+
const hash = new Bun.CryptoHasher("sha256").update(source).digest("hex").slice(0, 8);
|
|
166
|
+
return `scratch-${base || "inline-script"}-${hash}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function emitGlobalUpsertEvent(args: {
|
|
170
|
+
agentId: string;
|
|
171
|
+
script: ScriptRecord;
|
|
172
|
+
isNew: boolean;
|
|
173
|
+
isPromotion: boolean;
|
|
174
|
+
}) {
|
|
175
|
+
createEvent({
|
|
176
|
+
category: "system",
|
|
177
|
+
event: "script.global_upsert",
|
|
178
|
+
source: "api",
|
|
179
|
+
agentId: args.agentId,
|
|
180
|
+
data: {
|
|
181
|
+
scriptId: args.script.id,
|
|
182
|
+
name: args.script.name,
|
|
183
|
+
version: args.script.version,
|
|
184
|
+
contentHash: args.script.contentHash,
|
|
185
|
+
changedByAgentId: args.agentId,
|
|
186
|
+
isNew: args.isNew,
|
|
187
|
+
isPromotion: args.isPromotion,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function handleScripts(
|
|
193
|
+
req: IncomingMessage,
|
|
194
|
+
res: ServerResponse,
|
|
195
|
+
pathSegments: string[],
|
|
196
|
+
queryParams: URLSearchParams,
|
|
197
|
+
agentId: string | undefined,
|
|
198
|
+
): Promise<boolean> {
|
|
199
|
+
if (upsertRoute.match(req.method, pathSegments)) {
|
|
200
|
+
const parsed = await upsertRoute.parse(req, res, pathSegments, queryParams);
|
|
201
|
+
if (!parsed) return true;
|
|
202
|
+
const agent = requireAgent(res, agentId);
|
|
203
|
+
if (!agent) return true;
|
|
204
|
+
|
|
205
|
+
if (parsed.body.scope === "global" && !agent.isLead) {
|
|
206
|
+
jsonError(res, "Global scripts require a lead agent", 403);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const typecheck = typecheckScript(parsed.body.source);
|
|
211
|
+
if (!typecheck.ok) {
|
|
212
|
+
json(res, { error: "typecheck_failed", diagnostics: typecheck.diagnostics }, 400);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const existingAgentScript =
|
|
217
|
+
parsed.body.scope === "global"
|
|
218
|
+
? getScript({ name: parsed.body.name, scope: "agent", scopeId: agent.id })
|
|
219
|
+
: null;
|
|
220
|
+
const result = await upsertScriptByName({
|
|
221
|
+
name: parsed.body.name,
|
|
222
|
+
scope: parsed.body.scope,
|
|
223
|
+
scopeId: parsed.body.scope === "agent" ? agent.id : null,
|
|
224
|
+
source: parsed.body.source,
|
|
225
|
+
description: parsed.body.description,
|
|
226
|
+
intent: parsed.body.intent,
|
|
227
|
+
signatureJson: signatureJsonFor(parsed.body.source),
|
|
228
|
+
fsMode: parsed.body.fsMode,
|
|
229
|
+
agentId: agent.id,
|
|
230
|
+
isScratch: false,
|
|
231
|
+
typeChecked: true,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (parsed.body.scope === "global" && !result.contentDeduped) {
|
|
235
|
+
emitGlobalUpsertEvent({
|
|
236
|
+
agentId: agent.id,
|
|
237
|
+
script: result.script,
|
|
238
|
+
isNew: result.isNew,
|
|
239
|
+
isPromotion: Boolean(existingAgentScript),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
json(res, {
|
|
244
|
+
name: result.script.name,
|
|
245
|
+
version: result.script.version,
|
|
246
|
+
contentDeduped: result.contentDeduped,
|
|
247
|
+
});
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (runRoute.match(req.method, pathSegments)) {
|
|
252
|
+
const parsed = await runRoute.parse(req, res, pathSegments, queryParams);
|
|
253
|
+
if (!parsed) return true;
|
|
254
|
+
const agent = requireAgent(res, agentId);
|
|
255
|
+
if (!agent) return true;
|
|
256
|
+
|
|
257
|
+
let source = parsed.body.source;
|
|
258
|
+
let fsMode = parsed.body.fsMode;
|
|
259
|
+
if (parsed.body.name) {
|
|
260
|
+
const script = resolveScript(parsed.body.name, agent.id, parsed.body.scope);
|
|
261
|
+
if (!script) {
|
|
262
|
+
jsonError(res, "Script not found", 404);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
source = script.source;
|
|
266
|
+
fsMode = script.fsMode;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (fsMode === "workspace-rw") {
|
|
270
|
+
jsonError(res, "workspace-rw scripts are not supported by /api/scripts/run in v1", 501);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const output = await runScript({
|
|
275
|
+
source: source as string,
|
|
276
|
+
args: parsed.body.args,
|
|
277
|
+
fsMode,
|
|
278
|
+
agentId: agent.id,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
let autoSaved: { slug: string; reason: string } | undefined;
|
|
282
|
+
if (parsed.body.source && !output.error && output.exitCode === 0) {
|
|
283
|
+
const slug = scratchSlug(parsed.body.intent, parsed.body.source);
|
|
284
|
+
await upsertScriptByName({
|
|
285
|
+
name: slug,
|
|
286
|
+
scope: "agent",
|
|
287
|
+
scopeId: agent.id,
|
|
288
|
+
source: parsed.body.source,
|
|
289
|
+
description: `Scratch script: ${parsed.body.intent || slug}`,
|
|
290
|
+
intent: parsed.body.intent || "Inline script auto-saved after successful run",
|
|
291
|
+
signatureJson: signatureJsonFor(parsed.body.source),
|
|
292
|
+
fsMode: "none",
|
|
293
|
+
agentId: agent.id,
|
|
294
|
+
isScratch: true,
|
|
295
|
+
typeChecked: false,
|
|
296
|
+
changeReason: "Auto-saved successful inline run",
|
|
297
|
+
});
|
|
298
|
+
autoSaved = { slug, reason: "successful_inline_run" };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
json(
|
|
302
|
+
res,
|
|
303
|
+
scrubObject({
|
|
304
|
+
result: output.result,
|
|
305
|
+
autoSaved,
|
|
306
|
+
truncated: output.truncated,
|
|
307
|
+
durationMs: output.durationMs,
|
|
308
|
+
stdout: output.stdout,
|
|
309
|
+
stderr: output.stderr,
|
|
310
|
+
exitCode: output.exitCode,
|
|
311
|
+
error: output.error,
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (searchRoute.match(req.method, pathSegments)) {
|
|
318
|
+
const parsed = await searchRoute.parse(req, res, pathSegments, queryParams);
|
|
319
|
+
if (!parsed) return true;
|
|
320
|
+
const agent = requireAgent(res, agentId);
|
|
321
|
+
if (!agent) return true;
|
|
322
|
+
|
|
323
|
+
const matches = await searchScripts({
|
|
324
|
+
query: parsed.body.query,
|
|
325
|
+
scope: parsed.body.scope,
|
|
326
|
+
scopeId: agent.id,
|
|
327
|
+
limit: parsed.body.limit,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
json(res, {
|
|
331
|
+
results: matches.map(({ script, score }) => ({
|
|
332
|
+
name: script.name,
|
|
333
|
+
signature: JSON.parse(script.signatureJson),
|
|
334
|
+
description: script.description,
|
|
335
|
+
score,
|
|
336
|
+
})),
|
|
337
|
+
});
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (typesRoute.match(req.method, pathSegments)) {
|
|
342
|
+
const parsed = await typesRoute.parse(req, res, pathSegments, queryParams);
|
|
343
|
+
if (!parsed) return true;
|
|
344
|
+
const agent = requireAgent(res, agentId);
|
|
345
|
+
if (!agent) return true;
|
|
346
|
+
|
|
347
|
+
const script = resolveScript(parsed.params.name, agent.id, parsed.query.scope);
|
|
348
|
+
if (!script) {
|
|
349
|
+
jsonError(res, "Script not found", 404);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
json(res, {
|
|
353
|
+
signature: JSON.parse(script.signatureJson),
|
|
354
|
+
sdkTypes: SCRIPT_SDK_TYPES,
|
|
355
|
+
stdlibTypes: SCRIPT_STDLIB_TYPES,
|
|
356
|
+
});
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (deleteRoute.match(req.method, pathSegments)) {
|
|
361
|
+
const parsed = await deleteRoute.parse(req, res, pathSegments, queryParams);
|
|
362
|
+
if (!parsed) return true;
|
|
363
|
+
const agent = requireAgent(res, agentId);
|
|
364
|
+
if (!agent) return true;
|
|
365
|
+
|
|
366
|
+
if (parsed.query.scope === "global" && !agent.isLead) {
|
|
367
|
+
jsonError(res, "Global scripts require a lead agent", 403);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const deleted = deleteScript({
|
|
372
|
+
name: parsed.params.name,
|
|
373
|
+
scope: parsed.query.scope,
|
|
374
|
+
scopeId: parsed.query.scope === "agent" ? agent.id : null,
|
|
375
|
+
});
|
|
376
|
+
json(res, { deleted });
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return false;
|
|
381
|
+
}
|
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;
|