diff --git a/.changeset/fn-199-reasoning-summary.md b/.changeset/fn-199-reasoning-summary.md
new file mode 100644
index 0000000000..31bebec92f
--- /dev/null
+++ b/.changeset/fn-199-reasoning-summary.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Show detailed reasoning bodies alongside titles for supported Responses models.
+category: fix
+dev: Uses the pi Agent `onPayload` seam only for the OpenAI Responses API family.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index d50ef0ccb5..f24144ca6d 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -110,7 +110,7 @@ Press `Escape` to close the current/topmost dashboard popup. Popped-out task win
### Thinking traces
-Thinking panes split titled reasoning traces with captured bodies into independently expandable sections, with **Collapse all** and **Expand all** controls. Headings without captured reasoning stay inline in the flowing trace instead of becoming empty collapsible rows, and the defensive empty-state label appears at most once per section. **Raw trace** shows the original unsectioned capture and switches back with **Sectioned trace**; it is available for every trace that contained headings, including a titles-only trace rendered as one flowing block. The workflow live-log console remains raw and unsectioned by design. The same behavior applies while Planning Mode, Mission Interview, and Milestone/Slice Interview stream a generation.
+Thinking panes split titled reasoning traces with captured bodies into independently expandable sections, with **Collapse all** and **Expand all** controls. Responses-family models now request titled sections with their reasoning bodies. A titles-only trace can still arrive from a provider Fusion cannot configure; those headings stay inline in the flowing trace instead of becoming empty collapsible rows, and the defensive empty-state label appears at most once per section. **Raw trace** shows the original unsectioned capture and switches back with **Sectioned trace**; use it to diagnose a provider-side titles-only payload. The workflow live-log console remains raw and unsectioned by design. The same behavior applies while Planning Mode, Mission Interview, and Milestone/Slice Interview stream a generation.
### Chat Find
diff --git a/docs/settings-reference.md b/docs/settings-reference.md
index 0d45ad454b..03ff22c8bf 100644
--- a/docs/settings-reference.md
+++ b/docs/settings-reference.md
@@ -103,6 +103,10 @@ Fusion persists one canonical ordered vocabulary: `off`, `minimal`, `low`, `medi
Custom-provider models are presumed thinking-capable and expose all seven levels by default; no capability checkbox or other configuration is required. Fusion registers these levels with pi as transmissible, while pi owns API-specific `off` translation and up-then-down clamping. Consequently, a global or inherited thinking level that custom providers previously ignored is now sent to the gateway. A strict gateway can reject an unsupported effort; select **Off** for that gateway, which pi translates to its explicit non-reasoning form or omits when the API has no such form.
+#### Reasoning-summary detail
+
+When a real thinking effort is resolved, Fusion requests `reasoning.summary: "detailed"` for Responses-family models: `openai-responses`, `openai-codex-responses`, and `azure-openai-responses`. Pi forwards the effort through its simple stream path but not this summary preference; without Fusion's request shaping, those APIs use their shortest `"auto"` summary. Non-Responses APIs are unchanged. If a provider rejects detailed summaries, Fusion retries that run with the previous request shape. Detailed summaries can consume slightly more output tokens.
+
| `ntfyEnabled` | `boolean` | `false` | Enable ntfy push notifications. |
| `agentClarificationEnabled` | `boolean` | `false` | Legacy default for programmatic Planning Mode session notification eligibility. Dashboard Planning Mode always starts its infinite, user-validated interview with follow-up questions enabled; this setting no longer suppresses questions or creates a final summary. |
| `failureNotificationMode` | `"sticky-only" \| "terminal-only" \| "all"` | `"sticky-only"` | Failure notification behavior. `sticky-only` defers failed-task notifications by `failureNotificationDelayMs` and suppresses transient self-recoveries. `terminal-only` suppresses while auto-retry is still active and only dispatches when `paused === true` or `column === "in-review"` with `status === "failed"`. `all` restores legacy immediate failure notifications. |
diff --git a/packages/dashboard/app/components/__tests__/ThinkingTrace.surfaces.test.tsx b/packages/dashboard/app/components/__tests__/ThinkingTrace.surfaces.test.tsx
index 2e3010f412..1040fb7dfb 100644
--- a/packages/dashboard/app/components/__tests__/ThinkingTrace.surfaces.test.tsx
+++ b/packages/dashboard/app/components/__tests__/ThinkingTrace.surfaces.test.tsx
@@ -43,6 +43,25 @@ describe("ThinkingTrace production transcript surfaces", () => {
expectTraceIsolation(disclosure);
});
+ it("keeps detailed bodies in the mobile live-thinking pane", () => {
+ const previousWidth = window.innerWidth;
+ Object.defineProperty(window, "innerWidth", { configurable: true, value: 375 });
+ try {
+ const { container } = render();
+ const disclosure = container.querySelector("details.chat-message-thinking") as HTMLDetailsElement;
+ fireEvent.click(disclosure.querySelector("summary")!);
+
+ const sections = disclosure.querySelectorAll("[data-testid='thinking-trace-section']");
+ expect(sections).toHaveLength(3);
+ expect(sections[0]).toHaveAttribute("open");
+ expect(sections[0]).toHaveTextContent("Docker tests need development dependencies.");
+ expect(sections[1]).toHaveTextContent("Deployment commits remain independently reviewable.");
+ expect(sections[2]).toHaveTextContent("README edits remain visible in their own section.");
+ } finally {
+ Object.defineProperty(window, "innerWidth", { configurable: true, value: previousWidth });
+ }
+ });
+
it("renders the same isolated trace in agent logs in markdown and plain modes", () => {
const entries = [{ taskId: "FN-155", timestamp: "2026-08-22T00:00:00.000Z", type: "thinking", text: trace }] as AgentLogEntry[];
const { container, rerender } = render();
diff --git a/packages/dashboard/app/components/__tests__/ThinkingTrace.test.tsx b/packages/dashboard/app/components/__tests__/ThinkingTrace.test.tsx
index d2f0e9e5f6..a5d6984288 100644
--- a/packages/dashboard/app/components/__tests__/ThinkingTrace.test.tsx
+++ b/packages/dashboard/app/components/__tests__/ThinkingTrace.test.tsx
@@ -76,6 +76,10 @@ describe("ThinkingTrace", () => {
it("keeps every populated title expanded and isolated until its own section is collapsed", () => {
render();
expect(sections()).toHaveLength(4);
+ const renderedBodies = sections().map((section) => section.textContent).join("\n");
+ expect(renderedBodies).toContain("The Docker image needs development dependencies for test execution.");
+ expect(renderedBodies).toContain("Deployment commits should be split by independently reviewable behavior.");
+ expect(renderedBodies).toContain("The README change belongs in its own reviewed update.");
const deployment = sections().find((section) => section.textContent?.includes("Planning deployment commit structure"))!;
expect(deployment.textContent).toContain("Deployment commits should be split by independently reviewable behavior.");
fireEvent.click(within(deployment).getByText("Planning deployment commit structure"));
diff --git a/packages/engine/src/__tests__/pi-reasoning-summary.test.ts b/packages/engine/src/__tests__/pi-reasoning-summary.test.ts
new file mode 100644
index 0000000000..c41fb39d94
--- /dev/null
+++ b/packages/engine/src/__tests__/pi-reasoning-summary.test.ts
@@ -0,0 +1,150 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const createAgentSessionMock = vi.fn();
+const modelRegistry = {
+ modelRuntime: { getAuth: vi.fn(), refresh: vi.fn() },
+ find: vi.fn((provider: string, id: string) => ({ provider, id })),
+ getAll: vi.fn(() => []),
+ registerProvider: vi.fn(),
+};
+
+vi.mock("@earendil-works/pi-coding-agent", () => ({
+ LegacyCredentialStorage: { create: vi.fn(() => ({})) },
+ createAgentSession: createAgentSessionMock,
+ createBashTool: vi.fn((cwd: string) => ({ name: "bash", cwd })),
+ createCodingTools: vi.fn(() => []),
+ createEditTool: vi.fn(() => ({ name: "edit" })),
+ createExtensionRuntime: vi.fn(),
+ createFindTool: vi.fn(() => ({ name: "find" })),
+ createGrepTool: vi.fn(() => ({ name: "grep" })),
+ createLsTool: vi.fn(() => ({ name: "ls" })),
+ createReadOnlyTools: vi.fn(() => []),
+ createReadTool: vi.fn(() => ({ name: "read" })),
+ createWriteTool: vi.fn(() => ({ name: "write" })),
+ DefaultResourceLoader: class { async reload() {} },
+ DefaultPackageManager: class { async resolve() { return { extensions: [] }; } },
+ discoverAndLoadExtensions: vi.fn(async () => ({ runtime: { pendingProviderRegistrations: [] }, errors: [] })),
+ getAgentDir: () => "/mock-agent-dir",
+ ModelRegistry: class {},
+ ModelRuntime: { create: vi.fn(async () => modelRegistry.modelRuntime) },
+ SessionManager: { inMemory: () => ({ getSessionId: () => undefined }) },
+ SettingsManager: { inMemory: () => ({}) },
+}));
+
+vi.mock("../auth/auth-storage.js", () => ({
+ createFusionAuthStorage: vi.fn(() => ({})),
+ createFusionModelRegistry: vi.fn(async () => modelRegistry),
+}));
+vi.mock("../auth/model-registry-refresh.js", () => ({
+ refreshFusionModelRegistry: vi.fn(async () => "completed"),
+}));
+vi.mock("../auth/custom-providers.js", () => ({ readCustomProviders: vi.fn(() => []) }));
+
+function makeSession(agent: { onPayload?: (payload: unknown, model: { api?: unknown }) => unknown | Promise } = {}) {
+ return {
+ agent,
+ prompt: vi.fn(async () => undefined),
+ subscribe: vi.fn(),
+ dispose: vi.fn(),
+ setThinkingLevel: vi.fn(),
+ };
+}
+
+async function createSession(session = makeSession(), options: Record = {}) {
+ createAgentSessionMock.mockResolvedValueOnce({ session });
+ const { createPiAgentSessionRaw } = await import("../pi.js");
+ const result = await createPiAgentSessionRaw({
+ cwd: "/tmp",
+ systemPrompt: "test",
+ tools: "readonly",
+ defaultProvider: "openai",
+ defaultModelId: "gpt-5",
+ ...options,
+ });
+ return { result, session };
+}
+
+/*
+FNXC:ThinkingTrace 2026-08-27-10:45:
+Fusion can prove only the request payload it sends; a provider's generated reasoning bodies are outside this process. These tests therefore exercise the live createFnAgent session hook rather than asserting provider response content.
+*/
+describe("createFnAgent reasoning-summary payload hook", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ modelRegistry.find.mockImplementation((provider: string, id: string) => ({ provider, id }));
+ modelRegistry.getAll.mockReturnValue([]);
+ });
+
+ it("installs onPayload on every created pi session", async () => {
+ const session = makeSession();
+ await createSession(session);
+
+ expect(session.agent.onPayload).toEqual(expect.any(Function));
+ });
+
+ it("upgrades a Responses request while preserving its effort", async () => {
+ const { session } = await createSession();
+ const result = await session.agent.onPayload?.(
+ { reasoning: { effort: "medium", summary: "auto" } },
+ { api: "openai-responses" },
+ );
+
+ expect(result).toEqual({ reasoning: { effort: "medium", summary: "detailed" } });
+ });
+
+ it("leaves Anthropic and disabled-thinking requests unchanged", async () => {
+ const { session } = await createSession();
+ const anthropicPayload = { reasoning: { effort: "medium", summary: "auto" } };
+ const disabledPayload = { reasoning: { effort: "none" } };
+
+ expect(await session.agent.onPayload?.(anthropicPayload, { api: "anthropic-messages" })).toBeUndefined();
+ expect(await session.agent.onPayload?.(disabledPayload, { api: "openai-responses" })).toBeUndefined();
+ });
+
+ it("chains an upstream replacement and preserves it when Fusion makes no change", async () => {
+ const replacement = { reasoning: { effort: "high", summary: "auto" }, source: "upstream" };
+ const upstream = vi.fn(() => replacement);
+ const { session } = await createSession(makeSession({ onPayload: upstream }));
+
+ expect(await session.agent.onPayload?.({ ignored: true }, { api: "openai-responses" })).toEqual({
+ reasoning: { effort: "high", summary: "detailed" },
+ source: "upstream",
+ });
+ expect(await session.agent.onPayload?.({ ignored: true }, { api: "anthropic-messages" })).toBe(replacement);
+ expect(upstream).toHaveBeenCalledTimes(2);
+ });
+
+ it("does not upgrade requests when summary detail is off", async () => {
+ const { session } = await createSession(makeSession(), { reasoningSummaryDetail: "off" });
+
+ expect(await session.agent.onPayload?.(
+ { reasoning: { effort: "medium", summary: "auto" } },
+ { api: "openai-responses" },
+ )).toBeUndefined();
+ });
+
+ it("retries once on the same session after an unsupported-summary rejection", async () => {
+ const requests: unknown[] = [];
+ const agent: { onPayload?: (payload: unknown, model: { api?: unknown }) => Promise } = {};
+ const session = makeSession(agent);
+ const prompt = session.prompt;
+ let attempts = 0;
+ prompt.mockImplementation(async () => {
+ requests.push(await agent.onPayload?.(
+ { reasoning: { effort: "medium", summary: "auto" } },
+ { api: "openai-responses" },
+ ));
+ attempts += 1;
+ if (attempts === 1) throw new Error("Unsupported reasoning summary: detailed");
+ });
+
+ const { result } = await createSession(session);
+ await (result.session as any).promptWithFallback("test summary fallback");
+
+ expect(prompt).toHaveBeenCalledTimes(2);
+ expect(requests).toEqual([
+ { reasoning: { effort: "medium", summary: "detailed" } },
+ undefined,
+ ]);
+ });
+});
diff --git a/packages/engine/src/execution/__tests__/reasoning-summary-payload.test.ts b/packages/engine/src/execution/__tests__/reasoning-summary-payload.test.ts
new file mode 100644
index 0000000000..2bab11b530
--- /dev/null
+++ b/packages/engine/src/execution/__tests__/reasoning-summary-payload.test.ts
@@ -0,0 +1,102 @@
+import { describe, expect, it } from "vitest";
+import {
+ applyReasoningSummaryToPayload,
+ isReasoningSummaryUnsupportedError,
+} from "../reasoning-summary-payload.js";
+
+const responsesApis = ["openai-responses", "openai-codex-responses", "azure-openai-responses"] as const;
+const nonResponsesApis = [
+ "anthropic-messages",
+ "openai-completions",
+ "google-generative-ai",
+ "bedrock-converse-stream",
+ "mistral-conversations",
+ "pi-messages",
+] as const;
+
+describe("applyReasoningSummaryToPayload", () => {
+ it.each(responsesApis)("upgrades an enabled %s request while preserving effort", (api) => {
+ const payload = { reasoning: { effort: "medium", summary: "auto" }, input: "keep" };
+
+ expect(applyReasoningSummaryToPayload(payload, { api }, "detailed")).toEqual({
+ reasoning: { effort: "medium", summary: "detailed" },
+ input: "keep",
+ });
+ });
+
+ it.each(nonResponsesApis)("leaves %s payloads byte-identical", (api) => {
+ const payload = { reasoning: { effort: "high", summary: "auto" }, input: "keep" };
+
+ expect(applyReasoningSummaryToPayload(payload, { api }, "detailed")).toBeUndefined();
+ });
+
+ it("adds detailed only to an enabled request whose summary is absent", () => {
+ const payload = { reasoning: { effort: "high" } };
+
+ expect(applyReasoningSummaryToPayload(payload, { api: "openai-responses" }, "detailed")).toEqual({
+ reasoning: { effort: "high", summary: "detailed" },
+ });
+ });
+
+ it.each([
+ ["missing reasoning", { input: "keep" }],
+ ["thinking disabled", { reasoning: { effort: "none" } }],
+ ["existing detailed summary", { reasoning: { effort: "high", summary: "detailed" } }],
+ ["explicit concise summary", { reasoning: { effort: "high", summary: "concise" } }],
+ ["null payload", null],
+ ["string payload", "payload"],
+ ["array payload", []],
+ ["string reasoning", { reasoning: "high" }],
+ ["null reasoning", { reasoning: null }],
+ ])("does not alter %s", (_name, payload) => {
+ expect(applyReasoningSummaryToPayload(payload, { api: "openai-responses" }, "detailed")).toBeUndefined();
+ });
+
+ it("does not mutate either the request or its reasoning object", () => {
+ const reasoning = { effort: "medium", summary: "auto" };
+ const payload = { reasoning, input: "keep" };
+
+ const result = applyReasoningSummaryToPayload(payload, { api: "openai-responses" }, "detailed");
+
+ expect(payload).toEqual({ reasoning: { effort: "medium", summary: "auto" }, input: "keep" });
+ expect(result).not.toBe(payload);
+ expect(result?.reasoning).not.toBe(reasoning);
+ });
+
+ it("treats auto and off detail as no-op requests", () => {
+ const payload = { reasoning: { effort: "medium", summary: "auto" } };
+
+ expect(applyReasoningSummaryToPayload(payload, { api: "openai-responses" }, "auto")).toBeUndefined();
+ expect(applyReasoningSummaryToPayload(payload, { api: "openai-responses" }, "off")).toBeUndefined();
+ });
+
+ it("can deliberately request concise without overriding an explicit choice", () => {
+ const payload = { reasoning: { effort: "medium", summary: "auto" } };
+
+ expect(applyReasoningSummaryToPayload(payload, { api: "openai-responses" }, "concise")).toEqual({
+ reasoning: { effort: "medium", summary: "concise" },
+ });
+ });
+});
+
+describe("isReasoningSummaryUnsupportedError", () => {
+ it.each([
+ "Unsupported reasoning summary: detailed",
+ "reasoning_summary is invalid for this model",
+ "Unknown reasoning summary option",
+ "Summary reasoning is not supported by this endpoint",
+ ])("recognizes an explicit summary capability rejection: %s", (message) => {
+ expect(isReasoningSummaryUnsupportedError(message)).toBe(true);
+ });
+
+ it.each([
+ "400 Bad Request",
+ "maximum context length exceeded",
+ "invalid API key",
+ "cannot specify both thinking and reasoning_effort",
+ "reasoning effort is unsupported",
+ "summary field is invalid",
+ ])("does not misclassify unrelated provider errors: %s", (message) => {
+ expect(isReasoningSummaryUnsupportedError(message)).toBe(false);
+ });
+});
diff --git a/packages/engine/src/execution/reasoning-summary-payload.ts b/packages/engine/src/execution/reasoning-summary-payload.ts
new file mode 100644
index 0000000000..252a235368
--- /dev/null
+++ b/packages/engine/src/execution/reasoning-summary-payload.ts
@@ -0,0 +1,72 @@
+/*
+FNXC:ThinkingTrace 2026-08-27-10:45:
+Pi-coding-agent streams Responses models through `streamSimple`, which forwards reasoning effort but cannot carry `reasoningSummary`. Pi therefore defaults Responses payloads to the short `"auto"` summary that can contain titles without bodies.
+
+Pi 0.84.1 exposes `Agent.onPayload` as the request-shaping seam. This helper upgrades only already-enabled Responses reasoning; CLI and ACP runtimes are structurally exempt because they never construct a pi Agent.
+*/
+
+export const RESPONSES_FAMILY_APIS = new Set([
+ "openai-responses",
+ "openai-codex-responses",
+ "azure-openai-responses",
+] as const);
+
+export type ResponsesFamilyApi = typeof RESPONSES_FAMILY_APIS extends Set ? Api : never;
+export type ReasoningSummaryDetail = "auto" | "concise" | "detailed" | "off";
+
+type PayloadModel = { api?: unknown };
+type ReasoningPayload = { effort?: unknown; summary?: unknown };
+type ProviderPayload = Record & { reasoning?: unknown };
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+function isResponsesFamilyApi(api: unknown): api is ResponsesFamilyApi {
+ return typeof api === "string" && RESPONSES_FAMILY_APIS.has(api as ResponsesFamilyApi);
+}
+
+/**
+ * Returns a replacement only when an already-enabled Responses request needs a
+ * more detailed reasoning summary. This preserves pi's undefined-means-keep
+ * contract and never enables reasoning for a request that omitted it.
+ */
+export function applyReasoningSummaryToPayload(
+ payload: unknown,
+ model: PayloadModel,
+ detail: ReasoningSummaryDetail,
+): ProviderPayload | undefined {
+ if (detail === "off" || detail === "auto" || !isResponsesFamilyApi(model.api) || !isRecord(payload)) {
+ return undefined;
+ }
+
+ const reasoning = payload.reasoning;
+ if (!isRecord(reasoning)) {
+ return undefined;
+ }
+
+ const effort = reasoning.effort;
+ if (typeof effort !== "string" || effort.length === 0 || effort === "none") {
+ return undefined;
+ }
+
+ const summary = reasoning.summary;
+ if (summary !== undefined && summary !== "auto") {
+ return undefined;
+ }
+
+ return {
+ ...payload,
+ reasoning: {
+ ...reasoning,
+ summary: detail,
+ } satisfies ReasoningPayload,
+ };
+}
+
+/** Match only explicit provider rejections of the optional summary request. */
+export function isReasoningSummaryUnsupportedError(message: string): boolean {
+ const mentionsReasoningSummary = /\breasoning[\s_-]*summary\b|\bsummary\b[\s\S]{0,80}\breasoning\b/i.test(message);
+ const rejectsFeature = /\b(?:unsupported|not supported|unknown|unrecognized|invalid|not allowed|not available)\b/i.test(message);
+ return mentionsReasoningSummary && rejectsFeature;
+}
diff --git a/packages/engine/src/pi.ts b/packages/engine/src/pi.ts
index 2c23923076..0533d6a9cc 100644
--- a/packages/engine/src/pi.ts
+++ b/packages/engine/src/pi.ts
@@ -88,6 +88,11 @@ import { resolvePermanentAgentToolDecision } from "./agents/permanent-agent-gati
import type { SystemPromptLayers } from "./execution/prompt-layers.js";
import { READONLY_ALLOWLIST, filterCustomToolsForReadonly, isReadonlyAllowed } from "./workflows/workflow-step-tool-policy.js";
import { createStreamingDeltaNormalizer } from "./execution/streaming-delta.js";
+import {
+ applyReasoningSummaryToPayload,
+ isReasoningSummaryUnsupportedError,
+ type ReasoningSummaryDetail,
+} from "./execution/reasoning-summary-payload.js";
import { isModelAuthTierIncompatibilityError, isProviderModelNotFoundError, isUnsupportedMessageRoleError } from "./errors/transient-error-detector.js";
import { logMcpForwardingSkipped, runtimeSupportsMcp } from "./mcp/mcp-runtime-support.js";
import { connectMcpSessionTools, type McpClientFactory, type McpSessionToolset } from "./mcp/mcp-session-tools.js";
@@ -231,12 +236,14 @@ interface ToolHookResult {
type AgentToolHookSession = AgentSession & {
agent?: {
afterToolCall?: (payload: ToolHookPayload) => Promise;
+ onPayload?: (payload: unknown, model: { api?: unknown }) => unknown | undefined | Promise;
state?: {
messages?: Array>;
};
};
__fusionToolResultGuardInstalled?: boolean;
__fusionMessageContentGuardInstalled?: boolean;
+ __fusionReasoningSummaryPayloadHookInstalled?: boolean;
};
const FN_MEMORY_APPEND_TOOL_NAME = "fn_memory_append";
const FUSION_SHUTDOWN_WRAP_FLAG = "__fusionSessionShutdownDisposeWrapped";
@@ -1099,6 +1106,8 @@ export interface AgentOptions {
fallbackThinkingLevel?: string;
/** Default thinking effort level (e.g. "medium", "high"). When provided, sets the session's thinking level after creation. */
defaultThinkingLevel?: string;
+ /** Detail requested for already-enabled Responses reasoning summaries; defaults to detailed. */
+ reasoningSummaryDetail?: ReasoningSummaryDetail;
/** Optional pre-configured SessionManager. When provided, the agent session
* uses this instead of creating an in-memory session. Pass a file-based
* SessionManager to enable session persistence and pause/resume. */
@@ -3135,10 +3144,37 @@ export async function createPiAgentSessionRaw(options: AgentOptions): Promise {
+ if (session.__fusionReasoningSummaryPayloadHookInstalled || !session.agent) {
+ return;
+ }
+
+ const agent = session.agent;
+ const previousOnPayload = agent.onPayload;
+ agent.onPayload = async (payload, model) => {
+ const previousResult = previousOnPayload ? await previousOnPayload(payload, model) : undefined;
+ const effectivePayload = previousResult ?? payload;
+ const replacement = applyReasoningSummaryToPayload(
+ effectivePayload,
+ model,
+ reasoningSummaryCompatibilityDisabled ? "off" : reasoningSummaryDetail,
+ );
+ return replacement ?? previousResult;
+ };
+ session.__fusionReasoningSummaryPayloadHookInstalled = true;
+ };
+
let activeSession = sessionResult.session;
wrapSessionDisposeWithShutdown(activeSession);
installToolResultContentGuard(activeSession as AgentToolHookSession);
installMessageContentGuard(activeSession as AgentToolHookSession, sessionManager as unknown as SessionManagerLike);
+ installReasoningSummaryPayloadHook(activeSession as AgentToolHookSession);
(activeSession as any).__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === FN_MEMORY_APPEND_TOOL_NAME) === true;
const promptableSession = activeSession as PromptableSession;
@@ -3172,6 +3208,7 @@ export async function createPiAgentSessionRaw(options: AgentOptions): Promise tool.name === FN_MEMORY_APPEND_TOOL_NAME) === true;
const deltaNormalizer = createStreamingDeltaNormalizer();
targetSession.subscribe((event) => {
@@ -3358,6 +3395,17 @@ export async function createPiAgentSessionRaw(options: AgentOptions): Promise