Skip to content

feat(ai-studio): human decision node. A run parks on the canvas until a person decides - #154

Open
piotrblaszczyk wants to merge 7 commits into
feat/human-in-the-loopfrom
feat/WB-498-human-decision-node
Open

piotrblaszczyk wants to merge 7 commits into
feat/human-in-the-loopfrom
feat/WB-498-human-decision-node

Conversation

@piotrblaszczyk

Copy link
Copy Markdown
Contributor

What

A node an author drops on the AI Studio canvas that stops the run until a person decides. The worker executor returns { waiting: true } (durable pause, #120), the node carries a decisionRequest (#127), and POST /api/executions/:id/decision (#133) resumes it. The canvas shows the wait; the "Refund Review" template shows the whole loop. No engine or backend code changes.

Changes

  • apps/execution-worker
    • domain/ai-studio-nodes.ts: HumanDecisionNode with an empty config (the request travels on BaseNode.decisionRequest).
    • executors/human-decision.ts: returns { waiting: true }; a node without a request throws PermanentNodeExecutionError('decision_request_missing') instead of parking a run nobody can resolve. First classified error in the worker.
    • engines/temporal/worker.ts: registry entry ai-studio/human-decision.
  • apps/ai-studio/src/nodes/human-decision/
    • schema.ts: sharedProperties + decisionRequest: { type: 'object', properties: {} }, required: ['decisionRequest'].
    • uischema.ts: title only (follow-up: decision-request-properties-ui).
    • default-properties-data.ts: preset request, version 1, approve -> source:inner:approved (resume), reject -> source:inner:rejected (reject, reasonRequired: false), empty form. Ports come from getHandleId.
    • index.ts: palette item ai-studio/human-decision, icon UserCheck, outputSchema with action and effect for the variable picker; humanDecisionNodeType shared with app.tsx.
    • human-decision-template.tsx + CSS: own template via defineNodeTemplate, registered under the palette type in nodeTemplates on WorkflowBuilder.Root. One source handle per action that carries a port, id = port; one target handle; rows in NodeSection inside OptionalNodeContent, where the execution markers mount. Honours layoutDirection. Palette preview renders the header only. Malformed or missing request: header and target handle, no source handles.
  • apps/ai-studio/src/stores/use-execution-store.ts: node status waiting from node_waiting. Run status derived: waiting while any node waits, running once the last resolves, terminal events win. Same rule on the live path and on the snapshot after a reload (the engine's status write is advisory and may be skipped).
  • components/execution/node-markers.tsx: HourglassMedium for waiting, not clickable. highlighting.tsx: waiting keeps the active shadow. controls/ai-studio-controls.tsx: waiting counts as running, so a parked run shows Stop. highlighting.css / log-panel.module.css: --ai-studio-status-color--waiting from --ax-txt-info-default, badge node_waiting, status pill waiting.
  • apps/ai-studio/src/data/refund-review-flow.ts: template id 306, icon Receipt. trigger -> AI "Draft the Refund Reply" -> "Review Refund" -> approved -> AI "Send the Confirmation" -> visualize (markdown); rejected -> visualize (json). The node's request adds the refund form: refundAmount (number, required), orderDate (read-only), note. Both edges drawn, exactly one predecessor.
  • READMEs: AI Studio node list and one paragraph on what parks and resumes the run; worker note on classified errors.

Decisions worth a look

  • Own template over the SDK DecisionNode template: no "Add branch" placeholder, no decisionBranches, the port is written once, in the request. Handle ids keep the SDK shape source:inner:<action> so edge z-index and id helpers stay on their known path.
  • Executor without a request fails permanently. Parking would hang the run forever; the endpoint answers node_not_waiting for such a node.
  • Waiting marker and Stop button land here, not in the decision panel task: seeing the wait is half the demo.
  • Run status derived client-side, mirroring the engine's parked counter, set-based so duplicated or replayed events cannot drift. applySnapshot uses the same rule as applyEvent.
  • Preset form empty; the refund form lives only in the template's node.
  • Reject with no reject edge still ends the run incomplete (WB-499 is next).

Known gaps, unchanged on purpose

  • Nothing in the UI posts the decision yet; the loop closes with curl. Panel is a separate task (WB-502 and its designed successor).
  • Every Play creates a new workflow row and the executionId is not persisted, so a reload loses the run view. Separate task.
  • Authoring the request in the properties panel (follow-up: decision-request-properties-ui).
  • Node markers ignore terminal events: after cancel a waiting node keeps its hourglass, as a running node keeps its spinner today.
  • Dark-theme colour of the waiting token not checked.

Notes for the reviewer

  • No changeset: both apps are private.
  • Tests: worker 13 (+3), AI Studio 49 (+40), full suite 977 across 8 workspaces. Plan § 7 run by hand on the stack, every step as expected.
  • Deviations from the plan, all forced by the code: properties: {} on the request field (NodeSchema requires it), the preset shape pinned directly instead of importing the backend zod schema (AI Studio has no zod), own action rows because ConnectableItem is not a public SDK export.
  • Vocabulary: no "gate" in product code; test fixtures inherited from feat(temporal): durable pause. A run stops at a node and waits for a human verdict #120 keep theirs.

@piotrblaszczyk
piotrblaszczyk marked this pull request as ready for review September 15, 2026 13:05
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