| author | |
| committer | |
| log | 5bfd0598e763ba8221ae90b6b68524391a8492a0 |
| tree | 72028c083e0f5f5031273c87e97685765f084892 |
| parent | 060a8cef00dee1728be2fa7cd96ad0332ad15353 |
5 files changed, 721 insertions(+), 0 deletions(-)
apps/server/src/provider/Errors.ts created+282| ... | @@ -0,0 +1,282 @@ | ||
| 1 | import { Schema } from "effect"; | ||
| 2 | |||
| 3 | /** | ||
| 4 | * CheckpointValidationError - Invalid checkpoint API input. | ||
| 5 | */ | ||
| 6 | export class CheckpointValidationError extends Schema.TaggedErrorClass<CheckpointValidationError>()( | ||
| 7 | "CheckpointValidationError", | ||
| 8 | { | ||
| 9 | operation: Schema.String, | ||
| 10 | issue: Schema.String, | ||
| 11 | cause: Schema.optional(Schema.Defect), | ||
| 12 | }, | ||
| 13 | ) { | ||
| 14 | override get message(): string { | ||
| 15 | return `Checkpoint validation failed in ${this.operation}: ${this.issue}`; | ||
| 16 | } | ||
| 17 | } | ||
| 18 | |||
| 19 | /** | ||
| 20 | * CheckpointGitCommandError - Git command execution failed in checkpoint store. | ||
| 21 | */ | ||
| 22 | export class CheckpointGitCommandError extends Schema.TaggedErrorClass<CheckpointGitCommandError>()( | ||
| 23 | "CheckpointGitCommandError", | ||
| 24 | { | ||
| 25 | operation: Schema.String, | ||
| 26 | command: Schema.String, | ||
| 27 | cwd: Schema.String, | ||
| 28 | detail: Schema.String, | ||
| 29 | cause: Schema.optional(Schema.Defect), | ||
| 30 | }, | ||
| 31 | ) { | ||
| 32 | override get message(): string { | ||
| 33 | return `Checkpoint git command failed in ${this.operation}: ${this.command} (${this.cwd}) - ${this.detail}`; | ||
| 34 | } | ||
| 35 | } | ||
| 36 | |||
| 37 | /** | ||
| 38 | * CheckpointUnavailableError - Expected checkpoint does not exist. | ||
| 39 | */ | ||
| 40 | export class CheckpointUnavailableError extends Schema.TaggedErrorClass<CheckpointUnavailableError>()( | ||
| 41 | "CheckpointUnavailableError", | ||
| 42 | { | ||
| 43 | threadId: Schema.String, | ||
| 44 | turnCount: Schema.Number, | ||
| 45 | detail: Schema.String, | ||
| 46 | cause: Schema.optional(Schema.Defect), | ||
| 47 | }, | ||
| 48 | ) { | ||
| 49 | override get message(): string { | ||
| 50 | return `Checkpoint unavailable for thread ${this.threadId} turn ${this.turnCount}: ${this.detail}`; | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | /** | ||
| 55 | * CheckpointRepositoryError - Checkpointing unavailable for cwd/repository. | ||
| 56 | */ | ||
| 57 | export class CheckpointRepositoryError extends Schema.TaggedErrorClass<CheckpointRepositoryError>()( | ||
| 58 | "CheckpointRepositoryError", | ||
| 59 | { | ||
| 60 | cwd: Schema.String, | ||
| 61 | detail: Schema.String, | ||
| 62 | cause: Schema.optional(Schema.Defect), | ||
| 63 | }, | ||
| 64 | ) { | ||
| 65 | override get message(): string { | ||
| 66 | return `Checkpoint repository error for ${this.cwd}: ${this.detail}`; | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | /** | ||
| 71 | * CodexValidationError - Invalid Codex API input. | ||
| 72 | */ | ||
| 73 | export class CodexValidationError extends Schema.TaggedErrorClass<CodexValidationError>()( | ||
| 74 | "CodexValidationError", | ||
| 75 | { | ||
| 76 | operation: Schema.String, | ||
| 77 | issue: Schema.String, | ||
| 78 | cause: Schema.optional(Schema.Defect), | ||
| 79 | }, | ||
| 80 | ) { | ||
| 81 | override get message(): string { | ||
| 82 | return `Codex validation failed in ${this.operation}: ${this.issue}`; | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | /** | ||
| 87 | * CodexSessionNotFoundError - Session id is unknown. | ||
| 88 | */ | ||
| 89 | export class CodexSessionNotFoundError extends Schema.TaggedErrorClass<CodexSessionNotFoundError>()( | ||
| 90 | "CodexSessionNotFoundError", | ||
| 91 | { | ||
| 92 | sessionId: Schema.String, | ||
| 93 | cause: Schema.optional(Schema.Defect), | ||
| 94 | }, | ||
| 95 | ) { | ||
| 96 | override get message(): string { | ||
| 97 | return `Unknown codex session: ${this.sessionId}`; | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | /** | ||
| 102 | * CodexSessionClosedError - Session exists but is closed. | ||
| 103 | */ | ||
| 104 | export class CodexSessionClosedError extends Schema.TaggedErrorClass<CodexSessionClosedError>()( | ||
| 105 | "CodexSessionClosedError", | ||
| 106 | { | ||
| 107 | sessionId: Schema.String, | ||
| 108 | cause: Schema.optional(Schema.Defect), | ||
| 109 | }, | ||
| 110 | ) { | ||
| 111 | override get message(): string { | ||
| 112 | return `Codex session is closed: ${this.sessionId}`; | ||
| 113 | } | ||
| 114 | } | ||
| 115 | |||
| 116 | /** | ||
| 117 | * CodexProtocolError - Invalid/unexpected JSON-RPC payload from app-server. | ||
| 118 | */ | ||
| 119 | export class CodexProtocolError extends Schema.TaggedErrorClass<CodexProtocolError>()( | ||
| 120 | "CodexProtocolError", | ||
| 121 | { | ||
| 122 | operation: Schema.String, | ||
| 123 | detail: Schema.String, | ||
| 124 | cause: Schema.optional(Schema.Defect), | ||
| 125 | }, | ||
| 126 | ) { | ||
| 127 | override get message(): string { | ||
| 128 | return `Codex protocol error in ${this.operation}: ${this.detail}`; | ||
| 129 | } | ||
| 130 | } | ||
| 131 | |||
| 132 | /** | ||
| 133 | * CodexRequestError - JSON-RPC request failed or timed out. | ||
| 134 | */ | ||
| 135 | export class CodexRequestError extends Schema.TaggedErrorClass<CodexRequestError>()( | ||
| 136 | "CodexRequestError", | ||
| 137 | { | ||
| 138 | method: Schema.String, | ||
| 139 | detail: Schema.String, | ||
| 140 | cause: Schema.optional(Schema.Defect), | ||
| 141 | }, | ||
| 142 | ) { | ||
| 143 | override get message(): string { | ||
| 144 | return `Codex request failed for ${this.method}: ${this.detail}`; | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | /** | ||
| 149 | * CodexProcessError - codex app-server process lifecycle failure. | ||
| 150 | */ | ||
| 151 | export class CodexProcessError extends Schema.TaggedErrorClass<CodexProcessError>()( | ||
| 152 | "CodexProcessError", | ||
| 153 | { | ||
| 154 | sessionId: Schema.String, | ||
| 155 | detail: Schema.String, | ||
| 156 | cause: Schema.optional(Schema.Defect), | ||
| 157 | }, | ||
| 158 | ) { | ||
| 159 | override get message(): string { | ||
| 160 | return `Codex process error for session ${this.sessionId}: ${this.detail}`; | ||
| 161 | } | ||
| 162 | } | ||
| 163 | |||
| 164 | /** | ||
| 165 | * ProviderValidationError - Invalid provider API input. | ||
| 166 | */ | ||
| 167 | export class ProviderValidationError extends Schema.TaggedErrorClass<ProviderValidationError>()( | ||
| 168 | "ProviderValidationError", | ||
| 169 | { | ||
| 170 | operation: Schema.String, | ||
| 171 | issue: Schema.String, | ||
| 172 | cause: Schema.optional(Schema.Defect), | ||
| 173 | }, | ||
| 174 | ) { | ||
| 175 | override get message(): string { | ||
| 176 | return `Provider validation failed in ${this.operation}: ${this.issue}`; | ||
| 177 | } | ||
| 178 | } | ||
| 179 | |||
| 180 | /** | ||
| 181 | * ProviderUnsupportedError - Requested provider is not implemented. | ||
| 182 | */ | ||
| 183 | export class ProviderUnsupportedError extends Schema.TaggedErrorClass<ProviderUnsupportedError>()( | ||
| 184 | "ProviderUnsupportedError", | ||
| 185 | { | ||
| 186 | provider: Schema.String, | ||
| 187 | cause: Schema.optional(Schema.Defect), | ||
| 188 | }, | ||
| 189 | ) { | ||
| 190 | override get message(): string { | ||
| 191 | return `Provider '${this.provider}' is not implemented`; | ||
| 192 | } | ||
| 193 | } | ||
| 194 | |||
| 195 | /** | ||
| 196 | * ProviderSessionNotFoundError - Provider-facing session not found. | ||
| 197 | */ | ||
| 198 | export class ProviderSessionNotFoundError extends Schema.TaggedErrorClass<ProviderSessionNotFoundError>()( | ||
| 199 | "ProviderSessionNotFoundError", | ||
| 200 | { | ||
| 201 | sessionId: Schema.String, | ||
| 202 | cause: Schema.optional(Schema.Defect), | ||
| 203 | }, | ||
| 204 | ) { | ||
| 205 | override get message(): string { | ||
| 206 | return `Unknown provider session: ${this.sessionId}`; | ||
| 207 | } | ||
| 208 | } | ||
| 209 | |||
| 210 | /** | ||
| 211 | * ProviderCheckpointUnavailableError - Checkpointing unavailable for this session. | ||
| 212 | */ | ||
| 213 | export class ProviderCheckpointUnavailableError extends Schema.TaggedErrorClass<ProviderCheckpointUnavailableError>()( | ||
| 214 | "ProviderCheckpointUnavailableError", | ||
| 215 | { | ||
| 216 | sessionId: Schema.String, | ||
| 217 | detail: Schema.String, | ||
| 218 | cause: Schema.optional(Schema.Defect), | ||
| 219 | }, | ||
| 220 | ) { | ||
| 221 | override get message(): string { | ||
| 222 | return `Provider checkpoint unavailable for session ${this.sessionId}: ${this.detail}`; | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | /** | ||
| 227 | * ProviderCheckpointRangeError - Requested checkpoint range is invalid. | ||
| 228 | */ | ||
| 229 | export class ProviderCheckpointRangeError extends Schema.TaggedErrorClass<ProviderCheckpointRangeError>()( | ||
| 230 | "ProviderCheckpointRangeError", | ||
| 231 | { | ||
| 232 | sessionId: Schema.String, | ||
| 233 | fromTurnCount: Schema.Number, | ||
| 234 | toTurnCount: Schema.Number, | ||
| 235 | detail: Schema.String, | ||
| 236 | cause: Schema.optional(Schema.Defect), | ||
| 237 | }, | ||
| 238 | ) { | ||
| 239 | override get message(): string { | ||
| 240 | return `Provider checkpoint range error for session ${this.sessionId}: ${this.fromTurnCount}..${this.toTurnCount} (${this.detail})`; | ||
| 241 | } | ||
| 242 | } | ||
| 243 | |||
| 244 | /** | ||
| 245 | * ProviderFilesystemError - Filesystem checkpoint capture/restore failure. | ||
| 246 | */ | ||
| 247 | export class ProviderFilesystemError extends Schema.TaggedErrorClass<ProviderFilesystemError>()( | ||
| 248 | "ProviderFilesystemError", | ||
| 249 | { | ||
| 250 | sessionId: Schema.String, | ||
| 251 | detail: Schema.String, | ||
| 252 | cause: Schema.optional(Schema.Defect), | ||
| 253 | }, | ||
| 254 | ) { | ||
| 255 | override get message(): string { | ||
| 256 | return `Provider filesystem checkpoint error for session ${this.sessionId}: ${this.detail}`; | ||
| 257 | } | ||
| 258 | } | ||
| 259 | |||
| 260 | export type CheckpointStoreError = | ||
| 261 | | CheckpointValidationError | ||
| 262 | | CheckpointGitCommandError | ||
| 263 | | CheckpointUnavailableError | ||
| 264 | | CheckpointRepositoryError; | ||
| 265 | |||
| 266 | export type CodexServiceError = | ||
| 267 | | CodexValidationError | ||
| 268 | | CodexSessionNotFoundError | ||
| 269 | | CodexSessionClosedError | ||
| 270 | | CodexProtocolError | ||
| 271 | | CodexRequestError | ||
| 272 | | CodexProcessError; | ||
| 273 | |||
| 274 | export type ProviderServiceError = | ||
| 275 | | ProviderValidationError | ||
| 276 | | ProviderUnsupportedError | ||
| 277 | | ProviderSessionNotFoundError | ||
| 278 | | ProviderCheckpointUnavailableError | ||
| 279 | | ProviderCheckpointRangeError | ||
| 280 | | ProviderFilesystemError | ||
| 281 | | CodexServiceError | ||
| 282 | | CheckpointStoreError; | ||
apps/server/src/provider/Services/CheckpointStore.ts created+91| ... | @@ -0,0 +1,91 @@ | ||
| 1 | /** | ||
| 2 | * CheckpointStore - Repository interface for filesystem-backed workspace checkpoints. | ||
| 3 | * | ||
| 4 | * Uses Effect `ServiceMap.Service` for dependency injection and exposes typed | ||
| 5 | * domain errors for checkpoint lifecycle operations. | ||
| 6 | * | ||
| 7 | * @module CheckpointStore | ||
| 8 | */ | ||
| 9 | import { ServiceMap } from "effect"; | ||
| 10 | import type { Effect } from "effect"; | ||
| 11 | |||
| 12 | import type { CheckpointStoreError } from "../Errors.ts"; | ||
| 13 | |||
| 14 | export interface CaptureCheckpointInput { | ||
| 15 | readonly cwd: string; | ||
| 16 | readonly threadId: string; | ||
| 17 | readonly turnCount: number; | ||
| 18 | } | ||
| 19 | |||
| 20 | export interface EnsureRootCheckpointInput { | ||
| 21 | readonly cwd: string; | ||
| 22 | readonly threadId: string; | ||
| 23 | } | ||
| 24 | |||
| 25 | export interface RestoreCheckpointInput { | ||
| 26 | readonly cwd: string; | ||
| 27 | readonly threadId: string; | ||
| 28 | readonly turnCount: number; | ||
| 29 | } | ||
| 30 | |||
| 31 | export interface DiffCheckpointsInput { | ||
| 32 | readonly cwd: string; | ||
| 33 | readonly threadId: string; | ||
| 34 | readonly fromTurnCount: number; | ||
| 35 | readonly toTurnCount: number; | ||
| 36 | } | ||
| 37 | |||
| 38 | export interface PruneAfterTurnInput { | ||
| 39 | readonly cwd: string; | ||
| 40 | readonly threadId: string; | ||
| 41 | readonly maxTurnCount: number; | ||
| 42 | } | ||
| 43 | |||
| 44 | export interface CheckpointStoreShape { | ||
| 45 | /** | ||
| 46 | * Check whether cwd is inside a Git worktree. | ||
| 47 | */ | ||
| 48 | readonly isGitRepository: (cwd: string) => Effect.Effect<boolean, CheckpointStoreError>; | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Capture a checkpoint for a thread at a turn count. | ||
| 52 | */ | ||
| 53 | readonly captureCheckpoint: (input: CaptureCheckpointInput) => Effect.Effect<void, CheckpointStoreError>; | ||
| 54 | |||
| 55 | /** | ||
| 56 | * Check whether a checkpoint ref exists. | ||
| 57 | */ | ||
| 58 | readonly hasCheckpoint: (input: RestoreCheckpointInput) => Effect.Effect<boolean, CheckpointStoreError>; | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Ensure a turn-0 checkpoint exists for a thread. | ||
| 62 | */ | ||
| 63 | readonly ensureRootCheckpoint: ( | ||
| 64 | input: EnsureRootCheckpointInput, | ||
| 65 | ) => Effect.Effect<boolean, CheckpointStoreError>; | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Restore workspace/staging state to a checkpoint. | ||
| 69 | */ | ||
| 70 | readonly restoreCheckpoint: ( | ||
| 71 | input: RestoreCheckpointInput, | ||
| 72 | ) => Effect.Effect<boolean, CheckpointStoreError>; | ||
| 73 | |||
| 74 | /** | ||
| 75 | * Compute patch diff between two checkpoint turn counts. | ||
| 76 | */ | ||
| 77 | readonly diffCheckpoints: (input: DiffCheckpointsInput) => Effect.Effect<string, CheckpointStoreError>; | ||
| 78 | |||
| 79 | /** | ||
| 80 | * Delete checkpoints newer than the provided turn count. | ||
| 81 | */ | ||
| 82 | readonly pruneAfterTurn: (input: PruneAfterTurnInput) => Effect.Effect<void, CheckpointStoreError>; | ||
| 83 | } | ||
| 84 | |||
| 85 | /** | ||
| 86 | * CheckpointStore - Context tag for checkpoint persistence and restore operations. | ||
| 87 | */ | ||
| 88 | export class CheckpointStore extends ServiceMap.Service< | ||
| 89 | CheckpointStore, | ||
| 90 | CheckpointStoreShape | ||
| 91 | >()("provider/CheckpointStore") {} | ||
apps/server/src/provider/Services/Codex.ts created+110| ... | @@ -0,0 +1,110 @@ | ||
| 1 | /** | ||
| 2 | * CodexService - Service interface for codex app-server session lifecycle and RPC. | ||
| 3 | * | ||
| 4 | * Uses Effect `ServiceMap.Service` for dependency injection and returns typed | ||
| 5 | * domain errors for process, protocol, and session operations. | ||
| 6 | * | ||
| 7 | * @module CodexService | ||
| 8 | */ | ||
| 9 | import type { | ||
| 10 | ProviderApprovalDecision, | ||
| 11 | ProviderEvent, | ||
| 12 | ProviderSendTurnInput, | ||
| 13 | ProviderSession, | ||
| 14 | ProviderSessionStartInput, | ||
| 15 | ProviderTurnStartResult, | ||
| 16 | } from "@t3tools/contracts"; | ||
| 17 | import { ServiceMap } from "effect"; | ||
| 18 | import type { Effect } from "effect"; | ||
| 19 | |||
| 20 | import type { CodexServiceError } from "../Errors.ts"; | ||
| 21 | |||
| 22 | export interface CodexThreadTurnSnapshot { | ||
| 23 | readonly id: string; | ||
| 24 | readonly items: ReadonlyArray<unknown>; | ||
| 25 | } | ||
| 26 | |||
| 27 | export interface CodexThreadSnapshot { | ||
| 28 | readonly threadId: string; | ||
| 29 | readonly turns: ReadonlyArray<CodexThreadTurnSnapshot>; | ||
| 30 | } | ||
| 31 | |||
| 32 | export interface CodexServiceShape { | ||
| 33 | /** | ||
| 34 | * Start a codex app-server-backed provider session. | ||
| 35 | */ | ||
| 36 | readonly startSession: ( | ||
| 37 | input: ProviderSessionStartInput, | ||
| 38 | ) => Effect.Effect<ProviderSession, CodexServiceError>; | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Send a new turn to an active codex session. | ||
| 42 | */ | ||
| 43 | readonly sendTurn: ( | ||
| 44 | input: ProviderSendTurnInput, | ||
| 45 | ) => Effect.Effect<ProviderTurnStartResult, CodexServiceError>; | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Interrupt an active turn. | ||
| 49 | */ | ||
| 50 | readonly interruptTurn: ( | ||
| 51 | sessionId: string, | ||
| 52 | turnId?: string, | ||
| 53 | ) => Effect.Effect<void, CodexServiceError>; | ||
| 54 | |||
| 55 | /** | ||
| 56 | * Read a thread snapshot (including turns). | ||
| 57 | */ | ||
| 58 | readonly readThread: (sessionId: string) => Effect.Effect<CodexThreadSnapshot, CodexServiceError>; | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Roll back a thread by N turns. | ||
| 62 | */ | ||
| 63 | readonly rollbackThread: ( | ||
| 64 | sessionId: string, | ||
| 65 | numTurns: number, | ||
| 66 | ) => Effect.Effect<CodexThreadSnapshot, CodexServiceError>; | ||
| 67 | |||
| 68 | /** | ||
| 69 | * Respond to an interactive approval request from codex app-server. | ||
| 70 | */ | ||
| 71 | readonly respondToRequest: ( | ||
| 72 | sessionId: string, | ||
| 73 | requestId: string, | ||
| 74 | decision: ProviderApprovalDecision, | ||
| 75 | ) => Effect.Effect<void, CodexServiceError>; | ||
| 76 | |||
| 77 | /** | ||
| 78 | * Stop one session. | ||
| 79 | */ | ||
| 80 | readonly stopSession: (sessionId: string) => Effect.Effect<void, CodexServiceError>; | ||
| 81 | |||
| 82 | /** | ||
| 83 | * List active sessions. | ||
| 84 | */ | ||
| 85 | readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>; | ||
| 86 | |||
| 87 | /** | ||
| 88 | * Check whether session id is currently active. | ||
| 89 | */ | ||
| 90 | readonly hasSession: (sessionId: string) => Effect.Effect<boolean>; | ||
| 91 | |||
| 92 | /** | ||
| 93 | * Stop all sessions. | ||
| 94 | */ | ||
| 95 | readonly stopAll: () => Effect.Effect<void, CodexServiceError>; | ||
| 96 | |||
| 97 | /** | ||
| 98 | * Subscribe to provider events emitted by codex app-server. | ||
| 99 | */ | ||
| 100 | readonly subscribeToEvents: ( | ||
| 101 | callback: (event: ProviderEvent) => void, | ||
| 102 | ) => Effect.Effect<() => void, CodexServiceError>; | ||
| 103 | } | ||
| 104 | |||
| 105 | /** | ||
| 106 | * CodexService - Context tag for codex session and RPC operations. | ||
| 107 | */ | ||
| 108 | export class CodexService extends ServiceMap.Service<CodexService, CodexServiceShape>()( | ||
| 109 | "provider/CodexService", | ||
| 110 | ) {} | ||
apps/server/src/provider/Services/Provider.ts created+113| ... | @@ -0,0 +1,113 @@ | ||
| 1 | /** | ||
| 2 | * ProviderService - Service interface for provider sessions, turns, and checkpoints. | ||
| 3 | * | ||
| 4 | * Uses Effect `ServiceMap.Service` for dependency injection and returns typed | ||
| 5 | * domain errors for validation, session, codex, and checkpoint workflows. | ||
| 6 | * | ||
| 7 | * @module ProviderService | ||
| 8 | */ | ||
| 9 | import type { | ||
| 10 | ProviderEvent, | ||
| 11 | ProviderGetCheckpointDiffInput, | ||
| 12 | ProviderGetCheckpointDiffResult, | ||
| 13 | ProviderInterruptTurnInput, | ||
| 14 | ProviderListCheckpointsInput, | ||
| 15 | ProviderListCheckpointsResult, | ||
| 16 | ProviderRespondToRequestInput, | ||
| 17 | ProviderRevertToCheckpointInput, | ||
| 18 | ProviderRevertToCheckpointResult, | ||
| 19 | ProviderSendTurnInput, | ||
| 20 | ProviderSession, | ||
| 21 | ProviderSessionStartInput, | ||
| 22 | ProviderStopSessionInput, | ||
| 23 | ProviderTurnStartResult, | ||
| 24 | } from "@t3tools/contracts"; | ||
| 25 | import { ServiceMap } from "effect"; | ||
| 26 | import type { Effect } from "effect"; | ||
| 27 | |||
| 28 | import type { ProviderServiceError } from "../Errors.ts"; | ||
| 29 | |||
| 30 | export interface ProviderServiceShape { | ||
| 31 | /** | ||
| 32 | * Start a provider session. | ||
| 33 | */ | ||
| 34 | readonly startSession: ( | ||
| 35 | input: ProviderSessionStartInput, | ||
| 36 | ) => Effect.Effect<ProviderSession, ProviderServiceError>; | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Send a provider turn. | ||
| 40 | */ | ||
| 41 | readonly sendTurn: ( | ||
| 42 | input: ProviderSendTurnInput, | ||
| 43 | ) => Effect.Effect<ProviderTurnStartResult, ProviderServiceError>; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Interrupt a running provider turn. | ||
| 47 | */ | ||
| 48 | readonly interruptTurn: ( | ||
| 49 | input: ProviderInterruptTurnInput, | ||
| 50 | ) => Effect.Effect<void, ProviderServiceError>; | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Respond to a provider approval request. | ||
| 54 | */ | ||
| 55 | readonly respondToRequest: ( | ||
| 56 | input: ProviderRespondToRequestInput, | ||
| 57 | ) => Effect.Effect<void, ProviderServiceError>; | ||
| 58 | |||
| 59 | /** | ||
| 60 | * Stop a provider session. | ||
| 61 | */ | ||
| 62 | readonly stopSession: (input: ProviderStopSessionInput) => Effect.Effect<void, ProviderServiceError>; | ||
| 63 | |||
| 64 | /** | ||
| 65 | * List active provider sessions. | ||
| 66 | */ | ||
| 67 | readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>; | ||
| 68 | |||
| 69 | /** | ||
| 70 | * List checkpoints for a provider session. | ||
| 71 | */ | ||
| 72 | readonly listCheckpoints: ( | ||
| 73 | input: ProviderListCheckpointsInput, | ||
| 74 | ) => Effect.Effect<ProviderListCheckpointsResult, ProviderServiceError>; | ||
| 75 | |||
| 76 | /** | ||
| 77 | * Diff two checkpoints for a provider session. | ||
| 78 | */ | ||
| 79 | readonly getCheckpointDiff: ( | ||
| 80 | input: ProviderGetCheckpointDiffInput, | ||
| 81 | ) => Effect.Effect<ProviderGetCheckpointDiffResult, ProviderServiceError>; | ||
| 82 | |||
| 83 | /** | ||
| 84 | * Revert a provider session to a checkpoint. | ||
| 85 | */ | ||
| 86 | readonly revertToCheckpoint: ( | ||
| 87 | input: ProviderRevertToCheckpointInput, | ||
| 88 | ) => Effect.Effect<ProviderRevertToCheckpointResult, ProviderServiceError>; | ||
| 89 | |||
| 90 | /** | ||
| 91 | * Stop all provider sessions. | ||
| 92 | */ | ||
| 93 | readonly stopAll: () => Effect.Effect<void, ProviderServiceError>; | ||
| 94 | |||
| 95 | /** | ||
| 96 | * Dispose provider resources (logs/listeners). | ||
| 97 | */ | ||
| 98 | readonly dispose: () => Effect.Effect<void, ProviderServiceError>; | ||
| 99 | |||
| 100 | /** | ||
| 101 | * Subscribe to provider event stream. | ||
| 102 | */ | ||
| 103 | readonly subscribeToEvents: ( | ||
| 104 | callback: (event: ProviderEvent) => void, | ||
| 105 | ) => Effect.Effect<() => void, ProviderServiceError>; | ||
| 106 | } | ||
| 107 | |||
| 108 | /** | ||
| 109 | * ProviderService - Context tag for provider orchestration. | ||
| 110 | */ | ||
| 111 | export class ProviderService extends ServiceMap.Service<ProviderService, ProviderServiceShape>()( | ||
| 112 | "provider/ProviderService", | ||
| 113 | ) {} | ||
apps/server/src/provider/contracts.test.ts created+125| ... | @@ -0,0 +1,125 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { Effect, Layer, ManagedRuntime } from "effect"; | ||
| 4 | |||
| 5 | import { | ||
| 6 | CheckpointValidationError, | ||
| 7 | CodexValidationError, | ||
| 8 | ProviderFilesystemError, | ||
| 9 | ProviderUnsupportedError, | ||
| 10 | type CheckpointStoreError, | ||
| 11 | type CodexServiceError, | ||
| 12 | type ProviderServiceError, | ||
| 13 | } from "./Errors.ts"; | ||
| 14 | import { CheckpointStore } from "./Services/CheckpointStore.ts"; | ||
| 15 | import { CodexService } from "./Services/Codex.ts"; | ||
| 16 | import { ProviderService } from "./Services/Provider.ts"; | ||
| 17 | |||
| 18 | describe("provider service contracts", () => { | ||
| 19 | it("resolves provider, codex, and checkpoint services from ServiceMap tags", async () => { | ||
| 20 | const providerLive: typeof ProviderService.Service = { | ||
| 21 | startSession: () => | ||
| 22 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), | ||
| 23 | sendTurn: () => | ||
| 24 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), | ||
| 25 | interruptTurn: () => | ||
| 26 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), | ||
| 27 | respondToRequest: () => | ||
| 28 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), | ||
| 29 | stopSession: () => Effect.void, | ||
| 30 | listSessions: () => Effect.succeed([]), | ||
| 31 | listCheckpoints: () => | ||
| 32 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), | ||
| 33 | getCheckpointDiff: () => | ||
| 34 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), | ||
| 35 | revertToCheckpoint: () => | ||
| 36 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), | ||
| 37 | stopAll: () => Effect.void, | ||
| 38 | dispose: () => Effect.void, | ||
| 39 | subscribeToEvents: () => Effect.succeed(() => undefined), | ||
| 40 | }; | ||
| 41 | |||
| 42 | const codexLive: typeof CodexService.Service = { | ||
| 43 | startSession: () => | ||
| 44 | Effect.fail<CodexServiceError>( | ||
| 45 | new CodexValidationError({ operation: "startSession", issue: "not implemented" }), | ||
| 46 | ), | ||
| 47 | sendTurn: () => | ||
| 48 | Effect.fail<CodexServiceError>( | ||
| 49 | new CodexValidationError({ operation: "sendTurn", issue: "not implemented" }), | ||
| 50 | ), | ||
| 51 | interruptTurn: () => | ||
| 52 | Effect.fail<CodexServiceError>( | ||
| 53 | new CodexValidationError({ operation: "interruptTurn", issue: "not implemented" }), | ||
| 54 | ), | ||
| 55 | readThread: () => | ||
| 56 | Effect.fail<CodexServiceError>( | ||
| 57 | new CodexValidationError({ operation: "readThread", issue: "not implemented" }), | ||
| 58 | ), | ||
| 59 | rollbackThread: () => | ||
| 60 | Effect.fail<CodexServiceError>( | ||
| 61 | new CodexValidationError({ operation: "rollbackThread", issue: "not implemented" }), | ||
| 62 | ), | ||
| 63 | respondToRequest: () => | ||
| 64 | Effect.fail<CodexServiceError>( | ||
| 65 | new CodexValidationError({ operation: "respondToRequest", issue: "not implemented" }), | ||
| 66 | ), | ||
| 67 | stopSession: () => Effect.void, | ||
| 68 | listSessions: () => Effect.succeed([]), | ||
| 69 | hasSession: () => Effect.succeed(false), | ||
| 70 | stopAll: () => Effect.void, | ||
| 71 | subscribeToEvents: () => Effect.succeed(() => undefined), | ||
| 72 | }; | ||
| 73 | |||
| 74 | const checkpointLive: typeof CheckpointStore.Service = { | ||
| 75 | isGitRepository: () => Effect.succeed(false), | ||
| 76 | captureCheckpoint: () => Effect.void, | ||
| 77 | hasCheckpoint: () => Effect.succeed(false), | ||
| 78 | ensureRootCheckpoint: () => Effect.succeed(false), | ||
| 79 | restoreCheckpoint: () => Effect.succeed(false), | ||
| 80 | diffCheckpoints: () => | ||
| 81 | Effect.fail<CheckpointStoreError>( | ||
| 82 | new CheckpointValidationError({ | ||
| 83 | operation: "diffCheckpoints", | ||
| 84 | issue: "not implemented", | ||
| 85 | }), | ||
| 86 | ), | ||
| 87 | pruneAfterTurn: () => Effect.void, | ||
| 88 | }; | ||
| 89 | |||
| 90 | const runtime = ManagedRuntime.make( | ||
| 91 | Layer.mergeAll( | ||
| 92 | Layer.succeed(ProviderService, providerLive), | ||
| 93 | Layer.succeed(CodexService, codexLive), | ||
| 94 | Layer.succeed(CheckpointStore, checkpointLive), | ||
| 95 | ), | ||
| 96 | ); | ||
| 97 | |||
| 98 | const [provider, codex, checkpoint] = await Promise.all([ | ||
| 99 | runtime.runPromise(Effect.service(ProviderService)), | ||
| 100 | runtime.runPromise(Effect.service(CodexService)), | ||
| 101 | runtime.runPromise(Effect.service(CheckpointStore)), | ||
| 102 | ]); | ||
| 103 | |||
| 104 | expect(provider).toBe(providerLive); | ||
| 105 | expect(codex).toBe(codexLive); | ||
| 106 | expect(checkpoint).toBe(checkpointLive); | ||
| 107 | |||
| 108 | await runtime.dispose(); | ||
| 109 | }); | ||
| 110 | }); | ||
| 111 | |||
| 112 | describe("provider error taxonomy", () => { | ||
| 113 | it("keeps defect cause and message on tagged errors", () => { | ||
| 114 | const cause = new Error("filesystem failed"); | ||
| 115 | const error = new ProviderFilesystemError({ | ||
| 116 | sessionId: "sess-1", | ||
| 117 | detail: "capture failed", | ||
| 118 | cause, | ||
| 119 | }); | ||
| 120 | |||
| 121 | expect(error._tag).toBe("ProviderFilesystemError"); | ||
| 122 | expect(error.message).toContain("sess-1"); | ||
| 123 | expect(error.cause).toBe(cause); | ||
| 124 | }); | ||
| 125 | }); | ||