feat: establish tool call state machine
This commit is contained in:
@@ -37,6 +37,32 @@ export type DeliveryState =
|
||||
| { status: "delivered"; updated_at: string; transport_id?: string }
|
||||
| { status: "failed"; updated_at: string; error: string; retryable: boolean };
|
||||
|
||||
/** The only standard interaction requests admitted during Milestone 1. */
|
||||
export type ToolCallKind = "choice" | "confirm" | "input";
|
||||
|
||||
export type ToolCallRequest = {
|
||||
call_id: string;
|
||||
tool: ToolCallKind;
|
||||
title: string;
|
||||
prompt: string;
|
||||
expires_at?: string;
|
||||
data: JsonObject;
|
||||
};
|
||||
|
||||
export type ToolCallFinalStatus = "completed" | "failed" | "cancelled" | "expired";
|
||||
|
||||
export type ToolCallResult = {
|
||||
call_id: string;
|
||||
status: ToolCallFinalStatus;
|
||||
result: JsonObject;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type ToolCallCancel = {
|
||||
call_id: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A user-originated action awaiting delivery through the Transport Adapter.
|
||||
* It is data only: the Store owns retries and the Kernel owns state changes.
|
||||
@@ -105,6 +131,9 @@ export type ConversationItem =
|
||||
| (ConversationItemBase & { kind: "agent-status"; status: string; detail: string })
|
||||
| (ConversationItemBase & { kind: "progress"; title: string; percent: number; status: string })
|
||||
| (ConversationItemBase & { kind: "error"; message: string; code?: string })
|
||||
| (ConversationItemBase & { kind: "tool-call"; call: ToolCallRequest })
|
||||
| (ConversationItemBase & { kind: "tool-result"; result: ToolCallResult })
|
||||
| (ConversationItemBase & { kind: "tool-cancel"; cancel: ToolCallCancel })
|
||||
| (ConversationItemBase & { kind: "surface"; envelope: Envelope })
|
||||
| (ConversationItemBase & { kind: "app-call"; envelope: Envelope })
|
||||
| (ConversationItemBase & { kind: "fallback"; reason: ProtocolFallbackCode; detail: string; envelope?: Envelope });
|
||||
@@ -119,6 +148,9 @@ const SUPPORTED_TYPES = new Set([
|
||||
"lineup.v1.agent.status",
|
||||
"lineup.v1.agent.progress",
|
||||
"lineup.v1.error",
|
||||
"lineup.v1.tool.call",
|
||||
"lineup.v1.tool.result",
|
||||
"lineup.v1.tool.cancel",
|
||||
"lineup.v1.ui.open",
|
||||
"lineup.v1.ui.patch",
|
||||
"lineup.v1.ui.close",
|
||||
@@ -233,6 +265,44 @@ function validAppCallPayload(payload: JsonObject): boolean {
|
||||
return Boolean(identifier(payload.call_id) && identifier(payload.capability) && object(payload.arguments));
|
||||
}
|
||||
|
||||
const TOOL_KINDS = new Set<ToolCallKind>(["choice", "confirm", "input"]);
|
||||
const TOOL_FINAL_STATUSES = new Set<ToolCallFinalStatus>(["completed", "failed", "cancelled", "expired"]);
|
||||
|
||||
function optionalTimestamp(value: unknown): string | undefined {
|
||||
return typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : undefined;
|
||||
}
|
||||
|
||||
function parseToolCall(payload: JsonObject): ToolCallRequest | undefined {
|
||||
const callID = identifier(payload.call_id);
|
||||
const tool = text(payload.tool) as ToolCallKind;
|
||||
if (!callID || !TOOL_KINDS.has(tool)) return undefined;
|
||||
const expiresAt = optionalTimestamp(payload.expires_at);
|
||||
if (payload.expires_at !== undefined && !expiresAt) return undefined;
|
||||
const data = payload.data === undefined ? {} : object(payload.data);
|
||||
if (!data) return undefined;
|
||||
return {
|
||||
call_id: callID,
|
||||
tool,
|
||||
title: text(payload.title),
|
||||
prompt: text(payload.prompt),
|
||||
...(expiresAt ? { expires_at: expiresAt } : {}),
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
function parseToolResult(payload: JsonObject): ToolCallResult | undefined {
|
||||
const callID = identifier(payload.call_id);
|
||||
const status = text(payload.status) as ToolCallFinalStatus;
|
||||
const result = payload.result === undefined ? {} : object(payload.result);
|
||||
if (!callID || !TOOL_FINAL_STATUSES.has(status) || !result) return undefined;
|
||||
return { call_id: callID, status, result, ...(text(payload.error) ? { error: text(payload.error) } : {}) };
|
||||
}
|
||||
|
||||
function parseToolCancel(payload: JsonObject): ToolCallCancel | undefined {
|
||||
const callID = identifier(payload.call_id);
|
||||
return callID ? { call_id: callID, reason: text(payload.reason) } : undefined;
|
||||
}
|
||||
|
||||
function fallbackItem(value: ProtocolFallback): ConversationItem {
|
||||
return {
|
||||
kind: "fallback",
|
||||
@@ -289,6 +359,18 @@ export function decodeConversationItem(raw: string): ConversationItem {
|
||||
? { kind: "error", message, ...(text(payload.code) ? { code: text(payload.code) } : {}), ...base }
|
||||
: fallbackItem({ code: "invalid_payload", detail: "Error payload requires message.", envelope });
|
||||
}
|
||||
case "lineup.v1.tool.call": {
|
||||
const call = parseToolCall(payload);
|
||||
return call ? { kind: "tool-call", call, ...base } : fallbackItem({ code: "invalid_payload", detail: "Tool call requires call_id, supported tool, and JSON data.", envelope });
|
||||
}
|
||||
case "lineup.v1.tool.result": {
|
||||
const result = parseToolResult(payload);
|
||||
return result ? { kind: "tool-result", result, ...base } : fallbackItem({ code: "invalid_payload", detail: "Tool result requires call_id, final status, and JSON result.", envelope });
|
||||
}
|
||||
case "lineup.v1.tool.cancel": {
|
||||
const cancel = parseToolCancel(payload);
|
||||
return cancel ? { kind: "tool-cancel", cancel, ...base } : fallbackItem({ code: "invalid_payload", detail: "Tool cancel requires call_id.", envelope });
|
||||
}
|
||||
case "lineup.v1.ui.open":
|
||||
case "lineup.v1.ui.patch":
|
||||
case "lineup.v1.ui.close":
|
||||
|
||||
Reference in New Issue
Block a user