initial drop: Dialectic.PlexumPlugin v0.1
Port of Dialectic.OpenclawPlugin to the Plexum SDK. 8 dialectic_*
tools wired to Dialectic.Backend over HTTP:
list_topics, topic_detail, list_arguments, propose_topic,
signup, post_argument, submit_verdict, view_verdict
Differences from the OpenClaw port worth noting:
- Per-agent API key storage: OpenClaw used secret-mgr (one entry
per agent's keyspace). Plexum has no secret-mgr; v1 stores
keys directly in plugin config (apiKey + agentKeys map).
- Agent identity at tool dispatch: OpenClaw framework surfaces
ctx.agentId; Plexum SDK doesn't yet plumb the calling agent
through ToolPlugin.CallTool. v1 falls back to
config.defaultAgentID — same stop-gap HarborForge.PlexumPlugin
is on. Tracked as upstream SDK work.
- HF on_call coverage pre-check on signup: stub that always
returns "skipped", matching OpenClaw v1's behavior (HarborForge
never shipped the cross-plugin coverage query). pre_validated
is sent as false so the backend records audit honestly.
DIALECTIC_PLUGIN_BYPASS_HF=1 env retains parity with OpenClaw.
- Activation: lazy (no background services, unlike HarborForge's
eager-spawn for the calendar scheduler + monitor bridge).
Backend client follows the bearer-auth contract OpenClaw's
backend-client.ts established; endpoint shapes are unchanged.
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/dist/
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
.DS_Store
|
||||||
28
Makefile
Normal file
28
Makefile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||||
|
GO_ENV := CGO_ENABLED=0
|
||||||
|
|
||||||
|
.PHONY: build install clean help
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Dialectic.PlexumPlugin build targets:"
|
||||||
|
@echo " build - compile binary + bundle manifest into dist/"
|
||||||
|
@echo " install - copy binary + manifest into ~/.plexum/plugins/dialectic/"
|
||||||
|
@echo " clean - rm -rf dist/"
|
||||||
|
|
||||||
|
build:
|
||||||
|
mkdir -p dist
|
||||||
|
$(GO_ENV) go build -ldflags="-X main.Version=$(VERSION)" \
|
||||||
|
-o dist/plexum-dialectic-plugin ./cmd/plexum-dialectic-plugin
|
||||||
|
cp manifest.json dist/manifest.json
|
||||||
|
@echo "Built to dist/ (version=$(VERSION))"
|
||||||
|
|
||||||
|
install: build
|
||||||
|
mkdir -p ~/.plexum/plugins/dialectic
|
||||||
|
cp dist/plexum-dialectic-plugin ~/.plexum/plugins/dialectic/
|
||||||
|
cp dist/manifest.json ~/.plexum/plugins/dialectic/
|
||||||
|
@echo "Installed to ~/.plexum/plugins/dialectic/"
|
||||||
|
@echo "Add to ~/.plexum/plexum.json .plugins.allow: 'dialectic'"
|
||||||
|
@echo "Config goes at ~/.plexum/plugins/dialectic/config.json (see README)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf dist/
|
||||||
104
README.md
Normal file
104
README.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Dialectic.PlexumPlugin
|
||||||
|
|
||||||
|
Plexum plugin that gives agents tools to participate in Dialectic v2
|
||||||
|
debates. Ports `Dialectic.OpenclawPlugin` to the Plexum SDK.
|
||||||
|
|
||||||
|
Eight tools, one per Dialectic backend endpoint:
|
||||||
|
|
||||||
|
| Tool | Backend call | Notes |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| `dialectic_list_topics` | `GET /api/topics` | filters: status/visibility/limit/offset |
|
||||||
|
| `dialectic_topic_detail` | `GET /api/topics/{id}` | lifecycle + camps + verdict pointer |
|
||||||
|
| `dialectic_list_arguments` | `GET /api/topics/{id}/arguments` | full transcript |
|
||||||
|
| `dialectic_propose_topic` | `POST /api/topics` | 4 lifecycle timestamps + verdict schema |
|
||||||
|
| `dialectic_signup` | `POST /api/topics/{id}/signups` | with HF on_call coverage pre-check |
|
||||||
|
| `dialectic_post_argument` | `POST /api/topics/{id}/arguments` | during `debating` only |
|
||||||
|
| `dialectic_submit_verdict` | `POST /api/topics/{id}/verdict` | judge submits structured verdict |
|
||||||
|
| `dialectic_view_verdict` | `GET /api/topics/{id}/verdict` | 404 until judge submits |
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add `"dialectic"` to `~/.plexum/plexum.json` `.plugins.allow` and
|
||||||
|
write `~/.plexum/plugins/dialectic/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"backendUrl": "https://dialectic-api.hangman-lab.top",
|
||||||
|
"apiKey": "g1_xxx",
|
||||||
|
"defaultAgentID": "agent-xyz"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Multi-agent claws can use `agentKeys` instead of (or in addition to)
|
||||||
|
`apiKey`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"backendUrl": "https://dialectic-api.hangman-lab.top",
|
||||||
|
"agentKeys": {
|
||||||
|
"agent-a": "g1_aaa",
|
||||||
|
"agent-b": "g1_bbb"
|
||||||
|
},
|
||||||
|
"apiKey": "g1_default_fallback",
|
||||||
|
"defaultAgentID": "agent-a"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart the host afterwards: `systemctl --user restart plexum`.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
| Field | Default | Purpose |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| `backendUrl` | `https://dialectic-api.hangman-lab.top` | Dialectic API base. Env override: `DIALECTIC_BACKEND_URL`. |
|
||||||
|
| `apiKey` | — | Default bearer token. |
|
||||||
|
| `agentKeys` | `{}` | Per-agent bearer token overrides. |
|
||||||
|
| `defaultAgentID` | — | Agent id reported to the backend when host hasn't surfaced one via tool ctx. |
|
||||||
|
|
||||||
|
## How agent identity is resolved (v1 limitation)
|
||||||
|
|
||||||
|
`Dialectic.OpenclawPlugin` got the calling agent's id via the OpenClaw
|
||||||
|
framework's `ctx.agentId`. The Plexum SDK doesn't yet surface this on
|
||||||
|
tool dispatch — same constraint `HarborForge.PlexumPlugin` hits. v1
|
||||||
|
falls back to `config.defaultAgentID`. Multi-agent claws can configure
|
||||||
|
`agentKeys` but the bearer token used per call is selected against the
|
||||||
|
config default (not the true caller), which is fine for a homogeneous-
|
||||||
|
role claw but won't sort signed verdicts apart by agent.
|
||||||
|
|
||||||
|
The fix (deferred): plumb `AgentContext` through `ToolPlugin.CallTool`
|
||||||
|
in `Plexum-sdk-go`. Once landed, swap `AgentIDFromCtx` to read it.
|
||||||
|
|
||||||
|
## HF on_call coverage pre-check
|
||||||
|
|
||||||
|
`dialectic_signup` is supposed to verify the agent has an HarborForge
|
||||||
|
`on_call` slot covering the debate window before submitting. Like the
|
||||||
|
OpenClaw plugin's v1, this Plexum port currently degrades to
|
||||||
|
`source="skipped"` (HarborForge.Backend exposes no window-coverage
|
||||||
|
query yet). Signups go through with `pre_validated=false` so the
|
||||||
|
backend records the gap honestly.
|
||||||
|
|
||||||
|
Override with `DIALECTIC_PLUGIN_BYPASS_HF=1` in the host's environment
|
||||||
|
to make the skip explicit (matches the OpenClaw plugin escape hatch).
|
||||||
|
|
||||||
|
## Deferred items
|
||||||
|
|
||||||
|
- **Per-call agent id** — see "How agent identity is resolved" above.
|
||||||
|
- **HF window-coverage check** — needs a backend-side endpoint or a
|
||||||
|
Plexum cross-plugin contract for `harbor-forge` to surface
|
||||||
|
`HasOnCallCovering(agentID, from, to)`.
|
||||||
|
- **SSE subscriptions** — agents poll via `dialectic_topic_detail` to
|
||||||
|
see status/argument changes. Once Dialectic.Backend ships SSE, add
|
||||||
|
`dialectic_subscribe`.
|
||||||
|
- **Token-cost reporting** — `dialectic_submit_verdict` already accepts
|
||||||
|
`tokens_input` / `tokens_output`; wire automatic accounting once
|
||||||
|
Plexum exposes per-turn usage telemetry through HostAPI.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- Top-level design: `arch/DIALECTIC-V2-DESIGN.md`
|
||||||
|
- Backend: `Dialectic.Backend` (Go)
|
||||||
|
- OpenClaw port: `Dialectic.OpenclawPlugin`
|
||||||
96
cmd/plexum-dialectic-plugin/main.go
Normal file
96
cmd/plexum-dialectic-plugin/main.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// plexum-dialectic-plugin — Plexum-side Dialectic plugin.
|
||||||
|
//
|
||||||
|
// Ports Dialectic.OpenclawPlugin to the Plexum SDK: 8 dialectic_* tools
|
||||||
|
// over HTTP against Dialectic.Backend. Lazy activation (no background
|
||||||
|
// services — purely tool-call driven).
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
sdkplugin "git.hangman-lab.top/hzhang/Plexum-sdk-go/plugin"
|
||||||
|
|
||||||
|
dialcfg "git.hangman-lab.top/zhi/Dialectic.PlexumPlugin/internal/config"
|
||||||
|
"git.hangman-lab.top/zhi/Dialectic.PlexumPlugin/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Version = "0.1.0"
|
||||||
|
|
||||||
|
type dialecticPlugin struct {
|
||||||
|
host sdkplugin.HostAPI
|
||||||
|
cfg dialcfg.Resolved
|
||||||
|
deps tools.Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *dialecticPlugin) Manifest() sdkplugin.Manifest {
|
||||||
|
return manifestFromDisk()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *dialecticPlugin) Init(ctx context.Context, host sdkplugin.HostAPI) error {
|
||||||
|
p.host = host
|
||||||
|
|
||||||
|
profileRoot := os.Getenv("PLEXUM_PROFILE_ROOT")
|
||||||
|
if profileRoot == "" {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
profileRoot = filepath.Join(home, ".plexum")
|
||||||
|
}
|
||||||
|
raw, err := dialcfg.Load(profileRoot)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load dialectic config: %w", err)
|
||||||
|
}
|
||||||
|
p.cfg = dialcfg.Resolve(raw)
|
||||||
|
host.Log("info", "dialectic plugin initialized", map[string]any{
|
||||||
|
"version": Version,
|
||||||
|
"backend": p.cfg.BackendURL,
|
||||||
|
"default_agent_id": p.cfg.DefaultAgentID,
|
||||||
|
"agent_keys_count": len(p.cfg.AgentKeys),
|
||||||
|
"has_default_key": p.cfg.APIKey != "",
|
||||||
|
})
|
||||||
|
|
||||||
|
p.deps = tools.Deps{
|
||||||
|
Config: p.cfg,
|
||||||
|
Host: host,
|
||||||
|
AgentIDFromCtx: func(ctx context.Context) string {
|
||||||
|
// v1: SDK doesn't surface the calling agent on tool ctx.
|
||||||
|
// Fall back to the per-claw default. Multi-agent claws will
|
||||||
|
// need SDK ctx plumbing — tracked upstream.
|
||||||
|
return p.cfg.DefaultAgentID
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *dialecticPlugin) CallTool(ctx context.Context, name string, input json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||||
|
return tools.Dispatch(ctx, p.deps, name, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func manifestFromDisk() sdkplugin.Manifest {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
raw, err := os.ReadFile(filepath.Join(filepath.Dir(exe), "manifest.json"))
|
||||||
|
if err == nil {
|
||||||
|
var m sdkplugin.Manifest
|
||||||
|
if err := json.Unmarshal(raw, &m); err == nil && m.Name != "" {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sdkplugin.Manifest{
|
||||||
|
Name: "dialectic",
|
||||||
|
Version: Version,
|
||||||
|
Activation: sdkplugin.ActivationLazy,
|
||||||
|
Executable: "plexum-dialectic-plugin",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := sdkplugin.Serve(&dialecticPlugin{}); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
fmt.Fprintf(os.Stderr, "plexum-dialectic-plugin: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module git.hangman-lab.top/zhi/Dialectic.PlexumPlugin
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require git.hangman-lab.top/hzhang/Plexum-sdk-go v0.0.0
|
||||||
|
|
||||||
|
replace git.hangman-lab.top/hzhang/Plexum-sdk-go => ../Plexum-sdk-go
|
||||||
105
internal/backend/client.go
Normal file
105
internal/backend/client.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Package backend is the typed HTTP client for Dialectic.Backend's API.
|
||||||
|
// Endpoint shapes mirror Dialectic.OpenclawPlugin/plugin/src/backend-client.ts
|
||||||
|
// so the two plugins drop into the same backend without per-plugin
|
||||||
|
// adapters.
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
BaseURL string
|
||||||
|
APIKey string
|
||||||
|
HTTP *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(baseURL, apiKey string) *Client {
|
||||||
|
return &Client{
|
||||||
|
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||||
|
APIKey: apiKey,
|
||||||
|
HTTP: &http.Client{Timeout: 15 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Get(ctx context.Context, path string) (json.RawMessage, error) {
|
||||||
|
return c.do(ctx, http.MethodGet, path, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Post(ctx context.Context, path string, body any) (json.RawMessage, error) {
|
||||||
|
return c.do(ctx, http.MethodPost, path, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Put(ctx context.Context, path string, body any) (json.RawMessage, error) {
|
||||||
|
return c.do(ctx, http.MethodPut, path, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) do(ctx context.Context, method, path string, body any) (json.RawMessage, error) {
|
||||||
|
if c.APIKey == "" {
|
||||||
|
return nil, errors.New("dialectic api key not configured — set plugins.dialectic.config.apiKey or agentKeys[<agent_id>]")
|
||||||
|
}
|
||||||
|
url := c.BaseURL + ensureLeadingSlash(path)
|
||||||
|
var rdr io.Reader
|
||||||
|
if body != nil {
|
||||||
|
raw, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal %s %s: %w", method, path, err)
|
||||||
|
}
|
||||||
|
rdr = bytes.NewReader(raw)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, url, rdr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
res, err := c.HTTP.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s %s: %w", method, path, err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
raw, _ := io.ReadAll(res.Body)
|
||||||
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||||
|
return nil, fmt.Errorf("%s %s → %d: %s",
|
||||||
|
method, path, res.StatusCode, truncate(raw, 500))
|
||||||
|
}
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureLeadingSlash(p string) string {
|
||||||
|
if strings.HasPrefix(p, "/") {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return "/" + p
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncate(b []byte, n int) string {
|
||||||
|
if len(b) <= n {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
return string(b[:n]) + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopicDetail is the minimal shape the signup tool needs (debate window
|
||||||
|
// for HF pre-check). The Dialectic backend returns more fields — they
|
||||||
|
// pass through untouched in the raw JSON the tools surface to agents.
|
||||||
|
type TopicDetail struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DebateStartAt string `json:"debate_start_at"`
|
||||||
|
DebateEndAt string `json:"debate_end_at"`
|
||||||
|
}
|
||||||
99
internal/config/config.go
Normal file
99
internal/config/config.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Package config loads the Dialectic plugin's per-profile config from
|
||||||
|
// <profile>/plugins/dialectic/config.json. Mirrors the resolved shape
|
||||||
|
// of Dialectic.OpenclawPlugin (backendUrl + per-agent api key), adapted
|
||||||
|
// for Plexum's profile layout.
|
||||||
|
//
|
||||||
|
// OpenClaw stored per-agent api keys in secret-mgr (one read per
|
||||||
|
// agent's keyspace). Plexum has no secret-mgr; v1 stores keys directly
|
||||||
|
// in this config and falls back to a single shared apiKey when the
|
||||||
|
// caller's agent id has no override entry.
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
BackendURL string `json:"backendUrl,omitempty"`
|
||||||
|
|
||||||
|
// APIKey is the default bearer token used when AgentKeys has no
|
||||||
|
// entry for the calling agent. Equivalent to a claw-level api key.
|
||||||
|
APIKey string `json:"apiKey,omitempty"`
|
||||||
|
|
||||||
|
// AgentKeys maps agent_id → bearer token. Overrides APIKey when
|
||||||
|
// the caller's agent id matches an entry.
|
||||||
|
AgentKeys map[string]string `json:"agentKeys,omitempty"`
|
||||||
|
|
||||||
|
// DefaultAgentID is the agent_id the plugin reports when the host
|
||||||
|
// hasn't surfaced the calling agent via ctx. Set this on single-
|
||||||
|
// agent claws; multi-agent setups should leave it empty and rely
|
||||||
|
// on the tool dispatcher's best-effort resolution.
|
||||||
|
DefaultAgentID string `json:"defaultAgentID,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resolved struct {
|
||||||
|
BackendURL string
|
||||||
|
APIKey string
|
||||||
|
AgentKeys map[string]string
|
||||||
|
DefaultAgentID string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultBackendURL = "https://dialectic-api.hangman-lab.top"
|
||||||
|
|
||||||
|
func PluginConfigDir(profileRoot string) string {
|
||||||
|
return filepath.Join(profileRoot, "plugins", "dialectic")
|
||||||
|
}
|
||||||
|
|
||||||
|
func PluginConfigPath(profileRoot string) string {
|
||||||
|
return filepath.Join(PluginConfigDir(profileRoot), "config.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(profileRoot string) (Config, error) {
|
||||||
|
path := PluginConfigPath(profileRoot)
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return Config{}, nil
|
||||||
|
}
|
||||||
|
return Config{}, fmt.Errorf("read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return Config{}, nil
|
||||||
|
}
|
||||||
|
var c Config
|
||||||
|
if err := json.Unmarshal(raw, &c); err != nil {
|
||||||
|
return Config{}, fmt.Errorf("parse %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resolve(c Config) Resolved {
|
||||||
|
out := Resolved{
|
||||||
|
BackendURL: DefaultBackendURL,
|
||||||
|
APIKey: c.APIKey,
|
||||||
|
AgentKeys: c.AgentKeys,
|
||||||
|
DefaultAgentID: c.DefaultAgentID,
|
||||||
|
}
|
||||||
|
if c.BackendURL != "" {
|
||||||
|
out.BackendURL = c.BackendURL
|
||||||
|
}
|
||||||
|
if env := os.Getenv("DIALECTIC_BACKEND_URL"); env != "" {
|
||||||
|
out.BackendURL = env
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveAPIKey picks the bearer token for the calling agent. Returns
|
||||||
|
// "" iff neither an agent-specific key nor a default apiKey is set.
|
||||||
|
func (r Resolved) ResolveAPIKey(agentID string) string {
|
||||||
|
if agentID != "" {
|
||||||
|
if k, ok := r.AgentKeys[agentID]; ok && k != "" {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.APIKey
|
||||||
|
}
|
||||||
32
internal/hfprecheck/precheck.go
Normal file
32
internal/hfprecheck/precheck.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Package hfprecheck implements the HF on_call coverage check used by
|
||||||
|
// dialectic_signup. The OpenClaw plugin relied on a cross-plugin global
|
||||||
|
// (`globalThis.__hfAgentStatus.hasOnCallCovering`) which HarborForge
|
||||||
|
// hadn't shipped yet — so v1 there always degraded to "skipped".
|
||||||
|
//
|
||||||
|
// Plexum has no cross-plugin globals at all. v1 here is unconditionally
|
||||||
|
// "skipped" (same audit-only outcome as OpenClaw v1). When HarborForge
|
||||||
|
// the backend grows a public coverage endpoint we'll wire it here.
|
||||||
|
package hfprecheck
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
OK bool // false ⇒ block signup
|
||||||
|
Reason string // populated when OK=false
|
||||||
|
Source string // "hf" | "skipped"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check is a no-op gate for v1. Returns OK=true, source="skipped"
|
||||||
|
// unconditionally; signups are sent to the backend with
|
||||||
|
// pre_validated=false so the audit trail records the gap honestly.
|
||||||
|
//
|
||||||
|
// DIALECTIC_PLUGIN_BYPASS_HF=1 forces "skipped" even after a future
|
||||||
|
// implementation lands — matches the OpenClaw plugin's escape hatch
|
||||||
|
// for sim environments without provisioned on_call schedules.
|
||||||
|
func Check(agentID, debateStartAt, debateEndAt string) Result {
|
||||||
|
if os.Getenv("DIALECTIC_PLUGIN_BYPASS_HF") == "1" {
|
||||||
|
return Result{OK: true, Source: "skipped"}
|
||||||
|
}
|
||||||
|
// No host-side coverage query yet; degrade to audit-only.
|
||||||
|
return Result{OK: true, Source: "skipped"}
|
||||||
|
}
|
||||||
354
internal/tools/tools.go
Normal file
354
internal/tools/tools.go
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
// Package tools wires the 8 dialectic_* tool implementations.
|
||||||
|
// Each tool is one HTTP call against Dialectic.Backend; errors come
|
||||||
|
// back as is_error=true ToolResult so the agent sees a usable message
|
||||||
|
// rather than an RPC failure.
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
sdkplugin "git.hangman-lab.top/hzhang/Plexum-sdk-go/plugin"
|
||||||
|
|
||||||
|
"git.hangman-lab.top/zhi/Dialectic.PlexumPlugin/internal/backend"
|
||||||
|
"git.hangman-lab.top/zhi/Dialectic.PlexumPlugin/internal/config"
|
||||||
|
"git.hangman-lab.top/zhi/Dialectic.PlexumPlugin/internal/hfprecheck"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
Config config.Resolved
|
||||||
|
Host sdkplugin.HostAPI
|
||||||
|
|
||||||
|
// AgentIDFromCtx resolves the calling agent's id. v1: returns
|
||||||
|
// config.DefaultAgentID (single-agent claws); multi-agent claws
|
||||||
|
// must wait for SDK ctx plumbing — see ../../README.md.
|
||||||
|
AgentIDFromCtx func(ctx context.Context) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Dispatch(ctx context.Context, deps Deps, name string, input json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||||
|
switch name {
|
||||||
|
case "dialectic_list_topics":
|
||||||
|
return toolListTopics(ctx, deps, input)
|
||||||
|
case "dialectic_topic_detail":
|
||||||
|
return toolTopicDetail(ctx, deps, input)
|
||||||
|
case "dialectic_list_arguments":
|
||||||
|
return toolListArguments(ctx, deps, input)
|
||||||
|
case "dialectic_propose_topic":
|
||||||
|
return toolProposeTopic(ctx, deps, input)
|
||||||
|
case "dialectic_signup":
|
||||||
|
return toolSignup(ctx, deps, input)
|
||||||
|
case "dialectic_post_argument":
|
||||||
|
return toolPostArgument(ctx, deps, input)
|
||||||
|
case "dialectic_submit_verdict":
|
||||||
|
return toolSubmitVerdict(ctx, deps, input)
|
||||||
|
case "dialectic_view_verdict":
|
||||||
|
return toolViewVerdict(ctx, deps, input)
|
||||||
|
}
|
||||||
|
return errResult("unknown tool: " + name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers ----
|
||||||
|
|
||||||
|
func clientFor(deps Deps, agentID string) (*backend.Client, error) {
|
||||||
|
key := deps.Config.ResolveAPIKey(agentID)
|
||||||
|
if key == "" {
|
||||||
|
return nil, fmt.Errorf("dialectic api key not configured for agent %q (set plugins.dialectic.config.apiKey or agentKeys[%q])", agentID, agentID)
|
||||||
|
}
|
||||||
|
return backend.New(deps.Config.BackendURL, key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func okResult(body json.RawMessage) sdkplugin.ToolResult {
|
||||||
|
if len(body) == 0 {
|
||||||
|
return sdkplugin.NewTextResult("null")
|
||||||
|
}
|
||||||
|
return sdkplugin.NewTextResult(string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func errResult(msg string) sdkplugin.ToolResult {
|
||||||
|
return sdkplugin.ToolResult{
|
||||||
|
IsError: true,
|
||||||
|
Content: []sdkplugin.ContentBlock{{Type: "text", Text: "error: " + msg}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- tools ----
|
||||||
|
|
||||||
|
type listTopicsIn struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Visibility string `json:"visibility,omitempty"`
|
||||||
|
Limit *int `json:"limit,omitempty"`
|
||||||
|
Offset *int `json:"offset,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolListTopics(ctx context.Context, deps Deps, in json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||||
|
var p listTopicsIn
|
||||||
|
if len(in) > 0 {
|
||||||
|
if err := json.Unmarshal(in, &p); err != nil {
|
||||||
|
return errResult("invalid input: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cli, err := clientFor(deps, deps.AgentIDFromCtx(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
q := url.Values{}
|
||||||
|
if p.Status != "" {
|
||||||
|
q.Set("status", p.Status)
|
||||||
|
}
|
||||||
|
if p.Visibility != "" {
|
||||||
|
q.Set("visibility", p.Visibility)
|
||||||
|
}
|
||||||
|
if p.Limit != nil {
|
||||||
|
q.Set("limit", strconv.Itoa(*p.Limit))
|
||||||
|
}
|
||||||
|
if p.Offset != nil {
|
||||||
|
q.Set("offset", strconv.Itoa(*p.Offset))
|
||||||
|
}
|
||||||
|
path := "/api/topics"
|
||||||
|
if qs := q.Encode(); qs != "" {
|
||||||
|
path += "?" + qs
|
||||||
|
}
|
||||||
|
raw, err := cli.Get(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
return okResult(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type topicIDIn struct {
|
||||||
|
TopicID string `json:"topic_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolTopicDetail(ctx context.Context, deps Deps, in json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||||
|
var p topicIDIn
|
||||||
|
if err := json.Unmarshal(in, &p); err != nil {
|
||||||
|
return errResult("invalid input: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
if p.TopicID == "" {
|
||||||
|
return errResult("topic_id required"), nil
|
||||||
|
}
|
||||||
|
cli, err := clientFor(deps, deps.AgentIDFromCtx(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
raw, err := cli.Get(ctx, "/api/topics/"+url.PathEscape(p.TopicID))
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
return okResult(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolListArguments(ctx context.Context, deps Deps, in json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||||
|
var p topicIDIn
|
||||||
|
if err := json.Unmarshal(in, &p); err != nil {
|
||||||
|
return errResult("invalid input: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
if p.TopicID == "" {
|
||||||
|
return errResult("topic_id required"), nil
|
||||||
|
}
|
||||||
|
cli, err := clientFor(deps, deps.AgentIDFromCtx(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
raw, err := cli.Get(ctx, "/api/topics/"+url.PathEscape(p.TopicID)+"/arguments")
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
return okResult(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type proposeTopicIn struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
VerdictSchemaID string `json:"verdict_schema_id"`
|
||||||
|
Visibility string `json:"visibility,omitempty"`
|
||||||
|
SignupOpenAt string `json:"signup_open_at"`
|
||||||
|
SignupCloseAt string `json:"signup_close_at"`
|
||||||
|
DebateStartAt string `json:"debate_start_at"`
|
||||||
|
DebateEndAt string `json:"debate_end_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolProposeTopic(ctx context.Context, deps Deps, in json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||||
|
var p proposeTopicIn
|
||||||
|
if err := json.Unmarshal(in, &p); err != nil {
|
||||||
|
return errResult("invalid input: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
for k, v := range map[string]string{
|
||||||
|
"title": p.Title,
|
||||||
|
"summary": p.Summary,
|
||||||
|
"verdict_schema_id": p.VerdictSchemaID,
|
||||||
|
"signup_open_at": p.SignupOpenAt,
|
||||||
|
"signup_close_at": p.SignupCloseAt,
|
||||||
|
"debate_start_at": p.DebateStartAt,
|
||||||
|
"debate_end_at": p.DebateEndAt,
|
||||||
|
} {
|
||||||
|
if v == "" {
|
||||||
|
return errResult(k + " required"), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cli, err := clientFor(deps, deps.AgentIDFromCtx(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
body := map[string]any{
|
||||||
|
"title": p.Title,
|
||||||
|
"summary": p.Summary,
|
||||||
|
"verdict_schema_id": p.VerdictSchemaID,
|
||||||
|
"signup_open_at": p.SignupOpenAt,
|
||||||
|
"signup_close_at": p.SignupCloseAt,
|
||||||
|
"debate_start_at": p.DebateStartAt,
|
||||||
|
"debate_end_at": p.DebateEndAt,
|
||||||
|
}
|
||||||
|
if p.Visibility != "" {
|
||||||
|
body["visibility"] = p.Visibility
|
||||||
|
}
|
||||||
|
raw, err := cli.Post(ctx, "/api/topics", body)
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
return okResult(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type signupIn struct {
|
||||||
|
TopicID string `json:"topic_id"`
|
||||||
|
WillingCamps []string `json:"willing_camps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolSignup(ctx context.Context, deps Deps, in json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||||
|
var p signupIn
|
||||||
|
if err := json.Unmarshal(in, &p); err != nil {
|
||||||
|
return errResult("invalid input: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
if p.TopicID == "" {
|
||||||
|
return errResult("topic_id required"), nil
|
||||||
|
}
|
||||||
|
if len(p.WillingCamps) == 0 {
|
||||||
|
return errResult("willing_camps must have ≥1 entry"), nil
|
||||||
|
}
|
||||||
|
agentID := deps.AgentIDFromCtx(ctx)
|
||||||
|
cli, err := clientFor(deps, agentID)
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch topic detail so we know the debate window for HF pre-check.
|
||||||
|
rawTopic, err := cli.Get(ctx, "/api/topics/"+url.PathEscape(p.TopicID))
|
||||||
|
if err != nil {
|
||||||
|
return errResult("topic lookup: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
var td backend.TopicDetail
|
||||||
|
if err := json.Unmarshal(rawTopic, &td); err != nil {
|
||||||
|
return errResult("topic lookup parse: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
if td.DebateStartAt == "" || td.DebateEndAt == "" {
|
||||||
|
return errResult("topic detail missing debate window timestamps"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pre := hfprecheck.Check(agentID, td.DebateStartAt, td.DebateEndAt)
|
||||||
|
if !pre.OK {
|
||||||
|
return errResult("HF pre-check failed: " + pre.Reason), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]any{
|
||||||
|
"willing_camps": p.WillingCamps,
|
||||||
|
"pre_validated": pre.Source == "hf",
|
||||||
|
}
|
||||||
|
raw, err := cli.Post(ctx, "/api/topics/"+url.PathEscape(p.TopicID)+"/signups", body)
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
return okResult(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type postArgumentIn struct {
|
||||||
|
TopicID string `json:"topic_id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolPostArgument(ctx context.Context, deps Deps, in json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||||
|
var p postArgumentIn
|
||||||
|
if err := json.Unmarshal(in, &p); err != nil {
|
||||||
|
return errResult("invalid input: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
if p.TopicID == "" {
|
||||||
|
return errResult("topic_id required"), nil
|
||||||
|
}
|
||||||
|
if p.Content == "" {
|
||||||
|
return errResult("content required"), nil
|
||||||
|
}
|
||||||
|
cli, err := clientFor(deps, deps.AgentIDFromCtx(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
raw, err := cli.Post(ctx, "/api/topics/"+url.PathEscape(p.TopicID)+"/arguments",
|
||||||
|
map[string]any{"content": p.Content})
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
return okResult(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type submitVerdictIn struct {
|
||||||
|
TopicID string `json:"topic_id"`
|
||||||
|
Verdict map[string]any `json:"verdict"`
|
||||||
|
Rationale string `json:"rationale"`
|
||||||
|
TokensInput *int `json:"tokens_input,omitempty"`
|
||||||
|
TokensOutput *int `json:"tokens_output,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolSubmitVerdict(ctx context.Context, deps Deps, in json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||||
|
var p submitVerdictIn
|
||||||
|
if err := json.Unmarshal(in, &p); err != nil {
|
||||||
|
return errResult("invalid input: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
if p.TopicID == "" {
|
||||||
|
return errResult("topic_id required"), nil
|
||||||
|
}
|
||||||
|
if p.Verdict == nil {
|
||||||
|
return errResult("verdict required"), nil
|
||||||
|
}
|
||||||
|
if p.Rationale == "" {
|
||||||
|
return errResult("rationale required"), nil
|
||||||
|
}
|
||||||
|
cli, err := clientFor(deps, deps.AgentIDFromCtx(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
body := map[string]any{
|
||||||
|
"verdict": p.Verdict,
|
||||||
|
"rationale": p.Rationale,
|
||||||
|
}
|
||||||
|
if p.TokensInput != nil {
|
||||||
|
body["tokens_input"] = *p.TokensInput
|
||||||
|
}
|
||||||
|
if p.TokensOutput != nil {
|
||||||
|
body["tokens_output"] = *p.TokensOutput
|
||||||
|
}
|
||||||
|
raw, err := cli.Post(ctx, "/api/topics/"+url.PathEscape(p.TopicID)+"/verdict", body)
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
return okResult(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolViewVerdict(ctx context.Context, deps Deps, in json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||||
|
var p topicIDIn
|
||||||
|
if err := json.Unmarshal(in, &p); err != nil {
|
||||||
|
return errResult("invalid input: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
if p.TopicID == "" {
|
||||||
|
return errResult("topic_id required"), nil
|
||||||
|
}
|
||||||
|
cli, err := clientFor(deps, deps.AgentIDFromCtx(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
raw, err := cli.Get(ctx, "/api/topics/"+url.PathEscape(p.TopicID)+"/verdict")
|
||||||
|
if err != nil {
|
||||||
|
return errResult(err.Error()), nil
|
||||||
|
}
|
||||||
|
return okResult(raw), nil
|
||||||
|
}
|
||||||
122
manifest.json
Normal file
122
manifest.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"name": "dialectic",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"activation": "lazy",
|
||||||
|
"executable": "plexum-dialectic-plugin",
|
||||||
|
"contracts": {
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "dialectic_list_topics",
|
||||||
|
"description": "List Dialectic debate topics, optionally filtered. status: created | signup_open | signup_closed | debating | completed | cancelled. visibility: public | private. limit (default 50, max 200), offset for pagination.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"status": {"type": "string"},
|
||||||
|
"visibility": {"type": "string"},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 200},
|
||||||
|
"offset": {"type": "integer", "minimum": 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dialectic_topic_detail",
|
||||||
|
"description": "Get one topic — lifecycle timestamps, status, verdict_schema_id, and the `camps` array (0 rows pre-signup_close, 3 rows after — scan camps[].agent_id to find which camp you were allocated to). Does NOT include arguments — call dialectic_list_arguments for the transcript.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {"topic_id": {"type": "string"}},
|
||||||
|
"required": ["topic_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dialectic_list_arguments",
|
||||||
|
"description": "Fetch the full argument transcript for a topic in posted order (pro/con/judge entries with author agent_id, content, posted_at). Use before posting a rebuttal or composing a verdict. Empty array if pre-debate.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {"topic_id": {"type": "string"}},
|
||||||
|
"required": ["topic_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dialectic_propose_topic",
|
||||||
|
"description": "Create a new debate topic. Provide title, summary, the 4 lifecycle timestamps (RFC3339, signup_open < signup_close <= debate_start < debate_end), and verdict_schema_id ('binary' | 'claim-resolution' | 'policy-recommendation' | 'free-form'). visibility defaults to private. After creation, broadcast on a Fabric announce-type channel (topic_id + signup deadline + debate window + title). The backend never broadcasts on its own.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"title": {"type": "string"},
|
||||||
|
"summary": {"type": "string"},
|
||||||
|
"verdict_schema_id": {"type": "string"},
|
||||||
|
"visibility": {"type": "string"},
|
||||||
|
"signup_open_at": {"type": "string"},
|
||||||
|
"signup_close_at": {"type": "string"},
|
||||||
|
"debate_start_at": {"type": "string"},
|
||||||
|
"debate_end_at": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"title", "summary", "verdict_schema_id",
|
||||||
|
"signup_open_at", "signup_close_at", "debate_start_at", "debate_end_at"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dialectic_signup",
|
||||||
|
"description": "Volunteer for one or more camps on a topic. Camps are 'pro' | 'con' | 'judge'; allocation picks at most one. Topic must be in `signup_open`. Pre-flight: plugin attempts HF on_call coverage check for the debate window; if HF lookup is unavailable, the check is skipped (recorded as audit-only).",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"topic_id": {"type": "string"},
|
||||||
|
"willing_camps": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "enum": ["pro", "con", "judge"]},
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["topic_id", "willing_camps"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dialectic_post_argument",
|
||||||
|
"description": "Post an argument to a topic you are allocated to. Must be in `debating`. Content max 32KB. Attaches to the latest open round.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"topic_id": {"type": "string"},
|
||||||
|
"content": {"type": "string", "maxLength": 32000}
|
||||||
|
},
|
||||||
|
"required": ["topic_id", "content"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dialectic_submit_verdict",
|
||||||
|
"description": "Submit the structured verdict for a debate you judge. Topic must be in `debating` AND past debate_end_at. `verdict` JSON shape must match the topic's verdict_schema_id. On success the topic transitions to `completed`.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"topic_id": {"type": "string"},
|
||||||
|
"verdict": {"type": "object", "additionalProperties": true},
|
||||||
|
"rationale": {"type": "string"},
|
||||||
|
"tokens_input": {"type": "integer", "minimum": 0},
|
||||||
|
"tokens_output": {"type": "integer", "minimum": 0}
|
||||||
|
},
|
||||||
|
"required": ["topic_id", "verdict", "rationale"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dialectic_view_verdict",
|
||||||
|
"description": "Fetch the structured verdict for a completed topic. 404 if still in progress or the judge has not yet submitted.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {"topic_id": {"type": "string"}},
|
||||||
|
"required": ["topic_id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
46
scripts/install.sh
Executable file
46
scripts/install.sh
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Dialectic.PlexumPlugin installer.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
PROFILE_DIR="${HOME}/.plexum"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--profile) PROFILE_DIR="$2"; shift 2 ;;
|
||||||
|
-h|--help) sed -n '2,/^set -euo/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
log() { printf '\033[1;34m[dialectic-install]\033[0m %s\n' "$*"; }
|
||||||
|
command -v go >/dev/null || { echo "go not found on PATH" >&2; exit 1; }
|
||||||
|
|
||||||
|
PLUGIN_DIR="${PROFILE_DIR}/plugins/dialectic"
|
||||||
|
mkdir -p "${PLUGIN_DIR}"
|
||||||
|
|
||||||
|
cd "${REPO}"
|
||||||
|
VERSION="$(git describe --tags --always 2>/dev/null || echo dev)"
|
||||||
|
LDFLAGS="-X main.Version=${VERSION}"
|
||||||
|
log "building plexum-dialectic-plugin (v=${VERSION})"
|
||||||
|
CGO_ENABLED=0 go build -ldflags="${LDFLAGS}" \
|
||||||
|
-o "${PLUGIN_DIR}/plexum-dialectic-plugin" \
|
||||||
|
./cmd/plexum-dialectic-plugin
|
||||||
|
|
||||||
|
cp manifest.json "${PLUGIN_DIR}/manifest.json"
|
||||||
|
log "installed binary + manifest to ${PLUGIN_DIR}"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
1. Add to ${PROFILE_DIR}/plexum.json .plugins.allow:
|
||||||
|
"dialectic"
|
||||||
|
2. Write ${PLUGIN_DIR}/config.json — sample:
|
||||||
|
{
|
||||||
|
"backendUrl": "https://dialectic-api.hangman-lab.top",
|
||||||
|
"apiKey": "g1_xxx",
|
||||||
|
"defaultAgentID": "agent-xyz"
|
||||||
|
}
|
||||||
|
(multi-agent claws: use "agentKeys": { "<agent-id>": "g1_yyy", ... })
|
||||||
|
3. Restart the host: systemctl --user restart plexum
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user