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.
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.
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).
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.
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}
Which version of each call the page wants (archive::Pins).
What /card.png is of: a question, or with code, the code.
#[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.
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.
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.
/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}
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, ¶ms); 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.
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.
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.
/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.
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}
Sends markup to morph into the page. A closed channel means the visitor left; there is nobody to tell.
Everything a Jev call needs, built once per request.
Why no Jev call can be made at all.
504pub(crate) enum NoJev {
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}
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.
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}