The cycle in plain English
The core behaviour of Ask Small: every query, however many questions it hides, follows the same five steps (BRD §8).
- 1
Receive
The user submits one query, at most 140 characters, possibly holding more than one question. The cap is hard and UI-enforced; the agent never sees a longer input.
- 2
Parse
The agent reads the whole query and breaks it into a list of inferred questions — the harness keeps the first 3. Each is self-contained, answerable on its own without the others.
- 3
Resolve separately
Each inferred question is answered independently against the correct source: place questions go to places + scores + tags; festival questions go to the festival dataset; a “near me” question resolves the anchor first, then queries by proximity.
- 4
Club
Once every inferred question is resolved, the individual answers are combined into one coherent reply — in the order and shape the user asked.
- 5
Respond
Every recommendation names the place or event, the reason it matched (the score, tag, or festival fact), and the location; festival events add date and time.
Honesty on gaps
If an inferred question cannot be answered, the agent says so plainly for that part and still answers the parts it can. It never invents facts (FR-11, NFR-4).
Clarifying follow-up
Only when a slot genuinely cannot be inferred: at most 2 follow-up questions outstanding at a time, each asked once (rewrite D1; the original BRD allowed 1), with up to 4 options picked from tools — never free text in this version.
Under the hood — engine steps
The same cycle as the harness executes it:
intake → parse → anchors/keywords → dispatch → execution → follow-up gate →
finalize → grounding → reply, with defense and audit running through every block.
Intake guard
- Whole input capped at 140 characters, hard, UI-enforced
- May hold several questions; empty or abusive input handled gracefully
Parse — Round 1 · S1 resolver
- Query → inferred questions; harness keeps the first 3
- Slots per question:
what(intent) ·where(anchor) ·why(the grounding fact)
Resolve anchors & keywords
- Anchor ladder: landmark → area (
areas, geocode on miss) → session location → follow-up → city-wide - Keywords: noun phrases → embeddings → cosine top-k above the floor → canonical tags; below floor → follow-up trigger
Dispatch — rounds 1–2
- ≤3 tool calls per LLM response;
where: "near Q<k>"is an explicit dependency edge - Sequential, dependency-ordered, one shared read-only connection (D2)
- Category rule: place questions → places tools, festival questions → festival tools — never mixed
Execute tools
- Parameterised SQL over read-only
smallplaces.db; ≤5 rows per payload - Festival rows wrapped with a
past / today / upcoming / futureflag + relative + absolute time — the LLM never sees a raw timestamp - DB-first; Google Places fallback only when DB candidates fail the relevance gate (D4), labeled as Google-sourced
Follow-up gate
where: nullor failed routes → follow-up, a last resort decided by the harness, never an LLM whim- ≤2 outstanding, each pending question asked once (D1); ≤4 options, always from tools
- State travels in an HMAC-SHA256-signed echo (10 min TTL) — zero server state
Finalize — Round 3 · S2 finalizer
- One reply composed from gathered rows only, parts clubbed in the user’s order
- Each part: place/event + reason + location; events add relative day + absolute time
- No rows for a part → say so plainly for that part (FR-10)
Grounding check
- Harness string-matches every recommendation against gathered row names (G4)
- Mismatch → regenerate once → truncate or fall back
Respond
- Outcome:
answer·followup·fallback— never a guess (FR-11, NFR-4) - Round cap reached with no final → “Small doesn’t know yet.”
Audit
runs,run_events,followupswritten tosmallsystem.db— never read by inference- Headline metric: follow-up rate per 1,000 QA loops (D6)
Rounds, outcomes & ground rules
One back-and-forth = one model call + its tool calls executed + results returned. Hard cap of 3; bad JSON or a failed call costs a round (one re-ask, same cap).
Rounds
| Round | Allowed action |
|---|---|
| R1 | parse (what / where / why per question) → emit ≤3
tool calls for the independent questions |
| R2 | chained calls (where: "near Q<k>" — coords
sourced from Q<k>’s gathered rows) + any remaining calls |
| R3 | final only — tool actions are rejected by the harness |
Terminal outcomes
| Outcome | Shape |
|---|---|
| answer | clubbed parts, each grounded in gathered rows |
| followup | question + ≤4 options + signed echo; the run resumes on the pick |
| fallback | best partial answer, else “Small doesn’t know yet.” |
Cap reached with no final → fallback. Never a guess.
Ground rules held in code, not the prompt
- 140-character cap on the whole input; parse truncated to the first 3 inferred questions
- Every recommendation names only places/events present in the gathered rows
- Time flags computed tool-side — the LLM never does date arithmetic
- Places and festival categories never blend: enforced in SQL and the tool registry
- Knowledge DB opened read-only; a Google fallback result never writes to it
- Follow-up budget lives in the HMAC echo — no server-side conversation state
- Never stored: raw GPS, LLM reasoning traces, user identifiers
Follow-up resume path
The follow-up is two run() invocations with zero server-side
state between them — everything Run B needs travels inside the signed echo.
- Rounds 1–2 run; a pending question can’t resolve (
where: null/ geocode, keyword and festival routes all fail) - Follow-up-options tool supplies ≤4 options (P5 for places, F7 for festival)
- Response:
kind: "followup"+ question + options + signed echo
- Verify — HMAC signature, expiry,
picked ∈ options - Inject — the pick becomes the resolved slot; the LLM never sees the echo
- S3 prompt — fresh 3-round budget, another follow-up forbidden
- Compose — answer grounded in rows, or fallback
Between the runs, the client returns (echo, picked). The
reply is parsed only as a resolution of the pending product goal — pick an option, name an
area, refine intent; anything else re-presents the options once (D5). Rejections:
expired or tampered echo → re-run the original query · invalid pick → one retry · budget
exhausted → plain don’t-know for the pending part.
Example loops — view the steps
The three question-answer loops from the BRD. Pick a loop, then step through it message by message (click a step, use the buttons, or the ← → arrow keys).
Loops as written in the original BRD (one follow-up per query). The smallcommunity rewrite allows up to 2 outstanding follow-ups, each asked once (ASK_SPEC.md D1), and runs a round’s tool calls sequentially in dependency order (D2).