@hydra_db/openclaw 0.1.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/.github/workflows/publish.yaml +40 -0
- package/README.md +172 -0
- package/client.ts +214 -0
- package/commands/cli.ts +97 -0
- package/commands/onboarding.ts +485 -0
- package/commands/slash.ts +138 -0
- package/config.ts +128 -0
- package/context.ts +191 -0
- package/hooks/capture.ts +101 -0
- package/hooks/recall.ts +46 -0
- package/index.ts +212 -0
- package/log.ts +48 -0
- package/messages.ts +88 -0
- package/openclaw.plugin.json +74 -0
- package/package.json +27 -0
- package/session.ts +11 -0
- package/tools/delete.ts +54 -0
- package/tools/get.ts +57 -0
- package/tools/list.ts +56 -0
- package/tools/search.ts +64 -0
- package/tools/store.ts +116 -0
- package/tsconfig.json +23 -0
- package/types/hydra.ts +166 -0
- package/types/openclaw.d.ts +19 -0
package/log.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type LoggerBackend = {
|
|
2
|
+
info(msg: string): void
|
|
3
|
+
warn(msg: string): void
|
|
4
|
+
error(msg: string): void
|
|
5
|
+
debug?(msg: string): void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const TAG = "[hydra-db]"
|
|
9
|
+
|
|
10
|
+
let _backend: LoggerBackend | null = null
|
|
11
|
+
let _debug = false
|
|
12
|
+
|
|
13
|
+
export const log = {
|
|
14
|
+
init(backend: LoggerBackend, debug: boolean) {
|
|
15
|
+
_backend = backend
|
|
16
|
+
_debug = debug
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
setDebug(enabled: boolean) {
|
|
20
|
+
_debug = enabled
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
info(...args: unknown[]) {
|
|
24
|
+
const msg = `${TAG} ${args.map(String).join(" ")}`
|
|
25
|
+
if (_backend) _backend.info(msg)
|
|
26
|
+
else console.log(msg)
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
warn(...args: unknown[]) {
|
|
30
|
+
const msg = `${TAG} ${args.map(String).join(" ")}`
|
|
31
|
+
if (_backend) _backend.warn(msg)
|
|
32
|
+
else console.warn(msg)
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
error(...args: unknown[]) {
|
|
36
|
+
const msg = `${TAG} ${args.map(String).join(" ")}`
|
|
37
|
+
if (_backend) _backend.error(msg)
|
|
38
|
+
else console.error(msg)
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
debug(...args: unknown[]) {
|
|
42
|
+
if (!_debug) return
|
|
43
|
+
const msg = `${TAG} ${args.map(String).join(" ")}`
|
|
44
|
+
if (_backend?.debug) _backend.debug(msg)
|
|
45
|
+
else if (_backend) _backend.info(msg)
|
|
46
|
+
else console.debug(msg)
|
|
47
|
+
},
|
|
48
|
+
}
|
package/messages.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ConversationTurn } from "./types/hydra.ts"
|
|
2
|
+
|
|
3
|
+
export function containsIgnoreTerm(text: string, ignoreTerm: string): boolean {
|
|
4
|
+
return text.toLowerCase().includes(ignoreTerm.toLowerCase())
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function filterIgnoredTurns(
|
|
8
|
+
turns: ConversationTurn[],
|
|
9
|
+
ignoreTerm: string,
|
|
10
|
+
): ConversationTurn[] {
|
|
11
|
+
return turns.filter(
|
|
12
|
+
(t) =>
|
|
13
|
+
!containsIgnoreTerm(t.user, ignoreTerm) &&
|
|
14
|
+
!containsIgnoreTerm(t.assistant, ignoreTerm),
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function textFromMessage(msg: Record<string, unknown>): string {
|
|
19
|
+
const content = msg.content
|
|
20
|
+
if (typeof content === "string") return content
|
|
21
|
+
if (Array.isArray(content)) {
|
|
22
|
+
return content
|
|
23
|
+
.filter(
|
|
24
|
+
(b) =>
|
|
25
|
+
b &&
|
|
26
|
+
typeof b === "object" &&
|
|
27
|
+
(b as Record<string, unknown>).type === "text",
|
|
28
|
+
)
|
|
29
|
+
.map((b) => (b as Record<string, unknown>).text as string)
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join("\n")
|
|
32
|
+
}
|
|
33
|
+
return ""
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function extractAllTurns(messages: unknown[]): ConversationTurn[] {
|
|
37
|
+
const turns: ConversationTurn[] = []
|
|
38
|
+
let currentUserText: string | null = null
|
|
39
|
+
let currentAssistantText: string | null = null
|
|
40
|
+
|
|
41
|
+
for (const msg of messages) {
|
|
42
|
+
if (!msg || typeof msg !== "object") continue
|
|
43
|
+
const m = msg as Record<string, unknown>
|
|
44
|
+
const text = textFromMessage(m)
|
|
45
|
+
|
|
46
|
+
if (m.role === "user") {
|
|
47
|
+
if (!text) continue
|
|
48
|
+
if (currentUserText && currentAssistantText) {
|
|
49
|
+
turns.push({ user: currentUserText, assistant: currentAssistantText })
|
|
50
|
+
}
|
|
51
|
+
currentUserText = text
|
|
52
|
+
currentAssistantText = "no-message"
|
|
53
|
+
} else if (m.role === "assistant") {
|
|
54
|
+
if (!text) continue
|
|
55
|
+
currentAssistantText = text
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (currentUserText && currentAssistantText) {
|
|
60
|
+
turns.push({ user: currentUserText, assistant: currentAssistantText })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return turns
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getLatestTurn(messages: unknown[]): ConversationTurn | null {
|
|
67
|
+
let userIdx = -1
|
|
68
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
69
|
+
const m = messages[i]
|
|
70
|
+
if (m && typeof m === "object" && (m as Record<string, unknown>).role === "user") {
|
|
71
|
+
userIdx = i
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (userIdx < 0) return null
|
|
76
|
+
|
|
77
|
+
const userText = textFromMessage(messages[userIdx] as Record<string, unknown>)
|
|
78
|
+
if (!userText) return null
|
|
79
|
+
|
|
80
|
+
for (let i = userIdx + 1; i < messages.length; i++) {
|
|
81
|
+
const m = messages[i]
|
|
82
|
+
if (m && typeof m === "object" && (m as Record<string, unknown>).role === "assistant") {
|
|
83
|
+
const aText = textFromMessage(m as Record<string, unknown>)
|
|
84
|
+
if (aText) return { user: userText, assistant: aText }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-hydra-db",
|
|
3
|
+
"kind": "memory",
|
|
4
|
+
"uiHints": {
|
|
5
|
+
"apiKey": {
|
|
6
|
+
"label": "Hydra API Key",
|
|
7
|
+
"sensitive": true,
|
|
8
|
+
"placeholder": "your-hydra-api-key",
|
|
9
|
+
"help": "Your API key from Hydra (or use ${HYDRA_OPENCLAW_API_KEY})"
|
|
10
|
+
},
|
|
11
|
+
"tenantId": {
|
|
12
|
+
"label": "Tenant ID",
|
|
13
|
+
"placeholder": "tenant-01",
|
|
14
|
+
"help": "Your Hydra tenant identifier (or use ${HYDRA_OPENCLAW_TENANT_ID})"
|
|
15
|
+
},
|
|
16
|
+
"subTenantId": {
|
|
17
|
+
"label": "Sub-Tenant ID",
|
|
18
|
+
"placeholder": "hydra-openclaw",
|
|
19
|
+
"help": "Sub-tenant for data partitioning (default: hydra-openclaw)",
|
|
20
|
+
"advanced": true
|
|
21
|
+
},
|
|
22
|
+
"autoRecall": {
|
|
23
|
+
"label": "Auto-Recall",
|
|
24
|
+
"help": "Inject relevant memories before every AI turn (default: true)"
|
|
25
|
+
},
|
|
26
|
+
"autoCapture": {
|
|
27
|
+
"label": "Auto-Capture",
|
|
28
|
+
"help": "Automatically store conversation exchanges after every AI turn (default: true)"
|
|
29
|
+
},
|
|
30
|
+
"maxRecallResults": {
|
|
31
|
+
"label": "Max Recall Results",
|
|
32
|
+
"placeholder": "10",
|
|
33
|
+
"help": "Maximum memory chunks injected into context per turn",
|
|
34
|
+
"advanced": true
|
|
35
|
+
},
|
|
36
|
+
"recallMode": {
|
|
37
|
+
"label": "Recall Mode",
|
|
38
|
+
"help": "'fast' (default) or 'thinking' (personalised recall with deeper graph traversal)",
|
|
39
|
+
"advanced": true
|
|
40
|
+
},
|
|
41
|
+
"graphContext": {
|
|
42
|
+
"label": "Graph Context",
|
|
43
|
+
"help": "Include knowledge graph relations in recalled context (default: true)",
|
|
44
|
+
"advanced": true
|
|
45
|
+
},
|
|
46
|
+
"ignoreTerm": {
|
|
47
|
+
"label": "Ignore Term",
|
|
48
|
+
"placeholder": "hydra-ignore",
|
|
49
|
+
"help": "Messages containing this term will be excluded from recall and capture (default: hydra-ignore)"
|
|
50
|
+
},
|
|
51
|
+
"debug": {
|
|
52
|
+
"label": "Debug Logging",
|
|
53
|
+
"help": "Enable verbose debug logs for API calls and responses",
|
|
54
|
+
"advanced": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"configSchema": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"additionalProperties": false,
|
|
60
|
+
"properties": {
|
|
61
|
+
"apiKey": { "type": "string" },
|
|
62
|
+
"tenantId": { "type": "string" },
|
|
63
|
+
"subTenantId": { "type": "string" },
|
|
64
|
+
"autoRecall": { "type": "boolean" },
|
|
65
|
+
"autoCapture": { "type": "boolean" },
|
|
66
|
+
"maxRecallResults": { "type": "number", "minimum": 1, "maximum": 50 },
|
|
67
|
+
"recallMode": { "type": "string", "enum": ["fast", "thinking"] },
|
|
68
|
+
"graphContext": { "type": "boolean" },
|
|
69
|
+
"ignoreTerm": { "type": "string" },
|
|
70
|
+
"debug": { "type": "boolean" }
|
|
71
|
+
},
|
|
72
|
+
"required": []
|
|
73
|
+
}
|
|
74
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydra_db/openclaw",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenClaw plugin for Hydra DB — the State-of-the-art agentic memory system with auto-capture, recall, and knowledge graph context for open-claw",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@sinclair/typebox": "0.34.47"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"check-types": "tsc --noEmit"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"openclaw": ">=2026.1.29"
|
|
18
|
+
},
|
|
19
|
+
"openclaw": {
|
|
20
|
+
"extensions": [
|
|
21
|
+
"./index.ts"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/session.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function toSourceId(sessionId: string): string {
|
|
2
|
+
return `sess_${sessionId}`
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function toHookSourceId(sessionId: string): string {
|
|
6
|
+
return `hook_${sessionId}`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function toToolSourceId(sessionId: string): string {
|
|
10
|
+
return `tool_${sessionId}`
|
|
11
|
+
}
|
package/tools/delete.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { HydraClient } from "../client.ts"
|
|
4
|
+
import type { HydraPluginConfig } from "../config.ts"
|
|
5
|
+
import { log } from "../log.ts"
|
|
6
|
+
|
|
7
|
+
export function registerDeleteTool(
|
|
8
|
+
api: OpenClawPluginApi,
|
|
9
|
+
client: HydraClient,
|
|
10
|
+
_cfg: HydraPluginConfig,
|
|
11
|
+
): void {
|
|
12
|
+
api.registerTool(
|
|
13
|
+
{
|
|
14
|
+
name: "hydra_delete_memory",
|
|
15
|
+
label: "Hydra Delete Memory",
|
|
16
|
+
description:
|
|
17
|
+
"Delete a specific memory from Hydra by its memory ID. Use this when the user explicitly asks you to forget something or remove a specific piece of stored information. Always confirm the memory ID before deleting.",
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
memory_id: Type.String({
|
|
20
|
+
description: "The unique ID of the memory to delete",
|
|
21
|
+
}),
|
|
22
|
+
}),
|
|
23
|
+
async execute(
|
|
24
|
+
_toolCallId: string,
|
|
25
|
+
params: { memory_id: string },
|
|
26
|
+
) {
|
|
27
|
+
log.debug(`delete tool: memory_id=${params.memory_id}`)
|
|
28
|
+
|
|
29
|
+
const res = await client.deleteMemory(params.memory_id)
|
|
30
|
+
|
|
31
|
+
if (res.user_memory_deleted) {
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "text" as const,
|
|
36
|
+
text: `Successfully deleted memory: ${params.memory_id}`,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text" as const,
|
|
46
|
+
text: `Memory ${params.memory_id} was not found or has already been deleted.`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{ name: "hydra_delete_memory" },
|
|
53
|
+
)
|
|
54
|
+
}
|
package/tools/get.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { HydraClient } from "../client.ts"
|
|
4
|
+
import type { HydraPluginConfig } from "../config.ts"
|
|
5
|
+
import { log } from "../log.ts"
|
|
6
|
+
|
|
7
|
+
export function registerGetTool(
|
|
8
|
+
api: OpenClawPluginApi,
|
|
9
|
+
client: HydraClient,
|
|
10
|
+
_cfg: HydraPluginConfig,
|
|
11
|
+
): void {
|
|
12
|
+
api.registerTool(
|
|
13
|
+
{
|
|
14
|
+
name: "hydra_get_content",
|
|
15
|
+
label: "Hydra Get Content",
|
|
16
|
+
description:
|
|
17
|
+
"Fetch the full content of a specific source from Hydra by its source ID. Use this to retrieve the complete text of a memory source when you need more details than what's shown in search results.",
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
source_id: Type.String({
|
|
20
|
+
description: "The unique source ID to fetch content for",
|
|
21
|
+
}),
|
|
22
|
+
}),
|
|
23
|
+
async execute(
|
|
24
|
+
_toolCallId: string,
|
|
25
|
+
params: { source_id: string },
|
|
26
|
+
) {
|
|
27
|
+
log.debug(`get tool: source_id=${params.source_id}`)
|
|
28
|
+
|
|
29
|
+
const res = await client.fetchContent(params.source_id)
|
|
30
|
+
|
|
31
|
+
if (!res.success || res.error) {
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "text" as const,
|
|
36
|
+
text: `Failed to fetch source ${params.source_id}: ${res.error ?? "unknown error"}`,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const content = res.content ?? res.content_base64 ?? "(no text content available)"
|
|
43
|
+
const preview = content.length > 3000 ? `${content.slice(0, 3000)}…\n\n[Content truncated, showing first 3000 characters]` : content
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text" as const,
|
|
49
|
+
text: `Source: ${params.source_id}\n\n${preview}`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{ name: "hydra_get_content" },
|
|
56
|
+
)
|
|
57
|
+
}
|
package/tools/list.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { HydraClient } from "../client.ts"
|
|
4
|
+
import type { HydraPluginConfig } from "../config.ts"
|
|
5
|
+
import { log } from "../log.ts"
|
|
6
|
+
|
|
7
|
+
export function registerListTool(
|
|
8
|
+
api: OpenClawPluginApi,
|
|
9
|
+
client: HydraClient,
|
|
10
|
+
_cfg: HydraPluginConfig,
|
|
11
|
+
): void {
|
|
12
|
+
api.registerTool(
|
|
13
|
+
{
|
|
14
|
+
name: "hydra_list_memories",
|
|
15
|
+
label: "Hydra List Memories",
|
|
16
|
+
description:
|
|
17
|
+
"List all user memories stored in Hydra. Returns memory IDs and content summaries. Use this when the user asks what you remember about them or wants to see their stored information.",
|
|
18
|
+
parameters: Type.Object({}),
|
|
19
|
+
async execute(_toolCallId: string, _params: Record<string, never>) {
|
|
20
|
+
log.debug("list tool: fetching all memories")
|
|
21
|
+
|
|
22
|
+
const res = await client.listMemories()
|
|
23
|
+
const memories = res.user_memories ?? []
|
|
24
|
+
|
|
25
|
+
if (memories.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text" as const,
|
|
30
|
+
text: "No memories stored yet.",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const lines = memories.map((m, i) => {
|
|
37
|
+
const preview =
|
|
38
|
+
m.memory_content.length > 100
|
|
39
|
+
? `${m.memory_content.slice(0, 100)}…`
|
|
40
|
+
: m.memory_content
|
|
41
|
+
return `${i + 1}. [ID: ${m.memory_id}]\n ${preview}`
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text" as const,
|
|
48
|
+
text: `Found ${memories.length} memories:\n\n${lines.join("\n\n")}`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{ name: "hydra_list_memories" },
|
|
55
|
+
)
|
|
56
|
+
}
|
package/tools/search.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { HydraClient } from "../client.ts"
|
|
4
|
+
import type { HydraPluginConfig } from "../config.ts"
|
|
5
|
+
import { buildRecalledContext } from "../context.ts"
|
|
6
|
+
import { log } from "../log.ts"
|
|
7
|
+
import type { VectorChunk } from "../types/hydra.ts"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function registerSearchTool(
|
|
11
|
+
api: OpenClawPluginApi,
|
|
12
|
+
client: HydraClient,
|
|
13
|
+
cfg: HydraPluginConfig,
|
|
14
|
+
): void {
|
|
15
|
+
api.registerTool(
|
|
16
|
+
{
|
|
17
|
+
name: "hydra_search",
|
|
18
|
+
label: "Hydra Search",
|
|
19
|
+
description:
|
|
20
|
+
"Search through Hydra DB memories. Returns relevant chunks with graph-enriched context.",
|
|
21
|
+
parameters: Type.Object({
|
|
22
|
+
query: Type.String({ description: "Search query" }),
|
|
23
|
+
limit: Type.Optional(
|
|
24
|
+
Type.Number({ description: "Max results (default: 10)" }),
|
|
25
|
+
),
|
|
26
|
+
}),
|
|
27
|
+
async execute(
|
|
28
|
+
_toolCallId: string,
|
|
29
|
+
params: { query: string; limit?: number },
|
|
30
|
+
) {
|
|
31
|
+
const limit = params.limit ?? cfg.maxRecallResults
|
|
32
|
+
log.debug(`search tool: "${params.query}" limit=${limit}`)
|
|
33
|
+
|
|
34
|
+
const res = await client.recall(params.query, {
|
|
35
|
+
maxResults: limit,
|
|
36
|
+
mode: cfg.recallMode,
|
|
37
|
+
graphContext: cfg.graphContext,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!res.chunks || res.chunks.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text" as const, text: "No relevant memories found." }],
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const contextStr = buildRecalledContext(res)
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text" as const,
|
|
52
|
+
text: `Found ${res.chunks.length} \n\n---\nFull context:\n${contextStr}`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
details: {
|
|
56
|
+
count: res.chunks.length,
|
|
57
|
+
hasGraphContext: !!res.graph_context,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{ name: "hydra_search" },
|
|
63
|
+
)
|
|
64
|
+
}
|
package/tools/store.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { HydraClient } from "../client.ts"
|
|
4
|
+
import type { HydraPluginConfig } from "../config.ts"
|
|
5
|
+
import { log } from "../log.ts"
|
|
6
|
+
import { extractAllTurns, filterIgnoredTurns } from "../messages.ts"
|
|
7
|
+
import { toToolSourceId } from "../session.ts"
|
|
8
|
+
import type { ConversationTurn } from "../types/hydra.ts"
|
|
9
|
+
|
|
10
|
+
const MAX_STORE_TURNS = 10
|
|
11
|
+
|
|
12
|
+
function removeInjectedBlocks(text: string): string {
|
|
13
|
+
return text.replace(/<hydra-context>[\s\S]*?<\/hydra-context>\s*/g, "").trim()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function registerStoreTool(
|
|
17
|
+
api: OpenClawPluginApi,
|
|
18
|
+
client: HydraClient,
|
|
19
|
+
cfg: HydraPluginConfig,
|
|
20
|
+
getSessionId: () => string | undefined,
|
|
21
|
+
getMessages: () => unknown[],
|
|
22
|
+
): void {
|
|
23
|
+
api.registerTool(
|
|
24
|
+
{
|
|
25
|
+
name: "hydra_store",
|
|
26
|
+
label: "Hydra Store",
|
|
27
|
+
description:
|
|
28
|
+
"Save the full conversation history to Hydra DB memory. Use this to persist facts, preferences, or decisions the user wants remembered. The complete chat history will be sent for context-rich storage.",
|
|
29
|
+
parameters: Type.Object({
|
|
30
|
+
text: Type.String({
|
|
31
|
+
description: "A brief summary or note about what is being saved",
|
|
32
|
+
}),
|
|
33
|
+
title: Type.Optional(
|
|
34
|
+
Type.String({
|
|
35
|
+
description: "Optional title for the memory entry",
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
async execute(
|
|
40
|
+
_toolCallId: string,
|
|
41
|
+
params: { text: string; title?: string },
|
|
42
|
+
) {
|
|
43
|
+
const sid = getSessionId()
|
|
44
|
+
const sourceId = sid ? toToolSourceId(sid) : undefined
|
|
45
|
+
const messages = getMessages()
|
|
46
|
+
|
|
47
|
+
log.debug(`[store] tool called — sid=${sid ?? "none"} msgs=${messages.length} text="${params.text.slice(0, 50)}"`)
|
|
48
|
+
|
|
49
|
+
const rawTurns = extractAllTurns(messages)
|
|
50
|
+
const filteredTurns = filterIgnoredTurns(rawTurns, cfg.ignoreTerm)
|
|
51
|
+
const recentTurns = filteredTurns.slice(-MAX_STORE_TURNS)
|
|
52
|
+
const turns: ConversationTurn[] = recentTurns.map((t) => ({
|
|
53
|
+
user: removeInjectedBlocks(t.user),
|
|
54
|
+
assistant: removeInjectedBlocks(t.assistant),
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
log.debug(`[store] extracted ${rawTurns.length} total turns, ${rawTurns.length - filteredTurns.length} ignored, using last ${turns.length} (MAX_STORE_TURNS=${MAX_STORE_TURNS})`)
|
|
58
|
+
|
|
59
|
+
if (turns.length > 0 && sourceId) {
|
|
60
|
+
const now = new Date()
|
|
61
|
+
const readableTime = now.toLocaleString("en-US", {
|
|
62
|
+
weekday: "short",
|
|
63
|
+
year: "numeric",
|
|
64
|
+
month: "short",
|
|
65
|
+
day: "numeric",
|
|
66
|
+
hour: "2-digit",
|
|
67
|
+
minute: "2-digit",
|
|
68
|
+
timeZoneName: "short",
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const annotatedTurns = turns.map((t, i) => ({
|
|
72
|
+
user: i === 0 ? `[Temporal details: ${readableTime}]\n\n${t.user}` : t.user,
|
|
73
|
+
assistant: t.assistant,
|
|
74
|
+
}))
|
|
75
|
+
|
|
76
|
+
log.debug(`[store] ingesting ${annotatedTurns.length} conversation turns -> ${sourceId}`)
|
|
77
|
+
|
|
78
|
+
await client.ingestConversation(annotatedTurns, sourceId, {
|
|
79
|
+
metadata: {
|
|
80
|
+
captured_at: now.toISOString(),
|
|
81
|
+
source: "openclaw_tool",
|
|
82
|
+
note: params.text,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text" as const,
|
|
90
|
+
text: `Saved ${annotatedTurns.length} conversation turns to Hydra (${sourceId}). Note: "${params.text.length > 80 ? `${params.text.slice(0, 80)}…` : params.text}"`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
log.debug("[store] no conversation turns found, falling back to text ingestion")
|
|
97
|
+
|
|
98
|
+
await client.ingestText(params.text, {
|
|
99
|
+
sourceId,
|
|
100
|
+
title: params.title ?? "Agent Memory",
|
|
101
|
+
infer: true,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text" as const,
|
|
108
|
+
text: `Saved to Hydra: "${params.text.length > 80 ? `${params.text.slice(0, 80)}…` : params.text}"`,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{ name: "hydra_store" },
|
|
115
|
+
)
|
|
116
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"rootDir": "."
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"*.ts",
|
|
17
|
+
"tools/*.ts",
|
|
18
|
+
"hooks/*.ts",
|
|
19
|
+
"commands/*.ts",
|
|
20
|
+
"types/*.ts"
|
|
21
|
+
],
|
|
22
|
+
"exclude": ["node_modules", "dist", "ignore"]
|
|
23
|
+
}
|