@desplega.ai/agent-swarm 1.74.4 → 1.76.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 (88) hide show
  1. package/README.md +1 -1
  2. package/openapi.json +1264 -46
  3. package/package.json +2 -2
  4. package/src/be/db.ts +563 -9
  5. package/src/be/memory/edges-store.ts +69 -0
  6. package/src/be/memory/providers/sqlite-store.ts +4 -0
  7. package/src/be/memory/raters/explicit-self.ts +22 -0
  8. package/src/be/memory/raters/implicit-citation.ts +44 -0
  9. package/src/be/memory/raters/llm-client.ts +172 -0
  10. package/src/be/memory/raters/llm-summarizer.ts +218 -0
  11. package/src/be/memory/raters/llm.ts +375 -0
  12. package/src/be/memory/raters/noop.ts +14 -0
  13. package/src/be/memory/raters/registry.ts +86 -0
  14. package/src/be/memory/raters/retrieval.ts +88 -0
  15. package/src/be/memory/raters/run-server-raters.ts +97 -0
  16. package/src/be/memory/raters/store.ts +228 -0
  17. package/src/be/memory/raters/types.ts +101 -0
  18. package/src/be/memory/reranker.ts +32 -2
  19. package/src/be/memory/retrieval-store.ts +116 -0
  20. package/src/be/memory/types.ts +3 -0
  21. package/src/be/migrations/051_memory_posteriors_and_retrieval.sql +67 -0
  22. package/src/be/migrations/052_memory_edges.sql +36 -0
  23. package/src/be/migrations/053_agent_waiting_for_credentials_status.sql +61 -0
  24. package/src/be/migrations/054_agent_harness_provider.sql +21 -0
  25. package/src/be/migrations/055_agent_cred_status.sql +15 -0
  26. package/src/be/migrations/056_drop_agent_tasks_source_check.sql +139 -0
  27. package/src/be/migrations/057_inbox_item_state.sql +27 -0
  28. package/src/be/migrations/058_task_templates.sql +31 -0
  29. package/src/be/swarm-config-guard.ts +24 -0
  30. package/src/commands/credential-wait.ts +186 -0
  31. package/src/commands/provider-credentials.ts +434 -0
  32. package/src/commands/runner.ts +253 -21
  33. package/src/hooks/hook.ts +143 -66
  34. package/src/http/agents.ts +191 -1
  35. package/src/http/config.ts +11 -1
  36. package/src/http/core.ts +5 -0
  37. package/src/http/inbox-state.ts +89 -0
  38. package/src/http/index.ts +10 -0
  39. package/src/http/memory.ts +230 -1
  40. package/src/http/sessions.ts +86 -0
  41. package/src/http/status.ts +665 -0
  42. package/src/http/task-templates.ts +51 -0
  43. package/src/http/tasks.ts +85 -5
  44. package/src/http/users.ts +134 -0
  45. package/src/prompts/memories.ts +62 -0
  46. package/src/providers/claude-adapter.ts +22 -0
  47. package/src/providers/claude-managed-adapter.ts +24 -0
  48. package/src/providers/codex-adapter.ts +43 -1
  49. package/src/providers/devin-adapter.ts +18 -0
  50. package/src/providers/index.ts +7 -0
  51. package/src/providers/opencode-adapter.ts +60 -0
  52. package/src/providers/pi-mono-adapter.ts +71 -0
  53. package/src/providers/types.ts +34 -0
  54. package/src/server.ts +2 -0
  55. package/src/slack/handlers.ts +0 -1
  56. package/src/tests/agents-harness-provider.test.ts +333 -0
  57. package/src/tests/credential-check.test.ts +367 -0
  58. package/src/tests/credential-status-api.test.ts +223 -0
  59. package/src/tests/credential-status-routing.test.ts +150 -0
  60. package/src/tests/credential-wait.test.ts +282 -0
  61. package/src/tests/harness-provider-resolution.test.ts +242 -0
  62. package/src/tests/jira-sync.test.ts +1 -1
  63. package/src/tests/memory-edges.test.ts +722 -0
  64. package/src/tests/memory-rate-endpoint.test.ts +330 -0
  65. package/src/tests/memory-rate-tool.test.ts +252 -0
  66. package/src/tests/memory-rater-e2e.test.ts +578 -0
  67. package/src/tests/memory-rater-implicit-citation.test.ts +304 -0
  68. package/src/tests/memory-rater-llm-summarizer.test.ts +317 -0
  69. package/src/tests/memory-rater-llm.test.ts +964 -0
  70. package/src/tests/memory-rater-store.test.ts +249 -0
  71. package/src/tests/memory-reranker.test.ts +161 -2
  72. package/src/tests/migration-runner-regressions.test.ts +17 -2
  73. package/src/tests/mocks/mock-llm-rater-client.ts +35 -0
  74. package/src/tests/run-server-raters.test.ts +291 -0
  75. package/src/tests/sessions.test.ts +141 -0
  76. package/src/tests/status.test.ts +843 -0
  77. package/src/tests/stop-hook-task-resolution.test.ts +98 -0
  78. package/src/tests/template-recommendations.test.ts +148 -0
  79. package/src/tests/tool-annotations.test.ts +2 -2
  80. package/src/tests/use-dismissible-card.test.ts +140 -0
  81. package/src/tools/memory-rate.ts +166 -0
  82. package/src/tools/memory-search.ts +18 -0
  83. package/src/tools/store-progress.ts +37 -0
  84. package/src/tools/swarm-config/set-config.ts +17 -1
  85. package/src/tools/tool-config.ts +1 -0
  86. package/src/types.ts +122 -1
  87. package/src/utils/harness-provider.ts +32 -0
  88. package/tsconfig.json +0 -2
@@ -0,0 +1,291 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { runServerRaters } from "../be/memory/raters/run-server-raters";
3
+ import type { ApplyRatingResult } from "../be/memory/raters/store";
4
+ import type { MemoryRater, RatingContext, RatingEvent } from "../be/memory/raters/types";
5
+
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ // Pure unit tests for `runServerRaters` — the orchestration extracted from the
8
+ // inline IIFE at `src/tools/store-progress.ts` (PR #426 review feedback).
9
+ //
10
+ // All tests use stub raters and an in-memory `applyRating`, so no DB / no env
11
+ // fiddling is required. The DB-backed end-to-end coverage already lives in
12
+ // `memory-rater-implicit-citation.test.ts`.
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+
15
+ class StubRater implements MemoryRater {
16
+ public calls: RatingContext[] = [];
17
+ constructor(
18
+ public readonly name: string,
19
+ private readonly emit: (ctx: RatingContext) => RatingEvent[],
20
+ ) {}
21
+ async rate(ctx: RatingContext): Promise<RatingEvent[]> {
22
+ this.calls.push(ctx);
23
+ return this.emit(ctx);
24
+ }
25
+ }
26
+
27
+ function captureApply(): {
28
+ fn: (events: RatingEvent[], ctx: { taskId?: string }) => ApplyRatingResult;
29
+ calls: { events: RatingEvent[]; ctx: { taskId?: string } }[];
30
+ } {
31
+ const calls: { events: RatingEvent[]; ctx: { taskId?: string } }[] = [];
32
+ return {
33
+ calls,
34
+ fn: (events, ctx) => {
35
+ calls.push({ events, ctx });
36
+ return { applied: events.length, rejected: [] };
37
+ },
38
+ };
39
+ }
40
+
41
+ const baseInput = {
42
+ taskId: "task-1",
43
+ agentId: "agent-1",
44
+ retrievedMemoryIds: ["mem-A", "mem-B"],
45
+ evidence: "agent referenced mem-A in its work",
46
+ };
47
+
48
+ describe("runServerRaters", () => {
49
+ test("no-ops when retrievedMemoryIds is empty", async () => {
50
+ const apply = captureApply();
51
+ const stub = new StubRater("implicit-citation", () => [
52
+ { memoryId: "mem-A", signal: 1, weight: 0.5, source: "" },
53
+ ]);
54
+
55
+ const out = await runServerRaters(
56
+ { ...baseInput, retrievedMemoryIds: [] },
57
+ {
58
+ raters: [stub],
59
+ serverRaterNames: new Set(["implicit-citation"]),
60
+ weightMultiplierFor: () => 1,
61
+ applyRating: apply.fn,
62
+ },
63
+ );
64
+
65
+ expect(out).toEqual({ ratersFired: 0, outcomes: [] });
66
+ expect(stub.calls).toHaveLength(0);
67
+ expect(apply.calls).toHaveLength(0);
68
+ });
69
+
70
+ test("filters out raters whose name is not in the server allow-list", async () => {
71
+ const apply = captureApply();
72
+ const allowed = new StubRater("implicit-citation", () => [
73
+ { memoryId: "mem-A", signal: 1, weight: 0.5, source: "" },
74
+ ]);
75
+ const blocked = new StubRater("llm", () => [
76
+ { memoryId: "mem-A", signal: 1, weight: 0.9, source: "" },
77
+ ]);
78
+
79
+ const out = await runServerRaters(baseInput, {
80
+ raters: [allowed, blocked],
81
+ serverRaterNames: new Set(["implicit-citation"]),
82
+ weightMultiplierFor: () => 1,
83
+ applyRating: apply.fn,
84
+ });
85
+
86
+ expect(out.ratersFired).toBe(1);
87
+ expect(allowed.calls).toHaveLength(1);
88
+ expect(blocked.calls).toHaveLength(0);
89
+ expect(apply.calls).toHaveLength(1);
90
+ expect(apply.calls[0]!.events.map((e) => e.source)).toEqual(["implicit-citation"]);
91
+ });
92
+
93
+ test("stamps source from rater.name regardless of any source the rater set", async () => {
94
+ const apply = captureApply();
95
+ const spoofy = new StubRater("implicit-citation", () => [
96
+ // A misbehaving rater tries to spoof its source.
97
+ { memoryId: "mem-A", signal: 1, weight: 0.5, source: "evil-rater" },
98
+ { memoryId: "mem-B", signal: -1, weight: 0.25, source: "evil-rater" },
99
+ ]);
100
+
101
+ await runServerRaters(baseInput, {
102
+ raters: [spoofy],
103
+ serverRaterNames: new Set(["implicit-citation"]),
104
+ weightMultiplierFor: () => 1,
105
+ applyRating: apply.fn,
106
+ });
107
+
108
+ expect(apply.calls).toHaveLength(1);
109
+ for (const e of apply.calls[0]!.events) {
110
+ expect(e.source).toBe("implicit-citation");
111
+ }
112
+ });
113
+
114
+ test("applies the configured weight multiplier and clamps the result to [0, 1]", async () => {
115
+ const apply = captureApply();
116
+ const stub = new StubRater("implicit-citation", () => [
117
+ { memoryId: "mem-A", signal: 1, weight: 0.5, source: "" },
118
+ { memoryId: "mem-B", signal: -1, weight: 0.25, source: "" },
119
+ ]);
120
+
121
+ // Multiplier of 4 would push mem-A to 2.0, which must clamp to 1.0.
122
+ await runServerRaters(baseInput, {
123
+ raters: [stub],
124
+ serverRaterNames: new Set(["implicit-citation"]),
125
+ weightMultiplierFor: () => 4,
126
+ applyRating: apply.fn,
127
+ });
128
+
129
+ const stamped = apply.calls[0]!.events;
130
+ const a = stamped.find((e) => e.memoryId === "mem-A")!;
131
+ const b = stamped.find((e) => e.memoryId === "mem-B")!;
132
+ expect(a.weight).toBe(1);
133
+ expect(b.weight).toBe(1); // 0.25 * 4 = 1.0 (boundary)
134
+ // Signal must not change.
135
+ expect(a.signal).toBe(1);
136
+ expect(b.signal).toBe(-1);
137
+ });
138
+
139
+ test("clamps a negative multiplier weight to 0 (defensive — config is meant to be ≥ 0)", async () => {
140
+ const apply = captureApply();
141
+ const stub = new StubRater("implicit-citation", () => [
142
+ { memoryId: "mem-A", signal: 1, weight: 0.5, source: "" },
143
+ ]);
144
+
145
+ await runServerRaters(baseInput, {
146
+ raters: [stub],
147
+ serverRaterNames: new Set(["implicit-citation"]),
148
+ weightMultiplierFor: () => -10,
149
+ applyRating: apply.fn,
150
+ });
151
+
152
+ expect(apply.calls[0]!.events[0]!.weight).toBe(0);
153
+ });
154
+
155
+ test("multiplier of 0 zeroes the weight without dropping the event", async () => {
156
+ const apply = captureApply();
157
+ const stub = new StubRater("implicit-citation", () => [
158
+ { memoryId: "mem-A", signal: 1, weight: 0.5, source: "" },
159
+ ]);
160
+
161
+ await runServerRaters(baseInput, {
162
+ raters: [stub],
163
+ serverRaterNames: new Set(["implicit-citation"]),
164
+ weightMultiplierFor: () => 0,
165
+ applyRating: apply.fn,
166
+ });
167
+
168
+ expect(apply.calls).toHaveLength(1);
169
+ expect(apply.calls[0]!.events[0]!.weight).toBe(0);
170
+ });
171
+
172
+ test("skips applyRating entirely when a rater returns no events", async () => {
173
+ const apply = captureApply();
174
+ const empty = new StubRater("implicit-citation", () => []);
175
+
176
+ const out = await runServerRaters(baseInput, {
177
+ raters: [empty],
178
+ serverRaterNames: new Set(["implicit-citation"]),
179
+ weightMultiplierFor: () => 1,
180
+ applyRating: apply.fn,
181
+ });
182
+
183
+ expect(out.ratersFired).toBe(0);
184
+ expect(apply.calls).toHaveLength(0);
185
+ });
186
+
187
+ test("forwards taskId, agentId, retrievedMemoryIds, and evidence to each rater", async () => {
188
+ const apply = captureApply();
189
+ const stub = new StubRater("implicit-citation", () => [
190
+ { memoryId: "mem-A", signal: 1, weight: 0.5, source: "" },
191
+ ]);
192
+
193
+ await runServerRaters(baseInput, {
194
+ raters: [stub],
195
+ serverRaterNames: new Set(["implicit-citation"]),
196
+ weightMultiplierFor: () => 1,
197
+ applyRating: apply.fn,
198
+ });
199
+
200
+ expect(stub.calls).toHaveLength(1);
201
+ expect(stub.calls[0]).toEqual({
202
+ taskId: "task-1",
203
+ agentId: "agent-1",
204
+ retrievedMemoryIds: ["mem-A", "mem-B"],
205
+ evidence: "agent referenced mem-A in its work",
206
+ });
207
+ });
208
+
209
+ test("forwards taskId to applyRating context", async () => {
210
+ const apply = captureApply();
211
+ const stub = new StubRater("implicit-citation", () => [
212
+ { memoryId: "mem-A", signal: 1, weight: 0.5, source: "" },
213
+ ]);
214
+
215
+ await runServerRaters(baseInput, {
216
+ raters: [stub],
217
+ serverRaterNames: new Set(["implicit-citation"]),
218
+ weightMultiplierFor: () => 1,
219
+ applyRating: apply.fn,
220
+ });
221
+
222
+ expect(apply.calls[0]!.ctx).toEqual({ taskId: "task-1" });
223
+ });
224
+
225
+ test("fires every allow-listed rater independently and returns one outcome per fire", async () => {
226
+ const apply = captureApply();
227
+ const r1 = new StubRater("rater-1", () => [
228
+ { memoryId: "mem-A", signal: 1, weight: 0.5, source: "" },
229
+ ]);
230
+ const r2 = new StubRater("rater-2", () => [
231
+ { memoryId: "mem-B", signal: -1, weight: 0.25, source: "" },
232
+ ]);
233
+ const weights = new Map<string, number>([
234
+ ["rater-1", 0.5],
235
+ ["rater-2", 1],
236
+ ]);
237
+
238
+ const out = await runServerRaters(baseInput, {
239
+ raters: [r1, r2],
240
+ serverRaterNames: new Set(["rater-1", "rater-2"]),
241
+ weightMultiplierFor: (n) => weights.get(n) ?? 1,
242
+ applyRating: apply.fn,
243
+ });
244
+
245
+ expect(out.ratersFired).toBe(2);
246
+ expect(out.outcomes.map((o) => o.rater)).toEqual(["rater-1", "rater-2"]);
247
+ expect(apply.calls).toHaveLength(2);
248
+ // rater-1: 0.5 * 0.5 = 0.25
249
+ expect(apply.calls[0]!.events[0]!.weight).toBe(0.25);
250
+ expect(apply.calls[0]!.events[0]!.source).toBe("rater-1");
251
+ // rater-2: 0.25 * 1 = 0.25
252
+ expect(apply.calls[1]!.events[0]!.weight).toBe(0.25);
253
+ expect(apply.calls[1]!.events[0]!.source).toBe("rater-2");
254
+ });
255
+
256
+ test("propagates rater errors so callers can wrap with try/catch", async () => {
257
+ const apply = captureApply();
258
+ const broken: MemoryRater = {
259
+ name: "implicit-citation",
260
+ rate: async () => {
261
+ throw new Error("rater blew up");
262
+ },
263
+ };
264
+
265
+ await expect(
266
+ runServerRaters(baseInput, {
267
+ raters: [broken],
268
+ serverRaterNames: new Set(["implicit-citation"]),
269
+ weightMultiplierFor: () => 1,
270
+ applyRating: apply.fn,
271
+ }),
272
+ ).rejects.toThrow("rater blew up");
273
+ expect(apply.calls).toHaveLength(0);
274
+ });
275
+
276
+ test("preserves rater-supplied reasoning on stamped events", async () => {
277
+ const apply = captureApply();
278
+ const stub = new StubRater("implicit-citation", () => [
279
+ { memoryId: "mem-A", signal: 1, weight: 0.5, source: "", reasoning: "cited explicitly" },
280
+ ]);
281
+
282
+ await runServerRaters(baseInput, {
283
+ raters: [stub],
284
+ serverRaterNames: new Set(["implicit-citation"]),
285
+ weightMultiplierFor: () => 1,
286
+ applyRating: apply.fn,
287
+ });
288
+
289
+ expect(apply.calls[0]!.events[0]!.reasoning).toBe("cited explicitly");
290
+ });
291
+ });
@@ -0,0 +1,141 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import {
4
+ closeDb,
5
+ createAgent,
6
+ createTaskExtended,
7
+ getRootTaskChain,
8
+ initDb,
9
+ listRecentSessions,
10
+ } from "../be/db";
11
+
12
+ const TEST_DB_PATH = "./test-sessions.sqlite";
13
+
14
+ describe("sessions — getRootTaskChain + listRecentSessions", () => {
15
+ beforeAll(async () => {
16
+ for (const suffix of ["", "-wal", "-shm"]) {
17
+ try {
18
+ await unlink(`${TEST_DB_PATH}${suffix}`);
19
+ } catch {}
20
+ }
21
+ initDb(TEST_DB_PATH);
22
+ });
23
+
24
+ afterAll(async () => {
25
+ closeDb();
26
+ for (const suffix of ["", "-wal", "-shm"]) {
27
+ try {
28
+ await unlink(`${TEST_DB_PATH}${suffix}`);
29
+ } catch {}
30
+ }
31
+ });
32
+
33
+ test("empty chain — no rows for non-existent root", () => {
34
+ const chain = getRootTaskChain("nonexistent-root-id");
35
+ expect(chain).toEqual([]);
36
+ });
37
+
38
+ test("single-root chain — chain length 1", () => {
39
+ const agent = createAgent({
40
+ id: "sessions-test-agent-1",
41
+ name: "Sessions Test Agent 1",
42
+ isLead: false,
43
+ status: "idle",
44
+ });
45
+ const root = createTaskExtended("root only", { agentId: agent.id });
46
+
47
+ const chain = getRootTaskChain(root.id);
48
+ expect(chain).toHaveLength(1);
49
+ expect(chain[0].id).toBe(root.id);
50
+ expect(chain[0].parentTaskId).toBeUndefined();
51
+ });
52
+
53
+ test("3-level chain — root → child → grandchild", () => {
54
+ const agent = createAgent({
55
+ id: "sessions-test-agent-2",
56
+ name: "Sessions Test Agent 2",
57
+ isLead: false,
58
+ status: "idle",
59
+ });
60
+ const root = createTaskExtended("root", { agentId: agent.id });
61
+ const child = createTaskExtended("child", {
62
+ agentId: agent.id,
63
+ parentTaskId: root.id,
64
+ });
65
+ const grandchild = createTaskExtended("grandchild", {
66
+ agentId: agent.id,
67
+ parentTaskId: child.id,
68
+ });
69
+
70
+ const chain = getRootTaskChain(root.id);
71
+ expect(chain).toHaveLength(3);
72
+
73
+ // ordered by createdAt — root first, then child, then grandchild
74
+ expect(chain.map((t) => t.id)).toEqual([root.id, child.id, grandchild.id]);
75
+ expect(chain[0].parentTaskId).toBeUndefined();
76
+ expect(chain[1].parentTaskId).toBe(root.id);
77
+ expect(chain[2].parentTaskId).toBe(child.id);
78
+ });
79
+
80
+ test("parallel siblings — root with two children", () => {
81
+ const agent = createAgent({
82
+ id: "sessions-test-agent-3",
83
+ name: "Sessions Test Agent 3",
84
+ isLead: false,
85
+ status: "idle",
86
+ });
87
+ const root = createTaskExtended("parallel root", { agentId: agent.id });
88
+ const sibA = createTaskExtended("sibling A", {
89
+ agentId: agent.id,
90
+ parentTaskId: root.id,
91
+ });
92
+ const sibB = createTaskExtended("sibling B", {
93
+ agentId: agent.id,
94
+ parentTaskId: root.id,
95
+ });
96
+
97
+ const chain = getRootTaskChain(root.id);
98
+ expect(chain).toHaveLength(3);
99
+ expect(chain[0].id).toBe(root.id);
100
+ // siblings appear in createdAt order (sibA before sibB)
101
+ const ids = chain.map((t) => t.id);
102
+ expect(ids.indexOf(sibA.id)).toBeLessThan(ids.indexOf(sibB.id));
103
+ });
104
+
105
+ test("listRecentSessions returns root tasks with chain summary", () => {
106
+ const sessions = listRecentSessions({ limit: 50 });
107
+ // We've created multiple roots above; each non-empty session must surface.
108
+ expect(sessions.length).toBeGreaterThanOrEqual(3);
109
+
110
+ for (const s of sessions) {
111
+ // Root tasks only — never have parentTaskId
112
+ expect(s.root.parentTaskId).toBeUndefined();
113
+ expect(typeof s.chainTaskCount).toBe("number");
114
+ expect(s.chainTaskCount).toBeGreaterThanOrEqual(1);
115
+ expect(typeof s.lastActivityAt).toBe("string");
116
+ expect(typeof s.latestStatus).toBe("string");
117
+ }
118
+
119
+ // The 3-level chain must report chainTaskCount of 3
120
+ const threeLevel = sessions.find((s) => s.root.task === "root");
121
+ expect(threeLevel).toBeDefined();
122
+ expect(threeLevel?.chainTaskCount).toBe(3);
123
+
124
+ // The parallel-root must report chainTaskCount of 3 (root + 2 siblings)
125
+ const parallel = sessions.find((s) => s.root.task === "parallel root");
126
+ expect(parallel).toBeDefined();
127
+ expect(parallel?.chainTaskCount).toBe(3);
128
+
129
+ // The single-root chain must report chainTaskCount of 1
130
+ const single = sessions.find((s) => s.root.task === "root only");
131
+ expect(single).toBeDefined();
132
+ expect(single?.chainTaskCount).toBe(1);
133
+ });
134
+
135
+ test("listRecentSessions ordered by lastActivityAt DESC", () => {
136
+ const sessions = listRecentSessions({ limit: 50 });
137
+ for (let i = 1; i < sessions.length; i++) {
138
+ expect(sessions[i - 1].lastActivityAt >= sessions[i].lastActivityAt).toBe(true);
139
+ }
140
+ });
141
+ });