Skip to content

feat(runtime): add Effect composition root and agent runtime service - #56

Open
FreshlyBrewedCode wants to merge 5 commits into
37-move-headless-permissionsfrom
36-agent-runtime-service
Open

FreshlyBrewedCode wants to merge 5 commits into
37-move-headless-permissionsfrom
36-agent-runtime-service

Conversation

@FreshlyBrewedCode

@FreshlyBrewedCode FreshlyBrewedCode commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

Part of #32 · Closes #36

The agent adapter was a value threaded by hand through eight files and roughly twenty call sites
(cli.ts → daemon.ts → ServerOptions → StartTrackedRunOptions → DispatchEnv → startRun →
buildAgentStepEffect), defaulted to opencodeAdapter in two separate places, with no config
surface at all. docs/adr/0012-agent-runtime-seam.md §4 called this the worst plumbing in the
codebase and the strongest case to prove Layer/Context.Service on before #37 applies the
pattern to the remaining singletons. This PR makes the agent runtime the daemon's first Effect
context service, resolved where it's used instead of passed everywhere it might be needed.

What changed

  • src/runtime/agent-runtime.ts — new AgentRuntime context service (Context.Service) carrying
    { adapter }, with AgentRuntimeLayer(adapter) to swap it and makeAgentRuntime(adapter) for
    callers (tests, the CLI's direct-run path) that need a standalone ManagedRuntime.
  • src/runtime/agent-step.ts — buildAgentStepEffect now returns Effect.Effect<AgentStepHandle, never, AgentRuntime> and resolves the adapter with yield* AgentRuntime instead of taking it as
    an option.
  • src/config.ts — FactoryConfig.agent.adapter, defaulting to opencodeAdapter when unset, so
    existing factory.config.ts files keep working unchanged.
  • src/server/daemon.ts — builds the AgentRuntimeLayer from config and constructs one
    ManagedRuntime at the composition root, passed into serve() and the scheduler.
  • adapter removed as a pass-through field from ServerOptions, StartTrackedRunOptions,
    DispatchEnv, StartRunOptions and AgentStepEffectOptions — replaced by a single runtime: ManagedRuntime<AgentRuntime> threaded through the same call chain, so nested dispatch
    (dispatchChildRun → startTrackedRun → startRun) passes the runtime down rather than the raw
    adapter, and a grandchild run resolves the same service.
  • src/cli.ts — runCli builds a ManagedRuntime from options.adapter ?? config.agent.adapter
    for the no-daemon direct-run path.
  • Test suites across runtime/, server/, replay/ swap the corpus-replay/slow-fake adapter via
    AgentRuntimeLayer/makeAgentRuntime instead of an adapter option.
  • Fix-forward (this update): src/cli.ts's import.meta.main entrypoint had regressed off
    effect/unstable/cli — see "Notes for reviewers" below. Restored Command.run(factoryCommand, ...) as the real entrypoint while keeping the ManagedRuntime/AgentRuntimeLayer composition
    root in runCli intact, restored prepareWorkspace: options.clone !== undefined on runCli's
    startRun call (silently dropped by the same bad merge), and removed cli-commands.ts's
    runCommand hardcoding of adapter: opencodeAdapter, which had bypassed runCli's
    config-driven adapter resolution for factory run.

Notes for reviewers

  • Resolved: the import.meta.main entrypoint regression flagged in an earlier revision of this
    PR.
    This branch's merge-base conflict resolution against 33-parse-cli-with-effect-cli had
    silently reintroduced the pre-refactor(cli): replace hand-rolled parsers with effect/unstable/cli #52 hand-rolled USAGE/usageError/parseFlags/parseArgs/
    parseStartArgs/parseServeArgs argv parser into src/cli.ts's if (import.meta.main) block —
    the exact shape ADR 0009 §5 describes replacing — while src/cli-commands.ts's factoryCommand
    and src/cli-argv.test.ts kept passing in isolation, since neither drove the real entrypoint. CI
    stayed green while the shipped binary (bin/factory.js → src/cli.ts) silently lost generated
    help, typed flag validation, and --wizard/--completions. A follow-up commit
    (fix(cli): restore the effect/unstable/cli entrypoint after a bad merge) removed the hand-rolled
    parser, restored Command.run(factoryCommand, ...) as the entrypoint, kept this PR's actual new
    work (the ManagedRuntime composition root in runCli, wired so every command path still
    resolves the runtime from context), restored a silently-dropped prepareWorkspace flag on the
    direct-run path, and fixed cli-commands.ts's runCommand to stop overriding the config-resolved
    adapter. It also added tests in src/cli-argv.test.ts that exercise src/cli.ts's actual
    import.meta.main block (spawned-process checks against generated --help and typed --port
    validation, plus a static check for the hand-rolled parser's absence) so this class of regression
    — real code path drifting from what its own test suite drives — can't pass CI unnoticed again.
  • Two follow-up "fix" commits landed during review: one renamed the AgentSignal union's fields
    (kind/sessionId → _tag/value) for consistency across adapter/runtime/tests, and one
    restored ConcurrencyLimitError/DispatchCapError as Schema.TaggedError plus the
    domainErrorMessage translation in runtime/run.ts — both had briefly regressed to plain Error
    subclasses in the first commit's rewrite of server/runs.ts/runtime/run.ts. The final diff
    against base leaves those two classes and RunFailed.message formatting unchanged from what Represent domain failures as Schema.TaggedError #34
    already shipped.
  • ADR 0012 §4, ADR 0009 §5 and the ADR 0004 status note already describe this shape (merged ahead
    of implementation via Epic: lean on Effect where it earns it — CLI, agent-runtime seam, and a composition root #32's 8ec1ff9) and this PR touches no ADR files — the implementation
    matches the text as written.

Verification

  • bun run check (format:check + lint + typecheck + bun test) run locally, three clean runs in a
    row: 316 pass / 0 fail.
  • Exercised the real binary path (bin/factory.js → src/cli.ts's import.meta.main →
    Command.run(factoryCommand, ...)) by hand: factory --help and factory serve --help render
    effect/unstable/cli's generated help; factory init --dir <tmp> scaffolds .factory/;
    factory serve --port 0 boots the daemon and prints the assigned port; factory start <id> --input '{}' against $FACTORY_URL reaches it and surfaces the daemon's 404 hint; factory serve --port abc is rejected by the typed Int flag before the daemon starts.
  • New/updated tests: src/runtime/agent-runtime.test.ts pins the service resolving from context
    and swapping via AgentRuntimeLayer; src/runtime/run-signals.test.ts and the reworked
    src/replay/adapter.test.ts pin signal population end-to-end; src/runtime/agent-step.usage.test.ts
    keeps the four-component token accounting green through the new composition root;
    src/server/nested-runs.test.ts's depth-cap test exercises multi-level dispatch (root through
    several generations) with one shared runtime, covering grandchild inheritance;
    src/cli-argv.test.ts's new cli.ts entrypoint wiring block pins the real entrypoint against the
    regression described above.

Stack

  1. refactor(cli): replace hand-rolled parsers with effect/unstable/cli #52 33-parse-cli-with-effect-cli → main (issue Parse CLI arguments with effect/unstable/cli #33)
  2. refactor(errors): represent domain failures as Schema.TaggedError #53 34-domain-errors-as-tagged-errors (issue Represent domain failures as Schema.TaggedError #34)
  3. refactor(runtime): move chunk interpretation into the agent adapter #54 35-move-chunk-interpretation-into-adapter (issue Move chunk interpretation into the agent adapter #35)
  4. refactor(runtime): move headless permission setup into the agent adapter #55 37-move-headless-permissions (issue Move headless permission setup out of the workspace allocator #37)
  5. feat(runtime): add Effect composition root and agent runtime service #56 36-agent-runtime-service (issue Add an Effect composition root and make the agent runtime a service #36) — this PR
  6. refactor(daemon): move singletons into per-daemon layers #57 38-move-singletons-into-layers (issue Move the daemon's remaining singletons into layers #38)

Stack created with GitHub Stacks CLI • Give Feedback 💬

FreshlyBrewedCode and others added 5 commits September 23, 2026 06:58
…d to end

Run-level regression tests for issue #35: session/structured-output/
error signals land on AgentStepFinished.sessionId/.output/.error, and
the two-tier structured-output resolution (signal value, then finalText
re-parse) is unchanged.
closes #36

- Introduce AgentRuntime context service and AgentRuntimeLayer
- Resolve adapter from context in agent-step.ts instead of threading it
- Add agent.adapter to factory.config.ts with opencode default
- Pass ManagedRuntime through daemon/http/runs/scheduler/cli
- Remove adapter field from ServerOptions, StartTrackedRunOptions, DispatchEnv
- Update tests to provide agent runtime layer
Commit a507d1e's conflict resolution against 33-parse-cli-with-effect-cli
silently reverted PR #52: src/cli.ts's import.meta.main block regressed to
the pre-#52 hand-rolled USAGE/parseFlags/usageError/parseArgs parser, even
though src/cli-commands.ts's factoryCommand (the effect/unstable/cli command
tree) and its tests kept passing in isolation — so CI stayed green while the
shipped binary silently lost generated help, typed flag validation, and
--wizard/--completions.

Restore the base branch's entrypoint (import { factoryCommand } from
"./cli-commands"; Command.run(factoryCommand, ...)) while keeping this PR's
actual new work intact: the ManagedRuntime/AgentRuntimeLayer composition root
in runCli, which resolves the adapter from factory.config.ts (falling back to
opencodeAdapter) and threads a ManagedRuntime into startRun. Also restore
`prepareWorkspace: options.clone !== undefined`, which the same bad merge
had silently dropped from runCli's startRun call.

cli-commands.ts's runCommand was hardcoding `adapter: opencodeAdapter` on
every `factory run` invocation, which bypassed runCli's config-driven adapter
resolution and defeated issue #36's "runtime selectable from
factory.config.ts" criterion for the direct-run path. Drop that override so
runCli's own fallback (options.adapter ?? config.agent.adapter ??
opencodeAdapter) decides.

Add tests that exercise src/cli.ts's actual import.meta.main entrypoint (the
same path bin/factory.js runs in production), not just factoryCommand in
isolation: a static check that the file contains no hand-rolled parser, and
spawned-process checks that --help renders effect/unstable/cli's generated
help and that `serve --port abc` is rejected by the typed Int flag. Without
these, this class of regression can pass CI again undetected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Add an Effect composition root and make the agent runtime a service

1 participant