@desplega.ai/agent-swarm 1.86.0 → 1.87.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.
- package/openapi.json +72 -1
- package/package.json +3 -1
- package/src/be/db-queries/tracker.ts +21 -0
- package/src/be/db.ts +235 -14
- package/src/be/migrations/079_task_followup_config.sql +1 -0
- package/src/be/modelsdev-cache.json +77663 -74073
- package/src/cli.tsx +26 -0
- package/src/commands/context-preamble.ts +272 -0
- package/src/commands/e2b.ts +728 -0
- package/src/commands/resume-session.ts +35 -78
- package/src/commands/runner.ts +125 -13
- package/src/e2b/dispatch.ts +429 -0
- package/src/e2b/env.ts +206 -0
- package/src/heartbeat/heartbeat.ts +145 -30
- package/src/heartbeat/templates.ts +11 -7
- package/src/http/session-data.ts +8 -1
- package/src/http/tasks.ts +152 -3
- package/src/jira/sync.ts +4 -4
- package/src/linear/sync.ts +6 -5
- package/src/providers/claude-adapter.ts +10 -76
- package/src/providers/claude-managed-adapter.ts +61 -75
- package/src/providers/codex-adapter.ts +15 -18
- package/src/providers/codex-oauth/auth-json.ts +18 -1
- package/src/providers/codex-oauth/flow.ts +24 -1
- package/src/providers/types.ts +6 -0
- package/src/tasks/worker-follow-up.ts +162 -2
- package/src/telemetry.ts +11 -1
- package/src/tests/claude-adapter.test.ts +5 -27
- package/src/tests/claude-managed-adapter.test.ts +38 -52
- package/src/tests/codex-adapter.test.ts +6 -31
- package/src/tests/codex-oauth.test.ts +149 -3
- package/src/tests/codex-pool.test.ts +14 -3
- package/src/tests/e2b-dispatch.test.ts +330 -0
- package/src/tests/heartbeat-supersede-resume.test.ts +285 -0
- package/src/tests/heartbeat.test.ts +26 -16
- package/src/tests/prompt-template-remaining.test.ts +4 -0
- package/src/tests/resume-session.test.ts +42 -50
- package/src/tests/structured-output.test.ts +69 -0
- package/src/tests/task-completion-idempotency.test.ts +185 -2
- package/src/tests/task-supersede-resume.test.ts +722 -0
- package/src/tests/telemetry-init.test.ts +69 -0
- package/src/tests/vcs-tracking.test.ts +39 -0
- package/src/tools/send-task.ts +12 -1
- package/src/tools/store-progress.ts +2 -2
- package/src/tools/templates.ts +14 -2
- package/src/types.ts +46 -1
- package/src/workflows/executors/agent-task.ts +3 -0
|
@@ -208,7 +208,7 @@ describe("Heartbeat Triage", () => {
|
|
|
208
208
|
// ==========================================================================
|
|
209
209
|
|
|
210
210
|
describe("Code-Level Triage", () => {
|
|
211
|
-
test("auto-
|
|
211
|
+
test("auto-supersedes stalled task with no active session (DES-523)", async () => {
|
|
212
212
|
const agent = createAgent({ name: "dead-worker", isLead: false, status: "busy" });
|
|
213
213
|
const task = createTaskExtended("Stalled task", { agentId: agent.id });
|
|
214
214
|
startTask(task.id);
|
|
@@ -219,17 +219,21 @@ describe("Heartbeat Triage", () => {
|
|
|
219
219
|
|
|
220
220
|
const findings = await codeLevelTriage();
|
|
221
221
|
|
|
222
|
-
expect(findings.
|
|
223
|
-
expect(findings.
|
|
222
|
+
expect(findings.autoResumedTasks.length).toBe(1);
|
|
223
|
+
expect(findings.autoResumedTasks[0]!.taskId).toBe(task.id);
|
|
224
|
+
expect(findings.autoResumedTasks[0]!.reason).toContain("no active session");
|
|
225
|
+
expect(findings.autoFailedTasks.length).toBe(0);
|
|
224
226
|
expect(findings.stalledTasks.length).toBe(0);
|
|
225
227
|
|
|
226
|
-
// Verify task is
|
|
228
|
+
// Verify task is superseded (not failed) — the resume task carries the work forward.
|
|
229
|
+
// `failureReason` is unset on superseded tasks; the supersede reason lives on the log
|
|
230
|
+
// entry and on `findings.autoResumedTasks[].reason` (checked above).
|
|
227
231
|
const updated = getTaskById(task.id);
|
|
228
|
-
expect(updated?.status).toBe("
|
|
229
|
-
expect(updated?.failureReason).
|
|
232
|
+
expect(updated?.status).toBe("superseded");
|
|
233
|
+
expect(updated?.failureReason).toBeFalsy();
|
|
230
234
|
});
|
|
231
235
|
|
|
232
|
-
test("auto-
|
|
236
|
+
test("auto-supersedes stalled task with stale session heartbeat (DES-523)", async () => {
|
|
233
237
|
const agent = createAgent({ name: "crashed-worker", isLead: false, status: "busy" });
|
|
234
238
|
const task = createTaskExtended("Stalled task", { agentId: agent.id });
|
|
235
239
|
startTask(task.id);
|
|
@@ -250,14 +254,18 @@ describe("Heartbeat Triage", () => {
|
|
|
250
254
|
|
|
251
255
|
const findings = await codeLevelTriage();
|
|
252
256
|
|
|
253
|
-
expect(findings.
|
|
254
|
-
expect(findings.
|
|
257
|
+
expect(findings.autoResumedTasks.length).toBe(1);
|
|
258
|
+
expect(findings.autoResumedTasks[0]!.taskId).toBe(task.id);
|
|
259
|
+
expect(findings.autoResumedTasks[0]!.reason).toContain("stale");
|
|
260
|
+
expect(findings.autoFailedTasks.length).toBe(0);
|
|
255
261
|
expect(findings.stalledTasks.length).toBe(0);
|
|
256
262
|
|
|
257
|
-
// Verify task is
|
|
263
|
+
// Verify task is superseded and session is deleted.
|
|
264
|
+
// `failureReason` is unset on superseded tasks; the supersede reason lives on the log
|
|
265
|
+
// entry and on `findings.autoResumedTasks[].reason` (checked above).
|
|
258
266
|
const updated = getTaskById(task.id);
|
|
259
|
-
expect(updated?.status).toBe("
|
|
260
|
-
expect(updated?.failureReason).
|
|
267
|
+
expect(updated?.status).toBe("superseded");
|
|
268
|
+
expect(updated?.failureReason).toBeFalsy();
|
|
261
269
|
|
|
262
270
|
const session = getActiveSessionForTask(task.id);
|
|
263
271
|
expect(session).toBeNull();
|
|
@@ -361,7 +369,7 @@ describe("Heartbeat Triage", () => {
|
|
|
361
369
|
expect(findings.stalledTasks.length).toBe(0);
|
|
362
370
|
});
|
|
363
371
|
|
|
364
|
-
test("sets agent to idle after auto-
|
|
372
|
+
test("sets agent to idle after auto-superseding its only task", async () => {
|
|
365
373
|
const agent = createAgent({ name: "dead-worker", isLead: false, status: "busy" });
|
|
366
374
|
const task = createTaskExtended("Stalled task", { agentId: agent.id });
|
|
367
375
|
startTask(task.id);
|
|
@@ -371,7 +379,8 @@ describe("Heartbeat Triage", () => {
|
|
|
371
379
|
|
|
372
380
|
await codeLevelTriage();
|
|
373
381
|
|
|
374
|
-
// Agent should be set to idle since
|
|
382
|
+
// Agent should be set to idle since the parent task is terminal (superseded)
|
|
383
|
+
// and the resume follow-up was routed to the unassigned pool (worker is "dead").
|
|
375
384
|
const agents = getDb().query("SELECT status FROM agents WHERE id = ?").get(agent.id) as {
|
|
376
385
|
status: string;
|
|
377
386
|
};
|
|
@@ -404,7 +413,7 @@ describe("Heartbeat Triage", () => {
|
|
|
404
413
|
expect(tasks.length).toBe(1);
|
|
405
414
|
});
|
|
406
415
|
|
|
407
|
-
test("auto-
|
|
416
|
+
test("auto-supersedes stalled task with no session during sweep", async () => {
|
|
408
417
|
const worker = createAgent({ name: "dead-worker", isLead: false, status: "busy" });
|
|
409
418
|
const task = createTaskExtended("Stalled no-session", { agentId: worker.id });
|
|
410
419
|
startTask(task.id);
|
|
@@ -415,7 +424,8 @@ describe("Heartbeat Triage", () => {
|
|
|
415
424
|
await runHeartbeatSweep();
|
|
416
425
|
|
|
417
426
|
const updated = getTaskById(task.id);
|
|
418
|
-
|
|
427
|
+
// DES-523: heartbeat sweep now creates a resume follow-up instead of silently failing.
|
|
428
|
+
expect(updated?.status).toBe("superseded");
|
|
419
429
|
});
|
|
420
430
|
|
|
421
431
|
test("cleans stale sessions even when preflight gate bails", async () => {
|
|
@@ -324,6 +324,7 @@ describe("Task lifecycle — backward compatibility", () => {
|
|
|
324
324
|
test("task.worker.completed produces expected output", () => {
|
|
325
325
|
const result = resolveTemplate("task.worker.completed", {
|
|
326
326
|
agent_name: "Worker-1",
|
|
327
|
+
creator_agent: "creator-agent-1",
|
|
327
328
|
task_desc: "Fix the login page CSS",
|
|
328
329
|
output_summary: "Fixed the CSS alignment issue on the login form",
|
|
329
330
|
task_id: "task-abc-123",
|
|
@@ -334,6 +335,7 @@ describe("Task lifecycle — backward compatibility", () => {
|
|
|
334
335
|
const expected = `Worker task completed \u2014 review needed.
|
|
335
336
|
|
|
336
337
|
Agent: Worker-1
|
|
338
|
+
Original task created by agent creator-agent-1
|
|
337
339
|
Task: "Fix the login page CSS"
|
|
338
340
|
|
|
339
341
|
Output:
|
|
@@ -352,6 +354,7 @@ Use \`get-task-details\` with taskId "task-abc-123" for full details.`;
|
|
|
352
354
|
test("task.worker.failed produces expected output", () => {
|
|
353
355
|
const result = resolveTemplate("task.worker.failed", {
|
|
354
356
|
agent_name: "Worker-2",
|
|
357
|
+
creator_agent: "creator-agent-2",
|
|
355
358
|
task_desc: "Deploy to production",
|
|
356
359
|
failure_reason: "Docker build failed: missing dependency",
|
|
357
360
|
task_id: "task-def-456",
|
|
@@ -362,6 +365,7 @@ Use \`get-task-details\` with taskId "task-abc-123" for full details.`;
|
|
|
362
365
|
const expected = `Worker task failed \u2014 action needed.
|
|
363
366
|
|
|
364
367
|
Agent: Worker-2
|
|
368
|
+
Original task created by agent creator-agent-2
|
|
365
369
|
Task: "Deploy to production"
|
|
366
370
|
|
|
367
371
|
Failure reason: Docker build failed: missing dependency
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { resolveResumeSession } from "../commands/resume-session";
|
|
2
|
+
import { RESUME_DEPRECATED_REASON, resolveResumeSession } from "../commands/resume-session";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// Native resume was deprecated in the 2026-05-28 plan. resolveResumeSession
|
|
5
|
+
// is now an observability shim — it records the candidates that would have
|
|
6
|
+
// been resume targets in the old world, but never returns a resumeSessionId.
|
|
7
|
+
|
|
8
|
+
describe("resolveResumeSession (observability shim)", () => {
|
|
9
|
+
test("returns no resumeSessionId for any candidate", () => {
|
|
6
10
|
const resolution = resolveResumeSession("claude", [
|
|
7
11
|
{
|
|
8
12
|
source: "task",
|
|
@@ -11,28 +15,18 @@ describe("resolveResumeSession", () => {
|
|
|
11
15
|
},
|
|
12
16
|
]);
|
|
13
17
|
|
|
14
|
-
expect(resolution.resumeSessionId).
|
|
15
|
-
expect(resolution.source).
|
|
16
|
-
expect(resolution.provider).
|
|
17
|
-
expect(resolution.skipped).toEqual([]);
|
|
18
|
+
expect(resolution.resumeSessionId).toBeUndefined();
|
|
19
|
+
expect(resolution.source).toBeUndefined();
|
|
20
|
+
expect(resolution.provider).toBeUndefined();
|
|
18
21
|
});
|
|
19
22
|
|
|
20
|
-
test("
|
|
23
|
+
test("records every non-empty candidate in skipped with the deprecation reason", () => {
|
|
21
24
|
const resolution = resolveResumeSession("claude", [
|
|
22
25
|
{
|
|
23
26
|
source: "task",
|
|
24
|
-
sessionId: "
|
|
27
|
+
sessionId: "69dbe5a1-1130-45eb-983f-58a7a13c9c3c",
|
|
25
28
|
provider: "claude",
|
|
26
29
|
},
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
expect(resolution.resumeSessionId).toBeUndefined();
|
|
30
|
-
expect(resolution.skipped).toHaveLength(1);
|
|
31
|
-
expect(resolution.skipped[0]?.reason).toBe("Claude CLI --resume requires a UUID session id");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("normalizes legacy managed Claude rows to claude-managed", () => {
|
|
35
|
-
const resolution = resolveResumeSession("claude-managed", [
|
|
36
30
|
{
|
|
37
31
|
source: "parent",
|
|
38
32
|
sessionId: "sesn_resume_xyz",
|
|
@@ -41,53 +35,51 @@ describe("resolveResumeSession", () => {
|
|
|
41
35
|
},
|
|
42
36
|
]);
|
|
43
37
|
|
|
44
|
-
expect(resolution.
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
expect(resolution.skipped).toHaveLength(2);
|
|
39
|
+
for (const entry of resolution.skipped) {
|
|
40
|
+
expect(entry.reason).toBe(RESUME_DEPRECATED_REASON);
|
|
41
|
+
}
|
|
42
|
+
expect(resolution.skipped[0]?.source).toBe("task");
|
|
43
|
+
expect(resolution.skipped[0]?.sessionId).toBe("69dbe5a1-1130-45eb-983f-58a7a13c9c3c");
|
|
44
|
+
expect(resolution.skipped[1]?.source).toBe("parent");
|
|
45
|
+
expect(resolution.skipped[1]?.sessionId).toBe("sesn_resume_xyz");
|
|
47
46
|
});
|
|
48
47
|
|
|
49
|
-
test("
|
|
48
|
+
test("ignores candidates with empty / whitespace-only sessionId", () => {
|
|
50
49
|
const resolution = resolveResumeSession("claude", [
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
source: "parent",
|
|
58
|
-
sessionId: "69dbe5a1-1130-45eb-983f-58a7a13c9c3c",
|
|
59
|
-
provider: "claude",
|
|
60
|
-
},
|
|
50
|
+
{ source: "task", sessionId: undefined },
|
|
51
|
+
{ source: "task", sessionId: null },
|
|
52
|
+
{ source: "task", sessionId: "" },
|
|
53
|
+
{ source: "parent", sessionId: " " },
|
|
61
54
|
]);
|
|
62
55
|
|
|
63
|
-
expect(resolution.
|
|
64
|
-
expect(resolution.
|
|
65
|
-
expect(resolution.skipped).toHaveLength(1);
|
|
66
|
-
expect(resolution.skipped[0]?.reason).toContain("does not match current provider");
|
|
56
|
+
expect(resolution.skipped).toEqual([]);
|
|
57
|
+
expect(resolution.resumeSessionId).toBeUndefined();
|
|
67
58
|
});
|
|
68
59
|
|
|
69
|
-
test("
|
|
60
|
+
test("preserves the candidate provider in the skipped entry for observability", () => {
|
|
70
61
|
const resolution = resolveResumeSession("claude", [
|
|
71
62
|
{
|
|
72
63
|
source: "task",
|
|
73
|
-
sessionId: "
|
|
64
|
+
sessionId: "thread-codex",
|
|
65
|
+
provider: "codex",
|
|
74
66
|
},
|
|
75
67
|
]);
|
|
76
68
|
|
|
77
|
-
expect(resolution.
|
|
78
|
-
expect(resolution.skipped[0]?.
|
|
69
|
+
expect(resolution.skipped).toHaveLength(1);
|
|
70
|
+
expect(resolution.skipped[0]?.provider).toBe("codex");
|
|
71
|
+
expect(resolution.skipped[0]?.reason).toBe(RESUME_DEPRECATED_REASON);
|
|
79
72
|
});
|
|
80
73
|
|
|
81
|
-
test("
|
|
82
|
-
const
|
|
83
|
-
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
]);
|
|
74
|
+
test("currentProvider is ignored — same skipped output regardless", () => {
|
|
75
|
+
const candidates = [
|
|
76
|
+
{ source: "task" as const, sessionId: "abc-123", provider: "claude" as const },
|
|
77
|
+
];
|
|
78
|
+
const a = resolveResumeSession("claude", candidates);
|
|
79
|
+
const b = resolveResumeSession("pi", candidates);
|
|
80
|
+
const c = resolveResumeSession("codex", candidates);
|
|
89
81
|
|
|
90
|
-
expect(
|
|
91
|
-
expect(
|
|
82
|
+
expect(a.skipped).toEqual(b.skipped);
|
|
83
|
+
expect(b.skipped).toEqual(c.skipped);
|
|
92
84
|
});
|
|
93
85
|
});
|
|
@@ -253,6 +253,29 @@ describe("AgentTaskConfigSchema — outputSchema", () => {
|
|
|
253
253
|
expect(parsed.data.outputSchema).toEqual(config.outputSchema);
|
|
254
254
|
}
|
|
255
255
|
});
|
|
256
|
+
|
|
257
|
+
test("accepts followUpConfig in config", async () => {
|
|
258
|
+
const { AgentTaskExecutor } = await import("../workflows/executors/agent-task");
|
|
259
|
+
const executor = new AgentTaskExecutor({
|
|
260
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock DB for test
|
|
261
|
+
db: {} as any,
|
|
262
|
+
eventBus: { emit: () => {}, on: () => {}, off: () => {} },
|
|
263
|
+
interpolate: (t: string) => t,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const config = {
|
|
267
|
+
template: "Count files",
|
|
268
|
+
followUpConfig: {
|
|
269
|
+
onCompleted: "post the URL",
|
|
270
|
+
onFailed: "page Taras",
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
const parsed = executor.configSchema.safeParse(config);
|
|
274
|
+
expect(parsed.success).toBe(true);
|
|
275
|
+
if (parsed.success) {
|
|
276
|
+
expect(parsed.data.followUpConfig).toEqual(config.followUpConfig);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
256
279
|
});
|
|
257
280
|
|
|
258
281
|
// ─── Workflow agent-task → DB with outputSchema ──────────────
|
|
@@ -305,4 +328,50 @@ describe("Workflow agent-task creates task with outputSchema", () => {
|
|
|
305
328
|
expect(task).toBeDefined();
|
|
306
329
|
expect(task!.outputSchema).toEqual(schema);
|
|
307
330
|
});
|
|
331
|
+
|
|
332
|
+
test("followUpConfig flows from executor config to created task", async () => {
|
|
333
|
+
const { AgentTaskExecutor } = await import("../workflows/executors/agent-task");
|
|
334
|
+
const db = await import("../be/db");
|
|
335
|
+
|
|
336
|
+
const executor = new AgentTaskExecutor({
|
|
337
|
+
db: db as typeof import("../be/db"),
|
|
338
|
+
eventBus: { emit: () => {}, on: () => {}, off: () => {} },
|
|
339
|
+
interpolate: (t: string) => t,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const wf = createWorkflow({
|
|
343
|
+
name: "test-follow-up-config",
|
|
344
|
+
definition: { nodes: [], edges: [] },
|
|
345
|
+
});
|
|
346
|
+
const run = createWorkflowRun({ id: crypto.randomUUID(), workflowId: wf.id });
|
|
347
|
+
const step = createWorkflowRunStep({
|
|
348
|
+
id: crypto.randomUUID(),
|
|
349
|
+
runId: run.id,
|
|
350
|
+
nodeId: "n1",
|
|
351
|
+
nodeType: "agent-task",
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const followUpConfig = { disabled: true };
|
|
355
|
+
|
|
356
|
+
const result = await executor.run({
|
|
357
|
+
config: {
|
|
358
|
+
template: "Summarize the document",
|
|
359
|
+
followUpConfig,
|
|
360
|
+
},
|
|
361
|
+
context: {},
|
|
362
|
+
meta: {
|
|
363
|
+
runId: run.id,
|
|
364
|
+
stepId: step.id,
|
|
365
|
+
nodeId: "n1",
|
|
366
|
+
workflowId: wf.id,
|
|
367
|
+
dryRun: false,
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(result.status).toBe("success");
|
|
372
|
+
const taskId = (result as { correlationId?: string }).correlationId!;
|
|
373
|
+
const task = getTaskById(taskId);
|
|
374
|
+
expect(task).toBeDefined();
|
|
375
|
+
expect(task!.followUpConfig).toEqual(followUpConfig);
|
|
376
|
+
});
|
|
308
377
|
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createTaskExtended,
|
|
9
9
|
failTask,
|
|
10
10
|
getDb,
|
|
11
|
+
getLeadAgent,
|
|
11
12
|
getLogsByTaskId,
|
|
12
13
|
getTaskById,
|
|
13
14
|
initDb,
|
|
@@ -362,7 +363,7 @@ describe("worker task follow-up creation", () => {
|
|
|
362
363
|
slackThreadTs: "1700000000.000001",
|
|
363
364
|
slackUserId: "U123",
|
|
364
365
|
});
|
|
365
|
-
startTask(task.id
|
|
366
|
+
startTask(task.id);
|
|
366
367
|
|
|
367
368
|
const completed = completeTask(task.id, "Worker output");
|
|
368
369
|
expect(completed).not.toBeNull();
|
|
@@ -382,6 +383,152 @@ describe("worker task follow-up creation", () => {
|
|
|
382
383
|
expect(rows[0]!.slackThreadTs).toBe("1700000000.000001");
|
|
383
384
|
expect(rows[0]!.slackUserId).toBe("U123");
|
|
384
385
|
expect(rows[0]!.task).toContain("Worker output");
|
|
386
|
+
expect(rows[0]!.task).not.toContain("{{follow_up_instructions}}");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("skips lead follow-up when followUpConfig disables it", () => {
|
|
390
|
+
createAgent({
|
|
391
|
+
name: "follow-up-lead-disabled",
|
|
392
|
+
isLead: true,
|
|
393
|
+
status: "idle",
|
|
394
|
+
capabilities: [],
|
|
395
|
+
});
|
|
396
|
+
const worker = createAgent({
|
|
397
|
+
name: "follow-up-worker-disabled",
|
|
398
|
+
isLead: false,
|
|
399
|
+
status: "idle",
|
|
400
|
+
capabilities: [],
|
|
401
|
+
});
|
|
402
|
+
const task = createTaskExtended("Silent worker task", {
|
|
403
|
+
agentId: worker.id,
|
|
404
|
+
followUpConfig: { disabled: true },
|
|
405
|
+
});
|
|
406
|
+
startTask(task.id);
|
|
407
|
+
|
|
408
|
+
const completed = completeTask(task.id, "Worker output");
|
|
409
|
+
expect(completed).not.toBeNull();
|
|
410
|
+
|
|
411
|
+
const followUp = createWorkerTaskFollowUp({
|
|
412
|
+
task: completed!,
|
|
413
|
+
status: "completed",
|
|
414
|
+
output: "Worker output",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(followUp).toBeNull();
|
|
418
|
+
expect(listFollowUpTasks(task.id)).toHaveLength(0);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("injects onCompleted instructions into completed follow-up", () => {
|
|
422
|
+
createAgent({
|
|
423
|
+
name: "follow-up-lead-completed-instructions",
|
|
424
|
+
isLead: true,
|
|
425
|
+
status: "idle",
|
|
426
|
+
capabilities: [],
|
|
427
|
+
});
|
|
428
|
+
const worker = createAgent({
|
|
429
|
+
name: "follow-up-worker-completed-instructions",
|
|
430
|
+
isLead: false,
|
|
431
|
+
status: "idle",
|
|
432
|
+
capabilities: [],
|
|
433
|
+
});
|
|
434
|
+
const task = createTaskExtended("Worker task with completed instructions", {
|
|
435
|
+
agentId: worker.id,
|
|
436
|
+
creatorAgentId: worker.id,
|
|
437
|
+
followUpConfig: { onCompleted: "post the URL" },
|
|
438
|
+
});
|
|
439
|
+
startTask(task.id);
|
|
440
|
+
|
|
441
|
+
const completed = completeTask(task.id, "Worker output");
|
|
442
|
+
expect(completed).not.toBeNull();
|
|
443
|
+
|
|
444
|
+
const followUp = createWorkerTaskFollowUp({
|
|
445
|
+
task: completed!,
|
|
446
|
+
status: "completed",
|
|
447
|
+
output: "Worker output",
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(followUp).not.toBeNull();
|
|
451
|
+
const rows = listFollowUpTasks(task.id);
|
|
452
|
+
expect(rows).toHaveLength(1);
|
|
453
|
+
expect(rows[0]!.task).toContain(`Original task created by agent ${worker.id}`);
|
|
454
|
+
expect(rows[0]!.task).toContain("Additional instructions from the task creator:");
|
|
455
|
+
expect(rows[0]!.task).toContain("post the URL");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("injects only onFailed instructions into failed follow-up", () => {
|
|
459
|
+
createAgent({
|
|
460
|
+
name: "follow-up-lead-failed-instructions",
|
|
461
|
+
isLead: true,
|
|
462
|
+
status: "idle",
|
|
463
|
+
capabilities: [],
|
|
464
|
+
});
|
|
465
|
+
const worker = createAgent({
|
|
466
|
+
name: "follow-up-worker-failed-instructions",
|
|
467
|
+
isLead: false,
|
|
468
|
+
status: "idle",
|
|
469
|
+
capabilities: [],
|
|
470
|
+
});
|
|
471
|
+
const task = createTaskExtended("Worker task with failed instructions", {
|
|
472
|
+
agentId: worker.id,
|
|
473
|
+
creatorAgentId: worker.id,
|
|
474
|
+
followUpConfig: { onCompleted: "post the URL", onFailed: "page Taras" },
|
|
475
|
+
});
|
|
476
|
+
startTask(task.id);
|
|
477
|
+
|
|
478
|
+
const failed = failTask(task.id, "boom");
|
|
479
|
+
expect(failed).not.toBeNull();
|
|
480
|
+
|
|
481
|
+
const followUp = createWorkerTaskFollowUp({
|
|
482
|
+
task: failed!,
|
|
483
|
+
status: "failed",
|
|
484
|
+
failureReason: "boom",
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
expect(followUp).not.toBeNull();
|
|
488
|
+
const rows = listFollowUpTasks(task.id);
|
|
489
|
+
expect(rows).toHaveLength(1);
|
|
490
|
+
expect(rows[0]!.task).toContain(`Original task created by agent ${worker.id}`);
|
|
491
|
+
expect(rows[0]!.task).toContain("page Taras");
|
|
492
|
+
expect(rows[0]!.task).not.toContain("post the URL");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("inherits followUpConfig from parent task when child has no override", () => {
|
|
496
|
+
createAgent({
|
|
497
|
+
name: "follow-up-lead-inheritance",
|
|
498
|
+
isLead: true,
|
|
499
|
+
status: "idle",
|
|
500
|
+
capabilities: [],
|
|
501
|
+
});
|
|
502
|
+
const worker = createAgent({
|
|
503
|
+
name: "follow-up-worker-inheritance",
|
|
504
|
+
isLead: false,
|
|
505
|
+
status: "idle",
|
|
506
|
+
capabilities: [],
|
|
507
|
+
});
|
|
508
|
+
const parent = createTaskExtended("Parent task", {
|
|
509
|
+
agentId: worker.id,
|
|
510
|
+
followUpConfig: { disabled: true },
|
|
511
|
+
});
|
|
512
|
+
const child = createTaskExtended("Child task", {
|
|
513
|
+
agentId: worker.id,
|
|
514
|
+
parentTaskId: parent.id,
|
|
515
|
+
});
|
|
516
|
+
startTask(child.id);
|
|
517
|
+
|
|
518
|
+
const fetchedChild = getTaskById(child.id);
|
|
519
|
+
expect(fetchedChild!.followUpConfig).toEqual({ disabled: true });
|
|
520
|
+
|
|
521
|
+
const completed = completeTask(child.id, "Child output");
|
|
522
|
+
expect(completed).not.toBeNull();
|
|
523
|
+
|
|
524
|
+
const followUp = createWorkerTaskFollowUp({
|
|
525
|
+
task: completed!,
|
|
526
|
+
status: "completed",
|
|
527
|
+
output: "Child output",
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
expect(followUp).toBeNull();
|
|
531
|
+
expect(listFollowUpTasks(child.id)).toHaveLength(0);
|
|
385
532
|
});
|
|
386
533
|
|
|
387
534
|
test("does not create follow-up for lead-owned task", () => {
|
|
@@ -392,7 +539,7 @@ describe("worker task follow-up creation", () => {
|
|
|
392
539
|
capabilities: [],
|
|
393
540
|
});
|
|
394
541
|
const task = createTaskExtended("Lead task", { agentId: lead.id });
|
|
395
|
-
startTask(task.id
|
|
542
|
+
startTask(task.id);
|
|
396
543
|
|
|
397
544
|
const completed = completeTask(task.id, "Lead output");
|
|
398
545
|
expect(completed).not.toBeNull();
|
|
@@ -406,4 +553,40 @@ describe("worker task follow-up creation", () => {
|
|
|
406
553
|
expect(followUp).toBeNull();
|
|
407
554
|
expect(listFollowUpTasks(task.id)).toHaveLength(0);
|
|
408
555
|
});
|
|
556
|
+
|
|
557
|
+
test("marks original creator as you when lead created the worker task", () => {
|
|
558
|
+
const lead =
|
|
559
|
+
getLeadAgent() ??
|
|
560
|
+
createAgent({
|
|
561
|
+
name: "follow-up-lead-creator-you",
|
|
562
|
+
isLead: true,
|
|
563
|
+
status: "idle",
|
|
564
|
+
capabilities: [],
|
|
565
|
+
});
|
|
566
|
+
const worker = createAgent({
|
|
567
|
+
name: "follow-up-worker-creator-you",
|
|
568
|
+
isLead: false,
|
|
569
|
+
status: "idle",
|
|
570
|
+
capabilities: [],
|
|
571
|
+
});
|
|
572
|
+
const task = createTaskExtended("Worker task created by lead", {
|
|
573
|
+
agentId: worker.id,
|
|
574
|
+
creatorAgentId: lead.id,
|
|
575
|
+
});
|
|
576
|
+
startTask(task.id);
|
|
577
|
+
|
|
578
|
+
const completed = completeTask(task.id, "Worker output");
|
|
579
|
+
expect(completed).not.toBeNull();
|
|
580
|
+
|
|
581
|
+
const followUp = createWorkerTaskFollowUp({
|
|
582
|
+
task: completed!,
|
|
583
|
+
status: "completed",
|
|
584
|
+
output: "Worker output",
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
expect(followUp).not.toBeNull();
|
|
588
|
+
const rows = listFollowUpTasks(task.id);
|
|
589
|
+
expect(rows).toHaveLength(1);
|
|
590
|
+
expect(rows[0]!.task).toContain(`Original task created by agent ${lead.id} (you)`);
|
|
591
|
+
});
|
|
409
592
|
});
|