// 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 }