import { createJustBashNetworkSandboxSession } from "@ai-sdk/sandbox-just-bash"; import { plannerAgent } from "./agents"; import { githubTools, type GitHubToolsError, type Result } from "./github-tools"; import { registerCredential } from "./harnesses"; import { type FullPlanner, pauseOwnedRuns, type PlannerRuns, registerFullPlanner, type WorkflowControl, } from "./atomic/full"; import { createHumanInput } from "./atomic/human-input"; import { type PlannerWorkspace, plannerWorkspace } from "./atomic/workspace"; import type { HarnessAgent } from "@ai-sdk/harness/agent"; import type { ActiveOwnerBinding } from "../agent/active-owner"; import type { HostedRepository } from "../agent/repository"; import type { DocumentRoom } from "../agent/tools"; export type OpenError = | GitHubToolsError | { kind: "HarnessCapabilityUnsupported"; cause: unknown } | { kind: "Timeout"; cause: unknown } | { kind: "ShuttingDown"; cause: unknown }; type Sandbox = Awaited>; type PlannerAgent = typeof plannerAgent; export type PlannerChannel = { room: DocumentRoom; repository: HostedRepository; instructions: string | ((workspace?: PlannerWorkspace) => string); model?: string; /** `atomic` runs the session as a full Atomic session in the channel's workspace. */ harness?: string; }; export type PlannerSession = { stream: (prompt: string, abortSignal: AbortSignal) => ReturnType; destroy: () => Promise; /** Workflow runs this session owns; only atomic Planner sessions report them. */ runs?: () => PlannerRuns | undefined; /** Subscribes to run changes; returns the unsubscribe function. */ watchRuns?: (listener: (runs: PlannerRuns) => void) => () => void; /** Pauses every workflow run this session owns, resumably. */ pauseRuns?: () => Promise; /** Resumes the runs this session paused. */ resumeRuns?: () => Promise; }; export type PlannerSessionDependencies = { agent?: PlannerAgent; githubTools?: typeof githubTools; createSandbox?: () => Promise; registerCredential?: typeof registerCredential; timeoutMs?: number; }; export async function openPlannerSession( owner: ActiveOwnerBinding, channel: PlannerChannel, deps: PlannerSessionDependencies = {}, ): Promise> { try { if (owner.signal.aborted || !await owner.revalidate()) { return { ok: false, error: { kind: "Unavailable", cause: new Error("Planner owner unavailable") }, }; } } catch (cause) { return { ok: false, error: { kind: "Unavailable", cause } }; } let tools: Awaited>; try { tools = await (deps.githubTools ?? githubTools)(owner); } catch (cause) { return { ok: false, error: { kind: "Unavailable", cause } }; } if (!tools.ok) return tools; let sandbox: Sandbox; try { sandbox = await (deps.createSandbox ?? createJustBashNetworkSandboxSession)(); } catch (cause) { return { ok: false, error: { kind: "Unavailable", cause } }; } let unregister: (() => void) | undefined; let unregisterFull: (() => void) | undefined; let session: Awaited> | undefined; let timeout: ReturnType | undefined; try { if (owner.signal.aborted) throw new Error("Planner owner unavailable"); let sessionId = crypto.randomUUID(); unregister = (deps.registerCredential ?? registerCredential)(sessionId, owner.currentToken); let workspace = channel.harness === "atomic" ? await plannerWorkspace(channel.room.id, channel.repository) : undefined; let planner: FullPlanner | undefined; let listeners = new Set<(runs: PlannerRuns) => void>(); if (workspace) { planner = { cwd: workspace.cwd, humanInput: createHumanInput(channel.room), onRuns: runs => { for (let listener of listeners) listener(runs); }, }; unregisterFull = registerFullPlanner(sessionId, planner); } let instructions = typeof channel.instructions === "function" ? channel.instructions(workspace) : channel.instructions; let agent = deps.agent ?? plannerAgent; let opening = agent.createSession({ sessionId, sandboxSession: sandbox }); let deadline = new Promise((_, reject) => { timeout = setTimeout( () => reject(new Error("Planner session timed out")), deps.timeoutMs ?? 30_000, ); }); try { session = await Promise.race([opening, deadline]); } catch (cause) { void opening.then(late => late.destroy()).catch(() => {}); throw cause; } if (owner.signal.aborted) throw new Error("Planner owner unavailable"); let active = session; let release = unregister; let stopped: Promise | undefined; let control = (params: WorkflowControl) => { if (!planner?.control) throw new Error("The Planner has no live Atomic session to control."); return planner.control(params); }; let runControl = planner ? { runs: () => planner.runs, watchRuns: (listener: (runs: PlannerRuns) => void) => { listeners.add(listener); return () => listeners.delete(listener); }, pauseRuns: () => pauseOwnedRuns(control, planner.runs?.active ?? []), resumeRuns: async () => { for (let runId of planner.runs?.paused ?? []) await control({ action: "resume", runId }); }, } : {}; return { ok: true, value: { ...runControl, stream: (prompt, abortSignal) => agent.stream({ session: active, prompt, abortSignal, options: { ...channel, instructions, owner, githubTools: tools.value, }, }), destroy: () => stopped ??= (async () => { try { await active.destroy(); } finally { try { await sandbox.destroy(); } finally { release(); unregisterFull?.(); } } })(), }, }; } catch (cause) { try { await session?.destroy(); } catch (cleanupError) { console.error("[agent] Planner session cleanup failed:", cleanupError); } try { await sandbox.destroy(); } catch (cleanupError) { console.error("[agent] Planner sandbox cleanup failed:", cleanupError); } unregister?.(); unregisterFull?.(); let message = cause instanceof Error ? cause.message : String(cause); let kind: "Timeout" | "ShuttingDown" | "HarnessCapabilityUnsupported" | "Unavailable" = message.includes("timed out") ? "Timeout" : message.includes("shutting down") ? "ShuttingDown" : message.includes("capability") || message.includes("unsupported") ? "HarnessCapabilityUnsupported" : "Unavailable"; return { ok: false, error: { kind, cause } }; } finally { if (timeout) clearTimeout(timeout); } }