| author | |
| committer | |
| log | 150b3782b942e3b35899e134536efc0e4bbd51ac |
| tree | aa4d40ddbef78137fe10ae4acf9b808630ea2483 |
| parent | 5bfd0598e763ba8221ae90b6b68524391a8492a0 |
17 files changed, 955 insertions(+), 463 deletions(-)
apps/server/src/checkpointing/Errors.ts created+148| ... | ... | @@ -0,0 +1,148 @@ |
| 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 | * CheckpointCatalogPersistenceError - Metadata persistence/read failure. | |
| 72 | */ | |
| 73 | export class CheckpointCatalogPersistenceError extends Schema.TaggedErrorClass<CheckpointCatalogPersistenceError>()( | |
| 74 | "CheckpointCatalogPersistenceError", | |
| 75 | { | |
| 76 | operation: Schema.String, | |
| 77 | detail: Schema.String, | |
| 78 | cause: Schema.optional(Schema.Defect), | |
| 79 | }, | |
| 80 | ) { | |
| 81 | override get message(): string { | |
| 82 | return `Checkpoint catalog persistence error in ${this.operation}: ${this.detail}`; | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| 86 | /** | |
| 87 | * CheckpointServiceValidationError - Invalid application-level checkpoint request. | |
| 88 | */ | |
| 89 | export class CheckpointServiceValidationError extends Schema.TaggedErrorClass<CheckpointServiceValidationError>()( | |
| 90 | "CheckpointServiceValidationError", | |
| 91 | { | |
| 92 | operation: Schema.String, | |
| 93 | issue: Schema.String, | |
| 94 | cause: Schema.optional(Schema.Defect), | |
| 95 | }, | |
| 96 | ) { | |
| 97 | override get message(): string { | |
| 98 | return `Checkpoint service validation failed in ${this.operation}: ${this.issue}`; | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | /** | |
| 103 | * CheckpointSessionNotFoundError - Session is missing in checkpoint workflow. | |
| 104 | */ | |
| 105 | export class CheckpointSessionNotFoundError extends Schema.TaggedErrorClass<CheckpointSessionNotFoundError>()( | |
| 106 | "CheckpointSessionNotFoundError", | |
| 107 | { | |
| 108 | sessionId: Schema.String, | |
| 109 | cause: Schema.optional(Schema.Defect), | |
| 110 | }, | |
| 111 | ) { | |
| 112 | override get message(): string { | |
| 113 | return `Checkpoint session not found: ${this.sessionId}`; | |
| 114 | } | |
| 115 | } | |
| 116 | ||
| 117 | /** | |
| 118 | * CheckpointInvariantError - Inconsistent provider/filesystem/catalog state. | |
| 119 | */ | |
| 120 | export class CheckpointInvariantError extends Schema.TaggedErrorClass<CheckpointInvariantError>()( | |
| 121 | "CheckpointInvariantError", | |
| 122 | { | |
| 123 | operation: Schema.String, | |
| 124 | detail: Schema.String, | |
| 125 | cause: Schema.optional(Schema.Defect), | |
| 126 | }, | |
| 127 | ) { | |
| 128 | override get message(): string { | |
| 129 | return `Checkpoint invariant violation in ${this.operation}: ${this.detail}`; | |
| 130 | } | |
| 131 | } | |
| 132 | ||
| 133 | export type CheckpointStoreError = | |
| 134 | | CheckpointValidationError | |
| 135 | | CheckpointGitCommandError | |
| 136 | | CheckpointUnavailableError | |
| 137 | | CheckpointRepositoryError; | |
| 138 | ||
| 139 | export type CheckpointCatalogError = | |
| 140 | | CheckpointValidationError | |
| 141 | | CheckpointCatalogPersistenceError; | |
| 142 | ||
| 143 | export type CheckpointServiceError = | |
| 144 | | CheckpointStoreError | |
| 145 | | CheckpointCatalogError | |
| 146 | | CheckpointServiceValidationError | |
| 147 | | CheckpointSessionNotFoundError | |
| 148 | | CheckpointInvariantError; |
apps/server/src/checkpointing/Services/CheckpointCatalog.ts created+83| ... | ... | @@ -0,0 +1,83 @@ |
| 1 | /** | |
| 2 | * CheckpointCatalog - Metadata repository for checkpoints. | |
| 3 | * | |
| 4 | * Stores user-facing checkpoint metadata used by list/revert APIs. It does not | |
| 5 | * touch the filesystem or hidden Git refs; `CheckpointStore` owns that state. | |
| 6 | * It also does not drive provider conversation rollback. | |
| 7 | * | |
| 8 | * @module CheckpointCatalog | |
| 9 | */ | |
| 10 | import type { ProviderCheckpoint } from "@t3tools/contracts"; | |
| 11 | import { Option, ServiceMap } from "effect"; | |
| 12 | import type { Effect } from "effect"; | |
| 13 | ||
| 14 | import type { CheckpointCatalogError } from "../Errors.ts"; | |
| 15 | ||
| 16 | export interface UpsertCheckpointInput { | |
| 17 | readonly providerSessionId: string; | |
| 18 | readonly threadId: string; | |
| 19 | readonly turnCount: number; | |
| 20 | readonly messageCount: number; | |
| 21 | readonly label: string; | |
| 22 | readonly preview?: string; | |
| 23 | readonly createdAt: string; | |
| 24 | } | |
| 25 | ||
| 26 | export interface ListSessionCheckpointsInput { | |
| 27 | readonly providerSessionId: string; | |
| 28 | } | |
| 29 | ||
| 30 | export interface GetCheckpointInput { | |
| 31 | readonly providerSessionId: string; | |
| 32 | readonly turnCount: number; | |
| 33 | } | |
| 34 | ||
| 35 | export interface DeleteAfterTurnInput { | |
| 36 | readonly providerSessionId: string; | |
| 37 | readonly maxTurnCount: number; | |
| 38 | } | |
| 39 | ||
| 40 | export interface DeleteAllForSessionInput { | |
| 41 | readonly providerSessionId: string; | |
| 42 | } | |
| 43 | ||
| 44 | export interface CheckpointCatalogShape { | |
| 45 | /** | |
| 46 | * Insert or update one checkpoint metadata row. | |
| 47 | */ | |
| 48 | readonly upsertCheckpoint: (input: UpsertCheckpointInput) => Effect.Effect<void, CheckpointCatalogError>; | |
| 49 | ||
| 50 | /** | |
| 51 | * List checkpoints for a provider session. | |
| 52 | */ | |
| 53 | readonly listCheckpoints: ( | |
| 54 | input: ListSessionCheckpointsInput, | |
| 55 | ) => Effect.Effect<ReadonlyArray<ProviderCheckpoint>, CheckpointCatalogError>; | |
| 56 | ||
| 57 | /** | |
| 58 | * Read one checkpoint by turn count. | |
| 59 | */ | |
| 60 | readonly getCheckpoint: ( | |
| 61 | input: GetCheckpointInput, | |
| 62 | ) => Effect.Effect<Option.Option<ProviderCheckpoint>, CheckpointCatalogError>; | |
| 63 | ||
| 64 | /** | |
| 65 | * Delete checkpoint metadata newer than the provided turn count. | |
| 66 | */ | |
| 67 | readonly deleteAfterTurn: (input: DeleteAfterTurnInput) => Effect.Effect<void, CheckpointCatalogError>; | |
| 68 | ||
| 69 | /** | |
| 70 | * Delete all checkpoint metadata for a provider session. | |
| 71 | */ | |
| 72 | readonly deleteAllForSession: ( | |
| 73 | input: DeleteAllForSessionInput, | |
| 74 | ) => Effect.Effect<void, CheckpointCatalogError>; | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * CheckpointCatalog - Service tag for checkpoint metadata persistence. | |
| 79 | */ | |
| 80 | export class CheckpointCatalog extends ServiceMap.Service< | |
| 81 | CheckpointCatalog, | |
| 82 | CheckpointCatalogShape | |
| 83 | >()("checkpointing/CheckpointCatalog") {} |
apps/server/src/checkpointing/Services/CheckpointService.ts created+78| ... | ... | @@ -0,0 +1,78 @@ |
| 1 | /** | |
| 2 | * CheckpointService - Application orchestrator for checkpoint workflows. | |
| 3 | * | |
| 4 | * Coordinates `CheckpointStore` (Git refs), `CheckpointCatalog` (metadata), and | |
| 5 | * provider timeline operations resolved through `ProviderSessionDirectory` and | |
| 6 | * `ProviderAdapterRegistry` (read/rollback/hasSession on adapters). | |
| 7 | * | |
| 8 | * This service owns checkpoint workflow rules and validation, while concrete | |
| 9 | * storage/process details stay in lower-level services. | |
| 10 | * | |
| 11 | * @module CheckpointService | |
| 12 | */ | |
| 13 | import type { | |
| 14 | ProviderGetCheckpointDiffInput, | |
| 15 | ProviderGetCheckpointDiffResult, | |
| 16 | ProviderListCheckpointsInput, | |
| 17 | ProviderListCheckpointsResult, | |
| 18 | ProviderRevertToCheckpointInput, | |
| 19 | ProviderRevertToCheckpointResult, | |
| 20 | } from "@t3tools/contracts"; | |
| 21 | import { ServiceMap } from "effect"; | |
| 22 | import type { Effect } from "effect"; | |
| 23 | ||
| 24 | import type { CheckpointServiceError } from "../Errors.ts"; | |
| 25 | ||
| 26 | export interface InitializeCheckpointSessionInput { | |
| 27 | readonly providerSessionId: string; | |
| 28 | readonly cwd: string; | |
| 29 | } | |
| 30 | ||
| 31 | export interface CaptureCurrentTurnInput { | |
| 32 | readonly providerSessionId: string; | |
| 33 | readonly turnId?: string; | |
| 34 | readonly status?: string; | |
| 35 | } | |
| 36 | ||
| 37 | export interface CheckpointServiceShape { | |
| 38 | /** | |
| 39 | * Initialize root checkpoint and metadata for a provider session. | |
| 40 | */ | |
| 41 | readonly initializeForSession: ( | |
| 42 | input: InitializeCheckpointSessionInput, | |
| 43 | ) => Effect.Effect<void, CheckpointServiceError>; | |
| 44 | ||
| 45 | /** | |
| 46 | * Capture the current thread turn into checkpoint refs/metadata. | |
| 47 | */ | |
| 48 | readonly captureCurrentTurn: (input: CaptureCurrentTurnInput) => Effect.Effect<void, CheckpointServiceError>; | |
| 49 | ||
| 50 | /** | |
| 51 | * List checkpoints for a provider session. | |
| 52 | */ | |
| 53 | readonly listCheckpoints: ( | |
| 54 | input: ProviderListCheckpointsInput, | |
| 55 | ) => Effect.Effect<ProviderListCheckpointsResult, CheckpointServiceError>; | |
| 56 | ||
| 57 | /** | |
| 58 | * Diff two checkpoint turn counts for a provider session. | |
| 59 | */ | |
| 60 | readonly getCheckpointDiff: ( | |
| 61 | input: ProviderGetCheckpointDiffInput, | |
| 62 | ) => Effect.Effect<ProviderGetCheckpointDiffResult, CheckpointServiceError>; | |
| 63 | ||
| 64 | /** | |
| 65 | * Revert provider/filesystem state to a checkpoint. | |
| 66 | */ | |
| 67 | readonly revertToCheckpoint: ( | |
| 68 | input: ProviderRevertToCheckpointInput, | |
| 69 | ) => Effect.Effect<ProviderRevertToCheckpointResult, CheckpointServiceError>; | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * CheckpointService - Service tag for checkpoint orchestration. | |
| 74 | */ | |
| 75 | export class CheckpointService extends ServiceMap.Service< | |
| 76 | CheckpointService, | |
| 77 | CheckpointServiceShape | |
| 78 | >()("checkpointing/CheckpointService") {} |
apps/server/src/checkpointing/Services/CheckpointStore.ts created+95| ... | ... | @@ -0,0 +1,95 @@ |
| 1 | /** | |
| 2 | * CheckpointStore - Repository interface for filesystem-backed workspace checkpoints. | |
| 3 | * | |
| 4 | * Owns hidden Git-ref checkpoint capture/restore and diff computation for a | |
| 5 | * workspace thread timeline. It does not store user-facing checkpoint metadata | |
| 6 | * and does not coordinate provider conversation rollback. | |
| 7 | * | |
| 8 | * Uses Effect `ServiceMap.Service` for dependency injection and exposes typed | |
| 9 | * domain errors for checkpoint storage operations. | |
| 10 | * | |
| 11 | * @module CheckpointStore | |
| 12 | */ | |
| 13 | import { ServiceMap } from "effect"; | |
| 14 | import type { Effect } from "effect"; | |
| 15 | ||
| 16 | import type { CheckpointStoreError } from "../Errors.ts"; | |
| 17 | ||
| 18 | export interface CaptureCheckpointInput { | |
| 19 | readonly cwd: string; | |
| 20 | readonly threadId: string; | |
| 21 | readonly turnCount: number; | |
| 22 | } | |
| 23 | ||
| 24 | export interface EnsureRootCheckpointInput { | |
| 25 | readonly cwd: string; | |
| 26 | readonly threadId: string; | |
| 27 | } | |
| 28 | ||
| 29 | export interface RestoreCheckpointInput { | |
| 30 | readonly cwd: string; | |
| 31 | readonly threadId: string; | |
| 32 | readonly turnCount: number; | |
| 33 | } | |
| 34 | ||
| 35 | export interface DiffCheckpointsInput { | |
| 36 | readonly cwd: string; | |
| 37 | readonly threadId: string; | |
| 38 | readonly fromTurnCount: number; | |
| 39 | readonly toTurnCount: number; | |
| 40 | } | |
| 41 | ||
| 42 | export interface PruneAfterTurnInput { | |
| 43 | readonly cwd: string; | |
| 44 | readonly threadId: string; | |
| 45 | readonly maxTurnCount: number; | |
| 46 | } | |
| 47 | ||
| 48 | export interface CheckpointStoreShape { | |
| 49 | /** | |
| 50 | * Check whether cwd is inside a Git worktree. | |
| 51 | */ | |
| 52 | readonly isGitRepository: (cwd: string) => Effect.Effect<boolean, CheckpointStoreError>; | |
| 53 | ||
| 54 | /** | |
| 55 | * Capture a checkpoint for a thread at a turn count. | |
| 56 | */ | |
| 57 | readonly captureCheckpoint: (input: CaptureCheckpointInput) => Effect.Effect<void, CheckpointStoreError>; | |
| 58 | ||
| 59 | /** | |
| 60 | * Check whether a checkpoint ref exists. | |
| 61 | */ | |
| 62 | readonly hasCheckpoint: (input: RestoreCheckpointInput) => Effect.Effect<boolean, CheckpointStoreError>; | |
| 63 | ||
| 64 | /** | |
| 65 | * Ensure a turn-0 checkpoint exists for a thread. | |
| 66 | */ | |
| 67 | readonly ensureRootCheckpoint: ( | |
| 68 | input: EnsureRootCheckpointInput, | |
| 69 | ) => Effect.Effect<boolean, CheckpointStoreError>; | |
| 70 | ||
| 71 | /** | |
| 72 | * Restore workspace/staging state to a checkpoint. | |
| 73 | */ | |
| 74 | readonly restoreCheckpoint: ( | |
| 75 | input: RestoreCheckpointInput, | |
| 76 | ) => Effect.Effect<boolean, CheckpointStoreError>; | |
| 77 | ||
| 78 | /** | |
| 79 | * Compute patch diff between two checkpoint turn counts. | |
| 80 | */ | |
| 81 | readonly diffCheckpoints: (input: DiffCheckpointsInput) => Effect.Effect<string, CheckpointStoreError>; | |
| 82 | ||
| 83 | /** | |
| 84 | * Delete checkpoints newer than the provided turn count. | |
| 85 | */ | |
| 86 | readonly pruneAfterTurn: (input: PruneAfterTurnInput) => Effect.Effect<void, CheckpointStoreError>; | |
| 87 | } | |
| 88 | ||
| 89 | /** | |
| 90 | * CheckpointStore - Service tag for checkpoint persistence and restore operations. | |
| 91 | */ | |
| 92 | export class CheckpointStore extends ServiceMap.Service< | |
| 93 | CheckpointStore, | |
| 94 | CheckpointStoreShape | |
| 95 | >()("checkpointing/CheckpointStore") {} |
apps/server/src/orchestration/Service.ts+6-2| ... | ... | @@ -1,7 +1,11 @@ |
| 1 | 1 | /** |
| 2 | 2 | * OrchestrationEngineService - Service interface for orchestration command handling. |
| 3 | 3 | * |
| 4 | * Uses Effect `Context.Tag` for dependency injection. Command dispatch, | |
| 4 | * Owns command validation/dispatch and in-memory read-model updates backed by | |
| 5 | * `OrchestrationEventRepository` persistence. It does not own provider process | |
| 6 | * management or transport concerns (e.g. websocket request parsing). | |
| 7 | * | |
| 8 | * Uses Effect `ServiceMap.Service` for dependency injection. Command dispatch, | |
| 5 | 9 | * replay, and unknown-input decoding all return typed domain errors. |
| 6 | 10 | * |
| 7 | 11 | * @module OrchestrationEngineService |
| ... | ... | @@ -79,7 +83,7 @@ export interface OrchestrationEngineShape { |
| 79 | 83 | } |
| 80 | 84 | |
| 81 | 85 | /** |
| 82 | * OrchestrationEngineService - Context tag for orchestration engine access. | |
| 86 | * OrchestrationEngineService - Service tag for orchestration engine access. | |
| 83 | 87 | * |
| 84 | 88 | * @example |
| 85 | 89 | * ```ts |
apps/server/src/persistence/Services/OrchestrationEvents.ts+5-2| ... | ... | @@ -1,7 +1,10 @@ |
| 1 | 1 | /** |
| 2 | 2 | * OrchestrationEventRepository - Repository interface for orchestration events. |
| 3 | 3 | * |
| 4 | * Uses Effect `Context.Tag` for dependency injection and exposes typed | |
| 4 | * Owns durable append/replay access to the orchestration event stream. It does | |
| 5 | * not reduce events into read models or apply command validation rules. | |
| 6 | * | |
| 7 | * Uses Effect `ServiceMap.Service` for dependency injection and exposes typed | |
| 5 | 8 | * persistence/decode errors for event append and replay operations. |
| 6 | 9 | * |
| 7 | 10 | * @module OrchestrationEventRepository |
| ... | ... | @@ -44,7 +47,7 @@ export interface OrchestrationEventRepositoryShape { |
| 44 | 47 | } |
| 45 | 48 | |
| 46 | 49 | /** |
| 47 | * OrchestrationEventRepository - Context tag for orchestration event persistence. | |
| 50 | * OrchestrationEventRepository - Service tag for orchestration event persistence. | |
| 48 | 51 | * |
| 49 | 52 | * @example |
| 50 | 53 | * ```ts |
apps/server/src/persistence/Services/Projects.ts+5-2| ... | ... | @@ -1,7 +1,10 @@ |
| 1 | 1 | /** |
| 2 | 2 | * ProjectRepository - Repository interface for project persistence. |
| 3 | 3 | * |
| 4 | * Uses Effect `Context.Tag` for dependency injection and returns typed | |
| 4 | * Owns persisted project records (cwd, scripts, and cleanup of missing paths). | |
| 5 | * It does not manage provider sessions, orchestration events, or websocket APIs. | |
| 6 | * | |
| 7 | * Uses Effect `ServiceMap.Service` for dependency injection and returns typed | |
| 5 | 8 | * domain errors for all operations. |
| 6 | 9 | * |
| 7 | 10 | * @module ProjectRepository |
| ... | ... | @@ -62,7 +65,7 @@ export interface ProjectRepositoryShape { |
| 62 | 65 | } |
| 63 | 66 | |
| 64 | 67 | /** |
| 65 | * ProjectRepository - Context tag for project persistence dependency injection. | |
| 68 | * ProjectRepository - Service tag for project persistence dependency injection. | |
| 66 | 69 | * |
| 67 | 70 | * @example |
| 68 | 71 | * ```ts |
apps/server/src/provider/Errors.ts+40-105| ... | ... | @@ -1,163 +1,104 @@ |
| 1 | 1 | import { Schema } from "effect"; |
| 2 | 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 | } | |
| 3 | import type { CheckpointServiceError } from "../checkpointing/Errors.ts"; | |
| 69 | 4 | |
| 70 | 5 | /** |
| 71 | * CodexValidationError - Invalid Codex API input. | |
| 6 | * ProviderAdapterValidationError - Invalid adapter API input. | |
| 72 | 7 | */ |
| 73 | export class CodexValidationError extends Schema.TaggedErrorClass<CodexValidationError>()( | |
| 74 | "CodexValidationError", | |
| 8 | export class ProviderAdapterValidationError extends Schema.TaggedErrorClass<ProviderAdapterValidationError>()( | |
| 9 | "ProviderAdapterValidationError", | |
| 75 | 10 | { |
| 11 | provider: Schema.String, | |
| 76 | 12 | operation: Schema.String, |
| 77 | 13 | issue: Schema.String, |
| 78 | 14 | cause: Schema.optional(Schema.Defect), |
| 79 | 15 | }, |
| 80 | 16 | ) { |
| 81 | 17 | override get message(): string { |
| 82 | return `Codex validation failed in ${this.operation}: ${this.issue}`; | |
| 18 | return `Provider adapter validation failed (${this.provider}) in ${this.operation}: ${this.issue}`; | |
| 83 | 19 | } |
| 84 | 20 | } |
| 85 | 21 | |
| 86 | 22 | /** |
| 87 | * CodexSessionNotFoundError - Session id is unknown. | |
| 23 | * ProviderAdapterSessionNotFoundError - Adapter-owned session id is unknown. | |
| 88 | 24 | */ |
| 89 | export class CodexSessionNotFoundError extends Schema.TaggedErrorClass<CodexSessionNotFoundError>()( | |
| 90 | "CodexSessionNotFoundError", | |
| 25 | export class ProviderAdapterSessionNotFoundError extends Schema.TaggedErrorClass<ProviderAdapterSessionNotFoundError>()( | |
| 26 | "ProviderAdapterSessionNotFoundError", | |
| 91 | 27 | { |
| 28 | provider: Schema.String, | |
| 92 | 29 | sessionId: Schema.String, |
| 93 | 30 | cause: Schema.optional(Schema.Defect), |
| 94 | 31 | }, |
| 95 | 32 | ) { |
| 96 | 33 | override get message(): string { |
| 97 | return `Unknown codex session: ${this.sessionId}`; | |
| 34 | return `Unknown ${this.provider} adapter session: ${this.sessionId}`; | |
| 98 | 35 | } |
| 99 | 36 | } |
| 100 | 37 | |
| 101 | 38 | /** |
| 102 | * CodexSessionClosedError - Session exists but is closed. | |
| 39 | * ProviderAdapterSessionClosedError - Adapter session exists but is closed. | |
| 103 | 40 | */ |
| 104 | export class CodexSessionClosedError extends Schema.TaggedErrorClass<CodexSessionClosedError>()( | |
| 105 | "CodexSessionClosedError", | |
| 41 | export class ProviderAdapterSessionClosedError extends Schema.TaggedErrorClass<ProviderAdapterSessionClosedError>()( | |
| 42 | "ProviderAdapterSessionClosedError", | |
| 106 | 43 | { |
| 44 | provider: Schema.String, | |
| 107 | 45 | sessionId: Schema.String, |
| 108 | 46 | cause: Schema.optional(Schema.Defect), |
| 109 | 47 | }, |
| 110 | 48 | ) { |
| 111 | 49 | override get message(): string { |
| 112 | return `Codex session is closed: ${this.sessionId}`; | |
| 50 | return `${this.provider} adapter session is closed: ${this.sessionId}`; | |
| 113 | 51 | } |
| 114 | 52 | } |
| 115 | 53 | |
| 116 | 54 | /** |
| 117 | * CodexProtocolError - Invalid/unexpected JSON-RPC payload from app-server. | |
| 55 | * ProviderAdapterProtocolError - Invalid/unexpected provider protocol payload. | |
| 118 | 56 | */ |
| 119 | export class CodexProtocolError extends Schema.TaggedErrorClass<CodexProtocolError>()( | |
| 120 | "CodexProtocolError", | |
| 57 | export class ProviderAdapterProtocolError extends Schema.TaggedErrorClass<ProviderAdapterProtocolError>()( | |
| 58 | "ProviderAdapterProtocolError", | |
| 121 | 59 | { |
| 60 | provider: Schema.String, | |
| 122 | 61 | operation: Schema.String, |
| 123 | 62 | detail: Schema.String, |
| 124 | 63 | cause: Schema.optional(Schema.Defect), |
| 125 | 64 | }, |
| 126 | 65 | ) { |
| 127 | 66 | override get message(): string { |
| 128 | return `Codex protocol error in ${this.operation}: ${this.detail}`; | |
| 67 | return `Provider adapter protocol error (${this.provider}) in ${this.operation}: ${this.detail}`; | |
| 129 | 68 | } |
| 130 | 69 | } |
| 131 | 70 | |
| 132 | 71 | /** |
| 133 | * CodexRequestError - JSON-RPC request failed or timed out. | |
| 72 | * ProviderAdapterRequestError - Provider protocol request failed or timed out. | |
| 134 | 73 | */ |
| 135 | export class CodexRequestError extends Schema.TaggedErrorClass<CodexRequestError>()( | |
| 136 | "CodexRequestError", | |
| 74 | export class ProviderAdapterRequestError extends Schema.TaggedErrorClass<ProviderAdapterRequestError>()( | |
| 75 | "ProviderAdapterRequestError", | |
| 137 | 76 | { |
| 77 | provider: Schema.String, | |
| 138 | 78 | method: Schema.String, |
| 139 | 79 | detail: Schema.String, |
| 140 | 80 | cause: Schema.optional(Schema.Defect), |
| 141 | 81 | }, |
| 142 | 82 | ) { |
| 143 | 83 | override get message(): string { |
| 144 | return `Codex request failed for ${this.method}: ${this.detail}`; | |
| 84 | return `Provider adapter request failed (${this.provider}) for ${this.method}: ${this.detail}`; | |
| 145 | 85 | } |
| 146 | 86 | } |
| 147 | 87 | |
| 148 | 88 | /** |
| 149 | * CodexProcessError - codex app-server process lifecycle failure. | |
| 89 | * ProviderAdapterProcessError - Provider process lifecycle failure. | |
| 150 | 90 | */ |
| 151 | export class CodexProcessError extends Schema.TaggedErrorClass<CodexProcessError>()( | |
| 152 | "CodexProcessError", | |
| 91 | export class ProviderAdapterProcessError extends Schema.TaggedErrorClass<ProviderAdapterProcessError>()( | |
| 92 | "ProviderAdapterProcessError", | |
| 153 | 93 | { |
| 94 | provider: Schema.String, | |
| 154 | 95 | sessionId: Schema.String, |
| 155 | 96 | detail: Schema.String, |
| 156 | 97 | cause: Schema.optional(Schema.Defect), |
| 157 | 98 | }, |
| 158 | 99 | ) { |
| 159 | 100 | override get message(): string { |
| 160 | return `Codex process error for session ${this.sessionId}: ${this.detail}`; | |
| 101 | return `Provider adapter process error (${this.provider}) for session ${this.sessionId}: ${this.detail}`; | |
| 161 | 102 | } |
| 162 | 103 | } |
| 163 | 104 | |
| ... | ... | @@ -257,19 +198,13 @@ export class ProviderFilesystemError extends Schema.TaggedErrorClass<ProviderFil |
| 257 | 198 | } |
| 258 | 199 | } |
| 259 | 200 | |
| 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; | |
| 201 | export type ProviderAdapterError = | |
| 202 | | ProviderAdapterValidationError | |
| 203 | | ProviderAdapterSessionNotFoundError | |
| 204 | | ProviderAdapterSessionClosedError | |
| 205 | | ProviderAdapterProtocolError | |
| 206 | | ProviderAdapterRequestError | |
| 207 | | ProviderAdapterProcessError; | |
| 273 | 208 | |
| 274 | 209 | export type ProviderServiceError = |
| 275 | 210 | | ProviderValidationError |
| ... | ... | @@ -278,5 +213,5 @@ export type ProviderServiceError = |
| 278 | 213 | | ProviderCheckpointUnavailableError |
| 279 | 214 | | ProviderCheckpointRangeError |
| 280 | 215 | | ProviderFilesystemError |
| 281 | | CodexServiceError | |
| 282 | | CheckpointStoreError; | |
| 216 | | ProviderAdapterError | |
| 217 | | CheckpointServiceError; |
apps/server/src/provider/Services/CheckpointStore.ts deleted-91| ... | ... | @@ -1,91 +0,0 @@ |
| 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 deleted-110| ... | ... | @@ -1,110 +0,0 @@ |
| 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/CodexAdapter.ts created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | /** | |
| 2 | * CodexAdapter - Codex implementation of the generic provider adapter contract. | |
| 3 | * | |
| 4 | * This service owns Codex app-server process / JSON-RPC semantics and emits | |
| 5 | * Codex provider events. It does not perform cross-provider routing, shared | |
| 6 | * event fan-out, or checkpoint orchestration. | |
| 7 | * | |
| 8 | * Uses Effect `ServiceMap.Service` for dependency injection and returns the | |
| 9 | * shared provider-adapter error channel with `provider: "codex"` context. | |
| 10 | * | |
| 11 | * @module CodexAdapter | |
| 12 | */ | |
| 13 | import { ServiceMap } from "effect"; | |
| 14 | ||
| 15 | import type { ProviderAdapterError } from "../Errors.ts"; | |
| 16 | import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; | |
| 17 | ||
| 18 | export interface CodexAdapterShape extends ProviderAdapterShape<ProviderAdapterError> { | |
| 19 | readonly provider: "codex"; | |
| 20 | } | |
| 21 | ||
| 22 | /** | |
| 23 | * CodexAdapter - Service tag for Codex provider adapter operations. | |
| 24 | */ | |
| 25 | export class CodexAdapter extends ServiceMap.Service<CodexAdapter, CodexAdapterShape>()( | |
| 26 | "provider/CodexAdapter", | |
| 27 | ) {} |
apps/server/src/provider/Services/Provider.ts deleted-113| ... | ... | @@ -1,113 +0,0 @@ |
| 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/Services/ProviderAdapter.ts created+104| ... | ... | @@ -0,0 +1,104 @@ |
| 1 | /** | |
| 2 | * ProviderAdapter - Provider-specific runtime adapter contract. | |
| 3 | * | |
| 4 | * Defines the provider-native session/protocol operations that `ProviderService` | |
| 5 | * routes to after resolving the target provider. Implementations should focus | |
| 6 | * on provider behavior only and avoid cross-provider orchestration concerns. | |
| 7 | * | |
| 8 | * @module ProviderAdapter | |
| 9 | */ | |
| 10 | import type { | |
| 11 | ProviderApprovalDecision, | |
| 12 | ProviderEvent, | |
| 13 | ProviderKind, | |
| 14 | ProviderSendTurnInput, | |
| 15 | ProviderSession, | |
| 16 | ProviderSessionStartInput, | |
| 17 | ProviderTurnStartResult, | |
| 18 | } from "@t3tools/contracts"; | |
| 19 | import type { Effect } from "effect"; | |
| 20 | ||
| 21 | export interface ProviderThreadTurnSnapshot { | |
| 22 | readonly id: string; | |
| 23 | readonly items: ReadonlyArray<unknown>; | |
| 24 | } | |
| 25 | ||
| 26 | export interface ProviderThreadSnapshot { | |
| 27 | readonly threadId: string; | |
| 28 | readonly turns: ReadonlyArray<ProviderThreadTurnSnapshot>; | |
| 29 | } | |
| 30 | ||
| 31 | export interface ProviderAdapterShape<TError> { | |
| 32 | /** | |
| 33 | * Provider kind implemented by this adapter. | |
| 34 | */ | |
| 35 | readonly provider: ProviderKind; | |
| 36 | ||
| 37 | /** | |
| 38 | * Start a provider-backed session. | |
| 39 | */ | |
| 40 | readonly startSession: ( | |
| 41 | input: ProviderSessionStartInput, | |
| 42 | ) => Effect.Effect<ProviderSession, TError>; | |
| 43 | ||
| 44 | /** | |
| 45 | * Send a turn to an active provider session. | |
| 46 | */ | |
| 47 | readonly sendTurn: ( | |
| 48 | input: ProviderSendTurnInput, | |
| 49 | ) => Effect.Effect<ProviderTurnStartResult, TError>; | |
| 50 | ||
| 51 | /** | |
| 52 | * Interrupt an active turn. | |
| 53 | */ | |
| 54 | readonly interruptTurn: (sessionId: string, turnId?: string) => Effect.Effect<void, TError>; | |
| 55 | ||
| 56 | /** | |
| 57 | * Respond to an interactive approval request. | |
| 58 | */ | |
| 59 | readonly respondToRequest: ( | |
| 60 | sessionId: string, | |
| 61 | requestId: string, | |
| 62 | decision: ProviderApprovalDecision, | |
| 63 | ) => Effect.Effect<void, TError>; | |
| 64 | ||
| 65 | /** | |
| 66 | * Stop one provider session. | |
| 67 | */ | |
| 68 | readonly stopSession: (sessionId: string) => Effect.Effect<void, TError>; | |
| 69 | ||
| 70 | /** | |
| 71 | * List currently active provider sessions for this adapter. | |
| 72 | */ | |
| 73 | readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>; | |
| 74 | ||
| 75 | /** | |
| 76 | * Check whether this adapter owns an active session id. | |
| 77 | */ | |
| 78 | readonly hasSession: (sessionId: string) => Effect.Effect<boolean>; | |
| 79 | ||
| 80 | /** | |
| 81 | * Read a provider thread snapshot. | |
| 82 | */ | |
| 83 | readonly readThread: (sessionId: string) => Effect.Effect<ProviderThreadSnapshot, TError>; | |
| 84 | ||
| 85 | /** | |
| 86 | * Roll back a provider thread by N turns. | |
| 87 | */ | |
| 88 | readonly rollbackThread: ( | |
| 89 | sessionId: string, | |
| 90 | numTurns: number, | |
| 91 | ) => Effect.Effect<ProviderThreadSnapshot, TError>; | |
| 92 | ||
| 93 | /** | |
| 94 | * Stop all sessions owned by this adapter. | |
| 95 | */ | |
| 96 | readonly stopAll: () => Effect.Effect<void, TError>; | |
| 97 | ||
| 98 | /** | |
| 99 | * Subscribe to provider events emitted by this adapter. | |
| 100 | */ | |
| 101 | readonly subscribeToEvents: ( | |
| 102 | callback: (event: ProviderEvent) => void, | |
| 103 | ) => Effect.Effect<() => void, TError>; | |
| 104 | } |
apps/server/src/provider/Services/ProviderAdapterRegistry.ts created+37| ... | ... | @@ -0,0 +1,37 @@ |
| 1 | /** | |
| 2 | * ProviderAdapterRegistry - Lookup boundary for provider adapter implementations. | |
| 3 | * | |
| 4 | * Maps a provider kind to the concrete adapter service (Codex, Claude, etc). | |
| 5 | * It does not own session lifecycle or routing rules; `ProviderService` uses | |
| 6 | * this registry together with `ProviderSessionDirectory`. | |
| 7 | * | |
| 8 | * @module ProviderAdapterRegistry | |
| 9 | */ | |
| 10 | import type { ProviderKind } from "@t3tools/contracts"; | |
| 11 | import { ServiceMap } from "effect"; | |
| 12 | import type { Effect } from "effect"; | |
| 13 | ||
| 14 | import type { ProviderAdapterError, ProviderUnsupportedError } from "../Errors.ts"; | |
| 15 | import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; | |
| 16 | ||
| 17 | export interface ProviderAdapterRegistryShape { | |
| 18 | /** | |
| 19 | * Resolve the adapter for a provider kind. | |
| 20 | */ | |
| 21 | readonly getByProvider: ( | |
| 22 | provider: ProviderKind, | |
| 23 | ) => Effect.Effect<ProviderAdapterShape<ProviderAdapterError>, ProviderUnsupportedError>; | |
| 24 | ||
| 25 | /** | |
| 26 | * List provider kinds currently registered. | |
| 27 | */ | |
| 28 | readonly listProviders: () => Effect.Effect<ReadonlyArray<ProviderKind>>; | |
| 29 | } | |
| 30 | ||
| 31 | /** | |
| 32 | * ProviderAdapterRegistry - Service tag for provider adapter lookup. | |
| 33 | */ | |
| 34 | export class ProviderAdapterRegistry extends ServiceMap.Service< | |
| 35 | ProviderAdapterRegistry, | |
| 36 | ProviderAdapterRegistryShape | |
| 37 | >()("provider/ProviderAdapterRegistry") {} |
apps/server/src/provider/Services/ProviderService.ts created+117| ... | ... | @@ -0,0 +1,117 @@ |
| 1 | /** | |
| 2 | * ProviderService - Service interface for provider sessions, turns, and checkpoints. | |
| 3 | * | |
| 4 | * Acts as the cross-provider facade used by transports (WebSocket/RPC). It | |
| 5 | * resolves provider adapters through `ProviderAdapterRegistry`, routes | |
| 6 | * session-scoped calls via `ProviderSessionDirectory`, and exposes one unified | |
| 7 | * provider event stream to callers. | |
| 8 | * | |
| 9 | * Uses Effect `ServiceMap.Service` for dependency injection and returns typed | |
| 10 | * domain errors for validation, session, codex, and checkpoint workflows. | |
| 11 | * | |
| 12 | * @module ProviderService | |
| 13 | */ | |
| 14 | import type { | |
| 15 | ProviderEvent, | |
| 16 | ProviderGetCheckpointDiffInput, | |
| 17 | ProviderGetCheckpointDiffResult, | |
| 18 | ProviderInterruptTurnInput, | |
| 19 | ProviderListCheckpointsInput, | |
| 20 | ProviderListCheckpointsResult, | |
| 21 | ProviderRespondToRequestInput, | |
| 22 | ProviderRevertToCheckpointInput, | |
| 23 | ProviderRevertToCheckpointResult, | |
| 24 | ProviderSendTurnInput, | |
| 25 | ProviderSession, | |
| 26 | ProviderSessionStartInput, | |
| 27 | ProviderStopSessionInput, | |
| 28 | ProviderTurnStartResult, | |
| 29 | } from "@t3tools/contracts"; | |
| 30 | import { ServiceMap } from "effect"; | |
| 31 | import type { Effect } from "effect"; | |
| 32 | ||
| 33 | import type { ProviderServiceError } from "../Errors.ts"; | |
| 34 | ||
| 35 | export interface ProviderServiceShape { | |
| 36 | /** | |
| 37 | * Start a provider session. | |
| 38 | */ | |
| 39 | readonly startSession: ( | |
| 40 | input: ProviderSessionStartInput, | |
| 41 | ) => Effect.Effect<ProviderSession, ProviderServiceError>; | |
| 42 | ||
| 43 | /** | |
| 44 | * Send a provider turn. | |
| 45 | */ | |
| 46 | readonly sendTurn: ( | |
| 47 | input: ProviderSendTurnInput, | |
| 48 | ) => Effect.Effect<ProviderTurnStartResult, ProviderServiceError>; | |
| 49 | ||
| 50 | /** | |
| 51 | * Interrupt a running provider turn. | |
| 52 | */ | |
| 53 | readonly interruptTurn: ( | |
| 54 | input: ProviderInterruptTurnInput, | |
| 55 | ) => Effect.Effect<void, ProviderServiceError>; | |
| 56 | ||
| 57 | /** | |
| 58 | * Respond to a provider approval request. | |
| 59 | */ | |
| 60 | readonly respondToRequest: ( | |
| 61 | input: ProviderRespondToRequestInput, | |
| 62 | ) => Effect.Effect<void, ProviderServiceError>; | |
| 63 | ||
| 64 | /** | |
| 65 | * Stop a provider session. | |
| 66 | */ | |
| 67 | readonly stopSession: ( | |
| 68 | input: ProviderStopSessionInput, | |
| 69 | ) => Effect.Effect<void, ProviderServiceError>; | |
| 70 | ||
| 71 | /** | |
| 72 | * List active provider sessions. | |
| 73 | */ | |
| 74 | readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>; | |
| 75 | ||
| 76 | /** | |
| 77 | * List checkpoints for a provider session. | |
| 78 | */ | |
| 79 | readonly listCheckpoints: ( | |
| 80 | input: ProviderListCheckpointsInput, | |
| 81 | ) => Effect.Effect<ProviderListCheckpointsResult, ProviderServiceError>; | |
| 82 | ||
| 83 | /** | |
| 84 | * Diff two checkpoints for a provider session. | |
| 85 | */ | |
| 86 | readonly getCheckpointDiff: ( | |
| 87 | input: ProviderGetCheckpointDiffInput, | |
| 88 | ) => Effect.Effect<ProviderGetCheckpointDiffResult, ProviderServiceError>; | |
| 89 | ||
| 90 | /** | |
| 91 | * Revert a provider session to a checkpoint. | |
| 92 | */ | |
| 93 | readonly revertToCheckpoint: ( | |
| 94 | input: ProviderRevertToCheckpointInput, | |
| 95 | ) => Effect.Effect<ProviderRevertToCheckpointResult, ProviderServiceError>; | |
| 96 | ||
| 97 | /** | |
| 98 | * Stop all provider sessions. | |
| 99 | */ | |
| 100 | readonly stopAll: () => Effect.Effect<void, ProviderServiceError>; | |
| 101 | ||
| 102 | /** | |
| 103 | * Subscribe to provider event stream. | |
| 104 | * | |
| 105 | * Fan-out is owned by ProviderService (not by a standalone event-bus service). | |
| 106 | */ | |
| 107 | readonly subscribeToEvents: ( | |
| 108 | callback: (event: ProviderEvent) => void, | |
| 109 | ) => Effect.Effect<() => void, ProviderServiceError>; | |
| 110 | } | |
| 111 | ||
| 112 | /** | |
| 113 | * ProviderService - Service tag for provider orchestration. | |
| 114 | */ | |
| 115 | export class ProviderService extends ServiceMap.Service<ProviderService, ProviderServiceShape>()( | |
| 116 | "provider/ProviderService", | |
| 117 | ) {} |
apps/server/src/provider/Services/ProviderSessionDirectory.ts created+61| ... | ... | @@ -0,0 +1,61 @@ |
| 1 | /** | |
| 2 | * ProviderSessionDirectory - Session ownership index across provider adapters. | |
| 3 | * | |
| 4 | * Tracks which provider owns each `sessionId` so `ProviderService` can route | |
| 5 | * session-scoped calls to the correct adapter. It is metadata only and does not | |
| 6 | * perform provider RPC or checkpoint operations. | |
| 7 | * | |
| 8 | * @module ProviderSessionDirectory | |
| 9 | */ | |
| 10 | import type { ProviderKind } from "@t3tools/contracts"; | |
| 11 | import { Option, ServiceMap } from "effect"; | |
| 12 | import type { Effect } from "effect"; | |
| 13 | ||
| 14 | import type { ProviderSessionNotFoundError, ProviderValidationError } from "../Errors.ts"; | |
| 15 | ||
| 16 | export interface ProviderSessionBinding { | |
| 17 | readonly sessionId: string; | |
| 18 | readonly provider: ProviderKind; | |
| 19 | readonly threadId?: string; | |
| 20 | } | |
| 21 | ||
| 22 | export interface ProviderSessionDirectoryShape { | |
| 23 | /** | |
| 24 | * Record or update ownership for one provider session. | |
| 25 | */ | |
| 26 | readonly upsert: ( | |
| 27 | binding: ProviderSessionBinding, | |
| 28 | ) => Effect.Effect<void, ProviderValidationError>; | |
| 29 | ||
| 30 | /** | |
| 31 | * Resolve the provider owner for a session id. | |
| 32 | */ | |
| 33 | readonly getProvider: ( | |
| 34 | sessionId: string, | |
| 35 | ) => Effect.Effect<ProviderKind, ProviderSessionNotFoundError>; | |
| 36 | ||
| 37 | /** | |
| 38 | * Resolve the tracked thread id for a session, if known. | |
| 39 | */ | |
| 40 | readonly getThreadId: ( | |
| 41 | sessionId: string, | |
| 42 | ) => Effect.Effect<Option.Option<string>, ProviderSessionNotFoundError>; | |
| 43 | ||
| 44 | /** | |
| 45 | * Remove a session binding. | |
| 46 | */ | |
| 47 | readonly remove: (sessionId: string) => Effect.Effect<void>; | |
| 48 | ||
| 49 | /** | |
| 50 | * List tracked session ids. | |
| 51 | */ | |
| 52 | readonly listSessionIds: () => Effect.Effect<ReadonlyArray<string>>; | |
| 53 | } | |
| 54 | ||
| 55 | /** | |
| 56 | * ProviderSessionDirectory - Service tag for session ownership lookup. | |
| 57 | */ | |
| 58 | export class ProviderSessionDirectory extends ServiceMap.Service< | |
| 59 | ProviderSessionDirectory, | |
| 60 | ProviderSessionDirectoryShape | |
| 61 | >()("provider/ProviderSessionDirectory") {} |
apps/server/src/provider/contracts.test.ts+149-38| ... | ... | @@ -1,22 +1,31 @@ |
| 1 | 1 | import { describe, expect, it } from "vitest"; |
| 2 | 2 | |
| 3 | import { Effect, Layer, ManagedRuntime } from "effect"; | |
| 3 | import { Effect, Layer, ManagedRuntime, Option } from "effect"; | |
| 4 | 4 | |
| 5 | 5 | import { |
| 6 | CheckpointValidationError, | |
| 7 | CodexValidationError, | |
| 6 | ProviderAdapterValidationError, | |
| 8 | 7 | ProviderFilesystemError, |
| 8 | ProviderSessionNotFoundError, | |
| 9 | 9 | ProviderUnsupportedError, |
| 10 | type CheckpointStoreError, | |
| 11 | type CodexServiceError, | |
| 10 | type ProviderAdapterError, | |
| 12 | 11 | type ProviderServiceError, |
| 13 | 12 | } from "./Errors.ts"; |
| 14 | import { CheckpointStore } from "./Services/CheckpointStore.ts"; | |
| 15 | import { CodexService } from "./Services/Codex.ts"; | |
| 16 | import { ProviderService } from "./Services/Provider.ts"; | |
| 13 | import { | |
| 14 | CheckpointValidationError, | |
| 15 | type CheckpointCatalogError, | |
| 16 | type CheckpointServiceError, | |
| 17 | type CheckpointStoreError, | |
| 18 | } from "../checkpointing/Errors.ts"; | |
| 19 | import { CheckpointCatalog } from "../checkpointing/Services/CheckpointCatalog.ts"; | |
| 20 | import { CheckpointService } from "../checkpointing/Services/CheckpointService.ts"; | |
| 21 | import { CheckpointStore } from "../checkpointing/Services/CheckpointStore.ts"; | |
| 22 | import { CodexAdapter } from "./Services/CodexAdapter.ts"; | |
| 23 | import { ProviderAdapterRegistry } from "./Services/ProviderAdapterRegistry.ts"; | |
| 24 | import { ProviderService } from "./Services/ProviderService.ts"; | |
| 25 | import { ProviderSessionDirectory } from "./Services/ProviderSessionDirectory.ts"; | |
| 17 | 26 | |
| 18 | describe("provider service contracts", () => { | |
| 19 | it("resolves provider, codex, and checkpoint services from ServiceMap tags", async () => { | |
| 27 | describe("provider/checkpoint service contracts", () => { | |
| 28 | it("resolves all service tags from ServiceMap", async () => { | |
| 20 | 29 | const providerLive: typeof ProviderService.Service = { |
| 21 | 30 | startSession: () => |
| 22 | 31 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), |
| ... | ... | @@ -35,34 +44,58 @@ describe("provider service contracts", () => { |
| 35 | 44 | revertToCheckpoint: () => |
| 36 | 45 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), |
| 37 | 46 | stopAll: () => Effect.void, |
| 38 | dispose: () => Effect.void, | |
| 39 | 47 | subscribeToEvents: () => Effect.succeed(() => undefined), |
| 40 | 48 | }; |
| 41 | 49 | |
| 42 | const codexLive: typeof CodexService.Service = { | |
| 50 | const codexLive: typeof CodexAdapter.Service = { | |
| 51 | provider: "codex", | |
| 43 | 52 | startSession: () => |
| 44 | Effect.fail<CodexServiceError>( | |
| 45 | new CodexValidationError({ operation: "startSession", issue: "not implemented" }), | |
| 53 | Effect.fail<ProviderAdapterError>( | |
| 54 | new ProviderAdapterValidationError({ | |
| 55 | provider: "codex", | |
| 56 | operation: "startSession", | |
| 57 | issue: "not implemented", | |
| 58 | }), | |
| 46 | 59 | ), |
| 47 | 60 | sendTurn: () => |
| 48 | Effect.fail<CodexServiceError>( | |
| 49 | new CodexValidationError({ operation: "sendTurn", issue: "not implemented" }), | |
| 61 | Effect.fail<ProviderAdapterError>( | |
| 62 | new ProviderAdapterValidationError({ | |
| 63 | provider: "codex", | |
| 64 | operation: "sendTurn", | |
| 65 | issue: "not implemented", | |
| 66 | }), | |
| 50 | 67 | ), |
| 51 | 68 | interruptTurn: () => |
| 52 | Effect.fail<CodexServiceError>( | |
| 53 | new CodexValidationError({ operation: "interruptTurn", issue: "not implemented" }), | |
| 69 | Effect.fail<ProviderAdapterError>( | |
| 70 | new ProviderAdapterValidationError({ | |
| 71 | provider: "codex", | |
| 72 | operation: "interruptTurn", | |
| 73 | issue: "not implemented", | |
| 74 | }), | |
| 54 | 75 | ), |
| 55 | 76 | readThread: () => |
| 56 | Effect.fail<CodexServiceError>( | |
| 57 | new CodexValidationError({ operation: "readThread", issue: "not implemented" }), | |
| 77 | Effect.fail<ProviderAdapterError>( | |
| 78 | new ProviderAdapterValidationError({ | |
| 79 | provider: "codex", | |
| 80 | operation: "readThread", | |
| 81 | issue: "not implemented", | |
| 82 | }), | |
| 58 | 83 | ), |
| 59 | 84 | rollbackThread: () => |
| 60 | Effect.fail<CodexServiceError>( | |
| 61 | new CodexValidationError({ operation: "rollbackThread", issue: "not implemented" }), | |
| 85 | Effect.fail<ProviderAdapterError>( | |
| 86 | new ProviderAdapterValidationError({ | |
| 87 | provider: "codex", | |
| 88 | operation: "rollbackThread", | |
| 89 | issue: "not implemented", | |
| 90 | }), | |
| 62 | 91 | ), |
| 63 | 92 | respondToRequest: () => |
| 64 | Effect.fail<CodexServiceError>( | |
| 65 | new CodexValidationError({ operation: "respondToRequest", issue: "not implemented" }), | |
| 93 | Effect.fail<ProviderAdapterError>( | |
| 94 | new ProviderAdapterValidationError({ | |
| 95 | provider: "codex", | |
| 96 | operation: "respondToRequest", | |
| 97 | issue: "not implemented", | |
| 98 | }), | |
| 66 | 99 | ), |
| 67 | 100 | stopSession: () => Effect.void, |
| 68 | 101 | listSessions: () => Effect.succeed([]), |
| ... | ... | @@ -71,7 +104,7 @@ describe("provider service contracts", () => { |
| 71 | 104 | subscribeToEvents: () => Effect.succeed(() => undefined), |
| 72 | 105 | }; |
| 73 | 106 | |
| 74 | const checkpointLive: typeof CheckpointStore.Service = { | |
| 107 | const checkpointStoreLive: typeof CheckpointStore.Service = { | |
| 75 | 108 | isGitRepository: () => Effect.succeed(false), |
| 76 | 109 | captureCheckpoint: () => Effect.void, |
| 77 | 110 | hasCheckpoint: () => Effect.succeed(false), |
| ... | ... | @@ -87,39 +120,117 @@ describe("provider service contracts", () => { |
| 87 | 120 | pruneAfterTurn: () => Effect.void, |
| 88 | 121 | }; |
| 89 | 122 | |
| 123 | const checkpointCatalogLive: typeof CheckpointCatalog.Service = { | |
| 124 | upsertCheckpoint: () => Effect.void, | |
| 125 | listCheckpoints: () => Effect.succeed([]), | |
| 126 | getCheckpoint: () => Effect.succeed(Option.none()), | |
| 127 | deleteAfterTurn: () => Effect.void, | |
| 128 | deleteAllForSession: () => Effect.void, | |
| 129 | }; | |
| 130 | ||
| 131 | const checkpointServiceLive: typeof CheckpointService.Service = { | |
| 132 | initializeForSession: () => Effect.void, | |
| 133 | captureCurrentTurn: () => Effect.void, | |
| 134 | listCheckpoints: () => | |
| 135 | Effect.fail<CheckpointServiceError>( | |
| 136 | new CheckpointValidationError({ operation: "listCheckpoints", issue: "not implemented" }), | |
| 137 | ), | |
| 138 | getCheckpointDiff: () => | |
| 139 | Effect.fail<CheckpointServiceError>( | |
| 140 | new CheckpointValidationError({ | |
| 141 | operation: "getCheckpointDiff", | |
| 142 | issue: "not implemented", | |
| 143 | }), | |
| 144 | ), | |
| 145 | revertToCheckpoint: () => | |
| 146 | Effect.fail<CheckpointServiceError>( | |
| 147 | new CheckpointValidationError({ | |
| 148 | operation: "revertToCheckpoint", | |
| 149 | issue: "not implemented", | |
| 150 | }), | |
| 151 | ), | |
| 152 | }; | |
| 153 | ||
| 154 | const providerAdapterRegistryLive: typeof ProviderAdapterRegistry.Service = { | |
| 155 | getByProvider: (provider) => | |
| 156 | provider === "codex" | |
| 157 | ? Effect.succeed(codexLive) | |
| 158 | : Effect.fail<ProviderUnsupportedError>(new ProviderUnsupportedError({ provider })), | |
| 159 | listProviders: () => Effect.succeed(["codex"]), | |
| 160 | }; | |
| 161 | ||
| 162 | const providerSessionDirectoryLive: typeof ProviderSessionDirectory.Service = { | |
| 163 | upsert: () => Effect.void, | |
| 164 | getProvider: (sessionId) => | |
| 165 | Effect.fail<ProviderSessionNotFoundError>(new ProviderSessionNotFoundError({ sessionId })), | |
| 166 | getThreadId: (sessionId) => | |
| 167 | Effect.fail<ProviderSessionNotFoundError>(new ProviderSessionNotFoundError({ sessionId })), | |
| 168 | remove: () => Effect.void, | |
| 169 | listSessionIds: () => Effect.succeed([]), | |
| 170 | }; | |
| 171 | ||
| 90 | 172 | const runtime = ManagedRuntime.make( |
| 91 | 173 | Layer.mergeAll( |
| 92 | 174 | Layer.succeed(ProviderService, providerLive), |
| 93 | Layer.succeed(CodexService, codexLive), | |
| 94 | Layer.succeed(CheckpointStore, checkpointLive), | |
| 175 | Layer.succeed(CodexAdapter, codexLive), | |
| 176 | Layer.succeed(CheckpointStore, checkpointStoreLive), | |
| 177 | Layer.succeed(CheckpointCatalog, checkpointCatalogLive), | |
| 178 | Layer.succeed(CheckpointService, checkpointServiceLive), | |
| 179 | Layer.succeed(ProviderAdapterRegistry, providerAdapterRegistryLive), | |
| 180 | Layer.succeed(ProviderSessionDirectory, providerSessionDirectoryLive), | |
| 95 | 181 | ), |
| 96 | 182 | ); |
| 97 | 183 | |
| 98 | const [provider, codex, checkpoint] = await Promise.all([ | |
| 184 | const [ | |
| 185 | provider, | |
| 186 | codex, | |
| 187 | checkpointStore, | |
| 188 | checkpointCatalog, | |
| 189 | checkpointService, | |
| 190 | providerAdapterRegistry, | |
| 191 | providerSessionDirectory, | |
| 192 | ] = await Promise.all([ | |
| 99 | 193 | runtime.runPromise(Effect.service(ProviderService)), |
| 100 | runtime.runPromise(Effect.service(CodexService)), | |
| 194 | runtime.runPromise(Effect.service(CodexAdapter)), | |
| 101 | 195 | runtime.runPromise(Effect.service(CheckpointStore)), |
| 196 | runtime.runPromise(Effect.service(CheckpointCatalog)), | |
| 197 | runtime.runPromise(Effect.service(CheckpointService)), | |
| 198 | runtime.runPromise(Effect.service(ProviderAdapterRegistry)), | |
| 199 | runtime.runPromise(Effect.service(ProviderSessionDirectory)), | |
| 102 | 200 | ]); |
| 103 | 201 | |
| 104 | 202 | expect(provider).toBe(providerLive); |
| 105 | 203 | expect(codex).toBe(codexLive); |
| 106 | expect(checkpoint).toBe(checkpointLive); | |
| 204 | expect(checkpointStore).toBe(checkpointStoreLive); | |
| 205 | expect(checkpointCatalog).toBe(checkpointCatalogLive); | |
| 206 | expect(checkpointService).toBe(checkpointServiceLive); | |
| 207 | expect(providerAdapterRegistry).toBe(providerAdapterRegistryLive); | |
| 208 | expect(providerSessionDirectory).toBe(providerSessionDirectoryLive); | |
| 107 | 209 | |
| 108 | 210 | await runtime.dispose(); |
| 109 | 211 | }); |
| 110 | 212 | }); |
| 111 | 213 | |
| 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({ | |
| 214 | describe("provider/checkpoint errors", () => { | |
| 215 | it("preserves cause and message on tagged errors", () => { | |
| 216 | const fsCause = new Error("filesystem failed"); | |
| 217 | const fsError = new ProviderFilesystemError({ | |
| 116 | 218 | sessionId: "sess-1", |
| 117 | 219 | detail: "capture failed", |
| 118 | cause, | |
| 220 | cause: fsCause, | |
| 221 | }); | |
| 222 | ||
| 223 | expect(fsError._tag).toBe("ProviderFilesystemError"); | |
| 224 | expect(fsError.message).toContain("sess-1"); | |
| 225 | expect(fsError.cause).toBe(fsCause); | |
| 226 | }); | |
| 227 | ||
| 228 | it("accepts checkpoint catalog error channel type", () => { | |
| 229 | const error: CheckpointCatalogError = new CheckpointValidationError({ | |
| 230 | operation: "upsertCheckpoint", | |
| 231 | issue: "turnCount must be >= 0", | |
| 119 | 232 | }); |
| 120 | 233 | |
| 121 | expect(error._tag).toBe("ProviderFilesystemError"); | |
| 122 | expect(error.message).toContain("sess-1"); | |
| 123 | expect(error.cause).toBe(cause); | |
| 234 | expect(error._tag).toBe("CheckpointValidationError"); | |
| 124 | 235 | }); |
| 125 | 236 | }); |