@desplega.ai/agent-swarm 1.72.0 → 1.73.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 +1 -1
- package/package.json +1 -1
- package/src/http/trackers/jira.ts +5 -0
- package/src/slack/blocks.ts +9 -12
- package/src/tests/budget-admission.test.ts +5 -6
- package/src/tests/slack-blocks.test.ts +79 -12
- package/src/utils/constants.ts +19 -0
- package/src/workflows/executors/human-in-the-loop.ts +2 -1
- package/src/workflows/executors/raw-llm.ts +1 -1
- package/src/workflows/executors/validate.ts +1 -1
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.72.
|
|
5
|
+
"version": "1.72.1",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
package/package.json
CHANGED
|
@@ -136,6 +136,11 @@ function getWebhookUrl(req: IncomingMessage): string {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function getRedirectUri(req: IncomingMessage): string {
|
|
139
|
+
// Mirror src/jira/app.ts: prefer the explicit JIRA_REDIRECT_URI override,
|
|
140
|
+
// otherwise derive from the API base URL. Keeps the UI display consistent
|
|
141
|
+
// with the URI persisted into oauth_apps and used in the actual OAuth flow.
|
|
142
|
+
const override = process.env.JIRA_REDIRECT_URI?.trim();
|
|
143
|
+
if (override) return override;
|
|
139
144
|
return `${deriveApiBaseUrl(req)}/api/trackers/jira/callback`;
|
|
140
145
|
}
|
|
141
146
|
|
package/src/slack/blocks.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* across responses.ts, handlers.ts, thread-buffer.ts).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import { getAppUrl } from "../utils/constants";
|
|
9
9
|
|
|
10
10
|
// Slack limits section text to 3000 chars; we use 2900 for safety
|
|
11
11
|
const MAX_SECTION_LENGTH = 2900;
|
|
@@ -16,24 +16,21 @@ type SlackBlock = any;
|
|
|
16
16
|
// --- Shared utilities ---
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Get a Slack-formatted link to the task in the dashboard
|
|
19
|
+
* Get a Slack-formatted clickable link to the task in the dashboard.
|
|
20
|
+
* Always returns Slack mrkdwn link syntax (`<url|label>`) so partial task
|
|
21
|
+
* IDs are clickable in every message — falls back to the public dashboard
|
|
22
|
+
* when APP_URL is not configured.
|
|
20
23
|
*/
|
|
21
24
|
export function getTaskLink(taskId: string): string {
|
|
22
25
|
const shortId = taskId.slice(0, 8);
|
|
23
|
-
|
|
24
|
-
return `<${appUrl}?tab=tasks&task=${taskId}&expand=true|\`${shortId}\`>`;
|
|
25
|
-
}
|
|
26
|
-
return `\`${shortId}\``;
|
|
26
|
+
return `<${getTaskUrl(taskId)}|\`${shortId}\`>`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Get a raw dashboard URL for a task (for link buttons).
|
|
31
31
|
*/
|
|
32
32
|
export function getTaskUrl(taskId: string): string {
|
|
33
|
-
|
|
34
|
-
return `${appUrl}?tab=tasks&task=${taskId}&expand=true`;
|
|
35
|
-
}
|
|
36
|
-
return "";
|
|
33
|
+
return `${getAppUrl()}/tasks/${taskId}`;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
/**
|
|
@@ -214,9 +211,9 @@ export function buildProgressBlocks(opts: {
|
|
|
214
211
|
taskId: string;
|
|
215
212
|
progress: string;
|
|
216
213
|
}): SlackBlock[] {
|
|
217
|
-
const
|
|
214
|
+
const taskLink = getTaskLink(opts.taskId);
|
|
218
215
|
return [
|
|
219
|
-
sectionBlock(`*${opts.agentName}* (
|
|
216
|
+
sectionBlock(`*${opts.agentName}* (${taskLink}): ${opts.progress}`),
|
|
220
217
|
cancelActionBlock(opts.taskId),
|
|
221
218
|
];
|
|
222
219
|
}
|
|
@@ -81,6 +81,9 @@ function insertBudget(scope: "global" | "agent", scopeId: string, dailyBudgetUsd
|
|
|
81
81
|
.run(scope, scopeId, dailyBudgetUsd, Date.now(), Date.now());
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Pinned to the same UTC day as `NOW` so spend rows fall inside the queried day window regardless of when CI runs.
|
|
85
|
+
const DEFAULT_SPEND_CREATED_AT = "2026-04-28T12:00:00.000Z";
|
|
86
|
+
|
|
84
87
|
function insertSpendForAgent(
|
|
85
88
|
agentId: string,
|
|
86
89
|
totalCostUsd: number,
|
|
@@ -95,12 +98,8 @@ function insertSpendForAgent(
|
|
|
95
98
|
numTurns: 1,
|
|
96
99
|
model: "test-model",
|
|
97
100
|
});
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
getDb()
|
|
101
|
-
.prepare("UPDATE session_costs SET createdAt = ? WHERE id = ?")
|
|
102
|
-
.run(opts.createdAt, cost.id);
|
|
103
|
-
}
|
|
101
|
+
const createdAt = opts.createdAt ?? DEFAULT_SPEND_CREATED_AT;
|
|
102
|
+
getDb().prepare("UPDATE session_costs SET createdAt = ? WHERE id = ?").run(createdAt, cost.id);
|
|
104
103
|
return cost.id;
|
|
105
104
|
}
|
|
106
105
|
|
|
@@ -44,22 +44,65 @@ describe("markdownToSlack", () => {
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
describe("getTaskLink", () => {
|
|
47
|
-
test("returns
|
|
48
|
-
|
|
49
|
-
const link = getTaskLink(
|
|
50
|
-
|
|
47
|
+
test("always returns a Slack hyperlink with clickable short ID", () => {
|
|
48
|
+
const taskId = "abcdef12-3456-7890-abcd-ef1234567890";
|
|
49
|
+
const link = getTaskLink(taskId);
|
|
50
|
+
// Slack mrkdwn link syntax: <url|label>
|
|
51
|
+
expect(link).toMatch(
|
|
52
|
+
/^<https?:\/\/.+\/tasks\/abcdef12-3456-7890-abcd-ef1234567890\|`abcdef12`>$/,
|
|
53
|
+
);
|
|
54
|
+
expect(link).toContain("|`abcdef12`>");
|
|
55
|
+
expect(link).toContain(taskId);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("uses APP_URL when set", () => {
|
|
59
|
+
const original = process.env.APP_URL;
|
|
60
|
+
process.env.APP_URL = "https://my-custom-dashboard.example.com";
|
|
61
|
+
try {
|
|
62
|
+
const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
|
|
63
|
+
expect(link).toContain(
|
|
64
|
+
"https://my-custom-dashboard.example.com/tasks/abcdef12-3456-7890-abcd-ef1234567890",
|
|
65
|
+
);
|
|
66
|
+
} finally {
|
|
67
|
+
if (original === undefined) delete process.env.APP_URL;
|
|
68
|
+
else process.env.APP_URL = original;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("strips trailing slash from APP_URL", () => {
|
|
73
|
+
const original = process.env.APP_URL;
|
|
74
|
+
process.env.APP_URL = "https://dashboard.example.com/";
|
|
75
|
+
try {
|
|
76
|
+
const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
|
|
77
|
+
expect(link).toContain("https://dashboard.example.com/tasks/");
|
|
78
|
+
expect(link).not.toContain("//tasks/");
|
|
79
|
+
} finally {
|
|
80
|
+
if (original === undefined) delete process.env.APP_URL;
|
|
81
|
+
else process.env.APP_URL = original;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("falls back to public dashboard when APP_URL is unset", () => {
|
|
86
|
+
const original = process.env.APP_URL;
|
|
87
|
+
delete process.env.APP_URL;
|
|
88
|
+
try {
|
|
89
|
+
const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
|
|
90
|
+
expect(link).toContain(
|
|
91
|
+
"https://app.agent-swarm.dev/tasks/abcdef12-3456-7890-abcd-ef1234567890",
|
|
92
|
+
);
|
|
93
|
+
expect(link.startsWith("<")).toBe(true);
|
|
94
|
+
expect(link.endsWith(">")).toBe(true);
|
|
95
|
+
} finally {
|
|
96
|
+
if (original !== undefined) process.env.APP_URL = original;
|
|
97
|
+
}
|
|
51
98
|
});
|
|
52
99
|
});
|
|
53
100
|
|
|
54
101
|
describe("getTaskUrl", () => {
|
|
55
|
-
test("returns URL
|
|
102
|
+
test("always returns a non-empty URL containing the task ID", () => {
|
|
56
103
|
const url = getTaskUrl("some-id");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(url).toContain("some-id");
|
|
60
|
-
} else {
|
|
61
|
-
expect(url).toBe("");
|
|
62
|
-
}
|
|
104
|
+
expect(url).toContain("/tasks/some-id");
|
|
105
|
+
expect(url).toMatch(/^https?:\/\//);
|
|
63
106
|
});
|
|
64
107
|
});
|
|
65
108
|
|
|
@@ -93,6 +136,17 @@ describe("buildCompletedBlocks", () => {
|
|
|
93
136
|
expect(blocks[0].text.text).toContain("45s");
|
|
94
137
|
});
|
|
95
138
|
|
|
139
|
+
test("partial task ID is rendered as a clickable Slack hyperlink", () => {
|
|
140
|
+
const blocks = buildCompletedBlocks({
|
|
141
|
+
agentName: "Alpha",
|
|
142
|
+
taskId: "abcdef12-3456-7890-abcd-ef1234567890",
|
|
143
|
+
body: "Done",
|
|
144
|
+
});
|
|
145
|
+
expect(blocks[0].text.text).toMatch(
|
|
146
|
+
/<https?:\/\/[^|>]+\/tasks\/abcdef12-3456-7890-abcd-ef1234567890\|`abcdef12`>/,
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
96
150
|
test("splits long body into multiple sections", () => {
|
|
97
151
|
const longBody = "x".repeat(6000);
|
|
98
152
|
const blocks = buildCompletedBlocks({
|
|
@@ -148,7 +202,7 @@ describe("buildProgressBlocks", () => {
|
|
|
148
202
|
});
|
|
149
203
|
|
|
150
204
|
expect(blocks.length).toBe(2);
|
|
151
|
-
// Single line: *Gamma* (
|
|
205
|
+
// Single line: *Gamma* (<URL|`aabbccdd`>): Analyzing codebase...
|
|
152
206
|
// (no ⏳ prefix — progress strings now carry their own emoji)
|
|
153
207
|
expect(blocks[0].type).toBe("section");
|
|
154
208
|
expect(blocks[0].text.text).not.toContain("⏳");
|
|
@@ -161,6 +215,19 @@ describe("buildProgressBlocks", () => {
|
|
|
161
215
|
expect(blocks[1].elements[0].style).toBe("danger");
|
|
162
216
|
expect(blocks[1].elements[0].confirm).toBeDefined();
|
|
163
217
|
});
|
|
218
|
+
|
|
219
|
+
test("partial task ID is rendered as a clickable Slack hyperlink", () => {
|
|
220
|
+
const taskId = "aabbccdd-1234-5678-9012-abcdefabcdef";
|
|
221
|
+
const blocks = buildProgressBlocks({
|
|
222
|
+
agentName: "Gamma",
|
|
223
|
+
taskId,
|
|
224
|
+
progress: "Working...",
|
|
225
|
+
});
|
|
226
|
+
// Slack mrkdwn link syntax: <url|`shortId`>
|
|
227
|
+
expect(blocks[0].text.text).toMatch(
|
|
228
|
+
/<https?:\/\/[^|>]+\/tasks\/aabbccdd-1234-5678-9012-abcdefabcdef\|`aabbccdd`>/,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
164
231
|
});
|
|
165
232
|
|
|
166
233
|
describe("buildAssignmentSummaryBlocks", () => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants used across worker- and server-side code.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default dashboard URL used when `APP_URL` is unset. Points at the public
|
|
7
|
+
* production dashboard so links (Slack messages, approval URLs, etc.) are
|
|
8
|
+
* always renderable. Self-hosted operators should set `APP_URL` to override.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_APP_URL = "https://app.agent-swarm.dev";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the effective app/dashboard URL from `APP_URL` (with trailing
|
|
14
|
+
* slashes stripped), falling back to {@link DEFAULT_APP_URL}.
|
|
15
|
+
*/
|
|
16
|
+
export function getAppUrl(): string {
|
|
17
|
+
const raw = process.env.APP_URL?.trim();
|
|
18
|
+
return (raw || DEFAULT_APP_URL).replace(/\/+$/, "");
|
|
19
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ExecutorMeta } from "../../types";
|
|
3
|
+
import { getAppUrl } from "../../utils/constants";
|
|
3
4
|
import type { ExecutorResult } from "./base";
|
|
4
5
|
import { BaseExecutor } from "./base";
|
|
5
6
|
|
|
@@ -173,7 +174,7 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
|
173
174
|
): Promise<void> {
|
|
174
175
|
if (!config.notifications?.length) return;
|
|
175
176
|
|
|
176
|
-
const approvalUrl =
|
|
177
|
+
const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
|
|
177
178
|
const updatedChannels = [...config.notifications] as Array<
|
|
178
179
|
z.infer<typeof NotificationConfigSchema> & { messageTs?: string }
|
|
179
180
|
>;
|
|
@@ -33,7 +33,7 @@ export class RawLlmExecutor extends BaseExecutor<
|
|
|
33
33
|
_meta: ExecutorMeta,
|
|
34
34
|
): Promise<ExecutorResult<z.infer<typeof RawLlmOutputSchema>>> {
|
|
35
35
|
const prompt = this.deps.interpolate(config.prompt, context as Record<string, unknown>);
|
|
36
|
-
const modelName = config.model ?? "google/gemini-
|
|
36
|
+
const modelName = config.model ?? "google/gemini-3-flash-preview";
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
39
|
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
@@ -120,7 +120,7 @@ export class ValidateExecutor extends BaseExecutor<
|
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
const { object } = await generateObject({
|
|
123
|
-
model: openrouter("google/gemini-
|
|
123
|
+
model: openrouter("google/gemini-3-flash-preview"),
|
|
124
124
|
schema: jsonSchema({
|
|
125
125
|
type: "object",
|
|
126
126
|
properties: {
|