Files
oh-my-pi/packages/coding-agent/src/tools/think.ts
T
can1357 02cd22dc9b feat: added live tracking and stale status warnings for agent activity snapshots
- Added live tracking and stale status warnings for agent activity snapshots.
- Fixed text wrapping with ANSI escape sequences to defer style open sequences after whitespace.
- Added VirtualRenderScheduler for deterministic virtual-clock rendering tests.
2026-08-16 10:18:56 +03:00

97 lines
3.3 KiB
TypeScript

import { type } from "@oh-my-pi/omptype";
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
import type { Model } from "@oh-my-pi/pi-ai";
import { type Component, Markdown } from "@oh-my-pi/pi-tui";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
/** Whether a model transport can suppress native reasoning while private scratchpad thoughts are active. */
export function supportsExternalThinking(model: Model | null | undefined): boolean {
if (!model) return false;
const compat = model.compat;
const requiresThinking =
model.api === "anthropic-messages" &&
compat !== undefined &&
"requiresThinkingEnabled" in compat &&
compat.requiresThinkingEnabled === true;
if (model.reasoning && (requiresThinking || (model.thinking?.requiresEffort && !model.thinking.suppressWhenOff))) {
return false;
}
// Transports that reject `reasoning.effort` (xAI Grok 4 and the other
// reasoning-only Responses models) cannot honour `forceReasoningOff`, so the
// scratchpad would run alongside native reasoning instead of replacing it.
if (
model.reasoning &&
compat !== undefined &&
(("omitReasoningEffort" in compat && compat.omitReasoningEffort === true) ||
("supportsReasoningEffort" in compat && compat.supportsReasoningEffort === false))
) {
return false;
}
if (model.api === "google-generative-ai" || model.api === "google-gemini-cli" || model.api === "google-vertex") {
return !model.reasoning || model.thinking?.mode === "budget" || model.thinking?.suppressWhenOff === true;
}
return (
model.api === "openai-responses" ||
model.api === "azure-openai-responses" ||
model.api === "openai-codex-responses" ||
model.api === "anthropic-messages"
);
}
const thinkSchema = type({
thoughts: type("string").describe("private scratchpad; not shown to user"),
"+": "reject",
}).describe("private scratchpad; not shown to user");
type ThinkParams = typeof thinkSchema.infer;
export type ThinkRenderArgs = {
thoughts?: string;
};
export const thinkToolRenderer = {
inline: true,
renderCall(args: ThinkRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
const thoughts =
typeof args === "object" && args !== null && "thoughts" in args && typeof args.thoughts === "string"
? args.thoughts
: "";
return new Markdown(thoughts, 1, 0, getMarkdownTheme(), {
color: (text: string) => uiTheme.fg("thinkingText", text),
italic: true,
});
},
renderResult(): Component {
return undefined as unknown as Component;
},
};
interface ThinkToolDetails {
recorded: true;
}
/** Records private scratchpad thoughts while native model reasoning is disabled. */
export class ThinkTool implements AgentTool<typeof thinkSchema, ThinkToolDetails> {
readonly name = "think";
readonly approval = "read" as const;
readonly label = "Think";
readonly summary = "Record private scratchpad thoughts";
readonly description = "private scratchpad; not shown to user";
readonly parameters = thinkSchema;
readonly strict = true;
readonly intent = "omit" as const;
async execute(_toolCallId: string, _params: ThinkParams): Promise<AgentToolResult<ThinkToolDetails>> {
return {
content: [
{
type: "text",
text: "------",
},
],
details: { recorded: true },
};
}
}