import { RunnableConfig } from '@langchain/core/runnables'; import { GthConfig, PromptSegmentName, ScopedPromptsEntry } from '#src/config.js'; import type { GthCommand } from '#src/core/types.js'; import { SystemMessage } from '@langchain/core/messages'; /** * Creates new runnable config. * configurable.thread_id is an important part of that because it helps to distinguish different chat sessions. * We normally do not have multiple sessions in the terminal, but I had bad stuff happening in tests * and in another prototype project where I was importing Gaunt Sloth. */ export declare function getNewRunnableConfig(recursionLimit?: number): RunnableConfig; /** The config slice every prompt-segment read needs. */ type PromptReadConfig = Pick; /** * CFG-70 — the byte total above which a run warns that its scoped overlays have become a large * prompt in their own right. A **warning, never a cap**: nothing is truncated, because a guideline * cut mid-sentence is worse than a long prompt — the model then follows a rule whose exception was * removed. * * Where 64 KiB comes from, since a threshold nobody can argue with is a threshold nobody trusts: * * - **Measured floor.** Every prompt file gaunt-sloth ships — all seven bundled segments together * — is under 7 KB. This is an order of magnitude above the whole built-in prompt, so a user is * well past "a few per-module guideline files" before it says anything. * - **Context-window ceiling.** At the customary ~4 bytes per token this is roughly 16k tokens, * about an eighth of a 128k-token window — the smallest among the models `review` and `pr` * routinely run on — and the diff, the requirements and the model's own answer still have to fit * beside it. * * The cost of firing slightly early is one line of output, which is why the estimate is allowed to * be an estimate. */ export declare const SCOPED_PROMPT_BUDGET_BYTES = 65536; /** What the selected scoped entries cost, as {@link measureScopedPrompts} reports it. */ export interface ScopedPromptBudget { /** Total bytes of every scoped segment file the entries resolve to. */ totalBytes: number; /** Entries contributing at least one resolvable file, in config order. */ entryNames: string[]; /** Whether {@link totalBytes} is over {@link SCOPED_PROMPT_BUDGET_BYTES}. */ overBudget: boolean; } /** * CFG-70 — where one scoped entry's segment path lands, and whether anything is there. * * It reports the resolved path on **both** outcomes, because the caller that has to warn about a * missing file needs to name where it looked — a user who mistyped a path learns nothing from * being told their own string back. * * **Deliberately not {@link readPromptFile}**, which is the function every *root* segment goes * through. `readPromptFile` falls back to the installed package directory when the project file is * absent, and for a scoped path that fallback is wrong in both of its outcomes: a bundled default * of the same name would attach content the user never asked for to a module it has nothing to do * with, and the ordinary case — no such file in the package either — throws an ENOENT naming a * path inside `node_modules`, which tells a user with a typo in their config nothing they can act * on. A scoped path has no bundled default by construction: it names a file the user wrote. * * The one thing it shares with `readPromptFile` is {@link getGslothConfigReadPath}, so an identity * profile retargets a scoped path exactly as it retargets a root one. */ export declare function resolveScopedSegmentPath(path: string, identityProfile: string | undefined): { resolved: string; exists: boolean; }; /** * CFG-70 — what the selected scoped entries will cost, in bytes of file content. * * Exported so exactly one site owns the resolution-and-sizing walk: `review()` decides whether to * warn, and does not re-derive where a scoped file lives. Unresolvable paths contribute nothing * and are not reported here — {@link readPromptSegment} is where a user hears about those, once * per segment that actually wanted the file. */ export declare function measureScopedPrompts(entries: readonly ScopedPromptsEntry[], config: Pick): ScopedPromptBudget; /** * GS2-43 — the single composition point for one prompt segment, honouring the segment's * `prompts.` config: * - no setting → the segment's default-named file (`PROMPT_SEGMENT_FILES`), falling back * to the bundled default unless `noDefaultPrompts` — exactly the pre-GS2-43 behaviour; * - a `string` → shorthand for `{ path }`; * - `enabled: false` → the segment is dropped entirely (returns `''`), even its bundled default; * - `path` + `mode: 'replace'` (default) → the file replaces the built-in segment content; * - `path` + `mode: 'append'` → the file content is appended after the built-in content. * * CFG-70 — and then the run's selected path-scoped overlays are appended, for every one of the * seven segments and therefore for every caller. This is the seam because it is the sole reader of * `config.prompts?.[segment]`: `readGuidelines`, `readReviewInstructions`, `readSystemPrompt`, * `readChatPrompt`, `readCodePrompt`, `readExecPrompt` and `readBackstory` are all thin wrappers, * so one edit here reaches `buildSystemMessages`, `getReviewPreamble` and the `gth get * system-prompt` introspection alike, with no site left to forget. * * **The scoped overlay survives `enabled: false`, deliberately.** A disabled root segment plus * matched scoped entries composes to the module blocks ALONE. Read against `enabled`'s own * docstring above — "the segment is dropped entirely" — that looks like a bug, and it is not: * `enabled` turns off the *repository-wide* segment, which is a coherent and useful monorepo * config alongside `prompts.paths` ("no guidelines for the repo, only per-module ones"). Honouring * it the other way would silently discard entries the user wrote explicitly, and force that user * to point `guidelines` at an empty file to express what they already expressed. Never discarding * configuration someone wrote on purpose is the principle the whole feature is built on. * * **`.filter(Boolean)` is load-bearing, not tidiness.** With no scoped entries selected — which is * every run today, and every run of a project that does not use the feature — all four branches * must compose byte-identically to what they composed before this existed. A plain * `[base, scoped].join('\n')` appends a trailing newline when `scoped` is empty; the filter is * what makes the unset path an exact identity rather than an almost-identity. */ export declare function readPromptSegment(segment: PromptSegmentName, config: PromptReadConfig): string; export declare function readBackstory(config: PromptReadConfig): string; export declare function readGuidelines(config: Pick | string): string; export declare function readReviewInstructions(config: PromptReadConfig | string): string; export declare function readSystemPrompt(config: PromptReadConfig): string; export declare function readChatPrompt(config: PromptReadConfig): string; export declare function buildSystemMessages(config: GthConfig, modePrompt?: string | null): SystemMessage[]; export declare function readCodePrompt(config: PromptReadConfig): string; export declare function readExecPrompt(config: PromptReadConfig): string; /** * GS2-79 — the SINGLE place a command's mode prompt is chosen, for every site that composes a * system prompt via {@link buildSystemMessages}: the agent and the subagent profiles. * * It exists because the selection used to be an inline three-branch ternary copied to each of * those sites, and a command missing from one copy is silently served the CHAT prompt — the * default branch — rather than failing. That is how `review`/`pr` came to compose the chat prompt * while the review instructions were smuggled in as a caller-side leading `SystemMessage`, which * Anthropic rejects outright ("System messages are only permitted as the first passed message"). * One function means a new command is wired once, and `review`/`pr` cannot silently fall back to * chat again. * * `review`/`pr` resolve to the REVIEW INSTRUCTIONS (`.gsloth.review.md`), which is what makes a * review a review; every command without a mode prompt of its own keeps the chat prompt. */ export declare function readModePrompt(command: GthCommand | undefined, config: PromptReadConfig): string; /** * Read a prompt file from the project config dir (honouring identity profiles), falling back * to a packaged default unless `noDefaultPrompts` is set. Downstream packages owning their own * prompt files (e.g. the assistant's PR discovery prompt) pass `defaultPromptDir` pointing at * their package root; when omitted, the default is read from the core package. */ export declare function readPromptFile(filename: string, identityProfile: string | undefined, noDefaultPrompts?: boolean, defaultPromptDir?: string): string; /** * Wraps content within randomized block */ export declare function wrapContent(content: string, wrapBlockPrefix?: string, prefix?: string, alwaysWrap?: boolean): string; /** * Utility function to execute hook(s) - either a single hook or an array of hooks * Fully type-safe and works with any number of arguments * @param hooks - Single hook function or array of hook functions (or undefined) * @param args - Arguments to pass to each hook function */ export declare function executeHooks Promise>(hooks: T | T[] | undefined, ...args: Parameters): Promise; /** * Format tool call arguments in a human-readable way */ export declare function formatToolCallArgs(args: Record): string; /** * Format multiple tool calls for display (matches Invocation.ts behavior) */ export declare function formatToolCalls(toolCalls: Array<{ name: string; args?: Record; }>, maxLength?: number): string; export {};