History

Unify provider runtime events and add Cursor adapter layer

- migrate Claude/Codex/registry adapters to canonical runtime event types and payload shapes - add Cursor layer adapter plus tests and wire it through server layers - update provider contracts/runtime schemas and related adapter tests

Julius Marminge committed 8 months ago

Commit fd65a351 parent e80564814 files · +3746 −535Browse files

Unified · Split

14 files
  1. apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts +54 −40
  2. apps/server/src/provider/Layers/ClaudeCodeAdapter.ts +600 −48
  3. apps/server/src/provider/Layers/CodexAdapter.test.ts +5 −4
  4. apps/server/src/provider/Layers/CodexAdapter.ts +623 −155
  5. apps/server/src/provider/Layers/CursorAdapter.test.ts +350 −0
  6. apps/server/src/provider/Layers/CursorAdapter.ts +1378 −0
  7. apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts +20 −1
  8. apps/server/src/provider/Layers/ProviderAdapterRegistry.ts +2 −1
  9. apps/server/src/provider/Services/CursorAdapter.test.ts +97 −98
  10. apps/server/src/provider/Services/CursorAdapter.ts +74 −178
  11. apps/server/src/serverLayers.ts +5 −1
  12. packages/contracts/src/provider.test.ts +6 −0
  13. packages/contracts/src/provider.ts +6 −0
  14. packages/contracts/src/providerRuntime.ts +526 −9
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+54−40
@@ -229,7 +229,7 @@ describe("ClaudeCodeAdapterLive", () => { · 228 unmodified lines
229229 return Effect.gen(function* () {
230230 const adapter = yield* ClaudeCodeAdapter;
231231
232− const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 8).pipe(
232+ const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 11).pipe(
233233 Stream.runCollect,
234234 Effect.forkChild,
235235 );
@@ -315,40 +315,43 @@ describe("ClaudeCodeAdapterLive", () => { · 79 unmodified lines
315315 runtimeEvents.map((event) => event.type),
316316 [
317317 "session.started",
318+ "session.configured",
319+ "session.state.changed",
318320 "turn.started",
319321 "thread.started",
320− "message.delta",
321− "tool.started",
322− "tool.completed",
323− "message.completed",
322+ "content.delta",
323+ "item.started",
324+ "item.completed",
325+ "item.updated",
326+ "item.completed",
324327 "turn.completed",
325328 ],
326329 );
327330
328− const turnStarted = runtimeEvents[1];
331+ const turnStarted = runtimeEvents[3];
329332 assert.equal(turnStarted?.type, "turn.started");
330333 if (turnStarted?.type === "turn.started") {
331334 assert.equal(turnStarted.turnId, turn.turnId);
332335 }
333336
334− const deltaEvent = runtimeEvents[3];
335− assert.equal(deltaEvent?.type, "message.delta");
336− if (deltaEvent?.type === "message.delta") {
337− assert.equal(deltaEvent.delta, "Hi");
337+ const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta");
338+ assert.equal(deltaEvent?.type, "content.delta");
339+ if (deltaEvent?.type === "content.delta") {
340+ assert.equal(deltaEvent.payload.delta, "Hi");
338341 assert.equal(deltaEvent.turnId, turn.turnId);
339342 }
340343
341− const toolStarted = runtimeEvents[4];
342− assert.equal(toolStarted?.type, "tool.started");
343− if (toolStarted?.type === "tool.started") {
344− assert.equal(toolStarted.toolKind, "command");
344+ const toolStarted = runtimeEvents.find((event) => event.type === "item.started");
345+ assert.equal(toolStarted?.type, "item.started");
346+ if (toolStarted?.type === "item.started") {
347+ assert.equal(toolStarted.payload.itemType, "command_execution");
345348 }
346349
347− const turnCompleted = runtimeEvents[7];
350+ const turnCompleted = runtimeEvents[runtimeEvents.length - 1];
348351 assert.equal(turnCompleted?.type, "turn.completed");
349352 if (turnCompleted?.type === "turn.completed") {
350353 assert.equal(turnCompleted.turnId, turn.turnId);
351− assert.equal(turnCompleted.status, "completed");
354+ assert.equal(turnCompleted.payload.state, "completed");
352355 }
353356 }).pipe(
354357 Effect.provideService(Random.Random, makeDeterministicRandomService()),
@@ -361,7 +364,7 @@ describe("ClaudeCodeAdapterLive", () => { · 6 unmodified lines
361364 return Effect.gen(function* () {
362365 const adapter = yield* ClaudeCodeAdapter;
363366
364− const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe(
367+ const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe(
365368 Stream.runCollect,
366369 Effect.forkChild,
367370 );
@@ -416,22 +419,25 @@ describe("ClaudeCodeAdapterLive", () => { · 48 unmodified lines
416419 runtimeEvents.map((event) => event.type),
417420 [
418421 "session.started",
422+ "session.configured",
423+ "session.state.changed",
419424 "turn.started",
420425 "thread.started",
421− "message.delta",
422− "message.completed",
426+ "item.updated",
427+ "content.delta",
428+ "item.completed",
423429 "turn.completed",
424430 ],
425431 );
426432
427− const deltaIndex = runtimeEvents.findIndex((event) => event.type === "message.delta");
428− const completedIndex = runtimeEvents.findIndex((event) => event.type === "message.completed");
433+ const deltaIndex = runtimeEvents.findIndex((event) => event.type === "content.delta");
434+ const completedIndex = runtimeEvents.findIndex((event) => event.type === "item.completed");
429435 assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true);
430436
431437 const deltaEvent = runtimeEvents[deltaIndex];
432− assert.equal(deltaEvent?.type, "message.delta");
433− if (deltaEvent?.type === "message.delta") {
434− assert.equal(deltaEvent.delta, "Late text");
438+ assert.equal(deltaEvent?.type, "content.delta");
439+ if (deltaEvent?.type === "content.delta") {
440+ assert.equal(deltaEvent.payload.delta, "Late text");
435441 assert.equal(deltaEvent.turnId, turn.turnId);
436442 }
437443 }).pipe(
@@ -445,7 +451,7 @@ describe("ClaudeCodeAdapterLive", () => { · 7 unmodified lines
445451 return Effect.gen(function* () {
446452 const adapter = yield* ClaudeCodeAdapter;
447453
448− const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe(
454+ const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe(
449455 Stream.runCollect,
450456 Effect.forkChild,
451457 );
@@ -485,18 +491,21 @@ describe("ClaudeCodeAdapterLive", () => { · 33 unmodified lines
485491 runtimeEvents.map((event) => event.type),
486492 [
487493 "session.started",
494+ "session.configured",
495+ "session.state.changed",
488496 "turn.started",
489497 "thread.started",
490− "message.delta",
491− "message.completed",
498+ "item.updated",
499+ "content.delta",
500+ "item.completed",
492501 "turn.completed",
493502 ],
494503 );
495504
496− const deltaEvent = runtimeEvents.find((event) => event.type === "message.delta");
497− assert.equal(deltaEvent?.type, "message.delta");
498− if (deltaEvent?.type === "message.delta") {
499− assert.equal(deltaEvent.delta, "Fallback hello");
505+ const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta");
506+ assert.equal(deltaEvent?.type, "content.delta");
507+ if (deltaEvent?.type === "content.delta") {
508+ assert.equal(deltaEvent.payload.delta, "Fallback hello");
500509 assert.equal(deltaEvent.turnId, turn.turnId);
501510 }
502511 }).pipe(
@@ -552,7 +561,13 @@ describe("ClaudeCodeAdapterLive", () => { · 49 unmodified lines
552561 const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
553562 assert.deepEqual(
554563 runtimeEvents.map((event) => event.type),
555− ["session.started", "turn.started", "thread.started", "message.completed", "turn.completed"],
564+ [
565+ "session.started",
566+ "session.configured",
567+ "session.state.changed",
568+ "turn.started",
569+ "thread.started",
570+ ],
556571 );
557572
558573 const sessionStarted = runtimeEvents[0];
@@ -561,7 +576,7 @@ describe("ClaudeCodeAdapterLive", () => { · 2 unmodified lines
561576 assert.equal("threadId" in sessionStarted, false);
562577 }
563578
564− const threadStarted = runtimeEvents[2];
579+ const threadStarted = runtimeEvents[4];
565580 assert.equal(threadStarted?.type, "thread.started");
566581 if (threadStarted?.type === "thread.started") {
567582 assert.equal(threadStarted.threadId, "sdk-thread-real");
@@ -582,8 +597,7 @@ describe("ClaudeCodeAdapterLive", () => { · 14 unmodified lines
582597 approvalPolicy: "on-request",
583598 });
584599
585− const consumeSessionStarted = yield* Stream.runHead(adapter.streamEvents);
586− assert.equal(consumeSessionStarted._tag, "Some");
600+ yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain);
587601
588602 const createInput = harness.getLastCreateQueryInput();
589603 const canUseTool = createInput?.options.canUseTool;
@@ -613,8 +627,8 @@ describe("ClaudeCodeAdapterLive", () => { · 23 unmodified lines
613627 if (requested._tag !== "Some") {
614628 return;
615629 }
616− assert.equal(requested.value.type, "approval.requested");
617− if (requested.value.type !== "approval.requested") {
630+ assert.equal(requested.value.type, "request.opened");
631+ if (requested.value.type !== "request.opened") {
618632 return;
619633 }
620634
@@ -629,12 +643,12 @@ describe("ClaudeCodeAdapterLive", () => { · 8 unmodified lines
629643 if (resolved._tag !== "Some") {
630644 return;
631645 }
632− assert.equal(resolved.value.type, "approval.resolved");
633− if (resolved.value.type !== "approval.resolved") {
646+ assert.equal(resolved.value.type, "request.resolved");
647+ if (resolved.value.type !== "request.resolved") {
634648 return;
635649 }
636650 assert.equal(resolved.value.requestId, requested.value.requestId);
637− assert.equal(resolved.value.decision, "accept");
651+ assert.equal(resolved.value.payload.decision, "accept");
638652
639653 const permissionResult = yield* Effect.promise(() => permissionPromise);
640654 assert.equal((permissionResult as PermissionResult).behavior, "allow");
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+600−48
@@ -19,11 +19,12 @@ import { · 18 unmodified lines
1919 } from "@anthropic-ai/claude-agent-sdk";
2020 import {
2121 ApprovalRequestId,
22+ type CanonicalItemType,
23+ type CanonicalRequestType,
2224 EventId,
2325 type ProviderApprovalDecision,
2426 ProviderItemId,
2527 type ProviderRuntimeEvent,
26− type ProviderRuntimeToolKind,
2728 type ProviderRuntimeTurnStatus,
2829 type ProviderSendTurnInput,
2930 ProviderSessionId,
@@ -73,7 +74,7 @@ interface ClaudeTurnState { · 43 unmodified lines
7374 }
7475
7576 interface PendingApproval {
76− readonly requestKind: "command" | "file-change";
77+ readonly requestType: CanonicalRequestType;
7778 readonly detail?: string;
7879 readonly suggestions?: ReadonlyArray<PermissionUpdate>;
7980 readonly decision: Deferred.Deferred<ProviderApprovalDecision>;
@@ -81,7 +82,8 @@ interface PendingApproval { · 1 unmodified line
8182
8283 interface ToolInFlight {
8384 readonly itemId: string;
84− readonly toolKind: ProviderRuntimeToolKind;
85+ readonly itemType: CanonicalItemType;
86+ readonly toolName: string;
8587 readonly title: string;
8688 readonly detail?: string;
8789 }
@@ -188,7 +190,7 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef · 100 unmodified lines
188190 };
189191 }
190192
191−function classifyToolKind(toolName: string): ProviderRuntimeToolKind {
193+function classifyToolItemType(toolName: string): CanonicalItemType {
192194 const normalized = toolName.toLowerCase();
193195 if (
194196 normalized.includes("bash") ||
@@ -196,7 +198,7 @@ function classifyToolKind(toolName: string): ProviderRuntimeToolKind { · 1 unmodified line
196198 normalized.includes("shell") ||
197199 normalized.includes("terminal")
198200 ) {
199− return "command";
201+ return "command_execution";
200202 }
201203 if (
202204 normalized.includes("edit") ||
@@ -207,13 +209,18 @@ function classifyToolKind(toolName: string): ProviderRuntimeToolKind { · 4 unmodified lines
207209 normalized.includes("create") ||
208210 normalized.includes("delete")
209211 ) {
210− return "file-change";
212+ return "file_change";
211213 }
212− return "other";
214+ if (normalized.includes("mcp")) {
215+ return "mcp_tool_call";
216+ }
217+ return "dynamic_tool_call";
213218 }
214219
215−function classifyRequestKind(toolName: string): "command" | "file-change" {
216− return classifyToolKind(toolName) === "command" ? "command" : "file-change";
220+function classifyRequestType(toolName: string): CanonicalRequestType {
221+ return classifyToolItemType(toolName) === "command_execution"
222+ ? "command_execution_approval"
223+ : "file_change_approval";
217224 }
218225
219226 function summarizeToolRequest(toolName: string, input: Record<string, unknown>): string {
@@ -230,14 +237,18 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>): · 10 unmodified lines
230237 return `${toolName}: ${serialized.slice(0, 397)}...`;
231238 }
232239
233−function titleForTool(kind: ProviderRuntimeToolKind): string {
234− switch (kind) {
235− case "command":
240+function titleForTool(itemType: CanonicalItemType): string {
241+ switch (itemType) {
242+ case "command_execution":
236243 return "Command run";
237− case "file-change":
244+ case "file_change":
238245 return "File change";
239− case "other":
246+ case "mcp_tool_call":
247+ return "MCP tool call";
248+ case "dynamic_tool_call":
240249 return "Tool call";
250+ default:
251+ return "Item";
241252 }
242253 }
243254
@@ -284,6 +295,10 @@ function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStat · 40 unmodified lines
284295 return "failed";
285296 }
286297
298+function streamKindFromDeltaType(deltaType: string): "assistant_text" | "reasoning_text" {
299+ return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text";
300+}
301+
287302 function extractAssistantText(message: SDKMessage): string {
288303 if (message.type !== "assistant") {
289304 return "";
@@ -540,6 +555,20 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 250 unmodified lines
540555 sessionId: context.session.sessionId,
541556 createdAt: stamp.createdAt,
542557 threadId: nextThreadId,
558+ payload: {
559+ providerThreadId: nextThreadId,
560+ },
561+ providerRefs: {
562+ providerSessionId: context.session.sessionId,
563+ providerThreadId: nextThreadId,
564+ },
565+ raw: {
566+ source: "claude.sdk.message",
567+ method: "claude/thread/started",
568+ payload: {
569+ session_id: message.session_id,
570+ },
571+ },
543572 });
544573 }
545574 });
@@ -563,7 +592,44 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 17 unmodified lines
563592 createdAt: stamp.createdAt,
564593 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
565594 ...(turnState ? { turnId: turnState.turnId } : {}),
566− message,
595+ payload: {
596+ message,
597+ class: "provider_error",
598+ ...(cause !== undefined ? { detail: cause } : {}),
599+ },
600+ providerRefs: {
601+ providerSessionId: context.session.sessionId,
602+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
603+ ...(turnState ? { providerTurnId: turnState.turnId } : {}),
604+ },
605+ });
606+ });
607+
608+ const emitRuntimeWarning = (
609+ context: ClaudeSessionContext,
610+ message: string,
611+ detail?: unknown,
612+ ): Effect.Effect<void> =>
613+ Effect.gen(function* () {
614+ const turnState = context.turnState;
615+ const stamp = yield* makeEventStamp();
616+ yield* offerRuntimeEvent({
617+ type: "runtime.warning",
618+ eventId: stamp.eventId,
619+ provider: PROVIDER,
620+ sessionId: context.session.sessionId,
621+ createdAt: stamp.createdAt,
622+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
623+ ...(turnState ? { turnId: turnState.turnId } : {}),
624+ payload: {
625+ message,
626+ ...(detail !== undefined ? { detail } : {}),
627+ },
628+ providerRefs: {
629+ providerSessionId: context.session.sessionId,
630+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
631+ ...(turnState ? { providerTurnId: turnState.turnId } : {}),
632+ },
567633 });
568634 });
569635
@@ -571,6 +637,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 1 unmodified line
571637 context: ClaudeSessionContext,
572638 status: ProviderRuntimeTurnStatus,
573639 errorMessage?: string,
640+ result?: SDKResultMessage,
574641 ): Effect.Effect<void> =>
575642 Effect.gen(function* () {
576643 const turnState = context.turnState;
@@ -583,8 +650,20 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 6 unmodified lines
583650 sessionId: context.session.sessionId,
584651 createdAt: stamp.createdAt,
585652 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
586− status,
587− ...(errorMessage ? { errorMessage } : {}),
653+ payload: {
654+ state: status,
655+ ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}),
656+ ...(result?.usage ? { usage: result.usage } : {}),
657+ ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}),
658+ ...(typeof result?.total_cost_usd === "number"
659+ ? { totalCostUsd: result.total_cost_usd }
660+ : {}),
661+ ...(errorMessage ? { errorMessage } : {}),
662+ },
663+ providerRefs: {
664+ providerSessionId: context.session.sessionId,
665+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
666+ },
588667 });
589668 return;
590669 }
@@ -593,7 +672,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 2 unmodified lines
593672 if (!turnState.emittedTextDelta && turnState.fallbackAssistantText.length > 0) {
594673 const deltaStamp = yield* makeEventStamp();
595674 yield* offerRuntimeEvent({
596− type: "message.delta",
675+ type: "content.delta",
597676 eventId: deltaStamp.eventId,
598677 provider: PROVIDER,
599678 sessionId: context.session.sessionId,
@@ -601,13 +680,22 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 1 unmodified line
601680 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
602681 turnId: turnState.turnId,
603682 itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),
604− delta: turnState.fallbackAssistantText,
683+ payload: {
684+ streamKind: "assistant_text",
685+ delta: turnState.fallbackAssistantText,
686+ },
687+ providerRefs: {
688+ providerSessionId: context.session.sessionId,
689+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
690+ providerTurnId: turnState.turnId,
691+ providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),
692+ },
605693 });
606694 }
607695
608696 const stamp = yield* makeEventStamp();
609697 yield* offerRuntimeEvent({
610− type: "message.completed",
698+ type: "item.completed",
611699 eventId: stamp.eventId,
612700 provider: PROVIDER,
613701 sessionId: context.session.sessionId,
@@ -615,6 +703,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 1 unmodified line
615703 itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),
616704 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
617705 turnId: turnState.turnId,
706+ payload: {
707+ itemType: "assistant_message",
708+ status: "completed",
709+ title: "Assistant message",
710+ },
711+ providerRefs: {
712+ providerSessionId: context.session.sessionId,
713+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
714+ providerTurnId: turnState.turnId,
715+ providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),
716+ },
618717 });
619718 }
620719
@@ -632,8 +731,21 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 11 unmodified lines
632731 createdAt: stamp.createdAt,
633732 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
634733 turnId: turnState.turnId,
635− status,
636− ...(errorMessage ? { errorMessage } : {}),
734+ payload: {
735+ state: status,
736+ ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}),
737+ ...(result?.usage ? { usage: result.usage } : {}),
738+ ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}),
739+ ...(typeof result?.total_cost_usd === "number"
740+ ? { totalCostUsd: result.total_cost_usd }
741+ : {}),
742+ ...(errorMessage ? { errorMessage } : {}),
743+ },
744+ providerRefs: {
745+ providerSessionId: context.session.sessionId,
746+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
747+ providerTurnId: turnState.turnId,
748+ },
637749 });
638750
639751 const updatedAt = yield* nowIso;
@@ -673,7 +785,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 33 unmodified lines
673785 }
674786 const stamp = yield* makeEventStamp();
675787 yield* offerRuntimeEvent({
676− type: "message.delta",
788+ type: "content.delta",
677789 eventId: stamp.eventId,
678790 provider: PROVIDER,
679791 sessionId: context.session.sessionId,
@@ -681,7 +793,21 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 1 unmodified line
681793 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
682794 turnId: context.turnState.turnId,
683795 itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),
684− delta: event.delta.text,
796+ payload: {
797+ streamKind: streamKindFromDeltaType(event.delta.type),
798+ delta: event.delta.text,
799+ },
800+ providerRefs: {
801+ providerSessionId: context.session.sessionId,
802+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
803+ providerTurnId: context.turnState.turnId,
804+ providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),
805+ },
806+ raw: {
807+ source: "claude.sdk.message",
808+ method: "claude/stream_event/content_block_delta",
809+ payload: message,
810+ },
685811 });
686812 }
687813 return;
@@ -698,7 +824,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 10 unmodified lines
698824 }
699825
700826 const toolName = block.name;
701− const toolKind = classifyToolKind(toolName);
827+ const itemType = classifyToolItemType(toolName);
702828 const toolInput =
703829 typeof block.input === "object" && block.input !== null
704830 ? (block.input as Record<string, unknown>)
@@ -708,15 +834,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 3 unmodified lines
708834
709835 const tool: ToolInFlight = {
710836 itemId,
711− toolKind,
712− title: titleForTool(toolKind),
837+ itemType,
838+ toolName,
839+ title: titleForTool(itemType),
713840 detail,
714841 };
715842 context.inFlightTools.set(index, tool);
716843
717844 const stamp = yield* makeEventStamp();
718845 yield* offerRuntimeEvent({
719− type: "tool.started",
846+ type: "item.started",
720847 eventId: stamp.eventId,
721848 provider: PROVIDER,
722849 sessionId: context.session.sessionId,
@@ -724,9 +851,27 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 1 unmodified line
724851 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
725852 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
726853 itemId: ProviderItemId.makeUnsafe(tool.itemId),
727− toolKind: tool.toolKind,
728− title: tool.title,
729− ...(tool.detail ? { detail: tool.detail } : {}),
854+ payload: {
855+ itemType: tool.itemType,
856+ status: "inProgress",
857+ title: tool.title,
858+ ...(tool.detail ? { detail: tool.detail } : {}),
859+ data: {
860+ toolName: tool.toolName,
861+ input: toolInput,
862+ },
863+ },
864+ providerRefs: {
865+ providerSessionId: context.session.sessionId,
866+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
867+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
868+ providerItemId: ProviderItemId.makeUnsafe(tool.itemId),
869+ },
870+ raw: {
871+ source: "claude.sdk.message",
872+ method: "claude/stream_event/content_block_start",
873+ payload: message,
874+ },
730875 });
731876 return;
732877 }
@@ -741,7 +886,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 8 unmodified lines
741886
742887 const stamp = yield* makeEventStamp();
743888 yield* offerRuntimeEvent({
744− type: "tool.completed",
889+ type: "item.completed",
745890 eventId: stamp.eventId,
746891 provider: PROVIDER,
747892 sessionId: context.session.sessionId,
@@ -749,9 +894,23 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 1 unmodified line
749894 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
750895 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
751896 itemId: ProviderItemId.makeUnsafe(tool.itemId),
752− toolKind: tool.toolKind,
753− title: tool.title,
754− ...(tool.detail ? { detail: tool.detail } : {}),
897+ payload: {
898+ itemType: tool.itemType,
899+ status: "completed",
900+ title: tool.title,
901+ ...(tool.detail ? { detail: tool.detail } : {}),
902+ },
903+ providerRefs: {
904+ providerSessionId: context.session.sessionId,
905+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
906+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
907+ providerItemId: ProviderItemId.makeUnsafe(tool.itemId),
908+ },
909+ raw: {
910+ source: "claude.sdk.message",
911+ method: "claude/stream_event/content_block_stop",
912+ payload: message,
913+ },
755914 });
756915 }
757916 });
@@ -777,6 +936,35 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 19 unmodified lines
777936 fallbackAssistantText,
778937 };
779938 }
939+
940+ const stamp = yield* makeEventStamp();
941+ yield* offerRuntimeEvent({
942+ type: "item.updated",
943+ eventId: stamp.eventId,
944+ provider: PROVIDER,
945+ sessionId: context.session.sessionId,
946+ createdAt: stamp.createdAt,
947+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
948+ turnId: context.turnState.turnId,
949+ itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),
950+ payload: {
951+ itemType: "assistant_message",
952+ status: "inProgress",
953+ title: "Assistant message",
954+ data: message.message,
955+ },
956+ providerRefs: {
957+ providerSessionId: context.session.sessionId,
958+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
959+ providerTurnId: context.turnState.turnId,
960+ providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),
961+ },
962+ raw: {
963+ source: "claude.sdk.message",
964+ method: "claude/assistant",
965+ payload: message,
966+ },
967+ });
780968 }
781969
782970 context.lastAssistantUuid = message.uuid;
@@ -799,7 +987,251 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 16 unmodified lines
799987 yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");
800988 }
801989
802− yield* completeTurn(context, status, errorMessage);
990+ yield* completeTurn(context, status, errorMessage, message);
991+ });
992+
993+ const handleSystemMessage = (
994+ context: ClaudeSessionContext,
995+ message: SDKMessage,
996+ ): Effect.Effect<void> =>
997+ Effect.gen(function* () {
998+ if (message.type !== "system") {
999+ return;
1000+ }
1001+
1002+ const stamp = yield* makeEventStamp();
1003+ const base = {
1004+ eventId: stamp.eventId,
1005+ provider: PROVIDER,
1006+ sessionId: context.session.sessionId,
1007+ createdAt: stamp.createdAt,
1008+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1009+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1010+ providerRefs: {
1011+ providerSessionId: context.session.sessionId,
1012+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1013+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
1014+ },
1015+ raw: {
1016+ source: "claude.sdk.message" as const,
1017+ method: sdkNativeMethod(message),
1018+ messageType: `${message.type}:${message.subtype}`,
1019+ payload: message,
1020+ },
1021+ };
1022+
1023+ switch (message.subtype) {
1024+ case "init":
1025+ yield* offerRuntimeEvent({
1026+ ...base,
1027+ type: "session.configured",
1028+ payload: {
1029+ config: message as Record<string, unknown>,
1030+ },
1031+ });
1032+ return;
1033+ case "status":
1034+ yield* offerRuntimeEvent({
1035+ ...base,
1036+ type: "session.state.changed",
1037+ payload: {
1038+ state: message.status === "compacting" ? "waiting" : "running",
1039+ reason: `status:${message.status ?? "active"}`,
1040+ detail: message,
1041+ },
1042+ });
1043+ return;
1044+ case "compact_boundary":
1045+ yield* offerRuntimeEvent({
1046+ ...base,
1047+ type: "thread.state.changed",
1048+ payload: {
1049+ state: "compacted",
1050+ detail: message,
1051+ },
1052+ });
1053+ return;
1054+ case "hook_started":
1055+ yield* offerRuntimeEvent({
1056+ ...base,
1057+ type: "hook.started",
1058+ payload: {
1059+ hookId: message.hook_id,
1060+ hookName: message.hook_name,
1061+ hookEvent: message.hook_event,
1062+ },
1063+ });
1064+ return;
1065+ case "hook_progress":
1066+ yield* offerRuntimeEvent({
1067+ ...base,
1068+ type: "hook.progress",
1069+ payload: {
1070+ hookId: message.hook_id,
1071+ output: message.output,
1072+ stdout: message.stdout,
1073+ stderr: message.stderr,
1074+ },
1075+ });
1076+ return;
1077+ case "hook_response":
1078+ yield* offerRuntimeEvent({
1079+ ...base,
1080+ type: "hook.completed",
1081+ payload: {
1082+ hookId: message.hook_id,
1083+ outcome: message.outcome,
1084+ output: message.output,
1085+ stdout: message.stdout,
1086+ stderr: message.stderr,
1087+ ...(typeof message.exit_code === "number" ? { exitCode: message.exit_code } : {}),
1088+ },
1089+ });
1090+ return;
1091+ case "task_started":
1092+ yield* offerRuntimeEvent({
1093+ ...base,
1094+ type: "task.started",
1095+ payload: {
1096+ taskId: message.task_id,
1097+ description: message.description,
1098+ ...(message.task_type ? { taskType: message.task_type } : {}),
1099+ },
1100+ });
1101+ return;
1102+ case "task_progress":
1103+ yield* offerRuntimeEvent({
1104+ ...base,
1105+ type: "task.progress",
1106+ payload: {
1107+ taskId: message.task_id,
1108+ description: message.description,
1109+ ...(message.usage ? { usage: message.usage } : {}),
1110+ ...(message.last_tool_name ? { lastToolName: message.last_tool_name } : {}),
1111+ },
1112+ });
1113+ return;
1114+ case "task_notification":
1115+ yield* offerRuntimeEvent({
1116+ ...base,
1117+ type: "task.completed",
1118+ payload: {
1119+ taskId: message.task_id,
1120+ status: message.status,
1121+ ...(message.summary ? { summary: message.summary } : {}),
1122+ ...(message.usage ? { usage: message.usage } : {}),
1123+ },
1124+ });
1125+ return;
1126+ case "files_persisted":
1127+ yield* offerRuntimeEvent({
1128+ ...base,
1129+ type: "files.persisted",
1130+ payload: {
1131+ files: Array.isArray(message.files)
1132+ ? message.files.map((file: { filename: string; file_id: string }) => ({
1133+ filename: file.filename,
1134+ fileId: file.file_id,
1135+ }))
1136+ : [],
1137+ ...(Array.isArray(message.failed)
1138+ ? {
1139+ failed: message.failed.map((entry: { filename: string; error: string }) => ({
1140+ filename: entry.filename,
1141+ error: entry.error,
1142+ })),
1143+ }
1144+ : {}),
1145+ },
1146+ });
1147+ return;
1148+ default:
1149+ yield* emitRuntimeWarning(
1150+ context,
1151+ `Unhandled Claude system message subtype '${message.subtype}'.`,
1152+ message,
1153+ );
1154+ return;
1155+ }
1156+ });
1157+
1158+ const handleSdkTelemetryMessage = (
1159+ context: ClaudeSessionContext,
1160+ message: SDKMessage,
1161+ ): Effect.Effect<void> =>
1162+ Effect.gen(function* () {
1163+ const stamp = yield* makeEventStamp();
1164+ const base = {
1165+ eventId: stamp.eventId,
1166+ provider: PROVIDER,
1167+ sessionId: context.session.sessionId,
1168+ createdAt: stamp.createdAt,
1169+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1170+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1171+ providerRefs: {
1172+ providerSessionId: context.session.sessionId,
1173+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1174+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
1175+ },
1176+ raw: {
1177+ source: "claude.sdk.message" as const,
1178+ method: sdkNativeMethod(message),
1179+ messageType: message.type,
1180+ payload: message,
1181+ },
1182+ };
1183+
1184+ if (message.type === "tool_progress") {
1185+ yield* offerRuntimeEvent({
1186+ ...base,
1187+ type: "tool.progress",
1188+ payload: {
1189+ toolUseId: message.tool_use_id,
1190+ toolName: message.tool_name,
1191+ elapsedSeconds: message.elapsed_time_seconds,
1192+ ...(message.task_id ? { summary: `task:${message.task_id}` } : {}),
1193+ },
1194+ });
1195+ return;
1196+ }
1197+
1198+ if (message.type === "tool_use_summary") {
1199+ yield* offerRuntimeEvent({
1200+ ...base,
1201+ type: "tool.summary",
1202+ payload: {
1203+ summary: message.summary,
1204+ ...(message.preceding_tool_use_ids.length > 0
1205+ ? { precedingToolUseIds: message.preceding_tool_use_ids }
1206+ : {}),
1207+ },
1208+ });
1209+ return;
1210+ }
1211+
1212+ if (message.type === "auth_status") {
1213+ yield* offerRuntimeEvent({
1214+ ...base,
1215+ type: "auth.status",
1216+ payload: {
1217+ isAuthenticating: message.isAuthenticating,
1218+ output: message.output,
1219+ ...(message.error ? { error: message.error } : {}),
1220+ },
1221+ });
1222+ return;
1223+ }
1224+
1225+ if (message.type === "rate_limit") {
1226+ yield* offerRuntimeEvent({
1227+ ...base,
1228+ type: "account.rate-limits.updated",
1229+ payload: {
1230+ rateLimits: message,
1231+ },
1232+ });
1233+ return;
1234+ }
8031235 });
8041236
8051237 const handleSdkMessage = (
@@ -820,7 +1252,21 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 14 unmodified lines
8201252 case "result":
8211253 yield* handleResultMessage(context, message);
8221254 return;
1255+ case "system":
1256+ yield* handleSystemMessage(context, message);
1257+ return;
1258+ case "tool_progress":
1259+ case "tool_use_summary":
1260+ case "auth_status":
1261+ case "rate_limit":
1262+ yield* handleSdkTelemetryMessage(context, message);
1263+ return;
8231264 default:
1265+ yield* emitRuntimeWarning(
1266+ context,
1267+ `Unhandled Claude SDK message type '${message.type}'.`,
1268+ message,
1269+ );
8241270 return;
8251271 }
8261272 });
@@ -854,7 +1300,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 27 unmodified lines
8541300 yield* Deferred.succeed(pending.decision, "cancel");
8551301 const stamp = yield* makeEventStamp();
8561302 yield* offerRuntimeEvent({
857− type: "approval.resolved",
1303+ type: "request.resolved",
8581304 eventId: stamp.eventId,
8591305 provider: PROVIDER,
8601306 sessionId: context.session.sessionId,
@@ -862,8 +1308,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 1 unmodified line
8621308 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
8631309 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
8641310 requestId,
865− requestKind: pending.requestKind,
866− decision: "cancel",
1311+ payload: {
1312+ requestType: pending.requestType,
1313+ decision: "cancel",
1314+ },
1315+ providerRefs: {
1316+ providerSessionId: context.session.sessionId,
1317+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1318+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
1319+ providerRequestId: requestId,
1320+ },
8671321 });
8681322 }
8691323 context.pendingApprovals.clear();
@@ -893,7 +1347,14 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 23 unmodified lines
8931347 sessionId: context.session.sessionId,
8941348 createdAt: stamp.createdAt,
8951349 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
896− message: "Session stopped",
1350+ payload: {
1351+ reason: "Session stopped",
1352+ exitKind: "graceful",
1353+ },
1354+ providerRefs: {
1355+ providerSessionId: context.session.sessionId,
1356+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1357+ },
8971358 });
8981359 }
8991360
@@ -972,11 +1433,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 72 unmodified lines
9721433 }
9731434
9741435 const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4);
975− const requestKind = classifyRequestKind(toolName);
1436+ const requestType = classifyRequestType(toolName);
9761437 const detail = summarizeToolRequest(toolName, toolInput);
9771438 const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>();
9781439 const pendingApproval: PendingApproval = {
979− requestKind,
1440+ requestType,
9801441 detail,
9811442 decision: decisionDeferred,
9821443 ...(callbackOptions.suggestions
@@ -986,7 +1447,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 3 unmodified lines
9861447
9871448 const requestedStamp = yield* makeEventStamp();
9881449 yield* offerRuntimeEvent({
989− type: "approval.requested",
1450+ type: "request.opened",
9901451 eventId: requestedStamp.eventId,
9911452 provider: PROVIDER,
9921453 sessionId: context.session.sessionId,
@@ -994,8 +1455,29 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 1 unmodified line
9941455 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
9951456 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
9961457 requestId,
997− requestKind,
998− detail,
1458+ payload: {
1459+ requestType,
1460+ detail,
1461+ args: {
1462+ toolName,
1463+ input: toolInput,
1464+ ...(callbackOptions.toolUseID ? { toolUseId: callbackOptions.toolUseID } : {}),
1465+ },
1466+ },
1467+ providerRefs: {
1468+ providerSessionId: context.session.sessionId,
1469+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1470+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
1471+ providerRequestId: requestId,
1472+ },
1473+ raw: {
1474+ source: "claude.sdk.permission",
1475+ method: "canUseTool/request",
1476+ payload: {
1477+ toolName,
1478+ input: toolInput,
1479+ },
1480+ },
9991481 });
10001482
10011483 pendingApprovals.set(requestId, pendingApproval);
@@ -1017,7 +1499,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 15 unmodified lines
10171499
10181500 const resolvedStamp = yield* makeEventStamp();
10191501 yield* offerRuntimeEvent({
1020− type: "approval.resolved",
1502+ type: "request.resolved",
10211503 eventId: resolvedStamp.eventId,
10221504 provider: PROVIDER,
10231505 sessionId: context.session.sessionId,
@@ -1025,8 +1507,23 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 1 unmodified line
10251507 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
10261508 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
10271509 requestId,
1028− requestKind,
1029− decision,
1510+ payload: {
1511+ requestType,
1512+ decision,
1513+ },
1514+ providerRefs: {
1515+ providerSessionId: context.session.sessionId,
1516+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1517+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
1518+ providerRequestId: requestId,
1519+ },
1520+ raw: {
1521+ source: "claude.sdk.permission",
1522+ method: "canUseTool/decision",
1523+ payload: {
1524+ decision,
1525+ },
1526+ },
10301527 });
10311528
10321529 if (decision === "accept" || decision === "acceptForSession") {
@@ -1134,6 +1631,52 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 101 unmodified lines
11341631 sessionId,
11351632 createdAt: sessionStartedStamp.createdAt,
11361633 ...(threadId ? { threadId } : {}),
1634+ payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {},
1635+ providerRefs: {
1636+ providerSessionId: sessionId,
1637+ ...(threadId ? { providerThreadId: threadId } : {}),
1638+ },
1639+ });
1640+
1641+ const configuredStamp = yield* makeEventStamp();
1642+ yield* offerRuntimeEvent({
1643+ type: "session.configured",
1644+ eventId: configuredStamp.eventId,
1645+ provider: PROVIDER,
1646+ sessionId,
1647+ createdAt: configuredStamp.createdAt,
1648+ ...(threadId ? { threadId } : {}),
1649+ payload: {
1650+ config: {
1651+ ...(input.model ? { model: input.model } : {}),
1652+ ...(input.cwd ? { cwd: input.cwd } : {}),
1653+ ...(permissionMode ? { permissionMode } : {}),
1654+ ...(providerOptions?.maxThinkingTokens !== undefined
1655+ ? { maxThinkingTokens: providerOptions.maxThinkingTokens }
1656+ : {}),
1657+ },
1658+ },
1659+ providerRefs: {
1660+ providerSessionId: sessionId,
1661+ ...(threadId ? { providerThreadId: threadId } : {}),
1662+ },
1663+ });
1664+
1665+ const readyStamp = yield* makeEventStamp();
1666+ yield* offerRuntimeEvent({
1667+ type: "session.state.changed",
1668+ eventId: readyStamp.eventId,
1669+ provider: PROVIDER,
1670+ sessionId,
1671+ createdAt: readyStamp.createdAt,
1672+ ...(threadId ? { threadId } : {}),
1673+ payload: {
1674+ state: "ready",
1675+ },
1676+ providerRefs: {
1677+ providerSessionId: sessionId,
1678+ ...(threadId ? { providerThreadId: threadId } : {}),
1679+ },
11371680 });
11381681
11391682 Effect.runFork(runSdkStream(context));
@@ -1191,6 +1734,15 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { · 51 unmodified lines
11911734 createdAt: turnStartedStamp.createdAt,
11921735 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
11931736 turnId,
1737+ payload: {
1738+ ...(input.model ? { model: input.model } : {}),
1739+ ...(input.effort ? { effort: input.effort } : {}),
1740+ },
1741+ providerRefs: {
1742+ providerSessionId: context.session.sessionId,
1743+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1744+ providerTurnId: turnId,
1745+ },
11941746 });
11951747
11961748 const message = buildUserMessage(input);
apps/server/src/provider/Layers/CodexAdapter.test.ts+5−4
@@ -214,7 +214,7 @@ const lifecycleLayer = it.layer( · 213 unmodified lines
214214 );
215215
216216 lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
217− it.effect("maps completed agent message items to canonical message.completed events", () =>
217+ it.effect("maps completed agent message items to canonical item.completed events", () =>
218218 Effect.gen(function* () {
219219 const adapter = yield* CodexAdapter;
220220 const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);
@@ -244,12 +244,13 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { · 23 unmodified lines
244244 if (firstEvent._tag !== "Some") {
245245 return;
246246 }
247− assert.equal(firstEvent.value.type, "message.completed");
248− if (firstEvent.value.type !== "message.completed") {
247+ assert.equal(firstEvent.value.type, "item.completed");
248+ if (firstEvent.value.type !== "item.completed") {
249249 return;
250250 }
251251 assert.equal(firstEvent.value.itemId, "msg_1");
252252 assert.equal(firstEvent.value.turnId, "turn-1");
253+ assert.equal(firstEvent.value.payload.itemType, "assistant_message");
253254 }),
254255 );
255256
@@ -280,7 +281,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { · 24 unmodified lines
280281 return;
281282 }
282283 assert.equal(firstEvent.value.sessionId, "sess-1");
283− assert.equal(firstEvent.value.message, "Session stopped");
284+ assert.equal(firstEvent.value.payload.reason, "Session stopped");
284285 }),
285286 );
286287 });
apps/server/src/provider/Layers/CodexAdapter.ts+623−155
@@ -7,11 +7,10 @@ · 6 unmodified lines
77 * @module CodexAdapterLive
88 */
99 import {
10+ type CanonicalItemType,
11+ type CanonicalRequestType,
1012 type ProviderEvent,
1113 type ProviderRuntimeEvent,
12− type ProviderRuntimeTurnStatus,
13− type ProviderRuntimeToolKind,
14− ProviderItemId,
1514 ProviderApprovalDecision,
1615 ProviderSessionId,
1716 ProviderThreadId,
@@ -100,7 +99,11 @@ function asString(value: unknown): string | undefined { · 82 unmodified lines
10099 return typeof value === "string" ? value : undefined;
101100 }
102101
103−function toTurnStatus(value: unknown): ProviderRuntimeTurnStatus | undefined {
102+function asNumber(value: unknown): number | undefined {
103+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
104+}
105+
106+function toTurnStatus(value: unknown): "completed" | "failed" | "cancelled" | "interrupted" {
104107 switch (value) {
105108 case "completed":
106109 case "failed":
@@ -108,7 +111,7 @@ function toTurnStatus(value: unknown): ProviderRuntimeTurnStatus | undefined { · 1 unmodified line
108111 case "interrupted":
109112 return value;
110113 default:
111− return undefined;
114+ return "completed";
112115 }
113116 }
114117
@@ -123,29 +126,53 @@ function normalizeItemType(raw: unknown): string { · 8 unmodified lines
123126 .toLowerCase();
124127 }
125128
126−function shouldDropItemType(type: string): boolean {
127− if (type.includes("preamble") || type.includes("reasoning") || type.includes("thought")) {
128− return true;
129− }
130− return type === "work" || type.startsWith("work ");
129+function toCanonicalItemType(raw: unknown): CanonicalItemType {
130+ const type = normalizeItemType(raw);
131+ if (type.includes("user")) return "user_message";
132+ if (type.includes("agent message") || type.includes("assistant")) return "assistant_message";
133+ if (type.includes("reasoning") || type.includes("thought")) return "reasoning";
134+ if (type.includes("plan") || type.includes("todo")) return "plan";
135+ if (type.includes("command")) return "command_execution";
136+ if (type.includes("file change") || type.includes("patch") || type.includes("edit")) return "file_change";
137+ if (type.includes("mcp")) return "mcp_tool_call";
138+ if (type.includes("dynamic tool")) return "dynamic_tool_call";
139+ if (type.includes("collab")) return "collab_agent_tool_call";
140+ if (type.includes("web search")) return "web_search";
141+ if (type.includes("image")) return "image_view";
142+ if (type.includes("review entered")) return "review_entered";
143+ if (type.includes("review exited")) return "review_exited";
144+ if (type.includes("compact")) return "context_compaction";
145+ if (type.includes("error")) return "error";
146+ return "unknown";
131147 }
132148
133−function toolMeta(type: string):
134− | {
135− readonly toolKind: ProviderRuntimeToolKind;
136− readonly title: string;
137− }
138− | undefined {
139− if (type.includes("command")) {
140− return { toolKind: "command", title: "Command run" };
141− }
142− if (type.includes("file change")) {
143− return { toolKind: "file-change", title: "File change" };
144− }
145− if (type.includes("tool")) {
146− return { toolKind: "other", title: "Tool call" };
149+function itemTitle(itemType: CanonicalItemType): string | undefined {
150+ switch (itemType) {
151+ case "assistant_message":
152+ return "Assistant message";
153+ case "user_message":
154+ return "User message";
155+ case "reasoning":
156+ return "Reasoning";
157+ case "plan":
158+ return "Plan";
159+ case "command_execution":
160+ return "Command run";
161+ case "file_change":
162+ return "File change";
163+ case "mcp_tool_call":
164+ return "MCP tool call";
165+ case "dynamic_tool_call":
166+ return "Tool call";
167+ case "web_search":
168+ return "Web search";
169+ case "image_view":
170+ return "Image view";
171+ case "error":
172+ return "Error";
173+ default:
174+ return undefined;
147175 }
148− return undefined;
149176 }
150177
151178 function itemDetail(
@@ -174,80 +201,155 @@ function itemDetail( · 22 unmodified lines
174201 return undefined;
175202 }
176203
177−function mapMessageCompletedEvent(event: ProviderEvent): ProviderRuntimeEvent | undefined {
178− if (event.method !== "item/completed") {
179− return undefined;
204+function toRequestTypeFromMethod(method: string): CanonicalRequestType {
205+ switch (method) {
206+ case "item/commandExecution/requestApproval":
207+ return "command_execution_approval";
208+ case "item/fileChange/requestApproval":
209+ return "file_change_approval";
210+ case "applyPatchApproval":
211+ return "apply_patch_approval";
212+ case "execCommandApproval":
213+ return "exec_command_approval";
214+ case "item/tool/requestUserInput":
215+ return "tool_user_input";
216+ case "item/tool/call":
217+ return "dynamic_tool_call";
218+ case "account/chatgptAuthTokens/refresh":
219+ return "auth_tokens_refresh";
220+ default:
221+ return "unknown";
180222 }
223+}
181224
182− const payload = asObject(event.payload);
183− const item = asObject(payload?.item);
184− if (!item) {
185− return undefined;
225+function toRequestTypeFromKind(kind: unknown): CanonicalRequestType {
226+ switch (kind) {
227+ case "command":
228+ return "command_execution_approval";
229+ case "file-change":
230+ return "file_change_approval";
231+ default:
232+ return "unknown";
186233 }
234+}
187235
188− const normalizedType = normalizeItemType(item.type ?? item.kind);
189− if (!normalizedType.includes("agent message")) {
190− return undefined;
236+function toThreadState(value: unknown):
237+ | "active"
238+ | "idle"
239+ | "archived"
240+ | "closed"
241+ | "compacted"
242+ | "error" {
243+ switch (value) {
244+ case "idle":
245+ return "idle";
246+ case "archived":
247+ return "archived";
248+ case "closed":
249+ return "closed";
250+ case "compacted":
251+ return "compacted";
252+ case "error":
253+ case "failed":
254+ return "error";
255+ default:
256+ return "active";
191257 }
258+}
192259
193− const itemId = event.itemId ?? asString(item.id);
194− if (!itemId) {
195− return undefined;
260+function contentStreamKindFromMethod(
261+ method: string,
262+): "assistant_text" | "reasoning_text" | "reasoning_summary_text" | "plan_text" | "command_output" | "file_change_output" {
263+ switch (method) {
264+ case "item/agentMessage/delta":
265+ return "assistant_text";
266+ case "item/reasoning/textDelta":
267+ return "reasoning_text";
268+ case "item/reasoning/summaryTextDelta":
269+ return "reasoning_summary_text";
270+ case "item/plan/delta":
271+ return "plan_text";
272+ case "item/commandExecution/outputDelta":
273+ return "command_output";
274+ case "item/fileChange/outputDelta":
275+ return "file_change_output";
276+ default:
277+ return "assistant_text";
196278 }
279+}
280+
281+function eventRawSource(event: ProviderEvent): ProviderRuntimeEvent["raw"]["source"] {
282+ return event.kind === "request" ? "codex.app-server.request" : "codex.app-server.notification";
283+}
284+
285+function providerRefsFromEvent(event: ProviderEvent): ProviderRuntimeEvent["providerRefs"] | undefined {
286+ const refs: Record<string, string> = {
287+ providerSessionId: event.sessionId,
288+ };
289+ if (event.threadId) refs.providerThreadId = event.threadId;
290+ if (event.turnId) refs.providerTurnId = event.turnId;
291+ if (event.itemId) refs.providerItemId = event.itemId;
292+ if (event.requestId) refs.providerRequestId = event.requestId;
293+
294+ return Object.keys(refs).length > 0
295+ ? (refs as ProviderRuntimeEvent["providerRefs"])
296+ : undefined;
297+}
197298
299+function runtimeEventBase(event: ProviderEvent): Omit<ProviderRuntimeEvent, "type" | "payload"> {
300+ const refs = providerRefsFromEvent(event);
198301 return {
199− type: "message.completed",
200302 eventId: event.id,
201303 provider: event.provider,
202304 sessionId: event.sessionId,
203305 createdAt: event.createdAt,
204− itemId: ProviderItemId.makeUnsafe(itemId),
205306 ...(event.threadId ? { threadId: event.threadId } : {}),
206307 ...(event.turnId ? { turnId: event.turnId } : {}),
308+ ...(event.itemId ? { itemId: event.itemId } : {}),
309+ ...(event.requestId ? { requestId: event.requestId } : {}),
310+ ...(refs ? { providerRefs: refs } : {}),
311+ raw: {
312+ source: eventRawSource(event),
313+ method: event.method,
314+ payload: event.payload ?? {},
315+ },
207316 };
208317 }
209318
210−function mapToolEvent(event: ProviderEvent): ProviderRuntimeEvent | undefined {
211− if (event.method !== "item/started" && event.method !== "item/completed") {
212− return undefined;
213− }
319+function mapItemLifecycle(
320+ event: ProviderEvent,
321+ lifecycle: "item.started" | "item.updated" | "item.completed",
322+): ProviderRuntimeEvent | undefined {
214323 const payload = asObject(event.payload);
215324 const item = asObject(payload?.item);
216− if (!item) {
325+ const source = item ?? payload;
326+ if (!source) {
217327 return undefined;
218328 }
219− const normalizedType = normalizeItemType(item.type ?? item.kind);
220− if (shouldDropItemType(normalizedType)) {
221− return undefined;
222− }
223− const meta = toolMeta(normalizedType);
224− if (!meta) {
329+
330+ const itemType = toCanonicalItemType(source.type ?? source.kind);
331+ if (itemType === "unknown" && lifecycle !== "item.updated") {
225332 return undefined;
226333 }
227334
228− const eventBase = {
229− eventId: event.id,
230− provider: event.provider,
231− sessionId: event.sessionId,
232− createdAt: event.createdAt,
233− ...(event.threadId ? { threadId: event.threadId } : {}),
234− ...(event.turnId ? { turnId: event.turnId } : {}),
235− ...(event.itemId ? { itemId: event.itemId } : {}),
236− toolKind: meta.toolKind,
237− title: meta.title,
238− ...(payload ? { detail: itemDetail(item, payload) } : {}),
239− } as const;
240−
241− if (event.method === "item/started") {
242− return {
243− type: "tool.started",
244− ...eventBase,
245− };
246− }
335+ const detail = itemDetail(source, payload ?? source);
336+ const status =
337+ lifecycle === "item.started"
338+ ? "inProgress"
339+ : lifecycle === "item.completed"
340+ ? "completed"
341+ : undefined;
247342
248343 return {
249− type: "tool.completed",
250− ...eventBase,
344+ ...runtimeEventBase(event),
345+ type: lifecycle,
346+ payload: {
347+ itemType,
348+ ...(status ? { status } : {}),
349+ ...(itemTitle(itemType) ? { title: itemTitle(itemType) } : {}),
350+ ...(detail ? { detail } : {}),
351+ ...(event.payload !== undefined ? { data: event.payload } : {}),
352+ },
251353 };
252354 }
253355
@@ -261,78 +363,87 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime · 7 unmodified lines
261363 }
262364 return [
263365 {
366+ ...runtimeEventBase(event),
264367 type: "runtime.error",
265− eventId: event.id,
266− provider: event.provider,
267− sessionId: event.sessionId,
268− createdAt: event.createdAt,
269− ...(event.threadId ? { threadId: event.threadId } : {}),
270− ...(event.turnId ? { turnId: event.turnId } : {}),
271− ...(event.itemId ? { itemId: event.itemId } : {}),
272− message: event.message,
368+ payload: {
369+ message: event.message,
370+ class: "provider_error",
371+ ...(event.payload !== undefined ? { detail: event.payload } : {}),
372+ },
273373 },
274374 ];
275375 }
276376
277− if (event.kind === "request" && event.requestId && event.requestKind) {
377+ if (event.kind === "request") {
278378 const detail =
279379 asString(payload?.command) ?? asString(payload?.reason) ?? asString(payload?.prompt);
280380 return [
281381 {
282− type: "approval.requested",
283− eventId: event.id,
284− provider: event.provider,
285− sessionId: event.sessionId,
286− createdAt: event.createdAt,
287− ...(event.threadId ? { threadId: event.threadId } : {}),
288− ...(event.turnId ? { turnId: event.turnId } : {}),
289− ...(event.itemId ? { itemId: event.itemId } : {}),
290− requestId: event.requestId,
291− requestKind: event.requestKind,
292− ...(detail ? { detail } : {}),
382+ ...runtimeEventBase(event),
383+ type: "request.opened",
384+ payload: {
385+ requestType: toRequestTypeFromMethod(event.method),
386+ ...(detail ? { detail } : {}),
387+ ...(event.payload !== undefined ? { args: event.payload } : {}),
388+ },
293389 },
294390 ];
295391 }
296392
297393 if (event.method === "item/requestApproval/decision" && event.requestId) {
298394 const decision = Schema.decodeUnknownSync(ProviderApprovalDecision)(payload?.decision);
395+ const requestType =
396+ event.requestKind !== undefined
397+ ? toRequestTypeFromKind(event.requestKind)
398+ : toRequestTypeFromMethod(event.method);
299399 return [
300400 {
301− type: "approval.resolved",
302− eventId: event.id,
303− provider: event.provider,
304− sessionId: event.sessionId,
305− createdAt: event.createdAt,
306− ...(event.threadId ? { threadId: event.threadId } : {}),
307− ...(event.turnId ? { turnId: event.turnId } : {}),
308− ...(event.itemId ? { itemId: event.itemId } : {}),
309− requestId: event.requestId,
310− ...(event.requestKind ? { requestKind: event.requestKind } : {}),
311− ...(decision ? { decision } : {}),
401+ ...runtimeEventBase(event),
402+ type: "request.resolved",
403+ payload: {
404+ requestType,
405+ ...(decision ? { decision } : {}),
406+ ...(event.payload !== undefined ? { resolution: event.payload } : {}),
407+ },
312408 },
313409 ];
314410 }
315411
316− const messageCompleted = mapMessageCompletedEvent(event);
317− if (messageCompleted) {
318− return [messageCompleted];
412+ if (event.method === "session/connecting") {
413+ return [
414+ {
415+ ...runtimeEventBase(event),
416+ type: "session.state.changed",
417+ payload: {
418+ state: "starting",
419+ ...(event.message ? { reason: event.message } : {}),
420+ },
421+ },
422+ ];
319423 }
320424
321− const tool = mapToolEvent(event);
322− if (tool) {
323− return [tool];
425+ if (event.method === "session/ready") {
426+ return [
427+ {
428+ ...runtimeEventBase(event),
429+ type: "session.state.changed",
430+ payload: {
431+ state: "ready",
432+ ...(event.message ? { reason: event.message } : {}),
433+ },
434+ },
435+ ];
324436 }
325437
326438 if (event.method === "session/started") {
327439 return [
328440 {
441+ ...runtimeEventBase(event),
329442 type: "session.started",
330− eventId: event.id,
331− provider: event.provider,
332− sessionId: event.sessionId,
333− createdAt: event.createdAt,
334− ...(event.threadId ? { threadId: event.threadId } : {}),
335− ...(event.message ? { message: event.message } : {}),
443+ payload: {
444+ ...(event.message ? { message: event.message } : {}),
445+ ...(event.payload !== undefined ? { resume: event.payload } : {}),
446+ },
336447 },
337448 ];
338449 }
@@ -340,33 +451,82 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime · 1 unmodified line
340451 if (event.method === "session/exited" || event.method === "session/closed") {
341452 return [
342453 {
454+ ...runtimeEventBase(event),
343455 type: "session.exited",
344− eventId: event.id,
345− provider: event.provider,
346− sessionId: event.sessionId,
347− createdAt: event.createdAt,
348− ...(event.threadId ? { threadId: event.threadId } : {}),
349− ...(event.message ? { message: event.message } : {}),
456+ payload: {
457+ ...(event.message ? { reason: event.message } : {}),
458+ ...(event.method === "session/closed" ? { exitKind: "graceful" } : {}),
459+ },
350460 },
351461 ];
352462 }
353463
354464 if (event.method === "thread/started") {
355465 const payloadThreadId = asString(asObject(payload?.thread)?.id);
356− const threadId =
466+ const providerThreadId =
357467 event.threadId ??
358468 (payloadThreadId ? ProviderThreadId.makeUnsafe(payloadThreadId) : undefined);
359− if (!threadId) {
469+ if (!providerThreadId) {
360470 return [];
361471 }
362472 return [
363473 {
474+ ...runtimeEventBase(event),
364475 type: "thread.started",
365− eventId: event.id,
366− provider: event.provider,
367− sessionId: event.sessionId,
368− createdAt: event.createdAt,
369− threadId,
476+ payload: {
477+ providerThreadId,
478+ },
479+ },
480+ ];
481+ }
482+
483+ if (
484+ event.method === "thread/status/changed" ||
485+ event.method === "thread/archived" ||
486+ event.method === "thread/unarchived" ||
487+ event.method === "thread/closed" ||
488+ event.method === "thread/compacted"
489+ ) {
490+ return [
491+ {
492+ ...runtimeEventBase(event),
493+ type: "thread.state.changed",
494+ payload: {
495+ state:
496+ event.method === "thread/archived"
497+ ? "archived"
498+ : event.method === "thread/closed"
499+ ? "closed"
500+ : event.method === "thread/compacted"
501+ ? "compacted"
502+ : toThreadState(asObject(payload?.thread)?.state ?? payload?.state),
503+ ...(event.payload !== undefined ? { detail: event.payload } : {}),
504+ },
505+ },
506+ ];
507+ }
508+
509+ if (event.method === "thread/name/updated") {
510+ return [
511+ {
512+ ...runtimeEventBase(event),
513+ type: "thread.metadata.updated",
514+ payload: {
515+ ...(asString(payload?.threadName) ? { name: asString(payload?.threadName) } : {}),
516+ ...(event.payload !== undefined ? { metadata: asObject(event.payload) } : {}),
517+ },
518+ },
519+ ];
520+ }
521+
522+ if (event.method === "thread/tokenUsage/updated") {
523+ return [
524+ {
525+ ...runtimeEventBase(event),
526+ type: "thread.token-usage.updated",
527+ payload: {
528+ usage: event.payload ?? {},
529+ },
370530 },
371531 ];
372532 }
@@ -380,52 +540,360 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime · 7 unmodified lines
380540 }
381541 return [
382542 {
383− type: "turn.started",
384− eventId: event.id,
385− provider: event.provider,
386− sessionId: event.sessionId,
387− createdAt: event.createdAt,
388− ...(event.threadId ? { threadId: event.threadId } : {}),
543+ ...runtimeEventBase(event),
389544 turnId,
545+ type: "turn.started",
546+ payload: {
547+ ...(asString(turn?.model) ? { model: asString(turn?.model) } : {}),
548+ ...(asString(turn?.effort) ? { effort: asString(turn?.effort) } : {}),
549+ },
390550 },
391551 ];
392552 }
393553
394554 if (event.method === "turn/completed") {
555+ const errorMessage = asString(asObject(turn?.error)?.message);
395556 return [
396557 {
558+ ...runtimeEventBase(event),
397559 type: "turn.completed",
398− eventId: event.id,
399− provider: event.provider,
400− sessionId: event.sessionId,
401− createdAt: event.createdAt,
402− ...(event.threadId ? { threadId: event.threadId } : {}),
403− ...(event.turnId ? { turnId: event.turnId } : {}),
404− ...(toTurnStatus(turn?.status) ? { status: toTurnStatus(turn?.status) } : {}),
405− ...(asString(asObject(turn?.error)?.message)
406− ? { errorMessage: asString(asObject(turn?.error)?.message) }
407− : {}),
560+ payload: {
561+ state: toTurnStatus(turn?.status),
562+ ...(asString(turn?.stopReason) ? { stopReason: asString(turn?.stopReason) } : {}),
563+ ...(turn?.usage !== undefined ? { usage: turn.usage } : {}),
564+ ...(asObject(turn?.modelUsage) ? { modelUsage: asObject(turn?.modelUsage) } : {}),
565+ ...(asNumber(turn?.totalCostUsd) !== undefined
566+ ? { totalCostUsd: asNumber(turn?.totalCostUsd) }
567+ : {}),
568+ ...(errorMessage ? { errorMessage } : {}),
569+ },
570+ },
571+ ];
572+ }
573+
574+ if (event.method === "turn/aborted") {
575+ return [
576+ {
577+ ...runtimeEventBase(event),
578+ type: "turn.aborted",
579+ payload: {
580+ reason: event.message ?? "Turn aborted",
581+ },
582+ },
583+ ];
584+ }
585+
586+ if (event.method === "turn/plan/updated") {
587+ const steps = Array.isArray(payload?.plan) ? payload.plan : [];
588+ return [
589+ {
590+ ...runtimeEventBase(event),
591+ type: "turn.plan.updated",
592+ payload: {
593+ ...(asString(payload?.explanation) ? { explanation: asString(payload?.explanation) } : {}),
594+ plan: steps
595+ .map((entry) => asObject(entry))
596+ .filter((entry): entry is Record<string, unknown> => entry !== undefined)
597+ .map((entry) => ({
598+ step: asString(entry.step) ?? "step",
599+ status:
600+ entry.status === "completed" || entry.status === "inProgress"
601+ ? entry.status
602+ : "pending",
603+ })),
604+ },
408605 },
409606 ];
410607 }
411608
412− if (event.method === "item/agentMessage/delta" && event.textDelta && event.textDelta.length > 0) {
609+ if (event.method === "turn/diff/updated") {
413610 return [
414611 {
415− type: "message.delta",
416− eventId: event.id,
417− provider: event.provider,
418− sessionId: event.sessionId,
419− createdAt: event.createdAt,
420− ...(event.threadId ? { threadId: event.threadId } : {}),
421− ...(event.turnId ? { turnId: event.turnId } : {}),
422− ...(event.itemId ? { itemId: event.itemId } : {}),
423− delta: event.textDelta,
612+ ...runtimeEventBase(event),
613+ type: "turn.diff.updated",
614+ payload: {
615+ unifiedDiff:
616+ asString(payload?.unifiedDiff) ??
617+ asString(payload?.diff) ??
618+ asString(payload?.patch) ??
619+ "",
620+ },
424621 },
425622 ];
426623 }
427624
428− return [];
625+ if (event.method === "item/started") {
626+ const started = mapItemLifecycle(event, "item.started");
627+ return started ? [started] : [];
628+ }
629+
630+ if (event.method === "item/completed") {
631+ const completed = mapItemLifecycle(event, "item.completed");
632+ return completed ? [completed] : [];
633+ }
634+
635+ if (
636+ event.method === "item/reasoning/summaryPartAdded" ||
637+ event.method === "item/commandExecution/terminalInteraction"
638+ ) {
639+ const updated = mapItemLifecycle(event, "item.updated");
640+ return updated ? [updated] : [];
641+ }
642+
643+ if (
644+ event.method === "item/agentMessage/delta" ||
645+ event.method === "item/plan/delta" ||
646+ event.method === "item/commandExecution/outputDelta" ||
647+ event.method === "item/fileChange/outputDelta" ||
648+ event.method === "item/reasoning/summaryTextDelta" ||
649+ event.method === "item/reasoning/textDelta"
650+ ) {
651+ const delta =
652+ event.textDelta ??
653+ asString(payload?.delta) ??
654+ asString(payload?.text) ??
655+ asString(asObject(payload?.content)?.text);
656+ if (!delta || delta.length === 0) {
657+ return [];
658+ }
659+ return [
660+ {
661+ ...runtimeEventBase(event),
662+ type: "content.delta",
663+ payload: {
664+ streamKind: contentStreamKindFromMethod(event.method),
665+ delta,
666+ ...(typeof payload?.contentIndex === "number" ? { contentIndex: payload.contentIndex } : {}),
667+ ...(typeof payload?.summaryIndex === "number" ? { summaryIndex: payload.summaryIndex } : {}),
668+ },
669+ },
670+ ];
671+ }
672+
673+ if (event.method === "item/mcpToolCall/progress") {
674+ return [
675+ {
676+ ...runtimeEventBase(event),
677+ type: "tool.progress",
678+ payload: {
679+ ...(asString(payload?.toolUseId) ? { toolUseId: asString(payload?.toolUseId) } : {}),
680+ ...(asString(payload?.toolName) ? { toolName: asString(payload?.toolName) } : {}),
681+ ...(asString(payload?.summary) ? { summary: asString(payload?.summary) } : {}),
682+ ...(asNumber(payload?.elapsedSeconds) !== undefined
683+ ? { elapsedSeconds: asNumber(payload?.elapsedSeconds) }
684+ : {}),
685+ },
686+ },
687+ ];
688+ }
689+
690+ if (event.method === "serverRequest/resolved") {
691+ return [
692+ {
693+ ...runtimeEventBase(event),
694+ type: "request.resolved",
695+ payload: {
696+ requestType: "unknown",
697+ ...(event.payload !== undefined ? { resolution: event.payload } : {}),
698+ },
699+ },
700+ ];
701+ }
702+
703+ if (event.method === "model/rerouted") {
704+ return [
705+ {
706+ ...runtimeEventBase(event),
707+ type: "model.rerouted",
708+ payload: {
709+ fromModel: asString(payload?.fromModel) ?? "unknown",
710+ toModel: asString(payload?.toModel) ?? "unknown",
711+ reason: asString(payload?.reason) ?? "unknown",
712+ },
713+ },
714+ ];
715+ }
716+
717+ if (event.method === "deprecationNotice") {
718+ return [
719+ {
720+ ...runtimeEventBase(event),
721+ type: "deprecation.notice",
722+ payload: {
723+ summary: asString(payload?.summary) ?? "Deprecation notice",
724+ ...(asString(payload?.details) ? { details: asString(payload?.details) } : {}),
725+ },
726+ },
727+ ];
728+ }
729+
730+ if (event.method === "configWarning") {
731+ return [
732+ {
733+ ...runtimeEventBase(event),
734+ type: "config.warning",
735+ payload: {
736+ summary: asString(payload?.summary) ?? "Configuration warning",
737+ ...(asString(payload?.details) ? { details: asString(payload?.details) } : {}),
738+ ...(asString(payload?.path) ? { path: asString(payload?.path) } : {}),
739+ ...(payload?.range !== undefined ? { range: payload.range } : {}),
740+ },
741+ },
742+ ];
743+ }
744+
745+ if (event.method === "account/updated") {
746+ return [
747+ {
748+ ...runtimeEventBase(event),
749+ type: "account.updated",
750+ payload: {
751+ account: event.payload ?? {},
752+ },
753+ },
754+ ];
755+ }
756+
757+ if (event.method === "account/rateLimits/updated") {
758+ return [
759+ {
760+ ...runtimeEventBase(event),
761+ type: "account.rate-limits.updated",
762+ payload: {
763+ rateLimits: event.payload ?? {},
764+ },
765+ },
766+ ];
767+ }
768+
769+ if (event.method === "mcpServer/oauthLogin/completed") {
770+ return [
771+ {
772+ ...runtimeEventBase(event),
773+ type: "mcp.oauth.completed",
774+ payload: {
775+ success: payload?.success === true,
776+ ...(asString(payload?.name) ? { name: asString(payload?.name) } : {}),
777+ ...(asString(payload?.error) ? { error: asString(payload?.error) } : {}),
778+ },
779+ },
780+ ];
781+ }
782+
783+ if (event.method === "thread/realtime/started") {
784+ const realtimeSessionId = asString(payload?.realtimeSessionId);
785+ return [
786+ {
787+ ...runtimeEventBase(event),
788+ type: "thread.realtime.started",
789+ payload: {
790+ realtimeSessionId,
791+ },
792+ },
793+ ];
794+ }
795+
796+ if (event.method === "thread/realtime/itemAdded") {
797+ return [
798+ {
799+ ...runtimeEventBase(event),
800+ type: "thread.realtime.item-added",
801+ payload: {
802+ item: event.payload ?? {},
803+ },
804+ },
805+ ];
806+ }
807+
808+ if (event.method === "thread/realtime/outputAudio/delta") {
809+ return [
810+ {
811+ ...runtimeEventBase(event),
812+ type: "thread.realtime.audio.delta",
813+ payload: {
814+ audio: event.payload ?? {},
815+ },
816+ },
817+ ];
818+ }
819+
820+ if (event.method === "thread/realtime/error") {
821+ const message = asString(payload?.message) ?? event.message ?? "Realtime error";
822+ return [
823+ {
824+ ...runtimeEventBase(event),
825+ type: "thread.realtime.error",
826+ payload: {
827+ message,
828+ },
829+ },
830+ ];
831+ }
832+
833+ if (event.method === "thread/realtime/closed") {
834+ return [
835+ {
836+ ...runtimeEventBase(event),
837+ type: "thread.realtime.closed",
838+ payload: {
839+ reason: event.message,
840+ },
841+ },
842+ ];
843+ }
844+
845+ if (event.method === "error") {
846+ const message =
847+ asString(asObject(payload?.error)?.message) ?? event.message ?? "Provider runtime error";
848+ return [
849+ {
850+ ...runtimeEventBase(event),
851+ type: "runtime.error",
852+ payload: {
853+ message,
854+ class: "provider_error",
855+ ...(event.payload !== undefined ? { detail: event.payload } : {}),
856+ },
857+ },
858+ ];
859+ }
860+
861+ if (event.method === "windows/worldWritableWarning") {
862+ return [
863+ {
864+ ...runtimeEventBase(event),
865+ type: "runtime.warning",
866+ payload: {
867+ message: event.message ?? "Windows world-writable warning",
868+ ...(event.payload !== undefined ? { detail: event.payload } : {}),
869+ },
870+ },
871+ ];
872+ }
873+
874+ if (event.method === "windowsSandbox/setupCompleted") {
875+ return [
876+ {
877+ ...runtimeEventBase(event),
878+ type: "runtime.warning",
879+ payload: {
880+ message: event.message ?? "Windows sandbox setup completed",
881+ ...(event.payload !== undefined ? { detail: event.payload } : {}),
882+ },
883+ },
884+ ];
885+ }
886+
887+ return [
888+ {
889+ ...runtimeEventBase(event),
890+ type: "runtime.warning",
891+ payload: {
892+ message: `Unhandled Codex provider event method: ${event.method}`,
893+ ...(event.payload !== undefined ? { detail: event.payload } : {}),
894+ },
895+ },
896+ ];
429897 }
430898
431899 const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>
apps/server/src/provider/Layers/CursorAdapter.test.ts+350−0
@@ -0,0 +1,350 @@
1+import { EventEmitter } from "node:events";
2+import { PassThrough } from "node:stream";
3+import readline from "node:readline";
4+
5+import { ApprovalRequestId } from "@t3tools/contracts";
6+import { assert, describe, it } from "@effect/vitest";
7+import { Effect, Fiber, Stream } from "effect";
8+
9+import { ProviderAdapterValidationError } from "../Errors.ts";
10+import { CursorAdapter } from "../Services/CursorAdapter.ts";
11+import { makeCursorAdapterLive } from "./CursorAdapter.ts";
12+
13+class FakeCursorAcpProcess extends EventEmitter {
14+ readonly stdin = new PassThrough();
15+ readonly stdout = new PassThrough();
16+ readonly stderr = new PassThrough();
17+ killed = false;
18+
19+ private readonly input = readline.createInterface({ input: this.stdin });
20+ private permissionRequestId = 700;
21+ lastPermissionSelection: string | undefined;
22+
23+ constructor() {
24+ super();
25+ this.input.on("line", (line) => {
26+ const message = JSON.parse(line) as Record<string, unknown>;
27+ if (typeof message.method === "string") {
28+ this.handleRequest(message);
29+ return;
30+ }
31+
32+ if (message.id === this.permissionRequestId) {
33+ const optionId =
34+ (message.result as { outcome?: { optionId?: unknown } } | undefined)?.outcome?.optionId;
35+ if (typeof optionId === "string") {
36+ this.lastPermissionSelection = optionId;
37+ }
38+ }
39+ });
40+ }
41+
42+ kill(): boolean {
43+ if (this.killed) {
44+ return true;
45+ }
46+ this.killed = true;
47+ this.emit("exit", 0, null);
48+ return true;
49+ }
50+
51+ emitPermissionRequest(): void {
52+ this.emitServerMessage({
53+ jsonrpc: "2.0",
54+ id: this.permissionRequestId,
55+ method: "session/request_permission",
56+ params: {
57+ sessionId: "acp-session-1",
58+ toolCall: {
59+ toolCallId: "tool-perm-1",
60+ kind: "execute",
61+ title: "`pwd`",
62+ },
63+ options: [
64+ { optionId: "allow-once", kind: "allow_once" },
65+ { optionId: "allow-always", kind: "allow_always" },
66+ { optionId: "reject-once", kind: "reject_once" },
67+ ],
68+ },
69+ });
70+ }
71+
72+ private handleRequest(message: Record<string, unknown>): void {
73+ const method = message.method;
74+ const id = message.id;
75+ if (typeof method !== "string" || (typeof id !== "string" && typeof id !== "number")) {
76+ return;
77+ }
78+
79+ switch (method) {
80+ case "initialize":
81+ this.emitServerMessage({
82+ jsonrpc: "2.0",
83+ id,
84+ result: {
85+ protocolVersion: 1,
86+ agentCapabilities: {
87+ loadSession: true,
88+ },
89+ authMethods: [{ id: "cursor_login" }],
90+ },
91+ });
92+ return;
93+ case "authenticate":
94+ this.emitServerMessage({
95+ jsonrpc: "2.0",
96+ id,
97+ result: {},
98+ });
99+ return;
100+ case "session/new":
101+ this.emitServerMessage({
102+ jsonrpc: "2.0",
103+ id,
104+ result: {
105+ sessionId: "acp-session-1",
106+ modes: {
107+ currentModeId: "agent",
108+ availableModes: [{ id: "agent" }],
109+ },
110+ },
111+ });
112+ return;
113+ case "session/load":
114+ this.emitServerMessage({
115+ jsonrpc: "2.0",
116+ id,
117+ result: {},
118+ });
119+ return;
120+ case "session/prompt": {
121+ this.emitServerMessage({
122+ jsonrpc: "2.0",
123+ method: "session/update",
124+ params: {
125+ sessionId: "acp-session-1",
126+ update: {
127+ sessionUpdate: "agent_thought_chunk",
128+ content: {
129+ type: "text",
130+ text: "thinking",
131+ },
132+ },
133+ },
134+ });
135+
136+ this.emitServerMessage({
137+ jsonrpc: "2.0",
138+ method: "session/update",
139+ params: {
140+ sessionId: "acp-session-1",
141+ update: {
142+ sessionUpdate: "agent_message_chunk",
143+ content: {
144+ type: "text",
145+ text: "hello",
146+ },
147+ },
148+ },
149+ });
150+
151+ this.emitServerMessage({
152+ jsonrpc: "2.0",
153+ method: "session/update",
154+ params: {
155+ sessionId: "acp-session-1",
156+ update: {
157+ sessionUpdate: "tool_call",
158+ toolCallId: "tool-1",
159+ kind: "execute",
160+ title: "`pwd`",
161+ rawInput: { command: "pwd" },
162+ },
163+ },
164+ });
165+
166+ this.emitServerMessage({
167+ jsonrpc: "2.0",
168+ method: "session/update",
169+ params: {
170+ sessionId: "acp-session-1",
171+ update: {
172+ sessionUpdate: "tool_call_update",
173+ toolCallId: "tool-1",
174+ status: "completed",
175+ rawOutput: {
176+ exitCode: 0,
177+ stdout: "/tmp/project",
178+ stderr: "",
179+ },
180+ },
181+ },
182+ });
183+
184+ this.emitServerMessage({
185+ jsonrpc: "2.0",
186+ id,
187+ result: {
188+ stopReason: "end_turn",
189+ },
190+ });
191+ return;
192+ }
193+ case "session/cancel":
194+ this.emitServerMessage({
195+ jsonrpc: "2.0",
196+ id,
197+ error: {
198+ code: -32601,
199+ message: "Method not found",
200+ },
201+ });
202+ return;
203+ default:
204+ this.emitServerMessage({
205+ jsonrpc: "2.0",
206+ id,
207+ error: {
208+ code: -32601,
209+ message: `Unhandled method: ${method}`,
210+ },
211+ });
212+ }
213+ }
214+
215+ private emitServerMessage(message: unknown): void {
216+ this.stdout.write(`${JSON.stringify(message)}\n`);
217+ }
218+}
219+
220+describe("CursorAdapterLive", () => {
221+ it.effect("returns validation error for non-cursor provider on startSession", () => {
222+ const fake = new FakeCursorAcpProcess();
223+ const layer = makeCursorAdapterLive({
224+ createProcess: () => fake as never,
225+ });
226+
227+ return Effect.gen(function* () {
228+ const adapter = yield* CursorAdapter;
229+ const result = yield* adapter.startSession({ provider: "codex" }).pipe(Effect.result);
230+
231+ assert.equal(result._tag, "Failure");
232+ if (result._tag !== "Failure") {
233+ return;
234+ }
235+ assert.deepEqual(
236+ result.failure,
237+ new ProviderAdapterValidationError({
238+ provider: "cursor",
239+ operation: "startSession",
240+ issue: "Expected provider 'cursor' but received 'codex'.",
241+ }),
242+ );
243+ }).pipe(Effect.provide(layer));
244+ });
245+
246+ it.effect("maps ACP prompt/update events into canonical runtime events", () => {
247+ const fake = new FakeCursorAcpProcess();
248+ const layer = makeCursorAdapterLive({
249+ createProcess: () => fake as never,
250+ });
251+
252+ return Effect.gen(function* () {
253+ const adapter = yield* CursorAdapter;
254+
255+ const eventsFiber = yield* Stream.take(adapter.streamEvents, 11).pipe(
256+ Stream.runCollect,
257+ Effect.forkChild,
258+ );
259+
260+ const session = yield* adapter.startSession({
261+ provider: "cursor",
262+ cwd: "/tmp/project",
263+ });
264+
265+ const turn = yield* adapter.sendTurn({
266+ sessionId: session.sessionId,
267+ input: "hello",
268+ attachments: [],
269+ });
270+
271+ const events = Array.from(yield* Fiber.join(eventsFiber));
272+ assert.deepEqual(
273+ events.map((event) => event.type),
274+ [
275+ "session.configured",
276+ "session.started",
277+ "thread.started",
278+ "session.state.changed",
279+ "turn.started",
280+ "content.delta",
281+ "content.delta",
282+ "item.started",
283+ "item.completed",
284+ "item.completed",
285+ "turn.completed",
286+ ],
287+ );
288+
289+ const turnStarted = events[4];
290+ assert.equal(turnStarted?.type, "turn.started");
291+ if (turnStarted?.type === "turn.started") {
292+ assert.equal(turnStarted.turnId, turn.turnId);
293+ }
294+
295+ const completion = events[10];
296+ assert.equal(completion?.type, "turn.completed");
297+ if (completion?.type === "turn.completed") {
298+ assert.equal(completion.payload.state, "completed");
299+ }
300+ }).pipe(Effect.provide(layer));
301+ });
302+
303+ it.effect("bridges permission requests to request.opened/request.resolved", () => {
304+ const fake = new FakeCursorAcpProcess();
305+ const layer = makeCursorAdapterLive({
306+ createProcess: () => fake as never,
307+ });
308+
309+ return Effect.gen(function* () {
310+ const adapter = yield* CursorAdapter;
311+
312+ const session = yield* adapter.startSession({
313+ provider: "cursor",
314+ });
315+
316+ // consume startup events
317+ yield* Stream.take(adapter.streamEvents, 4).pipe(Stream.runDrain);
318+
319+ fake.emitPermissionRequest();
320+
321+ const opened = yield* Stream.runHead(adapter.streamEvents);
322+ assert.equal(opened._tag, "Some");
323+ if (opened._tag !== "Some") {
324+ return;
325+ }
326+ assert.equal(opened.value.type, "request.opened");
327+ if (opened.value.type !== "request.opened") {
328+ return;
329+ }
330+
331+ yield* adapter.respondToRequest(
332+ session.sessionId,
333+ ApprovalRequestId.makeUnsafe(opened.value.requestId),
334+ "acceptForSession",
335+ );
336+
337+ const resolved = yield* Stream.runHead(adapter.streamEvents);
338+ assert.equal(resolved._tag, "Some");
339+ if (resolved._tag !== "Some") {
340+ return;
341+ }
342+ assert.equal(resolved.value.type, "request.resolved");
343+ if (resolved.value.type !== "request.resolved") {
344+ return;
345+ }
346+ assert.equal(resolved.value.payload.decision, "acceptForSession");
347+ assert.equal(fake.lastPermissionSelection, "allow-always");
348+ }).pipe(Effect.provide(layer));
349+ });
350+});
apps/server/src/provider/Layers/CursorAdapter.ts+1378−0
@@ -0,0 +1,1378 @@
1+/**
2+ * CursorAdapterLive - Scoped live implementation for the Cursor ACP provider adapter.
3+ *
4+ * Spawns `agent acp` over stdio, manages JSON-RPC session lifecycle, and maps
5+ * ACP notifications/requests into canonical provider runtime events.
6+ *
7+ * @module CursorAdapterLive
8+ */
9+import { randomUUID } from "node:crypto";
10+import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
11+import readline from "node:readline";
12+
13+import {
14+ ApprovalRequestId,
15+ EventId,
16+ type CanonicalItemType,
17+ type CanonicalRequestType,
18+ ProviderSessionId,
19+ type ProviderRuntimeEvent,
20+ type ProviderSession,
21+ ProviderThreadId,
22+ ProviderTurnId,
23+ type ProviderTurnStartResult,
24+} from "@t3tools/contracts";
25+import { DateTime, Effect, Layer, Queue, Random, Schema, Stream } from "effect";
26+
27+import {
28+ ProviderAdapterProcessError,
29+ ProviderAdapterRequestError,
30+ ProviderAdapterSessionClosedError,
31+ ProviderAdapterSessionNotFoundError,
32+ ProviderAdapterValidationError,
33+ type ProviderAdapterError,
34+} from "../Errors.ts";
35+import {
36+ CursorAdapter,
37+ type CursorAdapterShape,
38+ CursorAcpInitializeResult,
39+ CursorAcpPermissionRequest,
40+ CursorAcpSessionNewResult,
41+ CursorAcpSessionPromptResult,
42+ CursorAcpSessionUpdateNotification,
43+} from "../Services/CursorAdapter.ts";
44+import { makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
45+
46+const PROVIDER = "cursor" as const;
47+const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
48+
49+interface CursorResumeState {
50+ readonly acpSessionId?: string;
51+}
52+
53+interface PendingRequest {
54+ readonly method: string;
55+ readonly timeout: ReturnType<typeof setTimeout>;
56+ readonly resolve: (value: unknown) => void;
57+ readonly reject: (error: Error) => void;
58+}
59+
60+interface PendingPermission {
61+ readonly jsonRpcId: string | number;
62+ readonly requestType: CanonicalRequestType;
63+ readonly options: ReadonlyArray<{ optionId: string }>;
64+}
65+
66+interface CursorTurnState {
67+ readonly turnId: ReturnType<typeof ProviderTurnId.makeUnsafe>;
68+ readonly assistantItemId: string;
69+ readonly startedToolCalls: Set<string>;
70+ readonly items: Array<unknown>;
71+}
72+
73+interface CursorSessionContext {
74+ session: ProviderSession;
75+ readonly child: ChildProcessWithoutNullStreams;
76+ readonly output: readline.Interface;
77+ readonly pending: Map<string, PendingRequest>;
78+ readonly pendingPermissions: Map<ApprovalRequestId, PendingPermission>;
79+ readonly turns: Array<{
80+ id: ReturnType<typeof ProviderTurnId.makeUnsafe>;
81+ items: Array<unknown>;
82+ }>;
83+ turnState: CursorTurnState | undefined;
84+ acpSessionId: string;
85+ nextRpcId: number;
86+ stopping: boolean;
87+}
88+
89+export interface CursorAdapterLiveOptions {
90+ readonly createProcess?: (input: {
91+ readonly binaryPath: string;
92+ readonly cwd: string;
93+ readonly env: NodeJS.ProcessEnv;
94+ }) => ChildProcessWithoutNullStreams;
95+ readonly nativeEventLogPath?: string;
96+}
97+
98+function toMessage(cause: unknown, fallback: string): string {
99+ if (cause instanceof Error && cause.message.length > 0) {
100+ return cause.message;
101+ }
102+ return fallback;
103+}
104+
105+function toSessionError(
106+ sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>,
107+ cause: unknown,
108+): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined {
109+ const normalized = toMessage(cause, "").toLowerCase();
110+ if (normalized.includes("unknown session") || normalized.includes("not found")) {
111+ return new ProviderAdapterSessionNotFoundError({
112+ provider: PROVIDER,
113+ sessionId,
114+ cause,
115+ });
116+ }
117+ if (normalized.includes("closed")) {
118+ return new ProviderAdapterSessionClosedError({
119+ provider: PROVIDER,
120+ sessionId,
121+ cause,
122+ });
123+ }
124+ return undefined;
125+}
126+
127+function toRequestError(
128+ sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>,
129+ method: string,
130+ cause: unknown,
131+): ProviderAdapterError {
132+ const sessionError = toSessionError(sessionId, cause);
133+ if (sessionError) {
134+ return sessionError;
135+ }
136+ return new ProviderAdapterRequestError({
137+ provider: PROVIDER,
138+ method,
139+ detail: toMessage(cause, `${method} failed`),
140+ cause,
141+ });
142+}
143+
144+function asObject(value: unknown): Record<string, unknown> | undefined {
145+ if (!value || typeof value !== "object") {
146+ return undefined;
147+ }
148+ return value as Record<string, unknown>;
149+}
150+
151+function asString(value: unknown): string | undefined {
152+ return typeof value === "string" ? value : undefined;
153+}
154+
155+function normalizeToolItemType(kind: unknown, title: unknown): CanonicalItemType {
156+ const normalizedKind = asString(kind)?.toLowerCase();
157+ const normalizedTitle = asString(title)?.toLowerCase();
158+
159+ if (normalizedKind === "execute") {
160+ return "command_execution";
161+ }
162+ if (normalizedKind === "edit" || normalizedKind === "write") {
163+ return "file_change";
164+ }
165+ if (normalizedKind === "mcp") {
166+ return "mcp_tool_call";
167+ }
168+ if (normalizedTitle?.includes("terminal")) {
169+ return "command_execution";
170+ }
171+ return "dynamic_tool_call";
172+}
173+
174+function normalizeRequestType(toolCall: unknown): CanonicalRequestType {
175+ const record = asObject(toolCall);
176+ const kind = asString(record?.kind)?.toLowerCase();
177+ if (kind === "execute") {
178+ return "command_execution_approval";
179+ }
180+ if (kind === "edit" || kind === "write") {
181+ return "file_change_approval";
182+ }
183+ return "unknown";
184+}
185+
186+function titleForItemType(itemType: CanonicalItemType): string {
187+ switch (itemType) {
188+ case "command_execution":
189+ return "Command run";
190+ case "file_change":
191+ return "File change";
192+ case "mcp_tool_call":
193+ return "MCP tool call";
194+ case "dynamic_tool_call":
195+ return "Tool call";
196+ default:
197+ return "Item";
198+ }
199+}
200+
201+function summarizeToolOutput(rawOutput: unknown): string | undefined {
202+ const output = asObject(rawOutput);
203+ if (!output) return undefined;
204+
205+ const stdout = asString(output.stdout);
206+ if (stdout && stdout.trim().length > 0) {
207+ return stdout.trim().slice(0, 400);
208+ }
209+
210+ const summary = JSON.stringify(output);
211+ return summary.length > 400 ? `${summary.slice(0, 397)}...` : summary;
212+}
213+
214+function mapStopReasonToTurnState(stopReason: string | undefined):
215+ | "completed"
216+ | "failed"
217+ | "interrupted"
218+ | "cancelled" {
219+ if (stopReason === "cancelled") return "cancelled";
220+ if (stopReason === "interrupted") return "interrupted";
221+ return "completed";
222+}
223+
224+function readCursorResumeState(resumeCursor: unknown): CursorResumeState | undefined {
225+ if (!resumeCursor || typeof resumeCursor !== "object") {
226+ return undefined;
227+ }
228+
229+ const cursor = resumeCursor as {
230+ acpSessionId?: unknown;
231+ sessionId?: unknown;
232+ };
233+
234+ const acpSessionId =
235+ typeof cursor.acpSessionId === "string"
236+ ? cursor.acpSessionId
237+ : typeof cursor.sessionId === "string"
238+ ? cursor.sessionId
239+ : undefined;
240+
241+ if (!acpSessionId) {
242+ return {};
243+ }
244+ return { acpSessionId };
245+}
246+
247+function writeCursorMessage(context: CursorSessionContext, message: unknown): void {
248+ if (!context.child.stdin.writable) {
249+ throw new Error("Cannot write to Cursor ACP stdin.");
250+ }
251+ context.child.stdin.write(`${JSON.stringify(message)}\n`);
252+}
253+
254+function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
255+ return Effect.gen(function* () {
256+ const nativeEventLogger =
257+ options?.nativeEventLogPath !== undefined
258+ ? makeEventNdjsonLogger(options.nativeEventLogPath)
259+ : undefined;
260+
261+ const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
262+ const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id));
263+ const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso });
264+
265+ const sessions = new Map<ProviderSessionId, CursorSessionContext>();
266+ const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();
267+
268+ const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>
269+ Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid);
270+
271+ const spawnCursorAcp = (input: {
272+ readonly binaryPath: string;
273+ readonly cwd: string;
274+ readonly env: NodeJS.ProcessEnv;
275+ }): ChildProcessWithoutNullStreams => {
276+ if (options?.createProcess) {
277+ return options.createProcess(input);
278+ }
279+ return spawn(input.binaryPath, ["acp"], {
280+ cwd: input.cwd,
281+ env: input.env,
282+ stdio: ["pipe", "pipe", "pipe"],
283+ });
284+ };
285+
286+ const sendRequest = (
287+ context: CursorSessionContext,
288+ method: string,
289+ params: unknown,
290+ timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
291+ ): Promise<unknown> => {
292+ const id = context.nextRpcId;
293+ context.nextRpcId += 1;
294+
295+ return new Promise((resolve, reject) => {
296+ const timeout = setTimeout(() => {
297+ context.pending.delete(String(id));
298+ reject(new Error(`Timed out waiting for ${method}.`));
299+ }, timeoutMs);
300+
301+ context.pending.set(String(id), {
302+ method,
303+ timeout,
304+ resolve,
305+ reject,
306+ });
307+
308+ writeCursorMessage(context, {
309+ jsonrpc: "2.0",
310+ id,
311+ method,
312+ params,
313+ });
314+ });
315+ };
316+
317+ const resolvePendingRequest = (context: CursorSessionContext, message: Record<string, unknown>) => {
318+ const id = message.id;
319+ if (typeof id !== "string" && typeof id !== "number") {
320+ return;
321+ }
322+ const pending = context.pending.get(String(id));
323+ if (!pending) {
324+ return;
325+ }
326+
327+ clearTimeout(pending.timeout);
328+ context.pending.delete(String(id));
329+
330+ const error = asObject(message.error);
331+ if (error) {
332+ pending.reject(new Error(`${pending.method} failed: ${JSON.stringify(error)}`));
333+ return;
334+ }
335+
336+ pending.resolve(message.result);
337+ };
338+
339+ const decodePermissionRequest = Schema.decodeUnknownSync(CursorAcpPermissionRequest);
340+ const decodeSessionUpdateNotification = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification);
341+
342+ const emitRuntimeWarning = (
343+ context: CursorSessionContext,
344+ message: string,
345+ detail?: unknown,
346+ ): Effect.Effect<void> =>
347+ Effect.gen(function* () {
348+ const stamp = yield* makeEventStamp();
349+ yield* offerRuntimeEvent({
350+ type: "runtime.warning",
351+ eventId: stamp.eventId,
352+ provider: PROVIDER,
353+ sessionId: context.session.sessionId,
354+ createdAt: stamp.createdAt,
355+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
356+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
357+ payload: {
358+ message,
359+ ...(detail !== undefined ? { detail } : {}),
360+ },
361+ providerRefs: {
362+ providerSessionId: context.session.sessionId,
363+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
364+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
365+ },
366+ });
367+ });
368+
369+ const completeTurn = (
370+ context: CursorSessionContext,
371+ state: "completed" | "failed" | "interrupted" | "cancelled",
372+ errorMessage?: string,
373+ stopReason?: string,
374+ ): Effect.Effect<void> =>
375+ Effect.gen(function* () {
376+ const turnState = context.turnState;
377+ if (!turnState) {
378+ return;
379+ }
380+
381+ const itemStamp = yield* makeEventStamp();
382+ yield* offerRuntimeEvent({
383+ type: "item.completed",
384+ eventId: itemStamp.eventId,
385+ provider: PROVIDER,
386+ sessionId: context.session.sessionId,
387+ createdAt: itemStamp.createdAt,
388+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
389+ turnId: turnState.turnId,
390+ itemId: turnState.assistantItemId,
391+ payload: {
392+ itemType: "assistant_message",
393+ status: "completed",
394+ title: "Assistant message",
395+ },
396+ providerRefs: {
397+ providerSessionId: context.session.sessionId,
398+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
399+ providerTurnId: turnState.turnId,
400+ providerItemId: turnState.assistantItemId,
401+ },
402+ });
403+
404+ context.turns.push({
405+ id: turnState.turnId,
406+ items: [...turnState.items],
407+ });
408+
409+ const stamp = yield* makeEventStamp();
410+ yield* offerRuntimeEvent({
411+ type: "turn.completed",
412+ eventId: stamp.eventId,
413+ provider: PROVIDER,
414+ sessionId: context.session.sessionId,
415+ createdAt: stamp.createdAt,
416+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
417+ turnId: turnState.turnId,
418+ payload: {
419+ state,
420+ ...(stopReason ? { stopReason } : {}),
421+ ...(errorMessage ? { errorMessage } : {}),
422+ },
423+ providerRefs: {
424+ providerSessionId: context.session.sessionId,
425+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
426+ providerTurnId: turnState.turnId,
427+ },
428+ });
429+
430+ context.turnState = undefined;
431+ context.session = {
432+ ...context.session,
433+ status: state === "failed" ? "error" : "ready",
434+ activeTurnId: undefined,
435+ ...(errorMessage ? { lastError: errorMessage } : {}),
436+ updatedAt: yield* nowIso,
437+ };
438+ });
439+
440+ const handlePermissionRequest = (
441+ context: CursorSessionContext,
442+ request: unknown,
443+ ): Effect.Effect<void> => {
444+ let decoded: ReturnType<typeof decodePermissionRequest>;
445+ try {
446+ decoded = decodePermissionRequest(request);
447+ } catch (error) {
448+ return emitRuntimeWarning(
449+ context,
450+ "Failed to decode Cursor ACP permission request.",
451+ error,
452+ );
453+ }
454+
455+ return Effect.gen(function* () {
456+ const requestId = ApprovalRequestId.makeUnsafe(randomUUID());
457+ const requestType = normalizeRequestType(decoded.params.toolCall);
458+
459+ context.pendingPermissions.set(requestId, {
460+ jsonRpcId: decoded.id,
461+ requestType,
462+ options: decoded.params.options.map((entry) => ({ optionId: entry.optionId })),
463+ });
464+
465+ const detail = asString(asObject(decoded.params.toolCall)?.title);
466+ const stamp = yield* makeEventStamp();
467+ yield* offerRuntimeEvent({
468+ type: "request.opened",
469+ eventId: stamp.eventId,
470+ provider: PROVIDER,
471+ sessionId: context.session.sessionId,
472+ createdAt: stamp.createdAt,
473+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
474+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
475+ requestId,
476+ payload: {
477+ requestType,
478+ ...(detail ? { detail } : {}),
479+ args: decoded.params,
480+ },
481+ providerRefs: {
482+ providerSessionId: context.session.sessionId,
483+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
484+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
485+ providerRequestId: String(decoded.id),
486+ },
487+ raw: {
488+ source: "cursor.acp.request",
489+ method: decoded.method,
490+ payload: decoded,
491+ },
492+ });
493+ });
494+ };
495+
496+ const handleSessionUpdateNotification = (
497+ context: CursorSessionContext,
498+ notification: unknown,
499+ ): Effect.Effect<void> => {
500+ let decoded: ReturnType<typeof decodeSessionUpdateNotification>;
501+ try {
502+ decoded = decodeSessionUpdateNotification(notification);
503+ } catch (error) {
504+ return emitRuntimeWarning(
505+ context,
506+ "Failed to decode Cursor ACP session/update notification.",
507+ error,
508+ );
509+ }
510+
511+ return Effect.gen(function* () {
512+ const update = decoded.params.update;
513+
514+ const base = {
515+ provider: PROVIDER,
516+ sessionId: context.session.sessionId,
517+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
518+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
519+ providerRefs: {
520+ providerSessionId: context.session.sessionId,
521+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
522+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
523+ },
524+ raw: {
525+ source: "cursor.acp.notification" as const,
526+ method: decoded.method,
527+ messageType: update.sessionUpdate,
528+ payload: decoded,
529+ },
530+ };
531+
532+ switch (update.sessionUpdate) {
533+ case "available_commands_update": {
534+ const stamp = yield* makeEventStamp();
535+ yield* offerRuntimeEvent({
536+ ...base,
537+ type: "session.configured",
538+ eventId: stamp.eventId,
539+ createdAt: stamp.createdAt,
540+ payload: {
541+ config: {
542+ availableCommands: update.availableCommands,
543+ },
544+ },
545+ });
546+ return;
547+ }
548+
549+ case "agent_thought_chunk": {
550+ if (!context.turnState) return;
551+ if (update.content.text.length === 0) return;
552+ const stamp = yield* makeEventStamp();
553+ yield* offerRuntimeEvent({
554+ ...base,
555+ type: "content.delta",
556+ eventId: stamp.eventId,
557+ createdAt: stamp.createdAt,
558+ turnId: context.turnState.turnId,
559+ itemId: context.turnState.assistantItemId,
560+ payload: {
561+ streamKind: "reasoning_text",
562+ delta: update.content.text,
563+ },
564+ });
565+ return;
566+ }
567+
568+ case "agent_message_chunk": {
569+ if (!context.turnState) return;
570+ if (update.content.text.length === 0) return;
571+ const stamp = yield* makeEventStamp();
572+ yield* offerRuntimeEvent({
573+ ...base,
574+ type: "content.delta",
575+ eventId: stamp.eventId,
576+ createdAt: stamp.createdAt,
577+ turnId: context.turnState.turnId,
578+ itemId: context.turnState.assistantItemId,
579+ payload: {
580+ streamKind: "assistant_text",
581+ delta: update.content.text,
582+ },
583+ });
584+ return;
585+ }
586+
587+ case "tool_call": {
588+ if (!context.turnState) return;
589+ const seen = context.turnState.startedToolCalls.has(update.toolCallId);
590+ const itemType = normalizeToolItemType(update.kind, update.title);
591+ const title = update.title ?? titleForItemType(itemType);
592+ const detail = asString(asObject(update.rawInput)?.command);
593+
594+ if (!seen) {
595+ context.turnState.startedToolCalls.add(update.toolCallId);
596+ const stamp = yield* makeEventStamp();
597+ yield* offerRuntimeEvent({
598+ ...base,
599+ type: "item.started",
600+ eventId: stamp.eventId,
601+ createdAt: stamp.createdAt,
602+ turnId: context.turnState.turnId,
603+ itemId: update.toolCallId,
604+ payload: {
605+ itemType,
606+ status: "inProgress",
607+ title,
608+ ...(detail ? { detail } : {}),
609+ ...(update.rawInput !== undefined ? { data: update.rawInput } : {}),
610+ },
611+ });
612+ return;
613+ }
614+
615+ const stamp = yield* makeEventStamp();
616+ yield* offerRuntimeEvent({
617+ ...base,
618+ type: "item.updated",
619+ eventId: stamp.eventId,
620+ createdAt: stamp.createdAt,
621+ turnId: context.turnState.turnId,
622+ itemId: update.toolCallId,
623+ payload: {
624+ itemType,
625+ status: "inProgress",
626+ title,
627+ ...(detail ? { detail } : {}),
628+ ...(update.rawInput !== undefined ? { data: update.rawInput } : {}),
629+ },
630+ });
631+ return;
632+ }
633+
634+ case "tool_call_update": {
635+ if (!context.turnState) return;
636+ const status = update.status === "completed" ? "completed" : "inProgress";
637+ const stamp = yield* makeEventStamp();
638+ const eventType = update.status === "completed" ? "item.completed" : "item.updated";
639+ yield* offerRuntimeEvent({
640+ ...base,
641+ type: eventType,
642+ eventId: stamp.eventId,
643+ createdAt: stamp.createdAt,
644+ turnId: context.turnState.turnId,
645+ itemId: update.toolCallId,
646+ payload: {
647+ itemType: "command_execution",
648+ status,
649+ title: "Tool call",
650+ ...(summarizeToolOutput(update.rawOutput)
651+ ? { detail: summarizeToolOutput(update.rawOutput) }
652+ : {}),
653+ ...(update.rawOutput !== undefined ? { data: update.rawOutput } : {}),
654+ },
655+ });
656+ return;
657+ }
658+ }
659+ });
660+ };
661+
662+ const handleStdoutLine = (context: CursorSessionContext, line: string): void => {
663+ let parsed: unknown;
664+ try {
665+ parsed = JSON.parse(line);
666+ } catch {
667+ Effect.runFork(
668+ emitRuntimeWarning(context, "Received invalid JSON from Cursor ACP.", {
669+ line,
670+ }),
671+ );
672+ return;
673+ }
674+
675+ const message = asObject(parsed);
676+ if (!message) {
677+ Effect.runFork(
678+ emitRuntimeWarning(context, "Received non-object protocol message from Cursor ACP."),
679+ );
680+ return;
681+ }
682+
683+ if (nativeEventLogger) {
684+ const nativeMethod =
685+ typeof message.method === "string"
686+ ? message.method
687+ : (typeof message.id === "string" || typeof message.id === "number")
688+ ? "cursor/acp/response"
689+ : "cursor/acp/message";
690+ const nativeKind =
691+ typeof message.method === "string" &&
692+ (typeof message.id === "string" || typeof message.id === "number")
693+ ? "request"
694+ : typeof message.method === "string"
695+ ? "notification"
696+ : "session";
697+ nativeEventLogger.write({
698+ observedAt: new Date().toISOString(),
699+ event: {
700+ id: EventId.makeUnsafe(randomUUID()),
701+ kind: nativeKind,
702+ provider: PROVIDER,
703+ sessionId: context.session.sessionId,
704+ createdAt: new Date().toISOString(),
705+ method: nativeMethod,
706+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
707+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
708+ payload: message,
709+ },
710+ });
711+ }
712+
713+ if (
714+ (typeof message.id === "string" || typeof message.id === "number") &&
715+ typeof message.method === "string"
716+ ) {
717+ if (message.method === "session/request_permission") {
718+ Effect.runFork(handlePermissionRequest(context, message));
719+ return;
720+ }
721+
722+ writeCursorMessage(context, {
723+ jsonrpc: "2.0",
724+ id: message.id,
725+ error: {
726+ code: -32601,
727+ message: `Unsupported server request: ${message.method}`,
728+ },
729+ });
730+ return;
731+ }
732+
733+ if (
734+ (typeof message.id === "string" || typeof message.id === "number") &&
735+ ("result" in message || "error" in message)
736+ ) {
737+ resolvePendingRequest(context, message);
738+ return;
739+ }
740+
741+ if (typeof message.method === "string") {
742+ if (message.method === "session/update") {
743+ Effect.runFork(handleSessionUpdateNotification(context, message));
744+ return;
745+ }
746+
747+ Effect.runFork(
748+ emitRuntimeWarning(
749+ context,
750+ `Unhandled Cursor ACP notification '${message.method}'.`,
751+ message,
752+ ),
753+ );
754+ return;
755+ }
756+
757+ Effect.runFork(
758+ emitRuntimeWarning(context, "Received unrecognized protocol message from Cursor ACP.", {
759+ message,
760+ }),
761+ );
762+ };
763+
764+ const stopSessionInternal = (
765+ context: CursorSessionContext,
766+ options?: { readonly emitExitEvent?: boolean },
767+ ): Effect.Effect<void> =>
768+ Effect.gen(function* () {
769+ if (context.stopping) return;
770+ context.stopping = true;
771+
772+ for (const pending of context.pending.values()) {
773+ clearTimeout(pending.timeout);
774+ pending.reject(new Error("Cursor session stopped before request completion."));
775+ }
776+ context.pending.clear();
777+
778+ if (context.turnState) {
779+ yield* completeTurn(context, "interrupted", "Session stopped.", "cancelled");
780+ }
781+
782+ context.output.close();
783+ if (!context.child.killed) {
784+ context.child.kill();
785+ }
786+
787+ context.session = {
788+ ...context.session,
789+ status: "closed",
790+ activeTurnId: undefined,
791+ updatedAt: yield* nowIso,
792+ };
793+
794+ if (options?.emitExitEvent !== false) {
795+ const stamp = yield* makeEventStamp();
796+ yield* offerRuntimeEvent({
797+ type: "session.exited",
798+ eventId: stamp.eventId,
799+ provider: PROVIDER,
800+ sessionId: context.session.sessionId,
801+ createdAt: stamp.createdAt,
802+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
803+ payload: {
804+ reason: "Session stopped",
805+ exitKind: "graceful",
806+ },
807+ providerRefs: {
808+ providerSessionId: context.session.sessionId,
809+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
810+ },
811+ });
812+ }
813+
814+ sessions.delete(context.session.sessionId);
815+ });
816+
817+ const requireSession = (
818+ sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>,
819+ ): Effect.Effect<CursorSessionContext, ProviderAdapterError> => {
820+ const context = sessions.get(sessionId);
821+ if (!context) {
822+ return Effect.fail(
823+ new ProviderAdapterSessionNotFoundError({
824+ provider: PROVIDER,
825+ sessionId,
826+ }),
827+ );
828+ }
829+ if (context.stopping || context.session.status === "closed") {
830+ return Effect.fail(
831+ new ProviderAdapterSessionClosedError({
832+ provider: PROVIDER,
833+ sessionId,
834+ }),
835+ );
836+ }
837+ return Effect.succeed(context);
838+ };
839+
840+ const startSession: CursorAdapterShape["startSession"] = (input) =>
841+ Effect.gen(function* () {
842+ if (input.provider !== undefined && input.provider !== PROVIDER) {
843+ return yield* new ProviderAdapterValidationError({
844+ provider: PROVIDER,
845+ operation: "startSession",
846+ issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,
847+ });
848+ }
849+
850+ const startedAt = yield* nowIso;
851+ const sessionId = ProviderSessionId.makeUnsafe(`cursor-session-${yield* Random.nextUUIDv4}`);
852+ const cwd = input.cwd ?? process.cwd();
853+ const cursorOptions = input.providerOptions?.cursor as
854+ | { binaryPath?: string }
855+ | undefined;
856+ const binaryPath = cursorOptions?.binaryPath ?? "agent";
857+ const resumeState = readCursorResumeState(input.resumeCursor);
858+
859+ const child = yield* Effect.try({
860+ try: () =>
861+ spawnCursorAcp({
862+ binaryPath,
863+ cwd,
864+ env: process.env,
865+ }),
866+ catch: (cause) =>
867+ new ProviderAdapterProcessError({
868+ provider: PROVIDER,
869+ sessionId,
870+ detail: toMessage(cause, "Failed to spawn Cursor ACP process."),
871+ cause,
872+ }),
873+ });
874+
875+ const output = readline.createInterface({ input: child.stdout });
876+
877+ const session: ProviderSession = {
878+ sessionId,
879+ provider: PROVIDER,
880+ status: "connecting",
881+ ...(input.cwd ? { cwd: input.cwd } : {}),
882+ ...(input.model ? { model: input.model } : {}),
883+ createdAt: startedAt,
884+ updatedAt: startedAt,
885+ };
886+
887+ const context: CursorSessionContext = {
888+ session,
889+ child,
890+ output,
891+ pending: new Map(),
892+ pendingPermissions: new Map(),
893+ turns: [],
894+ turnState: undefined,
895+ acpSessionId: resumeState?.acpSessionId ?? "",
896+ nextRpcId: 1,
897+ stopping: false,
898+ };
899+
900+ output.on("line", (line) => {
901+ handleStdoutLine(context, line);
902+ });
903+
904+ child.stderr.on("data", (chunk: Buffer) => {
905+ const message = chunk.toString().trim();
906+ if (message.length === 0) {
907+ return;
908+ }
909+ Effect.runFork(
910+ emitRuntimeWarning(context, "Cursor ACP stderr output", {
911+ message,
912+ }),
913+ );
914+ });
915+
916+ child.on("error", (error) => {
917+ Effect.runFork(
918+ Effect.gen(function* () {
919+ const stamp = yield* makeEventStamp();
920+ yield* offerRuntimeEvent({
921+ type: "runtime.error",
922+ eventId: stamp.eventId,
923+ provider: PROVIDER,
924+ sessionId: context.session.sessionId,
925+ createdAt: stamp.createdAt,
926+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
927+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
928+ payload: {
929+ message: error.message || "Cursor ACP process error.",
930+ class: "transport_error",
931+ detail: error,
932+ },
933+ });
934+ }),
935+ );
936+ });
937+
938+ child.on("exit", (code, signal) => {
939+ if (context.stopping) {
940+ return;
941+ }
942+ Effect.runFork(
943+ Effect.gen(function* () {
944+ if (context.turnState) {
945+ yield* completeTurn(
946+ context,
947+ "failed",
948+ `Cursor ACP exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`,
949+ );
950+ }
951+
952+ const stamp = yield* makeEventStamp();
953+ yield* offerRuntimeEvent({
954+ type: "session.exited",
955+ eventId: stamp.eventId,
956+ provider: PROVIDER,
957+ sessionId: context.session.sessionId,
958+ createdAt: stamp.createdAt,
959+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
960+ payload: {
961+ reason: `Cursor ACP exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`,
962+ exitKind: code === 0 ? "graceful" : "error",
963+ recoverable: code === 0,
964+ },
965+ });
966+
967+ sessions.delete(context.session.sessionId);
968+ }),
969+ );
970+ });
971+
972+ sessions.set(sessionId, context);
973+
974+ const initializeResult = yield* Effect.tryPromise({
975+ try: async () => sendRequest(context, "initialize", {}),
976+ catch: (cause) => toRequestError(sessionId, "initialize", cause),
977+ });
978+ const decodedInitialize = Schema.decodeUnknownSync(CursorAcpInitializeResult)(initializeResult);
979+
980+ const initStamp = yield* makeEventStamp();
981+ yield* offerRuntimeEvent({
982+ type: "session.configured",
983+ eventId: initStamp.eventId,
984+ provider: PROVIDER,
985+ sessionId,
986+ createdAt: initStamp.createdAt,
987+ payload: {
988+ config: decodedInitialize,
989+ },
990+ providerRefs: {
991+ providerSessionId: sessionId,
992+ },
993+ raw: {
994+ source: "cursor.acp.response",
995+ method: "initialize",
996+ payload: initializeResult,
997+ },
998+ });
999+
1000+ yield* Effect.tryPromise({
1001+ try: async () => sendRequest(context, "authenticate", { methodId: "cursor_login" }),
1002+ catch: () => undefined,
1003+ });
1004+
1005+ const acpSessionId = yield* Effect.tryPromise({
1006+ try: async () => {
1007+ if (resumeState?.acpSessionId) {
1008+ await sendRequest(context, "session/load", {
1009+ sessionId: resumeState.acpSessionId,
1010+ cwd,
1011+ mcpServers: [],
1012+ });
1013+ return resumeState.acpSessionId;
1014+ }
1015+
1016+ const sessionNewParams: {
1017+ cwd: string;
1018+ mcpServers: [];
1019+ model?: string;
1020+ } = {
1021+ cwd,
1022+ mcpServers: [],
1023+ };
1024+ if (input.model) {
1025+ sessionNewParams.model = input.model;
1026+ }
1027+ const result = await sendRequest(context, "session/new", sessionNewParams);
1028+ const decoded = Schema.decodeUnknownSync(CursorAcpSessionNewResult)(result);
1029+ return decoded.sessionId;
1030+ },
1031+ catch: (cause) => toRequestError(sessionId, "session/new|session/load", cause),
1032+ });
1033+
1034+ const threadId = ProviderThreadId.makeUnsafe(acpSessionId);
1035+ context.acpSessionId = acpSessionId;
1036+ context.session = {
1037+ ...context.session,
1038+ status: "ready",
1039+ threadId,
1040+ resumeCursor: {
1041+ acpSessionId,
1042+ },
1043+ updatedAt: yield* nowIso,
1044+ };
1045+
1046+ const sessionStartedStamp = yield* makeEventStamp();
1047+ yield* offerRuntimeEvent({
1048+ type: "session.started",
1049+ eventId: sessionStartedStamp.eventId,
1050+ provider: PROVIDER,
1051+ sessionId,
1052+ createdAt: sessionStartedStamp.createdAt,
1053+ threadId,
1054+ payload: resumeState?.acpSessionId ? { resume: input.resumeCursor } : {},
1055+ providerRefs: {
1056+ providerSessionId: sessionId,
1057+ providerThreadId: threadId,
1058+ },
1059+ });
1060+
1061+ const threadStartedStamp = yield* makeEventStamp();
1062+ yield* offerRuntimeEvent({
1063+ type: "thread.started",
1064+ eventId: threadStartedStamp.eventId,
1065+ provider: PROVIDER,
1066+ sessionId,
1067+ createdAt: threadStartedStamp.createdAt,
1068+ threadId,
1069+ payload: {
1070+ providerThreadId: threadId,
1071+ },
1072+ providerRefs: {
1073+ providerSessionId: sessionId,
1074+ providerThreadId: threadId,
1075+ },
1076+ });
1077+
1078+ const readyStamp = yield* makeEventStamp();
1079+ yield* offerRuntimeEvent({
1080+ type: "session.state.changed",
1081+ eventId: readyStamp.eventId,
1082+ provider: PROVIDER,
1083+ sessionId,
1084+ createdAt: readyStamp.createdAt,
1085+ threadId,
1086+ payload: {
1087+ state: "ready",
1088+ },
1089+ providerRefs: {
1090+ providerSessionId: sessionId,
1091+ providerThreadId: threadId,
1092+ },
1093+ });
1094+
1095+ return {
1096+ ...context.session,
1097+ };
1098+ });
1099+
1100+ const sendTurn: CursorAdapterShape["sendTurn"] = (input) =>
1101+ Effect.gen(function* () {
1102+ const context = yield* requireSession(input.sessionId);
1103+
1104+ if (context.turnState) {
1105+ return yield* new ProviderAdapterValidationError({
1106+ provider: PROVIDER,
1107+ operation: "sendTurn",
1108+ issue: `Session '${input.sessionId}' already has an active turn '${context.turnState.turnId}'.`,
1109+ });
1110+ }
1111+
1112+ const turnId = ProviderTurnId.makeUnsafe(`cursor-turn-${yield* Random.nextUUIDv4}`);
1113+ const turnState: CursorTurnState = {
1114+ turnId,
1115+ assistantItemId: `cursor-assistant-${yield* Random.nextUUIDv4}`,
1116+ startedToolCalls: new Set(),
1117+ items: [],
1118+ };
1119+
1120+ context.turnState = turnState;
1121+ context.session = {
1122+ ...context.session,
1123+ status: "running",
1124+ activeTurnId: turnId,
1125+ updatedAt: yield* nowIso,
1126+ };
1127+
1128+ const startedStamp = yield* makeEventStamp();
1129+ yield* offerRuntimeEvent({
1130+ type: "turn.started",
1131+ eventId: startedStamp.eventId,
1132+ provider: PROVIDER,
1133+ sessionId: context.session.sessionId,
1134+ createdAt: startedStamp.createdAt,
1135+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1136+ turnId,
1137+ payload: {
1138+ ...(input.model ? { model: input.model } : {}),
1139+ ...(input.effort ? { effort: input.effort } : {}),
1140+ },
1141+ providerRefs: {
1142+ providerSessionId: context.session.sessionId,
1143+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1144+ providerTurnId: turnId,
1145+ },
1146+ });
1147+
1148+ const promptText = input.input?.trim();
1149+ if (!promptText || promptText.length === 0) {
1150+ return yield* new ProviderAdapterValidationError({
1151+ provider: PROVIDER,
1152+ operation: "sendTurn",
1153+ issue: "Turn input must be non-empty.",
1154+ });
1155+ }
1156+
1157+ const promptResultRaw = yield* Effect.tryPromise({
1158+ try: async () =>
1159+ sendRequest(context, "session/prompt", {
1160+ sessionId: context.acpSessionId,
1161+ prompt: [{ type: "text", text: promptText }],
1162+ }),
1163+ catch: (cause) => toRequestError(input.sessionId, "session/prompt", cause),
1164+ });
1165+
1166+ const promptResult = Schema.decodeUnknownSync(CursorAcpSessionPromptResult)(promptResultRaw);
1167+ const turnStateValue = mapStopReasonToTurnState(promptResult.stopReason);
1168+ yield* completeTurn(
1169+ context,
1170+ turnStateValue,
1171+ turnStateValue === "failed" ? "Cursor prompt failed." : undefined,
1172+ promptResult.stopReason,
1173+ );
1174+
1175+ context.session = {
1176+ ...context.session,
1177+ status: "ready",
1178+ activeTurnId: undefined,
1179+ updatedAt: yield* nowIso,
1180+ resumeCursor: {
1181+ acpSessionId: context.acpSessionId,
1182+ },
1183+ };
1184+
1185+ return {
1186+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1187+ turnId,
1188+ ...(context.session.resumeCursor !== undefined
1189+ ? { resumeCursor: context.session.resumeCursor }
1190+ : {}),
1191+ } satisfies ProviderTurnStartResult;
1192+ });
1193+
1194+ const interruptTurn: CursorAdapterShape["interruptTurn"] = (sessionId, _turnId) =>
1195+ Effect.gen(function* () {
1196+ const context = yield* requireSession(sessionId);
1197+ if (!context.turnState) {
1198+ return;
1199+ }
1200+
1201+ const cancelResult = yield* Effect.tryPromise({
1202+ try: async () => sendRequest(context, "session/cancel", { sessionId: context.acpSessionId }, 15_000),
1203+ catch: (cause) => cause,
1204+ });
1205+
1206+ if (cancelResult instanceof Error) {
1207+ yield* emitRuntimeWarning(
1208+ context,
1209+ "Cursor ACP session/cancel is unavailable; marking turn as interrupted.",
1210+ cancelResult,
1211+ );
1212+ }
1213+
1214+ yield* completeTurn(context, "interrupted", "Turn interrupted by user.", "cancelled");
1215+ });
1216+
1217+ const readThread: CursorAdapterShape["readThread"] = (sessionId) =>
1218+ Effect.gen(function* () {
1219+ const context = yield* requireSession(sessionId);
1220+ return {
1221+ threadId: ProviderThreadId.makeUnsafe(context.acpSessionId),
1222+ turns: context.turns.map((turn) => ({
1223+ id: turn.id,
1224+ items: [...turn.items],
1225+ })),
1226+ };
1227+ });
1228+
1229+ const rollbackThread: CursorAdapterShape["rollbackThread"] = (sessionId, _numTurns) =>
1230+ Effect.fail(
1231+ new ProviderAdapterRequestError({
1232+ provider: PROVIDER,
1233+ method: "thread/rollback",
1234+ detail: `Cursor ACP does not support thread rollback for session '${sessionId}'.`,
1235+ }),
1236+ );
1237+
1238+ const respondToRequest: CursorAdapterShape["respondToRequest"] = (
1239+ sessionId,
1240+ requestId,
1241+ decision,
1242+ ) =>
1243+ Effect.gen(function* () {
1244+ const context = yield* requireSession(sessionId);
1245+ const pending = context.pendingPermissions.get(requestId);
1246+ if (!pending) {
1247+ return yield* new ProviderAdapterRequestError({
1248+ provider: PROVIDER,
1249+ method: "session/request_permission",
1250+ detail: `Unknown pending permission request: ${requestId}`,
1251+ });
1252+ }
1253+
1254+ const allowAlways = pending.options.find((option) => option.optionId === "allow-always");
1255+ const allowOnce = pending.options.find((option) => option.optionId === "allow-once");
1256+ const rejectOnce = pending.options.find((option) => option.optionId === "reject-once");
1257+
1258+ const optionId =
1259+ decision === "acceptForSession"
1260+ ? allowAlways?.optionId ?? allowOnce?.optionId
1261+ : decision === "accept"
1262+ ? allowOnce?.optionId ?? allowAlways?.optionId
1263+ : rejectOnce?.optionId ?? pending.options[0]?.optionId;
1264+
1265+ if (!optionId) {
1266+ return yield* new ProviderAdapterRequestError({
1267+ provider: PROVIDER,
1268+ method: "session/request_permission",
1269+ detail: `No selectable permission options for request: ${requestId}`,
1270+ });
1271+ }
1272+
1273+ writeCursorMessage(context, {
1274+ jsonrpc: "2.0",
1275+ id: pending.jsonRpcId,
1276+ result: {
1277+ outcome: {
1278+ outcome: "selected",
1279+ optionId,
1280+ },
1281+ },
1282+ });
1283+
1284+ context.pendingPermissions.delete(requestId);
1285+
1286+ const stamp = yield* makeEventStamp();
1287+ yield* offerRuntimeEvent({
1288+ type: "request.resolved",
1289+ eventId: stamp.eventId,
1290+ provider: PROVIDER,
1291+ sessionId: context.session.sessionId,
1292+ createdAt: stamp.createdAt,
1293+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1294+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1295+ requestId,
1296+ payload: {
1297+ requestType: pending.requestType,
1298+ decision,
1299+ resolution: {
1300+ optionId,
1301+ },
1302+ },
1303+ providerRefs: {
1304+ providerSessionId: context.session.sessionId,
1305+ ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1306+ ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
1307+ providerRequestId: requestId,
1308+ },
1309+ raw: {
1310+ source: "cursor.acp.response",
1311+ method: "session/request_permission",
1312+ payload: {
1313+ optionId,
1314+ },
1315+ },
1316+ });
1317+ });
1318+
1319+ const stopSession: CursorAdapterShape["stopSession"] = (sessionId) =>
1320+ Effect.gen(function* () {
1321+ const context = yield* requireSession(sessionId);
1322+ yield* stopSessionInternal(context, {
1323+ emitExitEvent: true,
1324+ });
1325+ });
1326+
1327+ const listSessions: CursorAdapterShape["listSessions"] = () =>
1328+ Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session })));
1329+
1330+ const hasSession: CursorAdapterShape["hasSession"] = (sessionId) =>
1331+ Effect.sync(() => {
1332+ const context = sessions.get(sessionId);
1333+ return context !== undefined && !context.stopping;
1334+ });
1335+
1336+ const stopAll: CursorAdapterShape["stopAll"] = () =>
1337+ Effect.forEach(
1338+ sessions,
1339+ ([, context]) =>
1340+ stopSessionInternal(context, {
1341+ emitExitEvent: true,
1342+ }),
1343+ { discard: true },
1344+ );
1345+
1346+ yield* Effect.addFinalizer(() =>
1347+ Effect.forEach(
1348+ sessions,
1349+ ([, context]) =>
1350+ stopSessionInternal(context, {
1351+ emitExitEvent: false,
1352+ }),
1353+ { discard: true },
1354+ ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))),
1355+ );
1356+
1357+ return {
1358+ provider: PROVIDER,
1359+ startSession,
1360+ sendTurn,
1361+ interruptTurn,
1362+ readThread,
1363+ rollbackThread,
1364+ respondToRequest,
1365+ stopSession,
1366+ listSessions,
1367+ hasSession,
1368+ stopAll,
1369+ streamEvents: Stream.fromQueue(runtimeEventQueue),
1370+ } satisfies CursorAdapterShape;
1371+ });
1372+}
1373+
1374+export const CursorAdapterLive = Layer.effect(CursorAdapter, makeCursorAdapter());
1375+
1376+export function makeCursorAdapterLive(options?: CursorAdapterLiveOptions) {
1377+ return Layer.effect(CursorAdapter, makeCursorAdapter(options));
1378+}
apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts+20−1
@@ -6,6 +6,7 @@ import { Effect, Layer, Stream } from "effect"; · 5 unmodified lines
66
77 import { ClaudeCodeAdapter, ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts";
88 import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";
9+import { CursorAdapter, CursorAdapterShape } from "../Services/CursorAdapter.ts";
910 import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
1011 import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
1112 import { ProviderUnsupportedError } from "../Errors.ts";
@@ -41,6 +42,21 @@ const fakeClaudeAdapter: ClaudeCodeAdapterShape = { · 29 unmodified lines
4142 streamEvents: Stream.empty,
4243 };
4344
45+const fakeCursorAdapter: CursorAdapterShape = {
46+ provider: "cursor",
47+ startSession: vi.fn(),
48+ sendTurn: vi.fn(),
49+ interruptTurn: vi.fn(),
50+ respondToRequest: vi.fn(),
51+ stopSession: vi.fn(),
52+ listSessions: vi.fn(),
53+ hasSession: vi.fn(),
54+ readThread: vi.fn(),
55+ rollbackThread: vi.fn(),
56+ stopAll: vi.fn(),
57+ streamEvents: Stream.empty,
58+};
59+
4460 const layer = it.layer(
4561 Layer.mergeAll(
4662 Layer.provide(
@@ -48,6 +64,7 @@ const layer = it.layer( · 1 unmodified line
4864 Layer.mergeAll(
4965 Layer.succeed(CodexAdapter, fakeCodexAdapter),
5066 Layer.succeed(ClaudeCodeAdapter, fakeClaudeAdapter),
67+ Layer.succeed(CursorAdapter, fakeCursorAdapter),
5168 ),
5269 ),
5370 NodeServices.layer,
@@ -60,11 +77,13 @@ layer("ProviderAdapterRegistryLive", (it) => { · 6 unmodified lines
6077 const registry = yield* ProviderAdapterRegistry;
6178 const codex = yield* registry.getByProvider("codex");
6279 const claude = yield* registry.getByProvider("claudeCode");
80+ const cursor = yield* registry.getByProvider("cursor");
6381 assert.equal(codex, fakeCodexAdapter);
6482 assert.equal(claude, fakeClaudeAdapter);
83+ assert.equal(cursor, fakeCursorAdapter);
6584
6685 const providers = yield* registry.listProviders();
67− assert.deepEqual(providers, ["codex", "claudeCode"]);
86+ assert.deepEqual(providers, ["codex", "claudeCode", "cursor"]);
6887 }),
6988 );
7089
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts+2−1
@@ -17,6 +17,7 @@ import { · 16 unmodified lines
1717 } from "../Services/ProviderAdapterRegistry.ts";
1818 import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts";
1919 import { CodexAdapter } from "../Services/CodexAdapter.ts";
20+import { CursorAdapter } from "../Services/CursorAdapter.ts";
2021
2122 export interface ProviderAdapterRegistryLiveOptions {
2223 readonly adapters?: ReadonlyArray<ProviderAdapterShape<ProviderAdapterError>>;
@@ -27,7 +28,7 @@ const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOption · 4 unmodified lines
2728 const adapters =
2829 options?.adapters !== undefined
2930 ? options.adapters
30− : [yield* CodexAdapter, yield* ClaudeCodeAdapter];
31+ : [yield* CodexAdapter, yield* ClaudeCodeAdapter, yield* CursorAdapter];
3132 const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter]));
3233
3334 const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => {
apps/server/src/provider/Services/CursorAdapter.test.ts+97−98
@@ -1,131 +1,130 @@
11 import { Schema } from "effect";
22 import { describe, expect, it } from "vitest";
33
4−import { CursorCliStreamEvent } from "./CursorAdapter.ts";
4+import {
5+ CursorAcpPermissionRequest,
6+ CursorAcpSessionPromptResult,
7+ CursorAcpSessionUpdateNotification,
8+} from "./CursorAdapter.ts";
59
6−describe("CursorCliStreamEvent", () => {
7− it("decodes system/init events", () => {
8− const decoded = Schema.decodeUnknownSync(CursorCliStreamEvent)({
9− type: "system",
10− subtype: "init",
11− session_id: "sess-1",
12− apiKeySource: "login",
13− cwd: "/tmp/project",
14− model: "Auto",
15− permissionMode: "default",
10+describe("Cursor ACP schemas", () => {
11+ it("decodes session/update thought and message chunks", () => {
12+ const thought = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification)({
13+ jsonrpc: "2.0",
14+ method: "session/update",
15+ params: {
16+ sessionId: "sess-1",
17+ update: {
18+ sessionUpdate: "agent_thought_chunk",
19+ content: {
20+ type: "text",
21+ text: "thinking",
22+ },
23+ },
24+ },
1625 });
1726
18− expect(decoded.type).toBe("system");
19− if (decoded.type !== "system") {
20− return;
21− }
22− expect(decoded.subtype).toBe("init");
23− expect(decoded.session_id).toBe("sess-1");
24− });
27+ expect(thought.params.update.sessionUpdate).toBe("agent_thought_chunk");
2528
26− it("decodes thinking delta/completed events", () => {
27− const delta = Schema.decodeUnknownSync(CursorCliStreamEvent)({
28− type: "thinking",
29− subtype: "delta",
30− text: "draft",
31− session_id: "sess-1",
32− timestamp_ms: 123,
29+ const message = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification)({
30+ jsonrpc: "2.0",
31+ method: "session/update",
32+ params: {
33+ sessionId: "sess-1",
34+ update: {
35+ sessionUpdate: "agent_message_chunk",
36+ content: {
37+ type: "text",
38+ text: "hello",
39+ },
40+ },
41+ },
3342 });
3443
35− expect(delta.type).toBe("thinking");
36− if (delta.type !== "thinking") {
37− return;
38− }
39− expect(delta.subtype).toBe("delta");
44+ expect(message.params.update.sessionUpdate).toBe("agent_message_chunk");
45+ });
4046
41− const completed = Schema.decodeUnknownSync(CursorCliStreamEvent)({
42− type: "thinking",
43− subtype: "completed",
44− session_id: "sess-1",
47+ it("decodes tool call lifecycle updates", () => {
48+ const started = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification)({
49+ jsonrpc: "2.0",
50+ method: "session/update",
51+ params: {
52+ sessionId: "sess-1",
53+ update: {
54+ sessionUpdate: "tool_call",
55+ toolCallId: "tool-1",
56+ title: "Terminal",
57+ kind: "execute",
58+ status: "pending",
59+ rawInput: { command: "pwd" },
60+ },
61+ },
4562 });
4663
47− expect(completed.type).toBe("thinking");
48− if (completed.type !== "thinking") {
49− return;
50− }
51− expect(completed.subtype).toBe("completed");
52− });
64+ expect(started.params.update.sessionUpdate).toBe("tool_call");
5365
54− it("decodes tool_call completed events with rejected results", () => {
55− const decoded = Schema.decodeUnknownSync(CursorCliStreamEvent)({
56− type: "tool_call",
57− subtype: "completed",
58− call_id: "tool-1\nline-2",
59− session_id: "sess-1",
60− tool_call: {
61− shellToolCall: {
62− args: {
63− command: "rm -rf /tmp/nope",
64− },
65− result: {
66− rejected: {
67− reason: "approval denied",
68− },
66+ const completed = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification)({
67+ jsonrpc: "2.0",
68+ method: "session/update",
69+ params: {
70+ sessionId: "sess-1",
71+ update: {
72+ sessionUpdate: "tool_call_update",
73+ toolCallId: "tool-1",
74+ status: "completed",
75+ rawOutput: {
76+ exitCode: 0,
77+ stdout: "ok",
78+ stderr: "",
6979 },
7080 },
7181 },
7282 });
7383
74− expect(decoded.type).toBe("tool_call");
75− if (decoded.type !== "tool_call") {
76− return;
77− }
78− expect(decoded.subtype).toBe("completed");
79− expect(decoded.call_id).toContain("\n");
84+ expect(completed.params.update.sessionUpdate).toBe("tool_call_update");
8085 });
8186
82− it("decodes result success events with usage payload", () => {
83− const decoded = Schema.decodeUnknownSync(CursorCliStreamEvent)({
84− type: "result",
85− subtype: "success",
86− duration_ms: 100,
87− duration_api_ms: 90,
88− is_error: false,
89− result: "OK",
90− session_id: "sess-1",
91− request_id: "req-1",
92− usage: {
93− inputTokens: 10,
94− outputTokens: 5,
95− cacheReadTokens: 2,
96− cacheWriteTokens: 1,
87+ it("decodes permission requests", () => {
88+ const decoded = Schema.decodeUnknownSync(CursorAcpPermissionRequest)({
89+ jsonrpc: "2.0",
90+ id: 9,
91+ method: "session/request_permission",
92+ params: {
93+ sessionId: "sess-1",
94+ toolCall: {
95+ toolCallId: "tool-1",
96+ kind: "execute",
97+ },
98+ options: [
99+ { optionId: "allow-once", name: "Allow once", kind: "allow_once" },
100+ { optionId: "reject-once", name: "Reject", kind: "reject_once" },
101+ ],
97102 },
98103 });
99104
100− expect(decoded.type).toBe("result");
101− if (decoded.type !== "result") {
102− return;
103− }
104− expect(decoded.subtype).toBe("success");
105− expect(decoded.is_error).toBe(false);
105+ expect(decoded.method).toBe("session/request_permission");
106+ expect(decoded.params.options).toHaveLength(2);
106107 });
107108
108− it("decodes connection/retry lifecycle events", () => {
109− const reconnecting = Schema.decodeUnknownSync(CursorCliStreamEvent)({
110− type: "connection",
111− subtype: "reconnecting",
112− session_id: "sess-1",
109+ it("decodes prompt completion result payload", () => {
110+ const decoded = Schema.decodeUnknownSync(CursorAcpSessionPromptResult)({
111+ stopReason: "end_turn",
113112 });
114− expect(reconnecting.type).toBe("connection");
115113
116− const retry = Schema.decodeUnknownSync(CursorCliStreamEvent)({
117− type: "retry",
118− subtype: "resuming",
119− session_id: "sess-1",
120− });
121− expect(retry.type).toBe("retry");
114+ expect(decoded.stopReason).toBe("end_turn");
122115 });
123116
124− it("rejects unsupported stream event types", () => {
117+ it("rejects unsupported update types", () => {
125118 expect(() =>
126− Schema.decodeUnknownSync(CursorCliStreamEvent)({
127− type: "bogus",
128− session_id: "sess-1",
119+ Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification)({
120+ jsonrpc: "2.0",
121+ method: "session/update",
122+ params: {
123+ sessionId: "sess-1",
124+ update: {
125+ sessionUpdate: "unknown_update",
126+ },
127+ },
129128 }),
130129 ).toThrow();
131130 });
apps/server/src/provider/Services/CursorAdapter.ts+74−178
@@ -1,11 +1,7 @@
11 /**
2− * CursorAdapter - Cursor CLI implementation of the generic provider adapter contract.
2+ * CursorAdapter - Cursor ACP implementation of the generic provider adapter contract.
33 *
4− * This service will own Cursor CLI (`agent`) process / stream-json semantics and emit
5− * canonical provider runtime events via the shared provider adapter contract.
6− *
7− * This file intentionally defines raw Cursor stream-json schemas up front so the future
8− * layer implementation can decode/validate NDJSON lines in a single place.
4+ * Defines ACP JSON-RPC schemas used by the Cursor adapter layer.
95 *
106 * @module CursorAdapter
117 */
@@ -14,201 +10,101 @@ import { Schema, ServiceMap } from "effect"; · 2 unmodified lines
1410 import type { ProviderAdapterError } from "../Errors.ts";
1511 import type { ProviderAdapterShape } from "./ProviderAdapter.ts";
1612
17−export const CursorCliSessionId = Schema.String.check(Schema.isNonEmpty());
18−export type CursorCliSessionId = typeof CursorCliSessionId.Type;
13+export const CursorAcpJsonRpcId = Schema.Union([Schema.String, Schema.Int]);
14+export type CursorAcpJsonRpcId = typeof CursorAcpJsonRpcId.Type;
1915
20−export const CursorCliTextContentPart = Schema.Struct({
16+export const CursorAcpTextContent = Schema.Struct({
2117 type: Schema.Literal("text"),
2218 text: Schema.String,
2319 });
24−export type CursorCliTextContentPart = typeof CursorCliTextContentPart.Type;
20+export type CursorAcpTextContent = typeof CursorAcpTextContent.Type;
2521
26−export const CursorCliContentPart = Schema.Union([
27− CursorCliTextContentPart,
22+export const CursorAcpSessionUpdate = Schema.Union([
23+ Schema.Struct({
24+ sessionUpdate: Schema.Literal("available_commands_update"),
25+ availableCommands: Schema.Array(
26+ Schema.Struct({
27+ name: Schema.String,
28+ description: Schema.optional(Schema.String),
29+ }),
30+ ),
31+ }),
2832 Schema.Struct({
29− type: Schema.String,
33+ sessionUpdate: Schema.Literal("agent_thought_chunk"),
34+ content: CursorAcpTextContent,
35+ }),
36+ Schema.Struct({
37+ sessionUpdate: Schema.Literal("agent_message_chunk"),
38+ content: CursorAcpTextContent,
39+ }),
40+ Schema.Struct({
41+ sessionUpdate: Schema.Literal("tool_call"),
42+ toolCallId: Schema.String,
43+ title: Schema.optional(Schema.String),
44+ kind: Schema.optional(Schema.String),
45+ status: Schema.optional(Schema.String),
46+ rawInput: Schema.optional(Schema.Unknown),
47+ }),
48+ Schema.Struct({
49+ sessionUpdate: Schema.Literal("tool_call_update"),
50+ toolCallId: Schema.String,
51+ status: Schema.String,
52+ rawOutput: Schema.optional(Schema.Unknown),
3053 }),
3154 ]);
32−export type CursorCliContentPart = typeof CursorCliContentPart.Type;
33−
34−export const CursorCliUserMessage = Schema.Struct({
35− role: Schema.Literal("user"),
36− content: Schema.Array(CursorCliContentPart),
37−});
38−export type CursorCliUserMessage = typeof CursorCliUserMessage.Type;
39−
40−export const CursorCliAssistantMessage = Schema.Struct({
41− role: Schema.Literal("assistant"),
42− content: Schema.Array(CursorCliContentPart),
43−});
44−export type CursorCliAssistantMessage = typeof CursorCliAssistantMessage.Type;
45−
46−export const CursorCliToolCallResult = Schema.Struct({
47− success: Schema.optional(Schema.Unknown),
48− failure: Schema.optional(Schema.Unknown),
49− rejected: Schema.optional(Schema.Unknown),
50−});
51−export type CursorCliToolCallResult = typeof CursorCliToolCallResult.Type;
52−
53−export const CursorCliToolCallEntry = Schema.Struct({
54− args: Schema.optional(Schema.Unknown),
55− result: Schema.optional(CursorCliToolCallResult),
56−});
57−export type CursorCliToolCallEntry = typeof CursorCliToolCallEntry.Type;
58−
59−export const CursorCliFunctionToolCall = Schema.Struct({
60− name: Schema.String,
61− arguments: Schema.String,
62− result: Schema.optional(CursorCliToolCallResult),
63−});
64−export type CursorCliFunctionToolCall = typeof CursorCliFunctionToolCall.Type;
65−
66−export const CursorCliToolCallPayload = Schema.Struct({
67− readToolCall: Schema.optional(CursorCliToolCallEntry),
68− writeToolCall: Schema.optional(CursorCliToolCallEntry),
69− editToolCall: Schema.optional(CursorCliToolCallEntry),
70− shellToolCall: Schema.optional(CursorCliToolCallEntry),
71− grepToolCall: Schema.optional(CursorCliToolCallEntry),
72− globToolCall: Schema.optional(CursorCliToolCallEntry),
73− function: Schema.optional(CursorCliFunctionToolCall),
74−});
75−export type CursorCliToolCallPayload = typeof CursorCliToolCallPayload.Type;
76−
77−export const CursorCliTokenUsage = Schema.Struct({
78− inputTokens: Schema.optional(Schema.Number),
79− outputTokens: Schema.optional(Schema.Number),
80− cacheReadTokens: Schema.optional(Schema.Number),
81− cacheWriteTokens: Schema.optional(Schema.Number),
82−});
83−export type CursorCliTokenUsage = typeof CursorCliTokenUsage.Type;
84−
85−const CursorCliTimestampMs = {
86− timestamp_ms: Schema.optional(Schema.Number),
87−} as const;
88−
89−export const CursorCliSystemInitEvent = Schema.Struct({
90− type: Schema.Literal("system"),
91− subtype: Schema.Literal("init"),
92− session_id: CursorCliSessionId,
93− apiKeySource: Schema.optional(Schema.String),
94− cwd: Schema.optional(Schema.String),
95− model: Schema.optional(Schema.String),
96− permissionMode: Schema.optional(Schema.String),
97− ...CursorCliTimestampMs,
98−});
99−export type CursorCliSystemInitEvent = typeof CursorCliSystemInitEvent.Type;
100−
101−export const CursorCliUserEvent = Schema.Struct({
102− type: Schema.Literal("user"),
103− message: CursorCliUserMessage,
104− session_id: CursorCliSessionId,
105− ...CursorCliTimestampMs,
106−});
107−export type CursorCliUserEvent = typeof CursorCliUserEvent.Type;
108−
109−export const CursorCliAssistantEvent = Schema.Struct({
110− type: Schema.Literal("assistant"),
111− message: CursorCliAssistantMessage,
112− session_id: CursorCliSessionId,
113− ...CursorCliTimestampMs,
114−});
115−export type CursorCliAssistantEvent = typeof CursorCliAssistantEvent.Type;
116−
117−export const CursorCliThinkingDeltaEvent = Schema.Struct({
118− type: Schema.Literal("thinking"),
119− subtype: Schema.Literal("delta"),
120− text: Schema.String,
121− session_id: CursorCliSessionId,
122− ...CursorCliTimestampMs,
123−});
124−export type CursorCliThinkingDeltaEvent = typeof CursorCliThinkingDeltaEvent.Type;
125−
126−export const CursorCliThinkingCompletedEvent = Schema.Struct({
127− type: Schema.Literal("thinking"),
128− subtype: Schema.Literal("completed"),
129− session_id: CursorCliSessionId,
130− ...CursorCliTimestampMs,
55+export type CursorAcpSessionUpdate = typeof CursorAcpSessionUpdate.Type;
56+
57+export const CursorAcpSessionUpdateNotification = Schema.Struct({
58+ jsonrpc: Schema.optional(Schema.Literal("2.0")),
59+ method: Schema.Literal("session/update"),
60+ params: Schema.Struct({
61+ sessionId: Schema.String,
62+ update: CursorAcpSessionUpdate,
63+ }),
13164 });
132−export type CursorCliThinkingCompletedEvent = typeof CursorCliThinkingCompletedEvent.Type;
65+export type CursorAcpSessionUpdateNotification = typeof CursorAcpSessionUpdateNotification.Type;
13366
134−export const CursorCliToolCallStartedEvent = Schema.Struct({
135− type: Schema.Literal("tool_call"),
136− subtype: Schema.Literal("started"),
137− call_id: Schema.String,
138− tool_call: CursorCliToolCallPayload,
139− session_id: CursorCliSessionId,
140− ...CursorCliTimestampMs,
67+export const CursorAcpPermissionOption = Schema.Struct({
68+ optionId: Schema.String,
69+ name: Schema.optional(Schema.String),
70+ kind: Schema.optional(Schema.String),
14171 });
142−export type CursorCliToolCallStartedEvent = typeof CursorCliToolCallStartedEvent.Type;
72+export type CursorAcpPermissionOption = typeof CursorAcpPermissionOption.Type;
14373
144−export const CursorCliToolCallCompletedEvent = Schema.Struct({
145− type: Schema.Literal("tool_call"),
146− subtype: Schema.Literal("completed"),
147− call_id: Schema.String,
148− tool_call: CursorCliToolCallPayload,
149− session_id: CursorCliSessionId,
150− ...CursorCliTimestampMs,
74+export const CursorAcpPermissionRequest = Schema.Struct({
75+ jsonrpc: Schema.optional(Schema.Literal("2.0")),
76+ id: CursorAcpJsonRpcId,
77+ method: Schema.Literal("session/request_permission"),
78+ params: Schema.Struct({
79+ sessionId: Schema.String,
80+ toolCall: Schema.optional(Schema.Unknown),
81+ options: Schema.Array(CursorAcpPermissionOption),
82+ }),
15183 });
152−export type CursorCliToolCallCompletedEvent = typeof CursorCliToolCallCompletedEvent.Type;
84+export type CursorAcpPermissionRequest = typeof CursorAcpPermissionRequest.Type;
15385
154−export const CursorCliResultSuccessEvent = Schema.Struct({
155− type: Schema.Literal("result"),
156− subtype: Schema.Literal("success"),
157− duration_ms: Schema.Number,
158− duration_api_ms: Schema.optional(Schema.Number),
159− is_error: Schema.Boolean,
160− result: Schema.String,
161− session_id: CursorCliSessionId,
162− request_id: Schema.optional(Schema.String),
163− usage: Schema.optional(CursorCliTokenUsage),
164− ...CursorCliTimestampMs,
86+export const CursorAcpInitializeResult = Schema.Struct({
87+ protocolVersion: Schema.optional(Schema.Int),
88+ agentCapabilities: Schema.optional(Schema.Unknown),
89+ authMethods: Schema.optional(Schema.Array(Schema.Unknown)),
16590 });
166−export type CursorCliResultSuccessEvent = typeof CursorCliResultSuccessEvent.Type;
91+export type CursorAcpInitializeResult = typeof CursorAcpInitializeResult.Type;
16792
168−export const CursorCliConnectionEvent = Schema.Struct({
169− type: Schema.Literal("connection"),
170− subtype: Schema.Literals(["reconnecting", "reconnected"]),
171− session_id: Schema.optional(CursorCliSessionId),
172− ...CursorCliTimestampMs,
93+export const CursorAcpSessionNewResult = Schema.Struct({
94+ sessionId: Schema.String,
95+ modes: Schema.optional(Schema.Unknown),
17396 });
174−export type CursorCliConnectionEvent = typeof CursorCliConnectionEvent.Type;
97+export type CursorAcpSessionNewResult = typeof CursorAcpSessionNewResult.Type;
17598
176−export const CursorCliRetryEvent = Schema.Struct({
177− type: Schema.Literal("retry"),
178− subtype: Schema.Literals(["starting", "resuming"]),
179− session_id: Schema.optional(CursorCliSessionId),
180− ...CursorCliTimestampMs,
99+export const CursorAcpSessionPromptResult = Schema.Struct({
100+ stopReason: Schema.optional(Schema.String),
181101 });
182−export type CursorCliRetryEvent = typeof CursorCliRetryEvent.Type;
183−
184−export const CursorCliStreamEvent = Schema.Union([
185− CursorCliSystemInitEvent,
186− CursorCliUserEvent,
187− CursorCliAssistantEvent,
188− CursorCliThinkingDeltaEvent,
189− CursorCliThinkingCompletedEvent,
190− CursorCliToolCallStartedEvent,
191− CursorCliToolCallCompletedEvent,
192− CursorCliResultSuccessEvent,
193− CursorCliConnectionEvent,
194− CursorCliRetryEvent,
195−]);
196−export type CursorCliStreamEvent = typeof CursorCliStreamEvent.Type;
102+export type CursorAcpSessionPromptResult = typeof CursorAcpSessionPromptResult.Type;
197103
198−/**
199− * CursorAdapterShape - Service API for the Cursor provider adapter.
200− *
201− * `provider` is intentionally narrowed to `"cursor"` here. Until contracts add
202− * Cursor to `ProviderKind`, this shape is defined via `Omit<...,"provider">`.
203− */
204−export interface CursorAdapterShape
205− extends Omit<ProviderAdapterShape<ProviderAdapterError>, "provider"> {
104+export interface CursorAdapterShape extends ProviderAdapterShape<ProviderAdapterError> {
206105 readonly provider: "cursor";
207106 }
208107
209−/**
210− * CursorAdapter - Service tag for Cursor provider adapter operations.
211− */
212108 export class CursorAdapter extends ServiceMap.Service<CursorAdapter, CursorAdapterShape>()(
213109 "t3/provider/Services/CursorAdapter",
214110 ) {}
apps/server/src/serverLayers.ts+5−1
@@ -20,6 +20,7 @@ import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRun · 19 unmodified lines
2020 import { ProviderUnsupportedError } from "./provider/Errors";
2121 import { makeClaudeCodeAdapterLive } from "./provider/Layers/ClaudeCodeAdapter";
2222 import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter";
23+import { makeCursorAdapterLive } from "./provider/Layers/CursorAdapter";
2324 import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";
2425 import { makeProviderServiceLive } from "./provider/Layers/ProviderService";
2526 import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory";
@@ -60,8 +61,11 @@ export function makeServerProviderLayer(): Layer.Layer< · 34 unmodified lines
6061 const claudeAdapterLayer = makeClaudeCodeAdapterLive({
6162 nativeEventLogPath: path.join(providerLogsDir, "provider-native.ndjson"),
6263 });
64+ const cursorAdapterLayer = makeCursorAdapterLive({
65+ nativeEventLogPath: path.join(providerLogsDir, "provider-native.ndjson"),
66+ });
6367 const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe(
64− Layer.provide(Layer.mergeAll(codexAdapterLayer, claudeAdapterLayer)),
68+ Layer.provide(Layer.mergeAll(codexAdapterLayer, claudeAdapterLayer, cursorAdapterLayer)),
6569 Layer.provideMerge(providerSessionDirectoryLayer),
6670 );
6771 return makeProviderServiceLive(
packages/contracts/src/provider.test.ts+6−0
@@ -52,11 +52,17 @@ describe("ProviderSessionStartInput", () => { · 51 unmodified lines
5252 provider: "cursor",
5353 cwd: "/tmp/workspace",
5454 model: "composer-1.5",
55+ providerOptions: {
56+ cursor: {
57+ binaryPath: "/usr/local/bin/agent",
58+ },
59+ },
5560 approvalPolicy: "on-request",
5661 sandboxMode: "workspace-write",
5762 });
5863 expect(parsed.provider).toBe("cursor");
5964 expect(parsed.model).toBe("composer-1.5");
65+ expect(parsed.providerOptions?.cursor?.binaryPath).toBe("/usr/local/bin/agent");
6066 expect(parsed.approvalPolicy).toBe("on-request");
6167 expect(parsed.sandboxMode).toBe("workspace-write");
6268 });
packages/contracts/src/provider.ts+6−0
@@ -62,9 +62,15 @@ export const ClaudeCodeProviderStartOptions = Schema.Struct({ · 61 unmodified lines
6262 });
6363 export type ClaudeCodeProviderStartOptions = typeof ClaudeCodeProviderStartOptions.Type;
6464
65+export const CursorProviderStartOptions = Schema.Struct({
66+ binaryPath: Schema.optional(TrimmedNonEmptyStringSchema),
67+});
68+export type CursorProviderStartOptions = typeof CursorProviderStartOptions.Type;
69+
6570 export const ProviderStartOptions = Schema.Struct({
6671 codex: Schema.optional(CodexProviderStartOptions),
6772 claudeCode: Schema.optional(ClaudeCodeProviderStartOptions),
73+ cursor: Schema.optional(CursorProviderStartOptions),
6874 });
6975 export type ProviderStartOptions = typeof ProviderStartOptions.Type;
7076
packages/contracts/src/providerRuntime.ts+526−9
@@ -9,6 +9,7 @@ import { · 8 unmodified lines
99 RuntimeItemId,
1010 RuntimeRequestId,
1111 RuntimeSessionId,
12+ RuntimeTaskId,
1213 ThreadId,
1314 TrimmedNonEmptyString,
1415 TurnId,
@@ -25,6 +26,9 @@ export const RuntimeEventRawSource = Schema.Literals([ · 10 unmodified lines
2526 "claude.sdk.message",
2627 "claude.sdk.permission",
2728 "codex.sdk.thread-event",
29+ "cursor.acp.notification",
30+ "cursor.acp.request",
31+ "cursor.acp.response",
2832 ]);
2933 export type RuntimeEventRawSource = typeof RuntimeEventRawSource.Type;
3034
@@ -48,6 +52,26 @@ export const ProviderRefs = Schema.Struct({ · 17 unmodified lines
4852 });
4953 export type ProviderRefs = typeof ProviderRefs.Type;
5054
55+export const RuntimeSessionState = Schema.Literals([
56+ "starting",
57+ "ready",
58+ "running",
59+ "waiting",
60+ "stopped",
61+ "error",
62+]);
63+export type RuntimeSessionState = typeof RuntimeSessionState.Type;
64+
65+export const RuntimeThreadState = Schema.Literals([
66+ "active",
67+ "idle",
68+ "archived",
69+ "closed",
70+ "compacted",
71+ "error",
72+]);
73+export type RuntimeThreadState = typeof RuntimeThreadState.Type;
74+
5175 export const RuntimeTurnState = Schema.Literals([
5276 "completed",
5377 "failed",
@@ -94,6 +118,12 @@ export const CanonicalItemType = Schema.Literals([ · 40 unmodified lines
94118 "file_change",
95119 "mcp_tool_call",
96120 "dynamic_tool_call",
121+ "collab_agent_tool_call",
122+ "web_search",
123+ "image_view",
124+ "review_entered",
125+ "review_exited",
126+ "context_compaction",
97127 "error",
98128 "unknown",
99129 ]);
@@ -106,17 +136,30 @@ export const CanonicalRequestType = Schema.Literals([ · 6 unmodified lines
106136 "exec_command_approval",
107137 "tool_user_input",
108138 "dynamic_tool_call",
139+ "auth_tokens_refresh",
109140 "unknown",
110141 ]);
111142 export type CanonicalRequestType = typeof CanonicalRequestType.Type;
112143
113144 export const ProviderRuntimeEventType = Schema.Literals([
114145 "session.started",
146+ "session.configured",
147+ "session.state.changed",
115148 "session.exited",
116149 "thread.started",
150+ "thread.state.changed",
151+ "thread.metadata.updated",
152+ "thread.token-usage.updated",
153+ "thread.realtime.started",
154+ "thread.realtime.item-added",
155+ "thread.realtime.audio.delta",
156+ "thread.realtime.error",
157+ "thread.realtime.closed",
117158 "turn.started",
118159 "turn.completed",
160+ "turn.aborted",
119161 "turn.plan.updated",
162+ "turn.diff.updated",
120163 "item.started",
121164 "item.updated",
122165 "item.completed",
@@ -125,17 +168,46 @@ export const ProviderRuntimeEventType = Schema.Literals([ · 2 unmodified lines
125168 "request.resolved",
126169 "user-input.requested",
127170 "user-input.resolved",
171+ "task.started",
172+ "task.progress",
173+ "task.completed",
174+ "hook.started",
175+ "hook.progress",
176+ "hook.completed",
177+ "tool.progress",
178+ "tool.summary",
179+ "auth.status",
180+ "account.updated",
181+ "account.rate-limits.updated",
182+ "mcp.status.updated",
183+ "mcp.oauth.completed",
184+ "model.rerouted",
185+ "config.warning",
186+ "deprecation.notice",
187+ "files.persisted",
128188 "runtime.warning",
129189 "runtime.error",
130190 ]);
131191 export type ProviderRuntimeEventType = typeof ProviderRuntimeEventType.Type;
132192
133193 export const SessionStartedType = Schema.Literal("session.started");
194+export const SessionConfiguredType = Schema.Literal("session.configured");
195+export const SessionStateChangedType = Schema.Literal("session.state.changed");
134196 export const SessionExitedType = Schema.Literal("session.exited");
135197 export const ThreadStartedType = Schema.Literal("thread.started");
198+export const ThreadStateChangedType = Schema.Literal("thread.state.changed");
199+export const ThreadMetadataUpdatedType = Schema.Literal("thread.metadata.updated");
200+export const ThreadTokenUsageUpdatedType = Schema.Literal("thread.token-usage.updated");
201+export const ThreadRealtimeStartedType = Schema.Literal("thread.realtime.started");
202+export const ThreadRealtimeItemAddedType = Schema.Literal("thread.realtime.item-added");
203+export const ThreadRealtimeAudioDeltaType = Schema.Literal("thread.realtime.audio.delta");
204+export const ThreadRealtimeErrorType = Schema.Literal("thread.realtime.error");
205+export const ThreadRealtimeClosedType = Schema.Literal("thread.realtime.closed");
136206 export const TurnStartedType = Schema.Literal("turn.started");
137207 export const TurnCompletedType = Schema.Literal("turn.completed");
208+export const TurnAbortedType = Schema.Literal("turn.aborted");
138209 export const TurnPlanUpdatedType = Schema.Literal("turn.plan.updated");
210+export const TurnDiffUpdatedType = Schema.Literal("turn.diff.updated");
139211 export const ItemStartedType = Schema.Literal("item.started");
140212 export const ItemUpdatedType = Schema.Literal("item.updated");
141213 export const ItemCompletedType = Schema.Literal("item.completed");
@@ -144,6 +216,23 @@ export const RequestOpenedType = Schema.Literal("request.opened"); · 2 unmodified lines
144216 export const RequestResolvedType = Schema.Literal("request.resolved");
145217 export const UserInputRequestedType = Schema.Literal("user-input.requested");
146218 export const UserInputResolvedType = Schema.Literal("user-input.resolved");
219+export const TaskStartedType = Schema.Literal("task.started");
220+export const TaskProgressType = Schema.Literal("task.progress");
221+export const TaskCompletedType = Schema.Literal("task.completed");
222+export const HookStartedType = Schema.Literal("hook.started");
223+export const HookProgressType = Schema.Literal("hook.progress");
224+export const HookCompletedType = Schema.Literal("hook.completed");
225+export const ToolProgressType = Schema.Literal("tool.progress");
226+export const ToolSummaryType = Schema.Literal("tool.summary");
227+export const AuthStatusType = Schema.Literal("auth.status");
228+export const AccountUpdatedType = Schema.Literal("account.updated");
229+export const AccountRateLimitsUpdatedType = Schema.Literal("account.rate-limits.updated");
230+export const McpStatusUpdatedType = Schema.Literal("mcp.status.updated");
231+export const McpOauthCompletedType = Schema.Literal("mcp.oauth.completed");
232+export const ModelReroutedType = Schema.Literal("model.rerouted");
233+export const ConfigWarningType = Schema.Literal("config.warning");
234+export const DeprecationNoticeType = Schema.Literal("deprecation.notice");
235+export const FilesPersistedType = Schema.Literal("files.persisted");
147236 export const RuntimeWarningType = Schema.Literal("runtime.warning");
148237 export const RuntimeErrorType = Schema.Literal("runtime.error");
149238
@@ -167,6 +256,18 @@ export const SessionStartedPayload = Schema.Struct({ · 17 unmodified lines
167256 });
168257 export type SessionStartedPayload = typeof SessionStartedPayload.Type;
169258
259+export const SessionConfiguredPayload = Schema.Struct({
260+ config: UnknownRecordSchema,
261+});
262+export type SessionConfiguredPayload = typeof SessionConfiguredPayload.Type;
263+
264+export const SessionStateChangedPayload = Schema.Struct({
265+ state: RuntimeSessionState,
266+ reason: Schema.optional(TrimmedNonEmptyStringSchema),
267+ detail: Schema.optional(Schema.Unknown),
268+});
269+export type SessionStateChangedPayload = typeof SessionStateChangedPayload.Type;
270+
170271 export const SessionExitedPayload = Schema.Struct({
171272 reason: Schema.optional(TrimmedNonEmptyStringSchema),
172273 recoverable: Schema.optional(Schema.Boolean),
@@ -179,6 +280,48 @@ export const ThreadStartedPayload = Schema.Struct({ · 6 unmodified lines
179280 });
180281 export type ThreadStartedPayload = typeof ThreadStartedPayload.Type;
181282
283+export const ThreadStateChangedPayload = Schema.Struct({
284+ state: RuntimeThreadState,
285+ detail: Schema.optional(Schema.Unknown),
286+});
287+export type ThreadStateChangedPayload = typeof ThreadStateChangedPayload.Type;
288+
289+export const ThreadMetadataUpdatedPayload = Schema.Struct({
290+ name: Schema.optional(TrimmedNonEmptyStringSchema),
291+ metadata: Schema.optional(UnknownRecordSchema),
292+});
293+export type ThreadMetadataUpdatedPayload = typeof ThreadMetadataUpdatedPayload.Type;
294+
295+export const ThreadTokenUsageUpdatedPayload = Schema.Struct({
296+ usage: Schema.Unknown,
297+});
298+export type ThreadTokenUsageUpdatedPayload = typeof ThreadTokenUsageUpdatedPayload.Type;
299+
300+export const ThreadRealtimeStartedPayload = Schema.Struct({
301+ realtimeSessionId: Schema.optional(TrimmedNonEmptyStringSchema),
302+});
303+export type ThreadRealtimeStartedPayload = typeof ThreadRealtimeStartedPayload.Type;
304+
305+export const ThreadRealtimeItemAddedPayload = Schema.Struct({
306+ item: Schema.Unknown,
307+});
308+export type ThreadRealtimeItemAddedPayload = typeof ThreadRealtimeItemAddedPayload.Type;
309+
310+export const ThreadRealtimeAudioDeltaPayload = Schema.Struct({
311+ audio: Schema.Unknown,
312+});
313+export type ThreadRealtimeAudioDeltaPayload = typeof ThreadRealtimeAudioDeltaPayload.Type;
314+
315+export const ThreadRealtimeErrorPayload = Schema.Struct({
316+ message: TrimmedNonEmptyStringSchema,
317+});
318+export type ThreadRealtimeErrorPayload = typeof ThreadRealtimeErrorPayload.Type;
319+
320+export const ThreadRealtimeClosedPayload = Schema.Struct({
321+ reason: Schema.optional(TrimmedNonEmptyStringSchema),
322+});
323+export type ThreadRealtimeClosedPayload = typeof ThreadRealtimeClosedPayload.Type;
324+
182325 export const TurnStartedPayload = Schema.Struct({
183326 model: Schema.optional(TrimmedNonEmptyStringSchema),
184327 effort: Schema.optional(TrimmedNonEmptyStringSchema),
@@ -195,6 +338,11 @@ export const TurnCompletedPayload = Schema.Struct({ · 10 unmodified lines
195338 });
196339 export type TurnCompletedPayload = typeof TurnCompletedPayload.Type;
197340
341+export const TurnAbortedPayload = Schema.Struct({
342+ reason: TrimmedNonEmptyStringSchema,
343+});
344+export type TurnAbortedPayload = typeof TurnAbortedPayload.Type;
345+
198346 export const RuntimePlanStep = Schema.Struct({
199347 step: TrimmedNonEmptyStringSchema,
200348 status: RuntimePlanStepStatus,
@@ -207,6 +355,11 @@ export const TurnPlanUpdatedPayload = Schema.Struct({ · 6 unmodified lines
207355 });
208356 export type TurnPlanUpdatedPayload = typeof TurnPlanUpdatedPayload.Type;
209357
358+export const TurnDiffUpdatedPayload = Schema.Struct({
359+ unifiedDiff: Schema.String,
360+});
361+export type TurnDiffUpdatedPayload = typeof TurnDiffUpdatedPayload.Type;
362+
210363 export const ItemLifecyclePayload = Schema.Struct({
211364 itemType: CanonicalItemType,
212365 status: Schema.optional(RuntimeItemStatus),
@@ -262,6 +415,136 @@ export const UserInputResolvedPayload = Schema.Struct({ · 49 unmodified lines
262415 });
263416 export type UserInputResolvedPayload = typeof UserInputResolvedPayload.Type;
264417
418+export const TaskStartedPayload = Schema.Struct({
419+ taskId: RuntimeTaskId,
420+ description: Schema.optional(TrimmedNonEmptyStringSchema),
421+ taskType: Schema.optional(TrimmedNonEmptyStringSchema),
422+});
423+export type TaskStartedPayload = typeof TaskStartedPayload.Type;
424+
425+export const TaskProgressPayload = Schema.Struct({
426+ taskId: RuntimeTaskId,
427+ description: TrimmedNonEmptyStringSchema,
428+ usage: Schema.optional(Schema.Unknown),
429+ lastToolName: Schema.optional(TrimmedNonEmptyStringSchema),
430+});
431+export type TaskProgressPayload = typeof TaskProgressPayload.Type;
432+
433+export const TaskCompletedPayload = Schema.Struct({
434+ taskId: RuntimeTaskId,
435+ status: Schema.Literals(["completed", "failed", "stopped"]),
436+ summary: Schema.optional(TrimmedNonEmptyStringSchema),
437+ usage: Schema.optional(Schema.Unknown),
438+});
439+export type TaskCompletedPayload = typeof TaskCompletedPayload.Type;
440+
441+export const HookStartedPayload = Schema.Struct({
442+ hookId: TrimmedNonEmptyStringSchema,
443+ hookName: TrimmedNonEmptyStringSchema,
444+ hookEvent: TrimmedNonEmptyStringSchema,
445+});
446+export type HookStartedPayload = typeof HookStartedPayload.Type;
447+
448+export const HookProgressPayload = Schema.Struct({
449+ hookId: TrimmedNonEmptyStringSchema,
450+ output: Schema.optional(Schema.String),
451+ stdout: Schema.optional(Schema.String),
452+ stderr: Schema.optional(Schema.String),
453+});
454+export type HookProgressPayload = typeof HookProgressPayload.Type;
455+
456+export const HookCompletedPayload = Schema.Struct({
457+ hookId: TrimmedNonEmptyStringSchema,
458+ outcome: Schema.Literals(["success", "error", "cancelled"]),
459+ output: Schema.optional(Schema.String),
460+ stdout: Schema.optional(Schema.String),
461+ stderr: Schema.optional(Schema.String),
462+ exitCode: Schema.optional(Schema.Int),
463+});
464+export type HookCompletedPayload = typeof HookCompletedPayload.Type;
465+
466+export const ToolProgressPayload = Schema.Struct({
467+ toolUseId: Schema.optional(TrimmedNonEmptyStringSchema),
468+ toolName: Schema.optional(TrimmedNonEmptyStringSchema),
469+ summary: Schema.optional(TrimmedNonEmptyStringSchema),
470+ elapsedSeconds: Schema.optional(Schema.Number),
471+});
472+export type ToolProgressPayload = typeof ToolProgressPayload.Type;
473+
474+export const ToolSummaryPayload = Schema.Struct({
475+ summary: TrimmedNonEmptyStringSchema,
476+ precedingToolUseIds: Schema.optional(Schema.Array(TrimmedNonEmptyStringSchema)),
477+});
478+export type ToolSummaryPayload = typeof ToolSummaryPayload.Type;
479+
480+export const AuthStatusPayload = Schema.Struct({
481+ isAuthenticating: Schema.optional(Schema.Boolean),
482+ output: Schema.optional(Schema.Array(Schema.String)),
483+ error: Schema.optional(TrimmedNonEmptyStringSchema),
484+});
485+export type AuthStatusPayload = typeof AuthStatusPayload.Type;
486+
487+export const AccountUpdatedPayload = Schema.Struct({
488+ account: Schema.Unknown,
489+});
490+export type AccountUpdatedPayload = typeof AccountUpdatedPayload.Type;
491+
492+export const AccountRateLimitsUpdatedPayload = Schema.Struct({
493+ rateLimits: Schema.Unknown,
494+});
495+export type AccountRateLimitsUpdatedPayload = typeof AccountRateLimitsUpdatedPayload.Type;
496+
497+export const McpStatusUpdatedPayload = Schema.Struct({
498+ status: Schema.Unknown,
499+});
500+export type McpStatusUpdatedPayload = typeof McpStatusUpdatedPayload.Type;
501+
502+export const McpOauthCompletedPayload = Schema.Struct({
503+ success: Schema.Boolean,
504+ name: Schema.optional(TrimmedNonEmptyStringSchema),
505+ error: Schema.optional(TrimmedNonEmptyStringSchema),
506+});
507+export type McpOauthCompletedPayload = typeof McpOauthCompletedPayload.Type;
508+
509+export const ModelReroutedPayload = Schema.Struct({
510+ fromModel: TrimmedNonEmptyStringSchema,
511+ toModel: TrimmedNonEmptyStringSchema,
512+ reason: TrimmedNonEmptyStringSchema,
513+});
514+export type ModelReroutedPayload = typeof ModelReroutedPayload.Type;
515+
516+export const ConfigWarningPayload = Schema.Struct({
517+ summary: TrimmedNonEmptyStringSchema,
518+ details: Schema.optional(TrimmedNonEmptyStringSchema),
519+ path: Schema.optional(TrimmedNonEmptyStringSchema),
520+ range: Schema.optional(Schema.Unknown),
521+});
522+export type ConfigWarningPayload = typeof ConfigWarningPayload.Type;
523+
524+export const DeprecationNoticePayload = Schema.Struct({
525+ summary: TrimmedNonEmptyStringSchema,
526+ details: Schema.optional(TrimmedNonEmptyStringSchema),
527+});
528+export type DeprecationNoticePayload = typeof DeprecationNoticePayload.Type;
529+
530+export const FilesPersistedPayload = Schema.Struct({
531+ files: Schema.Array(
532+ Schema.Struct({
533+ filename: TrimmedNonEmptyStringSchema,
534+ fileId: TrimmedNonEmptyStringSchema,
535+ }),
536+ ),
537+ failed: Schema.optional(
538+ Schema.Array(
539+ Schema.Struct({
540+ filename: TrimmedNonEmptyStringSchema,
541+ error: TrimmedNonEmptyStringSchema,
542+ }),
543+ ),
544+ ),
545+});
546+export type FilesPersistedPayload = typeof FilesPersistedPayload.Type;
547+
265548 export const RuntimeWarningPayload = Schema.Struct({
266549 message: TrimmedNonEmptyStringSchema,
267550 detail: Schema.optional(Schema.Unknown),
@@ -282,6 +565,20 @@ export const ProviderRuntimeSessionStartedEvent = Schema.Struct({ · 14 unmodified lines
282565 });
283566 export type ProviderRuntimeSessionStartedEvent = typeof ProviderRuntimeSessionStartedEvent.Type;
284567
568+export const ProviderRuntimeSessionConfiguredEvent = Schema.Struct({
569+ ...ProviderRuntimeEventBase.fields,
570+ type: SessionConfiguredType,
571+ payload: SessionConfiguredPayload,
572+});
573+export type ProviderRuntimeSessionConfiguredEvent = typeof ProviderRuntimeSessionConfiguredEvent.Type;
574+
575+export const ProviderRuntimeSessionStateChangedEvent = Schema.Struct({
576+ ...ProviderRuntimeEventBase.fields,
577+ type: SessionStateChangedType,
578+ payload: SessionStateChangedPayload,
579+});
580+export type ProviderRuntimeSessionStateChangedEvent = typeof ProviderRuntimeSessionStateChangedEvent.Type;
581+
285582 export const ProviderRuntimeSessionExitedEvent = Schema.Struct({
286583 ...ProviderRuntimeEventBase.fields,
287584 type: SessionExitedType,
@@ -296,6 +593,68 @@ export const ProviderRuntimeThreadStartedEvent = Schema.Struct({ · 8 unmodified lines
296593 });
297594 export type ProviderRuntimeThreadStartedEvent = typeof ProviderRuntimeThreadStartedEvent.Type;
298595
596+export const ProviderRuntimeThreadStateChangedEvent = Schema.Struct({
597+ ...ProviderRuntimeEventBase.fields,
598+ type: ThreadStateChangedType,
599+ payload: ThreadStateChangedPayload,
600+});
601+export type ProviderRuntimeThreadStateChangedEvent = typeof ProviderRuntimeThreadStateChangedEvent.Type;
602+
603+export const ProviderRuntimeThreadMetadataUpdatedEvent = Schema.Struct({
604+ ...ProviderRuntimeEventBase.fields,
605+ type: ThreadMetadataUpdatedType,
606+ payload: ThreadMetadataUpdatedPayload,
607+});
608+export type ProviderRuntimeThreadMetadataUpdatedEvent = typeof ProviderRuntimeThreadMetadataUpdatedEvent.Type;
609+
610+export const ProviderRuntimeThreadTokenUsageUpdatedEvent = Schema.Struct({
611+ ...ProviderRuntimeEventBase.fields,
612+ type: ThreadTokenUsageUpdatedType,
613+ payload: ThreadTokenUsageUpdatedPayload,
614+});
615+export type ProviderRuntimeThreadTokenUsageUpdatedEvent =
616+ typeof ProviderRuntimeThreadTokenUsageUpdatedEvent.Type;
617+
618+export const ProviderRuntimeThreadRealtimeStartedEvent = Schema.Struct({
619+ ...ProviderRuntimeEventBase.fields,
620+ type: ThreadRealtimeStartedType,
621+ payload: ThreadRealtimeStartedPayload,
622+});
623+export type ProviderRuntimeThreadRealtimeStartedEvent =
624+ typeof ProviderRuntimeThreadRealtimeStartedEvent.Type;
625+
626+export const ProviderRuntimeThreadRealtimeItemAddedEvent = Schema.Struct({
627+ ...ProviderRuntimeEventBase.fields,
628+ type: ThreadRealtimeItemAddedType,
629+ payload: ThreadRealtimeItemAddedPayload,
630+});
631+export type ProviderRuntimeThreadRealtimeItemAddedEvent =
632+ typeof ProviderRuntimeThreadRealtimeItemAddedEvent.Type;
633+
634+export const ProviderRuntimeThreadRealtimeAudioDeltaEvent = Schema.Struct({
635+ ...ProviderRuntimeEventBase.fields,
636+ type: ThreadRealtimeAudioDeltaType,
637+ payload: ThreadRealtimeAudioDeltaPayload,
638+});
639+export type ProviderRuntimeThreadRealtimeAudioDeltaEvent =
640+ typeof ProviderRuntimeThreadRealtimeAudioDeltaEvent.Type;
641+
642+export const ProviderRuntimeThreadRealtimeErrorEvent = Schema.Struct({
643+ ...ProviderRuntimeEventBase.fields,
644+ type: ThreadRealtimeErrorType,
645+ payload: ThreadRealtimeErrorPayload,
646+});
647+export type ProviderRuntimeThreadRealtimeErrorEvent =
648+ typeof ProviderRuntimeThreadRealtimeErrorEvent.Type;
649+
650+export const ProviderRuntimeThreadRealtimeClosedEvent = Schema.Struct({
651+ ...ProviderRuntimeEventBase.fields,
652+ type: ThreadRealtimeClosedType,
653+ payload: ThreadRealtimeClosedPayload,
654+});
655+export type ProviderRuntimeThreadRealtimeClosedEvent =
656+ typeof ProviderRuntimeThreadRealtimeClosedEvent.Type;
657+
299658 export const ProviderRuntimeTurnStartedEvent = Schema.Struct({
300659 ...ProviderRuntimeEventBase.fields,
301660 type: TurnStartedType,
@@ -310,6 +669,13 @@ export const ProviderRuntimeTurnCompletedEvent = Schema.Struct({ · 8 unmodified lines
310669 });
311670 export type ProviderRuntimeTurnCompletedEvent = typeof ProviderRuntimeTurnCompletedEvent.Type;
312671
672+export const ProviderRuntimeTurnAbortedEvent = Schema.Struct({
673+ ...ProviderRuntimeEventBase.fields,
674+ type: TurnAbortedType,
675+ payload: TurnAbortedPayload,
676+});
677+export type ProviderRuntimeTurnAbortedEvent = typeof ProviderRuntimeTurnAbortedEvent.Type;
678+
313679 export const ProviderRuntimeTurnPlanUpdatedEvent = Schema.Struct({
314680 ...ProviderRuntimeEventBase.fields,
315681 type: TurnPlanUpdatedType,
@@ -317,6 +683,13 @@ export const ProviderRuntimeTurnPlanUpdatedEvent = Schema.Struct({ · 1 unmodified line
317683 });
318684 export type ProviderRuntimeTurnPlanUpdatedEvent = typeof ProviderRuntimeTurnPlanUpdatedEvent.Type;
319685
686+export const ProviderRuntimeTurnDiffUpdatedEvent = Schema.Struct({
687+ ...ProviderRuntimeEventBase.fields,
688+ type: TurnDiffUpdatedType,
689+ payload: TurnDiffUpdatedPayload,
690+});
691+export type ProviderRuntimeTurnDiffUpdatedEvent = typeof ProviderRuntimeTurnDiffUpdatedEvent.Type;
692+
320693 export const ProviderRuntimeItemStartedEvent = Schema.Struct({
321694 ...ProviderRuntimeEventBase.fields,
322695 type: ItemStartedType,
@@ -372,8 +745,127 @@ export const ProviderRuntimeUserInputResolvedEvent = Schema.Struct({ · 49 unmodified lines
372745 type: UserInputResolvedType,
373746 payload: UserInputResolvedPayload,
374747 });
375−export type ProviderRuntimeUserInputResolvedEvent =
376− typeof ProviderRuntimeUserInputResolvedEvent.Type;
748+export type ProviderRuntimeUserInputResolvedEvent = typeof ProviderRuntimeUserInputResolvedEvent.Type;
749+
750+export const ProviderRuntimeTaskStartedEvent = Schema.Struct({
751+ ...ProviderRuntimeEventBase.fields,
752+ type: TaskStartedType,
753+ payload: TaskStartedPayload,
754+});
755+export type ProviderRuntimeTaskStartedEvent = typeof ProviderRuntimeTaskStartedEvent.Type;
756+
757+export const ProviderRuntimeTaskProgressEvent = Schema.Struct({
758+ ...ProviderRuntimeEventBase.fields,
759+ type: TaskProgressType,
760+ payload: TaskProgressPayload,
761+});
762+export type ProviderRuntimeTaskProgressEvent = typeof ProviderRuntimeTaskProgressEvent.Type;
763+
764+export const ProviderRuntimeTaskCompletedEvent = Schema.Struct({
765+ ...ProviderRuntimeEventBase.fields,
766+ type: TaskCompletedType,
767+ payload: TaskCompletedPayload,
768+});
769+export type ProviderRuntimeTaskCompletedEvent = typeof ProviderRuntimeTaskCompletedEvent.Type;
770+
771+export const ProviderRuntimeHookStartedEvent = Schema.Struct({
772+ ...ProviderRuntimeEventBase.fields,
773+ type: HookStartedType,
774+ payload: HookStartedPayload,
775+});
776+export type ProviderRuntimeHookStartedEvent = typeof ProviderRuntimeHookStartedEvent.Type;
777+
778+export const ProviderRuntimeHookProgressEvent = Schema.Struct({
779+ ...ProviderRuntimeEventBase.fields,
780+ type: HookProgressType,
781+ payload: HookProgressPayload,
782+});
783+export type ProviderRuntimeHookProgressEvent = typeof ProviderRuntimeHookProgressEvent.Type;
784+
785+export const ProviderRuntimeHookCompletedEvent = Schema.Struct({
786+ ...ProviderRuntimeEventBase.fields,
787+ type: HookCompletedType,
788+ payload: HookCompletedPayload,
789+});
790+export type ProviderRuntimeHookCompletedEvent = typeof ProviderRuntimeHookCompletedEvent.Type;
791+
792+export const ProviderRuntimeToolProgressEvent = Schema.Struct({
793+ ...ProviderRuntimeEventBase.fields,
794+ type: ToolProgressType,
795+ payload: ToolProgressPayload,
796+});
797+export type ProviderRuntimeToolProgressEvent = typeof ProviderRuntimeToolProgressEvent.Type;
798+
799+export const ProviderRuntimeToolSummaryEvent = Schema.Struct({
800+ ...ProviderRuntimeEventBase.fields,
801+ type: ToolSummaryType,
802+ payload: ToolSummaryPayload,
803+});
804+export type ProviderRuntimeToolSummaryEvent = typeof ProviderRuntimeToolSummaryEvent.Type;
805+
806+export const ProviderRuntimeAuthStatusEvent = Schema.Struct({
807+ ...ProviderRuntimeEventBase.fields,
808+ type: AuthStatusType,
809+ payload: AuthStatusPayload,
810+});
811+export type ProviderRuntimeAuthStatusEvent = typeof ProviderRuntimeAuthStatusEvent.Type;
812+
813+export const ProviderRuntimeAccountUpdatedEvent = Schema.Struct({
814+ ...ProviderRuntimeEventBase.fields,
815+ type: AccountUpdatedType,
816+ payload: AccountUpdatedPayload,
817+});
818+export type ProviderRuntimeAccountUpdatedEvent = typeof ProviderRuntimeAccountUpdatedEvent.Type;
819+
820+export const ProviderRuntimeAccountRateLimitsUpdatedEvent = Schema.Struct({
821+ ...ProviderRuntimeEventBase.fields,
822+ type: AccountRateLimitsUpdatedType,
823+ payload: AccountRateLimitsUpdatedPayload,
824+});
825+export type ProviderRuntimeAccountRateLimitsUpdatedEvent =
826+ typeof ProviderRuntimeAccountRateLimitsUpdatedEvent.Type;
827+
828+export const ProviderRuntimeMcpStatusUpdatedEvent = Schema.Struct({
829+ ...ProviderRuntimeEventBase.fields,
830+ type: McpStatusUpdatedType,
831+ payload: McpStatusUpdatedPayload,
832+});
833+export type ProviderRuntimeMcpStatusUpdatedEvent = typeof ProviderRuntimeMcpStatusUpdatedEvent.Type;
834+
835+export const ProviderRuntimeMcpOauthCompletedEvent = Schema.Struct({
836+ ...ProviderRuntimeEventBase.fields,
837+ type: McpOauthCompletedType,
838+ payload: McpOauthCompletedPayload,
839+});
840+export type ProviderRuntimeMcpOauthCompletedEvent = typeof ProviderRuntimeMcpOauthCompletedEvent.Type;
841+
842+export const ProviderRuntimeModelReroutedEvent = Schema.Struct({
843+ ...ProviderRuntimeEventBase.fields,
844+ type: ModelReroutedType,
845+ payload: ModelReroutedPayload,
846+});
847+export type ProviderRuntimeModelReroutedEvent = typeof ProviderRuntimeModelReroutedEvent.Type;
848+
849+export const ProviderRuntimeConfigWarningEvent = Schema.Struct({
850+ ...ProviderRuntimeEventBase.fields,
851+ type: ConfigWarningType,
852+ payload: ConfigWarningPayload,
853+});
854+export type ProviderRuntimeConfigWarningEvent = typeof ProviderRuntimeConfigWarningEvent.Type;
855+
856+export const ProviderRuntimeDeprecationNoticeEvent = Schema.Struct({
857+ ...ProviderRuntimeEventBase.fields,
858+ type: DeprecationNoticeType,
859+ payload: DeprecationNoticePayload,
860+});
861+export type ProviderRuntimeDeprecationNoticeEvent = typeof ProviderRuntimeDeprecationNoticeEvent.Type;
862+
863+export const ProviderRuntimeFilesPersistedEvent = Schema.Struct({
864+ ...ProviderRuntimeEventBase.fields,
865+ type: FilesPersistedType,
866+ payload: FilesPersistedPayload,
867+});
868+export type ProviderRuntimeFilesPersistedEvent = typeof ProviderRuntimeFilesPersistedEvent.Type;
377869
378870 export const ProviderRuntimeWarningEvent = Schema.Struct({
379871 ...ProviderRuntimeEventBase.fields,
@@ -391,11 +883,23 @@ export type ProviderRuntimeErrorEvent = typeof ProviderRuntimeErrorEvent.Type; · 11 unmodified lines
391883
392884 export const ProviderRuntimeEventV2 = Schema.Union([
393885 ProviderRuntimeSessionStartedEvent,
886+ ProviderRuntimeSessionConfiguredEvent,
887+ ProviderRuntimeSessionStateChangedEvent,
394888 ProviderRuntimeSessionExitedEvent,
395889 ProviderRuntimeThreadStartedEvent,
890+ ProviderRuntimeThreadStateChangedEvent,
891+ ProviderRuntimeThreadMetadataUpdatedEvent,
892+ ProviderRuntimeThreadTokenUsageUpdatedEvent,
893+ ProviderRuntimeThreadRealtimeStartedEvent,
894+ ProviderRuntimeThreadRealtimeItemAddedEvent,
895+ ProviderRuntimeThreadRealtimeAudioDeltaEvent,
896+ ProviderRuntimeThreadRealtimeErrorEvent,
897+ ProviderRuntimeThreadRealtimeClosedEvent,
396898 ProviderRuntimeTurnStartedEvent,
397899 ProviderRuntimeTurnCompletedEvent,
900+ ProviderRuntimeTurnAbortedEvent,
398901 ProviderRuntimeTurnPlanUpdatedEvent,
902+ ProviderRuntimeTurnDiffUpdatedEvent,
399903 ProviderRuntimeItemStartedEvent,
400904 ProviderRuntimeItemUpdatedEvent,
401905 ProviderRuntimeItemCompletedEvent,
@@ -404,6 +908,23 @@ export const ProviderRuntimeEventV2 = Schema.Union([ · 2 unmodified lines
404908 ProviderRuntimeRequestResolvedEvent,
405909 ProviderRuntimeUserInputRequestedEvent,
406910 ProviderRuntimeUserInputResolvedEvent,
911+ ProviderRuntimeTaskStartedEvent,
912+ ProviderRuntimeTaskProgressEvent,
913+ ProviderRuntimeTaskCompletedEvent,
914+ ProviderRuntimeHookStartedEvent,
915+ ProviderRuntimeHookProgressEvent,
916+ ProviderRuntimeHookCompletedEvent,
917+ ProviderRuntimeToolProgressEvent,
918+ ProviderRuntimeToolSummaryEvent,
919+ ProviderRuntimeAuthStatusEvent,
920+ ProviderRuntimeAccountUpdatedEvent,
921+ ProviderRuntimeAccountRateLimitsUpdatedEvent,
922+ ProviderRuntimeMcpStatusUpdatedEvent,
923+ ProviderRuntimeMcpOauthCompletedEvent,
924+ ProviderRuntimeModelReroutedEvent,
925+ ProviderRuntimeConfigWarningEvent,
926+ ProviderRuntimeDeprecationNoticeEvent,
927+ ProviderRuntimeFilesPersistedEvent,
407928 ProviderRuntimeWarningEvent,
408929 ProviderRuntimeErrorEvent,
409930 ]);
@@ -412,6 +933,7 @@ export type ProviderRuntimeEventV2 = typeof ProviderRuntimeEventV2.Type; · 2 unmodified lines
412933 export const ProviderRuntimeEvent = ProviderRuntimeEventV2;
413934 export type ProviderRuntimeEvent = ProviderRuntimeEventV2;
414935
936+// Compatibility aliases for call sites still importing legacy names.
415937 export const ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent;
416938 export type ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent;
417939 export const ProviderRuntimeMessageCompletedEvent = ProviderRuntimeItemCompletedEvent;
@@ -425,13 +947,8 @@ export type ProviderRuntimeApprovalRequestedEvent = ProviderRuntimeRequestOpened · 7 unmodified lines
425947 export const ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent;
426948 export type ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent;
427949
428−export const ProviderRuntimeToolKind = Schema.Literals([
429− "command_execution",
430− "file_change",
431− "mcp_tool_call",
432− "dynamic_tool_call",
433− "other",
434−]);
950+// Legacy helper aliases retained for adapters/tests.
951+export const ProviderRuntimeToolKind = Schema.Literals(["command", "file-change", "other"]);
435952 export type ProviderRuntimeToolKind = typeof ProviderRuntimeToolKind.Type;
436953
437954 export const ProviderRuntimeTurnStatus = RuntimeTurnState;