Skip to content

refactor(run): unify local/remote dispatch via Backend (10 baby steps)#2715

Merged
dgageot merged 10 commits into
docker:mainfrom
dgageot:remote-runtime-baby-steps
May 9, 2026
Merged

refactor(run): unify local/remote dispatch via Backend (10 baby steps)#2715
dgageot merged 10 commits into
docker:mainfrom
dgageot:remote-runtime-baby-steps

Conversation

@dgageot
Copy link
Copy Markdown
Member

@dgageot dgageot commented May 8, 2026

Why

docker agent run has two parallel code paths — a long-form local one and a remote shortcut that branches early in runOrExec. The shortcut bypasses team load options, RuntimeConfig, buildSessionOpts, the session spawner, and several optional sub-interfaces, so a long list of features silently break under --remote: --model, --prompt-file, --session-db, --session, --record, --fake, hooks, --working-dir, --hide-tool-results, max iterations, snapshots, model switching, pause, /tools, /sessions, /skills, MCP prompts, compaction, title regeneration, …

Rather than rewriting both paths in one go, this PR lays the groundwork: ten small, individually-revertible commits that make the eventual unification a mechanical change. No user-visible behaviour changes except clearer error messages and four flag-combo rejections that previously did nothing.

What

Ten commits, each shippable on its own:

  1. feat(runtime): introduce ErrUnsupported sentinel for remote runtime gaps — typed sentinel so silent nil/empty returns become explicit.
  2. feat(run): reject --remote combined with silently-broken flagsMarkFlagsMutuallyExclusive for --sandbox, --session-db, --session, --record, --fake. Stops the bleeding.
  3. fix(run): honour --dry-run before contacting the remote server--dry-run --remote no longer creates a server-side session before exiting.
  4. refactor(run): extract teamOpts and runtimeOpts helpers — pure tidy-first; preps the typed payload.
  5. refactor(run): introduce backend interface with localBackend impl — adds the seam; one implementation today.
  6. refactor(run): collapse local/remote branches via shared backendselectBackend + linear runOrExec. launchTUI disappears.
  7. refactor(runtime): centralize optional-capability checksPauserOf, ModelSwitcherOf, ToolsChangeSubscriberOf replace scattered type assertions.
  8. test(runtime): add backend-agnostic Runtime contract test — pins what Runtime means; runs against LocalRuntime today.
  9. refactor(runtime): promote TogglePause onto the Runtime interface — first sub-interface promotion. Template for the others.
  10. refactor(run): introduce LoadTeamRequest and CreateSessionRequest — typed config payloads, ready to travel over the wire.

After these ten commits, --remote is a Backend selector instead of a parallel universe, and adding new wire-protocol endpoints, file uploads, etc. becomes an isolated change against a stable spine.

What this PR does not do

  • It does not extend the wire protocol.
  • It does not add file uploads, a server-side shell endpoint, or a ServerPolicy.
  • It does not promote the remaining optional sub-interfaces (ModelSwitcher, snapshots, ToolsChangeSubscriber) onto Runtime — commit 9 establishes the pattern; the rest are mechanical follow-ups.

Each of the above is its own PR.

Validation

  • task build
  • task lint
  • task test

@dgageot dgageot requested a review from a team as a code owner May 8, 2026 17:51
@docker-agent
Copy link
Copy Markdown

PR Review Failed — The review agent encountered an error and could not complete the review. View logs.

@rumpl
Copy link
Copy Markdown
Member

rumpl commented May 9, 2026

This PR has merge conflicts with main. Could you rebase? Happy to review once it's clean.

dgageot added 10 commits May 9, 2026 09:00
Add a typed sentinel so callers can distinguish 'not supported by this
runtime' from other failures via errors.Is. Wrap the existing ad-hoc
strings in RemoteRuntime.RestartToolset and ExecuteMCPPrompt with %w so
the existing user-facing messages stay unchanged.
--sandbox, --session-db, --session, --record and --fake all silently
do nothing when paired with --remote because the remote branch in
runOrExec bypasses the code that would honour them. Reject these
combinations at parse time so users get a clear error instead of
guessing why their flag had no effect.
The remote branch in runOrExec called createRemoteRuntimeAndSession
(which POSTs /api/sessions to the server) before launchTUI's dry-run
check fired, so --dry-run --remote leaked a server-side session.

Move the check ahead of the network call and drop the now-redundant
copy from launchTUI.
Pull the []teamloader.Opt and []runtime.Opt construction into named
methods on runExecFlags. Both blocks were duplicated between
createLocalRuntimeAndSession and createSessionSpawner; the dedup is
mechanical and prepares the way for a typed configuration payload
that can travel to a remote backend.
Move the team load + local runtime/session construction (and the
runtime/toolset cleanup) behind a small backend abstraction. The
local code path now goes through it; the remote branch is unchanged.

The interface has one implementation today; the next step adds a
remoteBackend so runOrExec can collapse to a single linear flow.
remoteBackend wraps the previous createRemoteRuntimeAndSession; the
two-branch dispatch in runOrExec collapses to a single linear flow:
selectBackend → CreateRuntimeAndSession → handleExecMode|runTUI.

The launchTUI helper that existed only because the remote branch
couldn't share the local tail goes away.
Replace the scattered "interface{ TogglePause() bool }" and
"rt.(runtime.ModelSwitcher)" assertions with typed accessors
(PauserOf, ModelSwitcherOf, ToolsChangeSubscriberOf) in the runtime
package. The shape is the same but each capability is named, and a
future commit that promotes one of them onto the Runtime interface
only needs to delete the helper rather than chase down call sites.
runRuntimeContract exercises every non-streaming method on the
Runtime interface (CurrentAgentName, SetCurrentAgent, CurrentAgentTools,
RestartToolset, MCP prompts, Steer/FollowUp, Resume, Close, ...) and
runs against any factory.

Wired up against LocalRuntime today; future commits that promote
optional capabilities onto the interface and add a real RemoteRuntime
implementation will plug into the same suite to enforce parity.
TogglePause(ctx) (paused, err) is now part of Runtime. LocalRuntime
delegates to the existing implementation; RemoteRuntime returns
ErrUnsupported for now. App.TogglePause keys off the typed error
instead of a type assertion, and the optional Pauser interface goes
away.

Pattern for the remaining optional sub-interfaces (model switching,
snapshots, ...): each one becomes a copy of this commit.
Replace the scatter of f.modelOverrides / f.promptFiles / f.runConfig
and f.agentName / f.autoApprove / f.hideToolResults / f.sessionID /
f.snapshotsEnabled / f.globalPermissions arguments with two typed
payloads.

LocalBackend.CreateRuntimeAndSession builds them from the flags and
hands them to loadAgentFrom and createLocalRuntimeAndSession. The
field set is the data shape a future RemoteBackend will marshal over
the wire so the server runs teamloader.LoadWithConfig and session.New
with identical inputs to today's local code path.
@dgageot dgageot force-pushed the remote-runtime-baby-steps branch from d657f63 to 6c65b31 Compare May 9, 2026 07:04
Copy link
Copy Markdown
Member

@rumpl rumpl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved.

@dgageot dgageot merged commit 4d9cc48 into docker:main May 9, 2026
5 checks passed
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