@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.
- package/infrastructure/schema/00_workstream.surql +1 -0
- package/infrastructure/schema/02_execution_plan.surql +202 -52
- package/package.json +4 -87
- package/src/ai/index.ts +3 -0
- package/src/bifrost/bifrost.ts +94 -25
- package/src/bifrost/index.ts +1 -0
- package/src/config/agent-defaults.ts +30 -7
- package/src/config/constants.ts +0 -9
- package/src/config/debug-logger.ts +43 -0
- package/src/config/index.ts +5 -0
- package/src/config/model-constants.ts +8 -9
- package/src/config/workstream-defaults.ts +4 -0
- package/src/db/cursor-pagination.ts +2 -2
- package/src/db/index.ts +10 -0
- package/src/db/memory-store.ts +3 -71
- package/src/db/memory.ts +9 -15
- package/src/db/service.ts +42 -2
- package/src/db/tables.ts +9 -2
- package/src/document/index.ts +2 -0
- package/src/document/parsing.ts +0 -25
- package/src/embeddings/provider.ts +102 -22
- package/src/index.ts +15 -499
- package/src/queues/index.ts +10 -0
- package/src/redis/connection-accessor.ts +26 -0
- package/src/redis/connection.ts +1 -1
- package/src/redis/index.ts +9 -25
- package/src/redis/org-memory-lock.ts +1 -1
- package/src/redis/redis-lease-lock.ts +1 -1
- package/src/redis/stream-context.ts +54 -0
- package/src/runtime/agent-runtime-policy.ts +9 -5
- package/src/runtime/agent-stream-helpers.ts +6 -3
- package/src/runtime/agent-types.ts +1 -5
- package/src/runtime/approval-continuation.ts +68 -1
- package/src/runtime/chat-attachments.ts +1 -1
- package/src/runtime/chat-request-routing.ts +6 -2
- package/src/runtime/context-compaction-runtime.ts +2 -2
- package/src/runtime/context-compaction.ts +1 -1
- package/src/runtime/execution-plan.ts +22 -15
- package/src/runtime/index.ts +26 -0
- package/src/runtime/indexed-repositories-policy.ts +10 -10
- package/src/runtime/memory-pipeline.ts +0 -2
- package/src/runtime/runtime-config.ts +238 -0
- package/src/runtime/runtime-extensions.ts +3 -2
- package/src/runtime/runtime-worker-registry.ts +47 -0
- package/src/runtime/team-consultation-orchestrator.ts +9 -6
- package/src/runtime/team-consultation-prompts.ts +3 -2
- package/src/runtime/turn-lifecycle.ts +13 -5
- package/src/runtime/workstream-chat-helpers.ts +0 -54
- package/src/runtime/workstream-routing-policy.ts +3 -7
- package/src/runtime.ts +387 -0
- package/src/services/chat-attachments.service.ts +1 -1
- package/src/services/context-compaction.service.ts +1 -1
- package/src/services/document-chunk.service.ts +2 -2
- package/src/services/execution-plan.service.ts +584 -793
- package/src/services/index.ts +14 -0
- package/src/services/learned-skill.service.ts +82 -39
- package/src/services/memory.service.ts +5 -4
- package/src/services/mutating-approval.service.ts +1 -1
- package/src/services/organization-member.service.ts +1 -1
- package/src/services/organization.service.ts +1 -1
- package/src/services/plan-approval.service.ts +83 -0
- package/src/services/plan-artifact.service.ts +44 -0
- package/src/services/plan-builder.service.ts +61 -0
- package/src/services/plan-checkpoint.service.ts +53 -0
- package/src/services/plan-compiler.service.ts +81 -0
- package/src/services/plan-executor.service.ts +1624 -0
- package/src/services/plan-run.service.ts +422 -0
- package/src/services/plan-validator.service.ts +760 -0
- package/src/services/recent-activity-title.service.ts +1 -1
- package/src/services/recent-activity.service.ts +14 -16
- package/src/services/user.service.ts +2 -2
- package/src/services/workstream-message.service.ts +2 -3
- package/src/services/workstream-title.service.ts +1 -1
- package/src/services/workstream-turn-preparation.ts +156 -59
- package/src/services/workstream-turn.ts +26 -1
- package/src/services/workstream.service.ts +35 -9
- package/src/services/workstream.types.ts +1 -0
- package/src/storage/attachment-parser.ts +1 -1
- package/src/storage/attachment-storage.service.ts +11 -10
- package/src/storage/generated-document-storage.service.ts +7 -6
- package/src/storage/index.ts +10 -0
- package/src/system-agents/delegated-agent-factory.ts +78 -29
- package/src/system-agents/index.ts +4 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
- package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
- package/src/system-agents/skill-extractor.agent.ts +1 -1
- package/src/system-agents/skill-manager.agent.ts +2 -4
- package/src/system-agents/title-generator.agent.ts +2 -2
- package/src/tools/execution-plan.tool.ts +22 -48
- package/src/tools/firecrawl-client.ts +2 -2
- package/src/tools/index.ts +12 -0
- package/src/tools/log-hello-world.tool.ts +17 -0
- package/src/tools/research-topic.tool.ts +1 -1
- package/src/tools/team-think.tool.ts +1 -1
- package/src/tools/user-questions.tool.ts +2 -2
- package/src/utils/index.ts +6 -0
- package/src/workers/bootstrap.ts +8 -16
- package/src/workers/index.ts +7 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
- package/src/workers/skill-extraction.runner.ts +3 -3
- package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
- package/src/workers/utils/repo-structure-extractor.ts +2 -5
- package/src/workers/utils/repomix-file-sections.ts +42 -0
- package/src/config/env-shapes.ts +0 -121
- 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 !==
|
|
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 ===
|
|
152
|
-
includeProceedInOnboarding: params.onboardingActive && params.agentId ===
|
|
153
|
-
includeGithubIntegration: params.onboardingActive && params.agentId ===
|
|
154
|
-
includeIndexRepositoryByURL: params.onboardingActive && params.agentId ===
|
|
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
|
|
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: [
|
|
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,9 +1,49 @@
|
|
|
1
|
-
import type { ChatMessage } from '@lota-sdk/shared
|
|
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,14 +1,18 @@
|
|
|
1
|
-
import type { ChatMessage } from '@lota-sdk/shared
|
|
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
|
-
|
|
21
|
+
interface HelperModelRuntime {
|
|
22
22
|
generateHelperStructured<T>(params: GenerateHelperStructuredParams<T>): Promise<T>
|
|
23
23
|
generateHelperText(params: GenerateHelperTextParams): Promise<string>
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
interface CreateContextCompactionRuntimeDeps {
|
|
27
27
|
helperModelRuntime: HelperModelRuntime
|
|
28
28
|
now?: () => number
|
|
29
29
|
randomId?: () => string
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import type { SerializableExecutionPlan } from '@lota-sdk/shared
|
|
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
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- If the
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 ??=
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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(
|
|
27
|
-
|
|
24
|
+
export function emptyAgentContextMap(
|
|
25
|
+
roster: readonly IndexedRepoAgentName[] = agentRoster,
|
|
26
|
+
): Record<IndexedRepoAgentName, string> {
|
|
27
|
+
return Object.fromEntries(roster.map((agentId) => [agentId, '']))
|
|
28
28
|
}
|
|
@@ -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:
|
|
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?:
|
|
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
|
+
}
|