authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-01 10:17:56-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
logd36bbc177d29877a335cda8bbe6601727587cffe
tree54d8568c53394427c7d126a4d712edfad5679ce6
parentaeddb5759425de2f9fd589843a2392280d6c89ef

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

14 files changed, 3746 insertions(+), 535 deletions(-)

apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+54-40
......@@ -229,7 +229,7 @@ describe("ClaudeCodeAdapterLive", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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 {
1919} from "@anthropic-ai/claude-agent-sdk";
2020import {
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 {
7374}
7475
7576interface 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 {
8182
8283interface 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
188190 };
189191}
190192
191function classifyToolKind(toolName: string): ProviderRuntimeToolKind {
193function classifyToolItemType(toolName: string): CanonicalItemType {
192194 const normalized = toolName.toLowerCase();
193195 if (
194196 normalized.includes("bash") ||
......@@ -196,7 +198,7 @@ function classifyToolKind(toolName: string): ProviderRuntimeToolKind {
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 {
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
215function classifyRequestKind(toolName: string): "command" | "file-change" {
216 return classifyToolKind(toolName) === "command" ? "command" : "file-change";
220function classifyRequestType(toolName: string): CanonicalRequestType {
221 return classifyToolItemType(toolName) === "command_execution"
222 ? "command_execution_approval"
223 : "file_change_approval";
217224}
218225
219226function summarizeToolRequest(toolName: string, input: Record<string, unknown>): string {
......@@ -230,14 +237,18 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>):
230237 return `${toolName}: ${serialized.slice(0, 397)}...`;
231238}
232239
233function titleForTool(kind: ProviderRuntimeToolKind): string {
234 switch (kind) {
235 case "command":
240function 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
284295 return "failed";
285296}
286297
298function streamKindFromDeltaType(deltaType: string): "assistant_text" | "reasoning_text" {
299 return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text";
300}
301
287302function extractAssistantText(message: SDKMessage): string {
288303 if (message.type !== "assistant") {
289304 return "";
......@@ -540,6 +555,20 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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(
214214);
215215
216216lifecycleLayer("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) => {
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) => {
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 @@
77 * @module CodexAdapterLive
88 */
99import {
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 {
10099 return typeof value === "string" ? value : undefined;
101100}
102101
103function toTurnStatus(value: unknown): ProviderRuntimeTurnStatus | undefined {
102function asNumber(value: unknown): number | undefined {
103 return typeof value === "number" && Number.isFinite(value) ? value : undefined;
104}
105
106function 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 {
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 {
123126 .toLowerCase();
124127}
125128
126function 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 ");
129function 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
133function 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" };
149function 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
151178function itemDetail(
......@@ -174,80 +201,155 @@ function itemDetail(
174201 return undefined;
175202}
176203
177function mapMessageCompletedEvent(event: ProviderEvent): ProviderRuntimeEvent | undefined {
178 if (event.method !== "item/completed") {
179 return undefined;
204function 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;
225function 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;
236function 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;
260function 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
281function eventRawSource(event: ProviderEvent): ProviderRuntimeEvent["raw"]["source"] {
282 return event.kind === "request" ? "codex.app-server.request" : "codex.app-server.notification";
283}
284
285function 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
299function 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
210function mapToolEvent(event: ProviderEvent): ProviderRuntimeEvent | undefined {
211 if (event.method !== "item/started" && event.method !== "item/completed") {
212 return undefined;
213 }
319function 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
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
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
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
431899const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>
apps/server/src/provider/Layers/CursorAdapter.test.ts created+350
......@@ -0,0 +1,350 @@
1import { EventEmitter } from "node:events";
2import { PassThrough } from "node:stream";
3import readline from "node:readline";
4
5import { ApprovalRequestId } from "@t3tools/contracts";
6import { assert, describe, it } from "@effect/vitest";
7import { Effect, Fiber, Stream } from "effect";
8
9import { ProviderAdapterValidationError } from "../Errors.ts";
10import { CursorAdapter } from "../Services/CursorAdapter.ts";
11import { makeCursorAdapterLive } from "./CursorAdapter.ts";
12
13class 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
220describe("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 created+1378
......@@ -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 */
9import { randomUUID } from "node:crypto";
10import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
11import readline from "node:readline";
12
13import {
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";
25import { DateTime, Effect, Layer, Queue, Random, Schema, Stream } from "effect";
26
27import {
28 ProviderAdapterProcessError,
29 ProviderAdapterRequestError,
30 ProviderAdapterSessionClosedError,
31 ProviderAdapterSessionNotFoundError,
32 ProviderAdapterValidationError,
33 type ProviderAdapterError,
34} from "../Errors.ts";
35import {
36 CursorAdapter,
37 type CursorAdapterShape,
38 CursorAcpInitializeResult,
39 CursorAcpPermissionRequest,
40 CursorAcpSessionNewResult,
41 CursorAcpSessionPromptResult,
42 CursorAcpSessionUpdateNotification,
43} from "../Services/CursorAdapter.ts";
44import { makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
45
46const PROVIDER = "cursor" as const;
47const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
48
49interface CursorResumeState {
50 readonly acpSessionId?: string;
51}
52
53interface 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
60interface PendingPermission {
61 readonly jsonRpcId: string | number;
62 readonly requestType: CanonicalRequestType;
63 readonly options: ReadonlyArray<{ optionId: string }>;
64}
65
66interface CursorTurnState {
67 readonly turnId: ReturnType<typeof ProviderTurnId.makeUnsafe>;
68 readonly assistantItemId: string;
69 readonly startedToolCalls: Set<string>;
70 readonly items: Array<unknown>;
71}
72
73interface 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
89export 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
98function 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
105function 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
127function 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
144function 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
151function asString(value: unknown): string | undefined {
152 return typeof value === "string" ? value : undefined;
153}
154
155function 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
174function 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
186function 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
201function 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
214function 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
224function 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
247function 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
254function 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
1374export const CursorAdapterLive = Layer.effect(CursorAdapter, makeCursorAdapter());
1375
1376export 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";
66
77import { ClaudeCodeAdapter, ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts";
88import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";
9import { CursorAdapter, CursorAdapterShape } from "../Services/CursorAdapter.ts";
910import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
1011import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
1112import { ProviderUnsupportedError } from "../Errors.ts";
......@@ -41,6 +42,21 @@ const fakeClaudeAdapter: ClaudeCodeAdapterShape = {
4142 streamEvents: Stream.empty,
4243};
4344
45const 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
4460const layer = it.layer(
4561 Layer.mergeAll(
4662 Layer.provide(
......@@ -48,6 +64,7 @@ const layer = it.layer(
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) => {
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 {
1717} from "../Services/ProviderAdapterRegistry.ts";
1818import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts";
1919import { CodexAdapter } from "../Services/CodexAdapter.ts";
20import { CursorAdapter } from "../Services/CursorAdapter.ts";
2021
2122export interface ProviderAdapterRegistryLiveOptions {
2223 readonly adapters?: ReadonlyArray<ProviderAdapterShape<ProviderAdapterError>>;
......@@ -27,7 +28,7 @@ const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOption
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 @@
11import { Schema } from "effect";
22import { describe, expect, it } from "vitest";
33
4import { CursorCliStreamEvent } from "./CursorAdapter.ts";
4import {
5 CursorAcpPermissionRequest,
6 CursorAcpSessionPromptResult,
7 CursorAcpSessionUpdateNotification,
8} from "./CursorAdapter.ts";
59
6describe("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",
10describe("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";
1410import type { ProviderAdapterError } from "../Errors.ts";
1511import type { ProviderAdapterShape } from "./ProviderAdapter.ts";
1612
17export const CursorCliSessionId = Schema.String.check(Schema.isNonEmpty());
18export type CursorCliSessionId = typeof CursorCliSessionId.Type;
13export const CursorAcpJsonRpcId = Schema.Union([Schema.String, Schema.Int]);
14export type CursorAcpJsonRpcId = typeof CursorAcpJsonRpcId.Type;
1915
20export const CursorCliTextContentPart = Schema.Struct({
16export const CursorAcpTextContent = Schema.Struct({
2117 type: Schema.Literal("text"),
2218 text: Schema.String,
2319});
24export type CursorCliTextContentPart = typeof CursorCliTextContentPart.Type;
20export type CursorAcpTextContent = typeof CursorAcpTextContent.Type;
2521
26export const CursorCliContentPart = Schema.Union([
27 CursorCliTextContentPart,
22export 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]);
32export type CursorCliContentPart = typeof CursorCliContentPart.Type;
33
34export const CursorCliUserMessage = Schema.Struct({
35 role: Schema.Literal("user"),
36 content: Schema.Array(CursorCliContentPart),
37});
38export type CursorCliUserMessage = typeof CursorCliUserMessage.Type;
39
40export const CursorCliAssistantMessage = Schema.Struct({
41 role: Schema.Literal("assistant"),
42 content: Schema.Array(CursorCliContentPart),
43});
44export type CursorCliAssistantMessage = typeof CursorCliAssistantMessage.Type;
45
46export const CursorCliToolCallResult = Schema.Struct({
47 success: Schema.optional(Schema.Unknown),
48 failure: Schema.optional(Schema.Unknown),
49 rejected: Schema.optional(Schema.Unknown),
50});
51export type CursorCliToolCallResult = typeof CursorCliToolCallResult.Type;
52
53export const CursorCliToolCallEntry = Schema.Struct({
54 args: Schema.optional(Schema.Unknown),
55 result: Schema.optional(CursorCliToolCallResult),
56});
57export type CursorCliToolCallEntry = typeof CursorCliToolCallEntry.Type;
58
59export const CursorCliFunctionToolCall = Schema.Struct({
60 name: Schema.String,
61 arguments: Schema.String,
62 result: Schema.optional(CursorCliToolCallResult),
63});
64export type CursorCliFunctionToolCall = typeof CursorCliFunctionToolCall.Type;
65
66export 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});
75export type CursorCliToolCallPayload = typeof CursorCliToolCallPayload.Type;
76
77export 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});
83export type CursorCliTokenUsage = typeof CursorCliTokenUsage.Type;
84
85const CursorCliTimestampMs = {
86 timestamp_ms: Schema.optional(Schema.Number),
87} as const;
88
89export 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});
99export type CursorCliSystemInitEvent = typeof CursorCliSystemInitEvent.Type;
100
101export const CursorCliUserEvent = Schema.Struct({
102 type: Schema.Literal("user"),
103 message: CursorCliUserMessage,
104 session_id: CursorCliSessionId,
105 ...CursorCliTimestampMs,
106});
107export type CursorCliUserEvent = typeof CursorCliUserEvent.Type;
108
109export const CursorCliAssistantEvent = Schema.Struct({
110 type: Schema.Literal("assistant"),
111 message: CursorCliAssistantMessage,
112 session_id: CursorCliSessionId,
113 ...CursorCliTimestampMs,
114});
115export type CursorCliAssistantEvent = typeof CursorCliAssistantEvent.Type;
116
117export 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});
124export type CursorCliThinkingDeltaEvent = typeof CursorCliThinkingDeltaEvent.Type;
125
126export const CursorCliThinkingCompletedEvent = Schema.Struct({
127 type: Schema.Literal("thinking"),
128 subtype: Schema.Literal("completed"),
129 session_id: CursorCliSessionId,
130 ...CursorCliTimestampMs,
55export type CursorAcpSessionUpdate = typeof CursorAcpSessionUpdate.Type;
56
57export 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});
132export type CursorCliThinkingCompletedEvent = typeof CursorCliThinkingCompletedEvent.Type;
65export type CursorAcpSessionUpdateNotification = typeof CursorAcpSessionUpdateNotification.Type;
13366
134export 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,
67export const CursorAcpPermissionOption = Schema.Struct({
68 optionId: Schema.String,
69 name: Schema.optional(Schema.String),
70 kind: Schema.optional(Schema.String),
14171});
142export type CursorCliToolCallStartedEvent = typeof CursorCliToolCallStartedEvent.Type;
72export type CursorAcpPermissionOption = typeof CursorAcpPermissionOption.Type;
14373
144export 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,
74export 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});
152export type CursorCliToolCallCompletedEvent = typeof CursorCliToolCallCompletedEvent.Type;
84export type CursorAcpPermissionRequest = typeof CursorAcpPermissionRequest.Type;
15385
154export 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,
86export 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});
166export type CursorCliResultSuccessEvent = typeof CursorCliResultSuccessEvent.Type;
91export type CursorAcpInitializeResult = typeof CursorAcpInitializeResult.Type;
16792
168export const CursorCliConnectionEvent = Schema.Struct({
169 type: Schema.Literal("connection"),
170 subtype: Schema.Literals(["reconnecting", "reconnected"]),
171 session_id: Schema.optional(CursorCliSessionId),
172 ...CursorCliTimestampMs,
93export const CursorAcpSessionNewResult = Schema.Struct({
94 sessionId: Schema.String,
95 modes: Schema.optional(Schema.Unknown),
17396});
174export type CursorCliConnectionEvent = typeof CursorCliConnectionEvent.Type;
97export type CursorAcpSessionNewResult = typeof CursorAcpSessionNewResult.Type;
17598
176export const CursorCliRetryEvent = Schema.Struct({
177 type: Schema.Literal("retry"),
178 subtype: Schema.Literals(["starting", "resuming"]),
179 session_id: Schema.optional(CursorCliSessionId),
180 ...CursorCliTimestampMs,
99export const CursorAcpSessionPromptResult = Schema.Struct({
100 stopReason: Schema.optional(Schema.String),
181101});
182export type CursorCliRetryEvent = typeof CursorCliRetryEvent.Type;
183
184export 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]);
196export type CursorCliStreamEvent = typeof CursorCliStreamEvent.Type;
102export 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 */
204export interface CursorAdapterShape
205 extends Omit<ProviderAdapterShape<ProviderAdapterError>, "provider"> {
104export interface CursorAdapterShape extends ProviderAdapterShape<ProviderAdapterError> {
206105 readonly provider: "cursor";
207106}
208107
209/**
210 * CursorAdapter - Service tag for Cursor provider adapter operations.
211 */
212108export 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
2020import { ProviderUnsupportedError } from "./provider/Errors";
2121import { makeClaudeCodeAdapterLive } from "./provider/Layers/ClaudeCodeAdapter";
2222import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter";
23import { makeCursorAdapterLive } from "./provider/Layers/CursorAdapter";
2324import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";
2425import { makeProviderServiceLive } from "./provider/Layers/ProviderService";
2526import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory";
......@@ -60,8 +61,11 @@ export function makeServerProviderLayer(): Layer.Layer<
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
......@@ -52,11 +52,17 @@ describe("ProviderSessionStartInput", () => {
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
......@@ -62,9 +62,15 @@ export const ClaudeCodeProviderStartOptions = Schema.Struct({
6262});
6363export type ClaudeCodeProviderStartOptions = typeof ClaudeCodeProviderStartOptions.Type;
6464
65export const CursorProviderStartOptions = Schema.Struct({
66 binaryPath: Schema.optional(TrimmedNonEmptyStringSchema),
67});
68export type CursorProviderStartOptions = typeof CursorProviderStartOptions.Type;
69
6570export const ProviderStartOptions = Schema.Struct({
6671 codex: Schema.optional(CodexProviderStartOptions),
6772 claudeCode: Schema.optional(ClaudeCodeProviderStartOptions),
73 cursor: Schema.optional(CursorProviderStartOptions),
6874});
6975export type ProviderStartOptions = typeof ProviderStartOptions.Type;
7076
packages/contracts/src/providerRuntime.ts+526-9
......@@ -9,6 +9,7 @@ import {
99 RuntimeItemId,
1010 RuntimeRequestId,
1111 RuntimeSessionId,
12 RuntimeTaskId,
1213 ThreadId,
1314 TrimmedNonEmptyString,
1415 TurnId,
......@@ -25,6 +26,9 @@ export const RuntimeEventRawSource = Schema.Literals([
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]);
2933export type RuntimeEventRawSource = typeof RuntimeEventRawSource.Type;
3034
......@@ -48,6 +52,26 @@ export const ProviderRefs = Schema.Struct({
4852});
4953export type ProviderRefs = typeof ProviderRefs.Type;
5054
55export const RuntimeSessionState = Schema.Literals([
56 "starting",
57 "ready",
58 "running",
59 "waiting",
60 "stopped",
61 "error",
62]);
63export type RuntimeSessionState = typeof RuntimeSessionState.Type;
64
65export const RuntimeThreadState = Schema.Literals([
66 "active",
67 "idle",
68 "archived",
69 "closed",
70 "compacted",
71 "error",
72]);
73export type RuntimeThreadState = typeof RuntimeThreadState.Type;
74
5175export const RuntimeTurnState = Schema.Literals([
5276 "completed",
5377 "failed",
......@@ -94,6 +118,12 @@ export const CanonicalItemType = Schema.Literals([
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([
106136 "exec_command_approval",
107137 "tool_user_input",
108138 "dynamic_tool_call",
139 "auth_tokens_refresh",
109140 "unknown",
110141]);
111142export type CanonicalRequestType = typeof CanonicalRequestType.Type;
112143
113144export 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([
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]);
131191export type ProviderRuntimeEventType = typeof ProviderRuntimeEventType.Type;
132192
133193export const SessionStartedType = Schema.Literal("session.started");
194export const SessionConfiguredType = Schema.Literal("session.configured");
195export const SessionStateChangedType = Schema.Literal("session.state.changed");
134196export const SessionExitedType = Schema.Literal("session.exited");
135197export const ThreadStartedType = Schema.Literal("thread.started");
198export const ThreadStateChangedType = Schema.Literal("thread.state.changed");
199export const ThreadMetadataUpdatedType = Schema.Literal("thread.metadata.updated");
200export const ThreadTokenUsageUpdatedType = Schema.Literal("thread.token-usage.updated");
201export const ThreadRealtimeStartedType = Schema.Literal("thread.realtime.started");
202export const ThreadRealtimeItemAddedType = Schema.Literal("thread.realtime.item-added");
203export const ThreadRealtimeAudioDeltaType = Schema.Literal("thread.realtime.audio.delta");
204export const ThreadRealtimeErrorType = Schema.Literal("thread.realtime.error");
205export const ThreadRealtimeClosedType = Schema.Literal("thread.realtime.closed");
136206export const TurnStartedType = Schema.Literal("turn.started");
137207export const TurnCompletedType = Schema.Literal("turn.completed");
208export const TurnAbortedType = Schema.Literal("turn.aborted");
138209export const TurnPlanUpdatedType = Schema.Literal("turn.plan.updated");
210export const TurnDiffUpdatedType = Schema.Literal("turn.diff.updated");
139211export const ItemStartedType = Schema.Literal("item.started");
140212export const ItemUpdatedType = Schema.Literal("item.updated");
141213export const ItemCompletedType = Schema.Literal("item.completed");
......@@ -144,6 +216,23 @@ export const RequestOpenedType = Schema.Literal("request.opened");
144216export const RequestResolvedType = Schema.Literal("request.resolved");
145217export const UserInputRequestedType = Schema.Literal("user-input.requested");
146218export const UserInputResolvedType = Schema.Literal("user-input.resolved");
219export const TaskStartedType = Schema.Literal("task.started");
220export const TaskProgressType = Schema.Literal("task.progress");
221export const TaskCompletedType = Schema.Literal("task.completed");
222export const HookStartedType = Schema.Literal("hook.started");
223export const HookProgressType = Schema.Literal("hook.progress");
224export const HookCompletedType = Schema.Literal("hook.completed");
225export const ToolProgressType = Schema.Literal("tool.progress");
226export const ToolSummaryType = Schema.Literal("tool.summary");
227export const AuthStatusType = Schema.Literal("auth.status");
228export const AccountUpdatedType = Schema.Literal("account.updated");
229export const AccountRateLimitsUpdatedType = Schema.Literal("account.rate-limits.updated");
230export const McpStatusUpdatedType = Schema.Literal("mcp.status.updated");
231export const McpOauthCompletedType = Schema.Literal("mcp.oauth.completed");
232export const ModelReroutedType = Schema.Literal("model.rerouted");
233export const ConfigWarningType = Schema.Literal("config.warning");
234export const DeprecationNoticeType = Schema.Literal("deprecation.notice");
235export const FilesPersistedType = Schema.Literal("files.persisted");
147236export const RuntimeWarningType = Schema.Literal("runtime.warning");
148237export const RuntimeErrorType = Schema.Literal("runtime.error");
149238
......@@ -167,6 +256,18 @@ export const SessionStartedPayload = Schema.Struct({
167256});
168257export type SessionStartedPayload = typeof SessionStartedPayload.Type;
169258
259export const SessionConfiguredPayload = Schema.Struct({
260 config: UnknownRecordSchema,
261});
262export type SessionConfiguredPayload = typeof SessionConfiguredPayload.Type;
263
264export const SessionStateChangedPayload = Schema.Struct({
265 state: RuntimeSessionState,
266 reason: Schema.optional(TrimmedNonEmptyStringSchema),
267 detail: Schema.optional(Schema.Unknown),
268});
269export type SessionStateChangedPayload = typeof SessionStateChangedPayload.Type;
270
170271export const SessionExitedPayload = Schema.Struct({
171272 reason: Schema.optional(TrimmedNonEmptyStringSchema),
172273 recoverable: Schema.optional(Schema.Boolean),
......@@ -179,6 +280,48 @@ export const ThreadStartedPayload = Schema.Struct({
179280});
180281export type ThreadStartedPayload = typeof ThreadStartedPayload.Type;
181282
283export const ThreadStateChangedPayload = Schema.Struct({
284 state: RuntimeThreadState,
285 detail: Schema.optional(Schema.Unknown),
286});
287export type ThreadStateChangedPayload = typeof ThreadStateChangedPayload.Type;
288
289export const ThreadMetadataUpdatedPayload = Schema.Struct({
290 name: Schema.optional(TrimmedNonEmptyStringSchema),
291 metadata: Schema.optional(UnknownRecordSchema),
292});
293export type ThreadMetadataUpdatedPayload = typeof ThreadMetadataUpdatedPayload.Type;
294
295export const ThreadTokenUsageUpdatedPayload = Schema.Struct({
296 usage: Schema.Unknown,
297});
298export type ThreadTokenUsageUpdatedPayload = typeof ThreadTokenUsageUpdatedPayload.Type;
299
300export const ThreadRealtimeStartedPayload = Schema.Struct({
301 realtimeSessionId: Schema.optional(TrimmedNonEmptyStringSchema),
302});
303export type ThreadRealtimeStartedPayload = typeof ThreadRealtimeStartedPayload.Type;
304
305export const ThreadRealtimeItemAddedPayload = Schema.Struct({
306 item: Schema.Unknown,
307});
308export type ThreadRealtimeItemAddedPayload = typeof ThreadRealtimeItemAddedPayload.Type;
309
310export const ThreadRealtimeAudioDeltaPayload = Schema.Struct({
311 audio: Schema.Unknown,
312});
313export type ThreadRealtimeAudioDeltaPayload = typeof ThreadRealtimeAudioDeltaPayload.Type;
314
315export const ThreadRealtimeErrorPayload = Schema.Struct({
316 message: TrimmedNonEmptyStringSchema,
317});
318export type ThreadRealtimeErrorPayload = typeof ThreadRealtimeErrorPayload.Type;
319
320export const ThreadRealtimeClosedPayload = Schema.Struct({
321 reason: Schema.optional(TrimmedNonEmptyStringSchema),
322});
323export type ThreadRealtimeClosedPayload = typeof ThreadRealtimeClosedPayload.Type;
324
182325export const TurnStartedPayload = Schema.Struct({
183326 model: Schema.optional(TrimmedNonEmptyStringSchema),
184327 effort: Schema.optional(TrimmedNonEmptyStringSchema),
......@@ -195,6 +338,11 @@ export const TurnCompletedPayload = Schema.Struct({
195338});
196339export type TurnCompletedPayload = typeof TurnCompletedPayload.Type;
197340
341export const TurnAbortedPayload = Schema.Struct({
342 reason: TrimmedNonEmptyStringSchema,
343});
344export type TurnAbortedPayload = typeof TurnAbortedPayload.Type;
345
198346export const RuntimePlanStep = Schema.Struct({
199347 step: TrimmedNonEmptyStringSchema,
200348 status: RuntimePlanStepStatus,
......@@ -207,6 +355,11 @@ export const TurnPlanUpdatedPayload = Schema.Struct({
207355});
208356export type TurnPlanUpdatedPayload = typeof TurnPlanUpdatedPayload.Type;
209357
358export const TurnDiffUpdatedPayload = Schema.Struct({
359 unifiedDiff: Schema.String,
360});
361export type TurnDiffUpdatedPayload = typeof TurnDiffUpdatedPayload.Type;
362
210363export const ItemLifecyclePayload = Schema.Struct({
211364 itemType: CanonicalItemType,
212365 status: Schema.optional(RuntimeItemStatus),
......@@ -262,6 +415,136 @@ export const UserInputResolvedPayload = Schema.Struct({
262415});
263416export type UserInputResolvedPayload = typeof UserInputResolvedPayload.Type;
264417
418export const TaskStartedPayload = Schema.Struct({
419 taskId: RuntimeTaskId,
420 description: Schema.optional(TrimmedNonEmptyStringSchema),
421 taskType: Schema.optional(TrimmedNonEmptyStringSchema),
422});
423export type TaskStartedPayload = typeof TaskStartedPayload.Type;
424
425export const TaskProgressPayload = Schema.Struct({
426 taskId: RuntimeTaskId,
427 description: TrimmedNonEmptyStringSchema,
428 usage: Schema.optional(Schema.Unknown),
429 lastToolName: Schema.optional(TrimmedNonEmptyStringSchema),
430});
431export type TaskProgressPayload = typeof TaskProgressPayload.Type;
432
433export 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});
439export type TaskCompletedPayload = typeof TaskCompletedPayload.Type;
440
441export const HookStartedPayload = Schema.Struct({
442 hookId: TrimmedNonEmptyStringSchema,
443 hookName: TrimmedNonEmptyStringSchema,
444 hookEvent: TrimmedNonEmptyStringSchema,
445});
446export type HookStartedPayload = typeof HookStartedPayload.Type;
447
448export 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});
454export type HookProgressPayload = typeof HookProgressPayload.Type;
455
456export 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});
464export type HookCompletedPayload = typeof HookCompletedPayload.Type;
465
466export 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});
472export type ToolProgressPayload = typeof ToolProgressPayload.Type;
473
474export const ToolSummaryPayload = Schema.Struct({
475 summary: TrimmedNonEmptyStringSchema,
476 precedingToolUseIds: Schema.optional(Schema.Array(TrimmedNonEmptyStringSchema)),
477});
478export type ToolSummaryPayload = typeof ToolSummaryPayload.Type;
479
480export const AuthStatusPayload = Schema.Struct({
481 isAuthenticating: Schema.optional(Schema.Boolean),
482 output: Schema.optional(Schema.Array(Schema.String)),
483 error: Schema.optional(TrimmedNonEmptyStringSchema),
484});
485export type AuthStatusPayload = typeof AuthStatusPayload.Type;
486
487export const AccountUpdatedPayload = Schema.Struct({
488 account: Schema.Unknown,
489});
490export type AccountUpdatedPayload = typeof AccountUpdatedPayload.Type;
491
492export const AccountRateLimitsUpdatedPayload = Schema.Struct({
493 rateLimits: Schema.Unknown,
494});
495export type AccountRateLimitsUpdatedPayload = typeof AccountRateLimitsUpdatedPayload.Type;
496
497export const McpStatusUpdatedPayload = Schema.Struct({
498 status: Schema.Unknown,
499});
500export type McpStatusUpdatedPayload = typeof McpStatusUpdatedPayload.Type;
501
502export const McpOauthCompletedPayload = Schema.Struct({
503 success: Schema.Boolean,
504 name: Schema.optional(TrimmedNonEmptyStringSchema),
505 error: Schema.optional(TrimmedNonEmptyStringSchema),
506});
507export type McpOauthCompletedPayload = typeof McpOauthCompletedPayload.Type;
508
509export const ModelReroutedPayload = Schema.Struct({
510 fromModel: TrimmedNonEmptyStringSchema,
511 toModel: TrimmedNonEmptyStringSchema,
512 reason: TrimmedNonEmptyStringSchema,
513});
514export type ModelReroutedPayload = typeof ModelReroutedPayload.Type;
515
516export const ConfigWarningPayload = Schema.Struct({
517 summary: TrimmedNonEmptyStringSchema,
518 details: Schema.optional(TrimmedNonEmptyStringSchema),
519 path: Schema.optional(TrimmedNonEmptyStringSchema),
520 range: Schema.optional(Schema.Unknown),
521});
522export type ConfigWarningPayload = typeof ConfigWarningPayload.Type;
523
524export const DeprecationNoticePayload = Schema.Struct({
525 summary: TrimmedNonEmptyStringSchema,
526 details: Schema.optional(TrimmedNonEmptyStringSchema),
527});
528export type DeprecationNoticePayload = typeof DeprecationNoticePayload.Type;
529
530export 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});
546export type FilesPersistedPayload = typeof FilesPersistedPayload.Type;
547
265548export const RuntimeWarningPayload = Schema.Struct({
266549 message: TrimmedNonEmptyStringSchema,
267550 detail: Schema.optional(Schema.Unknown),
......@@ -282,6 +565,20 @@ export const ProviderRuntimeSessionStartedEvent = Schema.Struct({
282565});
283566export type ProviderRuntimeSessionStartedEvent = typeof ProviderRuntimeSessionStartedEvent.Type;
284567
568export const ProviderRuntimeSessionConfiguredEvent = Schema.Struct({
569 ...ProviderRuntimeEventBase.fields,
570 type: SessionConfiguredType,
571 payload: SessionConfiguredPayload,
572});
573export type ProviderRuntimeSessionConfiguredEvent = typeof ProviderRuntimeSessionConfiguredEvent.Type;
574
575export const ProviderRuntimeSessionStateChangedEvent = Schema.Struct({
576 ...ProviderRuntimeEventBase.fields,
577 type: SessionStateChangedType,
578 payload: SessionStateChangedPayload,
579});
580export type ProviderRuntimeSessionStateChangedEvent = typeof ProviderRuntimeSessionStateChangedEvent.Type;
581
285582export const ProviderRuntimeSessionExitedEvent = Schema.Struct({
286583 ...ProviderRuntimeEventBase.fields,
287584 type: SessionExitedType,
......@@ -296,6 +593,68 @@ export const ProviderRuntimeThreadStartedEvent = Schema.Struct({
296593});
297594export type ProviderRuntimeThreadStartedEvent = typeof ProviderRuntimeThreadStartedEvent.Type;
298595
596export const ProviderRuntimeThreadStateChangedEvent = Schema.Struct({
597 ...ProviderRuntimeEventBase.fields,
598 type: ThreadStateChangedType,
599 payload: ThreadStateChangedPayload,
600});
601export type ProviderRuntimeThreadStateChangedEvent = typeof ProviderRuntimeThreadStateChangedEvent.Type;
602
603export const ProviderRuntimeThreadMetadataUpdatedEvent = Schema.Struct({
604 ...ProviderRuntimeEventBase.fields,
605 type: ThreadMetadataUpdatedType,
606 payload: ThreadMetadataUpdatedPayload,
607});
608export type ProviderRuntimeThreadMetadataUpdatedEvent = typeof ProviderRuntimeThreadMetadataUpdatedEvent.Type;
609
610export const ProviderRuntimeThreadTokenUsageUpdatedEvent = Schema.Struct({
611 ...ProviderRuntimeEventBase.fields,
612 type: ThreadTokenUsageUpdatedType,
613 payload: ThreadTokenUsageUpdatedPayload,
614});
615export type ProviderRuntimeThreadTokenUsageUpdatedEvent =
616 typeof ProviderRuntimeThreadTokenUsageUpdatedEvent.Type;
617
618export const ProviderRuntimeThreadRealtimeStartedEvent = Schema.Struct({
619 ...ProviderRuntimeEventBase.fields,
620 type: ThreadRealtimeStartedType,
621 payload: ThreadRealtimeStartedPayload,
622});
623export type ProviderRuntimeThreadRealtimeStartedEvent =
624 typeof ProviderRuntimeThreadRealtimeStartedEvent.Type;
625
626export const ProviderRuntimeThreadRealtimeItemAddedEvent = Schema.Struct({
627 ...ProviderRuntimeEventBase.fields,
628 type: ThreadRealtimeItemAddedType,
629 payload: ThreadRealtimeItemAddedPayload,
630});
631export type ProviderRuntimeThreadRealtimeItemAddedEvent =
632 typeof ProviderRuntimeThreadRealtimeItemAddedEvent.Type;
633
634export const ProviderRuntimeThreadRealtimeAudioDeltaEvent = Schema.Struct({
635 ...ProviderRuntimeEventBase.fields,
636 type: ThreadRealtimeAudioDeltaType,
637 payload: ThreadRealtimeAudioDeltaPayload,
638});
639export type ProviderRuntimeThreadRealtimeAudioDeltaEvent =
640 typeof ProviderRuntimeThreadRealtimeAudioDeltaEvent.Type;
641
642export const ProviderRuntimeThreadRealtimeErrorEvent = Schema.Struct({
643 ...ProviderRuntimeEventBase.fields,
644 type: ThreadRealtimeErrorType,
645 payload: ThreadRealtimeErrorPayload,
646});
647export type ProviderRuntimeThreadRealtimeErrorEvent =
648 typeof ProviderRuntimeThreadRealtimeErrorEvent.Type;
649
650export const ProviderRuntimeThreadRealtimeClosedEvent = Schema.Struct({
651 ...ProviderRuntimeEventBase.fields,
652 type: ThreadRealtimeClosedType,
653 payload: ThreadRealtimeClosedPayload,
654});
655export type ProviderRuntimeThreadRealtimeClosedEvent =
656 typeof ProviderRuntimeThreadRealtimeClosedEvent.Type;
657
299658export const ProviderRuntimeTurnStartedEvent = Schema.Struct({
300659 ...ProviderRuntimeEventBase.fields,
301660 type: TurnStartedType,
......@@ -310,6 +669,13 @@ export const ProviderRuntimeTurnCompletedEvent = Schema.Struct({
310669});
311670export type ProviderRuntimeTurnCompletedEvent = typeof ProviderRuntimeTurnCompletedEvent.Type;
312671
672export const ProviderRuntimeTurnAbortedEvent = Schema.Struct({
673 ...ProviderRuntimeEventBase.fields,
674 type: TurnAbortedType,
675 payload: TurnAbortedPayload,
676});
677export type ProviderRuntimeTurnAbortedEvent = typeof ProviderRuntimeTurnAbortedEvent.Type;
678
313679export const ProviderRuntimeTurnPlanUpdatedEvent = Schema.Struct({
314680 ...ProviderRuntimeEventBase.fields,
315681 type: TurnPlanUpdatedType,
......@@ -317,6 +683,13 @@ export const ProviderRuntimeTurnPlanUpdatedEvent = Schema.Struct({
317683});
318684export type ProviderRuntimeTurnPlanUpdatedEvent = typeof ProviderRuntimeTurnPlanUpdatedEvent.Type;
319685
686export const ProviderRuntimeTurnDiffUpdatedEvent = Schema.Struct({
687 ...ProviderRuntimeEventBase.fields,
688 type: TurnDiffUpdatedType,
689 payload: TurnDiffUpdatedPayload,
690});
691export type ProviderRuntimeTurnDiffUpdatedEvent = typeof ProviderRuntimeTurnDiffUpdatedEvent.Type;
692
320693export const ProviderRuntimeItemStartedEvent = Schema.Struct({
321694 ...ProviderRuntimeEventBase.fields,
322695 type: ItemStartedType,
......@@ -372,8 +745,127 @@ export const ProviderRuntimeUserInputResolvedEvent = Schema.Struct({
372745 type: UserInputResolvedType,
373746 payload: UserInputResolvedPayload,
374747});
375export type ProviderRuntimeUserInputResolvedEvent =
376 typeof ProviderRuntimeUserInputResolvedEvent.Type;
748export type ProviderRuntimeUserInputResolvedEvent = typeof ProviderRuntimeUserInputResolvedEvent.Type;
749
750export const ProviderRuntimeTaskStartedEvent = Schema.Struct({
751 ...ProviderRuntimeEventBase.fields,
752 type: TaskStartedType,
753 payload: TaskStartedPayload,
754});
755export type ProviderRuntimeTaskStartedEvent = typeof ProviderRuntimeTaskStartedEvent.Type;
756
757export const ProviderRuntimeTaskProgressEvent = Schema.Struct({
758 ...ProviderRuntimeEventBase.fields,
759 type: TaskProgressType,
760 payload: TaskProgressPayload,
761});
762export type ProviderRuntimeTaskProgressEvent = typeof ProviderRuntimeTaskProgressEvent.Type;
763
764export const ProviderRuntimeTaskCompletedEvent = Schema.Struct({
765 ...ProviderRuntimeEventBase.fields,
766 type: TaskCompletedType,
767 payload: TaskCompletedPayload,
768});
769export type ProviderRuntimeTaskCompletedEvent = typeof ProviderRuntimeTaskCompletedEvent.Type;
770
771export const ProviderRuntimeHookStartedEvent = Schema.Struct({
772 ...ProviderRuntimeEventBase.fields,
773 type: HookStartedType,
774 payload: HookStartedPayload,
775});
776export type ProviderRuntimeHookStartedEvent = typeof ProviderRuntimeHookStartedEvent.Type;
777
778export const ProviderRuntimeHookProgressEvent = Schema.Struct({
779 ...ProviderRuntimeEventBase.fields,
780 type: HookProgressType,
781 payload: HookProgressPayload,
782});
783export type ProviderRuntimeHookProgressEvent = typeof ProviderRuntimeHookProgressEvent.Type;
784
785export const ProviderRuntimeHookCompletedEvent = Schema.Struct({
786 ...ProviderRuntimeEventBase.fields,
787 type: HookCompletedType,
788 payload: HookCompletedPayload,
789});
790export type ProviderRuntimeHookCompletedEvent = typeof ProviderRuntimeHookCompletedEvent.Type;
791
792export const ProviderRuntimeToolProgressEvent = Schema.Struct({
793 ...ProviderRuntimeEventBase.fields,
794 type: ToolProgressType,
795 payload: ToolProgressPayload,
796});
797export type ProviderRuntimeToolProgressEvent = typeof ProviderRuntimeToolProgressEvent.Type;
798
799export const ProviderRuntimeToolSummaryEvent = Schema.Struct({
800 ...ProviderRuntimeEventBase.fields,
801 type: ToolSummaryType,
802 payload: ToolSummaryPayload,
803});
804export type ProviderRuntimeToolSummaryEvent = typeof ProviderRuntimeToolSummaryEvent.Type;
805
806export const ProviderRuntimeAuthStatusEvent = Schema.Struct({
807 ...ProviderRuntimeEventBase.fields,
808 type: AuthStatusType,
809 payload: AuthStatusPayload,
810});
811export type ProviderRuntimeAuthStatusEvent = typeof ProviderRuntimeAuthStatusEvent.Type;
812
813export const ProviderRuntimeAccountUpdatedEvent = Schema.Struct({
814 ...ProviderRuntimeEventBase.fields,
815 type: AccountUpdatedType,
816 payload: AccountUpdatedPayload,
817});
818export type ProviderRuntimeAccountUpdatedEvent = typeof ProviderRuntimeAccountUpdatedEvent.Type;
819
820export const ProviderRuntimeAccountRateLimitsUpdatedEvent = Schema.Struct({
821 ...ProviderRuntimeEventBase.fields,
822 type: AccountRateLimitsUpdatedType,
823 payload: AccountRateLimitsUpdatedPayload,
824});
825export type ProviderRuntimeAccountRateLimitsUpdatedEvent =
826 typeof ProviderRuntimeAccountRateLimitsUpdatedEvent.Type;
827
828export const ProviderRuntimeMcpStatusUpdatedEvent = Schema.Struct({
829 ...ProviderRuntimeEventBase.fields,
830 type: McpStatusUpdatedType,
831 payload: McpStatusUpdatedPayload,
832});
833export type ProviderRuntimeMcpStatusUpdatedEvent = typeof ProviderRuntimeMcpStatusUpdatedEvent.Type;
834
835export const ProviderRuntimeMcpOauthCompletedEvent = Schema.Struct({
836 ...ProviderRuntimeEventBase.fields,
837 type: McpOauthCompletedType,
838 payload: McpOauthCompletedPayload,
839});
840export type ProviderRuntimeMcpOauthCompletedEvent = typeof ProviderRuntimeMcpOauthCompletedEvent.Type;
841
842export const ProviderRuntimeModelReroutedEvent = Schema.Struct({
843 ...ProviderRuntimeEventBase.fields,
844 type: ModelReroutedType,
845 payload: ModelReroutedPayload,
846});
847export type ProviderRuntimeModelReroutedEvent = typeof ProviderRuntimeModelReroutedEvent.Type;
848
849export const ProviderRuntimeConfigWarningEvent = Schema.Struct({
850 ...ProviderRuntimeEventBase.fields,
851 type: ConfigWarningType,
852 payload: ConfigWarningPayload,
853});
854export type ProviderRuntimeConfigWarningEvent = typeof ProviderRuntimeConfigWarningEvent.Type;
855
856export const ProviderRuntimeDeprecationNoticeEvent = Schema.Struct({
857 ...ProviderRuntimeEventBase.fields,
858 type: DeprecationNoticeType,
859 payload: DeprecationNoticePayload,
860});
861export type ProviderRuntimeDeprecationNoticeEvent = typeof ProviderRuntimeDeprecationNoticeEvent.Type;
862
863export const ProviderRuntimeFilesPersistedEvent = Schema.Struct({
864 ...ProviderRuntimeEventBase.fields,
865 type: FilesPersistedType,
866 payload: FilesPersistedPayload,
867});
868export type ProviderRuntimeFilesPersistedEvent = typeof ProviderRuntimeFilesPersistedEvent.Type;
377869
378870export const ProviderRuntimeWarningEvent = Schema.Struct({
379871 ...ProviderRuntimeEventBase.fields,
......@@ -391,11 +883,23 @@ export type ProviderRuntimeErrorEvent = typeof ProviderRuntimeErrorEvent.Type;
391883
392884export 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([
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;
412933export const ProviderRuntimeEvent = ProviderRuntimeEventV2;
413934export type ProviderRuntimeEvent = ProviderRuntimeEventV2;
414935
936// Compatibility aliases for call sites still importing legacy names.
415937export const ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent;
416938export type ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent;
417939export const ProviderRuntimeMessageCompletedEvent = ProviderRuntimeItemCompletedEvent;
......@@ -425,13 +947,8 @@ export type ProviderRuntimeApprovalRequestedEvent = ProviderRuntimeRequestOpened
425947export const ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent;
426948export type ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent;
427949
428export 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.
951export const ProviderRuntimeToolKind = Schema.Literals(["command", "file-change", "other"]);
435952export type ProviderRuntimeToolKind = typeof ProviderRuntimeToolKind.Type;
436953
437954export const ProviderRuntimeTurnStatus = RuntimeTurnState;