Skip to content

feat(agents): agents.runtime_command, and acp becomes a runtime you can name (#1634) - #1833

Merged
jhgaylor merged 2 commits into
mainfrom
stack/1634-2-runtime-command
Sep 11, 2026
Merged

feat(agents): agents.runtime_command, and acp becomes a runtime you can name (#1634)#1833
jhgaylor merged 2 commits into
mainfrom
stack/1634-2-runtime-command

Conversation

@jhgaylor

@jhgaylor jhgaylor commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

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: advice to fill in a field that
    does nothing is worse than no advice.
  • The export carries the field, so an account export stays a restorable copy.

Nothing runs a turn on this runtime any better than before this PR — the
dispatch that spawns it landed below, and the skills mount, the refusal path,
the console and the docs are above.

sdk-no-release: contract.json and the generated TypeScript types move
here, and the version bump is the last PR in the stack.


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

Copy link
Copy Markdown
Collaborator Author

The first run of A change to the SDK says whether it releases failed because the run started about two minutes before the sdk-no-release label landed on this PR. The label is what the gate's own error message asks for when a PR changes the published surface without releasing, and the version bump is #1839. Re-running.

@jhgaylor
jhgaylor force-pushed the stack/1634-2-runtime-command branch from 9f0ac07 to c7bdd83 Compare September 11, 2026 01:19
jhgaylor and others added 2 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
@jhgaylor
jhgaylor force-pushed the stack/1634-1-command-runtime branch from d7bf1b6 to 663828c Compare September 11, 2026 01:41
@jhgaylor
jhgaylor force-pushed the stack/1634-2-runtime-command branch from c7bdd83 to d577e1f Compare September 11, 2026 01:41
@jhgaylor
jhgaylor changed the base branch from stack/1634-1-command-runtime to main September 11, 2026 01:55
@jhgaylor
jhgaylor merged commit 39a56b9 into main Sep 11, 2026
24 checks passed
@jhgaylor
jhgaylor deleted the stack/1634-2-runtime-command 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

sdk-no-release Changes the SDK's published surface deliberately without cutting a release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants