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", () => {...@@ -229,7 +229,7 @@ describe("ClaudeCodeAdapterLive", () => {
229 return Effect.gen(function* () {229 return Effect.gen(function* () {
230 const adapter = yield* ClaudeCodeAdapter;230 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(
233 Stream.runCollect,233 Stream.runCollect,
234 Effect.forkChild,234 Effect.forkChild,
235 );235 );
...@@ -315,40 +315,43 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -315,40 +315,43 @@ describe("ClaudeCodeAdapterLive", () => {
315 runtimeEvents.map((event) => event.type),315 runtimeEvents.map((event) => event.type),
316 [316 [
317 "session.started",317 "session.started",
318 "session.configured",
319 "session.state.changed",
318 "turn.started",320 "turn.started",
319 "thread.started",321 "thread.started",
320 "message.delta",322 "content.delta",
321 "tool.started",323 "item.started",
322 "tool.completed",324 "item.completed",
323 "message.completed",325 "item.updated",
326 "item.completed",
324 "turn.completed",327 "turn.completed",
325 ],328 ],
326 );329 );
327330
328 const turnStarted = runtimeEvents[1];331 const turnStarted = runtimeEvents[3];
329 assert.equal(turnStarted?.type, "turn.started");332 assert.equal(turnStarted?.type, "turn.started");
330 if (turnStarted?.type === "turn.started") {333 if (turnStarted?.type === "turn.started") {
331 assert.equal(turnStarted.turnId, turn.turnId);334 assert.equal(turnStarted.turnId, turn.turnId);
332 }335 }
333336
334 const deltaEvent = runtimeEvents[3];337 const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta");
335 assert.equal(deltaEvent?.type, "message.delta");338 assert.equal(deltaEvent?.type, "content.delta");
336 if (deltaEvent?.type === "message.delta") {339 if (deltaEvent?.type === "content.delta") {
337 assert.equal(deltaEvent.delta, "Hi");340 assert.equal(deltaEvent.payload.delta, "Hi");
338 assert.equal(deltaEvent.turnId, turn.turnId);341 assert.equal(deltaEvent.turnId, turn.turnId);
339 }342 }
340343
341 const toolStarted = runtimeEvents[4];344 const toolStarted = runtimeEvents.find((event) => event.type === "item.started");
342 assert.equal(toolStarted?.type, "tool.started");345 assert.equal(toolStarted?.type, "item.started");
343 if (toolStarted?.type === "tool.started") {346 if (toolStarted?.type === "item.started") {
344 assert.equal(toolStarted.toolKind, "command");347 assert.equal(toolStarted.payload.itemType, "command_execution");
345 }348 }
346349
347 const turnCompleted = runtimeEvents[7];350 const turnCompleted = runtimeEvents[runtimeEvents.length - 1];
348 assert.equal(turnCompleted?.type, "turn.completed");351 assert.equal(turnCompleted?.type, "turn.completed");
349 if (turnCompleted?.type === "turn.completed") {352 if (turnCompleted?.type === "turn.completed") {
350 assert.equal(turnCompleted.turnId, turn.turnId);353 assert.equal(turnCompleted.turnId, turn.turnId);
351 assert.equal(turnCompleted.status, "completed");354 assert.equal(turnCompleted.payload.state, "completed");
352 }355 }
353 }).pipe(356 }).pipe(
354 Effect.provideService(Random.Random, makeDeterministicRandomService()),357 Effect.provideService(Random.Random, makeDeterministicRandomService()),
...@@ -361,7 +364,7 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -361,7 +364,7 @@ describe("ClaudeCodeAdapterLive", () => {
361 return Effect.gen(function* () {364 return Effect.gen(function* () {
362 const adapter = yield* ClaudeCodeAdapter;365 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(
365 Stream.runCollect,368 Stream.runCollect,
366 Effect.forkChild,369 Effect.forkChild,
367 );370 );
...@@ -416,22 +419,25 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -416,22 +419,25 @@ describe("ClaudeCodeAdapterLive", () => {
416 runtimeEvents.map((event) => event.type),419 runtimeEvents.map((event) => event.type),
417 [420 [
418 "session.started",421 "session.started",
422 "session.configured",
423 "session.state.changed",
419 "turn.started",424 "turn.started",
420 "thread.started",425 "thread.started",
421 "message.delta",426 "item.updated",
422 "message.completed",427 "content.delta",
428 "item.completed",
423 "turn.completed",429 "turn.completed",
424 ],430 ],
425 );431 );
426432
427 const deltaIndex = runtimeEvents.findIndex((event) => event.type === "message.delta");433 const deltaIndex = runtimeEvents.findIndex((event) => event.type === "content.delta");
428 const completedIndex = runtimeEvents.findIndex((event) => event.type === "message.completed");434 const completedIndex = runtimeEvents.findIndex((event) => event.type === "item.completed");
429 assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true);435 assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true);
430436
431 const deltaEvent = runtimeEvents[deltaIndex];437 const deltaEvent = runtimeEvents[deltaIndex];
432 assert.equal(deltaEvent?.type, "message.delta");438 assert.equal(deltaEvent?.type, "content.delta");
433 if (deltaEvent?.type === "message.delta") {439 if (deltaEvent?.type === "content.delta") {
434 assert.equal(deltaEvent.delta, "Late text");440 assert.equal(deltaEvent.payload.delta, "Late text");
435 assert.equal(deltaEvent.turnId, turn.turnId);441 assert.equal(deltaEvent.turnId, turn.turnId);
436 }442 }
437 }).pipe(443 }).pipe(
...@@ -445,7 +451,7 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -445,7 +451,7 @@ describe("ClaudeCodeAdapterLive", () => {
445 return Effect.gen(function* () {451 return Effect.gen(function* () {
446 const adapter = yield* ClaudeCodeAdapter;452 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(
449 Stream.runCollect,455 Stream.runCollect,
450 Effect.forkChild,456 Effect.forkChild,
451 );457 );
...@@ -485,18 +491,21 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -485,18 +491,21 @@ describe("ClaudeCodeAdapterLive", () => {
485 runtimeEvents.map((event) => event.type),491 runtimeEvents.map((event) => event.type),
486 [492 [
487 "session.started",493 "session.started",
494 "session.configured",
495 "session.state.changed",
488 "turn.started",496 "turn.started",
489 "thread.started",497 "thread.started",
490 "message.delta",498 "item.updated",
491 "message.completed",499 "content.delta",
500 "item.completed",
492 "turn.completed",501 "turn.completed",
493 ],502 ],
494 );503 );
495504
496 const deltaEvent = runtimeEvents.find((event) => event.type === "message.delta");505 const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta");
497 assert.equal(deltaEvent?.type, "message.delta");506 assert.equal(deltaEvent?.type, "content.delta");
498 if (deltaEvent?.type === "message.delta") {507 if (deltaEvent?.type === "content.delta") {
499 assert.equal(deltaEvent.delta, "Fallback hello");508 assert.equal(deltaEvent.payload.delta, "Fallback hello");
500 assert.equal(deltaEvent.turnId, turn.turnId);509 assert.equal(deltaEvent.turnId, turn.turnId);
501 }510 }
502 }).pipe(511 }).pipe(
...@@ -552,7 +561,13 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -552,7 +561,13 @@ describe("ClaudeCodeAdapterLive", () => {
552 const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));561 const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
553 assert.deepEqual(562 assert.deepEqual(
554 runtimeEvents.map((event) => event.type),563 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 ],
556 );571 );
557572
558 const sessionStarted = runtimeEvents[0];573 const sessionStarted = runtimeEvents[0];
...@@ -561,7 +576,7 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -561,7 +576,7 @@ describe("ClaudeCodeAdapterLive", () => {
561 assert.equal("threadId" in sessionStarted, false);576 assert.equal("threadId" in sessionStarted, false);
562 }577 }
563578
564 const threadStarted = runtimeEvents[2];579 const threadStarted = runtimeEvents[4];
565 assert.equal(threadStarted?.type, "thread.started");580 assert.equal(threadStarted?.type, "thread.started");
566 if (threadStarted?.type === "thread.started") {581 if (threadStarted?.type === "thread.started") {
567 assert.equal(threadStarted.threadId, "sdk-thread-real");582 assert.equal(threadStarted.threadId, "sdk-thread-real");
...@@ -582,8 +597,7 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -582,8 +597,7 @@ describe("ClaudeCodeAdapterLive", () => {
582 approvalPolicy: "on-request",597 approvalPolicy: "on-request",
583 });598 });
584599
585 const consumeSessionStarted = yield* Stream.runHead(adapter.streamEvents);600 yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain);
586 assert.equal(consumeSessionStarted._tag, "Some");
587601
588 const createInput = harness.getLastCreateQueryInput();602 const createInput = harness.getLastCreateQueryInput();
589 const canUseTool = createInput?.options.canUseTool;603 const canUseTool = createInput?.options.canUseTool;
...@@ -613,8 +627,8 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -613,8 +627,8 @@ describe("ClaudeCodeAdapterLive", () => {
613 if (requested._tag !== "Some") {627 if (requested._tag !== "Some") {
614 return;628 return;
615 }629 }
616 assert.equal(requested.value.type, "approval.requested");630 assert.equal(requested.value.type, "request.opened");
617 if (requested.value.type !== "approval.requested") {631 if (requested.value.type !== "request.opened") {
618 return;632 return;
619 }633 }
620634
...@@ -629,12 +643,12 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -629,12 +643,12 @@ describe("ClaudeCodeAdapterLive", () => {
629 if (resolved._tag !== "Some") {643 if (resolved._tag !== "Some") {
630 return;644 return;
631 }645 }
632 assert.equal(resolved.value.type, "approval.resolved");646 assert.equal(resolved.value.type, "request.resolved");
633 if (resolved.value.type !== "approval.resolved") {647 if (resolved.value.type !== "request.resolved") {
634 return;648 return;
635 }649 }
636 assert.equal(resolved.value.requestId, requested.value.requestId);650 assert.equal(resolved.value.requestId, requested.value.requestId);
637 assert.equal(resolved.value.decision, "accept");651 assert.equal(resolved.value.payload.decision, "accept");
638652
639 const permissionResult = yield* Effect.promise(() => permissionPromise);653 const permissionResult = yield* Effect.promise(() => permissionPromise);
640 assert.equal((permissionResult as PermissionResult).behavior, "allow");654 assert.equal((permissionResult as PermissionResult).behavior, "allow");
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+600-48
...@@ -19,11 +19,12 @@ import {...@@ -19,11 +19,12 @@ import {
19} from "@anthropic-ai/claude-agent-sdk";19} from "@anthropic-ai/claude-agent-sdk";
20import {20import {
21 ApprovalRequestId,21 ApprovalRequestId,
22 type CanonicalItemType,
23 type CanonicalRequestType,
22 EventId,24 EventId,
23 type ProviderApprovalDecision,25 type ProviderApprovalDecision,
24 ProviderItemId,26 ProviderItemId,
25 type ProviderRuntimeEvent,27 type ProviderRuntimeEvent,
26 type ProviderRuntimeToolKind,
27 type ProviderRuntimeTurnStatus,28 type ProviderRuntimeTurnStatus,
28 type ProviderSendTurnInput,29 type ProviderSendTurnInput,
29 ProviderSessionId,30 ProviderSessionId,
...@@ -73,7 +74,7 @@ interface ClaudeTurnState {...@@ -73,7 +74,7 @@ interface ClaudeTurnState {
73}74}
7475
75interface PendingApproval {76interface PendingApproval {
76 readonly requestKind: "command" | "file-change";77 readonly requestType: CanonicalRequestType;
77 readonly detail?: string;78 readonly detail?: string;
78 readonly suggestions?: ReadonlyArray<PermissionUpdate>;79 readonly suggestions?: ReadonlyArray<PermissionUpdate>;
79 readonly decision: Deferred.Deferred<ProviderApprovalDecision>;80 readonly decision: Deferred.Deferred<ProviderApprovalDecision>;
...@@ -81,7 +82,8 @@ interface PendingApproval {...@@ -81,7 +82,8 @@ interface PendingApproval {
8182
82interface ToolInFlight {83interface ToolInFlight {
83 readonly itemId: string;84 readonly itemId: string;
84 readonly toolKind: ProviderRuntimeToolKind;85 readonly itemType: CanonicalItemType;
86 readonly toolName: string;
85 readonly title: string;87 readonly title: string;
86 readonly detail?: string;88 readonly detail?: string;
87}89}
...@@ -188,7 +190,7 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef...@@ -188,7 +190,7 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef
188 };190 };
189}191}
190192
191function classifyToolKind(toolName: string): ProviderRuntimeToolKind {193function classifyToolItemType(toolName: string): CanonicalItemType {
192 const normalized = toolName.toLowerCase();194 const normalized = toolName.toLowerCase();
193 if (195 if (
194 normalized.includes("bash") ||196 normalized.includes("bash") ||
...@@ -196,7 +198,7 @@ function classifyToolKind(toolName: string): ProviderRuntimeToolKind {...@@ -196,7 +198,7 @@ function classifyToolKind(toolName: string): ProviderRuntimeToolKind {
196 normalized.includes("shell") ||198 normalized.includes("shell") ||
197 normalized.includes("terminal")199 normalized.includes("terminal")
198 ) {200 ) {
199 return "command";201 return "command_execution";
200 }202 }
201 if (203 if (
202 normalized.includes("edit") ||204 normalized.includes("edit") ||
...@@ -207,13 +209,18 @@ function classifyToolKind(toolName: string): ProviderRuntimeToolKind {...@@ -207,13 +209,18 @@ function classifyToolKind(toolName: string): ProviderRuntimeToolKind {
207 normalized.includes("create") ||209 normalized.includes("create") ||
208 normalized.includes("delete")210 normalized.includes("delete")
209 ) {211 ) {
210 return "file-change";212 return "file_change";
211 }213 }
212 return "other";214 if (normalized.includes("mcp")) {
215 return "mcp_tool_call";
216 }
217 return "dynamic_tool_call";
213}218}
214219
215function classifyRequestKind(toolName: string): "command" | "file-change" {220function classifyRequestType(toolName: string): CanonicalRequestType {
216 return classifyToolKind(toolName) === "command" ? "command" : "file-change";221 return classifyToolItemType(toolName) === "command_execution"
222 ? "command_execution_approval"
223 : "file_change_approval";
217}224}
218225
219function summarizeToolRequest(toolName: string, input: Record<string, unknown>): string {226function summarizeToolRequest(toolName: string, input: Record<string, unknown>): string {
...@@ -230,14 +237,18 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>):...@@ -230,14 +237,18 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>):
230 return `${toolName}: ${serialized.slice(0, 397)}...`;237 return `${toolName}: ${serialized.slice(0, 397)}...`;
231}238}
232239
233function titleForTool(kind: ProviderRuntimeToolKind): string {240function titleForTool(itemType: CanonicalItemType): string {
234 switch (kind) {241 switch (itemType) {
235 case "command":242 case "command_execution":
236 return "Command run";243 return "Command run";
237 case "file-change":244 case "file_change":
238 return "File change";245 return "File change";
239 case "other":246 case "mcp_tool_call":
247 return "MCP tool call";
248 case "dynamic_tool_call":
240 return "Tool call";249 return "Tool call";
250 default:
251 return "Item";
241 }252 }
242}253}
243254
...@@ -284,6 +295,10 @@ function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStat...@@ -284,6 +295,10 @@ function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStat
284 return "failed";295 return "failed";
285}296}
286297
298function streamKindFromDeltaType(deltaType: string): "assistant_text" | "reasoning_text" {
299 return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text";
300}
301
287function extractAssistantText(message: SDKMessage): string {302function extractAssistantText(message: SDKMessage): string {
288 if (message.type !== "assistant") {303 if (message.type !== "assistant") {
289 return "";304 return "";
...@@ -540,6 +555,20 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -540,6 +555,20 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
540 sessionId: context.session.sessionId,555 sessionId: context.session.sessionId,
541 createdAt: stamp.createdAt,556 createdAt: stamp.createdAt,
542 threadId: nextThreadId,557 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 },
543 });572 });
544 }573 }
545 });574 });
...@@ -563,7 +592,44 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -563,7 +592,44 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
563 createdAt: stamp.createdAt,592 createdAt: stamp.createdAt,
564 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),593 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
565 ...(turnState ? { turnId: turnState.turnId } : {}),594 ...(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 },
567 });633 });
568 });634 });
569635
...@@ -571,6 +637,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -571,6 +637,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
571 context: ClaudeSessionContext,637 context: ClaudeSessionContext,
572 status: ProviderRuntimeTurnStatus,638 status: ProviderRuntimeTurnStatus,
573 errorMessage?: string,639 errorMessage?: string,
640 result?: SDKResultMessage,
574 ): Effect.Effect<void> =>641 ): Effect.Effect<void> =>
575 Effect.gen(function* () {642 Effect.gen(function* () {
576 const turnState = context.turnState;643 const turnState = context.turnState;
...@@ -583,8 +650,20 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -583,8 +650,20 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
583 sessionId: context.session.sessionId,650 sessionId: context.session.sessionId,
584 createdAt: stamp.createdAt,651 createdAt: stamp.createdAt,
585 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),652 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
586 status,653 payload: {
587 ...(errorMessage ? { errorMessage } : {}),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 },
588 });667 });
589 return;668 return;
590 }669 }
...@@ -593,7 +672,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -593,7 +672,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
593 if (!turnState.emittedTextDelta && turnState.fallbackAssistantText.length > 0) {672 if (!turnState.emittedTextDelta && turnState.fallbackAssistantText.length > 0) {
594 const deltaStamp = yield* makeEventStamp();673 const deltaStamp = yield* makeEventStamp();
595 yield* offerRuntimeEvent({674 yield* offerRuntimeEvent({
596 type: "message.delta",675 type: "content.delta",
597 eventId: deltaStamp.eventId,676 eventId: deltaStamp.eventId,
598 provider: PROVIDER,677 provider: PROVIDER,
599 sessionId: context.session.sessionId,678 sessionId: context.session.sessionId,
...@@ -601,13 +680,22 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -601,13 +680,22 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
601 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),680 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
602 turnId: turnState.turnId,681 turnId: turnState.turnId,
603 itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),682 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 },
605 });693 });
606 }694 }
607695
608 const stamp = yield* makeEventStamp();696 const stamp = yield* makeEventStamp();
609 yield* offerRuntimeEvent({697 yield* offerRuntimeEvent({
610 type: "message.completed",698 type: "item.completed",
611 eventId: stamp.eventId,699 eventId: stamp.eventId,
612 provider: PROVIDER,700 provider: PROVIDER,
613 sessionId: context.session.sessionId,701 sessionId: context.session.sessionId,
...@@ -615,6 +703,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -615,6 +703,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
615 itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),703 itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),
616 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),704 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
617 turnId: turnState.turnId,705 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 },
618 });717 });
619 }718 }
620719
...@@ -632,8 +731,21 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -632,8 +731,21 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
632 createdAt: stamp.createdAt,731 createdAt: stamp.createdAt,
633 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),732 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
634 turnId: turnState.turnId,733 turnId: turnState.turnId,
635 status,734 payload: {
636 ...(errorMessage ? { errorMessage } : {}),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 },
637 });749 });
638750
639 const updatedAt = yield* nowIso;751 const updatedAt = yield* nowIso;
...@@ -673,7 +785,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -673,7 +785,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
673 }785 }
674 const stamp = yield* makeEventStamp();786 const stamp = yield* makeEventStamp();
675 yield* offerRuntimeEvent({787 yield* offerRuntimeEvent({
676 type: "message.delta",788 type: "content.delta",
677 eventId: stamp.eventId,789 eventId: stamp.eventId,
678 provider: PROVIDER,790 provider: PROVIDER,
679 sessionId: context.session.sessionId,791 sessionId: context.session.sessionId,
...@@ -681,7 +793,21 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -681,7 +793,21 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
681 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),793 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
682 turnId: context.turnState.turnId,794 turnId: context.turnState.turnId,
683 itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),795 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 },
685 });811 });
686 }812 }
687 return;813 return;
...@@ -698,7 +824,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -698,7 +824,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
698 }824 }
699825
700 const toolName = block.name;826 const toolName = block.name;
701 const toolKind = classifyToolKind(toolName);827 const itemType = classifyToolItemType(toolName);
702 const toolInput =828 const toolInput =
703 typeof block.input === "object" && block.input !== null829 typeof block.input === "object" && block.input !== null
704 ? (block.input as Record<string, unknown>)830 ? (block.input as Record<string, unknown>)
...@@ -708,15 +834,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -708,15 +834,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
708834
709 const tool: ToolInFlight = {835 const tool: ToolInFlight = {
710 itemId,836 itemId,
711 toolKind,837 itemType,
712 title: titleForTool(toolKind),838 toolName,
839 title: titleForTool(itemType),
713 detail,840 detail,
714 };841 };
715 context.inFlightTools.set(index, tool);842 context.inFlightTools.set(index, tool);
716843
717 const stamp = yield* makeEventStamp();844 const stamp = yield* makeEventStamp();
718 yield* offerRuntimeEvent({845 yield* offerRuntimeEvent({
719 type: "tool.started",846 type: "item.started",
720 eventId: stamp.eventId,847 eventId: stamp.eventId,
721 provider: PROVIDER,848 provider: PROVIDER,
722 sessionId: context.session.sessionId,849 sessionId: context.session.sessionId,
...@@ -724,9 +851,27 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -724,9 +851,27 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
724 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),851 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
725 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),852 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
726 itemId: ProviderItemId.makeUnsafe(tool.itemId),853 itemId: ProviderItemId.makeUnsafe(tool.itemId),
727 toolKind: tool.toolKind,854 payload: {
728 title: tool.title,855 itemType: tool.itemType,
729 ...(tool.detail ? { detail: tool.detail } : {}),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 },
730 });875 });
731 return;876 return;
732 }877 }
...@@ -741,7 +886,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -741,7 +886,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
741886
742 const stamp = yield* makeEventStamp();887 const stamp = yield* makeEventStamp();
743 yield* offerRuntimeEvent({888 yield* offerRuntimeEvent({
744 type: "tool.completed",889 type: "item.completed",
745 eventId: stamp.eventId,890 eventId: stamp.eventId,
746 provider: PROVIDER,891 provider: PROVIDER,
747 sessionId: context.session.sessionId,892 sessionId: context.session.sessionId,
...@@ -749,9 +894,23 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -749,9 +894,23 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
749 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),894 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
750 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),895 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
751 itemId: ProviderItemId.makeUnsafe(tool.itemId),896 itemId: ProviderItemId.makeUnsafe(tool.itemId),
752 toolKind: tool.toolKind,897 payload: {
753 title: tool.title,898 itemType: tool.itemType,
754 ...(tool.detail ? { detail: tool.detail } : {}),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 },
755 });914 });
756 }915 }
757 });916 });
...@@ -777,6 +936,35 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -777,6 +936,35 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
777 fallbackAssistantText,936 fallbackAssistantText,
778 };937 };
779 }938 }
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 });
780 }968 }
781969
782 context.lastAssistantUuid = message.uuid;970 context.lastAssistantUuid = message.uuid;
...@@ -799,7 +987,251 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -799,7 +987,251 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
799 yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");987 yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");
800 }988 }
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 }
803 });1235 });
8041236
805 const handleSdkMessage = (1237 const handleSdkMessage = (
...@@ -820,7 +1252,21 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -820,7 +1252,21 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
820 case "result":1252 case "result":
821 yield* handleResultMessage(context, message);1253 yield* handleResultMessage(context, message);
822 return;1254 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;
823 default:1264 default:
1265 yield* emitRuntimeWarning(
1266 context,
1267 `Unhandled Claude SDK message type '${message.type}'.`,
1268 message,
1269 );
824 return;1270 return;
825 }1271 }
826 });1272 });
...@@ -854,7 +1300,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -854,7 +1300,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
854 yield* Deferred.succeed(pending.decision, "cancel");1300 yield* Deferred.succeed(pending.decision, "cancel");
855 const stamp = yield* makeEventStamp();1301 const stamp = yield* makeEventStamp();
856 yield* offerRuntimeEvent({1302 yield* offerRuntimeEvent({
857 type: "approval.resolved",1303 type: "request.resolved",
858 eventId: stamp.eventId,1304 eventId: stamp.eventId,
859 provider: PROVIDER,1305 provider: PROVIDER,
860 sessionId: context.session.sessionId,1306 sessionId: context.session.sessionId,
...@@ -862,8 +1308,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -862,8 +1308,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
862 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),1308 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
863 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),1309 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
864 requestId,1310 requestId,
865 requestKind: pending.requestKind,1311 payload: {
866 decision: "cancel",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 },
867 });1321 });
868 }1322 }
869 context.pendingApprovals.clear();1323 context.pendingApprovals.clear();
...@@ -893,7 +1347,14 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -893,7 +1347,14 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
893 sessionId: context.session.sessionId,1347 sessionId: context.session.sessionId,
894 createdAt: stamp.createdAt,1348 createdAt: stamp.createdAt,
895 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),1349 ...(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 },
897 });1358 });
898 }1359 }
8991360
...@@ -972,11 +1433,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -972,11 +1433,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
972 }1433 }
9731434
974 const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4);1435 const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4);
975 const requestKind = classifyRequestKind(toolName);1436 const requestType = classifyRequestType(toolName);
976 const detail = summarizeToolRequest(toolName, toolInput);1437 const detail = summarizeToolRequest(toolName, toolInput);
977 const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>();1438 const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>();
978 const pendingApproval: PendingApproval = {1439 const pendingApproval: PendingApproval = {
979 requestKind,1440 requestType,
980 detail,1441 detail,
981 decision: decisionDeferred,1442 decision: decisionDeferred,
982 ...(callbackOptions.suggestions1443 ...(callbackOptions.suggestions
...@@ -986,7 +1447,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -986,7 +1447,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
9861447
987 const requestedStamp = yield* makeEventStamp();1448 const requestedStamp = yield* makeEventStamp();
988 yield* offerRuntimeEvent({1449 yield* offerRuntimeEvent({
989 type: "approval.requested",1450 type: "request.opened",
990 eventId: requestedStamp.eventId,1451 eventId: requestedStamp.eventId,
991 provider: PROVIDER,1452 provider: PROVIDER,
992 sessionId: context.session.sessionId,1453 sessionId: context.session.sessionId,
...@@ -994,8 +1455,29 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -994,8 +1455,29 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
994 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),1455 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
995 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),1456 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
996 requestId,1457 requestId,
997 requestKind,1458 payload: {
998 detail,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 },
999 });1481 });
10001482
1001 pendingApprovals.set(requestId, pendingApproval);1483 pendingApprovals.set(requestId, pendingApproval);
...@@ -1017,7 +1499,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -1017,7 +1499,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
10171499
1018 const resolvedStamp = yield* makeEventStamp();1500 const resolvedStamp = yield* makeEventStamp();
1019 yield* offerRuntimeEvent({1501 yield* offerRuntimeEvent({
1020 type: "approval.resolved",1502 type: "request.resolved",
1021 eventId: resolvedStamp.eventId,1503 eventId: resolvedStamp.eventId,
1022 provider: PROVIDER,1504 provider: PROVIDER,
1023 sessionId: context.session.sessionId,1505 sessionId: context.session.sessionId,
...@@ -1025,8 +1507,23 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -1025,8 +1507,23 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
1025 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),1507 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1026 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),1508 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1027 requestId,1509 requestId,
1028 requestKind,1510 payload: {
1029 decision,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 },
1030 });1527 });
10311528
1032 if (decision === "accept" || decision === "acceptForSession") {1529 if (decision === "accept" || decision === "acceptForSession") {
...@@ -1134,6 +1631,52 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -1134,6 +1631,52 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
1134 sessionId,1631 sessionId,
1135 createdAt: sessionStartedStamp.createdAt,1632 createdAt: sessionStartedStamp.createdAt,
1136 ...(threadId ? { threadId } : {}),1633 ...(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 },
1137 });1680 });
11381681
1139 Effect.runFork(runSdkStream(context));1682 Effect.runFork(runSdkStream(context));
...@@ -1191,6 +1734,15 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -1191,6 +1734,15 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
1191 createdAt: turnStartedStamp.createdAt,1734 createdAt: turnStartedStamp.createdAt,
1192 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),1735 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1193 turnId,1736 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 },
1194 });1746 });
11951747
1196 const message = buildUserMessage(input);1748 const message = buildUserMessage(input);
apps/server/src/provider/Layers/CodexAdapter.test.ts+5-4
...@@ -214,7 +214,7 @@ const lifecycleLayer = it.layer(...@@ -214,7 +214,7 @@ const lifecycleLayer = it.layer(
214);214);
215215
216lifecycleLayer("CodexAdapterLive lifecycle", (it) => {216lifecycleLayer("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", () =>
218 Effect.gen(function* () {218 Effect.gen(function* () {
219 const adapter = yield* CodexAdapter;219 const adapter = yield* CodexAdapter;
220 const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);220 const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);
...@@ -244,12 +244,13 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -244,12 +244,13 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
244 if (firstEvent._tag !== "Some") {244 if (firstEvent._tag !== "Some") {
245 return;245 return;
246 }246 }
247 assert.equal(firstEvent.value.type, "message.completed");247 assert.equal(firstEvent.value.type, "item.completed");
248 if (firstEvent.value.type !== "message.completed") {248 if (firstEvent.value.type !== "item.completed") {
249 return;249 return;
250 }250 }
251 assert.equal(firstEvent.value.itemId, "msg_1");251 assert.equal(firstEvent.value.itemId, "msg_1");
252 assert.equal(firstEvent.value.turnId, "turn-1");252 assert.equal(firstEvent.value.turnId, "turn-1");
253 assert.equal(firstEvent.value.payload.itemType, "assistant_message");
253 }),254 }),
254 );255 );
255256
...@@ -280,7 +281,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -280,7 +281,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
280 return;281 return;
281 }282 }
282 assert.equal(firstEvent.value.sessionId, "sess-1");283 assert.equal(firstEvent.value.sessionId, "sess-1");
283 assert.equal(firstEvent.value.message, "Session stopped");284 assert.equal(firstEvent.value.payload.reason, "Session stopped");
284 }),285 }),
285 );286 );
286});287});
apps/server/src/provider/Layers/CodexAdapter.ts+623-155
...@@ -7,11 +7,10 @@...@@ -7,11 +7,10 @@
7 * @module CodexAdapterLive7 * @module CodexAdapterLive
8 */8 */
9import {9import {
10 type CanonicalItemType,
11 type CanonicalRequestType,
10 type ProviderEvent,12 type ProviderEvent,
11 type ProviderRuntimeEvent,13 type ProviderRuntimeEvent,
12 type ProviderRuntimeTurnStatus,
13 type ProviderRuntimeToolKind,
14 ProviderItemId,
15 ProviderApprovalDecision,14 ProviderApprovalDecision,
16 ProviderSessionId,15 ProviderSessionId,
17 ProviderThreadId,16 ProviderThreadId,
...@@ -100,7 +99,11 @@ function asString(value: unknown): string | undefined {...@@ -100,7 +99,11 @@ function asString(value: unknown): string | undefined {
100 return typeof value === "string" ? value : undefined;99 return typeof value === "string" ? value : undefined;
101}100}
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" {
104 switch (value) {107 switch (value) {
105 case "completed":108 case "completed":
106 case "failed":109 case "failed":
...@@ -108,7 +111,7 @@ function toTurnStatus(value: unknown): ProviderRuntimeTurnStatus | undefined {...@@ -108,7 +111,7 @@ function toTurnStatus(value: unknown): ProviderRuntimeTurnStatus | undefined {
108 case "interrupted":111 case "interrupted":
109 return value;112 return value;
110 default:113 default:
111 return undefined;114 return "completed";
112 }115 }
113}116}
114117
...@@ -123,29 +126,53 @@ function normalizeItemType(raw: unknown): string {...@@ -123,29 +126,53 @@ function normalizeItemType(raw: unknown): string {
123 .toLowerCase();126 .toLowerCase();
124}127}
125128
126function shouldDropItemType(type: string): boolean {129function toCanonicalItemType(raw: unknown): CanonicalItemType {
127 if (type.includes("preamble") || type.includes("reasoning") || type.includes("thought")) {130 const type = normalizeItemType(raw);
128 return true;131 if (type.includes("user")) return "user_message";
129 }132 if (type.includes("agent message") || type.includes("assistant")) return "assistant_message";
130 return type === "work" || type.startsWith("work ");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";
131}147}
132148
133function toolMeta(type: string):149function itemTitle(itemType: CanonicalItemType): string | undefined {
134 | {150 switch (itemType) {
135 readonly toolKind: ProviderRuntimeToolKind;151 case "assistant_message":
136 readonly title: string;152 return "Assistant message";
137 }153 case "user_message":
138 | undefined {154 return "User message";
139 if (type.includes("command")) {155 case "reasoning":
140 return { toolKind: "command", title: "Command run" };156 return "Reasoning";
141 }157 case "plan":
142 if (type.includes("file change")) {158 return "Plan";
143 return { toolKind: "file-change", title: "File change" };159 case "command_execution":
144 }160 return "Command run";
145 if (type.includes("tool")) {161 case "file_change":
146 return { toolKind: "other", title: "Tool call" };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;
147 }175 }
148 return undefined;
149}176}
150177
151function itemDetail(178function itemDetail(
...@@ -174,80 +201,155 @@ function itemDetail(...@@ -174,80 +201,155 @@ function itemDetail(
174 return undefined;201 return undefined;
175}202}
176203
177function mapMessageCompletedEvent(event: ProviderEvent): ProviderRuntimeEvent | undefined {204function toRequestTypeFromMethod(method: string): CanonicalRequestType {
178 if (event.method !== "item/completed") {205 switch (method) {
179 return undefined;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";
180 }222 }
223}
181224
182 const payload = asObject(event.payload);225function toRequestTypeFromKind(kind: unknown): CanonicalRequestType {
183 const item = asObject(payload?.item);226 switch (kind) {
184 if (!item) {227 case "command":
185 return undefined;228 return "command_execution_approval";
229 case "file-change":
230 return "file_change_approval";
231 default:
232 return "unknown";
186 }233 }
234}
187235
188 const normalizedType = normalizeItemType(item.type ?? item.kind);236function toThreadState(value: unknown):
189 if (!normalizedType.includes("agent message")) {237 | "active"
190 return undefined;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";
191 }257 }
258}
192259
193 const itemId = event.itemId ?? asString(item.id);260function contentStreamKindFromMethod(
194 if (!itemId) {261 method: string,
195 return undefined;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";
196 }278 }
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);
198 return {301 return {
199 type: "message.completed",
200 eventId: event.id,302 eventId: event.id,
201 provider: event.provider,303 provider: event.provider,
202 sessionId: event.sessionId,304 sessionId: event.sessionId,
203 createdAt: event.createdAt,305 createdAt: event.createdAt,
204 itemId: ProviderItemId.makeUnsafe(itemId),
205 ...(event.threadId ? { threadId: event.threadId } : {}),306 ...(event.threadId ? { threadId: event.threadId } : {}),
206 ...(event.turnId ? { turnId: event.turnId } : {}),307 ...(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 },
207 };316 };
208}317}
209318
210function mapToolEvent(event: ProviderEvent): ProviderRuntimeEvent | undefined {319function mapItemLifecycle(
211 if (event.method !== "item/started" && event.method !== "item/completed") {320 event: ProviderEvent,
212 return undefined;321 lifecycle: "item.started" | "item.updated" | "item.completed",
213 }322): ProviderRuntimeEvent | undefined {
214 const payload = asObject(event.payload);323 const payload = asObject(event.payload);
215 const item = asObject(payload?.item);324 const item = asObject(payload?.item);
216 if (!item) {325 const source = item ?? payload;
326 if (!source) {
217 return undefined;327 return undefined;
218 }328 }
219 const normalizedType = normalizeItemType(item.type ?? item.kind);329
220 if (shouldDropItemType(normalizedType)) {330 const itemType = toCanonicalItemType(source.type ?? source.kind);
221 return undefined;331 if (itemType === "unknown" && lifecycle !== "item.updated") {
222 }
223 const meta = toolMeta(normalizedType);
224 if (!meta) {
225 return undefined;332 return undefined;
226 }333 }
227334
228 const eventBase = {335 const detail = itemDetail(source, payload ?? source);
229 eventId: event.id,336 const status =
230 provider: event.provider,337 lifecycle === "item.started"
231 sessionId: event.sessionId,338 ? "inProgress"
232 createdAt: event.createdAt,339 : lifecycle === "item.completed"
233 ...(event.threadId ? { threadId: event.threadId } : {}),340 ? "completed"
234 ...(event.turnId ? { turnId: event.turnId } : {}),341 : undefined;
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 }
247342
248 return {343 return {
249 type: "tool.completed",344 ...runtimeEventBase(event),
250 ...eventBase,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 },
251 };353 };
252}354}
253355
...@@ -261,78 +363,87 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime...@@ -261,78 +363,87 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime
261 }363 }
262 return [364 return [
263 {365 {
366 ...runtimeEventBase(event),
264 type: "runtime.error",367 type: "runtime.error",
265 eventId: event.id,368 payload: {
266 provider: event.provider,369 message: event.message,
267 sessionId: event.sessionId,370 class: "provider_error",
268 createdAt: event.createdAt,371 ...(event.payload !== undefined ? { detail: event.payload } : {}),
269 ...(event.threadId ? { threadId: event.threadId } : {}),372 },
270 ...(event.turnId ? { turnId: event.turnId } : {}),
271 ...(event.itemId ? { itemId: event.itemId } : {}),
272 message: event.message,
273 },373 },
274 ];374 ];
275 }375 }
276376
277 if (event.kind === "request" && event.requestId && event.requestKind) {377 if (event.kind === "request") {
278 const detail =378 const detail =
279 asString(payload?.command) ?? asString(payload?.reason) ?? asString(payload?.prompt);379 asString(payload?.command) ?? asString(payload?.reason) ?? asString(payload?.prompt);
280 return [380 return [
281 {381 {
282 type: "approval.requested",382 ...runtimeEventBase(event),
283 eventId: event.id,383 type: "request.opened",
284 provider: event.provider,384 payload: {
285 sessionId: event.sessionId,385 requestType: toRequestTypeFromMethod(event.method),
286 createdAt: event.createdAt,386 ...(detail ? { detail } : {}),
287 ...(event.threadId ? { threadId: event.threadId } : {}),387 ...(event.payload !== undefined ? { args: event.payload } : {}),
288 ...(event.turnId ? { turnId: event.turnId } : {}),388 },
289 ...(event.itemId ? { itemId: event.itemId } : {}),
290 requestId: event.requestId,
291 requestKind: event.requestKind,
292 ...(detail ? { detail } : {}),
293 },389 },
294 ];390 ];
295 }391 }
296392
297 if (event.method === "item/requestApproval/decision" && event.requestId) {393 if (event.method === "item/requestApproval/decision" && event.requestId) {
298 const decision = Schema.decodeUnknownSync(ProviderApprovalDecision)(payload?.decision);394 const decision = Schema.decodeUnknownSync(ProviderApprovalDecision)(payload?.decision);
395 const requestType =
396 event.requestKind !== undefined
397 ? toRequestTypeFromKind(event.requestKind)
398 : toRequestTypeFromMethod(event.method);
299 return [399 return [
300 {400 {
301 type: "approval.resolved",401 ...runtimeEventBase(event),
302 eventId: event.id,402 type: "request.resolved",
303 provider: event.provider,403 payload: {
304 sessionId: event.sessionId,404 requestType,
305 createdAt: event.createdAt,405 ...(decision ? { decision } : {}),
306 ...(event.threadId ? { threadId: event.threadId } : {}),406 ...(event.payload !== undefined ? { resolution: event.payload } : {}),
307 ...(event.turnId ? { turnId: event.turnId } : {}),407 },
308 ...(event.itemId ? { itemId: event.itemId } : {}),
309 requestId: event.requestId,
310 ...(event.requestKind ? { requestKind: event.requestKind } : {}),
311 ...(decision ? { decision } : {}),
312 },408 },
313 ];409 ];
314 }410 }
315411
316 const messageCompleted = mapMessageCompletedEvent(event);412 if (event.method === "session/connecting") {
317 if (messageCompleted) {413 return [
318 return [messageCompleted];414 {
415 ...runtimeEventBase(event),
416 type: "session.state.changed",
417 payload: {
418 state: "starting",
419 ...(event.message ? { reason: event.message } : {}),
420 },
421 },
422 ];
319 }423 }
320424
321 const tool = mapToolEvent(event);425 if (event.method === "session/ready") {
322 if (tool) {426 return [
323 return [tool];427 {
428 ...runtimeEventBase(event),
429 type: "session.state.changed",
430 payload: {
431 state: "ready",
432 ...(event.message ? { reason: event.message } : {}),
433 },
434 },
435 ];
324 }436 }
325437
326 if (event.method === "session/started") {438 if (event.method === "session/started") {
327 return [439 return [
328 {440 {
441 ...runtimeEventBase(event),
329 type: "session.started",442 type: "session.started",
330 eventId: event.id,443 payload: {
331 provider: event.provider,444 ...(event.message ? { message: event.message } : {}),
332 sessionId: event.sessionId,445 ...(event.payload !== undefined ? { resume: event.payload } : {}),
333 createdAt: event.createdAt,446 },
334 ...(event.threadId ? { threadId: event.threadId } : {}),
335 ...(event.message ? { message: event.message } : {}),
336 },447 },
337 ];448 ];
338 }449 }
...@@ -340,33 +451,82 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime...@@ -340,33 +451,82 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime
340 if (event.method === "session/exited" || event.method === "session/closed") {451 if (event.method === "session/exited" || event.method === "session/closed") {
341 return [452 return [
342 {453 {
454 ...runtimeEventBase(event),
343 type: "session.exited",455 type: "session.exited",
344 eventId: event.id,456 payload: {
345 provider: event.provider,457 ...(event.message ? { reason: event.message } : {}),
346 sessionId: event.sessionId,458 ...(event.method === "session/closed" ? { exitKind: "graceful" } : {}),
347 createdAt: event.createdAt,459 },
348 ...(event.threadId ? { threadId: event.threadId } : {}),
349 ...(event.message ? { message: event.message } : {}),
350 },460 },
351 ];461 ];
352 }462 }
353463
354 if (event.method === "thread/started") {464 if (event.method === "thread/started") {
355 const payloadThreadId = asString(asObject(payload?.thread)?.id);465 const payloadThreadId = asString(asObject(payload?.thread)?.id);
356 const threadId =466 const providerThreadId =
357 event.threadId ??467 event.threadId ??
358 (payloadThreadId ? ProviderThreadId.makeUnsafe(payloadThreadId) : undefined);468 (payloadThreadId ? ProviderThreadId.makeUnsafe(payloadThreadId) : undefined);
359 if (!threadId) {469 if (!providerThreadId) {
360 return [];470 return [];
361 }471 }
362 return [472 return [
363 {473 {
474 ...runtimeEventBase(event),
364 type: "thread.started",475 type: "thread.started",
365 eventId: event.id,476 payload: {
366 provider: event.provider,477 providerThreadId,
367 sessionId: event.sessionId,478 },
368 createdAt: event.createdAt,479 },
369 threadId,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 },
370 },530 },
371 ];531 ];
372 }532 }
...@@ -380,52 +540,360 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime...@@ -380,52 +540,360 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime
380 }540 }
381 return [541 return [
382 {542 {
383 type: "turn.started",543 ...runtimeEventBase(event),
384 eventId: event.id,
385 provider: event.provider,
386 sessionId: event.sessionId,
387 createdAt: event.createdAt,
388 ...(event.threadId ? { threadId: event.threadId } : {}),
389 turnId,544 turnId,
545 type: "turn.started",
546 payload: {
547 ...(asString(turn?.model) ? { model: asString(turn?.model) } : {}),
548 ...(asString(turn?.effort) ? { effort: asString(turn?.effort) } : {}),
549 },
390 },550 },
391 ];551 ];
392 }552 }
393553
394 if (event.method === "turn/completed") {554 if (event.method === "turn/completed") {
555 const errorMessage = asString(asObject(turn?.error)?.message);
395 return [556 return [
396 {557 {
558 ...runtimeEventBase(event),
397 type: "turn.completed",559 type: "turn.completed",
398 eventId: event.id,560 payload: {
399 provider: event.provider,561 state: toTurnStatus(turn?.status),
400 sessionId: event.sessionId,562 ...(asString(turn?.stopReason) ? { stopReason: asString(turn?.stopReason) } : {}),
401 createdAt: event.createdAt,563 ...(turn?.usage !== undefined ? { usage: turn.usage } : {}),
402 ...(event.threadId ? { threadId: event.threadId } : {}),564 ...(asObject(turn?.modelUsage) ? { modelUsage: asObject(turn?.modelUsage) } : {}),
403 ...(event.turnId ? { turnId: event.turnId } : {}),565 ...(asNumber(turn?.totalCostUsd) !== undefined
404 ...(toTurnStatus(turn?.status) ? { status: toTurnStatus(turn?.status) } : {}),566 ? { totalCostUsd: asNumber(turn?.totalCostUsd) }
405 ...(asString(asObject(turn?.error)?.message)567 : {}),
406 ? { errorMessage: asString(asObject(turn?.error)?.message) }568 ...(errorMessage ? { errorMessage } : {}),
407 : {}),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 },
408 },605 },
409 ];606 ];
410 }607 }
411608
412 if (event.method === "item/agentMessage/delta" && event.textDelta && event.textDelta.length > 0) {609 if (event.method === "turn/diff/updated") {
413 return [610 return [
414 {611 {
415 type: "message.delta",612 ...runtimeEventBase(event),
416 eventId: event.id,613 type: "turn.diff.updated",
417 provider: event.provider,614 payload: {
418 sessionId: event.sessionId,615 unifiedDiff:
419 createdAt: event.createdAt,616 asString(payload?.unifiedDiff) ??
420 ...(event.threadId ? { threadId: event.threadId } : {}),617 asString(payload?.diff) ??
421 ...(event.turnId ? { turnId: event.turnId } : {}),618 asString(payload?.patch) ??
422 ...(event.itemId ? { itemId: event.itemId } : {}),619 "",
423 delta: event.textDelta,620 },
424 },621 },
425 ];622 ];
426 }623 }
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 ];
429}897}
430898
431const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>899const 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";...@@ -6,6 +6,7 @@ import { Effect, Layer, Stream } from "effect";
66
7import { ClaudeCodeAdapter, ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts";7import { ClaudeCodeAdapter, ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts";
8import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";8import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";
9import { CursorAdapter, CursorAdapterShape } from "../Services/CursorAdapter.ts";
9import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";10import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
10import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";11import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
11import { ProviderUnsupportedError } from "../Errors.ts";12import { ProviderUnsupportedError } from "../Errors.ts";
...@@ -41,6 +42,21 @@ const fakeClaudeAdapter: ClaudeCodeAdapterShape = {...@@ -41,6 +42,21 @@ const fakeClaudeAdapter: ClaudeCodeAdapterShape = {
41 streamEvents: Stream.empty,42 streamEvents: Stream.empty,
42};43};
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
44const layer = it.layer(60const layer = it.layer(
45 Layer.mergeAll(61 Layer.mergeAll(
46 Layer.provide(62 Layer.provide(
...@@ -48,6 +64,7 @@ const layer = it.layer(...@@ -48,6 +64,7 @@ const layer = it.layer(
48 Layer.mergeAll(64 Layer.mergeAll(
49 Layer.succeed(CodexAdapter, fakeCodexAdapter),65 Layer.succeed(CodexAdapter, fakeCodexAdapter),
50 Layer.succeed(ClaudeCodeAdapter, fakeClaudeAdapter),66 Layer.succeed(ClaudeCodeAdapter, fakeClaudeAdapter),
67 Layer.succeed(CursorAdapter, fakeCursorAdapter),
51 ),68 ),
52 ),69 ),
53 NodeServices.layer,70 NodeServices.layer,
...@@ -60,11 +77,13 @@ layer("ProviderAdapterRegistryLive", (it) => {...@@ -60,11 +77,13 @@ layer("ProviderAdapterRegistryLive", (it) => {
60 const registry = yield* ProviderAdapterRegistry;77 const registry = yield* ProviderAdapterRegistry;
61 const codex = yield* registry.getByProvider("codex");78 const codex = yield* registry.getByProvider("codex");
62 const claude = yield* registry.getByProvider("claudeCode");79 const claude = yield* registry.getByProvider("claudeCode");
80 const cursor = yield* registry.getByProvider("cursor");
63 assert.equal(codex, fakeCodexAdapter);81 assert.equal(codex, fakeCodexAdapter);
64 assert.equal(claude, fakeClaudeAdapter);82 assert.equal(claude, fakeClaudeAdapter);
83 assert.equal(cursor, fakeCursorAdapter);
6584
66 const providers = yield* registry.listProviders();85 const providers = yield* registry.listProviders();
67 assert.deepEqual(providers, ["codex", "claudeCode"]);86 assert.deepEqual(providers, ["codex", "claudeCode", "cursor"]);
68 }),87 }),
69 );88 );
7089
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts+2-1
...@@ -17,6 +17,7 @@ import {...@@ -17,6 +17,7 @@ import {
17} from "../Services/ProviderAdapterRegistry.ts";17} from "../Services/ProviderAdapterRegistry.ts";
18import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts";18import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts";
19import { CodexAdapter } from "../Services/CodexAdapter.ts";19import { CodexAdapter } from "../Services/CodexAdapter.ts";
20import { CursorAdapter } from "../Services/CursorAdapter.ts";
2021
21export interface ProviderAdapterRegistryLiveOptions {22export interface ProviderAdapterRegistryLiveOptions {
22 readonly adapters?: ReadonlyArray<ProviderAdapterShape<ProviderAdapterError>>;23 readonly adapters?: ReadonlyArray<ProviderAdapterShape<ProviderAdapterError>>;
...@@ -27,7 +28,7 @@ const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOption...@@ -27,7 +28,7 @@ const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOption
27 const adapters =28 const adapters =
28 options?.adapters !== undefined29 options?.adapters !== undefined
29 ? options.adapters30 ? options.adapters
30 : [yield* CodexAdapter, yield* ClaudeCodeAdapter];31 : [yield* CodexAdapter, yield* ClaudeCodeAdapter, yield* CursorAdapter];
31 const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter]));32 const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter]));
3233
33 const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => {34 const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => {
apps/server/src/provider/Services/CursorAdapter.test.ts+97-98
...@@ -1,131 +1,130 @@...@@ -1,131 +1,130 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { describe, expect, it } from "vitest";2import { 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", () => {10describe("Cursor ACP schemas", () => {
7 it("decodes system/init events", () => {11 it("decodes session/update thought and message chunks", () => {
8 const decoded = Schema.decodeUnknownSync(CursorCliStreamEvent)({12 const thought = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification)({
9 type: "system",13 jsonrpc: "2.0",
10 subtype: "init",14 method: "session/update",
11 session_id: "sess-1",15 params: {
12 apiKeySource: "login",16 sessionId: "sess-1",
13 cwd: "/tmp/project",17 update: {
14 model: "Auto",18 sessionUpdate: "agent_thought_chunk",
15 permissionMode: "default",19 content: {
20 type: "text",
21 text: "thinking",
22 },
23 },
24 },
16 });25 });
1726
18 expect(decoded.type).toBe("system");27 expect(thought.params.update.sessionUpdate).toBe("agent_thought_chunk");
19 if (decoded.type !== "system") {
20 return;
21 }
22 expect(decoded.subtype).toBe("init");
23 expect(decoded.session_id).toBe("sess-1");
24 });
2528
26 it("decodes thinking delta/completed events", () => {29 const message = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification)({
27 const delta = Schema.decodeUnknownSync(CursorCliStreamEvent)({30 jsonrpc: "2.0",
28 type: "thinking",31 method: "session/update",
29 subtype: "delta",32 params: {
30 text: "draft",33 sessionId: "sess-1",
31 session_id: "sess-1",34 update: {
32 timestamp_ms: 123,35 sessionUpdate: "agent_message_chunk",
36 content: {
37 type: "text",
38 text: "hello",
39 },
40 },
41 },
33 });42 });
3443
35 expect(delta.type).toBe("thinking");44 expect(message.params.update.sessionUpdate).toBe("agent_message_chunk");
36 if (delta.type !== "thinking") {45 });
37 return;
38 }
39 expect(delta.subtype).toBe("delta");
4046
41 const completed = Schema.decodeUnknownSync(CursorCliStreamEvent)({47 it("decodes tool call lifecycle updates", () => {
42 type: "thinking",48 const started = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification)({
43 subtype: "completed",49 jsonrpc: "2.0",
44 session_id: "sess-1",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 },
45 });62 });
4663
47 expect(completed.type).toBe("thinking");64 expect(started.params.update.sessionUpdate).toBe("tool_call");
48 if (completed.type !== "thinking") {
49 return;
50 }
51 expect(completed.subtype).toBe("completed");
52 });
5365
54 it("decodes tool_call completed events with rejected results", () => {66 const completed = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification)({
55 const decoded = Schema.decodeUnknownSync(CursorCliStreamEvent)({67 jsonrpc: "2.0",
56 type: "tool_call",68 method: "session/update",
57 subtype: "completed",69 params: {
58 call_id: "tool-1\nline-2",70 sessionId: "sess-1",
59 session_id: "sess-1",71 update: {
60 tool_call: {72 sessionUpdate: "tool_call_update",
61 shellToolCall: {73 toolCallId: "tool-1",
62 args: {74 status: "completed",
63 command: "rm -rf /tmp/nope",75 rawOutput: {
64 },76 exitCode: 0,
65 result: {77 stdout: "ok",
66 rejected: {78 stderr: "",
67 reason: "approval denied",
68 },
69 },79 },
70 },80 },
71 },81 },
72 });82 });
7383
74 expect(decoded.type).toBe("tool_call");84 expect(completed.params.update.sessionUpdate).toBe("tool_call_update");
75 if (decoded.type !== "tool_call") {
76 return;
77 }
78 expect(decoded.subtype).toBe("completed");
79 expect(decoded.call_id).toContain("\n");
80 });85 });
8186
82 it("decodes result success events with usage payload", () => {87 it("decodes permission requests", () => {
83 const decoded = Schema.decodeUnknownSync(CursorCliStreamEvent)({88 const decoded = Schema.decodeUnknownSync(CursorAcpPermissionRequest)({
84 type: "result",89 jsonrpc: "2.0",
85 subtype: "success",90 id: 9,
86 duration_ms: 100,91 method: "session/request_permission",
87 duration_api_ms: 90,92 params: {
88 is_error: false,93 sessionId: "sess-1",
89 result: "OK",94 toolCall: {
90 session_id: "sess-1",95 toolCallId: "tool-1",
91 request_id: "req-1",96 kind: "execute",
92 usage: {97 },
93 inputTokens: 10,98 options: [
94 outputTokens: 5,99 { optionId: "allow-once", name: "Allow once", kind: "allow_once" },
95 cacheReadTokens: 2,100 { optionId: "reject-once", name: "Reject", kind: "reject_once" },
96 cacheWriteTokens: 1,101 ],
97 },102 },
98 });103 });
99104
100 expect(decoded.type).toBe("result");105 expect(decoded.method).toBe("session/request_permission");
101 if (decoded.type !== "result") {106 expect(decoded.params.options).toHaveLength(2);
102 return;
103 }
104 expect(decoded.subtype).toBe("success");
105 expect(decoded.is_error).toBe(false);
106 });107 });
107108
108 it("decodes connection/retry lifecycle events", () => {109 it("decodes prompt completion result payload", () => {
109 const reconnecting = Schema.decodeUnknownSync(CursorCliStreamEvent)({110 const decoded = Schema.decodeUnknownSync(CursorAcpSessionPromptResult)({
110 type: "connection",111 stopReason: "end_turn",
111 subtype: "reconnecting",
112 session_id: "sess-1",
113 });112 });
114 expect(reconnecting.type).toBe("connection");
115113
116 const retry = Schema.decodeUnknownSync(CursorCliStreamEvent)({114 expect(decoded.stopReason).toBe("end_turn");
117 type: "retry",
118 subtype: "resuming",
119 session_id: "sess-1",
120 });
121 expect(retry.type).toBe("retry");
122 });115 });
123116
124 it("rejects unsupported stream event types", () => {117 it("rejects unsupported update types", () => {
125 expect(() =>118 expect(() =>
126 Schema.decodeUnknownSync(CursorCliStreamEvent)({119 Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification)({
127 type: "bogus",120 jsonrpc: "2.0",
128 session_id: "sess-1",121 method: "session/update",
122 params: {
123 sessionId: "sess-1",
124 update: {
125 sessionUpdate: "unknown_update",
126 },
127 },
129 }),128 }),
130 ).toThrow();129 ).toThrow();
131 });130 });
apps/server/src/provider/Services/CursorAdapter.ts+74-178
...@@ -1,11 +1,7 @@...@@ -1,11 +1,7 @@
1/**1/**
2 * CursorAdapter - Cursor CLI implementation of the generic provider adapter contract.2 * CursorAdapter - Cursor ACP implementation of the generic provider adapter contract.
3 *3 *
4 * This service will own Cursor CLI (`agent`) process / stream-json semantics and emit4 * Defines ACP JSON-RPC schemas used by the Cursor adapter layer.
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.
9 *5 *
10 * @module CursorAdapter6 * @module CursorAdapter
11 */7 */
...@@ -14,201 +10,101 @@ import { Schema, ServiceMap } from "effect";...@@ -14,201 +10,101 @@ import { Schema, ServiceMap } from "effect";
14import type { ProviderAdapterError } from "../Errors.ts";10import type { ProviderAdapterError } from "../Errors.ts";
15import type { ProviderAdapterShape } from "./ProviderAdapter.ts";11import type { ProviderAdapterShape } from "./ProviderAdapter.ts";
1612
17export const CursorCliSessionId = Schema.String.check(Schema.isNonEmpty());13export const CursorAcpJsonRpcId = Schema.Union([Schema.String, Schema.Int]);
18export type CursorCliSessionId = typeof CursorCliSessionId.Type;14export type CursorAcpJsonRpcId = typeof CursorAcpJsonRpcId.Type;
1915
20export const CursorCliTextContentPart = Schema.Struct({16export const CursorAcpTextContent = Schema.Struct({
21 type: Schema.Literal("text"),17 type: Schema.Literal("text"),
22 text: Schema.String,18 text: Schema.String,
23});19});
24export type CursorCliTextContentPart = typeof CursorCliTextContentPart.Type;20export type CursorAcpTextContent = typeof CursorAcpTextContent.Type;
2521
26export const CursorCliContentPart = Schema.Union([22export const CursorAcpSessionUpdate = Schema.Union([
27 CursorCliTextContentPart,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 }),
28 Schema.Struct({32 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),
30 }),53 }),
31]);54]);
32export type CursorCliContentPart = typeof CursorCliContentPart.Type;55export type CursorAcpSessionUpdate = typeof CursorAcpSessionUpdate.Type;
3356
34export const CursorCliUserMessage = Schema.Struct({57export const CursorAcpSessionUpdateNotification = Schema.Struct({
35 role: Schema.Literal("user"),58 jsonrpc: Schema.optional(Schema.Literal("2.0")),
36 content: Schema.Array(CursorCliContentPart),59 method: Schema.Literal("session/update"),
37});60 params: Schema.Struct({
38export type CursorCliUserMessage = typeof CursorCliUserMessage.Type;61 sessionId: Schema.String,
3962 update: CursorAcpSessionUpdate,
40export const CursorCliAssistantMessage = Schema.Struct({63 }),
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,
131});64});
132export type CursorCliThinkingCompletedEvent = typeof CursorCliThinkingCompletedEvent.Type;65export type CursorAcpSessionUpdateNotification = typeof CursorAcpSessionUpdateNotification.Type;
13366
134export const CursorCliToolCallStartedEvent = Schema.Struct({67export const CursorAcpPermissionOption = Schema.Struct({
135 type: Schema.Literal("tool_call"),68 optionId: Schema.String,
136 subtype: Schema.Literal("started"),69 name: Schema.optional(Schema.String),
137 call_id: Schema.String,70 kind: Schema.optional(Schema.String),
138 tool_call: CursorCliToolCallPayload,
139 session_id: CursorCliSessionId,
140 ...CursorCliTimestampMs,
141});71});
142export type CursorCliToolCallStartedEvent = typeof CursorCliToolCallStartedEvent.Type;72export type CursorAcpPermissionOption = typeof CursorAcpPermissionOption.Type;
14373
144export const CursorCliToolCallCompletedEvent = Schema.Struct({74export const CursorAcpPermissionRequest = Schema.Struct({
145 type: Schema.Literal("tool_call"),75 jsonrpc: Schema.optional(Schema.Literal("2.0")),
146 subtype: Schema.Literal("completed"),76 id: CursorAcpJsonRpcId,
147 call_id: Schema.String,77 method: Schema.Literal("session/request_permission"),
148 tool_call: CursorCliToolCallPayload,78 params: Schema.Struct({
149 session_id: CursorCliSessionId,79 sessionId: Schema.String,
150 ...CursorCliTimestampMs,80 toolCall: Schema.optional(Schema.Unknown),
81 options: Schema.Array(CursorAcpPermissionOption),
82 }),
151});83});
152export type CursorCliToolCallCompletedEvent = typeof CursorCliToolCallCompletedEvent.Type;84export type CursorAcpPermissionRequest = typeof CursorAcpPermissionRequest.Type;
15385
154export const CursorCliResultSuccessEvent = Schema.Struct({86export const CursorAcpInitializeResult = Schema.Struct({
155 type: Schema.Literal("result"),87 protocolVersion: Schema.optional(Schema.Int),
156 subtype: Schema.Literal("success"),88 agentCapabilities: Schema.optional(Schema.Unknown),
157 duration_ms: Schema.Number,89 authMethods: Schema.optional(Schema.Array(Schema.Unknown)),
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,
165});90});
166export type CursorCliResultSuccessEvent = typeof CursorCliResultSuccessEvent.Type;91export type CursorAcpInitializeResult = typeof CursorAcpInitializeResult.Type;
16792
168export const CursorCliConnectionEvent = Schema.Struct({93export const CursorAcpSessionNewResult = Schema.Struct({
169 type: Schema.Literal("connection"),94 sessionId: Schema.String,
170 subtype: Schema.Literals(["reconnecting", "reconnected"]),95 modes: Schema.optional(Schema.Unknown),
171 session_id: Schema.optional(CursorCliSessionId),
172 ...CursorCliTimestampMs,
173});96});
174export type CursorCliConnectionEvent = typeof CursorCliConnectionEvent.Type;97export type CursorAcpSessionNewResult = typeof CursorAcpSessionNewResult.Type;
17598
176export const CursorCliRetryEvent = Schema.Struct({99export const CursorAcpSessionPromptResult = Schema.Struct({
177 type: Schema.Literal("retry"),100 stopReason: Schema.optional(Schema.String),
178 subtype: Schema.Literals(["starting", "resuming"]),
179 session_id: Schema.optional(CursorCliSessionId),
180 ...CursorCliTimestampMs,
181});101});
182export type CursorCliRetryEvent = typeof CursorCliRetryEvent.Type;102export type CursorAcpSessionPromptResult = typeof CursorAcpSessionPromptResult.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;
197103
198/**104export interface CursorAdapterShape extends ProviderAdapterShape<ProviderAdapterError> {
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"> {
206 readonly provider: "cursor";105 readonly provider: "cursor";
207}106}
208107
209/**
210 * CursorAdapter - Service tag for Cursor provider adapter operations.
211 */
212export class CursorAdapter extends ServiceMap.Service<CursorAdapter, CursorAdapterShape>()(108export class CursorAdapter extends ServiceMap.Service<CursorAdapter, CursorAdapterShape>()(
213 "t3/provider/Services/CursorAdapter",109 "t3/provider/Services/CursorAdapter",
214) {}110) {}
apps/server/src/serverLayers.ts+5-1
...@@ -20,6 +20,7 @@ import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRun...@@ -20,6 +20,7 @@ import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRun
20import { ProviderUnsupportedError } from "./provider/Errors";20import { ProviderUnsupportedError } from "./provider/Errors";
21import { makeClaudeCodeAdapterLive } from "./provider/Layers/ClaudeCodeAdapter";21import { makeClaudeCodeAdapterLive } from "./provider/Layers/ClaudeCodeAdapter";
22import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter";22import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter";
23import { makeCursorAdapterLive } from "./provider/Layers/CursorAdapter";
23import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";24import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";
24import { makeProviderServiceLive } from "./provider/Layers/ProviderService";25import { makeProviderServiceLive } from "./provider/Layers/ProviderService";
25import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory";26import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory";
...@@ -60,8 +61,11 @@ export function makeServerProviderLayer(): Layer.Layer<...@@ -60,8 +61,11 @@ export function makeServerProviderLayer(): Layer.Layer<
60 const claudeAdapterLayer = makeClaudeCodeAdapterLive({61 const claudeAdapterLayer = makeClaudeCodeAdapterLive({
61 nativeEventLogPath: path.join(providerLogsDir, "provider-native.ndjson"),62 nativeEventLogPath: path.join(providerLogsDir, "provider-native.ndjson"),
62 });63 });
64 const cursorAdapterLayer = makeCursorAdapterLive({
65 nativeEventLogPath: path.join(providerLogsDir, "provider-native.ndjson"),
66 });
63 const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe(67 const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe(
64 Layer.provide(Layer.mergeAll(codexAdapterLayer, claudeAdapterLayer)),68 Layer.provide(Layer.mergeAll(codexAdapterLayer, claudeAdapterLayer, cursorAdapterLayer)),
65 Layer.provideMerge(providerSessionDirectoryLayer),69 Layer.provideMerge(providerSessionDirectoryLayer),
66 );70 );
67 return makeProviderServiceLive(71 return makeProviderServiceLive(
packages/contracts/src/provider.test.ts+6
...@@ -52,11 +52,17 @@ describe("ProviderSessionStartInput", () => {...@@ -52,11 +52,17 @@ describe("ProviderSessionStartInput", () => {
52 provider: "cursor",52 provider: "cursor",
53 cwd: "/tmp/workspace",53 cwd: "/tmp/workspace",
54 model: "composer-1.5",54 model: "composer-1.5",
55 providerOptions: {
56 cursor: {
57 binaryPath: "/usr/local/bin/agent",
58 },
59 },
55 approvalPolicy: "on-request",60 approvalPolicy: "on-request",
56 sandboxMode: "workspace-write",61 sandboxMode: "workspace-write",
57 });62 });
58 expect(parsed.provider).toBe("cursor");63 expect(parsed.provider).toBe("cursor");
59 expect(parsed.model).toBe("composer-1.5");64 expect(parsed.model).toBe("composer-1.5");
65 expect(parsed.providerOptions?.cursor?.binaryPath).toBe("/usr/local/bin/agent");
60 expect(parsed.approvalPolicy).toBe("on-request");66 expect(parsed.approvalPolicy).toBe("on-request");
61 expect(parsed.sandboxMode).toBe("workspace-write");67 expect(parsed.sandboxMode).toBe("workspace-write");
62 });68 });
packages/contracts/src/provider.ts+6
...@@ -62,9 +62,15 @@ export const ClaudeCodeProviderStartOptions = Schema.Struct({...@@ -62,9 +62,15 @@ export const ClaudeCodeProviderStartOptions = Schema.Struct({
62});62});
63export type ClaudeCodeProviderStartOptions = typeof ClaudeCodeProviderStartOptions.Type;63export type ClaudeCodeProviderStartOptions = typeof ClaudeCodeProviderStartOptions.Type;
6464
65export const CursorProviderStartOptions = Schema.Struct({
66 binaryPath: Schema.optional(TrimmedNonEmptyStringSchema),
67});
68export type CursorProviderStartOptions = typeof CursorProviderStartOptions.Type;
69
65export const ProviderStartOptions = Schema.Struct({70export const ProviderStartOptions = Schema.Struct({
66 codex: Schema.optional(CodexProviderStartOptions),71 codex: Schema.optional(CodexProviderStartOptions),
67 claudeCode: Schema.optional(ClaudeCodeProviderStartOptions),72 claudeCode: Schema.optional(ClaudeCodeProviderStartOptions),
73 cursor: Schema.optional(CursorProviderStartOptions),
68});74});
69export type ProviderStartOptions = typeof ProviderStartOptions.Type;75export type ProviderStartOptions = typeof ProviderStartOptions.Type;
7076
packages/contracts/src/providerRuntime.ts+526-9
...@@ -9,6 +9,7 @@ import {...@@ -9,6 +9,7 @@ import {
9 RuntimeItemId,9 RuntimeItemId,
10 RuntimeRequestId,10 RuntimeRequestId,
11 RuntimeSessionId,11 RuntimeSessionId,
12 RuntimeTaskId,
12 ThreadId,13 ThreadId,
13 TrimmedNonEmptyString,14 TrimmedNonEmptyString,
14 TurnId,15 TurnId,
...@@ -25,6 +26,9 @@ export const RuntimeEventRawSource = Schema.Literals([...@@ -25,6 +26,9 @@ export const RuntimeEventRawSource = Schema.Literals([
25 "claude.sdk.message",26 "claude.sdk.message",
26 "claude.sdk.permission",27 "claude.sdk.permission",
27 "codex.sdk.thread-event",28 "codex.sdk.thread-event",
29 "cursor.acp.notification",
30 "cursor.acp.request",
31 "cursor.acp.response",
28]);32]);
29export type RuntimeEventRawSource = typeof RuntimeEventRawSource.Type;33export type RuntimeEventRawSource = typeof RuntimeEventRawSource.Type;
3034
...@@ -48,6 +52,26 @@ export const ProviderRefs = Schema.Struct({...@@ -48,6 +52,26 @@ export const ProviderRefs = Schema.Struct({
48});52});
49export type ProviderRefs = typeof ProviderRefs.Type;53export 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
51export const RuntimeTurnState = Schema.Literals([75export const RuntimeTurnState = Schema.Literals([
52 "completed",76 "completed",
53 "failed",77 "failed",
...@@ -94,6 +118,12 @@ export const CanonicalItemType = Schema.Literals([...@@ -94,6 +118,12 @@ export const CanonicalItemType = Schema.Literals([
94 "file_change",118 "file_change",
95 "mcp_tool_call",119 "mcp_tool_call",
96 "dynamic_tool_call",120 "dynamic_tool_call",
121 "collab_agent_tool_call",
122 "web_search",
123 "image_view",
124 "review_entered",
125 "review_exited",
126 "context_compaction",
97 "error",127 "error",
98 "unknown",128 "unknown",
99]);129]);
...@@ -106,17 +136,30 @@ export const CanonicalRequestType = Schema.Literals([...@@ -106,17 +136,30 @@ export const CanonicalRequestType = Schema.Literals([
106 "exec_command_approval",136 "exec_command_approval",
107 "tool_user_input",137 "tool_user_input",
108 "dynamic_tool_call",138 "dynamic_tool_call",
139 "auth_tokens_refresh",
109 "unknown",140 "unknown",
110]);141]);
111export type CanonicalRequestType = typeof CanonicalRequestType.Type;142export type CanonicalRequestType = typeof CanonicalRequestType.Type;
112143
113export const ProviderRuntimeEventType = Schema.Literals([144export const ProviderRuntimeEventType = Schema.Literals([
114 "session.started",145 "session.started",
146 "session.configured",
147 "session.state.changed",
115 "session.exited",148 "session.exited",
116 "thread.started",149 "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",
117 "turn.started",158 "turn.started",
118 "turn.completed",159 "turn.completed",
160 "turn.aborted",
119 "turn.plan.updated",161 "turn.plan.updated",
162 "turn.diff.updated",
120 "item.started",163 "item.started",
121 "item.updated",164 "item.updated",
122 "item.completed",165 "item.completed",
...@@ -125,17 +168,46 @@ export const ProviderRuntimeEventType = Schema.Literals([...@@ -125,17 +168,46 @@ export const ProviderRuntimeEventType = Schema.Literals([
125 "request.resolved",168 "request.resolved",
126 "user-input.requested",169 "user-input.requested",
127 "user-input.resolved",170 "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",
128 "runtime.warning",188 "runtime.warning",
129 "runtime.error",189 "runtime.error",
130]);190]);
131export type ProviderRuntimeEventType = typeof ProviderRuntimeEventType.Type;191export type ProviderRuntimeEventType = typeof ProviderRuntimeEventType.Type;
132192
133export const SessionStartedType = Schema.Literal("session.started");193export const SessionStartedType = Schema.Literal("session.started");
194export const SessionConfiguredType = Schema.Literal("session.configured");
195export const SessionStateChangedType = Schema.Literal("session.state.changed");
134export const SessionExitedType = Schema.Literal("session.exited");196export const SessionExitedType = Schema.Literal("session.exited");
135export const ThreadStartedType = Schema.Literal("thread.started");197export 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");
136export const TurnStartedType = Schema.Literal("turn.started");206export const TurnStartedType = Schema.Literal("turn.started");
137export const TurnCompletedType = Schema.Literal("turn.completed");207export const TurnCompletedType = Schema.Literal("turn.completed");
208export const TurnAbortedType = Schema.Literal("turn.aborted");
138export const TurnPlanUpdatedType = Schema.Literal("turn.plan.updated");209export const TurnPlanUpdatedType = Schema.Literal("turn.plan.updated");
210export const TurnDiffUpdatedType = Schema.Literal("turn.diff.updated");
139export const ItemStartedType = Schema.Literal("item.started");211export const ItemStartedType = Schema.Literal("item.started");
140export const ItemUpdatedType = Schema.Literal("item.updated");212export const ItemUpdatedType = Schema.Literal("item.updated");
141export const ItemCompletedType = Schema.Literal("item.completed");213export const ItemCompletedType = Schema.Literal("item.completed");
...@@ -144,6 +216,23 @@ export const RequestOpenedType = Schema.Literal("request.opened");...@@ -144,6 +216,23 @@ export const RequestOpenedType = Schema.Literal("request.opened");
144export const RequestResolvedType = Schema.Literal("request.resolved");216export const RequestResolvedType = Schema.Literal("request.resolved");
145export const UserInputRequestedType = Schema.Literal("user-input.requested");217export const UserInputRequestedType = Schema.Literal("user-input.requested");
146export const UserInputResolvedType = Schema.Literal("user-input.resolved");218export 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");
147export const RuntimeWarningType = Schema.Literal("runtime.warning");236export const RuntimeWarningType = Schema.Literal("runtime.warning");
148export const RuntimeErrorType = Schema.Literal("runtime.error");237export const RuntimeErrorType = Schema.Literal("runtime.error");
149238
...@@ -167,6 +256,18 @@ export const SessionStartedPayload = Schema.Struct({...@@ -167,6 +256,18 @@ export const SessionStartedPayload = Schema.Struct({
167});256});
168export type SessionStartedPayload = typeof SessionStartedPayload.Type;257export 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
170export const SessionExitedPayload = Schema.Struct({271export const SessionExitedPayload = Schema.Struct({
171 reason: Schema.optional(TrimmedNonEmptyStringSchema),272 reason: Schema.optional(TrimmedNonEmptyStringSchema),
172 recoverable: Schema.optional(Schema.Boolean),273 recoverable: Schema.optional(Schema.Boolean),
...@@ -179,6 +280,48 @@ export const ThreadStartedPayload = Schema.Struct({...@@ -179,6 +280,48 @@ export const ThreadStartedPayload = Schema.Struct({
179});280});
180export type ThreadStartedPayload = typeof ThreadStartedPayload.Type;281export 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
182export const TurnStartedPayload = Schema.Struct({325export const TurnStartedPayload = Schema.Struct({
183 model: Schema.optional(TrimmedNonEmptyStringSchema),326 model: Schema.optional(TrimmedNonEmptyStringSchema),
184 effort: Schema.optional(TrimmedNonEmptyStringSchema),327 effort: Schema.optional(TrimmedNonEmptyStringSchema),
...@@ -195,6 +338,11 @@ export const TurnCompletedPayload = Schema.Struct({...@@ -195,6 +338,11 @@ export const TurnCompletedPayload = Schema.Struct({
195});338});
196export type TurnCompletedPayload = typeof TurnCompletedPayload.Type;339export type TurnCompletedPayload = typeof TurnCompletedPayload.Type;
197340
341export const TurnAbortedPayload = Schema.Struct({
342 reason: TrimmedNonEmptyStringSchema,
343});
344export type TurnAbortedPayload = typeof TurnAbortedPayload.Type;
345
198export const RuntimePlanStep = Schema.Struct({346export const RuntimePlanStep = Schema.Struct({
199 step: TrimmedNonEmptyStringSchema,347 step: TrimmedNonEmptyStringSchema,
200 status: RuntimePlanStepStatus,348 status: RuntimePlanStepStatus,
...@@ -207,6 +355,11 @@ export const TurnPlanUpdatedPayload = Schema.Struct({...@@ -207,6 +355,11 @@ export const TurnPlanUpdatedPayload = Schema.Struct({
207});355});
208export type TurnPlanUpdatedPayload = typeof TurnPlanUpdatedPayload.Type;356export type TurnPlanUpdatedPayload = typeof TurnPlanUpdatedPayload.Type;
209357
358export const TurnDiffUpdatedPayload = Schema.Struct({
359 unifiedDiff: Schema.String,
360});
361export type TurnDiffUpdatedPayload = typeof TurnDiffUpdatedPayload.Type;
362
210export const ItemLifecyclePayload = Schema.Struct({363export const ItemLifecyclePayload = Schema.Struct({
211 itemType: CanonicalItemType,364 itemType: CanonicalItemType,
212 status: Schema.optional(RuntimeItemStatus),365 status: Schema.optional(RuntimeItemStatus),
...@@ -262,6 +415,136 @@ export const UserInputResolvedPayload = Schema.Struct({...@@ -262,6 +415,136 @@ export const UserInputResolvedPayload = Schema.Struct({
262});415});
263export type UserInputResolvedPayload = typeof UserInputResolvedPayload.Type;416export 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
265export const RuntimeWarningPayload = Schema.Struct({548export const RuntimeWarningPayload = Schema.Struct({
266 message: TrimmedNonEmptyStringSchema,549 message: TrimmedNonEmptyStringSchema,
267 detail: Schema.optional(Schema.Unknown),550 detail: Schema.optional(Schema.Unknown),
...@@ -282,6 +565,20 @@ export const ProviderRuntimeSessionStartedEvent = Schema.Struct({...@@ -282,6 +565,20 @@ export const ProviderRuntimeSessionStartedEvent = Schema.Struct({
282});565});
283export type ProviderRuntimeSessionStartedEvent = typeof ProviderRuntimeSessionStartedEvent.Type;566export 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
285export const ProviderRuntimeSessionExitedEvent = Schema.Struct({582export const ProviderRuntimeSessionExitedEvent = Schema.Struct({
286 ...ProviderRuntimeEventBase.fields,583 ...ProviderRuntimeEventBase.fields,
287 type: SessionExitedType,584 type: SessionExitedType,
...@@ -296,6 +593,68 @@ export const ProviderRuntimeThreadStartedEvent = Schema.Struct({...@@ -296,6 +593,68 @@ export const ProviderRuntimeThreadStartedEvent = Schema.Struct({
296});593});
297export type ProviderRuntimeThreadStartedEvent = typeof ProviderRuntimeThreadStartedEvent.Type;594export 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
299export const ProviderRuntimeTurnStartedEvent = Schema.Struct({658export const ProviderRuntimeTurnStartedEvent = Schema.Struct({
300 ...ProviderRuntimeEventBase.fields,659 ...ProviderRuntimeEventBase.fields,
301 type: TurnStartedType,660 type: TurnStartedType,
...@@ -310,6 +669,13 @@ export const ProviderRuntimeTurnCompletedEvent = Schema.Struct({...@@ -310,6 +669,13 @@ export const ProviderRuntimeTurnCompletedEvent = Schema.Struct({
310});669});
311export type ProviderRuntimeTurnCompletedEvent = typeof ProviderRuntimeTurnCompletedEvent.Type;670export 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
313export const ProviderRuntimeTurnPlanUpdatedEvent = Schema.Struct({679export const ProviderRuntimeTurnPlanUpdatedEvent = Schema.Struct({
314 ...ProviderRuntimeEventBase.fields,680 ...ProviderRuntimeEventBase.fields,
315 type: TurnPlanUpdatedType,681 type: TurnPlanUpdatedType,
...@@ -317,6 +683,13 @@ export const ProviderRuntimeTurnPlanUpdatedEvent = Schema.Struct({...@@ -317,6 +683,13 @@ export const ProviderRuntimeTurnPlanUpdatedEvent = Schema.Struct({
317});683});
318export type ProviderRuntimeTurnPlanUpdatedEvent = typeof ProviderRuntimeTurnPlanUpdatedEvent.Type;684export 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
320export const ProviderRuntimeItemStartedEvent = Schema.Struct({693export const ProviderRuntimeItemStartedEvent = Schema.Struct({
321 ...ProviderRuntimeEventBase.fields,694 ...ProviderRuntimeEventBase.fields,
322 type: ItemStartedType,695 type: ItemStartedType,
...@@ -372,8 +745,127 @@ export const ProviderRuntimeUserInputResolvedEvent = Schema.Struct({...@@ -372,8 +745,127 @@ export const ProviderRuntimeUserInputResolvedEvent = Schema.Struct({
372 type: UserInputResolvedType,745 type: UserInputResolvedType,
373 payload: UserInputResolvedPayload,746 payload: UserInputResolvedPayload,
374});747});
375export type ProviderRuntimeUserInputResolvedEvent =748export type ProviderRuntimeUserInputResolvedEvent = typeof ProviderRuntimeUserInputResolvedEvent.Type;
376 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
378export const ProviderRuntimeWarningEvent = Schema.Struct({870export const ProviderRuntimeWarningEvent = Schema.Struct({
379 ...ProviderRuntimeEventBase.fields,871 ...ProviderRuntimeEventBase.fields,
...@@ -391,11 +883,23 @@ export type ProviderRuntimeErrorEvent = typeof ProviderRuntimeErrorEvent.Type;...@@ -391,11 +883,23 @@ export type ProviderRuntimeErrorEvent = typeof ProviderRuntimeErrorEvent.Type;
391883
392export const ProviderRuntimeEventV2 = Schema.Union([884export const ProviderRuntimeEventV2 = Schema.Union([
393 ProviderRuntimeSessionStartedEvent,885 ProviderRuntimeSessionStartedEvent,
886 ProviderRuntimeSessionConfiguredEvent,
887 ProviderRuntimeSessionStateChangedEvent,
394 ProviderRuntimeSessionExitedEvent,888 ProviderRuntimeSessionExitedEvent,
395 ProviderRuntimeThreadStartedEvent,889 ProviderRuntimeThreadStartedEvent,
890 ProviderRuntimeThreadStateChangedEvent,
891 ProviderRuntimeThreadMetadataUpdatedEvent,
892 ProviderRuntimeThreadTokenUsageUpdatedEvent,
893 ProviderRuntimeThreadRealtimeStartedEvent,
894 ProviderRuntimeThreadRealtimeItemAddedEvent,
895 ProviderRuntimeThreadRealtimeAudioDeltaEvent,
896 ProviderRuntimeThreadRealtimeErrorEvent,
897 ProviderRuntimeThreadRealtimeClosedEvent,
396 ProviderRuntimeTurnStartedEvent,898 ProviderRuntimeTurnStartedEvent,
397 ProviderRuntimeTurnCompletedEvent,899 ProviderRuntimeTurnCompletedEvent,
900 ProviderRuntimeTurnAbortedEvent,
398 ProviderRuntimeTurnPlanUpdatedEvent,901 ProviderRuntimeTurnPlanUpdatedEvent,
902 ProviderRuntimeTurnDiffUpdatedEvent,
399 ProviderRuntimeItemStartedEvent,903 ProviderRuntimeItemStartedEvent,
400 ProviderRuntimeItemUpdatedEvent,904 ProviderRuntimeItemUpdatedEvent,
401 ProviderRuntimeItemCompletedEvent,905 ProviderRuntimeItemCompletedEvent,
...@@ -404,6 +908,23 @@ export const ProviderRuntimeEventV2 = Schema.Union([...@@ -404,6 +908,23 @@ export const ProviderRuntimeEventV2 = Schema.Union([
404 ProviderRuntimeRequestResolvedEvent,908 ProviderRuntimeRequestResolvedEvent,
405 ProviderRuntimeUserInputRequestedEvent,909 ProviderRuntimeUserInputRequestedEvent,
406 ProviderRuntimeUserInputResolvedEvent,910 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,
407 ProviderRuntimeWarningEvent,928 ProviderRuntimeWarningEvent,
408 ProviderRuntimeErrorEvent,929 ProviderRuntimeErrorEvent,
409]);930]);
...@@ -412,6 +933,7 @@ export type ProviderRuntimeEventV2 = typeof ProviderRuntimeEventV2.Type;...@@ -412,6 +933,7 @@ export type ProviderRuntimeEventV2 = typeof ProviderRuntimeEventV2.Type;
412export const ProviderRuntimeEvent = ProviderRuntimeEventV2;933export const ProviderRuntimeEvent = ProviderRuntimeEventV2;
413export type ProviderRuntimeEvent = ProviderRuntimeEventV2;934export type ProviderRuntimeEvent = ProviderRuntimeEventV2;
414935
936// Compatibility aliases for call sites still importing legacy names.
415export const ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent;937export const ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent;
416export type ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent;938export type ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent;
417export const ProviderRuntimeMessageCompletedEvent = ProviderRuntimeItemCompletedEvent;939export const ProviderRuntimeMessageCompletedEvent = ProviderRuntimeItemCompletedEvent;
...@@ -425,13 +947,8 @@ export type ProviderRuntimeApprovalRequestedEvent = ProviderRuntimeRequestOpened...@@ -425,13 +947,8 @@ export type ProviderRuntimeApprovalRequestedEvent = ProviderRuntimeRequestOpened
425export const ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent;947export const ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent;
426export type ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent;948export type ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent;
427949
428export const ProviderRuntimeToolKind = Schema.Literals([950// Legacy helper aliases retained for adapters/tests.
429 "command_execution",951export const ProviderRuntimeToolKind = Schema.Literals(["command", "file-change", "other"]);
430 "file_change",
431 "mcp_tool_call",
432 "dynamic_tool_call",
433 "other",
434]);
435export type ProviderRuntimeToolKind = typeof ProviderRuntimeToolKind.Type;952export type ProviderRuntimeToolKind = typeof ProviderRuntimeToolKind.Type;
436953
437export const ProviderRuntimeTurnStatus = RuntimeTurnState;954export const ProviderRuntimeTurnStatus = RuntimeTurnState;