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
JavaScript
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