UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

170 lines 11.3 kB
import { type ChildProcess } from 'node:child_process'; import type { FrameworkEvent } from './events.js'; import type { StartAgentKind, StartAgentOptions, StartAgentResult, AddProjectResult } from './dashboard/index.js'; import type { EventsSource, RemoteAgents } from './dashboard/rpc-serve.js'; import { type PreflightResult } from './preflight.js'; import { type DriverName } from './driver-names.js'; /** * The daemon's per-project business logic (#393/#736): spawning runs into worktrees, installing * projects, and app previews, plus the spawn/terminate plumbing those need. Split from daemon.ts * so that file reads as the daemon's lifecycle (state file, ports, boot, shutdown) and this reads * as what the daemon does for a project -- the split createProjectRuntime's own doc always * claimed, finished. */ /** * Locate the CLI entry to re-invoke for a detached child, refusing to re-exec a test * file. Under `node --test` (or a direct `node foo.test.js`) `process.argv[1]` is the test * file, which re-runs the whole suite instead of the daemon/run body — and that suite calls * back here, so each spawn spawns another: a fork bomb. A real agent passes the compiled bin * (or an explicit `binPath`), so the guard only ever trips in tests. */ export declare function resolveSpawnBin(explicitBinPath: string | undefined): string; /** * Clean up after a `git worktree add` that was SIGTERMed mid-write (#997). Observed behavior: git * removes its own administrative entry on the way out but leaves the partial checkout it had * already written, so `git worktree prune` finds nothing to do and the directory stays. * * Only a timeout kill is cleaned up. Any other rejection may be git refusing a path that was * already on disk before this agent asked for it, and that is not ours to delete. */ export declare function cleanupTimedOutWorktree(repo: string, agentId: string, err: unknown): Promise<void>; /** Spawn a detached, unref'd framework child (`node <binPath> --agent <specPath>`) that outlives us. */ export declare function spawnDetached(binPath: string, specPath: string, stderrFile?: string): ChildProcess; /** Where a spawned agent's stderr lands (#1261), so a child that dies at boot leaves a trace. */ export declare function agentStderrPath(cwd: string): string; /** * Leave a `failed` marker behind a child that died before writing its own lifecycle (#1261). * * A healthy agent's first act is opening its store (`agent.json` + `events.jsonl`); a child that * exited without one never booted — a module resolution error being the observed case — and with * stdio detached the crash went nowhere, so the session page polled "Waiting for the session to * start" forever. The daemon's exit handler is the one place that knows, so it writes the minimal * meta the page needs and surfaces the child's stderr tail in the agent log. A child that wrote its * own meta is left alone: its lifecycle is its own to report. */ export declare function markFailedStart(cwd: string, agentId: string, intent: string, detail: string): Promise<boolean>; /** Whether an agent's failure detail names a transient transport error (#1281). */ export declare function isTransientAgentFailure(detail: string | undefined): boolean; /** * The detail the agent's own `end` event failed with, off its archived event log (#1281), or * undefined when the agent did not fail by its own report. Only a child-written `end` counts: a * boot death (#1261) never writes one, and retrying an agent that cannot boot would just re-crash. */ export declare function lastAgentFailureDetail(eventsJsonl: string): string | undefined; /** How many times a transiently-dead agent is continued before its failure stands (#1281). */ export declare const MAX_TRANSIENT_RETRIES = 2; export declare function delay(ms: number): Promise<void>; /** * Stop a process and wait for it to actually go: SIGTERM, then SIGKILL if the grace period lapses. * Returns whether it was alive to begin with. * * Both callers need the escalation for the same reason and had their own copy of it: a process * that ignores SIGTERM (or wedges in shutdown) must not be left holding a port or a worktree. */ export declare function terminate(pid: number, graceMs: number): Promise<boolean>; /** * Wait until the daemon is finished with the agent slots it just stopped — the child gone *and* its * teardown done — or the timeout lapses. * * Killing a pid is not letting go of the repo. The child's `exit` event lands a turn after the * process disappears, and the teardown that event starts (archive the agent, commit its work, keep * or remove its checkout) runs well past that. Without this wait, shutdown's archive commit fires * while an agent is still being archived, and it misses that agent's ending (#912/#1179). * * A slot is settled when it is in neither map: `settle` drops it from `activeAgents` and parks its * teardown in `retiring`, and the teardown clears itself on the way out. An agent with no worktree * never enters `retiring` at all, and needs no special case — it is simply already gone from both. * * Bounded, because a wedged teardown must cost the shutdown its grace period, not the exit. */ export declare function waitOutSlots(keys: readonly string[], slots: { activeAgents: Map<string, number>; retiring: Map<string, Promise<void>>; }, timeoutMs: number): Promise<void>; /** * What the previous leg of an agent says about itself, for {@link waitOutFinishedLeg}. `unknown` is * the honest third answer — the leg has no readable state *this instant* — and is deliberately * not folded into either of the other two. */ export type FinishedLegState = 'ended' | 'running' | 'unknown'; /** * Wait out the previous leg of the agent a continuation is aimed at (#1529). A Resume clicked the * instant an agent's row flips `done` can land while the child that wrote that ending is still * mid-exit: the agent's slot then still holds a live pid, and the busy guard read "already active" * off a session that is over by its own account — a spurious refusal the E2E settings story * caught on a slow runner. A finished child's exit is imminent and its retirement is queued * right behind it (see `retiring` in {@link createProjectRuntime}), so wait for both, bounded by * `graceMs`, and let the reuse read a settled archive. A leg still calling itself `running` is a * genuine collision: not waited on, so the guard's refusal stands. * * `readLegState` is asked until it commits, rather than sampled once (#1540). A leg's state is * read off a `agent.json` its own process rewrites in place, so a single read can come back * `unknown` for reasons that have nothing to do with the leg — a torn read, or the beat between * the archive being written and the worktree going. Taking one such sample for "still running" * skipped the wait entirely and handed the continuation to the busy guard mid-exit: #1529's * refusal back as a rarer race, and the flake that sent this here. Only a leg that positively * reports `running` short-circuits; `unknown` re-asks on the next tick, and the loop still ends * the moment the slot clears, so the common path costs exactly one read as before. */ export declare function waitOutFinishedLeg(key: string, slots: { starting: Set<string>; activeAgents: Map<string, number>; retiring: Map<string, Promise<void>>; }, readLegState: () => Promise<FinishedLegState>, graceMs: number): Promise<void>; /** Inputs to {@link createProjectRuntime}. */ export interface ProjectRuntimeOptions { /** The daemon's home workspace; a run/preview with no project id targets it. */ cwd: string; /** Env for the registry lookups (#393). */ env: NodeJS.ProcessEnv; /** The CLI entry to spawn runs with (#345); undefined uses `process.argv[1]`. */ binPath?: string | undefined; /** The pause before a transient-death retry (#1281); undefined uses {@link TRANSIENT_RETRY_DELAY_MS}. A test seam. */ retryDelayMs?: number | undefined; /** How a start checks the agent can run (#1326); undefined runs the real {@link preflight}. A test seam. */ driverPreflight?: ((driver: DriverName) => Promise<PreflightResult>) | undefined; } /** The per-project agent + preview surface the dashboard drives, plus its teardown. */ export interface ProjectRuntime { onStart: (prompt: string, kind: StartAgentKind, options?: StartAgentOptions, targetProjectId?: string) => Promise<StartAgentResult>; onAddProject: (path: string, directory: boolean) => Promise<AddProjectResult>; /** The live event stream for an agent this daemon is relaying from a device (#1067), else undefined * so `onEvents` falls back to tailing the on-disk log. Wired as the dashboard's events source. */ remoteEventsSource: EventsSource; /** Tail a relay-started agent's on-disk events (#1067): the daemon's `/_relay/events` endpoint uses * it to stream one agent back to whichever daemon relayed it here. */ tailRelayEvents: (agentId: string, onEvent: (event: FrameworkEvent) => void) => () => void; /** The relayed-agent lookup the dashboard's read RPCs consult (#1067 slice 2): which device a remote * run runs on, so a run-scoped RPC forwards there instead of resolving a local checkout. */ remoteAgents: RemoteAgents; /** The device side of the relay (#1067 slice 2): run one whitelisted read/steer/handoff RPC against * this daemon's own home checkout, for a daemon that relayed an agent here. */ onRelayRpc: (fn: string, args: unknown[]) => Promise<unknown>; /** Live agents on a project (#685), so a background job can tell an idle project from a busy one. */ activeAgentCount: (targetProjectId: string) => number; /** * The agent ids this daemon is still responsible for: spawning, running, or mid-retirement. * * The background worktree sweep asks, because an agent's meta flips to `done` a beat *before* its * teardown archives and reclaims the checkout — so a sweep landing in that window would race * the teardown for the same directory. "Not live" on disk is not the same as "the daemon is * finished with it", and this is the difference. */ busyAgentIds: () => ReadonlySet<string>; /** * Stop the agents this daemon spawned. Returns how many were stopped. Called on shutdown, before * the previews go. */ stopAgents: (graceMs?: number) => Promise<number>; /** Stop every live preview so their dev servers do not outlive the daemon (#475). */ dispose: () => Promise<void>; } /** * The daemon's per-project runtime (#393): the agent and preview state keyed by project id, * plus the RPCs the dashboard invokes over `POST /_rpc/<name>`. A project runs any number of * concurrent agents (each in its own worktree, #736) and one preview. The home `cwd` is the default target — a request * with no project id (or the home id) resolves to it without a registry lookup. Split out of * {@link runDaemon} so the daemon body reads as lifecycle and this reads as business logic. */ export declare function createProjectRuntime({ cwd, env, binPath, retryDelayMs, driverPreflight }: ProjectRuntimeOptions): ProjectRuntime; //# sourceMappingURL=daemon-runtime.d.ts.map