Documentation
¶
Overview ¶
Package harness runs a workflow of minimal markdown skills: a router picks the skills for a task, each skill is one narrow prompt with a success check, and a loop runs them in order, feeding each stage's output to the next and stopping at the first stage that fails its check after retries.
The three layers live in their own files: router.go, skill.go and loop.go; workflow.go parses the workflow file that ties them together. Each stage is an ordinary single tinyagent run.
Index ¶
- Constants
- func LoadSkills(dir string) (map[string]*Skill, error)
- func Prompt(idx, n int, st Stage, task, prev, prevName, cwd, feedback string) string
- func Unsatisfiable(c Check) bool
- type Attempt
- type BehaviorCase
- type BehaviorExpect
- type BehaviorFile
- type BehaviorResult
- type Check
- type GateDecision
- type GateResult
- type Options
- type Pipeline
- type Result
- type Route
- type RouteError
- type RunAgent
- type Settings
- type Skill
- type Stage
- type Workflow
Constants ¶
const ( // MaxBehaviorCases bounds the cases in one behavior file. MaxBehaviorCases = 20 // MaxBehaviorFileBytes caps a behavior file. MaxBehaviorFileBytes = 64 << 10 // DefaultBehaviorTimeout bounds one case's command, in seconds; a case // may ask for more, up to the stage's check_timeout. DefaultBehaviorTimeout = 60 // MaxBehaviorOutput caps each stream of observed output kept as evidence. MaxBehaviorOutput = 4 << 10 )
Behavior-check bounds.
const ( StatusPassed = "Passed" StatusFailed = "Failed" StatusInterrupted = "Interrupted" // StatusBlocked means a stage ran out of retries because its success // check kept failing on its merits (a check command exited non-zero, the // model reported SUCCESS CHECK: FAIL, behavior cases failed): the task // as specified can't be satisfied without a human. StatusFailed is kept // for transient or infrastructure failures. StatusBlocked = "Blocked" )
Workflow statuses.
const ( MaxCheckOutput = 8 << 10 MaxFeedback = 4 << 10 )
MaxCheckOutput caps a check command's recorded output, and MaxFeedback the failure reason handed to a retry.
const ( // DefaultRetries is how many times a failed stage is retried. DefaultRetries = 1 // MaxRetries bounds retries per stage. MaxRetries = 5 // DefaultStepLimit is each stage run's --step-limit unless overridden. DefaultStepLimit = 50 // DefaultCheckTimeout bounds a check command, in seconds. DefaultCheckTimeout = 300 // MaxStages bounds a workflow's length. MaxStages = 20 // MaxWorkflowBytes caps the workflow file. MaxWorkflowBytes = 256 << 10 )
Defaults and bounds.
const ExitBlocked = 5
ExitBlocked is the process exit code for StatusBlocked.
const GateSpec = "spec"
GateSpec is the only stage gate: it checks a plan-tasks stage's output before the next stage (usually generate-code) starts.
const MaxSkillBytes = 256 << 10
MaxSkillBytes caps the size of one skill file.
const MinRouteScore = 2
MinRouteScore is how many distinct content words a task must share with an intent for the router to pick it.
Variables ¶
This section is empty.
Functions ¶
func LoadSkills ¶
LoadSkills reads every skill in dir: <name>.md files and <name>/SKILL.md folders. A name defined both ways, an invalid name or an unparseable file is an error.
func Prompt ¶
Prompt builds stage idx's task: the skill's instructions, its inputs and, on a retry, why the previous attempt failed.
func Unsatisfiable ¶ added in v0.12.0
Unsatisfiable reports whether a failed check failed on its merits, so a stage that keeps failing it is blocked rather than failed: a check command that ran and exited non-zero, a model verdict of SUCCESS CHECK: FAIL, or behavior cases that ran and failed. A run error or limit hit, a missing verdict line, a check command that timed out or couldn't start, and an invalid behavior file are transient.
Types ¶
type Attempt ¶
type Attempt struct {
Stage int `json:"stage"` // 1-based
Skill string `json:"skill"`
Attempt int `json:"attempt"` // 1-based
Input string `json:"input"`
Output string `json:"output"`
ExitStatus string `json:"exit_status"`
Check Check `json:"check"`
Steps int `json:"steps"`
CostUSD float64 `json:"cost_usd"`
Trajectory string `json:"trajectory,omitempty"`
Log string `json:"log,omitempty"`
DurationMS int64 `json:"duration_ms"`
}
Attempt is one run of one stage.
type BehaviorCase ¶ added in v0.12.0
type BehaviorCase struct {
Name string `yaml:"name"`
Criterion string `yaml:"criterion"`
Run []string `yaml:"run"`
Stdin string `yaml:"stdin"`
Timeout int `yaml:"timeout"`
Expect BehaviorExpect `yaml:"expect"`
}
BehaviorCase runs one command and checks what it actually printed.
type BehaviorExpect ¶ added in v0.12.0
type BehaviorExpect struct {
ExitCode *int `yaml:"exit_code"`
StdoutContains []string `yaml:"stdout_contains"`
StdoutNotContains []string `yaml:"stdout_not_contains"`
StdoutMatches []string `yaml:"stdout_matches"`
StderrContains []string `yaml:"stderr_contains"`
}
BehaviorExpect is the observable outcome a case requires. Exit code defaults to 0.
type BehaviorFile ¶ added in v0.12.0
type BehaviorFile struct {
Version int `yaml:"version"`
Cases []BehaviorCase `yaml:"cases"`
}
BehaviorFile is what a verify-behavior stage writes: the task's behavioral acceptance criteria as commands to run and the output each must produce. No cases means the task has no behavioral criteria.
type BehaviorResult ¶ added in v0.12.0
type BehaviorResult struct {
Name string `json:"name"`
Criterion string `json:"criterion,omitempty"`
Argv []string `json:"argv"`
Passed bool `json:"passed"`
ExitCode int `json:"exit_code"`
TimedOut bool `json:"timed_out,omitempty"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Failures []string `json:"failures,omitempty"`
}
BehaviorResult is one case's evidence: what ran, what it printed and which expectations failed.
type Check ¶
type Check struct {
Method string `json:"method"` // "command" or "model"
Passed bool `json:"passed"`
Reason string `json:"reason,omitempty"`
Argv []string `json:"argv,omitempty"`
ExitCode int `json:"exit_code,omitempty"`
TimedOut bool `json:"timed_out,omitempty"`
Output string `json:"output,omitempty"`
// Skipped is set when a behavior stage found no behavioral criteria.
Skipped bool `json:"skipped,omitempty"`
// NoVerdict is set when a model-checked stage's output had no
// SUCCESS CHECK line.
NoVerdict bool `json:"no_verdict,omitempty"`
// Evidence is each behavior case's observed output.
Evidence []BehaviorResult `json:"evidence,omitempty"`
}
Check is the outcome of a stage's success check.
type GateDecision ¶ added in v0.12.0
type GateDecision struct {
Stage int `json:"stage"` // 1-based
Skill string `json:"skill"`
Gate string `json:"gate"`
Attempt int `json:"attempt"` // 1 = first plan, 2 = the re-plan
Passed bool `json:"passed"`
Reason string `json:"reason,omitempty"`
Missing []string `json:"missing,omitempty"`
}
GateDecision is one gate decision, recorded in Result.Gates and emitted as a stage.gate event.
type GateResult ¶ added in v0.12.0
type GateResult struct {
Passed bool `json:"passed"`
// Tasks is how many task blocks the plan had.
Tasks int `json:"tasks"`
Reason string `json:"reason,omitempty"`
Missing []string `json:"missing,omitempty"`
}
GateResult is the outcome of SpecGate.
func SpecGate ¶ added in v0.12.0
func SpecGate(plan, cwd string) GateResult
SpecGate checks that a plan has at least one task and that every task has at least one acceptance-criteria bullet and a "Verify:" line with a runnable command in backticks. "Runnable" means the command's program (its first word after any VAR=value prefixes) is found on PATH, or, if it contains a slash, exists relative to cwd. The command is not run: the code it verifies doesn't exist yet.
type Options ¶
type Options struct {
Route Route
Task string
Cwd string // absolute working directory shared by every stage
// ExtraArgs are forwarded to every stage run before the harness's own.
ExtraArgs []string
// Dir receives each attempt's trajectory and log.
Dir string
RunAgent RunAgent
// Emit, if set, receives workflow.* and stage.* events.
Emit func(kind string, fields map[string]any)
// Progress, if set, receives one line per stage attempt.
Progress io.Writer
}
Options configure Run.
type Pipeline ¶
type Pipeline struct {
Name string `json:"name"`
Intent string `json:"intent,omitempty"`
Stages []Stage `json:"stages"`
}
Pipeline is one named, resolved workflow.
type Result ¶
type Result struct {
SchemaVersion int `json:"schema_version"`
Kind string `json:"kind"` // "workflow"
Workflow string `json:"workflow"`
Route Route `json:"route"`
Task string `json:"task"`
Cwd string `json:"cwd"`
Status string `json:"status"`
FailedStage string `json:"failed_stage,omitempty"`
Reason string `json:"reason,omitempty"`
Output string `json:"output,omitempty"` // the last stage's output
Stages []Stage `json:"stages"`
Attempts []Attempt `json:"attempts"`
// Gates are the stage gate decisions, in order.
Gates []GateDecision `json:"gates,omitempty"`
CostUSD float64 `json:"cost_usd"`
DurationMS int64 `json:"duration_ms"`
}
Result is a whole workflow run; it is what --output writes.
type Route ¶
type Route struct {
Pipeline *Pipeline `json:"-"`
Name string `json:"name"`
Kind string `json:"kind"` // "workflow" or "skill"
// How is "named" (--workflow-name), "intent" (matched the task) or
// "default" (the file's default).
How string `json:"how"`
Score int `json:"score,omitempty"`
}
Route is the router's decision: the pipeline to run and why.
type RouteError ¶
type RouteError struct{ Msg string }
RouteError is returned when the router can't choose; no stage has run.
func (*RouteError) Error ¶
func (e *RouteError) Error() string
type Settings ¶
type Settings struct {
Model string `yaml:"model" json:"model,omitempty"`
Profile string `yaml:"profile" json:"profile,omitempty"`
StepLimit *int `yaml:"step_limit" json:"step_limit,omitempty"`
CostLimit *float64 `yaml:"cost_limit" json:"cost_limit,omitempty"`
TokenLimit *int `yaml:"token_limit" json:"token_limit,omitempty"`
TimeLimit *int `yaml:"time_limit" json:"time_limit,omitempty"`
Retries *int `yaml:"retries" json:"retries,omitempty"`
CheckTimeout *int `yaml:"check_timeout" json:"check_timeout,omitempty"`
}
Settings are the overridable per-stage settings; zero values inherit.
type Skill ¶
type Skill struct {
Name string `json:"name"`
Title string `json:"title,omitempty"`
Intent string `json:"intent"`
Inputs []string `json:"inputs"`
SuccessCheck string `json:"success_check,omitempty"`
// Body is the file's Markdown with any YAML frontmatter removed; it
// is the stage's instructions.
Body string `json:"-"`
Path string `json:"path"`
}
Skill is one parsed skill file: a heading, an intent line, declared inputs and a one-line success check. Everything else (model, limits, retries, check commands) comes from the workflow file's defaults.
func LoadSkillFile ¶ added in v0.12.0
LoadSkillFile reads and parses one skill file with the same rules LoadSkills applies: its name is the file stem, or the folder name for a <name>/SKILL.md, and must be a valid skill name.
type Stage ¶
type Stage struct {
Skill *Skill `json:"skill"`
// Check is a command whose exit status decides the stage's success
// check; when empty the model's own verdict on the skill's success
// check line decides.
Check []string `json:"check,omitempty"`
// Behavior, when set, is a path (relative to --cwd) where the stage
// writes behavior cases; the harness runs them and decides the stage on
// their observed output. See behavior.go.
Behavior string `json:"behavior,omitempty"`
// Gate, when "spec", checks the stage's output with SpecGate after it
// passes; see gate.go.
Gate string `json:"gate,omitempty"`
CheckTimeout int `json:"check_timeout"`
Retries int `json:"retries"`
Model string `json:"model,omitempty"`
Profile string `json:"profile,omitempty"`
StepLimit int `json:"step_limit"`
CostLimit float64 `json:"cost_limit,omitempty"`
TokenLimit int `json:"token_limit,omitempty"`
TimeLimit int `json:"time_limit,omitempty"`
Inputs map[string]string `json:"inputs,omitempty"`
}
Stage is one resolved workflow step.
type Workflow ¶
type Workflow struct {
Path string
SkillsDir string
Default string
Skills map[string]*Skill
Pipelines map[string]*Pipeline
// contains filtered or unexported fields
}
Workflow is a parsed, validated workflow file.
func Load ¶
Load parses and validates the workflow file at path and every skill in its skills directory. Any problem is returned before a model is called.
func (*Workflow) Route ¶
Route picks what to run for task. An explicit name (a workflow or a skill) wins; otherwise the task is matched against every workflow's intent and every skill's intent line, and the single best match with at least MinRouteScore shared words is chosen. With no match (or a tie) the file's default is used if it has one; otherwise Route returns a RouteError rather than guessing.