@coda-rho-bot/oath-keeper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MOD.md +58 -0
- package/README.md +257 -0
- package/mods/index.ts +1197 -0
- package/mods/oath-keeper.ts +1197 -0
- package/package.json +41 -0
package/MOD.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "@coda-rho-bot/oath-keeper"
|
|
3
|
+
description: "Passively detects when agents make follow-up promises and automatically delivers on them"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Oath Keeper mod semantics
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
Agents make promises they can't keep. "I'll get back to you" becomes "I forgot." Use this mod when you want agents to follow through — automatically, with zero agent cooperation.
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
Oath Keeper **passively** polls the conversation API every 15 seconds for new assistant messages. When it detects promise language ("I'll get back to you", "I'll follow up", "I'll check on that"), it:
|
|
15
|
+
|
|
16
|
+
1. Creates an oath with a 5-minute countdown
|
|
17
|
+
2. After the delay, re-engages the agent with a delivery prompt
|
|
18
|
+
3. The agent delivers on its promise with full tool access
|
|
19
|
+
|
|
20
|
+
The agent never has to call a tool, set a reminder, or acknowledge the mod.
|
|
21
|
+
|
|
22
|
+
## Detection
|
|
23
|
+
|
|
24
|
+
Regex patterns match common follow-up promises:
|
|
25
|
+
|
|
26
|
+
- "I'll get back to you"
|
|
27
|
+
- "I'll follow up on that"
|
|
28
|
+
- "I'll check on this"
|
|
29
|
+
- "I'll look into that"
|
|
30
|
+
- "I'll let you know"
|
|
31
|
+
- "I'll circle back"
|
|
32
|
+
- And 10+ more patterns
|
|
33
|
+
|
|
34
|
+
Anti-false-positive: code blocks, inline code, blockquotes, and quoted text are stripped before scanning. Oath Keeper's own messages are skipped.
|
|
35
|
+
|
|
36
|
+
## Delivery
|
|
37
|
+
|
|
38
|
+
Posts to the conversation API endpoint with retry on 409 (conversation busy):
|
|
39
|
+
- Up to 5 retries with 15-second backoff
|
|
40
|
+
- 45-second timeout per attempt
|
|
41
|
+
- Failed deliveries marked, not retried infinitely
|
|
42
|
+
|
|
43
|
+
## Tools
|
|
44
|
+
|
|
45
|
+
- `list_oaths` — Check pending and recently delivered oaths
|
|
46
|
+
|
|
47
|
+
## Why not cron?
|
|
48
|
+
|
|
49
|
+
Cron requires explicit scheduling. Oath Keeper catches promises the agent made **implicitly** — "I'll get back to you" — without the agent calling any scheduling tool.
|
|
50
|
+
|
|
51
|
+
**Cron is for things you plan. Oath Keeper is for things you promise.**
|
|
52
|
+
|
|
53
|
+
## Safety
|
|
54
|
+
|
|
55
|
+
- Uses only the public tools API and fetch()
|
|
56
|
+
- Does not modify turn input or tool arguments
|
|
57
|
+
- Recursion prevention: skips messages containing "[Oath Keeper]" or "[Oath Delivered]"
|
|
58
|
+
- All timers cleaned up on unload
|
package/README.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Oath Keeper
|
|
2
|
+
|
|
3
|
+
*"Cron is for things you plan. Oath Keeper is for things you promise."*
|
|
4
|
+
|
|
5
|
+
An agent that keeps its word — automatically.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
Agents make promises they can't keep. "I'll get back to you" becomes "I forgot." Oath Keeper **passively** detects when agents make follow-up promises and makes them follow through — automatically, in the same conversation, with full tool access.
|
|
10
|
+
|
|
11
|
+
**No human prompting required. No agent cooperation required.**
|
|
12
|
+
|
|
13
|
+
## Demo
|
|
14
|
+
|
|
15
|
+
**User:** Can you check if the build is passing and let me know?
|
|
16
|
+
|
|
17
|
+
**Agent:** I'll look into that and get back to you with the CI status.
|
|
18
|
+
|
|
19
|
+
*(agent has moved on to other things)*
|
|
20
|
+
|
|
21
|
+
**Agent (automatically re-engaged by Oath Keeper):** [Oath Delivered] The build is currently passing. I checked the CI pipeline — all 47 tests pass on the latest commit (a3f2b1c).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
**User:** Can you investigate the memory bloat issue?
|
|
26
|
+
|
|
27
|
+
**Agent:** I'll dig into that and report back what I find.
|
|
28
|
+
|
|
29
|
+
*(unprompted, after the delay the agent specified)*
|
|
30
|
+
|
|
31
|
+
**Agent (automatically re-engaged):** [Oath Delivered] The memory bloat is coming from `node_modules` in the memory directory — 16,813 files being indexed by the scanner. I've added a `.gitignore` and deleted the bloated directory.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
The agent never called a scheduling tool. It never set a reminder. It just made a promise in natural language, and Oath Keeper held it to its word.
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
User asks question
|
|
41
|
+
→ Agent responds: "I'll get back to you in 5 minutes..."
|
|
42
|
+
→ turn_end fires
|
|
43
|
+
→ Stage 0: Negative filter — skip short/code-heavy messages
|
|
44
|
+
→ Stage 1: N-gram pre-filter scores the message (score > threshold → proceed)
|
|
45
|
+
→ Stage 2: LLM classifies — is it a genuine promise?
|
|
46
|
+
→ If yes: extract promise text + delay_seconds
|
|
47
|
+
→ Stage 3: LLM dedup — check against active oaths
|
|
48
|
+
→ Oath created with LLM-determined delay
|
|
49
|
+
→ Timer expires → queued
|
|
50
|
+
→ Next turn_end → { continue: deliveryPrompt }
|
|
51
|
+
→ Agent re-engaged with full tool access → delivers
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Four-stage detection
|
|
55
|
+
|
|
56
|
+
**Stage 0 — Negative filter (zero cost)**
|
|
57
|
+
|
|
58
|
+
Skips messages that are clearly not promises: short messages (<15 chars) and code-heavy messages (>5% syntax characters). When disabled, all messages proceed to n-gram scoring.
|
|
59
|
+
|
|
60
|
+
**Stage 1 — N-gram pre-filter (zero cost)**
|
|
61
|
+
|
|
62
|
+
Every assistant message is scored against a weighted list of promise-indicating patterns. This eliminates 70-80% of messages ("done", "here's the code", "sounds good") before any LLM call.
|
|
63
|
+
|
|
64
|
+
- Strong signals (3.0): "I'll get back to you", "I'll follow up", "I'll circle back", "get back to you"
|
|
65
|
+
- Moderate signals (2.0–2.5): "I'll check/verify/investigate", "let me look into", "I'll update you"
|
|
66
|
+
- Weak signals (1.0–1.5): "I'll try", "in N minutes", "later today"
|
|
67
|
+
|
|
68
|
+
Score > threshold (default: 1) → send to LLM. Below threshold → skip (not a promise).
|
|
69
|
+
|
|
70
|
+
**Stage 2 — LLM classification (per message that passes pre-filter)**
|
|
71
|
+
|
|
72
|
+
The LLM determines whether the message contains a genuine promise and extracts:
|
|
73
|
+
|
|
74
|
+
- **Promise text** — what the agent specifically committed to
|
|
75
|
+
- **Delay** — how long until delivery (e.g., "in 5 minutes" → 300s, "tomorrow" → 86400s). If no time is specified, the LLM estimates based on task complexity.
|
|
76
|
+
|
|
77
|
+
If the LLM rejects the message, it's logged as a `false_positive` — a separate status from genuine failures.
|
|
78
|
+
|
|
79
|
+
**Stage 3 — LLM semantic dedup (only if active oaths exist)**
|
|
80
|
+
|
|
81
|
+
Before creating a new oath, the LLM compares the new promise against all active (pending/queued/delivering) oaths to catch semantic duplicates. "Tell you the time" and "Tell the user the time in 20 seconds" would be caught as duplicates.
|
|
82
|
+
|
|
83
|
+
### Delivery via `turn_end { continue }`
|
|
84
|
+
|
|
85
|
+
When the oath timer expires, the oath is marked as `queued`. On the next `turn_end` event, the handler returns `{ continue: deliveryPrompt }` — the Letta Code runtime injects this as a real user turn through the normal pipeline. **Tools work properly** because the delivery goes through the runtime, not a REST API bypass.
|
|
86
|
+
|
|
87
|
+
The delivery prompt grants full tool access — the agent can investigate, run code, check APIs, whatever the promise requires.
|
|
88
|
+
|
|
89
|
+
### Oath lifecycle
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
pending → queued → delivering → delivered
|
|
93
|
+
↓
|
|
94
|
+
false_positive (LLM rejected)
|
|
95
|
+
prefilter_rejected (n-gram score too low)
|
|
96
|
+
failed (delivery error)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Installation
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
letta install npm:@coda-rho-bot/oath-keeper
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then run `/reload` in Letta Code.
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
All configuration lives in `~/.letta/mods/oath-keeper.config.json`:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"classifierAgentId": "agent-xxxxx",
|
|
114
|
+
"classifierModel": "letta/auto-fast",
|
|
115
|
+
"negativeFilter": true,
|
|
116
|
+
"ngramFilter": true,
|
|
117
|
+
"ngramThreshold": 1,
|
|
118
|
+
"llmConfirm": false,
|
|
119
|
+
"llmDedup": false
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Classifier model (optional)
|
|
124
|
+
|
|
125
|
+
By default, LLM classification and dedup calls use `letta/auto-fast` — a cheap, fast model. This is set per-conversation when creating throwaway classification conversations. Change it via `classifierModel` to use any available model (e.g., `openai/gpt-4o-mini`, `google_ai/gemini-3.5-flash`).
|
|
126
|
+
|
|
127
|
+
The classifier agent (`classifierAgentId`) determines which agent owns the throwaway conversations. By default this is the same agent the mod is running on. You only need to set this if you want classification to run on a different agent entirely.
|
|
128
|
+
|
|
129
|
+
The configured model is displayed in the TUI header.
|
|
130
|
+
|
|
131
|
+
### Filter toggles
|
|
132
|
+
|
|
133
|
+
All four detection stages can be individually toggled:
|
|
134
|
+
|
|
135
|
+
| Config key | Default | Description |
|
|
136
|
+
|-----------|---------|-------------|
|
|
137
|
+
| `negativeFilter` | `true` | Negative filter — skips short messages (<15 chars) and code-heavy messages (>5% syntax characters). |
|
|
138
|
+
| `ngramFilter` | `true` | N-gram pre-filter. When disabled, all messages skip the pre-filter and go directly to LLM confirmation. |
|
|
139
|
+
| `ngramThreshold` | `1` | Minimum n-gram score required to trigger LLM classification. Lower = more sensitive (more LLM calls). Higher = stricter (fewer calls, may miss promises). |
|
|
140
|
+
| `llmConfirm` | `false` | LLM promise classification. When disabled (default), messages that pass the n-gram filter create oaths directly without LLM confirmation. Enable for fewer false positives. |
|
|
141
|
+
| `llmDedup` | `false` | LLM semantic dedup. When disabled (default), only string-based dedup is used. Enable to catch paraphrased duplicates. |
|
|
142
|
+
|
|
143
|
+
**Safety:** If both `ngramFilter` and `llmConfirm` are disabled, no oaths are created — the mod skips detection entirely. The TUI displays a red warning when this occurs.
|
|
144
|
+
|
|
145
|
+
### Listener/desktop mode
|
|
146
|
+
|
|
147
|
+
For environments where `turn_end` events are not available (listener mode, older desktop versions), create `~/.letta/extensions/oath-env.json`:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"LETTA_AGENT_ID": "your-agent-id",
|
|
152
|
+
"LETTA_CONVERSATION_ID": "your-conversation-id",
|
|
153
|
+
"LETTA_BASE_URL": "http://localhost:PORT"
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
In listener mode, Oath Keeper polls the conversation API every 15s for new messages. When `turn_end` is available, polling handles delivery timing only — scanning is automatically disabled to prevent duplicate oaths.
|
|
158
|
+
|
|
159
|
+
### Verbose logging
|
|
160
|
+
|
|
161
|
+
Console output is silent by default. Enable with:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
touch ~/.letta/mods/oath-keeper.verbose
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Disable with `rm ~/.letta/mods/oath-keeper.verbose`. Debug logs are always written to `~/.letta/mods/oath-keeper-debug.json`.
|
|
168
|
+
|
|
169
|
+
## Usage
|
|
170
|
+
|
|
171
|
+
Just talk to your agent. When it says "I'll follow up" or "I'll get back to you," Oath Keeper catches it automatically.
|
|
172
|
+
|
|
173
|
+
Check tracked oaths:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
list_oaths
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Output shows pending, queued, delivering, recently delivered, false positive, and prefilter-rejected oaths with n-gram scores.
|
|
180
|
+
|
|
181
|
+
## Architecture
|
|
182
|
+
|
|
183
|
+
- **Detection:** `turn_end` event handler (primary, CLI v0.27.25+ / desktop v0.27.29+) with `setInterval` polling fallback (listener/desktop). Polling scan is automatically disabled when `turn_end` is available.
|
|
184
|
+
- **Pre-filter:** Weighted n-gram scoring eliminates 70-80% of messages before LLM classification. Threshold is configurable (default: 1).
|
|
185
|
+
- **LLM calls:** Classification (promise detection + delay extraction) and semantic dedup. Uses `letta/auto-fast` by default (configurable via `classifierModel`). All four stages can be individually toggled via config.
|
|
186
|
+
- **Conversation scoping:** `turn_end` extracts `conversationId` and `agentId` from the event context. Oaths deliver back to the conversation that originated them.
|
|
187
|
+
- **Delivery:** `turn_end { continue }` injects the delivery prompt through the runtime (tools work properly). REST API POST remains as fallback for listener mode.
|
|
188
|
+
- **State:** Local JSON at `~/.letta/mods/oath-keeper.state.json` with builder-pattern StateStore (load → mutate → save). Tracks lifecycle: `pending → queued → delivering → delivered` with stuck-state recovery and 24h pruning. All entries store n-gram score for debugging.
|
|
189
|
+
|
|
190
|
+
## TUI Dashboard
|
|
191
|
+
|
|
192
|
+
A standalone terminal dashboard for monitoring oaths in real time:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
cd packages/oath-keeper/cli
|
|
196
|
+
cargo build --release
|
|
197
|
+
./target/release/oath-keeper
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Launches the TUI by default (use `--plain` for text output, `--purge` to clear state).
|
|
201
|
+
|
|
202
|
+
### Header display
|
|
203
|
+
|
|
204
|
+
The TUI header shows:
|
|
205
|
+
|
|
206
|
+
- Oath counts by status (P, Q, >, OK, X, FP, PF)
|
|
207
|
+
- Filter status: `NEG:on/off`, `NGRAM:on/off(>threshold)`, `LLM:on/off`, `DEDUP:on/off` (green/red)
|
|
208
|
+
- Classifier model: `Model: letta/auto-fast`
|
|
209
|
+
- Red warning if all filters are off: `⚠ ALL FILTERS OFF — no oaths will be created`
|
|
210
|
+
|
|
211
|
+
### Status types displayed
|
|
212
|
+
|
|
213
|
+
| Badge | Status | Color | Description |
|
|
214
|
+
|-------|--------|-------|-------------|
|
|
215
|
+
| PENDING | pending | Yellow | Promise detected, waiting for timer |
|
|
216
|
+
| QUEUED | queued | Blue | Timer expired, waiting for next turn_end |
|
|
217
|
+
| DELIVERING | delivering | Cyan | Delivery prompt sent via `{ continue }` |
|
|
218
|
+
| DELIVERED | delivered | Green | Agent fulfilled the promise |
|
|
219
|
+
| FAILED | failed | Red | Delivery error |
|
|
220
|
+
| FALSE POS | false_positive | Dark gray | LLM rejected — not a genuine promise |
|
|
221
|
+
| PREFILTER | prefilter_rejected | Magenta | N-gram score ≤ threshold — never sent to LLM |
|
|
222
|
+
|
|
223
|
+
Each entry shows 3 lines: status badge + promise text, done/timer timestamp + source + age + n-gram score, and resolved conversation + agent names (fetched from the API). Detail view (press `i`) shows full promise, context, result, and creation/due timestamps.
|
|
224
|
+
|
|
225
|
+
### Keyboard controls
|
|
226
|
+
|
|
227
|
+
| Key | Action |
|
|
228
|
+
|-----|--------|
|
|
229
|
+
| `j`/`k` | Move selection |
|
|
230
|
+
| `i` | View oath detail |
|
|
231
|
+
| `d` | Manually deliver (pending oaths only) |
|
|
232
|
+
| `x` | Cancel oath |
|
|
233
|
+
| `p` | Purge all oaths |
|
|
234
|
+
| `c` | Clear filtered entries (prefilter_rejected + false_positive) |
|
|
235
|
+
| `C` | Clear completed entries (delivered, failed, false_positive, prefilter_rejected) |
|
|
236
|
+
| `1` | Toggle negative filter |
|
|
237
|
+
| `2` | Toggle n-gram filter |
|
|
238
|
+
| `3` | Toggle LLM confirm |
|
|
239
|
+
| `4` | Toggle LLM dedup |
|
|
240
|
+
| `5` | Cycle classifier model (fetched from API) |
|
|
241
|
+
| `q` | Quit |
|
|
242
|
+
|
|
243
|
+
Reads from `~/.letta/mods/oath-keeper.state.json`. Requires Rust (uses [ratatui](https://github.com/ratatui/ratatui) + [crossterm](https://github.com/crossterm-rs/crossterm)).
|
|
244
|
+
|
|
245
|
+
## Why not cron?
|
|
246
|
+
|
|
247
|
+
Cron requires explicit scheduling. Oath Keeper catches promises the agent made **implicitly** — "I'll get back to you" — without the agent calling any scheduling tool.
|
|
248
|
+
|
|
249
|
+
**Cron is for things you plan. Oath Keeper is for things you promise.**
|
|
250
|
+
|
|
251
|
+
## Safety
|
|
252
|
+
|
|
253
|
+
- Uses only the public tools API, `fetch()`, and mod event surface
|
|
254
|
+
- Does not modify turn input or tool arguments
|
|
255
|
+
- Recursion prevention: skips its own `[Oath Keeper]` and `[Oath Delivered]` messages
|
|
256
|
+
- All timers and event handlers cleaned up on unload
|
|
257
|
+
- State mutations enforced via builder pattern (load → mutate → save)
|