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 <noreply@anthropic.com>
This commit is contained in:
yushen 2026-02-04 15:50:47 +08:00
parent 0340358a9b
commit 953a29672a
5 changed files with 71 additions and 5 deletions

View file

@ -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<void> {
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,

View file

@ -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,
});

View file

@ -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,

View file

@ -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;
}

View file

@ -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 };
}
/**