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.

97 lines 5.27 kB
import { hasSessionIdPlaceholder, resolveSessionLink } from './session-link.js'; import { UsageMeter } from './usage.js'; /** * Emit the agent's opening `session` event. A literal link is shown right away; a * templated one (`.../{sessionId}`) can only resolve once the driver reports its * session id, so it waits for the `session-update` from {@link createDriverEventHandler}. */ export function emitSessionStart(opts) { const literal = opts.sessionLink && !hasSessionIdPlaceholder(opts.sessionLink) ? opts.sessionLink : undefined; opts.emit({ kind: 'session', driver: opts.driver.id, workspace: opts.cwd, fake: opts.driver.id === 'fake', ...(literal ? { sessionLink: literal } : {}), ...(opts.model ? { model: opts.model } : {}), }); } /** * Watch the driver's black box (#165) and turn it into the agent's stream: surface the real session * id as `session-update` once known (that is the honest handle a UI links to, and it changes per * prompt, so re-emit), and fold each turn's usage into the agent total. * * It used to trip two self-stops here as well — a per-agent USD cap and a mid-run quota gate — each * firing *after* the turn that crossed it, when its cost was already spent (E1). */ export function createDriverEventHandler(opts) { const { emit } = opts; let lastSessionId; const usage = new UsageMeter(); const onDriverEvent = (event) => { // The turn-start id announcement (#1322) is consumed here, not forwarded: it exists so the // `session-update` below lands before a Stop or a crash can lose the turn, and a transcript // row repeating an id the very next event also carries would only be noise. if (event.type === 'session') { if (event.sessionId !== lastSessionId) { lastSessionId = event.sessionId; const link = opts.sessionLink ? resolveSessionLink(opts.sessionLink, event.sessionId) : undefined; emit({ kind: 'session-update', sessionId: event.sessionId, ...(link ? { sessionLink: link } : {}) }); } return; } emit({ kind: 'driver', event }); if (event.type !== 'result') return; // The hand-off anchor (#1601) reaches the meta the same way the session id does: it is a // fact about the run the daemon needs after this process is gone, so only an event carries it. if (event.anchorSha) emit({ kind: 'cloud-anchor', sha: event.anchorSha }); if (event.sessionId && event.sessionId !== lastSessionId) { lastSessionId = event.sessionId; // A driver that knows its session's real URL (#1317, the cloud hand-off) beats the // session-link template, whose Claude default is the generic entry point. const link = event.sessionLink ?? (opts.sessionLink ? resolveSessionLink(opts.sessionLink, event.sessionId) : undefined); emit({ kind: 'session-update', sessionId: event.sessionId, ...(link ? { sessionLink: link } : {}) }); } if (!event.usage) return; usage.add(event.usage); const totals = usage.totals(); emit({ kind: 'usage', ...totals }); }; return { onDriverEvent }; } /** * Compose the agent's signal and wire its driver-event handler in one place. The caller's signal is * OR'd (via {@link AbortSignal.any}) with the one self-stop left — an answer that says to stop * (#358) — so anything downstream that watches `agentSignal` stops the same way regardless of which * fired. * * There were three (E1). A per-agent USD cap and a mid-run quota gate also aborted a session that * was already going, which is the worst moment to economise: the tokens are already spent, the * work is half-done, and what is saved is the cheap part while what is lost is the expensive part. * Spending is decided once, before a session starts. * * What survives is the one a *person* asked for. It used to be reached through the gate's kind — * a decline of an `await-confirmation` — and now through the option the agent marked, which is * the same stop with the plan-approval special case taken out of it (D6). */ export function createAgentControls(opts) { const answerController = new AbortController(); const agentSignal = AbortSignal.any([...(opts.signal ? [opts.signal] : []), answerController.signal]); const handler = createDriverEventHandler({ emit: opts.emit, sessionLink: opts.sessionLink }); return { ...handler, agentSignal, answerController }; } /** * Classify why an agent's turn loop threw and render the `end` event's `detail`. A caller interrupt * or an answer that said to stop (#358) are clean stops; anything else is a real failure. Shared * so the two agent paths can never disagree on what "stopped" means. */ export function endStopDetail(opts) { const callerAborted = opts.signal?.aborted === true; const answered = opts.answerController.signal.aborted; const detail = answered ? 'stopped by your answer' : opts.err instanceof Error ? opts.err.message : String(opts.err); return { stopped: callerAborted || answered, detail }; } //# sourceMappingURL=agent-telemetry.js.map