Skip to content

[2/3] Add direct action call path to dispatcher - #2992

Merged
George Ng (GeorgeNgMsft) merged 5 commits into
mainfrom
georgengmsft-guarded-action-execution
Sep 19, 2026
Merged

George Ng (GeorgeNgMsft) merged 5 commits into
mainfrom
georgengmsft-guarded-action-execution

Conversation

@GeorgeNgMsft

@GeorgeNgMsft George Ng (GeorgeNgMsft) commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

This PR adds a safe way for clients to run a specific TypeAgent action using structured input. It uses the dispatcher’s existing queue and action engine, asks the user before making changes, pauses when the action needs more information, and returns the action’s real result. It also lets the same conversation reconnect to unfinished work without allowing another client to take control.

Where this fits

This is layer 2 of the structured-action stack:

  1. [1/3] Add shared structured action contract discovery #2991 lets clients search for actions and inspect their input contracts.
  2. This PR safely runs the selected action and manages its lifecycle.
  3. [3/3] Update Copilot plugin to leverage new TypeAgent MCP tools #2993 exposes the feature through MCP and Direct Action adapters.

#2991 has merged, so this draft now targets main. Native stack #2994 still tracks [2991, 2992, 2993].

What changed

  • Add executeAction, continueAction, and cancelAction to the public dispatcher API and dispatcher RPC.
  • Run typed actions through the existing dispatcher queue and executeActions engine. The implementation does not turn parameters back into command text or send them through natural-language interpretation.
  • Look up the requested action by its exact schemaName and actionName. Search ranking is only used to help clients discover actions; it never decides which action is executed.
  • Recheck the current scope, permissions, enabled state, readiness, input schema, parameters, and safety policy before the action starts and again after waits.
  • Ask for confirmation unless the action is explicitly marked read-only. A read-only action can still require confirmation, and a caller cannot bypass confirmation by claiming that it already has approval.
  • Pause and resume the same operation when an action asks a question, shows a form, proposes an edit, or requests a choice. Continuing an operation does not run the action again.
  • Cancel structured operations without choosing a default answer. This public cancelAction is separate from the existing internal agent-RPC cancellation message: it checks the conversation scope and returns a structured result.
  • Return clear outcomes: completed, failed, cancelled, requires_interaction, unavailable, or execution_uncertain. Completed operations include the real action output, values, and entities.
  • Guard child actions and flow steps separately while allowing them to use results created earlier in the same operation. The operation does not silently reuse context from an earlier conversation turn.
  • Add an opt-in reconnect token for structured actions. The token belongs to one conversation and stays in memory. A reconnect takes ownership from the old connection, while stale or unrelated clients are rejected.
  • Clean up cancelled SDK choices across in-process and agent-RPC execution without changing normal chat behavior.
  • Fix a Jest ESM issue where signal-exit could change the exported shape of process while tests were loading.

API flow

First, the client joins a conversation and opts into structured actions:

const joined = await joinConversation(io, {
    conversationId,
    structuredActions: {},
});

const resumeToken = joined.structuredActions.resumeToken;

The client searches for an action through the API added in #2991, chooses one exact contract, and sends concrete parameters:

const search = await dispatcher.searchActions({ query: "save this item" });

const result = await dispatcher.executeAction({
    protocolVersion: 1,
    scopeId: search.scopeId,
    schemaName: "items",
    actionName: "save",
    parameters: { value: "example" },
});

If the action needs confirmation or another answer, it returns requires_interaction. The client sends the answer back to the same operation:

await dispatcher.continueAction({
    protocolVersion: 1,
    scopeId: result.scopeId,
    operationId: result.operationId,
    interactionId: result.interactionId,
    response: { type: "confirmation", approved: true },
});

Cancellation uses the same scopeId and operationId. If the client disconnects, it can rejoin the same conversation with the resume token instead of starting the action again.

There is no contract fingerprint or contract_stale result. The dispatcher checks the current action definition and validates the current parameters each time it reaches an execution boundary. If the action was removed or disabled, it is unavailable. If its current input schema no longer accepts the parameters, execution fails before entering the handler.

State limits

To keep memory use bounded, each dispatcher context keeps at most 100 live operations. Operations and interactions expire after 10 minutes, and the latest 100 finished results are retained for 10 minutes. The server keeps at most 100 structured-action conversation bindings, which expire after 30 minutes without use.

Validation

  • Built dispatcher types, dispatcher RPC, dispatcher, agent SDK, agent RPC, and the agent-server protocol, client, and server.
  • Passed the full dispatcher test run: 132 suites and 2,113 tests, with one pre-existing skip.
  • Passed 87 structured execution and discovery tests together.
  • Passed 63 focused RPC, SDK cancellation, reconnect, and server/host tests.
  • Passed the complexity, lint, test-debt, and circular-dependency checks with no new violations, skipped tests, or dependency cycles.

@GeorgeNgMsft
George Ng (GeorgeNgMsft) added this pull request to stack #2994 September 11, 2026 07:20
Base automatically changed from georgengmsft-structured-action-contracts to main September 18, 2026 06:44
@GeorgeNgMsft
George Ng (GeorgeNgMsft) force-pushed the georgengmsft-guarded-action-execution branch 3 times, most recently from bd73a44 to 2e64a44 Compare September 18, 2026 18:36
@GeorgeNgMsft George Ng (GeorgeNgMsft) changed the title [2/3] Add guarded structured execution and resumable interactions [2/3] Add direct action call path to dispatcher Sep 18, 2026
@GeorgeNgMsft
George Ng (GeorgeNgMsft) marked this pull request as ready for review September 18, 2026 23:42
Run typed actions through the existing dispatcher queue with live contract, scope and confirmation guards. Preserve true results, validate and resume interactions without replay, and bind trusted host reconnects to the same logical owner.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Materialize inherited process.emit before signal-exit registration can add an enumerable export during Jest ESM linking. Cover inherited and instrumented own emit properties, wrapper cleanup and real signal-exit callback behavior without host-process side effects.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve actions by exact identity against current scope, schema, readiness, permissions, and policy without contract fingerprints. Revalidate parameters and confirmation policy at effect boundaries and preserve isolated structured choices.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep in-flight operations authorized across trusted reconnect takeover, make continuation retries recover the current observable result without replay, and avoid marking entity preparation as a possible effect.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@GeorgeNgMsft
George Ng (GeorgeNgMsft) added this pull request to the merge queue Sep 19, 2026
Merged via the queue into main with commit d4d69de Sep 19, 2026
27 checks passed
George Ng (GeorgeNgMsft) added a commit that referenced this pull request Sep 19, 2026
This PR removes duplicated dispatcher cancellation code while keeping
the public cancellation APIs separate. `cancelCommand` and structured
`cancelAction` still provide different ownership checks and result
shapes, but they now share one internal helper for removing queued
requests, marking running requests cancelled, and aborting their
controller.

## Changes

- Add an internal `cancelQueuedRequest` helper for request-ID-based
queue cancellation.
- Reuse it from `Dispatcher.cancelCommand`.
- Reuse it from structured operation cancellation after the structured
terminal result has been published.
- Preserve event-before-abort ordering, queued/running/not-found
results, and idempotent running cancellation.
- Leave `cancelCommandByClientId` and the agent-RPC
`cancelAction(actionContextId)` path unchanged.

## Validation

- Dispatcher cancellation and queue suites: 5 suites, 112 tests.
- Dispatcher RPC: 19 tests.
- Agent-server structured host: 7 tests.
- Agent-server dependency build and formatting checks.
- Lint, complexity, circular-dependency, and test-debt ratchets against
PR #2992 head `a6049a3bf15966d09db5c0b5fc8e8b2deb0bf257`.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

2 participants