支持responses

This commit is contained in:
Sirius
2026-08-12 15:01:43 +08:00
parent 1e19973243
commit 3b9454c787
4 changed files with 662 additions and 89 deletions
+507 -39
View File
@@ -15,10 +15,23 @@ import type { BridgeConfig } from "./config.js";
import { FinalTurnAccumulator } from "./final-turn.js";
import { openAIError, type CommandUsage } from "./openai.js";
import type { RequestCoordinator } from "./request-coordinator.js";
import {
parseFunctionToolDecision,
parseFunctionToolArguments,
ToolDecisionError,
validateFunctionToolArguments,
validateFunctionToolSchemas,
type FunctionToolCall,
type FunctionToolChoice,
type FunctionToolDecisionRequest,
type FunctionToolDefinition,
} from "./tool-calling.js";
const responseIdPattern = /^resp_[A-Za-z0-9_-]{1,128}$/;
const responseIdSchema = z.string().regex(responseIdPattern, "Invalid response ID");
const roleSchema = z.enum(["system", "developer", "user", "assistant"]);
const functionNameSchema = z.string().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/);
const responseItemIdSchema = z.string().min(1).max(128);
const inputTextPartSchema = z.object({
type: z.literal("input_text"),
@@ -39,6 +52,42 @@ const inputMessageSchema = z.object({
]),
}).strict();
const inputFunctionCallSchema = z.object({
id: responseItemIdSchema.optional(),
type: z.literal("function_call"),
call_id: responseItemIdSchema,
name: functionNameSchema,
arguments: z.string(),
status: z.enum(["in_progress", "completed", "incomplete"]).optional(),
}).strict();
const inputFunctionCallOutputSchema = z.object({
id: responseItemIdSchema.optional(),
type: z.literal("function_call_output"),
call_id: responseItemIdSchema,
output: z.string(),
status: z.enum(["in_progress", "completed", "incomplete"]).optional(),
}).strict();
const responseInputItemSchema = z.union([
inputMessageSchema,
inputFunctionCallSchema,
inputFunctionCallOutputSchema,
]);
const responseFunctionToolSchema = z.object({
type: z.literal("function"),
name: functionNameSchema,
description: z.string().nullable().optional(),
parameters: z.union([z.record(z.unknown()), z.null()]).optional(),
strict: z.boolean().nullable().optional(),
}).strict();
const responseToolChoiceSchema = z.union([
z.enum(["none", "auto", "required"]),
z.object({ type: z.literal("function"), name: functionNameSchema }).strict(),
]);
const textFormatSchema = z.object({
type: z.literal("text"),
}).strict();
@@ -74,7 +123,7 @@ const metadataSchema = z.record(z.string().max(512)).superRefine((metadata, cont
export const responseRequestSchema = z.object({
model: z.string().min(1),
input: z.union([z.string(), z.array(inputMessageSchema).min(1)]),
input: z.union([z.string(), z.array(responseInputItemSchema).min(1)]),
instructions: z.string().nullable().optional().default(null),
stream: z.boolean().optional().default(false),
store: z.boolean().optional().default(true),
@@ -86,7 +135,38 @@ export const responseRequestSchema = z.object({
text: z.object({
format: responseTextFormatSchema,
}).strict().optional().default({ format: { type: "text" } }),
}).strict();
tools: z.array(responseFunctionToolSchema).min(1).max(128).optional(),
tool_choice: responseToolChoiceSchema.optional(),
parallel_tool_calls: z.boolean().optional(),
}).strict().superRefine((request, context) => {
const toolNames = new Set<string>();
for (const [index, tool] of (request.tools ?? []).entries()) {
if (toolNames.has(tool.name)) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ["tools", index, "name"],
message: `Duplicate tool name '${tool.name}'`,
});
}
toolNames.add(tool.name);
}
if (request.tool_choice !== undefined && request.tools === undefined) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ["tool_choice"],
message: "tool_choice requires tools",
});
}
if (typeof request.tool_choice === "object" && !toolNames.has(request.tool_choice.name)) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ["tool_choice", "name"],
message: `Unknown forced tool '${request.tool_choice.name}'`,
});
}
});
type ResponseRequest = z.infer<typeof responseRequestSchema>;
export type ResponseTextFormat = z.infer<typeof responseTextFormatSchema>;
@@ -98,13 +178,32 @@ interface ResponseInputPart {
text: string;
}
interface ResponseInputItem {
interface ResponseInputMessage {
id: string;
type: "message";
role: ResponseRole;
content: ResponseInputPart[];
}
interface ResponseInputFunctionCall {
id: string;
type: "function_call";
call_id: string;
name: string;
arguments: string;
status: "in_progress" | "completed" | "incomplete";
}
interface ResponseInputFunctionCallOutput {
id: string;
type: "function_call_output";
call_id: string;
output: string;
status: "in_progress" | "completed" | "incomplete";
}
type ResponseInputItem = ResponseInputMessage | ResponseInputFunctionCall | ResponseInputFunctionCallOutput;
interface ResponseOutputText {
type: "output_text";
text: string;
@@ -119,6 +218,17 @@ interface ResponseOutputMessage {
content: ResponseOutputText[];
}
interface ResponseOutputFunctionCall {
id: string;
type: "function_call";
call_id: string;
name: string;
arguments: string;
status: "completed" | "incomplete";
}
type ResponseOutputItem = ResponseOutputMessage | ResponseOutputFunctionCall;
interface ResponseUsage {
input_tokens: number;
output_tokens: number;
@@ -140,26 +250,34 @@ interface LocalResponse {
incomplete_details: { reason: string } | null;
instructions: string | null;
model: string;
output: ResponseOutputMessage[];
output: ResponseOutputItem[];
output_text: string;
previous_response_id: string | null;
reasoning: { effort: string | null; summary: null };
store: boolean;
metadata: Record<string, string>;
text: { format: ResponseTextFormat };
tools: ResponseFunctionTool[];
tool_choice: ResponseToolChoice;
parallel_tool_calls: boolean;
usage: ResponseUsage | null;
}
interface ResponseFunctionTool {
type: "function";
name: string;
description?: string | null;
parameters: Record<string, unknown>;
strict: boolean | null;
}
type ResponseToolChoice = "none" | "auto" | "required" | { type: "function"; name: string };
interface StoredResponse {
response: LocalResponse;
input_items: ResponseInputItem[];
}
interface PromptMessage {
role: ResponseRole;
content: string;
}
interface StructuredValidation {
validate: (text: string) => { valid: boolean; errors: string[] };
}
@@ -174,6 +292,7 @@ interface ExecutionResult {
chunks: string[];
usage?: CommandUsage;
validationErrors?: string[];
toolCalls?: FunctionToolCall[];
}
class ResponseApiError extends Error {
@@ -347,10 +466,17 @@ class ResponsesSseWriter {
}
finish(response: LocalResponse, terminalEvent: "response.completed" | "response.incomplete"): void {
if (response.output.some((item) => item.type === "function_call")) {
this.addToolCalls(response);
this.event(terminalEvent, { response });
return;
}
this.ensureFinalText(response.output_text);
if (this.outputStarted) {
const outputMessage = response.output[0];
if (!outputMessage) throw new Error("Response output message is missing");
if (!outputMessage || outputMessage.type !== "message") {
throw new Error("Response output message is missing");
}
const part = outputMessage.content[0];
if (!part) throw new Error("Response output text part is missing");
@@ -371,6 +497,27 @@ class ResponsesSseWriter {
this.event(terminalEvent, { response });
}
private addToolCalls(response: LocalResponse): void {
for (const [outputIndex, item] of response.output.entries()) {
if (item.type !== "function_call") continue;
const addedItem = { ...item, arguments: "", status: "in_progress" as const };
this.event("response.output_item.added", { output_index: outputIndex, item: addedItem });
if (item.arguments !== "") {
this.event("response.function_call_arguments.delta", {
item_id: item.id,
output_index: outputIndex,
delta: item.arguments,
});
}
this.event("response.function_call_arguments.done", {
item_id: item.id,
output_index: outputIndex,
arguments: item.arguments,
});
this.event("response.output_item.done", { output_index: outputIndex, item });
}
}
failed(response: LocalResponse): void {
this.event("response.failed", { response });
}
@@ -479,6 +626,7 @@ export async function registerResponseRoutes(
try {
parsed = parseResponseRequest(request.body);
validation = createStructuredValidation(parsed.text.format);
validateResponseToolRequest(parsed);
} catch (error) {
return sendRouteError(reply, error);
}
@@ -535,8 +683,10 @@ export async function registerResponseRoutes(
const chain = parsed.previous_response_id
? await store.loadChain(parsed.previous_response_id)
: [];
validateResponseFunctionHistory(parsed, chain, inputItems);
const prompt = buildResponsesPrompt(parsed, chain, inputItems);
const deadline = Date.now() + config.timeout_seconds * 1000;
const toolRequest = responseFunctionToolDecisionRequest(parsed);
const execution = await executeResponse({
config,
@@ -544,6 +694,7 @@ export async function registerResponseRoutes(
effort: effectiveEffort,
prompt,
format: parsed.text.format,
...(toolRequest ? { toolRequest } : {}),
signal: abortController.signal,
deadline,
...(validation ? { validation } : {}),
@@ -558,6 +709,7 @@ export async function registerResponseRoutes(
effort: effectiveEffort,
status: execution.validationErrors ? "incomplete" : "completed",
finalText: execution.finalText,
...(execution.toolCalls ? { toolCalls: execution.toolCalls } : {}),
...(execution.usage ? { usage: execution.usage } : {}),
...(execution.validationErrors ? { incompleteReason: "structured_output_validation_failed" } : {}),
});
@@ -668,6 +820,7 @@ function findUnsupportedParameter(body: unknown): string | undefined {
const supported = new Set([
"model", "input", "instructions", "stream", "store",
"previous_response_id", "metadata", "reasoning", "text",
"tools", "tool_choice", "parallel_tool_calls",
]);
const unknownTopLevel = Object.keys(body).find((key) => !supported.has(key));
if (unknownTopLevel) return unknownTopLevel;
@@ -679,10 +832,27 @@ function findUnsupportedParameter(body: unknown): string | undefined {
}
}
if (Array.isArray(body.tools)) {
for (const [toolIndex, tool] of body.tools.entries()) {
if (!isRecord(tool)) continue;
if (tool.type !== "function") return `tools.${toolIndex}.type`;
}
}
if (isRecord(body.tool_choice) && body.tool_choice.type !== "function") {
return "tool_choice.type";
}
if (!Array.isArray(body.input)) return undefined;
for (const [itemIndex, item] of body.input.entries()) {
if (!isRecord(item)) continue;
if (item.type !== undefined && item.type !== "message") return `input.${itemIndex}.type`;
if (
item.type !== undefined
&& item.type !== "message"
&& item.type !== "function_call"
&& item.type !== "function_call_output"
) return `input.${itemIndex}.type`;
if (item.type === "function_call" || item.type === "function_call_output") continue;
if (!Array.isArray(item.content)) continue;
for (const [partIndex, part] of item.content.entries()) {
if (!isRecord(part)) continue;
@@ -704,14 +874,35 @@ function normalizeInput(input: ResponseRequest["input"]): ResponseInputItem[] {
}];
}
return input.map((message) => ({
id: newMessageId(),
type: "message",
role: message.role,
content: typeof message.content === "string"
? [{ type: message.role === "assistant" ? "output_text" : "input_text", text: message.content }]
: message.content.map((part) => ({ type: part.type, text: part.text })),
}));
return input.map((item) => {
if (item.type === "function_call") {
return {
id: item.id ?? newFunctionCallItemId(),
type: "function_call",
call_id: item.call_id,
name: item.name,
arguments: item.arguments,
status: item.status ?? "completed",
};
}
if (item.type === "function_call_output") {
return {
id: item.id ?? newFunctionCallOutputItemId(),
type: "function_call_output",
call_id: item.call_id,
output: item.output,
status: item.status ?? "completed",
};
}
return {
id: newMessageId(),
type: "message",
role: item.role,
content: typeof item.content === "string"
? [{ type: item.role === "assistant" ? "output_text" : "input_text", text: item.content }]
: item.content.map((part) => ({ type: part.type, text: part.text })),
};
});
}
function buildResponsesPrompt(
@@ -719,24 +910,44 @@ function buildResponsesPrompt(
chain: StoredResponse[],
currentInput: ResponseInputItem[],
): string {
const messages: PromptMessage[] = [];
const history: Array<ResponseInputItem | ResponseOutputItem> = [];
for (const stored of chain) {
messages.push(...stored.input_items.map(inputItemToPromptMessage));
if (stored.response.output_text !== "") {
messages.push({ role: "assistant", content: stored.response.output_text });
}
history.push(...stored.input_items, ...stored.response.output);
}
messages.push(...currentInput.map(inputItemToPromptMessage));
history.push(...currentInput);
const envelope = {
protocol: "openai-responses-text-history-v1",
protocol: usesResponseToolCalling(request, history)
? "openai-responses-tools-history-v1"
: "openai-responses-text-history-v1",
instructions: request.instructions,
messages,
input: history,
tools: normalizeResponseTools(request.tools),
tool_choice: effectiveResponseToolChoice(request),
parallel_tool_calls: request.parallel_tool_calls ?? true,
};
if (usesResponseToolCalling(request, history)) {
return [
"下面 JSON 对象是外部客户端提交的一次 OpenAI Responses 工具调用任务,input 包含完整历史。",
"你只负责决定当前这一轮应该调用外部工具还是返回最终回答。外部工具由客户端执行,你不能模拟工具结果。",
"不要使用 Command Code 自身的文件、终端、搜索、网络或其他工具替代外部 tools;只依据 input、tools 和 function_call_output 作出决定。",
"严格执行 input 中的 system、developer、user 指令和当前 instructions,并结合 message、function_call、function_call_output 及其他已有 output item 理解完整历史。",
"只返回以下两个 JSON 对象之一,禁止 Markdown 代码围栏、前后说明和额外字段:",
"最终回答:{\"type\":\"final\",\"content\":\"面向用户的完整回答\"}",
"调用工具:{\"type\":\"tool_calls\",\"calls\":[{\"name\":\"工具名称\",\"arguments\":{}}]}",
"调用工具时,name 必须与 tools 中的名称完全一致,arguments 必须是符合该工具 parameters JSON Schema 的对象。",
"tool_choice 为 none 时必须返回 final;为 required 或指定函数时必须返回 tool_calls。parallel_tool_calls 为 false 时 calls 只能有一项。",
"需要工具提供信息或执行动作时返回 tool_calls 并立即结束;已有 function_call_output 且信息足够时返回 final。工具调用轮次禁止生成面向用户的正文。",
formatDirective(request.text.format),
"JSON 数据开始:",
JSON.stringify(envelope),
].join("\n\n");
}
return [
"下面 JSON 对象是外部客户端提交的完整 Responses 文本任务和本地重建的响应链。",
"按 system、developer、user、assistant 角色与顺序理解上下文;当前 instructions 是本次请求的高优先级开发者指令。",
"按 input 中的 system、developer、user、assistant 角色与顺序理解上下文;当前 instructions 是本次请求的高优先级开发者指令。",
"严格执行最后一个用户任务。不要复述 JSON,不要输出角色标签,不要暴露内部思考。",
formatDirective(request.text.format),
"JSON 数据开始:",
@@ -780,12 +991,17 @@ async function executeResponse(options: {
effort: string;
prompt: string;
format: ResponseTextFormat;
toolRequest?: FunctionToolDecisionRequest;
validation?: StructuredValidation;
signal: AbortSignal;
deadline: number;
writer?: ResponsesSseWriter;
}): Promise<ExecutionResult> {
const first = await executeAttempt(options, options.prompt, options.validation ? undefined : options.writer);
const constrained = options.validation !== undefined || options.toolRequest !== undefined;
const first = await executeAttempt(options, options.prompt, constrained ? undefined : options.writer);
if (options.toolRequest) {
return executeToolDecision({ ...options, toolRequest: options.toolRequest }, first);
}
if (!options.validation) {
return {
finalText: first.result.finalText,
@@ -821,6 +1037,91 @@ async function executeResponse(options: {
};
}
async function executeToolDecision(
options: {
config: BridgeConfig;
cliModel: string;
effort: string;
prompt: string;
format: ResponseTextFormat;
toolRequest: FunctionToolDecisionRequest;
validation?: StructuredValidation;
signal: AbortSignal;
deadline: number;
},
first: AttemptResult,
): Promise<ExecutionResult> {
try {
return executionFromToolDecision(
parseFunctionToolDecision(first.result.finalText, options.toolRequest),
first,
);
} catch (error) {
if (!(error instanceof ToolDecisionError)) throw error;
process.stderr.write(`\nResponses 工具调用决策校验失败,正在修复:${error.errors.join("; ")}\n`);
const repairPrompt = buildToolDecisionRepairPrompt(
options.prompt,
first.result.finalText,
error.errors,
);
const second = await executeAttempt(options, repairPrompt);
let decision;
try {
decision = parseFunctionToolDecision(second.result.finalText, options.toolRequest);
} catch (repairError) {
if (repairError instanceof ToolDecisionError) {
throw new ResponseApiError(
`Invalid tool decision after repair: ${repairError.errors.join("; ")}`,
502,
"invalid_tool_decision",
"server_error",
);
}
throw repairError;
}
const result = executionFromToolDecision(decision, second);
const usage = addUsage(first.result.usage, second.result.usage);
return { ...result, ...(usage ? { usage } : {}) };
}
}
function executionFromToolDecision(
decision: ReturnType<typeof parseFunctionToolDecision>,
attempt: AttemptResult,
): ExecutionResult {
if (decision.type === "tool_calls") {
return {
finalText: "",
chunks: [],
toolCalls: decision.toolCalls,
...(attempt.result.usage ? { usage: attempt.result.usage } : {}),
};
}
return {
finalText: decision.content,
chunks: [decision.content],
...(attempt.result.usage ? { usage: attempt.result.usage } : {}),
};
}
function buildToolDecisionRepairPrompt(
originalPrompt: string,
invalidOutput: string,
errors: string[],
): string {
return [
"上一次输出不符合 Responses 外部工具调用传输协议。保持原来的决策意图,只修复 JSON 结构、工具名称或参数。",
"只返回原任务要求的 final 或 tool_calls JSON 对象,禁止 Markdown 代码围栏、前后说明和额外字段。",
"校验错误:",
errors.join("\n"),
"上一次输出:",
invalidOutput,
"原始任务:",
originalPrompt,
].join("\n\n");
}
async function executeAttempt(
options: {
config: BridgeConfig;
@@ -910,14 +1211,32 @@ function createResponse(options: {
effort: string;
status: ResponseStatus;
finalText?: string;
toolCalls?: FunctionToolCall[];
usage?: CommandUsage;
error?: ResponseError;
incompleteReason?: string;
}): LocalResponse {
const finalText = options.finalText ?? "";
const terminal = options.status !== "in_progress";
const hasOutput = options.finalText !== undefined;
const outputStatus = options.status === "completed" ? "completed" : "incomplete";
const output: ResponseOutputItem[] = options.toolCalls
? options.toolCalls.map((call) => ({
id: newFunctionCallItemId(),
type: "function_call",
call_id: call.callId,
name: call.name,
arguments: call.arguments,
status: outputStatus,
}))
: options.finalText !== undefined
? [{
id: options.messageId,
type: "message",
role: "assistant",
status: outputStatus,
content: [{ type: "output_text", text: finalText, annotations: [] }],
}]
: [];
return {
id: options.id,
object: "response",
@@ -928,19 +1247,16 @@ function createResponse(options: {
incomplete_details: options.incompleteReason ? { reason: options.incompleteReason } : null,
instructions: options.request.instructions,
model: options.request.model,
output: hasOutput ? [{
id: options.messageId,
type: "message",
role: "assistant",
status: outputStatus,
content: [{ type: "output_text", text: finalText, annotations: [] }],
}] : [],
output,
output_text: finalText,
previous_response_id: options.request.previous_response_id,
reasoning: { effort: options.effort, summary: null },
store: options.request.store,
metadata: options.request.metadata,
text: { format: options.request.text.format },
tools: normalizeResponseTools(options.request.tools),
tool_choice: effectiveResponseToolChoice(options.request),
parallel_tool_calls: options.request.parallel_tool_calls ?? true,
usage: toResponseUsage(options.usage),
};
}
@@ -1115,8 +1431,152 @@ function responseNotFound(id: string): ResponseApiError {
);
}
function inputItemToPromptMessage(item: ResponseInputItem): PromptMessage {
return { role: item.role, content: item.content.map((part) => part.text).join("") };
function validateResponseToolRequest(request: ResponseRequest): void {
const errors = validateFunctionToolSchemas(responseFunctionTools(request));
if (errors.length > 0) {
throw new ResponseApiError(
errors.join("; "),
400,
"invalid_tool_schema",
"invalid_request_error",
"tools",
);
}
}
function validateResponseFunctionHistory(
request: ResponseRequest,
chain: StoredResponse[],
currentInput: ResponseInputItem[],
): void {
const tools = new Map(responseFunctionTools(request).map((tool) => [tool.name, tool]));
const callIds = new Set<string>();
const outputCallIds = new Set<string>();
const items: Array<{ item: ResponseInputItem | ResponseOutputItem; param: string }> = [];
for (const stored of chain) {
for (const item of stored.input_items) items.push({ item, param: "previous_response_id" });
for (const item of stored.response.output) items.push({ item, param: "previous_response_id" });
}
for (const [index, item] of currentInput.entries()) items.push({ item, param: `input.${index}` });
for (const { item, param } of items) {
if (item.type === "function_call") {
if (callIds.has(item.call_id)) {
throw new ResponseApiError(
`Duplicate function call_id '${item.call_id}'`,
400,
"invalid_function_call",
"invalid_request_error",
`${param}.call_id`,
);
}
callIds.add(item.call_id);
const tool = tools.get(item.name);
if (!tool) {
throw new ResponseApiError(
`Function call references unknown tool '${item.name}'`,
400,
"invalid_tool_name",
"invalid_request_error",
`${param}.name`,
);
}
let args: Record<string, unknown>;
try {
args = parseFunctionToolArguments(item.arguments, `${param}.arguments`);
} catch (error) {
if (error instanceof ToolDecisionError) {
throw new ResponseApiError(
error.errors.join("; "),
400,
"invalid_tool_arguments",
"invalid_request_error",
`${param}.arguments`,
);
}
throw error;
}
const errors = validateFunctionToolArguments(tool, item.name, args);
if (errors.length > 0) {
throw new ResponseApiError(
errors.join("; "),
400,
"invalid_tool_arguments",
"invalid_request_error",
`${param}.arguments`,
);
}
continue;
}
if (item.type === "function_call_output") {
if (!callIds.has(item.call_id)) {
throw new ResponseApiError(
`No preceding function_call found for call_id '${item.call_id}'`,
400,
"invalid_function_call_output",
"invalid_request_error",
`${param}.call_id`,
);
}
if (outputCallIds.has(item.call_id)) {
throw new ResponseApiError(
`Duplicate function_call_output for call_id '${item.call_id}'`,
400,
"invalid_function_call_output",
"invalid_request_error",
`${param}.call_id`,
);
}
outputCallIds.add(item.call_id);
}
}
}
function responseFunctionTools(request: ResponseRequest): FunctionToolDefinition[] {
return normalizeResponseTools(request.tools).map((tool) => ({
name: tool.name,
...(tool.description !== undefined ? { description: tool.description } : {}),
parameters: tool.parameters,
strict: tool.strict,
}));
}
function responseFunctionToolDecisionRequest(
request: ResponseRequest,
): FunctionToolDecisionRequest | undefined {
if (!request.tools) return undefined;
const toolChoice = effectiveResponseToolChoice(request);
return {
tools: responseFunctionTools(request),
toolChoice: typeof toolChoice === "string" ? toolChoice : { name: toolChoice.name },
parallelToolCalls: request.parallel_tool_calls ?? true,
};
}
function normalizeResponseTools(tools: ResponseRequest["tools"]): ResponseFunctionTool[] {
return (tools ?? []).map((tool) => ({
type: "function",
name: tool.name,
...(tool.description !== undefined ? { description: tool.description } : {}),
parameters: tool.parameters ?? { type: "object", properties: {} },
strict: tool.strict ?? null,
}));
}
function effectiveResponseToolChoice(request: ResponseRequest): ResponseToolChoice {
return request.tool_choice ?? (request.tools ? "auto" : "none");
}
function usesResponseToolCalling(
request: ResponseRequest,
history: Array<ResponseInputItem | ResponseOutputItem>,
): boolean {
return request.tools !== undefined || history.some((item) => (
item.type === "function_call" || item.type === "function_call_output"
));
}
function toResponseUsage(usage?: CommandUsage): ResponseUsage | null {
@@ -1155,6 +1615,14 @@ function newMessageId(): string {
return `msg_local_${randomHexId()}`;
}
function newFunctionCallItemId(): string {
return `fc_local_${randomHexId()}`;
}
function newFunctionCallOutputItemId(): string {
return `fco_local_${randomHexId()}`;
}
function randomHexId(): string {
return randomUUID().replaceAll("-", "");
}