Skip to content

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.

Every time you write a tool. It is the first return value of Tool.Execute, so you construct one on every code path.

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.

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

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())
}
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.