Chapter 10 · section 3 of 5
Built from the environment
Listing: ask/live.ts
import Anthropic from "@anthropic-ai/sdk";
import { askerFor } from "./answer.js";
import { LAWS } from "./corpus.js";
import type { Pipeline } from "./pipeline.js";
import { haikuRephraseFor, sideJobClient } from "./rephrase.js";
import { haikuRerankFor } from "./rerank.js";
import { ROWS } from "./schedule.js";
import { buildIndex, nearLawsFor, workersAiEmbedder } from "./vectors.js";
// The deployed pipeline, built from whatever the environment holds. Every
// stage that needs a credential the environment lacks is left out, and the
// pipeline answers as it did before that stage existed.
export async function livePipeline(): Promise<Pipeline> {
const embed = workersAiEmbedder();
const side = sideJobClient();
return {
ask: askerFor(new Anthropic()),
laws: LAWS,
...(embed === undefined ? {} : { near: nearLawsFor(await buildIndex(LAWS, embed), embed) }),
rephrase: haikuRephraseFor(side),
rerank: haikuRerankFor(side),
penalties: ROWS,
};
} livePipeline is the one place the stages are wired to real services, and the one ...(embed === undefined ? {} : ...) is the convention in miniature: no Cloudflare credentials, no near, and answerQuestion never looks for one. The deployed version of this function is sixteen lines in the worker’s request handler, one per dependency. Nine of them are factories that take a binding or a key and return undefined without it; the asker’s factory, without its key, returns an asker that fails every question as unavailable; the rest are plain closures and values.
Listing: ask/ask.ts
import { referenceOf } from "./answer.js";
import { livePipeline } from "./live.js";
import { answerQuestion, type Citation } from "./pipeline.js";
import { requireApiKey } from "./setup.js";
requireApiKey();
// One line naming the sources: the manual once with its pages, a code
// section once however many subsections were cited, a schedule once by its
// title, and the earliest as-of date -- only when every citation has one.
function sourcesLine(citations: Citation[]): string {
const names: string[] = [];
const pages: string[] = [];
for (const citation of citations) {
const reference = referenceOf(citation.location);
if (citation.kind === undefined) {
pages.push(reference.replace(/\s*\(printed\)$/, ""));
continue;
}
const name =
citation.kind === "statute"
? reference.replace(/(?:\s*[–-]?\s*\([^()]*\))+[\s–-]*$/, "")
: reference;
if (!names.includes(name)) names.push(name);
}
if (pages.length > 0) names.unshift(`Maryland driver's manual ${pages.join(", ")}`);
const dates = citations.map((citation) => citation.asOf);
const asOf = dates.every((date): date is string => date !== undefined) ? [...dates].sort()[0] : null;
return `Sources: ${names.join(" · ")}${asOf === null ? "" : ` · as of ${asOf}`}`;
}
async function main(): Promise<void> {
const question = process.argv.slice(2).join(" ");
if (question.length === 0) {
console.error('usage: tsx ask/ask.ts "<question>"');
process.exit(1);
}
const { body, outcome } = await answerQuestion(question, [], await livePipeline());
if (body.id === "answer") {
console.log(body.answer);
console.log(sourcesLine(body.citations));
} else {
console.log(body.pointer);
}
console.log(`\n[${outcome.model}]`);
}
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}); Run it:
npx tsx ask/ask.ts "how far from a fire hydrant can i park" Stay at least 15 feet from a fire hydrant.
Sources: Md. Code, Transp. § 21-1003 · as of 2026-10-05
[claude-opus-5-5] Then the question from Chapter 8:
npx tsx ask/ask.ts "what happens if i get pulled over and i don't have my license with me" The answer names the $50, says it can be paid without going to court, and the sources line names the section and the District Court’s schedule, with the earlier of the two dates. Then one the corpus cannot answer, can I turn right on red, which gets the not-covered line and [road-law:not-covered] in the brackets. Then, with CLOUDFLARE_API_TOKEN unset, the hydrant question again: the same answer, one stage fewer, nothing in the output to say so except that the Spanish version of the question now gets the not-covered line too, because without the index Spanish reaches nothing.
The sources line has one rule worth noticing, in its last three lines. The date shown is the earliest of the citations’ dates, and it is shown only when every citation has one. The deployed pipeline’s comment: a line that dated four rows and silently not the fifth would claim a check of the fifth that nobody made.