1import {
2 RuntimeTaskId,
3 type ProviderRuntimeTaskStartedEvent,
4 type ProviderRuntimeTaskProgressEvent,
5 type ProviderRuntimeTaskCompletedEvent,
6 type TurnId,
7} from "@t3tools/contracts";
8
9type TaskEvent =
10 | Pick<ProviderRuntimeTaskStartedEvent, "type" | "payload" | "turnId">
11 | Pick<ProviderRuntimeTaskProgressEvent, "type" | "payload" | "turnId">
12 | Pick<ProviderRuntimeTaskCompletedEvent, "type" | "payload" | "turnId">;
13
14export 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
25function 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
31function text(value: unknown): string | undefined {
32 return typeof value === "string" ? value.trim() || undefined : undefined;
33}
34
35function 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. */
61export 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}