STAGE 1 / MILESTONE 6

Harness Platform M6: making the agent runtime observable by construction

How M6 adds a one-request agent runtime with streaming model compatibility, append-before-yield events, typed steering and cancellation, and explicit self-hosting limits.

01 / MILESTONE CONTRACT

M6 makes the smallest agent run observable by construction.

Harness Platform already had a deterministic agent loop, policy, tools, sessions, and a growing control plane. M6 starts a compatibility-first migration toward a Pi-like kernel: one runtime contract owns model invocation, message state, event publication, steering, and cancellation—without pretending the self-hosting journey is finished.

The milestone deliberately proves one successful, text-only request. That narrow slice is where ordering promises can be made precise: a caller knows the run identity before consumption begins; every event is stored before it is seen; the consumer controls when the model may advance; and late control requests fail with typed semantics instead of silently becoming orphaned intent.

Stage 1 / Milestone 6 contract (condensed)text · source
M6 — Runtime contracts and event vocabulary

ship
├── AgentRuntime.run() → AsyncIterable<AgentEvent>
├── AgentRuntime.steer() and cancel()
├── streaming ModelAdapter
├── narrow injected EventStore
├── compatibility-target Tool and Workspace interfaces
├── MinimalAgentRuntime
└── CompleteModelAdapter for the existing completion API

prove
├── one successful text-only model request
├── deterministic event order
├── durable append before consumer visibility
├── consumer-driven backpressure
├── typed steering and cancellation lifecycle
└── additive coexistence with the M0 loop

defer
├── policy-gated multi-round tool loop
├── tool execution and workspace wiring
├── context-compaction behavior
├── durable store implementation and restart replay
├── live provider proof
└── self-hosted /doctor task

02 / ROADMAP BOUNDARY

The roadmap was decomposed before the implementation was allowed to grow.

The development record first explored a larger minimal kernel: model invocation, messages, a tool loop, policy, steering, compaction, persistence, and eventually a self-hosted task. That is too much semantic surface for one credible milestone. The work was split into M6 through M12 so each boundary can earn its own tests and release record.

M6–M12 implementation sequencetext
M6  runtime contracts + event vocabulary      complete
M7  deterministic multi-round session loop   planned
M8  bounded workspace + five tools           planned
M9  steering, follow-ups + compaction         planned
M10 durable replay + restart                  planned
M11 offline kernel-backed self-host runner    planned
M12 live self-hosted Harness doctor           planned

Only M6 in that sequence is public and complete. M7 through M12 remain roadmap intent until each has its own implementation and evidence. The public M6 task, pull request, source, and checks are the authority for this note.

03 / KERNEL OWNERSHIP

The runtime owns orchestration, then injects every effectful boundary.

MinimalAgentRuntime owns the caller-known run, session, and turn identities; a snapshot of caller input; the in-memory message view used for the request; event validation and publication; and the active/terminal lifecycle. Model behavior and event persistence enter through narrow ports.

Policy, provider credentials, concrete workspace operations, scheduling, UI, and side-effect enforcement remain outside. Tool and operational Workspace are compatibility targets in M6, not a route by which the model can touch the host.

M6 draws a narrow kernel boundaryM6 gives the runtime one narrow job: admit and snapshot a run, assemble its message context, make one text-only model request, and publish its lifecycle. Models and event storage remain injected ports; policy, tools, workspaces, credentials, and durable infrastructure remain outside.
M6 public runtime ports (condensed TypeScript)typescript · source
interface AgentRuntime {
  run(input: RunInput): AsyncIterable<AgentEvent>
  steer(runId: string, content: string): Promise<void>
  cancel(runId: string): Promise<void>
}

interface ModelAdapter {
  stream(request: ModelRequest): AsyncIterable<ModelEvent>
}

interface EventStore {
  append(event: AgentEvent): Promise<void>
  readSession(sessionId: string): AsyncIterable<AgentEvent>
}

04 / EVENT VOCABULARY

The completed message is replay truth; deltas are delivery detail.

A successful run produces a deterministic boundary order. Assistant deltas may be absent, but the completed assistant message is always present. That distinction matters because a completion-only provider cannot supply genuine token chunks, while a future replay still needs one authoritative message.

Canonical successful M6 event ordertext · source
turn.started
message.completed  role=user
model.request
message.delta      role=assistant · sequence=0..n-1 · zero or more
model.response
message.completed  role=assistant
turn.completed     status=completed

The event package adds strict schemas for turn start, message delta/completion, queued steering, context compaction, and turn completion. Existing model and tool event names remain canonical; M6 does not introduce competing synonyms. The envelope stays at version one so current readers retain compatibility.

Additive event vocabularytext · source
new in M6
├── turn.started
├── message.delta
├── message.completed
├── steering.queued
├── context.compacted
└── turn.completed

reused without synonyms
├── model.request
├── model.response
├── tool.call
├── policy.decision
└── tool.result

canonical envelope remains v: 1

05 / DURABILITY ORDER

An event cannot become observable before its append succeeds.

The important M6 invariant is not merely that events are eventually stored. The per-run writer serializes normal production with external steering and cancellation, awaits the injected store, and only then places the event at the consumer boundary. This makes the visible stream a prefix of the accepted evidence rather than a faster, less trustworthy side channel.

Admission is the deliberate exception to demand-driven production: the runtime eagerly appends and queues turn.started plus the completed user message. After the consumer accepts that pair, it appends model.request and waits for the caller to consume that boundary before invoking the model. Consumer-driven model backpressure begins after those durable admission boundaries.

Append before yield makes evidence part of control flowThe runtime eagerly appends and queues the turn-start and user-message admission pair. After the consumer acknowledges that pair, the runtime appends model.request and waits for the consumer to acknowledge it before invoking the model. Provider output is then mapped into canonical runtime boundaries; every such boundary is appended before yield.
Append-before-yield and failure protocoltext · source
eager admission
  1. append + queue turn.started
  2. append + queue the completed user message
  3. wait until the user message is consumed
  4. append + queue model.request
  5. wait until model.request is consumed, then invoke the model

post-admission model boundaries
  1. pull provider output only after consumer demand
  2. map text.delta → one message.delta boundary
     or response.completed → model.response, assistant message, turn completion
  3. for each canonical runtime boundary, await EventStore.append(event)
  4. only after that append succeeds, yield the runtime event

if append fails
  → poison the writer
  → trigger AbortSignal + best-effort iterator close if model was invoked
  → wake the consumer with EventAppendError
  → emit no later observable boundary

Backpressure follows the same discipline. Advancing the async iterator permits one event to reach the caller and, when appropriate, one more pull from the model. Tests assert that a stalled consumer does not let the producer race ahead. A provider text.delta becomes one message.delta; provider response.completed is never persisted or yielded itself, but drives the separately persisted model.response, completed assistant message, and turn completion. If persistence fails, the writer is poisoned, the consumer receives a typed append error, and no later boundary is exposed. If the model has already been invoked, its abort signal is triggered; an admission failure occurs before there is a provider request to cancel.

06 / MODEL COMPATIBILITY

Streaming arrives without deleting the completion-only model API.

M6 adds ModelAdapter.stream() with only two provider-side event forms: text.delta and response.completed. The existing Model.complete() contract remains available. CompleteModelAdapter bridges it by emitting one terminal response; it does not fabricate a token stream that the provider never produced.

Completion and streaming compatibility boundarytext · source
legacy path                         streaming path
─────────────────────────────────  ─────────────────────────────────
Model.complete(request)             ModelAdapter.stream(request)
        │                                   │
        └── CompleteModelAdapter ───────────┘

completion-only providers emit
└── one response.completed event

stream normalization
├── discard empty text chunks
├── split chunks larger than 1 MiB
├── preserve concatenated text exactly
└── cap the completed message at 16 MiB

The deterministic fake model implements both paths. Empty chunks disappear, chunks over one MiB are split without changing their concatenation, and the final text remains bounded by the completed-message schema. For M6, a valid terminal response is successful text, no tool calls, and finishReason: "stop".

07 / STEERING AND CANCELLATION

Controls linearize against one explicit request boundary.

run() synchronously registers the run and snapshots input before returning its async iterable. That lets a caller use its own run ID to steer or cancel even before the first next(). Early steering is appended before steer()resolves and joins the sole model request. Steering after that boundary receives SteeringClosedError; the runtime never acknowledges intent it cannot apply.

Steering and cancellation close around one request boundaryThe runtime registers an active run before asynchronous production, accepts steering only before its sole model boundary, and funnels active cancellation or iterator abandonment into one attempted canceled terminal append. A failed terminal append rejects with a typed error and leaves a failed tombstone.
M6 lifecycle semanticstext · source
steering
├── before the sole model boundary
│   └── append steering.queued before steer() resolves
│       and include it in that request
└── after the model boundary
    └── reject with SteeringClosedError; never orphan intent

cancellation
├── active run → coalesce concurrent requests
├── repeated cancel after canceled → no-op
├── iterator return()/throw() → abandonment becomes cancellation
├── forward AbortSignal to the model adapter
├── terminal append failure → EventAppendError + failed tombstone
└── retain a small terminal tombstone for typed later controls

Cancellation coalesces while active. A consumer that calls return() or throw() is treated as abandoning the run, so the runtime still attempts to persist a canceled terminal event even when that event will not be observed by the same iterator. Small terminal tombstones retain just enough identity to distinguish duplicate, missing, and already-terminal controls.

08 / ADDITIVE MIGRATION

The old loop and the new contract coexist on purpose.

Replacing a working loop while redefining its persistence and lifecycle semantics would turn every regression into an attribution problem. M6 instead exports the new runtime beside runAgent(), keeps Model.complete(), and adapts legacy models through one named bridge. No dependency or lockfile changes were needed.

Compatibility now, self-hosting in later milestonesM6 adds a streaming runtime beside the existing surface instead of replacing it. A completion-only model can cross the new boundary through CompleteModelAdapter; the tool loop, bounded workspace, compaction, replay, and self-hosted runner stay assigned to M7–M12.

This creates a migration seam: existing callers keep the M0 path; new focused tests can harden the M6 path; M7 can add the deterministic tool loop without smuggling workspace or provider behavior into this milestone. Compatibility is a temporary architecture strategy, not a claim that both paths should exist forever.

09 / MACHINE-READABLE SCOPE

The task admitted runtime contracts, events, model adaptation, and their proof.

The canonical manifest limited changes to three packages, two architecture documents, and the task itself. Network and push were denied; tests remained offline; delivery was a pull request. That scope prevented the contract milestone from absorbing provider setup, storage deployment, or future tool implementations.

M6 task manifest (abridged)yaml · source
id: m6-kernel-contracts
goal: add the compatibility-first runtime, streaming, event, and lifecycle contracts

allowed_paths:
  - packages/events/**
  - packages/kernel/**
  - packages/models/**
  - ARCHITECTURE.md
  - EVENTS.md
  - tasks/m6-kernel-contracts.yaml

permissions:
  network: deny
  git.push: deny

delivery:
  type: pull_request

Pull request #5 confirms the gate: 15 paths before tests, the same 15 paths afterward, and zero policy violations. The public diff contains the manifest, docs, event schemas and tests, model contracts and tests, and the runtime plus its focused test file—nothing outside the allowed surface.

10 / CHECKS-GATED DELIVERY

Three implementation commits crossed one verified pull request.

The release first introduced streaming model contracts, then corrected delta normalization, then added the minimal runtime and event vocabulary. The final head 6de0fd7 passed the manifest exit gate and CodeQL before pull request #5 merged as 98924a6.

Authoritative M6 release boundarytext · source
base          4bf5f68701dee38eecdc0830c4f1be0d937d3942
PR head       6de0fd70086c7a70c69e07da863cf7677b479f22
merge         98924a66628bc66a88093ec6bee05f426f0fea9d

pull request #5
├── 15 changed files
├── 2,546 insertions
├── 12 deletions
├── 3 implementation commits
└── no dependency or lockfile change
15changed files
2,546insertions
12deletions
3implementation commits

11 / VERIFIED RESULT

The tests attack ordering and lifecycle—not throughput.

PR-head CI passed 568 offline tests across 39 files, strict TypeScript, one golden scenario, and the M6 manifest gate. The report recorded run-report/v2, all 15 paths before and after tests, zero violations, and a successful report write. The exact merge then passed separate main-branch CI and CodeQL.

For publication, I also checked out the exact merge in a separate temporary clone under Node 24.18.0 and reproduced 568/568 tests, strict TypeScript, and the golden scenario. That independent run verifies the pin used by this article; it does not create a new Harness release or a live infrastructure test.

M6 verification ledgertext
public PR-head evidence · GitHub Actions 33449593023
├── strict TypeScript                         passed
├── test files                               39 / 39
├── offline tests                            568 / 568
├── golden scenarios                          1 / 1
├── allowed paths before and after tests     15
├── path-policy violations                    0
├── run-report/v2                            passed
└── PR-head checks                            4 / 4 green

post-merge evidence · exact merge 98924a6
├── CI 33449750084                           passed
└── CodeQL 33449749513                       passed

independent publication audit · exact merge · Node 24.18.0
├── clean-checkout tests                     568 / 568
├── strict TypeScript                        passed
└── golden scenarios                          1 / 1
568 / 568offline tests
39 / 39test files
1 / 1golden scenarios
0path violations

The new diff adds 33 focused cases: 15 runtime cases, six event-schema cases, seven fake streaming-model cases, four completion-adapter cases, and one delta-normalization case. They cover exact event order, append failure, one model pull per consumer advance, steering, cancellation, iterator abandonment, input snapshots, strict variants, size normalization, and typed invalid, duplicate, missing, and terminal controls.

12 / EVIDENCE ARTIFACT

The retained report attests the PR head, not the merge commit.

PR workflow 33449593023 uploaded gate-evidence-33449593023. Its JSON report pins head 6de0fd7, base 4bf5f68, the passing tests, 15-path pre/post policy snapshots, zero violations, seven serialized report events, and reportWritten: true.

That artifact expires on November 29, 2026; it is uploaded CI evidence, not a permanent Git artifact. Its SQLite file has five persisted events and still marks the session active, while the JSON includes the final delivered and run-recorded events. This note therefore does not describe the SQLite file as a closed terminal log. Separate post-merge workflows attest the exact merge commit.

13 / CURRENT TRUTH

M6 has strong local semantics and deliberately incomplete system behavior.

SurfaceWhat the evidence supportsWhat remains open
Runtime scopeMinimalAgentRuntime owns one text-only model request, message/context state, canonical event publication, and lifecycle controls.It does not run tools, make policy decisions, execute a multi-round loop, or replace runAgent().
Durability orderEvery observable event is appended through a serialized per-run writer before it can be yielded.M6 injects an EventStore interface; it does not add a production database, restart continuation, or replay engine.
StreamingConsumer advancement bounds both event delivery and the next pull from the model adapter.The completion adapter cannot invent provider deltas, and no live provider was exercised.
SteeringSteering durably linearized before the sole request joins that request; later steering receives a typed rejection.Follow-up turns and steering between multiple model rounds remain future work.
CancellationActive cancellation coalesces, abandonment attempts one canceled terminal append, and AbortSignal is forwarded at the model boundary when invoked.A terminal append can fail with EventAppendError and a failed tombstone; a remote provider may also ignore the signal.
Event vocabularyM6 validates new turn, message, steering, compaction, and terminal variants while preserving the existing envelope and tool-event names.context.compacted is a schema contract only; M6 does not choose, generate, or apply summaries.
VerificationThe exact merge passes 568 offline tests, strict type checking, one golden scenario, CI, and CodeQL.The counts and whole-suite duration are correctness evidence, not load, latency, capacity, or production-readiness evidence.
Delivery reviewThe author merged after automated CI and CodeQL checks passed on the PR head; the exact merge passed post-merge checks.There was no approving review, so the accurate label is checks-gated and author-merged—not peer-reviewed.

The accurate description is a compatibility-first runtime contract for one text-only request. It is not self-hosting, an autonomous tool runner, durable restart recovery, a production event store, a provider integration, or a load-tested service.

14 / FILE GUIDE

The runtime, vocabulary, adapters, and proof each have an inspectable home.

15 / WHAT IS NEXT

The next milestone should spend these semantics on a real tool loop.

  1. 01

    M7 — deterministic session loop

    Add multiple model rounds, canonical tool requests, durable policy intent before side effects, tool results, and bounded terminal conditions without weakening append-before-yield.

  2. 02

    M8 — bounded workspace and five tools

    Wire only fs.read, fs.list, fs.write, process.exec, and git.diff through explicit policy and workspace boundaries.

  3. 03

    M9–M10 — continuity

    Generalize steering and follow-ups, implement compaction, then prove replay and restart from durable evidence rather than in-memory state.

  4. 04

    M11–M12 — self-hosting proof

    Run an offline task through the new kernel, then complete a live self-hosted Harness doctor task with retained evidence and explicit provider/infrastructure gates.

16 / EVIDENCE LEDGER

Every shipped claim resolves to the public merge, task, pull request, or check run.

  • M6 merge 98924a6 — the public source pin for the runtime, events, model adapters, tests, and docs.
  • Pull request #5 — M6 runtime contracts and event vocabulary — authoritative 15-file diff, implementation commits, checks, and merge chronology.
  • M6 task contract and architecture record — acceptance, ownership, compatibility, lifecycle, and explicit deferrals.
  • PR-head CI and gate evidence and PR CodeQL — 568 tests, strict types, one golden scenario, 15-path policy evidence, and four green checks on 6de0fd7.
  • Exact-merge CI and CodeQL — separate successful checks on 98924a6 after merge.
  • Publication audit — a clean temporary checkout of the exact merge reproduced 568/568 offline tests, strict TypeScript, and 1/1 golden scenario. The private development conversation supplied chronology and roadmap intent only; it is intentionally not published as implementation evidence.

CONTINUE EXPLORING

Inspect the minimal runtime contract—and the self-hosting work it leaves open.

The pinned public merge contains the compatibility-first runtime, streaming model adapter, canonical event vocabulary, append-before-yield semantics, lifecycle controls, and offline proof described here.