Skip to content

test(runtimes): a turn on the acp runtime, end to end (#1634) - #1835

Merged
jhgaylor merged 4 commits into
mainfrom
stack/1634-4-turn-e2e
Sep 11, 2026
Merged

jhgaylor merged 4 commits into
mainfrom
stack/1634-4-turn-e2e

Conversation

@jhgaylor

@jhgaylor jhgaylor commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

The three PRs below this one add a runtime module, a column and a skills
mount. This is the acceptance for all three: a real ConversationServer
driving a scripted agent through a whole turn, on an account that holds no
inference credential at all.

The protocol half is already pinned by conversation_server_acp_test.exs and
is the same code. What this file pins is what is different.

  • The command the agent named is what gets spawned, as bash -lc <line>
    inside the sandbox, and initialize is what gets written to it rather than
    the prompt.
  • session/new carries no model to pin, so the model/failed stage this
    runtime could never recover from is unreachable.
  • Blocks reach the transcript and the stop reason closes the turn, with a null
    usage, so credits price the turn by sandbox time alone.
  • A refusal stop reason still ends the turn, an interrupt still reaches the
    command as session/cancel, and both permission verdicts still hold: a
    request is held and rendered and the answer goes back, and an auto_deny
    policy answers without asking anybody.

Test-only. Backing RuntimeDispatch.command("acp", agent) out to a fixed argv
fails "runs the agent's own command as a shell line inside the sandbox" and
nothing else, so the file measures the runtime rather than the harness.


Stack for #1634 (merge top down; each PR is based on the one below it in the list):

PR What it is
1 #1832 Fountain.CommandRuntime and the host dispatch for acp
2 #1833 agents.runtime_command, and acp becomes a runtime you can name
3 #1834 skills mount on a runtime the library does not know
4 #1835 a turn on the acp runtime, end to end
5 #1836 refuse a turn whose agent no longer carries a command
6 #1837 the acp runtime in the agent form
7 #1838 a real ACP program through a whole turn
8 #1839 docs, CHANGELOG and SDK 1.25.0

Replaces #1677, which was the same feature as one 1,760-line branch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A2iMoNvSvrkQQfnP4RzVi2

jhgaylor and others added 4 commits September 10, 2026 21:33
)

Every runtime Fountain has is an LLM coding agent. A deterministic program
that wants a warm machine, a vault, a thread, a schedule and a human who can
read what happened has nowhere to run: `Managoat.Runtimes` is a closed map of
four CLIs, each with a pinned adapter, a provider and an inference credential.

This adds the runtime module and the host dispatch that resolves it, and
nothing else. `"acp"` is not yet a value `Agent.changeset/2` accepts and the
`runtime_command` column it reads does not exist, so no request reaches this
code until the next PR in the stack opens the door.

- `Fountain.CommandRuntime` implements `Managoat.Runtimes` for a command the
  agent names. It writes no config, prepares no sandbox and builds no legacy
  command, so `Provisioning`'s `function_exported?` guards skip it. Its
  `default_env/2` ignores the credentials it is handed and exports one
  variable, `FOUNTAIN_SKILLS_DIR`.
- `Fountain.RuntimeDispatch` answers `acp` itself: `for_agent/1`,
  `acp_enabled?/1` and an `install/3` that has nothing to install. It gains
  `acp_model/2` (always nil for `acp`, so the pin path that reports
  `model`/`failed` is unreachable there), and the two predicates the schema
  needs next, `model_required?/1` and `command_required?/1`.
- `command/1` becomes `command/2`. The argv of this runtime is a property of
  the agent rather than of the runtime name, so the agent has to be in hand;
  every other clause ignores it. `TurnMachine.command/7` passes it through.

Stack for #1634 (merge top down):

1. **this PR** — the runtime module and the dispatch
2. `agents.runtime_command`, and `acp` becomes a runtime an agent can name
3. skills mount on the acp runtime
4. a turn on the acp runtime, end to end
5. refuse a turn whose agent no longer carries a command
6. the console form
7. the conformance fixture, a real OS process through a whole turn
8. docs, CHANGELOG and the SDK release

Refs #1634

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2iMoNvSvrkQQfnP4RzVi2
…an name (#1634)

Opens the door the previous PR built behind. `acp` joins the runtime enum, a
new `runtime_command` column carries the command it launches, and `model`
becomes optional for that one runtime.

- **The column.** `agents.runtime_command` is text: a value may be a whole
  shell line ("cd /srv/app && exec bin/agent acp"), and a length rule belongs
  in the changeset if one is ever wanted. `agents.model` loses its NOT NULL in
  the same migration, because whether a model is required is a rule about the
  pair of columns and belongs where both are in hand.
- **Both refusals are 422s naming the field.** A missing `runtime_command` on
  `acp`, or one carried by any other runtime, which resolves its own
  executable from a pinned table. A `runtime_command` that is stored and never
  run reads as configuration and is not.
- **A model is still checked when it is given.** Optional on `acp` and inert
  there, but it still has to parse and name a known provider, because a value
  that is stored and ignored is worse than one that is refused.
- **`model` is nullable on the wire**, in the response and in both request
  bodies, so an agent converted to `acp` can clear the model it no longer uses
  with `{"model": null}`. CastAndValidate runs before the changeset, so a
  non-nullable string there would reject the null with a 400 and leave the
  stale value on the row forever. `AgentRequest` drops `model` from `required`
  and the changeset enforces it instead, so a missing model is a 422 rather
  than a 400.
- Model suggestions return nothing for `acp`, and the export carries the field
  so an account export stays a restorable copy.

**The Swift SDK had to widen with the wire.** `FountainKit.Agent` declared
`public var model: String` on a `Decodable` struct, so an explicit null threw
`valueNotFound` — and because a page is decoded whole, one acp agent in the
account broke `agents.list()` for every Swift caller, not just a fetch of that
agent. `model` is now `String?`, `Agent` and `AgentInput` carry
`runtimeCommand`, and `Runtime.acp` joins the other four. The contract checker
does not compare a hand-written Swift model's optionality against a nullable
wire field, so nothing caught this: the two new decode tests do, and both fail
with the original `valueNotFound` when the widening is backed out.

`fountain apply` needed no change — the manifest's allowed spec keys derive
from `Agent.cast_fields/0` — but nothing pinned that, and the docs in the last
PR of this stack show a manifest using the field. Two tests in
`manifest_test.exs` now cover it, and the first fails if `runtime_command`
leaves `cast_fields/0`.

`sdk-no-release`: `contract.json`, the generated TypeScript types and the
Swift model move here; the TypeScript version bump is the last PR in the
stack, and the Swift entry goes under its existing `Unreleased` heading.

Refs #1634

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2iMoNvSvrkQQfnP4RzVi2
`SandboxSkills.mount/3` forwarded a runtime **string** to
`Managoat.Runtimes.Skills.install/3`, which resolves it through
`Managoat.Runtimes.for_runtime/1` — a closed map of the four packaged CLIs
that knows neither `acp` nor the deployed ACP fixture. It answered
`{:error, "unsupported runtime: acp"}`, and the sandbox came up with no skills
at all, including the two bundled ones, with nothing said about it anywhere.

- A string is resolved through `Fountain.RuntimeDispatch`, the host dispatch
  that already knows every runtime Fountain has. A module is passed straight
  through as before.
- A runtime nothing implements is logged and returned rather than swallowed. A
  missing skill is a degraded agent and not a broken one, which is the trade
  `Managoat.Runtimes.Skills` already makes for a single skill that will not
  install; this extends it to a runtime that cannot be resolved, without
  extending it to silence.

The deployed fixture was reached by this too, and its own changeset refuses an
agent carrying any skills, so nothing observable changes there.

Stack for #1634 (merge top down):

1. `Fountain.CommandRuntime`, the acp runtime's module
2. `agents.runtime_command`, and `acp` becomes nameable
3. **this PR** — skills mount on the acp runtime
4. a turn on the acp runtime, end to end
5. refuse a turn whose agent no longer carries a command
6. the console form
7. the conformance fixture, a real OS process through a whole turn
8. docs, CHANGELOG and the SDK release

Refs #1634

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2iMoNvSvrkQQfnP4RzVi2
The three PRs below this one add a runtime module, a column and a skills
mount. This is the acceptance for all three: a real `ConversationServer`
driving a scripted agent through a whole turn, on an account that holds no
inference credential at all.

The protocol half is already pinned by `conversation_server_acp_test.exs` and
is the same code. What this file pins is what is different.

- The command the agent named is what gets spawned, as `bash -lc <line>`
  inside the sandbox, and `initialize` is what gets written to it rather than
  the prompt.
- `session/new` carries no model to pin, so the `model`/`failed` stage this
  runtime could never recover from is unreachable.
- Blocks reach the transcript and the stop reason closes the turn, with a null
  usage, so credits price the turn by sandbox time alone.
- A `refusal` stop reason still ends the turn, an interrupt still reaches the
  command as `session/cancel`, and both permission verdicts still hold: a
  request is held and rendered and the answer goes back, and an `auto_deny`
  policy answers without asking anybody.

Test-only. Backing `RuntimeDispatch.command("acp", agent)` out to a fixed argv
fails "runs the agent's own command as a shell line inside the sandbox" and
nothing else, so the file measures the runtime rather than the harness.

Stack for #1634 (merge top down):

1. `Fountain.CommandRuntime`, the acp runtime's module
2. `agents.runtime_command`, and `acp` becomes nameable
3. skills mount on the acp runtime
4. **this PR** — a turn on the acp runtime, end to end
5. refuse a turn whose agent no longer carries a command
6. the console form
7. the conformance fixture, a real OS process through a whole turn
8. docs, CHANGELOG and the SDK release

Refs #1634

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2iMoNvSvrkQQfnP4RzVi2
@jhgaylor
jhgaylor force-pushed the stack/1634-4-turn-e2e branch from 0204796 to e8b769c Compare September 11, 2026 01:41
@jhgaylor
jhgaylor changed the base branch from stack/1634-3-skills to main September 11, 2026 01:57
@jhgaylor
jhgaylor merged commit 6f75af7 into main Sep 11, 2026
23 checks passed
@jhgaylor
jhgaylor deleted the stack/1634-4-turn-e2e branch September 11, 2026 02:21
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.

3 participants