orchestrate

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package orchestrate is the adaptive runner: a planner model and a frontier scheduler, married. The planner never works and the scheduler never thinks. A node's needs are the only launch gate code consults; the planner amends the frontier on every completion, concurrently, so execution never blocks on thought. The graph is never designed — it crystallizes as the trace of what the planner did.

This file is the CONTRACT: types the session, the room, the roster, and the rig all code against. Behavior lands behind these shapes.

Index

Constants

View Source
const (
	GateTopup  = "topup"
	GateFinish = "finish"
	GateStop   = "stop"
)

The three answers to the gate. They are the whole vocabulary, and a surface spells them exactly: "topup:2.50", "finish", "stop".

View Source
const DefaultLanes = 4

DefaultLanes is how many nodes run at once when the caller names no number. It is small because a node is small: parallelism here is node COUNT, and a width that outruns the provider's pacing buys queueing, not speed.

View Source
const NameWords = 3

NameWords is how many words a node's Node.Title may run to.

THREE IS THE LENGTH A PERSON READS AS A LABEL rather than as a sentence, and it is a cap and not a target — a two-word name is left at two. It is the figure the whole product names work by: internal/session's TaskNameWords is this constant, and the rail column that draws these rows cuts to the same number (internal/tui3's taskTitleWords). One figure, because a planner asked for more words than the column can show is a planner being billed for words thrown away on the way to the screen.

View Source
const RepeatLimit = 2

RepeatLimit is how many times one run may send work at the same file before it stops sending it at all.

TWO, because the first failure is news and the second is a pattern. A planner that reads a node's digest and cannot see the filesystem has exactly one way to react to work that did not land — send it again — and nothing in the loop ever tires of that: a real run spent eight nodes and twenty-seven minutes re-issuing one brief ("write the final report to research/…md"), every one of which came back saying it was about to do the work, and the file was never there. The third attempt is where a run has to stop guessing and say so.

View Source
const StoppedWord = "stopped; what finished is kept"

StoppedWord is the one sentence this package says about a stopped run, wherever the stop came from. It is exported because the session says it back to the person ([session.Agent.Cancel]) and two spellings of one decision is one spelling too many.

View Source
const SynthesisID = "synthesis"

SynthesisID is what the closing call is called in the trace. It is not a node — it is never scheduled, never cancellable, and its cost is the run's rather than any node's — and it has an id at all so that a surface drawing the run's spend has a name for the last thing it paid for.

View Source
const WarnMark = 0.80

WarnMark is where the run says the tank is getting low, once. It is early enough that a person can top up before anything stops, and late enough that it is not a line every run prints out of caution.

Variables

View Source
var PlannerPrompt = strings.ReplaceAll(plannerLaw, "{{NAME_WORDS}}", strconv.Itoa(NameWords))

PlannerPrompt is the planner's law, held as prose beside the contract it describes.

WHY AN ASSET AND NOT A STRING BUILDER: the ten laws are an ARGUMENT — about what a node is for, when an edge is real, and why saying nothing is an answer — and an argument is written in paragraphs. Held in Go it was held in fragments, every line wearing quotes and an escape, and the shape of it was invisible to the person changing it. Held here it is a document, diffed as a document, editable by somebody who is not going to open a compiler. This is internal/subharness/prompts' arrangement, for its reasons.

ONE PLACEHOLDER, AND IT IS A CAP THE CODE ENFORCES. This document describes a CONTRACT — the shapes in orchestrate.go — and a contract has no numbers to drift, with one exception: NameWords, the length a node's title may run to, which is also the length the column drawing it cuts to (NodeTitle holds a planner to it). A figure a model reasons with must be the figure the code applies, so it is filled from the constant rather than typed into the prose, exactly as the designer's guide fills its caps. What could also drift is the Amendment schema quoted in PART THREE, and a test in this package holds that against the struct tags rather than a renderer.

It is short on purpose. The designer is called once per design; the planner is called once at the start and once on EVERY node completion, so every paragraph here is billed against the run's one tank as many times as the run is wide.

Functions

func Covers

func Covers(scope []string, target string) bool

Covers reports whether a write scope contains one path. It is exported for the enforcement point: the scope is a claim about what a node may write, and the code that refuses a write outside it must read the claim exactly as the scheduler does when it decides two nodes collide.

func Dollars

func Dollars(amount float64) string

Dollars is the one way this run writes money, so that the note, the gauge and the gate all say it the same way.

func Meter

func Meter(tokens int, model string) float64

Meter is what one call's tokens cost when nobody said.

IT CHARGES EVERY TOKEN AT THE OUTPUT RATE, deliberately. The caller that reaches this has one number and not two, and the two rates differ by up to five times; a tank that empties early stops and asks a person, and a tank that empties late has already spent their money. Callers that know both halves — which is every caller reading a provider's usage — use MeterCall and get the honest figure.

func MeterCall

func MeterCall(in, out int, model string) float64

MeterCall is one call's cost from its two token counts.

func NodeNeedsName

func NodeNeedsName(n Node) bool

NodeNeedsName reports whether a node has arrived with nothing but the machine's own filing where its name should be.

THE TEST IS GENERIC AND IT IS THE WHOLE TEST: the name is missing, or it is the id over again. `token-bucket` spells out as `token bucket` and is a name; `r1` spells out as `r1` and is the id a planner numbered its own list with. There is no list of id shapes here and there must never be one — a blacklist of `r%d`, `n%d`, `step%d` is a rule that is out of date the first time a planner counts differently, and this question is the same question at every door: is there anything here a person could read as the name of the work.

func NodeTitle

func NodeTitle(n Node) string

NodeTitle is the name one node is drawn under, and it is the ONLY place in this package that answers the question — so every surface asking it gets the same answer, and none of them has to invent one.

TWO ANSWERS, AND NEITHER OF THEM READS THE GOAL. The planner's own title is taken as written, because a model that was asked to name the slice has done exactly the work a namer would be paid to do again. When there is none — an older planner, a reply that lost the key, a node this package minted itself ([Orchestrator.synthesize]) — the name is built from the ID, which the law already requires to be a slug: `token-bucket` is `token bucket`, and that is a derivation from a structured field rather than a sentence cut short.

A GOAL IS NEVER A NAME. That is the whole point of the field, and a fallback that reached for the goal would put the defect back the moment a planner forgot the key.

THE SECOND ANSWER IS THE LAST RESORT AND NOT THE PLAN. An id only reads as a name when the planner spelt one into it, and half the ids models mint are `r1`, `n3`, `synth` — filing, not language. Those are the ones NodeNeedsName picks out and Options.Name replaces before anybody reads the row; what is left down here is the answer for a run with no namer behind it at all, which is every headless caller and every scripted test.

func UsePrices added in v0.3.0

func UsePrices(src PriceSource)

UsePrices installs (or, with nil, removes) the source asked before the table. Replacing one source with another is the whole point — a tariff can move without this package being rebuilt — and a swap is safe to make while other goroutines are metering: a call in flight sees either the old source or the new one, never half of either.

Types

type Amendment

type Amendment struct {
	Add    []Node    `json:"add,omitempty"`
	Cancel []Cancel  `json:"cancel,omitempty"`
	Note   string    `json:"note,omitempty"` // one line, shown between completions
	Done   *DonePlan `json:"done,omitempty"`
}

Amendment is the planner's whole vocabulary: add nodes, cancel pending ones, say what it is thinking, or finish. An empty Amendment is the cheap NOOP — the expected answer to most completions.

func ParseAmendment

func ParseAmendment(raw string) (Amendment, error)

ParseAmendment reads one planner reply.

SILENCE IS A NOOP. A planner that answered with nothing, with whitespace, or with the word every model reaches for when it has nothing to say is not an error — it is the commonest legal answer there is, and a run that treated it as a failure would put a note on the person's screen after every completion.

func (Amendment) Empty

func (a Amendment) Empty() bool

Empty reports the NOOP: the planner looked, and there is nothing to change. It is the expected answer to most completions and it costs the run nothing.

type Cancel

type Cancel struct {
	ID     string `json:"id"`
	Reason string `json:"reason"`
}

Cancel drops a pending node. Running nodes are never touched (the commitment law); completed nodes are facts.

type DonePlan

type DonePlan struct {
	Brief string `json:"brief"`
}

DonePlan ends the run: the planner has judged the goal answered and names the synthesis brief. Synthesis grounds every claim in node ids.

type Executor

type Executor interface {
	Exec(ctx context.Context, n Node, deps []NodeStatus) (digest string, cost float64, err error)
}

Executor runs one node and returns its digest. The session supplies this: it is the agent loop with the node's whitelist and worktree resolved.

type Fuel

type Fuel struct {
	Cap   float64 `json:"cap"`
	Spent float64 `json:"spent"`
}

Fuel is the run's one tank. Cap is dollars the person approved; Spent rolls up every model call anywhere in the run, planner calls included.

func (Fuel) Gauge

func (f Fuel) Gauge() string

Gauge is the spend summary a surface prints: "$1.60 of $2.00", and just the spend when the run was never capped.

func (Fuel) Low

func (f Fuel) Low() bool

Low reports whether the tank has crossed the warning mark.

type Node

type Node struct {
	ID string `json:"id"`
	// Title is the TWO OR THREE WORDS THIS NODE IS CALLED, and it is a
	// first-class field rather than something a surface derives.
	//
	// IT EXISTS BECAUSE A PROMPT IS NOT A NAME. Every surface that draws work
	// draws a short title — the rail's column, home's cards, a roster row — and
	// until this field existed there was nothing on a node to draw but Goal, so
	// each of them cut the first few words off a self-contained brief. Law 4
	// makes those briefs open in the second person, so a run divided nine ways
	// drew nine rows all reading "You are a": the column named every worker
	// after how its instructions cleared their throat, and a person tracking one
	// of them had to read every row.
	//
	// THE PLANNER FILLS IT, and a node that arrives without one is named from
	// its ID — which is already a slug the planner minted as a name — by
	// [NodeTitle]. Nothing anywhere derives a title by truncating prose.
	//
	// AND IT IS THE ONE FIELD EVERY SURFACE READS. [Orchestrator.apply] settles
	// it once, at the only place the frontier is written, so a row is drawn from
	// this field rather than re-derived from the ID somewhere downstream: two
	// derivations of one name are two names the moment one of them learns
	// something the other did not — which is exactly what a run's rows did while
	// the namer ([Options.Name]) was filling them in.
	Title string   `json:"title"`
	Goal  string   `json:"goal"`            // self-contained: no "see above"
	Needs []string `json:"needs,omitempty"` // ids that must be Done before this may run
	Kind  string   `json:"kind,omitempty"`  // subharness node kind; empty is agent.loop
	// WriteScope is the set of repo paths this node may write (law 10). Two
	// nodes whose scopes intersect are not independent, and the scheduler
	// serializes them by force of an added Needs edge. Read-only nodes leave
	// this empty.
	WriteScope []string `json:"write_scope,omitempty"`
	// Worktree says the planner judged this node needs an isolated worktree
	// (law: hybrid collision policy — shared tree by default, worktree when
	// the deliverable is a branch or the planner says so). The path comes
	// from the session's WorktreePath seam, not from the planner.
	Worktree bool   `json:"worktree,omitempty"`
	Verify   string `json:"verify,omitempty"` // verify rung for this node's output, empty is none
}

Node is one small unit of work the planner wants. Small is the law: one question, one artifact, a handful of turns; parallelism comes from node count, never node size.

type NodeStatus

type NodeStatus struct {
	Node
	State  State   `json:"state"`
	Digest string  `json:"digest,omitempty"`
	Err    string  `json:"err,omitempty"`
	Cost   float64 `json:"cost"` // dollars this node has burned
}

NodeStatus is a Node plus what the run knows about it so far. Digest is the condensed output — what dependents and the planner see, never the raw artifact.

type Options

type Options struct {
	// Cap is the fuel tank in dollars. Zero is NO CAP — a run nobody bounded —
	// and it is the caller's decision, not a default this package invents.
	Cap float64
	// Lanes bounds how many nodes execute at once; zero is [DefaultLanes].
	Lanes int
	// Planner NAMES the model behind the [Planner] interface, for a surface to
	// draw beside the gauge ([Snapshot.Planner]). Nothing here reads it: which
	// model thinks is the caller's decision and this package only carries the
	// word, so an empty one costs a surface a segment and costs the run nothing.
	Planner string

	// OnNote carries one planner note, in the order the planner wrote them.
	OnNote func(text string)
	// OnFuel is the gauge's early warning, fired ONCE when spend crosses
	// [WarnMark]. It is not a running meter: a surface that wants the figure
	// at any other moment reads it off [Orchestrator.Snapshot].
	OnFuel func(f Fuel)
	// OnPause fires when the tank is empty and the run has stopped launching.
	// The answer comes back through [Orchestrator.Resolve].
	OnPause func(f Fuel)
	// OnNodes carries the crystallized graph every time any of it moves — a node
	// launched, a node landed, a node the planner added or a person cancelled. It
	// is the same slice [Snapshot.Nodes] carries, published at the same moment and
	// for a reader that cannot poll: the session registers a run's nodes as a task
	// FAMILY so the roster has a tree to draw (session's orchestrate.go), and a
	// registration driven by polling would be a clock asking a run whether
	// anything happened.
	//
	// IT IS THE WHOLE GRAPH AND NOT THE NODE THAT MOVED, because a publish is not
	// one transition: [Orchestrator.launch] starts up to a lane's worth at once,
	// and an amendment can add four nodes and cancel one. What moved is a
	// difference the reader already has to compute against what it drew last, so
	// this hands it the state rather than a guess at the event.
	//
	// The slice is the SNAPSHOT'S OWN and is read-only to the callback: writing
	// through it would edit what the next [Orchestrator.Snapshot] hands back.
	OnNodes func(nodes []NodeStatus)

	// Name is asked for the two or three words a node is called when the planner
	// did not write them ([NodeNeedsName] asks the question). It is the BACKSTOP
	// on the law that every row a person reads carries a name: the law asks the
	// planner for one on the call it adds the node, and a planner that answers
	// `{"id": "r1", "goal": …}` anyway is a fact about models rather than a fault
	// this package can refuse — dropping the amendment would throw away the
	// planner's judgement about what work exists to buy three words.
	//
	// THIS PACKAGE DOES NOT KNOW WHAT A MODEL IS, which is why it is a seam. The
	// session wires it to the one small namer everything else in the product is
	// named by (internal/session's taskname.go), so a run's rows and an admitted
	// task's row are named by the same call and read as one column.
	//
	// IT IS CALLED ON A GOROUTINE OF ITS OWN, off the loop, never under the lock,
	// and it may take seconds: NOTHING WAITS FOR IT. The node is on the frontier
	// and launchable before it is asked, the row is already drawn, and the answer
	// arrives as a rename ([Orchestrator.nameNode]). A seam that is nil, or one
	// that answers nothing, leaves the node named the way [NodeTitle] names it.
	Name func(ctx context.Context, n Node) string
}

Options is everything about a run that is not the goal, the planner or the executor: the tank, the width, and the three lanes a run talks on.

The callbacks are how this package says things without knowing what a surface is. They are called with no lock held and they must not block: the session's own emit (orchestrate.go) fans out to watchers on buffered streams, which is the shape they are written for.

type Orchestrator

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

Orchestrator is one adaptive run. Construct it with New; everything below mu is written by the loop and read by whoever is watching.

func New

func New(goal string, planner Planner, exec Executor, opts Options) *Orchestrator

New builds a run. Nothing starts until Run is called.

func (*Orchestrator) Cancel

func (o *Orchestrator) Cancel()

Cancel ends the run on a person's word, and it means STOP SPENDING NOW.

It is the harder half of a pair the fuel gate already has one of. "stop" at the gate is a decision taken with nothing in flight — the tank emptied, the running nodes landed, and the question was asked afterwards. This is the same decision taken mid-run, so it has to do the thing the gate never had to: cut the context every node and every planner call is on, and throw away what they had got to. A node's partial output is not a result — it is a half-answer to a question nobody is waiting for any more — so it is dropped rather than digested ([Orchestrator.land]).

WHAT IS KEPT IS THE TRACE. Every node that finished keeps its digest, the notes stay, the tank keeps what it metered, and the snapshot says Stopped. What does not happen is the synthesis: it is one more model call, and somebody who has just said stop is not asking to pay for it (see [Orchestrator.synthesize]).

A PENDING NODE STOPS INSTANTLY, and it stops IN PLACE — marked Cancelled, still in the graph — which is where this parts company with the planner's own cancel. An amendment DELETES the node it no longer wants (amend.go's dropLocked), because that is the planner changing its mind about work nobody has seen; this is a person ending work that is on their screen, and a chip that vanished under them would be the run denying it was ever asked for.

It is IDEMPOTENT, and cancelling a run that has already settled does nothing at all.

func (*Orchestrator) Charge

func (o *Orchestrator) Charge(dollars float64)

Charge bills the tank and fires the two things that can happen when it moves.

It is EXPORTED because the run's model calls do not all happen inside this package: the planner is the session's, and the law is that every model call anywhere in the run meters against one tank. A planner that did not call this would be spending money the gauge never sees.

func (*Orchestrator) Goal

func (o *Orchestrator) Goal() string

Goal is what the run was asked for, for a surface that has the run and not the sentence that started it.

func (*Orchestrator) Resolve

func (o *Orchestrator) Resolve(answer string) error

Resolve answers the fuel gate: "topup:<dollars>", "finish", or "stop".

It refuses an answer to a question nobody asked, and an answer to a question already answered, because both are a surface's bug and neither is something the run can act on quietly.

ONE STOP WORD EVERYWHERE. "stop" never travels down the gate's channel: it is Orchestrator.Cancel, because a run ended at the gate and a run ended from its page are one decision, and two settling paths for one decision is how two runs come to leave two different traces.

func (*Orchestrator) Run

func (o *Orchestrator) Run(ctx context.Context) (Snapshot, error)

Run walks the frontier until there is nothing left to walk, then synthesizes.

The error is the OPENING call's, and only ever that one: a run whose first planner call failed has no frontier and nothing to do, and there is no partial trace worth handing back. Every later failure — a node that broke, a planner that answered nonsense, a synthesis that would not come back — is a fact IN the snapshot rather than a reason to lose it.

func (*Orchestrator) Snapshot

func (o *Orchestrator) Snapshot() Snapshot

Snapshot returns the latest published state. It is safe for concurrent reads: what it hands back was built under the lock and is never written afterwards.

func (*Orchestrator) Steer

func (o *Orchestrator) Steer(text string)

Steer appends one line of the person's own instruction. It is carried on EVERY later View rather than delivered once: a planner call that arrived while the person was typing must not be the reason their correction is never seen, and steering outranks the plan.

func (*Orchestrator) Wait

func (o *Orchestrator) Wait()

Wait joins calls launched by this run, including names which may outlive its final snapshot. Call it only AFTER Run has returned and no caller can apply another amendment: that is the point after which no new calls are admitted. It does not change Run's prompt cancellation contract. Owners that must keep shutdown bounded apply their own grace around this join.

type Planner

type Planner interface {
	Plan(ctx context.Context, v View) (Amendment, error)
}

Planner thinks. It is called once at start and once per node completion, and it answers with an Amendment. NOOP is a first-class answer.

type Price

type Price struct {
	In  float64
	Out float64
}

Price is one model's list price in dollars per MILLION tokens, split the way every provider splits it. It is per million rather than per token because that is the number a person can check against the price page.

func PriceOf

func PriceOf(model string) (Price, bool)

PriceOf is one model's row, and whether anybody actually holds one. The id is matched the way the wire spells it, with suffixes an endpoint adds (":free", "@2026-01") cut before the lookup — and the cut, lowercased name is what the installed source is asked too, so a source never has to repeat this package's normalisation to agree with its own table.

A BARE NAME IS NOT A VENDOR'S ROW. A vendor serving a model on its own base does not charge the router's price for it, and a wrong price is worse than no price because it looks right.

type PriceSource added in v0.3.0

type PriceSource func(model string) (Price, bool)

PriceSource is a reader this package does not own, asked before the table falls back. It answers with the same pair PriceOf itself returns — one model's row in dollars per MILLION tokens — and false means "ask somebody else", never "free".

func CatalogPrices added in v0.3.0

func CatalogPrices(priceNow func(model string) (prompt, completion float64, known bool)) PriceSource

CatalogPrices adapts a reader that answers per TOKEN — the shape a model catalog publishes — into the per-MILLION PriceSource this package meters with, so the unit conversion exists in exactly one place. A reader that knows a model carries the answer straight through, a per-token zero included; a reader that publishes nothing for it answers false, and the table gets its turn. A nil reader adapts to a nil source: nobody published anything.

type Repairer

type Repairer interface {
	Repair(ctx context.Context, v View, why string) (Amendment, error)
}

Repairer is a Planner that can be shown its own refusal. It is optional: a planner that does not implement it simply loses the amendment it got wrong.

It exists because the two halves of "is this amendment legal" live in two places — the JSON in the model's caller, the frontier in this package — and only this side knows that cancelling a running node broke the commitment law. Handing that sentence back is one small call; re-planning from scratch on the next completion is not.

type Snapshot

type Snapshot struct {
	Goal   string       `json:"goal"`
	Nodes  []NodeStatus `json:"nodes"`
	Fuel   Fuel         `json:"fuel"`
	Notes  []string     `json:"notes,omitempty"` // planner notes, in order
	Steer  []string     `json:"steer,omitempty"` // user steering, in order
	Paused bool         `json:"paused"`          // out of fuel, awaiting the gate's answer
	Done   bool         `json:"done"`
	// Stopped says a PERSON ended this run early ([Orchestrator.Cancel]) rather
	// than the planner finishing it. Done is true beside it — the run is over
	// either way, and a surface waiting for one flag must not wait forever for
	// the other — and Answer is empty, because a run somebody stopped does not
	// go on to pay for a synthesis.
	Stopped bool   `json:"stopped,omitempty"`
	Answer  string `json:"answer,omitempty"` // the synthesis, once Done
	// Planner is the model the planner thinks with, carried from
	// [Options.Planner] and never read by this package. It is on the snapshot
	// because it is the one fact a surface cannot derive from the run: the
	// planner is an interface here, it is usually NOT the model the person is
	// talking to (the session resolves it through internal/roles), and it cuts
	// every node the tank pays for — so a gauge drawn without it says how much
	// is being spent and not whose judgement is spending it.
	Planner string `json:"planner,omitempty"`
}

Snapshot is the run rendered for a surface: the crystallized graph so far, the fuel gauge, the planner's notes, and whether the run is paused at the gate. The room draws this; the roster tree reads the parent/child shape of the run as a whole from the session.

type State

type State int

State is where one node is.

const (
	Queued State = iota // in the frontier, needs unmet
	Ready               // needs met, waiting for a slot
	Running
	Done
	Failed
	// Cancelled is a node a PERSON stopped: one that was in flight when the run
	// was cancelled and had its context cut under it, or one still pending that
	// will now never launch. It is deliberately not [Failed] — nothing about the
	// work went wrong and nobody made a finding about it — and a cancelled node
	// keeps no digest, because a half-answer handed on as a fact is worse than
	// no answer at all (run.go's [Orchestrator.Cancel]).
	Cancelled
)

type View

type View struct {
	Goal     string
	Results  []NodeStatus // completed nodes, digests only
	Frontier []NodeStatus // queued/ready/running
	Fuel     Fuel
	Steer    []string
}

View is what the planner sees on each call: the goal, condensed results, the frontier as it stands, the fuel gauge, and any steering the person typed since the last call. Digests only — the planner is the one big-context call in the system, and it stays small on purpose.

Jump to

Keyboard shortcuts

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