feat(kb): dynamic-kb-* tool family + <kb-block> subblock injection
Openclaw mirror of HarborForge.PlexumPlugin's Phase 2 work. Same tool
names + input schemas + return shapes + storage path layout so a
single ClawSkills workflow text works on both runtimes.
plugin/openclaw.plugin.json:
- 5 new dynamic-kb-* contracts.tools entries
plugin/tools/kbblock.ts (new):
- TS class mirror of Plexum internal/kbblock package
- Storage at <OPENCLAW_PATH>/agents/<agentId>/sessions/<sessionId>/
plugins/harbor-forge/kb-block.json
- Entry shape + Render emit <kb-fact id=N kb=<code>
source=topic:<slug>>...</kb-fact> identical to Plexum side
- Insert-order rendering (§9 #4), no fade in v1 (§9 #3 placeholder)
plugin/tools/kbclient.ts (new):
- TS HTTP client for HF backend KB routes:
GET /knowledge-bases[?project=<code>], /knowledge-bases/{id}/topics,
/knowledge-bases/{id}/tree, /knowledge-facts/{id}
- ListFacts flattens the tree client-side filtered by topic ids
- Bearer auth via plugin-level apiKey (per-agent hf-token resolution
deferred — TODO matching Plexum side)
plugin/tools/dynamic-kb.ts (new):
- 5 tool factories (createListKBs/Topics/Facts/Cache/Evict)
- Each accepts ctx in execute so cache/evict resolve sessionID
- KBDeps bundle wires client + token-for + makeClient + turn-for
plugin/index.ts:
- Register 5 KB tools via the existing wrapped api.registerTool
(already handles MCP content shape + PaddedCell tools-cache gate)
- before_prompt_build hook: when block non-empty, returns
appendSystemContext = <dynamic-block><kb-block>...</kb-block>
</dynamic-block>. Empty block → no append. Limitation: append
only (can't replace openclaw's baked-in static sys-msg content);
the dynamic-block lands at the end rather than slotted into
System[1] like on Plexum
No tests yet — sim verification on dind-t2 next (requires HF backend
container rebuild to expose /knowledge-* routes; current image
predates the KB module).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,16 @@ import { MonitorBridgeClient, type OpenClawMeta } from './core/monitor-bridge.js
|
||||
import type { OpenClawAgentInfo } from './core/openclaw-agents.js';
|
||||
import { registerGatewayStartHook } from './hooks/gateway-start.js';
|
||||
import { registerGatewayStopHook } from './hooks/gateway-stop.js';
|
||||
import { Block as KBBlock } from './tools/kbblock.js';
|
||||
import { KBClient } from './tools/kbclient.js';
|
||||
import {
|
||||
type KBDeps,
|
||||
createListKBsTool,
|
||||
createListTopicsTool,
|
||||
createListFactsTool,
|
||||
createCacheTool,
|
||||
createEvictTool,
|
||||
} from './tools/dynamic-kb.js';
|
||||
import {
|
||||
createCalendarBridgeClient,
|
||||
CalendarScheduler,
|
||||
@@ -928,6 +938,73 @@ function register(api: PluginAPI): void {
|
||||
},
|
||||
}));
|
||||
|
||||
// ---- dynamic-kb-* family (DESIGN-DYNAMIC-BLOCK.md §3.3 / §4.4)
|
||||
// Cross-runtime mirror of HarborForge.PlexumPlugin/internal/tools/kb.go +
|
||||
// /internal/kbblock + /internal/kbclient. v1 auth = plugin-level apiKey
|
||||
// via Bearer (per-agent hf-token resolution is a TODO matching the
|
||||
// Plexum side).
|
||||
const kbCfg = resolveConfig();
|
||||
const kbBackendUrl =
|
||||
typeof kbCfg?.backendUrl === 'string' && kbCfg.backendUrl
|
||||
? (kbCfg.backendUrl as string)
|
||||
: 'https://monitor.hangman-lab.top';
|
||||
const kbApiKey = typeof kbCfg?.apiKey === 'string' ? (kbCfg.apiKey as string) : '';
|
||||
const kbDeps: KBDeps = {
|
||||
client: kbApiKey ? new KBClient(kbBackendUrl, kbApiKey) : null,
|
||||
tokenFor: null,
|
||||
makeClient: null,
|
||||
turnFor: () => 0,
|
||||
};
|
||||
|
||||
// Wrap each KB tool factory: pass ctx into execute so cache/evict can
|
||||
// resolve the per-session kb-block.json path.
|
||||
const kbFactories = [
|
||||
createListKBsTool,
|
||||
createListTopicsTool,
|
||||
createListFactsTool,
|
||||
createCacheTool,
|
||||
createEvictTool,
|
||||
];
|
||||
for (const make of kbFactories) {
|
||||
api.registerTool((ctx: any) => {
|
||||
const tool = make(kbDeps);
|
||||
const inner = tool.execute;
|
||||
tool.execute = async (callId: string, params: any) =>
|
||||
inner(callId, params, { agentId: ctx?.agentId, sessionId: ctx?.sessionId });
|
||||
return tool;
|
||||
});
|
||||
}
|
||||
|
||||
// <kb-block> subblock injection via before_prompt_build hook
|
||||
// (DESIGN-DYNAMIC-BLOCK.md §2 + §7: openclaw side uses
|
||||
// appendSystemContext since the hook can't replace baked-in
|
||||
// <available_skills> precisely). Empty block → no append.
|
||||
const apiOn = (api as any).on;
|
||||
if (typeof apiOn === 'function') {
|
||||
apiOn.call(
|
||||
api,
|
||||
'before_prompt_build',
|
||||
async (_event: any, ctx: any) => {
|
||||
const agentId = ctx?.agentId;
|
||||
const sessionId = ctx?.sessionId;
|
||||
if (!agentId || !sessionId) return undefined;
|
||||
let body = '';
|
||||
try {
|
||||
const block = KBBlock.open(agentId, sessionId);
|
||||
body = block.render();
|
||||
} catch (err) {
|
||||
logger.warn(`kb-block render failed: ${err}`);
|
||||
return undefined;
|
||||
}
|
||||
if (!body) return undefined;
|
||||
return {
|
||||
appendSystemContext: `<dynamic-block>\n<kb-block>\n${body}</kb-block>\n</dynamic-block>\n`,
|
||||
};
|
||||
},
|
||||
);
|
||||
logger.info('HarborForge kb-block subblock hook registered');
|
||||
}
|
||||
|
||||
logger.info('HarborForge plugin registered (id: harbor-forge)');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user