PR: #387: feat(agents): Interactive runtime controls — Thinking · Authority · run-control · "+" menu [Story-1565] by @ankitmaheshwari-com
View PR →
Story 1565 · EPIC-1559 Agent runtime · ADR-129

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.

ControlValuesSendsShown in
ThinkingLow · Medium · High · Extra-Highthinking_levelInteractive only
AuthorityReview every step · Review exceptions default · Authorized · Firm policyauthority_mode (+ authorized_scope)Interactive only
Run-control (turn)Stop / cancelrun.stoppedInteractive (while running)
Run-control (batch)Pause · Resume · Stopconsumes run.paused/resumed/stoppedAuto
"+" menuAttach files · agent commands · Connectorsupload+scan path / capability keyInteractive (Connectors too)
No model-name leak. Thinking maps to a {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.

Live-data seam (named, not hidden). These are the presentational controls; wiring 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

Files changed

14 file(s), +1252 / -0 lines

Path Role + -
messages/en.jsonConfig+63-0
messages/hi.jsonConfig+63-0
src/components/agent-controls/agent-composer.stories.tsxComponent+119-0
src/components/agent-controls/agent-composer.tsxComponent+205-0
src/components/agent-controls/agent-controls.fixtures.tsComponent+83-0
src/components/agent-controls/agent-posture.tsComponent+77-0
src/components/agent-controls/authority-dropdown.stories.tsxComponent+47-0
src/components/agent-controls/authority-dropdown.tsxComponent+152-0
src/components/agent-controls/plus-menu.stories.tsxComponent+38-0
src/components/agent-controls/plus-menu.tsxComponent+128-0
src/components/agent-controls/run-control.stories.tsxComponent+35-0
src/components/agent-controls/run-control.tsxComponent+118-0
src/components/agent-controls/thinking-selector.stories.tsxComponent+33-0
src/components/agent-controls/thinking-selector.tsxComponent+91-0