agentstep

package
v0.0.422 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package agentstep is satelle's isolated-agent dispatch engine — the quality-management spine that runs a fresh-context agent over a workflow step and folds its result back in. It dispatches three kinds of isolated agent, all briefed and run through one seam (invoke.go): a GATE REVIEWER judging a status transition and returning a verdict, a NAMED EXECUTOR performing a step's work, and the STEP SUMMARISER narrating a transition. A reviewer is just one agent kind, so the engine (type Engine) is not named for it.

Reviewer path: the active workflow names a reviewer_skill per edge; the skill's markdown body rides as the agent's appended system prompt; the work item + requested transition go in on stdin; the agent prints one JSON object {decision, notes, reasoning}, parsed strictly into an accept/reject. Accept lets the caller enact; reject blocks and pushes the notes back to the executor. LLM gate and named-dispatch runs share Invoke (invoke.go) — one path that calls agentcli.Runner.Run (sty_ba860c8a).

The edge is gated only when the workflow names a reviewer_skill AND that skill's rubric is installed in the substrate. A named-but-absent rubric (e.g. the canonical default referencing a skill not yet embedded) is treated as advisory, so gating switches on exactly when the rubrics ship — the gateless baseline keeps working until then.

Index

Constants

View Source
const WorkflowStampPrefix = wfgovern.WorkflowStampPrefix

WorkflowStampPrefix re-exports the stamp tag prefix (owned by wfgovern).

Variables

This section is empty.

Functions

func GoverningWorkflow added in v0.0.134

func GoverningWorkflow(workflows []docindex.Doc, item workitem.Item) (docindex.Doc, bool)

GoverningWorkflow re-exports wfgovern.GoverningWorkflow.

func IsRateLimitOrUnavailable added in v0.0.294

func IsRateLimitOrUnavailable(err error, stdout []byte) bool

IsRateLimitOrUnavailable reports whether an isolated-agent failure should trigger secondary failover (sty_5bf61f89): rate limits, capacity, quota, and hard unavailability. Other errors stay fail-loud without fallback.

func OrderedWorkflows

func OrderedWorkflows(workflows []docindex.Doc, category string) []docindex.Doc

OrderedWorkflows re-exports wfgovern.OrderedWorkflows for callers that still import agentstep (web, CLI). Prefer wfgovern directly for new code.

func WorkflowConsistency

func WorkflowConsistency(workflows []docindex.Doc, resolve func(skill string) bool) []string

WorkflowConsistency reports cross-workflow inconsistencies an agent should advise the user about (sty_4c0c7246): (1) OVER-CONFIGURATION — two or more REPO workflows claim the same category (or the wildcard) at the same precedence, so the tiebreak is arbitrary; (2) a workflow that REFERENCES a skill (an edge gate or a node @skill: prompt) which does not resolve in the substrate. Empty when the workflow set is consistent. resolve may be nil to skip the skill check.

func WorkflowSkillProblems added in v0.0.374

func WorkflowSkillProblems(w docindex.Doc, resolve func(skill string) bool) []string

WorkflowSkillProblems reports, for ONE workflow, every skill it names that does not resolve in the substrate — edge gates, node @skill: prompts, and declared lifecycle hooks — plus any hook declaration defect.

This is the half of WorkflowConsistency that is meaningful per document. The ambiguity check is deliberately NOT here: it compares repo workflows against each other, so it is whole-set by nature and firing it on a single doc would misreport (sty_d59ec6a9 AC3).

Callers: WorkflowConsistency (whole set, where these are FAILs), and the authoring paths `workflow validate <name>` / `workflow create` (where they are WARNs — a repo mid-authoring writes the workflow before it writes the gate skills, so blocking there would make the ordinary sequence impossible).

Types

type Activity added in v0.0.408

type Activity struct {
	Label string
	Index int
	Total int
}

Activity is structured transition progress: which gate/phase is running, its 1-based index, and the size of the set (sty_598a8e1b).

type ChildState

type ChildState struct {
	ID     string `json:"id"`
	Status string `json:"status"`
}

ChildState is one child story's id and status, injected into a parent/epic close payload.

type DocGetter

type DocGetter interface {
	Get(ctx context.Context, kind, name string) (docindex.Doc, error)
	List(ctx context.Context, kind string) ([]docindex.Doc, error)
}

DocGetter is the read surface the engine needs over the authored-doc index (satisfied by *docindex.Store) — listing workflows (to resolve the one active for an item's category) and getting the reviewer skills / the baseline.

type DocState added in v0.0.274

type DocState struct {
	Name        string `json:"name"`
	Type        string `json:"type"`
	Body        string `json:"body,omitempty"`
	Truncated   bool   `json:"truncated,omitempty"`
	Binary      bool   `json:"binary,omitempty"`
	ContentType string `json:"content_type,omitempty"`
	Size        int64  `json:"size,omitempty"`
	SHA256      string `json:"sha256,omitempty"`
}

DocState is one story attachment injected into the transition payload. Binary attachments carry Binary=true with empty Body and must never consume docsPayloadCeiling (sty_40e5a305); fillPayloadDocs skips them entirely.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine judges status transitions against the active workflow's reviewer skills. A skill is either an LLM reviewer (its body rides as an isolated agent's system prompt) or a functional check (its frontmatter names a deterministic `check:` command the gate runs — the command's exit code is the verdict).

func New

func New(runner agentcli.Runner, docs DocGetter, repoRoot, model string) *Engine

New builds a Engine over the agent runner and doc index. model "" inherits the agent's default; the tool grant is read-only.

func (*Engine) DispatchExecutor

func (g *Engine) DispatchExecutor(ctx context.Context, item workitem.Item, toStatus string) (verb.DispatchResult, error)

DispatchExecutor implements verb.ExecutorDispatcher: when the TARGET state of an accepted transition is allocated to a NAMED agent (agent=<name>, neither "executor" nor "reviewer"), the binding's harness performs the step synchronously — prompt assembled from the item (title, body, acceptance criteria on stdin) plus the node's @skill rubric, tools/model/principles from the binding, nothing hardcoded (sty_fd427546). A missing binding or a failed run is an ERROR — the caller refuses the transition (broken definition never silently falls back in-loop, consistent with sty_d0d6bb67). agent=executor, agent-less and reviewer states dispatch nothing; a named binding whose harness is explicitly "in-loop" also stays with the orchestrator.

Flat dispatch (sty_05a5e203): this is the ONLY dispatch entering a state can cause, and it happens because the SPINE allocates the step — not because the state fires an agent of its own. Entry dispatch (on_enter_agent) is retired: steps never call steps, so an advisor is consulted by the orchestrator at a moment it chooses, and the route names which advisor that is.

func (*Engine) Gate

func (g *Engine) Gate(ctx context.Context, item workitem.Item, toStatus string) (verb.GateDecision, error)

Gate judges item's transition to toStatus against every reviewer governing the edge — the workflow-named reviewers (one, or an ordered list) followed by the always-on system reviewer layer. Each reviewer runs in order and ALL must accept; the first reject short-circuits and blocks the edge. It returns the per-reviewer verdicts in run order plus a top-level verdict mirroring the deciding reviewer (the first reject, or the last when all accept), so single-reviewer callers keep their contract. Gated=false (enact directly) when no reviewer governs the edge.

func (*Engine) Invoke added in v0.0.189

func (g *Engine) Invoke(ctx context.Context, req InvokeRequest) InvokeResult

Invoke is the ONLY path that calls agentcli.Runner.Run for LLM gate/dispatch steps (sty_ba860c8a). It resolves prompt assembly from the binding, runs the agent, and for ExpectVerdict applies the retry + verdict parse loop. On a classified rate-limit/unavailable failure, retries once on the binding's secondary (per-binding secondary= or [defaults] secondary) when configured (sty_5bf61f89).

func (*Engine) MandatorySummary added in v0.0.137

func (g *Engine) MandatorySummary(ctx context.Context, item workitem.Item) bool

MandatorySummary reports whether item's active workflow declares a MANDATORY step-summary node — used to gate the done-time missing-summary surfacing (sty_a1151fb0). Implements verb.StepSummariser.

func (*Engine) Retrospect added in v0.0.133

func (g *Engine) Retrospect(ctx context.Context, item workitem.Item) (verb.DispatchResult, error)

Retrospect dispatches the retrospective agent over a finished story: it pulls the story + its plan/summary/ledger by id, then emits 1–3 improvement PROPOSALS as backlog stories (its Bash(satelle:*) grant). Invoked per-story by `satelle story retrospect` — kept opt-in rather than auto-on-done so its cost (visible via `satelle story cost`, sty_a699ad14) is measured before it is made always-on. Returns the dispatch result (with captured output + token/wall-time cost) so the verb layer can record an agent_invocation for the cost view.

func (*Engine) ReviewCreate

func (g *Engine) ReviewCreate(ctx context.Context, draft verb.CreateDraft) (verb.GateDecision, error)

ReviewCreate judges a draft work item's required structure before it is persisted, DETERMINISTICALLY (internal/structure) — a clear goal and at least one numbered, testable acceptance criterion. No LLM, no agent CLI: the contract is code, so it is harness-independent and never flaky. Always Gated (the structure is the one thing satelle enforces on creation).

func (*Engine) SetActivity added in v0.0.408

func (g *Engine) SetActivity(fn func(itemID string, a Activity))

SetActivity wires the structured progress sink (CLI stamps the lease row).

func (*Engine) SetArtifactAttacher added in v0.0.351

func (g *Engine) SetArtifactAttacher(fn func(context.Context, workitem.Item, string, string, string) (string, string, error))

SetArtifactAttacher wires the verb-owned typed document writer used by structured step output contracts.

func (*Engine) SetChildrenResolver

func (g *Engine) SetChildrenResolver(fn func(ctx context.Context, parentID string) []ChildState)

SetChildrenResolver wires the resolver that lists a parent's child stories (id + status) so a container close gate judges the children-resolved rule from the payload satelle builds — not an on-disk story mirror. Nil-safe: an unwired resolver simply injects no children.

func (*Engine) SetConstitution added in v0.0.189

func (g *Engine) SetConstitution(body string)

SetConstitution sets the project constitution body injected order-zero into isolated agent briefings whenever principles ≠ none (design §5.3).

func (*Engine) SetDocsResolver added in v0.0.274

func (g *Engine) SetDocsResolver(fn func(ctx context.Context, itemID string) []DocState)

SetDocsResolver wires the resolver that lists an item's attachments so every isolated agent (reviewer, named executor, retrospective) receives plan/step summaries in the transition payload — the Bash-less read channel (sty_58fa970e). Nil-safe: an unwired resolver injects no docs.

func (*Engine) SetInjectPrinciples

func (g *Engine) SetInjectPrinciples(on bool)

SetInjectPrinciples sets whether the resident principles ride in an isolated reviewer's system prompt, from the agents layer's resolved `reviewer` binding (sty_46a40208). Defaults ON; a repo disables it with inject_principles = false. Also mutates reviewerBinding (deprecated alias + principles selector).

func (*Engine) SetLogDir

func (g *Engine) SetLogDir(dir string, cfg logfile.Config)

SetLogDir points the reviewer's transient-failure log at dir (the repo's <data_dir>/logs) and bounds it per cfg. When set, each transient reviewer failure — the failing subprocess's own output, e.g. a rate-limit message — is appended to reviewer.log so cross-session API contention is reviewable (sty_d71b0791), rotated daily + by size (sty_a67e6e8c). An empty dir disables logging.

func (*Engine) SetNamedAgents

func (g *Engine) SetNamedAgents(fn func(name string) (config.AgentBinding, bool))

SetNamedAgents wires the resolver for NAMED agent bindings from the agents layer (.satelle/workflows/agents.toml [<name>] sections) — the WHO of a workflow node's agent=<name> allocation (sty_fd427546). Nil keeps every step in-loop.

func (*Engine) SetProgress

func (g *Engine) SetProgress(fn func(msg string))

SetProgress wires the sink for one-line gate progress messages (the CLI prints them to stderr). nil disables emission.

func (*Engine) SetReviewerBinding added in v0.0.189

func (g *Engine) SetReviewerBinding(b config.AgentBinding)

SetReviewerBinding stores the full resolved [reviewer] binding as the single resolution source for Invoke (sty_ba860c8a). Scalar caches are synced from it.

func (*Engine) SetReviewerEnv added in v0.0.129

func (g *Engine) SetReviewerEnv(env map[string]string)

SetReviewerEnv sets the reviewer binding's resolved env (config.ResolveAgentEnvs), applied to every isolated reviewer AND step-summariser subprocess this Engine runs — so a repo can point its reviews at an alternate model backend the same way a named executor does (sty_001558ce). Absent/empty leaves the child env at the inherited process env. Also mutates reviewerBinding.

func (*Engine) SetReviewerModel

func (g *Engine) SetReviewerModel(model string)

SetReviewerModel sets the reviewer's model from the agents layer (the resolved `reviewer` binding's `model`). It rides as `--model` to every isolated reviewer this Engine runs, so a repo can review on a different model (e.g. sonnet) without touching the executor. An empty value is ignored, keeping the agent CLI's default model (no `--model` flag emitted). Also mutates reviewerBinding.

func (*Engine) SetReviewerTools

func (g *Engine) SetReviewerTools(tools string)

SetReviewerTools sets the reviewer's tool grant from the agents layer (the resolved `reviewer` binding). It governs every isolated LLM reviewer this Engine runs. The default remains the read-only grant; a repo may widen or narrow it in .satelle/workflows/agents.toml without touching the workflow. An empty value is ignored so callers can pass through an unset binding safely. Also mutates reviewerBinding.

func (*Engine) SetRunner

func (g *Engine) SetRunner(r agentcli.Runner)

SetRunner overrides the reviewer's agent-CLI runner — the agents layer's `reviewer` harness binding, resolved to a Runner. A nil runner is ignored, keeping the default configured at construction (the global `[agent] cli`).

func (*Engine) SetSecondaryResolver added in v0.0.294

func (g *Engine) SetSecondaryResolver(fn func(section string, b config.AgentBinding) (config.AgentBinding, string, bool))

SetSecondaryResolver wires rate-limit failover (sty_5bf61f89). The resolver returns (binding, name, ok) for a primary section + binding.

func (*Engine) SetTelemetry added in v0.0.138

func (g *Engine) SetTelemetry(fn TelemetryFunc)

SetTelemetry wires the sink the engine uses to record a dispatch-level telemetry event (agent-retry/agent-failure/agent-timeout). Pass nil to disable (the default) — every call site nil-checks via telemetryEvent.

func (*Engine) Summarise

func (g *Engine) Summarise(ctx context.Context, item workitem.Item, from, to string) (verb.SummaryResult, error)

Summarise runs the read-only summariser over an enacted transition and returns its prose recap (empty when no summariser rubric is installed). The binding named on the step node (agent=<name>, default [reviewer]) supplies the harness and model so a cheap summariser can narrate without burning the deep judgment model. Grant stays read-only (observes, never mutates).

principles stays PrinciplesNone regardless of the binding's principles= (deliberate): the summariser is a rubric-only narrator; honouring session principles on every transition would inject the constitution into every recap and defeat a cheap high-frequency path (sty_8ee40f94).

TODO(sty_ba860c8a): fold onto Invoke once a soft-fail/empty-retry expect mode exists without ballooning ExpectVerdict/ExpectPerform. Today it still uses buildRequest+runOnce directly (AC1 carve-out).

func (*Engine) WorkflowNameFor

func (g *Engine) WorkflowNameFor(ctx context.Context, category string) string

WorkflowNameFor returns the name of the workflow that governs a story of the given category — the value stamped on the story at create. Empty when no workflow governs the category. Used by the create path to record the choice.

func (*Engine) WorkflowStates

func (g *Engine) WorkflowStates(ctx context.Context, name string) ([]string, bool)

WorkflowStates returns the lifecycle states the named workflow declares — the nodes on its TRANSITIONS (an edge-less declared reviewer node like estimate/ step is a gate declaration, not a lifecycle state) — and whether the workflow resolves at all. The restamp validation seam (sty_ed3386cf): a story may only be re-stamped onto a workflow that declares its current status.

A lifecycle is a DERIVED ROUTE now, and a route's states depend on the story's category — which a name alone does not carry. So a name that resolves returns no states, and the caller skips the status check rather than stranding the story: the same contract an unparseable lifecycle always had here (sty_d953c5d8).

The derived route resolves by NAME, not by doc lookup (sty_81bb0dde). It is the one lifecycle that has no document of its own — it is built from the two route-source halves — so asking the index for a doc called "default" always misses, and `satelle story restamp` refused to stamp the very name the create path assigns. That is the exact round trip a stamp has to support: whatever WorkflowNameFor hands out, restamp must accept back.

type Expect added in v0.0.189

type Expect int

Expect selects the post-run contract for Invoke (sty_ba860c8a / design §4). ExpectVerdict runs the reviewer retry/parse loop; ExpectPerform is a single run.

const (
	// ExpectVerdict: isolated judge — output must parse to a gate decision.
	ExpectVerdict Expect = iota
	// ExpectPerform: performer — raw stdout, no verdict parse, no retry.
	ExpectPerform
)

func ExpectFromRole added in v0.0.189

func ExpectFromRole(role string) Expect

ExpectFromRole maps a resolved binding role to an Expect. role=reviewer → verdict; everything else → perform.

type InvokeRequest added in v0.0.189

type InvokeRequest struct {
	Binding config.AgentBinding // resolved binding (tools/model/env/settings/role/principles)
	Section string              // agents.toml section name (for role inference / logging)
	Rubric  string              // skill body from the workflow node/edge
	Payload any                 // marshalled to JSON stdin
	Charter string              // optional override; empty → charter from role/expect
	// Expect selects the contract. Zero value is ExpectVerdict — callers that
	// perform must set ExpectPerform explicitly.
	Expect  Expect
	Timeout time.Duration // ≤0 → no per-run deadline (tests) or caller-supplied 0
	// Runner overrides the runner built from Binding.Command. Reviewer path
	// passes g.runner (bootstrap-resolved); named dispatch builds from the binding.
	Runner agentcli.Runner
	// Attempts bounds verdict retries (0 → engine default). Perform ignores this.
	Attempts int
	// Sink, when set, receives live subprocess stdout (named dispatch live log).
	Sink io.Writer
	// OnEvent observes provider-neutral progressive events. It is composed with
	// the engine's interactive stderr progress consumer.
	OnEvent agentcli.EventHandler

	// Telemetry / progress context for the verdict retry loop (design note:
	// Invoke needs labels beyond the minimal design sketch so retry telemetry
	// and progress lines move intact from runReviewer).
	StoryID string
	Step    string // to-status or step name
	Skill   string // rubric skill name for telemetry/logging
	Actor   string // ledger/telemetry actor (default "reviewer" for verdict)
}

InvokeRequest describes one isolated-agent call through the shared Invoke seam. Call sites (runReviewer, DispatchExecutor, Retrospect) fill this after their own pre-flight; Invoke owns prompt assembly, Runner.Run, and (for verdict) retry/parse.

type InvokeResult added in v0.0.189

type InvokeResult struct {
	Stdout   []byte
	Usage    agentcli.UsageResult
	Command  string             // resolved harness command for ledger evidence
	Decision *verb.GateDecision // non-nil when ExpectVerdict and parse succeeded
	Err      error
}

InvokeResult is the outcome of one Invoke call.

type TelemetryFunc added in v0.0.138

type TelemetryFunc func(ctx context.Context, storyID, actor, kind string, data map[string]any)

TelemetryFunc records one typed telemetry/quality event for storyID. Callers pass the event's outcome/kind and its typed data (never env/secrets — the implementation validates). Implemented by verb.AppendTelemetry and wired via SetTelemetry; best-effort by contract, so it takes no error return.

Jump to

Keyboard shortcuts

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