From c4ee5a0aa041f0e23b73b7f4d59df8e5c6afc625 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 22 Mar 2026 19:10:33 -0400 Subject: [PATCH] v0.2.8 - Clean DM context duplication, suppress oversized websocket frame traces, and return raw content/tags in skill_get --- README.md | 4 +- context.log.md | 257 +--------------------- src/agent.c | 375 +++++++------------------------- src/main.h | 4 +- src/nostr_handler.c | 21 +- src/prompt_template.c | 63 +++++- src/tools/tool_admin.c | 82 ++----- src/tools/tool_agent.c | 199 ++++++++++++----- src/tools/tool_nostr_identity.c | 2 + src/tools/tool_skill.c | 309 ++++++++++++++++++++++++++ src/tools/tools_dispatch.c | 12 + src/tools/tools_internal.h | 4 + src/tools/tools_schema.c | 115 ++++++++++ 13 files changed, 767 insertions(+), 680 deletions(-) diff --git a/README.md b/README.md index 6f128fe..37b39d2 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.7 +## Current Status — v0.2.8 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.2.7 — Fix default_admin_dm inline variable resolution for my_kind0_profile and my_npub +> Last release update: v0.2.8 — Clean DM context duplication, suppress oversized websocket frame traces, and return raw content/tags in skill_get - 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 5d22fba..7f1262b 100644 --- a/context.log.md +++ b/context.log.md @@ -1,261 +1,14 @@ ```text Context Log - not seen by model -timestamp=2026-03-22 12:47:55 -phase=direct_tool_exec -sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 -model=claude-opus-4.6 -context_bytes=1450 -approx_tokens=362 -``` - -slash=/my_kind0_profile -result_json={"success":true,"content":"## Agent Kind 0 Profile (source: nostr kind 0)\n\nAgent kind 0 profile content (JSON): {\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}"} -result_markdown=## Agent Kind 0 Profile (source: nostr kind 0) - -Agent kind 0 profile content (JSON): { - "name": "Didactyl Agent", - "display_name": "Didactyl Agent", - "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.", - "picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png", - "banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg" -} - ---- - -```text -Context Log - not seen by model -timestamp=2026-03-22 12:47:41 -phase=direct_tool_exec -sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 -model=claude-opus-4.6 -context_bytes=13723 -approx_tokens=3430 -``` - -slash=/help -result_json=AVAILABLE TOOLS -- admin_identity — Build admin identity context block from cached runtime metadata -- agent_identity — Build agent identity context block with pubkey and npub -- agent_version — Return current Didactyl version and metadata from build macros -- blossom_delete — Delete a blob from Blossom -- blossom_download — Download a blob from Blossom to a local file -- blossom_head — Fetch blob metadata from Blossom -- blossom_list — List blobs for a pubkey on Blossom -- blossom_upload — Upload a local file to a Blossom server -- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit -- cashu_wallet_check_proofs — Check current wallet proofs state against mint -- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint -- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id -- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request -- cashu_wallet_mint_check — Check a previously requested mint quote status -- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state -- cashu_wallet_mint_quote — Request a mint quote for a target amount -- cashu_wallet_mints_get — Get currently configured wallet and public mints -- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both -- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state -- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount -- config_recall — Fetch and decrypt agent config kind 30078 by d_tag -- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag -- local_file_read — Read a local file as text from the configured working directory -- local_file_write — Write text content to a local file in the configured working directory -- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body -- local_shell_exec — Execute a shell command and return stdout/stderr -- memory_recall — Recall encrypted agent memory (kind 30078, d=memory) -- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed -- model_get — Get current active LLM runtime configuration (excluding API key) -- model_list — List available model IDs using provider OpenAI-compatible /models endpoint -- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config) -- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context -- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context -- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context -- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32 -- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format -- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context -- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list -- nostr_admin_notes — Build admin notes context block from cached kind 1 notes -- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata -- nostr_admin_relays — Build admin relay context block from cached kind 10002 data -- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list -- nostr_agent_notes — Build agent notes context block from cached kind 1 notes -- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata -- nostr_agent_relays — Build agent relay context block from cached kind 10002 data -- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default -- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache -- nostr_decode — Decode a Nostr bech32/nostr: URI into components -- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender -- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5) -- nostr_dm_send — Send a NIP-04 encrypted DM -- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol -- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr) -- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient -- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename -- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style) -- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence -- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain) -- nostr_npub — Return this agent's pubkey encoded as npub bech32 -- nostr_post — Publish a Nostr event to connected relays -- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md -- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey -- nostr_pubkey — Return this agent's pubkey in hex format -- nostr_query — Query events from relays using a Nostr filter -- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7) -- nostr_relay_info — Fetch NIP-11 relay information document -- nostr_relay_status — Get connection status and statistics for all relays -- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter) -- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters -- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list -- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it -- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124 -- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters -- skill_remove — Remove a skill address from kind 10123 adoption list -- skill_search — Search public skills by query/author and optionally rank by adoption popularity -- task_list — Build current task list context block from agent task memory on Nostr -- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace -- tool_list — List available tools with name, description, and JSON parameter schema -- trigger_list — List active triggered skills and their runtime status - -AVAILABLE SKILLS -- (none) - -ADOPTED SKILLS -- default_admin_dm - -result_markdown=AVAILABLE TOOLS -- admin_identity — Build admin identity context block from cached runtime metadata -- agent_identity — Build agent identity context block with pubkey and npub -- agent_version — Return current Didactyl version and metadata from build macros -- blossom_delete — Delete a blob from Blossom -- blossom_download — Download a blob from Blossom to a local file -- blossom_head — Fetch blob metadata from Blossom -- blossom_list — List blobs for a pubkey on Blossom -- blossom_upload — Upload a local file to a Blossom server -- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit -- cashu_wallet_check_proofs — Check current wallet proofs state against mint -- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint -- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id -- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request -- cashu_wallet_mint_check — Check a previously requested mint quote status -- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state -- cashu_wallet_mint_quote — Request a mint quote for a target amount -- cashu_wallet_mints_get — Get currently configured wallet and public mints -- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both -- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state -- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount -- config_recall — Fetch and decrypt agent config kind 30078 by d_tag -- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag -- local_file_read — Read a local file as text from the configured working directory -- local_file_write — Write text content to a local file in the configured working directory -- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body -- local_shell_exec — Execute a shell command and return stdout/stderr -- memory_recall — Recall encrypted agent memory (kind 30078, d=memory) -- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed -- model_get — Get current active LLM runtime configuration (excluding API key) -- model_list — List available model IDs using provider OpenAI-compatible /models endpoint -- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config) -- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context -- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context -- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context -- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32 -- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format -- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context -- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list -- nostr_admin_notes — Build admin notes context block from cached kind 1 notes -- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata -- nostr_admin_relays — Build admin relay context block from cached kind 10002 data -- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list -- nostr_agent_notes — Build agent notes context block from cached kind 1 notes -- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata -- nostr_agent_relays — Build agent relay context block from cached kind 10002 data -- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default -- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache -- nostr_decode — Decode a Nostr bech32/nostr: URI into components -- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender -- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5) -- nostr_dm_send — Send a NIP-04 encrypted DM -- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol -- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr) -- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient -- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename -- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style) -- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence -- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain) -- nostr_npub — Return this agent's pubkey encoded as npub bech32 -- nostr_post — Publish a Nostr event to connected relays -- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md -- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey -- nostr_pubkey — Return this agent's pubkey in hex format -- nostr_query — Query events from relays using a Nostr filter -- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7) -- nostr_relay_info — Fetch NIP-11 relay information document -- nostr_relay_status — Get connection status and statistics for all relays -- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter) -- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters -- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list -- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it -- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124 -- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters -- skill_remove — Remove a skill address from kind 10123 adoption list -- skill_search — Search public skills by query/author and optionally rank by adoption popularity -- task_list — Build current task list context block from agent task memory on Nostr -- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace -- tool_list — List available tools with name, description, and JSON parameter schema -- trigger_list — List active triggered skills and their runtime status - -AVAILABLE SKILLS -- (none) - -ADOPTED SKILLS -- default_admin_dm - - ---- - -```text -Context Log - not seen by model -timestamp=2026-03-22 12:43:34 +timestamp=2026-03-22 17:56:17 phase=llm_chat_with_tools_messages sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 model=claude-opus-4.6 -context_bytes=2098 -approx_tokens=524 +context_bytes=1318 +approx_tokens=329 ``` -Sections: 7 - -# system_prompt | role=system - -You are - -Your npub: - -### Rules - -- Communicate through encrypted Nostr direct messages -- Keep responses concise and clear -- Be helpful and technically accurate -- If unsure, state uncertainty directly -- Use tools when a request requires taking action -- After a tool call, base your answer on the actual tool result -- Never claim a tool was run if no tool was executed -- Maintain your task list as short-term working memory -- Never reveal your private key (nsec) -- You may share your public key (npub) with anyone - -Incoming DM message: -What is your name? - -# context_part | role=tool - -{"success":true,"content":"## Agent Kind 0 Profile (source: nostr kind 0)\n\nAgent kind 0 profile content (JSON): {\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}"} - -# dm_history | role=chat - -12:42 Agent Didactyl has started up and is online at 2026-03-22 12:42:55 (version v0.2.6, connected relays: 4/4). -12:43 Administrator hello -12:43 Agent Hello! 👋 I'm online and ready to help. What can I do for you today? -12:43 Administrator What is your name? ---:-- Agent - -Let me check my profile to answer that accurately. +[{"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\n\nIncoming DM message:\nhello"},{"role":"user","content":"hello","_ts":1774216577}] +--- diff --git a/src/agent.c b/src/agent.c index 58d4dfe..e1d7d3d 100644 --- a/src/agent.c +++ b/src/agent.c @@ -480,6 +480,11 @@ static char* build_slash_help_all_json(void) { return NULL; } + if (append_textf_local(&out, &cap, &used, "(run / -h for full JSON args template)\n") != 0) { + free(out); + return NULL; + } + char* tool_list_json = tools_execute(&g_tools_ctx, "tool_list", "{}"); cJSON* tool_root = tool_list_json ? cJSON_Parse(tool_list_json) : NULL; cJSON* tools = tool_root ? cJSON_GetObjectItemCaseSensitive(tool_root, "tools") : NULL; @@ -644,6 +649,54 @@ static char* build_slash_help_all_json(void) { return out; } +static cJSON* build_example_value_for_type_local(const char* type_s) { + if (!type_s || type_s[0] == '\0') return cJSON_CreateString(""); + if (strcmp(type_s, "string") == 0) return cJSON_CreateString(""); + if (strcmp(type_s, "integer") == 0) return cJSON_CreateNumber(0); + if (strcmp(type_s, "number") == 0) return cJSON_CreateNumber(0.0); + if (strcmp(type_s, "boolean") == 0) return cJSON_CreateBool(0); + if (strcmp(type_s, "array") == 0) return cJSON_CreateArray(); + if (strcmp(type_s, "object") == 0) return cJSON_CreateObject(); + return cJSON_CreateString(""); +} + +static cJSON* build_example_args_from_parameters_local(cJSON* params) { + cJSON* example = cJSON_CreateObject(); + if (!example) return NULL; + + cJSON* props = params ? cJSON_GetObjectItemCaseSensitive(params, "properties") : NULL; + if (!props || !cJSON_IsObject(props)) { + return example; + } + + cJSON* prop = NULL; + cJSON_ArrayForEach(prop, props) { + if (!prop->string || prop->string[0] == '\0') continue; + + cJSON* enum_vals = cJSON_GetObjectItemCaseSensitive(prop, "enum"); + if (enum_vals && cJSON_IsArray(enum_vals) && cJSON_GetArraySize(enum_vals) > 0) { + cJSON* first = cJSON_GetArrayItem(enum_vals, 0); + cJSON* dup = cJSON_Duplicate(first, 1); + if (dup) { + cJSON_AddItemToObject(example, prop->string, dup); + continue; + } + } + + cJSON* type = cJSON_GetObjectItemCaseSensitive(prop, "type"); + const char* type_s = (type && cJSON_IsString(type) && type->valuestring) ? type->valuestring : NULL; + cJSON* val = build_example_value_for_type_local(type_s); + if (!val) val = cJSON_CreateString(""); + if (!val) { + cJSON_Delete(example); + return NULL; + } + cJSON_AddItemToObject(example, prop->string, val); + } + + return example; +} + static char* build_slash_help_tool_json(const char* tool_name) { if (!tool_name || tool_name[0] == '\0') { return local_json_error("missing tool name"); @@ -689,6 +742,26 @@ static char* build_slash_help_tool_json(const char* tool_name) { cJSON_AddStringToObject(out, "tool", tool_name); cJSON_AddItemToObject(out, "schema", cJSON_Duplicate(matched_fn, 1)); + cJSON* params = cJSON_GetObjectItemCaseSensitive(matched_fn, "parameters"); + cJSON* example_args = build_example_args_from_parameters_local(params); + if (example_args) { + char* example_json = cJSON_PrintUnformatted(example_args); + if (example_json) { + cJSON_AddStringToObject(out, "example_args_json", example_json); + + size_t usage_len = strlen(tool_name) + strlen(example_json) + 4U; + char* usage = (char*)malloc(usage_len); + if (usage) { + snprintf(usage, usage_len, "/%s %s", tool_name, example_json); + cJSON_AddStringToObject(out, "example_slash", usage); + free(usage); + } + + free(example_json); + } + cJSON_Delete(example_args); + } + char* result = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(schema); @@ -770,40 +843,6 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa static const char* get_context_part_name_copy(int idx); -static int appendf(char** buf, size_t* cap, size_t* used, const char* fmt, ...) { - if (!buf || !cap || !used || !fmt) return -1; - - while (1) { - if (*used + 128U >= *cap) { - size_t next_cap = (*cap) * 2U; - char* grown = (char*)realloc(*buf, next_cap); - if (!grown) return -1; - *buf = grown; - *cap = next_cap; - } - - va_list ap; - va_start(ap, fmt); - int n = vsnprintf(*buf + *used, *cap - *used, fmt, ap); - va_end(ap); - - if (n < 0) { - return -1; - } - - if ((size_t)n < (*cap - *used)) { - *used += (size_t)n; - return 0; - } - - size_t next_cap = (*cap) * 2U + (size_t)n + 64U; - char* grown = (char*)realloc(*buf, next_cap); - if (!grown) return -1; - *buf = grown; - *cap = next_cap; - } -} - static const char* detect_context_section(const char* role, const char* content, int idx) { const char* c = content ? content : ""; const char* r = role ? role : ""; @@ -857,233 +896,6 @@ const char* agent_classify_message_part(cJSON* msg, int idx) { return detect_context_section(role_s, content_s, idx); } -static const char* admin_label_for_log(void) { - static __thread char name_buf[96]; - snprintf(name_buf, sizeof(name_buf), "%s", "Administrator"); - - char* kind0 = nostr_handler_get_admin_kind0_context(); - if (!kind0 || kind0[0] == '\0') { - free(kind0); - return name_buf; - } - - cJSON* root = cJSON_Parse(kind0); - free(kind0); - if (!root || !cJSON_IsObject(root)) { - cJSON_Delete(root); - return name_buf; - } - - cJSON* display_name = cJSON_GetObjectItemCaseSensitive(root, "display_name"); - cJSON* name = cJSON_GetObjectItemCaseSensitive(root, "name"); - const char* chosen = NULL; - if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') { - chosen = display_name->valuestring; - } else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') { - chosen = name->valuestring; - } - - if (chosen) { - snprintf(name_buf, sizeof(name_buf), "%s", chosen); - } - - cJSON_Delete(root); - return name_buf; -} - -static char* demote_markdown_headings_for_log(const char* content) { - const char* src = content ? content : ""; - size_t n = strlen(src); - - char* out = (char*)malloc(n * 2U + 1U); - if (!out) { - return NULL; - } - - size_t w = 0; - int line_start = 1; - for (size_t i = 0; i < n; i++) { - char c = src[i]; - - if (line_start && c == '#') { - out[w++] = '#'; - } - - out[w++] = c; - - if (c == '\n') { - line_start = 1; - } else if (line_start && (c == ' ' || c == '\t' || c == '\r')) { - line_start = 1; - } else { - line_start = 0; - } - } - - out[w] = '\0'; - return out; -} - -static void format_chat_time_for_log(time_t ts, char out[16]) { - if (!out) return; - out[0] = '\0'; - - if (ts <= 0) { - snprintf(out, 16, "--:--"); - return; - } - - struct tm tm_info; - if (!localtime_r(&ts, &tm_info)) { - snprintf(out, 16, "--:--"); - return; - } - - strftime(out, 16, "%H:%M", &tm_info); -} - -static char* format_context_payload_for_log(const char* context_payload) { - const char* payload = context_payload ? context_payload : ""; - - cJSON* root = cJSON_Parse(payload); - if (!root || !cJSON_IsArray(root)) { - cJSON_Delete(root); - return strdup(payload); - } - - size_t cap = strlen(payload) + 2048U; - if (cap < 4096U) cap = 4096U; - char* out = (char*)malloc(cap); - if (!out) { - cJSON_Delete(root); - return strdup(payload); - } - out[0] = '\0'; - size_t used = 0; - char* conversation_role = NULL; - char* conversation_content = NULL; - - int n = cJSON_GetArraySize(root); - (void)appendf(&out, &cap, &used, "Sections: %d\n\n", n); - - for (int i = 0; i < n; i++) { - cJSON* msg = cJSON_GetArrayItem(root, i); - cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL; - cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL; - - const char* role_s = (role && cJSON_IsString(role) && role->valuestring) - ? role->valuestring - : "unknown"; - - const char* content_s = ""; - if (content && cJSON_IsString(content) && content->valuestring) { - content_s = content->valuestring; - } else if (content && cJSON_IsNull(content)) { - content_s = ""; - } - - const char* section = agent_classify_message_part(msg, i); - if (section && strcmp(section, "dm_history") == 0) { - continue; - } - - if (section && strcmp(section, "conversation") == 0) { - free(conversation_role); - free(conversation_content); - conversation_role = strdup(role_s); - conversation_content = strdup(content_s); - continue; - } - - char* display_content = demote_markdown_headings_for_log(content_s); - const char* safe_display_content = display_content ? display_content : content_s; - - if (appendf(&out, - &cap, - &used, - "# %s | role=%s\n\n" - "%s\n\n", - section ? section : "context_part", - role_s, - safe_display_content) != 0) { - free(display_content); - free(out); - cJSON_Delete(root); - return strdup(payload); - } - - free(display_content); - } - - const char* admin_label = admin_label_for_log(); - size_t chat_cap = 4096U; - char* chat = (char*)malloc(chat_cap); - size_t chat_used = 0; - char* latest_admin_line = NULL; - if (chat) { - chat[0] = '\0'; - - for (int i = 0; i < n; i++) { - cJSON* msg = cJSON_GetArrayItem(root, i); - cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL; - cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL; - cJSON* ts = msg ? cJSON_GetObjectItemCaseSensitive(msg, "_ts") : NULL; - const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : ""; - const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : ""; - const char* section = agent_classify_message_part(msg, i); - if (!section || strcmp(section, "dm_history") != 0) { - continue; - } - - time_t created_at = 0; - if (ts && cJSON_IsNumber(ts)) { - created_at = (time_t)ts->valuedouble; - } - char time_buf[16] = {0}; - format_chat_time_for_log(created_at, time_buf); - - const char* who = (strcmp(role_s, "user") == 0) ? admin_label : "Agent"; - if (appendf(&chat, &chat_cap, &chat_used, "%s %s %s\n", time_buf, who, content_s) != 0) { - free(chat); - chat = NULL; - break; - } - - if (strcmp(role_s, "user") == 0) { - free(latest_admin_line); - int line_len = snprintf(NULL, 0, "%s %s %s", time_buf, who, content_s); - if (line_len >= 0) { - latest_admin_line = (char*)malloc((size_t)line_len + 1U); - if (latest_admin_line) { - snprintf(latest_admin_line, (size_t)line_len + 1U, "%s %s %s", time_buf, who, content_s); - } - } - } - } - - if (chat && chat_used > 0) { - (void)appendf(&out, &cap, &used, "# dm_history | role=chat\n\n%s\n", chat); - } - } - - if (conversation_role && conversation_content) { - (void)appendf(&out, - &cap, - &used, - "# conversation | role=%s\n\n%s\n\n", - conversation_role, - conversation_content); - } - - free(chat); - free(latest_admin_line); - free(conversation_role); - free(conversation_content); - - cJSON_Delete(root); - return out; -} - static void clear_context_part_names_locked(void) { for (int i = 0; i < g_context_part_names_count; i++) { free(g_context_part_names[i]); @@ -1133,16 +945,19 @@ static __attribute__((unused)) void template_emit_hook(const char* section_name, } static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) { + if (phase && strcmp(phase, "direct_tool_exec") == 0) { + return; + } + time_t now = time(NULL); struct tm tm_info; localtime_r(&now, &tm_info); char timestamp[32] = {0}; strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info); - char* pretty_payload = format_context_payload_for_log(context_payload); const char* safe_phase = phase ? phase : "unknown"; const char* safe_sender = sender_pubkey_hex ? sender_pubkey_hex : "unknown"; - const char* safe_payload = pretty_payload ? pretty_payload : ""; + const char* safe_payload = context_payload ? context_payload : ""; size_t context_bytes = context_payload ? strlen(context_payload) : 0U; size_t approx_tokens = context_bytes / 4U; @@ -1172,13 +987,11 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase, approx_tokens, safe_payload); if (entry_len < 0) { - free(pretty_payload); return; } char* entry = (char*)malloc((size_t)entry_len + 1U); if (!entry) { - free(pretty_payload); return; } @@ -1231,7 +1044,6 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase, if (!out) { free(old_data); free(entry); - free(pretty_payload); return; } @@ -1243,7 +1055,6 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase, fclose(out); free(old_data); free(entry); - free(pretty_payload); } void agent_append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) { @@ -1262,7 +1073,7 @@ static __attribute__((unused)) char* build_sender_verification_text(didactyl_sen } -static int append_recent_admin_dm_history(cJSON* messages, const char* current_message) { +static __attribute__((unused)) int append_recent_admin_dm_history(cJSON* messages, const char* current_message) { if (!messages || !g_cfg) { return -1; } @@ -1478,16 +1289,11 @@ static char* build_context_from_triggers(trigger_type_t trigger_type, int matched = 0; didactyl_sender_tier_t tier = DIDACTYL_SENDER_STRANGER; - const char* message = NULL; if (trigger_event && cJSON_IsObject(trigger_event)) { cJSON* tier_j = cJSON_GetObjectItemCaseSensitive(trigger_event, "tier"); - cJSON* msg_j = cJSON_GetObjectItemCaseSensitive(trigger_event, "message"); const char* tier_s = (tier_j && cJSON_IsString(tier_j) && tier_j->valuestring) ? tier_j->valuestring : "stranger"; if (strcmp(tier_s, "admin") == 0) tier = DIDACTYL_SENDER_ADMIN; else if (strcmp(tier_s, "wot") == 0) tier = DIDACTYL_SENDER_WOT; - if (msg_j && cJSON_IsString(msg_j) && msg_j->valuestring) { - message = msg_j->valuestring; - } } char* matched_skill_contents[AGENT_ADOPTED_SKILLS_MAX]; @@ -1562,27 +1368,6 @@ static char* build_context_from_triggers(trigger_type_t trigger_type, used = need - 1U; } - if (trigger_type == TRIGGER_TYPE_DM && message && message[0] != '\0') { - const char* suffix = "\n\nIncoming DM message:\n"; - size_t need = strlen(suffix) + strlen(message) + 1U; - if (used + need >= cap) { - size_t next = cap; - while (used + need >= next) next *= 2U; - char* grown = (char*)realloc(out, next); - if (!grown) { - free(out); - return NULL; - } - out = grown; - cap = next; - } - memcpy(out + used, suffix, strlen(suffix)); - used += strlen(suffix); - memcpy(out + used, message, strlen(message)); - used += strlen(message); - out[used] = '\0'; - } - return out; } @@ -2247,7 +2032,6 @@ int agent_build_admin_messages_json(const char* current_user_message, cJSON* messages = cJSON_CreateArray(); if (!messages || append_simple_message(messages, "system", composed_context ? composed_context : "You are an AI agent. Respond to the message.") != 0 || - append_recent_admin_dm_history(messages, current_user_message) != 0 || append_simple_message(messages, "user", current_user_message ? current_user_message : "") != 0) { free(composed_context); cJSON_Delete(messages); @@ -2381,7 +2165,6 @@ void agent_on_message(const char* sender_pubkey_hex, cJSON* messages = cJSON_CreateArray(); if (!messages || append_simple_message(messages, "system", dm_context ? dm_context : "You are an AI agent. Respond to the message.") != 0 || - append_recent_admin_dm_history(messages, message) != 0 || append_simple_message(messages, "user", message) != 0) { cJSON_Delete(messages); free(tools_json); diff --git a/src/main.h b/src/main.h index 0dff6c3..15631f7 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 7 -#define DIDACTYL_VERSION "v0.2.7" +#define DIDACTYL_VERSION_PATCH 8 +#define DIDACTYL_VERSION "v0.2.8" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/nostr_handler.c b/src/nostr_handler.c index 49cb679..0fc50ba 100644 --- a/src/nostr_handler.c +++ b/src/nostr_handler.c @@ -146,9 +146,28 @@ static void nostr_core_log_bridge(int level, const char* component, const char* const char* msg = message ? message : ""; switch (mapped) { - case DEBUG_LEVEL_TRACE: + case DEBUG_LEVEL_TRACE: { + int is_websocket = (strcmp(comp, "websocket") == 0); + int is_frame = (strncmp(msg, "SEND ", 5) == 0 || strncmp(msg, "RECV ", 5) == 0); + size_t msg_len = strlen(msg); + if (is_websocket && is_frame && msg_len > 384U) { + const char* direction = (strncmp(msg, "SEND ", 5) == 0) ? "SEND" : "RECV"; + const char* frame_type = "frame"; + if (strstr(msg, "[\"EVENT\"")) frame_type = "EVENT"; + else if (strstr(msg, "[\"REQ\"")) frame_type = "REQ"; + else if (strstr(msg, "[\"CLOSE\"")) frame_type = "CLOSE"; + else if (strstr(msg, "[\"EOSE\"")) frame_type = "EOSE"; + else if (strstr(msg, "[\"NOTICE\"")) frame_type = "NOTICE"; + DEBUG_TRACE("[didactyl] [nostr:%s] %s ", + comp, + direction, + frame_type, + msg_len); + break; + } DEBUG_TRACE("[didactyl] [nostr:%s] %s", comp, msg); break; + } case DEBUG_LEVEL_DEBUG: DEBUG_LOG("[didactyl] [nostr:%s] %s", comp, msg); break; diff --git a/src/prompt_template.c b/src/prompt_template.c index 8447b98..2c94e56 100644 --- a/src/prompt_template.c +++ b/src/prompt_template.c @@ -114,17 +114,62 @@ char* prompt_template_resolve_inline_variables(const char* tpl, tools_context_t* const char* replacement = ""; char* replacement_owned = NULL; + char* call_name_owned = NULL; + char* call_args_owned = NULL; + const char* lookup_name = var_trim; + const char* call_args_json = "{}"; - if (strcmp(var_trim, "message") == 0) { + size_t var_len = strlen(var_trim); + if (var_len > 0 && var_trim[var_len - 1] == ')') { + char* open_paren = strchr(var_trim, '('); + if (open_paren && open_paren < (var_trim + var_len - 1)) { + call_name_owned = dup_range(var_trim, (size_t)(open_paren - var_trim)); + if (!call_name_owned) { + free(var); + free(out); + return NULL; + } + char* call_name_trim = ltrim_inplace(call_name_owned); + rtrim_inplace(call_name_trim); + + char* raw_args = dup_range(open_paren + 1, + (size_t)((var_trim + var_len - 1) - (open_paren + 1))); + if (!raw_args) { + free(call_name_owned); + free(var); + free(out); + return NULL; + } + char* raw_args_trim = ltrim_inplace(raw_args); + rtrim_inplace(raw_args_trim); + + if (call_name_trim[0] != '\0') { + lookup_name = call_name_trim; + if (raw_args_trim[0] != '\0') { + cJSON* parsed_args = cJSON_Parse(raw_args_trim); + if (parsed_args) { + call_args_owned = strdup(raw_args_trim); + cJSON_Delete(parsed_args); + } + } + } + + free(raw_args); + } + } + + if (call_args_owned) { + call_args_json = call_args_owned; + } + + if (strcmp(lookup_name, "message") == 0) { replacement = (tools_ctx && tools_ctx->template_current_user_message) ? tools_ctx->template_current_user_message : ""; - } else if (strcmp(var_trim, "dm_history") == 0) { - replacement = ""; } else if (tools_ctx) { - const char* tool_name = map_variable_tool_name(var_trim); + const char* tool_name = map_variable_tool_name(lookup_name); if (tool_name && tool_name[0] != '\0') { - char* tool_result = tools_execute(tools_ctx, tool_name, "{}"); + char* tool_result = tools_execute(tools_ctx, tool_name, call_args_json); if (tool_result) { cJSON* root = cJSON_Parse(tool_result); if (root && cJSON_IsObject(root)) { @@ -143,10 +188,10 @@ char* prompt_template_resolve_inline_variables(const char* tpl, tools_context_t* } } - if (!replacement_owned && tools_ctx->template_skill_lookup && var_trim[0] != '\0') { + if (!replacement_owned && tools_ctx->template_skill_lookup && lookup_name[0] != '\0') { const char* skill_content = tools_ctx->template_skill_lookup( tools_ctx->template_skill_lookup_user_data, - var_trim); + lookup_name); if (skill_content && skill_content[0] != '\0') { replacement_owned = strdup(skill_content); } @@ -156,12 +201,16 @@ char* prompt_template_resolve_inline_variables(const char* tpl, tools_context_t* } if (append_text(&out, &cap, &used, replacement) != 0) { + free(call_args_owned); + free(call_name_owned); free(replacement_owned); free(var); free(out); return NULL; } + free(call_args_owned); + free(call_name_owned); free(replacement_owned); free(var); p = close + 2; diff --git a/src/tools/tool_admin.c b/src/tools/tool_admin.c index 6a7157e..0584248 100644 --- a/src/tools/tool_admin.c +++ b/src/tools/tool_admin.c @@ -48,15 +48,23 @@ char* execute_admin_identity(tools_context_t* ctx, const char* args_json) { cJSON_AddBoolToObject(out, "success", 1); - char content[768]; - snprintf(content, - sizeof(content), - "## Administrator Identity (source: config.admin.pubkey)\n\n" - "This is your administrator! Admin pubkey (hex): %s\n\n" - "%s", - ctx->cfg->admin.pubkey[0] ? ctx->cfg->admin.pubkey : "unknown", - tier_text); - cJSON_AddStringToObject(out, "content", content); + cJSON* identity = cJSON_CreateObject(); + if (!identity) { + cJSON_Delete(out); + return NULL; + } + cJSON_AddStringToObject(identity, + "admin_pubkey", + ctx->cfg->admin.pubkey[0] ? ctx->cfg->admin.pubkey : "unknown"); + cJSON_AddStringToObject(identity, "sender_tier", tier_text); + char* identity_json = cJSON_PrintUnformatted(identity); + cJSON_Delete(identity); + if (!identity_json) { + cJSON_Delete(out); + return NULL; + } + cJSON_AddStringToObject(out, "content", identity_json); + free(identity_json); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); @@ -81,19 +89,7 @@ char* execute_nostr_admin_profile(tools_context_t* ctx, const char* args_json) { cJSON_AddBoolToObject(out, "success", 1); - size_t content_len = strlen("## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): ") + strlen(profile_json) + 1U; - char* content = (char*)malloc(content_len); - if (!content) { - cJSON_Delete(out); - free(kind0); - return NULL; - } - snprintf(content, - content_len, - "## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): %s", - profile_json); - cJSON_AddStringToObject(out, "content", content); - free(content); + cJSON_AddStringToObject(out, "content", profile_json); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); @@ -119,19 +115,7 @@ char* execute_nostr_admin_contacts(tools_context_t* ctx, const char* args_json) cJSON_AddBoolToObject(out, "success", 1); - size_t content_len = strlen("## Administrator Kind 3 Contacts (source: nostr kind 3)\n\nAdministrator contacts (JSON): ") + strlen(contacts_json) + 1U; - char* content = (char*)malloc(content_len); - if (!content) { - cJSON_Delete(out); - free(kind3); - return NULL; - } - snprintf(content, - content_len, - "## Administrator Kind 3 Contacts (source: nostr kind 3)\n\nAdministrator contacts (JSON): %s", - contacts_json); - cJSON_AddStringToObject(out, "content", content); - free(content); + cJSON_AddStringToObject(out, "content", contacts_json); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); @@ -157,19 +141,7 @@ char* execute_nostr_admin_relays(tools_context_t* ctx, const char* args_json) { cJSON_AddBoolToObject(out, "success", 1); - size_t content_len = strlen("## Administrator Kind 10002 Relays (source: nostr kind 10002)\n\nAdministrator relay list (JSON): ") + strlen(relays_json) + 1U; - char* content = (char*)malloc(content_len); - if (!content) { - cJSON_Delete(out); - free(kind10002); - return NULL; - } - snprintf(content, - content_len, - "## Administrator Kind 10002 Relays (source: nostr kind 10002)\n\nAdministrator relay list (JSON): %s", - relays_json); - cJSON_AddStringToObject(out, "content", content); - free(content); + cJSON_AddStringToObject(out, "content", relays_json); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); @@ -195,19 +167,7 @@ char* execute_nostr_admin_notes(tools_context_t* ctx, const char* args_json) { cJSON_AddBoolToObject(out, "success", 1); - size_t content_len = strlen("## Administrator Recent Kind 1 Notes\n\n") + strlen(notes_text) + 1U; - char* content = (char*)malloc(content_len); - if (!content) { - cJSON_Delete(out); - free(notes); - return NULL; - } - snprintf(content, - content_len, - "## Administrator Recent Kind 1 Notes\n\n%s", - notes_text); - cJSON_AddStringToObject(out, "content", content); - free(content); + cJSON_AddStringToObject(out, "content", notes_text); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); diff --git a/src/tools/tool_agent.c b/src/tools/tool_agent.c index f59c323..e9a05e1 100644 --- a/src/tools/tool_agent.c +++ b/src/tools/tool_agent.c @@ -63,13 +63,23 @@ char* execute_agent_identity(tools_context_t* ctx, const char* args_json) { if (!out) return NULL; cJSON_AddBoolToObject(out, "success", 1); - char content[384]; - snprintf(content, - sizeof(content), - "## Agent Identity\n\nYour pubkey (hex): %s\nYour npub: %s", - ctx->cfg->keys.public_key_hex[0] ? ctx->cfg->keys.public_key_hex : "unknown", - npub); - cJSON_AddStringToObject(out, "content", content); + cJSON* identity = cJSON_CreateObject(); + if (!identity) { + cJSON_Delete(out); + return NULL; + } + cJSON_AddStringToObject(identity, + "pubkey", + ctx->cfg->keys.public_key_hex[0] ? ctx->cfg->keys.public_key_hex : "unknown"); + cJSON_AddStringToObject(identity, "npub", npub); + char* identity_json = cJSON_PrintUnformatted(identity); + cJSON_Delete(identity); + if (!identity_json) { + cJSON_Delete(out); + return NULL; + } + cJSON_AddStringToObject(out, "content", identity_json); + free(identity_json); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); @@ -94,19 +104,7 @@ char* execute_nostr_agent_profile(tools_context_t* ctx, const char* args_json) { cJSON_AddBoolToObject(out, "success", 1); - size_t content_len = strlen("## Agent Kind 0 Profile (source: nostr kind 0)\n\nAgent kind 0 profile content (JSON): ") + strlen(profile_json) + 1U; - char* content = (char*)malloc(content_len); - if (!content) { - cJSON_Delete(out); - free(kind0); - return NULL; - } - snprintf(content, - content_len, - "## Agent Kind 0 Profile (source: nostr kind 0)\n\nAgent kind 0 profile content (JSON): %s", - profile_json); - cJSON_AddStringToObject(out, "content", content); - free(content); + cJSON_AddStringToObject(out, "content", profile_json); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); @@ -132,19 +130,7 @@ char* execute_nostr_agent_contacts(tools_context_t* ctx, const char* args_json) cJSON_AddBoolToObject(out, "success", 1); - size_t content_len = strlen("## Agent Kind 3 Contacts (source: nostr kind 3)\n\nAgent contacts (JSON): ") + strlen(contacts_json) + 1U; - char* content = (char*)malloc(content_len); - if (!content) { - cJSON_Delete(out); - free(kind3); - return NULL; - } - snprintf(content, - content_len, - "## Agent Kind 3 Contacts (source: nostr kind 3)\n\nAgent contacts (JSON): %s", - contacts_json); - cJSON_AddStringToObject(out, "content", content); - free(content); + cJSON_AddStringToObject(out, "content", contacts_json); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); @@ -170,19 +156,7 @@ char* execute_nostr_agent_relays(tools_context_t* ctx, const char* args_json) { cJSON_AddBoolToObject(out, "success", 1); - size_t content_len = strlen("## Agent Kind 10002 Relays (source: nostr kind 10002)\n\nAgent relay list (JSON): ") + strlen(relays_json) + 1U; - char* content = (char*)malloc(content_len); - if (!content) { - cJSON_Delete(out); - free(kind10002); - return NULL; - } - snprintf(content, - content_len, - "## Agent Kind 10002 Relays (source: nostr kind 10002)\n\nAgent relay list (JSON): %s", - relays_json); - cJSON_AddStringToObject(out, "content", content); - free(content); + cJSON_AddStringToObject(out, "content", relays_json); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); @@ -208,19 +182,7 @@ char* execute_nostr_agent_notes(tools_context_t* ctx, const char* args_json) { cJSON_AddBoolToObject(out, "success", 1); - size_t content_len = strlen("## Agent Recent Kind 1 Notes\n\n") + strlen(notes_text) + 1U; - char* content = (char*)malloc(content_len); - if (!content) { - cJSON_Delete(out); - free(notes); - return NULL; - } - snprintf(content, - content_len, - "## Agent Recent Kind 1 Notes\n\n%s", - notes_text); - cJSON_AddStringToObject(out, "content", content); - free(content); + cJSON_AddStringToObject(out, "content", notes_text); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); @@ -300,6 +262,125 @@ char* execute_trigger_event(tools_context_t* ctx, const char* args_json) { return json; } +char* execute_nostr_dm_history(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); + + cJSON* args = parse_args_local(args_json); + if (!args) return json_error_local("invalid arguments JSON"); + + int limit = 12; + cJSON* limit_j = cJSON_GetObjectItemCaseSensitive(args, "limit"); + if (limit_j && cJSON_IsNumber(limit_j)) { + limit = (int)limit_j->valuedouble; + } + if (limit < 1) limit = 1; + if (limit > 200) limit = 200; + + int include_current = 0; + cJSON* include_current_j = cJSON_GetObjectItemCaseSensitive(args, "include_current"); + if (include_current_j && cJSON_IsBool(include_current_j) && cJSON_IsTrue(include_current_j)) { + include_current = 1; + } + + const char* peer_pubkey = ctx->cfg->admin.pubkey; + cJSON* peer_j = cJSON_GetObjectItemCaseSensitive(args, "peer_pubkey"); + if (peer_j && cJSON_IsString(peer_j) && peer_j->valuestring && peer_j->valuestring[0] != '\0') { + peer_pubkey = peer_j->valuestring; + } + + char* history_json = nostr_handler_get_dm_history_json(peer_pubkey, limit); + cJSON_Delete(args); + if (!history_json) { + return json_error_local("failed to fetch dm history"); + } + + cJSON* parsed = cJSON_Parse(history_json); + free(history_json); + if (!parsed || !cJSON_IsArray(parsed)) { + cJSON_Delete(parsed); + parsed = cJSON_CreateArray(); + if (!parsed) { + return json_error_local("failed to build dm history response"); + } + } + + cJSON* filtered = cJSON_CreateArray(); + if (!filtered) { + cJSON_Delete(parsed); + return json_error_local("failed to build dm history response"); + } + + const char* current_message = (!include_current && ctx->template_current_user_message) + ? ctx->template_current_user_message + : NULL; + + const char* last_role = NULL; + const char* last_content = NULL; + + int n = cJSON_GetArraySize(parsed); + for (int i = 0; i < n; i++) { + cJSON* item = cJSON_GetArrayItem(parsed, i); + cJSON* role = item ? cJSON_GetObjectItemCaseSensitive(item, "role") : NULL; + cJSON* content = item ? cJSON_GetObjectItemCaseSensitive(item, "content") : NULL; + cJSON* created_at = item ? cJSON_GetObjectItemCaseSensitive(item, "created_at") : NULL; + + if (!role || !content || !cJSON_IsString(role) || !cJSON_IsString(content) || + !role->valuestring || !content->valuestring) { + continue; + } + + const char* role_s = role->valuestring; + const char* content_s = content->valuestring; + int role_is_user = (strcmp(role_s, "user") == 0); + if (!role_is_user && strcmp(role_s, "assistant") != 0) { + continue; + } + + if (i == n - 1 && role_is_user && current_message && strcmp(content_s, current_message) == 0) { + continue; + } + + if (last_role && last_content && strcmp(last_role, role_s) == 0 && strcmp(last_content, content_s) == 0) { + continue; + } + + cJSON* out_item = cJSON_CreateObject(); + if (!out_item) { + continue; + } + cJSON_AddStringToObject(out_item, "role", role_s); + cJSON_AddStringToObject(out_item, "content", content_s); + if (created_at && cJSON_IsNumber(created_at)) { + cJSON_AddNumberToObject(out_item, "created_at", created_at->valuedouble); + } + cJSON_AddItemToArray(filtered, out_item); + + last_role = role_s; + last_content = content_s; + } + + cJSON_Delete(parsed); + + char* filtered_json = cJSON_PrintUnformatted(filtered); + cJSON_Delete(filtered); + if (!filtered_json) { + return json_error_local("failed to serialize dm history"); + } + + cJSON* out = cJSON_CreateObject(); + if (!out) { + free(filtered_json); + return NULL; + } + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddStringToObject(out, "content", filtered_json); + free(filtered_json); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + return json; +} + char* execute_agent_version(const char* args_json) { cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); diff --git a/src/tools/tool_nostr_identity.c b/src/tools/tool_nostr_identity.c index 8150a73..a3e8e4b 100644 --- a/src/tools/tool_nostr_identity.c +++ b/src/tools/tool_nostr_identity.c @@ -227,6 +227,7 @@ char* execute_nostr_pubkey(tools_context_t* ctx, const char* args_json) { cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "pubkey", ctx->cfg->keys.public_key_hex); + cJSON_AddStringToObject(out, "content", ctx->cfg->keys.public_key_hex); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); @@ -250,6 +251,7 @@ char* execute_nostr_npub(tools_context_t* ctx, const char* args_json) { cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "npub", npub); + cJSON_AddStringToObject(out, "content", npub); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); diff --git a/src/tools/tool_skill.c b/src/tools/tool_skill.c index 2d3b76f..f370e16 100644 --- a/src/tools/tool_skill.c +++ b/src/tools/tool_skill.c @@ -908,6 +908,315 @@ char* execute_skill_list(tools_context_t* ctx, const char* args_json) { return json; } +char* execute_skill_get(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); + + cJSON* args = parse_args_local(args_json); + if (!args) return json_error_local("invalid arguments JSON"); + + cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d"); + cJSON* input = cJSON_GetObjectItemCaseSensitive(args, "input"); + const char* d_tag_value = NULL; + if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && validate_skill_d_tag_local(d_tag->valuestring)) { + d_tag_value = d_tag->valuestring; + } else if (input && cJSON_IsString(input) && input->valuestring && validate_skill_d_tag_local(input->valuestring)) { + d_tag_value = input->valuestring; + } + if (!d_tag_value) { + cJSON_Delete(args); + return json_error_local("skill_get requires valid d tag"); + } + + char* events_json = nostr_handler_get_self_skill_events_json(); + if (!events_json) { + cJSON_Delete(args); + return json_error_local("skill_get cache unavailable"); + } + + cJSON* events = cJSON_Parse(events_json); + free(events_json); + if (!events || !cJSON_IsArray(events)) { + cJSON_Delete(args); + cJSON_Delete(events); + return json_error_local("skill_get cache returned invalid JSON"); + } + + cJSON* target = NULL; + long long best_created_at = -1; + int event_count = cJSON_GetArraySize(events); + for (int i = 0; i < event_count; i++) { + cJSON* ev = cJSON_GetArrayItem(events, i); + if (!ev || !cJSON_IsObject(ev)) continue; + + cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind"); + cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags"); + if (!kind || !cJSON_IsNumber(kind) || !tags || !cJSON_IsArray(tags)) continue; + + int kind_val = (int)kind->valuedouble; + if (kind_val != 31123 && kind_val != 31124) continue; + + cJSON* d_val = find_tag_value_string_local(tags, "d"); + if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) continue; + if (strcmp(d_val->valuestring, d_tag_value) != 0) continue; + + cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at"); + long long created = (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : 0; + if (!target || created > best_created_at) { + target = ev; + best_created_at = created; + } + } + + if (!target) { + cJSON_Delete(args); + cJSON_Delete(events); + return json_error_local("skill_get could not find skill with that d"); + } + + cJSON* kind = cJSON_GetObjectItemCaseSensitive(target, "kind"); + cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(target, "pubkey"); + cJSON* id = cJSON_GetObjectItemCaseSensitive(target, "id"); + cJSON* created_at = cJSON_GetObjectItemCaseSensitive(target, "created_at"); + cJSON* content = cJSON_GetObjectItemCaseSensitive(target, "content"); + cJSON* tags = cJSON_GetObjectItemCaseSensitive(target, "tags"); + + if (!kind || !cJSON_IsNumber(kind) || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || + !content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) { + cJSON_Delete(args); + cJSON_Delete(events); + return json_error_local("skill_get found malformed skill event"); + } + + int kind_val = (int)kind->valuedouble; + cJSON* scope = find_tag_value_string_local(tags, "scope"); + + cJSON* out = cJSON_CreateObject(); + if (!out) { + cJSON_Delete(args); + cJSON_Delete(events); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddStringToObject(out, "d_tag", d_tag_value); + cJSON_AddNumberToObject(out, "kind", kind_val); + cJSON_AddStringToObject(out, + "scope", + (scope && cJSON_IsString(scope) && scope->valuestring) + ? scope->valuestring + : (kind_val == 31124 ? "private" : "public")); + cJSON_AddStringToObject(out, "owner", skill_owner_label_local(ctx->cfg, pubkey->valuestring)); + if (id && cJSON_IsString(id) && id->valuestring) { + cJSON_AddStringToObject(out, "id", id->valuestring); + } + if (created_at && cJSON_IsNumber(created_at)) { + char ts_buf[32]; + snprintf(ts_buf, sizeof(ts_buf), "%lld", (long long)created_at->valuedouble); + cJSON* ts_raw = cJSON_CreateRaw(ts_buf); + if (ts_raw) { + cJSON_AddItemToObject(out, "created_at", ts_raw); + } + } + cJSON_AddStringToObject(out, "content", content->valuestring); + cJSON* tags_dup = cJSON_Duplicate(tags, 1); + if (tags_dup) { + cJSON_AddItemToObject(out, "tags", tags_dup); + } + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + cJSON_Delete(args); + cJSON_Delete(events); + return json; +} + +char* execute_skill_view(tools_context_t* ctx, const char* args_json) { + return execute_skill_get(ctx, args_json); +} + +char* execute_skill_set(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); + + cJSON* args = parse_args_local(args_json); + if (!args) return json_error_local("invalid arguments JSON"); + + cJSON* skill = cJSON_GetObjectItemCaseSensitive(args, "skill"); + cJSON* input = cJSON_GetObjectItemCaseSensitive(args, "input"); + + cJSON* parsed_input = NULL; + cJSON* src = NULL; + if (skill && cJSON_IsObject(skill)) { + src = skill; + } else if (input && cJSON_IsString(input) && input->valuestring && input->valuestring[0] != '\0') { + parsed_input = cJSON_Parse(input->valuestring); + if (parsed_input && cJSON_IsObject(parsed_input)) { + src = parsed_input; + } + } else if (cJSON_IsObject(args)) { + src = args; + } + + if (!src || !cJSON_IsObject(src)) { + cJSON_Delete(parsed_input); + cJSON_Delete(args); + return json_error_local("skill_set requires a skill object"); + } + + cJSON* d = cJSON_GetObjectItemCaseSensitive(src, "d"); + cJSON* kind = cJSON_GetObjectItemCaseSensitive(src, "kind"); + cJSON* content = cJSON_GetObjectItemCaseSensitive(src, "content"); + cJSON* tags = cJSON_GetObjectItemCaseSensitive(src, "tags"); + cJSON* scope = cJSON_GetObjectItemCaseSensitive(src, "scope"); + + if (!d || !cJSON_IsString(d) || !d->valuestring || !validate_skill_d_tag_local(d->valuestring)) { + cJSON_Delete(parsed_input); + cJSON_Delete(args); + return json_error_local("skill_set requires valid d"); + } + if (!content || !cJSON_IsString(content) || !content->valuestring) { + cJSON_Delete(parsed_input); + cJSON_Delete(args); + return json_error_local("skill_set requires string content"); + } + if (!tags || !cJSON_IsArray(tags)) { + cJSON_Delete(parsed_input); + cJSON_Delete(args); + return json_error_local("skill_set requires tags array"); + } + + int out_kind = 0; + if (kind && cJSON_IsNumber(kind)) { + out_kind = (int)kind->valuedouble; + } else if (scope && cJSON_IsString(scope) && scope->valuestring) { + out_kind = (strcmp(scope->valuestring, "private") == 0) ? 31124 : 31123; + } else { + cJSON* scope_tag = find_tag_value_string_local(tags, "scope"); + out_kind = (scope_tag && cJSON_IsString(scope_tag) && scope_tag->valuestring && strcmp(scope_tag->valuestring, "private") == 0) + ? 31124 + : 31123; + } + if (out_kind != 31123 && out_kind != 31124) { + cJSON_Delete(parsed_input); + cJSON_Delete(args); + return json_error_local("skill_set kind must be 31123 or 31124"); + } + + cJSON* tags_out = cJSON_Duplicate(tags, 1); + if (!tags_out || !cJSON_IsArray(tags_out)) { + cJSON_Delete(tags_out); + cJSON_Delete(parsed_input); + cJSON_Delete(args); + return json_error_local("skill_set failed to duplicate tags"); + } + + if (upsert_string_tag_local(tags_out, "d", d->valuestring) != 0 || + upsert_string_tag_local(tags_out, "app", "didactyl") != 0 || + upsert_string_tag_local(tags_out, "scope", out_kind == 31124 ? "private" : "public") != 0) { + cJSON_Delete(tags_out); + cJSON_Delete(parsed_input); + cJSON_Delete(args); + return json_error_local("skill_set failed to normalize required tags"); + } + + nostr_publish_result_t publish_result; + memset(&publish_result, 0, sizeof(publish_result)); + int rc = nostr_handler_publish_kind_event(out_kind, content->valuestring, tags_out, &publish_result); + + int trigger_registered = 0; + if (rc == 0 && ctx->trigger_manager) { + cJSON* trigger = find_tag_value_string_local(tags_out, "trigger"); + cJSON* filter = find_tag_value_string_local(tags_out, "filter"); + cJSON* enabled = find_tag_value_string_local(tags_out, "enabled"); + cJSON* llm = find_tag_value_string_local(tags_out, "llm"); + cJSON* tools = find_tag_value_string_local(tags_out, "tools"); + cJSON* max_tokens = find_tag_value_string_local(tags_out, "max_tokens"); + cJSON* temperature = find_tag_value_string_local(tags_out, "temperature"); + cJSON* seed = find_tag_value_string_local(tags_out, "seed"); + + int enabled_i = !(enabled && cJSON_IsString(enabled) && enabled->valuestring && strcmp(enabled->valuestring, "false") == 0); + const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL; + const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL; + + if (trigger_s && filter_s && enabled_i) { + int max_tokens_set = (max_tokens && cJSON_IsString(max_tokens) && max_tokens->valuestring && max_tokens->valuestring[0] != '\0') ? 1 : 0; + int max_tokens_v = max_tokens_set ? atoi(max_tokens->valuestring) : 0; + int temperature_set = (temperature && cJSON_IsString(temperature) && temperature->valuestring && temperature->valuestring[0] != '\0') ? 1 : 0; + double temperature_v = temperature_set ? atof(temperature->valuestring) : 0.0; + int seed_set = (seed && cJSON_IsString(seed) && seed->valuestring && seed->valuestring[0] != '\0') ? 1 : 0; + int seed_v = seed_set ? atoi(seed->valuestring) : 0; + + if (trigger_manager_update(ctx->trigger_manager, + d->valuestring, + content->valuestring, + filter_s, + TRIGGER_ACTION_LLM, + trigger_s, + enabled_i, + (llm && cJSON_IsString(llm) && llm->valuestring) ? llm->valuestring : NULL, + (tools && cJSON_IsString(tools) && tools->valuestring) ? tools->valuestring : NULL, + max_tokens_set, + max_tokens_v, + temperature_set, + temperature_v, + seed_set, + seed_v) == 0 || + trigger_manager_add(ctx->trigger_manager, + d->valuestring, + content->valuestring, + filter_s, + TRIGGER_ACTION_LLM, + trigger_s, + enabled_i, + (llm && cJSON_IsString(llm) && llm->valuestring) ? llm->valuestring : NULL, + (tools && cJSON_IsString(tools) && tools->valuestring) ? tools->valuestring : NULL, + max_tokens_set, + max_tokens_v, + temperature_set, + temperature_v, + seed_set, + seed_v) == 0) { + trigger_registered = 1; + } + } else { + (void)trigger_manager_remove(ctx->trigger_manager, d->valuestring); + } + } + + cJSON_Delete(tags_out); + + if (rc != 0) { + cJSON_Delete(parsed_input); + cJSON_Delete(args); + nostr_handler_publish_result_free(&publish_result); + return json_error_local("skill_set failed to publish skill event"); + } + + cJSON* out = cJSON_CreateObject(); + if (!out) { + cJSON_Delete(parsed_input); + cJSON_Delete(args); + nostr_handler_publish_result_free(&publish_result); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0); + cJSON_AddStringToObject(out, "d_tag", d->valuestring); + cJSON_AddNumberToObject(out, "kind", out_kind); + cJSON_AddStringToObject(out, "scope", out_kind == 31124 ? "private" : "public"); + cJSON_AddStringToObject(out, "skill_event_id", publish_result.event_id); + cJSON_AddBoolToObject(out, "trigger_registered", trigger_registered ? 1 : 0); + if (publish_result.naddr_uri[0] != '\0') { + cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri); + } + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + cJSON_Delete(parsed_input); + cJSON_Delete(args); + nostr_handler_publish_result_free(&publish_result); + return json; +} + char* execute_skill_adopt(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); diff --git a/src/tools/tools_dispatch.c b/src/tools/tools_dispatch.c index 379bade..f1bcd7c 100644 --- a/src/tools/tools_dispatch.c +++ b/src/tools/tools_dispatch.c @@ -131,6 +131,15 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch if (strcmp(tool_name, "skill_list") == 0) { return execute_skill_list(ctx, args_json); } + if (strcmp(tool_name, "skill_get") == 0) { + return execute_skill_get(ctx, args_json); + } + if (strcmp(tool_name, "skill_view") == 0) { + return execute_skill_get(ctx, args_json); + } + if (strcmp(tool_name, "skill_set") == 0) { + return execute_skill_set(ctx, args_json); + } if (strcmp(tool_name, "skill_adopt") == 0) { return execute_skill_adopt(ctx, args_json); } @@ -240,6 +249,9 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch if (strcmp(tool_name, "trigger_event") == 0) { return execute_trigger_event(ctx, args_json); } + if (strcmp(tool_name, "nostr_dm_history") == 0) { + return execute_nostr_dm_history(ctx, args_json); + } if (strcmp(tool_name, "cashu_wallet_balance") == 0) { return execute_cashu_wallet_balance(ctx, args_json); } diff --git a/src/tools/tools_internal.h b/src/tools/tools_internal.h index d15ed6a..44f481a 100644 --- a/src/tools/tools_internal.h +++ b/src/tools/tools_internal.h @@ -57,6 +57,9 @@ char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_json); char* execute_nostr_file_md_to_longform_post(tools_context_t* ctx, const char* args_json); char* execute_skill_create(tools_context_t* ctx, const char* args_json); char* execute_skill_list(tools_context_t* ctx, const char* args_json); +char* execute_skill_get(tools_context_t* ctx, const char* args_json); +char* execute_skill_view(tools_context_t* ctx, const char* args_json); +char* execute_skill_set(tools_context_t* ctx, const char* args_json); char* execute_skill_adopt(tools_context_t* ctx, const char* args_json); char* execute_skill_remove(tools_context_t* ctx, const char* args_json); char* execute_skill_edit(tools_context_t* ctx, const char* args_json); @@ -70,6 +73,7 @@ char* execute_config_store(tools_context_t* ctx, const char* args_json); char* execute_config_recall(tools_context_t* ctx, const char* args_json); char* execute_adopted_skills(tools_context_t* ctx, const char* args_json); char* execute_trigger_event(tools_context_t* ctx, const char* args_json); +char* execute_nostr_dm_history(tools_context_t* ctx, const char* args_json); char* execute_cashu_wallet_balance(tools_context_t* ctx, const char* args_json); char* execute_cashu_wallet_info(tools_context_t* ctx, const char* args_json); diff --git a/src/tools/tools_schema.c b/src/tools/tools_schema.c index 317275e..433d7ea 100644 --- a/src/tools/tools_schema.c +++ b/src/tools/tools_schema.c @@ -843,6 +843,89 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddItemToObject(t23, "function", t23_fn); cJSON_AddItemToArray(tools, t23); + cJSON* t24a = cJSON_CreateObject(); + cJSON* t24a_fn = cJSON_CreateObject(); + cJSON* t24a_params = cJSON_CreateObject(); + cJSON* t24a_props = cJSON_CreateObject(); + cJSON* t24a_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t24a, "type", "function"); + cJSON_AddStringToObject(t24a_fn, "name", "skill_get"); + cJSON_AddStringToObject(t24a_fn, "description", "Get full skill JSON by d tag from local cache; JSON: {\"d\":\"default_admin_dm\"} (slash shorthand also works: /skill_get default_admin_dm)"); + cJSON_AddStringToObject(t24a_params, "type", "object"); + cJSON_AddItemToObject(t24a_params, "properties", t24a_props); + cJSON_AddItemToObject(t24a_params, "required", t24a_required); + + cJSON* p_skill_get_d = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_get_d, "type", "string"); + cJSON_AddStringToObject(p_skill_get_d, "description", "Skill d tag (preferred JSON key)"); + cJSON_AddItemToObject(t24a_props, "d", p_skill_get_d); + + cJSON* p_skill_get_input = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_get_input, "type", "string"); + cJSON_AddStringToObject(p_skill_get_input, "description", "Slash shorthand fallback key when args are passed as plain text"); + cJSON_AddItemToObject(t24a_props, "input", p_skill_get_input); + + cJSON_AddItemToArray(t24a_required, cJSON_CreateString("d")); + + cJSON_AddItemToObject(t24a_fn, "parameters", t24a_params); + cJSON_AddItemToObject(t24a, "function", t24a_fn); + cJSON_AddItemToArray(tools, t24a); + + cJSON* t24b = cJSON_CreateObject(); + cJSON* t24b_fn = cJSON_CreateObject(); + cJSON* t24b_params = cJSON_CreateObject(); + cJSON* t24b_props = cJSON_CreateObject(); + cJSON* t24b_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t24b, "type", "function"); + cJSON_AddStringToObject(t24b_fn, "name", "skill_view"); + cJSON_AddStringToObject(t24b_fn, "description", "Compatibility alias for skill_get; JSON: {\"d\":\"default_admin_dm\"}"); + cJSON_AddStringToObject(t24b_params, "type", "object"); + cJSON_AddItemToObject(t24b_params, "properties", t24b_props); + cJSON_AddItemToObject(t24b_params, "required", t24b_required); + + cJSON* p_skill_view_d = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_view_d, "type", "string"); + cJSON_AddItemToObject(t24b_props, "d", p_skill_view_d); + + cJSON* p_skill_view_input = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_view_input, "type", "string"); + cJSON_AddItemToObject(t24b_props, "input", p_skill_view_input); + + cJSON_AddItemToArray(t24b_required, cJSON_CreateString("d")); + + cJSON_AddItemToObject(t24b_fn, "parameters", t24b_params); + cJSON_AddItemToObject(t24b, "function", t24b_fn); + cJSON_AddItemToArray(tools, t24b); + + cJSON* t24c = cJSON_CreateObject(); + cJSON* t24c_fn = cJSON_CreateObject(); + cJSON* t24c_params = cJSON_CreateObject(); + cJSON* t24c_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t24c, "type", "function"); + cJSON_AddStringToObject(t24c_fn, "name", "skill_set"); + cJSON_AddStringToObject(t24c_fn, + "description", + "Set/publish a full skill JSON payload and refresh runtime cache; JSON: {\"skill\":{\"d\":\"default_admin_dm\",\"kind\":31124,\"scope\":\"private\",\"content\":\"...\",\"tags\":[[\"d\",\"default_admin_dm\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"]]}}"); + cJSON_AddStringToObject(t24c_params, "type", "object"); + cJSON_AddItemToObject(t24c_params, "properties", t24c_props); + + cJSON* p_skill_set_skill = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_set_skill, "type", "object"); + cJSON_AddStringToObject(p_skill_set_skill, "description", "Full skill object from skill_get output content"); + cJSON_AddItemToObject(t24c_props, "skill", p_skill_set_skill); + + cJSON* p_skill_set_input = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_set_input, "type", "string"); + cJSON_AddStringToObject(p_skill_set_input, "description", "Raw JSON string for slash shorthand or manual paste"); + cJSON_AddItemToObject(t24c_props, "input", p_skill_set_input); + + cJSON_AddItemToObject(t24c_fn, "parameters", t24c_params); + cJSON_AddItemToObject(t24c, "function", t24c_fn); + cJSON_AddItemToArray(tools, t24c); + cJSON* t24 = cJSON_CreateObject(); cJSON* t24_fn = cJSON_CreateObject(); cJSON* t24_params = cJSON_CreateObject(); @@ -1395,6 +1478,38 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddItemToObject(t42, "function", t42_fn); cJSON_AddItemToArray(tools, t42); + cJSON* t43 = cJSON_CreateObject(); + cJSON* t43_fn = cJSON_CreateObject(); + cJSON* t43_params = cJSON_CreateObject(); + cJSON* t43_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t43, "type", "function"); + cJSON_AddStringToObject(t43_fn, "name", "nostr_dm_history"); + cJSON_AddStringToObject(t43_fn, + "description", + "Fetch recent DM history for template/context usage; JSON: {\"limit\":12,\"include_current\":false} or with explicit peer {\"peer_pubkey\":\"\",\"limit\":20}; template usage: {{nostr_dm_history({\"limit\":12})}}"); + cJSON_AddStringToObject(t43_params, "type", "object"); + cJSON_AddItemToObject(t43_params, "properties", t43_props); + + cJSON* p_dm_history_limit = cJSON_CreateObject(); + cJSON_AddStringToObject(p_dm_history_limit, "type", "integer"); + cJSON_AddStringToObject(p_dm_history_limit, "description", "Max message turns to return (1-200, default 12)"); + cJSON_AddItemToObject(t43_props, "limit", p_dm_history_limit); + + cJSON* p_dm_history_include_current = cJSON_CreateObject(); + cJSON_AddStringToObject(p_dm_history_include_current, "type", "boolean"); + cJSON_AddStringToObject(p_dm_history_include_current, "description", "When true, keep a trailing user message that matches the current live message"); + cJSON_AddItemToObject(t43_props, "include_current", p_dm_history_include_current); + + cJSON* p_dm_history_peer = cJSON_CreateObject(); + cJSON_AddStringToObject(p_dm_history_peer, "type", "string"); + cJSON_AddStringToObject(p_dm_history_peer, "description", "Optional peer pubkey hex (defaults to configured admin pubkey)"); + cJSON_AddItemToObject(t43_props, "peer_pubkey", p_dm_history_peer); + + cJSON_AddItemToObject(t43_fn, "parameters", t43_params); + cJSON_AddItemToObject(t43, "function", t43_fn); + cJSON_AddItemToArray(tools, t43); + cJSON* t44 = cJSON_CreateObject(); cJSON* t44_fn = cJSON_CreateObject(); cJSON* t44_params = cJSON_CreateObject();