From 77d5702f80e91aa311e517436bf32a86fcff18ee Mon Sep 17 00:00:00 2001 From: yushen Date: Wed, 4 Feb 2026 15:50:35 +0800 Subject: [PATCH 1/5] feat(agent): add reasoning mode types and thinking extraction Add ReasoningMode type (off/on/stream) to AgentOptions and related config types. Add extractThinking() for extracting thinking content blocks from LLM responses, mirroring the existing extractText() pattern. Co-Authored-By: Claude Opus 4.5 --- src/agent/extract-text.ts | 11 +++++++++++ src/agent/profile/types.ts | 2 ++ src/agent/session/types.ts | 2 ++ src/agent/types.ts | 7 +++++++ 4 files changed, 22 insertions(+) diff --git a/src/agent/extract-text.ts b/src/agent/extract-text.ts index 145a2c97..8075e61f 100644 --- a/src/agent/extract-text.ts +++ b/src/agent/extract-text.ts @@ -10,3 +10,14 @@ export function extractText(message: AgentMessage | undefined): string { .map((c) => c.text ?? "") .join(""); } + +/** Extract thinking/reasoning content from an AgentMessage */ +export function extractThinking(message: AgentMessage | undefined): string { + if (!message || typeof message !== "object" || !("content" in message)) return ""; + const content = (message as { content?: Array<{ type: string; thinking?: string }> }).content; + if (!Array.isArray(content)) return ""; + return content + .filter((c) => c.type === "thinking") + .map((c) => c.thinking ?? "") + .join(""); +} diff --git a/src/agent/profile/types.ts b/src/agent/profile/types.ts index f1de243d..f03f7d11 100644 --- a/src/agent/profile/types.ts +++ b/src/agent/profile/types.ts @@ -37,6 +37,8 @@ export interface ProfileConfig { model?: string; /** Default thinking level */ thinkingLevel?: string; + /** Reasoning mode: off, on, stream */ + reasoningMode?: "off" | "on" | "stream" | undefined; } /** Agent Profile configuration */ diff --git a/src/agent/session/types.ts b/src/agent/session/types.ts index c9086b74..72c810ab 100644 --- a/src/agent/session/types.ts +++ b/src/agent/session/types.ts @@ -4,6 +4,8 @@ export type SessionMeta = { provider?: string; model?: string; thinkingLevel?: string; + /** Reasoning mode: off, on, stream */ + reasoningMode?: string; /** Context window token 数 */ contextWindowTokens?: number; }; diff --git a/src/agent/types.ts b/src/agent/types.ts index c7e37658..6f7b7806 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -2,8 +2,13 @@ import type { ThinkingLevel } from "@mariozechner/pi-agent-core"; import type { SkillsConfig } from "./skills/types.js"; import type { ToolsConfig } from "./tools/policy.js"; +/** Controls how reasoning/thinking content blocks are handled */ +export type ReasoningMode = "off" | "on" | "stream"; + export type AgentRunResult = { text: string; + /** Extracted thinking/reasoning content (when reasoningMode !== "off") */ + thinking?: string | undefined; error?: string | undefined; }; @@ -28,6 +33,8 @@ export type AgentOptions = { /** System prompt, if profileId is set will auto-construct from profile */ systemPrompt?: string | undefined; thinkingLevel?: ThinkingLevel | undefined; + /** Controls how reasoning/thinking content is displayed: off, on, stream (default: stream) */ + reasoningMode?: ReasoningMode | undefined; /** Command execution directory */ cwd?: string | undefined; sessionId?: string | undefined; From 0340358a9be79958464bd905636e298e1bc18938 Mon Sep 17 00:00:00 2001 From: yushen Date: Wed, 4 Feb 2026 15:50:41 +0800 Subject: [PATCH 2/5] feat(sdk): add thinking content extraction for stream events Update StreamMessageEvent content type to include thinking blocks. Add extractThinkingFromEvent() helper and export it, enabling clients to access reasoning content from streamed agent responses. Co-Authored-By: Claude Opus 4.5 --- packages/sdk/src/actions/index.ts | 1 + packages/sdk/src/actions/stream.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/actions/index.ts b/packages/sdk/src/actions/index.ts index e73947a4..04525464 100644 --- a/packages/sdk/src/actions/index.ts +++ b/packages/sdk/src/actions/index.ts @@ -37,4 +37,5 @@ export { type StreamMessageEvent, type StreamToolEvent, extractTextFromEvent, + extractThinkingFromEvent, } from "./stream"; diff --git a/packages/sdk/src/actions/stream.ts b/packages/sdk/src/actions/stream.ts index 51329040..032bb962 100644 --- a/packages/sdk/src/actions/stream.ts +++ b/packages/sdk/src/actions/stream.ts @@ -12,7 +12,7 @@ export interface StreamMessageEvent { message: { id?: string; role: string; - content?: Array<{ type: string; text?: string }>; + content?: Array<{ type: string; text?: string; thinking?: string }>; }; assistantMessageEvent?: unknown; } @@ -47,3 +47,13 @@ export function extractTextFromEvent(event: StreamMessageEvent): string { .map((c) => c.text ?? "") .join(""); } + +/** Extract thinking/reasoning content from an AgentMessage content array */ +export function extractThinkingFromEvent(event: StreamMessageEvent): string { + const content = event.message?.content; + if (!Array.isArray(content)) return ""; + return content + .filter((c) => c.type === "thinking") + .map((c) => c.thinking ?? "") + .join(""); +} From 953a29672a3b709ad92c8b2cdf8a052c09ffdc2c Mon Sep 17 00:00:00 2001 From: yushen Date: Wed, 4 Feb 2026 15:50:47 +0800 Subject: [PATCH 3/5] feat(agent): implement reasoning mode in engine and CLI Wire reasoningMode through Agent runner, CLI output, and all CLI entry points (run, interactive, non-interactive). In stream mode, thinking content is printed to stderr in real-time. In on mode, thinking is shown after message completion. Default is stream. Co-Authored-By: Claude Opus 4.5 --- src/agent/cli/commands/run.ts | 7 ++++++ src/agent/cli/interactive.ts | 7 ++++++ src/agent/cli/non-interactive.ts | 7 ++++++ src/agent/cli/output.ts | 43 ++++++++++++++++++++++++++++++-- src/agent/runner.ts | 12 ++++++--- 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/agent/cli/commands/run.ts b/src/agent/cli/commands/run.ts index 23311e0a..e404013b 100644 --- a/src/agent/cli/commands/run.ts +++ b/src/agent/cli/commands/run.ts @@ -18,6 +18,7 @@ type RunOptions = { baseUrl?: string; system?: string; thinking?: string; + reasoning?: string; cwd?: string; session?: string; debug?: boolean; @@ -40,6 +41,7 @@ ${cyan("Options:")} ${yellow("--base-url")} URL Custom base URL for provider ${yellow("--system")} TEXT System prompt (ignored if --profile set) ${yellow("--thinking")} LEVEL Thinking level + ${yellow("--reasoning")} MODE Reasoning display mode (off, on, stream) ${yellow("--cwd")} DIR Working directory ${yellow("--session")} ID Session ID for persistence ${yellow("--debug")} Enable debug logging @@ -106,6 +108,10 @@ function parseArgs(argv: string[]): { opts: RunOptions; prompt: string } { opts.thinking = args.shift(); continue; } + if (arg === "--reasoning") { + opts.reasoning = args.shift(); + continue; + } if (arg === "--cwd") { opts.cwd = args.shift(); continue; @@ -192,6 +198,7 @@ export async function runCommand(args: string[]): Promise { baseUrl: opts.baseUrl, systemPrompt: opts.system, thinkingLevel: opts.thinking as any, + reasoningMode: (opts.reasoning as any) ?? undefined, cwd: opts.cwd, sessionId: opts.session, debug: opts.debug, diff --git a/src/agent/cli/interactive.ts b/src/agent/cli/interactive.ts index 68ccd68c..bd6131ab 100644 --- a/src/agent/cli/interactive.ts +++ b/src/agent/cli/interactive.ts @@ -12,6 +12,7 @@ type CliOptions = { model?: string | undefined; system?: string | undefined; thinking?: string | undefined; + reasoning?: string | undefined; cwd?: string | undefined; session?: string | undefined; help?: boolean | undefined; @@ -35,6 +36,7 @@ function printUsage() { console.log(` ${yellow("--model")} NAME Model name`); console.log(` ${yellow("--system")} TEXT System prompt (ignored if --profile is set)`); console.log(` ${yellow("--thinking")} LEVEL Thinking level`); + console.log(` ${yellow("--reasoning")} MODE Reasoning display mode (off, on, stream)`); console.log(` ${yellow("--cwd")} DIR Working directory for commands`); console.log(` ${yellow("--session")} ID Session ID to resume`); console.log(` ${yellow("--help")}, -h Show this help`); @@ -76,6 +78,10 @@ function parseArgs(argv: string[]) { opts.thinking = args.shift(); continue; } + if (arg === "--reasoning") { + opts.reasoning = args.shift(); + continue; + } if (arg === "--cwd") { opts.cwd = args.shift(); continue; @@ -343,6 +349,7 @@ class InteractiveCLI { model: this.opts.model, systemPrompt: this.opts.system, thinkingLevel: this.opts.thinking as AgentOptions["thinkingLevel"], + reasoningMode: (this.opts.reasoning as AgentOptions["reasoningMode"]) ?? undefined, cwd: this.opts.cwd, sessionId, }); diff --git a/src/agent/cli/non-interactive.ts b/src/agent/cli/non-interactive.ts index f0109209..004facfc 100644 --- a/src/agent/cli/non-interactive.ts +++ b/src/agent/cli/non-interactive.ts @@ -9,6 +9,7 @@ type CliOptions = { baseUrl?: string | undefined; system?: string | undefined; thinking?: string | undefined; + reasoning?: string | undefined; cwd?: string | undefined; session?: string | undefined; debug?: boolean | undefined; @@ -31,6 +32,7 @@ function printUsage() { console.log(" --base-url URL Custom base URL for the provider"); console.log(" --system TEXT System prompt (ignored if --profile is set)"); console.log(" --thinking LEVEL Thinking level"); + console.log(" --reasoning MODE Reasoning display mode (off, on, stream)"); console.log(" --cwd DIR Working directory for commands"); console.log(" --session ID Session ID for conversation persistence"); console.log(" --debug Enable debug logging"); @@ -87,6 +89,10 @@ function parseArgs(argv: string[]) { opts.thinking = args.shift(); continue; } + if (arg === "--reasoning") { + opts.reasoning = args.shift(); + continue; + } if (arg === "--cwd") { opts.cwd = args.shift(); continue; @@ -171,6 +177,7 @@ async function main() { baseUrl: opts.baseUrl, systemPrompt: opts.system, thinkingLevel: opts.thinking as any, + reasoningMode: (opts.reasoning as any) ?? undefined, cwd: opts.cwd, sessionId: opts.session, debug: opts.debug, diff --git a/src/agent/cli/output.ts b/src/agent/cli/output.ts index b2f77cfa..b47468ac 100644 --- a/src/agent/cli/output.ts +++ b/src/agent/cli/output.ts @@ -1,10 +1,13 @@ import type { AgentEvent, AgentMessage } from "@mariozechner/pi-agent-core"; -import { colors, createSpinner } from "./colors.js"; -import { extractText } from "../extract-text.js"; +import { colors, createSpinner, dim } from "./colors.js"; +import { extractText, extractThinking } from "../extract-text.js"; +import type { ReasoningMode } from "../types.js"; export type AgentOutputState = { lastAssistantText: string; + lastAssistantThinking: string; printedLen: number; + printedThinkingLen: number; streaming: boolean; }; @@ -171,10 +174,14 @@ export function formatResultSummary(name: string, result: unknown): string { export function createAgentOutput(params: { stdout: NodeJS.WritableStream; stderr: NodeJS.WritableStream; + reasoningMode?: ReasoningMode; }): AgentOutput { + const reasoningMode = params.reasoningMode ?? "stream"; const state: AgentOutputState = { lastAssistantText: "", + lastAssistantThinking: "", printedLen: 0, + printedThinkingLen: 0, streaming: false, }; @@ -194,11 +201,20 @@ export function createAgentOutput(params: { } state.streaming = true; state.printedLen = 0; + state.printedThinkingLen = 0; const text = extractText(msg); if (text.length > 0) { params.stdout.write(text); state.printedLen = text.length; } + // Stream thinking content in real-time + if (reasoningMode === "stream") { + const thinking = extractThinking(msg); + if (thinking.length > 0) { + params.stderr.write(dim(thinking)); + state.printedThinkingLen = thinking.length; + } + } } break; } @@ -210,6 +226,14 @@ export function createAgentOutput(params: { params.stdout.write(text.slice(state.printedLen)); state.printedLen = text.length; } + // Stream thinking content in real-time + if (reasoningMode === "stream") { + const thinking = extractThinking(msg); + if (thinking.length > state.printedThinkingLen) { + params.stderr.write(dim(thinking.slice(state.printedThinkingLen))); + state.printedThinkingLen = thinking.length; + } + } } break; } @@ -224,6 +248,21 @@ export function createAgentOutput(params: { if (state.streaming) params.stdout.write("\n"); state.streaming = false; state.lastAssistantText = text; + + // Extract and store thinking content + const thinking = extractThinking(msg); + state.lastAssistantThinking = thinking; + + // Show thinking at end for "on" mode + if (reasoningMode === "on" && thinking) { + params.stderr.write(`\n${dim("--- Thinking ---")}\n`); + params.stderr.write(dim(thinking)); + params.stderr.write(`\n${dim("--- End Thinking ---")}\n`); + } + // Finish streaming thinking with a newline + if (reasoningMode === "stream" && state.printedThinkingLen > 0) { + params.stderr.write("\n"); + } } break; } diff --git a/src/agent/runner.ts b/src/agent/runner.ts index 7acb3709..c395e395 100644 --- a/src/agent/runner.ts +++ b/src/agent/runner.ts @@ -1,6 +1,6 @@ import { Agent as PiAgentCore, type AgentEvent, type AgentMessage } from "@mariozechner/pi-agent-core"; import { v7 as uuidv7 } from "uuid"; -import type { AgentOptions, AgentRunResult } from "./types.js"; +import type { AgentOptions, AgentRunResult, ReasoningMode } from "./types.js"; import { createAgentOutput } from "./cli/output.js"; import { resolveModel, resolveTools } from "./tools.js"; import { @@ -70,6 +70,7 @@ export class Agent { private readonly skillManager?: SkillManager; private readonly contextWindowGuard: ContextWindowGuardResult; private readonly debug: boolean; + private readonly reasoningMode: ReasoningMode; private toolsOptions: AgentOptions; private readonly originalToolsConfig?: ToolsConfig; private readonly stderr: NodeJS.WritableStream; @@ -89,8 +90,9 @@ export class Agent { constructor(options: AgentOptions = {}) { const stdout = options.logger?.stdout ?? process.stdout; this.stderr = options.logger?.stderr ?? process.stderr; - this.output = createAgentOutput({ stdout, stderr: this.stderr }); this.debug = options.debug ?? false; + this.reasoningMode = options.reasoningMode ?? "stream"; + this.output = createAgentOutput({ stdout, stderr: this.stderr, reasoningMode: this.reasoningMode }); // Resolve provider and model from options > env vars > defaults const defaultProvider = options.provider ?? credentialManager.getLlmProvider() ?? "kimi-coding"; @@ -291,6 +293,7 @@ export class Agent { provider: this.agent.state.model?.provider, model: this.agent.state.model?.id, thinkingLevel: this.agent.state.thinkingLevel, + reasoningMode: this.reasoningMode, contextWindowTokens: this.contextWindowGuard.tokens, }); @@ -387,7 +390,10 @@ export class Agent { markAuthProfileGood(this.resolvedProvider, this.currentProfileId); } - return { text: this.output.state.lastAssistantText, error: this.agent.state.error }; + const thinking = this.reasoningMode !== "off" + ? this.output.state.lastAssistantThinking || undefined + : undefined; + return { text: this.output.state.lastAssistantText, thinking, error: this.agent.state.error }; } /** From 8fe2b5f010f582773c2ae0fa67c901f642aa5e57 Mon Sep 17 00:00:00 2001 From: yushen Date: Wed, 4 Feb 2026 16:00:53 +0800 Subject: [PATCH 4/5] fix(agent): resolve reasoningMode from profile config and session meta - Read reasoningMode from profile config and storedMeta when not explicitly set via options (matching thinkingLevel pattern) - Skip extractThinking() call when reasoningMode is "off" - Clean up redundant ?? undefined casts in CLI entry points Co-Authored-By: Claude Opus 4.5 --- src/agent/cli/commands/run.ts | 3 ++- src/agent/cli/interactive.ts | 2 +- src/agent/cli/non-interactive.ts | 2 +- src/agent/cli/output.ts | 4 ++-- src/agent/runner.ts | 14 +++++++++++++- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/agent/cli/commands/run.ts b/src/agent/cli/commands/run.ts index e404013b..df7127ab 100644 --- a/src/agent/cli/commands/run.ts +++ b/src/agent/cli/commands/run.ts @@ -7,6 +7,7 @@ */ import { Agent } from "../../runner.js"; +import type { AgentOptions } from "../../types.js"; import type { ToolsConfig } from "../../tools/policy.js"; import { cyan, yellow, dim } from "../colors.js"; @@ -198,7 +199,7 @@ export async function runCommand(args: string[]): Promise { baseUrl: opts.baseUrl, systemPrompt: opts.system, thinkingLevel: opts.thinking as any, - reasoningMode: (opts.reasoning as any) ?? undefined, + reasoningMode: opts.reasoning as AgentOptions["reasoningMode"], cwd: opts.cwd, sessionId: opts.session, debug: opts.debug, diff --git a/src/agent/cli/interactive.ts b/src/agent/cli/interactive.ts index bd6131ab..3a4e3659 100644 --- a/src/agent/cli/interactive.ts +++ b/src/agent/cli/interactive.ts @@ -349,7 +349,7 @@ class InteractiveCLI { model: this.opts.model, systemPrompt: this.opts.system, thinkingLevel: this.opts.thinking as AgentOptions["thinkingLevel"], - reasoningMode: (this.opts.reasoning as AgentOptions["reasoningMode"]) ?? undefined, + reasoningMode: this.opts.reasoning as AgentOptions["reasoningMode"], cwd: this.opts.cwd, sessionId, }); diff --git a/src/agent/cli/non-interactive.ts b/src/agent/cli/non-interactive.ts index 004facfc..2cff5c4f 100644 --- a/src/agent/cli/non-interactive.ts +++ b/src/agent/cli/non-interactive.ts @@ -177,7 +177,7 @@ async function main() { baseUrl: opts.baseUrl, systemPrompt: opts.system, thinkingLevel: opts.thinking as any, - reasoningMode: (opts.reasoning as any) ?? undefined, + reasoningMode: opts.reasoning as any, cwd: opts.cwd, sessionId: opts.session, debug: opts.debug, diff --git a/src/agent/cli/output.ts b/src/agent/cli/output.ts index b47468ac..fe4fa058 100644 --- a/src/agent/cli/output.ts +++ b/src/agent/cli/output.ts @@ -249,8 +249,8 @@ export function createAgentOutput(params: { state.streaming = false; state.lastAssistantText = text; - // Extract and store thinking content - const thinking = extractThinking(msg); + // Extract and store thinking content (skip when off) + const thinking = reasoningMode !== "off" ? extractThinking(msg) : ""; state.lastAssistantThinking = thinking; // Show thinking at end for "on" mode diff --git a/src/agent/runner.ts b/src/agent/runner.ts index c395e395..4440f237 100644 --- a/src/agent/runner.ts +++ b/src/agent/runner.ts @@ -70,7 +70,7 @@ export class Agent { private readonly skillManager?: SkillManager; private readonly contextWindowGuard: ContextWindowGuardResult; private readonly debug: boolean; - private readonly reasoningMode: ReasoningMode; + private reasoningMode: ReasoningMode; private toolsOptions: AgentOptions; private readonly originalToolsConfig?: ToolsConfig; private readonly stderr: NodeJS.WritableStream; @@ -267,6 +267,18 @@ export class Agent { this.agent.setThinkingLevel(options.thinkingLevel); } + // Resolve reasoningMode: options > profile config > storedMeta > default "stream" + if (!options.reasoningMode) { + const profileReasoningMode = this.profile?.getProfile()?.config?.reasoningMode; + const metaReasoningMode = storedMeta?.reasoningMode as ReasoningMode | undefined; + const resolved = profileReasoningMode ?? metaReasoningMode ?? "stream"; + if (resolved !== this.reasoningMode) { + this.reasoningMode = resolved; + // Re-create output with correct reasoningMode + this.output = createAgentOutput({ stdout, stderr: this.stderr, reasoningMode: this.reasoningMode }); + } + } + this.agent.setModel(model); // Save original tools config from options (for later merging during reload) From f64979211889e6fcbb1240dec984750ec094c9bb Mon Sep 17 00:00:00 2001 From: yushen Date: Wed, 4 Feb 2026 16:09:36 +0800 Subject: [PATCH 5/5] fix(agent): allow output reinit --- src/agent/runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/runner.ts b/src/agent/runner.ts index 4440f237..b7e2334e 100644 --- a/src/agent/runner.ts +++ b/src/agent/runner.ts @@ -64,7 +64,7 @@ export function isRotatableError(reason: AuthProfileFailureReason): boolean { export class Agent { private readonly agent: PiAgentCore; - private readonly output; + private output; private readonly session: SessionManager; private readonly profile?: ProfileManager; private readonly skillManager?: SkillManager;