lmjtfy.git / apps / lmjtfy / src / lib.rs
lib.rsannotatedlib.rssource891 lines · 39.7 KB · raw

lmjtfy, the Worker: the page, the stream that answers a question, and the gate that runs as the visitor types.

POST /ask answers with Datastar events. Each one is the whole transcript as it stands, so the browser holds no state: it morphs what it is sent.

What happens to an input is decided by rules (rules::RULES). First Jev is asked, in one request, for every fact the rules want: can it judge the input, what kind of question is it, is it several, and its answer if it is one yes-or-no question. That alone ends most queries: a refusal, or a direct answer. Only a question that needs options or a scale written, or splitting up, goes to the LLM (Workers AI), and then Jev answers what it wrote, again in one request. Jev costs a hundredth of what the LLM does.

No call is sent from here. Each goes to the archive (archive.rs), which returns the response it already holds for that exact request, or makes the call inside the day's budget and keeps it.

19use std::convert::Infallible;
20use std::time::Duration;
22use ::archive::{Ask, Called, Pot};
23use ::card::Card;
24use ask::{Judged, Kept, Outcome, Prepared, Wanted};
25use axum::Router;
26use axum::body::Body;
27use axum::extract::{Extension, Json, Path, Query, State};
28use axum::http::{HeaderMap, Response, header};
29use axum::routing::{get, post};
30use datastar::prelude::{ElementPatchMode, PatchElements, PatchSignals};
31use futures_channel::mpsc::{UnboundedSender, unbounded};
32use futures_util::StreamExt;
33use jev_client::Client;
34use jev_protocol::ModelId;
35use jev_worker::{FetchTransport, WorkerRuntime};
36use llm::Model;
37use rules::{Effect, End, Fact, Network, Next, Note, Value, Want};
38use serde::Deserialize;
39use tower_service::Service;
40use worker::{Context, Env, HttpRequest, event};
41
42mod ai;
43pub mod archive;
44mod browse;
45pub mod clone;
46mod diagram;
47pub mod events;
48pub mod meter;
49mod object;
50mod playground;
51pub mod view;
52
53use view::{Ending, JevCall, LlmCall, LlmOutcome, Tool, View};
54
55const DATASTAR: &str = include_str!("../../../ds-bundle/datastar.js");

The pixel emoji the site draws (the online list's flags, the vote buttons): SerenityOS's, cut down to those (third-party/serenity-emoji).

58const EMOJI: &[u8] = include_bytes!("../../../third-party/serenity-emoji/emoji.woff2");

The commit this Worker was built from (build.rs). Pages carry it, and the archive tells every page that connects what it is now.

62pub const BUILD: &str = env!("LMJTFY_BUILD");

What this deploy changed, for the people on the site: the built commit's Release-Note: trailer, or empty (build.rs).

65pub const NOTE: &str = env!("LMJTFY_NOTE");

The secret's name: lmjtfy's own Jev key, apart from the estate's general one, as jevstrudel has its own.

69const KEY: &str = "LMJTFY_TYPESAFE_API_KEY";
70const JEV_MODEL: &str = "JEV_MODEL";
71const LLM_MODEL: &str = "LLM_MODEL";
72const JEV_DOLLARS_PER_DAY: &str = "JEV_DOLLARS_PER_DAY";

What one visitor may do in a minute: full answers, and the facts request that runs as they type. Counted by the budget object (meter::admit). Cloudflare's own rate limiting binding was tried first and never refused a request on the live site (2026-10-02: 45 asks in 40 seconds, limit 10).

77const ASKS_PER_MINUTE: u32 = 10;
78const GLANCES_PER_MINUTE: u32 = 60;
80#[derive(Clone)]
81struct App {
82    env: Env,
83}
84
85#[event(fetch)]
86async fn fetch(mut request: HttpRequest, env: Env, context: Context) -> worker::Result<Response<Body>> {
87    let mut router = Router::new()
88        .route("/", get(page))
89        .route("/datastar.js", get(datastar))
90        .route("/emoji.woff2", get(emoji))
91        .route("/live.js", get(live_js))
92        .route("/icons/{name}", get(icon))
93        .route("/card.png", get(card))
94        .route("/rules", get(rules_page))
95        .route("/rules.svg", get(rules_svg))
96        .route("/ask", post(ask))
97        .route("/gate", post(gate))
98        .route("/rate", post(rate))
99        .route("/seen", post(seen))
100        .route("/feed", get(feed))
101        .route("/live", get(live))
102        .route("/{repo}", get(clone::landing))
103        .route("/{repo}/", get(clone::landing))
104        .route("/{repo}/{*path}", get(browse::path))
105        .route("/{repo}/info/refs", get(clone::refs))
106        .route("/{repo}/git-upload-pack", post(clone::upload_pack))
107        .with_state(App { env: env.clone() });
108    // The request as an event, before it is known what came of it
109    // (events.rs). A GET's is kept here, once it has a status, after the
110    // response has gone; a POST's by its handler, which knows how it ended.
111    let headers = request.headers().clone();
112    let event = events::of(&request);
113    if let Some(to) = moved(&request) {
114        let mut event = event.named("moved").with(headers.get(header::HOST).and_then(|host| host.to_str().ok()).unwrap_or_default());
115        event.status = 301.0;
116        context.wait_until(async move { events::record(&env, event).await });
117        return Ok(Response::builder()
118            .status(axum::http::StatusCode::MOVED_PERMANENTLY)
119            .header(header::LOCATION, to)
120            .header(header::CACHE_CONTROL, "public, max-age=86400")
121            .body(Body::empty())
122            .expect("static headers are valid"));
123    }
124    let got = event.clone().got();
125    request.extensions_mut().insert(event);
126    let mut response = router.call(request).await?;
127    if let Some(mut event) = got {
128        event.status = f64::from(response.status().as_u16());
129        // A page given to a browser is a visit, and says when it was counted.
130        let page = response.status().is_success() && response.headers().get(header::CONTENT_TYPE).is_some_and(|kind| kind.as_bytes().starts_with(b"text/html"));
131        if page
132            && let Some(cookie) = event.visit(&headers, js_sys::Date::now()).and_then(|cookie| axum::http::HeaderValue::from_str(&cookie).ok())
133        {
134            response.headers_mut().append(header::SET_COOKIE, cookie);
135        }
136        // Any page a browser is first given gives it its id, before it can
137        // ask anything, and that page's own event is the id's first: until
138        // 2026-10-03 only the home page gave one, and never said which.
139        if page
140            && event.browser.is_empty()
141            && event.client != "bot"
142            && let Some((id, cookie)) = new_browser()
143            && let Ok(cookie) = axum::http::HeaderValue::from_str(&cookie)
144        {
145            response.headers_mut().append(header::SET_COOKIE, cookie);
146            event.browser = id;
147        }
148        context.wait_until(async move { events::record(&env, event).await });
149    }
150    Ok(response)
151}

The site's address, bare. The addresses in ELSEWHERE lead to it: where it was first served, and the same name with www.

155const HOME: &str = "lmjtfy.fun";
156const ELSEWHERE: [&str; 2] = ["lmjtfy.deizel.workers.dev", "www.lmjtfy.fun"];

Where a request to another of the site's addresses should go instead, for good: the same path and query at HOME. People, link previews and git are sent on (git follows the redirect of its first request and clones from the new address). A page still open on the old address is left to finish there: its socket and its own requests (which a browser marks with Sec-Fetch-Mode other than navigate) would only break if sent on, and it moves the next time it is loaded.

165fn moved(request: &HttpRequest) -> Option<String> {
166    let host = request.headers().get(header::HOST)?.to_str().ok()?;
167    if !ELSEWHERE.contains(&host) {
168        return None;
169    }
170    let get = matches!(*request.method(), axum::http::Method::GET | axum::http::Method::HEAD);
171    let socket = request.headers().contains_key(header::UPGRADE);
172    let own = request.headers().get("sec-fetch-mode").is_some_and(|mode| mode != "navigate");
173    if !get || socket || own {
174        return None;
175    }
176    let path = request.uri().path_and_query().map_or("/", |path| path.as_str());
177    Some(format!("https://{HOME}{path}"))
178}
180#[derive(Deserialize)]
181struct Asked {
182    #[serde(default)]
183    q: String,

Which version of each call the page wants (archive::Pins).

185    #[serde(default)]
186    pins: String,
187}

What /card.png is of: a question, or with code, the code.

190#[derive(Deserialize)]
191struct Pictured {
192    #[serde(default)]
193    q: String,
194    code: Option<String>,

With code, which repository: lmjtfy.git when absent.

196    repo: Option<String>,
197}

#[worker::send]: asking the budget object is not Send, and an axum handler's future must be.

201#[worker::send]
202async fn page(State(app): State<App>, headers: HeaderMap, Query(asked): Query<Asked>) -> Response<Body> {
203    let budget = shared(&app.env).await;
204    let home = archive::home(&app.env).await;
205    let typed = ask::clean(&asked.q);
206    // For the link preview: what Jev said, if this was asked before.
207    let said = if typed.is_empty() { None } else { archive::answer(&app.env, &typed).await };
208    let page = view::page(&typed, said.as_ref(), &origin(&headers), budget, home.as_ref());
209    respond("text/html; charset=utf-8", "no-cache", page.into_string().into())
210}

Where this site is, for the preview's image address, which has to be whole. From the request, so the dev server previews itself.

214fn origin(headers: &HeaderMap) -> String {
215    let host = headers.get(header::HOST).and_then(|value| value.to_str().ok()).unwrap_or("localhost");
216    let scheme = if host.starts_with("localhost") || host.starts_with("127.") { "http" } else { "https" };
217    format!("{scheme}://{host}")
218}

The picture a link unfurls with: the question and what Jev said, read from the archive. It asks nothing, so a preview bot spends nothing.

222#[worker::send]
223async fn card(State(app): State<App>, headers: HeaderMap, Query(asked): Query<Pictured>) -> Response<Body> {
224    // `?code=<commit>`: the code page's picture. The commit is only there to
225    // change the address; what is drawn is the latest one read.
226    if asked.code.is_some() {
227        let repo = asked.repo.as_deref().and_then(clone::Repo::named).unwrap_or(clone::Repo::Lmjtfy);
228        let latest = clone::latest(&app.env, repo).await;
229        let url = format!("{}/{}", origin(&headers), repo.served());
230        let card = Card::Code { name: repo.served(), url: &url, recurse: !repo.submodules().is_empty(), about: repo.blurb(), branch: repo.branch(), latest: latest.as_ref() };
231        return respond("image/png", "public, max-age=3600", ::card::png(card).into());
232    }
233    let typed = ask::clean(&asked.q);
234    let said = if typed.is_empty() { None } else { archive::answer(&app.env, &typed).await };
235    let said = said.map(|entry| entry.answers).unwrap_or_default();
236    let card = if typed.is_empty() { Card::Home } else { Card::Question { input: &typed, said: &said } };
237    respond("image/png", "public, max-age=3600", ::card::png(card).into())
238}

/live: a page's socket, handed to the archive, which keeps it and tells it how many pages are open and what happens while it is.

242#[worker::send]
243async fn live(State(app): State<App>, mut request: axum::extract::Request) -> Response<Body> {
244    // Where Cloudflare placed the page, for the online list. The Worker sets
245    // these headers itself, over any the page sent, so a page cannot name its
246    // own place; the archive keeps them on the socket while it is open.
247    // Only a socket is handed on: the archive reads any other request as a
248    // message from this Worker, and a visitor's must never be one.
249    if !request.headers().get(header::UPGRADE).is_some_and(|upgrade| upgrade.as_bytes().eq_ignore_ascii_case(b"websocket")) {
250        return clone::refused(axum::http::StatusCode::UPGRADE_REQUIRED, "Not a socket.");
251    }
252    let place = request.extensions().get::<worker::Cf>().map(|cf| (cf.country(), cf.city()));
253    // Who connected, for the archive to keep (`events`).
254    let event = request.extensions().get::<events::Event>().and_then(|event| serde_json::to_string(event).ok());
255    let headers = request.headers_mut();
256    headers.remove(archive::COUNTRY);
257    headers.remove(archive::CITY);
258    headers.remove(archive::EVENT);
259    if let Some(event) = event.and_then(|event| axum::http::HeaderValue::from_str(&view::url_encoded(&event)).ok()) {
260        headers.insert(archive::EVENT, event);
261    }
262    headers.insert(archive::PLACED, axum::http::HeaderValue::from_static("1"));
263    if let Some((country, city)) = place {
264        for (name, value) in [(archive::COUNTRY, country), (archive::CITY, city)] {
265            if let Some(value) = value.and_then(|value| axum::http::HeaderValue::from_str(&view::url_encoded(&value)).ok()) {
266                headers.insert(name, value);
267            }
268        }
269    }
270    let request = match worker::Request::try_from(request) {
271        Ok(request) => request,
272        Err(_) => return clone::refused(axum::http::StatusCode::BAD_REQUEST, "Not a socket."),
273    };
274    match archive::live(&app.env, request).await {
275        Ok(response) => response.into(),
276        Err(_) => clone::refused(axum::http::StatusCode::BAD_GATEWAY, "The archive is unreachable."),
277    }
278}

What is left of the day's shared budgets, for the page.

281async fn shared(env: &Env) -> Option<view::Shared> {
282    let status = meter::status(env).await?;
283    let jev_per_day = env.var(JEV_DOLLARS_PER_DAY).ok()?.to_string().parse().ok()?;
284    Some(view::Shared { status, neurons_per_day: llm::FREE_NEURONS_PER_DAY, jev_dollars_per_day: jev_per_day })
285}

The rules as an SVG file, for the READMEs.

288async fn rules_svg() -> Response<Body> {
289    respond("image/svg+xml", "public, max-age=3600", diagram::standalone(&Network::lmjtfy()).into())
290}
292async fn datastar() -> Response<Body> {
293    respond("text/javascript; charset=utf-8", "public, max-age=86400", DATASTAR.into())
294}

The SharedWorker that holds one /live socket for a browser's tabs. Its address carries the build, so a deploy's pages get a new one; the script itself does not change with it, so it may be kept.

299async fn live_js() -> Response<Body> {
300    respond("text/javascript; charset=utf-8", "public, max-age=3600", include_str!("live.js").into())
301}
303mod pictures {
304    include!(concat!(env!("OUT_DIR"), "/icons.rs"));
305}

/icons/<name>.png: one of the explorer's pixel icons (third-party/jerrys-pixel-icons), from the table build.rs writes. They do not change under a name, so a browser may keep one for good.

310async fn icon(Path(name): Path<String>) -> Response<Body> {
311    let found = name.strip_suffix(".png").and_then(|name| pictures::ICONS.binary_search_by_key(&name, |(icon, _)| *icon).ok());
312    match found {
313        Some(at) => respond("image/png", "public, max-age=31536000, immutable", Body::from(pictures::ICONS[at].1)),
314        None => clone::refused(axum::http::StatusCode::NOT_FOUND, "No such icon."),
315    }
316}
318async fn emoji() -> Response<Body> {
319    respond("font/woff2", "public, max-age=604800", Body::from(EMOJI))
320}

The rules with the facts the link sets. Nothing is asked of anyone.

323async fn rules_page(Query(params): Query<Vec<(String, String)>>) -> Response<Body> {
324    let network = Network::lmjtfy();
325    let play = playground::play(&network, &params);
326    let says = playground::says(&network, &play);
327    let page = view::playground(&network, &play.known, &play.fired, &says, |clicked| playground::link(&play.known, clicked));
328    respond("text/html; charset=utf-8", "no-cache", page.into_string().into())
329}

Who is asking, for the visitor limit: Cloudflare's own header for the visitor's address, a key in the budget object's memory for a minute.

333fn visitor(headers: &HeaderMap) -> String {
334    headers.get("cf-connecting-ip").and_then(|value| value.to_str().ok()).unwrap_or("unknown").to_owned()
335}

Datastar posts the page's signals as JSON; q is the search box.

338async fn ask(State(app): State<App>, Extension(event): Extension<events::Event>, headers: HeaderMap, Json(asked): Json<Asked>) -> Response<Body> {
339    let visitor = visitor(&headers);
340    let browser = browser(&headers);
341    let pins = ::archive::Pins::parse(&asked.pins);
342    stream(move |events| answer(app, asked.q, pins, visitor, browser, event.named("answer"), events))
343}

A vote on Jev's answer: the search box still holds the question, and the button sets which way.

347#[derive(Deserialize)]
348struct Rated {
349    #[serde(default)]
350    q: String,
351    vote: String,
352}

How many votes a visitor may cast a minute.

355const VOTES_PER_MINUTE: u32 = 30;

/rate: a browser's vote on Jev's answer, and the votes after it, as the rating element. A browser with no id cannot vote; a question never answered has nothing to vote on.

360#[worker::send]
361async fn rate(State(app): State<App>, Extension(event): Extension<events::Event>, headers: HeaderMap, Json(rated): Json<Rated>) -> Response<Body> {
362    let input = ask::clean(&rated.q);
363    let browser = browser(&headers);
364    let vote = match rated.vote.as_str() {
365        "up" => Some(::archive::Vote::Up),
366        "down" => Some(::archive::Vote::Down),
367        _ => None,
368    };
369    let rating = match (&browser, vote) {
370        (Some(browser), Some(vote)) if !input.is_empty() && meter::admit(&app.env, "rate", visitor(&headers), VOTES_PER_MINUTE).await => {
371            archive::rate(&app.env, &input, browser, vote).await
372        }
373        _ => archive::rating(&app.env, &input, browser.as_deref().map(|browser| asker(browser, &input))).await,
374    };
375    events::record(&app.env, event.named("vote").with(rated.vote.chars().take(8).collect::<String>()).about(&input)).await;
376    let patch = PatchElements::new(view::rating(rating.as_ref(), browser.is_some()).into_string()).into_datastar_event();
377    respond("text/event-stream", "no-cache", Body::from(patch.to_string()))
378}

How many reports a visitor's pages may make a minute.

381const REPORTS_PER_MINUTE: u32 = 120;

/seen: a page's report of itself as it is left, or as a link off the site is followed (page.js): how long it was in view, how far down, the screen. Kept as a read or out event; the page is told nothing.

386#[worker::send]
387async fn seen(State(app): State<App>, Extension(event): Extension<events::Event>, headers: HeaderMap, body: String) -> Response<Body> {
388    if let Ok(report) = serde_json::from_str::<::archive::Report>(&body)
389        && meter::admit(&app.env, "seen", visitor(&headers), REPORTS_PER_MINUTE).await
390    {
391        events::record(&app.env, event.seen(&report)).await;
392    }
393    Response::builder().status(axum::http::StatusCode::NO_CONTENT).body(Body::empty()).expect("no headers to be wrong")
394}

The cookie that tells one browser from another, for counting each once per question. A random id the Worker gives a browser on its first page, kept a year. askers and ratings get only asker of it; events keeps it as it is, which is what ties one browser's rows together.

400const BROWSER: &str = "lmjtfy_browser";

This browser's id, if it has one.

403fn browser(headers: &HeaderMap) -> Option<String> {
404    let cookies = headers.get(header::COOKIE)?.to_str().ok()?;
405    cookies
406        .split(';')
407        .filter_map(|pair| pair.trim().split_once('='))
408        .find(|(name, _)| *name == BROWSER)
409        .map(|(_, id)| id.to_owned())
410        .filter(|id| id.len() == 36 && id.bytes().all(|b| b.is_ascii_hexdigit() || b == b'-'))
411}

A new browser id, as a Set-Cookie value, for a browser that has none.

414fn new_browser() -> Option<(String, String)> {
415    use wasm_bindgen::JsCast;
416    let crypto = js_sys::Reflect::get(&js_sys::global(), &"crypto".into()).ok()?;
417    let uuid = js_sys::Reflect::get(&crypto, &"randomUUID".into()).ok()?.dyn_into::<js_sys::Function>().ok()?.call0(&crypto).ok()?.as_string()?;
418    let cookie = format!("{BROWSER}={uuid}; Max-Age=31536000; Path=/; HttpOnly; Secure; SameSite=Lax");
419    Some((uuid, cookie))
420}

The browser for one question: its id and the question, hashed together, so the archive can tell a browser asking again from a new one and can link nothing else.

425pub(crate) fn asker(browser: &str, input: &str) -> ::archive::Asker {
426    use sha2::{Digest, Sha256};
427    let digest = Sha256::new().chain_update(browser.as_bytes()).chain_update([0]).chain_update(input.as_bytes()).finalize();
428    ::archive::Asker(digest.iter().map(|byte| format!("{byte:02x}")).collect())
429}

Where "asked lately" was scrolled to: the last line shown.

432#[derive(Deserialize)]
433struct Scrolled {
434    ms: f64,
435    q: String,
436}

/feed: the page of "asked lately" after the last line shown, put before the feed's end, and a new end. It reads the archive and asks nothing.

440#[worker::send]
441async fn feed(State(app): State<App>, Query(scrolled): Query<Scrolled>) -> Response<Body> {
442    let after = ::archive::Cursor { asked_ms: scrolled.ms, input: scrolled.q };
443    // An archive that cannot be read ends the feed here, rather than asking
444    // again for as long as the end is in view.
445    let entries = archive::older(&app.env, after).await.unwrap_or_default();
446    let (lines, end) = view::older(&entries);
447    let end = PatchElements::new(end.into_string()).selector("#more").mode(ElementPatchMode::Replace);
448    let mut body = String::new();
449    if !entries.is_empty() {
450        let lines = PatchElements::new(lines.into_string()).selector("#more").mode(ElementPatchMode::Before);
451        body.push_str(&lines.into_datastar_event().to_string());
452    }
453    body.push_str(&end.into_datastar_event().to_string());
454    respond("text/event-stream", "no-cache", Body::from(body))
455}

The same signals, while the visitor is still typing: the facts alone.

458async fn gate(State(app): State<App>, Extension(event): Extension<events::Event>, headers: HeaderMap, Json(asked): Json<Asked>) -> Response<Body> {
459    let visitor = visitor(&headers);
460    stream(move |events| glance(app, asked.q, visitor, event.named("gate"), events))
461}

An event stream fed by work. Jev and Workers AI calls are not Send (they hold JS values) and an axum handler must be, so the work runs on the isolate's own executor and reaches the response through a channel. The open response body is what keeps the request alive while it runs.

467fn stream<W, F>(work: W) -> Response<Body>
468where
469    W: FnOnce(UnboundedSender<String>) -> F,
470    F: Future<Output = ()> + 'static,
471{
472    let (events, stream) = unbounded::<String>();
473    wasm_bindgen_futures::spawn_local(work(events));
474    respond("text/event-stream", "no-cache", Body::from_stream(stream.map(Ok::<_, Infallible>)))
475}
477fn respond(content_type: &'static str, cache: &'static str, body: Body) -> Response<Body> {
478    Response::builder()
479        .header(header::CONTENT_TYPE, content_type)
480        .header(header::CACHE_CONTROL, cache)
481        .body(body)
482        .expect("static headers are valid")
483}

Sends markup to morph into the page. A closed channel means the visitor left; there is nobody to tell.

487fn patch(events: &UnboundedSender<String>, markup: maud::Markup) {
488    let event = PatchElements::new(markup.into_string()).into_datastar_event();
489    let _ = events.unbounded_send(event.to_string());
490}
492fn show(events: &UnboundedSender<String>, view: &View) {
493    patch(events, view::transcript(view));
494}

Everything a Jev call needs, built once per request.

497pub(crate) struct Jev {
498    pub(crate) client: Client<FetchTransport, WorkerRuntime>,
499    pub(crate) model: ModelId,
500    pub(crate) dollars_per_day: f64,
501}

Why no Jev call can be made at all.

504pub(crate) enum NoJev {

The Worker has no API key.

506    Offline,
507    Broken(String),
508}
510impl From<NoJev> for Ending {
511    fn from(none: NoJev) -> Self {
512        match none {
513            NoJev::Offline => Ending::Offline,
514            NoJev::Broken(error) => Ending::Failed { error },
515        }
516    }
517}

The Jev client. The Worker builds one to prepare requests and to know the site is online; the archive builds one to send them.

521pub(crate) fn jev(env: &Env) -> Result<Jev, NoJev> {
522    let key = env.secret(KEY).map(|secret| secret.to_string()).unwrap_or_default();
523    if key.trim().is_empty() {
524        return Err(NoJev::Offline);
525    }
526    let var = |name: &str| env.var(name).map(|var| var.to_string()).map_err(|e| NoJev::Broken(format!("{name}: {e}")));
527    let model = ModelId::pinned(&var(JEV_MODEL)?).map_err(|e| NoJev::Broken(e.to_string()))?;
528    let dollars_per_day: f64 = var(JEV_DOLLARS_PER_DAY)?
529        .parse()
530        .map_err(|_| NoJev::Broken(format!("{JEV_DOLLARS_PER_DAY} is not a number")))?;
531    let client = Client::new(FetchTransport::api(), WorkerRuntime, model.clone(), key.trim())
532        .map_err(|e| NoJev::Broken(e.to_string()))?;
533    Ok(Jev { client, model, dollars_per_day })
534}
536fn spent(pot: Pot) -> Ending {
537    Ending::Spent {
538        what: match pot {
539            Pot::Jev => "Jev",
540            Pot::Llm => "the LLM",
541        },
542    }
543}

Jev's answer to a prepared call, from the archive: the response it holds for this exact request, or the one it gets now. Err means the call was never made, so the page must not show it.

Which kept response, or whether to send again, is what the page's pins say for this request (archive::Pins::pick); the version it got comes back beside the answer.

552async fn judged_by_jev(
553    env: &Env,
554    jev: &Jev,
555    input: &str,
556    wanted: Wanted,
557    prepared: &Prepared,
558    pins: &::archive::Pins,
559) -> Result<(Outcome, Option<view::Versions>), Ending> {
560    let id = ::archive::call_id(jev_protocol::ENDPOINT, &prepared.request);
561    let ask = Ask::Jev { input: input.to_owned(), wanted, request: prepared.request.clone(), pick: pins.pick(&id) };
562    match archive::call(env, ask).await {
563        Ok(Called::Answered(record)) => Ok((
564            ask::read(
565            &jev.model,
566            prepared,
567            Kept {
568                body: &record.response,
569                sent: record.sent(),
570                took: Duration::from_secs_f64(record.took_ms / 1000.0),
571                attempts: record.attempts,
572                request_id: record.request_id,
573            },
574            ),
575            Some(view::Versions { id, version: record.version, versions: record.versions }),
576        )),
577        Ok(Called::Failed { error, request_id, took_ms }) => {
578            Ok((Outcome::Failed { error, request_id, took: Duration::from_secs_f64(took_ms / 1000.0), dollars: 0.0 }, None))
579        }
580        Ok(Called::Spent(pot)) => Err(spent(pot)),
581        Ok(Called::NotKept) => Err(Ending::NotKept { id }),
582        Err(error) => Err(Ending::Failed { error }),
583    }
584}

Asks Jev for facts about the input, all in one request, and records what they turned out to be. This is the rules' backfill: every Jev fact any live rule is waiting on, together.

589async fn learn(
590    env: &Env,
591    jev: &Jev,
592    view: &mut View,
593    facts: Vec<Fact>,
594    events: Option<&UnboundedSender<String>>,
595) -> Result<(), Ending> {
596    let failed = |error: String| Ending::Failed { error };
597    let wanted = Wanted::Facts(facts.clone());
598    let prepared = ask::wanted(&jev.model, &view.input, &wanted).map_err(|e| failed(e.to_string()))?;
599    view.facts = Some(JevCall::pending(&prepared));
600    if let Some(events) = events {
601        show(events, view);
602    }
603    let outcome = match judged_by_jev(env, jev, &view.input, wanted, &prepared, &view.pins).await {
604        Ok((outcome, kept)) => {
605            view.facts.as_mut().expect("just set").kept = kept;
606            outcome
607        }
608        Err(ending) => {
609            view.facts = None;
610            return Err(ending);
611        }
612    };
613    // Several facts can be read from one answer: each finds its question's.
614    let learned = match &outcome {
615        Outcome::Answered { judged, .. } => facts
616            .iter()
617            .map(|fact| {
618                let id = ask::question_id(*fact);
619                prepared
620                    .parts
621                    .iter()
622                    .position(|part| part.id == id)
623                    .and_then(|index| judged.get(index))
624                    .and_then(|judged| ask::learned(*fact, judged))
625                    .map(|value| (*fact, value))
626                    .ok_or_else(|| failed(format!("Jev's answer for {} is not the type that was asked", fact.name())))
627            })
628            .collect::<Result<Vec<_>, _>>(),
629        Outcome::Failed { error, .. } => Err(failed(error.clone())),
630    };
631    view.facts.as_mut().expect("just set").outcome = Some(outcome);
632    for (fact, value) in learned? {
633        view.known.learn(fact, value);
634    }
635    Ok(())
636}

Jev's probability that it can judge the input, once the facts are in.

639fn answerable(view: &View) -> Option<f64> {
640    match view.facts.as_ref()?.judged(ask::question_id(Fact::Answerable))? {
641        Judged::Noul(p_yes) => Some(*p_yes),
642        _ => None,
643    }
644}
646async fn answer(
647    app: App,
648    input: String,
649    pins: ::archive::Pins,
650    visitor: String,
651    browser: Option<String>,
652    mut event: events::Event,
653    events: UnboundedSender<String>,
654) {
655    let began = js_sys::Date::now();
656    let mut view = View::new(ask::clean(&input));
657    let browsing = pins.browsing();
658    view.pins = pins;
659    // Every ask counts towards the visitor's limit, an empty one too.
660    view.ending = Some(if !meter::admit(&app.env, "ask", visitor, ASKS_PER_MINUTE).await {
661        Ending::Slow
662    } else if view.input.is_empty() {
663        Ending::Empty
664    } else {
665        decide(&app.env, &mut view, &events).await
666    });
667    show(&events, &view);
668    // What was asked, how it ended and what it took. An old version looked
669    // at is an ask all the same, and says so.
670    let (sent, kept) = view.calls();
671    event.detail = if browsing { format!("{} (browsing)", events::ending(view.ending.as_ref())) } else { events::ending(view.ending.as_ref()).to_owned() };
672    (event.sent, event.kept) = (f64::from(sent), f64::from(kept));
673    event.llm = if view.llm.is_some() { 1.0 } else { 0.0 };
674    event.took_ms = js_sys::Date::now() - began;
675    events::record(&app.env, event.about(&view.input)).await;
676    // The page keeps the versions it is looking at, and no others.
677    let pinned = PatchSignals::new(serde_json::json!({ "pins": view.pinned() }).to_string()).into_datastar_event();
678    let _ = events.unbounded_send(pinned.to_string());
679    // The budget and the feed are the home page's, and it is hidden while an
680    // answer is up (page.css), so neither is sent again here. An old version
681    // looked at is not what Jev says now, so it is not kept as the answer.
682    if matches!(view.ending, Some(Ending::Answered)) && !browsing {
683        // The rules say whether it may be shown: Jev judged it fit.
684        let listed = Network::lmjtfy().notes(&view.known, Note::List);
685        let who = browser.as_deref().map(|browser| asker(browser, &view.input));
686        archive::asked(&app.env, &view.input, view.said(), view.llm.is_some(), listed, browser.as_deref()).await;
687        // The votes on the answer as it is now kept.
688        let rating = archive::rating(&app.env, &view.input, who).await;
689        patch(&events, view::rating(rating.as_ref(), browser.is_some()));
690    }
691}

Does what the rules say until they say the query has ended. The rules (rules::RULES) decide the order; this only carries each step out and records what it taught.

696async fn decide(env: &Env, view: &mut View, events: &UnboundedSender<String>) -> Ending {
697    let jev = match jev(env) {
698        Ok(jev) => jev,
699        Err(none) => return none.into(),
700    };
701    let network = Network::lmjtfy();
702    loop {
703        let (by, next) = network.decide(&view.known);
704        view.fired.extend(by);
705        let step = match next {
706            Next::Ask(facts) => learn(env, &jev, view, facts, Some(events)).await,
707            // One call writes everything the drafting rules that hold want.
708            Next::Do(Effect::Draft(_)) => drafted(env, view, &network.wants(&view.known), events).await,
709            Next::Do(Effect::Judge) => judge(env, &jev, view, events).await,
710            Next::End(End::NotAQuestion) => {
711                return match answerable(view) {
712                    Some(p_yes) => Ending::NotAQuestion { p_yes },
713                    None => Ending::Failed { error: "the rules refused an input Jev was not asked about".into() },
714                };
715            }
716            // Nothing is left to do. What to show is whatever the rules noted.
717            Next::Done if view.answered() => return Ending::Answered,
718            Next::Done => return Ending::NoQuestion,
719        };
720        if let Err(ending) = step {
721            return ending;
722        }
723        show(events, view);
724    }
725}

Asks Jev every question the LLM wrote, in one request.

728async fn judge(env: &Env, jev: &Jev, view: &mut View, events: &UnboundedSender<String>) -> Result<(), Ending> {
729    let drafts: Vec<(String, llm::Draft)> =
730        view.tools.iter().filter_map(|tool| Some((tool.id.clone(), tool.draft()?.clone()))).collect();
731    let wanted = Wanted::Drafted(drafts);
732    let prepared =
733        ask::wanted(&jev.model, &view.input, &wanted).map_err(|e| Ending::Failed { error: e.to_string() })?;
734    view.judging = Some(JevCall::pending(&prepared));
735    show(events, view);
736    match judged_by_jev(env, jev, &view.input, wanted, &prepared, &view.pins).await {
737        Ok((outcome, kept)) => {
738            let judging = view.judging.as_mut().expect("just set");
739            judging.outcome = Some(outcome);
740            judging.kept = kept;
741        }
742        Err(ending) => {
743            view.judging = None;
744            return Err(ending);
745        }
746    }
747    view.known.learn(Fact::Judged, Value::Bool(view.any_judged()));
748    Ok(())
749}

The LLM's questions for the input, from the archive, as the tool calls it made. A call Jev's protocol would refuse is kept, marked, and not sent.

753async fn drafted(env: &Env, view: &mut View, wants: &[Want], events: &UnboundedSender<String>) -> Result<(), Ending> {
754    let failed = |error: String| Ending::Failed { error };
755    let id = env.var(LLM_MODEL).map(|var| var.to_string()).map_err(|e| failed(format!("{LLM_MODEL}: {e}")))?;
756    let model = Model::find(&id).ok_or_else(|| failed(format!("{id} is not one of llm's candidates")))?;
757    let request = llm::request(&view.input, wants);
758    view.llm = Some(LlmCall { model: model.id, request: request.clone(), outcome: None, kept: None });
759    show(events, view);
760
761    let id = ::archive::call_id(model.id, &request);
762    let pick = view.pins.pick(&id);
763    let record = match archive::call(env, Ask::Llm { model: model.id.to_owned(), request, pick }).await {
764        Ok(Called::Answered(record)) => record,
765        Ok(Called::Failed { error, took_ms, .. }) => {
766            view.llm.as_mut().expect("just set").outcome = Some(LlmOutcome {
767                body: error.clone(),
768                neurons: 0.0,
769                took: Duration::from_secs_f64(took_ms / 1000.0),
770                dropped: 0,
771                sent: ask::Sent::Now,
772            });
773            return Err(failed(format!("the LLM: {error}")));
774        }
775        Ok(Called::Spent(pot)) => {
776            view.llm = None;
777            return Err(spent(pot));
778        }
779        Ok(Called::NotKept) => {
780            view.llm = None;
781            return Err(Ending::NotKept { id });
782        }
783        Err(error) => {
784            view.llm = None;
785            return Err(failed(error));
786        }
787    };
788    let parsed = llm::parse(&record.response);
789    view.llm.as_mut().expect("just set").kept = Some(view::Versions { id, version: record.version, versions: record.versions });
790    view.llm.as_mut().expect("just set").outcome = Some(LlmOutcome {
791        neurons: parsed.as_ref().map_or(0.0, |reply| model.neurons(reply.usage)),
792        took: Duration::from_secs_f64(record.took_ms / 1000.0),
793        dropped: parsed.as_ref().map_or(0, |reply| reply.dropped),
794        sent: record.sent(),
795        body: record.response,
796    });
797    let reply = parsed.map_err(|error| failed(format!("the LLM: {error}")))?;
798    view.tools = reply
799        .calls
800        .into_iter()
801        .enumerate()
802        .map(|(index, call)| {
803            let refused = call.draft.as_ref().ok().and_then(|draft| {
804                if !llm::takes(wants, draft) {
805                    // Jev has answered that reading itself, or the rules did not take it.
806                    return Some(format!("a {} was not what the rules asked the LLM for", draft.tool()));
807                }
808                ask::check(draft).err().map(|e| e.to_string())
809            });
810            // q1, q2, ...: the question's id in its request and on the page.
811            Tool { id: format!("q{}", index + 1), call, refused }
812        })
813        .collect();
814    let any = view.tools.iter().any(|tool| tool.draft().is_some());
815    view.known.learn(Fact::Drafted, Value::Bool(any));
816    Ok(())
817}

The facts alone, for the line under the search box. It is the same request /ask begins with, so what was typed is already kept by the time it is asked.

822async fn glance(app: App, input: String, visitor: String, event: events::Event, events: UnboundedSender<String>) {
823    let mut view = View::new(ask::clean(&input));
824    let seen = if view.input.is_empty() || !meter::admit(&app.env, "glance", visitor, GLANCES_PER_MINUTE).await {
825        None
826    } else {
827        match (jev(&app.env), Network::lmjtfy().next(&view.known)) {
828            (Ok(jev), Next::Ask(facts)) => learn(&app.env, &jev, &mut view, facts, None).await.ok(),
829            _ => None,
830        }
831    };
832    let (sent, kept) = view.calls();
833    let mut event = event.with(if seen.is_some() { "seen" } else { "" });
834    (event.sent, event.kept) = (f64::from(sent), f64::from(kept));
835    events::record(&app.env, event.about(&view.input)).await;
836    let glance = seen.and_then(|()| answerable(&view)).map(|answerable| view::Glance { answerable, kind: view.reading() });
837    patch(&events, view::live(glance));
838}
840#[cfg(test)]
841mod tests {
842    use super::*;
843
844    #[test]
845    fn a_browser_is_the_same_asker_of_one_question_and_unrelated_across_two() {
846        let id = "0b5f2a1e-7c3d-4e8f-9a6b-1c2d3e4f5a6b";
847        assert_eq!(asker(id, "is it?"), asker(id, "is it?"));
848        assert_ne!(asker(id, "is it?"), asker(id, "is it not?"));
849        assert_ne!(asker(id, "is it?"), asker("1b5f2a1e-7c3d-4e8f-9a6b-1c2d3e4f5a6b", "is it?"));
850        assert_eq!(asker(id, "is it?").0.len(), 64);
851        assert!(!asker(id, "is it?").0.contains(id));
852    }
853
854    fn request(method: &str, host: &str, path: &str, headers: &[(&str, &str)]) -> HttpRequest {
855        let mut request = axum::http::Request::builder().method(method).uri(path).header(header::HOST, host);
856        for (name, value) in headers {
857            request = request.header(*name, *value);
858        }
859        request.body(worker::Body::empty()).unwrap()
860    }
861
862    #[test]
863    fn the_old_address_leads_to_the_new_one_for_good() {
864        let to = |method, path, headers: &[(&str, &str)]| moved(&request(method, ELSEWHERE[0], path, headers));
865        assert_eq!(moved(&request("GET", "www.lmjtfy.fun", "/rules", &[])).as_deref(), Some("https://lmjtfy.fun/rules"));
866        assert_eq!(to("GET", "/?q=is+it%3F", &[]).as_deref(), Some("https://lmjtfy.fun/?q=is+it%3F"));
867        // git's first request, which it follows; a person's navigation.
868        assert_eq!(to("GET", "/lmjtfy.git/info/refs?service=git-upload-pack", &[]).as_deref(), Some("https://lmjtfy.fun/lmjtfy.git/info/refs?service=git-upload-pack"));
869        assert!(to("GET", "/rules", &[("sec-fetch-mode", "navigate")]).is_some());
870        // A page still open there finishes there.
871        assert_eq!(to("POST", "/ask", &[]), None);
872        assert_eq!(to("GET", "/live", &[("upgrade", "websocket")]), None);
873        assert_eq!(to("GET", "/feed?ms=1&q=x", &[("sec-fetch-mode", "cors")]), None);
874        // The new address, and a dev server, are home.
875        assert_eq!(moved(&request("GET", HOME, "/", &[])), None);
876        assert_eq!(moved(&request("GET", "localhost:8787", "/", &[])), None);
877    }
878
879    #[test]
880    fn only_a_well_formed_browser_cookie_is_read() {
881        let with = |cookie: &str| {
882            let mut headers = HeaderMap::new();
883            headers.insert(header::COOKIE, cookie.parse().unwrap());
884            browser(&headers)
885        };
886        let id = "0b5f2a1e-7c3d-4e8f-9a6b-1c2d3e4f5a6b";
887        assert_eq!(with(&format!("a=b; {BROWSER}={id}")).as_deref(), Some(id));
888        assert_eq!(with(&format!("{BROWSER}=<script>")), None);
889        assert_eq!(with("a=b"), None);
890    }
891}