Skip to content

ARCHITECTURE

Views Agent

Shipped

The Views agent is Aero's domain agent for Views — the saved filter + column + grouping configurations users build to browse projects, tasks, customers, and other OnRamp objects. It creates and edits views through a human-in-the-loop draft flow, edits live views directly with confirmation, and answers data-shaped questions inline through a read-only SQL gate. It runs on Strands inside the copilot runtime, dispatched by a global intent router.

copilot peers

4

Views is one of four domain agents the router dispatches to

model-facing tools

24

across 8 groups, from matchers to canvas CRUD

intent paths

3

A edit · B create · C the reactive ladder

SSE event types

6

progress, snapshots, refresh, navigation, chips

01 · WHERE IT SITS

A peer among peers

There is no agent hierarchy. The copilot runtime registers four peer domain agents from runtimes/copilot/agents.yaml; the router (agents/_router/) is infrastructure that classifies each user message and dispatches to one of them. It auto-enables whenever a runtime lists two or more agents. Two more domain agents live in separate runtimes (intel, portal) and never share a conversation with Views.

_router

Haiku classifier · RouterDecision {domain, handoff_context} · prompt generated from agent manifests at startup

views

View Builder

Views CRUD + outcome-framed data questions

playbooks

Playbook Editor

Onboarding template authoring

onramp_chat

OnRamp Chat

General product Q&A and fallback

insights

Insights

Chart-scoped analytics Q&A

re_engagement

intel runtime

Stalled-project analysis

portal

portal runtime

Customer-facing portal help

router → views

RouterDecision

Haiku classifier picks the domain; manifest-generated criteria

views → router

return_to_router

sets active_domain="router"; same-turn loop re-dispatches (max 3 hops)

views → insights

navigate_to_insights

ladder matched an existing chart — pivot instead of rebuild

How a handoff actually works. An agent never "calls" another agent. It sets agent_state["active_domain"] = "router" (the return_to_router tool) and returns; the runtime's same-turn dispatch loop sees the change, re-runs the router, and dispatches the next agent — capped at 3 hops per turn so two agents can't ping-pong. onramp_chat uses exactly this to push view create/edit questions to the Views specialist.

runtimes/copilot/agents.yaml · runtimes/copilot/main.py · agents/_router/agent.py · agents/views/tools/nav.py:47-58 · agents/onramp_chat/prompts/prompt.md

02 · WHAT IT ENTERTAINS

Three intents, one ladder

Every turn, the prompt classifies the message into one of three intents. The classification decides the flow — and whether the reactive ladder runs at all.

Intent A · edit-shaped

Modify the view you're on

On a view + an edit verb (add, remove, change, sort … instead, filter to, only show). Runs Path B: confirm once, then write straight to the live view with update_view_directly — no draft, which is why it confirms first.

"add a status filter" · "only show overdue ones"

Intent B · explicit create

Build a new view

A clear create verb (create a view, build me a view, new view). Runs Path A: propose in plain language first, write a draft only after the user approves, apply only when they say so.

"create a view of overdue projects" · "build me a view of customers behind on payments"

Intent C · data question

Answer, then offer

Interrogative phrasing with no edit/create verb (show me, how many, which, list, what's the average). Runs the reactive ladder below. A new View is a follow-up affordance, never the first move.

"how many tasks are blocked?" · "which customers are getting stuck?"

The reactive ladder — three answer surfaces

For a data question the agent checks three surfaces: an existing View, an existing Insights chart, and inline SQL results — fanned out in parallel. The matchers are enrichers, not gates: the data answer renders first, matched artifacts append as inline links, and the matchers are deterministic scorers, not LLM calls.

find_matching_view

Jaccard similarity of question tokens vs. each view's name (0.6), description (0.2), and filter fields (0.2); match threshold 0.5, with an object-type keyword gate so a task question never matches a customer view. Weights are git-tracked constants tuned by eval runs — no env vars.

find_matching_insight

Keyword score against the in-memory Insights chart catalog; threshold 0.4 and the winner must beat the runner-up by 0.2. On a match the agent appends the chart as an inline link — or pivots to the Insights agent for chart-scoped follow-ups.

Before offering to save an inline answer as a view, the agent runs a feasibility check (can_object_type_express_columns / can_view_express_query): every SQL column must be expressible as a view field. If not, it looks for a substitute field — same attribute, different point (planned vs. actual start date) — and names the gap in one sentence; with no substitute it sends the Known-Gap message and offers no save chip at all.

When it hands off

  • Back to the router (return_to_router) — the user pivots out of view work entirely: playbook edits, settings, general product questions.
  • To Insights (navigate_to_insights) — the ladder matched an existing chart and the user wants the chart, not a view.
  • Uncertain intent → chips, not guesses — on-view: Edit this view Show me the data; off-view: Build me a view Just answer the question. The prompt prefers answering over guessing an edit: answering is recoverable, a wrong edit is not.

Not today

  • No removals via merge — column and grouping merges are add/upsert only; the agent cannot drop a column or grouping level from an existing view.
  • Flat filters only — the agent writes a single AND/OR condition list; nested filter groups (which the Views UI itself supports) are outside its vocabulary.
  • No thisYear operator — period filters are two explicit ISO-date conditions (greaterEquals period start + less next period start) computed from the session's date anchor.
  • "Today" is session-start — the date anchor is resolved when the session begins; a cross-midnight session keeps yesterday's anchor.
  • Vendor users only — the internal endpoints reject portal/customer users.
  • Inline answers cap at 1,000 rows — the SQL gate truncates; the prompt's presentation rules forbid passing a capped sample off as the whole.

views_agent.md:37-121,245 · tools/routing.py · matcher_weights.py · tools/canvas.py (merge semantics) · shared/prompts/today_context.py

03 · HOW IT WORKS

Question to Final Answer — two example flows

“How are my projects doing this year?” (user is not on a view)

  1. 1User asks in the Aero panel
  2. 2Router classifies the intent
  3. 3pre_dispatch grounds the turn
  4. 4The reactive ladder fans out
  5. 5SQL executes through the read-only gate
  6. 6Progress streams live
  7. 7Answer first, then enrich
  8. 8Chips + telemetry close the turn

Frontend

User asks in the Aero panel

The chat panel sends the message to the copilot runtime with the user’s JWT. The active page contributes ui_context (route, current view id if any) on every turn.

Contractchat request → copilot runtime · Authorization: Bearer JWT · ui_context payload

app/ui-organization (Aero panel) · runtimes/copilot/main.py

step 1 of 8

04 · ANATOMY

What makes a Views agent

PROMPT

System prompt — 485 lines of operating doctrine

agents/views/prompts/views_agent.md, assembled at build time as base prompt + the date fragment + the SQL kit's UX guardrails. Its load-bearing rules:

"Views are ONE of three answer surfaces you can offer, not your default first move."
"There is NO thisYear, lastYear, or thisQuarter operator — never invent an operator key (the write is rejected with the bad key named)."
"Write your text response FIRST. Then call emit_suggest_replies LAST. Empty bubble + floating chips is a broken UX."

Sections: domain → drafts vs. direct edits → reactive routing (intent rules + a worked classification table) → output contract → tools → argument shapes & operator vocabulary → the hard ordering rule → Path A / Path B turn scripts → chip vocabulary → linking rules → out-of-scope → data-presentation rules.

views_agent.md:1-485 · agent.py:141

TOOLS

24 model-facing tools, 8 groups

Canvas CRUD 7

create_view_draftupdate_view_draftapply_view_draftdiscard_view_draftget_active_draftget_current_viewupdate_view_directly

Routing ladder 5

find_matching_viewfind_matching_insightcan_view_express_querycan_object_type_express_columnsnavigate_to_insights

Fields & listing 3

list_fields_for_object_typelist_supported_object_typeslist_my_views

SQL kit (shared) 3

read_via_sqldescribe_schemadescribe_lookup_table

Navigation 2

return_to_routeropen_view_in_draft_mode

State recording 2

record_ladder_branchrecord_sql_error_category

Chips 1

emit_suggest_replies

Entity links (shared) 1

build_entity_urls

Tool factories close over vendor_id / user_id / agent_state, so the model-facing signatures carry only what the model should decide — identity never appears in a tool argument.

agent.py:99-138 · tools/*.py

RUNTIME

Live process — server-driven, zero model tokens

No "thoughts" stream. A Strands hook maps 13 tool stages to plain-language rows ("Looking into your data", "Building your view") plus two model-phase rows ("Thinking through your question", "Responding"). When the first mapped tool of one of 8 scenarios starts, the whole scenario's rows appear at once — trigger in-progress, the rest pending — so the user sees the full plan immediately.

TickingToolExecutor emits a synthetic heartbeat every 0.12s during tool batches so events drain live instead of bursting at batch completion. Canvas writes emit view_section_snapshot per section, painting the View editor section-by-section before view_refresh_requested reconciles the full state.

progress_stages.py:44-140 · ticking_executor.py:39 · tools/canvas.py:189-226

PROMPT

Time context — a grounded "today"

today_context_fragment() appends the ISO UTC date to the system prompt at agent build (session start), with an explicit instruction not to infer the date from training data — the date alone isn't enough to override the model's prior on "what year it is". This anchors every "this year" / "last month" window the agent computes.

shared/prompts/today_context.py · agent.py:141

RUNTIME

SQL — read-only, allowlisted, categorized

The shared SQL read kit (vendor-user scope) POSTs to Flask's internal SQL endpoint, where the rewriter enforces the ORM-declared column allowlist and tenant scoping. The kit also contributes its own prompt fragment (the SQL UX rules) and a lookup-FK guard hook.

On failure the model records a category — user_scope_denied, column_not_exposed, query_too_complex, internal_error — for telemetry, and the user sees a friendly message, never rewriter vocabulary. Status and picklist filter values are encoded allowed_values ids, never English labels.

shared/tools/sql_read.py:4,162-267 · views_agent.md:141-153,257

CONTRACTS

Internal endpoints — the agent's write surface

MethodPathPurpose
POST/api/internal/views/draftcreate draft (or clone from parent)
GET / PUT / DELETE/api/internal/views/draft/{id}read / merge-update / discard draft
POST/api/internal/views/draft/{id}/applypromote draft to live view
GET / PUT/api/internal/views/{view_id}read / direct-edit a live view
GET/api/internal/views/object-typessupported object types (flag- and role-gated)
GET/api/internal/views/object-fields/{id}fields for an object type
GET/api/internal/views/my-viewscompact view list for duplicate detection

Every id-bearing route also accepts hashids. Auth is a Bearer JWT forwarded from the agent (not Flask-Login): the controller validates the user exists, is active, is vendor-side, and belongs to the claimed vendor — the cross-tenant guard. Errors are typed (401/403 auth, 404 draft/view/parent not found, 409 draft already exists) and tools translate them to friendly strings the model can reason about.

app/api/views/routes.py:50-71,253-336 · controllers/internal_draft_controller.py · agents/views/_views_api.py

CONTRACTS

Response shaping — chips are a locked vocabulary

Chip strings are a case-sensitive contract between the lifecycle, the prompt, and the FE renderer. The lifecycle emits them from the state the user is in next, in five states: terminal (no chips) · draft written this turn (Apply viewChange Discard — lifecycle OWNS these, stray model chips are ignored) · model chips on a no-write turn (model wins) · active draft on a read turn (draft chips again) · proposal-mode fallback (Approve DraftChange Reject).

Ladder branches map to their own chip sets — e.g. sql_inline_offviewSave as a new view; sql_inline_onview_can_expressUpdate this view Create a new view. Matched views and insights are appended as inline markdown links (via build_entity_urls), never navigation chips. The tool accepts at most 4 options of up to 60 characters each.

lifecycle.py:208-327 · views_agent.md:333-396 · tools/emit.py

RUNTIME

Session state & memory

Per-turn, pre_dispatch syncs the FE's ui_context into agent_state (current_view_id, current_view_name, current_object_type_id, is_draft_active), clears the previous turn's ladder branch, and — when the user navigated to a different view — drops active_draft_id and the per-object-type fields_cache.

The draft-id bridge: if the user is already viewing a draft, that draft becomes the active one, so edits update it in place instead of minting a second draft (which the backend rejects with 409). Conversation history is summarized by Strands (ratio 0.3, last 10 messages preserved) on top of AgentCore memory.

lifecycle.py:52-159 · agent.py:143-146,176

OBSERVABILITY

Telemetry — every turn lands on a span

post_turn tags the turn's OTel span with intent.classification (data / edit / create / uncertain — from the recorded ladder branch, or inferred from which write tool fired), ladder.branch (the exact rung token, powering drill-downs into one branch's behavior), and sql.error_category. Matcher calls tag match result, score, top candidate and threshold. EVENT_FIELDS surfaces current_view_id and active_draft_id on session/done events for correlation.

lifecycle.py:35-49,162-205 · agent.py:150-161 · tools/routing.py

Internal documentation — gated behind Cloudflare Access.