@lota-sdk/core 0.1.5

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 (153) hide show
  1. package/infrastructure/schema/00_workstream.surql +55 -0
  2. package/infrastructure/schema/01_memory.surql +47 -0
  3. package/infrastructure/schema/02_execution_plan.surql +62 -0
  4. package/infrastructure/schema/03_learned_skill.surql +32 -0
  5. package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
  6. package/package.json +128 -0
  7. package/src/ai/definitions.ts +308 -0
  8. package/src/bifrost/bifrost.ts +256 -0
  9. package/src/config/agent-defaults.ts +99 -0
  10. package/src/config/constants.ts +33 -0
  11. package/src/config/env-shapes.ts +122 -0
  12. package/src/config/logger.ts +29 -0
  13. package/src/config/model-constants.ts +31 -0
  14. package/src/config/search.ts +17 -0
  15. package/src/config/workstream-defaults.ts +68 -0
  16. package/src/db/base.service.ts +55 -0
  17. package/src/db/cursor-pagination.ts +73 -0
  18. package/src/db/memory-query-builder.ts +207 -0
  19. package/src/db/memory-store.helpers.ts +118 -0
  20. package/src/db/memory-store.rows.ts +29 -0
  21. package/src/db/memory-store.ts +974 -0
  22. package/src/db/memory-types.ts +193 -0
  23. package/src/db/memory.ts +505 -0
  24. package/src/db/record-id.ts +78 -0
  25. package/src/db/service.ts +932 -0
  26. package/src/db/startup.ts +152 -0
  27. package/src/db/tables.ts +20 -0
  28. package/src/document/org-document-chunking.ts +224 -0
  29. package/src/document/parsing.ts +40 -0
  30. package/src/embeddings/provider.ts +76 -0
  31. package/src/index.ts +302 -0
  32. package/src/queues/context-compaction.queue.ts +82 -0
  33. package/src/queues/document-processor.queue.ts +118 -0
  34. package/src/queues/memory-consolidation.queue.ts +65 -0
  35. package/src/queues/post-chat-memory.queue.ts +128 -0
  36. package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
  37. package/src/queues/regular-chat-memory-digest.config.ts +12 -0
  38. package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
  39. package/src/queues/skill-extraction.config.ts +9 -0
  40. package/src/queues/skill-extraction.queue.ts +62 -0
  41. package/src/redis/connection.ts +176 -0
  42. package/src/redis/index.ts +30 -0
  43. package/src/redis/org-memory-lock.ts +43 -0
  44. package/src/redis/redis-lease-lock.ts +158 -0
  45. package/src/runtime/agent-contract.ts +1 -0
  46. package/src/runtime/agent-prompt-context.ts +119 -0
  47. package/src/runtime/agent-runtime-policy.ts +192 -0
  48. package/src/runtime/agent-stream-helpers.ts +117 -0
  49. package/src/runtime/agent-types.ts +22 -0
  50. package/src/runtime/approval-continuation.ts +16 -0
  51. package/src/runtime/chat-attachments.ts +46 -0
  52. package/src/runtime/chat-message.ts +10 -0
  53. package/src/runtime/chat-request-routing.ts +21 -0
  54. package/src/runtime/chat-run-orchestration.ts +25 -0
  55. package/src/runtime/chat-run-registry.ts +20 -0
  56. package/src/runtime/chat-types.ts +18 -0
  57. package/src/runtime/context-compaction-constants.ts +11 -0
  58. package/src/runtime/context-compaction-runtime.ts +86 -0
  59. package/src/runtime/context-compaction.ts +909 -0
  60. package/src/runtime/execution-plan.ts +59 -0
  61. package/src/runtime/helper-model.ts +405 -0
  62. package/src/runtime/indexed-repositories-policy.ts +28 -0
  63. package/src/runtime/instruction-sections.ts +8 -0
  64. package/src/runtime/llm-content.ts +71 -0
  65. package/src/runtime/memory-block.ts +264 -0
  66. package/src/runtime/memory-digest-policy.ts +14 -0
  67. package/src/runtime/memory-format.ts +8 -0
  68. package/src/runtime/memory-pipeline.ts +570 -0
  69. package/src/runtime/memory-prompts-fact.ts +47 -0
  70. package/src/runtime/memory-prompts-parse.ts +3 -0
  71. package/src/runtime/memory-prompts-update.ts +37 -0
  72. package/src/runtime/memory-scope.ts +43 -0
  73. package/src/runtime/plugin-types.ts +10 -0
  74. package/src/runtime/retrieval-adapters.ts +25 -0
  75. package/src/runtime/retrieval-pipeline.ts +3 -0
  76. package/src/runtime/runtime-extensions.ts +154 -0
  77. package/src/runtime/skill-extraction-policy.ts +3 -0
  78. package/src/runtime/team-consultation-orchestrator.ts +245 -0
  79. package/src/runtime/team-consultation-prompts.ts +32 -0
  80. package/src/runtime/title-helpers.ts +12 -0
  81. package/src/runtime/turn-lifecycle.ts +28 -0
  82. package/src/runtime/workstream-chat-helpers.ts +187 -0
  83. package/src/runtime/workstream-routing-policy.ts +301 -0
  84. package/src/runtime/workstream-state.ts +261 -0
  85. package/src/services/attachment.service.ts +159 -0
  86. package/src/services/chat-attachments.service.ts +17 -0
  87. package/src/services/chat-run-registry.service.ts +3 -0
  88. package/src/services/context-compaction-runtime.ts +13 -0
  89. package/src/services/context-compaction.service.ts +115 -0
  90. package/src/services/document-chunk.service.ts +141 -0
  91. package/src/services/execution-plan.service.ts +890 -0
  92. package/src/services/learned-skill.service.ts +328 -0
  93. package/src/services/memory-assessment.service.ts +43 -0
  94. package/src/services/memory.service.ts +807 -0
  95. package/src/services/memory.utils.ts +84 -0
  96. package/src/services/mutating-approval.service.ts +110 -0
  97. package/src/services/recent-activity-title.service.ts +74 -0
  98. package/src/services/recent-activity.service.ts +397 -0
  99. package/src/services/workstream-change-tracker.service.ts +313 -0
  100. package/src/services/workstream-message.service.ts +283 -0
  101. package/src/services/workstream-title.service.ts +58 -0
  102. package/src/services/workstream-turn-preparation.ts +1340 -0
  103. package/src/services/workstream-turn.ts +37 -0
  104. package/src/services/workstream.service.ts +854 -0
  105. package/src/services/workstream.types.ts +118 -0
  106. package/src/storage/attachment-parser.ts +101 -0
  107. package/src/storage/attachment-storage.service.ts +391 -0
  108. package/src/storage/attachments.types.ts +11 -0
  109. package/src/storage/attachments.utils.ts +58 -0
  110. package/src/storage/generated-document-storage.service.ts +55 -0
  111. package/src/system-agents/agent-result.ts +27 -0
  112. package/src/system-agents/context-compacter.agent.ts +46 -0
  113. package/src/system-agents/delegated-agent-factory.ts +177 -0
  114. package/src/system-agents/helper-agent-options.ts +20 -0
  115. package/src/system-agents/memory-reranker.agent.ts +38 -0
  116. package/src/system-agents/memory.agent.ts +58 -0
  117. package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
  118. package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
  119. package/src/system-agents/researcher.agent.ts +34 -0
  120. package/src/system-agents/skill-extractor.agent.ts +88 -0
  121. package/src/system-agents/skill-manager.agent.ts +80 -0
  122. package/src/system-agents/title-generator.agent.ts +42 -0
  123. package/src/system-agents/workstream-tracker.agent.ts +58 -0
  124. package/src/tools/execution-plan.tool.ts +163 -0
  125. package/src/tools/fetch-webpage.tool.ts +132 -0
  126. package/src/tools/firecrawl-client.ts +12 -0
  127. package/src/tools/memory-block.tool.ts +55 -0
  128. package/src/tools/read-file-parts.tool.ts +80 -0
  129. package/src/tools/remember-memory.tool.ts +85 -0
  130. package/src/tools/research-topic.tool.ts +15 -0
  131. package/src/tools/search-tools.ts +55 -0
  132. package/src/tools/search-web.tool.ts +175 -0
  133. package/src/tools/team-think.tool.ts +125 -0
  134. package/src/tools/tool-contract.ts +21 -0
  135. package/src/tools/user-questions.tool.ts +18 -0
  136. package/src/utils/async.ts +50 -0
  137. package/src/utils/date-time.ts +34 -0
  138. package/src/utils/error.ts +10 -0
  139. package/src/utils/errors.ts +28 -0
  140. package/src/utils/hono-error-handler.ts +71 -0
  141. package/src/utils/string.ts +51 -0
  142. package/src/workers/bootstrap.ts +44 -0
  143. package/src/workers/memory-consolidation.worker.ts +318 -0
  144. package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
  145. package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
  146. package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
  147. package/src/workers/skill-extraction.runner.ts +331 -0
  148. package/src/workers/skill-extraction.worker.ts +22 -0
  149. package/src/workers/utils/repo-indexer-chunker.ts +331 -0
  150. package/src/workers/utils/repo-structure-extractor.ts +645 -0
  151. package/src/workers/utils/repomix-process-concurrency.ts +65 -0
  152. package/src/workers/utils/sandbox-error.ts +5 -0
  153. package/src/workers/worker-utils.ts +182 -0
@@ -0,0 +1,256 @@
1
+ import { createOpenAI } from '@ai-sdk/openai'
2
+ import { wrapLanguageModel } from 'ai'
3
+ import type { LanguageModelMiddleware } from 'ai'
4
+
5
+ import { isRecord, readString } from '../utils/string'
6
+
7
+ type BifrostExtraParams = Record<string, unknown>
8
+ type BifrostChatResponse = { body?: unknown }
9
+ type BifrostTransformParamsOptions = Parameters<NonNullable<LanguageModelMiddleware['transformParams']>>[0]
10
+ type WrapStreamOptions = Parameters<NonNullable<LanguageModelMiddleware['wrapStream']>>[0]
11
+ type BifrostCallOptions = WrapStreamOptions['params']
12
+ type BifrostGenerateResult = Awaited<ReturnType<WrapStreamOptions['doGenerate']>>
13
+ type BifrostStreamResult = Awaited<ReturnType<WrapStreamOptions['doStream']>>
14
+ type BifrostGeneratedContent = BifrostGenerateResult['content'][number]
15
+ type BifrostStreamPart = BifrostStreamResult['stream'] extends ReadableStream<infer T> ? T : never
16
+
17
+ const OPENROUTER_RESPONSE_HEALING_EXTRA_PARAMS = {
18
+ plugins: [{ id: 'response-healing' }],
19
+ } as const satisfies BifrostExtraParams
20
+
21
+ function readRequiredGatewayEnv(name: 'AI_GATEWAY_KEY' | 'AI_GATEWAY_URL'): string {
22
+ const value = process.env[name]?.trim()
23
+ if (value) return value
24
+ throw new Error(`[bifrost] Missing ${name}.`)
25
+ }
26
+
27
+ function readReasoningDetailsText(value: unknown): string | null {
28
+ if (!Array.isArray(value)) return null
29
+
30
+ const textParts = value
31
+ .map((item) => (isRecord(item) ? readString(item.text) : null))
32
+ .filter((item): item is string => item !== null)
33
+
34
+ if (textParts.length === 0) return null
35
+
36
+ return textParts.join('\n\n')
37
+ }
38
+
39
+ function readBifrostChatReasoningText(message: Record<string, unknown>): string | null {
40
+ return (
41
+ readString(message.reasoning) ??
42
+ readString(message.reasoning_content) ??
43
+ readReasoningDetailsText(message.reasoning_details)
44
+ )
45
+ }
46
+
47
+ export function extractBifrostChatReasoningText(responseBody: unknown): string | null {
48
+ if (!isRecord(responseBody) || !Array.isArray(responseBody.choices)) return null
49
+
50
+ for (const choice of responseBody.choices) {
51
+ if (!isRecord(choice) || !isRecord(choice.message)) continue
52
+
53
+ const reasoningText = readBifrostChatReasoningText(choice.message)
54
+ if (reasoningText) return reasoningText
55
+ }
56
+
57
+ return null
58
+ }
59
+
60
+ export function extractBifrostChatReasoningDeltaText(rawChunk: unknown): string | null {
61
+ if (!isRecord(rawChunk) || !Array.isArray(rawChunk.choices)) return null
62
+
63
+ for (const choice of rawChunk.choices) {
64
+ if (!isRecord(choice) || !isRecord(choice.delta)) continue
65
+
66
+ const reasoningText = readBifrostChatReasoningText(choice.delta)
67
+ if (reasoningText) return reasoningText
68
+ }
69
+
70
+ return null
71
+ }
72
+
73
+ export function injectBifrostChatReasoningContent(
74
+ content: readonly BifrostGeneratedContent[],
75
+ response?: BifrostChatResponse,
76
+ ): BifrostGeneratedContent[] {
77
+ if (content.some((part) => part.type === 'reasoning')) {
78
+ return [...content]
79
+ }
80
+
81
+ const reasoningText = extractBifrostChatReasoningText(response?.body)
82
+ if (!reasoningText) return [...content]
83
+
84
+ return [{ type: 'reasoning', text: reasoningText }, ...content]
85
+ }
86
+
87
+ function isReasoningEnabled(params: BifrostCallOptions): boolean {
88
+ if (!isRecord(params.providerOptions) || !isRecord(params.providerOptions.openai)) return false
89
+
90
+ const openaiOptions = params.providerOptions.openai
91
+ if (openaiOptions.forceReasoning === true) return true
92
+ if (typeof openaiOptions.reasoningSummary === 'string' && openaiOptions.reasoningSummary.length > 0) return true
93
+ return typeof openaiOptions.reasoningEffort === 'string' && openaiOptions.reasoningEffort !== 'none'
94
+ }
95
+
96
+ function shouldCloseInjectedReasoning(chunk: BifrostStreamPart): boolean {
97
+ return chunk.type !== 'stream-start' && chunk.type !== 'response-metadata' && chunk.type !== 'raw'
98
+ }
99
+
100
+ export function injectBifrostChatReasoningStream(
101
+ stream: ReadableStream<BifrostStreamPart>,
102
+ ): ReadableStream<BifrostStreamPart> {
103
+ const reasoningId = 'bifrost-reasoning-0'
104
+ let reasoningOpen = false
105
+ let reasoningClosed = false
106
+
107
+ return stream.pipeThrough(
108
+ new TransformStream<BifrostStreamPart, BifrostStreamPart>({
109
+ transform(chunk, controller) {
110
+ const closeReasoning = () => {
111
+ if (!reasoningOpen || reasoningClosed) return
112
+
113
+ controller.enqueue({ type: 'reasoning-end', id: reasoningId } as BifrostStreamPart)
114
+ reasoningClosed = true
115
+ }
116
+
117
+ if (chunk.type === 'raw') {
118
+ const reasoningDelta = reasoningClosed ? null : extractBifrostChatReasoningDeltaText(chunk.rawValue)
119
+ controller.enqueue(chunk)
120
+
121
+ if (reasoningDelta) {
122
+ if (!reasoningOpen) {
123
+ controller.enqueue({ type: 'reasoning-start', id: reasoningId } as BifrostStreamPart)
124
+ reasoningOpen = true
125
+ }
126
+
127
+ controller.enqueue({ type: 'reasoning-delta', id: reasoningId, delta: reasoningDelta } as BifrostStreamPart)
128
+ }
129
+ return
130
+ }
131
+
132
+ if (shouldCloseInjectedReasoning(chunk)) {
133
+ closeReasoning()
134
+ }
135
+
136
+ controller.enqueue(chunk)
137
+ },
138
+ flush(controller) {
139
+ if (!reasoningOpen || reasoningClosed) return
140
+ controller.enqueue({ type: 'reasoning-end', id: reasoningId } as BifrostStreamPart)
141
+ },
142
+ }),
143
+ )
144
+ }
145
+
146
+ function addBifrostReasoningRawChunks(
147
+ params: BifrostCallOptions,
148
+ type: BifrostTransformParamsOptions['type'],
149
+ ): BifrostCallOptions {
150
+ if (type !== 'stream' || !isReasoningEnabled(params) || params.includeRawChunks === true) {
151
+ return params
152
+ }
153
+
154
+ return { ...params, includeRawChunks: true }
155
+ }
156
+
157
+ export function injectBifrostExtraParamsRequestBody(
158
+ body: BodyInit | null | undefined,
159
+ extraParams: BifrostExtraParams,
160
+ ): BodyInit | null | undefined {
161
+ if (typeof body !== 'string') return body
162
+
163
+ let parsed: unknown
164
+ try {
165
+ parsed = JSON.parse(body)
166
+ } catch {
167
+ return body
168
+ }
169
+
170
+ if (!isRecord(parsed)) return body
171
+
172
+ const mergedExtraParams = isRecord(parsed.extra_params)
173
+ ? { ...parsed.extra_params, ...extraParams }
174
+ : { ...extraParams }
175
+
176
+ return JSON.stringify({ ...parsed, extra_params: mergedExtraParams })
177
+ }
178
+
179
+ function createBifrostFetchWithExtraParams(extraParams: BifrostExtraParams): typeof fetch {
180
+ const fetchWithExtraParams = (input: RequestInfo | URL, init?: RequestInit | BunFetchRequestInit) =>
181
+ globalThis.fetch(input, { ...init, body: injectBifrostExtraParamsRequestBody(init?.body, extraParams) })
182
+
183
+ return Object.assign(fetchWithExtraParams, { preconnect: globalThis.fetch.preconnect.bind(globalThis.fetch) })
184
+ }
185
+
186
+ function createBifrostProvider(extraParams?: BifrostExtraParams) {
187
+ const apiKey = readRequiredGatewayEnv('AI_GATEWAY_KEY')
188
+ if (!apiKey.startsWith('sk-bf-')) {
189
+ throw new Error('[bifrost] AI_GATEWAY_KEY must use the Bifrost virtual-key format (sk-bf-*).')
190
+ }
191
+
192
+ return createOpenAI({
193
+ baseURL: readRequiredGatewayEnv('AI_GATEWAY_URL'),
194
+ apiKey,
195
+ headers: { 'x-bf-vk': apiKey, ...(extraParams ? { 'x-bf-passthrough-extra-params': 'true' } : {}) },
196
+ ...(extraParams ? { fetch: createBifrostFetchWithExtraParams(extraParams) } : {}),
197
+ })
198
+ }
199
+
200
+ let provider: ReturnType<typeof createOpenAI> | null = null
201
+ let openRouterResponseHealingProvider: ReturnType<typeof createOpenAI> | null = null
202
+
203
+ export function getBifrostProvider() {
204
+ if (provider) return provider
205
+
206
+ provider = createBifrostProvider()
207
+
208
+ return provider
209
+ }
210
+
211
+ export function getBifrostOpenRouterResponseHealingProvider() {
212
+ if (openRouterResponseHealingProvider) return openRouterResponseHealingProvider
213
+
214
+ openRouterResponseHealingProvider = createBifrostProvider(OPENROUTER_RESPONSE_HEALING_EXTRA_PARAMS)
215
+
216
+ return openRouterResponseHealingProvider
217
+ }
218
+
219
+ export function bifrostModel(modelId: string) {
220
+ return getBifrostProvider()(modelId)
221
+ }
222
+
223
+ export function bifrostOpenRouterResponseHealingModel(modelId: string) {
224
+ return getBifrostOpenRouterResponseHealingProvider()(modelId)
225
+ }
226
+
227
+ export function bifrostChatModel(modelId: string) {
228
+ return wrapLanguageModel({
229
+ model: getBifrostProvider().chat(modelId),
230
+ middleware: {
231
+ specificationVersion: 'v3',
232
+ transformParams: async ({ params, type }) => addBifrostReasoningRawChunks(params, type),
233
+ wrapGenerate: async ({ doGenerate }) => {
234
+ const result = await doGenerate()
235
+
236
+ return {
237
+ ...result,
238
+ content: injectBifrostChatReasoningContent(
239
+ result.content,
240
+ result.response as BifrostChatResponse | undefined,
241
+ ),
242
+ }
243
+ },
244
+ wrapStream: async ({ doStream, params }) => {
245
+ const result = await doStream()
246
+ if (!isReasoningEnabled(params)) return result
247
+
248
+ return { ...result, stream: injectBifrostChatReasoningStream(result.stream) }
249
+ },
250
+ },
251
+ })
252
+ }
253
+
254
+ export function bifrostEmbeddingModel(modelId: string) {
255
+ return getBifrostProvider().embeddingModel(modelId)
256
+ }
@@ -0,0 +1,99 @@
1
+ type LotaAgentFactoryRegistry = Record<string, (...args: unknown[]) => unknown>
2
+
3
+ // Agent configuration — these are defaults that consumers override via createLotaRuntime config
4
+ export let agentDisplayNames: Record<string, string> = {}
5
+ export let agentShortDisplayNames: Record<string, string> = {}
6
+ export let agentRoster: readonly string[] = []
7
+ export let teamConsultParticipants: readonly string[] = []
8
+
9
+ export interface CoreWorkstreamProfile {
10
+ config: { coreType: string; agentId: string; title: string }
11
+ tools: readonly string[]
12
+ skills: readonly string[]
13
+ instructions: string
14
+ }
15
+
16
+ export let getCoreWorkstreamProfile: (coreType: string) => CoreWorkstreamProfile = (_coreType) => ({
17
+ config: { coreType: _coreType, agentId: '', title: '' },
18
+ tools: [],
19
+ skills: [],
20
+ instructions: '',
21
+ })
22
+
23
+ export function configureAgents(config: {
24
+ roster: readonly string[]
25
+ displayNames: Record<string, string>
26
+ shortDisplayNames?: Record<string, string>
27
+ teamConsultParticipants: readonly string[]
28
+ getCoreWorkstreamProfile?: (coreType: string) => CoreWorkstreamProfile
29
+ }): void {
30
+ agentRoster = config.roster
31
+ agentDisplayNames = config.displayNames
32
+ agentShortDisplayNames = config.shortDisplayNames ?? {}
33
+ teamConsultParticipants = config.teamConsultParticipants
34
+ if (config.getCoreWorkstreamProfile) {
35
+ getCoreWorkstreamProfile = config.getCoreWorkstreamProfile
36
+ }
37
+ }
38
+
39
+ export function isAgentName(value: unknown): boolean {
40
+ return typeof value === 'string' && new Set(agentRoster).has(value)
41
+ }
42
+
43
+ export function resolveAgentNameAlias(value: unknown): string | undefined {
44
+ if (typeof value !== 'string') return undefined
45
+ const lowered = value.trim().toLowerCase()
46
+ const aliasMap = new Map<string, string>()
47
+ for (const agent of agentRoster) {
48
+ aliasMap.set(agent.toLowerCase(), agent)
49
+ const displayName = agentDisplayNames[agent]
50
+ if (displayName) aliasMap.set(displayName.toLowerCase(), agent)
51
+ const shortName = agentShortDisplayNames[agent]
52
+ if (shortName) aliasMap.set(shortName.toLowerCase(), agent)
53
+ }
54
+ return aliasMap.get(lowered)
55
+ }
56
+
57
+ export let createAgent: LotaAgentFactoryRegistry = {}
58
+
59
+ export let buildAgentTools: (...args: unknown[]) => unknown = () => ({})
60
+ export let getAgentRuntimeConfig: (...args: unknown[]) => unknown = () => ({})
61
+ export let pluginRuntime: unknown = undefined
62
+
63
+ export function configureAgentFactory(config: {
64
+ createAgent: LotaAgentFactoryRegistry
65
+ buildAgentTools?: (...args: unknown[]) => unknown
66
+ getAgentRuntimeConfig?: (...args: unknown[]) => unknown
67
+ pluginRuntime?: unknown
68
+ }): void {
69
+ createAgent = config.createAgent
70
+ if (config.buildAgentTools) buildAgentTools = config.buildAgentTools
71
+ if (config.getAgentRuntimeConfig) getAgentRuntimeConfig = config.getAgentRuntimeConfig
72
+ if (config.pluginRuntime !== undefined) pluginRuntime = config.pluginRuntime
73
+ }
74
+
75
+ const AGENT_MENTION_REGEX = /(^|[^\w])@([a-z][a-z0-9_-]*)\b/gi
76
+
77
+ export interface AgentMentionMatch {
78
+ agent: string
79
+ mention: string
80
+ index: number
81
+ length: number
82
+ }
83
+
84
+ export function extractAgentMentions(message: string): AgentMentionMatch[] {
85
+ const matches: AgentMentionMatch[] = []
86
+ if (!message.trim()) return matches
87
+
88
+ const regex = new RegExp(AGENT_MENTION_REGEX)
89
+ for (const rawMatch of message.matchAll(regex)) {
90
+ const prefix = rawMatch[1] ?? ''
91
+ const rawAgent = (rawMatch[2] ?? '').toLowerCase()
92
+ if (!isAgentName(rawAgent)) continue
93
+
94
+ const index = rawMatch.index + prefix.length
95
+ matches.push({ agent: rawAgent, mention: `@${rawAgent}`, index, length: rawAgent.length + 1 })
96
+ }
97
+
98
+ return matches
99
+ }
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod'
2
+
3
+ // ============================================================================
4
+ // Memory System Constants
5
+ // ============================================================================
6
+
7
+ export const MEMORY = {
8
+ /** Default number of memory candidates to fetch */
9
+ DEFAULT_CANDIDATE_LIMIT: 12,
10
+ /** Maximum KNN limit for safety */
11
+ MAX_KNN_LIMIT: 100,
12
+ } as const
13
+
14
+ // ============================================================================
15
+ // Validation Helpers
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Validates that a value is a safe integer for KNN queries
20
+ * Throws if validation fails
21
+ */
22
+ export function validateKnnLimit(limit: unknown): number {
23
+ return z.number().int().positive().max(MEMORY.MAX_KNN_LIMIT).parse(limit)
24
+ }
25
+
26
+ /**
27
+ * Creates a KNN query string with validated limit
28
+ * Example: createKnnQuery(10) returns "<|10|>"
29
+ */
30
+ export function createKnnQuery(limit: unknown): string {
31
+ const validatedLimit = validateKnnLimit(limit)
32
+ return `<|${validatedLimit}|>`
33
+ }
@@ -0,0 +1,122 @@
1
+ import type { ZodTypeAny } from 'zod'
2
+ import { z } from 'zod'
3
+
4
+ const logLevelValues = ['trace', 'debug', 'info', 'warning', 'error', 'fatal'] as const
5
+
6
+ export const nodeServerEnvShape = {
7
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
8
+ PORT: z.coerce.number().default(3000),
9
+ MAIN_CLIENT_URL: z.string().min(1).default('http://localhost:5173'),
10
+ } as const satisfies Record<string, ZodTypeAny>
11
+
12
+ export const surrealDbEnvShape = {
13
+ SURREALDB_URL: z.string().min(1, 'SurrealDB URL is required'),
14
+ SURREALDB_USER: z.string().min(1, 'SurrealDB user is required'),
15
+ SURREALDB_PASSWORD: z.string().min(1, 'SurrealDB password is required'),
16
+ SURREALDB_NAMESPACE: z.string().min(1, 'SurrealDB namespace is required'),
17
+ SURREALDB_DATABASE: z.string().min(1, 'SurrealDB database is required'),
18
+ } as const satisfies Record<string, ZodTypeAny>
19
+
20
+ export const betterAuthEnvShape = {
21
+ BETTER_AUTH_SECRET: z.string().min(32, 'Better Auth secret must be at least 32 characters long'),
22
+ } as const satisfies Record<string, ZodTypeAny>
23
+
24
+ export const serverUrlEnvShape = { SERVER_URL: z.string().min(1, 'Server URL is required') } as const satisfies Record<
25
+ string,
26
+ ZodTypeAny
27
+ >
28
+
29
+ export const aiGatewayEnvShape = {
30
+ AI_GATEWAY_URL: z.string().url('AI gateway URL is required'),
31
+ AI_GATEWAY_KEY: z.string().min(1, 'AI gateway key is required'),
32
+ AI_GATEWAY_ADMIN: z.string().min(1).optional(),
33
+ AI_GATEWAY_PASS: z.string().min(1).optional(),
34
+ AI_EMBEDDING_MODEL: z.string().default('openai/text-embedding-3-small'),
35
+ } as const satisfies Record<string, ZodTypeAny>
36
+
37
+ export const firecrawlEnvShape = {
38
+ FIRECRAWL_API_KEY: z
39
+ .string()
40
+ .min(1, 'Firecrawl API key is required')
41
+ .refine((value) => value.startsWith('fc-'), 'Firecrawl API key must start with fc-')
42
+ .refine((value) => value !== 'dev-fire-key', 'Firecrawl API key placeholder is not allowed'),
43
+ FIRECRAWL_API_BASE_URL: z.string().url().optional(),
44
+ } as const satisfies Record<string, ZodTypeAny>
45
+
46
+ export const memorySearchEnvShape = {
47
+ MEMORY_SEARCH_K: z.coerce.number().int().positive().default(6),
48
+ } as const satisfies Record<string, ZodTypeAny>
49
+
50
+ export const redisEnvShape = { REDIS_URL: z.string().min(1, 'Redis URL is required') } as const satisfies Record<
51
+ string,
52
+ ZodTypeAny
53
+ >
54
+
55
+ export const s3StorageEnvShape = {
56
+ S3_ENDPOINT: z.string().min(1, 'S3 endpoint is required'),
57
+ S3_BUCKET: z.string().min(1, 'S3 bucket is required'),
58
+ S3_REGION: z.string().default('garage'),
59
+ S3_ACCESS_KEY_ID: z.string().min(1, 'S3 access key is required'),
60
+ S3_SECRET_ACCESS_KEY: z.string().min(1, 'S3 secret access key is required'),
61
+ ATTACHMENT_URL_EXPIRES_IN: z.coerce.number().positive('Attachment URL expiry must be positive').default(1800),
62
+ } as const satisfies Record<string, ZodTypeAny>
63
+
64
+ export const loggingEnvShape = { LOG_LEVEL: z.enum(logLevelValues).default('info') } as const satisfies Record<
65
+ string,
66
+ ZodTypeAny
67
+ >
68
+
69
+ export const githubAppEnvShape = {
70
+ GITHUB_APP_ID: z.string().optional(),
71
+ GITHUB_PRIVATE_KEY: z.string().optional(),
72
+ GITHUB_APP_SLUG: z.string().optional(),
73
+ } as const satisfies Record<string, ZodTypeAny>
74
+
75
+ export const linearOauthEnvShape = {
76
+ LINEAR_CLIENT_ID: z.string().optional(),
77
+ LINEAR_CLIENT_SECRET: z.string().optional(),
78
+ LINEAR_REDIRECT_URI: z.string().optional(),
79
+ } as const satisfies Record<string, ZodTypeAny>
80
+
81
+ export const marketingStudioEnvShape = {
82
+ MARKETING_STUDIO_BASE_URL: z.string().url().optional(),
83
+ MARKETING_STUDIO_ADMIN_API_KEY: z.string().min(1).optional(),
84
+ MARKETING_STUDIO_WEBHOOK_SIGNING_SECRET: z.string().min(1).optional(),
85
+ } as const satisfies Record<string, ZodTypeAny>
86
+
87
+ export function envKeys<TShape extends Record<string, ZodTypeAny>>(shape: TShape) {
88
+ return Object.freeze(Object.keys(shape)) as readonly Extract<keyof TShape, string>[]
89
+ }
90
+
91
+ const lotaSdkEnvSchema = z.object({
92
+ ...aiGatewayEnvShape,
93
+ ...memorySearchEnvShape,
94
+ ...redisEnvShape,
95
+ ...loggingEnvShape,
96
+ ...s3StorageEnvShape,
97
+ ...firecrawlEnvShape,
98
+ })
99
+
100
+ export const lotaSdkEnvKeys = Object.freeze([
101
+ ...envKeys(aiGatewayEnvShape),
102
+ ...envKeys(memorySearchEnvShape),
103
+ ...envKeys(redisEnvShape),
104
+ ...envKeys(loggingEnvShape),
105
+ ...envKeys(s3StorageEnvShape),
106
+ ...envKeys(firecrawlEnvShape),
107
+ ]) as readonly string[]
108
+
109
+ type LotaSdkEnv = z.infer<typeof lotaSdkEnvSchema>
110
+
111
+ let _env: LotaSdkEnv | undefined
112
+
113
+ export function setEnv(value: LotaSdkEnv): void {
114
+ _env = value
115
+ }
116
+
117
+ export const env: LotaSdkEnv = new Proxy({} as LotaSdkEnv, {
118
+ get(_target, prop: string) {
119
+ if (!_env) throw new Error(`lota-sdk env not configured. Call setEnv() before accessing env.${prop}`)
120
+ return (_env as Record<string, unknown>)[prop]
121
+ },
122
+ })
@@ -0,0 +1,29 @@
1
+ import type { LogLevel } from '@logtape/logtape'
2
+ import { configure, getAnsiColorFormatter, getConsoleSink, getLogger as getLogTapeLogger } from '@logtape/logtape'
3
+
4
+ export async function configureLotaLogger(logLevel: LogLevel): Promise<void> {
5
+ const formatter = getAnsiColorFormatter({ level: 'FULL' })
6
+
7
+ await configure({
8
+ reset: true,
9
+ sinks: { console: getConsoleSink({ formatter }) },
10
+ loggers: [
11
+ { category: ['logtape', 'meta'], lowestLevel: 'warning', sinks: ['console'] },
12
+ { category: ['server'], lowestLevel: logLevel, sinks: ['console'] },
13
+ { category: ['lota-sdk'], lowestLevel: logLevel, sinks: ['console'] },
14
+ { category: ['hono'], lowestLevel: logLevel, sinks: ['console'] },
15
+ ],
16
+ })
17
+ }
18
+
19
+ export function getLogger(category: readonly string[]) {
20
+ return getLogTapeLogger([...category])
21
+ }
22
+
23
+ export async function configureLogger(logLevel?: LogLevel): Promise<void> {
24
+ await configureLotaLogger(logLevel ?? 'info')
25
+ }
26
+
27
+ export const serverLogger = getLogger(['lota-sdk'])
28
+ export const chatLogger = getLogger(['lota-sdk', 'chat'])
29
+ export const aiLogger = getLogger(['lota-sdk', 'ai'])
@@ -0,0 +1,31 @@
1
+ export const OPENAI_REASONING_MODEL_ID = 'openai/gpt-5.4' as const
2
+
3
+ export const OPENROUTER_TEAM_AGENT_MODEL_ID = 'openrouter/google/gemini-3.1-pro-preview' as const
4
+ export const OPENROUTER_STRUCTURED_HELPER_MODEL_ID = 'openrouter/google/gemini-3-flash-preview:exacto' as const
5
+ export const OPENROUTER_DELEGATED_REASONING_MODEL_ID = 'openrouter/google/gemini-3-flash-preview:exacto' as const
6
+ export const OPENROUTER_WEB_RESEARCH_MODEL_ID = 'openrouter/stepfun/step-3.5-flash' as const
7
+ export const OPENROUTER_ARTIFACT_GENERATOR_MODEL_ID = 'openrouter/qwen/qwen3.5-flash-02-23' as const
8
+ export const OPENROUTER_REPO_INDEXER_MODEL_ID = 'openrouter/qwen/qwen3.5-flash-02-23:nitro' as const
9
+ export const OPENROUTER_CODE_ANALYSIS_MODEL_ID = 'openrouter/xiaomi/mimo-v2-flash' as const
10
+ export const OPENROUTER_FAST_REASONING_MODEL_ID = 'openrouter/openai/gpt-oss-120b:nitro' as const
11
+ export const OPENROUTER_STRUCTURED_REASONING_MODEL_ID = 'openrouter/openai/gpt-oss-120b:exacto' as const
12
+
13
+ export const OPENAI_HIGH_REASONING_PROVIDER_OPTIONS = {
14
+ openai: { forceReasoning: true, reasoningEffort: 'high', reasoningSummary: 'auto' },
15
+ } as const
16
+
17
+ export const OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS = {
18
+ openai: { forceReasoning: true, reasoningEffort: 'high', reasoningSummary: 'auto' },
19
+ } as const
20
+
21
+ export const OPENROUTER_XHIGH_REASONING_PROVIDER_OPTIONS = {
22
+ openai: { forceReasoning: true, reasoningEffort: 'xhigh', reasoningSummary: 'auto' },
23
+ } as const
24
+
25
+ export const OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS = {
26
+ openai: { forceReasoning: true, reasoningEffort: 'low', reasoningSummary: 'auto' },
27
+ } as const
28
+
29
+ export const OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS = {
30
+ openai: { forceReasoning: true, reasoningEffort: 'minimal', reasoningSummary: 'auto' },
31
+ } as const
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Search-related constants for vector search and hybrid search
3
+ */
4
+
5
+ /**
6
+ * Multiplier for vector search fetch limit.
7
+ * We fetch more candidates than needed (limit * multiplier) to allow for:
8
+ * - RRF (Reciprocal Rank Fusion) re-ranking
9
+ * - Hybrid search combining vector + full-text results
10
+ * - Filtering out low-quality matches before returning final results
11
+ */
12
+ export const VECTOR_SEARCH_OVERFETCH_MULTIPLIER = 2
13
+
14
+ /**
15
+ * Default limit for memory search queries
16
+ */
17
+ export const DEFAULT_MEMORY_SEARCH_LIMIT = 10
@@ -0,0 +1,68 @@
1
+ export interface WorkstreamBootstrapWelcomeConfig {
2
+ directAgentId: string
3
+ buildMessageText: (params: { userName?: string | null }) => string
4
+ }
5
+
6
+ export interface LotaWorkstreamBootstrapConfig {
7
+ onboardingDirectAgents?: readonly string[]
8
+ completedDirectAgents?: readonly string[]
9
+ coreTypesAfterOnboarding?: readonly string[]
10
+ ensureDefaultGroupOnCompleted?: boolean
11
+ onboardingWelcome?: WorkstreamBootstrapWelcomeConfig
12
+ }
13
+
14
+ export interface LotaWorkstreamConfig {
15
+ bootstrap?: LotaWorkstreamBootstrapConfig
16
+ }
17
+
18
+ interface ResolvedWorkstreamBootstrapConfig {
19
+ onboardingDirectAgents: readonly string[]
20
+ completedDirectAgents: readonly string[]
21
+ coreTypesAfterOnboarding: readonly string[]
22
+ ensureDefaultGroupOnCompleted: boolean
23
+ onboardingWelcome?: WorkstreamBootstrapWelcomeConfig
24
+ }
25
+
26
+ const DEFAULT_WORKSTREAM_BOOTSTRAP_CONFIG: ResolvedWorkstreamBootstrapConfig = {
27
+ onboardingDirectAgents: [],
28
+ completedDirectAgents: [],
29
+ coreTypesAfterOnboarding: [],
30
+ ensureDefaultGroupOnCompleted: true,
31
+ }
32
+
33
+ let resolvedWorkstreamBootstrapConfig: ResolvedWorkstreamBootstrapConfig = DEFAULT_WORKSTREAM_BOOTSTRAP_CONFIG
34
+
35
+ function withDedupedStrings(values: readonly string[]): string[] {
36
+ const seen = new Set<string>()
37
+ const deduped: string[] = []
38
+
39
+ for (const value of values) {
40
+ const normalized = value.trim()
41
+ if (!normalized || seen.has(normalized)) continue
42
+ seen.add(normalized)
43
+ deduped.push(normalized)
44
+ }
45
+
46
+ return deduped
47
+ }
48
+
49
+ export function configureWorkstreams(params: { agentRoster: readonly string[]; config?: LotaWorkstreamConfig }): void {
50
+ const bootstrap = params.config?.bootstrap
51
+ const onboardingWelcome = bootstrap?.onboardingWelcome
52
+ const onboardingDirectAgents = withDedupedStrings([
53
+ ...(bootstrap?.onboardingDirectAgents ?? params.agentRoster),
54
+ ...(onboardingWelcome ? [onboardingWelcome.directAgentId] : []),
55
+ ])
56
+
57
+ resolvedWorkstreamBootstrapConfig = {
58
+ onboardingDirectAgents,
59
+ completedDirectAgents: withDedupedStrings(bootstrap?.completedDirectAgents ?? params.agentRoster),
60
+ coreTypesAfterOnboarding: withDedupedStrings(bootstrap?.coreTypesAfterOnboarding ?? []),
61
+ ensureDefaultGroupOnCompleted: bootstrap?.ensureDefaultGroupOnCompleted ?? true,
62
+ ...(onboardingWelcome ? { onboardingWelcome } : {}),
63
+ }
64
+ }
65
+
66
+ export function getWorkstreamBootstrapConfig(): ResolvedWorkstreamBootstrapConfig {
67
+ return resolvedWorkstreamBootstrapConfig
68
+ }