specialists

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package specialists loads specialist .tmpl files from disk and turns them into ADK v2 agents. .tmpl files are YAML frontmatter (bounded by `---`) followed by a Markdown body used as the specialist's system prompt.

Schema follows docs/specialists-design.md. This package implements the spike subset (name, description, mode, instruction, model override) plus the output-schema contract. Tool allowlists are enforced at Build time (see filterToolsets), as is the model override: a spec's `model:` is resolved through BuildOptions.Resolve, and a declared override that cannot be resolved fails the build rather than falling back to the parent's model.

A spec's `output_schema:` names a JSON-Schema document relative to the .tmpl file; it is read, normalized and checked at load time (see schema.go) and reaches the agent as llmagent.Config.OutputSchema. From there ADK enforces it — a violation is an error on both paths, never a warning. In Task mode the schema becomes the finish_task declaration and an invalid call is rejected back to the model with the validation error, so a bad shape cannot become the task's output. In SingleTurn mode the reply is validated on the way out and a failure propagates as a run error.

Budget fields are parsed here but enforced elsewhere, per field:

  • max_wallclock_seconds — enforced in graph dispatch: pkg/graph maps it to workflow.NodeConfig.Timeout on the specialist's AgentNode (the sanctioned per-node wallclock knob).
  • max_turns and max_cost_usd — enforced by the session meter, not here: cost and turns are derived from UsageMetadata on the runner's event stream, which Build never sees. The roster's declarations become budget scopes (internal/compose.MeterScopes → budget.Config.Scopes) that the meter buckets by event author, so a specialist with a tighter ceiling than its workload stops the run on its own — see pkg/budget, "Scopes", for the composition rule and its two known limitations.

Attribution is by session.Event.Author, which carries the agent's name on every dispatch shape mast builds. Event.Branch is not the seam: in the coordinator/sub-agent-tool shape it is empty.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Build

func Build(spec Spec, opts BuildOptions) (adkagent.Agent, error)

Build turns a Spec into an ADK agent, dispatching to Task or SingleTurn constructors based on Spec.Mode. The agent runs on spec.Model when the spec declares one, opts.Model otherwise — see modelFor.

A spec's OutputSchema reaches both modes. The two enforce it differently — Task mode through the finish_task declaration, which rejects a malformed call back to the model, SingleTurn through validation of the reply, which fails the run — but in neither case can output that violates the contract become the specialist's result.

func BuildAll

func BuildAll(specs []Spec, opts BuildOptions) ([]adkagent.Agent, error)

BuildAll builds every Spec in specs using the same BuildOptions. Any error short-circuits and is returned with the offending Spec name.

Types

type Budget

type Budget struct {
	MaxTurns            int     `yaml:"max_turns,omitempty"`
	MaxWallclockSeconds int     `yaml:"max_wallclock_seconds,omitempty"`
	MaxCostUSD          float64 `yaml:"max_cost_usd,omitempty"`
}

Budget captures the per-specialist runtime bounds. See docs/specialists-design.md schema for field semantics, and the package doc above for where each is enforced: MaxWallclockSeconds by graph dispatch (pkg/graph → NodeConfig.Timeout), MaxTurns and MaxCostUSD by the session meter (pkg/budget scopes).

type BuildOptions

type BuildOptions struct {
	// Model is the parent's model — the default every specialist runs
	// on when it declares no `model:` override.
	Model model.LLM

	// Resolve resolves per-specialist `model:` overrides. Nil is legal
	// only when no Spec in the roster declares an override; Build
	// refuses a declared override it cannot resolve rather than
	// silently running the specialist on the parent's model (the bug
	// this field fixes — see docs/v0.3-plan.md W1.1).
	Resolve ModelResolver

	Tools    []tool.Tool
	Toolsets []tool.Toolset
}

BuildOptions carries the runtime bindings a Spec needs to become a concrete ADK agent. The model is required. Toolsets are offered to every built specialist but filtered through Spec.Tools.MCP first — see filterToolsets for the spike allowlist semantics.

type Capability added in v0.3.0

type Capability string

Capability is what a specialist is allowed to do to the world. It is the read/write half of the roster split: analysts diagnose, and a separate, declared specialist carries out changes.

The field exists because an allowlist alone cannot distinguish "this specialist may write" from "somebody added a write tool to a diagnoser and nobody noticed". A prompt saying *do not mutate* is not a control; a declaration a loader can refuse is. See docs/specialists-design.md, "Capability".

const (
	// CapabilityReadOnly is a specialist that may not reach a mutating
	// tool. Default when capability: is absent — the safe direction, and
	// the one most specialists want.
	CapabilityReadOnly Capability = "read_only"

	// CapabilityChangeExecutor is a specialist that may. Declaring it is
	// not an approval: every mutating call it makes still goes to the
	// write gate (pkg/approval) like any other.
	CapabilityChangeExecutor Capability = "change_executor"
)

type Frontmatter

type Frontmatter struct {
	Name        string        `yaml:"name,omitempty"`
	Description string        `yaml:"description"`
	Mode        Mode          `yaml:"mode,omitempty"`
	Model       string        `yaml:"model,omitempty"`
	Capability  Capability    `yaml:"capability,omitempty"`
	Budget      Budget        `yaml:"budget,omitempty"`
	Tools       ToolAllowlist `yaml:"tools,omitempty"`

	// OutputSchema is a path to a JSON-Schema document, relative to the
	// .tmpl file's own directory. It is a reference rather than an
	// inline block on purpose — see the comment at the top of schema.go.
	OutputSchema string `yaml:"output_schema,omitempty"`
}

Frontmatter is the YAML block at the top of a .tmpl file.

type MCPAllowlist

type MCPAllowlist struct {
	Server string   `yaml:"server"`
	Tools  []string `yaml:"tools,omitempty"`
}

MCPAllowlist is the per-MCP-server tool allowlist for a specialist.

type Mode

type Mode string

Mode is the ADK v2 agent mode a specialist runs in.

const (
	// ModeTask is a Task-mode specialist. Runs to a finish_task
	// completion. Default when mode: is absent.
	ModeTask Mode = "Task"

	// ModeSingleTurn is a SingleTurn-mode specialist. Runs exactly one
	// model call. The shape behind LLM-as-router classifiers.
	ModeSingleTurn Mode = "SingleTurn"
)

type ModelResolver added in v0.3.0

type ModelResolver func(name string) (model.LLM, error)

ModelResolver turns a specialist's `model:` frontmatter override into a concrete model.LLM. It exists so pkg/specialists can honor the override without depending on the provider packages: this package knows a specialist declared "claude-haiku-4-5", and nothing else about what that string means.

Implementations are expected to memoize — a roster of eight analysts on the same tier should share one provider client, not open eight. internal/compose.NewModelResolver is the one mast ships; it resolves through the same BuildModel path the root model came from.

type Spec

type Spec struct {
	// Filename is the path (or filename) the spec was loaded from,
	// preserved for diagnostics.
	Filename string

	// Frontmatter fields, promoted for convenience.
	Name        string
	Description string
	Mode        Mode
	Model       string
	Capability  Capability
	Budget      Budget
	Tools       ToolAllowlist

	// Instruction is the body of the .tmpl file — the specialist's
	// system prompt, verbatim.
	Instruction string

	// OutputSchema is the loaded, normalized and checked contract this
	// specialist's output must satisfy, or nil when the spec declares
	// none. Loaded eagerly by LoadFile so a broken schema is a load
	// error rather than a surprise on the first live turn.
	OutputSchema *genai.Schema

	// OutputSchemaPath is the resolved path OutputSchema came from,
	// preserved for diagnostics. Empty when the spec declares none.
	OutputSchemaPath string
}

Spec is a fully-loaded specialist: parsed frontmatter plus the raw Markdown body used as the system prompt.

func LoadDir

func LoadDir(dir string) ([]Spec, error)

LoadDir reads every *.tmpl file in dir non-recursively and parses each into a Spec. Results are returned sorted by Spec.Name for deterministic ordering.

func LoadFile

func LoadFile(path string) (Spec, error)

LoadFile reads and parses a single .tmpl file.

type ToolAllowlist

type ToolAllowlist struct {
	Builtin []string       `yaml:"builtin,omitempty"`
	MCP     []MCPAllowlist `yaml:"mcp,omitempty"`
	Skills  []string       `yaml:"skills,omitempty"`
}

ToolAllowlist is the composite allowlist of built-in tools, MCP tools, and skills a specialist may invoke.

Presence is significant per axis, per the normative table in docs/specialists-design.md: an absent field inherits everything on that axis, a present-but-empty field denies everything on it, and a non-empty field is a whitelist. `mcp: []` is therefore not the same declaration as no `mcp:` key at all — see InheritsAllMCP.

func (ToolAllowlist) InheritsAllMCP added in v0.3.0

func (t ToolAllowlist) InheritsAllMCP() bool

InheritsAllMCP reports whether this allowlist leaves the MCP axis unrestricted — i.e. the spec declared no `mcp:` key, so the specialist is offered every MCP toolset the workload has.

It exists because the distinction it draws is a nil check that reads like a typo. `mcp: []` decodes to an empty non-nil slice and means *deny every MCP tool*; a missing `mcp:` decodes to nil and means *grant them all*. Those are opposite outcomes one character apart, so the question gets asked through a named method rather than re-derived at each call site — filterToolsets enforces it, and internal/compose.CheckCapabilitySplit refuses the inherit-all case for a read_only specialist when the workload has a tool catalog.

Jump to

Keyboard shortcuts

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