| 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,7 +1,11 @@ |
| 1 | /** | 1 | /** |
| 2 | * OrchestrationEngineService - Service interface for orchestration command handling. | 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 | * replay, and unknown-input decoding all return typed domain errors. | 9 | * replay, and unknown-input decoding all return typed domain errors. |
| 6 | * | 10 | * |
| 7 | * @module OrchestrationEngineService | 11 | * @module OrchestrationEngineService |
| ... | @@ -79,7 +83,7 @@ export interface OrchestrationEngineShape { | ... | @@ -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 | * @example | 88 | * @example |
| 85 | * ```ts | 89 | * ```ts |
apps/server/src/persistence/Services/OrchestrationEvents.ts+5-2| ... | @@ -1,7 +1,10 @@ | ... | @@ -1,7 +1,10 @@ |
| 1 | /** | 1 | /** |
| 2 | * OrchestrationEventRepository - Repository interface for orchestration events. | 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 | * persistence/decode errors for event append and replay operations. | 8 | * persistence/decode errors for event append and replay operations. |
| 6 | * | 9 | * |
| 7 | * @module OrchestrationEventRepository | 10 | * @module OrchestrationEventRepository |
| ... | @@ -44,7 +47,7 @@ export interface OrchestrationEventRepositoryShape { | ... | @@ -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 | * @example | 52 | * @example |
| 50 | * ```ts | 53 | * ```ts |
apps/server/src/persistence/Services/Projects.ts+5-2| ... | @@ -1,7 +1,10 @@ | ... | @@ -1,7 +1,10 @@ |
| 1 | /** | 1 | /** |
| 2 | * ProjectRepository - Repository interface for project persistence. | 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 | * domain errors for all operations. | 8 | * domain errors for all operations. |
| 6 | * | 9 | * |
| 7 | * @module ProjectRepository | 10 | * @module ProjectRepository |
| ... | @@ -62,7 +65,7 @@ export interface ProjectRepositoryShape { | ... | @@ -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 | * @example | 70 | * @example |
| 68 | * ```ts | 71 | * ```ts |
apps/server/src/provider/Errors.ts+40-105| ... | @@ -1,163 +1,104 @@ | ... | @@ -1,163 +1,104 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | 2 | ||
| 3 | /** | 3 | import type { CheckpointServiceError } from "../checkpointing/Errors.ts"; |
| 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 | 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>()( | 8 | export class ProviderAdapterValidationError extends Schema.TaggedErrorClass<ProviderAdapterValidationError>()( |
| 74 | "CodexValidationError", | 9 | "ProviderAdapterValidationError", |
| 75 | { | 10 | { |
| 11 | provider: Schema.String, | ||
| 76 | operation: Schema.String, | 12 | operation: Schema.String, |
| 77 | issue: Schema.String, | 13 | issue: Schema.String, |
| 78 | cause: Schema.optional(Schema.Defect), | 14 | cause: Schema.optional(Schema.Defect), |
| 79 | }, | 15 | }, |
| 80 | ) { | 16 | ) { |
| 81 | override get message(): string { | 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>()( | 25 | export class ProviderAdapterSessionNotFoundError extends Schema.TaggedErrorClass<ProviderAdapterSessionNotFoundError>()( |
| 90 | "CodexSessionNotFoundError", | 26 | "ProviderAdapterSessionNotFoundError", |
| 91 | { | 27 | { |
| 28 | provider: Schema.String, | ||
| 92 | sessionId: Schema.String, | 29 | sessionId: Schema.String, |
| 93 | cause: Schema.optional(Schema.Defect), | 30 | cause: Schema.optional(Schema.Defect), |
| 94 | }, | 31 | }, |
| 95 | ) { | 32 | ) { |
| 96 | override get message(): string { | 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>()( | 41 | export class ProviderAdapterSessionClosedError extends Schema.TaggedErrorClass<ProviderAdapterSessionClosedError>()( |
| 105 | "CodexSessionClosedError", | 42 | "ProviderAdapterSessionClosedError", |
| 106 | { | 43 | { |
| 44 | provider: Schema.String, | ||
| 107 | sessionId: Schema.String, | 45 | sessionId: Schema.String, |
| 108 | cause: Schema.optional(Schema.Defect), | 46 | cause: Schema.optional(Schema.Defect), |
| 109 | }, | 47 | }, |
| 110 | ) { | 48 | ) { |
| 111 | override get message(): string { | 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>()( | 57 | export class ProviderAdapterProtocolError extends Schema.TaggedErrorClass<ProviderAdapterProtocolError>()( |
| 120 | "CodexProtocolError", | 58 | "ProviderAdapterProtocolError", |
| 121 | { | 59 | { |
| 60 | provider: Schema.String, | ||
| 122 | operation: Schema.String, | 61 | operation: Schema.String, |
| 123 | detail: Schema.String, | 62 | detail: Schema.String, |
| 124 | cause: Schema.optional(Schema.Defect), | 63 | cause: Schema.optional(Schema.Defect), |
| 125 | }, | 64 | }, |
| 126 | ) { | 65 | ) { |
| 127 | override get message(): string { | 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>()( | 74 | export class ProviderAdapterRequestError extends Schema.TaggedErrorClass<ProviderAdapterRequestError>()( |
| 136 | "CodexRequestError", | 75 | "ProviderAdapterRequestError", |
| 137 | { | 76 | { |
| 77 | provider: Schema.String, | ||
| 138 | method: Schema.String, | 78 | method: Schema.String, |
| 139 | detail: Schema.String, | 79 | detail: Schema.String, |
| 140 | cause: Schema.optional(Schema.Defect), | 80 | cause: Schema.optional(Schema.Defect), |
| 141 | }, | 81 | }, |
| 142 | ) { | 82 | ) { |
| 143 | override get message(): string { | 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>()( | 91 | export class ProviderAdapterProcessError extends Schema.TaggedErrorClass<ProviderAdapterProcessError>()( |
| 152 | "CodexProcessError", | 92 | "ProviderAdapterProcessError", |
| 153 | { | 93 | { |
| 94 | provider: Schema.String, | ||
| 154 | sessionId: Schema.String, | 95 | sessionId: Schema.String, |
| 155 | detail: Schema.String, | 96 | detail: Schema.String, |
| 156 | cause: Schema.optional(Schema.Defect), | 97 | cause: Schema.optional(Schema.Defect), |
| 157 | }, | 98 | }, |
| 158 | ) { | 99 | ) { |
| 159 | override get message(): string { | 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,19 +198,13 @@ export class ProviderFilesystemError extends Schema.TaggedErrorClass<ProviderFil |
| 257 | } | 198 | } |
| 258 | } | 199 | } |
| 259 | 200 | ||
| 260 | export type CheckpointStoreError = | 201 | export type ProviderAdapterError = |
| 261 | | CheckpointValidationError | 202 | | ProviderAdapterValidationError |
| 262 | | CheckpointGitCommandError | 203 | | ProviderAdapterSessionNotFoundError |
| 263 | | CheckpointUnavailableError | 204 | | ProviderAdapterSessionClosedError |
| 264 | | CheckpointRepositoryError; | 205 | | ProviderAdapterProtocolError |
| 265 | 206 | | ProviderAdapterRequestError | |
| 266 | export type CodexServiceError = | 207 | | ProviderAdapterProcessError; |
| 267 | | CodexValidationError | ||
| 268 | | CodexSessionNotFoundError | ||
| 269 | | CodexSessionClosedError | ||
| 270 | | CodexProtocolError | ||
| 271 | | CodexRequestError | ||
| 272 | | CodexProcessError; | ||
| 273 | 208 | ||
| 274 | export type ProviderServiceError = | 209 | export type ProviderServiceError = |
| 275 | | ProviderValidationError | 210 | | ProviderValidationError |
| ... | @@ -278,5 +213,5 @@ export type ProviderServiceError = | ... | @@ -278,5 +213,5 @@ export type ProviderServiceError = |
| 278 | | ProviderCheckpointUnavailableError | 213 | | ProviderCheckpointUnavailableError |
| 279 | | ProviderCheckpointRangeError | 214 | | ProviderCheckpointRangeError |
| 280 | | ProviderFilesystemError | 215 | | ProviderFilesystemError |
| 281 | | CodexServiceError | 216 | | ProviderAdapterError |
| 282 | | CheckpointStoreError; | 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,22 +1,31 @@ |
| 1 | import { describe, expect, it } from "vitest"; | 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 | import { | 5 | import { |
| 6 | CheckpointValidationError, | 6 | ProviderAdapterValidationError, |
| 7 | CodexValidationError, | ||
| 8 | ProviderFilesystemError, | 7 | ProviderFilesystemError, |
| 8 | ProviderSessionNotFoundError, | ||
| 9 | ProviderUnsupportedError, | 9 | ProviderUnsupportedError, |
| 10 | type CheckpointStoreError, | 10 | type ProviderAdapterError, |
| 11 | type CodexServiceError, | ||
| 12 | type ProviderServiceError, | 11 | type ProviderServiceError, |
| 13 | } from "./Errors.ts"; | 12 | } from "./Errors.ts"; |
| 14 | import { CheckpointStore } from "./Services/CheckpointStore.ts"; | 13 | import { |
| 15 | import { CodexService } from "./Services/Codex.ts"; | 14 | CheckpointValidationError, |
| 16 | import { ProviderService } from "./Services/Provider.ts"; | 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", () => { | 27 | describe("provider/checkpoint service contracts", () => { |
| 19 | it("resolves provider, codex, and checkpoint services from ServiceMap tags", async () => { | 28 | it("resolves all service tags from ServiceMap", async () => { |
| 20 | const providerLive: typeof ProviderService.Service = { | 29 | const providerLive: typeof ProviderService.Service = { |
| 21 | startSession: () => | 30 | startSession: () => |
| 22 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), | 31 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), |
| ... | @@ -35,34 +44,58 @@ describe("provider service contracts", () => { | ... | @@ -35,34 +44,58 @@ describe("provider service contracts", () => { |
| 35 | revertToCheckpoint: () => | 44 | revertToCheckpoint: () => |
| 36 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), | 45 | Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })), |
| 37 | stopAll: () => Effect.void, | 46 | stopAll: () => Effect.void, |
| 38 | dispose: () => Effect.void, | ||
| 39 | subscribeToEvents: () => Effect.succeed(() => undefined), | 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 | startSession: () => | 52 | startSession: () => |
| 44 | Effect.fail<CodexServiceError>( | 53 | Effect.fail<ProviderAdapterError>( |
| 45 | new CodexValidationError({ operation: "startSession", issue: "not implemented" }), | 54 | new ProviderAdapterValidationError({ |
| 55 | provider: "codex", | ||
| 56 | operation: "startSession", | ||
| 57 | issue: "not implemented", | ||
| 58 | }), | ||
| 46 | ), | 59 | ), |
| 47 | sendTurn: () => | 60 | sendTurn: () => |
| 48 | Effect.fail<CodexServiceError>( | 61 | Effect.fail<ProviderAdapterError>( |
| 49 | new CodexValidationError({ operation: "sendTurn", issue: "not implemented" }), | 62 | new ProviderAdapterValidationError({ |
| 63 | provider: "codex", | ||
| 64 | operation: "sendTurn", | ||
| 65 | issue: "not implemented", | ||
| 66 | }), | ||
| 50 | ), | 67 | ), |
| 51 | interruptTurn: () => | 68 | interruptTurn: () => |
| 52 | Effect.fail<CodexServiceError>( | 69 | Effect.fail<ProviderAdapterError>( |
| 53 | new CodexValidationError({ operation: "interruptTurn", issue: "not implemented" }), | 70 | new ProviderAdapterValidationError({ |
| 71 | provider: "codex", | ||
| 72 | operation: "interruptTurn", | ||
| 73 | issue: "not implemented", | ||
| 74 | }), | ||
| 54 | ), | 75 | ), |
| 55 | readThread: () => | 76 | readThread: () => |
| 56 | Effect.fail<CodexServiceError>( | 77 | Effect.fail<ProviderAdapterError>( |
| 57 | new CodexValidationError({ operation: "readThread", issue: "not implemented" }), | 78 | new ProviderAdapterValidationError({ |
| 79 | provider: "codex", | ||
| 80 | operation: "readThread", | ||
| 81 | issue: "not implemented", | ||
| 82 | }), | ||
| 58 | ), | 83 | ), |
| 59 | rollbackThread: () => | 84 | rollbackThread: () => |
| 60 | Effect.fail<CodexServiceError>( | 85 | Effect.fail<ProviderAdapterError>( |
| 61 | new CodexValidationError({ operation: "rollbackThread", issue: "not implemented" }), | 86 | new ProviderAdapterValidationError({ |
| 87 | provider: "codex", | ||
| 88 | operation: "rollbackThread", | ||
| 89 | issue: "not implemented", | ||
| 90 | }), | ||
| 62 | ), | 91 | ), |
| 63 | respondToRequest: () => | 92 | respondToRequest: () => |
| 64 | Effect.fail<CodexServiceError>( | 93 | Effect.fail<ProviderAdapterError>( |
| 65 | new CodexValidationError({ operation: "respondToRequest", issue: "not implemented" }), | 94 | new ProviderAdapterValidationError({ |
| 95 | provider: "codex", | ||
| 96 | operation: "respondToRequest", | ||
| 97 | issue: "not implemented", | ||
| 98 | }), | ||
| 66 | ), | 99 | ), |
| 67 | stopSession: () => Effect.void, | 100 | stopSession: () => Effect.void, |
| 68 | listSessions: () => Effect.succeed([]), | 101 | listSessions: () => Effect.succeed([]), |
| ... | @@ -71,7 +104,7 @@ describe("provider service contracts", () => { | ... | @@ -71,7 +104,7 @@ describe("provider service contracts", () => { |
| 71 | subscribeToEvents: () => Effect.succeed(() => undefined), | 104 | subscribeToEvents: () => Effect.succeed(() => undefined), |
| 72 | }; | 105 | }; |
| 73 | 106 | ||
| 74 | const checkpointLive: typeof CheckpointStore.Service = { | 107 | const checkpointStoreLive: typeof CheckpointStore.Service = { |
| 75 | isGitRepository: () => Effect.succeed(false), | 108 | isGitRepository: () => Effect.succeed(false), |
| 76 | captureCheckpoint: () => Effect.void, | 109 | captureCheckpoint: () => Effect.void, |
| 77 | hasCheckpoint: () => Effect.succeed(false), | 110 | hasCheckpoint: () => Effect.succeed(false), |
| ... | @@ -87,39 +120,117 @@ describe("provider service contracts", () => { | ... | @@ -87,39 +120,117 @@ describe("provider service contracts", () => { |
| 87 | pruneAfterTurn: () => Effect.void, | 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 | const runtime = ManagedRuntime.make( | 172 | const runtime = ManagedRuntime.make( |
| 91 | Layer.mergeAll( | 173 | Layer.mergeAll( |
| 92 | Layer.succeed(ProviderService, providerLive), | 174 | Layer.succeed(ProviderService, providerLive), |
| 93 | Layer.succeed(CodexService, codexLive), | 175 | Layer.succeed(CodexAdapter, codexLive), |
| 94 | Layer.succeed(CheckpointStore, checkpointLive), | 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 | runtime.runPromise(Effect.service(ProviderService)), | 193 | runtime.runPromise(Effect.service(ProviderService)), |
| 100 | runtime.runPromise(Effect.service(CodexService)), | 194 | runtime.runPromise(Effect.service(CodexAdapter)), |
| 101 | runtime.runPromise(Effect.service(CheckpointStore)), | 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 | expect(provider).toBe(providerLive); | 202 | expect(provider).toBe(providerLive); |
| 105 | expect(codex).toBe(codexLive); | 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 | await runtime.dispose(); | 210 | await runtime.dispose(); |
| 109 | }); | 211 | }); |
| 110 | }); | 212 | }); |
| 111 | 213 | ||
| 112 | describe("provider error taxonomy", () => { | 214 | describe("provider/checkpoint errors", () => { |
| 113 | it("keeps defect cause and message on tagged errors", () => { | 215 | it("preserves cause and message on tagged errors", () => { |
| 114 | const cause = new Error("filesystem failed"); | 216 | const fsCause = new Error("filesystem failed"); |
| 115 | const error = new ProviderFilesystemError({ | 217 | const fsError = new ProviderFilesystemError({ |
| 116 | sessionId: "sess-1", | 218 | sessionId: "sess-1", |
| 117 | detail: "capture failed", | 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"); | 234 | expect(error._tag).toBe("CheckpointValidationError"); |
| 122 | expect(error.message).toContain("sess-1"); | ||
| 123 | expect(error.cause).toBe(cause); | ||
| 124 | }); | 235 | }); |
| 125 | }); | 236 | }); |