@desplega.ai/agent-swarm 1.98.0 → 1.99.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 -0
- package/openapi.json +20 -1
- package/package.json +5 -5
- package/src/be/memory/link-resolver.ts +226 -0
- package/src/be/memory/providers/sqlite-store.ts +4 -2
- package/src/be/memory/raters/retrieval.ts +15 -4
- package/src/be/memory/raters/store.ts +4 -2
- package/src/be/memory/types.ts +1 -0
- package/src/be/migrations/096_memory_graph_phase1.sql +50 -0
- package/src/be/modelsdev-cache.ts +5 -0
- package/src/be/pricing-refresh.ts +189 -0
- package/src/be/scripts/typecheck.ts +3 -2
- package/src/be/seed-pricing.ts +5 -3
- package/src/commands/profile-sync.ts +83 -17
- package/src/commands/runner.ts +35 -3
- package/src/e2b/dispatch.ts +5 -0
- package/src/hooks/hook.ts +21 -5
- package/src/http/index.ts +2 -0
- package/src/http/memory.ts +116 -7
- package/src/providers/claude-adapter.ts +13 -2
- package/src/providers/pricing-sources.md +27 -9
- package/src/providers/types.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +5 -1
- package/src/scripts-runtime/types/stdlib.d.ts +2 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -1
- package/src/server.ts +2 -0
- package/src/slack/blocks.ts +58 -12
- package/src/slack/responses.ts +35 -12
- package/src/slack/watcher.ts +28 -7
- package/src/tests/internal-ai/complete-structured.test.ts +34 -1
- package/src/tests/memory-http-recall-gating.test.ts +172 -0
- package/src/tests/memory-link-resolver.test.ts +92 -0
- package/src/tests/opencode-adapter.test.ts +3 -0
- package/src/tests/pricing-refresh.test.ts +156 -0
- package/src/tests/profile-sync.test.ts +186 -0
- package/src/tests/scripts-mcp-e2e.test.ts +1 -1
- package/src/tests/slack-blocks.test.ts +48 -1
- package/src/tools/memory-get.ts +22 -1
- package/src/tools/memory-search.ts +8 -1
- package/src/tools/utils.ts +10 -0
- package/src/types.ts +2 -0
- package/src/utils/internal-ai/complete-structured.ts +10 -1
- package/tsconfig.json +1 -0
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
buildAssignmentSummaryBlocks,
|
|
4
4
|
buildBufferFlushBlocks,
|
|
5
5
|
buildCancelledBlocks,
|
|
6
|
+
buildCompletedBlockBatches,
|
|
6
7
|
buildCompletedBlocks,
|
|
7
8
|
buildFailedBlocks,
|
|
8
9
|
buildProgressBlocks,
|
|
@@ -20,6 +21,7 @@ describe("markdownToSlack", () => {
|
|
|
20
21
|
// **hello** → *hello* (Slack bold)
|
|
21
22
|
expect(markdownToSlack("**hello**")).toBe("*hello*");
|
|
22
23
|
expect(markdownToSlack("**hello world**")).toBe("*hello world*");
|
|
24
|
+
expect(markdownToSlack("__hello world__")).toBe("*hello world*");
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
test("converts italic", () => {
|
|
@@ -31,7 +33,11 @@ describe("markdownToSlack", () => {
|
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
test("converts links", () => {
|
|
34
|
-
expect(markdownToSlack("[click](https://example.com)")).toBe("
|
|
36
|
+
expect(markdownToSlack("[click](https://example.com)")).toBe("click (https://example.com)");
|
|
37
|
+
expect(markdownToSlack("")).toBe(
|
|
38
|
+
"diagram (https://example.com/a.png)",
|
|
39
|
+
);
|
|
40
|
+
expect(markdownToSlack("[click](https://example.com)")).not.toContain("<https://");
|
|
35
41
|
});
|
|
36
42
|
|
|
37
43
|
test("converts headers to bold", () => {
|
|
@@ -163,6 +169,27 @@ describe("buildCompletedBlocks", () => {
|
|
|
163
169
|
const totalText = bodySections.map((s) => s.text.text).join("");
|
|
164
170
|
expect(totalText).toBe(longBody);
|
|
165
171
|
});
|
|
172
|
+
|
|
173
|
+
test("batches very long body across multiple Slack messages without losing text", () => {
|
|
174
|
+
const longBody = "x".repeat(140_000);
|
|
175
|
+
const batches = buildCompletedBlockBatches({
|
|
176
|
+
agentName: "Alpha",
|
|
177
|
+
taskId: "abcdef12-3456-7890-abcd-ef1234567890",
|
|
178
|
+
body: longBody,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(batches.length).toBeGreaterThan(1);
|
|
182
|
+
for (const batch of batches) {
|
|
183
|
+
expect(batch.length).toBeLessThanOrEqual(45);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const bodyText = batches
|
|
187
|
+
.flatMap((batch) => batch.slice(1))
|
|
188
|
+
.map((block) => block.text.text)
|
|
189
|
+
.join("");
|
|
190
|
+
expect(bodyText).toBe(longBody);
|
|
191
|
+
expect(batches[1][0].text.text).toContain("continued · part 2");
|
|
192
|
+
});
|
|
166
193
|
});
|
|
167
194
|
|
|
168
195
|
describe("buildFailedBlocks", () => {
|
|
@@ -908,6 +935,26 @@ describe("buildTreeBlocks", () => {
|
|
|
908
935
|
expect(text).toContain("Result: 42");
|
|
909
936
|
});
|
|
910
937
|
|
|
938
|
+
test("completed root preview converts markdown and marks truncation explicitly", () => {
|
|
939
|
+
const root: TreeNode = {
|
|
940
|
+
taskId: makeTaskId("oooo0002"),
|
|
941
|
+
agentName: "Solo",
|
|
942
|
+
status: "completed",
|
|
943
|
+
slackReplySent: false,
|
|
944
|
+
output: `### Summary\n\n**Not broken** — ${"Automatic review on plain PR creation ".repeat(8)}`,
|
|
945
|
+
children: [],
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
const blocks = buildTreeBlocks([root]);
|
|
949
|
+
const text = blocks[0].text.text;
|
|
950
|
+
|
|
951
|
+
expect(text).toContain("*Summary*");
|
|
952
|
+
expect(text).toContain("*Not broken*");
|
|
953
|
+
expect(text).not.toContain("###");
|
|
954
|
+
expect(text).not.toContain("**Not broken**");
|
|
955
|
+
expect(text).toContain("more chars; full output in thread");
|
|
956
|
+
});
|
|
957
|
+
|
|
911
958
|
test("completed root with slackReplySent suppresses output", () => {
|
|
912
959
|
const root: TreeNode = {
|
|
913
960
|
taskId: makeTaskId("pppp0001"),
|
package/src/tools/memory-get.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { getMemoryStore } from "@/be/memory";
|
|
4
|
+
import { recordRetrievals } from "@/be/memory/raters/retrieval";
|
|
4
5
|
import { createToolRegistrar } from "@/tools/utils";
|
|
5
6
|
import type { AgentMemorySource } from "@/types";
|
|
6
7
|
import { AgentMemorySchema } from "@/types";
|
|
@@ -18,6 +19,12 @@ export const registerMemoryGetTool = (server: McpServer) => {
|
|
|
18
19
|
|
|
19
20
|
inputSchema: z.object({
|
|
20
21
|
memoryId: z.uuid().describe("The ID of the memory to retrieve."),
|
|
22
|
+
intent: z
|
|
23
|
+
.string()
|
|
24
|
+
.min(1)
|
|
25
|
+
.describe(
|
|
26
|
+
"Why you are retrieving this memory. Required. E.g. 'need full details of the auth fix pattern'.",
|
|
27
|
+
),
|
|
21
28
|
}),
|
|
22
29
|
outputSchema: z.object({
|
|
23
30
|
yourAgentId: z.string().uuid().optional(),
|
|
@@ -27,7 +34,7 @@ export const registerMemoryGetTool = (server: McpServer) => {
|
|
|
27
34
|
rateHint: z.string().optional(),
|
|
28
35
|
}),
|
|
29
36
|
},
|
|
30
|
-
async ({ memoryId }, requestInfo, _meta) => {
|
|
37
|
+
async ({ memoryId, intent }, requestInfo, _meta) => {
|
|
31
38
|
const memory = getMemoryStore().get(memoryId);
|
|
32
39
|
|
|
33
40
|
if (!memory) {
|
|
@@ -41,6 +48,20 @@ export const registerMemoryGetTool = (server: McpServer) => {
|
|
|
41
48
|
};
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
if (requestInfo.sourceTaskId && requestInfo.agentId) {
|
|
52
|
+
try {
|
|
53
|
+
recordRetrievals(
|
|
54
|
+
requestInfo.sourceTaskId,
|
|
55
|
+
requestInfo.agentId,
|
|
56
|
+
[{ memoryId: memory.id, similarity: 1.0 }],
|
|
57
|
+
requestInfo.sessionId,
|
|
58
|
+
{ intent, contextKey: requestInfo.contextKey, eventType: "get" },
|
|
59
|
+
);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error("[memory-get] recordRetrievals failed:", (err as Error).message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
const inTaskContext = !!requestInfo.sourceTaskId;
|
|
45
66
|
const rateHint =
|
|
46
67
|
inTaskContext && NUDGE_ELIGIBLE_SOURCES.has(memory.source as AgentMemorySource)
|
|
@@ -26,6 +26,12 @@ export const registerMemorySearchTool = (server: McpServer) => {
|
|
|
26
26
|
|
|
27
27
|
inputSchema: z.object({
|
|
28
28
|
query: z.string().min(1).describe("Natural language search query."),
|
|
29
|
+
intent: z
|
|
30
|
+
.string()
|
|
31
|
+
.min(1)
|
|
32
|
+
.describe(
|
|
33
|
+
"Why you are searching for this memory. Required. E.g. 'looking for auth pattern to fix login bug'.",
|
|
34
|
+
),
|
|
29
35
|
scope: z
|
|
30
36
|
.enum(["all", "agent", "swarm"])
|
|
31
37
|
.default("all")
|
|
@@ -56,7 +62,7 @@ export const registerMemorySearchTool = (server: McpServer) => {
|
|
|
56
62
|
_ratingNudge: z.string().optional(),
|
|
57
63
|
}),
|
|
58
64
|
},
|
|
59
|
-
async ({ query, scope, limit, source }, requestInfo, _meta) => {
|
|
65
|
+
async ({ query, intent, scope, limit, source }, requestInfo, _meta) => {
|
|
60
66
|
if (!requestInfo.agentId) {
|
|
61
67
|
return {
|
|
62
68
|
content: [{ type: "text", text: "Agent ID required for memory search." }],
|
|
@@ -97,6 +103,7 @@ export const registerMemorySearchTool = (server: McpServer) => {
|
|
|
97
103
|
requestInfo.agentId,
|
|
98
104
|
ranked.map((r) => ({ memoryId: r.id, similarity: r.similarity })),
|
|
99
105
|
requestInfo.sessionId,
|
|
106
|
+
{ intent, contextKey: requestInfo.contextKey, eventType: "search" },
|
|
100
107
|
);
|
|
101
108
|
} catch (err) {
|
|
102
109
|
console.error("[memory-search] recordRetrievals failed:", (err as Error).message);
|
package/src/tools/utils.ts
CHANGED
|
@@ -21,11 +21,13 @@ export type RequestInfo = {
|
|
|
21
21
|
sessionId: string | undefined;
|
|
22
22
|
agentId: string | undefined;
|
|
23
23
|
sourceTaskId: string | undefined;
|
|
24
|
+
contextKey: string | undefined;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export const getRequestInfo = (req: Meta): RequestInfo => {
|
|
27
28
|
const agentIdHeader = req.requestInfo?.headers?.["x-agent-id"];
|
|
28
29
|
const sourceTaskIdHeader = req.requestInfo?.headers?.["x-source-task-id"];
|
|
30
|
+
const contextKeyHeader = req.requestInfo?.headers?.["x-context-key"];
|
|
29
31
|
|
|
30
32
|
let agentId: string | undefined;
|
|
31
33
|
if (Array.isArray(agentIdHeader)) {
|
|
@@ -41,10 +43,18 @@ export const getRequestInfo = (req: Meta): RequestInfo => {
|
|
|
41
43
|
sourceTaskId = sourceTaskIdHeader;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
let contextKey: string | undefined;
|
|
47
|
+
if (Array.isArray(contextKeyHeader)) {
|
|
48
|
+
contextKey = contextKeyHeader?.[0];
|
|
49
|
+
} else if (typeof contextKeyHeader === "string") {
|
|
50
|
+
contextKey = contextKeyHeader;
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
return {
|
|
45
54
|
sessionId: req.sessionId || undefined,
|
|
46
55
|
agentId,
|
|
47
56
|
sourceTaskId,
|
|
57
|
+
contextKey,
|
|
48
58
|
};
|
|
49
59
|
};
|
|
50
60
|
|
package/src/types.ts
CHANGED
|
@@ -78,7 +78,8 @@ function stripJsonFences(raw: string): string {
|
|
|
78
78
|
return fenced?.[1] ? fenced[1].trim() : trimmed;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
/** Exported for tests only — production callers go through `completeStructured`. */
|
|
82
|
+
export async function defaultSpawnClaudeCli(
|
|
82
83
|
prompt: string,
|
|
83
84
|
model: string,
|
|
84
85
|
signal?: AbortSignal,
|
|
@@ -105,6 +106,14 @@ async function defaultSpawnClaudeCli(
|
|
|
105
106
|
if (!env.CLAUDE_CODE_OAUTH_TOKEN && env.AGENT_SWARM_CLAUDE_OAUTH_TOKEN) {
|
|
106
107
|
env.CLAUDE_CODE_OAUTH_TOKEN = env.AGENT_SWARM_CLAUDE_OAUTH_TOKEN;
|
|
107
108
|
}
|
|
109
|
+
// Recursion guard: this shellout is itself a full claude session — on exit it
|
|
110
|
+
// fires the same global Stop hook, whose session-summary path would spawn
|
|
111
|
+
// another `claude -p`, recursively (each level holds a ~0.5-1GB node process;
|
|
112
|
+
// observed OOM-wedging 8GB E2B worker sandboxes within ~90s, leaving 10+
|
|
113
|
+
// near-identical summarizer transcripts in ~/.claude/projects). The hook's
|
|
114
|
+
// `runStopHookSessionSummary` honors this flag — same convention as the
|
|
115
|
+
// `claude -p` shellout in `src/be/memory/raters/llm-client.ts`.
|
|
116
|
+
env.SKIP_SESSION_SUMMARY = "1";
|
|
108
117
|
const proc = Bun.spawn({
|
|
109
118
|
cmd,
|
|
110
119
|
env,
|