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.
448 lines • 18.7 kB
TypeScript
import type { DriverEvent } from './driver/index.js';
/** One selectable option in an interactive {@link ChoiceRequest} (#304). */
export interface ChoiceOption {
/** Stable id posted back when this option is picked. */
id: string;
/** The option shown to the user. */
label: string;
/** Optional one-line detail under the label (e.g. why an alternative lost). */
detail?: string;
/** In a multi-select ({@link ChoiceRequest.multi}), whether this option starts checked. Ignored for single-select. */
default?: boolean;
}
/**
* An interactive choice the agent pauses on until a pick arrives (#304). Emitted as
* a `choice` {@link FrameworkEvent}; the dashboard renders it in a panel and posts
* the pick back. The recommended option is the default the autopilot auto-accepts.
*/
export interface ChoiceRequest {
/** Unique id for this pending choice; the pick is posted back against it. */
id: string;
/** The question shown above the options (e.g. "Approve this plan?"). */
title: string;
/** The options to choose between (at least one). */
options: readonly ChoiceOption[];
/**
* The option id pre-selected as the default (autopilot auto-accepts it). Required
* for a single-select; omitted for a {@link multi} select, where each option's own
* {@link ChoiceOption.default} drives the pre-checked set instead.
*/
recommended?: string;
/**
* Render as a multi-select checklist (#332): each option is a checkbox pre-checked
* per its {@link ChoiceOption.default}, and the pick resolves to the selected
* *subset* of ids rather than one. Absent = the single-select gate (#304).
*/
multi?: boolean;
/** Auto-accept the recommended option after this many ms when autopilot is on. Default 10000. */
autoAcceptMs?: number;
/** The markdown file under approval (e.g. `PLAN_<slug>.agent.md`); the doc sidebar renders it. */
file?: string;
}
/**
* Why the post-merge (#326) cleanup step declined to run (#835). Every decline carries one,
* so "I turned it on and nothing happened" has an answer in the log.
*/
export type OnBeforeMergeableSkip =
/** The agent never signalled `setReadyForMerge()`, so there is nothing to clean up after. */
'not-ready-for-merge'
/** The agent was stopped (Stop button, Ctrl+C, budget cap) rather than finished. */
| 'run-stopped'
/** A fake/offline run: no agent to hand the follow-up prompt to. */
| 'fake-run'
/** The agent never called `setSessionName()`, which every line of the prompt names. */
| 'no-session-name'
/** `process.argv[1]` was empty, so there is no binary to spawn the follow-up with. */
| 'no-bin-path';
/**
* Why the end-of-session handoff (#1102) did nothing. Every one of these is a normal end rather
* than a fault, and each is reported so that "it was ticked and nothing happened" has an answer.
*
* Lives here beside {@link OnBeforeMergeableSkip} rather than with the handoff logic, because the
* event union is a leaf: the module that decides these imports the type, not the other way round.
*/
export type AutoHandoffSkip =
/** Neither box was ticked, so there was nothing to do. */
'not-armed'
/** The branch no longer exists (deleted, or never created). */
| 'branch-gone'
/** The session committed nothing the base branch does not already have. */
| 'no-commits'
/**
* The session's pending work could not be committed (#1376), so publishing would hand off a
* branch missing its last edits. The work stays in the checkout; teardown retries the commit.
*/
| 'commit-failed'
/** The repo has no remote to push to. */
| 'no-remote'
/** The branch already has a PR: opening a second one is the one mistake this must not make. */
| 'already-open'
/**
* The branch's PR is merged or closed and its head is still the branch tip (#1512): everything
* the session did already reached the human, so there is nothing left to publish. Only that
* exact case — a session that kept committing after its PR merged gets a fresh PR instead.
*/
| 'already-landed'
/** The branch is already on the remote at this commit, and only the push was asked for. */
| 'already-pushed'
/** The agent was stopped (Stop button, Ctrl+C, budget cap) rather than finished. */
| 'run-stopped'
/** A fake/offline run: nothing real to publish. */
| 'fake-run';
/**
* Why an armed merge did not run (#1363). The agent's config arms the merge; the authorization is
* the agent's, not the config's (rule settled on #1390): the agent must have declared the work
* done, and the framework must not already know of work pending in this session. The global
* `TODO_AGENTS.md` queue never withholds a merge — it is decoupled from sessions.
*/
export type MergeWithheldReason =
/** The agent never called setReadyForMerge(): the work was never declared done. */
'not-ready-for-merge'
/** The session's own `TODO_<SESSION_NAME>.agent.md` still has open entries. */
| 'session-todo-open';
/**
* How the merge half of a handoff went (#1216), when the agent was armed for it. Lives here beside
* {@link AutoHandoffSkip} for the same leaf-module reason.
*
* `auto-armed` is the preferred outcome: GitHub's own auto-merge takes the PR, so it lands when
* its checks pass rather than before them. `merged` is the fallback where the repo does not allow
* auto-merge and the PR was merged directly. `watched` (#1418) is the auto path's answer where
* GitHub cannot arm the merge and the PR's checks have not passed yet: merging directly there is
* exactly the lands-before-CI hazard (#1406), so the daemon's CI watch takes the PR instead and
* merges it once its checks go green. `failed` never fails the handoff — the PR exists either
* way, a human can still merge it by hand. `withheld` means the merge never ran at all (#1363):
* it was armed but not authorized, and the PR opened as a draft for a human instead.
*/
export type AutoMergeOutcome = {
outcome: 'auto-armed' | 'merged' | 'watched';
} | {
outcome: 'withheld';
reason: MergeWithheldReason;
} | {
outcome: 'failed';
error: string;
};
/** Who resolved a {@link ChoiceRequest}: a human, the autopilot countdown, or a headless auto-accept. */
export type ChoiceBy = 'user' | 'autopilot' | 'auto';
/** What a {@link import('./agent.js').RunFrameworkOptions.requestChoice} handler resolves with. */
export interface ChoicePick {
/** The picked option id, or (for a {@link ChoiceRequest.multi} select) the selected subset of ids. */
picked: string | readonly string[];
/** Who picked it. Default `'user'`. */
by?: ChoiceBy;
}
/** Normalize a {@link ChoicePick} (single id or subset) to a list of picked ids. */
export declare function pickedIds(picked: string | readonly string[]): string[];
/**
* The single event type the whole agent streams over. It unifies three sources so
* the dashboard (and terminal) render one timeline: the session's own narration
* (the moat: checklist verdicts, deploy), the wrapped
* agent's own black-box progress, and framework-level status. We own this stream
* (guardrail #2, #165) rather than surfacing the agent's transport directly.
*/
export type FrameworkEvent =
/**
* Emitted once at start: which agent is wrapped, the workspace, and a link. `model` is the
* model id the driver was started with (#1438), recorded per leg — a continuation (#762) emits
* its own `session` event and may run a different model, so readers fold the latest rather
* than pinning the first. Absent when the agent left the agent on its own default.
*/
{
kind: 'session';
driver: string;
workspace: string;
fake: boolean;
sessionLink?: string;
model?: string;
}
/**
* Emitted once the wrapped agent reports its real session id (not known at
* start). Carries the live id and, when a link template was supplied, the
* resolved URL to jump into that session (#165). Re-emitted if the id changes
* (each Claude Code prompt is a fresh session), keeping the link current.
*/
| {
kind: 'session-update';
sessionId: string;
sessionLink?: string;
}
/**
* The full system prompt sent to the wrapped agent for this agent (#343): the
* #326 block plus any personas / skills / memory framing, exactly as passed to
* the driver's system channel. Emitted once at session start so the dashboard
* can show the normally-hidden prompt (the per-turn user prompts arrive as
* `driver` `start` events, which already carry their text). Transparency, never
* gated on.
*/
| {
kind: 'system-prompt';
text: string;
}
/** What this session was asked for, emitted once as it opens (#211). */
| {
kind: 'intent';
text: string;
}
/** The wrapped agent's own progress, forwarded verbatim (never gated on). */
| {
kind: 'driver';
event: DriverEvent;
}
/**
* The generated app is booted and serving. Emitted after a successful agent when
* a serve config is set: the app is kept running so the user can open it, and
* the dashboard shows a live preview link (torn down on Ctrl+C).
*/
| {
kind: 'preview';
url: string;
command: string;
}
/**
* The agent's browser preview is up and listening on this loopback port (#813).
*
* Only the port travels. The dashboard reaches the stream through the daemon, which proxies
* to this port, so the agent's bridge stays same-origin-invisible and unreachable from the web.
* Frames themselves never enter the log: someone will type a password into that pane.
*/
| {
kind: 'browser-stream';
port: number;
}
/**
* The agent's browser is showing this page (#1455 item 6b): emitted for the first real
* (http/https) page and again on every change of page, so the transcript can host the live
* preview at its point of use rather than only in the rail. Only the URL travels — frames
* never enter the log, same rule as `browser-stream`. Re-emitted after each `session` so the
* row survives the dashboard's last-session slice (#829); readers fold repeats of the same
* URL in place rather than stacking duplicates, like `view` re-shows.
*/
| {
kind: 'browser';
url: string;
}
/** A framework-level log line. */
| {
kind: 'log';
message: string;
}
/**
* Something went wrong that only the user can fix (#1500), reported by the agent itself
* through an `error` block rather than left in prose the reader has to notice. The headline
* is the first line, the detail is the rest.
*
* An event, not a status: it says what happened at this point in the run and stays in the log
* as history — nothing clears it, because nothing can un-happen it. The project-level errors a
* background job finds between runs are the other half (project-errors.ts): those are
* conditions that are true *now*, and clear themselves when the condition is gone.
*/
| {
kind: 'error';
headline: string;
detail?: string;
}
/**
* An ad-hoc markdown view the agent pushed to show the user (#441), e.g. a plan,
* a summary, or a diff writeup. Non-blocking (unlike a `choice`): the dashboard
* renders it as a view in the right rail. `id` is stable per title, so re-showing
* the same view updates it in place rather than stacking a duplicate.
*/
| {
kind: 'view';
id: string;
title: string;
markdown: string;
}
/**
* The agent named the session (#326): the `[a-z0-9-]` slug it chose (also its
* `tf-<name>` branch), from a `setSessionName()` signal. Non-blocking;
* the dashboard shows it as the agent's label. Re-emitted on a rename.
*/
| {
kind: 'session-name';
name: string;
}
/**
* The agent signalled `setReadyForMerge()` (#326): it believes the work is complete
* and ready for human review. Non-blocking — it flips the agent's dashboard status from
* building (orange) to ready (green); the on-before-mergeable quality prompts hang off it.
*/
| {
kind: 'ready-for-merge';
}
/**
* The pull request the agent asked for (#1567/#1618), via an `open-pr` block: how an agent
* opens a PR *through* the framework instead of running `gh pr create` itself, so the ticket's
* issue reference and recording the number still apply. The title is the agent's name for the
* work and the description is what changed; either may be absent when the agent wrote only the
* other. Non-blocking; the end-of-agent handoff uses the latest one.
*/
| {
kind: 'open-pr';
title?: string;
description?: string;
}
/**
* The #326 post-merge cleanup step settled (#835): it queued the quality follow-ups,
* queued them but did not finish cleanly, or declined with a {@link OnBeforeMergeableSkip}.
*
* An event rather than stdout because the surfaces that need it cannot read stdout: a
* dashboard-started run is spawned with `stdio: 'ignore'`. Emitted only when the option
* was on, so an agent that never asked for the step stays quiet.
*/
| {
kind: 'on-before-mergeable';
outcome: 'queued' | 'incomplete';
} | {
kind: 'on-before-mergeable';
outcome: 'skipped';
reason: OnBeforeMergeableSkip;
}
/**
* What the end-of-session handoff is armed to do (#1102), emitted at the start and again
* whenever the dashboard's checkboxes change it.
*
* This is what makes the boxes survive a reload: the control channel carries the instruction,
* but only an event reaches the agent's meta, which is the one thing a tab opened later can read.
*
* `merge` carries the auto-merge arming (#1216) so the armed line can say the most consequential
* half of the plan (#1382): without it a merge-armed agent advertised "open a draft PR" and then
* merged to main. Optional because journals written before #1382 lack it; absent reads as off,
* the conservative display. It has no checkbox and never changes mid-run, so re-emits repeat it.
*/
| {
kind: 'handoff-armed';
push: boolean;
pr: boolean;
merge?: boolean;
}
/**
* The ticket this agent was started to implement (#1117), as a repo-relative `tickets/<file>.md`.
*
* Emitted once at start, and only when the framework itself chose the ticket — today that is the
* [Drain queue] run, whose queue entry links back to the ticket it was queued from (#1164). An
* event rather than a start argument for the usual reason: only an event reaches the agent's meta,
* and the meta is what a dashboard tab opened mid-run reads. Absent means nobody knows what this
* run is implementing, which is every hand-written prompt.
*/
| {
kind: 'ticket';
path: string;
}
/**
* The pull request this session's work is on (E6), the moment one is opened for it.
*
* An event for the same reason `ticket` and `branch` are: only an event reaches the agent's meta,
* and the meta is what every later surface reads. Before this, each of them re-resolved the PR
* live from the branch — trying the recorded branch, then the session-name branch, then the
* run-id branch, and filtering the results by whether the PR predated the session — which is a
* three-way guess plus a timestamp heuristic standing in for one integer nobody had written down.
*/
| {
kind: 'pull-request';
number: number;
url: string;
}
/**
* The branch the agent's work is on (#1277), observed off the checkout rather than guessed:
* emitted at start with the branch the agent actually begins on, and again when the framework
* renames the run-id branch after the agent names the session. Folded to `AgentMeta.branch`,
* which every surface resolves first — before this event the branch was stamped only at
* teardown (#799), so any read before that guessed between three naming schemes.
*/
| {
kind: 'branch';
branch: string;
}
/**
* The hand-off anchor a cloud run pushed for its session to clone at (#1601): an empty commit
* unique to this run, so the branch the session actually works on — a `claude/*` name of the
* cloud's own choosing, never the designated run branch — is recognizable later by plain
* ancestry. Folded to `AgentMeta.cloudAnchor`, which the daemon's adoption pass matches
* against origin's `claude/*` heads once the session has pushed its work.
*/
| {
kind: 'cloud-anchor';
sha: string;
}
/**
* What the end-of-session handoff actually did (#1102): pushed and/or opened a draft PR,
* declined for a reason that is not a fault, or failed at one of the two steps.
*
* Same reason as on-before-mergeable above: a dashboard-started agent has no stdout anyone reads,
* so an outcome that is not an event is an outcome nobody learns.
*/
| {
kind: 'handoff';
outcome: 'skipped';
reason: AutoHandoffSkip;
merge?: AutoMergeOutcome;
} | {
kind: 'handoff';
outcome: 'done';
pushed: boolean;
url?: string;
number?: number;
merge?: AutoMergeOutcome;
} | {
kind: 'handoff';
outcome: 'failed';
step: 'push' | 'pr';
error: string;
}
/**
* The work has settled and the agent is parked on the user (#785): it stays open as a
* conversation (#714), so its process is still alive and it still takes messages, but
* the agent is not doing anything until you say something.
*
* Emitted each time the agent parks, and undone by the next `driver` `start` — so "is it
* working or waiting for me" is answerable from the event log rather than inferred from
* a status that only changes when the agent ends.
*/
| {
kind: 'settled';
}
/**
* Cumulative token + cost usage for the agent so far (#322). Emitted after each
* agent turn that reports usage; the dashboard renders a live spend readout and
* the agent stops itself once `costUsd` reaches the budget cap, if one is set.
*
* `costUsd` is absent when the agent reports tokens but no price (#540), which
* is also when no budget cap can fire.
*/
| {
kind: 'usage';
costUsd?: number;
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
turns: number;
}
/**
* The agent paused on an interactive choice (#304) and is awaiting a pick. The
* dashboard renders the options with the recommended default pre-selected and
* posts the pick back; a headless agent auto-accepts the recommended option.
*/
| ({
kind: 'choice';
} & ChoiceRequest)
/** A pending {@link ChoiceRequest} was resolved — the agent continues on `picked` (one id, or the selected subset). */
| {
kind: 'choice-resolved';
id: string;
picked: string | readonly string[];
by: ChoiceBy;
}
/**
* The agent finished. `ok` is false when it threw. `stopped` marks the common,
* non-error case where the user interrupted it (the dashboard Stop button /
* Ctrl+C), so a surface can show "stopped" rather than "failed".
*/
| {
kind: 'end';
ok: boolean;
stopped?: boolean;
detail?: string;
};
//# sourceMappingURL=events.d.ts.map