| 1 | import { |
| 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"; |
| 22 | import * as Crypto from "effect/Crypto"; |
| 23 | import * as Schema from "effect/Schema"; |
| 24 | import { Tool, Toolkit } from "effect/ai"; |
| 25 | |
| 26 | import * as ThreadSearch from "../../../orchestration-v2/ThreadSearch.ts"; |
| 27 | import * as ScheduledTaskService from "../../../scheduledTasks/ScheduledTaskService.ts"; |
| 28 | import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts"; |
| 29 | import * as McpInvocationContext from "../../McpInvocationContext.ts"; |
| 30 | |
| 31 | const 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 | |
| 64 | const queueTarget = { threadId: Schema.optional(ThreadId), queuedRunId: RunId }; |
| 65 | const 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 | }; |
| 75 | const queueEntry = Schema.Struct({ |
| 76 | queuedRunId: RunId, |
| 77 | text: Schema.String, |
| 78 | truncated: Schema.Boolean, |
| 79 | }); |
| 80 | const 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); |
| 96 | const 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); |
| 104 | const 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); |
| 113 | const 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); |
| 118 | const 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); |
| 123 | const 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 | |
| 130 | const requestTarget = { threadId: Schema.optional(ThreadId), requestId: RuntimeRequestId }; |
| 131 | const 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 | }); |
| 147 | const pendingRequest = Schema.Struct({ |
| 148 | requestId: RuntimeRequestId, |
| 149 | questions: Schema.Array(question), |
| 150 | }); |
| 151 | const 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); |
| 160 | const 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); |
| 169 | const 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 | |
| 178 | const 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); |
| 192 | const 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 | |
| 202 | const transferResult = Schema.Struct({ sequence: NonNegativeInt, targetThreadId: ThreadId }); |
| 203 | const 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); |
| 214 | const 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); |
| 225 | const 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 | |
| 243 | const 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 | |
| 257 | const 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 | |
| 274 | export 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 | ); |