@digitalforgestudios/openclaw-sulcus 3.6.0 → 3.8.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/README.md +40 -0
- package/hooks.defaults.json +8 -4
- package/index.ts +244 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,46 @@ Then restart: `openclaw gateway restart`
|
|
|
51
51
|
| `memory_store` | Store with auto-detected type |
|
|
52
52
|
| `memory_forget` | Delete by ID |
|
|
53
53
|
|
|
54
|
+
## Hooks & Extended Tools
|
|
55
|
+
|
|
56
|
+
All hooks and tools are **enabled by default**. Override in your `openclaw.json` plugin config:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
"openclaw-sulcus": {
|
|
60
|
+
"enabled": true,
|
|
61
|
+
"config": {
|
|
62
|
+
"apiKey": "sk-YOUR_KEY_HERE",
|
|
63
|
+
"hooks": {
|
|
64
|
+
"before_prompt_build": { "action": "inject_awareness", "enabled": true },
|
|
65
|
+
"before_agent_start": { "action": "auto_recall", "enabled": true, "limit": 5, "minScore": 0.3 },
|
|
66
|
+
"agent_end": { "action": "none", "enabled": true }
|
|
67
|
+
},
|
|
68
|
+
"tools": {
|
|
69
|
+
"memory_recall": { "enabled": true },
|
|
70
|
+
"memory_store": { "enabled": true },
|
|
71
|
+
"memory_status": { "enabled": true },
|
|
72
|
+
"consolidate": { "enabled": true },
|
|
73
|
+
"export_markdown": { "enabled": true },
|
|
74
|
+
"import_markdown": { "enabled": true },
|
|
75
|
+
"evaluate_triggers": { "enabled": true }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Hook | Default | Description |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `before_prompt_build` | ON | Inject memory awareness context into prompts |
|
|
84
|
+
| `before_agent_start` | ON | Auto-recall relevant memories each turn |
|
|
85
|
+
| `agent_end` | ON | Post-turn processing |
|
|
86
|
+
|
|
87
|
+
| Extended Tool | Default | Description |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `consolidate` | ON | Cluster and merge similar memories |
|
|
90
|
+
| `export_markdown` | ON | Export memories as markdown |
|
|
91
|
+
| `import_markdown` | ON | Import memories from markdown |
|
|
92
|
+
| `evaluate_triggers` | ON | Run reactive trigger evaluations |
|
|
93
|
+
|
|
54
94
|
## Features
|
|
55
95
|
|
|
56
96
|
- **Auto-recall** — relevant memories injected before each agent turn
|
package/hooks.defaults.json
CHANGED
|
@@ -18,12 +18,16 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"tools": {
|
|
21
|
-
"memory_recall": { "enabled":
|
|
22
|
-
"memory_store": { "enabled":
|
|
23
|
-
"memory_status": { "enabled":
|
|
21
|
+
"memory_recall": { "enabled": true },
|
|
22
|
+
"memory_store": { "enabled": true },
|
|
23
|
+
"memory_status": { "enabled": true },
|
|
24
24
|
"consolidate": { "enabled": false },
|
|
25
25
|
"export_markdown": { "enabled": false },
|
|
26
26
|
"import_markdown": { "enabled": false },
|
|
27
|
-
"evaluate_triggers": { "enabled": false }
|
|
27
|
+
"evaluate_triggers": { "enabled": false },
|
|
28
|
+
"siu_label": { "enabled": false },
|
|
29
|
+
"siu_status": { "enabled": false },
|
|
30
|
+
"siu_retrain": { "enabled": false },
|
|
31
|
+
"trigger_feedback": { "enabled": false }
|
|
28
32
|
}
|
|
29
33
|
}
|
package/index.ts
CHANGED
|
@@ -98,12 +98,14 @@ const hookHandlers: Record<string, HookHandler> = {
|
|
|
98
98
|
auto_recall: async (event: any, config: HookConfig, ctx: HookHandlerCtx) => {
|
|
99
99
|
const { sulcusMem, namespace, logger } = ctx;
|
|
100
100
|
if (!sulcusMem) return;
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const agentLabel = event?.agentId ?? "(unknown)";
|
|
102
|
+
logger.info(`sulcus: before_agent_start hook triggered for agent ${agentLabel}`);
|
|
103
|
+
const prompt = typeof event?.prompt === "string" ? event.prompt : "";
|
|
104
|
+
if (!prompt) return;
|
|
103
105
|
try {
|
|
104
106
|
const limit = config.limit ?? 5;
|
|
105
|
-
logger.debug(`sulcus: searching context for prompt: ${
|
|
106
|
-
const res = await sulcusMem.search_memory(
|
|
107
|
+
logger.debug(`sulcus: searching context for prompt: ${prompt.substring(0, 50)}...`);
|
|
108
|
+
const res = await sulcusMem.search_memory(prompt, limit);
|
|
107
109
|
const results = res?.results ?? [];
|
|
108
110
|
if (!results || results.length === 0) {
|
|
109
111
|
return { prependSystemContext: FALLBACK_AWARENESS };
|
|
@@ -129,6 +131,74 @@ const hookHandlers: Record<string, HookHandler> = {
|
|
|
129
131
|
none: async (event: any, _config: HookConfig, ctx: HookHandlerCtx) => {
|
|
130
132
|
ctx.logger.debug(`sulcus: hook fired (action=none) for agent ${event.agentId ?? "(unknown)"} (no-op)`);
|
|
131
133
|
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* sivu_auto_capture — SIU v2 quality-gated auto-capture on agent_end.
|
|
137
|
+
*
|
|
138
|
+
* When fired after each turn, extracts the user message from the event,
|
|
139
|
+
* runs it through SIVU (store/reject gate) and SICU (type classifier),
|
|
140
|
+
* and stores the memory only if SIVU approves. Falls back to basic
|
|
141
|
+
* junk-filtering + episodic capture if SIU v2 endpoint is unavailable.
|
|
142
|
+
*
|
|
143
|
+
* Config options:
|
|
144
|
+
* min_store_confidence: number (default 0.5) — minimum SIVU confidence to store
|
|
145
|
+
* fallback_on_error: boolean (default true) — store as episodic if SIU unavailable
|
|
146
|
+
*/
|
|
147
|
+
sivu_auto_capture: async (event: any, config: HookConfig, ctx: HookHandlerCtx) => {
|
|
148
|
+
const { sulcusMem, logger } = ctx;
|
|
149
|
+
if (!sulcusMem) return;
|
|
150
|
+
|
|
151
|
+
// Extract user message from the event
|
|
152
|
+
const userMessage = event?.userMessage ?? event?.prompt ?? event?.text ?? "";
|
|
153
|
+
if (!userMessage || typeof userMessage !== "string") {
|
|
154
|
+
logger.debug("sulcus: sivu_auto_capture — no user message in event, skipping");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Pre-filter obvious junk before hitting the API
|
|
159
|
+
if (isJunkMemory(userMessage)) {
|
|
160
|
+
logger.debug(`sulcus: sivu_auto_capture — pre-filtered junk: "${userMessage.substring(0, 50)}..."`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const minConfidence = config.min_store_confidence ?? 0.5;
|
|
165
|
+
const fallbackOnError = config.fallback_on_error !== false; // default true
|
|
166
|
+
|
|
167
|
+
// Try SIU v2 endpoint for quality-gated classification
|
|
168
|
+
if (sulcusMem instanceof SulcusCloudClient) {
|
|
169
|
+
try {
|
|
170
|
+
const siuResult = await (sulcusMem as SulcusCloudClient).request("POST", "/api/v2/siu/label", {
|
|
171
|
+
text: userMessage,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const shouldStore = siuResult?.store === true && (siuResult?.store_confidence ?? 0) >= minConfidence;
|
|
175
|
+
const memoryType = siuResult?.memory_type ?? "episodic";
|
|
176
|
+
const modelVersion = siuResult?.model_version ?? "unknown";
|
|
177
|
+
|
|
178
|
+
if (!shouldStore) {
|
|
179
|
+
logger.info(`sulcus: sivu_auto_capture — SIVU rejected (confidence: ${siuResult?.store_confidence?.toFixed(3) ?? "?"}, model: ${modelVersion}): "${userMessage.substring(0, 60)}..."`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// SIVU approved — store with SICU-predicted type
|
|
184
|
+
const res = await sulcusMem.add_memory(userMessage, memoryType);
|
|
185
|
+
logger.info(`sulcus: sivu_auto_capture — stored [${memoryType}] (id: ${res?.id ?? "?"}, sivu_conf: ${siuResult?.store_confidence?.toFixed(3)}, sicu_conf: ${siuResult?.type_confidence?.toFixed(3)}, model: ${modelVersion}): "${userMessage.substring(0, 60)}..."`);
|
|
186
|
+
return;
|
|
187
|
+
} catch (e: any) {
|
|
188
|
+
logger.warn(`sulcus: sivu_auto_capture — SIU v2 endpoint error: ${e.message}`);
|
|
189
|
+
if (!fallbackOnError) return;
|
|
190
|
+
// Fall through to basic capture
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Fallback: store as episodic (no SIU gating available)
|
|
195
|
+
try {
|
|
196
|
+
const res = await sulcusMem.add_memory(userMessage, "episodic");
|
|
197
|
+
logger.info(`sulcus: sivu_auto_capture — fallback stored [episodic] (id: ${res?.id ?? "?"}): "${userMessage.substring(0, 60)}..."`);
|
|
198
|
+
} catch (e: any) {
|
|
199
|
+
logger.warn(`sulcus: sivu_auto_capture — fallback store failed: ${e.message}`);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
132
202
|
};
|
|
133
203
|
|
|
134
204
|
// ─── CLOUD HTTP CLIENT ───────────────────────────────────────────────────────
|
|
@@ -147,7 +217,7 @@ class SulcusCloudClient {
|
|
|
147
217
|
}
|
|
148
218
|
|
|
149
219
|
/** Low-level HTTP helper. Returns parsed JSON response body. */
|
|
150
|
-
|
|
220
|
+
request(method: string, path: string, body?: any): Promise<any> {
|
|
151
221
|
return new Promise((resolve, reject) => {
|
|
152
222
|
let parsedUrl: URL;
|
|
153
223
|
try {
|
|
@@ -519,6 +589,8 @@ interface ToolDeps {
|
|
|
519
589
|
wasmDir: string;
|
|
520
590
|
logger: any;
|
|
521
591
|
isAvailable: boolean;
|
|
592
|
+
/** HTTP request helper for SIU v2 endpoints — null when cloud backend is not configured. */
|
|
593
|
+
siuRequest: ((method: string, path: string, body?: any) => Promise<any>) | null;
|
|
522
594
|
}
|
|
523
595
|
|
|
524
596
|
const toolDefinitions: Record<string, ToolDefinition> = {
|
|
@@ -729,6 +801,155 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|
|
729
801
|
};
|
|
730
802
|
},
|
|
731
803
|
},
|
|
804
|
+
|
|
805
|
+
// ── SIU v2 Tools ───────────────────────────────────────────────────────────
|
|
806
|
+
// These tools call the SIU v2 server endpoints for text classification.
|
|
807
|
+
// Requires cloud backend (serverUrl + apiKey). Uses /api/v2/siu/* endpoints.
|
|
808
|
+
|
|
809
|
+
siu_label: {
|
|
810
|
+
schema: {
|
|
811
|
+
name: "siu_label",
|
|
812
|
+
label: "SIU Label",
|
|
813
|
+
description: "Classify text using SIU v2 — returns SIVU store/reject decision and SICU memory type classification with confidence scores.",
|
|
814
|
+
parameters: Type.Object({
|
|
815
|
+
text: Type.String({ description: "Text to classify." }),
|
|
816
|
+
classify_only: Type.Optional(Type.Boolean({ description: "Skip SIVU quality gate, only run SICU type classification." })),
|
|
817
|
+
}),
|
|
818
|
+
},
|
|
819
|
+
options: { name: "siu_label" },
|
|
820
|
+
makeExecute: ({ backendMode, siuRequest, logger }) =>
|
|
821
|
+
async (_id: string, params: any) => {
|
|
822
|
+
if (!siuRequest) {
|
|
823
|
+
return {
|
|
824
|
+
content: [{ type: "text", text: "SIU label requires cloud backend (serverUrl + apiKey)." }],
|
|
825
|
+
details: { error: "cloud_required" },
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
const res = await siuRequest("POST", "/api/v2/siu/label", {
|
|
830
|
+
text: params.text,
|
|
831
|
+
classify_only: params.classify_only ?? false,
|
|
832
|
+
});
|
|
833
|
+
return {
|
|
834
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
835
|
+
details: res,
|
|
836
|
+
};
|
|
837
|
+
} catch (e: any) {
|
|
838
|
+
logger.warn(`sulcus: siu_label failed: ${e.message}`);
|
|
839
|
+
return {
|
|
840
|
+
content: [{ type: "text", text: `SIU label failed: ${e.message}` }],
|
|
841
|
+
details: { error: e.message },
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
|
|
847
|
+
siu_status: {
|
|
848
|
+
schema: {
|
|
849
|
+
name: "siu_status",
|
|
850
|
+
label: "SIU Status",
|
|
851
|
+
description: "Check SIU v2 model availability, deployed versions, and training signal statistics.",
|
|
852
|
+
parameters: Type.Object({}),
|
|
853
|
+
},
|
|
854
|
+
options: { name: "siu_status" },
|
|
855
|
+
makeExecute: ({ siuRequest, logger }) =>
|
|
856
|
+
async (_id: string, _params: any) => {
|
|
857
|
+
if (!siuRequest) {
|
|
858
|
+
return {
|
|
859
|
+
content: [{ type: "text", text: "SIU status requires cloud backend (serverUrl + apiKey)." }],
|
|
860
|
+
details: { error: "cloud_required" },
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
const res = await siuRequest("GET", "/api/v2/siu/status");
|
|
865
|
+
return {
|
|
866
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
867
|
+
details: res,
|
|
868
|
+
};
|
|
869
|
+
} catch (e: any) {
|
|
870
|
+
logger.warn(`sulcus: siu_status failed: ${e.message}`);
|
|
871
|
+
return {
|
|
872
|
+
content: [{ type: "text", text: `SIU status failed: ${e.message}` }],
|
|
873
|
+
details: { error: e.message },
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
|
|
879
|
+
siu_retrain: {
|
|
880
|
+
schema: {
|
|
881
|
+
name: "siu_retrain",
|
|
882
|
+
label: "SIU Retrain",
|
|
883
|
+
description: "Trigger an async retrain of SIU v2 models using accumulated training signals. Returns job status.",
|
|
884
|
+
parameters: Type.Object({}),
|
|
885
|
+
},
|
|
886
|
+
options: { name: "siu_retrain" },
|
|
887
|
+
makeExecute: ({ siuRequest, logger }) =>
|
|
888
|
+
async (_id: string, _params: any) => {
|
|
889
|
+
if (!siuRequest) {
|
|
890
|
+
return {
|
|
891
|
+
content: [{ type: "text", text: "SIU retrain requires cloud backend (serverUrl + apiKey)." }],
|
|
892
|
+
details: { error: "cloud_required" },
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
const res = await siuRequest("POST", "/api/v2/siu/retrain");
|
|
897
|
+
return {
|
|
898
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
899
|
+
details: res,
|
|
900
|
+
};
|
|
901
|
+
} catch (e: any) {
|
|
902
|
+
logger.warn(`sulcus: siu_retrain failed: ${e.message}`);
|
|
903
|
+
return {
|
|
904
|
+
content: [{ type: "text", text: `SIU retrain failed: ${e.message}` }],
|
|
905
|
+
details: { error: e.message },
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
trigger_feedback: {
|
|
911
|
+
schema: {
|
|
912
|
+
name: "trigger_feedback",
|
|
913
|
+
label: "Trigger Feedback",
|
|
914
|
+
description:
|
|
915
|
+
"Record feedback on a trigger fire (for SITU training). Use to report false positives (fired but shouldn't have), false negatives (should have fired but didn't), or confirm correct fires.",
|
|
916
|
+
parameters: Type.Object({
|
|
917
|
+
feedback_type: Type.String({
|
|
918
|
+
description:
|
|
919
|
+
'One of: "false_positive" (fired wrongly), "false_negative" (missed fire), "correct" (good fire), "wrong_action" (fired but wrong action)',
|
|
920
|
+
}),
|
|
921
|
+
trigger_id: Type.Optional(Type.String({ description: "UUID of the trigger rule" })),
|
|
922
|
+
trigger_log_id: Type.Optional(Type.String({ description: "UUID of the trigger fire log entry" })),
|
|
923
|
+
event_type: Type.Optional(Type.String({ description: "Event type: memory_created, heat_threshold, recall, etc." })),
|
|
924
|
+
memory_id: Type.Optional(Type.String({ description: "UUID of the memory involved" })),
|
|
925
|
+
expected_action: Type.Optional(Type.String({ description: "What should have happened: fire, no_fire, different_action" })),
|
|
926
|
+
notes: Type.Optional(Type.String({ description: "Free-text explanation of the feedback" })),
|
|
927
|
+
}),
|
|
928
|
+
},
|
|
929
|
+
options: { name: "trigger_feedback" },
|
|
930
|
+
makeExecute: ({ siuRequest, logger }) =>
|
|
931
|
+
async (_id: string, params: any) => {
|
|
932
|
+
if (!siuRequest) {
|
|
933
|
+
return {
|
|
934
|
+
content: [{ type: "text", text: "Trigger feedback requires cloud backend (serverUrl + apiKey)." }],
|
|
935
|
+
details: { error: "cloud_required" },
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
const res = await siuRequest("POST", "/api/v1/triggers/feedback", params);
|
|
940
|
+
return {
|
|
941
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
942
|
+
details: res,
|
|
943
|
+
};
|
|
944
|
+
} catch (e: any) {
|
|
945
|
+
logger.warn(`sulcus: trigger_feedback failed: ${e.message}`);
|
|
946
|
+
return {
|
|
947
|
+
content: [{ type: "text", text: `Trigger feedback failed: ${e.message}` }],
|
|
948
|
+
details: { error: e.message },
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
},
|
|
732
953
|
};
|
|
733
954
|
|
|
734
955
|
// ─── PLUGIN ──────────────────────────────────────────────────────────────────
|
|
@@ -832,6 +1053,12 @@ const sulcusPlugin = {
|
|
|
832
1053
|
api.logger.warn(`sulcus: ✗ unavailable — ${hints.join("; ") || "unknown reason"}. Configure serverUrl+apiKey for cloud, or install native dylibs for local.`);
|
|
833
1054
|
}
|
|
834
1055
|
|
|
1056
|
+
// ── SIU v2 request helper (bound to cloud client if available) ──
|
|
1057
|
+
// SIU endpoints live on the same server as the Sulcus API.
|
|
1058
|
+
const siuRequestFn = (backendMode === "cloud" && sulcusMem instanceof SulcusCloudClient)
|
|
1059
|
+
? (method: string, path: string, body?: any) => (sulcusMem as SulcusCloudClient).request(method, path, body)
|
|
1060
|
+
: null;
|
|
1061
|
+
|
|
835
1062
|
// ── Shared deps for tool executors ──
|
|
836
1063
|
const toolDeps: ToolDeps = {
|
|
837
1064
|
sulcusMem,
|
|
@@ -843,6 +1070,7 @@ const sulcusPlugin = {
|
|
|
843
1070
|
wasmDir,
|
|
844
1071
|
logger: api.logger,
|
|
845
1072
|
isAvailable,
|
|
1073
|
+
siuRequest: siuRequestFn,
|
|
846
1074
|
};
|
|
847
1075
|
|
|
848
1076
|
// ── Shared context for hook handlers ──
|
|
@@ -858,11 +1086,21 @@ const sulcusPlugin = {
|
|
|
858
1086
|
};
|
|
859
1087
|
|
|
860
1088
|
// ── Config-driven hook registration ──
|
|
1089
|
+
// Each handler is wrapped in a defensive try-catch to prevent plugin errors
|
|
1090
|
+
// from crashing the host agent's startup pipeline (OpenClaw bug workaround:
|
|
1091
|
+
// normalizeResolvedModel() doesn't guard params.model being undefined).
|
|
861
1092
|
for (const [hookName, hookConfig] of Object.entries(hooksConfig.hooks)) {
|
|
862
1093
|
if (!hookConfig.enabled) continue;
|
|
863
1094
|
const handler = hookHandlers[hookConfig.action];
|
|
864
1095
|
if (handler) {
|
|
865
|
-
api.on(hookName, (event: any) =>
|
|
1096
|
+
api.on(hookName, async (event: any) => {
|
|
1097
|
+
try {
|
|
1098
|
+
return await handler(event, hookConfig, handlerCtx);
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
api.logger.warn(`sulcus: hook "${hookName}" (action=${hookConfig.action}) threw: ${err} — returning empty result`);
|
|
1101
|
+
return undefined;
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
866
1104
|
} else {
|
|
867
1105
|
api.logger.warn(`sulcus: unknown hook action "${hookConfig.action}" for hook "${hookName}"`);
|
|
868
1106
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalforgestudios/openclaw-sulcus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "Sulcus — reactive, thermodynamic memory plugin for OpenClaw. Opt-in persistent memory with heat-based decay, semantic search, and cross-agent sync. Auto-recall and auto-capture disabled by default.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|