Repository navigation
✨ Evaluate, preview and converse as a trusted host (#883, PR 1 of 2) - #890
Conversation
… PR 1 of 2) Replacement PR1, re-implemented on main's captured-selection and live- authorization model rather than rebased. #890's four commits and the Core-only bound-evaluation amendment from PR2 are redistributed here; the provider configuration, direct-provider and Sidekick work stays with PR2. A trusted host can now evaluate one complete Agent-generated **root**: the same admission, the same durable record and the same pinned resolution a fragment gets, in the one context that supplies a top-level <Output>. It answers with the canonical rendering and whether the root declared a region at all, so a host can tell an empty region from no region. What main contributes, and what changed to meet it: - The record's marker carries the root/fragment distinction. Main threaded one RECORD_VERSION = 4 through policyRecord, readPolicy, admitSource and readAdmission; that is now FRAGMENT_RECORD = 4 and ROOT_RECORD = 5, threaded as the marker an evaluation expects at the result and inside the policy. A marker-4 record cannot authorize a root and a marker-5 record cannot authorize a fragment, even for identical source and selected rows. Marker 4's closed shape is byte-identical to main's, so no context member, defaulted field or conversion exists and every fragment record stays readable. - The admission language travels with the walk. Lexical gained `language` and `topLevel`; <Output> left NO_GENERATED_CONTEXT and admits only in a root at the literal top level, validated by body-structure's own outputPropsViolation so <Output> is one contract. <Content> and <Return> stay barred in both. Every construct that encloses its children clears topLevel once. - Interpolation stays the fragment's refusal. A root begins with its own empty environment, so the only names it resolves are the ones it bound itself; a fragment expands against the document that admitted it and may not read a binding at all. - The selection is main's captured one, resolved per evaluation. selectedTables moved from Evaluate.ts to evaluation-profile.ts with the two refusals it raises, so the authored site and the root go through one machinery and neither can admit a row the other would not. The root's binding carries that selection as a closure, the minted <Syntax /> implementation, the guarded route and the captured reference — Core-private arguments, no Context authority, callback or token. Canonical <Syntax /> joins both the table a root may write and the entries it is described with, through admittedSymbols, with core's own documentation provenance. - An absent profile stays absent. A host that selected no generated components, or an execution that minted no canonical <Syntax />, leaves the bound operation refusing when it is called rather than failing the execution at startup — most executions never evaluate a root. - Main's own capture is preserved. The conflicting hunks in execute.ts kept main's describe(includes)/useRootSyntaxReference rather than #883's rootSyntaxReference, and the root narrows from that reference. - scanner.ts reports unfinished constructs, resolving its one conflict by keeping #883's separate pos-at-end branch and main's comment cleanup. generated-root-owner.ts allocates each evaluation a durable sequence past every one the history holds, partitions it from the document's continuation, and makes an authoritative failure the execution's own outcome. conversation.ts is taken at the amendment commit, which has the evaluation mailbox, readiness bridge and sequence partition and none of PR2's provider work. Evidence — the five frozen commands, all green on this tree: 1 host-generated-root, generated-xmd-root, generated-xmd-preview, agent-conversation 28 tests / 129 steps 2 generated-xmd, composition, selection, owner-metadata, evaluation-profile, evaluate-provider-lifetime 34 / 266 3 execution-protocol, document-output-api, syntax-component, syntax-api, syntax-loaded-copy 14 / 143 4 agent-components, agent-function-components, agent-session-launch, acp host-conversation, acp loaded-copy 24 / 160 5 workflow generated-observations, cli evaluate-component, cli evaluate-workflow 8 / 33 Total 108 tests / 731 steps, zero failures. `deno task check` exits 0. Commands 2, 3 and 5 were measured on pristine main first and come back at exactly the same counts, so main's fragment, selection, Syntax and workflow behaviour is untouched. The four ported Core tiers and the ACP tier had to be adapted from passing tables per call to declaring a captured selection — the handoff's "adapt discovered test setup while preserving their assertions". Every assertion is the accepted one except GR11, which asserted the removed `context` member and now asserts the markers it was replaced by. Refs #883
…883) Two live probes in the review of fdcef428 exposed contract violations the five frozen commands did not cover, and the public request, two new casts and the IN1/IN4 evidence needed correction with them. A captured canonical <Syntax /> is retained once. Core appended its own copy of the protected implementation to every root's table and description. An ordinary host selection may name `Syntax` — main's does — and main's capture then holds that same implementation behind this execution's own routed guard, so the two copies reached admission as one name under two definitions and every valid root of such a host refused. The whole row is now decided where the capture is known, in the selection canonical execution hands over: a row this execution's route answered for is kept as it stands, and a definition the route did not build is a different implementation under a protected name and stays the collision it was. Recognized by that routed provenance rather than by a name or a URL, so a same-name replacement is not accepted as canonical. GeneratedRootBinding has no definition of its own any more, because the selection is the one source for both the table a root writes and the vocabulary it reads about. An availability refusal is the call's answer. Two refusals left before the ordinary request boundary: the absent-profile operation threw HostExecutionError, and the selection was resolved ahead of `try`, so a class the host installed no table for threw ComponentInvocationError out of the evaluator. The generated-root owner reads a raised error as an authoritative failure of the run, so a host could catch the method's refusal, release its ordinary held document, and still be handed a failed canonical completion. Both now answer `Err` — the absent one in the classification a closed and a busy execution already refuse with, the unavailable class through the evaluator's own Result boundary. Nothing moves for durability, Files, secret-policy or teardown failures, which keep their authority. core/host exports the decided names. The settled public root request is GeneratedXmdRequest with its four members, and GeneratedEffectClass beside it because the request names it. The internal table-taking fragment request held that name and is now GeneratedFragmentRequest; the unapproved GeneratedXmdRootRequest is gone rather than kept as a second public name. The host root tier takes the request and the class from the entrypoint, so the corpus checks the published boundary. The two new formsOf array casts are gone, replaced by a typed form list and ordinary filtering. IN1 is established in both directions, at the seam that can establish it. The public owner gives each evaluation a sequence of its own, which is also what makes one kind's record unreachable from the other through it — so the reported gap was real and the old GR15c could pass on a fresh language refusal. Tier GRM drives the Core-private bound evaluator under a chosen coroutine, with source legal in both kinds and byte-identical selected rows, and reads back the expected-marker refusal by name with zero invoked effects; a matched marker resumes and performs, on both sides. The collision guard and an older build's marker-2 record have rows of their own. IN4's old-root journal case is E6b: a hand-built journal in the accepted pre-integration writer's shape (01abab2 — marker 2 at the result and in the policy, the `context: "root"` member, four-member structural identities) holding one completed root sequence with an acknowledged effect and one interrupted suffix. Opening repeats neither, the old bytes are unchanged, the main continuation takes no admission of theirs, and a new explicit root allocates generated_root:3 and writes marker 5. Also fixed in passing, because fdcef428 introduced them: fourteen error-level banned-word lint findings and seven unformatted files. `deno task lint` now exits 0, as it does on main. Refs #883
79d404d to
dbc0fc9
Compare
PR #890: ✨ Evaluate, preview and converse as a trusted host (#883, PR 1 of 2)23 files, +8849 / -241 Scope🔴 PR has 9090 lines changed. Split into focused PRs. 🟡 9090 lines changed. PRs under 400 receive more thorough review. 🟡 23 files changed. Are all changes related? StructuralOxlint structural signals:
Slop✅ Slop indicators look low. Static AnalysisOxlint: 88 diagnostics across 8 files (13 rules) no-unused-vars (29): packages/core/src/generated-root-owner.ts, packages/core/src/components/Evaluate.ts, packages/core/src/agent/function-components.ts (+2) CorrectnessNo extraneous code patterns detected. |
Closes nothing on its own — PR 1 of 2 for #883.
Why
xmd repl's Sidekick needs three things a document already has and a host doesnot: a way to evaluate generated XMD as a root rather than a fragment, a way
to show source that is still being typed without guessing at it, and a way to
hold one agent conversation open across a process. Today a host can reach none
of them:
<Output>is refused at the top level, the scanner reports nothingabout the construct it gave up on, and
<Session>/<Prompt>only exist insidean expansion with an element to name the placement and a journal already around
them.
This PR adds exactly those three capabilities and the specification they answer
to. It ships no Sidekick; PR 2 is the CLI.
What changes
Before:
<Output>was refused, and a fragment'sretained admission could be resumed as a root.
guess from a failed parse which refusals meant "not allowed" and which meant
"not finished yet".
DurablePreparationruns before
Execution.document, so one opened there reaches no provider andno placement coordinator.
After:
evaluateGeneratedXmdRoot(source)admits the fragment grammar plus oneconstruct — a top-level
<Output>— and the context is a term of theretained policy, so a root admission never resumes as a fragment or the
reverse.
previewGeneratedXmdRoot(source)is a pure prefix projection, noOperation.The scanner now reports what it abandoned (
UnfinishedConstruct), which iswhat tells "still arriving" from "not allowed" without a second grammar.
useAgentConversation(request)opens the genuine thing over aDurableStreamthe host supplies: canonical Session placement, journaled turns, verified
configuration readback, and a native identity the provider reattaches to.
How it works
The conversation is a host-declared identity component written into a
synthetic root core owns (
agent/conversation.md), because that is the onlyposition from which a session can be placed: canonical resolution hands the
element this execution's claimant, so the session runs under the engine's own
identity rather than under anything the host supplied. The host's
idnames thehistory and nothing else.
Its root is never completed. A durable root that records its terminal
replays instead of reopening, and reopening a chat is the ordinary case — so the
element serves turns and does not return, and the halt that releases the
resource appends nothing. What the history keeps is a partial continuation.
Each turn is one journaled Prompt under its own durable name, offered in the
conversation's sequence exactly as
<Prompt>offers one in a document's andthrough the same
runPromptthe component uses, run in a task of its own socancelling the call halts the turn.
Review guide
Start with:
packages/core/host.ts— the three exports and why each exists.Then review:
specs/acp-client-spec.md§A conversation a trusted host holds open, andspecs/executable-mdx-spec.md— the contract this implements.packages/core/src/agent/conversation.ts— the whole C3 capability.packages/core/src/generated-xmd.ts—GeneratedContextthreaded throughadmission, the retained
Policy.context, and theResultboundary.packages/core/src/generated-xmd-preview.tsandsrc/scanner.ts—the projection and the unfinished-construct reporting it needs.
Look carefully at:
deliver()and the caller'sensureinhandle(): the cancellation join.The
ensurethat reports a turn joined is registered before the turn isspawned, which is what makes it unwind last — cleanups registered later run
first, so the provider's own cleanup is finished before the caller is
released, and
busystays set through it.ordinaryGeneratedFailure(): which failures becomeErrwith a partialrendering and which are rethrown as themselves.
What must stay true
by
contextbeing a term of the retainedPolicycompared inpolicyHolds(), checked by GR13 and GR14.contextonly for a root, checked by GR10/GR11/GR15 and the whole releasedfragment row.
admits no second turn until it ends — enforced by the pre-spawn
ensureandby
live.busyclearing in the caller's ownfinally, checked by CV18/CV19.enforced by comparing the resolved and recorded
agentSessionIdagainst whatthe retained records establish, checked by CV21/CV23/CV24 and HC3.
opens — so a refusal never appends a terminal to the history it refused,
checked by CV14.
How to verify it
Errwith the truthful partial rendering, and fail if the partial were dropped or a
refused admission were allowed to carry one (GR8).
if
contextwere left out of the policy comparison.and fail if the preview inferred "still typing" from any parse failure.
and fail if it became an
Operationthat could.a caller's halt does not return until the provider's blocked cleanup finishes,
and fails if the caller merely signals cancellation and leaves. CV19 then
proves fresh explicit work still starts, so the fix is not a deadlock.
refused before the turn, refused after it, and unopenable when the history's
own turns name two conversations.
provider the way
installAgentProviderStackdoes, with ACPX's runtimereplaced by this package's scriptable fake and the host's own mapping
acknowledgement refused once — the ordering
provider.test.tsSM9demonstrates. The provider's assertion survives, the owner is released, and
explicit new work reconciles to that same conversation, keeps one store
record and does not resend the interrupted prompt. It fails if a reopening
created a replacement conversation or re-sent accepted work.
Also run:
deno task check(exit 0),deno task lint(0 errors, format clean),deno task validate:docs. The new test file was run under Node and Bun as wellas Deno, so it needs no runtime exclusion.
Scope
Included
evaluateGeneratedXmdRoot(),previewGeneratedXmdRoot()anduseAgentConversation()on@executablemd/core/host.architecture.md,specs/acp-client-spec.mdandspecs/executable-mdx-spec.mdupdated to match.Intentionally unchanged
which starts from this commit.
packages/acp/src/provider.ts. Tier HC was written to find out whetherthe provider's commit window needed a production change. It did not, so there
is none; the file is untouched and only gains a test alongside it.
evaluateGeneratedXmd()and its recordsbehave exactly as before, which the preservation row asserts.
New abstractions
GeneratedContext/ROOT_CONTEXTexists because the one thing separating aroot from a fragment is a ceiling, and a ceiling that is not retained with the
admission can be changed between runs. Consumers: admission, the policy
comparison, and the record.
UnfinishedConstructexists because the preview's whole question — is thisnot allowed, or just not finished — is unanswerable from a parse failure
alone. Consumers:
previewGeneratedXmdRoot().AgentConversation/AgentConversationRequest/AgentConversationErrorexist because a host conversation is a resource with a mailbox, not a door:
the handle reaches the element only through its queue. Consumers:
useAgentConversation()and, next, the CLI Sidekick.readsBinding()insrc/generated-interpolation.tsexists because admissionand preview must apply one interpolation rule; two copies would let a
preview promise what admission refuses.
Each new abstraction has multiple concrete uses or a clear justification.
No speculative functionality is included.
Generated or mechanical changes
packages/core/src/agent/function-components.tsonly exportsrunPromptandCarried, so the component and the host operation reach one definition of acanonical turn instead of a copy. No behavior change.
packages/core/src/errors.tsonly exports the existingfirstCause.Risks and limitations
it owns is always a partial continuation. A consumer that treats an absent
terminal as corruption would misread it; the spec states this.
reconciled, by design — reconciling it needs the provider's own account of
that turn, which is the host's to fetch. PR 2 is the first consumer that will
have to surface that refusal to a person.
them yet, so reverting this commit removes the capability without touching any
existing path.
Scope confirmation
Delivered onto current main (2026-10-16)
This branch is the replacement for the original stack, rebased onto main
fd8ae521(#927). Headdbc0fc92, two signed commits, basemain.Its old base was
986948ea. Main has since merged four #181 PRs (#902, #925,#926, #927), which touch REPL program and session, the runtime entrypoints and
the repository and issue composition. This PR overlaps that range on two
documentation files only, and
range-diffagainst the reviewed source reportsboth commits identical.
Evidence on this exact head — the five frozen Core/ACP selections:
111 tests, 743 steps, zero failures.
deno task check,deno task lintanddeno task validate:docsall exit 0, anddeno task check:jsrendsSuccess Dry run complete.PR #921 is stacked on this branch and merges after it.