@corgelabs/corge 0.0.1 → 1.0.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 (341) hide show
  1. package/dist/ai/index.d.ts +3 -0
  2. package/dist/ai/index.d.ts.map +1 -0
  3. package/dist/ai/index.js +3 -0
  4. package/dist/ai/index.js.map +1 -0
  5. package/dist/ai/schema.d.ts +83 -0
  6. package/dist/ai/schema.d.ts.map +1 -0
  7. package/dist/ai/schema.js +19 -0
  8. package/dist/ai/schema.js.map +1 -0
  9. package/dist/ai/service.d.ts +14 -0
  10. package/dist/ai/service.d.ts.map +1 -0
  11. package/dist/ai/service.js +266 -0
  12. package/dist/ai/service.js.map +1 -0
  13. package/dist/ai/utils.d.ts +10 -0
  14. package/dist/ai/utils.d.ts.map +1 -0
  15. package/dist/ai/utils.js +27 -0
  16. package/dist/ai/utils.js.map +1 -0
  17. package/dist/attachments/index.d.ts +4 -0
  18. package/dist/attachments/index.d.ts.map +1 -0
  19. package/dist/attachments/index.js +4 -0
  20. package/dist/attachments/index.js.map +1 -0
  21. package/dist/attachments/routes.d.ts +75 -0
  22. package/dist/attachments/routes.d.ts.map +1 -0
  23. package/dist/attachments/routes.js +18 -0
  24. package/dist/attachments/routes.js.map +1 -0
  25. package/dist/attachments/schema.d.ts +61 -0
  26. package/dist/attachments/schema.d.ts.map +1 -0
  27. package/dist/attachments/schema.js +30 -0
  28. package/dist/attachments/schema.js.map +1 -0
  29. package/dist/attachments/service.d.ts +27 -0
  30. package/dist/attachments/service.d.ts.map +1 -0
  31. package/dist/attachments/service.js +224 -0
  32. package/dist/attachments/service.js.map +1 -0
  33. package/dist/attachments/utils.d.ts +9 -0
  34. package/dist/attachments/utils.d.ts.map +1 -0
  35. package/dist/attachments/utils.js +80 -0
  36. package/dist/attachments/utils.js.map +1 -0
  37. package/dist/auth/index.d.ts +4 -0
  38. package/dist/auth/index.d.ts.map +1 -0
  39. package/dist/auth/index.js +4 -0
  40. package/dist/auth/index.js.map +1 -0
  41. package/dist/auth/oidc.d.ts +25 -0
  42. package/dist/auth/oidc.d.ts.map +1 -0
  43. package/dist/auth/oidc.js +298 -0
  44. package/dist/auth/oidc.js.map +1 -0
  45. package/dist/auth/recovery.d.ts +41 -0
  46. package/dist/auth/recovery.d.ts.map +1 -0
  47. package/dist/auth/recovery.js +250 -0
  48. package/dist/auth/recovery.js.map +1 -0
  49. package/dist/auth/routes.d.ts +226 -0
  50. package/dist/auth/routes.d.ts.map +1 -0
  51. package/dist/auth/routes.js +96 -0
  52. package/dist/auth/routes.js.map +1 -0
  53. package/dist/auth/schema.d.ts +158 -0
  54. package/dist/auth/schema.d.ts.map +1 -0
  55. package/dist/auth/schema.js +82 -0
  56. package/dist/auth/schema.js.map +1 -0
  57. package/dist/auth/service.d.ts +96 -0
  58. package/dist/auth/service.d.ts.map +1 -0
  59. package/dist/auth/service.js +129 -0
  60. package/dist/auth/service.js.map +1 -0
  61. package/dist/auth/session.d.ts +51 -0
  62. package/dist/auth/session.d.ts.map +1 -0
  63. package/dist/auth/session.js +344 -0
  64. package/dist/auth/session.js.map +1 -0
  65. package/dist/auth/utils.d.ts +17 -0
  66. package/dist/auth/utils.d.ts.map +1 -0
  67. package/dist/auth/utils.js +96 -0
  68. package/dist/auth/utils.js.map +1 -0
  69. package/dist/bin/corge.js +542 -0
  70. package/dist/chat/context.d.ts +10 -0
  71. package/dist/chat/context.d.ts.map +1 -0
  72. package/dist/chat/context.js +221 -0
  73. package/dist/chat/context.js.map +1 -0
  74. package/dist/chat/index.d.ts +4 -0
  75. package/dist/chat/index.d.ts.map +1 -0
  76. package/dist/chat/index.js +4 -0
  77. package/dist/chat/index.js.map +1 -0
  78. package/dist/chat/routes.d.ts +160 -0
  79. package/dist/chat/routes.d.ts.map +1 -0
  80. package/dist/chat/routes.js +210 -0
  81. package/dist/chat/routes.js.map +1 -0
  82. package/dist/chat/schema.d.ts +285 -0
  83. package/dist/chat/schema.d.ts.map +1 -0
  84. package/dist/chat/schema.js +74 -0
  85. package/dist/chat/schema.js.map +1 -0
  86. package/dist/chat/service.d.ts +64 -0
  87. package/dist/chat/service.d.ts.map +1 -0
  88. package/dist/chat/service.js +279 -0
  89. package/dist/chat/service.js.map +1 -0
  90. package/dist/chat/stream.d.ts +53 -0
  91. package/dist/chat/stream.d.ts.map +1 -0
  92. package/dist/chat/stream.js +339 -0
  93. package/dist/chat/stream.js.map +1 -0
  94. package/dist/chat/utils.d.ts +61 -0
  95. package/dist/chat/utils.d.ts.map +1 -0
  96. package/dist/chat/utils.js +285 -0
  97. package/dist/chat/utils.js.map +1 -0
  98. package/dist/cli/builder.d.ts +6 -0
  99. package/dist/cli/builder.d.ts.map +1 -0
  100. package/dist/cli/builder.js +91 -0
  101. package/dist/cli/builder.js.map +1 -0
  102. package/dist/cli/deployer.d.ts +2 -0
  103. package/dist/cli/deployer.d.ts.map +1 -0
  104. package/dist/cli/deployer.js +152 -0
  105. package/dist/cli/deployer.js.map +1 -0
  106. package/dist/cli/dev-server.d.ts +2 -0
  107. package/dist/cli/dev-server.d.ts.map +1 -0
  108. package/dist/cli/dev-server.js +70 -0
  109. package/dist/cli/dev-server.js.map +1 -0
  110. package/dist/cli/env.d.ts +4 -0
  111. package/dist/cli/env.d.ts.map +1 -0
  112. package/dist/cli/env.js +68 -0
  113. package/dist/cli/env.js.map +1 -0
  114. package/dist/cli/init.d.ts +6 -0
  115. package/dist/cli/init.d.ts.map +1 -0
  116. package/dist/cli/init.js +97 -0
  117. package/dist/cli/init.js.map +1 -0
  118. package/dist/config.d.ts +262 -0
  119. package/dist/config.d.ts.map +1 -0
  120. package/dist/config.js +212 -0
  121. package/dist/config.js.map +1 -0
  122. package/dist/db/index.d.ts +3 -0
  123. package/dist/db/index.d.ts.map +1 -0
  124. package/dist/db/index.js +3 -0
  125. package/dist/db/index.js.map +1 -0
  126. package/dist/db/schema.d.ts +410 -0
  127. package/dist/db/schema.d.ts.map +1 -0
  128. package/dist/db/schema.js +294 -0
  129. package/dist/db/schema.js.map +1 -0
  130. package/dist/db/service.d.ts +42 -0
  131. package/dist/db/service.d.ts.map +1 -0
  132. package/dist/db/service.js +372 -0
  133. package/dist/db/service.js.map +1 -0
  134. package/dist/db/utils.d.ts +25 -0
  135. package/dist/db/utils.d.ts.map +1 -0
  136. package/dist/db/utils.js +188 -0
  137. package/dist/db/utils.js.map +1 -0
  138. package/dist/emails/index.d.ts +4 -0
  139. package/dist/emails/index.d.ts.map +1 -0
  140. package/dist/emails/index.js +4 -0
  141. package/dist/emails/index.js.map +1 -0
  142. package/dist/emails/routes.d.ts +191 -0
  143. package/dist/emails/routes.d.ts.map +1 -0
  144. package/dist/emails/routes.js +39 -0
  145. package/dist/emails/routes.js.map +1 -0
  146. package/dist/emails/schema.d.ts +237 -0
  147. package/dist/emails/schema.d.ts.map +1 -0
  148. package/dist/emails/schema.js +98 -0
  149. package/dist/emails/schema.js.map +1 -0
  150. package/dist/emails/service.d.ts +22 -0
  151. package/dist/emails/service.d.ts.map +1 -0
  152. package/dist/emails/service.js +337 -0
  153. package/dist/emails/service.js.map +1 -0
  154. package/dist/emails/utils.d.ts +49 -0
  155. package/dist/emails/utils.d.ts.map +1 -0
  156. package/dist/emails/utils.js +341 -0
  157. package/dist/emails/utils.js.map +1 -0
  158. package/dist/files/index.d.ts +4 -0
  159. package/dist/files/index.d.ts.map +1 -0
  160. package/dist/files/index.js +4 -0
  161. package/dist/files/index.js.map +1 -0
  162. package/dist/files/routes.d.ts +72 -0
  163. package/dist/files/routes.d.ts.map +1 -0
  164. package/dist/files/routes.js +38 -0
  165. package/dist/files/routes.js.map +1 -0
  166. package/dist/files/schema.d.ts +57 -0
  167. package/dist/files/schema.d.ts.map +1 -0
  168. package/dist/files/schema.js +39 -0
  169. package/dist/files/schema.js.map +1 -0
  170. package/dist/files/service.d.ts +27 -0
  171. package/dist/files/service.d.ts.map +1 -0
  172. package/dist/files/service.js +175 -0
  173. package/dist/files/service.js.map +1 -0
  174. package/dist/files/utils.d.ts +3 -0
  175. package/dist/files/utils.d.ts.map +1 -0
  176. package/dist/files/utils.js +63 -0
  177. package/dist/files/utils.js.map +1 -0
  178. package/dist/index.d.ts +875 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +9919 -0
  181. package/dist/index.js.map +7 -0
  182. package/dist/payments/billing.d.ts +29 -0
  183. package/dist/payments/billing.d.ts.map +1 -0
  184. package/dist/payments/billing.js +378 -0
  185. package/dist/payments/billing.js.map +1 -0
  186. package/dist/payments/index.d.ts +4 -0
  187. package/dist/payments/index.d.ts.map +1 -0
  188. package/dist/payments/index.js +4 -0
  189. package/dist/payments/index.js.map +1 -0
  190. package/dist/payments/routes.d.ts +340 -0
  191. package/dist/payments/routes.d.ts.map +1 -0
  192. package/dist/payments/routes.js +56 -0
  193. package/dist/payments/routes.js.map +1 -0
  194. package/dist/payments/schema.d.ts +412 -0
  195. package/dist/payments/schema.d.ts.map +1 -0
  196. package/dist/payments/schema.js +260 -0
  197. package/dist/payments/schema.js.map +1 -0
  198. package/dist/payments/service.d.ts +31 -0
  199. package/dist/payments/service.d.ts.map +1 -0
  200. package/dist/payments/service.js +322 -0
  201. package/dist/payments/service.js.map +1 -0
  202. package/dist/payments/utils.d.ts +61 -0
  203. package/dist/payments/utils.d.ts.map +1 -0
  204. package/dist/payments/utils.js +369 -0
  205. package/dist/payments/utils.js.map +1 -0
  206. package/dist/router/guard.d.ts +19 -0
  207. package/dist/router/guard.d.ts.map +1 -0
  208. package/dist/router/guard.js +243 -0
  209. package/dist/router/guard.js.map +1 -0
  210. package/dist/router/index.d.ts +3 -0
  211. package/dist/router/index.d.ts.map +1 -0
  212. package/dist/router/index.js +3 -0
  213. package/dist/router/index.js.map +1 -0
  214. package/dist/router/schema.d.ts +103 -0
  215. package/dist/router/schema.d.ts.map +1 -0
  216. package/dist/router/schema.js +282 -0
  217. package/dist/router/schema.js.map +1 -0
  218. package/dist/router/service.d.ts +25 -0
  219. package/dist/router/service.d.ts.map +1 -0
  220. package/dist/router/service.js +339 -0
  221. package/dist/router/service.js.map +1 -0
  222. package/dist/tasks/index.d.ts +4 -0
  223. package/dist/tasks/index.d.ts.map +1 -0
  224. package/dist/tasks/index.js +4 -0
  225. package/dist/tasks/index.js.map +1 -0
  226. package/dist/tasks/routes.d.ts +54 -0
  227. package/dist/tasks/routes.d.ts.map +1 -0
  228. package/dist/tasks/routes.js +195 -0
  229. package/dist/tasks/routes.js.map +1 -0
  230. package/dist/tasks/schema.d.ts +261 -0
  231. package/dist/tasks/schema.d.ts.map +1 -0
  232. package/dist/tasks/schema.js +35 -0
  233. package/dist/tasks/schema.js.map +1 -0
  234. package/dist/tasks/service.d.ts +32 -0
  235. package/dist/tasks/service.d.ts.map +1 -0
  236. package/dist/tasks/service.js +201 -0
  237. package/dist/tasks/service.js.map +1 -0
  238. package/dist/tasks/steps.d.ts +18 -0
  239. package/dist/tasks/steps.d.ts.map +1 -0
  240. package/dist/tasks/steps.js +365 -0
  241. package/dist/tasks/steps.js.map +1 -0
  242. package/dist/tasks/utils.d.ts +75 -0
  243. package/dist/tasks/utils.d.ts.map +1 -0
  244. package/dist/tasks/utils.js +311 -0
  245. package/dist/tasks/utils.js.map +1 -0
  246. package/dist/tasks/watcher.d.ts +13 -0
  247. package/dist/tasks/watcher.d.ts.map +1 -0
  248. package/dist/tasks/watcher.js +255 -0
  249. package/dist/tasks/watcher.js.map +1 -0
  250. package/dist/tasks/worker.d.ts +28 -0
  251. package/dist/tasks/worker.d.ts.map +1 -0
  252. package/dist/tasks/worker.js +326 -0
  253. package/dist/tasks/worker.js.map +1 -0
  254. package/dist/utils/index.d.ts +3 -0
  255. package/dist/utils/index.d.ts.map +1 -0
  256. package/dist/utils/index.js +3 -0
  257. package/dist/utils/index.js.map +1 -0
  258. package/dist/utils/schema.d.ts +79 -0
  259. package/dist/utils/schema.d.ts.map +1 -0
  260. package/dist/utils/schema.js +41 -0
  261. package/dist/utils/schema.js.map +1 -0
  262. package/dist/utils/service.d.ts +32 -0
  263. package/dist/utils/service.d.ts.map +1 -0
  264. package/dist/utils/service.js +384 -0
  265. package/dist/utils/service.js.map +1 -0
  266. package/docs/SDK_SPEC.md +1645 -0
  267. package/infra/firestore.base.indexes.json +138 -0
  268. package/infra/firestore.base.rules +152 -0
  269. package/infra/storage.base.rules +8 -0
  270. package/package.json +58 -13
  271. package/src/ai/index.ts +2 -0
  272. package/src/ai/schema.ts +64 -0
  273. package/src/ai/service.ts +317 -0
  274. package/src/ai/utils.ts +38 -0
  275. package/src/attachments/index.ts +3 -0
  276. package/src/attachments/routes.ts +28 -0
  277. package/src/attachments/schema.ts +45 -0
  278. package/src/attachments/service.ts +315 -0
  279. package/src/attachments/utils.ts +115 -0
  280. package/src/auth/index.ts +3 -0
  281. package/src/auth/oidc.ts +419 -0
  282. package/src/auth/recovery.ts +365 -0
  283. package/src/auth/routes.ts +142 -0
  284. package/src/auth/schema.ts +181 -0
  285. package/src/auth/service.ts +183 -0
  286. package/src/auth/session.ts +414 -0
  287. package/src/auth/utils.ts +106 -0
  288. package/src/chat/context.ts +310 -0
  289. package/src/chat/index.ts +3 -0
  290. package/src/chat/routes.ts +264 -0
  291. package/src/chat/schema.ts +295 -0
  292. package/src/chat/service.ts +360 -0
  293. package/src/chat/stream.ts +481 -0
  294. package/src/chat/utils.ts +370 -0
  295. package/src/cli/builder.ts +93 -0
  296. package/src/cli/deployer.ts +184 -0
  297. package/src/cli/dev-server.ts +77 -0
  298. package/src/cli/env.ts +89 -0
  299. package/src/cli/init.ts +121 -0
  300. package/src/config.ts +447 -0
  301. package/src/db/index.ts +2 -0
  302. package/src/db/schema.ts +409 -0
  303. package/src/db/service.ts +477 -0
  304. package/src/db/utils.ts +250 -0
  305. package/src/emails/index.ts +3 -0
  306. package/src/emails/routes.ts +64 -0
  307. package/src/emails/schema.ts +239 -0
  308. package/src/emails/service.ts +488 -0
  309. package/src/emails/utils.ts +449 -0
  310. package/src/files/index.ts +3 -0
  311. package/src/files/routes.ts +44 -0
  312. package/src/files/schema.ts +76 -0
  313. package/src/files/service.ts +238 -0
  314. package/src/files/utils.ts +77 -0
  315. package/src/index.ts +425 -0
  316. package/src/payments/billing.ts +488 -0
  317. package/src/payments/index.ts +3 -0
  318. package/src/payments/routes.ts +92 -0
  319. package/src/payments/schema.ts +401 -0
  320. package/src/payments/service.ts +445 -0
  321. package/src/payments/utils.ts +472 -0
  322. package/src/router/guard.ts +319 -0
  323. package/src/router/index.ts +2 -0
  324. package/src/router/schema.ts +458 -0
  325. package/src/router/service.ts +453 -0
  326. package/src/tasks/index.ts +11 -0
  327. package/src/tasks/routes.ts +230 -0
  328. package/src/tasks/schema.ts +310 -0
  329. package/src/tasks/service.ts +254 -0
  330. package/src/tasks/steps.ts +472 -0
  331. package/src/tasks/utils.ts +445 -0
  332. package/src/tasks/watcher.ts +307 -0
  333. package/src/tasks/worker.ts +461 -0
  334. package/src/utils/index.ts +2 -0
  335. package/src/utils/schema.ts +69 -0
  336. package/src/utils/service.ts +461 -0
  337. package/tests/e2e.test.ts +2531 -0
  338. package/tsconfig.base.json +12 -0
  339. package/README.md +0 -2
  340. package/index.d.ts +0 -1
  341. package/index.js +0 -1
@@ -0,0 +1,295 @@
1
+ import type { FieldValue, Timestamp } from 'firebase-admin/firestore'
2
+ import type { ModelMessage, streamText } from 'ai'
3
+ import { z } from 'zod'
4
+ import type { AttachmentTypesMap } from '../attachments'
5
+ import type { AppBindings, DbRole, RateLimitConfig } from '../config'
6
+ import { entryId, fileRef, type EntryId, type FileRef } from '../db'
7
+ import type { Core } from '../index'
8
+ import type { AuthLevel } from '../router'
9
+
10
+ export type VectorValue = ReturnType<typeof FieldValue.vector>
11
+
12
+ export type SystemPromptBuilder = (ctx: {
13
+ today: string
14
+ userId: EntryId
15
+ env: AppBindings
16
+ }) => string | Promise<string>
17
+
18
+ export interface AgentToolPresets {
19
+ discovery?: string[]
20
+ workflow?: string[]
21
+ }
22
+
23
+ /** Per-user data injected into a chat turn by `AgentConfig.resolveUserContext`. */
24
+ export interface UserContext {
25
+ /** Compact flags (≤ ~1 KB). Sent to the InfoBase classifier and to the agent. */
26
+ state: Record<string, unknown>
27
+ /** Capped records (≤ ~8 KB). Sent to the agent only, never to the classifier. */
28
+ details?: Record<string, unknown>
29
+ }
30
+
31
+ export type UserContextResolver = (ctx: {
32
+ userId: EntryId
33
+ core: Core
34
+ signal: AbortSignal
35
+ }) => Promise<UserContext | undefined>
36
+
37
+ export interface AgentConfig {
38
+ name?: string
39
+ description?: string
40
+ model?: string
41
+ systemPrompt: string | SystemPromptBuilder
42
+ tools?: string[]
43
+ toolPresets?: AgentToolPresets
44
+ maxSteps?: number
45
+ useContext?: boolean
46
+ /** Restricts InfoBase items to those tagged with any of these audiences. Omit for no filtering. */
47
+ contextAudiences?: readonly string[]
48
+ /** Loads the caller's own data for each turn. Must honour `signal`; failures degrade to no user context. */
49
+ resolveUserContext?: UserContextResolver
50
+ attachmentTypes?: AttachmentTypesMap
51
+ auth?: AuthLevel
52
+ rateLimit?: RateLimitConfig
53
+ mcp?: boolean
54
+ }
55
+
56
+ export const defineAgent = <C extends AgentConfig>(config: C): C => config
57
+ export const defineAgents = <T extends Record<string, AgentConfig>>(agents: T): T => agents
58
+
59
+ export interface ChatMetadataDoc {
60
+ id: string
61
+ userId: string
62
+ agentId: string
63
+ title: string
64
+ model: string
65
+ messageCount: number
66
+ summary?: string | null
67
+ activeGenerationId?: string | null
68
+ deleted: boolean
69
+ createdAt: Timestamp | Date
70
+ updatedAt: Timestamp | Date
71
+ }
72
+
73
+ export type ChatMessageRole = 'user' | 'assistant' | 'system' | 'tool'
74
+
75
+ export interface ChatMessageDoc {
76
+ id: string
77
+ chatId: string
78
+ userId: string
79
+ role: ChatMessageRole
80
+ content: string
81
+ parts: unknown[]
82
+ toolInvocations?: unknown[]
83
+ attachments?: string[]
84
+ tokenCount?: number
85
+ cancelled?: boolean
86
+ createdAt: Timestamp | Date
87
+ embedding?: VectorValue
88
+ }
89
+
90
+ export type ChatMessagePublic = Omit<ChatMessageDoc, 'embedding' | 'createdAt'> & {
91
+ createdAt: Date
92
+ }
93
+
94
+ export interface RecalledMemorySnippet {
95
+ id: string
96
+ chatId: string
97
+ role: ChatMessageRole
98
+ content: string
99
+ scope: 'in_chat' | 'cross_chat'
100
+ }
101
+
102
+ export interface ContextItemSpec {
103
+ path: string
104
+ description: string
105
+ /** Audiences allowed to receive this item (matched against `AgentConfig.contextAudiences`). Omit to expose to every agent. */
106
+ audiences?: readonly string[]
107
+ }
108
+
109
+ export const defineContext = <const T extends readonly ContextItemSpec[]>(items: T): T => items
110
+
111
+ export interface ContextEntry {
112
+ path: string
113
+ description: string
114
+ markdown: string
115
+ }
116
+
117
+ export interface GetContextOptions {
118
+ maxSources?: number
119
+ /** Only items tagged with any of these audiences (or untagged items) are considered. */
120
+ audiences?: readonly string[]
121
+ /** Compact caller state forwarded to the classifier to judge relevance. */
122
+ userState?: Record<string, unknown>
123
+ }
124
+
125
+ export interface DynamicContextBlockInput {
126
+ currentTimeIso?: string
127
+ recalledMemories?: readonly RecalledMemorySnippet[]
128
+ infobaseContextEntries?: readonly ContextEntry[]
129
+ stagedAttachmentSummaries?: readonly string[]
130
+ userContext?: UserContext
131
+ }
132
+
133
+ export interface HybridRetrievalOptions {
134
+ userId?: EntryId
135
+ chatId?: string
136
+ messageCount?: number
137
+ prompt: string
138
+ useContext?: boolean
139
+ slidingWindowSize?: number
140
+ /** Agent whose `contextAudiences` and `resolveUserContext` scope this turn. */
141
+ agent?: Pick<AgentConfig, 'contextAudiences' | 'resolveUserContext'>
142
+ /** Verified caller role (defaults to `resolveUserRole(userId)`). */
143
+ role?: DbRole
144
+ abortSignal?: AbortSignal
145
+ }
146
+
147
+ export interface HybridRetrievalResult {
148
+ slidingWindowDocs: ChatMessagePublic[]
149
+ slidingWindowMessages: ModelMessage[]
150
+ recalledMemories: RecalledMemorySnippet[]
151
+ infobaseContextEntries: ContextEntry[]
152
+ userContext?: UserContext
153
+ }
154
+
155
+ export interface ListChatMessagesOptions {
156
+ limit?: number
157
+ before?: EntryId
158
+ }
159
+
160
+ export interface WorkflowToolSpec {
161
+ description: string
162
+ input: z.ZodType<any>
163
+ mcp?: boolean
164
+ tool?: boolean
165
+ execute: (input: any, userId: EntryId) => Promise<unknown>
166
+ }
167
+
168
+ export interface VerifiedAgentToolContext {
169
+ core: Core
170
+ chatId?: string
171
+ abortSignal?: AbortSignal
172
+ }
173
+
174
+ export interface RunAgentOptions {
175
+ modelOverride?: string
176
+ chatId?: string
177
+ abortSignal?: AbortSignal
178
+ dynamicContext?: DynamicContextBlockInput
179
+ core?: Core
180
+ onFinish?: Parameters<typeof streamText>[0]['onFinish']
181
+ }
182
+
183
+ export interface AskAgentOptions {
184
+ userId?: EntryId
185
+ includeContext?: boolean
186
+ abortSignal?: AbortSignal
187
+ }
188
+
189
+ export const ReasoningExpertInputDto = z.object({
190
+ problemStatement: z.string().describe('Self-contained specification of the complex reasoning task.'),
191
+ constraintsAndContext: z.string().describe('Compact summary of relevant data, constraints, or document fields.')
192
+ })
193
+
194
+ export const ReasoningExpertOutputDto = z.object({
195
+ analysisSummary: z.string(),
196
+ recommendedSolution: z.string(),
197
+ edgeCasesToWatch: z.array(z.string())
198
+ })
199
+
200
+ export interface RawAttachmentPart {
201
+ dataUrl?: string
202
+ ref?: FileRef
203
+ filename: string
204
+ }
205
+
206
+ export interface PersistTurnCompletionOpts {
207
+ turn: ReturnType<typeof import('ai').streamText>
208
+ getAccumulatedText: () => string
209
+ signal: AbortSignal
210
+ userVecPromise: Promise<number[] | null>
211
+ chatDocRef: import('firebase-admin/firestore').DocumentReference
212
+ chatId: string
213
+ userId: EntryId
214
+ resolvedAgentId: string
215
+ title: string
216
+ selectedModel: string
217
+ userMsgId: string
218
+ assistantMsgId: string
219
+ userCreatedAtMs: number
220
+ finalUserText: string
221
+ userPartsForStorage: unknown[]
222
+ stagedRefs: FileRef[]
223
+ unsubCancelWatch: (() => void) | undefined
224
+ key: string
225
+ }
226
+
227
+ export const chatIdParam = z.object({ chatId: entryId })
228
+
229
+ export const listChatMessagesQuery = z.object({
230
+ limit: z.coerce.number().int().min(1).max(200).optional(),
231
+ before: entryId.optional()
232
+ })
233
+
234
+ export const saveChatBody = z
235
+ .object({
236
+ id: entryId.optional(),
237
+ chatId: entryId.optional(),
238
+ title: z.string().min(1).max(256).optional(),
239
+ agentId: z.string().min(1).max(128).optional(),
240
+ model: z.string().min(1).max(128).optional(),
241
+ summary: z.string().max(4000).nullable().optional()
242
+ })
243
+ .strict()
244
+
245
+ export const streamChatMessageInput = z.object({
246
+ id: entryId.optional(),
247
+ role: z.literal('user').default('user'),
248
+ content: z.string().optional(),
249
+ parts: z.array(z.record(z.string(), z.unknown())).optional(),
250
+ attachments: z.array(fileRef).max(20).optional()
251
+ })
252
+
253
+ export type StreamChatMessageInput = z.infer<typeof streamChatMessageInput>
254
+
255
+ export const streamChatBody = z
256
+ .object({
257
+ id: entryId.optional(),
258
+ chatId: entryId.optional(),
259
+ agentId: z.string().min(1).max(128).optional(),
260
+ title: z.string().max(256).optional(),
261
+ model: z.string().max(128).optional(),
262
+ message: streamChatMessageInput
263
+ })
264
+ .strict()
265
+
266
+ export const openaiMessagePartSchema = z
267
+ .object({
268
+ type: z.string(),
269
+ text: z.string().optional(),
270
+ image_url: z.object({ url: z.string().optional() }).optional(),
271
+ file: z.object({ file_data: z.string().optional(), filename: z.string().optional() }).optional()
272
+ })
273
+ .passthrough()
274
+
275
+ export const openaiMessageSchema = z
276
+ .object({
277
+ role: z.string(),
278
+ content: z.union([z.string(), z.array(openaiMessagePartSchema), z.null()]).optional()
279
+ })
280
+ .passthrough()
281
+
282
+ export const openaiCompletionsBody = z
283
+ .object({
284
+ model: z.string(),
285
+ messages: z.array(openaiMessageSchema).min(1).max(500),
286
+ stream: z.boolean().optional(),
287
+ stream_options: z.object({ include_usage: z.boolean().optional() }).optional()
288
+ })
289
+ .extend({ model: z.string().optional() })
290
+ .passthrough()
291
+
292
+ export const webSearchInput = z.object({
293
+ query: z.string().min(1).optional(),
294
+ url: z.string().optional().describe('Full URL to open and read — wins over query')
295
+ })
@@ -0,0 +1,360 @@
1
+ import {
2
+ stepCountIs,
3
+ streamText,
4
+ tool as aiTool,
5
+ type ModelMessage,
6
+ type SystemModelMessage,
7
+ type ToolSet
8
+ } from 'ai'
9
+ import { registerAttachmentTypes } from '../attachments'
10
+ import { CORE_CONFIG } from '../config'
11
+ import type { EntryId } from '../db'
12
+ import { Core } from '../index'
13
+ import { ChatContextEngine } from './context'
14
+ import {
15
+ ReasoningExpertInputDto,
16
+ ReasoningExpertOutputDto,
17
+ webSearchInput,
18
+ type AgentConfig,
19
+ type AskAgentOptions,
20
+ type ChatMessagePublic,
21
+ type ContextEntry,
22
+ type ContextItemSpec,
23
+ type GetContextOptions,
24
+ type HybridRetrievalOptions,
25
+ type HybridRetrievalResult,
26
+ type ListChatMessagesOptions,
27
+ type RunAgentOptions,
28
+ type VerifiedAgentToolContext,
29
+ type WorkflowToolSpec
30
+ } from './schema'
31
+ import { ChatStreamEngine } from './stream'
32
+ import {
33
+ buildDynamicContextBlock,
34
+ extractLatestUserPrompt,
35
+ FORBIDDEN_TOOL_SCHEMA_KEYS,
36
+ isSafePublicUrl,
37
+ sanitizeToolInputSchema
38
+ } from './utils'
39
+
40
+ const toolRegistry = new Map<string, WorkflowToolSpec>()
41
+ const agentRegistry = new Map<string, AgentConfig>()
42
+
43
+ export class ChatService {
44
+ readonly #core: Core
45
+ readonly #contextEngine: ChatContextEngine
46
+ readonly #streamEngine: ChatStreamEngine
47
+
48
+ constructor(coreClient: Core) {
49
+ this.#core = coreClient
50
+ this.#contextEngine = new ChatContextEngine(coreClient)
51
+ this.#streamEngine = new ChatStreamEngine(coreClient, agentRegistry)
52
+ }
53
+
54
+ #registerWorkflowTool = (name: string, spec: WorkflowToolSpec): void => {
55
+ toolRegistry.set(name, {
56
+ ...spec,
57
+ mcp: spec.mcp === true,
58
+ tool: spec.tool !== false,
59
+ input: sanitizeToolInputSchema(spec.input)
60
+ })
61
+ }
62
+
63
+ #clearWorkflowTools = (): void => {
64
+ toolRegistry.clear()
65
+ }
66
+
67
+ #getMcpTools = (): Map<string, WorkflowToolSpec> =>
68
+ new Map([...toolRegistry.entries()].filter(([, spec]) => spec.mcp === true))
69
+
70
+ #registerAgents = (defs: Record<string, AgentConfig>, reset = false): void => {
71
+ if (reset) {
72
+ agentRegistry.clear()
73
+ registerAttachmentTypes(undefined, true)
74
+ }
75
+ for (const [id, cfg] of Object.entries(defs)) {
76
+ agentRegistry.set(id, { ...cfg, name: cfg.name ?? id })
77
+ if (cfg.attachmentTypes) registerAttachmentTypes(cfg.attachmentTypes)
78
+ }
79
+ }
80
+
81
+ #getRegisteredAgents = (): Record<string, AgentConfig> => Object.fromEntries(agentRegistry)
82
+
83
+ #getAgentRegistry = (): Map<string, AgentConfig> => agentRegistry
84
+
85
+ getContext = (
86
+ prompt: string,
87
+ catalog?: readonly ContextItemSpec[],
88
+ options?: GetContextOptions
89
+ ): Promise<ContextEntry[]> => this.#contextEngine.getContext(prompt, catalog, options)
90
+
91
+ retrieveContext = (opts: HybridRetrievalOptions): Promise<HybridRetrievalResult> =>
92
+ this.#contextEngine.retrieveContext(opts)
93
+
94
+ messages = (chatId: EntryId, opts?: ListChatMessagesOptions): Promise<ChatMessagePublic[]> =>
95
+ this.#streamEngine.messages(chatId, opts)
96
+
97
+ cancel = (chatId: EntryId): Promise<{ chatId: string; cancelled: true }> =>
98
+ this.#streamEngine.cancel(chatId)
99
+
100
+ #webSearchTool = (abortSignal?: AbortSignal) =>
101
+ aiTool({
102
+ description: 'Search the web (DuckDuckGo) or open a URL and read it as markdown. Use for current information.',
103
+ inputSchema: webSearchInput,
104
+ execute: async ({ query, url }) => {
105
+ const useUrl = isSafePublicUrl(url)
106
+ const target = useUrl ? url! : `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query ?? url ?? '')}`
107
+ const maxChars = useUrl ? 6000 : 4000
108
+ try {
109
+ const timeoutSignal = AbortSignal.timeout(20_000)
110
+ const signal = abortSignal ? AbortSignal.any([abortSignal, timeoutSignal]) : timeoutSignal
111
+ const jinaKey = this.#core.env.JINA_API_KEY
112
+ const res = await fetch(`https://r.jina.ai/${target}`, {
113
+ headers: {
114
+ Accept: 'text/plain, text/markdown;q=0.9, */*;q=0.8',
115
+ ...(jinaKey ? { Authorization: `Bearer ${jinaKey}` } : {})
116
+ },
117
+ signal
118
+ })
119
+ if (!res.ok) return { text: `Web ${useUrl ? 'page' : 'search'} failed (HTTP ${res.status}).` }
120
+ const reader = res.body?.getReader()
121
+ if (!reader) return { text: '' }
122
+ const decoder = new TextDecoder()
123
+ let text = ''
124
+ while (text.length < maxChars + 256) {
125
+ const { done, value } = await reader.read()
126
+ if (done) break
127
+ text += decoder.decode(value, { stream: true })
128
+ }
129
+ await reader.cancel().catch(() => {})
130
+ return { text: text.slice(0, maxChars).replace(/\s+\S*$/, '') }
131
+ } catch {
132
+ return { text: 'Web search failed.' }
133
+ }
134
+ }
135
+ })
136
+
137
+ #createReasoningExpertTool = (ctx: VerifiedAgentToolContext) =>
138
+ aiTool({
139
+ description:
140
+ 'Delegate complex architectural, legal, financial, or multi-constraint reasoning problems to a frontier reasoning model. Use only when standard analysis is insufficient.',
141
+ inputSchema: ReasoningExpertInputDto,
142
+ execute: async ({ problemStatement, constraintsAndContext }) => {
143
+ const result = await ctx.core.ai.generate(
144
+ `Problem:\n${problemStatement}\n\nConstraints & Context:\n${constraintsAndContext}`,
145
+ {
146
+ schema: ReasoningExpertOutputDto,
147
+ model: CORE_CONFIG.models.vertexExpert ?? 'claude-sonnet-4-5',
148
+ signal: ctx.abortSignal
149
+ }
150
+ )
151
+ return result.output
152
+ }
153
+ })
154
+
155
+ #buildAgentToolsForTurn = (toolKeys: string[] = [], ctx: VerifiedAgentToolContext): ToolSet => {
156
+ const tools: ToolSet = {}
157
+ const { core: turnCore, abortSignal } = ctx
158
+ const coreState = {
159
+ env: turnCore.env,
160
+ userId: turnCore.userId,
161
+ role: turnCore.role,
162
+ step: turnCore.step
163
+ }
164
+
165
+ for (const name of toolKeys) {
166
+ if (name === 'web_search') {
167
+ tools[name] = this.#webSearchTool(abortSignal)
168
+ } else if (name === 'consult_reasoning_expert') {
169
+ tools[name] = this.#createReasoningExpertTool(ctx)
170
+ } else {
171
+ const wf = toolRegistry.get(name)
172
+ if (wf && wf.tool !== false) {
173
+ tools[name] = aiTool({
174
+ description: wf.description,
175
+ inputSchema: sanitizeToolInputSchema(wf.input),
176
+ execute: (rawInput) =>
177
+ this.#core.internals.context.run(coreState, async () => {
178
+ const safeInput =
179
+ rawInput && typeof rawInput === 'object'
180
+ ? Object.fromEntries(
181
+ Object.entries(rawInput as Record<string, unknown>).filter(
182
+ ([k]) => !FORBIDDEN_TOOL_SCHEMA_KEYS.has(k)
183
+ )
184
+ )
185
+ : rawInput
186
+ return wf.execute(safeInput, turnCore.userId)
187
+ })
188
+ })
189
+ }
190
+ }
191
+ }
192
+ return tools
193
+ }
194
+
195
+ #resolveStaticSystemPrompt = async (agent: AgentConfig): Promise<string> => {
196
+ const raw =
197
+ typeof agent.systemPrompt === 'function'
198
+ ? await agent.systemPrompt({
199
+ today: 'see dynamic context',
200
+ userId: 'authenticated-user',
201
+ env: this.#core.env
202
+ })
203
+ : agent.systemPrompt
204
+ return raw
205
+ .replace(/\s*Today is \{\{today\}\}\.?/gi, '')
206
+ .replace(/\s*Current userId is \{\{userId\}\}\.?/gi, '')
207
+ .replaceAll('{{today}}', 'see dynamic context')
208
+ .replaceAll('{{userId}}', 'authenticated-user')
209
+ .trim()
210
+ }
211
+
212
+ #resolveStepActiveTools = (agent: AgentConfig, allToolNames: string[], stepNumber: number): string[] => {
213
+ if (allToolNames.length === 0) return []
214
+ const presets = agent.toolPresets
215
+ if (!presets) return allToolNames
216
+ if (stepNumber === 0 && presets.discovery?.length && presets.workflow?.length) {
217
+ return [...new Set([...presets.discovery, ...presets.workflow])].filter((k) => allToolNames.includes(k))
218
+ }
219
+ if (stepNumber > 0 && presets.workflow?.length) {
220
+ return presets.workflow.filter((k) => allToolNames.includes(k))
221
+ }
222
+ return allToolNames
223
+ }
224
+
225
+ stream = async (
226
+ agent: AgentConfig,
227
+ messages: ModelMessage[],
228
+ userId: EntryId = this.#core.userId,
229
+ opts?: RunAgentOptions
230
+ ): Promise<ReturnType<typeof streamText>> => {
231
+ await this.#core.internals.payments.enforceAiQuota(userId)
232
+ const [, staticCoreAgentInstructions] = await Promise.all([
233
+ this.#core.internals.resilience.acquireLimit('ai'),
234
+ this.#resolveStaticSystemPrompt(agent)
235
+ ])
236
+
237
+ const role = opts?.core?.role ?? this.#core.internals.auth.resolveUserRole(userId, this.#core.role)
238
+ const verifiedCore = opts?.core ?? new Core({ env: this.#core.env, userId, role })
239
+ const tools = this.#buildAgentToolsForTurn(agent.tools ?? [], {
240
+ core: verifiedCore,
241
+ chatId: opts?.chatId,
242
+ abortSignal: opts?.abortSignal
243
+ })
244
+ const allToolNames = Object.keys(tools)
245
+
246
+ const dynamicBlock = buildDynamicContextBlock({
247
+ currentTimeIso: opts?.dynamicContext?.currentTimeIso ?? new Date().toISOString(),
248
+ recalledMemories: opts?.dynamicContext?.recalledMemories,
249
+ infobaseContextEntries: opts?.dynamicContext?.infobaseContextEntries,
250
+ stagedAttachmentSummaries: opts?.dynamicContext?.stagedAttachmentSummaries,
251
+ userContext: opts?.dynamicContext?.userContext
252
+ })
253
+
254
+ const systemInstructions: SystemModelMessage[] = [
255
+ {
256
+ role: 'system',
257
+ content: staticCoreAgentInstructions,
258
+ providerOptions: {
259
+ anthropic: { cacheControl: { type: 'ephemeral' } },
260
+ vertex: { cacheControl: { type: 'ephemeral' } }
261
+ }
262
+ },
263
+ {
264
+ role: 'system',
265
+ content: dynamicBlock
266
+ }
267
+ ]
268
+
269
+ const messagesForLlm: ModelMessage[] = messages.filter((m) => m.role !== 'system')
270
+ const selectedModel = opts?.modelOverride || agent.model || CORE_CONFIG.models.vertex
271
+
272
+ return streamText({
273
+ model: this.#core.ai.languageModel(selectedModel),
274
+ system: systemInstructions,
275
+ messages: messagesForLlm,
276
+ tools,
277
+ ...(allToolNames.length > 0
278
+ ? {
279
+ activeTools: allToolNames,
280
+ prepareStep: ({ stepNumber, messages: stepMessages }) => ({
281
+ activeTools: this.#resolveStepActiveTools(agent, allToolNames, stepNumber),
282
+ ...(stepNumber > 2 && stepMessages.length > CORE_CONFIG.limits.chatMessagesLimit
283
+ ? { messages: stepMessages.slice(-CORE_CONFIG.limits.chatMessagesLimit) }
284
+ : {})
285
+ })
286
+ }
287
+ : {}),
288
+ stopWhen: stepCountIs(agent.maxSteps ?? CORE_CONFIG.models.defaultAgentMaxSteps),
289
+ ...(opts?.abortSignal ? { abortSignal: opts.abortSignal } : {}),
290
+ onFinish: async (event) => {
291
+ const usageObj = (event as { totalUsage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number }; usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number } }).totalUsage ?? event.usage
292
+ await this.#core.internals.payments.trackAiUsage({
293
+ userId,
294
+ model: selectedModel,
295
+ inputTokens: usageObj?.inputTokens ?? 0,
296
+ outputTokens: usageObj?.outputTokens ?? 0,
297
+ ...(typeof usageObj?.totalTokens === 'number' ? { totalTokens: usageObj.totalTokens } : {}),
298
+ source: 'chat'
299
+ })
300
+ await opts?.onFinish?.(event)
301
+ }
302
+ })
303
+ }
304
+
305
+ ask = async (
306
+ agentOrId: string | AgentConfig,
307
+ prompt: string | ModelMessage[],
308
+ options?: AskAgentOptions
309
+ ): Promise<{ text: string; messages: ModelMessage[] }> => {
310
+ const agent = typeof agentOrId === 'string' ? agentRegistry.get(agentOrId) : agentOrId
311
+ if (!agent) return this.#core.errors.httpError(404, `Unknown agent: ${String(agentOrId)}`, 'NOT_FOUND')
312
+
313
+ const userId = options?.userId ?? this.#core.userId ?? CORE_CONFIG.auth.adminUserId
314
+ const rawMessages: ModelMessage[] =
315
+ typeof prompt === 'string' ? [{ role: 'user', content: [{ type: 'text', text: prompt }] }] : [...prompt]
316
+ const latestPrompt = typeof prompt === 'string' ? prompt : extractLatestUserPrompt(rawMessages)
317
+
318
+ const hybrid =
319
+ options?.includeContext !== false
320
+ ? await this.retrieveContext({
321
+ userId,
322
+ prompt: latestPrompt,
323
+ useContext: agent.useContext !== false,
324
+ agent,
325
+ abortSignal: options?.abortSignal
326
+ })
327
+ : { recalledMemories: [], infobaseContextEntries: [], userContext: undefined }
328
+
329
+ const turn = await this.stream(agent, rawMessages.slice(-15), userId, {
330
+ abortSignal: options?.abortSignal,
331
+ dynamicContext: {
332
+ recalledMemories: hybrid.recalledMemories,
333
+ infobaseContextEntries: hybrid.infobaseContextEntries,
334
+ userContext: hybrid.userContext
335
+ }
336
+ })
337
+ const [text, res] = await Promise.all([turn.text, turn.response])
338
+ return {
339
+ text,
340
+ messages: [...rawMessages, ...(res.messages as unknown as ModelMessage[])]
341
+ }
342
+ }
343
+
344
+ get internals() {
345
+ return {
346
+ registerWorkflowTool: this.#registerWorkflowTool,
347
+ clearWorkflowTools: this.#clearWorkflowTools,
348
+ getMcpTools: this.#getMcpTools,
349
+ registerAgents: this.#registerAgents,
350
+ getRegisteredAgents: this.#getRegisteredAgents,
351
+ getAgentRegistry: this.#getAgentRegistry,
352
+ registerContextCatalog: this.#contextEngine.registerContextCatalog,
353
+ enforceAgentAccess: this.#streamEngine.enforceAgentAccess,
354
+ stageAndScanAttachments: this.#streamEngine.stageAndScanAttachments,
355
+ startChatStreamTurn: this.#streamEngine.startChatStreamTurn,
356
+ saveChat: this.#streamEngine.saveChat,
357
+ streamChatSse: this.#streamEngine.streamChatSse
358
+ }
359
+ }
360
+ }