| 1 | import { |
| 2 | RuntimeTaskId, |
| 3 | type ProviderRuntimeTaskStartedEvent, |
| 4 | type ProviderRuntimeTaskProgressEvent, |
| 5 | type ProviderRuntimeTaskCompletedEvent, |
| 6 | type TurnId, |
| 7 | } from "@t3tools/contracts"; |
| 8 | |
| 9 | type TaskEvent = |
| 10 | | Pick<ProviderRuntimeTaskStartedEvent, "type" | "payload" | "turnId"> |
| 11 | | Pick<ProviderRuntimeTaskProgressEvent, "type" | "payload" | "turnId"> |
| 12 | | Pick<ProviderRuntimeTaskCompletedEvent, "type" | "payload" | "turnId">; |
| 13 | |
| 14 | export interface GrokBackgroundTaskRecord { |
| 15 | readonly payload: { |
| 16 | readonly taskId: RuntimeTaskId; |
| 17 | readonly taskType: "monitor" | "shell"; |
| 18 | readonly description: string; |
| 19 | readonly title: string; |
| 20 | readonly toolUseId?: string; |
| 21 | }; |
| 22 | readonly turnId: TurnId | undefined; |
| 23 | } |
| 24 | |
| 25 | function record(value: unknown): Record<string, unknown> { |
| 26 | return typeof value === "object" && value !== null && !Array.isArray(value) |
| 27 | ? (value as Record<string, unknown>) |
| 28 | : {}; |
| 29 | } |
| 30 | |
| 31 | function text(value: unknown): string | undefined { |
| 32 | return typeof value === "string" ? value.trim() || undefined : undefined; |
| 33 | } |
| 34 | |
| 35 | function lifecycle(status: unknown, exitCode: unknown) { |
| 36 | switch (text(status)?.toLowerCase()) { |
| 37 | case "pending": |
| 38 | case "running": |
| 39 | return "running"; |
| 40 | case "completed": |
| 41 | case "success": |
| 42 | case "succeeded": |
| 43 | return "completed"; |
| 44 | case "failed": |
| 45 | case "error": |
| 46 | return "failed"; |
| 47 | case "stopped": |
| 48 | case "killed": |
| 49 | case "cancelled": |
| 50 | return "stopped"; |
| 51 | default: |
| 52 | return typeof exitCode === "number" && Number.isFinite(exitCode) |
| 53 | ? exitCode === 0 |
| 54 | ? "completed" |
| 55 | : "failed" |
| 56 | : undefined; |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | /** Map Grok's discriminated tool results, including notifications after the turn ends. */ |
| 61 | export function buildGrokBackgroundTaskEvents(input: { |
| 62 | readonly tasks: Map<string, GrokBackgroundTaskRecord>; |
| 63 | readonly toolCallId: string; |
| 64 | readonly rawInput: unknown; |
| 65 | readonly rawOutput: unknown; |
| 66 | readonly toolCallStatus: string | undefined; |
| 67 | readonly turnId?: TurnId | undefined; |
| 68 | }): TaskEvent[] { |
| 69 | const { tasks, toolCallId, toolCallStatus, turnId } = input; |
| 70 | const output = record(input.rawOutput); |
| 71 | const events: TaskEvent[] = []; |
| 72 | if ( |
| 73 | toolCallStatus !== "completed" && |
| 74 | toolCallStatus !== "failed" && |
| 75 | output.type !== "BackgroundTaskStarted" |
| 76 | ) { |
| 77 | return events; |
| 78 | } |
| 79 | const attribution = (task: GrokBackgroundTaskRecord) => |
| 80 | task.turnId !== undefined && task.turnId === turnId ? { turnId } : {}; |
| 81 | const start = ( |
| 82 | id: string, |
| 83 | taskType: "monitor" | "shell", |
| 84 | description: string, |
| 85 | toolUseId?: string, |
| 86 | ) => { |
| 87 | const known = tasks.get(id); |
| 88 | if (known) return known; |
| 89 | const task: GrokBackgroundTaskRecord = { |
| 90 | payload: { |
| 91 | taskId: RuntimeTaskId.make(id), |
| 92 | taskType, |
| 93 | description, |
| 94 | title: description, |
| 95 | ...(toolUseId ? { toolUseId } : {}), |
| 96 | }, |
| 97 | // Polls can rediscover older tasks without establishing their originating turn. |
| 98 | turnId: toolUseId ? turnId : undefined, |
| 99 | }; |
| 100 | tasks.set(id, task); |
| 101 | events.push({ type: "task.started", payload: task.payload, ...attribution(task) }); |
| 102 | return task; |
| 103 | }; |
| 104 | const complete = ( |
| 105 | task: GrokBackgroundTaskRecord, |
| 106 | status: "completed" | "failed" | "stopped", |
| 107 | summary?: string, |
| 108 | ) => { |
| 109 | tasks.delete(task.payload.taskId); |
| 110 | events.push({ |
| 111 | type: "task.completed", |
| 112 | payload: { ...task.payload, status, ...(summary ? { summary } : {}) }, |
| 113 | ...attribution(task), |
| 114 | }); |
| 115 | }; |
| 116 | |
| 117 | if (output.type === "Monitor" && toolCallStatus === "completed") { |
| 118 | const id = text(output.taskId); |
| 119 | if (id) start(id, "monitor", text(record(input.rawInput).description) ?? "Monitor", toolCallId); |
| 120 | } else if (output.type === "BackgroundTaskStarted") { |
| 121 | const id = text(output.task_id) ?? text(output.taskId); |
| 122 | const command = text(output.command); |
| 123 | if (id && command) start(id, "shell", command.split("\n")[0]!.slice(0, 200), toolCallId); |
| 124 | } else if (output.type === "TaskOutput" || output.type === "KillTask") { |
| 125 | const results = record(output.MultiResult).results; |
| 126 | for (const value of Array.isArray(results) ? results : [output.Result]) { |
| 127 | const result = record(value); |
| 128 | const id = text(result.task_id); |
| 129 | if (!id) continue; |
| 130 | if (output.type === "KillTask") { |
| 131 | const task = tasks.get(id); |
| 132 | if (task && toolCallStatus === "completed" && result.outcome === "killed") |
| 133 | complete(task, "stopped"); |
| 134 | continue; |
| 135 | } |
| 136 | const command = text(result.command); |
| 137 | const status = lifecycle(result.status, result.exit_code); |
| 138 | if (!command || !status || command.startsWith("[subagent:")) continue; |
| 139 | const task = start(id, /^\[monitor[:\]]/.test(command) ? "monitor" : "shell", command); |
| 140 | const summary = text(result.output) |
| 141 | ?.split("\n") |
| 142 | .find((line) => line.trim()) |
| 143 | ?.trim(); |
| 144 | if (status === "running") { |
| 145 | events.push({ |
| 146 | type: "task.progress", |
| 147 | payload: { ...task.payload, ...(summary ? { summary } : {}) }, |
| 148 | ...attribution(task), |
| 149 | }); |
| 150 | } else { |
| 151 | complete(task, status, summary); |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 | return events; |
| 156 | } |