Skip to content

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

Open
jasmine889966 wants to merge 1 commit into
andrewyng:mainfrom
jasmine889966:upstream/approval-intent-analysis
Open

feat: AI command intent analysis on approval prompts#468
jasmine889966 wants to merge 1 commit 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.

The annotation language follows the UI language (English and Chinese templates ship; the GUI re-syncs the language pref when the user switches locales) — building on the i18n system that just landed via #127.

Screenshots

Close-up of the card — the annotation sits between the command preview and the action buttons. Note the bolded consequence ("the existing file is permanently overwritten"), which is exactly the fact a non-shell-literate approver needs and cannot get from the raw command:

Approval card with the two-bullet intent annotation between the command preview and the Allow/Deny buttons

In context, inside the session (English and Chinese UI both supported; the annotation language follows the interface):

Full session view showing the annotated approval card in the transcript

Without the feature (or when the analyzer times out), the card is unchanged: the same raw command, no annotation — strictly no-op.

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-language summary of the consequences turns the approve/deny decision from "do I trust this string?" into "do I accept these outcomes?".

This slots into the direction main has been moving — provenance on the card, egress labels, plain-words scope notes all give the human more context; this PR extends the same idea from where the action goes to what the action will do, per command, in plain language.

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.
  • Runs only when a card will actually be shown. Rebased onto the permission-modes architecture: the analysis sits after the shadow-review spawn and the reviewer's allow/deny, immediately before PERMISSION_REQUIRED is emitted. A call that the reviewer (or a standing rule) resolves never spends the round trip — pinned by test_no_card_no_analysis.
  • Synchronous, before the card. Analysis runs inside _authorize, before the event 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 the event payload alongside the upstream fields (provenance, readonly_ok, standing_target), and PermissionRequest.intent flows into the Inbox snapshot, so unattended/reconnected cards show it too.
  • Bilingual output. The prompt template selects on the annotation language (English default, Chinese shipped); the GUI passes its current locale when toggling and re-syncs on languageChanged.

Notes

  • 20s timeout covers cloud-model tail latency (the analyzer runs the session's own model). 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.

Relation to the permission-modes work (#534)

Composes rather than competes: the reviewer decides (a risk verdict that can turn "ask the human" into "go ahead"), the intent annotation explains (plain-language consequences for the human who still has to decide). When the reviewer clears a call, no card is shown and the analyzer never runs; when the human still decides, they decide informed. The payload field sits beside approval_extras-style upstream data without touching them.

Files

  • coworker/intent_analysis/ — new module: prompts.py (bilingual system prompts), 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 post-reviewer analysis branch in _authorize.
  • coworker/agent.pybuild_engine forwards intent_analyzer.
  • coworker/server/manager.py — injects the analyzer at both build_engine sites when the pref is on (binding the annotation language), carries intent through approval_prompt_data, exposes the pref + language via get_settings / set_intent_analysis.
  • coworker/server/app.pyPOST /v1/settings/intent-analysis.
  • GUI — the annotation on the live card and the parked Inbox card, a Settings toggle, locale keys for both languages, and the languageChanged re-sync.

Tests

  • tests/test_intent_analysis.py (25) — prompt construction incl. language selection/fallback/clamping, input extraction per tool kind, output cleaning, analyzer success/failure/empty/prose-degrade, single-turn + tools-disabled contract.
  • tests/test_engine_intent.py (20) — DI plumbing; the None/success/timeout-no-crash/raises/stop-before/stop-mid branches of _authorize; no-card-no-analysis (standing allow ⇒ analyzer never invoked); payload coexistence with readonly_ok; build_engine passthrough; manager injection on/off with language binding; Inbox carry-through; REST roundtrip incl. the language field.
  • ApprovalCard.test.tsx — annotation renders when present (bold terms emphasized), 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 on top of current main (the analyzer only consumes a provider response on the paths where a card is shown, so scripted-provider approval tests are untouched); frontend tests are type-checked and follow the existing render/getByText pattern.

@jasmine889966
jasmine889966 force-pushed the upstream/approval-intent-analysis branch from f25d271 to 6fcaf4d Compare August 29, 2026 04:36
… UI's language

Rebased onto current main (permission modes / reviewer architecture):

- The analysis now runs only when a card will actually be shown: after the
  shadow-review spawn, right before PERMISSION_REQUIRED is emitted. A call the
  reviewer (or a standing rule) resolves never spends the extra round trip.
- The annotation rides the payload alongside the upstream fields (provenance,
  readonly_ok, standing_target) and PermissionRequest.intent, so parked Inbox
  approvals keep it across reconnects.
- Output language follows the UI language: bilingual prompt templates (en
  default), a language pref beside the toggle, re-synced on languageChanged.
- GUI: restrained annotation block on the live card and the parked card, a
  Settings toggle, i18n keys for both locales. A routine file write upgrades
  from the compact row to the full card so the annotation is visible.
- Timeout/raise/user-stop all degrade to 'no annotation' — the approval flow
  itself never breaks. Off by default; None stays a strict no-op.
@jasmine889966
jasmine889966 force-pushed the upstream/approval-intent-analysis branch from 6fcaf4d to 131b4f7 Compare August 31, 2026 06:37
@jasmine889966

Copy link
Copy Markdown
Author

Rebased onto current main (single clean commit on top of fb1bfc6) — mergeable is green now. Two things worth flagging for review:

  1. New placement, composition with the reviewer: the analysis runs only when a card will actually be shown — after the shadow-review spawn / reviewer verdict, right before PERMISSION_REQUIRED is emitted. A call the reviewer (or a standing rule) clears never spends the extra round trip; there's a test pinning that (test_no_card_no_analysis).
  2. Bilingual output: the annotation language follows the UI language (en/zh templates; re-synced on languageChanged), building on the i18n system from Add GUI internationalization with English and Simplified Chinese #127.

Backend: 45 new tests (tests/test_intent_analysis.py + tests/test_engine_intent.py), full suite green on top of main. Frontend: tsc clean, vitest green, i18n keys in both locales.

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