metaagent

package
v0.9.22 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package metaagent provides a generic framework for building AI agents.

The framework is fully generic over three type parameters:

  • Req: The request type (must implement promptbuilder.Bindable)
  • Resp: The structured response type returned by the agent
  • CB: The callbacks type providing tool implementations

This design allows agents to be composed with any combination of tools from the toolcall package (worktree tools, finding tools, custom tools).

Model Support

The model parameter determines which provider implementation is used:

  • Models starting with "gemini-" use Google's Generative AI SDK (native)
  • Models starting with "claude-" use Anthropic's SDK via Vertex AI (native)
  • Models in "publisher/model" format use Vertex AI's OpenAI-compatible endpoint

Usage

Define your callback type by composing tool callbacks:

type MyCallbacks = toolcall.FindingTools[toolcall.WorktreeTools[toolcall.EmptyTools]]

Create the corresponding tool provider:

tools := toolcall.NewFindingToolsProvider[*Result, toolcall.WorktreeTools[toolcall.EmptyTools]](
    toolcall.NewWorktreeToolsProvider[*Result, toolcall.EmptyTools](
        toolcall.NewEmptyToolsProvider[*Result](),
    ),
)

Configure and create the agent:

config := metaagent.Config[*Result, MyCallbacks]{
    SystemInstructions: systemPrompt,
    UserPrompt:         userPrompt,
    Tools:              tools,
}

agent, err := metaagent.New[*Request, *Result, MyCallbacks](ctx, projectID, region, model, config)
result, err := agent.Execute(ctx, request, callbacks)

The agent uses the submit_result tool to return structured results. The Resp type's JSON tags define the schema for the tool's payload.

Suspend/Resume (ask-a-friend)

Setting Config.SuspendToolName advertises a held-out ask-a-friend tool; when the model calls it, Execute returns a *checkpoint.Suspension instead of a Resp, and the paused conversation is later continued through the opt-in Resumer capability (obtained via AsResumer). Only the Claude backend supports this today — the Gemini and OpenAI-compatible backends reject a set SuspendToolName at construction until their executors grow suspend support. See the suspend package for the park/wake orchestration around it.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Agent

type Agent[Req promptbuilder.Bindable, Resp, CB any] interface {
	// Execute runs the agent with the given request and tool callbacks.
	Execute(ctx context.Context, request Req, callbacks CB) (Resp, error)
}

Agent is the interface for a configured meta-agent.

  • Req must implement promptbuilder.Bindable.
  • Resp is the structured response type.
  • CB is the type providing all tool callbacks.

func New

func New[Req promptbuilder.Bindable, Resp, CB any](
	ctx context.Context,
	projectID, region, modelName string,
	config Config[Resp, CB],
) (Agent[Req, Resp, CB], error)

New creates a new meta-agent with the given configuration. The modelName parameter determines which provider implementation is used:

  • Models starting with "gemini-" use Google's Generative AI SDK (native)
  • Models starting with "claude-" use Anthropic's SDK via Vertex AI (native)
  • Models in "publisher/model" format use Vertex AI's OpenAI-compatible endpoint
Example

ExampleNew demonstrates creating a new meta-agent with model selection. New selects the provider implementation based on the model name prefix: "gemini-" uses Google's Generative AI SDK, "claude-" uses Anthropic via Vertex AI.

package main

import (
	"context"
	"fmt"

	"chainguard.dev/driftlessaf/agents/metaagent"
	"chainguard.dev/driftlessaf/agents/promptbuilder"
	"chainguard.dev/driftlessaf/agents/toolcall"
)

// request is an example request type that implements promptbuilder.Bindable.
type request struct {
	Query string
}

func (r *request) Bind(p *promptbuilder.Prompt) (*promptbuilder.Prompt, error) {
	return p.BindXML("query", struct {
		XMLName struct{} `xml:"query"`
		Content string   `xml:",chardata"`
	}{
		Content: r.Query,
	})
}

// response is an example structured response type.
type response struct {
	Answer string `json:"answer"`
}

func main() {
	ctx := context.Background()

	tools := toolcall.NewEmptyToolsProvider[*response]()
	config := metaagent.Config[*response, toolcall.EmptyTools]{
		Tools: tools,
	}

	// An unsupported model prefix returns an error.
	_, err := metaagent.New[*request](ctx, "my-project", "us-central1", "unknown-model", config)
	if err != nil {
		fmt.Println("error:", err)
	}
}
Output:
error: unsupported model: unknown-model (expected gemini-*, claude-*, or publisher/model format)

type Config

type Config[Resp, CB any] struct {
	// SystemInstructions is the system prompt that defines the agent's role and behavior.
	SystemInstructions *promptbuilder.Prompt

	// UserPrompt is the template for formatting the user's request.
	// The Req type is bound to this template via its Bind method.
	UserPrompt *promptbuilder.Prompt

	// UserPromptSuffix is an optional static prompt appended as a separate
	// trailing block of the initial user message, after the bound UserPrompt.
	// When set, the leading block (UserPrompt with the request bound) gets a
	// cache breakpoint so executions that differ only in the suffix share its
	// cache entry. Intended for multi-pass agents reviewing one payload
	// through different lenses: the payload rides in UserPrompt, the per-pass
	// lens in the suffix. The request is never bound into the suffix — it
	// must be fully bound already. On the Gemini and OpenAI-compatible
	// backends (no per-block cache semantics) the built suffix is
	// concatenated onto the user prompt with a blank-line separator.
	UserPromptSuffix *promptbuilder.Prompt

	// Tools provides all tool definitions for this agent.
	// Compose providers using toolcall.NewFindingToolsProvider,
	// toolcall.NewWorktreeToolsProvider, and toolcall.NewEmptyToolsProvider.
	Tools toolcall.ToolProvider[Resp, CB]

	// MaxTurns sets the maximum number of conversation turns (LLM round-trips)
	// before the executor aborts. Zero means use the executor's default.
	MaxTurns int

	// ToolCallConcurrency bounds how many of a single turn's tool calls run
	// concurrently when the model emits more than one (parallel tool use).
	// Zero means use the executor's default (DefaultToolCallConcurrency). Set
	// to 1 to force strictly sequential tool dispatch — required for agents
	// whose tool handlers mutate shared state without their own synchronization.
	ToolCallConcurrency int

	// MaxTokens caps the model's output tokens per turn (the Anthropic
	// max_tokens parameter). Zero (the default) uses the meta-agent default of
	// 32000; the executor rejects values above 128000, the ceiling for current
	// Claude models. Because the executor streams every response, large values
	// do not risk the SDK's non-streaming HTTP timeout. Raise it for stages
	// whose turns need room for BOTH extended thinking and a tool call: at high
	// effort on a large context, adaptive thinking can otherwise consume the
	// whole budget and stop at max_tokens before the model emits its tool call,
	// which the executor surfaces as "no content in Claude's response". Claude
	// backend only; no effect on the Gemini or OpenAI backends.
	MaxTokens int64

	// ThinkingBudget enables Claude extended thinking with the given token
	// budget when running on the Claude backend. Zero (the default) leaves
	// thinking disabled. Must be at least 1024 and less than the executor's
	// max tokens (MaxTokens, or the 32000 default); see claudeexecutor.WithThinking.
	// On models where the Anthropic API has removed the explicit budget
	// parameter (Opus 4.7 and later), the executor automatically maps this to
	// adaptive thinking and the budget value is advisory only. No effect on
	// the Gemini or OpenAI backends.
	//
	// Deprecated: ThinkingBudget is Claude-only and already advisory-only on
	// Opus 4.7+. Use Effort, which works on every backend; do not set both.
	// The field will be removed once remaining consumers migrate.
	ThinkingBudget int64

	// Effort sets the provider-neutral reasoning-effort level, controlling how
	// deeply the model thinks and its overall token spend. Empty (the default)
	// leaves each backend's model default in place. Every backend maps the
	// level onto the nearest control the configured model supports:
	//   - Claude: output_config.effort — exact on Opus 4.7+/Sonnet 5/Fable 5;
	//     "xhigh" clamps to "high" on models that predate it (Sonnet 4.6,
	//     Opus 4.5/4.6); dropped with a warning on models without effort
	//     support; see claudeexecutor.WithEffort.
	//   - Gemini: thinkingLevel on Gemini 3.x models, thinkingBudget tiers on
	//     earlier models; see googleexecutor.WithEffort.
	//   - OpenAI-compatible: reasoning_effort, where xhigh and max clamp to
	//     "high"; reasoning models only, see openaiexecutor.WithEffort.
	// effort.XHigh is recommended for hard coding/agentic work on
	// Sonnet 5 / Opus 4.7+.
	Effort effort.Level

	// ResultValidators gate the terminal submit_result tool. When the model
	// submits a result that parses into Resp, every validator runs
	// concurrently against it; any findings reject the submission back to the
	// model as the tool's result — the agent loop continues until a submission
	// passes — and a validator error aborts the run. Empty (the default)
	// accepts every parsed submission. Findings are concatenated in
	// registration order. Validators must be safe for concurrent use; see
	// callbacks.ResultValidator.
	ResultValidators []callbacks.ResultValidator[Resp]

	// SuspendToolName, when non-empty, enables the ask-a-friend suspend/resume
	// capability: the backend advertises a held-out tool by this name, and when
	// the model calls it, Execute returns a *checkpoint.Suspension (extract it
	// with checkpoint.AsSuspension) carrying the envelope needed to resume the
	// paused conversation later, instead of a Resp. A resume-capable caller
	// obtains the resume path by type-asserting the constructed agent with
	// AsResumer.
	//
	// Claude backend only for now: the Gemini and OpenAI-compatible backends
	// reject a non-empty SuspendToolName at construction with a clear error
	// until their executors grow suspend support (DEV-2247 follow-up slices) —
	// silently ignoring it would advertise a lifecycle that can never fire.
	// The name must differ from the terminal submit tool's name and from every
	// caller-registered tool (both validated by the executor). Empty (the
	// default) leaves suspension disabled and the run's behavior byte-for-byte
	// unchanged.
	SuspendToolName string

	// SuspendToolDescription is the friend-facing description advertised to the
	// model for the suspend tool. Ignored when SuspendToolName is empty.
	SuspendToolDescription string
}

Config defines the configuration for a meta-agent instance.

  • Resp is the structured response type returned by the agent.
  • CB is the type providing all tool callbacks.

type Resumer added in v0.9.19

type Resumer[Req promptbuilder.Bindable, Resp, CB any] interface {
	Agent[Req, Resp, CB]

	// Resume rebuilds the paused conversation from env and continues it with the
	// given tool callbacks and human answers.
	Resume(ctx context.Context, env checkpoint.Envelope, answers map[string]string, callbacks CB) (Resp, error)
}

Resumer is the capability interface a resume-capable meta-agent satisfies in addition to Agent. It mirrors Agent.Execute but continues a previously suspended conversation from a checkpoint.Envelope instead of starting fresh.

Resumer is deliberately NOT folded into Agent: keeping it a separate, opt-in capability means the Agent interface never grows and every existing wrapper (usage-capturing decorators, adapters, downstream reconcilers, …) compiles unmodified. A caller that parks and wakes runs obtains it via AsResumer; a caller that only runs fresh conversations depends on Agent unchanged. This mirrors claudeexecutor.Resumer, which is likewise kept off the executor's exported Interface.

Today only the Claude backend implements Resumer; the Gemini and OpenAI-compatible backends gain it together with suspend support in their executors (DEV-2247 follow-up slices), and until then AsResumer reports false for agents built on them.

The answers map keys each pending tool-call ID (from Envelope.PendingToolCalls[i].ID — never re-derived from the tool name) to the human's answer. Answers flow raw: the underlying executor's Resume owns framing (checkpoint.FramedAnswers), and a missing entry is framed as an explicit empty-answer placeholder so no pending tool call is left unanswered. Resume returns checkpoint.ErrConfigDrift (wrapped) when the live executor config no longer matches the envelope, signaling the caller to rebuild from scratch.

func AsResumer added in v0.9.19

func AsResumer[Req promptbuilder.Bindable, Resp, CB any](agent Agent[Req, Resp, CB]) (Resumer[Req, Resp, CB], bool)

AsResumer type-asserts agent to the Resumer capability. It reports false when the agent's backend does not support suspend/resume, so callers can branch to a fresh-run fallback rather than assume every backend can wake a checkpoint.

Because a meta-agent builds its executor exactly once at construction, a resume path that must run against a *fresh* executor per wake should construct a new agent (via New) inside the reconcile and AsResumer that, rather than reuse a long-lived agent captured in main.

Example

ExampleAsResumer demonstrates obtaining the opt-in resume capability from a constructed agent. AsResumer reports false when the agent's backend does not support suspend/resume, so a waker can branch to a fresh run instead of assuming every backend can wake a checkpoint. Today only the Claude backend (Config.SuspendToolName set) yields a Resumer.

package main

import (
	"context"
	"fmt"

	"chainguard.dev/driftlessaf/agents/metaagent"
	"chainguard.dev/driftlessaf/agents/promptbuilder"
	"chainguard.dev/driftlessaf/agents/toolcall"
)

// request is an example request type that implements promptbuilder.Bindable.
type request struct {
	Query string
}

func (r *request) Bind(p *promptbuilder.Prompt) (*promptbuilder.Prompt, error) {
	return p.BindXML("query", struct {
		XMLName struct{} `xml:"query"`
		Content string   `xml:",chardata"`
	}{
		Content: r.Query,
	})
}

// response is an example structured response type.
type response struct {
	Answer string `json:"answer"`
}

// executeOnly is an Agent without the Resume capability, standing in for a
// backend that has not grown suspend/resume support yet.
type executeOnly struct{}

func (executeOnly) Execute(context.Context, *request, toolcall.EmptyTools) (*response, error) {
	return &response{}, nil
}

func main() {
	var agent metaagent.Agent[*request, *response, toolcall.EmptyTools] = executeOnly{}

	if resumer, ok := metaagent.AsResumer[*request](agent); ok {
		// Resume the parked conversation: answers are keyed by the pending
		// tool-call IDs persisted in the envelope (Envelope.PendingToolCalls).
		_ = resumer
		fmt.Println("resumable")
	} else {
		fmt.Println("not resumable: run from scratch")
	}
}
Output:
not resumable: run from scratch

Jump to

Keyboard shortcuts

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