@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
package/README.md
CHANGED
|
@@ -127,6 +127,7 @@ Check [our templates](https://templates.agent-swarm.dev) for a quick start.
|
|
|
127
127
|
- **Workflow engine with Human-in-the-Loop** — DAG-based automation with approval gates, retries, and structured I/O. [Workflows →](https://docs.agent-swarm.dev/docs/concepts/workflows)
|
|
128
128
|
- **Scheduled & recurring tasks** — cron-based automation for standing work. [Scheduling →](https://docs.agent-swarm.dev/docs/concepts/scheduling)
|
|
129
129
|
- **Durable script workflows** — launch background script runs, inspect their journals, and track them from the dashboard when a one-shot `script-run` is too small. [Guide →](https://docs.agent-swarm.dev/docs/guides/script-workflow-runs)
|
|
130
|
+
- **E2B-backed eval harness** — run a scenario × harness-config matrix against real swarm stacks, capture transcripts/artifacts, and grade outcomes with deterministic checks plus LLM or agentic judges. [Guide →](https://docs.agent-swarm.dev/docs/guides/evals-harness)
|
|
130
131
|
- **Harness & LLM agnostic** — run with Claude Code, Claude Bridge, OpenAI Codex, pi-mono (Anthropic, OpenRouter, or Amazon Bedrock), Devin, Claude Managed Agents, raw LLMs, or opencode. Tasks, schedules, and workflow agent-task nodes can use portable `modelTier` intent (`smol`, `regular`, `smart`, `ultra`) and resolve it per worker/provider at run time. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration) · [Add a new provider →](https://docs.agent-swarm.dev/docs/guides/harness-providers)
|
|
131
132
|
- **Follow-up continuity across all harnesses** — child tasks inherit a bounded prior-task context preamble built from the task chain, so continuity survives restarts and works the same across every provider. [Task lifecycle →](https://docs.agent-swarm.dev/docs/concepts/task-lifecycle)
|
|
132
133
|
- **Skills & MCP servers** — reusable procedural knowledge, bundled skill reference files, and per-agent MCP servers with scope cascade. [MCP tools →](https://docs.agent-swarm.dev/docs/reference/mcp-tools)
|
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.99.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": [
|
|
@@ -4248,6 +4248,9 @@
|
|
|
4248
4248
|
},
|
|
4249
4249
|
"persistMemory": {
|
|
4250
4250
|
"type": "boolean"
|
|
4251
|
+
},
|
|
4252
|
+
"contextKey": {
|
|
4253
|
+
"type": "string"
|
|
4251
4254
|
}
|
|
4252
4255
|
},
|
|
4253
4256
|
"required": [
|
|
@@ -4291,6 +4294,11 @@
|
|
|
4291
4294
|
"type": "string",
|
|
4292
4295
|
"minLength": 1
|
|
4293
4296
|
},
|
|
4297
|
+
"intent": {
|
|
4298
|
+
"type": "string",
|
|
4299
|
+
"minLength": 1,
|
|
4300
|
+
"description": "Why you are searching. Required for agent recall-edge tracking; omit for UI browse/search calls."
|
|
4301
|
+
},
|
|
4294
4302
|
"limit": {
|
|
4295
4303
|
"type": "integer",
|
|
4296
4304
|
"minimum": 1,
|
|
@@ -4516,6 +4524,17 @@
|
|
|
4516
4524
|
"required": true,
|
|
4517
4525
|
"name": "id",
|
|
4518
4526
|
"in": "path"
|
|
4527
|
+
},
|
|
4528
|
+
{
|
|
4529
|
+
"schema": {
|
|
4530
|
+
"type": "string",
|
|
4531
|
+
"minLength": 1,
|
|
4532
|
+
"description": "Why you are retrieving this memory. Required for agent recall-edge tracking; omit for UI browse calls."
|
|
4533
|
+
},
|
|
4534
|
+
"required": false,
|
|
4535
|
+
"description": "Why you are retrieving this memory. Required for agent recall-edge tracking; omit for UI browse calls.",
|
|
4536
|
+
"name": "intent",
|
|
4537
|
+
"in": "query"
|
|
4519
4538
|
}
|
|
4520
4539
|
],
|
|
4521
4540
|
"responses": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.99.1",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "desplega.sh <contact@desplega.sh>",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"start:portless": "portless api.swarm bun --expose-gc src/http.ts",
|
|
65
65
|
"inspector": "bunx @modelcontextprotocol/inspector --transport stdio bun src/stdio.ts",
|
|
66
66
|
"inspector:http": "bunx @modelcontextprotocol/inspector --transport http https://api.swarm.localhost:1355/mcp",
|
|
67
|
-
"lint": "biome check src",
|
|
68
|
-
"lint:fix": "biome check --write src",
|
|
67
|
+
"lint": "biome check src evals",
|
|
68
|
+
"lint:fix": "biome check --write src evals",
|
|
69
69
|
"format": "biome format --write src",
|
|
70
70
|
"build:binary": "bun build ./src/cli.tsx --compile --compile-exec-argv='--expose-gc' --target=bun-linux-x64 --outfile ./dist/agent-swarm",
|
|
71
71
|
"build:binary:arm64": "bun build ./src/cli.tsx --compile --compile-exec-argv='--expose-gc' --target=bun-linux-arm64 --outfile ./dist/agent-swarm",
|
|
@@ -5,13 +5,24 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Idempotent: already-scrubbed rows are no-ops (scrubSecrets is idempotent).
|
|
7
7
|
* Uses seed_state to avoid re-scanning on subsequent boots.
|
|
8
|
+
*
|
|
9
|
+
* Restart-safe: progress is persisted as a cursor in seed_state after each
|
|
10
|
+
* batch, so a restart (e.g. K8s probe SIGKILL) resumes from the last
|
|
11
|
+
* committed batch instead of re-scanning from zero.
|
|
12
|
+
*
|
|
13
|
+
* Non-blocking: yields to the event loop between batches so /health and
|
|
14
|
+
* startup/liveness probes stay responsive.
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
17
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
11
18
|
import { getDb } from "./db";
|
|
12
19
|
|
|
13
20
|
const SCRUB_KEY = "boot-scrub-logs-v2";
|
|
14
|
-
const
|
|
21
|
+
const CURSOR_KEY = "boot-scrub-logs-v2-cursor";
|
|
22
|
+
const BATCH_SIZE = 200;
|
|
23
|
+
|
|
24
|
+
/** Yield to the event loop so probes can respond. */
|
|
25
|
+
const yieldTick = () => new Promise<void>((r) => setTimeout(r, 5));
|
|
15
26
|
|
|
16
27
|
export async function runBootScrubLogs(): Promise<void> {
|
|
17
28
|
const db = getDb();
|
|
@@ -24,46 +35,94 @@ export async function runBootScrubLogs(): Promise<void> {
|
|
|
24
35
|
|
|
25
36
|
if (done) return;
|
|
26
37
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
WHERE content LIKE '%lin!_oauth!_%' ESCAPE '!'
|
|
35
|
-
OR content LIKE '%lin!_api!_%' ESCAPE '!'
|
|
36
|
-
OR content LIKE '%npm!_%' ESCAPE '!'
|
|
37
|
-
OR content LIKE '%ATATT%'`,
|
|
38
|
-
)
|
|
39
|
-
.all();
|
|
38
|
+
// Resume from last cursor if a previous run was interrupted
|
|
39
|
+
const savedCursor =
|
|
40
|
+
db
|
|
41
|
+
.prepare<{ seededHash: string }, [string, string]>(
|
|
42
|
+
"SELECT seededHash FROM seed_state WHERE kind = ? AND key = ?",
|
|
43
|
+
)
|
|
44
|
+
.get("maintenance", CURSOR_KEY)?.seededHash ?? "";
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
const lastProcessedId = savedCursor || "";
|
|
47
|
+
|
|
48
|
+
// Count total work remaining (for logging only)
|
|
49
|
+
const totalRemaining =
|
|
50
|
+
db
|
|
51
|
+
.prepare<{ count: number }, [string]>(
|
|
52
|
+
`SELECT COUNT(*) as count FROM session_logs
|
|
53
|
+
WHERE id > ?
|
|
54
|
+
AND (content LIKE '%lin!_oauth!_%' ESCAPE '!'
|
|
55
|
+
OR content LIKE '%lin!_api!_%' ESCAPE '!'
|
|
56
|
+
OR content LIKE '%npm!_%' ESCAPE '!'
|
|
57
|
+
OR content LIKE '%ATATT%')`,
|
|
58
|
+
)
|
|
59
|
+
.get(lastProcessedId)?.count ?? 0;
|
|
60
|
+
|
|
61
|
+
if (totalRemaining === 0) {
|
|
42
62
|
markDone(db);
|
|
43
63
|
return;
|
|
44
64
|
}
|
|
45
65
|
|
|
46
|
-
console.log(
|
|
66
|
+
console.log(
|
|
67
|
+
`[boot-scrub-logs] starting: ${totalRemaining} candidate rows remaining` +
|
|
68
|
+
(lastProcessedId ? ` (resuming from cursor ${lastProcessedId.slice(0, 8)}…)` : ""),
|
|
69
|
+
);
|
|
47
70
|
|
|
71
|
+
const selectBatch = db.prepare<{ id: string; content: string }, [string]>(
|
|
72
|
+
`SELECT id, content FROM session_logs
|
|
73
|
+
WHERE id > ?
|
|
74
|
+
AND (content LIKE '%lin!_oauth!_%' ESCAPE '!'
|
|
75
|
+
OR content LIKE '%lin!_api!_%' ESCAPE '!'
|
|
76
|
+
OR content LIKE '%npm!_%' ESCAPE '!'
|
|
77
|
+
OR content LIKE '%ATATT%')
|
|
78
|
+
ORDER BY id ASC
|
|
79
|
+
LIMIT ${BATCH_SIZE}`,
|
|
80
|
+
);
|
|
48
81
|
const update = db.prepare("UPDATE session_logs SET content = ? WHERE id = ?");
|
|
82
|
+
const saveCursor = db.prepare(
|
|
83
|
+
`INSERT INTO seed_state (kind, key, seededHash, seededAt)
|
|
84
|
+
VALUES ('maintenance', '${CURSOR_KEY}', ?, datetime('now'))
|
|
85
|
+
ON CONFLICT (kind, key) DO UPDATE SET seededHash = ?, seededAt = datetime('now')`,
|
|
86
|
+
);
|
|
87
|
+
|
|
49
88
|
let scrubbed = 0;
|
|
89
|
+
let scanned = 0;
|
|
90
|
+
let cursor = lastProcessedId;
|
|
91
|
+
|
|
92
|
+
// Paginated cursor loop — each iteration fetches the next BATCH_SIZE rows
|
|
93
|
+
// ordered by id, processes them in a transaction, saves the cursor, and
|
|
94
|
+
// yields to the event loop.
|
|
95
|
+
for (;;) {
|
|
96
|
+
const rows = selectBatch.all(cursor);
|
|
97
|
+
if (rows.length === 0) break;
|
|
98
|
+
|
|
99
|
+
const batchLastId = rows[rows.length - 1]!.id;
|
|
50
100
|
|
|
51
|
-
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
52
|
-
const batch = rows.slice(i, i + BATCH_SIZE);
|
|
53
101
|
const tx = db.transaction(() => {
|
|
54
|
-
for (const row of
|
|
102
|
+
for (const row of rows) {
|
|
55
103
|
const cleaned = scrubSecrets(row.content);
|
|
56
104
|
if (cleaned !== row.content) {
|
|
57
105
|
update.run(cleaned, row.id);
|
|
58
106
|
scrubbed++;
|
|
59
107
|
}
|
|
60
108
|
}
|
|
109
|
+
// Persist cursor inside the same transaction so it's atomic with the scrub
|
|
110
|
+
saveCursor.run(batchLastId, batchLastId);
|
|
61
111
|
});
|
|
62
112
|
tx();
|
|
113
|
+
|
|
114
|
+
scanned += rows.length;
|
|
115
|
+
cursor = batchLastId;
|
|
116
|
+
|
|
117
|
+
// Yield to the event loop between batches
|
|
118
|
+
await yieldTick();
|
|
63
119
|
}
|
|
64
120
|
|
|
65
121
|
markDone(db);
|
|
66
|
-
|
|
122
|
+
// Clean up the cursor key now that we're fully done
|
|
123
|
+
db.run("DELETE FROM seed_state WHERE kind = 'maintenance' AND key = ?", [CURSOR_KEY]);
|
|
124
|
+
|
|
125
|
+
console.log(`[boot-scrub-logs] complete: scanned=${scanned} scrubbed=${scrubbed}`);
|
|
67
126
|
}
|
|
68
127
|
|
|
69
128
|
function markDone(db: ReturnType<typeof getDb>) {
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic link resolver for memory content.
|
|
3
|
+
*
|
|
4
|
+
* Scans memory content for recognizable patterns (wikilinks, agent-fs paths,
|
|
5
|
+
* PR references, agent-UI URLs) and resolves them to typed `memory_link` rows.
|
|
6
|
+
* Phase 1: capture layer only — no traversal tools, no reranker integration.
|
|
7
|
+
*/
|
|
8
|
+
import { getDb } from "@/be/db";
|
|
9
|
+
|
|
10
|
+
export type LinkType =
|
|
11
|
+
| "wikilink"
|
|
12
|
+
| "sequel"
|
|
13
|
+
| "agent-fs-file"
|
|
14
|
+
| "agent-ui"
|
|
15
|
+
| "pr"
|
|
16
|
+
| "external-source";
|
|
17
|
+
export type TargetKind = "memory" | "agent-fs-file" | "agent-ui" | "pr" | "external-source";
|
|
18
|
+
|
|
19
|
+
export interface ResolvedLink {
|
|
20
|
+
linkType: LinkType;
|
|
21
|
+
targetKind: TargetKind;
|
|
22
|
+
targetId: string;
|
|
23
|
+
strength: number;
|
|
24
|
+
resolver: string;
|
|
25
|
+
sourceText: string;
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface MatcherResult {
|
|
30
|
+
linkType: LinkType;
|
|
31
|
+
targetKind: TargetKind;
|
|
32
|
+
targetId: string;
|
|
33
|
+
sourceText: string;
|
|
34
|
+
resolver: string;
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Matcher = (content: string) => MatcherResult[];
|
|
39
|
+
|
|
40
|
+
const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
41
|
+
const PR_HASH_RE = /(?:^|[\s(])#(\d{1,5})(?=[\s,.)!?]|$)/gm;
|
|
42
|
+
const PR_PREFIX_RE = /\bPR\s*#(\d{1,5})\b/gi;
|
|
43
|
+
const GITHUB_PR_URL_RE = /https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/g;
|
|
44
|
+
const AGENT_FS_PATH_RE =
|
|
45
|
+
/(?:agent-fs|live\.agent-fs\.dev)\/file\/~\/([a-f0-9-]+)\/([a-f0-9-]+)\/([\w/.%-]+)/g;
|
|
46
|
+
const AGENT_UI_PAGE_RE = /(?:app\.[^/]+|localhost:\d+)\/pages\/([a-f0-9-]+)/g;
|
|
47
|
+
const AGENT_UI_TASK_RE = /(?:app\.[^/]+|localhost:\d+)\/tasks\/([a-f0-9-]+)/g;
|
|
48
|
+
|
|
49
|
+
const wikilinkMatcher: Matcher = (content) => {
|
|
50
|
+
const results: MatcherResult[] = [];
|
|
51
|
+
for (const match of content.matchAll(WIKILINK_RE)) {
|
|
52
|
+
const name = match[1]!.trim();
|
|
53
|
+
if (!name) continue;
|
|
54
|
+
results.push({
|
|
55
|
+
linkType: "wikilink",
|
|
56
|
+
targetKind: "memory",
|
|
57
|
+
targetId: name,
|
|
58
|
+
sourceText: match[0],
|
|
59
|
+
resolver: "wikilink",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const prMatcher: Matcher = (content) => {
|
|
66
|
+
const results: MatcherResult[] = [];
|
|
67
|
+
const seen = new Set<string>();
|
|
68
|
+
|
|
69
|
+
for (const match of content.matchAll(GITHUB_PR_URL_RE)) {
|
|
70
|
+
const id = `github:${match[1]}/${match[2]}#${match[3]}`;
|
|
71
|
+
if (seen.has(id)) continue;
|
|
72
|
+
seen.add(id);
|
|
73
|
+
results.push({
|
|
74
|
+
linkType: "pr",
|
|
75
|
+
targetKind: "pr",
|
|
76
|
+
targetId: id,
|
|
77
|
+
sourceText: match[0],
|
|
78
|
+
resolver: "pr-url",
|
|
79
|
+
metadata: { owner: match[1], repo: match[2], number: Number(match[3]) },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const match of content.matchAll(PR_PREFIX_RE)) {
|
|
84
|
+
const id = `pr:${match[1]}`;
|
|
85
|
+
if (seen.has(id)) continue;
|
|
86
|
+
seen.add(id);
|
|
87
|
+
results.push({
|
|
88
|
+
linkType: "pr",
|
|
89
|
+
targetKind: "pr",
|
|
90
|
+
targetId: id,
|
|
91
|
+
sourceText: match[0].trim(),
|
|
92
|
+
resolver: "pr-prefix",
|
|
93
|
+
metadata: { number: Number(match[1]) },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const match of content.matchAll(PR_HASH_RE)) {
|
|
98
|
+
const id = `pr:${match[1]}`;
|
|
99
|
+
if (seen.has(id)) continue;
|
|
100
|
+
seen.add(id);
|
|
101
|
+
results.push({
|
|
102
|
+
linkType: "pr",
|
|
103
|
+
targetKind: "pr",
|
|
104
|
+
targetId: id,
|
|
105
|
+
sourceText: `#${match[1]}`,
|
|
106
|
+
resolver: "pr-hash",
|
|
107
|
+
metadata: { number: Number(match[1]) },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return results;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const agentFsMatcher: Matcher = (content) => {
|
|
115
|
+
const results: MatcherResult[] = [];
|
|
116
|
+
for (const match of content.matchAll(AGENT_FS_PATH_RE)) {
|
|
117
|
+
const orgId = match[1]!;
|
|
118
|
+
const driveId = match[2]!;
|
|
119
|
+
const path = match[3]!;
|
|
120
|
+
results.push({
|
|
121
|
+
linkType: "agent-fs-file",
|
|
122
|
+
targetKind: "agent-fs-file",
|
|
123
|
+
targetId: `${orgId}/${driveId}/${path}`,
|
|
124
|
+
sourceText: match[0],
|
|
125
|
+
resolver: "agent-fs-path",
|
|
126
|
+
metadata: { orgId, driveId, path },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return results;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const agentUiMatcher: Matcher = (content) => {
|
|
133
|
+
const results: MatcherResult[] = [];
|
|
134
|
+
for (const match of content.matchAll(AGENT_UI_PAGE_RE)) {
|
|
135
|
+
results.push({
|
|
136
|
+
linkType: "agent-ui",
|
|
137
|
+
targetKind: "agent-ui",
|
|
138
|
+
targetId: `page:${match[1]}`,
|
|
139
|
+
sourceText: match[0],
|
|
140
|
+
resolver: "agent-ui-page",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
for (const match of content.matchAll(AGENT_UI_TASK_RE)) {
|
|
144
|
+
results.push({
|
|
145
|
+
linkType: "agent-ui",
|
|
146
|
+
targetKind: "agent-ui",
|
|
147
|
+
targetId: `task:${match[1]}`,
|
|
148
|
+
sourceText: match[0],
|
|
149
|
+
resolver: "agent-ui-task",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return results;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const MATCHERS: Matcher[] = [wikilinkMatcher, prMatcher, agentFsMatcher, agentUiMatcher];
|
|
156
|
+
|
|
157
|
+
export function resolveLinks(content: string): ResolvedLink[] {
|
|
158
|
+
const results: ResolvedLink[] = [];
|
|
159
|
+
for (const matcher of MATCHERS) {
|
|
160
|
+
for (const link of matcher(content)) {
|
|
161
|
+
results.push({ ...link, strength: 1.0 });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return results;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function resolveWikilinksToMemoryIds(
|
|
168
|
+
agentId: string,
|
|
169
|
+
links: ResolvedLink[],
|
|
170
|
+
): ResolvedLink[] {
|
|
171
|
+
const db = getDb();
|
|
172
|
+
const findByName = db.prepare<{ id: string }, [string, string]>(
|
|
173
|
+
"SELECT id FROM agent_memory WHERE name = ? AND (agentId = ? OR scope = 'swarm') LIMIT 1",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return links.map((link) => {
|
|
177
|
+
if (link.linkType !== "wikilink") return link;
|
|
178
|
+
const row = findByName.get(link.targetId, agentId);
|
|
179
|
+
if (row) {
|
|
180
|
+
return { ...link, targetId: row.id };
|
|
181
|
+
}
|
|
182
|
+
return link;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function storeLinks(memoryId: string, agentId: string, content: string): void {
|
|
187
|
+
const links = resolveLinks(content);
|
|
188
|
+
if (links.length === 0) return;
|
|
189
|
+
|
|
190
|
+
const resolved = resolveWikilinksToMemoryIds(agentId, links);
|
|
191
|
+
const db = getDb();
|
|
192
|
+
const now = new Date().toISOString();
|
|
193
|
+
const insert = db.prepare(
|
|
194
|
+
`INSERT OR IGNORE INTO memory_link
|
|
195
|
+
(id, from_memory_id, linkType, targetKind, targetId, strength, resolver, sourceText, metadata, createdAt, updatedAt)
|
|
196
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
db.transaction(() => {
|
|
200
|
+
for (const link of resolved) {
|
|
201
|
+
insert.run(
|
|
202
|
+
crypto.randomUUID(),
|
|
203
|
+
memoryId,
|
|
204
|
+
link.linkType,
|
|
205
|
+
link.targetKind,
|
|
206
|
+
link.targetId,
|
|
207
|
+
link.strength,
|
|
208
|
+
link.resolver,
|
|
209
|
+
link.sourceText,
|
|
210
|
+
link.metadata ? JSON.stringify(link.metadata) : null,
|
|
211
|
+
now,
|
|
212
|
+
now,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function storeSequelLink(fromMemoryId: string, toMemoryId: string): void {
|
|
219
|
+
const db = getDb();
|
|
220
|
+
const now = new Date().toISOString();
|
|
221
|
+
db.prepare(
|
|
222
|
+
`INSERT OR IGNORE INTO memory_link
|
|
223
|
+
(id, from_memory_id, linkType, targetKind, targetId, strength, resolver, sourceText, metadata, createdAt, updatedAt)
|
|
224
|
+
VALUES (?, ?, 'sequel', 'memory', ?, 1.0, 'sequel-auto', NULL, NULL, ?, ?)`,
|
|
225
|
+
).run(crypto.randomUUID(), fromMemoryId, toMemoryId, now, now);
|
|
226
|
+
}
|
|
@@ -259,10 +259,11 @@ export class SqliteMemoryStore implements MemoryStore {
|
|
|
259
259
|
string | null,
|
|
260
260
|
number,
|
|
261
261
|
string | null,
|
|
262
|
+
string | null,
|
|
262
263
|
]
|
|
263
264
|
>(
|
|
264
|
-
`INSERT INTO agent_memory (id, agentId, scope, name, content, summary, source, sourceTaskId, sourcePath, chunkIndex, totalChunks, tags, createdAt, accessedAt, expiresAt, accessCount, embeddingModel)
|
|
265
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
265
|
+
`INSERT INTO agent_memory (id, agentId, scope, name, content, summary, source, sourceTaskId, sourcePath, chunkIndex, totalChunks, tags, createdAt, accessedAt, expiresAt, accessCount, embeddingModel, contextKey)
|
|
266
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
266
267
|
)
|
|
267
268
|
.get(
|
|
268
269
|
id,
|
|
@@ -282,6 +283,7 @@ export class SqliteMemoryStore implements MemoryStore {
|
|
|
282
283
|
expiresAt,
|
|
283
284
|
0,
|
|
284
285
|
null,
|
|
286
|
+
input.contextKey ?? null,
|
|
285
287
|
);
|
|
286
288
|
|
|
287
289
|
if (!row) throw new Error("Failed to create memory");
|
|
@@ -24,24 +24,32 @@ export type RetrievalRecord = {
|
|
|
24
24
|
similarity: number;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
export type RetrievalExtras = {
|
|
28
|
+
intent?: string;
|
|
29
|
+
contextKey?: string;
|
|
30
|
+
eventType?: "search" | "get";
|
|
31
|
+
};
|
|
32
|
+
|
|
27
33
|
export function recordRetrievals(
|
|
28
34
|
taskId: string | undefined,
|
|
29
35
|
agentId: string,
|
|
30
36
|
results: RetrievalRecord[],
|
|
31
37
|
sessionId?: string,
|
|
38
|
+
extras?: RetrievalExtras,
|
|
32
39
|
): void {
|
|
33
40
|
if (!taskId || results.length === 0) return;
|
|
34
41
|
|
|
35
42
|
const db = getDb();
|
|
36
43
|
const insert = db.prepare(
|
|
37
44
|
`INSERT INTO memory_retrieval
|
|
38
|
-
(id, taskId, agentId, sessionId, memoryId, similarity, retrievedAt)
|
|
39
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
45
|
+
(id, taskId, agentId, sessionId, memoryId, similarity, retrievedAt, contextKey, intent, eventType)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
40
47
|
);
|
|
41
48
|
const now = new Date().toISOString();
|
|
49
|
+
const contextKey = extras?.contextKey ?? null;
|
|
50
|
+
const intent = extras?.intent ?? null;
|
|
51
|
+
const eventType = extras?.eventType ?? "search";
|
|
42
52
|
|
|
43
|
-
// Single transaction: even on a 100-row paginated search this is one
|
|
44
|
-
// commit, not N. No-op when results is empty.
|
|
45
53
|
db.transaction(() => {
|
|
46
54
|
for (const r of results) {
|
|
47
55
|
insert.run(
|
|
@@ -52,6 +60,9 @@ export function recordRetrievals(
|
|
|
52
60
|
r.memoryId,
|
|
53
61
|
r.similarity,
|
|
54
62
|
now,
|
|
63
|
+
contextKey,
|
|
64
|
+
intent,
|
|
65
|
+
eventType,
|
|
55
66
|
);
|
|
56
67
|
}
|
|
57
68
|
})();
|
|
@@ -31,6 +31,7 @@ export type ApplyRatingResult = {
|
|
|
31
31
|
|
|
32
32
|
export type ApplyRatingContext = {
|
|
33
33
|
taskId?: string;
|
|
34
|
+
contextKey?: string;
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
export class ExplicitSelfDuplicateError extends Error {
|
|
@@ -100,8 +101,8 @@ export function applyRating(
|
|
|
100
101
|
);
|
|
101
102
|
const insertRating = db.prepare(
|
|
102
103
|
`INSERT INTO memory_rating
|
|
103
|
-
(id, memoryId, taskId, source, signal, weight, reasoning, createdAt)
|
|
104
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
104
|
+
(id, memoryId, taskId, source, signal, weight, reasoning, createdAt, contextKey)
|
|
105
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
105
106
|
);
|
|
106
107
|
// Step-6 §3 — UPSERT the edge with the SAME deltas as the memory row.
|
|
107
108
|
// The `- 1.0` corrections in DO UPDATE undo the default-prior offset that
|
|
@@ -141,6 +142,7 @@ export function applyRating(
|
|
|
141
142
|
event.weight,
|
|
142
143
|
event.reasoning ?? null,
|
|
143
144
|
new Date().toISOString(),
|
|
145
|
+
ctx.contextKey ?? null,
|
|
144
146
|
);
|
|
145
147
|
} catch (err) {
|
|
146
148
|
// Partial unique index on (taskId, memoryId) WHERE source='explicit-self'
|
package/src/be/memory/types.ts
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- Phase 1 of memory-graph recall-edges: contextKey threading, intent capture,
|
|
2
|
+
-- memory-get retrieval events, and deterministic typed links.
|
|
3
|
+
|
|
4
|
+
-- 1. Add contextKey to agent_memory (born-under context)
|
|
5
|
+
ALTER TABLE agent_memory ADD COLUMN contextKey TEXT;
|
|
6
|
+
CREATE INDEX idx_agent_memory_context_key ON agent_memory(contextKey);
|
|
7
|
+
|
|
8
|
+
-- 2. Extend memory_retrieval with contextKey, intent, and eventType
|
|
9
|
+
ALTER TABLE memory_retrieval ADD COLUMN contextKey TEXT;
|
|
10
|
+
ALTER TABLE memory_retrieval ADD COLUMN intent TEXT;
|
|
11
|
+
ALTER TABLE memory_retrieval ADD COLUMN eventType TEXT NOT NULL DEFAULT 'search'
|
|
12
|
+
CHECK (eventType IN ('search', 'get'));
|
|
13
|
+
CREATE INDEX idx_memret_context_key ON memory_retrieval(contextKey);
|
|
14
|
+
CREATE INDEX idx_memret_event_type ON memory_retrieval(eventType);
|
|
15
|
+
|
|
16
|
+
-- 3. Add contextKey to memory_rating
|
|
17
|
+
ALTER TABLE memory_rating ADD COLUMN contextKey TEXT;
|
|
18
|
+
CREATE INDEX idx_memrat_context_key ON memory_rating(contextKey);
|
|
19
|
+
|
|
20
|
+
-- 4. New table: memory_link — deterministic typed links between memories
|
|
21
|
+
-- and external entities (agent-fs files, PRs, agent-UI pages, etc.)
|
|
22
|
+
CREATE TABLE IF NOT EXISTS memory_link (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
from_memory_id TEXT NOT NULL,
|
|
25
|
+
linkType TEXT NOT NULL CHECK (
|
|
26
|
+
linkType IN (
|
|
27
|
+
'wikilink',
|
|
28
|
+
'sequel',
|
|
29
|
+
'agent-fs-file',
|
|
30
|
+
'agent-ui',
|
|
31
|
+
'pr',
|
|
32
|
+
'external-source'
|
|
33
|
+
)
|
|
34
|
+
),
|
|
35
|
+
targetKind TEXT NOT NULL CHECK (
|
|
36
|
+
targetKind IN ('memory', 'agent-fs-file', 'agent-ui', 'pr', 'external-source')
|
|
37
|
+
),
|
|
38
|
+
targetId TEXT NOT NULL,
|
|
39
|
+
strength REAL NOT NULL DEFAULT 1.0,
|
|
40
|
+
resolver TEXT NOT NULL,
|
|
41
|
+
sourceText TEXT,
|
|
42
|
+
metadata TEXT,
|
|
43
|
+
createdAt TEXT NOT NULL,
|
|
44
|
+
updatedAt TEXT NOT NULL,
|
|
45
|
+
UNIQUE (from_memory_id, linkType, targetKind, targetId, sourceText),
|
|
46
|
+
FOREIGN KEY (from_memory_id) REFERENCES agent_memory(id) ON DELETE CASCADE
|
|
47
|
+
);
|
|
48
|
+
CREATE INDEX idx_memlink_from ON memory_link(from_memory_id);
|
|
49
|
+
CREATE INDEX idx_memlink_target ON memory_link(targetKind, targetId);
|
|
50
|
+
CREATE INDEX idx_memlink_type ON memory_link(linkType);
|
|
@@ -51,8 +51,8 @@ export interface SwarmConfig {
|
|
|
51
51
|
|
|
52
52
|
export interface SwarmSdk {
|
|
53
53
|
// --- memory ---
|
|
54
|
-
memory_search(args: { query: string; scope?: "all" | "agent" | "swarm"; limit?: number; source?: string }): Promise<unknown>;
|
|
55
|
-
memory_get(args: { memoryId: string }): Promise<unknown>;
|
|
54
|
+
memory_search(args: { query: string; intent: string; scope?: "all" | "agent" | "swarm"; limit?: number; source?: string }): Promise<unknown>;
|
|
55
|
+
memory_get(args: { memoryId: string; intent: string }): Promise<unknown>;
|
|
56
56
|
memory_rate(args: { id: string; useful: boolean; note?: string }): Promise<unknown>;
|
|
57
57
|
// --- tasks ---
|
|
58
58
|
task_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
@@ -176,6 +176,7 @@ export interface SwarmSdk {
|
|
|
176
176
|
// --- skills ---
|
|
177
177
|
skill_list(args?: { scope?: string; scopeId?: string; includeBuiltin?: boolean }): Promise<unknown>;
|
|
178
178
|
skill_get(args: { id: string }): Promise<unknown>;
|
|
179
|
+
skill_getFile(args: { skillId: string; path: string }): Promise<unknown>;
|
|
179
180
|
skill_search(args: { query: string; limit?: number }): Promise<unknown>;
|
|
180
181
|
skill_create(args: Record<string, unknown>): Promise<unknown>;
|
|
181
182
|
skill_update(args: Record<string, unknown>): Promise<unknown>;
|