@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.
- package/openapi.json +496 -32
- package/package.json +14 -6
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +102 -31
- package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/pricing-normalize.ts +81 -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/be/seed-pricing.ts +293 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +21 -4
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +663 -246
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/context.ts +6 -2
- package/src/http/index.ts +126 -68
- 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/http/session-data.ts +74 -23
- package/src/linear/outbound.ts +9 -2
- package/src/otel-impl.ts +200 -0
- package/src/otel.ts +132 -0
- package/src/providers/claude-adapter.ts +52 -6
- package/src/providers/claude-managed-adapter.ts +43 -17
- package/src/providers/claude-managed-pricing.ts +34 -0
- package/src/providers/codex-adapter.ts +38 -27
- package/src/providers/codex-models.ts +22 -3
- package/src/providers/devin-adapter.ts +11 -0
- package/src/providers/opencode-adapter.ts +31 -7
- package/src/providers/pi-mono-adapter.ts +39 -7
- package/src/providers/pricing-sources.md +52 -0
- package/src/providers/swarm-events-shared.ts +8 -4
- package/src/providers/types.ts +33 -10
- 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 +18 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/claude-managed-adapter.test.ts +17 -3
- package/src/tests/claude-managed-setup.test.ts +10 -1
- package/src/tests/codex-adapter.test.ts +20 -19
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/context-snapshot.test.ts +2 -2
- package/src/tests/context-window.test.ts +65 -1
- package/src/tests/devin-adapter.test.ts +2 -0
- package/src/tests/http/context-routes.test.ts +161 -0
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/migration-063-schema-relax.test.ts +109 -0
- package/src/tests/opencode-adapter.test.ts +146 -1
- package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
- package/src/tests/pages-view-count.test.ts +30 -5
- package/src/tests/providers/codex-cost.test.ts +18 -0
- package/src/tests/providers/opencode-cost.test.ts +74 -0
- package/src/tests/providers/pi-cost.test.ts +128 -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 +54 -1
- package/src/tests/session-costs-codex-recompute.test.ts +35 -22
- package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
- package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
- package/src/tests/store-progress-cost.test.ts +6 -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/store-progress.ts +16 -60
- package/src/tools/tool-config.ts +7 -0
- package/src/tools/utils.ts +65 -12
- package/src/types.ts +122 -10
- package/src/utils/api-key.ts +28 -0
- package/src/utils/context-window.ts +104 -4
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +29 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- package/src/workflows/resume.ts +43 -5
|
@@ -0,0 +1,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
|
+
});
|
package/src/tools/memory-rate.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
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
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
// duplicate
|
|
261
|
-
//
|
|
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,
|