subagent

package module
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

Documentation

Overview

Package subagent implements a stack-frame sub-agent protocol: a parent agent hands off a free-text prompt to a freshly-constructed child *agent.Agent, and the child returns a structured Result validated against an enforced JSON schema. The frame is discarded after each invocation, giving strict state isolation between parent and child.

The protocol

The parent calls a tool produced by AsTool. The tool's input is a single "prompt" string field. The closure:

  1. Calls the application-supplied factory to obtain a fresh *agent.Agent. The factory is the sole extension point: callers compose the agent's pattern, model, and tool subset.
  2. Seeds a fresh ledger.Thread with the prompt and runs the agent. The agent's bound state (if any) is NOT used — the factory must omit agent.WithState to prevent the child from mutating parent state.
  3. Captures the produced turn from the agent's "turn_complete" event stream (the same mechanism x/compaction uses).
  4. Closes the agent. The fresh-per-call contract means a factory that returns the same agent twice will fail on the second call (the underlying step is closed).
  5. Collects the turn's text, parses it against ResultSchema, and returns a structured Result. A JSON parse error surfaces as a tool error; a parseable-but-schema-invalid payload (bad enum value, missing required field) surfaces as a Result with Status set to StatusFailed so the parent can branch on outcome without losing the diagnostic text.

Usage

sp, _ := subagent.ResultSystemPrompt()
factory := func() (*agent.Agent, error) {
    return agent.New("researcher",
        agent.WithProvider(prov),
        agent.WithSpec(spec),
        agent.WithPattern(&cognitive.React{}),
        agent.WithTransforms(sp),  // instruct the child to emit JSON
    ), nil
}
subT, subFn := subagent.AsTool(factory, "researcher",
    "Search the codebase and answer the prompt.")
_ = registry.Register(subT, subFn)

Structured return

AsTool's closure returns a Result struct (or a tool error). The result is JSON-marshalled into the LLM-facing wire format with lowercase keys matching ResultSchema:

{"status": "success" | "partial" | "failed",
 "summary": string,
 "findings": object | null}

Status values partition the outcome space:

  • StatusSuccess: the sub-agent fully completed the task.
  • StatusPartial: progress was made but the task was not finished.
  • StatusFailed: the sub-agent could not accomplish the task, OR the child's output failed schema validation (the raw payload is preserved in Summary for diagnosis).

The parent can branch on outcome directly:

res, err := fn(ctx, sandbox, args)
switch r := res.(type) {
case subagent.Result:
    switch r.Status {
    case subagent.StatusSuccess:
        // done
    case subagent.StatusPartial:
        // retry or escalate
    case subagent.StatusFailed:
        // log r.Summary, retry or escalate
    }
case nil:
    // err is set; tool failed (parse error, agent error, etc.)
}

System prompt helper

ResultSystemPrompt returns a loop.Transform preconfigured with the schema baked into the system prompt. Factories should include this in the child agent's WithTransforms so the child is instructed to emit JSON matching ResultSchema. AsTool cannot inject the transform post-construction because agent.Agent does not expose transform mutation; the factory's responsibility is to include it at build time.

Scope

Out of scope for v1: streaming deltas, parallel fan-out, span nesting under the parent, dynamic tool-set changes within a single call. See issue #517 for the design rationale.

Package subagent exposes a *agent.Agent as a tool.Tool under a strict stack-frame protocol: parent hands off a free-text prompt to a freshly-constructed child agent, and the child returns a structured Result validated against an enforced JSON schema. The frame is discarded after each invocation, giving strict state isolation between parent and child.

The pattern of the wrapped agent is opaque to the parent: a SingleShot agent is a one-shot helper; a ReAct agent runs a tool-use loop; a Verified agent retries on quality failures. The parent sees only the structured Result, not the child's internal turn structure.

Out of scope for v1: streaming deltas, parallel fan-out, span nesting under the parent, dynamic tool-set changes within a single call.

Index

Constants

This section is empty.

Variables

View Source
var ResultSchema = map[string]any{
	"type": "object",
	"properties": map[string]any{
		"status": map[string]any{
			"enum": []string{string(StatusSuccess), string(StatusPartial), string(StatusFailed)},
			"type": "string",
		},
		"summary": map[string]any{
			"type": "string",
		},
		"findings": map[string]any{
			"type":     "object",
			"nullable": true,
		},
	},
	"required":             []string{"status", "summary"},
	"additionalProperties": false,
}

ResultSchema is the JSON Schema that a sub-agent's textual output MUST conform to. AsTool validates the produced turn against this schema before constructing a Result. JSON parse failures are surfaced as tool errors (the closure returns a non-nil error); a parseable payload that fails schema validation is returned as a Result with Status set to StatusFailed.

The schema is exported so callers can reuse it for client-side validation, documentation, or constrained-decoding configurations on providers that support grammar-constrained generation. Constrained decoding is not currently wired by AsTool — it is a future per-provider extension orthogonal to this protocol.

Functions

func AsTool

func AsTool(build func() (*agent.Agent, error), name, description string) (tool.Tool, tool.ToolFunc)

AsTool wraps an application-supplied factory as a tool.Tool. Each invocation of the tool calls the factory to obtain a fresh *agent.Agent, runs the agent against a one-shot ledger seeded with the prompt, captures the produced turn from the agent's "turn_complete" event stream, and closes the agent. The factory is the sole extension point: callers compose the agent's pattern, model, and tool subset, then hand the factory to AsTool.

The tool's input is a JSON object with a single "prompt" string field; the agent runs once (its configured pattern decides whether that is one turn or many), and the produced assistant turn is collected from the agent's "turn_complete" event stream and returned as a structured Result (see result.go). The JSON schema for the result is enforced: malformed output surfaces as a tool error; schema-conformant-but-invalid payloads (bad enum value, missing field) surface as a Result with Status set to StatusFailed so the parent can branch on outcome without losing the diagnostic text.

Fresh-per-call contract: the factory MUST return a new *agent.Agent per invocation. The closure calls defer Close() on the returned agent; *agent.Agent.Close is idempotent (via sync.Once) but a closed agent's step will reject subsequent runs. A factory that returns the same agent twice will fail on the second call.

State isolation: the sub-agent runs against a fresh ledger.Thread seeded with the prompt as a RoleUser turn. The sub-agent's configured transforms, handlers, and pattern apply to that fresh thread. The agent MUST NOT be constructed with agent.WithState (which would auto-append to the bound state on every Emit); the factory's responsibility is to omit WithState from the agent's options.

AsTool returns both the tool.Tool descriptor and its callable ToolFunc. The caller is expected to register both with a tool.Registry, e.g.:

subT, subFn := subagent.AsTool(func() (*agent.Agent, error) {
    sp, _ := subagent.ResultSystemPrompt()
    return agent.New("researcher",
        agent.WithProvider(prov),
        agent.WithSpec(spec),
        agent.WithPattern(&cognitive.SingleShot{}),
        agent.WithTransforms(sp),
    ), nil
}, "researcher", "Search the codebase and answer the prompt.")
_ = registry.Register(subT, subFn)

func ResultSystemPrompt added in v0.2.0

func ResultSystemPrompt() (loop.Transform, error)

ResultSystemPrompt returns a loop.Transform that injects a RoleSystem turn instructing the sub-agent to emit a JSON object matching ResultSchema. The factory caller of AsTool should include this transform in the child agent's options via agent.WithTransforms, e.g.:

sp, _ := subagent.ResultSystemPrompt()
return agent.New("researcher", agent.WithTransforms(sp), ...), nil

The transform's content is rendered once at construction time using the current value of ResultSchema. If callers mutate ResultSchema at runtime (rare; it is a package-level constant), they must call ResultSystemPrompt again to refresh the prompt.

Why a helper: AsTool cannot inject the system-prompt transform post-construction because agent.Agent does not expose transform mutation. Having the factory include the transform at build time keeps the protocol's contract visible at the call site without adding new surface on agent.Agent.

Types

type Result added in v0.2.0

type Result struct {
	Status   Status `json:"status"`
	Summary  string `json:"summary"`
	Findings any    `json:"findings"`
}

Result is the structured outcome of a sub-agent invocation. It is returned by the tool-callable ToolFunc produced by AsTool when the child's output passes JSON schema validation; malformed output (a non-JSON textual response) surfaces as a tool error instead. A parseable-but-schema-invalid payload (e.g., a bad enum value or a missing required field) surfaces as Result{Status: StatusFailed, Summary: <raw payload>} so the parent can branch on outcome without losing the diagnostic text.

Findings is an opaque, optional bag for structured data the sub-agent wants to convey beyond the prose summary. The protocol does not constrain the shape of Findings; downstream consumers are expected to type-assert or re-validate as appropriate. When Findings is nil, the field marshals as JSON null (matching the schema's `"nullable": true` declaration).

JSON tags align the wire format with ResultSchema's lowercase keys so the LLM-facing serialization matches what the child was instructed to produce.

type Status added in v0.2.0

type Status string

Status is the outcome reported by a sub-agent's structured Result. The three values partition the response space: "success" indicates the sub-agent fully completed the requested task; "partial" indicates the sub-agent made progress but did not finish; "failed" indicates the sub-agent could not accomplish the task.

AsTool's closure enforces Status at the schema boundary: any other value (including the empty string) is rejected as invalid and surfaces as a Result with Status set to StatusFailed.

const (
	// StatusSuccess is the value of Result.Status when the sub-agent
	// fully completed the requested task.
	StatusSuccess Status = "success"
	// StatusPartial is the value of Result.Status when the sub-agent
	// made progress but did not finish.
	StatusPartial Status = "partial"
	// StatusFailed is the value of Result.Status when the sub-agent
	// could not accomplish the task, or when the sub-agent's output
	// failed schema validation.
	StatusFailed Status = "failed"
)

Jump to

Keyboard shortcuts

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