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.
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 task02 / 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 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 plannedOnly 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.
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.
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=completedThe 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.
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: 105 / 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.
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 boundaryBackpressure 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.
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 MiBThe 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
├── 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 controlsCancellation 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.
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.
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_requestPull 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.
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 change11 / 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.
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 / 1The 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.
| Surface | What the evidence supports | What remains open |
|---|---|---|
| Runtime scope | MinimalAgentRuntime 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 order | Every 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. |
| Streaming | Consumer 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. |
| Steering | Steering 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. |
| Cancellation | Active 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 vocabulary | M6 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. |
| Verification | The 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 review | The 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.
Runtime and architecture
- packages/kernel/src/runtime.ts — public contracts, MinimalAgentRuntime, serialized writer, stream, controls, and typed runtime errors.
- ARCHITECTURE.md — ownership, compatibility, persistence, streaming, and lifecycle boundaries.
- tasks/m6-kernel-contracts.yaml — acceptance criteria, allowed paths, permissions, budgets, and PR delivery.
Events and replay truth
- packages/events/src/schemas.ts — strict event variants and bounded message, delta, steering, and compaction data.
- EVENTS.md — canonical successful order, append-before-yield rule, and replay guidance.
- packages/events/test/events.test.ts — serialization stability and invalid-variant coverage.
Model boundary
- packages/models/src/model.ts — shared request and bounded streaming event contracts.
- packages/models/src/model-adapter.ts — completion-to-stream compatibility adapter.
- packages/models/src/fake-model.ts — deterministic completion and streaming fake with normalized chunks.
Behavioral proof
- packages/kernel/test/runtime.test.ts — ordering, backpressure, failure, steering, cancellation, abandonment, snapshots, and typed-control cases.
- packages/models/test/model-adapter.test.ts — request forwarding, completion adaptation, failure, and cancellation coverage.
- Pull request #5 — authoritative diff, three-commit history, checks, and merge record.
15 / WHAT IS NEXT
The next milestone should spend these semantics on a real tool loop.
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
98924a6after 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.