Merge pull request #84 from multica-ai/reasoning-mode-support
feat(agent): add reasoning mode for thinking content display
This commit is contained in:
commit
c70dd338c2
11 changed files with 119 additions and 7 deletions
|
|
@ -37,4 +37,5 @@ export {
|
|||
type StreamMessageEvent,
|
||||
type StreamToolEvent,
|
||||
extractTextFromEvent,
|
||||
extractThinkingFromEvent,
|
||||
} from "./stream";
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue