@desplega.ai/agent-swarm 1.92.1 → 1.92.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/src/be/db.ts +89 -0
- package/src/be/memory/boot-reembed.ts +85 -0
- package/src/be/memory/constants.ts +42 -1
- package/src/be/memory/providers/openai-embedding.ts +13 -0
- package/src/be/memory/providers/sqlite-store.ts +33 -1
- package/src/be/memory/reranker.ts +35 -17
- package/src/be/memory/types.ts +8 -0
- package/src/be/modelsdev-cache.json +5308 -2165
- package/src/be/seed-scripts/catalog/compound-insights.ts +371 -0
- package/src/http/index.ts +9 -0
- package/src/http/memory.ts +4 -0
- package/src/tasks/worker-follow-up.ts +12 -0
- package/src/tests/memory-e2e.test.ts +6 -6
- package/src/tests/memory-rater-e2e.test.ts +4 -5
- package/src/tests/memory-reranker.test.ts +135 -124
- package/src/tests/memory.test.ts +13 -12
- package/src/tests/seed-scripts.test.ts +205 -0
- package/src/tests/task-cascade-fail.test.ts +304 -0
- package/templates/workflows/llm-safe-release-context/config.json +13 -0
- package/templates/workflows/llm-safe-release-context/content.md +69 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
cancelTask,
|
|
5
|
+
cascadeFailDependents,
|
|
6
|
+
closeDb,
|
|
7
|
+
completeTask,
|
|
8
|
+
createAgent,
|
|
9
|
+
createTaskExtended,
|
|
10
|
+
failTask,
|
|
11
|
+
getDb,
|
|
12
|
+
getDependentTasks,
|
|
13
|
+
getTaskById,
|
|
14
|
+
initDb,
|
|
15
|
+
startTask,
|
|
16
|
+
supersedeTask,
|
|
17
|
+
} from "../be/db";
|
|
18
|
+
|
|
19
|
+
const TEST_DB_PATH = "./test-task-cascade-fail.sqlite";
|
|
20
|
+
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
initDb(TEST_DB_PATH);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(() => {
|
|
26
|
+
closeDb();
|
|
27
|
+
try {
|
|
28
|
+
unlinkSync(TEST_DB_PATH);
|
|
29
|
+
unlinkSync(`${TEST_DB_PATH}-wal`);
|
|
30
|
+
unlinkSync(`${TEST_DB_PATH}-shm`);
|
|
31
|
+
} catch {
|
|
32
|
+
// ignore
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("getDependentTasks", () => {
|
|
37
|
+
test("finds tasks that depend on a given parent", () => {
|
|
38
|
+
const agent = createAgent({
|
|
39
|
+
name: "dep-lookup-worker-1",
|
|
40
|
+
isLead: false,
|
|
41
|
+
status: "idle",
|
|
42
|
+
capabilities: [],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const parent = createTaskExtended("Parent task", { agentId: agent.id });
|
|
46
|
+
const child = createTaskExtended("Child task", {
|
|
47
|
+
agentId: agent.id,
|
|
48
|
+
dependsOn: [parent.id],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const deps = getDependentTasks(parent.id, { includeTerminal: true });
|
|
52
|
+
expect(deps.length).toBeGreaterThanOrEqual(1);
|
|
53
|
+
expect(deps.some((d) => d.id === child.id)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("filters out terminal tasks by default", () => {
|
|
57
|
+
const agent = createAgent({
|
|
58
|
+
name: "dep-lookup-worker-2",
|
|
59
|
+
isLead: false,
|
|
60
|
+
status: "idle",
|
|
61
|
+
capabilities: [],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const parent = createTaskExtended("Parent task 2", { agentId: agent.id });
|
|
65
|
+
const child1 = createTaskExtended("Child completed", {
|
|
66
|
+
agentId: agent.id,
|
|
67
|
+
dependsOn: [parent.id],
|
|
68
|
+
});
|
|
69
|
+
const child2 = createTaskExtended("Child pending", {
|
|
70
|
+
agentId: agent.id,
|
|
71
|
+
dependsOn: [parent.id],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
startTask(child1.id, agent.id);
|
|
75
|
+
completeTask(child1.id, "done");
|
|
76
|
+
|
|
77
|
+
const nonTerminalDeps = getDependentTasks(parent.id);
|
|
78
|
+
expect(nonTerminalDeps.some((d) => d.id === child1.id)).toBe(false);
|
|
79
|
+
expect(nonTerminalDeps.some((d) => d.id === child2.id)).toBe(true);
|
|
80
|
+
|
|
81
|
+
const allDeps = getDependentTasks(parent.id, { includeTerminal: true });
|
|
82
|
+
expect(allDeps.some((d) => d.id === child1.id)).toBe(true);
|
|
83
|
+
expect(allDeps.some((d) => d.id === child2.id)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("returns empty array when no dependents exist", () => {
|
|
87
|
+
const agent = createAgent({
|
|
88
|
+
name: "dep-lookup-worker-3",
|
|
89
|
+
isLead: false,
|
|
90
|
+
status: "idle",
|
|
91
|
+
capabilities: [],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const task = createTaskExtended("Lonely task", { agentId: agent.id });
|
|
95
|
+
const deps = getDependentTasks(task.id);
|
|
96
|
+
expect(deps).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("cascadeFailDependents", () => {
|
|
101
|
+
test("single-level cascade: failing parent fails its dependent", () => {
|
|
102
|
+
const agent = createAgent({
|
|
103
|
+
name: "cascade-worker-1",
|
|
104
|
+
isLead: false,
|
|
105
|
+
status: "idle",
|
|
106
|
+
capabilities: [],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const parent = createTaskExtended("Parent A", { agentId: agent.id });
|
|
110
|
+
const child = createTaskExtended("Child of A", {
|
|
111
|
+
agentId: agent.id,
|
|
112
|
+
dependsOn: [parent.id],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
startTask(parent.id, agent.id);
|
|
116
|
+
failTask(parent.id, "parent failed");
|
|
117
|
+
|
|
118
|
+
const childAfter = getTaskById(child.id);
|
|
119
|
+
expect(childAfter!.status).toBe("failed");
|
|
120
|
+
expect(childAfter!.failureReason).toContain("Blocked dependency");
|
|
121
|
+
expect(childAfter!.failureReason).toContain("was failed");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("multi-level recursive cascade: A→B→C all fail", () => {
|
|
125
|
+
const agent = createAgent({
|
|
126
|
+
name: "cascade-worker-2",
|
|
127
|
+
isLead: false,
|
|
128
|
+
status: "idle",
|
|
129
|
+
capabilities: [],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const taskA = createTaskExtended("Task A (root)", { agentId: agent.id });
|
|
133
|
+
const taskB = createTaskExtended("Task B (depends on A)", {
|
|
134
|
+
agentId: agent.id,
|
|
135
|
+
dependsOn: [taskA.id],
|
|
136
|
+
});
|
|
137
|
+
const taskC = createTaskExtended("Task C (depends on B)", {
|
|
138
|
+
agentId: agent.id,
|
|
139
|
+
dependsOn: [taskB.id],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
startTask(taskA.id, agent.id);
|
|
143
|
+
failTask(taskA.id, "root failure");
|
|
144
|
+
|
|
145
|
+
const bAfter = getTaskById(taskB.id);
|
|
146
|
+
expect(bAfter!.status).toBe("failed");
|
|
147
|
+
expect(bAfter!.failureReason).toContain("Blocked dependency");
|
|
148
|
+
|
|
149
|
+
const cAfter = getTaskById(taskC.id);
|
|
150
|
+
expect(cAfter!.status).toBe("failed");
|
|
151
|
+
expect(cAfter!.failureReason).toContain("Blocked dependency");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("cycle-safety: A↔B does not infinite-loop", () => {
|
|
155
|
+
const agent = createAgent({
|
|
156
|
+
name: "cascade-worker-3",
|
|
157
|
+
isLead: false,
|
|
158
|
+
status: "idle",
|
|
159
|
+
capabilities: [],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Create tasks with a dependency cycle: A depends on B, B depends on A
|
|
163
|
+
const taskA = createTaskExtended("Cycle A", { agentId: agent.id });
|
|
164
|
+
const taskB = createTaskExtended("Cycle B", {
|
|
165
|
+
agentId: agent.id,
|
|
166
|
+
dependsOn: [taskA.id],
|
|
167
|
+
});
|
|
168
|
+
// Manually update taskA to depend on taskB (creating a cycle)
|
|
169
|
+
getDb().run("UPDATE agent_tasks SET dependsOn = ? WHERE id = ?", [
|
|
170
|
+
JSON.stringify([taskB.id]),
|
|
171
|
+
taskA.id,
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
// This should not infinite-loop — the visited set protects us
|
|
175
|
+
startTask(taskA.id, agent.id);
|
|
176
|
+
const results = cascadeFailDependents(taskA.id, "failed");
|
|
177
|
+
|
|
178
|
+
// taskB should be cascade-failed
|
|
179
|
+
const bAfter = getTaskById(taskB.id);
|
|
180
|
+
expect(bAfter!.status).toBe("failed");
|
|
181
|
+
|
|
182
|
+
// results should include taskB but NOT loop infinitely
|
|
183
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
184
|
+
expect(results.some((r) => r.taskId === taskB.id)).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("already-completed dependent is left untouched", () => {
|
|
188
|
+
const agent = createAgent({
|
|
189
|
+
name: "cascade-worker-4",
|
|
190
|
+
isLead: false,
|
|
191
|
+
status: "idle",
|
|
192
|
+
capabilities: [],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const parent = createTaskExtended("Parent D", { agentId: agent.id });
|
|
196
|
+
const child = createTaskExtended("Child D (completed)", {
|
|
197
|
+
agentId: agent.id,
|
|
198
|
+
dependsOn: [parent.id],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
startTask(child.id, agent.id);
|
|
202
|
+
completeTask(child.id, "finished before parent failed");
|
|
203
|
+
|
|
204
|
+
startTask(parent.id, agent.id);
|
|
205
|
+
failTask(parent.id, "parent failed late");
|
|
206
|
+
|
|
207
|
+
const childAfter = getTaskById(child.id);
|
|
208
|
+
expect(childAfter!.status).toBe("completed");
|
|
209
|
+
expect(childAfter!.output).toBe("finished before parent failed");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("cancelTask cascades to dependents", () => {
|
|
213
|
+
const agent = createAgent({
|
|
214
|
+
name: "cascade-worker-5",
|
|
215
|
+
isLead: false,
|
|
216
|
+
status: "idle",
|
|
217
|
+
capabilities: [],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const parent = createTaskExtended("Parent cancel", { agentId: agent.id });
|
|
221
|
+
const child = createTaskExtended("Child of cancelled", {
|
|
222
|
+
agentId: agent.id,
|
|
223
|
+
dependsOn: [parent.id],
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
cancelTask(parent.id, "no longer needed");
|
|
227
|
+
|
|
228
|
+
const childAfter = getTaskById(child.id);
|
|
229
|
+
expect(childAfter!.status).toBe("failed");
|
|
230
|
+
expect(childAfter!.failureReason).toContain("was cancelled");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("supersedeTask cascades to dependents", () => {
|
|
234
|
+
const agent = createAgent({
|
|
235
|
+
name: "cascade-worker-6",
|
|
236
|
+
isLead: false,
|
|
237
|
+
status: "idle",
|
|
238
|
+
capabilities: [],
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const parent = createTaskExtended("Parent supersede", { agentId: agent.id });
|
|
242
|
+
const child = createTaskExtended("Child of superseded", {
|
|
243
|
+
agentId: agent.id,
|
|
244
|
+
dependsOn: [parent.id],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
startTask(parent.id, agent.id);
|
|
248
|
+
supersedeTask(parent.id, { reason: "context limit", resumeTaskId: null });
|
|
249
|
+
|
|
250
|
+
const childAfter = getTaskById(child.id);
|
|
251
|
+
expect(childAfter!.status).toBe("failed");
|
|
252
|
+
expect(childAfter!.failureReason).toContain("was superseded");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("wide fan-out: multiple dependents all cascade-failed", () => {
|
|
256
|
+
const agent = createAgent({
|
|
257
|
+
name: "cascade-worker-7",
|
|
258
|
+
isLead: false,
|
|
259
|
+
status: "idle",
|
|
260
|
+
capabilities: [],
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const parent = createTaskExtended("Parent wide", { agentId: agent.id });
|
|
264
|
+
const children = Array.from({ length: 5 }, (_, i) =>
|
|
265
|
+
createTaskExtended(`Child ${i}`, {
|
|
266
|
+
agentId: agent.id,
|
|
267
|
+
dependsOn: [parent.id],
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
startTask(parent.id, agent.id);
|
|
272
|
+
failTask(parent.id, "parent gone");
|
|
273
|
+
|
|
274
|
+
for (const child of children) {
|
|
275
|
+
const after = getTaskById(child.id);
|
|
276
|
+
expect(after!.status).toBe("failed");
|
|
277
|
+
expect(after!.failureReason).toContain("Blocked dependency");
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("diamond dependency: C depends on both A and B, only A fails", () => {
|
|
282
|
+
const agent = createAgent({
|
|
283
|
+
name: "cascade-worker-8",
|
|
284
|
+
isLead: false,
|
|
285
|
+
status: "idle",
|
|
286
|
+
capabilities: [],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const taskA = createTaskExtended("Diamond A", { agentId: agent.id });
|
|
290
|
+
const taskB = createTaskExtended("Diamond B", { agentId: agent.id });
|
|
291
|
+
const taskC = createTaskExtended("Diamond C (depends on A and B)", {
|
|
292
|
+
agentId: agent.id,
|
|
293
|
+
dependsOn: [taskA.id, taskB.id],
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
startTask(taskA.id, agent.id);
|
|
297
|
+
failTask(taskA.id, "A failed");
|
|
298
|
+
|
|
299
|
+
// C should be cascade-failed because one of its dependencies failed
|
|
300
|
+
const cAfter = getTaskById(taskC.id);
|
|
301
|
+
expect(cAfter!.status).toBe("failed");
|
|
302
|
+
expect(cAfter!.failureReason).toContain("Blocked dependency");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "workflow",
|
|
3
|
+
"name": "llm-safe-release-context",
|
|
4
|
+
"displayName": "LLM-Safe Release Context",
|
|
5
|
+
"slug": "llm-safe-release-context",
|
|
6
|
+
"title": "LLM-Safe Release Context",
|
|
7
|
+
"description": "Pattern for release-note workflows that keep full audit context separate from slim prompt context.",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"category": "workflows",
|
|
10
|
+
"placeholders": ["ORG_ID", "REPO_URL"],
|
|
11
|
+
"runAllSeedersCandidate": false,
|
|
12
|
+
"tags": ["workflow", "release-notes", "context"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# LLM-Safe Release Context
|
|
2
|
+
|
|
3
|
+
Use this pattern for release-note, changelog, and report workflows where one node gathers a large source artifact and later LLM nodes plan or write from it.
|
|
4
|
+
|
|
5
|
+
The key rule: keep the full artifact for audit/debugging, but pass a compact projection to every LLM prompt.
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"name": "LLM-safe release context",
|
|
10
|
+
"description": "Build full release context and a slim prompt-safe projection before downstream LLM nodes run.",
|
|
11
|
+
"nodes": [
|
|
12
|
+
{
|
|
13
|
+
"id": "context-builder",
|
|
14
|
+
"type": "agent-task",
|
|
15
|
+
"config": {
|
|
16
|
+
"template": "Build release context for {{REPO_URL}}. Write the full audit artifact to agent-fs at release-runs/{DATE}/context.json. Also write release-runs/{DATE}/context-slim.json with at most 150 commit objects containing only hash, shortHash, author, date, message, and changed file paths. Do not include patch bodies, diff hunks, raw git log --stat output, downloaded HTML, or other bulk text in context-slim.json. Return contextPath and contextSlimPath.",
|
|
17
|
+
"outputSchema": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"skip": { "type": "boolean" },
|
|
21
|
+
"reason": { "type": "string" },
|
|
22
|
+
"contextPath": { "type": "string" },
|
|
23
|
+
"contextSlimPath": { "type": "string" },
|
|
24
|
+
"itemCount": { "type": "number" }
|
|
25
|
+
},
|
|
26
|
+
"required": ["skip"]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"next": "plan-release"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "plan-release",
|
|
33
|
+
"type": "agent-task",
|
|
34
|
+
"inputs": { "context": "context-builder" },
|
|
35
|
+
"config": {
|
|
36
|
+
"template": "Read the slim context only:\nagent-fs --org {{ORG_ID}} cat {{context.taskOutput.contextSlimPath}}\n\nDo not read {{context.taskOutput.contextPath}} unless a human explicitly asks for the full audit artifact. Plan the release from the slim context and return JSON.",
|
|
37
|
+
"outputSchema": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"planPath": { "type": "string" },
|
|
41
|
+
"heroChange": { "type": "string" },
|
|
42
|
+
"themes": { "type": "array", "items": { "type": "string" } }
|
|
43
|
+
},
|
|
44
|
+
"required": ["planPath", "heroChange"]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"next": "write-release"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "write-release",
|
|
51
|
+
"type": "agent-task",
|
|
52
|
+
"inputs": { "context": "context-builder", "plan": "plan-release" },
|
|
53
|
+
"config": {
|
|
54
|
+
"template": "Read the approved plan and slim context only:\nagent-fs --org {{ORG_ID}} cat {{plan.taskOutput.planPath}}\nagent-fs --org {{ORG_ID}} cat {{context.taskOutput.contextSlimPath}}\n\nDo not read {{context.taskOutput.contextPath}}. Write the release artifact and return JSON.",
|
|
55
|
+
"outputSchema": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"contentPath": { "type": "string" },
|
|
59
|
+
"title": { "type": "string" }
|
|
60
|
+
},
|
|
61
|
+
"required": ["contentPath", "title"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This avoids the common failure mode where a large `context.json` fills the model window and the workflow fails as "structured output required" before the agent has enough context left to call `store-progress`.
|