WaitFor
Go · package github.com/muthuishere/toolnexus/golang · SPEC §10 · golang/client.go
type ClientOptions struct { // ... WaitFor func(Request) (Answer, error)}WaitFor is the one host slot for resolving a suspension: data → data, blocking (Go has no
await), on ClientOptions. When a tool call returns Pending(request), the loop calls
c.opts.WaitFor(request). Its interior is entirely unconstrained — open a browser and poll,
message a Slack channel and poll, write a file and watch it, forward the request over A2A to
another agent. Whatever it does, it returns an Answer, and the loop takes it from there.
When to use it
Section titled “When to use it”Set WaitFor whenever you want suspensions resolved in-process — the run blocks inside
Run/Stream/Ask until the human or system answers, then continues automatically. This is
the simplest posture: state lives on the live process, no persistence needed. Leave WaitFor
unset when you want the durable posture instead — the run halts with
RunResult.Status == "pending", and you resume it later, possibly in another process, with
Client.Resume (RunWithAnswer/AskWithAnswer).
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — WaitFor resolves a suspension inline
Section titled “1. The smallest useful call — WaitFor resolves a suspension inline”package main
import ( "context" "fmt" "log" "net/http" "net/http/httptest"
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":"ask","arguments":"{}"}}]}}],"usage":{"prompt_tokens":2,"completion_tokens":2,"total_tokens":4}}`)) return } _, _ = w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"got it"}}],"usage":{"prompt_tokens":2,"completion_tokens":1,"total_tokens":3}}`)) })) defer srv.Close()
ask := toolnexus.Tool{ Name: "ask", Description: "asks something", InputSchema: toolnexus.JSONSchema{"type": "object", "properties": map[string]any{}}, Source: toolnexus.SourceCustom, Execute: func(_ map[string]any, ctx *toolnexus.ToolContext) (toolnexus.ToolResult, error) { if ctx != nil && ctx.Answer != nil { return toolnexus.ToolResult{Output: "resolved"}, nil } return toolnexus.Pending(toolnexus.Request{Kind: "input", Prompt: "confirm?"}), nil }, }
tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{ExtraTools: []toolnexus.Tool{ask}}) if err != nil { log.Fatal(err) } defer tk.Close()
client := toolnexus.CreateClient(toolnexus.ClientOptions{ BaseURL: srv.URL, Style: toolnexus.StyleOpenAI, Model: "gpt-4o-mini", APIKey: "test-key", WaitFor: func(req toolnexus.Request) (toolnexus.Answer, error) { return toolnexus.Answer{ID: req.ID, Ok: true}, nil }, })
res, err := client.Run(context.Background(), "please ask", tk) if err != nil { log.Fatal(err) } if res.Status != "done" || res.Text != "got it" { log.Fatalf("unexpected: %+v", res) }
fmt.Println("ok:", res.Text)}2. The realistic case — no WaitFor set: the run halts durably instead
Section titled “2. The realistic case — no WaitFor set: the run halts durably instead”Omitting WaitFor entirely is the other, equally valid posture: the run does not hang or
error — it comes back with Status: "pending" and the Request for you to resolve out of
band.
package main
import ( "context" "fmt" "log" "net/http" "net/http/httptest"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":null,"tool_calls":[{"id":"c1","type":"function","function":{"name":"ask","arguments":"{}"}}]}}],"usage":{"prompt_tokens":2,"completion_tokens":2,"total_tokens":4}}`)) })) defer srv.Close()
ask := toolnexus.Tool{ Name: "ask", Description: "asks something", InputSchema: toolnexus.JSONSchema{"type": "object", "properties": map[string]any{}}, Source: toolnexus.SourceCustom, Execute: func(_ map[string]any, _ *toolnexus.ToolContext) (toolnexus.ToolResult, error) { return toolnexus.Pending(toolnexus.Request{Kind: "input", Prompt: "confirm?"}), nil }, }
tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{ExtraTools: []toolnexus.Tool{ask}}) if err != nil { log.Fatal(err) } defer tk.Close()
// No WaitFor configured. client := toolnexus.CreateClient(toolnexus.ClientOptions{ BaseURL: srv.URL, Style: toolnexus.StyleOpenAI, Model: "gpt-4o-mini", APIKey: "test-key", })
res, err := client.Run(context.Background(), "please ask", tk) if err != nil { log.Fatal(err) } if res.Status != "pending" || res.Pending == nil { log.Fatalf("expected a durable pending halt, got status=%q pending=%v", res.Status, res.Pending) } if res.Pending.Prompt != "confirm?" { log.Fatalf("unexpected pending request: %+v", res.Pending) }
fmt.Println("ok: halted durably with", res.Pending.Kind, "-", res.Pending.Prompt)}3. The full surface — declined vs granted, and the loop rule on retry
Section titled “3. The full surface — declined vs granted, and the loop rule on retry”Answer.Ok == false feeds back a "declined/expired: <prompt>" error result and the model
decides what to do next — the run does not abort.
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":"delete_record","arguments":"{}"}}]}}],"usage":{"prompt_tokens":2,"completion_tokens":2,"total_tokens":4}}`)) return } _, _ = w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"understood, not deleting"}}],"usage":{"prompt_tokens":2,"completion_tokens":2,"total_tokens":4}}`)) })) defer srv.Close()
del := toolnexus.Tool{ Name: "delete_record", Description: "deletes a record", InputSchema: toolnexus.JSONSchema{"type": "object", "properties": map[string]any{}}, Source: toolnexus.SourceCustom, Execute: func(_ map[string]any, ctx *toolnexus.ToolContext) (toolnexus.ToolResult, error) { if ctx != nil && ctx.Answer != nil { return toolnexus.ToolResult{Output: "deleted"}, nil } return toolnexus.Pending(toolnexus.Request{Kind: "approval", Prompt: "delete this record?"}), nil }, }
tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{ExtraTools: []toolnexus.Tool{del}}) if err != nil { log.Fatal(err) } defer tk.Close()
client := toolnexus.CreateClient(toolnexus.ClientOptions{ BaseURL: srv.URL, Style: toolnexus.StyleOpenAI, Model: "gpt-4o-mini", APIKey: "test-key", WaitFor: func(req toolnexus.Request) (toolnexus.Answer, error) { return toolnexus.Answer{ID: req.ID, Ok: false, Reason: "declined"}, nil }, })
res, err := client.Run(context.Background(), "delete the record", tk) if err != nil { log.Fatal(err) } if res.Status != "done" { log.Fatalf("a decline continues the loop, not aborts it: %+v", res) } if !strings.Contains(res.ToolCalls[0].Output, "declined") { log.Fatalf("expected the declined error text fed back, got %q", res.ToolCalls[0].Output) } if !res.ToolCalls[0].IsError { log.Fatal("a declined resolution is an error tool_result") }
fmt.Println("ok:", res.Text)}The loop rule
Section titled “The loop rule”WaitFor |
Answer.Ok |
What happens |
|---|---|---|
| configured | true |
Retries the same tool once with ctx.Answer set. Still-suspended retry ⇒ error "unresolved: <prompt>", never loops forever. |
| configured | false |
Feeds back error "declined/expired: <prompt>". The loop continues; the model decides. |
| not configured | — | The run halts: RunResult.Status == "pending", RunResult.Pending set. Resume later via Client.Resume. |
See also
Section titled “See also”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.PendingOf— Detect that a RunResult is parked rather than finished, and get the Request that parked it.Client.Resume— The answer-carrying entry point: resume a parked run in a different process.