支持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
+10 -67
View File
@@ -92,56 +92,25 @@ curl http://127.0.0.1:18000/v1/chat/completions \
<!-- gitnexus:start -->
# 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: "<error or symptom>"})` — find execution flows related to the issue
2. `gitnexus_context({name: "<suspect function>"})` — 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 |
+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("-", "");
}