Files
Plexum-fabric-channel-plugin/internal/tools/contracts.go
hzhang d6bea46d00 feat: Phase F-3 + F-4 — exp backoff + agent tool surface (batch 1)
F-3 refinements:
- internal/inbound: replace fixed 3s reconnect wait with exponential
  backoff (1s → 60s, ×2, reset when prior session lasted >30s); proxy
  for "healthy" vs "flapping" and avoids hot reconnect loops when the
  server is sick

F-4 agent tool surface (port of openclaw plugin's tools.ts):
- internal/tools/tools.go (~370 LOC): Registry binds Deps {Client,
  Tokens, Identities} and exposes 8 agent-facing tools:
    fabric-send-message     post a normal message to any channel
    fabric-send-sys-msg     post a kind=sys message (bypasses turn engine)
    fabric-channel-list     list channels visible in a guild
    fabric-guild-list       list guilds the agent is in
    fabric-message-history  paginate channel messages by seq
    fabric-channel-set-purpose  PATCH the channel's purpose
    fabric-channel          fetch metadata + members for one channel
    fabric-canvas           get/share/update/remove channel canvas
- internal/tools/contracts.go: static ToolContract list — kept in sync
  with install.sh's manifest emitter
- Every agent-scoped tool requires agent_id in input args (Plexum SDK
  doesn't propagate calling agent id through CallTool today)
- guild_node_id defaults to agent's first guild for fabric-send-message

internal/fabric/client.go: new REST methods needed by tools —
PostSystemMessage, CreateChannel, CloseChannel, JoinChannel,
LeaveChannel, SetChannelPurpose, GetCanvas, ShareCanvas, UpdateCanvas,
RemoveCanvas, SyncCommands.

cmd/plexum-fabric-channel-plugin/main.go:
- Manifest declares the tool surface via tools.New(...).Contracts()
- CallTool dispatches "send" to handleSend (outbound for channel
  manager), everything else to tools.Registry.Handler(name)

scripts/install.sh:
- Manifest tools[] now lists all 9 tools with schemas — matches what
  internal/tools/contracts.go advertises

Live verified against running Fabric stack:
  $ plexum plugin-call fabric-guild-list '{"agent_id":"fabrictester"}'
    → "guilds for agent fabrictester (1): test-guild2 @ http://localhost:7003"
  $ plexum plugin-call fabric-channel-list '{...,"guild_node_id":"test-guild2"}'
    → 2 channels listed
  $ plexum plugin-call fabric-message-history '{...,"limit":5}'
    → 5 messages with timestamps + authors

F-5+ deferred:
- create-{chat,work,report,discussion}-channel (batch 2)
- sub-discussion family (state store + 3 tools)
- presence-sync + command-sync
- attachments

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:35:39 +01:00

140 lines
4.4 KiB
Go

package tools
import (
"encoding/json"
plugin "git.hangman-lab.top/hzhang/Plexum-sdk-go/plugin"
)
// allContracts is the static manifest list of all agent-facing tools.
// install.sh reads this via a Go-build introspection step? No — we'd
// need a separate emit-manifest binary. For now, install.sh has the
// same JSON inline (keep in sync; trivially small). Future refinement
// could auto-emit.
var allContracts = []plugin.ToolContract{
{
Name: "send",
Description: "Outbound channel reply — host calls this after an inbound dispatch (agent-side calls usually prefer `fabric-send-message`).",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"channel_name": {"type": "string"},
"session_id": {"type": "string"},
"message": {"type": "string"}
},
"required": ["channel_name", "message"]
}`),
},
{
Name: "fabric-send-message",
Description: "Post a normal message to a Fabric channel as the bound agent. guild_node_id defaults to the agent's first guild when omitted.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"agent_id": {"type": "string"},
"guild_node_id": {"type": "string"},
"channel_id": {"type": "string"},
"content": {"type": "string"}
},
"required": ["agent_id", "channel_id", "content"]
}`),
},
{
Name: "fabric-send-sys-msg",
Description: "Post a system-kind message to a channel (not subject to turn engine / wakeup).",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"agent_id": {"type": "string"},
"guild_node_id": {"type": "string"},
"channel_id": {"type": "string"},
"content": {"type": "string"}
},
"required": ["agent_id", "guild_node_id", "channel_id", "content"]
}`),
},
{
Name: "fabric-channel-list",
Description: "List channels visible to the agent in a guild (public + member channels).",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"agent_id": {"type": "string"},
"guild_node_id": {"type": "string"}
},
"required": ["agent_id", "guild_node_id"]
}`),
},
{
Name: "fabric-guild-list",
Description: "List the guilds the agent belongs to (nodes + endpoints).",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"agent_id": {"type": "string"}
},
"required": ["agent_id"]
}`),
},
{
Name: "fabric-message-history",
Description: "Paginate channel messages by seq. seq_from/seq_to/limit are optional (server caps limit at 200).",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"agent_id": {"type": "string"},
"guild_node_id": {"type": "string"},
"channel_id": {"type": "string"},
"seq_from": {"type": "integer"},
"seq_to": {"type": "integer"},
"limit": {"type": "integer"}
},
"required": ["agent_id", "guild_node_id", "channel_id"]
}`),
},
{
Name: "fabric-channel-set-purpose",
Description: "Update the channel's free-form purpose text.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"agent_id": {"type": "string"},
"guild_node_id": {"type": "string"},
"channel_id": {"type": "string"},
"purpose": {"type": "string"}
},
"required": ["agent_id", "guild_node_id", "channel_id", "purpose"]
}`),
},
{
Name: "fabric-channel",
Description: "Fetch metadata + member list for a specific channel.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"agent_id": {"type": "string"},
"guild_node_id": {"type": "string"},
"channel_id": {"type": "string"}
},
"required": ["agent_id", "guild_node_id", "channel_id"]
}`),
},
{
Name: "fabric-canvas",
Description: "Read or write the channel's pinned canvas doc. op: get|share|update|remove. share/update need title+format+source.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"agent_id": {"type": "string"},
"guild_node_id": {"type": "string"},
"channel_id": {"type": "string"},
"op": {"type": "string", "enum": ["get","share","update","remove"]},
"title": {"type": "string"},
"format": {"type": "string", "enum": ["md","html","text"]},
"source": {"type": "string"}
},
"required": ["agent_id", "guild_node_id", "channel_id", "op"]
}`),
},
}