Skip to content

Page Context & Deep Links

The assistant is page-aware. When a user opens Vernis on a detail page like /contacts/abc-123, the system prompt automatically includes which entity they’re viewing — so they can say “show me their calls” without specifying who.

User navigates to /contacts/abc-123
→ AssistantPanel reads the current Route signal
→ route_to_page_context() maps Route → PageContext
→ PageContext sent with every WsClientMessage::UserMessage
→ Server validates entity_type and entity_id
→ Server fetches entity name from DB (e.g. "John Doe")
→ System prompt injects: "The user is viewing contact John Doe (ID: abc-123)"
→ AI resolves "this contact" / "them" / "their" to that entity
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct PageContext {
pub route: String, // "/contacts/abc-123"
pub entity_type: Option<String>, // "contact"
pub entity_id: Option<String>, // "abc-123"
}

On list pages and non-entity routes, entity_type and entity_id are None. The assistant still knows the route (e.g. “the contacts list”) but has no entity to scope to.

The AssistantPanel component reads the current route via use_route::<Route>() and converts it on every navigation:

let page_context = route_to_page_context(&current_route);

This PageContext is attached to every WsClientMessage::UserMessage:

WsClientMessage::UserMessage {
content: user_text,
page_context: Some(page_context),
}

page_context_mapper.rs defines route_to_page_context() — a match over all detail-view Route variants:

Route variantentity_typeExample path
ContactDetailsView { id }contact/contacts/{id}
CallDetailsView { id }call/calls/{id}
PlanDetailsView { id }plan/plans/{id}
PlanTemplateDetailsView { id }plan_template/plan-templates/{id}
AgentDetailsView { id }agent/agents/{id}
TaskDetailsView { id }task/tasks/{id}
PhoneDetailsView { id }phone/phones/{id}
KnowledgeDetailsView { id }knowledge/knowledge/{id}
AnalyzerDetailsView { id }analyzer/analyzers/{id}
TextAgentDetailsView { id }text_agent/text-agents/{id}
MessagingContactView { contact_id }contact/messaging/{id}
Any other routeNonevaries

When you add a new detail-view route, add a match arm here so the assistant picks it up.

All entity type ↔ route path mappings live in one constant:

pub const ENTITY_ROUTES: &[(&str, &str, &str, &str)] = &[
// (entity_type, route_prefix, display_label, prompt_label)
("contact", "/contacts", "contact", "contact"),
("call", "/calls", "call", "call"),
("plan", "/plans", "plan", "plan"),
("plan_template", "/plan-templates", "plan_template", "plan template"),
("agent", "/agents", "agent", "agent"),
("task", "/tasks", "task", "task"),
("phone", "/phones", "phone", "phone number"),
("knowledge", "/knowledge", "knowledge", "knowledge base"),
("analyzer", "/analyzers", "analyzer", "analyzer"),
("text_agent", "/text-agents", "text_agent", "text agent"),
("messaging", "/messaging", "messaging (contact)", "messaging thread"),
];

This constant drives three functions:

FunctionPurpose
entity_deep_link(entity_type, entity_id)Returns the URL path for an entity (e.g. /contacts/abc-123)
all_deep_link_patterns()Returns all patterns for system prompt link docs
entity_type_label(entity_type)Returns a human-friendly label (e.g. “phone number”)

All assistant tools use entity_deep_link() instead of hardcoded format!() strings:

use crate::mods::assistant::page_context_mapper::entity_deep_link;
// Before (scattered across tool files):
let link = format!("/contacts/{}", contact_id);
// After (one function, one source of truth):
let link = entity_deep_link("contact", &contact_id.to_string());

Every tool that returns entity data includes a deep_link field in its JSON response. This applies to both read-only and write tools — the AI uses these links directly instead of constructing URLs.

Read-only tools with deep_link in results:

ToolEntity typeExample deep_link
ai_search_contactscontact/contacts/abc-123
ai_get_contact_detailscontact/contacts/abc-123
ai_find_contact_by_phonecontact/contacts/abc-123
ai_get_contact_messagescontact/contacts/abc-123
ai_get_unanswered_contactscontact/contacts/abc-123
ai_get_plan_templatesplan_template/plan-templates/def-456

Data types with deep_link field:

These shared types include a deep_link: String field populated via entity_deep_link() at construction time:

TypeEntity typeUsed by
AiAgentInfoagentAgent listing/detail tools
AiKnowledgeBaseInfoknowledgeKnowledge base tools
AiPhoneNumberInfophonePhone number tools
AiPlanEnrollmentplanPlan listing tools

Write tools also return deep_link in their success responses — see Write Actions & Confirmation Gate.

The system prompt includes an explicit instruction:

Never construct URLs yourself. Only use the deep_link values returned by tool results.

This prevents the AI from fabricating URLs with incorrect entity IDs. All links must come from actual tool responses where the server provides verified, org-scoped entity IDs.

The server validates all client-supplied fields before interpolating them into the system prompt:

  1. Entity type — checked against ENTITY_ROUTES. Unknown types produce a generic “unknown page” message.
  2. Entity ID — parsed as a UUID. Non-UUID strings are rejected.
  3. Entity name — fetched from DB, then passed through sanitize_for_prompt() which strips control characters and truncates to 200 chars.
  4. User name — also sanitized before prompt interpolation.
fn sanitize_for_prompt(s: &str) -> String {
s.chars()
.filter(|c| !c.is_control())
.take(200)
.collect::<String>()
.trim()
.to_string()
}

When the user is on a detail page, the server fetches a readable name for the entity. Each entity type has a custom query:

Entity typeWhat’s fetchedOrg scoping
contactfirst_name + last_nameDirect organization_id filter
callDirection + date + phone numberJOIN through phone_number.organization_id
plantitleDirect filter
tasktitleDirect filter
agentnameDirect filter
phonenumberDirect filter

All queries scope to the user’s organization to prevent cross-tenant data leakage.

FilePurpose
src/mods/assistant/page_context_mapper.rsENTITY_ROUTES, route_to_page_context(), entity_deep_link()
src/mods/assistant/types/assistant_types.rsPageContext struct definition
src/mods/assistant/services/build_system_prompt_service.rsbuild_page_context_section(), sanitize_for_prompt()
src/mods/assistant/components/assistant_panel_component.rsRoute detection via use_route