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.
310 lines • 13.4 kB
TypeScript
import type { DriverImplId } from '../driver-names.js';
/**
* The **driver** seam: the one abstraction The Framework wraps a coding-agent
* CLI behind. A driver treats the agent (Claude Code today, Codex / opencode
* later) as a **black box**: we hand it a prompt, let its *own* loop run to
* completion, then read the code it produced and gate on the outcome ourselves.
*
* The seam is deliberately the **code and the outcome**, never the agent's
* individual tool calls (guardrail from #165). We drive by prompting and verify
* by result (builds / serves / review-passes), so the wrapped agent keeps its
* subscription-based auth and its internal loop stays untouched and swappable.
*
* Decision (#166, option A): a single execution path. Everything runs *through*
* the driver; personas become prompt-framing ({@link DriverStartOptions.system}),
* and each loop pass (review / security / QA / UX) is a **fresh** {@link
* DriverSession.prompt} call, so `prompt` is the fresh-context unit.
*
* `Driver` is intentionally tiny: `start` a session, `prompt` it, read the code,
* `dispose`. It mirrors the runner seam's shape so a second agent slots in behind
* the same three methods.
*/
/** A wrapped coding-agent CLI. Boots {@link DriverSession}s bound to a workspace. */
export interface Driver {
/**
* Stable id for this concrete implementation, e.g. `"claude-code"`. Not the driver's *name*
* (`claude`), which is the user's choice: one driver has an implementation per place it can run
* (D5). {@link driverFromImpl} maps this back to the choice.
*/
readonly id: DriverImplId;
/** Boot a session bound to a workspace directory. */
start(opts: DriverStartOptions): Promise<DriverSession>;
/**
* Ask the agent where the account's subscription quota stands (#521), for the
* consumption limits in #519. Account-wide and independent of any session, so
* it hangs off the driver rather than off {@link DriverSession}.
*
* Optional: an agent that can't report it omits the method entirely, the same
* way {@link DriverRateLimit} is omitted by drivers that can't emit it.
*/
readQuota?(opts?: {
signal?: AbortSignal;
}): Promise<DriverQuota>;
}
/** How to boot a {@link DriverSession}. */
export interface DriverStartOptions {
/** Absolute path to the workspace the agent reads and edits. */
cwd: string;
/**
* Role framing prepended to every prompt in this session (option A: personas
* are prompt-framing, not a separate agent). Maps to the agent's system prompt.
*/
system?: string;
/** Model id to pass through when the wrapped agent supports selecting one. */
model?: string;
/** Abort the whole session; disposing kills the underlying process. */
signal?: AbortSignal;
/**
* Resume a prior agent session id (#720): seed the session so its very first
* prompt (with `resume`) continues that conversation instead of starting fresh.
* This is how a finished agent is revived from the dashboard — its captured session
* id is threaded here so the opening message lands with the full prior context.
* A driver that can't resume ignores it and runs fresh (the best-effort contract).
*/
resumeSessionId?: string;
/**
* Observe the agent's *own* progress as it works. Black-box granularity: we
* forward these for visibility (the dashboard) but never branch control flow
* on them. Isolated: a throwing callback must not break the agent.
*/
onEvent?: (event: DriverEvent) => void;
}
/** A booted agent session, bound to one workspace. */
export interface DriverSession {
/** Stable id (the agent's own session id when it exposes one). */
readonly id: string;
/** Absolute workspace path the agent is bound to. */
readonly cwd: string;
/**
* Send one prompt, let the agent's built-in loop run to completion, and
* resolve with its final turn. Each call is a **fresh** invocation (fresh
* context per loop pass) unless a driver documents otherwise.
*/
prompt(text: string, opts?: DriverPromptOptions): Promise<DriverTurn>;
/**
* Read a file the agent produced (the seam is the code). Optional: a driver
* whose workspace is not host-readable may omit it and rely on a runner.
*/
readCode?(path: string): Promise<string>;
/** Tear the session down (kill the process, free resources). Idempotent. */
dispose(): Promise<void>;
}
/** Per-prompt overrides. */
export interface DriverPromptOptions {
/** Extra framing for just this prompt, appended after the session `system`. */
system?: string;
/** Abort just this prompt (the in-flight invocation). */
signal?: AbortSignal;
/**
* Continue the agent's *previous* turn instead of starting fresh (#714): the
* live-chat path resumes the same session so the message lands in the ongoing
* conversation with full context. Best-effort — a driver that can't resume
* (or has no prior turn yet) runs a fresh invocation, the normal case. Honored
* by the Claude Code driver via `--resume <sessionId>`.
*/
resume?: boolean;
}
/** The outcome of one {@link DriverSession.prompt} turn. */
export interface DriverTurn {
/** The agent's final assistant text for this prompt. */
text: string;
/**
* The agent's session id for this turn, when it exposes one. The MVP
* persistence shortcut is to forward the agent's own transcript rather than
* keep our own store (#165), so this is the handle a UI links to.
*/
sessionId?: string;
/** Token + cost accounting for this turn, when the agent reports it (#322). */
usage?: DriverUsage;
}
/**
* Token and cost accounting for one turn, as reported by the wrapped agent (#322).
* Claude Code emits this on its final `result` line; drivers that cannot report
* it simply omit it. Costs are whatever the agent computed, in USD.
*
* Tokens are the part every agent reports; the price is the part only some do
* (#540). So `costUsd` is optional and the tokens are not: Codex reports counts
* and no price, and an agent that can't price a turn reports the tokens it does
* know rather than nothing at all.
*/
export interface DriverUsage {
/**
* Cost of the turn in USD, when the agent prices its own turns. Omitted when it
* doesn't — never `0`, which would read as free rather than as unknown.
*
* Note this is a notional price under a subscription: the user pays a flat fee,
* and the agent reports what the turn would have cost on metered API pricing.
* What a subscription actually spends is quota, which {@link DriverQuota}
* carries and the consumption limits (#519) gate on.
*/
costUsd?: number;
/** Non-cached input tokens. */
inputTokens: number;
/** Output tokens. */
outputTokens: number;
/** Tokens read from the prompt cache. */
cacheReadTokens: number;
/** Tokens written to the prompt cache. */
cacheCreationTokens: number;
}
/**
* Where the account's subscription quota stands, as reported by the wrapped
* agent (#517). Claude Code emits one of these per turn on its `stream-json`
* output; drivers that cannot report it simply omit it. This is the account
* limit, not this agent's spend — {@link DriverUsage} covers the latter.
*/
export interface DriverRateLimit {
/**
* Whether the account may still spend against this window: `allowed`,
* `allowed_warning`, or `rejected`. Left open rather than a union — only
* `allowed` has been observed, and a status we don't know is the signal we're
* capturing for, so it must surface rather than be dropped.
*/
status: string;
/**
* Which quota window this reports on (`five_hour`, `seven_day`,
* `seven_day_opus`, `seven_day_sonnet`, `weekly`). Left open for the same
* reason: the agent adds windows as plans change.
*/
window: string;
/** When the window resets, epoch **milliseconds** (the agent reports seconds). */
resetsAt: number;
}
/**
* One quota window and how much of it the account has burned (#521).
*
* The three concepts here are easy to confuse. {@link DriverUsage} is what *this
* run* spent. {@link DriverRateLimit} is a per-turn traffic light (are we still
* allowed to spend, and when does the window reset). This is the missing middle:
* the *proportion* of a window consumed, which is the only one of the three that
* can fill a progress bar.
*/
export interface DriverQuotaWindow {
/** The window's name exactly as the agent phrased it, e.g. `"Current session"`. */
label: string;
/**
* Normalized window, so callers can gate without matching on prose.
* `session` is Claude's 5-hour window, `week` its all-models week, and
* `week-model` a single model's week (Opus/Sonnet get their own).
*
* Note there is deliberately no `day`: Claude measures a 5-hour session and a
* week, and nothing per day (#519 was specced against a daily limit that does
* not exist).
*/
kind: 'session' | 'week' | 'week-model' | 'unknown';
/** How much of the window is gone, 0-100. */
percentUsed: number;
/**
* When the window resets, as the agent worded it (`"Jul 18 at 7am
* (Asia/Jerusalem)"`). Prose, not a timestamp: the agent prints no year, so
* parsing it to an epoch would be guesswork. {@link DriverRateLimit.resetsAt}
* carries the exact epoch for the window it reports on.
*/
resetsAtText?: string;
}
/**
* Why a quota read came back empty.
*
* The split that matters is transient vs authoritative. `fetch-failed`,
* `timeout` and `unrecognized` describe *this attempt* (the agent's own usage
* fetch can be refused upstream, with a penalty window), so a recent reading is
* still worth showing and asking again may work. The remaining reasons describe
* the account or the install and are a statement about the setup, so a retained
* reading must not outlive them.
*/
export type DriverQuotaUnavailableReason =
/** The agent's own usage fetch failed, e.g. refused upstream. Transient. */
'fetch-failed'
/** The agent did not answer in time. Transient. */
| 'timeout'
/** The agent binary isn't installed or isn't on `PATH`. */
| 'agent-not-found'
/** The account has no subscription quota to report (e.g. API-key auth). */
| 'no-subscription'
/**
* The agent answered, but not in a shape we recognize (it reworded the readout).
*
* Transient, because it describes one answer rather than the install: an
* update notice printed ahead of the JSON, or empty stdout while the CLI
* swaps itself under a long-lived daemon, both land here and both are gone by
* the next read (#960).
*/
| 'unrecognized';
/** Whether a {@link DriverQuotaUnavailableReason} describes this attempt rather than the setup. */
export declare function isTransientQuotaReason(reason: DriverQuotaUnavailableReason): boolean;
/**
* Where the account's subscription quota stands (#521), as a whole reading.
*
* Modelled as available-or-not rather than as an empty window list, so a caller
* can't mistake "we couldn't ask" for "nothing is used".
*/
export type DriverQuota = {
available: true;
windows: DriverQuotaWindow[];
} | {
available: false;
reason: DriverQuotaUnavailableReason;
};
/**
* A black-box progress event from the wrapped agent. We forward these to the
* dashboard for visibility but never gate on them: the loop gates on the code /
* outcome, not on which tool the agent reached for.
*/
export type DriverEvent =
/** A prompt was sent; the agent's loop is starting. */
{
type: 'start';
prompt: string;
}
/**
* The agent announced its session id, at the start of the turn (#1322). `result` repeats it,
* but a turn that never settles — a manual Stop, an error, a kill — used to take the id down
* with it, and with it the agent's `claude --resume` handle. Telemetry consumes this one rather
* than forwarding it: the id is plumbing, not conversation.
*/
| {
type: 'session';
sessionId: string;
}
/** An assistant text chunk streamed out. */
| {
type: 'text';
text: string;
}
/** The agent used a tool. We surface the name only, not the arguments. */
| {
type: 'action';
label: string;
}
/**
* The turn settled with this final text. `sessionLink` is the real URL of the session,
* for a driver whose session has one of its own (#1317) — the cloud hand-off — so the
* meta can link there instead of the generic entry point; drivers without one omit it.
* `anchorSha` is the hand-off anchor commit (#1601), for a driver whose session does its
* work on a branch of its own naming that this machine can only recognize later by
* ancestry; drivers whose work stays on the designated branch omit it.
*/
| {
type: 'result';
text: string;
sessionId?: string;
sessionLink?: string;
anchorSha?: string;
usage?: DriverUsage;
}
/** Where the account's subscription quota stands (#517). */
| {
type: 'rate-limit';
limit: DriverRateLimit;
}
/** The agent (or its transport) errored. */
| {
type: 'error';
message: string;
}
/** Something the driver worked around, worth telling the user about (#778). */
| {
type: 'notice';
message: string;
};
//# sourceMappingURL=types.d.ts.map