@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,119 @@
1
+ import { toOptionalIsoDateTimeString } from '../utils/date-time'
2
+
3
+ type StructuredProfile = Record<string, unknown>
4
+
5
+ interface AgentPromptContext {
6
+ systemWorkspaceDetails: string
7
+ }
8
+
9
+ interface DomainEventLike {
10
+ actor?: string
11
+ action?: string
12
+ category?: string
13
+ significance?: string
14
+ summary?: string
15
+ createdAt?: unknown
16
+ }
17
+
18
+ interface BuildAgentPromptContextParams {
19
+ workspaceName?: string
20
+ summaryBlock?: string
21
+ structuredProfile?: StructuredProfile
22
+ promptSummary?: string
23
+ userName?: string | null
24
+ recentDomainEvents: DomainEventLike[]
25
+ }
26
+
27
+ function normalizeSummaryBlock(block?: string): string {
28
+ const normalized = block?.trim()
29
+ return normalized && normalized.length > 0 ? normalized : 'No workspace profile summary has been recorded yet.'
30
+ }
31
+
32
+ function normalizeUserName(name?: string | null): string {
33
+ const normalized = name?.trim()
34
+ return normalized && normalized.length > 0 ? normalized : 'Unknown'
35
+ }
36
+
37
+ function normalizeWorkspaceName(name?: string): string {
38
+ const normalized = name?.trim()
39
+ return normalized && normalized.length > 0 ? normalized : 'Workspace'
40
+ }
41
+
42
+ function hasStructuredProfileData(profile?: StructuredProfile): boolean {
43
+ return Boolean(profile && Object.keys(profile).length > 0)
44
+ }
45
+
46
+ function formatStructuredProfile(profile?: StructuredProfile): string {
47
+ if (!hasStructuredProfileData(profile)) {
48
+ return 'No structured workspace profile has been recorded yet.'
49
+ }
50
+
51
+ return JSON.stringify(profile, null, 2)
52
+ }
53
+
54
+ function formatDomainEvent(event: DomainEventLike, index: number): string {
55
+ const summary = typeof event.summary === 'string' && event.summary.trim().length > 0 ? event.summary.trim() : null
56
+ if (summary) {
57
+ const createdAt = toOptionalIsoDateTimeString(event.createdAt)
58
+ return createdAt ? `${index + 1}. ${summary} | createdAt=${createdAt}` : `${index + 1}. ${summary}`
59
+ }
60
+
61
+ const createdAt = toOptionalIsoDateTimeString(event.createdAt)
62
+ const segments = [`${index + 1}.`]
63
+ if (typeof event.actor === 'string' && event.actor.trim().length > 0) {
64
+ segments.push(`actor=${event.actor}`)
65
+ }
66
+ if (typeof event.action === 'string' && event.action.trim().length > 0) {
67
+ segments.push(`action=${event.action}`)
68
+ }
69
+ if (typeof event.category === 'string' && event.category.trim().length > 0) {
70
+ segments.push(`category=${event.category}`)
71
+ }
72
+ if (typeof event.significance === 'string' && event.significance.trim().length > 0) {
73
+ segments.push(`significance=${event.significance}`)
74
+ }
75
+
76
+ if (createdAt) {
77
+ segments.push(`createdAt=${createdAt}`)
78
+ }
79
+
80
+ if (segments.length === 1) {
81
+ segments.push('No structured event details.')
82
+ }
83
+
84
+ return segments.join(' | ')
85
+ }
86
+
87
+ function formatRecentDomainEvents(events: DomainEventLike[]): string {
88
+ if (events.length === 0) {
89
+ return 'No domain events recorded yet.'
90
+ }
91
+
92
+ return events.map((event, index) => formatDomainEvent(event, index)).join('\n')
93
+ }
94
+
95
+ export function buildAgentPromptContext(params: BuildAgentPromptContextParams): AgentPromptContext {
96
+ const workspaceName = normalizeWorkspaceName(params.workspaceName)
97
+ const summaryBlock = normalizeSummaryBlock(params.summaryBlock)
98
+ const structuredProfile = formatStructuredProfile(params.structuredProfile)
99
+ const promptSummary = params.promptSummary?.trim()
100
+ const userName = normalizeUserName(params.userName)
101
+ const recentDomainEvents = formatRecentDomainEvents(params.recentDomainEvents)
102
+
103
+ const workspaceDetailsBlock = [
104
+ '<workspace-profile>',
105
+ `name=${workspaceName}`,
106
+ 'summary-block:',
107
+ summaryBlock,
108
+ 'structured-profile-json:',
109
+ structuredProfile,
110
+ ...(promptSummary ? ['prompt-summary:', promptSummary] : []),
111
+ '</workspace-profile>',
112
+ ].join('\n')
113
+
114
+ const userDetailsBlock = ['<user-details>', `name=${userName}`, '</user-details>'].join('\n')
115
+
116
+ const domainEventsBlock = ['<domain-events>', 'last=5', recentDomainEvents, '</domain-events>'].join('\n')
117
+
118
+ return { systemWorkspaceDetails: [workspaceDetailsBlock, userDetailsBlock, domainEventsBlock].join('\n\n') }
119
+ }
@@ -0,0 +1,192 @@
1
+ import type { ChatMode } from './agent-types'
2
+ import { resolveReasoningProfile } from './workstream-routing-policy'
3
+ import type { ReasoningProfileName } from './workstream-routing-policy'
4
+
5
+ export interface AgentRuntimeConfig<TAgent extends string> {
6
+ id: TAgent
7
+ displayName: string
8
+ mode: ChatMode
9
+ extraInstructions?: string
10
+ maxSteps: number
11
+ reasoningProfile: ReasoningProfileName
12
+ toolCallBudget: number
13
+ maxInputTokensHint: number
14
+ }
15
+
16
+ export interface AgentToolPolicy<TSkill extends PropertyKey> {
17
+ resolvedMode: ChatMode
18
+ skills: TSkill[]
19
+ includeMemorySearch: boolean
20
+ includeConversationSearch: boolean
21
+ includeMemoryRemember: boolean
22
+ includeOrgActionSearch: boolean
23
+ includeMemoryBlockAppend: boolean
24
+ includeReadFileParts: boolean
25
+ includeInspectWebsite: boolean
26
+ includeProceedInOnboarding: boolean
27
+ includeGithubIntegration: boolean
28
+ includeIndexRepositoryByURL: boolean
29
+ includeIndexedRepository: boolean
30
+ }
31
+
32
+ export function toChatMode(workstreamMode: 'direct' | 'group'): ChatMode {
33
+ return workstreamMode === 'direct' ? 'fixedWorkstreamMode' : 'workstreamMode'
34
+ }
35
+
36
+ function toMemoryBlockSection(memoryBlock: string | undefined): string | undefined {
37
+ if (memoryBlock === undefined) return undefined
38
+ const content = memoryBlock.trim()
39
+ return ['<memory-block>', content, '</memory-block>'].join('\n')
40
+ }
41
+
42
+ export function resolveActiveAgentSkills<TAgent extends string, TSkill extends PropertyKey>(params: {
43
+ agentId: TAgent
44
+ workstreamMode: 'direct' | 'group'
45
+ mode?: ChatMode
46
+ onboardingActive: boolean
47
+ linearInstalled: boolean
48
+ getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
49
+ }): TSkill[] {
50
+ const mode = params.mode ?? toChatMode(params.workstreamMode)
51
+ const baseSkills = params
52
+ .getAgentSkills(params.agentId, mode)
53
+ .filter((skill) => (params.linearInstalled ? true : skill !== ('linear' as TSkill)))
54
+
55
+ if (!params.onboardingActive) {
56
+ return baseSkills
57
+ }
58
+
59
+ if (params.agentId !== ('chief' as TAgent)) {
60
+ return []
61
+ }
62
+
63
+ return baseSkills.filter((skill) => skill === ('asking-user-questions' as TSkill))
64
+ }
65
+
66
+ export function buildAgentRuntimeConfig<TAgent extends string, TSkill extends PropertyKey>(params: {
67
+ agentId: TAgent
68
+ displayNameByAgent: Record<TAgent, string>
69
+ workstreamMode: 'direct' | 'group'
70
+ mode?: ChatMode
71
+ skills?: TSkill[]
72
+ onboardingActive: boolean
73
+ linearInstalled: boolean
74
+ reasoningProfile?: ReasoningProfileName
75
+ systemWorkspaceDetails?: string
76
+ preSeededMemoriesSection?: string
77
+ retrievedKnowledgeSection?: string
78
+ workstreamMemoryBlock?: string
79
+ workstreamStateSection?: string
80
+ responseGuardSection?: string
81
+ learnedSkillsSection?: string
82
+ additionalInstructionSections?: string[]
83
+ getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
84
+ buildGlobalRuleInstructionSection: () => string
85
+ buildSkillInstructionSection: (skills: TSkill[]) => string
86
+ buildOnboardingPromptSection: () => string
87
+ }): AgentRuntimeConfig<TAgent> {
88
+ const mode = params.mode ?? toChatMode(params.workstreamMode)
89
+ const profile = resolveReasoningProfile({ message: '', explicitProfile: params.reasoningProfile ?? 'standard' })
90
+ const rulesSection = params.buildGlobalRuleInstructionSection()
91
+ const skillsSection =
92
+ params.skills && params.skills.length > 0 ? params.buildSkillInstructionSection(params.skills) : ''
93
+ const onboardingPromptSection = params.onboardingActive ? params.buildOnboardingPromptSection() : ''
94
+ const instructionSections = [
95
+ rulesSection,
96
+ skillsSection,
97
+ params.learnedSkillsSection?.trim(),
98
+ onboardingPromptSection,
99
+ params.systemWorkspaceDetails?.trim(),
100
+ params.preSeededMemoriesSection?.trim(),
101
+ params.retrievedKnowledgeSection?.trim(),
102
+ toMemoryBlockSection(params.workstreamMemoryBlock),
103
+ params.workstreamStateSection?.trim(),
104
+ ...(params.additionalInstructionSections?.map((section) => section.trim()) ?? []),
105
+ params.responseGuardSection?.trim(),
106
+ params.onboardingActive ? 'Onboarding is active. Keep responses onboarding-focused and concise.' : undefined,
107
+ ].filter((value): value is string => Boolean(value && value.length > 0))
108
+ const extraInstructions = instructionSections.length > 0 ? instructionSections.join('\n\n') : undefined
109
+
110
+ return {
111
+ id: params.agentId,
112
+ displayName: params.displayNameByAgent[params.agentId] ?? params.agentId,
113
+ mode,
114
+ extraInstructions,
115
+ maxSteps: profile.maxSteps,
116
+ reasoningProfile: profile.name,
117
+ toolCallBudget: profile.toolCallBudget,
118
+ maxInputTokensHint: profile.maxInputTokensHint,
119
+ }
120
+ }
121
+
122
+ export function buildWorkstreamAgentToolPolicy<TAgent extends string, TSkill extends PropertyKey>(params: {
123
+ agentId: TAgent
124
+ workstreamMode: 'direct' | 'group'
125
+ mode?: ChatMode
126
+ onboardingActive: boolean
127
+ linearInstalled: boolean
128
+ githubInstalled: boolean
129
+ provideRepoTool: boolean
130
+ getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
131
+ }): AgentToolPolicy<TSkill> {
132
+ const resolvedMode = params.mode ?? toChatMode(params.workstreamMode)
133
+ const skills = resolveActiveAgentSkills({
134
+ agentId: params.agentId,
135
+ workstreamMode: params.workstreamMode,
136
+ mode: resolvedMode,
137
+ onboardingActive: params.onboardingActive,
138
+ linearInstalled: params.linearInstalled,
139
+ getAgentSkills: params.getAgentSkills,
140
+ })
141
+
142
+ return {
143
+ resolvedMode,
144
+ skills,
145
+ includeMemorySearch: !params.onboardingActive,
146
+ includeConversationSearch: !params.onboardingActive,
147
+ includeMemoryRemember: !params.onboardingActive,
148
+ includeOrgActionSearch: !params.onboardingActive,
149
+ includeMemoryBlockAppend: true,
150
+ includeReadFileParts: true,
151
+ includeInspectWebsite: params.onboardingActive && params.agentId === ('chief' as TAgent),
152
+ includeProceedInOnboarding: params.onboardingActive && params.agentId === ('chief' as TAgent),
153
+ includeGithubIntegration: params.onboardingActive && params.agentId === ('chief' as TAgent),
154
+ includeIndexRepositoryByURL: params.onboardingActive && params.agentId === ('chief' as TAgent),
155
+ includeIndexedRepository: params.githubInstalled && params.provideRepoTool,
156
+ }
157
+ }
158
+
159
+ export function buildTeamConsultationAgentToolPolicy<TAgent extends string, TSkill extends PropertyKey>(params: {
160
+ agentId: TAgent
161
+ blockedSkills: readonly TSkill[]
162
+ blockedToolNames: readonly string[]
163
+ githubInstalled: boolean
164
+ provideRepoTool: boolean
165
+ getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
166
+ }): AgentToolPolicy<TSkill> & { blockedToolNames: Set<string> } {
167
+ const skills = resolveActiveAgentSkills({
168
+ agentId: params.agentId,
169
+ workstreamMode: 'group',
170
+ mode: 'fixedWorkstreamMode',
171
+ onboardingActive: false,
172
+ linearInstalled: false,
173
+ getAgentSkills: params.getAgentSkills,
174
+ }).filter((skill) => !params.blockedSkills.includes(skill))
175
+
176
+ return {
177
+ resolvedMode: 'fixedWorkstreamMode',
178
+ skills,
179
+ includeMemorySearch: true,
180
+ includeConversationSearch: true,
181
+ includeMemoryRemember: false,
182
+ includeOrgActionSearch: true,
183
+ includeMemoryBlockAppend: false,
184
+ includeReadFileParts: true,
185
+ includeInspectWebsite: false,
186
+ includeProceedInOnboarding: false,
187
+ includeGithubIntegration: false,
188
+ includeIndexRepositoryByURL: false,
189
+ includeIndexedRepository: params.githubInstalled && params.provideRepoTool,
190
+ blockedToolNames: new Set(params.blockedToolNames),
191
+ }
192
+ }
@@ -0,0 +1,117 @@
1
+ import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
2
+ import type { LanguageModelUsage, UIMessageStreamOptions } from 'ai'
3
+
4
+ import { agentDisplayNames } from '../config/agent-defaults'
5
+
6
+ export function readFiniteNumber(value: unknown): number | undefined {
7
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined
8
+ }
9
+
10
+ export function readRecord(value: unknown): Record<string, unknown> | undefined {
11
+ return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined
12
+ }
13
+
14
+ export function readOpenRouterUsageCost(usage: LanguageModelUsage): { cost?: number; upstreamInferenceCost?: number } {
15
+ const rawUsage = readRecord(usage.raw)
16
+ if (!rawUsage) return {}
17
+
18
+ const cost = readFiniteNumber(rawUsage.cost)
19
+ const costDetails = readRecord(rawUsage.cost_details)
20
+ const upstreamInferenceCost = readFiniteNumber(costDetails?.upstream_inference_cost)
21
+
22
+ return {
23
+ ...(cost !== undefined ? { cost } : {}),
24
+ ...(upstreamInferenceCost !== undefined ? { upstreamInferenceCost } : {}),
25
+ }
26
+ }
27
+
28
+ export function createAgentMessageMetadata(params: {
29
+ agentId: string
30
+ agentName: string
31
+ }): NonNullable<UIMessageStreamOptions<ChatMessage>['messageMetadata']> {
32
+ return ({ part }) => {
33
+ if (part.type === 'start') {
34
+ return { agentId: params.agentId, agentName: params.agentName, createdAt: Date.now() }
35
+ }
36
+
37
+ if (part.type === 'finish') {
38
+ const usageCost = readOpenRouterUsageCost(part.totalUsage)
39
+ return {
40
+ finishReason: part.finishReason,
41
+ ...(part.totalUsage.inputTokens !== undefined ? { inputTokens: part.totalUsage.inputTokens } : {}),
42
+ ...(part.totalUsage.outputTokens !== undefined ? { outputTokens: part.totalUsage.outputTokens } : {}),
43
+ ...(part.totalUsage.totalTokens !== undefined ? { totalTokens: part.totalUsage.totalTokens } : {}),
44
+ ...usageCost,
45
+ }
46
+ }
47
+
48
+ return undefined
49
+ }
50
+ }
51
+
52
+ export function createServerRunAbortController(externalAbortSignal?: AbortSignal) {
53
+ const controller = new AbortController()
54
+ const abort = (reason?: unknown) => {
55
+ if (controller.signal.aborted) return
56
+ controller.abort(reason ?? new DOMException('Run stopped by user.', 'AbortError'))
57
+ }
58
+
59
+ const abortFromExternal = () => {
60
+ abort((externalAbortSignal as (AbortSignal & { reason?: unknown }) | undefined)?.reason)
61
+ }
62
+
63
+ if (externalAbortSignal) {
64
+ if (externalAbortSignal.aborted) {
65
+ abortFromExternal()
66
+ } else {
67
+ externalAbortSignal.addEventListener('abort', abortFromExternal, { once: true })
68
+ }
69
+ }
70
+
71
+ return {
72
+ controller,
73
+ signal: controller.signal,
74
+ abort,
75
+ dispose: () => {
76
+ if (!externalAbortSignal) return
77
+ externalAbortSignal.removeEventListener('abort', abortFromExternal)
78
+ },
79
+ }
80
+ }
81
+
82
+ export function createTimedAbortSignal(parentSignal: AbortSignal, timeoutMs: number) {
83
+ const controller = new AbortController()
84
+ let didTimeout = false
85
+ const abortFromParent = () => {
86
+ controller.abort((parentSignal as AbortSignal & { reason?: unknown }).reason)
87
+ }
88
+ const timeoutId = setTimeout(() => {
89
+ didTimeout = true
90
+ controller.abort(new Error(`Timed out after ${timeoutMs}ms`))
91
+ }, timeoutMs)
92
+
93
+ if (parentSignal.aborted) {
94
+ abortFromParent()
95
+ } else {
96
+ parentSignal.addEventListener('abort', abortFromParent, { once: true })
97
+ }
98
+
99
+ return {
100
+ signal: controller.signal,
101
+ didTimeout: () => didTimeout,
102
+ dispose: () => {
103
+ clearTimeout(timeoutId)
104
+ parentSignal.removeEventListener('abort', abortFromParent)
105
+ },
106
+ }
107
+ }
108
+
109
+ export function buildSpecialistTaskMessage(params: { agentId: string; task: string }): ChatMessage {
110
+ const displayName = agentDisplayNames[params.agentId] ?? params.agentId
111
+ return {
112
+ id: Bun.randomUUIDv7(),
113
+ role: 'user',
114
+ parts: [{ type: 'text', text: [`Chief of Staff request for ${displayName}:`, params.task.trim()].join('\n') }],
115
+ metadata: { createdAt: Date.now() },
116
+ }
117
+ }
@@ -0,0 +1,22 @@
1
+ import type { Output, PrepareStepFunction, StopCondition, ToolLoopAgentOnFinishCallback, ToolSet } from 'ai'
2
+
3
+ export type ChatMode = 'direct' | 'workstreamMode' | 'fixedWorkstreamMode'
4
+
5
+ export interface CreateRoutedAgentOptions<TTools extends ToolSet = ToolSet> {
6
+ mode: ChatMode
7
+ tools: TTools
8
+ extraInstructions?: string
9
+ stopWhen?: StopCondition<TTools> | Array<StopCondition<TTools>>
10
+ prepareStep?: PrepareStepFunction<TTools>
11
+ maxRetries?: number
12
+ modelOverride?: { model: unknown; providerOptions?: Record<string, unknown> }
13
+ onFinish?: ToolLoopAgentOnFinishCallback<TTools>
14
+ }
15
+
16
+ export interface CreateHelperToolLoopAgentOptions {
17
+ instructions?: string
18
+ maxOutputTokens?: number
19
+ temperature?: number
20
+ output?: Output.Output
21
+ maxRetries?: number
22
+ }
@@ -0,0 +1,16 @@
1
+ import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
2
+
3
+ export function hasApprovalRespondedParts(message: ChatMessage): boolean {
4
+ return message.parts.some((part) => 'state' in part && (part as { state: string }).state === 'approval-responded')
5
+ }
6
+
7
+ export function isApprovalContinuationRequest(messages: ChatMessage[]): boolean {
8
+ const lastAssistant = [...messages].reverse().find((message) => message.role === 'assistant')
9
+ if (!lastAssistant) return false
10
+
11
+ const lastMessageIndex = messages.lastIndexOf(lastAssistant)
12
+ const hasUserAfter = messages.slice(lastMessageIndex + 1).some((message) => message.role === 'user')
13
+ if (hasUserAfter) return false
14
+
15
+ return hasApprovalRespondedParts(lastAssistant)
16
+ }
@@ -0,0 +1,46 @@
1
+ import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
2
+
3
+ import type { ReadableUploadMetadataLike } from './chat-types'
4
+ export type { ReadableUploadMetadataLike } from './chat-types'
5
+
6
+ export function buildReadableUploadMetadataText(uploads: ReadableUploadMetadataLike[]): string {
7
+ if (uploads.length === 0) return ''
8
+
9
+ const lines = uploads.map((upload) => {
10
+ const sizeSegment = upload.sizeBytes === null ? '' : `, sizeBytes=${upload.sizeBytes}`
11
+ return `- ${upload.filename} (${upload.mediaType}${sizeSegment}) storageKey=${upload.storageKey}`
12
+ })
13
+
14
+ return [
15
+ 'Uploaded files are available via tool readFileParts.',
16
+ 'Call readFileParts with storageKey and part. Each part contains up to 25 pages.',
17
+ 'Available uploads:',
18
+ ...lines,
19
+ ].join('\n')
20
+ }
21
+
22
+ export function buildModelInputMessagesWithUploadMetadata(params: {
23
+ messages: ChatMessage[]
24
+ latestUserMessageId: string
25
+ uploadMetadataText: string
26
+ }): ChatMessage[] {
27
+ return params.messages.map((message) => {
28
+ const fileParts = message.parts.filter((part) => part.type === 'file')
29
+ if (fileParts.length === 0) return message
30
+
31
+ const nonFileParts = message.parts.filter((part) => part.type !== 'file')
32
+ const attachmentNames = fileParts
33
+ .map((part) => (typeof part.filename === 'string' && part.filename.trim() ? part.filename.trim() : 'Attachment'))
34
+ .join(', ')
35
+
36
+ const nextParts: ChatMessage['parts'] = [...nonFileParts]
37
+ if (attachmentNames) {
38
+ nextParts.push({ type: 'text', text: `User attached files: ${attachmentNames}` })
39
+ }
40
+ if (message.id === params.latestUserMessageId && params.uploadMetadataText) {
41
+ nextParts.push({ type: 'text', text: params.uploadMetadataText })
42
+ }
43
+
44
+ return { ...message, parts: nextParts }
45
+ })
46
+ }
@@ -0,0 +1,10 @@
1
+ import type { MessagePartLike } from './chat-types'
2
+
3
+ export function hasMessageContent(parts: readonly MessagePartLike[]): boolean {
4
+ for (const part of parts) {
5
+ if (part.type === 'file') return true
6
+ if (part.type === 'text' && typeof part.text === 'string' && part.text.trim().length > 0) return true
7
+ }
8
+
9
+ return false
10
+ }
@@ -0,0 +1,21 @@
1
+ import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
2
+
3
+ import { isApprovalContinuationRequest } from './approval-continuation'
4
+
5
+ export type RoutedChatRequest =
6
+ | { kind: 'approval-continuation'; approvalMessages: ChatMessage[] }
7
+ | { kind: 'turn'; inputMessage: ChatMessage }
8
+ | { kind: 'invalid'; message: string }
9
+
10
+ export function routeWorkstreamChatMessages(messages: ChatMessage[]): RoutedChatRequest {
11
+ if (isApprovalContinuationRequest(messages)) {
12
+ return { kind: 'approval-continuation', approvalMessages: messages }
13
+ }
14
+
15
+ const inputMessage = [...messages].reverse().find((message) => message.role === 'user')
16
+ if (!inputMessage) {
17
+ return { kind: 'invalid', message: 'Only user messages can be submitted to this endpoint.' }
18
+ }
19
+
20
+ return { kind: 'turn', inputMessage }
21
+ }
@@ -0,0 +1,25 @@
1
+ const COMPACTION_POLL_INTERVAL_MS = 200
2
+ const COMPACTION_MAX_WAIT_MS = 120_000
3
+
4
+ export async function waitForCompactionIfNeeded<TEntity>(params: {
5
+ entityId: string
6
+ entityLabel: string
7
+ loadEntity: () => Promise<TEntity>
8
+ isCompacting: (entity: TEntity) => boolean
9
+ }): Promise<TEntity> {
10
+ let entity = await params.loadEntity()
11
+ if (!params.isCompacting(entity)) return entity
12
+
13
+ const deadline = Date.now() + COMPACTION_MAX_WAIT_MS
14
+ while (params.isCompacting(entity)) {
15
+ if (Date.now() > deadline) {
16
+ throw new Error(
17
+ `${params.entityLabel} ${params.entityId} compaction did not complete within ${COMPACTION_MAX_WAIT_MS}ms`,
18
+ )
19
+ }
20
+ await Bun.sleep(COMPACTION_POLL_INTERVAL_MS)
21
+ entity = await params.loadEntity()
22
+ }
23
+
24
+ return entity
25
+ }
@@ -0,0 +1,20 @@
1
+ export class ChatRunRegistry {
2
+ private controllers = new Map<string, AbortController>()
3
+
4
+ register(runId: string, controller: AbortController): void {
5
+ this.controllers.set(runId, controller)
6
+ }
7
+
8
+ unregister(runId: string): void {
9
+ this.controllers.delete(runId)
10
+ }
11
+
12
+ stop(runId: string, reason?: unknown): boolean {
13
+ const controller = this.controllers.get(runId)
14
+ if (!controller) return false
15
+
16
+ this.controllers.delete(runId)
17
+ controller.abort(reason)
18
+ return true
19
+ }
20
+ }
@@ -0,0 +1,18 @@
1
+ export interface MessagePartLike {
2
+ type?: string
3
+ text?: string
4
+ [key: string]: unknown
5
+ }
6
+
7
+ export interface ChatMessageLike<TPart extends MessagePartLike = MessagePartLike> {
8
+ role: string
9
+ parts: TPart[]
10
+ metadata?: unknown
11
+ }
12
+
13
+ export interface ReadableUploadMetadataLike {
14
+ storageKey: string
15
+ filename: string
16
+ mediaType: string
17
+ sizeBytes: number | null
18
+ }
@@ -0,0 +1,11 @@
1
+ export const CONTEXT_COMPACTION_THRESHOLD_RATIO = 0.7
2
+ export const CONTEXT_OUTPUT_RESERVE_TOKENS = 32_000
3
+ export const CONTEXT_SAFETY_MARGIN_TOKENS = 8_000
4
+ export const COMPACTION_CHUNK_MAX_CHARS = 120_000
5
+ export const SUMMARY_ROLLUP_MAX_TOKENS = 80_000
6
+ export const CONTEXT_COMPACTION_INCLUDED_TOOL_NAMES = ['userQuestions', 'proceedInOnboarding'] as const
7
+ export const CONTEXT_COMPACTION_INCLUDED_TOOL_PREFIXES = ['linear'] as const
8
+ export const MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES = 15
9
+ export const MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES = 10
10
+ export const CONTEXT_SIZE = 200_000
11
+ export const WORKSTREAM_RAW_TAIL_MESSAGES = 6