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