Repository navigation
feat(workflow): choose agent and model separately, ACP as the default runtime - #71
Open
FreshlyBrewedCode wants to merge 1 commit into
Open
FreshlyBrewedCode wants to merge 1 commit into
FreshlyBrewedCode wants to merge 1 commit into
Conversation
… runtime
ADR 0013 §2-§4 on the authoring and config surface.
- `agent?: "claude" | "opencode"` next to `model` on `AgentCallOptions`,
workflow `agent` defaults, schedule `agent`, and the run request
(`POST /api/runs` `agent: {agent?, model?}`, `factory start --agent
--model`).
- `config.agent`: `default`, `models`, `hostSettings` (default "ignore"),
validated at load; `adapter` stays the injection point for replay/fakes.
- Resolution (`runtime/agent-choice.ts`): agent from the most specific level
that names one, else `agent.default`; model from the most specific level at
or above the one that chose the agent, else `agent.models[agent]`. A step
whose model resolves to nothing fails before it starts, naming the fix.
- The default runtime is the ACP runtime: one `acpAdapter` per agent
definition, picked per step by the new `AgentAdapterOptions.agent`. The
agent runtime service carries the config rung (default agent, models).
- `AgentStepStarted.agent` (optional for old logs); run detail shows it.
- `factory init` and this repo's config write `agent.default` and
`agent.models`. The daemon checks the configured agents at start and logs
what cannot run.
Removed `DEFAULT_MODEL` (#40): model ids belong to the agent and the host,
and an agent left on its own default runs on the host's settings, so factory
keeps no built-in model; projects set `agent.models`. Removed
`permissionMode` (#39): nothing read it, and every permission ask is now
answered by the ACP client (`allow_always`, then `allow_once`), so a per-call
mode would promise control the runtime overrides. A behavioural mode (e.g.
read-only plan) can come back as an explicit, validated `mode` passthrough.
Closes #64, closes #39, closes #40
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
FreshlyBrewedCode
added this pull request to stack #70
October 2, 2026 19:49
This was referenced Oct 2, 2026
This branch has not been deployed
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.
What
This PR implements ADR 0013 §2–§4 on the authoring and config surface, and makes the ACP adapter (#63) the default runtime.
agent?: "claude" | "opencode"next tomodelat every level:AgentCallOptionsand the workflowagentdefaults;agent;POST /api/runstakesagent: { agent?, model? }(validated, 400 otherwise), andfactory starttakes--agentand--model.config.agenthasdefault,modelsandhostSettings(default"ignore", per agent). It is validated at load: unknown agents, empty models and badhostSettingsvalues throw and name the field. Workflow and schedule agent choices are validated at load too.adapteris still the injection point for replay and fakes.src/runtime/agent-choice.ts. The levels, most specific first, are call > run (request or schedule) > workflow > config.agent.default.agent.models[agent].AgentStepStartedand before any agent process starts. The message names the fix:agent step "implement": no model for agent "claude": name one with \model` on the ctx.agent call, the run, the schedule or the workflow, or set `agent.models.claude` in factory.config.ts`.AgentAdapterOptionsgainsagent, resolved likemodel.AgentRuntimeservice now carries the config rung (defaults: { default, models }) beside the adapter.run.tsresolves the choice from Effect context (ADR 0012 §4), and the boundaries stay plain async.acpAgentsAdapter(hostSettings), keeps oneacpAdapterper agent definition, built on first use, and dispatches onoptions.agent.config.agent.adapterkeeps working for every agent.hostSettingsfeedsacpAgent(kind, { hostSettings }).DEFAULT_MODEL(Add a config-level default agent model #40) andpermissionMode(permissionMode is declared on the authoring surface and read by nobody #39). The commit message records why.AgentStepStarted.agentis optional, so old logs have none. Run detail showsagentabovemodel, and the event summary readsimplement · claude · haiku.factory initwritesagent.default: "claude"andmodels: { claude: "sonnet", opencode: "opencode/big-pickle" }..factory/factory.config.tsgets the samemodels, but withdefault: "opencode"(see Deviations).implement-parent-issuedrops its own hardcodedomniroute/…model. That model is a host-config provider, andhostSettings: "ignore"hides it. The workflow now takes an optionalagent/modelinput.checkAgents(configuredAgents(config))in the background and logs each problem. The configured agents are the default, every agent with a model, and every agent a workflow or schedule names. The check is skipped when an adapter is injected.Fixes to the layer below (#69, branch
acp/adapter)The live runs turned up two defects in the adapter. Both are fixed on that branch, and the stack is rebased:
fix(runtime): send ACP an absolute cwd for a relative workspace. ACP'ssession/newrequires an absolutecwd, but the default workspace root is relative (.factory/workspaces). As a result, every daemon-allocated Claude step failed with "cwdmust be an absolute path". The fake ACP agent now validatescwdthe same way claude-agent-acp does. The new test fails without the fix and passes with it.fix(runtime): always pin the session's model over ACP. With host settings ignored, claude-agent-acp still reports the user'smodelsetting (haikuon this host) as the current model, while the Agent SDK runs its own default. The adapter skippedset_config_optionwhen the requested model equalled that reported value. So a step asking forhaikuactually ran on a 1M-context model, costing 0.080 USD for 330 output tokens, and the model said it was "Claude Opus 5.5". After the fix, the same run reports a 200k window, costs 0.050 USD, and the model says "I'm Claude Haiku 4.5".Testing
bun run checkis green: 429 tests on this layer, and 378 onacp/adapteralone.nix develop --command bun run test:e2e: 15 passed.agent-choice.test.ts: a 15-row table for the resolution rule. It covers carry-across, the chooser's own model, the run level, the config-less case, an unknown agent and a missing model. The same file checks the removals:DEFAULT_MODELis absent, and@ts-expect-errorguardspermissionMode.run.test.ts: the adapter receives the resolved agent and model, andAgentStepStartedrecords both. Also the run level, and an unresolvable model that fails with noAgentStepStartedand no adapter call.agent-runtime.test.ts:acpAgentsAdapterlaunches the resolved agent per step over the fake ACP agent, on the right model, with per-agenthostSettings. Also the runtime defaults.config.test.ts: defaults, carry-through, six rejection cases, workflow and schedule validation, andconfiguredAgents.http.test.ts: the requestagentpicks that agent's configured model; the request model wins; there are three 400s.daemon.agents.test.ts: the check runs with the configured agents, is skipped for an injected adapter, logs each problem, and survives a failing check.cli-argv.test.ts:--agent/--model, and an unknown--agentis rejected.init.test.ts: the generated config loads, and itsagentblock validates.events.test.ts:AgentStepStarted.agentround-trips, and an old log without it still decodes.run-events.test.ts: the SPA fold and the event summary.Live validation
The setup is a scratch project: a local bare
origin, a slug that is not on GitHub, and a workflow with no write-back. Its config isagent: { default: "claude", models: { claude: "haiku", opencode: "opencode/big-pickle" } }, andfactory serveruns on port 3017.AgentStepStartedfactory start hello-local --watch(default agent)agent: "claude", model: "haiku"acp.usagereports a 200k window and 0.050 USD. The model replied "I'm Claude Haiku 4.5".math.tschangedfactory start hello-local --agent opencode --watchagent: "opencode", model: "opencode/big-pickle"(fromagent.models, not haiku)math.tschangedfactory start hello-local --model no-such-model --watchagent: "claude", model: "no-such-model"claude has no model "no-such-model" (12 offered, e.g. default, opus, …)factory servewithPATH=/usr/bin:/binagents: opencode is configured but cannot run on this host: \opencode` is not on PATH`Deviations
DEFAULT_AGENT = "opencode", kept for continuity) and must name their model themselves. Otherwise they fail with the message above.RunStarted. A per-call option may still supply the model, so the run can't know earlier. The failure still comes before the step event and before any process is spawned.factory initdefaults toclaude, but this repo's own config defaults toopencode. Its runs (implement-parent-issue, on the prod instance) stay where they ran before, and on a free model. Themodelsentries match the init template.fakeAgents(adapter)inreplay/adapter.tssuppliesFAKE_AGENT_MODELS, and the test call sites use it.Notes for later layers
AgentStepStarted.agentis optional, andAgentStepView.agentisstring | undefinedin the SPA.usagesignal is still ignored inagent-step.ts.acp.usagechunks with context and cost, ready to record.opencode-adapter.ts,prepareWorkspace,sandbox-config.ts).acpAgentsAdapter.prepareWorkspaceis a no-op.extractOpencodeSignal.agent-runtime.test.tsimportsopencodeAdapteronly to assert that it is not the default.factory start --agent/--modelandPOST /api/runsagentexist for the live legs.cwdand always pinning the model, belong in ADR 0013's amendments and the finding. The second one weakens finding 13 §1's "currentValue" reading for Claude.agentconfig block, the--agent/--modelflags, and that a config-less run must name a model.Closes #64
Closes #39
Closes #40
Part of #62
🤖 Generated with Claude Code