1import {
2 ScheduledTaskId,
3 ScheduledTask,
4 OrchestrationSearchThreadsInput,
5 OrchestrationSearchThreadsResult,
6 OrchestrationV2ThreadForkSourcePoint,
7 OrchestrationV2ContextTransfer,
8 TrimmedNonEmptyString,
9 ModelSelection,
10 RuntimeMode,
11 ProviderInteractionMode,
12 RuntimeRequestId,
13 ProviderUserInputAnswers,
14 IsoDateTime,
15 OrchestratorMcpFailure,
16 OrchestrationV2DispatchCommandResult,
17 ThreadId,
18 RunId,
19 NonNegativeInt,
20 ProjectId,
21} from "@t3tools/contracts";
22import * as Crypto from "effect/Crypto";
23import * as Schema from "effect/Schema";
24import { Tool, Toolkit } from "effect/ai";
25
26import * as ThreadSearch from "../../../orchestration-v2/ThreadSearch.ts";
27import * as ScheduledTaskService from "../../../scheduledTasks/ScheduledTaskService.ts";
28import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts";
29import * as McpInvocationContext from "../../McpInvocationContext.ts";
30
31const ThreadOrganizeTool = Tool.make("t3_thread_organize", {
32 description:
33 "Pin, snooze, settle, archive, or mark a thread unread. Omit threadId for this thread. snooze requires snoozedUntil. Settling closes this chat's terminals and browser tabs, stops their running commands and media, and preserves conversation history. Unsettling cannot restore running processes or page state; tell the user before settling work with open resources. Settling this thread takes effect when your turn completes, returning settlesWhenTurnEnds=true; a turn that fails or is interrupted, or a queued message, leaves it active.",
34 parameters: Schema.Struct({
35 threadId: Schema.optional(ThreadId),
36 action: Schema.Literals([
37 "pin",
38 "unpin",
39 "snooze",
40 "unsnooze",
41 "settle",
42 "unsettle",
43 "archive",
44 "unarchive",
45 "mark_unread",
46 ]),
47 snoozedUntil: Schema.optional(IsoDateTime),
48 }),
49 success: Schema.Union([
50 OrchestrationV2DispatchCommandResult,
51 Schema.Struct({ settlesWhenTurnEnds: Schema.Literal(true) }),
52 ]),
53 failure: OrchestratorMcpFailure,
54 failureMode: "return" as const,
55 dependencies: [
56 McpInvocationContext.McpInvocationContext,
57 ThreadManagementService.ThreadManagementService,
58 Crypto.Crypto,
59 ],
60})
61 .annotate(Tool.Title, "Organize a thread")
62 .annotate(Tool.Destructive, true);
63
64const queueTarget = { threadId: Schema.optional(ThreadId), queuedRunId: RunId };
65const commandTool = {
66 success: OrchestrationV2DispatchCommandResult,
67 failure: OrchestratorMcpFailure,
68 failureMode: "return" as const,
69 dependencies: [
70 McpInvocationContext.McpInvocationContext,
71 ThreadManagementService.ThreadManagementService,
72 Crypto.Crypto,
73 ],
74};
75const queueEntry = Schema.Struct({
76 queuedRunId: RunId,
77 text: Schema.String,
78 truncated: Schema.Boolean,
79});
80const QueueListTool = Tool.make("t3_queue_list", {
81 ...commandTool,
82 description:
83 "List queued messages in delivery order. Results are a live offset page; use t3_thread_read for full thread history.",
84 parameters: Schema.Struct({
85 threadId: Schema.optional(ThreadId),
86 cursor: Schema.optional(NonNegativeInt),
87 limit: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))),
88 }),
89 success: Schema.Struct({
90 items: Schema.Array(queueEntry),
91 nextCursor: Schema.NullOr(NonNegativeInt),
92 }),
93})
94 .annotate(Tool.Readonly, true)
95 .annotate(Tool.Destructive, false);
96const QueueReadTool = Tool.make("t3_queue_read", {
97 ...commandTool,
98 description: "Read up to 16,000 characters of a queued message. Omit threadId for this thread.",
99 parameters: Schema.Struct(queueTarget),
100 success: queueEntry,
101})
102 .annotate(Tool.Readonly, true)
103 .annotate(Tool.Destructive, false);
104const QueueEditTool = Tool.make("t3_queue_edit", {
105 ...commandTool,
106 description:
107 "Replace a queued message's text, preserving its attachments. The service rejects runs that are no longer queued.",
108 parameters: Schema.Struct({
109 ...queueTarget,
110 text: Schema.String.check(Schema.isMaxLength(100000)),
111 }),
112}).annotate(Tool.Destructive, true);
113const QueueCancelTool = Tool.make("t3_queue_cancel", {
114 ...commandTool,
115 description: "Cancel a queued run using the existing queue command.",
116 parameters: Schema.Struct(queueTarget),
117}).annotate(Tool.Destructive, true);
118const QueueReorderTool = Tool.make("t3_queue_reorder", {
119 ...commandTool,
120 description: "Move a queued run before another queued run, or to the end with beforeRunId=null.",
121 parameters: Schema.Struct({ ...queueTarget, beforeRunId: Schema.NullOr(RunId) }),
122}).annotate(Tool.Destructive, true);
123const QueuePromoteTool = Tool.make("t3_queue_promote_to_steer", {
124 ...commandTool,
125 description:
126 "Deliver a queued message as steering to the specified active run. Existing provider and run-state rules apply.",
127 parameters: Schema.Struct({ ...queueTarget, targetRunId: RunId }),
128}).annotate(Tool.Destructive, true);
129
130const requestTarget = { threadId: Schema.optional(ThreadId), requestId: RuntimeRequestId };
131const question = Schema.Struct({
132 id: Schema.String,
133 header: Schema.String,
134 question: Schema.String,
135 options: Schema.Array(
136 Schema.Struct({
137 label: Schema.String,
138 description: Schema.String,
139 value: Schema.optional(Schema.String),
140 }),
141 ),
142 multiSelect: Schema.optional(Schema.Boolean),
143 allowCustomAnswer: Schema.optional(Schema.Boolean),
144 initialAnswer: Schema.optional(Schema.String),
145 required: Schema.optional(Schema.Boolean),
146});
147const pendingRequest = Schema.Struct({
148 requestId: RuntimeRequestId,
149 questions: Schema.Array(question),
150});
151const PendingRequestListTool = Tool.make("t3_pending_request_list", {
152 ...commandTool,
153 description:
154 "List pending user questions in a thread. Omit threadId for this thread. Approval requests are not included.",
155 parameters: Schema.Struct({ threadId: Schema.optional(ThreadId) }),
156 success: Schema.Struct({ requestIds: Schema.Array(RuntimeRequestId) }),
157})
158 .annotate(Tool.Readonly, true)
159 .annotate(Tool.Destructive, false);
160const PendingRequestReadTool = Tool.make("t3_pending_request_read", {
161 ...commandTool,
162 description:
163 "Read a pending user question. Answer with t3_pending_request_respond; existing live or message response handling is used.",
164 parameters: Schema.Struct(requestTarget),
165 success: pendingRequest,
166})
167 .annotate(Tool.Readonly, true)
168 .annotate(Tool.Destructive, false);
169const PendingRequestRespondTool = Tool.make("t3_pending_request_respond", {
170 ...commandTool,
171 description:
172 "Answer a pending user-input request using the existing runtime response command. This cannot approve a permission request.",
173 parameters: Schema.Struct({ ...requestTarget, answers: ProviderUserInputAnswers }),
174})
175 .annotate(Tool.Destructive, true)
176 .annotate(Tool.OpenWorld, true);
177
178const ThreadConfigurationTool = Tool.make("t3_thread_configuration", {
179 ...commandTool,
180 description:
181 "Read a thread's provider/model selection and modes. Omit threadId for this thread. orchestrator_capabilities lists available providers and models.",
182 parameters: Schema.Struct({ threadId: Schema.optional(ThreadId) }),
183 success: Schema.Struct({
184 threadId: ThreadId,
185 modelSelection: ModelSelection,
186 runtimeMode: RuntimeMode,
187 interactionMode: ProviderInteractionMode,
188 }),
189})
190 .annotate(Tool.Readonly, true)
191 .annotate(Tool.Destructive, false);
192const ThreadConfigureTool = Tool.make("t3_thread_configure", {
193 ...commandTool,
194 description:
195 "Set a thread's provider, model and options with the existing selection command. Omit threadId for this thread. This does not change permission modes. Use orchestrator_capabilities to choose a selection.",
196 parameters: Schema.Struct({
197 threadId: Schema.optional(ThreadId),
198 modelSelection: ModelSelection,
199 }),
200}).annotate(Tool.Destructive, true);
201
202const transferResult = Schema.Struct({ sequence: NonNegativeInt, targetThreadId: ThreadId });
203const ThreadForkTool = Tool.make("t3_thread_fork", {
204 ...commandTool,
205 description:
206 "Fork a thread from a stable run or checkpoint using the existing fork command. Omit threadId to fork this thread. The fork inherits the source configuration. Acceptance does not mean a provider turn has completed.",
207 parameters: Schema.Struct({
208 threadId: Schema.optional(ThreadId),
209 sourcePoint: OrchestrationV2ThreadForkSourcePoint,
210 title: Schema.optional(TrimmedNonEmptyString),
211 }),
212 success: transferResult,
213}).annotate(Tool.Destructive, true);
214const ThreadMergeBackTool = Tool.make("t3_thread_merge_back", {
215 ...commandTool,
216 description:
217 "Merge context from a thread back to a related thread in the same project. Omit sourceThreadId to merge from this thread. Existing lineage and transfer rules apply.",
218 parameters: Schema.Struct({
219 sourceThreadId: Schema.optional(ThreadId),
220 targetThreadId: ThreadId,
221 sourcePoint: OrchestrationV2ThreadForkSourcePoint,
222 }),
223 success: transferResult,
224}).annotate(Tool.Destructive, true);
225const ThreadTransfersTool = Tool.make("t3_thread_transfers", {
226 ...commandTool,
227 description: "Read context transfer status for a thread. Omit threadId for this thread.",
228 parameters: Schema.Struct({ threadId: Schema.optional(ThreadId) }),
229 success: Schema.Struct({
230 transfers: Schema.Array(
231 Schema.Struct({
232 id: OrchestrationV2ContextTransfer.fields.id,
233 sourceThreadId: OrchestrationV2ContextTransfer.fields.sourceThreadId,
234 targetThreadId: OrchestrationV2ContextTransfer.fields.targetThreadId,
235 status: OrchestrationV2ContextTransfer.fields.status,
236 }),
237 ),
238 }),
239})
240 .annotate(Tool.Readonly, true)
241 .annotate(Tool.Destructive, false);
242
243const ThreadSearchTool = Tool.make("t3_thread_search", {
244 ...commandTool,
245 description:
246 "Search active thread titles and content with the app's existing bounded search. Matches are limited to one project (projectId, else the calling thread's project) out of the global top matches, so this may return fewer than limit. A caller outside a T3 thread that omits projectId searches every project. No pagination or exhaustive-result guarantee.",
247 parameters: Schema.Struct({
248 ...OrchestrationSearchThreadsInput.fields,
249 projectId: Schema.optional(ProjectId),
250 }),
251 success: OrchestrationSearchThreadsResult,
252 dependencies: [...commandTool.dependencies, ThreadSearch.ThreadSearch],
253})
254 .annotate(Tool.Readonly, true)
255 .annotate(Tool.Destructive, false);
256
257const ScheduledTaskRunTool = Tool.make("run_scheduled_task_now", {
258 ...commandTool,
259 description:
260 "Run a scheduled task now through the existing scheduler. Requires a full-access/default caller. Each call is a new manual run; completion means dispatch/bookkeeping completed, not that the provider turn finished.",
261 parameters: Schema.Struct({ taskId: ScheduledTaskId }),
262 success: Schema.Struct({
263 taskId: ScheduledTaskId,
264 threadId: ScheduledTask.fields.threadId,
265 lastRunStatus: ScheduledTask.fields.lastRunStatus,
266 runCount: NonNegativeInt,
267 nextRunAt: ScheduledTask.fields.nextRunAt,
268 }),
269 dependencies: [...commandTool.dependencies, ScheduledTaskService.ScheduledTaskService],
270})
271 .annotate(Tool.Destructive, true)
272 .annotate(Tool.OpenWorld, true);
273
274export const ThreadToolkit = Toolkit.make(
275 ScheduledTaskRunTool,
276 ThreadSearchTool,
277 ThreadForkTool,
278 ThreadMergeBackTool,
279 ThreadTransfersTool,
280 ThreadConfigurationTool,
281 ThreadConfigureTool,
282 PendingRequestListTool,
283 PendingRequestReadTool,
284 PendingRequestRespondTool,
285 ThreadOrganizeTool,
286 QueueListTool,
287 QueueReadTool,
288 QueueEditTool,
289 QueueCancelTool,
290 QueueReorderTool,
291 QueuePromoteTool,
292);