Documentation
¶
Overview ¶
Package orchestrator is the gridctl single-writer multi-agent primitive. An Orchestrator owns a typed State; subagents are invoked via Handoff and ParallelHandoff and never receive a pointer to that State. The orchestrator is the only thing that can write — subagents see typed inputs the orchestrator derives from Snapshot, and return typed outputs the orchestrator merges back via Apply.
Single-writer enforcement is structural, not advisory: there is no path for a subagent to reach into the orchestrator's State because the orchestrator never hands out a pointer. Apply serialises mutations behind a mutex so concurrent merges from a parallel batch land in a well-defined order.
Subagents are dispatched through agent.ToolCaller — the same surface the runtime invokes any MCP tool through. A *skill.Registry satisfies the interface directly (typed Go skills); pkg/agent/gateway adapts a *mcp.Gateway so the orchestrator can call any tool the gateway exposes (TS skills via the registry's TS dispatcher, downstream MCP servers, or another gridctl instance pointed at this one). One code path covers local and remote handoff alike.
Parallel handoffs are capped at HardMaxParallel = 4. SetMaxParallel can lower the cap (useful for tests and rate-limited providers) but requests above the ceiling are clamped with a warning rather than rejected — the cap is the orchestrator's invariant, not the caller's preference.
Index ¶
Constants ¶
const ( // DefaultMaxParallel is the orchestrator's default concurrency for // ParallelHandoff. Mirrors sandbox.DefaultMaxParallel so a TS // skill's parallel() and a Go orchestrator's ParallelHandoff observe // the same hard cap. DefaultMaxParallel = 4 // HardMaxParallel is the ceiling SetMaxParallel cannot exceed. // Requests above the ceiling are clamped with a warning. The cap // exists because handoffs trigger LLM calls and tool invocations // that are individually expensive; "let the user fan out as wide // as they want" is not a safe default for an orchestrator that // composes onto third-party APIs. HardMaxParallel = 4 )
Variables ¶
This section is empty.
Functions ¶
func Handoff ¶
Handoff dispatches a single subagent call through the orchestrator's ToolCaller and decodes the result into Out via JSON. State is not passed to the subagent — the caller is responsible for constructing Input from a Snapshot and merging Output back via Apply.
Handoff is a free function (not a method) because Go does not allow generic methods to introduce new type parameters. The Orchestrator's State parameter is required so the function statically witnesses which orchestrator the handoff belongs to; the runtime does not otherwise need it.
Types ¶
type Call ¶
type Call struct {
// Skill is the unprefixed skill / tool name the orchestrator's
// caller will resolve. Empty Skill names are rejected at handoff
// time.
Skill string
// Input is the typed argument object. Non-nil values are JSON-
// marshaled, then unmarshaled into a map[string]any so the
// downstream caller (which expects map[string]any per the
// pkg/mcp.AgentClient.CallTool contract) sees a JSON-clean view.
Input any
}
Call describes a single handoff. Skill is the unprefixed skill name the configured agent.ToolCaller resolves; Input is the typed input the orchestrator marshals to JSON before dispatch. Input may be a struct, a map[string]any, or nil (treated as an empty object).
type Orchestrator ¶
type Orchestrator[State any] struct { // contains filtered or unexported fields }
Orchestrator owns a State that only the orchestrator's caller can mutate. State is read out via Snapshot (deep-copied) and updated via Apply (mutex-serialised). Handoff and ParallelHandoff invoke subagents through the configured agent.ToolCaller; subagents receive only the typed Input the caller provides — never a pointer or reference to State.
The zero value is not usable. Construct via New.
func New ¶
func New[State any](caller agent.ToolCaller, initial State) *Orchestrator[State]
New returns an Orchestrator initialised with the given caller and initial state. The caller can be nil; calls to Handoff and ParallelHandoff return an error in that case rather than panicking, matching the Article V (no panic in library code) discipline.
State is taken by value: the orchestrator owns its copy from the constructor call onward.
func (*Orchestrator[State]) Apply ¶
func (o *Orchestrator[State]) Apply(fn func(*State) error) error
Apply runs fn under the orchestrator's write lock. fn receives a pointer to the live State so it can mutate fields directly; the pointer must NOT escape fn — it is invalid after Apply returns and concurrent reads via Snapshot will not see partial updates.
fn returning an error leaves the state unchanged from fn's perspective: errors do not roll back mutations fn already performed before returning. Callers that need transactional semantics should stage the mutation in a local copy and assign it inside fn only on success.
Apply with a nil fn returns an error rather than silently no-oping — passing nil is almost always a mistake.
func (*Orchestrator[State]) SetLogger ¶
func (o *Orchestrator[State]) SetLogger(logger *slog.Logger)
SetLogger replaces the orchestrator's slog.Logger. The provided logger is wrapped with a "component=agent-orchestrator" attribute so downstream filters can match on it. nil is a no-op.
func (*Orchestrator[State]) SetMaxParallel ¶
func (o *Orchestrator[State]) SetMaxParallel(n int)
SetMaxParallel sets the parallel-handoff concurrency cap. Values <= 0 are treated as "use DefaultMaxParallel"; values above HardMaxParallel are clamped at the ceiling with a warning emitted on the next ParallelHandoff. The cap is read at ParallelHandoff start; in-flight batches are not retroactively rebalanced.
func (*Orchestrator[State]) SetTracer ¶
func (o *Orchestrator[State]) SetTracer(tracer trace.Tracer)
SetTracer replaces the orchestrator's trace.Tracer. Use this when wiring the orchestrator under a non-default tracer provider (tests most often). nil is a no-op.
func (*Orchestrator[State]) Snapshot ¶
func (o *Orchestrator[State]) Snapshot() State
Snapshot returns a deep copy of the current State. The copy is produced by JSON round-trip; State types with unexported fields, channels, or function values must marshal cleanly for the copy to be faithful — the orchestrator deliberately rejects sharing pointers rather than supporting opaque-by-design types. If JSON round-trip fails (which only happens for non-marshalable State types), Snapshot returns the zero value of State and logs the error; callers should treat State as a JSON-marshalable value object.
type Result ¶
Result is the per-item outcome from ParallelHandoff. Index references the position of the originating Call in the input slice (useful when the caller needs to merge results positionally back into State). Err captures per-item failures; ParallelHandoff returns nil at the batch level when individual handoffs fail so callers can decide how to merge — partial success is a real outcome for parallel agentic flows and the orchestrator does not pre-empt that decision.
func ParallelHandoff ¶
func ParallelHandoff[State, Out any](ctx context.Context, o *Orchestrator[State], calls []Call) ([]Result[Out], error)
ParallelHandoff dispatches every Call concurrently with concurrency capped at min(SetMaxParallel, HardMaxParallel). The returned Results slice mirrors the calls slice positionally — Results[i] is the outcome of calls[i], regardless of completion order. Per-call errors surface in Result.Err; the batch-level error is non-nil only on invalid arguments (nil orchestrator) or context cancellation that occurred before any handoff started.
Cancelling ctx in flight stops new handoffs from being scheduled and surfaces ctx.Err() on calls that had not yet started; in-flight handoffs receive the cancelled context and propagate the cancellation through their own logic. The function only returns once every scheduled handoff goroutine has exited, so callers can rely on Results being fully populated.