@memoryrelay/plugin-memoryrelay-ai 0.15.7 → 0.16.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/index.ts +472 -4873
- package/openclaw.plugin.json +41 -3
- package/package.json +1 -1
- package/src/client/memoryrelay-client.ts +816 -0
- package/src/context/namespace-router.ts +19 -0
- package/src/context/request-context.ts +39 -0
- package/src/context/session-resolver.ts +93 -0
- package/src/filters/content-patterns.ts +32 -0
- package/src/filters/noise-patterns.ts +33 -0
- package/src/filters/non-interactive.ts +30 -0
- package/src/hooks/activity.ts +51 -0
- package/src/hooks/agent-end.ts +48 -0
- package/src/hooks/before-agent-start.ts +109 -0
- package/src/hooks/before-prompt-build.ts +46 -0
- package/src/hooks/compaction.ts +51 -0
- package/src/hooks/privacy.ts +44 -0
- package/src/hooks/session-lifecycle.ts +47 -0
- package/src/hooks/subagent.ts +62 -0
- package/src/pipelines/capture/content-strip.ts +14 -0
- package/src/pipelines/capture/dedup.ts +17 -0
- package/src/pipelines/capture/index.ts +13 -0
- package/src/pipelines/capture/message-filter.ts +16 -0
- package/src/pipelines/capture/store.ts +33 -0
- package/src/pipelines/capture/trigger-gate.ts +21 -0
- package/src/pipelines/capture/truncate.ts +16 -0
- package/src/pipelines/recall/format.ts +30 -0
- package/src/pipelines/recall/index.ts +12 -0
- package/src/pipelines/recall/rank.ts +40 -0
- package/src/pipelines/recall/scope-resolver.ts +20 -0
- package/src/pipelines/recall/search.ts +43 -0
- package/src/pipelines/recall/trigger-gate.ts +17 -0
- package/src/pipelines/runner.ts +25 -0
- package/src/pipelines/types.ts +157 -0
- package/src/tools/agent-tools.ts +127 -0
- package/src/tools/decision-tools.ts +309 -0
- package/src/tools/entity-tools.ts +215 -0
- package/src/tools/health-tools.ts +42 -0
- package/src/tools/memory-tools.ts +690 -0
- package/src/tools/pattern-tools.ts +250 -0
- package/src/tools/project-tools.ts +444 -0
- package/src/tools/session-tools.ts +195 -0
- package/src/tools/v2-tools.ts +228 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/hooks/subagent.ts
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { PluginConfig, MemoryRelayClient } from "../pipelines/types.js";
|
|
4
|
+
|
|
5
|
+
export interface AutoCaptureConfig {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
tier: string;
|
|
8
|
+
blocklist?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function registerSubagentHooks(
|
|
12
|
+
api: OpenClawPluginApi,
|
|
13
|
+
config: PluginConfig,
|
|
14
|
+
client: MemoryRelayClient,
|
|
15
|
+
agentId: string,
|
|
16
|
+
autoCaptureConfig: AutoCaptureConfig,
|
|
17
|
+
isBlocklisted: (content: string, blocklist: string[]) => boolean,
|
|
18
|
+
): void {
|
|
19
|
+
api.on("subagent_spawned", async (event, _ctx) => {
|
|
20
|
+
try {
|
|
21
|
+
api.logger.debug?.(
|
|
22
|
+
`memory-memoryrelay: subagent spawned: ${event.agentId} (session: ${event.childSessionKey}, label: ${event.label || "none"})`
|
|
23
|
+
);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
api.logger.warn?.(`memory-memoryrelay: subagent_spawned hook failed: ${String(err)}`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
api.on("subagent_ended", async (event, _ctx) => {
|
|
30
|
+
try {
|
|
31
|
+
const outcome = event.outcome || "unknown";
|
|
32
|
+
const summary = `Subagent ${event.targetSessionKey} ended: ${event.reason} (outcome: ${outcome})`;
|
|
33
|
+
|
|
34
|
+
// Only store subagent completions if autoCapture is enabled and content passes filters (#44)
|
|
35
|
+
if (autoCaptureConfig.enabled) {
|
|
36
|
+
if (isBlocklisted(summary, autoCaptureConfig.blocklist || [])) {
|
|
37
|
+
api.logger.debug?.(`memory-memoryrelay: subagent completion blocklisted, skipping storage`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Skip routine completion events — only store failures or unusual outcomes
|
|
42
|
+
if (outcome === "ok" || outcome === "success") {
|
|
43
|
+
api.logger.debug?.(`memory-memoryrelay: skipping routine subagent completion: ${summary}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await client.store(summary, {
|
|
48
|
+
category: "subagent-activity",
|
|
49
|
+
source: "subagent_ended_hook",
|
|
50
|
+
agent: agentId,
|
|
51
|
+
outcome,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
api.logger.debug?.(`memory-memoryrelay: stored subagent completion: ${summary}`);
|
|
55
|
+
} else {
|
|
56
|
+
api.logger.debug?.(`memory-memoryrelay: autoCapture disabled, skipping subagent completion storage`);
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
api.logger.warn?.(`memory-memoryrelay: subagent_ended hook failed: ${String(err)}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CaptureStage } from "../types.js";
|
|
2
|
+
import { stripContent } from "../../filters/content-patterns.js";
|
|
3
|
+
|
|
4
|
+
export const captureContentStrip: CaptureStage = {
|
|
5
|
+
name: "content-strip",
|
|
6
|
+
enabled: () => true,
|
|
7
|
+
execute: async (input, _ctx) => {
|
|
8
|
+
const cleaned = input.messages
|
|
9
|
+
.map(msg => ({ ...msg, content: stripContent(msg.content) }))
|
|
10
|
+
.filter(msg => msg.content.length >= 10);
|
|
11
|
+
if (cleaned.length === 0) return { action: "skip" };
|
|
12
|
+
return { action: "continue", data: { messages: cleaned } };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CaptureStage } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const captureDedup: CaptureStage = {
|
|
4
|
+
name: "dedup",
|
|
5
|
+
enabled: () => true,
|
|
6
|
+
execute: async (input, ctx) => {
|
|
7
|
+
const kept = [];
|
|
8
|
+
for (const msg of input.messages) {
|
|
9
|
+
const existing = await ctx.client.search(msg.content, 1, 0.95, {
|
|
10
|
+
namespace: ctx.requestCtx.namespace,
|
|
11
|
+
});
|
|
12
|
+
if (existing.length === 0) { kept.push(msg); }
|
|
13
|
+
}
|
|
14
|
+
if (kept.length === 0) return { action: "skip" };
|
|
15
|
+
return { action: "continue", data: { messages: kept } };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CaptureStage } from "../types.js";
|
|
2
|
+
import { captureTriggerGate } from "./trigger-gate.js";
|
|
3
|
+
import { captureMessageFilter } from "./message-filter.js";
|
|
4
|
+
import { captureContentStrip } from "./content-strip.js";
|
|
5
|
+
import { captureTruncate } from "./truncate.js";
|
|
6
|
+
import { captureDedup } from "./dedup.js";
|
|
7
|
+
import { captureStore } from "./store.js";
|
|
8
|
+
|
|
9
|
+
export const capturePipeline: CaptureStage[] = [
|
|
10
|
+
captureTriggerGate, captureMessageFilter, captureContentStrip, captureTruncate, captureDedup, captureStore,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export { captureTriggerGate, captureMessageFilter, captureContentStrip, captureTruncate, captureDedup, captureStore };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CaptureStage } from "../types.js";
|
|
2
|
+
import { shouldDropMessage, isAssistantBoilerplate } from "../../filters/noise-patterns.js";
|
|
3
|
+
|
|
4
|
+
export const captureMessageFilter: CaptureStage = {
|
|
5
|
+
name: "message-filter",
|
|
6
|
+
enabled: () => true,
|
|
7
|
+
execute: async (input, _ctx) => {
|
|
8
|
+
const kept = input.messages.filter(msg => {
|
|
9
|
+
if (shouldDropMessage(msg)) return false;
|
|
10
|
+
if (isAssistantBoilerplate(msg)) return false;
|
|
11
|
+
return true;
|
|
12
|
+
});
|
|
13
|
+
if (kept.length === 0) return { action: "skip" };
|
|
14
|
+
return { action: "continue", data: { messages: kept } };
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CaptureStage } from "../types.js";
|
|
2
|
+
import { resolveScope } from "../../filters/content-patterns.js";
|
|
3
|
+
|
|
4
|
+
export const captureStore: CaptureStage = {
|
|
5
|
+
name: "store",
|
|
6
|
+
enabled: () => true,
|
|
7
|
+
execute: async (input, ctx) => {
|
|
8
|
+
const tier = ctx.config.autoCapture?.tier ?? "smart";
|
|
9
|
+
const maxCapture = tier === "conservative" ? 1 : tier === "aggressive" ? 5 : 3;
|
|
10
|
+
const toStore = input.messages.slice(0, maxCapture);
|
|
11
|
+
|
|
12
|
+
// Resolve session UUID for session-scoped storage
|
|
13
|
+
let sessionId: string | undefined;
|
|
14
|
+
if (ctx.sessionResolver) {
|
|
15
|
+
try {
|
|
16
|
+
const entry = await ctx.sessionResolver.resolve(ctx.requestCtx);
|
|
17
|
+
sessionId = entry.sessionId;
|
|
18
|
+
} catch {
|
|
19
|
+
// Continue without session_id if resolution fails
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const msg of toStore) {
|
|
24
|
+
const scope = resolveScope(msg.content);
|
|
25
|
+
const opts: Record<string, unknown> = { scope };
|
|
26
|
+
if (scope === "session" && sessionId) {
|
|
27
|
+
opts.session_id = sessionId;
|
|
28
|
+
}
|
|
29
|
+
await ctx.client.store(msg.content, { source: "auto-capture", scope }, opts);
|
|
30
|
+
}
|
|
31
|
+
return { action: "continue", data: input };
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CaptureStage } from "../types.js";
|
|
2
|
+
import { isNonInteractive } from "../../filters/non-interactive.js";
|
|
3
|
+
|
|
4
|
+
export const captureTriggerGate: CaptureStage = {
|
|
5
|
+
name: "trigger-gate",
|
|
6
|
+
enabled: () => true,
|
|
7
|
+
execute: async (input, ctx) => {
|
|
8
|
+
if (isNonInteractive({
|
|
9
|
+
trigger: ctx.requestCtx.trigger,
|
|
10
|
+
sessionKey: ctx.requestCtx.sessionKey,
|
|
11
|
+
prompt: ctx.requestCtx.prompt,
|
|
12
|
+
})) {
|
|
13
|
+
return { action: "skip" };
|
|
14
|
+
}
|
|
15
|
+
if (ctx.requestCtx.isSubagent) {
|
|
16
|
+
const policy = ctx.config.namespace?.subagentPolicy ?? "inherit";
|
|
17
|
+
if (policy === "skip") { return { action: "skip" }; }
|
|
18
|
+
}
|
|
19
|
+
return { action: "continue", data: input };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CaptureStage } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const captureTruncate: CaptureStage = {
|
|
4
|
+
name: "truncate",
|
|
5
|
+
enabled: () => true,
|
|
6
|
+
execute: async (input, ctx) => {
|
|
7
|
+
const maxLength = ctx.config.autoCapture?.maxMessageLength ?? 2000;
|
|
8
|
+
const truncated = input.messages.map(msg => ({
|
|
9
|
+
...msg,
|
|
10
|
+
content: msg.content.length > maxLength
|
|
11
|
+
? msg.content.slice(0, maxLength) + "\u2026"
|
|
12
|
+
: msg.content,
|
|
13
|
+
}));
|
|
14
|
+
return { action: "continue", data: { messages: truncated } };
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RecallStage, Memory } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export function formatMemories(longTerm: Memory[], session: Memory[], isSubagent: boolean): string {
|
|
4
|
+
const sections: string[] = [];
|
|
5
|
+
if (longTerm.length > 0) {
|
|
6
|
+
sections.push(`<long-term-memories>\n${longTerm.map(m => `- ${m.content}`).join("\n")}\n</long-term-memories>`);
|
|
7
|
+
}
|
|
8
|
+
if (session.length > 0) {
|
|
9
|
+
sections.push(`<session-memories>\n${session.map(m => `- ${m.content}`).join("\n")}\n</session-memories>`);
|
|
10
|
+
}
|
|
11
|
+
if (isSubagent && sections.length > 0) {
|
|
12
|
+
sections.unshift("_These memories belong to the parent session. Use for context only._");
|
|
13
|
+
}
|
|
14
|
+
return sections.join("\n\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const recallFormat: RecallStage = {
|
|
18
|
+
name: "format",
|
|
19
|
+
enabled: () => true,
|
|
20
|
+
execute: async (input, ctx) => {
|
|
21
|
+
const { isSubagent } = ctx.requestCtx;
|
|
22
|
+
const longTermMemories = (input.longTerm ?? []).map(s => s.memory);
|
|
23
|
+
const sessionMemories = (input.session ?? []).map(s => s.memory);
|
|
24
|
+
if (longTermMemories.length === 0 && sessionMemories.length === 0) {
|
|
25
|
+
return { action: "skip" };
|
|
26
|
+
}
|
|
27
|
+
const formatted = formatMemories(longTermMemories, sessionMemories, isSubagent);
|
|
28
|
+
return { action: "continue", data: { ...input, formatted } };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RecallStage } from "../types.js";
|
|
2
|
+
import { recallTriggerGate } from "./trigger-gate.js";
|
|
3
|
+
import { recallScopeResolver } from "./scope-resolver.js";
|
|
4
|
+
import { recallSearch } from "./search.js";
|
|
5
|
+
import { recallRank } from "./rank.js";
|
|
6
|
+
import { recallFormat } from "./format.js";
|
|
7
|
+
|
|
8
|
+
export const recallPipeline: RecallStage[] = [
|
|
9
|
+
recallTriggerGate, recallScopeResolver, recallSearch, recallRank, recallFormat,
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export { recallTriggerGate, recallScopeResolver, recallSearch, recallRank, recallFormat };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RecallStage, Memory } from "../types.js";
|
|
2
|
+
|
|
3
|
+
interface RankingConfig {
|
|
4
|
+
freshnessBoost?: boolean;
|
|
5
|
+
freshnessWindowHours?: number;
|
|
6
|
+
importanceBoost?: boolean;
|
|
7
|
+
tierBoost?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function scoreMemory(memory: Memory, similarity: number, rankingConfig: RankingConfig): number {
|
|
11
|
+
let score = similarity;
|
|
12
|
+
if (rankingConfig.freshnessBoost !== false) {
|
|
13
|
+
const windowHours = rankingConfig.freshnessWindowHours ?? 24;
|
|
14
|
+
const ageHours = (Date.now() - new Date(memory.created_at).getTime()) / 3_600_000;
|
|
15
|
+
if (ageHours < windowHours) { score += 0.1 * (1 - ageHours / windowHours); }
|
|
16
|
+
}
|
|
17
|
+
if (rankingConfig.importanceBoost !== false && memory.importance != null) {
|
|
18
|
+
score += 0.1 * memory.importance;
|
|
19
|
+
}
|
|
20
|
+
if (rankingConfig.tierBoost !== false && memory.tier === "hot") { score += 0.05; }
|
|
21
|
+
return Math.min(score, 1.0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const recallRank: RecallStage = {
|
|
25
|
+
name: "rank",
|
|
26
|
+
enabled: () => true,
|
|
27
|
+
execute: async (input, ctx) => {
|
|
28
|
+
const limit = ctx.config.recallLimit ?? 5;
|
|
29
|
+
const rankingConfig = ctx.config.ranking ?? {};
|
|
30
|
+
const scoredLongTerm = (input.longTerm ?? [])
|
|
31
|
+
.map(r => ({ memory: r.memory, finalScore: scoreMemory(r.memory, r.finalScore, rankingConfig) }))
|
|
32
|
+
.sort((a, b) => b.finalScore - a.finalScore)
|
|
33
|
+
.slice(0, limit);
|
|
34
|
+
const scoredSession = (input.session ?? [])
|
|
35
|
+
.map(r => ({ memory: r.memory, finalScore: scoreMemory(r.memory, r.finalScore, rankingConfig) }))
|
|
36
|
+
.sort((a, b) => b.finalScore - a.finalScore)
|
|
37
|
+
.slice(0, limit);
|
|
38
|
+
return { action: "continue", data: { ...input, longTerm: scoredLongTerm, session: scoredSession } };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RecallStage } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const recallScopeResolver: RecallStage = {
|
|
4
|
+
name: "scope-resolver",
|
|
5
|
+
enabled: () => true,
|
|
6
|
+
execute: async (input, ctx) => {
|
|
7
|
+
const { isSubagent, parentSessionKey, sessionKey } = ctx.requestCtx;
|
|
8
|
+
const policy = ctx.config.namespace?.subagentPolicy ?? "inherit";
|
|
9
|
+
if (isSubagent && policy === "skip") {
|
|
10
|
+
return { action: "skip" };
|
|
11
|
+
}
|
|
12
|
+
const resolvedSessionKey = (isSubagent && policy === "inherit")
|
|
13
|
+
? parentSessionKey ?? sessionKey
|
|
14
|
+
: sessionKey;
|
|
15
|
+
return {
|
|
16
|
+
action: "continue",
|
|
17
|
+
data: { ...input, resolvedSessionKey },
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RecallStage } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const recallSearch: RecallStage = {
|
|
4
|
+
name: "search",
|
|
5
|
+
enabled: (ctx) => !!ctx.config.autoRecall,
|
|
6
|
+
execute: async (input, ctx) => {
|
|
7
|
+
const { client } = ctx;
|
|
8
|
+
const { namespace } = ctx.requestCtx;
|
|
9
|
+
const resolvedSessionKey = input.resolvedSessionKey ?? ctx.requestCtx.sessionKey;
|
|
10
|
+
const limit = ctx.config.recallLimit ?? 5;
|
|
11
|
+
const threshold = ctx.config.recallThreshold ?? 0.3;
|
|
12
|
+
|
|
13
|
+
// Resolve session key to MemoryRelay session UUID
|
|
14
|
+
let sessionId: string | undefined;
|
|
15
|
+
if (ctx.sessionResolver) {
|
|
16
|
+
try {
|
|
17
|
+
const entry = await ctx.sessionResolver.resolve({
|
|
18
|
+
...ctx.requestCtx,
|
|
19
|
+
sessionKey: resolvedSessionKey,
|
|
20
|
+
});
|
|
21
|
+
sessionId = entry.sessionId;
|
|
22
|
+
} catch {
|
|
23
|
+
// Fall back to raw session key if resolution fails
|
|
24
|
+
sessionId = resolvedSessionKey;
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
sessionId = resolvedSessionKey;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const [longTerm, session] = await Promise.all([
|
|
31
|
+
client.search(input.prompt, limit, threshold, { scope: "long-term", namespace }),
|
|
32
|
+
client.search(input.prompt, limit, threshold, { scope: "session", session_id: sessionId, namespace }),
|
|
33
|
+
]);
|
|
34
|
+
return {
|
|
35
|
+
action: "continue",
|
|
36
|
+
data: {
|
|
37
|
+
...input,
|
|
38
|
+
longTerm: longTerm.map(r => ({ memory: r.memory, finalScore: r.score })),
|
|
39
|
+
session: session.map(r => ({ memory: r.memory, finalScore: r.score })),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RecallStage } from "../types.js";
|
|
2
|
+
import { isNonInteractive } from "../../filters/non-interactive.js";
|
|
3
|
+
|
|
4
|
+
export const recallTriggerGate: RecallStage = {
|
|
5
|
+
name: "trigger-gate",
|
|
6
|
+
enabled: () => true,
|
|
7
|
+
execute: async (input, ctx) => {
|
|
8
|
+
if (isNonInteractive({
|
|
9
|
+
trigger: ctx.requestCtx.trigger,
|
|
10
|
+
sessionKey: ctx.requestCtx.sessionKey,
|
|
11
|
+
prompt: ctx.requestCtx.prompt,
|
|
12
|
+
})) {
|
|
13
|
+
return { action: "skip" };
|
|
14
|
+
}
|
|
15
|
+
return { action: "continue", data: input };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PipelineContext } from "./types.js";
|
|
2
|
+
|
|
3
|
+
interface Stage<TInput> {
|
|
4
|
+
name: string;
|
|
5
|
+
enabled: (ctx: PipelineContext) => boolean;
|
|
6
|
+
execute: (input: TInput, ctx: PipelineContext) => Promise<
|
|
7
|
+
| { action: "continue"; data: TInput }
|
|
8
|
+
| { action: "skip" }
|
|
9
|
+
>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runPipeline<TInput>(
|
|
13
|
+
stages: Stage<TInput>[],
|
|
14
|
+
input: TInput,
|
|
15
|
+
ctx: PipelineContext,
|
|
16
|
+
): Promise<TInput | null> {
|
|
17
|
+
let current = input;
|
|
18
|
+
for (const stage of stages) {
|
|
19
|
+
if (!stage.enabled(ctx)) continue;
|
|
20
|
+
const result = await stage.execute(current, ctx);
|
|
21
|
+
if (result.action === "skip") return null;
|
|
22
|
+
current = result.data;
|
|
23
|
+
}
|
|
24
|
+
return current;
|
|
25
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export interface Memory {
|
|
2
|
+
id: string;
|
|
3
|
+
content: string;
|
|
4
|
+
agent_id: string;
|
|
5
|
+
user_id: string;
|
|
6
|
+
metadata: Record<string, string>;
|
|
7
|
+
entities: string[];
|
|
8
|
+
created_at: string;
|
|
9
|
+
updated_at: string;
|
|
10
|
+
importance?: number;
|
|
11
|
+
tier?: "hot" | "warm" | "cold";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ConversationMessage {
|
|
15
|
+
role: "user" | "assistant" | "system";
|
|
16
|
+
content: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ScoredMemory {
|
|
20
|
+
memory: Memory;
|
|
21
|
+
finalScore: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RequestContext {
|
|
25
|
+
readonly sessionKey: string;
|
|
26
|
+
readonly agentId: string | null;
|
|
27
|
+
readonly channel: string | null;
|
|
28
|
+
readonly trigger: string | null;
|
|
29
|
+
readonly prompt: string;
|
|
30
|
+
readonly isSubagent: boolean;
|
|
31
|
+
readonly parentSessionKey: string | null;
|
|
32
|
+
readonly namespace: string;
|
|
33
|
+
readonly timestamp: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PluginConfig {
|
|
37
|
+
apiKey?: string;
|
|
38
|
+
agentId?: string;
|
|
39
|
+
apiUrl?: string;
|
|
40
|
+
defaultProject?: string;
|
|
41
|
+
autoRecall?: boolean;
|
|
42
|
+
recallLimit?: number;
|
|
43
|
+
recallThreshold?: number;
|
|
44
|
+
excludeChannels?: string[];
|
|
45
|
+
autoCapture?: {
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
tier: "off" | "conservative" | "smart" | "aggressive";
|
|
48
|
+
confirmFirst?: number;
|
|
49
|
+
maxMessageLength?: number;
|
|
50
|
+
stripLargeCodeBlocks?: boolean;
|
|
51
|
+
categories?: {
|
|
52
|
+
credentials?: boolean;
|
|
53
|
+
preferences?: boolean;
|
|
54
|
+
technical?: boolean;
|
|
55
|
+
personal?: boolean;
|
|
56
|
+
};
|
|
57
|
+
blocklist?: string[];
|
|
58
|
+
};
|
|
59
|
+
namespace?: {
|
|
60
|
+
isolateAgents?: boolean;
|
|
61
|
+
subagentPolicy?: "inherit" | "isolate" | "skip";
|
|
62
|
+
};
|
|
63
|
+
ranking?: {
|
|
64
|
+
freshnessBoost?: boolean;
|
|
65
|
+
freshnessWindowHours?: number;
|
|
66
|
+
importanceBoost?: boolean;
|
|
67
|
+
tierBoost?: boolean;
|
|
68
|
+
};
|
|
69
|
+
sessionTimeoutMinutes?: number;
|
|
70
|
+
sessionCleanupIntervalMinutes?: number;
|
|
71
|
+
debug?: boolean;
|
|
72
|
+
verbose?: boolean;
|
|
73
|
+
maxLogEntries?: number;
|
|
74
|
+
logFile?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface StoreOptions {
|
|
78
|
+
deduplicate?: boolean;
|
|
79
|
+
dedup_threshold?: number;
|
|
80
|
+
project?: string;
|
|
81
|
+
importance?: number;
|
|
82
|
+
tier?: string;
|
|
83
|
+
scope?: string;
|
|
84
|
+
session_id?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SearchOptions {
|
|
88
|
+
include_confidential?: boolean;
|
|
89
|
+
include_archived?: boolean;
|
|
90
|
+
compress?: boolean;
|
|
91
|
+
max_context_tokens?: number;
|
|
92
|
+
project?: string;
|
|
93
|
+
tier?: string;
|
|
94
|
+
min_importance?: number;
|
|
95
|
+
scope?: string;
|
|
96
|
+
session_id?: string;
|
|
97
|
+
namespace?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface MemoryRelayClient {
|
|
101
|
+
search(query: string, limit?: number, threshold?: number, opts?: SearchOptions): Promise<Array<{ memory: Memory; score: number }>>;
|
|
102
|
+
store(content: string, metadata?: Record<string, string>, options?: StoreOptions): Promise<Memory>;
|
|
103
|
+
list(limit?: number, offset?: number, opts?: { scope?: string }): Promise<Memory[]>;
|
|
104
|
+
getOrCreateSession(
|
|
105
|
+
externalId: string,
|
|
106
|
+
agentId?: string,
|
|
107
|
+
title?: string,
|
|
108
|
+
project?: string,
|
|
109
|
+
metadata?: Record<string, string>,
|
|
110
|
+
): Promise<{ id: string }>;
|
|
111
|
+
endSession(sessionId: string, summary?: string): Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface SessionResolverLike {
|
|
115
|
+
resolve(requestCtx: RequestContext): Promise<{ sessionId: string; externalId: string }>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface PipelineContext {
|
|
119
|
+
readonly requestCtx: RequestContext;
|
|
120
|
+
readonly config: PluginConfig;
|
|
121
|
+
readonly client: MemoryRelayClient;
|
|
122
|
+
readonly sessionResolver?: SessionResolverLike;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface RecallInput {
|
|
126
|
+
prompt: string;
|
|
127
|
+
memories: Memory[];
|
|
128
|
+
scope: "session" | "long-term" | "all";
|
|
129
|
+
resolvedSessionKey?: string;
|
|
130
|
+
longTerm?: ScoredMemory[];
|
|
131
|
+
session?: ScoredMemory[];
|
|
132
|
+
formatted?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type RecallResult =
|
|
136
|
+
| { action: "continue"; data: RecallInput }
|
|
137
|
+
| { action: "skip" };
|
|
138
|
+
|
|
139
|
+
export interface RecallStage {
|
|
140
|
+
name: string;
|
|
141
|
+
enabled: (ctx: PipelineContext) => boolean;
|
|
142
|
+
execute: (input: RecallInput, ctx: PipelineContext) => Promise<RecallResult>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface CaptureInput {
|
|
146
|
+
messages: ConversationMessage[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type CaptureResult =
|
|
150
|
+
| { action: "continue"; data: CaptureInput }
|
|
151
|
+
| { action: "skip" };
|
|
152
|
+
|
|
153
|
+
export interface CaptureStage {
|
|
154
|
+
name: string;
|
|
155
|
+
enabled: (ctx: PipelineContext) => boolean;
|
|
156
|
+
execute: (input: CaptureInput, ctx: PipelineContext) => Promise<CaptureResult>;
|
|
157
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { PluginConfig } from "../pipelines/types.js";
|
|
3
|
+
import type { MemoryRelayClient } from "../client/memoryrelay-client.js";
|
|
4
|
+
|
|
5
|
+
export function registerAgentTools(
|
|
6
|
+
api: OpenClawPluginApi,
|
|
7
|
+
config: PluginConfig,
|
|
8
|
+
client: MemoryRelayClient,
|
|
9
|
+
isToolEnabled: (name: string) => boolean,
|
|
10
|
+
): void {
|
|
11
|
+
|
|
12
|
+
// --------------------------------------------------------------------------
|
|
13
|
+
// 14. agent_list
|
|
14
|
+
// --------------------------------------------------------------------------
|
|
15
|
+
if (isToolEnabled("agent_list")) {
|
|
16
|
+
api.registerTool((ctx) => ({
|
|
17
|
+
|
|
18
|
+
name: "agent_list",
|
|
19
|
+
description: "List available agents.",
|
|
20
|
+
parameters: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
limit: {
|
|
24
|
+
type: "number",
|
|
25
|
+
description: "Maximum agents to return. Default 20.",
|
|
26
|
+
minimum: 1,
|
|
27
|
+
maximum: 100,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
execute: async (_id, args: { limit?: number }) => {
|
|
32
|
+
try {
|
|
33
|
+
const result = await client.listAgents(args.limit);
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
36
|
+
details: { result },
|
|
37
|
+
};
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: `Failed to list agents: ${String(err)}` }],
|
|
41
|
+
details: { error: String(err) },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
{ name: "agent_list" },
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --------------------------------------------------------------------------
|
|
51
|
+
// 15. agent_create
|
|
52
|
+
// --------------------------------------------------------------------------
|
|
53
|
+
if (isToolEnabled("agent_create")) {
|
|
54
|
+
api.registerTool((ctx) => ({
|
|
55
|
+
|
|
56
|
+
name: "agent_create",
|
|
57
|
+
description: "Create a new agent. Agents serve as memory namespaces and isolation boundaries.",
|
|
58
|
+
parameters: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
name: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Agent name.",
|
|
64
|
+
},
|
|
65
|
+
description: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "Optional agent description.",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
required: ["name"],
|
|
71
|
+
},
|
|
72
|
+
execute: async (_id, args: { name: string; description?: string }) => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await client.createAgent(args.name, args.description);
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
77
|
+
details: { result },
|
|
78
|
+
};
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: `Failed to create agent: ${String(err)}` }],
|
|
82
|
+
details: { error: String(err) },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
{ name: "agent_create" },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --------------------------------------------------------------------------
|
|
92
|
+
// 16. agent_get
|
|
93
|
+
// --------------------------------------------------------------------------
|
|
94
|
+
if (isToolEnabled("agent_get")) {
|
|
95
|
+
api.registerTool((ctx) => ({
|
|
96
|
+
|
|
97
|
+
name: "agent_get",
|
|
98
|
+
description: "Get details about a specific agent by ID.",
|
|
99
|
+
parameters: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
id: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Agent UUID.",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
required: ["id"],
|
|
108
|
+
},
|
|
109
|
+
execute: async (_id, args: { id: string }) => {
|
|
110
|
+
try {
|
|
111
|
+
const result = await client.getAgent(args.id);
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
114
|
+
details: { result },
|
|
115
|
+
};
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text: `Failed to get agent: ${String(err)}` }],
|
|
119
|
+
details: { error: String(err) },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
{ name: "agent_get" },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|