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:
h z
2026-06-05 20:15:34 +01:00
parent 046a7753e6
commit 9e43021ba5
7 changed files with 1075 additions and 1 deletions

View File

@@ -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,