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>
This commit is contained in:
@@ -29,6 +29,7 @@ import (
|
||||
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/identity"
|
||||
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/inbound"
|
||||
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/tokens"
|
||||
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/tools"
|
||||
)
|
||||
|
||||
// HostConfig is the plugin's own config at
|
||||
@@ -51,6 +52,7 @@ type fabricPlugin struct {
|
||||
byFabric config.ByFabricChannel
|
||||
client *fabric.Client
|
||||
tokens *tokens.Cache
|
||||
tools *tools.Registry
|
||||
|
||||
// Goroutine handle for the inbound supervisor. Cancelled on
|
||||
// plugin shutdown (we don't have an explicit shutdown signal in
|
||||
@@ -66,12 +68,10 @@ type fabricPlugin struct {
|
||||
}
|
||||
|
||||
func (p *fabricPlugin) Manifest() plugin.Manifest {
|
||||
// Manifest channels are populated dynamically from channels/*.json
|
||||
// at startup: the operator adds a channels/<name>.json + restarts
|
||||
// the gateway, and the matching ChannelContract entry surfaces here.
|
||||
// Both halves needed because Plexum's host registry reads the
|
||||
// manifest's channel names too.
|
||||
channels := p.dynamicChannelContracts()
|
||||
// Tool surface = the agent-facing tool family (port of openclaw
|
||||
// plugin's tools.ts, F-4). The "send" outbound is also there for
|
||||
// the host's channel manager.
|
||||
return plugin.Manifest{
|
||||
Name: config.PluginName,
|
||||
Version: "0.1.0",
|
||||
@@ -79,21 +79,7 @@ func (p *fabricPlugin) Manifest() plugin.Manifest {
|
||||
Executable: "plexum-fabric-channel-plugin",
|
||||
Contracts: plugin.Contracts{
|
||||
Channels: channels,
|
||||
Tools: []plugin.ToolContract{
|
||||
{
|
||||
Name: "send",
|
||||
Description: "Post a plain-text message to the bound Fabric channel as the agent user.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel_name": {"type": "string"},
|
||||
"session_id": {"type": "string"},
|
||||
"message": {"type": "string"}
|
||||
},
|
||||
"required": ["channel_name", "message"]
|
||||
}`),
|
||||
},
|
||||
},
|
||||
Tools: tools.New(tools.Deps{}).Contracts(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -169,6 +155,11 @@ func (p *fabricPlugin) Init(ctx context.Context, host plugin.HostAPI) error {
|
||||
return p.client.AgentLogin(loginCtx, entry.FabricAPIKey)
|
||||
})
|
||||
|
||||
// Agent-facing tool surface (port of openclaw's tools.ts).
|
||||
p.tools = tools.New(tools.Deps{
|
||||
Client: p.client, Tokens: p.tokens, Identities: p.identities,
|
||||
})
|
||||
|
||||
host.Log("info", "fabric channel plugin initialized", map[string]any{
|
||||
"center": p.cfg.CenterAPIBase,
|
||||
"identity_path": idPath,
|
||||
@@ -284,11 +275,22 @@ func (p *fabricPlugin) warmSessions(ctx context.Context) error {
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// CallTool handles the "send" outbound tool.
|
||||
// CallTool dispatches by tool name. "send" is the host-side channel
|
||||
// outbound (driven by Plexum's channel manager); everything else
|
||||
// delegates to the tools.Registry built at init time.
|
||||
func (p *fabricPlugin) CallTool(ctx context.Context, name string, input json.RawMessage) (plugin.ToolResult, error) {
|
||||
if name != "send" {
|
||||
return plugin.ToolResult{}, fmt.Errorf("unknown tool: %s", name)
|
||||
if name == "send" {
|
||||
return p.handleSend(ctx, input)
|
||||
}
|
||||
if handler := p.tools.Handler(name); handler != nil {
|
||||
return handler(ctx, input)
|
||||
}
|
||||
return plugin.ToolResult{}, fmt.Errorf("unknown tool: %s", name)
|
||||
}
|
||||
|
||||
// handleSend implements the "send" outbound tool: posts the assistant
|
||||
// reply via Fabric REST as the agent bound to the Plexum channel name.
|
||||
func (p *fabricPlugin) handleSend(ctx context.Context, input json.RawMessage) (plugin.ToolResult, error) {
|
||||
var args struct {
|
||||
ChannelName string `json:"channel_name"`
|
||||
SessionID string `json:"session_id"`
|
||||
@@ -304,7 +306,6 @@ func (p *fabricPlugin) CallTool(ctx context.Context, name string, input json.Raw
|
||||
"channel_name": args.ChannelName, "len": len(args.Message),
|
||||
})
|
||||
|
||||
// Find the binding for this plexum channel name.
|
||||
var binding *config.FabricBinding
|
||||
for i := range p.bindings {
|
||||
if p.bindings[i].PlexumChannelName == args.ChannelName {
|
||||
@@ -316,19 +317,12 @@ func (p *fabricPlugin) CallTool(ctx context.Context, name string, input json.Raw
|
||||
return errResult("unknown plexum channel: " + args.ChannelName), nil
|
||||
}
|
||||
|
||||
// Resolve the bound agent's session (may need refresh — F-2 will
|
||||
// add a proper TTL + background refresh; for F-1 we re-login lazily
|
||||
// if the cache is empty).
|
||||
sess, err := p.sessionFor(ctx, binding.AgentID)
|
||||
if err != nil {
|
||||
return errResult("session for agent " + binding.AgentID + ": " + err.Error()), nil
|
||||
}
|
||||
|
||||
// Pick the guild endpoint + token for the target guild_node_id.
|
||||
var (
|
||||
endpoint string
|
||||
token string
|
||||
)
|
||||
var endpoint, token string
|
||||
for _, g := range sess.Guilds {
|
||||
if g.NodeID == binding.FabricGuildNodeID {
|
||||
endpoint = g.Endpoint
|
||||
|
||||
Reference in New Issue
Block a user