1import {
2 isRuntimeModeAbove,
3 managedRuntimeModeCeilingMessage,
4 ModelSelection,
5 OrchestrationV2AppThread,
6 ProjectId,
7 ProviderInstanceId,
8 RuntimeMode,
9 ThreadId,
10} from "@t3tools/contracts";
11import * as Context from "effect/Context";
12import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";
14import * as Option from "effect/Option";
15import * as Schema from "effect/Schema";
16
17import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts";
18import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
19import * as ServerSettings from "../serverSettings.ts";
20import * as ProjectStore from "./ProjectStore.ts";
21
22export class RuntimePolicyResolveError extends Schema.TaggedError<RuntimePolicyResolveError>()(
23 "RuntimePolicyResolveError",
24 {
25 projectId: ProjectId,
26 providerInstanceId: ProviderInstanceId,
27 cause: Schema.optional(Schema.Defect()),
28 },
29) {
30 override get message(): string {
31 return `Failed to resolve runtime policy for provider instance ${this.providerInstanceId} in project ${this.projectId}.`;
32 }
33}
34
35/**
36 * A turn would start in a mode above the policy's ceiling. The orchestrator refuses those when
37 * they are dispatched; this catches the ones it never saw, such as a run resumed after a restart
38 * on a thread whose mode predates the policy.
39 */
40export class RuntimePolicyAboveCeilingError extends Schema.TaggedError<RuntimePolicyAboveCeilingError>()(
41 "RuntimePolicyAboveCeilingError",
42 {
43 threadId: ThreadId,
44 runtimeMode: RuntimeMode,
45 maxRuntimeMode: RuntimeMode,
46 organization: Schema.String,
47 },
48) {
49 override get message(): string {
50 return `${managedRuntimeModeCeilingMessage(this.organization, this.maxRuntimeMode)} Switch this thread's mode to continue.`;
51 }
52}
53
54export const isRuntimePolicyAboveCeilingError = Schema.is(RuntimePolicyAboveCeilingError);
55
56export const RuntimePolicyV2Error = Schema.Union([
57 RuntimePolicyResolveError,
58 RuntimePolicyAboveCeilingError,
59]);
60export type RuntimePolicyV2Error = typeof RuntimePolicyV2Error.Type;
61
62export const RuntimePolicyV2Override = Schema.Struct({
63 cwd: Schema.optional(Schema.String),
64 approvalPolicy: Schema.optional(Schema.Unknown),
65 sandboxPolicy: Schema.optional(Schema.Unknown),
66 reasoningEffort: Schema.optional(Schema.String),
67});
68export type RuntimePolicyV2Override = typeof RuntimePolicyV2Override.Type;
69
70export interface RuntimePolicyV2Shape {
71 readonly resolve: (input: {
72 readonly thread: OrchestrationV2AppThread;
73 readonly modelSelection: ModelSelection;
74 }) => Effect.Effect<ProviderAdapter.ProviderAdapterV2RuntimePolicy, RuntimePolicyV2Error>;
75}
76
77export class RuntimePolicyV2 extends Context.Service<RuntimePolicyV2, RuntimePolicyV2Shape>()(
78 "t3/orchestration-v2/RuntimePolicy/RuntimePolicyV2",
79) {}
80
81export const layer: Layer.Layer<RuntimePolicyV2> = Layer.succeed(RuntimePolicyV2, {
82 resolve: Effect.fn("RuntimePolicyV2.resolve")(function (input) {
83 return Effect.succeed({
84 runtimeMode: input.thread.runtimeMode,
85 interactionMode: input.thread.interactionMode,
86 cwd: input.thread.worktreePath,
87 });
88 }),
89});
90
91function providerRuntimeMode(
92 runtimeMode: RuntimeMode,
93 supportedRuntimeModes: ReadonlyArray<RuntimeMode> | undefined,
94): RuntimeMode {
95 return supportedRuntimeModes === undefined ||
96 supportedRuntimeModes.length === 0 ||
97 supportedRuntimeModes.includes(runtimeMode)
98 ? runtimeMode
99 : "approval-required";
100}
101
102const refuseAboveCeiling = (
103 managedPolicy: ServerSettings.ServerSettingsService["Service"]["managedPolicy"],
104 thread: OrchestrationV2AppThread,
105) => {
106 const maxRuntimeMode = managedPolicy?.limits?.maxRuntimeMode;
107 return managedPolicy !== undefined &&
108 maxRuntimeMode !== undefined &&
109 isRuntimeModeAbove(thread.runtimeMode, maxRuntimeMode)
110 ? Effect.fail(
111 new RuntimePolicyAboveCeilingError({
112 threadId: thread.id,
113 runtimeMode: thread.runtimeMode,
114 maxRuntimeMode,
115 organization: managedPolicy.organization,
116 }),
117 )
118 : Effect.void;
119};
120
121/** Lays the policy's ceiling over another runtime policy, as `layerFromProjectStore` does. */
122export const layerEnforcingCeiling: Layer.Layer<
123 RuntimePolicyV2,
124 never,
125 RuntimePolicyV2 | ServerSettings.ServerSettingsService
126> = Layer.effect(
127 RuntimePolicyV2,
128 Effect.gen(function* () {
129 const base = yield* RuntimePolicyV2;
130 const { managedPolicy } = yield* ServerSettings.ServerSettingsService;
131 return RuntimePolicyV2.of({
132 resolve: Effect.fn("RuntimePolicyV2.resolveWithinCeiling")(function* (input) {
133 yield* refuseAboveCeiling(managedPolicy, input.thread);
134 return yield* base.resolve(input);
135 }),
136 });
137 }),
138);
139
140export const layerFromProjectStore: Layer.Layer<
141 RuntimePolicyV2,
142 never,
143 | ProjectStore.ProjectStoreV2
144 | ProviderInstanceRegistry.ProviderInstanceRegistry
145 | ServerSettings.ServerSettingsService
146> = Layer.effect(
147 RuntimePolicyV2,
148 Effect.gen(function* () {
149 const projects = yield* ProjectStore.ProjectStoreV2;
150 const providerInstances = yield* ProviderInstanceRegistry.ProviderInstanceRegistry;
151 // The same ceiling the orchestrator enforces, fixed for the server's life.
152 const { managedPolicy } = yield* ServerSettings.ServerSettingsService;
153 const maxRuntimeMode = managedPolicy?.limits?.maxRuntimeMode;
154 return RuntimePolicyV2.of({
155 resolve: Effect.fn("RuntimePolicyV2.resolve")(function* (input) {
156 yield* refuseAboveCeiling(managedPolicy, input.thread);
157 const instance = yield* providerInstances.getInstance(input.modelSelection.instanceId);
158 const supportedRuntimeModes =
159 instance === undefined
160 ? undefined
161 : (yield* instance.snapshot.getSnapshot).supportedRuntimeModes;
162 const cwd =
163 input.thread.worktreePath ??
164 (yield* projects.get(input.thread.projectId).pipe(
165 Effect.mapError(
166 (cause) =>
167 new RuntimePolicyResolveError({
168 projectId: input.thread.projectId,
169 providerInstanceId: input.modelSelection.instanceId,
170 cause,
171 }),
172 ),
173 Effect.flatMap(
174 Option.match({
175 onNone: () =>
176 Effect.fail(
177 new RuntimePolicyResolveError({
178 projectId: input.thread.projectId,
179 providerInstanceId: input.modelSelection.instanceId,
180 cause: "Project not found.",
181 }),
182 ),
183 onSome: (project) => Effect.succeed(project.workspaceRoot),
184 }),
185 ),
186 ));
187 return ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
188 runtimeMode: providerRuntimeMode(input.thread.runtimeMode, supportedRuntimeModes),
189 interactionMode: input.thread.interactionMode,
190 cwd,
191 ...(maxRuntimeMode === undefined ? {} : { maxRuntimeMode }),
192 });
193 }),
194 });
195 }),
196);
197
198export function layerWithOverride(
199 override: RuntimePolicyV2Override,
200): Layer.Layer<RuntimePolicyV2, never, RuntimePolicyV2> {
201 return Layer.effect(
202 RuntimePolicyV2,
203 Effect.gen(function* () {
204 const base = yield* RuntimePolicyV2;
205 return {
206 resolve: (input) =>
207 base.resolve(input).pipe(
208 Effect.map((policy) =>
209 ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
210 ...policy,
211 ...(override.cwd === undefined ? {} : { cwd: override.cwd }),
212 ...(override.approvalPolicy === undefined
213 ? {}
214 : { approvalPolicy: override.approvalPolicy }),
215 ...(override.sandboxPolicy === undefined
216 ? {}
217 : { sandboxPolicy: override.sandboxPolicy }),
218 ...(override.reasoningEffort === undefined
219 ? {}
220 : { reasoningEffort: override.reasoningEffort }),
221 }),
222 ),
223 ),
224 } satisfies RuntimePolicyV2Shape;
225 }),
226 );
227}