@lota-sdk/core 0.1.11 → 0.1.13

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 (95) hide show
  1. package/package.json +2 -87
  2. package/src/ai/index.ts +3 -0
  3. package/src/bifrost/index.ts +1 -0
  4. package/src/config/agent-defaults.ts +30 -7
  5. package/src/config/constants.ts +0 -9
  6. package/src/config/debug-logger.ts +43 -0
  7. package/src/config/index.ts +5 -0
  8. package/src/config/model-constants.ts +0 -3
  9. package/src/config/workstream-defaults.ts +4 -0
  10. package/src/db/cursor-pagination.ts +2 -2
  11. package/src/db/index.ts +10 -0
  12. package/src/db/memory.ts +9 -15
  13. package/src/document/index.ts +2 -0
  14. package/src/document/parsing.ts +0 -25
  15. package/src/embeddings/provider.ts +17 -8
  16. package/src/index.ts +15 -505
  17. package/src/queues/index.ts +10 -0
  18. package/src/redis/connection-accessor.ts +26 -0
  19. package/src/redis/connection.ts +1 -1
  20. package/src/redis/index.ts +9 -25
  21. package/src/redis/org-memory-lock.ts +1 -1
  22. package/src/redis/redis-lease-lock.ts +1 -1
  23. package/src/redis/stream-context.ts +12 -2
  24. package/src/runtime/agent-runtime-policy.ts +9 -5
  25. package/src/runtime/agent-stream-helpers.ts +6 -3
  26. package/src/runtime/agent-types.ts +1 -5
  27. package/src/runtime/approval-continuation.ts +9 -1
  28. package/src/runtime/chat-attachments.ts +1 -1
  29. package/src/runtime/chat-request-routing.ts +1 -1
  30. package/src/runtime/context-compaction-runtime.ts +2 -2
  31. package/src/runtime/context-compaction.ts +1 -1
  32. package/src/runtime/execution-plan.ts +1 -1
  33. package/src/runtime/index.ts +26 -0
  34. package/src/runtime/indexed-repositories-policy.ts +10 -10
  35. package/src/runtime/memory-pipeline.ts +0 -2
  36. package/src/runtime/runtime-config.ts +238 -0
  37. package/src/runtime/runtime-extensions.ts +3 -2
  38. package/src/runtime/runtime-worker-registry.ts +47 -0
  39. package/src/runtime/team-consultation-orchestrator.ts +9 -6
  40. package/src/runtime/team-consultation-prompts.ts +3 -2
  41. package/src/runtime/turn-lifecycle.ts +1 -1
  42. package/src/runtime/workstream-chat-helpers.ts +0 -54
  43. package/src/runtime/workstream-routing-policy.ts +3 -7
  44. package/src/runtime.ts +387 -0
  45. package/src/services/chat-attachments.service.ts +1 -1
  46. package/src/services/context-compaction.service.ts +1 -1
  47. package/src/services/execution-plan.service.ts +14 -16
  48. package/src/services/index.ts +14 -0
  49. package/src/services/learned-skill.service.ts +80 -37
  50. package/src/services/memory.service.ts +5 -4
  51. package/src/services/mutating-approval.service.ts +1 -1
  52. package/src/services/organization-member.service.ts +1 -1
  53. package/src/services/organization.service.ts +1 -1
  54. package/src/services/plan-approval.service.ts +2 -2
  55. package/src/services/plan-artifact.service.ts +2 -3
  56. package/src/services/plan-builder.service.ts +1 -1
  57. package/src/services/plan-checkpoint.service.ts +2 -2
  58. package/src/services/plan-compiler.service.ts +2 -2
  59. package/src/services/plan-executor.service.ts +10 -9
  60. package/src/services/plan-run.service.ts +2 -2
  61. package/src/services/plan-validator.service.ts +4 -4
  62. package/src/services/recent-activity-title.service.ts +1 -1
  63. package/src/services/recent-activity.service.ts +14 -16
  64. package/src/services/user.service.ts +2 -2
  65. package/src/services/workstream-message.service.ts +2 -3
  66. package/src/services/workstream-title.service.ts +1 -1
  67. package/src/services/workstream-turn-preparation.ts +105 -50
  68. package/src/services/workstream-turn.ts +14 -1
  69. package/src/services/workstream.service.ts +9 -9
  70. package/src/storage/attachment-parser.ts +1 -1
  71. package/src/storage/attachment-storage.service.ts +11 -10
  72. package/src/storage/generated-document-storage.service.ts +7 -6
  73. package/src/storage/index.ts +10 -0
  74. package/src/system-agents/delegated-agent-factory.ts +78 -29
  75. package/src/system-agents/index.ts +4 -0
  76. package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
  77. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  78. package/src/system-agents/skill-extractor.agent.ts +1 -1
  79. package/src/system-agents/skill-manager.agent.ts +2 -4
  80. package/src/tools/execution-plan.tool.ts +2 -2
  81. package/src/tools/firecrawl-client.ts +2 -2
  82. package/src/tools/index.ts +12 -0
  83. package/src/tools/research-topic.tool.ts +1 -1
  84. package/src/tools/team-think.tool.ts +1 -1
  85. package/src/tools/user-questions.tool.ts +2 -2
  86. package/src/utils/index.ts +6 -0
  87. package/src/workers/bootstrap.ts +8 -16
  88. package/src/workers/index.ts +7 -0
  89. package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
  90. package/src/workers/skill-extraction.runner.ts +1 -1
  91. package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
  92. package/src/workers/utils/repo-structure-extractor.ts +2 -5
  93. package/src/workers/utils/repomix-file-sections.ts +42 -0
  94. package/src/config/env-shapes.ts +0 -121
  95. package/src/runtime/agent-contract.ts +0 -1
@@ -1,3 +1,5 @@
1
+ import { getLeadAgentId } from '../config/agent-defaults'
2
+ import { resolveOnboardingOwnerAgentId } from '../config/workstream-defaults'
1
3
  import type { ChatMode } from './agent-types'
2
4
  import { resolveReasoningProfile } from './workstream-routing-policy'
3
5
  import type { ReasoningProfileName } from './workstream-routing-policy'
@@ -48,6 +50,7 @@ export function resolveActiveAgentSkills<TAgent extends string, TSkill extends P
48
50
  getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
49
51
  }): TSkill[] {
50
52
  const mode = params.mode ?? toChatMode(params.workstreamMode)
53
+ const onboardingOwnerAgentId = resolveOnboardingOwnerAgentId(getLeadAgentId()) as TAgent
51
54
  const baseSkills = params
52
55
  .getAgentSkills(params.agentId, mode)
53
56
  .filter((skill) => (params.linearInstalled ? true : skill !== ('linear' as TSkill)))
@@ -56,7 +59,7 @@ export function resolveActiveAgentSkills<TAgent extends string, TSkill extends P
56
59
  return baseSkills
57
60
  }
58
61
 
59
- if (params.agentId !== ('chief' as TAgent)) {
62
+ if (params.agentId !== onboardingOwnerAgentId) {
60
63
  return []
61
64
  }
62
65
 
@@ -130,6 +133,7 @@ export function buildWorkstreamAgentToolPolicy<TAgent extends string, TSkill ext
130
133
  getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
131
134
  }): AgentToolPolicy<TSkill> {
132
135
  const resolvedMode = params.mode ?? toChatMode(params.workstreamMode)
136
+ const onboardingOwnerAgentId = resolveOnboardingOwnerAgentId(getLeadAgentId()) as TAgent
133
137
  const skills = resolveActiveAgentSkills({
134
138
  agentId: params.agentId,
135
139
  workstreamMode: params.workstreamMode,
@@ -148,10 +152,10 @@ export function buildWorkstreamAgentToolPolicy<TAgent extends string, TSkill ext
148
152
  includeOrgActionSearch: !params.onboardingActive,
149
153
  includeMemoryBlockAppend: true,
150
154
  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
+ includeInspectWebsite: params.onboardingActive && params.agentId === onboardingOwnerAgentId,
156
+ includeProceedInOnboarding: params.onboardingActive && params.agentId === onboardingOwnerAgentId,
157
+ includeGithubIntegration: params.onboardingActive && params.agentId === onboardingOwnerAgentId,
158
+ includeIndexRepositoryByURL: params.onboardingActive && params.agentId === onboardingOwnerAgentId,
155
159
  includeIndexedRepository: params.githubInstalled && params.provideRepoTool,
156
160
  }
157
161
  }
@@ -1,7 +1,7 @@
1
- import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
1
+ import type { ChatMessage } from '@lota-sdk/shared'
2
2
  import type { LanguageModelUsage, UIMessageStreamOptions } from 'ai'
3
3
 
4
- import { agentDisplayNames } from '../config/agent-defaults'
4
+ import { agentDisplayNames, getLeadAgentDisplayName } from '../config/agent-defaults'
5
5
 
6
6
  export function readFiniteNumber(value: unknown): number | undefined {
7
7
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined
@@ -108,10 +108,13 @@ export function createTimedAbortSignal(parentSignal: AbortSignal, timeoutMs: num
108
108
 
109
109
  export function buildSpecialistTaskMessage(params: { agentId: string; task: string }): ChatMessage {
110
110
  const displayName = agentDisplayNames[params.agentId] ?? params.agentId
111
+ const leadAgentDisplayName = getLeadAgentDisplayName()
111
112
  return {
112
113
  id: Bun.randomUUIDv7(),
113
114
  role: 'user',
114
- parts: [{ type: 'text', text: [`Chief of Staff request for ${displayName}:`, params.task.trim()].join('\n') }],
115
+ parts: [
116
+ { type: 'text', text: [`${leadAgentDisplayName} request for ${displayName}:`, params.task.trim()].join('\n') },
117
+ ],
115
118
  metadata: { createdAt: Date.now() },
116
119
  }
117
120
  }
@@ -1,5 +1 @@
1
- export type {
2
- ChatMode,
3
- CreateHelperToolLoopAgentOptions,
4
- CreateRoutedAgentOptions,
5
- } from '@lota-sdk/shared/runtime/agent-types'
1
+ export type { ChatMode, CreateHelperToolLoopAgentOptions, CreateRoutedAgentOptions } from '@lota-sdk/shared'
@@ -1,4 +1,6 @@
1
- import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
1
+ import type { ChatMessage } from '@lota-sdk/shared'
2
+
3
+ import { TABLES } from '../db/tables'
2
4
 
3
5
  export function hasApprovalRespondedParts(message: ChatMessage): boolean {
4
6
  return message.parts.some((part) => 'state' in part && (part as { state: string }).state === 'approval-responded')
@@ -61,12 +63,18 @@ const PLAN_TOOL_NAMES = new Set([
61
63
  'resumeExecutionPlanRun',
62
64
  ])
63
65
 
66
+ function isPlanApprovalId(value: unknown): value is string {
67
+ return typeof value === 'string' && value.startsWith(`${TABLES.PLAN_APPROVAL}:`)
68
+ }
69
+
64
70
  export function isNativeToolApprovalRequest(messages: ChatMessage[]): boolean {
65
71
  const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant')
66
72
  if (!lastAssistant) return false
67
73
 
68
74
  return lastAssistant.parts.some((part) => {
69
75
  if (!('state' in part) || (part as { state: string }).state !== 'approval-responded') return false
76
+ const approvalId = (part as { approval?: { id?: unknown } }).approval?.id
77
+ if (isPlanApprovalId(approvalId)) return false
70
78
  const type = (part as { type?: string }).type
71
79
  if (typeof type !== 'string' || !type.startsWith('tool-')) return false
72
80
  const toolName = type.slice(5)
@@ -1,4 +1,4 @@
1
- import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
1
+ import type { ChatMessage } from '@lota-sdk/shared'
2
2
 
3
3
  import type { ReadableUploadMetadataLike } from './chat-types'
4
4
  export type { ReadableUploadMetadataLike } from './chat-types'
@@ -1,4 +1,4 @@
1
- import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
1
+ import type { ChatMessage } from '@lota-sdk/shared'
2
2
 
3
3
  import { isApprovalContinuationRequest, isNativeToolApprovalRequest } from './approval-continuation'
4
4
 
@@ -18,12 +18,12 @@ import {
18
18
  import type { GenerateHelperStructuredParams, GenerateHelperTextParams } from './helper-model'
19
19
  import { StructuredCompactionOutputSchema } from './workstream-state'
20
20
 
21
- export interface HelperModelRuntime {
21
+ interface HelperModelRuntime {
22
22
  generateHelperStructured<T>(params: GenerateHelperStructuredParams<T>): Promise<T>
23
23
  generateHelperText(params: GenerateHelperTextParams): Promise<string>
24
24
  }
25
25
 
26
- export interface CreateContextCompactionRuntimeDeps {
26
+ interface CreateContextCompactionRuntimeDeps {
27
27
  helperModelRuntime: HelperModelRuntime
28
28
  now?: () => number
29
29
  randomId?: () => string
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto'
2
2
 
3
- import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
3
+ import type { ChatMessage } from '@lota-sdk/shared'
4
4
 
5
5
  import { readString } from '../utils/string'
6
6
  import {
@@ -1,4 +1,4 @@
1
- import type { SerializableExecutionPlan } from '@lota-sdk/shared/schemas/execution-plan'
1
+ import type { SerializableExecutionPlan } from '@lota-sdk/shared'
2
2
 
3
3
  export const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
4
4
  - Before doing multi-step work, create a contract-driven execution plan instead of tracking steps only in prose.
@@ -0,0 +1,26 @@
1
+ export * from './approval-continuation'
2
+ export * from './agent-runtime-policy'
3
+ export * from './agent-stream-helpers'
4
+ export * from './agent-types'
5
+ export * from './chat-request-routing'
6
+ export * from './chat-run-registry'
7
+ export * from './context-compaction'
8
+ export * from './execution-plan'
9
+ export * from './helper-model'
10
+ export * from './indexed-repositories-policy'
11
+ export * from './instruction-sections'
12
+ export * from './memory-block'
13
+ export * from './memory-digest-policy'
14
+ export * from './memory-scope'
15
+ export * from './llm-content'
16
+ export * from './plugin-types'
17
+ export * from './runtime-config'
18
+ export * from './runtime-extensions'
19
+ export * from './runtime-worker-registry'
20
+ export * from './skill-extraction-policy'
21
+ export * from './team-consultation-orchestrator'
22
+ export * from './team-consultation-prompts'
23
+ export * from './turn-lifecycle'
24
+ export * from './workstream-chat-helpers'
25
+ export * from './workstream-routing-policy'
26
+ export * from './workstream-state'
@@ -1,3 +1,5 @@
1
+ import { agentRoster } from '../config/agent-defaults'
2
+
1
3
  export type IndexedRepoAgentName = string
2
4
 
3
5
  export const REPO_SECTION_NAMES = [
@@ -13,16 +15,14 @@ export type RepoSectionName = (typeof REPO_SECTION_NAMES)[number]
13
15
 
14
16
  const ALL_REPO_SECTIONS: RepoSectionName[] = [...REPO_SECTION_NAMES]
15
17
 
16
- export const DEFAULT_REPO_SECTIONS_BY_AGENT: Record<IndexedRepoAgentName, RepoSectionName[]> = {
17
- chief: [...ALL_REPO_SECTIONS],
18
- ceo: [...ALL_REPO_SECTIONS],
19
- cto: [...ALL_REPO_SECTIONS],
20
- cpo: [...ALL_REPO_SECTIONS],
21
- cmo: [...ALL_REPO_SECTIONS],
22
- cfo: [...ALL_REPO_SECTIONS],
23
- mentor: [...ALL_REPO_SECTIONS],
18
+ export function buildDefaultRepoSectionsByAgent(
19
+ roster: readonly IndexedRepoAgentName[] = agentRoster,
20
+ ): Record<IndexedRepoAgentName, RepoSectionName[]> {
21
+ return Object.fromEntries(roster.map((agentId) => [agentId, [...ALL_REPO_SECTIONS]]))
24
22
  }
25
23
 
26
- export function emptyAgentContextMap(): Record<IndexedRepoAgentName, string> {
27
- return { chief: '', ceo: '', cto: '', cpo: '', cmo: '', cfo: '', mentor: '' }
24
+ export function emptyAgentContextMap(
25
+ roster: readonly IndexedRepoAgentName[] = agentRoster,
26
+ ): Record<IndexedRepoAgentName, string> {
27
+ return Object.fromEntries(roster.map((agentId) => [agentId, '']))
28
28
  }
@@ -1,5 +1,3 @@
1
- export { agentScopeId, ORG_SCOPE_PREFIX, scopeId } from './memory-scope'
2
-
3
1
  interface MemoryFactInput {
4
2
  content: string
5
3
  confidence: number
@@ -0,0 +1,238 @@
1
+ import { z } from 'zod'
2
+
3
+ import type { CoreWorkstreamProfile } from '../config/agent-defaults'
4
+ import type { LotaWorkstreamConfig, WorkstreamBootstrapWelcomeConfig } from '../config/workstream-defaults'
5
+ import type { LotaPlugin } from './plugin-types'
6
+ import type { LotaRuntimeAdapters, LotaRuntimeTurnHooks } from './runtime-extensions'
7
+ import type { LotaRuntimeWorkerExtensions } from './runtime-worker-registry'
8
+
9
+ const logLevelValues = ['trace', 'debug', 'info', 'warning', 'error', 'fatal'] as const
10
+
11
+ type LotaAgentFactoryRegistry = Record<string, (...args: unknown[]) => unknown>
12
+
13
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null
14
+
15
+ function isStringOrUrl(value: unknown): value is string | URL {
16
+ return typeof value === 'string' || value instanceof URL
17
+ }
18
+
19
+ function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
20
+ return typeof value === 'function'
21
+ }
22
+
23
+ function isStringRecord(value: unknown): value is Record<string, string> {
24
+ return isRecord(value) && Object.values(value).every((entry) => typeof entry === 'string')
25
+ }
26
+
27
+ function isAgentFactoryRegistry(value: unknown): value is LotaAgentFactoryRegistry {
28
+ return isRecord(value) && Object.values(value).every((entry) => typeof entry === 'function')
29
+ }
30
+
31
+ function isPluginRuntimeRecord(value: unknown): value is Record<string, LotaPlugin> {
32
+ return isRecord(value)
33
+ }
34
+
35
+ function isToolProviderRecord(value: unknown): value is Record<string, unknown> {
36
+ return isRecord(value)
37
+ }
38
+
39
+ function isWorkerExtensionRecord(value: unknown): value is LotaRuntimeWorkerExtensions {
40
+ if (!isRecord(value)) return false
41
+
42
+ for (const key of ['start', 'schedule'] as const) {
43
+ const entry = value[key]
44
+ if (entry !== undefined && !isRecord(entry)) {
45
+ return false
46
+ }
47
+ }
48
+
49
+ return true
50
+ }
51
+
52
+ const workstreamBootstrapWelcomeConfigSchema = z.object({
53
+ directAgentId: z.string().trim().min(1),
54
+ buildMessageText: z.custom<WorkstreamBootstrapWelcomeConfig['buildMessageText']>(isFunction, {
55
+ error: 'onboardingWelcome.buildMessageText must be a function',
56
+ }),
57
+ })
58
+
59
+ const workstreamConfigSchema = z.object({
60
+ bootstrap: z
61
+ .object({
62
+ onboardingDirectAgents: z.array(z.string().trim().min(1)).optional(),
63
+ completedDirectAgents: z.array(z.string().trim().min(1)).optional(),
64
+ coreTypesAfterOnboarding: z.array(z.string().trim().min(1)).optional(),
65
+ ensureDefaultGroupOnCompleted: z.boolean().optional(),
66
+ onboardingWelcome: workstreamBootstrapWelcomeConfigSchema.optional(),
67
+ })
68
+ .optional(),
69
+ })
70
+
71
+ const agentsConfigSchema = z
72
+ .object({
73
+ roster: z.array(z.string().trim().min(1)).min(1),
74
+ leadAgentId: z.string().trim().min(1),
75
+ displayNames: z.custom<Record<string, string>>(isStringRecord, {
76
+ error: 'agents.displayNames must be a string record',
77
+ }),
78
+ shortDisplayNames: z
79
+ .custom<Record<string, string>>(isStringRecord, { error: 'agents.shortDisplayNames must be a string record' })
80
+ .optional(),
81
+ teamConsultParticipants: z.array(z.string().trim().min(1)),
82
+ getCoreWorkstreamProfile: z
83
+ .custom<(coreType: string) => CoreWorkstreamProfile>(isFunction, {
84
+ error: 'agents.getCoreWorkstreamProfile must be a function',
85
+ })
86
+ .optional(),
87
+ createAgent: z
88
+ .custom<LotaAgentFactoryRegistry>(isAgentFactoryRegistry, {
89
+ error: 'agents.createAgent must be a function registry',
90
+ })
91
+ .optional(),
92
+ buildAgentTools: z
93
+ .custom<(...args: unknown[]) => unknown>(isFunction, { error: 'agents.buildAgentTools must be a function' })
94
+ .optional(),
95
+ getAgentRuntimeConfig: z
96
+ .custom<(...args: unknown[]) => unknown>(isFunction, { error: 'agents.getAgentRuntimeConfig must be a function' })
97
+ .optional(),
98
+ })
99
+ .superRefine((value, ctx) => {
100
+ if (!value.roster.includes(value.leadAgentId)) {
101
+ ctx.addIssue({
102
+ code: 'custom',
103
+ path: ['leadAgentId'],
104
+ message: 'agents.leadAgentId must be present in agents.roster',
105
+ })
106
+ }
107
+
108
+ for (const agentId of value.roster) {
109
+ if (!value.displayNames[agentId]) {
110
+ ctx.addIssue({
111
+ code: 'custom',
112
+ path: ['displayNames', agentId],
113
+ message: `Missing display name for agent "${agentId}"`,
114
+ })
115
+ }
116
+ }
117
+ })
118
+
119
+ export const LotaRuntimeConfigSchema = z.object({
120
+ database: z.object({
121
+ url: z.string().trim().min(1),
122
+ namespace: z.string().trim().min(1),
123
+ username: z.string().trim().min(1),
124
+ password: z.string().trim().min(1),
125
+ }),
126
+ redis: z.object({ url: z.string().trim().min(1) }),
127
+ aiGateway: z.object({
128
+ url: z.string().trim().min(1),
129
+ key: z.string().trim().min(1),
130
+ admin: z.string().trim().min(1).optional(),
131
+ pass: z.string().trim().min(1).optional(),
132
+ embeddingModel: z.string().trim().min(1).default('openai/text-embedding-3-small'),
133
+ }),
134
+ s3: z.object({
135
+ endpoint: z.string().trim().min(1),
136
+ bucket: z.string().trim().min(1),
137
+ region: z.string().trim().min(1).default('garage'),
138
+ accessKeyId: z.string().trim().min(1),
139
+ secretAccessKey: z.string().trim().min(1),
140
+ attachmentUrlExpiresIn: z.coerce.number().positive().default(1800),
141
+ }),
142
+ firecrawl: z.object({
143
+ apiKey: z
144
+ .string()
145
+ .min(1, 'Firecrawl API key is required')
146
+ .refine((value) => value.startsWith('fc-'), 'Firecrawl API key must start with fc-')
147
+ .refine((value) => value !== 'dev-fire-key', 'Firecrawl API key placeholder is not allowed'),
148
+ apiBaseUrl: z.string().trim().url().optional(),
149
+ }),
150
+ logging: z.object({ level: z.enum(logLevelValues).default('info') }).default({ level: 'info' }),
151
+ memory: z
152
+ .object({
153
+ searchK: z.coerce.number().int().positive().default(6),
154
+ embeddingCacheTtlSeconds: z.coerce.number().int().positive().default(3600),
155
+ })
156
+ .default({ searchK: 6, embeddingCacheTtlSeconds: 3600 }),
157
+ workstreams: workstreamConfigSchema.default({}),
158
+ backgroundProcessing: z
159
+ .object({
160
+ memoryExtractionFrequency: z.coerce.number().int().positive().default(3),
161
+ skillExtractionFrequency: z.coerce.number().int().positive().default(5),
162
+ memoryDigestFrequency: z.coerce.number().int().positive().default(1),
163
+ memoryConsolidationFrequency: z.coerce.number().int().positive().default(10),
164
+ })
165
+ .default({
166
+ memoryExtractionFrequency: 3,
167
+ skillExtractionFrequency: 5,
168
+ memoryDigestFrequency: 1,
169
+ memoryConsolidationFrequency: 10,
170
+ }),
171
+ agents: agentsConfigSchema,
172
+ toolProviders: z.custom<Record<string, unknown>>(isToolProviderRecord).optional(),
173
+ extraSchemaFiles: z.array(z.custom<string | URL>(isStringOrUrl)).optional(),
174
+ extraWorkers: z.custom<LotaRuntimeWorkerExtensions>(isWorkerExtensionRecord).optional(),
175
+ pluginRuntime: z.custom<Record<string, LotaPlugin>>(isPluginRuntimeRecord).optional(),
176
+ runtimeAdapters: z.custom<LotaRuntimeAdapters>(isRecord).optional(),
177
+ turnHooks: z.custom<LotaRuntimeTurnHooks>(isRecord).optional(),
178
+ })
179
+
180
+ export type LotaRuntimeConfig = z.input<typeof LotaRuntimeConfigSchema>
181
+ export type ResolvedLotaRuntimeConfig = z.infer<typeof LotaRuntimeConfigSchema>
182
+
183
+ export const WORKER_BOOTSTRAP_ENV_SCHEMA = z.object({
184
+ SURREALDB_URL: z.string().trim().min(1),
185
+ SURREALDB_NAMESPACE: z.string().trim().min(1),
186
+ SURREALDB_USER: z.string().trim().min(1),
187
+ SURREALDB_PASSWORD: z.string().trim().min(1),
188
+ DB_SCHEMA_FINGERPRINT: z.string().trim().min(1).optional(),
189
+ })
190
+
191
+ export type WorkerBootstrapEnv = z.infer<typeof WORKER_BOOTSTRAP_ENV_SCHEMA>
192
+
193
+ export const LOTA_RUNTIME_ENV_KEYS = Object.freeze([
194
+ 'SURREALDB_URL',
195
+ 'SURREALDB_NAMESPACE',
196
+ 'SURREALDB_USER',
197
+ 'SURREALDB_PASSWORD',
198
+ 'REDIS_URL',
199
+ 'AI_GATEWAY_URL',
200
+ 'AI_GATEWAY_KEY',
201
+ 'AI_GATEWAY_ADMIN',
202
+ 'AI_GATEWAY_PASS',
203
+ 'AI_EMBEDDING_MODEL',
204
+ 'S3_ENDPOINT',
205
+ 'S3_BUCKET',
206
+ 'S3_REGION',
207
+ 'S3_ACCESS_KEY_ID',
208
+ 'S3_SECRET_ACCESS_KEY',
209
+ 'ATTACHMENT_URL_EXPIRES_IN',
210
+ 'FIRECRAWL_API_KEY',
211
+ 'FIRECRAWL_API_BASE_URL',
212
+ 'LOG_LEVEL',
213
+ 'MEMORY_SEARCH_K',
214
+ ])
215
+
216
+ let runtimeConfig: ResolvedLotaRuntimeConfig | null = null
217
+
218
+ export function parseLotaRuntimeConfig(config: LotaRuntimeConfig): ResolvedLotaRuntimeConfig {
219
+ return LotaRuntimeConfigSchema.parse(config)
220
+ }
221
+
222
+ export function configureRuntimeConfig(config: ResolvedLotaRuntimeConfig): void {
223
+ runtimeConfig = config
224
+ }
225
+
226
+ export function getRuntimeConfig(): ResolvedLotaRuntimeConfig {
227
+ if (!runtimeConfig) {
228
+ throw new Error('Lota runtime config not configured. Call createLotaRuntime() first.')
229
+ }
230
+
231
+ return runtimeConfig
232
+ }
233
+
234
+ export function parseWorkerBootstrapEnv(env: Record<string, string | undefined>): WorkerBootstrapEnv {
235
+ return WORKER_BOOTSTRAP_ENV_SCHEMA.parse(env)
236
+ }
237
+
238
+ export type { LotaAgentFactoryRegistry, LotaWorkstreamConfig }
@@ -2,6 +2,7 @@ import type { ToolSet } from 'ai'
2
2
 
3
3
  import type { RecordIdRef } from '../db/record-id'
4
4
  import type { ReadableUploadMetadata } from '../services/attachment.service'
5
+ import type { LotaRuntimeWorkerExtensions } from './runtime-worker-registry'
5
6
 
6
7
  export interface LotaRuntimeBackgroundCursor {
7
8
  createdAt: Date
@@ -102,7 +103,7 @@ interface RuntimeExtensionsState {
102
103
  adapters: LotaRuntimeAdapters
103
104
  turnHooks: LotaRuntimeTurnHooks
104
105
  toolProviders: ToolSet
105
- extraWorkers: Record<string, unknown>
106
+ extraWorkers: LotaRuntimeWorkerExtensions
106
107
  }
107
108
 
108
109
  const EMPTY_TOOLS = {} as ToolSet
@@ -118,7 +119,7 @@ export function configureRuntimeExtensions(params?: {
118
119
  adapters?: LotaRuntimeAdapters
119
120
  turnHooks?: LotaRuntimeTurnHooks
120
121
  toolProviders?: ToolSet
121
- extraWorkers?: Record<string, unknown>
122
+ extraWorkers?: LotaRuntimeWorkerExtensions
122
123
  }): void {
123
124
  runtimeExtensionsState = {
124
125
  adapters: params?.adapters ?? {},
@@ -0,0 +1,47 @@
1
+ import { startContextCompactionWorker } from '../queues/context-compaction.queue'
2
+ import { scheduleRecurringConsolidation, startMemoryConsolidationWorker } from '../queues/memory-consolidation.queue'
3
+ import { startPostChatMemoryWorker } from '../queues/post-chat-memory.queue'
4
+ import { startRecentActivityTitleRefinementWorker } from '../queues/recent-activity-title-refinement.queue'
5
+ import { startRegularChatMemoryDigestWorker } from '../queues/regular-chat-memory-digest.queue'
6
+ import { startSkillExtractionWorker } from '../queues/skill-extraction.queue'
7
+ import { startWorkstreamTitleGenerationWorker } from '../queues/workstream-title-generation.queue'
8
+
9
+ export interface LotaRuntimeWorkerStartRegistry {
10
+ contextCompaction: typeof startContextCompactionWorker
11
+ memoryConsolidation: typeof startMemoryConsolidationWorker
12
+ postChatMemory: typeof startPostChatMemoryWorker
13
+ regularChatMemoryDigest: typeof startRegularChatMemoryDigestWorker
14
+ skillExtraction: typeof startSkillExtractionWorker
15
+ workstreamTitleGeneration: typeof startWorkstreamTitleGenerationWorker
16
+ recentActivityTitleRefinement: typeof startRecentActivityTitleRefinementWorker
17
+ }
18
+
19
+ export interface LotaRuntimeWorkerScheduleRegistry {
20
+ recurringConsolidation: typeof scheduleRecurringConsolidation
21
+ }
22
+
23
+ export interface LotaRuntimeWorkers {
24
+ start: LotaRuntimeWorkerStartRegistry & Record<string, unknown>
25
+ schedule: LotaRuntimeWorkerScheduleRegistry & Record<string, unknown>
26
+ }
27
+
28
+ export interface LotaRuntimeWorkerExtensions {
29
+ start?: Record<string, unknown>
30
+ schedule?: Record<string, unknown>
31
+ }
32
+
33
+ export function buildRuntimeWorkerRegistry(extraWorkers?: LotaRuntimeWorkerExtensions): LotaRuntimeWorkers {
34
+ return {
35
+ start: {
36
+ contextCompaction: startContextCompactionWorker,
37
+ memoryConsolidation: startMemoryConsolidationWorker,
38
+ postChatMemory: startPostChatMemoryWorker,
39
+ regularChatMemoryDigest: startRegularChatMemoryDigestWorker,
40
+ skillExtraction: startSkillExtractionWorker,
41
+ workstreamTitleGeneration: startWorkstreamTitleGenerationWorker,
42
+ recentActivityTitleRefinement: startRecentActivityTitleRefinementWorker,
43
+ ...extraWorkers?.start,
44
+ },
45
+ schedule: { recurringConsolidation: scheduleRecurringConsolidation, ...extraWorkers?.schedule },
46
+ }
47
+ }
@@ -1,7 +1,5 @@
1
- import { withMessageCreatedAt } from '@lota-sdk/shared/runtime/chat-message-metadata'
2
- import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
3
- import { ConsultTeamArgsSchema } from '@lota-sdk/shared/schemas/tools'
4
- import type { ConsultTeamResultData } from '@lota-sdk/shared/schemas/tools'
1
+ import { ConsultTeamArgsSchema, withMessageCreatedAt } from '@lota-sdk/shared'
2
+ import type { ChatMessage, ConsultTeamResultData } from '@lota-sdk/shared'
5
3
  import { convertToModelMessages, readUIMessageStream, tool as createTool } from 'ai'
6
4
 
7
5
  import { agentDisplayNames, teamConsultParticipants } from '../config/agent-defaults'
@@ -79,9 +77,14 @@ export interface CreateConsultTeamToolParams {
79
77
  }
80
78
 
81
79
  export function createConsultTeamTool(params: CreateConsultTeamToolParams) {
80
+ const participantNames = teamConsultParticipants
81
+ .map((agentId) => agentDisplayNames[agentId] ?? agentId)
82
+ .filter((value) => value.trim().length > 0)
83
+ const participantSummary =
84
+ participantNames.length > 0 ? participantNames.join(', ') : 'the configured specialist participants'
85
+
82
86
  return createTool({
83
- description:
84
- 'Consult the specialist team in parallel before replying. Use this when the answer benefits from structured executive input across product, engineering, finance, marketing, strategy, and mentorship.',
87
+ description: `Consult the specialist team in parallel before replying. Use this when the answer benefits from structured input across ${participantSummary}.`,
85
88
  inputSchema: ConsultTeamArgsSchema,
86
89
  execute: async function* ({ task }) {
87
90
  const uploadMetadataText = buildReadableUploadMetadataText(params.availableUploads)
@@ -1,10 +1,11 @@
1
- import { agentDisplayNames } from '../config/agent-defaults'
1
+ import { agentDisplayNames, getLeadAgentDisplayName } from '../config/agent-defaults'
2
2
 
3
3
  export function buildTeamConsultationResponseGuard(params: { agentId: string; task: string }) {
4
4
  const agentName = agentDisplayNames[params.agentId] ?? params.agentId
5
+ const leadAgentDisplayName = getLeadAgentDisplayName()
5
6
  return [
6
7
  '<team-consultation-agent-protocol>',
7
- '- You are participating in a structured internal team consultation led by Chief of Staff.',
8
+ `- You are participating in a structured internal team consultation led by ${leadAgentDisplayName}.`,
8
9
  `- Your role for this response is ${agentName}.`,
9
10
  '- Use markdown when it helps clarity.',
10
11
  '- Make the recommendation, explain key tradeoffs, and note the next decision.',
@@ -1,4 +1,4 @@
1
- import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
1
+ import type { ChatMessage } from '@lota-sdk/shared'
2
2
 
3
3
  export async function finalizeTurnRun(params: {
4
4
  serverRunId: string
@@ -16,49 +16,6 @@ function getAgentName(message: ChatMessageLike): string | undefined {
16
16
  return resolvedAgentName ? (agentDisplayNames[resolvedAgentName] ?? value.trim()) : value.trim()
17
17
  }
18
18
 
19
- function truncateTrackerText(value: string, maxChars: number): string {
20
- return value.length <= maxChars ? value : `${value.slice(0, maxChars).trimEnd()}...`
21
- }
22
-
23
- function toTrackerToolLine(part: ChatMessageLike['parts'][number]): string | null {
24
- if (typeof part !== 'object') return null
25
- const record = part as Record<string, unknown>
26
- const type = typeof record.type === 'string' ? record.type : null
27
- if (!type?.startsWith('tool-')) return null
28
-
29
- const toolName = type.slice('tool-'.length) || 'unknown'
30
- const state = typeof record.state === 'string' ? record.state : null
31
- const input = record.input
32
- const output = record.output
33
-
34
- const inputTask =
35
- input && typeof input === 'object' && typeof (input as Record<string, unknown>).task === 'string'
36
- ? truncateTrackerText(((input as Record<string, unknown>).task as string).trim(), 220)
37
- : null
38
- const outputSummary =
39
- output && typeof output === 'object' && typeof (output as Record<string, unknown>).summary === 'string'
40
- ? truncateTrackerText(((output as Record<string, unknown>).summary as string).trim(), 220)
41
- : null
42
- const outputResult =
43
- output && typeof output === 'object' && typeof (output as Record<string, unknown>).result === 'string'
44
- ? truncateTrackerText(((output as Record<string, unknown>).result as string).trim(), 220)
45
- : null
46
- const errorText = typeof record.errorText === 'string' ? truncateTrackerText(record.errorText.trim(), 220) : null
47
-
48
- if (state === 'output-error') {
49
- return errorText ? `Tool ${toolName} failed: ${errorText}` : `Tool ${toolName} failed.`
50
- }
51
-
52
- if (state !== 'output-available') {
53
- return inputTask ? `Tool ${toolName} ran for: ${inputTask}` : `Tool ${toolName} ran.`
54
- }
55
-
56
- if (outputSummary) return `Tool ${toolName} completed: ${outputSummary}`
57
- if (inputTask) return `Tool ${toolName} completed for: ${inputTask}`
58
- if (outputResult) return `Tool ${toolName} completed: ${outputResult}`
59
- return `Tool ${toolName} completed.`
60
- }
61
-
62
19
  export function extractMessageText(message: ChatMessageLike): string {
63
20
  return message.parts
64
21
  .flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [part.text] : []))
@@ -66,17 +23,6 @@ export function extractMessageText(message: ChatMessageLike): string {
66
23
  .trim()
67
24
  }
68
25
 
69
- export function extractTrackerMessageText(message: ChatMessageLike): string {
70
- const textParts = message.parts.flatMap((part) =>
71
- part.type === 'text' && typeof part.text === 'string' ? [part.text] : [],
72
- )
73
- const toolParts = message.parts
74
- .map((part) => toTrackerToolLine(part))
75
- .filter((value): value is string => Boolean(value))
76
-
77
- return [...textParts, ...toolParts].join('\n').trim()
78
- }
79
-
80
26
  export function toHistoryMessages(messages: ChatMessageLike[], maxItems = 24): WorkstreamHistoryMessage[] {
81
27
  return messages
82
28
  .map((message): WorkstreamHistoryMessage | null => {