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.
199 lines • 18.7 kB
TypeScript
import type { FrameworkEvent } from './events.js';
/**
* The framework-owned "await" protocol (#337 / #339): the *code side* of the
* `showChoices()` / `showMultiSelect()` + `AWAIT` macros the system prompt delegates. The
* driver runs each agent turn as a black box to completion (#165), so the only way
* the framework can learn the agent stopped to ask (rather than deciding for itself)
* is a signal in the turn's final message. This appends one to the system prompt: it
* does not restate the macros, it pins *how* to emit an awaited choice so the
* turn-boundary gate can detect it. Kept minimal and self-contained so it survives the
* #326 wording still being written. The text lives in `prompts/protocols/await.md` (#551).
*/
export declare const AWAIT_PROTOCOL = "## Awaiting a choice\nWhen these instructions tell you to showChoices() / showMultiSelect() / showMarkdown() and then AWAIT, do not decide for the user.\nEnd your turn with one fenced code block, then stop.\nTag it `await-choices`:\n```await-choices\n{ \"title\": \"<the question>\", \"options\": [{ \"label\": \"<option>\", \"detail\": \"<optional one-liner>\" }], \"recommended\": \"<the label to default to>\" }\n```\nEvery question you stop to ask is this one block, whatever it is about:\n- An approval is two options: `{ \"title\": \"Ship this?\", \"options\": [{ \"label\": \"Approve\" }, { \"label\": \"Decline\", \"stop\": true }], \"recommended\": \"Approve\" }`.\n- A plan or document you wrote and want signed off adds `\"file\": \"PLAN_<slug>.agent.md\"`, and the framework shows that file beside the question.\n- Several answers at once (showMultiSelect) adds `\"multi\": true`, and `\"default\": true` on the entries that start checked.\n- `recommended` is what the framework picks when nobody is there to answer, so name the option that is safe to take unattended \u2014 never an option marked `stop`.\n- `\"stop\": true` marks an answer that ends the session instead of resuming you: the user is taking over and will come back with fresh instructions. Mark the option that rejects your work \u2014 declining a plan, saying no to the approach \u2014 and leave it off everything else. You are not re-prompted with that answer, so do not plan around being told it.\n\nThe framework shows it, waits for the user, and re-prompts you with their answer. Do not continue past it on your own.\n\n## Handing the browser to a human\nWhen you are working in a browser and hit something you cannot or should not get past yourself \u2014 a login wall, a captcha, an SSO or 2FA step \u2014 stop and hand it over. Never type a password, never attempt a captcha, and never use a credential you found lying around in the repo or the environment. Ask with the same `await-choices` block, naming the page and recommending the option that is true when nobody is there:\n```await-choices\n{ \"title\": \"<what the human needs to do> (<the page you are stuck on>)\", \"options\": [{ \"label\": \"Handled it\" }, { \"label\": \"Could not handle it\" }], \"recommended\": \"Could not handle it\" }\n```\nThe user acts in that browser, then you are re-prompted. If their answer says it was not handled, do not retry the same page \u2014 say what you could not reach and work on what you can, or stop.\n\n## Showing a document without waiting\nTo display markdown in the side panel without blocking (a plan, a summary, a writeup) and keep working, put a `show-markdown` block anywhere in your turn. The first line is its title:\n```show-markdown\n# <title>\n<the markdown body>\n```\nThis just shows it; you do not stop. Re-emit the same title to update that view in place.";
/**
* Told to a hands-off agent only (#1234): the await gates {@link AWAIT_PROTOCOL} just taught are
* not available in this session, so an ambiguous prompt takes its most plausible reading instead
* of parking forever on a question nobody attached can answer. Worded as availability rather
* than as a rule, so it deletes itself cleanly once choices become a per-session capability.
* The text lives in `prompts/protocols/hands_off.md`.
*/
export declare const HANDS_OFF_PROTOCOL = "## This session runs detached \u2014 decide alone, land everything\nIt was handed to a remote service: nothing that can answer a gate is attached to it, and no\nmachine sees its workspace. Two consequences:\n- Where these instructions say to showChoices() / showMultiSelect() / showMarkdown() and then\n AWAIT: do not emit the await block and do not stop \u2014 take the most plausible interpretation,\n the option you would have marked `recommended`, state in one line which assumption you made,\n and carry the work through to the end. The non-blocking blocks (show-markdown,\n set-session-name, ready-for-merge) are unaffected.\n- Before ending: commit your work on your session branch and open a pull request for it. If the\n deliverable is analysis, a plan, or a decision, write it into committed files \u2014 a result that\n lives only in this conversation, or in a gitignored file, reaches nobody. End without a pull\n request only when the task genuinely required no repository change, and say so explicitly in\n your final message.";
/**
* Told to the agent only when the agent has a browser (#824): that it has one, and that anything
* it needs to see or act on goes through the chrome-devtools tools rather than `WebFetch`.
* Lives in `prompts/protocols/browser.md`.
*/
export declare const BROWSER_PROTOCOL = "## You have a real browser\n\nThis run has a real Chrome attached, through the `chrome-devtools` tools (`new_page`, `navigate_page`, `click`, `fill`, `take_snapshot`, `evaluate_script`). It is the same browser a human can watch and take over, so use it for anything you need to *see* or *act on*: pages that render their content with JavaScript, a flow you have to click through, a form to fill, an app you are checking actually works.\n\nWhen you only need to read a page, `WebFetch` is still the better tool: it is faster and hands you the text directly. Use the browser when `WebFetch` would come back with nothing useful, such as a page that is blank until its JavaScript runs.\n\nPrefer navigating within a single page rather than opening new pages, so the user can more easily watch you navigate.";
/**
* The session-lifecycle protocol (#326): the code side of the `setSessionName()` and
* `setReadyForMerge()` actions the system prompt calls out. Like {@link AWAIT_PROTOCOL},
* it does not restate *when* to act — the system prompt owns that — it only pins *how* to
* emit the signal so the turn-boundary can detect it. Both are non-blocking: the agent
* emits the block and keeps going (the framework records it and reflects it in the
* dashboard). Injected alongside AWAIT_PROTOCOL. The text lives in
* `prompts/protocols/signal.md` (#551).
*/
export declare const SIGNAL_PROTOCOL = "## Session name\nWhen you call setSessionName(<name>) (after creating and checking out the `tf-<name>` branch), also emit a `set-session-name` block naming it, so the dashboard shows which session this is. The first non-empty line is the name (a `[a-z0-9-]` slug):\n```set-session-name\n<name>\n```\nYou do not stop; re-emit it if you rename the session.\n\n## Ready for merge\nWhen you call setReadyForMerge() \u2014 you believe the work is complete and ready for human review \u2014 emit an empty `ready-for-merge` block. This flips the dashboard status from building to ready; it does not stop your turn.\n```ready-for-merge\n```\n\n## Opening a pull request\nWhenever you emit `ready-for-merge`, emit an `open-pr` block too, naming and describing the work. The Framework opens the pull request for you \u2014 you do not need to run `gh pr create` yourself. Write it like a commit message: the first line is the title, the rest is the body.\n```open-pr\n<one line naming what the change does, under 100 characters>\n\n<what changed, and why \u2014 markdown, as long as it needs to be>\n```\nWithout it the pull request has no name for your work and can only repeat the prompt you were given, which does not say what the work turned out to be. The Framework supplies the rest: the ticket's issue reference where there is one, and recording the number so every surface shows the same pull request. You do not stop, and you can re-emit it as the work changes \u2014 the last one is used. Opening the pull request yourself instead still works; you then own all of the above.\n\n## Reporting an error\nWhen you hit something only the user can fix \u2014 a missing file you were told to read, a command that will not run, a login you do not have \u2014 emit an `error` block saying what is wrong, then carry on or stop as the task requires. The first line is the headline; anything below it is the detail.\n```error\n<what is wrong, in one line>\n\n<the detail: what you ran, what it said>\n```\nThe Framework marks it in the session log and counts it on the session, so the user sees it without reading the whole log. It does not stop your turn and it does not ask the user anything \u2014 use `AWAIT` for a question. Report the same thing once: a re-emitted identical block is ignored.";
/** One option of an await gate: what the user picks between. */
export interface AwaitOption {
/** Stable id the pick is posted back against; synthesized from position when the agent names none. */
id: string;
/** The option as shown to the user. */
label: string;
/** An optional one-liner under the label. */
detail?: string;
/** Starts checked. Only meaningful on a {@link ParsedAwaitGate.multi} gate. */
default?: boolean;
/**
* Picking this ends the session rather than resuming the agent with it (#358).
*
* Some answers are not instructions to carry on, they are "stop, I will take it from here" —
* declining a plan being the one that matters, because the user's next move is fresh
* instructions and building on a plan they rejected is the single worst thing to do with the
* interval. Which answers those are is a property of the question, so the agent marks them,
* rather than the framework inferring it from a gate kind that no longer exists.
*/
stop?: boolean;
}
/**
* A question the agent stopped to ask, parsed from an `await-choices` block (#337).
*
* There were four of these — a single choice, a multi-select, a plan approval, and handing over a
* browser — each with its own tag, parser, resolution branch and dashboard card, for what is one
* question with N options every time. Approve/Decline is two options; "handled it / could not" is
* two options; a plan approval is that pair with a file attached. Collapsing them means the agent
* learns one block instead of four, and a new kind of question needs no new code at all.
*/
export interface ParsedAwaitGate {
/** The question shown above the options. */
title: string;
/** The options to pick between (at least one, or the gate does not parse). */
options: AwaitOption[];
/** The option to default to, which autopilot accepts, when the agent named one. */
recommended?: string;
/** Any number of options may be picked rather than exactly one, each starting checked per its `default`. */
multi?: boolean;
/** A markdown file the question is about (a plan under approval); the doc sidebar renders it. */
file?: string;
}
/**
* How many times the agent may stop to ask, and be resumed, before an agent stops honoring
* gates and just finishes. A property of the await protocol, so every path that runs gates
* shares it: a build, a direct prompt, and the backlog loop each used to declare their own.
*/
export declare const MAX_AWAIT_ROUNDS = 5;
/**
* The prompt that resumes the agent after the user answers a gate. One wording for
* every path that runs gates (a direct prompt, the backlog loop, a build): the agent
* already knows what it is working on from the session, so the clause that used to
* vary per caller ("Continue" / "Continue the backlog entry" / "Continue building X")
* carried no distinct meaning to it. One constant so a reword lands everywhere at once
* instead of one path and not the others (#570).
*
* No "do not ask again" tail: a capable agent does not re-ask a settled question on
* its own, so spelling it out is babysitting we leave off until an agent shows it is
* needed (#570 review).
*/
export declare function continuationPrompt(question: string, answer: string): string;
/**
* The log line for the other outcome: the user picked a `stop` option, so there is no
* continuation prompt and the session ends here (#358). Addressed to the user rather than to the
* agent — the agent is not told anything, which is the point — and it names the answer, because
* "stopped" on its own reads like a failure when it was a decision.
*/
export declare function stopMessage(answer: string): string;
/** A non-blocking markdown view the agent pushed via a `show-markdown` block (#441). */
export interface ParsedMarkdownView {
/** Stable id (a slug of the title), so re-showing the same title updates in place. */
id: string;
/** The view's title (the first `# ` heading, or a fallback). */
title: string;
/** The markdown body (the heading line removed). */
markdown: string;
}
/**
* Parse every `show-markdown` block (per {@link AWAIT_PROTOCOL}) out of a turn's text
* (#441) — a non-blocking view the agent pushed to the side panel, so a turn may carry
* several and does not stop. Each block's first `# ` line is the title (the rest is the
* body); a block with no heading falls back to "Note". Blank blocks are skipped, and two
* blocks that slug to the same id keep the later one (an in-turn update). Never throws.
*/
export declare function parseMarkdownViews(text: string): ParsedMarkdownView[];
/**
* Parse the session name the agent set this turn (#326), from the last `set-session-name`
* block (per {@link SIGNAL_PROTOCOL}) — its first non-empty line, slugified to `[a-z0-9-]`
* so it matches the branch-name shape. Returns `undefined` when the agent did not set one
* (the common case) or the block is blank. A later block in the same turn wins (a rename).
*/
export declare function parseSessionName(text: string): string | undefined;
/** What the agent asked the framework to open a pull request with (#1567/#1618). */
export interface ParsedPullRequest {
/** The agent's name for the work: the block's first line, when it reads as a title. */
title?: string;
/** What changed and why: everything after that line, or the whole block when there is no title. */
description?: string;
}
/**
* Parse the pull request the agent asked for this turn (#1567), from the last non-empty
* `open-pr` block (per {@link SIGNAL_PROTOCOL}). Returns `undefined` when the agent wrote none,
* which simply leaves the handoff describing the work itself. A later block in the same turn
* wins, so an agent may revise it as the work changes.
*
* The block is how an agent opens a pull request *through the framework* rather than by
* reaching for `gh` itself: the title and the description are the agent's, and the handoff keeps
* the parts that have to be consistent — the ticket's issue reference and recording the number
* on the agent.
*
* Shaped like a commit message, and read like one: the first line names the work, the rest
* describes it. A first line too long to be a name is not treated as one (#1618) — the block is
* all description then, and the title falls back to the session's name rather than being cut
* mid-sentence, which is how a raw prompt ended up as a permanent commit subject.
*/
export declare function parsePullRequest(text: string): ParsedPullRequest | undefined;
/** An error the agent reported this turn (#1500), split the way the block is written. */
export interface ParsedError {
/** What is wrong, in one line: the block's first line. */
headline: string;
/** What it ran and what that said: everything below the headline, when the agent wrote any. */
detail?: string;
}
/**
* Parse the errors the agent reported this turn (#1500), from every non-empty `error` block
* (per {@link SIGNAL_PROTOCOL}), in the order they were written.
*
* Unlike the other signals, every block is kept rather than only the last: two different things
* going wrong in one turn are two errors, and collapsing them would lose one. Blocks that are
* empty are skipped — an error with nothing to say is not an error.
*/
export declare function parseErrors(text: string): ParsedError[];
/**
* Whether the agent signalled `setReadyForMerge()` this turn (#326): the presence of a
* `ready-for-merge` block (per {@link SIGNAL_PROTOCOL}) anywhere in the text. Non-blocking
* and body-less — it just flips the agent from building to ready-for-review.
*/
export declare function parseReadyForMerge(text: string): boolean;
/**
* Parse the await gate a turn ended on (#337), from the last usable `await-choices` block in its
* text. Returns `undefined` when the agent just finished — the common case, so a normal build
* flows straight through.
*
* Tolerant by design, because a bad parse must never crash a build: ids are synthesized from
* position when the agent names none, a label-less option is dropped, a blank title falls back,
* `recommended` may be given as a label or an id, and a malformed block is ignored. A block whose
* options all fall away is not a gate — the agent carries on rather than parking on an empty question.
*/
export declare function parseAwaitGate(text: string): ParsedAwaitGate | undefined;
/**
* Emit the {@link PROTOCOLS_SIGNAL} signals an agent turn carries: markdown views, the errors it
* reported, the session name, `setReadyForMerge()`, and a pull-request description. Every turn the framework prompts goes through
* one of these, because the protocols are unconditional (see `composeAgentSystem`) — the
* agent is told it can signal on any turn, so any turn we don't parse drops the signal.
*
* The returned function holds the dedupe state for the turns it covers: `ready-for-merge`
* fires once, a session name and a pull-request description only re-emit on an actual
* change, and an error is logged once however often the agent restates it. Each caller makes one
* for its own span of turns (a build's await rounds, the whole backlog), so keep it for as
* many turns as should share that dedupe rather than making one per turn.
*/
export declare function createTurnSignalEmitter(emit: (event: FrameworkEvent) => void): (text: string) => void;
//# sourceMappingURL=turn-gate.d.ts.map