import { OrchestratorMcpEnvironmentTarget, ScheduledTaskId, ScheduledTask, OrchestrationSearchThreadsInput, OrchestrationThreadSearchMatch, OrchestrationV2ThreadForkSourcePoint, OrchestrationV2ContextTransfer, TrimmedNonEmptyString, ModelSelection, RuntimeMode, ProviderInteractionMode, RuntimeRequestId, ProviderUserInputAnswers, IsoDateTime, OrchestratorMcpFailure, OrchestrationV2DispatchCommandResult, OwnedWorkClosureReason, OwnedWorkItem, OwnedWorkItemId, OrchestrationV2RuntimeRequest, WorkReviewDeliverable, ThreadId, RunId, NonNegativeInt, ProjectId, ProviderInstanceId, THREAD_SEARCH_QUERY_MAX_LENGTH, } from "@t3tools/contracts"; import * as SecureRandom from "effect/crypto/SecureRandom"; import * as Schema from "effect/Schema"; import { Tool, Toolkit } from "effect/ai"; import * as ThreadSearch from "../../../orchestration-v2/ThreadSearch.ts"; import * as ScheduledTaskService from "../../../scheduledTasks/ScheduledTaskService.ts"; import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts"; import * as McpInvocationContext from "../../McpInvocationContext.ts"; const ThreadOrganizeTool = Tool.make("t3_thread_organize", { description: "Pin, snooze, settle, archive, or mark a thread unread. Omit `threadId` for this thread. `snooze` needs `snoozedUntil`. Settling this thread applies at turn end. A failed turn or queued message leaves it active.", parameters: Schema.Struct({ environmentId: OrchestratorMcpEnvironmentTarget, threadId: Schema.optional(ThreadId), action: Schema.Literals([ "pin", "unpin", "snooze", "unsnooze", "settle", "unsettle", "archive", "unarchive", "mark_unread", ]), snoozedUntil: Schema.optional(IsoDateTime), }), success: Schema.Union([ OrchestrationV2DispatchCommandResult, Schema.Struct({ settlesWhenTurnEnds: Schema.Literal(true) }), ]), failure: OrchestratorMcpFailure, failureMode: "return" as const, dependencies: [ McpInvocationContext.McpInvocationContext, ThreadManagementService.ThreadManagementService, SecureRandom.SecureRandom, ], }) .annotate(Tool.Title, "Organize a thread") .annotate(Tool.Strict, true) .annotate(Tool.Destructive, true); const commandTool = { success: OrchestrationV2DispatchCommandResult, failure: OrchestratorMcpFailure, failureMode: "return" as const, dependencies: [ McpInvocationContext.McpInvocationContext, ThreadManagementService.ThreadManagementService, SecureRandom.SecureRandom, ], }; const queueEntry = Schema.Struct({ queuedRunId: RunId, text: Schema.String, truncated: Schema.Boolean, }); const QUEUE_READ_ACTIONS = ["list", "read"] as const; const QueueTool = Tool.make("t3_queue", { ...commandTool, description: "Manage a thread's queued messages. Omit `threadId` for this thread. list: send order, paged by `cursor` and `limit`. read: up to 16,000 chars of one. edit: replace its `text`; attachments stay. cancel. reorder: before `beforeRunId`, or to the end with null. promote_to_steer: send it now as steering to active `targetRunId`. Writes fail once the run is no longer queued.", parameters: Schema.Struct({ action: Schema.Literals([ ...QUEUE_READ_ACTIONS, "edit", "cancel", "reorder", "promote_to_steer", ]), environmentId: OrchestratorMcpEnvironmentTarget, threadId: Schema.optional(ThreadId), queuedRunId: Schema.optional(RunId), text: Schema.optional(Schema.String.check(Schema.isMaxLength(100000))), beforeRunId: Schema.optional(Schema.NullOr(RunId)), targetRunId: Schema.optional(RunId), cursor: Schema.optional(NonNegativeInt), limit: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))), }), success: Schema.Union([ Schema.Struct({ items: Schema.Array(queueEntry), nextCursor: Schema.NullOr(NonNegativeInt) }), queueEntry, OrchestrationV2DispatchCommandResult, ]), }).annotate(Tool.Destructive, true); const OWNED_WORK_READ_ACTIONS = ["list", "read"] as const; const OwnedWorkTool = Tool.make("t3_owned_work", { ...commandTool, description: "Owned work on a thread (Thread Details): assign → claim → progress → hold/attention → handoff → close. Omit `threadId` for this thread. list/read (history on each item). assign: `title`. claim/progress/handoff/hold/resume/attention/complete/cancel: `itemId` (+ `progress`, `toThreadId`, `reason`/`note`, or complete `closureReason`). complete needs `closureReason` (done|canceled|handed_off|denied|escalation|superseded|no_follow_on); handed_off/escalation need `closureTarget`. Closure is delivery, not acceptance. Hold/attention keep the thread open until resume.", parameters: Schema.Struct({ action: Schema.Literals([ ...OWNED_WORK_READ_ACTIONS, "assign", "claim", "progress", "handoff", "hold", "resume", "attention", "complete", "cancel", ]), environmentId: OrchestratorMcpEnvironmentTarget, threadId: Schema.optional(ThreadId), itemId: Schema.optional(OwnedWorkItemId), title: Schema.optional(TrimmedNonEmptyString), description: Schema.optional(Schema.String.check(Schema.isMaxLength(100000))), assigneeThreadId: Schema.optional(Schema.NullOr(ThreadId)), toThreadId: Schema.optional(ThreadId), progress: Schema.optional(Schema.String.check(Schema.isMaxLength(100000))), note: Schema.optional(Schema.String.check(Schema.isMaxLength(100000))), reason: Schema.optional(Schema.String.check(Schema.isMaxLength(100000))), closureReason: Schema.optional(OwnedWorkClosureReason), closureTarget: Schema.optional(TrimmedNonEmptyString), openOnly: Schema.optional(Schema.Boolean), }), success: Schema.Union([ Schema.Struct({ items: Schema.Array(OwnedWorkItem) }), OwnedWorkItem, OrchestrationV2DispatchCommandResult, ]), }).annotate(Tool.Destructive, true); const WORK_REVIEW_READ_ACTIONS = ["list", "read"] as const; const WorkReviewSummary = Schema.Struct({ requestId: RuntimeRequestId, status: OrchestrationV2RuntimeRequest.fields.status, criteria: Schema.optional(Schema.String), name: Schema.optional(Schema.String), note: Schema.optional(Schema.String), decision: Schema.optional(OrchestrationV2RuntimeRequest.fields.decision), deliverable: Schema.optional(WorkReviewDeliverable), createdAt: OrchestrationV2RuntimeRequest.fields.createdAt, }); const WorkReviewTool = Tool.make("t3_work_review", { ...commandTool, description: "Ask for human review of unfinished work as a pending approval card. Omit `threadId` for this thread. list/read pending or recent reviews. request: `criteria` (+ optional `name`, `note`, `deliverable`). accept/reject: `requestId` (+ optional `note` as feedback). Decline clears the ask so you can fix and request again; Accept records acceptance.", parameters: Schema.Struct({ action: Schema.Literals([...WORK_REVIEW_READ_ACTIONS, "request", "accept", "reject"]), environmentId: OrchestratorMcpEnvironmentTarget, threadId: Schema.optional(ThreadId), requestId: Schema.optional(RuntimeRequestId), name: Schema.optional(TrimmedNonEmptyString), criteria: Schema.optional(Schema.String.check(Schema.isMaxLength(100000))), note: Schema.optional(Schema.String.check(Schema.isMaxLength(100000))), deliverable: Schema.optional(WorkReviewDeliverable), openOnly: Schema.optional(Schema.Boolean), }), success: Schema.Union([ Schema.Struct({ reviews: Schema.Array(WorkReviewSummary) }), WorkReviewSummary, OrchestrationV2DispatchCommandResult, ]), }).annotate(Tool.Destructive, true); const question = Schema.Struct({ id: Schema.String, header: Schema.String, question: Schema.String, options: Schema.Array( Schema.Struct({ label: Schema.String, description: Schema.String, value: Schema.optional(Schema.String), }), ), multiSelect: Schema.optional(Schema.Boolean), allowCustomAnswer: Schema.optional(Schema.Boolean), initialAnswer: Schema.optional(Schema.String), required: Schema.optional(Schema.Boolean), }); const PendingRequestTool = Tool.make("t3_pending_request", { ...commandTool, description: "Pending user questions in a thread. Omit `threadId` for this thread. list: their ids; approvals are not listed. read: one by `requestId`. respond: answer one with `answers`; this cannot approve a permission request.", parameters: Schema.Struct({ action: Schema.Literals(["list", "read", "respond"]), environmentId: OrchestratorMcpEnvironmentTarget, threadId: Schema.optional(ThreadId), requestId: Schema.optional(RuntimeRequestId), answers: Schema.optional(ProviderUserInputAnswers), }), success: Schema.Union([ Schema.Struct({ requestIds: Schema.Array(RuntimeRequestId) }), Schema.Struct({ requestId: RuntimeRequestId, questions: Schema.Array(question) }), OrchestrationV2DispatchCommandResult, ]), }) .annotate(Tool.Destructive, true) .annotate(Tool.OpenWorld, true); const threadConfiguration = Schema.Struct({ threadId: ThreadId, modelSelection: ModelSelection, runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode, }); const ThreadConfigureTool = Tool.make("t3_thread_configure", { ...commandTool, description: "Read a thread's provider, model, and modes, or set its provider, model, and options with `modelSelection`. Omit `threadId` for this thread. Permission modes stay. Values from `orchestrator_capabilities`.", parameters: Schema.Struct({ environmentId: OrchestratorMcpEnvironmentTarget, threadId: Schema.optional(ThreadId), modelSelection: Schema.optional(ModelSelection), }), success: Schema.Union([threadConfiguration, OrchestrationV2DispatchCommandResult]), }).annotate(Tool.Destructive, true); const ThreadTransferTool = Tool.make("t3_thread_transfer", { ...commandTool, description: "Move context between threads. Omit `threadId` for this thread. fork: copy it from `sourcePoint` (default latest stable run) into a new thread with the same setup; no turn runs yet. merge_back: merge it from `sourcePoint` into a related `targetThreadId` in the same project. status: its context transfers.", parameters: Schema.Struct({ action: Schema.Literals(["fork", "merge_back", "status"]), environmentId: OrchestratorMcpEnvironmentTarget, threadId: Schema.optional(ThreadId), sourcePoint: Schema.optional(OrchestrationV2ThreadForkSourcePoint), targetThreadId: Schema.optional(ThreadId), title: Schema.optional(TrimmedNonEmptyString), }), success: Schema.Union([ Schema.Struct({ sequence: NonNegativeInt, targetThreadId: ThreadId }), Schema.Struct({ transfers: Schema.Array( Schema.Struct({ id: OrchestrationV2ContextTransfer.fields.id, sourceThreadId: OrchestrationV2ContextTransfer.fields.sourceThreadId, targetThreadId: OrchestrationV2ContextTransfer.fields.targetThreadId, status: OrchestrationV2ContextTransfer.fields.status, }), ), }), ]), }).annotate(Tool.Destructive, true); const ThreadSearchTool = Tool.make("t3_thread_search", { ...commandTool, description: "Search active threads by title, branch, PR, project, then message text. Skips subagent threads. `projectId`: one project, else yours, else all outside a thread. Not exhaustive.", parameters: Schema.Struct({ environmentId: OrchestratorMcpEnvironmentTarget, ...OrchestrationSearchThreadsInput.fields, projectId: Schema.optional(ProjectId), }), success: Schema.Struct({ matches: Schema.Array( Schema.Struct({ threadId: OrchestrationThreadSearchMatch.fields.threadId, projectId: OrchestrationThreadSearchMatch.fields.projectId, title: Schema.String, link: Schema.optional(Schema.String), source: Schema.Literals(["title", "user", "assistant"]), snippet: Schema.optional(OrchestrationThreadSearchMatch.fields.snippet), messageCreatedAt: Schema.optional(OrchestrationThreadSearchMatch.fields.messageCreatedAt), }), ), }), dependencies: [...commandTool.dependencies, ThreadSearch.ThreadSearch], }) .annotate(Tool.Readonly, true) .annotate(Tool.Destructive, false); const RosterFindTool = Tool.make("t3_roster_find", { ...commandTool, description: "Who already knows this topic? Ranked top-level threads by title, branch, PR, project, or messages. `relation`: owns (title/branch/PR) or discussed (message mention); prefer owns for a specialist. Skips subagents. Includes branch, provider, model, tags, hit strength. `projectId`: one project, else yours, else all. Not exhaustive.", parameters: Schema.Struct({ environmentId: OrchestratorMcpEnvironmentTarget, topic: TrimmedNonEmptyString.check( Schema.isMinLength(2), Schema.isMaxLength(THREAD_SEARCH_QUERY_MAX_LENGTH), ), projectId: Schema.optional(ProjectId), limit: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 20 }))), }), success: Schema.Struct({ matches: Schema.Array( Schema.Struct({ threadId: ThreadId, projectId: ProjectId, title: Schema.String, link: Schema.optional(Schema.String), why: Schema.Literals(["title", "user", "assistant"]), relation: Schema.Literals(["owns", "discussed"]), snippet: Schema.optional(OrchestrationThreadSearchMatch.fields.snippet), messageCreatedAt: Schema.optional(OrchestrationThreadSearchMatch.fields.messageCreatedAt), branch: Schema.NullOr(Schema.String), providerInstanceId: ProviderInstanceId, model: Schema.String, tags: Schema.Array(Schema.String), hitStrength: Schema.optional(Schema.Finite), messageHits: Schema.optional(Schema.Int), latestActivityAt: Schema.NullOr(IsoDateTime), }), ), }), dependencies: [...commandTool.dependencies, ThreadSearch.ThreadSearch], }) .annotate(Tool.Readonly, true) .annotate(Tool.Destructive, false); const ScheduledTaskRunTool = Tool.make("run_scheduled_task_now", { ...commandTool, description: "Run one scheduled task now. Needs a full-access or default caller. Returns its thread, or null if not started. Success means started, not finished.", parameters: Schema.Struct({ environmentId: OrchestratorMcpEnvironmentTarget, taskId: ScheduledTaskId, }), success: Schema.Struct({ taskId: ScheduledTaskId, threadId: ScheduledTask.fields.threadId, link: Schema.optional(Schema.String), lastRunStatus: ScheduledTask.fields.lastRunStatus, runCount: NonNegativeInt, nextRunAt: ScheduledTask.fields.nextRunAt, }), dependencies: [...commandTool.dependencies, ScheduledTaskService.ScheduledTaskService], }) .annotate(Tool.Destructive, true) .annotate(Tool.OpenWorld, true); export const ThreadToolkit = Toolkit.make( ScheduledTaskRunTool, ThreadSearchTool, RosterFindTool, ThreadConfigureTool, ThreadTransferTool, ThreadOrganizeTool, PendingRequestTool, QueueTool, OwnedWorkTool, WorkReviewTool, );