agenttool

package module
v0.0.15 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 2, 2026 License: MIT Imports: 16 Imported by: 13

README

agenttool

The tool contract for Go agents over Open Responses: what a tool is, how a typed Go function becomes one, how its JSON Schema is generated and validated, how a batch of calls executes, and MCP adapters in both directions. It is the contract that agentturn runs and that any other loop can run; no loop is imported here.

  • The root module depends on openresponses and the standard library.
  • mcpclient and mcpserver are nested modules on the official MCP Go SDK.

Install

go get github.com/ChristopherDavenport/agenttool

Go 1.25 or later.

A tool

type ReadFileArgs struct {
	Path     string `json:"path" desc:"Absolute path to read"`
	MaxBytes int    `json:"max_bytes,omitempty" desc:"Stop after this many bytes"`
}

var ReadFile = agenttool.New("read_file", "Read a file from disk",
	func(ctx context.Context, a ReadFileArgs) (string, error) {
		b, err := os.ReadFile(a.Path)
		if err != nil {
			return "", err
		}
		return string(b), nil
	})

New reflects the schema from the argument struct at registration time. The generator understands json, desc and enum tags, nested structs, slices, maps, pointers and time.Time, and produces the strict form on request (WithStrict(): every field required, additionalProperties: false, pointers nullable). Arguments are validated against that schema before the function runs, so a missing required property or a value outside an enum reaches the model as an error it can retry on rather than a zero value.

The return type decides what the model sees: a string as text, openresponses.Contents as parts, so image-returning tools need no second constructor, and anything else as JSON. Errors returned from the function become error outputs; a tool never encodes an error as content.

The contract

type Tool interface {
	Name() string
	Description() string
	Parameters() json.RawMessage
	Execute(ctx context.Context, call Call) (Result, error)
}

Call carries the call ID, the raw arguments and an optional progress callback; Result carries the output the model sees, app-only Details, and a Terminate hint. On error the model sees the error and the output is ignored. Details that implement Recordable, one method naming a namespace, can be written to a session by a recorder that does not know their type: RecordOf gives the namespace and the value's JSON.

Optional interfaces refine a tool. Sequential forces a batch containing it to run one call at a time and Resource serialises only the calls that touch one piece of shared state; Annotated carries MCP's behavioural hints for a policy layer to read; Confined says whether a call will run in a sandbox and by what, so a shared permission preset can ask about the calls that leave one without knowing a product's own argument for leaving it; Strict marks the schema strict; Replayable says whether a call may run again; and io.Closer releases what the tool owns. WithSequential(), WithResource(), WithAnnotations(), WithStrict(), WithConfined(), WithReplay() and WithCloser() set them on a tool from New or NewFunc. Embedding such a tool in a struct to add a method compiles and silently drops the rest, so a tool built here gains a property through its option.

NewFunc builds a tool from plain values and a raw function for schemas that come from elsewhere; SchemaFor[T]() gives the schema New would reflect; Set is a list with lookup; Definition produces the openresponses.FunctionTool for a request.

A tool that stands in for another, to audit it, to grant on use, to record, is built with Wrap, which forwards every optional interface the wrapped tool declares. Embedding Tool in a struct forwards the four methods alone, so a wrapped bash that was Sequential would run in a parallel batch and nothing would fail:

audited := agenttool.Wrap(bash, func(ctx context.Context, call agenttool.Call) (agenttool.Result, error) {
	log.Info("call", "tool", bash.Name(), "id", call.ID)
	return bash.Execute(ctx, call)
})
// IsSequential(audited), ResourceOf(audited), AnnotationsOf(audited)
// all answer as they do for bash; Unwrap(audited) is bash.

The contract itself, language-neutral, is RFC 0001: what a definition is and how it is hashed, which properties a tool may declare and what each defaults to, the error convention, the batch rules, and the Go and MCP bindings as tables against it. The types above are its Go binding.

A handle on the record before the call ends

Result.Details is read when the call ends, so a tool that is killed while it runs leaves nothing behind, and that is the one case where a handle to what it started is wanted. A tool knows the moment the side effect begins, so it writes the handle then:

var Bash = agenttool.New("bash", "Run a shell command",
	func(ctx context.Context, a BashArgs) (string, error) {
		cmd := exec.CommandContext(ctx, "bash", "-c", a.Command)
		cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
		if err := cmd.Start(); err != nil {
			return "", err
		}
		// Durable before the first byte of output: a daemon that comes
		// back after a crash can reap the group.
		if err := agenttool.WriteRecord(ctx, ProcessGroup{PGID: cmd.Process.Pid}); err != nil {
			return "", err
		}
		return wait(cmd)
	})

type ProcessGroup struct {
	PGID int `json:"pgid"`
}

func (ProcessGroup) RecordNS() string { return "shell:process-group" }

WriteRecord returns when the write is durable and is a no-op when no recorder is installed, so a tool calls it unconditionally and a test installs nothing. The harness puts the recorder on the context, either per call with ContextWithRecorder or once on the executor:

exec := agenttool.Executor{Recorder: func(ctx context.Context, rec *agenttool.Record) error {
	call, _ := agenttool.CallFrom(ctx) // which call this belongs to
	return session.AppendCustom(ctx, rec.NS, call.ID, rec.Data)
}}

The record a tool writes this way and the Recordable it returns as Details are the same namespace at two moments: what it started, and how it ended.

A tool that needs the user's answer before it can go on, "delete the branch?", asks through the Elicitor the harness put on the call's context with ContextWithElicitor, so the question reaches the harness's policy and its record as part of the call. mcpclient's WithElicitation() routes an MCP server's elicitation there, and a tool served by mcpserver asks through the same Elicitor, which mcpserver sends to the client, so a question survives the move out of process.

Interrupting a call, closing a tool

A tool that owns a process, a container or a persistent shell has two moments, and the contract answers them separately.

Stopping a call in flight is the call's context. It is cancelled from another goroutine while Execute runs, which is what a host's Ctrl-C is, so a tool signals or kills what that call started and returns; a partial result with no error is the right answer when the output so far is worth the model's while. The shell the tool value owns is untouched, because the context belongs to the call.

func (s *Shell) Execute(ctx context.Context, call agenttool.Call) (agenttool.Result, error) {
	out, err := s.start(call.Args)
	select {
	case res := <-out:
		return res, err
	case <-ctx.Done():
		s.signal(os.Interrupt)               // the foreground command, not the shell
		return agenttool.Text(s.drain()), nil // what it printed before the interrupt
	}
}

func (s *Shell) Close() error { return s.container.Remove() }

There is no Interrupt method. The reference agent that has one has it because its language has no cancellation to hand, and a second way to say the same thing would leave a tool guessing which one a host used. The model's own interrupt, "send C-c and keep the shell", is an argument of the shell tool and needs nothing from the contract.

Releasing what outlives the call is io.Closer. It belongs to the host and never to a run or a batch, since a session outlives many runs and the executor closes nothing; agenttool.Set(tools).Close() closes the ones that implement it and joins their errors. A tool built with New owns something through WithCloser, beside the rest of what it declares:

var Bash = agenttool.New("bash", "Run a shell command", shell.run,
	agenttool.WithResource("shell:session"),
	agenttool.WithConfined(shell.confined), // (ctx, args) → (bool, "container:agent")
	agenttool.WithCloser(shell.close))

The same shell served over MCP arrives unconfined, since MCP has no field for it; the host that put the server in a container says so with mcpclient.WithConfined("container:agent", "bash"), or WithConfinedFunc when some calls leave it.

A batch

Executor runs a batch: parallel up to a bound, or sequential when asked. Events are yielded from the caller's goroutine, so a consumer never sees two at once.

jobs := []agenttool.Job{{Tool: ReadFile, Call: agenttool.Call{ID: call.ID, Args: call.Arguments}}}
outputs := make([]agenttool.Result, len(jobs))
for ev := range (agenttool.Executor{MaxParallel: 4}).Execute(ctx, jobs) {
	switch {
	case !ev.Final:
		show(ev.Result) // progress the tool reported through Call.Update
	case ev.Err != nil:
		outputs[ev.Index] = agenttool.ErrorResult(ev.Err)
	default:
		outputs[ev.Index] = ev.Result
	}
}

An Event names its job by Index. It is Final exactly once per job, carrying the Result or the Err, and completions arrive in completion order, not job order. A batch reaches the executor all at once and a job waits for a slot in the bound or its turn behind a shared resource, so the moment a call is handed to its tool is later, and OnStart is called then, on the job's goroutine, with the call on the context; a session recorder writes its dispatch entry there, so a call cut off before it is known never to have run.

A tool that owns shared state names it, and only the calls that touch it wait for each other:

var Bash = agenttool.New("bash", "Run a shell command", run,
	agenttool.WithResource("shell:session"))

Two bash calls in one batch then run one after the other in the model's order, while the five reads beside them still run together. Sequential remains the wider claim, "nothing else runs while I do", and takes the whole batch; a tool that reports both is sequential. Two tools that name the same resource share it, which is how a shell tool and the tool that restarts that shell stay apart, and mcpclient.WithResource("shell:session", "bash") names it for a remote tool, since MCP has no field for one. Whether the second call waits or is refused stays the tool's choice.

The grouping is the executor's, and a harness can read it rather than restate it: Executor.Chains returns the chains a batch runs in, each in the model's order, for a harness that must know which call a given one follows, as agentturn does to settle a call before dispatching the next of its chain.

The scope of both is one batch. The executor sees one batch at a time, so a bash in a sub-agent's batch, which runs under the parent call and alongside the rest of the parent's batch, or a bash in a second run over the same container, is not held off by the parent's. A tool whose state outlives a batch guards it itself, with a mutex around the command or by telling the model the previous one is still running, and names a resource as well so that within a batch the model's order holds:

func (s *Shell) Execute(ctx context.Context, call agenttool.Call) (agenttool.Result, error) {
	if !s.mu.TryLock() {
		return agenttool.Result{}, errors.New("the previous command is still running")
	}
	defer s.mu.Unlock()
	return s.run(ctx, call)
}

The refusal is an error, not text, so the model sees it as one and retries rather than reading it as the command's answer.

A tool that panics completes with a PanicError whose message is one line; the stack is on the value for errors.As. Results is the shortcut when progress is not needed. A loop that owns its own scheduling needs only the interface.

MCP

The native contract is the centre and MCP is an edge: an MCP tool is a name, a description, an input schema and a call over a transport, which is a subset of Tool. Both adapters use github.com/modelcontextprotocol/go-sdk and map, nothing more.

// Consume: a remote server's tools as Tool values.
s, err := mcpclient.Connect(ctx, &mcp.CommandTransport{Command: cmd}, mcpclient.WithPrefix("fs"))
tools := s.Tools()   // fs__read, fs__write, ...
info := s.ServerInfo() // the server's own name and version, for a host naming several

// Serve: Go tools over MCP.
server, err := mcpserver.NewServer("my-tools", "0.1.0", ReadFile)
err = server.Run(ctx, &mcp.StdioTransport{})

mcpclient maps text, image, audio and resource content to output parts, isError to a returned error, a tool's annotations to agenttool.Annotations, progress notifications to the call's progress callback, and refreshes its snapshot on tool-list-changed. The hints say a search is read-only and a delete_repo destructive, which is what a permission layer keys on instead of a per-server name list; AnnotationsOf reads them from any tool and mcpserver serves the ones a Go tool carries, so they survive a round trip. They are the server's word, so a policy may use them to be stricter and may not use them alone to allow a call. The refresh is a round trip: Await blocks until it has landed, and a call that returns after the notification arrived waits for it. A server need not notify before it answers, and the reference Go SDK arms the notification on a 10 ms timer, so a call whose list looks unchanged waits up to mcpclient.DefaultNotificationGrace for one before returning; a tool that adds a tool then returns with the list already current. WithNotificationGrace bounds or disables that wait, which is skipped for a read-only tool and for a server that advertises no tool-list-changed notification. mcpserver validates arguments against each tool's schema, maps outputs back to MCP content and forwards progress when the request carries a token. One Tool value serves every client that connects, so the call's context carries the session it arrived on: mcpserver.SessionFrom(ctx) is what a tool holding a working directory, a container or a shell keys that state on, and two editor windows then get two shells instead of one.

A record crosses too. The tool that most wants one, a shell inside a sandbox recording which container served the call, is the tool most likely to be behind a server, so mcpserver puts the Record of a Recordable result under one reserved _meta key and mcpclient makes it the Details on its side, where RecordOf reads it as it would in process. The call is on the context in a served tool, as under the executor, and a recorder a host installs with ContextWithRecorder on the context it opens the session with, the one it gives server.Run over stdio or the initialize request's over streamable HTTP, reaches WriteRecord in every call; that is how the SDK behaves today rather than a guarantee, and a host that wants none of it installs the recorder in a Wrap around its tools. make interop checks both against the upstream reference server and the MCP Inspector over stdio.

A server behind OAuth

mcpclient has no OAuth of its own yet (#58). A hosted server that requires it answers the initialize request 401, and the Go SDK can do the rest on a streamable-HTTP transport: metadata discovery, client registration, PKCE, refresh and step-up on 403. It is the SDK's auth.AuthorizationCodeHandler, and since Connect takes any transport, a harness can hand it one today. The SDK leaves one part to the caller, the fetcher, which shows the user the authorization URL and returns the code the authorization server redirects back with:

h, err := auth.NewAuthorizationCodeHandler(&auth.AuthorizationCodeHandlerConfig{
	PreregisteredClient: &oauthex.ClientCredentials{ClientID: "my-harness"},
	RedirectURL:         "http://127.0.0.1:8765/callback",
	AuthorizationCodeFetcher: func(ctx context.Context, args *auth.AuthorizationArgs) (*auth.AuthorizationResult, error) {
		if ask, ok := agenttool.ElicitorFrom(ctx); ok {
			// Mid-session: the user of the call that met the 401.
			ans, err := ask(ctx, agenttool.Elicitation{Message: "Sign in to the deploy server", URL: args.URL})
			if err != nil {
				return nil, err
			}
			if ans.Action != agenttool.ActionAccept {
				return nil, fmt.Errorf("authorization: %s", ans.Action)
			}
		} else {
			// At Connect: there is no call, so nobody to ask through the harness.
			fmt.Fprintln(os.Stderr, "sign in at", args.URL)
		}
		return callback.Wait(ctx) // yours: the code and state that reach RedirectURL
	},
})
s, err := mcpclient.Connect(ctx, &mcp.StreamableClientTransport{Endpoint: endpoint, OAuthHandler: h}, mcpclient.WithElicitation())

The handler keeps its tokens in memory, so a restart means consenting again. mcpclient.StoreTokens keeps them in a TokenStore instead. Call it on the config before building the handler:

cfg := &auth.AuthorizationCodeHandlerConfig{ /* as above */ }
key := mcpclient.TokenKey{Endpoint: endpoint, Subject: userID} // Subject "" with one user
err := mcpclient.StoreTokens(ctx, cfg, store, key, func(err error) {
	log.Printf("saving the grant for %s: %v", endpoint, err) // the error, never the record
})
h, err := auth.NewAuthorizationCodeHandler(cfg)

A stored grant starts the connection authorized. Each refresh that changes the token is saved, including a refresh token the provider rotates, so the grant still refreshes after the next restart. The record holds the token endpoint and the client credentials as well as the token, because the SDK learns them only while it authorizes. MemoryTokenStore is for tests. Where a real store keeps its secrets, and how it encrypts them, is the host's choice. A store shared by several processes needs its own locking, because a provider that rotates refresh tokens accepts each one only once.

What this route does not do, all of it tracked in #58:

  • Nobody to ask at Connect. The first 401 comes from initialize, before any call, so no elicitor is on the fetcher's context. A fetcher that waits for an answer holds Connect until its context ends. A daemon or a hosted front has no browser to open and no terminal to print to, so it has to authorize some other way before it connects.
  • Mid-session it can ask, as the same user only. A 401 after the provider refuses the refresh token, or a 403 step-up, runs the fetcher on the context of the call that met it. A token that expires with no refresh token never reaches the server: the SDK's token source fails the request instead, so nothing asks. With StoreTokens the expired token is sent, the server answers 401, and the fetcher runs as it does for a refused refresh. The elicitor is there, and with WithElicitation so is the call, so the question lands in the record under that call. The server pins its session to the user of the token that opened it. A token for anyone else is refused with 403 "session user mismatch", and the call fails.
  • One connection, one user. The handler reads its token source on the connection's context, never the call's, so it cannot pick a token per caller. A host serving several users opens a connection for each.
  • Nothing receives the redirect. RedirectURL is yours to serve: a loopback listener (RFC 8252) for a harness on the user's machine, or a route on the front that hosts it.
  • A URL answer is only an action. Accepting says the user went to the page. The code still arrives at the redirect, and it and the tokens never pass through Answer or the record.

Development

make check    # gofmt, vet, deps, staticcheck, govulncheck, race tests, every module
make interop  # upstream MCP interoperability; needs npx and the network

See CONTRIBUTING.md.

License

MIT. See LICENSE.

Documentation

Overview

Package agenttool is the tool contract for Go agents over Open Responses: what a tool is, how a typed Go function becomes one, how its JSON Schema is generated and validated, and how a batch of calls executes.

The package imports openresponses and the standard library only, so a tool written with New can be handed to any loop that speaks Open Responses. agentturn is one such loop and depends on this module; the reverse never holds, and a test enforces the boundary. The MCP adapters, mcpclient and mcpserver, are nested modules that map to and from this contract.

type ReadFileArgs struct {
	Path     string `json:"path" desc:"Absolute path to read"`
	MaxBytes int    `json:"max_bytes,omitempty" desc:"Stop after this many bytes"`
}

var ReadFile = agenttool.New("read_file", "Read a file from disk",
	func(ctx context.Context, a ReadFileArgs) (string, error) { ... })

Errors returned from Execute become error outputs the model sees; a tool never encodes an error as normal content.

Index

Constants

View Source
const DefaultMaxParallel = 8

DefaultMaxParallel bounds concurrent tool calls in a batch when the caller sets no limit.

Variables

View Source
var NoArgsSchema = json.RawMessage(`{"type":"object","properties":{},"required":[]}`)

NoArgsSchema is the parameters schema of a tool that takes no arguments, and what Definition serves for a nil Parameters, so "no arguments" is one definition however the tool was built: it is the schema New reflects from NoArgs, and mcpserver serves the same bytes for a nil schema.

Functions

func ConfinedBy added in v0.0.6

func ConfinedBy(ctx context.Context, t Tool, args json.RawMessage) (bool, string)

ConfinedBy reports whether a call of t with args will run confined, and what confines it. A tool that does not implement Confined reports false and "", which says the tool does not claim a sandbox rather than that it has none: a policy treats both as unconfined and may not read a false as permission to skip a prompt it would otherwise raise.

func ContextWithElicitor added in v0.0.9

func ContextWithElicitor(ctx context.Context, fn Elicitor) context.Context

ContextWithElicitor returns ctx carrying fn as the elicitor a tool asks through. A nil fn removes any elicitor already on the context, so a tool under it asks nobody.

func ContextWithRecorder added in v0.0.6

func ContextWithRecorder(ctx context.Context, fn RecordFunc) context.Context

ContextWithRecorder returns ctx carrying fn as the recorder that WriteRecord calls. A harness installs it around a tool call, per call, so the record it writes can be filed beside that call; a nil fn removes any recorder already on the context.

func Decode

func Decode[T any](raw json.RawMessage) (T, error)

Decode unmarshals the raw arguments into T. Empty arguments decode as an empty object. Errors are phrased for the model.

func Definition

func Definition(t Tool) *openresponses.FunctionTool

Definition builds the function tool that describes t on a request. A nil Parameters is served as NoArgsSchema rather than null, so the definition's parameters is always an object schema and its hash does not depend on which constructor built the tool.

func IsSequential

func IsSequential(t Tool) bool

IsSequential reports whether t asks to run alone.

func IsStrict

func IsStrict(t Tool) bool

IsStrict reports whether t's schema is strict.

func Progress

func Progress(ctx context.Context, r Result)

Progress reports progress on the call attached to ctx. It is a no-op when there is no call or the caller did not ask for updates.

func ResourceOf added in v0.0.6

func ResourceOf(t Tool) string

ResourceOf returns the shared state t names, or "" when it names none. A Sequential tool takes the whole batch and reports "" here whatever it says, so a caller scheduling by resource does not have to check both.

func SchemaFor

func SchemaFor[T any](opts ...Option) (json.RawMessage, error)

SchemaFor returns the JSON Schema for T. See SchemaOf.

func SchemaOf

func SchemaOf(t reflect.Type, opts ...Option) (json.RawMessage, error)

SchemaOf returns the JSON Schema object New would use for an argument type t: the one t supplies when it implements Schemer, otherwise the reflected schema of Reflect. Of the options only WithStrict applies, and not to a Schemer's own schema.

Exported fields become properties named by their json tag. A "desc" tag becomes the description and an "enum" tag, comma separated, becomes the enum. Embedded structs are flattened under encoding/json's rules: of several fields promoted under one name the shallowest wins, a tagged one wins among equals, and the rest of a tie is dropped as the encoder would drop it. Supported kinds are bool, the integer and float kinds, string, slices and arrays, maps with string keys, nested structs, pointers, time.Time (a date-time string), []byte (a string), json.RawMessage and interfaces (any value) and types implementing encoding.TextMarshaler (a string).

In strict mode every property is required, additionalProperties is false on every object, pointer fields are nullable with null added to their enum when they have one, and maps are rejected because strict mode cannot express them.

Outside strict mode a field is optional when its tag says omitempty or omitzero or when it is a pointer.

The shape of the output, member order, the order of required, enum and a type union, and the strict rules stated as schema rules rather than reflection rules, is specified in docs/rfcs/0001-tool-contract.md under "Schema generation", and testdata/schema/manifest.json describes the golden corpus in those terms.

func WithCall

func WithCall(ctx context.Context, call Call) context.Context

WithCall attaches the call to ctx so the tool function can find it with CallFrom. A New tool does this before calling its function.

func WriteRecord added in v0.0.6

func WriteRecord(ctx context.Context, details Recordable) error

WriteRecord writes details to the record now, through the recorder on ctx, and returns when the write is durable. It is what a tool calls during Execute for the handle to something it has just started: a process group after the fork, a temporary directory after the mkdir, a remote job ID once the server has answered. A tool that is killed mid-call leaves nothing behind otherwise, since Result.Details is read only when the call ends, and that is the one case where the handle is wanted.

It is a no-op returning nil when no recorder is installed, so a tool can call it unconditionally and tests need to install nothing. The namespace and JSON are RecordOf's, so a value written here and returned again as Result.Details is recorded under one namespace twice, the later write describing the call as it ended.

Types

type Action added in v0.0.9

type Action string

Action is what the user did with an Elicitation.

const (
	// ActionAccept is an answer: the user submitted the form, or
	// confirmed, and [Answer.Content] holds what they gave.
	ActionAccept Action = "accept"
	// ActionDecline is an explicit no.
	ActionDecline Action = "decline"
	// ActionCancel is no answer: the user dismissed the question, or
	// nobody was asked. A harness that cannot put a question to anyone
	// answers it, rather than [ActionDecline], which claims a choice.
	ActionCancel Action = "cancel"
)

type Annotated added in v0.0.6

type Annotated interface {
	Annotations() Annotations
}

Annotated is implemented by a tool that carries Annotations.

type Annotations added in v0.0.6

type Annotations struct {
	// Title is a human-readable name for display.
	Title string
	// ReadOnly says the tool does not modify its environment.
	ReadOnly bool
	// Destructive says the tool may make destructive updates, rather
	// than only additive ones. It is meaningful only when ReadOnly is
	// false, and MCP's default for a tool that carries annotations
	// without this one is true.
	Destructive bool
	// Idempotent says that calling the tool again with the same
	// arguments has no further effect. It is meaningful only when
	// ReadOnly is false.
	Idempotent bool
	// OpenWorld says the tool may interact with entities outside a
	// closed domain, as a web search does and a memory tool does not.
	// MCP's default for a tool that carries annotations without this one
	// is true.
	OpenWorld bool
}

Annotations are the behavioural hints a tool carries: what a policy layer keys on when it wants to treat a search differently from a delete. The fields are MCP's tool annotations, so a remote tool's hints survive the adapter in both directions, and a Go tool may set them with WithAnnotations.

They are hints and never authoritative. MCP's own specification says a client must not make tool-use decisions on the annotations of an untrusted server, and a Go tool's are only as good as its author, so a policy may use them to be stricter and must not use them alone to allow a call. The zero value is a tool that says nothing, which is not the same as a tool that says it is harmless: ReadOnly false means "unstated" as often as "writes".

func AnnotationsOf added in v0.0.6

func AnnotationsOf(t Tool) Annotations

AnnotationsOf returns t's annotations, or the zero value when it carries none, which says nothing about the tool rather than saying it is harmless.

type Answer added in v0.0.9

type Answer struct {
	Action Action
	// Content is the submitted form, a JSON object matching the
	// question's Schema, when the action is [ActionAccept]; nil
	// otherwise.
	Content json.RawMessage
}

Answer is the user's reply to an Elicitation.

type Call

type Call struct {
	// ID is the call_id of the function_call item.
	ID string
	// Args is the raw JSON arguments object as the model wrote it.
	Args json.RawMessage
	// IdempotencyKey names the logical operation this call is, for a
	// tool that deduplicates on it; see [ReplayKeyed]. The harness mints
	// it and keeps it the same when it runs this call again, and a new
	// call from the model, which has a new ID, gets a new key. It is
	// opaque, and it is not the ID: a call ID is unique within one
	// response, which is too narrow to deduplicate against a service
	// that outlives the session. Empty means the harness supplies none,
	// and a keyed tool then deduplicates nothing.
	IdempotencyKey string
	// OnUpdate, when set, receives progress before the final result. It
	// may be called from the tool's goroutine; the caller serialises it.
	OnUpdate func(Result)
}

Call is one invocation of a tool.

func CallFrom

func CallFrom(ctx context.Context) (Call, bool)

CallFrom returns the call attached to ctx, if any.

func (Call) Update

func (c Call) Update(r Result)

Update reports progress to the caller when it asked for it.

type Confined added in v0.0.6

type Confined interface {
	Confined(ctx context.Context, args json.RawMessage) (bool, string)
}

Confined is implemented by a tool that knows whether a call will run inside a sandbox, and says so before it runs. A permission layer can then ask about the calls that leave the sandbox without knowing the product's own argument for leaving it: both reference agents put an OS sandbox under one shell tool and make the escape hatch an argument of that tool, so a shared preset that cannot see confinement either prompts for every harmless command or names one product's field and serves only that product.

Confined answers for the call args describe, with the context the call will run under, since confinement can depend on what the host put there. The string names what confines it, "seatbelt", "landlock+seccomp", "container:agent-sandbox", for the prompt and the record; it is empty when the answer is false.

A tool that runs a sandbox implements it. A tool that does not is not expected to, and ConfinedBy answers false for it, which a policy reads as unconfined, since the safe mistake is to ask. Nothing here enforces anything: this is what a tool reports, never what it runs.

type Elicitation added in v0.0.9

type Elicitation struct {
	// Message is the question, for the user.
	Message string
	// Schema is the JSON Schema of the answer a form asks for, an
	// object of flat properties; nil when the question is not a form.
	Schema json.RawMessage
	// URL is the page the user is sent to when the question is answered
	// out of band, which is how a tool asks for a credential without
	// seeing it; empty for a form.
	URL string
}

Elicitation is a question a tool asks the user in the middle of a call, and needs answered before the call can go on: "delete the branch?", or a form for the details a request left out. It reaches the harness through the Elicitor on the call's context, so the harness can show it, apply its policy to it and record it under the call that asked, rather than meeting it as a callback inside a tool that is running.

type Elicitor added in v0.0.9

type Elicitor func(ctx context.Context, q Elicitation) (Answer, error)

Elicitor answers a question a tool asks the user mid-call. A harness installs one with ContextWithElicitor around a call, as it does a recorder, and a tool that needs an answer calls it; mcpclient calls it for a server's elicitation. ctx is the call's, with the call on it through CallFrom, so the harness can file the question and the answer under the call that asked and say who answered.

It may be called for two questions of one call at once, since a tool can ask several together; a harness that shows one question at a time serialises them itself.

An error is the harness failing to ask, not the user saying no, and the tool sees it as an error; a refusal is an Answer with ActionDecline or ActionCancel.

func ElicitorFrom added in v0.0.9

func ElicitorFrom(ctx context.Context) (Elicitor, bool)

ElicitorFrom returns the elicitor on ctx, if any. A tool without one has nobody to ask, and treats the question as ActionCancel.

type Event

type Event struct {
	Index  int
	Final  bool
	Result Result
	Err    error
}

Event is one step of a batch: a progress update when Final is false, otherwise the completion of the job at Index. Completions arrive in completion order, not job order.

type Executor

type Executor struct {
	// MaxParallel bounds concurrency; zero means [DefaultMaxParallel].
	MaxParallel int
	// Sequential forces every batch to run one job at a time in order.
	// A batch containing a [Sequential] tool runs that way regardless.
	// Serialising one tool against itself is [Resource], which costs the
	// rest of the batch nothing.
	Sequential bool
	// Recorder, when set, is installed on every job's context with
	// [ContextWithRecorder], so a tool that calls [WriteRecord] while it
	// runs reaches the host without the host threading a context per
	// job. It is left unset by a caller that has no record to write, and
	// a recorder already on the context passed to [Executor.Execute] is
	// then used as it is.
	Recorder RecordFunc
	// OnStart, when set, is called the moment a job is handed to its
	// tool: after the job has taken a slot in the batch's bound and its
	// turn in its chain, with the [Call] on ctx, and before Execute. A
	// batch is handed to the executor all at once and a job whose turn
	// has not come has not started, so this is where a harness records
	// that a call was dispatched: a call cut off before OnStart never
	// reached its tool, and one cut off after may have.
	//
	// An error stops the job: it completes with that error and the
	// tool does not run, so a dispatch the harness could not make
	// durable is never followed by a side effect the record cannot
	// see, which is the direction that matters. A panic in it is the
	// harness's and completes the job with an error naming OnStart,
	// not a [PanicError] blamed on the tool.
	//
	// It runs on the job's goroutine, holding the job's slot in the
	// bound and its turn in its chain while it runs, so a durable write
	// there delays that job and whatever waits behind it, every later
	// job in a serial batch, and nothing else. Jobs start concurrently,
	// so it must be safe to call concurrently. It is not called for a
	// job cancelled before its turn, which completes with the
	// cancellation as its error. The job it receives is the job as the
	// tool will see it, its Call.OnUpdate the executor's own forwarder,
	// so an update sent from here reaches the consumer as progress
	// before the tool has run.
	OnStart func(ctx context.Context, job Job) error
}

Executor runs batches of tool calls.

func (Executor) Chains added in v0.0.15

func (e Executor) Chains(jobs []Job) [][]int

Chains returns the chains Executor.Execute runs jobs in, each a list of indices into jobs in the model's order, together covering every job once. The jobs of one chain run one after the other, each handed to its tool once the one before it has completed and the consumer has taken its final event; chains run alongside each other, within MaxParallel. A serial batch, from Executor.Sequential, a Sequential tool or a MaxParallel of one, is one chain of every job in order. Otherwise the jobs whose tools name one Resource form a chain, placed where its first job is, and every other job, one with a nil tool included, is a chain of its own. A batch of no jobs has no chains.

It is the grouping Execute uses, exposed so that a harness which must know the job before a given one in its chain, to wait for that job to be settled before dispatching the next, reads it here rather than restating the rules and drifting from them (#66). It reads the tools' properties and runs nothing.

func (Executor) Execute

func (e Executor) Execute(ctx context.Context, jobs []Job) iter.Seq[Event]

Execute runs jobs and yields their events from the caller's goroutine, so a consumer never sees two events at once. Progress updates from a tool that calls Call.OnUpdate are forwarded as non-final events; the executor installs its own OnUpdate and chains to the one on the job, if any, from the yielding goroutine. A tool that panics completes with an error. Breaking out of the loop cancels the batch and waits for running tools to return. Each job's tool finds its Call on the context with CallFrom, and Executor.Recorder on it with RecorderFrom.

Jobs whose tools name the same Resource run one after the other in the model's order and alongside the rest of the batch; jobs that name none run in parallel up to MaxParallel. A serial batch, from Executor.Sequential or a Sequential tool, runs every job in the model's order and ignores resources, since it is already stricter. Both orderings are within the batch given here: two Execute calls, in one executor or two, order nothing against each other, so a tool whose state outlives a batch guards it itself.

func (Executor) Results

func (e Executor) Results(ctx context.Context, jobs []Job) ([]Result, []error)

Results collects the final results of a batch in job order. It is the convenience over Executor.Execute for callers that do not need progress.

type Job

type Job struct {
	Tool Tool
	Call Call
}

Job is one call of a batch: the tool to run and the call to run it with.

type NoArgs

type NoArgs struct{}

NoArgs is the argument type of a tool that takes no arguments.

type Option

type Option func(*options)

Option configures a tool built by New or NewFunc, or a schema from Reflect, SchemaOf and SchemaFor. Each says which options apply to it.

func WithAnnotations added in v0.0.6

func WithAnnotations(a Annotations) Option

WithAnnotations sets the behavioural hints the tool carries; see Annotations, which a policy layer may read and must not trust alone.

func WithCloser added in v0.0.9

func WithCloser(fn func() error) Option

WithCloser gives the tool something to release: the tool is then an io.Closer whose Close calls fn, so Set.Close releases what the tool owns across calls, a persistent shell or a container. Without it the tool is not a closer, and a nil fn is no closer.

It is how a tool built here comes to own something. Embedding the tool in a struct that adds Close compiles, and drops every other property the tool declares; see Wrap.

func WithConfined added in v0.0.9

func WithConfined(fn func(ctx context.Context, args json.RawMessage) (bool, string)) Option

WithConfined has the tool report, through fn, whether a call will run inside a sandbox and what confines it; see Confined, which says what the answer means and what it does not. A tool built without it, or with a nil fn, answers as ConfinedBy does for a tool that declares nothing: false and "".

func WithParameters

func WithParameters(schema json.RawMessage) Option

WithParameters replaces the reflected schema of a New tool with schema. Arguments are then not validated before decoding, because the tool cannot know what the schema promises.

func WithReplay added in v0.0.9

func WithReplay(fn func(ctx context.Context, args json.RawMessage) Replay) Option

WithReplay has the tool report, through fn, whether a call that may already have run can run again; see Replayable. A tool built without it, or with a nil fn, reads as ReplayUnknown, and so does an answer this package does not know.

func WithResource added in v0.0.6

func WithResource(name string) Option

WithResource names the shared state a call of the tool touches, so Executor runs two calls of it in one batch one after the other while the rest of the batch runs alongside; see Resource, including what a tool whose state outlives a batch owes itself. An empty name is no resource.

func WithSequential

func WithSequential() Option

WithSequential marks the tool Sequential.

func WithStrict

func WithStrict() Option

WithStrict sets the strict flag on the function tool, and has New reflect the schema under the strict rules so the flag is true of it. On a schema the author supplied, through NewFunc, WithParameters or a Schemer, it is the author's claim and nothing checks it.

func WithoutValidation

func WithoutValidation() Option

WithoutValidation skips a New tool's argument check against the reflected schema, leaving the decoder as the only guard.

type PanicError added in v0.0.3

type PanicError struct {
	Tool  string
	Value any
	Stack []byte
}

PanicError is the error a job completes with when its tool panicked. Error reports the tool and the panic value in one line, which is what the model sees; the stack is kept for hosts and subscribers that recover it with errors.As, and never reaches the conversation.

func (*PanicError) Error added in v0.0.3

func (e *PanicError) Error() string

type ProgressInfo added in v0.0.3

type ProgressInfo struct {
	Progress float64
	Total    float64
	Message  string
}

ProgressInfo, when set as the Details of a progress update, carries the numbers behind it: how far the tool is, out of how much, and a message. The fields mirror the MCP progress notification so the two adapters forward it in either direction; a tool with no numbers to report leaves Details unset and sends text alone.

type Property

type Property struct {
	Name   string
	Schema *Schema
}

Property is one named member of an object schema.

type Record added in v0.0.5

type Record struct {
	NS   string
	Data json.RawMessage
}

Record is the durable form of a Recordable Details value: the namespace and the JSON a recorder writes under it.

func RecordOf added in v0.0.5

func RecordOf(details any) (*Record, error)

RecordOf returns the record of details when it implements Recordable, and nil when it is nil or any other value, which is not an error: most Details are for subscribers in the same process. An empty namespace or a value that does not marshal is an error.

type RecordFunc added in v0.0.6

type RecordFunc func(ctx context.Context, rec *Record) error

RecordFunc writes one record durably. A harness installs it with ContextWithRecorder so that a tool can put a handle on the record at the moment the side effect begins, rather than at the end of the call, which is where RecordOf reads Result.Details. It is called from the tool's goroutine, possibly more than once and possibly concurrently with another call's, so an implementation must be safe for concurrent use and must have written the record durably before it returns; that is the whole point of the seam. The error it returns reaches the tool, which decides whether a record it could not write fails the call.

The call the record belongs to is on the context: a recorder reads it with CallFrom, which Executor fills in for every tool it runs.

func RecorderFrom added in v0.0.6

func RecorderFrom(ctx context.Context) (RecordFunc, bool)

RecorderFrom returns the recorder on ctx, if any. A tool that would spend real work building its details can ask first; WriteRecord makes the same check.

type Recordable added in v0.0.5

type Recordable interface {
	RecordNS() string
}

Recordable is implemented by a Details value that is meant to outlive the run. A recorder that does not know the type can still write it, as a namespaced JSON entry beside the call it came from, through RecordOf. RecordNS names the entry's namespace, "owner:kind" by convention, and must not be empty. The data is the value's JSON as json.Marshal produces it, so a type shapes it with MarshalJSON like anywhere else; a MarshalJSON on a pointer receiver applies only when Details holds the pointer. Details for in-process subscribers alone, such as ProgressInfo or a live handle, do not implement it and are not recorded.

type Replay added in v0.0.9

type Replay int

Replay is a tool's answer to whether a call that may already have run can run again: after a kill between the dispatch and the result, when a harness resuming the session cannot know whether the side effect happened, or after an error that may have come after it.

const (
	// ReplayUnknown says nothing about running the call again, so it
	// must not be run again: the outcome of the first attempt is
	// ambiguous, and the harness completes the call telling the model
	// so, for it to check before it tries again. It is the default.
	ReplayUnknown Replay = iota
	// ReplaySafe says that running the call again with these arguments
	// has no effect beyond the first run's: it reads, or it sets state
	// to a value the arguments fix. Its output may differ.
	ReplaySafe
	// ReplayKeyed says the tool deduplicates on [Call.IdempotencyKey]:
	// a second call with the key of a call that completed has no
	// further effect. It should return that call's outcome, its error
	// included, should refuse the key while the first call is still
	// running, and should refuse it with different arguments. It is
	// safe to run again only with the key the first attempt carried,
	// and without one it is [ReplayUnknown].
	ReplayKeyed
)

func ReplayOf added in v0.0.9

func ReplayOf(ctx context.Context, t Tool, args json.RawMessage) Replay

ReplayOf reports whether a call of t with args may run again. A tool that does not implement Replayable, or answers with a value this package does not know, reports ReplayUnknown, so a value added later reads as the safe mistake to an older harness.

func (Replay) String added in v0.0.9

func (r Replay) String() string

type Replayable added in v0.0.9

type Replayable interface {
	Replay(ctx context.Context, args json.RawMessage) Replay
}

Replayable is implemented by a tool that knows whether a call can run again, and says so for the call args describe, with the context it would run under. It is per call because the answer is: a shell runs ls again harmlessly and git push not, and a tool that writes a file is safe to repeat and one that appends to it is not.

It is a claim, and nothing weaker stands in for it. ReplayOf answers ReplayUnknown for a tool that does not implement it, even one whose Annotations say ReadOnly or Idempotent: those are hints a policy may use to be stricter, and running a call twice is an allow. A tool from mcpclient reads as ReplayUnknown, since MCP carries no such claim and an untrusted server's hints are not one.

type Resource added in v0.0.6

type Resource interface {
	Resource() string
}

Resource is implemented by a tool that owns shared state, naming it, so that Executor runs two calls of one batch that touch the same state one after the other, in the model's order, while everything else in the batch runs alongside them. The name is free-form and "<kind>:<id>" by convention, "shell:session" or "container:47"; two tools that return the same name share the lock, which is how a shell tool and a tool that restarts that shell stay apart.

The scope is one batch. The executor sees one batch at a time and orders nothing across two: a call in a sub-agent's batch, which runs under the parent call and alongside the rest of the parent's batch, or a call in a concurrent run over the same workspace, is not held off by this. A tool whose state outlives a batch, a persistent shell, a container, a pool, guards that state itself, with a mutex around the command or by answering the second call with "the previous command is still running", and declares a resource as well so that within a batch the model's order holds. A tool that pools, and can serve two calls at once, declares nothing.

It is the answer for a tool that must not run twice at once, and Sequential is the answer for a tool that must not run while anything else does. A tool that reports both is sequential: the batch, not the resource, is what it claims. A tool that reports neither runs in parallel with everything, which is the default and stays the default.

Whether a second call waits or is refused stays the tool's choice: nothing here stops a tool from answering "the previous command is still running" instead of blocking, which is what a persistent shell usually wants the model to see.

type Result

type Result struct {
	// Output is what the model sees.
	Output openresponses.FunctionCallOutputData
	// Details is app-only data for subscribers and fronts. It is never
	// sent to the model. A value that implements [Recordable] can also
	// be written to a session by a recorder that does not know its
	// type; see [RecordOf].
	Details any
	// Terminate hints that the loop should stop after this batch instead
	// of calling the model again. The loop honours it only when every
	// result in the batch sets it.
	Terminate bool
}

Result is what a tool produced.

func ErrorResult

func ErrorResult(err error) Result

ErrorResult builds the output the model sees when a tool fails. The text form is "Error: <message>" so the model can tell it apart from a normal result and retry.

func Output

func Output(v any) (Result, error)

Output converts a Go value into a Result following the rules of New.

func Parts

func Parts(parts ...openresponses.Content) Result

Parts builds a result whose output is a list of content parts.

func Text

func Text(s string) Result

Text builds a result whose output is a string.

type Schema

type Schema struct {
	// Type is a JSON Schema type name, or empty for any value.
	Type string
	// Nullable adds "null" to the type, as strict mode requires for
	// optional fields.
	Nullable    bool
	Description string
	Format      string
	Enum        []any
	Properties  []Property
	Required    []string
	// AdditionalProperties and NoAdditional share the JSON key
	// "additionalProperties". NoAdditional emits false and wins when both
	// are set, as strict mode requires; AdditionalProperties emits a
	// schema for the values of a map. Neither set, the key is omitted and
	// unknown properties are allowed.
	AdditionalProperties *Schema
	NoAdditional         bool
	Items                *Schema
}

Schema is a JSON Schema fragment as the generator builds it. Keys are emitted in a fixed order and properties keep struct field order, so the output is stable across runs and readable in a request.

The type can be built by hand for Schema.Validate. A nil or empty slice is omitted from the JSON, so a hand-built object schema emits "properties" and "required" only when they are set, while the generator sets both to empty slices and always emits them. Type "" accepts any value.

func Reflect

func Reflect(t reflect.Type, opts ...Option) (*Schema, error)

Reflect returns the schema tree that SchemaOf serialises, for callers that want to validate with it. t must be a struct or a pointer to one; Schemer is not consulted, since a Schemer supplies JSON, not a tree. Of the options only WithStrict applies.

func (Schema) MarshalJSON

func (s Schema) MarshalJSON() ([]byte, error)

MarshalJSON emits the schema with a fixed key order. The receiver is a value so that a Schema value, on its own or inside another struct, marshals the same way as a pointer; the validate methods take a pointer because a nil Items or AdditionalProperties accepts anything.

func (*Schema) Validate

func (s *Schema) Validate(v any) error

Validate checks a decoded JSON value (maps, slices, strings, json.Number or float64, bools, nil) against s: its type, enum, required properties, additionalProperties when false, and its items and properties recursively. A schema with no Type accepts anything; one whose Type is not a JSON Schema type name accepts nothing, so a misspelt hand-built schema fails on its first use rather than silently passing everything.

func (*Schema) ValidateJSON

func (s *Schema) ValidateJSON(raw json.RawMessage) error

ValidateJSON checks raw against s. Empty raw is an empty object.

type Schemer

type Schemer interface {
	JSONSchema() json.RawMessage
}

Schemer is implemented by argument types that supply their own JSON Schema instead of the reflected one.

type Sequential

type Sequential interface {
	Sequential() bool
}

Sequential is implemented by tools that must not run alongside other tools in the same batch. When any tool in a batch reports true, the whole batch runs one call at a time in the model's order.

type Set

type Set []Tool

Set is a list of tools with lookup by name.

func (Set) Close added in v0.0.6

func (s Set) Close() error

Close closes every tool in the set that implements io.Closer, in order, and returns their errors joined; a tool that implements it releases what it owns beyond one call, such as a container or a persistent shell. It is the host's to call, once, when the session that built the tools is over, and never a loop's: the loop's runs come and go while the tools stay. Closing a set twice is the tools' business, as is a call that arrives after.

A tool from mcpclient is not a closer; the remote's session is closed through mcpclient.Remote.Close, which serves every tool of that server.

func (Set) Definitions

func (s Set) Definitions() openresponses.Tools

Definitions returns the function tools for a request, in order.

func (Set) Lookup

func (s Set) Lookup(name string) (Tool, bool)

Lookup returns the tool named name.

func (Set) Validate

func (s Set) Validate() error

Validate reports an empty or duplicate name.

type Strict

type Strict interface {
	Strict() bool
}

Strict is implemented by a tool that claims its schema keeps the strict rules (every field required, additionalProperties false, optional fields nullable), so the provider may enforce them. The flag is set on the function tool. New makes the claim true for a schema it reflected under WithStrict; for a schema the author supplied, through NewFunc, WithParameters or a Schemer, the claim is the author's and is not checked.

type Tool

type Tool interface {
	Name() string
	Description() string
	// Parameters is the JSON Schema of the arguments object. nil means
	// the tool takes no arguments.
	Parameters() json.RawMessage
	// Execute runs one call. On error the model sees the error and
	// Result.Output is ignored; Result.Details may still be set for
	// subscribers, as mcpclient does with the raw MCP result. The
	// context is the call's, and its cancellation is the host's
	// interrupt: a tool that owns a process stops it there and keeps
	// anything the tool value owns across calls, which [io.Closer]
	// releases.
	Execute(ctx context.Context, call Call) (Result, error)
}

Tool is something the model can call. Name and Parameters become the function tool on the request; Execute runs one call. Everything else a tool declares is an optional interface, Strict, Sequential, Resource, Annotated, Confined, Replayable and io.Closer, found by type assertion; a struct that embeds Tool forwards these four methods alone and drops all of those, so a tool that stands in for another is built with Wrap.

func New

func New[Args, Out any](name, description string, fn func(context.Context, Args) (Out, error), opts ...Option) Tool

New builds a Tool from a typed function. The schema is reflected from Args at registration time (see SchemaOf); a type that cannot be expressed panics here rather than at call time, like a bad regexp in regexp.MustCompile. Call.Args is validated against that schema, so a missing required property, a wrong type or a value outside an enum is returned as an error the model can retry on, then decoded into Args with the standard decoder. Properties the schema does not name are ignored, or rejected under WithStrict, whose schema says so.

Validation covers reflected schemas only. When Args implements Schemer or the schema comes from WithParameters, the tool cannot know what the schema promises, so arguments go straight to the decoder; WithoutValidation asks for the same on a reflected one.

Out maps to the output the model sees: a string passes through as text, openresponses.Contents goes out as parts, a Result or an openresponses.FunctionCallOutputData is used as is, and anything else is marshalled to JSON.

The function can reach its Call through CallFrom on the context, for the call ID or to report progress.

func NewFunc added in v0.0.3

func NewFunc(name, description string, parameters json.RawMessage, fn func(ctx context.Context, call Call) (Result, error), opts ...Option) Tool

NewFunc builds a Tool from plain values and a function that takes the raw call: the untyped counterpart of New for tools whose schema comes from elsewhere, such as a remote server. parameters is served verbatim and never validated; nil means the tool takes no arguments. WithStrict, WithSequential, WithResource, WithAnnotations, WithConfined, WithReplay and WithCloser apply; the options that shape a reflected schema do not. A nil fn panics here, like a bad schema in New.

func Unwrap added in v0.0.8

func Unwrap(t Tool) Tool

Unwrap returns the tool t was built around by Wrap, and nil when t is not a wrapper. A host that needs the concrete tool behind a chain of wrappers calls it until it returns nil.

func Wrap added in v0.0.8

func Wrap(t Tool, exec func(ctx context.Context, call Call) (Result, error)) Tool

Wrap returns a tool that runs exec in place of t.Execute and is t in every other way: its name, description and parameters, and every property t declares, strict, sequential, resource, annotations, confined, replay and closer, reported exactly as t reports them. It is how a policy that grants on use, a decorator that records, or a proxy that audits stands in for a tool without changing what the executor and a policy layer learn about it.

Embedding Tool in a struct forwards the four methods alone and silently drops every optional interface, so a wrapped bash that was Sequential runs in a parallel batch and nothing fails. The set of optional interfaces is this package's and grows with it, so a wrapper written elsewhere is right only until the next one is added; this one is kept right here, and a test refuses a new optional interface it does not forward.

The wrapper reports each property through the same reader a harness uses, so a property t does not declare reads as its default, which is what t reports too. Closer is presence rather than a value, so the wrapper implements io.Closer exactly when t does, and its Close closes t; a wrapper around a tool that owns nothing owns nothing. Unwrap returns t. A nil exec panics, like a nil function in NewFunc.

type ValidationError

type ValidationError struct {
	// Path locates the offending value: "" for the root, otherwise a
	// dotted property path with [i] for array elements.
	Path string
	Msg  string
}

ValidationError reports arguments that do not satisfy a schema. Its message is phrased for the model, which sees it as the error output and can retry.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error returns "invalid arguments: <path>: <msg>".

Directories

Path Synopsis
mcpclient module
mcpserver module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL