@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 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)