253 lines
8.6 KiB
Markdown
253 lines
8.6 KiB
Markdown
# 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:
|
|
|
|
1. Adding a C function to produce the data
|
|
2. Adding an `if (strcmp(...))` branch in the resolver
|
|
3. Documenting the new variable name
|
|
4. 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:
|
|
|
|
```yaml
|
|
---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
|
|
|
|
```mermaid
|
|
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_manage` with `{"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:
|
|
|
|
```yaml
|
|
- section: tools
|
|
role: system
|
|
tool: tool_list
|
|
result_field: tools
|
|
skip_if_empty: true
|
|
```
|
|
|
|
### Implementation Phases
|
|
|
|
#### Phase 1: Add context tools + tool: directive support
|
|
|
|
1. Add new `context_*` tools to `tools.c` that wrap the existing cached data getters
|
|
2. Extend `prompt_template_section_t` with `tool_name` and `tool_args` fields
|
|
3. Extend `prompt_template_parse()` to recognize `tool:` and `args:` directives
|
|
4. Extend `prompt_template_build_messages()` to call `tools_execute()` when a section has `tool_name` set
|
|
5. Update the soul template in `config.jsonc` to use `tool:` directives
|
|
6. Keep `content:` + `{{variable}}` working as a fallback for backward compatibility
|
|
|
|
#### Phase 2: Migrate all sections to tool-based
|
|
|
|
1. Convert all template sections from `content: {{variable}}` to `tool:` directives
|
|
2. Verify all context data flows through tools
|
|
3. Update `context_template.md` to reflect new syntax
|
|
|
|
#### Phase 3: Remove legacy variable resolver
|
|
|
|
1. Remove `agent_template_resolve_var()` and all its helper functions that are now redundant
|
|
2. Remove `prompt_var_resolver_fn` callback from `prompt_template_build_messages()`
|
|
3. Clean up dead code in `agent.c`
|
|
4. Update all documentation
|
|
|
|
### Template Section Struct Changes
|
|
|
|
```c
|
|
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 its `if (strcmp(...))` branches
|
|
- `build_tool_schemas_json_string()`
|
|
- `build_tasks_content_string()` (replaced by `task_manage` tool)
|
|
- `build_admin_recent_posts_text()` (replaced by `nostr_admin_notes` tool)
|
|
- `build_admin_profile_plain_text()` (replaced by `nostr_admin_profile` tool)
|
|
- `build_admin_relay_list_plain_text()` (replaced by `nostr_admin_relays` tool)
|
|
- `build_sender_verification_text()` (folded into `admin_identity` tool)
|
|
- `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_fn` callback type
|
|
|
|
### Benefits
|
|
|
|
1. **Single data-fetching mechanism** — tools are the only way to get data
|
|
2. **No more variable name mismatches** — tool names are validated at registration
|
|
3. **User-configurable context** — admin can add any tool output to context by editing the soul template
|
|
4. **Testable** — `--test-tool nostr_admin_profile` shows exactly what goes into context
|
|
5. **Self-documenting** — `tool_list` shows all available context tools with descriptions
|
|
6. **Extensible** — new tools automatically become available as context sources
|
|
7. **Provider overrides still work** — `provider:` directive can still override formatting per-provider
|
|
|
|
### Risks
|
|
|
|
1. **Performance** — Tool calls add function dispatch overhead vs direct variable resolution. Mitigated: context tools read cached data, no network calls.
|
|
2. **Backward compatibility** — Old soul templates with `{{variables}}` would break. Mitigated: Phase 1 keeps both paths working.
|
|
3. **Tool context threading** — `tools_execute()` needs access to the tools context, which `prompt_template_build_messages()` doesn't currently have. Solution: pass `tools_context_t*` to the build function.
|