ir

package
v0.3.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: 22 Imported by: 0

Documentation

Overview

Package ir provides stable IR types (the contract), structural validation, and the definition digest.

Index

Constants

View Source
const CallWorkflowSegment = "workflow"
View Source
const DigestScheme = "awf-d1:sha256:"

DigestScheme is the self-describing content-address prefix shared by workflow digests and state blob refs. Bump (awf-d2) only on a deliberate canonicalization change; state.blobRefPrefix and container/docker.snapshotBlobScheme are pinned equal to this by tests.

Variables

View Source
var QuorumVerdictFields = map[string]bool{"passed": true, "votes": true, "agree": true}

QuorumVerdictFields is the fixed typed-output shape a quorum reducer commits. It is the SINGLE source of truth for the {passed, votes, agree} verdict contract: this validator binds downstream refs against it, and engine's runQuorumReduce must produce EXACTLY these keys — pinned by a cross-package engine test (TestQuorumReduceOutputMatchesVerdictFields) so the producer (engine/reduce.go) and the validator can never silently drift. Exported only for that test.

Functions

func CallWorkflowParentPath

func CallWorkflowParentPath(callPath string) string

func CatalogText added in v0.3.0

func CatalogText(code string) string

CatalogText returns the catalog's stable message text for a diagnostic code, or "" if code is not registered. catalog itself stays unexported (it is validate's internal composition table, keyed by ad hoc call sites across ir/*.go), but some codes — AWF1065 chief among them — are emitted OUTSIDE ir/ entirely (cli/backend_features.go's run-start capability guard; see the AWF1065 catalog comment above) and until now had no way to read the catalog's text without hand-duplicating it as a literal. CatalogText is that read-only accessor: a second call site composing the same message from the same code can no longer drift from the catalog's wording.

func ChildPath

func ChildPath(parent, keyword string, idx int, branch string) string

ChildPath returns the path of a control node's named child block — the convention is

<parent>.<keyword>[idx].<branch>

e.g. `if[1].then`, `loop[0].body`, `gate[2].generate`, `try[0].catch`. Centralizes the branch-label / path-join convention so every validation pass (validateStructural, validateRefs, validateSchema, indexProducers) addresses the same nested node identically. Without this helper, a typo in any one walker's `path+".then"` literal would silently produce a divergent path for the same node in different passes.

func ContainerPath

func ContainerPath(name, field string) string

ContainerPath builds a dotted path for container metadata, e.g.:

ContainerPath("lab", "image") → "containers.lab.image"
ContainerPath("lab", "")      → "containers.lab"

Used by the structural and compose passes for container-attached diagnostics.

func FirstRuntimeComposePath

func FirstRuntimeComposePath(wf *Workflow) (string, bool)

FirstRuntimeComposePath returns the first static path of a runtime compose block, or ("", false) when the workflow does not use runtime compose.

func HasErrors

func HasErrors(ds []Diagnostic) bool

HasErrors reports whether ds contains at least one Diagnostic of Severity Error. Used by the CLI (slice 1.6) to decide the exit code: zero only when HasErrors returns false.

func LastStepID

func LastStepID(body NodeList) (string, bool)

LastStepID returns the id of the body's LAST node when that node is a code/agent step, plus a present flag. The prune controller reads the score from the committed step output at ItemPath(mapPath, N) + "." + id, so the engine needs the id of the score-producing step; the validator already guarantees (AWF5008) the last node is a step with a numeric score field. Returns ("", false) for an empty body or a control-flow terminal node — mirrors lastStepSchema's walk so engine + validator agree on which node produces the score.

func MapImageTargetOwners

func MapImageTargetOwners(wf *Workflow) map[string][]string

func MapImageTargets

func MapImageTargets(wf *Workflow) map[string]bool

MapImageTargets returns the bare container name of every `map` whose `image:` supplies a runtime per-element image (P6a). Such a container may declare resources alone — its image arrives per-element at dispatch. The runtime's capability guard (cli) also uses this to detect a runtime-image workflow. Recurses through every step-bearing control kind.

func MapProductShape

func MapProductShape(mapPath string) bool

MapProductShape reports whether a map at static mapPath is in the same v1 single-map envelope that aggregate refs can resolve: exactly one map boundary, no gate or loop multiplicity.

func NodeSubtreeDigest

func NodeSubtreeDigest(n Node) (string, error)

NodeSubtreeDigest is the content hash of a single node's definition subtree (RFC-8785/JCS canonical, scheme-prefixed). Reused as one input to the WS-6b node_key. Node.MarshalJSON already emits the canonical key-presence shape. Exported so engine.Commit can compute the key without reaching into ir internals.

func OutputFilesByStepID

func OutputFilesByStepID(wf *Workflow) map[string]OutputFiles

OutputFilesByStepID indexes every code/agent step's and reduced map product's output_files by id in ONE graph walk. Shared by input_files validation (ir/validate_input_files.go) and engine resolution (engine.resolveInputFiles) so neither walks per-ref.

func PathFor

func PathFor(parent, keyword, stepID string, index int) string

PathFor computes the static IR path of a node for use in a Diagnostic. parent is the path of the enclosing node (empty at the top level); keyword is the control-node keyword (e.g. "if", "loop", "gate") and stepID is the step's id — exactly one of {keyword, stepID} is non-empty (a step has an id; a control node has a keyword); index is the node's position in its parent's children list (used only for control nodes — steps are addressed by id).

If both are set (a caller bug), stepID takes precedence; the keyword is ignored. The "?[index]" fallback below covers the neither-set case.

The runtime addressing function (engine/path, Phase 2) extends this with iter/attempt suffixes for in-flight nodes (e.g. `loop[0].body.iter-3`, `gate[0].attempt-2.generate`). The static prefix the validator emits matches the runtime form — `loop[0].body` is the same string in both, just without the per-iteration suffix.

Per AWF standard §8: "step nodes by id; control nodes positionally — try[0].catch, if[1].then, loop[0].body.iter-3, gate[0].attempt-2.generate, parallel[2], map[0].item-3."

func SingleMapBodyShape

func SingleMapBodyShape(staticPath string) (mapPath, suffix string, ok bool)

SingleMapBodyShape reports whether staticPath is the v1 aggregation shape: exactly one map[ boundary, no gate[ or loop[ anywhere, the map segment followed by "body". Returns the map's static path ("...map[N]") and the producer's suffix after ".body.". The single owner of this grammar predicate (engine uses it too).

Suffix contract: suffix has NO leading dot (it is the path segments after ".body.", joined by "."); callers compose it as ItemPath(mapPath, N) + "." + suffix.

func WalkNodes

func WalkNodes(list NodeList, parent string, visit func(n Node, path string))

WalkNodes is the single pre-order traversal of a NodeList. It calls visit once per node — in slice order, parent before children — with the node's OWN static IR path, then recurses into every child branch using ir.PathFor / ir.ChildPath, centralizing the branch-label convention AND the Parallel quirk (children are addressed under the bare parallel[i] path, no branch label) so the family of index/collect walkers can never drift from the runtime addressing in engine/path.go. Slice order is load-bearing: indexProducers / StepPathIndex are last-write-wins on (validator-rejected) duplicate ids, so the order must match the old walkers' `for i, n := range`.

visit is called for every node EXCEPT *Skip — skip carries no step/map identity and no addressable child, and is absent from the §8 addressing grammar, so none of the index/collect callers act on it (byte-identical to the walkers this replaces). Leaf-only callers (step indexers) no-op on control kinds via their type switch; control-emitting callers (the map indexers) act on the kind they care about. All per-caller state (accumulator maps, *collector) is captured by the closure, never threaded through this signature — that minimalism is what lets the pure builders share one shape. Walkers that carry traversal-direction state (walkRefs's evaluateAllowed) or do node-anchored validation on most kinds (walkStructural) are deliberately NOT built on this; see their definitions.

ir.Node is a sealed sum type (the unexported isNode marker), so default is unreachable for valid input; it panics — like go/ast.Walk and the existing cli/runtimes.go walkAgentRefsNodes — so a future node kind added without updating this switch fails loudly instead of being silently dropped. This is forward-compat documentation, not a coverage guarantee; the static guarantee (gochecksumtype + //sumtype:decl on ir.Node) is a separate, out-of-scope call.

Types

type AgentRole

type AgentRole struct {
	Uses         string    `json:"uses"`
	Model        string    `json:"model,omitempty"`
	SystemPrompt string    `json:"system_prompt,omitempty"`
	With         RawConfig `json:"with,omitempty"`
}

AgentRole is one named, reusable agent configuration in the top-level `agents:` map. It binds an EXISTING adapter (Uses) plus opaque defaults the base adapter consumes: Model and SystemPrompt are convenience fields that the run-start resolver folds into the role's With map as opaque keys (the base adapter — e.g. claude — already reads with["model"]/with["system_prompt"]). With carries arbitrary opaque base-adapter config (e.g. mcp_servers — the memory MCP handle). AWF never interprets a With key; the named adapter validates it.

A role does NOT carry a typed-output schema: the typed-output contract is the STEP's own output_schema (engine/agent_step.go sources inv.OutputSchema from the AgentStep, never from with:), and the with: overlay seam cannot reach it (matching design spec §3.3 — a role is adapter + model + system_prompt + with: only).

type AgentStep

type AgentStep struct {
	ID           string      `json:"id"`
	Container    string      `json:"container,omitempty"`
	Uses         string      `json:"uses"`
	With         RawConfig   `json:"with,omitempty"`
	Continues    string      `json:"continues,omitempty"` // id of the prior agent turn to continue (engine-owned thread)
	OutputSchema *JSONSchema `json:"output_schema,omitempty"`
	OutputFiles  OutputFiles `json:"output_files,omitempty"`
	// OutputArtifact, valid on any agent step with output_schema (container-backed
	// or containerless — F39), serializes the step's validated typed Output as
	// canonical JSON and publishes it as the named artifact step.<id>.files.<name>.
	// Mutually exclusive with output_files; requires output_schema. The dispatcher
	// always produces the bytes from the committed Output, never captured from
	// the container filesystem.
	OutputArtifact string            `json:"output_artifact,omitempty"`
	Skills         *StepSkillRouting `json:"skills,omitempty"`
	// InputFiles maps an in-container destination path → a static artifact or asset
	// reference (step.<id>.files.<name> or asset.<id>). The engine resolves each to
	// a CAS blob from a prior named output_files commit or run-start asset snapshot
	// and stages the bytes via Backend.CopyTo BEFORE this step runs. Static, not a
	// {{ }} template (AWF3007). Requires a container (rejected on containerless
	// agent steps at runtime).
	InputFiles     map[string]string `json:"input_files,omitempty"`
	Timeout        *Timeout          `json:"timeout,omitempty"`
	IdempotencyKey *Template         `json:"idempotency_key,omitempty"`
	Retry          *RetryPolicy      `json:"retry,omitempty"`
}

type ArtifactContract

type ArtifactContract struct {
	Format    string
	Schema    *JSONSchema
	SchemaRef string
}

func ContractFromOutputFile

func ContractFromOutputFile(of OutputFile) ArtifactContract

type ArtifactExports

type ArtifactExports map[string]string

type CallStep

type CallStep struct {
	ID         string                   `json:"id"`
	Call       string                   `json:"call"`
	Input      map[string]TemplateValue `json:"input,omitempty"`
	InputFiles map[string]string        `json:"input_files,omitempty"`
}

type CodeStep

type CodeStep struct {
	ID           string      `json:"id"`
	Container    string      `json:"container,omitempty"`
	Run          string      `json:"run"`
	Timeout      *Timeout    `json:"timeout,omitempty"`
	OutputSchema *JSONSchema `json:"output_schema,omitempty"`
	OutputFiles  OutputFiles `json:"output_files,omitempty"`
	// InputFiles maps an in-container destination path → a static artifact or asset
	// reference (step.<id>.files.<name> or asset.<id>). The engine resolves each to
	// a CAS blob from a prior named output_files commit or run-start asset snapshot
	// and stages the bytes via Backend.CopyTo BEFORE this step runs. Static, not a
	// {{ }} template (AWF3007). Requires a container (rejected on containerless
	// agent steps at runtime).
	InputFiles     map[string]string `json:"input_files,omitempty"`
	IdempotencyKey *Template         `json:"idempotency_key,omitempty"`
	Retry          *RetryPolicy      `json:"retry,omitempty"`
}

type Compose

type Compose struct {
	As      string   `json:"as"`
	From    string   `json:"from"`
	Service Template `json:"service"`
	Body    NodeList `json:"body"`
}

func (*Compose) MarshalJSON

func (n *Compose) MarshalJSON() ([]byte, error)

type ComposeValidationError

type ComposeValidationError struct {
	Code    string
	Message string
}

func ValidateComposeBytes

func ValidateComposeBytes(filename string, bytes []byte, requiredServices ...string) []ComposeValidationError

type Container

type Container struct {
	Image     string     `json:"image,omitempty"`
	Compose   string     `json:"compose,omitempty"`
	Service   string     `json:"service,omitempty"`
	Snapshot  string     `json:"snapshot,omitempty"`
	Cmd       []string   `json:"cmd,omitempty"`
	Keepalive *bool      `json:"keepalive,omitempty"`
	Resources *Resources `json:"resources,omitempty"`
}

Container is backed by exactly one of Image or Compose (structural validation per the standard §3).

type Diagnostic

type Diagnostic struct {
	Severity Severity `json:"severity"`
	Source   string   `json:"source,omitempty"`
	Path     string   `json:"path"`
	Code     string   `json:"code"`
	Message  string   `json:"message"`
}

Diagnostic is one validation finding. Path is the static IR path (see ir/path.go) where the issue lives — empty for top-level / definition-wide findings. Code is a stable enum from the catalog below; never renumbered (the catalog is an API for --output json consumers).

Slice 1.4 collects all diagnostics in a single pass (no fail-fast). Slice 1.6's CLI computes the exit code via HasErrors.

func Validate

func Validate(ld *LoadedDefinition) []Diagnostic

Validate is the AWF validator entry point. It walks ld in five passes (one per rule family) and returns every Diagnostic found — collect-all, no fail-fast. The returned slice is stable-sorted by (Code, Path, Message) so golden-file tests over the diagnostic stream are stable regardless of map-iteration order.

Passes (each lives in its own validate_*.go file):

  • recovery (AWF1064) — retry.recovery is one of "continue"/"restart"/unset (a typo would silently fall back to the per-adapter default)
  • structural (AWF1xxx) — §4 step shape, §5 control-flow field requirements, §3 container shape, parallel/map distinct-container rule
  • agents (AWF1033/4) — top-level agents: role-definition shape (non-empty uses:, role name not in the <vendor>/<name> adapter-ref form) and every uses: ref resolving to a declared role OR a syntactically-valid base ref
  • skills (AWF1040-45, AWF3010-11) — native skill corpus declarations, loaded directory layout, and agent-step routing shape/staging constraints
  • calls (AWF1046-51) — imported workflow call targets, input contracts, workflow outputs, and workflow artifact exports
  • reduce (AWF1035, AWF5006, AWF1009) — map reduce: fan-in shape (exactly one of run:/quorum:; quorum needs field:; a run: reducer needs a resolvable container:) and quorum/field aggregation scope (field: names a real body field; min_success and reduce:{quorum} are mutually exclusive)
  • prune (AWF1037, AWF5008) — map prune: frontier shape (a `score` field + exactly one of keep: <positive int> / stop_when:) and score-field binding (score names a numeric field in the body's last step's output_schema)
  • misplaced_with (AWF1061) — reserved step-level key names nested inside an agent step's with: block (silently ignored by the engine; advisory warning)
  • refs (AWF3001/2) — output_schema-iff-referenced cross-walk via the template package (Slots → ParseRef per Template; ParseExpr → References per Expr)
  • input_files (AWF3007) — every input_files value is a static step.<id>.files.<name> ref naming a prior in-scope step's NAMED output_files artifact (dst absolute + clean)
  • output_files (AWF3009, AWF3014) — named output_files contract metadata shape and schema_ref assets; AWF3014 is the output_artifact (typed-output → artifact) rule set, valid on any output_schema agent step (container-backed or containerless, F39)
  • staging_literal (AWF3015) — run:/reduce.run hardcoding the docker-only staging path /work/.awf instead of $AWF_STAGING_ROOT (warning; native's staging root differs)
  • schema (AWF2001/2) — JSON Schema well-formedness + §7 floor (warning, agents only)
  • compose (AWF3003/4/5) — compose-go/v2 parse + digest-pinning of every inner image

Validate performs no filesystem I/O of its own — all reads happened in loader.Load before the LoadedDefinition was constructed. (compose-go/v2 is configured with SkipExtends, SkipInclude, and SkipResolveEnvironment so its transitive file-following directives don't reopen a file-read primitive.)

Validate(nil) returns one Error diagnostic (AWF1003) so the slice-1.6 CLI can surface a nil LoadedDefinition gracefully rather than panicking.

func (Diagnostic) String

func (d Diagnostic) String() string

String renders a Diagnostic in human-readable form for the default CLI output. Format:

error AWF1005 at graph[0].run: image is a tag, not a digest

The JSON projection (`awf validate --output json`) marshals the struct via its lowercase json tags (severity/source/path/code/message), so consumers like `jq '.diagnostics[].code'` read all-lowercase keys consistent with the validateResult envelope around them.

type Duration

type Duration time.Duration

Duration marshals to integer nanoseconds (digest stability); parses int ns or a Go duration string.

func (*Duration) MarshalJSON

func (d *Duration) MarshalJSON() ([]byte, error)

func (*Duration) UnmarshalJSON

func (d *Duration) UnmarshalJSON(b []byte) error

type Expr

type Expr string

Expr is an unparsed condition expression (if.cond / loop.until / gate.until).

type Gate

type Gate struct {
	Generate    NodeList `json:"generate"`
	Evaluate    NodeList `json:"evaluate"`
	Until       Expr     `json:"until"`
	MaxAttempts int      `json:"max_attempts"`
}

func (*Gate) MarshalJSON

func (n *Gate) MarshalJSON() ([]byte, error)

type If

type If struct {
	Cond Expr     `json:"cond"`
	Then NodeList `json:"then"`
	Else NodeList `json:"else,omitempty"`
}

func (*If) MarshalJSON

func (n *If) MarshalJSON() ([]byte, error)

Six object-valued control nodes share the same wrap pattern. Map has its own dedicated MarshalJSON below (F51's two-arm `over:` union needs to fold OverItems into the same wire key as Over, which the generic one-liner can't express).

type JSONSchema

type JSONSchema map[string]any

JSONSchema is a JSON Schema document, preserved as decoded JSON for canonicalization + validation. A defined map type is sufficient — json.Marshal sorts map keys alphabetically (digest-stable), and json.Unmarshal decodes any object directly. No custom marshalers needed (verified).

func MapCompactProducer

func MapCompactProducer(m *Map) (suffix string, schema *JSONSchema, ok bool)

MapCompactProducer reports the final typed body-step product for a map without reduce:. Named compact map products expose that final step's typed outputs as a compact array for downstream map over: expressions.

type LoadedAsset

type LoadedAsset struct {
	ID           string            `json:"id"`
	DeclaredPath string            `json:"declared_path"`
	IsDir        bool              `json:"is_dir"`
	Files        []LoadedAssetFile `json:"files"`
}

type LoadedAssetFile

type LoadedAssetFile struct {
	Path  string `json:"path"`
	Bytes []byte `json:"bytes"`
}

func (LoadedAssetFile) SHA256

func (f LoadedAssetFile) SHA256() string

func (LoadedAssetFile) Size

func (f LoadedAssetFile) Size() int64

Size and SHA256 are derived from Bytes — never stored, so they cannot drift from the content. The durable manifest (RunStartedAssetFile), the digest, and skillroute all derive from Bytes too.

type LoadedDefinition

type LoadedDefinition struct {
	// Workflow is the parsed IR. Non-nil if Load succeeded.
	Workflow *Workflow

	// WorkflowPath is the absolute path of the workflow file (for error attribution).
	WorkflowPath string

	// ComposeFiles maps each container's cleaned workflow-relative compose path
	// (e.g. "lab/compose.yml") to the raw bytes the loader read. Forward-slashed regardless
	// of OS so the digest input is portable.
	ComposeFiles map[string][]byte

	// Assets maps each top-level asset id to the run-start snapshot loaded from disk.
	Assets map[string]LoadedAsset

	// Modules maps logical module ids to loaded workflow modules. The root module id is "".
	Modules map[string]*LoadedModule

	// ImportEdges records resolved parent -> child import relationships.
	ImportEdges []LoadedImportEdge
}

LoadedDefinition is the loader's output and the validator's input — the parsed Workflow plus the raw bytes of every referenced compose file, keyed by the cleaned workflow-relative forward-slash path. That key form is what ir/digest.go consumes for the spec §E compose-fold (length-prefixed (path, sha256(bytes)) entries, sorted by path, folded into the workflow's content digest).

Lives in the `ir` package because validation — also in `ir` per docs/runtime-design.md — consumes it. Putting LoadedDefinition in `loader` would create a cycle (loader already imports ir for Workflow/Container/Node).

func (*LoadedDefinition) ComputeDigest

func (ld *LoadedDefinition) ComputeDigest() (string, error)

ComputeDigest returns the content digest of the fully loaded definition. For a root-only definition it delegates exactly to Workflow.ComputeDigest to preserve the single-workflow digest contract. When imports are present, it folds the root workflow digest, each imported module's workflow digest, and the import edges into one deterministic loaded-definition digest.

func (*LoadedDefinition) Module

func (ld *LoadedDefinition) Module(id string) (*LoadedModule, bool)

func (*LoadedDefinition) Root

func (ld *LoadedDefinition) Root() *LoadedModule

func (*LoadedDefinition) WalkImportEdges

func (ld *LoadedDefinition) WalkImportEdges(fn func(LoadedImportEdge) error) error

func (*LoadedDefinition) WalkModules

func (ld *LoadedDefinition) WalkModules(fn func(*LoadedModule) error) error

type LoadedImportEdge

type LoadedImportEdge struct {
	ParentID     string
	ImportID     string
	DeclaredPath string
	ChildID      string
}

type LoadedModule

type LoadedModule struct {
	ID           string
	Workflow     *Workflow
	WorkflowPath string
	ComposeFiles map[string][]byte
	Assets       map[string]LoadedAsset

	// RawDoc is the raw top-level YAML mapping for this module's workflow file, from the
	// pre-typed-unmarshal decode stage. nil if the document's top level wasn't a mapping.
	// A future strict-key validator uses it to detect keys the typed unmarshal silently
	// discarded; nil means "no strict check available" for this module.
	RawDoc map[string]any
}

type Loop

type Loop struct {
	Until    *Expr    `json:"until,omitempty"`
	MaxIters *int     `json:"max_iters,omitempty"`
	Body     NodeList `json:"body"`
}

func (*Loop) MarshalJSON

func (n *Loop) MarshalJSON() ([]byte, error)

type Map

type Map struct {
	ID string `json:"id,omitempty"`
	// Over carries the `{{ }}` expression arm of the two-arm `over:` union (F51): a JSON
	// string on the wire. Empty when the literal-sequence arm (OverItems) is populated
	// instead — exactly one of Over/OverItems is live on a decoded Map, never both. The
	// shape-dispatch lives in Map.UnmarshalJSON/MarshalJSON (node_unmarshal.go /
	// node_marshal.go); engine/map.go picks the arm to iterate.
	Over Expr `json:"over"`
	// OverItems carries the literal-sequence arm of `over:` (F51): a plain JSON array
	// instead of a `{{ }}` expression (e.g. an inline matrix `over: [a, b, c]`). Each item
	// is a scalar or flat object/array with NO `{{ }}` templates — the literal arm is
	// fully static, no engine-scope substitution. json:"-" because it is folded manually
	// into the wire "over" key (never a separate "over_items" key) by Map's custom
	// MarshalJSON; that also keeps TestEveryExportedFieldHasNonEmptyJSONTag happy the same
	// way Parallel.Children and Skip.Reason already do.
	OverItems []any    `json:"-"`
	As        string   `json:"as"`
	Container string   `json:"container"`
	Image     Template `json:"image,omitempty"`
	// Concurrency is presence-tracked (F45): nil means `concurrency:` was omitted from the
	// source, which the loader desugars to 1 (serial) BEFORE any digest/validate pass runs
	// (loader.Load) — so a workflow that omits concurrency: and one that writes
	// `concurrency: 1` produce byte-identical normalized IR (same digest). A non-nil pointer
	// to a value <= 0 is a validation error (AWF1012); engine/map.go additionally defends
	// against a stray nil/<=0 that slipped past the loader with its own capSize<1 backstop.
	Concurrency *int     `json:"concurrency,omitempty"`
	MinSuccess  *Ratio   `json:"min_success,omitempty"`
	Body        NodeList `json:"body"`
	Reduce      *Reduce  `json:"reduce,omitempty"`
	// Prune is the optional frontier clause (SP5, spec §3.2b). nil for a plain
	// result-blind map. Folds into the digest via omitempty.
	Prune *Prune `json:"prune,omitempty"`
}

func (*Map) MarshalJSON

func (n *Map) MarshalJSON() ([]byte, error)

MarshalJSON re-emits Map's "over" key as whichever arm of the F51 two-arm union is live: the literal sequence (OverItems) if non-nil, else the `{{ }}` expression string (Over) as before. OverItems itself is tagged json:"-" on the struct (node.go), so the embedded alias below never emits a second "over_items" key — only the explicit `Over json.RawMessage` field (which shadows the alias's own promoted "over" field) does.

NOTE: putting the explicit "over" field before the embedded alias means the RAW (pre-JCS) byte output can order "over" ahead of the alias's "id" — NOT the struct's field order. This is DELIBERATELY accepted: every digest/round-trip path runs jcs.Transform (RFC 8785 key-sort) downstream, so the raw order is never observed. The F51 expression-arm digest is pinned by TestDigestF51ExpressionArmGolden to catch any accidental value shift here.

func (*Map) UnmarshalJSON added in v0.3.0

func (n *Map) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes a Map's inner object (b is unwrapped — the value under the "map" wire key, per unmarshalControl) and shape-dispatches its "over" field: F51's two-arm union. A JSON string decodes into Over (the `{{ }}` expression arm, exactly as before a literal sequence was accepted). A JSON array decodes into OverItems (the literal-sequence arm) after rejecting any item that contains a `{{ }} ` template — the literal arm is fully static, no engine-scope substitution is ever applied to it. An absent or null "over" key leaves both arms empty/nil — NOT a decode error — so a workflow missing `over:` entirely still decodes cleanly and AWF1012 (structural validation) reports the missing-field diagnostic exactly as it did before F51 (see testdata/invalid/AWF1012-map-missing-fields.yaml). Every other Map field (id/as/container/image/concurrency/min_success/body/reduce/prune) decodes through the embedded alias exactly as unmarshalControl's other cases do, so nothing is dropped.

type Node

type Node interface {
	// contains filtered or unexported methods
}

Node is one of the thirteen node kinds (4 step + 9 control). Discriminated by key-presence at the wire level (the AWF standard's surface), so adding/removing a kind touches FOUR places in this package — keep them in sync:

  1. The type declaration and isNode() method in this file (node.go).
  2. The MarshalJSON method in node_marshal.go (either via `wrap` for object-valued kinds, or a custom shape for Skip / Parallel per the standard).
  3. The factory entry in `controlKeys` or `stepKeys` in node_unmarshal.go.
  4. The case in `unmarshalControl` in node_unmarshal.go (control kinds only; step kinds use a flat `json.Unmarshal` into the value the factory returned).

TestNodeRegistryExhaustive guards (1)/(3)/(4); the tag-reflection test (tags_test.go) guards json tags on every exported field. Adding a node kind without all four edits surfaces as a test failure (or, in the worst case, as the "unknown control key" runtime error).

type NodeList

type NodeList []Node

NodeList is the type of every node-bearing field. A bare []Node cannot unmarshal (interface elements), so all such fields use this named type whose UnmarshalJSON (in node_unmarshal.go) dispatches each element by key-presence. Marshaling works through each element's own MarshalJSON, so no custom marshaler is needed for the slice itself.

func (*NodeList) UnmarshalJSON

func (ns *NodeList) UnmarshalJSON(b []byte) error

UnmarshalJSON dispatches each raw element through unmarshalNode (key-presence union). NodeList is the type of every node-bearing field; see node.go.

type OutputFile

type OutputFile struct {
	Name      string
	Path      string
	Format    string
	Schema    *JSONSchema
	SchemaRef string
}

OutputFile is one captured artifact. Name is the handle name for the NAMED (map) wire form; empty for the bare-list form (capture-only, unreferenceable). Never struct-marshaled (OutputFiles has a custom MarshalJSON) → no json tags, not in ir/tags_test.go's irTypes().

type OutputFiles

type OutputFiles []OutputFile

OutputFiles unmarshals from EITHER a bare list (["/out/a"] → unnamed) OR a name→path map ({"report":"/out/r.md"} → named, referenceable as step.<id>.files.<name>). MarshalJSON re-emits the original shape so bare-list workflows stay byte-identical (digest-stable).

func (OutputFiles) MarshalJSON

func (o OutputFiles) MarshalJSON() ([]byte, error)

func (OutputFiles) PathForName

func (o OutputFiles) PathForName(name string) (string, bool)

PathForName returns the declared container path for a named artifact (ok=false if no named entry matches). Used by the engine resolver to map step.<id>.files.<name> → the key in NodeResult.Files.

func (OutputFiles) Paths

func (o OutputFiles) Paths() []string

Paths returns the in-container paths to capture (Backend.CaptureFiles), in declaration order. nil for empty. Unchanged capture semantics.

func (*OutputFiles) UnmarshalJSON

func (o *OutputFiles) UnmarshalJSON(b []byte) error

type Parallel

type Parallel struct {
	// Children is carried by Parallel's custom (un)marshalers as the wrapper's array value
	// (`{"parallel":[<node>,...]}` per the standard §5.4). json:"-" documents that fact and
	// keeps the tag-reflection test happy.
	Children NodeList `json:"-"`
}

func (*Parallel) MarshalJSON

func (n *Parallel) MarshalJSON() ([]byte, error)

Parallel marshals to the standard's array-value form: {"parallel":[<node>,...]} (§5.4).

type Prune

type Prune struct {
	// Score is the name of the NUMERIC field the body's last step declares in
	// its output_schema. The engine reads it from the committed item's typed
	// outputs (never parses text). Required (AWF1037 / AWF5008).
	Score string `json:"score"`
	// Keep is the "keep the top k scorers" policy. Wire form: keep: <k> (a
	// plain positive integer; F21 dropped the earlier top(<k>) wrapper — it
	// was the format's only function-call-shaped literal and top-k was
	// always the only mode, so the wrapper carried zero information).
	// Mutually exclusive with StopWhen (AWF1037).
	Keep *PruneKeep `json:"keep,omitempty"`
	// StopWhen is a bounded boolean expression over `best.score` (the running
	// best). Once true, all still-running/queued items are pruned. Mutually
	// exclusive with Keep (AWF1037). Evaluated via template.EvalBoolString.
	StopWhen string `json:"stop_when,omitempty"`
}

Prune is the optional frontier clause on a map (spec §3.2b). It reads a typed numeric `score` field per item as items commit and cancels in-flight/queued losers. Exactly one of Keep / StopWhen is set (AWF1037).

The whole struct folds into the workflow definition digest via the Map field's omitempty pointer — a nil Prune keeps pre-SP5 digests byte-identical; a non-nil Prune is pinned, so resume against a changed policy is a hard drift error (the existing pinning guard).

type PruneKeep

type PruneKeep struct {
	K int
}

PruneKeep is the "keep the top k scorers" policy. Custom (un)marshal so the wire form is a plain positive JSON integer while the in-memory form is a typed K — mirroring how Skip/Parallel carry a non-object scalar through custom (un)marshalers. NOTE: K has no json tag (the custom marshaler owns the wire form), so PruneKeep is deliberately NOT in ir/tags_test.go's irTypes() list (Task 2 Step 5).

F21: the wire form used to be the function-call-shaped string "top(<k>)" — the format's only literal of that shape, and pure noise since top-k was always the only mode. It is now a plain positive integer; the removed string form is a hard decode-time rejection, not a silent alias.

func (PruneKeep) MarshalJSON

func (p PruneKeep) MarshalJSON() ([]byte, error)

MarshalJSON re-emits the canonical plain-integer wire form (digest-stable).

func (*PruneKeep) UnmarshalJSON

func (p *PruneKeep) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts a plain positive JSON integer. The removed top(<k>) string form gets a migration-specific message; any other non-integer or non-positive value gets AWF1037's "keep must be a positive integer".

type Ratio

type Ratio = json.Number

Ratio is map.min_success: a count (e.g. 3) or a fraction (e.g. 0.8). It is a type ALIAS to json.Number, not a defined type — a defined type over json.Number loses the special numeric-token decoding (verified during planning), so `min_success: 3` would fail to unmarshal.

Consumption (engine `map` fan-in, runtime-design.md §5): call .Int64() or .Float64() to interpret; the consumer must check which form. Strictness (rejecting a JSON-string form like "0.8" that json.Number also accepts) is the validator's job — see runtime-design.md §4.

type RawConfig

type RawConfig map[string]any

RawConfig is opaque agent config (spec §4.2). The core never reads its keys; the adapter validates it.

type React

type React struct {
	ID           string      `json:"id,omitempty"`
	With         RawConfig   `json:"with,omitempty"`
	Prompt       string      `json:"prompt"`
	Tools        []string    `json:"tools"`
	MaxTurns     int         `json:"max_turns,omitempty"`
	OutputSchema *JSONSchema `json:"output_schema,omitempty"`
}

React is the model+tools+loop node (P3 A3). A control-style wrapper of the Map class: its runtime path is react[N] (keyword[index], NOT its id); the id is for output addressing only ({{ <id>.* }} / awf outputs --step <id>) via producer registration. tools is required and non-empty; with is the awf/llm config minus prompt (the engine owns the messages array). See spec §3.2.

func (*React) MarshalJSON

func (n *React) MarshalJSON() ([]byte, error)

type Reduce

type Reduce struct {
	Quorum       *Ratio      `json:"quorum,omitempty"`
	Field        string      `json:"field,omitempty"`
	Run          string      `json:"run,omitempty"`
	Container    string      `json:"container,omitempty"`
	OutputSchema *JSONSchema `json:"output_schema,omitempty"`
	OutputFiles  OutputFiles `json:"output_files,omitempty"`
}

Reduce is the fan-in clause on a Map (C2a). (Parallel is deferred — its bare-array wire-form has no slot for a sibling reduce: key; see SP2 Task 8.) Exactly one of Quorum or Run is set (validator AWF1035). The reduced output REPLACES the node's per-item array output (engine/scope.go aggregateMapOutputs prefers the node-path NodeResult when Reduce != nil).

  • Quorum form: the node succeeds iff at least Quorum branches produced a true Field. Quorum reuses Ratio (= json.Number) — an int count or a (0,1] fraction — exactly like Map.MinSuccess, which it generalizes.
  • Run form: an author shell reducer. The engine stages every branch's named output_files artifact + a canonical-JSON manifest of all branch typed outputs into Container via Backend.CopyTo (the SP1 artifact channel), then runs Run and commits OutputSchema/OutputFiles at the node path.

Field was named Over through v0.2.0 (json:"over"); renamed (F16) because it collides in spelling — but not in meaning — with Map.Over (the fan-out expression). The old `over:` spelling under reduce: is now a hard rename, detected position-aware by validateUnknownKeys (AWF1066): Map's own `over:` is unaffected since the renamed-key set is registered per Go struct type.

func (*Reduce) IsQuorum

func (r *Reduce) IsQuorum() bool

IsQuorum / IsRun report which form is set. (Validator AWF1035 guarantees exactly one; these are the engine's branch.) Both are nil-receiver safe.

func (*Reduce) IsRun

func (r *Reduce) IsRun() bool

type Resources

type Resources struct {
	CPU string `json:"cpu,omitempty"`
	Mem string `json:"mem,omitempty"`
}

type RetryPolicy

type RetryPolicy struct {
	Attempts              int       `json:"attempts,omitempty"`
	Backoff               string    `json:"backoff,omitempty"`
	Initial               *Duration `json:"initial,omitempty"`
	Max                   *Duration `json:"max,omitempty"`
	NonRetryableExitCodes []int     `json:"non_retryable_exit_codes,omitempty"`
	// Recovery selects how a retry re-runs the step after a transient fault:
	// "continue" resumes the adapter's persistent session, "restart" re-launches
	// fresh. Unset (the omitempty zero value) means "let the engine resolve a
	// per-adapter default" — see engine.effectiveRecovery. omitempty is
	// load-bearing: an unset recovery must marshal to the exact bytes it did
	// before this field existed so ComputeDigest stays byte-identical.
	Recovery string `json:"recovery,omitempty"`
}

RetryPolicy mirrors the spec §6 default shape.

type Severity

type Severity int

Severity classifies a Diagnostic. Only Error contributes to the non-zero exit code that `awf validate` will return (slice 1.6); warnings inform but don't fail the run.

const (
	Error Severity = iota
	Warning
)

func (Severity) MarshalJSON

func (s Severity) MarshalJSON() ([]byte, error)

MarshalJSON emits the human-readable severity name ("error" / "warning") rather than the bare iota int. The CLI's `--output json` output is the primary consumer; opaque integers would be unusable for CI / IDE / dashboard tooling that reads the diagnostic stream.

func (Severity) String

func (s Severity) String() string

func (*Severity) UnmarshalJSON

func (s *Severity) UnmarshalJSON(b []byte) error

UnmarshalJSON is the inverse of MarshalJSON; lets tooling round-trip the diagnostic stream without needing a parallel string-to-int mapping.

type SignalStep

type SignalStep struct {
	ID    string `json:"id"`
	Await string `json:"await"`
	// Where is an optional bounded-boolean clause selecting WHICH buffered signal
	// of name Await to consume (F18). It is ONE `{{ }}` envelope parsed once as a
	// bounded expression — the SAME grammar/evaluator if:/until: use, never
	// string-substituted. `signal.<field>` resolves against the delivered
	// signal's JSON payload (e.g. signal.candidate_id); every other root
	// (input.*, step.*, run.*, <as>.*, …) resolves against the surrounding engine
	// scope, exactly like any other {{ }} field. Empty → earliest-first per name
	// (unchanged). Folds into the digest (omitempty keeps where-less workflows
	// byte-identical). No arithmetic/calls/loops (spec §3.5 / §8 C5).
	Where        string      `json:"where,omitempty"`
	Timeout      *Duration   `json:"timeout,omitempty"`
	OutputSchema *JSONSchema `json:"output_schema,omitempty"`
}

type SkillCorpus

type SkillCorpus struct {
	From   string `json:"from"`
	Layout string `json:"layout"`
	Router string `json:"router"`
}

type Skip

type Skip struct {
	// Reason is carried by Skip's custom (un)marshalers as the wrapper's string value
	// (`{"skip":"<reason>"}` per the standard §5.6). json:"-" documents that fact and
	// keeps the tag-reflection test happy.
	Reason string `json:"-"`
}

func (*Skip) MarshalJSON

func (n *Skip) MarshalJSON() ([]byte, error)

Skip marshals to the standard's string-value form: {"skip":"<reason>"} (§5.6).

type StepSkillRouting

type StepSkillRouting struct {
	From  string   `json:"from"`
	Query Template `json:"query"`
	Limit int      `json:"limit"`
	Into  string   `json:"into"`
}

type Template

type Template string

Template is an unparsed templated string (e.g. "{{ input.cve_id }}:pr"). Parsed in the template package.

type TemplateValue

type TemplateValue = json.RawMessage

type Timeout added in v0.3.0

type Timeout struct {
	Wall *Duration
	Idle *Duration
}

Timeout is a step's timeout policy. On the wire it is EITHER a bare Go-duration scalar — the historical wall-clock per-attempt deadline — OR a { wall, idle } map. Idle is the maximum time a step may produce no output (no stdout/stderr chunk for a code step, no AgentEvent for an agent step) before the attempt is cancelled; an idle expiry is a `retryable_failure`, identical in class to a wall expiry, and rides the existing retry policy. Only code (run:) and agent (uses:) steps carry a *Timeout; await/tool timeouts stay scalar *Duration.

Digest stability: when Idle is nil the value marshals back to the bare nanosecond integer a *Duration would emit, so a scalar-form workflow's digest is byte-identical to before the map form existed.

func (*Timeout) MarshalJSON added in v0.3.0

func (t *Timeout) MarshalJSON() ([]byte, error)

func (*Timeout) UnmarshalJSON added in v0.3.0

func (t *Timeout) UnmarshalJSON(b []byte) error

type Tool

type Tool struct {
	Description string      `json:"description"`
	InputSchema *JSONSchema `json:"input_schema,omitempty"`
	Impl        ToolImpl    `json:"impl"`
}

Tool is one entry of the top-level tools: map (P3 A4). The map KEY is the tool name (sent to the model); Tool is the value. A tool is offered to a react: step's model, which calls it by name.

type ToolImpl

type ToolImpl struct {
	Run         string            `json:"run,omitempty"`
	Container   string            `json:"container,omitempty"`
	Timeout     *Duration         `json:"timeout,omitempty"`
	OutputFiles OutputFiles       `json:"output_files,omitempty"`
	InputFiles  map[string]string `json:"input_files,omitempty"`
	Retry       *RetryPolicy      `json:"retry,omitempty"`
}

ToolImpl is the executable body of a tool — a run: step that names a containers:-declared container. It is a DEDICATED id-less type, NOT a reused CodeStep: CodeStep.ID is `json:"id"` WITHOUT omitempty, so embedding a CodeStep would serialize an empty "id":"" into the JCS workflow digest. At execution time the engine synthesizes a real CodeStep from these fields (the reduce.go pattern). run:-ONLY — ir.CodeStep has no Exec field, so an exec: form cannot be synthesized into a CodeStep (rev #20). All fields are omitempty so an absent field never enters the digest.

type Try

type Try struct {
	Do      NodeList `json:"do"`
	Catch   NodeList `json:"catch,omitempty"`
	Finally NodeList `json:"finally,omitempty"`
}

func (*Try) MarshalJSON

func (n *Try) MarshalJSON() ([]byte, error)

type Workflow

type Workflow struct {
	ID              string                   `json:"workflow"`
	Version         int                      `json:"version"`
	InputSchema     *JSONSchema              `json:"input_schema,omitempty"`
	InputFiles      WorkflowInputFiles       `json:"input_files,omitempty"`
	Env             []string                 `json:"env,omitempty"`
	Assets          map[string]string        `json:"assets,omitempty"`
	Skills          map[string]SkillCorpus   `json:"skills,omitempty"`
	Imports         map[string]string        `json:"imports,omitempty"`
	Agents          map[string]AgentRole     `json:"agents,omitempty"`
	Containers      map[string]Container     `json:"containers"`
	OutputSchema    *JSONSchema              `json:"output_schema,omitempty"`
	Outputs         map[string]TemplateValue `json:"outputs,omitempty"`
	ArtifactExports ArtifactExports          `json:"output_files,omitempty"`
	// Tools is the top-level tools: map (P3 A4) — tool name → definition. Offered
	// to react: steps. Folds into the digest automatically (whole-workflow JCS).
	Tools  map[string]Tool `json:"tools,omitempty"`
	Graph  NodeList        `json:"graph"`
	Digest string          `json:"-"`
}

Workflow is the top-level definition. The Digest field is populated by run-start (via (*Workflow).SetDigest) or compared on resume; it is excluded from its own hash input (`json:"-"`). After json.Unmarshal, Digest is empty until set.

func (*Workflow) ComputeDigest

func (w *Workflow) ComputeDigest(composeFiles map[string][]byte, assets map[string]LoadedAsset) (string, error)

ComputeDigest returns the self-describing content digest of the workflow, folding in the sha256 of each referenced compose file (keyed by cleaned, workflow-relative path supplied by the loader) and each loaded asset file. Pure: does not modify w. The Digest field is excluded (`json:"-"`). composeFiles/assets are nil if none. See (*Workflow).SetDigest for the in-place variant.

func (*Workflow) RoleByName

func (w *Workflow) RoleByName(name string) (AgentRole, bool)

RoleByName returns the declared role (ok=false if none). The single accessor the validator and the run-start resolver share so role lookup lives in one place.

func (*Workflow) SetDigest

func (w *Workflow) SetDigest(composeFiles map[string][]byte, assets map[string]LoadedAsset) (string, error)

SetDigest computes the workflow's content digest via ComputeDigest and stores it in w.Digest. Returns the computed value for convenience. Convenient for run-start / loader callers that want both the value and the field populated; ComputeDigest itself remains pure.

func (*Workflow) StructuralDigest

func (w *Workflow) StructuralDigest(composeFiles map[string][]byte, assets map[string]LoadedAsset) (string, error)

StructuralDigest returns a self-describing content digest of the workflow's skeleton: the same compose-file and asset hashes as ComputeDigest, PLUS a hash over every node's (path, type) pair — but NOT the node body (Run text, With params, etc.). Two workflows with identical structure but different node bodies produce the same StructuralDigest and different ComputeDigest values. This is used as the WS-6b guard: resume rejects a workflow whose node set or topology changed since the original run, while tolerating body-only edits.

Implementation note: the workflow-level JSON (Digest excluded) and the compose/asset tail are hashed EXACTLY as ComputeDigest does — the only difference is that the node graph is replaced by its skeleton walk (path + reflect type) instead of being part of the JSON. The compose/asset tail code is duplicated (not refactored) to guarantee ComputeDigest's byte output is unchanged; see the TestGoldenDigest golden test.

v1 caveat: only the root workflow's structure is covered. Imported modules are not walked (T6b will extend this when per-module structural guards land).

type WorkflowInputFiles

type WorkflowInputFiles map[string]ArtifactContract

func (WorkflowInputFiles) MarshalJSON

func (w WorkflowInputFiles) MarshalJSON() ([]byte, error)

func (*WorkflowInputFiles) UnmarshalJSON

func (w *WorkflowInputFiles) UnmarshalJSON(b []byte) error

Jump to

Keyboard shortcuts

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