Skip to content

feat: AI command intent analysis on approval prompts - #468

Open
jasmine889966 wants to merge 6 commits into
andrewyng:mainfrom
jasmine889966:upstream/approval-intent-analysis
Open

feat: AI command intent analysis on approval prompts#468
jasmine889966 wants to merge 6 commits into
andrewyng:mainfrom
jasmine889966:upstream/approval-intent-analysis

Conversation

@jasmine889966

@jasmine889966 jasmine889966 commented Aug 7, 2026

Copy link
Copy Markdown

What

Before surfacing an approval prompt, ask the model to summarize what the command will do in a couple of bullet points, so the consequences are clear at a glance.

When the engine decides a tool call needs_user, it runs one extra single-turn provider.complete (tools disabled, mirroring the compaction summarize_span pattern) and attaches the result as intent on the PERMISSION_REQUIRED event. The approval card renders it as a short, restrained annotation — transparent background, a small CSS dot, secondary-grey text, with the most critical terms bolded.

Why

Approval prompts today show the raw command and ask the user to decide. For anyone who isn't fluent in shell (or the specific tool's args), "is this safe to allow?" is a real question. A two-bullet, plain-English summary of the consequences turns the approve/deny decision from "do I trust this string?" into "do I accept these outcomes?".

How

  • Dependency injection, off by default. TurnEngine gains an optional intent_analyzer callable (default None = feature off, upstream behavior unchanged). None short-circuits before any work, so this is a strict no-op for callers that don't opt in.
  • Opt-in. The feature defaults to off; users enable it in Settings ("Explain commands before I approve them"). This keeps it a strict no-op for existing tests (which drive approvals through scripted providers) and for users who don't want the extra round trip.
  • Synchronous, before the card. Analysis runs inside _authorize, before PERMISSION_REQUIRED is emitted, so the card appears already annotated (no loading flicker). The call sits inside _interruptible so a user Stop resolves immediately, and a wait_for timeout is swallowed by try/except — the approval flow never crashes on a slow/failed analysis (it degrades to no annotation).
  • Carried through every path. The annotation rides on both the live event payload and PermissionRequest.intent, and approval_prompt_data writes it into the Inbox snapshot, so unattended/reconnected cards show it too.

Notes

  • 20s timeout covers cloud-model tail latency (the analyzer runs the session's own model, which may route through a provider with non-trivial first-token latency). The approval flow is unaffected on timeout — it just renders without the annotation. Happy to tighten if a smaller bound is preferred.
  • The opt-in default was chosen after CI showed that defaulting to on consumed a scripted provider response in upstream approval tests. Flipping back to opt-in fixed them.

Files

  • coworker/intent_analysis/ — new module: prompts.py (English system prompt), analyzer.py (extract the operation description from the tool call, run one round trip, clean to bullet lines).
  • coworker/engine.pyPermissionRequest.intent, TurnEngine.__init__ params, the _authorize analysis branch.
  • coworker/agent.pybuild_engine forwards intent_analyzer.
  • coworker/server/manager.py — injects the analyzer at both build_engine sites when the pref is on, carries intent through approval_prompt_data, exposes the pref via get_settings / set_intent_analysis.
  • coworker/server/app.pyPOST /v1/settings/intent-analysis.
  • GUI — render the annotation on the live card and the Inbox card, plus a Settings toggle.

Tests

  • tests/test_intent_analysis.py — prompt construction, input extraction, output cleaning, analyzer success/failure/empty.
  • tests/test_engine_intent.py — the None/success/timeout-no-crash/raises/stop branches of _authorize, build_engine passthrough, manager intent carry-through, and the REST toggle roundtrip.
  • ApprovalCard.test.tsx — annotation renders when present, is absent when missing, and forces the full card for routine file writes (which otherwise render as a compact one-line row).

All backend tests pass (including the upstream test_durable_resume / test_ui_refresh approval tests); frontend tests are type-checked and follow the existing render/getByText pattern.

malin1997 added 6 commits August 7, 2026 16:38
Add an optional intent_analyzer callable to TurnEngine (dependency
injection, default None = feature off, upstream behavior unchanged).
Before emitting PERMISSION_REQUIRED, _authorize synchronously calls
the analyzer to produce a consequence summary; the result is attached
to both the event payload (as 'intent') and the PermissionRequest, so
surfaces can render it on the approval card.

Robustness:
- wait_for timeout is wrapped in try/except so a TimeoutError never
  crashes _authorize (analysis degrades to intent=None).
- The call runs inside _interruptible, so a user Stop during analysis
  resolves immediately; if Stop fired, no card is surfaced (the turn
  goes straight to the interrupted path, avoiding flash-then-deny).
- intent_analyzer_timeout defaults to 20s (cloud-model tail latency);
  tests can inject smaller values.

PermissionRequest gains an optional 'intent' field so approvers /
Inbox snapshots carry the annotation through every park path.
Add coworker/intent_analysis/ — a small module that turns a tool call
into a short, plain-English consequence summary via a single blocking
provider.complete call (tools disabled, mirroring compaction's
summarize_span pattern):

- prompts.py: English system prompt (bullet points, bold for the most
  critical terms, severity emphasis for dangerous operations) + user
  prompt builder. No language routing — upstream is English-only.
- analyzer.py: extract_input() shapes the tool call into a structured
  description (run_shell command / file path / message target);
  analyze() runs one round trip and cleans the output to bullet lines,
  returning None on any failure or empty result.

Wiring:
- agent.build_engine accepts and forwards intent_analyzer.
- SessionManager injects analyze() at both build_engine sites when the
  'intent_analysis' pref is on (default on), carries request.intent
  through approval_prompt_data so Inbox snapshots stay annotated, and
  exposes the pref via get_settings + set_intent_analysis.
- POST /v1/settings/intent-analysis toggles it.

The pref defaults to True; timeout defaults to 20s (covers cloud-model
tail latency). Both are readily adjustable if maintainers prefer opt-in
or a tighter bound.
Render the 'intent' annotation from PERMISSION_REQUIRED on both the live
ApprovalCard and the parked InboxItemCard:

- types.ts: optional intent?: string on ApprovalItem.
- App.tsx: read d.intent when building the approval item.
- ApprovalCard.tsx: renderIntentText() strips a leading bullet marker
  (the dot is CSS) and turns **bold** into <strong>; the full card shows
  an <ul> of intent lines under the title. Routine file writes that
  normally render as a compact one-line row fall back to the full card
  when an intent is present (the row can't show it).
- InboxItemCard.tsx: same <ul> for parked approvals (reads item.data.intent).
- styles.css: .approval-intent* — transparent background, a 4px CSS dot,
  secondary-grey body text, body-ink bold spans.
Add an 'Explain commands before I approve them' checkbox in Settings,
modeled on the existing context-bar toggle (local useState + getSettings
+ setIntentAnalysis). When off, the manager stops injecting the analyzer
and approval cards render without the annotation.

- api.ts: intent_analysis? on ModelSettings + setIntentAnalysis().
- SettingsView.tsx: IntentAnalysisCard, mounted next to ContextBarCard.
- test_intent_analysis.py: prompts (rules/clamp/truncation/None),
  extract_input (shell/file/send/fallback), _clean (fences/labels/empty),
  analyze (positional signature, success, provider error, empty output).
- test_engine_intent.py: PermissionRequest.intent + TurnEngine fields,
  _authorize branches (None/success/timeout-no-crash/raises/stop-before/
  stop-mid-flight), build_engine passthrough, manager approval_prompt_data
  intent carry-through, get_settings field, and the REST toggle roundtrip.
- ApprovalCard.test.tsx: intent block renders when present, absent when
  missing, and forces the full card for routine file writes.
- pyproject.toml: register the 'slow' marker used by the timeout/stop tests.
Defaulting the pref to True broke upstream tests that drive approvals
through a ScriptedProvider (test_durable_resume,
test_ui_refresh_cross_cutting_e2e): the analyzer consumed a scripted
provider.complete response before the model could use it, so the
post-approval reply never landed.

Making it opt-in (default False) keeps the feature a strict no-op for
existing tests and users unless they explicitly turn it on in Settings.
The PR description is updated to reflect opt-in as the default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant