@desplega.ai/agent-swarm 1.72.1 → 1.73.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/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.
|
|
5
|
+
"version": "1.73.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
|
}
|
|
@@ -352,12 +349,10 @@ function renderTree(root: TreeNode): string {
|
|
|
352
349
|
const visibleChildren = root.children.slice(0, MAX_VISIBLE_CHILDREN);
|
|
353
350
|
const hiddenCount = root.children.length - visibleChildren.length;
|
|
354
351
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const isLast = i === visibleChildren.length - 1 && hiddenCount === 0;
|
|
358
|
-
const prefix = isLast ? "└ " : "├ ";
|
|
359
|
-
const continuationPrefix = isLast ? " " : "│ ";
|
|
352
|
+
const prefix = "↳ ";
|
|
353
|
+
const continuationPrefix = " ";
|
|
360
354
|
|
|
355
|
+
for (const child of visibleChildren) {
|
|
361
356
|
lines.push(`${prefix}${renderNodeLine(child)}`);
|
|
362
357
|
|
|
363
358
|
for (const detail of renderChildDetail(child, continuationPrefix)) {
|
|
@@ -366,7 +361,7 @@ function renderTree(root: TreeNode): string {
|
|
|
366
361
|
}
|
|
367
362
|
|
|
368
363
|
if (hiddenCount > 0) {
|
|
369
|
-
lines.push(
|
|
364
|
+
lines.push(`↳ _and ${hiddenCount} more..._`);
|
|
370
365
|
}
|
|
371
366
|
|
|
372
367
|
return lines.join("\n");
|
|
@@ -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", () => {
|
|
@@ -549,14 +616,14 @@ describe("buildTreeBlocks", () => {
|
|
|
549
616
|
|
|
550
617
|
// Root line
|
|
551
618
|
expect(lines[0]).toContain("⏳ *Lead*");
|
|
552
|
-
// Worker1 line with
|
|
553
|
-
expect(lines[1]).toMatch(
|
|
554
|
-
// Worker1 progress indented under continuation
|
|
555
|
-
expect(lines[2]).toMatch(
|
|
556
|
-
// Worker2 line with
|
|
557
|
-
expect(lines[3]).toMatch(
|
|
619
|
+
// Worker1 line with ↳ prefix
|
|
620
|
+
expect(lines[1]).toMatch(/^↳ ⏳ \*Worker1\*/);
|
|
621
|
+
// Worker1 progress indented under continuation (3 spaces, aligned under ↳ )
|
|
622
|
+
expect(lines[2]).toMatch(/^ {3}Fetching data\.\.\.$/);
|
|
623
|
+
// Worker2 line with ↳ prefix
|
|
624
|
+
expect(lines[3]).toMatch(/^↳ ⏳ \*Worker2\*/);
|
|
558
625
|
// Worker2 progress indented
|
|
559
|
-
expect(lines[4]).toMatch(/^ {
|
|
626
|
+
expect(lines[4]).toMatch(/^ {3}Compiling\.\.\.$/);
|
|
560
627
|
});
|
|
561
628
|
|
|
562
629
|
test("max children collapse (9+ children -> 8 shown + 'and 1 more...')", () => {
|
|
@@ -587,8 +654,8 @@ describe("buildTreeBlocks", () => {
|
|
|
587
654
|
expect(text).toContain("*Worker8*");
|
|
588
655
|
expect(text).not.toContain("*Worker9*");
|
|
589
656
|
expect(text).toContain("and 1 more...");
|
|
590
|
-
// The "and N more..." line uses
|
|
591
|
-
expect(lines[lines.length - 1]).toContain("
|
|
657
|
+
// The "and N more..." line uses ↳ prefix
|
|
658
|
+
expect(lines[lines.length - 1]).toContain("↳ _and 1 more..._");
|
|
592
659
|
});
|
|
593
660
|
|
|
594
661
|
test("max children collapse with many hidden", () => {
|
|
@@ -770,7 +837,7 @@ describe("buildTreeBlocks", () => {
|
|
|
770
837
|
expect(blocks.length).toBe(1);
|
|
771
838
|
});
|
|
772
839
|
|
|
773
|
-
test("tree
|
|
840
|
+
test("tree indent: all children use ↳ prefix", () => {
|
|
774
841
|
const root: TreeNode = {
|
|
775
842
|
taskId: makeTaskId("nnnn0001"),
|
|
776
843
|
agentName: "Lead",
|
|
@@ -800,10 +867,10 @@ describe("buildTreeBlocks", () => {
|
|
|
800
867
|
const blocks = buildTreeBlocks([root]);
|
|
801
868
|
const lines = blocks[0].text.text.split("\n");
|
|
802
869
|
|
|
803
|
-
//
|
|
804
|
-
expect(lines[1]).toMatch(
|
|
805
|
-
expect(lines[2]).toMatch(
|
|
806
|
-
expect(lines[3]).toMatch(
|
|
870
|
+
// All children use ↳ (no branching distinction in proportional fonts)
|
|
871
|
+
expect(lines[1]).toMatch(/^↳ /);
|
|
872
|
+
expect(lines[2]).toMatch(/^↳ /);
|
|
873
|
+
expect(lines[3]).toMatch(/^↳ /);
|
|
807
874
|
});
|
|
808
875
|
|
|
809
876
|
test("completed root with output (no slackReplySent, no children)", () => {
|
|
@@ -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
|
>;
|