ToolResult
Go · module github.com/muthuishere/toolnexus/golang · SPEC §1 · golang/types.go
type ToolResult struct { Output string `json:"output"` IsError bool `json:"isError"` Parts []ContentPart `json:"parts,omitempty"` Metadata map[string]any `json:"metadata,omitempty"`}The first half of what every Execute returns. Four fields, and the whole tool-calling loop is
built on them: Output is the text handed back to the model, IsError says whether the call
failed, Parts carries non-text output (an image, a PDF, an audio clip), and Metadata is
free-form — except for one reserved key that turns a result into a suspension.
Parts is optional and omitted when empty, so a tool written before it existed serialises
byte-identically. Output stays required even when Parts is set — it is what the
transcript, compaction, token estimation and any text-only provider see. A tool returning an
image sets Output to a description ("screenshot, 1280x720 png") and Parts to the image;
setting Output to "" because “the picture says it” makes the call invisible to everything
except the one provider that renders it.
When to use it
Section titled “When to use it”Every time you write a tool. It is the first return value of
Tool.Execute, so you construct one on every code path.
Why an error flag and a returned error
Section titled “Why an error flag and a returned error”Output is always a string — it is what the model reads. Serialize structured data yourself
(json.Marshal) rather than expecting the loop to do it. Bytes go in
Parts, never base64d into Output.
Examples
Section titled “Examples”1. Success and failure on the same tool
Section titled “1. Success and failure on the same tool”package main
import ( "fmt" "log"
toolnexus "github.com/muthuishere/toolnexus/golang")
var config = map[string]string{"region": "eu-west-1"}
func readConfig(key string) toolnexus.ToolResult { v, ok := config[key] if !ok { // Recoverable: the model can read this and try another key. return toolnexus.ToolResult{Output: "No such config key: " + key, IsError: true} } return toolnexus.ToolResult{Output: v, IsError: false}}
func main() { found := readConfig("region") if found.Output != "eu-west-1" || found.IsError { log.Fatalf("unexpected: %+v", found) }
missing := readConfig("nope") if !missing.IsError { log.Fatal("expected IsError") }
fmt.Println("ok:", found.Output, "|", missing.Output)}2. Structured output and metadata
Section titled “2. Structured output and metadata”Output must be a string, so serialize deliberately. Metadata rides alongside for your code —
the model never sees it, which makes it the right place for bookkeeping.
package main
import ( "fmt" "log" "strings"
toolnexus "github.com/muthuishere/toolnexus/golang")
func search(q string) toolnexus.ToolResult { hits := []struct { ID int Title string }{ {1, "Getting started"}, {2, "Advanced usage"}, }
var lines []string var ids []int for _, h := range hits { lines = append(lines, fmt.Sprintf("#%d %s", h.ID, h.Title)) ids = append(ids, h.ID) }
return toolnexus.ToolResult{ // The model reads this. Make it legible, not just valid. Output: strings.Join(lines, "\n"), IsError: false, // Your code reads this. The model never sees it. Metadata: map[string]any{"title": "search: " + q, "count": len(hits), "ids": ids}, }}
func main() { res := search("usage") if res.Metadata["count"] != 2 { log.Fatalf("unexpected count: %v", res.Metadata["count"]) } if !strings.Contains(res.Output, "Advanced usage") { log.Fatal("expected the second hit in output") } fmt.Println("ok:", res.Metadata["title"])}3. The reserved key — Metadata["pending"] is a suspension
Section titled “3. The reserved key — Metadata["pending"] is a suspension”Metadata is free-form with one exception. A pending key holding a Request means “this tool
cannot finish until something out-of-band happens” — the loop parks the run instead of returning.
You rarely write this by hand; Pending builds it for you.
package main
import ( "fmt" "log"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { // Pending() returns a ToolResult carrying Metadata["pending"] = Request. res := toolnexus.Pending(toolnexus.Request{Kind: "input", Prompt: "Which environment?"})
if !res.IsError { log.Fatal("a parked call is not a success") } req := toolnexus.PendingOf(res) if req == nil { log.Fatal("PendingOf should read the suspension back off the result") } if req.Kind != "input" || req.Prompt != "Which environment?" { log.Fatalf("unexpected request: %+v", req) } if req.ID == "" { log.Fatal("an id is generated as the correlation key") }
// AuthRequired is sugar for the login case. Go takes the prompt explicitly — // pass "" to accept the default ("Authorization required to continue"). auth := toolnexus.AuthRequired("https://example.com/login", "") authReq := toolnexus.PendingOf(auth) if authReq.Kind != "authorization" || authReq.URL != "https://example.com/login" { log.Fatalf("unexpected auth request: %+v", authReq) }
// An ordinary result has no suspension. if toolnexus.PendingOf(toolnexus.ToolResult{Output: "done"}) != nil { log.Fatal("expected no suspension on a plain result") }
fmt.Println("ok:", req.Kind, "|", authReq.Kind)}4. Non-text output — Parts alongside Output
Section titled “4. Non-text output — Parts alongside Output”A tool that produces an image fills both fields: Output describes it, Parts carries it. The
built-in read does exactly this for a recognised media file, and is the shortest way to see the
shape.
package main
import ( "fmt" "log" "os" "path/filepath" "strings"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { // TOOLNEXUS_REPO is set by the docs test runner; in your own code just use a path. png := filepath.Join(os.Getenv("TOOLNEXUS_REPO"), "examples", "media", "fixture.png")
var read *toolnexus.Tool for _, t := range toolnexus.CreateBuiltinTools() { if t.Name == "read" { tool := t read = &tool } }
res, err := read.Execute(map[string]any{"path": png}, nil) if err != nil || res.IsError { log.Fatalf("unexpected read result: %+v %v", res, err) }
// Output is REQUIRED even with parts: it is what the transcript, compaction // and any text-only provider see. "<path> (<mimeType>, <bytes> bytes)". if !strings.HasSuffix(res.Output, "fixture.png (image/png, 82 bytes)") { log.Fatalf("unexpected output: %q", res.Output) } if len(res.Parts) != 1 { log.Fatalf("expected one part, got %d", len(res.Parts)) } img := res.Parts[0] if img.Type != toolnexus.PartImage || img.MimeType != "image/png" || img.Bytes() != 82 { log.Fatalf("unexpected part: %s", img) } // The bytes are in the part, never base64d into the text the model reads. if strings.Contains(res.Output, img.Data) { log.Fatal("Output must not carry the base64") }
// A plain text file has no parts at all — byte-identical to pre-0.17 behaviour. plain, err := read.Execute(map[string]any{ "path": filepath.Join(os.Getenv("TOOLNEXUS_REPO"), "examples", "mcp.json"), }, nil) if err != nil || plain.IsError || len(plain.Parts) != 0 { log.Fatalf("a text read must carry no parts: %+v %v", plain, err) }
fmt.Println("ok:", res.Output, "|", img.String())}Fields
Section titled “Fields”| Field | Type | What it is |
|---|---|---|
Output |
string |
The text handed to the model. Always a string — serialize structured data yourself. |
IsError |
bool |
Whether the call failed. Fed back to the model, distinct from a returned error. |
Parts |
[]ContentPart |
Optional non-text output (§1B). Omitted when empty. Output stays required alongside it. |
Metadata |
map[string]any |
Free-form, for your code. Reserved: pending holds a §10 Request. |
See also
Section titled “See also”ContentPart— what goes inPartsTool— what returns thisToolContext— whatExecutereceivesPending— build a suspending resultPendingOf— read a suspension back off a result