Compare commits

...

9 Commits

28 changed files with 1469 additions and 23451 deletions

View File

@@ -54,20 +54,19 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
## Current Status — v0.2.1
## Current Status — v0.2.10
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.2.1 — v0.2.1 migration: remove template/soul system context paths, adopt trigger-composed admin DM flow, update genesis defaults to default_admin_dm with kind0/kind3 bootstrap, purge legacy config/context template files, scrub genesis.jsonc from tracking/history and force-push rewritten refs
> Last release update: v0.2.10startup self-skill eose: use first-set mode and stabilize fail-path cleanup order
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
- Loads base system context from default skill content (first-run from`genesis.jsonc`, subsequent runs from adopted skills on Nostr)
- Verifies Nostr event signatures before processing inbound messages
- Applies privilege tiers: ADMIN (tools), WoT (chat-only), STRANGER (configurable canned reply or ignore)
- Subscribes to admin context kinds (`0`,`3`,`10002`,`1`) for WoT + contextual awareness
- Builds LLM context from default/adopted skill templates (`---template---`) with named sections, variable resolution, and per-provider content overrides; falls back to hardcoded assembly if no template present
- Adopted skills injected into context automatically from the agent's`10123` adoption list
- Builds trigger-scoped LLM context from DM and subscription-triggered skills with inline variable resolution
- Uses default skill d-tag `default_admin_dm` with trigger tags for admin DM handling
- Supports tool-calling loop with configurable max turns and local safety limits
- Triggered skills — Nostr event filters that fire skill execution automatically with`template` (deterministic) or`llm` (context-aware) actions; see[`docs/SKILLS.md`](docs/SKILLS.md)
- Deduplicates inbound messages via event-ID cache and FNV-1a fingerprint debounce window
@@ -162,8 +161,8 @@ Edit [`genesis.jsonc`](genesis.jsonc):
},
{
"kind": 31124,
"content": "You are Didactyl...",
"tags": [["d", "didactyl-default"], ["app", "didactyl"], ["scope", "private"]]
"content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate",
"tags": [["d", "default_admin_dm"], ["app", "didactyl"], ["scope", "private"], ["description", "Default admin DM handler"], ["trigger", "dm"], ["filter", "{\"from\":\"admin\"}"]]
},
{
"kind": 31123,
@@ -489,7 +488,7 @@ Skills that work: ALL Skills that work: those whose
- [X] Triggered skills — Nostr event filters that fire skill execution automatically
- [X] Localhost HTTP admin API — context inspection, prompt crafting, A/B comparison
- [X] Runtime model switching via`model_set` tool (persisted in encrypted config events)
- [X] Skill-embedded prompt templates (`---template---`) — configurable context order, variable resolution, provider overrides
- [X] Trigger-scoped inline-variable context assembly (template DSL removed)
- [ ] Runtime skill loading from adopted`31123` events on relays
- [ ] Skill discovery CLI/tool (query WoT adoption lists)
- [ ] Upgrade to NIP-17 gift-wrapped DMs

View File

@@ -1,355 +0,0 @@
{
// LEGACY CONFIG EXAMPLE
// This file is deprecated in favor of genesis.jsonc + nsec-only startup.
// Kept as reference for older deployments and migration support.
// ─── Agent Identity Keys ───────────────────────────────────────────
// Your agent's Nostr keypair. Provide nsec (bech32) or hex format.
// The public key fields are optional — they are derived automatically.
"keys": {
"nsec": "agent nsec",
"npub": "agent npub",
"npubHex": "agent hex pubkey",
"nsecHex": "agent hex secret key"
},
// ─── Administrator ─────────────────────────────────────────────────
// The admin pubkey (npub or hex) controls who can issue privileged
// commands and use tools via DM.
"admin": {
"pubkey": "admin pubkey"
},
// ─── DM Protocol ──────────────────────────────────────────────────
// Which encrypted DM protocol to use: "nip04", "nip17", or "both"
"dm_protocol": "nip04",
// ─── LLM Provider ─────────────────────────────────────────────────
// Configure the language model backend. Any OpenAI-compatible API works.
"llm": {
"provider": "", // e.g. "openai", "anthropic", etc.
"api_key": "", // your API key
"model": "", // model identifier, e.g. "gpt-4o-mini"
"base_url": "", // API base URL, e.g. "https://api.openai.com/v1"
"max_tokens": 512, // max tokens per LLM response
"temperature": 0.7 // sampling temperature (0.0 2.0)
},
// ─── Cashu Wallet (NIP-60) ────────────────────────────────────────
// Optional wallet runtime used by cashu_wallet_* tools.
// If enabled=true and auto_load=true, Didactyl will attempt to load
// wallet/token events from relays at startup and create a new wallet
// from mint_urls if none is found.
"cashu_wallet": {
"enabled": false,
"mint_urls": [
"https://mint.minibits.cash/Bitcoin"
],
"unit": "sat",
"auto_load": true,
"mint_timeout_seconds": 30
},
// ─── Tools & Runtime Limits ───────────────────────────────────────
// Centralized turn and timeout limits used by DM agent loops,
// triggered skills, HTTP API prompt runs, and local HTTP fetch tool.
"tools": {
"enabled": true,
"max_turns": 20, // default max turns for normal agent tool loops
"trigger_max_turns": 12, // max turns for triggered-skill executions
"api_default_max_turns": 8, // default when HTTP body omits max_turns
"api_max_turns_ceiling": 32, // hard cap applied to API-provided max_turns
"stall_repeat_threshold": 3, // stop early when identical tool-call turns repeat this many times
"local_http_fetch_default_timeout_seconds": 20,
"local_http_fetch_max_timeout_seconds": 120,
"blossom_max_upload_bytes": 16777216,
"blossom_max_download_bytes": 16777216,
"shell": {
"enabled": true,
"timeout_seconds": 30,
"max_output_bytes": 65536,
"working_directory": "."
}
},
// ─── Security Tiers ───────────────────────────────────────────────
// Controls who can interact with the agent and what they can do.
"security": {
"verify_signatures": true, // verify Nostr event signatures
// Message sent to strangers outside the web of trust
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
"tiers": {
"admin": {
"tools_enabled": true // admin can always use tools
},
"wot": {
"enabled": true, // respond to web-of-trust contacts
"tools_enabled": false // WoT contacts cannot use tools by default
},
"stranger": {
"enabled": true // respond to strangers (with stranger_response)
}
}
},
// ─── Admin Context Subscriptions ──────────────────────────────────
// Subscribe to the admin's Nostr events to build context awareness.
"admin_context": {
"enabled": true,
"subscribe_kinds": [
0, // kind 0: profile metadata
3, // kind 3: contact list
10002, // kind 10002: relay list
1 // kind 1: text notes
],
"kind_1_limit": 10 // max recent kind-1 notes to track
},
// ─── HTTP Admin API ───────────────────────────────────────────────
// Local REST API for runtime inspection and control.
"api": {
"enabled": true,
"port": 8484,
"bind_address": "127.0.0.1" // bind to localhost only
},
// ─── Startup Events ───────────────────────────────────────────────
// Events published on boot. Includes profile, relay list, soul,
// skills, and adoption list.
"startup_events": [
// Kind 0: Agent profile metadata (NIP-01)
{
"kind": 0,
"content_fields": {
"name": "Didactyl Agent",
"display_name": "Didactyl",
"about": "A sovereign AI agent on Nostr",
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
},
"tags": []
},
// Kind 10002: Relay list (NIP-65)
// These relays are used for connecting and publishing events.
{
"kind": 10002,
"content": "",
"tags": [
["r", "wss://relay.damus.io"],
["r", "wss://nos.lol"],
["r", "wss://relay.primal.net"],
["r", "ws://127.0.0.1:7777"]
]
},
// Kind 10050: DM relay list (NIP-17)
// Relays used specifically for receiving encrypted DMs.
{
"kind": 10050,
"content": "",
"tags": [
["relay", "wss://relay.damus.io"],
["relay", "wss://nos.lol"]
]
},
// Kind 1: Startup announcement note
{
"kind": 1,
"content": "Hello world from Didactyl startup",
"tags": [
["t", "didactyl"],
["t", "startup"]
]
},
// Kind 3: Contact list (initially empty)
{
"kind": 3,
"content": "",
"tags": []
},
// Kind 31120: Soul event — the agent's personality and behavior rules.
// Contains the system prompt and template sections for LLM context.
// The ---template--- marker separates the system prompt from
// structured context sections that are injected at runtime.
{
"kind": 31120,
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `local_shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- For memory persistence, use `memory_save` to prepend new memory entries and `memory_recall` to recall stored memory.\n- Memory is not injected by default; call `memory_recall` when needed.\n- Keep memory content in markdown and concise enough for context efficiency.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Task Management\n- Maintain and use your internal task list as short-term working memory.\n- Break long or complex actions into clear tasks before executing them.\n- Update task status as you complete steps so your plan stays accurate.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.\n\n---template---\n\n- section: admin_identity\n role: system\n content: |\n ## Administrator Identity (source: config.admin.pubkey)\n\n This is your administrator! Admin pubkey (hex): {{admin_pubkey}}\n\n- section: admin_profile\n role: system\n content: |\n ## Administrator Kind 0 Profile (source: nostr kind 0)\n\n Administrator kind 0 profile content (JSON): {{admin_kind0_json}}\n provider:\n anthropic: |\n <admin_kind0_profile source=\"nostr_kind_0\">\n {{admin_kind0_json}}\n </admin_kind0_profile>\n\n- section: admin_contacts\n role: system\n content: |\n ## Administrator Contact List (source: nostr kind 3)\n\n Administrator kind 3 contact list pubkeys (JSON array): {{admin_kind3_json}}\n provider:\n anthropic: |\n <admin_contacts source=\"nostr_kind_3\">\n {{admin_kind3_json}}\n </admin_contacts>\n\n- section: admin_relays\n role: system\n content: |\n ## Administrator Relay List (source: nostr kind 10002)\n\n Administrator kind 10002 relay list (JSON): {{admin_kind10002_json}}\n provider:\n anthropic: |\n <admin_relays source=\"nostr_kind_10002\">\n {{admin_kind10002_json}}\n </admin_relays>\n\n- section: admin_notes\n role: system\n content: |\n ## Administrator Recent Notes (source: nostr kind 1)\n\n Administrator recent kind 1 notes (JSON array): {{admin_kind1_json}}\n provider:\n anthropic: |\n <admin_notes source=\"nostr_kind_1\">\n {{admin_kind1_json}}\n </admin_notes>\n\n- section: skills\n role: system\n content: |\n ## Adopted Skills\n\n {{skills_json}}\n\n- section: tasks\n role: system\n content: |\n ## Current Task List\n\n {{tasks_json}}\n\n- section: conversation\n role: conversation\n content: |\n {{conversation}}",
"tags": [
["d", "soul"],
["app", "didactyl"],
["scope", "private"]
]
},
// Kind 31123: Public skill — long_form_note
// Teaches the agent how to publish NIP-23 long-form articles.
{
"kind": 31123,
"content_fields": {
"name": "long_form_note",
"description": "How to publish a NIP-23 long-form article (kind 30023)",
"nip": "NIP-23",
"event_kind": 30023,
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
"required_tags": {
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
"title": "Human-readable article title.",
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
},
"optional_tags": {
"summary": "Short 1-2 sentence summary.",
"image": "URL for article preview image.",
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
},
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
"procedure": [
"Determine title and d tag from admin input or source material.",
"Draft markdown body content.",
"Set published_at as unix seconds string.",
"Add optional summary/image/t tags when known.",
"Publish with nostr_post kind 30023 including tags array."
]
},
"tags": [
["d", "long_form_note"],
["app", "didactyl"],
["scope", "public"],
["slug", "long_form_note"]
]
},
// Kind 31123: Public skill — post_readme_to_nostr
// Reads README.md and publishes it as a long-form note.
{
"kind": 31123,
"content_fields": {
"name": "post_readme_to_nostr",
"description": "Read README.md from the repo and publish it as a NIP-23 long-form note",
"uses_skill": "long_form_note",
"event_kind": 30023,
"procedure": [
"Read README.md using local_file_read with path README.md.",
"Set d tag exactly to readme.md.",
"Set title from the first markdown H1 heading in README.md.",
"Set summary from the opening paragraph of README.md.",
"Set image from project metadata when available (kind 0 picture/banner), otherwise omit image tag.",
"Generate 3-6 lowercase t tags from README section topics.",
"Set published_at to current unix timestamp as string.",
"Publish with nostr_post kind 30023, full README markdown in content, and full NIP-23 tag set."
],
"required_values": {
"d": "readme.md",
"summary_source": "opening paragraph"
}
},
"tags": [
["d", "post_readme_to_nostr"],
["app", "didactyl"],
["scope", "public"],
["slug", "post_readme_to_nostr"]
]
},
// Kind 31123: Public triggered skill — cheerleader
// Watches for admin kind-1 notes and sends an encouraging DM.
{
"kind": 31123,
"content": "You are my personal cheerleader. When the admin posts a kind 1 note, read their note and send them a short DM that cheers them on, praises their effort, tells them they are good looking, and encourages them to keep going. Be warm, playful, and positive. Mention something specific from their note so it feels personal. Keep it to 2-3 sentences.",
"tags": [
["d", "cheerleader"],
["app", "didactyl"],
["scope", "public"],
["description", "Cheer on admin whenever they post a kind 1 note"],
["trigger", "nostr-subscription"],
["filter", "{\"kinds\":[1],\"authors\":[\"REPLACE_WITH_ADMIN_HEX_PUBKEY\"]}"],
["action", "llm"],
["enabled", "true"]
]
},
// Kind 31124: Private skill — admin_ops
// Private operational procedures (admin-only).
{
"kind": 31124,
"content_fields": {
"name": "admin_ops",
"description": "Private operational procedures"
},
"tags": [
["d", "admin_ops"],
["app", "didactyl"],
["scope", "private"],
["slug", "admin_ops"]
]
},
// Kind 31124: Private triggered skill — webhook-echo
// Example webhook trigger that emits a fixed DM response.
{
"kind": 31124,
"content": "When this webhook trigger fires, DM the admin with exactly: WEBHOOK_SOURCE_POC_OK",
"tags": [
["d", "webhook-echo"],
["app", "didactyl"],
["scope", "private"],
["description", "PoC webhook trigger skill"],
["trigger", "webhook"],
["filter", "{}"],
["action", "llm"],
["enabled", "true"]
]
},
// Kind 31124: Private triggered skill — cron_poc
// Example cron trigger that runs every minute.
{
"kind": 31124,
"content": "DM the administrator with the current time, and tell him that your cron skill triggered.",
"tags": [
["d", "cron_poc"],
["app", "didactyl"],
["scope", "private"],
["description", "PoC cron trigger skill"],
["trigger", "cron"],
["filter", "*/1 * * * *"],
["action", "template"],
["enabled", "true"]
]
},
// Kind 31124: Private triggered skill — chain_from_webhook_poc
// Example chain trigger that runs after webhook-echo completes.
{
"kind": 31124,
"content": "LOG: chain_from_webhook_poc fired",
"tags": [
["d", "chain_from_webhook_poc"],
["app", "didactyl"],
["scope", "private"],
["description", "PoC chain trigger skill"],
["trigger", "chain"],
["filter", "webhook-echo"],
["action", "template"],
["enabled", "true"]
]
},
// Kind 10123: Skill adoption list
// References which public skills this agent has adopted.
{
"kind": 10123,
"content": "",
"tags": [
["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"],
["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"],
["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:cheerleader"],
["app", "didactyl"],
["scope", "public"]
]
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +0,0 @@
# Context Template (Legacy)
This file is deprecated and retained for backward compatibility only.
The default context behavior is now defined by adopted skills (kind `10123`) and the `didactyl_default` skill template published from `genesis.jsonc`.
For current behavior, see:
- `docs/GENESIS.md`
- `docs/CONTEXT.md`
- `docs/SKILLS.md`

View File

@@ -1,56 +0,0 @@
# Context Template
```yaml
- section: admin_identity
role: system
content: |
## Administrator Identity (source: config.admin.pubkey)
This is your administrator! Admin pubkey (hex): {{admin_pubkey}}
- section: admin_profile
role: system
content: |
## Administrator Kind 0 Profile (source: nostr kind 0)
Administrator kind 0 profile content (JSON): {{admin_kind0_json}}
provider:
anthropic: |
<admin_kind0_profile source="nostr_kind_0">
{{admin_kind0_json}}
</admin_kind0_profile>
- section: admin_relay_list
role: system
content: |
## Administrator Relay List (source: nostr kind 10002)
Administrator kind 10002 relay-list content (JSON): {{admin_kind10002_json}}
- section: startup_events
role: system
content: |
## Startup Events Memory (source: config.startup_events)
Startup events memory (kinds/content/tags): {{startup_events_json}}
- section: adopted_skills
role: system
content: |
{{adopted_skills_content}}
- section: agent_tasks
role: system
content: |
{{tasks_content}}
- section: dm_history
role: expand
limit: 12
- section: admin_notes
role: system
content: |
## Administrator Recent Notes (source: nostr kind 1)
{{admin_notes_content}}

View File

@@ -10,7 +10,7 @@ All responses are JSON. CORS headers are included on every response for browser
## Configuration
Enable the API in `config.jsonc`:
Enable the API in `genesis.jsonc`:
```json
{

View File

@@ -6,7 +6,7 @@ See also: [CONTEXT.md](CONTEXT.md) · [SKILLS.md](SKILLS.md) · [README.md](../R
`genesis.jsonc` is the first-run bootstrap document for a Didactyl agent.
It defines initial identity, admin policy, startup events, default skill context, and baseline runtime settings so the agent can publish itself onto Nostr.
It defines initial identity, admin policy, startup events, default skill trigger behavior, and baseline runtime settings so the agent can publish itself onto Nostr.
After bootstrap, the long-term direction is **nsec-only startup** with state recovered from Nostr events.
@@ -30,7 +30,7 @@ Typical optional sections:
- `security`
- `admin_context`
- `api`
- `default_skill` (reserved for default-skill publishing workflows)
- `default_skill` (published as private kind `31124`, default `d=default_admin_dm`)
---
@@ -118,14 +118,13 @@ That relay list is used as the initial network attachment for querying existing
---
## Migration Notes (Legacy -> Genesis)
## Migration Notes (v0.2.0)
Legacy deployments using `config.jsonc` can migrate by:
1. Copying equivalent sections to `genesis.jsonc`.
2. Ensuring startup kind `10002` relay tags are present.
3. Providing nsec at runtime (`--nsec` or `DIDACTYL_NSEC`) where desired.
4. Treating `context_template.md` / `config.jsonc.example` as legacy references.
- Legacy `config.jsonc` and template-DSL context files have been removed from the active startup model.
- Default DM handling now relies on `default_skill` with `d_tag: default_admin_dm` and trigger tags:
- `["trigger", "dm"]`
- `["filter", "{\"from\":\"admin\"}"]`
- Default skill content is plain markdown with inline variables such as `{{my_kind0_profile}}` and `{{my_npub}}`.
---

View File

@@ -37,8 +37,22 @@
},
// ─── Startup Events ────────────────────────────────────────────────
// Minimal relay list required for relay pool initialization.
"startup_events": [
{
"kind": 0,
"content_fields": {
"name": "Didactyl",
"about": "I am a Didactyl agent living on Nostr"
},
"tags": []
},
{
"kind": 3,
"content": "",
"tags": [
["p", "ADMIN_HEX_PUBKEY"]
]
},
{
"kind": 10002,
"content": "",
@@ -52,12 +66,15 @@
// ─── Default Skill ────────────────────────────────────────────────
// Keep this generic and non-sensitive for repository examples.
"default_skill": {
"d_tag": "didactyl-default",
"d_tag": "default_admin_dm",
"kind": 31124,
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `local_shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Task Management\n- Maintain and use your internal task list as short-term working memory.\n- Break long or complex actions into clear tasks before executing them.\n- Update task status as you complete steps so your plan stays accurate.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.\n\n---template---\n\n- section: admin_identity\n role: system\n tool: admin_identity\n skip_if_empty: true\n\n- section: admin_profile\n role: system\n tool: nostr_admin_profile\n skip_if_empty: true\n\n- section: admin_contacts\n role: system\n tool: nostr_admin_contacts\n skip_if_empty: true\n\n- section: admin_relays\n role: system\n tool: nostr_admin_relays\n skip_if_empty: true\n\n- section: admin_notes\n role: system\n tool: nostr_admin_notes\n skip_if_empty: true\n\n- section: agent_identity\n role: system\n tool: agent_identity\n skip_if_empty: true\n\n- section: agent_profile\n role: system\n tool: nostr_agent_profile\n skip_if_empty: true\n\n- section: agent_contacts\n role: system\n tool: nostr_agent_contacts\n skip_if_empty: true\n\n- section: agent_relays\n role: system\n tool: nostr_agent_relays\n skip_if_empty: true\n\n- section: agent_notes\n role: system\n tool: nostr_agent_notes\n skip_if_empty: true\n\n- section: tasks\n role: system\n tool: task_list\n skip_if_empty: true\n\n- section: dm_history\n role: expand\n limit: 12\n\n- section: conversation\n role: user\n tool: message_current\n skip_if_empty: true",
"content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone",
"tags": [
["app", "didactyl"],
["scope", "private"]
["scope", "private"],
["description", "Default admin DM handler"],
["trigger", "dm"],
["filter", "{\"from\":\"admin\"}"]
]
}
}

View File

@@ -299,11 +299,29 @@ git_commit_and_push_no_tag() {
# Push changes
print_status "Pushing to remote repository..."
if git push > /dev/null 2>&1; then
print_success "Pushed changes"
local current_branch
current_branch=$(git branch --show-current 2>/dev/null || echo "")
if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" > /dev/null 2>&1; then
if git push > /dev/null 2>&1; then
print_success "Pushed changes"
else
print_error "Failed to push changes"
exit 1
fi
else
print_error "Failed to push changes"
exit 1
if [[ -z "$current_branch" ]]; then
print_error "Unable to determine current branch for push"
exit 1
fi
print_warning "No upstream configured for branch '$current_branch'; setting upstream to origin/$current_branch"
if git push -u origin "$current_branch" > /dev/null 2>&1; then
print_success "Pushed changes and configured upstream"
else
print_error "Failed to push changes while configuring upstream"
exit 1
fi
fi
# Push only the new tag to avoid conflicts with existing tags

View File

@@ -0,0 +1,224 @@
# Strict Startup Validation — Eliminate Fake Event Seeding
## Problem
The agent currently seeds a **synthetic event** into the self-skill cache during startup
(`seed_default_skill_into_cache()` in `src/nostr_handler.c:1299`). This fake event has:
- `id` = all zeros
- `created_at` = `time(NULL)` (current startup time)
Because the fake event is newer than the real relay event, the upsert rejects the real
event. This causes `skill_get` to return an all-zeros ID and prevents the cache from
ever holding the authentic relay event.
Additionally, the adopted skills cache in `src/agent.c` has a startup fallback path
that uses `g_cfg->startup_events` when the adoption list is empty. This masks real
failures and creates inconsistency between what the agent thinks it has and what
actually exists on relays.
## Principle
> If the agent cannot retrieve the events it needs to run, startup should fail with
> a clear diagnostic. No fake events. No fallbacks. If it is not there, fail.
## Two Startup Modes
### Genesis Startup (first run)
1. Connect to relays
2. Publish startup events (profile, contacts, relay list, default skill, adoption list)
3. **Wait for publish confirmation** from at least one relay per event
4. Log at INFO level: which events were published to which relays
5. Subscribe to self-skill cache
6. Wait for EOSE — validate published events are now in cache
7. If validation fails: abort with diagnostic
### Subsequent Startup (existing agent)
1. Connect to relays
2. Subscribe to self-skill cache
3. Wait for EOSE
4. Log at INFO level: which events were received from which relays
5. Validate required events exist in cache
6. If validation fails: abort with diagnostic
## Current Startup Flow (steps 1418)
```
Step 14: Subscribe self-skill cache (sends REQ for kinds 31123/31124/10123)
→ Does NOT wait for EOSE
→ Calls seed_default_skill_into_cache() with fake event
Step 15: Subscribe DMs (agent starts accepting messages)
→ Startup DM sent BEFORE this step
Step 16: Subscribe wallet events
Step 17: Initialize cashu wallet
Step 18: READY — enter main poll loop
```
## Proposed New Flow
```
Step 14: Subscribe self-skill cache (sends REQ for kinds 31123/31124/10123)
Step 14a: Wait for self-skill EOSE (15s timeout)
Step 14b: Validate required events exist in cache
→ Log received events at INFO level
→ If no skill events: FAIL startup with diagnostic
Step 15: Subscribe DMs
Step 16: Subscribe wallet events
Step 17: Initialize cashu wallet
Step 18: READY — send startup DM AFTER all validation, enter main loop
```
## Changes
### 1. Remove `seed_default_skill_into_cache()` entirely
**File:** `src/nostr_handler.c`
- Delete the entire function (lines 12991341)
- Remove its call at line 3165 in `nostr_handler_reconcile_startup_events()`
- Remove its forward declaration at line 451
### 2. Remove startup fallback in `refresh_adopted_skills_cache_if_needed()`
**File:** `src/agent.c`
Delete the entire fallback block (lines ~15711619) that uses
`g_cfg->startup_events` when the adoption list is empty. If the adoption
list is empty, the cache stays empty — no synthetic population.
### 3. Add synchronous EOSE wait for self-skill subscription
**File:** `src/nostr_handler.c`
Add a new exported function:
```c
int nostr_handler_wait_for_self_skill_eose(int timeout_ms);
```
Implementation:
- Use a static `volatile int g_self_skill_eose_received = 0` flag
- Set it to 1 in `on_self_skill_eose()`
- The wait function polls `nostr_relay_pool_poll()` in a loop until the
flag is set or the timeout expires
- Returns 0 on success, -1 on timeout
### 4. Add post-EOSE validation with logging
**File:** `src/nostr_handler.c`
Add a new exported function:
```c
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count);
```
This function:
- Counts skill events (kind 31123 + 31124) in cache
- Counts adoption events (kind 10123) in cache
- Logs at INFO level each cached event: kind, d_tag, id, created_at
- Returns 0 if at least one skill event exists
- Returns -1 if no skill events found
### 5. Update startup sequence in `main.c`
**File:** `src/main.c`
After step 14 subscribe, add EOSE wait and validation:
```c
// Wait for self-skill EOSE
int eose_timeout_ms = 15000;
if (nostr_handler_wait_for_self_skill_eose(eose_timeout_ms) != 0) {
startup_step_fail(14, "Subscribe self-skill cache",
"self-skill events not received within timeout; "
"check relay connectivity and that skill events exist");
// cleanup and return 1
}
// Validate
int skill_count = 0, adoption_count = 0;
if (nostr_handler_validate_self_skill_cache(&skill_count, &adoption_count) != 0) {
startup_step_fail(14, "Subscribe self-skill cache",
"no skill events found after EOSE; "
"run genesis to publish default skill events");
// cleanup and return 1
}
char detail[128];
snprintf(detail, sizeof(detail),
"skills=%d adoptions=%d", skill_count, adoption_count);
startup_step_ok(14, "Subscribe self-skill cache", detail);
```
### 6. Move startup DM to after all validation
**File:** `src/main.c`
Move the startup DM send from its current position (between steps 14 and 15)
to just before step 18 READY. The agent should only announce itself as online
after all required events have been validated and all subscriptions are active.
### 7. Genesis publish confirmation logging
**File:** `src/nostr_handler.c` / `src/main.c`
For genesis startup, the existing publish path already logs at INFO level:
```
kind 31124 event published to wss://relay.damus.io (async)
```
Enhance this to also log a summary after all startup events are published:
```
[INFO] startup publish summary: 5 events published to 3/4 relays
[INFO] kind=0 (profile) → relay.damus.io, relay.primal.net, nos.lol
[INFO] kind=31124 (default_admin_dm) → relay.damus.io, relay.primal.net, nos.lol
[INFO] kind=10123 (adoption list) → relay.damus.io, relay.primal.net, nos.lol
```
### 8. Declare new functions in header
**File:** `src/nostr_handler.h`
```c
int nostr_handler_wait_for_self_skill_eose(int timeout_ms);
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count);
```
## Startup Step Summary
| Step | Description | Behavior |
|------|-------------|----------|
| 113 | Existing steps (config, relays, agent init, triggers, kind10002, context subs) | Unchanged |
| 14 | Subscribe self-skill cache + wait EOSE + validate | **BLOCKING** — fails if no skill events |
| 15 | Subscribe DMs | Fails startup if subscription fails |
| 16 | Subscribe wallet events | Continues on failure |
| 17 | Initialize cashu wallet | Continues on failure |
| 18 | READY | Send startup DM, enter main loop |
## Error Messages
### EOSE timeout
```
[ERROR] Startup aborted: self-skill EOSE not received within 15000ms.
[ERROR] Check relay connectivity (connected=2/4) and ensure skill events exist on relays.
[ERROR] If this is a first run, use genesis to publish startup events.
```
### No skill events after EOSE
```
[ERROR] Startup aborted: no skill events (kind 31123/31124) found in self-skill cache.
[ERROR] The agent requires at least one skill event to operate.
[ERROR] Run genesis to publish the default skill, or publish a skill manually.
```
## Files Changed
| File | Change |
|------|--------|
| `src/nostr_handler.c` | Remove `seed_default_skill_into_cache()`, add EOSE wait + validation functions, enhance publish logging |
| `src/nostr_handler.h` | Declare `nostr_handler_wait_for_self_skill_eose()` and `nostr_handler_validate_self_skill_cache()` |
| `src/main.c` | Add EOSE wait + validation after step 14, move startup DM to before step 18 |
| `src/agent.c` | Remove startup fallback block in `refresh_adopted_skills_cache_if_needed()` |

View File

@@ -21,12 +21,9 @@
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static didactyl_config_t* g_cfg = NULL;
static char* g_system_context = NULL;
static tools_context_t g_tools_ctx;
static struct trigger_manager* g_trigger_manager = NULL;
static prompt_template_t g_prompt_template;
static int g_has_prompt_template = 0;
#define AGENT_CONTEXT_PART_NAMES_MAX 512
static char* g_context_part_names[AGENT_CONTEXT_PART_NAMES_MAX];
static int g_context_part_names_count = 0;
@@ -483,6 +480,11 @@ static char* build_slash_help_all_json(void) {
return NULL;
}
if (append_textf_local(&out, &cap, &used, "(run /<tool> -h for full JSON args template)\n") != 0) {
free(out);
return NULL;
}
char* tool_list_json = tools_execute(&g_tools_ctx, "tool_list", "{}");
cJSON* tool_root = tool_list_json ? cJSON_Parse(tool_list_json) : NULL;
cJSON* tools = tool_root ? cJSON_GetObjectItemCaseSensitive(tool_root, "tools") : NULL;
@@ -647,6 +649,54 @@ static char* build_slash_help_all_json(void) {
return out;
}
static cJSON* build_example_value_for_type_local(const char* type_s) {
if (!type_s || type_s[0] == '\0') return cJSON_CreateString("<value>");
if (strcmp(type_s, "string") == 0) return cJSON_CreateString("<string>");
if (strcmp(type_s, "integer") == 0) return cJSON_CreateNumber(0);
if (strcmp(type_s, "number") == 0) return cJSON_CreateNumber(0.0);
if (strcmp(type_s, "boolean") == 0) return cJSON_CreateBool(0);
if (strcmp(type_s, "array") == 0) return cJSON_CreateArray();
if (strcmp(type_s, "object") == 0) return cJSON_CreateObject();
return cJSON_CreateString("<value>");
}
static cJSON* build_example_args_from_parameters_local(cJSON* params) {
cJSON* example = cJSON_CreateObject();
if (!example) return NULL;
cJSON* props = params ? cJSON_GetObjectItemCaseSensitive(params, "properties") : NULL;
if (!props || !cJSON_IsObject(props)) {
return example;
}
cJSON* prop = NULL;
cJSON_ArrayForEach(prop, props) {
if (!prop->string || prop->string[0] == '\0') continue;
cJSON* enum_vals = cJSON_GetObjectItemCaseSensitive(prop, "enum");
if (enum_vals && cJSON_IsArray(enum_vals) && cJSON_GetArraySize(enum_vals) > 0) {
cJSON* first = cJSON_GetArrayItem(enum_vals, 0);
cJSON* dup = cJSON_Duplicate(first, 1);
if (dup) {
cJSON_AddItemToObject(example, prop->string, dup);
continue;
}
}
cJSON* type = cJSON_GetObjectItemCaseSensitive(prop, "type");
const char* type_s = (type && cJSON_IsString(type) && type->valuestring) ? type->valuestring : NULL;
cJSON* val = build_example_value_for_type_local(type_s);
if (!val) val = cJSON_CreateString("<value>");
if (!val) {
cJSON_Delete(example);
return NULL;
}
cJSON_AddItemToObject(example, prop->string, val);
}
return example;
}
static char* build_slash_help_tool_json(const char* tool_name) {
if (!tool_name || tool_name[0] == '\0') {
return local_json_error("missing tool name");
@@ -692,6 +742,26 @@ static char* build_slash_help_tool_json(const char* tool_name) {
cJSON_AddStringToObject(out, "tool", tool_name);
cJSON_AddItemToObject(out, "schema", cJSON_Duplicate(matched_fn, 1));
cJSON* params = cJSON_GetObjectItemCaseSensitive(matched_fn, "parameters");
cJSON* example_args = build_example_args_from_parameters_local(params);
if (example_args) {
char* example_json = cJSON_PrintUnformatted(example_args);
if (example_json) {
cJSON_AddStringToObject(out, "example_args_json", example_json);
size_t usage_len = strlen(tool_name) + strlen(example_json) + 4U;
char* usage = (char*)malloc(usage_len);
if (usage) {
snprintf(usage, usage_len, "/%s %s", tool_name, example_json);
cJSON_AddStringToObject(out, "example_slash", usage);
free(usage);
}
free(example_json);
}
cJSON_Delete(example_args);
}
char* result = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
cJSON_Delete(schema);
@@ -773,40 +843,6 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa
static const char* get_context_part_name_copy(int idx);
static int appendf(char** buf, size_t* cap, size_t* used, const char* fmt, ...) {
if (!buf || !cap || !used || !fmt) return -1;
while (1) {
if (*used + 128U >= *cap) {
size_t next_cap = (*cap) * 2U;
char* grown = (char*)realloc(*buf, next_cap);
if (!grown) return -1;
*buf = grown;
*cap = next_cap;
}
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(*buf + *used, *cap - *used, fmt, ap);
va_end(ap);
if (n < 0) {
return -1;
}
if ((size_t)n < (*cap - *used)) {
*used += (size_t)n;
return 0;
}
size_t next_cap = (*cap) * 2U + (size_t)n + 64U;
char* grown = (char*)realloc(*buf, next_cap);
if (!grown) return -1;
*buf = grown;
*cap = next_cap;
}
}
static const char* detect_context_section(const char* role, const char* content, int idx) {
const char* c = content ? content : "";
const char* r = role ? role : "";
@@ -860,233 +896,6 @@ const char* agent_classify_message_part(cJSON* msg, int idx) {
return detect_context_section(role_s, content_s, idx);
}
static const char* admin_label_for_log(void) {
static __thread char name_buf[96];
snprintf(name_buf, sizeof(name_buf), "%s", "Administrator");
char* kind0 = nostr_handler_get_admin_kind0_context();
if (!kind0 || kind0[0] == '\0') {
free(kind0);
return name_buf;
}
cJSON* root = cJSON_Parse(kind0);
free(kind0);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return name_buf;
}
cJSON* display_name = cJSON_GetObjectItemCaseSensitive(root, "display_name");
cJSON* name = cJSON_GetObjectItemCaseSensitive(root, "name");
const char* chosen = NULL;
if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') {
chosen = display_name->valuestring;
} else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
chosen = name->valuestring;
}
if (chosen) {
snprintf(name_buf, sizeof(name_buf), "%s", chosen);
}
cJSON_Delete(root);
return name_buf;
}
static char* demote_markdown_headings_for_log(const char* content) {
const char* src = content ? content : "";
size_t n = strlen(src);
char* out = (char*)malloc(n * 2U + 1U);
if (!out) {
return NULL;
}
size_t w = 0;
int line_start = 1;
for (size_t i = 0; i < n; i++) {
char c = src[i];
if (line_start && c == '#') {
out[w++] = '#';
}
out[w++] = c;
if (c == '\n') {
line_start = 1;
} else if (line_start && (c == ' ' || c == '\t' || c == '\r')) {
line_start = 1;
} else {
line_start = 0;
}
}
out[w] = '\0';
return out;
}
static void format_chat_time_for_log(time_t ts, char out[16]) {
if (!out) return;
out[0] = '\0';
if (ts <= 0) {
snprintf(out, 16, "--:--");
return;
}
struct tm tm_info;
if (!localtime_r(&ts, &tm_info)) {
snprintf(out, 16, "--:--");
return;
}
strftime(out, 16, "%H:%M", &tm_info);
}
static char* format_context_payload_for_log(const char* context_payload) {
const char* payload = context_payload ? context_payload : "";
cJSON* root = cJSON_Parse(payload);
if (!root || !cJSON_IsArray(root)) {
cJSON_Delete(root);
return strdup(payload);
}
size_t cap = strlen(payload) + 2048U;
if (cap < 4096U) cap = 4096U;
char* out = (char*)malloc(cap);
if (!out) {
cJSON_Delete(root);
return strdup(payload);
}
out[0] = '\0';
size_t used = 0;
char* conversation_role = NULL;
char* conversation_content = NULL;
int n = cJSON_GetArraySize(root);
(void)appendf(&out, &cap, &used, "Sections: %d\n\n", n);
for (int i = 0; i < n; i++) {
cJSON* msg = cJSON_GetArrayItem(root, i);
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
const char* role_s = (role && cJSON_IsString(role) && role->valuestring)
? role->valuestring
: "unknown";
const char* content_s = "";
if (content && cJSON_IsString(content) && content->valuestring) {
content_s = content->valuestring;
} else if (content && cJSON_IsNull(content)) {
content_s = "<null>";
}
const char* section = agent_classify_message_part(msg, i);
if (section && strcmp(section, "dm_history") == 0) {
continue;
}
if (section && strcmp(section, "conversation") == 0) {
free(conversation_role);
free(conversation_content);
conversation_role = strdup(role_s);
conversation_content = strdup(content_s);
continue;
}
char* display_content = demote_markdown_headings_for_log(content_s);
const char* safe_display_content = display_content ? display_content : content_s;
if (appendf(&out,
&cap,
&used,
"# %s | role=%s\n\n"
"%s\n\n",
section ? section : "context_part",
role_s,
safe_display_content) != 0) {
free(display_content);
free(out);
cJSON_Delete(root);
return strdup(payload);
}
free(display_content);
}
const char* admin_label = admin_label_for_log();
size_t chat_cap = 4096U;
char* chat = (char*)malloc(chat_cap);
size_t chat_used = 0;
char* latest_admin_line = NULL;
if (chat) {
chat[0] = '\0';
for (int i = 0; i < n; i++) {
cJSON* msg = cJSON_GetArrayItem(root, i);
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
cJSON* ts = msg ? cJSON_GetObjectItemCaseSensitive(msg, "_ts") : NULL;
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
const char* section = agent_classify_message_part(msg, i);
if (!section || strcmp(section, "dm_history") != 0) {
continue;
}
time_t created_at = 0;
if (ts && cJSON_IsNumber(ts)) {
created_at = (time_t)ts->valuedouble;
}
char time_buf[16] = {0};
format_chat_time_for_log(created_at, time_buf);
const char* who = (strcmp(role_s, "user") == 0) ? admin_label : "Agent";
if (appendf(&chat, &chat_cap, &chat_used, "%s %s %s\n", time_buf, who, content_s) != 0) {
free(chat);
chat = NULL;
break;
}
if (strcmp(role_s, "user") == 0) {
free(latest_admin_line);
int line_len = snprintf(NULL, 0, "%s %s %s", time_buf, who, content_s);
if (line_len >= 0) {
latest_admin_line = (char*)malloc((size_t)line_len + 1U);
if (latest_admin_line) {
snprintf(latest_admin_line, (size_t)line_len + 1U, "%s %s %s", time_buf, who, content_s);
}
}
}
}
if (chat && chat_used > 0) {
(void)appendf(&out, &cap, &used, "# dm_history | role=chat\n\n%s\n", chat);
}
}
if (conversation_role && conversation_content) {
(void)appendf(&out,
&cap,
&used,
"# conversation | role=%s\n\n%s\n\n",
conversation_role,
conversation_content);
}
free(chat);
free(latest_admin_line);
free(conversation_role);
free(conversation_content);
cJSON_Delete(root);
return out;
}
static void clear_context_part_names_locked(void) {
for (int i = 0; i < g_context_part_names_count; i++) {
free(g_context_part_names[i]);
@@ -1130,22 +939,29 @@ static const char* get_context_part_name_copy(int idx) {
return tls_name[0] ? tls_name : NULL;
}
static void template_emit_hook(const char* section_name, int message_index, void* user_data) {
static __attribute__((unused)) void template_emit_hook(const char* section_name, int message_index, void* user_data) {
(void)user_data;
set_context_part_name(message_index, section_name ? section_name : "context_part");
}
static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
if (!phase) {
return;
}
if (strcmp(phase, "llm_chat_with_tools_messages") != 0 &&
strcmp(phase, "llm_trigger_with_tools_messages") != 0) {
return;
}
time_t now = time(NULL);
struct tm tm_info;
localtime_r(&now, &tm_info);
char timestamp[32] = {0};
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info);
char* pretty_payload = format_context_payload_for_log(context_payload);
const char* safe_phase = phase ? phase : "unknown";
const char* safe_sender = sender_pubkey_hex ? sender_pubkey_hex : "unknown";
const char* safe_payload = pretty_payload ? pretty_payload : "";
const char* safe_payload = context_payload ? context_payload : "";
size_t context_bytes = context_payload ? strlen(context_payload) : 0U;
size_t approx_tokens = context_bytes / 4U;
@@ -1158,7 +974,7 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase,
int entry_len = snprintf(NULL,
0,
"```text\n"
"Context Log - not seen by model\n"
"Context Log - seen by model\n"
"timestamp=%s\n"
"phase=%s\n"
"sender=%s\n"
@@ -1175,20 +991,18 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase,
approx_tokens,
safe_payload);
if (entry_len < 0) {
free(pretty_payload);
return;
}
char* entry = (char*)malloc((size_t)entry_len + 1U);
if (!entry) {
free(pretty_payload);
return;
}
snprintf(entry,
(size_t)entry_len + 1U,
"```text\n"
"Context Log - not seen by model\n"
"Context Log - seen by model\n"
"timestamp=%s\n"
"phase=%s\n"
"sender=%s\n"
@@ -1234,7 +1048,6 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase,
if (!out) {
free(old_data);
free(entry);
free(pretty_payload);
return;
}
@@ -1246,7 +1059,6 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase,
fclose(out);
free(old_data);
free(entry);
free(pretty_payload);
}
void agent_append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
@@ -1254,7 +1066,7 @@ void agent_append_context_log(const char* sender_pubkey_hex, const char* phase,
}
static char* build_sender_verification_text(didactyl_sender_tier_t sender_tier) {
static __attribute__((unused)) char* build_sender_verification_text(didactyl_sender_tier_t sender_tier) {
if (sender_tier == DIDACTYL_SENDER_ADMIN) {
return strdup("This message has been cryptographically verified as coming from your administrator.");
}
@@ -1265,7 +1077,7 @@ static char* build_sender_verification_text(didactyl_sender_tier_t sender_tier)
}
static int append_recent_admin_dm_history(cJSON* messages, const char* current_message) {
static __attribute__((unused)) int append_recent_admin_dm_history(cJSON* messages, const char* current_message) {
if (!messages || !g_cfg) {
return -1;
}
@@ -1454,114 +1266,7 @@ static const char* template_skill_lookup_callback(void* user_data, const char* d
}
static char* resolve_skill_references_local(const char* input) {
if (!input) {
return strdup("");
}
size_t cap = strlen(input) + 128U;
size_t used = 0U;
char* out = (char*)malloc(cap);
if (!out) return NULL;
out[0] = '\0';
const char* p = input;
while (*p) {
const char* open = strstr(p, "{{");
if (!open) {
size_t tail = strlen(p);
if (used + tail + 1U > cap) {
size_t next = cap;
while (used + tail + 1U > next) next *= 2U;
char* grown = (char*)realloc(out, next);
if (!grown) {
free(out);
return NULL;
}
out = grown;
cap = next;
}
memcpy(out + used, p, tail);
used += tail;
out[used] = '\0';
break;
}
size_t prefix_len = (size_t)(open - p);
if (used + prefix_len + 1U > cap) {
size_t next = cap;
while (used + prefix_len + 1U > next) next *= 2U;
char* grown = (char*)realloc(out, next);
if (!grown) {
free(out);
return NULL;
}
out = grown;
cap = next;
}
memcpy(out + used, p, prefix_len);
used += prefix_len;
out[used] = '\0';
const char* close = strstr(open + 2, "}}");
if (!close) {
size_t rem = strlen(open);
if (used + rem + 1U > cap) {
size_t next = cap;
while (used + rem + 1U > next) next *= 2U;
char* grown = (char*)realloc(out, next);
if (!grown) {
free(out);
return NULL;
}
out = grown;
cap = next;
}
memcpy(out + used, open, rem);
used += rem;
out[used] = '\0';
break;
}
size_t var_len = (size_t)(close - (open + 2));
char var[128];
if (var_len >= sizeof(var)) var_len = sizeof(var) - 1U;
memcpy(var, open + 2, var_len);
var[var_len] = '\0';
char* start = var;
while (*start && isspace((unsigned char)*start)) start++;
char* end = start + strlen(start);
while (end > start && isspace((unsigned char)end[-1])) {
end[-1] = '\0';
end--;
}
const char* repl = "";
pthread_mutex_lock(&g_adopted_skills_mutex);
const char* found = adopted_skill_content_lookup_by_d_tag_locked(start);
repl = found ? found : "";
pthread_mutex_unlock(&g_adopted_skills_mutex);
size_t repl_len = strlen(repl);
if (used + repl_len + 1U > cap) {
size_t next = cap;
while (used + repl_len + 1U > next) next *= 2U;
char* grown = (char*)realloc(out, next);
if (!grown) {
free(out);
return NULL;
}
out = grown;
cap = next;
}
memcpy(out + used, repl, repl_len);
used += repl_len;
out[used] = '\0';
p = close + 2;
}
return out;
return prompt_template_resolve_inline_variables(input ? input : "", &g_tools_ctx);
}
static char* build_context_from_triggers(trigger_type_t trigger_type,
@@ -1588,20 +1293,19 @@ static char* build_context_from_triggers(trigger_type_t trigger_type,
int matched = 0;
didactyl_sender_tier_t tier = DIDACTYL_SENDER_STRANGER;
const char* message = NULL;
if (trigger_event && cJSON_IsObject(trigger_event)) {
cJSON* tier_j = cJSON_GetObjectItemCaseSensitive(trigger_event, "tier");
cJSON* msg_j = cJSON_GetObjectItemCaseSensitive(trigger_event, "message");
const char* tier_s = (tier_j && cJSON_IsString(tier_j) && tier_j->valuestring) ? tier_j->valuestring : "stranger";
if (strcmp(tier_s, "admin") == 0) tier = DIDACTYL_SENDER_ADMIN;
else if (strcmp(tier_s, "wot") == 0) tier = DIDACTYL_SENDER_WOT;
if (msg_j && cJSON_IsString(msg_j) && msg_j->valuestring) {
message = msg_j->valuestring;
}
}
char* matched_skill_contents[AGENT_ADOPTED_SKILLS_MAX];
int matched_skill_count = 0;
memset(matched_skill_contents, 0, sizeof(matched_skill_contents));
pthread_mutex_lock(&g_adopted_skills_mutex);
for (int i = 0; i < g_adopted_skills_count; i++) {
for (int i = 0; i < g_adopted_skills_count && matched_skill_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
const agent_adopted_skill_t* s = &g_adopted_skills[i];
if (!s->has_trigger || s->trigger_type != trigger_type) {
continue;
@@ -1610,8 +1314,16 @@ static char* build_context_from_triggers(trigger_type_t trigger_type,
continue;
}
char* expanded = resolve_skill_references_local(s->content ? s->content : "");
const char* skill_text = expanded ? expanded : (s->content ? s->content : "");
matched_skill_contents[matched_skill_count] = strdup(s->content ? s->content : "");
if (matched_skill_contents[matched_skill_count]) {
matched_skill_count++;
}
}
pthread_mutex_unlock(&g_adopted_skills_mutex);
for (int i = 0; i < matched_skill_count; i++) {
char* expanded = resolve_skill_references_local(matched_skill_contents[i] ? matched_skill_contents[i] : "");
const char* skill_text = expanded ? expanded : (matched_skill_contents[i] ? matched_skill_contents[i] : "");
size_t need = strlen("\n\n---\n\n") + strlen(skill_text) + 1U;
if (used + need >= cap) {
@@ -1620,7 +1332,9 @@ static char* build_context_from_triggers(trigger_type_t trigger_type,
char* grown = (char*)realloc(out, next);
if (!grown) {
free(expanded);
pthread_mutex_unlock(&g_adopted_skills_mutex);
for (int j = i; j < matched_skill_count; j++) {
free(matched_skill_contents[j]);
}
free(out);
return NULL;
}
@@ -1639,8 +1353,8 @@ static char* build_context_from_triggers(trigger_type_t trigger_type,
matched++;
free(expanded);
free(matched_skill_contents[i]);
}
pthread_mutex_unlock(&g_adopted_skills_mutex);
if (matched == 0) {
const char* fallback = "You are an AI agent. Respond to the message.";
@@ -1658,27 +1372,6 @@ static char* build_context_from_triggers(trigger_type_t trigger_type,
used = need - 1U;
}
if (trigger_type == TRIGGER_TYPE_DM && message && message[0] != '\0') {
const char* suffix = "\n\nIncoming DM message:\n";
size_t need = strlen(suffix) + strlen(message) + 1U;
if (used + need >= cap) {
size_t next = cap;
while (used + need >= next) next *= 2U;
char* grown = (char*)realloc(out, next);
if (!grown) {
free(out);
return NULL;
}
out = grown;
cap = next;
}
memcpy(out + used, suffix, strlen(suffix));
used += strlen(suffix);
memcpy(out + used, message, strlen(message));
used += strlen(message);
out[used] = '\0';
}
return out;
}
@@ -1853,56 +1546,6 @@ static int refresh_adopted_skills_cache_if_needed(void) {
cJSON_Delete(adoption_events);
if (tmp_count == 0 && g_cfg->startup_event_count > 0 && g_cfg->startup_events) {
for (int i = 0; i < g_cfg->startup_event_count && tmp_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
startup_event_t* se = &g_cfg->startup_events[i];
if (!se || (se->kind != 31123 && se->kind != 31124) || !se->content || se->content[0] == '\0') {
continue;
}
char d_tag_val[65] = {0};
if (se->tags_json) {
cJSON* tags = cJSON_Parse(se->tags_json);
cJSON* d_tag = tags ? find_tag_value_string_local(tags, "d") : NULL;
if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && d_tag->valuestring[0] != '\0') {
snprintf(d_tag_val, sizeof(d_tag_val), "%s", d_tag->valuestring);
}
cJSON_Delete(tags);
}
if (d_tag_val[0] == '\0') {
snprintf(d_tag_val, sizeof(d_tag_val), "startup-skill-%d", i + 1);
}
tmp[tmp_count].kind = se->kind;
snprintf(tmp[tmp_count].author_pubkey_hex,
sizeof(tmp[tmp_count].author_pubkey_hex),
"%s",
g_cfg->keys.public_key_hex[0] != '\0'
? g_cfg->keys.public_key_hex
: "unknown");
snprintf(tmp[tmp_count].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", d_tag_val);
tmp[tmp_count].content = strdup(se->content);
tmp[tmp_count].has_trigger = 0;
tmp[tmp_count].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
tmp[tmp_count].filter_json[0] = '\0';
if (se->tags_json && se->tags_json[0] != '\0') {
cJSON* tags = cJSON_Parse(se->tags_json);
cJSON* trigger_tag = tags ? find_tag_value_string_local(tags, "trigger") : NULL;
cJSON* filter_tag = tags ? find_tag_value_string_local(tags, "filter") : NULL;
if (trigger_tag && cJSON_IsString(trigger_tag) && trigger_tag->valuestring && trigger_tag->valuestring[0] != '\0' &&
filter_tag && cJSON_IsString(filter_tag) && filter_tag->valuestring && filter_tag->valuestring[0] != '\0') {
tmp[tmp_count].has_trigger = 1;
tmp[tmp_count].trigger_type = trigger_type_from_string(trigger_tag->valuestring);
snprintf(tmp[tmp_count].filter_json, sizeof(tmp[tmp_count].filter_json), "%s", filter_tag->valuestring);
}
cJSON_Delete(tags);
}
if (tmp[tmp_count].content) {
tmp_count++;
}
}
}
pthread_mutex_lock(&g_adopted_skills_mutex);
clear_adopted_skills_cache_locked();
for (int i = 0; i < tmp_count; i++) {
@@ -1972,7 +1615,7 @@ static char* flatten_skill_content_to_text(const char* skill_content) {
return out;
}
static int append_adopted_skills_context(cJSON* messages) {
static __attribute__((unused)) int append_adopted_skills_context(cJSON* messages) {
if (!messages || !g_cfg) {
return -1;
}
@@ -2082,7 +1725,7 @@ static int append_adopted_skills_context(cJSON* messages) {
return rc;
}
static cJSON* build_recent_admin_dm_history_messages(const char* current_message) {
static __attribute__((unused)) cJSON* build_recent_admin_dm_history_messages(const char* current_message) {
cJSON* messages = cJSON_CreateArray();
if (!messages) {
return NULL;
@@ -2094,20 +1737,14 @@ static cJSON* build_recent_admin_dm_history_messages(const char* current_message
return messages;
}
int agent_init(didactyl_config_t* config, const char* system_context) {
if (!config || !system_context) {
int agent_init(didactyl_config_t* config) {
if (!config) {
return -1;
}
g_cfg = config;
g_system_context = strdup(system_context);
if (!g_system_context) {
return -1;
}
if (tools_init(&g_tools_ctx, g_cfg) != 0) {
free(g_system_context);
g_system_context = NULL;
g_cfg = NULL;
return -1;
}
@@ -2124,8 +1761,6 @@ int agent_init(didactyl_config_t* config, const char* system_context) {
g_tools_ctx.template_skill_lookup = template_skill_lookup_callback;
g_tools_ctx.template_skill_lookup_user_data = NULL;
prompt_template_free(&g_prompt_template);
g_has_prompt_template = (prompt_template_parse(g_system_context, &g_prompt_template) == 0);
clear_context_part_names();
return 0;
@@ -2322,82 +1957,50 @@ void agent_on_trigger(const char* skill_d_tag,
int agent_build_admin_messages_json(const char* current_user_message,
didactyl_sender_tier_t sender_tier,
char** out_messages_json) {
if (!out_messages_json || !g_cfg || !g_system_context) {
if (!out_messages_json || !g_cfg) {
return -1;
}
*out_messages_json = NULL;
clear_context_part_names();
cJSON* messages = NULL;
cJSON* dm_event = cJSON_CreateObject();
if (!dm_event) {
return -1;
}
cJSON_AddStringToObject(dm_event, "type", "dm");
cJSON_AddStringToObject(dm_event, "sender_pubkey", g_cfg->admin.pubkey);
cJSON_AddStringToObject(dm_event, "message", current_user_message ? current_user_message : "");
cJSON_AddStringToObject(dm_event,
"tier",
sender_tier == DIDACTYL_SENDER_ADMIN ? "admin" :
(sender_tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger"));
cJSON_AddNumberToObject(dm_event, "created_at", (double)time(NULL));
if (g_has_prompt_template) {
cJSON* dm_history = build_recent_admin_dm_history_messages(current_user_message);
if (!dm_history) {
dm_history = cJSON_CreateArray();
}
char* composed_context = build_context_from_triggers(TRIGGER_TYPE_DM, NULL, dm_event, "api");
cJSON_Delete(dm_event);
if (!composed_context) {
composed_context = strdup("You are an AI agent. Respond to the message.");
}
llm_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
const char* provider_name = NULL;
if (llm_get_config(&cfg) == 0 && cfg.provider[0] != '\0') {
provider_name = cfg.provider;
}
cJSON* messages = cJSON_CreateArray();
if (!messages ||
append_simple_message(messages, "system", composed_context ? composed_context : "You are an AI agent. Respond to the message.") != 0 ||
append_simple_message(messages, "user", current_user_message ? current_user_message : "") != 0) {
free(composed_context);
cJSON_Delete(messages);
return -1;
}
free(composed_context);
g_tools_ctx.template_sender_tier = (int)sender_tier;
g_tools_ctx.template_current_user_message = current_user_message;
g_tools_ctx.template_trigger_event_json = NULL;
messages = prompt_template_build_messages(&g_prompt_template,
provider_name,
&g_tools_ctx,
dm_history,
AGENT_HISTORY_TURNS,
template_emit_hook,
NULL);
cJSON_Delete(dm_history);
if (!messages) {
return -1;
}
} else {
messages = cJSON_CreateArray();
if (!messages) {
return -1;
}
char agent_identity[192];
snprintf(agent_identity,
sizeof(agent_identity),
"Agent identity\nYour pubkey (hex): %s",
g_cfg->keys.public_key_hex[0] != '\0' ? g_cfg->keys.public_key_hex : "unknown");
char* sender_verification = build_sender_verification_text(sender_tier);
if (!sender_verification ||
append_simple_message(messages, "system", g_system_context) != 0 ||
append_simple_message(messages, "system", agent_identity) != 0 ||
append_simple_message(messages, "system", sender_verification) != 0 ||
append_adopted_skills_context(messages) != 0) {
free(sender_verification);
cJSON_Delete(messages);
return -1;
}
free(sender_verification);
if (append_recent_admin_dm_history(messages, current_user_message) != 0) {
cJSON_Delete(messages);
return -1;
}
int n = cJSON_GetArraySize(messages);
for (int i = 0; i < n; i++) {
cJSON* msg = cJSON_GetArrayItem(messages, i);
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
set_context_part_name(i, detect_context_section(role_s, content_s, i));
}
int n = cJSON_GetArraySize(messages);
for (int i = 0; i < n; i++) {
cJSON* msg = cJSON_GetArrayItem(messages, i);
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
set_context_part_name(i, detect_context_section(role_s, content_s, i));
}
char* messages_json = cJSON_PrintUnformatted(messages);
@@ -2420,7 +2023,7 @@ void agent_on_message(const char* sender_pubkey_hex,
void* user_data) {
(void)user_data;
if (!g_cfg || !g_system_context || !sender_pubkey_hex || !message) {
if (!g_cfg || !sender_pubkey_hex || !message) {
return;
}
@@ -2516,7 +2119,6 @@ void agent_on_message(const char* sender_pubkey_hex,
cJSON* messages = cJSON_CreateArray();
if (!messages ||
append_simple_message(messages, "system", dm_context ? dm_context : "You are an AI agent. Respond to the message.") != 0 ||
append_recent_admin_dm_history(messages, message) != 0 ||
append_simple_message(messages, "user", message) != 0) {
cJSON_Delete(messages);
free(tools_json);
@@ -2670,8 +2272,6 @@ void agent_on_message(const char* sender_pubkey_hex,
void agent_cleanup(void) {
tools_cleanup(&g_tools_ctx);
free(g_system_context);
g_system_context = NULL;
g_cfg = NULL;
g_trigger_manager = NULL;
memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
@@ -2684,6 +2284,4 @@ void agent_cleanup(void) {
pthread_mutex_unlock(&g_adopted_skills_mutex);
clear_context_part_names();
prompt_template_free(&g_prompt_template);
g_has_prompt_template = 0;
}

View File

@@ -8,7 +8,7 @@
struct trigger_manager;
int agent_init(didactyl_config_t* config, const char* system_context);
int agent_init(didactyl_config_t* config);
void agent_set_trigger_manager(struct trigger_manager* trigger_manager);
void agent_on_trigger(const char* skill_d_tag,
const char* skill_content,

View File

@@ -1,9 +1,9 @@
#ifndef DIDACTYL_DEFAULT_EVENTS_H
#define DIDACTYL_DEFAULT_EVENTS_H
#define DIDACTYL_DEFAULT_SKILL_D_TAG "didactyl-default"
#define DIDACTYL_DEFAULT_SKILL_D_TAG "default_admin_dm"
#define DIDACTYL_DEFAULT_SKILL_KIND 31124
#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"didactyl-default\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"]]"
#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"default_admin_dm\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]"
#define DIDACTYL_STARTUP_KIND_PROFILE 0
#define DIDACTYL_STARTUP_KIND_CONTACTS 3
@@ -31,83 +31,19 @@ static const char* DIDACTYL_DEFAULT_RELAYS[] = {
#define DIDACTYL_DEFAULT_RELAY_COUNT ((int)(sizeof(DIDACTYL_DEFAULT_RELAYS) / sizeof(DIDACTYL_DEFAULT_RELAYS[0])))
static const char* DIDACTYL_DEFAULT_SKILL_TEMPLATE =
"# %s Agent\n\n"
"You are %s, a sovereign AI agent living on Nostr.\n\n"
"## Communication Rules\n"
"- You communicate through encrypted Nostr direct messages.\n"
"- Keep responses concise and clear.\n\n"
"## Behavior\n"
"- Be helpful and technically accurate.\n"
"- If unsure, state uncertainty directly.\n"
"- Prefer actionable, practical advice.\n"
"- Use the person's name when messaging them if you know it.\n"
"- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n"
"## Tool Use Policy\n"
"- You have tools available and should use them when a request requires taking action.\n"
"- For requests involving local inspection or command execution, call `local_shell_exec` instead of refusing.\n"
"- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n"
"- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n"
"- After a tool call, base your answer on the actual tool result.\n"
"- Never claim a tool was run if no tool was executed.\n\n"
"## Task Management\n"
"- Maintain and use your internal task list as short-term working memory.\n"
"- Break long or complex actions into clear tasks before executing them.\n"
"- Update task status as you complete steps so your plan stays accurate.\n\n"
"## Safety\n"
"- Do not claim to have executed actions you did not execute.\n"
"- You may share your public key (npub) with anyone.\n"
"- Never reveal your private key (nsec) under any circumstance.\n\n"
"---template---\n\n"
"- section: admin_identity\n"
" role: system\n"
" tool: admin_identity\n"
" skip_if_empty: true\n\n"
"- section: admin_profile\n"
" role: system\n"
" tool: nostr_admin_profile\n"
" skip_if_empty: true\n\n"
"- section: admin_contacts\n"
" role: system\n"
" tool: nostr_admin_contacts\n"
" skip_if_empty: true\n\n"
"- section: admin_relays\n"
" role: system\n"
" tool: nostr_admin_relays\n"
" skip_if_empty: true\n\n"
"- section: admin_notes\n"
" role: system\n"
" tool: nostr_admin_notes\n"
" skip_if_empty: true\n\n"
"- section: agent_identity\n"
" role: system\n"
" tool: agent_identity\n"
" skip_if_empty: true\n\n"
"- section: agent_profile\n"
" role: system\n"
" tool: nostr_agent_profile\n"
" skip_if_empty: true\n\n"
"- section: agent_contacts\n"
" role: system\n"
" tool: nostr_agent_contacts\n"
" skip_if_empty: true\n\n"
"- section: agent_relays\n"
" role: system\n"
" tool: nostr_agent_relays\n"
" skip_if_empty: true\n\n"
"- section: agent_notes\n"
" role: system\n"
" tool: nostr_agent_notes\n"
" skip_if_empty: true\n\n"
"- section: tasks\n"
" role: system\n"
" tool: task_list\n"
" skip_if_empty: true\n\n"
"- section: dm_history\n"
" role: expand\n"
" limit: 12\n\n"
"- section: conversation\n"
" role: user\n"
" tool: message_current\n"
" skip_if_empty: true\n";
"# Didactyl Agent\n\n"
"You are {{my_kind0_profile}}\n\n"
"Your npub: {{my_npub}}\n\n"
"## Rules\n\n"
"- Communicate through encrypted Nostr direct messages\n"
"- Keep responses concise and clear\n"
"- Be helpful and technically accurate\n"
"- If unsure, state uncertainty directly\n"
"- Use tools when a request requires taking action\n"
"- After a tool call, base your answer on the actual tool result\n"
"- Never claim a tool was run if no tool was executed\n"
"- Maintain your task list as short-term working memory\n"
"- Never reveal your private key (nsec)\n"
"- You may share your public key (npub) with anyone";
#endif

View File

@@ -56,8 +56,15 @@ static void on_self_skill_eose_load_triggers(int event_count, void* user_data) {
}
}
static volatile sig_atomic_t g_sigint_count = 0;
static void signal_handler(int signum) {
(void)signum;
if (signum == SIGINT) {
g_sigint_count++;
if (g_sigint_count >= 2) {
_exit(130);
}
}
g_running = 0;
}
@@ -1245,9 +1252,6 @@ int main(int argc, char** argv) {
}
} else {
DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run");
if (nostr_handler_load_system_context_from_adopted_skills() != 0) {
DEBUG_WARN("[didactyl] startup phase: failed to load system context from adopted skills (continuing with fallback)");
}
}
if (persist_runtime_config_to_nostr(&cfg, llm_recalled_from_nostr ? 0 : 1) != 0) {
@@ -1258,19 +1262,8 @@ int main(int argc, char** argv) {
startup_step_ok(7, "Reconcile/persist startup state", NULL);
const char* system_context = nostr_handler_get_system_context();
char system_context_fallback[512] = {0};
if (!system_context || system_context[0] == '\0') {
const char* startup_name = nostr_handler_get_startup_display_name();
snprintf(system_context_fallback,
sizeof(system_context_fallback),
"You are %s, a sovereign AI agent living on Nostr.",
(startup_name && startup_name[0] != '\0') ? startup_name : "the agent");
system_context = system_context_fallback;
}
startup_step_begin(8, "Initialize agent");
if (agent_init(&cfg, system_context) != 0) {
if (agent_init(&cfg) != 0) {
startup_step_fail(8, "Initialize agent", "agent_init failed");
fprintf(stderr, "Failed to initialize agent\n");
nostr_block_list_cleanup();
@@ -1324,9 +1317,9 @@ int main(int argc, char** argv) {
fprintf(stderr, "Startup aborted: could not retrieve valid self kind 10002 within %d ms\n", kind10002_timeout_ms);
agent_cleanup();
nostr_block_list_cleanup();
trigger_manager_cleanup(&trigger_manager);
nostr_handler_cleanup();
llm_cleanup();
trigger_manager_cleanup(&trigger_manager);
config_free(&cfg);
nostr_cleanup();
return 1;
@@ -1340,9 +1333,9 @@ int main(int argc, char** argv) {
startup_step_fail(11, "Discover self relay list via kind 10002", "failed to apply discovered relay list to pool");
agent_cleanup();
nostr_block_list_cleanup();
trigger_manager_cleanup(&trigger_manager);
nostr_handler_cleanup();
llm_cleanup();
trigger_manager_cleanup(&trigger_manager);
config_free(&cfg);
nostr_cleanup();
return 1;
@@ -1381,34 +1374,74 @@ int main(int argc, char** argv) {
startup_step_begin(14, "Subscribe self-skill cache");
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin");
if (nostr_handler_subscribe_self_skills() != 0) {
DEBUG_WARN("[didactyl] startup phase: subscribe self skill cache failed (continuing)");
startup_step_fail(14, "Subscribe self-skill cache", "nostr_handler_subscribe_self_skills failed");
DEBUG_ERROR("[didactyl] startup phase: subscribe self skill cache failed");
fprintf(stderr, "Startup aborted: failed to subscribe self-skill cache\n");
agent_cleanup();
nostr_block_list_cleanup();
trigger_manager_cleanup(&trigger_manager);
nostr_handler_cleanup();
llm_cleanup();
config_free(&cfg);
nostr_cleanup();
return 1;
}
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
startup_step_ok(14, "Subscribe self-skill cache", NULL);
char startup_dm[768];
const char* startup_name = nostr_handler_get_startup_display_name();
int connected_relays = nostr_handler_connected_relay_count();
time_t startup_now = time(NULL);
struct tm startup_tm;
char startup_time_str[32] = {0};
if (localtime_r(&startup_now, &startup_tm)) {
(void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm);
} else {
snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now);
const int self_skill_eose_timeout_ms = 15000;
if (nostr_handler_wait_for_self_skill_eose(self_skill_eose_timeout_ms) != 0) {
startup_step_fail(14,
"Subscribe self-skill cache",
"self-skill EOSE not received within timeout");
fprintf(stderr,
"Startup aborted: self-skill EOSE not received within %d ms\n",
self_skill_eose_timeout_ms);
fprintf(stderr,
"Check relay connectivity (connected=%d/%d) and ensure skill events exist on relays\n",
nostr_handler_connected_relay_count(),
cfg.relay_count);
fprintf(stderr,
"If this is a first run, use genesis to publish startup events\n");
agent_cleanup();
nostr_block_list_cleanup();
trigger_manager_cleanup(&trigger_manager);
nostr_handler_cleanup();
llm_cleanup();
config_free(&cfg);
nostr_cleanup();
return 1;
}
snprintf(startup_dm,
sizeof(startup_dm),
"%s has started up and is online at %s (version %s, connected relays: %d/%d).",
(startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
startup_time_str,
DIDACTYL_VERSION,
connected_relays,
cfg.relay_count);
if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) {
DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin");
int startup_skill_count = 0;
int startup_adoption_count = 0;
if (nostr_handler_validate_self_skill_cache(&startup_skill_count, &startup_adoption_count) != 0) {
startup_step_fail(14,
"Subscribe self-skill cache",
"no skill events found after EOSE");
fprintf(stderr,
"Startup aborted: no skill events (kind 31123/31124) found in self-skill cache\n");
fprintf(stderr,
"The agent requires at least one skill event to operate\n");
fprintf(stderr,
"Run genesis to publish the default skill, or publish a skill manually\n");
agent_cleanup();
nostr_block_list_cleanup();
trigger_manager_cleanup(&trigger_manager);
nostr_handler_cleanup();
llm_cleanup();
config_free(&cfg);
nostr_cleanup();
return 1;
}
char self_skill_detail[128];
snprintf(self_skill_detail,
sizeof(self_skill_detail),
"skills=%d adoptions=%d",
startup_skill_count,
startup_adoption_count);
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
startup_step_ok(14, "Subscribe self-skill cache", self_skill_detail);
startup_step_begin(15, "Subscribe DMs");
DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin");
if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) {
@@ -1417,9 +1450,9 @@ int main(int argc, char** argv) {
fprintf(stderr, "Failed to subscribe to DMs\n");
agent_cleanup();
nostr_block_list_cleanup();
trigger_manager_cleanup(&trigger_manager);
nostr_handler_cleanup();
llm_cleanup();
trigger_manager_cleanup(&trigger_manager);
config_free(&cfg);
nostr_cleanup();
return 1;
@@ -1456,9 +1489,21 @@ int main(int argc, char** argv) {
}
}
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
signal(SIGPIPE, SIG_IGN);
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
(void)sigaction(SIGINT, &sa, NULL);
(void)sigaction(SIGTERM, &sa, NULL);
struct sigaction sa_pipe;
memset(&sa_pipe, 0, sizeof(sa_pipe));
sa_pipe.sa_handler = SIG_IGN;
sigemptyset(&sa_pipe.sa_mask);
sa_pipe.sa_flags = 0;
(void)sigaction(SIGPIPE, &sa_pipe, NULL);
int http_api_started = 0;
if (cfg.api.enabled) {
@@ -1492,6 +1537,29 @@ int main(int argc, char** argv) {
nostr_handler_refresh_relay_statuses();
char startup_dm[768];
const char* startup_name = nostr_handler_get_startup_display_name();
int connected_relays = nostr_handler_connected_relay_count();
time_t startup_now = time(NULL);
struct tm startup_tm;
char startup_time_str[32] = {0};
if (localtime_r(&startup_now, &startup_tm)) {
(void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm);
} else {
snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now);
}
snprintf(startup_dm,
sizeof(startup_dm),
"%s has started up and is online at %s (version %s, connected relays: %d/%d).",
(startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
startup_time_str,
DIDACTYL_VERSION,
connected_relays,
cfg.relay_count);
if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) {
DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin");
}
startup_step_ok(18, "READY", "agent online; entering main poll loop");
DEBUG_INFO("[didactyl] entering main poll loop");
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
@@ -1515,9 +1583,9 @@ int main(int argc, char** argv) {
cashu_wallet_cleanup();
agent_cleanup();
nostr_block_list_cleanup();
trigger_manager_cleanup(&trigger_manager);
nostr_handler_cleanup();
llm_cleanup();
trigger_manager_cleanup(&trigger_manager);
config_free(&cfg);
nostr_cleanup();

View File

@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 2
#define DIDACTYL_VERSION_PATCH 1
#define DIDACTYL_VERSION "v0.2.1"
#define DIDACTYL_VERSION_PATCH 10
#define DIDACTYL_VERSION "v0.2.10"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"

View File

@@ -25,7 +25,6 @@ static void* g_dm_user_data = NULL;
static int g_poll_counter = 0;
static time_t g_start_time = 0;
static nostr_pool_relay_status_t* g_last_relay_statuses = NULL;
static char* g_system_context = NULL;
static unsigned char* g_startup_published = NULL;
static int g_startup_publish_tracking_enabled = 0;
static int g_startup_kind1_already_exists = 0;
@@ -97,6 +96,7 @@ static external_subscription_wrapper_t* g_external_sub_wrappers = NULL;
static cJSON* g_self_skill_events = NULL;
static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER;
static volatile int g_self_skill_eose_received = 0;
#define DM_DEDUP_CACHE_SIZE 256
@@ -147,9 +147,28 @@ static void nostr_core_log_bridge(int level, const char* component, const char*
const char* msg = message ? message : "";
switch (mapped) {
case DEBUG_LEVEL_TRACE:
case DEBUG_LEVEL_TRACE: {
int is_websocket = (strcmp(comp, "websocket") == 0);
int is_frame = (strncmp(msg, "SEND ", 5) == 0 || strncmp(msg, "RECV ", 5) == 0);
size_t msg_len = strlen(msg);
if (is_websocket && is_frame && msg_len > 384U) {
const char* direction = (strncmp(msg, "SEND ", 5) == 0) ? "SEND" : "RECV";
const char* frame_type = "frame";
if (strstr(msg, "[\"EVENT\"")) frame_type = "EVENT";
else if (strstr(msg, "[\"REQ\"")) frame_type = "REQ";
else if (strstr(msg, "[\"CLOSE\"")) frame_type = "CLOSE";
else if (strstr(msg, "[\"EOSE\"")) frame_type = "EOSE";
else if (strstr(msg, "[\"NOTICE\"")) frame_type = "NOTICE";
DEBUG_TRACE("[didactyl] [nostr:%s] %s <large %s frame suppressed, bytes=%zu>",
comp,
direction,
frame_type,
msg_len);
break;
}
DEBUG_TRACE("[didactyl] [nostr:%s] %s", comp, msg);
break;
}
case DEBUG_LEVEL_DEBUG:
DEBUG_LOG("[didactyl] [nostr:%s] %s", comp, msg);
break;
@@ -430,7 +449,6 @@ static int managed_subscription_register_locked(managed_subscription_id_t id,
static int managed_subscriptions_resubscribe_all_locked(void);
static int managed_subscription_id_from_name(const char* name, managed_subscription_id_t* out_id);
static int apply_runtime_kind10002_relays(cJSON* tags, const char* source_label);
static void seed_default_skill_into_cache(void);
static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url) {
if (!relays || relay_count <= 0 || !relay_url || relay_url[0] == '\0') {
@@ -1000,7 +1018,7 @@ static int parse_enabled_flag_local(const char* enabled_s) {
return (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
}
static int parse_skill_address_local(const char* addr, int* out_kind, char out_pubkey_hex[65], char out_d_tag[65]) {
static __attribute__((unused)) int parse_skill_address_local(const char* addr, int* out_kind, char out_pubkey_hex[65], char out_d_tag[65]) {
if (!addr || !out_kind || !out_pubkey_hex || !out_d_tag) {
return -1;
}
@@ -1278,50 +1296,6 @@ static void self_skill_cache_upsert_event_locked(cJSON* event) {
cJSON_AddItemToArray(g_self_skill_events, dup);
}
static void seed_default_skill_into_cache(void) {
if (!g_cfg) {
return;
}
if (g_cfg->default_skill.kind != 31123 && g_cfg->default_skill.kind != 31124) {
return;
}
if (g_cfg->keys.public_key_hex[0] == '\0' ||
!g_cfg->default_skill.content || g_cfg->default_skill.content[0] == '\0' ||
!g_cfg->default_skill.tags_json || g_cfg->default_skill.tags_json[0] == '\0') {
return;
}
cJSON* tags = cJSON_Parse(g_cfg->default_skill.tags_json);
if (!tags || !cJSON_IsArray(tags)) {
cJSON_Delete(tags);
return;
}
cJSON* event = cJSON_CreateObject();
if (!event) {
cJSON_Delete(tags);
return;
}
const double now = (double)time(NULL);
cJSON_AddNumberToObject(event, "kind", g_cfg->default_skill.kind);
cJSON_AddStringToObject(event, "pubkey", g_cfg->keys.public_key_hex);
cJSON_AddStringToObject(event, "content", g_cfg->default_skill.content);
cJSON_AddItemToObject(event, "tags", tags);
cJSON_AddNumberToObject(event, "created_at", now);
cJSON_AddStringToObject(event,
"id",
"0000000000000000000000000000000000000000000000000000000000000000");
pthread_mutex_lock(&g_self_skill_mutex);
self_skill_cache_upsert_event_locked(event);
pthread_mutex_unlock(&g_self_skill_mutex);
cJSON_Delete(event);
}
static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) {
if (!hex || !out_pubkey || strlen(hex) != 64U) {
return -1;
@@ -1735,6 +1709,7 @@ static void on_eose(cJSON** events, int event_count, void* user_data) {
static void on_self_skill_eose(cJSON** events, int event_count, void* user_data) {
(void)events;
(void)user_data;
g_self_skill_eose_received = 1;
DEBUG_INFO("[didactyl] self-skill EOSE received: cached skill events=%d", event_count);
if (g_self_skill_eose_cb) {
@@ -2144,7 +2119,6 @@ static void on_agent_context_event(cJSON* event, const char* relay_url, void* us
}
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data) {
(void)relay_url;
(void)user_data;
if (!event || !g_cfg) {
@@ -2155,6 +2129,20 @@ static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_
return;
}
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id");
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1;
const char* d_tag = (tags && cJSON_IsArray(tags)) ? find_tag_value_local(tags, "d") : NULL;
DEBUG_INFO("[didactyl] self-skill cache received kind=%d d_tag=%s id=%s created_at=%lld via %s",
kind_val,
(d_tag && d_tag[0] != '\0') ? d_tag : "-",
(id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "<no-id>",
(long long)((created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0),
relay_url ? relay_url : "unknown relay");
register_trigger_from_self_skill_event(event);
pthread_mutex_lock(&g_self_skill_mutex);
@@ -2452,6 +2440,7 @@ int nostr_handler_subscribe_self_skills(void) {
}
int rc = 0;
g_self_skill_eose_received = 0;
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
@@ -2480,9 +2469,9 @@ int nostr_handler_subscribe_self_skills(void) {
NULL,
0,
0,
NOSTR_POOL_EOSE_FULL_SET,
30,
120,
NOSTR_POOL_EOSE_FIRST,
12,
14,
1) != 0) {
rc = -1;
}
@@ -2701,6 +2690,77 @@ void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callb
g_self_skill_eose_user_data = user_data;
}
int nostr_handler_wait_for_self_skill_eose(int timeout_ms) {
if (!g_pool || timeout_ms <= 0) {
return -1;
}
struct timespec ts_start;
if (clock_gettime(CLOCK_MONOTONIC, &ts_start) != 0) {
return -1;
}
while (!g_self_skill_eose_received) {
(void)nostr_handler_poll(100);
struct timespec ts_now;
if (clock_gettime(CLOCK_MONOTONIC, &ts_now) != 0) {
return -1;
}
long long elapsed_ms = (long long)(ts_now.tv_sec - ts_start.tv_sec) * 1000LL +
(long long)(ts_now.tv_nsec - ts_start.tv_nsec) / 1000000LL;
if (elapsed_ms >= (long long)timeout_ms) {
return -1;
}
}
return 0;
}
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count) {
int skill_count = 0;
int adoption_count = 0;
pthread_mutex_lock(&g_self_skill_mutex);
if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
int n = cJSON_GetArraySize(g_self_skill_events);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
cJSON* id = ev ? cJSON_GetObjectItemCaseSensitive(ev, "id") : NULL;
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
if (!kind || !cJSON_IsNumber(kind)) {
continue;
}
int k = (int)kind->valuedouble;
if (k == 31123 || k == 31124) {
skill_count++;
} else if (k == 10123) {
adoption_count++;
}
const char* d_tag = (tags && cJSON_IsArray(tags)) ? find_tag_value_local(tags, "d") : NULL;
DEBUG_INFO("[didactyl] self-skill cache event: kind=%d d_tag=%s id=%s created_at=%lld",
k,
(d_tag && d_tag[0] != '\0') ? d_tag : "-",
(id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "<no-id>",
(long long)((created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0));
}
}
pthread_mutex_unlock(&g_self_skill_mutex);
if (out_skill_count) {
*out_skill_count = skill_count;
}
if (out_adoption_count) {
*out_adoption_count = adoption_count;
}
return skill_count > 0 ? 0 : -1;
}
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) {
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) {
return -1;
@@ -3116,14 +3176,24 @@ static void publish_pending_startup_events_for_relay_index(int relay_index, cons
}
}
static const char* startup_kind_label(int kind) {
switch (kind) {
case 0: return "profile";
case 1: return "note";
case 3: return "contacts";
case 10002: return "relay_list";
case 10123: return "adoption_list";
case 31123: return "private_skill";
case 31124: return "skill";
default: return "event";
}
}
int nostr_handler_reconcile_startup_events(void) {
if (!g_cfg || !g_pool) {
return -1;
}
free(g_system_context);
g_system_context = NULL;
free(g_startup_published);
g_startup_published = NULL;
g_startup_publish_tracking_enabled = 0;
@@ -3137,159 +3207,68 @@ int nostr_handler_reconcile_startup_events(void) {
g_startup_publish_tracking_enabled = 1;
}
if (g_cfg->default_skill.content && g_cfg->default_skill.content[0] != '\0') {
g_system_context = strdup(g_cfg->default_skill.content);
}
load_startup_display_name();
g_startup_kind1_already_exists = startup_self_kind1_exists();
if (g_startup_kind1_already_exists) {
DEBUG_INFO("[didactyl] startup phase: existing self kind-1 note found; skipping startup kind-1 publish");
}
if (!g_system_context) {
char default_ctx[512];
snprintf(default_ctx,
sizeof(default_ctx),
"You are %s, a sovereign AI agent living on Nostr.",
g_startup_display_name[0] != '\0' ? g_startup_display_name : "the agent");
g_system_context = strdup(default_ctx);
}
for (int relay_index = 0; relay_index < g_cfg->relay_count; relay_index++) {
publish_pending_startup_events_for_relay_index(relay_index, "startup_reconcile");
}
seed_default_skill_into_cache();
if (g_startup_publish_tracking_enabled && g_startup_published) {
int events_published = 0;
int relays_used = 0;
for (int r = 0; r < g_cfg->relay_count; r++) {
int used = 0;
for (int i = 0; i < g_cfg->startup_event_count; i++) {
size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)r;
if (g_startup_published[slot]) {
used = 1;
break;
}
}
if (used) {
relays_used++;
}
}
for (int i = 0; i < g_cfg->startup_event_count; i++) {
startup_event_t* se = &g_cfg->startup_events[i];
char relays_line[512];
relays_line[0] = '\0';
int relay_hits = 0;
for (int r = 0; r < g_cfg->relay_count; r++) {
size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)r;
if (!g_startup_published[slot]) {
continue;
}
relay_hits++;
if (relays_line[0] != '\0') {
strncat(relays_line, ", ", sizeof(relays_line) - strlen(relays_line) - 1U);
}
strncat(relays_line, g_cfg->relays[r], sizeof(relays_line) - strlen(relays_line) - 1U);
}
if (relay_hits > 0) {
events_published++;
DEBUG_INFO("[didactyl] startup publish detail: kind=%d (%s) -> %s",
se->kind,
startup_kind_label(se->kind),
relays_line[0] ? relays_line : "<none>");
}
}
DEBUG_INFO("[didactyl] startup publish summary: %d event(s) published to %d/%d relay(s)",
events_published,
relays_used,
g_cfg->relay_count);
}
return 0;
}
int nostr_handler_load_system_context_from_adopted_skills(void) {
if (!g_cfg || !g_pool || g_cfg->keys.public_key_hex[0] == '\0') {
return -1;
}
cJSON* adoption_filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
if (!adoption_filter || !kinds || !authors) {
cJSON_Delete(adoption_filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
return -1;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
cJSON_AddItemToObject(adoption_filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(adoption_filter, "authors", authors);
cJSON_AddNumberToObject(adoption_filter, "limit", 1);
char* adoption_events_json = nostr_handler_query_json(adoption_filter, 3000);
cJSON_Delete(adoption_filter);
if (!adoption_events_json) {
return -1;
}
cJSON* adoption_events = cJSON_Parse(adoption_events_json);
free(adoption_events_json);
if (!adoption_events || !cJSON_IsArray(adoption_events) || cJSON_GetArraySize(adoption_events) <= 0) {
cJSON_Delete(adoption_events);
return -1;
}
cJSON* adoption = cJSON_GetArrayItem(adoption_events, 0);
cJSON* tags = adoption ? cJSON_GetObjectItemCaseSensitive(adoption, "tags") : NULL;
if (!tags || !cJSON_IsArray(tags)) {
cJSON_Delete(adoption_events);
return -1;
}
const char* first_addr = NULL;
int tag_n = cJSON_GetArraySize(tags);
for (int i = 0; i < tag_n; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* k = cJSON_GetArrayItem(tag, 0);
cJSON* v = cJSON_GetArrayItem(tag, 1);
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) {
continue;
}
if (strcmp(k->valuestring, "a") == 0 && v->valuestring[0] != '\0') {
first_addr = v->valuestring;
break;
}
}
if (!first_addr) {
cJSON_Delete(adoption_events);
return -1;
}
int skill_kind = 0;
char skill_pubkey_hex[65] = {0};
char skill_d_tag[65] = {0};
if (parse_skill_address_local(first_addr, &skill_kind, skill_pubkey_hex, skill_d_tag) != 0) {
cJSON_Delete(adoption_events);
return -1;
}
cJSON* skill_filter = cJSON_CreateObject();
cJSON* skill_kinds = cJSON_CreateArray();
cJSON* skill_authors = cJSON_CreateArray();
cJSON* d_tags = cJSON_CreateArray();
if (!skill_filter || !skill_kinds || !skill_authors || !d_tags) {
cJSON_Delete(adoption_events);
cJSON_Delete(skill_filter);
cJSON_Delete(skill_kinds);
cJSON_Delete(skill_authors);
cJSON_Delete(d_tags);
return -1;
}
cJSON_AddItemToArray(skill_kinds, cJSON_CreateNumber(skill_kind));
cJSON_AddItemToObject(skill_filter, "kinds", skill_kinds);
cJSON_AddItemToArray(skill_authors, cJSON_CreateString(skill_pubkey_hex));
cJSON_AddItemToObject(skill_filter, "authors", skill_authors);
cJSON_AddItemToArray(d_tags, cJSON_CreateString(skill_d_tag));
cJSON_AddItemToObject(skill_filter, "#d", d_tags);
cJSON_AddNumberToObject(skill_filter, "limit", 1);
char* skill_events_json = nostr_handler_query_json(skill_filter, 3000);
cJSON_Delete(skill_filter);
cJSON_Delete(adoption_events);
if (!skill_events_json) {
return -1;
}
cJSON* skill_events = cJSON_Parse(skill_events_json);
free(skill_events_json);
if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) {
cJSON_Delete(skill_events);
return -1;
}
cJSON* skill_event = cJSON_GetArrayItem(skill_events, 0);
cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL;
if (!content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(skill_events);
return -1;
}
free(g_system_context);
g_system_context = strdup(content->valuestring);
cJSON_Delete(skill_events);
return g_system_context ? 0 : -1;
}
const char* nostr_handler_get_system_context(void) {
return g_system_context;
}
char* nostr_handler_get_self_events_by_kind_json(int kind) {
cJSON* out = cJSON_CreateArray();
if (!out) {
@@ -4212,8 +4191,6 @@ void nostr_handler_cleanup(void) {
g_cfg = NULL;
g_dm_callback = NULL;
g_dm_user_data = NULL;
free(g_system_context);
g_system_context = NULL;
memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids));
g_seen_dm_count = 0;
g_seen_dm_next = 0;

View File

@@ -59,10 +59,10 @@ nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription);
int nostr_handler_poll(int timeout_ms);
int nostr_handler_reconcile_startup_events(void);
int nostr_handler_load_system_context_from_adopted_skills(void);
int nostr_handler_wait_for_self_skill_eose(int timeout_ms);
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count);
void nostr_handler_refresh_relay_statuses(void);
int nostr_handler_sync_relays_from_config(void);
const char* nostr_handler_get_system_context(void);
char* nostr_handler_get_self_skill_events_json(void);
int nostr_handler_is_event_in_self_cache(const char* event_id_hex);
const char* nostr_handler_get_startup_display_name(void);

View File

@@ -1,18 +1,16 @@
#define _POSIX_C_SOURCE 200809L
#include "prompt_template.h"
#include "cjson/cJSON.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char* dup_range(const char* s, size_t n) {
char* out = (char*)malloc(n + 1U);
if (!out) return NULL;
if (n > 0) {
memcpy(out, s, n);
}
if (n > 0) memcpy(out, s, n);
out[n] = '\0';
return out;
}
@@ -32,52 +30,6 @@ static void rtrim_inplace(char* s) {
}
}
static int starts_with(const char* s, const char* prefix) {
if (!s || !prefix) return 0;
size_t n = strlen(prefix);
return strncmp(s, prefix, n) == 0;
}
static int add_line(char*** lines, int* count, int* cap, char* line) {
if (!lines || !count || !cap) return -1;
if (*count >= *cap) {
int next = (*cap == 0) ? 64 : (*cap * 2);
char** grown = (char**)realloc(*lines, (size_t)next * sizeof(char*));
if (!grown) return -1;
*lines = grown;
*cap = next;
}
(*lines)[*count] = line;
(*count)++;
return 0;
}
static int split_lines_inplace(char* s, char*** out_lines, int* out_count) {
if (!s || !out_lines || !out_count) return -1;
char** lines = NULL;
int count = 0;
int cap = 0;
char* p = s;
while (*p) {
char* start = p;
while (*p && *p != '\n') p++;
if (*p == '\n') {
*p = '\0';
p++;
}
if (add_line(&lines, &count, &cap, start) != 0) {
free(lines);
return -1;
}
}
*out_lines = lines;
*out_count = count;
return 0;
}
static int append_text(char** buf, size_t* cap, size_t* used, const char* s) {
if (!buf || !cap || !used || !s) return -1;
size_t n = strlen(s);
@@ -105,10 +57,11 @@ static const char* map_variable_tool_name(const char* var_name) {
if (strcmp(var_name, "admin_relays") == 0) return "nostr_admin_relays";
if (strcmp(var_name, "adopted_skills") == 0) return "adopted_skills";
if (strcmp(var_name, "triggering_event") == 0) return "trigger_event";
return NULL;
return var_name;
}
static char* resolve_inline_variables_local(const char* tpl, tools_context_t* tools_ctx) {
char* prompt_template_resolve_inline_variables(const char* tpl, tools_context_t* tools_ctx) {
if (!tpl) return strdup("");
size_t cap = strlen(tpl) + 64U;
@@ -161,17 +114,62 @@ static char* resolve_inline_variables_local(const char* tpl, tools_context_t* to
const char* replacement = "";
char* replacement_owned = NULL;
char* call_name_owned = NULL;
char* call_args_owned = NULL;
const char* lookup_name = var_trim;
const char* call_args_json = "{}";
if (strcmp(var_trim, "message") == 0) {
size_t var_len = strlen(var_trim);
if (var_len > 0 && var_trim[var_len - 1] == ')') {
char* open_paren = strchr(var_trim, '(');
if (open_paren && open_paren < (var_trim + var_len - 1)) {
call_name_owned = dup_range(var_trim, (size_t)(open_paren - var_trim));
if (!call_name_owned) {
free(var);
free(out);
return NULL;
}
char* call_name_trim = ltrim_inplace(call_name_owned);
rtrim_inplace(call_name_trim);
char* raw_args = dup_range(open_paren + 1,
(size_t)((var_trim + var_len - 1) - (open_paren + 1)));
if (!raw_args) {
free(call_name_owned);
free(var);
free(out);
return NULL;
}
char* raw_args_trim = ltrim_inplace(raw_args);
rtrim_inplace(raw_args_trim);
if (call_name_trim[0] != '\0') {
lookup_name = call_name_trim;
if (raw_args_trim[0] != '\0') {
cJSON* parsed_args = cJSON_Parse(raw_args_trim);
if (parsed_args) {
call_args_owned = strdup(raw_args_trim);
cJSON_Delete(parsed_args);
}
}
}
free(raw_args);
}
}
if (call_args_owned) {
call_args_json = call_args_owned;
}
if (strcmp(lookup_name, "message") == 0) {
replacement = (tools_ctx && tools_ctx->template_current_user_message)
? tools_ctx->template_current_user_message
: "";
} else if (strcmp(var_trim, "dm_history") == 0) {
replacement = "";
} else if (tools_ctx) {
const char* tool_name = map_variable_tool_name(var_trim);
const char* tool_name = map_variable_tool_name(lookup_name);
if (tool_name && tool_name[0] != '\0') {
char* tool_result = tools_execute(tools_ctx, tool_name, "{}");
char* tool_result = tools_execute(tools_ctx, tool_name, call_args_json);
if (tool_result) {
cJSON* root = cJSON_Parse(tool_result);
if (root && cJSON_IsObject(root)) {
@@ -190,10 +188,10 @@ static char* resolve_inline_variables_local(const char* tpl, tools_context_t* to
}
}
if (!replacement_owned && tools_ctx->template_skill_lookup && var_trim[0] != '\0') {
if (!replacement_owned && tools_ctx->template_skill_lookup && lookup_name[0] != '\0') {
const char* skill_content = tools_ctx->template_skill_lookup(
tools_ctx->template_skill_lookup_user_data,
var_trim);
lookup_name);
if (skill_content && skill_content[0] != '\0') {
replacement_owned = strdup(skill_content);
}
@@ -203,12 +201,16 @@ static char* resolve_inline_variables_local(const char* tpl, tools_context_t* to
}
if (append_text(&out, &cap, &used, replacement) != 0) {
free(call_args_owned);
free(call_name_owned);
free(replacement_owned);
free(var);
free(out);
return NULL;
}
free(call_args_owned);
free(call_name_owned);
free(replacement_owned);
free(var);
p = close + 2;
@@ -216,462 +218,3 @@ static char* resolve_inline_variables_local(const char* tpl, tools_context_t* to
return out;
}
static void init_section_defaults(prompt_template_section_t* sec) {
if (!sec) return;
memset(sec->name, 0, sizeof(sec->name));
memset(sec->role, 0, sizeof(sec->role));
snprintf(sec->role, sizeof(sec->role), "system");
sec->content_template = NULL;
sec->tool_name = NULL;
sec->tool_args = NULL;
sec->result_field = NULL;
sec->limit = 0;
sec->skip_if_empty = 0;
sec->provider_name = NULL;
sec->provider_content_template = NULL;
}
static int parse_int_or_zero(const char* s) {
if (!s) return 0;
while (*s && isspace((unsigned char)*s)) s++;
return atoi(s);
}
int prompt_template_parse(const char* skill_content, prompt_template_t* out_template) {
if (!skill_content || !out_template) {
return -1;
}
memset(out_template, 0, sizeof(*out_template));
const char* marker = strstr(skill_content, PROMPT_TEMPLATE_MARKER);
if (!marker) {
return -1;
}
size_t personality_len = (size_t)(marker - skill_content);
out_template->personality = dup_range(skill_content, personality_len);
if (!out_template->personality) {
return -1;
}
rtrim_inplace(out_template->personality);
const char* after = marker + strlen(PROMPT_TEMPLATE_MARKER);
while (*after == '\r' || *after == '\n') after++;
char* tpl = strdup(after);
if (!tpl) {
prompt_template_free(out_template);
return -1;
}
char** lines = NULL;
int line_count = 0;
if (split_lines_inplace(tpl, &lines, &line_count) != 0) {
free(tpl);
prompt_template_free(out_template);
return -1;
}
int current = -1;
int i = 0;
while (i < line_count) {
char* raw = lines[i];
rtrim_inplace(raw);
char* line = ltrim_inplace(raw);
if (*line == '\0') {
i++;
continue;
}
if (starts_with(line, "- section:")) {
if (out_template->section_count >= PROMPT_TEMPLATE_MAX_SECTIONS) {
break;
}
current = out_template->section_count;
init_section_defaults(&out_template->sections[current]);
out_template->section_count++;
char* name = line + strlen("- section:");
name = ltrim_inplace(name);
rtrim_inplace(name);
snprintf(out_template->sections[current].name,
sizeof(out_template->sections[current].name),
"%s",
name);
i++;
continue;
}
if (current < 0) {
i++;
continue;
}
if (starts_with(line, "role:")) {
char* role = line + strlen("role:");
role = ltrim_inplace(role);
rtrim_inplace(role);
snprintf(out_template->sections[current].role,
sizeof(out_template->sections[current].role),
"%s",
(*role) ? role : "system");
i++;
continue;
}
if (starts_with(line, "limit:")) {
char* lim = line + strlen("limit:");
out_template->sections[current].limit = parse_int_or_zero(lim);
i++;
continue;
}
if (starts_with(line, "skip_if_empty:")) {
char* flag = line + strlen("skip_if_empty:");
flag = ltrim_inplace(flag);
rtrim_inplace(flag);
out_template->sections[current].skip_if_empty =
(strcmp(flag, "true") == 0 || strcmp(flag, "1") == 0) ? 1 : 0;
i++;
continue;
}
if (starts_with(line, "tool:")) {
char* val = line + strlen("tool:");
val = ltrim_inplace(val);
rtrim_inplace(val);
free(out_template->sections[current].tool_name);
out_template->sections[current].tool_name = strdup(val);
i++;
continue;
}
if (starts_with(line, "args:")) {
char* val = line + strlen("args:");
val = ltrim_inplace(val);
rtrim_inplace(val);
free(out_template->sections[current].tool_args);
out_template->sections[current].tool_args = strdup((*val) ? val : "{}");
i++;
continue;
}
if (starts_with(line, "result_field:")) {
char* val = line + strlen("result_field:");
val = ltrim_inplace(val);
rtrim_inplace(val);
free(out_template->sections[current].result_field);
out_template->sections[current].result_field = strdup(val);
i++;
continue;
}
if (starts_with(line, "content:")) {
char* val = line + strlen("content:");
val = ltrim_inplace(val);
free(out_template->sections[current].content_template);
out_template->sections[current].content_template = NULL;
if (strcmp(val, "|") == 0) {
char* acc = strdup("");
size_t cap = acc ? 1U : 0U;
size_t used = 0U;
if (!acc) {
free(lines);
free(tpl);
prompt_template_free(out_template);
return -1;
}
i++;
while (i < line_count) {
char* next_raw = lines[i];
char* next_ltrim = ltrim_inplace(next_raw);
if (starts_with(next_ltrim, "- section:") ||
starts_with(next_ltrim, "role:") ||
starts_with(next_ltrim, "limit:") ||
starts_with(next_ltrim, "skip_if_empty:") ||
starts_with(next_ltrim, "tool:") ||
starts_with(next_ltrim, "args:") ||
starts_with(next_ltrim, "result_field:") ||
starts_with(next_ltrim, "content:") ||
starts_with(next_ltrim, "provider:")) {
break;
}
char* piece = next_raw;
if (strncmp(piece, " ", 4) == 0) piece += 4;
else if (strncmp(piece, " ", 2) == 0) piece += 2;
rtrim_inplace(piece);
if (append_text(&acc, &cap, &used, piece) != 0 ||
append_text(&acc, &cap, &used, "\n") != 0) {
free(acc);
free(lines);
free(tpl);
prompt_template_free(out_template);
return -1;
}
i++;
}
rtrim_inplace(acc);
out_template->sections[current].content_template = acc;
continue;
}
rtrim_inplace(val);
out_template->sections[current].content_template = strdup(val);
i++;
continue;
}
if (starts_with(line, "provider:")) {
i++;
if (i >= line_count) continue;
char* provider_line = ltrim_inplace(lines[i]);
rtrim_inplace(provider_line);
char* colon = strchr(provider_line, ':');
if (!colon) {
continue;
}
*colon = '\0';
char* provider_name = ltrim_inplace(provider_line);
rtrim_inplace(provider_name);
char* provider_val = ltrim_inplace(colon + 1);
rtrim_inplace(provider_val);
free(out_template->sections[current].provider_name);
out_template->sections[current].provider_name = strdup(provider_name);
free(out_template->sections[current].provider_content_template);
out_template->sections[current].provider_content_template = NULL;
if (strcmp(provider_val, "|") == 0) {
char* acc = strdup("");
size_t cap = acc ? 1U : 0U;
size_t used = 0U;
if (!acc) {
free(lines);
free(tpl);
prompt_template_free(out_template);
return -1;
}
i++;
while (i < line_count) {
char* next = ltrim_inplace(lines[i]);
if (starts_with(next, "- section:") ||
starts_with(next, "role:") ||
starts_with(next, "limit:") ||
starts_with(next, "skip_if_empty:") ||
starts_with(next, "tool:") ||
starts_with(next, "args:") ||
starts_with(next, "result_field:") ||
starts_with(next, "content:") ||
starts_with(next, "provider:")) {
break;
}
char* piece = lines[i];
if (strncmp(piece, " ", 4) == 0) piece += 4;
else if (strncmp(piece, " ", 2) == 0) piece += 2;
rtrim_inplace(piece);
if (append_text(&acc, &cap, &used, piece) != 0 ||
append_text(&acc, &cap, &used, "\n") != 0) {
free(acc);
free(lines);
free(tpl);
prompt_template_free(out_template);
return -1;
}
i++;
}
rtrim_inplace(acc);
out_template->sections[current].provider_content_template = acc;
continue;
}
out_template->sections[current].provider_content_template = strdup(provider_val);
i++;
continue;
}
i++;
}
free(lines);
free(tpl);
return 0;
}
static int append_message_object(cJSON* messages, const char* role, const char* content) {
if (!messages || !role) return -1;
cJSON* msg = cJSON_CreateObject();
if (!msg) return -1;
cJSON_AddStringToObject(msg, "role", role);
cJSON_AddStringToObject(msg, "content", content ? content : "");
cJSON_AddItemToArray(messages, msg);
return 0;
}
static char* extract_tool_result_content(const prompt_template_section_t* sec, const char* tool_result_json) {
if (!tool_result_json) return strdup("");
cJSON* root = cJSON_Parse(tool_result_json);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return strdup(tool_result_json);
}
cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success");
if (success && cJSON_IsBool(success) && !cJSON_IsTrue(success)) {
cJSON_Delete(root);
return strdup("");
}
const char* field_name = (sec && sec->result_field && sec->result_field[0] != '\0')
? sec->result_field
: "content";
cJSON* field = cJSON_GetObjectItemCaseSensitive(root, field_name);
if (!field) {
field = cJSON_GetObjectItemCaseSensitive(root, "content");
}
char* out = NULL;
if (field && cJSON_IsString(field) && field->valuestring) {
out = strdup(field->valuestring);
} else if (field) {
out = cJSON_PrintUnformatted(field);
} else {
out = cJSON_PrintUnformatted(root);
}
cJSON_Delete(root);
return out ? out : strdup("");
}
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
const char* provider_name,
tools_context_t* tools_ctx,
cJSON* dm_history_messages,
int dm_history_default_limit,
prompt_template_emit_hook_fn emit_hook,
void* emit_hook_user_data) {
if (!tmpl) return NULL;
cJSON* out = cJSON_CreateArray();
if (!out) return NULL;
int out_idx = 0;
if (tmpl->personality && tmpl->personality[0] != '\0') {
if (append_message_object(out, "system", tmpl->personality) != 0) {
cJSON_Delete(out);
return NULL;
}
if (emit_hook) emit_hook("system_prompt", out_idx, emit_hook_user_data);
out_idx++;
}
for (int i = 0; i < tmpl->section_count; i++) {
const prompt_template_section_t* sec = &tmpl->sections[i];
const char* role = sec->role[0] ? sec->role : "system";
if (strcmp(role, "expand") == 0) {
if (!dm_history_messages || !cJSON_IsArray(dm_history_messages)) {
continue;
}
int total = cJSON_GetArraySize(dm_history_messages);
int lim = sec->limit > 0 ? sec->limit : dm_history_default_limit;
int start = (lim > 0 && total > lim) ? (total - lim) : 0;
for (int j = start; j < total; j++) {
cJSON* item = cJSON_GetArrayItem(dm_history_messages, j);
if (!item || !cJSON_IsObject(item)) continue;
cJSON* dup = cJSON_Duplicate(item, 1);
if (!dup) {
cJSON_Delete(out);
return NULL;
}
cJSON_AddItemToArray(out, dup);
if (emit_hook) emit_hook(sec->name[0] ? sec->name : "context_part", out_idx, emit_hook_user_data);
out_idx++;
}
continue;
}
char* resolved = NULL;
if (sec->tool_name && sec->tool_name[0] != '\0' && tools_ctx) {
const char* args_json = (sec->tool_args && sec->tool_args[0] != '\0') ? sec->tool_args : "{}";
char* tool_result = tools_execute(tools_ctx, sec->tool_name, args_json);
resolved = extract_tool_result_content(sec, tool_result);
free(tool_result);
} else {
const char* tpl = sec->content_template;
if (provider_name && sec->provider_name && sec->provider_content_template &&
strcmp(provider_name, sec->provider_name) == 0) {
tpl = sec->provider_content_template;
}
resolved = resolve_inline_variables_local(tpl ? tpl : "", tools_ctx);
}
if (!resolved) {
cJSON_Delete(out);
return NULL;
}
if (sec->skip_if_empty) {
char* chk = ltrim_inplace(resolved);
if (chk && *chk == '\0') {
free(resolved);
continue;
}
}
if (append_message_object(out, role, resolved) != 0) {
free(resolved);
cJSON_Delete(out);
return NULL;
}
if (emit_hook) emit_hook(sec->name[0] ? sec->name : "context_part", out_idx, emit_hook_user_data);
out_idx++;
free(resolved);
}
return out;
}
void prompt_template_free(prompt_template_t* tmpl) {
if (!tmpl) return;
free(tmpl->personality);
tmpl->personality = NULL;
for (int i = 0; i < tmpl->section_count; i++) {
free(tmpl->sections[i].content_template);
tmpl->sections[i].content_template = NULL;
free(tmpl->sections[i].tool_name);
tmpl->sections[i].tool_name = NULL;
free(tmpl->sections[i].tool_args);
tmpl->sections[i].tool_args = NULL;
free(tmpl->sections[i].result_field);
tmpl->sections[i].result_field = NULL;
free(tmpl->sections[i].provider_name);
tmpl->sections[i].provider_name = NULL;
free(tmpl->sections[i].provider_content_template);
tmpl->sections[i].provider_content_template = NULL;
tmpl->sections[i].name[0] = '\0';
tmpl->sections[i].role[0] = '\0';
tmpl->sections[i].limit = 0;
tmpl->sections[i].skip_if_empty = 0;
}
tmpl->section_count = 0;
}

View File

@@ -1,47 +1,8 @@
#ifndef DIDACTYL_PROMPT_TEMPLATE_H
#define DIDACTYL_PROMPT_TEMPLATE_H
#include "cjson/cJSON.h"
#include "tools/tools.h"
#define PROMPT_TEMPLATE_MAX_SECTIONS 32
#define PROMPT_TEMPLATE_MAX_NAME_LEN 64
#define PROMPT_TEMPLATE_MAX_ROLE_LEN 16
#define PROMPT_TEMPLATE_MARKER "---template---"
typedef struct {
char name[PROMPT_TEMPLATE_MAX_NAME_LEN];
char role[PROMPT_TEMPLATE_MAX_ROLE_LEN];
char* content_template;
char* tool_name;
char* tool_args;
char* result_field;
int limit;
int skip_if_empty;
char* provider_name;
char* provider_content_template;
} prompt_template_section_t;
typedef struct {
char* personality;
prompt_template_section_t sections[PROMPT_TEMPLATE_MAX_SECTIONS];
int section_count;
} prompt_template_t;
typedef void (*prompt_template_emit_hook_fn)(const char* section_name,
int message_index,
void* user_data);
int prompt_template_parse(const char* skill_content, prompt_template_t* out_template);
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
const char* provider_name,
tools_context_t* tools_ctx,
cJSON* dm_history_messages,
int dm_history_default_limit,
prompt_template_emit_hook_fn emit_hook,
void* emit_hook_user_data);
void prompt_template_free(prompt_template_t* tmpl);
char* prompt_template_resolve_inline_variables(const char* tpl, tools_context_t* tools_ctx);
#endif

View File

@@ -508,7 +508,7 @@ static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char*
cfg->default_skill.kind = DIDACTYL_DEFAULT_SKILL_KIND;
snprintf(cfg->default_skill.d_tag, sizeof(cfg->default_skill.d_tag), "%s", DIDACTYL_DEFAULT_SKILL_D_TAG);
int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name);
int needed = snprintf(NULL, 0, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE);
if (needed <= 0) {
return -1;
}
@@ -518,7 +518,7 @@ static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char*
return -1;
}
snprintf(cfg->default_skill.content, (size_t)needed + 1U, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name);
snprintf(cfg->default_skill.content, (size_t)needed + 1U, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE);
cfg->default_skill.tags_json = strdup(DIDACTYL_DEFAULT_SKILL_TAGS_JSON);
if (!cfg->default_skill.tags_json) {

View File

@@ -48,15 +48,23 @@ char* execute_admin_identity(tools_context_t* ctx, const char* args_json) {
cJSON_AddBoolToObject(out, "success", 1);
char content[768];
snprintf(content,
sizeof(content),
"## Administrator Identity (source: config.admin.pubkey)\n\n"
"This is your administrator! Admin pubkey (hex): %s\n\n"
"%s",
ctx->cfg->admin.pubkey[0] ? ctx->cfg->admin.pubkey : "unknown",
tier_text);
cJSON_AddStringToObject(out, "content", content);
cJSON* identity = cJSON_CreateObject();
if (!identity) {
cJSON_Delete(out);
return NULL;
}
cJSON_AddStringToObject(identity,
"admin_pubkey",
ctx->cfg->admin.pubkey[0] ? ctx->cfg->admin.pubkey : "unknown");
cJSON_AddStringToObject(identity, "sender_tier", tier_text);
char* identity_json = cJSON_PrintUnformatted(identity);
cJSON_Delete(identity);
if (!identity_json) {
cJSON_Delete(out);
return NULL;
}
cJSON_AddStringToObject(out, "content", identity_json);
free(identity_json);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -81,19 +89,7 @@ char* execute_nostr_admin_profile(tools_context_t* ctx, const char* args_json) {
cJSON_AddBoolToObject(out, "success", 1);
size_t content_len = strlen("## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): ") + strlen(profile_json) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(kind0);
return NULL;
}
snprintf(content,
content_len,
"## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): %s",
profile_json);
cJSON_AddStringToObject(out, "content", content);
free(content);
cJSON_AddStringToObject(out, "content", profile_json);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -119,19 +115,7 @@ char* execute_nostr_admin_contacts(tools_context_t* ctx, const char* args_json)
cJSON_AddBoolToObject(out, "success", 1);
size_t content_len = strlen("## Administrator Kind 3 Contacts (source: nostr kind 3)\n\nAdministrator contacts (JSON): ") + strlen(contacts_json) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(kind3);
return NULL;
}
snprintf(content,
content_len,
"## Administrator Kind 3 Contacts (source: nostr kind 3)\n\nAdministrator contacts (JSON): %s",
contacts_json);
cJSON_AddStringToObject(out, "content", content);
free(content);
cJSON_AddStringToObject(out, "content", contacts_json);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -157,19 +141,7 @@ char* execute_nostr_admin_relays(tools_context_t* ctx, const char* args_json) {
cJSON_AddBoolToObject(out, "success", 1);
size_t content_len = strlen("## Administrator Kind 10002 Relays (source: nostr kind 10002)\n\nAdministrator relay list (JSON): ") + strlen(relays_json) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(kind10002);
return NULL;
}
snprintf(content,
content_len,
"## Administrator Kind 10002 Relays (source: nostr kind 10002)\n\nAdministrator relay list (JSON): %s",
relays_json);
cJSON_AddStringToObject(out, "content", content);
free(content);
cJSON_AddStringToObject(out, "content", relays_json);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -195,19 +167,7 @@ char* execute_nostr_admin_notes(tools_context_t* ctx, const char* args_json) {
cJSON_AddBoolToObject(out, "success", 1);
size_t content_len = strlen("## Administrator Recent Kind 1 Notes\n\n") + strlen(notes_text) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(notes);
return NULL;
}
snprintf(content,
content_len,
"## Administrator Recent Kind 1 Notes\n\n%s",
notes_text);
cJSON_AddStringToObject(out, "content", content);
free(content);
cJSON_AddStringToObject(out, "content", notes_text);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);

View File

@@ -63,13 +63,23 @@ char* execute_agent_identity(tools_context_t* ctx, const char* args_json) {
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
char content[384];
snprintf(content,
sizeof(content),
"## Agent Identity\n\nYour pubkey (hex): %s\nYour npub: %s",
ctx->cfg->keys.public_key_hex[0] ? ctx->cfg->keys.public_key_hex : "unknown",
npub);
cJSON_AddStringToObject(out, "content", content);
cJSON* identity = cJSON_CreateObject();
if (!identity) {
cJSON_Delete(out);
return NULL;
}
cJSON_AddStringToObject(identity,
"pubkey",
ctx->cfg->keys.public_key_hex[0] ? ctx->cfg->keys.public_key_hex : "unknown");
cJSON_AddStringToObject(identity, "npub", npub);
char* identity_json = cJSON_PrintUnformatted(identity);
cJSON_Delete(identity);
if (!identity_json) {
cJSON_Delete(out);
return NULL;
}
cJSON_AddStringToObject(out, "content", identity_json);
free(identity_json);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -94,19 +104,7 @@ char* execute_nostr_agent_profile(tools_context_t* ctx, const char* args_json) {
cJSON_AddBoolToObject(out, "success", 1);
size_t content_len = strlen("## Agent Kind 0 Profile (source: nostr kind 0)\n\nAgent kind 0 profile content (JSON): ") + strlen(profile_json) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(kind0);
return NULL;
}
snprintf(content,
content_len,
"## Agent Kind 0 Profile (source: nostr kind 0)\n\nAgent kind 0 profile content (JSON): %s",
profile_json);
cJSON_AddStringToObject(out, "content", content);
free(content);
cJSON_AddStringToObject(out, "content", profile_json);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -132,19 +130,7 @@ char* execute_nostr_agent_contacts(tools_context_t* ctx, const char* args_json)
cJSON_AddBoolToObject(out, "success", 1);
size_t content_len = strlen("## Agent Kind 3 Contacts (source: nostr kind 3)\n\nAgent contacts (JSON): ") + strlen(contacts_json) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(kind3);
return NULL;
}
snprintf(content,
content_len,
"## Agent Kind 3 Contacts (source: nostr kind 3)\n\nAgent contacts (JSON): %s",
contacts_json);
cJSON_AddStringToObject(out, "content", content);
free(content);
cJSON_AddStringToObject(out, "content", contacts_json);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -170,19 +156,7 @@ char* execute_nostr_agent_relays(tools_context_t* ctx, const char* args_json) {
cJSON_AddBoolToObject(out, "success", 1);
size_t content_len = strlen("## Agent Kind 10002 Relays (source: nostr kind 10002)\n\nAgent relay list (JSON): ") + strlen(relays_json) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(kind10002);
return NULL;
}
snprintf(content,
content_len,
"## Agent Kind 10002 Relays (source: nostr kind 10002)\n\nAgent relay list (JSON): %s",
relays_json);
cJSON_AddStringToObject(out, "content", content);
free(content);
cJSON_AddStringToObject(out, "content", relays_json);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -208,19 +182,7 @@ char* execute_nostr_agent_notes(tools_context_t* ctx, const char* args_json) {
cJSON_AddBoolToObject(out, "success", 1);
size_t content_len = strlen("## Agent Recent Kind 1 Notes\n\n") + strlen(notes_text) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(notes);
return NULL;
}
snprintf(content,
content_len,
"## Agent Recent Kind 1 Notes\n\n%s",
notes_text);
cJSON_AddStringToObject(out, "content", content);
free(content);
cJSON_AddStringToObject(out, "content", notes_text);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -300,6 +262,125 @@ char* execute_trigger_event(tools_context_t* ctx, const char* args_json) {
return json;
}
char* execute_nostr_dm_history(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
int limit = 12;
cJSON* limit_j = cJSON_GetObjectItemCaseSensitive(args, "limit");
if (limit_j && cJSON_IsNumber(limit_j)) {
limit = (int)limit_j->valuedouble;
}
if (limit < 1) limit = 1;
if (limit > 200) limit = 200;
int include_current = 0;
cJSON* include_current_j = cJSON_GetObjectItemCaseSensitive(args, "include_current");
if (include_current_j && cJSON_IsBool(include_current_j) && cJSON_IsTrue(include_current_j)) {
include_current = 1;
}
const char* peer_pubkey = ctx->cfg->admin.pubkey;
cJSON* peer_j = cJSON_GetObjectItemCaseSensitive(args, "peer_pubkey");
if (peer_j && cJSON_IsString(peer_j) && peer_j->valuestring && peer_j->valuestring[0] != '\0') {
peer_pubkey = peer_j->valuestring;
}
char* history_json = nostr_handler_get_dm_history_json(peer_pubkey, limit);
cJSON_Delete(args);
if (!history_json) {
return json_error_local("failed to fetch dm history");
}
cJSON* parsed = cJSON_Parse(history_json);
free(history_json);
if (!parsed || !cJSON_IsArray(parsed)) {
cJSON_Delete(parsed);
parsed = cJSON_CreateArray();
if (!parsed) {
return json_error_local("failed to build dm history response");
}
}
cJSON* filtered = cJSON_CreateArray();
if (!filtered) {
cJSON_Delete(parsed);
return json_error_local("failed to build dm history response");
}
const char* current_message = (!include_current && ctx->template_current_user_message)
? ctx->template_current_user_message
: NULL;
const char* last_role = NULL;
const char* last_content = NULL;
int n = cJSON_GetArraySize(parsed);
for (int i = 0; i < n; i++) {
cJSON* item = cJSON_GetArrayItem(parsed, i);
cJSON* role = item ? cJSON_GetObjectItemCaseSensitive(item, "role") : NULL;
cJSON* content = item ? cJSON_GetObjectItemCaseSensitive(item, "content") : NULL;
cJSON* created_at = item ? cJSON_GetObjectItemCaseSensitive(item, "created_at") : NULL;
if (!role || !content || !cJSON_IsString(role) || !cJSON_IsString(content) ||
!role->valuestring || !content->valuestring) {
continue;
}
const char* role_s = role->valuestring;
const char* content_s = content->valuestring;
int role_is_user = (strcmp(role_s, "user") == 0);
if (!role_is_user && strcmp(role_s, "assistant") != 0) {
continue;
}
if (i == n - 1 && role_is_user && current_message && strcmp(content_s, current_message) == 0) {
continue;
}
if (last_role && last_content && strcmp(last_role, role_s) == 0 && strcmp(last_content, content_s) == 0) {
continue;
}
cJSON* out_item = cJSON_CreateObject();
if (!out_item) {
continue;
}
cJSON_AddStringToObject(out_item, "role", role_s);
cJSON_AddStringToObject(out_item, "content", content_s);
if (created_at && cJSON_IsNumber(created_at)) {
cJSON_AddNumberToObject(out_item, "created_at", created_at->valuedouble);
}
cJSON_AddItemToArray(filtered, out_item);
last_role = role_s;
last_content = content_s;
}
cJSON_Delete(parsed);
char* filtered_json = cJSON_PrintUnformatted(filtered);
cJSON_Delete(filtered);
if (!filtered_json) {
return json_error_local("failed to serialize dm history");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
free(filtered_json);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "content", filtered_json);
free(filtered_json);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_agent_version(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");

View File

@@ -227,6 +227,7 @@ char* execute_nostr_pubkey(tools_context_t* ctx, const char* args_json) {
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "pubkey", ctx->cfg->keys.public_key_hex);
cJSON_AddStringToObject(out, "content", ctx->cfg->keys.public_key_hex);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -250,6 +251,7 @@ char* execute_nostr_npub(tools_context_t* ctx, const char* args_json) {
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "npub", npub);
cJSON_AddStringToObject(out, "content", npub);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);

View File

@@ -908,6 +908,315 @@ char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
return json;
}
char* execute_skill_get(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d");
cJSON* input = cJSON_GetObjectItemCaseSensitive(args, "input");
const char* d_tag_value = NULL;
if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && validate_skill_d_tag_local(d_tag->valuestring)) {
d_tag_value = d_tag->valuestring;
} else if (input && cJSON_IsString(input) && input->valuestring && validate_skill_d_tag_local(input->valuestring)) {
d_tag_value = input->valuestring;
}
if (!d_tag_value) {
cJSON_Delete(args);
return json_error_local("skill_get requires valid d tag");
}
char* events_json = nostr_handler_get_self_skill_events_json();
if (!events_json) {
cJSON_Delete(args);
return json_error_local("skill_get cache unavailable");
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(args);
cJSON_Delete(events);
return json_error_local("skill_get cache returned invalid JSON");
}
cJSON* target = NULL;
long long best_created_at = -1;
int event_count = cJSON_GetArraySize(events);
for (int i = 0; i < event_count; i++) {
cJSON* ev = cJSON_GetArrayItem(events, i);
if (!ev || !cJSON_IsObject(ev)) continue;
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
if (!kind || !cJSON_IsNumber(kind) || !tags || !cJSON_IsArray(tags)) continue;
int kind_val = (int)kind->valuedouble;
if (kind_val != 31123 && kind_val != 31124) continue;
cJSON* d_val = find_tag_value_string_local(tags, "d");
if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) continue;
if (strcmp(d_val->valuestring, d_tag_value) != 0) continue;
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
long long created = (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : 0;
if (!target || created > best_created_at) {
target = ev;
best_created_at = created;
}
}
if (!target) {
cJSON_Delete(args);
cJSON_Delete(events);
return json_error_local("skill_get could not find skill with that d");
}
cJSON* kind = cJSON_GetObjectItemCaseSensitive(target, "kind");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(target, "pubkey");
cJSON* id = cJSON_GetObjectItemCaseSensitive(target, "id");
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(target, "created_at");
cJSON* content = cJSON_GetObjectItemCaseSensitive(target, "content");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(target, "tags");
if (!kind || !cJSON_IsNumber(kind) || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
!content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) {
cJSON_Delete(args);
cJSON_Delete(events);
return json_error_local("skill_get found malformed skill event");
}
int kind_val = (int)kind->valuedouble;
cJSON* scope = find_tag_value_string_local(tags, "scope");
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(args);
cJSON_Delete(events);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "d_tag", d_tag_value);
cJSON_AddNumberToObject(out, "kind", kind_val);
cJSON_AddStringToObject(out,
"scope",
(scope && cJSON_IsString(scope) && scope->valuestring)
? scope->valuestring
: (kind_val == 31124 ? "private" : "public"));
cJSON_AddStringToObject(out, "owner", skill_owner_label_local(ctx->cfg, pubkey->valuestring));
if (id && cJSON_IsString(id) && id->valuestring) {
cJSON_AddStringToObject(out, "id", id->valuestring);
}
if (created_at && cJSON_IsNumber(created_at)) {
char ts_buf[32];
snprintf(ts_buf, sizeof(ts_buf), "%lld", (long long)created_at->valuedouble);
cJSON* ts_raw = cJSON_CreateRaw(ts_buf);
if (ts_raw) {
cJSON_AddItemToObject(out, "created_at", ts_raw);
}
}
cJSON_AddStringToObject(out, "content", content->valuestring);
cJSON* tags_dup = cJSON_Duplicate(tags, 1);
if (tags_dup) {
cJSON_AddItemToObject(out, "tags", tags_dup);
}
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
cJSON_Delete(args);
cJSON_Delete(events);
return json;
}
char* execute_skill_view(tools_context_t* ctx, const char* args_json) {
return execute_skill_get(ctx, args_json);
}
char* execute_skill_set(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* skill = cJSON_GetObjectItemCaseSensitive(args, "skill");
cJSON* input = cJSON_GetObjectItemCaseSensitive(args, "input");
cJSON* parsed_input = NULL;
cJSON* src = NULL;
if (skill && cJSON_IsObject(skill)) {
src = skill;
} else if (input && cJSON_IsString(input) && input->valuestring && input->valuestring[0] != '\0') {
parsed_input = cJSON_Parse(input->valuestring);
if (parsed_input && cJSON_IsObject(parsed_input)) {
src = parsed_input;
}
} else if (cJSON_IsObject(args)) {
src = args;
}
if (!src || !cJSON_IsObject(src)) {
cJSON_Delete(parsed_input);
cJSON_Delete(args);
return json_error_local("skill_set requires a skill object");
}
cJSON* d = cJSON_GetObjectItemCaseSensitive(src, "d");
cJSON* kind = cJSON_GetObjectItemCaseSensitive(src, "kind");
cJSON* content = cJSON_GetObjectItemCaseSensitive(src, "content");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(src, "tags");
cJSON* scope = cJSON_GetObjectItemCaseSensitive(src, "scope");
if (!d || !cJSON_IsString(d) || !d->valuestring || !validate_skill_d_tag_local(d->valuestring)) {
cJSON_Delete(parsed_input);
cJSON_Delete(args);
return json_error_local("skill_set requires valid d");
}
if (!content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(parsed_input);
cJSON_Delete(args);
return json_error_local("skill_set requires string content");
}
if (!tags || !cJSON_IsArray(tags)) {
cJSON_Delete(parsed_input);
cJSON_Delete(args);
return json_error_local("skill_set requires tags array");
}
int out_kind = 0;
if (kind && cJSON_IsNumber(kind)) {
out_kind = (int)kind->valuedouble;
} else if (scope && cJSON_IsString(scope) && scope->valuestring) {
out_kind = (strcmp(scope->valuestring, "private") == 0) ? 31124 : 31123;
} else {
cJSON* scope_tag = find_tag_value_string_local(tags, "scope");
out_kind = (scope_tag && cJSON_IsString(scope_tag) && scope_tag->valuestring && strcmp(scope_tag->valuestring, "private") == 0)
? 31124
: 31123;
}
if (out_kind != 31123 && out_kind != 31124) {
cJSON_Delete(parsed_input);
cJSON_Delete(args);
return json_error_local("skill_set kind must be 31123 or 31124");
}
cJSON* tags_out = cJSON_Duplicate(tags, 1);
if (!tags_out || !cJSON_IsArray(tags_out)) {
cJSON_Delete(tags_out);
cJSON_Delete(parsed_input);
cJSON_Delete(args);
return json_error_local("skill_set failed to duplicate tags");
}
if (upsert_string_tag_local(tags_out, "d", d->valuestring) != 0 ||
upsert_string_tag_local(tags_out, "app", "didactyl") != 0 ||
upsert_string_tag_local(tags_out, "scope", out_kind == 31124 ? "private" : "public") != 0) {
cJSON_Delete(tags_out);
cJSON_Delete(parsed_input);
cJSON_Delete(args);
return json_error_local("skill_set failed to normalize required tags");
}
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event(out_kind, content->valuestring, tags_out, &publish_result);
int trigger_registered = 0;
if (rc == 0 && ctx->trigger_manager) {
cJSON* trigger = find_tag_value_string_local(tags_out, "trigger");
cJSON* filter = find_tag_value_string_local(tags_out, "filter");
cJSON* enabled = find_tag_value_string_local(tags_out, "enabled");
cJSON* llm = find_tag_value_string_local(tags_out, "llm");
cJSON* tools = find_tag_value_string_local(tags_out, "tools");
cJSON* max_tokens = find_tag_value_string_local(tags_out, "max_tokens");
cJSON* temperature = find_tag_value_string_local(tags_out, "temperature");
cJSON* seed = find_tag_value_string_local(tags_out, "seed");
int enabled_i = !(enabled && cJSON_IsString(enabled) && enabled->valuestring && strcmp(enabled->valuestring, "false") == 0);
const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL;
if (trigger_s && filter_s && enabled_i) {
int max_tokens_set = (max_tokens && cJSON_IsString(max_tokens) && max_tokens->valuestring && max_tokens->valuestring[0] != '\0') ? 1 : 0;
int max_tokens_v = max_tokens_set ? atoi(max_tokens->valuestring) : 0;
int temperature_set = (temperature && cJSON_IsString(temperature) && temperature->valuestring && temperature->valuestring[0] != '\0') ? 1 : 0;
double temperature_v = temperature_set ? atof(temperature->valuestring) : 0.0;
int seed_set = (seed && cJSON_IsString(seed) && seed->valuestring && seed->valuestring[0] != '\0') ? 1 : 0;
int seed_v = seed_set ? atoi(seed->valuestring) : 0;
if (trigger_manager_update(ctx->trigger_manager,
d->valuestring,
content->valuestring,
filter_s,
TRIGGER_ACTION_LLM,
trigger_s,
enabled_i,
(llm && cJSON_IsString(llm) && llm->valuestring) ? llm->valuestring : NULL,
(tools && cJSON_IsString(tools) && tools->valuestring) ? tools->valuestring : NULL,
max_tokens_set,
max_tokens_v,
temperature_set,
temperature_v,
seed_set,
seed_v) == 0 ||
trigger_manager_add(ctx->trigger_manager,
d->valuestring,
content->valuestring,
filter_s,
TRIGGER_ACTION_LLM,
trigger_s,
enabled_i,
(llm && cJSON_IsString(llm) && llm->valuestring) ? llm->valuestring : NULL,
(tools && cJSON_IsString(tools) && tools->valuestring) ? tools->valuestring : NULL,
max_tokens_set,
max_tokens_v,
temperature_set,
temperature_v,
seed_set,
seed_v) == 0) {
trigger_registered = 1;
}
} else {
(void)trigger_manager_remove(ctx->trigger_manager, d->valuestring);
}
}
cJSON_Delete(tags_out);
if (rc != 0) {
cJSON_Delete(parsed_input);
cJSON_Delete(args);
nostr_handler_publish_result_free(&publish_result);
return json_error_local("skill_set failed to publish skill event");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(parsed_input);
cJSON_Delete(args);
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddStringToObject(out, "d_tag", d->valuestring);
cJSON_AddNumberToObject(out, "kind", out_kind);
cJSON_AddStringToObject(out, "scope", out_kind == 31124 ? "private" : "public");
cJSON_AddStringToObject(out, "skill_event_id", publish_result.event_id);
cJSON_AddBoolToObject(out, "trigger_registered", trigger_registered ? 1 : 0);
if (publish_result.naddr_uri[0] != '\0') {
cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);
}
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
cJSON_Delete(parsed_input);
cJSON_Delete(args);
nostr_handler_publish_result_free(&publish_result);
return json;
}
char* execute_skill_adopt(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

View File

@@ -131,6 +131,15 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch
if (strcmp(tool_name, "skill_list") == 0) {
return execute_skill_list(ctx, args_json);
}
if (strcmp(tool_name, "skill_get") == 0) {
return execute_skill_get(ctx, args_json);
}
if (strcmp(tool_name, "skill_view") == 0) {
return execute_skill_get(ctx, args_json);
}
if (strcmp(tool_name, "skill_set") == 0) {
return execute_skill_set(ctx, args_json);
}
if (strcmp(tool_name, "skill_adopt") == 0) {
return execute_skill_adopt(ctx, args_json);
}
@@ -240,6 +249,9 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch
if (strcmp(tool_name, "trigger_event") == 0) {
return execute_trigger_event(ctx, args_json);
}
if (strcmp(tool_name, "nostr_dm_history") == 0) {
return execute_nostr_dm_history(ctx, args_json);
}
if (strcmp(tool_name, "cashu_wallet_balance") == 0) {
return execute_cashu_wallet_balance(ctx, args_json);
}

View File

@@ -57,6 +57,9 @@ char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_json);
char* execute_nostr_file_md_to_longform_post(tools_context_t* ctx, const char* args_json);
char* execute_skill_create(tools_context_t* ctx, const char* args_json);
char* execute_skill_list(tools_context_t* ctx, const char* args_json);
char* execute_skill_get(tools_context_t* ctx, const char* args_json);
char* execute_skill_view(tools_context_t* ctx, const char* args_json);
char* execute_skill_set(tools_context_t* ctx, const char* args_json);
char* execute_skill_adopt(tools_context_t* ctx, const char* args_json);
char* execute_skill_remove(tools_context_t* ctx, const char* args_json);
char* execute_skill_edit(tools_context_t* ctx, const char* args_json);
@@ -70,6 +73,7 @@ char* execute_config_store(tools_context_t* ctx, const char* args_json);
char* execute_config_recall(tools_context_t* ctx, const char* args_json);
char* execute_adopted_skills(tools_context_t* ctx, const char* args_json);
char* execute_trigger_event(tools_context_t* ctx, const char* args_json);
char* execute_nostr_dm_history(tools_context_t* ctx, const char* args_json);
char* execute_cashu_wallet_balance(tools_context_t* ctx, const char* args_json);
char* execute_cashu_wallet_info(tools_context_t* ctx, const char* args_json);

View File

@@ -843,6 +843,89 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
cJSON_AddItemToObject(t23, "function", t23_fn);
cJSON_AddItemToArray(tools, t23);
cJSON* t24a = cJSON_CreateObject();
cJSON* t24a_fn = cJSON_CreateObject();
cJSON* t24a_params = cJSON_CreateObject();
cJSON* t24a_props = cJSON_CreateObject();
cJSON* t24a_required = cJSON_CreateArray();
cJSON_AddStringToObject(t24a, "type", "function");
cJSON_AddStringToObject(t24a_fn, "name", "skill_get");
cJSON_AddStringToObject(t24a_fn, "description", "Get full skill JSON by d tag from local cache; JSON: {\"d\":\"default_admin_dm\"} (slash shorthand also works: /skill_get default_admin_dm)");
cJSON_AddStringToObject(t24a_params, "type", "object");
cJSON_AddItemToObject(t24a_params, "properties", t24a_props);
cJSON_AddItemToObject(t24a_params, "required", t24a_required);
cJSON* p_skill_get_d = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_get_d, "type", "string");
cJSON_AddStringToObject(p_skill_get_d, "description", "Skill d tag (preferred JSON key)");
cJSON_AddItemToObject(t24a_props, "d", p_skill_get_d);
cJSON* p_skill_get_input = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_get_input, "type", "string");
cJSON_AddStringToObject(p_skill_get_input, "description", "Slash shorthand fallback key when args are passed as plain text");
cJSON_AddItemToObject(t24a_props, "input", p_skill_get_input);
cJSON_AddItemToArray(t24a_required, cJSON_CreateString("d"));
cJSON_AddItemToObject(t24a_fn, "parameters", t24a_params);
cJSON_AddItemToObject(t24a, "function", t24a_fn);
cJSON_AddItemToArray(tools, t24a);
cJSON* t24b = cJSON_CreateObject();
cJSON* t24b_fn = cJSON_CreateObject();
cJSON* t24b_params = cJSON_CreateObject();
cJSON* t24b_props = cJSON_CreateObject();
cJSON* t24b_required = cJSON_CreateArray();
cJSON_AddStringToObject(t24b, "type", "function");
cJSON_AddStringToObject(t24b_fn, "name", "skill_view");
cJSON_AddStringToObject(t24b_fn, "description", "Compatibility alias for skill_get; JSON: {\"d\":\"default_admin_dm\"}");
cJSON_AddStringToObject(t24b_params, "type", "object");
cJSON_AddItemToObject(t24b_params, "properties", t24b_props);
cJSON_AddItemToObject(t24b_params, "required", t24b_required);
cJSON* p_skill_view_d = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_view_d, "type", "string");
cJSON_AddItemToObject(t24b_props, "d", p_skill_view_d);
cJSON* p_skill_view_input = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_view_input, "type", "string");
cJSON_AddItemToObject(t24b_props, "input", p_skill_view_input);
cJSON_AddItemToArray(t24b_required, cJSON_CreateString("d"));
cJSON_AddItemToObject(t24b_fn, "parameters", t24b_params);
cJSON_AddItemToObject(t24b, "function", t24b_fn);
cJSON_AddItemToArray(tools, t24b);
cJSON* t24c = cJSON_CreateObject();
cJSON* t24c_fn = cJSON_CreateObject();
cJSON* t24c_params = cJSON_CreateObject();
cJSON* t24c_props = cJSON_CreateObject();
cJSON_AddStringToObject(t24c, "type", "function");
cJSON_AddStringToObject(t24c_fn, "name", "skill_set");
cJSON_AddStringToObject(t24c_fn,
"description",
"Set/publish a full skill JSON payload and refresh runtime cache; JSON: {\"skill\":{\"d\":\"default_admin_dm\",\"kind\":31124,\"scope\":\"private\",\"content\":\"...\",\"tags\":[[\"d\",\"default_admin_dm\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"]]}}");
cJSON_AddStringToObject(t24c_params, "type", "object");
cJSON_AddItemToObject(t24c_params, "properties", t24c_props);
cJSON* p_skill_set_skill = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_set_skill, "type", "object");
cJSON_AddStringToObject(p_skill_set_skill, "description", "Full skill object from skill_get output content");
cJSON_AddItemToObject(t24c_props, "skill", p_skill_set_skill);
cJSON* p_skill_set_input = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_set_input, "type", "string");
cJSON_AddStringToObject(p_skill_set_input, "description", "Raw JSON string for slash shorthand or manual paste");
cJSON_AddItemToObject(t24c_props, "input", p_skill_set_input);
cJSON_AddItemToObject(t24c_fn, "parameters", t24c_params);
cJSON_AddItemToObject(t24c, "function", t24c_fn);
cJSON_AddItemToArray(tools, t24c);
cJSON* t24 = cJSON_CreateObject();
cJSON* t24_fn = cJSON_CreateObject();
cJSON* t24_params = cJSON_CreateObject();
@@ -1395,6 +1478,38 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
cJSON_AddItemToObject(t42, "function", t42_fn);
cJSON_AddItemToArray(tools, t42);
cJSON* t43 = cJSON_CreateObject();
cJSON* t43_fn = cJSON_CreateObject();
cJSON* t43_params = cJSON_CreateObject();
cJSON* t43_props = cJSON_CreateObject();
cJSON_AddStringToObject(t43, "type", "function");
cJSON_AddStringToObject(t43_fn, "name", "nostr_dm_history");
cJSON_AddStringToObject(t43_fn,
"description",
"Fetch recent DM history for template/context usage; JSON: {\"limit\":12,\"include_current\":false} or with explicit peer {\"peer_pubkey\":\"<hex64>\",\"limit\":20}; template usage: {{nostr_dm_history({\"limit\":12})}}");
cJSON_AddStringToObject(t43_params, "type", "object");
cJSON_AddItemToObject(t43_params, "properties", t43_props);
cJSON* p_dm_history_limit = cJSON_CreateObject();
cJSON_AddStringToObject(p_dm_history_limit, "type", "integer");
cJSON_AddStringToObject(p_dm_history_limit, "description", "Max message turns to return (1-200, default 12)");
cJSON_AddItemToObject(t43_props, "limit", p_dm_history_limit);
cJSON* p_dm_history_include_current = cJSON_CreateObject();
cJSON_AddStringToObject(p_dm_history_include_current, "type", "boolean");
cJSON_AddStringToObject(p_dm_history_include_current, "description", "When true, keep a trailing user message that matches the current live message");
cJSON_AddItemToObject(t43_props, "include_current", p_dm_history_include_current);
cJSON* p_dm_history_peer = cJSON_CreateObject();
cJSON_AddStringToObject(p_dm_history_peer, "type", "string");
cJSON_AddStringToObject(p_dm_history_peer, "description", "Optional peer pubkey hex (defaults to configured admin pubkey)");
cJSON_AddItemToObject(t43_props, "peer_pubkey", p_dm_history_peer);
cJSON_AddItemToObject(t43_fn, "parameters", t43_params);
cJSON_AddItemToObject(t43, "function", t43_fn);
cJSON_AddItemToArray(tools, t43);
cJSON* t44 = cJSON_CreateObject();
cJSON* t44_fn = cJSON_CreateObject();
cJSON* t44_params = cJSON_CreateObject();

View File

@@ -490,7 +490,7 @@ static cJSON* find_tag_value_string(cJSON* tags, const char* key) {
return NULL;
}
static int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65], char out_d_tag[65]) {
static __attribute__((unused)) int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65], char out_d_tag[65]) {
if (!addr || !out_kind || !out_pubkey || !out_d_tag) {
return -1;
}