1import {
2 ProviderApprovalDecision,
3 ProviderSessionId,
4 ProviderUserInputAnswers,
5 RuntimeRequestId,
6 ThreadId,
7} from "@t3tools/contracts";
8import * as Context from "effect/Context";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as Option from "effect/Option";
12import * as Schema from "effect/Schema";
13
14import * as ProjectionStore from "./ProjectionStore.ts";
15import * as ProviderSessionManager from "./ProviderSessionManager.ts";
16
17export class RuntimeRequestResponseExecutionError extends Schema.TaggedError<RuntimeRequestResponseExecutionError>()(
18 "RuntimeRequestResponseExecutionError",
19 {
20 reason: Schema.Literals([
21 "request-missing",
22 "request-not-ready",
23 "request-not-resumable",
24 "provider-session-not-active",
25 "unexpected-failure",
26 ]),
27 threadId: ThreadId,
28 providerSessionId: ProviderSessionId,
29 requestId: RuntimeRequestId,
30 cause: Schema.optional(Schema.Defect()),
31 },
32) {
33 override get message(): string {
34 switch (this.reason) {
35 case "request-missing":
36 return `Runtime request ${this.requestId} no longer exists on thread ${this.threadId}.`;
37 case "request-not-ready":
38 return `Runtime request ${this.requestId} on thread ${this.threadId} is not ready for response execution.`;
39 case "request-not-resumable":
40 return `Runtime request ${this.requestId} on thread ${this.threadId} is not resumable on provider session ${this.providerSessionId}.`;
41 case "provider-session-not-active":
42 return `Provider session ${this.providerSessionId} is not active for runtime request ${this.requestId} on thread ${this.threadId}.`;
43 case "unexpected-failure":
44 return `Failed to respond to runtime request ${this.requestId} on thread ${this.threadId} via provider session ${this.providerSessionId}.`;
45 }
46 }
47}
48
49const isRuntimeRequestResponseExecutionError = Schema.is(RuntimeRequestResponseExecutionError);
50
51export interface RuntimeRequestServiceV2Shape {
52 readonly respond: (input: {
53 readonly threadId: ThreadId;
54 readonly providerSessionId: ProviderSessionId;
55 readonly requestId: RuntimeRequestId;
56 readonly decision?: ProviderApprovalDecision;
57 readonly answers?: ProviderUserInputAnswers;
58 }) => Effect.Effect<void, RuntimeRequestResponseExecutionError>;
59}
60
61export class RuntimeRequestServiceV2 extends Context.Service<
62 RuntimeRequestServiceV2,
63 RuntimeRequestServiceV2Shape
64>()("t3/orchestration-v2/RuntimeRequestService/RuntimeRequestServiceV2") {}
65
66export const layer: Layer.Layer<
67 RuntimeRequestServiceV2,
68 never,
69 ProjectionStore.ProjectionStoreV2 | ProviderSessionManager.ProviderSessionManagerV2
70> = Layer.effect(
71 RuntimeRequestServiceV2,
72 Effect.gen(function* () {
73 const projections = yield* ProjectionStore.ProjectionStoreV2;
74 const sessions = yield* ProviderSessionManager.ProviderSessionManagerV2;
75
76 return RuntimeRequestServiceV2.of({
77 respond: (input) =>
78 Effect.gen(function* () {
79 const request = yield* projections.getRuntimeRequest(input.threadId, input.requestId);
80 if (request === undefined) {
81 return yield* new RuntimeRequestResponseExecutionError({
82 reason: "request-missing",
83 threadId: input.threadId,
84 providerSessionId: input.providerSessionId,
85 requestId: input.requestId,
86 });
87 }
88 // Dispatch validates the request while it is pending, then persists the
89 // resolved projection before this effect is executed.
90 if (request.status !== "resolved") {
91 return yield* new RuntimeRequestResponseExecutionError({
92 reason: "request-not-ready",
93 threadId: input.threadId,
94 providerSessionId: input.providerSessionId,
95 requestId: input.requestId,
96 });
97 }
98 if (
99 request.responseCapability.type !== "live" ||
100 request.responseCapability.providerSessionId !== input.providerSessionId
101 ) {
102 return yield* new RuntimeRequestResponseExecutionError({
103 reason: "request-not-resumable",
104 threadId: input.threadId,
105 providerSessionId: input.providerSessionId,
106 requestId: input.requestId,
107 });
108 }
109 const session = yield* sessions.get(input.providerSessionId);
110 if (Option.isNone(session)) {
111 return yield* new RuntimeRequestResponseExecutionError({
112 reason: "provider-session-not-active",
113 threadId: input.threadId,
114 providerSessionId: input.providerSessionId,
115 requestId: input.requestId,
116 });
117 }
118 yield* session.value.respondToRuntimeRequest({
119 requestId: input.requestId,
120 ...(input.decision === undefined ? {} : { decision: input.decision }),
121 ...(input.answers === undefined ? {} : { answers: input.answers }),
122 });
123 }).pipe(
124 Effect.mapError((cause) =>
125 isRuntimeRequestResponseExecutionError(cause)
126 ? cause
127 : new RuntimeRequestResponseExecutionError({
128 reason: "unexpected-failure",
129 threadId: input.threadId,
130 providerSessionId: input.providerSessionId,
131 requestId: input.requestId,
132 cause,
133 }),
134 ),
135 ),
136 });
137 }),
138);