feat: add /reasoning reasoning visibility

This commit is contained in:
Peter Steinberger
2026-01-07 06:16:38 +01:00
parent cb2a72f8a9
commit 1673a221f8
32 changed files with 370 additions and 23 deletions

View File

@@ -603,6 +603,7 @@ Quick reference (send these in chat):
| `/activation mention\|always` | Group activation (owner-only) | | `/activation mention\|always` | Group activation (owner-only) |
| `/think <level>` | Set thinking level (off\|minimal\|low\|medium\|high) | | `/think <level>` | Set thinking level (off\|minimal\|low\|medium\|high) |
| `/verbose on\|off` | Toggle verbose mode | | `/verbose on\|off` | Toggle verbose mode |
| `/reasoning on\|off` | Toggle reasoning visibility |
| `/elevated on\|off` | Toggle elevated bash mode (approved senders only) | | `/elevated on\|off` | Toggle elevated bash mode (approved senders only) |
| `/model <name>` | Switch AI model (see below) | | `/model <name>` | Switch AI model (see below) |
| `/queue <mode>` | Queue mode (see below) | | `/queue <mode>` | Queue mode (see below) |

View File

@@ -40,6 +40,7 @@ Text + native (when enabled):
- `/reset` or `/new` - `/reset` or `/new`
- `/think <level>` (aliases: `/thinking`, `/t`) - `/think <level>` (aliases: `/thinking`, `/t`)
- `/verbose on|off` (alias: `/v`) - `/verbose on|off` (alias: `/v`)
- `/reasoning on|off` (alias: `/reason`)
- `/elevated on|off` (alias: `/elev`) - `/elevated on|off` (alias: `/elev`)
- `/model <name>` - `/model <name>`
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`) - `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`)

View File

@@ -34,6 +34,12 @@ read_when:
- Inline directive affects only that message; session/global defaults apply otherwise. - Inline directive affects only that message; session/global defaults apply otherwise.
- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `<emoji> <tool-name>: <arg>` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas. If you toggle `/verbose on|off` while a run is in-flight, subsequent tool bubbles honor the new setting. - When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `<emoji> <tool-name>: <arg>` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas. If you toggle `/verbose on|off` while a run is in-flight, subsequent tool bubbles honor the new setting.
## Reasoning visibility (/reasoning)
- Levels: `on|off`.
- Directive-only message toggles whether thinking blocks are shown as italic text in replies.
- When enabled, any model-provided reasoning content is appended as a separate italic block.
- Alias: `/reason`.
## Related ## Related
- Elevated mode docs live in [`docs/elevated.md`](/tools/elevated). - Elevated mode docs live in [`docs/elevated.md`](/tools/elevated).

View File

@@ -51,6 +51,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
- `/model <provider/model>` (or `/model list`, `/models`) - `/model <provider/model>` (or `/model list`, `/models`)
- `/think <off|minimal|low|medium|high>` - `/think <off|minimal|low|medium|high>`
- `/verbose <on|off>` - `/verbose <on|off>`
- `/reasoning <on|off>`
- `/elevated <on|off>` - `/elevated <on|off>`
- `/elev <on|off>` - `/elev <on|off>`
- `/activation <mention|always>` - `/activation <mention|always>`

View File

@@ -13,7 +13,11 @@ import {
type Skill, type Skill,
} from "@mariozechner/pi-coding-agent"; } from "@mariozechner/pi-coding-agent";
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js"; import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
import type { ThinkLevel, VerboseLevel } from "../auto-reply/thinking.js"; import type {
ReasoningLevel,
ThinkLevel,
VerboseLevel,
} from "../auto-reply/thinking.js";
import { formatToolAggregate } from "../auto-reply/tool-meta.js"; import { formatToolAggregate } from "../auto-reply/tool-meta.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { getMachineDisplayName } from "../infra/machine-name.js"; import { getMachineDisplayName } from "../infra/machine-name.js";
@@ -53,7 +57,11 @@ import {
type BlockReplyChunking, type BlockReplyChunking,
subscribeEmbeddedPiSession, subscribeEmbeddedPiSession,
} from "./pi-embedded-subscribe.js"; } from "./pi-embedded-subscribe.js";
import { extractAssistantText } from "./pi-embedded-utils.js"; import {
extractAssistantText,
extractAssistantThinking,
formatReasoningMarkdown,
} from "./pi-embedded-utils.js";
import { toToolDefinitions } from "./pi-tool-definition-adapter.js"; import { toToolDefinitions } from "./pi-tool-definition-adapter.js";
import { createClawdbotCodingTools } from "./pi-tools.js"; import { createClawdbotCodingTools } from "./pi-tools.js";
import { resolveSandboxContext } from "./sandbox.js"; import { resolveSandboxContext } from "./sandbox.js";
@@ -575,6 +583,7 @@ export async function runEmbeddedPiAgent(params: {
authProfileId?: string; authProfileId?: string;
thinkLevel?: ThinkLevel; thinkLevel?: ThinkLevel;
verboseLevel?: VerboseLevel; verboseLevel?: VerboseLevel;
reasoningLevel?: ReasoningLevel;
bashElevated?: BashElevatedDefaults; bashElevated?: BashElevatedDefaults;
timeoutMs: number; timeoutMs: number;
runId: string; runId: string;
@@ -846,6 +855,7 @@ export async function runEmbeddedPiAgent(params: {
session, session,
runId: params.runId, runId: params.runId,
verboseLevel: params.verboseLevel, verboseLevel: params.verboseLevel,
includeReasoning: params.reasoningLevel === "on",
shouldEmitToolResult: params.shouldEmitToolResult, shouldEmitToolResult: params.shouldEmitToolResult,
onToolResult: params.onToolResult, onToolResult: params.onToolResult,
onBlockReply: params.onBlockReply, onBlockReply: params.onBlockReply,
@@ -1064,10 +1074,22 @@ export async function runEmbeddedPiAgent(params: {
} }
} }
const fallbackText = lastAssistant
? (() => {
const base = extractAssistantText(lastAssistant);
if (params.reasoningLevel !== "on") return base;
const thinking = extractAssistantThinking(lastAssistant);
const formatted = thinking
? formatReasoningMarkdown(thinking)
: "";
if (!formatted) return base;
return base ? `${base}\n\n${formatted}` : formatted;
})()
: "";
for (const text of assistantTexts.length for (const text of assistantTexts.length
? assistantTexts ? assistantTexts
: lastAssistant : fallbackText
? [extractAssistantText(lastAssistant)] ? [fallbackText]
: []) { : []) {
const { text: cleanedText, mediaUrls } = splitMediaFromOutput(text); const { text: cleanedText, mediaUrls } = splitMediaFromOutput(text);
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0)) if (!cleanedText && (!mediaUrls || mediaUrls.length === 0))

View File

@@ -10,6 +10,8 @@ import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js"; import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
import { import {
extractAssistantText, extractAssistantText,
extractAssistantThinking,
formatReasoningMarkdown,
inferToolMetaFromArgs, inferToolMetaFromArgs,
} from "./pi-embedded-utils.js"; } from "./pi-embedded-utils.js";
@@ -85,6 +87,7 @@ export function subscribeEmbeddedPiSession(params: {
session: AgentSession; session: AgentSession;
runId: string; runId: string;
verboseLevel?: "off" | "on"; verboseLevel?: "off" | "on";
includeReasoning?: boolean;
shouldEmitToolResult?: () => boolean; shouldEmitToolResult?: () => boolean;
onToolResult?: (payload: { onToolResult?: (payload: {
text?: string; text?: string;
@@ -120,6 +123,7 @@ export function subscribeEmbeddedPiSession(params: {
let pendingCompactionRetry = 0; let pendingCompactionRetry = 0;
let compactionRetryResolve: (() => void) | undefined; let compactionRetryResolve: (() => void) | undefined;
let compactionRetryPromise: Promise<void> | null = null; let compactionRetryPromise: Promise<void> | null = null;
let lastReasoningSent: string | undefined;
const ensureCompactionPromise = () => { const ensureCompactionPromise = () => {
if (!compactionRetryPromise) { if (!compactionRetryPromise) {
@@ -216,6 +220,24 @@ export function subscribeEmbeddedPiSession(params: {
}); });
}; };
const extractThinkingFromText = (text: string): string => {
if (!text || !THINKING_TAG_RE.test(text)) return "";
THINKING_TAG_RE.lastIndex = 0;
let result = "";
let lastIndex = 0;
let inThinking = false;
for (const match of text.matchAll(THINKING_TAG_RE)) {
const idx = match.index ?? 0;
if (inThinking) {
result += text.slice(lastIndex, idx);
}
const tag = match[0].toLowerCase();
inThinking = !tag.includes("/");
lastIndex = idx + match[0].length;
}
return result.trim();
};
const resetForCompactionRetry = () => { const resetForCompactionRetry = () => {
assistantTexts.length = 0; assistantTexts.length = 0;
toolMetas.length = 0; toolMetas.length = 0;
@@ -244,6 +266,7 @@ export function subscribeEmbeddedPiSession(params: {
blockChunker?.reset(); blockChunker?.reset();
lastStreamedAssistant = undefined; lastStreamedAssistant = undefined;
lastBlockReplyText = undefined; lastBlockReplyText = undefined;
lastReasoningSent = undefined;
assistantTextBaseline = assistantTexts.length; assistantTextBaseline = assistantTexts.length;
} }
} }
@@ -470,19 +493,26 @@ export function subscribeEmbeddedPiSession(params: {
if (evt.type === "message_end") { if (evt.type === "message_end") {
const msg = (evt as AgentEvent & { message: AgentMessage }).message; const msg = (evt as AgentEvent & { message: AgentMessage }).message;
if (msg?.role === "assistant") { if (msg?.role === "assistant") {
const assistantMessage = msg as AssistantMessage;
const rawText = extractAssistantText(assistantMessage);
const cleaned = params.enforceFinalTag const cleaned = params.enforceFinalTag
? stripThinkingSegments( ? stripThinkingSegments(stripUnpairedThinkingTags(rawText))
stripUnpairedThinkingTags( : stripThinkingSegments(rawText);
extractAssistantText(msg as AssistantMessage), const baseText =
),
)
: stripThinkingSegments(
extractAssistantText(msg as AssistantMessage),
);
const text =
params.enforceFinalTag && cleaned params.enforceFinalTag && cleaned
? (extractFinalText(cleaned)?.trim() ?? cleaned) ? (extractFinalText(cleaned)?.trim() ?? cleaned)
: cleaned; : cleaned;
const rawThinking = params.includeReasoning
? extractAssistantThinking(assistantMessage) ||
extractThinkingFromText(rawText)
: "";
const formattedReasoning = rawThinking
? formatReasoningMarkdown(rawThinking)
: "";
const text =
baseText && formattedReasoning
? `${baseText}\n\n${formattedReasoning}`
: baseText || formattedReasoning;
const addedDuringMessage = const addedDuringMessage =
assistantTexts.length > assistantTextBaseline; assistantTexts.length > assistantTextBaseline;
@@ -516,6 +546,16 @@ export function subscribeEmbeddedPiSession(params: {
} }
} }
} }
const onBlockReply = params.onBlockReply;
const shouldEmitReasoningBlock =
Boolean(formattedReasoning) &&
Boolean(onBlockReply) &&
formattedReasoning !== lastReasoningSent &&
(blockReplyBreak === "text_end" || Boolean(blockChunker));
if (shouldEmitReasoningBlock && formattedReasoning && onBlockReply) {
lastReasoningSent = formattedReasoning;
void onBlockReply({ text: formattedReasoning });
}
deltaBuffer = ""; deltaBuffer = "";
blockBuffer = ""; blockBuffer = "";
blockChunker?.reset(); blockChunker?.reset();

View File

@@ -19,6 +19,32 @@ export function extractAssistantText(msg: AssistantMessage): string {
return blocks.join("\n").trim(); return blocks.join("\n").trim();
} }
export function extractAssistantThinking(msg: AssistantMessage): string {
if (!Array.isArray(msg.content)) return "";
const blocks = msg.content
.map((block) => {
if (!block || typeof block !== "object") return "";
const record = block as unknown as Record<string, unknown>;
if (record.type === "thinking" && typeof record.thinking === "string") {
return record.thinking.trim();
}
return "";
})
.filter(Boolean);
return blocks.join("\n").trim();
}
export function formatReasoningMarkdown(text: string): string {
const trimmed = text.trim();
if (!trimmed) return "";
const lines = trimmed.split(/\r?\n/);
const wrapped = lines
.map((line) => line.trim())
.map((line) => (line ? `_${line}_` : ""))
.filter((line) => line.length > 0);
return wrapped.length > 0 ? [`_Reasoning:_`, ...wrapped].join("\n") : "";
}
export function inferToolMetaFromArgs( export function inferToolMetaFromArgs(
toolName: string, toolName: string,
args: unknown, args: unknown,

View File

@@ -79,6 +79,13 @@ const CHAT_COMMANDS: ChatCommandDefinition[] = [
textAliases: ["/verbose", "/v"], textAliases: ["/verbose", "/v"],
acceptsArgs: true, acceptsArgs: true,
}, },
{
key: "reasoning",
nativeName: "reasoning",
description: "Toggle reasoning visibility.",
textAliases: ["/reasoning", "/reason"],
acceptsArgs: true,
},
{ {
key: "elevated", key: "elevated",
nativeName: "elevated", nativeName: "elevated",

View File

@@ -15,6 +15,7 @@ import { drainSystemEvents } from "../infra/system-events.js";
import { import {
extractElevatedDirective, extractElevatedDirective,
extractQueueDirective, extractQueueDirective,
extractReasoningDirective,
extractReplyToTag, extractReplyToTag,
extractThinkDirective, extractThinkDirective,
extractVerboseDirective, extractVerboseDirective,
@@ -99,6 +100,12 @@ describe("directive parsing", () => {
expect(res.verboseLevel).toBe("on"); expect(res.verboseLevel).toBe("on");
}); });
it("matches reasoning directive", () => {
const res = extractReasoningDirective("/reasoning on please");
expect(res.hasDirective).toBe(true);
expect(res.reasoningLevel).toBe("on");
});
it("matches elevated with leading space", () => { it("matches elevated with leading space", () => {
const res = extractElevatedDirective(" please /elevated on now"); const res = extractElevatedDirective(" please /elevated on now");
expect(res.hasDirective).toBe(true); expect(res.hasDirective).toBe(true);

View File

@@ -66,6 +66,7 @@ import type { MsgContext, TemplateContext } from "./templating.js";
import { import {
type ElevatedLevel, type ElevatedLevel,
normalizeThinkLevel, normalizeThinkLevel,
type ReasoningLevel,
type ThinkLevel, type ThinkLevel,
type VerboseLevel, type VerboseLevel,
} from "./thinking.js"; } from "./thinking.js";
@@ -75,6 +76,7 @@ import type { GetReplyOptions, ReplyPayload } from "./types.js";
export { export {
extractElevatedDirective, extractElevatedDirective,
extractReasoningDirective,
extractThinkDirective, extractThinkDirective,
extractVerboseDirective, extractVerboseDirective,
} from "./reply/directives.js"; } from "./reply/directives.js";
@@ -288,6 +290,9 @@ export async function getReplyFromConfig(
hasVerboseDirective: false, hasVerboseDirective: false,
verboseLevel: undefined, verboseLevel: undefined,
rawVerboseLevel: undefined, rawVerboseLevel: undefined,
hasReasoningDirective: false,
reasoningLevel: undefined,
rawReasoningLevel: undefined,
hasElevatedDirective: false, hasElevatedDirective: false,
elevatedLevel: undefined, elevatedLevel: undefined,
rawElevatedLevel: undefined, rawElevatedLevel: undefined,
@@ -310,6 +315,7 @@ export async function getReplyFromConfig(
const hasDirective = const hasDirective =
parsedDirectives.hasThinkDirective || parsedDirectives.hasThinkDirective ||
parsedDirectives.hasVerboseDirective || parsedDirectives.hasVerboseDirective ||
parsedDirectives.hasReasoningDirective ||
parsedDirectives.hasElevatedDirective || parsedDirectives.hasElevatedDirective ||
parsedDirectives.hasStatusDirective || parsedDirectives.hasStatusDirective ||
parsedDirectives.hasModelDirective || parsedDirectives.hasModelDirective ||
@@ -327,6 +333,7 @@ export async function getReplyFromConfig(
...parsedDirectives, ...parsedDirectives,
hasThinkDirective: false, hasThinkDirective: false,
hasVerboseDirective: false, hasVerboseDirective: false,
hasReasoningDirective: false,
hasStatusDirective: false, hasStatusDirective: false,
hasModelDirective: false, hasModelDirective: false,
hasQueueDirective: false, hasQueueDirective: false,
@@ -377,6 +384,10 @@ export async function getReplyFromConfig(
(directives.verboseLevel as VerboseLevel | undefined) ?? (directives.verboseLevel as VerboseLevel | undefined) ??
(sessionEntry?.verboseLevel as VerboseLevel | undefined) ?? (sessionEntry?.verboseLevel as VerboseLevel | undefined) ??
(agentCfg?.verboseDefault as VerboseLevel | undefined); (agentCfg?.verboseDefault as VerboseLevel | undefined);
const resolvedReasoningLevel: ReasoningLevel =
(directives.reasoningLevel as ReasoningLevel | undefined) ??
(sessionEntry?.reasoningLevel as ReasoningLevel | undefined) ??
"off";
const resolvedElevatedLevel = elevatedAllowed const resolvedElevatedLevel = elevatedAllowed
? ((directives.elevatedLevel as ElevatedLevel | undefined) ?? ? ((directives.elevatedLevel as ElevatedLevel | undefined) ??
(sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ?? (sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
@@ -542,6 +553,7 @@ export async function getReplyFromConfig(
defaultGroupActivation: () => defaultActivation, defaultGroupActivation: () => defaultActivation,
resolvedThinkLevel, resolvedThinkLevel,
resolvedVerboseLevel: resolvedVerboseLevel ?? "off", resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
resolvedReasoningLevel,
resolvedElevatedLevel, resolvedElevatedLevel,
resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel, resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel,
provider, provider,
@@ -734,6 +746,7 @@ export async function getReplyFromConfig(
authProfileId, authProfileId,
thinkLevel: resolvedThinkLevel, thinkLevel: resolvedThinkLevel,
verboseLevel: resolvedVerboseLevel, verboseLevel: resolvedVerboseLevel,
reasoningLevel: resolvedReasoningLevel,
elevatedLevel: resolvedElevatedLevel, elevatedLevel: resolvedElevatedLevel,
bashElevated: { bashElevated: {
enabled: elevatedEnabled, enabled: elevatedEnabled,

View File

@@ -221,6 +221,7 @@ export async function runReplyAgent(params: {
authProfileId: followupRun.run.authProfileId, authProfileId: followupRun.run.authProfileId,
thinkLevel: followupRun.run.thinkLevel, thinkLevel: followupRun.run.thinkLevel,
verboseLevel: followupRun.run.verboseLevel, verboseLevel: followupRun.run.verboseLevel,
reasoningLevel: followupRun.run.reasoningLevel,
bashElevated: followupRun.run.bashElevated, bashElevated: followupRun.run.bashElevated,
timeoutMs: followupRun.run.timeoutMs, timeoutMs: followupRun.run.timeoutMs,
runId, runId,

View File

@@ -41,7 +41,12 @@ import {
formatTokenCount, formatTokenCount,
} from "../status.js"; } from "../status.js";
import type { MsgContext } from "../templating.js"; import type { MsgContext } from "../templating.js";
import type { ElevatedLevel, ThinkLevel, VerboseLevel } from "../thinking.js"; import type {
ElevatedLevel,
ReasoningLevel,
ThinkLevel,
VerboseLevel,
} from "../thinking.js";
import type { ReplyPayload } from "../types.js"; import type { ReplyPayload } from "../types.js";
import { isAbortTrigger, setAbortMemory } from "./abort.js"; import { isAbortTrigger, setAbortMemory } from "./abort.js";
import type { InlineDirectives } from "./directive-handling.js"; import type { InlineDirectives } from "./directive-handling.js";
@@ -202,6 +207,7 @@ export async function handleCommands(params: {
defaultGroupActivation: () => "always" | "mention"; defaultGroupActivation: () => "always" | "mention";
resolvedThinkLevel?: ThinkLevel; resolvedThinkLevel?: ThinkLevel;
resolvedVerboseLevel: VerboseLevel; resolvedVerboseLevel: VerboseLevel;
resolvedReasoningLevel: ReasoningLevel;
resolvedElevatedLevel?: ElevatedLevel; resolvedElevatedLevel?: ElevatedLevel;
resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>; resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
provider: string; provider: string;
@@ -226,6 +232,7 @@ export async function handleCommands(params: {
defaultGroupActivation, defaultGroupActivation,
resolvedThinkLevel, resolvedThinkLevel,
resolvedVerboseLevel, resolvedVerboseLevel,
resolvedReasoningLevel,
resolvedElevatedLevel, resolvedElevatedLevel,
resolveDefaultThinkingLevel, resolveDefaultThinkingLevel,
provider, provider,
@@ -405,6 +412,7 @@ export async function handleCommands(params: {
resolvedThink: resolvedThink:
resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()), resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
resolvedVerbose: resolvedVerboseLevel, resolvedVerbose: resolvedVerboseLevel,
resolvedReasoning: resolvedReasoningLevel,
resolvedElevated: resolvedElevatedLevel, resolvedElevated: resolvedElevatedLevel,
modelAuth: resolveModelAuthLabel(provider, cfg), modelAuth: resolveModelAuthLabel(provider, cfg),
webLinked, webLinked,

View File

@@ -32,9 +32,11 @@ import type { ReplyPayload } from "../types.js";
import { import {
type ElevatedLevel, type ElevatedLevel,
extractElevatedDirective, extractElevatedDirective,
extractReasoningDirective,
extractStatusDirective, extractStatusDirective,
extractThinkDirective, extractThinkDirective,
extractVerboseDirective, extractVerboseDirective,
type ReasoningLevel,
type ThinkLevel, type ThinkLevel,
type VerboseLevel, type VerboseLevel,
} from "./directives.js"; } from "./directives.js";
@@ -155,6 +157,9 @@ export type InlineDirectives = {
hasVerboseDirective: boolean; hasVerboseDirective: boolean;
verboseLevel?: VerboseLevel; verboseLevel?: VerboseLevel;
rawVerboseLevel?: string; rawVerboseLevel?: string;
hasReasoningDirective: boolean;
reasoningLevel?: ReasoningLevel;
rawReasoningLevel?: string;
hasElevatedDirective: boolean; hasElevatedDirective: boolean;
elevatedLevel?: ElevatedLevel; elevatedLevel?: ElevatedLevel;
rawElevatedLevel?: string; rawElevatedLevel?: string;
@@ -188,12 +193,18 @@ export function parseInlineDirectives(body: string): InlineDirectives {
rawLevel: rawVerboseLevel, rawLevel: rawVerboseLevel,
hasDirective: hasVerboseDirective, hasDirective: hasVerboseDirective,
} = extractVerboseDirective(thinkCleaned); } = extractVerboseDirective(thinkCleaned);
const {
cleaned: reasoningCleaned,
reasoningLevel,
rawLevel: rawReasoningLevel,
hasDirective: hasReasoningDirective,
} = extractReasoningDirective(verboseCleaned);
const { const {
cleaned: elevatedCleaned, cleaned: elevatedCleaned,
elevatedLevel, elevatedLevel,
rawLevel: rawElevatedLevel, rawLevel: rawElevatedLevel,
hasDirective: hasElevatedDirective, hasDirective: hasElevatedDirective,
} = extractElevatedDirective(verboseCleaned); } = extractElevatedDirective(reasoningCleaned);
const { cleaned: statusCleaned, hasDirective: hasStatusDirective } = const { cleaned: statusCleaned, hasDirective: hasStatusDirective } =
extractStatusDirective(elevatedCleaned); extractStatusDirective(elevatedCleaned);
const { const {
@@ -225,6 +236,9 @@ export function parseInlineDirectives(body: string): InlineDirectives {
hasVerboseDirective, hasVerboseDirective,
verboseLevel, verboseLevel,
rawVerboseLevel, rawVerboseLevel,
hasReasoningDirective,
reasoningLevel,
rawReasoningLevel,
hasElevatedDirective, hasElevatedDirective,
elevatedLevel, elevatedLevel,
rawElevatedLevel, rawElevatedLevel,
@@ -257,6 +271,7 @@ export function isDirectiveOnly(params: {
if ( if (
!directives.hasThinkDirective && !directives.hasThinkDirective &&
!directives.hasVerboseDirective && !directives.hasVerboseDirective &&
!directives.hasReasoningDirective &&
!directives.hasElevatedDirective && !directives.hasElevatedDirective &&
!directives.hasModelDirective && !directives.hasModelDirective &&
!directives.hasQueueDirective !directives.hasQueueDirective
@@ -367,6 +382,11 @@ export async function handleDirectiveOnly(params: {
text: `Unrecognized verbose level "${directives.rawVerboseLevel ?? ""}". Valid levels: off, on.`, text: `Unrecognized verbose level "${directives.rawVerboseLevel ?? ""}". Valid levels: off, on.`,
}; };
} }
if (directives.hasReasoningDirective && !directives.reasoningLevel) {
return {
text: `Unrecognized reasoning level "${directives.rawReasoningLevel ?? ""}". Valid levels: on, off.`,
};
}
if (directives.hasElevatedDirective && !directives.elevatedLevel) { if (directives.hasElevatedDirective && !directives.elevatedLevel) {
return { return {
text: `Unrecognized elevated level "${directives.rawElevatedLevel ?? ""}". Valid levels: off, on.`, text: `Unrecognized elevated level "${directives.rawElevatedLevel ?? ""}". Valid levels: off, on.`,
@@ -476,6 +496,11 @@ export async function handleDirectiveOnly(params: {
if (directives.verboseLevel === "off") delete sessionEntry.verboseLevel; if (directives.verboseLevel === "off") delete sessionEntry.verboseLevel;
else sessionEntry.verboseLevel = directives.verboseLevel; else sessionEntry.verboseLevel = directives.verboseLevel;
} }
if (directives.hasReasoningDirective && directives.reasoningLevel) {
if (directives.reasoningLevel === "off")
delete sessionEntry.reasoningLevel;
else sessionEntry.reasoningLevel = directives.reasoningLevel;
}
if (directives.hasElevatedDirective && directives.elevatedLevel) { if (directives.hasElevatedDirective && directives.elevatedLevel) {
if (directives.elevatedLevel === "off") delete sessionEntry.elevatedLevel; if (directives.elevatedLevel === "off") delete sessionEntry.elevatedLevel;
else sessionEntry.elevatedLevel = directives.elevatedLevel; else sessionEntry.elevatedLevel = directives.elevatedLevel;
@@ -533,6 +558,13 @@ export async function handleDirectiveOnly(params: {
: `${SYSTEM_MARK} Verbose logging enabled.`, : `${SYSTEM_MARK} Verbose logging enabled.`,
); );
} }
if (directives.hasReasoningDirective && directives.reasoningLevel) {
parts.push(
directives.reasoningLevel === "off"
? `${SYSTEM_MARK} Reasoning visibility disabled.`
: `${SYSTEM_MARK} Reasoning visibility enabled.`,
);
}
if (directives.hasElevatedDirective && directives.elevatedLevel) { if (directives.hasElevatedDirective && directives.elevatedLevel) {
parts.push( parts.push(
directives.elevatedLevel === "off" directives.elevatedLevel === "off"
@@ -634,6 +666,14 @@ export async function persistInlineDirectives(params: {
} }
updated = true; updated = true;
} }
if (directives.hasReasoningDirective && directives.reasoningLevel) {
if (directives.reasoningLevel === "off") {
delete sessionEntry.reasoningLevel;
} else {
sessionEntry.reasoningLevel = directives.reasoningLevel;
}
updated = true;
}
if ( if (
directives.hasElevatedDirective && directives.hasElevatedDirective &&
directives.elevatedLevel && directives.elevatedLevel &&

View File

@@ -1,6 +1,8 @@
import type { ReasoningLevel } from "../thinking.js";
import { import {
type ElevatedLevel, type ElevatedLevel,
normalizeElevatedLevel, normalizeElevatedLevel,
normalizeReasoningLevel,
normalizeThinkLevel, normalizeThinkLevel,
normalizeVerboseLevel, normalizeVerboseLevel,
type ThinkLevel, type ThinkLevel,
@@ -74,6 +76,28 @@ export function extractElevatedDirective(body?: string): {
}; };
} }
export function extractReasoningDirective(body?: string): {
cleaned: string;
reasoningLevel?: ReasoningLevel;
rawLevel?: string;
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
const match = body.match(
/(?:^|\s)\/(?:reasoning|reason)(?=$|\s|:)\s*:?\s*([a-zA-Z-]+)\b/i,
);
const reasoningLevel = normalizeReasoningLevel(match?.[1]);
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
: body.trim();
return {
cleaned,
reasoningLevel,
rawLevel: match?.[1],
hasDirective: !!match,
};
}
export function extractStatusDirective(body?: string): { export function extractStatusDirective(body?: string): {
cleaned: string; cleaned: string;
hasDirective: boolean; hasDirective: boolean;
@@ -89,4 +113,4 @@ export function extractStatusDirective(body?: string): {
}; };
} }
export type { ElevatedLevel, ThinkLevel, VerboseLevel }; export type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel };

View File

@@ -131,6 +131,7 @@ export function createFollowupRunner(params: {
authProfileId: queued.run.authProfileId, authProfileId: queued.run.authProfileId,
thinkLevel: queued.run.thinkLevel, thinkLevel: queued.run.thinkLevel,
verboseLevel: queued.run.verboseLevel, verboseLevel: queued.run.verboseLevel,
reasoningLevel: queued.run.reasoningLevel,
bashElevated: queued.run.bashElevated, bashElevated: queued.run.bashElevated,
timeoutMs: queued.run.timeoutMs, timeoutMs: queued.run.timeoutMs,
runId, runId,

View File

@@ -4,7 +4,12 @@ import type { ClawdbotConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js"; import type { SessionEntry } from "../../config/sessions.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import type { OriginatingChannelType } from "../templating.js"; import type { OriginatingChannelType } from "../templating.js";
import type { ElevatedLevel, ThinkLevel, VerboseLevel } from "./directives.js"; import type {
ElevatedLevel,
ReasoningLevel,
ThinkLevel,
VerboseLevel,
} from "./directives.js";
import { isRoutableChannel } from "./route-reply.js"; import { isRoutableChannel } from "./route-reply.js";
export type QueueMode = export type QueueMode =
| "steer" | "steer"
@@ -54,6 +59,7 @@ export type FollowupRun = {
authProfileId?: string; authProfileId?: string;
thinkLevel?: ThinkLevel; thinkLevel?: ThinkLevel;
verboseLevel?: VerboseLevel; verboseLevel?: VerboseLevel;
reasoningLevel?: ReasoningLevel;
elevatedLevel?: ElevatedLevel; elevatedLevel?: ElevatedLevel;
bashElevated?: { bashElevated?: {
enabled: boolean; enabled: boolean;

View File

@@ -20,7 +20,12 @@ import {
type SessionScope, type SessionScope,
} from "../config/sessions.js"; } from "../config/sessions.js";
import { shortenHomePath } from "../utils.js"; import { shortenHomePath } from "../utils.js";
import type { ElevatedLevel, ThinkLevel, VerboseLevel } from "./thinking.js"; import type {
ElevatedLevel,
ReasoningLevel,
ThinkLevel,
VerboseLevel,
} from "./thinking.js";
type AgentConfig = NonNullable<ClawdbotConfig["agent"]>; type AgentConfig = NonNullable<ClawdbotConfig["agent"]>;
@@ -34,6 +39,7 @@ type StatusArgs = {
groupActivation?: "mention" | "always"; groupActivation?: "mention" | "always";
resolvedThink?: ThinkLevel; resolvedThink?: ThinkLevel;
resolvedVerbose?: VerboseLevel; resolvedVerbose?: VerboseLevel;
resolvedReasoning?: ReasoningLevel;
resolvedElevated?: ElevatedLevel; resolvedElevated?: ElevatedLevel;
modelAuth?: string; modelAuth?: string;
now?: number; now?: number;
@@ -173,6 +179,7 @@ export function buildStatusMessage(args: StatusArgs): string {
const thinkLevel = args.resolvedThink ?? args.agent?.thinkingDefault ?? "off"; const thinkLevel = args.resolvedThink ?? args.agent?.thinkingDefault ?? "off";
const verboseLevel = const verboseLevel =
args.resolvedVerbose ?? args.agent?.verboseDefault ?? "off"; args.resolvedVerbose ?? args.agent?.verboseDefault ?? "off";
const reasoningLevel = args.resolvedReasoning ?? "off";
const elevatedLevel = const elevatedLevel =
args.resolvedElevated ?? args.resolvedElevated ??
args.sessionEntry?.elevatedLevel ?? args.sessionEntry?.elevatedLevel ??
@@ -241,8 +248,8 @@ export function buildStatusMessage(args: StatusArgs): string {
)}${entry?.abortedLastRun ? " • last run aborted" : ""}`; )}${entry?.abortedLastRun ? " • last run aborted" : ""}`;
const optionsLine = runtime.sandboxed const optionsLine = runtime.sandboxed
? `Options: thinking=${thinkLevel} | verbose=${verboseLevel} | elevated=${elevatedLevel}` ? `Options: thinking=${thinkLevel} | verbose=${verboseLevel} | reasoning=${reasoningLevel} | elevated=${elevatedLevel}`
: `Options: thinking=${thinkLevel} | verbose=${verboseLevel}`; : `Options: thinking=${thinkLevel} | verbose=${verboseLevel} | reasoning=${reasoningLevel}`;
const modelLabel = model ? `${provider}/${model}` : "unknown"; const modelLabel = model ? `${provider}/${model}` : "unknown";
@@ -273,6 +280,6 @@ export function buildHelpMessage(): string {
return [ return [
" Help", " Help",
"Shortcuts: /new reset | /compact [instructions] | /restart relink", "Shortcuts: /new reset | /compact [instructions] | /restart relink",
"Options: /think <level> | /verbose on|off | /elevated on|off | /model <id>", "Options: /think <level> | /verbose on|off | /reasoning on|off | /elevated on|off | /model <id>",
].join("\n"); ].join("\n");
} }

View File

@@ -1,8 +1,20 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { normalizeThinkLevel } from "./thinking.js"; import { normalizeReasoningLevel, normalizeThinkLevel } from "./thinking.js";
describe("normalizeThinkLevel", () => { describe("normalizeThinkLevel", () => {
it("accepts mid as medium", () => { it("accepts mid as medium", () => {
expect(normalizeThinkLevel("mid")).toBe("medium"); expect(normalizeThinkLevel("mid")).toBe("medium");
}); });
}); });
describe("normalizeReasoningLevel", () => {
it("accepts on/off", () => {
expect(normalizeReasoningLevel("on")).toBe("on");
expect(normalizeReasoningLevel("off")).toBe("off");
});
it("accepts show/hide", () => {
expect(normalizeReasoningLevel("show")).toBe("on");
expect(normalizeReasoningLevel("hide")).toBe("off");
});
});

View File

@@ -1,6 +1,7 @@
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high"; export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
export type VerboseLevel = "off" | "on"; export type VerboseLevel = "off" | "on";
export type ElevatedLevel = "off" | "on"; export type ElevatedLevel = "off" | "on";
export type ReasoningLevel = "off" | "on";
// Normalize user-provided thinking level strings to the canonical enum. // Normalize user-provided thinking level strings to the canonical enum.
export function normalizeThinkLevel( export function normalizeThinkLevel(
@@ -55,3 +56,31 @@ export function normalizeElevatedLevel(
if (["on", "true", "yes", "1"].includes(key)) return "on"; if (["on", "true", "yes", "1"].includes(key)) return "on";
return undefined; return undefined;
} }
// Normalize reasoning visibility flags used to toggle reasoning exposure.
export function normalizeReasoningLevel(
raw?: string | null,
): ReasoningLevel | undefined {
if (!raw) return undefined;
const key = raw.toLowerCase();
if (
[
"off",
"false",
"no",
"0",
"hide",
"hidden",
"disable",
"disabled",
].includes(key)
)
return "off";
if (
["on", "true", "yes", "1", "show", "visible", "enable", "enabled"].includes(
key,
)
)
return "on";
return undefined;
}

View File

@@ -26,6 +26,7 @@ type SessionRow = {
abortedLastRun?: boolean; abortedLastRun?: boolean;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
reasoningLevel?: string;
groupActivation?: string; groupActivation?: string;
inputTokens?: number; inputTokens?: number;
outputTokens?: number; outputTokens?: number;
@@ -99,6 +100,7 @@ const formatFlagsCell = (row: SessionRow, rich: boolean) => {
const flags = [ const flags = [
row.thinkingLevel ? `think:${row.thinkingLevel}` : null, row.thinkingLevel ? `think:${row.thinkingLevel}` : null,
row.verboseLevel ? `verbose:${row.verboseLevel}` : null, row.verboseLevel ? `verbose:${row.verboseLevel}` : null,
row.reasoningLevel ? `reasoning:${row.reasoningLevel}` : null,
row.groupActivation ? `activation:${row.groupActivation}` : null, row.groupActivation ? `activation:${row.groupActivation}` : null,
row.systemSent ? "system" : null, row.systemSent ? "system" : null,
row.abortedLastRun ? "aborted" : null, row.abortedLastRun ? "aborted" : null,
@@ -147,6 +149,7 @@ function toRows(store: Record<string, SessionEntry>): SessionRow[] {
abortedLastRun: entry?.abortedLastRun, abortedLastRun: entry?.abortedLastRun,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
groupActivation: entry?.groupActivation, groupActivation: entry?.groupActivation,
inputTokens: entry?.inputTokens, inputTokens: entry?.inputTokens,
outputTokens: entry?.outputTokens, outputTokens: entry?.outputTokens,

View File

@@ -33,6 +33,7 @@ export type SessionStatus = {
age: number | null; age: number | null;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
reasoningLevel?: string;
elevatedLevel?: string; elevatedLevel?: string;
systemSent?: boolean; systemSent?: boolean;
abortedLastRun?: boolean; abortedLastRun?: boolean;
@@ -111,6 +112,7 @@ export async function getStatusSummary(): Promise<StatusSummary> {
age, age,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
elevatedLevel: entry?.elevatedLevel, elevatedLevel: entry?.elevatedLevel,
systemSent: entry?.systemSent, systemSent: entry?.systemSent,
abortedLastRun: entry?.abortedLastRun, abortedLastRun: entry?.abortedLastRun,
@@ -198,6 +200,9 @@ const buildFlags = (entry: SessionEntry): string[] => {
const verbose = entry?.verboseLevel; const verbose = entry?.verboseLevel;
if (typeof verbose === "string" && verbose.length > 0) if (typeof verbose === "string" && verbose.length > 0)
flags.push(`verbose:${verbose}`); flags.push(`verbose:${verbose}`);
const reasoning = entry?.reasoningLevel;
if (typeof reasoning === "string" && reasoning.length > 0)
flags.push(`reasoning:${reasoning}`);
const elevated = entry?.elevatedLevel; const elevated = entry?.elevatedLevel;
if (typeof elevated === "string" && elevated.length > 0) if (typeof elevated === "string" && elevated.length > 0)
flags.push(`elevated:${elevated}`); flags.push(`elevated:${elevated}`);

View File

@@ -40,6 +40,7 @@ export type SessionEntry = {
chatType?: SessionChatType; chatType?: SessionChatType;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
reasoningLevel?: string;
elevatedLevel?: string; elevatedLevel?: string;
providerOverride?: string; providerOverride?: string;
modelOverride?: string; modelOverride?: string;

View File

@@ -323,6 +323,7 @@ export const SessionsPatchParamsSchema = Type.Object(
key: NonEmptyString, key: NonEmptyString,
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
reasoningLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),

View File

@@ -20,6 +20,7 @@ import { resolveAgentTimeoutMs } from "../agents/timeout.js";
import { normalizeGroupActivation } from "../auto-reply/group-activation.js"; import { normalizeGroupActivation } from "../auto-reply/group-activation.js";
import { import {
normalizeElevatedLevel, normalizeElevatedLevel,
normalizeReasoningLevel,
normalizeThinkLevel, normalizeThinkLevel,
normalizeVerboseLevel, normalizeVerboseLevel,
} from "../auto-reply/thinking.js"; } from "../auto-reply/thinking.js";
@@ -434,6 +435,26 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
} }
} }
if ("reasoningLevel" in p) {
const raw = p.reasoningLevel;
if (raw === null) {
delete next.reasoningLevel;
} else if (raw !== undefined) {
const normalized = normalizeReasoningLevel(String(raw));
if (!normalized) {
return {
ok: false,
error: {
code: ErrorCodes.INVALID_REQUEST,
message: `invalid reasoningLevel: ${String(raw)}`,
},
};
}
if (normalized === "off") delete next.reasoningLevel;
else next.reasoningLevel = normalized;
}
}
if ("elevatedLevel" in p) { if ("elevatedLevel" in p) {
const raw = p.elevatedLevel; const raw = p.elevatedLevel;
if (raw === null) { if (raw === null) {
@@ -602,6 +623,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
abortedLastRun: false, abortedLastRun: false,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
model: entry?.model, model: entry?.model,
contextTokens: entry?.contextTokens, contextTokens: entry?.contextTokens,
sendPolicy: entry?.sendPolicy, sendPolicy: entry?.sendPolicy,
@@ -986,6 +1008,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
updatedAt: now, updatedAt: now,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
systemSent: entry?.systemSent, systemSent: entry?.systemSent,
lastProvider: entry?.lastProvider, lastProvider: entry?.lastProvider,
lastTo: entry?.lastTo, lastTo: entry?.lastTo,
@@ -1125,6 +1148,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
updatedAt: now, updatedAt: now,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
systemSent: entry?.systemSent, systemSent: entry?.systemSent,
sendPolicy: entry?.sendPolicy, sendPolicy: entry?.sendPolicy,
lastProvider: entry?.lastProvider, lastProvider: entry?.lastProvider,
@@ -1207,6 +1231,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
updatedAt: now, updatedAt: now,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
systemSent: entry?.systemSent, systemSent: entry?.systemSent,
sendPolicy: entry?.sendPolicy, sendPolicy: entry?.sendPolicy,
lastProvider: entry?.lastProvider, lastProvider: entry?.lastProvider,

View File

@@ -82,6 +82,7 @@ export const agentHandlers: GatewayRequestHandlers = {
updatedAt: now, updatedAt: now,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
systemSent: entry?.systemSent, systemSent: entry?.systemSent,
sendPolicy: entry?.sendPolicy, sendPolicy: entry?.sendPolicy,
skillsSnapshot: entry?.skillsSnapshot, skillsSnapshot: entry?.skillsSnapshot,

View File

@@ -200,6 +200,7 @@ export const chatHandlers: GatewayRequestHandlers = {
updatedAt: now, updatedAt: now,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
systemSent: entry?.systemSent, systemSent: entry?.systemSent,
sendPolicy: entry?.sendPolicy, sendPolicy: entry?.sendPolicy,
lastProvider: entry?.lastProvider, lastProvider: entry?.lastProvider,

View File

@@ -17,6 +17,7 @@ import {
} from "../../agents/pi-embedded.js"; } from "../../agents/pi-embedded.js";
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js"; import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
import { import {
normalizeReasoningLevel,
normalizeThinkLevel, normalizeThinkLevel,
normalizeVerboseLevel, normalizeVerboseLevel,
} from "../../auto-reply/thinking.js"; } from "../../auto-reply/thinking.js";
@@ -211,6 +212,28 @@ export const sessionsHandlers: GatewayRequestHandlers = {
} }
} }
if ("reasoningLevel" in p) {
const raw = p.reasoningLevel;
if (raw === null) {
delete next.reasoningLevel;
} else if (raw !== undefined) {
const normalized = normalizeReasoningLevel(String(raw));
if (!normalized) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
'invalid reasoningLevel (use "on"|"off")',
),
);
return;
}
if (normalized === "off") delete next.reasoningLevel;
else next.reasoningLevel = normalized;
}
}
if ("model" in p) { if ("model" in p) {
const raw = p.model; const raw = p.model;
if (raw === null) { if (raw === null) {
@@ -370,6 +393,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
abortedLastRun: false, abortedLastRun: false,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
model: entry?.model, model: entry?.model,
contextTokens: entry?.contextTokens, contextTokens: entry?.contextTokens,
sendPolicy: entry?.sendPolicy, sendPolicy: entry?.sendPolicy,

View File

@@ -43,6 +43,7 @@ export type GatewaySessionRow = {
abortedLastRun?: boolean; abortedLastRun?: boolean;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
reasoningLevel?: string;
elevatedLevel?: string; elevatedLevel?: string;
sendPolicy?: "allow" | "deny"; sendPolicy?: "allow" | "deny";
inputTokens?: number; inputTokens?: number;
@@ -441,6 +442,7 @@ export function listSessionsFromStore(params: {
abortedLastRun: entry?.abortedLastRun, abortedLastRun: entry?.abortedLastRun,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
elevatedLevel: entry?.elevatedLevel, elevatedLevel: entry?.elevatedLevel,
sendPolicy: entry?.sendPolicy, sendPolicy: entry?.sendPolicy,
inputTokens: entry?.inputTokens, inputTokens: entry?.inputTokens,

View File

@@ -2,6 +2,7 @@ import type { SlashCommand } from "@mariozechner/pi-tui";
const THINK_LEVELS = ["off", "minimal", "low", "medium", "high"]; const THINK_LEVELS = ["off", "minimal", "low", "medium", "high"];
const VERBOSE_LEVELS = ["on", "off"]; const VERBOSE_LEVELS = ["on", "off"];
const REASONING_LEVELS = ["on", "off"];
const ELEVATED_LEVELS = ["on", "off"]; const ELEVATED_LEVELS = ["on", "off"];
const ACTIVATION_LEVELS = ["mention", "always"]; const ACTIVATION_LEVELS = ["mention", "always"];
const TOGGLE = ["on", "off"]; const TOGGLE = ["on", "off"];
@@ -53,6 +54,14 @@ export function getSlashCommands(): SlashCommand[] {
(value) => ({ value, label: value }), (value) => ({ value, label: value }),
), ),
}, },
{
name: "reasoning",
description: "Set reasoning on/off",
getArgumentCompletions: (prefix) =>
REASONING_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map(
(value) => ({ value, label: value }),
),
},
{ {
name: "elevated", name: "elevated",
description: "Set elevated on/off", description: "Set elevated on/off",
@@ -103,6 +112,7 @@ export function helpText(): string {
"/model <provider/model> (or /models)", "/model <provider/model> (or /models)",
"/think <off|minimal|low|medium|high>", "/think <off|minimal|low|medium|high>",
"/verbose <on|off>", "/verbose <on|off>",
"/reasoning <on|off>",
"/elevated <on|off>", "/elevated <on|off>",
"/elev <on|off>", "/elev <on|off>",
"/activation <mention|always>", "/activation <mention|always>",

View File

@@ -40,6 +40,7 @@ export type GatewaySessionList = {
updatedAt?: number | null; updatedAt?: number | null;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
reasoningLevel?: string;
sendPolicy?: string; sendPolicy?: string;
model?: string; model?: string;
contextTokens?: number | null; contextTokens?: number | null;

View File

@@ -45,6 +45,7 @@ type AgentEvent = {
type SessionInfo = { type SessionInfo = {
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
reasoningLevel?: string;
model?: string; model?: string;
contextTokens?: number | null; contextTokens?: number | null;
totalTokens?: number | null; totalTokens?: number | null;
@@ -167,10 +168,11 @@ export async function runTui(opts: TuiOptions) {
); );
const think = sessionInfo.thinkingLevel ?? "off"; const think = sessionInfo.thinkingLevel ?? "off";
const verbose = sessionInfo.verboseLevel ?? "off"; const verbose = sessionInfo.verboseLevel ?? "off";
const reasoning = sessionInfo.reasoningLevel ?? "off";
const deliver = deliverDefault ? "on" : "off"; const deliver = deliverDefault ? "on" : "off";
footer.setText( footer.setText(
theme.dim( theme.dim(
`${connection} | session ${sessionLabel} | model ${modelLabel} | think ${think} | verbose ${verbose} | ${tokens} | deliver ${deliver}`, `${connection} | session ${sessionLabel} | model ${modelLabel} | think ${think} | verbose ${verbose} | reasoning ${reasoning} | ${tokens} | deliver ${deliver}`,
), ),
); );
}; };
@@ -198,6 +200,7 @@ export async function runTui(opts: TuiOptions) {
sessionInfo = { sessionInfo = {
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
model: entry?.model ?? result.defaults?.model ?? undefined, model: entry?.model ?? result.defaults?.model ?? undefined,
contextTokens: entry?.contextTokens ?? result.defaults?.contextTokens, contextTokens: entry?.contextTokens ?? result.defaults?.contextTokens,
totalTokens: entry?.totalTokens ?? null, totalTokens: entry?.totalTokens ?? null,
@@ -586,6 +589,22 @@ export async function runTui(opts: TuiOptions) {
chatLog.addSystem(`verbose failed: ${String(err)}`); chatLog.addSystem(`verbose failed: ${String(err)}`);
} }
break; break;
case "reasoning":
if (!args) {
chatLog.addSystem("usage: /reasoning <on|off>");
break;
}
try {
await client.patchSession({
key: currentSessionKey,
reasoningLevel: args,
});
chatLog.addSystem(`reasoning set to ${args}`);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`reasoning failed: ${String(err)}`);
}
break;
case "elevated": case "elevated":
if (!args) { if (!args) {
chatLog.addSystem("usage: /elevated <on|off>"); chatLog.addSystem("usage: /elevated <on|off>");

View File

@@ -226,6 +226,7 @@ export type GatewaySessionRow = {
abortedLastRun?: boolean; abortedLastRun?: boolean;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
reasoningLevel?: string;
elevatedLevel?: string; elevatedLevel?: string;
inputTokens?: number; inputTokens?: number;
outputTokens?: number; outputTokens?: number;
@@ -251,6 +252,7 @@ export type SessionsPatchResult = {
updatedAt?: number; updatedAt?: number;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
reasoningLevel?: string;
elevatedLevel?: string; elevatedLevel?: string;
}; };
}; };