fix(coding-agent): pass thinking level to branch summarization (#11482)

The `/tree` "Summarize" option ignored the session's thinking level.
`AgentSession` already passes `thinkingLevel` into the
`generateBranchSummary` options, but `generateBranchSummary` never read
it, so the request was built without `reasoning`.

For reasoning models on OpenAI Responses and Azure Responses, a missing
`reasoning` becomes `effort: thinkingLevelMap.off ?? "none"`. Models
that don't accept `"none"` then reject the branch summary with a 400
error:

```
Unsupported value: 'none' is not supported with the 'gpt-6-astra-…' model.
```

This change adds `thinkingLevel` to `GenerateBranchSummaryOptions`. It
sets `reasoning` on the request when the model supports reasoning and
the level is not `off`, the same rule compaction uses in
`createSummarizationOptions`.

**Testing**
- Added a test to
`packages/coding-agent/test/branch-summarization.test.ts` that records
the stream options for three cases: a reasoning model at `xhigh`, a
reasoning model at `off`, and a non-reasoning model. The expected
`reasoning` values are `["xhigh", undefined, undefined]`. Without the
fix, the test fails and every case gets `undefined`. With the fix, it
passes.
- Ran `branch-summarization`, `agent-session-branching`,
`branch-summary-extensions` and `compaction-summary-reasoning`: all
pass.
- `npm run check` passes.
- Not tested against a real provider endpoint.

Fixes #11444

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
This commit is contained in:
robopipi[bot] 2026-10-27 15:38:06 +01:00 • committed by GitHub
parent 842c73170a
commit 269afc66ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 27 additions and 1 deletions

View file

@ -5,7 +5,7 @@
* a summary of the branch being left so context isn't lost.
*/
import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core";
import type { AgentMessage, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core";
import type { RetryCallbacks, RetryPolicy } from "@earendil-works/pi-ai";
import { contentText, normalizeContext } from "@earendil-works/pi-ai";
import type { Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai/compat";
@ -75,6 +75,8 @@ export interface GenerateBranchSummaryOptions {
env?: Record<string, string>;
/** Abort signal for cancellation */
signal: AbortSignal;
/** Thinking level for reasoning-capable models */
thinkingLevel?: ThinkingLevel;
/** Optional custom instructions for summarization */
customInstructions?: string;
/** If true, customInstructions replaces the default prompt instead of being appended */
@ -300,6 +302,7 @@ export async function generateBranchSummary(
headers,
env,
signal,
thinkingLevel,
customInstructions,
replaceInstructions,
reserveTokens = 16384,
@ -348,6 +351,9 @@ export async function generateBranchSummary(
// so transient stream drops reuse the configured retry policy.
const context = normalizeContext({ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages });
const requestOptions: SimpleStreamOptions = { apiKey, headers, env, signal };
if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
requestOptions.reasoning = thinkingLevel;
}
const response = await completeSummarization(model, context, requestOptions, streamFn, retry, callbacks);
// Check if aborted or errored

View file

@ -44,6 +44,26 @@ function response(content: AssistantMessage["content"]): AssistantMessage {
}
describe("branch summarization", () => {
it("passes the thinking level only for reasoning-capable models", async () => {
const seen: Array<SimpleStreamOptions["reasoning"]> = [];
const streamFn: StreamFn = (_model, _context, options) => {
seen.push(options?.reasoning);
const stream = createAssistantMessageEventStream();
queueMicrotask(() =>
stream.push({ type: "done", reason: "stop", message: response([{ type: "text", text: "summary" }]) }),
);
return stream;
};
const reasoningModel = { ...model, reasoning: true };
const signal = new AbortController().signal;
await generateBranchSummary(entries, { model: reasoningModel, signal, streamFn, thinkingLevel: "xhigh" });
await generateBranchSummary(entries, { model: reasoningModel, signal, streamFn, thinkingLevel: "off" });
await generateBranchSummary(entries, { model, signal, streamFn, thinkingLevel: "high" });
expect(seen).toEqual(["xhigh", undefined, undefined]);
});
it("does not override tool choice for branch summaries", async () => {
let requestOptions: SimpleStreamOptions | undefined;
const streamFn: StreamFn = (_model, _context, options) => {