Algolia-Central
Documentation/04-Functions-Reference.md
04 — Functions Reference (ALIVE Call Graph)
Last updated: 2026-04-17. Signatures read from code, not guessed.
Every function on the critical path. Each entry: purpose, signature (input/output), dependencies (calls / called-by), side effects, observed timing. Functions marked
[EXPAND]need deeper documentation in a later pass.
Conventions
- Input: argument list with types
- Output: return type
- Calls: functions this function invokes
- Called by: functions that invoke this one
- Side effects: network, Redis, SSE emit, logging beyond standard
- Timing: observed from live traces (averaged across Runs 001–006)
Entry — handler (api-src/search.ts)
handler(req: VercelRequest, res: VercelResponse): Promise<void>
- Purpose: The single entry into the system. CORS, rate limit, circuit breaker, input validation, then dispatch.
- Calls:
checkRateLimit(clientId)→lib/security/rate-limitercheckCircuit()→lib/security/circuit-breakernormalizePersonaID(rawPersona)→lib/search/persona_routingisSpecialist(persona)→lib/search/persona_routing- Dynamic import →
igniteMaverickorigniteSpecialistfromlib/search/orchestrator recordSuccess()/recordFailure()→ circuit breaker- Called by: Vercel runtime
- Side effects: Sets CORS + security response headers; writes SSE chunks to
res; may return HTTP 400/405/429/500/503 - Branching:
- If
req.body.consent === true→igniteSpecialist({trigger:'execute'}) - If
isSpecialist(persona)→igniteSpecialist({trigger}) - Else →
igniteMaverick()
Maverick Path
igniteMaverick(input, sessionId, requestId?): Promise<ReadableStream>
- File:
lib/search/orchestrator.ts:45 - Input:
{query: string, history: any[]},sessionId: string,requestId?: string - Output:
ReadableStream(SSE body) - Calls (in order):
1.
getSessionState(sessionId)→ Redis 2.resetSessionState(sessionId)if first turn 3.extractSignals(input, sessionState, requestId)→ Gemini 4.guardIntent(query, intent)→ intent validator 5.updateSessionState(sessionId, newSignals)→ Redis merge 6.countLockedSignals(sessionState)→ Redis util 7.analyzeDiscoveryState(...)→ discovery state 8.fetchGoldenMap()→ Algolia Atlas load (cached 60s) 9.orchestrateRetrieval({...})→ main retrieval call 10.injectPricingIfNeeded(query, concepts, chunks)→ conditional chunk injection 11.trackView(...)→ Insights (fire-and-forget) 12.generateDossierSummary(sessionState, lockedSignals)→ dossier text 13.formatHitsForLLM(chunks)→ prompt injection string 14.buildMaverickPromptWithPersona(...)→ final system prompt 15.provider.startChat({systemPrompt, history, temperature, maxTokens})→ Gemini chat init 16.streamMaverickResponse({...})→ the streaming + audit 17. Handoff regex parse, keyword override (STRONG_BRUNO_SIGNALS) 18.addAskedSignal(sessionId, signal)→ Redis track 19.updateSessionState(sessionId, {pendingHandoff, active_persona})if handoff 20.processPostGenerationMetadata(...)→ metadata manager 21.updateSessionState(sessionId, {turn_count: +1})22.constructFinalEvent(...)→ final payload 23.persistSessionTelemetry(...)→ Redis telemetry - Side effects: Writes ~10 distinct SSE event types via
controller.enqueue - Error behavior: catch block emits
event: error {error: message}and closes controller
extractSignals(input, existingState, requestId?): Promise<BrainOutput>
- File:
lib/search/signal_extractor.ts:172 - Input:
{query, history},existingState: SessionState,requestId? - Output:
BrainOutput(typed — seeprompts/brain.ts:39) — containsintent, onion_signals, entities, search_query, keyword_query, discovery_question, filters, ... - Calls:
CHIP_SIGNAL_MAP[query.toLowerCase()]→ chip answer detection (line 183)getBrainPrompt(currentDate)→prompts/brain.ts:66getLLMProvider()→lib/llm/indexLLMAPI.extractSignals(fn, requestId)→ rate-limited wrapperprovider.generateContent({systemPrompt, messages, jsonMode:true, model:'gemini-2.0-flash'})- Called by:
igniteMaverick(step 3) - Side effects: single Gemini call (~1800–2000ms — 27% of Maverick turn time)
- Error behavior: returns a safe fallback
BrainOutputwith all signals null - Carry-forward rule (prompt-level): passes current SessionState as
CURRENT USER PROFILEblock; tells LLM to preserve existing values unless explicitly contradicted. F-026: this rule is a PROMPT instruction only — no code-level enforcement. If the LLM ignores it, nothing catches.
detectPushback(query, state): { contradictsField, denialStrength } — NEW Step 3 (F-026)
- File:
lib/search/pushback_detector.ts - Input: current user query + session state (8-signal shape)
- Output:
{ contradictsField: string | null, denialStrength: number (0..1) } - Purpose: Regex/keyword-only pass (NO LLM call). Detects when the user is pushing back on a previously-locked signal, so orchestrator can null the field BEFORE signal_extract merges with the old value via
||. - Called by:
orchestrator.igniteMaverickstep 1.5 (between session load and signal extract). - Side effects: none (pure). Caller issues
updateSessionStateto null the field. - Patterns matched: "I am not X", "we are not", "I never said", "that's wrong", "what makes you say", "why would you think". Word-boundary token match against field-specific keywords AND the current locked value.
detectChipAnswer(query: string): { field, value } | null
- File:
lib/search/signal_extractor.ts:285 - Input: raw query
- Output: matched chip entry from
CHIP_SIGNAL_MAPor null - Purpose: when user clicks a canned discovery chip (e.g., "Poor relevance"), we bypass LLM and directly map to the right signal field.
analyzeDiscoveryState(sessionState, askedSignals, query, turnCount, intent): DiscoveryAnalysisResult
- File:
lib/search/discovery_analyzer.ts:36 - Input:
SessionState,askedSignals: string[],query: string,turnCount: number,intent: string - Output:
{isQualified: boolean, isStrictDiscovery: boolean, isFatigueReached: boolean, availableFields: string[], showDiscoveryUI: boolean, ...} - Calls: internal-only
- Called by:
igniteMaverick(step 7) - Side effects: pure (no network / no state mutation)
- F-035 location: qualification rule is here. Currently requires high lockedSignals + turn count. Step 3 will relax.
generateDossierSummary(rawState, lockedSignals): string
- File:
lib/search/discovery_analyzer.ts:190 - Purpose: pretty-prints the 8-signal state as a short markdown string (observed 213–271 chars)
- Called by:
igniteMaverick(step 12). Output is emitted asevent: maverick_headerand also injected into the Maverick system prompt.
fetchGoldenMap(): Promise<any[]>
- File:
lib/search/retrieval_orchestrator.ts:570 - Input: none
- Output: Array of Atlas entries (classification map)
- Side effects: Algolia
searchSingleIndexon Atlas index; memoized 60s via in-process cache - Called by:
igniteMaverick(step 8); also byclassifyLink()for customer URL resolution
orchestrateRetrieval(options): Promise<RetrievalResult>
- File:
lib/search/retrieval_orchestrator.ts:113 - Input:
RetrievalOptions(line 21) —{query, searchQuery, keywordQuery?, filters, mapData, persona, sourceTypeFilters?, matchCount} - Output:
RetrievalResult(line 44) —{chunks, atlasMatch, strategy, queryID, ...} - Calls:
- Algolia
searchSingleIndexon NeuralSearch index - Internal strategy selection: filtered → relaxed → fallback
- Called by:
igniteMaverick(step 9) - Side effects: network call to Algolia; observed 700–1600ms
injectPricingIfNeeded(query, architectureConcepts, chunks): any[]
- File:
lib/search/retrieval_orchestrator.ts:532 - Purpose: If query mentions pricing/scale, adds pricing chunks to the result set that might not have been semantically matched
- Called by:
igniteMaverick(step 10)
formatHitsForLLM(hits): string
- File:
lib/search/chunk-merger.ts:5 - Input:
hits: any[] - Output: a single formatted string with
[SOURCE N] title \n url \n chunk_textfor each hit - Called by:
igniteMaverick(step 13) + potentially bybuildMaverickPromptWithPersona
buildMaverickPromptWithPersona(dossier, sources, discoveryInstructions, turnCount, isStrictDiscovery): Promise<string>
- File:
lib/search/prompts/maverick.ts:12 - Purpose: Assembles the full Maverick system prompt (sandwich structure: persona → knowledge → directives)
- Output size: ~16KB baseline; known to spike to 157KB (F-006 — root cause unknown)
- F-032 lives here: the Namedrop template instruction that leaks into UI
streamMaverickResponse(options): Promise<StreamResult>
- File:
lib/search/stream_processor.ts:60 - Input:
StreamOptions(line 26) —{encoder, controller, chat, query, lockedSignals, isQualified, isFatigueReached, isAccelerated, availableFields, rawState, products, contextSources, sessionId} - Output:
StreamResult(line 45) —{fullContent, askedSignal, detectedSources, zombiePreventionTriggered, auditResult} - Calls:
new InlineStreamAuditor(contextSources, products)— line 82inlineAuditor.auditParagraph(paragraph)per paragraph — line 90 (the hallucination guard)chat.sendMessageStream(query)— Gemini streaming- Eventually: post-stream
ContentAuditor.auditContent()for HEAD checks - Called by:
igniteMaverick(step 16) - Side effects: writes
event: chunkper paragraph to SSE; writesevent: audit_tracediagnostic lines
determineHandoffTarget(sessionState, query): 'elena' | 'bruno'
- File:
lib/search/question_expander.ts:16 - Purpose: keyword score — which specialist is the better match for this query?
- Called by:
igniteMaverick(step 17 — AFTER regex match, as a fallback) - Known issue: 3-layer handoff routing (regex → this function → STRONG_BRUNO_SIGNALS override) is a Frankenstein pattern — F-027 cleanup target.
processPostGenerationMetadata(options): Promise<MetadataResult>
- File:
lib/search/metadata_manager.ts:98 - Purpose: computes
wordCount,uniqueSources[], updates discovery history - Called by:
igniteMaverick(step 20)
constructFinalEvent(options): object
- File:
lib/search/metadata_manager.ts:315 - Purpose: assembles the
event: finalSSE payload (content, sources, signals, discoveryResult, etc.) - Called by:
igniteMaverick(step 22),igniteSpecialist(step 9 equivalent — Specialist builds its own final payload inline, does NOT call this)
persistSessionTelemetry(sessionId, turns, state, handoff, chunkCount, status): Promise<void>
- File:
lib/search/telemetry.ts:93 - Side effects: Redis write (telemetry:sessionId key)
- Called by: both
igniteMaverickandigniteSpecialist
Specialist Path
igniteSpecialist(input, sessionId, requestId?): Promise<ReadableStream>
- File:
lib/search/orchestrator.ts:611 - Input:
{query, history, persona: 'elena'|'bruno', trigger?, expandedQuestion?},sessionId,requestId? - Output:
ReadableStream - Calls (in order):
1.
validatePersona(input.persona)→ guardrail 2.getSessionState(sessionId)→ Redis 3.buildAgentStudioMessage({query, trigger, expandedQuestion, sessionState})→ internal 4.getAgentId(validatedPersona)→ agent ID resolver 5.callAgentStudio({agentId, message, history})→ Agent Studio POST 6.adaptAgentStudioStream(response, {encoder, controller, persona, sessionId})→ parse + pass through 7. Parallel HEAD check on all links in response (lines 697–712) 8.persistSessionTelemetry(...)→ Redis 9. Emitevent: finalinline (not viaconstructFinalEvent) - Side effects: Agent Studio HTTP call (1500–1800ms first byte); total stream 3–35s
- 🚨 F-038: Does NOT instantiate
InlineStreamAuditor. Specialist output is NOT audited for paragraph-level hallucinations, attributed metrics, or grounding.
buildAgentStudioMessage(params): string
- File:
lib/search/orchestrator.ts:789(internal, not exported) - Input:
{query: string, trigger?: string, expandedQuestion?: string, sessionState: SessionState} - Output: XML-tagged message string —
<user_question>...</user_question>\n<extracted_entities>...\n</extracted_entities> - Trigger branching:
handshake→ constructs canned "EXECUTE HANDSHAKE PROTOCOL" instructionexecute/consent→ uses expandedQuestion from session's pendingHandoff- default → uses raw query
callAgentStudio(request): Promise<Response>
- File:
lib/agent-studio/client.ts:62 - Input:
{agentId, message, history} - Output: raw
Responsewith SSE body - Side effects: HTTPS POST to Agent Studio endpoint
- Observed timing: 1500–1800ms to first byte
getAgentId(persona: AgentPersona): string
- File:
lib/agent-studio/client.ts:150 - Output:
'f029acbb-...'for elena,'facb549e-...'for bruno - Lookup:
AGENT_IDS[persona]
adaptAgentStudioStream(response, options): Promise<StreamAdapterResult>
- File:
lib/agent-studio/stream-adapter.ts:42 - Input: raw Agent Studio Response,
{encoder, controller, persona, sessionId} - Output:
{fullContent: string, sources: Source[]} - Purpose: parses Agent Studio's XML-tagged SSE chunks, strips control tags via
StatefulStreamStripper, emits cleanevent: chunk {content}to the client - 🚨 F-038: this is where the hallucination audit SHOULD be wired in but currently isn't.
Session State (redis.ts)
getSessionState(sessionId): Promise<SessionState>
- File:
lib/search/redis.ts:105 - Side effects: Redis GET
- Observed timing: 41–53ms (warm)
updateSessionState(sessionId, updates: Partial<SessionState>): Promise<boolean>
- File:
lib/search/redis.ts:189 - Pattern: partial merge into existing state
- Called at: multiple points in both ignite functions
countLockedSignals(state: SessionState): number
- File:
lib/search/redis.ts:225 - Logic: counts non-null signal fields (stack, scale, role, pain, brand, industry, product, architecture_concepts)
- F-026 related: a "wrong but present" value counts as locked — no notion of "revisable" signals. Step 3 fix.
addAskedSignal(sessionId, signal): Promise<boolean>
- Purpose: appends to
state.asked_signals[]— no-repeat protocol - Side effects: Redis
resetSessionState(sessionId): Promise<boolean>
- Purpose: called on first turn (history.length === 0) to clear prior session
Audit Primitives (audit.ts)
classifyLink(label, url, ctx: ClassifyContext): LinkDecision
- File:
lib/search/audit.ts:159 - Input: link label text, URL, context (sources + mapData + allowedHostnames + healthCheck enabled flag)
- Output:
LinkDecision={action: 'keep'|'replace'|'strip', reason, replacement?} - Purpose: THE core decision function for hallucination-prevention link audit. Called per link in every paragraph.
- Decision tree:
1. Hostname not in ALLOWED_HOSTNAMES → strip
2. URL matches
HALLUCINATED_CUSTOMER_SENTINEL→ strip sentence 3. Generic label pattern matches → try upgrade via sources 4. Customer path pattern → verify against mapData or strip 5. Technical doc URL + not whitelisted path → strip 6. Default → keep - Called by:
InlineStreamAuditor.auditParagraph(and formerly post-streamContentAuditor.auditContent)
upgradeGenericLinkLabels(content, sources): string
- File:
lib/search/audit.ts:256 - Replaces generic labels ("Algolia blog") with real doc titles from sources. Step 3 added a sibling scrubber
normalizeLinkLabelSuffix()that runs right after to collapse duplicated| Algolia | Algoliasuffixes (F-031).
normalizeLinkLabelSuffix(content): string — NEW Step 3 (F-031)
- File:
lib/search/audit.ts - Regex collapses
[Title | Algolia | Algolia(| Algolia)*](url)to[Title | Algolia](url). Case-insensitive. Middle-of-label| Algoliauntouched.
stripPromptInstructionLeaks(content): string — NEW Step 3 (F-032)
- File:
lib/search/audit.ts - Strips whole heading lines when a prompt-template label (Namedrop, VALUE SELL, Customer Example, SIGNAL EXTRACTION) appears at line-start followed by colon. Defence-in-depth against prompt leaks.
validateCustomerClaim(sentence, sources, mapData): { valid, reason? } — NEW Step 3 (F-038)
- File:
lib/search/audit.ts - Purpose: Hallucination guard for specialist responses (and Maverick). Returns
valid=falsewhen a sentence contains a non-Algolia capitalized name + metric (%, $, Nx) but fails ANY of: company in sources, metric in sources, case-study link in sentence. - Called from:
InlineStreamAuditor.auditGroundingin a non-destructive sentence-level regex replace.
stripDanglingAttributions(content): string
- File:
lib/search/audit.ts:269 - Purpose: strips orphaned "according to ..." or "— Source X" fragments after a link was removed
stripHallucinatedCustomerSentences(content): string
- File:
lib/search/audit.ts:300 - Purpose: removes entire sentences that mention a fabricated customer (flagged by classifyLink with
HALLUCINATED_CUSTOMER_SENTINEL).
findCanonicalCustomerUrl(label, mapData, sources): string | null
- File:
lib/search/audit.ts:119 - Purpose: when the label sounds like a customer name, find the canonical
algolia.com/customers/<slug>URL via mapData/sources
formatTraceLine(label, url, decision): string
- File:
lib/search/audit.ts:224 - Purpose: produces the
[AUDIT] KEEP | "Label" → URL | reasonconsole lines visible in live trace - Called by:
InlineStreamAuditorafter every decision
Audit Classes (content_auditor.ts)
class InlineStreamAuditor
- File:
lib/search/content_auditor.ts:375 - Constructor:
new InlineStreamAuditor(contextSources: any[], products: any[]) - Primary method:
auditParagraph(paragraph: string): string - Runs:
htmlAnchorsToMarkdown→ match links →classifyLinkper link →applyDecision→stripDanglingAttributions→stripHallucinatedCustomerSentences→upgradeGenericLinkLabels - Returns the cleaned paragraph (or the original if no issues)
- Log lines emitted:
[AUDIT] ══ paragraph in (N chars, M links) ══/[AUDIT] KEEP|STRIP|REPLACE | .../[AUDIT] ══ paragraph out (N chars) ══ - Currently instantiated by:
streamMaverickResponseonly — F-038
class ContentAuditor
- File:
lib/search/content_auditor.ts:113 - Post-Step-2 scope: link HEAD checks only (tone/grounding/confidence methods deleted in Step 2)
- Primary method:
async auditContent(content, sources, config): Promise<AuditResult>— runs AFTER stream complete
Helper Utilities
normalizePersonaID(raw: string|null|undefined): PersonaID
- File:
lib/search/persona_routing.ts:24 - Maps frontend values like "solutions_engineer" → canonical "elena"
isSpecialist(persona: string): boolean
- File:
lib/search/persona_routing.ts:34 persona in ['elena', 'bruno']
guardIntent(query: string, currentIntent?: 'ae'|'technical'): 'ae'|'technical'
- File:
lib/search/domain_guardrails.ts:110 - Scans query for TECHNICAL_MARKERS / NON_TECHNICAL_BLOCKERS to override LLM intent if LLM got it wrong
validatePersona(persona?: string|null): Persona
- File:
lib/search/domain_guardrails.ts:159 - Defaults to 'maverick' if invalid input
trackView(params: {index, eventName, userToken, queryId, objectIds}): Promise<void>
- File:
lib/search/insights.ts:46 - Fire-and-forget Insights event
Frontend Hooks
useChatStream({...})
- File:
src/hooks/chat/useChatStream.ts:20 - Purpose: React hook — sends POST to /api/search (Maverick), parses SSE, updates message state
- Events handled: pulse, pipeline_step, signals, sources, maverick_header, chunk, discovery, handoff_proposal, quality, audit, turn_snapshot, final, error
- P-9 target: currently triggers 3 message re-renders per turn (signals → sources → final) — collapse to 1 reducer commit
useSpecialist({...})
- File:
src/hooks/chat/useSpecialist.ts:18 - Purpose: React hook — sends POST to /api/search with persona=elena|bruno and either trigger=handshake or consent=true
- Events handled: same set as useChatStream, specialist-variant payloads
- Duplication (F-027): ~80 lines of handlers duplicated between the two hooks. Step 7 target.
parseStreamTags(content): {...}
- File:
src/hooks/chat/streamParsers.ts:7 - Purpose: parses special inline markers (
<specialist_handoff>,<discovery_pivot>) from the streamed markdown
normalizeSources(rawSources): Source[]
- File:
src/hooks/chat/streamParsers.ts:106 - Purpose: normalizes source shape across Maverick + Specialist payloads
[EXPAND] Gaps to fill later
These are marked for deepening in a later doc pass:
retrieval_orchestrator.ts— full strategy selection tree (filtered → relaxed → fallback)discovery_analyzer.ts— the exact qualification formula (F-035 target)content_auditor.ts—InlineStreamAuditor.auditGroundingbehavior (specifics of attributed-metric detection)persona_loader.ts— YAML load pipeline, cache invalidation logiclib/llm/index.ts— thegetLLMProvider()abstraction (not yet documented)lib/security/rate-limiter— sliding window + bucket logiclib/security/circuit-breaker— state machine
Cross-references
- 02-Data-Flow — when each function fires in the request lifecycle
- 03-Files-Reference — the file-level catalog
- 08-Audit-Pipeline — deep dive on InlineStreamAuditor + F-038 gap
- 09-Session-State — SessionState schema
- 10-Known-Issues — F-026, F-031, F-035, F-038 all trace back to functions listed here