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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user