@desplega.ai/agent-swarm 1.98.1 → 1.99.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 +1 -0
- package/openapi.json +20 -1
- package/package.json +3 -3
- package/src/be/boot-scrub-logs.ts +79 -20
- 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/scripts/typecheck.ts +3 -2
- package/src/commands/runner.ts +12 -2
- package/src/e2b/dispatch.ts +5 -0
- package/src/http/memory.ts +116 -7
- package/src/providers/claude-adapter.ts +13 -2
- 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/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/profile-sync.test.ts +1 -1
- package/src/tests/scripts-mcp-e2e.test.ts +1 -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/utils/internal-ai/complete-structured.ts +10 -1
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolveLinks } from "../be/memory/link-resolver";
|
|
3
|
+
|
|
4
|
+
describe("resolveLinks", () => {
|
|
5
|
+
test("extracts wikilinks from content", () => {
|
|
6
|
+
const links = resolveLinks("See [[auth-fix-pattern]] and [[pr585-codex-binary]] for context.");
|
|
7
|
+
expect(links).toHaveLength(2);
|
|
8
|
+
expect(links[0]).toMatchObject({
|
|
9
|
+
linkType: "wikilink",
|
|
10
|
+
targetKind: "memory",
|
|
11
|
+
targetId: "auth-fix-pattern",
|
|
12
|
+
resolver: "wikilink",
|
|
13
|
+
});
|
|
14
|
+
expect(links[1]).toMatchObject({
|
|
15
|
+
linkType: "wikilink",
|
|
16
|
+
targetKind: "memory",
|
|
17
|
+
targetId: "pr585-codex-binary",
|
|
18
|
+
resolver: "wikilink",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("extracts PR references with hash notation", () => {
|
|
23
|
+
const links = resolveLinks("Fixed in #696 and PR #470.");
|
|
24
|
+
const prLinks = links.filter((l) => l.linkType === "pr");
|
|
25
|
+
expect(prLinks.length).toBeGreaterThanOrEqual(2);
|
|
26
|
+
const ids = prLinks.map((l) => l.targetId);
|
|
27
|
+
expect(ids).toContain("pr:696");
|
|
28
|
+
expect(ids).toContain("pr:470");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("extracts full GitHub PR URLs", () => {
|
|
32
|
+
const links = resolveLinks(
|
|
33
|
+
"See https://github.com/desplega-ai/agent-swarm/pull/763 for the fix.",
|
|
34
|
+
);
|
|
35
|
+
const prLinks = links.filter((l) => l.linkType === "pr");
|
|
36
|
+
expect(prLinks).toHaveLength(1);
|
|
37
|
+
expect(prLinks[0]).toMatchObject({
|
|
38
|
+
linkType: "pr",
|
|
39
|
+
targetKind: "pr",
|
|
40
|
+
targetId: "github:desplega-ai/agent-swarm#763",
|
|
41
|
+
resolver: "pr-url",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("extracts agent-fs paths", () => {
|
|
46
|
+
const links = resolveLinks(
|
|
47
|
+
"Plan at live.agent-fs.dev/file/~/648a5f3c-35c8-4f11-8673-b89de52cd6bd/2faf73ba-4eee-4472-8b3b-359c4ed6bfbb/thoughts/plan.md",
|
|
48
|
+
);
|
|
49
|
+
const fsLinks = links.filter((l) => l.linkType === "agent-fs-file");
|
|
50
|
+
expect(fsLinks).toHaveLength(1);
|
|
51
|
+
expect(fsLinks[0]!.targetKind).toBe("agent-fs-file");
|
|
52
|
+
expect(fsLinks[0]!.resolver).toBe("agent-fs-path");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("extracts agent-ui page links", () => {
|
|
56
|
+
const links = resolveLinks(
|
|
57
|
+
"See app.agent-swarm.dev/pages/abc12345-1234-1234-1234-123456789abc",
|
|
58
|
+
);
|
|
59
|
+
const uiLinks = links.filter((l) => l.linkType === "agent-ui");
|
|
60
|
+
expect(uiLinks).toHaveLength(1);
|
|
61
|
+
expect(uiLinks[0]).toMatchObject({
|
|
62
|
+
targetKind: "agent-ui",
|
|
63
|
+
targetId: "page:abc12345-1234-1234-1234-123456789abc",
|
|
64
|
+
resolver: "agent-ui-page",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("deduplicates PR references", () => {
|
|
69
|
+
const links = resolveLinks("PR #696, see also PR #696 again, and #696 once more.");
|
|
70
|
+
const prLinks = links.filter((l) => l.linkType === "pr");
|
|
71
|
+
const ids696 = prLinks.filter((l) => l.targetId === "pr:696");
|
|
72
|
+
expect(ids696).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("returns empty array for content without links", () => {
|
|
76
|
+
const links = resolveLinks("This is plain text with no links or references.");
|
|
77
|
+
expect(links).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("handles mixed content with multiple link types", () => {
|
|
81
|
+
const content = `
|
|
82
|
+
See [[memory-search-fix]] for context.
|
|
83
|
+
PR #696 fixed the embedding issue.
|
|
84
|
+
Plan: live.agent-fs.dev/file/~/648a5f3c-35c8-4f11-8673-b89de52cd6bd/2faf73ba/thoughts/plan.md
|
|
85
|
+
`;
|
|
86
|
+
const links = resolveLinks(content);
|
|
87
|
+
const types = new Set(links.map((l) => l.linkType));
|
|
88
|
+
expect(types.has("wikilink")).toBe(true);
|
|
89
|
+
expect(types.has("pr")).toBe(true);
|
|
90
|
+
expect(types.has("agent-fs-file")).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -682,12 +682,15 @@ describe("OpencodeAdapter — per-task isolation (DES-300)", () => {
|
|
|
682
682
|
|
|
683
683
|
expect(lastCreateOpencodeConfig).toBeDefined();
|
|
684
684
|
const opts = lastCreateOpencodeConfig as {
|
|
685
|
+
timeout?: number;
|
|
685
686
|
config?: {
|
|
686
687
|
model?: string;
|
|
687
688
|
mcp?: Record<string, unknown>;
|
|
688
689
|
permission?: Record<string, string>;
|
|
689
690
|
};
|
|
690
691
|
};
|
|
692
|
+
// Server-start timeout must override the SDK's 5s default (E2B cold-start flake)
|
|
693
|
+
expect(opts.timeout).toBe(30_000);
|
|
691
694
|
expect(opts.config?.model).toBe("claude-sonnet-4-6");
|
|
692
695
|
expect(opts.config?.mcp?.swarm).toBeDefined();
|
|
693
696
|
const swarm = opts.config?.mcp?.swarm as {
|
|
@@ -453,7 +453,7 @@ describe("collectProfilePayloads (baseline integration)", () => {
|
|
|
453
453
|
[SOUL_MD_PATH]: LONG,
|
|
454
454
|
[IDENTITY_MD_PATH]: LONG,
|
|
455
455
|
[TOOLS_MD_PATH]: "tools",
|
|
456
|
-
|
|
456
|
+
"/workspace/HEARTBEAT.md": "heartbeat",
|
|
457
457
|
[IDENTITY_BASELINES_PATH]: JSON.stringify(baselines),
|
|
458
458
|
});
|
|
459
459
|
|
|
@@ -330,7 +330,7 @@ describe("script_ MCP HTTP proxy tools", () => {
|
|
|
330
330
|
const tools = buildToolServer();
|
|
331
331
|
const source = `
|
|
332
332
|
import type { ScriptContext, SwarmSdk } from "swarm-sdk";
|
|
333
|
-
const compileOnly = (swarm: SwarmSdk) => swarm.memory_search({ query: "foo" });
|
|
333
|
+
const compileOnly = (swarm: SwarmSdk) => swarm.memory_search({ query: "foo", intent: "test" });
|
|
334
334
|
export default async (_args: unknown, ctx: ScriptContext) => {
|
|
335
335
|
void compileOnly;
|
|
336
336
|
return { hasMemorySearch: typeof ctx.swarm.memory_search === "function" };
|
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
|
|
|
@@ -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,
|