engine

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package engine is the pure fold at the center of jevkit SDLC execution: Apply(graph, state, event) -> (state, []Effect). It performs no I/O, holds no clock, and never calls Jev, runs a command, or spawns an agent itself — those are Effects a client (the MCP conductor tools, or jevkit's own opt-in driver) resolves and feeds back as Events. This is what makes jevkit's "decision support, not autonomous execution" posture structural rather than a convention someone has to remember: the engine cannot execute anything even if it wanted to.

Because internal/sdlc/compile already unrolls every bounded reroute loop into an acyclic chain terminated by a synthesized terminal node, the engine never needs its own reroute counter: reaching that terminal node *is* reroute exhaustion. The one budget the engine does track per node is budgets.maxNodeAttempts, which bounds retrying a single node's own failed execution (a crashed or timed-out invocation) — a runtime concern with no graph edge of its own, deliberately kept out of the compiled graph (see internal/sdlc/compile's package doc).

Index

Constants

View Source
const (
	StatusRunning  = "running"
	StatusTerminal = "terminal"
	StatusFailed   = "failed"
)

Run statuses.

Variables

This section is empty.

Functions

func Apply

func Apply(g *graph.Graph, st State, ev Event) (State, []Effect, error)

Apply advances st by resolving the Effect that produced ev. It is a pure function: st is never mutated in place, and calling it twice with the same (g, st, ev) always returns the same result — which is what lets a ledger replay a run's event journal to rebuild identical state on resume, with no second execution path.

func FirstUnsatisfiedNode

func FirstUnsatisfiedNode(g *graph.Graph, satisfied map[string]bool) (string, error)

FirstUnsatisfiedNode walks forward from g.Entry, skipping over a work node whose every required declared artifact is already in satisfied, and returns the first node it lands on that isn't such a node.

Only a work node is ever skipped this way: seeding an artifact substitutes for the node that would have produced it, but a gate, select, decide or check node is a decision or verification step, never something an artifact's mere presence can stand in for — skipping one of those would mean silently picking an outcome nothing actually decided. A join is likewise never itself returned, matching engine.enter's own pass-through: walking through one costs nothing since it has no artifacts of its own.

func NewRun

func NewRun(g *graph.Graph) (State, []Effect, error)

NewRun starts a fresh run at g's entry node.

func NewRunAt

func NewRunAt(g *graph.Graph, startNode string) (State, []Effect, error)

NewRunAt starts a fresh run at startNode instead of g.Entry: the seeded- artifact entry point a `sdlc start --file` skips forward to. startNode must be a real node in g.

Types

type AskJev

type AskJev struct {
	NodeID      string
	Kind        AskJevKind
	QuestionSet string
	Candidates  []string
	State       *spec.StateSpec
}

AskJev asks the caller to classify against QuestionSet and report a registry.Decision back as JevDecided. Candidates is populated only for a select node (the call-time criteria a caller assembles from the catalog); State names which run inputs/artifacts to bound and pass as Jev state.

type AskJevKind

type AskJevKind string

AskJevKind distinguishes the three Jev-driven node kinds; Apply uses it only to decide how to interpret the returned Decision, never to call Jev itself.

const (
	AskJevSelect AskJevKind = "select"
	AskJevGate   AskJevKind = "gate"
	AskJevDecide AskJevKind = "decide"
)

type Assignment

type Assignment struct {
	// Self is true when the node pins agent: self: the orchestrator does the
	// work inline, no delegation.
	Self bool
	// AgentID is the catalog id Jev (or a declared default) chose, when Self
	// is false.
	AgentID string
}

Assignment names which agent should carry out a RunWork effect.

type CommandCompleted

type CommandCompleted struct {
	NodeID          string
	Route           string
	ExecutionFailed bool
	P               Progress
}

CommandCompleted resolves a RunCommand effect: Route names which of the check node's declared routes the exit status maps to. ExecutionFailed marks the command itself failing to run (not found, killed) rather than running and exiting non-zero — that distinction is what budgets.maxNodeAttempts retries.

type Effect

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

Effect is something Apply wants its caller to resolve and feed back as an Event. The engine never resolves an Effect itself; it only ever produces the request. Effect is a sealed set: RunWork, RunCommand, AskJev, WaitForHuman and RunFinished.

type Event

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

Event feeds an Effect's resolution back into Apply. It is a sealed set: WorkCompleted, CommandCompleted, JevDecided and HumanResponded.

Every Event carries a Progress: the caller's own cumulative tally of elapsed run time and estimated Jev/agent spend. The engine has no clock and no cost data of its own, so it trusts these totals and compares them against the graph's budgets on every Apply call — the caller decides how to measure both; the engine only ever enforces the ceiling.

type HumanResponded

type HumanResponded struct {
	NodeID string
	Route  string
	P      Progress
}

HumanResponded resolves a WaitForHuman effect with the chosen route.

type JevDecided

type JevDecided struct {
	NodeID    string
	Decision  registry.Decision
	Available bool
	P         Progress
}

JevDecided resolves an AskJev effect. Available is false when Jev could not be reached and Decision is a synthesized fallback to the node's declared default — "a run never stalls on the classifier".

type Progress

type Progress struct {
	ActiveSeconds    int
	EstimatedCostUsd float64
}

Progress is the caller-tracked cumulative spend reported on every Event.

type RunCommand

type RunCommand struct {
	NodeID  string
	Attempt int
	Command string
}

RunCommand asks the caller to run Command and report back which of the node's declared route labels the exit status corresponds to.

type RunFinished

type RunFinished struct {
	Status  string
	Outcome string
}

RunFinished reports that the run has stopped: Outcome is a terminal node's declared outcome on success, or the engine's own failure reason when Status is StatusFailed. It is the only Effect a caller need not resolve with an Event — there is nothing left to drive.

type RunWork

type RunWork struct {
	NodeID     string
	Attempt    int
	Assignment Assignment
	Objective  string
	Consumes   []spec.Artifact
	Produces   []spec.Artifact
}

RunWork asks the caller to have Assignment execute Objective and produce Produces from Consumes. Attempt is 1-based and increases only when a prior attempt's execution itself failed (WorkCompleted.ExecutionFailed) and budgets.maxNodeAttempts allows another try.

type State

type State struct {
	// Current is the node awaiting an effect's resolution. Empty only before
	// NewRun, and once Status leaves StatusRunning.
	Current string `json:"current"`
	Status  string `json:"status"`
	// Outcome is set once Status != StatusRunning: a terminal node's declared
	// outcome, or an engine-detected failure reason (budget-exceeded:...,
	// node-attempts-exhausted:...).
	Outcome string `json:"outcome,omitempty"`

	// Attempts counts executions of each node's own effect, keyed by node
	// id; it exists only to bound retries of a single failed invocation
	// (budgets.maxNodeAttempts), never to model routing.
	Attempts map[string]int `json:"attempts,omitempty"`
	// Assigned records, for each work node reached via a select's assignTo,
	// the agent id Jev (or a fallback) chose.
	Assigned map[string]string `json:"assigned,omitempty"`
	// Supervised marks a work node whose assignment came from a "gather"
	// (medium-confidence) select decision: its own attempts budget is
	// effectively 1 — a supervised pick that fails does not get retried with
	// the same low-confidence agent, it fails straight to escalation.
	Supervised map[string]bool `json:"supervised,omitempty"`
	// JevAvailable records, for each select/gate/decide node, whether Jev
	// was available for its decision (false means a declared default/
	// fallback was used because Jev could not be reached) — audit only.
	JevAvailable map[string]bool `json:"jevAvailable,omitempty"`
	// Satisfied lists artifact paths already produced or seeded.
	Satisfied map[string]bool `json:"satisfied,omitempty"`

	// ActiveSeconds and EstimatedCostUsd are cumulative totals the caller
	// tracks and reports on every event (the engine has no clock and no
	// cost data of its own); they are compared against the graph's budgets
	// on every Apply call.
	ActiveSeconds    int     `json:"activeSeconds"`
	EstimatedCostUsd float64 `json:"estimatedCostUsd"`

	Warnings []string `json:"warnings,omitempty"`
}

State is the run's entire persisted state: flat and JSON-serializable, so a ledger can store it and a resume can rebuild it by replaying events through Apply, never by re-deriving anything Apply didn't already return.

type WaitForHuman

type WaitForHuman struct {
	NodeID string
	Prompt string
	Routes []string
}

WaitForHuman asks the caller to present Prompt and collect one of Routes.

type WorkCompleted

type WorkCompleted struct {
	NodeID          string
	ProducedPaths   []string
	ExecutionFailed bool
	// CostReported is false when the agent's usage could not be itemized
	// (a native subagent's cost is opaque and parent-owned, per the plan's
	// Cost section). budgets.missingUsage governs what happens then.
	CostReported bool
	P            Progress
}

WorkCompleted resolves a RunWork effect. ExecutionFailed marks the invocation itself failing to run at all (crash, timeout, transport error) — the only condition budgets.maxNodeAttempts retries; it is never set for an agent that ran but produced nothing useful, which ProducedPaths already captures structurally (a missing required artifact fails the node without consuming an attempt, since nothing about retrying the identical objective would fix a spec the agent already tried once to satisfy — that failure is for a downstream gate/decide node to route on, per the plan's own "gates matter more than the selector" design).

Jump to

Keyboard shortcuts

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