Close the execution loop: facet-fct agent - #13
Merged
Conversation
`run` compiles one request and stops, so §16's Runtime Guard and Appendix F's provenance chain existed only in simulation: `ToolExecutor` was reachable from `test_runner` and its mocks, and from nowhere else. Deterministic agent execution was specified but never executed. `facet-fct agent` drives the turn cycle — the model answers, the guard decides, tools execute, results enter the next turn. The model sits behind a `ModelClient` trait so this stays a runtime rather than a vendor wrapper; `ScriptedClient` replays recorded turns, which makes guard behaviour testable without a network and gives item 3 a seam to plug a real provider into. Three properties now hold in fact rather than on paper: - A `tool_call` guard decision is taken before the call is initiated (§16.6.1a), against the effect class declared on the `@interface` function, and lands in the artifact with its input hash. - A denial stops the run with F454 *and still writes the artifact*. A refused run is exactly the case where provenance matters, so the denied decision is recorded before the error is reported (§18.1.3). This is why a denial is an outcome in `AgentOutcome` rather than an early return. - One hash chain spans the whole run, with contiguous `seq` across turns, and replaying the same script reproduces the head byte for byte — the property that makes an attestation over `hash_chain.head` worth anything. Tools are registered from the contract's own `@interface` blocks, so a call to something the contract never declared has nowhere to land. `run` and `agent` now share one compile path (`compile_contract`), so a turn built by the loop is byte-identical to the same contract compiled by `run`. Coverage rises to 57 of 206: the loop unblocks §16.6 and Appendix F statements that could not previously be exercised at all. What is still out of reach is recorded in ROADMAP.md and in docs/19-agent-loop.md — F455 needs a condition that fails at evaluation time rather than a policy that says no, and tool results are appended to the rendered payload rather than fed back through the document, so a turn is still a patched payload rather than a recompiled contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ROADMAP item 2.
The problem
runcompiles one request and stops. So §16's Runtime Guard and Appendix F's provenance chain existed only in simulation —ToolExecutorwas reachable fromtest_runnerand its mocks, and from nowhere else. Deterministic agent execution was specified but never executed, which is also why §16 sat at 12/65 coverage with the rest blocked rather than merely unwritten.The loop
facet-fct agentdrives the turn cycle: model answers → guard decides → tool executes → result enters the next turn.The model sits behind a
ModelClienttrait, so this is a runtime and not a vendor wrapper.ScriptedClientreplays recorded turns — guard behaviour becomes testable without a network, and item 3 gets a seam to plug a real provider into.What now holds in fact rather than on paper
A real guard decision. Taken before the call is initiated (§16.6.1a), against the effect declared on the
@interfacefunction:A denial is still provenance. Drop the
tool_callrule and the same script yieldsseq=4 … denied, the run stops withF454, the tool never executes — and the artifact is still written. A refused run is exactly when the record matters, so a denial is an outcome inAgentOutcome, not an early return.One chain over the whole run.
seqis contiguous across turns, and replaying the same script reproduces the head byte for byte — the property that makes an attestation overhash_chain.headworth anything.Tools are registered from the contract's own
@interfaceblocks, so a call to something the contract never declared has nowhere to land.Also
runandagentshare one compile path (compile_contract), so a turn built by the loop is byte-identical to the same contract compiled byrun. Coverage rises 51 → 57.Honest limits (in
ROADMAP.mdanddocs/19-agent-loop.md)F455is not reachable from the loop yet: it needs a condition that fails at evaluation time, not a policy that says no.ScriptedClient.🤖 Generated with Claude Code