feat: add trusted action groups
This commit is contained in:
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user