feat: establish tool call state machine
This commit is contained in:
@@ -101,6 +101,9 @@ rendererRegistry.register("markdown", (item, context) => context.renderMarkdown(
|
||||
rendererRegistry.register("agent-status", (item, context) => context.renderAgentStatus(item));
|
||||
rendererRegistry.register("progress", (item, context) => context.renderProgress(item));
|
||||
rendererRegistry.register("error", (item, context) => context.renderError(item));
|
||||
rendererRegistry.register("tool-call", (item, context) => context.renderToolCall(item));
|
||||
rendererRegistry.register("tool-result", (item, context) => context.renderToolResult(item));
|
||||
rendererRegistry.register("tool-cancel", (item, context) => context.renderToolCancel(item));
|
||||
rendererRegistry.register("surface", (item, context) => context.renderSurface(item));
|
||||
rendererRegistry.register("app-call", (item, context) => context.renderAppCall(item));
|
||||
rendererRegistry.register("fallback", (item, context) => context.renderFallback(item));
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -71,4 +71,59 @@ describe("InteractionKernel", () => {
|
||||
// The first id has expired from the bounded set and is accepted again.
|
||||
expect(kernel.ingest({ raw: envelope({ id: "evt_status_thinking" }), source: "live" }).disposition).toBe("accepted");
|
||||
});
|
||||
|
||||
it("projects tool calls by call_id and applies a result only after submission", () => {
|
||||
const kernel = new InteractionKernel();
|
||||
const call = kernel.ingest({
|
||||
raw: envelope({
|
||||
id: "evt_tool_call",
|
||||
type: "lineup.v1.tool.call",
|
||||
payload: { call_id: "call_kernel_001", tool: "choice", title: "Choose", prompt: "One", data: {} },
|
||||
}),
|
||||
source: "sync",
|
||||
received_at: "2026-08-03T08:00:00.000Z",
|
||||
});
|
||||
const duplicateCallID = kernel.ingest({
|
||||
raw: envelope({
|
||||
id: "evt_tool_call_duplicate_id",
|
||||
type: "lineup.v1.tool.call",
|
||||
payload: { call_id: "call_kernel_001", tool: "choice", title: "Ignored", prompt: "Ignored", data: {} },
|
||||
}),
|
||||
source: "live",
|
||||
});
|
||||
const earlyResult = kernel.ingest({
|
||||
raw: envelope({ id: "evt_tool_result_early", type: "lineup.v1.tool.result", payload: { call_id: "call_kernel_001", status: "completed", result: {} } }),
|
||||
source: "live",
|
||||
});
|
||||
|
||||
expect(call.snapshot.tool_calls).toEqual([expect.objectContaining({ call_id: "call_kernel_001", status: "pending" })]);
|
||||
expect(duplicateCallID.snapshot.tool_calls).toEqual([
|
||||
expect.objectContaining({ request: expect.objectContaining({ title: "Choose" }) }),
|
||||
]);
|
||||
expect(earlyResult.snapshot.tool_calls).toEqual([expect.objectContaining({ status: "pending" })]);
|
||||
expect(kernel.submitToolCall("call_kernel_001", "2026-08-03T08:01:00.000Z")).toMatchObject({ disposition: "accepted" });
|
||||
|
||||
const completed = kernel.ingest({
|
||||
raw: envelope({ id: "evt_tool_result_completed", type: "lineup.v1.tool.result", payload: { call_id: "call_kernel_001", status: "completed", result: { selected: "alpha" } } }),
|
||||
source: "live",
|
||||
});
|
||||
expect(completed.snapshot.tool_calls).toEqual([
|
||||
expect.objectContaining({ call_id: "call_kernel_001", status: "completed", result: { selected: "alpha" } }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps malformed tool payloads on the safe fallback path", () => {
|
||||
const kernel = new InteractionKernel();
|
||||
const invalidCall = kernel.ingest({
|
||||
raw: envelope({ id: "evt_invalid_tool_call", type: "lineup.v1.tool.call", payload: { call_id: "call_bad", tool: "shell", data: {} } }),
|
||||
source: "sync",
|
||||
});
|
||||
const invalidResult = kernel.ingest({
|
||||
raw: envelope({ id: "evt_invalid_tool_result", type: "lineup.v1.tool.result", payload: { call_id: "call_bad", status: "submitted", result: {} } }),
|
||||
source: "sync",
|
||||
});
|
||||
|
||||
expect(invalidCall.items).toEqual([expect.objectContaining({ kind: "fallback", reason: "invalid_payload" })]);
|
||||
expect(invalidResult.items).toEqual([expect.objectContaining({ kind: "fallback", reason: "invalid_payload" })]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,10 @@ import {
|
||||
decodeConversationItem,
|
||||
type Actor,
|
||||
type ConversationItem,
|
||||
type JsonObject,
|
||||
type ToolCallFinalStatus,
|
||||
} from "../protocol";
|
||||
import { ToolCallStateMachine, type ToolCallRecord } from "./tool-call-state";
|
||||
|
||||
/** Transport metadata is intentionally opaque to the protocol and renderer. */
|
||||
export type IncomingMessage = {
|
||||
@@ -23,6 +26,7 @@ export type AgentPresence = {
|
||||
export type KernelSnapshot = {
|
||||
seen_envelope_ids: number;
|
||||
agent_presence: readonly AgentPresence[];
|
||||
tool_calls: readonly ToolCallRecord[];
|
||||
};
|
||||
|
||||
export type KernelResult =
|
||||
@@ -39,6 +43,7 @@ export class InteractionKernel {
|
||||
private readonly seenIDs = new Set<string>();
|
||||
private readonly seenOrder: string[] = [];
|
||||
private readonly presenceByActor = new Map<string, AgentPresence>();
|
||||
private readonly toolCalls = new ToolCallStateMachine();
|
||||
|
||||
public constructor(private readonly maxRememberedEnvelopeIDs = 2_000) {
|
||||
if (!Number.isSafeInteger(maxRememberedEnvelopeIDs) || maxRememberedEnvelopeIDs < 1) {
|
||||
@@ -61,14 +66,24 @@ export class InteractionKernel {
|
||||
return {
|
||||
seen_envelope_ids: this.seenIDs.size,
|
||||
agent_presence: [...this.presenceByActor.values()].map(presence => ({ ...presence })),
|
||||
tool_calls: this.toolCalls.snapshot(),
|
||||
};
|
||||
}
|
||||
|
||||
public submitToolCall(callID: string, updatedAt: string) {
|
||||
return this.toolCalls.submit(callID, updatedAt);
|
||||
}
|
||||
|
||||
public completeToolCall(callID: string, status: ToolCallFinalStatus, result: JsonObject, updatedAt: string, error?: string) {
|
||||
return this.toolCalls.complete({ call_id: callID, status, result, ...(error ? { error } : {}) }, updatedAt);
|
||||
}
|
||||
|
||||
/** Clear only the in-memory projection when the active conversation changes. */
|
||||
public reset(): void {
|
||||
this.seenIDs.clear();
|
||||
this.seenOrder.length = 0;
|
||||
this.presenceByActor.clear();
|
||||
this.toolCalls.reset();
|
||||
}
|
||||
|
||||
private remember(id: string): void {
|
||||
@@ -81,6 +96,18 @@ export class InteractionKernel {
|
||||
}
|
||||
|
||||
private project(item: ConversationItem, updatedAt: string): void {
|
||||
if (item.kind === "tool-call" && item.envelope) {
|
||||
this.toolCalls.receive(item.call, item.envelope.conversation_id, updatedAt);
|
||||
return;
|
||||
}
|
||||
if (item.kind === "tool-cancel") {
|
||||
this.toolCalls.cancel(item.cancel, updatedAt);
|
||||
return;
|
||||
}
|
||||
if (item.kind === "tool-result") {
|
||||
this.toolCalls.complete(item.result, updatedAt);
|
||||
return;
|
||||
}
|
||||
if (item.kind !== "agent-status" || !item.envelope) return;
|
||||
const actor = item.envelope.sender;
|
||||
const actorKey = `${actor.kind}:${actor.id}`;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ToolCallRequest } from "../protocol";
|
||||
import { ToolCallStateMachine } from "./tool-call-state";
|
||||
|
||||
const RECEIVED_AT = "2026-08-03T08:00:00.000Z";
|
||||
|
||||
function request(overrides: Partial<ToolCallRequest> = {}): ToolCallRequest {
|
||||
return {
|
||||
call_id: "call_state_001",
|
||||
tool: "choice",
|
||||
title: "Choose a path",
|
||||
prompt: "Select one option.",
|
||||
data: { source: "test" },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("ToolCallStateMachine", () => {
|
||||
it("creates one pending record per call_id and preserves the first request", () => {
|
||||
const state = new ToolCallStateMachine();
|
||||
const first = state.receive(request(), "conversation_001", RECEIVED_AT);
|
||||
const duplicate = state.receive(request({ title: "Replacement must be ignored" }), "conversation_002", "2026-08-03T08:01:00.000Z");
|
||||
|
||||
expect(first).toMatchObject({ disposition: "accepted", record: { status: "pending", conversation_id: "conversation_001" } });
|
||||
expect(duplicate).toMatchObject({ disposition: "duplicate", record: { request: { title: "Choose a path" } } });
|
||||
expect(state.snapshot()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("permits only pending to submitted to a final result", () => {
|
||||
const state = new ToolCallStateMachine();
|
||||
state.receive(request(), "conversation_001", RECEIVED_AT);
|
||||
|
||||
expect(state.complete({ call_id: "call_state_001", status: "completed", result: { selected: "alpha" } }, "2026-08-03T08:01:00.000Z"))
|
||||
.toMatchObject({ disposition: "invalid_transition", record: { status: "pending" } });
|
||||
expect(state.submit("call_state_001", "2026-08-03T08:02:00.000Z"))
|
||||
.toMatchObject({ disposition: "accepted", record: { status: "submitted" } });
|
||||
expect(state.complete({ call_id: "call_state_001", status: "completed", result: { selected: "alpha" } }, "2026-08-03T08:03:00.000Z"))
|
||||
.toMatchObject({ disposition: "accepted", record: { status: "completed", result: { selected: "alpha" } } });
|
||||
expect(state.submit("call_state_001", "2026-08-03T08:04:00.000Z").disposition).toBe("invalid_transition");
|
||||
expect(state.submit("missing_call", "2026-08-03T08:04:00.000Z").disposition).toBe("missing");
|
||||
|
||||
state.receive(request({ call_id: "call_state_failed" }), "conversation_001", RECEIVED_AT);
|
||||
state.submit("call_state_failed", "2026-08-03T08:05:00.000Z");
|
||||
expect(state.complete({ call_id: "call_state_failed", status: "failed", result: {}, error: "Validation rejected" }, "2026-08-03T08:06:00.000Z"))
|
||||
.toMatchObject({ disposition: "accepted", record: { status: "failed", error: "Validation rejected" } });
|
||||
});
|
||||
|
||||
it("cancels an active request but never changes a terminal record", () => {
|
||||
const state = new ToolCallStateMachine();
|
||||
state.receive(request(), "conversation_001", RECEIVED_AT);
|
||||
|
||||
expect(state.cancel({ call_id: "call_state_001", reason: "User left the flow" }, "2026-08-03T08:01:00.000Z"))
|
||||
.toMatchObject({ disposition: "accepted", record: { status: "cancelled", error: "User left the flow" } });
|
||||
expect(state.cancel({ call_id: "call_state_001", reason: "Again" }, "2026-08-03T08:02:00.000Z").disposition).toBe("invalid_transition");
|
||||
expect(state.get("missing_call")?.status).toBeUndefined();
|
||||
});
|
||||
|
||||
it("expires only active requests, including one that is already expired on receipt", () => {
|
||||
const state = new ToolCallStateMachine();
|
||||
state.receive(request({ call_id: "expired_on_receipt", expires_at: "2026-08-03T07:59:59.000Z" }), "conversation_001", RECEIVED_AT);
|
||||
state.receive(request({ call_id: "expires_later", expires_at: "2026-08-03T08:05:00.000Z" }), "conversation_001", RECEIVED_AT);
|
||||
state.receive(request({ call_id: "no_expiry" }), "conversation_001", RECEIVED_AT);
|
||||
|
||||
expect(state.get("expired_on_receipt")?.status).toBe("expired");
|
||||
expect(state.expire("2026-08-03T08:05:00.000Z")).toEqual([
|
||||
expect.objectContaining({ call_id: "expires_later", status: "expired" }),
|
||||
]);
|
||||
expect(state.get("no_expiry")?.status).toBe("pending");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import type {
|
||||
JsonObject,
|
||||
ToolCallCancel,
|
||||
ToolCallFinalStatus,
|
||||
ToolCallRequest,
|
||||
ToolCallResult,
|
||||
} from "../protocol";
|
||||
|
||||
export type ToolCallStatus = "pending" | "submitted" | ToolCallFinalStatus;
|
||||
|
||||
export type ToolCallRecord = {
|
||||
call_id: string;
|
||||
conversation_id: string;
|
||||
request: ToolCallRequest;
|
||||
status: ToolCallStatus;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
result?: JsonObject;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type ToolCallTransition =
|
||||
| { disposition: "accepted"; record: ToolCallRecord }
|
||||
| { disposition: "duplicate" | "invalid_transition" | "missing"; record?: ToolCallRecord };
|
||||
|
||||
/**
|
||||
* Platform-neutral state machine for trusted standard interaction calls.
|
||||
* It has no UI, persistence, Transport, capability, or side-effect handler;
|
||||
* later M1 tasks connect those boundaries through this explicit lifecycle.
|
||||
*/
|
||||
export class ToolCallStateMachine {
|
||||
private readonly calls = new Map<string, ToolCallRecord>();
|
||||
|
||||
public receive(request: ToolCallRequest, conversationID: string, receivedAt: string): ToolCallTransition {
|
||||
const existing = this.calls.get(request.call_id);
|
||||
if (existing) return { disposition: "duplicate", record: copy(existing) };
|
||||
const expired = request.expires_at !== undefined && Date.parse(request.expires_at) <= Date.parse(receivedAt);
|
||||
const record: ToolCallRecord = {
|
||||
call_id: request.call_id,
|
||||
conversation_id: conversationID,
|
||||
request: { ...request, data: { ...request.data } },
|
||||
status: expired ? "expired" : "pending",
|
||||
created_at: receivedAt,
|
||||
updated_at: receivedAt,
|
||||
};
|
||||
this.calls.set(record.call_id, record);
|
||||
return { disposition: "accepted", record: copy(record) };
|
||||
}
|
||||
|
||||
public submit(callID: string, updatedAt: string): ToolCallTransition {
|
||||
return this.transition(callID, updatedAt, ["pending"], record => { record.status = "submitted"; });
|
||||
}
|
||||
|
||||
public complete(result: ToolCallResult, updatedAt: string): ToolCallTransition {
|
||||
return this.transition(result.call_id, updatedAt, ["submitted"], record => {
|
||||
record.status = result.status;
|
||||
record.result = { ...result.result };
|
||||
record.error = result.error;
|
||||
});
|
||||
}
|
||||
|
||||
public cancel(cancel: ToolCallCancel, updatedAt: string): ToolCallTransition {
|
||||
return this.transition(cancel.call_id, updatedAt, ["pending", "submitted"], record => {
|
||||
record.status = "cancelled";
|
||||
record.error = cancel.reason || undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public expire(now: string): readonly ToolCallRecord[] {
|
||||
const changed: ToolCallRecord[] = [];
|
||||
const nowTime = Date.parse(now);
|
||||
for (const record of this.calls.values()) {
|
||||
if ((record.status === "pending" || record.status === "submitted") && record.request.expires_at && Date.parse(record.request.expires_at) <= nowTime) {
|
||||
record.status = "expired";
|
||||
record.updated_at = now;
|
||||
changed.push(copy(record));
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public get(callID: string): ToolCallRecord | undefined {
|
||||
const record = this.calls.get(callID);
|
||||
return record ? copy(record) : undefined;
|
||||
}
|
||||
|
||||
public snapshot(): readonly ToolCallRecord[] {
|
||||
return [...this.calls.values()].map(copy);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.calls.clear();
|
||||
}
|
||||
|
||||
private transition(
|
||||
callID: string,
|
||||
updatedAt: string,
|
||||
allowed: readonly ToolCallStatus[],
|
||||
apply: (record: ToolCallRecord) => void,
|
||||
): ToolCallTransition {
|
||||
const record = this.calls.get(callID);
|
||||
if (!record) return { disposition: "missing" };
|
||||
if (!allowed.includes(record.status)) return { disposition: "invalid_transition", record: copy(record) };
|
||||
apply(record);
|
||||
record.updated_at = updatedAt;
|
||||
return { disposition: "accepted", record: copy(record) };
|
||||
}
|
||||
}
|
||||
|
||||
function copy(record: ToolCallRecord): ToolCallRecord {
|
||||
return {
|
||||
...record,
|
||||
request: { ...record.request, data: { ...record.request.data } },
|
||||
...(record.result ? { result: { ...record.result } } : {}),
|
||||
};
|
||||
}
|
||||
@@ -52,6 +52,18 @@ export class TrustedDOMRendererContext {
|
||||
this.renderSystemMessage(`Agent 错误:${item.message}`);
|
||||
}
|
||||
|
||||
public renderToolCall(item: Extract<ConversationItem, { kind: "tool-call" }>): void {
|
||||
this.renderSystemMessage(`收到 ${item.call.tool} 标准交互请求;交互卡片将在 M1-02 / M1-03 接入。`);
|
||||
}
|
||||
|
||||
public renderToolResult(item: Extract<ConversationItem, { kind: "tool-result" }>): void {
|
||||
this.renderSystemMessage(`收到工具调用结果(${item.result.call_id} · ${item.result.status})。`);
|
||||
}
|
||||
|
||||
public renderToolCancel(item: Extract<ConversationItem, { kind: "tool-cancel" }>): void {
|
||||
this.renderSystemMessage(`工具调用已取消(${item.cancel.call_id})。`);
|
||||
}
|
||||
|
||||
public renderSurface(item: Extract<ConversationItem, { kind: "surface" }>): void {
|
||||
this.renderSystemMessage(`收到扩展界面请求(${item.envelope.type});Mini Runtime Surface Host 将在下一步接入。`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user