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.

112 lines 5.99 kB
import { runAwaitRounds } from './await-gate.js'; import { composeRunSystem, renderSystemPrompt } from './system-prompt.js'; import { createRunControls, emitSessionStart, endStopDetail } from './run-telemetry.js'; import { createTurnSignalEmitter } from './turn-gate.js'; import { leaveResumeNote } from './todo-loop.js'; /** * Run one prompt to completion through the driver, pausing on each await gate * (#337/#339) and re-prompting with the user's answer. Emits the same * {@link FrameworkEvent} stream a build run does (`session`, `driver`, `choice`, * `usage`, `end`), so the dashboard, the store, and the control channel (#344) * all work unchanged. */ export async function runPrompt(opts) { const events = []; const emit = (event) => { events.push(event); // Swallow a listener that throws, as runFramework does: emit runs both inside and outside the // try below (the session-start and system-prompt events fire first), so an unguarded throw would // escape the run uncaught or skip its `end` event. if (opts.onEvent) { try { opts.onEvent(event); } catch (err) { console.error('[framework] onEvent threw; ignoring:', err); } } }; // The built-in #326 prompt + any user SYSTEM.md frame the session (#301). The // await protocol is always on — honoring the prompt's gates is the whole point // of this path. const tf = { prompt: opts.prompt, params: { autopilot: opts.autopilot === true, ...(opts.eco ? { eco: opts.eco } : {}) }, }; // The "read" half of the bind mechanism (#1121/#1129): a topic run's channel lists the projects // it can bind to, read through the same injected seam the gate resolves against so no `node:fs` // reaches this path. Absent for a non-topic run. const topicProjects = opts.topic && opts.bind ? (await opts.bind.listProjects()).map(p => p.path) : undefined; const system = composeRunSystem({ antiLazyPill: opts.antiLazyPill, browser: opts.browser, handsOff: opts.driver.handsOff === true, topic: opts.topic, ...(topicProjects ? { topicProjects } : {}), transparent: opts.transparent, user: opts.systemPrompt, tf, context: opts.context }); // The template's `# User prompt` half carries the prompt (today it renders to // exactly `opts.prompt`; any framing Rom adds around the slot rides along). With // the built-in prompt off (or transparent, #625), the raw prompt is sent as-is. // Resuming a finished run (#720) also sends it raw: the `--resume`d transcript already // carries the framing, so re-wrapping it would only duplicate the system template. const resuming = typeof opts.resumeSessionId === 'string' && opts.resumeSessionId.length > 0; const firstPrompt = resuming || opts.transparent || opts.antiLazyPill === false ? opts.prompt : renderSystemPrompt(tf).user; emitSessionStart({ emit, driver: opts.driver, cwd: opts.cwd, sessionLink: opts.sessionLink, model: opts.model }); // Surface the exact system prompt the agent runs under (#343). The user prompts // ride along as `driver` `start` events, so the dashboard can show them all. emit({ kind: 'system-prompt', text: system }); // Usage accounting + the self-stops, the same wiring as a build run (#322/#529): // the run signal composes the caller's abort with the budget/consumption aborts. // (The decline controller is inert here — this path finishes a declined plan cleanly.) const { runSignal, onDriverEvent, consumptionTrip, budgetController, consumptionController, declineController } = createRunControls({ emit, signal: opts.signal, sessionLink: opts.sessionLink, budgetUsd: opts.budgetUsd, consumptionGate: opts.consumptionGate, }); const session = await opts.driver.start({ cwd: opts.cwd, system, ...(opts.model ? { model: opts.model } : {}), ...(resuming ? { resumeSessionId: opts.resumeSessionId } : {}), signal: runSignal, onEvent: onDriverEvent, }); // Non-blocking signals the agent emitted this turn: markdown views (#441) and the #326 // lifecycle signals (session name, ready-for-merge). None stop the turn. const emitTurnSignals = createTurnSignalEmitter(emit); try { // A declined plan (#358) ends the run cleanly: the user takes over with fresh instructions. const rounds = await runAwaitRounds({ session, prompt: firstPrompt, emitTurnSignals, requestChoice: opts.requestChoice, emit, signal: runSignal, ...(opts.bind ? { bind: opts.bind } : {}), ...(resuming ? { resume: true } : {}), ...(opts.messages ? { messages: opts.messages } : {}), ...(opts.stayOpenChat ? { stayOpenChat: true } : {}), ...(opts.recordMessage ? { recordMessage: opts.recordMessage } : {}), }); // The agent kept asking past the limit: finish with the latest turn rather than loop. if (rounds.exhausted) emit({ kind: 'log', message: 'Finishing the session (await limit reached).' }); emit({ kind: 'end', ok: true }); return { text: rounds.text, events }; } catch (err) { const { stopped, detail } = await endStopDetail({ err, ...(opts.signal ? { signal: opts.signal } : {}), budgetController, consumptionController, declineController, consumptionTrip, ...(opts.budgetUsd != null ? { budgetUsd: opts.budgetUsd } : {}), leaveResumeNote: () => leaveResumeNote(opts.cwd, events, emit), }); emit({ kind: 'end', ok: false, ...(stopped ? { stopped: true } : {}), detail }); throw err; } finally { await session.dispose(); } } //# sourceMappingURL=prompt-run.js.map