agent

package
v0.4.0 Latest Latest
Warning

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

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

Documentation

Overview

Package agent provides the Adapter interface — the seam between the engine's dispatcher and external agent CLIs (Claude Code first, per awf-workflow(5) (Agent step) and runtime-design.md §8).

Phase 5 slice 5.1 ships only the interface, the typed `Registry` value type, the read-only `Resolver` subset, the shared types (AgentInvocation / AgentResult / AgentEvent / MetricSet / Caps), and the in-memory scripted fake (sub-package agent/fake). Slice 5.2 wires AgentStep dispatch in engine/local_dispatcher.go to call Adapter.Launch via Resolver. Slice 5.3 adds the real `agent/claude` sub-package backed by the Claude Code CLI. Cross-impl conformance is the conformance.RunSuite + RunAgentSuite job (Buckets 12/13/14/15).

Per runtime-design §3, agent depends on container (for Handle and Backend in adapter implementations) and ir (for RawConfig + JSONSchema in AgentInvocation). It does NOT depend on engine or state — Adapter impls execute work and return typed results; the dispatcher writes to state.

Capabilities() reports the adapter's claims about its typed-output pipeline. NativeSchema = true means the harness validates against the schema internally (Claude Code's --json-schema). NativeSchema = false means the adapter must produce typed output some other way (the structuring-call pattern — see Phase 5 design Appendix H); Bucket 15 (conformance.RunSuite) enforces the contract for such adapters.

SECURITY: AgentInvocation.Env is type SecretEnv. Two guarantees lock secret values out of the standard leak vectors:

  • All standard fmt verbs (`%v`, `%s`, `%q`, `%#v`, `%+v`) call SecretEnv's Stringer/GoStringer and emit a redacted string showing only key names. Locked by TestSecretEnv_RedactsInStandardFormatters and TestSecretEnv_RedactsInsideStruct.
  • The `Env` field is tagged `json:"-"`, so json.Marshal cannot serialize values. The engine's state log is JSON; secrets therefore cannot reach the journal. Locked by TestAgentInvocation_RetainsRawConfig.

Phase 6 obs's OTel projection MUST still avoid attaching AgentInvocation.Env to any span attribute (OTel attributes bypass the fmt and JSON guards entirely).

Index

Constants

View Source
const (
	AgentEventKindToolUse  = "tool_use"
	AgentEventKindToolCall = "tool_call"
)

Agent event Kind values that denote a tool call (Claude emits "tool_use"; droid emits "tool_call"). Consumers that count tool activity match these.

View Source
const (
	CostSourceReported = "reported"
	CostSourceDerived  = "derived"
)

MetricCost.Source values: an adapter that wraps a harness reporting its own dollar figure (Claude Code's total_cost_usd) stamps CostSourceReported; a future token-only adapter's derived cost (deferred — no pricing package ships yet; see runtime-design.md §10) would stamp CostSourceDerived.

View Source
const ToolResultHeadTail = 4

ToolResultHeadTail is the default head/tail line budget for a tool result's preview, passed to Elide.

Variables

View Source
var (
	ErrLiveLeaseConflict   = errors.New("agent: live lease conflict")
	ErrLiveLeaseStaleOwned = errors.New("agent: live lease stale owned")
	ErrLiveRegistryIO      = errors.New("agent: live registry I/O")
	ErrLiveReplayRequired  = errors.New("agent: live replay required")
	ErrPermissionDenied    = errors.New("agent: permission denied")
	ErrLiveRedaction       = errors.New("agent: live redaction")
)

Functions

func CountLines

func CountLines(s string) int

CountLines returns the human line count of s: 0 for empty; otherwise the number of "\n"-separated lines, where a trailing newline does NOT add a phantom line.

func DetectMIME

func DetectMIME(name string, content []byte) (string, error)

DetectMIME infers a forwardable MIME from the CONTENT via http.DetectContentType (pure-Go WHATWG sniffing: deterministic across hosts; covers pdf/png/jpeg/gif AND webp — the webp signature is in net/http/sniff.go). The file name is deliberately NOT consulted: mime.TypeByExtension reads the host MIME database, which varies per machine and would make the same workflow non-portable; content is the source of truth regardless. Returns an error for anything outside supportedMIME so a bad input fails fast (caller -> permanent_failure).

func Elide

func Elide(s string, headN, tailN int) string

Elide returns a bounded head+tail view of s: if s has <= headN+tailN lines, the whole thing (trailing newline trimmed); else the first headN lines, a "… N more line(s) …" marker, and the last tailN lines. Every returned line is capped to maxDisplayLine bytes (clip), so neither a huge line count nor a single pathological line can bloat EventDisplay.Text. The tail is kept because errors and results land at the end.

func ExtractJSONObject added in v0.3.0

func ExtractJSONObject(s string) (map[string]any, error)

ExtractJSONObject scans s for a JSON object, using json.Decoder (which consumes exactly one well-formed value), returning the LAST object that decodes (right bias for an agent that reasons before its final JSON). We do NOT schema-validate here (the engine's ValidateOutputMap does, so a wrong-but-valid object becomes a retryable schema failure) and do NOT pull in a json-repair dependency (it could fabricate a valid-but-wrong object).

This is the "directive+parse" output_schema fidelity tier's parser (see docs/runtime-design.md): the adapter asks the model, in-prompt, to make its final message a single JSON object, then parses free text for it as a backstop against prose/fences. Every adapter on that tier (goose, droid, and awfllm's non-API-constrained fallback) calls this one implementation — hoisted (F37) from three byte-identical copies. The only prior difference between them was the not-found error's package-name prefix, which carried no behavior (no caller matches on the string; each just wraps a non-nil error as *ErrUnparseableOutput).

func PrependFeedback added in v0.3.0

func PrependFeedback(prompt string, feedback map[string]any) (string, error)

PrependFeedback returns prompt with the gate's prior verdict prepended in the canonical "<previous verdict>\n<json>\n\n<prompt>" form, or prompt unchanged when feedback is empty. This is the single source of the repair-feedback wire format; every adapter that supports gate repair calls it. The format string and json.Marshal match the pre-refactor inline blocks byte-for-byte.

func RedactDisplayText

func RedactDisplayText(s string) string

RedactDisplayText removes common inline secret shapes from already-sanitized display text. It is best-effort defense-in-depth for previews, not a promise that provider-owned raw transcripts contain no secrets.

func SanitizeDisplayBytes

func SanitizeDisplayBytes(in []byte) string

SanitizeDisplayBytes is the byte-oriented variant for provider payloads. It drops invalid UTF-8 instead of replacing it, so previews cannot smuggle raw control bytes through replacement rendering.

func SanitizeDisplayText

func SanitizeDisplayText(s string) string

SanitizeDisplayText removes terminal control sequences and non-printing controls from text that may be rendered or copied into durable previews. Newline and tab are preserved because existing renderers use them for layout.

func StripThinkTags

func StripThinkTags(s string) string

StripThinkTags removes a leading reasoning block delimited by a closing think tag (</think> or </thinking>), keeping only the text AFTER the LAST such tag — reasoning models emit "...reasoning...</think>{json}". Uses the LAST tag (not the first) so multiple reasoning blocks are all dropped. If no closing tag is present, returns s unchanged (the brace-scan handles tag-less output).

func SummarizeToolInput

func SummarizeToolInput(raw json.RawMessage) string

SummarizeToolInput picks the most meaningful value from a tool call's argument object for a one-line display, without a per-tool registry: a salient key first, else the first scalar by sorted key. The result is clipped to maxDisplayLine. Returns "" if raw is empty or not a JSON object.

func SupportedMIMEs

func SupportedMIMEs() []string

SupportedMIMEs returns the detection-floor MIMEs (the keys of supportedMIME), sorted. It is the single list that downstream adapters consult to prove their per-transport MIME classification has not drifted from the floor.

Types

type Adapter

type Adapter interface {
	// Ref returns the agent-runtime identifier. Must match the workflow's
	// AgentStep.Uses literal exactly (e.g. "anthropic/claude-code"). Stable
	// over the adapter's lifetime; Registry uses this as the key.
	Ref() string

	// Capabilities reports static claims about this adapter's behavior.
	// Must be callable on a freshly-constructed adapter (no per-invocation
	// state needed). Tested by Bucket 14/15 routing in slice 5.4 + 5.2.
	Capabilities() Caps

	// Version returns the concrete version string this adapter resolves to
	// for the given container handle (the binary's PATH is per-container).
	// Called once per (ref, container) pair at run start; the result is
	// persisted in run.started.Runtimes. Resume re-invokes Version and
	// hard-errors on drift (spec §8 pinning). Adapters that don't have a
	// natural binary version (the fake) return a constructor-supplied
	// constant; adapters wrapping a CLI return the CLI's --version output
	// or equivalent.
	//
	// The handle is passed so an adapter wrapping a containerized binary
	// can `Backend.Exec(handle, "claude --version")` to discover the
	// version in the binary's actual deployment environment. Slice 5.1's
	// fake adapter ignores handle.
	Version(ctx context.Context, handle container.Handle) (string, error)

	// ValidateConfig is called once per node at run start (U1's cli/with_guard.go
	// walk) AND defensively at dispatch (slice 5.2, local_dispatcher_agent.go).
	// Returns *ErrInvalidConfig with a path-aware message on rejection. Strict
	// by default: unknown keys are errors (slice 5.3 enforces this for the
	// Claude Code adapter).
	ValidateConfig(with ir.RawConfig) error

	// Launch runs the agent inside the supplied handle. Slice 5.3 γ contract:
	// returns IMMEDIATELY with both channels open. The events channel emits
	// AgentEvents as the harness produces them (one per content block for
	// assistant messages); the caller MUST range over it concurrently with
	// awaiting outcome. The events channel CLOSES when the harness's stream
	// ends (process exit + stdout pipe drain). The outcome channel delivers
	// exactly one AgentOutcome AFTER events closes, then closes itself.
	//
	// === DRAIN-OR-LEAK CONTRACT =====================================
	// Callers MUST drain the events channel to completion. Failure to
	// drain blocks the adapter's parser goroutine on `events <- ev` once
	// the channel's internal buffer fills (Claude adapter: 16 events;
	// fake adapter: scripted-event-count + 1, so unbounded for practical
	// purposes). A blocked parser goroutine cannot send AgentOutcome on
	// outcomeCh — it hangs until ctx-cancel. Two safe patterns:
	//
	//   // Sequential drain (simplest):
	//   events, outcomeCh, err := adapter.Launch(...)
	//   if err != nil { /* pre-launch failure */ }
	//   for ev := range events { /* render, log, etc. */ }
	//   outcome := <-outcomeCh
	//
	//   // Concurrent drain (runAgent's pattern — preserves realtime UX):
	//   events, outcomeCh, err := adapter.Launch(...)
	//   if err != nil { /* pre-launch failure */ }
	//   drainDone := make(chan []AgentEvent, 1)
	//   go func() {
	//       var buf []AgentEvent
	//       for ev := range events {
	//           buf = append(buf, ev)
	//           // tap.Write(ev) here — this is where realtime renders.
	//       }
	//       drainDone <- buf
	//   }()
	//   outcome := <-outcomeCh
	//   buf := <-drainDone
	//
	// Adapters that buffer events to a size matching scripted/expected
	// event count (the fake's pattern) are forgiving — caller-side bugs
	// that ignore events don't leak. The Claude adapter cannot do this
	// because event count is unknown until claude exits, so it ships a
	// fixed-size buffer and relies on the caller to drain.
	// ================================================================
	//
	// On a non-nil err return (pre-launch), BOTH channels are nil — the
	// adapter never reached the launch step. On a successful return both
	// channels are non-nil and must be drained.
	//
	// Independence (spec §5.5): when Capabilities().PersistentSession is false,
	// Launch MUST NOT reference, store, or reuse any state from a prior Launch
	// call against the same Adapter instance. When PersistentSession is true,
	// any reuse must be explicit adapter-owned provider state (for example
	// with.session), and PR0a guards prevent such adapters from running in
	// gate.evaluate.
	//
	// (Pre-slice-5.3: signature was (AgentResult, <-chan AgentEvent,
	// error) with events pre-closed before Launch returned — buffer-then-
	// burst. The γ rewrite enables true realtime UX; the events
	// buffer-then-burst pattern is preserved by the caller's
	// `for range events` loop.)
	Launch(ctx context.Context, handle container.Handle, inv AgentInvocation) (<-chan AgentEvent, <-chan AgentOutcome, error)
}

Adapter is the seam the engine's dispatcher uses to launch external agent runtimes (Claude Code, Goose, Codex, Droid — Phase 5 ships only Claude Code, slice 5.3). Each concrete Adapter wraps one harness CLI; the interface stays uniform across them so a workflow's `uses:` refs can be satisfied by any registered adapter matching the Ref() string.

Lifecycle in slice 5.2 dispatcher (runAgentStep):

  1. resolver.Lookup(node.Uses) → Adapter (else *ErrAdapterNotFound)
  2. adapter.ValidateConfig(with) once per node, at run-start (U1's CLI walk, cli/with_guard.go's checkWithConfigForLoadedDefinition — ExitUsage on rejection, before the log opens) and again at dispatch time (defensive, local_dispatcher_agent.go) — strict schema validation.
  3. adapter.Launch(ctx, handle, inv) per attempt (one per gate attempt; one per retry). The returned <-chan AgentEvent stays open while the harness produces events; it MUST be drained by the caller (the dispatcher) and closed by the adapter before AgentResult is returned synchronously.

Phase 5 design decision 7: adapters with Caps.PersistentSession == false MUST NOT reuse sessions across calls. Each Launch is a fresh context — for Claude Code this means no --continue / --resume / --session-id flags; ValidateConfig rejects any with-key that would enable session reuse.

Adapters with Caps.PersistentSession == true may reuse provider-owned state only through explicit adapter-owned configuration (for example with.session); the core still treats `with:` as opaque. PR0a CLI and defensive engine guards reject PersistentSession adapters in gate.evaluate so the gate's independence property (spec §5.5) remains engine-enforced.

Phase 5 design decision 16: Capabilities() reports static claims about the adapter's typed-output pipeline and safety properties. The conformance suite routes adapters to the right buckets, and run-start plus defensive guards use safety caps to fail closed before Launch.

type AgentEvent

type AgentEvent struct {
	Kind    string `json:"kind"`
	Payload []byte `json:"payload,omitempty"`
	Stream  string `json:"stream,omitempty"`
	Live    bool   `json:"live,omitempty"`
	// Display is the adapter-populated, normalized summary the live renderer uses.
	// json:"-" — transient presentation. For Live events, the interpreter copies
	// sanitized scalar display metadata into agent.event; it never serializes this
	// struct directly. Zero value (DisplayOther) → terse fallback.
	Display EventDisplay `json:"-"`
}

AgentEvent is one slice of progress emitted live on the channel returned by Adapter.Launch. Kind is harness-specific (slice 5.3 enumerates the stream-json kinds for Claude Code: "system", "assistant", "user", "tool_use", "tool_result", "thinking", "result", "rate_limit"). Payload is the raw bytes a strict adapter's harness emitted for the event (CAS-offloaded by the dispatcher in slice 5.2 if larger than 4 KiB). Live adapters set Live and must put only normalized/redacted durable payload bytes here — never raw provider transcripts. Stream is "stdout" or "stderr" — typed loosely to match container/Backend's IOChunk.

type AgentInvocation

type AgentInvocation struct {
	NodePath   string       `json:"node_path"`      // engine/path output for the AgentStep (e.g. "graph[0]" or "gate[2].attempt-1.generate[0]")
	Uses       string       `json:"uses"`           // agent-runtime ref (must match Adapter.Ref())
	RunContext RunContext   `json:"run_context"`    // explicit run/epoch identity for live-capable adapters
	With       ir.RawConfig `json:"with,omitempty"` // opaque per-runtime config; validated by Adapter.ValidateConfig
	// RoleWith is the engine-resolved (scope-substituted) role with: for a
	// DerivedAdapter-backed step. The engine substitutes the role's raw templated
	// with: against the step scope and threads it here so the role layer merges as
	// already-rendered config. Nil for a non-role step or a direct caller (tests) —
	// then DerivedAdapter falls back to its stored raw role with:. Engine
	// operational state like Thread/ResumeSession; json:"-": never journaled.
	RoleWith        ir.RawConfig   `json:"-"`
	OutputSchema    *ir.JSONSchema `json:"output_schema,omitempty"`    // step's output_schema (the adapter passes to harness as --json-schema or layer-2 extractor schema)
	Env             SecretEnv      `json:"-"`                          // env vars forwarded into the harness exec (slice 5.3 reads ANTHROPIC_API_KEY etc.); never JSON-marshaled so secrets cannot reach the state log
	IdempotencyKey  string         `json:"idempotency_key,omitempty"`  // resolved-template; passed to harness env per spec §10
	Feedback        ir.RawConfig   `json:"feedback,omitempty"`         // prior gate verdict on repair attempts > 1 (nil on attempt 1)
	Thread          []ThreadTurn   `json:"thread,omitempty"`           // engine-assembled prior turns (separate channel from Feedback); generator-only
	ContextEvidence []ThreadTurn   `json:"context_evidence,omitempty"` // evaluator-only source evidence; not active conversation history
	// InputFiles carries resolved file bytes to a CONTAINERLESS adapter (the
	// containerless analog of container.InputFile staging). Empty for
	// container-backed steps. json:"-": bytes never reach the state log.
	InputFiles []InputFile `json:"-"`
	// Attempt is the 1-based retry-attempt index the engine is currently on for
	// this node (1 on the first try, 2 on the first retry, …). Threaded from
	// NodeIntent.Attempt by the dispatcher so an adapter can distinguish a fresh
	// try from a retry. Engine operational state like ResumeSession, not author
	// with: config; json:"-": never journaled. Zero when a caller builds an
	// AgentInvocation directly (tests) without wiring it.
	Attempt int `json:"-"`
	// RecoveryContinue is true when the resolved retry.recovery strategy for this
	// step is "continue" (resume the persistent session on a retry) rather than
	// "restart". Set by the engine from the merged retry.Policy (engine resolves an
	// unset value to a per-adapter default); meaningful only for a PersistentSession
	// adapter. Combined with Attempt>0 it tells the adapter that a leftover
	// in-progress turn from a prior attempt in THIS run may be abandoned and the
	// thread resumed, instead of hard-halting for a cross-process replay. Engine
	// operational state like Attempt/ResumeSession; json:"-": never journaled.
	RecoveryContinue bool `json:"-"`
	// ResumeSession is set true by the engine when it successfully restored a
	// committed session transcript for this node before calling adapter.Launch.
	// The adapter uses it to select the correct CLI flag:
	//   - false (fresh turn): pass --session-id <uuid> (create/adopt a session)
	//   - true  (restored turn): pass --resume <uuid> (re-prime from restored transcript)
	// It is engine operational state, not author with: config — modelled after
	// the existing Thread/ContextEvidence fields. json:"-": never journaled.
	ResumeSession bool `json:"-"`
	// SessionConfigDir is the absolute per-run CLAUDE_CONFIG_DIR the engine
	// computes for claude-family adapters (<stagingRoot>/claude-session/<run-id>);
	// the adapter forwards it as the CLAUDE_CONFIG_DIR env var so claude relocates
	// its whole config tree per run. Set for any container-backed IsolatedConfigDir
	// adapter — both the base anthropic/claude-code adapter (config isolation only)
	// and the session adapter (which ALSO captures the projects/ subtree). Empty
	// for non-claude / containerless steps.
	// Engine-set, like ResumeSession — keep it out of the journal.
	SessionConfigDir string `json:"-"`
	// WorkflowDir is the absolute directory containing the step's own module's
	// workflow file (ir.LoadedModule.WorkflowPath's directory — the loader
	// resolves imports/assets relative to this same directory). The engine sets
	// it for every agent step (engine/agent_step.go, from ictx.def.Module).
	// Consumed by a Containerless PersistentSession adapter (agent/codexlive,
	// F33) to default a host-side `cwd` when the workflow author omits it —
	// codexlive has no container filesystem, so its `cwd` is a real host path
	// the harness operates in. Engine-set, like SessionConfigDir; empty only
	// when a caller builds an AgentInvocation directly (tests) without wiring it.
	WorkflowDir string `json:"-"`
}

AgentInvocation is the per-call input handed to Adapter.Launch. Slice 5.2 wiring (engine/local_dispatcher.go runAgentStep) constructs one of these per AgentStep, resolves the templated With through template.Evaluator, reads Env from the run-start env-passthrough, and threads Feedback from the gate's prior verdict on repair attempts.

SECURITY: Env is SecretEnv. Its values redact under all standard fmt verbs (locked by TestSecretEnv_RedactsInStandardFormatters), and the `json:"-"` tag prevents JSON serialization from ever exposing them. See the SecretEnv doc-comment for the formatter + JSON guarantees.

type AgentOutcome

type AgentOutcome struct {
	Result AgentResult `json:"result,omitempty"`
	Err    error       `json:"-"` // errors don't JSON-marshal well; engine state log handles errors separately
}

AgentOutcome is the envelope the streaming Adapter.Launch contract delivers via its outcome channel (slice 5.3 γ). Carries either an AgentResult (happy path) or an Err (in-flight failure: *ErrUnparseableOutput, *ErrAgentLaunch, *ErrAuthFailureSentinel wrapped via *ErrAgentLaunch). Exactly one is populated; the other is the type's zero value.

Why an envelope vs (AgentResult, error) tuple? The outcome channel must carry both pieces atomically — a struct on a channel is the idiomatic way to do that in Go. Tuple-returning a 2-value `(<-chan AgentResult, <-chan error)` would force the caller to select between two channels with no guaranteed ordering; the envelope avoids that.

type AgentResult

type AgentResult struct {
	Output   map[string]any    `json:"output,omitempty"`
	ExitCode int               `json:"exit_code"`
	Metrics  MetricSet         `json:"metrics"`
	Files    map[string][]byte `json:"files,omitempty"`
	// Live is a data-only handoff from PersistentSession adapters to the
	// dispatcher/interpreter. It carries operational registry metadata for
	// post-commit finalization; it is never a bindable output.
	Live *LiveDispatch `json:"-"`
	// Transcript is the adapter-provided verbatim {clean user prompt, verbatim
	// final assistant message} pair for continues: threading. The engine reads
	// no with: key — the ADAPTER supplies both halves (reading its own with:
	// legitimately). json:"-": never journaled raw (Phase 4 Commit content-
	// addresses it as a blob ref when the step participates in a conversation),
	// matching the Env SecretEnv json:"-" precedent.
	Transcript ThreadTurn `json:"-"`
}

AgentResult is the synchronous return of Adapter.Launch. Output is the validated typed output matching the step's OutputSchema (per spec §4.2 — references bind only to typed fields). ExitCode mirrors a process exit code shape (0 = ok); the engine reads it but currently only treats nonzero as a transport-class signal — quality verdicts live in Output. Metrics carries OTel-bound counters (Phase 6 obs reads them). Files contains adapter-provided extra artifacts to commit. For container-backed agent steps, the engine captures declared output_files from the container at the commit boundary; adapters do not need to read those paths themselves. Containerless adapters cannot declare output_files.

type Caps

type Caps struct {
	NativeSchema bool `json:"native_schema"`

	// Containerless reports that this adapter performs its work WITHOUT a
	// container (e.g. a direct network call). When true, an agent step may
	// omit `container:` (ir/validate_structural.go allows the empty ref; the
	// run-start guard in cli/runtimes.go permits it only for these adapters).
	// Zero value false -> every CLI-wrapping adapter keeps requiring a
	// container, unchanged.
	Containerless bool `json:"containerless"`

	// Threaded reports that this adapter prepends an engine-supplied
	// AgentInvocation.Thread to its request (continues: threading). This drives
	// conformance routing plus CLI and defensive engine guards (a continues:
	// step against a non-Threaded adapter fails fast). Direct Containerless
	// precedent.
	Threaded bool `json:"threaded,omitempty"`

	// ContextEvidence reports that this adapter can render engine-assembled
	// evaluator source context as evidence, without treating it as an active
	// conversation continuation. Normal continues: threading still uses Threaded.
	ContextEvidence bool `json:"context_evidence,omitempty"`

	// PersistentSession reports that this adapter can reuse a live/persistent
	// session across launches. It is a static runtime declaration used by CLI
	// guards, defensive engine guards, conformance routing, and docs. Zero value
	// false preserves the fresh-launch behavior required for gate evaluators;
	// PersistentSession adapters are rejected in gate.evaluate contexts while
	// remaining allowed elsewhere, including gate.generate.
	PersistentSession bool `json:"persistent_session,omitempty"`

	// InlineInputFiles reports that a Containerless adapter delivers a step's
	// input_files to the model as inline message parts (agent.InputFile) rather
	// than staging into a container. awf/llm does this; codex-live does not.
	// Zero value false FAILS CLOSED: the run-start guard rejects a containerless
	// step declaring input_files against a non-inline adapter instead of
	// silently dropping the files.
	InlineInputFiles bool `json:"inline_input_files,omitempty"`

	// IsolatedConfigDir reports that this adapter benefits from a per-run,
	// RunID-keyed isolated config directory (mapped to its config-dir env var,
	// e.g. CLAUDE_CONFIG_DIR) so concurrent runs do not collide on shared host
	// config. The engine computes the dir (<staging-root>/claude-session/<run-id>)
	// and threads it via AgentInvocation.SessionConfigDir; the adapter sets it on
	// the exec env. Container-backed only (a Containerless adapter has no container
	// filesystem to isolate). Orthogonal to PersistentSession, which ADDITIONALLY
	// captures/restores the session as that dir's projects/ subtree.
	IsolatedConfigDir bool `json:"isolated_config_dir,omitempty"`

	// SurfacesLiveness grades how finely this adapter streams live progress
	// signals a stall watchdog can trust as proof the turn is still working.
	// The zero value LivenessNone means "not measured / no signal", so an
	// adapter that hasn't been characterized never overclaims. Only adapters we
	// have MEASURED set a higher tier (codexlive = Coarse, claude-family = Fine).
	SurfacesLiveness Liveness `json:"surfaces_liveness,omitempty"`
}

Caps reports an Adapter's static capabilities. Read by the conformance suite to route adapters to the correct bucket (NativeSchema=true -> Bucket 14 path; NativeSchema=false -> Bucket 15 layer-2 contract). The typed-output contract remains AgentResult.Output regardless of how the adapter produced it; other fields are static declarations used by CLI guards, defensive engine guards, conformance routing, and documentation.

Future fields land here additively as new Phase 5+ adapters surface distinguishing capabilities (e.g. SupportsThinking, SupportsTools).

type CredentialNamer

type CredentialNamer interface {
	RequiredEnv() []string
}

CredentialNamer is an optional Adapter interface: RequiredEnv returns the CREDENTIAL env var names this adapter can authenticate with. The run-start credential-presence preflight WARNS (never fails) when NONE of them is set — surfacing a likely Launch failure before any container boots, while preserving the "missing env is silently omitted; auth fails at Launch" contract (advisory).

type DerivedAdapter

type DerivedAdapter struct {
	// contains filtered or unexported fields
}

DerivedAdapter is a role-bound view of a base Adapter (C3). It is registered under the role name (Ref() == roleName) so the engine's unchanged resolver.Lookup(as.Uses) finds it. Every call overlays the step's opaque with: ON TOP of the role's with: (step key wins) and forwards to the base — the merge is KEY-BLIND (maps.Copy, never inspecting a key), so with:-opacity holds: AWF still never interprets a with: field; the named base adapter does.

Independence (spec §5.5) and the no-session contract are the base adapter's concern — DerivedAdapter holds no per-call state; the role with: is fixed at construction, though a per-call engine-resolved override (AgentInvocation.RoleWith) may replace it at Launch (see mergeRole).

func NewDerivedAdapter

func NewDerivedAdapter(roleName string, base Adapter, roleWith ir.RawConfig) *DerivedAdapter

NewDerivedAdapter binds base under roleName with roleWith. roleWith is defensive-copied; nil is treated as empty.

func (*DerivedAdapter) Capabilities

func (d *DerivedAdapter) Capabilities() Caps

Capabilities / Version delegate to the base (the role does not change the binary or its typed-output pipeline).

func (*DerivedAdapter) Launch

func (d *DerivedAdapter) Launch(ctx context.Context, h container.Handle, inv AgentInvocation) (<-chan AgentEvent, <-chan AgentOutcome, error)

func (*DerivedAdapter) PreflightResume

func (d *DerivedAdapter) PreflightResume(ctx context.Context, req LiveResumePreflightRequest) error

func (*DerivedAdapter) Ref

func (d *DerivedAdapter) Ref() string

Ref returns the ROLE name — so the workflow's uses: <role> resolves here and run-start Version pinning records (role, container) distinctly from the base.

func (*DerivedAdapter) RoleWith added in v0.4.0

func (d *DerivedAdapter) RoleWith() ir.RawConfig

RoleWith returns a fresh copy of the role's raw with: (may contain templates).

func (*DerivedAdapter) RunToolLoop

func (*DerivedAdapter) ValidateConfig

func (d *DerivedAdapter) ValidateConfig(with ir.RawConfig) error

func (*DerivedAdapter) ValidateResolvedConfig added in v0.4.0

func (d *DerivedAdapter) ValidateResolvedConfig(resolvedRole, step ir.RawConfig) error

ValidateResolvedConfig validates base config against the resolved role layer (mergeRole) — the exact map Launch forwards — instead of the raw d.roleWith.

func (*DerivedAdapter) Version

func (d *DerivedAdapter) Version(ctx context.Context, h container.Handle) (string, error)

type DisplayClass

type DisplayClass uint8

DisplayClass is the normalized, agent-agnostic category of a live AgentEvent. Each agent.Adapter maps its harness-specific event vocabulary onto these so a single renderer can present any agent's events without parsing its raw JSON. The zero value is DisplayOther (unclassified → terse fallback).

const (
	DisplayOther          DisplayClass = iota // unknown/unclassified → terse fallback
	DisplayInit                               // session start (model, tools)
	DisplayAssistant                          // assistant narration (full)
	DisplayAssistantDelta                     // an incremental assistant token-delta (renderer writes raw, no newline; concatenates char-by-char)
	DisplayReasoning                          // chain-of-thought (dimmed, elided)
	DisplayToolCall                           // a tool invocation (one-line summary)
	DisplayToolResult                         // a tool's output (collapsed: status + size + head/tail)
	DisplayFinal                              // the final answer (full)
	DisplayError                              // an error (highlighted)
	DisplayNotice                             // transient notice (rate-limit/retry/status)
)

func (DisplayClass) String

func (c DisplayClass) String() string

type DisplayStreamSanitizer

type DisplayStreamSanitizer struct {
	// contains filtered or unexported fields
}

DisplayStreamSanitizer sanitizes a sequence of chunks from one terminal-like stream. It carries incomplete escape/control sequences across chunks so a provider cannot split OSC/DCS/APC/PM content and leak the tail in the next AgentEvent.

func (*DisplayStreamSanitizer) SanitizeBytes

func (s *DisplayStreamSanitizer) SanitizeBytes(in []byte) string

func (*DisplayStreamSanitizer) SanitizeText

func (s *DisplayStreamSanitizer) SanitizeText(chunk string) string

type ErrAdapterAlreadyRegistered

type ErrAdapterAlreadyRegistered struct {
	Ref string
}

ErrAdapterAlreadyRegistered is returned by Registry.Register when a second adapter is registered under the same Ref(). The Registry never silently overwrites — the caller (CLI start-time wiring) controls registration order and a duplicate signals a configuration bug.

func (*ErrAdapterAlreadyRegistered) Error

type ErrAdapterNotFound

type ErrAdapterNotFound struct {
	Ref string
}

ErrAdapterNotFound is returned by Resolver.Lookup's caller via wrapping when a workflow's `uses:` ref doesn't match any registered adapter. Lookup itself returns (nil, false); the caller (slice 5.2 dispatcher and this slice's CLI resolveRuntimes helper) wraps in this typed error for the top-level error path.

func (*ErrAdapterNotFound) Error

func (e *ErrAdapterNotFound) Error() string

type ErrAgentLaunch

type ErrAgentLaunch struct {
	Cause     error
	RetryHint *RetryHint
}

ErrAgentLaunch is returned by Adapter.Launch when the harness failed to start or terminated abnormally (transport / launch class). Wraps the concrete cause for errors.Is matching at the top of the call chain. The slice 5.2 dispatcher maps this to retryable_failure.

RetryHint is optional: when the cause is a provider rate-limit/overload with a known retry window, the adapter sets it and the engine honors that delay.

func (*ErrAgentLaunch) Error

func (e *ErrAgentLaunch) Error() string

func (*ErrAgentLaunch) Unwrap

func (e *ErrAgentLaunch) Unwrap() error

type ErrInvalidConfig

type ErrInvalidConfig struct {
	Ref    string // adapter ref (e.g. "anthropic/claude-code")
	Key    string // the with-key that caused the error (e.g. "session_id"); empty if the whole config is bad
	Reason string // human-readable reason

	// KeyUnknown is true when the rejection is an unknown with-key (a typo'd
	// or wrong-adapter key name). The run-start guard always surfaces these;
	// value-shape errors are suppressed for templated ("{{...}}") values.
	KeyUnknown bool
}

ErrInvalidConfig is the result of Adapter.ValidateConfig rejection. Carries enough context to point at the offending workflow key in the CLI's diagnostic.

func (*ErrInvalidConfig) Error

func (e *ErrInvalidConfig) Error() string

type ErrUnparseableOutput

type ErrUnparseableOutput struct {
	NodePath string
}

ErrUnparseableOutput is returned by Adapter.Launch when the harness produced output that doesn't validate against AgentInvocation.OutputSchema. The slice 5.2 dispatcher maps this to retryable_failure (the engine's existing retry policy handles re-execution).

func (*ErrUnparseableOutput) Error

func (e *ErrUnparseableOutput) Error() string

type EventDisplay

type EventDisplay struct {
	Class   DisplayClass
	Tool    string // tool name (ToolCall/ToolResult)
	Text    string // see semantics above
	Lines   int    // ToolResult: full output line count (0 = n/a)
	Bytes   int    // ToolResult: full output byte size (0 = n/a)
	IsError bool   // ToolResult/Error: failure state
}

EventDisplay is the adapter-populated, presentation-neutral summary the live renderer consumes. NEVER journaled (the field is json:"-"); the durable record is AgentEvent.Payload. The adapter fills only the fields relevant to Class.

Text by Class: Assistant/Final → the full text; AssistantDelta → one incremental chunk of assistant text (the renderer writes it without a trailing newline so a stream of deltas renders character-by-character; the durable record is the per-event Payload as usual); Reasoning → the reasoning text (renderer elides); ToolCall → a short arg summary; ToolResult → a bounded head+tail of the output (the adapter elides — the full body stays in Payload); Error/Notice/Init → the message/summary. Lines/Bytes are the FULL output counts for ToolResult.

MUST hold only scalar fields: EventDisplay is embedded by value in every AgentEvent, so it stays ==-comparable and cheap to copy. (AgentEvent itself is not ==-comparable — its Payload is a []byte.) A slice/map/pointer field here would break EventDisplay's value semantics and alias bytes into each event.

type InputFile

type InputFile struct {
	Name    string
	Content []byte
	MIME    string
}

InputFile is one resolved file handed to a containerless adapter. Name is the logical label from the step's input_files key (NOT a container path). MIME is inferred from the CONTENT by sniffing — never from the file name.

type LiveDispatch

type LiveDispatch struct {
	AdapterRef     string
	SessionKey     string
	SessionKeyHash string
	LeaseID        string
	ActiveTurnID   string
	ProviderTurnID string
	RunID          string
	NodePath       string
	Epoch          uint32
	CommittedUnix  int64
}

LiveDispatch is the adapter-owned, data-only metadata needed to reconcile a successful persistent turn after node.completed has been committed. The raw SessionKey is in-memory only so the registry path can be updated; durable AWF events should use SessionKeyHash if they ever need to reference the session.

type LiveResumePreflightRequest

type LiveResumePreflightRequest struct {
	NodePath     string       `json:"node_path"`
	AdapterRef   string       `json:"adapter_ref"`
	With         ir.RawConfig `json:"with,omitempty"`
	RunID        string       `json:"run_id"`
	CurrentEpoch uint32       `json:"current_epoch"`
	NextEpoch    uint32       `json:"next_epoch"`
}

LiveResumePreflightRequest is the data-only request the CLI gives a live adapter before appending run.resumed. The core treats With as opaque after template substitution; adapter-owned keys such as session/cwd are validated by the adapter's PreflightResume implementation.

type Liveness added in v0.3.0

type Liveness uint8

Liveness grades the degree to which an Adapter surfaces live progress signals (streamed deltas) between the start and end of a turn. A stall watchdog reads this to decide how confidently a quiet stretch means "hung" rather than "working silently". The zero value is LivenessNone so an unmeasured adapter honestly declares no signal instead of overclaiming.

const (
	LivenessNone   Liveness = iota // no live progress signal (default; unmeasured)
	LivenessCoarse                 // coarse progress deltas (e.g. reasoning-summary chunks)
	LivenessFine                   // fine-grained streamed deltas (e.g. thinking_delta tokens)
)

type MetricCost

type MetricCost struct {
	Source   string  `json:"source,omitempty"`   // CostSourceReported | CostSourceDerived
	Currency string  `json:"currency,omitempty"` // ISO-4217; empty == USD
	Total    float64 `json:"total,omitempty"`
	Input    float64 `json:"input,omitempty"`  // derived only; includes cache, folded (future task)
	Output   float64 `json:"output,omitempty"` // derived only
}

MetricCost is a step's cost. Two states discriminated by Source:

reported = harness gave a per-call total (Claude total_cost_usd): Total set, NO split.
derived  = computed from rates: INVARIANT Total == Input + Output.

type MetricSet

type MetricSet struct {
	Cost   MetricCost   `json:"cost"`
	Tokens MetricTokens `json:"tokens"`
	Turns  int          `json:"turns"`
	// Model is the id pricing keyed on: the resolved model if the harness reported it
	// (claude system/init, codexlive thread/start), else the requested with:{model}, else empty.
	Model string `json:"model,omitempty"`
}

MetricSet aggregates the per-step counters the obs package (Phase 6) projects to OTel.

type MetricTokens

type MetricTokens struct {
	Input              int `json:"input"`
	Output             int `json:"output"`
	CacheCreationInput int `json:"cache_creation_input,omitempty"`
	CacheReadInput     int `json:"cache_read_input,omitempty"`
}

type ReactTurn

type ReactTurn struct {
	Role       string     `json:"role"`
	Content    string     `json:"content,omitempty"`
	ToolCalls  []ToolCall `json:"tool_calls,omitempty"`   // assistant turns only; OMIT (not []) when none
	ToolCallID string     `json:"tool_call_id,omitempty"` // tool turns only
}

ReactTurn is one message in an engine-owned tool-loop conversation. Role is "user" | "assistant" | "tool". An assistant turn may carry ToolCalls; a tool turn carries ToolCallID + Content (the result). Distinct from ThreadTurn (continues:) which cannot represent tool_calls / tool-role messages.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is the CLI-side store of registered Adapters. The zero value is usable (no New() constructor — matches bytes.Buffer / sync.Mutex stdlib shape). Goroutine-safe: Register takes a write lock; Lookup and Refs take a read lock (Phase 3 parallel branches Lookup concurrently after Phase 5 slice 5.2 wires AgentStep).

Registry satisfies the Resolver interface — the read-only subset the engine's dispatcher (slice 5.2) takes. Use *Registry at CLI start-time (where Register matters); the engine works with Resolver only.

func (*Registry) Lookup

func (r *Registry) Lookup(ref string) (Adapter, bool)

Lookup returns the registered Adapter for ref (true) or (nil, false). Implements Resolver. Concurrency-safe.

func (*Registry) Refs

func (r *Registry) Refs() []string

Refs returns all registered refs in sorted order. Phase 6 obs reads this for the `awf inspect` output; sort guarantees deterministic display.

Go 1.23+ idiom: maps.Keys returns an iter.Seq[string] (lazy iteration over the map's keys), slices.Sorted consumes it into a sorted slice in one allocation. Equivalent in semantics to the manual loop + sort.Strings version, but cleaner and one fewer named intermediate.

func (*Registry) Register

func (r *Registry) Register(a Adapter) error

Register adds an adapter under its Ref(). Returns *ErrAdapterAlreadyRegistered if a different adapter is already registered under that ref. Empty refs are rejected with a non-typed error (configuration bug; not worth a dedicated type since callers shouldn't be passing empty refs).

type Resolver

type Resolver interface {
	// Lookup returns the Adapter registered under ref (true) or (nil, false)
	// if no such adapter is registered. Concurrency-safe; multiple goroutines
	// may Lookup concurrently (Phase 3 parallel branches dispatch concurrently
	// after Phase 5 wires AgentStep).
	Lookup(ref string) (Adapter, bool)
}

Resolver is the read-only subset of Registry. The engine's dispatcher (slice 5.2) takes a Resolver, not a *Registry, so the dispatcher cannot Register new adapters mid-run (CLAUDE.md "interpreter is the only writer to state" — the registry is fixed at CLI start-time per Phase 5 decision 3 / 17). cli/run.go and cli/resume.go (this slice) work with *Registry; engine internals work with Resolver.

type ResumePreflighter

type ResumePreflighter interface {
	PreflightResume(context.Context, LiveResumePreflightRequest) error
}

ResumePreflighter is implemented only by adapters whose Capabilities report PersistentSession. The CLI calls it on resume after folding the AWF log and before appending run.resumed, giving the adapter a chance to reject unsafe live replay without mutating AWF state.

type RetryHint

type RetryHint struct {
	RetryAfter time.Duration
}

RetryHint is an optional, server-derived "wait this long before retrying" signal an adapter may attach to a transient ErrAgentLaunch. RetryAfter is parsed from a provider Retry-After header or a rate-limit reset time (e.g. Anthropic anthropic-ratelimit-*-reset, or the Claude Code rate_limit event's resetsAt). The engine dispatcher lifts it onto DispatchResult.RetryAfter and engine.RunWithRetry honors it (capped at retry.MaxHonoredRetryAfter) so AWF waits out the actual window instead of the short exp curve. Runtime-only — never journaled.

type RoleResolvedValidator added in v0.4.0

type RoleResolvedValidator interface {
	ValidateResolvedConfig(resolvedRole, step ir.RawConfig) error
}

RoleResolvedValidator validates the config a role-backed Launch will actually send: the ENGINE-RESOLVED role layer (not the raw template) overlaid by the step. Without this, dispatch-time ValidateConfig(with) validates the raw (still `{{ input.* }}`-templated) d.roleWith via merge/ValidateConfig above, which disagrees with what Launch forwards (mergeRole(inv.RoleWith, inv.With)).

type RoleWithProvider added in v0.4.0

type RoleWithProvider interface {
	RoleWith() ir.RawConfig
}

RoleWithProvider is implemented by a role-bound adapter (DerivedAdapter). The engine reads the raw (possibly {{ input.* }}-templated) role with: so it can substitute it against the step scope before launch. A base adapter does not implement it (nil role layer).

type RunContext

type RunContext struct {
	RunID        string `json:"run_id"`
	CurrentEpoch uint32 `json:"current_epoch"`
	NextEpoch    uint32 `json:"next_epoch"`
}

RunContext is the explicit run identity passed to every agent invocation. CurrentEpoch is the epoch the invocation is executing in; NextEpoch is the epoch a live replay request should target. Normal dispatch uses the same value for both. Resume preflight can construct requests with distinct values.

type SecretEnv

type SecretEnv map[string]string

SecretEnv is the type used for env-passthrough values that contain secrets (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN).

Formatter safety (locked by TestSecretEnv_RedactsInStandardFormatters): SecretEnv's String + GoString methods redact values under every fmt verb that consults them — `%v`, `%s`, `%q`, `%#v`, and `%+v`. (Even %+v calls Stringer on a defined map type; the redaction holds whether SecretEnv is printed directly or as a field of AgentInvocation.)

JSON safety: AgentInvocation.Env is tagged `json:"-"` so json.Marshal can never serialize the values. The engine's state log is JSON; this guarantees env values never reach the journal even if a future caller marshals an AgentInvocation.

func (SecretEnv) GoString

func (e SecretEnv) GoString() string

GoString returns the same redacted representation. Go's %#v formatter consults this; without GoString, %#v would print the literal map and leak secrets.

func (SecretEnv) String

func (e SecretEnv) String() string

String returns a redacted representation showing key names but not values. Called by fmt.Sprintf with %v / %s / %q.

type ThreadTurn

type ThreadTurn struct {
	User      string `json:"user"`
	Assistant string `json:"assistant"`
}

ThreadTurn is one prior (user, assistant) exchange in an engine-owned conversation. The engine assembles a slice of these from the durable log (continues: threading) and feeds it to the generating turn only; it is never a bindable reference and never visible to until/templates.

type ToolCall

type ToolCall struct {
	Index     int    `json:"index"` // stable position; the J in react[N].round-K.tool-J
	ID        string `json:"id"`    // matches the tool-role message's tool_call_id
	Name      string `json:"name"`
	Arguments string `json:"arguments"`
}

ToolCall is one model-emitted tool invocation. Arguments is the RAW model-emitted JSON string, stored verbatim (the §4.5 determinism invariant — never reserialized).

type ToolDef

type ToolDef struct {
	Name        string         `json:"name"`
	Description string         `json:"description"`
	InputSchema map[string]any `json:"input_schema"`
}

ToolDef is a tool offered to the model (name + description + parameters schema).

type ToolLoopInvocation

type ToolLoopInvocation struct {
	NodePath     string         `json:"node_path"`
	Uses         string         `json:"uses"`
	With         ir.RawConfig   `json:"with,omitempty"`
	Messages     []ReactTurn    `json:"messages"`
	Tools        []ToolDef      `json:"tools"`
	OutputSchema *ir.JSONSchema `json:"output_schema,omitempty"` // steers response_format (§6); engine validates post-hoc
}

ToolLoopInvocation is ONE model call with tools attached + the full prior message history. The engine (runReact) owns the history; the adapter just executes the call. NOTE: no Env field — the awf/llm key rides a.env at adapter construction (config.go), never the invocation (rev #17).

type ToolLoopResult

type ToolLoopResult struct {
	Text         string         `json:"text"`
	Output       map[string]any `json:"output,omitempty"`
	ToolCalls    []ToolCall     `json:"tool_calls,omitempty"`
	FinishReason string         `json:"finish_reason"`
	Metrics      *MetricSet     `json:"metrics,omitempty"`
}

ToolLoopResult is the model's response for one call. Output is the PARSED final answer (rev #4): on a natural-stop round with an output_schema, RunToolLoop parses the assistant text into Output via the adapter's own extractJSONObject and returns *ErrUnparseableOutput on a miss — so the ENGINE validates Output with engine.ValidateOutputMap WITHOUT importing agent/awfllm. Output is nil on tool_calls rounds, on max_turns truncation, and when no output_schema is declared.

type ToolLoopRunner

type ToolLoopRunner interface {
	RunToolLoop(ctx context.Context, inv ToolLoopInvocation) (ToolLoopResult, error)
}

ToolLoopRunner is implemented only by adapters that can run an engine-mediated tool loop (Caps.Containerless && Caps.Threaded — in v1, only awf/llm). It is an OPTIONAL interface (the ResumePreflighter pattern), NOT part of the Adapter seam, so the other four adapters are untouched. runReact obtains it via a type assertion.

Directories

Path Synopsis
Package awfllm implements agent.Adapter as a single, streaming LLM call against any OpenAI-compatible Chat Completions endpoint (OpenAI, Ollama, vLLM, llama.cpp, LM Studio, LiteLLM/Bifrost gateways).
Package awfllm implements agent.Adapter as a single, streaming LLM call against any OpenAI-compatible Chat Completions endpoint (OpenAI, Ollama, vLLM, llama.cpp, LM Studio, LiteLLM/Bifrost gateways).
Package claude implements agent.Adapter against the Claude Code CLI.
Package claude implements agent.Adapter against the Claude Code CLI.
Package claudesession implements agent.Adapter for Claude Code with deterministic session-id reuse (anthropic/claude-code-session).
Package claudesession implements agent.Adapter for Claude Code with deterministic session-id reuse (anthropic/claude-code-session).
Package codex implements agent.Adapter against OpenAI's `codex` CLI (the `codex exec` non-interactive subcommand).
Package codex implements agent.Adapter against OpenAI's `codex` CLI (the `codex exec` non-interactive subcommand).
Package droid implements agent.Adapter against Factory AI's `droid` CLI (the `droid exec` non-interactive subcommand).
Package droid implements agent.Adapter against Factory AI's `droid` CLI (the `droid exec` non-interactive subcommand).
Package fake provides an in-memory scripted agent.Adapter implementation.
Package fake provides an in-memory scripted agent.Adapter implementation.
Package goose implements agent.Adapter against Block's `goose` CLI (the `goose run` non-interactive subcommand).
Package goose implements agent.Adapter against Block's `goose` CLI (the `goose run` non-interactive subcommand).

Jump to

Keyboard shortcuts

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