import { type ChatAttachment, CommandId, EventId, type OrchestrationEvent, type ProviderKind, type OrchestrationSession, type ThreadId, type TurnId,} from "@t3tools/contracts";import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect"; import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";import { ProviderService } from "../../provider/Services/ProviderService.ts";import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";import { ProviderCommandReactor, type ProviderCommandReactorShape,} from "../Services/ProviderCommandReactor.ts"; type ProviderIntentEvent = Extract< OrchestrationEvent, { type: | "thread.turn-start-requested" | "thread.turn-interrupt-requested" | "thread.approval-response-requested" | "thread.session-stop-requested"; }>; function toNonEmptyProviderInput(value: string | undefined): string | undefined { const normalized = value?.trim(); return normalized && normalized.length > 0 ? normalized : undefined;} function mapProviderSessionStatusToOrchestrationStatus( status: "connecting" | "ready" | "running" | "error" | "closed",): OrchestrationSession["status"] { switch (status) { case "connecting": return "starting"; case "running": return "running"; case "error": return "error"; case "closed": return "stopped"; case "ready": default: return "ready"; }} const turnStartKeyForEvent = (event: ProviderIntentEvent): string => event.commandId !== null ? `command:${event.commandId}` : `event:${event.eventId}`; const serverCommandId = (tag: string): CommandId => CommandId.makeUnsafe(`server:${tag}:${crypto.randomUUID()}`); const HANDLED_TURN_START_KEY_MAX = 10_000;const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); const make = Effect.gen(function* () { const orchestrationEngine = yield* OrchestrationEngineService; const providerService = yield* ProviderService; const handledTurnStartKeys = yield* Cache.make<string, true>({ capacity: HANDLED_TURN_START_KEY_MAX, timeToLive: HANDLED_TURN_START_KEY_TTL, lookup: () => Effect.succeed(true), }); const hasHandledTurnStartRecently = (key: string) => Cache.getOption(handledTurnStartKeys, key).pipe( Effect.flatMap((cached) => Cache.set(handledTurnStartKeys, key, true).pipe(Effect.as(Option.isSome(cached))), ), ); const appendProviderFailureActivity = (input: { readonly threadId: ThreadId; readonly kind: | "provider.turn.start.failed" | "provider.turn.interrupt.failed" | "provider.approval.respond.failed" | "provider.session.stop.failed"; readonly summary: string; readonly detail: string; readonly turnId: TurnId | null; readonly createdAt: string; }) => orchestrationEngine.dispatch({ type: "thread.activity.append", commandId: serverCommandId("provider-failure-activity"), threadId: input.threadId, activity: { id: EventId.makeUnsafe(crypto.randomUUID()), tone: "error", kind: input.kind, summary: input.summary, payload: { detail: input.detail, }, turnId: input.turnId, createdAt: input.createdAt, }, createdAt: input.createdAt, }); const setThreadSession = (input: { readonly threadId: ThreadId; readonly session: OrchestrationSession; readonly createdAt: string; }) => orchestrationEngine.dispatch({ type: "thread.session.set", commandId: serverCommandId("provider-session-set"), threadId: input.threadId, session: input.session, createdAt: input.createdAt, }); const resolveThread = Effect.fnUntraced(function* (threadId: ThreadId) { const readModel = yield* orchestrationEngine.getReadModel(); return readModel.threads.find((entry) => entry.id === threadId); }); const ensureSessionForThread = Effect.fnUntraced(function* ( threadId: ThreadId, createdAt: string, ) { const readModel = yield* orchestrationEngine.getReadModel(); const thread = readModel.threads.find((entry) => entry.id === threadId); if (!thread) { return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`)); } const existingSessionId = thread.session?.providerSessionId; if (existingSessionId) { return existingSessionId; } const preferredProvider: ProviderKind | undefined = thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode" ? thread.session.providerName : undefined; const effectiveCwd = resolveThreadWorkspaceCwd({ thread, projects: readModel.projects, }); const startedSession = yield* providerService.startSession( threadId, { ...(preferredProvider ? { provider: preferredProvider } : {}), ...(effectiveCwd ? { cwd: effectiveCwd } : {}), ...(thread.model ? { model: thread.model } : {}), }, ); yield* setThreadSession({ threadId, session: { threadId, status: mapProviderSessionStatusToOrchestrationStatus(startedSession.status), providerName: startedSession.provider, providerSessionId: startedSession.sessionId, providerThreadId: startedSession.threadId ?? null, // Provider turn ids are not orchestration turn ids. activeTurnId: null, lastError: startedSession.lastError ?? null, updatedAt: startedSession.updatedAt, }, createdAt, }); return startedSession.sessionId; }); const sendTurnForThread = Effect.fnUntraced(function* (input: { readonly threadId: ThreadId; readonly messageText: string; readonly attachments?: ReadonlyArray<ChatAttachment>; readonly model?: string; readonly effort?: string; readonly createdAt: string; }) { const thread = yield* resolveThread(input.threadId); if (!thread) { return; } const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt); const normalizedInput = toNonEmptyProviderInput(input.messageText); const normalizedAttachments = input.attachments ?? []; yield* providerService.sendTurn({ sessionId, ...(normalizedInput ? { input: normalizedInput } : {}), ...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}), ...(input.model !== undefined ? { model: input.model } : {}), ...(input.effort !== undefined ? { effort: input.effort } : {}), }); }); const processTurnStartRequested = Effect.fnUntraced(function* ( event: Extract<ProviderIntentEvent, { type: "thread.turn-start-requested" }>, ) { const key = turnStartKeyForEvent(event); if (yield* hasHandledTurnStartRecently(key)) { return; } const thread = yield* resolveThread(event.payload.threadId); if (!thread) { return; } const message = thread.messages.find((entry) => entry.id === event.payload.messageId); if (!message || message.role !== "user") { yield* appendProviderFailureActivity({ threadId: event.payload.threadId, kind: "provider.turn.start.failed", summary: "Provider turn start failed", detail: `User message '${event.payload.messageId}' was not found for turn start request.`, turnId: null, createdAt: event.payload.createdAt, }); return; } yield* sendTurnForThread({ threadId: event.payload.threadId, messageText: message.text, ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}), createdAt: event.payload.createdAt, }); }); const processTurnInterruptRequested = Effect.fnUntraced(function* ( event: Extract<ProviderIntentEvent, { type: "thread.turn-interrupt-requested" }>, ) { const thread = yield* resolveThread(event.payload.threadId); if (!thread) { return; } const sessionId = thread.session?.providerSessionId; if (!sessionId) { return yield* appendProviderFailureActivity({ threadId: event.payload.threadId, kind: "provider.turn.interrupt.failed", summary: "Provider turn interrupt failed", detail: "No active provider session is bound to this thread.", turnId: event.payload.turnId ?? null, createdAt: event.payload.createdAt, }); } // Orchestration turn ids are not provider turn ids, so interrupt by session. yield* providerService.interruptTurn({ sessionId }); }); const processApprovalResponseRequested = Effect.fnUntraced(function* ( event: Extract<ProviderIntentEvent, { type: "thread.approval-response-requested" }>, ) { const thread = yield* resolveThread(event.payload.threadId); if (!thread) { return; } const sessionId = thread.session?.providerSessionId; if (!sessionId) { return yield* appendProviderFailureActivity({ threadId: event.payload.threadId, kind: "provider.approval.respond.failed", summary: "Provider approval response failed", detail: "No active provider session is bound to this thread.", turnId: null, createdAt: event.payload.createdAt, }); } yield* providerService.respondToRequest({ sessionId, requestId: event.payload.requestId, decision: event.payload.decision, }); }); const processSessionStopRequested = Effect.fnUntraced(function* ( event: Extract<ProviderIntentEvent, { type: "thread.session-stop-requested" }>, ) { const thread = yield* resolveThread(event.payload.threadId); if (!thread) { return; } const now = event.payload.createdAt; const sessionId = thread.session?.providerSessionId; if (sessionId) { yield* providerService.stopSession({ sessionId }); } yield* setThreadSession({ threadId: thread.id, session: { threadId: thread.id, status: "stopped", providerName: thread.session?.providerName ?? null, providerSessionId: null, providerThreadId: null, activeTurnId: null, lastError: thread.session?.lastError ?? null, updatedAt: now, }, createdAt: now, }); }); const processDomainEvent = (event: ProviderIntentEvent) => Effect.gen(function* () { switch (event.type) { case "thread.turn-start-requested": yield* processTurnStartRequested(event); return; case "thread.turn-interrupt-requested": yield* processTurnInterruptRequested(event); return; case "thread.approval-response-requested": yield* processApprovalResponseRequested(event); return; case "thread.session-stop-requested": yield* processSessionStopRequested(event); return; } }); const processDomainEventSafely = (event: ProviderIntentEvent) => processDomainEvent(event).pipe( Effect.catchCause((cause) => { if (Cause.hasInterruptsOnly(cause)) { return Effect.failCause(cause); } return Effect.logWarning("provider command reactor failed to process event", { eventType: event.type, cause: Cause.pretty(cause), }); }), ); const start: ProviderCommandReactorShape["start"] = Effect.gen(function* () { const queue = yield* Queue.unbounded<ProviderIntentEvent>(); yield* Effect.addFinalizer(() => Queue.shutdown(queue).pipe(Effect.asVoid)); yield* Effect.forkScoped( Effect.forever(Queue.take(queue).pipe(Effect.flatMap(processDomainEventSafely))), ); yield* Effect.forkScoped( Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => { if ( event.type !== "thread.turn-start-requested" && event.type !== "thread.turn-interrupt-requested" && event.type !== "thread.approval-response-requested" && event.type !== "thread.session-stop-requested" ) { return Effect.void; } return Queue.offer(queue, event).pipe(Effect.asVoid); }), ); }); return { start, } satisfies ProviderCommandReactorShape;}); export const ProviderCommandReactorLive = Layer.effect(ProviderCommandReactor, make);
import {
type ChatAttachment,
CommandId,
EventId,
type OrchestrationEvent,
type ProviderKind,
type OrchestrationSession,
type ThreadId,
type TurnId,
} from "@t3tools/contracts";
import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect";
import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
import { ProviderService } from "../../provider/Services/ProviderService.ts";
import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
import {
ProviderCommandReactor,
type ProviderCommandReactorShape,
} from "../Services/ProviderCommandReactor.ts";
type ProviderIntentEvent = Extract<
OrchestrationEvent,
{
type:
| "thread.turn-start-requested"
| "thread.turn-interrupt-requested"
| "thread.approval-response-requested"
| "thread.session-stop-requested";
}
>;
function toNonEmptyProviderInput(value: string | undefined): string | undefined {
const normalized = value?.trim();
return normalized && normalized.length > 0 ? normalized : undefined;
}
function mapProviderSessionStatusToOrchestrationStatus(
status: "connecting" | "ready" | "running" | "error" | "closed",
): OrchestrationSession["status"] {
switch (status) {
case "connecting":
return "starting";
case "running":
return "running";
case "error":
return "error";
case "closed":
return "stopped";
case "ready":
default:
return "ready";
}
}
const turnStartKeyForEvent = (event: ProviderIntentEvent): string =>
event.commandId !== null ? `command:${event.commandId}` : `event:${event.eventId}`;
const serverCommandId = (tag: string): CommandId =>
CommandId.makeUnsafe(`server:${tag}:${crypto.randomUUID()}`);
const HANDLED_TURN_START_KEY_MAX = 10_000;
const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30);
const make = Effect.gen(function* () {
const orchestrationEngine = yield* OrchestrationEngineService;
const providerService = yield* ProviderService;
const handledTurnStartKeys = yield* Cache.make<string, true>({
capacity: HANDLED_TURN_START_KEY_MAX,
timeToLive: HANDLED_TURN_START_KEY_TTL,
lookup: () => Effect.succeed(true),
});
const hasHandledTurnStartRecently = (key: string) =>
Cache.getOption(handledTurnStartKeys, key).pipe(
Effect.flatMap((cached) =>
Cache.set(handledTurnStartKeys, key, true).pipe(Effect.as(Option.isSome(cached))),
),
);
const appendProviderFailureActivity = (input: {
readonly threadId: ThreadId;
readonly kind:
| "provider.turn.start.failed"
| "provider.turn.interrupt.failed"
| "provider.approval.respond.failed"
| "provider.session.stop.failed";
readonly summary: string;
readonly detail: string;
readonly turnId: TurnId | null;
readonly createdAt: string;
}) =>
orchestrationEngine.dispatch({
type: "thread.activity.append",
commandId: serverCommandId("provider-failure-activity"),
threadId: input.threadId,
activity: {
id: EventId.makeUnsafe(crypto.randomUUID()),
tone: "error",
kind: input.kind,
summary: input.summary,
payload: {
detail: input.detail,
},
turnId: input.turnId,
createdAt: input.createdAt,
},
createdAt: input.createdAt,
});
const setThreadSession = (input: {
readonly threadId: ThreadId;
readonly session: OrchestrationSession;
readonly createdAt: string;
}) =>
orchestrationEngine.dispatch({
type: "thread.session.set",
commandId: serverCommandId("provider-session-set"),
threadId: input.threadId,
session: input.session,
createdAt: input.createdAt,
});
const resolveThread = Effect.fnUntraced(function* (threadId: ThreadId) {
const readModel = yield* orchestrationEngine.getReadModel();
return readModel.threads.find((entry) => entry.id === threadId);
});
const ensureSessionForThread = Effect.fnUntraced(function* (
threadId: ThreadId,
createdAt: string,
) {
const readModel = yield* orchestrationEngine.getReadModel();
const thread = readModel.threads.find((entry) => entry.id === threadId);
if (!thread) {
return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`));
}
const existingSessionId = thread.session?.providerSessionId;
if (existingSessionId) {
return existingSessionId;
}
const preferredProvider: ProviderKind | undefined =
thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode"
? thread.session.providerName
: undefined;
const effectiveCwd = resolveThreadWorkspaceCwd({
thread,
projects: readModel.projects,
});
const startedSession = yield* providerService.startSession(
threadId,
{
...(preferredProvider ? { provider: preferredProvider } : {}),
...(effectiveCwd ? { cwd: effectiveCwd } : {}),
...(thread.model ? { model: thread.model } : {}),
},
);
yield* setThreadSession({
threadId,
session: {
threadId,
status: mapProviderSessionStatusToOrchestrationStatus(startedSession.status),
providerName: startedSession.provider,
providerSessionId: startedSession.sessionId,
providerThreadId: startedSession.threadId ?? null,
// Provider turn ids are not orchestration turn ids.
activeTurnId: null,
lastError: startedSession.lastError ?? null,
updatedAt: startedSession.updatedAt,
},
createdAt,
});
return startedSession.sessionId;
});
const sendTurnForThread = Effect.fnUntraced(function* (input: {
readonly threadId: ThreadId;
readonly messageText: string;
readonly attachments?: ReadonlyArray<ChatAttachment>;
readonly model?: string;
readonly effort?: string;
readonly createdAt: string;
}) {
const thread = yield* resolveThread(input.threadId);
if (!thread) {
return;
}
const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt);
const normalizedInput = toNonEmptyProviderInput(input.messageText);
const normalizedAttachments = input.attachments ?? [];
yield* providerService.sendTurn({
sessionId,
...(normalizedInput ? { input: normalizedInput } : {}),
...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}),
...(input.model !== undefined ? { model: input.model } : {}),
...(input.effort !== undefined ? { effort: input.effort } : {}),
});
});
const processTurnStartRequested = Effect.fnUntraced(function* (
event: Extract<ProviderIntentEvent, { type: "thread.turn-start-requested" }>,
) {
const key = turnStartKeyForEvent(event);
if (yield* hasHandledTurnStartRecently(key)) {
return;
}
const thread = yield* resolveThread(event.payload.threadId);
if (!thread) {
return;
}
const message = thread.messages.find((entry) => entry.id === event.payload.messageId);
if (!message || message.role !== "user") {
yield* appendProviderFailureActivity({
threadId: event.payload.threadId,
kind: "provider.turn.start.failed",
summary: "Provider turn start failed",
detail: `User message '${event.payload.messageId}' was not found for turn start request.`,
turnId: null,
createdAt: event.payload.createdAt,
});
return;
}
yield* sendTurnForThread({
threadId: event.payload.threadId,
messageText: message.text,
...(message.attachments !== undefined ? { attachments: message.attachments } : {}),
...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}),
createdAt: event.payload.createdAt,
});
});
const processTurnInterruptRequested = Effect.fnUntraced(function* (
event: Extract<ProviderIntentEvent, { type: "thread.turn-interrupt-requested" }>,
) {
const thread = yield* resolveThread(event.payload.threadId);
if (!thread) {
return;
}
const sessionId = thread.session?.providerSessionId;
if (!sessionId) {
return yield* appendProviderFailureActivity({
threadId: event.payload.threadId,
kind: "provider.turn.interrupt.failed",
summary: "Provider turn interrupt failed",
detail: "No active provider session is bound to this thread.",
turnId: event.payload.turnId ?? null,
createdAt: event.payload.createdAt,
});
}
// Orchestration turn ids are not provider turn ids, so interrupt by session.
yield* providerService.interruptTurn({ sessionId });
});
const processApprovalResponseRequested = Effect.fnUntraced(function* (
event: Extract<ProviderIntentEvent, { type: "thread.approval-response-requested" }>,
) {
const thread = yield* resolveThread(event.payload.threadId);
if (!thread) {
return;
}
const sessionId = thread.session?.providerSessionId;
if (!sessionId) {
return yield* appendProviderFailureActivity({
threadId: event.payload.threadId,
kind: "provider.approval.respond.failed",
summary: "Provider approval response failed",
detail: "No active provider session is bound to this thread.",
turnId: null,
createdAt: event.payload.createdAt,
});
}
yield* providerService.respondToRequest({
sessionId,
requestId: event.payload.requestId,
decision: event.payload.decision,
});
});
const processSessionStopRequested = Effect.fnUntraced(function* (
event: Extract<ProviderIntentEvent, { type: "thread.session-stop-requested" }>,
) {
const thread = yield* resolveThread(event.payload.threadId);
if (!thread) {
return;
}
const now = event.payload.createdAt;
const sessionId = thread.session?.providerSessionId;
if (sessionId) {
yield* providerService.stopSession({ sessionId });
}
yield* setThreadSession({
threadId: thread.id,
session: {
threadId: thread.id,
status: "stopped",
providerName: thread.session?.providerName ?? null,
providerSessionId: null,
providerThreadId: null,
activeTurnId: null,
lastError: thread.session?.lastError ?? null,
updatedAt: now,
},
createdAt: now,
});
});
const processDomainEvent = (event: ProviderIntentEvent) =>
Effect.gen(function* () {
switch (event.type) {
case "thread.turn-start-requested":
yield* processTurnStartRequested(event);
return;
case "thread.turn-interrupt-requested":
yield* processTurnInterruptRequested(event);
return;
case "thread.approval-response-requested":
yield* processApprovalResponseRequested(event);
return;
case "thread.session-stop-requested":
yield* processSessionStopRequested(event);
return;
}
});
const processDomainEventSafely = (event: ProviderIntentEvent) =>
processDomainEvent(event).pipe(
Effect.catchCause((cause) => {
if (Cause.hasInterruptsOnly(cause)) {
return Effect.failCause(cause);
}
return Effect.logWarning("provider command reactor failed to process event", {
eventType: event.type,
cause: Cause.pretty(cause),
});
}),
);
const start: ProviderCommandReactorShape["start"] = Effect.gen(function* () {
const queue = yield* Queue.unbounded<ProviderIntentEvent>();
yield* Effect.addFinalizer(() => Queue.shutdown(queue).pipe(Effect.asVoid));
yield* Effect.forkScoped(
Effect.forever(Queue.take(queue).pipe(Effect.flatMap(processDomainEventSafely))),
);
yield* Effect.forkScoped(
Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => {
if (
event.type !== "thread.turn-start-requested" &&
event.type !== "thread.turn-interrupt-requested" &&
event.type !== "thread.approval-response-requested" &&
event.type !== "thread.session-stop-requested"
) {
return Effect.void;
}
return Queue.offer(queue, event).pipe(Effect.asVoid);
}),
);
});
return {
start,
} satisfies ProviderCommandReactorShape;
});
export const ProviderCommandReactorLive = Layer.effect(ProviderCommandReactor, make);