import { expect } from "@effect/vitest"; import { Effect } from "effect"; import type { McpCallResult } from "../src/surfaces/mcp"; import { scenario } from "../src/scenario"; import { Mcp, Target } from "../src/services"; type CellEvent = { readonly type?: unknown; readonly item?: { readonly type?: unknown; readonly content?: { readonly type?: unknown; readonly text?: unknown; }; readonly notification?: { readonly message?: unknown; readonly data?: unknown; }; }; }; type CellObservation = { readonly status?: unknown; readonly cellId?: unknown; readonly cursor?: unknown; readonly events?: readonly CellEvent[]; readonly result?: { readonly result?: unknown; readonly output?: readonly unknown[]; }; }; const cellObservation = (result: McpCallResult): CellObservation => { const structured = (result.raw as { readonly structuredContent?: unknown }).structuredContent; expect(structured, `cell call returned structured content: ${result.text.slice(0, 300)}`).toEqual( expect.any(Object), ); return structured as CellObservation; }; const eventTypes = (observation: CellObservation): readonly unknown[] => observation.events?.map((event) => event.type) ?? []; const eventsOf = (observation: CellObservation): readonly CellEvent[] => observation.events ?? []; const textOutput = (events: readonly CellEvent[]): readonly string[] => events.flatMap((event) => event.type === "output" && event.item?.type === "content" && event.item.content?.type === "text" && typeof event.item.content.text === "string" ? [event.item.content.text] : [], ); const notificationOutput = (events: readonly CellEvent[]): readonly string[] => events.flatMap((event) => event.type === "output" && event.item?.type === "notification" && typeof event.item.notification?.message === "string" ? [event.item.notification.message] : [], ); const requireString = (value: unknown, label: string): string => { expect(value, label).toEqual(expect.any(String)); return value as string; }; const requireNumber = (value: unknown, label: string): number => { expect(value, label).toEqual(expect.any(Number)); return value as number; }; scenario( "Codemode ยท persistent cells yield, wait, notify, and terminate through MCP", { timeout: 180_000 }, Effect.gen(function* () { const target = yield* Target; const mcp = yield* Mcp; const identity = yield* target.newIdentity(); const session = mcp.session(identity); const tools = yield* session.listTools(); expect(tools, "persistent cell tools are advertised").toEqual( expect.arrayContaining(["execute_cell", "wait_cell", "terminate_cell"]), ); const first = yield* session.call("execute_cell", { yieldAfterMs: 250, code: [ 'text("phase 1");', "await yield_control();", 'text("phase 2");', "await yieldControl();", 'notify({ message: "cell almost done", data: { phase: 3 } });', 'text("phase 3");', 'return { cell: "done" };', ].join("\n"), }); expect(first.ok, `execute_cell starts and reaches the first yield: ${first.text}`).toBe(true); const firstCell = cellObservation(first); expect(firstCell.status, "the first observation leaves the cell running").toBe("running"); const cellId = requireString(firstCell.cellId, "execute_cell returns a reusable cell id"); let cursor = requireNumber(firstCell.cursor, "execute_cell returns an event cursor"); const firstEvents: CellEvent[] = [...eventsOf(firstCell)]; for ( let attempt = 0; attempt < 2 && !firstEvents.some((event) => event.type === "yielded"); attempt++ ) { const yielded = yield* session.call("wait_cell", { cellId, after: cursor, timeoutMs: 5_000, }); expect(yielded.ok, `wait_cell observes the first yield: ${yielded.text}`).toBe(true); const yieldedCell = cellObservation(yielded); cursor = requireNumber(yieldedCell.cursor, "wait_cell advances the cursor"); firstEvents.push(...eventsOf(yieldedCell)); } expect( firstEvents.map((event) => event.type), "the first phase includes output and an explicit yield checkpoint", ).toEqual(expect.arrayContaining(["output", "yielded"])); expect(textOutput(firstEvents), "phase 1 output is visible immediately").toContain("phase 1"); const secondEvents: CellEvent[] = []; for (let attempt = 0; attempt < 4; attempt++) { const next = yield* session.call("wait_cell", { cellId, after: cursor, timeoutMs: 5_000, }); expect(next.ok, `wait_cell observes phase 2 progress: ${next.text}`).toBe(true); const nextCell = cellObservation(next); cursor = requireNumber(nextCell.cursor, "wait_cell advances the phase 2 cursor"); secondEvents.push(...eventsOf(nextCell)); if ( textOutput(secondEvents).includes("phase 2") && secondEvents.some((event) => event.type === "yielded") ) { break; } } expect(textOutput(secondEvents), "phase 2 output is delivered incrementally").toContain( "phase 2", ); expect( secondEvents.map((event) => event.type), "phase 2 includes another explicit yield checkpoint", ).toContain("yielded"); const finalEvents: CellEvent[] = []; let completedCell: CellObservation | undefined; for (let attempt = 0; attempt < 5; attempt++) { const next = yield* session.call("wait_cell", { cellId, after: cursor, timeoutMs: 5_000, }); expect(next.ok, `wait_cell observes completion progress: ${next.text}`).toBe(true); const nextCell = cellObservation(next); cursor = requireNumber(nextCell.cursor, "wait_cell advances the completion cursor"); finalEvents.push(...eventsOf(nextCell)); if (nextCell.status === "completed") { completedCell = nextCell; break; } } expect(completedCell, "wait_cell eventually observes the completed cell").toEqual( expect.any(Object), ); const observedCompletedCell = completedCell as CellObservation; expect(observedCompletedCell.status, "the final observation is completed").toBe("completed"); expect( finalEvents.map((event) => event.type), "completion includes final outputs and terminal event", ).toEqual(expect.arrayContaining(["output", "completed"])); expect( notificationOutput(finalEvents), "notifications are emitted as structured events", ).toContain("cell almost done"); expect(textOutput(finalEvents), "phase 3 output is visible before completion").toContain( "phase 3", ); expect( observedCompletedCell.result?.result, "the returned value is preserved on completion", ).toEqual({ cell: "done" }); const timerRun = yield* session.call("execute_cell", { yieldAfterMs: 10, code: [ "await new Promise((resolve) => setTimeout(resolve, 50));", 'text("timer fired");', 'return { timer: "done" };', ].join("\n"), }); expect(timerRun.ok, `execute_cell starts a timer-backed cell: ${timerRun.text}`).toBe(true); let timerCell = cellObservation(timerRun); const timerCellId = requireString(timerCell.cellId, "timer cell id is reusable"); let timerCursor = requireNumber(timerCell.cursor, "timer cell returns an event cursor"); const timerEvents: CellEvent[] = [...eventsOf(timerCell)]; for (let attempt = 0; attempt < 5 && timerCell.status !== "completed"; attempt++) { const next = yield* session.call("wait_cell", { cellId: timerCellId, after: timerCursor, timeoutMs: 5_000, }); expect(next.ok, `wait_cell observes timer-backed completion: ${next.text}`).toBe(true); timerCell = cellObservation(next); timerCursor = requireNumber(timerCell.cursor, "wait_cell advances the timer cursor"); timerEvents.push(...eventsOf(timerCell)); } expect(timerCell.status, "timer-backed cells can complete after the first observation").toBe( "completed", ); expect(textOutput(timerEvents), "timer output is visible on completion").toContain( "timer fired", ); expect(timerCell.result?.result, "timer cell return value is preserved").toEqual({ timer: "done", }); const running = yield* session.call("execute_cell", { yieldAfterMs: 250, code: [ "let i = 0;", "while (true) {", " text(`loop ${i}`);", " i += 1;", " await yield_control();", "}", ].join("\n"), }); expect(running.ok, `execute_cell starts a cooperative long-running cell: ${running.text}`).toBe( true, ); const runningCell = cellObservation(running); expect(runningCell.status, "the loop cell is running after its first yield").toBe("running"); const runningCellId = requireString(runningCell.cellId, "loop cell id is reusable"); const terminated = yield* session.call("terminate_cell", { cellId: runningCellId, }); expect(terminated.ok, `terminate_cell returns a terminal observation: ${terminated.text}`).toBe( true, ); const terminatedCell = cellObservation(terminated); expect(terminatedCell.status, "terminate_cell marks the cell terminated").toBe("terminated"); expect(eventTypes(terminatedCell), "the termination event is visible to clients").toContain( "terminated", ); }), );