@desplega.ai/agent-swarm 1.51.2 → 1.52.1
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 +131 -0
- package/openapi.json +767 -4
- package/package.json +3 -1
- package/src/be/db.ts +669 -0
- package/src/be/migrations/019_skills.sql +65 -0
- package/src/be/migrations/020_approval_requests.sql +41 -0
- package/src/be/migrations/runner.ts +4 -4
- package/src/be/skill-parser.ts +70 -0
- package/src/be/skill-sync.ts +106 -0
- package/src/commands/runner.ts +299 -52
- package/src/http/agents.ts +29 -0
- package/src/http/approval-requests.ts +310 -0
- package/src/http/config.ts +3 -3
- package/src/http/index.ts +26 -2
- package/src/http/poll.ts +15 -0
- package/src/http/skills.ts +479 -0
- package/src/http/tasks.ts +94 -0
- package/src/linear/outbound.ts +12 -12
- package/src/prompts/base-prompt.ts +8 -0
- package/src/providers/claude-adapter.ts +19 -3
- package/src/scheduler/scheduler.ts +24 -1
- package/src/server.ts +29 -0
- package/src/slack/blocks.ts +1 -1
- package/src/tests/approval-requests.test.ts +948 -0
- package/src/tests/skill-parser.test.ts +178 -0
- package/src/tests/skill-sync.test.ts +171 -0
- package/src/tests/slack-blocks.test.ts +3 -2
- package/src/tests/structured-output.test.ts +1 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/tool-call-progress.test.ts +207 -0
- package/src/tests/tool-registrar-no-input.test.ts +114 -0
- package/src/tests/update-profile-auth.test.ts +1 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tools/request-human-input.ts +117 -0
- package/src/tools/skills/index.ts +11 -0
- package/src/tools/skills/skill-create.ts +105 -0
- package/src/tools/skills/skill-delete.ts +67 -0
- package/src/tools/skills/skill-get.ts +75 -0
- package/src/tools/skills/skill-install-remote.ts +152 -0
- package/src/tools/skills/skill-install.ts +101 -0
- package/src/tools/skills/skill-list.ts +77 -0
- package/src/tools/skills/skill-publish.ts +123 -0
- package/src/tools/skills/skill-search.ts +43 -0
- package/src/tools/skills/skill-sync-remote.ts +128 -0
- package/src/tools/skills/skill-uninstall.ts +60 -0
- package/src/tools/skills/skill-update.ts +128 -0
- package/src/tools/store-progress.ts +31 -0
- package/src/tools/templates.ts +28 -0
- package/src/tools/tool-config.ts +16 -0
- package/src/tools/utils.ts +9 -7
- package/src/types.ts +54 -0
- package/src/workflows/executors/human-in-the-loop.ts +273 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/recovery.ts +72 -0
- package/src/workflows/resume.ts +65 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
createSkill,
|
|
5
|
+
deleteSkill,
|
|
6
|
+
getAgentSkills,
|
|
7
|
+
getSkillById,
|
|
8
|
+
installSkill,
|
|
9
|
+
listSkills,
|
|
10
|
+
searchSkills,
|
|
11
|
+
uninstallSkill,
|
|
12
|
+
updateSkill,
|
|
13
|
+
} from "../be/db";
|
|
14
|
+
import { parseSkillContent } from "../be/skill-parser";
|
|
15
|
+
import { syncSkillsToFilesystem } from "../be/skill-sync";
|
|
16
|
+
import { route } from "./route-def";
|
|
17
|
+
import { json, jsonError } from "./utils";
|
|
18
|
+
|
|
19
|
+
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const listSkillsRoute = route({
|
|
22
|
+
method: "get",
|
|
23
|
+
path: "/api/skills",
|
|
24
|
+
pattern: ["api", "skills"],
|
|
25
|
+
summary: "List skills with optional filters",
|
|
26
|
+
tags: ["Skills"],
|
|
27
|
+
auth: { apiKey: true },
|
|
28
|
+
query: z.object({
|
|
29
|
+
type: z.string().optional(),
|
|
30
|
+
scope: z.string().optional(),
|
|
31
|
+
agentId: z.string().optional(),
|
|
32
|
+
enabled: z.string().optional(),
|
|
33
|
+
search: z.string().optional(),
|
|
34
|
+
}),
|
|
35
|
+
responses: {
|
|
36
|
+
200: { description: "Skill list" },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const getSkillRoute = route({
|
|
41
|
+
method: "get",
|
|
42
|
+
path: "/api/skills/{id}",
|
|
43
|
+
pattern: ["api", "skills", null],
|
|
44
|
+
summary: "Get skill by ID",
|
|
45
|
+
tags: ["Skills"],
|
|
46
|
+
auth: { apiKey: true },
|
|
47
|
+
params: z.object({ id: z.string() }),
|
|
48
|
+
responses: {
|
|
49
|
+
200: { description: "Skill details" },
|
|
50
|
+
404: { description: "Skill not found" },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const createSkillRoute = route({
|
|
55
|
+
method: "post",
|
|
56
|
+
path: "/api/skills",
|
|
57
|
+
pattern: ["api", "skills"],
|
|
58
|
+
summary: "Create a new skill",
|
|
59
|
+
tags: ["Skills"],
|
|
60
|
+
auth: { apiKey: true },
|
|
61
|
+
body: z.object({
|
|
62
|
+
content: z.string().min(1),
|
|
63
|
+
type: z.string().optional(),
|
|
64
|
+
scope: z.string().optional(),
|
|
65
|
+
ownerAgentId: z.string().optional(),
|
|
66
|
+
}),
|
|
67
|
+
responses: {
|
|
68
|
+
201: { description: "Skill created" },
|
|
69
|
+
400: { description: "Validation error" },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const updateSkillRoute = route({
|
|
74
|
+
method: "put",
|
|
75
|
+
path: "/api/skills/{id}",
|
|
76
|
+
pattern: ["api", "skills", null],
|
|
77
|
+
summary: "Update a skill",
|
|
78
|
+
tags: ["Skills"],
|
|
79
|
+
auth: { apiKey: true },
|
|
80
|
+
params: z.object({ id: z.string() }),
|
|
81
|
+
body: z.record(z.string(), z.unknown()),
|
|
82
|
+
responses: {
|
|
83
|
+
200: { description: "Skill updated" },
|
|
84
|
+
404: { description: "Skill not found" },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const deleteSkillRoute = route({
|
|
89
|
+
method: "delete",
|
|
90
|
+
path: "/api/skills/{id}",
|
|
91
|
+
pattern: ["api", "skills", null],
|
|
92
|
+
summary: "Delete a skill",
|
|
93
|
+
tags: ["Skills"],
|
|
94
|
+
auth: { apiKey: true },
|
|
95
|
+
params: z.object({ id: z.string() }),
|
|
96
|
+
responses: {
|
|
97
|
+
200: { description: "Skill deleted" },
|
|
98
|
+
404: { description: "Skill not found" },
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const installSkillRoute = route({
|
|
103
|
+
method: "post",
|
|
104
|
+
path: "/api/skills/{id}/install",
|
|
105
|
+
pattern: ["api", "skills", null, "install"],
|
|
106
|
+
summary: "Install skill for an agent",
|
|
107
|
+
tags: ["Skills"],
|
|
108
|
+
auth: { apiKey: true },
|
|
109
|
+
params: z.object({ id: z.string() }),
|
|
110
|
+
body: z.object({
|
|
111
|
+
agentId: z.string(),
|
|
112
|
+
}),
|
|
113
|
+
responses: {
|
|
114
|
+
200: { description: "Skill installed" },
|
|
115
|
+
404: { description: "Skill not found" },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const uninstallSkillRoute = route({
|
|
120
|
+
method: "delete",
|
|
121
|
+
path: "/api/skills/{id}/install/{agentId}",
|
|
122
|
+
pattern: ["api", "skills", null, "install", null],
|
|
123
|
+
summary: "Uninstall skill for an agent",
|
|
124
|
+
tags: ["Skills"],
|
|
125
|
+
auth: { apiKey: true },
|
|
126
|
+
params: z.object({ id: z.string(), agentId: z.string() }),
|
|
127
|
+
responses: {
|
|
128
|
+
200: { description: "Skill uninstalled" },
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const installRemoteRoute = route({
|
|
133
|
+
method: "post",
|
|
134
|
+
path: "/api/skills/install-remote",
|
|
135
|
+
pattern: ["api", "skills", "install-remote"],
|
|
136
|
+
summary: "Install a remote skill from GitHub",
|
|
137
|
+
tags: ["Skills"],
|
|
138
|
+
auth: { apiKey: true },
|
|
139
|
+
body: z.object({
|
|
140
|
+
sourceRepo: z.string(),
|
|
141
|
+
sourcePath: z.string().optional(),
|
|
142
|
+
scope: z.string().optional(),
|
|
143
|
+
isComplex: z.boolean().optional(),
|
|
144
|
+
}),
|
|
145
|
+
responses: {
|
|
146
|
+
201: { description: "Remote skill installed" },
|
|
147
|
+
400: { description: "Fetch failed" },
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const syncRemoteRoute = route({
|
|
152
|
+
method: "post",
|
|
153
|
+
path: "/api/skills/sync-remote",
|
|
154
|
+
pattern: ["api", "skills", "sync-remote"],
|
|
155
|
+
summary: "Trigger remote skill sync",
|
|
156
|
+
tags: ["Skills"],
|
|
157
|
+
auth: { apiKey: true },
|
|
158
|
+
body: z.object({
|
|
159
|
+
skillId: z.string().optional(),
|
|
160
|
+
force: z.boolean().optional(),
|
|
161
|
+
}),
|
|
162
|
+
responses: {
|
|
163
|
+
200: { description: "Sync results" },
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const syncFilesystemRoute = route({
|
|
168
|
+
method: "post",
|
|
169
|
+
path: "/api/skills/sync-filesystem",
|
|
170
|
+
pattern: ["api", "skills", "sync-filesystem"],
|
|
171
|
+
summary: "Sync installed skills to agent filesystem",
|
|
172
|
+
tags: ["Skills"],
|
|
173
|
+
auth: { apiKey: true, agentId: true },
|
|
174
|
+
responses: {
|
|
175
|
+
200: { description: "Filesystem sync results" },
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const getAgentSkillsRoute = route({
|
|
180
|
+
method: "get",
|
|
181
|
+
path: "/api/agents/{id}/skills",
|
|
182
|
+
pattern: ["api", "agents", null, "skills"],
|
|
183
|
+
summary: "Get all skills installed for an agent",
|
|
184
|
+
tags: ["Skills"],
|
|
185
|
+
auth: { apiKey: true },
|
|
186
|
+
params: z.object({ id: z.string() }),
|
|
187
|
+
responses: {
|
|
188
|
+
200: { description: "Agent skills list" },
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export async function handleSkills(
|
|
195
|
+
req: IncomingMessage,
|
|
196
|
+
res: ServerResponse,
|
|
197
|
+
pathSegments: string[],
|
|
198
|
+
queryParams: URLSearchParams,
|
|
199
|
+
myAgentId: string | undefined,
|
|
200
|
+
): Promise<boolean> {
|
|
201
|
+
// GET /api/agents/:id/skills (must be before /api/skills routes)
|
|
202
|
+
if (getAgentSkillsRoute.match(req.method, pathSegments)) {
|
|
203
|
+
const parsed = await getAgentSkillsRoute.parse(req, res, pathSegments, queryParams);
|
|
204
|
+
if (!parsed) return true;
|
|
205
|
+
const skills = getAgentSkills(parsed.params.id);
|
|
206
|
+
json(res, { skills, total: skills.length });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// POST /api/skills/install-remote (must be before /api/skills/:id)
|
|
211
|
+
if (installRemoteRoute.match(req.method, pathSegments)) {
|
|
212
|
+
const parsed = await installRemoteRoute.parse(req, res, pathSegments, queryParams);
|
|
213
|
+
if (!parsed) return true;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const branch = "main";
|
|
217
|
+
const filePath = parsed.body.sourcePath ? `${parsed.body.sourcePath}/SKILL.md` : "SKILL.md";
|
|
218
|
+
const rawUrl = `https://raw.githubusercontent.com/${parsed.body.sourceRepo}/${branch}/${filePath}`;
|
|
219
|
+
|
|
220
|
+
let content = "";
|
|
221
|
+
if (!parsed.body.isComplex) {
|
|
222
|
+
const response = await fetch(rawUrl);
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
jsonError(res, `Failed to fetch SKILL.md: HTTP ${response.status}`, 400);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
content = await response.text();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let name: string;
|
|
231
|
+
let description: string;
|
|
232
|
+
if (content) {
|
|
233
|
+
const pm = parseSkillContent(content);
|
|
234
|
+
name = pm.name;
|
|
235
|
+
description = pm.description;
|
|
236
|
+
} else {
|
|
237
|
+
name = parsed.body.sourcePath?.split("/").pop() || parsed.body.sourceRepo;
|
|
238
|
+
description = `Complex skill from ${parsed.body.sourceRepo}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const skill = createSkill({
|
|
242
|
+
name,
|
|
243
|
+
description,
|
|
244
|
+
content,
|
|
245
|
+
type: "remote",
|
|
246
|
+
scope: (parsed.body.scope as "global" | "swarm") ?? "global",
|
|
247
|
+
sourceUrl: rawUrl,
|
|
248
|
+
sourceRepo: parsed.body.sourceRepo,
|
|
249
|
+
sourcePath: parsed.body.sourcePath,
|
|
250
|
+
sourceBranch: branch,
|
|
251
|
+
isComplex: parsed.body.isComplex ?? false,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
json(res, { skill }, 201);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
jsonError(res, err instanceof Error ? err.message : "Unknown error", 400);
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// POST /api/skills/sync-remote
|
|
262
|
+
if (syncRemoteRoute.match(req.method, pathSegments)) {
|
|
263
|
+
const parsed = await syncRemoteRoute.parse(req, res, pathSegments, queryParams);
|
|
264
|
+
if (!parsed) return true;
|
|
265
|
+
|
|
266
|
+
const remoteSkills = parsed.body.skillId
|
|
267
|
+
? (() => {
|
|
268
|
+
const s = getSkillById(parsed.body.skillId!);
|
|
269
|
+
return s && s.type === "remote" ? [s] : [];
|
|
270
|
+
})()
|
|
271
|
+
: listSkills({ type: "remote" });
|
|
272
|
+
|
|
273
|
+
let updated = 0;
|
|
274
|
+
const errors: string[] = [];
|
|
275
|
+
|
|
276
|
+
for (const skill of remoteSkills) {
|
|
277
|
+
if (skill.isComplex || !skill.sourceRepo) continue;
|
|
278
|
+
try {
|
|
279
|
+
const fp = skill.sourcePath ? `${skill.sourcePath}/SKILL.md` : "SKILL.md";
|
|
280
|
+
const url = `https://raw.githubusercontent.com/${skill.sourceRepo}/${skill.sourceBranch}/${fp}`;
|
|
281
|
+
const resp = await fetch(url);
|
|
282
|
+
if (!resp.ok) {
|
|
283
|
+
errors.push(`${skill.name}: HTTP ${resp.status}`);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const newContent = await resp.text();
|
|
287
|
+
const newHash = new Bun.CryptoHasher("sha256").update(newContent).digest("hex");
|
|
288
|
+
const now = new Date().toISOString();
|
|
289
|
+
|
|
290
|
+
if (parsed.body.force || newHash !== skill.sourceHash) {
|
|
291
|
+
const pm = parseSkillContent(newContent);
|
|
292
|
+
updateSkill(skill.id, {
|
|
293
|
+
content: newContent,
|
|
294
|
+
name: pm.name,
|
|
295
|
+
description: pm.description,
|
|
296
|
+
allowedTools: pm.allowedTools,
|
|
297
|
+
model: pm.model,
|
|
298
|
+
effort: pm.effort,
|
|
299
|
+
context: pm.context,
|
|
300
|
+
agent: pm.agent,
|
|
301
|
+
disableModelInvocation: pm.disableModelInvocation,
|
|
302
|
+
userInvocable: pm.userInvocable,
|
|
303
|
+
sourceHash: newHash,
|
|
304
|
+
lastFetchedAt: now,
|
|
305
|
+
});
|
|
306
|
+
updated++;
|
|
307
|
+
} else {
|
|
308
|
+
// Content unchanged — still update lastFetchedAt
|
|
309
|
+
updateSkill(skill.id, { lastFetchedAt: now });
|
|
310
|
+
}
|
|
311
|
+
} catch (err) {
|
|
312
|
+
errors.push(`${skill.name}: ${err instanceof Error ? err.message : "Unknown"}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
json(res, { updated, checked: remoteSkills.length, errors });
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// POST /api/skills/sync-filesystem
|
|
321
|
+
if (syncFilesystemRoute.match(req.method, pathSegments)) {
|
|
322
|
+
// This endpoint is called by the runner to sync skills to the filesystem
|
|
323
|
+
const agentId = myAgentId;
|
|
324
|
+
if (!agentId) {
|
|
325
|
+
jsonError(res, "X-Agent-ID required", 400);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const result = syncSkillsToFilesystem(agentId);
|
|
330
|
+
json(res, {
|
|
331
|
+
synced: result.synced,
|
|
332
|
+
removed: result.removed,
|
|
333
|
+
errors: result.errors,
|
|
334
|
+
message: `Synced ${result.synced} skills, removed ${result.removed} stale entries`,
|
|
335
|
+
});
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// POST /api/skills/:id/install
|
|
340
|
+
if (installSkillRoute.match(req.method, pathSegments)) {
|
|
341
|
+
const parsed = await installSkillRoute.parse(req, res, pathSegments, queryParams);
|
|
342
|
+
if (!parsed) return true;
|
|
343
|
+
|
|
344
|
+
const skill = getSkillById(parsed.params.id);
|
|
345
|
+
if (!skill) {
|
|
346
|
+
jsonError(res, "Skill not found", 404);
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const agentSkill = installSkill(parsed.body.agentId, parsed.params.id);
|
|
352
|
+
json(res, { agentSkill });
|
|
353
|
+
} catch (err) {
|
|
354
|
+
jsonError(res, err instanceof Error ? err.message : "Install failed", 400);
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// DELETE /api/skills/:id/install/:agentId
|
|
360
|
+
if (uninstallSkillRoute.match(req.method, pathSegments)) {
|
|
361
|
+
const parsed = await uninstallSkillRoute.parse(req, res, pathSegments, queryParams);
|
|
362
|
+
if (!parsed) return true;
|
|
363
|
+
|
|
364
|
+
const removed = uninstallSkill(parsed.params.agentId, parsed.params.id);
|
|
365
|
+
json(res, { success: removed });
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// GET /api/skills
|
|
370
|
+
if (listSkillsRoute.match(req.method, pathSegments)) {
|
|
371
|
+
const parsed = await listSkillsRoute.parse(req, res, pathSegments, queryParams);
|
|
372
|
+
if (!parsed) return true;
|
|
373
|
+
|
|
374
|
+
const skills = parsed.query.search
|
|
375
|
+
? searchSkills(parsed.query.search)
|
|
376
|
+
: listSkills({
|
|
377
|
+
type: parsed.query.type as "remote" | "personal" | undefined,
|
|
378
|
+
scope: parsed.query.scope as "global" | "swarm" | "agent" | undefined,
|
|
379
|
+
ownerAgentId: parsed.query.agentId,
|
|
380
|
+
isEnabled:
|
|
381
|
+
parsed.query.enabled !== undefined ? parsed.query.enabled === "true" : undefined,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
json(res, { skills, total: skills.length });
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// GET /api/skills/:id
|
|
389
|
+
if (getSkillRoute.match(req.method, pathSegments)) {
|
|
390
|
+
const parsed = await getSkillRoute.parse(req, res, pathSegments, queryParams);
|
|
391
|
+
if (!parsed) return true;
|
|
392
|
+
|
|
393
|
+
const skill = getSkillById(parsed.params.id);
|
|
394
|
+
if (!skill) {
|
|
395
|
+
jsonError(res, "Skill not found", 404);
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
json(res, skill);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// POST /api/skills
|
|
403
|
+
if (createSkillRoute.match(req.method, pathSegments)) {
|
|
404
|
+
const parsed = await createSkillRoute.parse(req, res, pathSegments, queryParams);
|
|
405
|
+
if (!parsed) return true;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const pm = parseSkillContent(parsed.body.content);
|
|
409
|
+
const skill = createSkill({
|
|
410
|
+
name: pm.name,
|
|
411
|
+
description: pm.description,
|
|
412
|
+
content: parsed.body.content,
|
|
413
|
+
type: (parsed.body.type as "remote" | "personal") ?? "personal",
|
|
414
|
+
scope: (parsed.body.scope as "global" | "swarm" | "agent") ?? "agent",
|
|
415
|
+
ownerAgentId: parsed.body.ownerAgentId,
|
|
416
|
+
allowedTools: pm.allowedTools,
|
|
417
|
+
model: pm.model,
|
|
418
|
+
effort: pm.effort,
|
|
419
|
+
context: pm.context,
|
|
420
|
+
agent: pm.agent,
|
|
421
|
+
disableModelInvocation: pm.disableModelInvocation,
|
|
422
|
+
userInvocable: pm.userInvocable,
|
|
423
|
+
});
|
|
424
|
+
json(res, { skill }, 201);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
jsonError(res, err instanceof Error ? err.message : "Create failed", 400);
|
|
427
|
+
}
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// PUT /api/skills/:id
|
|
432
|
+
if (updateSkillRoute.match(req.method, pathSegments)) {
|
|
433
|
+
const parsed = await updateSkillRoute.parse(req, res, pathSegments, queryParams);
|
|
434
|
+
if (!parsed) return true;
|
|
435
|
+
|
|
436
|
+
const updates: Record<string, unknown> = {};
|
|
437
|
+
for (const [key, value] of Object.entries(parsed.body)) {
|
|
438
|
+
updates[key] = value;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// If content is being updated, re-parse frontmatter
|
|
442
|
+
if (typeof updates.content === "string") {
|
|
443
|
+
const pm = parseSkillContent(updates.content as string);
|
|
444
|
+
updates.name = pm.name;
|
|
445
|
+
updates.description = pm.description;
|
|
446
|
+
updates.allowedTools = pm.allowedTools;
|
|
447
|
+
updates.model = pm.model;
|
|
448
|
+
updates.effort = pm.effort;
|
|
449
|
+
updates.context = pm.context;
|
|
450
|
+
updates.agent = pm.agent;
|
|
451
|
+
updates.disableModelInvocation = pm.disableModelInvocation;
|
|
452
|
+
updates.userInvocable = pm.userInvocable;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const skill = updateSkill(parsed.params.id, updates as Parameters<typeof updateSkill>[1]);
|
|
456
|
+
if (!skill) {
|
|
457
|
+
jsonError(res, "Skill not found", 404);
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
json(res, { skill });
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// DELETE /api/skills/:id
|
|
465
|
+
if (deleteSkillRoute.match(req.method, pathSegments)) {
|
|
466
|
+
const parsed = await deleteSkillRoute.parse(req, res, pathSegments, queryParams);
|
|
467
|
+
if (!parsed) return true;
|
|
468
|
+
|
|
469
|
+
const deleted = deleteSkill(parsed.params.id);
|
|
470
|
+
if (!deleted) {
|
|
471
|
+
jsonError(res, "Skill not found", 404);
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
json(res, { success: true });
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return false;
|
|
479
|
+
}
|
package/src/http/tasks.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { ensure } from "@desplega.ai/business-use";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import {
|
|
4
5
|
cancelTask,
|
|
@@ -232,6 +233,24 @@ export async function handleTasks(
|
|
|
232
233
|
source: (parsed.body.source as import("../types").AgentTaskSource) || "api",
|
|
233
234
|
outputSchema: parsed.body.outputSchema || undefined,
|
|
234
235
|
});
|
|
236
|
+
|
|
237
|
+
ensure({
|
|
238
|
+
id: "created",
|
|
239
|
+
flow: "task",
|
|
240
|
+
runId: task.id,
|
|
241
|
+
data: {
|
|
242
|
+
taskId: task.id,
|
|
243
|
+
agentId: task.agentId,
|
|
244
|
+
source: parsed.body.source || "api",
|
|
245
|
+
status: task.status,
|
|
246
|
+
task: task.task.slice(0, 200),
|
|
247
|
+
priority: task.priority,
|
|
248
|
+
tags: task.tags,
|
|
249
|
+
parentTaskId: task.parentTaskId,
|
|
250
|
+
epicId: task.epicId,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
235
254
|
json(res, task, 201);
|
|
236
255
|
} catch (error) {
|
|
237
256
|
console.error("[HTTP] Failed to create task:", error);
|
|
@@ -291,6 +310,36 @@ export async function handleTasks(
|
|
|
291
310
|
return true;
|
|
292
311
|
}
|
|
293
312
|
|
|
313
|
+
if (task.status === "pending") {
|
|
314
|
+
ensure({
|
|
315
|
+
id: "cancelled_pending",
|
|
316
|
+
flow: "task",
|
|
317
|
+
runId: parsed.params.id,
|
|
318
|
+
depIds: ["created"],
|
|
319
|
+
data: {
|
|
320
|
+
taskId: parsed.params.id,
|
|
321
|
+
agentId: task.agentId,
|
|
322
|
+
previousStatus: task.status,
|
|
323
|
+
reason,
|
|
324
|
+
},
|
|
325
|
+
validator: (data) => data.previousStatus === "pending",
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
ensure({
|
|
329
|
+
id: "cancelled_in_progress",
|
|
330
|
+
flow: "task",
|
|
331
|
+
runId: parsed.params.id,
|
|
332
|
+
depIds: ["started"],
|
|
333
|
+
data: {
|
|
334
|
+
taskId: parsed.params.id,
|
|
335
|
+
agentId: task.agentId,
|
|
336
|
+
previousStatus: task.status,
|
|
337
|
+
reason,
|
|
338
|
+
},
|
|
339
|
+
validator: (data) => data.previousStatus === "in_progress",
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
294
343
|
if (task.agentId) {
|
|
295
344
|
updateAgentStatusFromCapacity(task.agentId);
|
|
296
345
|
}
|
|
@@ -386,6 +435,25 @@ export async function handleTasks(
|
|
|
386
435
|
return true;
|
|
387
436
|
}
|
|
388
437
|
|
|
438
|
+
if (result.task && !("alreadyFinished" in result && result.alreadyFinished)) {
|
|
439
|
+
const finishEventId = parsed.body.status === "completed" ? "completed" : "failed";
|
|
440
|
+
ensure({
|
|
441
|
+
id: finishEventId,
|
|
442
|
+
flow: "task",
|
|
443
|
+
runId: parsed.params.id,
|
|
444
|
+
depIds: ["started"],
|
|
445
|
+
data: {
|
|
446
|
+
taskId: parsed.params.id,
|
|
447
|
+
agentId: myAgentId,
|
|
448
|
+
previousStatus: "in_progress",
|
|
449
|
+
...(finishEventId === "completed"
|
|
450
|
+
? { hasOutput: !!parsed.body.output }
|
|
451
|
+
: { failureReason: parsed.body.failureReason }),
|
|
452
|
+
},
|
|
453
|
+
validator: (data) => data.previousStatus === "in_progress",
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
389
457
|
json(res, {
|
|
390
458
|
success: true,
|
|
391
459
|
alreadyFinished: "alreadyFinished" in result ? result.alreadyFinished : false,
|
|
@@ -430,6 +498,19 @@ export async function handleTasks(
|
|
|
430
498
|
return true;
|
|
431
499
|
}
|
|
432
500
|
|
|
501
|
+
ensure({
|
|
502
|
+
id: "paused",
|
|
503
|
+
flow: "task",
|
|
504
|
+
runId: parsed.params.id,
|
|
505
|
+
depIds: ["started"],
|
|
506
|
+
data: {
|
|
507
|
+
taskId: parsed.params.id,
|
|
508
|
+
agentId: task.agentId,
|
|
509
|
+
previousStatus: task.status,
|
|
510
|
+
},
|
|
511
|
+
validator: (data) => data.previousStatus === "in_progress",
|
|
512
|
+
});
|
|
513
|
+
|
|
433
514
|
json(res, { success: true, task: pausedTask });
|
|
434
515
|
return true;
|
|
435
516
|
}
|
|
@@ -460,6 +541,19 @@ export async function handleTasks(
|
|
|
460
541
|
return true;
|
|
461
542
|
}
|
|
462
543
|
|
|
544
|
+
ensure({
|
|
545
|
+
id: "resumed",
|
|
546
|
+
flow: "task",
|
|
547
|
+
runId: parsed.params.id,
|
|
548
|
+
depIds: ["paused"],
|
|
549
|
+
data: {
|
|
550
|
+
taskId: parsed.params.id,
|
|
551
|
+
agentId: task.agentId,
|
|
552
|
+
previousStatus: task.status,
|
|
553
|
+
},
|
|
554
|
+
validator: (data) => data.previousStatus === "paused",
|
|
555
|
+
});
|
|
556
|
+
|
|
463
557
|
json(res, { success: true, task: resumedTask });
|
|
464
558
|
return true;
|
|
465
559
|
}
|
package/src/linear/outbound.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { getTrackerSync, updateTrackerSync } from "../be/db-queries/tracker";
|
|
2
2
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
3
3
|
import { getLinearClient } from "./client";
|
|
4
|
-
import {
|
|
5
|
-
endAgentSession,
|
|
6
|
-
postAgentSessionAction,
|
|
7
|
-
postAgentSessionThought,
|
|
8
|
-
taskSessionMap,
|
|
9
|
-
} from "./sync";
|
|
4
|
+
import { endAgentSession, postAgentSessionAction, taskSessionMap } from "./sync";
|
|
10
5
|
|
|
11
6
|
let subscribed = false;
|
|
12
7
|
|
|
@@ -60,8 +55,9 @@ async function handleTaskProgress(data: unknown): Promise<void> {
|
|
|
60
55
|
const sessionId = taskSessionMap.get(taskId);
|
|
61
56
|
if (!sessionId) return;
|
|
62
57
|
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
// Use 'action' activity type — Linear renders it as a structured tool invocation card
|
|
59
|
+
postAgentSessionAction(sessionId, progress).catch((err) => {
|
|
60
|
+
console.error(`[Linear Outbound] Failed to post progress action for task ${taskId}:`, err);
|
|
65
61
|
});
|
|
66
62
|
}
|
|
67
63
|
|
|
@@ -75,7 +71,9 @@ async function handleTaskCompleted(data: unknown): Promise<void> {
|
|
|
75
71
|
if (shouldSkipForLoopPrevention(sync)) return;
|
|
76
72
|
|
|
77
73
|
const sessionId = taskSessionMap.get(taskId);
|
|
78
|
-
const body = output
|
|
74
|
+
const body = output
|
|
75
|
+
? `Task completed.\n\n+++ Output\n${output.slice(0, 2000)}\n+++`
|
|
76
|
+
: "Task completed.";
|
|
79
77
|
|
|
80
78
|
// Prefer AgentSession activity (shows in the agent panel) over issue comment (avoids duplication)
|
|
81
79
|
if (sessionId) {
|
|
@@ -93,7 +91,7 @@ async function handleTaskCompleted(data: unknown): Promise<void> {
|
|
|
93
91
|
return;
|
|
94
92
|
}
|
|
95
93
|
const comment = output
|
|
96
|
-
? `Task completed by swarm agent.\n\
|
|
94
|
+
? `Task completed by swarm agent.\n\n+++ Output\n${output.slice(0, 2000)}\n+++`
|
|
97
95
|
: "Task completed by swarm agent.";
|
|
98
96
|
await client.createComment({ issueId: sync.externalId, body: comment });
|
|
99
97
|
console.log(`[Linear Outbound] Posted completion comment for task ${taskId}`);
|
|
@@ -121,7 +119,9 @@ async function handleTaskFailed(data: unknown): Promise<void> {
|
|
|
121
119
|
if (shouldSkipForLoopPrevention(sync)) return;
|
|
122
120
|
|
|
123
121
|
const sessionId = taskSessionMap.get(taskId);
|
|
124
|
-
const body = failureReason
|
|
122
|
+
const body = failureReason
|
|
123
|
+
? `Task failed.\n\n+++ Error Details\n${failureReason.slice(0, 2000)}\n+++`
|
|
124
|
+
: "Task failed.";
|
|
125
125
|
|
|
126
126
|
// Prefer AgentSession error activity over issue comment (avoids duplication)
|
|
127
127
|
if (sessionId) {
|
|
@@ -139,7 +139,7 @@ async function handleTaskFailed(data: unknown): Promise<void> {
|
|
|
139
139
|
return;
|
|
140
140
|
}
|
|
141
141
|
const comment = failureReason
|
|
142
|
-
? `Task failed.\n\
|
|
142
|
+
? `Task failed.\n\n+++ Error Details\n${failureReason.slice(0, 2000)}\n+++`
|
|
143
143
|
: "Task failed.";
|
|
144
144
|
await client.createComment({ issueId: sync.externalId, body: comment });
|
|
145
145
|
console.log(`[Linear Outbound] Posted failure comment for task ${taskId}`);
|
|
@@ -38,6 +38,8 @@ export type BasePromptArgs = {
|
|
|
38
38
|
clonePath: string;
|
|
39
39
|
warning?: string | null;
|
|
40
40
|
};
|
|
41
|
+
/** Pre-fetched skill summaries for the installed skills section */
|
|
42
|
+
skillsSummary?: { name: string; description: string }[];
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
|
|
@@ -68,6 +70,12 @@ export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
// Installed skills section (progressive disclosure — name + description only)
|
|
74
|
+
if (args.skillsSummary && args.skillsSummary.length > 0) {
|
|
75
|
+
const summaries = args.skillsSummary.map((s) => `- /${s.name}: ${s.description}`).join("\n");
|
|
76
|
+
prompt += `\n\n## Installed Skills\n\nThe following skills are available. Use the Skill tool to invoke them by name.\n\n${summaries}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
// Repo context (protected, never truncated)
|
|
72
80
|
if (args.repoContext) {
|
|
73
81
|
prompt += "\n\n## Repository Context\n\n";
|