Documentation
¶
Overview ¶
Package ir provides stable IR types (the contract), structural validation, and the definition digest.
Index ¶
- Constants
- Variables
- func CallWorkflowParentPath(callPath string) string
- func CatalogText(code string) string
- func ChildPath(parent, keyword string, idx int, branch string) string
- func ContainerPath(name, field string) string
- func FirstRuntimeComposePath(wf *Workflow) (string, bool)
- func HasErrors(ds []Diagnostic) bool
- func LastStepID(body NodeList) (string, bool)
- func MapImageTargetOwners(wf *Workflow) map[string][]string
- func MapImageTargets(wf *Workflow) map[string]bool
- func MapProductShape(mapPath string) bool
- func NodeSubtreeDigest(n Node) (string, error)
- func OutputFilesByStepID(wf *Workflow) map[string]OutputFiles
- func PathFor(parent, keyword, stepID string, index int) string
- func SingleMapBodyShape(staticPath string) (mapPath, suffix string, ok bool)
- func WalkNodes(list NodeList, parent string, visit func(n Node, path string))
- type AgentRole
- type AgentStep
- type ArtifactContract
- type ArtifactExports
- type CallStep
- type CodeStep
- type Compose
- type ComposeValidationError
- type Container
- type Diagnostic
- type Duration
- type Expr
- type Gate
- type If
- type JSONSchema
- type LoadedAsset
- type LoadedAssetFile
- type LoadedDefinition
- func (ld *LoadedDefinition) ComputeDigest() (string, error)
- func (ld *LoadedDefinition) Module(id string) (*LoadedModule, bool)
- func (ld *LoadedDefinition) Root() *LoadedModule
- func (ld *LoadedDefinition) WalkImportEdges(fn func(LoadedImportEdge) error) error
- func (ld *LoadedDefinition) WalkModules(fn func(*LoadedModule) error) error
- type LoadedImportEdge
- type LoadedModule
- type Loop
- type Map
- type Node
- type NodeList
- type OutputFile
- type OutputFiles
- type Parallel
- type Prune
- type PruneKeep
- type Ratio
- type RawConfig
- type React
- type Reduce
- type Resources
- type RetryPolicy
- type Severity
- type SignalStep
- type SkillCorpus
- type Skip
- type StepSkillRouting
- type Template
- type TemplateValue
- type Timeout
- type Tool
- type ToolImpl
- type Try
- type Workflow
- func (w *Workflow) ComputeDigest(composeFiles map[string][]byte, assets map[string]LoadedAsset) (string, error)
- func (w *Workflow) RoleByName(name string) (AgentRole, bool)
- func (w *Workflow) SetDigest(composeFiles map[string][]byte, assets map[string]LoadedAsset) (string, error)
- func (w *Workflow) StructuralDigest(composeFiles map[string][]byte, assets map[string]LoadedAsset) (string, error)
- type WorkflowInputFiles
Constants ¶
const CallWorkflowSegment = "workflow"
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 ¶
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 CatalogText ¶ added in v0.3.0
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 ¶
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 ¶
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 ¶
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 ¶
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 MapImageTargets ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
type ComposeValidationError ¶
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/1067) — top-level agents: role-definition shape (non-empty uses:, role name not in the <vendor>/<name> adapter-ref form), every uses: ref resolving to a declared role OR a syntactically-valid base ref, and role config {{ }} templates restricted to input.* in the positions the engine actually substitutes (model, system_prompt, top-level with: values)
- 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 ¶
Duration marshals to integer nanoseconds (digest stability); parses int ns or a Go duration string.
func (*Duration) MarshalJSON ¶
func (*Duration) UnmarshalJSON ¶
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 ¶
type If ¶
type If struct {
Cond Expr `json:"cond"`
Then NodeList `json:"then"`
Else NodeList `json:"else,omitempty"`
}
func (*If) MarshalJSON ¶
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 ¶
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 ¶
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 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 ¶
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 ¶
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
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:
- The type declaration and isNode() method in this file (node.go).
- The MarshalJSON method in node_marshal.go (either via `wrap` for object-valued kinds, or a custom shape for Skip / Parallel per the standard).
- The factory entry in `controlKeys` or `stepKeys` in node_unmarshal.go.
- 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 ¶
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 ¶
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 ¶
MarshalJSON re-emits the canonical plain-integer wire form (digest-stable).
func (*PruneKeep) UnmarshalJSON ¶
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 ¶
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 ¶
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 ¶
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.
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.
func (Severity) MarshalJSON ¶
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) UnmarshalJSON ¶
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 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 ¶
Skip marshals to the standard's string-value form: {"skip":"<reason>"} (§5.6).
type StepSkillRouting ¶
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
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 (*Timeout) UnmarshalJSON ¶ added in v0.3.0
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 ¶
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 ¶
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
Source Files
¶
- agents.go
- diagnostic.go
- digest.go
- loaded.go
- map_products.go
- node.go
- node_marshal.go
- node_unmarshal.go
- output_files.go
- path.go
- prune.go
- reduce.go
- runtime_compose.go
- tool.go
- types.go
- validate.go
- validate_agents.go
- validate_compose.go
- validate_continues.go
- validate_duration_scalars.go
- validate_input_files.go
- validate_misplaced_with.go
- validate_output_files.go
- validate_prune.go
- validate_recovery.go
- validate_reduce.go
- validate_refs.go
- validate_schema.go
- validate_skills.go
- validate_staging_literal.go
- validate_structural.go
- validate_tools.go
- validate_unknown_keys.go
- walk.go