Skip to content

Latest commit

 

History

History
91 lines (69 loc) · 3.6 KB

File metadata and controls

91 lines (69 loc) · 3.6 KB

07 — Execution Model (linear, branch/merge, loop)

A harness is a graph, and the graph can run in three shapes. The first two are the graph's structure and run inside a single FlowRun; the third is time and lives one layer up.

1. Linear path

The default. Nodes run in order; each node's output is the next node's input.

[load-task] -> [start-env] -> [step-1] -> [verify]
const path = createFlowPath({ id, nodes, extractors, sink });
const run = await path.run(input);

Under the hood this is a modelgraph with no connections — a straight line.

2. Branch / merge

Pass the harness as nodes + edges. The edges ARE the connections: an edge from -> to feeds to with from's output. Several edges out of one node is a branch (its feeders run together); several edges into one node is a merge (the merge node receives the ordered list of its feeders' outputs).

                 /-> [Cheap Draft]   -\
[Context] -> ( branch )                ( merge ) -> [Select] -> [Verify]
                 \-> [Careful Draft] -/
const graph = createFlowGraph({ id, nodes, edges, extractors, sink });
const run = await graph.run(input);

The result is the same FlowRun as a linear path — path, trace, signals, accepted — so printFlowGraph, scorePath (Φ = Q / C), comparePaths, and findBottleneck all work unchanged. The run's path is the actual executed order (a merge node runs only after every feeder has finished).

This is the real point of branch/merge: which branch wins is a property of the graph, not of any one node. See ../labs/08-nonlinear-harness — with no hint the cheap-but-wrong branch wins on Φ and the run is rejected; once feedback supplies a hint the careful branch wins the merge and the run is accepted. Same nodes, different flow.

3. Loop / repeat (stays in the harness layer)

Repeating a run is time, not graph structure, so it does not belong in the core graph. The core rejects cycles. A bounded retry is composed on top with runWithFeedback, which routes the failed run's observation/error back into a new input:

const loop = await runWithFeedback({
  path: graph,            // a FlowPath or a FlowGraph
  input,
  retry: (lastRun, attempt) => /* build the next input, or undefined to stop */,
  maxAttempts: 3,
});

Keeping loops here keeps the primitive layer (modelgraph) barebones: it understands a single pass through a branch/merge graph and nothing about iteration, convergence, or saturation.

Why this matters: structure is a tuning knob

Once a harness is data (nodes + edges) rather than hand-wired control flow, the structure itself becomes something you can change and measure:

  • add a branch (try two strategies, keep the best by Φ)
  • add a merge (combine candidates)
  • add a node (e.g. a cache / reuse-observation step — see the off-by-default seam in ../labs/07-terminal-bench-live/lib/flow-build.ts)
  • wrap in a bounded loop

Each move is an edit to the spec, and each produces a FlowRun you can score.

Out of scope (next phase)

This document describes the substrate only. An automated harness optimizer — a catalog of structural moves (add cache / add branch / add retry), a Φ-ranked search over candidate specs, and held-out validation so a move has to generalize — is the next phase. See 08-self-improving-harness.md for what that loop is, how it maps onto these primitives against a real benchmark (Terminal-Bench / Harbor), and the honest constraints that keep it legitimate.