From 8affbf4bedf83418e3b6c8e69404f8d31d9e5745 Mon Sep 17 00:00:00 2001 From: Sirius Date: Wed, 12 Aug 2026 15:42:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81reasoning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 77 +++-------------- src/responses.ts | 219 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 207 insertions(+), 89 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5a9aaea..becd397 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,56 +92,25 @@ curl http://127.0.0.1:18000/v1/chat/completions \ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **command-code-openai-bridge** (106 symbols, 201 relationships, 9 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **command-code-openai-bridge** (428 symbols, 1186 relationships, 33 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. -> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. +> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939). ## Always Do -- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. -- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: `detect_changes({scope: "compare", base_ref: "main"})`. - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. -- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. -- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. - -## When Debugging - -1. `gitnexus_query({query: ""})` — find execution flows related to the issue -2. `gitnexus_context({name: ""})` — see all callers, callees, and process participation -3. `READ gitnexus://repo/command-code-openai-bridge/process/{processName}` — trace the full execution flow step by step -4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed - -## When Refactoring - -- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`. -- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code. -- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed. +- When exploring unfamiliar code, use `query({search_query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`. +- For security review, `explain({target: "fileOrSymbol"})` lists taint findings (source→sink flows; needs `analyze --pdg`). ## Never Do -- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER edit a function, class, or method without first running `impact` on it. - NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. -- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. -- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. - -## Tools Quick Reference - -| Tool | When to use | Command | -|------|-------------|---------| -| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` | -| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` | -| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` | -| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` | -| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` | -| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` | - -## Impact Risk Levels - -| Depth | Meaning | Action | -|-------|---------|--------| -| d=1 | WILL BREAK — direct callers/importers | MUST update these | -| d=2 | LIKELY AFFECTED — indirect deps | Should test | -| d=3 | MAY NEED TESTING — transitive | Test if critical path | +- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph. +- NEVER commit changes without running `detect_changes()` to check affected scope. ## Resources @@ -152,32 +121,6 @@ This project is indexed by GitNexus as **command-code-openai-bridge** (106 symbo | `gitnexus://repo/command-code-openai-bridge/processes` | All execution flows | | `gitnexus://repo/command-code-openai-bridge/process/{name}` | Step-by-step execution trace | -## Self-Check Before Finishing - -Before completing any code modification task, verify: -1. `gitnexus_impact` was run for all modified symbols -2. No HIGH/CRITICAL risk warnings were ignored -3. `gitnexus_detect_changes()` confirms changes match expected scope -4. All d=1 (WILL BREAK) dependents were updated - -## Keeping the Index Fresh - -After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it: - -```bash -npx gitnexus analyze -``` - -If the index previously included embeddings, preserve them by adding `--embeddings`: - -```bash -npx gitnexus analyze --embeddings -``` - -To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.** - -> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`. - ## CLI | Task | Read this skill file | diff --git a/src/responses.ts b/src/responses.ts index c8345c8..2a04ee0 100644 --- a/src/responses.ts +++ b/src/responses.ts @@ -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): 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(); + private readonly completedReasoning = new Map(); 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 { 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("-", ""); }