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(""); +} diff --git a/src/agent/cli/commands/run.ts b/src/agent/cli/commands/run.ts index 23311e0a..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"; @@ -18,6 +19,7 @@ type RunOptions = { baseUrl?: string; system?: string; thinking?: string; + reasoning?: string; cwd?: string; session?: string; debug?: boolean; @@ -40,6 +42,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 +109,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 +199,7 @@ export async function runCommand(args: string[]): Promise { baseUrl: opts.baseUrl, systemPrompt: opts.system, thinkingLevel: opts.thinking as any, + 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 68ccd68c..3a4e3659 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"], cwd: this.opts.cwd, sessionId, }); diff --git a/src/agent/cli/non-interactive.ts b/src/agent/cli/non-interactive.ts index f0109209..2cff5c4f 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, 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..fe4fa058 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 (skip when off) + const thinking = reasoningMode !== "off" ? 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/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/runner.ts b/src/agent/runner.ts index 3745e2eb..39923470 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 { @@ -69,12 +69,13 @@ 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; private readonly contextWindowGuard: ContextWindowGuardResult; private readonly debug: boolean; + private reasoningMode: ReasoningMode; private toolsOptions: AgentOptions; private readonly originalToolsConfig?: ToolsConfig; private readonly stderr: NodeJS.WritableStream; @@ -94,8 +95,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"; @@ -254,6 +256,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) @@ -288,6 +302,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, }); @@ -384,7 +399,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 }; } /** 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;