@lota-sdk/core 0.1.9 → 0.1.12

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 (105) hide show
  1. package/infrastructure/schema/00_workstream.surql +1 -0
  2. package/infrastructure/schema/02_execution_plan.surql +202 -52
  3. package/package.json +4 -87
  4. package/src/ai/index.ts +3 -0
  5. package/src/bifrost/bifrost.ts +94 -25
  6. package/src/bifrost/index.ts +1 -0
  7. package/src/config/agent-defaults.ts +30 -7
  8. package/src/config/constants.ts +0 -9
  9. package/src/config/debug-logger.ts +43 -0
  10. package/src/config/index.ts +5 -0
  11. package/src/config/model-constants.ts +8 -9
  12. package/src/config/workstream-defaults.ts +4 -0
  13. package/src/db/cursor-pagination.ts +2 -2
  14. package/src/db/index.ts +10 -0
  15. package/src/db/memory-store.ts +3 -71
  16. package/src/db/memory.ts +9 -15
  17. package/src/db/service.ts +42 -2
  18. package/src/db/tables.ts +9 -2
  19. package/src/document/index.ts +2 -0
  20. package/src/document/parsing.ts +0 -25
  21. package/src/embeddings/provider.ts +102 -22
  22. package/src/index.ts +15 -499
  23. package/src/queues/index.ts +10 -0
  24. package/src/redis/connection-accessor.ts +26 -0
  25. package/src/redis/connection.ts +1 -1
  26. package/src/redis/index.ts +9 -25
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +1 -1
  29. package/src/redis/stream-context.ts +54 -0
  30. package/src/runtime/agent-runtime-policy.ts +9 -5
  31. package/src/runtime/agent-stream-helpers.ts +6 -3
  32. package/src/runtime/agent-types.ts +1 -5
  33. package/src/runtime/approval-continuation.ts +68 -1
  34. package/src/runtime/chat-attachments.ts +1 -1
  35. package/src/runtime/chat-request-routing.ts +6 -2
  36. package/src/runtime/context-compaction-runtime.ts +2 -2
  37. package/src/runtime/context-compaction.ts +1 -1
  38. package/src/runtime/execution-plan.ts +22 -15
  39. package/src/runtime/index.ts +26 -0
  40. package/src/runtime/indexed-repositories-policy.ts +10 -10
  41. package/src/runtime/memory-pipeline.ts +0 -2
  42. package/src/runtime/runtime-config.ts +238 -0
  43. package/src/runtime/runtime-extensions.ts +3 -2
  44. package/src/runtime/runtime-worker-registry.ts +47 -0
  45. package/src/runtime/team-consultation-orchestrator.ts +9 -6
  46. package/src/runtime/team-consultation-prompts.ts +3 -2
  47. package/src/runtime/turn-lifecycle.ts +13 -5
  48. package/src/runtime/workstream-chat-helpers.ts +0 -54
  49. package/src/runtime/workstream-routing-policy.ts +3 -7
  50. package/src/runtime.ts +387 -0
  51. package/src/services/chat-attachments.service.ts +1 -1
  52. package/src/services/context-compaction.service.ts +1 -1
  53. package/src/services/document-chunk.service.ts +2 -2
  54. package/src/services/execution-plan.service.ts +584 -793
  55. package/src/services/index.ts +14 -0
  56. package/src/services/learned-skill.service.ts +82 -39
  57. package/src/services/memory.service.ts +5 -4
  58. package/src/services/mutating-approval.service.ts +1 -1
  59. package/src/services/organization-member.service.ts +1 -1
  60. package/src/services/organization.service.ts +1 -1
  61. package/src/services/plan-approval.service.ts +83 -0
  62. package/src/services/plan-artifact.service.ts +44 -0
  63. package/src/services/plan-builder.service.ts +61 -0
  64. package/src/services/plan-checkpoint.service.ts +53 -0
  65. package/src/services/plan-compiler.service.ts +81 -0
  66. package/src/services/plan-executor.service.ts +1624 -0
  67. package/src/services/plan-run.service.ts +422 -0
  68. package/src/services/plan-validator.service.ts +760 -0
  69. package/src/services/recent-activity-title.service.ts +1 -1
  70. package/src/services/recent-activity.service.ts +14 -16
  71. package/src/services/user.service.ts +2 -2
  72. package/src/services/workstream-message.service.ts +2 -3
  73. package/src/services/workstream-title.service.ts +1 -1
  74. package/src/services/workstream-turn-preparation.ts +156 -59
  75. package/src/services/workstream-turn.ts +26 -1
  76. package/src/services/workstream.service.ts +35 -9
  77. package/src/services/workstream.types.ts +1 -0
  78. package/src/storage/attachment-parser.ts +1 -1
  79. package/src/storage/attachment-storage.service.ts +11 -10
  80. package/src/storage/generated-document-storage.service.ts +7 -6
  81. package/src/storage/index.ts +10 -0
  82. package/src/system-agents/delegated-agent-factory.ts +78 -29
  83. package/src/system-agents/index.ts +4 -0
  84. package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
  85. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  86. package/src/system-agents/skill-extractor.agent.ts +1 -1
  87. package/src/system-agents/skill-manager.agent.ts +2 -4
  88. package/src/system-agents/title-generator.agent.ts +2 -2
  89. package/src/tools/execution-plan.tool.ts +22 -48
  90. package/src/tools/firecrawl-client.ts +2 -2
  91. package/src/tools/index.ts +12 -0
  92. package/src/tools/log-hello-world.tool.ts +17 -0
  93. package/src/tools/research-topic.tool.ts +1 -1
  94. package/src/tools/team-think.tool.ts +1 -1
  95. package/src/tools/user-questions.tool.ts +2 -2
  96. package/src/utils/index.ts +6 -0
  97. package/src/workers/bootstrap.ts +8 -16
  98. package/src/workers/index.ts +7 -0
  99. package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
  100. package/src/workers/skill-extraction.runner.ts +3 -3
  101. package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
  102. package/src/workers/utils/repo-structure-extractor.ts +2 -5
  103. package/src/workers/utils/repomix-file-sections.ts +42 -0
  104. package/src/config/env-shapes.ts +0 -121
  105. 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,9 +1,49 @@
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')
5
7
  }
6
8
 
9
+ export interface ApprovalContinuationResponse {
10
+ approvalId: string
11
+ approved: boolean
12
+ comments?: string
13
+ requiredEdits: string[]
14
+ }
15
+
16
+ export function readApprovalContinuationResponse(message: ChatMessage): ApprovalContinuationResponse | null {
17
+ for (const part of message.parts) {
18
+ if (!('state' in part) || (part as { state: string }).state !== 'approval-responded') continue
19
+ const approval = (part as { approval?: unknown }).approval
20
+ if (!approval || typeof approval !== 'object') continue
21
+
22
+ const record = approval as {
23
+ id?: unknown
24
+ approved?: unknown
25
+ reason?: unknown
26
+ comments?: unknown
27
+ requiredEdits?: unknown
28
+ }
29
+ if (typeof record.id !== 'string' || typeof record.approved !== 'boolean') continue
30
+
31
+ const requiredEdits = Array.isArray(record.requiredEdits)
32
+ ? record.requiredEdits.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
33
+ : []
34
+ const comments =
35
+ typeof record.comments === 'string' && record.comments.trim().length > 0
36
+ ? record.comments.trim()
37
+ : typeof record.reason === 'string' && record.reason.trim().length > 0
38
+ ? record.reason.trim()
39
+ : undefined
40
+
41
+ return { approvalId: record.id, approved: record.approved, comments, requiredEdits }
42
+ }
43
+
44
+ return null
45
+ }
46
+
7
47
  export function isApprovalContinuationRequest(messages: ChatMessage[]): boolean {
8
48
  const lastAssistant = [...messages].reverse().find((message) => message.role === 'assistant')
9
49
  if (!lastAssistant) return false
@@ -14,3 +54,30 @@ export function isApprovalContinuationRequest(messages: ChatMessage[]): boolean
14
54
 
15
55
  return hasApprovalRespondedParts(lastAssistant)
16
56
  }
57
+
58
+ const PLAN_TOOL_NAMES = new Set([
59
+ 'createExecutionPlan',
60
+ 'replaceExecutionPlan',
61
+ 'submitExecutionNodeResult',
62
+ 'getActiveExecutionPlan',
63
+ 'resumeExecutionPlanRun',
64
+ ])
65
+
66
+ function isPlanApprovalId(value: unknown): value is string {
67
+ return typeof value === 'string' && value.startsWith(`${TABLES.PLAN_APPROVAL}:`)
68
+ }
69
+
70
+ export function isNativeToolApprovalRequest(messages: ChatMessage[]): boolean {
71
+ const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant')
72
+ if (!lastAssistant) return false
73
+
74
+ return lastAssistant.parts.some((part) => {
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
78
+ const type = (part as { type?: string }).type
79
+ if (typeof type !== 'string' || !type.startsWith('tool-')) return false
80
+ const toolName = type.slice(5)
81
+ return !PLAN_TOOL_NAMES.has(toolName)
82
+ })
83
+ }
@@ -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,14 +1,18 @@
1
- import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
1
+ import type { ChatMessage } from '@lota-sdk/shared'
2
2
 
3
- import { isApprovalContinuationRequest } from './approval-continuation'
3
+ import { isApprovalContinuationRequest, isNativeToolApprovalRequest } from './approval-continuation'
4
4
 
5
5
  export type RoutedChatRequest =
6
6
  | { kind: 'approval-continuation'; approvalMessages: ChatMessage[] }
7
+ | { kind: 'native-tool-approval'; messages: ChatMessage[] }
7
8
  | { kind: 'turn'; inputMessage: ChatMessage }
8
9
  | { kind: 'invalid'; message: string }
9
10
 
10
11
  export function routeWorkstreamChatMessages(messages: ChatMessage[]): RoutedChatRequest {
11
12
  if (isApprovalContinuationRequest(messages)) {
13
+ if (isNativeToolApprovalRequest(messages)) {
14
+ return { kind: 'native-tool-approval', messages }
15
+ }
12
16
  return { kind: 'approval-continuation', approvalMessages: messages }
13
17
  }
14
18
 
@@ -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,14 +1,13 @@
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
- - Before doing multi-step work, create an execution plan instead of tracking steps only in prose.
5
- - Keep plans short and operational. Prefer 2 to 7 tasks unless the work truly needs more.
6
- - Use execution-plan tools to create, replace, update, inspect, and restart the plan.
7
- - Only one execution-plan task may be active at a time.
8
- - Every task should have a concrete rationale and a concise output summary when work happens.
9
- - Treat prior task output summaries and carried tasks in <execution-plan-state> as observed facts.
10
- - If the ordered steps materially change, replace the plan instead of silently rewriting it in prose.
11
- - If the active plan is blocked, do not continue blindly. Replan, restart a task, ask for user input, or abort.
4
+ - Before doing multi-step work, create a contract-driven execution plan instead of tracking steps only in prose.
5
+ - Plans are graph-capable workflow contracts. Every execution node must define objective, instructions, deliverables, success criteria, completion checks, retry policy, failure policy, and tool/context policy.
6
+ - The runtime executor owns lifecycle truth. Do not claim that a node is complete until submitExecutionNodeResult succeeds.
7
+ - Use execution-plan tools to create, replace, inspect, submit node results, and resume runs.
8
+ - Treat the active execution run in <execution-plan-state> as authoritative. Do not mutate run or node status in prose.
9
+ - Work only on nodes that are active or explicitly ready for your executor. If a node is awaiting human input or approval, stop and let the runtime resume it.
10
+ - If the graph, contracts, or success criteria materially change, replace the plan instead of silently drifting.
12
11
  </execution-plan-protocol>`
13
12
 
14
13
  export function formatExecutionPlanForPrompt(plan: SerializableExecutionPlan | null | undefined): string | undefined {
@@ -16,11 +15,11 @@ export function formatExecutionPlanForPrompt(plan: SerializableExecutionPlan | n
16
15
 
17
16
  const payload = {
18
17
  policy: {
19
- activePlanIsAuthoritative: true,
20
- priorOutputSummariesAreObservedFacts: true,
21
- singleActiveTask: true,
22
- explicitReplanRequired: true,
23
- failureBudget: 2,
18
+ executorOwnsLifecycleTruth: true,
19
+ contractDrivenExecution: true,
20
+ humanGatesAreDurable: true,
21
+ artifactsAreFirstClassOutputs: true,
22
+ checkpointRecoveryEnabled: true,
24
23
  },
25
24
  plan,
26
25
  }
@@ -43,16 +42,24 @@ export function createExecutionPlanInstructionSectionCache(params: {
43
42
  disabled?: boolean
44
43
  loadPlan: () => Promise<SerializableExecutionPlan | null | undefined>
45
44
  }) {
45
+ let planPromise: Promise<SerializableExecutionPlan | null | undefined> | null = null
46
46
  let sectionsPromise: Promise<string[] | undefined> | null = null
47
47
 
48
48
  return {
49
49
  invalidate() {
50
+ planPromise = null
50
51
  sectionsPromise = null
51
52
  },
53
+ async getPlan(): Promise<SerializableExecutionPlan | null | undefined> {
54
+ if (params.disabled) return undefined
55
+
56
+ planPromise ??= params.loadPlan()
57
+ return await planPromise
58
+ },
52
59
  async getSections(): Promise<string[] | undefined> {
53
60
  if (params.disabled) return undefined
54
61
 
55
- sectionsPromise ??= params.loadPlan().then((plan) => buildExecutionPlanInstructionSections(plan))
62
+ sectionsPromise ??= this.getPlan().then((plan) => buildExecutionPlanInstructionSections(plan))
56
63
  return await sectionsPromise
57
64
  },
58
65
  }
@@ -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
+ }