Skip to content

Close the execution loop: facet-fct agent - #13

Merged
rokoss21 merged 1 commit into
masterfrom
feat/execution-loop
Aug 21, 2026
Merged

Close the execution loop: facet-fct agent#13
rokoss21 merged 1 commit into
masterfrom
feat/execution-loop

Conversation

@rokoss21

Copy link
Copy Markdown
Owner

ROADMAP item 2.

The problem

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, which is also why §16 sat at 12/65 coverage with the rest blocked rather than merely unwritten.

The loop

facet-fct agent drives the turn cycle: model answers → guard decides → tool executes → result enters the next turn.

facet-fct agent --input contract.facet --script turns.json --artifact execution.json --exec

The model sits behind a ModelClient trait, so this is a runtime and not a vendor wrapper. ScriptedClient replays 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 @interface function:

seq=3 tool_expose   WeatherAPI.get_current   allowed  rule=expose-weather
seq=4 tool_call     WeatherAPI.get_current   allowed  rule=call-weather

A denial is still provenance. Drop the tool_call rule and the same script yields seq=4 … denied, the run stops with F454, 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 in AgentOutcome, not an early return.

One chain over the whole run. seq is contiguous 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.

Also

run and agent 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 51 → 57.

Honest limits (in ROADMAP.md and docs/19-agent-loop.md)

  • F455 is not reachable from the loop yet: it needs a condition that fails at evaluation time, not a policy that says no.
  • Tool results are appended to the rendered payload as messages, so a turn is a patched payload rather than a recompiled contract. Fine for provenance; the next step is feeding results back through the document.
  • The only client is ScriptedClient.

🤖 Generated with Claude Code

`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>
@rokoss21
rokoss21 merged commit d4a1a3d into master Aug 21, 2026
14 checks passed
@rokoss21
rokoss21 deleted the feat/execution-loop branch August 21, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant