[Blazor] Add Components.AI human approval flows - #68329
Open
javiercn wants to merge 4 commits into
Open
Conversation
javiercn
force-pushed
the
javiercn-components-ai-05-human-in-loop
branch
from
August 11, 2026 07:05
ad5d7bf to
7b86b12
Compare
Member
Author
|
/azp run aspnetcore-ci |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
javiercn
force-pushed
the
javiercn-components-ai-05-human-in-loop
branch
from
August 12, 2026 13:01
7b86b12 to
b56220d
Compare
3 tasks
Rationale: let provider-neutral Components.AI conversations surface model approval requests and pause without coupling the runtime to AG-UI or executing gated work prematurely. Implementation: map approval requests ahead of generic server tools, preserve nested function blocks and call IDs, expose single-use approve and reject responses, and resume AgentContext with the correct user-role continuation while retaining existing UI-action ordering. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 323b0ef8-7905-4041-8388-a08586c0bd34
Rationale: demonstrate the canonical Human in the Loop scenario across the real DojoClient to AGUIDojoApi HTTP/SSE boundary while keeping model execution and replay on the API side. Implementation: add the /human_in_the_loop endpoint and keyed AGUIChatClient, declare generate_task_steps as a browser UI action, render selectable task steps, and return approved selections or rejection before the model continuation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 323b0ef8-7905-4041-8388-a08586c0bd34
Rationale: lock down approval lifecycle semantics and the canonical Human in the Loop browser flow without replacing DojoClient's real AG-UI transport. Implementation: cover one-shot approval responses, exact call-ID pairing, user-role continuation, multiple streamed approvals, nested custom blocks, and approve/select plus reject behavior through the dual-host E2E harness. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 323b0ef8-7905-4041-8388-a08586c0bd34
Rationale: keep deterministic Human in the Loop browser coverage independent of live model credentials while exercising the real AG-UI HTTP/SSE boundary. Implementation: record the task-step proposal plus approved-selection and rejected continuations, including exact generate_task_steps call IDs and AG-UI 0.0.5 result payloads. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 323b0ef8-7905-4041-8388-a08586c0bd34
kotlarmilos
force-pushed
the
javiercn-components-ai-05-human-in-loop
branch
from
August 12, 2026 16:47
b56220d to
abd4b1f
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Adds provider-neutral “human approval” (HITL) support to Components.AI by introducing an interactive-block contract, mapping ToolApprovalRequestContent into a new approval block type, and wiring conversation pause/resume semantics end-to-end (including a DojoClient scenario and E2E replay coverage).
Changes:
- Introduce
IInteractiveBlockand new interactive block types (FunctionApprovalBlock,InteractiveFunctionBlock) to unify “pause until user responds” semantics. - Add
FunctionApprovalHandler+ nested mapping support (BlockMappingContext.CreateInnerBlock) and updateAgentContextto wait for interactive results and choose continuation role. - Add a canonical DojoClient “Human in the Loop” scenario plus E2E tests/recordings and supporting UI/CSS.
Show a summary per file
| File | Description |
|---|---|
| src/Components/AI/testassets/DojoClient/Program.cs | Registers a keyed chat client for the new HITL scenario endpoint. |
| src/Components/AI/testassets/DojoClient/DojoScenarios.cs | Adds the /human_in_the_loop scenario route constant. |
| src/Components/AI/testassets/DojoClient/Components/Scenarios/HumanInTheLoop/TaskStepList.razor.css | Adds scenario-specific task-card styling for selecting/rejecting steps. |
| src/Components/AI/testassets/DojoClient/Components/Scenarios/HumanInTheLoop/TaskStepList.razor | Implements the task-step selection UI and serializes selection back through a UI action tool call. |
| src/Components/AI/testassets/DojoClient/Components/Scenarios/HumanInTheLoop/TaskStep.cs | Defines the task-step payload model (description/status) for tool args/results. |
| src/Components/AI/testassets/DojoClient/Components/Scenarios/HumanInTheLoop/HumanInTheLoopScenario.razor | Adds the Dojo scenario page and registers the generate_task_steps UI action. |
| src/Components/AI/testassets/DojoClient/Components/Pages/Home.razor | Links the new scenario from the Dojo home page. |
| src/Components/AI/testassets/DojoClient/Components/_Imports.razor | Imports the new scenario namespace for Razor compilation. |
| src/Components/AI/testassets/DojoClient.E2E.Tests/Tests/HumanInTheLoopScenarioTests.cs | Adds Playwright E2E coverage for approve/select and reject-all flows. |
| src/Components/AI/testassets/DojoClient.E2E.Tests/ServiceOverrides/RecordedScript.cs | Extends the recording schema to optionally assert exact tool results. |
| src/Components/AI/testassets/DojoClient.E2E.Tests/ServiceOverrides/RecordedChatClient.cs | Adds assertions to compare actual tool results against the baseline recording. |
| src/Components/AI/testassets/DojoClient.E2E.Tests/ServiceOverrides/DojoModelOverrides.cs | Adds a model override entry for the HITL recording. |
| src/Components/AI/testassets/DojoClient.E2E.Tests/Baselines/HumanInTheLoop.recording.json | Adds a replay recording baseline validating tool-call/result pairing and continuations. |
| src/Components/AI/testassets/AGUIDojoApi/ScriptedChatClient.cs | Extends scripted model behavior to emit generate_task_steps and summarize tool results. |
| src/Components/AI/testassets/AGUIDojoApi/Program.cs | Maps a new Dojo API endpoint for the HITL scenario. |
| src/Components/AI/testassets/AGUIDojoApi/ChatClientAgentFactory.cs | Adds a system prompt for HITL planning behavior/tool usage. |
| src/Components/AI/test/Pipeline/FunctionApprovalHandlerTests.cs | Adds unit tests for approval mapping and nested custom function block preservation. |
| src/Components/AI/test/Engine/AgentContextApprovalTests.cs | Adds engine tests ensuring approval continuations resume with ChatRole.User. |
| src/Components/AI/test/Blocks/FunctionApprovalBlockTests.cs | Adds unit tests for one-shot approve/reject behavior and reason preservation. |
| src/Components/AI/src/wwwroot/ai-chat.css | Adds shared UI styles for approval rendering and a primary button variant. |
| src/Components/AI/src/PublicAPI.Unshipped.txt | Updates public API surface for approvals, interactive blocks, and nested mapping. |
| src/Components/AI/src/Pipeline/FunctionApprovalHandler.cs | Adds a handler that maps ToolApprovalRequestContent into FunctionApprovalBlock instances. |
| src/Components/AI/src/Pipeline/BlockMappingPipeline.cs | Inserts the approval handler into the mapping precedence and passes handlers into context. |
| src/Components/AI/src/Pipeline/BlockMappingContext.cs | Adds CreateInnerBlock to map nested tool calls through existing handlers. |
| src/Components/AI/src/Engine/AgentContext.cs | Generalizes “wait for UI actions” into “wait for interactive blocks” and chooses continuation role. |
| src/Components/AI/src/Components/MessageListContext.cs | Adds built-in rendering for approval blocks (tool name, args, approve/reject actions). |
| src/Components/AI/src/Blocks/UIActionBlock.cs | Implements IInteractiveBlock and makes GetResultAsync public. |
| src/Components/AI/src/Blocks/InteractiveFunctionBlock.cs | Introduces a base wrapper for interactive blocks that represent function invocations. |
| src/Components/AI/src/Blocks/IInteractiveBlock.cs | Adds the shared “pause until result” contract for interactive blocks. |
| src/Components/AI/src/Blocks/FunctionApprovalBlock.cs | Adds the approval block implementation (one-shot approve/reject producing MEAI response content). |
| src/Components/AI/src/Blocks/ApprovalStatus.cs | Adds a public approval status enum (Pending/Approved/Rejected). |
Review details
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 31/31 changed files
- Comments generated: 3
- Review effort level: Lite
Comment on lines
+27
to
+30
| var functionResult = messageList[^1].Contents | ||
| .OfType<FunctionResultContent>() | ||
| .Any(result => result.CallId == "backend-tool-weather-1"); | ||
| .SingleOrDefault(); | ||
| var response = functionResult switch |
| MessageId = Update.MessageId, | ||
| Contents = [content], | ||
| }; | ||
| var context = new BlockMappingContext(update); |
Comment on lines
+42
to
+49
| <div class="task-steps-card__actions"> | ||
| <button class="task-steps-btn task-steps-btn--reject" @onclick="RejectAsync"> | ||
| Reject | ||
| </button> | ||
| <button class="task-steps-btn task-steps-btn--confirm" @onclick="ConfirmAsync"> | ||
| Confirm | ||
| </button> | ||
| </div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This is position 5 of native stack #68340 and depends on #68327. Relative to
javiercn-components-ai-04-server-tools(8c3be521) only, it adds provider-neutral handling forToolApprovalRequestContent, then lights the canonical Human in the Loopgenerate_task_stepsexperience across the existing DojoClient → AGUIDojoApi HTTP/SSE boundary. The cross-cutting constraint is that approval semantics stay inMicrosoft.AspNetCore.Components.AIwhile AG-UI-specific endpoint/client/replay code stays in test assets; the browser continues to use the realAGUIChatClient.Design
The conversation engine now waits on one small public contract rather than knowing every interactive block type. The existing
UIActionBlockand newFunctionApprovalBlockare the two implementations in this layer: both asynchronously produce continuation content, but UI actions produceFunctionResultContentforChatRole.Tool, while approvals produceToolApprovalResponseContentforChatRole.User.The approval contract preserves the original request and the already-mapped function block.
InteractiveFunctionBlockexposes the wrapped call/result/tool/arguments uniformly, so a source-generated or consumer-defined function renderer can remain nested inside the approval UI instead of being flattened into an untyped fallback.The rejected alternative was to special-case approvals in
AgentContextor couple the product to AG-UI. Instead, the product consumes onlyMicrosoft.Extensions.AIapproval content, and the existing interactive abstraction handles both equivalence classes without transport knowledge.Implementation
Approval mapping is deliberately ordered after consumer handlers and registered UI actions, but before generic server-tool rendering. That preserves the established UI-action → approval → server-tool precedence: a consumer can customize the nested call, an explicitly registered client action still wins, and an approval request cannot be consumed as an ordinary invocation.
The approval handler first asks the same handler set to map the wrapped tool call. This is the key extensibility path: typed/custom blocks survive as
InnerBlock; only an unrecognized call uses the generic fallback. Its state is one-shot so a completed active handler cannot claim and swallow a later approval update.Approve and reject are one equivalence class: both take the same lock, accept only the
Pendingstate, create the response from the original request, and notify once. Their only deltas are final status,Approved, and optionalReason.AgentContextcollects every interactive block emitted by one model pass, publishesAwaitingInput, waits for them together, and resumes streaming. The role rule is the protocol boundary: all ordinary function results continue as tool content; any approval response makes the continuation a user message, as required by the MEAI approval contract.The canonical dojo scenario is the second HITL equivalence class. It registers
generate_task_stepsas a browser UI action and renders only that tool with a selectable task card. Confirm serializes enabled/disabled statuses and invokes once; Reject uses the same path after disabling every step, so the model receives either the selected descriptions or the single rejection result.The permanent browser fixture launches AGUIDojoApi and DojoClient separately. Only the API-side model is replayed; DojoClient receives the API URL and retains
AGUI.Client.AGUIChatClient, so request serialization, HTTP POST, SSE parsing, task-card interaction, and continuation all cross the real wire. One shared harness covers two deltas: deselect 2 of 5 then confirm, or reject all. The generated recording asserts the exacthuman-in-the-loop-steps-1call/result pairing and both continuation payloads.Outcome
AwaitingInput→ user-role continuationHumanInTheLoopScenarioTestspassedReview guidance / acceptance criteria: focus on the handler order, the one-shot lock, and the continuation role choice rather than the task-card CSS or generated JSON. Accept when an approval pauses the conversation; the first approve/reject response preserves the exact tool call and optional reason; duplicate responses are ignored; custom typed function blocks remain nested; selected task steps alone reach the continuation; rejection reaches the no-steps continuation; and the browser input is disabled while awaiting the interaction and re-enabled afterward.