@desplega.ai/agent-swarm 1.91.0 → 1.92.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 (114) hide show
  1. package/README.md +3 -2
  2. package/openapi.json +1005 -152
  3. package/package.json +6 -6
  4. package/plugin/skills/pages/SKILL.md +5 -2
  5. package/src/be/db.ts +662 -19
  6. package/src/be/memory/constants.ts +2 -1
  7. package/src/be/memory/providers/openai-embedding.ts +2 -5
  8. package/src/be/memory/providers/sqlite-store.ts +293 -76
  9. package/src/be/memory/types.ts +35 -0
  10. package/src/be/migrations/083_script_workflows.sql +51 -0
  11. package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
  12. package/src/be/migrations/085_script_runs_kind.sql +9 -0
  13. package/src/be/migrations/086_pages_default_authed.sql +64 -0
  14. package/src/be/migrations/087_skill_files.sql +19 -0
  15. package/src/be/modelsdev-cache.json +42310 -38617
  16. package/src/be/scripts/typecheck.ts +49 -0
  17. package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
  18. package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
  19. package/src/be/seed-scripts/catalog/compound-insights.ts +310 -6
  20. package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
  21. package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
  22. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +506 -0
  23. package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
  24. package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
  25. package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
  26. package/src/be/seed-scripts/catalog/tool-usage.ts +6 -3
  27. package/src/be/seed-scripts/index.ts +51 -5
  28. package/src/be/seed-skills/index.ts +3 -3
  29. package/src/be/skill-sync.ts +91 -7
  30. package/src/be/swarm-config-guard.ts +17 -0
  31. package/src/commands/runner.ts +49 -4
  32. package/src/heartbeat/templates.ts +20 -16
  33. package/src/http/db-query.ts +20 -5
  34. package/src/http/index.ts +51 -7
  35. package/src/http/mcp-user.ts +23 -0
  36. package/src/http/mcp.ts +58 -0
  37. package/src/http/memory.ts +58 -0
  38. package/src/http/pages.ts +1 -1
  39. package/src/http/script-runs.ts +557 -0
  40. package/src/http/scripts.ts +39 -2
  41. package/src/http/skills.ts +225 -0
  42. package/src/prompts/session-templates.ts +24 -4
  43. package/src/providers/claude-adapter.ts +107 -28
  44. package/src/script-workflows/executor.ts +110 -0
  45. package/src/script-workflows/harness.ts +73 -0
  46. package/src/script-workflows/label-lint.ts +51 -0
  47. package/src/script-workflows/limits.ts +22 -0
  48. package/src/script-workflows/supervisor.ts +139 -0
  49. package/src/script-workflows/workflow-ctx.ts +209 -0
  50. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  51. package/src/scripts-runtime/swarm-sdk.ts +13 -0
  52. package/src/scripts-runtime/types/stdlib.d.ts +61 -0
  53. package/src/scripts-runtime/types/swarm-sdk.d.ts +61 -0
  54. package/src/server.ts +4 -0
  55. package/src/slack/handlers.ts +11 -4
  56. package/src/slack/message-text.ts +98 -0
  57. package/src/slack/thread-buffer.ts +5 -3
  58. package/src/tests/claude-adapter-binary.test.ts +271 -74
  59. package/src/tests/create-page-tool.test.ts +19 -2
  60. package/src/tests/db-query.test.ts +28 -0
  61. package/src/tests/error-tracker.test.ts +121 -0
  62. package/src/tests/harness-provider-resolution.test.ts +33 -0
  63. package/src/tests/heartbeat-checklist.test.ts +36 -0
  64. package/src/tests/mcp-tools.test.ts +6 -0
  65. package/src/tests/mcp-transport-gc.test.ts +58 -0
  66. package/src/tests/memory-health-endpoint.test.ts +78 -0
  67. package/src/tests/memory-store.test.ts +221 -1
  68. package/src/tests/pages-http.test.ts +20 -2
  69. package/src/tests/pages-storage.test.ts +26 -0
  70. package/src/tests/prompt-template-session.test.ts +34 -5
  71. package/src/tests/script-runs-http.test.ts +278 -0
  72. package/src/tests/script-workflows-label-lint.test.ts +43 -0
  73. package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
  74. package/src/tests/scripts-mcp-e2e.test.ts +102 -2
  75. package/src/tests/seed-scripts.test.ts +468 -3
  76. package/src/tests/skill-files-http.test.ts +171 -0
  77. package/src/tests/skill-files.test.ts +162 -0
  78. package/src/tests/skill-get-file-tool.test.ts +110 -0
  79. package/src/tests/skill-sync.test.ts +125 -6
  80. package/src/tests/slack-message-text.test.ts +250 -0
  81. package/src/tests/system-default-skills.test.ts +40 -0
  82. package/src/tools/create-page.ts +2 -2
  83. package/src/tools/db-query.ts +16 -6
  84. package/src/tools/script-runs.ts +123 -0
  85. package/src/tools/skills/index.ts +1 -0
  86. package/src/tools/skills/skill-get-file.ts +80 -0
  87. package/src/tools/slack-read.ts +12 -3
  88. package/src/tools/tool-config.ts +6 -2
  89. package/src/types.ts +72 -0
  90. package/src/utils/error-tracker.ts +40 -1
  91. package/src/utils/internal-ai/complete-structured.ts +10 -4
  92. package/src/workflows/executors/raw-llm.ts +76 -59
  93. package/templates/schedules/daily-blocker-digest/content.md +68 -54
  94. package/templates/schedules/daily-compounding-reflection/content.md +4 -4
  95. package/templates/schedules/daily-hn-briefing/content.md +5 -5
  96. package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
  97. package/templates/schedules/gtm-weekly-review/content.md +9 -9
  98. package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
  99. package/templates/skills/agentmail-sending/content.md +6 -7
  100. package/templates/skills/desloppify/content.md +8 -9
  101. package/templates/skills/jira-interaction/content.md +25 -33
  102. package/templates/skills/kapso-whatsapp/content.md +29 -30
  103. package/templates/skills/linear-interaction/content.md +8 -9
  104. package/templates/skills/pages/content.md +205 -55
  105. package/templates/skills/profile-corruption-escalation/content.md +44 -85
  106. package/templates/skills/script-workflows/config.json +14 -0
  107. package/templates/skills/script-workflows/content.md +68 -0
  108. package/templates/skills/sprite-cli/content.md +4 -5
  109. package/templates/skills/swarm-scripts/content.md +2 -3
  110. package/templates/skills/turso-interaction/content.md +14 -17
  111. package/templates/skills/workflow-iterate/content.md +38 -391
  112. package/templates/skills/x-api-interactions/content.md +4 -6
  113. package/templates/skills/scheduled-task-resilience/config.json +0 -14
  114. package/templates/skills/scheduled-task-resilience/content.md +0 -95
@@ -37,8 +37,10 @@ describe("system-default skills", () => {
37
37
  const names = skills.map((skill) => skill.name);
38
38
 
39
39
  expect(names).toContain("attio-interaction");
40
+ expect(names).toContain("script-workflows");
40
41
  expect(names).toContain("swarm-scripts");
41
42
  expect(skills.find((skill) => skill.name === "attio-interaction")?.systemDefault).toBe(true);
43
+ expect(skills.find((skill) => skill.name === "script-workflows")?.systemDefault).toBe(true);
42
44
  expect(skills.find((skill) => skill.name === "swarm-scripts")?.systemDefault).toBe(true);
43
45
  expect(skills.find((skill) => skill.name === "kv-storage")?.systemDefault).toBe(true);
44
46
  expect(skills.find((skill) => skill.name === "pages")?.systemDefault).toBe(true);
@@ -48,6 +50,7 @@ describe("system-default skills", () => {
48
50
 
49
51
  const defaults = getSystemDefaultSkills().map((skill) => skill.name);
50
52
  expect(defaults).toContain("attio-interaction");
53
+ expect(defaults).toContain("script-workflows");
51
54
  expect(defaults).toContain("swarm-scripts");
52
55
  expect(defaults).toContain("kv-storage");
53
56
  expect(defaults).toContain("pages");
@@ -78,6 +81,43 @@ describe("system-default skills", () => {
78
81
  expect(skills.find((skill) => skill.id === manualDefault.id)?.isActive).toBe(true);
79
82
  });
80
83
 
84
+ test("existing agents see swarm-scope skills without explicit install rows", () => {
85
+ const existingAgent = createAgent({
86
+ name: "Existing Swarm Skill Worker",
87
+ description: "Created before a swarm-scope skill",
88
+ role: "worker",
89
+ isLead: false,
90
+ status: "idle",
91
+ maxTasks: 1,
92
+ capabilities: [],
93
+ });
94
+
95
+ const swarmSkill = createSkill({
96
+ name: "manual-swarm-scope-skill",
97
+ description: "Manual swarm scope skill",
98
+ content:
99
+ "---\nname: manual-swarm-scope-skill\ndescription: Manual swarm scope skill\n---\nBody.",
100
+ type: "personal",
101
+ scope: "swarm",
102
+ systemDefault: false,
103
+ });
104
+
105
+ const installRow = getDb()
106
+ .prepare<{ count: number }, [string, string]>(
107
+ `SELECT COUNT(*) AS count
108
+ FROM agent_skills
109
+ WHERE agentId = ?
110
+ AND skillId = ?`,
111
+ )
112
+ .get(existingAgent.id, swarmSkill.id);
113
+
114
+ expect(installRow?.count ?? 0).toBe(0);
115
+
116
+ const skills = getAgentSkills(existingAgent.id);
117
+ expect(skills.map((skill) => skill.name)).toContain("manual-swarm-scope-skill");
118
+ expect(skills.find((skill) => skill.id === swarmSkill.id)?.isActive).toBe(true);
119
+ });
120
+
81
121
  test("new agents get concrete agent_skills rows for system defaults", () => {
82
122
  const beforeAgent = createAgent({
83
123
  name: "Concrete Install Worker",
@@ -82,8 +82,8 @@ export const registerCreatePageTool = (server: McpServer) => {
82
82
  contentType: PageContentTypeSchema.describe(
83
83
  "'text/html' renders directly at /p/:id; 'application/json' is rendered by the SPA.",
84
84
  ),
85
- authMode: PageAuthModeSchema.default("public").describe(
86
- "'public' — no gate; 'authed' — requires page-session cookie; 'password' — requires key.",
85
+ authMode: PageAuthModeSchema.default("authed").describe(
86
+ "'authed' — requires page-session cookie (default); 'public' — no gate and must be explicit; 'password' — requires key.",
87
87
  ),
88
88
  password: z
89
89
  .string()
@@ -1,10 +1,21 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
- import { executeReadOnlyQuery } from "@/http/db-query";
3
+ import { DbQueryInputShape, executeReadOnlyQuery, resolveDbQuerySql } from "@/http/db-query";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
5
 
6
6
  const MCP_MAX_ROWS = 100;
7
7
 
8
+ const DbQueryToolInputSchema = z
9
+ .object({
10
+ ...DbQueryInputShape,
11
+ sql: z.string().optional().describe("SQL query (read-only only — writes are rejected)"),
12
+ query: z.string().optional().describe("Deprecated runtime alias for sql."),
13
+ params: z.array(z.any()).optional().default([]).describe("Query parameters"),
14
+ })
15
+ .refine((body) => body.sql !== undefined || body.query !== undefined, {
16
+ message: "Either sql or query is required",
17
+ });
18
+
8
19
  export const registerDbQueryTool = (server: McpServer) => {
9
20
  createToolRegistrar(server)(
10
21
  "db-query",
@@ -13,10 +24,7 @@ export const registerDbQueryTool = (server: McpServer) => {
13
24
  description:
14
25
  "Execute a read-only SQL query against the swarm database. Available to all authenticated agents — be aware results may include secrets (oauth_tokens, configs). Results capped at 100 rows.",
15
26
  annotations: { readOnlyHint: true },
16
- inputSchema: z.object({
17
- sql: z.string().describe("SQL query (read-only only — writes are rejected)"),
18
- params: z.array(z.any()).optional().default([]).describe("Query parameters"),
19
- }),
27
+ inputSchema: DbQueryToolInputSchema,
20
28
  outputSchema: z.object({
21
29
  success: z.boolean(),
22
30
  columns: z.array(z.string()),
@@ -26,8 +34,10 @@ export const registerDbQueryTool = (server: McpServer) => {
26
34
  truncated: z.boolean(),
27
35
  }),
28
36
  },
29
- async ({ sql, params }, _requestInfo, _meta) => {
37
+ async (input, _requestInfo, _meta) => {
30
38
  try {
39
+ const sql = resolveDbQuerySql(input);
40
+ const params = input.params ?? [];
31
41
  const result = executeReadOnlyQuery(sql, params, MCP_MAX_ROWS);
32
42
  const truncated = result.total > MCP_MAX_ROWS;
33
43
 
@@ -0,0 +1,123 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar } from "@/tools/utils";
4
+ import { ScriptRunStatusSchema } from "@/types";
5
+ import { proxyScriptsApi, scriptNameSchema, scriptToolOutputSchema } from "./script-common";
6
+
7
+ export const LAUNCH_SCRIPT_RUN_DESCRIPTION =
8
+ "Launch a durable one-off script workflow run. The run executes in the background and can be inspected with get-script-run for terminal status and journal entries.";
9
+
10
+ export const GET_SCRIPT_RUN_DESCRIPTION =
11
+ "Get a durable script workflow run by ID, including its journal entries for swarm-script, raw-llm, and agent-task steps.";
12
+
13
+ export const LIST_SCRIPT_RUNS_DESCRIPTION =
14
+ "List durable script workflow runs, optionally filtered by status or agent ID.";
15
+
16
+ export const registerScriptRunsTools = (server: McpServer) => {
17
+ const register = createToolRegistrar(server);
18
+
19
+ register(
20
+ "launch-script-run",
21
+ {
22
+ title: "Launch Script Run",
23
+ description: LAUNCH_SCRIPT_RUN_DESCRIPTION,
24
+ annotations: { openWorldHint: true },
25
+ inputSchema: z.object({
26
+ source: z.string().min(1).describe("TypeScript script workflow source."),
27
+ args: z.unknown().optional().describe("JSON-serializable workflow arguments."),
28
+ idempotencyKey: z
29
+ .string()
30
+ .min(1)
31
+ .max(200)
32
+ .optional()
33
+ .describe("Optional key that returns the existing run instead of launching a duplicate."),
34
+ scriptName: scriptNameSchema
35
+ .optional()
36
+ .describe("Optional human-readable script/workflow name for the run."),
37
+ requestedByUserId: z
38
+ .string()
39
+ .optional()
40
+ .describe("Optional canonical user ID to attribute the run to."),
41
+ }),
42
+ outputSchema: scriptToolOutputSchema,
43
+ },
44
+ async (args, requestInfo) =>
45
+ proxyScriptsApi({
46
+ method: "POST",
47
+ path: "/api/script-runs",
48
+ body: { ...args, background: true },
49
+ requestInfo,
50
+ successMessage: (data) => {
51
+ const id =
52
+ typeof data === "object" && data !== null && "id" in data
53
+ ? String((data as { id: unknown }).id)
54
+ : "unknown";
55
+ return `Script run launched: ${id}.`;
56
+ },
57
+ }),
58
+ );
59
+
60
+ register(
61
+ "get-script-run",
62
+ {
63
+ title: "Get Script Run",
64
+ description: GET_SCRIPT_RUN_DESCRIPTION,
65
+ annotations: { readOnlyHint: true, openWorldHint: false },
66
+ inputSchema: z.object({
67
+ id: z.string().uuid().describe("Script run ID."),
68
+ }),
69
+ outputSchema: scriptToolOutputSchema,
70
+ },
71
+ async ({ id }, requestInfo) =>
72
+ proxyScriptsApi({
73
+ method: "GET",
74
+ path: `/api/script-runs/${encodeURIComponent(id)}`,
75
+ requestInfo,
76
+ successMessage: (data) => {
77
+ const status =
78
+ typeof data === "object" &&
79
+ data !== null &&
80
+ "run" in data &&
81
+ typeof (data as { run?: { status?: unknown } }).run?.status === "string"
82
+ ? (data as { run: { status: string } }).run.status
83
+ : "unknown";
84
+ return `Script run ${id} status: ${status}.`;
85
+ },
86
+ }),
87
+ );
88
+
89
+ register(
90
+ "list-script-runs",
91
+ {
92
+ title: "List Script Runs",
93
+ description: LIST_SCRIPT_RUNS_DESCRIPTION,
94
+ annotations: { readOnlyHint: true, openWorldHint: false },
95
+ inputSchema: z.object({
96
+ status: ScriptRunStatusSchema.optional().describe("Optional script run status filter."),
97
+ agentId: z.string().optional().describe("Optional agent ID filter."),
98
+ limit: z.number().int().min(1).max(500).default(50).describe("Maximum runs to return."),
99
+ offset: z.number().int().min(0).default(0).describe("Pagination offset."),
100
+ }),
101
+ outputSchema: scriptToolOutputSchema,
102
+ },
103
+ async ({ status, agentId, limit, offset }, requestInfo) => {
104
+ const params = new URLSearchParams();
105
+ if (status) params.set("status", status);
106
+ if (agentId) params.set("agentId", agentId);
107
+ params.set("limit", String(limit));
108
+ params.set("offset", String(offset));
109
+ return proxyScriptsApi({
110
+ method: "GET",
111
+ path: `/api/script-runs?${params.toString()}`,
112
+ requestInfo,
113
+ successMessage: (data) => {
114
+ const total =
115
+ typeof data === "object" && data !== null && "total" in data
116
+ ? Number((data as { total: unknown }).total)
117
+ : 0;
118
+ return `Found ${Number.isFinite(total) ? total : 0} script run(s).`;
119
+ },
120
+ });
121
+ },
122
+ );
123
+ };
@@ -1,6 +1,7 @@
1
1
  export { registerSkillCreateTool } from "./skill-create";
2
2
  export { registerSkillDeleteTool } from "./skill-delete";
3
3
  export { registerSkillGetTool } from "./skill-get";
4
+ export { registerSkillGetFileTool } from "./skill-get-file";
4
5
  export { registerSkillInstallTool } from "./skill-install";
5
6
  export { registerSkillInstallRemoteTool } from "./skill-install-remote";
6
7
  export { registerSkillListTool } from "./skill-list";
@@ -0,0 +1,80 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getSkillById, getSkillFile } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+
6
+ export const registerSkillGetFileTool = (server: McpServer) => {
7
+ createToolRegistrar(server)(
8
+ "skill-get-file",
9
+ {
10
+ title: "Get Skill File",
11
+ annotations: { destructiveHint: false },
12
+ description:
13
+ "Fetch a bundled reference file from a complex skill by skillId and relative path. Use this when the file is not available on disk.",
14
+ inputSchema: z.object({
15
+ skillId: z.string().describe("Skill ID"),
16
+ path: z.string().describe("Relative path, e.g. references/animations.md"),
17
+ }),
18
+ outputSchema: z.object({
19
+ yourAgentId: z.string().uuid().optional(),
20
+ success: z.boolean(),
21
+ message: z.string(),
22
+ file: z.any().optional(),
23
+ }),
24
+ },
25
+ async (args, requestInfo, _meta) => {
26
+ const skill = getSkillById(args.skillId);
27
+ if (!skill) {
28
+ return {
29
+ content: [{ type: "text", text: "Skill not found." }],
30
+ structuredContent: {
31
+ yourAgentId: requestInfo.agentId,
32
+ success: false,
33
+ message: "Skill not found.",
34
+ },
35
+ };
36
+ }
37
+
38
+ let file = null;
39
+ try {
40
+ file = getSkillFile(args.skillId, args.path);
41
+ } catch (err) {
42
+ const message = err instanceof Error ? err.message : "Invalid file path.";
43
+ return {
44
+ content: [{ type: "text", text: message }],
45
+ structuredContent: {
46
+ yourAgentId: requestInfo.agentId,
47
+ success: false,
48
+ message,
49
+ },
50
+ };
51
+ }
52
+
53
+ if (!file) {
54
+ return {
55
+ content: [{ type: "text", text: "Skill file not found." }],
56
+ structuredContent: {
57
+ yourAgentId: requestInfo.agentId,
58
+ success: false,
59
+ message: "Skill file not found.",
60
+ },
61
+ };
62
+ }
63
+
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: `Skill file "${skill.name}/${file.path}" (${file.mimeType}):\n\n${file.content}`,
69
+ },
70
+ ],
71
+ structuredContent: {
72
+ yourAgentId: requestInfo.agentId,
73
+ success: true,
74
+ message: `Found skill file "${file.path}".`,
75
+ file,
76
+ },
77
+ };
78
+ },
79
+ );
80
+ };
@@ -3,6 +3,7 @@ import * as z from "zod";
3
3
  import { getAgentById, getInboxMessageById, getTaskById } from "@/be/db";
4
4
  import { getSlackApp } from "@/slack/app";
5
5
  import { downloadFile } from "@/slack/files";
6
+ import { extractSlackMessageText } from "@/slack/message-text";
6
7
  import { createToolRegistrar } from "@/tools/utils";
7
8
 
8
9
  /**
@@ -203,6 +204,13 @@ export const registerSlackReadTool = (server: McpServer) => {
203
204
  text?: string;
204
205
  ts: string;
205
206
  files?: RawFile[];
207
+ attachments?: Array<{
208
+ fallback?: string;
209
+ text?: string;
210
+ title?: string;
211
+ pretext?: string;
212
+ }>;
213
+ blocks?: unknown[];
206
214
  };
207
215
 
208
216
  let rawMessages: RawMessage[] = [];
@@ -267,8 +275,9 @@ export const registerSlackReadTool = (server: McpServer) => {
267
275
  }> = [];
268
276
 
269
277
  for (const m of rawMessages) {
270
- // Include messages with text OR files
271
- if (!m.text && (!m.files || m.files.length === 0)) continue;
278
+ // Include messages with text, attachments, blocks, or files
279
+ const extractedText = extractSlackMessageText(m);
280
+ if (!extractedText && (!m.files || m.files.length === 0)) continue;
272
281
 
273
282
  const isBot =
274
283
  m.user === botUserId || m.bot_id !== undefined || m.subtype === "bot_message";
@@ -330,7 +339,7 @@ export const registerSlackReadTool = (server: McpServer) => {
330
339
  user: m.user,
331
340
  username,
332
341
  isBot,
333
- text: m.text || "",
342
+ text: extractedText,
334
343
  ts: m.ts,
335
344
  files,
336
345
  });
@@ -128,11 +128,12 @@ export const DEFERRED_TOOLS = new Set([
128
128
  // Approval Requests (1)
129
129
  "request-human-input",
130
130
 
131
- // Skills (11)
131
+ // Skills (12)
132
132
  "skill-create",
133
133
  "skill-update",
134
134
  "skill-delete",
135
135
  "skill-get",
136
+ "skill-get-file",
136
137
  "skill-list",
137
138
  "skill-search",
138
139
  "skill-install",
@@ -164,12 +165,15 @@ export const DEFERRED_TOOLS = new Set([
164
165
  "kv-incr",
165
166
  "kv-list",
166
167
 
167
- // Reusable scripts (5)
168
+ // Reusable scripts (8)
168
169
  "script-search",
169
170
  "script-run",
170
171
  "script-upsert",
171
172
  "script-delete",
172
173
  "script-query-types",
174
+ "launch-script-run",
175
+ "get-script-run",
176
+ "list-script-runs",
173
177
 
174
178
  // External command routes (1)
175
179
  "swarm_x",
package/src/types.ts CHANGED
@@ -1535,6 +1535,65 @@ export const WorkflowRunSchema = z.object({
1535
1535
  });
1536
1536
  export type WorkflowRun = z.infer<typeof WorkflowRunSchema>;
1537
1537
 
1538
+ // --- Script Workflow Runs ---
1539
+
1540
+ export const ScriptRunStatusSchema = z.enum([
1541
+ "running",
1542
+ "paused",
1543
+ "completed",
1544
+ "failed",
1545
+ "cancelled",
1546
+ "aborted_limit",
1547
+ ]);
1548
+ export type ScriptRunStatus = z.infer<typeof ScriptRunStatusSchema>;
1549
+
1550
+ export const TERMINAL_SCRIPT_RUN_STATUSES = [
1551
+ "completed",
1552
+ "failed",
1553
+ "cancelled",
1554
+ "aborted_limit",
1555
+ ] as const;
1556
+ export type TerminalScriptRunStatus = (typeof TERMINAL_SCRIPT_RUN_STATUSES)[number];
1557
+
1558
+ // `workflow` = durable background run launched via /api/script-runs (has a journal).
1559
+ // `inline` = synchronous one-off run via /api/scripts/run (no journal).
1560
+ export const ScriptRunKindSchema = z.enum(["workflow", "inline"]);
1561
+ export type ScriptRunKind = z.infer<typeof ScriptRunKindSchema>;
1562
+
1563
+ export const ScriptRunSchema = z.object({
1564
+ id: z.string().uuid(),
1565
+ agentId: z.string(),
1566
+ scriptName: z.string().optional(),
1567
+ source: z.string(),
1568
+ args: z.unknown(),
1569
+ kind: ScriptRunKindSchema,
1570
+ status: ScriptRunStatusSchema,
1571
+ pid: z.number().int().optional(),
1572
+ startedAt: z.string(),
1573
+ finishedAt: z.string().optional(),
1574
+ output: z.unknown().optional(),
1575
+ error: z.string().optional(),
1576
+ lastHeartbeatAt: z.string().optional(),
1577
+ idempotencyKey: z.string().optional(),
1578
+ requestedByUserId: z.string().optional(),
1579
+ });
1580
+ export type ScriptRun = z.infer<typeof ScriptRunSchema>;
1581
+
1582
+ export const ScriptRunJournalEntrySchema = z.object({
1583
+ id: z.string().uuid(),
1584
+ runId: z.string().uuid(),
1585
+ stepKey: z.string(),
1586
+ stepType: z.string(),
1587
+ config: z.record(z.string(), z.unknown()),
1588
+ status: z.enum(["completed", "failed"]),
1589
+ result: z.unknown().optional(),
1590
+ error: z.string().optional(),
1591
+ startedAt: z.string(),
1592
+ completedAt: z.string().optional(),
1593
+ durationMs: z.number().optional(),
1594
+ });
1595
+ export type ScriptRunJournalEntry = z.infer<typeof ScriptRunJournalEntrySchema>;
1596
+
1538
1597
  // --- Workflow Run Step ---
1539
1598
 
1540
1599
  export const WorkflowRunStepStatusSchema = z.enum([
@@ -1742,6 +1801,19 @@ export const SkillWithInstallInfoSchema = SkillSchema.extend({
1742
1801
  });
1743
1802
  export type SkillWithInstallInfo = z.infer<typeof SkillWithInstallInfoSchema>;
1744
1803
 
1804
+ export const SkillFileSchema = z.object({
1805
+ id: z.string(),
1806
+ skillId: z.string(),
1807
+ path: z.string(),
1808
+ content: z.string(),
1809
+ mimeType: z.string(),
1810
+ isBinary: z.boolean(),
1811
+ size: z.number().nullable(),
1812
+ createdAt: z.string(),
1813
+ lastUpdatedAt: z.string(),
1814
+ });
1815
+ export type SkillFile = z.infer<typeof SkillFileSchema>;
1816
+
1745
1817
  // ── MCP Servers ──────────────────────────────────────────────────────────
1746
1818
 
1747
1819
  export const McpServerTransportSchema = z.enum(["stdio", "http", "sse"]);
@@ -28,7 +28,46 @@ export const MAX_RATE_LIMIT_RESET_MS = 7 * 24 * 60 * 60 * 1000;
28
28
  * "429 Too Many Requests"; does not match "No conversation found with session ID".
29
29
  */
30
30
  export function isRateLimitMessage(s: string): boolean {
31
- return /rate.?limit|hit your[\w\s-]*limit|usage[ _-]?limit|too many requests|\b429\b/i.test(s);
31
+ return (
32
+ /rate.?limit|hit your[\w\s-]*limit|usage[ _-]?limit|too many requests|\b429\b/i.test(s) ||
33
+ isCodexCreditsExhaustedMessage(s)
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Detects Codex's workspace-credit-exhausted error, which surfaces as:
39
+ * "Your workspace is out of credits. Ask your workspace owner to refill in order to continue."
40
+ * This wording does not match the standard rate-limit patterns, so it needs its own predicate.
41
+ * Kept specific to avoid false positives — "refill" alone is intentionally excluded.
42
+ */
43
+ export function isCodexCreditsExhaustedMessage(s: string): boolean {
44
+ return /out of credits|refill in order to continue|workspace owner to refill/i.test(s);
45
+ }
46
+
47
+ /** Default cooldown applied when a Codex OAuth slot returns a credits-exhausted error.
48
+ * The workspace credit cap is weekly, so a 2-hour cooldown is conservative but avoids
49
+ * the sawtooth of the 5-minute tier-3 fallback re-handing the dead slot every 5 minutes.
50
+ */
51
+ export const CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS = 2 * 60 * 60 * 1000; // 2h
52
+
53
+ /** Floor for the operator-tunable Codex credits cooldown — never shorter than the tier-3 fallback. */
54
+ export const MIN_CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS = 5 * 60 * 1000; // 5m
55
+
56
+ /**
57
+ * Resolve the effective Codex credits-exhausted cooldown (ms) from a raw config
58
+ * value (string | number | undefined). Falls back to the default constant on
59
+ * absent / empty / non-finite / non-positive input, then clamps to
60
+ * [MIN_CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS, MAX_RATE_LIMIT_RESET_MS].
61
+ * Pure + side-effect free so it's unit-testable and cheap to call.
62
+ */
63
+ export function resolveCodexCreditsExhaustedCooldownMs(
64
+ raw: string | number | undefined | null,
65
+ ): number {
66
+ if (raw === undefined || raw === null || raw === "") return CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS;
67
+ const n =
68
+ typeof raw === "number" ? raw : /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : Number.NaN;
69
+ if (!Number.isFinite(n) || n <= 0) return CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS;
70
+ return Math.min(Math.max(n, MIN_CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS), MAX_RATE_LIMIT_RESET_MS);
32
71
  }
33
72
 
34
73
  /**
@@ -84,10 +84,16 @@ async function defaultSpawnClaudeCli(
84
84
  signal?: AbortSignal,
85
85
  jsonSchema?: object,
86
86
  ): Promise<string> {
87
- // CLAUDE_BINARY may be a single binary ("claude", "shannon") or a
88
- // whitespace-separated command string ("bunx @dexh/shannon"). See
89
- // parseClaudeBinary in src/providers/claude-adapter.ts.
90
- const claudeBinaryArgv = (process.env.CLAUDE_BINARY ?? "claude").trim().split(/\s+/);
87
+ // SWARM_USE_CLAUDE_BRIDGE mirrors the main claude adapter's subscription-pool
88
+ // routing. Otherwise CLAUDE_BINARY may be a single binary, an absolute path,
89
+ // or a whitespace-separated command string.
90
+ const useClaudeBridge = ["true", "1"].includes(
91
+ (process.env.SWARM_USE_CLAUDE_BRIDGE ?? "").trim().toLowerCase(),
92
+ );
93
+ const claudeBinaryRaw = useClaudeBridge
94
+ ? "claude-bridge"
95
+ : (process.env.CLAUDE_BINARY ?? "claude").trim();
96
+ const claudeBinaryArgv = (claudeBinaryRaw || "claude").split(/\s+/);
91
97
  const cmd = [...claudeBinaryArgv, "-p", "--model", model, "--output-format", "json"];
92
98
  if (jsonSchema) {
93
99
  cmd.push("--json-schema", JSON.stringify(jsonSchema));