V2 must not assume every provider can do what Codex can do. Provider behavior should be expressed through capabilities and policies, not provider-name conditionals spread across orchestration.
| 1 | type ProviderCapabilities = { |
| 2 | sessions: SessionCapabilities; |
| 3 | threads: ThreadCapabilities; |
| 4 | turns: TurnCapabilities; |
| 5 | streaming: StreamingCapabilities; |
| 6 | tools: ToolCapabilities; |
| 7 | approvals: ApprovalCapabilities; |
| 8 | planning: PlanningCapabilities; |
| 9 | subagents: SubagentCapabilities; |
| 10 | context: ContextCapabilities; |
| 11 | checkpointing: CheckpointCapabilities; |
| 12 | identity: IdentityCapabilities; |
| 13 | }; |
Capabilities should be versioned and emitted by each adapter at session start.
| 1 | type SessionCapabilities = { |
| 2 | supportsMultipleProviderThreadsPerSession: boolean; |
| 3 | supportsModelSwitchInSession: boolean; |
| 4 | supportsProviderSwitchingViaHandoff: boolean; |
| 5 | supportsRuntimeModeSwitchInSession: boolean; |
| 6 | pendingRequestsSurviveRestart: boolean; |
| 7 | }; |
Policy examples:
not_resumable.Provider-thread resumption is not a capability. It is a required adapter primitive. Adapters must be able to start from a stored provider cursor/session/thread handle or return a runtime resume failure.
| 1 | type ThreadCapabilities = { |
| 2 | canCreateEmptyThread: boolean; |
| 3 | canReadThreadSnapshot: boolean; |
| 4 | canRollbackThread: boolean; |
| 5 | canForkThread: boolean; |
| 6 | canForkFromTurn: boolean; |
| 7 | canForkFromSubagentThread: boolean; |
| 8 | exposesNativeThreadId: boolean; |
| 9 | }; |
Codex can expose strong thread ids and rollback snapshots. Claude may support native forking through its own model/session primitives. Other providers may only support synthetic app forks.
| 1 | type TurnCapabilities = { |
| 2 | exposesNativeTurnId: boolean; |
| 3 | emitsTurnStarted: boolean; |
| 4 | emitsTurnCompleted: boolean; |
| 5 | supportsInterrupt: boolean; |
| 6 | supportsActiveSteering: boolean; |
| 7 | supportsSteeringByInterruptRestart: boolean; |
| 8 | supportsQueuedMessages: boolean; |
| 9 | terminalStatusQuality: "strong" | "weak" | "none"; |
| 10 | }; |
If terminalStatusQuality is weak, V2 should use adapter policy to infer terminal state, but still mark correlation strength accordingly.
supportsActiveSteering means the provider can modify an in-flight turn directly. supportsSteeringByInterruptRestart means V2 can implement app-level steering by interrupting the active turn and starting a replacement attempt. Most providers should support the latter if they support interruption and normal follow-up turns.
| 1 | type StreamingCapabilities = { |
| 2 | streamsAssistantText: boolean; |
| 3 | streamsReasoning: boolean; |
| 4 | streamsToolOutput: boolean; |
| 5 | streamsPlanText: boolean; |
| 6 | emitsMessageCompleted: boolean; |
| 7 | }; |
If message completion is not emitted, the normalizer closes assistant messages at root run terminal.
| 1 | type ToolCapabilities = { |
| 2 | exposesToolItemIds: boolean; |
| 3 | emitsToolStarted: boolean; |
| 4 | emitsToolCompleted: boolean; |
| 5 | emitsToolOutput: boolean; |
| 6 | supportsMcpTools: boolean; |
| 7 | supportsDynamicToolCallbacks: boolean; |
| 8 | }; |
Weak providers may produce only textual tool summaries. The adapter can still create ordered turnItems and execution nodes by scoped ordinal.
| 1 | type ApprovalCapabilities = { |
| 2 | supportsCommandApproval: boolean; |
| 3 | supportsFileReadApproval: boolean; |
| 4 | supportsFileChangeApproval: boolean; |
| 5 | supportsApplyPatchApproval: boolean; |
| 6 | approvalsHaveNativeRequestIds: boolean; |
| 7 | approvalCallbacksAreLiveOnly: boolean; |
| 8 | approvalsCanOriginateFromSubagents: boolean; |
| 9 | }; |
The UI should show pending approvals from any execution node, including subagents, if they are respondable.
| 1 | type PlanningCapabilities = { |
| 2 | emitsPlanUpdated: boolean; |
| 3 | emitsTodoList: boolean; |
| 4 | emitsProposedPlan: boolean; |
| 5 | supportsStructuredQuestions: boolean; |
| 6 | planDeltasHaveItemIds: boolean; |
| 7 | }; |
Mapping policy:
emitsTodoList: update live run progress.emitsProposedPlan: create accept/implementable plan artifacts.supportsStructuredQuestions: create respondable user-input requests.If a provider only emits plain assistant text, the app should not pretend it has structured plans.
| 1 | type SubagentCapabilities = { |
| 2 | supportsSubagents: boolean; |
| 3 | exposesSubagentThreadIds: boolean; |
| 4 | emitsSubagentLifecycle: boolean; |
| 5 | canWaitForSubagents: boolean; |
| 6 | canCloseSubagents: boolean; |
| 7 | canForkSubagentThread: boolean; |
| 8 | }; |
When exposesSubagentThreadIds is true, subagent provider threads become addressable ProviderThread records. When false, subagents can still be shown as nested execution nodes if the provider exposes enough lifecycle information.
Context handoff controls provider switching and provider-thread reconstruction.
| 1 | type ContextCapabilities = { |
| 2 | acceptsSystemContext: boolean; |
| 3 | acceptsDeveloperContext: boolean; |
| 4 | acceptsSyntheticUserContext: boolean; |
| 5 | canGenerateSummaries: boolean; |
| 6 | canConsumeHandoffSummaries: boolean; |
| 7 | supportsDeltaHandoff: boolean; |
| 8 | supportsFullThreadHandoff: boolean; |
| 9 | maxRecommendedHandoffChars: number | null; |
| 10 | }; |
Policy examples:
supportsDeltaHandoff is available, return to a previous provider thread with a summary of only off-provider runs.Checkpointing is primarily app-owned, but provider conversation rollback is provider-dependent.
| 1 | type CheckpointCapabilities = { |
| 2 | appCanCheckpointFilesystem: boolean; |
| 3 | supportsNestedCheckpointScopes: boolean; |
| 4 | providerCanRollbackConversation: boolean; |
| 5 | providerRollbackReturnsSnapshot: boolean; |
| 6 | providerCanReadConversationSnapshot: boolean; |
| 7 | }; |
If provider rollback is unsupported, filesystem rollback can still happen, but provider conversation state must be restarted or marked divergent. Nested checkpoint scopes are app-owned; providers only affect whether their nested provider conversations can be rolled back to match restored filesystem state.
| 1 | type IdentityCapabilities = { |
| 2 | nativeThreadIds: "strong" | "weak" | "none"; |
| 3 | nativeTurnIds: "strong" | "weak" | "none"; |
| 4 | nativeItemIds: "strong" | "weak" | "none"; |
| 5 | nativeRequestIds: "strong" | "weak" | "none"; |
| 6 | }; |
This controls how the normalizer correlates events:
strong: use native id as scoped provider ref.weak: use native id plus ordinal/fingerprint.none: allocate by scoped ordinal.Every feature should declare what happens when capability is missing.
Examples:
| 1 | interrupt unsupported |
| 2 | -> stop session if allowed, otherwise mark unsupported |
| 3 | |
| 4 | active steering unsupported |
| 5 | -> interrupt active turn and restart the run as a steering replacement attempt |
| 6 | |
| 7 | fork unsupported |
| 8 | -> synthetic fork from app projection if policy allows |
| 9 | |
| 10 | rollback unsupported |
| 11 | -> restore filesystem checkpoint, restart provider context, mark provider state divergent |
| 12 | |
| 13 | nested checkpoint unsupported |
| 14 | -> capture only root-run checkpoint and record child filesystem activity as uncheckpointed |
| 15 | |
| 16 | provider switch return unsupported |
| 17 | -> create fresh provider thread with full app-thread summary |
| 18 | |
| 19 | structured approvals unsupported |
| 20 | -> provider runs under configured sandbox policy; no approval UI |
| 21 | |
| 22 | plan_updated unsupported |
| 23 | -> no live todo UI; rely on assistant messages |
The UI should not hide unsupported behavior behind provider-specific errors. It should receive typed capability results.
Each provider adapter should expose:
| 1 | type ProviderAdapter = { |
| 2 | getCapabilities(): ProviderCapabilities; |
| 3 | startSession(input): ProviderSession; |
| 4 | ensureProviderThread(input): ProviderThread; |
| 5 | sendRun(input): ProviderTurnStartResult; |
| 6 | steerRun?(input): void; |
| 7 | interrupt(input): void; |
| 8 | respondToRequest(input): void; |
| 9 | readThreadSnapshot?(input): ProviderThreadSnapshot; |
| 10 | rollbackThread?(input): ProviderThreadSnapshot | void; |
| 11 | forkThread?(input): ProviderThread; |
| 12 | streamEvents(): Stream<ProviderAdapterEvent>; |
| 13 | }; |
Optional methods are guarded by capabilities. The orchestration layer should not call optional methods without checking capability or going through a policy wrapper.
Raw provider frames should be logged by the provider transport/runtime as bounded diagnostics. Adapter streams should expose normalized provider events that the V2 normalizer can turn into app orchestration events.
The UI should receive capability-informed affordances:
This keeps the product predictable across Codex, Claude, Cursor, OpenCode, and future providers.