STAGE 1 / MILESTONE 7
Harness Platform M7: putting durable policy before tool execution
How M7 adds a deterministic multi-round session loop with versioned context, strict tool admission, durable policy-before-effect ordering, hard budgets, cancellation, and timeouts.
01 / MILESTONE CONTRACT
M7 turns one durable request into a loop that can reason, act, observe, and continue.
M6 established the narrow runtime boundary: append events before exposing them, let the consumer control progress, and keep lifecycle outcomes typed. M7 spends those semantics on the first complete agent turn—multiple model rounds joined by strictly admitted, policy-fenced pure-tool calls and immutable observations.
The milestone is deliberately smaller than “an autonomous coding agent.” It proves the control loop itself. A model can request a registered pure function, the runtime can validate and authorize it, its result can enter the next context revision, and the model can finish—all without giving model output a direct path to files, processes, the network, or a live provider.
M7 — Deterministic minimal session loop
ship
├── one turn with repeated model / pure-tool rounds
├── versioned, immutable message and context snapshots
├── strict model-stream and tool-argument validation
├── tool intent → durable policy → optional permission → execution
├── hard step, cumulative-token, and requested-tool-call budgets
├── per-round model deadlines and cooperative cancellation
└── one terminal-finalization path
preserve
├── append-before-yield and consumer backpressure from M6
├── legacy runAgent()
├── legacy Model.complete() through CompleteModelAdapter
└── additive event decoding
defer
├── operational workspace, file, process, network, or MCP tools
├── live provider evidence
├── multi-turn follow-ups and mid-loop steering
├── context compaction and restart replay
├── exactly-once external effects
└── a CLI or service that self-hosts on this runtime02 / MULTI-ROUND LOOP
A tool result is not an endpoint; it becomes the next model observation.
The canonical test uses two model requests. The first asks for a pure tool. The runtime records the assistant intention, validates and fences the call, executes only after an allowed decision is durable, stores the result, and appends a typed tool message. The second request receives that observation and produces the final answer.
agent.started
turn.started
message.completed role=user revision=1
model.request contextVersion=1 messageRevision=1
model.response finishReason=tool_calls
message.completed role=assistant revision=2
tool.call durable intent · no execution yet
policy.decision allow | ask | deny
permission.requested # ask only
permission.resolved # ask only
tool.result after allowed pure execution
message.completed role=tool revision=3
model.request contextVersion=1 messageRevision=3
message.delta role=assistant · 0..n
model.response finishReason=stop
message.completed role=assistant revision=4
turn.completed
agent.stoppedZero or more assistant deltas may appear before each completed response. Calls are processed sequentially, not in parallel. agent.started and agent.stopped bookend exactly one turn.started and one turn.completed outcome when terminal appends succeed. A text-only answer is the same structure with the tool segment omitted.
03 / VERSIONED CONTEXT
Every request can name the exact immutable message history it consumed.
M7 introduces explicit version-one message state and model context. A prior context starts at a revision equal to its validated message count. Appending the new user message, assistant intention, tool observation, and final assistant response advances the revision one step at a time. Each model.request records both contextVersion and messageRevision.
type VersionedMessageState = Readonly<{
version: 1
revision: number
messages: readonly ChatMessage[]
}>
type VersionedModelContext = Readonly<{
version: 1
messageRevision: number
messages: readonly ChatMessage[]
tools: readonly ToolDefinition[]
}>
appendMessage(state, message)
→ clones + validates the current snapshot
→ appends a detached frozen message
→ returns revision + 1
buildModelContext(state, tools)
→ returns a detached frozen request snapshot
→ records the exact messageRevision consumedThese are detached deep snapshots, not frozen wrappers around caller-owned references. The normalizer rejects getters and setters, symbols, cycles, named properties on arrays, non-finite numbers, excessive depth, excessive node counts, and values over their byte bounds. That hardening prevents a model, consumer, permission callback, or tool from mutating the data that a later boundary believes it authorized.
04 / MODEL STREAM INTEGRITY
Provider output must agree with itself before it can become an executable intention.
The model protocol now has three frames: text delta, complete tool call, and completed response. The runtime aggregates text, buffers tool intentions, and cross-checks both against the terminal response. It rejects a missing terminal, text after tool calls, mismatched content, mismatched call arrays, reused IDs, impossible finish reasons, malformed usage, accessor-backed values, and oversized structures. The first response.completed ends model pulling, so a provider frame queued after it is not inspected or described as a duplicate-terminal rejection.
valid provider stream
├── text.delta* nonempty · bounded · before tool calls
├── tool.call* complete ordered intentions
└── response.completed first terminal frame ends model pulling
cross-check before canonical tool.call
├── concatenated deltas === completed content, when deltas exist
├── streamed calls === terminal calls by id, name, and JSON value
├── finishReason agrees with tool presence
├── usage counters are safe, finite, and internally consistent
├── call IDs are unique across the turn
└── objects are bounded ordinary data with no accessors or exotic fields
failure
└── RUNTIME_MODEL_STREAM_INVALID → failed terminal path
· no effect from that malformed roundThe existing completion API remains usable. CompleteModelAdapter emits each already-complete tool intention followed by one terminal response; it does not fabricate token deltas. Native adapters keep their real stream. The entire M7 verification lane uses FakeModel or local deterministic adapters—never a production model endpoint.
05 / TOOL ADMISSION
Unknown and invalid calls become observations, never authorization questions.
The canonical tool.call event means “the model requested this intent,” not “the runtime already executed it.” After that intent is durable, the runtime resolves the registered definition and validates a detached argument snapshot. Only a known call with valid input can reach policy.
for each requested tool intention
1. persist tool.call
2. find the registered tool
3. validate a detached JSON argument snapshot
unknown name
→ tool.result { ok: false, code: "TOOL_NOT_FOUND" }
→ tool observation enters context
→ no policy derivation · no permission · no execution
invalid input
→ tool.result { ok: false, code: "TOOL_BAD_INPUT" }
→ tool observation enters context
→ no policy derivation · no permission · no executionA typed failed result is intentionally returned to the conversation. The model may recover in a later round instead of losing the entire turn. Tool exceptions and invalid tool outputs follow the same observation pattern with their own error codes, while an unpersisted intent cannot progress at all.
06 / DURABLE EXECUTION FENCE
The intent and the authorization record exist before the effect begins.
M7 extends M6's append-before-yield discipline across the tool boundary. It stores the requested call, validates input, lets the tool derive its authorization intent, requires the trusted pure marker, and then chooses the built-in or injected decision. It stores policy.decision and any interactive permission resolution before invoking the implementation. If any pre-effect append fails, execution is prevented.
durable tool.call
│
▼
validate registered tool + immutable input
│
▼
derive authorization intent
│
▼
require trusted pure boundary · choose allow | ask | deny
│
▼
durable policy.decision
│
├── deny ────────────────→ failed tool.result observation
│
└── ask → durable permission.requested
→ wait for resolver
→ durable permission.resolved
│
▼ allowed only
execute trusted in-process pure tool
│
▼
durable tool.result → durable tool message → next model context07 / PERMISSION SEMANTICS
Allow, ask, and deny remain explicit even when the tool is pure.
A valid pure call can be allowed, denied, or paused for a resolver. When no permission controller is supplied, a registry-marked pure tool receives the built-in runtime.m7.pure allow decision. An ask path persists the request before waiting and persists its resolution before executing. A hard deny cannot be overridden. A missing or failed resolver fails closed. Resolver decisions are attributed to the operator; synthetic denials remain attributed to the kernel.
allow
└── execute only after policy.decision append succeeds
ask
├── persist permission.requested
├── pause without executing
├── resolver allow → persist operator resolution → execute
└── resolver missing, failed, canceled, or deny
→ persist kernel/operator denial when possible → do not execute
run-scoped grant
├── key = action + subject
├── may satisfy a later matching ask in the same run
└── never overrides a later hard deny
no PermissionController
└── a registered pure tool is allowed by runtime.m7.pureAn allowed ask may create a run-scoped grant keyed by action and subject, so a later matching call does not ask twice. The cache is deliberately smaller than a global approval: it ends with the run, and a later hard deny still wins.
08 / HARD BUDGETS
The loop stops at model-round, reported-token, and requested-call boundaries.
Runtime budgets are part of execution control, not advisory telemetry. Steps are checked before a model request; requested tool intentions count even when unknown or invalid; and prompt plus completion usage accumulates across rounds. Warnings are durable and normally appear once per metric after the 50-percent threshold.
runtime budget behavior
────────────────────────────────── ───────────────────────────────────────────
maxSteps (default 8) checked before each model request
maxModelTokens (optional) cumulative prompt + completion usage
maxToolCalls (optional) counts requested intentions, even invalid
warning threshold once per metric at 50% unless forced
over-limit tool intention tool.call stays durable; no policy/effect
provider token overshoot response recorded; no next model/tool unit
terminal status budget_exceeded
task manifest authoring budget 100,000 model tokens · 200 tool calls
governs development, not runtime defaultsThe remaining token allowance is passed to the next request, but the provider reports actual usage only after producing a response. That response can overshoot the remaining allowance; M7 records it and blocks the next model or tool unit. An exactly-at-limit stop response may complete, while an exactly-at-limit tool response cannot start another effect.
09 / CANCELLATION AND DEADLINES
All stop reasons converge; the runtime cannot force non-cooperative work to stop.
Every model round gets its own deadline, defaulting to 60 seconds. Cancellation can arrive through the runtime control, a caller signal, or iterator abandonment. The runtime forwards AbortSignal through model, permission, and tool waits, asks an active iterator to return, and gives cleanup 100 milliseconds before finalization continues.
model wait
├── per-round deadline · default 60,000 ms
├── timeout aborts the request
└── terminal status failed
permission wait / cooperative tool wait
├── cancel, external AbortSignal, or iterator abandonment
└── terminal status canceled
cleanup
├── forward AbortSignal
├── call iterator.return() best-effort
├── wait at most 100 ms for cleanup
└── cannot force code that ignores cancellation
terminal publication
├── turn.completed
└── agent.stopped
append uncertainty
└── never retry a durable append that may already have succeededSuccessful completion, budget exhaustion, cancellation, timeout, and other failures share one finalizer. It attempts turn.completed before agent.stopped. A pre-append event-construction failure may retry once as a failed outcome. A durable append is never retried because the store may already have accepted it. Persistence failure can therefore leave an incomplete terminal pair.
10 / MACHINE-READABLE SCOPE
The development task allowed runtime, event, and model work—and denied the network.
The M7 manifest limits the change to EVENTS.md, the events, kernel, and models packages, plus the task itself. Network and Git push are denied, commands are allowlisted, and the delivery target is a pull request. Its 100,000-model-token and 200-tool-call authoring budget governs the development agent, not applications using the runtime.
id: m7-deterministic-session-loop
goal: deterministic multi-round model and pure-tool execution
allowed_paths:
- EVENTS.md
- packages/events/**
- packages/kernel/**
- packages/models/**
- tasks/m7-deterministic-session-loop.yaml
permissions:
network: deny
git.push: deny
delivery:
type: pull_requestThe retained PR-head report checks the path boundary both before and after tests. All 15 changed paths remained inside the manifest and the report records zero violations.
11 / DELIVERY CHRONOLOGY
One implementation commit merged quickly, then its automated evidence finished.
Pull request #6 was created at 19:44:06 UTC on September 1, 2026 and merged 27 seconds later as 41af384. The displayed PR CI and CodeQL runs had started but had not finished when the merge happened. Both later passed on feature head 8e6bf73, and separate push workflows passed on the exact merge.
base 98924a66628bc66a88093ec6bee05f426f0fea9d
feature head 8e6bf735a8685f1f1deab63fe691f8df6c434166
merge 41af384b6d990c53aefe81e826e59cc33f00c47c
pull request #6
├── 15 changed files
├── 4,465 insertions
├── 277 deletions
├── 1 implementation commit
└── no dependency or lockfile change12 / VERIFIED RESULT
Adversarial tests exercise the loop’s boundaries, not its capacity.
PR-head CI subsequently passed 623 tests across 40 files, strict TypeScript, one golden scenario, and the two-sided path gate. The pull-request description identifies 59 M7 runtime cases. They cover multiple rounds, unknown and invalid tools, execution failure, malformed streams, mutable and accessor-backed values, permission and model waits, cancellation races, deadlines, budgets, append failures, and single-terminal behavior.
The development chronology also records two material corrections before that release. An early patch was moved from a stale pre-M6 checkout onto an isolated tree based on exact M6 merge 98924a6. Adversarial review then found a nested accessor-backed JSON gap and a terminal-event construction edge; the final source and tests harden both boundaries.
development correction
└── move the M7 patch from a stale pre-M6 checkout
onto an isolated tree based on exact M6 merge 98924a6
adversarial finding 1
├── nested accessor-backed JSON could cross the first normalizer
└── inspect descriptors once; reject accessors, symbols, and exotic arrays
adversarial finding 2
├── terminal event construction could fail before producing a terminal pair
└── centralize finalization; retry only pre-append construction as failed
race hardening
└── exercise cancellation, timeout, iterator cleanup, mutation, and append failureFor this publication I created a clean detached checkout of exact merge 41af384 under Node 26.5.0. With loopback sockets available, it reproduced 623/623 tests, strict TypeScript, and 1/1 golden scenario. This confirms the article pin; it does not add a new release, provider run, or operating benchmark. The command output is an unretained local publication record, not a public Harness artifact.
public PR-head evidence · GitHub Actions 33551227339
├── strict TypeScript passed
├── test files 40 / 40
├── workspace tests 623 / 623
├── M7 runtime cases 59 / 59
├── golden scenarios 1 / 1
├── changed paths checked before / after 15 / 15
├── path-policy violations 0
└── run-report/v2 passed
post-merge evidence · exact merge 41af384
├── CI 33551269520 passed
└── CodeQL workflow 33551268282 passed
unretained local publication audit · exact merge · Node 26.5.0
├── clean-checkout tests 623 / 623
├── strict TypeScript passed
└── golden scenarios 1 / 113 / EVIDENCE ARTIFACT
The retained gate artifact attests the feature head and has a finite lifetime.
Workflow 33551227339 uploaded gate-evidence-33551227339. Its JSON report pins head 8e6bf73, base 98924a6, 623 passing tests, both 15-path decisions, zero violations, and run-report/v2 status passed. The artifact is retention-bound rather than version-controlled evidence.
The report has seven serialized run events. Its accompanying SQLite file contains five events and still marks the session active; the final delivered and run-recorded events exist only in the JSON report. This note therefore does not call that database a closed terminal session log. Exact-merge CI, the public source pin, and the unretained local checkout are separate evidence lanes.
Both CodeQL analysis jobs completed successfully. GitHub's separate PR differential result was nevertheless inconclusive because one default-setup configuration was missing, so this article makes no “zero vulnerabilities” claim.
14 / CURRENT TRUTH
M7 proves local control semantics while leaving operational authority closed.
| Surface | What the evidence supports | What remains open |
|---|---|---|
| Session scope | One admitted turn may make multiple sequential model requests and execute multiple registered pure-tool intentions. | There is no general multi-turn follow-up manager, mid-loop steering channel, or production CLI/service path on MinimalAgentRuntime. |
| State | Every user, assistant, and tool message advances a version-1 immutable snapshot; each request names the revision it consumed. | M7 does not compact context, load a prior durable checkpoint, or resume a half-finished turn after restart. |
| Model boundary | FakeModel and local adapters prove strict text/tool stream validation, aggregation, ordering, deadlines, and failure semantics offline. | No live model provider, provider retry policy, billing-grade token counter, or provider cancellation compliance was exercised. |
| Tool boundary | Unknown and invalid calls cannot reach authorization or execution; valid calls require durable intent and authorization first. | Only WeakMap-registered pure tools are in scope. No file, process, workspace, network, secret, MCP, or remote tool is exposed. |
| Purity | The registry makes the executable set explicit and testable, and M7 passes an AbortSignal into each trusted implementation. | The pure marker is a trusted in-process capability—not a sandbox, proof of no side effects, or defense against malicious tool code. |
| Durability | Intent, policy, and any permission resolution append before execution; observable events append before yield. | EventStore remains injected and tested in memory. A tool.result append can fail after an effect, so M7 does not promise exactly-once execution. |
| Budgets | Steps, cumulative reported tokens, and requested tool calls have explicit stopping boundaries and durable warnings. | A provider response can overshoot the remaining token allowance before reporting usage; the runtime prevents the next unit of work. |
| Cancellation | Model, permission, and cooperative tool waits receive cancellation; cleanup is bounded and terminal publication is centralized. | External code that ignores AbortSignal cannot be force-killed, and a failed terminal append can leave no complete terminal pair. |
| Verification | Public head CI and a local exact-merge publication checkout passed 623 tests, strict types, and one deterministic golden scenario. | Those are correctness checks—not load, throughput, latency, capacity, live-provider, or production-durability evidence. |
The precise claim is a deterministic, append-fenced, single-turn multi-round loop proven offline with FakeModel and registered pure tools. It is not a production tool runner, isolated executor, durable resume engine, live model integration, multi-user service, or self-hosted Harness.
15 / FILE GUIDE
The loop, state, model protocol, evidence, and release contract are all inspectable.
Runtime and immutable state
- packages/kernel/src/runtime.ts — multi-round orchestration, tool and permission fences, budgets, cancellation, stream validation, and terminal publication.
- packages/kernel/src/state.ts — versioned, detached message state and model-context snapshots.
- packages/kernel/src/run.ts — permission and bounded JSON contracts shared with the legacy path.
Events and tool boundary
- packages/events/src/schemas.ts — additive runtime identities, state revisions, tool messages, cumulative usage, and step-budget warnings.
- EVENTS.md — canonical model/tool loop and permission ordering; its summary table retains the post-merge identity-field mismatch described above.
- packages/tools/src/tool.ts — the pre-existing trusted registration marker that bounds M7 to pure tools.
Model compatibility
- packages/models/src/model.ts — provider-neutral messages, calls, usage, context revisions, and stream vocabulary.
- packages/models/src/model-adapter.ts — legacy completion-to-stream tool-intention adaptation.
- packages/models/src/fake-model.ts — deterministic offline responses used by the proof lane.
Behavioral proof and scope
- packages/kernel/test/runtime.test.ts — 59 expanded runtime cases covering success, validation, persistence, policy, budgets, cancellation, and races.
- packages/kernel/test/state.test.ts — revision, detachment, immutability, and hostile-object coverage.
- tasks/m7-deterministic-session-loop.yaml — acceptance criteria, allowed paths, permissions, authoring budget, and PR delivery.
16 / WHAT IS NEXT
The current roadmap turns this local loop into a governed self-hosting platform.
A documentation-only follow-up first described M8 through M12. The current public roadmap has since decomposed that work through M76. That later planning does not enlarge what M7 shipped; it makes the remaining boundaries smaller and reviewable.
M7 deterministic single-turn session loop complete
M8–M11 workspace boundary, Local/Docker adapters, five tools planned
M12–M15 steering, compaction, durable replay, restart safety planned
M16–M18 offline runner, authorship attestation, live doctor planned
later lanes
├── M19–M31 effects, policy, durable sessions, SDK
├── M32–M42 MCP and ACP
├── M43–M58 remote execution, Docker, Kubernetes control plane
├── M59–M66 governed self-release
├── M67–M71 Canvas
└── M72–M76 automation ingress and rehearsal17 / EVIDENCE LEDGER
Repository and delivery claims resolve to public evidence; local reproduction is labeled.
- M7 merge
41af384— the exact public source pin for this article. - Pull request #6 — deterministic minimal session loop — authoritative 15-file diff, one-commit history, merge chronology, checks, and post-merge automated review.
- M7 task contract and event-order record — acceptance, scope, permissions, multi-round order, and permission semantics.
- PR-head CI and gate artifact and PR CodeQL workflow — subsequently successful checks on
8e6bf73, with the CodeQL differential caveat stated above. - Exact-merge CI and exact-merge CodeQL workflow — separate successful push workflows on
41af384. - Post-merge Copilot review — one unresolved low-severity documentation/schema catalog mismatch; no human approval is visible.
- Unretained local publication audit — a clean detached checkout of the exact merge reproduced 623/623 tests, strict TypeScript, and 1/1 golden scenario under Node 26.5.0. The shared development conversation supplied chronology and intent only; it is intentionally not published as implementation evidence.