/** * Shared code-mode system-prompt augmentations (GS2-27). * * These notes describe capabilities of the agent itself — the opt-in `run_shell_command` tool and * the real filesystem cwd — rather than of any one graph builder, which is why they live in core * rather than beside the agent that composes them: a note composed inside one backend is a note * the next backend silently loses, the drift GS2-21 fixed for the main prompt. */ import type { McpServerInstruction } from '#src/core/types.js'; import { type FilesystemToolsConfig } from '#src/config/filesystem-tools.js'; /** * EXT-26: the platform-agnostic tail shared by both {@link appendOsShellNote} branches. * * The recurring failure mode on non-POSIX hosts is not just wrong command NAMES but shell * REDIRECTION quoting: a grouped/multi-line `echo` redirect on cmd.exe reported success yet wrote * a 0-byte file. So on every platform we steer file creation/mutation to the built-in * `write_file`/`edit_file` tools (which never touch the shell's quoting) and keep each shell * command a single line. Kept short — this is prompt text an LLM reads, not documentation. */ export declare const OS_SHELL_GUIDANCE: string; /** * EXT-26: append an OS + shell-dialect note to the composed code-mode system prompt. * * The model was never told its host OS or which shell `run_shell_command` uses, so on * non-POSIX hosts it defaulted to POSIX idioms that fail (ran `ls` where cmd.exe has `dir`, a * multi-line echo-redirect that wrote 0 bytes, a PowerShell here-string, `python -c` multi-line). * This is ORTHOGONAL to the EXT-13/16/22 path-namespace notes: those say WHERE the model is (path * form); this says WHAT shell it speaks (dialect). Backend-agnostic — the lean backend also exposes * `run_shell_command`, so GS2-27 composes it in the shared path. * * The shell is derived from the SAME rule Node's `spawn(command, { shell: true })` uses — exactly * how `run_shell_command` spawns (GthDevToolkit spawn) — so on `win32` it is cmd.exe (via * `%ComSpec%`) and on POSIX it is `/bin/sh` (POSIX sh, NOT guaranteed bash). Computed from * `process.platform` at call time so the text is correct per host. Returns the note alone when * there is no base prompt. A single injection is authoritative (nothing in the base prompt * contradicts shell dialect), so unlike EXT-22 no correction middleware is needed. */ export declare function appendOsShellNote(systemPrompt: string | undefined): string; /** * EXT-13 (part b): append a real-cwd / path-model note to the composed code-mode system prompt. * * Code mode runs in REAL-path mode, so the filesystem tools and `run_shell_command` share one * real-absolute-path namespace rooted at `cwd`. The shared `.gsloth.code.md` prompt already tells * the model "the current working directory is provided to you separately"; this note is what * provides it — without it the model assumes `/` is cwd and hands `/`-rooted paths to the real-fs * shell. The cwd is injected dynamically (never baked into the .md). Backend-agnostic — the lean * backend also runs real-fs code sessions and previously received NO cwd value, so GS2-27 composes * it in the shared path. Returns the note alone when there is no base prompt. */ export declare function appendCwdNote(systemPrompt: string | undefined, cwd: string): string; /** GS2-35 — the configured Git co-author identity (both fields optional; each defaults on its own). */ export interface CommitCoAuthor { name?: string; email?: string; } /** * GS2-35/EXT-83: append the commit-writing rules to the composed code-mode system prompt. * * Gaunt Sloth has **no dedicated git-commit tool** — the agent commits by calling * `run_shell_command` with `git commit`, composing the message (including any trailer) itself. That * leaves three things it must be told, and all three live here because all three are about * committing: * * 1. **WHO the co-author is.** Left unguided, models emit their own model name from trained habit, * which is factually wrong: the commit was produced by *Gaunt Sloth*, not by the model. The note * states the exact trailer to emit. EXT-83 — rather than enumerate model names not to write (a * denylist is stale the day a new vendor ships, and an enumeration beside a catch-all teaches the * model that the list is the rule), the correct name is SUPPLIED: the resolved * {@link ResolvedModelIdentity} decorates the DEFAULT name as `Gaunt Sloth (provider:model)`, so * the real model is named while the authorship stays Gaunt Sloth's. * 2. **HOW the message reaches git.** A commit message passed inline in a double-quoted shell * argument is EXPANDED BY THE SHELL before git runs, so a message that quotes code the way * ordinary technical prose does is executed as a command. The note states that mechanism rather * than merely forbidding the construct — naming a construct without its mechanism has been * measured not to work. A file path carries no shell metacharacters, so the file form removes the * failure mode instead of asking the model to avoid it. * 3. **HOW the change is staged.** Rule 2 leaves the message file untracked in the working tree at * `git add` time, so an unscoped `git add -A` / `git add .` / `git commit -a` sweeps it into the * commit and onto the pull request. The note states that mechanism, steers staging to named * paths, and allows the last resort for a change too large to enumerate only once the message * file is out of the tree — an escape hatch that kept the file would reintroduce exactly the * defect the rule closes. It binds at `git add` time, MID-flow while the model is still acting, * because the tidy-up step at the end is the one that gets dropped. The scratchpad sentence * beside it names NO path: the write tool refuses any path outside the working folder at every * approval mode, so naming one buys a refused call. EXT-97 is the interim mitigation; the * mechanism that owns the file end to end is EXT-93. * * **The clause that names the writing tool is gated on `filesystem`** (EXT-84), through the one * shared derivation {@link isWriteFileToolRegistered} — the same interpretation that decides which * tools are actually registered. Naming an unregistered tool while forbidding both the shell * fallback and the inline flag leaves the model NO compliant path, and its likeliest recovery is * the inline form this note exists to prevent. So when the write tool is registered the note names * it literally (the overwhelmingly common case, and a literal name is what makes the instruction * actionable); when it is not, the note names no tool, keeps both prohibitions and the file form, * and supplies the compliant path that remains: do not commit, and hand the message back to the * user. That branch states no availability claim of its own — registering filesystem tools is the * agent's decision, made from the same `filesystem` value, so a note asserting "you have no * file-writing tool" could be flatly false. * * The note's prose carries **no backtick and no other markup** — including no angle-bracket * placeholder: it is the one piece of guidance whose subject is how to write a commit message, so * quoting its own examples in backticks would demonstrate the exact style rule 2 exists to stop, and * an angle-bracket placeholder copied literally is itself a shell input redirect. The `` of * the trailer line is the exception the RFC form requires, and is scoped out of the scan. * * The identity is config-driven (`commit.coAuthor` in `GthConfig`). * Each field falls back INDEPENDENTLY to the Gaunt Sloth account * ({@link DEFAULT_COMMIT_CO_AUTHOR_NAME} / {@link DEFAULT_COMMIT_CO_AUTHOR_EMAIL}) — so a partial * override (name only, or a config that bypassed the loader) still yields a complete trailer, and a * fully-absent config yields the default account. Blank/whitespace values are treated as unset. An * EXPLICITLY CONFIGURED name is emitted verbatim, with no identity spliced in — the user asked for * that string; the identity decorates only the default. An unresolvable identity (`undefined`) falls * back to the plain default name, never to a placeholder. * * Backend-agnostic: composed through the shared code path, so any backend injects it (the * git-commit capability rides on `run_shell_command`, exposed in code mode). Returns the note alone * when there is no base prompt. */ export declare function appendCommitCoAuthorNote(systemPrompt: string | undefined, coAuthor?: CommitCoAuthor, modelIdentity?: ResolvedModelIdentity, filesystem?: FilesystemToolsConfig): string; /** * GS2-34/GS2-53 — the resolved active-model identity, as a STRUCTURED value. * * `hasProvider` is the authoritative "a real provider half was resolved" signal — carried * explicitly rather than inferred from `identity.includes(':')`, because a bare model name can * itself contain a colon (e.g. an Ollama/HF tag `gemma3:27b`) and would otherwise be mistaken for a * `provider:model` string. It is true iff a non-empty provider (configured `type` OR a non-empty * `_llmType()`) formed the leading `provider:` segment; false when `identity` is the bare model. */ export interface ResolvedModelIdentity { /** `provider:model` when a provider resolved, otherwise the bare `model`. */ identity: string; /** True iff a real provider formed the `provider:` segment (never merely a colon in the model). */ hasProvider: boolean; } /** * GS2-34: resolve the active model identity from the effective config, for injection into the * system prompt by {@link appendModelContextNote}. * * Both halves are read the SAME way the rest of gsloth already surfaces the active model: * - MODEL: `config.modelDisplayName` (the string the status line renders — set by the loader from * `llm.model`), falling back to the live model's own `model` field. * - PROVIDER: the configured `config.modelProviderType` (the raw `llm.type` the loader stashed — * `openrouter`/`deepseek`/`xai`/`anthropic`/…) when present, otherwise the live LangChain * model's `_llmType()` (the source the AG-UI `/info` endpoint reports). GS2-53 — preferring the * configured `type` is what stops a provider being labelled with the client it happens to be * built on: `_llmType()` is the model CLASS's own label and does not track the gth provider * namespace, so `huggingface` (a `ChatOpenAI` aimed at the HF router) reports `openai`, and * `google-genai` and `vertexai` both report `google`. It is also the safer source in general: * `_llmType()` is the model class's opinion, while the `type` is * the user's own declaration, so a provider swapped onto a different client (as `openrouter` * was) keeps its configured label without this file needing to know. The `type` is absent for * module configs (which * hand us an already-built LLM), where we fall back to `_llmType()` unchanged. The MODEL half is * always exact. * * Returns `{ identity: 'provider:model', hasProvider: true }` when both resolve, `{ identity: * 'model', hasProvider: false }` when only the model resolves, and `undefined` when the model is * unknown (a provider with no model is not a usable identity) — in which case {@link * appendModelContextNote} injects nothing, leaving the prompt exactly as before. `hasProvider` is * the authoritative provider-present flag (GS2-53), so a bare model whose NAME contains a colon * (e.g. `gemma3:27b`) is correctly reported as `hasProvider: false`. `_llmType()` is called * defensively (guarded) so a provider whose accessor throws can never break prompt assembly (and is * skipped entirely when a configured `type` is present). */ export declare function resolveModelIdentity(config: { llm?: { _llmType?: () => string; model?: string; }; modelDisplayName?: string; modelProviderType?: string; } | null | undefined): ResolvedModelIdentity | undefined; /** * GS2-34: append the active model-identity note to the composed system prompt. * * The agent otherwise has no reliable knowledge of which `provider:model` is serving it, so it * cannot answer "what model are you?" accurately or reason about its own capabilities/limits. This * injects a single first-party line naming the resolved identity (see {@link resolveModelIdentity}). * * Injected in EVERY mode (chat/ask/code/exec), NOT gated on the resolved toolset like the * cwd/os-shell/commit notes: "which model are you?" can be asked in any session, so the identity * must be visible everywhere. Config-gated by `injectModelContext` (default ON): a caller passes * an `undefined` `modelIdentity` — because the config opted out (`injectModelContext: false`) or * because no model could be resolved — and the base prompt is returned UNCHANGED (no line), * preserving the current prompt byte-for-byte. * * A short capability note from the GS2-6 model catalog is a DEFERRED follow-up (GS2-6 has not * landed): this injects the bare `provider:model` identity only. */ export declare function appendModelContextNote(systemPrompt: string | undefined, modelIdentity: ResolvedModelIdentity | undefined): string | undefined; /** * EXT-32: hard per-server cap on injected MCP instruction length. * * A connected MCP server could advertise a very long `instructions` string that would then ride * along in EVERY turn's system prompt and bloat context. This is a simple defensive constant (no * config-schema plumbing — that stays out of this node): text beyond the cap is dropped and a * truncation marker is appended so the model knows it was clipped. Generous enough that realistic * server instructions pass through untouched. */ export declare const MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER = 4000; /** EXT-32: truncation marker appended when a server's instructions exceed the per-server cap. */ export declare const MCP_INSTRUCTIONS_TRUNCATION_MARKER = "\u2026 [truncated]"; /** * EXT-32: append connected MCP servers' discovery `instructions` to the composed system prompt. * * Each connected MCP server may return an `instructions` string in its `initialize` handshake that * describes how to use its tools. We surface those to the model — but the text is **server-supplied * and therefore a prompt-injection surface**, so it is: * - fenced in an explicit `[BEGIN/END MCP SERVER-PROVIDED CONTEXT]` block (never blended into * first-party prompt text), * - labelled per server (`--- Server: "name" ---`) so the model can attribute each block, and * - bracketed by a leading framing line AND a trailing first-party reassertion, so the LAST thing * the model reads is gsloth's own authority, not the server text — server instructions may not * override the system instructions, safety rules, or the user's directives. * * Backend-agnostic: composed through the shared path, so any backend injects it. * Empty/absent contributes NOTHING: when no server supplied * (non-whitespace) instructions the base prompt is returned unchanged — no empty header, no dangling * label. Each server's text is trimmed and capped at {@link MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER}. * Returns the block alone when there is no base prompt. */ export declare function appendMcpServerInstructionsNote(systemPrompt: string | undefined, instructions: McpServerInstruction[] | undefined): string | undefined;