| 1 | import { |
| 2 | type OrchestrationV2Notification, |
| 3 | MessageId, |
| 4 | ProviderDriverKind, |
| 5 | ProviderThreadId, |
| 6 | RunId, |
| 7 | ThreadId, |
| 8 | } from "@t3tools/contracts"; |
| 9 | import * as Context from "effect/Context"; |
| 10 | import * as Effect from "effect/Effect"; |
| 11 | import * as Layer from "effect/Layer"; |
| 12 | import * as Option from "effect/Option"; |
| 13 | import * as Queue from "effect/Queue"; |
| 14 | |
| 15 | export interface ProviderContinuationRequest { |
| 16 | readonly threadId: ThreadId; |
| 17 | readonly providerThreadId: ProviderThreadId; |
| 18 | readonly driver: ProviderDriverKind; |
| 19 | readonly detail: string | null; |
| 20 | readonly notification?: OrchestrationV2Notification; |
| 21 | /** |
| 22 | * Durable ownership for an app-owned delegated-task completion delivery. |
| 23 | * The continuation worker re-reads the cohort before dispatching so a later |
| 24 | * task can join a queued wake and a stopped or acknowledged cohort is dropped. |
| 25 | */ |
| 26 | readonly delegatedCompletion?: { |
| 27 | readonly parentRunId: RunId; |
| 28 | readonly generation: number; |
| 29 | readonly messageId: MessageId; |
| 30 | }; |
| 31 | /** |
| 32 | * How the continuation turn gets its content. |
| 33 | * |
| 34 | * `adapter_buffered` (default) is the provider-native wake: the adapter has |
| 35 | * already buffered the CLI's wake output, and the dispatched message only |
| 36 | * triggers ingestion. `ClaudeAdapterV2` deliberately discards the message |
| 37 | * text on that path. |
| 38 | * |
| 39 | * `message_text` is for app-owned work with no buffered provider output, such |
| 40 | * as a delegated child finishing. The text is the entire wake, so it must |
| 41 | * reach the provider as a real prompt. |
| 42 | */ |
| 43 | readonly delivery?: "adapter_buffered" | "message_text"; |
| 44 | /** |
| 45 | * The message the continuation run starts from, when the adapter picks it: |
| 46 | * its turn input carries this id, so the adapter can tell which of its held |
| 47 | * wakes a run was dispatched for. Otherwise the service allocates one. |
| 48 | */ |
| 49 | readonly messageId?: MessageId; |
| 50 | readonly dispatchIfCurrent?: <A, E, R>( |
| 51 | effect: Effect.Effect<A, E, R>, |
| 52 | ) => Effect.Effect<Option.Option<A>, E, R>; |
| 53 | /** Clears a pending offer that the continuation worker intentionally drops. */ |
| 54 | readonly clearIfCurrent?: () => Effect.Effect<void>; |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Adapters offer a continuation request when provider-native work completes |
| 59 | * outside an active turn (for example a Claude background task wake turn) so |
| 60 | * the orchestrator can start a run that ingests it. The default reference |
| 61 | * drops requests, keeping adapter construction dependency-free in tests; the |
| 62 | * live layer must be shared with the ProviderContinuationService worker that |
| 63 | * drains it. |
| 64 | */ |
| 65 | export class ProviderContinuationRequests extends Context.Reference<{ |
| 66 | readonly offer: (request: ProviderContinuationRequest) => Effect.Effect<void>; |
| 67 | readonly take: Effect.Effect<ProviderContinuationRequest>; |
| 68 | }>("@t3tools/provider-core/server/ProviderContinuationRequests", { |
| 69 | defaultValue: () => ({ offer: () => Effect.void, take: Effect.never }), |
| 70 | }) {} |
| 71 | |
| 72 | export const layer = Layer.effect( |
| 73 | ProviderContinuationRequests, |
| 74 | Effect.gen(function* () { |
| 75 | const queue = yield* Queue.unbounded<ProviderContinuationRequest>(); |
| 76 | return { |
| 77 | offer: (request: ProviderContinuationRequest) => |
| 78 | Queue.offer(queue, request).pipe(Effect.asVoid), |
| 79 | take: Queue.take(queue), |
| 80 | }; |
| 81 | }), |
| 82 | ); |