1import {
2 type OrchestrationV2Notification,
3 MessageId,
4 ProviderDriverKind,
5 ProviderThreadId,
6 RunId,
7 ThreadId,
8} from "@t3tools/contracts";
9import * as Context from "effect/Context";
10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";
12import * as Option from "effect/Option";
13import * as Queue from "effect/Queue";
14
15export 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 */
65export 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
72export 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);