Counsel gets a steering wheel for the agent
PRAMAAN's agents have been single-shot, fixed-model, fire-and-forget: you
ask, they run, you take what comes. This Story adds the Interactive
controls ratified by ADR-129 — the controls counsel
uses to set posture before and during a turn: how hard the agent
thinks (Thinking), how much it may do before asking
(Authority), the ability to stop a turn
(and pause/resume an Auto batch), and a "+" menu for
attachments and the agent's declared commands. Three orthogonal axes plus
run-control, on one composer row — built on the Atlas drawer substrate so
the modal/page never resizes for the drawer.
🛰️ Satellite — why posture must be a control, not a guess
The stakes: a legal agent that mutates a Record is nothing like a chat toy. A routine batch of 200 uploads wants speed and a light touch; a sensitive privilege call wants the agent to stop and ask. The same agent, minutes apart, needs two completely different postures. If posture is fixed at build time, counsel is stuck either over-trusting (the agent quietly does something it should have flagged) or under-trusting (every reversible rename becomes a confirm dialog). Both erode the product promise — "drop 200 files, review the ~8 that need judgement."
The naive fix bolts on a coding-agent "Full access" toggle and a model dropdown. It detonates twice. First, "Full access" is a blank cheque — exactly the thing you must never give an agent that can mark a filing served or promote firm-wide vocabulary. Second, a model dropdown leaks the cost lever (model name → price → tokens) onto the surface, turning a judgement control into a billing negotiation. The real design separates the axes: Thinking is "how hard," named in plain effort terms with no model, price, or token ever shown; Authority is "how much before asking," with "Authorized" deliberately scoped and expiring, never unrestricted. The UI only requests posture; the broker enforces floors and the non-delegable list.
✈️ Airplane — three dials and a brake on one composer
Think of the agent composer as a cockpit strip beneath the message box. Left to right: a "+" to load fuel (attachments) and select a pre-armed maneuver (commands); a Thinking dial; an Authority dial; and on the right, either Send or — while the engine is running — a Stop brake. Auto mode (the extraction worker doing a batch) takes the dials away entirely and leaves only the brake set: Pause / Resume / Stop.
| Control | Values | Sends | Shown in |
|---|---|---|---|
| Thinking | Low · Medium · High · Extra-High | thinking_level | Interactive only |
| Authority | Review every step · Review exceptions default · Authorized · Firm policy | authority_mode (+ authorized_scope) | Interactive only |
| Run-control (turn) | Stop / cancel | run.stopped | Interactive (while running) |
| Run-control (batch) | Pause · Resume · Stop | consumes run.paused/resumed/stopped | Auto |
| "+" menu | Attach files · agent commands · Connectors | upload+scan path / capability key | Interactive (Connectors too) |
{model, reasoning_effort} pair broker-side. The
surface deliberately carries only the four effort labels — never a model
name, price, or token count (ADR-129 D2). The whole posture vocabulary
lives in the AgentControls i18n namespace (en + hi).
🔬 Microscopic — the pieces, and why each is shaped the way it is
The posture model — request, never enforce
agent-posture.ts defines the three request axes
(ThinkingLevel, AuthorityMode,
AuthorizedScope) with their render orders and the spec
defaults (Thinking = medium, Authority =
review_exceptions). These are client request types,
kept local because the generated agent_invoke.v1 contract
(Story-1561) is not yet codegen'd into this repo's src/gen.
When it lands they collapse onto the generated enums (HR-22). The comment
says so, in the file, so the seam is impossible to miss.
Thinking selector — one dial, four words
A compact dropdown (gauge glyph + current label). The menu lists Low / Medium / High / Extra-High, each with a plain-language effort hint ("Faster, lighter reasoning"). There is no fifth column for "model" because there is no model on this surface, by design.
Authority dropdown — "Authorized" is scoped, not blank
Four modes, each with an executive one-liner. Picking Authorized reveals what makes it safe: a scope picker (this turn / this batch / selected documents / this matter) and an expiry line ("Expires when this batch completes"). "Do it" means this job — never standing delegation. The trigger tints amber when Authorized is active so the elevated grant is visible at a glance. The copy is executive, not "Full access."
Run-control — turn brake vs batch transport
One component, two variants. turn renders a single Stop
(cancel the in-flight turn) — we deliberately do not build
pause/resume for chat. batch renders Pause/Resume + Stop,
driven by the parent-owned BatchRunState that it derives from
the run.paused/resumed/stopped stream. Stop is not rollback —
applied effects stay; undo-batch lives on the Record header.
"+" menu — attachments inherit the scan invariant
The "+" opens to Attach files, the agent's declared commands, and
(Interactive only) Connectors. The component only opens a file picker and
hands the FileList up; the parent gateway routes those bytes
through the same ingestion + GuardDuty malware gate as any upload.
No unscanned bytes reach the brain, and the "+" never invents a capability —
it lists exactly the agent's pre-registered set.
The composer — assembly, and the Auto/Interactive gate
AgentComposer stitches the four controls beneath an
auto-growing textarea. mode is the gate: interactive
shows the dials + "+" and emits the current AgentPosture on
Send; auto returns only the batch run-control — every
selector and the "+" are gone, which is the acceptance criterion "Auto shows
no selector" rendered literally.
Verified in Storybook
Every primitive ships a co-located *.stories.tsx — the
selectors at rest and open, Authorized expanded with scope + expiry, the
batch run-control in running / paused / stopped, the "+" with and without
connectors, and the full composer in Interactive (default + running) and
Auto (running + paused). The components are label-driven (copy passed as
props), so the stories render deterministically without Clerk or a gateway.
onSend →
agent_invoke.v1 through the gateway, and resolving the
AgentControls labels at the live Atlas/Blake route, lands when
the contract is codegen'd into src/gen (depends-on Story-1561 /
rails Story-1562). This Story delivers the controls + posture model + i18n;
the route integration is the follow-up.
What shipped
src/components/agent-controls/agent-posture.ts— the three request axes + defaults.thinking-selector.tsx,authority-dropdown.tsx,run-control.tsx,plus-menu.tsx,agent-composer.tsx— controls + assembly, each with stories.AgentControlsnamespace inmessages/en.json+messages/hi.json.- No model name / price / token on the surface; semantic tokens + Tailwind palette utilities only; a11y labels on every control.
Files changed
14 file(s), +1252 / -0 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +63 | -0 |
| messages/hi.json | Config | +63 | -0 |
| src/components/agent-controls/agent-composer.stories.tsx | Component | +119 | -0 |
| src/components/agent-controls/agent-composer.tsx | Component | +205 | -0 |
| src/components/agent-controls/agent-controls.fixtures.ts | Component | +83 | -0 |
| src/components/agent-controls/agent-posture.ts | Component | +77 | -0 |
| src/components/agent-controls/authority-dropdown.stories.tsx | Component | +47 | -0 |
| src/components/agent-controls/authority-dropdown.tsx | Component | +152 | -0 |
| src/components/agent-controls/plus-menu.stories.tsx | Component | +38 | -0 |
| src/components/agent-controls/plus-menu.tsx | Component | +128 | -0 |
| src/components/agent-controls/run-control.stories.tsx | Component | +35 | -0 |
| src/components/agent-controls/run-control.tsx | Component | +118 | -0 |
| src/components/agent-controls/thinking-selector.stories.tsx | Component | +33 | -0 |
| src/components/agent-controls/thinking-selector.tsx | Component | +91 | -0 |