1import {
2 type RuntimeEventRawSource,
3 RuntimeItemId,
4 type CanonicalRequestType,
5 type EventId,
6 type ProviderApprovalDecision,
7 type ProviderApprovalOption,
8 type ProviderDriverKind,
9 type ProviderRuntimeEvent,
10 type RuntimeRequestId,
11 type ThreadId,
12 type TurnId,
13} from "@t3tools/contracts";
14
15import {
16 type AcpPermissionRequest,
17 type AcpPlanUpdate,
18 type AcpToolCallState,
19 canonicalItemTypeFromAcpToolKind,
20} from "./runtimeModel.ts";
21
22type AcpAdapterRawSource = Extract<
23 RuntimeEventRawSource,
24 "acp.jsonrpc" | `acp.${string}.extension`
25>;
26
27interface AcpEventStamp {
28 readonly eventId: EventId;
29 readonly createdAt: string;
30}
31
32type AcpCanonicalRequestType = Extract<
33 CanonicalRequestType,
34 "exec_command_approval" | "file_read_approval" | "file_change_approval" | "dynamic_tool_call"
35>;
36
37function canonicalRequestTypeFromAcpKind(kind: string | "unknown"): AcpCanonicalRequestType {
38 switch (kind) {
39 case "execute":
40 return "exec_command_approval";
41 case "read":
42 return "file_read_approval";
43 case "edit":
44 case "delete":
45 case "move":
46 return "file_change_approval";
47 default:
48 return "dynamic_tool_call";
49 }
50}
51
52function runtimeItemStatusFromAcpToolStatus(
53 status: AcpToolCallState["status"],
54): "inProgress" | "completed" | "failed" | undefined {
55 switch (status) {
56 case "pending":
57 case "inProgress":
58 return "inProgress";
59 case "completed":
60 return "completed";
61 case "failed":
62 return "failed";
63 default:
64 return undefined;
65 }
66}
67
68export function makeAcpRequestOpenedEvent(input: {
69 readonly stamp: AcpEventStamp;
70 readonly provider: ProviderDriverKind;
71 readonly threadId: ThreadId;
72 readonly turnId: TurnId | undefined;
73 readonly requestId: RuntimeRequestId;
74 readonly permissionRequest: AcpPermissionRequest;
75 readonly approvalOptions?: ReadonlyArray<ProviderApprovalOption>;
76 readonly detail: string;
77 readonly args: unknown;
78 readonly source: AcpAdapterRawSource;
79 readonly method: string;
80 readonly rawPayload: unknown;
81}): ProviderRuntimeEvent {
82 return {
83 type: "request.opened",
84 ...input.stamp,
85 provider: input.provider,
86 threadId: input.threadId,
87 turnId: input.turnId,
88 requestId: input.requestId,
89 payload: {
90 requestType: canonicalRequestTypeFromAcpKind(input.permissionRequest.kind),
91 detail: input.detail,
92 args: input.args,
93 ...(input.approvalOptions !== undefined ? { options: input.approvalOptions } : {}),
94 },
95 raw: {
96 source: input.source,
97 method: input.method,
98 payload: input.rawPayload,
99 },
100 };
101}
102
103export function makeAcpRequestResolvedEvent(input: {
104 readonly stamp: AcpEventStamp;
105 readonly provider: ProviderDriverKind;
106 readonly threadId: ThreadId;
107 readonly turnId: TurnId | undefined;
108 readonly requestId: RuntimeRequestId;
109 readonly permissionRequest: AcpPermissionRequest;
110 readonly decision: ProviderApprovalDecision;
111}): ProviderRuntimeEvent {
112 return {
113 type: "request.resolved",
114 ...input.stamp,
115 provider: input.provider,
116 threadId: input.threadId,
117 turnId: input.turnId,
118 requestId: input.requestId,
119 payload: {
120 requestType: canonicalRequestTypeFromAcpKind(input.permissionRequest.kind),
121 decision: input.decision,
122 },
123 };
124}
125
126export function makeAcpPlanUpdatedEvent(input: {
127 readonly stamp: AcpEventStamp;
128 readonly provider: ProviderDriverKind;
129 readonly threadId: ThreadId;
130 readonly turnId: TurnId | undefined;
131 readonly payload: AcpPlanUpdate;
132 readonly source: AcpAdapterRawSource;
133 readonly method: string;
134 readonly rawPayload: unknown;
135}): ProviderRuntimeEvent {
136 const payload =
137 input.payload.kind === "items"
138 ? {
139 plan: input.payload.plan,
140 ...(input.payload.explanation == null ? {} : { explanation: input.payload.explanation }),
141 }
142 : input.payload.kind === "removed"
143 ? { plan: [] }
144 : {
145 plan: [
146 {
147 step:
148 input.payload.kind === "markdown"
149 ? input.payload.markdown
150 : input.payload.kind === "file"
151 ? `Plan file: ${input.payload.uri}`
152 : `[Unsupported ACP plan content: ${input.payload.contentType}]`,
153 status: "pending" as const,
154 },
155 ],
156 };
157 return {
158 type: "turn.plan.updated",
159 ...input.stamp,
160 provider: input.provider,
161 threadId: input.threadId,
162 turnId: input.turnId,
163 payload,
164 raw: {
165 source: input.source,
166 method: input.method,
167 payload: input.rawPayload,
168 },
169 };
170}
171
172export function makeAcpToolCallEvent(input: {
173 readonly stamp: AcpEventStamp;
174 readonly provider: ProviderDriverKind;
175 readonly threadId: ThreadId;
176 readonly turnId: TurnId | undefined;
177 readonly toolCall: AcpToolCallState;
178 readonly rawPayload: unknown;
179}): ProviderRuntimeEvent {
180 const runtimeStatus = runtimeItemStatusFromAcpToolStatus(input.toolCall.status);
181 return {
182 type:
183 input.toolCall.status === "completed" || input.toolCall.status === "failed"
184 ? "item.completed"
185 : "item.updated",
186 ...input.stamp,
187 provider: input.provider,
188 threadId: input.threadId,
189 turnId: input.turnId,
190 itemId: RuntimeItemId.make(input.toolCall.toolCallId),
191 payload: {
192 itemType: canonicalItemTypeFromAcpToolKind(input.toolCall.kind),
193 ...(runtimeStatus ? { status: runtimeStatus } : {}),
194 ...(input.toolCall.title ? { title: input.toolCall.title } : {}),
195 ...(input.toolCall.detail ? { detail: input.toolCall.detail } : {}),
196 ...(Object.keys(input.toolCall.data).length > 0 ? { data: input.toolCall.data } : {}),
197 },
198 raw: {
199 source: "acp.jsonrpc",
200 method: "session/update",
201 payload: input.rawPayload,
202 },
203 };
204}
205
206export function makeAcpAssistantItemEvent(input: {
207 readonly stamp: AcpEventStamp;
208 readonly provider: ProviderDriverKind;
209 readonly threadId: ThreadId;
210 readonly turnId: TurnId | undefined;
211 readonly itemId: string;
212 readonly lifecycle: "item.started" | "item.completed";
213}): ProviderRuntimeEvent {
214 return {
215 type: input.lifecycle,
216 ...input.stamp,
217 provider: input.provider,
218 threadId: input.threadId,
219 turnId: input.turnId,
220 itemId: RuntimeItemId.make(input.itemId),
221 payload: {
222 itemType: "assistant_message",
223 status: input.lifecycle === "item.completed" ? "completed" : "inProgress",
224 },
225 };
226}
227
228export function makeAcpContentDeltaEvent(input: {
229 readonly stamp: AcpEventStamp;
230 readonly provider: ProviderDriverKind;
231 readonly threadId: ThreadId;
232 readonly turnId: TurnId | undefined;
233 readonly itemId?: string;
234 readonly streamKind?: "assistant_text" | "reasoning_text";
235 readonly text: string;
236 readonly rawPayload: unknown;
237}): ProviderRuntimeEvent {
238 return {
239 type: "content.delta",
240 ...input.stamp,
241 provider: input.provider,
242 threadId: input.threadId,
243 turnId: input.turnId,
244 ...(input.itemId ? { itemId: RuntimeItemId.make(input.itemId) } : {}),
245 payload: {
246 streamKind: input.streamKind ?? "assistant_text",
247 delta: input.text,
248 },
249 raw: {
250 source: "acp.jsonrpc",
251 method: "session/update",
252 payload: input.rawPayload,
253 },
254 };
255}