ir

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 24, 2026 License: Apache-2.0 Imports: 21 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 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"`
	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        *Duration         `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      *Duration   `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):

  • 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 over:; a run: reducer needs a resolvable container:) and quorum/over aggregation scope (over: 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: top(<k>) / 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) — named output_files contract metadata shape and schema_ref assets
  • 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)

Seven object-valued control nodes share the same wrap pattern.

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
}

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        Expr     `json:"over"`
	As          string   `json:"as"`
	Container   string   `json:"container"`
	Image       Template `json:"image,omitempty"`
	Concurrency int      `json:"concurrency"`
	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)

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: top(<k>).
	// 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 parsed top(k) policy. Custom (un)marshal so the wire form is the string "top(<k>)" 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).

func ParsePruneKeep

func ParsePruneKeep(raw string) (PruneKeep, error)

ParsePruneKeep parses "top(<k>)" into a PruneKeep with a positive K. Trims surrounding and inner whitespace. Any other shape (wrong head, non-int, k <= 0, unbalanced parens) is an error — the validator surfaces it as AWF1037.

func (PruneKeep) MarshalJSON

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

MarshalJSON re-emits the canonical "top(<k>)" wire form (digest-stable).

func (*PruneKeep) UnmarshalJSON

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

UnmarshalJSON parses the wire string "top(<k>)" into K (AWF1037 on bad shape).

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"`
	Over         string      `json:"over,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 Over 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.

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"`
}

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. `{{ … }}` slots substitute from the surrounding
	// engine scope (e.g. {{ hyp.id }}); bare identifiers resolve against the
	// delivered signal's JSON payload (e.g. candidate_id == payload.candidate_id).
	// Empty → earliest-first per name (unchanged). Folds into the digest (omitempty
	// keeps where-less workflows byte-identical). Reuses the bounded boolean
	// evaluator — no arithmetic (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 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"`
	Input           *JSONSchema              `json:"input,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