| 1 | # Provider Capability System |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | ## Capability Shape |
| 6 | |
| 7 | ```ts |
| 8 | type ProviderCapabilities = { |
| 9 | sessions: SessionCapabilities; |
| 10 | threads: ThreadCapabilities; |
| 11 | turns: TurnCapabilities; |
| 12 | streaming: StreamingCapabilities; |
| 13 | tools: ToolCapabilities; |
| 14 | approvals: ApprovalCapabilities; |
| 15 | planning: PlanningCapabilities; |
| 16 | subagents: SubagentCapabilities; |
| 17 | context: ContextCapabilities; |
| 18 | checkpointing: CheckpointCapabilities; |
| 19 | identity: IdentityCapabilities; |
| 20 | }; |
| 21 | ``` |
| 22 | |
| 23 | Capabilities should be versioned and emitted by each adapter at session start. |
| 24 | |
| 25 | ## Session Capabilities |
| 26 | |
| 27 | ```ts |
| 28 | type SessionCapabilities = { |
| 29 | supportsMultipleProviderThreadsPerSession: boolean; |
| 30 | supportsModelSwitchInSession: boolean; |
| 31 | supportsProviderSwitchingViaHandoff: boolean; |
| 32 | supportsRuntimeModeSwitchInSession: boolean; |
| 33 | pendingRequestsSurviveRestart: boolean; |
| 34 | }; |
| 35 | ``` |
| 36 | |
| 37 | Policy examples: |
| 38 | |
| 39 | - If model switching is unsupported, start a new provider session. |
| 40 | - If pending requests do not survive restart, mark old requests `not_resumable`. |
| 41 | |
| 42 | 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. |
| 43 | |
| 44 | ## Thread Capabilities |
| 45 | |
| 46 | ```ts |
| 47 | type ThreadCapabilities = { |
| 48 | canCreateEmptyThread: boolean; |
| 49 | canReadThreadSnapshot: boolean; |
| 50 | canRollbackThread: boolean; |
| 51 | canForkThread: boolean; |
| 52 | canForkFromTurn: boolean; |
| 53 | canForkFromSubagentThread: boolean; |
| 54 | exposesNativeThreadId: boolean; |
| 55 | }; |
| 56 | ``` |
| 57 | |
| 58 | 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. |
| 59 | |
| 60 | ## Turn Capabilities |
| 61 | |
| 62 | ```ts |
| 63 | type TurnCapabilities = { |
| 64 | exposesNativeTurnId: boolean; |
| 65 | emitsTurnStarted: boolean; |
| 66 | emitsTurnCompleted: boolean; |
| 67 | supportsInterrupt: boolean; |
| 68 | supportsActiveSteering: boolean; |
| 69 | supportsSteeringByInterruptRestart: boolean; |
| 70 | supportsQueuedMessages: boolean; |
| 71 | terminalStatusQuality: "strong" | "weak" | "none"; |
| 72 | }; |
| 73 | ``` |
| 74 | |
| 75 | If `terminalStatusQuality` is weak, V2 should use adapter policy to infer terminal state, but still mark correlation strength accordingly. |
| 76 | |
| 77 | `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. |
| 78 | |
| 79 | ## Streaming Capabilities |
| 80 | |
| 81 | ```ts |
| 82 | type StreamingCapabilities = { |
| 83 | streamsAssistantText: boolean; |
| 84 | streamsReasoning: boolean; |
| 85 | streamsToolOutput: boolean; |
| 86 | streamsPlanText: boolean; |
| 87 | emitsMessageCompleted: boolean; |
| 88 | }; |
| 89 | ``` |
| 90 | |
| 91 | If message completion is not emitted, the normalizer closes assistant messages at root run terminal. |
| 92 | |
| 93 | ## Tool Capabilities |
| 94 | |
| 95 | ```ts |
| 96 | type ToolCapabilities = { |
| 97 | exposesToolItemIds: boolean; |
| 98 | emitsToolStarted: boolean; |
| 99 | emitsToolCompleted: boolean; |
| 100 | emitsToolOutput: boolean; |
| 101 | supportsMcpTools: boolean; |
| 102 | supportsDynamicToolCallbacks: boolean; |
| 103 | }; |
| 104 | ``` |
| 105 | |
| 106 | Weak providers may produce only textual tool summaries. The adapter can still create ordered `turnItems` and execution nodes by scoped ordinal. |
| 107 | |
| 108 | ## Approval Capabilities |
| 109 | |
| 110 | ```ts |
| 111 | type ApprovalCapabilities = { |
| 112 | supportsCommandApproval: boolean; |
| 113 | supportsFileReadApproval: boolean; |
| 114 | supportsFileChangeApproval: boolean; |
| 115 | supportsApplyPatchApproval: boolean; |
| 116 | approvalsHaveNativeRequestIds: boolean; |
| 117 | approvalCallbacksAreLiveOnly: boolean; |
| 118 | approvalsCanOriginateFromSubagents: boolean; |
| 119 | }; |
| 120 | ``` |
| 121 | |
| 122 | The UI should show pending approvals from any execution node, including subagents, if they are respondable. |
| 123 | |
| 124 | ## Planning Capabilities |
| 125 | |
| 126 | ```ts |
| 127 | type PlanningCapabilities = { |
| 128 | emitsPlanUpdated: boolean; |
| 129 | emitsTodoList: boolean; |
| 130 | emitsProposedPlan: boolean; |
| 131 | supportsStructuredQuestions: boolean; |
| 132 | planDeltasHaveItemIds: boolean; |
| 133 | }; |
| 134 | ``` |
| 135 | |
| 136 | Mapping policy: |
| 137 | |
| 138 | - `emitsTodoList`: update live run progress. |
| 139 | - `emitsProposedPlan`: create accept/implementable plan artifacts. |
| 140 | - `supportsStructuredQuestions`: create respondable user-input requests. |
| 141 | |
| 142 | If a provider only emits plain assistant text, the app should not pretend it has structured plans. |
| 143 | |
| 144 | ## Subagent Capabilities |
| 145 | |
| 146 | ```ts |
| 147 | type SubagentCapabilities = { |
| 148 | supportsSubagents: boolean; |
| 149 | exposesSubagentThreadIds: boolean; |
| 150 | emitsSubagentLifecycle: boolean; |
| 151 | canWaitForSubagents: boolean; |
| 152 | canCloseSubagents: boolean; |
| 153 | canForkSubagentThread: boolean; |
| 154 | }; |
| 155 | ``` |
| 156 | |
| 157 | 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. |
| 158 | |
| 159 | ## Context Handoff Capabilities |
| 160 | |
| 161 | Context handoff controls provider switching and provider-thread reconstruction. |
| 162 | |
| 163 | ```ts |
| 164 | type ContextCapabilities = { |
| 165 | acceptsSystemContext: boolean; |
| 166 | acceptsDeveloperContext: boolean; |
| 167 | acceptsSyntheticUserContext: boolean; |
| 168 | canGenerateSummaries: boolean; |
| 169 | canConsumeHandoffSummaries: boolean; |
| 170 | supportsDeltaHandoff: boolean; |
| 171 | supportsFullThreadHandoff: boolean; |
| 172 | maxRecommendedHandoffChars: number | null; |
| 173 | }; |
| 174 | ``` |
| 175 | |
| 176 | Policy examples: |
| 177 | |
| 178 | - If `supportsDeltaHandoff` is available, return to a previous provider thread with a summary of only off-provider runs. |
| 179 | - If delta handoff is unsupported or low quality, create a new provider thread with a full thread summary. |
| 180 | - If the provider cannot accept explicit context, provider switching should require synthetic user context or be marked unsupported. |
| 181 | - If no provider can generate summaries, use the app's configured summarization provider or a local summarizer capability. |
| 182 | |
| 183 | ## Checkpoint Capabilities |
| 184 | |
| 185 | Checkpointing is primarily app-owned, but provider conversation rollback is provider-dependent. |
| 186 | |
| 187 | ```ts |
| 188 | type CheckpointCapabilities = { |
| 189 | appCanCheckpointFilesystem: boolean; |
| 190 | supportsNestedCheckpointScopes: boolean; |
| 191 | providerCanRollbackConversation: boolean; |
| 192 | providerRollbackReturnsSnapshot: boolean; |
| 193 | providerCanReadConversationSnapshot: boolean; |
| 194 | }; |
| 195 | ``` |
| 196 | |
| 197 | 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. |
| 198 | |
| 199 | ## Identity Capabilities |
| 200 | |
| 201 | ```ts |
| 202 | type IdentityCapabilities = { |
| 203 | nativeThreadIds: "strong" | "weak" | "none"; |
| 204 | nativeTurnIds: "strong" | "weak" | "none"; |
| 205 | nativeItemIds: "strong" | "weak" | "none"; |
| 206 | nativeRequestIds: "strong" | "weak" | "none"; |
| 207 | }; |
| 208 | ``` |
| 209 | |
| 210 | This controls how the normalizer correlates events: |
| 211 | |
| 212 | - `strong`: use native id as scoped provider ref. |
| 213 | - `weak`: use native id plus ordinal/fingerprint. |
| 214 | - `none`: allocate by scoped ordinal. |
| 215 | |
| 216 | ## Degradation Policies |
| 217 | |
| 218 | Every feature should declare what happens when capability is missing. |
| 219 | |
| 220 | Examples: |
| 221 | |
| 222 | ```text |
| 223 | interrupt unsupported |
| 224 | -> stop session if allowed, otherwise mark unsupported |
| 225 | |
| 226 | active steering unsupported |
| 227 | -> interrupt active turn and restart the run as a steering replacement attempt |
| 228 | |
| 229 | fork unsupported |
| 230 | -> synthetic fork from app projection if policy allows |
| 231 | |
| 232 | rollback unsupported |
| 233 | -> restore filesystem checkpoint, restart provider context, mark provider state divergent |
| 234 | |
| 235 | nested checkpoint unsupported |
| 236 | -> capture only root-run checkpoint and record child filesystem activity as uncheckpointed |
| 237 | |
| 238 | provider switch return unsupported |
| 239 | -> create fresh provider thread with full app-thread summary |
| 240 | |
| 241 | structured approvals unsupported |
| 242 | -> provider runs under configured sandbox policy; no approval UI |
| 243 | |
| 244 | plan_updated unsupported |
| 245 | -> no live todo UI; rely on assistant messages |
| 246 | ``` |
| 247 | |
| 248 | The UI should not hide unsupported behavior behind provider-specific errors. It should receive typed capability results. |
| 249 | |
| 250 | ## Adapter Contract |
| 251 | |
| 252 | Each provider adapter should expose: |
| 253 | |
| 254 | ```ts |
| 255 | type ProviderAdapter = { |
| 256 | getCapabilities(): ProviderCapabilities; |
| 257 | startSession(input): ProviderSession; |
| 258 | ensureProviderThread(input): ProviderThread; |
| 259 | sendRun(input): ProviderTurnStartResult; |
| 260 | steerRun?(input): void; |
| 261 | interrupt(input): void; |
| 262 | respondToRequest(input): void; |
| 263 | readThreadSnapshot?(input): ProviderThreadSnapshot; |
| 264 | rollbackThread?(input): ProviderThreadSnapshot | void; |
| 265 | forkThread?(input): ProviderThread; |
| 266 | streamEvents(): Stream<ProviderAdapterEvent>; |
| 267 | }; |
| 268 | ``` |
| 269 | |
| 270 | Optional methods are guarded by capabilities. The orchestration layer should not call optional methods without checking capability or going through a policy wrapper. |
| 271 | |
| 272 | 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. |
| 273 | |
| 274 | ## Capability-Driven UI |
| 275 | |
| 276 | The UI should receive capability-informed affordances: |
| 277 | |
| 278 | - show or hide fork action by fork source. |
| 279 | - show interrupt as available, destructive fallback, or unavailable. |
| 280 | - show approvals as respondable or expired. |
| 281 | - show plan/todo panels only when structured plan artifacts exist. |
| 282 | - show rollback only when app checkpoint exists, and annotate provider rollback support. |
| 283 | |
| 284 | This keeps the product predictable across Codex, Claude, Cursor, OpenCode, and future providers. |