支持reasoning

This commit is contained in:
Sirius
2026-08-12 15:42:59 +08:00
parent cfece34a71
commit 8affbf4bed
2 changed files with 207 additions and 89 deletions
+197 -22
View File
@@ -227,7 +227,20 @@ interface ResponseOutputFunctionCall {
status: "completed" | "incomplete";
}
type ResponseOutputItem = ResponseOutputMessage | ResponseOutputFunctionCall;
interface ResponseReasoningSummaryText {
type: "summary_text";
text: string;
}
interface ResponseOutputReasoning {
id: string;
type: "reasoning";
summary: ResponseReasoningSummaryText[];
status: "in_progress" | "completed" | "incomplete";
content?: Array<{ type: "reasoning_text"; text: string }>;
}
type ResponseOutputItem = ResponseOutputMessage | ResponseOutputFunctionCall | ResponseOutputReasoning;
interface ResponseUsage {
input_tokens: number;
@@ -285,11 +298,13 @@ interface StructuredValidation {
interface AttemptResult {
result: CommandResult;
finalChunks: string[];
reasoningItems: ResponseOutputReasoning[];
}
interface ExecutionResult {
finalText: string;
chunks: string[];
reasoningItems: ResponseOutputReasoning[];
usage?: CommandUsage;
validationErrors?: string[];
toolCalls?: FunctionToolCall[];
@@ -411,10 +426,72 @@ class ResponseStore {
}
}
class ResponseReasoningAccumulator {
private readonly completed: ResponseOutputReasoning[] = [];
private current: { id: string; outputIndex: number; text: string } | undefined;
constructor(private readonly writer?: ResponsesSseWriter) {}
event(event: Record<string, unknown>): void {
if (event.type === "thinking_start") {
this.close();
return;
}
if (event.type === "thinking_delta" && typeof event.delta === "string") {
this.add(event.delta);
return;
}
if (
event.type === "thinking_end"
|| event.type === "turn_start"
|| event.type === "text_delta"
|| event.type === "message_end"
|| event.type === "turn_end"
) {
this.close();
}
}
finish(): ResponseOutputReasoning[] {
this.close();
return [...this.completed];
}
private add(delta: string): void {
if (delta === "") return;
if (!this.current) {
this.current = {
id: newReasoningItemId(),
outputIndex: this.completed.length,
text: "",
};
this.writer?.beginReasoning(this.current.id, this.current.outputIndex);
}
this.current.text += delta;
this.writer?.addReasoningDelta(this.current.id, this.current.outputIndex, delta);
}
private close(): void {
if (!this.current) return;
const item: ResponseOutputReasoning = {
id: this.current.id,
type: "reasoning",
summary: [{ type: "summary_text", text: this.current.text }],
status: "completed",
};
this.completed.push(item);
this.writer?.finishReasoning(item, this.current.outputIndex);
this.current = undefined;
}
}
class ResponsesSseWriter {
private sequenceNumber = 0;
private outputStarted = false;
private outputText = "";
private textOutputIndex: number | undefined;
private readonly emittedReasoningIds = new Set<string>();
private readonly completedReasoning = new Map<string, ResponseOutputReasoning>();
constructor(
private readonly response: ServerResponse,
@@ -432,11 +509,72 @@ class ResponsesSseWriter {
this.event("response.in_progress", { response: initial });
}
beginReasoning(itemId: string, outputIndex: number): void {
if (this.emittedReasoningIds.has(itemId)) return;
this.emittedReasoningIds.add(itemId);
this.event("response.output_item.added", {
output_index: outputIndex,
item: {
id: itemId,
type: "reasoning",
summary: [],
status: "in_progress",
},
});
this.event("response.reasoning_summary_part.added", {
item_id: itemId,
output_index: outputIndex,
summary_index: 0,
part: { type: "summary_text", text: "" },
});
}
addReasoningDelta(itemId: string, outputIndex: number, delta: string): void {
if (delta === "" || !this.emittedReasoningIds.has(itemId)) return;
this.event("response.reasoning_summary_text.delta", {
item_id: itemId,
output_index: outputIndex,
summary_index: 0,
delta,
});
}
finishReasoning(item: ResponseOutputReasoning, outputIndex: number): void {
if (this.completedReasoning.has(item.id)) return;
const part = item.summary[0];
if (!part) return;
this.completedReasoning.set(item.id, item);
this.event("response.reasoning_summary_text.done", {
item_id: item.id,
output_index: outputIndex,
summary_index: 0,
text: part.text,
});
this.event("response.reasoning_summary_part.done", {
item_id: item.id,
output_index: outputIndex,
summary_index: 0,
part,
});
this.event("response.output_item.done", { output_index: outputIndex, item });
}
addReasoningItems(items: ResponseOutputReasoning[]): void {
for (const [outputIndex, item] of items.entries()) {
if (this.emittedReasoningIds.has(item.id)) continue;
this.beginReasoning(item.id, outputIndex);
const part = item.summary[0];
if (part?.text) this.addReasoningDelta(item.id, outputIndex, part.text);
this.finishReasoning(item, outputIndex);
}
}
addText(chunks: string[]): void {
if (!this.outputStarted) {
this.outputStarted = true;
this.textOutputIndex = this.completedReasoning.size;
this.event("response.output_item.added", {
output_index: 0,
output_index: this.textOutputIndex,
item: {
id: this.messageId,
type: "message",
@@ -447,7 +585,7 @@ class ResponsesSseWriter {
});
this.event("response.content_part.added", {
item_id: this.messageId,
output_index: 0,
output_index: this.textOutputIndex,
content_index: 0,
part: { type: "output_text", text: "", annotations: [] },
});
@@ -458,7 +596,7 @@ class ResponsesSseWriter {
this.outputText += delta;
this.event("response.output_text.delta", {
item_id: this.messageId,
output_index: 0,
output_index: this.textOutputIndex,
content_index: 0,
delta,
});
@@ -471,28 +609,38 @@ class ResponsesSseWriter {
this.event(terminalEvent, { response });
return;
}
const outputIndex = response.output.findIndex((item) => (
item.type === "message" && item.id === this.messageId
));
if (outputIndex < 0) {
this.event(terminalEvent, { response });
return;
}
this.ensureFinalText(response.output_text);
if (this.outputStarted) {
const outputMessage = response.output[0];
const outputMessage = response.output[outputIndex];
if (!outputMessage || outputMessage.type !== "message") {
throw new Error("Response output message is missing");
}
if (this.textOutputIndex !== outputIndex) {
throw new Error("Response output message index does not match the streamed item index");
}
const part = outputMessage.content[0];
if (!part) throw new Error("Response output text part is missing");
this.event("response.output_text.done", {
item_id: this.messageId,
output_index: 0,
output_index: outputIndex,
content_index: 0,
text: response.output_text,
});
this.event("response.content_part.done", {
item_id: this.messageId,
output_index: 0,
output_index: outputIndex,
content_index: 0,
part,
});
this.event("response.output_item.done", { output_index: 0, item: outputMessage });
this.event("response.output_item.done", { output_index: outputIndex, item: outputMessage });
}
this.event(terminalEvent, { response });
}
@@ -709,6 +857,7 @@ export async function registerResponseRoutes(
effort: effectiveEffort,
status: execution.validationErrors ? "incomplete" : "completed",
finalText: execution.finalText,
reasoningItems: execution.reasoningItems,
...(execution.toolCalls ? { toolCalls: execution.toolCalls } : {}),
...(execution.usage ? { usage: execution.usage } : {}),
...(execution.validationErrors ? { incompleteReason: "structured_output_validation_failed" } : {}),
@@ -718,6 +867,7 @@ export async function registerResponseRoutes(
responseCompleted = true;
if (writer) {
writer.addReasoningItems(execution.reasoningItems);
if (validation) writer.addText(execution.chunks);
writer.finish(
terminalResponse,
@@ -933,6 +1083,7 @@ function buildResponsesPrompt(
"你只负责决定当前这一轮应该调用外部工具还是返回最终回答。外部工具由客户端执行,你不能模拟工具结果。",
"不要使用 Command Code 自身的文件、终端、搜索、网络或其他工具替代外部 tools;只依据 input、tools 和 function_call_output 作出决定。",
"严格执行 input 中的 system、developer、user 指令和当前 instructions,并结合 message、function_call、function_call_output 及其他已有 output item 理解完整历史。",
"历史 reasoning item 只包含此前公开的 summary 文本记录,不是可恢复的内部推理状态;仅把 summary 当作普通历史上下文。",
"只返回以下两个 JSON 对象之一,禁止 Markdown 代码围栏、前后说明和额外字段:",
"最终回答:{\"type\":\"final\",\"content\":\"面向用户的完整回答\"}",
"调用工具:{\"type\":\"tool_calls\",\"calls\":[{\"name\":\"工具名称\",\"arguments\":{}}]}",
@@ -948,6 +1099,7 @@ function buildResponsesPrompt(
return [
"下面 JSON 对象是外部客户端提交的完整 Responses 文本任务和本地重建的响应链。",
"按 input 中的 system、developer、user、assistant 角色与顺序理解上下文;当前 instructions 是本次请求的高优先级开发者指令。",
"历史 reasoning item 只包含此前公开的 summary 文本记录,不是可恢复的内部推理状态;仅把 summary 当作普通历史上下文。",
"严格执行最后一个用户任务。不要复述 JSON,不要输出角色标签,不要暴露内部思考。",
formatDirective(request.text.format),
"JSON 数据开始:",
@@ -1006,6 +1158,7 @@ async function executeResponse(options: {
return {
finalText: first.result.finalText,
chunks: first.finalChunks,
reasoningItems: first.reasoningItems,
...(first.result.usage ? { usage: first.result.usage } : {}),
};
}
@@ -1015,6 +1168,7 @@ async function executeResponse(options: {
return {
finalText: first.result.finalText,
chunks: first.finalChunks,
reasoningItems: first.reasoningItems,
...(first.result.usage ? { usage: first.result.usage } : {}),
};
}
@@ -1032,6 +1186,7 @@ async function executeResponse(options: {
return {
finalText: second.result.finalText,
chunks: second.finalChunks,
reasoningItems: second.reasoningItems,
...(combinedUsage ? { usage: combinedUsage } : {}),
...(!secondValidation.valid ? { validationErrors: secondValidation.errors } : {}),
};
@@ -1094,6 +1249,7 @@ function executionFromToolDecision(
return {
finalText: "",
chunks: [],
reasoningItems: attempt.reasoningItems,
toolCalls: decision.toolCalls,
...(attempt.result.usage ? { usage: attempt.result.usage } : {}),
};
@@ -1101,6 +1257,7 @@ function executionFromToolDecision(
return {
finalText: decision.content,
chunks: [decision.content],
reasoningItems: attempt.reasoningItems,
...(attempt.result.usage ? { usage: attempt.result.usage } : {}),
};
}
@@ -1134,23 +1291,32 @@ async function executeAttempt(
writer?: ResponsesSseWriter,
): Promise<AttemptResult> {
const accumulator = new FinalTurnAccumulator();
const result = await runCommandCode(
options.config,
options.cliModel,
options.effort,
prompt,
options.signal,
{
timeoutMs: options.deadline - Date.now(),
onEvent: (event) => {
const chunks = accumulator.event(event);
if (chunks && writer) writer.addText(chunks);
const reasoning = new ResponseReasoningAccumulator(writer);
let result: CommandResult;
try {
result = await runCommandCode(
options.config,
options.cliModel,
options.effort,
prompt,
options.signal,
{
timeoutMs: options.deadline - Date.now(),
onEvent: (event) => {
reasoning.event(event);
const chunks = accumulator.event(event);
if (chunks && writer) writer.addText(chunks);
},
},
},
);
);
} catch (error) {
reasoning.finish();
throw error;
}
return {
result,
finalChunks: accumulator.finalChunks ?? [result.finalText],
reasoningItems: reasoning.finish(),
};
}
@@ -1211,6 +1377,7 @@ function createResponse(options: {
effort: string;
status: ResponseStatus;
finalText?: string;
reasoningItems?: ResponseOutputReasoning[];
toolCalls?: FunctionToolCall[];
usage?: CommandUsage;
error?: ResponseError;
@@ -1219,7 +1386,7 @@ function createResponse(options: {
const finalText = options.finalText ?? "";
const terminal = options.status !== "in_progress";
const outputStatus = options.status === "completed" ? "completed" : "incomplete";
const output: ResponseOutputItem[] = options.toolCalls
const generatedOutput: ResponseOutputItem[] = options.toolCalls
? options.toolCalls.map((call) => ({
id: newFunctionCallItemId(),
type: "function_call",
@@ -1237,6 +1404,10 @@ function createResponse(options: {
content: [{ type: "output_text", text: finalText, annotations: [] }],
}]
: [];
const output: ResponseOutputItem[] = [
...(options.reasoningItems ?? []),
...generatedOutput,
];
return {
id: options.id,
object: "response",
@@ -1623,6 +1794,10 @@ function newFunctionCallOutputItemId(): string {
return `fco_local_${randomHexId()}`;
}
function newReasoningItemId(): string {
return `rs_local_${randomHexId()}`;
}
function randomHexId(): string {
return randomUUID().replaceAll("-", "");
}