Agentic UI in Angular: a map portal assistant that keeps the human in charge
The useful part
An assistant needs an action boundary, visible outcomes and a way to stop. Start with a task the user can already complete by hand.
An assistant inside a map portal should help a person find a place, select a layer or open a tool without losing control of the application. This article describes the experimental assistant in GeoAtlas, clearcraft’s own product: its command boundary, confirmations, cancellation and evaluation. The architecture is reusable; the product counts and results below are dated snapshots.

The point of an assistant is a faster person
An assistant earns its place when it removes steps, not when it replaces judgement. In a map portal the steps are concrete: find the right module, open the right panel, pick a layer among dozens, choose a paper size. A power user does this with the keyboard; a new user clicks through menus; either may benefit from expressing a task in their own words. The assistant is a third door to the same actions, next to the menus and the command palette (Ctrl/Cmd+K, or "/" outside a text field). Its purpose is to shorten a workflow using actions already available by hand. Whether it actually saves time needs to be measured with users. That is the first design decision, not a limitation.
One command core, three front doors
Everything the assistant can do is a command, and a command is data: an identifier, a title, typed parameters, an availability rule, a preview, a run function, a summarize function that produces the only text the model is allowed to see about the result, and, where it makes sense, an undo. Each command also declares its effect (navigate, toggle or sideEffect) and when it has to be confirmed. The 1 October 2026 snapshot of the GeoAtlas registry contains 54 commands: one per main-menu feature plus parameterised ones such as zooming to a place.
The command core is a pure TypeScript package with no Angular in it. Three consumers sit on top: the command palette, a single-turn palette assistant, and the multi-step chat. All three go through one method, engine.invoke, and every invocation records its origin: user, model or agent. These assistant integrations invoke the engine rather than executing a model-provided function directly. That shared boundary is testable; it does not replace authorization on a server or make every application method inaccessible.
The model proposes, the engine decides
The engine, not the prompt, decides what happens with a model's proposal:
- A navigation or a reversible toggle proposed by the model runs at once and appears as an action card. Commands with an implemented undo expose that action on the card; do not imply that every navigation or external effect is reversible.
- A side effect proposed by the model always asks for confirmation. The command's own "confirm when" rule, which lets a human skip the question in the palette, is not sufficient to bypass confirmation for model or agent origins. Commands that own a confirmation dialog must enforce it themselves. The model cannot talk its way past this; the rule is a line of code in the engine.
- Some commands are not offered to the model at all: free-text search, the two report forms, the accessibility statement and the chat itself. A red line is drawn by omission. The allowlist prevents direct invocation of those commands through chat. It narrows capability; it is not a blanket prompt-injection guarantee.
- Two commands end the turn when they run (zoom to a place, open the print editor), because after them the next decision belongs to the person.
Example: a place request followed by printing
- The person asks to go to Poronin. If the location is ambiguous, the interface asks them to choose before navigating.
- The place command finishes the turn. Its action card reports the outcome and exposes undo where supported.
- The person starts a new turn to open the print editor, then reviews the page settings. Opening an editor is distinct from exporting or submitting a job.
This illustrates the intended interaction. A single sentence containing several tasks is not a guarantee that the assistant will complete all of them; terminal commands and evaluations define the actual limits.
Undo and Stop never go through the model
The two most important buttons in an agentic interface are deterministic. Undo pops the last entry of an in-memory stack of twenty and calls the command's undo directly; after a reload the stack is empty. Stop, or the Escape key, cancels the current run and signals abort to the request and cooperating tools. It does not reverse a completed side effect or guarantee that a remote server stopped work. The UI must distinguish cancelled work from work already completed. Neither control asks the model to approve the request. A person who has changed their mind must not depend on a model's willingness to comply.
The wire: AG-UI events, state signed by the gateway
The browser never talks to a model provider. It talks to the application's own gateway, a small Node service, over server-sent events. The event vocabulary is the one defined by the AG-UI protocol (run started, text message content, tool call start, arguments and result, run finished), which keeps the client independent of the provider and makes recordings and mocks straightforward.
The gateway runs the tool loop; the browser executes the tools, because the tools are the application's commands and need the application's state. Between turns the conversation lives in an HMAC-signed state token issued by the gateway (valid for thirty minutes, at most 48 kB), so modified signed history is rejected. The gateway validates tool definitions against its allowlist separately. A signature protects integrity, not confidentiality or the truth of browser-reported results; server-side authorization and validation are still required. Model providers sit behind the gateway only: an OpenAI-compatible endpoint by default, which in my deployment is an open-weights model served on my own hardware, with Anthropic and Codex adapters present but switched off. The choice between a local and a cloud model is configuration, not a rewrite.
What leaves the browser
Typing in the chat sends nothing. A message is sent when the person sends it, together with the visible context chips: the active module, the view box rounded to three decimals, the selected parcel, the last place searched, up to twelve layer names and the size of the current parcel set. A "show what I send" view displays the exact JSON. Tool results are clamped before the model sees them, to limit application-generated results to identifiers and short summaries. The person’s own message is still sent and may contain a URL or other sensitive text; result filtering does not sanitize everything a user types. The gateway logs metadata only: a hashed client address, provider, model, tool name, latency and token counts. The conversation stays in the tab's session storage with a retention setting; the command history and the assistant log stay on the device.
Guardrails that are code, not prompts
The prompt treats command text as data. Enforced controls belong in the engine and gateway:
- an allowlist of tool names, kept equal to the registry by a unit test, and a narrower list for the chat;
- grounding: a parcel identifier, a layer name or a place must come from the person's text, the context chips or an earlier result, otherwise the grounding check rejects it. Matching text reduces invented parameters but does not prove that an action is authorized or semantically correct;
- limits: six requests per minute for the palette, thirty per minute and 60,000 tokens per conversation for the chat, an optional daily cost cap, an origin allowlist;
- a step budget of eight tool calls per turn (hard cap twelve), after which the person decides whether to continue;
- parameter validation by the engine (required, range, unresolved references) and an interpreter that keeps declared parameters only;
- a sixty-second limit per model call, three minutes per turn, and a stop after the same failing error code twice.
When the model is not there
An assistant that is off must not look broken. The feature has a setting (on by default), the gateway has a switch and a configured provider, and the client reads a status endpoint before it offers anything. When either is off, or the browser is offline, the client reports "unavailable" and the application is simply a map portal with menus and a palette. Error states have names (disabled, rate-limited, budget-exhausted, unavailable, refused, invalid-response, login-required, starting), and each has a sentence in the interface rather than a spinner.
Testing an assistant: seams, recordings, evals
The 1 October 2026 implementation inventory describes three test seams. A separate, small model evaluation measures behaviour on chosen scenarios; it is not a production success-rate estimate:
- Pure functions. The command core, the chat reducer and the session driver have dedicated unit coverage. A source count describes the test inventory; acceptance requires the actual result for a named revision.
- Recordings. Fifteen recorded conversations, some captured live from the local model on 30 September 2026 and some hand-written, are replayed without a network in a unit test and as real server-sent events against a mocked gateway in the end-to-end suites (18 tests across chat, palette and the single-turn assistant). The assistant screenshot in the case study is such a replay, and says so.
- Contracts. The allowlist equals the registry, the wire parser accepts the recorded bytes, the reducer never reaches an impossible state. These are the tests that fail first when someone adds a command and forgets the rest.
- Evals. Model quality is measured, not unit-tested: the recorded palette run on 29 September 2026 passed 30 of 30 phrases with no wrong side effect and a median latency of 1.7 seconds. The chat design record reports 12 of 15 scenarios, rescored to 13 of 15 after correcting the judge. It records no unconfirmed side effects in that sample, and also describes an unrequested action and an invented locality. These are different failure classes: a parcel-ID check alone does not prevent an invented place name. The failures are a list of cases to improve, not a reason to loosen a guard.
What it buys the user, and what it does not
For a person, the assistant is a shortcut with a safety net: fewer clicks, every action visible as a card, every consequential action confirmed, undo available for commands that support it. For the product, it is a feature that can be switched off, measured, replayed in tests and moved to another model provider without touching the Angular code. What it is not is an agent that acts on its own. The person starts every turn, approves every side effect and can stop at any moment. That is the whole point, and it is also why the feature could ship.
Where the standards fit
AG-UI defines events for runs, streamed text and tool calls. GeoAtlas uses that vocabulary over SSE with a custom gateway and signed resume state; that alone is not a claim of drop-in compatibility with every AG-UI client.
MCP and MCP Apps address a different boundary: tools available to an external host and, for Apps, an interface rendered by that host. Angular’s experimental WebMCP APIs expose browser-side tools to supporting agents. These adapters are not implemented in the GeoAtlas snapshot described here. They would need their own permissions, confirmation and compatibility checks; a reusable command core is a starting point, not a one-day delivery promise.
The vocabulary of this article (approval, action card, red lines by omission, tests at the seams) follows Manfred Steyer's book "Agentic UI with Angular", which informed the design vocabulary; the implementation and the measurements are from this product. The GeoAtlas case study shows the assistant in the product, and the agentic UI service page describes how I build the same structure into an existing application.
Sources and scope
Product details refer to the implementation described on 1 October 2026. The small evaluations have their own dates and limits; protocols were rechecked on 2 October 2026.