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.

448 lines 18.7 kB
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