@nowledge/openclaw-nowledge-mem 0.6.15 → 0.7.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/CHANGELOG.md +33 -1
- package/CLAUDE.md +59 -5
- package/README.md +5 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/ce-state.js +8 -0
- package/src/client.js +50 -87
- package/src/context-engine.js +406 -0
- package/src/hooks/behavioral.js +8 -2
- package/src/hooks/capture.js +166 -105
- package/src/hooks/recall.js +38 -26
- package/src/index.js +22 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nowledge Mem Context Engine for OpenClaw.
|
|
3
|
+
*
|
|
4
|
+
* Registers alongside the memory slot (kind: "memory"). When users activate
|
|
5
|
+
* this CE via `plugins.slots.contextEngine: "nowledge-mem"`, it replaces the
|
|
6
|
+
* hook-based approach with the richer CE lifecycle:
|
|
7
|
+
*
|
|
8
|
+
* assemble() — behavioral guidance + recalled memories via systemPromptAddition
|
|
9
|
+
* afterTurn() — continuous thread capture + triage/distillation (every turn)
|
|
10
|
+
* compact() — memory-aware compaction (key decisions preserved in summaries)
|
|
11
|
+
* prepareSubagentSpawn() — child sessions inherit relevant memory context
|
|
12
|
+
* bootstrap() — pre-warm Working Memory for first assemble
|
|
13
|
+
*
|
|
14
|
+
* When not activated, the existing hooks (behavioral, recall, capture) work
|
|
15
|
+
* as before — full backward compatibility.
|
|
16
|
+
*
|
|
17
|
+
* Design:
|
|
18
|
+
* - ownsCompaction: false — we enhance compaction instructions, not the algorithm
|
|
19
|
+
* - Messages pass through unchanged — we only add systemPromptAddition
|
|
20
|
+
* - State is per-session (keyed by sessionKey) with bounded cache size
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { ceState } from "./ce-state.js";
|
|
24
|
+
import { BASE_GUIDANCE, SESSION_CONTEXT_GUIDANCE } from "./hooks/behavioral.js";
|
|
25
|
+
import { appendOrCreateThread, triageAndDistill } from "./hooks/capture.js";
|
|
26
|
+
import {
|
|
27
|
+
MAX_QUERY_LENGTH,
|
|
28
|
+
SHORT_QUERY_THRESHOLD,
|
|
29
|
+
buildRecalledKnowledgeBlock,
|
|
30
|
+
escapeForPrompt,
|
|
31
|
+
} from "./hooks/recall.js";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Per-session state
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Session context cache: sessionKey -> { wm, memories, lastWmFetch } */
|
|
38
|
+
const _sessions = new Map();
|
|
39
|
+
|
|
40
|
+
/** Subagent memory injection: childSessionKey -> { wm, memories } */
|
|
41
|
+
const _childContext = new Map();
|
|
42
|
+
|
|
43
|
+
const MAX_SESSION_ENTRIES = 100;
|
|
44
|
+
const MAX_CHILD_ENTRIES = 50;
|
|
45
|
+
const WM_CACHE_TTL_MS = 60_000; // 1 min — re-fetch Working Memory after this
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Query building (adapted for CE's assemble params)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract plain text from an AgentMessage content field.
|
|
53
|
+
* AgentMessage content can be string or structured blocks.
|
|
54
|
+
*/
|
|
55
|
+
function extractText(content) {
|
|
56
|
+
if (typeof content === "string") return content.trim();
|
|
57
|
+
if (!Array.isArray(content)) return "";
|
|
58
|
+
const parts = [];
|
|
59
|
+
for (const block of content) {
|
|
60
|
+
if (!block || typeof block !== "object") continue;
|
|
61
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
62
|
+
const text = block.text.trim();
|
|
63
|
+
if (text) parts.push(text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return parts.join("\n").trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a search query from the assemble() params.
|
|
71
|
+
*
|
|
72
|
+
* Uses `prompt` (the user's current input) as the primary signal.
|
|
73
|
+
* Falls back to the last user message in `messages` if prompt is absent.
|
|
74
|
+
* For short queries, augments with recent conversation context.
|
|
75
|
+
*/
|
|
76
|
+
function buildAssembleSearchQuery(prompt, messages) {
|
|
77
|
+
// Prefer the prompt parameter (the raw user input for this turn)
|
|
78
|
+
let queryText = typeof prompt === "string" ? prompt.trim() : "";
|
|
79
|
+
|
|
80
|
+
// Fallback: extract last user message from messages array
|
|
81
|
+
if (!queryText && Array.isArray(messages)) {
|
|
82
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
83
|
+
const msg = messages[i];
|
|
84
|
+
if (msg?.role === "user") {
|
|
85
|
+
queryText = extractText(msg.content);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!queryText || queryText.length < 3) return "";
|
|
92
|
+
|
|
93
|
+
// Substantial query — use alone
|
|
94
|
+
if (queryText.length >= SHORT_QUERY_THRESHOLD) {
|
|
95
|
+
return queryText.slice(0, MAX_QUERY_LENGTH);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Short query — augment with recent conversation context for topic grounding
|
|
99
|
+
if (Array.isArray(messages) && messages.length > 1) {
|
|
100
|
+
const contextParts = [];
|
|
101
|
+
const start = Math.max(0, messages.length - 4); // last 3 messages before current
|
|
102
|
+
for (let i = start; i < messages.length - 1; i++) {
|
|
103
|
+
const msg = messages[i];
|
|
104
|
+
if (!msg?.role || (msg.role !== "user" && msg.role !== "assistant"))
|
|
105
|
+
continue;
|
|
106
|
+
const text = extractText(msg.content);
|
|
107
|
+
if (!text) continue;
|
|
108
|
+
contextParts.push(text.length > 150 ? `${text.slice(0, 150)}…` : text);
|
|
109
|
+
}
|
|
110
|
+
if (contextParts.length > 0) {
|
|
111
|
+
return `${queryText}\n\n${contextParts.join("\n")}`.slice(
|
|
112
|
+
0,
|
|
113
|
+
MAX_QUERY_LENGTH,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return queryText.slice(0, MAX_QUERY_LENGTH);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Cache management
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
function evictIfNeeded(map, max) {
|
|
126
|
+
if (map.size <= max) return;
|
|
127
|
+
// Evict oldest entries (first inserted)
|
|
128
|
+
const excess = map.size - max;
|
|
129
|
+
let count = 0;
|
|
130
|
+
for (const key of map.keys()) {
|
|
131
|
+
if (count >= excess) break;
|
|
132
|
+
map.delete(key);
|
|
133
|
+
count++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getSessionState(key) {
|
|
138
|
+
let state = _sessions.get(key);
|
|
139
|
+
if (!state) {
|
|
140
|
+
state = { wm: null, memories: [], lastWmFetch: 0 };
|
|
141
|
+
_sessions.set(key, state);
|
|
142
|
+
evictIfNeeded(_sessions, MAX_SESSION_ENTRIES);
|
|
143
|
+
}
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Context Engine factory
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create the CE factory function for api.registerContextEngine().
|
|
153
|
+
*
|
|
154
|
+
* @param {import('./client.js').NowledgeMemClient} client
|
|
155
|
+
* @param {object} cfg Parsed plugin config
|
|
156
|
+
* @param {object} logger OpenClaw logger
|
|
157
|
+
* @returns {() => object} Factory that creates the engine instance
|
|
158
|
+
*/
|
|
159
|
+
export function createNowledgeMemContextEngineFactory(client, cfg, logger) {
|
|
160
|
+
return () => {
|
|
161
|
+
ceState.active = true;
|
|
162
|
+
logger.info("nowledge-mem: context engine activated");
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
info: {
|
|
166
|
+
id: "nowledge-mem",
|
|
167
|
+
name: "Nowledge Mem",
|
|
168
|
+
version: "0.7.0",
|
|
169
|
+
ownsCompaction: false,
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// ------------------------------------------------------------------
|
|
173
|
+
// bootstrap — pre-warm Working Memory for first assemble()
|
|
174
|
+
// ------------------------------------------------------------------
|
|
175
|
+
async bootstrap({ sessionId, sessionKey }) {
|
|
176
|
+
const key = sessionKey || sessionId;
|
|
177
|
+
try {
|
|
178
|
+
const wm = await client.readWorkingMemory();
|
|
179
|
+
const state = getSessionState(key);
|
|
180
|
+
state.wm = wm;
|
|
181
|
+
state.lastWmFetch = Date.now();
|
|
182
|
+
logger.debug?.(`ce: bootstrap — WM loaded for ${key}`);
|
|
183
|
+
return { bootstrapped: true };
|
|
184
|
+
} catch (err) {
|
|
185
|
+
logger.warn(`ce: bootstrap — WM read failed: ${err}`);
|
|
186
|
+
return { bootstrapped: false, reason: String(err) };
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// ------------------------------------------------------------------
|
|
191
|
+
// ingest / ingestBatch — lightweight; real capture is in afterTurn
|
|
192
|
+
// ------------------------------------------------------------------
|
|
193
|
+
async ingest({ isHeartbeat }) {
|
|
194
|
+
return { ingested: !isHeartbeat };
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
async ingestBatch({ messages, isHeartbeat }) {
|
|
198
|
+
return { ingestedCount: isHeartbeat ? 0 : (messages?.length ?? 0) };
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
// ------------------------------------------------------------------
|
|
202
|
+
// assemble — behavioral guidance + recalled memories in systemPromptAddition
|
|
203
|
+
//
|
|
204
|
+
// Messages pass through unchanged. We never own message selection —
|
|
205
|
+
// the runtime's sanitize → validate → limit pipeline handles that.
|
|
206
|
+
// ------------------------------------------------------------------
|
|
207
|
+
async assemble({ sessionId, sessionKey, messages, prompt }) {
|
|
208
|
+
const key = sessionKey || sessionId;
|
|
209
|
+
const state = getSessionState(key);
|
|
210
|
+
const sections = [];
|
|
211
|
+
|
|
212
|
+
// 1. Behavioral guidance (always — ~50 tokens)
|
|
213
|
+
sections.push(
|
|
214
|
+
cfg.sessionContext ? SESSION_CONTEXT_GUIDANCE : BASE_GUIDANCE,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// 2. Working Memory (refresh if stale)
|
|
218
|
+
try {
|
|
219
|
+
if (!state.wm || Date.now() - state.lastWmFetch > WM_CACHE_TTL_MS) {
|
|
220
|
+
state.wm = await client.readWorkingMemory();
|
|
221
|
+
state.lastWmFetch = Date.now();
|
|
222
|
+
}
|
|
223
|
+
if (state.wm?.available) {
|
|
224
|
+
sections.push(
|
|
225
|
+
`<working-memory>\n${escapeForPrompt(state.wm.content)}\n</working-memory>`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
logger.debug?.(`ce: assemble — WM read failed: ${err}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 3. Recalled memories (when sessionContext enabled)
|
|
233
|
+
if (cfg.sessionContext) {
|
|
234
|
+
const query = buildAssembleSearchQuery(prompt, messages);
|
|
235
|
+
if (query) {
|
|
236
|
+
try {
|
|
237
|
+
const results = await client.searchRich(
|
|
238
|
+
query,
|
|
239
|
+
cfg.maxContextResults,
|
|
240
|
+
);
|
|
241
|
+
const minScore = (cfg.recallMinScore ?? 0) / 100;
|
|
242
|
+
const filtered =
|
|
243
|
+
minScore > 0
|
|
244
|
+
? results.filter((r) => (r.score ?? 0) >= minScore)
|
|
245
|
+
: results;
|
|
246
|
+
if (filtered.length > 0) {
|
|
247
|
+
state.memories = filtered; // cache for compact()
|
|
248
|
+
sections.push(buildRecalledKnowledgeBlock(filtered));
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
logger.debug?.(`ce: assemble — recall failed: ${err}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 4. Subagent memory injection (one-time for child sessions)
|
|
257
|
+
const childCtx = _childContext.get(key);
|
|
258
|
+
if (childCtx) {
|
|
259
|
+
if (childCtx.wm?.available) {
|
|
260
|
+
sections.push(
|
|
261
|
+
`<parent-context>\n${escapeForPrompt(childCtx.wm.content)}\n</parent-context>`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (childCtx.memories?.length > 0) {
|
|
265
|
+
sections.push(
|
|
266
|
+
buildRecalledKnowledgeBlock(
|
|
267
|
+
childCtx.memories,
|
|
268
|
+
"parent-knowledge",
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
_childContext.delete(key);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_sessions.set(key, state);
|
|
276
|
+
|
|
277
|
+
const systemPromptAddition =
|
|
278
|
+
sections.length > 1
|
|
279
|
+
? `<nowledge-mem>\n${sections.join("\n\n")}\n</nowledge-mem>`
|
|
280
|
+
: sections.length === 1
|
|
281
|
+
? sections[0] // just the behavioral guidance, skip wrapper
|
|
282
|
+
: undefined;
|
|
283
|
+
|
|
284
|
+
return { messages, estimatedTokens: 0, systemPromptAddition };
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
// ------------------------------------------------------------------
|
|
288
|
+
// compact — enhance compaction with memory context, then delegate
|
|
289
|
+
//
|
|
290
|
+
// When key decisions/learnings from this conversation have been saved
|
|
291
|
+
// to the knowledge graph, we tell the compactor so it can reference
|
|
292
|
+
// them concisely rather than losing them in summarization.
|
|
293
|
+
// ------------------------------------------------------------------
|
|
294
|
+
async compact(params) {
|
|
295
|
+
const key = params.sessionKey || params.sessionId;
|
|
296
|
+
const state = _sessions.get(key);
|
|
297
|
+
|
|
298
|
+
let enhanced = params.customInstructions || "";
|
|
299
|
+
if (state?.memories?.length > 0) {
|
|
300
|
+
const memoryHints = state.memories
|
|
301
|
+
.slice(0, 8)
|
|
302
|
+
.map((m) => {
|
|
303
|
+
const title = m.title || "(untitled)";
|
|
304
|
+
const snippet = (m.content || "").slice(0, 120);
|
|
305
|
+
return `- ${title}: ${snippet}`;
|
|
306
|
+
})
|
|
307
|
+
.join("\n");
|
|
308
|
+
enhanced += `\n\nThe user has the following knowledge saved in their personal knowledge graph (Nowledge Mem). When compacting older messages, reference these items by name rather than repeating them in full — the complete version is preserved in the graph:\n${memoryHints}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const { delegateCompactionToRuntime } = await import(
|
|
313
|
+
"openclaw/plugin-sdk/core"
|
|
314
|
+
);
|
|
315
|
+
return delegateCompactionToRuntime({
|
|
316
|
+
...params,
|
|
317
|
+
customInstructions: enhanced.trim() || undefined,
|
|
318
|
+
});
|
|
319
|
+
} catch (err) {
|
|
320
|
+
logger.warn(`ce: compact delegation failed: ${err}`);
|
|
321
|
+
// Tell the runtime we couldn't compact — it will handle overflow recovery
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
compacted: false,
|
|
325
|
+
reason: "delegation-unavailable",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
// ------------------------------------------------------------------
|
|
331
|
+
// afterTurn — continuous thread capture + triage/distillation
|
|
332
|
+
//
|
|
333
|
+
// Fires after every turn (more granular than agent_end hook).
|
|
334
|
+
// The dedup layer in appendOrCreateThread ensures no duplicates.
|
|
335
|
+
// ------------------------------------------------------------------
|
|
336
|
+
async afterTurn({
|
|
337
|
+
sessionId,
|
|
338
|
+
sessionKey,
|
|
339
|
+
sessionFile,
|
|
340
|
+
messages,
|
|
341
|
+
isHeartbeat,
|
|
342
|
+
}) {
|
|
343
|
+
if (isHeartbeat) return;
|
|
344
|
+
|
|
345
|
+
const event = { messages, sessionFile };
|
|
346
|
+
const ctx = { sessionId, sessionKey };
|
|
347
|
+
|
|
348
|
+
// 1. Always capture thread (idempotent, deduped)
|
|
349
|
+
const captureResult = await appendOrCreateThread({
|
|
350
|
+
client,
|
|
351
|
+
logger,
|
|
352
|
+
event,
|
|
353
|
+
ctx,
|
|
354
|
+
reason: "turn",
|
|
355
|
+
maxMessageChars: cfg.maxThreadMessageChars,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// 2. Triage + distill (shared logic with agent_end path)
|
|
359
|
+
await triageAndDistill({ client, cfg, logger, captureResult, ctx });
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
// ------------------------------------------------------------------
|
|
363
|
+
// prepareSubagentSpawn — propagate memory context to child sessions
|
|
364
|
+
//
|
|
365
|
+
// When OpenClaw spawns parallel research agents, they start without
|
|
366
|
+
// memory context. We inject the parent's Working Memory and recently
|
|
367
|
+
// recalled memories so the child has relevant background.
|
|
368
|
+
// ------------------------------------------------------------------
|
|
369
|
+
async prepareSubagentSpawn({ parentSessionKey, childSessionKey }) {
|
|
370
|
+
try {
|
|
371
|
+
const parentState = _sessions.get(parentSessionKey);
|
|
372
|
+
if (parentState) {
|
|
373
|
+
_childContext.set(childSessionKey, {
|
|
374
|
+
wm: parentState.wm,
|
|
375
|
+
memories: (parentState.memories || []).slice(0, 3),
|
|
376
|
+
});
|
|
377
|
+
evictIfNeeded(_childContext, MAX_CHILD_ENTRIES);
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
rollback: () => _childContext.delete(childSessionKey),
|
|
381
|
+
};
|
|
382
|
+
} catch (err) {
|
|
383
|
+
logger.debug?.(`ce: subagent spawn prep failed: ${err}`);
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
// ------------------------------------------------------------------
|
|
389
|
+
// onSubagentEnded — clean up child context cache
|
|
390
|
+
// ------------------------------------------------------------------
|
|
391
|
+
async onSubagentEnded({ childSessionKey }) {
|
|
392
|
+
_childContext.delete(childSessionKey);
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
// ------------------------------------------------------------------
|
|
396
|
+
// dispose — clean up all state
|
|
397
|
+
// ------------------------------------------------------------------
|
|
398
|
+
async dispose() {
|
|
399
|
+
ceState.active = false;
|
|
400
|
+
_sessions.clear();
|
|
401
|
+
_childContext.clear();
|
|
402
|
+
logger.info("nowledge-mem: context engine disposed");
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
}
|
package/src/hooks/behavioral.js
CHANGED
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
* When sessionContext is enabled, guidance is adjusted to note that
|
|
11
11
|
* relevant memories are already injected — reducing redundant searches
|
|
12
12
|
* while keeping the save nudge and thread awareness.
|
|
13
|
+
*
|
|
14
|
+
* When the context engine is active, this hook is a no-op —
|
|
15
|
+
* assemble() handles behavioral guidance via systemPromptAddition.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
import { ceState } from "../ce-state.js";
|
|
19
|
+
|
|
20
|
+
export const BASE_GUIDANCE = [
|
|
16
21
|
"<nowledge-mem-guidance>",
|
|
17
22
|
"You have access to the user's personal knowledge graph (Nowledge Mem).",
|
|
18
23
|
"Before answering questions about prior work, decisions, dates, people, preferences, or plans:",
|
|
@@ -23,7 +28,7 @@ const BASE_GUIDANCE = [
|
|
|
23
28
|
"</nowledge-mem-guidance>",
|
|
24
29
|
].join("\n");
|
|
25
30
|
|
|
26
|
-
const SESSION_CONTEXT_GUIDANCE = [
|
|
31
|
+
export const SESSION_CONTEXT_GUIDANCE = [
|
|
27
32
|
"<nowledge-mem-guidance>",
|
|
28
33
|
"You have access to the user's personal knowledge graph (Nowledge Mem).",
|
|
29
34
|
"Relevant memories and your Working Memory have already been injected into this prompt.",
|
|
@@ -37,6 +42,7 @@ const SESSION_CONTEXT_GUIDANCE = [
|
|
|
37
42
|
export function buildBehavioralHook(logger, { sessionContext = false } = {}) {
|
|
38
43
|
const guidance = sessionContext ? SESSION_CONTEXT_GUIDANCE : BASE_GUIDANCE;
|
|
39
44
|
return (_event, _ctx) => {
|
|
45
|
+
if (ceState.active) return;
|
|
40
46
|
logger.debug?.("behavioral: injecting guidance");
|
|
41
47
|
return { appendSystemContext: guidance };
|
|
42
48
|
};
|