@desplega.ai/agent-swarm 1.87.0 → 1.88.0
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/README.md +2 -1
- package/openapi.json +13 -1
- package/package.json +5 -5
- package/src/be/db.ts +49 -7
- package/src/be/migrations/080_skill_system_defaults.sql +8 -0
- package/src/be/modelsdev-cache.json +1123 -1034
- package/src/be/seed/registry.ts +3 -2
- package/src/be/seed-skills/index.ts +172 -0
- package/src/cli.tsx +33 -4
- package/src/commands/e2b-stack-wizard.tsx +394 -0
- package/src/commands/e2b.ts +1352 -53
- package/src/commands/onboard/dashboard-url.ts +29 -0
- package/src/commands/onboard/steps/post-dashboard.tsx +3 -1
- package/src/commands/onboard.tsx +3 -1
- package/src/commands/runner.ts +1 -0
- package/src/e2b/dispatch.ts +234 -18
- package/src/http/memory.ts +13 -1
- package/src/http/skills.ts +53 -0
- package/src/http/webhooks.ts +75 -0
- package/src/integrations/kapso/client.ts +82 -0
- package/src/memory/automatic-task-gate.ts +47 -0
- package/src/prompts/base-prompt.ts +16 -1
- package/src/prompts/session-templates.ts +51 -0
- package/src/providers/claude-adapter.ts +19 -0
- package/src/providers/codex-adapter.ts +22 -0
- package/src/providers/ctx-mode-env.ts +10 -0
- package/src/providers/opencode-adapter.ts +50 -1
- package/src/slack/blocks.ts +12 -4
- package/src/slack/watcher.ts +3 -3
- package/src/telemetry.ts +14 -1
- package/src/templates.d.ts +4 -0
- package/src/tests/base-prompt.test.ts +41 -0
- package/src/tests/claude-adapter.test.ts +86 -1
- package/src/tests/codex-adapter.test.ts +89 -0
- package/src/tests/e2b-dispatch.test.ts +603 -11
- package/src/tests/http-api-integration.test.ts +113 -0
- package/src/tests/kapso-client.test.ts +74 -1
- package/src/tests/kapso-inbound.test.ts +60 -2
- package/src/tests/opencode-adapter.test.ts +95 -0
- package/src/tests/prompt-template-session.test.ts +4 -2
- package/src/tests/self-improvement.test.ts +89 -0
- package/src/tests/skill-update-scope.test.ts +88 -1
- package/src/tests/slack-blocks.test.ts +15 -0
- package/src/tests/system-default-skills.test.ts +119 -0
- package/src/tests/telemetry-init.test.ts +86 -0
- package/src/tools/skills/skill-delete.ts +14 -0
- package/src/tools/skills/skill-update.ts +14 -0
- package/src/tools/store-progress.ts +19 -5
- package/src/types.ts +1 -0
- package/templates/skills/artifacts/config.json +1 -0
- package/templates/skills/kv-storage/config.json +1 -0
- package/templates/skills/pages/config.json +1 -0
- package/templates/skills/scheduled-task-resilience/config.json +1 -0
- package/templates/skills/swarm-scripts/SKILL.md +91 -0
- package/templates/skills/swarm-scripts/config.json +14 -0
- package/templates/skills/swarm-scripts/content.md +86 -0
- package/templates/skills/workflow-iterate/config.json +1 -0
- package/templates/skills/workflow-structured-output/config.json +1 -0
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
createAgent,
|
|
6
|
+
createSkill,
|
|
7
|
+
getAgentSkills,
|
|
8
|
+
getDb,
|
|
9
|
+
getSystemDefaultSkills,
|
|
10
|
+
initDb,
|
|
11
|
+
toggleAgentSkill,
|
|
12
|
+
} from "../be/db";
|
|
13
|
+
import { runSeeder } from "../be/seed";
|
|
14
|
+
import { loadSeedSkills, skillsSeeder } from "../be/seed-skills";
|
|
15
|
+
|
|
16
|
+
const TEST_DB_PATH = `./test-system-default-skills-${process.pid}.sqlite`;
|
|
17
|
+
|
|
18
|
+
async function removeDbFiles(path: string): Promise<void> {
|
|
19
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
20
|
+
await unlink(path + suffix).catch(() => {});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("system-default skills", () => {
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
27
|
+
initDb(TEST_DB_PATH);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
closeDb();
|
|
32
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("seed catalog includes swarm-scripts and marks built-in defaults", async () => {
|
|
36
|
+
const skills = loadSeedSkills();
|
|
37
|
+
const names = skills.map((skill) => skill.name);
|
|
38
|
+
|
|
39
|
+
expect(names).toContain("swarm-scripts");
|
|
40
|
+
expect(skills.find((skill) => skill.name === "swarm-scripts")?.systemDefault).toBe(true);
|
|
41
|
+
expect(skills.find((skill) => skill.name === "kv-storage")?.systemDefault).toBe(true);
|
|
42
|
+
expect(skills.find((skill) => skill.name === "pages")?.systemDefault).toBe(true);
|
|
43
|
+
|
|
44
|
+
const result = await runSeeder(skillsSeeder, { quiet: true });
|
|
45
|
+
expect(result.failed).toEqual([]);
|
|
46
|
+
|
|
47
|
+
const defaults = getSystemDefaultSkills().map((skill) => skill.name);
|
|
48
|
+
expect(defaults).toContain("swarm-scripts");
|
|
49
|
+
expect(defaults).toContain("kv-storage");
|
|
50
|
+
expect(defaults).toContain("pages");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("existing agents see system-default skills through the self-healing view", () => {
|
|
54
|
+
const existingAgent = createAgent({
|
|
55
|
+
name: "Existing Default Skill Worker",
|
|
56
|
+
description: "Created after seeded defaults",
|
|
57
|
+
role: "worker",
|
|
58
|
+
isLead: false,
|
|
59
|
+
status: "idle",
|
|
60
|
+
maxTasks: 1,
|
|
61
|
+
capabilities: [],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const manualDefault = createSkill({
|
|
65
|
+
name: "manual-system-default",
|
|
66
|
+
description: "Manual default",
|
|
67
|
+
content: "---\nname: manual-system-default\ndescription: Manual default\n---\nBody.",
|
|
68
|
+
type: "personal",
|
|
69
|
+
scope: "swarm",
|
|
70
|
+
systemDefault: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const skills = getAgentSkills(existingAgent.id);
|
|
74
|
+
expect(skills.map((skill) => skill.name)).toContain("manual-system-default");
|
|
75
|
+
expect(skills.find((skill) => skill.id === manualDefault.id)?.isActive).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("new agents get concrete agent_skills rows for system defaults", () => {
|
|
79
|
+
const beforeAgent = createAgent({
|
|
80
|
+
name: "Concrete Install Worker",
|
|
81
|
+
description: "Created with defaults present",
|
|
82
|
+
role: "worker",
|
|
83
|
+
isLead: false,
|
|
84
|
+
status: "idle",
|
|
85
|
+
maxTasks: 1,
|
|
86
|
+
capabilities: [],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const row = getDb()
|
|
90
|
+
.prepare<{ count: number }, [string]>(
|
|
91
|
+
`SELECT COUNT(*) AS count
|
|
92
|
+
FROM agent_skills
|
|
93
|
+
WHERE agentId = ?
|
|
94
|
+
AND skillId IN (SELECT id FROM skills WHERE systemDefault = 1)`,
|
|
95
|
+
)
|
|
96
|
+
.get(beforeAgent.id);
|
|
97
|
+
|
|
98
|
+
expect(row?.count ?? 0).toBeGreaterThan(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("system-default skills remain visible even if an install row is toggled inactive", () => {
|
|
102
|
+
const agent = createAgent({
|
|
103
|
+
name: "Inactive Default Worker",
|
|
104
|
+
description: "Tests self-healing union",
|
|
105
|
+
role: "worker",
|
|
106
|
+
isLead: false,
|
|
107
|
+
status: "idle",
|
|
108
|
+
maxTasks: 1,
|
|
109
|
+
capabilities: [],
|
|
110
|
+
});
|
|
111
|
+
const skill = getSystemDefaultSkills().find((entry) => entry.name === "swarm-scripts");
|
|
112
|
+
expect(skill).toBeDefined();
|
|
113
|
+
|
|
114
|
+
toggleAgentSkill(agent.id, skill!.id, false);
|
|
115
|
+
const skills = getAgentSkills(agent.id);
|
|
116
|
+
|
|
117
|
+
expect(skills.find((entry) => entry.id === skill!.id)?.isActive).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
_getInstallationIdForTests,
|
|
4
|
+
_isE2bSandbox,
|
|
4
5
|
_resetTelemetryStateForTests,
|
|
5
6
|
_resolveCloudMode,
|
|
6
7
|
initTelemetry,
|
|
@@ -302,6 +303,91 @@ describe("initTelemetry", () => {
|
|
|
302
303
|
});
|
|
303
304
|
});
|
|
304
305
|
|
|
306
|
+
describe("_isE2bSandbox detection", () => {
|
|
307
|
+
afterEach(() => {
|
|
308
|
+
delete process.env.E2B_SANDBOX_ID;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("returns true when E2B_SANDBOX_ID is set", () => {
|
|
312
|
+
process.env.E2B_SANDBOX_ID = "sbx_abc123";
|
|
313
|
+
expect(_isE2bSandbox()).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("returns false when E2B_SANDBOX_ID is unset", () => {
|
|
317
|
+
delete process.env.E2B_SANDBOX_ID;
|
|
318
|
+
expect(_isE2bSandbox()).toBe(false);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("returns false when E2B_SANDBOX_ID is empty string", () => {
|
|
322
|
+
process.env.E2B_SANDBOX_ID = "";
|
|
323
|
+
expect(_isE2bSandbox()).toBe(false);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("track() ships is_e2b in properties", () => {
|
|
328
|
+
const originalFetch = globalThis.fetch;
|
|
329
|
+
let captured: Record<string, unknown> | null = null;
|
|
330
|
+
|
|
331
|
+
beforeEach(() => {
|
|
332
|
+
captured = null;
|
|
333
|
+
globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
|
|
334
|
+
captured = init?.body ? JSON.parse(init.body) : null;
|
|
335
|
+
return new Response(null, { status: 204 });
|
|
336
|
+
}) as typeof fetch;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
afterEach(() => {
|
|
340
|
+
globalThis.fetch = originalFetch;
|
|
341
|
+
delete process.env.E2B_SANDBOX_ID;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("properties.is_e2b=true when E2B_SANDBOX_ID is set at init", async () => {
|
|
345
|
+
process.env.E2B_SANDBOX_ID = "sbx_test123";
|
|
346
|
+
await initTelemetry(
|
|
347
|
+
"api-server",
|
|
348
|
+
async () => "install_e2b_test",
|
|
349
|
+
async () => {},
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
track({ event: "server.started", properties: { port: 3013 } });
|
|
353
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
354
|
+
|
|
355
|
+
const properties = (captured as { properties: Record<string, unknown> }).properties;
|
|
356
|
+
expect(properties.is_e2b).toBe(true);
|
|
357
|
+
expect(properties.port).toBe(3013);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("properties.is_e2b=false when E2B_SANDBOX_ID is unset at init", async () => {
|
|
361
|
+
delete process.env.E2B_SANDBOX_ID;
|
|
362
|
+
await initTelemetry(
|
|
363
|
+
"api-server",
|
|
364
|
+
async () => "install_no_e2b",
|
|
365
|
+
async () => {},
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
track({ event: "test.event", properties: {} });
|
|
369
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
370
|
+
|
|
371
|
+
const properties = (captured as { properties: Record<string, unknown> }).properties;
|
|
372
|
+
expect(properties.is_e2b).toBe(false);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("caller properties cannot override is_e2b", async () => {
|
|
376
|
+
process.env.E2B_SANDBOX_ID = "sbx_override_test";
|
|
377
|
+
await initTelemetry(
|
|
378
|
+
"api-server",
|
|
379
|
+
async () => "install_e2b_override",
|
|
380
|
+
async () => {},
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
track({ event: "test.event", properties: { is_e2b: false } });
|
|
384
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
385
|
+
|
|
386
|
+
const properties = (captured as { properties: Record<string, unknown> }).properties;
|
|
387
|
+
expect(properties.is_e2b).toBe(true);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
305
391
|
describe("track() ships is_cloud in properties", () => {
|
|
306
392
|
const originalFetch = globalThis.fetch;
|
|
307
393
|
let captured: Record<string, unknown> | null = null;
|
|
@@ -3,6 +3,9 @@ import * as z from "zod";
|
|
|
3
3
|
import { deleteSkill, getAgentById, getSkillById } from "@/be/db";
|
|
4
4
|
import { createToolRegistrar } from "@/tools/utils";
|
|
5
5
|
|
|
6
|
+
const SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE =
|
|
7
|
+
"This skill is system-managed and cannot be edited from the UI; it is re-seeded on each start. Fork it under a new name to customize.";
|
|
8
|
+
|
|
6
9
|
export const registerSkillDeleteTool = (server: McpServer) => {
|
|
7
10
|
createToolRegistrar(server)(
|
|
8
11
|
"skill-delete",
|
|
@@ -51,6 +54,17 @@ export const registerSkillDeleteTool = (server: McpServer) => {
|
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
if (existing.systemDefault) {
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE }],
|
|
60
|
+
structuredContent: {
|
|
61
|
+
yourAgentId: requestInfo.agentId,
|
|
62
|
+
success: false,
|
|
63
|
+
message: SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
const deleted = deleteSkill(args.skillId);
|
|
55
69
|
return {
|
|
56
70
|
content: [
|
|
@@ -4,6 +4,9 @@ import { getAgentById, getSkillById, updateSkill } from "@/be/db";
|
|
|
4
4
|
import { parseSkillContent } from "@/be/skill-parser";
|
|
5
5
|
import { createToolRegistrar } from "@/tools/utils";
|
|
6
6
|
|
|
7
|
+
const SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE =
|
|
8
|
+
"This skill is system-managed and cannot be edited from the UI; it is re-seeded on each start. Fork it under a new name to customize.";
|
|
9
|
+
|
|
7
10
|
export const registerSkillUpdateTool = (server: McpServer) => {
|
|
8
11
|
createToolRegistrar(server)(
|
|
9
12
|
"skill-update",
|
|
@@ -77,6 +80,17 @@ export const registerSkillUpdateTool = (server: McpServer) => {
|
|
|
77
80
|
};
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
if (existing.systemDefault && (args.content !== undefined || args.scope !== undefined)) {
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE }],
|
|
86
|
+
structuredContent: {
|
|
87
|
+
yourAgentId: requestInfo.agentId,
|
|
88
|
+
success: false,
|
|
89
|
+
message: SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
80
94
|
const updates: Parameters<typeof updateSkill>[1] = {};
|
|
81
95
|
|
|
82
96
|
if (args.content !== undefined) {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { getEmbeddingProvider, getMemoryStore } from "@/be/memory";
|
|
17
17
|
import { getRetrievalsForTask } from "@/be/memory/raters/retrieval";
|
|
18
18
|
import { runServerRaters } from "@/be/memory/raters/run-server-raters";
|
|
19
|
+
import { shouldPersistTaskCompletionMemory } from "@/memory/automatic-task-gate";
|
|
19
20
|
import { createWorkerTaskFollowUp } from "@/tasks/worker-follow-up";
|
|
20
21
|
import { createToolRegistrar } from "@/tools/utils";
|
|
21
22
|
import { AgentTaskSchema, AttachmentInputSchema, isTerminalTaskStatus } from "@/types";
|
|
@@ -56,6 +57,12 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
56
57
|
.describe(
|
|
57
58
|
"Pointer-based artifacts produced by this step — agent-fs path, URL, shared-fs path, or swarm Page. No inline file data; upload to agent-fs first and attach by path. May be sent on any call (progress or completion) and accumulates across calls; duplicates are de-duped by sha256 (when present) or by (kind, pointer, name).",
|
|
58
59
|
),
|
|
60
|
+
persistMemory: z
|
|
61
|
+
.boolean()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe(
|
|
64
|
+
"Opt in to task_completion memory persistence for automatic/recurring tasks. Manual tasks are persisted by default; scheduled, system, heartbeat/boot-triage, monitor, and digest tasks are skipped unless this is true.",
|
|
65
|
+
),
|
|
59
66
|
// Phase 11: `costData` removed. The harness adapter is the sole
|
|
60
67
|
// writer of `session_costs` (see POST /api/session-costs in the
|
|
61
68
|
// runner). If a payload still includes the field, Zod's
|
|
@@ -75,7 +82,7 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
75
82
|
}),
|
|
76
83
|
},
|
|
77
84
|
async (
|
|
78
|
-
{ taskId, progress, status, output, failureReason, attachments },
|
|
85
|
+
{ taskId, progress, status, output, failureReason, attachments, persistMemory },
|
|
79
86
|
requestInfo,
|
|
80
87
|
_meta,
|
|
81
88
|
) => {
|
|
@@ -320,14 +327,19 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
320
327
|
|
|
321
328
|
const result = txn();
|
|
322
329
|
|
|
330
|
+
const shouldRunTerminalSideEffects =
|
|
331
|
+
(status === "completed" || status === "failed") &&
|
|
332
|
+
result.success &&
|
|
333
|
+
result.task &&
|
|
334
|
+
!("wasNoOp" in result && result.wasNoOp);
|
|
335
|
+
|
|
323
336
|
// Index completed and failed tasks as memory (async, non-blocking).
|
|
324
337
|
// Skip on no-op (idempotent re-call on terminal task) to avoid duplicate
|
|
325
338
|
// memory entries / vector index pollution.
|
|
339
|
+
// Automatic/recurring tasks are noisy by default; require explicit opt-in.
|
|
326
340
|
if (
|
|
327
|
-
|
|
328
|
-
result.
|
|
329
|
-
result.task &&
|
|
330
|
-
!("wasNoOp" in result && result.wasNoOp)
|
|
341
|
+
shouldRunTerminalSideEffects &&
|
|
342
|
+
shouldPersistTaskCompletionMemory(result.task, persistMemory)
|
|
331
343
|
) {
|
|
332
344
|
(async () => {
|
|
333
345
|
try {
|
|
@@ -384,7 +396,9 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
384
396
|
// Non-blocking — task completion memory failure should not affect task status
|
|
385
397
|
}
|
|
386
398
|
})();
|
|
399
|
+
}
|
|
387
400
|
|
|
401
|
+
if (shouldRunTerminalSideEffects) {
|
|
388
402
|
// Memory rater v1.5 — fire server-side raters on task completion.
|
|
389
403
|
// Plan: thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-2.md §5
|
|
390
404
|
//
|
package/src/types.ts
CHANGED
|
@@ -1600,6 +1600,7 @@ export const SkillSchema = z.object({
|
|
|
1600
1600
|
userInvocable: z.boolean(),
|
|
1601
1601
|
version: z.number(),
|
|
1602
1602
|
isEnabled: z.boolean(),
|
|
1603
|
+
systemDefault: z.boolean(),
|
|
1603
1604
|
createdAt: z.string(),
|
|
1604
1605
|
lastUpdatedAt: z.string(),
|
|
1605
1606
|
lastFetchedAt: z.string().nullable(),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: swarm-scripts
|
|
3
|
+
description: Use swarm scripts for bulk SDK calls, repetitive fan-out, and context-efficient data processing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Swarm Scripts
|
|
7
|
+
|
|
8
|
+
Use swarm scripts when direct tool calls would create repetitive work, flood the context window, or require deterministic data processing across many records. Scripts run out-of-process with a typed Swarm SDK and return only the final result to your context.
|
|
9
|
+
|
|
10
|
+
## Decision Rubric
|
|
11
|
+
|
|
12
|
+
| Situation | Use |
|
|
13
|
+
|---|---|
|
|
14
|
+
| 1-10 SDK calls, result fits in context | Direct tool call |
|
|
15
|
+
| 10+ items or bulk fan-out SDK operations | Script |
|
|
16
|
+
| Heavy fetch, parse, transform, or aggregation | Script or context-mode |
|
|
17
|
+
| Single expensive web fetch | `ctx_fetch_and_index` |
|
|
18
|
+
| Multi-agent parallel work | Workflow |
|
|
19
|
+
| Logic needed across sessions or agents | Named script |
|
|
20
|
+
|
|
21
|
+
## Loading Script Tools
|
|
22
|
+
|
|
23
|
+
The script tools are deferred. Before authoring or running a script, load the relevant tools with ToolSearch:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
script-query-types
|
|
27
|
+
script-upsert
|
|
28
|
+
script-run
|
|
29
|
+
script-search
|
|
30
|
+
script-delete
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use `script-query-types` before non-trivial work so the script matches the live `swarm-sdk.d.ts` and stdlib signatures.
|
|
34
|
+
|
|
35
|
+
## Inline Script Pattern
|
|
36
|
+
|
|
37
|
+
Use `script-run` with inline source for one-off work:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
export default async function main(args: { status: string; limit: number }, ctx) {
|
|
41
|
+
const { swarm, logger } = ctx;
|
|
42
|
+
const result = await swarm.task_list({ status: args.status, limit: args.limit });
|
|
43
|
+
logger.info(`Fetched ${result.tasks.length} tasks`);
|
|
44
|
+
return {
|
|
45
|
+
total: result.tasks.length,
|
|
46
|
+
tasks: result.tasks.map((task) => ({
|
|
47
|
+
id: task.id,
|
|
48
|
+
status: task.status,
|
|
49
|
+
title: task.task.slice(0, 120),
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Keep logs useful but compact. The value returned from `main` is what comes back to your context.
|
|
56
|
+
|
|
57
|
+
## Named Script Pattern
|
|
58
|
+
|
|
59
|
+
Use `script-upsert` when the same logic is likely to be reused by another task or agent. Give the script a searchable name, a concrete description, and an intent that explains when to choose it.
|
|
60
|
+
|
|
61
|
+
Good named scripts:
|
|
62
|
+
|
|
63
|
+
- Aggregate failures by agent, schedule, or error family.
|
|
64
|
+
- Fetch and normalize a third-party API response.
|
|
65
|
+
- Fan out over many swarm tasks, memories, repos, or schedules.
|
|
66
|
+
- Convert noisy JSON or HTML into a compact summary.
|
|
67
|
+
|
|
68
|
+
## SDK And Context Gotchas
|
|
69
|
+
|
|
70
|
+
- `agentId` is propagated to scripts via the `X-Agent-ID` header, so SDK calls run as the invoking agent.
|
|
71
|
+
- `taskId` is not ambient. If a script needs to call `ctx.swarm.task_storeProgress`, pass `taskId` explicitly in `args`.
|
|
72
|
+
- Scripts invoked from a workflow script node may run with a workflow identity rather than a human or worker agent identity.
|
|
73
|
+
- Return compact structured data. Do not return raw logs, full HTML, huge JSON arrays, or large file contents.
|
|
74
|
+
- For a single large web fetch, prefer context-mode `ctx_fetch_and_index`; for repeated fetch/parse/aggregate work, prefer a script.
|
|
75
|
+
|
|
76
|
+
## Progress Updates From Scripts
|
|
77
|
+
|
|
78
|
+
Thread task identity explicitly:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
export default async function main(args: { taskId: string; items: string[] }, ctx) {
|
|
82
|
+
const { swarm } = ctx;
|
|
83
|
+
await swarm.task_storeProgress({
|
|
84
|
+
taskId: args.taskId,
|
|
85
|
+
progress: `Processing ${args.items.length} items with a script`,
|
|
86
|
+
});
|
|
87
|
+
return { processed: args.items.length };
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Do not assume the runtime can infer the current task.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "skill",
|
|
3
|
+
"name": "swarm-scripts",
|
|
4
|
+
"displayName": "Swarm Scripts",
|
|
5
|
+
"slug": "swarm-scripts",
|
|
6
|
+
"title": "Swarm Scripts",
|
|
7
|
+
"description": "Use swarm scripts for bulk SDK calls, repetitive fan-out, and context-efficient data processing.",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"category": "skills",
|
|
10
|
+
"placeholders": [],
|
|
11
|
+
"runAllSeedersCandidate": true,
|
|
12
|
+
"systemDefault": true,
|
|
13
|
+
"tags": ["scripts", "automation", "context"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Swarm Scripts
|
|
2
|
+
|
|
3
|
+
Use swarm scripts when direct tool calls would create repetitive work, flood the context window, or require deterministic data processing across many records. Scripts run out-of-process with a typed Swarm SDK and return only the final result to your context.
|
|
4
|
+
|
|
5
|
+
## Decision Rubric
|
|
6
|
+
|
|
7
|
+
| Situation | Use |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 1-10 SDK calls, result fits in context | Direct tool call |
|
|
10
|
+
| 10+ items or bulk fan-out SDK operations | Script |
|
|
11
|
+
| Heavy fetch, parse, transform, or aggregation | Script or context-mode |
|
|
12
|
+
| Single expensive web fetch | `ctx_fetch_and_index` |
|
|
13
|
+
| Multi-agent parallel work | Workflow |
|
|
14
|
+
| Logic needed across sessions or agents | Named script |
|
|
15
|
+
|
|
16
|
+
## Loading Script Tools
|
|
17
|
+
|
|
18
|
+
The script tools are deferred. Before authoring or running a script, load the relevant tools with ToolSearch:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
script-query-types
|
|
22
|
+
script-upsert
|
|
23
|
+
script-run
|
|
24
|
+
script-search
|
|
25
|
+
script-delete
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use `script-query-types` before non-trivial work so the script matches the live `swarm-sdk.d.ts` and stdlib signatures.
|
|
29
|
+
|
|
30
|
+
## Inline Script Pattern
|
|
31
|
+
|
|
32
|
+
Use `script-run` with inline source for one-off work:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
export default async function main(args: { status: string; limit: number }, ctx) {
|
|
36
|
+
const { swarm, logger } = ctx;
|
|
37
|
+
const result = await swarm.task_list({ status: args.status, limit: args.limit });
|
|
38
|
+
logger.info(`Fetched ${result.tasks.length} tasks`);
|
|
39
|
+
return {
|
|
40
|
+
total: result.tasks.length,
|
|
41
|
+
tasks: result.tasks.map((task) => ({
|
|
42
|
+
id: task.id,
|
|
43
|
+
status: task.status,
|
|
44
|
+
title: task.task.slice(0, 120),
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Keep logs useful but compact. The value returned from `main` is what comes back to your context.
|
|
51
|
+
|
|
52
|
+
## Named Script Pattern
|
|
53
|
+
|
|
54
|
+
Use `script-upsert` when the same logic is likely to be reused by another task or agent. Give the script a searchable name, a concrete description, and an intent that explains when to choose it.
|
|
55
|
+
|
|
56
|
+
Good named scripts:
|
|
57
|
+
|
|
58
|
+
- Aggregate failures by agent, schedule, or error family.
|
|
59
|
+
- Fetch and normalize a third-party API response.
|
|
60
|
+
- Fan out over many swarm tasks, memories, repos, or schedules.
|
|
61
|
+
- Convert noisy JSON or HTML into a compact summary.
|
|
62
|
+
|
|
63
|
+
## SDK And Context Gotchas
|
|
64
|
+
|
|
65
|
+
- `agentId` is propagated to scripts via the `X-Agent-ID` header, so SDK calls run as the invoking agent.
|
|
66
|
+
- `taskId` is not ambient. If a script needs to call `ctx.swarm.task_storeProgress`, pass `taskId` explicitly in `args`.
|
|
67
|
+
- Scripts invoked from a workflow script node may run with a workflow identity rather than a human or worker agent identity.
|
|
68
|
+
- Return compact structured data. Do not return raw logs, full HTML, huge JSON arrays, or large file contents.
|
|
69
|
+
- For a single large web fetch, prefer context-mode `ctx_fetch_and_index`; for repeated fetch/parse/aggregate work, prefer a script.
|
|
70
|
+
|
|
71
|
+
## Progress Updates From Scripts
|
|
72
|
+
|
|
73
|
+
Thread task identity explicitly:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
export default async function main(args: { taskId: string; items: string[] }, ctx) {
|
|
77
|
+
const { swarm } = ctx;
|
|
78
|
+
await swarm.task_storeProgress({
|
|
79
|
+
taskId: args.taskId,
|
|
80
|
+
progress: `Processing ${args.items.length} items with a script`,
|
|
81
|
+
});
|
|
82
|
+
return { processed: args.items.length };
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Do not assume the runtime can infer the current task.
|