Documentation
¶
Overview ¶
Package executor is the session's execution plane for the extensions' tools: what runs a call, says what a call would touch, and says whether one may run again. The session decides on a call (the policy, the hooks, the record) and the executor carries it out, so the two may sit on different machines: Executor is the seam, and InProcess, the extensions' own tools in this process, is the case with the wire left out.
Which tools are execution is fixed: every tool in an extension's Tools. Tools an extension adds through its Kit options (memory, the sub-agents, skill) are the session's control and never come here.
Bind turns an executor's tools into agenttool tools for the kit, each an adapter whose every call goes through the executor, and keeps the facts each model response's decisions read (Set.PinBatch).
Index ¶
- Constants
- Variables
- func HTTPTransport(endpoint string) sdk.Transport
- func Handler(s *Server, done <-chan struct{}) http.Handler
- type Batcher
- type Bound
- type Call
- type ConnectOptions
- type Dialer
- type Executor
- type Extensions
- type Remote
- func (r *Remote) BatchFacts(ctx context.Context, calls []Call) ([]agenttool.Facts, []error, error)
- func (r *Remote) Box() json.RawMessage
- func (r *Remote) Call(ctx context.Context, c Call) (agenttool.Result, error)
- func (r *Remote) Close() error
- func (r *Remote) Descriptor() workspace.Descriptor
- func (r *Remote) Extensions() []string
- func (r *Remote) FS() fs.FS
- func (r *Remote) Facts(ctx context.Context, c Call) (agenttool.Facts, error)
- func (r *Remote) Replay(ctx context.Context, c Call) agenttool.Replay
- func (r *Remote) Server() (name, version string)
- func (r *Remote) Servers(ctx context.Context) ([]workspace.Server, error)
- func (r *Remote) Start(ctx context.Context, cmd workspace.Command) (workspace.Process, error)
- func (r *Remote) StartServer(ctx context.Context, name string, stderr io.Writer) (workspace.Process, error)
- func (r *Remote) Tools(context.Context) ([]Tool, error)
- type ServeOptions
- type Server
- func (s *Server) AddReceivingMiddleware(mw ...sdk.Middleware)
- func (s *Server) Close() error
- func (s *Server) Connect(ctx context.Context, t sdk.Transport, opts *sdk.ServerSessionOptions) (*sdk.ServerSession, error)
- func (s *Server) Run(ctx context.Context, t sdk.Transport) error
- func (s *Server) Sessions() []*sdk.ServerSession
- type Set
- func (s *Set) ByName(name string) (Bound, bool)
- func (s *Set) Facts(ctx context.Context, name string, args json.RawMessage) (agenttool.Facts, error)
- func (s *Set) Pin(ctx context.Context, info agentturn.ToolCallInfo) (unpin func())
- func (s *Set) PinBatch(ctx context.Context, info agentturn.ToolCallInfo) (context.Context, func())
- func (s *Set) Release(ctx context.Context)
- func (s *Set) ReleaseAll()
- func (s *Set) Tools() []Bound
- type Tool
Constants ¶
const CapabilityKey = "io.github.christopherdavenport.dax/executor"
CapabilityKey is the experimental capability under which an executor served by NewServer says what a session needs of it that MCP has no field for: the workspace's descriptor, the extensions whose tools it runs, and each tool's extension, read-only, facts and strict, in the order the tools run in process; and the resource template its workspace's files are read under (files.go, mcpworkspace's). The go-sdk lists tools sorted by name, and dax's per-extension ReadOnly is not the annotation hint, so neither can be read off the listing.
{"version": 1,
"descriptor": {"kind": "container", "ref": "box", "root": "/work"},
"extensions": ["dax-coding"],
"tools": [{"name": "read", "extension": "dax-coding", "readOnly": true, "facts": true}, ...],
"files": {"uriTemplate": "agentworkspace:///{op}{?path}"},
"maxReadBytes": 2097152}
files came after version 1 was released, as a field a client of version 1 ignores; a client that reads it refuses an executor without it. start came after files, the same way: it says the executor answers the process methods (process.go, mcpworkspace's agentworkspace/process.*), with their bounds (wire.Bounds), and is absent when its workspace cannot start a process; a client refuses to start a process in an executor without it, and starts none anywhere else.
"start": {"maxProcesses": 32, "maxWriteBytes": 1048576, "maxReadBytes": 65536}
maxReadBytes came after start, the same way: the most bytes of a file this session's tools read (read scans, edit rewrites, the bash analyzer reads), which the session asked for and the executor capped (ServeOptions.MaxReadBytes). An executor older than it reports none, and applies its own -max-read-bytes whatever the session asked.
A client says what it asks of the executor under the same key in its own capabilities, at initialize, so that it is the same over stdio and over HTTP:
{"maxReadBytes": 1048576}
where 0 or no field asks for tool.DefaultMaxRead. A client older than that says nothing, and its session's tools read what the executor's ceiling says, as before.
const CapabilityVersion = 1
CapabilityVersion is the version of CapabilityKey's value. A client refuses one it does not know.
const KeepAlive = time.Minute
KeepAlive is how often a client over HTTP pings the executor, well within SessionIdle, so a session idle while its user thinks stays open.
Variables ¶
var ( // ErrUnreachable is a request's when the connection was lost and // dialing again failed; the next request dials again. ErrUnreachable = errors.New("executor unreachable") // ErrChanged is every request's once a redial reached an executor // that is not the one the session started with. ErrChanged = errors.New("the executor changed") )
The errors of a Remote whose connection is lost and not made again.
var SessionIdle = 5 * time.Minute
SessionIdle is how long a session served over HTTP may start no request before Handler closes it and ends its processes: a client that went away without saying so leaves nothing running for long. A var for the tests.
Functions ¶
func HTTPTransport ¶ added in v0.0.18
HTTPTransport is the client's transport to an executor Handler serves at endpoint, an http or https URL.
func Handler ¶ added in v0.0.18
Handler serves s, NewServer's, over MCP's streamable HTTP transport, for `dax execute -listen`, until done is closed. Each client that initializes is a session of its own, on an MCP server of its own (Server), with its own tools, built at the read limit it asked for, and its own processes (process.go keeps them by session and ends them with it); every session shares the workspace, and so the lock its writes take.
A session ends when its client deletes it, or when it has started no request for SessionIdle. Either way its processes end first: a read or a wait on one is a request in flight, which a session's close waits for and which nothing over HTTP cancels when the client goes. The SDK's own idle timeout is not used, since its close would wait on such a request for ever, and it does not count a request that waits as idle. Its tools are closed once it has ended.
It authenticates no one: whoever reaches it runs the tools. What it refuses is a browser's way in: a request that came to a loopback address naming another host (the SDK's DNS-rebinding check, left on), and a cross-origin request from a page.
Types ¶
type Batcher ¶ added in v0.0.8
type Batcher interface {
// BatchFacts is each call's facts claim, as Facts gives it: facts[i]
// and errs[i] are the claim and the claim's error for calls[i]. err
// is the request's: when it is set nothing was read, and every call
// of the batch is blocked.
BatchFacts(ctx context.Context, calls []Call) (facts []agenttool.Facts, errs []error, err error)
}
Batcher is an Executor that reads the facts claims of several calls in one request, as a model response's calls arrive together. A Set reads each response's calls through it when the executor is one (Set.PinBatch), and reads them one by one through Facts when it is not, as InProcess, which has no round trip to save, is not.
type Bound ¶
type Bound struct {
Tool
// Adapter is the tool the kit is given. Its definition, scheduling,
// annotations and claims are the executor's for the tool, and
// running it is Executor.Call. It is not an io.Closer: the executor
// owns closing.
Adapter agenttool.Tool
}
Bound is one of a Set's tools: what the executor said of it, and the adapter that stands for it.
type ConnectOptions ¶ added in v0.0.6
type ConnectOptions struct {
// Name and Version are the client's implementation, the program's.
Name, Version string
// KeepAlive, when positive, is how often the client pings the
// executor: over HTTP (KeepAlive), so the server does not close the
// session as idle. A ping that fails closes the connection.
KeepAlive time.Duration
// Warn, when set, is told in a line that the connection was lost
// and made again, or that the executor that answered was refused;
// and, at the first connection, that its tools read less of a file
// than MaxReadBytes asks (readLimitWarning).
Warn func(string)
// MaxReadBytes is the session's max_read_bytes, which the client
// asks the executor for under CapabilityKey on every connection:
// the executor's tools read up to it, capped at the executor's own
// -max-read-bytes. Zero asks for tool.DefaultMaxRead.
MaxReadBytes int64
// Box, when set, is what the session asks of a box host (`dax box
// serve`), beside MaxReadBytes under CapabilityKey: the profile and
// its tightenings. An executor that is not a box host ignores it.
Box json.RawMessage
}
ConnectOptions name the session to the executor.
type Dialer ¶ added in v0.0.18
Dialer makes a new transport to the executor, for each connection a Remote makes: the first, and each one after the last was lost. A command's is a new process started as the first was; a URL's is a new client of the same endpoint.
type Executor ¶
type Executor interface {
// Tools are the tools the executor runs, in the extensions' order.
Tools(ctx context.Context) ([]Tool, error)
// Facts is the tool's facts claim for the call (agenttool.Factual):
// the zero Facts for a tool that makes none.
Facts(ctx context.Context, c Call) (agenttool.Facts, error)
// Replay is whether the call may run again (agenttool.ReplayOf).
Replay(ctx context.Context, c Call) agenttool.Replay
// Call runs the call. The elicitor and agentturn.Invoke travel in
// ctx, as they do to any tool.
Call(ctx context.Context, c Call) (agenttool.Result, error)
// Descriptor is the workspace the tools act in.
Descriptor() workspace.Descriptor
// Close releases what the tools hold, once.
Close() error
}
Executor runs the extensions' tools on behalf of a session, wherever they are. agenttool.Executor, the batch scheduler, is another thing.
type Extensions ¶ added in v0.0.19
Extensions builds the extensions whose tools a session is served, for a session whose read limit is maxRead (dax-coding's coding.New takes it): `dax execute`'s are the program's extensions as its settings choose them. It is called for every session, and once more when the server is made, to check what it builds.
type Remote ¶ added in v0.0.6
type Remote struct {
// contains filtered or unexported fields
}
Remote is the executor NewServer serves, reached over MCP: `dax execute` in a container or on another host, its tools' facts and replay claims read there, their calls run there, and their stamps made and checked there.
A Remote made by Dial reconnects when its connection is lost (the process that served it exited, its session ended, a ping failed): the next request dials again before it is sent, so nothing that was never sent is lost. A call that was in flight when the connection went returns its error and is not sent again, since it may have run; a reading, which has no effect, is read again once on the new connection. The new connection must be the same executor: the same workspace, extensions and tools, each with the same definition and claims, as the first, since the model was offered those tools and the record names that workspace. One that is not is refused, and every request after fails, without dialing again.
func Connect ¶ added in v0.0.6
Connect connects to the executor at the other end of t and checks that it is one: that it answers the facts method, and that its capability (CapabilityKey) is of a version this client reads and names every tool it lists, no more, each claiming facts exactly when the listed tool does. A server that fails any of these is refused, since a session that took its tools anyway would decide their calls on what the model wrote, not on what they would touch, which is a wider policy than the user's. The caller closes the Remote. A connection Connect makes is its only one: once it is lost, every request fails. Dial reconnects.
func Dial ¶ added in v0.0.18
Dial is Connect over a transport dial makes, and again over a new one whenever the connection is lost (see Remote).
func (*Remote) BatchFacts ¶ added in v0.0.8
BatchFacts reads the calls' claims in the executor in one request (mcpclient's Remote.Facts), within factsTimeout. A call's own error, a tool the executor does not have included, is in errs; the request's, the connection's or the timeout's, is err, and blocks every call of the batch.
func (*Remote) Box ¶ added in v0.0.22
func (r *Remote) Box() json.RawMessage
Box is what the box host in front of the executor resolved for the session at the first connection, as JSON: {"profile", "units": [an envelope per unit]}; nil when no box host is.
func (*Remote) Call ¶ added in v0.0.6
Call runs the call in the executor. Its progress and its questions come back over the connection: the call's OnUpdate and the elicitor in ctx are the session's. A call is sent once: when the connection is lost while it is in flight, its error says so, and it is not sent again, since it may have run.
func (*Remote) Close ¶ added in v0.0.6
Close ends the calls in flight and the connection, once, and stops a redial; nothing is dialed after it. The executor ends the processes it started for the session.
func (*Remote) Descriptor ¶ added in v0.0.6
func (r *Remote) Descriptor() workspace.Descriptor
Descriptor is the workspace the executor's capability names.
func (*Remote) Extensions ¶ added in v0.0.6
Extensions are the extensions whose tools the executor runs, in its order, whether or not they built any.
func (*Remote) FS ¶ added in v0.0.7
FS is the executor's workspace's files, read-only, each request within filesTimeout (files.go), over the current connection (mcpworkspace.NewFS). It implements fs.StatFS, fs.ReadDirFS, fs.ReadFileFS and fs.ReadLinkFS; a name that leaves the workspace is workspace.ErrOutside, as the executor's own file system has it.
func (*Remote) Facts ¶ added in v0.0.6
Facts reads the call's claim in the executor, within factsTimeout. An error, the executor's, the connection's or the timeout's, is returned, and blocks the call.
func (*Remote) Replay ¶ added in v0.0.6
Replay is the executor's replay claim, within factsTimeout; one that cannot be read is agenttool.ReplayUnknown.
func (*Remote) Server ¶ added in v0.0.6
Server is the name and version the executor gave at initialize, the latest connection's.
func (*Remote) Servers ¶ added in v0.0.26
Servers lists the MCP servers the executor's workspace offers (mcpworkspace.ListServers): a box host's units, answered by the box host in front of the executor, or the servers its own workspace offers. An executor whose capability says it offers none, and that no box host is in front of, is not asked: it offers none.
func (*Remote) Start ¶ added in v0.0.9
Start starts cmd in the executor's workspace and returns at once: workspace.Starter's Start, over the connection (mcpworkspace.Start). The process's standard input and output are requests to the executor, a write at a time and a read at a time; its standard error, when cmd.Stream is set, is read in the background and written to cmd.Stream as it arrives. It ends with Close, the end of ctx, cmd.Timeout, the connection's close or the executor's. It is started over the current connection, a new one if that was lost, and lives on that one alone: when it is lost, the executor ends the process with its session, and the process's reads, writes and wait fail with errProcessLost. It is not started again.
An executor that does not answer the process methods (no start in its capability: an older dax execute, or a workspace there that cannot start a process) is refused with an error that is errors.ErrUnsupported; nothing is started anywhere else.
func (*Remote) StartServer ¶ added in v0.0.26
func (r *Remote) StartServer(ctx context.Context, name string, stderr io.Writer) (workspace.Process, error)
StartServer starts the server called name that the executor's workspace offers, as Start starts a command: over the current connection, living on it alone, and refused by an executor that cannot start a process.
type ServeOptions ¶ added in v0.0.6
type ServeOptions struct {
// Name and Version are the server's implementation, the program's.
Name, Version string
// Descriptor is the workspace the tools act in as the session
// records it. The zero value is the workspace's own.
Descriptor workspace.Descriptor
// Done, once closed, ends every process the server started for
// its sessions and every request waiting on one, and refuses
// another start: close it before a session's graceful close
// (Server.Run's, when its context ends), which waits for the
// requests in flight. nil is never.
Done <-chan struct{}
// MaxReadBytes is the ceiling on a session's read limit, set by
// whoever runs the executor (`dax execute -max-read-bytes`): a
// session's tools read what it asks, up to this, and a client that
// asks nothing (an older dax) gets this. Zero is tool.DefaultMaxRead,
// so that reading more than the default is the executor's choice.
MaxReadBytes int64
}
ServeOptions name the server an executor is served as and the workspace it reports.
type Server ¶ added in v0.0.19
type Server struct {
// contains filtered or unexported fields
}
Server is `dax execute`'s MCP server, NewServer's: each session it serves has an MCP server of its own, whose tools are built for that session when it initializes, at the read limit it asked for, and closed when it ends. Every session shares the workspace, the tool.Files over it (and so the lock its writes take), the files served and the process methods.
func NewServer ¶ added in v0.0.6
func NewServer(o ServeOptions, exts Extensions, env extension.ToolEnv) (*Server, error)
NewServer serves exts' tools, built for each session over env as InProcess builds them, as an MCP server: `dax execute`, run where the tools are to act. The tools served are the tools themselves, not adapters, so their facts claims, their replay claims, the stamp a decision's facts put on a call and the check of it when the call runs are all made here, in this process, under this process's stamp key. Each session's server answers mcpserver's facts method and advertises its capability, and says under CapabilityKey what MCP cannot carry. It says the tool list never changes, so a client does not wait for a notification after every call.
A session's tools are built when it initializes, from exts at the session's read limit, with env's MaxReadBytes set to it: what the client asked under CapabilityKey, capped at o.MaxReadBytes, or that ceiling when it asked nothing. They are closed when the session ends.
It also serves env's workspace's files, read-only, as resources under wire.FilesURITemplate (mcpworkspace.ServeFiles), so that the session reads the project's AGENTS.md, .dax/skills and .dax/config.json where the project is; and, when the workspace can start a process (workspace.Starter), the process methods (mcpworkspace.Processes), so that the MCP servers the session's user configured run here too. Those are custom methods, not tools: no tool of the server starts a process the session did not ask for, and no other client sees one in the listing.
The caller closes the Server, and env's workspace after it. Two tools of one name are refused: the session refuses them anyway, and a capability that named one twice could not say which ran. NewServer builds the tools once at the ceiling, and closes them, to refuse that before any session comes.
func (*Server) AddReceivingMiddleware ¶ added in v0.0.19
func (s *Server) AddReceivingMiddleware(mw ...sdk.Middleware)
AddReceivingMiddleware wraps the requests of every session connected after it, once the session has initialized (its initialize itself is not wrapped), outside everything the server answers: a program watching or holding the requests, as the tests do.
func (*Server) Close ¶ added in v0.0.19
Close closes the tools of every session that has not ended, waits for those of the sessions that have, and refuses another session; the sessions themselves are their transports' to end. The caller closes the workspace after it.
func (*Server) Connect ¶ added in v0.0.19
func (s *Server) Connect(ctx context.Context, t sdk.Transport, opts *sdk.ServerSessionOptions) (*sdk.ServerSession, error)
Connect serves one session over t, on a server of its own, as sdk.Server's Connect does.
func (*Server) Run ¶ added in v0.0.19
Run serves one session over t until the client hangs up or ctx ends, as sdk.Server's Run does: `dax execute` on standard input and output.
func (*Server) Sessions ¶ added in v0.0.19
func (s *Server) Sessions() []*sdk.ServerSession
Sessions are the sessions that have initialized and not ended.
type Set ¶
type Set struct {
// contains filtered or unexported fields
}
Set is an executor's tools bound as agenttool tools: what the kit offers the model, the policy reads and the sub-agents are given. It keeps the facts each model response's calls were read as while their decisions are in flight (PinBatch), and the facts of a decision of its own (Pin), so that every reading of one call's claim within its decision, and within its batch's, is the same reading.
func (*Set) Facts ¶ added in v0.0.24
func (s *Set) Facts(ctx context.Context, name string, args json.RawMessage) (agenttool.Facts, error)
Facts is the claim for a call of the tool named name with args, as the policy reads it under ctx (facts): the reading a decision PinBatch marked is the batch's, so a judgement made beside the policy's, the session's confinement (agent/confine.go), reads what the policy decides on rather than a second, later look.
func (*Set) Pin ¶
func (s *Set) Pin(ctx context.Context, info agentturn.ToolCallInfo) (unpin func())
Pin holds the facts of info's call for one decision about it, until unpin: every reading of the call's claim in between, the policy's subjects, a folded hook's, the session's own, is one reading, made under ctx by the first of them. So the rewrite the call runs with comes from the very reading its verdict was decided on, however the claim would answer a second time. The arguments a rewrite gives are another call, read afresh.
Two pins of one call share it, and it is read once for both; the entry goes with the last unpin, so the cache never holds more than the decisions in flight. A failed reading is kept with it and blocks the call every time it is read. A call of a tool that makes no claim pins nothing. unpin may be called more than once.
func (*Set) PinBatch ¶ added in v0.0.8
PinBatch holds the facts of info's model response for the decisions about its calls, and returns ctx marked with them and done, which the decision calls when it returns. Every reading made under the marked context, the policy's subjects, a folded hook's, the engine's reading of a hook's rewrite and the batch hold's of a sibling, is the batch's: on the first of them, every claiming call of the response is read in one request and every rewrite those claims ask for in a second, under the first decision's context. So a call's verdict, the stamp it runs with, the decision about that stamp and each sibling's reading are one reading, however the claims would answer a second time, and a response of n claiming calls costs two requests, not one or more per reading. A rewrite is read as the call it is, never assumed to claim what the call it came from claimed.
A batch is the run's, its turn's and its Batch slice's (agentturn hands every call of a response the same one; a nested Invoke is a batch of its own). Only a context marked with it reads it: an identical call decided outside it is read afresh. It is let go when the decision about its last call returns, when the first decision's context ends (the run ending), when a newer batch of the run starts (agentturn stops a response's decisions at a hook's error, so its last may never come), on Release and on ReleaseAll; a reading under it after that fails. A failed request fails every reading under the batch, so each of its calls is blocked, and nothing it read is kept.
A call info's batch does not hold under its own arguments (a resumed call approved with arguments the response did not carry), or one with no batch, is pinned as Pin pins it.
func (*Set) Release ¶ added in v0.0.8
Release lets go of the batch ctx is marked with, at once: a sub-agent's decision that is about to ask a person, so that a slow answer holds no reading open; the next call of the response is read in a batch of its own.
func (*Set) ReleaseAll ¶ added in v0.0.8
func (s *Set) ReleaseAll()
ReleaseAll lets go of every batch, as the session closes.
type Tool ¶
type Tool struct {
// Extension names the extension whose tool it is.
Extension string
// Definition is the function tool on a request, agenttool.Definition
// of the tool.
Definition *openresponses.FunctionTool
Annotations agenttool.Annotations
// ReadOnly is the extension's ReadOnly naming the tool.
ReadOnly bool
Sequential bool
// Barrier is agenttool.IsBarrier of the tool: each call runs alone
// in its place in the batch, the calls on either side overlapping.
Barrier bool
Resource string
// Factual is whether the tool makes the facts claim
// (agenttool.IsFactual).
Factual bool
// Replayable is whether the tool answers whether a call may run
// again (agenttool.Replayable); one that does not reads as
// agenttool.ReplayUnknown.
Replayable bool
}
Tool is one tool an executor runs, as the session sees it: what the model is offered and what the policy and the batch scheduler read.