1import {
2 CommandId,
3 type ModelSelection,
4 type OrchestrationV2ThreadProjection,
5 type ProviderInteractionMode,
6 type RuntimeMode,
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 Schema from "effect/Schema";
13
14import * as ThreadManagement from "./ThreadManagementService.ts";
15
16export class ThreadLifecycleError extends Schema.TaggedError<ThreadLifecycleError>()(
17 "ThreadLifecycleError",
18 {
19 operation: Schema.Literals([
20 "archive",
21 "unarchive",
22 "delete",
23 "update-metadata",
24 "set-runtime-mode",
25 "set-interaction-mode",
26 "set-model-selection",
27 ]),
28 threadId: ThreadId,
29 cause: Schema.Defect(),
30 },
31) {
32 override get message(): string {
33 return `Thread lifecycle operation '${this.operation}' failed for ${this.threadId}.`;
34 }
35}
36
37export class ThreadLifecycleService extends Context.Service<
38 ThreadLifecycleService,
39 {
40 readonly archive: (input: {
41 readonly commandId: CommandId;
42 readonly threadId: ThreadId;
43 }) => Effect.Effect<Pick<OrchestrationV2ThreadProjection, "thread">, ThreadLifecycleError>;
44 readonly unarchive: (input: {
45 readonly commandId: CommandId;
46 readonly threadId: ThreadId;
47 }) => Effect.Effect<Pick<OrchestrationV2ThreadProjection, "thread">, ThreadLifecycleError>;
48 readonly delete: (input: {
49 readonly commandId: CommandId;
50 readonly threadId: ThreadId;
51 }) => Effect.Effect<Pick<OrchestrationV2ThreadProjection, "thread">, ThreadLifecycleError>;
52 readonly updateMetadata: (input: {
53 readonly commandId: CommandId;
54 readonly threadId: ThreadId;
55 readonly title?: string;
56 readonly branch?: string | null;
57 readonly worktreePath?: string | null;
58 }) => Effect.Effect<Pick<OrchestrationV2ThreadProjection, "thread">, ThreadLifecycleError>;
59 readonly setRuntimeMode: (input: {
60 readonly commandId: CommandId;
61 readonly threadId: ThreadId;
62 readonly runtimeMode: RuntimeMode;
63 }) => Effect.Effect<Pick<OrchestrationV2ThreadProjection, "thread">, ThreadLifecycleError>;
64 readonly setInteractionMode: (input: {
65 readonly commandId: CommandId;
66 readonly threadId: ThreadId;
67 readonly interactionMode: ProviderInteractionMode;
68 }) => Effect.Effect<Pick<OrchestrationV2ThreadProjection, "thread">, ThreadLifecycleError>;
69 readonly setModelSelection: (input: {
70 readonly commandId: CommandId;
71 readonly threadId: ThreadId;
72 readonly modelSelection: ModelSelection;
73 }) => Effect.Effect<Pick<OrchestrationV2ThreadProjection, "thread">, ThreadLifecycleError>;
74 }
75>()("t3/orchestration-v2/ThreadLifecycleService") {}
76
77const make = Effect.gen(function* () {
78 const threads = yield* ThreadManagement.ThreadManagementService;
79
80 const dispatch = <Operation extends ThreadLifecycleError["operation"]>(
81 operation: Operation,
82 threadId: ThreadId,
83 command: Parameters<ThreadManagement.ThreadManagementService["Service"]["dispatch"]>[0],
84 ) =>
85 threads.dispatch(command).pipe(
86 Effect.andThen(threads.getThreadRecords(threadId, [])),
87 Effect.mapError((cause) => new ThreadLifecycleError({ operation, threadId, cause })),
88 );
89
90 return ThreadLifecycleService.of({
91 archive: (input) =>
92 dispatch("archive", input.threadId, {
93 type: "thread.archive",
94 commandId: input.commandId,
95 threadId: input.threadId,
96 }),
97 unarchive: (input) =>
98 dispatch("unarchive", input.threadId, {
99 type: "thread.unarchive",
100 commandId: input.commandId,
101 threadId: input.threadId,
102 }),
103 delete: (input) =>
104 dispatch("delete", input.threadId, {
105 type: "thread.delete",
106 commandId: input.commandId,
107 threadId: input.threadId,
108 }),
109 updateMetadata: (input) =>
110 dispatch("update-metadata", input.threadId, {
111 type: "thread.metadata.update",
112 commandId: input.commandId,
113 threadId: input.threadId,
114 ...(input.title === undefined ? {} : { title: input.title }),
115 ...(input.branch === undefined ? {} : { branch: input.branch }),
116 ...(input.worktreePath === undefined ? {} : { worktreePath: input.worktreePath }),
117 }),
118 setRuntimeMode: (input) =>
119 dispatch("set-runtime-mode", input.threadId, {
120 type: "thread.runtime-mode.set",
121 commandId: input.commandId,
122 threadId: input.threadId,
123 runtimeMode: input.runtimeMode,
124 }),
125 setInteractionMode: (input) =>
126 dispatch("set-interaction-mode", input.threadId, {
127 type: "thread.interaction-mode.set",
128 commandId: input.commandId,
129 threadId: input.threadId,
130 interactionMode: input.interactionMode,
131 }),
132 setModelSelection: (input) =>
133 dispatch("set-model-selection", input.threadId, {
134 type: "thread.model-selection.set",
135 commandId: input.commandId,
136 threadId: input.threadId,
137 modelSelection: input.modelSelection,
138 }),
139 });
140});
141
142export const layer = Layer.effect(ThreadLifecycleService, make);