Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9526c3a20 | ||
|
|
6edad064c6 | ||
|
|
656d251a2a | ||
|
|
6ad668b693 |
1528
.skills-edit-check.mjs
Normal file
1528
.skills-edit-check.mjs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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.24
|
||||
## Current Status — v0.2.28
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.2.24 — Improve skills editor trigger filters with per-trigger helper widgets and presets
|
||||
> Last release update: v0.2.28 — Updated LT and Anvil genesis configs: unified DM history context as startup skill, migrated default skills into startup_events, rotated admin npub to npub1rmz9..., and redeployed/restarted didactyl+simon+anvil
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
@@ -20,8 +20,8 @@ Minimum practical sections:
|
||||
|
||||
- `key.nsec` (or runtime `--nsec` / `DIDACTYL_NSEC`)
|
||||
- `admin.pubkey`
|
||||
- `llm`
|
||||
- `startup_events` (must include kind `10002` relay tags)
|
||||
- `encrypted_events` with a `kind:30078` / `d_tag:user-settings` payload containing `global_llm` and `didactyl`
|
||||
|
||||
Typical optional sections:
|
||||
|
||||
@@ -96,17 +96,39 @@ Security behavior:
|
||||
|
||||
## Encrypted Config Events
|
||||
|
||||
Didactyl exposes config persistence tools for encrypted self-config on Nostr:
|
||||
Didactyl uses encrypted runtime config in two ways:
|
||||
|
||||
- `config_store` — publish encrypted kind `30078` config by `d_tag`
|
||||
- `config_recall` — query+decrypt kind `30078` config by `d_tag`
|
||||
1. At bootstrap time via `genesis.jsonc` `encrypted_events` entries
|
||||
2. At runtime via encrypted kind `30078` self-events (`config_store` / `config_recall`)
|
||||
|
||||
Recommended tags:
|
||||
`encrypted_events` format:
|
||||
|
||||
- `d=llm_config`
|
||||
- `d=agent_config`
|
||||
- `kind` (currently `30078`)
|
||||
- `d_tag` (required: `user-settings`)
|
||||
- `content` (JSON string payload to encrypt and publish)
|
||||
|
||||
These are encrypted to self with NIP-44.
|
||||
`d=user-settings` payload shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 2,
|
||||
"updatedAt": 0,
|
||||
"global_llm": {
|
||||
"provider": "openai",
|
||||
"api_key": "sk-...",
|
||||
"model": "gpt-4o-mini",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"didactyl": {
|
||||
"admin_pubkey": "npub1...",
|
||||
"dm_protocol": "nip04"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These values are encrypted to self with NIP-44 before publish.
|
||||
|
||||
---
|
||||
|
||||
@@ -116,6 +138,8 @@ These are encrypted to self with NIP-44.
|
||||
|
||||
That relay list is used as the initial network attachment for querying existing state and publishing startup events.
|
||||
|
||||
`encrypted_events` are a separate section and are not part of relay-list derivation.
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes (v0.2.0)
|
||||
|
||||
@@ -18,16 +18,15 @@
|
||||
// Supported values: "nip04", "nip17", or "both".
|
||||
"dm_protocol": "nip04",
|
||||
|
||||
// ─── LLM Provider ──────────────────────────────────────────────────
|
||||
// OpenAI-compatible endpoint settings.
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"api_key": "sk-REPLACE_WITH_API_KEY",
|
||||
"model": "gpt-4o-mini",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
// ─── Encrypted Startup Config Events ───────────────────────────────
|
||||
// These are published as NIP-44 encrypted kind 30078 self-events on first run.
|
||||
"encrypted_events": [
|
||||
{
|
||||
"kind": 30078,
|
||||
"d_tag": "user-settings",
|
||||
"content": "{\"v\":2,\"updatedAt\":0,\"global_llm\":{\"provider\":\"openai\",\"api_key\":\"sk-REPLACE_WITH_API_KEY\",\"model\":\"gpt-4o-mini\",\"base_url\":\"https://api.openai.com/v1\",\"max_tokens\":512,\"temperature\":0.7},\"didactyl\":{\"admin_pubkey\":\"npub1REPLACE_WITH_ADMIN_PUBKEY\",\"dm_protocol\":\"nip04\",\"max_turns\":40}}"
|
||||
}
|
||||
],
|
||||
|
||||
// ─── HTTP Admin API ────────────────────────────────────────────────
|
||||
"api": {
|
||||
@@ -60,7 +59,6 @@
|
||||
["r", "wss://relay.damus.io"],
|
||||
["r", "wss://relay.primal.net"]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
@@ -76,12 +74,12 @@
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "## Recent Conversation\n\n{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}",
|
||||
"content": "## DM History Context\n\n### Instructions\n- Reference prior conversation naturally when it's relevant to the current request.\n- Do not repeat entire DM history back to the user unless explicitly asked.\n- Use this context to avoid asking questions that were already answered in recent messages.\n\n### Recent DM History (last 10 messages)\n\n{{nostr_dm_history({\"limit\":10,\"format\":\"text\"})}}",
|
||||
"tags": [
|
||||
["d", "dm_history"],
|
||||
["d", "dm_history_context"],
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "DM conversation history for context continuity"],
|
||||
["description", "DM history context"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"]
|
||||
]
|
||||
|
||||
80
genesis.lt.didactyl.jsonc
Normal file
80
genesis.lt.didactyl.jsonc
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
// ─── Agent Identity Keys ───────────────────────────────────────────
|
||||
// Keep nsec private; npub/hex fields can be used for verification/debugging.
|
||||
"key": {
|
||||
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e"
|
||||
},
|
||||
|
||||
// ─── Administrator ─────────────────────────────────────────────────
|
||||
// Admin pubkey (npub or hex) controls privileged interactions.
|
||||
"admin": {
|
||||
"pubkey": "npub1rmz9gu6de0m0u4ysrn39crrud099ahvfgs6pvasl4hpjr5ud7yus54xv06"
|
||||
},
|
||||
|
||||
// ─── DM Protocol ──────────────────────────────────────────────────
|
||||
// Supported values: "nip04", "nip17", or "both".
|
||||
"dm_protocol": "nip04",
|
||||
|
||||
// ─── HTTP Admin API ───────────────────────────────────────────────
|
||||
// Local API for model/context inspection and runtime controls.
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"port": 8484,
|
||||
"bind_address": "127.0.0.1"
|
||||
},
|
||||
|
||||
// ─── Encrypted Startup Config Events ──────────────────────────────
|
||||
// These are published as NIP-44 encrypted kind 30078 self-events on first run.
|
||||
"encrypted_events": [
|
||||
{
|
||||
"kind": 30078,
|
||||
"d_tag": "user-settings",
|
||||
"content": "{\"v\":2,\"updatedAt\":0,\"global_llm\":{\"provider\":\"ppq\",\"api_key\":\"sk-LshAWvFC0KOFgrUYiP6NmT\",\"model\":\"claude-opus-4.6\",\"base_url\":\"https://api.ppq.ai\",\"max_tokens\":200000,\"temperature\":0.7},\"didactyl\":{\"admin_pubkey\":\"npub1rmz9gu6de0m0u4ysrn39crrud099ahvfgs6pvasl4hpjr5ud7yus54xv06\",\"dm_protocol\":\"nip04\",\"max_turns\":40}}"
|
||||
}
|
||||
],
|
||||
|
||||
// ─── Startup Events ───────────────────────────────────────────────
|
||||
// Minimal relay list required for relay pool initialization.
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["r", "wss://relay.damus.io"],
|
||||
["r", "wss://relay.primal.net"],
|
||||
["r", "wss://relay.laantungir.net"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "## DM History Context\n\n### Instructions\n- Reference prior conversation naturally when it's relevant to the current request.\n- Do not repeat entire DM history back to the user unless explicitly asked.\n- Use this context to avoid asking questions that were already answered in recent messages.\n\n### Recent DM History (last 10 messages)\n\n{{nostr_dm_history({\"limit\":10,\"format\":\"text\"})}}",
|
||||
"tags": [
|
||||
["d", "dm_history_context"],
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "DM history context"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `local_shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Task Management\n- Maintain and use your internal task list as short-term working memory.\n- Break long or complex actions into clear tasks before executing them.\n- Update task status as you complete steps so your plan stays accurate.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.\n\n---template---\n\n- section: admin_identity\n role: system\n tool: admin_identity\n skip_if_empty: true\n\n- section: admin_profile\n role: system\n tool: nostr_admin_profile\n skip_if_empty: true\n\n- section: admin_contacts\n role: system\n tool: nostr_admin_contacts\n skip_if_empty: true\n\n- section: admin_relays\n role: system\n tool: nostr_admin_relays\n skip_if_empty: true\n\n- section: admin_notes\n role: system\n tool: nostr_admin_notes\n skip_if_empty: true\n\n- section: agent_identity\n role: system\n tool: agent_identity\n skip_if_empty: true\n\n- section: agent_profile\n role: system\n tool: nostr_agent_profile\n skip_if_empty: true\n\n- section: agent_contacts\n role: system\n tool: nostr_agent_contacts\n skip_if_empty: true\n\n- section: agent_relays\n role: system\n tool: nostr_agent_relays\n skip_if_empty: true\n\n- section: agent_notes\n role: system\n tool: nostr_agent_notes\n skip_if_empty: true\n\n- section: tasks\n role: system\n tool: task_list\n skip_if_empty: true\n\n- section: dm_history\n role: expand\n limit: 12\n\n- section: conversation\n role: user\n tool: message_current\n skip_if_empty: true",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"didactyl-default"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
101
genesis.lt.simon.jsonc
Normal file
101
genesis.lt.simon.jsonc
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
// ─── Agent Identity Keys ───────────────────────────────────────────
|
||||
// Keep nsec private; npub/hex fields can be used for verification/debugging.
|
||||
"key": {
|
||||
"nsec": "nsec17yzm0lyr479z73sgnpf9yuxv0nwclpaff0k45npsh49nddpc3fhqmhh2ph"
|
||||
},
|
||||
// ─── Administrator ─────────────────────────────────────────────────
|
||||
// Admin pubkey (npub or hex) controls privileged interactions.
|
||||
"admin": {
|
||||
"pubkey": "npub1rmz9gu6de0m0u4ysrn39crrud099ahvfgs6pvasl4hpjr5ud7yus54xv06"
|
||||
},
|
||||
// ─── DM Protocol ──────────────────────────────────────────────────
|
||||
// Supported values: "nip04", "nip17", or "both".
|
||||
"dm_protocol": "nip04",
|
||||
// ─── HTTP Admin API ───────────────────────────────────────────────
|
||||
// Local API for model/context inspection and runtime controls.
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"port": 8485,
|
||||
"bind_address": "127.0.0.1"
|
||||
},
|
||||
// ─── Encrypted Startup Config Events ──────────────────────────────
|
||||
// These are published as NIP-44 encrypted kind 30078 self-events on first run.
|
||||
"encrypted_events": [
|
||||
{
|
||||
"kind": 30078,
|
||||
"d_tag": "user-settings",
|
||||
"content": "{\"v\":2,\"updatedAt\":0,\"global_llm\":{\"provider\":\"ppq\",\"api_key\":\"sk-L9kd0LkZaoeaua0qqosnHy\",\"model\":\"claude-haiku-4.5\",\"base_url\":\"https://api.ppq.ai\",\"max_tokens\":512,\"temperature\":0.7},\"didactyl\":{\"admin_pubkey\":\"npub1rmz9gu6de0m0u4ysrn39crrud099ahvfgs6pvasl4hpjr5ud7yus54xv06\",\"dm_protocol\":\"nip04\",\"max_turns\":40}}"
|
||||
}
|
||||
],
|
||||
// ─── Startup Events ───────────────────────────────────────────────
|
||||
// Minimal relay list required for relay pool initialization.
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"r",
|
||||
"wss://relay.damus.io"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://relay.primal.net"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://relay.laantungir.net"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "## DM History Context\n\n### Instructions\n- Reference prior conversation naturally when it's relevant to the current request.\n- Do not repeat entire DM history back to the user unless explicitly asked.\n- Use this context to avoid asking questions that were already answered in recent messages.\n\n### Recent DM History (last 10 messages)\n\n{{nostr_dm_history({\"limit\":10,\"format\":\"text\"})}}",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"dm_history_context"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
],
|
||||
[
|
||||
"description",
|
||||
"DM history context"
|
||||
],
|
||||
[
|
||||
"trigger",
|
||||
"dm"
|
||||
],
|
||||
[
|
||||
"filter",
|
||||
"{\"from\":\"admin\"}"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `local_shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Task Management\n- Maintain and use your internal task list as short-term working memory.\n- Break long or complex actions into clear tasks before executing them.\n- Update task status as you complete steps so your plan stays accurate.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.\n\n---template---\n\n- section: admin_identity\n role: system\n tool: admin_identity\n skip_if_empty: true\n\n- section: admin_profile\n role: system\n tool: nostr_admin_profile\n skip_if_empty: true\n\n- section: admin_contacts\n role: system\n tool: nostr_admin_contacts\n skip_if_empty: true\n\n- section: admin_relays\n role: system\n tool: nostr_admin_relays\n skip_if_empty: true\n\n- section: admin_notes\n role: system\n tool: nostr_admin_notes\n skip_if_empty: true\n\n- section: agent_identity\n role: system\n tool: agent_identity\n skip_if_empty: true\n\n- section: agent_profile\n role: system\n tool: nostr_agent_profile\n skip_if_empty: true\n\n- section: agent_contacts\n role: system\n tool: nostr_agent_contacts\n skip_if_empty: true\n\n- section: agent_relays\n role: system\n tool: nostr_agent_relays\n skip_if_empty: true\n\n- section: agent_notes\n role: system\n tool: nostr_agent_notes\n skip_if_empty: true\n\n- section: tasks\n role: system\n tool: task_list\n skip_if_empty: true\n\n- section: dm_history\n role: expand\n limit: 12\n\n- section: conversation\n role: user\n tool: message_current\n skip_if_empty: true",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"didactyl-default"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
285
plans/global_llm_alignment.md
Normal file
285
plans/global_llm_alignment.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# Global LLM Config Alignment — Plan
|
||||
|
||||
Align LLM configuration storage across Didactyl (C agent) and client-ndk (web pages) so both projects read and write the same canonical schema, as specified in [SETTINGS.md](../../client-ndk/docs/SETTINGS.md).
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
LLM provider config is currently stored in two incompatible ways:
|
||||
|
||||
| Project | Storage | d-tag | Namespace | Schema |
|
||||
|---------|---------|-------|-----------|--------|
|
||||
| Didactyl agent | `kind 30078` under agent pubkey | `llm_config` | flat root | `{ provider, api_key, model, base_url, max_tokens, temperature }` |
|
||||
| client-ndk pages | `kind 30078` under user pubkey | `user-settings` | `settings.ai` | `{ provider, api_key, model, base_url, max_tokens, temperature, providers[], favorites[] }` |
|
||||
| skills-edit.html | `kind 30078` under user pubkey | `llm_config` | flat root | Same as Didactyl — standalone event |
|
||||
|
||||
This creates three issues:
|
||||
1. **skills-edit.html** writes a standalone `d:llm_config` event under the user pubkey, duplicating the centralized `settings.ai` data
|
||||
2. **Didactyl** cannot read the admin user's LLM preferences from their `d:user-settings` event
|
||||
3. **Namespace mismatch** — the centralized settings use `ai` (v1) / `global_llm` (v2 target) while Didactyl uses flat fields at root level
|
||||
|
||||
---
|
||||
|
||||
## Target State
|
||||
|
||||
Per [SETTINGS.md §3](../../client-ndk/docs/SETTINGS.md) and [§8](../../client-ndk/docs/SETTINGS.md):
|
||||
|
||||
### Canonical Schema — `global_llm` namespace
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "ppq",
|
||||
"api_key": "sk-...",
|
||||
"model": "claude-opus-4.6",
|
||||
"base_url": "https://api.ppq.ai",
|
||||
"max_tokens": 200000,
|
||||
"temperature": 0.7,
|
||||
"providers": [
|
||||
{
|
||||
"name": "ppq",
|
||||
"base_url": "https://api.ppq.ai",
|
||||
"api_key": "sk-...",
|
||||
"models": ["claude-opus-4.6", "claude-haiku-4.5"]
|
||||
}
|
||||
],
|
||||
"favorites": ["claude-opus-4.6"]
|
||||
}
|
||||
```
|
||||
|
||||
### Storage locations after alignment
|
||||
|
||||
| Actor | Event | d-tag | Where LLM lives | Notes |
|
||||
|-------|-------|-------|------------------|-------|
|
||||
| User pubkey | `kind 30078` | `user-settings` | `global_llm` namespace | Canonical source for user LLM prefs |
|
||||
| Agent pubkey | `kind 30078` | `llm_config` | flat root | Agent runtime config — flat fields are a subset of `global_llm` |
|
||||
| User pubkey | `kind 30078` | `llm_config` | **DEPRECATED** | skills-edit.html stops writing this |
|
||||
|
||||
### Cross-project reading
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph User Pubkey Events
|
||||
US[d:user-settings<br/>global_llm namespace]
|
||||
end
|
||||
subgraph Agent Pubkey Events
|
||||
AL[d:llm_config<br/>flat fields]
|
||||
end
|
||||
subgraph Consumers
|
||||
SE[skills-edit.html]
|
||||
AI[ai.html / skills-tv.html]
|
||||
DA[Didactyl Agent]
|
||||
end
|
||||
SE -->|getUserSettings - global_llm| US
|
||||
AI -->|getUserSettings - global_llm| US
|
||||
DA -->|recall own d:llm_config| AL
|
||||
DA -->|optionally read admin global_llm| US
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Detailed Changes
|
||||
|
||||
### Phase 1: genesis.jsonc alignment
|
||||
|
||||
**File**: [`genesis.jsonc`](../../didactyl/genesis.jsonc)
|
||||
|
||||
Current `"llm"` section already uses compatible flat field names. The only issue is `provider` contains a URL (`"https://api.ppq.ai"`) instead of a short name (`"ppq"`).
|
||||
|
||||
**Change**: Update `provider` to be a short name. The `base_url` field already carries the URL.
|
||||
|
||||
```jsonc
|
||||
"llm": {
|
||||
"provider": "ppq", // was "https://api.ppq.ai"
|
||||
"api_key": "sk-...",
|
||||
"model": "claude-opus-4.6",
|
||||
"base_url": "https://api.ppq.ai",
|
||||
"max_tokens": 200000,
|
||||
"temperature": 0.7
|
||||
}
|
||||
```
|
||||
|
||||
Also update [`genesis.jsonc.example`](../../didactyl/genesis.jsonc.example) to match.
|
||||
|
||||
**No C struct changes needed** — `llm_config_t.provider` is `char[32]` which holds short names fine.
|
||||
|
||||
### Phase 2: Didactyl C-side — persist writes global_llm-compatible fields
|
||||
|
||||
**File**: [`src/tools/tool_model.c`](../../didactyl/src/tools/tool_model.c:33) — `persist_llm_config_nostr()`
|
||||
|
||||
Already writes: `provider`, `api_key`, `model`, `base_url`, `max_tokens`, `temperature` — these are the exact flat fields in `global_llm`. **No change needed** to the persist path.
|
||||
|
||||
**File**: [`src/main.c`](../../didactyl/src/main.c:844) — `persist_runtime_config_to_nostr()`
|
||||
|
||||
Same flat fields. **No change needed**.
|
||||
|
||||
### Phase 3: Didactyl C-side — recall from admin `d:user-settings`
|
||||
|
||||
**File**: [`src/main.c`](../../didactyl/src/main.c:882) — `recover_missing_runtime_config_from_nostr()`
|
||||
|
||||
Currently only queries the agent's own pubkey for `d:llm_config`. Add a fallback path:
|
||||
|
||||
1. After failing to find own `d:llm_config`, query admin pubkey for `d:user-settings`
|
||||
2. Decrypt with NIP-44 using admin pubkey as sender
|
||||
3. Parse JSON, extract `global_llm` object (fall back to `ai` for v1 compat)
|
||||
4. Pass the extracted object to `apply_recalled_llm_config()`
|
||||
|
||||
**File**: [`src/main.c`](../../didactyl/src/main.c:729) — `apply_recalled_llm_config()`
|
||||
|
||||
Already reads flat fields from a JSON object. If we pass it the `global_llm` sub-object, it works as-is. It ignores unknown fields like `providers` and `favorites`. **No change needed**.
|
||||
|
||||
**New function**: `fetch_admin_user_settings_llm()` in `main.c`:
|
||||
|
||||
```c
|
||||
static int fetch_admin_user_settings_llm(didactyl_config_t* cfg, char** out_plaintext) {
|
||||
// 1. Query kind:30078, authors:[admin_pubkey], #d:["user-settings"]
|
||||
// 2. NIP-44 decrypt content (admin encrypted to self — agent cannot decrypt)
|
||||
// WAIT: Agent cannot decrypt admin's self-encrypted content!
|
||||
// This path only works if admin explicitly shares config with agent.
|
||||
// Alternative: Agent reads its OWN d:user-settings if one exists.
|
||||
// 3. Parse JSON, extract global_llm or ai sub-object
|
||||
// 4. Serialize sub-object to *out_plaintext
|
||||
}
|
||||
```
|
||||
|
||||
**Important constraint**: The admin's `d:user-settings` is NIP-44 self-encrypted (admin encrypts to admin). The agent cannot decrypt it because it does not have the admin's private key.
|
||||
|
||||
**Revised approach**: Instead of reading the admin's settings directly, the agent should:
|
||||
|
||||
1. **Primary**: Read its own `d:llm_config` (current behavior — works)
|
||||
2. **Secondary**: If the admin wants to push LLM config to the agent, they use the `model_set` tool via DM, which calls [`persist_llm_config_nostr()`](../../didactyl/src/tools/tool_model.c:33) to write `d:llm_config` under the agent pubkey
|
||||
3. **New path**: The web UI (skills-edit or a future agent-config page) can write `d:llm_config` under the **agent's** pubkey by publishing a NIP-44 encrypted event addressed to the agent. The agent can then decrypt this on recall.
|
||||
|
||||
Actually, re-reading [SETTINGS.md §8](../../client-ndk/docs/SETTINGS.md):
|
||||
|
||||
> When Didactyl wants to know the user's LLM preferences:
|
||||
> `kind:30078, authors:[admin_pubkey], #d:[user-settings]`
|
||||
> Parse the `global_llm` field.
|
||||
|
||||
This implies the agent CAN read it. But the content is NIP-44 self-encrypted by the admin. The agent would need the admin to encrypt a copy for the agent, OR the admin publishes their settings unencrypted (unlikely for API keys), OR there is a shared-secret mechanism.
|
||||
|
||||
**Resolution**: The practical cross-project path is:
|
||||
- The **web UI** reads the user's own `global_llm` from `d:user-settings` (it can decrypt its own data)
|
||||
- The **web UI** can optionally push config to the agent's `d:llm_config` event (encrypt to agent pubkey)
|
||||
- The **agent** reads its own `d:llm_config` as today
|
||||
- If the agent needs admin LLM config at startup, the genesis.jsonc provides it, and the setup wizard persists it to `d:llm_config`
|
||||
|
||||
So Phase 3 simplifies to: **no C-side recall changes needed for reading admin settings**. The alignment is about ensuring the JSON field names are compatible so the web UI can bridge the two.
|
||||
|
||||
### Phase 4: skills-edit.html — migrate to centralized global_llm
|
||||
|
||||
**File**: [`../client-ndk/www/skills-edit.html`](../../client-ndk/www/skills-edit.html:1076)
|
||||
|
||||
Current behavior:
|
||||
- [`fetchNostrLlmConfig()`](../../client-ndk/www/skills-edit.html:1076) subscribes to `kind:30078, #d:['llm_config']` under user pubkey
|
||||
- Decrypts and parses the standalone event
|
||||
- Merges into `aiConfig`
|
||||
|
||||
New behavior:
|
||||
- Remove `fetchNostrLlmConfig()` entirely
|
||||
- In [`initializeLlmHelperFromSettings()`](../../client-ndk/www/skills-edit.html:1195), read from `pageSettings.global_llm` (with fallback to `pageSettings.ai` for v1 compat)
|
||||
- The `pageSettings` object is already populated by `getUserSettings()` and kept live by `onUserSettings()`
|
||||
- When the user changes provider/model via the LLM helper, patch back via `patchUserSettings({ global_llm: { ... } })`
|
||||
|
||||
**Specific changes**:
|
||||
|
||||
1. Remove `fetchNostrLlmConfig()` function (~120 lines)
|
||||
2. Remove `llmConfigLoadNonce` variable
|
||||
3. Update `initializeLlmHelperFromSettings()`:
|
||||
|
||||
```js
|
||||
async function initializeLlmHelperFromSettings() {
|
||||
aiConfig = loadAiConfigLocal(aiConfig || getDefaultAiConfig(), AI_STORAGE_KEY);
|
||||
|
||||
// Read from centralized settings (global_llm with ai fallback)
|
||||
const nostrLlm = pageSettings?.global_llm || pageSettings?.ai;
|
||||
if (nostrLlm && typeof nostrLlm === 'object') {
|
||||
aiConfig = mergeAiConfigFromSettings(aiConfig, nostrLlm);
|
||||
}
|
||||
|
||||
aiConfig = saveAiConfigLocal(aiConfig, AI_STORAGE_KEY);
|
||||
renderLlmProviderOptions();
|
||||
renderLlmModelOptions();
|
||||
syncLlmHelperFromSkillInput();
|
||||
fetchLlmModels();
|
||||
}
|
||||
```
|
||||
|
||||
4. Update `selectLlmProvider()` and `toggleCurrentModelFavorite()` to also patch centralized settings:
|
||||
|
||||
```js
|
||||
// After saving to local storage, also persist to centralized settings
|
||||
await patchUserSettings({
|
||||
global_llm: {
|
||||
provider: aiConfig.provider,
|
||||
api_key: aiConfig.api_key,
|
||||
model: aiConfig.model,
|
||||
base_url: aiConfig.base_url,
|
||||
max_tokens: aiConfig.max_tokens,
|
||||
temperature: aiConfig.temperature,
|
||||
providers: aiConfig.providers,
|
||||
favorites: aiConfig.favorites
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
5. Add `patchUserSettings` to the imports from `init-ndk.mjs`
|
||||
|
||||
### Phase 5: ai-ui.mjs — support global_llm namespace
|
||||
|
||||
**File**: [`../client-ndk/www/js/ai-ui.mjs`](../../client-ndk/www/js/ai-ui.mjs:72)
|
||||
|
||||
The [`mergeAiConfigFromSettings()`](../../client-ndk/www/js/ai-ui.mjs:72) function already accepts any object with the right flat fields. It works with both `settings.ai` and `settings.global_llm` — the caller just passes the right sub-object.
|
||||
|
||||
**No changes needed to ai-ui.mjs itself.** The callers (skills-edit.html and other pages) just need to read from `global_llm` instead of `ai`.
|
||||
|
||||
### Phase 6: Update SETTINGS.md
|
||||
|
||||
Mark Issue 1 as resolved. Update the audit findings section to reflect that skills-edit.html now uses centralized `global_llm`.
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Project | Change |
|
||||
|------|---------|--------|
|
||||
| `genesis.jsonc` | didactyl | Fix `provider` to short name |
|
||||
| `genesis.jsonc.example` | didactyl | Fix `provider` to short name |
|
||||
| `../client-ndk/www/skills-edit.html` | client-ndk | Remove `fetchNostrLlmConfig`, read from `pageSettings.global_llm`, add `patchUserSettings` import and calls |
|
||||
| `../client-ndk/docs/SETTINGS.md` | client-ndk | Mark Issue 1 resolved |
|
||||
|
||||
---
|
||||
|
||||
## What Does NOT Change
|
||||
|
||||
| Component | Why |
|
||||
|-----------|-----|
|
||||
| `llm_config_t` C struct | Field names already match `global_llm` flat fields |
|
||||
| `persist_llm_config_nostr()` in tool_model.c | Already writes compatible flat fields |
|
||||
| `apply_recalled_llm_config()` in main.c | Already reads compatible flat fields, ignores extras |
|
||||
| `persist_runtime_config_to_nostr()` in main.c | Already writes compatible flat fields |
|
||||
| Agent's own `d:llm_config` event | Stays as agent runtime config under agent pubkey |
|
||||
| `ai-ui.mjs` | `mergeAiConfigFromSettings()` already handles the right shape |
|
||||
| `ndk-worker.js` | Settings infrastructure unchanged — just a new namespace key |
|
||||
|
||||
---
|
||||
|
||||
## Migration Safety
|
||||
|
||||
- **v1 → v2 compat**: skills-edit.html reads `global_llm || ai` so it works with both old and new settings
|
||||
- **Local storage fallback**: `loadAiConfigLocal()` still provides defaults if no Nostr settings exist
|
||||
- **No breaking change for agent**: Agent continues reading its own `d:llm_config` — the flat field names are already compatible
|
||||
- **Standalone `d:llm_config` under user pubkey**: Becomes orphaned but harmless. Can be cleaned up later.
|
||||
|
||||
---
|
||||
|
||||
## Encryption Constraint
|
||||
|
||||
The agent **cannot** decrypt the admin's `d:user-settings` because it is NIP-44 self-encrypted by the admin. Cross-project LLM config sharing works through:
|
||||
|
||||
1. **Genesis file** — admin provides initial LLM config
|
||||
2. **Setup wizard** — persists to agent's own `d:llm_config`
|
||||
3. **model_set tool** — admin sends DM to update agent LLM config at runtime
|
||||
4. **Future**: Web UI could publish a `d:llm_config` event encrypted to the agent's pubkey
|
||||
|
||||
This is the correct architecture — the agent should not need to read the admin's private settings directly.
|
||||
283
plans/multi_model_skill_pipelines.md
Normal file
283
plans/multi_model_skill_pipelines.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# Multi-Model Skill Pipelines
|
||||
|
||||
## Overview
|
||||
|
||||
Didactyl's skill system already supports executing a series of tasks where each task uses a different LLM model — even from different providers. This document describes what works today, what the limitations are, and what improvements would unlock more powerful multi-step workflows.
|
||||
|
||||
---
|
||||
|
||||
## What Works Today
|
||||
|
||||
### Per-Skill LLM Override
|
||||
|
||||
Every skill can declare its own `llm` tag with a provider/model spec and fallback chain:
|
||||
|
||||
```
|
||||
["llm", "anthropic/claude-sonnet-4-20250514, cheap"]
|
||||
["llm", "openai/gpt-4o-mini"]
|
||||
["llm", "best"]
|
||||
```
|
||||
|
||||
When a triggered skill fires, the runtime applies the skill's execution parameters (model, temperature, max_tokens) before calling the LLM, then restores the agent defaults afterward. This happens in `apply_trigger_runtime_to_llm_config()` in `src/trigger_manager.c`.
|
||||
|
||||
The `llm` tag supports the `provider/model` format. If the tag contains a slash, the runtime parses the provider name and model name separately and overrides both in the LLM config for that execution.
|
||||
|
||||
### Chain Triggers Connect Skills Sequentially
|
||||
|
||||
The `chain` trigger type fires when another skill completes execution. The `filter` field specifies the source skill's `d` tag:
|
||||
|
||||
```json
|
||||
{
|
||||
"trigger": "chain",
|
||||
"filter": "source-skill-d-tag"
|
||||
}
|
||||
```
|
||||
|
||||
After a triggered skill completes, `trigger_manager_fire_chains()` in `src/trigger_manager.c` looks for all adopted skills with `trigger=chain` whose `filter` matches the completed skill's d-tag, and fires them.
|
||||
|
||||
### Combined: Multi-Model Pipeline
|
||||
|
||||
By combining per-skill LLM overrides with chain triggers, you can build a pipeline where each step uses a different model:
|
||||
|
||||
```
|
||||
DM arrives
|
||||
│
|
||||
├─ Skill: triage
|
||||
│ llm: openai/gpt-4o-mini (fast/cheap)
|
||||
│ trigger: dm
|
||||
│ → Classifies the request
|
||||
│
|
||||
├─ chain fires ──→ Skill: deep-analysis
|
||||
│ llm: anthropic/claude-sonnet-4-20250514 (powerful)
|
||||
│ trigger: chain, filter: triage
|
||||
│ → Performs thorough analysis
|
||||
│
|
||||
└─ chain fires ──→ Skill: summarize
|
||||
llm: openai/gpt-4o-mini (cheap)
|
||||
trigger: chain, filter: deep-analysis
|
||||
→ Summarizes and DMs admin
|
||||
```
|
||||
|
||||
Each skill gets its own model, temperature, and max_tokens applied independently.
|
||||
|
||||
### Execution Parameter Resolution Per Step
|
||||
|
||||
For each triggered skill execution:
|
||||
|
||||
1. Start with agent/app defaults
|
||||
2. Apply the skill's `llm` tag (parsed as `provider/model` if slash present)
|
||||
3. Apply the skill's `temperature` tag if present
|
||||
4. Apply the skill's `max_tokens` tag if present
|
||||
5. Execute with those effective settings
|
||||
6. Restore defaults after the run
|
||||
|
||||
---
|
||||
|
||||
## Concrete Example
|
||||
|
||||
### Skill 1: triage (cheap fast model)
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "## Triage\n\nClassify the incoming message:\n- If it needs deep research, use the memory_save tool to store the classification and key details.\n- If trivial, respond directly.\n\n{{message}}",
|
||||
"tags": [
|
||||
["d", "triage"],
|
||||
["description", "Fast triage of incoming messages"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"],
|
||||
["llm", "openai/gpt-4o-mini, cheap"],
|
||||
["temperature", "0"],
|
||||
["max_tokens", "200"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Skill 2: deep-analysis (powerful model, chains from triage)
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "## Deep Analysis\n\n{{identity}}\n\nRecall the triage classification from memory. Perform thorough analysis using available tools. Save your findings to memory for the next step.\n\nOriginal request context:\n{{message}}",
|
||||
"tags": [
|
||||
["d", "deep-analysis"],
|
||||
["description", "Thorough analysis with powerful model"],
|
||||
["trigger", "chain"],
|
||||
["filter", "triage"],
|
||||
["llm", "anthropic/claude-sonnet-4-20250514, best"],
|
||||
["max_tokens", "2000"],
|
||||
["requires_tool", "memory_recall"],
|
||||
["requires_tool", "memory_save"],
|
||||
["requires_tool", "nostr_query"],
|
||||
["requires_skill", "identity"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Skill 3: summarize (cheap model, chains from deep-analysis)
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "## Summarize\n\nRecall the analysis findings from memory. Write a concise summary and DM it to admin.",
|
||||
"tags": [
|
||||
["d", "summarize"],
|
||||
["description", "Summarize analysis and notify admin"],
|
||||
["trigger", "chain"],
|
||||
["filter", "deep-analysis"],
|
||||
["llm", "openai/gpt-4o-mini, cheap"],
|
||||
["max_tokens", "500"],
|
||||
["requires_tool", "memory_recall"],
|
||||
["requires_tool", "nostr_dm_send"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current Capabilities
|
||||
|
||||
| Capability | Status | Notes |
|
||||
|---|---|---|
|
||||
| Different model per skill | ✅ Works | Via `llm` tag on each skill |
|
||||
| Different provider per skill | ✅ Works | `provider/model` format in `llm` tag |
|
||||
| Sequential multi-step pipelines | ✅ Works | Via `chain` trigger type |
|
||||
| Per-step temperature | ✅ Works | Via `temperature` tag |
|
||||
| Per-step max_tokens | ✅ Works | Via `max_tokens` tag |
|
||||
| Fallback chains per skill | ✅ Works | `provider/model, provider/model, cheap` |
|
||||
| LLM config restore after each step | ✅ Works | Runtime saves/restores global config |
|
||||
|
||||
---
|
||||
|
||||
## Current Limitations
|
||||
|
||||
### 1. No Direct Data Passing Between Chain Steps
|
||||
|
||||
**Problem:** Chain triggers fire with the *original* triggering event, not the output of the previous skill. Skill B doesn't automatically receive Skill A's output.
|
||||
|
||||
**Current workaround:** Use `memory_save` at the end of each step and `memory_recall` at the start of the next. This works but is fragile — memory is a shared scratchpad, not a structured pipeline bus.
|
||||
|
||||
**Potential improvement:** Extend the chain trigger event to include the previous skill's final LLM response text. In `trigger_manager_fire_chains()`, the chain event could carry a `"previous_output"` field that the next skill accesses via `{{triggering_event}}`.
|
||||
|
||||
### 2. No Conditional Branching
|
||||
|
||||
**Problem:** All chain skills matching a source d-tag fire unconditionally. You can't say "if triage classifies as X, run skill A; if Y, run skill B."
|
||||
|
||||
**Current workaround:** The chained skill can check the triggering event or memory and decide to do nothing if the condition doesn't match. But it still fires and consumes an LLM call.
|
||||
|
||||
**Potential improvement:** Add an optional `chain_condition` tag that the runtime evaluates before firing. Could be a simple JSON match against the previous output, or a keyword presence check.
|
||||
|
||||
### 3. No Parallel Fan-Out
|
||||
|
||||
**Problem:** Multiple chain skills matching the same source fire sequentially, not in parallel.
|
||||
|
||||
**Current workaround:** This is fine for most use cases. True parallelism would require thread-safe LLM config management.
|
||||
|
||||
### 4. Chain Depth Limit of 5
|
||||
|
||||
**Problem:** `s_chain_depth` in `trigger_manager_fire_chains()` caps at 5 levels to prevent runaway chains.
|
||||
|
||||
**Current workaround:** 5 steps is usually sufficient. For longer pipelines, the last step could use a tool to trigger a new chain externally.
|
||||
|
||||
**Potential improvement:** Make the depth limit configurable via genesis config.
|
||||
|
||||
### 5. Provider Credentials Are Global
|
||||
|
||||
**Problem:** The runtime has one set of API keys per provider. If Skill A uses `anthropic/claude-sonnet-4-20250514` and Skill B uses `openai/gpt-4o`, both providers must be configured in the agent's LLM config. There's no per-skill credential storage.
|
||||
|
||||
**Current workaround:** Configure all needed providers in the agent's genesis config or via `model_set` tool. The runtime already supports provider switching via the `provider` field in `llm_config_t`.
|
||||
|
||||
**Potential improvement:** None needed for most cases — agents typically have a small number of providers configured globally.
|
||||
|
||||
---
|
||||
|
||||
## Architecture: How It Works in Code
|
||||
|
||||
### Trigger Execution Flow
|
||||
|
||||
```
|
||||
trigger_manager fires skill
|
||||
│
|
||||
├─ Save current llm_config (old_cfg)
|
||||
│
|
||||
├─ apply_trigger_runtime_to_llm_config(trigger, &next_cfg)
|
||||
│ ├─ Parse llm tag: "anthropic/claude-sonnet-4-20250514"
|
||||
│ │ ├─ Set cfg->provider = "anthropic"
|
||||
│ │ └─ Set cfg->model = "claude-sonnet-4-20250514"
|
||||
│ ├─ Apply max_tokens if present
|
||||
│ └─ Apply temperature if present
|
||||
│
|
||||
├─ llm_set_config(&next_cfg)
|
||||
│
|
||||
├─ Execute skill (agent_on_trigger)
|
||||
│ ├─ Build context from triggered skills
|
||||
│ ├─ Call llm_chat_with_tools_messages()
|
||||
│ └─ Tool loop until completion
|
||||
│
|
||||
├─ Restore llm_set_config(&old_cfg)
|
||||
│
|
||||
└─ trigger_manager_fire_chains(source_d_tag)
|
||||
├─ Find chain skills where filter == source_d_tag
|
||||
├─ For each matching chain skill:
|
||||
│ ├─ Save config again
|
||||
│ ├─ Apply chain skill's llm override
|
||||
│ ├─ Execute chain skill
|
||||
│ ├─ Restore config
|
||||
│ └─ Recursively fire chains (depth < 5)
|
||||
└─ Done
|
||||
```
|
||||
|
||||
### Key Source Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/trigger_manager.c` | Trigger matching, chain firing, LLM config override/restore |
|
||||
| `src/agent.c` | `agent_on_trigger()` — builds context and runs LLM loop |
|
||||
| `src/llm.c` | `llm_chat_with_tools_messages()` — actual LLM API call |
|
||||
| `docs/SKILLS.md` | Skill spec including `llm` tag format and chain triggers |
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements (Not Yet Implemented)
|
||||
|
||||
### Priority 1: Chain Output Forwarding
|
||||
|
||||
Pass the previous skill's output to the next chain step via the triggering event:
|
||||
|
||||
```c
|
||||
// In trigger_manager_fire_chains():
|
||||
cJSON_AddStringToObject(event, "previous_output", last_response_text);
|
||||
```
|
||||
|
||||
The chained skill would access this via `{{triggering_event}}` in its template, seeing:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "chain",
|
||||
"source_d_tag": "triage",
|
||||
"previous_output": "Classification: needs deep analysis. Key topics: ..."
|
||||
}
|
||||
```
|
||||
|
||||
### Priority 2: Conditional Chain Firing
|
||||
|
||||
Add an optional `chain_condition` tag:
|
||||
|
||||
```json
|
||||
["chain_condition", "{\"previous_output_contains\":\"needs deep analysis\"}"]
|
||||
```
|
||||
|
||||
The runtime would check this before firing the chain skill.
|
||||
|
||||
### Priority 3: Configurable Chain Depth
|
||||
|
||||
```json
|
||||
// In genesis.jsonc:
|
||||
"trigger_chain_max_depth": 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Multi-model skill pipelines work today using per-skill `llm` tags and `chain` triggers. The main gap is data flow between steps (currently requires memory_save/recall workaround). The system is designed for this use case — each skill execution gets its own model config applied and restored — it just needs better inter-step communication to be truly seamless.
|
||||
359
src/agent.c
359
src/agent.c
@@ -23,22 +23,11 @@
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include "context_format.h"
|
||||
#include "context_roles.h"
|
||||
#include "json_to_markdown.h"
|
||||
|
||||
static didactyl_config_t* g_cfg = NULL;
|
||||
static tools_context_t g_tools_ctx;
|
||||
static struct trigger_manager* g_trigger_manager = NULL;
|
||||
|
||||
typedef enum {
|
||||
CONTEXT_DEBUG_OFF = 0,
|
||||
CONTEXT_DEBUG_INIT = 1,
|
||||
CONTEXT_DEBUG_FULL = 2
|
||||
} context_debug_mode_t;
|
||||
|
||||
static context_debug_mode_t g_context_debug_mode = CONTEXT_DEBUG_OFF;
|
||||
static char g_context_debug_run_stamp[32] = {0};
|
||||
static int g_context_debug_turn = 0;
|
||||
|
||||
#define AGENT_CONTEXT_PART_NAMES_MAX 512
|
||||
static char* g_context_part_names[AGENT_CONTEXT_PART_NAMES_MAX];
|
||||
static int g_context_part_names_count = 0;
|
||||
@@ -263,8 +252,6 @@ static int append_tool_result_message(cJSON* messages, const char* tool_call_id,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload);
|
||||
|
||||
static char* local_json_error(const char* msg) {
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
@@ -848,7 +835,6 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa
|
||||
message,
|
||||
result_json ? result_json : "",
|
||||
dm_payload ? dm_payload : "");
|
||||
append_context_log(sender_pubkey_hex, "direct_tool_exec", log_payload);
|
||||
free(log_payload);
|
||||
}
|
||||
|
||||
@@ -915,125 +901,6 @@ const char* agent_classify_message_part(cJSON* msg, int idx) {
|
||||
return detect_context_section(role_s, content_s, idx);
|
||||
}
|
||||
|
||||
static char* render_messages_json_as_markdown(const char* messages_json) {
|
||||
if (!messages_json || messages_json[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_Parse(messages_json);
|
||||
if (!root || !cJSON_IsArray(root)) {
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* out = (char*)malloc(4096);
|
||||
if (!out) {
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
size_t cap = 4096;
|
||||
size_t used = 0;
|
||||
out[0] = '\0';
|
||||
|
||||
int n = cJSON_GetArraySize(root);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* msg = cJSON_GetArrayItem(root, i);
|
||||
if (!msg || !cJSON_IsObject(msg)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role");
|
||||
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "message";
|
||||
|
||||
if (i > 0) {
|
||||
if (append_textf_local(&out, &cap, &used, "\n---\n\n") != 0) {
|
||||
free(out);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (append_textf_local(&out,
|
||||
&cap,
|
||||
&used,
|
||||
"**Message %d (%s)**\n\n",
|
||||
i + 1,
|
||||
role_s) != 0) {
|
||||
free(out);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* field = NULL;
|
||||
cJSON_ArrayForEach(field, msg) {
|
||||
if (!field || !field->string) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* key = field->string;
|
||||
if (strcmp(key, "_ts") == 0) {
|
||||
continue; // Internal timestamp metadata; not useful in human-readable snapshots
|
||||
}
|
||||
|
||||
char* raw_value = NULL;
|
||||
char* rendered_md = NULL;
|
||||
|
||||
if (strcmp(key, "content") == 0 && strcmp(role_s, "tool") == 0 && cJSON_IsString(field) && field->valuestring) {
|
||||
cJSON* tool_payload = cJSON_Parse(field->valuestring);
|
||||
if (tool_payload && cJSON_IsObject(tool_payload)) {
|
||||
cJSON* inner = cJSON_GetObjectItemCaseSensitive(tool_payload, "content");
|
||||
if (inner && cJSON_IsString(inner) && inner->valuestring) {
|
||||
cJSON* inner_json = cJSON_Parse(inner->valuestring);
|
||||
if (inner_json) {
|
||||
cJSON_ReplaceItemInObjectCaseSensitive(tool_payload, "content", inner_json);
|
||||
}
|
||||
}
|
||||
raw_value = cJSON_PrintUnformatted(tool_payload);
|
||||
cJSON_Delete(tool_payload);
|
||||
}
|
||||
}
|
||||
|
||||
if (!raw_value) {
|
||||
if (cJSON_IsString(field) && field->valuestring) {
|
||||
raw_value = strdup(field->valuestring);
|
||||
} else {
|
||||
raw_value = cJSON_PrintUnformatted(field);
|
||||
}
|
||||
}
|
||||
|
||||
if (raw_value) {
|
||||
rendered_md = json_to_markdown(raw_value, 8);
|
||||
}
|
||||
|
||||
const char* display = rendered_md ? rendered_md : (raw_value ? raw_value : "");
|
||||
if (append_textf_local(&out,
|
||||
&cap,
|
||||
&used,
|
||||
"- **%s**: %s\n",
|
||||
key,
|
||||
display) != 0) {
|
||||
free(rendered_md);
|
||||
free(raw_value);
|
||||
free(out);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
free(rendered_md);
|
||||
free(raw_value);
|
||||
}
|
||||
|
||||
if (append_textf_local(&out, &cap, &used, "\n") != 0) {
|
||||
free(out);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
@@ -1082,147 +949,6 @@ static __attribute__((unused)) void template_emit_hook(const char* section_name,
|
||||
set_context_part_name(message_index, section_name ? section_name : "context_part");
|
||||
}
|
||||
|
||||
static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
|
||||
(void)sender_pubkey_hex;
|
||||
|
||||
if (!phase || g_context_debug_mode == CONTEXT_DEBUG_OFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
int is_initial_phase =
|
||||
(strcmp(phase, "llm_chat_with_tools_messages") == 0 ||
|
||||
strcmp(phase, "llm_trigger") == 0);
|
||||
int is_turn_phase =
|
||||
(strcmp(phase, "llm_chat_with_tools_turn") == 0 ||
|
||||
strcmp(phase, "llm_trigger_with_tools_turn") == 0);
|
||||
|
||||
if (!is_initial_phase && !is_turn_phase) {
|
||||
return;
|
||||
}
|
||||
if (g_context_debug_mode == CONTEXT_DEBUG_INIT && !is_initial_phase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* safe_payload = context_payload ? context_payload : "";
|
||||
char* turn_markdown = NULL;
|
||||
if (is_turn_phase && context_payload && context_payload[0] == '[') {
|
||||
turn_markdown = render_messages_json_as_markdown(context_payload);
|
||||
if (turn_markdown) {
|
||||
safe_payload = turn_markdown;
|
||||
}
|
||||
}
|
||||
|
||||
int entry_len = snprintf(NULL, 0, "%s\n\n", safe_payload);
|
||||
if (entry_len < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char* entry = (char*)malloc((size_t)entry_len + 1U);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
snprintf(entry, (size_t)entry_len + 1U, "%s\n\n", safe_payload);
|
||||
|
||||
char* old_data = NULL;
|
||||
size_t old_len = 0;
|
||||
|
||||
FILE* in = fopen("context.log.md", "rb");
|
||||
if (in) {
|
||||
if (fseek(in, 0, SEEK_END) == 0) {
|
||||
long sz = ftell(in);
|
||||
if (sz > 0 && fseek(in, 0, SEEK_SET) == 0) {
|
||||
old_len = (size_t)sz;
|
||||
old_data = (char*)malloc(old_len);
|
||||
if (old_data) {
|
||||
size_t n = fread(old_data, 1, old_len, in);
|
||||
if (n != old_len) {
|
||||
free(old_data);
|
||||
old_data = NULL;
|
||||
old_len = 0;
|
||||
}
|
||||
} else {
|
||||
old_len = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(in);
|
||||
}
|
||||
|
||||
FILE* out = fopen("context.log.md", "wb");
|
||||
if (!out) {
|
||||
free(old_data);
|
||||
free(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
(void)fwrite(entry, 1, (size_t)entry_len, out);
|
||||
if (old_data && old_len > 0) {
|
||||
(void)fwrite(old_data, 1, old_len, out);
|
||||
}
|
||||
|
||||
fclose(out);
|
||||
free(old_data);
|
||||
|
||||
if (mkdir("context.logs", 0755) != 0 && errno != EEXIST) {
|
||||
free(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
struct tm tm_info;
|
||||
localtime_r(&now, &tm_info);
|
||||
|
||||
if (g_context_debug_run_stamp[0] == '\0') {
|
||||
strftime(g_context_debug_run_stamp, sizeof(g_context_debug_run_stamp), "%Y%m%dT%H%M%S", &tm_info);
|
||||
g_context_debug_turn = 0;
|
||||
}
|
||||
|
||||
char snapshot_path[256] = {0};
|
||||
if (is_initial_phase) {
|
||||
snprintf(snapshot_path,
|
||||
sizeof(snapshot_path),
|
||||
"context.logs/%s_init.md",
|
||||
g_context_debug_run_stamp);
|
||||
} else {
|
||||
g_context_debug_turn++;
|
||||
snprintf(snapshot_path,
|
||||
sizeof(snapshot_path),
|
||||
"context.logs/%s_t%03d.md",
|
||||
g_context_debug_run_stamp,
|
||||
g_context_debug_turn);
|
||||
}
|
||||
|
||||
FILE* snapshot = fopen(snapshot_path, "wb");
|
||||
if (snapshot) {
|
||||
(void)fwrite(safe_payload, 1, strlen(safe_payload), snapshot);
|
||||
fclose(snapshot);
|
||||
}
|
||||
|
||||
free(entry);
|
||||
free(turn_markdown);
|
||||
}
|
||||
|
||||
int agent_set_context_debug_mode(const char* mode) {
|
||||
if (!mode || mode[0] == '\0' || strcmp(mode, "off") == 0) {
|
||||
g_context_debug_mode = CONTEXT_DEBUG_OFF;
|
||||
} else if (strcmp(mode, "init") == 0) {
|
||||
g_context_debug_mode = CONTEXT_DEBUG_INIT;
|
||||
} else if (strcmp(mode, "full") == 0) {
|
||||
g_context_debug_mode = CONTEXT_DEBUG_FULL;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_context_debug_run_stamp[0] = '\0';
|
||||
g_context_debug_turn = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void agent_append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
|
||||
append_context_log(sender_pubkey_hex, phase, context_payload);
|
||||
}
|
||||
|
||||
|
||||
static __attribute__((unused)) char* build_sender_verification_text(didactyl_sender_tier_t sender_tier) {
|
||||
if (sender_tier == DIDACTYL_SENDER_ADMIN) {
|
||||
return strdup("This message has been cryptographically verified as coming from your administrator.");
|
||||
@@ -1579,6 +1305,12 @@ static void clear_adopted_skills_cache_locked(void) {
|
||||
g_adopted_skills_count = 0;
|
||||
}
|
||||
|
||||
void agent_invalidate_adopted_skills_cache(void) {
|
||||
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||
g_adopted_skills_last_refresh_at = 0;
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
}
|
||||
|
||||
static int refresh_adopted_skills_cache_if_needed(void) {
|
||||
if (!g_cfg) {
|
||||
return -1;
|
||||
@@ -2047,7 +1779,6 @@ void agent_on_trigger(const char* skill_d_tag,
|
||||
return;
|
||||
}
|
||||
|
||||
append_context_log(g_cfg->admin.pubkey, "llm_trigger", full_markdown);
|
||||
context_roles_free(&roles);
|
||||
free(full_markdown);
|
||||
|
||||
@@ -2064,8 +1795,6 @@ void agent_on_trigger(const char* skill_d_tag,
|
||||
break;
|
||||
}
|
||||
|
||||
append_context_log(g_cfg->admin.pubkey, "llm_trigger_with_tools_turn", messages_json);
|
||||
|
||||
llm_response_t resp;
|
||||
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
|
||||
free(messages_json);
|
||||
@@ -2165,7 +1894,10 @@ int agent_build_admin_messages_json(const char* current_user_message,
|
||||
(sender_tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger"));
|
||||
cJSON_AddNumberToObject(dm_event, "created_at", (double)time(NULL));
|
||||
|
||||
const char* prev_current_user_message = g_tools_ctx.template_current_user_message;
|
||||
g_tools_ctx.template_current_user_message = current_user_message ? current_user_message : "";
|
||||
char* composed_context = build_context_from_triggers(TRIGGER_TYPE_DM, NULL, dm_event, "api");
|
||||
g_tools_ctx.template_current_user_message = prev_current_user_message;
|
||||
cJSON_Delete(dm_event);
|
||||
if (!composed_context) {
|
||||
composed_context = strdup("You are an AI agent. Respond to the message.");
|
||||
@@ -2233,7 +1965,6 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
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_with_role(sender_pubkey_hex,
|
||||
denied,
|
||||
DIDACTYL_DM_HISTORY_TOOL_RESPONSE);
|
||||
@@ -2256,24 +1987,15 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
(tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger"));
|
||||
cJSON_AddNumberToObject(dm_event, "created_at", (double)time(NULL));
|
||||
|
||||
const char* prev_current_user_message = g_tools_ctx.template_current_user_message;
|
||||
g_tools_ctx.template_current_user_message = message ? message : "";
|
||||
char* dm_context = build_context_from_triggers(TRIGGER_TYPE_DM, NULL, dm_event, "dm");
|
||||
g_tools_ctx.template_current_user_message = prev_current_user_message;
|
||||
cJSON_Delete(dm_event);
|
||||
if (!dm_context) {
|
||||
dm_context = strdup("You are an AI agent. Respond to the message.");
|
||||
}
|
||||
|
||||
size_t full_len = strlen(dm_context) + strlen("\n\nuser:\n") + strlen(message) + 1U;
|
||||
char* full_markdown = (char*)malloc(full_len);
|
||||
if (!full_markdown) {
|
||||
free(dm_context);
|
||||
return;
|
||||
}
|
||||
snprintf(full_markdown, full_len, "%s\n\nuser:\n%s", dm_context, message);
|
||||
free(dm_context);
|
||||
|
||||
context_roles_t roles;
|
||||
if (context_roles_split(full_markdown, &roles) != 0) {
|
||||
free(full_markdown);
|
||||
if (!dm_context) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2282,22 +2004,18 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
? "You are responding to a web-of-trust contact. Keep the response helpful and concise. Tool use is disabled for this tier."
|
||||
: "You are responding in chat-only mode. Tool use is disabled.";
|
||||
|
||||
size_t ctx_len = strlen(roles.system_content ? roles.system_content : "") + strlen("\n\n") + strlen(tier_prefix) + 1U;
|
||||
size_t ctx_len = strlen(dm_context) + strlen("\n\n") + strlen(tier_prefix) + 1U;
|
||||
char* system_for_chat = (char*)malloc(ctx_len);
|
||||
if (!system_for_chat) {
|
||||
context_roles_free(&roles);
|
||||
free(full_markdown);
|
||||
free(dm_context);
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(system_for_chat, ctx_len, "%s\n\n%s", roles.system_content ? roles.system_content : "", tier_prefix);
|
||||
snprintf(system_for_chat, ctx_len, "%s\n\n%s", dm_context, tier_prefix);
|
||||
|
||||
append_context_log(sender_pubkey_hex, "llm_chat", full_markdown);
|
||||
|
||||
char* response = llm_chat(system_for_chat, roles.user_content ? roles.user_content : message);
|
||||
char* response = llm_chat(system_for_chat, message);
|
||||
free(system_for_chat);
|
||||
context_roles_free(&roles);
|
||||
free(full_markdown);
|
||||
free(dm_context);
|
||||
|
||||
if (!response) {
|
||||
const char* fallback = "I could not get a response from the LLM right now.";
|
||||
@@ -2316,20 +2034,18 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
|
||||
char* tools_json = tools_build_openai_schema_json(&g_tools_ctx);
|
||||
if (!tools_json) {
|
||||
context_roles_free(&roles);
|
||||
free(full_markdown);
|
||||
free(dm_context);
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Tool schema generation failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
if (!messages ||
|
||||
append_simple_message(messages, "system", roles.system_content ? roles.system_content : "You are an AI agent. Respond to the message.") != 0 ||
|
||||
append_simple_message(messages, "user", roles.user_content ? roles.user_content : message) != 0) {
|
||||
append_simple_message(messages, "system", dm_context) != 0 ||
|
||||
append_simple_message(messages, "user", message) != 0) {
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
context_roles_free(&roles);
|
||||
free(full_markdown);
|
||||
free(dm_context);
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to initialize conversation messages.");
|
||||
return;
|
||||
}
|
||||
@@ -2339,7 +2055,7 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
cJSON_AddNumberToObject(live_user_msg, "_ts", (double)time(NULL));
|
||||
}
|
||||
|
||||
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 20;
|
||||
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 40;
|
||||
int stall_repeat_threshold = g_cfg->tools.stall_repeat_threshold > 1 ? g_cfg->tools.stall_repeat_threshold : 3;
|
||||
uint64_t last_tool_fp = 0;
|
||||
int repeated_tool_turns = 0;
|
||||
@@ -2348,8 +2064,6 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
char* final_answer_owned = NULL;
|
||||
int turns_run = 0;
|
||||
|
||||
append_context_log(sender_pubkey_hex, "llm_chat_with_tools_messages", full_markdown);
|
||||
|
||||
for (int turn = 0; turn < max_turns; turn++) {
|
||||
turns_run = turn + 1;
|
||||
char* messages_json = cJSON_PrintUnformatted(messages);
|
||||
@@ -2357,8 +2071,6 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
break;
|
||||
}
|
||||
|
||||
append_context_log(sender_pubkey_hex, "llm_chat_with_tools_turn", messages_json);
|
||||
|
||||
llm_response_t resp;
|
||||
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
|
||||
free(messages_json);
|
||||
@@ -2366,8 +2078,7 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "LLM request failed.");
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
context_roles_free(&roles);
|
||||
free(full_markdown);
|
||||
free(dm_context);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2420,8 +2131,7 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
llm_response_free(&resp);
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
context_roles_free(&roles);
|
||||
free(full_markdown);
|
||||
free(dm_context);
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to append tool result.");
|
||||
return;
|
||||
}
|
||||
@@ -2452,10 +2162,22 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
if (!final_answer_owned) {
|
||||
final_answer_owned = strdup(exited_on_stall
|
||||
? "I stopped repeated tool calls after detecting a loop and could not produce a final summary."
|
||||
: "I hit my tool-use limit for this request.");
|
||||
: "I reached the tool-turn limit before getting a final response from the model.");
|
||||
}
|
||||
|
||||
const char* final_answer = final_answer_owned ? final_answer_owned : "I hit my tool-use limit for this request.";
|
||||
if (exhausted_on_max_turns) {
|
||||
const char* base = final_answer_owned ? final_answer_owned : "I reached the tool-turn limit before getting a final response from the model.";
|
||||
const char* suffix = "\n\nI have paused to avoid running forever. Reply with 'continue' and I will continue from the current context.";
|
||||
size_t need = strlen(base) + strlen(suffix) + 1U;
|
||||
char* with_prompt = (char*)malloc(need);
|
||||
if (with_prompt) {
|
||||
snprintf(with_prompt, need, "%s%s", base, suffix);
|
||||
free(final_answer_owned);
|
||||
final_answer_owned = with_prompt;
|
||||
}
|
||||
}
|
||||
|
||||
const char* final_answer = final_answer_owned ? final_answer_owned : "I reached the tool-turn limit before getting a final response from the model.";
|
||||
|
||||
if (exited_on_stall || exhausted_on_max_turns) {
|
||||
notify_admin_limit_diagnostic("dm_agent_loop",
|
||||
@@ -2478,8 +2200,7 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
free(final_answer_owned);
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
context_roles_free(&roles);
|
||||
free(full_markdown);
|
||||
free(dm_context);
|
||||
}
|
||||
|
||||
void agent_cleanup(void) {
|
||||
|
||||
@@ -23,8 +23,7 @@ int agent_build_admin_messages_json(const char* current_user_message,
|
||||
char** out_messages_json);
|
||||
tools_context_t* agent_tools_context(void);
|
||||
const char* agent_classify_message_part(cJSON* msg, int idx);
|
||||
int agent_set_context_debug_mode(const char* mode);
|
||||
void agent_append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload);
|
||||
void agent_invalidate_adopted_skills_cache(void);
|
||||
void agent_cleanup(void);
|
||||
|
||||
#endif
|
||||
224
src/config.c
224
src/config.c
@@ -294,7 +294,7 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
|
||||
}
|
||||
|
||||
if (config->tools.max_turns < 1) {
|
||||
config->tools.max_turns = 8;
|
||||
config->tools.max_turns = 40;
|
||||
}
|
||||
if (config->tools.trigger_max_turns < 1) {
|
||||
config->tools.trigger_max_turns = config->tools.max_turns;
|
||||
@@ -496,6 +496,174 @@ static int parse_dm_protocol_config(cJSON* root, didactyl_config_t* config) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_llm_payload_object(cJSON* llm_obj, didactyl_config_t* config, int require_all_fields) {
|
||||
if (!llm_obj || !cJSON_IsObject(llm_obj) || !config) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (copy_json_string(llm_obj, "provider", config->llm.provider, sizeof(config->llm.provider), 0) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (copy_json_string(llm_obj, "api_key", config->llm.api_key, sizeof(config->llm.api_key), require_all_fields) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (copy_json_string(llm_obj, "model", config->llm.model, sizeof(config->llm.model), require_all_fields) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (copy_json_string(llm_obj, "base_url", config->llm.base_url, sizeof(config->llm.base_url), require_all_fields) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(llm_obj, "max_tokens");
|
||||
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(llm_obj, "temperature");
|
||||
|
||||
if (max_tokens && cJSON_IsNumber(max_tokens)) {
|
||||
config->llm.max_tokens = (int)max_tokens->valuedouble;
|
||||
} else if (config->llm.max_tokens <= 0) {
|
||||
config->llm.max_tokens = 512;
|
||||
}
|
||||
|
||||
if (temperature && cJSON_IsNumber(temperature)) {
|
||||
config->llm.temperature = temperature->valuedouble;
|
||||
} else if (config->llm.temperature <= 0.0) {
|
||||
config->llm.temperature = 0.7;
|
||||
}
|
||||
|
||||
if (config->llm.provider[0] == '\0') {
|
||||
snprintf(config->llm.provider, sizeof(config->llm.provider), "%s", "openai");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_agent_payload_object(cJSON* agent_obj, didactyl_config_t* config) {
|
||||
if (!agent_obj || !cJSON_IsObject(agent_obj) || !config) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(agent_obj, "admin_pubkey");
|
||||
if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') {
|
||||
char decoded[65] = {0};
|
||||
if (decode_pubkey_hex_or_npub(admin_pubkey->valuestring, decoded) != 0) {
|
||||
return -1;
|
||||
}
|
||||
snprintf(config->admin.pubkey, sizeof(config->admin.pubkey), "%s", decoded);
|
||||
}
|
||||
|
||||
cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(agent_obj, "dm_protocol");
|
||||
if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') {
|
||||
if (strcmp(dm_protocol->valuestring, "nip04") == 0) {
|
||||
config->dm_protocol = DM_PROTOCOL_NIP04;
|
||||
} else if (strcmp(dm_protocol->valuestring, "nip17") == 0) {
|
||||
config->dm_protocol = DM_PROTOCOL_NIP17;
|
||||
} else if (strcmp(dm_protocol->valuestring, "both") == 0) {
|
||||
config->dm_protocol = DM_PROTOCOL_BOTH;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(agent_obj, "max_turns");
|
||||
if (max_turns) {
|
||||
if (!cJSON_IsNumber(max_turns)) {
|
||||
return -1;
|
||||
}
|
||||
int parsed = (int)max_turns->valuedouble;
|
||||
if (parsed < 1) {
|
||||
parsed = 40;
|
||||
}
|
||||
config->tools.max_turns = parsed;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_user_settings_payload_object(cJSON* user_settings_obj, didactyl_config_t* config) {
|
||||
if (!user_settings_obj || !cJSON_IsObject(user_settings_obj) || !config) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* global_llm = cJSON_GetObjectItemCaseSensitive(user_settings_obj, "global_llm");
|
||||
if (!global_llm || !cJSON_IsObject(global_llm) ||
|
||||
parse_llm_payload_object(global_llm, config, 1) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* didactyl = cJSON_GetObjectItemCaseSensitive(user_settings_obj, "didactyl");
|
||||
if (!didactyl || !cJSON_IsObject(didactyl) ||
|
||||
parse_agent_payload_object(didactyl, config) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_encrypted_events(cJSON* root, didactyl_config_t* config) {
|
||||
cJSON* arr = cJSON_GetObjectItemCaseSensitive(root, "encrypted_events");
|
||||
if (!arr) {
|
||||
return 0;
|
||||
}
|
||||
if (!cJSON_IsArray(arr)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int count = cJSON_GetArraySize(arr);
|
||||
if (count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
config->encrypted_events = (encrypted_event_t*)calloc((size_t)count, sizeof(encrypted_event_t));
|
||||
if (!config->encrypted_events) {
|
||||
return -1;
|
||||
}
|
||||
config->encrypted_event_count = count;
|
||||
|
||||
int user_settings_seen = 0;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
cJSON* item = cJSON_GetArrayItem(arr, i);
|
||||
if (!item || !cJSON_IsObject(item)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(item, "kind");
|
||||
cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(item, "d_tag");
|
||||
cJSON* content = cJSON_GetObjectItemCaseSensitive(item, "content");
|
||||
|
||||
if (!kind || !cJSON_IsNumber(kind) ||
|
||||
!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || d_tag->valuestring[0] == '\0' ||
|
||||
!content || !cJSON_IsString(content) || !content->valuestring) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
encrypted_event_t* ee = &config->encrypted_events[i];
|
||||
ee->kind = (int)kind->valuedouble;
|
||||
ee->d_tag = strdup(d_tag->valuestring);
|
||||
ee->content = strdup(content->valuestring);
|
||||
if (!ee->d_tag || !ee->content) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ee->kind == 30078 && strcmp(ee->d_tag, "user-settings") == 0 && ee->content[0] != '\0') {
|
||||
cJSON* user_settings_payload = cJSON_Parse(ee->content);
|
||||
if (!user_settings_payload || !cJSON_IsObject(user_settings_payload) ||
|
||||
parse_user_settings_payload_object(user_settings_payload, config) != 0) {
|
||||
cJSON_Delete(user_settings_payload);
|
||||
return -1;
|
||||
}
|
||||
cJSON_Delete(user_settings_payload);
|
||||
user_settings_seen = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!user_settings_seen) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_api_config(cJSON* root, didactyl_config_t* config) {
|
||||
cJSON* api = cJSON_GetObjectItemCaseSensitive(root, "api");
|
||||
if (!api || !cJSON_IsObject(api)) {
|
||||
@@ -1244,6 +1412,14 @@ void config_free(didactyl_config_t* config) {
|
||||
|
||||
free_cashu_wallet_mints(&config->cashu_wallet);
|
||||
|
||||
if (config->encrypted_events) {
|
||||
for (int i = 0; i < config->encrypted_event_count; i++) {
|
||||
free(config->encrypted_events[i].d_tag);
|
||||
free(config->encrypted_events[i].content);
|
||||
}
|
||||
free(config->encrypted_events);
|
||||
}
|
||||
|
||||
memset(config, 0, sizeof(*config));
|
||||
}
|
||||
|
||||
@@ -1260,7 +1436,7 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
config->dm_protocol = DM_PROTOCOL_NIP04;
|
||||
|
||||
config->tools.enabled = 1;
|
||||
config->tools.max_turns = 20;
|
||||
config->tools.max_turns = 40;
|
||||
config->tools.trigger_max_turns = 12;
|
||||
config->tools.api_default_max_turns = 8;
|
||||
config->tools.api_max_turns_ceiling = 32;
|
||||
@@ -1351,9 +1527,8 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
cJSON* admin = cJSON_GetObjectItemCaseSensitive(root, "admin");
|
||||
cJSON* llm = cJSON_GetObjectItemCaseSensitive(root, "llm");
|
||||
|
||||
if (!admin || !cJSON_IsObject(admin) ||
|
||||
!llm || !cJSON_IsObject(llm)) {
|
||||
config_set_error("config must include object sections: admin, llm");
|
||||
if (!admin || !cJSON_IsObject(admin)) {
|
||||
config_set_error("config must include object section: admin");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
@@ -1381,6 +1556,11 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_encrypted_events(root, config) != 0) {
|
||||
config_set_error("invalid encrypted_events configuration");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_legacy_default_skill_to_startup_events(root, config) != 0) {
|
||||
config_set_error("invalid legacy default_skill configuration");
|
||||
goto cleanup;
|
||||
@@ -1391,32 +1571,16 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (copy_json_string(llm, "provider", config->llm.provider, sizeof(config->llm.provider), 0) != 0) {
|
||||
config_set_error("llm.provider must be a string if provided");
|
||||
goto cleanup;
|
||||
if (llm) {
|
||||
if (!cJSON_IsObject(llm)) {
|
||||
config_set_error("llm must be an object when provided");
|
||||
goto cleanup;
|
||||
}
|
||||
if (parse_llm_payload_object(llm, config, 1) != 0) {
|
||||
config_set_error("llm configuration is invalid (required: api_key, model, base_url)");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
if (config->llm.provider[0] == '\0') {
|
||||
strcpy(config->llm.provider, "openai");
|
||||
}
|
||||
|
||||
if (copy_json_string(llm, "api_key", config->llm.api_key, sizeof(config->llm.api_key), 1) != 0) {
|
||||
config_set_error("llm.api_key is required and must be a string");
|
||||
goto cleanup;
|
||||
}
|
||||
if (copy_json_string(llm, "model", config->llm.model, sizeof(config->llm.model), 1) != 0) {
|
||||
config_set_error("llm.model is required and must be a string");
|
||||
goto cleanup;
|
||||
}
|
||||
if (copy_json_string(llm, "base_url", config->llm.base_url, sizeof(config->llm.base_url), 1) != 0) {
|
||||
config_set_error("llm.base_url is required and must be a string");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(llm, "max_tokens");
|
||||
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(llm, "temperature");
|
||||
|
||||
config->llm.max_tokens = (max_tokens && cJSON_IsNumber(max_tokens)) ? (int)max_tokens->valuedouble : 512;
|
||||
config->llm.temperature = (temperature && cJSON_IsNumber(temperature)) ? temperature->valuedouble : 0.7;
|
||||
|
||||
if (parse_dm_protocol_config(root, config) != 0) {
|
||||
config_set_error("invalid dm_protocol configuration (expected 'nip04', 'nip17', or 'both')");
|
||||
|
||||
@@ -64,6 +64,12 @@ typedef struct {
|
||||
char* tags_json; // JSON array string for tags, optional
|
||||
} startup_event_t;
|
||||
|
||||
typedef struct {
|
||||
int kind;
|
||||
char* d_tag;
|
||||
char* content;
|
||||
} encrypted_event_t;
|
||||
|
||||
typedef struct {
|
||||
int enabled;
|
||||
int tools_enabled;
|
||||
@@ -124,6 +130,8 @@ typedef struct {
|
||||
cashu_wallet_config_t cashu_wallet;
|
||||
startup_event_t* startup_events;
|
||||
int startup_event_count;
|
||||
encrypted_event_t* encrypted_events;
|
||||
int encrypted_event_count;
|
||||
char config_path[OW_MAX_URL_LEN];
|
||||
} didactyl_config_t;
|
||||
|
||||
|
||||
@@ -882,18 +882,6 @@ static char* execute_slash_command_http(const char* message) {
|
||||
}
|
||||
|
||||
char* markdown_result = format_result_markdown_http(result_json);
|
||||
const char* dm_payload = markdown_result ? markdown_result : result_json;
|
||||
|
||||
size_t log_cap = strlen(message) + strlen(result_json ? result_json : "") + strlen(dm_payload ? dm_payload : "") + 192U;
|
||||
char* log_payload = (char*)malloc(log_cap);
|
||||
if (log_payload) {
|
||||
snprintf(log_payload, log_cap, "slash=%s\nresult_json=%s\nresult_markdown=%s",
|
||||
message,
|
||||
result_json ? result_json : "",
|
||||
dm_payload ? dm_payload : "");
|
||||
agent_append_context_log("http_api_agent", "direct_tool_exec", log_payload);
|
||||
free(log_payload);
|
||||
}
|
||||
|
||||
free(result_json);
|
||||
return markdown_result ? markdown_result : strdup("Command executed with no output.");
|
||||
@@ -902,7 +890,6 @@ static char* execute_slash_command_http(const char* message) {
|
||||
static cJSON* run_prompt_with_tools_convo(cJSON* convo,
|
||||
int max_turns,
|
||||
const char* log_sender,
|
||||
const char* log_phase,
|
||||
const char* tool_limit_message) {
|
||||
if (!convo || !cJSON_IsArray(convo)) return NULL;
|
||||
|
||||
@@ -945,10 +932,6 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
|
||||
char* messages_json = cJSON_PrintUnformatted(convo);
|
||||
if (!messages_json) break;
|
||||
|
||||
agent_append_context_log(log_sender ? log_sender : "http_api",
|
||||
log_phase ? log_phase : "llm_chat_with_tools_messages_http_api",
|
||||
messages_json);
|
||||
|
||||
llm_response_t resp;
|
||||
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
|
||||
free(messages_json);
|
||||
@@ -1115,7 +1098,6 @@ static cJSON* run_prompt_with_tools(cJSON* body) {
|
||||
cJSON* root = run_prompt_with_tools_convo(convo,
|
||||
max_turns,
|
||||
"http_api",
|
||||
"llm_chat_with_tools_messages_http_api",
|
||||
"I hit my tool-use limit for this prompt run.");
|
||||
cJSON_Delete(convo);
|
||||
return root;
|
||||
@@ -1379,7 +1361,6 @@ static void handle_prompt_agent(struct mg_connection* c, const struct mg_http_me
|
||||
result = run_prompt_with_tools_convo(convo,
|
||||
max_turns,
|
||||
"http_api_agent",
|
||||
"llm_chat_with_tools_messages_agent_api",
|
||||
"I hit my tool-use limit for this request.");
|
||||
cJSON_Delete(convo);
|
||||
|
||||
|
||||
75
src/llm.c
75
src/llm.c
@@ -7,6 +7,10 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "debug.h"
|
||||
@@ -15,6 +19,51 @@
|
||||
|
||||
static llm_config_t g_cfg;
|
||||
static int g_initialized = 0;
|
||||
static pthread_mutex_t g_llm_request_log_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static unsigned long g_llm_request_log_seq = 0;
|
||||
|
||||
static int provider_log_next_stamp_seq_local(char* stamp, size_t stamp_size, unsigned long* seq_out) {
|
||||
if (!stamp || stamp_size == 0 || !seq_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
stamp[0] = '\0';
|
||||
*seq_out = 0;
|
||||
|
||||
if (mkdir("context.logs", 0755) != 0 && errno != EEXIST) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
struct tm tm_info;
|
||||
localtime_r(&now, &tm_info);
|
||||
strftime(stamp, stamp_size, "%Y%m%dT%H%M%S", &tm_info);
|
||||
|
||||
pthread_mutex_lock(&g_llm_request_log_mutex);
|
||||
*seq_out = ++g_llm_request_log_seq;
|
||||
pthread_mutex_unlock(&g_llm_request_log_mutex);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void log_provider_payload_local(const char* payload,
|
||||
const char* stamp,
|
||||
unsigned long seq,
|
||||
const char* phase) {
|
||||
if (!payload || payload[0] == '\0' || !stamp || stamp[0] == '\0' || !phase || phase[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
char path[256] = {0};
|
||||
snprintf(path, sizeof(path), "context.logs/%s_%s%04lu.json", stamp, phase, seq);
|
||||
|
||||
FILE* out = fopen(path, "wb");
|
||||
if (!out) {
|
||||
return;
|
||||
}
|
||||
(void)fwrite(payload, 1, strlen(payload), out);
|
||||
fclose(out);
|
||||
}
|
||||
|
||||
static int url_looks_like_websocket(const char* url) {
|
||||
if (!url) return 0;
|
||||
@@ -82,6 +131,19 @@ static char* perform_http_request(const char* url, const char* body, int is_post
|
||||
DEBUG_INFO("[didactyl] llm request: method=GET url=%s", url);
|
||||
}
|
||||
|
||||
char provider_log_stamp[32] = {0};
|
||||
unsigned long provider_log_seq = 0;
|
||||
int provider_log_has_pair = 0;
|
||||
|
||||
if (is_post && body && body[0] != '\0') {
|
||||
if (provider_log_next_stamp_seq_local(provider_log_stamp,
|
||||
sizeof(provider_log_stamp),
|
||||
&provider_log_seq) == 0) {
|
||||
log_provider_payload_local(body, provider_log_stamp, provider_log_seq, "req");
|
||||
provider_log_has_pair = 1;
|
||||
}
|
||||
}
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
@@ -89,6 +151,19 @@ static char* perform_http_request(const char* url, const char* body, int is_post
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (resp.body && resp.body_len > 0) {
|
||||
if (!provider_log_has_pair) {
|
||||
if (provider_log_next_stamp_seq_local(provider_log_stamp,
|
||||
sizeof(provider_log_stamp),
|
||||
&provider_log_seq) == 0) {
|
||||
provider_log_has_pair = 1;
|
||||
}
|
||||
}
|
||||
if (provider_log_has_pair) {
|
||||
log_provider_payload_local(resp.body, provider_log_stamp, provider_log_seq, "res");
|
||||
}
|
||||
}
|
||||
|
||||
if (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
DEBUG_ERROR("[didactyl] llm http request failed: status=%ld", resp.status_code);
|
||||
if (resp.status_code == 101) {
|
||||
|
||||
214
src/main.c
214
src/main.c
@@ -107,8 +107,6 @@ static void print_usage(const char* prog) {
|
||||
" Enable HTTP API and bind address (example: 127.0.0.1).\n"
|
||||
" --debug <0-5>\n"
|
||||
" Log level (0=fatal, 1=error, 2=warn, 3=info, 4=debug, 5=trace).\n"
|
||||
" --context-debug <off|init|full>\n"
|
||||
" Context snapshot mode (off=none, init=initial only, full=initial+every turn).\n"
|
||||
" --dump-schemas\n"
|
||||
" Print tool schemas JSON and exit.\n"
|
||||
" --test-tool <name> <args_json>\n"
|
||||
@@ -117,8 +115,6 @@ static void print_usage(const char* prog) {
|
||||
"Environment:\n"
|
||||
" DIDACTYL_NSEC\n"
|
||||
" Fallback private key when --nsec is not provided.\n"
|
||||
" DIDACTYL_CONTEXT_DEBUG\n"
|
||||
" Context snapshot mode override: off, init, or full.\n"
|
||||
"\n"
|
||||
"Examples:\n"
|
||||
" 1) Genesis-based startup\n"
|
||||
@@ -726,7 +722,7 @@ static int fetch_self_config_plaintext(const didactyl_config_t* cfg, const char*
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int apply_recalled_llm_config(didactyl_config_t* cfg, const char* plaintext) {
|
||||
static int apply_recalled_user_settings(didactyl_config_t* cfg, const char* plaintext) {
|
||||
if (!cfg || !plaintext) return -1;
|
||||
|
||||
cJSON* root = cJSON_Parse(plaintext);
|
||||
@@ -735,16 +731,29 @@ static int apply_recalled_llm_config(didactyl_config_t* cfg, const char* plainte
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* provider = cJSON_GetObjectItemCaseSensitive(root, "provider");
|
||||
cJSON* api_key = cJSON_GetObjectItemCaseSensitive(root, "api_key");
|
||||
cJSON* model = cJSON_GetObjectItemCaseSensitive(root, "model");
|
||||
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(root, "base_url");
|
||||
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(root, "max_tokens");
|
||||
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(root, "temperature");
|
||||
cJSON* global_llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm");
|
||||
cJSON* didactyl = cJSON_GetObjectItemCaseSensitive(root, "didactyl");
|
||||
if (!global_llm || !cJSON_IsObject(global_llm) || !didactyl || !cJSON_IsObject(didactyl)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* provider = cJSON_GetObjectItemCaseSensitive(global_llm, "provider");
|
||||
cJSON* api_key = cJSON_GetObjectItemCaseSensitive(global_llm, "api_key");
|
||||
cJSON* model = cJSON_GetObjectItemCaseSensitive(global_llm, "model");
|
||||
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(global_llm, "base_url");
|
||||
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(global_llm, "max_tokens");
|
||||
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(global_llm, "temperature");
|
||||
|
||||
cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey");
|
||||
cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol");
|
||||
cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns");
|
||||
|
||||
if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' ||
|
||||
!model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' ||
|
||||
!base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0') {
|
||||
!base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0' ||
|
||||
!admin_pubkey || !cJSON_IsString(admin_pubkey) || !admin_pubkey->valuestring || admin_pubkey->valuestring[0] == '\0' ||
|
||||
!dm_protocol || !cJSON_IsString(dm_protocol) || !dm_protocol->valuestring || dm_protocol->valuestring[0] == '\0') {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
@@ -765,31 +774,20 @@ static int apply_recalled_llm_config(didactyl_config_t* cfg, const char* plainte
|
||||
cfg->llm.temperature = temperature->valuedouble;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int apply_recalled_agent_config(didactyl_config_t* cfg, const char* plaintext) {
|
||||
if (!cfg || !plaintext) return -1;
|
||||
|
||||
cJSON* root = cJSON_Parse(plaintext);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(root, "admin_pubkey");
|
||||
cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(root, "dm_protocol");
|
||||
|
||||
if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') {
|
||||
{
|
||||
char decoded[65] = {0};
|
||||
if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) == 0) {
|
||||
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded);
|
||||
if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) != 0) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded);
|
||||
}
|
||||
|
||||
if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') {
|
||||
dm_protocol_from_string(dm_protocol->valuestring, cfg);
|
||||
dm_protocol_from_string(dm_protocol->valuestring, cfg);
|
||||
|
||||
if (max_turns && cJSON_IsNumber(max_turns)) {
|
||||
int parsed = (int)max_turns->valuedouble;
|
||||
cfg->tools.max_turns = (parsed > 0) ? parsed : 40;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
@@ -844,39 +842,46 @@ static int publish_encrypted_self_config(const didactyl_config_t* cfg, const cha
|
||||
static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg, int persist_llm_config) {
|
||||
if (!cfg) return -1;
|
||||
|
||||
int llm_rc = 0;
|
||||
if (persist_llm_config) {
|
||||
cJSON* llm = cJSON_CreateObject();
|
||||
if (!llm) return -1;
|
||||
cJSON_AddStringToObject(llm, "provider", cfg->llm.provider);
|
||||
cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key);
|
||||
cJSON_AddStringToObject(llm, "model", cfg->llm.model);
|
||||
cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url);
|
||||
cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens);
|
||||
cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature);
|
||||
|
||||
char* llm_json = cJSON_PrintUnformatted(llm);
|
||||
cJSON_Delete(llm);
|
||||
if (!llm_json) return -1;
|
||||
|
||||
llm_rc = publish_encrypted_self_config(cfg, "llm_config", llm_json);
|
||||
free(llm_json);
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] startup phase: skipping llm_config publish because it was recalled from Nostr");
|
||||
if (!persist_llm_config) {
|
||||
DEBUG_INFO("[didactyl] startup phase: skipping user-settings publish because runtime config was recalled from Nostr");
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* agent = cJSON_CreateObject();
|
||||
if (!agent) return llm_rc;
|
||||
cJSON_AddStringToObject(agent, "admin_pubkey", cfg->admin.pubkey);
|
||||
cJSON_AddStringToObject(agent, "dm_protocol", dm_protocol_to_string(cfg->dm_protocol));
|
||||
char* agent_json = cJSON_PrintUnformatted(agent);
|
||||
cJSON_Delete(agent);
|
||||
if (!agent_json) return llm_rc;
|
||||
cJSON* user_settings = cJSON_CreateObject();
|
||||
cJSON* global_llm = cJSON_CreateObject();
|
||||
cJSON* didactyl = cJSON_CreateObject();
|
||||
if (!user_settings || !global_llm || !didactyl) {
|
||||
cJSON_Delete(user_settings);
|
||||
cJSON_Delete(global_llm);
|
||||
cJSON_Delete(didactyl);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int agent_rc = publish_encrypted_self_config(cfg, "agent_config", agent_json);
|
||||
free(agent_json);
|
||||
cJSON_AddNumberToObject(user_settings, "v", 2);
|
||||
cJSON_AddNumberToObject(user_settings, "updatedAt", (double)time(NULL));
|
||||
|
||||
return (llm_rc == 0 && agent_rc == 0) ? 0 : -1;
|
||||
cJSON_AddStringToObject(global_llm, "provider", cfg->llm.provider);
|
||||
cJSON_AddStringToObject(global_llm, "api_key", cfg->llm.api_key);
|
||||
cJSON_AddStringToObject(global_llm, "model", cfg->llm.model);
|
||||
cJSON_AddStringToObject(global_llm, "base_url", cfg->llm.base_url);
|
||||
cJSON_AddNumberToObject(global_llm, "max_tokens", cfg->llm.max_tokens);
|
||||
cJSON_AddNumberToObject(global_llm, "temperature", cfg->llm.temperature);
|
||||
cJSON_AddItemToObject(user_settings, "global_llm", global_llm);
|
||||
|
||||
cJSON_AddStringToObject(didactyl, "admin_pubkey", cfg->admin.pubkey);
|
||||
cJSON_AddStringToObject(didactyl, "dm_protocol", dm_protocol_to_string(cfg->dm_protocol));
|
||||
cJSON_AddNumberToObject(didactyl, "max_turns", cfg->tools.max_turns > 0 ? cfg->tools.max_turns : 40);
|
||||
cJSON_AddItemToObject(user_settings, "didactyl", didactyl);
|
||||
|
||||
char* user_settings_json = cJSON_PrintUnformatted(user_settings);
|
||||
cJSON_Delete(user_settings);
|
||||
if (!user_settings_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = publish_encrypted_self_config(cfg, "user-settings", user_settings_json);
|
||||
free(user_settings_json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
|
||||
@@ -884,45 +889,22 @@ static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int llm_recalled_from_nostr = 0;
|
||||
|
||||
if (!llm_config_is_complete(&cfg->llm)) {
|
||||
char* llm_plaintext = NULL;
|
||||
if (fetch_self_config_plaintext(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) {
|
||||
if (apply_recalled_llm_config(cfg, llm_plaintext) == 0) {
|
||||
llm_recalled_from_nostr = 1;
|
||||
DEBUG_INFO("[didactyl] startup phase: recovered llm_config from Nostr");
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to apply recalled llm_config");
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] startup phase: llm_config recall unavailable");
|
||||
}
|
||||
free(llm_plaintext);
|
||||
char* settings_plaintext = NULL;
|
||||
if (fetch_self_config_plaintext(cfg, "user-settings", &settings_plaintext) != 0 || !settings_plaintext) {
|
||||
DEBUG_WARN("[didactyl] startup phase: user-settings recall unavailable");
|
||||
free(settings_plaintext);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* agent_plaintext = NULL;
|
||||
if (fetch_self_config_plaintext(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) {
|
||||
didactyl_config_t recalled = *cfg;
|
||||
if (apply_recalled_agent_config(&recalled, agent_plaintext) == 0) {
|
||||
if (!admin_config_is_complete(cfg)) {
|
||||
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", recalled.admin.pubkey);
|
||||
cfg->dm_protocol = recalled.dm_protocol;
|
||||
DEBUG_INFO("[didactyl] startup phase: recovered agent_config from Nostr");
|
||||
} else if (recalled.admin.pubkey[0] != '\0' && strcmp(cfg->admin.pubkey, recalled.admin.pubkey) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: agent_config admin pubkey differs from loaded config (loaded=%.16s..., nostr=%.16s...); loaded config wins",
|
||||
cfg->admin.pubkey,
|
||||
recalled.admin.pubkey);
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to parse/decrypt recalled agent_config");
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] startup phase: agent_config recall unavailable");
|
||||
if (apply_recalled_user_settings(cfg, settings_plaintext) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to parse/decrypt recalled user-settings");
|
||||
free(settings_plaintext);
|
||||
return 0;
|
||||
}
|
||||
free(agent_plaintext);
|
||||
|
||||
return llm_recalled_from_nostr;
|
||||
free(settings_plaintext);
|
||||
DEBUG_INFO("[didactyl] startup phase: recovered user-settings from Nostr");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -935,8 +917,6 @@ int main(int argc, char** argv) {
|
||||
int dump_schemas = 0;
|
||||
const char* test_tool_name = NULL;
|
||||
const char* test_tool_args = "{}";
|
||||
const char* context_debug_mode = "off";
|
||||
|
||||
didactyl_config_t cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
int config_preloaded = 0;
|
||||
@@ -1000,8 +980,6 @@ int main(int argc, char** argv) {
|
||||
api_port_override = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--api-bind") == 0 && i + 1 < argc) {
|
||||
api_bind_override = argv[++i];
|
||||
} else if (strcmp(argv[i], "--context-debug") == 0 && i + 1 < argc) {
|
||||
context_debug_mode = argv[++i];
|
||||
} else if (strcmp(argv[i], "--dump-schemas") == 0) {
|
||||
dump_schemas = 1;
|
||||
} else if (strcmp(argv[i], "--test-tool") == 0 && i + 2 < argc) {
|
||||
@@ -1064,22 +1042,6 @@ int main(int argc, char** argv) {
|
||||
DEBUG_INFO("[didactyl] startup phase: admin override applied from --admin (%.16s...)", cfg.admin.pubkey);
|
||||
}
|
||||
|
||||
{
|
||||
const char* env_context_debug = getenv("DIDACTYL_CONTEXT_DEBUG");
|
||||
if (env_context_debug && env_context_debug[0] != '\0' &&
|
||||
strcmp(context_debug_mode, "off") == 0) {
|
||||
context_debug_mode = env_context_debug;
|
||||
}
|
||||
|
||||
if (agent_set_context_debug_mode(context_debug_mode) != 0) {
|
||||
fprintf(stderr, "Invalid context debug mode '%s' (expected off, init, or full)\n", context_debug_mode);
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
DEBUG_INFO("[didactyl] context debug mode: %s", context_debug_mode);
|
||||
}
|
||||
|
||||
if (config_ensure_startup_skill_adoption(&cfg) != 0) {
|
||||
fprintf(stderr, "Failed to synthesize startup skill adoption events\n");
|
||||
config_free(&cfg);
|
||||
@@ -1223,13 +1185,13 @@ int main(int argc, char** argv) {
|
||||
|
||||
startup_step_begin(2, "Recover runtime config from Nostr");
|
||||
int llm_recalled_from_nostr = recover_missing_runtime_config_from_nostr(&cfg);
|
||||
startup_step_ok(2, "Recover runtime config from Nostr", llm_recalled_from_nostr ? "llm_config recalled" : NULL);
|
||||
startup_step_ok(2, "Recover runtime config from Nostr", llm_recalled_from_nostr ? "user-settings recalled" : NULL);
|
||||
|
||||
startup_step_begin(3, "Validate LLM config");
|
||||
if (!llm_config_is_complete(&cfg.llm)) {
|
||||
startup_step_fail(3, "Validate LLM config", "missing llm_config (model/base_url/api_key)");
|
||||
startup_step_fail(3, "Validate LLM config", "missing user-settings.global_llm (model/base_url/api_key)");
|
||||
fprintf(stderr,
|
||||
"Missing LLM config: provide genesis file values or store d=llm_config via config_store\n");
|
||||
"Missing LLM config: provide genesis file values or store d=user-settings with global_llm\n");
|
||||
nostr_block_list_cleanup();
|
||||
nostr_handler_cleanup();
|
||||
config_free(&cfg);
|
||||
@@ -1242,7 +1204,7 @@ int main(int argc, char** argv) {
|
||||
if (!admin_config_is_complete(&cfg)) {
|
||||
startup_step_fail(4, "Validate admin config", "missing admin pubkey");
|
||||
fprintf(stderr,
|
||||
"Missing admin config: provide admin.pubkey in genesis, pass --admin, or store d=agent_config via config_store\n");
|
||||
"Missing admin config: provide admin.pubkey in genesis, pass --admin, or store d=user-settings with didactyl.admin_pubkey\n");
|
||||
nostr_block_list_cleanup();
|
||||
nostr_handler_cleanup();
|
||||
config_free(&cfg);
|
||||
@@ -1284,10 +1246,14 @@ int main(int argc, char** argv) {
|
||||
DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run");
|
||||
}
|
||||
|
||||
if (persist_runtime_config_to_nostr(&cfg, llm_recalled_from_nostr ? 0 : 1) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to persist encrypted runtime config to Nostr");
|
||||
if (bootstrap_mode || first_run) {
|
||||
if (persist_runtime_config_to_nostr(&cfg, llm_recalled_from_nostr ? 0 : 1) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to persist encrypted runtime config to Nostr");
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] startup phase: persisted encrypted runtime config to Nostr");
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] startup phase: persisted encrypted runtime config to Nostr");
|
||||
DEBUG_INFO("[didactyl] startup phase: skipping encrypted runtime config publish on subsequent run");
|
||||
}
|
||||
|
||||
startup_step_ok(7, "Reconcile/persist startup state", NULL);
|
||||
|
||||
@@ -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 24
|
||||
#define DIDACTYL_VERSION "v0.2.24"
|
||||
#define DIDACTYL_VERSION_PATCH 28
|
||||
#define DIDACTYL_VERSION "v0.2.28"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "trigger_manager.h"
|
||||
#include "nostr_block_list.h"
|
||||
#include "cashu_wallet.h"
|
||||
#include "agent.h"
|
||||
|
||||
#define NIP17_MAX_RELAYS 32
|
||||
#define NIP17_MAX_GIFT_WRAPS 8
|
||||
@@ -1028,6 +1029,287 @@ static const char* find_tag_value_local(cJSON* tags, const char* key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int nip44_encrypt_self_local(const char* plaintext, char** out_ciphertext) {
|
||||
if (!g_cfg || !out_ciphertext) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* plain = plaintext ? plaintext : "";
|
||||
size_t out_cap = (strlen(plain) * 4U) + 1024U;
|
||||
char* ciphertext = (char*)malloc(out_cap);
|
||||
if (!ciphertext) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = nostr_nip44_encrypt(g_cfg->keys.private_key,
|
||||
g_cfg->keys.public_key,
|
||||
plain,
|
||||
ciphertext,
|
||||
out_cap);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(ciphertext);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_ciphertext = ciphertext;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int nip44_decrypt_self_local(const char* ciphertext, char** out_plaintext) {
|
||||
if (!g_cfg || !ciphertext || !out_plaintext) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t out_cap = strlen(ciphertext) + 1024U;
|
||||
char* plaintext = (char*)malloc(out_cap);
|
||||
if (!plaintext) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = nostr_nip44_decrypt(g_cfg->keys.private_key,
|
||||
g_cfg->keys.public_key,
|
||||
ciphertext,
|
||||
plaintext,
|
||||
out_cap);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(plaintext);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_plaintext = plaintext;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static cJSON* duplicate_public_d_tags_local(cJSON* tags) {
|
||||
cJSON* out = cJSON_CreateArray();
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
return out;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
||||
if (!key || !cJSON_IsString(key) || !key->valuestring || strcmp(key->valuestring, "d") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* dup = cJSON_Duplicate(tag, 1);
|
||||
if (!dup) {
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToArray(out, dup);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static cJSON* duplicate_private_non_d_tags_local(cJSON* tags) {
|
||||
cJSON* out = cJSON_CreateArray();
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
return out;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
||||
if (!key || !cJSON_IsString(key) || !key->valuestring || strcmp(key->valuestring, "d") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* dup = cJSON_Duplicate(tag, 1);
|
||||
if (!dup) {
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToArray(out, dup);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static int encode_private_skill_event_payload_local(const char* content,
|
||||
cJSON* tags,
|
||||
char** out_encrypted_content,
|
||||
cJSON** out_public_tags) {
|
||||
if (!out_encrypted_content || !out_public_tags) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_encrypted_content = NULL;
|
||||
*out_public_tags = NULL;
|
||||
|
||||
cJSON* payload = cJSON_CreateObject();
|
||||
cJSON* private_tags = duplicate_private_non_d_tags_local(tags);
|
||||
cJSON* public_tags = duplicate_public_d_tags_local(tags);
|
||||
if (!payload || !private_tags || !public_tags) {
|
||||
cJSON_Delete(payload);
|
||||
cJSON_Delete(private_tags);
|
||||
cJSON_Delete(public_tags);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(payload, "content", content ? content : "");
|
||||
cJSON_AddItemToObject(payload, "private_tags", private_tags);
|
||||
|
||||
char* payload_json = cJSON_PrintUnformatted(payload);
|
||||
cJSON_Delete(payload);
|
||||
if (!payload_json) {
|
||||
cJSON_Delete(public_tags);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* encrypted_content = NULL;
|
||||
int rc = nip44_encrypt_self_local(payload_json, &encrypted_content);
|
||||
free(payload_json);
|
||||
if (rc != 0) {
|
||||
cJSON_Delete(public_tags);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_encrypted_content = encrypted_content;
|
||||
*out_public_tags = public_tags;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int tag_tuple_exists_local(cJSON* tags, const char* key, const char* value) {
|
||||
if (!tags || !cJSON_IsArray(tags) || !key || !value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* ex_key = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* ex_value = cJSON_GetArrayItem(tag, 1);
|
||||
if (!ex_key || !ex_value ||
|
||||
!cJSON_IsString(ex_key) || !ex_key->valuestring ||
|
||||
!cJSON_IsString(ex_value) || !ex_value->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(ex_key->valuestring, key) == 0 && strcmp(ex_value->valuestring, value) == 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int merge_private_tags_into_event_local(cJSON* event, cJSON* private_tags) {
|
||||
if (!event || !cJSON_IsObject(event) || !private_tags || !cJSON_IsArray(private_tags)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
tags = cJSON_CreateArray();
|
||||
if (!tags) {
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddItemToObject(event, "tags", tags);
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(private_tags);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(private_tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* value = cJSON_GetArrayItem(tag, 1);
|
||||
if (!key || !value ||
|
||||
!cJSON_IsString(key) || !key->valuestring ||
|
||||
!cJSON_IsString(value) || !value->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(key->valuestring, "d") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tag_tuple_exists_local(tags, key->valuestring, value->valuestring)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* dup = cJSON_Duplicate(tag, 1);
|
||||
if (!dup) {
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddItemToArray(tags, dup);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int normalize_private_skill_event_local(cJSON* event) {
|
||||
if (!event || !cJSON_IsObject(event)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
|
||||
cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content");
|
||||
if (!kind || !cJSON_IsNumber(kind) || (int)kind->valuedouble != 31124 ||
|
||||
!content || !cJSON_IsString(content) || !content->valuestring) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* plaintext = NULL;
|
||||
if (nip44_decrypt_self_local(content->valuestring, &plaintext) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* payload = cJSON_Parse(plaintext);
|
||||
if (!payload || !cJSON_IsObject(payload)) {
|
||||
cJSON_Delete(payload);
|
||||
free(plaintext);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* payload_content = cJSON_GetObjectItemCaseSensitive(payload, "content");
|
||||
cJSON* private_tags = cJSON_GetObjectItemCaseSensitive(payload, "private_tags");
|
||||
if (!payload_content || !cJSON_IsString(payload_content) || !payload_content->valuestring ||
|
||||
!private_tags || !cJSON_IsArray(private_tags)) {
|
||||
cJSON_Delete(payload);
|
||||
free(plaintext);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* content_item = cJSON_CreateString(payload_content->valuestring);
|
||||
if (content_item) {
|
||||
cJSON_ReplaceItemInObject(event, "content", content_item);
|
||||
}
|
||||
|
||||
(void)merge_private_tags_into_event_local(event, private_tags);
|
||||
|
||||
cJSON_Delete(payload);
|
||||
free(plaintext);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_enabled_flag_local(const char* enabled_s) {
|
||||
if (!enabled_s || enabled_s[0] == '\0') {
|
||||
return 1;
|
||||
@@ -2142,10 +2424,14 @@ static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_
|
||||
}
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
|
||||
int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1;
|
||||
if (kind_val == 31124) {
|
||||
(void)normalize_private_skill_event_local(event);
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -2160,6 +2446,8 @@ static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
self_skill_cache_upsert_event_locked(event);
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
|
||||
agent_invalidate_adopted_skills_cache();
|
||||
}
|
||||
|
||||
static void on_wallet_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
@@ -2921,19 +3209,36 @@ static int publish_kind_event_to_relays(int kind,
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* content_to_publish = content;
|
||||
cJSON* tags_to_publish = tags;
|
||||
char* encrypted_content = NULL;
|
||||
cJSON* public_tags = NULL;
|
||||
|
||||
if (kind == 31124) {
|
||||
if (encode_private_skill_event_payload_local(content, tags, &encrypted_content, &public_tags) != 0) {
|
||||
return -1;
|
||||
}
|
||||
content_to_publish = encrypted_content;
|
||||
tags_to_publish = public_tags;
|
||||
}
|
||||
|
||||
cJSON* tags_copy = NULL;
|
||||
if (tags) {
|
||||
tags_copy = cJSON_Duplicate(tags, 1);
|
||||
if (tags_to_publish) {
|
||||
tags_copy = cJSON_Duplicate(tags_to_publish, 1);
|
||||
if (!tags_copy) {
|
||||
free(encrypted_content);
|
||||
cJSON_Delete(public_tags);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event(kind, content, tags_copy, g_cfg->keys.private_key, time(NULL));
|
||||
cJSON* event = nostr_create_and_sign_event(kind, content_to_publish, tags_copy, g_cfg->keys.private_key, time(NULL));
|
||||
if (tags_copy) {
|
||||
cJSON_Delete(tags_copy);
|
||||
}
|
||||
if (!event) {
|
||||
free(encrypted_content);
|
||||
cJSON_Delete(public_tags);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -2966,12 +3271,17 @@ static int publish_kind_event_to_relays(int kind,
|
||||
sent);
|
||||
|
||||
if (sent > 0) {
|
||||
if (kind == 31124) {
|
||||
(void)normalize_private_skill_event_local(event);
|
||||
}
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
self_skill_cache_upsert_event_locked(event);
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
}
|
||||
|
||||
cJSON_Delete(event);
|
||||
free(encrypted_content);
|
||||
cJSON_Delete(public_tags);
|
||||
return sent > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ static void config_set_defaults(didactyl_config_t* cfg) {
|
||||
cfg->dm_protocol = DM_PROTOCOL_NIP04;
|
||||
|
||||
cfg->tools.enabled = 1;
|
||||
cfg->tools.max_turns = 20;
|
||||
cfg->tools.max_turns = 40;
|
||||
cfg->tools.trigger_max_turns = 12;
|
||||
cfg->tools.api_default_max_turns = 8;
|
||||
cfg->tools.api_max_turns_ceiling = 32;
|
||||
@@ -981,7 +981,7 @@ static int fetch_and_decrypt_self_config_wizard(const didactyl_config_t* cfg, co
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int apply_recalled_llm_config_wizard(didactyl_config_t* cfg, const char* plaintext) {
|
||||
static int apply_recalled_user_settings_wizard(didactyl_config_t* cfg, const char* plaintext) {
|
||||
if (!cfg || !plaintext) return -1;
|
||||
|
||||
cJSON* root = cJSON_Parse(plaintext);
|
||||
@@ -990,16 +990,29 @@ static int apply_recalled_llm_config_wizard(didactyl_config_t* cfg, const char*
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* provider = cJSON_GetObjectItemCaseSensitive(root, "provider");
|
||||
cJSON* api_key = cJSON_GetObjectItemCaseSensitive(root, "api_key");
|
||||
cJSON* model = cJSON_GetObjectItemCaseSensitive(root, "model");
|
||||
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(root, "base_url");
|
||||
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(root, "max_tokens");
|
||||
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(root, "temperature");
|
||||
cJSON* global_llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm");
|
||||
cJSON* didactyl = cJSON_GetObjectItemCaseSensitive(root, "didactyl");
|
||||
if (!global_llm || !cJSON_IsObject(global_llm) || !didactyl || !cJSON_IsObject(didactyl)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* provider = cJSON_GetObjectItemCaseSensitive(global_llm, "provider");
|
||||
cJSON* api_key = cJSON_GetObjectItemCaseSensitive(global_llm, "api_key");
|
||||
cJSON* model = cJSON_GetObjectItemCaseSensitive(global_llm, "model");
|
||||
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(global_llm, "base_url");
|
||||
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(global_llm, "max_tokens");
|
||||
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(global_llm, "temperature");
|
||||
|
||||
cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey");
|
||||
cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol");
|
||||
cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns");
|
||||
|
||||
if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' ||
|
||||
!model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' ||
|
||||
!base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0') {
|
||||
!base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0' ||
|
||||
!admin_pubkey || !cJSON_IsString(admin_pubkey) || !admin_pubkey->valuestring || admin_pubkey->valuestring[0] == '\0' ||
|
||||
!dm_protocol || !cJSON_IsString(dm_protocol) || !dm_protocol->valuestring || dm_protocol->valuestring[0] == '\0') {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
@@ -1020,37 +1033,26 @@ static int apply_recalled_llm_config_wizard(didactyl_config_t* cfg, const char*
|
||||
cfg->llm.temperature = temperature->valuedouble;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int apply_recalled_agent_config_wizard(didactyl_config_t* cfg, const char* plaintext) {
|
||||
if (!cfg || !plaintext) return -1;
|
||||
|
||||
cJSON* root = cJSON_Parse(plaintext);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(root, "admin_pubkey");
|
||||
cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(root, "dm_protocol");
|
||||
|
||||
if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') {
|
||||
{
|
||||
char decoded[65] = {0};
|
||||
if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) == 0) {
|
||||
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded);
|
||||
if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) != 0) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded);
|
||||
}
|
||||
|
||||
if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') {
|
||||
if (strcmp(dm_protocol->valuestring, "nip17") == 0) {
|
||||
cfg->dm_protocol = DM_PROTOCOL_NIP17;
|
||||
} else if (strcmp(dm_protocol->valuestring, "both") == 0) {
|
||||
cfg->dm_protocol = DM_PROTOCOL_BOTH;
|
||||
} else {
|
||||
cfg->dm_protocol = DM_PROTOCOL_NIP04;
|
||||
}
|
||||
if (strcmp(dm_protocol->valuestring, "nip17") == 0) {
|
||||
cfg->dm_protocol = DM_PROTOCOL_NIP17;
|
||||
} else if (strcmp(dm_protocol->valuestring, "both") == 0) {
|
||||
cfg->dm_protocol = DM_PROTOCOL_BOTH;
|
||||
} else {
|
||||
cfg->dm_protocol = DM_PROTOCOL_NIP04;
|
||||
}
|
||||
|
||||
if (max_turns && cJSON_IsNumber(max_turns)) {
|
||||
int parsed = (int)max_turns->valuedouble;
|
||||
cfg->tools.max_turns = (parsed > 0) ? parsed : 40;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
@@ -1077,17 +1079,11 @@ static int recover_full_config_from_nostr(didactyl_config_t* cfg,
|
||||
|
||||
(void)query_self_kind0_name(cfg, out_agent_name, out_agent_name_size, out_agent_name_found);
|
||||
|
||||
char* llm_plaintext = NULL;
|
||||
if (fetch_and_decrypt_self_config_wizard(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) {
|
||||
(void)apply_recalled_llm_config_wizard(cfg, llm_plaintext);
|
||||
char* settings_plaintext = NULL;
|
||||
if (fetch_and_decrypt_self_config_wizard(cfg, "user-settings", &settings_plaintext) == 0 && settings_plaintext) {
|
||||
(void)apply_recalled_user_settings_wizard(cfg, settings_plaintext);
|
||||
}
|
||||
free(llm_plaintext);
|
||||
|
||||
char* agent_plaintext = NULL;
|
||||
if (fetch_and_decrypt_self_config_wizard(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) {
|
||||
(void)apply_recalled_agent_config_wizard(cfg, agent_plaintext);
|
||||
}
|
||||
free(agent_plaintext);
|
||||
free(settings_plaintext);
|
||||
|
||||
nostr_handler_cleanup();
|
||||
return 0;
|
||||
@@ -1153,34 +1149,39 @@ static int publish_encrypted_self_config_wizard(const didactyl_config_t* cfg,
|
||||
static int persist_runtime_config_to_nostr_wizard(const didactyl_config_t* cfg) {
|
||||
if (!cfg) return -1;
|
||||
|
||||
cJSON* llm = cJSON_CreateObject();
|
||||
if (!llm) return -1;
|
||||
cJSON_AddStringToObject(llm, "provider", cfg->llm.provider);
|
||||
cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key);
|
||||
cJSON_AddStringToObject(llm, "model", cfg->llm.model);
|
||||
cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url);
|
||||
cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens);
|
||||
cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature);
|
||||
cJSON* user_settings = cJSON_CreateObject();
|
||||
cJSON* global_llm = cJSON_CreateObject();
|
||||
cJSON* didactyl = cJSON_CreateObject();
|
||||
if (!user_settings || !global_llm || !didactyl) {
|
||||
cJSON_Delete(user_settings);
|
||||
cJSON_Delete(global_llm);
|
||||
cJSON_Delete(didactyl);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* llm_json = cJSON_PrintUnformatted(llm);
|
||||
cJSON_Delete(llm);
|
||||
if (!llm_json) return -1;
|
||||
cJSON_AddNumberToObject(user_settings, "v", 2);
|
||||
cJSON_AddNumberToObject(user_settings, "updatedAt", (double)time(NULL));
|
||||
|
||||
int llm_rc = publish_encrypted_self_config_wizard(cfg, "llm_config", llm_json);
|
||||
free(llm_json);
|
||||
cJSON_AddStringToObject(global_llm, "provider", cfg->llm.provider);
|
||||
cJSON_AddStringToObject(global_llm, "api_key", cfg->llm.api_key);
|
||||
cJSON_AddStringToObject(global_llm, "model", cfg->llm.model);
|
||||
cJSON_AddStringToObject(global_llm, "base_url", cfg->llm.base_url);
|
||||
cJSON_AddNumberToObject(global_llm, "max_tokens", cfg->llm.max_tokens);
|
||||
cJSON_AddNumberToObject(global_llm, "temperature", cfg->llm.temperature);
|
||||
cJSON_AddItemToObject(user_settings, "global_llm", global_llm);
|
||||
|
||||
cJSON* agent = cJSON_CreateObject();
|
||||
if (!agent) return llm_rc;
|
||||
cJSON_AddStringToObject(agent, "admin_pubkey", cfg->admin.pubkey);
|
||||
cJSON_AddStringToObject(agent, "dm_protocol", dm_protocol_to_string_local(cfg->dm_protocol));
|
||||
char* agent_json = cJSON_PrintUnformatted(agent);
|
||||
cJSON_Delete(agent);
|
||||
if (!agent_json) return llm_rc;
|
||||
cJSON_AddStringToObject(didactyl, "admin_pubkey", cfg->admin.pubkey);
|
||||
cJSON_AddStringToObject(didactyl, "dm_protocol", dm_protocol_to_string_local(cfg->dm_protocol));
|
||||
cJSON_AddNumberToObject(didactyl, "max_turns", cfg->tools.max_turns > 0 ? cfg->tools.max_turns : 40);
|
||||
cJSON_AddItemToObject(user_settings, "didactyl", didactyl);
|
||||
|
||||
int agent_rc = publish_encrypted_self_config_wizard(cfg, "agent_config", agent_json);
|
||||
free(agent_json);
|
||||
char* settings_json = cJSON_PrintUnformatted(user_settings);
|
||||
cJSON_Delete(user_settings);
|
||||
if (!settings_json) return -1;
|
||||
|
||||
return (llm_rc == 0 && agent_rc == 0) ? 0 : -1;
|
||||
int rc = publish_encrypted_self_config_wizard(cfg, "user-settings", settings_json);
|
||||
free(settings_json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int persist_runtime_config_to_nostr_wizard_online(const didactyl_config_t* cfg) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../config.h"
|
||||
@@ -30,7 +31,9 @@ static cJSON* parse_args_local(const char* args_json) {
|
||||
return args;
|
||||
}
|
||||
|
||||
static int persist_llm_config_nostr(tools_context_t* ctx, const llm_config_t* cfg, char** out_error) {
|
||||
static int persist_global_llm_user_settings_nostr(tools_context_t* ctx,
|
||||
const llm_config_t* cfg,
|
||||
char** out_error) {
|
||||
if (!ctx || !ctx->cfg || !cfg) {
|
||||
if (out_error) *out_error = strdup("invalid persist context");
|
||||
return -1;
|
||||
@@ -38,28 +41,136 @@ static int persist_llm_config_nostr(tools_context_t* ctx, const llm_config_t* cf
|
||||
|
||||
if (out_error) *out_error = NULL;
|
||||
|
||||
cJSON* args = cJSON_CreateObject();
|
||||
cJSON* content = cJSON_CreateObject();
|
||||
if (!args || !content) {
|
||||
cJSON_Delete(args);
|
||||
cJSON_Delete(content);
|
||||
if (out_error) *out_error = strdup("allocation failure while building llm_config payload");
|
||||
cJSON* recall_args = cJSON_CreateObject();
|
||||
if (!recall_args) {
|
||||
if (out_error) *out_error = strdup("allocation failure while building config_recall args");
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddStringToObject(recall_args, "d_tag", "user-settings");
|
||||
|
||||
char* recall_args_json = cJSON_PrintUnformatted(recall_args);
|
||||
cJSON_Delete(recall_args);
|
||||
if (!recall_args_json) {
|
||||
if (out_error) *out_error = strdup("failed to serialize config_recall args");
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(args, "d_tag", "llm_config");
|
||||
cJSON_AddStringToObject(content, "provider", cfg->provider);
|
||||
cJSON_AddStringToObject(content, "api_key", cfg->api_key);
|
||||
cJSON_AddStringToObject(content, "model", cfg->model);
|
||||
cJSON_AddStringToObject(content, "base_url", cfg->base_url);
|
||||
cJSON_AddNumberToObject(content, "max_tokens", cfg->max_tokens);
|
||||
cJSON_AddNumberToObject(content, "temperature", cfg->temperature);
|
||||
cJSON_AddItemToObject(args, "content", content);
|
||||
char* recall_result = execute_config_recall(ctx, recall_args_json);
|
||||
free(recall_args_json);
|
||||
if (!recall_result) {
|
||||
if (out_error) *out_error = strdup("config_recall returned no response");
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* store_args_json = cJSON_PrintUnformatted(args);
|
||||
cJSON_Delete(args);
|
||||
cJSON* recall_root = cJSON_Parse(recall_result);
|
||||
free(recall_result);
|
||||
if (!recall_root || !cJSON_IsObject(recall_root)) {
|
||||
cJSON_Delete(recall_root);
|
||||
if (out_error) *out_error = strdup("config_recall returned invalid JSON");
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* recall_success = cJSON_GetObjectItemCaseSensitive(recall_root, "success");
|
||||
if (!recall_success || !cJSON_IsBool(recall_success) || !cJSON_IsTrue(recall_success)) {
|
||||
cJSON* err = cJSON_GetObjectItemCaseSensitive(recall_root, "error");
|
||||
if (out_error) {
|
||||
if (err && cJSON_IsString(err) && err->valuestring) {
|
||||
*out_error = strdup(err->valuestring);
|
||||
} else {
|
||||
*out_error = strdup("config_recall failed");
|
||||
}
|
||||
}
|
||||
cJSON_Delete(recall_root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* found_j = cJSON_GetObjectItemCaseSensitive(recall_root, "found");
|
||||
int found = (found_j && cJSON_IsBool(found_j) && cJSON_IsTrue(found_j)) ? 1 : 0;
|
||||
|
||||
cJSON* content_json = cJSON_GetObjectItemCaseSensitive(recall_root, "content_json");
|
||||
cJSON* user_settings = NULL;
|
||||
if (found && content_json && cJSON_IsObject(content_json)) {
|
||||
user_settings = cJSON_Duplicate(content_json, 1);
|
||||
}
|
||||
if (!user_settings) {
|
||||
user_settings = cJSON_CreateObject();
|
||||
}
|
||||
cJSON_Delete(recall_root);
|
||||
|
||||
if (!user_settings) {
|
||||
if (out_error) *out_error = strdup("allocation failure while preparing user-settings payload");
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(user_settings, "v");
|
||||
cJSON_AddNumberToObject(user_settings, "v", 2);
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(user_settings, "updatedAt");
|
||||
cJSON_AddNumberToObject(user_settings, "updatedAt", (double)time(NULL));
|
||||
|
||||
cJSON* global_llm = cJSON_GetObjectItemCaseSensitive(user_settings, "global_llm");
|
||||
if (global_llm && !cJSON_IsObject(global_llm)) {
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(user_settings, "global_llm");
|
||||
global_llm = NULL;
|
||||
}
|
||||
if (!global_llm) {
|
||||
cJSON* new_global_llm = cJSON_CreateObject();
|
||||
if (!new_global_llm) {
|
||||
cJSON_Delete(user_settings);
|
||||
if (out_error) *out_error = strdup("allocation failure while preparing global_llm payload");
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddItemToObject(user_settings, "global_llm", new_global_llm);
|
||||
global_llm = new_global_llm;
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(global_llm, "provider");
|
||||
cJSON_AddStringToObject(global_llm, "provider", cfg->provider);
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(global_llm, "api_key");
|
||||
cJSON_AddStringToObject(global_llm, "api_key", cfg->api_key);
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(global_llm, "model");
|
||||
cJSON_AddStringToObject(global_llm, "model", cfg->model);
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(global_llm, "base_url");
|
||||
cJSON_AddStringToObject(global_llm, "base_url", cfg->base_url);
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(global_llm, "max_tokens");
|
||||
cJSON_AddNumberToObject(global_llm, "max_tokens", cfg->max_tokens);
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(global_llm, "temperature");
|
||||
cJSON_AddNumberToObject(global_llm, "temperature", cfg->temperature);
|
||||
|
||||
cJSON* didactyl = cJSON_GetObjectItemCaseSensitive(user_settings, "didactyl");
|
||||
if (didactyl && !cJSON_IsObject(didactyl)) {
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(user_settings, "didactyl");
|
||||
didactyl = NULL;
|
||||
}
|
||||
if (!didactyl) {
|
||||
cJSON* new_didactyl = cJSON_CreateObject();
|
||||
if (!new_didactyl) {
|
||||
cJSON_Delete(user_settings);
|
||||
if (out_error) *out_error = strdup("allocation failure while preparing didactyl payload");
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddItemToObject(user_settings, "didactyl", new_didactyl);
|
||||
didactyl = new_didactyl;
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(didactyl, "max_turns");
|
||||
cJSON_AddNumberToObject(didactyl,
|
||||
"max_turns",
|
||||
(ctx->cfg->tools.max_turns > 0) ? ctx->cfg->tools.max_turns : 40);
|
||||
|
||||
cJSON* store_args = cJSON_CreateObject();
|
||||
if (!store_args) {
|
||||
cJSON_Delete(user_settings);
|
||||
if (out_error) *out_error = strdup("allocation failure while building config_store args");
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(store_args, "d_tag", "user-settings");
|
||||
cJSON_AddItemToObject(store_args, "content", user_settings);
|
||||
|
||||
char* store_args_json = cJSON_PrintUnformatted(store_args);
|
||||
cJSON_Delete(store_args);
|
||||
if (!store_args_json) {
|
||||
if (out_error) *out_error = strdup("failed to serialize llm_config payload");
|
||||
if (out_error) *out_error = strdup("failed to serialize user-settings payload");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -70,17 +181,17 @@ static int persist_llm_config_nostr(tools_context_t* ctx, const llm_config_t* cf
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_Parse(store_result);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
cJSON* store_root = cJSON_Parse(store_result);
|
||||
free(store_result);
|
||||
if (!store_root || !cJSON_IsObject(store_root)) {
|
||||
cJSON_Delete(store_root);
|
||||
if (out_error) *out_error = strdup("config_store returned invalid JSON");
|
||||
free(store_result);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success");
|
||||
if (!success || !cJSON_IsBool(success) || !cJSON_IsTrue(success)) {
|
||||
cJSON* err = cJSON_GetObjectItemCaseSensitive(root, "error");
|
||||
cJSON* store_success = cJSON_GetObjectItemCaseSensitive(store_root, "success");
|
||||
if (!store_success || !cJSON_IsBool(store_success) || !cJSON_IsTrue(store_success)) {
|
||||
cJSON* err = cJSON_GetObjectItemCaseSensitive(store_root, "error");
|
||||
if (out_error) {
|
||||
if (err && cJSON_IsString(err) && err->valuestring) {
|
||||
*out_error = strdup(err->valuestring);
|
||||
@@ -88,13 +199,11 @@ static int persist_llm_config_nostr(tools_context_t* ctx, const llm_config_t* cf
|
||||
*out_error = strdup("config_store failed");
|
||||
}
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
free(store_result);
|
||||
cJSON_Delete(store_root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
free(store_result);
|
||||
cJSON_Delete(store_root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -216,7 +325,7 @@ char* execute_model_set(tools_context_t* ctx, const char* args_json) {
|
||||
ctx->cfg->llm = cfg;
|
||||
|
||||
char* persist_error = NULL;
|
||||
int persisted = (persist_llm_config_nostr(ctx, &cfg, &persist_error) == 0) ? 1 : 0;
|
||||
int persisted = (persist_global_llm_user_settings_nostr(ctx, &cfg, &persist_error) == 0) ? 1 : 0;
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "../debug.h"
|
||||
#include "../nostr_handler.h"
|
||||
#include "../trigger_manager.h"
|
||||
#include "../agent.h"
|
||||
|
||||
static char* json_error_local(const char* msg) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
@@ -1769,6 +1770,21 @@ char* execute_skill_edit(tools_context_t* ctx, const char* args_json) {
|
||||
return json;
|
||||
}
|
||||
|
||||
char* execute_skill_refresh(tools_context_t* ctx, const char* args_json) {
|
||||
(void)args_json;
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
agent_invalidate_adopted_skills_cache();
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "message", "adopted skills cache invalidated; next context build will reload skills");
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
|
||||
char* execute_skill_search(const char* args_json) {
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
@@ -152,6 +152,9 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch
|
||||
if (strcmp(tool_name, "skill_edit") == 0) {
|
||||
return execute_skill_edit(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "skill_refresh") == 0) {
|
||||
return execute_skill_refresh(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "trigger_list") == 0) {
|
||||
return execute_trigger_list(ctx, args_json);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ 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);
|
||||
char* execute_skill_refresh(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_skill_search(const char* args_json);
|
||||
|
||||
char* execute_memory_save(tools_context_t* ctx, const char* args_json);
|
||||
|
||||
@@ -1077,6 +1077,21 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToObject(t25b, "function", t25b_fn);
|
||||
cJSON_AddItemToArray(tools, t25b);
|
||||
|
||||
cJSON* t25c = cJSON_CreateObject();
|
||||
cJSON* t25c_fn = cJSON_CreateObject();
|
||||
cJSON* t25c_params = cJSON_CreateObject();
|
||||
cJSON* t25c_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t25c, "type", "function");
|
||||
cJSON_AddStringToObject(t25c_fn, "name", "skill_refresh");
|
||||
cJSON_AddStringToObject(t25c_fn, "description", "Invalidate adopted skills cache so next context build reloads latest skill events from cache/relays");
|
||||
cJSON_AddStringToObject(t25c_params, "type", "object");
|
||||
cJSON_AddItemToObject(t25c_params, "properties", t25c_props);
|
||||
|
||||
cJSON_AddItemToObject(t25c_fn, "parameters", t25c_params);
|
||||
cJSON_AddItemToObject(t25c, "function", t25c_fn);
|
||||
cJSON_AddItemToArray(tools, t25c);
|
||||
|
||||
cJSON* t26 = cJSON_CreateObject();
|
||||
cJSON* t26_fn = cJSON_CreateObject();
|
||||
cJSON* t26_params = cJSON_CreateObject();
|
||||
|
||||
Reference in New Issue
Block a user