Skip to content
66 changes: 36 additions & 30 deletions ts/docs/plans/copilot-direct-actions/director-actions.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# TypeAgent-Copilot Structured Action Invocation

**Status:** Draft
**Last Updated:** 2026-09-10
**Last Updated:** 2026-09-17

## Summary

Expand Down Expand Up @@ -111,7 +111,7 @@ Discovery should provide enough information for Copilot to determine:
- Input schema
- Relevant constraints
- Expected outputs and any authorization, confirmation, or interaction requirements
- Whether the action is currently available, and what setup is missing if it is not
- Only actions that are currently active and enabled

**Conceptually:**

Expand All @@ -127,51 +127,43 @@ Discovery should provide enough information for Copilot to determine:
input schema input schema
```

For large action catalogs, discovery should preferably support search and progressive disclosure rather than requiring the entire TypeAgent action catalog to be loaded into Copilot's context.
For large action catalogs, discovery should support search and progressive disclosure rather than requiring the entire TypeAgent action catalog to be registered as MCP tools or loaded into Copilot's context.

Use two required levels of progressive disclosure:
`searchActions` accepts one required free-text query. When the existing semantic action candidate index is available, discovery reuses it to return the five highest-ranked permitted actions. Results are ordered by descending semantic score with stable schema-name/action-name ordering for ties. Ranking scores remain internal and are not part of the discovery or RPC contract.

1. **Action summary:** Search or list compact action identifiers, descriptions, and current availability.
2. **Action contract:** Retrieve one closed, self-contained contract with its parameters, referenced types, constraints, outputs, and interaction requirements.
If semantic ranking is unavailable or fails, discovery falls back to the existing case-insensitive contiguous substring match across schema name, action name, and description. The fallback returns every match in stable identity order rather than silently truncating the set. A successful semantic search with zero candidates is authoritative and does not trigger literal fallback.

Server status and capability or schema names may be returned as metadata and search filters, but they should not be mandatory retrieval stages. Treating them as required levels would add round trips without improving the contract boundary. The action is the unit of selection, caching, and compatibility.
Each result is a closed, self-contained action contract with its identity, description, parameters, referenced types, constraints, outputs, and interaction requirements. This combines candidate discovery and contract hydration so the normal structured path requires only discovery and execution. The action remains the unit of selection, caching, and compatibility.

The normal flow is:

```text
search actions → get selected action contract → execute action
search action contracts → execute selected action
```

A caller that already knows the action should be able to fetch its contract directly; a caller with a current contract should not need to repeat discovery. A contract must include referenced enums and nested types without loading unrelated actions from the same schema.
A caller with a current contract should not need to repeat discovery. A contract must include referenced enums and nested types without loading unrelated actions from the same schema.

Contracts may be reused within the same server and session/permission scope. TypeAgent must detect an outdated contract before execution and ask the caller to refresh it using the exact-match mechanism below.
Contracts may be reused within the same server and session/permission scope. Execution resolves the exact action identity against the current schema and validates the submitted parameters before any effect is possible. If the action was removed or its parameters are no longer valid, execution returns the corresponding unavailable or validation failure rather than reinterpreting the request.

Discovery must respect the caller's permissions. It neither enables actions nor grants permission to execute them. No match or an ambiguous match should lead to clarification or natural-language handling, not guessed action parameters.
Discovery must respect the caller's permissions. It neither enables actions nor grants permission to execute them. Denied, disabled, and inactive actions are filtered before ranking and contract hydration. A query with no candidates should lead to clarification or natural-language handling, not guessed action parameters. When multiple actions match, the caller must select from the returned contracts or clarify if it cannot do so confidently.

### Contract Versioning
### Protocol Versioning

Discovery responses include a protocol version for the structured-action envelope and an opaque fingerprint for each action contract. Execution must supply the fingerprint returned with the selected contract.

TypeAgent compares the supplied fingerprint with the current contract before any effect is possible. A mismatch returns `contract_stale` without executing the action. The caller must fetch the current contract and construct a new request; TypeAgent must not reinterpret parameters under the changed contract.

The initial implementation may conservatively use the existing schema source hash. The target fingerprint should hash a canonical representation of the selected action's execution-relevant contract, including parameter types, required fields, constraints, referenced definitions, outputs, and interaction shape. Descriptions and transient availability, authentication, permission, and readiness state must not affect the fingerprint.

Version 1 uses exact fingerprint matching rather than attempting semantic compatibility between arbitrary schema changes. This intentionally favors a safe refresh over complex compatibility rules for unions, nested types, constraints, and interaction results.
Discovery responses include a protocol version for the structured-action envelope. Versioning applies to the shared request and response shapes. Execution compatibility is determined from the current action definition when the call is made.

## MCP Interface

Expose a small, fixed set of operations through the existing TypeAgent MCP server:

- Search or list action summaries.
- Retrieve one complete action contract.
- Search complete action contracts with a required free-text query.
- Execute one action against that contract.
- Continue or cancel a pending interaction when the transport cannot represent that interaction directly.

These operations are normal MCP tools. Individual TypeAgent actions remain data returned by discovery rather than being registered as separate MCP tools. This keeps a large, dynamic, permission-sensitive catalog out of Copilot's native tool list and allows enabled actions to change during a session. Native per-action tools may be reconsidered if Copilot supports reliable dynamic tool-list refresh and large catalogs without excessive context use.

Use the existing `schemaName` and `actionName` as the action identity. Keep them as separate request fields even if discovery also provides a joined display identifier.

The MCP package is a transport adapter over a shared structured-action service in the dispatcher or agent server. Discovery, contract generation, fingerprinting, validation, readiness checks, execution, and interaction state do not belong in the MCP adapter.
The MCP package is a transport adapter over a shared structured-action service in the dispatcher or agent server. Discovery, contract generation, validation, readiness checks, execution, and interaction state do not belong in the MCP adapter.

## Responsibility Boundaries

Expand All @@ -196,14 +188,13 @@ Bind calls to the intended conversation and make any use of prior-turn context e
For structured invocation, Copilot selecting an action does not count as user confirmation. Before execution, TypeAgent must:

1. Bind the request to the correct caller, TypeAgent session, and Copilot conversation.
2. Reject a stale contract.
3. Resolve the current action and validate its parameters.
4. Check that the schema and action are enabled.
5. Run agent readiness and setup checks.
6. Preserve authentication and resource authorization enforced by the owning service.
7. Request user confirmation for destructive, external, costly, or sensitive effects.
2. Resolve the current action and validate its parameters.
3. Check that the schema and action are enabled.
4. Run agent readiness and setup checks.
5. Preserve authentication and resource authorization enforced by the owning service.
6. Request user confirmation for destructive, external, costly, or sensitive effects.

A required choice or form returns `requires_interaction` with an opaque, session-bound, single-use interaction ID. A later call submits the user's response or cancels the interaction. The integration must not choose a default answer on the user's behalf. Completion, failure, cancellation, `contract_stale`, unavailability, and uncertain execution after a disconnect or timeout must remain distinct result states.
A required choice or form returns `requires_interaction` with an opaque, session-bound, single-use interaction ID. A later call submits the user's response or cancels the interaction. The integration must not choose a default answer on the user's behalf. Completion, failure, cancellation, validation failure, unavailability, and uncertain execution after a disconnect or timeout must remain distinct result states.

## Multi-Step Behavior

Expand All @@ -217,10 +208,25 @@ Do not expose a general-purpose structured plan API in version 1. Such an API wo

## Shared Integration Service

Direct and MCP integration modes share the same transport-neutral structured-action service. It owns discovery, action identity and contracts, fingerprints, validation, readiness and authorization checks, execution, structured results, and interaction and cancellation semantics.
Direct and MCP integration modes share the same transport-neutral structured-action service. It owns discovery, action identity and contracts, validation, readiness and authorization checks, execution, structured results, and interaction and cancellation semantics.

MCP maps the service to MCP tools and structured content. Direct mode calls it through the dispatcher interface. This does not change ordinary Direct-mode prompts: user-originated natural language continues through TypeAgent's intent resolution, and only callers that already know the action and concrete parameters use the shared structured interface.

Delivery is layered. Layer 1 is the query-only `searchActions`, which uses the shared semantic candidate index when available and hydrates complete contracts only for the permitted ranked candidates. Literal matching remains the offline fallback when ranking is unavailable or fails. Layer 2 adds `executeAction`. Layer 3 adds the MCP and Direct adapters over the shared service. The target foreground path remains two calls: discovery followed by execution.

Dynamic schema updates rebuild their semantic entries from the final parsed schema and replace the prior entries only when the new index is ready. Discovery resolves each ranked identity against the current parsed definition before creating a contract, so a concurrent schema update cannot hydrate a stale definition. Background schema startup failures settle readiness while retaining the schema error for existing status and enablement behavior.

This reuse is intentionally limited to candidate ranking. Structured discovery does not reuse the ordinary dispatcher pipeline's grammar matching, translator cache, conversation context, LLM schema/action selection, or parameter translation. Copilot still selects one returned contract and supplies its structured parameters in the separate execution call.

## Future Considerations

- Evaluate BM25 as a local fallback or retrieval improvement. It could avoid a remote embedding dependency while providing better relevance than literal substring matching. Preserve stable identity ordering for ties and measure retrieval quality before changing ranking or limits.
- After parity validation, migrate the dispatcher's remaining `semanticSearchActionSchema` callers to the shared candidate-ranker result. Keep the compatibility wrapper while those callers still depend on the legacy semantic-search shape.
- A TypeAgent-aware Copilot plugin or adapter could prefetch a compact, permission-filtered agent catalog when it connects. Optional server-issued agent hints may boost ranking, but must not authorize an action or act as strict filters.
- Cache catalog data on the host outside model context where possible, with catalog version or change signals for invalidation. The server may also keep search documents and contracts warm.
- Query-only discovery remains correct without prefetch, hints, or warm caches. Generic MCP hosts are not guaranteed to prefetch, so the fixed discovery operation remains the fallback.
- At scale, payload and model-context token cost are the primary risks; scanning the in-memory catalog is not expected to dominate.

## Architectural Model

```text
Expand Down
11 changes: 11 additions & 0 deletions ts/packages/agentSdk/src/agentInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ export type GrammarContent = {
sourceMap?: string | undefined;
};

export type ActionEffect = "read-only" | "state-changing" | "unknown";

export type ActionPolicy = {
// Omission is unknown, never an exemption from effect confirmation.
effects?: ActionEffect;
// Even a read-only action can explicitly require confirmation.
confirmation?: "required";
};

export type SchemaManifest = {
description: string;
schemaType: string | SchemaTypeNames; // string if there are only action schemas
Expand All @@ -83,6 +92,8 @@ export type SchemaManifest = {
injected?: boolean; // whether the translator is injected into other domains, default is false
cached?: boolean; // whether the translator's action should be cached, default is true
streamingActions?: string[];
// Exact action names. Applies to structured invocation, not NL routing.
actionPolicies?: Record<string, ActionPolicy>;
};

export type ActionManifest = {
Expand Down
2 changes: 2 additions & 0 deletions ts/packages/agentSdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export {
SchemaContent,
SchemaFormat,
SchemaManifest,
ActionEffect,
ActionPolicy,
AppAgent,
AppAgentEvent,
AgentMessageKind,
Expand Down
70 changes: 61 additions & 9 deletions ts/packages/defaultAgentProvider/src/mcpAgentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type McpAppAgent = {
agent: AppAgent;
connection: McpConnection | undefined;
serverProcess?: ChildProcess | undefined;
loadError?: Error;
};
export type McpAppAgentRecord = {
agentP: Promise<McpAppAgent>;
Expand Down Expand Up @@ -243,6 +244,7 @@ function createMcpAppAgentRecord(
let connection: McpConnection | undefined;
let serverProcess: ChildProcess | undefined;
let agent: AppAgent;
let loadError: Error | undefined;
try {
if (info.serverCommand !== undefined) {
const occupied =
Expand Down Expand Up @@ -317,9 +319,11 @@ function createMcpAppAgentRecord(
return convertToolResult(action.actionName, result);
},
};
} catch (error: any) {
} catch (error: unknown) {
loadError =
error instanceof Error ? error : new Error(String(error));
debugError(
`[${appAgentName}] failed to connect: ${error?.message ?? error}`,
`[${appAgentName}] failed to connect: ${loadError.message}`,
);
if (connection !== undefined) {
await connection.close().catch(() => {});
Expand All @@ -332,7 +336,7 @@ function createMcpAppAgentRecord(
agent = {
updateAgentContext() {
// Delay throwing error until the agent is used.
throw error;
throw loadError;
},
};
}
Expand All @@ -354,6 +358,7 @@ function createMcpAppAgentRecord(
connection,
agent,
serverProcess,
...(loadError === undefined ? {} : { loadError }),
};
};
return {
Expand Down Expand Up @@ -381,9 +386,12 @@ export function createMcpAppAgentProvider(
agentName: string,
manifest: AppAgentManifest,
) => void)[] = [];
const schemaFailedCallbacks: ((agentName: string, error: Error) => void)[] =
[];

// Manifests that are already resolved (so late-registered callbacks fire immediately)
const resolvedManifests = new Map<string, AppAgentManifest>();
const schemaFailures = new Map<string, Error>();

function startBackgroundAgent(appAgentName: string) {
if (
Expand All @@ -405,19 +413,41 @@ export function createMcpAppAgentProvider(
instanceConfig?.[appAgentName],
);
backgroundRecords.set(appAgentName, record);
schemaFailures.delete(appAgentName);

record.agentP
.then((agentData) => {
const notifyFailure = (error: Error) => {
if (backgroundRecords.get(appAgentName) === record) {
backgroundRecords.delete(appAgentName);
}
schemaFailures.set(appAgentName, error);
for (const cb of schemaFailedCallbacks) {
cb(appAgentName, error);
}
};

record.agentP.then(
(agentData) => {
if (agentData.connection !== undefined) {
schemaFailures.delete(appAgentName);
resolvedManifests.set(appAgentName, agentData.manifest);
for (const cb of schemaReadyCallbacks) {
cb(appAgentName, agentData.manifest);
}
} else {
notifyFailure(
agentData.loadError ??
new Error(
`MCP agent '${appAgentName}' failed to load`,
),
);
}
})
.catch(() => {
// errors surface when the agent is actually used
});
},
(error: unknown) => {
notifyFailure(
error instanceof Error ? error : new Error(String(error)),
);
},
);
}

function getMpcAppAgentRecord(appAgentName: string) {
Expand All @@ -438,6 +468,21 @@ export function createMcpAppAgentProvider(
if (info === undefined) {
throw new Error(`Invalid app agent: ${appAgentName}`);
}
if (info.serverCommand !== undefined) {
// Retry through the background path so a recovered connection also
// publishes its generated schema to the dispatcher.
startBackgroundAgent(appAgentName);
const retry = backgroundRecords.get(appAgentName);
if (retry === undefined) {
throw new Error(
`Failed to start MCP app agent: ${appAgentName}`,
);
}
retry.count++;
backgroundRecords.delete(appAgentName);
mcpAppAgents.set(appAgentName, retry);
return retry;
}
const record = createMcpAppAgentRecord(
name,
version,
Expand Down Expand Up @@ -467,6 +512,13 @@ export function createMcpAppAgentProvider(
}
},

onSchemaFailed(callback) {
schemaFailedCallbacks.push(callback);
for (const [agentName, error] of schemaFailures) {
callback(agentName, error);
}
},

async getAppAgentManifest(appAgentName: string) {
const info = infos[appAgentName];
if (info === undefined) {
Expand Down
Loading
Loading