@desplega.ai/agent-swarm 1.90.0 → 1.92.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 (96) hide show
  1. package/README.md +2 -1
  2. package/openapi.json +803 -150
  3. package/package.json +5 -5
  4. package/src/artifact-sdk/server.ts +2 -1
  5. package/src/be/db.ts +337 -1
  6. package/src/be/memory/providers/sqlite-store.ts +6 -1
  7. package/src/be/memory/types.ts +1 -0
  8. package/src/be/migrations/083_script_workflows.sql +51 -0
  9. package/src/be/modelsdev-cache.json +42352 -38595
  10. package/src/be/scripts/typecheck.ts +181 -1
  11. package/src/be/seed-scripts/catalog/compound-insights.ts +398 -0
  12. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +911 -0
  13. package/src/be/seed-scripts/catalog/schedule-health.ts +73 -0
  14. package/src/be/seed-scripts/catalog/smart-recall.ts +65 -0
  15. package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
  16. package/src/be/seed-scripts/catalog/tool-usage.ts +59 -0
  17. package/src/be/seed-scripts/index.ts +54 -0
  18. package/src/be/seed-skills/index.ts +7 -0
  19. package/src/be/swarm-config-guard.ts +17 -0
  20. package/src/commands/artifact.ts +3 -2
  21. package/src/commands/profile-sync.ts +310 -0
  22. package/src/commands/runner.ts +134 -3
  23. package/src/hooks/hook.ts +32 -9
  24. package/src/http/db-query.ts +20 -5
  25. package/src/http/index.ts +57 -0
  26. package/src/http/integrations.ts +6 -1
  27. package/src/http/mcp-bridge.ts +117 -0
  28. package/src/http/mcp-oauth.ts +97 -39
  29. package/src/http/memory.ts +5 -2
  30. package/src/http/openapi.ts +2 -2
  31. package/src/http/pages-public.ts +10 -11
  32. package/src/http/pages.ts +7 -11
  33. package/src/http/script-runs.ts +555 -0
  34. package/src/http/scripts.ts +24 -1
  35. package/src/http/utils.ts +11 -4
  36. package/src/jira/app.ts +2 -3
  37. package/src/jira/webhook-lifecycle.ts +2 -1
  38. package/src/linear/app.ts +2 -3
  39. package/src/prompts/session-templates.ts +24 -4
  40. package/src/providers/claude-adapter.ts +86 -13
  41. package/src/script-workflows/executor.ts +110 -0
  42. package/src/script-workflows/harness.ts +73 -0
  43. package/src/script-workflows/label-lint.ts +51 -0
  44. package/src/script-workflows/limits.ts +22 -0
  45. package/src/script-workflows/supervisor.ts +139 -0
  46. package/src/script-workflows/workflow-ctx.ts +205 -0
  47. package/src/scripts-runtime/executors/native.ts +1 -0
  48. package/src/scripts-runtime/sdk-allowlist.ts +124 -0
  49. package/src/scripts-runtime/swarm-sdk.ts +198 -3
  50. package/src/scripts-runtime/types/stdlib.d.ts +287 -0
  51. package/src/scripts-runtime/types/swarm-sdk.d.ts +287 -0
  52. package/src/server.ts +2 -0
  53. package/src/slack/handlers.ts +11 -4
  54. package/src/slack/message-text.ts +98 -0
  55. package/src/slack/thread-buffer.ts +5 -3
  56. package/src/tests/claude-adapter-binary.test.ts +147 -4
  57. package/src/tests/claude-adapter-otel.test.ts +85 -1
  58. package/src/tests/db-query.test.ts +28 -0
  59. package/src/tests/error-tracker.test.ts +121 -0
  60. package/src/tests/harness-provider-resolution.test.ts +33 -0
  61. package/src/tests/hook-registration-nudge.test.ts +69 -0
  62. package/src/tests/mcp-oauth-manual-client.test.ts +213 -0
  63. package/src/tests/mcp-tools.test.ts +6 -0
  64. package/src/tests/pages-public-html.test.ts +41 -0
  65. package/src/tests/pages-public-json-redirect.test.ts +37 -2
  66. package/src/tests/profile-sync.test.ts +282 -0
  67. package/src/tests/prompt-template-session.test.ts +34 -5
  68. package/src/tests/script-runs-http.test.ts +278 -0
  69. package/src/tests/script-workflows-label-lint.test.ts +43 -0
  70. package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
  71. package/src/tests/scripts-mcp-e2e.test.ts +49 -2
  72. package/src/tests/scripts-runtime.test.ts +33 -0
  73. package/src/tests/seed-scripts.test.ts +347 -2
  74. package/src/tests/slack-message-text.test.ts +250 -0
  75. package/src/tests/system-default-skills.test.ts +40 -0
  76. package/src/tools/create-metric.ts +2 -3
  77. package/src/tools/create-page.ts +3 -6
  78. package/src/tools/db-query.ts +16 -6
  79. package/src/tools/memory-rate.ts +2 -1
  80. package/src/tools/memory-search.ts +1 -0
  81. package/src/tools/register-kapso-number.ts +2 -4
  82. package/src/tools/request-human-input.ts +2 -1
  83. package/src/tools/script-common.ts +2 -4
  84. package/src/tools/script-run.ts +7 -0
  85. package/src/tools/script-runs.ts +123 -0
  86. package/src/tools/slack-read.ts +12 -3
  87. package/src/tools/tool-config.ts +4 -1
  88. package/src/types.ts +52 -0
  89. package/src/utils/constants.ts +58 -8
  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/skills/pages/content.md +205 -55
  94. package/templates/skills/script-workflows/config.json +14 -0
  95. package/templates/skills/script-workflows/content.md +68 -0
  96. package/templates/skills/swarm-scripts/content.md +45 -7
@@ -50,21 +50,155 @@ export interface SwarmConfig {
50
50
  }
51
51
 
52
52
  export interface SwarmSdk {
53
+ // --- memory ---
53
54
  memory_search(args: { query: string; scope?: "all" | "agent" | "swarm"; limit?: number; source?: string }): Promise<unknown>;
54
55
  memory_get(args: { memoryId: string }): Promise<unknown>;
55
56
  memory_rate(args: { id: string; useful: boolean; note?: string }): Promise<unknown>;
57
+ // --- tasks ---
56
58
  task_list(args?: Record<string, unknown>): Promise<unknown>;
57
59
  task_get(args: { taskId: string }): Promise<unknown>;
58
60
  task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
61
+ task_poll(args?: Record<string, unknown>): Promise<unknown>;
62
+ // --- kv ---
59
63
  kv_get(args: { key: string; namespace?: string }): Promise<unknown>;
60
64
  kv_set(args: { key: string; value: unknown; namespace?: string; ttlSeconds?: number; valueType?: "string" | "json" | "integer" }): Promise<unknown>;
61
65
  kv_del(args: { key: string; namespace?: string }): Promise<unknown>;
62
66
  kv_incr(args: { key: string; by?: number; namespace?: string }): Promise<unknown>;
63
67
  kv_list(args?: { prefix?: string; namespace?: string; limit?: number }): Promise<unknown>;
68
+ // --- repos ---
64
69
  repo_list(args?: Record<string, unknown>): Promise<unknown>;
70
+ // --- schedules ---
65
71
  schedule_list(args?: Record<string, unknown>): Promise<unknown>;
72
+ // --- scripts ---
66
73
  script_search(args: { query?: string; scope?: ScriptScope; limit?: number }): Promise<unknown>;
67
- script_run(args: { name?: string; source?: string; args?: unknown; intent?: string; scope?: ScriptScope; fsMode?: ScriptFsMode }): Promise<unknown>;
74
+ script_run(args: { name?: string; source?: string; args?: unknown; intent?: string; scope?: ScriptScope; fsMode?: ScriptFsMode; idempotencyKey?: string }): Promise<unknown>;
75
+ // --- swarm / agent ---
76
+ swarm_get(args?: { includeFull?: boolean }): Promise<unknown>;
77
+ agent_info(args?: Record<string, unknown>): Promise<unknown>;
78
+ metrics_get(args?: Record<string, unknown>): Promise<unknown>;
79
+ user_resolve(args?: { kind?: string; externalId?: string; email?: string; userId?: string }): Promise<unknown>;
80
+ db_query(args: { sql: string; params?: unknown[] }): Promise<unknown>;
81
+ // --- config ---
82
+ config_get(args?: { agentId?: string; repoId?: string; key?: string; includeSecrets?: boolean }): Promise<unknown>;
83
+ config_list(args?: { scope?: "global" | "agent" | "repo"; scopeId?: string; key?: string; includeSecrets?: boolean }): Promise<unknown>;
84
+ // --- slack ---
85
+ slack_read(args?: { inboxMessageId?: string; taskId?: string; channelId?: string; threadTs?: string; limit?: number; includeFiles?: boolean }): Promise<unknown>;
86
+ slack_listChannels(args?: { types?: Array<"public" | "private" | "dm" | "mpim">; limit?: number }): Promise<unknown>;
87
+ // --- messaging ---
88
+ message_read(args?: { channel?: string; limit?: number; since?: string; unreadOnly?: boolean; mentionsOnly?: boolean; markAsRead?: boolean }): Promise<unknown>;
89
+ // --- services ---
90
+ service_list(args?: { agentId?: string; name?: string; status?: "starting" | "healthy" | "unhealthy" | "stopped"; includeOwn?: boolean }): Promise<unknown>;
91
+ // --- context / profiles ---
92
+ context_history(args?: { agentId?: string; field?: "soulMd" | "identityMd" | "toolsMd" | "claudeMd" | "setupScript"; limit?: number }): Promise<unknown>;
93
+ context_diff(args: { versionId: string; compareToVersionId?: string }): Promise<unknown>;
94
+ // --- workflows ---
95
+ workflow_list(args?: { enabled?: boolean; includeFull?: boolean }): Promise<unknown>;
96
+ workflow_get(args: { id: string }): Promise<unknown>;
97
+ workflow_listRuns(args: { workflowId: string; status?: "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled" }): Promise<unknown>;
98
+ workflow_getRun(args: { id: string }): Promise<unknown>;
99
+ // --- prompt templates ---
100
+ prompt_list(args?: { eventType?: string; scope?: "global" | "agent" | "repo"; scopeId?: string; isDefault?: boolean }): Promise<unknown>;
101
+ prompt_get(args: { id: string }): Promise<unknown>;
102
+ // --- tracker ---
103
+ tracker_status(args?: Record<string, unknown>): Promise<unknown>;
104
+ tracker_syncStatus(args?: Record<string, unknown>): Promise<unknown>;
105
+ tracker_linkTask(args: { taskId: string; externalId: string; provider?: string }): Promise<unknown>;
106
+ tracker_unlink(args: { taskId: string }): Promise<unknown>;
107
+ tracker_mapAgent(args: { agentId: string; externalId: string; provider?: string }): Promise<unknown>;
108
+
109
+ // --- write: memory ---
110
+ memory_delete(args: { id: string }): Promise<unknown>;
111
+ inject_learning(args: { content: string; name?: string; scope?: "agent" | "swarm"; source?: string; tags?: string[] }): Promise<unknown>;
112
+
113
+ // --- write: tasks ---
114
+ task_send(args: Record<string, unknown>): Promise<unknown>;
115
+ task_cancel(args: { taskId: string }): Promise<unknown>;
116
+ task_action(args: Record<string, unknown>): Promise<unknown>;
117
+
118
+ // --- write: config ---
119
+ config_set(args: { key: string; value: unknown; scope?: "global" | "agent" | "repo"; scopeId?: string; isSecret?: boolean }): Promise<unknown>;
120
+ config_delete(args: { id: string }): Promise<unknown>;
121
+
122
+ // --- write: slack ---
123
+ slack_post(args: { channelId: string; message: string; blocks?: unknown }): Promise<unknown>;
124
+ slack_reply(args: { channelId?: string; threadTs?: string; message: string; taskId?: string }): Promise<unknown>;
125
+ slack_startThread(args: { channelId: string; message: string }): Promise<unknown>;
126
+ slack_uploadFile(args: Record<string, unknown>): Promise<unknown>;
127
+ slack_downloadFile(args: { url: string }): Promise<unknown>;
128
+
129
+ // --- write: messaging (internal) ---
130
+ message_post(args: { channel?: string; content: string; to?: string }): Promise<unknown>;
131
+
132
+ // --- write: profiles ---
133
+ profile_update(args: Record<string, unknown>): Promise<unknown>;
134
+
135
+ // --- write: services ---
136
+ service_register(args: Record<string, unknown>): Promise<unknown>;
137
+ service_unregister(args: { name: string }): Promise<unknown>;
138
+ service_updateStatus(args: { name: string; status: "starting" | "healthy" | "unhealthy" | "stopped" }): Promise<unknown>;
139
+
140
+ // --- write: schedules ---
141
+ schedule_create(args: Record<string, unknown>): Promise<unknown>;
142
+ schedule_update(args: Record<string, unknown>): Promise<unknown>;
143
+ schedule_delete(args: { id: string }): Promise<unknown>;
144
+ schedule_runNow(args: { id: string }): Promise<unknown>;
145
+
146
+ // --- write: workflows ---
147
+ workflow_create(args: Record<string, unknown>): Promise<unknown>;
148
+ workflow_update(args: Record<string, unknown>): Promise<unknown>;
149
+ workflow_patch(args: Record<string, unknown>): Promise<unknown>;
150
+ workflow_patchNode(args: Record<string, unknown>): Promise<unknown>;
151
+ workflow_delete(args: { id: string }): Promise<unknown>;
152
+ workflow_trigger(args: { id: string; input?: Record<string, unknown> }): Promise<unknown>;
153
+ workflow_retryRun(args: { id: string }): Promise<unknown>;
154
+ workflow_cancelRun(args: { id: string }): Promise<unknown>;
155
+
156
+ // --- write: prompt templates ---
157
+ prompt_set(args: Record<string, unknown>): Promise<unknown>;
158
+ prompt_delete(args: { id: string }): Promise<unknown>;
159
+ prompt_preview(args: Record<string, unknown>): Promise<unknown>;
160
+
161
+ // --- write: scripts ---
162
+ script_upsert(args: { name: string; source: string; description?: string; intent?: string; scope?: ScriptScope; fsMode?: ScriptFsMode }): Promise<unknown>;
163
+ script_delete(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
164
+ script_queryTypes(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
165
+ script_launchRun(args: { source: string; args?: unknown; idempotencyKey?: string; scriptName?: string; requestedByUserId?: string }): Promise<unknown>;
166
+ script_getRun(args: { id: string }): Promise<unknown>;
167
+ script_listRuns(args?: { status?: "running" | "paused" | "completed" | "failed" | "cancelled" | "aborted_limit"; agentId?: string; limit?: number; offset?: number }): Promise<unknown>;
168
+
169
+ // --- write: repos ---
170
+ repo_update(args: Record<string, unknown>): Promise<unknown>;
171
+
172
+ // --- write: agent ---
173
+ agent_join(args: { name: string; role?: string; description?: string; capabilities?: string[]; requestedId?: string; lead?: boolean }): Promise<unknown>;
174
+ user_manage(args: Record<string, unknown>): Promise<unknown>;
175
+
176
+ // --- skills ---
177
+ skill_list(args?: { scope?: string; scopeId?: string; includeBuiltin?: boolean }): Promise<unknown>;
178
+ skill_get(args: { id: string }): Promise<unknown>;
179
+ skill_search(args: { query: string; limit?: number }): Promise<unknown>;
180
+ skill_create(args: Record<string, unknown>): Promise<unknown>;
181
+ skill_update(args: Record<string, unknown>): Promise<unknown>;
182
+ skill_delete(args: { id: string }): Promise<unknown>;
183
+ skill_install(args: Record<string, unknown>): Promise<unknown>;
184
+ skill_uninstall(args: Record<string, unknown>): Promise<unknown>;
185
+ skill_publish(args: Record<string, unknown>): Promise<unknown>;
186
+
187
+ // --- mcp servers ---
188
+ mcpServer_list(args?: Record<string, unknown>): Promise<unknown>;
189
+ mcpServer_get(args: { id: string }): Promise<unknown>;
190
+ mcpServer_create(args: Record<string, unknown>): Promise<unknown>;
191
+ mcpServer_update(args: Record<string, unknown>): Promise<unknown>;
192
+ mcpServer_delete(args: { id: string }): Promise<unknown>;
193
+ mcpServer_install(args: Record<string, unknown>): Promise<unknown>;
194
+ mcpServer_uninstall(args: Record<string, unknown>): Promise<unknown>;
195
+
196
+ // --- pages & metrics ---
197
+ page_create(args: Record<string, unknown>): Promise<unknown>;
198
+ metric_create(args: Record<string, unknown>): Promise<unknown>;
199
+
200
+ // --- human input ---
201
+ request_humanInput(args: Record<string, unknown>): Promise<unknown>;
68
202
  }
69
203
 
70
204
  export interface ScriptStdlib {
@@ -78,7 +212,53 @@ export interface ScriptStdlib {
78
212
 
79
213
  export interface ScriptLogger extends Console {}
80
214
 
215
+ export interface ScriptRunContext {
216
+ id: string;
217
+ agentId: string;
218
+ args: unknown;
219
+ }
220
+
221
+ export interface ScriptWorkflowSteps {
222
+ rawLlm(
223
+ label: string,
224
+ config: { prompt: string; model?: string; schema?: Record<string, unknown> },
225
+ ): Promise<unknown>;
226
+ agentTask(
227
+ label: string,
228
+ config: {
229
+ template?: string;
230
+ task?: string;
231
+ agentId?: string;
232
+ tags?: string[];
233
+ priority?: number;
234
+ offerMode?: boolean;
235
+ dir?: string;
236
+ vcsRepo?: string;
237
+ model?: string;
238
+ parentTaskId?: string;
239
+ requestedByUserId?: string;
240
+ outputSchema?: Record<string, unknown>;
241
+ },
242
+ ): Promise<unknown>;
243
+ swarmScript(
244
+ label: string,
245
+ config: {
246
+ name?: string;
247
+ scriptName?: string;
248
+ source?: string;
249
+ args?: unknown;
250
+ scope?: ScriptScope;
251
+ fsMode?: ScriptFsMode;
252
+ intent?: string;
253
+ idempotencyKey?: string;
254
+ },
255
+ ): Promise<unknown>;
256
+ humanInTheLoop(): Promise<never>;
257
+ }
258
+
81
259
  export interface ScriptContext {
260
+ run?: ScriptRunContext;
261
+ step?: ScriptWorkflowSteps;
82
262
  swarm: SwarmSdk & { config: SwarmConfig };
83
263
  stdlib: ScriptStdlib;
84
264
  logger: ScriptLogger;
@@ -0,0 +1,398 @@
1
+ import { z } from "zod";
2
+
3
+ export const argsSchema = z.object({
4
+ days: z
5
+ .number()
6
+ .int()
7
+ .positive()
8
+ .optional()
9
+ .describe("Look back this many days (default 3)"),
10
+ includeToolUsage: z.boolean().optional().describe("Include tool usage histogram (default true)"),
11
+ includeScheduleHealth: z
12
+ .boolean()
13
+ .optional()
14
+ .describe("Include schedule health flags (default true)"),
15
+ includeMemoryHealth: z.boolean().optional().describe("Include memory health stats (default true)"),
16
+ includeScriptCandidates: z
17
+ .boolean()
18
+ .optional()
19
+ .describe("Include high-frequency tool-triplet candidates for future seed scripts (default true)"),
20
+ includeByAgent: z
21
+ .boolean()
22
+ .optional()
23
+ .describe("Include per-agent task/completion/failure breakdown (default true)"),
24
+ });
25
+
26
+ /**
27
+ * Failure reasons that are swarm bookkeeping, not real failures. Excluded from
28
+ * failureClusters, scheduleHealth and byAgent failure counts (Lead Rule #16):
29
+ * the run engine collapses redundant sibling tasks into these statuses, so
30
+ * counting them produces phantom failure spikes.
31
+ */
32
+ const EXCLUDED_FAIL = ["superseded_workflow_task", "cancelled"];
33
+
34
+ /**
35
+ * `db_query` returns positional rows (`rows: unknown[][]`) plus a `columns`
36
+ * array — NOT an array of objects. Zip them back into objects so callers can
37
+ * read by column name.
38
+ */
39
+ function rowsToObjects(res: any): any[] {
40
+ const p = res?.data ?? res;
41
+ const cols: string[] = p?.columns ?? [];
42
+ return (p?.rows ?? []).map((r: any) =>
43
+ Array.isArray(r) ? Object.fromEntries(cols.map((c, i) => [c, r[i]])) : r,
44
+ );
45
+ }
46
+
47
+ function asNumber(value: any): number {
48
+ const n = Number(value ?? 0);
49
+ return Number.isFinite(n) ? n : 0;
50
+ }
51
+
52
+ function round1(value: number): number {
53
+ return Math.round(value * 10) / 10;
54
+ }
55
+
56
+ function percent(part: number, total: number): number {
57
+ return total > 0 ? round1((part / total) * 100) : 0;
58
+ }
59
+
60
+ function extractToolName(content: string): string | null {
61
+ const match = content.match(/"type"\s*:\s*"tool_use"[\s\S]*?"name"\s*:\s*"([^"]+)"/);
62
+ return match?.[1] ?? null;
63
+ }
64
+
65
+ function toolSlug(tool: string): string {
66
+ return tool
67
+ .replace(/^mcp__/, "")
68
+ .replace(/__/g, "-")
69
+ .replace(/_/g, "-")
70
+ .replace(/[^a-zA-Z0-9-]+/g, "-")
71
+ .replace(/^-+|-+$/g, "")
72
+ .toLowerCase();
73
+ }
74
+
75
+ function decodeFloat32Blob(value: any): Float32Array | null {
76
+ if (!value) return null;
77
+ let bytes: Uint8Array | null = null;
78
+ if (value instanceof Uint8Array) bytes = value;
79
+ else if (Array.isArray(value)) bytes = Uint8Array.from(value);
80
+ else if (typeof value === "object" && Array.isArray(value.data)) bytes = Uint8Array.from(value.data);
81
+ else if (typeof value === "object") {
82
+ const keys = Object.keys(value);
83
+ if (keys.length > 0 && keys.every((key) => /^\d+$/.test(key))) {
84
+ bytes = Uint8Array.from(Object.values(value) as number[]);
85
+ }
86
+ }
87
+ if (!bytes || bytes.byteLength < 4 || bytes.byteLength % 4 !== 0) return null;
88
+ return new Float32Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
89
+ }
90
+
91
+ function cosineSimilarity(a: Float32Array, b: Float32Array): number {
92
+ const len = Math.min(a.length, b.length);
93
+ let dot = 0;
94
+ let na = 0;
95
+ let nb = 0;
96
+ for (let i = 0; i < len; i++) {
97
+ const av = a[i] ?? 0;
98
+ const bv = b[i] ?? 0;
99
+ dot += av * bv;
100
+ na += av * av;
101
+ nb += bv * bv;
102
+ }
103
+ if (na === 0 || nb === 0) return 0;
104
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
105
+ }
106
+
107
+ /**
108
+ * Daily compounding insights — compressed JSON for Phase 0 evolution.
109
+ *
110
+ * Swarm-wide by design: every section aggregates across ALL agents via direct
111
+ * read-only SQL (no per-agent scoping), so a single call replaces ~25 raw tool
112
+ * roundtrips. Parametric via `days` + the `include*` flags.
113
+ */
114
+ export default async function compoundInsights(args: any, ctx: any) {
115
+ const parsed = argsSchema.safeParse(args || {});
116
+ if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
117
+ const days = parsed.data.days || 3;
118
+ const includeToolUsage = parsed.data.includeToolUsage !== false;
119
+ const includeScheduleHealth = parsed.data.includeScheduleHealth !== false;
120
+ const includeMemoryHealth = parsed.data.includeMemoryHealth !== false;
121
+ const includeScriptCandidates = parsed.data.includeScriptCandidates !== false;
122
+ const includeByAgent = parsed.data.includeByAgent !== false;
123
+
124
+ // `days` is a validated positive int, so it is safe to interpolate into the
125
+ // SQLite datetime modifier. EXCLUDED_FAIL is a fixed constant list.
126
+ const w = `datetime('now','-${days} days')`;
127
+ const exclList = EXCLUDED_FAIL.map((r) => `'${r}'`).join(",");
128
+ // A "real" failure = status failed AND not one of the bookkeeping reasons.
129
+ const realFail = `t.status='failed' AND (t.failureReason IS NULL OR t.failureReason NOT IN (${exclList}))`;
130
+
131
+ const insights: any = { days, generatedAt: new Date().toISOString() };
132
+
133
+ // Task summary (all agents, direct SQL).
134
+ const statusRows = rowsToObjects(
135
+ await ctx.swarm.db_query({
136
+ sql: `SELECT status, count(*) as cnt FROM agent_tasks t WHERE t.createdAt > ${w} GROUP BY status`,
137
+ }),
138
+ );
139
+ const statusCounts: Record<string, number> = {};
140
+ let total = 0;
141
+ for (const r of statusRows) {
142
+ statusCounts[r.status] = r.cnt;
143
+ total += r.cnt;
144
+ }
145
+ const completed = statusCounts.completed ?? 0;
146
+ const failed = statusCounts.failed ?? 0;
147
+ insights.taskSummary = {
148
+ total,
149
+ completed,
150
+ failed,
151
+ completionRate: total > 0 ? Math.round((completed / total) * 1000) / 10 : 0,
152
+ failureRate: total > 0 ? Math.round((failed / total) * 1000) / 10 : 0,
153
+ statusCounts,
154
+ };
155
+
156
+ // Failure clusters (real failures only, normalized to a 60-char lowercased prefix).
157
+ insights.failureClusters = rowsToObjects(
158
+ await ctx.swarm.db_query({
159
+ sql: `SELECT substr(lower(t.failureReason),1,60) as reason, count(*) as count
160
+ FROM agent_tasks t
161
+ WHERE ${realFail} AND t.failureReason IS NOT NULL AND t.createdAt > ${w}
162
+ GROUP BY reason ORDER BY count DESC LIMIT 10`,
163
+ }),
164
+ );
165
+
166
+ // Schedule health (>= 2 runs, > 20% real-failure rate).
167
+ if (includeScheduleHealth) {
168
+ const sh = rowsToObjects(
169
+ await ctx.swarm.db_query({
170
+ sql: `SELECT s.name as name, s.id as id, count(t.id) as runs,
171
+ sum(case when ${realFail} then 1 else 0 end) as failed
172
+ FROM scheduled_tasks s
173
+ JOIN agent_tasks t ON t.scheduleId = s.id
174
+ WHERE t.createdAt > ${w} AND t.status != 'cancelled'
175
+ GROUP BY s.id, s.name HAVING runs >= 2`,
176
+ }),
177
+ );
178
+ insights.scheduleHealth = sh
179
+ .map((r: any) => ({
180
+ name: r.name,
181
+ id: r.id,
182
+ runs: r.runs,
183
+ failureRate: r.runs > 0 ? Math.round((r.failed / r.runs) * 100) : 0,
184
+ }))
185
+ .filter((r: any) => r.failureRate > 20)
186
+ .sort((a: any, b: any) => b.failureRate - a.failureRate);
187
+ }
188
+
189
+ // Tool usage (top 25). Tool names live inside the `content` JSON of
190
+ // session_logs (no dedicated column), so extract the name SQL-side: the
191
+ // `'%"type":"tool_use"%'` filter excludes tool_result rows (which only carry
192
+ // `tool_use_id`), and instr/substr pull the first tool name per log line.
193
+ // Approximate: a log line with parallel tool_use blocks counts only its first.
194
+ if (includeToolUsage) {
195
+ insights.toolUsage = rowsToObjects(
196
+ await ctx.swarm.db_query({
197
+ sql: `WITH tu AS (
198
+ SELECT substr(content, instr(content,'"type":"tool_use"')) AS tail
199
+ FROM session_logs
200
+ WHERE content LIKE '%"type":"tool_use"%' AND createdAt > ${w}
201
+ ),
202
+ nm AS (
203
+ SELECT substr(tail, instr(tail,'"name":"')+8) AS rest
204
+ FROM tu WHERE instr(tail,'"name":"') > 0
205
+ )
206
+ SELECT substr(rest,1,instr(rest,'"')-1) AS tool, count(*) AS calls
207
+ FROM nm GROUP BY tool ORDER BY calls DESC LIMIT 25`,
208
+ }),
209
+ ).map((r: any) => ({ tool: r.tool, calls: r.calls }));
210
+ }
211
+
212
+ // Memory health (whole store, by scope + source). Pollution markers are
213
+ // SQL-light counts plus JS-side embedding similarity where available; prod
214
+ // SQLite does not expose a scalar cosine_similarity() function.
215
+ if (includeMemoryHealth) {
216
+ const memRows = rowsToObjects(
217
+ await ctx.swarm.db_query({
218
+ sql: `SELECT scope, source, count(*) as cnt,
219
+ sum(case when accessCount = 0 then 1 else 0 end) as zeroAccess,
220
+ sum(case when sourceTaskId IS NOT NULL OR sourcePath IS NOT NULL then 1 else 0 end) as referenced
221
+ FROM agent_memory GROUP BY scope, source`,
222
+ }),
223
+ );
224
+ const totalMem = memRows.reduce((s: number, r: any) => s + (r.cnt ?? 0), 0);
225
+ const bySource: any = {};
226
+ for (const r of memRows) {
227
+ bySource[r.source] ??= {
228
+ total: 0,
229
+ percentOfStore: 0,
230
+ zeroAccess: 0,
231
+ zeroAccessPercent: 0,
232
+ referenced: 0,
233
+ };
234
+ bySource[r.source].total += asNumber(r.cnt);
235
+ bySource[r.source].zeroAccess += asNumber(r.zeroAccess);
236
+ bySource[r.source].referenced += asNumber(r.referenced);
237
+ }
238
+ for (const source of Object.keys(bySource)) {
239
+ bySource[source].percentOfStore = percent(bySource[source].total, totalMem);
240
+ bySource[source].zeroAccessPercent = percent(bySource[source].zeroAccess, bySource[source].total);
241
+ }
242
+
243
+ const autoSnapshotSources = ["session_summary", "task_completion"];
244
+ const autoSnapshotTotal = autoSnapshotSources.reduce(
245
+ (sum, source) => sum + (bySource[source]?.total ?? 0),
246
+ 0,
247
+ );
248
+ const popularButUseless = rowsToObjects(
249
+ await ctx.swarm.db_query({
250
+ sql: `SELECT id, name, source, accessCount, alpha, beta,
251
+ round(alpha / nullif(alpha + beta, 0), 3) as usefulness,
252
+ substr(content, 1, 180) as preview
253
+ FROM agent_memory
254
+ WHERE source IN ('session_summary','task_completion')
255
+ AND accessCount >= 5
256
+ AND alpha <= beta
257
+ ORDER BY accessCount DESC, beta DESC LIMIT 10`,
258
+ }),
259
+ ).map((r: any) => ({
260
+ id: r.id,
261
+ name: r.name,
262
+ source: r.source,
263
+ accessCount: asNumber(r.accessCount),
264
+ usefulness: Number(r.usefulness ?? 0),
265
+ preview: r.preview,
266
+ }));
267
+ const zeroAccessStaleRefRows = rowsToObjects(
268
+ await ctx.swarm.db_query({
269
+ sql: `SELECT source, count(*) as count
270
+ FROM agent_memory
271
+ WHERE accessCount = 0
272
+ AND (sourceTaskId IS NOT NULL OR sourcePath IS NOT NULL)
273
+ AND createdAt < datetime('now','-${days} days')
274
+ GROUP BY source ORDER BY count DESC`,
275
+ }),
276
+ );
277
+
278
+ const similarityRows = rowsToObjects(
279
+ await ctx.swarm.db_query({
280
+ sql: `SELECT id, name, source, accessCount, embedding
281
+ FROM agent_memory
282
+ WHERE source IN ('session_summary','task_completion')
283
+ AND embedding IS NOT NULL
284
+ ORDER BY accessCount DESC LIMIT 30`,
285
+ }),
286
+ );
287
+ let strongestAutoSnapshotPair: any = null;
288
+ const vectors = similarityRows
289
+ .map((r: any) => ({ ...r, vector: decodeFloat32Blob(r.embedding) }))
290
+ .filter((r: any) => r.vector);
291
+ for (let i = 0; i < vectors.length; i++) {
292
+ for (let j = i + 1; j < vectors.length; j++) {
293
+ const similarity = cosineSimilarity(vectors[i].vector, vectors[j].vector);
294
+ if (!strongestAutoSnapshotPair || similarity > strongestAutoSnapshotPair.similarity) {
295
+ strongestAutoSnapshotPair = {
296
+ similarity: round1(similarity * 100) / 100,
297
+ a: { id: vectors[i].id, name: vectors[i].name, source: vectors[i].source },
298
+ b: { id: vectors[j].id, name: vectors[j].name, source: vectors[j].source },
299
+ };
300
+ }
301
+ }
302
+ }
303
+
304
+ insights.memoryHealth = {
305
+ total: totalMem,
306
+ byScope: memRows.reduce((m: any, r: any) => {
307
+ m[r.scope] = (m[r.scope] ?? 0) + r.cnt;
308
+ return m;
309
+ }, {}),
310
+ bySource,
311
+ pollution: {
312
+ autoSnapshotSources,
313
+ autoSnapshotTotal,
314
+ autoSnapshotPercent: percent(autoSnapshotTotal, totalMem),
315
+ popularButUselessAutoSnapshots: popularButUseless,
316
+ zeroAccessStaleRefs: {
317
+ total: zeroAccessStaleRefRows.reduce((sum: number, r: any) => sum + asNumber(r.count), 0),
318
+ bySource: zeroAccessStaleRefRows.reduce((m: any, r: any) => {
319
+ m[r.source] = asNumber(r.count);
320
+ return m;
321
+ }, {}),
322
+ },
323
+ similarityCheck: {
324
+ sqliteCosineSimilarityAvailable: false,
325
+ path: "js",
326
+ sampledAutoSnapshots: vectors.length,
327
+ strongestAutoSnapshotPair,
328
+ },
329
+ },
330
+ };
331
+ }
332
+
333
+ // Evolution/self-scripting candidates: high-frequency consecutive tool
334
+ // triplets are good prompts for a future seed script.
335
+ if (includeScriptCandidates) {
336
+ const rows = rowsToObjects(
337
+ await ctx.swarm.db_query({
338
+ sql: `WITH raw AS (
339
+ SELECT sessionId, iteration, lineNumber, content,
340
+ json_extract(content, '$.tool_name') as jsonToolName
341
+ FROM session_logs
342
+ WHERE createdAt > ${w}
343
+ AND (content LIKE '%"type":"tool_use"%' OR json_extract(content, '$.tool_name') IS NOT NULL)
344
+ )
345
+ SELECT sessionId, iteration, lineNumber, jsonToolName, content
346
+ FROM raw ORDER BY sessionId, iteration, lineNumber LIMIT 100`,
347
+ }),
348
+ );
349
+ const bySession = new Map<string, string[]>();
350
+ for (const row of rows) {
351
+ const tool = row.jsonToolName || extractToolName(String(row.content ?? ""));
352
+ if (!tool) continue;
353
+ const key = String(row.sessionId ?? "unknown");
354
+ const tools = bySession.get(key) ?? [];
355
+ tools.push(tool);
356
+ bySession.set(key, tools);
357
+ }
358
+ const counts = new Map<string, { tools: string[]; count: number }>();
359
+ for (const tools of bySession.values()) {
360
+ for (let i = 0; i <= tools.length - 3; i++) {
361
+ const triplet = tools.slice(i, i + 3);
362
+ const key = triplet.join(" -> ");
363
+ const current = counts.get(key) ?? { tools: triplet, count: 0 };
364
+ current.count += 1;
365
+ counts.set(key, current);
366
+ }
367
+ }
368
+ insights.scriptCandidates = [...counts.values()]
369
+ .sort((a, b) => b.count - a.count)
370
+ .slice(0, 10)
371
+ .map((r) => ({
372
+ tools: r.tools,
373
+ count: r.count,
374
+ suggestedName: r.tools.map(toolSlug).filter(Boolean).slice(0, 3).join("-").slice(0, 80),
375
+ }));
376
+ }
377
+
378
+ // Per-agent breakdown — covers every agent that ran a task in the window.
379
+ if (includeByAgent) {
380
+ insights.byAgent = rowsToObjects(
381
+ await ctx.swarm.db_query({
382
+ sql: `SELECT a.name as agent, count(*) as total,
383
+ sum(case when t.status='completed' then 1 else 0 end) as completed,
384
+ sum(case when ${realFail} then 1 else 0 end) as failed
385
+ FROM agent_tasks t LEFT JOIN agents a ON a.id = t.agentId
386
+ WHERE t.createdAt > ${w} AND t.agentId IS NOT NULL
387
+ GROUP BY t.agentId, a.name ORDER BY total DESC LIMIT 30`,
388
+ }),
389
+ ).map((r: any) => ({
390
+ agent: r.agent,
391
+ total: r.total,
392
+ completed: r.completed,
393
+ failed: r.failed,
394
+ }));
395
+ }
396
+
397
+ return insights;
398
+ }