Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c5beda3c9 | ||
|
|
7c32683e75 | ||
|
|
645f6ea610 | ||
|
|
b569d41409 | ||
|
|
b3b1ddb2dd | ||
|
|
c4ee5a0aa0 | ||
|
|
2398005eff | ||
|
|
b06c267db4 |
@@ -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.5
|
||||
## Current Status — v0.2.13
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.2.5 — Fix increment_and_push upstream auto-setup and finalize SIGINT shutdown fix
|
||||
> Last release update: v0.2.13 — Unify startup skills, rename identity skill, add dm_history text format, and enforce startup publish success
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
21668
context.log.md
21668
context.log.md
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ See also: [CONTEXT.md](CONTEXT.md) · [SKILLS.md](SKILLS.md) · [README.md](../R
|
||||
|
||||
`genesis.jsonc` is the first-run bootstrap document for a Didactyl agent.
|
||||
|
||||
It defines initial identity, admin policy, startup events, default skill trigger behavior, and baseline runtime settings so the agent can publish itself onto Nostr.
|
||||
It defines initial identity, admin policy, startup events (including startup skills), and baseline runtime settings so the agent can publish itself onto Nostr.
|
||||
|
||||
After bootstrap, the long-term direction is **nsec-only startup** with state recovered from Nostr events.
|
||||
|
||||
@@ -30,7 +30,7 @@ Typical optional sections:
|
||||
- `security`
|
||||
- `admin_context`
|
||||
- `api`
|
||||
- `default_skill` (published as private kind `31124`, default `d=default_admin_dm`)
|
||||
- Startup skills in `startup_events` (typically private kind `31124`, e.g. `d=identity_and_rules` and `d=dm_history`)
|
||||
|
||||
---
|
||||
|
||||
@@ -121,10 +121,11 @@ That relay list is used as the initial network attachment for querying existing
|
||||
## Migration Notes (v0.2.0)
|
||||
|
||||
- Legacy `config.jsonc` and template-DSL context files have been removed from the active startup model.
|
||||
- Default DM handling now relies on `default_skill` with `d_tag: default_admin_dm` and trigger tags:
|
||||
- `["trigger", "dm"]`
|
||||
- `["filter", "{\"from\":\"admin\"}"]`
|
||||
- Default skill content is plain markdown with inline variables such as `{{my_kind0_profile}}` and `{{my_npub}}`.
|
||||
- Startup skills are defined directly in `startup_events` as kind `31123`/`31124` events.
|
||||
- Typical default DM stack is two startup skills with tags:
|
||||
- `d=identity_and_rules` with `trigger=dm` and `filter={"from":"admin"}`
|
||||
- `d=dm_history` with `trigger=dm` and `filter={"from":"admin"}`
|
||||
- Skill content is plain markdown with inline variables such as `{{my_kind0_profile}}`, `{{my_npub}}`, and `{{nostr_dm_history({"format":"text","limit":12})}}`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -61,20 +61,30 @@
|
||||
["r", "wss://relay.primal.net"]
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// ─── Default Skill ────────────────────────────────────────────────
|
||||
// Keep this generic and non-sensitive for repository examples.
|
||||
"default_skill": {
|
||||
"d_tag": "default_admin_dm",
|
||||
"kind": 31124,
|
||||
"content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone",
|
||||
"tags": [
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "Default admin DM handler"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone",
|
||||
"tags": [
|
||||
["d", "identity_and_rules"],
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "Agent identity and behavioral rules"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "## Recent Conversation\n\n{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}",
|
||||
"tags": [
|
||||
["d", "dm_history"],
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "DM conversation history for context continuity"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
246
plans/dm_history_context_cleanup.md
Normal file
246
plans/dm_history_context_cleanup.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# DM History Context Cleanup, Skill Rename & Startup Events Unification
|
||||
|
||||
## Problems
|
||||
|
||||
### 1. DM History as JSON in System Prompt
|
||||
The `{{nostr_dm_history}}` template variable resolves to a raw JSON array string pasted into the system prompt. The LLM receives conversation history as JSON-within-text, wasting tokens and forcing it to parse structured data embedded in prose.
|
||||
|
||||
### 2. Misleading Skill Name
|
||||
The skill `default_admin_dm` is really an identity and rules skill. Rename to `identity_and_rules`.
|
||||
|
||||
### 3. `default_skill` Config Section Is Redundant
|
||||
The `default_skill` section in genesis.jsonc is a special-case config path that converts a skill into a startup event and auto-adopts it. This is unnecessary — skills should just be startup events like everything else. The program should treat all startup events equally and halt if any fail to publish.
|
||||
|
||||
## Solution
|
||||
|
||||
Three coordinated changes:
|
||||
|
||||
### Part A: DM History Text Format
|
||||
Add a `format` parameter to the `nostr_dm_history` tool. Create a separate `dm_history` skill.
|
||||
|
||||
### Part B: Skill Rename
|
||||
Rename `default_admin_dm` → `identity_and_rules` everywhere.
|
||||
|
||||
### Part C: Eliminate `default_skill` Config Section
|
||||
Move skills into `startup_events`. Auto-adopt kind 31123/31124 startup events. Make startup event publishing mandatory (halt on failure).
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Part A: DM History Text Format
|
||||
|
||||
#### A1. Add `format` parameter to `execute_nostr_dm_history()`
|
||||
|
||||
**File:** `src/tools/tool_agent.c` — line 265
|
||||
|
||||
Add a `format` parameter: `"json"` (default, current behavior) or `"text"`.
|
||||
|
||||
When `format` is `"text"`, after the filtering loop (lines 320-360), iterate the filtered array and build plain text:
|
||||
|
||||
```
|
||||
User: Good morning
|
||||
Assistant: Good morning! ☀️ How can I help you today?
|
||||
User: What is the capital of England?
|
||||
Assistant: London! 🇬🇧
|
||||
```
|
||||
|
||||
Existing `include_current=false` default already deduplicates the current live message (line 339).
|
||||
|
||||
#### A2. Add `format` to the tool schema
|
||||
|
||||
**File:** `src/tools/tools_schema.c` — around line 1493
|
||||
|
||||
Add `format` property with description. Update tool description to mention the parameter.
|
||||
|
||||
---
|
||||
|
||||
### Part B: Skill Rename
|
||||
|
||||
#### B1. Update `src/default_events.h`
|
||||
|
||||
- `DIDACTYL_DEFAULT_SKILL_D_TAG`: `"default_admin_dm"` → `"identity_and_rules"`
|
||||
- `DIDACTYL_DEFAULT_SKILL_TAGS_JSON`: update d-tag and description
|
||||
- `DIDACTYL_DEFAULT_SKILL_TEMPLATE`: keep as-is (already clean, no `{{nostr_dm_history}}`)
|
||||
|
||||
#### B2. Update tool description examples in `src/tools/tools_schema.c`
|
||||
|
||||
- Line 854: `skill_get` example
|
||||
- Line 883: `skill_view` example
|
||||
- Line 911: `skill_set` example
|
||||
|
||||
#### B3. Update `docs/GENESIS.md`
|
||||
|
||||
- Line 33 and 124: `default_admin_dm` → `identity_and_rules`
|
||||
|
||||
---
|
||||
|
||||
### Part C: Eliminate `default_skill` Config Section
|
||||
|
||||
#### C1. Auto-adopt kind 31123/31124 startup events
|
||||
|
||||
**File:** `src/config.c`
|
||||
|
||||
Replace `config_ensure_default_skill_startup_events()` with a new function `config_ensure_startup_skill_adoption()` that:
|
||||
|
||||
1. Scans all startup events for kind 31123 and 31124
|
||||
2. For each skill found, extracts its `d_tag` from tags
|
||||
3. Builds the skill address (`kind:pubkey:d_tag`)
|
||||
4. Ensures a kind 10123 adoption list startup event exists with all skill addresses
|
||||
5. If no 10123 event exists, creates one
|
||||
|
||||
This replaces the single-skill logic with a generic "adopt all startup skills" approach.
|
||||
|
||||
#### C2. Remove `default_skill` config parsing
|
||||
|
||||
**File:** `src/config.c`
|
||||
|
||||
- Remove `parse_default_skill_config()` function (lines 733-816)
|
||||
- Remove the call at line 1369
|
||||
- Keep backward compatibility: if `default_skill` section exists in genesis.jsonc, convert it to a startup event during parsing (migration path) — OR just remove support and let users update their genesis.jsonc
|
||||
|
||||
#### C3. Remove `default_skill_config_t` from config struct
|
||||
|
||||
**File:** `src/config.h`
|
||||
|
||||
- Remove `default_skill_config_t` typedef (lines 67-72)
|
||||
- Remove `default_skill` field from `didactyl_config_t` (line 134)
|
||||
|
||||
#### C4. Update `src/main.c`
|
||||
|
||||
- Line 1060: Replace `config_ensure_default_skill_startup_events()` call with `config_ensure_startup_skill_adoption()`
|
||||
|
||||
#### C5. Make startup event publishing mandatory
|
||||
|
||||
**File:** `src/nostr_handler.c`
|
||||
|
||||
In the startup publish flow (around line 3150), after all relays have been tried:
|
||||
- Check that each startup event was published to at least one relay
|
||||
- If any event failed on all relays, log an error and return failure
|
||||
- The caller in `main.c` should halt on this failure
|
||||
|
||||
#### C6. Update setup wizard
|
||||
|
||||
**File:** `src/setup_wizard.c`
|
||||
|
||||
- `configure_default_skill_for_agent()` (line 498): Instead of populating `cfg->default_skill`, append the identity skill as a startup event directly
|
||||
- Add the `dm_history` skill as a second startup event
|
||||
- The auto-adoption logic (C1) handles the adoption list
|
||||
|
||||
#### C7. Update `default_events.h`
|
||||
|
||||
Add the `dm_history` skill template:
|
||||
|
||||
```c
|
||||
#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_D_TAG "dm_history"
|
||||
|
||||
static const char* DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE =
|
||||
"## Recent Conversation\n\n"
|
||||
"{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Part D: Genesis Config Updates
|
||||
|
||||
#### D1. Update `genesis.jsonc.example`
|
||||
|
||||
Remove `default_skill` section. Add skills as startup events:
|
||||
|
||||
```jsonc
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 0,
|
||||
"content_fields": {
|
||||
"name": "Didactyl",
|
||||
"about": "I am a Didactyl agent living on Nostr"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"content": "",
|
||||
"tags": [["p", "ADMIN_HEX_PUBKEY"]]
|
||||
},
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["r", "wss://relay.damus.io"],
|
||||
["r", "wss://relay.primal.net"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone",
|
||||
"tags": [
|
||||
["d", "identity_and_rules"],
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "Agent identity and behavioral rules"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "## Recent Conversation\n\n{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}",
|
||||
"tags": [
|
||||
["d", "dm_history"],
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "DM conversation history for context continuity"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"]
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### D2. Update live `genesis.jsonc`
|
||||
|
||||
Same changes as D1 but for the actual deployment config.
|
||||
|
||||
---
|
||||
|
||||
## Result
|
||||
|
||||
### Before
|
||||
|
||||
```json
|
||||
[
|
||||
{"role": "system", "content": "You are {...}\n\nYour npub: npub1...\n\n## Rules\n...\n[{\"role\":\"user\",\"content\":\"Good morning\",...}]"},
|
||||
{"role": "user", "content": "And Fiji?"}
|
||||
]
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```json
|
||||
[
|
||||
{"role": "system", "content": "# Didactyl Agent\n\nYou are {...}\n\nYour npub: npub1...\n\n## Rules\n...\n\n---\n\n## Recent Conversation\n\nUser: Good morning\nAssistant: Good morning! ☀️ How can I help you today?\nUser: What is the capital of England?\nAssistant: London! 🇬🇧\nUser: How about France?\nAssistant: Paris! 🇫🇷"},
|
||||
{"role": "user", "content": "And Fiji?"}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/tools/tool_agent.c` | Add `format` parameter to `execute_nostr_dm_history()` |
|
||||
| `src/tools/tools_schema.c` | Add `format` to schema; update example d-tags |
|
||||
| `src/default_events.h` | Rename d-tag; add `dm_history` skill template |
|
||||
| `src/config.h` | Remove `default_skill_config_t` and field |
|
||||
| `src/config.c` | Remove `parse_default_skill_config()`; replace `config_ensure_default_skill_startup_events()` with `config_ensure_startup_skill_adoption()` |
|
||||
| `src/main.c` | Update call to new adoption function |
|
||||
| `src/nostr_handler.c` | Make startup event publishing mandatory (halt on total failure) |
|
||||
| `src/setup_wizard.c` | Put skills in startup_events; add dm_history |
|
||||
| `genesis.jsonc.example` | Remove `default_skill`; add skills as startup events |
|
||||
| `genesis.jsonc` | Same as example |
|
||||
| `docs/GENESIS.md` | Update references |
|
||||
|
||||
## Migration Note
|
||||
|
||||
For backward compatibility, consider keeping `parse_default_skill_config()` as a migration shim that converts `default_skill` into a startup event with a deprecation warning. This way existing genesis.jsonc files with `default_skill` still work but users are prompted to update.
|
||||
224
plans/strict_startup_validation.md
Normal file
224
plans/strict_startup_validation.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Strict Startup Validation — Eliminate Fake Event Seeding
|
||||
|
||||
## Problem
|
||||
|
||||
The agent currently seeds a **synthetic event** into the self-skill cache during startup
|
||||
(`seed_default_skill_into_cache()` in `src/nostr_handler.c:1299`). This fake event has:
|
||||
|
||||
- `id` = all zeros
|
||||
- `created_at` = `time(NULL)` (current startup time)
|
||||
|
||||
Because the fake event is newer than the real relay event, the upsert rejects the real
|
||||
event. This causes `skill_get` to return an all-zeros ID and prevents the cache from
|
||||
ever holding the authentic relay event.
|
||||
|
||||
Additionally, the adopted skills cache in `src/agent.c` has a startup fallback path
|
||||
that uses `g_cfg->startup_events` when the adoption list is empty. This masks real
|
||||
failures and creates inconsistency between what the agent thinks it has and what
|
||||
actually exists on relays.
|
||||
|
||||
## Principle
|
||||
|
||||
> If the agent cannot retrieve the events it needs to run, startup should fail with
|
||||
> a clear diagnostic. No fake events. No fallbacks. If it is not there, fail.
|
||||
|
||||
## Two Startup Modes
|
||||
|
||||
### Genesis Startup (first run)
|
||||
|
||||
1. Connect to relays
|
||||
2. Publish startup events (profile, contacts, relay list, default skill, adoption list)
|
||||
3. **Wait for publish confirmation** from at least one relay per event
|
||||
4. Log at INFO level: which events were published to which relays
|
||||
5. Subscribe to self-skill cache
|
||||
6. Wait for EOSE — validate published events are now in cache
|
||||
7. If validation fails: abort with diagnostic
|
||||
|
||||
### Subsequent Startup (existing agent)
|
||||
|
||||
1. Connect to relays
|
||||
2. Subscribe to self-skill cache
|
||||
3. Wait for EOSE
|
||||
4. Log at INFO level: which events were received from which relays
|
||||
5. Validate required events exist in cache
|
||||
6. If validation fails: abort with diagnostic
|
||||
|
||||
## Current Startup Flow (steps 14–18)
|
||||
|
||||
```
|
||||
Step 14: Subscribe self-skill cache (sends REQ for kinds 31123/31124/10123)
|
||||
→ Does NOT wait for EOSE
|
||||
→ Calls seed_default_skill_into_cache() with fake event
|
||||
Step 15: Subscribe DMs (agent starts accepting messages)
|
||||
→ Startup DM sent BEFORE this step
|
||||
Step 16: Subscribe wallet events
|
||||
Step 17: Initialize cashu wallet
|
||||
Step 18: READY — enter main poll loop
|
||||
```
|
||||
|
||||
## Proposed New Flow
|
||||
|
||||
```
|
||||
Step 14: Subscribe self-skill cache (sends REQ for kinds 31123/31124/10123)
|
||||
Step 14a: Wait for self-skill EOSE (15s timeout)
|
||||
Step 14b: Validate required events exist in cache
|
||||
→ Log received events at INFO level
|
||||
→ If no skill events: FAIL startup with diagnostic
|
||||
Step 15: Subscribe DMs
|
||||
Step 16: Subscribe wallet events
|
||||
Step 17: Initialize cashu wallet
|
||||
Step 18: READY — send startup DM AFTER all validation, enter main loop
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. Remove `seed_default_skill_into_cache()` entirely
|
||||
|
||||
**File:** `src/nostr_handler.c`
|
||||
|
||||
- Delete the entire function (lines 1299–1341)
|
||||
- Remove its call at line 3165 in `nostr_handler_reconcile_startup_events()`
|
||||
- Remove its forward declaration at line 451
|
||||
|
||||
### 2. Remove startup fallback in `refresh_adopted_skills_cache_if_needed()`
|
||||
|
||||
**File:** `src/agent.c`
|
||||
|
||||
Delete the entire fallback block (lines ~1571–1619) that uses
|
||||
`g_cfg->startup_events` when the adoption list is empty. If the adoption
|
||||
list is empty, the cache stays empty — no synthetic population.
|
||||
|
||||
### 3. Add synchronous EOSE wait for self-skill subscription
|
||||
|
||||
**File:** `src/nostr_handler.c`
|
||||
|
||||
Add a new exported function:
|
||||
|
||||
```c
|
||||
int nostr_handler_wait_for_self_skill_eose(int timeout_ms);
|
||||
```
|
||||
|
||||
Implementation:
|
||||
- Use a static `volatile int g_self_skill_eose_received = 0` flag
|
||||
- Set it to 1 in `on_self_skill_eose()`
|
||||
- The wait function polls `nostr_relay_pool_poll()` in a loop until the
|
||||
flag is set or the timeout expires
|
||||
- Returns 0 on success, -1 on timeout
|
||||
|
||||
### 4. Add post-EOSE validation with logging
|
||||
|
||||
**File:** `src/nostr_handler.c`
|
||||
|
||||
Add a new exported function:
|
||||
|
||||
```c
|
||||
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count);
|
||||
```
|
||||
|
||||
This function:
|
||||
- Counts skill events (kind 31123 + 31124) in cache
|
||||
- Counts adoption events (kind 10123) in cache
|
||||
- Logs at INFO level each cached event: kind, d_tag, id, created_at
|
||||
- Returns 0 if at least one skill event exists
|
||||
- Returns -1 if no skill events found
|
||||
|
||||
### 5. Update startup sequence in `main.c`
|
||||
|
||||
**File:** `src/main.c`
|
||||
|
||||
After step 14 subscribe, add EOSE wait and validation:
|
||||
|
||||
```c
|
||||
// Wait for self-skill EOSE
|
||||
int eose_timeout_ms = 15000;
|
||||
if (nostr_handler_wait_for_self_skill_eose(eose_timeout_ms) != 0) {
|
||||
startup_step_fail(14, "Subscribe self-skill cache",
|
||||
"self-skill events not received within timeout; "
|
||||
"check relay connectivity and that skill events exist");
|
||||
// cleanup and return 1
|
||||
}
|
||||
|
||||
// Validate
|
||||
int skill_count = 0, adoption_count = 0;
|
||||
if (nostr_handler_validate_self_skill_cache(&skill_count, &adoption_count) != 0) {
|
||||
startup_step_fail(14, "Subscribe self-skill cache",
|
||||
"no skill events found after EOSE; "
|
||||
"run genesis to publish default skill events");
|
||||
// cleanup and return 1
|
||||
}
|
||||
|
||||
char detail[128];
|
||||
snprintf(detail, sizeof(detail),
|
||||
"skills=%d adoptions=%d", skill_count, adoption_count);
|
||||
startup_step_ok(14, "Subscribe self-skill cache", detail);
|
||||
```
|
||||
|
||||
### 6. Move startup DM to after all validation
|
||||
|
||||
**File:** `src/main.c`
|
||||
|
||||
Move the startup DM send from its current position (between steps 14 and 15)
|
||||
to just before step 18 READY. The agent should only announce itself as online
|
||||
after all required events have been validated and all subscriptions are active.
|
||||
|
||||
### 7. Genesis publish confirmation logging
|
||||
|
||||
**File:** `src/nostr_handler.c` / `src/main.c`
|
||||
|
||||
For genesis startup, the existing publish path already logs at INFO level:
|
||||
```
|
||||
kind 31124 event published to wss://relay.damus.io (async)
|
||||
```
|
||||
|
||||
Enhance this to also log a summary after all startup events are published:
|
||||
```
|
||||
[INFO] startup publish summary: 5 events published to 3/4 relays
|
||||
[INFO] kind=0 (profile) → relay.damus.io, relay.primal.net, nos.lol
|
||||
[INFO] kind=31124 (default_admin_dm) → relay.damus.io, relay.primal.net, nos.lol
|
||||
[INFO] kind=10123 (adoption list) → relay.damus.io, relay.primal.net, nos.lol
|
||||
```
|
||||
|
||||
### 8. Declare new functions in header
|
||||
|
||||
**File:** `src/nostr_handler.h`
|
||||
|
||||
```c
|
||||
int nostr_handler_wait_for_self_skill_eose(int timeout_ms);
|
||||
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count);
|
||||
```
|
||||
|
||||
## Startup Step Summary
|
||||
|
||||
| Step | Description | Behavior |
|
||||
|------|-------------|----------|
|
||||
| 1–13 | Existing steps (config, relays, agent init, triggers, kind10002, context subs) | Unchanged |
|
||||
| 14 | Subscribe self-skill cache + wait EOSE + validate | **BLOCKING** — fails if no skill events |
|
||||
| 15 | Subscribe DMs | Fails startup if subscription fails |
|
||||
| 16 | Subscribe wallet events | Continues on failure |
|
||||
| 17 | Initialize cashu wallet | Continues on failure |
|
||||
| 18 | READY | Send startup DM, enter main loop |
|
||||
|
||||
## Error Messages
|
||||
|
||||
### EOSE timeout
|
||||
```
|
||||
[ERROR] Startup aborted: self-skill EOSE not received within 15000ms.
|
||||
[ERROR] Check relay connectivity (connected=2/4) and ensure skill events exist on relays.
|
||||
[ERROR] If this is a first run, use genesis to publish startup events.
|
||||
```
|
||||
|
||||
### No skill events after EOSE
|
||||
```
|
||||
[ERROR] Startup aborted: no skill events (kind 31123/31124) found in self-skill cache.
|
||||
[ERROR] The agent requires at least one skill event to operate.
|
||||
[ERROR] Run genesis to publish the default skill, or publish a skill manually.
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/nostr_handler.c` | Remove `seed_default_skill_into_cache()`, add EOSE wait + validation functions, enhance publish logging |
|
||||
| `src/nostr_handler.h` | Declare `nostr_handler_wait_for_self_skill_eose()` and `nostr_handler_validate_self_skill_cache()` |
|
||||
| `src/main.c` | Add EOSE wait + validation after step 14, move startup DM to before step 18 |
|
||||
| `src/agent.c` | Remove startup fallback block in `refresh_adopted_skills_cache_if_needed()` |
|
||||
581
src/agent.c
581
src/agent.c
@@ -15,6 +15,7 @@
|
||||
#include "nostr_handler.h"
|
||||
#include "trigger_manager.h"
|
||||
#include "tools/tools.h"
|
||||
#include "prompt_template.h"
|
||||
#include "cjson/cJSON.h"
|
||||
#include "debug.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
@@ -479,6 +480,11 @@ static char* build_slash_help_all_json(void) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (append_textf_local(&out, &cap, &used, "(run /<tool> -h for full JSON args template)\n") != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* tool_list_json = tools_execute(&g_tools_ctx, "tool_list", "{}");
|
||||
cJSON* tool_root = tool_list_json ? cJSON_Parse(tool_list_json) : NULL;
|
||||
cJSON* tools = tool_root ? cJSON_GetObjectItemCaseSensitive(tool_root, "tools") : NULL;
|
||||
@@ -643,6 +649,54 @@ static char* build_slash_help_all_json(void) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static cJSON* build_example_value_for_type_local(const char* type_s) {
|
||||
if (!type_s || type_s[0] == '\0') return cJSON_CreateString("<value>");
|
||||
if (strcmp(type_s, "string") == 0) return cJSON_CreateString("<string>");
|
||||
if (strcmp(type_s, "integer") == 0) return cJSON_CreateNumber(0);
|
||||
if (strcmp(type_s, "number") == 0) return cJSON_CreateNumber(0.0);
|
||||
if (strcmp(type_s, "boolean") == 0) return cJSON_CreateBool(0);
|
||||
if (strcmp(type_s, "array") == 0) return cJSON_CreateArray();
|
||||
if (strcmp(type_s, "object") == 0) return cJSON_CreateObject();
|
||||
return cJSON_CreateString("<value>");
|
||||
}
|
||||
|
||||
static cJSON* build_example_args_from_parameters_local(cJSON* params) {
|
||||
cJSON* example = cJSON_CreateObject();
|
||||
if (!example) return NULL;
|
||||
|
||||
cJSON* props = params ? cJSON_GetObjectItemCaseSensitive(params, "properties") : NULL;
|
||||
if (!props || !cJSON_IsObject(props)) {
|
||||
return example;
|
||||
}
|
||||
|
||||
cJSON* prop = NULL;
|
||||
cJSON_ArrayForEach(prop, props) {
|
||||
if (!prop->string || prop->string[0] == '\0') continue;
|
||||
|
||||
cJSON* enum_vals = cJSON_GetObjectItemCaseSensitive(prop, "enum");
|
||||
if (enum_vals && cJSON_IsArray(enum_vals) && cJSON_GetArraySize(enum_vals) > 0) {
|
||||
cJSON* first = cJSON_GetArrayItem(enum_vals, 0);
|
||||
cJSON* dup = cJSON_Duplicate(first, 1);
|
||||
if (dup) {
|
||||
cJSON_AddItemToObject(example, prop->string, dup);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* type = cJSON_GetObjectItemCaseSensitive(prop, "type");
|
||||
const char* type_s = (type && cJSON_IsString(type) && type->valuestring) ? type->valuestring : NULL;
|
||||
cJSON* val = build_example_value_for_type_local(type_s);
|
||||
if (!val) val = cJSON_CreateString("<value>");
|
||||
if (!val) {
|
||||
cJSON_Delete(example);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToObject(example, prop->string, val);
|
||||
}
|
||||
|
||||
return example;
|
||||
}
|
||||
|
||||
static char* build_slash_help_tool_json(const char* tool_name) {
|
||||
if (!tool_name || tool_name[0] == '\0') {
|
||||
return local_json_error("missing tool name");
|
||||
@@ -688,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);
|
||||
@@ -704,7 +778,9 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa
|
||||
|
||||
size_t tool_len = (size_t)(p - (message + 1));
|
||||
if (tool_len == 0 || tool_len >= 128U) {
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "{\"success\":false,\"error\":\"invalid slash command\"}");
|
||||
(void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex,
|
||||
"{\"success\":false,\"error\":\"invalid slash command\"}",
|
||||
DIDACTYL_DM_HISTORY_TOOL_RESPONSE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -761,7 +837,9 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa
|
||||
free(log_payload);
|
||||
}
|
||||
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, dm_payload ? dm_payload : "Command executed with no output.");
|
||||
(void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex,
|
||||
dm_payload ? dm_payload : "Command executed with no output.",
|
||||
DIDACTYL_DM_HISTORY_TOOL_RESPONSE);
|
||||
free(markdown_result);
|
||||
free(result_json);
|
||||
return 1;
|
||||
@@ -769,40 +847,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 : "";
|
||||
@@ -856,233 +900,6 @@ const char* agent_classify_message_part(cJSON* msg, int idx) {
|
||||
return detect_context_section(role_s, content_s, idx);
|
||||
}
|
||||
|
||||
static const char* admin_label_for_log(void) {
|
||||
static __thread char name_buf[96];
|
||||
snprintf(name_buf, sizeof(name_buf), "%s", "Administrator");
|
||||
|
||||
char* kind0 = nostr_handler_get_admin_kind0_context();
|
||||
if (!kind0 || kind0[0] == '\0') {
|
||||
free(kind0);
|
||||
return name_buf;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_Parse(kind0);
|
||||
free(kind0);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return name_buf;
|
||||
}
|
||||
|
||||
cJSON* display_name = cJSON_GetObjectItemCaseSensitive(root, "display_name");
|
||||
cJSON* name = cJSON_GetObjectItemCaseSensitive(root, "name");
|
||||
const char* chosen = NULL;
|
||||
if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') {
|
||||
chosen = display_name->valuestring;
|
||||
} else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
|
||||
chosen = name->valuestring;
|
||||
}
|
||||
|
||||
if (chosen) {
|
||||
snprintf(name_buf, sizeof(name_buf), "%s", chosen);
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return name_buf;
|
||||
}
|
||||
|
||||
static char* demote_markdown_headings_for_log(const char* content) {
|
||||
const char* src = content ? content : "";
|
||||
size_t n = strlen(src);
|
||||
|
||||
char* out = (char*)malloc(n * 2U + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t w = 0;
|
||||
int line_start = 1;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
char c = src[i];
|
||||
|
||||
if (line_start && c == '#') {
|
||||
out[w++] = '#';
|
||||
}
|
||||
|
||||
out[w++] = c;
|
||||
|
||||
if (c == '\n') {
|
||||
line_start = 1;
|
||||
} else if (line_start && (c == ' ' || c == '\t' || c == '\r')) {
|
||||
line_start = 1;
|
||||
} else {
|
||||
line_start = 0;
|
||||
}
|
||||
}
|
||||
|
||||
out[w] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
static void format_chat_time_for_log(time_t ts, char out[16]) {
|
||||
if (!out) return;
|
||||
out[0] = '\0';
|
||||
|
||||
if (ts <= 0) {
|
||||
snprintf(out, 16, "--:--");
|
||||
return;
|
||||
}
|
||||
|
||||
struct tm tm_info;
|
||||
if (!localtime_r(&ts, &tm_info)) {
|
||||
snprintf(out, 16, "--:--");
|
||||
return;
|
||||
}
|
||||
|
||||
strftime(out, 16, "%H:%M", &tm_info);
|
||||
}
|
||||
|
||||
static char* format_context_payload_for_log(const char* context_payload) {
|
||||
const char* payload = context_payload ? context_payload : "";
|
||||
|
||||
cJSON* root = cJSON_Parse(payload);
|
||||
if (!root || !cJSON_IsArray(root)) {
|
||||
cJSON_Delete(root);
|
||||
return strdup(payload);
|
||||
}
|
||||
|
||||
size_t cap = strlen(payload) + 2048U;
|
||||
if (cap < 4096U) cap = 4096U;
|
||||
char* out = (char*)malloc(cap);
|
||||
if (!out) {
|
||||
cJSON_Delete(root);
|
||||
return strdup(payload);
|
||||
}
|
||||
out[0] = '\0';
|
||||
size_t used = 0;
|
||||
char* conversation_role = NULL;
|
||||
char* conversation_content = NULL;
|
||||
|
||||
int n = cJSON_GetArraySize(root);
|
||||
(void)appendf(&out, &cap, &used, "Sections: %d\n\n", n);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* msg = cJSON_GetArrayItem(root, i);
|
||||
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
|
||||
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
|
||||
|
||||
const char* role_s = (role && cJSON_IsString(role) && role->valuestring)
|
||||
? role->valuestring
|
||||
: "unknown";
|
||||
|
||||
const char* content_s = "";
|
||||
if (content && cJSON_IsString(content) && content->valuestring) {
|
||||
content_s = content->valuestring;
|
||||
} else if (content && cJSON_IsNull(content)) {
|
||||
content_s = "<null>";
|
||||
}
|
||||
|
||||
const char* section = agent_classify_message_part(msg, i);
|
||||
if (section && strcmp(section, "dm_history") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section && strcmp(section, "conversation") == 0) {
|
||||
free(conversation_role);
|
||||
free(conversation_content);
|
||||
conversation_role = strdup(role_s);
|
||||
conversation_content = strdup(content_s);
|
||||
continue;
|
||||
}
|
||||
|
||||
char* display_content = demote_markdown_headings_for_log(content_s);
|
||||
const char* safe_display_content = display_content ? display_content : content_s;
|
||||
|
||||
if (appendf(&out,
|
||||
&cap,
|
||||
&used,
|
||||
"# %s | role=%s\n\n"
|
||||
"%s\n\n",
|
||||
section ? section : "context_part",
|
||||
role_s,
|
||||
safe_display_content) != 0) {
|
||||
free(display_content);
|
||||
free(out);
|
||||
cJSON_Delete(root);
|
||||
return strdup(payload);
|
||||
}
|
||||
|
||||
free(display_content);
|
||||
}
|
||||
|
||||
const char* admin_label = admin_label_for_log();
|
||||
size_t chat_cap = 4096U;
|
||||
char* chat = (char*)malloc(chat_cap);
|
||||
size_t chat_used = 0;
|
||||
char* latest_admin_line = NULL;
|
||||
if (chat) {
|
||||
chat[0] = '\0';
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* msg = cJSON_GetArrayItem(root, i);
|
||||
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
|
||||
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
|
||||
cJSON* ts = msg ? cJSON_GetObjectItemCaseSensitive(msg, "_ts") : NULL;
|
||||
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
|
||||
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
||||
const char* section = agent_classify_message_part(msg, i);
|
||||
if (!section || strcmp(section, "dm_history") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
time_t created_at = 0;
|
||||
if (ts && cJSON_IsNumber(ts)) {
|
||||
created_at = (time_t)ts->valuedouble;
|
||||
}
|
||||
char time_buf[16] = {0};
|
||||
format_chat_time_for_log(created_at, time_buf);
|
||||
|
||||
const char* who = (strcmp(role_s, "user") == 0) ? admin_label : "Agent";
|
||||
if (appendf(&chat, &chat_cap, &chat_used, "%s %s %s\n", time_buf, who, content_s) != 0) {
|
||||
free(chat);
|
||||
chat = NULL;
|
||||
break;
|
||||
}
|
||||
|
||||
if (strcmp(role_s, "user") == 0) {
|
||||
free(latest_admin_line);
|
||||
int line_len = snprintf(NULL, 0, "%s %s %s", time_buf, who, content_s);
|
||||
if (line_len >= 0) {
|
||||
latest_admin_line = (char*)malloc((size_t)line_len + 1U);
|
||||
if (latest_admin_line) {
|
||||
snprintf(latest_admin_line, (size_t)line_len + 1U, "%s %s %s", time_buf, who, content_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chat && chat_used > 0) {
|
||||
(void)appendf(&out, &cap, &used, "# dm_history | role=chat\n\n%s\n", chat);
|
||||
}
|
||||
}
|
||||
|
||||
if (conversation_role && conversation_content) {
|
||||
(void)appendf(&out,
|
||||
&cap,
|
||||
&used,
|
||||
"# conversation | role=%s\n\n%s\n\n",
|
||||
conversation_role,
|
||||
conversation_content);
|
||||
}
|
||||
|
||||
free(chat);
|
||||
free(latest_admin_line);
|
||||
free(conversation_role);
|
||||
free(conversation_content);
|
||||
|
||||
cJSON_Delete(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void clear_context_part_names_locked(void) {
|
||||
for (int i = 0; i < g_context_part_names_count; i++) {
|
||||
free(g_context_part_names[i]);
|
||||
@@ -1132,16 +949,23 @@ 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) {
|
||||
return;
|
||||
}
|
||||
if (strcmp(phase, "llm_chat_with_tools_messages") != 0 &&
|
||||
strcmp(phase, "llm_trigger_with_tools_messages") != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
struct tm tm_info;
|
||||
localtime_r(&now, &tm_info);
|
||||
char timestamp[32] = {0};
|
||||
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info);
|
||||
|
||||
char* pretty_payload = format_context_payload_for_log(context_payload);
|
||||
const char* safe_phase = phase ? phase : "unknown";
|
||||
const char* safe_sender = sender_pubkey_hex ? sender_pubkey_hex : "unknown";
|
||||
const char* safe_payload = pretty_payload ? pretty_payload : "";
|
||||
const char* safe_payload = context_payload ? context_payload : "";
|
||||
size_t context_bytes = context_payload ? strlen(context_payload) : 0U;
|
||||
size_t approx_tokens = context_bytes / 4U;
|
||||
|
||||
@@ -1154,7 +978,7 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase,
|
||||
int entry_len = snprintf(NULL,
|
||||
0,
|
||||
"```text\n"
|
||||
"Context Log - not seen by model\n"
|
||||
"Context Log - seen by model\n"
|
||||
"timestamp=%s\n"
|
||||
"phase=%s\n"
|
||||
"sender=%s\n"
|
||||
@@ -1171,20 +995,18 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase,
|
||||
approx_tokens,
|
||||
safe_payload);
|
||||
if (entry_len < 0) {
|
||||
free(pretty_payload);
|
||||
return;
|
||||
}
|
||||
|
||||
char* entry = (char*)malloc((size_t)entry_len + 1U);
|
||||
if (!entry) {
|
||||
free(pretty_payload);
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(entry,
|
||||
(size_t)entry_len + 1U,
|
||||
"```text\n"
|
||||
"Context Log - not seen by model\n"
|
||||
"Context Log - seen by model\n"
|
||||
"timestamp=%s\n"
|
||||
"phase=%s\n"
|
||||
"sender=%s\n"
|
||||
@@ -1230,7 +1052,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;
|
||||
}
|
||||
|
||||
@@ -1242,7 +1063,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) {
|
||||
@@ -1261,7 +1081,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;
|
||||
}
|
||||
@@ -1450,114 +1270,7 @@ static const char* template_skill_lookup_callback(void* user_data, const char* d
|
||||
}
|
||||
|
||||
static char* resolve_skill_references_local(const char* input) {
|
||||
if (!input) {
|
||||
return strdup("");
|
||||
}
|
||||
|
||||
size_t cap = strlen(input) + 128U;
|
||||
size_t used = 0U;
|
||||
char* out = (char*)malloc(cap);
|
||||
if (!out) return NULL;
|
||||
out[0] = '\0';
|
||||
|
||||
const char* p = input;
|
||||
while (*p) {
|
||||
const char* open = strstr(p, "{{");
|
||||
if (!open) {
|
||||
size_t tail = strlen(p);
|
||||
if (used + tail + 1U > cap) {
|
||||
size_t next = cap;
|
||||
while (used + tail + 1U > next) next *= 2U;
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(out + used, p, tail);
|
||||
used += tail;
|
||||
out[used] = '\0';
|
||||
break;
|
||||
}
|
||||
|
||||
size_t prefix_len = (size_t)(open - p);
|
||||
if (used + prefix_len + 1U > cap) {
|
||||
size_t next = cap;
|
||||
while (used + prefix_len + 1U > next) next *= 2U;
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(out + used, p, prefix_len);
|
||||
used += prefix_len;
|
||||
out[used] = '\0';
|
||||
|
||||
const char* close = strstr(open + 2, "}}");
|
||||
if (!close) {
|
||||
size_t rem = strlen(open);
|
||||
if (used + rem + 1U > cap) {
|
||||
size_t next = cap;
|
||||
while (used + rem + 1U > next) next *= 2U;
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(out + used, open, rem);
|
||||
used += rem;
|
||||
out[used] = '\0';
|
||||
break;
|
||||
}
|
||||
|
||||
size_t var_len = (size_t)(close - (open + 2));
|
||||
char var[128];
|
||||
if (var_len >= sizeof(var)) var_len = sizeof(var) - 1U;
|
||||
memcpy(var, open + 2, var_len);
|
||||
var[var_len] = '\0';
|
||||
|
||||
char* start = var;
|
||||
while (*start && isspace((unsigned char)*start)) start++;
|
||||
char* end = start + strlen(start);
|
||||
while (end > start && isspace((unsigned char)end[-1])) {
|
||||
end[-1] = '\0';
|
||||
end--;
|
||||
}
|
||||
|
||||
const char* repl = "";
|
||||
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||
const char* found = adopted_skill_content_lookup_by_d_tag_locked(start);
|
||||
repl = found ? found : "";
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
|
||||
size_t repl_len = strlen(repl);
|
||||
if (used + repl_len + 1U > cap) {
|
||||
size_t next = cap;
|
||||
while (used + repl_len + 1U > next) next *= 2U;
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(out + used, repl, repl_len);
|
||||
used += repl_len;
|
||||
out[used] = '\0';
|
||||
|
||||
p = close + 2;
|
||||
}
|
||||
|
||||
return out;
|
||||
return prompt_template_resolve_inline_variables(input ? input : "", &g_tools_ctx);
|
||||
}
|
||||
|
||||
static char* build_context_from_triggers(trigger_type_t trigger_type,
|
||||
@@ -1584,20 +1297,19 @@ static char* build_context_from_triggers(trigger_type_t trigger_type,
|
||||
int matched = 0;
|
||||
|
||||
didactyl_sender_tier_t tier = DIDACTYL_SENDER_STRANGER;
|
||||
const char* message = NULL;
|
||||
if (trigger_event && cJSON_IsObject(trigger_event)) {
|
||||
cJSON* tier_j = cJSON_GetObjectItemCaseSensitive(trigger_event, "tier");
|
||||
cJSON* msg_j = cJSON_GetObjectItemCaseSensitive(trigger_event, "message");
|
||||
const char* tier_s = (tier_j && cJSON_IsString(tier_j) && tier_j->valuestring) ? tier_j->valuestring : "stranger";
|
||||
if (strcmp(tier_s, "admin") == 0) tier = DIDACTYL_SENDER_ADMIN;
|
||||
else if (strcmp(tier_s, "wot") == 0) tier = DIDACTYL_SENDER_WOT;
|
||||
if (msg_j && cJSON_IsString(msg_j) && msg_j->valuestring) {
|
||||
message = msg_j->valuestring;
|
||||
}
|
||||
}
|
||||
|
||||
char* matched_skill_contents[AGENT_ADOPTED_SKILLS_MAX];
|
||||
int matched_skill_count = 0;
|
||||
memset(matched_skill_contents, 0, sizeof(matched_skill_contents));
|
||||
|
||||
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||
for (int i = 0; i < g_adopted_skills_count; i++) {
|
||||
for (int i = 0; i < g_adopted_skills_count && matched_skill_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
|
||||
const agent_adopted_skill_t* s = &g_adopted_skills[i];
|
||||
if (!s->has_trigger || s->trigger_type != trigger_type) {
|
||||
continue;
|
||||
@@ -1606,8 +1318,16 @@ static char* build_context_from_triggers(trigger_type_t trigger_type,
|
||||
continue;
|
||||
}
|
||||
|
||||
char* expanded = resolve_skill_references_local(s->content ? s->content : "");
|
||||
const char* skill_text = expanded ? expanded : (s->content ? s->content : "");
|
||||
matched_skill_contents[matched_skill_count] = strdup(s->content ? s->content : "");
|
||||
if (matched_skill_contents[matched_skill_count]) {
|
||||
matched_skill_count++;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
|
||||
for (int i = 0; i < matched_skill_count; i++) {
|
||||
char* expanded = resolve_skill_references_local(matched_skill_contents[i] ? matched_skill_contents[i] : "");
|
||||
const char* skill_text = expanded ? expanded : (matched_skill_contents[i] ? matched_skill_contents[i] : "");
|
||||
|
||||
size_t need = strlen("\n\n---\n\n") + strlen(skill_text) + 1U;
|
||||
if (used + need >= cap) {
|
||||
@@ -1616,7 +1336,9 @@ static char* build_context_from_triggers(trigger_type_t trigger_type,
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(expanded);
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
for (int j = i; j < matched_skill_count; j++) {
|
||||
free(matched_skill_contents[j]);
|
||||
}
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
@@ -1635,8 +1357,8 @@ static char* build_context_from_triggers(trigger_type_t trigger_type,
|
||||
matched++;
|
||||
|
||||
free(expanded);
|
||||
free(matched_skill_contents[i]);
|
||||
}
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
|
||||
if (matched == 0) {
|
||||
const char* fallback = "You are an AI agent. Respond to the message.";
|
||||
@@ -1654,27 +1376,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;
|
||||
}
|
||||
|
||||
@@ -1849,56 +1550,6 @@ static int refresh_adopted_skills_cache_if_needed(void) {
|
||||
|
||||
cJSON_Delete(adoption_events);
|
||||
|
||||
if (tmp_count == 0 && g_cfg->startup_event_count > 0 && g_cfg->startup_events) {
|
||||
for (int i = 0; i < g_cfg->startup_event_count && tmp_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
|
||||
startup_event_t* se = &g_cfg->startup_events[i];
|
||||
if (!se || (se->kind != 31123 && se->kind != 31124) || !se->content || se->content[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
char d_tag_val[65] = {0};
|
||||
if (se->tags_json) {
|
||||
cJSON* tags = cJSON_Parse(se->tags_json);
|
||||
cJSON* d_tag = tags ? find_tag_value_string_local(tags, "d") : NULL;
|
||||
if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && d_tag->valuestring[0] != '\0') {
|
||||
snprintf(d_tag_val, sizeof(d_tag_val), "%s", d_tag->valuestring);
|
||||
}
|
||||
cJSON_Delete(tags);
|
||||
}
|
||||
if (d_tag_val[0] == '\0') {
|
||||
snprintf(d_tag_val, sizeof(d_tag_val), "startup-skill-%d", i + 1);
|
||||
}
|
||||
|
||||
tmp[tmp_count].kind = se->kind;
|
||||
snprintf(tmp[tmp_count].author_pubkey_hex,
|
||||
sizeof(tmp[tmp_count].author_pubkey_hex),
|
||||
"%s",
|
||||
g_cfg->keys.public_key_hex[0] != '\0'
|
||||
? g_cfg->keys.public_key_hex
|
||||
: "unknown");
|
||||
snprintf(tmp[tmp_count].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", d_tag_val);
|
||||
tmp[tmp_count].content = strdup(se->content);
|
||||
tmp[tmp_count].has_trigger = 0;
|
||||
tmp[tmp_count].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
|
||||
tmp[tmp_count].filter_json[0] = '\0';
|
||||
if (se->tags_json && se->tags_json[0] != '\0') {
|
||||
cJSON* tags = cJSON_Parse(se->tags_json);
|
||||
cJSON* trigger_tag = tags ? find_tag_value_string_local(tags, "trigger") : NULL;
|
||||
cJSON* filter_tag = tags ? find_tag_value_string_local(tags, "filter") : NULL;
|
||||
if (trigger_tag && cJSON_IsString(trigger_tag) && trigger_tag->valuestring && trigger_tag->valuestring[0] != '\0' &&
|
||||
filter_tag && cJSON_IsString(filter_tag) && filter_tag->valuestring && filter_tag->valuestring[0] != '\0') {
|
||||
tmp[tmp_count].has_trigger = 1;
|
||||
tmp[tmp_count].trigger_type = trigger_type_from_string(trigger_tag->valuestring);
|
||||
snprintf(tmp[tmp_count].filter_json, sizeof(tmp[tmp_count].filter_json), "%s", filter_tag->valuestring);
|
||||
}
|
||||
cJSON_Delete(tags);
|
||||
}
|
||||
if (tmp[tmp_count].content) {
|
||||
tmp_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||
clear_adopted_skills_cache_locked();
|
||||
for (int i = 0; i < tmp_count; i++) {
|
||||
@@ -2339,7 +1990,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);
|
||||
@@ -2393,12 +2043,16 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
}
|
||||
|
||||
int allow_tools = (tier == DIDACTYL_SENDER_ADMIN) && g_cfg->tools.enabled && g_cfg->security.admin.tools_enabled;
|
||||
int inbound_role = (message[0] == '/') ? DIDACTYL_DM_HISTORY_TOOL_REQUEST : DIDACTYL_DM_HISTORY_USER;
|
||||
(void)nostr_handler_dm_history_remember(sender_pubkey_hex, message, (didactyl_dm_history_role_t)inbound_role);
|
||||
|
||||
if (message[0] == '/') {
|
||||
if (!allow_tools) {
|
||||
const char* denied = "{\"success\":false,\"error\":\"slash commands are disabled for this sender tier\"}";
|
||||
append_context_log(sender_pubkey_hex, "direct_tool_exec", denied);
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, denied);
|
||||
(void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex,
|
||||
denied,
|
||||
DIDACTYL_DM_HISTORY_TOOL_RESPONSE);
|
||||
return;
|
||||
}
|
||||
if (handle_slash_command(sender_pubkey_hex, message)) {
|
||||
@@ -2473,7 +2127,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);
|
||||
|
||||
137
src/config.c
137
src/config.c
@@ -692,6 +692,9 @@ static int set_tag_value_string(cJSON* tags, const char* tag_key, const char* ta
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int startup_event_has_d_tag(const startup_event_t* se, const char* d_tag);
|
||||
static int append_startup_event(didactyl_config_t* config, int kind, const char* content, const char* tags_json);
|
||||
|
||||
static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) {
|
||||
if (!item || !tags || !cJSON_IsArray(tags)) {
|
||||
return 0;
|
||||
@@ -730,7 +733,7 @@ static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) {
|
||||
return set_tag_value_string(tags, "d", d_tag);
|
||||
}
|
||||
|
||||
static int parse_default_skill_config(cJSON* root, didactyl_config_t* config) {
|
||||
static int parse_legacy_default_skill_to_startup_events(cJSON* root, didactyl_config_t* config) {
|
||||
if (!root || !config) {
|
||||
return -1;
|
||||
}
|
||||
@@ -746,19 +749,20 @@ static int parse_default_skill_config(cJSON* root, didactyl_config_t* config) {
|
||||
cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(def, "content_fields");
|
||||
cJSON* tags = cJSON_GetObjectItemCaseSensitive(def, "tags");
|
||||
|
||||
if (!kind || !cJSON_IsNumber(kind)) {
|
||||
if (!kind || !cJSON_IsNumber(kind) || !d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || d_tag->valuestring[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int kind_i = (int)kind->valuedouble;
|
||||
if (kind_i != 31123 && kind_i != 31124) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || d_tag->valuestring[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
if (strlen(d_tag->valuestring) >= sizeof(config->default_skill.d_tag)) {
|
||||
return -1;
|
||||
for (int i = 0; i < config->startup_event_count; i++) {
|
||||
startup_event_t* se = &config->startup_events[i];
|
||||
if (se->kind == kind_i && startup_event_has_d_tag(se, d_tag->valuestring)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
char* content_text = NULL;
|
||||
@@ -806,13 +810,10 @@ static int parse_default_skill_config(cJSON* root, didactyl_config_t* config) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
free(config->default_skill.content);
|
||||
free(config->default_skill.tags_json);
|
||||
config->default_skill.kind = kind_i;
|
||||
snprintf(config->default_skill.d_tag, sizeof(config->default_skill.d_tag), "%s", d_tag->valuestring);
|
||||
config->default_skill.content = content_text;
|
||||
config->default_skill.tags_json = tags_json;
|
||||
return 0;
|
||||
int rc = append_startup_event(config, kind_i, content_text, tags_json);
|
||||
free(content_text);
|
||||
free(tags_json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int parse_startup_events(cJSON* root, didactyl_config_t* config) {
|
||||
@@ -1094,42 +1095,48 @@ static int append_startup_event(didactyl_config_t* config, int kind, const char*
|
||||
return 0;
|
||||
}
|
||||
|
||||
int config_ensure_default_skill_startup_events(didactyl_config_t* config) {
|
||||
if (!config || !config->default_skill.content || config->default_skill.content[0] == '\0' ||
|
||||
config->default_skill.d_tag[0] == '\0') {
|
||||
int config_ensure_startup_skill_adoption(didactyl_config_t* config) {
|
||||
if (!config || config->keys.public_key_hex[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int has_default_skill_event = 0;
|
||||
cJSON* needed_addrs = cJSON_CreateArray();
|
||||
if (!needed_addrs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < config->startup_event_count; i++) {
|
||||
startup_event_t* se = &config->startup_events[i];
|
||||
if (se->kind == config->default_skill.kind && startup_event_has_d_tag(se, config->default_skill.d_tag)) {
|
||||
has_default_skill_event = 1;
|
||||
break;
|
||||
if (!se || (se->kind != 31123 && se->kind != 31124) || !se->tags_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_Parse(se->tags_json);
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(tags);
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* d_val = find_tag_value_string(tags, "d");
|
||||
if (d_val && cJSON_IsString(d_val) && d_val->valuestring && d_val->valuestring[0] != '\0') {
|
||||
char addr[256];
|
||||
snprintf(addr,
|
||||
sizeof(addr),
|
||||
"%d:%s:%s",
|
||||
se->kind,
|
||||
config->keys.public_key_hex,
|
||||
d_val->valuestring);
|
||||
cJSON_AddItemToArray(needed_addrs, cJSON_CreateString(addr));
|
||||
}
|
||||
|
||||
cJSON_Delete(tags);
|
||||
}
|
||||
|
||||
if (!has_default_skill_event) {
|
||||
if (append_startup_event(config,
|
||||
config->default_skill.kind,
|
||||
config->default_skill.content,
|
||||
config->default_skill.tags_json ? config->default_skill.tags_json : "[]") != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (config->keys.public_key_hex[0] == '\0') {
|
||||
if (cJSON_GetArraySize(needed_addrs) == 0) {
|
||||
cJSON_Delete(needed_addrs);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char addr[256];
|
||||
snprintf(addr,
|
||||
sizeof(addr),
|
||||
"%d:%s:%s",
|
||||
config->default_skill.kind,
|
||||
config->keys.public_key_hex,
|
||||
config->default_skill.d_tag);
|
||||
|
||||
for (int i = 0; i < config->startup_event_count; i++) {
|
||||
startup_event_t* se = &config->startup_events[i];
|
||||
if (se->kind != 10123 || !se->tags_json) {
|
||||
@@ -1142,35 +1149,49 @@ int config_ensure_default_skill_startup_events(didactyl_config_t* config) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (adoption_tags_has_addr(tags, addr)) {
|
||||
cJSON_Delete(tags);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (append_adoption_addr_tag(tags, addr) != 0) {
|
||||
cJSON_Delete(tags);
|
||||
return -1;
|
||||
int needed_n = cJSON_GetArraySize(needed_addrs);
|
||||
for (int j = 0; j < needed_n; j++) {
|
||||
cJSON* addr_j = cJSON_GetArrayItem(needed_addrs, j);
|
||||
if (!addr_j || !cJSON_IsString(addr_j) || !addr_j->valuestring || addr_j->valuestring[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
if (!adoption_tags_has_addr(tags, addr_j->valuestring) && append_adoption_addr_tag(tags, addr_j->valuestring) != 0) {
|
||||
cJSON_Delete(tags);
|
||||
cJSON_Delete(needed_addrs);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
char* updated = cJSON_PrintUnformatted(tags);
|
||||
cJSON_Delete(tags);
|
||||
if (!updated) {
|
||||
cJSON_Delete(needed_addrs);
|
||||
return -1;
|
||||
}
|
||||
|
||||
free(se->tags_json);
|
||||
se->tags_json = updated;
|
||||
cJSON_Delete(needed_addrs);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) {
|
||||
cJSON_Delete(needed_addrs);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (append_adoption_addr_tag(tags, addr) != 0) {
|
||||
cJSON_Delete(tags);
|
||||
return -1;
|
||||
int needed_n = cJSON_GetArraySize(needed_addrs);
|
||||
for (int j = 0; j < needed_n; j++) {
|
||||
cJSON* addr_j = cJSON_GetArrayItem(needed_addrs, j);
|
||||
if (!addr_j || !cJSON_IsString(addr_j) || !addr_j->valuestring || addr_j->valuestring[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
if (append_adoption_addr_tag(tags, addr_j->valuestring) != 0) {
|
||||
cJSON_Delete(tags);
|
||||
cJSON_Delete(needed_addrs);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* app = cJSON_CreateArray();
|
||||
@@ -1179,17 +1200,19 @@ int config_ensure_default_skill_startup_events(didactyl_config_t* config) {
|
||||
cJSON_Delete(app);
|
||||
cJSON_Delete(scope);
|
||||
cJSON_Delete(tags);
|
||||
cJSON_Delete(needed_addrs);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddItemToArray(app, cJSON_CreateString("app"));
|
||||
cJSON_AddItemToArray(app, cJSON_CreateString("didactyl"));
|
||||
cJSON_AddItemToArray(scope, cJSON_CreateString("scope"));
|
||||
cJSON_AddItemToArray(scope, cJSON_CreateString(config->default_skill.kind == 31124 ? "private" : "public"));
|
||||
cJSON_AddItemToArray(scope, cJSON_CreateString("private"));
|
||||
cJSON_AddItemToArray(tags, app);
|
||||
cJSON_AddItemToArray(tags, scope);
|
||||
|
||||
char* tags_json = cJSON_PrintUnformatted(tags);
|
||||
cJSON_Delete(tags);
|
||||
cJSON_Delete(needed_addrs);
|
||||
if (!tags_json) {
|
||||
return -1;
|
||||
}
|
||||
@@ -1219,9 +1242,6 @@ void config_free(didactyl_config_t* config) {
|
||||
free(config->startup_events);
|
||||
}
|
||||
|
||||
free(config->default_skill.content);
|
||||
free(config->default_skill.tags_json);
|
||||
|
||||
free_cashu_wallet_mints(&config->cashu_wallet);
|
||||
|
||||
memset(config, 0, sizeof(*config));
|
||||
@@ -1280,11 +1300,6 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
config->api.port = 8484;
|
||||
snprintf(config->api.bind_address, sizeof(config->api.bind_address), "%s", "127.0.0.1");
|
||||
|
||||
config->default_skill.kind = 31124;
|
||||
config->default_skill.d_tag[0] = '\0';
|
||||
config->default_skill.content = NULL;
|
||||
config->default_skill.tags_json = NULL;
|
||||
|
||||
config->cashu_wallet.enabled = 0;
|
||||
config->cashu_wallet.mint_urls = NULL;
|
||||
config->cashu_wallet.mint_count = 0;
|
||||
@@ -1366,8 +1381,8 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_default_skill_config(root, config) != 0) {
|
||||
config_set_error("invalid default_skill configuration");
|
||||
if (parse_legacy_default_skill_to_startup_events(root, config) != 0) {
|
||||
config_set_error("invalid legacy default_skill configuration");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
|
||||
10
src/config.h
10
src/config.h
@@ -64,13 +64,6 @@ typedef struct {
|
||||
char* tags_json; // JSON array string for tags, optional
|
||||
} startup_event_t;
|
||||
|
||||
typedef struct {
|
||||
int kind;
|
||||
char d_tag[65];
|
||||
char* content;
|
||||
char* tags_json;
|
||||
} default_skill_config_t;
|
||||
|
||||
typedef struct {
|
||||
int enabled;
|
||||
int tools_enabled;
|
||||
@@ -131,12 +124,11 @@ typedef struct {
|
||||
cashu_wallet_config_t cashu_wallet;
|
||||
startup_event_t* startup_events;
|
||||
int startup_event_count;
|
||||
default_skill_config_t default_skill;
|
||||
char config_path[OW_MAX_URL_LEN];
|
||||
} didactyl_config_t;
|
||||
|
||||
int config_load(const char* path, didactyl_config_t* config);
|
||||
int config_ensure_default_skill_startup_events(didactyl_config_t* config);
|
||||
int config_ensure_startup_skill_adoption(didactyl_config_t* config);
|
||||
const char* config_last_error(void);
|
||||
void config_free(didactyl_config_t* config);
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
#ifndef DIDACTYL_DEFAULT_EVENTS_H
|
||||
#define DIDACTYL_DEFAULT_EVENTS_H
|
||||
|
||||
#define DIDACTYL_DEFAULT_SKILL_D_TAG "default_admin_dm"
|
||||
#define DIDACTYL_DEFAULT_SKILL_D_TAG "identity_and_rules"
|
||||
#define DIDACTYL_DEFAULT_SKILL_KIND 31124
|
||||
#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"default_admin_dm\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]"
|
||||
#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"identity_and_rules\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Agent identity and behavioral rules\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]"
|
||||
|
||||
#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_D_TAG "dm_history"
|
||||
#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND 31124
|
||||
#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON "[[\"d\",\"dm_history\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"DM conversation history for context continuity\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]"
|
||||
|
||||
#define DIDACTYL_STARTUP_KIND_PROFILE 0
|
||||
#define DIDACTYL_STARTUP_KIND_CONTACTS 3
|
||||
@@ -46,4 +50,8 @@ static const char* DIDACTYL_DEFAULT_SKILL_TEMPLATE =
|
||||
"- Never reveal your private key (nsec)\n"
|
||||
"- You may share your public key (npub) with anyone";
|
||||
|
||||
static const char* DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE =
|
||||
"## Recent Conversation\n\n"
|
||||
"{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}";
|
||||
|
||||
#endif
|
||||
130
src/main.c
130
src/main.c
@@ -1057,8 +1057,8 @@ int main(int argc, char** argv) {
|
||||
DEBUG_INFO("[didactyl] startup phase: admin override applied from --admin (%.16s...)", cfg.admin.pubkey);
|
||||
}
|
||||
|
||||
if (config_ensure_default_skill_startup_events(&cfg) != 0) {
|
||||
fprintf(stderr, "Failed to synthesize startup events from default_skill\n");
|
||||
if (config_ensure_startup_skill_adoption(&cfg) != 0) {
|
||||
fprintf(stderr, "Failed to synthesize startup skill adoption events\n");
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
@@ -1248,7 +1248,14 @@ int main(int argc, char** argv) {
|
||||
startup_step_begin(7, "Reconcile/persist startup state");
|
||||
if (bootstrap_mode || first_run) {
|
||||
if (nostr_handler_reconcile_startup_events() != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: reconcile startup events failed (continuing)");
|
||||
startup_step_fail(7, "Reconcile/persist startup state", "startup event reconciliation failed");
|
||||
fprintf(stderr, "Failed to publish startup events; aborting startup\n");
|
||||
nostr_block_list_cleanup();
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run");
|
||||
@@ -1317,9 +1324,9 @@ int main(int argc, char** argv) {
|
||||
fprintf(stderr, "Startup aborted: could not retrieve valid self kind 10002 within %d ms\n", kind10002_timeout_ms);
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
@@ -1333,9 +1340,9 @@ int main(int argc, char** argv) {
|
||||
startup_step_fail(11, "Discover self relay list via kind 10002", "failed to apply discovered relay list to pool");
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
@@ -1374,34 +1381,74 @@ int main(int argc, char** argv) {
|
||||
startup_step_begin(14, "Subscribe self-skill cache");
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin");
|
||||
if (nostr_handler_subscribe_self_skills() != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: subscribe self skill cache failed (continuing)");
|
||||
startup_step_fail(14, "Subscribe self-skill cache", "nostr_handler_subscribe_self_skills failed");
|
||||
DEBUG_ERROR("[didactyl] startup phase: subscribe self skill cache failed");
|
||||
fprintf(stderr, "Startup aborted: failed to subscribe self-skill cache\n");
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
|
||||
startup_step_ok(14, "Subscribe self-skill cache", NULL);
|
||||
|
||||
char startup_dm[768];
|
||||
const char* startup_name = nostr_handler_get_startup_display_name();
|
||||
int connected_relays = nostr_handler_connected_relay_count();
|
||||
time_t startup_now = time(NULL);
|
||||
struct tm startup_tm;
|
||||
char startup_time_str[32] = {0};
|
||||
if (localtime_r(&startup_now, &startup_tm)) {
|
||||
(void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm);
|
||||
} else {
|
||||
snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now);
|
||||
const int self_skill_eose_timeout_ms = 15000;
|
||||
if (nostr_handler_wait_for_self_skill_eose(self_skill_eose_timeout_ms) != 0) {
|
||||
startup_step_fail(14,
|
||||
"Subscribe self-skill cache",
|
||||
"self-skill EOSE not received within timeout");
|
||||
fprintf(stderr,
|
||||
"Startup aborted: self-skill EOSE not received within %d ms\n",
|
||||
self_skill_eose_timeout_ms);
|
||||
fprintf(stderr,
|
||||
"Check relay connectivity (connected=%d/%d) and ensure skill events exist on relays\n",
|
||||
nostr_handler_connected_relay_count(),
|
||||
cfg.relay_count);
|
||||
fprintf(stderr,
|
||||
"If this is a first run, use genesis to publish startup events\n");
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
snprintf(startup_dm,
|
||||
sizeof(startup_dm),
|
||||
"%s has started up and is online at %s (version %s, connected relays: %d/%d).",
|
||||
(startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
|
||||
startup_time_str,
|
||||
DIDACTYL_VERSION,
|
||||
connected_relays,
|
||||
cfg.relay_count);
|
||||
if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin");
|
||||
|
||||
int startup_skill_count = 0;
|
||||
int startup_adoption_count = 0;
|
||||
if (nostr_handler_validate_self_skill_cache(&startup_skill_count, &startup_adoption_count) != 0) {
|
||||
startup_step_fail(14,
|
||||
"Subscribe self-skill cache",
|
||||
"no skill events found after EOSE");
|
||||
fprintf(stderr,
|
||||
"Startup aborted: no skill events (kind 31123/31124) found in self-skill cache\n");
|
||||
fprintf(stderr,
|
||||
"The agent requires at least one skill event to operate\n");
|
||||
fprintf(stderr,
|
||||
"Run genesis to publish the default skill, or publish a skill manually\n");
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
char self_skill_detail[128];
|
||||
snprintf(self_skill_detail,
|
||||
sizeof(self_skill_detail),
|
||||
"skills=%d adoptions=%d",
|
||||
startup_skill_count,
|
||||
startup_adoption_count);
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
|
||||
startup_step_ok(14, "Subscribe self-skill cache", self_skill_detail);
|
||||
|
||||
startup_step_begin(15, "Subscribe DMs");
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin");
|
||||
if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) {
|
||||
@@ -1410,9 +1457,9 @@ int main(int argc, char** argv) {
|
||||
fprintf(stderr, "Failed to subscribe to DMs\n");
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
@@ -1497,6 +1544,29 @@ int main(int argc, char** argv) {
|
||||
|
||||
nostr_handler_refresh_relay_statuses();
|
||||
|
||||
char startup_dm[768];
|
||||
const char* startup_name = nostr_handler_get_startup_display_name();
|
||||
int connected_relays = nostr_handler_connected_relay_count();
|
||||
time_t startup_now = time(NULL);
|
||||
struct tm startup_tm;
|
||||
char startup_time_str[32] = {0};
|
||||
if (localtime_r(&startup_now, &startup_tm)) {
|
||||
(void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm);
|
||||
} else {
|
||||
snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now);
|
||||
}
|
||||
snprintf(startup_dm,
|
||||
sizeof(startup_dm),
|
||||
"%s has started up and is online at %s (version %s, connected relays: %d/%d).",
|
||||
(startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
|
||||
startup_time_str,
|
||||
DIDACTYL_VERSION,
|
||||
connected_relays,
|
||||
cfg.relay_count);
|
||||
if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin");
|
||||
}
|
||||
|
||||
startup_step_ok(18, "READY", "agent online; entering main poll loop");
|
||||
DEBUG_INFO("[didactyl] entering main poll loop");
|
||||
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
|
||||
@@ -1520,9 +1590,9 @@ int main(int argc, char** argv) {
|
||||
cashu_wallet_cleanup();
|
||||
agent_cleanup();
|
||||
nostr_block_list_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
nostr_handler_cleanup();
|
||||
llm_cleanup();
|
||||
trigger_manager_cleanup(&trigger_manager);
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
|
||||
|
||||
@@ -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 5
|
||||
#define DIDACTYL_VERSION "v0.2.5"
|
||||
#define DIDACTYL_VERSION_PATCH 13
|
||||
#define DIDACTYL_VERSION "v0.2.13"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -96,6 +96,7 @@ static external_subscription_wrapper_t* g_external_sub_wrappers = NULL;
|
||||
|
||||
static cJSON* g_self_skill_events = NULL;
|
||||
static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static volatile int g_self_skill_eose_received = 0;
|
||||
|
||||
#define DM_DEDUP_CACHE_SIZE 256
|
||||
|
||||
@@ -146,9 +147,28 @@ static void nostr_core_log_bridge(int level, const char* component, const char*
|
||||
const char* msg = message ? message : "";
|
||||
|
||||
switch (mapped) {
|
||||
case DEBUG_LEVEL_TRACE:
|
||||
case DEBUG_LEVEL_TRACE: {
|
||||
int is_websocket = (strcmp(comp, "websocket") == 0);
|
||||
int is_frame = (strncmp(msg, "SEND ", 5) == 0 || strncmp(msg, "RECV ", 5) == 0);
|
||||
size_t msg_len = strlen(msg);
|
||||
if (is_websocket && is_frame && msg_len > 384U) {
|
||||
const char* direction = (strncmp(msg, "SEND ", 5) == 0) ? "SEND" : "RECV";
|
||||
const char* frame_type = "frame";
|
||||
if (strstr(msg, "[\"EVENT\"")) frame_type = "EVENT";
|
||||
else if (strstr(msg, "[\"REQ\"")) frame_type = "REQ";
|
||||
else if (strstr(msg, "[\"CLOSE\"")) frame_type = "CLOSE";
|
||||
else if (strstr(msg, "[\"EOSE\"")) frame_type = "EOSE";
|
||||
else if (strstr(msg, "[\"NOTICE\"")) frame_type = "NOTICE";
|
||||
DEBUG_TRACE("[didactyl] [nostr:%s] %s <large %s frame suppressed, bytes=%zu>",
|
||||
comp,
|
||||
direction,
|
||||
frame_type,
|
||||
msg_len);
|
||||
break;
|
||||
}
|
||||
DEBUG_TRACE("[didactyl] [nostr:%s] %s", comp, msg);
|
||||
break;
|
||||
}
|
||||
case DEBUG_LEVEL_DEBUG:
|
||||
DEBUG_LOG("[didactyl] [nostr:%s] %s", comp, msg);
|
||||
break;
|
||||
@@ -197,7 +217,7 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
time_t created_at;
|
||||
int incoming;
|
||||
didactyl_dm_history_role_t role;
|
||||
char peer_pubkey_hex[65];
|
||||
char* content;
|
||||
} dm_history_entry_t;
|
||||
@@ -330,14 +350,17 @@ static void dm_history_clear_locked(void) {
|
||||
free(g_dm_history_ring[i].content);
|
||||
g_dm_history_ring[i].content = NULL;
|
||||
g_dm_history_ring[i].created_at = 0;
|
||||
g_dm_history_ring[i].incoming = 0;
|
||||
g_dm_history_ring[i].role = DIDACTYL_DM_HISTORY_ASSISTANT;
|
||||
g_dm_history_ring[i].peer_pubkey_hex[0] = '\0';
|
||||
}
|
||||
g_dm_history_count = 0;
|
||||
g_dm_history_next = 0;
|
||||
}
|
||||
|
||||
static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at) {
|
||||
static void dm_history_remember(const char* peer_pubkey_hex,
|
||||
const char* content,
|
||||
didactyl_dm_history_role_t role,
|
||||
time_t created_at) {
|
||||
if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U || !content) {
|
||||
return;
|
||||
}
|
||||
@@ -361,13 +384,24 @@ static void dm_history_remember(const char* peer_pubkey_hex, const char* content
|
||||
free(g_dm_history_ring[slot].content);
|
||||
g_dm_history_ring[slot].content = dup;
|
||||
g_dm_history_ring[slot].created_at = (created_at > 0) ? created_at : time(NULL);
|
||||
g_dm_history_ring[slot].incoming = incoming ? 1 : 0;
|
||||
g_dm_history_ring[slot].role = role;
|
||||
memcpy(g_dm_history_ring[slot].peer_pubkey_hex, peer_pubkey_hex, 64U);
|
||||
g_dm_history_ring[slot].peer_pubkey_hex[64] = '\0';
|
||||
|
||||
pthread_mutex_unlock(&g_dm_history_mutex);
|
||||
}
|
||||
|
||||
int nostr_handler_dm_history_remember(const char* peer_pubkey_hex,
|
||||
const char* content,
|
||||
didactyl_dm_history_role_t role) {
|
||||
if (!peer_pubkey_hex || !content) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
dm_history_remember(peer_pubkey_hex, content, role, time(NULL));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char* relay_status_str(nostr_pool_relay_status_t status) {
|
||||
switch (status) {
|
||||
case NOSTR_POOL_RELAY_DISCONNECTED:
|
||||
@@ -407,7 +441,10 @@ static void register_trigger_from_self_skill_event(cJSON* event);
|
||||
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data);
|
||||
static void on_wallet_event(cJSON* event, const char* relay_url, void* user_data);
|
||||
static void dm_history_clear_locked(void);
|
||||
static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at);
|
||||
static void dm_history_remember(const char* peer_pubkey_hex,
|
||||
const char* content,
|
||||
didactyl_dm_history_role_t role,
|
||||
time_t created_at);
|
||||
static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url);
|
||||
static void free_relay_list_local(char** relays, int relay_count);
|
||||
static int extract_relays_from_kind10002_tags(cJSON* tags, char*** out_relays, int* out_relay_count);
|
||||
@@ -429,7 +466,6 @@ static int managed_subscription_register_locked(managed_subscription_id_t id,
|
||||
static int managed_subscriptions_resubscribe_all_locked(void);
|
||||
static int managed_subscription_id_from_name(const char* name, managed_subscription_id_t* out_id);
|
||||
static int apply_runtime_kind10002_relays(cJSON* tags, const char* source_label);
|
||||
static void seed_default_skill_into_cache(void);
|
||||
|
||||
static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url) {
|
||||
if (!relays || relay_count <= 0 || !relay_url || relay_url[0] == '\0') {
|
||||
@@ -1277,50 +1313,6 @@ static void self_skill_cache_upsert_event_locked(cJSON* event) {
|
||||
cJSON_AddItemToArray(g_self_skill_events, dup);
|
||||
}
|
||||
|
||||
static void seed_default_skill_into_cache(void) {
|
||||
if (!g_cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_cfg->default_skill.kind != 31123 && g_cfg->default_skill.kind != 31124) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_cfg->keys.public_key_hex[0] == '\0' ||
|
||||
!g_cfg->default_skill.content || g_cfg->default_skill.content[0] == '\0' ||
|
||||
!g_cfg->default_skill.tags_json || g_cfg->default_skill.tags_json[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_Parse(g_cfg->default_skill.tags_json);
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(tags);
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* event = cJSON_CreateObject();
|
||||
if (!event) {
|
||||
cJSON_Delete(tags);
|
||||
return;
|
||||
}
|
||||
|
||||
const double now = (double)time(NULL);
|
||||
cJSON_AddNumberToObject(event, "kind", g_cfg->default_skill.kind);
|
||||
cJSON_AddStringToObject(event, "pubkey", g_cfg->keys.public_key_hex);
|
||||
cJSON_AddStringToObject(event, "content", g_cfg->default_skill.content);
|
||||
cJSON_AddItemToObject(event, "tags", tags);
|
||||
cJSON_AddNumberToObject(event, "created_at", now);
|
||||
cJSON_AddStringToObject(event,
|
||||
"id",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000");
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
self_skill_cache_upsert_event_locked(event);
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
|
||||
cJSON_Delete(event);
|
||||
}
|
||||
|
||||
static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) {
|
||||
if (!hex || !out_pubkey || strlen(hex) != 64U) {
|
||||
return -1;
|
||||
@@ -1716,11 +1708,6 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
(int)tier,
|
||||
received_protocol == DM_PROTOCOL_NIP17 ? "nip17" : "nip04");
|
||||
|
||||
dm_history_remember(sender_pubkey_hex,
|
||||
decrypted,
|
||||
1,
|
||||
time(NULL));
|
||||
|
||||
g_dm_callback(sender_pubkey_hex, decrypted, tier, g_dm_user_data);
|
||||
free(decrypted);
|
||||
}
|
||||
@@ -1734,6 +1721,7 @@ static void on_eose(cJSON** events, int event_count, void* user_data) {
|
||||
static void on_self_skill_eose(cJSON** events, int event_count, void* user_data) {
|
||||
(void)events;
|
||||
(void)user_data;
|
||||
g_self_skill_eose_received = 1;
|
||||
DEBUG_INFO("[didactyl] self-skill EOSE received: cached skill events=%d", event_count);
|
||||
|
||||
if (g_self_skill_eose_cb) {
|
||||
@@ -2143,7 +2131,6 @@ static void on_agent_context_event(cJSON* event, const char* relay_url, void* us
|
||||
}
|
||||
|
||||
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
(void)relay_url;
|
||||
(void)user_data;
|
||||
|
||||
if (!event || !g_cfg) {
|
||||
@@ -2154,6 +2141,20 @@ static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
|
||||
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
||||
cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id");
|
||||
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
|
||||
int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1;
|
||||
const char* d_tag = (tags && cJSON_IsArray(tags)) ? find_tag_value_local(tags, "d") : NULL;
|
||||
|
||||
DEBUG_INFO("[didactyl] self-skill cache received kind=%d d_tag=%s id=%s created_at=%lld via %s",
|
||||
kind_val,
|
||||
(d_tag && d_tag[0] != '\0') ? d_tag : "-",
|
||||
(id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "<no-id>",
|
||||
(long long)((created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0),
|
||||
relay_url ? relay_url : "unknown relay");
|
||||
|
||||
register_trigger_from_self_skill_event(event);
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
@@ -2451,6 +2452,7 @@ int nostr_handler_subscribe_self_skills(void) {
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
g_self_skill_eose_received = 0;
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
@@ -2479,9 +2481,9 @@ int nostr_handler_subscribe_self_skills(void) {
|
||||
NULL,
|
||||
0,
|
||||
0,
|
||||
NOSTR_POOL_EOSE_FULL_SET,
|
||||
30,
|
||||
120,
|
||||
NOSTR_POOL_EOSE_FIRST,
|
||||
12,
|
||||
14,
|
||||
1) != 0) {
|
||||
rc = -1;
|
||||
}
|
||||
@@ -2700,7 +2702,80 @@ void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callb
|
||||
g_self_skill_eose_user_data = user_data;
|
||||
}
|
||||
|
||||
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) {
|
||||
int nostr_handler_wait_for_self_skill_eose(int timeout_ms) {
|
||||
if (!g_pool || timeout_ms <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct timespec ts_start;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts_start) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (!g_self_skill_eose_received) {
|
||||
(void)nostr_handler_poll(100);
|
||||
|
||||
struct timespec ts_now;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts_now) != 0) {
|
||||
return -1;
|
||||
}
|
||||
long long elapsed_ms = (long long)(ts_now.tv_sec - ts_start.tv_sec) * 1000LL +
|
||||
(long long)(ts_now.tv_nsec - ts_start.tv_nsec) / 1000000LL;
|
||||
if (elapsed_ms >= (long long)timeout_ms) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count) {
|
||||
int skill_count = 0;
|
||||
int adoption_count = 0;
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
|
||||
int n = cJSON_GetArraySize(g_self_skill_events);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
|
||||
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
|
||||
cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
|
||||
cJSON* id = ev ? cJSON_GetObjectItemCaseSensitive(ev, "id") : NULL;
|
||||
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
|
||||
if (!kind || !cJSON_IsNumber(kind)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int k = (int)kind->valuedouble;
|
||||
if (k == 31123 || k == 31124) {
|
||||
skill_count++;
|
||||
} else if (k == 10123) {
|
||||
adoption_count++;
|
||||
}
|
||||
|
||||
const char* d_tag = (tags && cJSON_IsArray(tags)) ? find_tag_value_local(tags, "d") : NULL;
|
||||
DEBUG_INFO("[didactyl] self-skill cache event: kind=%d d_tag=%s id=%s created_at=%lld",
|
||||
k,
|
||||
(d_tag && d_tag[0] != '\0') ? d_tag : "-",
|
||||
(id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "<no-id>",
|
||||
(long long)((created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0));
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
|
||||
if (out_skill_count) {
|
||||
*out_skill_count = skill_count;
|
||||
}
|
||||
if (out_adoption_count) {
|
||||
*out_adoption_count = adoption_count;
|
||||
}
|
||||
|
||||
return skill_count > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
int nostr_handler_send_dm_with_role(const char* recipient_pubkey_hex,
|
||||
const char* message,
|
||||
didactyl_dm_history_role_t role) {
|
||||
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) {
|
||||
return -1;
|
||||
}
|
||||
@@ -2782,7 +2857,7 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
if (sent > 0) {
|
||||
dm_history_remember(recipient_pubkey_hex,
|
||||
message,
|
||||
0,
|
||||
role,
|
||||
time(NULL));
|
||||
}
|
||||
|
||||
@@ -2791,26 +2866,34 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
return sent > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message) {
|
||||
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) {
|
||||
return nostr_handler_send_dm_with_role(recipient_pubkey_hex,
|
||||
message,
|
||||
DIDACTYL_DM_HISTORY_ASSISTANT);
|
||||
}
|
||||
|
||||
int nostr_handler_send_dm_auto_with_role(const char* recipient_pubkey_hex,
|
||||
const char* message,
|
||||
didactyl_dm_history_role_t role) {
|
||||
if (!recipient_pubkey_hex || !message) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!g_cfg) {
|
||||
return nostr_handler_send_dm(recipient_pubkey_hex, message);
|
||||
return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role);
|
||||
}
|
||||
|
||||
if (g_cfg->dm_protocol == DM_PROTOCOL_NIP04) {
|
||||
return nostr_handler_send_dm(recipient_pubkey_hex, message);
|
||||
return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role);
|
||||
}
|
||||
|
||||
if (g_cfg->dm_protocol == DM_PROTOCOL_NIP17) {
|
||||
return nostr_handler_send_dm_nip17(recipient_pubkey_hex, message, NULL);
|
||||
return nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, message, NULL, role);
|
||||
}
|
||||
|
||||
dm_protocol_t target = sender_protocol_lookup(recipient_pubkey_hex);
|
||||
if (target == DM_PROTOCOL_NIP17) {
|
||||
int rc17 = nostr_handler_send_dm_nip17(recipient_pubkey_hex, message, NULL);
|
||||
int rc17 = nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, message, NULL, role);
|
||||
if (rc17 == 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -2818,7 +2901,13 @@ int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* mes
|
||||
recipient_pubkey_hex);
|
||||
}
|
||||
|
||||
return nostr_handler_send_dm(recipient_pubkey_hex, message);
|
||||
return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role);
|
||||
}
|
||||
|
||||
int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message) {
|
||||
return nostr_handler_send_dm_auto_with_role(recipient_pubkey_hex,
|
||||
message,
|
||||
DIDACTYL_DM_HISTORY_ASSISTANT);
|
||||
}
|
||||
|
||||
static int publish_kind_event_to_relays(int kind,
|
||||
@@ -3115,6 +3204,19 @@ static void publish_pending_startup_events_for_relay_index(int relay_index, cons
|
||||
}
|
||||
}
|
||||
|
||||
static const char* startup_kind_label(int kind) {
|
||||
switch (kind) {
|
||||
case 0: return "profile";
|
||||
case 1: return "note";
|
||||
case 3: return "contacts";
|
||||
case 10002: return "relay_list";
|
||||
case 10123: return "adoption_list";
|
||||
case 31123: return "private_skill";
|
||||
case 31124: return "skill";
|
||||
default: return "event";
|
||||
}
|
||||
}
|
||||
|
||||
int nostr_handler_reconcile_startup_events(void) {
|
||||
if (!g_cfg || !g_pool) {
|
||||
return -1;
|
||||
@@ -3143,7 +3245,66 @@ int nostr_handler_reconcile_startup_events(void) {
|
||||
publish_pending_startup_events_for_relay_index(relay_index, "startup_reconcile");
|
||||
}
|
||||
|
||||
seed_default_skill_into_cache();
|
||||
if (g_startup_publish_tracking_enabled && g_startup_published) {
|
||||
int events_published = 0;
|
||||
int relays_used = 0;
|
||||
int missing_any_event = 0;
|
||||
|
||||
for (int r = 0; r < g_cfg->relay_count; r++) {
|
||||
int used = 0;
|
||||
for (int i = 0; i < g_cfg->startup_event_count; i++) {
|
||||
size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)r;
|
||||
if (g_startup_published[slot]) {
|
||||
used = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (used) {
|
||||
relays_used++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_cfg->startup_event_count; i++) {
|
||||
startup_event_t* se = &g_cfg->startup_events[i];
|
||||
char relays_line[512];
|
||||
relays_line[0] = '\0';
|
||||
int relay_hits = 0;
|
||||
for (int r = 0; r < g_cfg->relay_count; r++) {
|
||||
size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)r;
|
||||
if (!g_startup_published[slot]) {
|
||||
continue;
|
||||
}
|
||||
relay_hits++;
|
||||
if (relays_line[0] != '\0') {
|
||||
strncat(relays_line, ", ", sizeof(relays_line) - strlen(relays_line) - 1U);
|
||||
}
|
||||
strncat(relays_line, g_cfg->relays[r], sizeof(relays_line) - strlen(relays_line) - 1U);
|
||||
}
|
||||
if (relay_hits > 0) {
|
||||
events_published++;
|
||||
DEBUG_INFO("[didactyl] startup publish detail: kind=%d (%s) -> %s",
|
||||
se->kind,
|
||||
startup_kind_label(se->kind),
|
||||
relays_line[0] ? relays_line : "<none>");
|
||||
} else {
|
||||
missing_any_event = 1;
|
||||
DEBUG_ERROR("[didactyl] startup publish missing: kind=%d (%s) was not published to any relay",
|
||||
se->kind,
|
||||
startup_kind_label(se->kind));
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] startup publish summary: %d/%d event(s) published to at least one relay; relays used=%d/%d",
|
||||
events_published,
|
||||
g_cfg->startup_event_count,
|
||||
relays_used,
|
||||
g_cfg->relay_count);
|
||||
|
||||
if (missing_any_event) {
|
||||
DEBUG_ERROR("[didactyl] startup reconcile failed: one or more startup events were not published to any relay");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -3561,7 +3722,10 @@ int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int ena
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) {
|
||||
int nostr_handler_send_dm_nip17_with_role(const char* recipient_pubkey_hex,
|
||||
const char* message,
|
||||
const char* subject,
|
||||
didactyl_dm_history_role_t role) {
|
||||
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message || recipient_pubkey_hex[0] == '\0' || message[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
@@ -3651,12 +3815,19 @@ int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* me
|
||||
|
||||
dm_history_remember(recipient_pubkey_hex,
|
||||
message,
|
||||
0,
|
||||
role,
|
||||
time(NULL));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) {
|
||||
return nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex,
|
||||
message,
|
||||
subject,
|
||||
DIDACTYL_DM_HISTORY_ASSISTANT);
|
||||
}
|
||||
|
||||
char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit) {
|
||||
if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U) {
|
||||
return strdup("[]");
|
||||
@@ -3710,7 +3881,15 @@ char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit)
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
cJSON_AddStringToObject(item, "role", entry->incoming ? "user" : "assistant");
|
||||
const char* role_s = "assistant";
|
||||
if (entry->role == DIDACTYL_DM_HISTORY_USER) {
|
||||
role_s = "user";
|
||||
} else if (entry->role == DIDACTYL_DM_HISTORY_TOOL_REQUEST) {
|
||||
role_s = "tool_request";
|
||||
} else if (entry->role == DIDACTYL_DM_HISTORY_TOOL_RESPONSE) {
|
||||
role_s = "tool_response";
|
||||
}
|
||||
cJSON_AddStringToObject(item, "role", role_s);
|
||||
cJSON_AddStringToObject(item, "content", entry->content);
|
||||
cJSON_AddNumberToObject(item, "created_at", (double)entry->created_at);
|
||||
cJSON_AddItemToArray(arr, item);
|
||||
|
||||
@@ -13,6 +13,13 @@ typedef enum {
|
||||
DIDACTYL_SENDER_STRANGER = 3
|
||||
} didactyl_sender_tier_t;
|
||||
|
||||
typedef enum {
|
||||
DIDACTYL_DM_HISTORY_USER = 1,
|
||||
DIDACTYL_DM_HISTORY_ASSISTANT = 2,
|
||||
DIDACTYL_DM_HISTORY_TOOL_REQUEST = 3,
|
||||
DIDACTYL_DM_HISTORY_TOOL_RESPONSE = 4
|
||||
} didactyl_dm_history_role_t;
|
||||
|
||||
typedef void (*dm_callback_t)(const char* sender_pubkey_hex,
|
||||
const char* message,
|
||||
didactyl_sender_tier_t tier,
|
||||
@@ -42,7 +49,13 @@ void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callb
|
||||
int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data);
|
||||
int nostr_handler_subscribe_wallet_events(void);
|
||||
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message);
|
||||
int nostr_handler_send_dm_with_role(const char* recipient_pubkey_hex,
|
||||
const char* message,
|
||||
didactyl_dm_history_role_t role);
|
||||
int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message);
|
||||
int nostr_handler_send_dm_auto_with_role(const char* recipient_pubkey_hex,
|
||||
const char* message,
|
||||
didactyl_dm_history_role_t role);
|
||||
int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags, nostr_publish_result_t* out_result);
|
||||
void nostr_handler_publish_result_free(nostr_publish_result_t* result);
|
||||
char* nostr_handler_query_json(cJSON* filter, int timeout_ms);
|
||||
@@ -59,6 +72,8 @@ nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
|
||||
int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription);
|
||||
int nostr_handler_poll(int timeout_ms);
|
||||
int nostr_handler_reconcile_startup_events(void);
|
||||
int nostr_handler_wait_for_self_skill_eose(int timeout_ms);
|
||||
int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count);
|
||||
void nostr_handler_refresh_relay_statuses(void);
|
||||
int nostr_handler_sync_relays_from_config(void);
|
||||
char* nostr_handler_get_self_skill_events_json(void);
|
||||
@@ -79,6 +94,13 @@ char* nostr_handler_relay_info_json(const char* relay_url);
|
||||
char* nostr_handler_subscription_status_json(void);
|
||||
int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int enabled, int enabled_set);
|
||||
int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject);
|
||||
int nostr_handler_send_dm_nip17_with_role(const char* recipient_pubkey_hex,
|
||||
const char* message,
|
||||
const char* subject,
|
||||
didactyl_dm_history_role_t role);
|
||||
int nostr_handler_dm_history_remember(const char* peer_pubkey_hex,
|
||||
const char* content,
|
||||
didactyl_dm_history_role_t role);
|
||||
char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit);
|
||||
void nostr_handler_cleanup(void);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -372,17 +372,64 @@ static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_ta
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int tags_json_get_d_tag_local(const char* tags_json, char* out, size_t out_size) {
|
||||
if (!tags_json || !out || out_size == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out[0] = '\0';
|
||||
cJSON* tags = cJSON_Parse(tags_json);
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(tags);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
||||
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
||||
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue;
|
||||
if (strcmp(k->valuestring, "d") == 0 && v->valuestring[0] != '\0') {
|
||||
snprintf(out, out_size, "%s", v->valuestring);
|
||||
cJSON_Delete(tags);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int upsert_startup_event(didactyl_config_t* cfg, int kind, const char* content, const char* tags_json) {
|
||||
if (!cfg || !content || !tags_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int is_skill_kind = (kind == 31123 || kind == 31124);
|
||||
char incoming_d[128] = {0};
|
||||
int incoming_has_d = (is_skill_kind && tags_json_get_d_tag_local(tags_json, incoming_d, sizeof(incoming_d)) == 0);
|
||||
|
||||
for (int i = 0; i < cfg->startup_event_count; i++) {
|
||||
startup_event_t* se = &cfg->startup_events[i];
|
||||
if (se->kind != kind) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_skill_kind) {
|
||||
if (!incoming_has_d) {
|
||||
continue;
|
||||
}
|
||||
char existing_d[128] = {0};
|
||||
if (tags_json_get_d_tag_local(se->tags_json, existing_d, sizeof(existing_d)) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(existing_d, incoming_d) != 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
char* content_dup = strdup(content);
|
||||
char* tags_dup = strdup(tags_json);
|
||||
if (!content_dup || !tags_dup) {
|
||||
@@ -495,35 +542,22 @@ static int apply_default_startup_events(didactyl_config_t* cfg, const char* agen
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char* agent_name) {
|
||||
static int configure_default_skills_for_agent(didactyl_config_t* cfg, const char* agent_name) {
|
||||
if (!cfg || !agent_name || agent_name[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
free(cfg->default_skill.content);
|
||||
cfg->default_skill.content = NULL;
|
||||
free(cfg->default_skill.tags_json);
|
||||
cfg->default_skill.tags_json = NULL;
|
||||
|
||||
cfg->default_skill.kind = DIDACTYL_DEFAULT_SKILL_KIND;
|
||||
snprintf(cfg->default_skill.d_tag, sizeof(cfg->default_skill.d_tag), "%s", DIDACTYL_DEFAULT_SKILL_D_TAG);
|
||||
|
||||
int needed = snprintf(NULL, 0, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE);
|
||||
if (needed <= 0) {
|
||||
if (upsert_startup_event(cfg,
|
||||
DIDACTYL_DEFAULT_SKILL_KIND,
|
||||
DIDACTYL_DEFAULT_SKILL_TEMPLATE,
|
||||
DIDACTYL_DEFAULT_SKILL_TAGS_JSON) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cfg->default_skill.content = (char*)malloc((size_t)needed + 1U);
|
||||
if (!cfg->default_skill.content) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
snprintf(cfg->default_skill.content, (size_t)needed + 1U, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE);
|
||||
|
||||
cfg->default_skill.tags_json = strdup(DIDACTYL_DEFAULT_SKILL_TAGS_JSON);
|
||||
if (!cfg->default_skill.tags_json) {
|
||||
free(cfg->default_skill.content);
|
||||
cfg->default_skill.content = NULL;
|
||||
if (upsert_startup_event(cfg,
|
||||
DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND,
|
||||
DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE,
|
||||
DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -2157,8 +2191,8 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
if (prompt_llm_config(cfg) != 0) return -1;
|
||||
}
|
||||
|
||||
if (configure_default_skill_for_agent(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to prepare default skill event content.%s\n", ANSI_RED, ANSI_RESET);
|
||||
if (configure_default_skills_for_agent(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to prepare default startup skill events.%s\n", ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -31,6 +31,25 @@ static cJSON* parse_args_local(const char* args_json) {
|
||||
return args;
|
||||
}
|
||||
|
||||
static int append_text_local(char** buf, size_t* cap, size_t* used, const char* s) {
|
||||
if (!buf || !cap || !used || !s) return -1;
|
||||
size_t n = strlen(s);
|
||||
if (*used + n + 1U > *cap) {
|
||||
size_t next = *cap;
|
||||
while (*used + n + 1U > next) {
|
||||
next = (next == 0U) ? 256U : (next * 2U);
|
||||
}
|
||||
char* grown = (char*)realloc(*buf, next);
|
||||
if (!grown) return -1;
|
||||
*buf = grown;
|
||||
*cap = next;
|
||||
}
|
||||
memcpy(*buf + *used, s, n);
|
||||
*used += n;
|
||||
(*buf)[*used] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* execute_message_current(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
@@ -63,13 +82,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 +123,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 +149,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 +175,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 +201,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 +281,172 @@ 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;
|
||||
}
|
||||
|
||||
int format_text = 0;
|
||||
cJSON* format_j = cJSON_GetObjectItemCaseSensitive(args, "format");
|
||||
if (format_j && cJSON_IsString(format_j) && format_j->valuestring) {
|
||||
if (strcmp(format_j->valuestring, "text") == 0) {
|
||||
format_text = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const char* peer_pubkey = ctx->cfg->admin.pubkey;
|
||||
cJSON* peer_j = cJSON_GetObjectItemCaseSensitive(args, "peer_pubkey");
|
||||
if (peer_j && cJSON_IsString(peer_j) && peer_j->valuestring && peer_j->valuestring[0] != '\0') {
|
||||
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* rendered = NULL;
|
||||
|
||||
if (format_text) {
|
||||
size_t cap = 256U;
|
||||
size_t used = 0U;
|
||||
rendered = (char*)malloc(cap);
|
||||
if (!rendered) {
|
||||
cJSON_Delete(filtered);
|
||||
return json_error_local("failed to serialize dm history");
|
||||
}
|
||||
rendered[0] = '\0';
|
||||
|
||||
int m = cJSON_GetArraySize(filtered);
|
||||
for (int i = 0; i < m; i++) {
|
||||
cJSON* item = cJSON_GetArrayItem(filtered, i);
|
||||
cJSON* role = item ? cJSON_GetObjectItemCaseSensitive(item, "role") : NULL;
|
||||
cJSON* content = item ? cJSON_GetObjectItemCaseSensitive(item, "content") : NULL;
|
||||
if (!role || !content || !cJSON_IsString(role) || !cJSON_IsString(content) ||
|
||||
!role->valuestring || !content->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* prefix = (strcmp(role->valuestring, "user") == 0) ? "User: " : "Assistant: ";
|
||||
if (append_text_local(&rendered, &cap, &used, prefix) != 0 ||
|
||||
append_text_local(&rendered, &cap, &used, content->valuestring) != 0 ||
|
||||
append_text_local(&rendered, &cap, &used, "\n") != 0) {
|
||||
free(rendered);
|
||||
cJSON_Delete(filtered);
|
||||
return json_error_local("failed to serialize dm history");
|
||||
}
|
||||
}
|
||||
|
||||
if (used > 0U && rendered[used - 1] == '\n') {
|
||||
rendered[used - 1] = '\0';
|
||||
}
|
||||
} else {
|
||||
rendered = cJSON_PrintUnformatted(filtered);
|
||||
if (!rendered) {
|
||||
cJSON_Delete(filtered);
|
||||
return json_error_local("failed to serialize dm history");
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(filtered);
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) {
|
||||
free(rendered);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "content", rendered);
|
||||
free(rendered);
|
||||
|
||||
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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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\":\"identity_and_rules\"} (slash shorthand also works: /skill_get identity_and_rules)");
|
||||
cJSON_AddStringToObject(t24a_params, "type", "object");
|
||||
cJSON_AddItemToObject(t24a_params, "properties", t24a_props);
|
||||
cJSON_AddItemToObject(t24a_params, "required", t24a_required);
|
||||
|
||||
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\":\"identity_and_rules\"}");
|
||||
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\":\"identity_and_rules\",\"kind\":31124,\"scope\":\"private\",\"content\":\"...\",\"tags\":[[\"d\",\"identity_and_rules\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"]]}}");
|
||||
cJSON_AddStringToObject(t24c_params, "type", "object");
|
||||
cJSON_AddItemToObject(t24c_params, "properties", t24c_props);
|
||||
|
||||
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,45 @@ 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,\"format\":\"json\"} or with explicit peer {\"peer_pubkey\":\"<hex64>\",\"limit\":20,\"format\":\"text\"}; template usage: {{nostr_dm_history({\"limit\":12,\"format\":\"text\"})}}");
|
||||
cJSON_AddStringToObject(t43_params, "type", "object");
|
||||
cJSON_AddItemToObject(t43_params, "properties", t43_props);
|
||||
|
||||
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* p_dm_history_format = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_dm_history_format, "type", "string");
|
||||
cJSON_AddStringToObject(p_dm_history_format,
|
||||
"description",
|
||||
"Output format: 'json' (default) or 'text' (User:/Assistant: lines)");
|
||||
cJSON_AddItemToObject(t43_props, "format", p_dm_history_format);
|
||||
|
||||
cJSON_AddItemToObject(t43_fn, "parameters", t43_params);
|
||||
cJSON_AddItemToObject(t43, "function", t43_fn);
|
||||
cJSON_AddItemToArray(tools, t43);
|
||||
|
||||
cJSON* t44 = cJSON_CreateObject();
|
||||
cJSON* t44_fn = cJSON_CreateObject();
|
||||
cJSON* t44_params = cJSON_CreateObject();
|
||||
|
||||
Reference in New Issue
Block a user