Documentation
¶
Overview ¶
Package execir defines the execution IR: the derived, never-authored projection of a checked .agent program where control flow lives (ADR 002 §5, issue #199).
ADR 002 fixes two sibling projections of one checked program. The resource projection (internal/spec resources) is what humans author, what `plan` diffs, and what `apply` writes; it never acquires an expression language, so `Branch` and `Loop` may not appear on a spec.WorkflowStep (ADR 002 §4). The execution IR — this package — is the other projection: it holds InvokeTool/InvokeAgent/InvokeWorkflow/Fork/Branch/Loop/Return and is where a conditional or a loop becomes something after the AST is discarded. It is never hand-authored and has no YAML surface.
The two projections are NOT a pipeline: the execution IR is lowered directly from the checked program (internal/lang/lower.LowerExec), not derived from the resource projection, which by design cannot represent control flow.
Ingress convergence is the DESIGN target, not yet the implementation. ADR 002 §5 requires both ingress paths to converge on this IR — a straight-line YAML workflow lowering to the same flat node list a straight-line `.agent` workflow does, so execution semantics never diverge. This package provides the target and the `.agent` lowering (LowerExec); the YAML side still executes as a WorkflowStep DAG in internal/engine, and a YAML->execir lowering plus running the engine from execir is a follow-up. Until that lands, do not read this package as proof the two paths already share an interpreter.
Runtime independence: nodes reference values by the source binding namespace (parameter names, assignment targets, loop variables), not by resource-model interpolation tokens such as ${steps.x.output}. An Interp executes a Program against a [Scope] and an injected Invoker; the package deliberately does not depend on the engine, state, or model registry, so control-flow semantics are unit-testable in isolation (issue #199, library-level scope).
Index ¶
Constants ¶
const DefaultMaxConcurrency = 8
DefaultMaxConcurrency bounds goroutine fan-out for a parallel Loop or Fork when the caller sets no override. It mirrors the engine's step-concurrency default without importing the engine (execir must stay runtime-independent).
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type BinOp ¶
BinOp is a comparison or logical connective. Op is one of: "==", "!=", "<", "<=", ">", ">=", "&&", "||".
type Branch ¶
Branch is a conditional: run Then when Cond evaluates truthy, else Else. Else may be empty. Branch lives only in the execution IR (ADR 002 §4); the resource projection instead flattens both arms into steps so the effect bound is the union over branches.
type Expr ¶
type Expr interface {
// contains filtered or unexported methods
}
Expr is a boolean condition tree.
type Fork ¶
type Fork struct {
Pos Pos
Branches []ForkBranch
}
Fork runs statically-known branches concurrently and joins at its end — the lowering of `parallel { a = ...; b = ... }` (#192). The join is the implicit barrier at the close of the node: every branch completes before the node that follows the Fork begins.
type ForkBranch ¶
ForkBranch is one statically-known parallel branch: a name it binds and the nodes it runs.
type Interp ¶
Interp executes a Program against an Invoker. Zero values pick built-in defaults: MaxLoopIterations falls back to spec.DefaultMaxLoopIterations and MaxConcurrency to DefaultMaxConcurrency.
type InvokeAgent ¶
InvokeAgent invokes a declared agent. Bind is the result binding, or "".
type InvokeTool ¶
InvokeTool is a deterministic workflow-level tool call `uses(args)`. Bind is the source binding name the result is stored under, or "" when the call is evaluated only for its effect (a bare-expression statement).
type InvokeWorkflow ¶
InvokeWorkflow invokes a declared subworkflow. Bind is the result binding, or "".
type Invoker ¶
type Invoker interface {
InvokeTool(ctx context.Context, uses string, args map[string]any) (any, error)
InvokeAgent(ctx context.Context, agent string, args map[string]any) (any, error)
InvokeWorkflow(ctx context.Context, workflow string, args map[string]any) (any, error)
}
Invoker performs the effectful leaf operations. The execution IR carries no I/O of its own; an engine adapter supplies one that runs a real tool/agent/ subworkflow, while tests supply a recording stub. Implementations must be safe for concurrent use — a parallel Loop or Fork invokes from several goroutines.
type Let ¶
Let binds a name to a value without invoking anything — the lowering of an alias assignment `x = y`.
type Loop ¶
Loop iterates Body once per element of Collection with Var bound to the element. Parallel marks dynamic fan-out — iterations run with bounded concurrency (ADR 002 §1: dynamic fan-out is a loop, not a graph field).
Scope rules differ by kind, and match the type checker:
- A SEQUENTIAL Loop runs its iterations in order on the ENCLOSING scope. The loop variable and any body binding escape the loop (last iteration wins), and a Return in the body returns from the workflow and stops the loop.
- A PARALLEL Loop runs each iteration in an ISOLATED child scope, so iterations never race and no body binding escapes; `return` is not lowered into a parallel body (LowerExec rejects it), so isolation loses nothing.
There is no unbounded (`while`) loop in the surface, and the interpreter caps the iteration count (internal/spec MaxLoopIterations) so termination is always bounded (#199).
type Node ¶
type Node interface {
// contains filtered or unexported methods
}
Node is one execution-IR construct.
type Pos ¶
Pos aliases spec.Pos so an execution-IR node can carry the same position a lang AST node did, with no conversion (mirrors lang.Pos).
type Program ¶
Program is the execution lowering of one workflow: its parameter names, the ordered top-level nodes to execute, and a canonical digest that folds into the workflow hash (ADR 002 §5; see Program.Digest and internal/plan).
func (*Program) Digest ¶
Digest returns a hex SHA-256 over a canonical, position-independent encoding of the program's executable shape. ADR 002 §5 requires the execution-IR digest to fold into the workflow hash so a lowering change with no resource-level change still invalidates a stale plan (internal/plan.WorkflowSpecHashWithExec).
Positions are deliberately excluded: moving a workflow within a file must not change what it executes, matching the resource projection's own canonicalization. Two programs share a digest iff they invoke the same operations with the same arguments under the same control flow and binding names.