execir

package
v0.1.93 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

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

View Source
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

type BinOp struct {
	Op   string
	X, Y Expr
}

BinOp is a comparison or logical connective. Op is one of: "==", "!=", "<", "<=", ">", ">=", "&&", "||".

type Branch

type Branch struct {
	Pos  Pos
	Cond Expr
	Then []Node
	Else []Node
}

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

type ForkBranch struct {
	Bind  string
	Nodes []Node
}

ForkBranch is one statically-known parallel branch: a name it binds and the nodes it runs.

type Interp

type Interp struct {
	Invoker           Invoker
	MaxLoopIterations int
	MaxConcurrency    int
}

Interp executes a Program against an Invoker. Zero values pick built-in defaults: MaxLoopIterations falls back to spec.DefaultMaxLoopIterations and MaxConcurrency to DefaultMaxConcurrency.

func (*Interp) Run

func (in *Interp) Run(ctx context.Context, prog *Program, input map[string]any) (any, error)

Run executes prog with the given workflow input and returns the value set by a Return node (nil if the program returns nothing).

type InvokeAgent

type InvokeAgent struct {
	Pos   Pos
	Bind  string
	Agent string
	Args  map[string]Value
}

InvokeAgent invokes a declared agent. Bind is the result binding, or "".

type InvokeTool

type InvokeTool struct {
	Pos  Pos
	Bind string
	Uses string
	Args map[string]Value
}

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

type InvokeWorkflow struct {
	Pos      Pos
	Bind     string
	Workflow string
	Args     map[string]Value
}

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 Leaf

type Leaf struct{ V Value }

Leaf wraps a Value (Ref or Lit) as a condition leaf.

type Let

type Let struct {
	Pos   Pos
	Bind  string
	Value Value
}

Let binds a name to a value without invoking anything — the lowering of an alias assignment `x = y`.

type Lit

type Lit struct {
	Pos Pos
	V   any
}

Lit is a literal operand: a string, int64, float64, or bool.

type Loop

type Loop struct {
	Pos        Pos
	Var        string
	Collection Value
	Body       []Node
	Parallel   bool
}

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 Not

type Not struct{ X Expr }

Not is logical negation.

type Pos

type Pos = spec.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

type Program struct {
	Workflow string
	Params   []string
	Body     []Node
}

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

func (p *Program) Digest() string

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.

type Ref

type Ref struct {
	Pos  Pos
	Path []string
}

Ref is a dotted path into the runtime scope, e.g. ["pr","number"] resolving scope["pr"] then its "number" field, or ["input","repo"], or a loop variable. Path is always non-empty.

type Return

type Return struct {
	Pos   Pos
	Value Value
}

Return sets the workflow output value.

type Value

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

Value is a data operand: a Ref into the runtime scope or a Lit.

Jump to

Keyboard shortcuts

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