Architecture
The layers, module layout, and dependency graph for the alpha SDK — now built around the Harness primitive.
Little Workflow: published on npm as little-workflow@0.2.0-alpha.1 (2026-10-06). Sections labeled UNRELEASED describe source-only changes. Release status
The alpha SDK ships as a single @little-stack/workflow package. This milestone introduces the Harness as the agent-runtime primitive that unifies Planner, Orchestrator, Worker, and Fixer roles under one contract. The four architectural layers below reflect the new design.
The Four Layers
┌─────────────────────────────────────────────────────┐
│ Control Plane │
│ defineWorkflow() runWorkflow() │
│ WorkflowDefinition WorkflowDefinitionHash │
└────────────────────┬────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────┐
│ Harness Layer │
│ Harness contract workflowHarness (default) │
│ Roles: Planner · Orchestrator · Worker · Fixer │
│ harness.session.* / harness.model.* / harness.tool_call.* │
└────────────────────┬────────────────────────────────┘
│ spawns
┌────────────────────▼────────────────────────────────┐
│ Sub-Run Layer │
│ Orchestrator → plan_workflow / run_workflow │
│ Sub-workflows: own WorkflowVersion + event log │
│ Per-step LWIR scheduler (unchanged) │
└────────────────────┬────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────┐
│ Storage Layer │
│ LocalWorld (events/ artifacts/ locks/) │
│ Memory Stores (/mnt/memory/workflow|pipeline|org/) │
│ Run Scratch (/mnt/scratch/own|from-*/peer-steps/) │
└─────────────────────────────────────────────────────┘Control Plane
defineWorkflow() produces a WorkflowDefinition. A WorkflowDefinitionHash (new in this milestone — see Identity + Manifests) is computed once at authoring time: it is the sha256 of the definition's identity-bearing fields (planner config, models, schemas, tools, memory config). This is distinct from WorkflowVersion, which is the per-run LWIR hash (unchanged).
Two entry points start a run:
runWorkflow(workflow, input, options?)→ spawns a Planner harness session that drafts LWIR (a fixed-plan workflow's planner returns its plan without a model call), then executes steps via Worker harness sessions.runOrchestration({ workflows: [w1, w2, ...], orchestrator, input })(deprecated) → spawns an Orchestrator harness session that composes sub-workflow runs via tool calls (plan_workflow,run_workflow,start_workflow).
Harness Layer
Every agent role — Planner, Orchestrator, Worker, Fixer — lives in a Harness session. A harness is an agent runtime: model + bash + tools + memory/scratch mounts + skills + event recording. The SDK ships workflowHarness as the default; devs substitute via the same Harness contract.
type Harness = {
run(task: HarnessTask, ctx: HarnessContext): Promise<HarnessResult>;
};
type HarnessTask =
| { kind: "plan"; workflowSnapshot; input }
| { kind: "orchestrate"; available; input }
| { kind: "execute_step"; step; stepInput }
| { kind: "fix_step"; step; error; attempts };One contract, four task kinds. See Harness for the full spec.
Worker unification: the prior alpha had separate RuntimeAiAdapter, RuntimeCodeRunner, and tool-dispatch paths. All three are now unified through the worker harness. ai.generate steps make one model call with no tools; code.run steps validate and compile, but the default worker harness refuses to execute them (it checks the step's sandbox policy and then throws), so running one needs a custom worker.harness that implements execute_step; tool.call steps invoke the named tool directly. The harness emits the same event vocabulary for all.
Sub-Run Layer
When the Orchestrator calls run_workflow, the SDK creates a sub-run with its own runId, its own event log, and its own per-step LWIR scheduler (unchanged). Sub-runs are fully independent for durability: a sub-run crash resumes via its own event log without touching the orchestrator's session.
The orchestrator's harness records harness.tool_call.started with args.subRunId and harness.tool_call.succeeded with result.runId. Two levels of memoization compose cleanly — the orchestrator's session memoizes completed tool calls; each sub-run memoizes completed LWIR steps.
See Runtime Execution and Replay for how both levels interact.
Storage Layer
Three storage surfaces, all filesystem-backed in alpha (localWorld):
| Surface | Path | Scope | Notes |
|---|---|---|---|
| Event log | <dataDir>/events/events.db | Per run id | SQLite/WAL, append-only event rows |
| Artifacts | <dataDir>/artifacts/ | Per run | Content-addressed blobs |
| Memory Stores | <dataDir>/memory/workflows/<id>/, pipelines/<key>/, org/ | Cross-run | Mounted at /mnt/memory/ inside harness sessions |
| Run Scratch | <dataDir>/runs/<runId>/scratch/ (hierarchical) | Per run | Mounted at /mnt/scratch/ inside harness sessions |
Memory Stores persist across runs and accumulate lessons. Run Scratch is ephemeral — orchestrator writes for planners, planners write for workers — and expires with the run. See Memory + Scratch.
Package Layout
packages/little-workflow/src/
authoring.ts defineWorkflow(), runWorkflow(), runOrchestration(), model(), skill(), types
fixed-plan.ts defineWorkflow({ plan }) — the fixed-plan planner harness
tool-registry.ts createToolRegistry() — AI SDK tool({...}) as the primitive
schema.ts schema + output-mode normalization (JSON Schema / Zod)
canonical.ts canonical JSON + sha256 digest
lwir.ts LWIR types, Ajv validator, structural + expression checks
expressions.ts deterministic {{ ... }} expression language
compiler.ts compile pipeline (WorkflowDefinitionSnapshot → LWIR → WorkflowVersion)
compiler-lock.ts WorkflowVersion + validation hashes
harness/
types.ts re-exports Harness, HarnessTask, HarnessContext, HarnessResult, event types (defined in little-harness)
event-recorder.ts HarnessEventRecorder — writes harness.session.*, harness.model.*, harness.tool_call.* events
memory.ts Memory Store mount construction + path enforcement
scratch.ts Run Scratch mount construction + hierarchy
skills.ts Skill loading + system-prompt injection
orchestrator.ts plan_workflow / run_workflow / start_workflow tool implementations
run-state.ts materializeRunStateFromEvents — folds event stream into MaterializedRunState
world.ts Local World: events, artifacts, manifests, locks
outer-loop.ts outer-loop scheduler + manifest read/write (Layer B)
runtime.ts LWIR step scheduler + worker harness dispatch
replay.ts replayRun() helper
model-registry.ts bundled model registry (pricing, capabilities, context window)
pricing.ts the single tokens -> USD site (priceModelCall, UsageTotals)
run-report.ts per-step tokens / cache-hit share / dollars from run state
cli-core.ts little validate | run | events | replay | report
cli.ts process.argv entry
index.ts the root entry: an explicit export list (src/index.test.ts pins it)
entries/ the /lwir, /eval and /internals entry pointsModule Dependency Graph
Arrows point from importer to importee. The Harness layer sits between the control plane and the runtime/world layers.
Responsibility Boundaries
| Module | Owns | Does Not Own |
|---|---|---|
authoring.ts | WorkflowDefinition, WorkflowDefinitionHash computation, model() factory + slot-id derivation, skill() loader, public re-exports | Validation, hashing, execution |
tool-registry.ts | createToolRegistry(), AI SDK tool({...}) storage, ToolRegistrySnapshot for manifests, MCP attachment | Schema validation, hashing |
schema.ts | Schema + output-mode normalization. Zod / JSON Schema → NormalizedSchemaDescriptor | Hashing (uses canonical.ts), validation |
canonical.ts | Canonical JSON encoding + sha256: digests | Anything semantic |
lwir.ts | LWIR types, structural validator, expression presence checks, alpha JSON Schema subset | Request-binding (compiler), execution (runtime) |
expressions.ts | Parses and evaluates {{ ... }} expressions | Side effects, schema validation, async |
compiler.ts | Compile pipeline: normalize → snapshot → planner harness session → validate → register | Step execution, persistence |
compiler-lock.ts | WorkflowVersion + validationHash over a stable lock seed | Calling Ajv |
harness/types.ts | All Harness types: Harness, HarnessTask, HarnessContext, HarnessResult, all event types | Implementation |
packages/little-harness/src/workflow-harness/workflow-harness.ts | Default harness: AI SDK streamText / generateText agent loop, bash tool, cwd-scoping | Workflow domain knowledge; it only sees ctx.* |
harness/event-recorder.ts | HarnessEventRecorder — emits harness.session.*, harness.model.*, harness.tool_call.* events | Materializing state |
memory.ts | Memory Store mount construction, path allowlist enforcement, lazy dir creation | Cross-run memory consolidation (dreaming) |
scratch.ts | Run Scratch mount hierarchy construction, cleanup policy stub | Memory (different lifecycle) |
skills.ts | Skill loading from directory + single-file forms, system-prompt frontmatter injection | Skill authoring, discovery |
orchestrator.ts | plan_workflow / run_workflow / start_workflow tool implementations; sub-run spawning + concurrency cap | Harness agent loop (delegates to workflowHarness) |
world.ts | On-disk store: SQLite/WAL event rows, artifact manifests + blobs, run locks, outer-loop manifests, sha256 verification, durable writes | Knowing what a step means |
outer-loop.ts | Outer-loop scheduler: per-cycle compile-and-run, supervise() dispatch, OuterLoopCycleCompleted | Individual step execution |
run-state.ts | Folding an event stream into MaterializedRunState. Pure and replay-safe | I/O |
runtime.ts | LWIR step scheduler: registering work, dispatching worker harness, retries, cancellation, parallel branches, run finalization | Persistence (world.ts), planning (compiler.ts), orchestration (orchestrator.ts) |
replay.ts | Thin replay wrapper returning ReplayPlanStep[] + hydrated artifacts | New computation |
model-registry.ts | Bundled model registry JSON + getModelInfoFor(providerId, modelId). Pricing + capabilities for known models | Live sync (post-alpha) |
pricing.ts | Turning recorded tokens + model identity into USD, and folding calls into UsageTotals. The only place cost is computed | Reading a cost off an event — events are token-only |
run-report.ts | Per-step and run-total tokens, cache-hit share, and dollars; JSON and table rendering | Pre-run cost estimation (not implemented) |
cli-core.ts | The little command surface — init, add, validate, run, test, events, replay, report | Loading TS modules; no harness is wired here |
Identity At Each Scope
Three hashes give auditable identity at three different scopes:
| Hash | Computed when | Covers |
|---|---|---|
WorkflowDefinitionHash | defineWorkflow() | id, schemas, planner config, models, tools, memory config |
WorkflowVersion (= LWIR hash) | Planner emits LWIR | The complete LWIR graph — per run |
ManifestHash | Each harness session starts | The session's capability surface (model, harness, skills, tools, memory stores) |
See Identity + Manifests for drift detection details.
Why One Package
Alpha keeps modules in one package: the wiring is dense and the surface is small. Splitting points are already drawn — harness/ could become @little-workflow/harness, world.ts could become @little-workflow/world-postgres. Those splits happen post-alpha when the boundaries are validated against production load.