authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-27 17:35:49-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-27 17:35:49-07:00
log59a383e1ea4914ea12d3551dce2f715b59218618
treef614cac245f92ea6957c75c759292fca657f8b6a
parentfb7260792fbbb84252213541a76dd61d52243bdf
signature Signed by PGP key B5690EEEBB952194

Add Effect.fn refactor checklist (#1476)


2 files changed, 2165 insertions(+), 2041 deletions(-)

apps/server/src/provider/Layers/ClaudeAdapter.ts+1982-2041
...@@ -552,69 +552,67 @@ function buildClaudeImageContentBlock(input: {...@@ -552,69 +552,67 @@ function buildClaudeImageContentBlock(input: {
552 };552 };
553}553}
554554
555function buildUserMessageEffect(555const buildUserMessageEffect = Effect.fn("buildUserMessageEffect")(function* (
556 input: ProviderSendTurnInput,556 input: ProviderSendTurnInput,
557 dependencies: {557 dependencies: {
558 readonly fileSystem: FileSystem.FileSystem;558 readonly fileSystem: FileSystem.FileSystem;
559 readonly attachmentsDir: string;559 readonly attachmentsDir: string;
560 },560 },
561): Effect.Effect<SDKUserMessage, ProviderAdapterRequestError> {561) {
562 return Effect.gen(function* () {562 const text = buildPromptText(input);
563 const text = buildPromptText(input);563 const sdkContent: Array<Record<string, unknown>> = [];
564 const sdkContent: Array<Record<string, unknown>> = [];
565564
566 if (text.length > 0) {565 if (text.length > 0) {
567 sdkContent.push({ type: "text", text });566 sdkContent.push({ type: "text", text });
568 }567 }
569568
570 for (const attachment of input.attachments ?? []) {569 for (const attachment of input.attachments ?? []) {
571 if (attachment.type !== "image") {570 if (attachment.type !== "image") {
572 continue;571 continue;
573 }572 }
574573
575 if (!SUPPORTED_CLAUDE_IMAGE_MIME_TYPES.has(attachment.mimeType)) {574 if (!SUPPORTED_CLAUDE_IMAGE_MIME_TYPES.has(attachment.mimeType)) {
576 return yield* new ProviderAdapterRequestError({575 return yield* new ProviderAdapterRequestError({
577 provider: PROVIDER,576 provider: PROVIDER,
578 method: "turn/start",577 method: "turn/start",
579 detail: `Unsupported Claude image attachment type '${attachment.mimeType}'.`,578 detail: `Unsupported Claude image attachment type '${attachment.mimeType}'.`,
580 });579 });
581 }580 }
582581
583 const attachmentPath = resolveAttachmentPath({582 const attachmentPath = resolveAttachmentPath({
584 attachmentsDir: dependencies.attachmentsDir,583 attachmentsDir: dependencies.attachmentsDir,
585 attachment,584 attachment,
585 });
586 if (!attachmentPath) {
587 return yield* new ProviderAdapterRequestError({
588 provider: PROVIDER,
589 method: "turn/start",
590 detail: `Invalid attachment id '${attachment.id}'.`,
586 });591 });
587 if (!attachmentPath) {592 }
588 return yield* new ProviderAdapterRequestError({
589 provider: PROVIDER,
590 method: "turn/start",
591 detail: `Invalid attachment id '${attachment.id}'.`,
592 });
593 }
594593
595 const bytes = yield* dependencies.fileSystem.readFile(attachmentPath).pipe(594 const bytes = yield* dependencies.fileSystem.readFile(attachmentPath).pipe(
596 Effect.mapError(595 Effect.mapError(
597 (cause) =>596 (cause) =>
598 new ProviderAdapterRequestError({597 new ProviderAdapterRequestError({
599 provider: PROVIDER,598 provider: PROVIDER,
600 method: "turn/start",599 method: "turn/start",
601 detail: toMessage(cause, "Failed to read attachment file."),600 detail: toMessage(cause, "Failed to read attachment file."),
602 cause,601 cause,
603 }),602 }),
604 ),603 ),
605 );604 );
606605
607 sdkContent.push(606 sdkContent.push(
608 buildClaudeImageContentBlock({607 buildClaudeImageContentBlock({
609 mimeType: attachment.mimeType,608 mimeType: attachment.mimeType,
610 bytes,609 bytes,
611 }),610 }),
612 );611 );
613 }612 }
614613
615 return buildUserMessage({ sdkContent });614 return buildUserMessage({ sdkContent });
616 });615});
617}
618616
619function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus {617function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus {
620 if (result.subtype === "success") {618 if (result.subtype === "success") {
...@@ -911,2198 +909,2141 @@ function sdkNativeItemId(message: SDKMessage): string | undefined {...@@ -911,2198 +909,2141 @@ function sdkNativeItemId(message: SDKMessage): string | undefined {
911 return undefined;909 return undefined;
912}910}
913911
914function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {912const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
915 return Effect.gen(function* () {913 options?: ClaudeAdapterLiveOptions,
916 const fileSystem = yield* FileSystem.FileSystem;914) {
917 const serverConfig = yield* ServerConfig;915 const fileSystem = yield* FileSystem.FileSystem;
918 const nativeEventLogger =916 const serverConfig = yield* ServerConfig;
919 options?.nativeEventLogger ??917 const nativeEventLogger =
920 (options?.nativeEventLogPath !== undefined918 options?.nativeEventLogger ??
921 ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, {919 (options?.nativeEventLogPath !== undefined
922 stream: "native",920 ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, {
923 })921 stream: "native",
924 : undefined);922 })
925923 : undefined);
926 const createQuery =924
927 options?.createQuery ??925 const createQuery =
928 ((input: {926 options?.createQuery ??
929 readonly prompt: AsyncIterable<SDKUserMessage>;927 ((input: {
930 readonly options: ClaudeQueryOptions;928 readonly prompt: AsyncIterable<SDKUserMessage>;
931 }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime);929 readonly options: ClaudeQueryOptions;
932930 }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime);
933 const sessions = new Map<ThreadId, ClaudeSessionContext>();931
934 const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();932 const sessions = new Map<ThreadId, ClaudeSessionContext>();
935 const serverSettingsService = yield* ServerSettingsService;933 const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();
936934 const serverSettingsService = yield* ServerSettingsService;
937 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);935
938 const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id));936 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
939 const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso });937 const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id));
940938 const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso });
941 const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>939
942 Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid);940 const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>
943941 Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid);
944 const logNativeSdkMessage = (942
945 context: ClaudeSessionContext,943 const logNativeSdkMessage = Effect.fn("logNativeSdkMessage")(function* (
946 message: SDKMessage,944 context: ClaudeSessionContext,
947 ): Effect.Effect<void> =>945 message: SDKMessage,
948 Effect.gen(function* () {946 ) {
949 if (!nativeEventLogger) {947 if (!nativeEventLogger) {
950 return;948 return;
951 }949 }
952950
953 const observedAt = new Date().toISOString();951 const observedAt = new Date().toISOString();
954 const itemId = sdkNativeItemId(message);952 const itemId = sdkNativeItemId(message);
955
956 yield* nativeEventLogger.write(
957 {
958 observedAt,
959 event: {
960 id:
961 "uuid" in message && typeof message.uuid === "string"
962 ? message.uuid
963 : crypto.randomUUID(),
964 kind: "notification",
965 provider: PROVIDER,
966 createdAt: observedAt,
967 method: sdkNativeMethod(message),
968 ...(typeof message.session_id === "string"
969 ? { providerThreadId: message.session_id }
970 : {}),
971 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
972 ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}),
973 payload: message,
974 },
975 },
976 context.session.threadId,
977 );
978 });
979953
980 const snapshotThread = (954 yield* nativeEventLogger.write(
981 context: ClaudeSessionContext,
982 ): Effect.Effect<
983 {955 {
984 threadId: ThreadId;956 observedAt,
985 turns: ReadonlyArray<{957 event: {
986 id: TurnId;958 id:
987 items: ReadonlyArray<unknown>;959 "uuid" in message && typeof message.uuid === "string"
988 }>;960 ? message.uuid
961 : crypto.randomUUID(),
962 kind: "notification",
963 provider: PROVIDER,
964 createdAt: observedAt,
965 method: sdkNativeMethod(message),
966 ...(typeof message.session_id === "string"
967 ? { providerThreadId: message.session_id }
968 : {}),
969 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
970 ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}),
971 payload: message,
972 },
989 },973 },
990 ProviderAdapterValidationError974 context.session.threadId,
991 > =>975 );
992 Effect.gen(function* () {976 });
993 const threadId = context.session.threadId;977
994 if (!threadId) {978 const snapshotThread = Effect.fn("snapshotThread")(function* (context: ClaudeSessionContext) {
995 return yield* new ProviderAdapterValidationError({979 const threadId = context.session.threadId;
996 provider: PROVIDER,980 if (!threadId) {
997 operation: "readThread",981 return yield* new ProviderAdapterValidationError({
998 issue: "Session thread id is not initialized yet.",982 provider: PROVIDER,
999 });983 operation: "readThread",
1000 }984 issue: "Session thread id is not initialized yet.",
1001 return {
1002 threadId,
1003 turns: context.turns.map((turn) => ({
1004 id: turn.id,
1005 items: [...turn.items],
1006 })),
1007 };
1008 });985 });
986 }
987 return {
988 threadId,
989 turns: context.turns.map((turn) => ({
990 id: turn.id,
991 items: [...turn.items],
992 })),
993 };
994 });
1009995
1010 const updateResumeCursor = (context: ClaudeSessionContext): Effect.Effect<void> =>996 const updateResumeCursor = Effect.fn("updateResumeCursor")(function* (
1011 Effect.gen(function* () {997 context: ClaudeSessionContext,
1012 const threadId = context.session.threadId;998 ) {
1013 if (!threadId) return;999 const threadId = context.session.threadId;
1000 if (!threadId) return;
10141001
1015 const resumeCursor = {1002 const resumeCursor = {
1016 threadId,1003 threadId,
1017 ...(context.resumeSessionId ? { resume: context.resumeSessionId } : {}),1004 ...(context.resumeSessionId ? { resume: context.resumeSessionId } : {}),
1018 ...(context.lastAssistantUuid ? { resumeSessionAt: context.lastAssistantUuid } : {}),1005 ...(context.lastAssistantUuid ? { resumeSessionAt: context.lastAssistantUuid } : {}),
1019 turnCount: context.turns.length,1006 turnCount: context.turns.length,
1020 };1007 };
10211008
1022 context.session = {1009 context.session = {
1023 ...context.session,1010 ...context.session,
1024 resumeCursor,1011 resumeCursor,
1025 updatedAt: yield* nowIso,1012 updatedAt: yield* nowIso,
1026 };1013 };
1014 });
1015
1016 const ensureAssistantTextBlock = Effect.fn("ensureAssistantTextBlock")(function* (
1017 context: ClaudeSessionContext,
1018 blockIndex: number,
1019 options?: {
1020 readonly fallbackText?: string;
1021 readonly streamClosed?: boolean;
1022 },
1023 ) {
1024 const turnState = context.turnState;
1025 if (!turnState) {
1026 return undefined;
1027 }
1028
1029 const existing = turnState.assistantTextBlocks.get(blockIndex);
1030 if (existing && !existing.completionEmitted) {
1031 if (existing.fallbackText.length === 0 && options?.fallbackText) {
1032 existing.fallbackText = options.fallbackText;
1033 }
1034 if (options?.streamClosed) {
1035 existing.streamClosed = true;
1036 }
1037 return { blockIndex, block: existing };
1038 }
1039
1040 const block: AssistantTextBlockState = {
1041 itemId: yield* Random.nextUUIDv4,
1042 blockIndex,
1043 emittedTextDelta: false,
1044 fallbackText: options?.fallbackText ?? "",
1045 streamClosed: options?.streamClosed ?? false,
1046 completionEmitted: false,
1047 };
1048 turnState.assistantTextBlocks.set(blockIndex, block);
1049 turnState.assistantTextBlockOrder.push(block);
1050 return { blockIndex, block };
1051 });
1052
1053 const createSyntheticAssistantTextBlock = Effect.fn("createSyntheticAssistantTextBlock")(
1054 function* (context: ClaudeSessionContext, fallbackText: string) {
1055 const turnState = context.turnState;
1056 if (!turnState) {
1057 return undefined;
1058 }
1059
1060 const blockIndex = turnState.nextSyntheticAssistantBlockIndex;
1061 turnState.nextSyntheticAssistantBlockIndex -= 1;
1062 return yield* ensureAssistantTextBlock(context, blockIndex, {
1063 fallbackText,
1064 streamClosed: true,
1027 });1065 });
1066 },
1067 );
10281068
1029 const ensureAssistantTextBlock = (1069 const completeAssistantTextBlock = Effect.fn("completeAssistantTextBlock")(function* (
1030 context: ClaudeSessionContext,1070 context: ClaudeSessionContext,
1031 blockIndex: number,1071 block: AssistantTextBlockState,
1032 options?: {1072 options?: {
1033 readonly fallbackText?: string;1073 readonly force?: boolean;
1034 readonly streamClosed?: boolean;1074 readonly rawMethod?: string;
1035 },1075 readonly rawPayload?: unknown;
1036 ): Effect.Effect<1076 },
1037 | {1077 ) {
1038 readonly blockIndex: number;1078 const turnState = context.turnState;
1039 readonly block: AssistantTextBlockState;1079 if (!turnState || block.completionEmitted) {
1040 }1080 return;
1041 | undefined1081 }
1042 > =>
1043 Effect.gen(function* () {
1044 const turnState = context.turnState;
1045 if (!turnState) {
1046 return undefined;
1047 }
10481082
1049 const existing = turnState.assistantTextBlocks.get(blockIndex);1083 if (!options?.force && !block.streamClosed) {
1050 if (existing && !existing.completionEmitted) {1084 return;
1051 if (existing.fallbackText.length === 0 && options?.fallbackText) {1085 }
1052 existing.fallbackText = options.fallbackText;
1053 }
1054 if (options?.streamClosed) {
1055 existing.streamClosed = true;
1056 }
1057 return { blockIndex, block: existing };
1058 }
10591086
1060 const block: AssistantTextBlockState = {1087 if (!block.emittedTextDelta && block.fallbackText.length > 0) {
1061 itemId: yield* Random.nextUUIDv4,1088 const deltaStamp = yield* makeEventStamp();
1062 blockIndex,1089 yield* offerRuntimeEvent({
1063 emittedTextDelta: false,1090 type: "content.delta",
1064 fallbackText: options?.fallbackText ?? "",1091 eventId: deltaStamp.eventId,
1065 streamClosed: options?.streamClosed ?? false,1092 provider: PROVIDER,
1066 completionEmitted: false,1093 createdAt: deltaStamp.createdAt,
1067 };1094 threadId: context.session.threadId,
1068 turnState.assistantTextBlocks.set(blockIndex, block);1095 turnId: turnState.turnId,
1069 turnState.assistantTextBlockOrder.push(block);1096 itemId: asRuntimeItemId(block.itemId),
1070 return { blockIndex, block };1097 payload: {
1098 streamKind: "assistant_text",
1099 delta: block.fallbackText,
1100 },
1101 providerRefs: nativeProviderRefs(context),
1102 ...(options?.rawMethod || options?.rawPayload
1103 ? {
1104 raw: {
1105 source: "claude.sdk.message" as const,
1106 ...(options.rawMethod ? { method: options.rawMethod } : {}),
1107 payload: options?.rawPayload,
1108 },
1109 }
1110 : {}),
1071 });1111 });
1112 }
10721113
1073 const createSyntheticAssistantTextBlock = (1114 block.completionEmitted = true;
1074 context: ClaudeSessionContext,1115 if (turnState.assistantTextBlocks.get(block.blockIndex) === block) {
1075 fallbackText: string,1116 turnState.assistantTextBlocks.delete(block.blockIndex);
1076 ): Effect.Effect<1117 }
1077 | {1118
1078 readonly blockIndex: number;1119 const stamp = yield* makeEventStamp();
1079 readonly block: AssistantTextBlockState;1120 yield* offerRuntimeEvent({
1080 }1121 type: "item.completed",
1081 | undefined1122 eventId: stamp.eventId,
1082 > =>1123 provider: PROVIDER,
1083 Effect.gen(function* () {1124 createdAt: stamp.createdAt,
1084 const turnState = context.turnState;1125 itemId: asRuntimeItemId(block.itemId),
1085 if (!turnState) {1126 threadId: context.session.threadId,
1086 return undefined;1127 turnId: turnState.turnId,
1087 }1128 payload: {
1129 itemType: "assistant_message",
1130 status: "completed",
1131 title: "Assistant message",
1132 ...(block.fallbackText.length > 0 ? { detail: block.fallbackText } : {}),
1133 },
1134 providerRefs: nativeProviderRefs(context),
1135 ...(options?.rawMethod || options?.rawPayload
1136 ? {
1137 raw: {
1138 source: "claude.sdk.message" as const,
1139 ...(options.rawMethod ? { method: options.rawMethod } : {}),
1140 payload: options?.rawPayload,
1141 },
1142 }
1143 : {}),
1144 });
1145 });
1146
1147 const backfillAssistantTextBlocksFromSnapshot = Effect.fn(
1148 "backfillAssistantTextBlocksFromSnapshot",
1149 )(function* (context: ClaudeSessionContext, message: SDKMessage) {
1150 const turnState = context.turnState;
1151 if (!turnState) {
1152 return;
1153 }
10881154
1089 const blockIndex = turnState.nextSyntheticAssistantBlockIndex;1155 const snapshotTextBlocks = extractAssistantTextBlocks(message);
1090 turnState.nextSyntheticAssistantBlockIndex -= 1;1156 if (snapshotTextBlocks.length === 0) {
1091 return yield* ensureAssistantTextBlock(context, blockIndex, {1157 return;
1092 fallbackText,1158 }
1093 streamClosed: true,1159
1160 const orderedBlocks = turnState.assistantTextBlockOrder.map((block) => ({
1161 blockIndex: block.blockIndex,
1162 block,
1163 }));
1164
1165 for (const [position, text] of snapshotTextBlocks.entries()) {
1166 const existingEntry = orderedBlocks[position];
1167 const entry =
1168 existingEntry ??
1169 (yield* createSyntheticAssistantTextBlock(context, text).pipe(
1170 Effect.map((created) => {
1171 if (!created) {
1172 return undefined;
1173 }
1174 orderedBlocks.push(created);
1175 return created;
1176 }),
1177 ));
1178 if (!entry) {
1179 continue;
1180 }
1181
1182 if (entry.block.fallbackText.length === 0) {
1183 entry.block.fallbackText = text;
1184 }
1185
1186 if (entry.block.streamClosed && !entry.block.completionEmitted) {
1187 yield* completeAssistantTextBlock(context, entry.block, {
1188 rawMethod: "claude/assistant",
1189 rawPayload: message,
1094 });1190 });
1191 }
1192 }
1193 });
1194
1195 const ensureThreadId = Effect.fn("ensureThreadId")(function* (
1196 context: ClaudeSessionContext,
1197 message: SDKMessage,
1198 ) {
1199 if (typeof message.session_id !== "string" || message.session_id.length === 0) {
1200 return;
1201 }
1202 const nextThreadId = message.session_id;
1203 context.resumeSessionId = message.session_id;
1204 yield* updateResumeCursor(context);
1205
1206 if (context.lastThreadStartedId !== nextThreadId) {
1207 context.lastThreadStartedId = nextThreadId;
1208 const stamp = yield* makeEventStamp();
1209 yield* offerRuntimeEvent({
1210 type: "thread.started",
1211 eventId: stamp.eventId,
1212 provider: PROVIDER,
1213 createdAt: stamp.createdAt,
1214 threadId: context.session.threadId,
1215 payload: {
1216 providerThreadId: nextThreadId,
1217 },
1218 providerRefs: {},
1219 raw: {
1220 source: "claude.sdk.message",
1221 method: "claude/thread/started",
1222 payload: {
1223 session_id: message.session_id,
1224 },
1225 },
1095 });1226 });
1227 }
1228 });
10961229
1097 const completeAssistantTextBlock = (1230 const emitRuntimeError = Effect.fn("emitRuntimeError")(function* (
1098 context: ClaudeSessionContext,1231 context: ClaudeSessionContext,
1099 block: AssistantTextBlockState,1232 message: string,
1100 options?: {1233 cause?: unknown,
1101 readonly force?: boolean;1234 ) {
1102 readonly rawMethod?: string;1235 if (cause !== undefined) {
1103 readonly rawPayload?: unknown;1236 void cause;
1237 }
1238 const turnState = context.turnState;
1239 const stamp = yield* makeEventStamp();
1240 yield* offerRuntimeEvent({
1241 type: "runtime.error",
1242 eventId: stamp.eventId,
1243 provider: PROVIDER,
1244 createdAt: stamp.createdAt,
1245 threadId: context.session.threadId,
1246 ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}),
1247 payload: {
1248 message,
1249 class: "provider_error",
1250 ...(cause !== undefined ? { detail: cause } : {}),
1104 },1251 },
1105 ): Effect.Effect<void> =>1252 providerRefs: nativeProviderRefs(context),
1106 Effect.gen(function* () {1253 });
1107 const turnState = context.turnState;1254 });
1108 if (!turnState || block.completionEmitted) {
1109 return;
1110 }
11111255
1112 if (!options?.force && !block.streamClosed) {1256 const emitRuntimeWarning = Effect.fn("emitRuntimeWarning")(function* (
1113 return;1257 context: ClaudeSessionContext,
1114 }1258 message: string,
1259 detail?: unknown,
1260 ) {
1261 const turnState = context.turnState;
1262 const stamp = yield* makeEventStamp();
1263 yield* offerRuntimeEvent({
1264 type: "runtime.warning",
1265 eventId: stamp.eventId,
1266 provider: PROVIDER,
1267 createdAt: stamp.createdAt,
1268 threadId: context.session.threadId,
1269 ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}),
1270 payload: {
1271 message,
1272 ...(detail !== undefined ? { detail } : {}),
1273 },
1274 providerRefs: nativeProviderRefs(context),
1275 });
1276 });
11151277
1116 if (!block.emittedTextDelta && block.fallbackText.length > 0) {1278 const emitProposedPlanCompleted = Effect.fn("emitProposedPlanCompleted")(function* (
1117 const deltaStamp = yield* makeEventStamp();1279 context: ClaudeSessionContext,
1118 yield* offerRuntimeEvent({1280 input: {
1119 type: "content.delta",1281 readonly planMarkdown: string;
1120 eventId: deltaStamp.eventId,1282 readonly toolUseId?: string | undefined;
1121 provider: PROVIDER,1283 readonly rawSource: "claude.sdk.message" | "claude.sdk.permission";
1122 createdAt: deltaStamp.createdAt,1284 readonly rawMethod: string;
1123 threadId: context.session.threadId,1285 readonly rawPayload: unknown;
1124 turnId: turnState.turnId,1286 },
1125 itemId: asRuntimeItemId(block.itemId),1287 ) {
1126 payload: {1288 const turnState = context.turnState;
1127 streamKind: "assistant_text",1289 const planMarkdown = input.planMarkdown.trim();
1128 delta: block.fallbackText,1290 if (!turnState || planMarkdown.length === 0) {
1129 },1291 return;
1130 providerRefs: nativeProviderRefs(context),1292 }
1131 ...(options?.rawMethod || options?.rawPayload1293
1132 ? {1294 const captureKey = exitPlanCaptureKey({
1133 raw: {1295 toolUseId: input.toolUseId,
1134 source: "claude.sdk.message" as const,1296 planMarkdown,
1135 ...(options.rawMethod ? { method: options.rawMethod } : {}),1297 });
1136 payload: options?.rawPayload,1298 if (turnState.capturedProposedPlanKeys.has(captureKey)) {
1137 },1299 return;
1138 }1300 }
1139 : {}),1301 turnState.capturedProposedPlanKeys.add(captureKey);
1140 });1302
1141 }1303 const stamp = yield* makeEventStamp();
1304 yield* offerRuntimeEvent({
1305 type: "turn.proposed.completed",
1306 eventId: stamp.eventId,
1307 provider: PROVIDER,
1308 createdAt: stamp.createdAt,
1309 threadId: context.session.threadId,
1310 turnId: turnState.turnId,
1311 payload: {
1312 planMarkdown,
1313 },
1314 providerRefs: nativeProviderRefs(context, {
1315 providerItemId: input.toolUseId,
1316 }),
1317 raw: {
1318 source: input.rawSource,
1319 method: input.rawMethod,
1320 payload: input.rawPayload,
1321 },
1322 });
1323 });
1324
1325 const completeTurn = Effect.fn("completeTurn")(function* (
1326 context: ClaudeSessionContext,
1327 status: ProviderRuntimeTurnStatus,
1328 errorMessage?: string,
1329 result?: SDKResultMessage,
1330 ) {
1331 const resultUsage =
1332 result?.usage && typeof result.usage === "object" ? { ...result.usage } : undefined;
1333 const resultContextWindow = maxClaudeContextWindowFromModelUsage(result?.modelUsage);
1334 if (resultContextWindow !== undefined) {
1335 context.lastKnownContextWindow = resultContextWindow;
1336 }
11421337
1143 block.completionEmitted = true;1338 // The SDK result.usage contains *accumulated* totals across all API calls
1144 if (turnState.assistantTextBlocks.get(block.blockIndex) === block) {1339 // (input_tokens, cache_read_input_tokens, etc. summed over every request).
1145 turnState.assistantTextBlocks.delete(block.blockIndex);1340 // This does NOT represent the current context window size.
1341 // Instead, use the last known context-window-accurate usage from task_progress
1342 // events and treat the accumulated total as totalProcessedTokens.
1343 const accumulatedSnapshot = normalizeClaudeTokenUsage(
1344 resultUsage,
1345 resultContextWindow ?? context.lastKnownContextWindow,
1346 );
1347 const lastGoodUsage = context.lastKnownTokenUsage;
1348 const maxTokens = resultContextWindow ?? context.lastKnownContextWindow;
1349 const usageSnapshot: ThreadTokenUsageSnapshot | undefined = lastGoodUsage
1350 ? {
1351 ...lastGoodUsage,
1352 ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0
1353 ? { maxTokens }
1354 : {}),
1355 ...(accumulatedSnapshot && accumulatedSnapshot.usedTokens > lastGoodUsage.usedTokens
1356 ? { totalProcessedTokens: accumulatedSnapshot.usedTokens }
1357 : {}),
1146 }1358 }
1359 : accumulatedSnapshot;
11471360
1148 const stamp = yield* makeEventStamp();1361 const turnState = context.turnState;
1362 if (!turnState) {
1363 if (usageSnapshot) {
1364 const usageStamp = yield* makeEventStamp();
1149 yield* offerRuntimeEvent({1365 yield* offerRuntimeEvent({
1150 type: "item.completed",1366 type: "thread.token-usage.updated",
1151 eventId: stamp.eventId,1367 eventId: usageStamp.eventId,
1152 provider: PROVIDER,1368 provider: PROVIDER,
1153 createdAt: stamp.createdAt,1369 createdAt: usageStamp.createdAt,
1154 itemId: asRuntimeItemId(block.itemId),
1155 threadId: context.session.threadId,1370 threadId: context.session.threadId,
1156 turnId: turnState.turnId,
1157 payload: {1371 payload: {
1158 itemType: "assistant_message",1372 usage: usageSnapshot,
1159 status: "completed",
1160 title: "Assistant message",
1161 ...(block.fallbackText.length > 0 ? { detail: block.fallbackText } : {}),
1162 },1373 },
1163 providerRefs: nativeProviderRefs(context),1374 providerRefs: {},
1164 ...(options?.rawMethod || options?.rawPayload
1165 ? {
1166 raw: {
1167 source: "claude.sdk.message" as const,
1168 ...(options.rawMethod ? { method: options.rawMethod } : {}),
1169 payload: options?.rawPayload,
1170 },
1171 }
1172 : {}),
1173 });1375 });
1376 }
1377
1378 const stamp = yield* makeEventStamp();
1379 yield* offerRuntimeEvent({
1380 type: "turn.completed",
1381 eventId: stamp.eventId,
1382 provider: PROVIDER,
1383 createdAt: stamp.createdAt,
1384 threadId: context.session.threadId,
1385 payload: {
1386 state: status,
1387 ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}),
1388 ...(result?.usage ? { usage: result.usage } : {}),
1389 ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}),
1390 ...(typeof result?.total_cost_usd === "number"
1391 ? { totalCostUsd: result.total_cost_usd }
1392 : {}),
1393 ...(errorMessage ? { errorMessage } : {}),
1394 },
1395 providerRefs: {},
1174 });1396 });
1397 return;
1398 }
11751399
1176 const backfillAssistantTextBlocksFromSnapshot = (1400 for (const [index, tool] of context.inFlightTools.entries()) {
1177 context: ClaudeSessionContext,1401 const toolStamp = yield* makeEventStamp();
1178 message: SDKMessage,1402 yield* offerRuntimeEvent({
1179 ): Effect.Effect<void> =>1403 type: "item.completed",
1180 Effect.gen(function* () {1404 eventId: toolStamp.eventId,
1181 const turnState = context.turnState;1405 provider: PROVIDER,
1182 if (!turnState) {1406 createdAt: toolStamp.createdAt,
1183 return;1407 threadId: context.session.threadId,
1184 }1408 turnId: turnState.turnId,
1409 itemId: asRuntimeItemId(tool.itemId),
1410 payload: {
1411 itemType: tool.itemType,
1412 status: status === "completed" ? "completed" : "failed",
1413 title: tool.title,
1414 ...(tool.detail ? { detail: tool.detail } : {}),
1415 data: {
1416 toolName: tool.toolName,
1417 input: tool.input,
1418 },
1419 },
1420 providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
1421 raw: {
1422 source: "claude.sdk.message",
1423 method: "claude/result",
1424 payload: result ?? { status },
1425 },
1426 });
1427 context.inFlightTools.delete(index);
1428 }
1429 // Clear any remaining stale entries (e.g. from interrupted content blocks)
1430 context.inFlightTools.clear();
1431
1432 for (const block of turnState.assistantTextBlockOrder) {
1433 yield* completeAssistantTextBlock(context, block, {
1434 force: true,
1435 rawMethod: "claude/result",
1436 rawPayload: result ?? { status },
1437 });
1438 }
11851439
1186 const snapshotTextBlocks = extractAssistantTextBlocks(message);1440 context.turns.push({
1187 if (snapshotTextBlocks.length === 0) {1441 id: turnState.turnId,
1188 return;1442 items: [...turnState.items],
1189 }1443 });
11901444
1191 const orderedBlocks = turnState.assistantTextBlockOrder.map((block) => ({1445 if (usageSnapshot) {
1192 blockIndex: block.blockIndex,1446 const usageStamp = yield* makeEventStamp();
1193 block,1447 yield* offerRuntimeEvent({
1194 }));1448 type: "thread.token-usage.updated",
11951449 eventId: usageStamp.eventId,
1196 for (const [position, text] of snapshotTextBlocks.entries()) {1450 provider: PROVIDER,
1197 const existingEntry = orderedBlocks[position];1451 createdAt: usageStamp.createdAt,
1198 const entry =1452 threadId: context.session.threadId,
1199 existingEntry ??1453 turnId: turnState.turnId,
1200 (yield* createSyntheticAssistantTextBlock(context, text).pipe(1454 payload: {
1201 Effect.map((created) => {1455 usage: usageSnapshot,
1202 if (!created) {1456 },
1203 return undefined;1457 providerRefs: nativeProviderRefs(context),
1204 }1458 });
1205 orderedBlocks.push(created);1459 }
1206 return created;
1207 }),
1208 ));
1209 if (!entry) {
1210 continue;
1211 }
12121460
1213 if (entry.block.fallbackText.length === 0) {1461 const stamp = yield* makeEventStamp();
1214 entry.block.fallbackText = text;1462 yield* offerRuntimeEvent({
1215 }1463 type: "turn.completed",
1464 eventId: stamp.eventId,
1465 provider: PROVIDER,
1466 createdAt: stamp.createdAt,
1467 threadId: context.session.threadId,
1468 turnId: turnState.turnId,
1469 payload: {
1470 state: status,
1471 ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}),
1472 ...(result?.usage ? { usage: result.usage } : {}),
1473 ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}),
1474 ...(typeof result?.total_cost_usd === "number"
1475 ? { totalCostUsd: result.total_cost_usd }
1476 : {}),
1477 ...(errorMessage ? { errorMessage } : {}),
1478 },
1479 providerRefs: nativeProviderRefs(context),
1480 });
12161481
1217 if (entry.block.streamClosed && !entry.block.completionEmitted) {1482 const updatedAt = yield* nowIso;
1218 yield* completeAssistantTextBlock(context, entry.block, {1483 context.turnState = undefined;
1219 rawMethod: "claude/assistant",1484 context.session = {
1220 rawPayload: message,1485 ...context.session,
1221 });1486 status: "ready",
1222 }1487 activeTurnId: undefined,
1223 }1488 updatedAt,
1224 });1489 ...(status === "failed" && errorMessage ? { lastError: errorMessage } : {}),
1490 };
1491 yield* updateResumeCursor(context);
1492 });
1493
1494 const handleStreamEvent = Effect.fn("handleStreamEvent")(function* (
1495 context: ClaudeSessionContext,
1496 message: SDKMessage,
1497 ) {
1498 if (message.type !== "stream_event") {
1499 return;
1500 }
12251501
1226 const ensureThreadId = (1502 const { event } = message;
1227 context: ClaudeSessionContext,1503
1228 message: SDKMessage,1504 if (event.type === "content_block_delta") {
1229 ): Effect.Effect<void> =>1505 if (
1230 Effect.gen(function* () {1506 (event.delta.type === "text_delta" || event.delta.type === "thinking_delta") &&
1231 if (typeof message.session_id !== "string" || message.session_id.length === 0) {1507 context.turnState
1508 ) {
1509 const deltaText =
1510 event.delta.type === "text_delta"
1511 ? event.delta.text
1512 : typeof event.delta.thinking === "string"
1513 ? event.delta.thinking
1514 : "";
1515 if (deltaText.length === 0) {
1232 return;1516 return;
1233 }1517 }
1234 const nextThreadId = message.session_id;1518 const streamKind = streamKindFromDeltaType(event.delta.type);
1235 context.resumeSessionId = message.session_id;1519 const assistantBlockEntry =
1236 yield* updateResumeCursor(context);1520 event.delta.type === "text_delta"
12371521 ? yield* ensureAssistantTextBlock(context, event.index)
1238 if (context.lastThreadStartedId !== nextThreadId) {1522 : context.turnState.assistantTextBlocks.get(event.index)
1239 context.lastThreadStartedId = nextThreadId;1523 ? {
1240 const stamp = yield* makeEventStamp();1524 blockIndex: event.index,
1241 yield* offerRuntimeEvent({1525 block: context.turnState.assistantTextBlocks.get(
1242 type: "thread.started",1526 event.index,
1243 eventId: stamp.eventId,1527 ) as AssistantTextBlockState,
1244 provider: PROVIDER,1528 }
1245 createdAt: stamp.createdAt,1529 : undefined;
1246 threadId: context.session.threadId,1530 if (assistantBlockEntry?.block && event.delta.type === "text_delta") {
1247 payload: {1531 assistantBlockEntry.block.emittedTextDelta = true;
1248 providerThreadId: nextThreadId,
1249 },
1250 providerRefs: {},
1251 raw: {
1252 source: "claude.sdk.message",
1253 method: "claude/thread/started",
1254 payload: {
1255 session_id: message.session_id,
1256 },
1257 },
1258 });
1259 }
1260 });
1261
1262 const emitRuntimeError = (
1263 context: ClaudeSessionContext,
1264 message: string,
1265 cause?: unknown,
1266 ): Effect.Effect<void> =>
1267 Effect.gen(function* () {
1268 if (cause !== undefined) {
1269 void cause;
1270 }1532 }
1271 const turnState = context.turnState;
1272 const stamp = yield* makeEventStamp();1533 const stamp = yield* makeEventStamp();
1273 yield* offerRuntimeEvent({1534 yield* offerRuntimeEvent({
1274 type: "runtime.error",1535 type: "content.delta",
1275 eventId: stamp.eventId,1536 eventId: stamp.eventId,
1276 provider: PROVIDER,1537 provider: PROVIDER,
1277 createdAt: stamp.createdAt,1538 createdAt: stamp.createdAt,
1278 threadId: context.session.threadId,1539 threadId: context.session.threadId,
1279 ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}),1540 turnId: context.turnState.turnId,
1541 ...(assistantBlockEntry?.block
1542 ? { itemId: asRuntimeItemId(assistantBlockEntry.block.itemId) }
1543 : {}),
1280 payload: {1544 payload: {
1281 message,1545 streamKind,
1282 class: "provider_error",1546 delta: deltaText,
1283 ...(cause !== undefined ? { detail: cause } : {}),
1284 },1547 },
1285 providerRefs: nativeProviderRefs(context),1548 providerRefs: nativeProviderRefs(context),
1286 });1549 raw: {
1287 });1550 source: "claude.sdk.message",
12881551 method: "claude/stream_event/content_block_delta",
1289 const emitRuntimeWarning = (1552 payload: message,
1290 context: ClaudeSessionContext,
1291 message: string,
1292 detail?: unknown,
1293 ): Effect.Effect<void> =>
1294 Effect.gen(function* () {
1295 const turnState = context.turnState;
1296 const stamp = yield* makeEventStamp();
1297 yield* offerRuntimeEvent({
1298 type: "runtime.warning",
1299 eventId: stamp.eventId,
1300 provider: PROVIDER,
1301 createdAt: stamp.createdAt,
1302 threadId: context.session.threadId,
1303 ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}),
1304 payload: {
1305 message,
1306 ...(detail !== undefined ? { detail } : {}),
1307 },1553 },
1308 providerRefs: nativeProviderRefs(context),
1309 });1554 });
1310 });1555 return;
1556 }
13111557
1312 const emitProposedPlanCompleted = (1558 if (event.delta.type === "input_json_delta") {
1313 context: ClaudeSessionContext,1559 const tool = context.inFlightTools.get(event.index);
1314 input: {1560 if (!tool || typeof event.delta.partial_json !== "string") {
1315 readonly planMarkdown: string;
1316 readonly toolUseId?: string | undefined;
1317 readonly rawSource: "claude.sdk.message" | "claude.sdk.permission";
1318 readonly rawMethod: string;
1319 readonly rawPayload: unknown;
1320 },
1321 ): Effect.Effect<void> =>
1322 Effect.gen(function* () {
1323 const turnState = context.turnState;
1324 const planMarkdown = input.planMarkdown.trim();
1325 if (!turnState || planMarkdown.length === 0) {
1326 return;1561 return;
1327 }1562 }
13281563
1329 const captureKey = exitPlanCaptureKey({1564 const partialInputJson = tool.partialInputJson + event.delta.partial_json;
1330 toolUseId: input.toolUseId,1565 const parsedInput = tryParseJsonRecord(partialInputJson);
1331 planMarkdown,1566 const detail = parsedInput ? summarizeToolRequest(tool.toolName, parsedInput) : tool.detail;
1332 });1567 let nextTool: ToolInFlight = {
1333 if (turnState.capturedProposedPlanKeys.has(captureKey)) {1568 ...tool,
1569 partialInputJson,
1570 ...(parsedInput ? { input: parsedInput } : {}),
1571 ...(detail ? { detail } : {}),
1572 };
1573
1574 const nextFingerprint =
1575 parsedInput && Object.keys(parsedInput).length > 0
1576 ? toolInputFingerprint(parsedInput)
1577 : undefined;
1578 context.inFlightTools.set(event.index, nextTool);
1579
1580 if (
1581 !parsedInput ||
1582 !nextFingerprint ||
1583 tool.lastEmittedInputFingerprint === nextFingerprint
1584 ) {
1334 return;1585 return;
1335 }1586 }
1336 turnState.capturedProposedPlanKeys.add(captureKey);1587
1588 nextTool = {
1589 ...nextTool,
1590 lastEmittedInputFingerprint: nextFingerprint,
1591 };
1592 context.inFlightTools.set(event.index, nextTool);
13371593
1338 const stamp = yield* makeEventStamp();1594 const stamp = yield* makeEventStamp();
1339 yield* offerRuntimeEvent({1595 yield* offerRuntimeEvent({
1340 type: "turn.proposed.completed",1596 type: "item.updated",
1341 eventId: stamp.eventId,1597 eventId: stamp.eventId,
1342 provider: PROVIDER,1598 provider: PROVIDER,
1343 createdAt: stamp.createdAt,1599 createdAt: stamp.createdAt,
1344 threadId: context.session.threadId,1600 threadId: context.session.threadId,
1345 turnId: turnState.turnId,1601 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
1602 itemId: asRuntimeItemId(nextTool.itemId),
1346 payload: {1603 payload: {
1347 planMarkdown,1604 itemType: nextTool.itemType,
1605 status: "inProgress",
1606 title: nextTool.title,
1607 ...(nextTool.detail ? { detail: nextTool.detail } : {}),
1608 data: {
1609 toolName: nextTool.toolName,
1610 input: nextTool.input,
1611 },
1348 },1612 },
1349 providerRefs: nativeProviderRefs(context, {1613 providerRefs: nativeProviderRefs(context, { providerItemId: nextTool.itemId }),
1350 providerItemId: input.toolUseId,
1351 }),
1352 raw: {1614 raw: {
1353 source: input.rawSource,1615 source: "claude.sdk.message",
1354 method: input.rawMethod,1616 method: "claude/stream_event/content_block_delta/input_json_delta",
1355 payload: input.rawPayload,1617 payload: message,
1356 },1618 },
1357 });1619 });
1358 });1620 }
1621 return;
1622 }
13591623
1360 const completeTurn = (1624 if (event.type === "content_block_start") {
1361 context: ClaudeSessionContext,1625 const { index, content_block: block } = event;
1362 status: ProviderRuntimeTurnStatus,1626 if (block.type === "text") {
1363 errorMessage?: string,1627 yield* ensureAssistantTextBlock(context, index, {
1364 result?: SDKResultMessage,1628 fallbackText: extractContentBlockText(block),
1365 ): Effect.Effect<void> =>1629 });
1366 Effect.gen(function* () {1630 return;
1367 const resultUsage =1631 }
1368 result?.usage && typeof result.usage === "object" ? { ...result.usage } : undefined;1632 if (
1369 const resultContextWindow = maxClaudeContextWindowFromModelUsage(result?.modelUsage);1633 block.type !== "tool_use" &&
1370 if (resultContextWindow !== undefined) {1634 block.type !== "server_tool_use" &&
1371 context.lastKnownContextWindow = resultContextWindow;1635 block.type !== "mcp_tool_use"
1372 }1636 ) {
1637 return;
1638 }
13731639
1374 // The SDK result.usage contains *accumulated* totals across all API calls1640 const toolName = block.name;
1375 // (input_tokens, cache_read_input_tokens, etc. summed over every request).1641 const itemType = classifyToolItemType(toolName);
1376 // This does NOT represent the current context window size.1642 const toolInput =
1377 // Instead, use the last known context-window-accurate usage from task_progress1643 typeof block.input === "object" && block.input !== null
1378 // events and treat the accumulated total as totalProcessedTokens.1644 ? (block.input as Record<string, unknown>)
1379 const accumulatedSnapshot = normalizeClaudeTokenUsage(1645 : {};
1380 resultUsage,1646 const itemId = block.id;
1381 resultContextWindow ?? context.lastKnownContextWindow,1647 const detail = summarizeToolRequest(toolName, toolInput);
1382 );1648 const inputFingerprint =
1383 const lastGoodUsage = context.lastKnownTokenUsage;1649 Object.keys(toolInput).length > 0 ? toolInputFingerprint(toolInput) : undefined;
1384 const maxTokens = resultContextWindow ?? context.lastKnownContextWindow;1650
1385 const usageSnapshot: ThreadTokenUsageSnapshot | undefined = lastGoodUsage1651 const tool: ToolInFlight = {
1386 ? {1652 itemId,
1387 ...lastGoodUsage,1653 itemType,
1388 ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 01654 toolName,
1389 ? { maxTokens }1655 title: titleForTool(itemType),
1390 : {}),1656 detail,
1391 ...(accumulatedSnapshot && accumulatedSnapshot.usedTokens > lastGoodUsage.usedTokens1657 input: toolInput,
1392 ? { totalProcessedTokens: accumulatedSnapshot.usedTokens }1658 partialInputJson: "",
1393 : {}),1659 ...(inputFingerprint ? { lastEmittedInputFingerprint: inputFingerprint } : {}),
1394 }1660 };
1395 : accumulatedSnapshot;1661 context.inFlightTools.set(index, tool);
1662
1663 const stamp = yield* makeEventStamp();
1664 yield* offerRuntimeEvent({
1665 type: "item.started",
1666 eventId: stamp.eventId,
1667 provider: PROVIDER,
1668 createdAt: stamp.createdAt,
1669 threadId: context.session.threadId,
1670 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
1671 itemId: asRuntimeItemId(tool.itemId),
1672 payload: {
1673 itemType: tool.itemType,
1674 status: "inProgress",
1675 title: tool.title,
1676 ...(tool.detail ? { detail: tool.detail } : {}),
1677 data: {
1678 toolName: tool.toolName,
1679 input: toolInput,
1680 },
1681 },
1682 providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
1683 raw: {
1684 source: "claude.sdk.message",
1685 method: "claude/stream_event/content_block_start",
1686 payload: message,
1687 },
1688 });
1689 return;
1690 }
13961691
1397 const turnState = context.turnState;1692 if (event.type === "content_block_stop") {
1398 if (!turnState) {1693 const { index } = event;
1399 if (usageSnapshot) {1694 const assistantBlock = context.turnState?.assistantTextBlocks.get(index);
1400 const usageStamp = yield* makeEventStamp();1695 if (assistantBlock) {
1401 yield* offerRuntimeEvent({1696 assistantBlock.streamClosed = true;
1402 type: "thread.token-usage.updated",1697 yield* completeAssistantTextBlock(context, assistantBlock, {
1403 eventId: usageStamp.eventId,1698 rawMethod: "claude/stream_event/content_block_stop",
1404 provider: PROVIDER,1699 rawPayload: message,
1405 createdAt: usageStamp.createdAt,1700 });
1406 threadId: context.session.threadId,1701 return;
1407 payload: {1702 }
1408 usage: usageSnapshot,1703 const tool = context.inFlightTools.get(index);
1409 },1704 if (!tool) {
1410 providerRefs: {},1705 return;
1411 });1706 }
1412 }1707 }
1708 });
14131709
1414 const stamp = yield* makeEventStamp();1710 const handleUserMessage = Effect.fn("handleUserMessage")(function* (
1415 yield* offerRuntimeEvent({1711 context: ClaudeSessionContext,
1416 type: "turn.completed",1712 message: SDKMessage,
1417 eventId: stamp.eventId,1713 ) {
1418 provider: PROVIDER,1714 if (message.type !== "user") {
1419 createdAt: stamp.createdAt,1715 return;
1420 threadId: context.session.threadId,1716 }
1421 payload: {
1422 state: status,
1423 ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}),
1424 ...(result?.usage ? { usage: result.usage } : {}),
1425 ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}),
1426 ...(typeof result?.total_cost_usd === "number"
1427 ? { totalCostUsd: result.total_cost_usd }
1428 : {}),
1429 ...(errorMessage ? { errorMessage } : {}),
1430 },
1431 providerRefs: {},
1432 });
1433 return;
1434 }
14351717
1436 for (const [index, tool] of context.inFlightTools.entries()) {1718 if (context.turnState) {
1437 const toolStamp = yield* makeEventStamp();1719 context.turnState.items.push(message.message);
1438 yield* offerRuntimeEvent({1720 }
1439 type: "item.completed",
1440 eventId: toolStamp.eventId,
1441 provider: PROVIDER,
1442 createdAt: toolStamp.createdAt,
1443 threadId: context.session.threadId,
1444 turnId: turnState.turnId,
1445 itemId: asRuntimeItemId(tool.itemId),
1446 payload: {
1447 itemType: tool.itemType,
1448 status: status === "completed" ? "completed" : "failed",
1449 title: tool.title,
1450 ...(tool.detail ? { detail: tool.detail } : {}),
1451 data: {
1452 toolName: tool.toolName,
1453 input: tool.input,
1454 },
1455 },
1456 providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
1457 raw: {
1458 source: "claude.sdk.message",
1459 method: "claude/result",
1460 payload: result ?? { status },
1461 },
1462 });
1463 context.inFlightTools.delete(index);
1464 }
1465 // Clear any remaining stale entries (e.g. from interrupted content blocks)
1466 context.inFlightTools.clear();
1467
1468 for (const block of turnState.assistantTextBlockOrder) {
1469 yield* completeAssistantTextBlock(context, block, {
1470 force: true,
1471 rawMethod: "claude/result",
1472 rawPayload: result ?? { status },
1473 });
1474 }
14751721
1476 context.turns.push({1722 for (const toolResult of toolResultBlocksFromUserMessage(message)) {
1477 id: turnState.turnId,1723 const toolEntry = Array.from(context.inFlightTools.entries()).find(
1478 items: [...turnState.items],1724 ([, tool]) => tool.itemId === toolResult.toolUseId,
1479 });1725 );
1726 if (!toolEntry) {
1727 continue;
1728 }
14801729
1481 if (usageSnapshot) {1730 const [index, tool] = toolEntry;
1482 const usageStamp = yield* makeEventStamp();1731 const itemStatus = toolResult.isError ? "failed" : "completed";
1483 yield* offerRuntimeEvent({1732 const toolData = {
1484 type: "thread.token-usage.updated",1733 toolName: tool.toolName,
1485 eventId: usageStamp.eventId,1734 input: tool.input,
1486 provider: PROVIDER,1735 result: toolResult.block,
1487 createdAt: usageStamp.createdAt,1736 };
1488 threadId: context.session.threadId,1737
1489 turnId: turnState.turnId,1738 const updatedStamp = yield* makeEventStamp();
1490 payload: {1739 yield* offerRuntimeEvent({
1491 usage: usageSnapshot,1740 type: "item.updated",
1492 },1741 eventId: updatedStamp.eventId,
1493 providerRefs: nativeProviderRefs(context),1742 provider: PROVIDER,
1494 });1743 createdAt: updatedStamp.createdAt,
1495 }1744 threadId: context.session.threadId,
1745 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
1746 itemId: asRuntimeItemId(tool.itemId),
1747 payload: {
1748 itemType: tool.itemType,
1749 status: toolResult.isError ? "failed" : "inProgress",
1750 title: tool.title,
1751 ...(tool.detail ? { detail: tool.detail } : {}),
1752 data: toolData,
1753 },
1754 providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
1755 raw: {
1756 source: "claude.sdk.message",
1757 method: "claude/user",
1758 payload: message,
1759 },
1760 });
14961761
1497 const stamp = yield* makeEventStamp();1762 const streamKind = toolResultStreamKind(tool.itemType);
1763 if (streamKind && toolResult.text.length > 0 && context.turnState) {
1764 const deltaStamp = yield* makeEventStamp();
1498 yield* offerRuntimeEvent({1765 yield* offerRuntimeEvent({
1499 type: "turn.completed",1766 type: "content.delta",
1500 eventId: stamp.eventId,1767 eventId: deltaStamp.eventId,
1501 provider: PROVIDER,1768 provider: PROVIDER,
1502 createdAt: stamp.createdAt,1769 createdAt: deltaStamp.createdAt,
1503 threadId: context.session.threadId,1770 threadId: context.session.threadId,
1504 turnId: turnState.turnId,1771 turnId: context.turnState.turnId,
1772 itemId: asRuntimeItemId(tool.itemId),
1505 payload: {1773 payload: {
1506 state: status,1774 streamKind,
1507 ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}),1775 delta: toolResult.text,
1508 ...(result?.usage ? { usage: result.usage } : {}),1776 },
1509 ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}),1777 providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
1510 ...(typeof result?.total_cost_usd === "number"1778 raw: {
1511 ? { totalCostUsd: result.total_cost_usd }1779 source: "claude.sdk.message",
1512 : {}),1780 method: "claude/user",
1513 ...(errorMessage ? { errorMessage } : {}),1781 payload: message,
1514 },1782 },
1515 providerRefs: nativeProviderRefs(context),
1516 });1783 });
1784 }
15171785
1518 const updatedAt = yield* nowIso;1786 const completedStamp = yield* makeEventStamp();
1519 context.turnState = undefined;1787 yield* offerRuntimeEvent({
1520 context.session = {1788 type: "item.completed",
1521 ...context.session,1789 eventId: completedStamp.eventId,
1522 status: "ready",1790 provider: PROVIDER,
1523 activeTurnId: undefined,1791 createdAt: completedStamp.createdAt,
1524 updatedAt,1792 threadId: context.session.threadId,
1525 ...(status === "failed" && errorMessage ? { lastError: errorMessage } : {}),1793 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
1526 };1794 itemId: asRuntimeItemId(tool.itemId),
1527 yield* updateResumeCursor(context);1795 payload: {
1796 itemType: tool.itemType,
1797 status: itemStatus,
1798 title: tool.title,
1799 ...(tool.detail ? { detail: tool.detail } : {}),
1800 data: toolData,
1801 },
1802 providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
1803 raw: {
1804 source: "claude.sdk.message",
1805 method: "claude/user",
1806 payload: message,
1807 },
1528 });1808 });
15291809
1530 const handleStreamEvent = (1810 context.inFlightTools.delete(index);
1531 context: ClaudeSessionContext,1811 }
1532 message: SDKMessage,1812 });
1533 ): Effect.Effect<void> =>
1534 Effect.gen(function* () {
1535 if (message.type !== "stream_event") {
1536 return;
1537 }
1538
1539 const { event } = message;
1540
1541 if (event.type === "content_block_delta") {
1542 if (
1543 (event.delta.type === "text_delta" || event.delta.type === "thinking_delta") &&
1544 context.turnState
1545 ) {
1546 const deltaText =
1547 event.delta.type === "text_delta"
1548 ? event.delta.text
1549 : typeof event.delta.thinking === "string"
1550 ? event.delta.thinking
1551 : "";
1552 if (deltaText.length === 0) {
1553 return;
1554 }
1555 const streamKind = streamKindFromDeltaType(event.delta.type);
1556 const assistantBlockEntry =
1557 event.delta.type === "text_delta"
1558 ? yield* ensureAssistantTextBlock(context, event.index)
1559 : context.turnState.assistantTextBlocks.get(event.index)
1560 ? {
1561 blockIndex: event.index,
1562 block: context.turnState.assistantTextBlocks.get(
1563 event.index,
1564 ) as AssistantTextBlockState,
1565 }
1566 : undefined;
1567 if (assistantBlockEntry?.block && event.delta.type === "text_delta") {
1568 assistantBlockEntry.block.emittedTextDelta = true;
1569 }
1570 const stamp = yield* makeEventStamp();
1571 yield* offerRuntimeEvent({
1572 type: "content.delta",
1573 eventId: stamp.eventId,
1574 provider: PROVIDER,
1575 createdAt: stamp.createdAt,
1576 threadId: context.session.threadId,
1577 turnId: context.turnState.turnId,
1578 ...(assistantBlockEntry?.block
1579 ? { itemId: asRuntimeItemId(assistantBlockEntry.block.itemId) }
1580 : {}),
1581 payload: {
1582 streamKind,
1583 delta: deltaText,
1584 },
1585 providerRefs: nativeProviderRefs(context),
1586 raw: {
1587 source: "claude.sdk.message",
1588 method: "claude/stream_event/content_block_delta",
1589 payload: message,
1590 },
1591 });
1592 return;
1593 }
1594
1595 if (event.delta.type === "input_json_delta") {
1596 const tool = context.inFlightTools.get(event.index);
1597 if (!tool || typeof event.delta.partial_json !== "string") {
1598 return;
1599 }
1600
1601 const partialInputJson = tool.partialInputJson + event.delta.partial_json;
1602 const parsedInput = tryParseJsonRecord(partialInputJson);
1603 const detail = parsedInput
1604 ? summarizeToolRequest(tool.toolName, parsedInput)
1605 : tool.detail;
1606 let nextTool: ToolInFlight = {
1607 ...tool,
1608 partialInputJson,
1609 ...(parsedInput ? { input: parsedInput } : {}),
1610 ...(detail ? { detail } : {}),
1611 };
1612
1613 const nextFingerprint =
1614 parsedInput && Object.keys(parsedInput).length > 0
1615 ? toolInputFingerprint(parsedInput)
1616 : undefined;
1617 context.inFlightTools.set(event.index, nextTool);
1618
1619 if (
1620 !parsedInput ||
1621 !nextFingerprint ||
1622 tool.lastEmittedInputFingerprint === nextFingerprint
1623 ) {
1624 return;
1625 }
1626
1627 nextTool = {
1628 ...nextTool,
1629 lastEmittedInputFingerprint: nextFingerprint,
1630 };
1631 context.inFlightTools.set(event.index, nextTool);
1632
1633 const stamp = yield* makeEventStamp();
1634 yield* offerRuntimeEvent({
1635 type: "item.updated",
1636 eventId: stamp.eventId,
1637 provider: PROVIDER,
1638 createdAt: stamp.createdAt,
1639 threadId: context.session.threadId,
1640 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
1641 itemId: asRuntimeItemId(nextTool.itemId),
1642 payload: {
1643 itemType: nextTool.itemType,
1644 status: "inProgress",
1645 title: nextTool.title,
1646 ...(nextTool.detail ? { detail: nextTool.detail } : {}),
1647 data: {
1648 toolName: nextTool.toolName,
1649 input: nextTool.input,
1650 },
1651 },
1652 providerRefs: nativeProviderRefs(context, { providerItemId: nextTool.itemId }),
1653 raw: {
1654 source: "claude.sdk.message",
1655 method: "claude/stream_event/content_block_delta/input_json_delta",
1656 payload: message,
1657 },
1658 });
1659 }
1660 return;
1661 }
1662
1663 if (event.type === "content_block_start") {
1664 const { index, content_block: block } = event;
1665 if (block.type === "text") {
1666 yield* ensureAssistantTextBlock(context, index, {
1667 fallbackText: extractContentBlockText(block),
1668 });
1669 return;
1670 }
1671 if (
1672 block.type !== "tool_use" &&
1673 block.type !== "server_tool_use" &&
1674 block.type !== "mcp_tool_use"
1675 ) {
1676 return;
1677 }
16781813
1679 const toolName = block.name;1814 const handleAssistantMessage = Effect.fn("handleAssistantMessage")(function* (
1680 const itemType = classifyToolItemType(toolName);1815 context: ClaudeSessionContext,
1681 const toolInput =1816 message: SDKMessage,
1682 typeof block.input === "object" && block.input !== null1817 ) {
1683 ? (block.input as Record<string, unknown>)1818 if (message.type !== "assistant") {
1684 : {};1819 return;
1685 const itemId = block.id;1820 }
1686 const detail = summarizeToolRequest(toolName, toolInput);
1687 const inputFingerprint =
1688 Object.keys(toolInput).length > 0 ? toolInputFingerprint(toolInput) : undefined;
1689
1690 const tool: ToolInFlight = {
1691 itemId,
1692 itemType,
1693 toolName,
1694 title: titleForTool(itemType),
1695 detail,
1696 input: toolInput,
1697 partialInputJson: "",
1698 ...(inputFingerprint ? { lastEmittedInputFingerprint: inputFingerprint } : {}),
1699 };
1700 context.inFlightTools.set(index, tool);
1701
1702 const stamp = yield* makeEventStamp();
1703 yield* offerRuntimeEvent({
1704 type: "item.started",
1705 eventId: stamp.eventId,
1706 provider: PROVIDER,
1707 createdAt: stamp.createdAt,
1708 threadId: context.session.threadId,
1709 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
1710 itemId: asRuntimeItemId(tool.itemId),
1711 payload: {
1712 itemType: tool.itemType,
1713 status: "inProgress",
1714 title: tool.title,
1715 ...(tool.detail ? { detail: tool.detail } : {}),
1716 data: {
1717 toolName: tool.toolName,
1718 input: toolInput,
1719 },
1720 },
1721 providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
1722 raw: {
1723 source: "claude.sdk.message",
1724 method: "claude/stream_event/content_block_start",
1725 payload: message,
1726 },
1727 });
1728 return;
1729 }
17301821
1731 if (event.type === "content_block_stop") {1822 // Auto-start a synthetic turn for assistant messages that arrive without
1732 const { index } = event;1823 // an active turn (e.g., background agent/subagent responses between user prompts).
1733 const assistantBlock = context.turnState?.assistantTextBlocks.get(index);1824 if (!context.turnState) {
1734 if (assistantBlock) {1825 const turnId = TurnId.makeUnsafe(yield* Random.nextUUIDv4);
1735 assistantBlock.streamClosed = true;1826 const startedAt = yield* nowIso;
1736 yield* completeAssistantTextBlock(context, assistantBlock, {1827 context.turnState = {
1737 rawMethod: "claude/stream_event/content_block_stop",1828 turnId,
1738 rawPayload: message,1829 startedAt,
1739 });1830 items: [],
1740 return;1831 assistantTextBlocks: new Map(),
1741 }1832 assistantTextBlockOrder: [],
1742 const tool = context.inFlightTools.get(index);1833 capturedProposedPlanKeys: new Set(),
1743 if (!tool) {1834 nextSyntheticAssistantBlockIndex: -1,
1744 return;1835 };
1745 }1836 context.session = {
1746 }1837 ...context.session,
1838 status: "running",
1839 activeTurnId: turnId,
1840 updatedAt: startedAt,
1841 };
1842 const turnStartedStamp = yield* makeEventStamp();
1843 yield* offerRuntimeEvent({
1844 type: "turn.started",
1845 eventId: turnStartedStamp.eventId,
1846 provider: PROVIDER,
1847 createdAt: turnStartedStamp.createdAt,
1848 threadId: context.session.threadId,
1849 turnId,
1850 payload: {},
1851 providerRefs: {
1852 ...nativeProviderRefs(context),
1853 providerTurnId: turnId,
1854 },
1855 raw: {
1856 source: "claude.sdk.message",
1857 method: "claude/synthetic-turn-start",
1858 payload: {},
1859 },
1747 });1860 });
1861 }
17481862
1749 const handleUserMessage = (1863 const content = message.message?.content;
1750 context: ClaudeSessionContext,1864 if (Array.isArray(content)) {
1751 message: SDKMessage,1865 for (const block of content) {
1752 ): Effect.Effect<void> =>1866 if (!block || typeof block !== "object") {
1753 Effect.gen(function* () {1867 continue;
1754 if (message.type !== "user") {
1755 return;
1756 }
1757
1758 if (context.turnState) {
1759 context.turnState.items.push(message.message);
1760 }
1761
1762 for (const toolResult of toolResultBlocksFromUserMessage(message)) {
1763 const toolEntry = Array.from(context.inFlightTools.entries()).find(
1764 ([, tool]) => tool.itemId === toolResult.toolUseId,
1765 );
1766 if (!toolEntry) {
1767 continue;
1768 }
1769
1770 const [index, tool] = toolEntry;
1771 const itemStatus = toolResult.isError ? "failed" : "completed";
1772 const toolData = {
1773 toolName: tool.toolName,
1774 input: tool.input,
1775 result: toolResult.block,
1776 };
1777
1778 const updatedStamp = yield* makeEventStamp();
1779 yield* offerRuntimeEvent({
1780 type: "item.updated",
1781 eventId: updatedStamp.eventId,
1782 provider: PROVIDER,
1783 createdAt: updatedStamp.createdAt,
1784 threadId: context.session.threadId,
1785 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
1786 itemId: asRuntimeItemId(tool.itemId),
1787 payload: {
1788 itemType: tool.itemType,
1789 status: toolResult.isError ? "failed" : "inProgress",
1790 title: tool.title,
1791 ...(tool.detail ? { detail: tool.detail } : {}),
1792 data: toolData,
1793 },
1794 providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
1795 raw: {
1796 source: "claude.sdk.message",
1797 method: "claude/user",
1798 payload: message,
1799 },
1800 });
1801
1802 const streamKind = toolResultStreamKind(tool.itemType);
1803 if (streamKind && toolResult.text.length > 0 && context.turnState) {
1804 const deltaStamp = yield* makeEventStamp();
1805 yield* offerRuntimeEvent({
1806 type: "content.delta",
1807 eventId: deltaStamp.eventId,
1808 provider: PROVIDER,
1809 createdAt: deltaStamp.createdAt,
1810 threadId: context.session.threadId,
1811 turnId: context.turnState.turnId,
1812 itemId: asRuntimeItemId(tool.itemId),
1813 payload: {
1814 streamKind,
1815 delta: toolResult.text,
1816 },
1817 providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
1818 raw: {
1819 source: "claude.sdk.message",
1820 method: "claude/user",
1821 payload: message,
1822 },
1823 });
1824 }
1825
1826 const completedStamp = yield* makeEventStamp();
1827 yield* offerRuntimeEvent({
1828 type: "item.completed",
1829 eventId: completedStamp.eventId,
1830 provider: PROVIDER,
1831 createdAt: completedStamp.createdAt,
1832 threadId: context.session.threadId,
1833 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
1834 itemId: asRuntimeItemId(tool.itemId),
1835 payload: {
1836 itemType: tool.itemType,
1837 status: itemStatus,
1838 title: tool.title,
1839 ...(tool.detail ? { detail: tool.detail } : {}),
1840 data: toolData,
1841 },
1842 providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
1843 raw: {
1844 source: "claude.sdk.message",
1845 method: "claude/user",
1846 payload: message,
1847 },
1848 });
1849
1850 context.inFlightTools.delete(index);
1851 }1868 }
1852 });1869 const toolUse = block as {
18531870 type?: unknown;
1854 const handleAssistantMessage = (1871 id?: unknown;
1855 context: ClaudeSessionContext,1872 name?: unknown;
1856 message: SDKMessage,1873 input?: unknown;
1857 ): Effect.Effect<void> =>1874 };
1858 Effect.gen(function* () {1875 if (toolUse.type !== "tool_use" || toolUse.name !== "ExitPlanMode") {
1859 if (message.type !== "assistant") {1876 continue;
1860 return;
1861 }1877 }
18621878 const planMarkdown = extractExitPlanModePlan(toolUse.input);
1863 // Auto-start a synthetic turn for assistant messages that arrive without1879 if (!planMarkdown) {
1864 // an active turn (e.g., background agent/subagent responses between user prompts).1880 continue;
1865 if (!context.turnState) {
1866 const turnId = TurnId.makeUnsafe(yield* Random.nextUUIDv4);
1867 const startedAt = yield* nowIso;
1868 context.turnState = {
1869 turnId,
1870 startedAt,
1871 items: [],
1872 assistantTextBlocks: new Map(),
1873 assistantTextBlockOrder: [],
1874 capturedProposedPlanKeys: new Set(),
1875 nextSyntheticAssistantBlockIndex: -1,
1876 };
1877 context.session = {
1878 ...context.session,
1879 status: "running",
1880 activeTurnId: turnId,
1881 updatedAt: startedAt,
1882 };
1883 const turnStartedStamp = yield* makeEventStamp();
1884 yield* offerRuntimeEvent({
1885 type: "turn.started",
1886 eventId: turnStartedStamp.eventId,
1887 provider: PROVIDER,
1888 createdAt: turnStartedStamp.createdAt,
1889 threadId: context.session.threadId,
1890 turnId,
1891 payload: {},
1892 providerRefs: {
1893 ...nativeProviderRefs(context),
1894 providerTurnId: turnId,
1895 },
1896 raw: {
1897 source: "claude.sdk.message",
1898 method: "claude/synthetic-turn-start",
1899 payload: {},
1900 },
1901 });
1902 }1881 }
1882 yield* emitProposedPlanCompleted(context, {
1883 planMarkdown,
1884 toolUseId: typeof toolUse.id === "string" ? toolUse.id : undefined,
1885 rawSource: "claude.sdk.message",
1886 rawMethod: "claude/assistant",
1887 rawPayload: message,
1888 });
1889 }
1890 }
19031891
1904 const content = message.message?.content;1892 if (context.turnState) {
1905 if (Array.isArray(content)) {1893 context.turnState.items.push(message.message);
1906 for (const block of content) {1894 yield* backfillAssistantTextBlocksFromSnapshot(context, message);
1907 if (!block || typeof block !== "object") {1895 }
1908 continue;
1909 }
1910 const toolUse = block as {
1911 type?: unknown;
1912 id?: unknown;
1913 name?: unknown;
1914 input?: unknown;
1915 };
1916 if (toolUse.type !== "tool_use" || toolUse.name !== "ExitPlanMode") {
1917 continue;
1918 }
1919 const planMarkdown = extractExitPlanModePlan(toolUse.input);
1920 if (!planMarkdown) {
1921 continue;
1922 }
1923 yield* emitProposedPlanCompleted(context, {
1924 planMarkdown,
1925 toolUseId: typeof toolUse.id === "string" ? toolUse.id : undefined,
1926 rawSource: "claude.sdk.message",
1927 rawMethod: "claude/assistant",
1928 rawPayload: message,
1929 });
1930 }
1931 }
19321896
1933 if (context.turnState) {1897 context.lastAssistantUuid = message.uuid;
1934 context.turnState.items.push(message.message);1898 yield* updateResumeCursor(context);
1935 yield* backfillAssistantTextBlocksFromSnapshot(context, message);1899 });
1936 }
19371900
1938 context.lastAssistantUuid = message.uuid;1901 const handleResultMessage = Effect.fn("handleResultMessage")(function* (
1939 yield* updateResumeCursor(context);1902 context: ClaudeSessionContext,
1940 });1903 message: SDKMessage,
1904 ) {
1905 if (message.type !== "result") {
1906 return;
1907 }
19411908
1942 const handleResultMessage = (1909 const status = turnStatusFromResult(message);
1943 context: ClaudeSessionContext,1910 const errorMessage = message.subtype === "success" ? undefined : message.errors[0];
1944 message: SDKMessage,
1945 ): Effect.Effect<void> =>
1946 Effect.gen(function* () {
1947 if (message.type !== "result") {
1948 return;
1949 }
19501911
1951 const status = turnStatusFromResult(message);1912 if (status === "failed") {
1952 const errorMessage = message.subtype === "success" ? undefined : message.errors[0];1913 yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");
1914 }
19531915
1954 if (status === "failed") {1916 yield* completeTurn(context, status, errorMessage, message);
1955 yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");1917 });
1956 }
19571918
1958 yield* completeTurn(context, status, errorMessage, message);1919 const handleSystemMessage = Effect.fn("handleSystemMessage")(function* (
1959 });1920 context: ClaudeSessionContext,
1921 message: SDKMessage,
1922 ) {
1923 if (message.type !== "system") {
1924 return;
1925 }
19601926
1961 const handleSystemMessage = (1927 const stamp = yield* makeEventStamp();
1962 context: ClaudeSessionContext,1928 const base = {
1963 message: SDKMessage,1929 eventId: stamp.eventId,
1964 ): Effect.Effect<void> =>1930 provider: PROVIDER,
1965 Effect.gen(function* () {1931 createdAt: stamp.createdAt,
1966 if (message.type !== "system") {1932 threadId: context.session.threadId,
1967 return;1933 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
1968 }1934 providerRefs: nativeProviderRefs(context),
1935 raw: {
1936 source: "claude.sdk.message" as const,
1937 method: sdkNativeMethod(message),
1938 messageType: `${message.type}:${message.subtype}`,
1939 payload: message,
1940 },
1941 };
19691942
1970 const stamp = yield* makeEventStamp();1943 switch (message.subtype) {
1971 const base = {1944 case "init":
1972 eventId: stamp.eventId,1945 yield* offerRuntimeEvent({
1973 provider: PROVIDER,1946 ...base,
1974 createdAt: stamp.createdAt,1947 type: "session.configured",
1975 threadId: context.session.threadId,1948 payload: {
1976 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),1949 config: message as Record<string, unknown>,
1977 providerRefs: nativeProviderRefs(context),
1978 raw: {
1979 source: "claude.sdk.message" as const,
1980 method: sdkNativeMethod(message),
1981 messageType: `${message.type}:${message.subtype}`,
1982 payload: message,
1983 },1950 },
1984 };1951 });
19851952 return;
1986 switch (message.subtype) {1953 case "status":
1987 case "init":1954 yield* offerRuntimeEvent({
1988 yield* offerRuntimeEvent({1955 ...base,
1989 ...base,1956 type: "session.state.changed",
1990 type: "session.configured",1957 payload: {
1991 payload: {1958 state: message.status === "compacting" ? "waiting" : "running",
1992 config: message as Record<string, unknown>,1959 reason: `status:${message.status ?? "active"}`,
1993 },1960 detail: message,
1994 });1961 },
1995 return;1962 });
1996 case "status":1963 return;
1997 yield* offerRuntimeEvent({1964 case "compact_boundary":
1998 ...base,1965 yield* offerRuntimeEvent({
1999 type: "session.state.changed",1966 ...base,
2000 payload: {1967 type: "thread.state.changed",
2001 state: message.status === "compacting" ? "waiting" : "running",1968 payload: {
2002 reason: `status:${message.status ?? "active"}`,1969 state: "compacted",
2003 detail: message,1970 detail: message,
2004 },1971 },
2005 });1972 });
2006 return;1973 return;
2007 case "compact_boundary":1974 case "hook_started":
2008 yield* offerRuntimeEvent({1975 yield* offerRuntimeEvent({
2009 ...base,1976 ...base,
2010 type: "thread.state.changed",1977 type: "hook.started",
2011 payload: {1978 payload: {
2012 state: "compacted",1979 hookId: message.hook_id,
2013 detail: message,1980 hookName: message.hook_name,
2014 },1981 hookEvent: message.hook_event,
2015 });1982 },
2016 return;1983 });
2017 case "hook_started":1984 return;
2018 yield* offerRuntimeEvent({1985 case "hook_progress":
2019 ...base,1986 yield* offerRuntimeEvent({
2020 type: "hook.started",1987 ...base,
2021 payload: {1988 type: "hook.progress",
2022 hookId: message.hook_id,1989 payload: {
2023 hookName: message.hook_name,1990 hookId: message.hook_id,
2024 hookEvent: message.hook_event,1991 output: message.output,
2025 },1992 stdout: message.stdout,
2026 });1993 stderr: message.stderr,
2027 return;1994 },
2028 case "hook_progress":1995 });
2029 yield* offerRuntimeEvent({1996 return;
2030 ...base,1997 case "hook_response":
2031 type: "hook.progress",1998 yield* offerRuntimeEvent({
2032 payload: {1999 ...base,
2033 hookId: message.hook_id,2000 type: "hook.completed",
2034 output: message.output,2001 payload: {
2035 stdout: message.stdout,2002 hookId: message.hook_id,
2036 stderr: message.stderr,2003 outcome: message.outcome,
2037 },2004 output: message.output,
2038 });2005 stdout: message.stdout,
2039 return;2006 stderr: message.stderr,
2040 case "hook_response":2007 ...(typeof message.exit_code === "number" ? { exitCode: message.exit_code } : {}),
2041 yield* offerRuntimeEvent({2008 },
2042 ...base,2009 });
2043 type: "hook.completed",2010 return;
2044 payload: {2011 case "task_started":
2045 hookId: message.hook_id,2012 yield* offerRuntimeEvent({
2046 outcome: message.outcome,2013 ...base,
2047 output: message.output,2014 type: "task.started",
2048 stdout: message.stdout,2015 payload: {
2049 stderr: message.stderr,2016 taskId: RuntimeTaskId.makeUnsafe(message.task_id),
2050 ...(typeof message.exit_code === "number" ? { exitCode: message.exit_code } : {}),2017 description: message.description,
2051 },2018 ...(message.task_type ? { taskType: message.task_type } : {}),
2052 });2019 },
2053 return;2020 });
2054 case "task_started":2021 return;
2055 yield* offerRuntimeEvent({2022 case "task_progress":
2056 ...base,2023 if (message.usage) {
2057 type: "task.started",2024 const normalizedUsage = normalizeClaudeTokenUsage(
2058 payload: {2025 message.usage,
2059 taskId: RuntimeTaskId.makeUnsafe(message.task_id),2026 context.lastKnownContextWindow,
2060 description: message.description,2027 );
2061 ...(message.task_type ? { taskType: message.task_type } : {}),2028 if (normalizedUsage) {
2062 },2029 context.lastKnownTokenUsage = normalizedUsage;
2063 });2030 const usageStamp = yield* makeEventStamp();
2064 return;
2065 case "task_progress":
2066 if (message.usage) {
2067 const normalizedUsage = normalizeClaudeTokenUsage(
2068 message.usage,
2069 context.lastKnownContextWindow,
2070 );
2071 if (normalizedUsage) {
2072 context.lastKnownTokenUsage = normalizedUsage;
2073 const usageStamp = yield* makeEventStamp();
2074 yield* offerRuntimeEvent({
2075 ...base,
2076 eventId: usageStamp.eventId,
2077 createdAt: usageStamp.createdAt,
2078 type: "thread.token-usage.updated",
2079 payload: {
2080 usage: normalizedUsage,
2081 },
2082 });
2083 }
2084 }
2085 yield* offerRuntimeEvent({
2086 ...base,
2087 type: "task.progress",
2088 payload: {
2089 taskId: RuntimeTaskId.makeUnsafe(message.task_id),
2090 description: message.description,
2091 ...(message.summary ? { summary: message.summary } : {}),
2092 ...(message.usage ? { usage: message.usage } : {}),
2093 ...(message.last_tool_name ? { lastToolName: message.last_tool_name } : {}),
2094 },
2095 });
2096 return;
2097 case "task_notification":
2098 if (message.usage) {
2099 const normalizedUsage = normalizeClaudeTokenUsage(
2100 message.usage,
2101 context.lastKnownContextWindow,
2102 );
2103 if (normalizedUsage) {
2104 context.lastKnownTokenUsage = normalizedUsage;
2105 const usageStamp = yield* makeEventStamp();
2106 yield* offerRuntimeEvent({
2107 ...base,
2108 eventId: usageStamp.eventId,
2109 createdAt: usageStamp.createdAt,
2110 type: "thread.token-usage.updated",
2111 payload: {
2112 usage: normalizedUsage,
2113 },
2114 });
2115 }
2116 }
2117 yield* offerRuntimeEvent({2031 yield* offerRuntimeEvent({
2118 ...base,2032 ...base,
2119 type: "task.completed",2033 eventId: usageStamp.eventId,
2034 createdAt: usageStamp.createdAt,
2035 type: "thread.token-usage.updated",
2120 payload: {2036 payload: {
2121 taskId: RuntimeTaskId.makeUnsafe(message.task_id),2037 usage: normalizedUsage,
2122 status: message.status,
2123 ...(message.summary ? { summary: message.summary } : {}),
2124 ...(message.usage ? { usage: message.usage } : {}),
2125 },2038 },
2126 });2039 });
2127 return;2040 }
2128 case "files_persisted":2041 }
2042 yield* offerRuntimeEvent({
2043 ...base,
2044 type: "task.progress",
2045 payload: {
2046 taskId: RuntimeTaskId.makeUnsafe(message.task_id),
2047 description: message.description,
2048 ...(message.summary ? { summary: message.summary } : {}),
2049 ...(message.usage ? { usage: message.usage } : {}),
2050 ...(message.last_tool_name ? { lastToolName: message.last_tool_name } : {}),
2051 },
2052 });
2053 return;
2054 case "task_notification":
2055 if (message.usage) {
2056 const normalizedUsage = normalizeClaudeTokenUsage(
2057 message.usage,
2058 context.lastKnownContextWindow,
2059 );
2060 if (normalizedUsage) {
2061 context.lastKnownTokenUsage = normalizedUsage;
2062 const usageStamp = yield* makeEventStamp();
2129 yield* offerRuntimeEvent({2063 yield* offerRuntimeEvent({
2130 ...base,2064 ...base,
2131 type: "files.persisted",2065 eventId: usageStamp.eventId,
2066 createdAt: usageStamp.createdAt,
2067 type: "thread.token-usage.updated",
2132 payload: {2068 payload: {
2133 files: Array.isArray(message.files)2069 usage: normalizedUsage,
2134 ? message.files.map((file: { filename: string; file_id: string }) => ({
2135 filename: file.filename,
2136 fileId: file.file_id,
2137 }))
2138 : [],
2139 ...(Array.isArray(message.failed)
2140 ? {
2141 failed: message.failed.map((entry: { filename: string; error: string }) => ({
2142 filename: entry.filename,
2143 error: entry.error,
2144 })),
2145 }
2146 : {}),
2147 },2070 },
2148 });2071 });
2149 return;2072 }
2150 default:
2151 yield* emitRuntimeWarning(
2152 context,
2153 `Unhandled Claude system message subtype '${message.subtype}'.`,
2154 message,
2155 );
2156 return;
2157 }2073 }
2158 });2074 yield* offerRuntimeEvent({
21592075 ...base,
2160 const handleSdkTelemetryMessage = (2076 type: "task.completed",
2161 context: ClaudeSessionContext,2077 payload: {
2162 message: SDKMessage,2078 taskId: RuntimeTaskId.makeUnsafe(message.task_id),
2163 ): Effect.Effect<void> =>2079 status: message.status,
2164 Effect.gen(function* () {2080 ...(message.summary ? { summary: message.summary } : {}),
2165 const stamp = yield* makeEventStamp();2081 ...(message.usage ? { usage: message.usage } : {}),
2166 const base = {
2167 eventId: stamp.eventId,
2168 provider: PROVIDER,
2169 createdAt: stamp.createdAt,
2170 threadId: context.session.threadId,
2171 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
2172 providerRefs: nativeProviderRefs(context),
2173 raw: {
2174 source: "claude.sdk.message" as const,
2175 method: sdkNativeMethod(message),
2176 messageType: message.type,
2177 payload: message,
2178 },2082 },
2179 };2083 });
21802084 return;
2181 if (message.type === "tool_progress") {2085 case "files_persisted":
2182 yield* offerRuntimeEvent({2086 yield* offerRuntimeEvent({
2183 ...base,2087 ...base,
2184 type: "tool.progress",2088 type: "files.persisted",
2185 payload: {2089 payload: {
2186 toolUseId: message.tool_use_id,2090 files: Array.isArray(message.files)
2187 toolName: message.tool_name,2091 ? message.files.map((file: { filename: string; file_id: string }) => ({
2188 elapsedSeconds: message.elapsed_time_seconds,2092 filename: file.filename,
2189 ...(message.task_id ? { summary: `task:${message.task_id}` } : {}),2093 fileId: file.file_id,
2190 },2094 }))
2191 });2095 : [],
2192 return;2096 ...(Array.isArray(message.failed)
2193 }2097 ? {
21942098 failed: message.failed.map((entry: { filename: string; error: string }) => ({
2195 if (message.type === "tool_use_summary") {2099 filename: entry.filename,
2196 yield* offerRuntimeEvent({2100 error: entry.error,
2197 ...base,2101 })),
2198 type: "tool.summary",2102 }
2199 payload: {2103 : {}),
2200 summary: message.summary,2104 },
2201 ...(message.preceding_tool_use_ids.length > 02105 });
2202 ? { precedingToolUseIds: message.preceding_tool_use_ids }2106 return;
2203 : {}),2107 default:
2204 },2108 yield* emitRuntimeWarning(
2205 });2109 context,
2206 return;2110 `Unhandled Claude system message subtype '${message.subtype}'.`,
2207 }2111 message,
2112 );
2113 return;
2114 }
2115 });
22082116
2209 if (message.type === "auth_status") {2117 const handleSdkTelemetryMessage = Effect.fn("handleSdkTelemetryMessage")(function* (
2210 yield* offerRuntimeEvent({2118 context: ClaudeSessionContext,
2211 ...base,2119 message: SDKMessage,
2212 type: "auth.status",2120 ) {
2213 payload: {2121 const stamp = yield* makeEventStamp();
2214 isAuthenticating: message.isAuthenticating,2122 const base = {
2215 output: message.output,2123 eventId: stamp.eventId,
2216 ...(message.error ? { error: message.error } : {}),2124 provider: PROVIDER,
2217 },2125 createdAt: stamp.createdAt,
2218 });2126 threadId: context.session.threadId,
2219 return;2127 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
2220 }2128 providerRefs: nativeProviderRefs(context),
2129 raw: {
2130 source: "claude.sdk.message" as const,
2131 method: sdkNativeMethod(message),
2132 messageType: message.type,
2133 payload: message,
2134 },
2135 };
22212136
2222 if (message.type === "rate_limit_event") {2137 if (message.type === "tool_progress") {
2223 yield* offerRuntimeEvent({2138 yield* offerRuntimeEvent({
2224 ...base,2139 ...base,
2225 type: "account.rate-limits.updated",2140 type: "tool.progress",
2226 payload: {2141 payload: {
2227 rateLimits: message,2142 toolUseId: message.tool_use_id,
2228 },2143 toolName: message.tool_name,
2229 });2144 elapsedSeconds: message.elapsed_time_seconds,
2230 return;2145 ...(message.task_id ? { summary: `task:${message.task_id}` } : {}),
2231 }2146 },
2232 });2147 });
2148 return;
2149 }
22332150
2234 const handleSdkMessage = (2151 if (message.type === "tool_use_summary") {
2235 context: ClaudeSessionContext,2152 yield* offerRuntimeEvent({
2236 message: SDKMessage,2153 ...base,
2237 ): Effect.Effect<void> =>2154 type: "tool.summary",
2238 Effect.gen(function* () {2155 payload: {
2239 yield* logNativeSdkMessage(context, message);2156 summary: message.summary,
2240 yield* ensureThreadId(context, message);2157 ...(message.preceding_tool_use_ids.length > 0
22412158 ? { precedingToolUseIds: message.preceding_tool_use_ids }
2242 switch (message.type) {2159 : {}),
2243 case "stream_event":2160 },
2244 yield* handleStreamEvent(context, message);
2245 return;
2246 case "user":
2247 yield* handleUserMessage(context, message);
2248 return;
2249 case "assistant":
2250 yield* handleAssistantMessage(context, message);
2251 return;
2252 case "result":
2253 yield* handleResultMessage(context, message);
2254 return;
2255 case "system":
2256 yield* handleSystemMessage(context, message);
2257 return;
2258 case "tool_progress":
2259 case "tool_use_summary":
2260 case "auth_status":
2261 case "rate_limit_event":
2262 yield* handleSdkTelemetryMessage(context, message);
2263 return;
2264 default:
2265 yield* emitRuntimeWarning(
2266 context,
2267 `Unhandled Claude SDK message type '${message.type}'.`,
2268 message,
2269 );
2270 return;
2271 }
2272 });2161 });
2162 return;
2163 }
22732164
2274 const runSdkStream = (context: ClaudeSessionContext): Effect.Effect<void, Error> =>2165 if (message.type === "auth_status") {
2275 Stream.fromAsyncIterable(context.query, (cause) =>2166 yield* offerRuntimeEvent({
2276 toError(cause, "Claude runtime stream failed."),2167 ...base,
2277 ).pipe(2168 type: "auth.status",
2278 Stream.takeWhile(() => !context.stopped),2169 payload: {
2279 Stream.runForEach((message) => handleSdkMessage(context, message)),2170 isAuthenticating: message.isAuthenticating,
2280 );2171 output: message.output,
22812172 ...(message.error ? { error: message.error } : {}),
2282 const handleStreamExit = (2173 },
2283 context: ClaudeSessionContext,
2284 exit: Exit.Exit<void, Error>,
2285 ): Effect.Effect<void> =>
2286 Effect.gen(function* () {
2287 if (context.stopped) {
2288 return;
2289 }
2290
2291 if (Exit.isFailure(exit)) {
2292 if (isClaudeInterruptedCause(exit.cause)) {
2293 if (context.turnState) {
2294 yield* completeTurn(
2295 context,
2296 "interrupted",
2297 interruptionMessageFromClaudeCause(exit.cause),
2298 );
2299 }
2300 } else {
2301 const message = messageFromClaudeStreamCause(
2302 exit.cause,
2303 "Claude runtime stream failed.",
2304 );
2305 yield* emitRuntimeError(context, message, Cause.pretty(exit.cause));
2306 yield* completeTurn(context, "failed", message);
2307 }
2308 } else if (context.turnState) {
2309 yield* completeTurn(context, "interrupted", "Claude runtime stream ended.");
2310 }
2311
2312 yield* stopSessionInternal(context, {
2313 emitExitEvent: true,
2314 });
2315 });2174 });
2175 return;
2176 }
23162177
2317 const stopSessionInternal = (2178 if (message.type === "rate_limit_event") {
2318 context: ClaudeSessionContext,2179 yield* offerRuntimeEvent({
2319 options?: { readonly emitExitEvent?: boolean },2180 ...base,
2320 ): Effect.Effect<void> =>2181 type: "account.rate-limits.updated",
2321 Effect.gen(function* () {2182 payload: {
2322 if (context.stopped) return;2183 rateLimits: message,
23232184 },
2324 context.stopped = true;
2325
2326 for (const [requestId, pending] of context.pendingApprovals) {
2327 yield* Deferred.succeed(pending.decision, "cancel");
2328 const stamp = yield* makeEventStamp();
2329 yield* offerRuntimeEvent({
2330 type: "request.resolved",
2331 eventId: stamp.eventId,
2332 provider: PROVIDER,
2333 createdAt: stamp.createdAt,
2334 threadId: context.session.threadId,
2335 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
2336 requestId: asRuntimeRequestId(requestId),
2337 payload: {
2338 requestType: pending.requestType,
2339 decision: "cancel",
2340 },
2341 providerRefs: nativeProviderRefs(context),
2342 });
2343 }
2344 context.pendingApprovals.clear();
2345
2346 if (context.turnState) {
2347 yield* completeTurn(context, "interrupted", "Session stopped.");
2348 }
2349
2350 yield* Queue.shutdown(context.promptQueue);
2351
2352 const streamFiber = context.streamFiber;
2353 context.streamFiber = undefined;
2354 if (streamFiber && streamFiber.pollUnsafe() === undefined) {
2355 yield* Fiber.interrupt(streamFiber);
2356 }
2357
2358 // @effect-diagnostics-next-line tryCatchInEffectGen:off
2359 try {
2360 context.query.close();
2361 } catch (cause) {
2362 yield* emitRuntimeError(context, "Failed to close Claude runtime query.", cause);
2363 }
2364
2365 const updatedAt = yield* nowIso;
2366 context.session = {
2367 ...context.session,
2368 status: "closed",
2369 activeTurnId: undefined,
2370 updatedAt,
2371 };
2372
2373 if (options?.emitExitEvent !== false) {
2374 const stamp = yield* makeEventStamp();
2375 yield* offerRuntimeEvent({
2376 type: "session.exited",
2377 eventId: stamp.eventId,
2378 provider: PROVIDER,
2379 createdAt: stamp.createdAt,
2380 threadId: context.session.threadId,
2381 payload: {
2382 reason: "Session stopped",
2383 exitKind: "graceful",
2384 },
2385 providerRefs: {},
2386 });
2387 }
2388
2389 sessions.delete(context.session.threadId);
2390 });2185 });
23912186 return;
2392 const requireSession = (2187 }
2393 threadId: ThreadId,2188 });
2394 ): Effect.Effect<ClaudeSessionContext, ProviderAdapterError> => {2189
2395 const context = sessions.get(threadId);2190 const handleSdkMessage = Effect.fn("handleSdkMessage")(function* (
2396 if (!context) {2191 context: ClaudeSessionContext,
2397 return Effect.fail(2192 message: SDKMessage,
2398 new ProviderAdapterSessionNotFoundError({2193 ) {
2399 provider: PROVIDER,2194 yield* logNativeSdkMessage(context, message);
2400 threadId,2195 yield* ensureThreadId(context, message);
2401 }),2196
2402 );2197 switch (message.type) {
2403 }2198 case "stream_event":
2404 if (context.stopped || context.session.status === "closed") {2199 yield* handleStreamEvent(context, message);
2405 return Effect.fail(2200 return;
2406 new ProviderAdapterSessionClosedError({2201 case "user":
2407 provider: PROVIDER,2202 yield* handleUserMessage(context, message);
2408 threadId,2203 return;
2409 }),2204 case "assistant":
2205 yield* handleAssistantMessage(context, message);
2206 return;
2207 case "result":
2208 yield* handleResultMessage(context, message);
2209 return;
2210 case "system":
2211 yield* handleSystemMessage(context, message);
2212 return;
2213 case "tool_progress":
2214 case "tool_use_summary":
2215 case "auth_status":
2216 case "rate_limit_event":
2217 yield* handleSdkTelemetryMessage(context, message);
2218 return;
2219 default:
2220 yield* emitRuntimeWarning(
2221 context,
2222 `Unhandled Claude SDK message type '${message.type}'.`,
2223 message,
2410 );2224 );
2411 }2225 return;
2412 return Effect.succeed(context);2226 }
2413 };2227 });
24142228
2415 const startSession: ClaudeAdapterShape["startSession"] = (input) =>2229 const runSdkStream = (context: ClaudeSessionContext): Effect.Effect<void, Error> =>
2416 Effect.gen(function* () {2230 Stream.fromAsyncIterable(context.query, (cause) =>
2417 if (input.provider !== undefined && input.provider !== PROVIDER) {2231 toError(cause, "Claude runtime stream failed."),
2418 return yield* new ProviderAdapterValidationError({2232 ).pipe(
2419 provider: PROVIDER,2233 Stream.takeWhile(() => !context.stopped),
2420 operation: "startSession",2234 Stream.runForEach((message) => handleSdkMessage(context, message)),
2421 issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,2235 );
2422 });2236
2237 const handleStreamExit = Effect.fn("handleStreamExit")(function* (
2238 context: ClaudeSessionContext,
2239 exit: Exit.Exit<void, Error>,
2240 ) {
2241 if (context.stopped) {
2242 return;
2243 }
2244
2245 if (Exit.isFailure(exit)) {
2246 if (isClaudeInterruptedCause(exit.cause)) {
2247 if (context.turnState) {
2248 yield* completeTurn(
2249 context,
2250 "interrupted",
2251 interruptionMessageFromClaudeCause(exit.cause),
2252 );
2423 }2253 }
2254 } else {
2255 const message = messageFromClaudeStreamCause(exit.cause, "Claude runtime stream failed.");
2256 yield* emitRuntimeError(context, message, Cause.pretty(exit.cause));
2257 yield* completeTurn(context, "failed", message);
2258 }
2259 } else if (context.turnState) {
2260 yield* completeTurn(context, "interrupted", "Claude runtime stream ended.");
2261 }
24242262
2425 const startedAt = yield* nowIso;2263 yield* stopSessionInternal(context, {
2426 const resumeState = readClaudeResumeState(input.resumeCursor);2264 emitExitEvent: true,
2427 const threadId = input.threadId;2265 });
2428 const existingResumeSessionId = resumeState?.resume;2266 });
2429 const newSessionId =
2430 existingResumeSessionId === undefined ? yield* Random.nextUUIDv4 : undefined;
2431 const sessionId = existingResumeSessionId ?? newSessionId;
2432
2433 const promptQueue = yield* Queue.unbounded<PromptQueueItem>();
2434 const prompt = Stream.fromQueue(promptQueue).pipe(
2435 Stream.filter((item) => item.type === "message"),
2436 Stream.map((item) => item.message),
2437 Stream.catchCause((cause) =>
2438 Cause.hasInterruptsOnly(cause) ? Stream.empty : Stream.failCause(cause),
2439 ),
2440 Stream.toAsyncIterable,
2441 );
24422267
2443 const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>();2268 const stopSessionInternal = Effect.fn("stopSessionInternal")(function* (
2444 const pendingUserInputs = new Map<ApprovalRequestId, PendingUserInput>();2269 context: ClaudeSessionContext,
2445 const inFlightTools = new Map<number, ToolInFlight>();2270 options?: { readonly emitExitEvent?: boolean },
24462271 ) {
2447 const contextRef = yield* Ref.make<ClaudeSessionContext | undefined>(undefined);2272 if (context.stopped) return;
24482273
2449 /**2274 context.stopped = true;
2450 * Handle AskUserQuestion tool calls by emitting a `user-input.requested`2275
2451 * runtime event and waiting for the user to respond via `respondToUserInput`.2276 for (const [requestId, pending] of context.pendingApprovals) {
2452 */2277 yield* Deferred.succeed(pending.decision, "cancel");
2453 const handleAskUserQuestion = (2278 const stamp = yield* makeEventStamp();
2454 context: ClaudeSessionContext,2279 yield* offerRuntimeEvent({
2455 toolInput: Record<string, unknown>,2280 type: "request.resolved",
2456 callbackOptions: { readonly signal: AbortSignal; readonly toolUseID?: string },2281 eventId: stamp.eventId,
2457 ) =>2282 provider: PROVIDER,
2458 Effect.gen(function* () {2283 createdAt: stamp.createdAt,
2459 const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4);2284 threadId: context.session.threadId,
24602285 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
2461 // Parse questions from the SDK's AskUserQuestion input.2286 requestId: asRuntimeRequestId(requestId),
2462 const rawQuestions = Array.isArray(toolInput.questions) ? toolInput.questions : [];2287 payload: {
2463 const questions: Array<UserInputQuestion> = rawQuestions.map(2288 requestType: pending.requestType,
2464 (q: Record<string, unknown>, idx: number) => ({2289 decision: "cancel",
2465 id: typeof q.header === "string" ? q.header : `q-${idx}`,2290 },
2466 header: typeof q.header === "string" ? q.header : `Question ${idx + 1}`,2291 providerRefs: nativeProviderRefs(context),
2467 question: typeof q.question === "string" ? q.question : "",2292 });
2468 options: Array.isArray(q.options)2293 }
2469 ? q.options.map((opt: Record<string, unknown>) => ({2294 context.pendingApprovals.clear();
2470 label: typeof opt.label === "string" ? opt.label : "",
2471 description: typeof opt.description === "string" ? opt.description : "",
2472 }))
2473 : [],
2474 multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : false,
2475 }),
2476 );
2477
2478 const answersDeferred = yield* Deferred.make<ProviderUserInputAnswers>();
2479 let aborted = false;
2480 const pendingInput: PendingUserInput = {
2481 questions,
2482 answers: answersDeferred,
2483 };
2484
2485 // Emit user-input.requested so the UI can present the questions.
2486 const requestedStamp = yield* makeEventStamp();
2487 yield* offerRuntimeEvent({
2488 type: "user-input.requested",
2489 eventId: requestedStamp.eventId,
2490 provider: PROVIDER,
2491 createdAt: requestedStamp.createdAt,
2492 threadId: context.session.threadId,
2493 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
2494 requestId: asRuntimeRequestId(requestId),
2495 payload: { questions },
2496 providerRefs: nativeProviderRefs(context, {
2497 providerItemId: callbackOptions.toolUseID,
2498 }),
2499 raw: {
2500 source: "claude.sdk.permission",
2501 method: "canUseTool/AskUserQuestion",
2502 payload: { toolName: "AskUserQuestion", input: toolInput },
2503 },
2504 });
25052295
2506 pendingUserInputs.set(requestId, pendingInput);2296 if (context.turnState) {
25072297 yield* completeTurn(context, "interrupted", "Session stopped.");
2508 // Handle abort (e.g. turn interrupted while waiting for user input).2298 }
2509 const onAbort = () => {
2510 if (!pendingUserInputs.has(requestId)) {
2511 return;
2512 }
2513 aborted = true;
2514 pendingUserInputs.delete(requestId);
2515 Effect.runFork(Deferred.succeed(answersDeferred, {} as ProviderUserInputAnswers));
2516 };
2517 callbackOptions.signal.addEventListener("abort", onAbort, { once: true });
2518
2519 // Block until the user provides answers.
2520 const answers = yield* Deferred.await(answersDeferred);
2521 pendingUserInputs.delete(requestId);
2522
2523 // Emit user-input.resolved so the UI knows the interaction completed.
2524 const resolvedStamp = yield* makeEventStamp();
2525 yield* offerRuntimeEvent({
2526 type: "user-input.resolved",
2527 eventId: resolvedStamp.eventId,
2528 provider: PROVIDER,
2529 createdAt: resolvedStamp.createdAt,
2530 threadId: context.session.threadId,
2531 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
2532 requestId: asRuntimeRequestId(requestId),
2533 payload: { answers },
2534 providerRefs: nativeProviderRefs(context, {
2535 providerItemId: callbackOptions.toolUseID,
2536 }),
2537 raw: {
2538 source: "claude.sdk.permission",
2539 method: "canUseTool/AskUserQuestion/resolved",
2540 payload: { answers },
2541 },
2542 });
25432299
2544 if (aborted) {2300 yield* Queue.shutdown(context.promptQueue);
2545 return {
2546 behavior: "deny",
2547 message: "User cancelled tool execution.",
2548 } satisfies PermissionResult;
2549 }
25502301
2551 // Return the answers to the SDK in the expected format:2302 const streamFiber = context.streamFiber;
2552 // { questions: [...], answers: { questionText: selectedLabel } }2303 context.streamFiber = undefined;
2553 return {2304 if (streamFiber && streamFiber.pollUnsafe() === undefined) {
2554 behavior: "allow",2305 yield* Fiber.interrupt(streamFiber);
2555 updatedInput: {2306 }
2556 questions: toolInput.questions,
2557 answers,
2558 },
2559 } satisfies PermissionResult;
2560 });
2561
2562 const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) =>
2563 Effect.runPromise(
2564 Effect.gen(function* () {
2565 const context = yield* Ref.get(contextRef);
2566 if (!context) {
2567 return {
2568 behavior: "deny",
2569 message: "Claude session context is unavailable.",
2570 } satisfies PermissionResult;
2571 }
2572
2573 // Handle AskUserQuestion: surface clarifying questions to the
2574 // user via the user-input runtime event channel, regardless of
2575 // runtime mode (plan mode relies on this heavily).
2576 if (toolName === "AskUserQuestion") {
2577 return yield* handleAskUserQuestion(context, toolInput, callbackOptions);
2578 }
2579
2580 if (toolName === "ExitPlanMode") {
2581 const planMarkdown = extractExitPlanModePlan(toolInput);
2582 if (planMarkdown) {
2583 yield* emitProposedPlanCompleted(context, {
2584 planMarkdown,
2585 toolUseId: callbackOptions.toolUseID,
2586 rawSource: "claude.sdk.permission",
2587 rawMethod: "canUseTool/ExitPlanMode",
2588 rawPayload: {
2589 toolName,
2590 input: toolInput,
2591 },
2592 });
2593 }
25942307
2595 return {2308 // @effect-diagnostics-next-line tryCatchInEffectGen:off
2596 behavior: "deny",2309 try {
2597 message:2310 context.query.close();
2598 "The client captured your proposed plan. Stop here and wait for the user's feedback or implementation request in a later turn.",2311 } catch (cause) {
2599 } satisfies PermissionResult;2312 yield* emitRuntimeError(context, "Failed to close Claude runtime query.", cause);
2600 }2313 }
2601
2602 const runtimeMode = input.runtimeMode ?? "full-access";
2603 if (runtimeMode === "full-access") {
2604 return {
2605 behavior: "allow",
2606 updatedInput: toolInput,
2607 } satisfies PermissionResult;
2608 }
2609
2610 const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4);
2611 const requestType = classifyRequestType(toolName);
2612 const detail = summarizeToolRequest(toolName, toolInput);
2613 const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>();
2614 const pendingApproval: PendingApproval = {
2615 requestType,
2616 detail,
2617 decision: decisionDeferred,
2618 ...(callbackOptions.suggestions
2619 ? { suggestions: callbackOptions.suggestions }
2620 : {}),
2621 };
2622
2623 const requestedStamp = yield* makeEventStamp();
2624 yield* offerRuntimeEvent({
2625 type: "request.opened",
2626 eventId: requestedStamp.eventId,
2627 provider: PROVIDER,
2628 createdAt: requestedStamp.createdAt,
2629 threadId: context.session.threadId,
2630 ...(context.turnState
2631 ? { turnId: asCanonicalTurnId(context.turnState.turnId) }
2632 : {}),
2633 requestId: asRuntimeRequestId(requestId),
2634 payload: {
2635 requestType,
2636 detail,
2637 args: {
2638 toolName,
2639 input: toolInput,
2640 ...(callbackOptions.toolUseID ? { toolUseId: callbackOptions.toolUseID } : {}),
2641 },
2642 },
2643 providerRefs: nativeProviderRefs(context, {
2644 providerItemId: callbackOptions.toolUseID,
2645 }),
2646 raw: {
2647 source: "claude.sdk.permission",
2648 method: "canUseTool/request",
2649 payload: {
2650 toolName,
2651 input: toolInput,
2652 },
2653 },
2654 });
2655
2656 pendingApprovals.set(requestId, pendingApproval);
2657
2658 const onAbort = () => {
2659 if (!pendingApprovals.has(requestId)) {
2660 return;
2661 }
2662 pendingApprovals.delete(requestId);
2663 Effect.runFork(Deferred.succeed(decisionDeferred, "cancel"));
2664 };
2665
2666 callbackOptions.signal.addEventListener("abort", onAbort, {
2667 once: true,
2668 });
2669
2670 const decision = yield* Deferred.await(decisionDeferred);
2671 pendingApprovals.delete(requestId);
2672
2673 const resolvedStamp = yield* makeEventStamp();
2674 yield* offerRuntimeEvent({
2675 type: "request.resolved",
2676 eventId: resolvedStamp.eventId,
2677 provider: PROVIDER,
2678 createdAt: resolvedStamp.createdAt,
2679 threadId: context.session.threadId,
2680 ...(context.turnState
2681 ? { turnId: asCanonicalTurnId(context.turnState.turnId) }
2682 : {}),
2683 requestId: asRuntimeRequestId(requestId),
2684 payload: {
2685 requestType,
2686 decision,
2687 },
2688 providerRefs: nativeProviderRefs(context, {
2689 providerItemId: callbackOptions.toolUseID,
2690 }),
2691 raw: {
2692 source: "claude.sdk.permission",
2693 method: "canUseTool/decision",
2694 payload: {
2695 decision,
2696 },
2697 },
2698 });
2699
2700 if (decision === "accept" || decision === "acceptForSession") {
2701 return {
2702 behavior: "allow",
2703 updatedInput: toolInput,
2704 ...(decision === "acceptForSession" && pendingApproval.suggestions
2705 ? { updatedPermissions: [...pendingApproval.suggestions] }
2706 : {}),
2707 } satisfies PermissionResult;
2708 }
2709
2710 return {
2711 behavior: "deny",
2712 message:
2713 decision === "cancel"
2714 ? "User cancelled tool execution."
2715 : "User declined tool execution.",
2716 } satisfies PermissionResult;
2717 }),
2718 );
27192314
2720 const claudeSettings = yield* serverSettingsService.getSettings.pipe(2315 const updatedAt = yield* nowIso;
2721 Effect.map((settings) => settings.providers.claudeAgent),2316 context.session = {
2722 Effect.mapError(2317 ...context.session,
2723 (error) =>2318 status: "closed",
2724 new ProviderAdapterProcessError({2319 activeTurnId: undefined,
2725 provider: PROVIDER,2320 updatedAt,
2726 threadId: input.threadId,2321 };
2727 detail: error.message,
2728 cause: error,
2729 }),
2730 ),
2731 );
2732 const claudeBinaryPath = claudeSettings.binaryPath;
2733 const modelSelection =
2734 input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;
2735 const caps = getClaudeModelCapabilities(modelSelection?.model);
2736 const apiModelId = modelSelection ? resolveApiModelId(modelSelection) : undefined;
2737 const effort = (resolveEffort(caps, modelSelection?.options?.effort) ??
2738 null) as ClaudeCodeEffort | null;
2739 const fastMode = modelSelection?.options?.fastMode === true && caps.supportsFastMode;
2740 const thinking =
2741 typeof modelSelection?.options?.thinking === "boolean" && caps.supportsThinkingToggle
2742 ? modelSelection.options.thinking
2743 : undefined;
2744 const effectiveEffort = getEffectiveClaudeCodeEffort(effort);
2745 const permissionMode =
2746 input.runtimeMode === "full-access" ? "bypassPermissions" : undefined;
2747 const settings = {
2748 ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
2749 ...(fastMode ? { fastMode: true } : {}),
2750 };
27512322
2752 const queryOptions: ClaudeQueryOptions = {2323 if (options?.emitExitEvent !== false) {
2753 ...(input.cwd ? { cwd: input.cwd } : {}),2324 const stamp = yield* makeEventStamp();
2754 ...(apiModelId ? { model: apiModelId } : {}),2325 yield* offerRuntimeEvent({
2755 pathToClaudeCodeExecutable: claudeBinaryPath,2326 type: "session.exited",
2756 settingSources: [...CLAUDE_SETTING_SOURCES],2327 eventId: stamp.eventId,
2757 ...(effectiveEffort ? { effort: effectiveEffort } : {}),2328 provider: PROVIDER,
2758 ...(permissionMode ? { permissionMode } : {}),2329 createdAt: stamp.createdAt,
2759 ...(permissionMode === "bypassPermissions"2330 threadId: context.session.threadId,
2760 ? { allowDangerouslySkipPermissions: true }2331 payload: {
2761 : {}),2332 reason: "Session stopped",
2762 ...(Object.keys(settings).length > 0 ? { settings } : {}),2333 exitKind: "graceful",
2763 ...(existingResumeSessionId ? { resume: existingResumeSessionId } : {}),2334 },
2764 ...(newSessionId ? { sessionId: newSessionId } : {}),2335 providerRefs: {},
2765 includePartialMessages: true,2336 });
2766 canUseTool,2337 }
2767 env: process.env,
2768 ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}),
2769 };
27702338
2771 const queryRuntime = yield* Effect.try({2339 sessions.delete(context.session.threadId);
2772 try: () =>2340 });
2773 createQuery({
2774 prompt,
2775 options: queryOptions,
2776 }),
2777 catch: (cause) =>
2778 new ProviderAdapterProcessError({
2779 provider: PROVIDER,
2780 threadId,
2781 detail: toMessage(cause, "Failed to start Claude runtime session."),
2782 cause,
2783 }),
2784 });
27852341
2786 const session: ProviderSession = {2342 const requireSession = (
2343 threadId: ThreadId,
2344 ): Effect.Effect<ClaudeSessionContext, ProviderAdapterError> => {
2345 const context = sessions.get(threadId);
2346 if (!context) {
2347 return Effect.fail(
2348 new ProviderAdapterSessionNotFoundError({
2349 provider: PROVIDER,
2787 threadId,2350 threadId,
2351 }),
2352 );
2353 }
2354 if (context.stopped || context.session.status === "closed") {
2355 return Effect.fail(
2356 new ProviderAdapterSessionClosedError({
2788 provider: PROVIDER,2357 provider: PROVIDER,
2789 status: "ready",2358 threadId,
2790 runtimeMode: input.runtimeMode,2359 }),
2791 ...(input.cwd ? { cwd: input.cwd } : {}),2360 );
2792 ...(modelSelection?.model ? { model: modelSelection.model } : {}),2361 }
2793 ...(threadId ? { threadId } : {}),2362 return Effect.succeed(context);
2794 resumeCursor: {2363 };
2795 ...(threadId ? { threadId } : {}),
2796 ...(sessionId ? { resume: sessionId } : {}),
2797 ...(resumeState?.resumeSessionAt
2798 ? { resumeSessionAt: resumeState.resumeSessionAt }
2799 : {}),
2800 turnCount: resumeState?.turnCount ?? 0,
2801 },
2802 createdAt: startedAt,
2803 updatedAt: startedAt,
2804 };
2805
2806 const context: ClaudeSessionContext = {
2807 session,
2808 promptQueue,
2809 query: queryRuntime,
2810 streamFiber: undefined,
2811 startedAt,
2812 basePermissionMode: permissionMode,
2813 currentApiModelId: apiModelId,
2814 resumeSessionId: sessionId,
2815 pendingApprovals,
2816 pendingUserInputs,
2817 turns: [],
2818 inFlightTools,
2819 turnState: undefined,
2820 lastKnownContextWindow: undefined,
2821 lastKnownTokenUsage: undefined,
2822 lastAssistantUuid: resumeState?.resumeSessionAt,
2823 lastThreadStartedId: undefined,
2824 stopped: false,
2825 };
2826 yield* Ref.set(contextRef, context);
2827 sessions.set(threadId, context);
28282364
2829 const sessionStartedStamp = yield* makeEventStamp();2365 const startSession: ClaudeAdapterShape["startSession"] = Effect.fn("startSession")(
2830 yield* offerRuntimeEvent({2366 function* (input) {
2831 type: "session.started",2367 if (input.provider !== undefined && input.provider !== PROVIDER) {
2832 eventId: sessionStartedStamp.eventId,2368 return yield* new ProviderAdapterValidationError({
2833 provider: PROVIDER,2369 provider: PROVIDER,
2834 createdAt: sessionStartedStamp.createdAt,2370 operation: "startSession",
2835 threadId,2371 issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,
2836 payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {},
2837 providerRefs: {},
2838 });2372 });
2373 }
2374
2375 const startedAt = yield* nowIso;
2376 const resumeState = readClaudeResumeState(input.resumeCursor);
2377 const threadId = input.threadId;
2378 const existingResumeSessionId = resumeState?.resume;
2379 const newSessionId =
2380 existingResumeSessionId === undefined ? yield* Random.nextUUIDv4 : undefined;
2381 const sessionId = existingResumeSessionId ?? newSessionId;
2382
2383 const promptQueue = yield* Queue.unbounded<PromptQueueItem>();
2384 const prompt = Stream.fromQueue(promptQueue).pipe(
2385 Stream.filter((item) => item.type === "message"),
2386 Stream.map((item) => item.message),
2387 Stream.catchCause((cause) =>
2388 Cause.hasInterruptsOnly(cause) ? Stream.empty : Stream.failCause(cause),
2389 ),
2390 Stream.toAsyncIterable,
2391 );
28392392
2840 const configuredStamp = yield* makeEventStamp();2393 const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>();
2394 const pendingUserInputs = new Map<ApprovalRequestId, PendingUserInput>();
2395 const inFlightTools = new Map<number, ToolInFlight>();
2396
2397 const contextRef = yield* Ref.make<ClaudeSessionContext | undefined>(undefined);
2398
2399 /**
2400 * Handle AskUserQuestion tool calls by emitting a `user-input.requested`
2401 * runtime event and waiting for the user to respond via `respondToUserInput`.
2402 */
2403 const handleAskUserQuestion = Effect.fn("handleAskUserQuestion")(function* (
2404 context: ClaudeSessionContext,
2405 toolInput: Record<string, unknown>,
2406 callbackOptions: { readonly signal: AbortSignal; readonly toolUseID?: string },
2407 ) {
2408 const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4);
2409
2410 // Parse questions from the SDK's AskUserQuestion input.
2411 const rawQuestions = Array.isArray(toolInput.questions) ? toolInput.questions : [];
2412 const questions: Array<UserInputQuestion> = rawQuestions.map(
2413 (q: Record<string, unknown>, idx: number) => ({
2414 id: typeof q.header === "string" ? q.header : `q-${idx}`,
2415 header: typeof q.header === "string" ? q.header : `Question ${idx + 1}`,
2416 question: typeof q.question === "string" ? q.question : "",
2417 options: Array.isArray(q.options)
2418 ? q.options.map((opt: Record<string, unknown>) => ({
2419 label: typeof opt.label === "string" ? opt.label : "",
2420 description: typeof opt.description === "string" ? opt.description : "",
2421 }))
2422 : [],
2423 multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : false,
2424 }),
2425 );
2426
2427 const answersDeferred = yield* Deferred.make<ProviderUserInputAnswers>();
2428 let aborted = false;
2429 const pendingInput: PendingUserInput = {
2430 questions,
2431 answers: answersDeferred,
2432 };
2433
2434 // Emit user-input.requested so the UI can present the questions.
2435 const requestedStamp = yield* makeEventStamp();
2841 yield* offerRuntimeEvent({2436 yield* offerRuntimeEvent({
2842 type: "session.configured",2437 type: "user-input.requested",
2843 eventId: configuredStamp.eventId,2438 eventId: requestedStamp.eventId,
2844 provider: PROVIDER,2439 provider: PROVIDER,
2845 createdAt: configuredStamp.createdAt,2440 createdAt: requestedStamp.createdAt,
2846 threadId,2441 threadId: context.session.threadId,
2847 payload: {2442 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
2848 config: {2443 requestId: asRuntimeRequestId(requestId),
2849 ...(apiModelId ? { model: apiModelId } : {}),2444 payload: { questions },
2850 ...(input.cwd ? { cwd: input.cwd } : {}),2445 providerRefs: nativeProviderRefs(context, {
2851 ...(effectiveEffort ? { effort: effectiveEffort } : {}),2446 providerItemId: callbackOptions.toolUseID,
2852 ...(permissionMode ? { permissionMode } : {}),2447 }),
2853 ...(fastMode ? { fastMode: true } : {}),2448 raw: {
2854 },2449 source: "claude.sdk.permission",
2450 method: "canUseTool/AskUserQuestion",
2451 payload: { toolName: "AskUserQuestion", input: toolInput },
2855 },2452 },
2856 providerRefs: {},
2857 });2453 });
28582454
2859 const readyStamp = yield* makeEventStamp();2455 pendingUserInputs.set(requestId, pendingInput);
2456
2457 // Handle abort (e.g. turn interrupted while waiting for user input).
2458 const onAbort = () => {
2459 if (!pendingUserInputs.has(requestId)) {
2460 return;
2461 }
2462 aborted = true;
2463 pendingUserInputs.delete(requestId);
2464 Effect.runFork(Deferred.succeed(answersDeferred, {} as ProviderUserInputAnswers));
2465 };
2466 callbackOptions.signal.addEventListener("abort", onAbort, { once: true });
2467
2468 // Block until the user provides answers.
2469 const answers = yield* Deferred.await(answersDeferred);
2470 pendingUserInputs.delete(requestId);
2471
2472 // Emit user-input.resolved so the UI knows the interaction completed.
2473 const resolvedStamp = yield* makeEventStamp();
2860 yield* offerRuntimeEvent({2474 yield* offerRuntimeEvent({
2861 type: "session.state.changed",2475 type: "user-input.resolved",
2862 eventId: readyStamp.eventId,2476 eventId: resolvedStamp.eventId,
2863 provider: PROVIDER,2477 provider: PROVIDER,
2864 createdAt: readyStamp.createdAt,2478 createdAt: resolvedStamp.createdAt,
2865 threadId,2479 threadId: context.session.threadId,
2866 payload: {2480 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
2867 state: "ready",2481 requestId: asRuntimeRequestId(requestId),
2482 payload: { answers },
2483 providerRefs: nativeProviderRefs(context, {
2484 providerItemId: callbackOptions.toolUseID,
2485 }),
2486 raw: {
2487 source: "claude.sdk.permission",
2488 method: "canUseTool/AskUserQuestion/resolved",
2489 payload: { answers },
2868 },2490 },
2869 providerRefs: {},
2870 });2491 });
28712492
2872 const streamFiber = Effect.runFork(runSdkStream(context));2493 if (aborted) {
2873 context.streamFiber = streamFiber;2494 return {
2874 streamFiber.addObserver((exit) => {2495 behavior: "deny",
2875 if (context.stopped) {2496 message: "User cancelled tool execution.",
2876 return;2497 } satisfies PermissionResult;
2877 }2498 }
2878 if (context.streamFiber === streamFiber) {
2879 context.streamFiber = undefined;
2880 }
2881 Effect.runFork(handleStreamExit(context, exit));
2882 });
28832499
2500 // Return the answers to the SDK in the expected format:
2501 // { questions: [...], answers: { questionText: selectedLabel } }
2884 return {2502 return {
2885 ...session,2503 behavior: "allow",
2886 };2504 updatedInput: {
2505 questions: toolInput.questions,
2506 answers,
2507 },
2508 } satisfies PermissionResult;
2887 });2509 });
28882510
2889 const sendTurn: ClaudeAdapterShape["sendTurn"] = (input) =>2511 const canUseToolEffect = Effect.fn("canUseTool")(function* (
2890 Effect.gen(function* () {2512 toolName: Parameters<CanUseTool>[0],
2891 const context = yield* requireSession(input.threadId);2513 toolInput: Parameters<CanUseTool>[1],
2892 const modelSelection =2514 callbackOptions: Parameters<CanUseTool>[2],
2893 input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;2515 ) {
2516 const context = yield* Ref.get(contextRef);
2517 if (!context) {
2518 return {
2519 behavior: "deny",
2520 message: "Claude session context is unavailable.",
2521 } satisfies PermissionResult;
2522 }
28942523
2895 if (context.turnState) {2524 // Handle AskUserQuestion: surface clarifying questions to the
2896 // Auto-close a stale synthetic turn (from background agent responses2525 // user via the user-input runtime event channel, regardless of
2897 // between user prompts) to prevent blocking the user's next turn.2526 // runtime mode (plan mode relies on this heavily).
2898 yield* completeTurn(context, "completed");2527 if (toolName === "AskUserQuestion") {
2528 return yield* handleAskUserQuestion(context, toolInput, callbackOptions);
2899 }2529 }
29002530
2901 if (modelSelection?.model) {2531 if (toolName === "ExitPlanMode") {
2902 const apiModelId = resolveApiModelId(modelSelection);2532 const planMarkdown = extractExitPlanModePlan(toolInput);
2903 if (context.currentApiModelId !== apiModelId) {2533 if (planMarkdown) {
2904 yield* Effect.tryPromise({2534 yield* emitProposedPlanCompleted(context, {
2905 try: () => context.query.setModel(apiModelId),2535 planMarkdown,
2906 catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause),2536 toolUseId: callbackOptions.toolUseID,
2537 rawSource: "claude.sdk.permission",
2538 rawMethod: "canUseTool/ExitPlanMode",
2539 rawPayload: {
2540 toolName,
2541 input: toolInput,
2542 },
2907 });2543 });
2908 context.currentApiModelId = apiModelId;
2909 }2544 }
2910 context.session = {
2911 ...context.session,
2912 model: modelSelection.model,
2913 };
2914 }
29152545
2916 // Apply interaction mode by switching the SDK's permission mode.2546 return {
2917 // "plan" maps directly to the SDK's "plan" permission mode;2547 behavior: "deny",
2918 // "default" restores the session's original permission mode.2548 message:
2919 // When interactionMode is absent we leave the current mode unchanged.2549 "The client captured your proposed plan. Stop here and wait for the user's feedback or implementation request in a later turn.",
2920 if (input.interactionMode === "plan") {2550 } satisfies PermissionResult;
2921 yield* Effect.tryPromise({
2922 try: () => context.query.setPermissionMode("plan"),
2923 catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause),
2924 });
2925 } else if (input.interactionMode === "default") {
2926 yield* Effect.tryPromise({
2927 try: () =>
2928 context.query.setPermissionMode(context.basePermissionMode ?? "bypassPermissions"),
2929 catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause),
2930 });
2931 }2551 }
29322552
2933 const turnId = TurnId.makeUnsafe(yield* Random.nextUUIDv4);2553 const runtimeMode = input.runtimeMode ?? "full-access";
2934 const turnState: ClaudeTurnState = {2554 if (runtimeMode === "full-access") {
2935 turnId,2555 return {
2936 startedAt: yield* nowIso,2556 behavior: "allow",
2937 items: [],2557 updatedInput: toolInput,
2938 assistantTextBlocks: new Map(),2558 } satisfies PermissionResult;
2939 assistantTextBlockOrder: [],2559 }
2940 capturedProposedPlanKeys: new Set(),
2941 nextSyntheticAssistantBlockIndex: -1,
2942 };
29432560
2944 const updatedAt = yield* nowIso;2561 const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4);
2945 context.turnState = turnState;2562 const requestType = classifyRequestType(toolName);
2946 context.session = {2563 const detail = summarizeToolRequest(toolName, toolInput);
2947 ...context.session,2564 const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>();
2948 status: "running",2565 const pendingApproval: PendingApproval = {
2949 activeTurnId: turnId,2566 requestType,
2950 updatedAt,2567 detail,
2568 decision: decisionDeferred,
2569 ...(callbackOptions.suggestions ? { suggestions: callbackOptions.suggestions } : {}),
2951 };2570 };
29522571
2953 const turnStartedStamp = yield* makeEventStamp();2572 const requestedStamp = yield* makeEventStamp();
2954 yield* offerRuntimeEvent({2573 yield* offerRuntimeEvent({
2955 type: "turn.started",2574 type: "request.opened",
2956 eventId: turnStartedStamp.eventId,2575 eventId: requestedStamp.eventId,
2957 provider: PROVIDER,2576 provider: PROVIDER,
2958 createdAt: turnStartedStamp.createdAt,2577 createdAt: requestedStamp.createdAt,
2959 threadId: context.session.threadId,2578 threadId: context.session.threadId,
2960 turnId,2579 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
2961 payload: modelSelection?.model ? { model: modelSelection.model } : {},2580 requestId: asRuntimeRequestId(requestId),
2962 providerRefs: {},2581 payload: {
2582 requestType,
2583 detail,
2584 args: {
2585 toolName,
2586 input: toolInput,
2587 ...(callbackOptions.toolUseID ? { toolUseId: callbackOptions.toolUseID } : {}),
2588 },
2589 },
2590 providerRefs: nativeProviderRefs(context, {
2591 providerItemId: callbackOptions.toolUseID,
2592 }),
2593 raw: {
2594 source: "claude.sdk.permission",
2595 method: "canUseTool/request",
2596 payload: {
2597 toolName,
2598 input: toolInput,
2599 },
2600 },
2963 });2601 });
29642602
2965 const message = yield* buildUserMessageEffect(input, {2603 pendingApprovals.set(requestId, pendingApproval);
2966 fileSystem,2604
2967 attachmentsDir: serverConfig.attachmentsDir,2605 const onAbort = () => {
2606 if (!pendingApprovals.has(requestId)) {
2607 return;
2608 }
2609 pendingApprovals.delete(requestId);
2610 Effect.runFork(Deferred.succeed(decisionDeferred, "cancel"));
2611 };
2612
2613 callbackOptions.signal.addEventListener("abort", onAbort, {
2614 once: true,
2968 });2615 });
29692616
2970 yield* Queue.offer(context.promptQueue, {2617 const decision = yield* Deferred.await(decisionDeferred);
2971 type: "message",2618 pendingApprovals.delete(requestId);
2972 message,
2973 }).pipe(Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause)));
29742619
2975 return {2620 const resolvedStamp = yield* makeEventStamp();
2621 yield* offerRuntimeEvent({
2622 type: "request.resolved",
2623 eventId: resolvedStamp.eventId,
2624 provider: PROVIDER,
2625 createdAt: resolvedStamp.createdAt,
2976 threadId: context.session.threadId,2626 threadId: context.session.threadId,
2977 turnId,2627 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
2978 ...(context.session.resumeCursor !== undefined2628 requestId: asRuntimeRequestId(requestId),
2979 ? { resumeCursor: context.session.resumeCursor }2629 payload: {
2980 : {}),2630 requestType,
2981 };2631 decision,
2632 },
2633 providerRefs: nativeProviderRefs(context, {
2634 providerItemId: callbackOptions.toolUseID,
2635 }),
2636 raw: {
2637 source: "claude.sdk.permission",
2638 method: "canUseTool/decision",
2639 payload: {
2640 decision,
2641 },
2642 },
2643 });
2644
2645 if (decision === "accept" || decision === "acceptForSession") {
2646 return {
2647 behavior: "allow",
2648 updatedInput: toolInput,
2649 ...(decision === "acceptForSession" && pendingApproval.suggestions
2650 ? { updatedPermissions: [...pendingApproval.suggestions] }
2651 : {}),
2652 } satisfies PermissionResult;
2653 }
2654
2655 return {
2656 behavior: "deny",
2657 message:
2658 decision === "cancel"
2659 ? "User cancelled tool execution."
2660 : "User declined tool execution.",
2661 } satisfies PermissionResult;
2982 });2662 });
29832663
2984 const interruptTurn: ClaudeAdapterShape["interruptTurn"] = (threadId, _turnId) =>2664 const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) =>
2985 Effect.gen(function* () {2665 Effect.runPromise(canUseToolEffect(toolName, toolInput, callbackOptions));
2986 const context = yield* requireSession(threadId);2666
2987 yield* Effect.tryPromise({2667 const claudeSettings = yield* serverSettingsService.getSettings.pipe(
2988 try: () => context.query.interrupt(),2668 Effect.map((settings) => settings.providers.claudeAgent),
2989 catch: (cause) => toRequestError(threadId, "turn/interrupt", cause),2669 Effect.mapError(
2990 });2670 (error) =>
2671 new ProviderAdapterProcessError({
2672 provider: PROVIDER,
2673 threadId: input.threadId,
2674 detail: error.message,
2675 cause: error,
2676 }),
2677 ),
2678 );
2679 const claudeBinaryPath = claudeSettings.binaryPath;
2680 const modelSelection =
2681 input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;
2682 const caps = getClaudeModelCapabilities(modelSelection?.model);
2683 const apiModelId = modelSelection ? resolveApiModelId(modelSelection) : undefined;
2684 const effort = (resolveEffort(caps, modelSelection?.options?.effort) ??
2685 null) as ClaudeCodeEffort | null;
2686 const fastMode = modelSelection?.options?.fastMode === true && caps.supportsFastMode;
2687 const thinking =
2688 typeof modelSelection?.options?.thinking === "boolean" && caps.supportsThinkingToggle
2689 ? modelSelection.options.thinking
2690 : undefined;
2691 const effectiveEffort = getEffectiveClaudeCodeEffort(effort);
2692 const permissionMode = input.runtimeMode === "full-access" ? "bypassPermissions" : undefined;
2693 const settings = {
2694 ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
2695 ...(fastMode ? { fastMode: true } : {}),
2696 };
2697
2698 const queryOptions: ClaudeQueryOptions = {
2699 ...(input.cwd ? { cwd: input.cwd } : {}),
2700 ...(apiModelId ? { model: apiModelId } : {}),
2701 pathToClaudeCodeExecutable: claudeBinaryPath,
2702 settingSources: [...CLAUDE_SETTING_SOURCES],
2703 ...(effectiveEffort ? { effort: effectiveEffort } : {}),
2704 ...(permissionMode ? { permissionMode } : {}),
2705 ...(permissionMode === "bypassPermissions"
2706 ? { allowDangerouslySkipPermissions: true }
2707 : {}),
2708 ...(Object.keys(settings).length > 0 ? { settings } : {}),
2709 ...(existingResumeSessionId ? { resume: existingResumeSessionId } : {}),
2710 ...(newSessionId ? { sessionId: newSessionId } : {}),
2711 includePartialMessages: true,
2712 canUseTool,
2713 env: process.env,
2714 ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}),
2715 };
2716
2717 const queryRuntime = yield* Effect.try({
2718 try: () =>
2719 createQuery({
2720 prompt,
2721 options: queryOptions,
2722 }),
2723 catch: (cause) =>
2724 new ProviderAdapterProcessError({
2725 provider: PROVIDER,
2726 threadId,
2727 detail: toMessage(cause, "Failed to start Claude runtime session."),
2728 cause,
2729 }),
2730 });
2731
2732 const session: ProviderSession = {
2733 threadId,
2734 provider: PROVIDER,
2735 status: "ready",
2736 runtimeMode: input.runtimeMode,
2737 ...(input.cwd ? { cwd: input.cwd } : {}),
2738 ...(modelSelection?.model ? { model: modelSelection.model } : {}),
2739 ...(threadId ? { threadId } : {}),
2740 resumeCursor: {
2741 ...(threadId ? { threadId } : {}),
2742 ...(sessionId ? { resume: sessionId } : {}),
2743 ...(resumeState?.resumeSessionAt ? { resumeSessionAt: resumeState.resumeSessionAt } : {}),
2744 turnCount: resumeState?.turnCount ?? 0,
2745 },
2746 createdAt: startedAt,
2747 updatedAt: startedAt,
2748 };
2749
2750 const context: ClaudeSessionContext = {
2751 session,
2752 promptQueue,
2753 query: queryRuntime,
2754 streamFiber: undefined,
2755 startedAt,
2756 basePermissionMode: permissionMode,
2757 currentApiModelId: apiModelId,
2758 resumeSessionId: sessionId,
2759 pendingApprovals,
2760 pendingUserInputs,
2761 turns: [],
2762 inFlightTools,
2763 turnState: undefined,
2764 lastKnownContextWindow: undefined,
2765 lastKnownTokenUsage: undefined,
2766 lastAssistantUuid: resumeState?.resumeSessionAt,
2767 lastThreadStartedId: undefined,
2768 stopped: false,
2769 };
2770 yield* Ref.set(contextRef, context);
2771 sessions.set(threadId, context);
2772
2773 const sessionStartedStamp = yield* makeEventStamp();
2774 yield* offerRuntimeEvent({
2775 type: "session.started",
2776 eventId: sessionStartedStamp.eventId,
2777 provider: PROVIDER,
2778 createdAt: sessionStartedStamp.createdAt,
2779 threadId,
2780 payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {},
2781 providerRefs: {},
2991 });2782 });
29922783
2993 const readThread: ClaudeAdapterShape["readThread"] = (threadId) =>2784 const configuredStamp = yield* makeEventStamp();
2994 Effect.gen(function* () {2785 yield* offerRuntimeEvent({
2995 const context = yield* requireSession(threadId);2786 type: "session.configured",
2996 return yield* snapshotThread(context);2787 eventId: configuredStamp.eventId,
2788 provider: PROVIDER,
2789 createdAt: configuredStamp.createdAt,
2790 threadId,
2791 payload: {
2792 config: {
2793 ...(apiModelId ? { model: apiModelId } : {}),
2794 ...(input.cwd ? { cwd: input.cwd } : {}),
2795 ...(effectiveEffort ? { effort: effectiveEffort } : {}),
2796 ...(permissionMode ? { permissionMode } : {}),
2797 ...(fastMode ? { fastMode: true } : {}),
2798 },
2799 },
2800 providerRefs: {},
2997 });2801 });
29982802
2999 const rollbackThread: ClaudeAdapterShape["rollbackThread"] = (threadId, numTurns) =>2803 const readyStamp = yield* makeEventStamp();
3000 Effect.gen(function* () {2804 yield* offerRuntimeEvent({
3001 const context = yield* requireSession(threadId);2805 type: "session.state.changed",
3002 const nextLength = Math.max(0, context.turns.length - numTurns);2806 eventId: readyStamp.eventId,
3003 context.turns.splice(nextLength);2807 provider: PROVIDER,
3004 yield* updateResumeCursor(context);2808 createdAt: readyStamp.createdAt,
3005 return yield* snapshotThread(context);2809 threadId,
2810 payload: {
2811 state: "ready",
2812 },
2813 providerRefs: {},
3006 });2814 });
30072815
3008 const respondToRequest: ClaudeAdapterShape["respondToRequest"] = (2816 const streamFiber = Effect.runFork(runSdkStream(context));
3009 threadId,2817 context.streamFiber = streamFiber;
3010 requestId,2818 streamFiber.addObserver((exit) => {
3011 decision,2819 if (context.stopped) {
3012 ) =>2820 return;
3013 Effect.gen(function* () {
3014 const context = yield* requireSession(threadId);
3015 const pending = context.pendingApprovals.get(requestId);
3016 if (!pending) {
3017 return yield* new ProviderAdapterRequestError({
3018 provider: PROVIDER,
3019 method: "item/requestApproval/decision",
3020 detail: `Unknown pending approval request: ${requestId}`,
3021 });
3022 }2821 }
2822 if (context.streamFiber === streamFiber) {
2823 context.streamFiber = undefined;
2824 }
2825 Effect.runFork(handleStreamExit(context, exit));
2826 });
2827
2828 return {
2829 ...session,
2830 };
2831 },
2832 );
2833
2834 const sendTurn: ClaudeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) {
2835 const context = yield* requireSession(input.threadId);
2836 const modelSelection =
2837 input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;
30232838
3024 context.pendingApprovals.delete(requestId);2839 if (context.turnState) {
3025 yield* Deferred.succeed(pending.decision, decision);2840 // Auto-close a stale synthetic turn (from background agent responses
2841 // between user prompts) to prevent blocking the user's next turn.
2842 yield* completeTurn(context, "completed");
2843 }
2844
2845 if (modelSelection?.model) {
2846 const apiModelId = resolveApiModelId(modelSelection);
2847 if (context.currentApiModelId !== apiModelId) {
2848 yield* Effect.tryPromise({
2849 try: () => context.query.setModel(apiModelId),
2850 catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause),
2851 });
2852 context.currentApiModelId = apiModelId;
2853 }
2854 context.session = {
2855 ...context.session,
2856 model: modelSelection.model,
2857 };
2858 }
2859
2860 // Apply interaction mode by switching the SDK's permission mode.
2861 // "plan" maps directly to the SDK's "plan" permission mode;
2862 // "default" restores the session's original permission mode.
2863 // When interactionMode is absent we leave the current mode unchanged.
2864 if (input.interactionMode === "plan") {
2865 yield* Effect.tryPromise({
2866 try: () => context.query.setPermissionMode("plan"),
2867 catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause),
2868 });
2869 } else if (input.interactionMode === "default") {
2870 yield* Effect.tryPromise({
2871 try: () =>
2872 context.query.setPermissionMode(context.basePermissionMode ?? "bypassPermissions"),
2873 catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause),
3026 });2874 });
2875 }
30272876
3028 const respondToUserInput: ClaudeAdapterShape["respondToUserInput"] = (2877 const turnId = TurnId.makeUnsafe(yield* Random.nextUUIDv4);
3029 threadId,2878 const turnState: ClaudeTurnState = {
3030 requestId,2879 turnId,
3031 answers,2880 startedAt: yield* nowIso,
3032 ) =>2881 items: [],
3033 Effect.gen(function* () {2882 assistantTextBlocks: new Map(),
3034 const context = yield* requireSession(threadId);2883 assistantTextBlockOrder: [],
3035 const pending = context.pendingUserInputs.get(requestId);2884 capturedProposedPlanKeys: new Set(),
3036 if (!pending) {2885 nextSyntheticAssistantBlockIndex: -1,
3037 return yield* new ProviderAdapterRequestError({2886 };
3038 provider: PROVIDER,2887
3039 method: "item/tool/respondToUserInput",2888 const updatedAt = yield* nowIso;
3040 detail: `Unknown pending user-input request: ${requestId}`,2889 context.turnState = turnState;
3041 });2890 context.session = {
3042 }2891 ...context.session,
2892 status: "running",
2893 activeTurnId: turnId,
2894 updatedAt,
2895 };
2896
2897 const turnStartedStamp = yield* makeEventStamp();
2898 yield* offerRuntimeEvent({
2899 type: "turn.started",
2900 eventId: turnStartedStamp.eventId,
2901 provider: PROVIDER,
2902 createdAt: turnStartedStamp.createdAt,
2903 threadId: context.session.threadId,
2904 turnId,
2905 payload: modelSelection?.model ? { model: modelSelection.model } : {},
2906 providerRefs: {},
2907 });
2908
2909 const message = yield* buildUserMessageEffect(input, {
2910 fileSystem,
2911 attachmentsDir: serverConfig.attachmentsDir,
2912 });
2913
2914 yield* Queue.offer(context.promptQueue, {
2915 type: "message",
2916 message,
2917 }).pipe(Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause)));
2918
2919 return {
2920 threadId: context.session.threadId,
2921 turnId,
2922 ...(context.session.resumeCursor !== undefined
2923 ? { resumeCursor: context.session.resumeCursor }
2924 : {}),
2925 };
2926 });
30432927
3044 context.pendingUserInputs.delete(requestId);2928 const interruptTurn: ClaudeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")(
3045 yield* Deferred.succeed(pending.answers, answers);2929 function* (threadId, _turnId) {
2930 const context = yield* requireSession(threadId);
2931 yield* Effect.tryPromise({
2932 try: () => context.query.interrupt(),
2933 catch: (cause) => toRequestError(threadId, "turn/interrupt", cause),
3046 });2934 });
2935 },
2936 );
30472937
3048 const stopSession: ClaudeAdapterShape["stopSession"] = (threadId) =>2938 const readThread: ClaudeAdapterShape["readThread"] = Effect.fn("readThread")(
3049 Effect.gen(function* () {2939 function* (threadId) {
3050 const context = yield* requireSession(threadId);2940 const context = yield* requireSession(threadId);
3051 yield* stopSessionInternal(context, {2941 return yield* snapshotThread(context);
3052 emitExitEvent: true,2942 },
2943 );
2944
2945 const rollbackThread: ClaudeAdapterShape["rollbackThread"] = Effect.fn("rollbackThread")(
2946 function* (threadId, numTurns) {
2947 const context = yield* requireSession(threadId);
2948 const nextLength = Math.max(0, context.turns.length - numTurns);
2949 context.turns.splice(nextLength);
2950 yield* updateResumeCursor(context);
2951 return yield* snapshotThread(context);
2952 },
2953 );
2954
2955 const respondToRequest: ClaudeAdapterShape["respondToRequest"] = Effect.fn("respondToRequest")(
2956 function* (threadId, requestId, decision) {
2957 const context = yield* requireSession(threadId);
2958 const pending = context.pendingApprovals.get(requestId);
2959 if (!pending) {
2960 return yield* new ProviderAdapterRequestError({
2961 provider: PROVIDER,
2962 method: "item/requestApproval/decision",
2963 detail: `Unknown pending approval request: ${requestId}`,
3053 });2964 });
2965 }
2966
2967 context.pendingApprovals.delete(requestId);
2968 yield* Deferred.succeed(pending.decision, decision);
2969 },
2970 );
2971
2972 const respondToUserInput: ClaudeAdapterShape["respondToUserInput"] = Effect.fn(
2973 "respondToUserInput",
2974 )(function* (threadId, requestId, answers) {
2975 const context = yield* requireSession(threadId);
2976 const pending = context.pendingUserInputs.get(requestId);
2977 if (!pending) {
2978 return yield* new ProviderAdapterRequestError({
2979 provider: PROVIDER,
2980 method: "item/tool/respondToUserInput",
2981 detail: `Unknown pending user-input request: ${requestId}`,
3054 });2982 });
2983 }
30552984
3056 const listSessions: ClaudeAdapterShape["listSessions"] = () =>2985 context.pendingUserInputs.delete(requestId);
3057 Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session })));2986 yield* Deferred.succeed(pending.answers, answers);
2987 });
30582988
3059 const hasSession: ClaudeAdapterShape["hasSession"] = (threadId) =>2989 const stopSession: ClaudeAdapterShape["stopSession"] = Effect.fn("stopSession")(
3060 Effect.sync(() => {2990 function* (threadId) {
3061 const context = sessions.get(threadId);2991 const context = yield* requireSession(threadId);
3062 return context !== undefined && !context.stopped;2992 yield* stopSessionInternal(context, {
2993 emitExitEvent: true,
3063 });2994 });
2995 },
2996 );
30642997
3065 const stopAll: ClaudeAdapterShape["stopAll"] = () =>2998 const listSessions: ClaudeAdapterShape["listSessions"] = () =>
3066 Effect.forEach(2999 Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session })));
3067 sessions,
3068 ([, context]) =>
3069 stopSessionInternal(context, {
3070 emitExitEvent: true,
3071 }),
3072 { discard: true },
3073 );
30743000
3075 yield* Effect.addFinalizer(() =>3001 const hasSession: ClaudeAdapterShape["hasSession"] = (threadId) =>
3076 Effect.forEach(3002 Effect.sync(() => {
3077 sessions,3003 const context = sessions.get(threadId);
3078 ([, context]) =>3004 return context !== undefined && !context.stopped;
3079 stopSessionInternal(context, {3005 });
3080 emitExitEvent: false,3006
3081 }),3007 const stopAll: ClaudeAdapterShape["stopAll"] = () =>
3082 { discard: true },3008 Effect.forEach(
3083 ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))),3009 sessions,
3010 ([, context]) =>
3011 stopSessionInternal(context, {
3012 emitExitEvent: true,
3013 }),
3014 { discard: true },
3084 );3015 );
30853016
3086 return {3017 yield* Effect.addFinalizer(() =>
3087 provider: PROVIDER,3018 Effect.forEach(
3088 capabilities: {3019 sessions,
3089 sessionModelSwitch: "in-session",3020 ([, context]) =>
3090 },3021 stopSessionInternal(context, {
3091 startSession,3022 emitExitEvent: false,
3092 sendTurn,3023 }),
3093 interruptTurn,3024 { discard: true },
3094 readThread,3025 ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))),
3095 rollbackThread,3026 );
3096 respondToRequest,3027
3097 respondToUserInput,3028 return {
3098 stopSession,3029 provider: PROVIDER,
3099 listSessions,3030 capabilities: {
3100 hasSession,3031 sessionModelSwitch: "in-session",
3101 stopAll,3032 },
3102 streamEvents: Stream.fromQueue(runtimeEventQueue),3033 startSession,
3103 } satisfies ClaudeAdapterShape;3034 sendTurn,
3104 });3035 interruptTurn,
3105}3036 readThread,
3037 rollbackThread,
3038 respondToRequest,
3039 respondToUserInput,
3040 stopSession,
3041 listSessions,
3042 hasSession,
3043 stopAll,
3044 streamEvents: Stream.fromQueue(runtimeEventQueue),
3045 } satisfies ClaudeAdapterShape;
3046});
31063047
3107export const ClaudeAdapterLive = Layer.effect(ClaudeAdapter, makeClaudeAdapter());3048export const ClaudeAdapterLive = Layer.effect(ClaudeAdapter, makeClaudeAdapter());
31083049
docs/effect-fn-checklist.md created+183
...@@ -0,0 +1,183 @@
1# Effect.fn Refactor Checklist
2
3Generated from a repo scan for non-test wrapper-style candidates matching either `=> Effect.gen(function* ...)` or `return Effect.gen(function* ...)`.
4
5Refactor Method:
6
7```ts
8// Old
9function old () {
10 return Effect.gen(function* () {
11 ...
12 });
13}
14
15const old2 = () => Effect.gen(function* () {
16 ...
17});
18```
19
20```ts
21// New
22const new = Effect.fn('functionName')(function* () {
23 ...
24})
25```
26
27## Summary
28
29- Total non-test candidates: `322`
30
31## Suggested Order
32
33- [ ] `apps/server/src/provider/Layers/ProviderService.ts`
34- [x] `apps/server/src/provider/Layers/ClaudeAdapter.ts`
35- [ ] `apps/server/src/provider/Layers/CodexAdapter.ts`
36- [ ] `apps/server/src/git/Layers/GitCore.ts`
37- [ ] `apps/server/src/git/Layers/GitManager.ts`
38- [ ] `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`
39- [ ] `apps/server/src/orchestration/Layers/ProjectionPipeline.ts`
40- [ ] `apps/server/src/orchestration/Layers/OrchestrationEngine.ts`
41- [ ] `apps/server/src/provider/Layers/EventNdjsonLogger.ts`
42- [ ] `Everything else`
43
44## Checklist
45
46### `apps/server/src/provider/Layers/ClaudeAdapter.ts` (`62`)
47
48- [x] [buildUserMessageEffect](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L554)
49- [x] [makeClaudeAdapter](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L913)
50- [x] [startSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2414)
51- [x] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2887)
52- [x] [interruptTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2975)
53- [x] [readThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2984)
54- [x] [rollbackThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2990)
55- [x] [stopSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L3039)
56- [x] Internal helpers and callback wrappers in this file
57
58### `apps/server/src/git/Layers/GitCore.ts` (`58`)
59
60- [ ] [makeGitCore](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L495)
61- [ ] [handleTraceLine](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L317)
62- [ ] [emitCompleteLines](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L449)
63- [ ] [commit](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1178)
64- [ ] [pushCurrentBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1217)
65- [ ] [pullCurrentBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1316)
66- [ ] [checkoutBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1697)
67- [ ] Service methods and callback wrappers in this file
68
69### `apps/server/src/git/Layers/GitManager.ts` (`28`)
70
71- [ ] [configurePullRequestHeadUpstream](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L387)
72- [ ] [materializePullRequestHeadBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L428)
73- [ ] [findOpenPr](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L576)
74- [ ] [findLatestPr](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L602)
75- [ ] [runCommitStep](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L728)
76- [ ] [runPrStep](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L842)
77- [ ] [runFeatureBranchStep](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L1106)
78- [ ] Remaining helpers and nested callback wrappers in this file
79
80### `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` (`25`)
81
82- [ ] [runProjectorForEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L1161)
83- [ ] [applyProjectsProjection](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L357)
84- [ ] [applyThreadsProjection](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L415)
85- [ ] `Effect.forEach(..., threadId => Effect.gen(...))` callbacks around `L250`
86- [ ] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L264`
87- [ ] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L305`
88- [ ] Remaining apply helpers in this file
89
90### `apps/server/src/provider/Layers/ProviderService.ts` (`24`)
91
92- [ ] [makeProviderService](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L134)
93- [ ] [recoverSessionForThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L196)
94- [ ] [resolveRoutableSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L255)
95- [ ] [startSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L284)
96- [ ] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L347)
97- [ ] [interruptTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L393)
98- [ ] [respondToRequest](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L411)
99- [ ] [respondToUserInput](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L430)
100- [ ] [stopSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L445)
101- [ ] [listSessions](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L466)
102- [ ] [rollbackConversation](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L516)
103- [ ] [runStopAll](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L538)
104
105### `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (`14`)
106
107- [ ] [finalizeAssistantMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L680)
108- [ ] [upsertProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L722)
109- [ ] [finalizeBufferedProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L761)
110- [ ] [clearTurnStateForSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L800)
111- [ ] [processRuntimeEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L908)
112- [ ] Nested callback wrappers in this file
113
114### `apps/server/src/provider/Layers/CodexAdapter.ts` (`12`)
115
116- [ ] [makeCodexAdapter](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1317)
117- [ ] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1399)
118- [ ] [writeNativeEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1546)
119- [ ] [listener](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1555)
120- [ ] Remaining nested callback wrappers in this file
121
122### `apps/server/src/checkpointing/Layers/CheckpointStore.ts` (`10`)
123
124- [ ] [captureCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L89)
125- [ ] [restoreCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L183)
126- [ ] [diffCheckpoints](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L220)
127- [ ] [deleteCheckpointRefs](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L252)
128- [ ] Nested callback wrappers in this file
129
130### `apps/server/src/provider/Layers/EventNdjsonLogger.ts` (`9`)
131
132- [ ] [toLogMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L77)
133- [ ] [makeThreadWriter](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L102)
134- [ ] [makeEventNdjsonLogger](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L174)
135- [ ] [write](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L231)
136- [ ] [close](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L247)
137- [ ] Flush and writer-resolution callback wrappers in this file
138
139### `apps/server/scripts/cli.ts` (`8`)
140
141- [ ] Command handlers around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L125)
142- [ ] Command handlers around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L170)
143- [ ] Resource callbacks around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L221)
144- [ ] Resource callbacks around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L239)
145
146### `apps/server/src/orchestration/Layers/OrchestrationEngine.ts` (`7`)
147
148- [ ] [processEnvelope](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L64)
149- [ ] [dispatch](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L218)
150- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L162)
151- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L200)
152
153### `apps/server/src/orchestration/projector.ts` (`5`)
154
155- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L242)
156- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L336)
157- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L397)
158- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L446)
159- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L478)
160
161### Smaller clusters
162
163- [ ] [packages/shared/src/DrainableWorker.ts](/Users/julius/Development/Work/codething-mvp/packages/shared/src/DrainableWorker.ts) (`4`)
164- [ ] [apps/server/src/wsServer/pushBus.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer/pushBus.ts) (`4`)
165- [ ] [apps/server/src/wsServer.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer.ts) (`4`)
166- [ ] [apps/server/src/provider/Layers/ProviderRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderRegistry.ts) (`4`)
167- [ ] [apps/server/src/persistence/Layers/Sqlite.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Layers/Sqlite.ts) (`4`)
168- [ ] [apps/server/src/orchestration/Layers/ProviderCommandReactor.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts) (`4`)
169- [ ] [apps/server/src/main.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/main.ts) (`4`)
170- [ ] [apps/server/src/keybindings.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/keybindings.ts) (`4`)
171- [ ] [apps/server/src/git/Layers/CodexTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/CodexTextGeneration.ts) (`4`)
172- [ ] [apps/server/src/serverLayers.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/serverLayers.ts) (`3`)
173- [ ] [apps/server/src/telemetry/Layers/AnalyticsService.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Layers/AnalyticsService.ts) (`2`)
174- [ ] [apps/server/src/telemetry/Identify.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Identify.ts) (`2`)
175- [ ] [apps/server/src/provider/Layers/ProviderAdapterRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts) (`2`)
176- [ ] [apps/server/src/provider/Layers/CodexProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexProvider.ts) (`2`)
177- [ ] [apps/server/src/provider/Layers/ClaudeProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeProvider.ts) (`2`)
178- [ ] [apps/server/src/persistence/NodeSqliteClient.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/NodeSqliteClient.ts) (`2`)
179- [ ] [apps/server/src/persistence/Migrations.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Migrations.ts) (`2`)
180- [ ] [apps/server/src/open.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/open.ts) (`2`)
181- [ ] [apps/server/src/git/Layers/ClaudeTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/ClaudeTextGeneration.ts) (`2`)
182- [ ] [apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts) (`2`)
183- [ ] [apps/server/src/provider/makeManagedServerProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/makeManagedServerProvider.ts) (`1`)