| 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 | }); |