diff --git a/README.md b/README.md index 9aa7658..5e0a17b 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ 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.12 +## Current Status — v0.2.13 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.2.12 — Fix nostr_core_lib EOSE_FIRST semantics and relay pool subscription cleanup +> Last release update: v0.2.13 — Unify startup skills, rename identity skill, add dm_history text format, and enforce startup publish success - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/context.log.md b/context.log.md index a4a6299..4db5c89 100644 --- a/context.log.md +++ b/context.log.md @@ -1,70 +1,56 @@ ```text Context Log - seen by model -timestamp=2026-03-22 19:38:41 +timestamp=2026-03-23 07:00:39 phase=llm_chat_with_tools_messages sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 model=claude-opus-4.6 -context_bytes=15036 -approx_tokens=3759 +context_bytes=2065 +approx_tokens=516 ``` -[{"role":"system","content":"You are {\"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\"}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\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 [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-22 19:31:53 (version v0.2.8, connected relays: 5/5).\",\"created_at\":1774222313},{\"role\":\"user\",\"content\":\"hello\",\"created_at\":1774222324},{\"role\":\"assistant\",\"content\":\"Hello! 👋 How can I help you today? I'm the Didactyl Agent — I can answer questions about the Didactyl project, help with Nostr-related tasks, or assist with anything else you need. Just let me know!\",\"created_at\":1774222330},{\"role\":\"user\",\"content\":\"/help\",\"created_at\":1774222358},{\"role\":\"assistant\",\"content\":\"AVAILABLE TOOLS\\n(run / -h for full JSON args template)\\n- admin_identity — Build admin identity context block from cached runtime metadata\\n- agent_identity — Build agent identity context block with pubkey and npub\\n- agent_version — Return current Didactyl version and metadata from build macros\\n- blossom_delete — Delete a blob from Blossom\\n- blossom_download — Download a blob from Blossom to a local file\\n- blossom_head — Fetch blob metadata from Blossom\\n- blossom_list — List blobs for a pubkey on Blossom\\n- blossom_upload — Upload a local file to a Blossom server\\n- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit\\n- cashu_wallet_check_proofs — Check current wallet proofs state against mint\\n- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint\\n- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id\\n- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request\\n- cashu_wallet_mint_check — Check a previously requested mint quote status\\n- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state\\n- cashu_wallet_mint_quote — Request a mint quote for a target amount\\n- cashu_wallet_mints_get — Get currently configured wallet and public mints\\n- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both\\n- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state\\n- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount\\n- config_recall — Fetch and decrypt agent config kind 30078 by d_tag\\n- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag\\n- local_file_read — Read a local file as text from the configured working directory\\n- local_file_write — Write text content to a local file in the configured working directory\\n- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body\\n- local_shell_exec — Execute a shell command and return stdout/stderr\\n- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)\\n- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed\\n- model_get — Get current active LLM runtime configuration (excluding API key)\\n- model_list — List available model IDs using provider OpenAI-compatible /models endpoint\\n- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)\\n- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context\\n- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context\\n- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context\\n- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32\\n- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format\\n- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context\\n- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list\\n- nostr_admin_notes — Build admin notes context block from cached kind 1 notes\\n- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata\\n- nostr_admin_relays — Build admin relay context block from cached kind 10002 data\\n- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list\\n- nostr_agent_notes — Build agent notes context block from cached kind 1 notes\\n- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata\\n- nostr_agent_relays — Build agent relay context block from cached kind 10002 data\\n- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default\\n- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache\\n- nostr_decode — Decode a Nostr bech32/nostr: URI into components\\n- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender\\n- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)\\n- nostr_dm_history — Fetch recent DM history for template/context usage; JSON: {\\\"limit\\\":12,\\\"include_current\\\":false} or with explicit peer {\\\"peer_pubkey\\\":\\\"\\\",\\\"limit\\\":20}; template usage: {{nostr_dm_history({\\\"limit\\\":12})}}\\n- nostr_dm_send — Send a NIP-04 encrypted DM\\n- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol\\n- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)\\n- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient\\n- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename\\n- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)\\n- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence\\n- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)\\n- nostr_npub — Return this agent's pubkey encoded as npub bech32\\n- nostr_post — Publish a Nostr event to connected relays\\n- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md\\n- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey\\n- nostr_pubkey — Return this agent's pubkey in hex format\\n- nostr_query — Query events from relays using a Nostr filter\\n- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)\\n- nostr_relay_info — Fetch NIP-11 relay information document\\n- nostr_relay_status — Get connection status and statistics for all relays\\n- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)\\n- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters\\n- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list\\n- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it\\n- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124\\n- skill_get — Get full skill JSON by d tag from local cache; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"} (slash shorthand also works: /skill_get default_admin_dm)\\n- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters\\n- skill_remove — Remove a skill address from kind 10123 adoption list\\n- skill_search — Search public skills by query/author and optionally rank by adoption popularity\\n- skill_set — 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\\\"]]}}\\n- skill_view — Compatibility alias for skill_get; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"}\\n- task_list — Build current task list context block from agent task memory on Nostr\\n- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace\\n- tool_list — List available tools with name, description, and JSON parameter schema\\n- trigger_list — List active triggered skills and their runtime status\\n\\nAVAILABLE SKILLS\\n- default_admin_dm — Default admin DM handler\\n\\nADOPTED SKILLS\\n- default_admin_dm\\n\",\"created_at\":1774222361},{\"role\":\"user\",\"content\":\"/skill_get default_admin_dm\",\"created_at\":1774222404},{\"role\":\"assistant\",\"content\":\"{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You 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 {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\\\\\"from\\\\\\\":\\\\\\\"admin\\\\\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222404},{\"role\":\"user\",\"content\":\"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\\n\\n /skill_get default_admin_dm\\n7922 3/22/2026, 7:33:24 PM\\nkind4-nip04\\n⋯\\n\\n{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You 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 {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\"from\\\":\\\"admin\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222576},{\"role\":\"assistant\",\"content\":\"Good catch! The relay query found **two versions** of the `default_admin_dm` skill:\\n\\n### Version 1 (older, more complete)\\n- **ID:** `b874122d049aaade3b802db9e80119db0b5c337adb85bd8ba65464d99e44c9a1`\\n- **Created:** `1774221663` (7:21:03 PM)\\n- Has `action`, `enabled` tags ✅\\n- Content has `{{nostr_dm_history}}` on its own line ✅\\n\\n### Version 2 (newer, the one in cache)\\n- **ID:** `bf7f93366ef3a70f30fa64c405631325f58c7f2a15d9217d6b14558ca2189ef7`\\n- **Created:** `1774222308` (7:31:48 PM)\\n- **Missing** `action` and `enabled` tags ⚠️\\n- `{{nostr_dm_history}}` is appended inline to the last rule line ⚠️\\n\\nSo the **real event ID** on relays is `bf7f93...189ef7`, not all zeros. The `skill_get` command returned `0000...0000` as the ID, which appears to be a display bug in the local cache — it has the correct content but didn't store/return the actual event ID.\\n\\nThe newer version (v2) also looks like it was republished with slightly degraded formatting (missing `action`/`enabled` tags and the template got concatenated). Would you like me to republish the skill with the correct tags restored from v1?\",\"created_at\":1774222598},{\"role\":\"user\",\"content\":\"/skill_get default_admin_dm\",\"created_at\":1774222631},{\"role\":\"assistant\",\"content\":\"{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You 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 {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\\\\\"from\\\\\\\":\\\\\\\"admin\\\\\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222631},{\"role\":\"user\",\"content\":\"What is the capital of Finland?\",\"created_at\":1774222721}]"},{"role":"user","content":"What is the capital of Finland?","_ts":1774222721}] +[{"role":"system","content":"You are {\n \"name\": \"Didactyl Agent\",\n \"display_name\": \"Didactyl Agent\",\n \"about\": \"A sovereign AI agent on Nostr.\\n\\nMission: Help maintain and answer questions about the Didactyl project.\\n\\nhttps://git.laantungir.net/laantungir/didactyl\\n\\nNot your keys, not your Bitcoin.\\nNot your keys, not your Agent.\\nNot your keys, not your Robot.\",\n \"picture\": \"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\n \"banner\": \"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"\n}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\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 [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-23 06:58:49 (version v0.2.12, connected relays: 5/5).\",\"created_at\":1774263529},{\"role\":\"user\",\"content\":\"Good morning\",\"created_at\":1774263548},{\"role\":\"assistant\",\"content\":\"Good morning! ☀️ How can I help you today?\",\"created_at\":1774263551},{\"role\":\"user\",\"content\":\"What is the capital of England?\",\"created_at\":1774263616},{\"role\":\"assistant\",\"content\":\"London! 🇬🇧\",\"created_at\":1774263619},{\"role\":\"user\",\"content\":\"How about France?\",\"created_at\":1774263628},{\"role\":\"assistant\",\"content\":\"Paris! 🇫🇷\",\"created_at\":1774263632},{\"role\":\"user\",\"content\":\"And Fiji?\",\"created_at\":1774263639}]"},{"role":"user","content":"And Fiji?","_ts":1774263639}] --- ```text Context Log - seen by model -timestamp=2026-03-22 19:36:28 +timestamp=2026-03-23 07:00:28 phase=llm_chat_with_tools_messages sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 model=claude-opus-4.6 -context_bytes=16892 -approx_tokens=4223 +context_bytes=1918 +approx_tokens=479 ``` -[{"role":"system","content":"You are {\"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\"}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\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 [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-22 19:31:53 (version v0.2.8, connected relays: 5/5).\",\"created_at\":1774222313},{\"role\":\"user\",\"content\":\"hello\",\"created_at\":1774222324},{\"role\":\"assistant\",\"content\":\"Hello! 👋 How can I help you today? I'm the Didactyl Agent — I can answer questions about the Didactyl project, help with Nostr-related tasks, or assist with anything else you need. Just let me know!\",\"created_at\":1774222330},{\"role\":\"user\",\"content\":\"/help\",\"created_at\":1774222358},{\"role\":\"assistant\",\"content\":\"AVAILABLE TOOLS\\n(run / -h for full JSON args template)\\n- admin_identity — Build admin identity context block from cached runtime metadata\\n- agent_identity — Build agent identity context block with pubkey and npub\\n- agent_version — Return current Didactyl version and metadata from build macros\\n- blossom_delete — Delete a blob from Blossom\\n- blossom_download — Download a blob from Blossom to a local file\\n- blossom_head — Fetch blob metadata from Blossom\\n- blossom_list — List blobs for a pubkey on Blossom\\n- blossom_upload — Upload a local file to a Blossom server\\n- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit\\n- cashu_wallet_check_proofs — Check current wallet proofs state against mint\\n- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint\\n- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id\\n- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request\\n- cashu_wallet_mint_check — Check a previously requested mint quote status\\n- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state\\n- cashu_wallet_mint_quote — Request a mint quote for a target amount\\n- cashu_wallet_mints_get — Get currently configured wallet and public mints\\n- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both\\n- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state\\n- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount\\n- config_recall — Fetch and decrypt agent config kind 30078 by d_tag\\n- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag\\n- local_file_read — Read a local file as text from the configured working directory\\n- local_file_write — Write text content to a local file in the configured working directory\\n- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body\\n- local_shell_exec — Execute a shell command and return stdout/stderr\\n- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)\\n- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed\\n- model_get — Get current active LLM runtime configuration (excluding API key)\\n- model_list — List available model IDs using provider OpenAI-compatible /models endpoint\\n- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)\\n- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context\\n- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context\\n- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context\\n- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32\\n- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format\\n- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context\\n- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list\\n- nostr_admin_notes — Build admin notes context block from cached kind 1 notes\\n- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata\\n- nostr_admin_relays — Build admin relay context block from cached kind 10002 data\\n- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list\\n- nostr_agent_notes — Build agent notes context block from cached kind 1 notes\\n- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata\\n- nostr_agent_relays — Build agent relay context block from cached kind 10002 data\\n- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default\\n- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache\\n- nostr_decode — Decode a Nostr bech32/nostr: URI into components\\n- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender\\n- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)\\n- nostr_dm_history — Fetch recent DM history for template/context usage; JSON: {\\\"limit\\\":12,\\\"include_current\\\":false} or with explicit peer {\\\"peer_pubkey\\\":\\\"\\\",\\\"limit\\\":20}; template usage: {{nostr_dm_history({\\\"limit\\\":12})}}\\n- nostr_dm_send — Send a NIP-04 encrypted DM\\n- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol\\n- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)\\n- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient\\n- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename\\n- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)\\n- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence\\n- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)\\n- nostr_npub — Return this agent's pubkey encoded as npub bech32\\n- nostr_post — Publish a Nostr event to connected relays\\n- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md\\n- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey\\n- nostr_pubkey — Return this agent's pubkey in hex format\\n- nostr_query — Query events from relays using a Nostr filter\\n- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)\\n- nostr_relay_info — Fetch NIP-11 relay information document\\n- nostr_relay_status — Get connection status and statistics for all relays\\n- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)\\n- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters\\n- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list\\n- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it\\n- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124\\n- skill_get — Get full skill JSON by d tag from local cache; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"} (slash shorthand also works: /skill_get default_admin_dm)\\n- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters\\n- skill_remove — Remove a skill address from kind 10123 adoption list\\n- skill_search — Search public skills by query/author and optionally rank by adoption popularity\\n- skill_set — 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\\\"]]}}\\n- skill_view — Compatibility alias for skill_get; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"}\\n- task_list — Build current task list context block from agent task memory on Nostr\\n- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace\\n- tool_list — List available tools with name, description, and JSON parameter schema\\n- trigger_list — List active triggered skills and their runtime status\\n\\nAVAILABLE SKILLS\\n- default_admin_dm — Default admin DM handler\\n\\nADOPTED SKILLS\\n- default_admin_dm\\n\",\"created_at\":1774222361},{\"role\":\"user\",\"content\":\"/skill_get default_admin_dm\",\"created_at\":1774222404},{\"role\":\"assistant\",\"content\":\"{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You 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 {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\\\\\"from\\\\\\\":\\\\\\\"admin\\\\\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222404},{\"role\":\"user\",\"content\":\"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\\n\\n /skill_get default_admin_dm\\n7922 3/22/2026, 7:33:24 PM\\nkind4-nip04\\n⋯\\n\\n{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You 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 {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\"from\\\":\\\"admin\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222576}]"},{"role":"user","content":"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\n\n /skill_get default_admin_dm\n7922 3/22/2026, 7:33:24 PM\nkind4-nip04\n⋯\n\n{\"success\":true,\"d_tag\":\"default_admin_dm\",\"kind\":31124,\"scope\":\"private\",\"owner\":\"agent\",\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"created_at\":1774222308,\"content\":\"You 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 {{nostr_dm_history}}\",\"tags\":[[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\"from\":\"admin\"}\"],[\"d\",\"default_admin_dm\"]]}","_ts":1774222576},{"role":"assistant","content":"\n\nYou're right — that `id` of all zeros looks suspicious. Let me query the relays directly for this event.","tool_calls":[{"id":"toolu_bdrk_01Lo8nHAHmPPhMzDxSp1CLXr","type":"function","function":{"name":"my_pubkey","arguments":"{}"}}]},{"role":"tool","tool_call_id":"toolu_bdrk_01Lo8nHAHmPPhMzDxSp1CLXr","content":"{\"success\":true,\"pubkey\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\",\"content\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\"}"},{"role":"assistant","content":null,"tool_calls":[{"id":"toolu_bdrk_01AhjRUEVi3vkcvwPXJ6t1pv","type":"function","function":{"name":"nostr_query","arguments":"{\"filter\": {\"authors\":[\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\"],\"kinds\":[31124],\"#d\":[\"default_admin_dm\"],\"limit\":5}}"}}]},{"role":"tool","tool_call_id":"toolu_bdrk_01AhjRUEVi3vkcvwPXJ6t1pv","content":"{\"success\":true,\"events\":[{\"pubkey\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\",\"created_at\":1774221663,\"kind\":31124,\"tags\":[[\"description\",\"Default admin DM handler\"],[\"d\",\"default_admin_dm\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"],[\"action\",\"llm\"],[\"enabled\",\"true\"]],\"content\":\"You 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\\n\\n{{nostr_dm_history}}\",\"id\":\"b874122d049aaade3b802db9e80119db0b5c337adb85bd8ba65464d99e44c9a1\",\"sig\":\"5c16bdc4306953604ed4b85e7aa5fb684dcf2c8858ba72fa32a9d26556682e73058b3a1c9077c1c4ce6e4d6ba88d076f5d611ca78050a2a2cfafe318f621e7b9\"},{\"content\":\"You 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 {{nostr_dm_history}}\",\"created_at\":1774222308,\"id\":\"bf7f93366ef3a70f30fa64c405631325f58c7f2a15d9217d6b14558ca2189ef7\",\"kind\":31124,\"pubkey\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\",\"sig\":\"b3d9c15a75badcad501d08416a6d3bf3847cd0a5ec9b1425fbbaf8a02f1e947cf0ba7c91ccc863011be9b1c9e33429db4d82013edbf9e6f8e01df11717b33527\",\"tags\":[[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"],[\"d\",\"default_admin_dm\"]]}]}"}] +[{"role":"system","content":"You are {\n \"name\": \"Didactyl Agent\",\n \"display_name\": \"Didactyl Agent\",\n \"about\": \"A sovereign AI agent on Nostr.\\n\\nMission: Help maintain and answer questions about the Didactyl project.\\n\\nhttps://git.laantungir.net/laantungir/didactyl\\n\\nNot your keys, not your Bitcoin.\\nNot your keys, not your Agent.\\nNot your keys, not your Robot.\",\n \"picture\": \"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\n \"banner\": \"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"\n}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\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 [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-23 06:58:49 (version v0.2.12, connected relays: 5/5).\",\"created_at\":1774263529},{\"role\":\"user\",\"content\":\"Good morning\",\"created_at\":1774263548},{\"role\":\"assistant\",\"content\":\"Good morning! ☀️ How can I help you today?\",\"created_at\":1774263551},{\"role\":\"user\",\"content\":\"What is the capital of England?\",\"created_at\":1774263616},{\"role\":\"assistant\",\"content\":\"London! 🇬🇧\",\"created_at\":1774263619},{\"role\":\"user\",\"content\":\"How about France?\",\"created_at\":1774263628}]"},{"role":"user","content":"How about France?","_ts":1774263628}] --- ```text Context Log - seen by model -timestamp=2026-03-22 19:36:20 +timestamp=2026-03-23 07:00:16 phase=llm_chat_with_tools_messages sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 model=claude-opus-4.6 -context_bytes=14107 -approx_tokens=3526 +context_bytes=1768 +approx_tokens=442 ``` -[{"role":"system","content":"You are {\"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\"}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\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 [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-22 19:31:53 (version v0.2.8, connected relays: 5/5).\",\"created_at\":1774222313},{\"role\":\"user\",\"content\":\"hello\",\"created_at\":1774222324},{\"role\":\"assistant\",\"content\":\"Hello! 👋 How can I help you today? I'm the Didactyl Agent — I can answer questions about the Didactyl project, help with Nostr-related tasks, or assist with anything else you need. Just let me know!\",\"created_at\":1774222330},{\"role\":\"user\",\"content\":\"/help\",\"created_at\":1774222358},{\"role\":\"assistant\",\"content\":\"AVAILABLE TOOLS\\n(run / -h for full JSON args template)\\n- admin_identity — Build admin identity context block from cached runtime metadata\\n- agent_identity — Build agent identity context block with pubkey and npub\\n- agent_version — Return current Didactyl version and metadata from build macros\\n- blossom_delete — Delete a blob from Blossom\\n- blossom_download — Download a blob from Blossom to a local file\\n- blossom_head — Fetch blob metadata from Blossom\\n- blossom_list — List blobs for a pubkey on Blossom\\n- blossom_upload — Upload a local file to a Blossom server\\n- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit\\n- cashu_wallet_check_proofs — Check current wallet proofs state against mint\\n- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint\\n- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id\\n- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request\\n- cashu_wallet_mint_check — Check a previously requested mint quote status\\n- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state\\n- cashu_wallet_mint_quote — Request a mint quote for a target amount\\n- cashu_wallet_mints_get — Get currently configured wallet and public mints\\n- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both\\n- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state\\n- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount\\n- config_recall — Fetch and decrypt agent config kind 30078 by d_tag\\n- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag\\n- local_file_read — Read a local file as text from the configured working directory\\n- local_file_write — Write text content to a local file in the configured working directory\\n- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body\\n- local_shell_exec — Execute a shell command and return stdout/stderr\\n- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)\\n- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed\\n- model_get — Get current active LLM runtime configuration (excluding API key)\\n- model_list — List available model IDs using provider OpenAI-compatible /models endpoint\\n- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)\\n- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context\\n- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context\\n- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context\\n- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32\\n- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format\\n- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context\\n- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list\\n- nostr_admin_notes — Build admin notes context block from cached kind 1 notes\\n- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata\\n- nostr_admin_relays — Build admin relay context block from cached kind 10002 data\\n- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list\\n- nostr_agent_notes — Build agent notes context block from cached kind 1 notes\\n- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata\\n- nostr_agent_relays — Build agent relay context block from cached kind 10002 data\\n- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default\\n- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache\\n- nostr_decode — Decode a Nostr bech32/nostr: URI into components\\n- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender\\n- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)\\n- nostr_dm_history — Fetch recent DM history for template/context usage; JSON: {\\\"limit\\\":12,\\\"include_current\\\":false} or with explicit peer {\\\"peer_pubkey\\\":\\\"\\\",\\\"limit\\\":20}; template usage: {{nostr_dm_history({\\\"limit\\\":12})}}\\n- nostr_dm_send — Send a NIP-04 encrypted DM\\n- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol\\n- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)\\n- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient\\n- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename\\n- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)\\n- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence\\n- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)\\n- nostr_npub — Return this agent's pubkey encoded as npub bech32\\n- nostr_post — Publish a Nostr event to connected relays\\n- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md\\n- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey\\n- nostr_pubkey — Return this agent's pubkey in hex format\\n- nostr_query — Query events from relays using a Nostr filter\\n- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)\\n- nostr_relay_info — Fetch NIP-11 relay information document\\n- nostr_relay_status — Get connection status and statistics for all relays\\n- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)\\n- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters\\n- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list\\n- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it\\n- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124\\n- skill_get — Get full skill JSON by d tag from local cache; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"} (slash shorthand also works: /skill_get default_admin_dm)\\n- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters\\n- skill_remove — Remove a skill address from kind 10123 adoption list\\n- skill_search — Search public skills by query/author and optionally rank by adoption popularity\\n- skill_set — 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\\\"]]}}\\n- skill_view — Compatibility alias for skill_get; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"}\\n- task_list — Build current task list context block from agent task memory on Nostr\\n- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace\\n- tool_list — List available tools with name, description, and JSON parameter schema\\n- trigger_list — List active triggered skills and their runtime status\\n\\nAVAILABLE SKILLS\\n- default_admin_dm — Default admin DM handler\\n\\nADOPTED SKILLS\\n- default_admin_dm\\n\",\"created_at\":1774222361},{\"role\":\"user\",\"content\":\"/skill_get default_admin_dm\",\"created_at\":1774222404},{\"role\":\"assistant\",\"content\":\"{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You 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 {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\\\\\"from\\\\\\\":\\\\\\\"admin\\\\\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222404},{\"role\":\"user\",\"content\":\"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\\n\\n /skill_get default_admin_dm\\n7922 3/22/2026, 7:33:24 PM\\nkind4-nip04\\n⋯\\n\\n{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You 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 {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\"from\\\":\\\"admin\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222576}]"},{"role":"user","content":"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\n\n /skill_get default_admin_dm\n7922 3/22/2026, 7:33:24 PM\nkind4-nip04\n⋯\n\n{\"success\":true,\"d_tag\":\"default_admin_dm\",\"kind\":31124,\"scope\":\"private\",\"owner\":\"agent\",\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"created_at\":1774222308,\"content\":\"You 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 {{nostr_dm_history}}\",\"tags\":[[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\"from\":\"admin\"}\"],[\"d\",\"default_admin_dm\"]]}","_ts":1774222576},{"role":"assistant","content":"\n\nYou're right — that `id` of all zeros looks suspicious. Let me query the relays directly for this event.","tool_calls":[{"id":"toolu_bdrk_01Lo8nHAHmPPhMzDxSp1CLXr","type":"function","function":{"name":"my_pubkey","arguments":"{}"}}]},{"role":"tool","tool_call_id":"toolu_bdrk_01Lo8nHAHmPPhMzDxSp1CLXr","content":"{\"success\":true,\"pubkey\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\",\"content\":\"52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8\"}"}] +[{"role":"system","content":"You are {\n \"name\": \"Didactyl Agent\",\n \"display_name\": \"Didactyl Agent\",\n \"about\": \"A sovereign AI agent on Nostr.\\n\\nMission: Help maintain and answer questions about the Didactyl project.\\n\\nhttps://git.laantungir.net/laantungir/didactyl\\n\\nNot your keys, not your Bitcoin.\\nNot your keys, not your Agent.\\nNot your keys, not your Robot.\",\n \"picture\": \"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\n \"banner\": \"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"\n}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\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 [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-23 06:58:49 (version v0.2.12, connected relays: 5/5).\",\"created_at\":1774263529},{\"role\":\"user\",\"content\":\"Good morning\",\"created_at\":1774263548},{\"role\":\"assistant\",\"content\":\"Good morning! ☀️ How can I help you today?\",\"created_at\":1774263551},{\"role\":\"user\",\"content\":\"What is the capital of England?\",\"created_at\":1774263616}]"},{"role":"user","content":"What is the capital of England?","_ts":1774263616}] --- ```text Context Log - seen by model -timestamp=2026-03-22 19:36:16 +timestamp=2026-03-23 06:59:08 phase=llm_chat_with_tools_messages sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 model=claude-opus-4.6 -context_bytes=13575 -approx_tokens=3393 +context_bytes=1541 +approx_tokens=385 ``` -[{"role":"system","content":"You are {\"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\"}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\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 [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-22 19:31:53 (version v0.2.8, connected relays: 5/5).\",\"created_at\":1774222313},{\"role\":\"user\",\"content\":\"hello\",\"created_at\":1774222324},{\"role\":\"assistant\",\"content\":\"Hello! 👋 How can I help you today? I'm the Didactyl Agent — I can answer questions about the Didactyl project, help with Nostr-related tasks, or assist with anything else you need. Just let me know!\",\"created_at\":1774222330},{\"role\":\"user\",\"content\":\"/help\",\"created_at\":1774222358},{\"role\":\"assistant\",\"content\":\"AVAILABLE TOOLS\\n(run / -h for full JSON args template)\\n- admin_identity — Build admin identity context block from cached runtime metadata\\n- agent_identity — Build agent identity context block with pubkey and npub\\n- agent_version — Return current Didactyl version and metadata from build macros\\n- blossom_delete — Delete a blob from Blossom\\n- blossom_download — Download a blob from Blossom to a local file\\n- blossom_head — Fetch blob metadata from Blossom\\n- blossom_list — List blobs for a pubkey on Blossom\\n- blossom_upload — Upload a local file to a Blossom server\\n- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit\\n- cashu_wallet_check_proofs — Check current wallet proofs state against mint\\n- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint\\n- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id\\n- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request\\n- cashu_wallet_mint_check — Check a previously requested mint quote status\\n- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state\\n- cashu_wallet_mint_quote — Request a mint quote for a target amount\\n- cashu_wallet_mints_get — Get currently configured wallet and public mints\\n- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both\\n- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state\\n- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount\\n- config_recall — Fetch and decrypt agent config kind 30078 by d_tag\\n- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag\\n- local_file_read — Read a local file as text from the configured working directory\\n- local_file_write — Write text content to a local file in the configured working directory\\n- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body\\n- local_shell_exec — Execute a shell command and return stdout/stderr\\n- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)\\n- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed\\n- model_get — Get current active LLM runtime configuration (excluding API key)\\n- model_list — List available model IDs using provider OpenAI-compatible /models endpoint\\n- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)\\n- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context\\n- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context\\n- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context\\n- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32\\n- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format\\n- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context\\n- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list\\n- nostr_admin_notes — Build admin notes context block from cached kind 1 notes\\n- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata\\n- nostr_admin_relays — Build admin relay context block from cached kind 10002 data\\n- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list\\n- nostr_agent_notes — Build agent notes context block from cached kind 1 notes\\n- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata\\n- nostr_agent_relays — Build agent relay context block from cached kind 10002 data\\n- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default\\n- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache\\n- nostr_decode — Decode a Nostr bech32/nostr: URI into components\\n- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender\\n- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)\\n- nostr_dm_history — Fetch recent DM history for template/context usage; JSON: {\\\"limit\\\":12,\\\"include_current\\\":false} or with explicit peer {\\\"peer_pubkey\\\":\\\"\\\",\\\"limit\\\":20}; template usage: {{nostr_dm_history({\\\"limit\\\":12})}}\\n- nostr_dm_send — Send a NIP-04 encrypted DM\\n- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol\\n- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)\\n- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient\\n- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename\\n- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)\\n- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence\\n- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)\\n- nostr_npub — Return this agent's pubkey encoded as npub bech32\\n- nostr_post — Publish a Nostr event to connected relays\\n- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md\\n- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey\\n- nostr_pubkey — Return this agent's pubkey in hex format\\n- nostr_query — Query events from relays using a Nostr filter\\n- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)\\n- nostr_relay_info — Fetch NIP-11 relay information document\\n- nostr_relay_status — Get connection status and statistics for all relays\\n- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)\\n- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters\\n- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list\\n- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it\\n- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124\\n- skill_get — Get full skill JSON by d tag from local cache; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"} (slash shorthand also works: /skill_get default_admin_dm)\\n- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters\\n- skill_remove — Remove a skill address from kind 10123 adoption list\\n- skill_search — Search public skills by query/author and optionally rank by adoption popularity\\n- skill_set — 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\\\"]]}}\\n- skill_view — Compatibility alias for skill_get; JSON: {\\\"d\\\":\\\"default_admin_dm\\\"}\\n- task_list — Build current task list context block from agent task memory on Nostr\\n- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace\\n- tool_list — List available tools with name, description, and JSON parameter schema\\n- trigger_list — List active triggered skills and their runtime status\\n\\nAVAILABLE SKILLS\\n- default_admin_dm — Default admin DM handler\\n\\nADOPTED SKILLS\\n- default_admin_dm\\n\",\"created_at\":1774222361},{\"role\":\"user\",\"content\":\"/skill_get default_admin_dm\",\"created_at\":1774222404},{\"role\":\"assistant\",\"content\":\"{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You 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 {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\\\\\"from\\\\\\\":\\\\\\\"admin\\\\\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222404},{\"role\":\"user\",\"content\":\"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\\n\\n /skill_get default_admin_dm\\n7922 3/22/2026, 7:33:24 PM\\nkind4-nip04\\n⋯\\n\\n{\\\"success\\\":true,\\\"d_tag\\\":\\\"default_admin_dm\\\",\\\"kind\\\":31124,\\\"scope\\\":\\\"private\\\",\\\"owner\\\":\\\"agent\\\",\\\"id\\\":\\\"0000000000000000000000000000000000000000000000000000000000000000\\\",\\\"created_at\\\":1774222308,\\\"content\\\":\\\"You 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 {{nostr_dm_history}}\\\",\\\"tags\\\":[[\\\"app\\\",\\\"didactyl\\\"],[\\\"scope\\\",\\\"private\\\"],[\\\"description\\\",\\\"Default admin DM handler\\\"],[\\\"trigger\\\",\\\"dm\\\"],[\\\"filter\\\",\\\"{\\\"from\\\":\\\"admin\\\"}\\\"],[\\\"d\\\",\\\"default_admin_dm\\\"]]}\",\"created_at\":1774222576}]"},{"role":"user","content":"This agent id doesn't look right. Can you do a direct nostr search on your relays for this event under your npub?\n\n /skill_get default_admin_dm\n7922 3/22/2026, 7:33:24 PM\nkind4-nip04\n⋯\n\n{\"success\":true,\"d_tag\":\"default_admin_dm\",\"kind\":31124,\"scope\":\"private\",\"owner\":\"agent\",\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"created_at\":1774222308,\"content\":\"You 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 {{nostr_dm_history}}\",\"tags\":[[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\"from\":\"admin\"}\"],[\"d\",\"default_admin_dm\"]]}","_ts":1774222576}] - ---- - -```text -Context Log - seen by model -timestamp=2026-03-22 19:32:06 -phase=llm_chat_with_tools_messages -sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 -model=claude-opus-4.6 -context_bytes=1526 -approx_tokens=381 -``` - -[{"role":"system","content":"You are {\n \"name\": \"Didactyl Agent\",\n \"display_name\": \"Didactyl Agent\",\n \"about\": \"A sovereign AI agent on Nostr.\\n\\nMission: Help maintain and answer questions about the Didactyl project.\\n\\nhttps://git.laantungir.net/laantungir/didactyl\\n\\nNot your keys, not your Bitcoin.\\nNot your keys, not your Agent.\\nNot your keys, not your Robot.\",\n \"picture\": \"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\n \"banner\": \"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"\n}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\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 [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-22 19:31:53 (version v0.2.8, connected relays: 5/5).\",\"created_at\":1774222313},{\"role\":\"user\",\"content\":\"hello\",\"created_at\":1774222324}]"},{"role":"user","content":"hello","_ts":1774222326}] +[{"role":"system","content":"You are {\n \"name\": \"Didactyl Agent\",\n \"display_name\": \"Didactyl Agent\",\n \"about\": \"A sovereign AI agent on Nostr.\\n\\nMission: Help maintain and answer questions about the Didactyl project.\\n\\nhttps://git.laantungir.net/laantungir/didactyl\\n\\nNot your keys, not your Bitcoin.\\nNot your keys, not your Agent.\\nNot your keys, not your Robot.\",\n \"picture\": \"https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png\",\n \"banner\": \"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg\"\n}\n\nYour npub: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn\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 [{\"role\":\"assistant\",\"content\":\"Didactyl has started up and is online at 2026-03-23 06:58:49 (version v0.2.12, connected relays: 5/5).\",\"created_at\":1774263529},{\"role\":\"user\",\"content\":\"Good morning\",\"created_at\":1774263548}]"},{"role":"user","content":"Good morning","_ts":1774263548}] --- diff --git a/docs/GENESIS.md b/docs/GENESIS.md index a8f4607..88e1415 100644 --- a/docs/GENESIS.md +++ b/docs/GENESIS.md @@ -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 trigger behavior, and baseline runtime settings so the agent can publish itself onto Nostr. +It defines initial identity, admin policy, startup events (including startup skills), 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` (published as private kind `31124`, default `d=default_admin_dm`) +- Startup skills in `startup_events` (typically private kind `31124`, e.g. `d=identity_and_rules` and `d=dm_history`) --- @@ -121,10 +121,11 @@ That relay list is used as the initial network attachment for querying existing ## Migration Notes (v0.2.0) - 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}}`. +- Startup skills are defined directly in `startup_events` as kind `31123`/`31124` events. +- Typical default DM stack is two startup skills with tags: + - `d=identity_and_rules` with `trigger=dm` and `filter={"from":"admin"}` + - `d=dm_history` with `trigger=dm` and `filter={"from":"admin"}` +- Skill content is plain markdown with inline variables such as `{{my_kind0_profile}}`, `{{my_npub}}`, and `{{nostr_dm_history({"format":"text","limit":12})}}`. --- diff --git a/genesis.jsonc.example b/genesis.jsonc.example index ea15f61..0530353 100644 --- a/genesis.jsonc.example +++ b/genesis.jsonc.example @@ -61,20 +61,30 @@ ["r", "wss://relay.primal.net"] ] } - ], - - // ─── Default Skill ──────────────────────────────────────────────── - // Keep this generic and non-sensitive for repository examples. - "default_skill": { - "d_tag": "default_admin_dm", - "kind": 31124, - "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"], - ["description", "Default admin DM handler"], - ["trigger", "dm"], - ["filter", "{\"from\":\"admin\"}"] - ] - } + }, + { + "kind": 31124, + "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": [ + ["d", "identity_and_rules"], + ["app", "didactyl"], + ["scope", "private"], + ["description", "Agent identity and behavioral rules"], + ["trigger", "dm"], + ["filter", "{\"from\":\"admin\"}"] + ] + }, + { + "kind": 31124, + "content": "## Recent Conversation\n\n{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}", + "tags": [ + ["d", "dm_history"], + ["app", "didactyl"], + ["scope", "private"], + ["description", "DM conversation history for context continuity"], + ["trigger", "dm"], + ["filter", "{\"from\":\"admin\"}"] + ] + } + ] } diff --git a/plans/dm_history_context_cleanup.md b/plans/dm_history_context_cleanup.md new file mode 100644 index 0000000..f24afa7 --- /dev/null +++ b/plans/dm_history_context_cleanup.md @@ -0,0 +1,246 @@ +# DM History Context Cleanup, Skill Rename & Startup Events Unification + +## Problems + +### 1. DM History as JSON in System Prompt +The `{{nostr_dm_history}}` template variable resolves to a raw JSON array string pasted into the system prompt. The LLM receives conversation history as JSON-within-text, wasting tokens and forcing it to parse structured data embedded in prose. + +### 2. Misleading Skill Name +The skill `default_admin_dm` is really an identity and rules skill. Rename to `identity_and_rules`. + +### 3. `default_skill` Config Section Is Redundant +The `default_skill` section in genesis.jsonc is a special-case config path that converts a skill into a startup event and auto-adopts it. This is unnecessary — skills should just be startup events like everything else. The program should treat all startup events equally and halt if any fail to publish. + +## Solution + +Three coordinated changes: + +### Part A: DM History Text Format +Add a `format` parameter to the `nostr_dm_history` tool. Create a separate `dm_history` skill. + +### Part B: Skill Rename +Rename `default_admin_dm` → `identity_and_rules` everywhere. + +### Part C: Eliminate `default_skill` Config Section +Move skills into `startup_events`. Auto-adopt kind 31123/31124 startup events. Make startup event publishing mandatory (halt on failure). + +--- + +## Implementation Steps + +### Part A: DM History Text Format + +#### A1. Add `format` parameter to `execute_nostr_dm_history()` + +**File:** `src/tools/tool_agent.c` — line 265 + +Add a `format` parameter: `"json"` (default, current behavior) or `"text"`. + +When `format` is `"text"`, after the filtering loop (lines 320-360), iterate the filtered array and build plain text: + +``` +User: Good morning +Assistant: Good morning! ☀️ How can I help you today? +User: What is the capital of England? +Assistant: London! 🇬🇧 +``` + +Existing `include_current=false` default already deduplicates the current live message (line 339). + +#### A2. Add `format` to the tool schema + +**File:** `src/tools/tools_schema.c` — around line 1493 + +Add `format` property with description. Update tool description to mention the parameter. + +--- + +### Part B: Skill Rename + +#### B1. Update `src/default_events.h` + +- `DIDACTYL_DEFAULT_SKILL_D_TAG`: `"default_admin_dm"` → `"identity_and_rules"` +- `DIDACTYL_DEFAULT_SKILL_TAGS_JSON`: update d-tag and description +- `DIDACTYL_DEFAULT_SKILL_TEMPLATE`: keep as-is (already clean, no `{{nostr_dm_history}}`) + +#### B2. Update tool description examples in `src/tools/tools_schema.c` + +- Line 854: `skill_get` example +- Line 883: `skill_view` example +- Line 911: `skill_set` example + +#### B3. Update `docs/GENESIS.md` + +- Line 33 and 124: `default_admin_dm` → `identity_and_rules` + +--- + +### Part C: Eliminate `default_skill` Config Section + +#### C1. Auto-adopt kind 31123/31124 startup events + +**File:** `src/config.c` + +Replace `config_ensure_default_skill_startup_events()` with a new function `config_ensure_startup_skill_adoption()` that: + +1. Scans all startup events for kind 31123 and 31124 +2. For each skill found, extracts its `d_tag` from tags +3. Builds the skill address (`kind:pubkey:d_tag`) +4. Ensures a kind 10123 adoption list startup event exists with all skill addresses +5. If no 10123 event exists, creates one + +This replaces the single-skill logic with a generic "adopt all startup skills" approach. + +#### C2. Remove `default_skill` config parsing + +**File:** `src/config.c` + +- Remove `parse_default_skill_config()` function (lines 733-816) +- Remove the call at line 1369 +- Keep backward compatibility: if `default_skill` section exists in genesis.jsonc, convert it to a startup event during parsing (migration path) — OR just remove support and let users update their genesis.jsonc + +#### C3. Remove `default_skill_config_t` from config struct + +**File:** `src/config.h` + +- Remove `default_skill_config_t` typedef (lines 67-72) +- Remove `default_skill` field from `didactyl_config_t` (line 134) + +#### C4. Update `src/main.c` + +- Line 1060: Replace `config_ensure_default_skill_startup_events()` call with `config_ensure_startup_skill_adoption()` + +#### C5. Make startup event publishing mandatory + +**File:** `src/nostr_handler.c` + +In the startup publish flow (around line 3150), after all relays have been tried: +- Check that each startup event was published to at least one relay +- If any event failed on all relays, log an error and return failure +- The caller in `main.c` should halt on this failure + +#### C6. Update setup wizard + +**File:** `src/setup_wizard.c` + +- `configure_default_skill_for_agent()` (line 498): Instead of populating `cfg->default_skill`, append the identity skill as a startup event directly +- Add the `dm_history` skill as a second startup event +- The auto-adoption logic (C1) handles the adoption list + +#### C7. Update `default_events.h` + +Add the `dm_history` skill template: + +```c +#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_D_TAG "dm_history" + +static const char* DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE = + "## Recent Conversation\n\n" + "{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}"; +``` + +--- + +### Part D: Genesis Config Updates + +#### D1. Update `genesis.jsonc.example` + +Remove `default_skill` section. Add skills as startup events: + +```jsonc +"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": "", + "tags": [ + ["r", "wss://relay.damus.io"], + ["r", "wss://relay.primal.net"] + ] + }, + { + "kind": 31124, + "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": [ + ["d", "identity_and_rules"], + ["app", "didactyl"], + ["scope", "private"], + ["description", "Agent identity and behavioral rules"], + ["trigger", "dm"], + ["filter", "{\"from\":\"admin\"}"] + ] + }, + { + "kind": 31124, + "content": "## Recent Conversation\n\n{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}", + "tags": [ + ["d", "dm_history"], + ["app", "didactyl"], + ["scope", "private"], + ["description", "DM conversation history for context continuity"], + ["trigger", "dm"], + ["filter", "{\"from\":\"admin\"}"] + ] + } + ] +``` + +#### D2. Update live `genesis.jsonc` + +Same changes as D1 but for the actual deployment config. + +--- + +## Result + +### Before + +```json +[ + {"role": "system", "content": "You are {...}\n\nYour npub: npub1...\n\n## Rules\n...\n[{\"role\":\"user\",\"content\":\"Good morning\",...}]"}, + {"role": "user", "content": "And Fiji?"} +] +``` + +### After + +```json +[ + {"role": "system", "content": "# Didactyl Agent\n\nYou are {...}\n\nYour npub: npub1...\n\n## Rules\n...\n\n---\n\n## Recent Conversation\n\nUser: Good morning\nAssistant: Good morning! ☀️ How can I help you today?\nUser: What is the capital of England?\nAssistant: London! 🇬🇧\nUser: How about France?\nAssistant: Paris! 🇫🇷"}, + {"role": "user", "content": "And Fiji?"} +] +``` + +--- + +## Files Changed + +| File | Change | +|------|--------| +| `src/tools/tool_agent.c` | Add `format` parameter to `execute_nostr_dm_history()` | +| `src/tools/tools_schema.c` | Add `format` to schema; update example d-tags | +| `src/default_events.h` | Rename d-tag; add `dm_history` skill template | +| `src/config.h` | Remove `default_skill_config_t` and field | +| `src/config.c` | Remove `parse_default_skill_config()`; replace `config_ensure_default_skill_startup_events()` with `config_ensure_startup_skill_adoption()` | +| `src/main.c` | Update call to new adoption function | +| `src/nostr_handler.c` | Make startup event publishing mandatory (halt on total failure) | +| `src/setup_wizard.c` | Put skills in startup_events; add dm_history | +| `genesis.jsonc.example` | Remove `default_skill`; add skills as startup events | +| `genesis.jsonc` | Same as example | +| `docs/GENESIS.md` | Update references | + +## Migration Note + +For backward compatibility, consider keeping `parse_default_skill_config()` as a migration shim that converts `default_skill` into a startup event with a deprecation warning. This way existing genesis.jsonc files with `default_skill` still work but users are prompted to update. diff --git a/src/agent.c b/src/agent.c index b44ee78..9d0a5d0 100644 --- a/src/agent.c +++ b/src/agent.c @@ -778,7 +778,9 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa size_t tool_len = (size_t)(p - (message + 1)); if (tool_len == 0 || tool_len >= 128U) { - (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "{\"success\":false,\"error\":\"invalid slash command\"}"); + (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex, + "{\"success\":false,\"error\":\"invalid slash command\"}", + DIDACTYL_DM_HISTORY_TOOL_RESPONSE); return 1; } @@ -835,7 +837,9 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa free(log_payload); } - (void)nostr_handler_send_dm_auto(sender_pubkey_hex, dm_payload ? dm_payload : "Command executed with no output."); + (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex, + dm_payload ? dm_payload : "Command executed with no output.", + DIDACTYL_DM_HISTORY_TOOL_RESPONSE); free(markdown_result); free(result_json); return 1; @@ -2039,12 +2043,16 @@ void agent_on_message(const char* sender_pubkey_hex, } int allow_tools = (tier == DIDACTYL_SENDER_ADMIN) && g_cfg->tools.enabled && g_cfg->security.admin.tools_enabled; + int inbound_role = (message[0] == '/') ? DIDACTYL_DM_HISTORY_TOOL_REQUEST : DIDACTYL_DM_HISTORY_USER; + (void)nostr_handler_dm_history_remember(sender_pubkey_hex, message, (didactyl_dm_history_role_t)inbound_role); if (message[0] == '/') { if (!allow_tools) { const char* denied = "{\"success\":false,\"error\":\"slash commands are disabled for this sender tier\"}"; append_context_log(sender_pubkey_hex, "direct_tool_exec", denied); - (void)nostr_handler_send_dm_auto(sender_pubkey_hex, denied); + (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex, + denied, + DIDACTYL_DM_HISTORY_TOOL_RESPONSE); return; } if (handle_slash_command(sender_pubkey_hex, message)) { diff --git a/src/config.c b/src/config.c index 670a981..562acc0 100644 --- a/src/config.c +++ b/src/config.c @@ -692,6 +692,9 @@ static int set_tag_value_string(cJSON* tags, const char* tag_key, const char* ta return 0; } +static int startup_event_has_d_tag(const startup_event_t* se, const char* d_tag); +static int append_startup_event(didactyl_config_t* config, int kind, const char* content, const char* tags_json); + static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) { if (!item || !tags || !cJSON_IsArray(tags)) { return 0; @@ -730,7 +733,7 @@ static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) { return set_tag_value_string(tags, "d", d_tag); } -static int parse_default_skill_config(cJSON* root, didactyl_config_t* config) { +static int parse_legacy_default_skill_to_startup_events(cJSON* root, didactyl_config_t* config) { if (!root || !config) { return -1; } @@ -746,19 +749,20 @@ static int parse_default_skill_config(cJSON* root, didactyl_config_t* config) { cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(def, "content_fields"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(def, "tags"); - if (!kind || !cJSON_IsNumber(kind)) { + if (!kind || !cJSON_IsNumber(kind) || !d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || d_tag->valuestring[0] == '\0') { return -1; } + int kind_i = (int)kind->valuedouble; if (kind_i != 31123 && kind_i != 31124) { return -1; } - if (!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || d_tag->valuestring[0] == '\0') { - return -1; - } - if (strlen(d_tag->valuestring) >= sizeof(config->default_skill.d_tag)) { - return -1; + for (int i = 0; i < config->startup_event_count; i++) { + startup_event_t* se = &config->startup_events[i]; + if (se->kind == kind_i && startup_event_has_d_tag(se, d_tag->valuestring)) { + return 0; + } } char* content_text = NULL; @@ -806,13 +810,10 @@ static int parse_default_skill_config(cJSON* root, didactyl_config_t* config) { return -1; } - free(config->default_skill.content); - free(config->default_skill.tags_json); - config->default_skill.kind = kind_i; - snprintf(config->default_skill.d_tag, sizeof(config->default_skill.d_tag), "%s", d_tag->valuestring); - config->default_skill.content = content_text; - config->default_skill.tags_json = tags_json; - return 0; + int rc = append_startup_event(config, kind_i, content_text, tags_json); + free(content_text); + free(tags_json); + return rc; } static int parse_startup_events(cJSON* root, didactyl_config_t* config) { @@ -1094,42 +1095,48 @@ static int append_startup_event(didactyl_config_t* config, int kind, const char* return 0; } -int config_ensure_default_skill_startup_events(didactyl_config_t* config) { - if (!config || !config->default_skill.content || config->default_skill.content[0] == '\0' || - config->default_skill.d_tag[0] == '\0') { +int config_ensure_startup_skill_adoption(didactyl_config_t* config) { + if (!config || config->keys.public_key_hex[0] == '\0') { return 0; } - int has_default_skill_event = 0; + cJSON* needed_addrs = cJSON_CreateArray(); + if (!needed_addrs) { + return -1; + } + for (int i = 0; i < config->startup_event_count; i++) { startup_event_t* se = &config->startup_events[i]; - if (se->kind == config->default_skill.kind && startup_event_has_d_tag(se, config->default_skill.d_tag)) { - has_default_skill_event = 1; - break; + if (!se || (se->kind != 31123 && se->kind != 31124) || !se->tags_json) { + continue; } + + cJSON* tags = cJSON_Parse(se->tags_json); + if (!tags || !cJSON_IsArray(tags)) { + cJSON_Delete(tags); + continue; + } + + cJSON* d_val = find_tag_value_string(tags, "d"); + if (d_val && cJSON_IsString(d_val) && d_val->valuestring && d_val->valuestring[0] != '\0') { + char addr[256]; + snprintf(addr, + sizeof(addr), + "%d:%s:%s", + se->kind, + config->keys.public_key_hex, + d_val->valuestring); + cJSON_AddItemToArray(needed_addrs, cJSON_CreateString(addr)); + } + + cJSON_Delete(tags); } - if (!has_default_skill_event) { - if (append_startup_event(config, - config->default_skill.kind, - config->default_skill.content, - config->default_skill.tags_json ? config->default_skill.tags_json : "[]") != 0) { - return -1; - } - } - - if (config->keys.public_key_hex[0] == '\0') { + if (cJSON_GetArraySize(needed_addrs) == 0) { + cJSON_Delete(needed_addrs); return 0; } - char addr[256]; - snprintf(addr, - sizeof(addr), - "%d:%s:%s", - config->default_skill.kind, - config->keys.public_key_hex, - config->default_skill.d_tag); - for (int i = 0; i < config->startup_event_count; i++) { startup_event_t* se = &config->startup_events[i]; if (se->kind != 10123 || !se->tags_json) { @@ -1142,35 +1149,49 @@ int config_ensure_default_skill_startup_events(didactyl_config_t* config) { continue; } - if (adoption_tags_has_addr(tags, addr)) { - cJSON_Delete(tags); - return 0; - } - - if (append_adoption_addr_tag(tags, addr) != 0) { - cJSON_Delete(tags); - return -1; + int needed_n = cJSON_GetArraySize(needed_addrs); + for (int j = 0; j < needed_n; j++) { + cJSON* addr_j = cJSON_GetArrayItem(needed_addrs, j); + if (!addr_j || !cJSON_IsString(addr_j) || !addr_j->valuestring || addr_j->valuestring[0] == '\0') { + continue; + } + if (!adoption_tags_has_addr(tags, addr_j->valuestring) && append_adoption_addr_tag(tags, addr_j->valuestring) != 0) { + cJSON_Delete(tags); + cJSON_Delete(needed_addrs); + return -1; + } } char* updated = cJSON_PrintUnformatted(tags); cJSON_Delete(tags); if (!updated) { + cJSON_Delete(needed_addrs); return -1; } free(se->tags_json); se->tags_json = updated; + cJSON_Delete(needed_addrs); return 0; } cJSON* tags = cJSON_CreateArray(); if (!tags) { + cJSON_Delete(needed_addrs); return -1; } - if (append_adoption_addr_tag(tags, addr) != 0) { - cJSON_Delete(tags); - return -1; + int needed_n = cJSON_GetArraySize(needed_addrs); + for (int j = 0; j < needed_n; j++) { + cJSON* addr_j = cJSON_GetArrayItem(needed_addrs, j); + if (!addr_j || !cJSON_IsString(addr_j) || !addr_j->valuestring || addr_j->valuestring[0] == '\0') { + continue; + } + if (append_adoption_addr_tag(tags, addr_j->valuestring) != 0) { + cJSON_Delete(tags); + cJSON_Delete(needed_addrs); + return -1; + } } cJSON* app = cJSON_CreateArray(); @@ -1179,17 +1200,19 @@ int config_ensure_default_skill_startup_events(didactyl_config_t* config) { cJSON_Delete(app); cJSON_Delete(scope); cJSON_Delete(tags); + cJSON_Delete(needed_addrs); return -1; } cJSON_AddItemToArray(app, cJSON_CreateString("app")); cJSON_AddItemToArray(app, cJSON_CreateString("didactyl")); cJSON_AddItemToArray(scope, cJSON_CreateString("scope")); - cJSON_AddItemToArray(scope, cJSON_CreateString(config->default_skill.kind == 31124 ? "private" : "public")); + cJSON_AddItemToArray(scope, cJSON_CreateString("private")); cJSON_AddItemToArray(tags, app); cJSON_AddItemToArray(tags, scope); char* tags_json = cJSON_PrintUnformatted(tags); cJSON_Delete(tags); + cJSON_Delete(needed_addrs); if (!tags_json) { return -1; } @@ -1219,9 +1242,6 @@ void config_free(didactyl_config_t* config) { free(config->startup_events); } - free(config->default_skill.content); - free(config->default_skill.tags_json); - free_cashu_wallet_mints(&config->cashu_wallet); memset(config, 0, sizeof(*config)); @@ -1280,11 +1300,6 @@ int config_load(const char* path, didactyl_config_t* config) { config->api.port = 8484; snprintf(config->api.bind_address, sizeof(config->api.bind_address), "%s", "127.0.0.1"); - config->default_skill.kind = 31124; - config->default_skill.d_tag[0] = '\0'; - config->default_skill.content = NULL; - config->default_skill.tags_json = NULL; - config->cashu_wallet.enabled = 0; config->cashu_wallet.mint_urls = NULL; config->cashu_wallet.mint_count = 0; @@ -1366,8 +1381,8 @@ int config_load(const char* path, didactyl_config_t* config) { goto cleanup; } - if (parse_default_skill_config(root, config) != 0) { - config_set_error("invalid default_skill configuration"); + if (parse_legacy_default_skill_to_startup_events(root, config) != 0) { + config_set_error("invalid legacy default_skill configuration"); goto cleanup; } diff --git a/src/config.h b/src/config.h index 90d9e38..bac4182 100644 --- a/src/config.h +++ b/src/config.h @@ -64,13 +64,6 @@ typedef struct { char* tags_json; // JSON array string for tags, optional } startup_event_t; -typedef struct { - int kind; - char d_tag[65]; - char* content; - char* tags_json; -} default_skill_config_t; - typedef struct { int enabled; int tools_enabled; @@ -131,12 +124,11 @@ typedef struct { cashu_wallet_config_t cashu_wallet; startup_event_t* startup_events; int startup_event_count; - default_skill_config_t default_skill; char config_path[OW_MAX_URL_LEN]; } didactyl_config_t; int config_load(const char* path, didactyl_config_t* config); -int config_ensure_default_skill_startup_events(didactyl_config_t* config); +int config_ensure_startup_skill_adoption(didactyl_config_t* config); const char* config_last_error(void); void config_free(didactyl_config_t* config); diff --git a/src/default_events.h b/src/default_events.h index 407afa6..0a55199 100644 --- a/src/default_events.h +++ b/src/default_events.h @@ -1,9 +1,13 @@ #ifndef DIDACTYL_DEFAULT_EVENTS_H #define DIDACTYL_DEFAULT_EVENTS_H -#define DIDACTYL_DEFAULT_SKILL_D_TAG "default_admin_dm" +#define DIDACTYL_DEFAULT_SKILL_D_TAG "identity_and_rules" #define DIDACTYL_DEFAULT_SKILL_KIND 31124 -#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_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"identity_and_rules\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Agent identity and behavioral rules\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]" + +#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_D_TAG "dm_history" +#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND 31124 +#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON "[[\"d\",\"dm_history\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"DM conversation history for context continuity\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]" #define DIDACTYL_STARTUP_KIND_PROFILE 0 #define DIDACTYL_STARTUP_KIND_CONTACTS 3 @@ -46,4 +50,8 @@ static const char* DIDACTYL_DEFAULT_SKILL_TEMPLATE = "- Never reveal your private key (nsec)\n" "- You may share your public key (npub) with anyone"; +static const char* DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE = + "## Recent Conversation\n\n" + "{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}"; + #endif \ No newline at end of file diff --git a/src/main.c b/src/main.c index 71e6139..dc91cf7 100644 --- a/src/main.c +++ b/src/main.c @@ -1057,8 +1057,8 @@ int main(int argc, char** argv) { DEBUG_INFO("[didactyl] startup phase: admin override applied from --admin (%.16s...)", cfg.admin.pubkey); } - if (config_ensure_default_skill_startup_events(&cfg) != 0) { - fprintf(stderr, "Failed to synthesize startup events from default_skill\n"); + if (config_ensure_startup_skill_adoption(&cfg) != 0) { + fprintf(stderr, "Failed to synthesize startup skill adoption events\n"); config_free(&cfg); nostr_cleanup(); return 1; @@ -1248,7 +1248,14 @@ int main(int argc, char** argv) { startup_step_begin(7, "Reconcile/persist startup state"); if (bootstrap_mode || first_run) { if (nostr_handler_reconcile_startup_events() != 0) { - DEBUG_WARN("[didactyl] startup phase: reconcile startup events failed (continuing)"); + startup_step_fail(7, "Reconcile/persist startup state", "startup event reconciliation failed"); + fprintf(stderr, "Failed to publish startup events; aborting startup\n"); + nostr_block_list_cleanup(); + nostr_handler_cleanup(); + llm_cleanup(); + config_free(&cfg); + nostr_cleanup(); + return 1; } } else { DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run"); diff --git a/src/main.h b/src/main.h index 142ea88..b6f9989 100644 --- a/src/main.h +++ b/src/main.h @@ -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 12 -#define DIDACTYL_VERSION "v0.2.12" +#define DIDACTYL_VERSION_PATCH 13 +#define DIDACTYL_VERSION "v0.2.13" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/nostr_handler.c b/src/nostr_handler.c index 02d4920..6abbb53 100644 --- a/src/nostr_handler.c +++ b/src/nostr_handler.c @@ -217,7 +217,7 @@ typedef struct { typedef struct { time_t created_at; - int incoming; + didactyl_dm_history_role_t role; char peer_pubkey_hex[65]; char* content; } dm_history_entry_t; @@ -350,14 +350,17 @@ static void dm_history_clear_locked(void) { free(g_dm_history_ring[i].content); g_dm_history_ring[i].content = NULL; g_dm_history_ring[i].created_at = 0; - g_dm_history_ring[i].incoming = 0; + g_dm_history_ring[i].role = DIDACTYL_DM_HISTORY_ASSISTANT; g_dm_history_ring[i].peer_pubkey_hex[0] = '\0'; } g_dm_history_count = 0; g_dm_history_next = 0; } -static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at) { +static void dm_history_remember(const char* peer_pubkey_hex, + const char* content, + didactyl_dm_history_role_t role, + time_t created_at) { if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U || !content) { return; } @@ -381,13 +384,24 @@ static void dm_history_remember(const char* peer_pubkey_hex, const char* content free(g_dm_history_ring[slot].content); g_dm_history_ring[slot].content = dup; g_dm_history_ring[slot].created_at = (created_at > 0) ? created_at : time(NULL); - g_dm_history_ring[slot].incoming = incoming ? 1 : 0; + g_dm_history_ring[slot].role = role; memcpy(g_dm_history_ring[slot].peer_pubkey_hex, peer_pubkey_hex, 64U); g_dm_history_ring[slot].peer_pubkey_hex[64] = '\0'; pthread_mutex_unlock(&g_dm_history_mutex); } +int nostr_handler_dm_history_remember(const char* peer_pubkey_hex, + const char* content, + didactyl_dm_history_role_t role) { + if (!peer_pubkey_hex || !content) { + return -1; + } + + dm_history_remember(peer_pubkey_hex, content, role, time(NULL)); + return 0; +} + static const char* relay_status_str(nostr_pool_relay_status_t status) { switch (status) { case NOSTR_POOL_RELAY_DISCONNECTED: @@ -427,7 +441,10 @@ static void register_trigger_from_self_skill_event(cJSON* event); static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data); static void on_wallet_event(cJSON* event, const char* relay_url, void* user_data); static void dm_history_clear_locked(void); -static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at); +static void dm_history_remember(const char* peer_pubkey_hex, + const char* content, + didactyl_dm_history_role_t role, + time_t created_at); static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url); static void free_relay_list_local(char** relays, int relay_count); static int extract_relays_from_kind10002_tags(cJSON* tags, char*** out_relays, int* out_relay_count); @@ -1691,11 +1708,6 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) { (int)tier, received_protocol == DM_PROTOCOL_NIP17 ? "nip17" : "nip04"); - dm_history_remember(sender_pubkey_hex, - decrypted, - 1, - time(NULL)); - g_dm_callback(sender_pubkey_hex, decrypted, tier, g_dm_user_data); free(decrypted); } @@ -2761,7 +2773,9 @@ int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adopt return skill_count > 0 ? 0 : -1; } -int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) { +int nostr_handler_send_dm_with_role(const char* recipient_pubkey_hex, + const char* message, + didactyl_dm_history_role_t role) { if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) { return -1; } @@ -2843,7 +2857,7 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) if (sent > 0) { dm_history_remember(recipient_pubkey_hex, message, - 0, + role, time(NULL)); } @@ -2852,26 +2866,34 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) return sent > 0 ? 0 : -1; } -int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message) { +int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) { + return nostr_handler_send_dm_with_role(recipient_pubkey_hex, + message, + DIDACTYL_DM_HISTORY_ASSISTANT); +} + +int nostr_handler_send_dm_auto_with_role(const char* recipient_pubkey_hex, + const char* message, + didactyl_dm_history_role_t role) { if (!recipient_pubkey_hex || !message) { return -1; } if (!g_cfg) { - return nostr_handler_send_dm(recipient_pubkey_hex, message); + return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role); } if (g_cfg->dm_protocol == DM_PROTOCOL_NIP04) { - return nostr_handler_send_dm(recipient_pubkey_hex, message); + return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role); } if (g_cfg->dm_protocol == DM_PROTOCOL_NIP17) { - return nostr_handler_send_dm_nip17(recipient_pubkey_hex, message, NULL); + return nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, message, NULL, role); } dm_protocol_t target = sender_protocol_lookup(recipient_pubkey_hex); if (target == DM_PROTOCOL_NIP17) { - int rc17 = nostr_handler_send_dm_nip17(recipient_pubkey_hex, message, NULL); + int rc17 = nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, message, NULL, role); if (rc17 == 0) { return 0; } @@ -2879,7 +2901,13 @@ int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* mes recipient_pubkey_hex); } - return nostr_handler_send_dm(recipient_pubkey_hex, message); + return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role); +} + +int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message) { + return nostr_handler_send_dm_auto_with_role(recipient_pubkey_hex, + message, + DIDACTYL_DM_HISTORY_ASSISTANT); } static int publish_kind_event_to_relays(int kind, @@ -3220,6 +3248,7 @@ int nostr_handler_reconcile_startup_events(void) { if (g_startup_publish_tracking_enabled && g_startup_published) { int events_published = 0; int relays_used = 0; + int missing_any_event = 0; for (int r = 0; r < g_cfg->relay_count; r++) { int used = 0; @@ -3257,13 +3286,24 @@ int nostr_handler_reconcile_startup_events(void) { se->kind, startup_kind_label(se->kind), relays_line[0] ? relays_line : ""); + } else { + missing_any_event = 1; + DEBUG_ERROR("[didactyl] startup publish missing: kind=%d (%s) was not published to any relay", + se->kind, + startup_kind_label(se->kind)); } } - DEBUG_INFO("[didactyl] startup publish summary: %d event(s) published to %d/%d relay(s)", + DEBUG_INFO("[didactyl] startup publish summary: %d/%d event(s) published to at least one relay; relays used=%d/%d", events_published, + g_cfg->startup_event_count, relays_used, g_cfg->relay_count); + + if (missing_any_event) { + DEBUG_ERROR("[didactyl] startup reconcile failed: one or more startup events were not published to any relay"); + return -1; + } } return 0; @@ -3682,7 +3722,10 @@ int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int ena return rc; } -int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) { +int nostr_handler_send_dm_nip17_with_role(const char* recipient_pubkey_hex, + const char* message, + const char* subject, + didactyl_dm_history_role_t role) { if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message || recipient_pubkey_hex[0] == '\0' || message[0] == '\0') { return -1; } @@ -3772,12 +3815,19 @@ int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* me dm_history_remember(recipient_pubkey_hex, message, - 0, + role, time(NULL)); return 0; } +int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) { + return nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, + message, + subject, + DIDACTYL_DM_HISTORY_ASSISTANT); +} + char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit) { if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U) { return strdup("[]"); @@ -3831,7 +3881,15 @@ char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit) if (!item) { continue; } - cJSON_AddStringToObject(item, "role", entry->incoming ? "user" : "assistant"); + const char* role_s = "assistant"; + if (entry->role == DIDACTYL_DM_HISTORY_USER) { + role_s = "user"; + } else if (entry->role == DIDACTYL_DM_HISTORY_TOOL_REQUEST) { + role_s = "tool_request"; + } else if (entry->role == DIDACTYL_DM_HISTORY_TOOL_RESPONSE) { + role_s = "tool_response"; + } + cJSON_AddStringToObject(item, "role", role_s); cJSON_AddStringToObject(item, "content", entry->content); cJSON_AddNumberToObject(item, "created_at", (double)entry->created_at); cJSON_AddItemToArray(arr, item); diff --git a/src/nostr_handler.h b/src/nostr_handler.h index f2ee8f9..25faa2a 100644 --- a/src/nostr_handler.h +++ b/src/nostr_handler.h @@ -13,6 +13,13 @@ typedef enum { DIDACTYL_SENDER_STRANGER = 3 } didactyl_sender_tier_t; +typedef enum { + DIDACTYL_DM_HISTORY_USER = 1, + DIDACTYL_DM_HISTORY_ASSISTANT = 2, + DIDACTYL_DM_HISTORY_TOOL_REQUEST = 3, + DIDACTYL_DM_HISTORY_TOOL_RESPONSE = 4 +} didactyl_dm_history_role_t; + typedef void (*dm_callback_t)(const char* sender_pubkey_hex, const char* message, didactyl_sender_tier_t tier, @@ -42,7 +49,13 @@ void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callb int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data); int nostr_handler_subscribe_wallet_events(void); int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message); +int nostr_handler_send_dm_with_role(const char* recipient_pubkey_hex, + const char* message, + didactyl_dm_history_role_t role); int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message); +int nostr_handler_send_dm_auto_with_role(const char* recipient_pubkey_hex, + const char* message, + didactyl_dm_history_role_t role); int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags, nostr_publish_result_t* out_result); void nostr_handler_publish_result_free(nostr_publish_result_t* result); char* nostr_handler_query_json(cJSON* filter, int timeout_ms); @@ -81,6 +94,13 @@ char* nostr_handler_relay_info_json(const char* relay_url); char* nostr_handler_subscription_status_json(void); int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int enabled, int enabled_set); int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject); +int nostr_handler_send_dm_nip17_with_role(const char* recipient_pubkey_hex, + const char* message, + const char* subject, + didactyl_dm_history_role_t role); +int nostr_handler_dm_history_remember(const char* peer_pubkey_hex, + const char* content, + didactyl_dm_history_role_t role); char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit); void nostr_handler_cleanup(void); diff --git a/src/setup_wizard.c b/src/setup_wizard.c index 30b50ba..014853e 100644 --- a/src/setup_wizard.c +++ b/src/setup_wizard.c @@ -372,17 +372,64 @@ static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_ta return 0; } +static int tags_json_get_d_tag_local(const char* tags_json, char* out, size_t out_size) { + if (!tags_json || !out || out_size == 0) { + return -1; + } + + out[0] = '\0'; + cJSON* tags = cJSON_Parse(tags_json); + if (!tags || !cJSON_IsArray(tags)) { + cJSON_Delete(tags); + return -1; + } + + int n = cJSON_GetArraySize(tags); + for (int i = 0; i < 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, "d") == 0 && v->valuestring[0] != '\0') { + snprintf(out, out_size, "%s", v->valuestring); + cJSON_Delete(tags); + return 0; + } + } + + cJSON_Delete(tags); + return -1; +} + static int upsert_startup_event(didactyl_config_t* cfg, int kind, const char* content, const char* tags_json) { if (!cfg || !content || !tags_json) { return -1; } + int is_skill_kind = (kind == 31123 || kind == 31124); + char incoming_d[128] = {0}; + int incoming_has_d = (is_skill_kind && tags_json_get_d_tag_local(tags_json, incoming_d, sizeof(incoming_d)) == 0); + for (int i = 0; i < cfg->startup_event_count; i++) { startup_event_t* se = &cfg->startup_events[i]; if (se->kind != kind) { continue; } + if (is_skill_kind) { + if (!incoming_has_d) { + continue; + } + char existing_d[128] = {0}; + if (tags_json_get_d_tag_local(se->tags_json, existing_d, sizeof(existing_d)) != 0) { + continue; + } + if (strcmp(existing_d, incoming_d) != 0) { + continue; + } + } + char* content_dup = strdup(content); char* tags_dup = strdup(tags_json); if (!content_dup || !tags_dup) { @@ -495,35 +542,22 @@ static int apply_default_startup_events(didactyl_config_t* cfg, const char* agen return 0; } -static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char* agent_name) { +static int configure_default_skills_for_agent(didactyl_config_t* cfg, const char* agent_name) { if (!cfg || !agent_name || agent_name[0] == '\0') { return -1; } - free(cfg->default_skill.content); - cfg->default_skill.content = NULL; - free(cfg->default_skill.tags_json); - cfg->default_skill.tags_json = NULL; - - 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, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE); - if (needed <= 0) { + if (upsert_startup_event(cfg, + DIDACTYL_DEFAULT_SKILL_KIND, + DIDACTYL_DEFAULT_SKILL_TEMPLATE, + DIDACTYL_DEFAULT_SKILL_TAGS_JSON) != 0) { return -1; } - cfg->default_skill.content = (char*)malloc((size_t)needed + 1U); - if (!cfg->default_skill.content) { - return -1; - } - - 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) { - free(cfg->default_skill.content); - cfg->default_skill.content = NULL; + if (upsert_startup_event(cfg, + DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND, + DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE, + DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON) != 0) { return -1; } @@ -2157,8 +2191,8 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t if (prompt_llm_config(cfg) != 0) return -1; } - if (configure_default_skill_for_agent(cfg, agent_name) != 0) { - fprintf(stderr, "%sFailed to prepare default skill event content.%s\n", ANSI_RED, ANSI_RESET); + if (configure_default_skills_for_agent(cfg, agent_name) != 0) { + fprintf(stderr, "%sFailed to prepare default startup skill events.%s\n", ANSI_RED, ANSI_RESET); return -1; } diff --git a/src/tools/tool_agent.c b/src/tools/tool_agent.c index e9a05e1..fa1b6a6 100644 --- a/src/tools/tool_agent.c +++ b/src/tools/tool_agent.c @@ -31,6 +31,25 @@ static cJSON* parse_args_local(const char* args_json) { return args; } +static int append_text_local(char** buf, size_t* cap, size_t* used, const char* s) { + if (!buf || !cap || !used || !s) return -1; + size_t n = strlen(s); + if (*used + n + 1U > *cap) { + size_t next = *cap; + while (*used + n + 1U > next) { + next = (next == 0U) ? 256U : (next * 2U); + } + char* grown = (char*)realloc(*buf, next); + if (!grown) return -1; + *buf = grown; + *cap = next; + } + memcpy(*buf + *used, s, n); + *used += n; + (*buf)[*used] = '\0'; + return 0; +} + char* execute_message_current(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); @@ -282,6 +301,14 @@ char* execute_nostr_dm_history(tools_context_t* ctx, const char* args_json) { include_current = 1; } + int format_text = 0; + cJSON* format_j = cJSON_GetObjectItemCaseSensitive(args, "format"); + if (format_j && cJSON_IsString(format_j) && format_j->valuestring) { + if (strcmp(format_j->valuestring, "text") == 0) { + format_text = 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') { @@ -361,20 +388,59 @@ char* execute_nostr_dm_history(tools_context_t* ctx, const char* args_json) { cJSON_Delete(parsed); - char* filtered_json = cJSON_PrintUnformatted(filtered); - cJSON_Delete(filtered); - if (!filtered_json) { - return json_error_local("failed to serialize dm history"); + char* rendered = NULL; + + if (format_text) { + size_t cap = 256U; + size_t used = 0U; + rendered = (char*)malloc(cap); + if (!rendered) { + cJSON_Delete(filtered); + return json_error_local("failed to serialize dm history"); + } + rendered[0] = '\0'; + + int m = cJSON_GetArraySize(filtered); + for (int i = 0; i < m; i++) { + cJSON* item = cJSON_GetArrayItem(filtered, i); + cJSON* role = item ? cJSON_GetObjectItemCaseSensitive(item, "role") : NULL; + cJSON* content = item ? cJSON_GetObjectItemCaseSensitive(item, "content") : NULL; + if (!role || !content || !cJSON_IsString(role) || !cJSON_IsString(content) || + !role->valuestring || !content->valuestring) { + continue; + } + + const char* prefix = (strcmp(role->valuestring, "user") == 0) ? "User: " : "Assistant: "; + if (append_text_local(&rendered, &cap, &used, prefix) != 0 || + append_text_local(&rendered, &cap, &used, content->valuestring) != 0 || + append_text_local(&rendered, &cap, &used, "\n") != 0) { + free(rendered); + cJSON_Delete(filtered); + return json_error_local("failed to serialize dm history"); + } + } + + if (used > 0U && rendered[used - 1] == '\n') { + rendered[used - 1] = '\0'; + } + } else { + rendered = cJSON_PrintUnformatted(filtered); + if (!rendered) { + cJSON_Delete(filtered); + return json_error_local("failed to serialize dm history"); + } } + cJSON_Delete(filtered); + cJSON* out = cJSON_CreateObject(); if (!out) { - free(filtered_json); + free(rendered); return NULL; } cJSON_AddBoolToObject(out, "success", 1); - cJSON_AddStringToObject(out, "content", filtered_json); - free(filtered_json); + cJSON_AddStringToObject(out, "content", rendered); + free(rendered); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); diff --git a/src/tools/tools_schema.c b/src/tools/tools_schema.c index 433d7ea..b63cda7 100644 --- a/src/tools/tools_schema.c +++ b/src/tools/tools_schema.c @@ -851,7 +851,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { 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_fn, "description", "Get full skill JSON by d tag from local cache; JSON: {\"d\":\"identity_and_rules\"} (slash shorthand also works: /skill_get identity_and_rules)"); cJSON_AddStringToObject(t24a_params, "type", "object"); cJSON_AddItemToObject(t24a_params, "properties", t24a_props); cJSON_AddItemToObject(t24a_params, "required", t24a_required); @@ -880,7 +880,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { 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_fn, "description", "Compatibility alias for skill_get; JSON: {\"d\":\"identity_and_rules\"}"); cJSON_AddStringToObject(t24b_params, "type", "object"); cJSON_AddItemToObject(t24b_params, "properties", t24b_props); cJSON_AddItemToObject(t24b_params, "required", t24b_required); @@ -908,7 +908,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { 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\"]]}}"); + "Set/publish a full skill JSON payload and refresh runtime cache; JSON: {\"skill\":{\"d\":\"identity_and_rules\",\"kind\":31124,\"scope\":\"private\",\"content\":\"...\",\"tags\":[[\"d\",\"identity_and_rules\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"]]}}"); cJSON_AddStringToObject(t24c_params, "type", "object"); cJSON_AddItemToObject(t24c_params, "properties", t24c_props); @@ -1487,7 +1487,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { 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\":\"\",\"limit\":20}; template usage: {{nostr_dm_history({\"limit\":12})}}"); + "Fetch recent DM history for template/context usage; JSON: {\"limit\":12,\"include_current\":false,\"format\":\"json\"} or with explicit peer {\"peer_pubkey\":\"\",\"limit\":20,\"format\":\"text\"}; template usage: {{nostr_dm_history({\"limit\":12,\"format\":\"text\"})}}"); cJSON_AddStringToObject(t43_params, "type", "object"); cJSON_AddItemToObject(t43_params, "properties", t43_props); @@ -1506,6 +1506,13 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { 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* p_dm_history_format = cJSON_CreateObject(); + cJSON_AddStringToObject(p_dm_history_format, "type", "string"); + cJSON_AddStringToObject(p_dm_history_format, + "description", + "Output format: 'json' (default) or 'text' (User:/Assistant: lines)"); + cJSON_AddItemToObject(t43_props, "format", p_dm_history_format); + cJSON_AddItemToObject(t43_fn, "parameters", t43_params); cJSON_AddItemToObject(t43, "function", t43_fn); cJSON_AddItemToArray(tools, t43);