Files
didactyl/plans/skill_driven_architecture.md
T

316 lines
12 KiB
Markdown

# Skill-Driven Architecture — Implementation Plan
## Summary
Overhaul Didactyl so that **skills are the universal unit of behavior** and **Nostr is the source of truth for all agent state**. The agent boots from a single nsec; everything else — context template, LLM config, relay list, personality — lives on Nostr as skills or encrypted events.
---
## Architecture Decisions (Finalized)
| Decision | Detail |
|----------|--------|
| Genesis config | `genesis.jsonc` is consumed on first run only; can be deleted afterward |
| Runtime identity seed | nsec (via CLI flag or systemd credential) is the only required input after genesis |
| Context template | Becomes the `didactyl_default` skill — first entry in kind 10123 adoption list |
| Context modes | Removed. `inject`/`full`/`override` replaced by adoption-list-order composition |
| Execution params | `llm`, `temperature`, `max_tokens`, `seed`, `tools` move from skill content to trigger tags |
| DM trigger type | New trigger type `dm` — makes DM handling consistent with all other triggers |
| LLM config storage | Kind 30078 `d=llm_config`, NIP-44 encrypted to self, stored on Nostr |
| Admin config storage | Admin pubkey stored as tag on soul event or dedicated config event on Nostr |
| Bootstrap relays | Listed in `genesis.jsonc`; agent adopts admin relay list on first run |
| Skill portability | Skills are general Nostr events; implementation-specific variables resolve to empty |
---
## Phase 1 — Specification Updates
Update the authoritative documentation to reflect all decisions before writing code.
### 1.1 Update docs/SKILLS.md
- Remove `llm`, `temperature`, `max_tokens`, `seed`, `tools` from the Content Fields table
- Add execution-parameter tags to the Trigger Tags table: `llm`, `temperature`, `max_tokens`, `seed`, `tools`
- Add `dm` trigger type with filter format `{"from":"admin"}`, `{"from":"wot"}`, `{"from":"any"}`
- Update all examples to show execution params as tags, not content fields
- Update the private skill decrypted payload example accordingly
- Remove the LLM Specification section header (move fallback-chain docs into trigger tag reference)
- Update the Execution Flow mermaid diagram
### 1.2 Update docs/CONTEXT.md
- Remove the Context Modes section entirely (inject/full/override)
- Remove the context_mode branching from the Context Assembly Flow mermaid diagram
- Replace with adoption-list-order composition model
- Update Context Parts table to reference skills instead of soul event
- Update Token Budget section to remove context_mode references
### 1.3 Write docs/GENESIS.md (new)
- Document the genesis.jsonc format and purpose
- Document the first-run flow: genesis consumed, events published, LLM config encrypted
- Document the subsequent-run flow: nsec only, everything fetched from Nostr
- Document bootstrap relay strategy
---
## Phase 2 — Genesis Startup Flow
### 2.1 Define genesis.jsonc schema
```jsonc
{
"key": { "nsec": "nsec1..." },
"admin": { "pubkey": "npub1..." },
"dm_protocol": "nip04",
"llm": {
"provider": "...",
"api_key": "...",
"model": "...",
"base_url": "...",
"max_tokens": 512,
"temperature": 0.7
},
"api": {
"enabled": true,
"port": 8484,
"bind_address": "127.0.0.1"
},
"bootstrap_relays": [
"wss://relay.damus.io",
"wss://nos.lol"
],
"startup_events": [
// Kind 0 profile, kind 10002 relay list, kind 10050 DM relays,
// kind 3 contact list, kind 31123/31124 skills,
// kind 10123 adoption list
],
"default_skill": {
// The didactyl_default skill content — soul + context template
}
}
```
### 2.2 Implement first-run detection
- On startup, check if the agent has published a kind 10002 event (relay list) on bootstrap relays
- If no kind 10002 found: this is a first run — consume genesis.jsonc
- If kind 10002 found: this is a subsequent run — fetch everything from Nostr
### 2.3 Implement genesis event publishing
- Connect to bootstrap relays
- Fetch admin kind 10002 (relay list) and adopt those relays
- Publish all startup_events from genesis.jsonc
- Publish default_skill as kind 31123 with `["d", "didactyl_default"]`
- Publish kind 10123 adoption list with didactyl_default as first entry
- Encrypt and publish LLM config as kind 30078 `d=llm_config`
- Encrypt and publish admin pubkey + dm_protocol as kind 30078 `d=agent_config`
### 2.4 Implement nsec-only startup
- Accept nsec via `--nsec` CLI flag or `DIDACTYL_NSEC` environment variable
- Accept optional `--api-port` and `--api-bind` for local API config
- Derive pubkey from nsec
- Connect to hardcoded bootstrap relays (compiled-in fallback list)
- Fetch own kind 10002 → connect to own relays
- Fetch kind 30078 `d=llm_config` → decrypt → initialize LLM client
- Fetch kind 30078 `d=agent_config` → decrypt → get admin pubkey, dm_protocol
- Fetch kind 10123 → get adoption list
- Fetch adopted skills → build context template
- Proceed to normal operation
---
## Phase 3 — Adoption-List-Order Context Assembly
### 3.1 Refactor context building in agent.c
Current flow (agent_build_admin_messages_json):
- If prompt template exists: use prompt_template_build_messages
- Else: hardcoded context assembly (append_admin_identity_context, append_startup_events_context, append_adopted_skills_context, etc.)
New flow:
- Fetch kind 10123 adoption list
- For each adopted skill in order:
- Fetch skill content
- If skill has a template: resolve template variables via tools_execute
- Append resolved content as system message(s)
- Append DM history (if dm trigger)
- Append user message / triggering event
### 3.2 Unify template variable resolution
Current state: two template formats exist:
- YAML section format with `tool:` directives (prompt_template.c)
- `{{variable}}` inline syntax (documented in SKILLS.md but not fully implemented)
Target: single `{{variable}}` format that resolves through tools_execute.
- Implement `{{variable}}` resolution in a new or updated template engine
- Each `{{variable_name}}` calls `tools_execute(ctx, variable_name, "{}")`
- Unknown variables resolve to empty string
- The YAML section format becomes legacy (still supported for backward compat during transition)
### 3.3 Remove hardcoded context assembly
- Remove append_admin_identity_context, append_startup_events_context, append_adopted_skills_context from agent.c
- These become unnecessary because the didactyl_default skill template handles all of it via {{variable}} resolution
- Keep the functions available as tools so templates can call them
---
## Phase 4 — Execution Params on Trigger Tags
### 4.1 Update trigger_manager to read execution params from tags
Current trigger registration reads: `trigger`, `filter`, `action`, `enabled`
Add reading of: `llm`, `temperature`, `max_tokens`, `seed`, `tools`
Store these on the trigger_entry_t struct.
### 4.2 Apply execution params at invocation time
When a trigger fires:
- If `llm` tag present: temporarily override the LLM model for this execution
- If `temperature` tag present: temporarily override temperature
- If `max_tokens` tag present: temporarily override max_tokens
- If `tools` tag present: filter available tools for this execution
- After execution: restore defaults
Use the existing model_set/model_get pattern (llm_set_config/llm_get_config) for temporary overrides.
### 4.3 Remove execution params from skill content parsing
- Stop reading `llm`, `temperature`, `max_tokens`, `seed`, `tools` from skill content JSON
- These fields in content are ignored (backward compat: warn if present)
---
## Phase 5 — DM Trigger Type
### 5.1 Add dm trigger type to trigger_manager
- New trigger type: `TRIGGER_TYPE_DM`
- Filter format: `{"from":"admin"}`, `{"from":"wot"}`, `{"from":"any"}`
- Registration: when loading skills with `["trigger", "dm"]`, register as DM trigger
### 5.2 Refactor agent_on_message to use trigger dispatch
Current flow:
- agent_on_message receives DM
- Checks sender tier
- Builds context directly
- Calls LLM
New flow:
- agent_on_message receives DM
- Checks sender tier
- Finds matching DM trigger(s) from registered triggers
- For each matching trigger: execute via the standard trigger execution path
- If no DM trigger matches: fall back to default behavior (or reject)
### 5.3 didactyl_default gets a dm trigger
The default skill in genesis.jsonc includes:
```json
["trigger", "dm"],
["filter", "{\"from\":\"admin\"}"],
["llm", "default"],
["tools", "true"],
["enabled", "true"]
```
This makes the normal admin DM conversation a triggered skill execution, consistent with everything else.
---
## Phase 6 — Encrypted Config Storage
### 6.1 Implement config_store tool
New tool: `config_store` — encrypts and publishes agent config to Nostr
- Kind 30078 with configurable d-tag
- NIP-44 encrypted to self (same pattern as memory tool)
- Used for: `d=llm_config`, `d=agent_config`
### 6.2 Implement config_recall tool
New tool: `config_recall` — fetches and decrypts agent config from Nostr
- Query kind 30078 by d-tag and own pubkey
- NIP-44 decrypt
- Return JSON content
### 6.3 Use config tools during startup
- During genesis: call config_store for llm_config and agent_config
- During nsec-only startup: call config_recall to recover LLM and agent config
- These tools are also available to the LLM for runtime config changes
---
## Phase 7 — Documentation Cleanup
### 7.1 Update README.md
- Update startup instructions to reflect genesis.jsonc
- Update architecture overview
- Remove references to config.jsonc as the primary config
### 7.2 Deprecate config.jsonc
- Keep config.jsonc.example as reference but mark as legacy
- Document migration path from config.jsonc to genesis.jsonc
### 7.3 Update context_template.md
- Mark as legacy/deprecated
- Point to didactyl_default skill as the replacement
---
## File Change Summary
| File | Change Type | Description |
|------|------------|-------------|
| `docs/SKILLS.md` | Modify | Execution params to trigger tags, add dm trigger type |
| `docs/CONTEXT.md` | Modify | Remove context_mode, adoption-list composition |
| `docs/GENESIS.md` | New | Genesis config documentation |
| `genesis.jsonc` | Modify | Add bootstrap_relays, flesh out default_skill and startup_events |
| `src/agent.c` | Major refactor | Adoption-list context assembly, DM trigger dispatch |
| `src/config.c` | Modify | Support genesis.jsonc format, nsec-only mode |
| `src/config.h` | Modify | New config structs for minimal runtime config |
| `src/main.c` | Modify | First-run detection, nsec CLI flag, genesis flow |
| `src/trigger_manager.c` | Modify | DM trigger type, execution params from tags |
| `src/trigger_manager.h` | Modify | New trigger type enum, execution param fields |
| `src/prompt_template.c` | Modify | {{variable}} resolution engine |
| `src/tools/tool_config.c` | New | config_store and config_recall tools |
| `src/tools/tools_dispatch.c` | Modify | Register new config tools |
| `src/tools/tools_schema.c` | Modify | Schema for new config tools |
| `README.md` | Modify | Updated startup docs |
| `context_template.md` | Deprecate | Replaced by didactyl_default skill |
| `config.jsonc.example` | Deprecate | Replaced by genesis.jsonc |
---
## Dependency Order
```mermaid
graph TD
P1[Phase 1: Spec Updates] --> P2[Phase 2: Genesis Startup]
P1 --> P3[Phase 3: Adoption-List Context]
P1 --> P4[Phase 4: Execution Params on Tags]
P1 --> P5[Phase 5: DM Trigger Type]
P2 --> P6[Phase 6: Encrypted Config Storage]
P3 --> P5
P4 --> P5
P5 --> P7[Phase 7: Documentation Cleanup]
P6 --> P7
```
Phases 2, 3, 4 can proceed in parallel after Phase 1. Phase 5 depends on 3 and 4. Phase 6 depends on 2. Phase 7 is last.