agent

package
v0.4.0 Latest Latest
Warning

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

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

Documentation

Overview

Package agent provides the bucket-1 shim over ADK v2's runner and llmagent primitives. It hides the llmagent.Config boilerplate behind mast-shaped constructors and encodes the mode conventions the design corpus uses (Task-mode specialists, SingleTurn classifiers, Chat-mode coordinators).

Index

Constants

View Source
const ApprovedCallsMarker = "An operator has APPROVED the following calls"

ApprovedCallsMarker introduces the calls an operator approved, in the event pkg/graph puts on the change executor's branch (v0.4 W7.0).

Spelled here rather than imported, because pkg/graph's own tests import this package and the dependency can only run one way. TestApprovalPreambleCarriesTheMarker (pkg/graph) is what keeps the two strings one string: a fake that never matches would silently stop executing approved calls and every leg would still pass, just testing the reason-driven path twice.

View Source
const DefaultChatInstruction = `` /* 751-byte string literal not displayed */

DefaultChatInstruction is the fallback system prompt for Chat-mode agents (NewCoordinator). Chat mode fronts an interactive operator — an attach-mode terminal or a mast-web session — so the framing is conversational rather than autonomous (docs/positioning.md, "Change shape" -> DefaultInstruction: "Chat-mode gets conversational framing for attach-mode / mast-web operators").

View Source
const DefaultSingleTurnInstruction = `` /* 334-byte string literal not displayed */

DefaultSingleTurnInstruction is the fallback system prompt for SingleTurn-mode agents (NewSingleTurnAgent). SingleTurn agents are classifier-shaped — one model call, no tool loop — so the default is deliberately minimal (docs/positioning.md, "Change shape" -> DefaultInstruction: "SingleTurn-mode gets minimal framing (used by LLM-as-router classifiers)").

View Source
const DefaultTaskInstruction = `` /* 1270-byte string literal not displayed */

DefaultTaskInstruction is the fallback system prompt for Task-mode agents (NewTaskAgent). Task mode is mast's unattended workhorse, so the default encodes the unattended-loop discipline from docs/positioning.md ("Change shape" -> DefaultInstruction): conservative defaults; explicit state persistence to the eventlog; fail-fast on ambiguity; structured tool preference; plan-before-act; subagents over open-ended search.

View Source
const FinishTaskToolName = "finish_task"

FinishTaskToolName is ADK's name for the tool a Task-mode agent reports through. ADK's own constant lives in internal/workflowinternal, which is unreachable from here; TestFinishTaskIsStillCalledThat pins the spelling against a real specialist's declarations, so a rename upstream fails a test rather than silently disarming FinishOnStall.

View Source
const StallInstruction = "This agent ended its turn without reporting, and it has " +
	"no interactive channel, so whatever it was waiting for will never arrive. Treat " +
	"its part of the work as unchecked: do not infer that it found nothing, and say " +
	"plainly in your own answer what was not completed. Do not delegate the same " +
	"question to it again. Its last message follows verbatim."

StallInstruction is what the caller is told to do about it, and it is part of the payload rather than of the marker because the marker is for a counter and this is for a model.

It says three things, each of which was a way the first version went wrong: an empty result from a stalled agent means *not checked* rather than checked and clean; whatever the agent was waiting for is never coming, because a Task specialist on an unattended run has no interactive channel; and re-delegating the same question just spends the same turn again.

View Source
const StallMarker = "INCOMPLETE — no result from this agent."

StallMarker opens the text FinishOnStall writes on a stalled agent's behalf.

Exported because a degraded run has to be countable. The whole point of the guard is to turn a dead run into a partial answer, and a partial answer that nothing counts is indistinguishable from a complete one. Use Stalled to test for it rather than matching the string by hand.

Variables

This section is empty.

Functions

func DefaultStallPayload added in v0.4.0

func DefaultStallPayload(agentName, lastWords string) map[string]any

DefaultStallPayload is the payload for an agent that declares no OutputSchema. ADK gives such an agent a single required string parameter named "result", and the marker, the instruction and the agent's last words all go into it.

It is the default precisely because it is the one case where mast can know what an empty result looks like: the schema is ADK's own and it has exactly one field, so there is nothing to fabricate. Every other schema needs a StallPayload from the caller.

func FinishOnStall added in v0.4.0

func FinishOnStall(agentName string, payload StallPayload) llmagent.AfterModelCallback

FinishOnStall converts a Task agent's silent turn into a finish_task call, so an agent that stops without reporting costs its own part of the answer instead of the whole run. Install it through TaskAgentConfig.AfterModelCallbacks, or across a roster through specialists.BuildOptions.OnStall.

A nil payload means DefaultStallPayload, which is correct only for an agent with no OutputSchema; see StallPayload.

The failure this replaces

ADK ends the *caller's* turn when a Task sub-agent drains its iterator without calling finish_task. runChat is explicit about it (agent/llmagent/llm_agent_wrapper.go:518):

if out == nil {
	// Task sub-agent drained its iterator without ever
	// calling finish_task (e.g. it emitted a natural-
	// language question to the user and is waiting for
	// the reply). ...
	return false
}

Returning false stops the outer re-entry loop, so the coordinator never regains control and the run produces nothing at all. ADK's reasoning is sound for an interactive coordinator, where the user's next message should route back into the paused task. It is wrong for an unattended agent: nothing will ever answer, and the delegation stays unresolved forever. The damage is out of all proportion to the cause — a coordinator that had already collected results from three other specialists loses those too.

Why a callback and not a prompt

DefaultTaskInstruction already carries the unattended-loop discipline, and it is necessary and not sufficient. A specialist measured downstream asked its question anyway, because it had genuinely run out of moves: a prompt cannot forbid the only remaining action.

Why an AfterModelCallback and not a wrapper agent

A Task agent cannot be wrapped from outside ADK — see the note on TaskAgentConfig.AfterModelCallbacks. So the interception happens one layer down, at the response. For a Task agent, a model response carrying no function calls is terminal: the flow loop returns, the iterator drains, and runChat sees out == nil. Appending a finish_task call to that response makes the base flow execute it — handleFunctionCalls reads the post-callback response, and finalizeModelResponseEvent stamps the call with an ID from the same generator a model-issued call gets — which produces exactly the event pair runTask waits for: the call sets pendingFCArgs, and the tool's success response promotes them to the node's Output.

func NewCoordinator

func NewCoordinator(cfg CoordinatorConfig) (adkagent.Agent, error)

NewCoordinator constructs a Chat-mode LlmAgent with the given sub-agents and tools. The coordinator drives the top-level conversation; sub-agents handle delegated tasks.

When cfg.Instruction is empty, DefaultChatInstruction is used. A non-empty Instruction is used verbatim — callers with a bundle-specific prompt (e.g. router.Build's per-workload coordinator default) keep full control.

func NewEchoModel

func NewEchoModel(name string) model.LLM

NewEchoModel returns a model.LLM that responds to any request with a canned "acknowledged" reply summarising the last user message. It makes no network calls and requires no credentials, so it lets the runtime wiring be smoke-tested end-to-end without ADK-model dependencies.

This is a spike-only helper; it will be replaced by google.golang.org/adk/v2/model/gemini.NewModel once the real Gemini wiring lands (spike step 6).

func NewSingleTurnAgent

func NewSingleTurnAgent(cfg SingleTurnAgentConfig) (adkagent.Agent, error)

NewSingleTurnAgent constructs a SingleTurn-mode agent.

When cfg.Instruction is empty, DefaultSingleTurnInstruction is used. A non-empty Instruction is used verbatim — specialists keep full control of their prompt; nothing is prepended.

func NewTaskAgent

func NewTaskAgent(cfg TaskAgentConfig) (adkagent.Agent, error)

NewTaskAgent constructs a Task-mode agent. Suitable for per-failure-mode specialists (diagnose, remediate, return a structured digest).

When cfg.Instruction is empty, DefaultTaskInstruction is used. A non-empty Instruction is used verbatim — specialists keep full control of their prompt; nothing is prepended.

func NewToolActorModel added in v0.2.0

func NewToolActorModel(name string) model.LLM

NewToolActorModel returns a REQUEST-DRIVEN offline fake model that drives registered tool calls deterministically — the double the v0.2 end-to-end UAT (scripts/uat-v0.2.sh) needs to exercise the crash / drain / abort legs against a real, blocking MCP tool.

Why not the scripted provider (pkg/providers/mock)? The scripted model replays a fixed positional list of turns behind a single global cursor that RESETS to 0 on every process restart. The legs this UAT targets restart the daemon mid-flight, and the daemon's model-call count differs before vs. after a restart (auto-resume drives a continuation turn; an ambiguous-effect session refuses the mutating call; etc.), so a positional script and the live call sequence drift apart across exactly the crash-restart boundary under test. A request-driven double has no cursor: it decides purely from the current request, so it is restart-safe, session-independent, and needs no per-leg JSONL. This is a documented deviation from docs/uat-v0.2-plan.md's "scripted provider" note (which predates local/stdio MCP); see that doc's implementation status.

Behavior, decided per request:

  • Coordinator turn (Chat mode: a sub-agent "task" tool is offered, finish_task is not): if the delegation tool has not yet produced a response in the history, call it once (delegate to the worker); otherwise emit a short final text (the worker's result has come back — the turn is done).
  • Worker turn (Task mode: finish_task is offered): if an operator's approved change set is on this branch, make its calls as written (v0.4 W7.0 — see nextApprovedCall). Otherwise, if the incident envelope's reason selects a UAT tool (apply -> apply_change, read -> read_status) that is registered and not yet answered in the history, call it once; otherwise call finish_task. Its arguments satisfy the declared output schema when the specialist declares one (schemafill.go), including the change set it proposes (MAST_FAKE_PROPOSED_CHANGE).
  • Classifier turn (SingleTurn mode: no tools at all): reply with the bare incident reason, so graph dispatch routes to the real per-failure-mode specialist rather than the Default edge.

Selecting the tool from the inject reason keeps each leg's control in the harness's payload (reason "ApplyChange" vs "ReadStatus"), not in a brittle out-of-band script. It is offline and credential-free.

Set MAST_TOOLACTOR_DEBUG=1 to log each request's offered tool names and the chosen action to stderr — used when adapting the fixture to a new ADK tool-naming convention.

func StallText added in v0.4.0

func StallText(agentName, lastWords string) string

StallText composes the marker, the agent's name, the instruction, and the agent's own last words. A StallPayload for a richer schema should use it for whichever field carries prose, so that the convention Stalled recognises is the same one everywhere.

The last words are kept verbatim, and they are usually the most informative thing in the whole delegation: the question the agent wanted to ask names the data it could not get.

func Stalled added in v0.4.0

func Stalled(text string) bool

Stalled reports whether text is something FinishOnStall wrote — the summary field of a synthesised result, or the whole of a default one.

This is the counting seam. A caller that aggregates specialist results should run their text through it and report the total, the way it would report an error rate: a run rescued by the guard reached the end with a hole in it, and it must not be filed next to a run that had nothing missing.

Types

type CoordinatorConfig

type CoordinatorConfig struct {
	Name        string
	Description string
	Instruction string
	Model       model.LLM
	SubAgents   []adkagent.Agent
	Tools       []tool.Tool
	Toolsets    []tool.Toolset

	// BeforeModelCallbacks and AfterModelCallbacks pass straight through to
	// llmagent.Config, with ADK's semantics: a Before callback that returns a
	// non-nil response short-circuits the model call — and with it every After
	// callback — while an After callback that returns a non-nil response
	// replaces the one the model produced.
	//
	// Here for the same reason as TaskAgentConfig's — see the longer note
	// there — and on both so that the two configs do not differ arbitrarily
	// in what a caller can reach.
	BeforeModelCallbacks []llmagent.BeforeModelCallback
	AfterModelCallbacks  []llmagent.AfterModelCallback
}

CoordinatorConfig bundles the parameters for constructing a Chat-mode coordinator with Task or SingleTurn sub-agents. ADK auto-installs a task tool for each Task sub-agent and a single_turn tool for each SingleTurn sub-agent — the coordinator's LLM invokes sub-agents by calling those tools. No explicit agenttool.New call is required for this dispatch pattern.

type SingleTurnAgentConfig

type SingleTurnAgentConfig struct {
	Name         string
	Description  string
	Instruction  string
	Model        model.LLM
	InputSchema  *genai.Schema
	OutputSchema *genai.Schema
}

SingleTurnAgentConfig bundles the parameters for constructing a SingleTurn-mode LlmAgent. SingleTurn agents run exactly one model call; no finish_task loop. Cheap and predictable — the shape behind LLM-as-router classifiers.

type StallPayload added in v0.4.0

type StallPayload func(agentName, lastWords string) map[string]any

StallPayload builds the finish_task arguments FinishOnStall submits for an agent that stopped without reporting. It is given the agent's name and its last words — the text of the terminal turn, thinking blocks excluded — and must return a value that satisfies the agent's OutputSchema, because ADK validates the injected call exactly as it validates a model-issued one.

The payload is the caller's to write and deliberately not mast's to guess. A conforming value is not the same as an *empty* one, and only the roster that wrote the schema knows the difference: a report contract may forbid a severity above "ok" when the findings list is empty, in which case the severity is forced; a findings array must come back empty rather than filled, because a fabricated entry enters the caller's incident stream as a real fault named "a subagent stopped talking". mast has the machinery to synthesise a schema-conforming value — see conformingArgs in schemafill.go, which the offline fakes use — and using it here would be wrong for exactly that reason. It invents content. A stall report must invent none.

Whatever it returns should carry StallMarker at the head of the field a reader will actually see, so Stalled can find it.

type TaskAgentConfig

type TaskAgentConfig struct {
	Name         string
	Description  string
	Instruction  string
	Model        model.LLM
	Tools        []tool.Tool
	Toolsets     []tool.Toolset
	OutputSchema *genai.Schema

	// DisallowTransferToParent and DisallowTransferToPeers suppress ADK's
	// transfer_to_agent tool. They pass straight through to llmagent.Config,
	// and setting both empties the agent's transfer target list, which makes
	// shouldUseAutoFlow false and removes the tool *and* its instruction block
	// from every request the agent makes.
	//
	// Set both on a Task specialist that hangs off a Chat-mode coordinator.
	// Peers are already unreachable — transferTargets skips Task-mode agents,
	// so a specialist's only offered destination is the coordinator above it —
	// and taking that one destination is fatal: ADK forwards the transfer
	// in-process, so the coordinator's runChat executes under the specialist's
	// node context, and its first act is to re-dispatch the still-unresolved
	// delegation call through workflow.RunNode. That fails with "workflow:
	// RunNode called outside a dynamic node" and the run produces nothing.
	//
	// The flags are right on their own terms and not only as a crash fix.
	// Delegation to a specialist is one-way: a specialist that transfers
	// abandons the question it was asked, and the coordinator gets no digest
	// to merge.
	//
	// The zero value leaves ADK's default (transfer permitted) untouched.
	DisallowTransferToParent bool
	DisallowTransferToPeers  bool

	// BeforeModelCallbacks and AfterModelCallbacks pass straight through to
	// llmagent.Config, with ADK's semantics: a Before callback that returns a
	// non-nil response short-circuits the model call — and with it every After
	// callback — while an After callback that returns a non-nil response
	// replaces the one the model produced.
	//
	// The seam is here rather than composable from outside because a Task
	// agent cannot be wrapped. Both the coordinator's delegation-tool builder
	// and workflow.AgentNode recognise a Task agent by type-asserting to
	// internal/llminternal.Agent, which no package outside ADK can implement,
	// so an agent.Agent wrapper is silently demoted to a transfer target and
	// delegation stops working. Anything that has to intervene in a
	// specialist's turn has to do it at the response.
	//
	// FinishOnStall is the reason this field exists; see stall.go.
	BeforeModelCallbacks []llmagent.BeforeModelCallback
	AfterModelCallbacks  []llmagent.AfterModelCallback
}

TaskAgentConfig bundles the parameters for constructing a Task-mode LlmAgent. Task-mode agents auto-install the ADK-provided finish_task helper; the value the agent passes to finish_task becomes the agent's output.

Jump to

Keyboard shortcuts

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