feat: add trusted action groups

This commit is contained in:
2026-08-03 15:34:17 +08:00
parent c384368f66
commit bddf15a2d2
9 changed files with 173 additions and 11 deletions
+38
View File
@@ -40,6 +40,19 @@ export type DeliveryState =
/** The only standard interaction requests admitted during Milestone 1. */
export type ToolCallKind = "choice" | "confirm" | "input";
export type ActionGroupMode = "single-choice" | "multi-choice" | "button";
export type ActionGroupAction = {
id: string;
label: string;
description?: string;
};
export type ActionGroup = {
mode: ActionGroupMode;
actions: readonly ActionGroupAction[];
};
export type ToolCallRequest = {
call_id: string;
tool: ToolCallKind;
@@ -47,6 +60,8 @@ export type ToolCallRequest = {
prompt: string;
expires_at?: string;
data: JsonObject;
/** Present only for the trusted `choice` renderer introduced in M1-02. */
action_group?: ActionGroup;
};
export type ToolCallFinalStatus = "completed" | "failed" | "cancelled" | "expired";
@@ -267,11 +282,31 @@ function validAppCallPayload(payload: JsonObject): boolean {
const TOOL_KINDS = new Set<ToolCallKind>(["choice", "confirm", "input"]);
const TOOL_FINAL_STATUSES = new Set<ToolCallFinalStatus>(["completed", "failed", "cancelled", "expired"]);
const ACTION_GROUP_MODES = new Set<ActionGroupMode>(["single-choice", "multi-choice", "button"]);
function optionalTimestamp(value: unknown): string | undefined {
return typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : undefined;
}
function parseActionGroup(value: unknown): ActionGroup | undefined {
const candidate = object(value);
const mode = text(candidate?.mode) as ActionGroupMode;
if (!candidate || !ACTION_GROUP_MODES.has(mode) || !Array.isArray(candidate.actions) || candidate.actions.length < 1 || candidate.actions.length > 12) return undefined;
const seen = new Set<string>();
const actions: ActionGroupAction[] = [];
for (const value of candidate.actions) {
const action = object(value);
const id = action && identifier(action.id);
const label = action && text(action.label).trim();
const description = action ? text(action.description).trim() : "";
if (!id || !label || label.length > 200 || seen.has(id)) return undefined;
if (description.length > 500) return undefined;
seen.add(id);
actions.push({ id, label, ...(description ? { description } : {}) });
}
return { mode, actions };
}
function parseToolCall(payload: JsonObject): ToolCallRequest | undefined {
const callID = identifier(payload.call_id);
const tool = text(payload.tool) as ToolCallKind;
@@ -280,6 +315,8 @@ function parseToolCall(payload: JsonObject): ToolCallRequest | undefined {
if (payload.expires_at !== undefined && !expiresAt) return undefined;
const data = payload.data === undefined ? {} : object(payload.data);
if (!data) return undefined;
const actionGroup = tool === "choice" ? parseActionGroup(data.action_group) : undefined;
if (tool === "choice" && !actionGroup) return undefined;
return {
call_id: callID,
tool,
@@ -287,6 +324,7 @@ function parseToolCall(payload: JsonObject): ToolCallRequest | undefined {
prompt: text(payload.prompt),
...(expiresAt ? { expires_at: expiresAt } : {}),
data,
...(actionGroup ? { action_group: actionGroup } : {}),
};
}