You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Dangerous declarative actions (`confirmText`, `mode:'delete'`, `variant:'danger'`) can now be exposed to the LLM safely. Instead of being skipped outright, they are registered as tools whose handler enqueues a pending request and returns `{ status: 'pending_approval', pendingActionId }` to the model. A human approves (or rejects) from Studio's pending-actions inbox; the service then re-runs the exact same dispatcher.
9
+
10
+
### New surface
11
+
12
+
- New system object `ai_pending_actions` (id, conversation_id?, message_id?, object_name, action_name, tool_name, tool_input, status [`pending`|`approved`|`executed`|`failed`|`rejected`], result?, error?, rejection_reason?, proposed_by, decided_by?, proposed_at, decided_at?).
13
+
- New built-in Studio view `AiPendingActionView` with `pending` / `executed` / `rejected` / `failed` sub-views and per-row **Approve** / **Reject** API actions.
14
+
- New methods on `IAIService` (all optional, gated on a wired `IDataEngine`):
- New exported predicate `actionRequiresApproval(action)` for Studio's exposure surface.
26
+
27
+
### Wiring
28
+
29
+
`AIServicePluginOptions` gains `enableActionApproval?: boolean` (default `false`). When `true` and an `IDataEngine` is available, dangerous actions are registered and routed through the queue.
30
+
31
+
```ts
32
+
kernel.use(newAIServicePlugin({
33
+
enableActionApproval: true, // opt in
34
+
apiActionBaseUrl: 'http://localhost:3000',
35
+
}));
36
+
```
37
+
38
+
### Internals
39
+
40
+
-`actionSkipReason()` accepts `enableActionApproval` + `aiService` in its ctx and stops returning `"requires confirmation"` / `"mode='delete'"` / `"variant='danger'"` when HITL is wired.
41
+
-`registerActionsAsTools()` pre-registers a *bypass-approval* dispatcher per dangerous tool via `aiService.registerPendingActionDispatcher(toolName, fn)`; approval calls back into the same code path with `enableActionApproval` flipped off, so a single handler implementation serves both proposal and execution.
42
+
-`createActionToolHandler()` short-circuits to `proposePendingAction()` when `enableActionApproval && actionRequiresApproval(action) && ctx.aiService?.proposePendingAction`.
43
+
44
+
### Out of scope (deferred)
45
+
46
+
Slack/email notifications, approver routing (any signed-in user can approve in v1), auto-expiry of pending requests, resuming the same LLM turn after approval (operators get a fresh assistant message instead).
5.[Natural Language Queries](#natural-language-queries)
17
18
6.[Predictive Analytics](#predictive-analytics)
@@ -371,7 +372,7 @@ The AI runtime walks every registered object's `actions[]` and exposes the ones
371
372
|`api`| HTTP call to `action.target` via `ApiActionClient` (default: `fetch`) |`apiBaseUrl` (or a custom `apiClient`) |
372
373
|`flow`|`IAutomationService.execute(target, { triggerData })`|`automation` service registered |
373
374
374
-
Studio-only types (`url`, `modal`, `form`) and any action with `confirmText`, `mode: 'delete'`, or `variant: 'danger'` are skipped — those require Phase-3 HITL approval. Owners can also opt out per action with `aiExposed: false`.
375
+
Studio-only types (`url`, `modal`, `form`) are always skipped. Dangerous actions — those with `confirmText`, `mode: 'delete'`, or `variant: 'danger'`— are skipped **by default**, but can be opted into the [HITL approval queue](#human-in-the-loop-approval) so an operator authorises every call. Owners can also opt out per action with `aiExposed: false`.
375
376
376
377
### Wiring it up
377
378
@@ -405,6 +406,33 @@ For api actions, the request body is built from three sources (last wins):
405
406
406
407
`registerActionsAsTools()` returns `{ registered, skipped }`. Studio surfaces the skipped list with reasons (e.g. `"no apiClient or apiBaseUrl configured"`, `"mode='delete' — destructive"`) so action authors can see at a glance whether their action will be LLM-callable.
407
408
409
+
### Human-In-The-Loop approval
410
+
411
+
Destructive actions are too risky to let the LLM execute autonomously, but locking them away entirely defeats agentic UX. The HITL queue strikes the balance: the LLM gets to **propose** the call, a human gets to **approve** it.
412
+
413
+
Enable the queue via the plugin option (default: off):
When the LLM picks an approval-gated tool (e.g. `action_delete_task`), the runtime:
425
+
426
+
1. Persists an `ai_pending_actions` row with `status:'pending'`, `tool_input`, `proposed_by`, etc.
427
+
2. Returns `{ status: 'pending_approval', pendingActionId }` to the model so it can summarise (e.g. "I've requested approval to delete task #42.").
428
+
3. The operator sees the proposal in the **AI Pending Actions** Studio inbox.
429
+
4. Clicking **Approve** calls `POST /api/v1/ai/pending-actions/:id/approve` (`ai:approve` permission). The service looks up the pre-registered dispatcher and executes the action with HITL routing disabled, so the same code path runs. The row transitions to `executed` (or `failed` if dispatch threw).
430
+
5. Clicking **Reject** calls `POST /api/v1/ai/pending-actions/:id/reject` with an optional reason.
431
+
432
+
The queue is exposed via four REST endpoints (`GET`, `GET/:id`, `POST/:id/approve`, `POST/:id/reject`) and via `IAIService.{proposePendingAction,approvePendingAction,rejectPendingAction,listPendingActions}` for programmatic use.
433
+
434
+
**Why a dedicated queue (not the multi-step `IApprovalService`)?** AI tool-call HITL is ephemeral: subject is the proposed *call*, not a stable record state; there's no predefined process; operators expect single-click yes/no. The pending-action queue is a thin write log + dispatcher map that delivers that UX without dragging in process-engine overhead.
0 commit comments