Skip to content

AnswerDeclined

Go · package github.com/muthuishere/toolnexus/golang · SPEC §10 · golang/relay.go

func AnswerDeclined(id, reason string) Answer // reason == "" ⇒ "declined"
type Answer struct {
ID string `json:"id"`
Ok bool `json:"ok"` // false here — a refusal, not an error
Data map[string]any `json:"data,omitempty"`
Reason string `json:"reason,omitempty"`
}

AnswerDeclined(id, reason) builds the Answer a host returns when a human — or the host itself — refused the suspended request: no payload, Ok: false, and a Reason the loop feeds back to the model. An empty reason defaults to "declined" rather than an empty string, so the model never sees a refusal with nothing attached to explain it.

A declined relay is an error tool_result, not an aborted run (§10, ADR-0010 D6): the run keeps going, the model reads the refusal in its next turn, and can recover — try something else, ask a follow-up, or report back to the user that access was denied.

Reach for AnswerDeclined any time the party who would resolve a suspension says no: a human declines an approval prompt, a login was cancelled, a durable request expired unanswered and you want to close it out explicitly rather than leave it parked forever. Pass it to whichever resume path you’re using — inline as the return value of a WaitFor function, or out-of-band via Runtime.Resume / Client.RunWithAnswer.

1. The smallest useful call — build the Answer and inspect its shape

Section titled “1. The smallest useful call — build the Answer and inspect its shape”
package main
import (
"fmt"
"log"
toolnexus "github.com/muthuishere/toolnexus/golang"
)
func main() {
ans := toolnexus.AnswerDeclined("req-1", "not authorized")
if ans.ID != "req-1" || ans.Ok {
log.Fatalf("unexpected answer: %+v", ans)
}
if ans.Reason != "not authorized" {
log.Fatalf("Reason = %q, want %q", ans.Reason, "not authorized")
}
// An empty reason defaults rather than shipping blank.
def := toolnexus.AnswerDeclined("req-2", "")
if def.Reason != "declined" {
log.Fatalf("default Reason = %q, want %q", def.Reason, "declined")
}
fmt.Println("ok:", ans.Reason, "/", def.Reason)
}

2. The realistic case — decline a relayed tool call and let the run finish anyway

Section titled “2. The realistic case — decline a relayed tool call and let the run finish anyway”
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"strings"
toolnexus "github.com/muthuishere/toolnexus/golang"
)
func main() {
turn := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
turn++
w.Header().Set("Content-Type", "application/json")
if turn == 1 {
_, _ = w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":null,"tool_calls":[{"id":"c1","type":"function","function":{"name":"refund","arguments":"{}"}}]}}],"usage":{"prompt_tokens":2,"completion_tokens":2,"total_tokens":4}}`))
return
}
_, _ = w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"understood, no refund issued"}}],"usage":{"prompt_tokens":2,"completion_tokens":1,"total_tokens":3}}`))
}))
defer srv.Close()
refund := toolnexus.RelayTool("refund", "issue a refund", nil)
tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{Builtins: false, ExtraTools: []toolnexus.Tool{refund}})
if err != nil {
log.Fatal(err)
}
defer tk.Close()
client := toolnexus.CreateClient(toolnexus.ClientOptions{BaseURL: srv.URL, Style: toolnexus.StyleOpenAI, Model: "stub", APIKey: "k"})
halted, err := client.Run(context.Background(), "refund the customer", tk)
if err != nil {
log.Fatal(err)
}
if halted.Status != "pending" {
log.Fatalf("expected a durable halt, got %q", halted.Status)
}
// The approver said no — the run still completes, it just knows it was refused.
resumed, err := client.RunWithAnswer(context.Background(), tk, halted.Messages, *halted.Pending,
toolnexus.AnswerDeclined(halted.Pending.ID, "amount exceeds refund policy"))
if err != nil {
log.Fatal(err)
}
if resumed.Status != "done" || !strings.Contains(resumed.Text, "no refund") {
log.Fatalf("unexpected: %+v", resumed)
}
fmt.Println("ok:", resumed.Text)
}

3. The full surface — comparing a decline against a successful answer for the same call

Section titled “3. The full surface — comparing a decline against a successful answer for the same call”
package main
import (
"fmt"
"log"
toolnexus "github.com/muthuishere/toolnexus/golang"
)
func main() {
declined := toolnexus.AnswerDeclined("req-1", "")
output := toolnexus.AnswerOutput("req-1", "approved")
if declined.Ok {
log.Fatal("a decline must carry Ok:false")
}
if !output.Ok {
log.Fatal("a real output must carry Ok:true")
}
if declined.Data != nil {
log.Fatalf("a decline carries no payload, got %+v", declined.Data)
}
if output.Reason != "" {
log.Fatalf("a success carries no Reason, got %q", output.Reason)
}
fmt.Println("ok: declined.Ok =", declined.Ok, "output.Ok =", output.Ok)
}
  • Pending — Return a Pending from a tool to park the run until someone answers.
  • AuthRequired — The auth-shaped suspension: hand back a URL, resume once the user has granted access.
  • WaitFor — The single hook where the host resolves a suspension — in-process prompt or durable queue, same contract.
  • PendingOf — Detect that a RunResult is parked rather than finished, and get the Request that parked it.
  • AnswerOutput — The success counterpart: wrap a real result instead of a refusal.
  • Runtime.Resume / RunWithAnswer — Where a built Answer actually gets applied to a halted run.