@desplega.ai/agent-swarm 1.92.2 → 1.94.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.
Files changed (122) hide show
  1. package/README.md +2 -2
  2. package/openapi.json +242 -3
  3. package/package.json +5 -5
  4. package/src/be/db.ts +152 -11
  5. package/src/be/memory/boot-reembed.ts +0 -1
  6. package/src/be/memory/providers/sqlite-store.ts +42 -25
  7. package/src/be/memory/raters/llm-client.ts +12 -5
  8. package/src/be/memory/types.ts +3 -0
  9. package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
  10. package/src/be/migrations/089_harness_variant.sql +2 -0
  11. package/src/be/migrations/090_model_tiers.sql +2 -0
  12. package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
  13. package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
  14. package/src/be/migrations/093_slack_message_tracking.sql +6 -0
  15. package/src/be/migrations/runner.ts +52 -0
  16. package/src/be/modelsdev-cache.json +3264 -1166
  17. package/src/be/scripts/boot-reembed.ts +74 -0
  18. package/src/be/scripts/db.ts +19 -3
  19. package/src/be/seed/index.ts +1 -1
  20. package/src/be/seed/registry.ts +2 -2
  21. package/src/be/seed/runner.ts +5 -5
  22. package/src/be/seed/types.ts +6 -1
  23. package/src/be/seed-pricing.ts +2 -0
  24. package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
  25. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
  26. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
  27. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
  28. package/src/be/seed-scripts/index.ts +8 -7
  29. package/src/be/skill-sync.ts +28 -179
  30. package/src/commands/runner.ts +197 -10
  31. package/src/http/api-keys.ts +42 -0
  32. package/src/http/index.ts +13 -2
  33. package/src/http/mcp-bridge.ts +1 -1
  34. package/src/http/memory.ts +23 -24
  35. package/src/http/metrics.ts +55 -6
  36. package/src/http/schedules.ts +16 -15
  37. package/src/http/script-runs.ts +7 -1
  38. package/src/http/scripts.ts +147 -1
  39. package/src/http/tasks.ts +17 -6
  40. package/src/model-tiers.ts +140 -0
  41. package/src/providers/claude-adapter.ts +33 -1
  42. package/src/providers/claude-managed-adapter.ts +3 -0
  43. package/src/providers/claude-managed-models.ts +16 -0
  44. package/src/providers/codex-adapter.ts +8 -1
  45. package/src/providers/codex-models.ts +1 -0
  46. package/src/providers/codex-oauth/auth-json.ts +1 -0
  47. package/src/providers/harness-version.ts +7 -0
  48. package/src/providers/opencode-adapter.ts +12 -4
  49. package/src/providers/pi-mono-adapter.ts +90 -8
  50. package/src/providers/types.ts +2 -0
  51. package/src/scheduler/scheduler.ts +22 -34
  52. package/src/scripts-runtime/egress-secrets.ts +83 -0
  53. package/src/scripts-runtime/eval-harness.ts +4 -0
  54. package/src/scripts-runtime/executors/types.ts +7 -0
  55. package/src/scripts-runtime/loader.ts +2 -0
  56. package/src/server-user.ts +8 -2
  57. package/src/slack/channel-join.ts +41 -0
  58. package/src/slack/responses.ts +39 -11
  59. package/src/slack/watcher.ts +121 -8
  60. package/src/tests/additive-buffer.test.ts +0 -1
  61. package/src/tests/agents-list-model-display.test.ts +13 -0
  62. package/src/tests/api-key-tracking.test.ts +113 -0
  63. package/src/tests/approval-requests.test.ts +0 -6
  64. package/src/tests/aws-error-classifier.test.ts +148 -0
  65. package/src/tests/claude-managed-adapter.test.ts +12 -0
  66. package/src/tests/claude-managed-setup.test.ts +0 -4
  67. package/src/tests/codex-pool.test.ts +2 -6
  68. package/src/tests/context-window.test.ts +7 -0
  69. package/src/tests/http-api-integration.test.ts +23 -6
  70. package/src/tests/memory-edges.test.ts +0 -2
  71. package/src/tests/memory-rate-endpoint.test.ts +0 -2
  72. package/src/tests/memory-rater-e2e.test.ts +0 -2
  73. package/src/tests/memory-store.test.ts +19 -1
  74. package/src/tests/memory.test.ts +51 -0
  75. package/src/tests/metrics-http.test.ts +137 -3
  76. package/src/tests/migration-046-budgets.test.ts +33 -0
  77. package/src/tests/migration-runner-regressions.test.ts +69 -0
  78. package/src/tests/model-control.test.ts +162 -46
  79. package/src/tests/opencode-adapter.test.ts +9 -0
  80. package/src/tests/pi-mono-adapter.test.ts +319 -0
  81. package/src/tests/providers/pi-cost.test.ts +9 -0
  82. package/src/tests/reload-config.test.ts +33 -17
  83. package/src/tests/runner-fallback-output.test.ts +50 -0
  84. package/src/tests/runner-skills-refresh.test.ts +216 -46
  85. package/src/tests/script-runs-http.test.ts +7 -1
  86. package/src/tests/scripts-boot-reembed.test.ts +163 -0
  87. package/src/tests/scripts-embeddings.test.ts +90 -0
  88. package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
  89. package/src/tests/seed-scripts.test.ts +13 -1
  90. package/src/tests/seed.test.ts +26 -1
  91. package/src/tests/session-attach.test.ts +6 -6
  92. package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
  93. package/src/tests/skill-fs-writer.test.ts +250 -0
  94. package/src/tests/slack-attachments-block.test.ts +0 -1
  95. package/src/tests/slack-blocks.test.ts +0 -1
  96. package/src/tests/slack-channel-join.test.ts +80 -0
  97. package/src/tests/slack-identity-resolution.test.ts +0 -1
  98. package/src/tests/slack-watcher.test.ts +66 -0
  99. package/src/tests/structured-output.test.ts +0 -2
  100. package/src/tests/use-dismissible-card.test.ts +0 -4
  101. package/src/tests/workflow-agent-task.test.ts +5 -2
  102. package/src/tests/workflow-validation-port-routing.test.ts +181 -0
  103. package/src/tools/memory-get.ts +11 -0
  104. package/src/tools/memory-search.ts +18 -0
  105. package/src/tools/schedules/create-schedule.ts +71 -70
  106. package/src/tools/schedules/update-schedule.ts +43 -31
  107. package/src/tools/send-task.ts +16 -5
  108. package/src/tools/slack-post.ts +18 -15
  109. package/src/tools/slack-read.ts +9 -11
  110. package/src/tools/slack-reply.ts +18 -15
  111. package/src/tools/slack-start-thread.ts +17 -14
  112. package/src/tools/task-action.ts +11 -3
  113. package/src/types.ts +40 -0
  114. package/src/utils/aws-error-classifier.ts +97 -0
  115. package/src/utils/context-window.ts +5 -0
  116. package/src/utils/credentials.test.ts +68 -0
  117. package/src/utils/credentials.ts +66 -5
  118. package/src/utils/pretty-print.ts +25 -10
  119. package/src/utils/skill-fs-writer.ts +220 -0
  120. package/src/utils/skills-refresh.ts +123 -40
  121. package/src/workflows/engine.ts +3 -2
  122. package/src/workflows/executors/agent-task.ts +3 -1
@@ -2,15 +2,27 @@
2
2
  * Worker-side per-task skill refresh.
3
3
  *
4
4
  * Polls the cheap signature endpoint; on a hash mismatch, refetches the
5
- * full skill list and re-runs filesystem sync (claude/pi/codex dirs). The
6
- * worker stores the signature returned in the list response so the cached
7
- * hash always corresponds exactly to the snapshot it acted on avoids a
8
- * stale-hash race between the signature and list endpoints.
5
+ * full skill list and writes SKILL.md files to the worker's local HOME via
6
+ * writeSkillsToFilesystem() from skill-fs-writer.ts. This ensures newly
7
+ * created/approved skills land on the worker disk mid-sessionno container
8
+ * restart required.
9
+ *
10
+ * Previously Step 3 POSTed to /api/skills/sync-filesystem, which wrote to
11
+ * the API server's HOME instead of the worker disk. Now Step 3 builds
12
+ * SkillFsEntry[] from the already-fetched skill data and writes locally.
13
+ * For complex skills the worker fetches bundled files via N+1 HTTP calls
14
+ * (acceptable for v1 — simple skills need zero extra fetches).
15
+ *
16
+ * The /api/skills/sync-filesystem endpoint is retained for single-box local
17
+ * dev (where API and worker share a HOME). Workers no longer call it.
9
18
  *
10
19
  * Transient errors are swallowed (returned as `changed: false`) so a flaky
11
20
  * API can't churn the system prompt.
12
21
  */
13
22
 
23
+ import { homedir } from "node:os";
24
+ import { type SkillFsEntry, writeSkillsToFilesystem } from "./skill-fs-writer";
25
+
14
26
  export type SkillsRefreshContext = {
15
27
  apiUrl: string;
16
28
  swarmUrl: string;
@@ -27,6 +39,7 @@ export type SkillsRefreshResult = {
27
39
  export async function refreshSkillsIfChanged(
28
40
  ctx: SkillsRefreshContext,
29
41
  lastHashRef: { current: string | null },
42
+ homeOverride?: string,
30
43
  ): Promise<SkillsRefreshResult> {
31
44
  const { apiUrl, apiKey, agentId, role } = ctx;
32
45
  const authHeaders: Record<string, string> = { "X-Agent-ID": agentId };
@@ -52,70 +65,140 @@ export async function refreshSkillsIfChanged(
52
65
  return { changed: false };
53
66
  }
54
67
 
55
- // Step 2: full fetch + sync (only reached when hash differs or first call)
56
- let summary: { name: string; description: string }[] | undefined;
68
+ // Step 2: full fetch (only reached when hash differs or first call)
69
+ // Keep the full skill rows including content, id, isComplex — data is
70
+ // already on the wire, was previously discarded.
71
+ type SkillRow = {
72
+ id: string;
73
+ name: string;
74
+ description: string;
75
+ content: string | null;
76
+ isComplex: boolean;
77
+ isEnabled: boolean;
78
+ isActive: boolean;
79
+ };
80
+ let skillRows: SkillRow[] = [];
57
81
  let newHash: string | null = null;
82
+ let listFetchOk = false;
58
83
  try {
59
84
  const skillsResp = await fetch(`${apiUrl}/api/agents/${agentId}/skills`, {
60
85
  headers: authHeaders,
61
86
  });
62
87
  if (skillsResp.ok) {
63
88
  const skillsData = (await skillsResp.json()) as {
64
- skills: { name: string; description: string; isActive: boolean; isEnabled: boolean }[];
89
+ skills: SkillRow[];
65
90
  signature?: string;
66
91
  };
67
- summary = skillsData.skills
68
- .filter((s) => s.isActive && s.isEnabled)
69
- .map((s) => ({ name: s.name, description: s.description }));
92
+ skillRows = skillsData.skills;
70
93
  if (typeof skillsData.signature === "string") {
71
94
  newHash = skillsData.signature;
72
95
  }
96
+ listFetchOk = true;
73
97
  }
74
98
  } catch {
75
- // Non-fatalskills are optional
99
+ // Transient network / parse error bail out without touching the local FS
100
+ }
101
+
102
+ // Guard: a failed list fetch must not proceed to writeSkillsToFilesystem.
103
+ // An empty entries array would wipe every swarm-managed skill directory from
104
+ // the worker disk, which is worse than leaving the cache stale.
105
+ if (!listFetchOk) {
106
+ return { changed: false };
76
107
  }
77
108
 
78
- // Step 3: filesystem sync (claude/pi/codex dirs)
109
+ const summary = skillRows
110
+ .filter((s) => s.isActive && s.isEnabled)
111
+ .map((s) => ({ name: s.name, description: s.description }));
112
+
113
+ // Step 3: build SkillFsEntry[] and write to THIS worker's local HOME.
114
+ //
115
+ // For complex+enabled skills, fetch bundled files via N+1 HTTP calls
116
+ // (GET /api/skills/:id/files for manifest, then per non-binary file).
117
+ // Simple skills (the common case) need zero extra fetches.
79
118
  let syncOk = false;
80
119
  try {
81
- const syncHeaders: Record<string, string> = {
82
- "Content-Type": "application/json",
83
- "X-Agent-ID": agentId,
84
- };
85
- if (apiKey) syncHeaders.Authorization = `Bearer ${apiKey}`;
86
- const syncRes = await fetch(`${apiUrl}/api/skills/sync-filesystem`, {
87
- method: "POST",
88
- headers: syncHeaders,
89
- });
90
- if (syncRes.ok) {
91
- const syncResult = (await syncRes.json()) as {
92
- synced: number;
93
- removed: number;
94
- errors: string[];
95
- };
96
- console.log(
97
- `[${role}] Skills synced: ${syncResult.synced} written, ${syncResult.removed} removed`,
98
- );
99
- if (syncResult.errors.length > 0) {
100
- console.warn(`[${role}] Skill sync errors: ${syncResult.errors.join(", ")}`);
120
+ const entries: SkillFsEntry[] = [];
121
+
122
+ for (const skill of skillRows) {
123
+ if (!skill.isActive || !skill.isEnabled) continue;
124
+
125
+ const files: { path: string; content: string; isBinary: boolean }[] = [];
126
+
127
+ if (skill.isComplex) {
128
+ // Fetch manifest to know which files exist + which are binary
129
+ try {
130
+ const manifestResp = await fetch(`${apiUrl}/api/skills/${skill.id}/files`, {
131
+ headers: authHeaders,
132
+ });
133
+ if (manifestResp.ok) {
134
+ const manifestData = (await manifestResp.json()) as {
135
+ files: { path: string; isBinary: boolean }[];
136
+ };
137
+
138
+ // Fetch content for each non-binary file (N+1 acceptable for v1)
139
+ for (const manifestEntry of manifestData.files) {
140
+ if (manifestEntry.isBinary) {
141
+ files.push({ path: manifestEntry.path, content: "", isBinary: true });
142
+ continue;
143
+ }
144
+ try {
145
+ const encodedPath = manifestEntry.path.split("/").map(encodeURIComponent).join("/");
146
+ const fileResp = await fetch(
147
+ `${apiUrl}/api/skills/${skill.id}/files/${encodedPath}`,
148
+ { headers: authHeaders },
149
+ );
150
+ if (fileResp.ok) {
151
+ const fileData = (await fileResp.json()) as {
152
+ file: { path: string; content: string; isBinary: boolean };
153
+ };
154
+ files.push({
155
+ path: fileData.file.path,
156
+ content: fileData.file.content,
157
+ isBinary: fileData.file.isBinary,
158
+ });
159
+ }
160
+ } catch {
161
+ // Non-fatal — skip this file
162
+ }
163
+ }
164
+ }
165
+ } catch {
166
+ // Non-fatal — treat as no files (will skip complex skill per writer logic)
167
+ }
101
168
  }
102
- syncOk = true;
103
- } else {
104
- console.warn(`[${role}] Skill sync failed: HTTP ${syncRes.status}`);
169
+
170
+ entries.push({
171
+ id: skill.id,
172
+ name: skill.name,
173
+ content: skill.content ?? null,
174
+ isComplex: skill.isComplex,
175
+ isEnabled: skill.isEnabled,
176
+ isActive: skill.isActive,
177
+ files,
178
+ });
179
+ }
180
+
181
+ const writeResult = writeSkillsToFilesystem(entries, "all", homeOverride ?? homedir());
182
+ console.log(
183
+ `[${role}] Skills synced: ${writeResult.synced} written, ${writeResult.removed} removed`,
184
+ );
185
+ if (writeResult.errors.length > 0) {
186
+ console.warn(`[${role}] Skill sync errors: ${writeResult.errors.join(", ")}`);
105
187
  }
188
+ syncOk = true;
106
189
  } catch (err) {
107
190
  console.warn(`[${role}] Skill sync failed: ${(err as Error).message}`);
108
191
  }
109
192
 
110
- if (summary === undefined && newHash === null) {
193
+ if (skillRows.length === 0 && newHash === null) {
111
194
  return { changed: false };
112
195
  }
113
196
 
114
- // Only cache the new hash once the FS sync has actually succeeded —
115
- // otherwise a transient sync failure would leave the cached hash matching
197
+ // Only cache the new hash once the local FS write has actually succeeded —
198
+ // otherwise a transient write failure would leave the cached hash matching
116
199
  // the current signature, causing later polls to short-circuit and the
117
- // disk state to stay stale until an unrelated skill mutation. The next
118
- // poll re-enters this code path (lastHashRef unchanged) and retries.
200
+ // disk state to stay stale forever. The next poll re-enters this code path
201
+ // (lastHashRef unchanged) and retries.
119
202
  if (syncOk && newHash !== null) {
120
203
  lastHashRef.current = newHash;
121
204
  }
@@ -496,6 +496,7 @@ async function executeStep(
496
496
  // 4. Deep-interpolate config using local context (not global ctx)
497
497
  const { value: interpolatedValue, unresolved } = deepInterpolate(node.config, interpolationCtx);
498
498
  const interpolatedConfig = interpolatedValue as Record<string, unknown>;
499
+ const executionCtx: Record<string, unknown> = { ...ctx, ...interpolationCtx };
499
500
 
500
501
  if (unresolved.length > 0) {
501
502
  console.warn(
@@ -524,7 +525,7 @@ async function executeStep(
524
525
  result = await Promise.race([
525
526
  executor.run({
526
527
  config: interpolatedConfig,
527
- context: ctx,
528
+ context: executionCtx,
528
529
  meta,
529
530
  }),
530
531
  timeoutPromise(timeoutMs),
@@ -595,7 +596,7 @@ async function executeStep(
595
596
  // 7. Run validation if configured
596
597
  let validationResult: ValidationRunResult | undefined;
597
598
  if (node.validation) {
598
- validationResult = await runStepValidation(registry, node, result.output, ctx, meta);
599
+ validationResult = await runStepValidation(registry, node, result.output, executionCtx, meta);
599
600
 
600
601
  if (validationResult.outcome === "halt") {
601
602
  const errorMsg = "Validation failed (mustPass)";
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { ModelTierSchema, splitLegacyModelAlias } from "../../model-tiers";
2
3
  import { workflowContextKey } from "../../tasks/context-key";
3
4
  import { withSiblingAwareness } from "../../tasks/sibling-awareness";
4
5
  import type { ExecutorMeta } from "../../types";
@@ -17,6 +18,7 @@ const AgentTaskConfigSchema = z.object({
17
18
  dir: z.string().min(1).optional(),
18
19
  vcsRepo: z.string().min(1).optional(),
19
20
  model: z.string().min(1).optional(),
21
+ modelTier: ModelTierSchema.optional(),
20
22
  parentTaskId: z.string().uuid().optional(),
21
23
  requestedByUserId: z.string().optional(),
22
24
  outputSchema: z.record(z.string(), z.unknown()).optional(),
@@ -94,7 +96,7 @@ export class AgentTaskExecutor extends BaseExecutor<
94
96
  workflowRunStepId: meta.stepId,
95
97
  dir: effectiveDir,
96
98
  vcsRepo: effectiveVcsRepo,
97
- model: config.model,
99
+ ...splitLegacyModelAlias({ model: config.model, modelTier: config.modelTier }),
98
100
  parentTaskId: config.parentTaskId,
99
101
  requestedByUserId: config.requestedByUserId ?? meta.requestedByUserId,
100
102
  outputSchema: config.outputSchema,