@desplega.ai/agent-swarm 1.75.0 → 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.
- package/README.md +1 -1
- package/openapi.json +973 -36
- package/package.json +2 -2
- package/src/be/db.ts +527 -9
- package/src/be/memory/raters/llm-summarizer.ts +218 -0
- package/src/be/memory/raters/llm.ts +56 -75
- package/src/be/memory/retrieval-store.ts +21 -0
- package/src/be/migrations/054_agent_harness_provider.sql +21 -0
- package/src/be/migrations/055_agent_cred_status.sql +15 -0
- package/src/be/migrations/056_drop_agent_tasks_source_check.sql +139 -0
- package/src/be/migrations/057_inbox_item_state.sql +27 -0
- package/src/be/migrations/058_task_templates.sql +31 -0
- package/src/be/swarm-config-guard.ts +24 -0
- package/src/commands/credential-wait.ts +1 -1
- package/src/commands/provider-credentials.ts +434 -0
- package/src/commands/runner.ts +229 -42
- package/src/hooks/hook.ts +115 -95
- package/src/http/agents.ts +82 -2
- package/src/http/config.ts +11 -1
- package/src/http/inbox-state.ts +89 -0
- package/src/http/index.ts +10 -0
- package/src/http/sessions.ts +86 -0
- package/src/http/status.ts +665 -0
- package/src/http/task-templates.ts +51 -0
- package/src/http/tasks.ts +85 -5
- package/src/http/users.ts +134 -0
- package/src/providers/claude-adapter.ts +5 -0
- package/src/providers/codex-adapter.ts +1 -1
- package/src/providers/index.ts +1 -1
- package/src/slack/handlers.ts +0 -1
- package/src/tests/agents-harness-provider.test.ts +333 -0
- package/src/tests/credential-check.test.ts +32 -1
- package/src/tests/credential-status-api.test.ts +42 -0
- package/src/tests/harness-provider-resolution.test.ts +242 -0
- package/src/tests/jira-sync.test.ts +1 -1
- package/src/tests/memory-rater-llm-summarizer.test.ts +317 -0
- package/src/tests/memory-rater-llm.test.ts +265 -107
- package/src/tests/migration-runner-regressions.test.ts +17 -2
- package/src/tests/sessions.test.ts +141 -0
- package/src/tests/status.test.ts +843 -0
- package/src/tests/stop-hook-task-resolution.test.ts +98 -0
- package/src/tests/template-recommendations.test.ts +148 -0
- package/src/tests/use-dismissible-card.test.ts +140 -0
- package/src/tools/swarm-config/set-config.ts +17 -1
- package/src/types.ts +117 -0
- package/src/utils/harness-provider.ts +32 -0
- package/tsconfig.json +0 -2
- package/src/providers/credentials.ts +0 -74
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { listTaskTemplates } from "../be/db";
|
|
4
|
+
import { TaskTemplateKindSchema } from "../types";
|
|
5
|
+
import { route } from "./route-def";
|
|
6
|
+
import { json } from "./utils";
|
|
7
|
+
|
|
8
|
+
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const listTemplates = route({
|
|
11
|
+
method: "get",
|
|
12
|
+
path: "/api/task-templates",
|
|
13
|
+
pattern: ["api", "task-templates"],
|
|
14
|
+
summary: "List task templates ('To start' bucket)",
|
|
15
|
+
tags: ["Task Templates"],
|
|
16
|
+
query: z.object({
|
|
17
|
+
category: z.string().optional(),
|
|
18
|
+
/** v2 hook — v1 callers always pass `kind=task` (or omit). */
|
|
19
|
+
kind: TaskTemplateKindSchema.optional(),
|
|
20
|
+
/** Case-insensitive LIKE match against `title` OR `description`. */
|
|
21
|
+
query: z.string().optional(),
|
|
22
|
+
}),
|
|
23
|
+
responses: {
|
|
24
|
+
200: { description: "Task template list" },
|
|
25
|
+
401: { description: "Unauthorized" },
|
|
26
|
+
},
|
|
27
|
+
auth: { apiKey: true },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export async function handleTaskTemplates(
|
|
33
|
+
req: IncomingMessage,
|
|
34
|
+
res: ServerResponse,
|
|
35
|
+
pathSegments: string[],
|
|
36
|
+
queryParams: URLSearchParams,
|
|
37
|
+
): Promise<boolean> {
|
|
38
|
+
if (listTemplates.match(req.method, pathSegments)) {
|
|
39
|
+
const parsed = await listTemplates.parse(req, res, pathSegments, queryParams);
|
|
40
|
+
if (!parsed) return true;
|
|
41
|
+
const templates = listTaskTemplates({
|
|
42
|
+
category: parsed.query.category,
|
|
43
|
+
kind: parsed.query.kind,
|
|
44
|
+
query: parsed.query.query,
|
|
45
|
+
});
|
|
46
|
+
json(res, { templates });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
package/src/http/tasks.ts
CHANGED
|
@@ -7,10 +7,12 @@ import {
|
|
|
7
7
|
failTask,
|
|
8
8
|
getAllTasks,
|
|
9
9
|
getDb,
|
|
10
|
+
getLeadAgent,
|
|
10
11
|
getLogsByTaskId,
|
|
11
12
|
getPausedTasksForAgent,
|
|
12
13
|
getTaskById,
|
|
13
14
|
getTasksCount,
|
|
15
|
+
getUserById,
|
|
14
16
|
pauseTask,
|
|
15
17
|
resumeTask,
|
|
16
18
|
updateAgentStatusFromCapacity,
|
|
@@ -20,7 +22,13 @@ import {
|
|
|
20
22
|
} from "../be/db";
|
|
21
23
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
22
24
|
import { telemetry } from "../telemetry";
|
|
23
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
type AgentTaskSource,
|
|
27
|
+
AgentTaskSourceSchema,
|
|
28
|
+
type AgentTaskStatus,
|
|
29
|
+
AgentTaskStatusSchema,
|
|
30
|
+
ProviderNameSchema,
|
|
31
|
+
} from "../types";
|
|
24
32
|
import { route } from "./route-def";
|
|
25
33
|
import { json, jsonError } from "./utils";
|
|
26
34
|
|
|
@@ -33,16 +41,22 @@ const listTasks = route({
|
|
|
33
41
|
summary: "List tasks with filters",
|
|
34
42
|
tags: ["Tasks"],
|
|
35
43
|
query: z.object({
|
|
44
|
+
/** Single status, or comma-separated list (e.g. "failed,cancelled"). */
|
|
36
45
|
status: z.string().optional(),
|
|
37
46
|
agentId: z.string().optional(),
|
|
38
47
|
scheduleId: z.string().optional(),
|
|
39
48
|
search: z.string().optional(),
|
|
40
49
|
includeHeartbeat: z.enum(["true", "false"]).optional(),
|
|
50
|
+
/** ISO 8601 — return only tasks created on/after this timestamp. */
|
|
51
|
+
createdAfter: z.string().datetime().optional(),
|
|
52
|
+
/** Comma-separated source filter (e.g. `ui,slack`). Omit to include all. */
|
|
53
|
+
source: z.string().optional(),
|
|
41
54
|
limit: z.coerce.number().int().optional(),
|
|
42
55
|
offset: z.coerce.number().int().optional(),
|
|
43
56
|
}),
|
|
44
57
|
responses: {
|
|
45
58
|
200: { description: "Paginated task list" },
|
|
59
|
+
400: { description: "Validation error (e.g. unknown status token)" },
|
|
46
60
|
},
|
|
47
61
|
});
|
|
48
62
|
|
|
@@ -62,9 +76,10 @@ const createTask = route({
|
|
|
62
76
|
offeredTo: z.string().optional(),
|
|
63
77
|
dir: z.string().optional(),
|
|
64
78
|
parentTaskId: z.string().optional(),
|
|
65
|
-
source:
|
|
79
|
+
source: AgentTaskSourceSchema.optional(),
|
|
66
80
|
outputSchema: z.record(z.string(), z.unknown()).optional(),
|
|
67
81
|
contextKey: z.string().optional(),
|
|
82
|
+
requestedByUserId: z.string().optional(),
|
|
68
83
|
}),
|
|
69
84
|
responses: {
|
|
70
85
|
201: { description: "Task created" },
|
|
@@ -239,12 +254,53 @@ export async function handleTasks(
|
|
|
239
254
|
if (listTasks.match(req.method, pathSegments)) {
|
|
240
255
|
const parsed = await listTasks.parse(req, res, pathSegments, queryParams);
|
|
241
256
|
if (!parsed) return true;
|
|
257
|
+
|
|
258
|
+
// Multi-status CSV: split on `,` and validate each token against the
|
|
259
|
+
// canonical enum. Empty / single-status callers still work.
|
|
260
|
+
let status: AgentTaskStatus | AgentTaskStatus[] | undefined;
|
|
261
|
+
if (parsed.query.status) {
|
|
262
|
+
const tokens = parsed.query.status
|
|
263
|
+
.split(",")
|
|
264
|
+
.map((s) => s.trim())
|
|
265
|
+
.filter(Boolean);
|
|
266
|
+
const validated: AgentTaskStatus[] = [];
|
|
267
|
+
for (const tok of tokens) {
|
|
268
|
+
const result = AgentTaskStatusSchema.safeParse(tok);
|
|
269
|
+
if (!result.success) {
|
|
270
|
+
jsonError(res, `Invalid status token: ${tok}`, 400);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
validated.push(result.data);
|
|
274
|
+
}
|
|
275
|
+
status = validated.length === 1 ? validated[0] : validated;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let source: AgentTaskSource[] | undefined;
|
|
279
|
+
if (parsed.query.source) {
|
|
280
|
+
const tokens = parsed.query.source
|
|
281
|
+
.split(",")
|
|
282
|
+
.map((s) => s.trim())
|
|
283
|
+
.filter(Boolean);
|
|
284
|
+
const validated: AgentTaskSource[] = [];
|
|
285
|
+
for (const tok of tokens) {
|
|
286
|
+
const result = AgentTaskSourceSchema.safeParse(tok);
|
|
287
|
+
if (!result.success) {
|
|
288
|
+
jsonError(res, `Invalid source token: ${tok}`, 400);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
validated.push(result.data);
|
|
292
|
+
}
|
|
293
|
+
if (validated.length > 0) source = validated;
|
|
294
|
+
}
|
|
295
|
+
|
|
242
296
|
const filters = {
|
|
243
|
-
status
|
|
297
|
+
status,
|
|
244
298
|
agentId: parsed.query.agentId || undefined,
|
|
245
299
|
scheduleId: parsed.query.scheduleId || undefined,
|
|
246
300
|
search: parsed.query.search || undefined,
|
|
247
301
|
includeHeartbeat: parsed.query.includeHeartbeat === "true" || undefined,
|
|
302
|
+
createdAfter: parsed.query.createdAfter || undefined,
|
|
303
|
+
source,
|
|
248
304
|
limit: parsed.query.limit,
|
|
249
305
|
offset: parsed.query.offset,
|
|
250
306
|
};
|
|
@@ -258,9 +314,32 @@ export async function handleTasks(
|
|
|
258
314
|
const parsed = await createTask.parse(req, res, pathSegments, queryParams);
|
|
259
315
|
if (!parsed) return true;
|
|
260
316
|
|
|
317
|
+
// Tolerant `requestedByUserId`: prevent the deleted-user race from
|
|
318
|
+
// becoming a 500 — if the referenced user doesn't exist, log and drop
|
|
319
|
+
// the field rather than letting the FK fail at INSERT.
|
|
320
|
+
let requestedByUserId = parsed.body.requestedByUserId || undefined;
|
|
321
|
+
if (requestedByUserId && !getUserById(requestedByUserId)) {
|
|
322
|
+
console.warn(
|
|
323
|
+
`[tasks] requestedByUserId ${requestedByUserId} does not exist — coercing to NULL`,
|
|
324
|
+
);
|
|
325
|
+
requestedByUserId = undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Default agent for ingress-created tasks: when no explicit `agentId` is
|
|
329
|
+
// provided, route to the lead so the task has an owner immediately
|
|
330
|
+
// (regardless of whether it's a root or a follow-up under a parentTaskId).
|
|
331
|
+
// Without this, UI composer follow-ups land unassigned and never get
|
|
332
|
+
// picked up. Mirrors Slack's pattern (slack/actions.ts uses lead?.id when
|
|
333
|
+
// there's no working agent).
|
|
334
|
+
let defaultAgentId = parsed.body.agentId || undefined;
|
|
335
|
+
if (!defaultAgentId) {
|
|
336
|
+
const lead = getLeadAgent();
|
|
337
|
+
if (lead) defaultAgentId = lead.id;
|
|
338
|
+
}
|
|
339
|
+
|
|
261
340
|
try {
|
|
262
341
|
const task = createTaskWithSiblingAwareness(parsed.body.task, {
|
|
263
|
-
agentId:
|
|
342
|
+
agentId: defaultAgentId,
|
|
264
343
|
creatorAgentId: myAgentId || undefined,
|
|
265
344
|
taskType: parsed.body.taskType || undefined,
|
|
266
345
|
tags: parsed.body.tags || undefined,
|
|
@@ -269,9 +348,10 @@ export async function handleTasks(
|
|
|
269
348
|
offeredTo: parsed.body.offeredTo || undefined,
|
|
270
349
|
dir: parsed.body.dir || undefined,
|
|
271
350
|
parentTaskId: parsed.body.parentTaskId || undefined,
|
|
272
|
-
source:
|
|
351
|
+
source: parsed.body.source || "api",
|
|
273
352
|
outputSchema: parsed.body.outputSchema || undefined,
|
|
274
353
|
contextKey: parsed.body.contextKey || undefined,
|
|
354
|
+
requestedByUserId,
|
|
275
355
|
});
|
|
276
356
|
|
|
277
357
|
ensure({
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createUser, getAllUsers, getUserById, updateUser } from "../be/db";
|
|
4
|
+
import { route } from "./route-def";
|
|
5
|
+
import { json, jsonError } from "./utils";
|
|
6
|
+
|
|
7
|
+
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const listUsers = route({
|
|
10
|
+
method: "get",
|
|
11
|
+
path: "/api/users",
|
|
12
|
+
pattern: ["api", "users"],
|
|
13
|
+
summary: "List all users",
|
|
14
|
+
tags: ["Users"],
|
|
15
|
+
responses: {
|
|
16
|
+
200: { description: "List of users" },
|
|
17
|
+
401: { description: "Unauthorized" },
|
|
18
|
+
},
|
|
19
|
+
auth: { apiKey: true },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const createUserRoute = route({
|
|
23
|
+
method: "post",
|
|
24
|
+
path: "/api/users",
|
|
25
|
+
pattern: ["api", "users"],
|
|
26
|
+
summary: "Create a new user",
|
|
27
|
+
tags: ["Users"],
|
|
28
|
+
body: z.object({
|
|
29
|
+
name: z.string().min(1),
|
|
30
|
+
email: z.string().optional(),
|
|
31
|
+
role: z.string().optional(),
|
|
32
|
+
notes: z.string().optional(),
|
|
33
|
+
slackUserId: z.string().optional(),
|
|
34
|
+
linearUserId: z.string().optional(),
|
|
35
|
+
githubUsername: z.string().optional(),
|
|
36
|
+
gitlabUsername: z.string().optional(),
|
|
37
|
+
emailAliases: z.array(z.string()).optional(),
|
|
38
|
+
preferredChannel: z.string().optional(),
|
|
39
|
+
timezone: z.string().optional(),
|
|
40
|
+
}),
|
|
41
|
+
responses: {
|
|
42
|
+
200: { description: "User created" },
|
|
43
|
+
400: { description: "Validation error" },
|
|
44
|
+
401: { description: "Unauthorized" },
|
|
45
|
+
},
|
|
46
|
+
auth: { apiKey: true },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const updateUserRoute = route({
|
|
50
|
+
method: "put",
|
|
51
|
+
path: "/api/users/{id}",
|
|
52
|
+
pattern: ["api", "users", null],
|
|
53
|
+
summary: "Update an existing user (partial — at least one field required)",
|
|
54
|
+
tags: ["Users"],
|
|
55
|
+
params: z.object({ id: z.string() }),
|
|
56
|
+
body: z
|
|
57
|
+
.object({
|
|
58
|
+
name: z.string().min(1).optional(),
|
|
59
|
+
email: z.string().optional(),
|
|
60
|
+
role: z.string().optional(),
|
|
61
|
+
notes: z.string().optional(),
|
|
62
|
+
slackUserId: z.string().optional(),
|
|
63
|
+
linearUserId: z.string().optional(),
|
|
64
|
+
githubUsername: z.string().optional(),
|
|
65
|
+
gitlabUsername: z.string().optional(),
|
|
66
|
+
emailAliases: z.array(z.string()).optional(),
|
|
67
|
+
preferredChannel: z.string().optional(),
|
|
68
|
+
timezone: z.string().optional(),
|
|
69
|
+
})
|
|
70
|
+
.refine((v) => Object.keys(v).length > 0, {
|
|
71
|
+
message: "At least one field must be provided",
|
|
72
|
+
}),
|
|
73
|
+
responses: {
|
|
74
|
+
200: { description: "User updated" },
|
|
75
|
+
400: { description: "Validation error or empty body" },
|
|
76
|
+
401: { description: "Unauthorized" },
|
|
77
|
+
404: { description: "User not found" },
|
|
78
|
+
},
|
|
79
|
+
auth: { apiKey: true },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export async function handleUsers(
|
|
85
|
+
req: IncomingMessage,
|
|
86
|
+
res: ServerResponse,
|
|
87
|
+
pathSegments: string[],
|
|
88
|
+
queryParams: URLSearchParams,
|
|
89
|
+
): Promise<boolean> {
|
|
90
|
+
if (listUsers.match(req.method, pathSegments)) {
|
|
91
|
+
const parsed = await listUsers.parse(req, res, pathSegments, queryParams);
|
|
92
|
+
if (!parsed) return true;
|
|
93
|
+
const users = getAllUsers();
|
|
94
|
+
json(res, { users });
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (createUserRoute.match(req.method, pathSegments)) {
|
|
99
|
+
const parsed = await createUserRoute.parse(req, res, pathSegments, queryParams);
|
|
100
|
+
if (!parsed) return true;
|
|
101
|
+
try {
|
|
102
|
+
const user = createUser(parsed.body);
|
|
103
|
+
json(res, { user });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to create user", 500);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (updateUserRoute.match(req.method, pathSegments)) {
|
|
111
|
+
const parsed = await updateUserRoute.parse(req, res, pathSegments, queryParams);
|
|
112
|
+
if (!parsed) return true;
|
|
113
|
+
|
|
114
|
+
// 404 if user not found before update — keeps the contract honest.
|
|
115
|
+
if (!getUserById(parsed.params.id)) {
|
|
116
|
+
jsonError(res, "User not found", 404);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const user = updateUser(parsed.params.id, parsed.body);
|
|
122
|
+
if (!user) {
|
|
123
|
+
jsonError(res, "User not found", 404);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
json(res, { user });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to update user", 500);
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
@@ -194,6 +194,11 @@ class ClaudeSession implements ProviderSession {
|
|
|
194
194
|
ENABLE_PROMPT_CACHING_1H: "1",
|
|
195
195
|
...(config.env || process.env),
|
|
196
196
|
TASK_FILE: taskFilePath,
|
|
197
|
+
// Belt-and-braces: TASK_FILE on disk can disappear mid-session (race
|
|
198
|
+
// with task lifecycle), which silently drops the Stop-hook memory
|
|
199
|
+
// rater. The hook prefers these env vars when present. See PR #444.
|
|
200
|
+
AGENT_SWARM_TASK_ID: config.taskId,
|
|
201
|
+
AGENT_SWARM_AGENT_ID: config.agentId,
|
|
197
202
|
} as Record<string, string>,
|
|
198
203
|
stdout: "pipe",
|
|
199
204
|
stderr: "pipe",
|
|
@@ -690,7 +690,7 @@ class CodexSession implements ProviderSession {
|
|
|
690
690
|
// `contextPercent` is on a 0-100 scale across all providers — claude
|
|
691
691
|
// emits `(used / total) * 100`, pi-mono passes through `usage.percent`
|
|
692
692
|
// which is already 0-100. The dashboard at
|
|
693
|
-
//
|
|
693
|
+
// ui/src/pages/tasks/[id]/page.tsx renders it via `.toFixed(0)`
|
|
694
694
|
// expecting an integer percent, so a 0-1 fraction would render as
|
|
695
695
|
// "0%" instead of e.g. "40%".
|
|
696
696
|
this.emit({
|
package/src/providers/index.ts
CHANGED