Designing a System Prompt Pipeline and Runtime Todo Reminders for an AI Programming Agent
This document details a language‑agnostic design for generating system prompts via a structured data‑to‑template pipeline and introduces two runtime reminder mechanisms—TodoNudge and TodoGate—that help an AI programming agent keep its todo list up to date during interaction.
Overall Architecture
The system prompt is generated at agent startup by a structured‑data → template‑rendering pipeline. The pipeline consists of three stages:
Generate a placeholder JSON object from environment variables and configuration fields.
Select a base template (Primary or Subagent) according to the prompt_mode (Extend or Full) and the system_prompt override option, then render it with a Jinja2‑compatible engine that uses custom delimiters.
If the mode is Extend and a custom body is provided, render the body and append it to the rendered base template.
The final string is returned as the system prompt.
Prompt Context Structure
The context is a serializable data structure (JSON/YAML) that contains all inputs required to render the prompt. Core fields are: version (integer) – structure version for forward compatibility. prompt_mode (enum) – "Extend" (default) or "Full". audience (enum) – "Primary" (main Agent) or "Subagent". memory_enabled (boolean) – whether the memory subsystem is active. system_prompt_label (string) – label inserted at the beginning of the prompt (default "Grok").
Environment variables such as os_name, shell_path, working_directory, current_date, and is_non_interactive.
Prompt Rendering Modes
Extend – Default. Render the selected base template, then render the optional custom body and concatenate the two.
Full – Render the custom body as the complete system prompt; no base template is used.
Template Override Options
None – Use the standard base template (Primary or Subagent version).
Codex – Use a dedicated apply‑patch template that is decrypted on demand and cleared after use.
Custom – Use a caller‑provided template string.
Runtime Reminder Mechanisms
TodoNudge (soft reminder)
A periodic reminder injected when the model goes several turns without invoking any todo‑related tool. enabled (bool) – default true. turns_since_todo_write (int) – number of turns after the last todo write before triggering (default 3). turns_between_reminders (int) – minimum interval between successive reminders (default 5).
In the current version the trigger logic has been removed, but the configuration is retained for future use.
TodoGate (hard check)
At the end of each turn, if the model has not called a tool and there remain unfinished todos, TodoGate forces a reminder and a new turn. enabled (bool) – default false (must be explicitly turned on). max_fires_per_prompt (int) – maximum number of times the gate may fire for a single user prompt (default 2).
TodoGate distinguishes between "backed" in‑progress todos (those with a corresponding background task) and "unbacked" in‑progress todos. Only unbacked in‑progress items and pending items trigger a reminder, preventing false alarms for work that is already being processed.
Decision logic:
if pending is empty and in_progress_unbacked is empty:
Continue (allow turn to end)
else:
Nudge (inject reminder and force continuation)When the fire count reaches max_fires_per_prompt, the gate stops injecting reminders and emits a notice that the limit has been reached.
Injection Format
All reminders are wrapped in a simple XML‑like tag:
<system-reminder>
{reminder content}
</system-reminder>The model treats the content as system‑level context rather than user input.
Template Placeholder System
Custom Delimiters
The engine uses Jinja2 syntax but replaces the standard {{ }} delimiters with:
Variable substitution – ${{ }} (e.g., ${{ tools.by_kind.read }}).
Conditional blocks – ${% %} (e.g., ${%- if tools.by_kind.plan %}…${%- endif %}).
Comments – ${# #} (e.g., ${# this comment is omitted #}).
Tool‑Name Mappings (First Placeholder Category)
Templates reference tool names via tools.by_kind.<category>. The mapping is resolved at render time. Example mappings: ${{ tools.by_kind.read }} →
"read_file" ${{ tools.by_kind.plan }}→
"todo_write" ${{ tools.by_kind.execute }}→ "run_terminal_command" If a tool category is not present in the current tool set, the surrounding conditional block evaluates to false and the content is omitted, allowing the template to adapt dynamically.
Environment & Identity Variables (Second Placeholder Category)
These variables are provided directly in the top‑level context and can be referenced without a namespace: os_name (string) – e.g.,
"macos" shell_path(string) – e.g.,
"/bin/zsh" working_directory(string) – e.g.,
"/Users/example/project" current_date(string) – e.g.,
"2025-07-16" memory_enabled(bool) – e.g.,
false is_non_interactive(bool) – false (default) or true for autonomous‑agent mode. system_prompt_label (string) – e.g.,
"Grok"Context Merging Rules
The engine first serializes its internal tools and params namespaces, then inserts the environment variable key‑value pairs at the top level. Namespaces do not clash because tool mappings reside under tools while environment variables are top‑level keys.
Template Protection Mechanism
Every tool‑name variable reference ${{ tools.by_kind.X }} must be wrapped in a conditional block ${%- if tools.by_kind.X %} … ${%- endif %}. This static check prevents empty strings when a tool is not registered. Built‑in tool categories (e.g., search) are exempt.
Three Base Templates
Primary Agent Template – Full interactive session, includes tool usage rules, role instructions, and user guide.
Subagent Template – Trimmed version for lightweight sub‑agents; role information is omitted.
Apply‑Patch Template – Dedicated template for patch‑apply scenarios; stored in an obfuscated file, decrypted on demand, and cleared from memory after use.
Audience Differentiation
The audience field selects between Primary and Subagent processing paths:
Primary – Uses the full Primary template, injects role instructions, and retains role summaries.
Subagent – Uses the Subagent template, skips role injection, and clears role summaries during persistence.
Non‑Interactive Mode
The is_non_interactive flag controls rendering of UI‑specific sections. When true, user‑guide blocks and shell prompts are omitted, declaring the agent as an autonomous system suitable for SDKs, stdio pipelines, or other automation contexts.
Key Design Decisions
Separate data from rendering – the prompt context is pure data; rendering is performed by an isolated template engine.
Custom delimiters – avoid conflicts with tool descriptions that frequently contain {{ }}.
Dynamic tool‑name resolution – tools.by_kind mappings enable the same template to work with different tool sets.
Conditional blocks protect against missing tools by omitting their placeholders.
Audience (Primary vs Subagent) influences template choice, role injection, and persistence handling.
TodoNudge provides a soft, periodic nudge; TodoGate provides a hard check that can be disabled by default to limit extra inference cost.
TodoGate’s "backed/unbacked" partition prevents unnecessary reminders for tasks already supported by background processes.
A hard fire‑count limit (default 2) caps the worst‑case extra computation.
Removal of the task‑completion discipline block from templates leaves TodoGate’s condition always false, preserving compatibility for future reinstatement.
References
xai‑org/grok‑build: https://github.com/xai-org/grok-build
agiwo: https://github.com/xhwSkhizein/agiwo
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
o-ai.tech
I’ll keep you updated with the latest AI news and tech developments in real time—let’s embrace AI together!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
