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
- Variables
- func CountLines(s string) int
- func DetectMIME(name string, content []byte) (string, error)
- func Elide(s string, headN, tailN int) string
- func RedactDisplayText(s string) string
- func SanitizeDisplayBytes(in []byte) string
- func SanitizeDisplayText(s string) string
- func StripThinkTags(s string) string
- func SummarizeToolInput(raw json.RawMessage) string
- func SupportedMIMEs() []string
- type Adapter
- type AgentEvent
- type AgentInvocation
- type AgentOutcome
- type AgentResult
- type Caps
- type CredentialNamer
- type DerivedAdapter
- func (d *DerivedAdapter) Capabilities() Caps
- func (d *DerivedAdapter) Launch(ctx context.Context, h container.Handle, inv AgentInvocation) (<-chan AgentEvent, <-chan AgentOutcome, error)
- func (d *DerivedAdapter) PreflightResume(ctx context.Context, req LiveResumePreflightRequest) error
- func (d *DerivedAdapter) Ref() string
- func (d *DerivedAdapter) RunToolLoop(ctx context.Context, inv ToolLoopInvocation) (ToolLoopResult, error)
- func (d *DerivedAdapter) ValidateConfig(with ir.RawConfig) error
- func (d *DerivedAdapter) Version(ctx context.Context, h container.Handle) (string, error)
- type DisplayClass
- type DisplayStreamSanitizer
- type ErrAdapterAlreadyRegistered
- type ErrAdapterNotFound
- type ErrAgentLaunch
- type ErrInvalidConfig
- type ErrUnparseableOutput
- type EventDisplay
- type InputFile
- type LiveDispatch
- type LiveResumePreflightRequest
- type MetricCost
- type MetricSet
- type MetricTokens
- type ReactTurn
- type Registry
- type Resolver
- type ResumePreflighter
- type RetryHint
- type RunContext
- type SecretEnv
- type ThreadTurn
- type ToolCall
- type ToolDef
- type ToolLoopInvocation
- type ToolLoopResult
- type ToolLoopRunner
Constants ¶
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.
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.
const ToolResultHeadTail = 4
ToolResultHeadTail is the default head/tail line budget for a tool result's preview, passed to Elide.
Variables ¶
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 ¶
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 ¶
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 ¶
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 RedactDisplayText ¶
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 ¶
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 ¶
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 ¶
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 AND defensively
// at dispatch (slice 5.2). 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):
- resolver.Lookup(node.Uses) → Adapter (else *ErrAdapterNotFound)
- adapter.ValidateConfig(with) once per node, at run-start (CLI walk) and at dispatch time (defensive) — strict schema validation.
- 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
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:"-"`
// 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:"-"`
}
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"`
// 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"`
}
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.
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) RunToolLoop ¶
func (d *DerivedAdapter) RunToolLoop(ctx context.Context, inv ToolLoopInvocation) (ToolLoopResult, error)
func (*DerivedAdapter) ValidateConfig ¶
func (d *DerivedAdapter) ValidateConfig(with ir.RawConfig) 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 ¶
func (e *ErrAdapterAlreadyRegistered) Error() string
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 ¶
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
}
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 ¶
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 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 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 ¶
Lookup returns the registered Adapter for ref (true) or (nil, false). Implements Resolver. Concurrency-safe.
func (*Registry) Refs ¶
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 ¶
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 ¶
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 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 ¶
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.
type ThreadTurn ¶
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.
Source Files
¶
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). |