8.6 KiB
Plan: Unify Template Variables with Tools
Problem
The soul template system (---template--- in kind 31120) uses {{variable}} placeholders that are resolved by a hardcoded C function (agent_template_resolve_var()). This is a parallel data-fetching system alongside the existing tool registry. Every new context data source requires:
- Adding a C function to produce the data
- Adding an
if (strcmp(...))branch in the resolver - Documenting the new variable name
- Hoping the template author uses the exact right name
This caused the bug that started this investigation — 5 out of 9 template variables were misspelled/mismatched, producing empty context sections and a provider rejection.
Goal
Eliminate template variables entirely. Template sections fetch their data by calling tools. The tool registry is the single mechanism for getting data into context.
Design
New Template Syntax
Replace content: | with tool: and optional args: directives:
---template---
- section: admin_identity
role: system
tool: admin_identity
skip_if_empty: true
- section: admin_profile
role: system
tool: nostr_admin_profile
skip_if_empty: true
- section: admin_contacts
role: system
tool: nostr_admin_contacts
skip_if_empty: true
- section: admin_relays
role: system
tool: nostr_admin_relays
skip_if_empty: true
- section: admin_notes
role: system
tool: nostr_admin_notes
skip_if_empty: true
- section: tools
role: system
tool: tool_list
skip_if_empty: true
- section: tasks
role: system
tool: task_list
skip_if_empty: true
- section: dm_history
role: expand
limit: 12
- section: conversation
role: user
tool: message_current
skip_if_empty: true
A section can still use the old content: with {{variables}} for static text or mixed content. But the primary mechanism for dynamic data is tool:.
How It Works
flowchart TD
DM[Incoming DM] --> BUILD[Build context from template]
BUILD --> SOUL[Emit personality as system msg]
SOUL --> LOOP[For each template section]
LOOP --> CHECK{Has tool: directive?}
CHECK -->|Yes| EXEC["tools_execute(tool_name, args)"]
EXEC --> FORMAT[Extract content from JSON result]
FORMAT --> EMIT[Emit as chat message]
CHECK -->|No| RESOLVE["Resolve {{vars}} from content template<br/>(legacy path, eventually removed)"]
RESOLVE --> EMIT
EMIT --> LOOP
LOOP --> DONE[Complete messages array]
New Context Tools
Add lightweight "context tools" that read from cached in-memory data. These are fast (no network), deterministic, and use the same tools_execute() interface as everything else.
| Tool | Returns | Source |
|---|---|---|
admin_identity |
Admin pubkey + verification text | config + sender tier |
nostr_admin_profile |
Admin kind 0 profile JSON | cached g_admin_kind0_json |
nostr_admin_contacts |
Admin kind 3 contacts JSON array | cached g_admin_wot_contacts |
nostr_admin_relays |
Admin kind 10002 relay list JSON | cached g_admin_kind10002_json |
nostr_admin_notes |
Admin recent kind 1 notes | cached g_admin_kind1_notes |
task_list |
Current task list from tasks.json | file read (already exists as task_manage with action: list) |
message_current |
Current user message text | passed via tool context |
agent_identity |
Agent pubkey, npub | config |
Existing tools that already work for context:
tool_list— returns tool schemas (already exists)task_managewith{"action":"list"}— returns tasks (already exists)local_file_read— can read any file (already exists)
Tool Result → Message Content
Tool results are JSON objects like {"success":true,"content":"..."}. The template system extracts the content field (or a configurable field) as the message text. If the tool returns an error or empty content, and skip_if_empty: true is set, the section is omitted.
For tools that return structured data (like tool_list returning a schema array), the template can specify a result_field or format to control extraction:
- section: tools
role: system
tool: tool_list
result_field: tools
skip_if_empty: true
Implementation Phases
Phase 1: Add context tools + tool: directive support
- Add new
context_*tools totools.cthat wrap the existing cached data getters - Extend
prompt_template_section_twithtool_nameandtool_argsfields - Extend
prompt_template_parse()to recognizetool:andargs:directives - Extend
prompt_template_build_messages()to calltools_execute()when a section hastool_nameset - Update the soul template in
config.jsoncto usetool:directives - Keep
content:+{{variable}}working as a fallback for backward compatibility
Phase 2: Migrate all sections to tool-based
- Convert all template sections from
content: {{variable}}totool:directives - Verify all context data flows through tools
- Update
context_template.mdto reflect new syntax
Phase 3: Remove legacy variable resolver
- Remove
agent_template_resolve_var()and all its helper functions that are now redundant - Remove
prompt_var_resolver_fncallback fromprompt_template_build_messages() - Clean up dead code in
agent.c - Update all documentation
Template Section Struct Changes
typedef struct {
char name[PROMPT_TEMPLATE_MAX_NAME_LEN];
char role[PROMPT_TEMPLATE_MAX_ROLE_LEN];
char* content_template; // legacy: {{variable}} content
char* tool_name; // NEW: tool to call for content
char* tool_args; // NEW: JSON args for tool call
char* result_field; // NEW: which JSON field to extract (default: "content")
int limit;
int skip_if_empty;
char* provider_name;
char* provider_content_template;
} prompt_template_section_t;
Soul Template Example (After Migration)
# Didactyl Agent
You are Didactyl, a sovereign AI agent living on Nostr.
...
---template---
- section: admin_identity
role: system
tool: admin_identity
skip_if_empty: true
- section: admin_profile
role: system
tool: nostr_admin_profile
skip_if_empty: true
- section: admin_contacts
role: system
tool: nostr_admin_contacts
skip_if_empty: true
- section: admin_relays
role: system
tool: nostr_admin_relays
skip_if_empty: true
- section: admin_notes
role: system
tool: nostr_admin_notes
skip_if_empty: true
- section: tools
role: system
tool: tool_list
result_field: tools
skip_if_empty: true
- section: tasks
role: system
tool: task_manage
args: {"action":"list"}
result_field: tasks
skip_if_empty: true
- section: dm_history
role: expand
limit: 12
- section: conversation
role: user
tool: message_current
skip_if_empty: true
What Gets Deleted Eventually
agent_template_resolve_var()and all itsif (strcmp(...))branchesbuild_tool_schemas_json_string()build_tasks_content_string()(replaced bytask_managetool)build_admin_recent_posts_text()(replaced bynostr_admin_notestool)build_admin_profile_plain_text()(replaced bynostr_admin_profiletool)build_admin_relay_list_plain_text()(replaced bynostr_admin_relaystool)build_sender_verification_text()(folded intoadmin_identitytool)build_startup_events_json_string()(replaced by tool if needed)build_adopted_skills_payload_string()(replaced by tool if needed)nostr_handler_get_admin_kind3_context()(just added, will be replaced by tool)- The
prompt_var_resolver_fncallback type
Benefits
- Single data-fetching mechanism — tools are the only way to get data
- No more variable name mismatches — tool names are validated at registration
- User-configurable context — admin can add any tool output to context by editing the soul template
- Testable —
--test-tool nostr_admin_profileshows exactly what goes into context - Self-documenting —
tool_listshows all available context tools with descriptions - Extensible — new tools automatically become available as context sources
- Provider overrides still work —
provider:directive can still override formatting per-provider
Risks
- Performance — Tool calls add function dispatch overhead vs direct variable resolution. Mitigated: context tools read cached data, no network calls.
- Backward compatibility — Old soul templates with
{{variables}}would break. Mitigated: Phase 1 keeps both paths working. - Tool context threading —
tools_execute()needs access to the tools context, whichprompt_template_build_messages()doesn't currently have. Solution: passtools_context_t*to the build function.