feat(kb): dynamic-kb-* tool family + <kb-block> subblock provider
Implements the HF side of Plexum DESIGN-DYNAMIC-BLOCK.md Phase 2 — the
HarborForge knowledge-base block agents cache facts into per session.
Cross-runtime aligned with the ClawSkills workflow text (same tool
names + input schemas + return shapes will land on openclaw side later).
manifest.json:
- 5 new dynamic-kb-* tool contracts (list-kbs / list-topics /
list-facts / cache / evict)
- dynamicSubblocks contract entry declaring this plugin owns
the "kb-block" <dynamic-block> subblock
internal/kbblock/ (new):
- Per-session storage at <PLEXUM_PROFILE_ROOT>/agents/<id>/sessions/
<sid>/plugins/harbor-forge/kb-block.json
- Entry carries ID (HF backend DB primary key) + KBCode + SourceTopic
+ Content + InsertSeq for §9 #4 cache-insertion-order rendering
- Render emits <kb-fact id=N kb=<code> source=topic:<slug>>...
</kb-fact> (no title/description per §9 #8; source attr omitted
when empty)
- Fade NOT applied in v1 — §9 #3 lock has fade params shared with
memory but implementation deferred until prod data informs whether
KB needs it; agent dynamic-kb-evict is the only eviction path
- 11 unit tests
internal/kbclient/ (new):
- Typed HTTP client for HarborForge.Backend KB routes verified
against app/api/routers/knowledge.py
- GET /knowledge-bases[?project=<code>] (list KBs)
- GET /knowledge-bases/{kb_code}/topics (list topics)
- GET /knowledge-bases/{kb_code}/tree (full hierarchy — ListFacts
flattens this client-side filtered by topic ids; backend has no
flat list-facts-in-topic route)
- GET /knowledge-facts/{id} per fact (GetFacts batch loop)
- Auth: plugin-level Bearer APIKey. Per-agent hf-token resolution
is a TODO when SDK exposes secret-mgr access.
internal/tools/kb.go (new) + tools.go:
- 5 tool functions hooked into Dispatch
- KBDeps struct bundles Client + ProfileRoot + SessionFor + Turn
- Cache/evict use SessionFor lookup populated by main.go's
RenderDynamicSubblock (called per turn by host; carries sessionID)
cmd/plexum-harborforge-plugin/main.go:
- kbClient field initialized when BackendURL + APIKey present
- profileRoot cached for kbblock path resolution
- agentSession sync.Map tracks agentID → sessionID; populated by
RenderDynamicSubblock so subsequent tool calls in the same turn
can resolve the per-session kb-block.json path
- Implements sdkplugin.DynamicBlockProvider.RenderDynamicSubblock:
opens kbblock for (agentID, sessionID) and returns its Render()
body; host wraps in <kb-block>...</kb-block>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,12 @@ type Deps struct {
|
||||
// host injects this via the tool dispatch context; main.go's
|
||||
// CallTool reads it from the ctx and stashes here.
|
||||
AgentIDFromCtx func(ctx context.Context) string
|
||||
|
||||
// KB wires the dynamic-kb-* tool family (DESIGN-DYNAMIC-BLOCK.md
|
||||
// §3.3 / §4.4). Zero value when HF KB backend is unconfigured;
|
||||
// each KB tool then returns a graceful "backend unavailable"
|
||||
// error rather than crashing.
|
||||
KB KBDeps
|
||||
}
|
||||
|
||||
// Dispatch is the entry point main.go's ToolPlugin.CallTool calls.
|
||||
@@ -61,6 +67,16 @@ func Dispatch(ctx context.Context, deps Deps, name string, input json.RawMessage
|
||||
return toolCalendarResume(ctx, deps)
|
||||
case "harborforge_restart_status":
|
||||
return toolRestartStatus(deps)
|
||||
case "dynamic-kb-list-kbs":
|
||||
return ToolKBListKBs(ctx, deps.KB, input)
|
||||
case "dynamic-kb-list-topics":
|
||||
return ToolKBListTopics(ctx, deps.KB, input)
|
||||
case "dynamic-kb-list-facts":
|
||||
return ToolKBListFacts(ctx, deps.KB, input)
|
||||
case "dynamic-kb-cache":
|
||||
return ToolKBCache(ctx, deps.KB, deps.AgentIDFromCtx(ctx), input)
|
||||
case "dynamic-kb-evict":
|
||||
return ToolKBEvict(ctx, deps.KB, deps.AgentIDFromCtx(ctx), input)
|
||||
}
|
||||
return sdkplugin.ToolResult{
|
||||
IsError: true,
|
||||
|
||||
Reference in New Issue
Block a user