1251 lines · 48.9 KB · TypeScript
RawDownload
import { assert, describe, it } from "@effect/vitest";import {  CommandId,  MessageId,  type ModelSelection,  type OrchestrationV2Command,  type OrchestrationV2ProviderCapabilities,  type OrchestrationV2ProviderSession,  type OrchestrationV2ProviderThread,  ProjectId,  ProviderInstanceId,  ProviderThreadId,  ProviderTurnId,  ThreadId,  TurnItemId,  ProviderDriverKind,} from "@t3tools/contracts";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as PubSub from "effect/PubSub";import * as Ref from "effect/Ref";import * as Stream from "effect/Stream";import * as SqlClient from "effect/unstable/sql/SqlClient"; import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";import { ClaudeProviderCapabilitiesV2 } from "../Adapters/ClaudeAdapterV2.ts";import { CodexProviderCapabilitiesV2 } from "../Adapters/CodexAdapterV2.ts";import { CursorProviderCapabilitiesV2 } from "../Adapters/CursorAdapterV2.ts";import { layer as eventSinkLayer } from "../EventSink.ts";import { layer as eventStoreLayer } from "../EventStore.ts";import {  LegacyV1ThreadImporter,  layer as legacyV1ThreadImporterLayer,} from "../LegacyV1ThreadImporter.ts";import { OrchestratorV2 } from "../Orchestrator.ts";import {  ProjectionMaintenanceV2,  layer as projectionMaintenanceLayer,} from "../ProjectionMaintenance.ts";import { layer as projectionStoreLayer } from "../ProjectionStore.ts";import {  type ProviderAdapterV2Event,  ProviderAdapterProtocolError,  type ProviderAdapterV2Shape,} from "../ProviderAdapter.ts";import { makeLayer as makeProviderAdapterRegistryLayer } from "../ProviderAdapterRegistry.ts";import { makeProviderFailure } from "../ProviderFailure.ts";import {  CLAUDE_MODEL_SELECTION,  CODEX_MODEL_SELECTION,  CURSOR_MODEL_SELECTION,} from "./fixtures/shared.ts";import { makeOrchestratorV2ReplayLayerWithRegistry } from "./ProviderReplayHarness.ts";import { checkpointWorkspace } from "./ReplayFixtureWorkspace.ts"; const threadId = ThreadId.make("thread:provider-switch");const projectId = ProjectId.make("project:provider-switch");const firstPrompt = "Respond with exactly: codex before switch";const claudePrompt = "Respond with exactly: claude switched response";const returnPrompt = "Respond with exactly: codex after return";const CODEX_DRIVER = ProviderDriverKind.make("codex");const CLAUDE_DRIVER = ProviderDriverKind.make("claudeAgent");const CURSOR_DRIVER = ProviderDriverKind.make("cursor"); interface CapturedTurn {  readonly driver: ProviderDriverKind;  readonly threadId: ThreadId;  readonly providerThreadId: ProviderThreadId;  readonly text: string;} function unimplemented(driver: ProviderDriverKind, detail: string) {  return Effect.fail(new ProviderAdapterProtocolError({ driver, detail }));} function makeTestAdapter(input: {  readonly instanceId: ProviderInstanceId;  readonly driver: ProviderDriverKind;  readonly capabilities: OrchestrationV2ProviderCapabilities;  readonly modelSelection: ModelSelection;  readonly responseByRunOrdinal: Readonly<Record<number, string>>;  readonly responseByThreadId?: Readonly<Record<string, Readonly<Record<number, string>>>>;  readonly capturedTurns: Ref.Ref<ReadonlyArray<CapturedTurn>>;  readonly failResume?: boolean;  readonly failedRunOrdinals?: ReadonlySet<number>;}): ProviderAdapterV2Shape {  return {    instanceId: input.instanceId,    driver: input.driver,    getCapabilities: () => Effect.succeed(input.capabilities),    planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" }),    openSession: (sessionInput) =>      Effect.gen(function* () {        const events = yield* PubSub.unbounded<ProviderAdapterV2Event>();        const now = yield* DateTime.now;        const providerSession: OrchestrationV2ProviderSession = {          id: sessionInput.providerSessionId,          driver: input.driver,          providerInstanceId: input.instanceId,          status: "ready",          cwd: sessionInput.runtimePolicy.cwd ?? process.cwd(),          model: input.modelSelection.model,          capabilities: input.capabilities,          createdAt: now,          updatedAt: now,          lastError: null,        };         return {          instanceId: input.instanceId,          driver: input.driver,          providerSessionId: sessionInput.providerSessionId,          providerSession,          events: Stream.fromPubSub(events),          ensureThread: (threadInput) =>            Effect.gen(function* () {              const createdAt = yield* DateTime.now;              const nativeThreadId = `${input.driver}:${threadInput.threadId}`;              return {                id: ProviderThreadId.make(`provider-thread:${nativeThreadId}`),                driver: input.driver,                providerInstanceId: input.instanceId,                providerSessionId: sessionInput.providerSessionId,                appThreadId: threadInput.threadId,                ownerNodeId: null,                nativeThreadRef: {                  driver: input.driver,                  nativeId: nativeThreadId,                  strength: "strong",                },                nativeConversationHeadRef: null,                status: "idle",                firstRunOrdinal: null,                lastRunOrdinal: null,                handoffIds: [],                forkedFrom: null,                createdAt,                updatedAt: createdAt,              } satisfies OrchestrationV2ProviderThread;            }),          resumeThread: ({ providerThread }) =>            input.failResume              ? unimplemented(input.driver, "simulated native resume failure")              : Effect.succeed(providerThread),          startTurn: (turnInput) =>            Effect.gen(function* () {              yield* Effect.yieldNow;              yield* Ref.update(input.capturedTurns, (turns) => [                ...turns,                {                  driver: input.driver,                  threadId: turnInput.threadId,                  providerThreadId: turnInput.providerThread.id,                  text: turnInput.message.text,                },              ]);              const eventTime = yield* DateTime.now;              const providerTurnId = ProviderTurnId.make(                `provider-turn:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}`,              );              if (input.failedRunOrdinals?.has(turnInput.runOrdinal) === true) {                yield* PubSub.publish(events, {                  type: "provider_turn.updated",                  driver: input.driver,                  providerTurn: {                    id: providerTurnId,                    providerThreadId: turnInput.providerThread.id,                    nodeId: turnInput.rootNodeId,                    runAttemptId: turnInput.attemptId,                    nativeTurnRef: {                      driver: input.driver,                      nativeId: `native-turn:${turnInput.threadId}:${turnInput.runOrdinal}`,                      strength: "strong",                    },                    ordinal: turnInput.runOrdinal,                    status: "failed",                    startedAt: eventTime,                    completedAt: eventTime,                  },                });                yield* PubSub.publish(events, {                  type: "turn.terminal",                  driver: input.driver,                  providerThreadId: turnInput.providerThread.id,                  providerTurnId,                  runOrdinal: turnInput.runOrdinal,                  failureItemOrdinal: turnInput.runOrdinal * 100 + 1,                  status: "failed",                  failure: makeProviderFailure({                    message: "Simulated provider failure.",                    code: "simulated_failure",                    class: "provider_error",                  }),                  threadDisposition: "reusable",                });                return;              }              const response =                input.responseByThreadId?.[turnInput.threadId]?.[turnInput.runOrdinal] ??                input.responseByRunOrdinal[turnInput.runOrdinal] ??                `${input.driver} response for run ${turnInput.runOrdinal}`;              const providerEvents: ReadonlyArray<ProviderAdapterV2Event> = [                {                  type: "provider_turn.updated",                  driver: input.driver,                  providerTurn: {                    id: providerTurnId,                    providerThreadId: turnInput.providerThread.id,                    nodeId: turnInput.rootNodeId,                    runAttemptId: turnInput.attemptId,                    nativeTurnRef: {                      driver: input.driver,                      nativeId: `native-turn:${turnInput.threadId}:${turnInput.runOrdinal}`,                      strength: "strong",                    },                    ordinal: turnInput.runOrdinal,                    status: "completed",                    startedAt: eventTime,                    completedAt: eventTime,                  },                },                {                  type: "turn_item.updated",                  driver: input.driver,                  turnItem: {                    id: TurnItemId.make(                      `turn-item:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}:assistant`,                    ),                    threadId: turnInput.threadId,                    runId: turnInput.runId,                    nodeId: turnInput.rootNodeId,                    providerThreadId: turnInput.providerThread.id,                    providerTurnId,                    nativeItemRef: null,                    parentItemId: null,                    ordinal: turnInput.runOrdinal * 100 + 1,                    status: "completed",                    title: null,                    startedAt: eventTime,                    completedAt: eventTime,                    updatedAt: eventTime,                    type: "assistant_message",                    messageId: MessageId.make(                      `message:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}:assistant`,                    ),                    text: response,                    streaming: false,                  },                },                {                  type: "turn.terminal",                  driver: input.driver,                  providerThreadId: turnInput.providerThread.id,                  providerTurnId,                  runOrdinal: turnInput.runOrdinal,                  status: "completed",                  failure: null,                  threadDisposition: "reusable",                },              ];              for (const event of providerEvents) {                yield* PubSub.publish(events, event);              }            }),          steerTurn: () => Effect.void,          interruptTurn: () => Effect.void,          respondToRuntimeRequest: () => Effect.void,          readThreadSnapshot: () =>            unimplemented(input.driver, "readThreadSnapshot unused in provider switch test"),          rollbackThread: () =>            unimplemented(input.driver, "rollbackThread unused in provider switch test"),          forkThread: () =>            unimplemented(input.driver, "forkThread unused in provider switch test"),        };      }),  };} const waitForIdle = Effect.fn("ProviderSwitchTest.waitForIdle")(function* (  targetThreadId: ThreadId,) {  const orchestrator = yield* OrchestratorV2;  for (let attempt = 0; attempt < 1_000; attempt += 1) {    const projection = yield* orchestrator.getThreadProjection(targetThreadId);    if (      projection.runs.every(        (run) => !["queued", "starting", "running", "waiting"].includes(run.status),      )    ) {      return projection;    }    yield* Effect.sleep("5 millis");  }  return yield* Effect.die(new Error("Provider switch test timed out waiting for idle"));}); describe("orchestration v2 provider switching", () => {  it.live("reissues imported v1 context when switching after the first provider fails", () =>    Effect.scoped(      Effect.gen(function* () {        const importedThreadId = ThreadId.make("thread:provider-switch:legacy-import");        const importedProjectId = ProjectId.make("project:provider-switch:legacy-import");        const failedPrompt = "This first provider attempt should fail.";        const recoveryPrompt = "What was the imported release marker?";        const cwd = yield* checkpointWorkspace("provider-switch-legacy-import");        const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);        const registryLayer = makeProviderAdapterRegistryLayer([          makeTestAdapter({            instanceId: ProviderInstanceId.make("codex"),            driver: CODEX_DRIVER,            capabilities: CodexProviderCapabilitiesV2,            modelSelection: CODEX_MODEL_SELECTION,            responseByRunOrdinal: {},            capturedTurns,            failedRunOrdinals: new Set([1]),          }),          makeTestAdapter({            instanceId: ProviderInstanceId.make("claudeAgent"),            driver: CLAUDE_DRIVER,            capabilities: ClaudeProviderCapabilitiesV2,            modelSelection: CLAUDE_MODEL_SELECTION,            responseByRunOrdinal: { 2: "The imported release marker is violet." },            capturedTurns,          }),        ]);        const databaseLayer = SqlitePersistenceMemory;        const eventStoreProvided = eventStoreLayer.pipe(Layer.provideMerge(databaseLayer));        const projectionStoreProvided = projectionStoreLayer.pipe(          Layer.provideMerge(databaseLayer),        );        const storesProvided = Layer.mergeAll(          databaseLayer,          eventStoreProvided,          projectionStoreProvided,        );        const eventSinkProvided = eventSinkLayer.pipe(Layer.provide(storesProvided));        const importerProvided = legacyV1ThreadImporterLayer.pipe(          Layer.provide(Layer.mergeAll(storesProvided, eventSinkProvided)),        );        const maintenanceProvided = projectionMaintenanceLayer.pipe(Layer.provide(storesProvided));        const orchestratorProvided = makeOrchestratorV2ReplayLayerWithRegistry(          {            name: "provider-switch-legacy-import",            runtimePolicyOverride: {              cwd,              approvalPolicy: "never",              sandboxPolicy: {                type: "readOnly",                access: { type: "fullAccess" },                networkAccess: false,              },            },          },          registryLayer,          { databaseLayer },        );        const testLayer = Layer.mergeAll(          storesProvided,          importerProvided,          maintenanceProvided,          orchestratorProvided,        );         const projection = yield* Effect.gen(function* () {          const sql = yield* SqlClient.SqlClient;          const importer = yield* LegacyV1ThreadImporter;          const maintenance = yield* ProjectionMaintenanceV2;          const orchestrator = yield* OrchestratorV2;           yield* sql`            INSERT INTO projection_projects (              project_id,              title,              workspace_root,              default_model_selection_json,              scripts_json,              created_at,              updated_at,              deleted_at            ) VALUES (              ${importedProjectId},              'Imported provider switch project',              ${cwd},              '{"instanceId":"codex","model":"gpt-5.4"}',              '[]',              '2026-01-01T00:00:00.000Z',              '2026-01-01T00:00:00.000Z',              NULL            )          `;          yield* sql`            INSERT INTO projection_threads (              thread_id,              project_id,              title,              model_selection_json,              runtime_mode,              interaction_mode,              branch,              worktree_path,              latest_turn_id,              created_at,              updated_at,              archived_at,              settled_override,              settled_at,              deleted_at            ) VALUES (              ${importedThreadId},              ${importedProjectId},              'Imported provider switch thread',              '{"instanceId":"codex","model":"gpt-5.4"}',              'full-access',              'default',              'main',              ${cwd},              NULL,              '2026-01-01T00:00:00.000Z',              '2026-01-01T00:00:00.000Z',              NULL,              NULL,              NULL,              NULL            )          `;          yield* sql`            INSERT INTO projection_thread_messages (              message_id,              thread_id,              turn_id,              role,              text,              attachments_json,              is_streaming,              created_at,              updated_at            ) VALUES              (                'message:provider-switch:legacy-import:user',                ${importedThreadId},                NULL,                'user',                'Remember that the imported release marker is violet.',                '[]',                0,                '2026-01-01T01:00:00.000Z',                '2026-01-01T01:00:00.000Z'              ),              (                'message:provider-switch:legacy-import:assistant',                ${importedThreadId},                NULL,                'assistant',                'I will remember violet.',                '[]',                0,                '2026-01-01T01:01:00.000Z',                '2026-01-01T01:01:00.000Z'              )          `;           yield* importer.reconcileShells;          yield* maintenance.rebuild;          yield* importer.ensureTranscript(importedThreadId);           yield* orchestrator.dispatch({            type: "message.dispatch",            createdBy: "user",            creationSource: "web",            commandId: CommandId.make("command:provider-switch:legacy-import:failed"),            threadId: importedThreadId,            messageId: MessageId.make("message:provider-switch:legacy-import:failed"),            text: failedPrompt,            attachments: [],            modelSelection: CODEX_MODEL_SELECTION,            dispatchMode: { type: "start_immediately" },          });          yield* waitForIdle(importedThreadId);          yield* orchestrator.dispatch({            type: "message.dispatch",            createdBy: "user",            creationSource: "web",            commandId: CommandId.make("command:provider-switch:legacy-import:recovery"),            threadId: importedThreadId,            messageId: MessageId.make("message:provider-switch:legacy-import:recovery"),            text: recoveryPrompt,            attachments: [],            modelSelection: CLAUDE_MODEL_SELECTION,            dispatchMode: { type: "start_immediately" },          });          return yield* waitForIdle(importedThreadId);        }).pipe(Effect.provide(testLayer));         const turns = yield* Ref.get(capturedTurns);        assert.deepEqual(          projection.runs.map((run) => [run.providerInstanceId, run.status]),          [            ["codex", "failed"],            ["claudeAgent", "completed"],

Lines 1–500 of 1251.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t