refactor(prompt-templates): migrated prompt utilities to pi-utils package

- Extracted prompt rendering and formatting utilities from coding-agent to centralized pi-utils package with new API surface (prompt.render, prompt.format, prompt.registerHelper).
- Migrated parseFrontmatter utility from coding-agent to pi-utils package; updated 8 files to import from @oh-my-pi/pi-utils.
- Removed 170-line prompt-format.ts module and consolidated 192 lines of Handlebars helper registrations into pi-utils prompt module.
- Updated 60+ files across coding-agent and typescript-edit-benchmark to use new prompt.render() and prompt.format() API from pi-utils.
- Simplified prompt-templates.ts by delegating core functionality to pi-utils while retaining custom helper registrations (jtdToTypeScript, jsonStringify, etc.).
This commit is contained in:
can1357
2026-04-08 05:47:35 +02:00
parent 6d07944654
commit a21a542afd
79 changed files with 641 additions and 649 deletions
+1
View File
@@ -169,6 +169,7 @@
"version": "13.19.0",
"dependencies": {
"beautiful-mermaid": "^1.1",
"handlebars": "^4.7.9",
"winston": "^3.19",
"winston-daily-rotate-file": "^5.0",
},
+2 -2
View File
@@ -160,7 +160,7 @@ The Extensions dashboard also loads `slash-commands` capability and displays act
If text still starts with `/`, attempt markdown command expansion.
4. **Prompt templates** (`expandPromptTemplate`)
Applied after slash/custom processing.
5. **Delivery**
5. **Delivery**
- idle: prompt is sent immediately to agent
- streaming: prompt is queued as steer/follow-up depending on `streamingBehavior`
@@ -177,7 +177,7 @@ This is why slash command expansion sits before prompt-template expansion, and w
- if matched, applies:
- positional replacement: `$1`, `$2`, ...
- aggregate replacement: `$ARGUMENTS` and `$@`
- then template rendering via `renderPromptTemplate` with `{ args, ARGUMENTS, arguments }`
- then template rendering via `prompt.render` with `{ args, ARGUMENTS, arguments }`
- if no match, returns original text unchanged
### `parseCommandArgs` caveats
+5 -1
View File
@@ -1,7 +1,6 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Simplified chunk edit operations: removed `append_child`, `prepend_child`, `append_sibling`, `prepend_sibling`, and `replace_body` ops in favor of unified `replace`, `before`, `after`, `prepend`, and `append` with region targeting (`@container`, `@prologue`, `@body`, `@epilogue`)
@@ -41,6 +40,9 @@
### Changed
- Moved prompt formatting and rendering utilities from `coding-agent` to `pi-utils` package; `renderPromptTemplate()` and `formatPromptContent()` now accessed via `prompt.render()` and `prompt.format()` from `@oh-my-pi/pi-utils`
- Moved `parseFrontmatter()` utility from `coding-agent` to `pi-utils` package; now imported from `@oh-my-pi/pi-utils` instead of local utils
- Consolidated prompt template handling: `TemplateContext` type now available as `prompt.TemplateContext` from `@oh-my-pi/pi-utils`
- DAP initialization now advertises support for `runInTerminal` and `startDebugging` reverse requests, and memory references
- Debug tool schema expanded with new parameters for instruction/data breakpoints, memory operations, and custom requests
- DAP session state now tracks instruction and data breakpoints separately from source breakpoints
@@ -78,6 +80,8 @@
### Removed
- Deleted `src/utils/prompt-format.ts` module; prompt formatting logic moved to `pi-utils`
- Deleted `src/utils/frontmatter.ts` module; frontmatter parsing logic moved to `pi-utils`
- Removed `waitForChildProcess` utility (child process termination now handled by native `killTree` from pi-natives)
- `grep-chunk.md` (folded into unified grep template)
- `startMacAppearanceObserver` export (use `MacAppearanceObserver.start()`)
@@ -1,4 +1,5 @@
#!/usr/bin/env bun
import { prompt } from "@oh-my-pi/pi-utils";
/**
* Format prompt files (mixed XML + Markdown + Handlebars).
*
@@ -14,7 +15,6 @@
* 9. Bold RFC 2119 keywords (MUST, SHOULD, MAY, etc.) in prompt content
*/
import { Glob } from "bun";
import { formatPromptContent } from "../src/utils/prompt-format";
const PROMPTS_DIR = new URL("../src/prompts/", import.meta.url).pathname;
const COMMIT_PROMPTS_DIR = new URL("../src/commit/prompts/", import.meta.url).pathname;
@@ -42,7 +42,7 @@ async function main() {
for (const fullPath of files) {
const original = await Bun.file(fullPath).text();
const formatted = formatPromptContent(original, PROMPT_FORMAT_OPTIONS);
const formatted = prompt.format(original, PROMPT_FORMAT_OPTIONS);
if (original !== `${formatted}\n`) {
if (check) {
@@ -1,7 +1,7 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
import { renderPromptTemplate } from "../config/prompt-templates";
import { prompt } from "@oh-my-pi/pi-utils";
import type { ExtensionContext, ExtensionFactory } from "../extensibility/extensions";
import commandResumeTemplate from "./command-resume.md" with { type: "text" };
import { pathMatchesContractPath } from "./contract";
@@ -214,7 +214,7 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
dashboard.updateWidget(ctx, runtime);
await api.setActiveTools([...new Set([...api.getActiveTools(), ...EXPERIMENT_TOOL_NAMES])]);
api.sendUserMessage(
renderPromptTemplate(commandResumeTemplate, {
prompt.render(commandResumeTemplate, {
autoresearch_md_path: autoresearchMdPath,
branch_status_line: branchResult.created
? `Created and checked out dedicated git branch \`${branchResult.branchName}\` before resuming.`
@@ -303,7 +303,7 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
api.sendMessage(
{
customType: "autoresearch-resume",
content: renderPromptTemplate(resumeMessageTemplate, {
content: prompt.render(resumeMessageTemplate, {
autoresearch_md_path: autoresearchMdPath,
has_ideas: fs.existsSync(ideasPath),
has_pending_run: Boolean(pendingRun),
@@ -347,7 +347,7 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
});
const hasAutoresearchMd = fs.existsSync(autoresearchMdPath);
return {
systemPrompt: renderPromptTemplate(promptTemplate, {
systemPrompt: prompt.render(promptTemplate, {
base_system_prompt: event.systemPrompt,
has_goal: goal.trim().length > 0,
goal,
@@ -1,10 +1,10 @@
import { INTENT_FIELD, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { Markdown } from "@oh-my-pi/pi-tui";
import { prompt } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import typesDescriptionPrompt from "../../commit/prompts/types-description.md" with { type: "text" };
import type { ModelRegistry } from "../../config/model-registry";
import { renderPromptTemplate } from "../../config/prompt-templates";
import type { Settings } from "../../config/settings";
import { getMarkdownTheme } from "../../modes/theme/theme";
import { createAgentSession } from "../../sdk";
@@ -36,8 +36,8 @@ export interface ExistingChangelogEntries {
}
export async function runCommitAgentSession(input: CommitAgentInput): Promise<CommitAgentState> {
const typesDescription = renderPromptTemplate(typesDescriptionPrompt);
const systemPrompt = renderPromptTemplate(agentSystemPrompt, {
const typesDescription = prompt.render(typesDescriptionPrompt);
const systemPrompt = prompt.render(agentSystemPrompt, {
types_description: typesDescription,
});
const state: CommitAgentState = { diffText: input.diffText };
@@ -149,7 +149,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
});
try {
const prompt = renderPromptTemplate(agentUserPrompt, {
const agentUserMessage = prompt.render(agentUserPrompt, {
user_context: input.userContext,
changelog_targets: input.changelogTargets.length > 0 ? input.changelogTargets.join("\n") : undefined,
existing_changelog_entries: input.existingChangelogEntries,
@@ -158,7 +158,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
let retryCount = 0;
const needsChangelog = input.requireChangelog && input.changelogTargets.length > 0;
await session.prompt(prompt, {
await session.prompt(agentUserMessage, {
attribution: "agent",
expandPromptTemplates: false,
});
@@ -1,6 +1,6 @@
import * as path from "node:path";
import { createInterface } from "node:readline/promises";
import { $env, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
import { $env, getProjectDir, isEnoent, prompt } from "@oh-my-pi/pi-utils";
import { applyChangelogProposals } from "../../commit/changelog";
import { detectChangelogBoundaries } from "../../commit/changelog/detect";
import { parseUnreleasedSection } from "../../commit/changelog/parse";
@@ -8,7 +8,6 @@ import { formatCommitMessage } from "../../commit/message";
import { resolvePrimaryModel, resolveSmolModel } from "../../commit/model-selection";
import type { CommitCommandArgs, ConventionalAnalysis } from "../../commit/types";
import { ModelRegistry } from "../../config/model-registry";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { Settings } from "../../config/settings";
import { discoverAuthStorage, discoverContextFiles } from "../../sdk";
import * as git from "../../utils/git";
@@ -305,8 +304,8 @@ async function confirmSplitCommitPlan(plan: SplitCommitPlan): Promise<boolean> {
}
const rl = createInterface({ input: process.stdin, output: process.stdout });
try {
const prompt = renderPromptTemplate(splitConfirmPrompt, { count: plan.commits.length });
const answer = await rl.question(prompt);
const splitConfirmQuestion = prompt.render(splitConfirmPrompt, { count: plan.commits.length });
const answer = await rl.question(splitConfirmQuestion);
return ["y", "yes"].includes(answer.trim().toLowerCase());
} finally {
rl.close();
@@ -1,9 +1,9 @@
import { prompt } from "@oh-my-pi/pi-utils";
import { Type } from "@sinclair/typebox";
import analyzeFilePrompt from "../../../commit/agentic/prompts/analyze-file.md" with { type: "text" };
import type { CommitAgentState } from "../../../commit/agentic/state";
import type { NumstatEntry } from "../../../commit/types";
import type { ModelRegistry } from "../../../config/model-registry";
import { renderPromptTemplate } from "../../../config/prompt-templates";
import type { Settings } from "../../../config/settings";
import type { CustomTool, CustomToolContext } from "../../../extensibility/custom-tools/types";
import type { AuthStorage } from "../../../session/auth-storage";
@@ -66,7 +66,7 @@ export function createAnalyzeFileTool(options: {
const numstat = options.state.overview?.numstat ?? [];
const tasks = params.files.map((file, index) => {
const relatedFiles = formatRelatedFiles(params.files, file, numstat);
const prompt = renderPromptTemplate(analyzeFilePrompt, {
const assignment = prompt.render(analyzeFilePrompt, {
file,
goal: params.goal,
related_files: relatedFiles,
@@ -74,7 +74,7 @@ export function createAnalyzeFileTool(options: {
return {
id: `AnalyzeFile${index + 1}`,
description: `Analyze ${file}`,
assignment: prompt,
assignment,
};
});
const taskParams: TaskParams = {
@@ -1,11 +1,11 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import { Type } from "@sinclair/typebox";
import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
import analysisUserPrompt from "../../commit/prompts/analysis-user.md" with { type: "text" };
import type { ChangelogCategory, ConventionalAnalysis } from "../../commit/types";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { toReasoningEffort } from "../../thinking";
import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
@@ -76,7 +76,7 @@ export async function generateConventionalAnalysis({
stat,
diff,
}: ConventionalAnalysisInput): Promise<ConventionalAnalysis> {
const prompt = renderPromptTemplate(analysisUserPrompt, {
const userContent = prompt.render(analysisUserPrompt, {
context_files: contextFiles && contextFiles.length > 0 ? contextFiles : undefined,
user_context: userContext,
types_description: typesDescription,
@@ -89,8 +89,8 @@ export async function generateConventionalAnalysis({
const response = await completeSimple(
model,
{
systemPrompt: renderPromptTemplate(analysisSystemPrompt),
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
systemPrompt: prompt.render(analysisSystemPrompt),
messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
tools: [ConventionalAnalysisTool],
},
{ apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
@@ -1,11 +1,11 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import { Type } from "@sinclair/typebox";
import summarySystemPrompt from "../../commit/prompts/summary-system.md" with { type: "text" };
import summaryUserPrompt from "../../commit/prompts/summary-user.md" with { type: "text" };
import type { CommitSummary } from "../../commit/types";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { toReasoningEffort } from "../../thinking";
import { extractTextContent, extractToolCall } from "../utils";
@@ -44,7 +44,7 @@ export async function generateSummary({
userContext,
}: SummaryInput): Promise<CommitSummary> {
const systemPrompt = renderSummaryPrompt({ commitType, scope, maxChars });
const userPrompt = renderPromptTemplate(summaryUserPrompt, {
const userPrompt = prompt.render(summaryUserPrompt, {
user_context: userContext,
details: details.join("\n"),
stat,
@@ -73,7 +73,7 @@ function renderSummaryPrompt({
maxChars: number;
}): string {
const scopePrefix = scope ? `(${scope})` : "";
return renderPromptTemplate(summarySystemPrompt, {
return prompt.render(summarySystemPrompt, {
commit_type: commitType,
scope_prefix: scopePrefix,
chars: String(maxChars),
@@ -1,11 +1,11 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import { type TSchema, Type } from "@sinclair/typebox";
import changelogSystemPrompt from "../../commit/prompts/changelog-system.md" with { type: "text" };
import changelogUserPrompt from "../../commit/prompts/changelog-user.md" with { type: "text" };
import { CHANGELOG_CATEGORIES, type ChangelogCategory, type ChangelogGenerationResult } from "../../commit/types";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { toReasoningEffort } from "../../thinking";
import { extractTextContent, extractToolCall, parseJsonPayload } from "../utils";
@@ -48,7 +48,7 @@ export async function generateChangelogEntries({
stat,
diff,
}: ChangelogPromptInput): Promise<ChangelogGenerationResult> {
const prompt = renderPromptTemplate(changelogUserPrompt, {
const userContent = prompt.render(changelogUserPrompt, {
changelog_path: changelogPath,
is_package_changelog: isPackageChangelog,
existing_entries: existingEntries,
@@ -58,8 +58,8 @@ export async function generateChangelogEntries({
const response = await completeSimple(
model,
{
systemPrompt: renderPromptTemplate(changelogSystemPrompt),
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
systemPrompt: prompt.render(changelogSystemPrompt),
messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
tools: [changelogTool],
},
{ apiKey, maxTokens: 1200, reasoning: toReasoningEffort(thinkingLevel) },
@@ -1,11 +1,11 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
import { completeSimple } from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import fileObserverSystemPrompt from "../../commit/prompts/file-observer-system.md" with { type: "text" };
import fileObserverUserPrompt from "../../commit/prompts/file-observer-user.md" with { type: "text" };
import type { FileDiff, FileObservation } from "../../commit/types";
import { isExcludedFile } from "../../commit/utils/exclusions";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { toReasoningEffort } from "../../thinking";
import { truncateToTokenLimit } from "./utils";
@@ -38,7 +38,7 @@ export async function runMapPhase({
config,
}: MapPhaseInput): Promise<FileObservation[]> {
const filtered = files.filter(file => !isExcludedFile(file.filename));
const systemPrompt = renderPromptTemplate(fileObserverSystemPrompt);
const systemPrompt = prompt.render(fileObserverSystemPrompt);
const maxFileTokens = config?.maxFileTokens ?? MAX_FILE_TOKENS;
const maxConcurrency = config?.maxConcurrency ?? MAX_CONCURRENCY;
const timeoutMs = config?.timeoutMs ?? MAP_PHASE_TIMEOUT_MS;
@@ -56,14 +56,14 @@ export async function runMapPhase({
const contextHeader = generateContextHeader(filtered, file.filename);
const truncated = truncateToTokenLimit(file.content, maxFileTokens);
const prompt = renderPromptTemplate(fileObserverUserPrompt, {
const userContent = prompt.render(fileObserverUserPrompt, {
filename: file.filename,
diff: truncated,
context_header: contextHeader,
});
const request = {
systemPrompt,
messages: [{ role: "user", content: prompt, timestamp: Date.now() }] as Message[],
messages: [{ role: "user", content: userContent, timestamp: Date.now() }] as Message[],
};
const response = await withRetry(
@@ -1,11 +1,11 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import { Type } from "@sinclair/typebox";
import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { type: "text" };
import reduceUserPrompt from "../../commit/prompts/reduce-user.md" with { type: "text" };
import type { ChangelogCategory, ConventionalAnalysis, FileObservation } from "../../commit/types";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { toReasoningEffort } from "../../thinking";
import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
@@ -67,7 +67,7 @@ export async function runReducePhase({
scopeCandidates,
typesDescription,
}: ReducePhaseInput): Promise<ConventionalAnalysis> {
const prompt = renderPromptTemplate(reduceUserPrompt, {
const userContent = prompt.render(reduceUserPrompt, {
types_description: typesDescription,
observations: observations.flatMap(obs => obs.observations.map(line => `- ${obs.file}: ${line}`)).join("\n"),
stat,
@@ -76,8 +76,8 @@ export async function runReducePhase({
const response = await completeSimple(
model,
{
systemPrompt: renderPromptTemplate(reduceSystemPrompt),
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
systemPrompt: prompt.render(reduceSystemPrompt),
messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
tools: [ReduceTool],
},
{ apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
+3 -4
View File
@@ -1,9 +1,8 @@
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
import { getProjectDir, logger, prompt } from "@oh-my-pi/pi-utils";
import { ModelRegistry } from "../config/model-registry";
import { renderPromptTemplate } from "../config/prompt-templates";
import { Settings } from "../config/settings";
import { discoverAuthStorage } from "../sdk";
import { loadProjectContextFiles } from "../system-prompt";
@@ -26,7 +25,7 @@ import type { CommitCommandArgs, ConventionalAnalysis } from "./types";
const SUMMARY_MAX_CHARS = 72;
const RECENT_COMMITS_COUNT = 8;
const TYPES_DESCRIPTION = renderPromptTemplate(typesDescriptionPrompt);
const TYPES_DESCRIPTION = prompt.render(typesDescriptionPrompt);
/**
* Execute the omp commit pipeline for staged changes.
@@ -236,7 +235,7 @@ async function generateSummaryWithRetry(input: {
}
function buildRetryContext(base: string | undefined, errors: string[]): string {
return renderPromptTemplate(summaryRetryPrompt, {
return prompt.render(summaryRetryPrompt, {
base_context: base,
errors: errors.join("; "),
});
@@ -1,13 +1,17 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { type ChunkAnchorStyle, formatAnchor } from "@oh-my-pi/pi-natives";
import { getProjectDir, getProjectPromptsDir, getPromptsDir, logger } from "@oh-my-pi/pi-utils";
import Handlebars from "handlebars";
import {
getProjectDir,
getProjectPromptsDir,
getPromptsDir,
logger,
parseFrontmatter,
prompt,
} from "@oh-my-pi/pi-utils";
import { computeLineHash } from "../edit/modes/hashline";
import { jtdToTypeScript } from "../tools/jtd-to-typescript";
import { parseCommandArgs, substituteArgs } from "../utils/command-args";
import { parseFrontmatter } from "../utils/frontmatter";
import { formatPromptContent } from "../utils/prompt-format";
/**
* Represents a prompt template loaded from a markdown file
@@ -19,215 +23,7 @@ export interface PromptTemplate {
source: string; // e.g., "(user)", "(project)", "(project:frontend)"
}
export interface TemplateContext extends Record<string, unknown> {
args?: string[];
ARGUMENTS?: string;
arguments?: string;
}
const handlebars = Handlebars.create();
handlebars.registerHelper("arg", function (this: TemplateContext, index: number | string): string {
const args = this.args ?? [];
const parsedIndex = typeof index === "number" ? index : Number.parseInt(index, 10);
if (!Number.isFinite(parsedIndex)) return "";
const zeroBased = parsedIndex - 1;
if (zeroBased < 0) return "";
return args[zeroBased] ?? "";
});
/**
* {{#list items prefix="- " suffix="" join="\n"}}{{this}}{{/list}}
* Renders an array with customizable prefix, suffix, and join separator.
* Note: Use \n in join for newlines (will be unescaped automatically).
*/
handlebars.registerHelper(
"list",
function (this: unknown, context: unknown[], options: Handlebars.HelperOptions): string {
if (!Array.isArray(context) || context.length === 0) return "";
const prefix = (options.hash.prefix as string) ?? "";
const suffix = (options.hash.suffix as string) ?? "";
const rawSeparator = (options.hash.join as string) ?? "\n";
const separator = rawSeparator.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
return context.map(item => `${prefix}${options.fn(item)}${suffix}`).join(separator);
},
);
/**
* {{join array ", "}}
* Joins an array with a separator (default: ", ").
*/
handlebars.registerHelper("join", (context: unknown[], separator?: unknown): string => {
if (!Array.isArray(context)) return "";
const sep = typeof separator === "string" ? separator : ", ";
return context.join(sep);
});
/**
* {{default value "fallback"}}
* Returns the value if truthy, otherwise returns the fallback.
*/
handlebars.registerHelper("default", (value: unknown, defaultValue: unknown): unknown => value || defaultValue);
/**
* {{pluralize count "item" "items"}}
* Returns "1 item" or "5 items" based on count.
*/
handlebars.registerHelper(
"pluralize",
(count: number, singular: string, plural: string): string => `${count} ${count === 1 ? singular : plural}`,
);
/**
* {{#when value "==" compare}}...{{else}}...{{/when}}
* Conditional block with comparison operators: ==, ===, !=, !==, >, <, >=, <=
*/
handlebars.registerHelper(
"when",
function (this: unknown, lhs: unknown, operator: string, rhs: unknown, options: Handlebars.HelperOptions): string {
const ops: Record<string, (a: unknown, b: unknown) => boolean> = {
"==": (a, b) => a === b,
"===": (a, b) => a === b,
"!=": (a, b) => a !== b,
"!==": (a, b) => a !== b,
">": (a, b) => (a as number) > (b as number),
"<": (a, b) => (a as number) < (b as number),
">=": (a, b) => (a as number) >= (b as number),
"<=": (a, b) => (a as number) <= (b as number),
};
const fn = ops[operator];
if (!fn) return options.inverse(this);
return fn(lhs, rhs) ? options.fn(this) : options.inverse(this);
},
);
/**
* {{#ifAny a b c}}...{{else}}...{{/ifAny}}
* True if any argument is truthy.
*/
handlebars.registerHelper("ifAny", function (this: unknown, ...args: unknown[]): string {
const options = args.pop() as Handlebars.HelperOptions;
return args.some(Boolean) ? options.fn(this) : options.inverse(this);
});
/**
* {{#ifAll a b c}}...{{else}}...{{/ifAll}}
* True if all arguments are truthy.
*/
handlebars.registerHelper("ifAll", function (this: unknown, ...args: unknown[]): string {
const options = args.pop() as Handlebars.HelperOptions;
return args.every(Boolean) ? options.fn(this) : options.inverse(this);
});
/**
* {{#table rows headers="Col1|Col2"}}{{col1}}|{{col2}}{{/table}}
* Generates a markdown table from an array of objects.
*/
handlebars.registerHelper(
"table",
function (this: unknown, context: unknown[], options: Handlebars.HelperOptions): string {
if (!Array.isArray(context) || context.length === 0) return "";
const headersStr = options.hash.headers as string | undefined;
const headers = headersStr?.split("|") ?? [];
const separator = headers.map(() => "---").join(" | ");
const headerRow = headers.length > 0 ? `| ${headers.join(" | ")} |\n| ${separator} |\n` : "";
const rows = context.map(item => `| ${options.fn(item).trim()} |`).join("\n");
return headerRow + rows;
},
);
/**
* {{#codeblock lang="diff"}}...{{/codeblock}}
* Wraps content in a fenced code block.
*/
handlebars.registerHelper("codeblock", function (this: unknown, options: Handlebars.HelperOptions): string {
const lang = (options.hash.lang as string) ?? "";
const content = options.fn(this).trim();
return `\`\`\`${lang}\n${content}\n\`\`\``;
});
/**
* {{#xml "tag"}}content{{/xml}}
* Wraps content in XML-style tags. Returns empty string if content is empty.
*/
handlebars.registerHelper("xml", function (this: unknown, tag: string, options: Handlebars.HelperOptions): string {
const content = options.fn(this).trim();
if (!content) return "";
return `<${tag}>\n${content}\n</${tag}>`;
});
/**
* {{escapeXml value}}
* Escapes XML special characters: & < > "
*/
handlebars.registerHelper("escapeXml", (value: unknown): string => {
if (value == null) return "";
return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
});
/**
* {{len array}}
* Returns the length of an array or string.
*/
handlebars.registerHelper("len", (value: unknown): number => {
if (Array.isArray(value)) return value.length;
if (typeof value === "string") return value.length;
return 0;
});
/**
* {{add a b}}
* Adds two numbers.
*/
handlebars.registerHelper("add", (a: number, b: number): number => (a ?? 0) + (b ?? 0));
/**
* {{sub a b}}
* Subtracts b from a.
*/
handlebars.registerHelper("sub", (a: number, b: number): number => (a ?? 0) - (b ?? 0));
/**
* {{#has collection item}}...{{else}}...{{/has}}
* Checks if an array includes an item or if a Set/Map has a key.
*/
handlebars.registerHelper(
"has",
function (this: unknown, collection: unknown, item: unknown, options: Handlebars.HelperOptions): string {
let found = false;
if (Array.isArray(collection)) {
found = collection.includes(item);
} else if (collection instanceof Set) {
found = collection.has(item);
} else if (collection instanceof Map) {
found = collection.has(item);
} else if (collection && typeof collection === "object") {
if (typeof item === "string" || typeof item === "number" || typeof item === "symbol") {
found = item in collection;
}
}
return found ? options.fn(this) : options.inverse(this);
},
);
/**
* {{includes array item}}
* Returns true if array includes item. For use in other helpers.
*/
handlebars.registerHelper("includes", (collection: unknown, item: unknown): boolean => {
if (Array.isArray(collection)) return collection.includes(item);
if (collection instanceof Set) return collection.has(item);
if (collection instanceof Map) return collection.has(item);
return false;
});
/**
* {{not value}}
* Returns logical NOT of value. For use in subexpressions.
*/
handlebars.registerHelper("not", (value: unknown): boolean => !value);
handlebars.registerHelper("jtdToTypeScript", (schema: unknown): string => {
prompt.registerHelper("jtdToTypeScript", (schema: unknown): string => {
try {
return jtdToTypeScript(schema);
} catch {
@@ -235,7 +31,7 @@ handlebars.registerHelper("jtdToTypeScript", (schema: unknown): string => {
}
});
handlebars.registerHelper("jsonStringify", (value: unknown): string => JSON.stringify(value));
prompt.registerHelper("jsonStringify", (value: unknown): string => JSON.stringify(value));
/**
* Renders a section separator:
@@ -248,7 +44,7 @@ export function sectionSeparator(name: string): string {
return `\n\n═══════════${name}═══════════\n`;
}
handlebars.registerHelper("SECTION_SEPERATOR", (name: unknown): string => sectionSeparator(String(name)));
prompt.registerHelper("SECTION_SEPERATOR", (name: unknown): string => sectionSeparator(String(name)));
function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; text: string; ref: string } {
const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
@@ -262,7 +58,7 @@ function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; t
* {{href lineNum "content"}} — compute a real hashline ref for prompt examples.
* Returns `"lineNum#hash"` using the actual hash algorithm.
*/
handlebars.registerHelper("href", (lineNum: unknown, content: unknown): string => {
prompt.registerHelper("href", (lineNum: unknown, content: unknown): string => {
const { ref } = formatHashlineRef(lineNum, content);
return JSON.stringify(ref);
});
@@ -271,7 +67,7 @@ handlebars.registerHelper("href", (lineNum: unknown, content: unknown): string =
* {{hline lineNum "content"}} — format a full read-style line with prefix.
* Returns `"lineNum#hash:content"`.
*/
handlebars.registerHelper("hline", (lineNum: unknown, content: unknown): string => {
prompt.registerHelper("hline", (lineNum: unknown, content: unknown): string => {
const { ref, text } = formatHashlineRef(lineNum, content);
return `${ref}:${text}`;
});
@@ -280,7 +76,7 @@ handlebars.registerHelper("hline", (lineNum: unknown, content: unknown): string
* {{anchor name checksum}} — render a branch anchor tag using the current anchor style.
* Style is resolved from the template context (`anchorStyle`) or defaults to "full".
*/
handlebars.registerHelper("anchor", function (this: TemplateContext, name: string, checksum: string): string {
prompt.registerHelper("anchor", function (this: prompt.TemplateContext, name: string, checksum: string): string {
const style = (this.anchorStyle as ChunkAnchorStyle) ?? "full";
return formatAnchor(name, checksum, style);
});
@@ -291,7 +87,7 @@ handlebars.registerHelper("anchor", function (this: TemplateContext, name: strin
* In `kind` style each segment is trimmed to its kind prefix (`class.fn`).
* In `bare` style the path is omitted (the model uses only `crc` to identify chunks).
*/
handlebars.registerHelper("sel", function (this: TemplateContext, chunkPath: string): string {
prompt.registerHelper("sel", function (this: prompt.TemplateContext, chunkPath: string): string {
const style = (this.anchorStyle as ChunkAnchorStyle) ?? "full";
if (style === "full") return chunkPath;
if (style === "bare") return "";
@@ -327,12 +123,6 @@ export function appendInlineArgsFallback(
return `${rendered}\n\n${argsText}`;
}
export function renderPromptTemplate(template: string, context: TemplateContext = {}): string {
const compiled = handlebars.compile(template, { noEscape: true, strict: false });
const rendered = compiled(context ?? {});
return formatPromptContent(rendered, { renderPhase: "post-render" });
}
/**
* Recursively scan a directory for .md files (and symlinks to .md files) and load them as prompt templates
*/
@@ -459,7 +249,7 @@ export function expandPromptTemplate(text: string, templates: PromptTemplate[]):
const argsText = args.join(" ");
const usesInlineArgPlaceholders = templateUsesInlineArgPlaceholders(template.content);
const substituted = substituteArgs(template.content, args);
const rendered = renderPromptTemplate(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
const rendered = prompt.render(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
return appendInlineArgsFallback(rendered, argsText, usesInlineArgPlaceholders);
}
@@ -4,7 +4,7 @@
* Primary provider for OMP native configs. Supports all capabilities.
*/
import * as path from "node:path";
import { logger, tryParseJson } from "@oh-my-pi/pi-utils";
import { logger, parseFrontmatter, tryParseJson } from "@oh-my-pi/pi-utils";
import { registerProvider } from "../capability";
import { type ContextFile, contextFileCapability } from "../capability/context-file";
import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
@@ -22,7 +22,6 @@ import { type SystemPrompt, systemPromptCapability } from "../capability/system-
import { type CustomTool, toolCapability } from "../capability/tool";
import type { LoadContext, LoadResult } from "../capability/types";
import { expandTilde } from "../tools/path-utils";
import { parseFrontmatter } from "../utils/frontmatter";
import {
buildRuleFromMarkdown,
createSourceMeta,
+2 -2
View File
@@ -7,7 +7,7 @@
* User directory: ~/.codex
*/
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { logger, parseFrontmatter } from "@oh-my-pi/pi-utils";
import { registerProvider } from "../capability";
import type { ContextFile } from "../capability/context-file";
import { contextFileCapability } from "../capability/context-file";
@@ -28,7 +28,7 @@ import { slashCommandCapability } from "../capability/slash-command";
import type { CustomTool } from "../capability/tool";
import { toolCapability } from "../capability/tool";
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
import { parseFrontmatter } from "../utils/frontmatter";
import {
createSourceMeta,
discoverExtensionModulePaths,
@@ -12,12 +12,13 @@
* - instructions: *.instructions.md in .github/instructions/ with applyTo frontmatter
*/
import * as path from "node:path";
import { parseFrontmatter } from "@oh-my-pi/pi-utils";
import { registerProvider } from "../capability";
import { type ContextFile, contextFileCapability } from "../capability/context-file";
import { readFile } from "../capability/fs";
import { type Instruction, instructionCapability } from "../capability/instruction";
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
import { parseFrontmatter } from "../utils/frontmatter";
import { calculateDepth, createSourceMeta, getProjectPath, loadFilesFromDir } from "./helpers";
const PROVIDER_ID = "github";
@@ -3,13 +3,13 @@ import * as os from "node:os";
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { FileType, glob } from "@oh-my-pi/pi-natives";
import { CONFIG_DIR_NAME, getConfigDirName, getProjectDir, tryParseJson } from "@oh-my-pi/pi-utils";
import { CONFIG_DIR_NAME, getConfigDirName, getProjectDir, parseFrontmatter, tryParseJson } from "@oh-my-pi/pi-utils";
import { readDirEntries, readFile } from "../capability/fs";
import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
import type { Skill, SkillFrontmatter } from "../capability/skill";
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
import { parseThinkingLevel } from "../thinking";
import { parseFrontmatter } from "../utils/frontmatter";
import { buildPluginDirRoot } from "./plugin-dir-roots";
/**
@@ -16,7 +16,7 @@
* Priority: 55 (tool-specific provider)
*/
import * as path from "node:path";
import { logger, tryParseJson } from "@oh-my-pi/pi-utils";
import { logger, parseFrontmatter, tryParseJson } from "@oh-my-pi/pi-utils";
import { registerProvider } from "../capability";
import { type ContextFile, contextFileCapability } from "../capability/context-file";
import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
@@ -26,7 +26,7 @@ import { type Settings, settingsCapability } from "../capability/settings";
import { type Skill, skillCapability } from "../capability/skill";
import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
import { parseFrontmatter } from "../utils/frontmatter";
import {
createSourceMeta,
discoverExtensionModulePaths,
+5 -5
View File
@@ -1,5 +1,5 @@
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { renderPromptTemplate } from "../config/prompt-templates";
import { prompt } from "@oh-my-pi/pi-utils";
import {
createLspWritethrough,
type FileDiagnosticsResult,
@@ -194,7 +194,7 @@ export class EditTool implements AgentTool<TInput> {
return {
chunk: {
description: (session: ToolSession) =>
renderPromptTemplate(chunkEditDescription, {
prompt.render(chunkEditDescription, {
anchorStyle: resolveAnchorStyle(session.settings),
}),
parameters: chunkEditParamsSchema,
@@ -212,7 +212,7 @@ export class EditTool implements AgentTool<TInput> {
},
},
patch: {
description: () => renderPromptTemplate(patchDescription),
description: () => prompt.render(patchDescription),
parameters: patchEditSchema,
invalidParamsMessage: "Invalid edit parameters for patch mode.",
validate: isPatchParams,
@@ -230,7 +230,7 @@ export class EditTool implements AgentTool<TInput> {
},
},
hashline: {
description: () => renderPromptTemplate(hashlineDescription),
description: () => prompt.render(hashlineDescription),
parameters: hashlineEditParamsSchema,
invalidParamsMessage: "Invalid edit parameters for hashline mode.",
validate: isHashlineParams,
@@ -246,7 +246,7 @@ export class EditTool implements AgentTool<TInput> {
},
},
replace: {
description: () => renderPromptTemplate(replaceDescription),
description: () => prompt.render(replaceDescription),
parameters: replaceEditSchema,
invalidParamsMessage: "Invalid edit parameters for replace mode.",
validate: isReplaceParams,
@@ -1,4 +1,4 @@
import { renderPromptTemplate } from "../../../../config/prompt-templates";
import { prompt } from "@oh-my-pi/pi-utils";
import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
import type { HookCommandContext } from "../../../../extensibility/hooks/types";
import ciGreenRequestTemplate from "../../../../prompts/ci-green-request.md" with { type: "text" };
@@ -20,6 +20,6 @@ export class GreenCommand implements CustomCommand {
async execute(_args: string[], _ctx: HookCommandContext): Promise<string> {
const headTag = await getHeadTag(this.api);
return renderPromptTemplate(ciGreenRequestTemplate, { headTag });
return prompt.render(ciGreenRequestTemplate, { headTag });
}
}
@@ -11,7 +11,7 @@
* rich context for the orchestrating agent to distribute work across
* multiple reviewer agents based on diff weight and locality.
*/
import { renderPromptTemplate } from "../../../../config/prompt-templates";
import { prompt } from "@oh-my-pi/pi-utils";
import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
import type { HookCommandContext } from "../../../../extensibility/hooks/types";
import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
@@ -209,7 +209,7 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): str
hunksPreview: skipDiff ? getDiffPreview(f.hunks, linesPerFile) : "",
}));
return renderPromptTemplate(reviewRequestTemplate, {
return prompt.render(reviewRequestTemplate, {
mode,
files: filesWithExt,
excluded: stats.excluded,
@@ -1,10 +1,7 @@
import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
import { slashCommandCapability } from "../capability/slash-command";
import {
appendInlineArgsFallback,
renderPromptTemplate,
templateUsesInlineArgPlaceholders,
} from "../config/prompt-templates";
import { appendInlineArgsFallback, templateUsesInlineArgPlaceholders } from "../config/prompt-templates";
import type { SlashCommand } from "../discovery";
import { loadCapability } from "../discovery";
import {
@@ -14,7 +11,6 @@ import {
} from "../slash-commands/builtin-registry";
import { EMBEDDED_COMMAND_TEMPLATES } from "../task/commands";
import { parseCommandArgs, substituteArgs } from "../utils/command-args";
import { parseFrontmatter } from "../utils/frontmatter";
export type SlashCommandSource = "extension" | "prompt" | "skill";
@@ -223,7 +219,7 @@ export function expandSlashCommand(text: string, fileCommands: FileSlashCommand[
const argsText = args.join(" ");
const usesInlineArgPlaceholders = templateUsesInlineArgPlaceholders(fileCommand.content);
const substituted = substituteArgs(fileCommand.content, args);
const rendered = renderPromptTemplate(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
const rendered = prompt.render(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
return appendInlineArgsFallback(rendered, argsText, usesInlineArgPlaceholders);
}
+2 -3
View File
@@ -1,9 +1,8 @@
import * as fs from "node:fs";
import path from "node:path";
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { logger, once, untilAborted } from "@oh-my-pi/pi-utils";
import { logger, once, prompt, untilAborted } from "@oh-my-pi/pi-utils";
import type { BunFile } from "bun";
import { renderPromptTemplate } from "../config/prompt-templates";
import { type Theme, theme } from "../modes/theme/theme";
import lspDescription from "../prompts/tools/lsp.md" with { type: "text" };
import type { ToolSession } from "../tools";
@@ -1088,7 +1087,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
readonly inline = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(lspDescription);
this.description = prompt.render(lspDescription);
}
static createIf(session: ToolSession): LspTool | null {
+4 -5
View File
@@ -4,10 +4,9 @@ import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import { completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
import { getAgentDbPath, getMemoriesDir, logger, parseJsonlLenient } from "@oh-my-pi/pi-utils";
import { getAgentDbPath, getMemoriesDir, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
import type { ModelRegistry } from "../config/model-registry";
import { parseModelString } from "../config/model-resolver";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { Settings } from "../config/settings";
import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
@@ -166,7 +165,7 @@ export async function buildMemoryToolDeveloperInstructions(
const truncated = truncateByApproxTokens(summary, cfg.summaryInjectionTokenLimit);
if (!truncated.trim()) return undefined;
return renderPromptTemplate(readPathTemplate, {
return prompt.render(readPathTemplate, {
memory_summary: truncated,
});
}
@@ -585,7 +584,7 @@ async function runStage1Job(options: {
const serializedItems = JSON.stringify(persisted);
const budgetTokens = Math.floor(modelMaxTokens * config.rolloutPayloadPercent);
const truncatedItems = truncateByApproxTokens(serializedItems, budgetTokens);
const inputPrompt = renderPromptTemplate(stageOneInputTemplate, {
const inputPrompt = prompt.render(stageOneInputTemplate, {
thread_id: claim.threadId,
response_items_json: truncatedItems,
});
@@ -716,7 +715,7 @@ async function runConsolidationModel(options: { memoryRoot: string; model: Model
const { memoryRoot, model, apiKey } = options;
const rawMemories = await Bun.file(path.join(memoryRoot, "raw_memories.md")).text();
const rolloutSummaries = await readRolloutSummaries(memoryRoot);
const input = renderPromptTemplate(consolidationTemplate, {
const input = prompt.render(consolidationTemplate, {
raw_memories: truncateByApproxTokens(rawMemories, 20_000),
rollout_summaries: truncateByApproxTokens(rolloutSummaries, 12_000),
});
@@ -31,7 +31,7 @@ import {
visibleWidth,
wrapTextWithAnsi,
} from "@oh-my-pi/pi-tui";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { isEnoent, prompt } from "@oh-my-pi/pi-utils";
import { YAML } from "bun";
import { getConfigDirs } from "../../config";
import type { ModelRegistry } from "../../config/model-registry";
@@ -41,7 +41,6 @@ import {
resolveConfiguredModelPatterns,
resolveModelOverride,
} from "../../config/model-resolver";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { Settings } from "../../config/settings";
import agentCreationArchitectPrompt from "../../prompts/system/agent-creation-architect.md" with { type: "text" };
import agentCreationUserPrompt from "../../prompts/system/agent-creation-user.md" with { type: "text" };
@@ -627,8 +626,8 @@ export class AgentDashboard extends Container {
throw new Error("No available model to generate agent specification.");
}
const systemPrompt = renderPromptTemplate(agentCreationArchitectPrompt, { TASK_TOOL_NAME: "task" });
const userPrompt = renderPromptTemplate(agentCreationUserPrompt, { request: description });
const systemPrompt = prompt.render(agentCreationArchitectPrompt, { TASK_TOOL_NAME: "task" });
const userPrompt = prompt.render(agentCreationUserPrompt, { request: description });
const { session } = await createAgentSession({
cwd: this.cwd,
@@ -1,6 +1,6 @@
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import { type AssistantMessage, type Context, streamSimple } from "@oh-my-pi/pi-ai";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { prompt } from "@oh-my-pi/pi-utils";
import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
import { toReasoningEffort } from "../../thinking";
import { BtwPanelComponent } from "../components/btw-panel";
@@ -137,7 +137,7 @@ export class BtwController {
content: [
{
type: "text",
text: renderPromptTemplate(btwUserPrompt, { question }),
text: prompt.render(btwUserPrompt, { question }),
},
],
attribution: "user",
@@ -15,10 +15,9 @@ import {
} from "@oh-my-pi/pi-ai";
import type { Component, SlashCommand } from "@oh-my-pi/pi-tui";
import { Container, Loader, Markdown, ProcessTerminal, Spacer, Text, TUI, visibleWidth } from "@oh-my-pi/pi-tui";
import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { KeybindingsManager } from "../config/keybindings";
import { renderPromptTemplate } from "../config/prompt-templates";
import { type Settings, settings } from "../config/settings";
import type {
ExtensionUIContext,
@@ -899,11 +898,11 @@ export class InteractiveMode implements InteractiveModeContext {
}
this.session.setPlanReferencePath(options.finalPlanFilePath);
this.session.markPlanReferenceSent();
const prompt = renderPromptTemplate(planModeApprovedPrompt, {
const planModePrompt = prompt.render(planModeApprovedPrompt, {
planContent,
finalPlanFilePath: options.finalPlanFilePath,
});
await this.session.prompt(prompt, { synthetic: true });
await this.session.prompt(planModePrompt, { synthetic: true });
}
async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
+3 -6
View File
@@ -19,6 +19,7 @@ import {
getSearchDbDir,
logger,
postmortem,
prompt,
} from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { AsyncJobManager } from "./async";
@@ -27,11 +28,7 @@ import { loadCapability } from "./capability";
import { type Rule, ruleCapability } from "./capability/rule";
import { ModelRegistry } from "./config/model-registry";
import { formatModelString, parseModelPattern, parseModelString, resolveModelRoleValue } from "./config/model-resolver";
import {
loadPromptTemplates as loadPromptTemplatesInternal,
type PromptTemplate,
renderPromptTemplate,
} from "./config/prompt-templates";
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates";
import { Settings, type SkillsSettings } from "./config/settings";
import { CursorExecHandlers } from "./cursor";
import "./discovery";
@@ -872,7 +869,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
onJobComplete: async (jobId, result, job) => {
if (!session) return;
const formattedResult = await formatAsyncResultForFollowUp(result);
const message = renderPromptTemplate(asyncResultTemplate, { jobId, result: formattedResult });
const message = prompt.render(asyncResultTemplate, { jobId, result: formattedResult });
const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
await session.sendCustomMessage(
{
@@ -50,7 +50,7 @@ import {
parseRateLimitReason,
} from "@oh-my-pi/pi-ai";
import { killTree, MacOSPowerAssertion, type SearchDb } from "@oh-my-pi/pi-natives";
import { abortableSleep, getAgentDbPath, isEnoent, logger, setNativeKillTree } from "@oh-my-pi/pi-utils";
import { abortableSleep, getAgentDbPath, isEnoent, logger, prompt, setNativeKillTree } from "@oh-my-pi/pi-utils";
import type { AsyncJob, AsyncJobManager } from "../async";
import type { Rule } from "../capability/rule";
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
@@ -61,7 +61,7 @@ import {
type ResolvedModelRoleValue,
resolveModelRoleValue,
} from "../config/model-resolver";
import { expandPromptTemplate, type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
import type { Settings, SkillsSettings } from "../config/settings";
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
@@ -317,7 +317,7 @@ interface HandoffOptions {
/** Standard thinking levels */
const AUTO_HANDOFF_THRESHOLD_FOCUS = renderPromptTemplate(autoHandoffThresholdFocusPrompt);
const AUTO_HANDOFF_THRESHOLD_FOCUS = prompt.render(autoHandoffThresholdFocusPrompt);
type RetryFallbackChains = Record<string, string[]>;
@@ -1139,7 +1139,7 @@ export class AgentSession {
if (this.#pendingTtsrInjections.length === 0) return undefined;
const rules = this.#pendingTtsrInjections;
const content = rules
.map(r => renderPromptTemplate(ttsrInterruptTemplate, { name: r.name, path: r.path, content: r.content }))
.map(r => prompt.render(ttsrInterruptTemplate, { name: r.name, path: r.path, content: r.content }))
.join("\n\n");
this.#pendingTtsrInjections = [];
return { content, rules };
@@ -2210,7 +2210,7 @@ export class AgentSession {
throw error;
}
const content = renderPromptTemplate(planModeReferencePrompt, {
const content = prompt.render(planModeReferencePrompt, {
planFilePath,
planContent,
});
@@ -2247,7 +2247,7 @@ export class AgentSession {
: sessionPlanUrl;
const planExists = fs.existsSync(resolvedPlanPath);
const content = renderPromptTemplate(planModeActivePrompt, {
const content = prompt.render(planModeActivePrompt, {
planFilePath: displayPlanPath,
planExists,
askToolName: "ask",
@@ -3773,7 +3773,7 @@ export class AgentSession {
}
// Build the handoff prompt
const handoffPrompt = renderPromptTemplate(handoffDocumentPrompt, {
const handoffPrompt = prompt.render(handoffDocumentPrompt, {
additionalFocus: customInstructions,
});
@@ -4041,7 +4041,7 @@ export class AgentSession {
return;
}
const reminder = renderPromptTemplate(planModeToolDecisionReminderPrompt, {
const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
askToolName: "ask",
exitToolName: "exit_plan_mode",
});
@@ -4088,7 +4088,7 @@ export class AgentSession {
return undefined;
}
const eagerTodoReminder = renderPromptTemplate(eagerTodoPrompt);
const eagerTodoReminder = prompt.render(eagerTodoPrompt);
return {
message: {
@@ -7,7 +7,7 @@
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { Model } from "@oh-my-pi/pi-ai";
import { completeSimple } from "@oh-my-pi/pi-ai";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { prompt } from "@oh-my-pi/pi-utils";
import branchSummaryPrompt from "../../prompts/compaction/branch-summary.md" with { type: "text" };
import branchSummaryPreamble from "../../prompts/compaction/branch-summary-preamble.md" with { type: "text" };
import {
@@ -244,9 +244,9 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
// Summary Generation
// ============================================================================
const BRANCH_SUMMARY_PREAMBLE = renderPromptTemplate(branchSummaryPreamble);
const BRANCH_SUMMARY_PREAMBLE = prompt.render(branchSummaryPreamble);
const BRANCH_SUMMARY_PROMPT = renderPromptTemplate(branchSummaryPrompt);
const BRANCH_SUMMARY_PROMPT = prompt.render(branchSummaryPrompt);
/**
* Generate a summary of abandoned branch entries.
@@ -26,8 +26,7 @@ import {
getOpenAIResponsesHistoryPayload,
normalizeResponsesToolCallId,
} from "@oh-my-pi/pi-ai/utils";
import { logger } from "@oh-my-pi/pi-utils";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { logger, prompt } from "@oh-my-pi/pi-utils";
import compactionShortSummaryPrompt from "../../prompts/compaction/compaction-short-summary.md" with { type: "text" };
import compactionSummaryPrompt from "../../prompts/compaction/compaction-summary.md" with { type: "text" };
import compactionTurnPrefixPrompt from "../../prompts/compaction/compaction-turn-prefix.md" with { type: "text" };
@@ -474,11 +473,11 @@ export function findCutPoint(
// Summarization
// ============================================================================
const SUMMARIZATION_PROMPT = renderPromptTemplate(compactionSummaryPrompt);
const SUMMARIZATION_PROMPT = prompt.render(compactionSummaryPrompt);
const UPDATE_SUMMARIZATION_PROMPT = renderPromptTemplate(compactionUpdateSummaryPrompt);
const UPDATE_SUMMARIZATION_PROMPT = prompt.render(compactionUpdateSummaryPrompt);
const SHORT_SUMMARY_PROMPT = renderPromptTemplate(compactionShortSummaryPrompt);
const SHORT_SUMMARY_PROMPT = prompt.render(compactionShortSummaryPrompt);
function formatAdditionalContext(context: string[] | undefined): string {
if (!context || context.length === 0) return "";
@@ -1201,7 +1200,7 @@ export function prepareCompaction(
// Main compaction function
// ============================================================================
const TURN_PREFIX_SUMMARIZATION_PROMPT = renderPromptTemplate(compactionTurnPrefixPrompt);
const TURN_PREFIX_SUMMARIZATION_PROMPT = prompt.render(compactionTurnPrefixPrompt);
/**
* Generate summaries for compaction using prepared data.
@@ -3,7 +3,7 @@
*/
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { Message } from "@oh-my-pi/pi-ai";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { prompt } from "@oh-my-pi/pi-utils";
import fileOperationsTemplate from "../../prompts/system/file-operations.md" with { type: "text" };
import summarizationSystemPrompt from "../../prompts/system/summarization-system.md" with { type: "text" };
@@ -86,7 +86,7 @@ function stripFileOperationTags(summary: string): string {
}
export function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {
if (readFiles.length === 0 && modifiedFiles.length === 0) return "";
return renderPromptTemplate(fileOperationsTemplate, {
return prompt.render(fileOperationsTemplate, {
readFiles: truncateFileList(readFiles),
modifiedFiles: truncateFileList(modifiedFiles),
});
@@ -181,4 +181,4 @@ export function serializeConversation(messages: Message[]): string {
// Summarization System Prompt
// ============================================================================
export const SUMMARIZATION_SYSTEM_PROMPT = renderPromptTemplate(summarizationSystemPrompt);
export const SUMMARIZATION_SYSTEM_PROMPT = prompt.render(summarizationSystemPrompt);
@@ -14,7 +14,7 @@ import type {
TextContent,
ToolResultMessage,
} from "@oh-my-pi/pi-ai";
import { renderPromptTemplate } from "../config/prompt-templates";
import { prompt } from "@oh-my-pi/pi-utils";
import branchSummaryContextPrompt from "../prompts/compaction/branch-summary-context.md" with { type: "text" };
import compactionSummaryContextPrompt from "../prompts/compaction/compaction-summary-context.md" with { type: "text" };
import type { OutputMeta } from "../tools/output-meta";
@@ -310,7 +310,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
content: [
{
type: "text" as const,
text: renderPromptTemplate(BRANCH_SUMMARY_TEMPLATE, { summary: m.summary }),
text: prompt.render(BRANCH_SUMMARY_TEMPLATE, { summary: m.summary }),
},
],
attribution: "agent",
@@ -322,7 +322,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
content: [
{
type: "text" as const,
text: renderPromptTemplate(COMPACTION_SUMMARY_TEMPLATE, { summary: m.summary }),
text: prompt.render(COMPACTION_SUMMARY_TEMPLATE, { summary: m.summary }),
},
],
attribution: "agent",
+3 -5
View File
@@ -6,17 +6,15 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils";
import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
import { $ } from "bun";
import { contextFileCapability } from "./capability/context-file";
import { systemPromptCapability } from "./capability/system-prompt";
import { renderPromptTemplate } from "./config/prompt-templates";
import type { SkillsSettings } from "./config/settings";
import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
import { loadSkills, type Skill } from "./extensibility/skills";
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
import { formatPromptContent } from "./utils/prompt-format";
interface AlwaysApplyRule {
name: string;
@@ -25,7 +23,7 @@ interface AlwaysApplyRule {
}
function normalizePromptBlock(content: string): string {
return formatPromptContent(content, { renderPhase: "post-render" }).trim();
return prompt.format(content, { renderPhase: "post-render" }).trim();
}
function splitComparablePromptBlocks(content: string | null | undefined): string[] {
@@ -608,5 +606,5 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
eagerTasks,
secretsEnabled,
};
return renderPromptTemplate(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
return prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
}
+4 -4
View File
@@ -4,7 +4,7 @@
* Agents are embedded at build time via Bun's import with { type: "text" }.
*/
import { Effort } from "@oh-my-pi/pi-ai";
import { renderPromptTemplate } from "../config/prompt-templates";
import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
import { parseAgentFields } from "../discovery/helpers";
import designerMd from "../prompts/agents/designer.md" with { type: "text" };
import exploreMd from "../prompts/agents/explore.md" with { type: "text" };
@@ -15,7 +15,7 @@ import oracleMd from "../prompts/agents/oracle.md" with { type: "text" };
import planMd from "../prompts/agents/plan.md" with { type: "text" };
import reviewerMd from "../prompts/agents/reviewer.md" with { type: "text" };
import taskMd from "../prompts/agents/task.md" with { type: "text" };
import { parseFrontmatter } from "../utils/frontmatter";
import type { AgentDefinition, AgentSource } from "./types";
interface AgentFrontmatter {
@@ -35,9 +35,9 @@ interface EmbeddedAgentDef {
}
function buildAgentContent(def: EmbeddedAgentDef): string {
const body = renderPromptTemplate(def.template);
const body = prompt.render(def.template);
if (!def.frontmatter) return body;
return renderPromptTemplate(agentFrontmatterTemplate, { ...def.frontmatter, body });
return prompt.render(agentFrontmatterTemplate, { ...def.frontmatter, body });
}
const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
+2 -5
View File
@@ -4,16 +4,13 @@
* Commands are embedded at build time via Bun's import with { type: "text" }.
*/
import * as path from "node:path";
import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
import { renderPromptTemplate } from "../config/prompt-templates";
import { loadCapability } from "../discovery";
// Embed command markdown files at build time
import initMd from "../prompts/agents/init.md" with { type: "text" };
import { parseFrontmatter } from "../utils/frontmatter";
const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
{ name: "init.md", content: renderPromptTemplate(initMd) },
];
const EMBEDDED_COMMANDS: { name: string; content: string }[] = [{ name: "init.md", content: prompt.render(initMd) }];
export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
+4 -4
View File
@@ -6,12 +6,12 @@
import path from "node:path";
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { SearchDb } from "@oh-my-pi/pi-natives";
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
import type { TSchema } from "@sinclair/typebox";
import Ajv, { type ValidateFunction } from "ajv";
import { ModelRegistry } from "../config/model-registry";
import { resolveModelOverride } from "../config/model-resolver";
import { type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
import type { PromptTemplate } from "../config/prompt-templates";
import { Settings } from "../config/settings";
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
import type { CustomTool } from "../extensibility/custom-tools/types";
@@ -965,7 +965,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
skills: options.skills,
promptTemplates: options.promptTemplates,
systemPrompt: defaultPrompt =>
renderPromptTemplate(subagentSystemPromptTemplate, {
prompt.render(subagentSystemPromptTemplate, {
base: defaultPrompt,
agent: agent.systemPrompt,
worktree: worktree ?? "",
@@ -1106,7 +1106,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
while (!submitResultCalled && retryCount < MAX_SUBMIT_RESULT_RETRIES && !abortSignal.aborted) {
try {
retryCount++;
const reminder = renderPromptTemplate(submitReminderTemplate, {
const reminder = prompt.render(submitReminderTemplate, {
retryCount,
maxRetries: MAX_SUBMIT_RESULT_RETRIES,
});
+3 -4
View File
@@ -17,10 +17,9 @@ import * as os from "node:os";
import path from "node:path";
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { Usage } from "@oh-my-pi/pi-ai";
import { $env, Snowflake } from "@oh-my-pi/pi-utils";
import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
import type { ToolSession } from "..";
import { resolveAgentModelPatterns } from "../config/model-resolver";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { Theme } from "../modes/theme/theme";
import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
@@ -136,7 +135,7 @@ function renderDescription(
disabledAgents: string[],
): string {
const filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
return renderPromptTemplate(taskDescriptionTemplate, {
return prompt.render(taskDescriptionTemplate, {
agents: filteredAgents,
MAX_CONCURRENCY: maxConcurrency,
isolationEnabled,
@@ -1157,7 +1156,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
const outputIds = results.filter(r => !r.aborted || r.output.trim()).map(r => `agent://${r.id}`);
const backendSummaryPrefix = isolationBackendWarning ? `\n\n${isolationBackendWarning}` : "";
const summary = renderPromptTemplate(taskSummaryTemplate, {
const summary = prompt.render(taskSummaryTemplate, {
successCount,
totalCount: results.length,
cancelledCount,
+2 -2
View File
@@ -1,4 +1,4 @@
import { renderPromptTemplate } from "../config/prompt-templates";
import { prompt } from "@oh-my-pi/pi-utils";
import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
import type { TaskItem } from "./types";
@@ -25,7 +25,7 @@ export function renderTemplate(context: string | undefined, task: TaskItem): Ren
return { task: assignment || context!, assignment: assignment || context!, id, description };
}
return {
task: renderPromptTemplate(subagentUserPromptTemplate, { context, assignment }),
task: prompt.render(subagentUserPromptTemplate, { context, assignment }),
assignment,
id,
description,
+2 -3
View File
@@ -17,9 +17,8 @@
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { type Component, Container, Markdown, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
import { untilAborted } from "@oh-my-pi/pi-utils";
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
import askDescription from "../prompts/tools/ask.md" with { type: "text" };
@@ -385,7 +384,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
readonly strict = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(askDescription);
this.description = prompt.render(askDescription);
}
static createIf(session: ToolSession): AskTool | null {
+2 -3
View File
@@ -3,9 +3,8 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { untilAborted } from "@oh-my-pi/pi-utils";
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import { computeLineHash } from "../edit/modes/hashline";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
@@ -71,7 +70,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
readonly strict = true;
readonly deferrable = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(astEditDescription);
this.description = prompt.render(astEditDescription);
}
async execute(
+2 -3
View File
@@ -3,9 +3,8 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { untilAborted } from "@oh-my-pi/pi-utils";
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import { computeLineHash } from "../edit/modes/hashline";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
@@ -65,7 +64,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
readonly strict = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(astGrepDescription);
this.description = prompt.render(astGrepDescription);
}
async execute(
@@ -1,6 +1,6 @@
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import awaitDescription from "../prompts/tools/await.md" with { type: "text" };
import type { ToolSession } from "./index";
@@ -36,7 +36,7 @@ export class AwaitTool implements AgentTool<typeof awaitSchema, AwaitToolDetails
readonly strict = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(awaitDescription);
this.description = prompt.render(awaitDescription);
}
static createIf(session: ToolSession): AwaitTool | null {
+2 -3
View File
@@ -3,9 +3,8 @@ import * as path from "node:path";
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { Component } from "@oh-my-pi/pi-tui";
import { ImageProtocol, TERMINAL, Text } from "@oh-my-pi/pi-tui";
import { $env, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
import { $env, getProjectDir, isEnoent, prompt } from "@oh-my-pi/pi-utils";
import { Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import { type BashResult, executeBash } from "../exec/bash-executor";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import { truncateToVisualLines } from "../modes/components/visual-truncate";
@@ -218,7 +217,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
constructor(private readonly session: ToolSession) {
this.#asyncEnabled = this.session.settings.get("async.enabled");
this.parameters = this.#asyncEnabled ? bashSchemaWithAsync : bashSchemaBase;
this.description = renderPromptTemplate(bashDescription, {
this.description = prompt.render(bashDescription, {
asyncEnabled: this.#asyncEnabled,
hasAstGrep: this.session.settings.get("astGrep.enabled"),
hasAstEdit: this.session.settings.get("astEdit.enabled"),
+2 -3
View File
@@ -4,7 +4,7 @@ import * as path from "node:path";
import { Readability } from "@mozilla/readability";
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { StringEnum } from "@oh-my-pi/pi-ai";
import { $which, getPuppeteerDir, logger, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
import { $which, getPuppeteerDir, logger, prompt, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { type HTMLElement, parseHTML } from "linkedom";
import type {
@@ -16,7 +16,6 @@ import type {
default as Puppeteer,
SerializedAXNode,
} from "puppeteer";
import { renderPromptTemplate } from "../config/prompt-templates";
import browserDescription from "../prompts/tools/browser.md" with { type: "text" };
import type { ToolSession } from "../sdk";
import { resizeImage } from "../utils/image-resize";
@@ -539,7 +538,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
readonly #patchedClients = new WeakSet<object>();
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(browserDescription, {});
this.description = prompt.render(browserDescription, {});
}
async #closeBrowser(): Promise<void> {
@@ -1,9 +1,8 @@
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { untilAborted } from "@oh-my-pi/pi-utils";
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
import calculatorDescription from "../prompts/tools/calculator.md" with { type: "text" };
@@ -402,7 +401,7 @@ export class CalculatorTool implements AgentTool<typeof calculatorSchema, Calcul
readonly strict = true;
constructor(_session: ToolSession) {
this.description = renderPromptTemplate(calculatorDescription);
this.description = prompt.render(calculatorDescription);
}
async execute(
@@ -1,6 +1,6 @@
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import cancelJobDescription from "../prompts/tools/cancel-job.md" with { type: "text" };
import type { ToolSession } from "./index";
@@ -23,7 +23,7 @@ export class CancelJobTool implements AgentTool<typeof cancelJobSchema, CancelJo
readonly strict = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(cancelJobDescription);
this.description = prompt.render(cancelJobDescription);
}
static createIf(session: ToolSession): CancelJobTool | null {
@@ -1,6 +1,6 @@
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import checkpointDescription from "../prompts/tools/checkpoint.md" with { type: "text" };
import rewindDescription from "../prompts/tools/rewind.md" with { type: "text" };
import type { ToolSession } from ".";
@@ -54,7 +54,7 @@ export class CheckpointTool implements AgentTool<typeof checkpointSchema, Checkp
readonly strict = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(checkpointDescription);
this.description = prompt.render(checkpointDescription);
}
static createIf(session: ToolSession): CheckpointTool | null {
@@ -96,7 +96,7 @@ export class RewindTool implements AgentTool<typeof rewindSchema, RewindToolDeta
readonly strict = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(rewindDescription);
this.description = prompt.render(rewindDescription);
}
static createIf(session: ToolSession): RewindTool | null {
+2 -2
View File
@@ -7,8 +7,8 @@ import type {
} from "@oh-my-pi/pi-agent-core";
import { StringEnum } from "@oh-my-pi/pi-ai";
import { type Component, Text } from "@oh-my-pi/pi-tui";
import { prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import {
type DapBreakpointRecord,
type DapCapabilities,
@@ -612,7 +612,7 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
readonly concurrency = "exclusive";
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(debugDescription);
this.description = prompt.render(debugDescription);
}
static createIf(session: ToolSession): DebugTool | null {
@@ -1,8 +1,7 @@
import * as fs from "node:fs/promises";
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { isEnoent, prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import exitPlanModeDescription from "../prompts/tools/exit-plan-mode.md" with { type: "text" };
import type { ToolSession } from ".";
import { resolvePlanPath } from "./plan-mode-guard";
@@ -49,7 +48,7 @@ export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, Ex
readonly concurrency = "exclusive";
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(exitPlanModeDescription);
this.description = prompt.render(exitPlanModeDescription);
}
async execute(
+2 -3
View File
@@ -4,10 +4,9 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
import { FileType, type GlobMatch, glob } from "@oh-my-pi/pi-natives";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
import { isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
import type { Static } from "@sinclair/typebox";
import { Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
import findDescription from "../prompts/tools/find.md" with { type: "text" };
@@ -86,7 +85,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
options?: FindToolOptions,
) {
this.#customOps = options?.operations;
this.description = renderPromptTemplate(findDescription);
this.description = prompt.render(findDescription);
}
async execute(
@@ -1,10 +1,9 @@
import * as os from "node:os";
import * as path from "node:path";
import { getAntigravityHeaders, getEnvApiKey, StringEnum } from "@oh-my-pi/pi-ai";
import { $env, isEnoent, ptree, readSseJson, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
import { $env, isEnoent, prompt, ptree, readSseJson, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import type { ModelRegistry } from "../config/model-registry";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { CustomTool } from "../extensibility/custom-tools/types";
import geminiImageDescription from "../prompts/tools/gemini-image.md" with { type: "text" };
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
@@ -599,7 +598,7 @@ async function parseAntigravitySseForImage(response: Response, signal?: AbortSig
export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageToolDetails> = {
name: "generate_image",
label: "GenerateImage",
description: renderPromptTemplate(geminiImageDescription),
description: prompt.render(geminiImageDescription),
parameters: geminiImageSchema,
async execute(_toolCallId, params, _onUpdate, ctx, signal) {
return untilAborted(signal, async () => {
+10 -11
View File
@@ -1,9 +1,8 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { abortableSleep, isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
import { abortableSleep, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import ghIssueViewDescription from "../prompts/tools/gh-issue-view.md" with { type: "text" };
import ghPrCheckoutDescription from "../prompts/tools/gh-pr-checkout.md" with { type: "text" };
import ghPrDiffDescription from "../prompts/tools/gh-pr-diff.md" with { type: "text" };
@@ -1902,7 +1901,7 @@ function buildTextResult(
export class GhRepoViewTool implements AgentTool<typeof ghRepoViewSchema, GhToolDetails> {
readonly name = "gh_repo_view";
readonly label = "GitHub Repo";
readonly description = renderPromptTemplate(ghRepoViewDescription);
readonly description = prompt.render(ghRepoViewDescription);
readonly parameters = ghRepoViewSchema;
readonly strict = true;
@@ -1943,7 +1942,7 @@ export class GhRepoViewTool implements AgentTool<typeof ghRepoViewSchema, GhTool
export class GhIssueViewTool implements AgentTool<typeof ghIssueViewSchema, GhToolDetails> {
readonly name = "gh_issue_view";
readonly label = "GitHub Issue";
readonly description = renderPromptTemplate(ghIssueViewDescription);
readonly description = prompt.render(ghIssueViewDescription);
readonly parameters = ghIssueViewSchema;
readonly strict = true;
@@ -1980,7 +1979,7 @@ export class GhIssueViewTool implements AgentTool<typeof ghIssueViewSchema, GhTo
export class GhPrViewTool implements AgentTool<typeof ghPrViewSchema, GhToolDetails> {
readonly name = "gh_pr_view";
readonly label = "GitHub PR";
readonly description = renderPromptTemplate(ghPrViewDescription);
readonly description = prompt.render(ghPrViewDescription);
readonly parameters = ghPrViewSchema;
readonly strict = true;
@@ -2024,7 +2023,7 @@ export class GhPrViewTool implements AgentTool<typeof ghPrViewSchema, GhToolDeta
export class GhPrDiffTool implements AgentTool<typeof ghPrDiffSchema, GhToolDetails> {
readonly name = "gh_pr_diff";
readonly label = "GitHub PR Diff";
readonly description = renderPromptTemplate(ghPrDiffDescription);
readonly description = prompt.render(ghPrDiffDescription);
readonly parameters = ghPrDiffSchema;
readonly strict = true;
@@ -2073,7 +2072,7 @@ export class GhPrDiffTool implements AgentTool<typeof ghPrDiffSchema, GhToolDeta
export class GhPrCheckoutTool implements AgentTool<typeof ghPrCheckoutSchema, GhToolDetails> {
readonly name = "gh_pr_checkout";
readonly label = "GitHub PR Checkout";
readonly description = renderPromptTemplate(ghPrCheckoutDescription);
readonly description = prompt.render(ghPrCheckoutDescription);
readonly parameters = ghPrCheckoutSchema;
readonly strict = true;
@@ -2204,7 +2203,7 @@ export class GhPrCheckoutTool implements AgentTool<typeof ghPrCheckoutSchema, Gh
export class GhPrPushTool implements AgentTool<typeof ghPrPushSchema, GhToolDetails> {
readonly name = "gh_pr_push";
readonly label = "GitHub PR Push";
readonly description = renderPromptTemplate(ghPrPushDescription);
readonly description = prompt.render(ghPrPushDescription);
readonly parameters = ghPrPushSchema;
readonly strict = true;
@@ -2265,7 +2264,7 @@ export class GhPrPushTool implements AgentTool<typeof ghPrPushSchema, GhToolDeta
export class GhSearchIssuesTool implements AgentTool<typeof ghSearchIssuesSchema, GhToolDetails> {
readonly name = "gh_search_issues";
readonly label = "GitHub Issue Search";
readonly description = renderPromptTemplate(ghSearchIssuesDescription);
readonly description = prompt.render(ghSearchIssuesDescription);
readonly parameters = ghSearchIssuesSchema;
readonly strict = true;
@@ -2300,7 +2299,7 @@ export class GhSearchIssuesTool implements AgentTool<typeof ghSearchIssuesSchema
export class GhSearchPrsTool implements AgentTool<typeof ghSearchPrsSchema, GhToolDetails> {
readonly name = "gh_search_prs";
readonly label = "GitHub PR Search";
readonly description = renderPromptTemplate(ghSearchPrsDescription);
readonly description = prompt.render(ghSearchPrsDescription);
readonly parameters = ghSearchPrsSchema;
readonly strict = true;
@@ -2335,7 +2334,7 @@ export class GhSearchPrsTool implements AgentTool<typeof ghSearchPrsSchema, GhTo
export class GhRunWatchTool implements AgentTool<typeof ghRunWatchSchema, GhToolDetails> {
readonly name = "gh_run_watch";
readonly label = "GitHub Run Watch";
readonly description = renderPromptTemplate(ghRunWatchDescription);
readonly description = prompt.render(ghRunWatchDescription);
readonly parameters = ghRunWatchSchema;
readonly strict = true;
+2 -3
View File
@@ -4,9 +4,8 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
import { type GrepMatch, GrepOutputMode, type GrepResult, grep } from "@oh-my-pi/pi-natives";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { untilAborted } from "@oh-my-pi/pi-utils";
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import { formatChunkedGrepLine } from "../edit/modes/chunk";
import { computeLineHash } from "../edit/modes/hashline";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -74,7 +73,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
constructor(private readonly session: ToolSession) {
const displayMode = resolveFileDisplayMode(session);
this.description = renderPromptTemplate(grepDescription, {
this.description = prompt.render(grepDescription, {
IS_HASHLINE_MODE: displayMode.hashLines,
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
IS_CHUNK_MODE: displayMode.chunked,
@@ -1,8 +1,8 @@
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { type Api, type AssistantMessage, completeSimple, type Model } from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { expandRoleAlias, resolveModelFromString } from "../config/model-resolver";
import { renderPromptTemplate } from "../config/prompt-templates";
import inspectImageDescription from "../prompts/tools/inspect-image.md" with { type: "text" };
import inspectImageSystemPromptTemplate from "../prompts/tools/inspect-image-system.md" with { type: "text" };
import {
@@ -49,7 +49,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
private readonly session: ToolSession,
private readonly completeImageRequest: typeof completeSimple = completeSimple,
) {
this.description = renderPromptTemplate(inspectImageDescription);
this.description = prompt.render(inspectImageDescription);
}
async execute(
@@ -127,7 +127,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
const response = await this.completeImageRequest(
model,
{
systemPrompt: renderPromptTemplate(inspectImageSystemPromptTemplate),
systemPrompt: prompt.render(inspectImageSystemPromptTemplate),
messages: [
{
role: "user",
+2 -3
View File
@@ -4,9 +4,8 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
import type { ImageContent } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
import { Markdown, Text } from "@oh-my-pi/pi-tui";
import { getProjectDir } from "@oh-my-pi/pi-utils";
import { getProjectDir, prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import { executePython, getPreludeDocs, type PythonExecutorOptions } from "../ipy/executor";
import type { PreludeHelper, PythonStatusEvent } from "../ipy/kernel";
@@ -136,7 +135,7 @@ function renderJsonTree(value: unknown, theme: Theme, expanded: boolean, maxDept
export function getPythonToolDescription(): string {
const helpers = getPreludeDocs();
const categories = groupPreludeHelpers(helpers);
return renderPromptTemplate(pythonDescription, { categories });
return prompt.render(pythonDescription, { categories });
}
export interface PythonToolOptions {
+3 -4
View File
@@ -5,9 +5,8 @@ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
import { glob } from "@oh-my-pi/pi-natives";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { getRemoteDir, untilAborted } from "@oh-my-pi/pi-utils";
import { getRemoteDir, prompt, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import {
type ChunkReadTarget,
formatChunkedRead,
@@ -452,10 +451,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
this.#inspectImageEnabled = session.settings.get("inspect_image.enabled");
this.description =
resolveEditMode(session) === "chunk"
? renderPromptTemplate(readChunkDescription, {
? prompt.render(readChunkDescription, {
anchorStyle: resolveAnchorStyle(session.settings),
})
: renderPromptTemplate(readDescription, {
: prompt.render(readDescription, {
DEFAULT_LIMIT: String(this.#defaultLimit),
DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
IS_HASHLINE_MODE: displayMode.hashLines,
@@ -1,7 +1,6 @@
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { type MermaidAsciiRenderOptions, renderMermaidAscii } from "@oh-my-pi/pi-utils";
import { type MermaidAsciiRenderOptions, prompt, renderMermaidAscii } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import renderMermaidDescription from "../prompts/tools/render-mermaid.md" with { type: "text" };
import type { ToolSession } from "./index";
@@ -41,7 +40,7 @@ export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema,
readonly strict = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(renderMermaidDescription);
this.description = prompt.render(renderMermaidDescription);
}
async execute(
+2 -3
View File
@@ -1,9 +1,8 @@
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { untilAborted } from "@oh-my-pi/pi-utils";
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
import resolveDescription from "../prompts/tools/resolve.md" with { type: "text" };
@@ -42,7 +41,7 @@ export class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolD
readonly strict = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(resolveDescription);
this.description = prompt.render(resolveDescription);
}
async execute(
@@ -1,7 +1,7 @@
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { type Component, Text } from "@oh-my-pi/pi-tui";
import { prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import {
buildDiscoverableMCPSearchIndex,
@@ -105,7 +105,7 @@ function supportsMCPToolDiscoveryExecution(session: ToolSession): session is MCP
export function renderSearchToolBm25Description(discoverableTools: DiscoverableMCPTool[] = []): string {
const summary = summarizeDiscoverableMCPTools(discoverableTools);
return renderPromptTemplate(searchToolBm25Description, {
return prompt.render(searchToolBm25Description, {
discoverableMCPToolCount: summary.toolCount,
discoverableMCPServerSummaries: summary.servers.map(formatDiscoverableMCPToolServerSummary),
hasDiscoverableMCPServers: summary.servers.length > 0,
+2 -2
View File
@@ -1,10 +1,10 @@
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import type { SSHHost } from "../capability/ssh";
import { sshCapability } from "../capability/ssh";
import { renderPromptTemplate } from "../config/prompt-templates";
import { loadCapability } from "../discovery";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
@@ -59,7 +59,7 @@ async function formatHostEntry(host: SSHHost): Promise<string> {
}
async function formatDescription(hosts: SSHHost[]): Promise<string> {
const baseDescription = renderPromptTemplate(sshDescriptionBase);
const baseDescription = prompt.render(sshDescriptionBase);
if (hosts.length === 0) {
return baseDescription;
}
@@ -2,9 +2,9 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
import { StringEnum } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import chalk from "chalk";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
import todoWriteDescription from "../prompts/tools/todo-write.md" with { type: "text" };
@@ -347,7 +347,7 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
readonly strict = true;
constructor(private readonly session: ToolSession) {
this.description = renderPromptTemplate(todoWriteDescription);
this.description = prompt.render(todoWriteDescription);
}
async execute(
+2 -3
View File
@@ -9,10 +9,9 @@ import type {
} from "@oh-my-pi/pi-agent-core";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
import { isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { unzipSync, zipSync } from "fflate";
import { renderPromptTemplate } from "../config/prompt-templates";
import { stripHashlinePrefixes } from "../edit";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
@@ -149,7 +148,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
this.#writethrough = enableLsp
? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
: writethroughNoop;
this.description = renderPromptTemplate(writeDescription);
this.description = prompt.render(writeDescription);
}
async #resolveArchiveWritePath(writePath: string): Promise<ResolvedArchiveWritePath | null> {
@@ -5,16 +5,15 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { completeSimple } from "@oh-my-pi/pi-ai";
import { logger } from "@oh-my-pi/pi-utils";
import { logger, prompt } from "@oh-my-pi/pi-utils";
import type { ModelRegistry } from "../config/model-registry";
import { resolveModelRoleValue } from "../config/model-resolver";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { Settings } from "../config/settings";
import MODEL_PRIO from "../priority.json" with { type: "json" };
import commitSystemPrompt from "../prompts/system/commit-message-system.md" with { type: "text" };
import { toReasoningEffort } from "../thinking";
const COMMIT_SYSTEM_PROMPT = renderPromptTemplate(commitSystemPrompt);
const COMMIT_SYSTEM_PROMPT = prompt.render(commitSystemPrompt);
const MAX_DIFF_CHARS = 4000;
/** File patterns that should be excluded from commit message generation diffs. */
@@ -1,170 +0,0 @@
export type PromptRenderPhase = "pre-render" | "post-render";
export interface PromptFormatOptions {
renderPhase?: PromptRenderPhase;
replaceAsciiSymbols?: boolean;
boldRfc2119Keywords?: boolean;
}
// Opening XML tag (not self-closing, not closing)
const OPENING_XML = /^<([a-z_-]+)(?:\s+[^>]*)?>$/;
// Closing XML tag
const CLOSING_XML = /^<\/([a-z_-]+)>$/;
// Handlebars block start: {{#if}}, {{#has}}, {{#list}}, etc.
const OPENING_HBS = /^\{\{#/;
// Handlebars block end: {{/if}}, {{/has}}, {{/list}}, etc.
const CLOSING_HBS = /^\{\{\//;
// List item (- or * or 1.)
const LIST_ITEM = /^(?:[-*]\s|\d+\.\s)/;
// Code fence
const CODE_FENCE = /^```/;
// Table row
const TABLE_ROW = /^\|.*\|$/;
// Table separator (|---|---|)
const TABLE_SEP = /^\|[-:\s|]+\|$/;
/** RFC 2119 keywords used in prompts. */
const RFC2119_KEYWORDS = /\b(?:MUST NOT|SHOULD NOT|SHALL NOT|RECOMMENDED|REQUIRED|OPTIONAL|SHOULD|SHALL|MUST|MAY)\b/g;
function boldRfc2119Keywords(line: string): string {
return line.replace(RFC2119_KEYWORDS, (match, offset, source) => {
const isAlreadyBold =
source[offset - 2] === "*" &&
source[offset - 1] === "*" &&
source[offset + match.length] === "*" &&
source[offset + match.length + 1] === "*";
if (isAlreadyBold) {
return match;
}
return `**${match}**`;
});
}
/** Compact a table row by trimming cell padding */
function compactTableRow(line: string): string {
const cells = line.split("|");
return cells.map(c => c.trim()).join("|");
}
/** Compact a table separator row */
function compactTableSep(line: string): string {
const cells = line.split("|").filter(c => c.trim());
const normalized = cells.map(c => {
const trimmed = c.trim();
const left = trimmed.startsWith(":");
const right = trimmed.endsWith(":");
if (left && right) return ":---:";
if (left) return ":---";
if (right) return "---:";
return "---";
});
return `|${normalized.join("|")}|`;
}
function replaceCommonAsciiSymbols(line: string): string {
return line
.replace(/\.{3}/g, "…")
.replace(/<->/g, "↔")
.replace(/->/g, "→")
.replace(/<-/g, "←")
.replace(/!=/g, "≠")
.replace(/<=/g, "≤")
.replace(/>=/g, "≥");
}
export function formatPromptContent(content: string, options: PromptFormatOptions = {}): string {
const {
renderPhase = "post-render",
replaceAsciiSymbols = false,
boldRfc2119Keywords: shouldBoldRfc2119 = false,
} = options;
const isPreRender = renderPhase === "pre-render";
const lines = content.split("\n");
const result: string[] = [];
let inCodeBlock = false;
const topLevelTags: string[] = [];
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trimEnd();
let trimmedStart = line.trimStart();
if (CODE_FENCE.test(trimmedStart)) {
inCodeBlock = !inCodeBlock;
result.push(line);
continue;
}
if (inCodeBlock) {
result.push(line);
continue;
}
if (replaceAsciiSymbols) {
line = replaceCommonAsciiSymbols(line);
}
trimmedStart = line.trimStart();
const trimmed = line.trim();
const isOpeningXml = OPENING_XML.test(trimmedStart) && !trimmedStart.endsWith("/>");
if (isOpeningXml && line.length === trimmedStart.length) {
const match = OPENING_XML.exec(trimmedStart);
if (match) topLevelTags.push(match[1]);
}
const closingMatch = CLOSING_XML.exec(trimmedStart);
if (closingMatch) {
const tagName = closingMatch[1];
if (topLevelTags.length > 0 && topLevelTags[topLevelTags.length - 1] === tagName) {
topLevelTags.pop();
}
} else if (isPreRender && trimmedStart.startsWith("{{")) {
/* keep indentation as-is in pre-render for Handlebars markers */
} else if (TABLE_SEP.test(trimmedStart)) {
const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
line = `${leadingWhitespace}${compactTableSep(trimmedStart)}`;
} else if (TABLE_ROW.test(trimmedStart)) {
const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
line = `${leadingWhitespace}${compactTableRow(trimmedStart)}`;
}
if (shouldBoldRfc2119) {
line = boldRfc2119Keywords(line);
}
const isBlank = trimmed === "";
if (isBlank) {
const prevLine = result[result.length - 1]?.trim() ?? "";
const nextLine = lines[i + 1]?.trim() ?? "";
if (LIST_ITEM.test(nextLine)) {
continue;
}
if (OPENING_XML.test(prevLine) || (isPreRender && OPENING_HBS.test(prevLine))) {
continue;
}
if (CLOSING_XML.test(nextLine) || (isPreRender && CLOSING_HBS.test(nextLine))) {
continue;
}
const prevIsBlank = prevLine === "";
if (prevIsBlank) {
continue;
}
}
if (CLOSING_XML.test(trimmed) || (isPreRender && CLOSING_HBS.test(trimmed))) {
while (result.length > 0 && result[result.length - 1].trim() === "") {
result.pop();
}
}
result.push(line);
}
while (result.length > 0 && result[result.length - 1].trim() === "") {
result.pop();
}
return result.join("\n");
}
@@ -5,15 +5,14 @@ import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { completeSimple } from "@oh-my-pi/pi-ai";
import { logger } from "@oh-my-pi/pi-utils";
import { logger, prompt } from "@oh-my-pi/pi-utils";
import type { ModelRegistry } from "../config/model-registry";
import { resolveRoleSelection } from "../config/model-resolver";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { Settings } from "../config/settings";
import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
import { toReasoningEffort } from "../thinking";
const TITLE_SYSTEM_PROMPT = renderPromptTemplate(titleSystemPrompt);
const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
const DEFAULT_TERMINAL_TITLE = "π";
const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
@@ -1,4 +1,4 @@
import { parseFrontmatter } from "../../utils/frontmatter";
import { parseFrontmatter } from "@oh-my-pi/pi-utils";
import type { RenderResult, SpecialHandler } from "./types";
import { buildResult, loadPage } from "./types";
import { asString } from "./utils";
@@ -7,8 +7,8 @@
*/
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { StringEnum } from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import { Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../../config/prompt-templates";
import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types";
import type { Theme } from "../../modes/theme/theme";
import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { type: "text" };
@@ -213,7 +213,7 @@ export class SearchTool implements AgentTool<typeof webSearchSchema, SearchRende
readonly strict = true;
constructor(_session: ToolSession) {
this.description = renderPromptTemplate(webSearchDescription);
this.description = prompt.render(webSearchDescription);
}
async execute(
@@ -231,7 +231,7 @@ export class SearchTool implements AgentTool<typeof webSearchSchema, SearchRende
export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRenderDetails> = {
name: "web_search",
label: "Web Search",
description: renderPromptTemplate(webSearchDescription),
description: prompt.render(webSearchDescription),
parameters: webSearchSchema,
async execute(
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { parseFrontmatter } from "@oh-my-pi/pi-coding-agent/utils/frontmatter";
import { parseFrontmatter } from "@oh-my-pi/pi-utils";
describe("parseFrontmatter", () => {
const parse = (content: string) => parseFrontmatter(content, { source: "tests:frontmatter", level: "off" });
@@ -1,12 +1,12 @@
import { describe, expect, it } from "bun:test";
import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
import planModeApprovedPrompt from "@oh-my-pi/pi-coding-agent/prompts/system/plan-mode-approved.md" with {
type: "text",
};
import { prompt } from "@oh-my-pi/pi-utils";
describe("plan-mode-approved prompt", () => {
it("includes final plan artifact path in injected execution prompt", () => {
const rendered = renderPromptTemplate(planModeApprovedPrompt, {
const rendered = prompt.render(planModeApprovedPrompt, {
planContent: "1. Do work",
finalPlanFilePath: "local://WP_MIGRATION_PLAN.md",
});
@@ -1,11 +1,11 @@
import { describe, expect, test } from "bun:test";
import { formatPromptContent } from "@oh-my-pi/pi-coding-agent/utils/prompt-format";
import { prompt } from "@oh-my-pi/pi-utils";
describe("formatPromptContent renderPhase", () => {
describe("prompt.format renderPhase", () => {
test("pre-render preserves indentation on Handlebars block lines", () => {
const input = "<root>\n {{#if ok}}\n value\n {{/if}}\n</root>";
const output = formatPromptContent(input, { renderPhase: "pre-render" });
const output = prompt.format(input, { renderPhase: "pre-render" });
expect(output).toBe("<root>\n {{#if ok}}\n value\n {{/if}}\n</root>");
});
@@ -13,7 +13,7 @@ describe("formatPromptContent renderPhase", () => {
test("pre-render preserves leading tabs", () => {
const input = "\t<root>\n\t {{#if ok}}\n\t value\n\t {{/if}}\n</root>";
const output = formatPromptContent(input, { renderPhase: "pre-render" });
const output = prompt.format(input, { renderPhase: "pre-render" });
expect(output).toBe(input);
});
@@ -21,7 +21,7 @@ describe("formatPromptContent renderPhase", () => {
test("pre-render trims trailing whitespace", () => {
const input = "\t<root> \n\t {{#if ok}}\t\n\t value \n\t {{/if}} \n</root>";
const output = formatPromptContent(input, { renderPhase: "pre-render" });
const output = prompt.format(input, { renderPhase: "pre-render" });
expect(output).toBe("\t<root>\n\t {{#if ok}}\n\t value\n\t {{/if}}\n</root>");
});
@@ -29,7 +29,7 @@ describe("formatPromptContent renderPhase", () => {
test("post-render mode preserves indentation on Handlebars-like lines", () => {
const input = "<root>\n {{#if ok}}\n value\n {{/if}}\n</root>";
const output = formatPromptContent(input, { renderPhase: "post-render" });
const output = prompt.format(input, { renderPhase: "post-render" });
expect(output).toBe("<root>\n {{#if ok}}\n value\n {{/if}}\n</root>");
});
@@ -37,8 +37,8 @@ describe("formatPromptContent renderPhase", () => {
test("pre-render removes blank line before closing Handlebars block while post-render keeps it", () => {
const input = "<root>\n{{#if ok}}\nvalue\n\n{{/if}}\n</root>";
const preRender = formatPromptContent(input, { renderPhase: "pre-render" });
const postRender = formatPromptContent(input, { renderPhase: "post-render" });
const preRender = prompt.format(input, { renderPhase: "pre-render" });
const postRender = prompt.format(input, { renderPhase: "post-render" });
expect(preRender).toBe("<root>\n{{#if ok}}\nvalue\n{{/if}}\n</root>");
expect(postRender).toBe("<root>\n{{#if ok}}\nvalue\n\n{{/if}}\n</root>");
@@ -46,7 +46,7 @@ describe("formatPromptContent renderPhase", () => {
test("pre-render compacts table rows and does not duplicate content when replacing ascii", () => {
const input =
'|`cat <<\'EOF\' > file`|`write(path="file", content="...")`|\n|`sed -i \'s/old/new/\' file`|`edit(path="file", edits=[...])`|';
const output = formatPromptContent(input, {
const output = prompt.format(input, {
renderPhase: "pre-render",
replaceAsciiSymbols: true,
});
@@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { renderPromptTemplate, type TemplateContext } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
import { buildSystemPrompt } from "@oh-my-pi/pi-coding-agent/system-prompt";
import { prompt } from "@oh-my-pi/pi-utils";
import Handlebars from "handlebars";
const baseGitContext = {
@@ -16,7 +16,7 @@ const baseGitContext = {
const systemPromptsDir = path.resolve(import.meta.dir, "../src/prompts/system");
const baseRenderContext: TemplateContext = {
const baseRenderContext: prompt.TemplateContext = {
TASK_TOOL_NAME: "task",
ARGUMENTS: "alpha beta",
agent: "You are a delegated worker",
@@ -101,7 +101,7 @@ describe("system Handlebars prompt templates", () => {
const templatePath = path.join(systemPromptsDir, "custom-system-prompt.md");
const template = await Bun.file(templatePath).text();
const both = renderPromptTemplate(template, {
const both = prompt.render(template, {
...baseRenderContext,
contextFiles: [{ path: "a.txt", content: "A" }],
git: { ...baseGitContext, isRepo: true },
@@ -110,7 +110,7 @@ describe("system Handlebars prompt templates", () => {
expect(both).toContain("## Context");
expect(both).toContain("## Version Control");
const contextOnly = renderPromptTemplate(template, {
const contextOnly = prompt.render(template, {
...baseRenderContext,
contextFiles: [{ path: "a.txt", content: "A" }],
git: { isRepo: false },
@@ -119,7 +119,7 @@ describe("system Handlebars prompt templates", () => {
expect(contextOnly).toContain("## Context");
expect(contextOnly).not.toContain("## Version Control");
const gitOnly = renderPromptTemplate(template, {
const gitOnly = prompt.render(template, {
...baseRenderContext,
contextFiles: [],
git: {
@@ -134,7 +134,7 @@ describe("system Handlebars prompt templates", () => {
expect(gitOnly).not.toContain("## Context");
expect(gitOnly).toContain("## Version Control");
const neither = renderPromptTemplate(template, {
const neither = prompt.render(template, {
...baseRenderContext,
contextFiles: [],
git: { isRepo: false },
@@ -149,7 +149,7 @@ describe("system Handlebars prompt templates", () => {
const template = await Bun.file(templatePath).text();
const baseTools = baseRenderContext.tools as string[];
const withInspectImage = renderPromptTemplate(template, {
const withInspectImage = prompt.render(template, {
...baseRenderContext,
tools: [...baseTools, "inspect_image"],
});
@@ -157,7 +157,7 @@ describe("system Handlebars prompt templates", () => {
expect(withInspectImage).toContain("**MUST** use `inspect_image` over `read`");
expect(withInspectImage).toContain("Write a specific `question` for `inspect_image`");
const withoutInspectImage = renderPromptTemplate(template, {
const withoutInspectImage = prompt.render(template, {
...baseRenderContext,
tools: baseTools.filter((tool: string) => tool !== "inspect_image"),
});
@@ -168,7 +168,7 @@ describe("system Handlebars prompt templates", () => {
const templatePath = path.join(systemPromptsDir, "system-prompt.md");
const template = await Bun.file(templatePath).text();
const rendered = renderPromptTemplate(template, {
const rendered = prompt.render(template, {
...baseRenderContext,
mcpDiscoveryMode: true,
hasMCPDiscoveryServers: true,
@@ -10,8 +10,8 @@ import * as path from "node:path";
import type { AgentMessage, ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Model } from "@oh-my-pi/pi-ai";
import { computeLineHash, formatSessionDumpText, RpcClient, renderPromptTemplate } from "@oh-my-pi/pi-coding-agent";
import { Snowflake } from "@oh-my-pi/pi-utils";
import { computeLineHash, formatSessionDumpText, RpcClient } from "@oh-my-pi/pi-coding-agent";
import { prompt, Snowflake } from "@oh-my-pi/pi-utils";
import { diffLines } from "diff";
import { formatDirectory } from "./formatter";
import { discoverSharedInfra, InProcessClient, type SharedInfra } from "./in-process-client";
@@ -612,21 +612,21 @@ type BenchmarkPromptDelivery = {
};
function buildBenchmarkSystemPrompt(params: { multiFile: boolean; config: BenchmarkConfig }): string {
return renderPromptTemplate(benchmarkSystemPrompt, {
return prompt.render(benchmarkSystemPrompt, {
multiFile: params.multiFile,
instructions: buildInstructions(params.config),
});
}
function buildInitialBenchmarkPrompt(params: { taskPrompt: string; guidedContext?: string | null }): string {
return renderPromptTemplate(benchmarkTaskPrompt, {
return prompt.render(benchmarkTaskPrompt, {
task_prompt: params.taskPrompt,
guided_context: params.guidedContext ?? undefined,
});
}
function buildRetryBenchmarkPrompt(params: { retryContext: string; guidedContext?: string | null }): string {
return renderPromptTemplate(benchmarkRetryPrompt, {
return prompt.render(benchmarkRetryPrompt, {
retry_context: params.retryContext,
guided_context: params.guidedContext ?? undefined,
});
+1
View File
@@ -28,6 +28,7 @@
},
"dependencies": {
"beautiful-mermaid": "^1.1",
"handlebars": "^4.7.9",
"winston": "^3.19",
"winston-daily-rotate-file": "^5.0"
},
@@ -1,5 +1,6 @@
import { logger, truncate } from "@oh-my-pi/pi-utils";
import { YAML } from "bun";
import { truncate } from "./format";
import * as logger from "./logger";
function stripHtmlComments(content: string): string {
return content.replace(/<!--[\s\S]*?-->/g, "");
+2
View File
@@ -4,6 +4,7 @@ export * from "./color";
export * from "./dirs";
export * from "./env";
export * from "./format";
export * from "./frontmatter";
export * from "./fs-error";
export * from "./glob";
export * from "./hook-fetch";
@@ -13,6 +14,7 @@ export * from "./mermaid-ascii";
export * as postmortem from "./postmortem";
export * as procmgr from "./procmgr";
export { setNativeKillTree } from "./procmgr";
export * as prompt from "./prompt";
export * as ptree from "./ptree";
export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
export * from "./snowflake";
+401
View File
@@ -0,0 +1,401 @@
import type { HelperDelegate, HelperOptions, Template, TemplateDelegate } from "handlebars";
import Handlebars from "handlebars";
export type { HelperDelegate, HelperOptions, Template, TemplateDelegate };
export type PromptRenderPhase = "pre-render" | "post-render";
export interface PromptFormatOptions {
renderPhase?: PromptRenderPhase;
replaceAsciiSymbols?: boolean;
boldRfc2119Keywords?: boolean;
}
// Opening XML tag (not self-closing, not closing)
const OPENING_XML = /^<([a-z_-]+)(?:\s+[^>]*)?>$/;
// Closing XML tag
const CLOSING_XML = /^<\/([a-z_-]+)>$/;
// Handlebars block start: {{#if}}, {{#has}}, {{#list}}, etc.
const OPENING_HBS = /^\{\{#/;
// Handlebars block end: {{/if}}, {{/has}}, {{/list}}, etc.
const CLOSING_HBS = /^\{\{\//;
// List item (- or * or 1.)
const LIST_ITEM = /^(?:[-*]\s|\d+\.\s)/;
// Code fence
const CODE_FENCE = /^```/;
// Table row
const TABLE_ROW = /^\|.*\|$/;
// Table separator (|---|---|)
const TABLE_SEP = /^\|[-:\s|]+\|$/;
/** RFC 2119 keywords used in prompts. */
const RFC2119_KEYWORDS = /\b(?:MUST NOT|SHOULD NOT|SHALL NOT|RECOMMENDED|REQUIRED|OPTIONAL|SHOULD|SHALL|MUST|MAY)\b/g;
function boldRfc2119Keywords(line: string): string {
return line.replace(RFC2119_KEYWORDS, (match, offset, source) => {
const isAlreadyBold =
source[offset - 2] === "*" &&
source[offset - 1] === "*" &&
source[offset + match.length] === "*" &&
source[offset + match.length + 1] === "*";
if (isAlreadyBold) {
return match;
}
return `**${match}**`;
});
}
/** Compact a table row by trimming cell padding */
function compactTableRow(line: string): string {
const cells = line.split("|");
return cells.map(c => c.trim()).join("|");
}
/** Compact a table separator row */
function compactTableSep(line: string): string {
const cells = line.split("|").filter(c => c.trim());
const normalized = cells.map(c => {
const trimmed = c.trim();
const left = trimmed.startsWith(":");
const right = trimmed.endsWith(":");
if (left && right) return ":---:";
if (left) return ":---";
if (right) return "---:";
return "---";
});
return `|${normalized.join("|")}|`;
}
function replaceCommonAsciiSymbols(line: string): string {
return line
.replace(/\.{3}/g, "…")
.replace(/<->/g, "↔")
.replace(/->/g, "→")
.replace(/<-/g, "←")
.replace(/!=/g, "≠")
.replace(/<=/g, "≤")
.replace(/>=/g, "≥");
}
export function format(content: string, options: PromptFormatOptions = {}): string {
const {
renderPhase = "post-render",
replaceAsciiSymbols = false,
boldRfc2119Keywords: shouldBoldRfc2119 = false,
} = options;
const isPreRender = renderPhase === "pre-render";
const lines = content.split("\n");
const result: string[] = [];
let inCodeBlock = false;
const topLevelTags: string[] = [];
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trimEnd();
let trimmedStart = line.trimStart();
if (CODE_FENCE.test(trimmedStart)) {
inCodeBlock = !inCodeBlock;
result.push(line);
continue;
}
if (inCodeBlock) {
result.push(line);
continue;
}
if (replaceAsciiSymbols) {
line = replaceCommonAsciiSymbols(line);
}
trimmedStart = line.trimStart();
const trimmed = line.trim();
const isOpeningXml = OPENING_XML.test(trimmedStart) && !trimmedStart.endsWith("/>");
if (isOpeningXml && line.length === trimmedStart.length) {
const match = OPENING_XML.exec(trimmedStart);
if (match) topLevelTags.push(match[1]);
}
const closingMatch = CLOSING_XML.exec(trimmedStart);
if (closingMatch) {
const tagName = closingMatch[1];
if (topLevelTags.length > 0 && topLevelTags[topLevelTags.length - 1] === tagName) {
topLevelTags.pop();
}
} else if (isPreRender && trimmedStart.startsWith("{{")) {
/* keep indentation as-is in pre-render for Handlebars markers */
} else if (TABLE_SEP.test(trimmedStart)) {
const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
line = `${leadingWhitespace}${compactTableSep(trimmedStart)}`;
} else if (TABLE_ROW.test(trimmedStart)) {
const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
line = `${leadingWhitespace}${compactTableRow(trimmedStart)}`;
}
if (shouldBoldRfc2119) {
line = boldRfc2119Keywords(line);
}
const isBlank = trimmed === "";
if (isBlank) {
const prevLine = result[result.length - 1]?.trim() ?? "";
const nextLine = lines[i + 1]?.trim() ?? "";
if (LIST_ITEM.test(nextLine)) {
continue;
}
if (OPENING_XML.test(prevLine) || (isPreRender && OPENING_HBS.test(prevLine))) {
continue;
}
if (CLOSING_XML.test(nextLine) || (isPreRender && CLOSING_HBS.test(nextLine))) {
continue;
}
const prevIsBlank = prevLine === "";
if (prevIsBlank) {
continue;
}
}
if (CLOSING_XML.test(trimmed) || (isPreRender && CLOSING_HBS.test(trimmed))) {
while (result.length > 0 && result[result.length - 1].trim() === "") {
result.pop();
}
}
result.push(line);
}
while (result.length > 0 && result[result.length - 1].trim() === "") {
result.pop();
}
return result.join("\n");
}
export interface TemplateContext extends Record<string, unknown> {
args?: string[];
ARGUMENTS?: string;
arguments?: string;
}
const handlebars = Handlebars.create();
handlebars.registerHelper("arg", function (this: TemplateContext, index: number | string): string {
const args = this.args ?? [];
const parsedIndex = typeof index === "number" ? index : Number.parseInt(index, 10);
if (!Number.isFinite(parsedIndex)) return "";
const zeroBased = parsedIndex - 1;
if (zeroBased < 0) return "";
return args[zeroBased] ?? "";
});
/**
* {{#list items prefix="- " suffix="" join="\n"}}{{this}}{{/list}}
* Renders an array with customizable prefix, suffix, and join separator.
* Note: Use \n in join for newlines (will be unescaped automatically).
*/
handlebars.registerHelper(
"list",
function (this: unknown, context: unknown[], options: Handlebars.HelperOptions): string {
if (!Array.isArray(context) || context.length === 0) return "";
const prefix = (options.hash.prefix as string) ?? "";
const suffix = (options.hash.suffix as string) ?? "";
const rawSeparator = (options.hash.join as string) ?? "\n";
const separator = rawSeparator.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
return context.map(item => `${prefix}${options.fn(item)}${suffix}`).join(separator);
},
);
/**
* {{join array ", "}}
* Joins an array with a separator (default: ", ").
*/
handlebars.registerHelper("join", (context: unknown[], separator?: unknown): string => {
if (!Array.isArray(context)) return "";
const sep = typeof separator === "string" ? separator : ", ";
return context.join(sep);
});
/**
* {{default value "fallback"}}
* Returns the value if truthy, otherwise returns the fallback.
*/
handlebars.registerHelper("default", (value: unknown, defaultValue: unknown): unknown => value || defaultValue);
/**
* {{pluralize count "item" "items"}}
* Returns "1 item" or "5 items" based on count.
*/
handlebars.registerHelper(
"pluralize",
(count: number, singular: string, plural: string): string => `${count} ${count === 1 ? singular : plural}`,
);
/**
* {{#when value "==" compare}}...{{else}}...{{/when}}
* Conditional block with comparison operators: ==, ===, !=, !==, >, <, >=, <=
*/
handlebars.registerHelper(
"when",
function (this: unknown, lhs: unknown, operator: string, rhs: unknown, options: Handlebars.HelperOptions): string {
const ops: Record<string, (a: unknown, b: unknown) => boolean> = {
"==": (a, b) => a === b,
"===": (a, b) => a === b,
"!=": (a, b) => a !== b,
"!==": (a, b) => a !== b,
">": (a, b) => (a as number) > (b as number),
"<": (a, b) => (a as number) < (b as number),
">=": (a, b) => (a as number) >= (b as number),
"<=": (a, b) => (a as number) <= (b as number),
};
const fn = ops[operator];
if (!fn) return options.inverse(this);
return fn(lhs, rhs) ? options.fn(this) : options.inverse(this);
},
);
/**
* {{#ifAny a b c}}...{{else}}...{{/ifAny}}
* True if any argument is truthy.
*/
handlebars.registerHelper("ifAny", function (this: unknown, ...args: unknown[]): string {
const options = args.pop() as Handlebars.HelperOptions;
return args.some(Boolean) ? options.fn(this) : options.inverse(this);
});
/**
* {{#ifAll a b c}}...{{else}}...{{/ifAll}}
* True if all arguments are truthy.
*/
handlebars.registerHelper("ifAll", function (this: unknown, ...args: unknown[]): string {
const options = args.pop() as Handlebars.HelperOptions;
return args.every(Boolean) ? options.fn(this) : options.inverse(this);
});
/**
* {{#table rows headers="Col1|Col2"}}{{col1}}|{{col2}}{{/table}}
* Generates a markdown table from an array of objects.
*/
handlebars.registerHelper(
"table",
function (this: unknown, context: unknown[], options: Handlebars.HelperOptions): string {
if (!Array.isArray(context) || context.length === 0) return "";
const headersStr = options.hash.headers as string | undefined;
const headers = headersStr?.split("|") ?? [];
const separator = headers.map(() => "---").join(" | ");
const headerRow = headers.length > 0 ? `| ${headers.join(" | ")} |\n| ${separator} |\n` : "";
const rows = context.map(item => `| ${options.fn(item).trim()} |`).join("\n");
return headerRow + rows;
},
);
/**
* {{#codeblock lang="diff"}}...{{/codeblock}}
* Wraps content in a fenced code block.
*/
handlebars.registerHelper("codeblock", function (this: unknown, options: Handlebars.HelperOptions): string {
const lang = (options.hash.lang as string) ?? "";
const content = options.fn(this).trim();
return `\`\`\`${lang}\n${content}\n\`\`\``;
});
/**
* {{#xml "tag"}}content{{/xml}}
* Wraps content in XML-style tags. Returns empty string if content is empty.
*/
handlebars.registerHelper("xml", function (this: unknown, tag: string, options: Handlebars.HelperOptions): string {
const content = options.fn(this).trim();
if (!content) return "";
return `<${tag}>\n${content}\n</${tag}>`;
});
/**
* {{escapeXml value}}
* Escapes XML special characters: & < > "
*/
handlebars.registerHelper("escapeXml", (value: unknown): string => {
if (value == null) return "";
return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
});
/**
* {{len array}}
* Returns the length of an array or string.
*/
handlebars.registerHelper("len", (value: unknown): number => {
if (Array.isArray(value)) return value.length;
if (typeof value === "string") return value.length;
return 0;
});
/**
* {{add a b}}
* Adds two numbers.
*/
handlebars.registerHelper("add", (a: number, b: number): number => (a ?? 0) + (b ?? 0));
/**
* {{sub a b}}
* Subtracts b from a.
*/
handlebars.registerHelper("sub", (a: number, b: number): number => (a ?? 0) - (b ?? 0));
/**
* {{#has collection item}}...{{else}}...{{/has}}
* Checks if an array includes an item or if a Set/Map has a key.
*/
handlebars.registerHelper(
"has",
function (this: unknown, collection: unknown, item: unknown, options: Handlebars.HelperOptions): string {
let found = false;
if (Array.isArray(collection)) {
found = collection.includes(item);
} else if (collection instanceof Set) {
found = collection.has(item);
} else if (collection instanceof Map) {
found = collection.has(item);
} else if (collection && typeof collection === "object") {
if (typeof item === "string" || typeof item === "number" || typeof item === "symbol") {
found = item in collection;
}
}
return found ? options.fn(this) : options.inverse(this);
},
);
/**
* {{includes array item}}
* Returns true if array includes item. For use in other helpers.
*/
handlebars.registerHelper("includes", (collection: unknown, item: unknown): boolean => {
if (Array.isArray(collection)) return collection.includes(item);
if (collection instanceof Set) return collection.has(item);
if (collection instanceof Map) return collection.has(item);
return false;
});
/**
* {{not value}}
* Returns logical NOT of value. For use in subexpressions.
*/
handlebars.registerHelper("not", (value: unknown): boolean => !value);
export function registerHelper(name: string, fn: HelperDelegate): void {
handlebars.registerHelper(name, fn);
}
export function registerPartial(name: string, fn: Template): void {
handlebars.registerPartial(name, fn);
}
export function compile(template: string): (context: TemplateContext) => string {
return handlebars.compile(template, { noEscape: true, strict: false });
}
export function render(template: string, context: TemplateContext = {}): string {
const compiled = compile(template);
const rendered = compiled(context ?? {});
return format(rendered, { renderPhase: "post-render" });
}