@desplega.ai/agent-swarm 1.102.0 → 1.103.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/dist/{actions-j9399ktm.js → actions-7txktrdj.js} +6 -6
- package/dist/{app-tnq1pz27.js → app-xm3xsdye.js} +3 -3
- package/dist/{assistant-c7y92gkw.js → assistant-qpv63wk6.js} +9 -9
- package/dist/{boot-reembed-k7e6fc1p.js → boot-reembed-9cxqka3e.js} +3 -3
- package/dist/{boot-reembed-b97r699t.js → boot-reembed-chxhrwv8.js} +4 -4
- package/dist/{boot-scrub-logs-14kbc61y.js → boot-scrub-logs-8qtfkcz8.js} +2 -2
- package/dist/{claude-managed-adapter-g07dbz3b.js → claude-managed-adapter-z29hjgzz.js} +3 -3
- package/dist/{cli-tsz1q7gk.js → cli-2307phk1.js} +375 -23
- package/dist/{cli-g5px3d7x.js → cli-2957pqpj.js} +24 -1
- package/dist/{cli-9qascwj7.js → cli-2qynerth.js} +1 -1
- package/dist/{cli-k1ghyj8b.js → cli-4fnp2tc3.js} +1 -1
- package/dist/{cli-hc4rd6sv.js → cli-85pf4z38.js} +1 -1
- package/dist/{cli-zk4gn40w.js → cli-8hyv6c2v.js} +6 -14
- package/dist/{cli-k81y1cjb.js → cli-8w95v19d.js} +1 -1
- package/dist/{cli-7xdq3jg3.js → cli-92fqyjcv.js} +1 -1
- package/dist/{cli-zctkpdmd.js → cli-dz3d6zjg.js} +1 -1
- package/dist/{cli-d0t4854c.js → cli-fedn86nz.js} +213 -476
- package/dist/{cli-g861fwjp.js → cli-fwxdt7kt.js} +3 -3
- package/dist/{cli-2aaa9mqr.js → cli-fygaw191.js} +2 -2
- package/dist/{cli-qbc6mncp.js → cli-gzepjz54.js} +2 -2
- package/dist/{cli-r68m8arr.js → cli-jt9d9fjw.js} +1 -1
- package/dist/{cli-axtg1zaa.js → cli-khe4x17a.js} +1 -1
- package/dist/{cli-sygq3fhn.js → cli-kk51f3tf.js} +5 -5
- package/dist/{cli-dgab5x2h.js → cli-krvxq8bc.js} +1 -1
- package/dist/{cli-xde8vhhp.js → cli-ksq38x4n.js} +2 -2
- package/dist/{cli-stqqf5kh.js → cli-m8f6qzck.js} +1 -1
- package/dist/{cli-ap4md0gc.js → cli-ns0r7mkr.js} +30 -5
- package/dist/{cli-t2jk33ta.js → cli-rttgde5f.js} +18 -6
- package/dist/{cli-mhj07xbq.js → cli-rvae030h.js} +3 -3
- package/dist/{cli-q6jzg6wj.js → cli-s14sb64w.js} +4 -4
- package/dist/{cli-zmf7refz.js → cli-v9283nma.js} +1 -1
- package/dist/{cli-fjx4x7nn.js → cli-xmtsan9k.js} +1 -1
- package/dist/{cli-vgck9mqq.js → cli-yeemwevj.js} +155 -37
- package/dist/{cli-5bqa41fd.js → cli-yfwwjeft.js} +1 -1
- package/dist/{cli-s8jw5bmg.js → cli-yp2krx5r.js} +2 -2
- package/dist/cli.js +9 -9
- package/dist/{codex-adapter-wf119mnw.js → codex-adapter-w0xharsg.js} +3 -3
- package/dist/{codex-session-runner-1gknw7mh.js → codex-session-runner-kzbjcm2k.js} +3 -3
- package/dist/{commands-2wyn3swb.js → commands-aqn63mcw.js} +2 -2
- package/dist/{db-567ntpxv.js → db-azrvwsnj.js} +2 -2
- package/dist/{handlers-wvwpk2kj.js → handlers-hf61har3.js} +9 -9
- package/dist/{hook-fatkywc0.js → hook-306p3yz4.js} +2 -2
- package/dist/{http-eq6wxve6.js → http-zts4haq7.js} +81 -62
- package/dist/{index-400x6p3j.js → index-29vg51x6.js} +6 -6
- package/dist/{index-9wqh5352.js → index-3c651yfk.js} +10 -10
- package/dist/{index-6aj8966p.js → index-bfh9hgek.js} +5 -5
- package/dist/{index-d1rm4ea3.js → index-h2yqjyk0.js} +8 -8
- package/dist/{keepalive-6x9nx92g.js → keepalive-a0dg5pds.js} +4 -4
- package/dist/{lead-5fgqwe13.js → lead-7mcxedkc.js} +21 -21
- package/dist/{maintenance-eajx5k1k.js → maintenance-mrzqd53t.js} +4 -4
- package/dist/{onboard-jc1w1edg.js → onboard-4dkpv9wa.js} +2 -2
- package/dist/{otel-impl-8k375amf.js → otel-impl-jt7gpyp9.js} +1 -1
- package/dist/{pi-mono-adapter-hf8sw7hs.js → pi-mono-adapter-zf7dt1wg.js} +1 -1
- package/dist/{pricing-refresh-zed6916t.js → pricing-refresh-xqnzh8dt.js} +4 -4
- package/dist/{seed-pricing-5805qzkz.js → seed-pricing-56tjm1fj.js} +3 -3
- package/dist/{setup-wz05vxh0.js → setup-tbwk8j10.js} +2 -2
- package/dist/{worker-j359graf.js → worker-e03za92t.js} +21 -21
- package/openapi.json +70 -1
- package/package.json +1 -1
- package/src/be/db.ts +83 -13
- package/src/be/migrations/098_repo_hooks_config.sql +3 -0
- package/src/be/task-lifecycle-events.ts +42 -0
- package/src/commands/runner.ts +174 -25
- package/src/github/task-reactions.ts +21 -0
- package/src/hooks/tool-loop-detection.test.ts +40 -0
- package/src/hooks/tool-loop-detection.ts +37 -1
- package/src/http/agents.ts +7 -0
- package/src/http/index.ts +16 -2
- package/src/http/prompt-templates.ts +1 -1
- package/src/http/repos.ts +5 -1
- package/src/http/schedules.ts +1 -1
- package/src/http/session-data.ts +2 -0
- package/src/http/tasks.ts +2 -1
- package/src/http/workflows.ts +1 -0
- package/src/prompts/resolver.ts +1 -1
- package/src/scheduler/scheduler.ts +25 -2
- package/src/server-runtime-counters.ts +12 -0
- package/src/server-user.ts +1 -2
- package/src/server.ts +5 -0
- package/src/telemetry.ts +18 -1
- package/src/tests/http-api-integration.test.ts +4 -0
- package/src/tests/model-control.test.ts +6 -6
- package/src/tests/swarm-repos.test.ts +25 -2
- package/src/tests/task-lifecycle-events.test.ts +53 -0
- package/src/tests/task-lifecycle-telemetry.test.ts +37 -1
- package/src/tests/telemetry-init.test.ts +32 -0
- package/src/tools/prompt-templates/preview.ts +1 -1
- package/src/tools/repos/update-repo.ts +6 -1
- package/src/tools/schedules/create-schedule.ts +1 -1
- package/src/tools/schedules/update-schedule.ts +1 -1
- package/src/tools/send-task.ts +7 -2
- package/src/tools/task-action.ts +6 -2
- package/src/types.ts +155 -1
- package/src/utils/template.ts +127 -0
- package/src/workflows/engine.ts +7 -0
- package/src/workflows/executors/agent-task.ts +1 -2
- package/src/workflows/template.ts +12 -121
- package/src/workflows/triggers.ts +6 -2
- package/templates/community/code-health-reports/PLAYBOOK.md +355 -0
- package/templates/community/code-health-reports/README.md +38 -0
- package/templates/community/code-health-reports/lead-prompt.md +52 -0
- package/templates/community/code-health-reports/report.mjs +378 -0
- package/templates/community/code-health-reports/run.sh +143 -0
- package/templates/schedules/weekly-code-health-reports/config.json +13 -0
- package/templates/schedules/weekly-code-health-reports/content.md +60 -0
- package/src/model-tiers.ts +0 -140
- package/templates/community/.gitkeep +0 -0
package/src/http/tasks.ts
CHANGED
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
updateTaskProgress,
|
|
24
24
|
updateTaskVcs,
|
|
25
25
|
} from "../be/db";
|
|
26
|
-
import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
|
|
27
26
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
28
27
|
import { createResumeFollowUp, createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
|
|
29
28
|
import {
|
|
@@ -32,8 +31,10 @@ import {
|
|
|
32
31
|
type AgentTaskStatus,
|
|
33
32
|
AgentTaskStatusSchema,
|
|
34
33
|
isTerminalTaskStatus,
|
|
34
|
+
ModelTierSchema,
|
|
35
35
|
ProviderNameSchema,
|
|
36
36
|
ResumeReasonSchema,
|
|
37
|
+
splitLegacyModelAlias,
|
|
37
38
|
} from "../types";
|
|
38
39
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
39
40
|
import { route } from "./route-def";
|
package/src/http/workflows.ts
CHANGED
|
@@ -663,6 +663,7 @@ export async function handleWorkflows(
|
|
|
663
663
|
let runId: string;
|
|
664
664
|
try {
|
|
665
665
|
runId = await startWorkflowExecution(workflow, body, getExecutorRegistry(), {
|
|
666
|
+
triggerType: "api",
|
|
666
667
|
requestedByUserId: auth?.kind === "user" ? auth.userId : undefined,
|
|
667
668
|
});
|
|
668
669
|
} catch (err) {
|
package/src/prompts/resolver.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* workers which have no local database (architecture invariant: workers communicate via HTTP only).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { interpolate } from "../
|
|
11
|
+
import { interpolate } from "../utils/template";
|
|
12
12
|
import { getTemplateDefinition } from "./registry";
|
|
13
13
|
|
|
14
14
|
// ─── DB Resolver Injection ──────────────────────────────────────────────────
|
|
@@ -3,6 +3,7 @@ import { CronExpressionParser } from "cron-parser";
|
|
|
3
3
|
import { getDb, getDueScheduledTasks, getScheduledTaskById, updateScheduledTask } from "@/be/db";
|
|
4
4
|
import { scheduleContextKey } from "@/tasks/context-key";
|
|
5
5
|
import { createTaskWithSiblingAwareness } from "@/tasks/sibling-awareness";
|
|
6
|
+
import { telemetry } from "@/telemetry";
|
|
6
7
|
import type { AgentTask, ScheduledTask } from "@/types";
|
|
7
8
|
import type { ExecutorRegistry } from "@/workflows/executors/registry";
|
|
8
9
|
import { handleScheduleTrigger } from "@/workflows/triggers";
|
|
@@ -48,9 +49,9 @@ async function recoverMissedSchedules(): Promise<void> {
|
|
|
48
49
|
`(was due ${Math.round(missedBy / 1000)}s ago)`,
|
|
49
50
|
);
|
|
50
51
|
|
|
52
|
+
let triggeredWorkflows = false;
|
|
51
53
|
try {
|
|
52
54
|
// Check if any workflows are linked to this schedule
|
|
53
|
-
let triggeredWorkflows = false;
|
|
54
55
|
if (executorRegistry) {
|
|
55
56
|
const runIds = await handleScheduleTrigger(schedule.id, schedule, executorRegistry);
|
|
56
57
|
if (runIds.length > 0) {
|
|
@@ -88,7 +89,18 @@ async function recoverMissedSchedules(): Promise<void> {
|
|
|
88
89
|
if (schedule.scheduleType === "one_time") {
|
|
89
90
|
console.log(`[Scheduler] One-time schedule "${schedule.name}" recovered and auto-disabled`);
|
|
90
91
|
}
|
|
92
|
+
telemetry.schedule("executed", {
|
|
93
|
+
scheduleType: schedule.scheduleType,
|
|
94
|
+
triggeredWorkflows,
|
|
95
|
+
wasRecovered: true,
|
|
96
|
+
});
|
|
91
97
|
} catch (err) {
|
|
98
|
+
telemetry.schedule("error", {
|
|
99
|
+
scheduleType: schedule.scheduleType,
|
|
100
|
+
triggeredWorkflows,
|
|
101
|
+
wasRecovered: true,
|
|
102
|
+
consecutiveErrors: schedule.consecutiveErrors ?? 0,
|
|
103
|
+
});
|
|
92
104
|
console.error(`[Scheduler] Error recovering "${schedule.name}":`, err);
|
|
93
105
|
}
|
|
94
106
|
}
|
|
@@ -142,9 +154,9 @@ function getBackoffMs(consecutiveErrors: number): number {
|
|
|
142
154
|
* Tracks consecutive errors and applies exponential backoff on failure.
|
|
143
155
|
*/
|
|
144
156
|
async function executeSchedule(schedule: ScheduledTask): Promise<void> {
|
|
157
|
+
let triggeredWorkflows = false;
|
|
145
158
|
try {
|
|
146
159
|
// Check if any workflows are linked to this schedule
|
|
147
|
-
let triggeredWorkflows = false;
|
|
148
160
|
if (executorRegistry) {
|
|
149
161
|
const runIds = await handleScheduleTrigger(schedule.id, schedule, executorRegistry);
|
|
150
162
|
if (runIds.length > 0) {
|
|
@@ -187,6 +199,11 @@ async function executeSchedule(schedule: ScheduledTask): Promise<void> {
|
|
|
187
199
|
});
|
|
188
200
|
console.log(`[Scheduler] Executed schedule "${schedule.name}", next run: ${nextRun}`);
|
|
189
201
|
}
|
|
202
|
+
telemetry.schedule("executed", {
|
|
203
|
+
scheduleType: schedule.scheduleType,
|
|
204
|
+
triggeredWorkflows,
|
|
205
|
+
wasRecovered: false,
|
|
206
|
+
});
|
|
190
207
|
} catch (err) {
|
|
191
208
|
const errorCount = (schedule.consecutiveErrors ?? 0) + 1;
|
|
192
209
|
const now = new Date();
|
|
@@ -228,6 +245,12 @@ async function executeSchedule(schedule: ScheduledTask): Promise<void> {
|
|
|
228
245
|
}
|
|
229
246
|
|
|
230
247
|
updateScheduledTask(schedule.id, updates);
|
|
248
|
+
telemetry.schedule("error", {
|
|
249
|
+
scheduleType: schedule.scheduleType,
|
|
250
|
+
triggeredWorkflows,
|
|
251
|
+
wasRecovered: false,
|
|
252
|
+
consecutiveErrors: errorCount,
|
|
253
|
+
});
|
|
231
254
|
}
|
|
232
255
|
}
|
|
233
256
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const runtimeCounters = globalThis as typeof globalThis & {
|
|
2
|
+
__agentSwarmServerSessionsProcessed?: number;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export function incrementServerSessionsProcessed(): void {
|
|
6
|
+
runtimeCounters.__agentSwarmServerSessionsProcessed =
|
|
7
|
+
(runtimeCounters.__agentSwarmServerSessionsProcessed ?? 0) + 1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getServerSessionsProcessed(): number {
|
|
11
|
+
return runtimeCounters.__agentSwarmServerSessionsProcessed ?? 0;
|
|
12
|
+
}
|
package/src/server-user.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import pkg from "../package.json";
|
|
4
|
-
import { ModelTierSchema } from "./model-tiers";
|
|
5
4
|
import {
|
|
6
5
|
cancelTaskHandler,
|
|
7
6
|
cancelTaskInputSchema,
|
|
@@ -21,7 +20,7 @@ import {
|
|
|
21
20
|
} from "./tools/task-action";
|
|
22
21
|
import { userCtx } from "./tools/task-tool-ctx";
|
|
23
22
|
import { createToolRegistrar } from "./tools/utils";
|
|
24
|
-
import type
|
|
23
|
+
import { ModelTierSchema, type User } from "./types";
|
|
25
24
|
|
|
26
25
|
const userSendTaskInputSchema = z.object({
|
|
27
26
|
task: z.string().min(1).describe("The task description to send."),
|
package/src/server.ts
CHANGED
|
@@ -3,6 +3,7 @@ import pkg from "../package.json";
|
|
|
3
3
|
import { initDb } from "./be/db";
|
|
4
4
|
import { startPricingRefreshLoop } from "./be/pricing-refresh";
|
|
5
5
|
import { seedPricingFromModelsDev } from "./be/seed-pricing";
|
|
6
|
+
import { registerGithubTaskReactions } from "./github/task-reactions";
|
|
6
7
|
import { registerCancelTaskTool } from "./tools/cancel-task";
|
|
7
8
|
import { registerContextDiffTool } from "./tools/context-diff";
|
|
8
9
|
import { registerContextHistoryTool } from "./tools/context-history";
|
|
@@ -175,6 +176,10 @@ export function createServer() {
|
|
|
175
176
|
seedPricingFromModelsDev();
|
|
176
177
|
startPricingRefreshLoop();
|
|
177
178
|
|
|
179
|
+
// Subscribe API-side integrations to task-lifecycle events. Idempotent.
|
|
180
|
+
// (Inverts the old be/db → github/task-reactions import; see cycle-break #4.)
|
|
181
|
+
registerGithubTaskReactions();
|
|
182
|
+
|
|
178
183
|
const server = new McpServer(
|
|
179
184
|
{
|
|
180
185
|
name: pkg.name,
|
package/src/telemetry.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Importable from both API server and workers
|
|
8
8
|
*/
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
|
+
import pkg from "../package.json";
|
|
10
11
|
|
|
11
12
|
const TELEMETRY_ENDPOINT = "https://proxy.desplega.sh/v1/events";
|
|
12
13
|
const PRODUCT = "agent-swarm";
|
|
@@ -195,6 +196,7 @@ export function track(options: TrackOptions): void {
|
|
|
195
196
|
// anonymous, and the boolean is sufficient to split cloud vs self-host.
|
|
196
197
|
is_cloud: cachedIsCloud,
|
|
197
198
|
is_e2b: cachedIsE2b,
|
|
199
|
+
swarmVersion: pkg.version,
|
|
198
200
|
},
|
|
199
201
|
metadata: {
|
|
200
202
|
transport: "https",
|
|
@@ -238,7 +240,6 @@ export const telemetry = {
|
|
|
238
240
|
props: {
|
|
239
241
|
taskId: string;
|
|
240
242
|
source?: string;
|
|
241
|
-
tags?: string[];
|
|
242
243
|
durationMs?: number;
|
|
243
244
|
hasParent?: boolean;
|
|
244
245
|
agentId?: string;
|
|
@@ -256,4 +257,20 @@ export const telemetry = {
|
|
|
256
257
|
session(event: string, props: { agentId: string; taskId?: string; [k: string]: unknown }): void {
|
|
257
258
|
track({ event: `session.${event}`, properties: props });
|
|
258
259
|
},
|
|
260
|
+
|
|
261
|
+
schedule(event: string, props: Record<string, unknown>): void {
|
|
262
|
+
track({ event: `schedule.${event}`, properties: props });
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
workflow(event: string, props: Record<string, unknown>): void {
|
|
266
|
+
track({ event: `workflow.${event}`, properties: props });
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
agent(event: string, props: Record<string, unknown>): void {
|
|
270
|
+
track({ event: `agent.${event}`, properties: props });
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
compaction(event: string, props: Record<string, unknown>): void {
|
|
274
|
+
track({ event: `compaction.${event}`, properties: props });
|
|
275
|
+
},
|
|
259
276
|
};
|
|
@@ -1187,6 +1187,10 @@ describe("Repos", () => {
|
|
|
1187
1187
|
expect(status).toBe(201);
|
|
1188
1188
|
expect(body.id).toBeDefined();
|
|
1189
1189
|
expect(body.name).toBe("test-repo");
|
|
1190
|
+
expect(body.clonePath).toBe("/workspace/personal/repos/test-repo");
|
|
1191
|
+
expect(body.defaultBranch).toBe("main");
|
|
1192
|
+
expect(body.autoClone).toBe(true);
|
|
1193
|
+
expect(body.hooks).toEqual({ enabled: true });
|
|
1190
1194
|
ids.repo = body.id;
|
|
1191
1195
|
});
|
|
1192
1196
|
|
|
@@ -12,17 +12,17 @@ import {
|
|
|
12
12
|
updateScheduledTask,
|
|
13
13
|
upsertSwarmConfig,
|
|
14
14
|
} from "../be/db";
|
|
15
|
-
import {
|
|
16
|
-
parseModelTier,
|
|
17
|
-
resolveModelTier,
|
|
18
|
-
resolveTaskModelSelection,
|
|
19
|
-
splitLegacyModelAlias,
|
|
20
|
-
} from "../model-tiers";
|
|
21
15
|
import { runScheduleNow } from "../scheduler";
|
|
22
16
|
import { createScheduleInputSchema } from "../tools/schedules/create-schedule";
|
|
23
17
|
import { updateScheduleInputSchema } from "../tools/schedules/update-schedule";
|
|
24
18
|
import { sendTaskInputSchema } from "../tools/send-task";
|
|
25
19
|
import { taskActionInputSchema } from "../tools/task-action";
|
|
20
|
+
import {
|
|
21
|
+
parseModelTier,
|
|
22
|
+
resolveModelTier,
|
|
23
|
+
resolveTaskModelSelection,
|
|
24
|
+
splitLegacyModelAlias,
|
|
25
|
+
} from "../types";
|
|
26
26
|
|
|
27
27
|
const TEST_DB_PATH = "./test-model-control.sqlite";
|
|
28
28
|
|
|
@@ -47,9 +47,10 @@ describe("Swarm Repos", () => {
|
|
|
47
47
|
expect(repo.id).toBeDefined();
|
|
48
48
|
expect(repo.url).toBe("https://github.com/desplega-ai/agent-swarm");
|
|
49
49
|
expect(repo.name).toBe("agent-swarm");
|
|
50
|
-
expect(repo.clonePath).toBe("/workspace/repos/agent-swarm");
|
|
50
|
+
expect(repo.clonePath).toBe("/workspace/personal/repos/agent-swarm");
|
|
51
51
|
expect(repo.defaultBranch).toBe("main");
|
|
52
52
|
expect(repo.autoClone).toBe(true);
|
|
53
|
+
expect(repo.hooks).toEqual({ enabled: true });
|
|
53
54
|
expect(repo.createdAt).toBeDefined();
|
|
54
55
|
expect(repo.lastUpdatedAt).toBeDefined();
|
|
55
56
|
});
|
|
@@ -61,11 +62,23 @@ describe("Swarm Repos", () => {
|
|
|
61
62
|
clonePath: "/workspace/custom/other",
|
|
62
63
|
defaultBranch: "develop",
|
|
63
64
|
autoClone: false,
|
|
65
|
+
hooks: { enabled: true },
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
expect(repo.clonePath).toBe("/workspace/custom/other");
|
|
67
69
|
expect(repo.defaultBranch).toBe("develop");
|
|
68
70
|
expect(repo.autoClone).toBe(false);
|
|
71
|
+
expect(repo.hooks).toEqual({ enabled: true });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("should create a repo with explicitly disabled hooks", () => {
|
|
75
|
+
const repo = createSwarmRepo({
|
|
76
|
+
url: "https://github.com/desplega-ai/disabled-hooks-repo",
|
|
77
|
+
name: "disabled-hooks-repo",
|
|
78
|
+
hooks: { enabled: false },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(repo.hooks).toEqual({ enabled: false });
|
|
69
82
|
});
|
|
70
83
|
|
|
71
84
|
test("should list repos", () => {
|
|
@@ -122,14 +135,24 @@ describe("Swarm Repos", () => {
|
|
|
122
135
|
const updated = updateSwarmRepo(repo!.id, {
|
|
123
136
|
defaultBranch: "develop",
|
|
124
137
|
autoClone: false,
|
|
138
|
+
hooks: { enabled: true },
|
|
125
139
|
});
|
|
126
140
|
|
|
127
141
|
expect(updated).not.toBeNull();
|
|
128
142
|
expect(updated?.defaultBranch).toBe("develop");
|
|
129
143
|
expect(updated?.autoClone).toBe(false);
|
|
144
|
+
expect(updated?.hooks).toEqual({ enabled: true });
|
|
130
145
|
expect(updated?.lastUpdatedAt).not.toBe(repo?.lastUpdatedAt);
|
|
131
146
|
});
|
|
132
147
|
|
|
148
|
+
test("should disable hooks by clearing hooks config", () => {
|
|
149
|
+
const repo = getSwarmRepoByName("agent-swarm");
|
|
150
|
+
expect(repo?.hooks).toEqual({ enabled: true });
|
|
151
|
+
|
|
152
|
+
const updated = updateSwarmRepo(repo!.id, { hooks: null });
|
|
153
|
+
expect(updated?.hooks).toEqual({ enabled: false });
|
|
154
|
+
});
|
|
155
|
+
|
|
133
156
|
test("should update repo name and clonePath", () => {
|
|
134
157
|
const repo = createSwarmRepo({
|
|
135
158
|
url: "https://github.com/desplega-ai/temp-repo",
|
|
@@ -265,7 +288,7 @@ describe("Swarm Repos", () => {
|
|
|
265
288
|
createSwarmRepo({
|
|
266
289
|
url: "https://github.com/desplega-ai/unique-url-2",
|
|
267
290
|
name: "unique-name",
|
|
268
|
-
clonePath: "/workspace/repos/agent-swarm",
|
|
291
|
+
clonePath: "/workspace/personal/repos/agent-swarm",
|
|
269
292
|
}),
|
|
270
293
|
).toThrow();
|
|
271
294
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { emitTaskStarted, onTaskStarted } from "../be/task-lifecycle-events";
|
|
3
|
+
import type { AgentTask } from "../types";
|
|
4
|
+
|
|
5
|
+
function makeTask(overrides: Partial<AgentTask> = {}): AgentTask {
|
|
6
|
+
return {
|
|
7
|
+
id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
8
|
+
agentId: "11111111-2222-3333-4444-555555555555",
|
|
9
|
+
task: "Test task",
|
|
10
|
+
status: "in_progress",
|
|
11
|
+
source: "github",
|
|
12
|
+
priority: 50,
|
|
13
|
+
tags: [],
|
|
14
|
+
dependsOn: [],
|
|
15
|
+
wasPaused: false,
|
|
16
|
+
createdAt: new Date().toISOString(),
|
|
17
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
18
|
+
...overrides,
|
|
19
|
+
} as AgentTask;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("task-lifecycle-events", () => {
|
|
23
|
+
test("emitTaskStarted invokes a registered onTaskStarted handler with the task", () => {
|
|
24
|
+
const received: AgentTask[] = [];
|
|
25
|
+
onTaskStarted((task) => {
|
|
26
|
+
received.push(task);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const task = makeTask();
|
|
30
|
+
emitTaskStarted(task);
|
|
31
|
+
|
|
32
|
+
expect(received).toContain(task);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a throwing handler does not break emit and later handlers still run", () => {
|
|
36
|
+
let laterRan = false;
|
|
37
|
+
onTaskStarted(() => {
|
|
38
|
+
throw new Error("boom");
|
|
39
|
+
});
|
|
40
|
+
onTaskStarted(() => {
|
|
41
|
+
laterRan = true;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(() => emitTaskStarted(makeTask())).not.toThrow();
|
|
45
|
+
expect(laterRan).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a promise-rejecting handler is fire-and-forget and does not throw", () => {
|
|
49
|
+
onTaskStarted(() => Promise.reject(new Error("async boom")));
|
|
50
|
+
|
|
51
|
+
expect(() => emitTaskStarted(makeTask())).not.toThrow();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
failTask,
|
|
10
10
|
getDb,
|
|
11
11
|
initDb,
|
|
12
|
+
updateTaskClaudeSessionId,
|
|
12
13
|
} from "../be/db";
|
|
13
14
|
import { telemetry } from "../telemetry";
|
|
14
15
|
|
|
@@ -69,7 +70,6 @@ describe("task lifecycle telemetry", () => {
|
|
|
69
70
|
props: {
|
|
70
71
|
taskId: task.id,
|
|
71
72
|
source: "mcp",
|
|
72
|
-
tags: ["telemetry"],
|
|
73
73
|
hasParent: false,
|
|
74
74
|
priority: 60,
|
|
75
75
|
},
|
|
@@ -150,4 +150,40 @@ describe("task lifecycle telemetry", () => {
|
|
|
150
150
|
});
|
|
151
151
|
expect(typeof calls[0]?.props.durationMs).toBe("number");
|
|
152
152
|
});
|
|
153
|
+
|
|
154
|
+
test("emits structured task provider and harness context instead of tags", async () => {
|
|
155
|
+
const task = createTaskExtended("complete telemetry with harness context", {
|
|
156
|
+
agentId: WORKER_ID,
|
|
157
|
+
source: "mcp",
|
|
158
|
+
tags: ["telemetry"],
|
|
159
|
+
});
|
|
160
|
+
updateTaskClaudeSessionId(
|
|
161
|
+
task.id,
|
|
162
|
+
"provider-session-1",
|
|
163
|
+
"codex",
|
|
164
|
+
undefined,
|
|
165
|
+
"gpt-5.5",
|
|
166
|
+
"stock",
|
|
167
|
+
{ version: "0.40.1" },
|
|
168
|
+
);
|
|
169
|
+
await flushMicrotasks();
|
|
170
|
+
calls = [];
|
|
171
|
+
|
|
172
|
+
completeTask(task.id, "done");
|
|
173
|
+
await flushMicrotasks();
|
|
174
|
+
|
|
175
|
+
expect(calls).toHaveLength(1);
|
|
176
|
+
expect(calls[0]).toMatchObject({
|
|
177
|
+
event: "completed",
|
|
178
|
+
props: {
|
|
179
|
+
taskId: task.id,
|
|
180
|
+
source: "mcp",
|
|
181
|
+
agentId: WORKER_ID,
|
|
182
|
+
provider: "codex",
|
|
183
|
+
harnessVariant: "stock",
|
|
184
|
+
harnessVersion: "0.40.1",
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
expect(calls[0]?.props.tags).toBeUndefined();
|
|
188
|
+
});
|
|
153
189
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import pkg from "../../package.json";
|
|
2
3
|
import {
|
|
3
4
|
_getInstallationIdForTests,
|
|
4
5
|
_isE2bSandbox,
|
|
@@ -478,6 +479,37 @@ describe("initTelemetry", () => {
|
|
|
478
479
|
});
|
|
479
480
|
});
|
|
480
481
|
|
|
482
|
+
describe("track() ships swarmVersion in properties", () => {
|
|
483
|
+
const originalFetch = globalThis.fetch;
|
|
484
|
+
let captured: Record<string, unknown> | null = null;
|
|
485
|
+
|
|
486
|
+
beforeEach(() => {
|
|
487
|
+
captured = null;
|
|
488
|
+
globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
|
|
489
|
+
captured = init?.body ? JSON.parse(init.body) : null;
|
|
490
|
+
return new Response(null, { status: 204 });
|
|
491
|
+
}) as typeof fetch;
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
afterEach(() => {
|
|
495
|
+
globalThis.fetch = originalFetch;
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("includes the package version on every event and ignores caller overrides", async () => {
|
|
499
|
+
await initTelemetry(
|
|
500
|
+
"worker",
|
|
501
|
+
async () => "install_version_test",
|
|
502
|
+
async () => {},
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
track({ event: "test.event", properties: { swarmVersion: "spoofed" } });
|
|
506
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
507
|
+
|
|
508
|
+
const properties = (captured as { properties: Record<string, unknown> }).properties;
|
|
509
|
+
expect(properties.swarmVersion).toBe(pkg.version);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
481
513
|
describe("track() metadata.environment", () => {
|
|
482
514
|
const originalFetch = globalThis.fetch;
|
|
483
515
|
const originalNodeEnv = process.env.NODE_ENV;
|
|
@@ -2,7 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { getTemplateDefinition } from "@/prompts/registry";
|
|
4
4
|
import { createToolRegistrar } from "@/tools/utils";
|
|
5
|
-
import { interpolate } from "@/
|
|
5
|
+
import { interpolate } from "@/utils/template";
|
|
6
6
|
|
|
7
7
|
export const registerPreviewPromptTemplateTool = (server: McpServer) => {
|
|
8
8
|
createToolRegistrar(server)(
|
|
@@ -2,7 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { updateSwarmRepo } from "@/be/db";
|
|
4
4
|
import { createToolRegistrar } from "@/tools/utils";
|
|
5
|
-
import { RepoGuidelinesSchema, SwarmRepoSchema } from "@/types";
|
|
5
|
+
import { RepoGuidelinesSchema, RepoHooksSchema, SwarmRepoSchema } from "@/types";
|
|
6
6
|
|
|
7
7
|
export const registerUpdateRepoTool = (server: McpServer) => {
|
|
8
8
|
createToolRegistrar(server)(
|
|
@@ -20,6 +20,11 @@ export const registerUpdateRepoTool = (server: McpServer) => {
|
|
|
20
20
|
clonePath: z.string().optional().describe("New clone path."),
|
|
21
21
|
defaultBranch: z.string().optional().describe("New default branch."),
|
|
22
22
|
autoClone: z.boolean().optional().describe("Whether to auto-clone."),
|
|
23
|
+
hooks: RepoHooksSchema.nullable()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe(
|
|
26
|
+
"Repository hook install config. Set { enabled: true } to opt into best-effort worker hook installation, or null to disable.",
|
|
27
|
+
),
|
|
23
28
|
guidelines: RepoGuidelinesSchema.nullable()
|
|
24
29
|
.optional()
|
|
25
30
|
.describe(
|
|
@@ -5,7 +5,7 @@ import { resolveTaskAuditUserId } from "@/be/audit-user";
|
|
|
5
5
|
import { createScheduledTask, getAgentById, getScheduledTaskByName } from "@/be/db";
|
|
6
6
|
import { calculateNextRun } from "@/scheduler";
|
|
7
7
|
import { createToolRegistrar } from "@/tools/utils";
|
|
8
|
-
import { ModelTierSchema, splitLegacyModelAlias } from "../../
|
|
8
|
+
import { ModelTierSchema, splitLegacyModelAlias } from "../../types";
|
|
9
9
|
|
|
10
10
|
export const createScheduleInputSchema = z.object({
|
|
11
11
|
name: z.string().min(1).max(100).describe("Unique name for the schedule (e.g., 'daily-cleanup')"),
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { mergeScheduleTiming, validateRecurringTiming } from "@/be/schedules/validate";
|
|
12
12
|
import { calculateNextRun } from "@/scheduler";
|
|
13
13
|
import { createToolRegistrar } from "@/tools/utils";
|
|
14
|
-
import { ModelTierSchema, splitLegacyModelAlias } from "../../
|
|
14
|
+
import { ModelTierSchema, splitLegacyModelAlias } from "../../types";
|
|
15
15
|
|
|
16
16
|
export const updateScheduleInputSchema = z.object({
|
|
17
17
|
scheduleId: z.string().uuid().optional().describe("Schedule ID to update"),
|
package/src/tools/send-task.ts
CHANGED
|
@@ -15,8 +15,13 @@ import { repointTrackerSyncBySwarmId } from "@/be/db-queries/tracker";
|
|
|
15
15
|
import { findDuplicateTask } from "@/tools/task-dedup";
|
|
16
16
|
import { ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
|
|
17
17
|
import { createToolRegistrar } from "@/tools/utils";
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
type AgentTask,
|
|
20
|
+
AgentTaskSchema,
|
|
21
|
+
FollowUpConfigSchema,
|
|
22
|
+
ModelTierSchema,
|
|
23
|
+
splitLegacyModelAlias,
|
|
24
|
+
} from "@/types";
|
|
20
25
|
|
|
21
26
|
export const sendTaskInputSchema = z.object({
|
|
22
27
|
agentId: z
|
package/src/tools/task-action.ts
CHANGED
|
@@ -27,8 +27,12 @@ import {
|
|
|
27
27
|
} from "@/be/db";
|
|
28
28
|
import { assertOwnsTask, ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
|
|
29
29
|
import { createToolRegistrar } from "@/tools/utils";
|
|
30
|
-
import {
|
|
31
|
-
|
|
30
|
+
import {
|
|
31
|
+
AgentTaskSchema,
|
|
32
|
+
BudgetRefusalCauseSchema,
|
|
33
|
+
ModelTierSchema,
|
|
34
|
+
splitLegacyModelAlias,
|
|
35
|
+
} from "@/types";
|
|
32
36
|
|
|
33
37
|
export const TaskActionSchema = z.enum([
|
|
34
38
|
"create",
|