@desplega.ai/agent-swarm 1.79.4 → 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.
Files changed (130) hide show
  1. package/openapi.json +496 -32
  2. package/package.json +14 -6
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/db.ts +102 -31
  5. package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
  6. package/src/be/migrations/064_scripts.sql +39 -0
  7. package/src/be/migrations/065_script_embeddings.sql +7 -0
  8. package/src/be/pricing-normalize.ts +81 -0
  9. package/src/be/scripts/db.ts +391 -0
  10. package/src/be/scripts/embeddings.ts +231 -0
  11. package/src/be/scripts/maintenance.ts +9 -0
  12. package/src/be/scripts/typecheck.ts +193 -0
  13. package/src/be/seed-pricing.ts +293 -0
  14. package/src/cli.tsx +22 -5
  15. package/src/commands/artifact.ts +3 -2
  16. package/src/commands/claude-managed-setup.ts +21 -4
  17. package/src/commands/codex-login.ts +5 -3
  18. package/src/commands/onboard.tsx +2 -1
  19. package/src/commands/runner.ts +663 -246
  20. package/src/commands/setup.tsx +5 -3
  21. package/src/hooks/hook.ts +4 -3
  22. package/src/http/context.ts +6 -2
  23. package/src/http/index.ts +126 -68
  24. package/src/http/memory.ts +28 -0
  25. package/src/http/openapi.ts +1 -0
  26. package/src/http/page-proxy.ts +2 -1
  27. package/src/http/route-def.ts +1 -0
  28. package/src/http/schedules.ts +37 -0
  29. package/src/http/scripts.ts +381 -0
  30. package/src/http/session-data.ts +74 -23
  31. package/src/linear/outbound.ts +9 -2
  32. package/src/otel-impl.ts +200 -0
  33. package/src/otel.ts +132 -0
  34. package/src/providers/claude-adapter.ts +52 -6
  35. package/src/providers/claude-managed-adapter.ts +43 -17
  36. package/src/providers/claude-managed-pricing.ts +34 -0
  37. package/src/providers/codex-adapter.ts +38 -27
  38. package/src/providers/codex-models.ts +22 -3
  39. package/src/providers/devin-adapter.ts +11 -0
  40. package/src/providers/opencode-adapter.ts +31 -7
  41. package/src/providers/pi-mono-adapter.ts +39 -7
  42. package/src/providers/pricing-sources.md +52 -0
  43. package/src/providers/swarm-events-shared.ts +8 -4
  44. package/src/providers/types.ts +33 -10
  45. package/src/scripts-runtime/ctx.ts +23 -0
  46. package/src/scripts-runtime/eval-harness.ts +39 -0
  47. package/src/scripts-runtime/executors/native.ts +229 -0
  48. package/src/scripts-runtime/executors/registry.ts +16 -0
  49. package/src/scripts-runtime/executors/types.ts +63 -0
  50. package/src/scripts-runtime/extract-signature.ts +81 -0
  51. package/src/scripts-runtime/import-allowlist.ts +109 -0
  52. package/src/scripts-runtime/loader.ts +96 -0
  53. package/src/scripts-runtime/redacted.ts +48 -0
  54. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  55. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  56. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  57. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  58. package/src/scripts-runtime/stdlib/index.ts +16 -0
  59. package/src/scripts-runtime/stdlib/table.ts +17 -0
  60. package/src/scripts-runtime/swarm-config.ts +35 -0
  61. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  62. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  63. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  64. package/src/server.ts +18 -0
  65. package/src/tests/api-key.test.ts +33 -0
  66. package/src/tests/claude-managed-adapter.test.ts +17 -3
  67. package/src/tests/claude-managed-setup.test.ts +10 -1
  68. package/src/tests/codex-adapter.test.ts +20 -19
  69. package/src/tests/codex-login.test.ts +1 -1
  70. package/src/tests/context-snapshot.test.ts +2 -2
  71. package/src/tests/context-window.test.ts +65 -1
  72. package/src/tests/devin-adapter.test.ts +2 -0
  73. package/src/tests/http/context-routes.test.ts +161 -0
  74. package/src/tests/linear-outbound-sync.test.ts +109 -0
  75. package/src/tests/mcp-tools.test.ts +69 -0
  76. package/src/tests/migration-063-schema-relax.test.ts +109 -0
  77. package/src/tests/opencode-adapter.test.ts +146 -1
  78. package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
  79. package/src/tests/pages-view-count.test.ts +30 -5
  80. package/src/tests/providers/codex-cost.test.ts +18 -0
  81. package/src/tests/providers/opencode-cost.test.ts +74 -0
  82. package/src/tests/providers/pi-cost.test.ts +128 -0
  83. package/src/tests/redacted.test.ts +29 -0
  84. package/src/tests/runner-tool-spans.test.ts +268 -0
  85. package/src/tests/script-executor-conformance.test.ts +142 -0
  86. package/src/tests/script-executor-registry.test.ts +17 -0
  87. package/src/tests/scripts-db.test.ts +329 -0
  88. package/src/tests/scripts-embeddings.test.ts +291 -0
  89. package/src/tests/scripts-extract-signature.test.ts +47 -0
  90. package/src/tests/scripts-http.test.ts +350 -0
  91. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  92. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  93. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  94. package/src/tests/scripts-runtime.test.ts +289 -0
  95. package/src/tests/sdk-allowlist.test.ts +59 -0
  96. package/src/tests/secret-scrubber.test.ts +54 -1
  97. package/src/tests/session-costs-codex-recompute.test.ts +35 -22
  98. package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
  99. package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
  100. package/src/tests/store-progress-cost.test.ts +6 -1
  101. package/src/tests/swarm-config.test.ts +38 -0
  102. package/src/tests/tool-annotations.test.ts +2 -2
  103. package/src/tests/tool-call-progress.test.ts +30 -0
  104. package/src/tests/workflow-e2e.test.ts +218 -0
  105. package/src/tests/workflow-executors.test.ts +32 -2
  106. package/src/tests/workflow-input-redaction.test.ts +232 -0
  107. package/src/tests/workflow-swarm-script.test.ts +273 -0
  108. package/src/tools/memory-rate.ts +2 -1
  109. package/src/tools/script-common.ts +88 -0
  110. package/src/tools/script-delete.ts +35 -0
  111. package/src/tools/script-query-types.ts +37 -0
  112. package/src/tools/script-run.ts +43 -0
  113. package/src/tools/script-search.ts +32 -0
  114. package/src/tools/script-upsert.ts +43 -0
  115. package/src/tools/store-progress.ts +16 -60
  116. package/src/tools/tool-config.ts +7 -0
  117. package/src/tools/utils.ts +65 -12
  118. package/src/types.ts +122 -10
  119. package/src/utils/api-key.ts +28 -0
  120. package/src/utils/context-window.ts +104 -4
  121. package/src/utils/page-session.ts +8 -6
  122. package/src/utils/secret-scrubber.ts +29 -1
  123. package/src/workflows/engine.ts +12 -4
  124. package/src/workflows/executors/index.ts +1 -0
  125. package/src/workflows/executors/registry.ts +2 -0
  126. package/src/workflows/executors/script.ts +12 -1
  127. package/src/workflows/executors/swarm-script.ts +170 -0
  128. package/src/workflows/input.ts +65 -0
  129. package/src/workflows/recovery.ts +31 -3
  130. package/src/workflows/resume.ts +43 -5
@@ -0,0 +1,273 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { z } from "zod";
4
+ import {
5
+ closeDb,
6
+ createAgent,
7
+ createWorkflow,
8
+ getDb,
9
+ getWorkflowRun,
10
+ getWorkflowRunStepsByRunId,
11
+ initDb,
12
+ } from "../be/db";
13
+ import { upsertScriptByName } from "../be/scripts/db";
14
+ import { setScriptEmbeddingProviderForTests } from "../be/scripts/embeddings";
15
+ import type { Workflow, WorkflowDefinition } from "../types";
16
+ import { startWorkflowExecution } from "../workflows/engine";
17
+ import { InProcessEventBus } from "../workflows/event-bus";
18
+ import {
19
+ BaseExecutor,
20
+ type ExecutorDependencies,
21
+ type ExecutorResult,
22
+ } from "../workflows/executors/base";
23
+ import { ExecutorRegistry } from "../workflows/executors/registry";
24
+ import { SwarmScriptExecutor } from "../workflows/executors/swarm-script";
25
+ import { interpolate } from "../workflows/template";
26
+
27
+ const TEST_DB_PATH = "./test-workflow-swarm-script.sqlite";
28
+ const API_KEY = "test-workflow-swarm-script-key-1234567890";
29
+
30
+ const noOpEmbeddingProvider = {
31
+ name: "test/noop-workflow-script-embedding",
32
+ dimensions: 1,
33
+ async embed() {
34
+ return null;
35
+ },
36
+ async embedBatch(texts: string[]) {
37
+ return texts.map(() => null);
38
+ },
39
+ };
40
+
41
+ const signatureJson = JSON.stringify({
42
+ args: { type: "object" },
43
+ result: { type: "object" },
44
+ });
45
+
46
+ class EchoExecutor extends BaseExecutor<typeof EchoExecutor.schema, typeof EchoExecutor.outSchema> {
47
+ static readonly schema = z.object({ value: z.string() });
48
+ static readonly outSchema = z.object({ value: z.string() });
49
+
50
+ readonly type = "echo";
51
+ readonly mode = "instant" as const;
52
+ readonly configSchema = EchoExecutor.schema;
53
+ readonly outputSchema = EchoExecutor.outSchema;
54
+
55
+ protected async execute(
56
+ config: z.infer<typeof EchoExecutor.schema>,
57
+ ): Promise<ExecutorResult<z.infer<typeof EchoExecutor.outSchema>>> {
58
+ return { status: "success", output: { value: config.value } };
59
+ }
60
+ }
61
+
62
+ let savedEnv: NodeJS.ProcessEnv;
63
+ let agentId: string;
64
+ let deps: ExecutorDependencies;
65
+ let registry: ExecutorRegistry;
66
+
67
+ async function removeDbFiles(): Promise<void> {
68
+ for (const suffix of ["", "-wal", "-shm"]) {
69
+ try {
70
+ await unlink(TEST_DB_PATH + suffix);
71
+ } catch (error) {
72
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
73
+ }
74
+ }
75
+ }
76
+
77
+ function makeWorkflow(def: WorkflowDefinition): Workflow {
78
+ const wf = createWorkflow({
79
+ name: `swarm-script-test-${crypto.randomUUID()}`,
80
+ definition: def,
81
+ createdByAgentId: agentId,
82
+ });
83
+ return wf;
84
+ }
85
+
86
+ async function saveScript(name: string, source: string) {
87
+ return upsertScriptByName({
88
+ name,
89
+ scope: "agent",
90
+ scopeId: agentId,
91
+ source,
92
+ description: `${name} test script`,
93
+ intent: "workflow-swarm-script test fixture",
94
+ signatureJson,
95
+ agentId,
96
+ typeChecked: true,
97
+ });
98
+ }
99
+
100
+ beforeAll(async () => {
101
+ savedEnv = { ...process.env };
102
+ await removeDbFiles();
103
+ initDb(TEST_DB_PATH);
104
+ process.env.AGENT_SWARM_API_KEY = API_KEY;
105
+ delete process.env.API_KEY;
106
+ setScriptEmbeddingProviderForTests(noOpEmbeddingProvider);
107
+
108
+ const agent = createAgent({ name: "workflow-script-agent", isLead: true, status: "idle" });
109
+ agentId = agent.id;
110
+
111
+ const eventBus = new InProcessEventBus();
112
+ const db = await import("../be/db");
113
+ deps = {
114
+ db,
115
+ eventBus,
116
+ interpolate: (template, ctx) => interpolate(template, ctx).result,
117
+ };
118
+ registry = new ExecutorRegistry();
119
+ registry.register(new EchoExecutor(deps));
120
+ registry.register(new SwarmScriptExecutor(deps));
121
+ });
122
+
123
+ afterAll(async () => {
124
+ setScriptEmbeddingProviderForTests(null);
125
+ closeDb();
126
+ await removeDbFiles();
127
+ for (const key of Object.keys(process.env)) {
128
+ if (!(key in savedEnv)) delete process.env[key];
129
+ }
130
+ for (const [key, value] of Object.entries(savedEnv)) {
131
+ if (value === undefined) delete process.env[key];
132
+ else process.env[key] = value;
133
+ }
134
+ });
135
+
136
+ beforeEach(() => {
137
+ getDb().run("DELETE FROM workflow_run_steps");
138
+ getDb().run("DELETE FROM workflow_runs");
139
+ getDb().run("DELETE FROM scripts");
140
+ getDb().run("DELETE FROM workflows");
141
+ });
142
+
143
+ describe("SwarmScriptExecutor", () => {
144
+ test("A workflow with one swarm-script node resolves by name + runs + returns result", async () => {
145
+ await saveScript(
146
+ "add-one",
147
+ `export default async (args: { value: number }) => ({ value: args.value + 1 });`,
148
+ );
149
+
150
+ const executor = new SwarmScriptExecutor(deps);
151
+ const wf = makeWorkflow({ nodes: [] });
152
+ const result = await executor.run({
153
+ config: { scriptName: "add-one", args: { value: 6 } },
154
+ context: {},
155
+ meta: {
156
+ runId: crypto.randomUUID(),
157
+ stepId: crypto.randomUUID(),
158
+ nodeId: "script",
159
+ workflowId: wf.id,
160
+ dryRun: false,
161
+ },
162
+ });
163
+
164
+ expect(result.status).toBe("success");
165
+ expect(result.output?.result).toEqual({ value: 7 });
166
+ expect(result.output?.scriptName).toBe("add-one");
167
+ });
168
+
169
+ test("pinHash correctly resolves to a historic script_versions row", async () => {
170
+ const first = await saveScript("versioned", `export default async () => ({ version: "old" });`);
171
+ await saveScript("versioned", `export default async () => ({ version: "new" });`);
172
+
173
+ const executor = new SwarmScriptExecutor(deps);
174
+ const wf = makeWorkflow({ nodes: [] });
175
+ const result = await executor.run({
176
+ config: { scriptName: "versioned", pinHash: first.script.contentHash },
177
+ context: {},
178
+ meta: {
179
+ runId: crypto.randomUUID(),
180
+ stepId: crypto.randomUUID(),
181
+ nodeId: "script",
182
+ workflowId: wf.id,
183
+ dryRun: false,
184
+ },
185
+ });
186
+
187
+ expect(result.status).toBe("success");
188
+ expect(result.output?.result).toEqual({ version: "old" });
189
+ expect(result.output?.contentHash).toBe(first.script.contentHash);
190
+ expect(result.output?.version).toBe(1);
191
+ });
192
+
193
+ test("inputs mapping from a predecessor node correctly populates args", async () => {
194
+ await saveScript(
195
+ "from-input",
196
+ `export default async (args: { value: string }) => ({ seen: args.value });`,
197
+ );
198
+ const wf = makeWorkflow({
199
+ nodes: [
200
+ { id: "source", type: "echo", config: { value: "mapped-value" }, next: "script" },
201
+ {
202
+ id: "script",
203
+ type: "swarm-script",
204
+ inputs: { sourceValue: "source.value" },
205
+ config: { scriptName: "from-input", args: { value: "{{sourceValue}}" } },
206
+ },
207
+ ],
208
+ });
209
+
210
+ const runId = await startWorkflowExecution(wf, {}, registry);
211
+ const run = getWorkflowRun(runId);
212
+ const steps = getWorkflowRunStepsByRunId(runId);
213
+ const scriptStep = steps.find((step) => step.nodeId === "script");
214
+
215
+ expect(run?.status).toBe("completed");
216
+ expect(scriptStep?.status).toBe("completed");
217
+ expect(scriptStep?.output).toMatchObject({ result: { seen: "mapped-value" } });
218
+ });
219
+
220
+ test("fsMode workspace-rw is rejected at config validation with a clear error message", async () => {
221
+ await saveScript("noop", `export default async () => ({ ok: true });`);
222
+ const executor = new SwarmScriptExecutor(deps);
223
+ const wf = makeWorkflow({ nodes: [] });
224
+ const result = await executor.run({
225
+ config: { scriptName: "noop", fsMode: "workspace-rw" },
226
+ context: {},
227
+ meta: {
228
+ runId: crypto.randomUUID(),
229
+ stepId: crypto.randomUUID(),
230
+ nodeId: "script",
231
+ workflowId: wf.id,
232
+ dryRun: false,
233
+ },
234
+ });
235
+
236
+ expect(result.status).toBe("failed");
237
+ expect(result.error).toContain("workspace-rw");
238
+
239
+ const success = await executor.run({
240
+ config: { scriptName: "noop", fsMode: "none" },
241
+ context: {},
242
+ meta: {
243
+ runId: crypto.randomUUID(),
244
+ stepId: crypto.randomUUID(),
245
+ nodeId: "script",
246
+ workflowId: wf.id,
247
+ dryRun: false,
248
+ },
249
+ });
250
+ expect(success.status).toBe("success");
251
+ });
252
+
253
+ test("Failure in the script surfaces as a workflow-node failure", async () => {
254
+ await saveScript("throws", `export default async () => { throw new Error("boom"); };`);
255
+ const executor = new SwarmScriptExecutor(deps);
256
+ const wf = makeWorkflow({ nodes: [] });
257
+ const result = await executor.run({
258
+ config: { scriptName: "throws" },
259
+ context: {},
260
+ meta: {
261
+ runId: crypto.randomUUID(),
262
+ stepId: crypto.randomUUID(),
263
+ nodeId: "script",
264
+ workflowId: wf.id,
265
+ dryRun: false,
266
+ },
267
+ });
268
+
269
+ expect(result.status).toBe("failed");
270
+ expect(result.error).toContain("boom");
271
+ expect(result.output?.exitCode).not.toBe(0);
272
+ });
273
+ });
@@ -2,6 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import { REFERENCES_SOURCE_MAX_LENGTH, sanitizeReferencesSource } from "@/be/memory/raters/types";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
+ import { getApiKey } from "@/utils/api-key";
5
6
 
6
7
  /**
7
8
  * Plan: thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-5.md §1
@@ -92,7 +93,7 @@ export const registerMemoryRateTool = (server: McpServer) => {
92
93
  }
93
94
 
94
95
  const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
95
- const apiKey = process.env.API_KEY || "";
96
+ const apiKey = getApiKey();
96
97
 
97
98
  const event = {
98
99
  memoryId: id,
@@ -0,0 +1,88 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ import * as z from "zod";
3
+ import { getApiKey } from "@/utils/api-key";
4
+ import type { RequestInfo } from "./utils";
5
+
6
+ export const SCRIPT_TRANSPORT_ERROR =
7
+ "script_* tools require HTTP MCP transport — agent identity is not available over stdio in this build. Switch to MCP_BASE_URL=http://... or invoke the HTTP API directly.";
8
+
9
+ export const scriptNameSchema = z.string().min(1).max(200);
10
+ export const scriptScopeSchema = z.enum(["agent", "global"]);
11
+ export const scriptFsModeSchema = z.enum(["none", "workspace-rw"]);
12
+
13
+ export const scriptToolOutputSchema = z.object({
14
+ success: z.boolean(),
15
+ status: z.number(),
16
+ data: z.unknown().optional(),
17
+ error: z.string().optional(),
18
+ });
19
+
20
+ export type ScriptToolStructuredContent = z.infer<typeof scriptToolOutputSchema>;
21
+
22
+ function apiBaseUrl(): string {
23
+ return (process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`).replace(
24
+ /\/+$/,
25
+ "",
26
+ );
27
+ }
28
+
29
+ function toolError(message: string, status = 400): CallToolResult {
30
+ return {
31
+ isError: true,
32
+ content: [{ type: "text", text: message }],
33
+ structuredContent: {
34
+ success: false,
35
+ status,
36
+ error: message,
37
+ } satisfies ScriptToolStructuredContent,
38
+ };
39
+ }
40
+
41
+ export async function proxyScriptsApi(args: {
42
+ method: "GET" | "POST" | "DELETE";
43
+ path: string;
44
+ body?: unknown;
45
+ requestInfo: RequestInfo;
46
+ successMessage: (data: unknown) => string;
47
+ }): Promise<CallToolResult> {
48
+ if (!args.requestInfo.agentId) return toolError(SCRIPT_TRANSPORT_ERROR);
49
+
50
+ const apiKey = getApiKey();
51
+ const res = await fetch(`${apiBaseUrl()}${args.path}`, {
52
+ method: args.method,
53
+ headers: {
54
+ Authorization: `Bearer ${apiKey}`,
55
+ "X-Agent-ID": args.requestInfo.agentId,
56
+ "Content-Type": "application/json",
57
+ },
58
+ body: args.body === undefined ? undefined : JSON.stringify(args.body),
59
+ });
60
+
61
+ const text = await res.text();
62
+ let data: unknown;
63
+ if (text) {
64
+ try {
65
+ data = JSON.parse(text);
66
+ } catch {
67
+ data = { error: text };
68
+ }
69
+ }
70
+
71
+ const error =
72
+ typeof data === "object" && data !== null && "error" in data
73
+ ? String((data as { error: unknown }).error)
74
+ : undefined;
75
+
76
+ if (!res.ok) {
77
+ return toolError(error ?? `Scripts API request failed with ${res.status}`, res.status);
78
+ }
79
+
80
+ return {
81
+ content: [{ type: "text", text: args.successMessage(data) }],
82
+ structuredContent: {
83
+ success: true,
84
+ status: res.status,
85
+ data,
86
+ } satisfies ScriptToolStructuredContent,
87
+ };
88
+ }
@@ -0,0 +1,35 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar } from "@/tools/utils";
4
+ import {
5
+ proxyScriptsApi,
6
+ scriptNameSchema,
7
+ scriptScopeSchema,
8
+ scriptToolOutputSchema,
9
+ } from "./script-common";
10
+
11
+ export const SCRIPT_DELETE_DESCRIPTION =
12
+ "Remove a swarm-shared script from the catalog. Versions table preserves history.";
13
+
14
+ export const registerScriptDeleteTool = (server: McpServer) => {
15
+ createToolRegistrar(server)(
16
+ "script-delete",
17
+ {
18
+ title: "Script Delete",
19
+ description: SCRIPT_DELETE_DESCRIPTION,
20
+ annotations: { destructiveHint: true, openWorldHint: false },
21
+ inputSchema: z.object({
22
+ name: scriptNameSchema.describe("Script name to delete."),
23
+ scope: scriptScopeSchema.default("agent").describe("Script scope to delete from."),
24
+ }),
25
+ outputSchema: scriptToolOutputSchema,
26
+ },
27
+ async ({ name, scope }, requestInfo) =>
28
+ proxyScriptsApi({
29
+ method: "DELETE",
30
+ path: `/api/scripts/${encodeURIComponent(name)}?scope=${encodeURIComponent(scope)}`,
31
+ requestInfo,
32
+ successMessage: () => "Script delete completed.",
33
+ }),
34
+ );
35
+ };
@@ -0,0 +1,37 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar } from "@/tools/utils";
4
+ import {
5
+ proxyScriptsApi,
6
+ scriptNameSchema,
7
+ scriptScopeSchema,
8
+ scriptToolOutputSchema,
9
+ } from "./script-common";
10
+
11
+ export const SCRIPT_QUERY_TYPES_DESCRIPTION =
12
+ "Fetch the signature + the auto-generated `swarm-sdk.d.ts` (derived from the live MCP tool registry) + the `stdlib.d.ts` blobs — for IDE-style introspection before authoring or running a script. The same types are used by `script-upsert`'s typecheck pass, so they are authoritative.";
13
+
14
+ export const registerScriptQueryTypesTool = (server: McpServer) => {
15
+ createToolRegistrar(server)(
16
+ "script-query-types",
17
+ {
18
+ title: "Script Query Types",
19
+ description: SCRIPT_QUERY_TYPES_DESCRIPTION,
20
+ annotations: { readOnlyHint: true, openWorldHint: false },
21
+ inputSchema: z.object({
22
+ name: scriptNameSchema.describe("Script name whose signature should be fetched."),
23
+ scope: scriptScopeSchema.optional().describe("Optional scope for script resolution."),
24
+ }),
25
+ outputSchema: scriptToolOutputSchema,
26
+ },
27
+ async ({ name, scope }, requestInfo) => {
28
+ const query = scope ? `?scope=${encodeURIComponent(scope)}` : "";
29
+ return proxyScriptsApi({
30
+ method: "GET",
31
+ path: `/api/scripts/${encodeURIComponent(name)}/types${query}`,
32
+ requestInfo,
33
+ successMessage: () => "Script type query completed.",
34
+ });
35
+ },
36
+ );
37
+ };
@@ -0,0 +1,43 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar } from "@/tools/utils";
4
+ import {
5
+ proxyScriptsApi,
6
+ scriptFsModeSchema,
7
+ scriptNameSchema,
8
+ scriptScopeSchema,
9
+ scriptToolOutputSchema,
10
+ } from "./script-common";
11
+
12
+ export const SCRIPT_RUN_DESCRIPTION =
13
+ "Run a named swarm-shared script (callable across agents and from workflow `swarm-script` nodes), OR inline source (auto-saved as scratch to the catalog). Use for swarm-visible, durable scripts. For local-only throwaway TS, use code-mode `run`.";
14
+
15
+ export const registerScriptRunTool = (server: McpServer) => {
16
+ createToolRegistrar(server)(
17
+ "script-run",
18
+ {
19
+ title: "Script Run",
20
+ description: SCRIPT_RUN_DESCRIPTION,
21
+ annotations: { openWorldHint: true },
22
+ inputSchema: z.object({
23
+ name: scriptNameSchema.optional().describe("Name of a reusable script to run."),
24
+ source: z.string().min(1).optional().describe("Inline TypeScript source to run."),
25
+ args: z.unknown().optional().describe("JSON-serializable script arguments."),
26
+ intent: z.string().default("").describe("Why this script is being run."),
27
+ scope: scriptScopeSchema.optional().describe("Optional scope for named script resolution."),
28
+ fsMode: scriptFsModeSchema
29
+ .default("none")
30
+ .describe("Filesystem mode. v1 supports none only."),
31
+ }),
32
+ outputSchema: scriptToolOutputSchema,
33
+ },
34
+ async (args, requestInfo) =>
35
+ proxyScriptsApi({
36
+ method: "POST",
37
+ path: "/api/scripts/run",
38
+ body: args,
39
+ requestInfo,
40
+ successMessage: () => "Script run completed.",
41
+ }),
42
+ );
43
+ };
@@ -0,0 +1,32 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar } from "@/tools/utils";
4
+ import { proxyScriptsApi, scriptScopeSchema, scriptToolOutputSchema } from "./script-common";
5
+
6
+ export const SCRIPT_SEARCH_DESCRIPTION =
7
+ "Semantic search over swarm-shared TypeScript scripts (catalog persisted in the agent-swarm DB; callable from agents and workflows). For ephemeral throwaway TS on your local machine, use code-mode instead.";
8
+
9
+ export const registerScriptSearchTool = (server: McpServer) => {
10
+ createToolRegistrar(server)(
11
+ "script-search",
12
+ {
13
+ title: "Script Search",
14
+ description: SCRIPT_SEARCH_DESCRIPTION,
15
+ annotations: { readOnlyHint: true, openWorldHint: false },
16
+ inputSchema: z.object({
17
+ query: z.string().default("").describe("Search query for reusable scripts."),
18
+ scope: scriptScopeSchema.optional().describe("Optional script scope filter."),
19
+ limit: z.number().int().min(1).max(100).default(10).describe("Maximum results."),
20
+ }),
21
+ outputSchema: scriptToolOutputSchema,
22
+ },
23
+ async (args, requestInfo) =>
24
+ proxyScriptsApi({
25
+ method: "POST",
26
+ path: "/api/scripts/search",
27
+ body: args,
28
+ requestInfo,
29
+ successMessage: () => "Script search completed.",
30
+ }),
31
+ );
32
+ };
@@ -0,0 +1,43 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar } from "@/tools/utils";
4
+ import {
5
+ proxyScriptsApi,
6
+ scriptFsModeSchema,
7
+ scriptNameSchema,
8
+ scriptScopeSchema,
9
+ scriptToolOutputSchema,
10
+ } from "./script-common";
11
+
12
+ export const SCRIPT_UPSERT_DESCRIPTION =
13
+ "Persist a TypeScript script to the swarm catalog under your agent scope (or global if you're a lead). Other agents and workflow nodes will be able to find and run it. For local-only scripts, use code-mode `save`.";
14
+
15
+ export const registerScriptUpsertTool = (server: McpServer) => {
16
+ createToolRegistrar(server)(
17
+ "script-upsert",
18
+ {
19
+ title: "Script Upsert",
20
+ description: SCRIPT_UPSERT_DESCRIPTION,
21
+ annotations: { openWorldHint: false },
22
+ inputSchema: z.object({
23
+ name: scriptNameSchema.describe("Stable script name within the selected scope."),
24
+ source: z.string().min(1).describe("TypeScript source with a default export function."),
25
+ description: z.string().default("").describe("Human-readable script description."),
26
+ intent: z.string().default("").describe("Why this script exists."),
27
+ scope: scriptScopeSchema.default("agent").describe("Persist under agent or global scope."),
28
+ fsMode: scriptFsModeSchema
29
+ .default("none")
30
+ .describe("Filesystem mode. v1 supports none only."),
31
+ }),
32
+ outputSchema: scriptToolOutputSchema,
33
+ },
34
+ async (args, requestInfo) =>
35
+ proxyScriptsApi({
36
+ method: "POST",
37
+ path: "/api/scripts/upsert",
38
+ body: args,
39
+ requestInfo,
40
+ successMessage: () => "Script upsert completed.",
41
+ }),
42
+ );
43
+ };
@@ -3,7 +3,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import * as z from "zod";
4
4
  import {
5
5
  completeTask,
6
- createSessionCost,
7
6
  createTaskExtended,
8
7
  failTask,
9
8
  getAgentById,
@@ -24,32 +23,12 @@ import { AgentTaskSchema } from "@/types";
24
23
  import "./templates";
25
24
  import { validateJsonSchema } from "@/workflows/json-schema-validator";
26
25
 
27
- // Schema for optional cost data that agents can self-report.
28
- // In practice the harness adapter (claude/codex/opencode/etc.) is the
29
- // authoritative source of cost data it gets written via
30
- // POST /api/session-costs from the runner. Agents calling store-progress
31
- // rarely know the real numbers and have been observed echoing the example
32
- // values from this schema (e.g. model="opus" on a gpt-5-nano run). The
33
- // handler below silently drops payloads where every numeric field is zero.
34
- const CostDataSchema = z
35
- .object({
36
- totalCostUsd: z.number().min(0).describe("Total cost in USD"),
37
- inputTokens: z.number().int().min(0).optional().describe("Input tokens used"),
38
- outputTokens: z.number().int().min(0).optional().describe("Output tokens used"),
39
- cacheReadTokens: z.number().int().min(0).optional().describe("Cache read tokens"),
40
- cacheWriteTokens: z.number().int().min(0).optional().describe("Cache write tokens"),
41
- durationMs: z.number().int().min(0).optional().describe("Duration in milliseconds"),
42
- numTurns: z.number().int().min(1).optional().describe("Number of turns/iterations"),
43
- model: z
44
- .string()
45
- .optional()
46
- .describe(
47
- "Model identifier reported by the agent (only set if the agent has the real ID; do NOT echo the schema example).",
48
- ),
49
- })
50
- .describe(
51
- "Optional self-reported cost data. The harness adapter writes the authoritative cost record automatically — only pass this if you have real, non-zero numbers from a model that doesn't surface usage to the harness.",
52
- );
26
+ // Phase 11: the `cost` / `costData` field was removed from this tool's input
27
+ // schema. Adapters (claude/codex/pi/opencode/devin/claude-managed) are the
28
+ // sole writers of `session_costs` rows via `POST /api/session-costs`. Agents
29
+ // calling `store-progress` rarely knew the real numbers and historically
30
+ // echoed the schema example, producing noise rows keyed `mcp-<taskId>-<ts>`
31
+ // that double-counted alongside the harness's authoritative entry.
53
32
 
54
33
  export const registerStoreProgressTool = (server: McpServer) => {
55
34
  createToolRegistrar(server)(
@@ -72,9 +51,10 @@ export const registerStoreProgressTool = (server: McpServer) => {
72
51
  .string()
73
52
  .optional()
74
53
  .describe("The reason for failure (used when failing)."),
75
- costData: CostDataSchema.optional().describe(
76
- "Optional cost data for tracking session costs. When provided, a session cost record will be created linked to this task.",
77
- ),
54
+ // Phase 11: `costData` removed. The harness adapter is the sole
55
+ // writer of `session_costs` (see POST /api/session-costs in the
56
+ // runner). If a payload still includes the field, Zod's
57
+ // `unknownKeys` default drops it silently.
78
58
  }),
79
59
  outputSchema: z.object({
80
60
  success: z.boolean(),
@@ -89,7 +69,7 @@ export const registerStoreProgressTool = (server: McpServer) => {
89
69
  ),
90
70
  }),
91
71
  },
92
- async ({ taskId, progress, status, output, failureReason, costData }, requestInfo, _meta) => {
72
+ async ({ taskId, progress, status, output, failureReason }, requestInfo, _meta) => {
93
73
  if (!requestInfo.agentId) {
94
74
  return {
95
75
  content: [
@@ -254,35 +234,11 @@ export const registerStoreProgressTool = (server: McpServer) => {
254
234
  }
255
235
  }
256
236
 
257
- // Store cost data only if the agent provided non-trivial numbers.
258
- // Agents observed copying the schema example (e.g. model="opus"
259
- // on a gpt-5-nano run) with all-zero token/cost fields, producing
260
- // duplicate noise rows in session_costs alongside the harness's
261
- // authoritative entry. Drop those silently.
262
- const hasRealCost =
263
- costData &&
264
- (costData.totalCostUsd > 0 ||
265
- (costData.inputTokens ?? 0) > 0 ||
266
- (costData.outputTokens ?? 0) > 0 ||
267
- (costData.cacheReadTokens ?? 0) > 0 ||
268
- (costData.cacheWriteTokens ?? 0) > 0);
269
-
270
- if (hasRealCost && requestInfo.agentId) {
271
- createSessionCost({
272
- sessionId: `mcp-${taskId}-${Date.now()}`, // Generate unique session ID for MCP-based tasks
273
- taskId,
274
- agentId: requestInfo.agentId,
275
- totalCostUsd: costData.totalCostUsd,
276
- inputTokens: costData.inputTokens ?? 0,
277
- outputTokens: costData.outputTokens ?? 0,
278
- cacheReadTokens: costData.cacheReadTokens ?? 0,
279
- cacheWriteTokens: costData.cacheWriteTokens ?? 0,
280
- durationMs: costData.durationMs ?? 0,
281
- numTurns: costData.numTurns ?? 1,
282
- model: costData.model ?? "unknown",
283
- isError: status === "failed",
284
- });
285
- }
237
+ // Phase 11: removed the per-call `session_costs` insert. The harness
238
+ // adapter is the sole writer of cost rows now (via the runner's
239
+ // `POST /api/session-costs`); store-progress historically wrote a
240
+ // duplicate row keyed `mcp-<taskId>-<ts>` whenever an agent
241
+ // hallucinated a `costData` payload.
286
242
 
287
243
  return {
288
244
  success: true,