@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,271 @@
1
+ // Phase 2 fix — adapter-emitted model ids carry harness-specific routing
2
+ // prefixes (`openrouter/`, `github-copilot/`, …) that the pricing seed does
3
+ // not. Before the fix every opencode + pi-via-copilot run fell through to
4
+ // `costSource='unpriced'` even when a seeded rate row existed. This suite
5
+ // regresses the drift cases observed in real-harness E2E.
6
+
7
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
8
+ import { unlink } from "node:fs/promises";
9
+ import {
10
+ createServer as createHttpServer,
11
+ type IncomingMessage,
12
+ type Server,
13
+ type ServerResponse,
14
+ } from "node:http";
15
+ import { closeDb, createAgent, getDb, initDb, insertPricingRow } from "../be/db";
16
+ import { normalizeModelKey } from "../be/pricing-normalize";
17
+ import { handleCore } from "../http/core";
18
+ import { handleSessionData } from "../http/session-data";
19
+ import { getPathSegments, parseQueryParams } from "../http/utils";
20
+
21
+ const TEST_DB_PATH = "./test-model-key-normalize.sqlite";
22
+ const API_KEY = "test-model-key-normalize";
23
+
24
+ async function removeDbFiles(path: string): Promise<void> {
25
+ for (const suffix of ["", "-wal", "-shm"]) {
26
+ try {
27
+ await unlink(path + suffix);
28
+ } catch (error) {
29
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
30
+ }
31
+ }
32
+ }
33
+
34
+ async function listen(server: Server): Promise<number> {
35
+ await new Promise<void>((resolve) => server.listen(0, resolve));
36
+ const addr = server.address();
37
+ if (!addr || typeof addr === "string") throw new Error("no port");
38
+ return addr.port;
39
+ }
40
+
41
+ function createTestServer(apiKey: string): Server {
42
+ return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
43
+ const myAgentId = req.headers["x-agent-id"] as string | undefined;
44
+ const handled = await handleCore(req, res, myAgentId, apiKey);
45
+ if (handled) return;
46
+ const pathSegments = getPathSegments(req.url || "");
47
+ const queryParams = parseQueryParams(req.url || "");
48
+ const ok = await handleSessionData(req, res, pathSegments, queryParams, myAgentId);
49
+ if (!ok) {
50
+ res.writeHead(404);
51
+ res.end("Not Found");
52
+ }
53
+ });
54
+ }
55
+
56
+ let server: Server;
57
+ let port: number;
58
+ let testAgent: { id: string };
59
+
60
+ beforeAll(async () => {
61
+ await removeDbFiles(TEST_DB_PATH);
62
+ initDb(TEST_DB_PATH);
63
+ testAgent = createAgent({ name: "model-key-normalize-test", isLead: false, status: "idle" });
64
+ server = createTestServer(API_KEY);
65
+ port = await listen(server);
66
+ });
67
+
68
+ afterAll(async () => {
69
+ await new Promise<void>((resolve) => server.close(() => resolve()));
70
+ closeDb();
71
+ await removeDbFiles(TEST_DB_PATH);
72
+ });
73
+
74
+ afterEach(() => {
75
+ const db = getDb();
76
+ db.prepare("DELETE FROM session_costs").run();
77
+ db.prepare("DELETE FROM pricing WHERE effective_from > 0").run();
78
+ });
79
+
80
+ function authedFetch(path: string, init: RequestInit = {}): Promise<Response> {
81
+ return fetch(`http://localhost:${port}${path}`, {
82
+ ...init,
83
+ headers: {
84
+ Authorization: `Bearer ${API_KEY}`,
85
+ "Content-Type": "application/json",
86
+ ...(init.headers ?? {}),
87
+ },
88
+ });
89
+ }
90
+
91
+ interface CostResponse {
92
+ success: boolean;
93
+ cost: {
94
+ totalCostUsd: number;
95
+ model: string;
96
+ costSource: "harness" | "pricing-table" | "unpriced";
97
+ };
98
+ }
99
+
100
+ describe("normalizeModelKey()", () => {
101
+ test("strips opencode routing prefix `openrouter/`", () => {
102
+ expect(normalizeModelKey("opencode", "openrouter/anthropic/claude-sonnet-4.5")).toBe(
103
+ "anthropic/claude-sonnet-4.5",
104
+ );
105
+ });
106
+
107
+ test("strips pi routing prefix `github-copilot/`", () => {
108
+ expect(normalizeModelKey("pi", "github-copilot/gpt-5.4")).toBe("gpt-5.4");
109
+ });
110
+
111
+ test("strips pi routing prefix `openrouter/`", () => {
112
+ expect(normalizeModelKey("pi", "openrouter/anthropic/claude-sonnet-4.5")).toBe(
113
+ "anthropic/claude-sonnet-4.5",
114
+ );
115
+ });
116
+
117
+ test("strips pi routing prefix `openrouter/` for deepseek (Phase 3 fix regression)", () => {
118
+ // The exact case from today's E2E (2026-05-18): pi-mono emits
119
+ // `openrouter/deepseek/deepseek-v4-flash`, the pricing seed keys the row
120
+ // under bare `deepseek/deepseek-v4-flash`. Drift collapsed before this
121
+ // assertion exists; keep it as an explicit regression guard.
122
+ expect(normalizeModelKey("pi", "openrouter/deepseek/deepseek-v4-flash")).toBe(
123
+ "deepseek/deepseek-v4-flash",
124
+ );
125
+ });
126
+
127
+ test("strips opencode routing prefix `openrouter/` for deepseek (Phase 3 fix regression)", () => {
128
+ // Same model, different harness — opencode-adapter wraps the underlying
129
+ // model id under the same `openrouter/` proxy prefix.
130
+ expect(normalizeModelKey("opencode", "openrouter/deepseek/deepseek-v4-flash")).toBe(
131
+ "deepseek/deepseek-v4-flash",
132
+ );
133
+ });
134
+
135
+ test("is a no-op for canonical claude ids", () => {
136
+ expect(normalizeModelKey("claude", "claude-opus-4-7")).toBe("claude-opus-4-7");
137
+ });
138
+
139
+ test("is idempotent", () => {
140
+ const once = normalizeModelKey("opencode", "openrouter/anthropic/claude-sonnet-4.5");
141
+ const twice = normalizeModelKey("opencode", once);
142
+ expect(twice).toBe(once);
143
+ });
144
+
145
+ test("lowercases mixed-case input", () => {
146
+ expect(normalizeModelKey("opencode", "OpenRouter/Anthropic/Claude-Sonnet-4.5")).toBe(
147
+ "anthropic/claude-sonnet-4.5",
148
+ );
149
+ });
150
+ });
151
+
152
+ describe("Phase 2 fix — POST /api/session-costs normalizes routing prefixes", () => {
153
+ test("opencode `openrouter/anthropic/claude-sonnet-4.5` resolves the seeded `anthropic/claude-sonnet-4.5` row", async () => {
154
+ // Seed mirrors what models.dev → seed-pricing.ts produces for the
155
+ // openrouter section: bare `anthropic/<id>` under the `opencode` provider.
156
+ insertPricingRow({
157
+ provider: "opencode",
158
+ model: "anthropic/claude-sonnet-4.5",
159
+ tokenClass: "input",
160
+ effectiveFrom: 1,
161
+ pricePerMillionUsd: 3,
162
+ });
163
+ insertPricingRow({
164
+ provider: "opencode",
165
+ model: "anthropic/claude-sonnet-4.5",
166
+ tokenClass: "output",
167
+ effectiveFrom: 1,
168
+ pricePerMillionUsd: 15,
169
+ });
170
+
171
+ const res = await authedFetch(`/api/session-costs`, {
172
+ method: "POST",
173
+ body: JSON.stringify({
174
+ sessionId: "opencode-normalize-1",
175
+ agentId: testAgent.id,
176
+ totalCostUsd: 0.42, // harness-reported, expected to be overwritten
177
+ inputTokens: 1_000_000,
178
+ outputTokens: 100_000,
179
+ // The exact string the opencode adapter emits today.
180
+ model: "openrouter/anthropic/claude-sonnet-4.5",
181
+ provider: "opencode",
182
+ durationMs: 1_000,
183
+ numTurns: 1,
184
+ }),
185
+ });
186
+ expect(res.status).toBe(201);
187
+ const body = (await res.json()) as CostResponse;
188
+ // 1M @ $3 + 100k @ $15 = $3 + $1.50 = $4.50
189
+ expect(body.cost.costSource).toBe("pricing-table");
190
+ expect(body.cost.totalCostUsd).toBeCloseTo(4.5, 5);
191
+ // Original adapter-emitted string is preserved on the row for debugging.
192
+ expect(body.cost.model).toBe("openrouter/anthropic/claude-sonnet-4.5");
193
+ });
194
+
195
+ test("pi `github-copilot/gpt-5.4` resolves the seeded bare `gpt-5.4` row", async () => {
196
+ insertPricingRow({
197
+ provider: "pi",
198
+ model: "gpt-5.4",
199
+ tokenClass: "input",
200
+ effectiveFrom: 1,
201
+ pricePerMillionUsd: 2,
202
+ });
203
+ insertPricingRow({
204
+ provider: "pi",
205
+ model: "gpt-5.4",
206
+ tokenClass: "output",
207
+ effectiveFrom: 1,
208
+ pricePerMillionUsd: 8,
209
+ });
210
+
211
+ const res = await authedFetch(`/api/session-costs`, {
212
+ method: "POST",
213
+ body: JSON.stringify({
214
+ sessionId: "pi-copilot-normalize-1",
215
+ agentId: testAgent.id,
216
+ totalCostUsd: 9.99,
217
+ inputTokens: 500_000,
218
+ outputTokens: 250_000,
219
+ model: "github-copilot/gpt-5.4",
220
+ provider: "pi",
221
+ durationMs: 1_000,
222
+ numTurns: 1,
223
+ }),
224
+ });
225
+ expect(res.status).toBe(201);
226
+ const body = (await res.json()) as CostResponse;
227
+ // 500k @ $2 + 250k @ $8 = $1 + $2 = $3
228
+ expect(body.cost.costSource).toBe("pricing-table");
229
+ expect(body.cost.totalCostUsd).toBeCloseTo(3.0, 5);
230
+ expect(body.cost.model).toBe("github-copilot/gpt-5.4");
231
+ });
232
+
233
+ test("claude `claude-opus-4-7` (no prefix) still resolves — regression guard", async () => {
234
+ // The bug report flagged claude-adapter as already-working. Make sure
235
+ // we did not regress its bare-id lookup.
236
+ insertPricingRow({
237
+ provider: "claude",
238
+ model: "claude-opus-4-7",
239
+ tokenClass: "input",
240
+ effectiveFrom: 1,
241
+ pricePerMillionUsd: 15,
242
+ });
243
+ insertPricingRow({
244
+ provider: "claude",
245
+ model: "claude-opus-4-7",
246
+ tokenClass: "output",
247
+ effectiveFrom: 1,
248
+ pricePerMillionUsd: 75,
249
+ });
250
+
251
+ const res = await authedFetch(`/api/session-costs`, {
252
+ method: "POST",
253
+ body: JSON.stringify({
254
+ sessionId: "claude-bare-1",
255
+ agentId: testAgent.id,
256
+ totalCostUsd: 1.23,
257
+ inputTokens: 1_000_000,
258
+ outputTokens: 100_000,
259
+ model: "claude-opus-4-7",
260
+ provider: "claude",
261
+ durationMs: 1_000,
262
+ numTurns: 1,
263
+ }),
264
+ });
265
+ expect(res.status).toBe(201);
266
+ const body = (await res.json()) as CostResponse;
267
+ // 1M @ $15 + 100k @ $75 = $15 + $7.50 = $22.50
268
+ expect(body.cost.costSource).toBe("pricing-table");
269
+ expect(body.cost.totalCostUsd).toBeCloseTo(22.5, 5);
270
+ });
271
+ });
@@ -0,0 +1,170 @@
1
+ // Phase 2: POST /api/session-costs recompute fires for every provider with
2
+ // seeded pricing rows — not just codex. Unknown (provider, model) pairs are
3
+ // tagged `costSource='unpriced'`.
4
+
5
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
6
+ import { unlink } from "node:fs/promises";
7
+ import {
8
+ createServer as createHttpServer,
9
+ type IncomingMessage,
10
+ type Server,
11
+ type ServerResponse,
12
+ } from "node:http";
13
+ import { closeDb, createAgent, getDb, initDb, insertPricingRow } from "../be/db";
14
+ import { handleCore } from "../http/core";
15
+ import { handleSessionData } from "../http/session-data";
16
+ import { getPathSegments, parseQueryParams } from "../http/utils";
17
+
18
+ const TEST_DB_PATH = "./test-recompute-all-providers.sqlite";
19
+ const API_KEY = "test-recompute-all";
20
+
21
+ async function removeDbFiles(path: string): Promise<void> {
22
+ for (const suffix of ["", "-wal", "-shm"]) {
23
+ try {
24
+ await unlink(path + suffix);
25
+ } catch (error) {
26
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
27
+ }
28
+ }
29
+ }
30
+
31
+ async function listen(server: Server): Promise<number> {
32
+ await new Promise<void>((resolve) => server.listen(0, resolve));
33
+ const addr = server.address();
34
+ if (!addr || typeof addr === "string") throw new Error("no port");
35
+ return addr.port;
36
+ }
37
+
38
+ function createTestServer(apiKey: string): Server {
39
+ return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
40
+ const myAgentId = req.headers["x-agent-id"] as string | undefined;
41
+ const handled = await handleCore(req, res, myAgentId, apiKey);
42
+ if (handled) return;
43
+ const pathSegments = getPathSegments(req.url || "");
44
+ const queryParams = parseQueryParams(req.url || "");
45
+ const ok = await handleSessionData(req, res, pathSegments, queryParams, myAgentId);
46
+ if (!ok) {
47
+ res.writeHead(404);
48
+ res.end("Not Found");
49
+ }
50
+ });
51
+ }
52
+
53
+ let server: Server;
54
+ let port: number;
55
+ let testAgent: { id: string };
56
+
57
+ beforeAll(async () => {
58
+ await removeDbFiles(TEST_DB_PATH);
59
+ initDb(TEST_DB_PATH);
60
+ testAgent = createAgent({ name: "recompute-all-test", isLead: false, status: "idle" });
61
+ server = createTestServer(API_KEY);
62
+ port = await listen(server);
63
+ });
64
+
65
+ afterAll(async () => {
66
+ await new Promise<void>((resolve) => server.close(() => resolve()));
67
+ closeDb();
68
+ await removeDbFiles(TEST_DB_PATH);
69
+ });
70
+
71
+ afterEach(() => {
72
+ const db = getDb();
73
+ db.prepare("DELETE FROM session_costs").run();
74
+ // Wipe everything we explicitly inserted (effective_from > 0); leave the
75
+ // migration-046 codex seeds alone.
76
+ db.prepare("DELETE FROM pricing WHERE effective_from > 0").run();
77
+ });
78
+
79
+ function authedFetch(path: string, init: RequestInit = {}): Promise<Response> {
80
+ return fetch(`http://localhost:${port}${path}`, {
81
+ ...init,
82
+ headers: {
83
+ Authorization: `Bearer ${API_KEY}`,
84
+ "Content-Type": "application/json",
85
+ ...(init.headers ?? {}),
86
+ },
87
+ });
88
+ }
89
+
90
+ interface CostResponse {
91
+ success: boolean;
92
+ cost: {
93
+ totalCostUsd: number;
94
+ costSource: "harness" | "pricing-table" | "unpriced";
95
+ };
96
+ }
97
+
98
+ function seedTwoClassRates(provider: string, model: string, inputRate = 1, outputRate = 10) {
99
+ insertPricingRow({
100
+ provider: provider as Parameters<typeof insertPricingRow>[0]["provider"],
101
+ model,
102
+ tokenClass: "input",
103
+ effectiveFrom: 1,
104
+ pricePerMillionUsd: inputRate,
105
+ });
106
+ insertPricingRow({
107
+ provider: provider as Parameters<typeof insertPricingRow>[0]["provider"],
108
+ model,
109
+ tokenClass: "output",
110
+ effectiveFrom: 1,
111
+ pricePerMillionUsd: outputRate,
112
+ });
113
+ }
114
+
115
+ describe("Phase 2 — POST /api/session-costs recompute fires for every provider", () => {
116
+ for (const provider of [
117
+ "claude",
118
+ "claude-managed",
119
+ "codex",
120
+ "pi",
121
+ "opencode",
122
+ "devin",
123
+ "gemini",
124
+ ] as const) {
125
+ test(`provider=${provider} with seeded rows → costSource='pricing-table'`, async () => {
126
+ seedTwoClassRates(provider, `${provider}-test-model`, 2, 10);
127
+
128
+ const res = await authedFetch(`/api/session-costs`, {
129
+ method: "POST",
130
+ body: JSON.stringify({
131
+ sessionId: `${provider}-recompute-1`,
132
+ agentId: testAgent.id,
133
+ totalCostUsd: 999.99, // worker-reported; expected to be overwritten
134
+ inputTokens: 1_000_000, // 1M input
135
+ outputTokens: 500_000, // 500k output
136
+ model: `${provider}-test-model`,
137
+ provider,
138
+ durationMs: 1_000,
139
+ numTurns: 1,
140
+ }),
141
+ });
142
+ expect(res.status).toBe(201);
143
+ const body = (await res.json()) as CostResponse;
144
+ expect(body.cost.costSource).toBe("pricing-table");
145
+ // 1M @ 2 + 0.5M @ 10 = $2 + $5 = $7
146
+ expect(body.cost.totalCostUsd).toBeCloseTo(7.0, 5);
147
+ });
148
+ }
149
+
150
+ test("unknown (provider, model) pair → costSource='unpriced', worker value preserved", async () => {
151
+ const res = await authedFetch(`/api/session-costs`, {
152
+ method: "POST",
153
+ body: JSON.stringify({
154
+ sessionId: "unpriced-1",
155
+ agentId: testAgent.id,
156
+ totalCostUsd: 1.23,
157
+ inputTokens: 100,
158
+ outputTokens: 50,
159
+ model: "gpt-future-2027",
160
+ provider: "codex",
161
+ durationMs: 1_000,
162
+ numTurns: 1,
163
+ }),
164
+ });
165
+ expect(res.status).toBe(201);
166
+ const body = (await res.json()) as CostResponse;
167
+ expect(body.cost.costSource).toBe("unpriced");
168
+ expect(body.cost.totalCostUsd).toBe(1.23);
169
+ });
170
+ });
@@ -24,7 +24,12 @@ type TestCostData = {
24
24
  model?: string;
25
25
  };
26
26
 
27
- describe("store-progress with cost data", () => {
27
+ // Phase 11 NOTE: the `store-progress` MCP tool no longer accepts a `costData`
28
+ // field — adapters are the sole writers of `session_costs`. These tests now
29
+ // exercise the lower-level `createSessionCost` API directly, which the runner
30
+ // still calls via `POST /api/session-costs`. They verify the DB write path
31
+ // hasn't regressed, NOT the tool's input schema.
32
+ describe("createSessionCost direct API (was: store-progress with cost data)", () => {
28
33
  let agentId: string;
29
34
  let taskId: string;
30
35
 
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { SwarmConfigPayload } from "../scripts-runtime/executors/types";
3
+ import { Redacted } from "../scripts-runtime/redacted";
4
+ import { SwarmConfig } from "../scripts-runtime/swarm-config";
5
+
6
+ const payload: SwarmConfigPayload = {
7
+ system: {
8
+ apiKey: { value: "test-api-key", isSecret: true },
9
+ agentId: { value: "agent-1", isSecret: false },
10
+ mcpBaseUrl: { value: "http://localhost:3013", isSecret: false },
11
+ },
12
+ user: {
13
+ "user-key": { value: "user-value", isSecret: true },
14
+ },
15
+ };
16
+
17
+ describe("SwarmConfig", () => {
18
+ test("hydrates system values as Redacted values with metadata", () => {
19
+ const config = new SwarmConfig(payload);
20
+ expect(Redacted.value(config.apiKey)).toBe("test-api-key");
21
+ expect(Redacted.meta(config.apiKey)).toEqual({ type: "system", isSecret: true });
22
+ expect(Redacted.value(config.agentId)).toBe("agent-1");
23
+ expect(Redacted.meta(config.mcpBaseUrl)).toEqual({ type: "system", isSecret: false });
24
+ });
25
+
26
+ test("returns user-set config values", () => {
27
+ const config = new SwarmConfig(payload);
28
+ const value = config.get("user-key");
29
+ expect(value).toBeDefined();
30
+ expect(Redacted.value(value!)).toBe("user-value");
31
+ expect(Redacted.meta(value!)).toEqual({ type: "user", isSecret: true });
32
+ });
33
+
34
+ test("missing user keys return undefined", () => {
35
+ const config = new SwarmConfig(payload);
36
+ expect(config.get("missing")).toBeUndefined();
37
+ });
38
+ });
@@ -323,9 +323,9 @@ describe("Tool Annotations & Classification", () => {
323
323
  test("registered tool count matches expected total", () => {
324
324
  const count = Object.keys(tools).length;
325
325
  // We expect all tools to be registered when all capabilities are enabled (default)
326
- // Includes 11 skill tools and 7 MCP server tools
326
+ // Includes 11 skill tools, 7 MCP server tools, and reusable script tools
327
327
  expect(count).toBeGreaterThanOrEqual(45);
328
- expect(count).toBeLessThanOrEqual(95);
328
+ expect(count).toBeLessThanOrEqual(100);
329
329
  });
330
330
 
331
331
  test("core tools are fewer than deferred tools", () => {
@@ -151,6 +151,31 @@ describe("toolCallToProgress", () => {
151
151
  expect(result).toBe("💬 Posting to Slack");
152
152
  });
153
153
 
154
+ test("agent-swarm:script-search has pretty label", () => {
155
+ const result = toolCallToProgress("mcp__agent-swarm__script-search", {});
156
+ expect(result).toBe("📜 Searching scripts");
157
+ });
158
+
159
+ test("agent-swarm:script-run has pretty label", () => {
160
+ const result = toolCallToProgress("mcp__agent-swarm__script-run", {});
161
+ expect(result).toBe("📜 Running script");
162
+ });
163
+
164
+ test("agent-swarm:script-upsert has pretty label", () => {
165
+ const result = toolCallToProgress("mcp__agent-swarm__script-upsert", {});
166
+ expect(result).toBe("📜 Saving script");
167
+ });
168
+
169
+ test("agent-swarm:script-delete has pretty label", () => {
170
+ const result = toolCallToProgress("mcp__agent-swarm__script-delete", {});
171
+ expect(result).toBe("📜 Deleting script");
172
+ });
173
+
174
+ test("agent-swarm:script-query-types has pretty label", () => {
175
+ const result = toolCallToProgress("mcp__agent-swarm__script-query-types", {});
176
+ expect(result).toBe("📜 Reading script types");
177
+ });
178
+
154
179
  // --- Agent-swarm MCP tool NOT in lookup (humanized fallback) ---
155
180
 
156
181
  test("unknown agent-swarm tool gets humanized fallback", () => {
@@ -191,6 +216,11 @@ describe("toolCallToProgress", () => {
191
216
  expect(result).toBe("🗃️ Querying database");
192
217
  });
193
218
 
219
+ test("bare agent-swarm script-run has pretty label", () => {
220
+ const result = toolCallToProgress("script-run", {});
221
+ expect(result).toBe("📜 Running script");
222
+ });
223
+
194
224
  test("codex MCP args with agent-swarm server use pretty labels", () => {
195
225
  const result = toolCallToProgress("send-task", {
196
226
  server: "agent-swarm",