OpenCode server-API: fix agent→model binding, native fork & per-session diff#2188
OpenCode server-API: fix agent→model binding, native fork & per-session diff#2188shrimpwtf wants to merge 10 commits into
Conversation
|
| Filename | Overview |
|---|---|
| packages/server/src/server/agent/providers/opencode-agent.ts | Adds agent→model binding, native fork, and per-session diff for OpenCode. Core logic is sound, but setMode now makes a live network call to getAvailableModes() to resolve the bound model — if the OpenCode server is unavailable the mode switch fails entirely rather than degrading gracefully. |
| packages/server/src/server/agent/agent-manager.ts | Extracts registerImportedProviderSession shared between import and fork, and surfaces the re-bound model after a mode switch via getRuntimeInfo(). Refactor is clean; model surfacing is correct for OpenCode and a no-op for other providers. |
| packages/server/src/server/session.ts | Adds handleAgentForkRequest for the native-fork RPC path, mirroring the existing import pattern and correctly threading the parent agent label. |
| packages/server/src/server/agent/import-sessions.ts | Adds forkProviderSession as a thin orchestration wrapper mirroring importProviderSession. Consistent with existing pattern; no issues. |
| packages/server/src/server/agent/agent-sdk-types.ts | Adds fork/diff types and parentSessionId to ImportedProviderSession. parentSessionId is populated by OpenCode's fork response but never consumed in production code. |
| packages/protocol/src/messages.ts | Adds AgentForkRequest/Response message pair and opencodeSessionDiff/agentNativeFork capability flags. All additions are .optional() and backward-compatible. |
| packages/app/src/agent-stream/view.tsx | Routes OpenCode fork through the new native-fork RPC when agentNativeFork capability is present; all other providers fall through unchanged. |
| packages/client/src/daemon-client.ts | Adds forkAgent client method following the same request/response pattern as other RPC methods. No issues. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as view.tsx
participant DC as DaemonClient
participant Sess as session.ts
participant AM as AgentManager
participant OC as OpenCodeAgentClient
Note over UI,OC: Native Fork flow
UI->>DC: forkAgent(agentId, opts)
DC->>Sess: agent.fork.request
Sess->>AM: forkProviderSession(...)
AM->>OC: forkSession(providerHandleId, cwd)
OC->>OC: client.session.fork(sessionID, directory)
OC-->>AM: ImportedProviderSession
AM->>AM: registerImportedProviderSession(forked)
AM-->>Sess: ManagedAgent
Sess-->>DC: agent.fork.response
DC-->>UI: AgentSnapshotPayload
UI->>UI: navigateToAgent(forkedAgent)
Note over UI,OC: Agent-Model binding flow
UI->>DC: setAgentMode(agentId, modeId)
DC->>Sess: set_agent_mode_request
Sess->>AM: setAgentMode(agentId, modeId)
AM->>OC: session.setMode(modeId)
OC->>OC: getAvailableModes() [network call]
OC->>OC: applyAgentBoundModelForModeSelection
AM->>OC: session.getRuntimeInfo()
OC-->>AM: model bound to agent
AM->>AM: "agent.runtimeInfo.model = effectiveModel"
AM-->>Sess: emitState(agent)
Sess-->>DC: agent_state (model updated)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI as view.tsx
participant DC as DaemonClient
participant Sess as session.ts
participant AM as AgentManager
participant OC as OpenCodeAgentClient
Note over UI,OC: Native Fork flow
UI->>DC: forkAgent(agentId, opts)
DC->>Sess: agent.fork.request
Sess->>AM: forkProviderSession(...)
AM->>OC: forkSession(providerHandleId, cwd)
OC->>OC: client.session.fork(sessionID, directory)
OC-->>AM: ImportedProviderSession
AM->>AM: registerImportedProviderSession(forked)
AM-->>Sess: ManagedAgent
Sess-->>DC: agent.fork.response
DC-->>UI: AgentSnapshotPayload
UI->>UI: navigateToAgent(forkedAgent)
Note over UI,OC: Agent-Model binding flow
UI->>DC: setAgentMode(agentId, modeId)
DC->>Sess: set_agent_mode_request
Sess->>AM: setAgentMode(agentId, modeId)
AM->>OC: session.setMode(modeId)
OC->>OC: getAvailableModes() [network call]
OC->>OC: applyAgentBoundModelForModeSelection
AM->>OC: session.getRuntimeInfo()
OC-->>AM: model bound to agent
AM->>AM: "agent.runtimeInfo.model = effectiveModel"
AM-->>Sess: emitState(agent)
Sess-->>DC: agent_state (model updated)
Reviews (3): Last reviewed commit: "fix(opencode): address greptile review o..." | Re-trigger Greptile
Additive, protocol-safe field on AgentMode for per-mode model association. Includes both-direction wire-compat test. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
mapOpenCodeAgentToMode now carries each OpenCode agent's bound model as AgentMode.model (providerID/modelID). OpenCodeAgentSession applies the selected agent's bound model as the effective prompt model on setMode, lets an explicit setModel override win until the next switch, and re-applies the new agent's bound model across switches (atlas→kimi, sisyphus). Covered by unit tests using the fake OpenCode harness. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
setAgentMode now reads the session's resulting effective model via getRuntimeInfo() and surfaces it into agent.config.model and runtimeInfo.model, then emits state so the client model picker reflects the switch (e.g. OpenCode plugins binding atlas->kimi). Covered by an agent-manager test asserting runtimeInfo.model updates on a mode switch to an agent with a bound model. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
OpenCode persists 'Allow always' server-side through reply:"always" on permission.reply (there is no separate 'remember' boolean in the API, verified against @opencode-ai/sdk 1.14.46 and the OpenCode permissions docs). Document that intent in resolveOpenCodePermissionReply and add a real e2e asserting a remembered bash permission is not re-prompted on a subsequent matching call within the session. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Add a provider-scoped forkSession on OpenCodeAgentClient that calls the
native session.fork({ messageID }) endpoint, returning a real provider
child session that preserves provider-side history rather than
re-importing a text transcript. The forked session is tied to its parent
via parentSessionId. The generic client-side fork flow for other
providers is unchanged.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Add server_info.features.opencodeSessionDiff capability flag (schema + emit, dated COMPAT v0.2.0) gating an OpenCode per-session changed-files view. Wire OpenCodeAgentSession.diffSession() to the native session.diff() endpoint, mapping FileDiff entries to a provider-agnostic SessionChangedFile shape. Client detects the capability via a single-point gate and shows an update-host affordance when absent (no fallback path). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Wire the client Fork chat action into OpenCodeAgentClient.forkSession() for OpenCode agents. Add AgentManager.forkProviderSession (mirroring importProviderSession) plus an import-sessions orchestration wrapper that places the forked child in a workspace and registers it as a managed agent tied to its parent. Expose it over a new capability-gated agent.fork RPC and a DaemonClient.forkAgent method; the agent-stream fork handler uses native fork for OpenCode when the daemon advertises agentNativeFork and keeps the generic re-import flow for other providers. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…n diff Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Align the SDK with the installed opencode server binary (1.18.3), resolving version skew from 1.14.46. Wire-compatible: build, typecheck, lint, and the opencode-agent test suite (70 tests) all pass. The agent-name round-trip (GET /agent name -> prompt agent field) and the permission reply:"always" model are unchanged in 1.18.3. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Throw on diff entries missing a file path instead of silently emitting an empty path - Use the real OpenCode test model config in the permission-remember e2e instead of a placeholder Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
ef7db6b to
6f092ed
Compare
…ssion diff) Resolved view.tsx conflict by keeping both Codex rate-limit reset state and OpenCode agentNativeFork capability flag.
Bring in Add Transcripts UI, export RPC, draft transcript attachments, and docs. Kept OpenCode native fork (getpaseo#2188) handlers/types alongside transcript export.
|
Closing in favor of #2256, which splits out just the agent→model-on-mode-switch fix as a self-contained, provider-clean change: no |
Summary
Closes gaps in Paseo's OpenCode HTTP server-API integration. OpenCode already runs on the server API (not ACP) — this PR fixes the agent→model binding bug and lights up unused endpoints, all provider-scoped.
Headline fix: agent→model binding (atlas→kimi)
OpenCode's
GET /agentreturns each agent's boundmodel(e.g. oh-my-openagent bindsatlas→kimi). Paseo was discarding it, so switching agents never switched the model. Now:AgentModecarries an optionalmodel(providerID/modelID).AgentManager.setAgentMode()surfaces the re-bound model intoruntimeInfo.modelso the composer picker updates.Also included
reply:"always"(session-scoped); no nonexistentrememberflag. Added a CI-gated e2e.session.fork()for OpenCode (real provider child, history preserved), gated by anagentNativeForkcapability. Other providers keep the generic flow.session.diff()mapped to a provider-agnostic changed-files shape, gated by anopencodeSessionDiffcapability with a client update-host affordance (no fallback path).Investigated / deferred (see mission backlog)
find*), agent-context shell, and user-facing diff/search UI panels were evaluated and deferred (duplicates or need a larger UI subsystem).SECURITY.mdpolicy decision ("Paseo never stores or transmits provider API keys").Compatibility
.optional(); new capabilities gated inserver_info.features.*with datedCOMPAT()tags (v0.2.0). No shared ACP files (copilot/cursor/kiro/trae/generic) were behavior-changed.Testing
daemon-e2e/opencode-*.real.e2e.test.ts.npm run typecheck+ targeted lint green. Full suite intentionally not run locally (per repo rules) — relies on CI.