diff --git a/README.md b/README.md index 159259a..e2ff71c 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,11 @@ Skills support context modes (`inject`, `full`, `override`) and per-skill LLM fa 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.0.66 +## Current Status — v0.0.67 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.0.66 — Complete tools refactor, move all tools-named sources/headers into src/tools, and update build wiring +> Last release update: v0.0.67 — Add agent self-context subscriptions, tools, aliases, and prompt template sections - 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/config.jsonc.example b/config.jsonc.example index 4f4eb39..1c3f493 100644 --- a/config.jsonc.example +++ b/config.jsonc.example @@ -31,6 +31,25 @@ "temperature": 0.7 // sampling temperature (0.0 – 2.0) }, + // ─── Tools & Runtime Limits ─────────────────────────────────────── + // Centralized turn and timeout limits used by DM agent loops, + // triggered skills, HTTP API prompt runs, and local HTTP fetch tool. + "tools": { + "enabled": true, + "max_turns": 8, // default max turns for normal agent tool loops + "trigger_max_turns": 6, // max turns for triggered-skill executions + "api_default_max_turns": 4, // default when HTTP body omits max_turns + "api_max_turns_ceiling": 16, // hard cap applied to API-provided max_turns + "local_http_fetch_default_timeout_seconds": 20, + "local_http_fetch_max_timeout_seconds": 120, + "shell": { + "enabled": true, + "timeout_seconds": 30, + "max_output_bytes": 65536, + "working_directory": "." + } + }, + // ─── Security Tiers ─────────────────────────────────────────────── // Controls who can interact with the agent and what they can do. "security": { diff --git a/context_template.md b/context_template.md index e9799ce..6bfc3f8 100644 --- a/context_template.md +++ b/context_template.md @@ -26,6 +26,31 @@ tool: nostr_admin_notes skip_if_empty: true +- section: agent_identity + role: system + tool: agent_identity + skip_if_empty: true + +- section: agent_profile + role: system + tool: nostr_agent_profile + skip_if_empty: true + +- section: agent_contacts + role: system + tool: nostr_agent_contacts + skip_if_empty: true + +- section: agent_relays + role: system + tool: nostr_agent_relays + skip_if_empty: true + +- section: agent_notes + role: system + tool: nostr_agent_notes + skip_if_empty: true + - section: tasks role: system tool: task_list diff --git a/plans/agent_self_context.md b/plans/agent_self_context.md new file mode 100644 index 0000000..ec1064f --- /dev/null +++ b/plans/agent_self_context.md @@ -0,0 +1,204 @@ +# Agent Self-Context: Know Thyself + +## Problem + +When the agent is asked about itself — its profile, contacts, relays, or recent notes — it has no idea. The context template currently injects **administrator** identity/profile/contacts/relays/notes into the system prompt, but nothing equivalent for the **agent's own** Nostr identity beyond a bare pubkey+npub from `agent_identity`. + +The agent publishes its own kind 0 (profile), kind 3 (contacts), kind 10002 (relays), and kind 1 (notes) at startup, but never subscribes to or caches those events for self-awareness. + +## Solution Overview + +Mirror the admin context pattern for the agent itself: + +1. **Subscribe** to the agent's own kind 0/3/10002/1 events from relays +2. **Cache** them in `nostr_handler.c` (same pattern as `g_admin_kind0_json` etc.) +3. **Expose** them via new `nostr_handler_get_agent_*` API functions +4. **Create context tools** that format the cached data into context blocks +5. **Add to context template** so the agent always knows about itself +6. **Add callable tool aliases** like `my_kind0_profile` for on-demand use + +```mermaid +flowchart TD + A[Startup: publish kind 0/3/10002/1] --> B[Subscribe to own events] + B --> C[Cache in nostr_handler globals] + C --> D[Context tools read cache] + D --> E[Context template injects into system prompt] + D --> F[LLM can call tools on-demand] + + subgraph Admin Context - existing + G[g_admin_kind0_json] + H[g_admin_kind3 contacts] + I[g_admin_kind10002_json] + J[g_admin_kind1_notes] + end + + subgraph Agent Context - new + K[g_agent_kind0_json] + L[g_agent_kind3 contacts] + M[g_agent_kind10002_json] + N[g_agent_kind1_notes] + end +``` + +## Detailed Changes + +### 1. nostr_handler.c — Agent Self-Context Cache + +Add new static globals mirroring the admin pattern: + +```c +static char* g_agent_kind0_json = NULL; // kind 0 profile JSON +static char* g_agent_kind10002_json = NULL; // kind 10002 relay list JSON +static char** g_agent_kind3_contacts = NULL; // kind 3 contact pubkeys +static int g_agent_kind3_contact_count = 0; +static admin_kind1_note_t* g_agent_kind1_notes = NULL; // reuse struct +static int g_agent_kind1_note_count = 0; +``` + +### 2. nostr_handler.c — Agent Self-Context Subscription + +Create `nostr_handler_subscribe_agent_context()` that subscribes to kinds 0, 3, 10002, 1 filtered by the agent's own pubkey. This is separate from the self-skills subscription (which handles 31123/31124/10123). + +The callback `on_agent_context_event()` will parse and cache events using the same logic as `on_admin_context_event()`. + +**Alternative considered:** Expanding `nostr_handler_subscribe_self_skills()` to include these kinds. Rejected because the self-skills sub has different EOSE handling and a callback for skill loading. Keeping them separate is cleaner. + +### 3. nostr_handler.h — New API Functions + +```c +int nostr_handler_subscribe_agent_context(void); +char* nostr_handler_get_agent_kind0_context(void); +char* nostr_handler_get_agent_kind3_context(void); +char* nostr_handler_get_agent_kind10002_context(void); +char* nostr_handler_get_agent_kind1_notes_context(void); +``` + +### 4. main.c — Call Agent Context Subscription at Startup + +Add `nostr_handler_subscribe_agent_context()` call after admin context subscription, before self-skills subscription. + +### 5. tool_agent.c — New Context Tools + +Create four new tool functions following the exact pattern from `tool_admin.c`: + +| Tool Name | Description | Data Source | +|-----------|-------------|-------------| +| `nostr_agent_profile` | Agent's kind 0 profile metadata | `nostr_handler_get_agent_kind0_context()` | +| `nostr_agent_contacts` | Agent's kind 3 contact list | `nostr_handler_get_agent_kind3_context()` | +| `nostr_agent_relays` | Agent's kind 10002 relay list | `nostr_handler_get_agent_kind10002_context()` | +| `nostr_agent_notes` | Agent's recent kind 1 notes | `nostr_handler_get_agent_kind1_notes_context()` | + +Each returns a `content` field with markdown-formatted context, e.g.: +``` +## Agent Kind 0 Profile (source: nostr kind 0) + +Agent kind 0 profile content (JSON): {"name":"Didactyl Agent","display_name":"Didactyl",...} +``` + +### 6. tools_internal.h — Declare New Functions + +```c +char* execute_nostr_agent_profile(tools_context_t* ctx, const char* args_json); +char* execute_nostr_agent_contacts(tools_context_t* ctx, const char* args_json); +char* execute_nostr_agent_relays(tools_context_t* ctx, const char* args_json); +char* execute_nostr_agent_notes(tools_context_t* ctx, const char* args_json); +``` + +### 7. tools_dispatch.c — Register + Aliases + +Add dispatch entries: +```c +if (strcmp(tool_name, "nostr_agent_profile") == 0) return execute_nostr_agent_profile(ctx, args_json); +if (strcmp(tool_name, "nostr_agent_contacts") == 0) return execute_nostr_agent_contacts(ctx, args_json); +if (strcmp(tool_name, "nostr_agent_relays") == 0) return execute_nostr_agent_relays(ctx, args_json); +if (strcmp(tool_name, "nostr_agent_notes") == 0) return execute_nostr_agent_notes(ctx, args_json); + +// Friendly aliases +if (strcmp(tool_name, "my_kind0_profile") == 0) return execute_nostr_agent_profile(ctx, args_json); +if (strcmp(tool_name, "my_contacts") == 0) return execute_nostr_agent_contacts(ctx, args_json); +if (strcmp(tool_name, "my_relays") == 0) return execute_nostr_agent_relays(ctx, args_json); +if (strcmp(tool_name, "my_notes") == 0) return execute_nostr_agent_notes(ctx, args_json); +``` + +### 8. tools_schema.c — OpenAI Function Schemas + +Add 8 new tool schemas (4 canonical + 4 aliases), all with empty parameters (no-arg tools), following the pattern of `admin_identity`/`nostr_admin_profile` etc. + +### 9. Context Template Update + +Update the kind 31120 soul content in `config.jsonc` and `context_template.md` to add agent sections **after** admin sections: + +```yaml +- section: admin_notes + role: system + tool: nostr_admin_notes + skip_if_empty: true + +# NEW: Agent self-context sections +- section: agent_identity + role: system + tool: agent_identity + skip_if_empty: true + +- section: agent_profile + role: system + tool: nostr_agent_profile + skip_if_empty: true + +- section: agent_contacts + role: system + tool: nostr_agent_contacts + skip_if_empty: true + +- section: agent_relays + role: system + tool: nostr_agent_relays + skip_if_empty: true + +- section: agent_notes + role: system + tool: nostr_agent_notes + skip_if_empty: true + +- section: tasks + role: system + tool: task_list + skip_if_empty: true +``` + +### 10. nostr_handler.c Cleanup + +Add cleanup for agent context globals in `nostr_handler_cleanup()`, mirroring `free_admin_context_locked()`. + +## File Change Summary + +| File | Change Type | Description | +|------|-------------|-------------| +| `src/nostr_handler.h` | Modify | Add 5 new function declarations | +| `src/nostr_handler.c` | Modify | Add agent context cache globals, subscription, event handler, getter functions, cleanup | +| `src/main.c` | Modify | Call `nostr_handler_subscribe_agent_context()` at startup | +| `src/tools/tool_agent.c` | Modify | Add 4 new context tool execute functions | +| `src/tools/tools_internal.h` | Modify | Declare 4 new execute functions | +| `src/tools/tools_dispatch.c` | Modify | Add 8 dispatch entries (4 tools + 4 aliases) | +| `src/tools/tools_schema.c` | Modify | Add 8 OpenAI function schemas | +| `config.jsonc` | Modify | Update kind 31120 template section | +| `config.jsonc.example` | Modify | Update kind 31120 template section | +| `context_template.md` | Modify | Add agent_* sections after admin_* sections | + +## Context Token Impact + +Each agent context section adds roughly the same token count as its admin counterpart: +- agent_identity: ~40 tokens (already exists, just adding to template) +- agent_profile: ~80-150 tokens (depends on profile richness) +- agent_contacts: ~50-200 tokens (depends on contact count) +- agent_relays: ~50-100 tokens +- agent_notes: ~100-300 tokens (depends on note count/length) + +Total additional context: ~320-790 tokens. With `skip_if_empty: true`, empty sections cost 0 tokens. + +## Design Decisions + +1. **Separate subscription vs expanding self-skills sub**: Separate is cleaner — different EOSE semantics, different callback needs. +2. **Cache from relay vs read from config**: Cache from relay is more accurate (reflects what's actually published, not just what config says). The startup_events in config are the *intent*; the relay data is the *reality*. +3. **Alias naming**: `my_kind0_profile` matches the existing `my_npub`/`my_pubkey` pattern. Also adding `my_contacts`, `my_relays`, `my_notes` for consistency. +4. **No new config section needed**: The agent context subscription is unconditional — an agent should always know about itself. No `agent_context.enabled` toggle needed. diff --git a/src/agent.c b/src/agent.c index e4a6928..7481413 100644 --- a/src/agent.c +++ b/src/agent.c @@ -1987,7 +1987,9 @@ void agent_on_trigger(const char* skill_d_tag, free(system_prompt); free(user_prompt); - int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 6; + int max_turns = g_cfg->tools.trigger_max_turns > 0 + ? g_cfg->tools.trigger_max_turns + : (g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8); for (int turn = 0; turn < max_turns; turn++) { char* messages_json = cJSON_PrintUnformatted(messages); diff --git a/src/config.c b/src/config.c index 5fa1328..e7c6151 100644 --- a/src/config.c +++ b/src/config.c @@ -222,12 +222,36 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) { cJSON* enabled = cJSON_GetObjectItemCaseSensitive(tools, "enabled"); cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(tools, "max_turns"); + cJSON* trigger_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "trigger_max_turns"); + cJSON* api_default_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "api_default_max_turns"); + cJSON* api_max_turns_ceiling = cJSON_GetObjectItemCaseSensitive(tools, "api_max_turns_ceiling"); + cJSON* local_http_fetch_default_timeout_seconds = + cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_default_timeout_seconds"); + cJSON* local_http_fetch_max_timeout_seconds = + cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_max_timeout_seconds"); if (enabled && cJSON_IsBool(enabled)) { config->tools.enabled = cJSON_IsTrue(enabled) ? 1 : 0; } if (max_turns && cJSON_IsNumber(max_turns)) { config->tools.max_turns = (int)max_turns->valuedouble; } + if (trigger_max_turns && cJSON_IsNumber(trigger_max_turns)) { + config->tools.trigger_max_turns = (int)trigger_max_turns->valuedouble; + } + if (api_default_max_turns && cJSON_IsNumber(api_default_max_turns)) { + config->tools.api_default_max_turns = (int)api_default_max_turns->valuedouble; + } + if (api_max_turns_ceiling && cJSON_IsNumber(api_max_turns_ceiling)) { + config->tools.api_max_turns_ceiling = (int)api_max_turns_ceiling->valuedouble; + } + if (local_http_fetch_default_timeout_seconds && cJSON_IsNumber(local_http_fetch_default_timeout_seconds)) { + config->tools.local_http_fetch_default_timeout_seconds = + (int)local_http_fetch_default_timeout_seconds->valuedouble; + } + if (local_http_fetch_max_timeout_seconds && cJSON_IsNumber(local_http_fetch_max_timeout_seconds)) { + config->tools.local_http_fetch_max_timeout_seconds = + (int)local_http_fetch_max_timeout_seconds->valuedouble; + } cJSON* shell = cJSON_GetObjectItemCaseSensitive(tools, "shell"); if (!shell || !cJSON_IsObject(shell)) { @@ -255,6 +279,31 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) { return -1; } + if (config->tools.max_turns < 1) { + config->tools.max_turns = 8; + } + if (config->tools.trigger_max_turns < 1) { + config->tools.trigger_max_turns = config->tools.max_turns; + } + if (config->tools.api_default_max_turns < 1) { + config->tools.api_default_max_turns = config->tools.max_turns; + } + if (config->tools.api_max_turns_ceiling < 1) { + config->tools.api_max_turns_ceiling = 16; + } + if (config->tools.api_default_max_turns > config->tools.api_max_turns_ceiling) { + config->tools.api_default_max_turns = config->tools.api_max_turns_ceiling; + } + if (config->tools.local_http_fetch_default_timeout_seconds < 1) { + config->tools.local_http_fetch_default_timeout_seconds = 20; + } + if (config->tools.local_http_fetch_max_timeout_seconds < 1) { + config->tools.local_http_fetch_max_timeout_seconds = 120; + } + if (config->tools.local_http_fetch_default_timeout_seconds > config->tools.local_http_fetch_max_timeout_seconds) { + config->tools.local_http_fetch_default_timeout_seconds = config->tools.local_http_fetch_max_timeout_seconds; + } + return 0; } @@ -746,6 +795,11 @@ int config_load(const char* path, didactyl_config_t* config) { config->tools.enabled = 1; config->tools.max_turns = 8; + config->tools.trigger_max_turns = 6; + config->tools.api_default_max_turns = 4; + config->tools.api_max_turns_ceiling = 16; + config->tools.local_http_fetch_default_timeout_seconds = 20; + config->tools.local_http_fetch_max_timeout_seconds = 120; config->tools.shell.enabled = 1; config->tools.shell.timeout_seconds = 30; config->tools.shell.max_output_bytes = 65536; diff --git a/src/config.h b/src/config.h index b8bb87f..4e9ac4b 100644 --- a/src/config.h +++ b/src/config.h @@ -47,6 +47,11 @@ typedef struct { typedef struct { int enabled; int max_turns; + int trigger_max_turns; + int api_default_max_turns; + int api_max_turns_ceiling; + int local_http_fetch_default_timeout_seconds; + int local_http_fetch_max_timeout_seconds; shell_tools_config_t shell; } tools_config_t; diff --git a/src/http_api.c b/src/http_api.c index d799bb7..ef6b8a8 100644 --- a/src/http_api.c +++ b/src/http_api.c @@ -755,8 +755,13 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo, const char* tool_limit_message) { if (!convo || !cJSON_IsArray(convo)) return NULL; + int max_ceiling = 16; + if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_max_turns_ceiling > 0) { + max_ceiling = g_api_ctx.cfg->tools.api_max_turns_ceiling; + } + if (max_turns < 1) max_turns = 1; - if (max_turns > 16) max_turns = 16; + if (max_turns > max_ceiling) max_turns = max_ceiling; char* tools_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx); if (!tools_json) { @@ -888,6 +893,9 @@ static cJSON* run_prompt_with_tools(cJSON* body) { } int max_turns = 4; + if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) { + max_turns = g_api_ctx.cfg->tools.api_default_max_turns; + } cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns"); if (max_turns_node && cJSON_IsNumber(max_turns_node)) { max_turns = (int)max_turns_node->valuedouble; @@ -1104,6 +1112,9 @@ static void handle_prompt_agent(struct mg_connection* c, const struct mg_http_me } int max_turns = 4; + if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) { + max_turns = g_api_ctx.cfg->tools.api_default_max_turns; + } cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns"); if (max_turns_node && cJSON_IsNumber(max_turns_node)) { max_turns = (int)max_turns_node->valuedouble; diff --git a/src/main.c b/src/main.c index c0bfd15..ebee1ab 100644 --- a/src/main.c +++ b/src/main.c @@ -284,6 +284,12 @@ int main(int argc, char** argv) { } DEBUG_INFO("[didactyl] startup phase: subscribe admin context end"); + DEBUG_INFO("[didactyl] startup phase: subscribe agent self context begin"); + if (nostr_handler_subscribe_agent_context() != 0) { + DEBUG_WARN("[didactyl] startup phase: subscribe agent self context failed (continuing)"); + } + DEBUG_INFO("[didactyl] startup phase: subscribe agent self context end"); + DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin"); if (nostr_handler_subscribe_self_skills() != 0) { DEBUG_WARN("[didactyl] startup phase: subscribe self skill cache failed (continuing)"); diff --git a/src/main.h b/src/main.h index 7c129f0..15d8459 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 0 -#define DIDACTYL_VERSION_PATCH 66 -#define DIDACTYL_VERSION "v0.0.66" +#define DIDACTYL_VERSION_PATCH 67 +#define DIDACTYL_VERSION "v0.0.67" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/nostr_handler.c b/src/nostr_handler.c index 7a6ce70..8bcd790 100644 --- a/src/nostr_handler.c +++ b/src/nostr_handler.c @@ -42,6 +42,13 @@ typedef struct { static admin_kind1_note_t* g_admin_kind1_notes = NULL; static int g_admin_kind1_note_count = 0; +static char* g_agent_kind0_json = NULL; +static char* g_agent_kind10002_json = NULL; +static char** g_agent_contacts = NULL; +static int g_agent_contact_count = 0; +static admin_kind1_note_t* g_agent_kind1_notes = NULL; +static int g_agent_kind1_note_count = 0; + static pthread_mutex_t g_admin_ctx_mutex = PTHREAD_MUTEX_INITIALIZER; static cJSON* g_self_skill_events = NULL; @@ -269,6 +276,11 @@ static void on_admin_context_event(cJSON* event, const char* relay_url, void* us static int parse_kind3_wot_contacts(cJSON* tags); static int parse_kind10002_relays(cJSON* tags); static void upsert_kind1_note(time_t created_at, const char* content); +static void free_agent_context_locked(void); +static int parse_kind3_agent_contacts(cJSON* tags); +static int parse_kind10002_agent_relays(cJSON* tags); +static void upsert_agent_kind1_note(time_t created_at, const char* content); +static void on_agent_context_event(cJSON* event, const char* relay_url, void* user_data); static int startup_self_kind1_exists(void); static void load_startup_display_name(void); static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback); @@ -1114,6 +1126,31 @@ static void free_admin_context_locked(void) { g_admin_kind1_note_count = 0; } +static void free_agent_context_locked(void) { + free(g_agent_kind0_json); + g_agent_kind0_json = NULL; + free(g_agent_kind10002_json); + g_agent_kind10002_json = NULL; + + if (g_agent_contacts) { + for (int i = 0; i < g_agent_contact_count; i++) { + free(g_agent_contacts[i]); + } + free(g_agent_contacts); + } + g_agent_contacts = NULL; + g_agent_contact_count = 0; + + if (g_agent_kind1_notes) { + for (int i = 0; i < g_agent_kind1_note_count; i++) { + free(g_agent_kind1_notes[i].content); + } + free(g_agent_kind1_notes); + } + g_agent_kind1_notes = NULL; + g_agent_kind1_note_count = 0; +} + static int parse_kind3_wot_contacts(cJSON* tags) { if (!tags || !cJSON_IsArray(tags)) { return 0; @@ -1242,6 +1279,134 @@ static void upsert_kind1_note(time_t created_at, const char* content) { } } +static int parse_kind3_agent_contacts(cJSON* tags) { + if (!tags || !cJSON_IsArray(tags)) { + return 0; + } + + if (g_agent_contacts) { + for (int i = 0; i < g_agent_contact_count; i++) { + free(g_agent_contacts[i]); + } + free(g_agent_contacts); + g_agent_contacts = NULL; + g_agent_contact_count = 0; + } + + 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* key = cJSON_GetArrayItem(tag, 0); + cJSON* val = cJSON_GetArrayItem(tag, 1); + if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) { + continue; + } + + if (strcmp(key->valuestring, "p") != 0 || strlen(val->valuestring) != 64U) { + continue; + } + + char* dup = strdup(val->valuestring); + if (!dup) { + return -1; + } + + char** grown = (char**)realloc(g_agent_contacts, (size_t)(g_agent_contact_count + 1) * sizeof(char*)); + if (!grown) { + free(dup); + return -1; + } + + g_agent_contacts = grown; + g_agent_contacts[g_agent_contact_count++] = dup; + } + + return 0; +} + +static int parse_kind10002_agent_relays(cJSON* tags) { + if (!tags || !cJSON_IsArray(tags)) { + free(g_agent_kind10002_json); + g_agent_kind10002_json = strdup("[]"); + return g_agent_kind10002_json ? 0 : -1; + } + + cJSON* relays = cJSON_CreateArray(); + if (!relays) { + 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* key = cJSON_GetArrayItem(tag, 0); + cJSON* val = cJSON_GetArrayItem(tag, 1); + if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) { + continue; + } + + if (strcmp(key->valuestring, "r") != 0 || val->valuestring[0] == '\0') { + continue; + } + + cJSON_AddItemToArray(relays, cJSON_CreateString(val->valuestring)); + } + + char* relays_json = cJSON_PrintUnformatted(relays); + cJSON_Delete(relays); + if (!relays_json) { + return -1; + } + + free(g_agent_kind10002_json); + g_agent_kind10002_json = relays_json; + return 0; +} + +static void upsert_agent_kind1_note(time_t created_at, const char* content) { + if (!content) { + return; + } + + int limit = g_cfg && g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10; + if (limit > 256) { + limit = 256; + } + + char* dup = strdup(content); + if (!dup) { + return; + } + + admin_kind1_note_t* grown = (admin_kind1_note_t*)realloc(g_agent_kind1_notes, + (size_t)(g_agent_kind1_note_count + 1) * sizeof(admin_kind1_note_t)); + if (!grown) { + free(dup); + return; + } + + g_agent_kind1_notes = grown; + g_agent_kind1_notes[g_agent_kind1_note_count].created_at = created_at; + g_agent_kind1_notes[g_agent_kind1_note_count].content = dup; + g_agent_kind1_note_count++; + + while (g_agent_kind1_note_count > limit) { + free(g_agent_kind1_notes[0].content); + memmove(&g_agent_kind1_notes[0], + &g_agent_kind1_notes[1], + (size_t)(g_agent_kind1_note_count - 1) * sizeof(admin_kind1_note_t)); + g_agent_kind1_note_count--; + } +} + static void on_admin_context_event(cJSON* event, const char* relay_url, void* user_data) { (void)relay_url; (void)user_data; @@ -1287,6 +1452,51 @@ static void on_admin_context_event(cJSON* event, const char* relay_url, void* us pthread_mutex_unlock(&g_admin_ctx_mutex); } +static void on_agent_context_event(cJSON* event, const char* relay_url, void* user_data) { + (void)relay_url; + (void)user_data; + + if (!event || !g_cfg) { + return; + } + + if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) { + return; + } + + cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); + cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey"); + cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content"); + cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); + cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at"); + + if (!kind || !pubkey || !cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) || !pubkey->valuestring) { + return; + } + + if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) { + return; + } + + int k = (int)kind->valuedouble; + + pthread_mutex_lock(&g_admin_ctx_mutex); + + if (k == 0 && content && cJSON_IsString(content) && content->valuestring) { + free(g_agent_kind0_json); + g_agent_kind0_json = strdup(content->valuestring); + } else if (k == 3 && tags && cJSON_IsArray(tags)) { + (void)parse_kind3_agent_contacts(tags); + } else if (k == 10002 && tags && cJSON_IsArray(tags)) { + (void)parse_kind10002_agent_relays(tags); + } else if (k == 1 && content && cJSON_IsString(content) && content->valuestring) { + time_t ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : time(NULL); + upsert_agent_kind1_note(ts, content->valuestring); + } + + pthread_mutex_unlock(&g_admin_ctx_mutex); +} + static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data) { (void)relay_url; (void)user_data; @@ -1469,6 +1679,96 @@ int nostr_handler_subscribe_admin_context(void) { return rc; } +int nostr_handler_subscribe_agent_context(void) { + if (!g_cfg || !g_pool) { + return -1; + } + + int rc = 0; + + cJSON* profile_filter = cJSON_CreateObject(); + cJSON* profile_kinds = cJSON_CreateArray(); + cJSON* profile_authors = cJSON_CreateArray(); + if (!profile_filter || !profile_kinds || !profile_authors) { + cJSON_Delete(profile_filter); + cJSON_Delete(profile_kinds); + cJSON_Delete(profile_authors); + return -1; + } + + cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(0)); + cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(3)); + cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(10002)); + cJSON_AddItemToObject(profile_filter, "kinds", profile_kinds); + cJSON_AddItemToArray(profile_authors, cJSON_CreateString(g_cfg->keys.public_key_hex)); + cJSON_AddItemToObject(profile_filter, "authors", profile_authors); + cJSON_AddNumberToObject(profile_filter, "limit", 64); + + nostr_pool_subscription_t* profile_sub = nostr_relay_pool_subscribe( + g_pool, + (const char**)g_cfg->relays, + g_cfg->relay_count, + profile_filter, + on_agent_context_event, + on_eose, + NULL, + 0, + 1, + NOSTR_POOL_EOSE_FULL_SET, + 30, + 120); + + cJSON_Delete(profile_filter); + if (!profile_sub) { + rc = -1; + } + + cJSON* notes_filter = cJSON_CreateObject(); + cJSON* notes_kinds = cJSON_CreateArray(); + cJSON* notes_authors = cJSON_CreateArray(); + if (!notes_filter || !notes_kinds || !notes_authors) { + cJSON_Delete(notes_filter); + cJSON_Delete(notes_kinds); + cJSON_Delete(notes_authors); + return -1; + } + + int kind1_limit = g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10; + if (kind1_limit > 256) { + kind1_limit = 256; + } + + cJSON_AddItemToArray(notes_kinds, cJSON_CreateNumber(1)); + cJSON_AddItemToObject(notes_filter, "kinds", notes_kinds); + cJSON_AddItemToArray(notes_authors, cJSON_CreateString(g_cfg->keys.public_key_hex)); + cJSON_AddItemToObject(notes_filter, "authors", notes_authors); + cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit); + + nostr_pool_subscription_t* notes_sub = nostr_relay_pool_subscribe( + g_pool, + (const char**)g_cfg->relays, + g_cfg->relay_count, + notes_filter, + on_agent_context_event, + on_eose, + NULL, + 0, + 1, + NOSTR_POOL_EOSE_FULL_SET, + 30, + 120); + + cJSON_Delete(notes_filter); + if (!notes_sub) { + rc = -1; + } + + if (rc == 0) { + DEBUG_INFO("[didactyl] agent self-context subscriptions active for pubkey %.16s...", g_cfg->keys.public_key_hex); + } + return rc; +} + void nostr_handler_set_trigger_manager(struct trigger_manager* trigger_manager) { g_trigger_manager = trigger_manager; } @@ -2603,6 +2903,89 @@ char* nostr_handler_get_admin_kind1_notes_context(void) { return out; } +char* nostr_handler_get_agent_kind0_context(void) { + if (!g_cfg) { + return NULL; + } + + pthread_mutex_lock(&g_admin_ctx_mutex); + char* out = g_agent_kind0_json ? strdup(g_agent_kind0_json) : NULL; + pthread_mutex_unlock(&g_admin_ctx_mutex); + return out; +} + +char* nostr_handler_get_agent_kind3_context(void) { + if (!g_cfg) { + return NULL; + } + + pthread_mutex_lock(&g_admin_ctx_mutex); + + cJSON* out = cJSON_CreateArray(); + if (!out) { + pthread_mutex_unlock(&g_admin_ctx_mutex); + return NULL; + } + + for (int i = 0; i < g_agent_contact_count; i++) { + const char* pk = g_agent_contacts[i]; + if (!pk || pk[0] == '\0') continue; + cJSON_AddItemToArray(out, cJSON_CreateString(pk)); + } + + pthread_mutex_unlock(&g_admin_ctx_mutex); + + char* out_json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + return out_json; +} + +char* nostr_handler_get_agent_kind10002_context(void) { + if (!g_cfg) { + return NULL; + } + + pthread_mutex_lock(&g_admin_ctx_mutex); + char* out = g_agent_kind10002_json ? strdup(g_agent_kind10002_json) : NULL; + pthread_mutex_unlock(&g_admin_ctx_mutex); + return out; +} + +char* nostr_handler_get_agent_kind1_notes_context(void) { + if (!g_cfg) { + return NULL; + } + + pthread_mutex_lock(&g_admin_ctx_mutex); + + if (g_agent_kind1_note_count <= 0 || !g_agent_kind1_notes) { + pthread_mutex_unlock(&g_admin_ctx_mutex); + return NULL; + } + + size_t total = strlen("Agent recent public notes:\n") + 1U; + for (int i = 0; i < g_agent_kind1_note_count; i++) { + total += strlen("- ") + strlen(g_agent_kind1_notes[i].content ? g_agent_kind1_notes[i].content : "") + 1U; + } + + char* out = (char*)malloc(total); + if (!out) { + pthread_mutex_unlock(&g_admin_ctx_mutex); + return NULL; + } + + out[0] = '\0'; + strcat(out, "Agent recent public notes:\n"); + for (int i = 0; i < g_agent_kind1_note_count; i++) { + strcat(out, "- "); + strcat(out, g_agent_kind1_notes[i].content ? g_agent_kind1_notes[i].content : ""); + strcat(out, "\n"); + } + + pthread_mutex_unlock(&g_admin_ctx_mutex); + return out; +} + int nostr_handler_is_wot_contact(const char* pubkey_hex) { if (!pubkey_hex || strlen(pubkey_hex) != 64U) { return 0; @@ -2695,6 +3078,7 @@ void nostr_handler_cleanup(void) { pthread_mutex_lock(&g_admin_ctx_mutex); free_admin_context_locked(); + free_agent_context_locked(); pthread_mutex_unlock(&g_admin_ctx_mutex); pthread_mutex_lock(&g_self_skill_mutex); diff --git a/src/nostr_handler.h b/src/nostr_handler.h index 05485e3..15318be 100644 --- a/src/nostr_handler.h +++ b/src/nostr_handler.h @@ -35,6 +35,7 @@ typedef void (*nostr_self_skill_eose_cb_t)(int event_count, void* user_data); int nostr_handler_init(didactyl_config_t* config); void nostr_handler_set_trigger_manager(struct trigger_manager* trigger_manager); int nostr_handler_subscribe_admin_context(void); +int nostr_handler_subscribe_agent_context(void); int nostr_handler_subscribe_self_skills(void); char* nostr_handler_get_self_events_by_kind_json(int kind); void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callback, void* user_data); @@ -66,6 +67,10 @@ char* nostr_handler_get_admin_kind0_context(void); char* nostr_handler_get_admin_kind3_context(void); char* nostr_handler_get_admin_kind10002_context(void); char* nostr_handler_get_admin_kind1_notes_context(void); +char* nostr_handler_get_agent_kind0_context(void); +char* nostr_handler_get_agent_kind3_context(void); +char* nostr_handler_get_agent_kind10002_context(void); +char* nostr_handler_get_agent_kind1_notes_context(void); int nostr_handler_is_wot_contact(const char* pubkey_hex); char* nostr_handler_relay_status_json(void); char* nostr_handler_relay_info_json(const char* relay_url); diff --git a/src/tools/tool_agent.c b/src/tools/tool_agent.c index 184f00b..8a17e4d 100644 --- a/src/tools/tool_agent.c +++ b/src/tools/tool_agent.c @@ -3,10 +3,12 @@ #include "tools_internal.h" #include +#include #include #include "cjson/cJSON.h" #include "../main.h" +#include "../nostr_handler.h" #include "../../nostr_core_lib/nostr_core/nostr_core.h" static char* json_error_local(const char* msg) { @@ -76,6 +78,162 @@ char* execute_agent_identity(tools_context_t* ctx, const char* args_json) { return json; } +char* execute_nostr_agent_profile(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_Delete(args); + + char* kind0 = nostr_handler_get_agent_kind0_context(); + const char* profile_json = (kind0 && kind0[0]) ? kind0 : "{}"; + + cJSON* out = cJSON_CreateObject(); + if (!out) { + free(kind0); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddStringToObject(out, "agent_kind0_json", profile_json); + + 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); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + free(kind0); + return json; +} + +char* execute_nostr_agent_contacts(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_Delete(args); + + char* kind3 = nostr_handler_get_agent_kind3_context(); + const char* contacts_json = (kind3 && kind3[0]) ? kind3 : "[]"; + + cJSON* out = cJSON_CreateObject(); + if (!out) { + free(kind3); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddStringToObject(out, "agent_kind3_json", contacts_json); + + 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); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + free(kind3); + return json; +} + +char* execute_nostr_agent_relays(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_Delete(args); + + char* kind10002 = nostr_handler_get_agent_kind10002_context(); + const char* relays_json = (kind10002 && kind10002[0]) ? kind10002 : "[]"; + + cJSON* out = cJSON_CreateObject(); + if (!out) { + free(kind10002); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddStringToObject(out, "agent_kind10002_json", relays_json); + + 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); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + free(kind10002); + return json; +} + +char* execute_nostr_agent_notes(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_Delete(args); + + char* notes = nostr_handler_get_agent_kind1_notes_context(); + const char* notes_text = (notes && notes[0]) ? notes : ""; + + cJSON* out = cJSON_CreateObject(); + if (!out) { + free(notes); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddStringToObject(out, "agent_notes_content", notes_text); + + 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); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + free(notes); + 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_local.c b/src/tools/tool_local.c index 6e6f692..ba38c57 100644 --- a/src/tools/tool_local.c +++ b/src/tools/tool_local.c @@ -194,9 +194,19 @@ char* execute_local_http_fetch(tools_context_t* ctx, const char* args_json) { return json_error_local("local_http_fetch GET requests cannot include body"); } - int timeout_seconds = (timeout && cJSON_IsNumber(timeout)) ? (int)timeout->valuedouble : 20; - if (timeout_seconds <= 0) timeout_seconds = 20; - if (timeout_seconds > 120) timeout_seconds = 120; + int default_timeout = ctx->cfg->tools.local_http_fetch_default_timeout_seconds > 0 + ? ctx->cfg->tools.local_http_fetch_default_timeout_seconds + : 20; + int max_timeout = ctx->cfg->tools.local_http_fetch_max_timeout_seconds > 0 + ? ctx->cfg->tools.local_http_fetch_max_timeout_seconds + : 120; + if (default_timeout > max_timeout) { + default_timeout = max_timeout; + } + + int timeout_seconds = (timeout && cJSON_IsNumber(timeout)) ? (int)timeout->valuedouble : default_timeout; + if (timeout_seconds <= 0) timeout_seconds = default_timeout; + if (timeout_seconds > max_timeout) timeout_seconds = max_timeout; int hard_max = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536; int max_bytes = (maxb && cJSON_IsNumber(maxb)) ? (int)maxb->valuedouble : hard_max; diff --git a/src/tools/tools_dispatch.c b/src/tools/tools_dispatch.c index cb65ed4..8488fb3 100644 --- a/src/tools/tools_dispatch.c +++ b/src/tools/tools_dispatch.c @@ -174,6 +174,30 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch if (strcmp(tool_name, "agent_identity") == 0) { return execute_agent_identity(ctx, args_json); } + if (strcmp(tool_name, "nostr_agent_profile") == 0) { + return execute_nostr_agent_profile(ctx, args_json); + } + if (strcmp(tool_name, "nostr_agent_contacts") == 0) { + return execute_nostr_agent_contacts(ctx, args_json); + } + if (strcmp(tool_name, "nostr_agent_relays") == 0) { + return execute_nostr_agent_relays(ctx, args_json); + } + if (strcmp(tool_name, "nostr_agent_notes") == 0) { + return execute_nostr_agent_notes(ctx, args_json); + } + if (strcmp(tool_name, "my_kind0_profile") == 0) { + return execute_nostr_agent_profile(ctx, args_json); + } + if (strcmp(tool_name, "my_contacts") == 0) { + return execute_nostr_agent_contacts(ctx, args_json); + } + if (strcmp(tool_name, "my_relays") == 0) { + return execute_nostr_agent_relays(ctx, args_json); + } + if (strcmp(tool_name, "my_notes") == 0) { + return execute_nostr_agent_notes(ctx, args_json); + } return json_error("unknown tool"); } diff --git a/src/tools/tools_internal.h b/src/tools/tools_internal.h index 9306abb..fa53854 100644 --- a/src/tools/tools_internal.h +++ b/src/tools/tools_internal.h @@ -13,6 +13,10 @@ char* execute_trigger_list(tools_context_t* ctx, const char* args_json); char* execute_message_current(tools_context_t* ctx, const char* args_json); char* execute_agent_identity(tools_context_t* ctx, const char* args_json); char* execute_agent_version(const char* args_json); +char* execute_nostr_agent_profile(tools_context_t* ctx, const char* args_json); +char* execute_nostr_agent_contacts(tools_context_t* ctx, const char* args_json); +char* execute_nostr_agent_relays(tools_context_t* ctx, const char* args_json); +char* execute_nostr_agent_notes(tools_context_t* ctx, const char* args_json); char* execute_model_get(const char* args_json); char* execute_model_set(tools_context_t* ctx, const char* args_json); char* execute_model_list(const char* args_json); diff --git a/src/tools/tools_schema.c b/src/tools/tools_schema.c index 39044b3..a435224 100644 --- a/src/tools/tools_schema.c +++ b/src/tools/tools_schema.c @@ -1066,6 +1066,118 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddItemToObject(t41, "function", t41_fn); cJSON_AddItemToArray(tools, t41); + cJSON* t41b = cJSON_CreateObject(); + cJSON* t41b_fn = cJSON_CreateObject(); + cJSON* t41b_params = cJSON_CreateObject(); + cJSON* t41b_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t41b, "type", "function"); + cJSON_AddStringToObject(t41b_fn, "name", "nostr_agent_profile"); + cJSON_AddStringToObject(t41b_fn, "description", "Build agent profile context block from cached kind 0 metadata"); + cJSON_AddStringToObject(t41b_params, "type", "object"); + cJSON_AddItemToObject(t41b_params, "properties", t41b_props); + cJSON_AddItemToObject(t41b_fn, "parameters", t41b_params); + cJSON_AddItemToObject(t41b, "function", t41b_fn); + cJSON_AddItemToArray(tools, t41b); + + cJSON* t41c = cJSON_CreateObject(); + cJSON* t41c_fn = cJSON_CreateObject(); + cJSON* t41c_params = cJSON_CreateObject(); + cJSON* t41c_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t41c, "type", "function"); + cJSON_AddStringToObject(t41c_fn, "name", "nostr_agent_contacts"); + cJSON_AddStringToObject(t41c_fn, "description", "Build agent contacts context block from cached kind 3 contact list"); + cJSON_AddStringToObject(t41c_params, "type", "object"); + cJSON_AddItemToObject(t41c_params, "properties", t41c_props); + cJSON_AddItemToObject(t41c_fn, "parameters", t41c_params); + cJSON_AddItemToObject(t41c, "function", t41c_fn); + cJSON_AddItemToArray(tools, t41c); + + cJSON* t41d = cJSON_CreateObject(); + cJSON* t41d_fn = cJSON_CreateObject(); + cJSON* t41d_params = cJSON_CreateObject(); + cJSON* t41d_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t41d, "type", "function"); + cJSON_AddStringToObject(t41d_fn, "name", "nostr_agent_relays"); + cJSON_AddStringToObject(t41d_fn, "description", "Build agent relay context block from cached kind 10002 data"); + cJSON_AddStringToObject(t41d_params, "type", "object"); + cJSON_AddItemToObject(t41d_params, "properties", t41d_props); + cJSON_AddItemToObject(t41d_fn, "parameters", t41d_params); + cJSON_AddItemToObject(t41d, "function", t41d_fn); + cJSON_AddItemToArray(tools, t41d); + + cJSON* t41e = cJSON_CreateObject(); + cJSON* t41e_fn = cJSON_CreateObject(); + cJSON* t41e_params = cJSON_CreateObject(); + cJSON* t41e_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t41e, "type", "function"); + cJSON_AddStringToObject(t41e_fn, "name", "nostr_agent_notes"); + cJSON_AddStringToObject(t41e_fn, "description", "Build agent notes context block from cached kind 1 notes"); + cJSON_AddStringToObject(t41e_params, "type", "object"); + cJSON_AddItemToObject(t41e_params, "properties", t41e_props); + cJSON_AddItemToObject(t41e_fn, "parameters", t41e_params); + cJSON_AddItemToObject(t41e, "function", t41e_fn); + cJSON_AddItemToArray(tools, t41e); + + cJSON* t41f = cJSON_CreateObject(); + cJSON* t41f_fn = cJSON_CreateObject(); + cJSON* t41f_params = cJSON_CreateObject(); + cJSON* t41f_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t41f, "type", "function"); + cJSON_AddStringToObject(t41f_fn, "name", "my_kind0_profile"); + cJSON_AddStringToObject(t41f_fn, "description", "Alias for nostr_agent_profile: return this agent's kind 0 profile context"); + cJSON_AddStringToObject(t41f_params, "type", "object"); + cJSON_AddItemToObject(t41f_params, "properties", t41f_props); + cJSON_AddItemToObject(t41f_fn, "parameters", t41f_params); + cJSON_AddItemToObject(t41f, "function", t41f_fn); + cJSON_AddItemToArray(tools, t41f); + + cJSON* t41g = cJSON_CreateObject(); + cJSON* t41g_fn = cJSON_CreateObject(); + cJSON* t41g_params = cJSON_CreateObject(); + cJSON* t41g_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t41g, "type", "function"); + cJSON_AddStringToObject(t41g_fn, "name", "my_contacts"); + cJSON_AddStringToObject(t41g_fn, "description", "Alias for nostr_agent_contacts: return this agent's kind 3 contacts context"); + cJSON_AddStringToObject(t41g_params, "type", "object"); + cJSON_AddItemToObject(t41g_params, "properties", t41g_props); + cJSON_AddItemToObject(t41g_fn, "parameters", t41g_params); + cJSON_AddItemToObject(t41g, "function", t41g_fn); + cJSON_AddItemToArray(tools, t41g); + + cJSON* t41h = cJSON_CreateObject(); + cJSON* t41h_fn = cJSON_CreateObject(); + cJSON* t41h_params = cJSON_CreateObject(); + cJSON* t41h_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t41h, "type", "function"); + cJSON_AddStringToObject(t41h_fn, "name", "my_relays"); + cJSON_AddStringToObject(t41h_fn, "description", "Alias for nostr_agent_relays: return this agent's kind 10002 relay context"); + cJSON_AddStringToObject(t41h_params, "type", "object"); + cJSON_AddItemToObject(t41h_params, "properties", t41h_props); + cJSON_AddItemToObject(t41h_fn, "parameters", t41h_params); + cJSON_AddItemToObject(t41h, "function", t41h_fn); + cJSON_AddItemToArray(tools, t41h); + + cJSON* t41i = cJSON_CreateObject(); + cJSON* t41i_fn = cJSON_CreateObject(); + cJSON* t41i_params = cJSON_CreateObject(); + cJSON* t41i_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t41i, "type", "function"); + cJSON_AddStringToObject(t41i_fn, "name", "my_notes"); + cJSON_AddStringToObject(t41i_fn, "description", "Alias for nostr_agent_notes: return this agent's recent kind 1 notes context"); + cJSON_AddStringToObject(t41i_params, "type", "object"); + cJSON_AddItemToObject(t41i_params, "properties", t41i_props); + cJSON_AddItemToObject(t41i_fn, "parameters", t41i_params); + cJSON_AddItemToObject(t41i, "function", t41i_fn); + cJSON_AddItemToArray(tools, t41i); + cJSON* t42 = cJSON_CreateObject(); cJSON* t42_fn = cJSON_CreateObject(); cJSON* t42_params = cJSON_CreateObject();