@bryti/agent 0.0.1 → 0.1.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.
Files changed (228) hide show
  1. package/Dockerfile +27 -0
  2. package/README.md +77 -50
  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 +673 -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 +94 -0
  119. package/dist/projection/reflection.d.ts.map +1 -0
  120. package/dist/projection/reflection.js +334 -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
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Per-channel FIFO message queue with merge and backpressure.
3
+ *
4
+ * Two core guarantees:
5
+ *
6
+ * 1. Per-channel serialization: only one message is processed at a time per
7
+ * channel. Subsequent messages queue up behind it rather than racing into
8
+ * the agent loop in parallel.
9
+ *
10
+ * 2. Burst merging: rapid-fire messages that arrive within MERGE_WINDOW_MS of
11
+ * each other are joined into a single prompt before being dispatched. This
12
+ * handles the common "user sends three quick messages" pattern without the
13
+ * agent seeing three separate incomplete thoughts.
14
+ *
15
+ * New messages queue up to MAX_DEPTH; beyond that the caller gets a rejection
16
+ * callback (backpressure signal, not silent drop).
17
+ */
18
+ const MAX_DEPTH = 10;
19
+ // 2-3 seconds is the sweet spot: fast enough that the user experiences a
20
+ // single response (not noticeable delay), long enough to catch split messages
21
+ // that arrive in separate network frames. The current 5s value is conservative
22
+ // and can be tuned down if latency matters more.
23
+ const MERGE_WINDOW_MS = 5000;
24
+ const RATE_LIMIT_MAX = 10;
25
+ const RATE_LIMIT_WINDOW_MS = 60_000;
26
+ /**
27
+ * Sliding window rate limiter. Tracks timestamps of recent messages per user
28
+ * and rejects when the limit is exceeded.
29
+ */
30
+ class RateLimiter {
31
+ maxMessages;
32
+ windowMs;
33
+ windows = new Map();
34
+ constructor(maxMessages, windowMs) {
35
+ this.maxMessages = maxMessages;
36
+ this.windowMs = windowMs;
37
+ }
38
+ /**
39
+ * Check if a message from this user should be allowed.
40
+ * Returns true if allowed, false if rate-limited.
41
+ */
42
+ check(userId) {
43
+ const now = Date.now();
44
+ const cutoff = now - this.windowMs;
45
+ let timestamps = this.windows.get(userId);
46
+ if (!timestamps) {
47
+ timestamps = [];
48
+ this.windows.set(userId, timestamps);
49
+ }
50
+ // Prune old entries
51
+ while (timestamps.length > 0 && timestamps[0] < cutoff) {
52
+ timestamps.shift();
53
+ }
54
+ if (timestamps.length >= this.maxMessages) {
55
+ return false;
56
+ }
57
+ timestamps.push(now);
58
+ return true;
59
+ }
60
+ }
61
+ /**
62
+ * Serialises processing per channel and merges rapid follow-up messages.
63
+ */
64
+ export class MessageQueue {
65
+ queues = new Map();
66
+ processFn;
67
+ rejectFn;
68
+ maxDepth;
69
+ mergeWindowMs;
70
+ rateLimiter;
71
+ constructor(processFn, rejectFn, maxDepth = MAX_DEPTH, mergeWindowMs = MERGE_WINDOW_MS) {
72
+ this.processFn = processFn;
73
+ this.rejectFn = rejectFn;
74
+ this.maxDepth = maxDepth;
75
+ this.mergeWindowMs = mergeWindowMs;
76
+ this.rateLimiter = new RateLimiter(RATE_LIMIT_MAX, RATE_LIMIT_WINDOW_MS);
77
+ }
78
+ /**
79
+ * Enqueue a message. If nothing is processing for this channel, starts
80
+ * draining immediately. Rate-limited per user (10 messages/minute).
81
+ */
82
+ enqueue(msg) {
83
+ const key = msg.channelId;
84
+ // Rate limiting: skip for internal messages (worker triggers, scheduler)
85
+ const rawObj = msg.raw;
86
+ const isInternal = rawObj?.type != null;
87
+ if (!isInternal && !this.rateLimiter.check(msg.userId)) {
88
+ console.warn(`[queue] Rate limit exceeded for user ${msg.userId}`);
89
+ this.rejectFn(msg).catch((err) => console.error("rejectFn error:", err));
90
+ return;
91
+ }
92
+ let q = this.queues.get(key);
93
+ if (!q) {
94
+ q = { entries: [], processing: false };
95
+ this.queues.set(key, q);
96
+ }
97
+ if (q.entries.length >= this.maxDepth) {
98
+ // Queue full: invoke the rejection callback immediately and return without
99
+ // enqueuing. This is a backpressure signal to the caller — the message is
100
+ // not silently dropped, but it is not queued either. The caller decides
101
+ // how to respond (e.g., send "I'm busy" to the user).
102
+ this.rejectFn(msg).catch((err) => console.error("rejectFn error:", err));
103
+ return;
104
+ }
105
+ q.entries.push({ msg, arrivedAt: Date.now() });
106
+ if (!q.processing) {
107
+ this.drain(key).catch((err) => console.error("Queue drain error:", err));
108
+ }
109
+ }
110
+ /**
111
+ * Drain the queue for a channel sequentially, merging close messages.
112
+ */
113
+ async drain(key) {
114
+ const q = this.queues.get(key);
115
+ if (!q)
116
+ return;
117
+ q.processing = true;
118
+ while (q.entries.length > 0) {
119
+ const batch = this.takeMergeBatch(q);
120
+ const merged = this.mergeEntries(batch);
121
+ try {
122
+ await this.processFn(merged);
123
+ }
124
+ catch (err) {
125
+ console.error("processMessage error:", err);
126
+ }
127
+ }
128
+ q.processing = false;
129
+ }
130
+ /**
131
+ * Take a batch of entries that should be merged together.
132
+ *
133
+ * The first entry is always included. Additional entries are included if they
134
+ * arrived within mergeWindowMs of the FIRST entry in the batch. This is a
135
+ * fixed window anchored to the first message, not a sliding window — an
136
+ * entry that arrives 1ms after the previous one is still excluded if it
137
+ * falls outside the window from the first entry.
138
+ */
139
+ takeMergeBatch(q) {
140
+ const batch = [];
141
+ const first = q.entries.shift();
142
+ batch.push(first);
143
+ while (q.entries.length > 0) {
144
+ const next = q.entries[0];
145
+ if (next.arrivedAt - first.arrivedAt <= this.mergeWindowMs) {
146
+ batch.push(q.entries.shift());
147
+ }
148
+ else {
149
+ break;
150
+ }
151
+ }
152
+ return batch;
153
+ }
154
+ /**
155
+ * Merge multiple queue entries into a single IncomingMessage by joining
156
+ * their text with newlines. Metadata (userId, channelId, platform, etc.) is
157
+ * taken from the first entry.
158
+ *
159
+ * Note: images (and other non-text attachments) from subsequent burst
160
+ * entries are currently dropped — only their text is merged.
161
+ * TODO: carry images from all burst entries into the merged message.
162
+ */
163
+ mergeEntries(entries) {
164
+ if (entries.length === 1) {
165
+ return entries[0].msg;
166
+ }
167
+ const texts = entries.map((e) => e.msg.text).filter(Boolean);
168
+ return {
169
+ ...entries[0].msg,
170
+ text: texts.join("\n"),
171
+ };
172
+ }
173
+ /**
174
+ * Number of queued (not-yet-processing) messages for a channel.
175
+ * Exposed for monitoring dashboards and unit tests.
176
+ */
177
+ queueDepth(channelId) {
178
+ return this.queues.get(channelId)?.entries.length ?? 0;
179
+ }
180
+ /**
181
+ * Whether the channel is currently mid-process (drain loop running).
182
+ * Exposed for monitoring dashboards and unit tests.
183
+ */
184
+ isProcessing(channelId) {
185
+ return this.queues.get(channelId)?.processing ?? false;
186
+ }
187
+ }
188
+ //# sourceMappingURL=message-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-queue.js","sourceRoot":"","sources":["../src/message-queue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared model infrastructure: AuthStorage, ModelRegistry, models.json
3
+ * generation, and model string resolution. Used by the main agent, workers,
4
+ * reflection, and the guardrail.
5
+ *
6
+ * This module does three things:
7
+ * 1. Generates models.json from bryti config so pi's ModelRegistry
8
+ * discovers all configured providers and models.
9
+ * 2. Sets up AuthStorage, bridging Claude CLI credentials and pi auth.json
10
+ * so every LLM caller gets tokens without extra wiring.
11
+ * 3. Exposes model resolution helpers (resolveModel, resolveFirstModel)
12
+ * that translate "provider/modelId" config strings into registry Model
13
+ * objects with fuzzy fallback.
14
+ *
15
+ * These three concerns live together because every LLM-calling component
16
+ * (agent, workers, reflection, guardrail) needs all three: without models.json
17
+ * the registry has nothing to load; without AuthStorage the registry cannot
18
+ * authenticate; without resolution helpers callers cannot turn config strings
19
+ * into actual Model objects.
20
+ */
21
+ import type { Model } from "@mariozechner/pi-ai";
22
+ import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
23
+ import type { Config } from "./config.js";
24
+ export interface ModelInfra {
25
+ authStorage: AuthStorage;
26
+ modelRegistry: ModelRegistry;
27
+ agentDir: string;
28
+ }
29
+ /**
30
+ * Create the auth and model registry infrastructure from config. Generates
31
+ * models.json, sets up AuthStorage with runtime API keys, and initializes
32
+ * ModelRegistry. Shared by everything that talks to an LLM.
33
+ *
34
+ * Auth priority for Anthropic:
35
+ * 1. Claude CLI credentials (~/.claude/.credentials.json) — primary source
36
+ * 2. Pi auth.json (~/.pi/agent/auth.json) — fallback for pi CLI users
37
+ * 3. Explicit api_key in config — always wins as a runtime override
38
+ */
39
+ export declare function createModelInfra(config: Config): ModelInfra;
40
+ /**
41
+ * Resolve a "provider/modelId" string against the registry.
42
+ *
43
+ * Resolution order:
44
+ * 1. Exact match via ModelRegistry.find(provider, modelId).
45
+ * 2. Fuzzy match: scans all available models for one whose id contains
46
+ * modelId as a substring. This handles cases where model IDs in the
47
+ * registry include version suffixes (e.g. "claude-3-5-sonnet-20241022")
48
+ * that the shorter config string ("claude-3-5-sonnet") doesn't carry.
49
+ *
50
+ * Returns null if neither strategy finds a match.
51
+ */
52
+ export declare function resolveModel(modelString: string, modelRegistry: ModelRegistry): Model<any> | null;
53
+ /**
54
+ * Resolve the first usable model from an ordered list of "provider/modelId"
55
+ * candidate strings.
56
+ *
57
+ * Used by the guardrail to pick a model without requiring a full config
58
+ * context: it passes its ordered preference list and gets back whatever is
59
+ * actually available in the registry. Returns the first successful resolution,
60
+ * not the "best" or highest-capability model. Returns null if no candidate
61
+ * resolves.
62
+ */
63
+ export declare function resolveFirstModel(candidates: string[], modelRegistry: ModelRegistry): Model<any> | null;
64
+ //# sourceMappingURL=model-infra.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-infra.d.ts","sourceRoot":"","sources":["../src/model-infra.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EACL,WAAW,EACX,aAAa,EACd,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAM1C,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAwID;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAyB3D;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,aAAa,GAC3B,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAanB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAAE,EACpB,aAAa,EAAE,aAAa,GAC3B,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAMnB"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Shared model infrastructure: AuthStorage, ModelRegistry, models.json
3
+ * generation, and model string resolution. Used by the main agent, workers,
4
+ * reflection, and the guardrail.
5
+ *
6
+ * This module does three things:
7
+ * 1. Generates models.json from bryti config so pi's ModelRegistry
8
+ * discovers all configured providers and models.
9
+ * 2. Sets up AuthStorage, bridging Claude CLI credentials and pi auth.json
10
+ * so every LLM caller gets tokens without extra wiring.
11
+ * 3. Exposes model resolution helpers (resolveModel, resolveFirstModel)
12
+ * that translate "provider/modelId" config strings into registry Model
13
+ * objects with fuzzy fallback.
14
+ *
15
+ * These three concerns live together because every LLM-calling component
16
+ * (agent, workers, reflection, guardrail) needs all three: without models.json
17
+ * the registry has nothing to load; without AuthStorage the registry cannot
18
+ * authenticate; without resolution helpers callers cannot turn config strings
19
+ * into actual Model objects.
20
+ */
21
+ import fs from "node:fs";
22
+ import os from "node:os";
23
+ import path from "node:path";
24
+ import { AuthStorage, ModelRegistry, } from "@mariozechner/pi-coding-agent";
25
+ // ---------------------------------------------------------------------------
26
+ // models.json generation
27
+ // ---------------------------------------------------------------------------
28
+ /**
29
+ * Generate models.json from bryti config so pi's ModelRegistry discovers
30
+ * all configured providers and models.
31
+ */
32
+ function generateModelsJson(config, agentDir) {
33
+ const modelsJsonPath = path.join(agentDir, "models.json");
34
+ const providers = {};
35
+ for (const provider of config.models.providers) {
36
+ if (provider.name === "groq") {
37
+ // Groq's provider format is not currently compatible with the SDK's
38
+ // openai-completions adapter (auth header shape differs). Skipping
39
+ // prevents ModelRegistry from producing broken entries.
40
+ // TODO: revisit once the SDK adds a native Groq adapter.
41
+ continue;
42
+ }
43
+ // Providers without an api_key use OAuth from ~/.pi/agent/auth.json.
44
+ // We still need to include them in models.json so custom model IDs
45
+ // (not yet in SDK built-ins) are discoverable.
46
+ // Use "oauth" as a placeholder apiKey: pi's auth.json holds the real token
47
+ // and AuthStorage resolves it at call time, but ModelRegistry requires a
48
+ // non-empty apiKey field to accept the entry at all.
49
+ const apiKey = provider.api_key || "oauth";
50
+ providers[provider.name] = {
51
+ baseUrl: provider.base_url || `https://api.${provider.name}.com`,
52
+ api: provider.api || "openai-completions",
53
+ apiKey: apiKey,
54
+ models: provider.models.map((m) => ({
55
+ id: m.id,
56
+ name: m.name || m.id,
57
+ contextWindow: m.context_window || 200000,
58
+ maxTokens: m.max_tokens || 32000,
59
+ input: m.input ?? ["text", "image"],
60
+ ...(m.api && { api: m.api }),
61
+ ...(m.cost && { cost: m.cost }),
62
+ ...(m.compat && { compat: m.compat }),
63
+ })),
64
+ };
65
+ }
66
+ fs.writeFileSync(modelsJsonPath, JSON.stringify({ providers }, null, 2), "utf-8");
67
+ }
68
+ /**
69
+ * Seed AuthStorage with Anthropic credentials from the Claude CLI credential
70
+ * file (~/.claude/.credentials.json) if no Anthropic credential is already
71
+ * present in auth.json.
72
+ *
73
+ * Claude CLI is the primary auth source for users who have it installed.
74
+ * Pi auth.json is used as the fallback (for users who logged in via `pi login`).
75
+ *
76
+ * Once seeded, pi's AuthStorage manages the refresh lifecycle going forward:
77
+ * it will auto-refresh the token using the refresh token when it expires.
78
+ *
79
+ * Credential format conversion:
80
+ * Claude CLI stores { accessToken, refreshToken, expiresAt } under the
81
+ * "claudeAiOauth" key. Pi expects { type: "oauth", access, refresh, expires }.
82
+ * The field names are remapped here; the values are passed through unchanged.
83
+ *
84
+ * This function is called before runtime overrides are applied (setRuntimeApiKey),
85
+ * so an explicit api_key in config always wins over the seeded credential.
86
+ */
87
+ function seedFromClaudeCliIfNeeded(authStorage) {
88
+ // Already have Anthropic creds — nothing to do
89
+ if (authStorage.has("anthropic")) {
90
+ return;
91
+ }
92
+ const claudeCredPath = path.join(os.homedir(), ".claude", ".credentials.json");
93
+ let raw;
94
+ try {
95
+ raw = fs.readFileSync(claudeCredPath, "utf-8");
96
+ }
97
+ catch {
98
+ // File doesn't exist or isn't readable — Claude CLI not installed
99
+ return;
100
+ }
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(raw);
104
+ }
105
+ catch {
106
+ console.warn("[auth] ~/.claude/.credentials.json is not valid JSON, skipping");
107
+ return;
108
+ }
109
+ const creds = parsed.claudeAiOauth;
110
+ if (!creds?.accessToken || !creds?.refreshToken) {
111
+ console.warn("[auth] ~/.claude/.credentials.json has no usable claudeAiOauth credentials, skipping");
112
+ return;
113
+ }
114
+ // Convert Claude CLI format → pi AuthStorage OAuthCredential format:
115
+ // Claude CLI: { accessToken, refreshToken, expiresAt }
116
+ // pi: { type: "oauth", access, refresh, expires }
117
+ authStorage.set("anthropic", {
118
+ type: "oauth",
119
+ access: creds.accessToken,
120
+ refresh: creds.refreshToken,
121
+ expires: creds.expiresAt ?? Date.now(),
122
+ });
123
+ console.log("[auth] Seeded Anthropic credentials from Claude CLI (~/.claude/.credentials.json)");
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Infrastructure creation
127
+ // ---------------------------------------------------------------------------
128
+ /**
129
+ * Create the auth and model registry infrastructure from config. Generates
130
+ * models.json, sets up AuthStorage with runtime API keys, and initializes
131
+ * ModelRegistry. Shared by everything that talks to an LLM.
132
+ *
133
+ * Auth priority for Anthropic:
134
+ * 1. Claude CLI credentials (~/.claude/.credentials.json) — primary source
135
+ * 2. Pi auth.json (~/.pi/agent/auth.json) — fallback for pi CLI users
136
+ * 3. Explicit api_key in config — always wins as a runtime override
137
+ */
138
+ export function createModelInfra(config) {
139
+ const agentDir = path.join(config.data_dir, ".pi");
140
+ fs.mkdirSync(agentDir, { recursive: true });
141
+ generateModelsJson(config, agentDir);
142
+ // Auth: start with ~/.pi/agent/auth.json (pi CLI OAuth creds), then
143
+ // seed from Claude CLI if no Anthropic credential is present there.
144
+ // AuthStorage uses file-level locking so concurrent token refreshes are safe.
145
+ const authStorage = new AuthStorage();
146
+ // Seed from Claude CLI before applying runtime overrides, so an explicit
147
+ // api_key in config still wins (setRuntimeApiKey takes precedence).
148
+ seedFromClaudeCliIfNeeded(authStorage);
149
+ for (const provider of config.models.providers) {
150
+ if (provider.api_key) {
151
+ authStorage.setRuntimeApiKey(provider.name, provider.api_key);
152
+ }
153
+ }
154
+ const modelRegistry = new ModelRegistry(authStorage, path.join(agentDir, "models.json"));
155
+ modelRegistry.refresh();
156
+ return { authStorage, modelRegistry, agentDir };
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // Model resolution
160
+ // ---------------------------------------------------------------------------
161
+ /**
162
+ * Resolve a "provider/modelId" string against the registry.
163
+ *
164
+ * Resolution order:
165
+ * 1. Exact match via ModelRegistry.find(provider, modelId).
166
+ * 2. Fuzzy match: scans all available models for one whose id contains
167
+ * modelId as a substring. This handles cases where model IDs in the
168
+ * registry include version suffixes (e.g. "claude-3-5-sonnet-20241022")
169
+ * that the shorter config string ("claude-3-5-sonnet") doesn't carry.
170
+ *
171
+ * Returns null if neither strategy finds a match.
172
+ */
173
+ export function resolveModel(modelString, modelRegistry) {
174
+ const [providerName, modelId] = modelString.includes("/")
175
+ ? modelString.split("/")
176
+ : [modelString, modelString];
177
+ let model = modelRegistry.find(providerName, modelId);
178
+ if (!model) {
179
+ const available = modelRegistry.getAvailable();
180
+ model = available.find((m) => m.provider === providerName && m.id.includes(modelId));
181
+ }
182
+ return model ?? null;
183
+ }
184
+ /**
185
+ * Resolve the first usable model from an ordered list of "provider/modelId"
186
+ * candidate strings.
187
+ *
188
+ * Used by the guardrail to pick a model without requiring a full config
189
+ * context: it passes its ordered preference list and gets back whatever is
190
+ * actually available in the registry. Returns the first successful resolution,
191
+ * not the "best" or highest-capability model. Returns null if no candidate
192
+ * resolves.
193
+ */
194
+ export function resolveFirstModel(candidates, modelRegistry) {
195
+ for (const candidate of candidates) {
196
+ const model = resolveModel(candidate, modelRegistry);
197
+ if (model)
198
+ return model;
199
+ }
200
+ return null;
201
+ }
202
+ //# sourceMappingURL=model-infra.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-infra.js","sourceRoot":"","sources":["../src/model-infra.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Format projections for system prompt injection.
3
+ */
4
+ import type { Projection } from "./store.js";
5
+ /**
6
+ * Render a short projection list suitable for inclusion in the system prompt.
7
+ * Capped at maxItems to avoid crowding context.
8
+ */
9
+ export declare function formatProjectionsForPrompt(projections: Projection[], maxItems?: number): string;
10
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/projection/format.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,UAAU,EAAE,EAAE,QAAQ,SAAK,GAAG,MAAM,CAwB3F"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Format projections for system prompt injection.
3
+ */
4
+ /**
5
+ * Render a short projection list suitable for inclusion in the system prompt.
6
+ * Capped at maxItems to avoid crowding context.
7
+ */
8
+ export function formatProjectionsForPrompt(projections, maxItems = 15) {
9
+ if (projections.length === 0) {
10
+ return "No upcoming projections.";
11
+ }
12
+ const capped = projections.slice(0, maxItems);
13
+ const lines = capped.map((p) => {
14
+ const when = p.trigger_on_fact
15
+ ? `[waiting for: "${p.trigger_on_fact}"]`
16
+ : p.resolved_when
17
+ ? `[${p.resolved_when.slice(0, 16)}, ${p.resolution}]`
18
+ : p.raw_when
19
+ ? `[${p.raw_when}, ${p.resolution}]`
20
+ : `[someday]`;
21
+ const recur = p.recurrence ? ` [recurring: ${p.recurrence}]` : "";
22
+ const ctx = p.context ? ` — ${p.context}` : "";
23
+ return `- ${when}${recur} ${p.summary}${ctx} (id: ${p.id})`;
24
+ });
25
+ const overflow = projections.length > maxItems
26
+ ? `\n(${projections.length - maxItems} more — use projection_list to see all)`
27
+ : "";
28
+ return lines.join("\n") + overflow;
29
+ }
30
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/projection/format.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;;GAGG;AACH,MAAM,UAAU,0BAA0B,CAAC,WAAyB,EAAE,QAAQ,GAAG,EAAE;IACjF,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,0BAA0B,CAAC;IACpC,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,CAAC,CAAC,eAAe;YAC5B,CAAC,CAAC,kBAAkB,CAAC,CAAC,eAAe,IAAI;YACzC,CAAC,CAAC,CAAC,CAAC,aAAa;gBACf,CAAC,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,GAAG;gBACtD,CAAC,CAAC,CAAC,CAAC,QAAQ;oBACV,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,UAAU,GAAG;oBACpC,CAAC,CAAC,WAAW,CAAC;QACpB,MAAM,KAAK,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAClE,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,OAAO,GAAG,GAAG,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,GAAG,QAAQ;QAC5C,CAAC,CAAC,MAAM,WAAW,CAAC,MAAM,GAAG,QAAQ,yCAAyC;QAC9E,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC;AACrC,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Projection: forward-looking agent memory.
3
+ * Memory answers "what do I know?"; projection answers "what do I expect?"
4
+ */
5
+ export { createProjectionStore } from "./store.js";
6
+ export type { Projection, ProjectionResolution, ProjectionStatus, ProjectionStore, ProjectionDependency, ProjectionDependencyInput, DependencyConditionType, } from "./store.js";
7
+ export { formatProjectionsForPrompt } from "./format.js";
8
+ export { createProjectionTools } from "./tools.js";
9
+ export { runReflection } from "./reflection.js";
10
+ export type { ReflectionResult } from "./reflection.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/projection/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,YAAY,EACV,UAAU,EACV,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Projection: forward-looking agent memory.
3
+ * Memory answers "what do I know?"; projection answers "what do I expect?"
4
+ */
5
+ export { createProjectionStore } from "./store.js";
6
+ export { formatProjectionsForPrompt } from "./format.js";
7
+ export { createProjectionTools } from "./tools.js";
8
+ export { runReflection } from "./reflection.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/projection/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAUnD,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Projection reflection pass.
3
+ *
4
+ * Lightweight background job that scans recent conversation history for
5
+ * future references the agent missed during live chat.
6
+ *
7
+ * Runs every 30 min via cron. Reads the JSONL audit log, makes a single
8
+ * completeSimple() call with a narrow extraction prompt (no agent loop,
9
+ * no tools), parses the JSON output, and writes projections directly to
10
+ * SQLite. Existing pending projections are included in the prompt so the
11
+ * model won't duplicate them. A per-user timestamp tracks the last run
12
+ * to skip unchanged transcripts.
13
+ *
14
+ * Why completeSimple() instead of a full agent loop?
15
+ * The reflection pass has no side effects and requires no tool calls. It only
16
+ * needs one prompt in and one JSON blob out. Using a full agent loop would add
17
+ * latency, cost, and the risk of unintended tool invocations. completeSimple()
18
+ * is cheaper, faster, and keeps the pass strictly read-only from the model's
19
+ * perspective.
20
+ */
21
+ import type { Config } from "../config.js";
22
+ import type { ProjectionResolution, ProjectionStore } from "./store.js";
23
+ export interface ProjectionCandidate {
24
+ summary: string;
25
+ when?: string;
26
+ resolution?: ProjectionResolution;
27
+ context?: string;
28
+ }
29
+ export interface ArchiveCandidate {
30
+ content: string;
31
+ }
32
+ export interface ReflectionOutput {
33
+ project: ProjectionCandidate[];
34
+ /**
35
+ * TODO: archive candidates are extracted from the LLM output but are not
36
+ * currently written anywhere. This field is a placeholder for a future
37
+ * feature that would auto-insert noteworthy facts into archival memory
38
+ * during the reflection pass.
39
+ */
40
+ archive: ArchiveCandidate[];
41
+ }
42
+ export interface ReflectionResult {
43
+ /** Number of projections written to the store. */
44
+ projectionsAdded: number;
45
+ /** Raw candidates extracted (before dedup). */
46
+ candidates: ProjectionCandidate[];
47
+ /** Whether reflection was skipped (no new messages). */
48
+ skipped: boolean;
49
+ /** Reason for skipping, if applicable. */
50
+ skipReason?: string;
51
+ }
52
+ interface HistoryEntry {
53
+ role: "user" | "assistant" | "system" | "tool";
54
+ content: string;
55
+ timestamp: string;
56
+ }
57
+ /**
58
+ * Read user+assistant messages from the JSONL audit log for the last
59
+ * `windowMinutes`. Returns entries in chronological order, capped at
60
+ * `maxMessages`.
61
+ *
62
+ * Reads from the JSONL audit log written by src/compaction/history.ts, NOT
63
+ * from the pi SDK session file. The audit log is the only way to get
64
+ * structured turn-by-turn history outside of a live session context: the pi
65
+ * session file is append-only and interleaved with tool scaffolding, whereas
66
+ * the audit log contains clean role/content/timestamp records per message.
67
+ */
68
+ export declare function readRecentHistory(historyDir: string, windowMinutes: number, maxMessages?: number): HistoryEntry[];
69
+ interface CompletionMessage {
70
+ role: "system" | "user";
71
+ content: string;
72
+ }
73
+ /**
74
+ * Single chat completion via the pi SDK provider layer. Uses reflection_model
75
+ * if configured, otherwise falls back to the primary model and then the
76
+ * fallback chain. This lets operators use a cheaper model for reflection.
77
+ */
78
+ export declare function sdkComplete(config: Config, messages: CompletionMessage[]): Promise<string>;
79
+ /**
80
+ * Parse the LLM output, tolerating markdown code fences and minor formatting.
81
+ *
82
+ * Even with `temperature: 0` and an explicit "output JSON only" instruction,
83
+ * models occasionally wrap their response in a ```json ... ``` code fence.
84
+ * The stripping step removes those fences before calling JSON.parse(), so
85
+ * both bare JSON and fenced JSON are accepted.
86
+ */
87
+ export declare function parseReflectionOutput(raw: string): ReflectionOutput;
88
+ /**
89
+ * Run one reflection pass for a user. Reads recent conversation, extracts
90
+ * future references via LLM, and writes new projections to the store.
91
+ */
92
+ export declare function runReflection(config: Config, userId: string, windowMinutes?: number, store?: ProjectionStore, completeFn?: typeof sdkComplete): Promise<ReflectionResult>;
93
+ export {};
94
+ //# sourceMappingURL=reflection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reflection.d.ts","sourceRoot":"","sources":["../../src/projection/reflection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAUxE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,gBAAgB,EAAE,MAAM,CAAC;IACzB,+CAA+C;IAC/C,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC,wDAAwD;IACxD,OAAO,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAMD,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,WAAW,SAAK,GACf,YAAY,EAAE,CAyChB;AAuCD,UAAU,iBAAiB;IACzB,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,iBAAiB,EAAE,GAC5B,OAAO,CAAC,MAAM,CAAC,CA6CjB;AA+CD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAgBnE;AAMD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,aAAa,SAAK,EAClB,KAAK,CAAC,EAAE,eAAe,EACvB,UAAU,CAAC,EAAE,OAAO,WAAW,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAwH3B"}