@memorystack/clawdbot-memorystack 1.0.0 → 1.1.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/hooks/capture.ts +28 -2
- package/index.ts +83 -8
- package/package.json +2 -2
- package/tools/add.ts +57 -6
- package/tools/consolidate.ts +116 -0
- package/tools/delete.ts +68 -0
- package/tools/reflect.ts +119 -0
- package/tools/search.ts +144 -40
- package/verify_recent.ts +52 -0
package/hooks/capture.ts
CHANGED
|
@@ -18,7 +18,11 @@ function getLastTurn(messages: unknown[]): unknown[] {
|
|
|
18
18
|
return lastUserIdx >= 0 ? messages.slice(lastUserIdx) : messages;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function buildCaptureHandler(
|
|
21
|
+
export function buildCaptureHandler(
|
|
22
|
+
cfg: MemorystackConfig,
|
|
23
|
+
getSessionKey?: () => string | undefined,
|
|
24
|
+
getContext?: () => Record<string, any>,
|
|
25
|
+
) {
|
|
22
26
|
return async (event: Record<string, unknown>) => {
|
|
23
27
|
if (
|
|
24
28
|
!event.success ||
|
|
@@ -72,6 +76,8 @@ export function buildCaptureHandler(cfg: MemorystackConfig) {
|
|
|
72
76
|
if (captured.length === 0) return;
|
|
73
77
|
|
|
74
78
|
const content = captured.join("\n\n");
|
|
79
|
+
const sessionKey = getSessionKey?.();
|
|
80
|
+
const ctx = getContext?.() || {};
|
|
75
81
|
|
|
76
82
|
log.debug(
|
|
77
83
|
`capturing ${captured.length} texts (${content.length} chars)`,
|
|
@@ -84,8 +90,28 @@ export function buildCaptureHandler(cfg: MemorystackConfig) {
|
|
|
84
90
|
enableLogging: cfg.debug,
|
|
85
91
|
});
|
|
86
92
|
|
|
93
|
+
// Prepare metadata from context
|
|
94
|
+
const metadata: Record<string, any> = {
|
|
95
|
+
source: "clawdbot_auto_capture",
|
|
96
|
+
session_id: sessionKey,
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
user_id: ctx.userId,
|
|
99
|
+
agent_id: ctx.agentId,
|
|
100
|
+
provider: ctx.Provider,
|
|
101
|
+
model: ctx.Model,
|
|
102
|
+
sender_name: ctx.SenderName,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Remove undefined values
|
|
106
|
+
Object.keys(metadata).forEach(key => metadata[key] === undefined && delete metadata[key]);
|
|
107
|
+
|
|
87
108
|
await client.add(content, {
|
|
88
|
-
|
|
109
|
+
sessionId: sessionKey,
|
|
110
|
+
agentId: ctx.agentId,
|
|
111
|
+
userId: ctx.userId,
|
|
112
|
+
teamId: ctx.teamId,
|
|
113
|
+
conversationId: ctx.conversationId,
|
|
114
|
+
metadata: metadata,
|
|
89
115
|
});
|
|
90
116
|
|
|
91
117
|
log.debug(`captured ${captured.length} messages successfully`);
|
package/index.ts
CHANGED
|
@@ -4,9 +4,40 @@ import { initLogger } from "./logger.ts";
|
|
|
4
4
|
import { registerSearchTool } from "./tools/search.ts";
|
|
5
5
|
import { registerAddTool } from "./tools/add.ts";
|
|
6
6
|
import { registerStatsTool } from "./tools/stats.ts";
|
|
7
|
+
import { registerDeleteTool } from "./tools/delete.ts";
|
|
8
|
+
import { registerReflectTool } from "./tools/reflect.ts";
|
|
9
|
+
import { registerConsolidateTool } from "./tools/consolidate.ts";
|
|
7
10
|
import { buildRecallHandler } from "./hooks/recall.ts";
|
|
8
11
|
import { buildCaptureHandler } from "./hooks/capture.ts";
|
|
9
12
|
|
|
13
|
+
// Helper to properly parse Moltbot session keys
|
|
14
|
+
// Format: "agent:{agentId}:{rest}" where rest may be "subagent:{uuid}" or "main" etc.
|
|
15
|
+
function parseSessionKey(sessionKey: string | undefined | null): {
|
|
16
|
+
agentId: string;
|
|
17
|
+
subagentId: string | null;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
} {
|
|
20
|
+
const DEFAULT_AGENT = "main";
|
|
21
|
+
if (!sessionKey) return { agentId: DEFAULT_AGENT, subagentId: null, sessionId: "" };
|
|
22
|
+
|
|
23
|
+
const parts = sessionKey.split(":");
|
|
24
|
+
if (parts.length < 3 || parts[0] !== "agent") {
|
|
25
|
+
// Not a standard agent session key, return as-is
|
|
26
|
+
return { agentId: DEFAULT_AGENT, subagentId: null, sessionId: sessionKey };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const agentId = parts[1] || DEFAULT_AGENT;
|
|
30
|
+
const rest = parts.slice(2).join(":");
|
|
31
|
+
|
|
32
|
+
// Check if this is a subagent session
|
|
33
|
+
let subagentId: string | null = null;
|
|
34
|
+
if (rest.toLowerCase().startsWith("subagent:")) {
|
|
35
|
+
subagentId = rest.slice("subagent:".length);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { agentId, subagentId, sessionId: sessionKey };
|
|
39
|
+
}
|
|
40
|
+
|
|
10
41
|
export default {
|
|
11
42
|
id: "clawdbot-memorystack",
|
|
12
43
|
name: "MemoryStack",
|
|
@@ -24,20 +55,64 @@ export default {
|
|
|
24
55
|
|
|
25
56
|
initLogger(api.logger, cfg.debug);
|
|
26
57
|
|
|
58
|
+
// Context tracking
|
|
59
|
+
let currentContext: Record<string, any> = {};
|
|
60
|
+
const getContext = () => currentContext;
|
|
61
|
+
const getSessionKey = () => currentContext.sessionKey;
|
|
62
|
+
|
|
27
63
|
// Register tools
|
|
28
|
-
registerSearchTool(api, cfg);
|
|
29
|
-
registerAddTool(api, cfg);
|
|
64
|
+
registerSearchTool(api, cfg, getSessionKey, getContext);
|
|
65
|
+
registerAddTool(api, cfg, getSessionKey, getContext);
|
|
30
66
|
registerStatsTool(api, cfg);
|
|
67
|
+
registerDeleteTool(api, cfg);
|
|
68
|
+
registerReflectTool(api, cfg);
|
|
69
|
+
registerConsolidateTool(api, cfg);
|
|
31
70
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
71
|
+
// ALWAYS capture context on before_agent_start (regardless of autoRecall setting)
|
|
72
|
+
// This ensures tools like memorystack_add have access to agentId, subagentId, etc.
|
|
73
|
+
const recallHandler = cfg.autoRecall ? buildRecallHandler(cfg, getSessionKey) : null;
|
|
74
|
+
|
|
75
|
+
api.on("before_agent_start", (event: Record<string, unknown>, ctx: Record<string, unknown>) => {
|
|
76
|
+
if (ctx) {
|
|
77
|
+
currentContext = { ...ctx };
|
|
78
|
+
|
|
79
|
+
// Parse session key to extract proper IDs
|
|
80
|
+
// Moltbot passes agentId incorrectly as "agent" (the literal prefix)
|
|
81
|
+
// We need to parse the sessionKey to get the actual agentId
|
|
82
|
+
const parsed = parseSessionKey(currentContext.sessionKey as string);
|
|
83
|
+
|
|
84
|
+
// Use parsed agentId, overriding the incorrect ctx.agentId
|
|
85
|
+
if (!currentContext.agentId || currentContext.agentId === "agent") {
|
|
86
|
+
currentContext.agentId = parsed.agentId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Track subagent ID separately if present (for scoped searches)
|
|
90
|
+
if (parsed.subagentId) {
|
|
91
|
+
currentContext.subagentId = parsed.subagentId;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Normalize other commonly used fields
|
|
95
|
+
if (!currentContext.userId && currentContext.SenderId) currentContext.userId = currentContext.SenderId;
|
|
96
|
+
|
|
97
|
+
// Map group context to MemoryStack scoping
|
|
98
|
+
if (currentContext.groupId) currentContext.teamId = currentContext.groupId;
|
|
99
|
+
if (currentContext.groupChannel) currentContext.conversationId = currentContext.groupChannel;
|
|
100
|
+
// Fallback for conversation ID from session key if no group channel
|
|
101
|
+
if (!currentContext.conversationId && getSessionKey()) currentContext.conversationId = getSessionKey();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Only run recall if autoRecall is enabled
|
|
105
|
+
if (recallHandler) {
|
|
106
|
+
return recallHandler(event);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
37
109
|
|
|
38
110
|
// Auto-capture hook
|
|
39
111
|
if (cfg.autoCapture) {
|
|
40
|
-
|
|
112
|
+
const captureHandler = buildCaptureHandler(cfg, getSessionKey, getContext);
|
|
113
|
+
api.on("agent_end", (event: Record<string, unknown>) => {
|
|
114
|
+
return captureHandler(event);
|
|
115
|
+
});
|
|
41
116
|
}
|
|
42
117
|
|
|
43
118
|
// Register service
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memorystack/clawdbot-memorystack",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Clawdbot MemoryStack memory plugin - long-term memory for your AI assistant",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@memorystack/sdk": "^1.0.
|
|
25
|
+
"@memorystack/sdk": "^1.0.4",
|
|
26
26
|
"@sinclair/typebox": "0.34.47"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
package/tools/add.ts
CHANGED
|
@@ -7,21 +7,51 @@ import { log } from "../logger.ts";
|
|
|
7
7
|
export function registerAddTool(
|
|
8
8
|
api: ClawdbotPluginApi,
|
|
9
9
|
cfg: MemorystackConfig,
|
|
10
|
+
getSessionKey?: () => string | undefined,
|
|
11
|
+
getContext?: () => Record<string, any>,
|
|
10
12
|
): void {
|
|
11
13
|
api.registerTool(
|
|
12
14
|
{
|
|
13
15
|
name: "memorystack_add",
|
|
14
16
|
label: "Memory Add",
|
|
15
|
-
description:
|
|
17
|
+
description: `Save important information to long-term memory with automatic importance scoring.
|
|
18
|
+
|
|
19
|
+
**Automatic Context Capture**: The following are automatically captured from your session context:
|
|
20
|
+
- agent_id: Your agent identifier (e.g., "main", subagent UUID)
|
|
21
|
+
- session_id: Current session key for conversation tracking
|
|
22
|
+
- team_id: Team/group identifier (if in a group chat)
|
|
23
|
+
- conversation_id: Specific conversation thread ID
|
|
24
|
+
- user_id: The sender's ID (can be overridden with userId parameter)
|
|
25
|
+
|
|
26
|
+
You don't need to specify these manually - they are captured automatically to organize memories.`,
|
|
16
27
|
parameters: Type.Object({
|
|
17
28
|
text: Type.String({ description: "Information to remember" }),
|
|
18
|
-
userId: Type.Optional(Type.String({ description: "
|
|
29
|
+
userId: Type.Optional(Type.String({ description: "Override user ID (defaults to sender)" })),
|
|
30
|
+
memory_type: Type.Optional(
|
|
31
|
+
Type.String({
|
|
32
|
+
description: "Type of memory: fact, preference, episode, procedure, belief",
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
19
35
|
}),
|
|
20
36
|
async execute(
|
|
21
37
|
_toolCallId: string,
|
|
22
|
-
params: { text: string; userId?: string },
|
|
38
|
+
params: { text: string; userId?: string; memory_type?: string },
|
|
23
39
|
) {
|
|
24
|
-
|
|
40
|
+
const sessionKey = getSessionKey?.();
|
|
41
|
+
const ctx = getContext?.() || {};
|
|
42
|
+
|
|
43
|
+
// For subagents, use the subagent UUID as the agent_id
|
|
44
|
+
// This ensures subagent memories are properly attributed
|
|
45
|
+
const effectiveAgentId = ctx.subagentId || ctx.agentId || "main";
|
|
46
|
+
|
|
47
|
+
log.debugRequest("add", {
|
|
48
|
+
textLength: params.text.length,
|
|
49
|
+
userId: params.userId,
|
|
50
|
+
sessionKey,
|
|
51
|
+
agentId: ctx.agentId,
|
|
52
|
+
subagentId: ctx.subagentId,
|
|
53
|
+
effectiveAgentId,
|
|
54
|
+
});
|
|
25
55
|
|
|
26
56
|
const client = new MemoryStackClient({
|
|
27
57
|
apiKey: cfg.apiKey,
|
|
@@ -29,9 +59,30 @@ export function registerAddTool(
|
|
|
29
59
|
enableLogging: cfg.debug,
|
|
30
60
|
});
|
|
31
61
|
|
|
62
|
+
// Prepare metadata from context
|
|
63
|
+
const metadata: Record<string, any> = {
|
|
64
|
+
source: "clawdbot_tool",
|
|
65
|
+
session_key: sessionKey,
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
user_id: params.userId ?? ctx.userId,
|
|
68
|
+
parent_agent_id: ctx.agentId, // Track the parent agent
|
|
69
|
+
subagent_id: ctx.subagentId, // Track if this is a subagent
|
|
70
|
+
provider: ctx.Provider,
|
|
71
|
+
model: ctx.Model,
|
|
72
|
+
sender_name: ctx.SenderName,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Remove undefined values
|
|
76
|
+
Object.keys(metadata).forEach(key => metadata[key] === undefined && delete metadata[key]);
|
|
77
|
+
|
|
32
78
|
const result = await client.add(params.text, {
|
|
33
|
-
userId: params.userId,
|
|
34
|
-
|
|
79
|
+
userId: params.userId ?? ctx.userId,
|
|
80
|
+
sessionId: sessionKey,
|
|
81
|
+
agentId: effectiveAgentId, // Use subagentId if available!
|
|
82
|
+
teamId: ctx.teamId,
|
|
83
|
+
conversationId: ctx.conversationId,
|
|
84
|
+
memory_type: params.memory_type,
|
|
85
|
+
metadata: metadata,
|
|
35
86
|
});
|
|
36
87
|
|
|
37
88
|
log.debugResponse("add", {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
3
|
+
import { MemoryStackClient } from "@memorystack/sdk";
|
|
4
|
+
import type { MemorystackConfig } from "../config.ts";
|
|
5
|
+
import { log } from "../logger.ts";
|
|
6
|
+
|
|
7
|
+
export function registerConsolidateTool(
|
|
8
|
+
api: ClawdbotPluginApi,
|
|
9
|
+
cfg: MemorystackConfig,
|
|
10
|
+
): void {
|
|
11
|
+
api.registerTool(
|
|
12
|
+
{
|
|
13
|
+
name: "memorystack_consolidate",
|
|
14
|
+
label: "Consolidate Memories",
|
|
15
|
+
description:
|
|
16
|
+
"Merge duplicate or highly similar memories to reduce noise and keep memory clean. Useful for maintenance.",
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
similarityThreshold: Type.Optional(
|
|
19
|
+
Type.Number({
|
|
20
|
+
description: "Similarity threshold 0-1 (default: 0.85). Higher = stricter matching.",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
dryRun: Type.Optional(
|
|
24
|
+
Type.Boolean({
|
|
25
|
+
description: "Preview only, don't merge (default: false)",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
maxPairs: Type.Optional(
|
|
29
|
+
Type.Number({
|
|
30
|
+
description: "Max pairs to process (default: 20)",
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
}),
|
|
34
|
+
async execute(
|
|
35
|
+
_toolCallId: string,
|
|
36
|
+
params: {
|
|
37
|
+
similarityThreshold?: number;
|
|
38
|
+
dryRun?: boolean;
|
|
39
|
+
maxPairs?: number;
|
|
40
|
+
},
|
|
41
|
+
) {
|
|
42
|
+
const similarityThreshold = params.similarityThreshold ?? 0.85;
|
|
43
|
+
const dryRun = params.dryRun ?? false;
|
|
44
|
+
const maxPairs = params.maxPairs ?? 20;
|
|
45
|
+
|
|
46
|
+
log.debugRequest("consolidate", { similarityThreshold, dryRun, maxPairs });
|
|
47
|
+
|
|
48
|
+
const client = new MemoryStackClient({
|
|
49
|
+
apiKey: cfg.apiKey,
|
|
50
|
+
baseUrl: cfg.baseUrl,
|
|
51
|
+
enableLogging: cfg.debug,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const result = await client.consolidateMemories({
|
|
56
|
+
similarityThreshold,
|
|
57
|
+
dryRun,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
log.debugResponse("consolidate", {
|
|
61
|
+
merged: result.memoriesMerged ?? 0,
|
|
62
|
+
processed: result.memoriesProcessed ?? 0,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Format output
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
lines.push(`# 🧹 Memory Consolidation`);
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push(`**Memories Processed:** ${result.memoriesProcessed ?? 0}`);
|
|
70
|
+
lines.push(`**Duplicates Merged:** ${result.memoriesMerged ?? 0}`);
|
|
71
|
+
lines.push(`**Memories Removed:** ${result.memoriesRemoved ?? 0}`);
|
|
72
|
+
|
|
73
|
+
if (result.mergedPairs && result.mergedPairs.length > 0) {
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("## Merged Pairs");
|
|
76
|
+
for (const pair of result.mergedPairs.slice(0, 5)) {
|
|
77
|
+
lines.push(`- Merged: "${pair.original?.substring(0, 50)}..." → "${pair.merged?.substring(0, 50)}..."`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (dryRun) {
|
|
82
|
+
lines.push("");
|
|
83
|
+
lines.push("*This was a dry run - no changes were made.*");
|
|
84
|
+
} else if (result.memoriesMerged === 0) {
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push("✨ No duplicates found - your memory is already clean!");
|
|
87
|
+
} else {
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push("✅ Consolidation complete!");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "text" as const,
|
|
96
|
+
text: lines.join("\n"),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
details: result,
|
|
100
|
+
};
|
|
101
|
+
} catch (error: any) {
|
|
102
|
+
log.error("consolidate failed", error);
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text" as const,
|
|
107
|
+
text: `❌ Consolidation failed: ${error?.message || "Unknown error"}`,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{ name: "memorystack_consolidate" },
|
|
115
|
+
);
|
|
116
|
+
}
|
package/tools/delete.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
3
|
+
import { MemoryStackClient } from "@memorystack/sdk";
|
|
4
|
+
import type { MemorystackConfig } from "../config.ts";
|
|
5
|
+
import { log } from "../logger.ts";
|
|
6
|
+
|
|
7
|
+
export function registerDeleteTool(
|
|
8
|
+
api: ClawdbotPluginApi,
|
|
9
|
+
cfg: MemorystackConfig,
|
|
10
|
+
): void {
|
|
11
|
+
api.registerTool(
|
|
12
|
+
{
|
|
13
|
+
name: "memorystack_delete",
|
|
14
|
+
label: "Delete Memory",
|
|
15
|
+
description:
|
|
16
|
+
"Delete a memory by ID. Use search first to find the memory ID if you only have the content.",
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
memoryId: Type.String({ description: "UUID of the memory to delete" }),
|
|
19
|
+
hard: Type.Optional(
|
|
20
|
+
Type.Boolean({
|
|
21
|
+
description: "Permanently delete (true) or soft delete (false, default)",
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
}),
|
|
25
|
+
async execute(
|
|
26
|
+
_toolCallId: string,
|
|
27
|
+
params: { memoryId: string; hard?: boolean },
|
|
28
|
+
) {
|
|
29
|
+
log.debugRequest("delete", { memoryId: params.memoryId, hard: params.hard });
|
|
30
|
+
|
|
31
|
+
const client = new MemoryStackClient({
|
|
32
|
+
apiKey: cfg.apiKey,
|
|
33
|
+
baseUrl: cfg.baseUrl,
|
|
34
|
+
enableLogging: cfg.debug,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = await client.deleteMemory(params.memoryId, params.hard ?? false);
|
|
39
|
+
|
|
40
|
+
log.debugResponse("delete", { success: result.success });
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text" as const,
|
|
46
|
+
text: result.success
|
|
47
|
+
? `✅ Memory deleted successfully (ID: ${params.memoryId})`
|
|
48
|
+
: `❌ Failed to delete memory`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
details: result,
|
|
52
|
+
};
|
|
53
|
+
} catch (error: any) {
|
|
54
|
+
log.error("delete failed", error);
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text" as const,
|
|
59
|
+
text: `❌ Failed to delete memory: ${error?.message || "Unknown error"}`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{ name: "memorystack_delete" },
|
|
67
|
+
);
|
|
68
|
+
}
|
package/tools/reflect.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
3
|
+
import { MemoryStackClient } from "@memorystack/sdk";
|
|
4
|
+
import type { MemorystackConfig } from "../config.ts";
|
|
5
|
+
import { log } from "../logger.ts";
|
|
6
|
+
|
|
7
|
+
export function registerReflectTool(
|
|
8
|
+
api: ClawdbotPluginApi,
|
|
9
|
+
cfg: MemorystackConfig,
|
|
10
|
+
): void {
|
|
11
|
+
api.registerTool(
|
|
12
|
+
{
|
|
13
|
+
name: "memorystack_reflect",
|
|
14
|
+
label: "Reflect on Memories",
|
|
15
|
+
description:
|
|
16
|
+
"Analyze memories to discover patterns, generate insights, and identify recurring themes. Great for understanding user behavior over time.",
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
timeWindowDays: Type.Optional(
|
|
19
|
+
Type.Number({
|
|
20
|
+
description: "Analyze memories from last N days (default: 7, max: 90)",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
analysisDepth: Type.Optional(
|
|
24
|
+
Type.String({
|
|
25
|
+
description: "Analysis depth: 'shallow' (faster) or 'deep' (more thorough)",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
dryRun: Type.Optional(
|
|
29
|
+
Type.Boolean({
|
|
30
|
+
description: "Preview only, don't save insights (default: false)",
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
}),
|
|
34
|
+
async execute(
|
|
35
|
+
_toolCallId: string,
|
|
36
|
+
params: {
|
|
37
|
+
timeWindowDays?: number;
|
|
38
|
+
analysisDepth?: string;
|
|
39
|
+
dryRun?: boolean;
|
|
40
|
+
},
|
|
41
|
+
) {
|
|
42
|
+
const timeWindowDays = params.timeWindowDays ?? 7;
|
|
43
|
+
const analysisDepth = params.analysisDepth ?? "shallow";
|
|
44
|
+
const dryRun = params.dryRun ?? false;
|
|
45
|
+
|
|
46
|
+
log.debugRequest("reflect", { timeWindowDays, analysisDepth, dryRun });
|
|
47
|
+
|
|
48
|
+
const client = new MemoryStackClient({
|
|
49
|
+
apiKey: cfg.apiKey,
|
|
50
|
+
baseUrl: cfg.baseUrl,
|
|
51
|
+
enableLogging: cfg.debug,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const result = await client.reflectOnMemories({
|
|
56
|
+
timeWindowDays,
|
|
57
|
+
analysisDepth: analysisDepth as "shallow" | "deep",
|
|
58
|
+
dryRun,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
log.debugResponse("reflect", {
|
|
62
|
+
patterns: result.patterns?.length ?? 0,
|
|
63
|
+
insights: result.insightsGenerated ?? 0,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Format output
|
|
67
|
+
const lines: string[] = [];
|
|
68
|
+
lines.push(`# 🔮 Memory Reflection`);
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push(`**Memories Analyzed:** ${result.memoriesAnalyzed}`);
|
|
71
|
+
lines.push(`**Patterns Found:** ${result.patterns?.length ?? 0}`);
|
|
72
|
+
lines.push(`**Insights Generated:** ${result.insightsGenerated ?? 0}`);
|
|
73
|
+
|
|
74
|
+
if (result.patterns && result.patterns.length > 0) {
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push("## Patterns");
|
|
77
|
+
for (const pattern of result.patterns.slice(0, 5)) {
|
|
78
|
+
lines.push(`- **${pattern.type || "Pattern"}:** ${pattern.description || pattern.content}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (result.insights && result.insights.length > 0) {
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push("## Insights");
|
|
85
|
+
for (const insight of result.insights.slice(0, 5)) {
|
|
86
|
+
lines.push(`- ${insight.content || insight}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
lines.push("");
|
|
92
|
+
lines.push("*This was a dry run - no insights were saved.*");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text" as const,
|
|
99
|
+
text: lines.join("\n"),
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
details: result,
|
|
103
|
+
};
|
|
104
|
+
} catch (error: any) {
|
|
105
|
+
log.error("reflect failed", error);
|
|
106
|
+
return {
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: "text" as const,
|
|
110
|
+
text: `❌ Reflection failed: ${error?.message || "Unknown error"}`,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{ name: "memorystack_reflect" },
|
|
118
|
+
);
|
|
119
|
+
}
|
package/tools/search.ts
CHANGED
|
@@ -7,18 +7,43 @@ import { log } from "../logger.ts";
|
|
|
7
7
|
export function registerSearchTool(
|
|
8
8
|
api: ClawdbotPluginApi,
|
|
9
9
|
cfg: MemorystackConfig,
|
|
10
|
+
getSessionKey?: () => string | undefined,
|
|
11
|
+
getContext?: () => Record<string, any>,
|
|
10
12
|
): void {
|
|
11
13
|
api.registerTool(
|
|
12
14
|
{
|
|
13
15
|
name: "memorystack_search",
|
|
14
16
|
label: "Memory Search",
|
|
15
|
-
description:
|
|
16
|
-
|
|
17
|
+
description: `Search through long-term memories using semantic search.
|
|
18
|
+
|
|
19
|
+
**Automatic Context**: Your agent_id, session_id, team_id, and conversation_id are available for filtering.
|
|
20
|
+
|
|
21
|
+
**Scoping Options**:
|
|
22
|
+
- scope="global": Search all memories (default)
|
|
23
|
+
- scope="agent": Search only memories from THIS agent
|
|
24
|
+
- scope="team": Search only memories from this team/group
|
|
25
|
+
|
|
26
|
+
**Explicit Filters** (override scope):
|
|
27
|
+
- agent_id: Search memories from a specific agent/subagent by UUID
|
|
28
|
+
- session_id: Search memories from a specific session
|
|
29
|
+
- metadata: Filter by metadata key-value pairs (e.g., parent_agent_id, source)
|
|
30
|
+
|
|
31
|
+
**Filtering**: Supports memory_type, min_confidence, days_ago, and current_session filters.`,
|
|
17
32
|
parameters: Type.Object({
|
|
18
33
|
query: Type.String({ description: "Search query" }),
|
|
19
34
|
limit: Type.Optional(
|
|
20
35
|
Type.Number({ description: "Max results (default: 5)" }),
|
|
21
36
|
),
|
|
37
|
+
agent_id: Type.Optional(
|
|
38
|
+
Type.String({
|
|
39
|
+
description: "Filter by specific agent/subagent UUID (e.g., from a spawned subagent)",
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
session_id: Type.Optional(
|
|
43
|
+
Type.String({
|
|
44
|
+
description: "Filter by specific session key",
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
22
47
|
memory_type: Type.Optional(
|
|
23
48
|
Type.String({
|
|
24
49
|
description:
|
|
@@ -35,19 +60,47 @@ export function registerSearchTool(
|
|
|
35
60
|
description: "Only memories from last N days",
|
|
36
61
|
}),
|
|
37
62
|
),
|
|
63
|
+
current_session: Type.Optional(
|
|
64
|
+
Type.Boolean({
|
|
65
|
+
description: "Filter to current session only (default: false)",
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
scope: Type.Optional(
|
|
69
|
+
Type.String({
|
|
70
|
+
description: "Scope of search: 'global', 'agent', 'team' (default: 'global')",
|
|
71
|
+
enum: ["global", "agent", "team"],
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
metadata: Type.Optional(
|
|
75
|
+
Type.Record(Type.String(), Type.Any(), {
|
|
76
|
+
description: "Filter by metadata fields (e.g., { parent_agent_id: 'main' })",
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
38
79
|
}),
|
|
39
80
|
async execute(
|
|
40
81
|
_toolCallId: string,
|
|
41
82
|
params: {
|
|
42
83
|
query: string;
|
|
43
84
|
limit?: number;
|
|
85
|
+
agent_id?: string;
|
|
86
|
+
session_id?: string;
|
|
44
87
|
memory_type?: string;
|
|
45
88
|
min_confidence?: number;
|
|
46
89
|
days_ago?: number;
|
|
90
|
+
current_session?: boolean;
|
|
91
|
+
scope?: "global" | "agent" | "team";
|
|
92
|
+
metadata?: Record<string, any>;
|
|
47
93
|
},
|
|
48
94
|
) {
|
|
49
95
|
const limit = params.limit ?? 5;
|
|
50
|
-
|
|
96
|
+
const sessionKey = getSessionKey?.();
|
|
97
|
+
const ctx = getContext?.() || {};
|
|
98
|
+
|
|
99
|
+
log.debugRequest("search", {
|
|
100
|
+
query: params.query,
|
|
101
|
+
scope: params.scope,
|
|
102
|
+
agentId: ctx.agentId
|
|
103
|
+
});
|
|
51
104
|
|
|
52
105
|
const client = new MemoryStackClient({
|
|
53
106
|
apiKey: cfg.apiKey,
|
|
@@ -55,51 +108,102 @@ export function registerSearchTool(
|
|
|
55
108
|
enableLogging: cfg.debug,
|
|
56
109
|
});
|
|
57
110
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
111
|
+
// Build search options
|
|
112
|
+
const searchOpts: any = {
|
|
113
|
+
limit,
|
|
114
|
+
userId: ctx.userId, // Default to current user
|
|
115
|
+
min_confidence: params.min_confidence,
|
|
116
|
+
memory_type: params.memory_type,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// For subagents, use subagentId as the effective agent
|
|
120
|
+
const effectiveAgentId = ctx.subagentId || ctx.agentId;
|
|
121
|
+
|
|
122
|
+
// Priority: explicit params > scope > default
|
|
123
|
+
// 1. Explicit agent_id/session_id params override everything
|
|
124
|
+
if (params.agent_id) {
|
|
125
|
+
searchOpts.agentId = params.agent_id;
|
|
126
|
+
} else if (params.scope === "agent" && effectiveAgentId) {
|
|
127
|
+
// 2. Scope-based filtering
|
|
128
|
+
searchOpts.agentId = effectiveAgentId;
|
|
129
|
+
} else if (params.scope === "team" && ctx.teamId) {
|
|
130
|
+
searchOpts.teamId = ctx.teamId;
|
|
131
|
+
}
|
|
132
|
+
// 3. global scope = no agent filter (default)
|
|
133
|
+
|
|
134
|
+
// Session filtering: explicit param > current_session flag
|
|
135
|
+
if (params.session_id) {
|
|
136
|
+
searchOpts.sessionId = params.session_id;
|
|
137
|
+
} else if (params.current_session && sessionKey) {
|
|
138
|
+
searchOpts.sessionId = sessionKey;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Metadata filtering (passed directly to SDK)
|
|
142
|
+
if (params.metadata && Object.keys(params.metadata).length > 0) {
|
|
143
|
+
searchOpts.metadata = params.metadata;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Apply Time Filter (handled via generic params if needed, or client-side filtering)
|
|
147
|
+
// improved SDK might support start_date, but for now we can rely on natural language query
|
|
148
|
+
// or pass it if backend supports it. The backend supports start_date.
|
|
149
|
+
if (params.days_ago) {
|
|
150
|
+
const date = new Date();
|
|
151
|
+
date.setDate(date.getDate() - params.days_ago);
|
|
152
|
+
searchOpts.start_date = date.toISOString();
|
|
61
153
|
}
|
|
62
154
|
|
|
63
|
-
|
|
155
|
+
try {
|
|
156
|
+
const results = await client.search(params.query, searchOpts);
|
|
157
|
+
|
|
158
|
+
if (results.count === 0) {
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{ type: "text" as const, text: "No relevant memories found." },
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const text = results.results
|
|
167
|
+
.map((r, i) => {
|
|
168
|
+
const type = r.memory_type ? ` [${r.memory_type}]` : "";
|
|
169
|
+
const conf = r.confidence
|
|
170
|
+
? ` (${(r.confidence * 100).toFixed(0)}%)`
|
|
171
|
+
: "";
|
|
172
|
+
// Full ISO timestamp for temporal reasoning (e.g., 2024-06-03T14:30:00Z)
|
|
173
|
+
const timestamp = r.created_at ? ` [${r.created_at}]` : "";
|
|
174
|
+
return `${i + 1}. ${r.content}${type}${conf}${timestamp}`;
|
|
175
|
+
})
|
|
176
|
+
.join("\n");
|
|
177
|
+
|
|
178
|
+
log.debugResponse("search", { count: results.count });
|
|
64
179
|
|
|
65
|
-
if (results.count === 0) {
|
|
66
180
|
return {
|
|
67
181
|
content: [
|
|
68
|
-
{
|
|
182
|
+
{
|
|
183
|
+
type: "text" as const,
|
|
184
|
+
text: `Found ${results.count} memories:\n\n${text}`,
|
|
185
|
+
},
|
|
69
186
|
],
|
|
187
|
+
details: {
|
|
188
|
+
count: results.count,
|
|
189
|
+
mode: results.mode,
|
|
190
|
+
memories: results.results.map((r) => ({
|
|
191
|
+
id: r.id,
|
|
192
|
+
content: r.content,
|
|
193
|
+
memory_type: r.memory_type,
|
|
194
|
+
confidence: r.confidence,
|
|
195
|
+
created_at: r.created_at,
|
|
196
|
+
metadata: r.metadata,
|
|
197
|
+
})),
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
} catch (error: any) {
|
|
201
|
+
log.error("search failed", error);
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text: `Search failed: ${error.message}` }],
|
|
204
|
+
isError: true,
|
|
70
205
|
};
|
|
71
206
|
}
|
|
72
|
-
|
|
73
|
-
const text = results.results
|
|
74
|
-
.map((r, i) => {
|
|
75
|
-
const type = r.memory_type ? ` [${r.memory_type}]` : "";
|
|
76
|
-
const conf = r.confidence
|
|
77
|
-
? ` (${(r.confidence * 100).toFixed(0)}%)`
|
|
78
|
-
: "";
|
|
79
|
-
return `${i + 1}. ${r.content}${type}${conf}`;
|
|
80
|
-
})
|
|
81
|
-
.join("\n");
|
|
82
|
-
|
|
83
|
-
log.debugResponse("search", { count: results.count });
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
content: [
|
|
87
|
-
{
|
|
88
|
-
type: "text" as const,
|
|
89
|
-
text: `Found ${results.count} memories:\n\n${text}`,
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
details: {
|
|
93
|
-
count: results.count,
|
|
94
|
-
mode: results.mode,
|
|
95
|
-
memories: results.results.map((r) => ({
|
|
96
|
-
id: r.id,
|
|
97
|
-
content: r.content,
|
|
98
|
-
memory_type: r.memory_type,
|
|
99
|
-
confidence: r.confidence,
|
|
100
|
-
})),
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
207
|
},
|
|
104
208
|
},
|
|
105
209
|
{ name: "memorystack_search" },
|
package/verify_recent.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { MemoryStackClient } from "@memorystack/sdk";
|
|
2
|
+
|
|
3
|
+
const API_KEY = "mem_live_KT9roc_L4O60KJ6_l7SGdzXeTib1RHoF3QlmhLghMbQ";
|
|
4
|
+
|
|
5
|
+
async function verifyRecentMemories() {
|
|
6
|
+
const client = new MemoryStackClient({
|
|
7
|
+
apiKey: API_KEY,
|
|
8
|
+
baseUrl: "http://localhost:3000",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
console.log("🔍 Inspecting Most Recent Memories\n");
|
|
12
|
+
console.log("=".repeat(60) + "\n");
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Fetch 5 most recent memories
|
|
16
|
+
const results = await client.search("memorystack", {
|
|
17
|
+
limit: 5,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
console.log(`Found ${results.count} recent memories.\n`);
|
|
21
|
+
|
|
22
|
+
results.results.forEach((m, i) => {
|
|
23
|
+
console.log(`Memory #${i + 1}`);
|
|
24
|
+
console.log(` ID: ${m.id}`);
|
|
25
|
+
console.log(` Agent ID: "${m.agent_id}"`);
|
|
26
|
+
console.log(` Session ID: "${m.session_id}"`);
|
|
27
|
+
console.log(` Created At: ${m.created_at}`);
|
|
28
|
+
console.log(` Content: ${m.content.substring(0, 100)}...`);
|
|
29
|
+
|
|
30
|
+
// Check metadata for subagent details
|
|
31
|
+
if (m.metadata) {
|
|
32
|
+
console.log(` Metadata:`);
|
|
33
|
+
if (m.metadata.subagent_id) console.log(` - subagent_id: ${m.metadata.subagent_id}`);
|
|
34
|
+
if (m.metadata.parent_agent_id) console.log(` - parent_agent_id: ${m.metadata.parent_agent_id}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Verification logic
|
|
38
|
+
if (m.agent_id !== "main" && m.agent_id !== "agent") {
|
|
39
|
+
console.log(` ✅ SUCCESS: Uses specific Agent ID!`);
|
|
40
|
+
} else if (m.metadata?.subagent_id) {
|
|
41
|
+
console.log(` ⚠️ Stored as 'main', but has subagent_id in metadata.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log("-".repeat(40));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
} catch (e: any) {
|
|
48
|
+
console.log(`❌ Error: ${e.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
verifyRecentMemories().catch(console.error);
|