@lota-sdk/core 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infrastructure/schema/00_workstream.surql +55 -0
- package/infrastructure/schema/01_memory.surql +47 -0
- package/infrastructure/schema/02_execution_plan.surql +62 -0
- package/infrastructure/schema/03_learned_skill.surql +32 -0
- package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
- package/package.json +128 -0
- package/src/ai/definitions.ts +308 -0
- package/src/bifrost/bifrost.ts +256 -0
- package/src/config/agent-defaults.ts +99 -0
- package/src/config/constants.ts +33 -0
- package/src/config/env-shapes.ts +122 -0
- package/src/config/logger.ts +29 -0
- package/src/config/model-constants.ts +31 -0
- package/src/config/search.ts +17 -0
- package/src/config/workstream-defaults.ts +68 -0
- package/src/db/base.service.ts +55 -0
- package/src/db/cursor-pagination.ts +73 -0
- package/src/db/memory-query-builder.ts +207 -0
- package/src/db/memory-store.helpers.ts +118 -0
- package/src/db/memory-store.rows.ts +29 -0
- package/src/db/memory-store.ts +974 -0
- package/src/db/memory-types.ts +193 -0
- package/src/db/memory.ts +505 -0
- package/src/db/record-id.ts +78 -0
- package/src/db/service.ts +932 -0
- package/src/db/startup.ts +152 -0
- package/src/db/tables.ts +20 -0
- package/src/document/org-document-chunking.ts +224 -0
- package/src/document/parsing.ts +40 -0
- package/src/embeddings/provider.ts +76 -0
- package/src/index.ts +302 -0
- package/src/queues/context-compaction.queue.ts +82 -0
- package/src/queues/document-processor.queue.ts +118 -0
- package/src/queues/memory-consolidation.queue.ts +65 -0
- package/src/queues/post-chat-memory.queue.ts +128 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
- package/src/queues/regular-chat-memory-digest.config.ts +12 -0
- package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
- package/src/queues/skill-extraction.config.ts +9 -0
- package/src/queues/skill-extraction.queue.ts +62 -0
- package/src/redis/connection.ts +176 -0
- package/src/redis/index.ts +30 -0
- package/src/redis/org-memory-lock.ts +43 -0
- package/src/redis/redis-lease-lock.ts +158 -0
- package/src/runtime/agent-contract.ts +1 -0
- package/src/runtime/agent-prompt-context.ts +119 -0
- package/src/runtime/agent-runtime-policy.ts +192 -0
- package/src/runtime/agent-stream-helpers.ts +117 -0
- package/src/runtime/agent-types.ts +22 -0
- package/src/runtime/approval-continuation.ts +16 -0
- package/src/runtime/chat-attachments.ts +46 -0
- package/src/runtime/chat-message.ts +10 -0
- package/src/runtime/chat-request-routing.ts +21 -0
- package/src/runtime/chat-run-orchestration.ts +25 -0
- package/src/runtime/chat-run-registry.ts +20 -0
- package/src/runtime/chat-types.ts +18 -0
- package/src/runtime/context-compaction-constants.ts +11 -0
- package/src/runtime/context-compaction-runtime.ts +86 -0
- package/src/runtime/context-compaction.ts +909 -0
- package/src/runtime/execution-plan.ts +59 -0
- package/src/runtime/helper-model.ts +405 -0
- package/src/runtime/indexed-repositories-policy.ts +28 -0
- package/src/runtime/instruction-sections.ts +8 -0
- package/src/runtime/llm-content.ts +71 -0
- package/src/runtime/memory-block.ts +264 -0
- package/src/runtime/memory-digest-policy.ts +14 -0
- package/src/runtime/memory-format.ts +8 -0
- package/src/runtime/memory-pipeline.ts +570 -0
- package/src/runtime/memory-prompts-fact.ts +47 -0
- package/src/runtime/memory-prompts-parse.ts +3 -0
- package/src/runtime/memory-prompts-update.ts +37 -0
- package/src/runtime/memory-scope.ts +43 -0
- package/src/runtime/plugin-types.ts +10 -0
- package/src/runtime/retrieval-adapters.ts +25 -0
- package/src/runtime/retrieval-pipeline.ts +3 -0
- package/src/runtime/runtime-extensions.ts +154 -0
- package/src/runtime/skill-extraction-policy.ts +3 -0
- package/src/runtime/team-consultation-orchestrator.ts +245 -0
- package/src/runtime/team-consultation-prompts.ts +32 -0
- package/src/runtime/title-helpers.ts +12 -0
- package/src/runtime/turn-lifecycle.ts +28 -0
- package/src/runtime/workstream-chat-helpers.ts +187 -0
- package/src/runtime/workstream-routing-policy.ts +301 -0
- package/src/runtime/workstream-state.ts +261 -0
- package/src/services/attachment.service.ts +159 -0
- package/src/services/chat-attachments.service.ts +17 -0
- package/src/services/chat-run-registry.service.ts +3 -0
- package/src/services/context-compaction-runtime.ts +13 -0
- package/src/services/context-compaction.service.ts +115 -0
- package/src/services/document-chunk.service.ts +141 -0
- package/src/services/execution-plan.service.ts +890 -0
- package/src/services/learned-skill.service.ts +328 -0
- package/src/services/memory-assessment.service.ts +43 -0
- package/src/services/memory.service.ts +807 -0
- package/src/services/memory.utils.ts +84 -0
- package/src/services/mutating-approval.service.ts +110 -0
- package/src/services/recent-activity-title.service.ts +74 -0
- package/src/services/recent-activity.service.ts +397 -0
- package/src/services/workstream-change-tracker.service.ts +313 -0
- package/src/services/workstream-message.service.ts +283 -0
- package/src/services/workstream-title.service.ts +58 -0
- package/src/services/workstream-turn-preparation.ts +1340 -0
- package/src/services/workstream-turn.ts +37 -0
- package/src/services/workstream.service.ts +854 -0
- package/src/services/workstream.types.ts +118 -0
- package/src/storage/attachment-parser.ts +101 -0
- package/src/storage/attachment-storage.service.ts +391 -0
- package/src/storage/attachments.types.ts +11 -0
- package/src/storage/attachments.utils.ts +58 -0
- package/src/storage/generated-document-storage.service.ts +55 -0
- package/src/system-agents/agent-result.ts +27 -0
- package/src/system-agents/context-compacter.agent.ts +46 -0
- package/src/system-agents/delegated-agent-factory.ts +177 -0
- package/src/system-agents/helper-agent-options.ts +20 -0
- package/src/system-agents/memory-reranker.agent.ts +38 -0
- package/src/system-agents/memory.agent.ts +58 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
- package/src/system-agents/researcher.agent.ts +34 -0
- package/src/system-agents/skill-extractor.agent.ts +88 -0
- package/src/system-agents/skill-manager.agent.ts +80 -0
- package/src/system-agents/title-generator.agent.ts +42 -0
- package/src/system-agents/workstream-tracker.agent.ts +58 -0
- package/src/tools/execution-plan.tool.ts +163 -0
- package/src/tools/fetch-webpage.tool.ts +132 -0
- package/src/tools/firecrawl-client.ts +12 -0
- package/src/tools/memory-block.tool.ts +55 -0
- package/src/tools/read-file-parts.tool.ts +80 -0
- package/src/tools/remember-memory.tool.ts +85 -0
- package/src/tools/research-topic.tool.ts +15 -0
- package/src/tools/search-tools.ts +55 -0
- package/src/tools/search-web.tool.ts +175 -0
- package/src/tools/team-think.tool.ts +125 -0
- package/src/tools/tool-contract.ts +21 -0
- package/src/tools/user-questions.tool.ts +18 -0
- package/src/utils/async.ts +50 -0
- package/src/utils/date-time.ts +34 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/errors.ts +28 -0
- package/src/utils/hono-error-handler.ts +71 -0
- package/src/utils/string.ts +51 -0
- package/src/workers/bootstrap.ts +44 -0
- package/src/workers/memory-consolidation.worker.ts +318 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
- package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
- package/src/workers/skill-extraction.runner.ts +331 -0
- package/src/workers/skill-extraction.worker.ts +22 -0
- package/src/workers/utils/repo-indexer-chunker.ts +331 -0
- package/src/workers/utils/repo-structure-extractor.ts +645 -0
- package/src/workers/utils/repomix-process-concurrency.ts +65 -0
- package/src/workers/utils/sandbox-error.ts +5 -0
- package/src/workers/worker-utils.ts +182 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { toOptionalIsoDateTimeString } from '../utils/date-time'
|
|
2
|
+
|
|
3
|
+
type StructuredProfile = Record<string, unknown>
|
|
4
|
+
|
|
5
|
+
interface AgentPromptContext {
|
|
6
|
+
systemWorkspaceDetails: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface DomainEventLike {
|
|
10
|
+
actor?: string
|
|
11
|
+
action?: string
|
|
12
|
+
category?: string
|
|
13
|
+
significance?: string
|
|
14
|
+
summary?: string
|
|
15
|
+
createdAt?: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface BuildAgentPromptContextParams {
|
|
19
|
+
workspaceName?: string
|
|
20
|
+
summaryBlock?: string
|
|
21
|
+
structuredProfile?: StructuredProfile
|
|
22
|
+
promptSummary?: string
|
|
23
|
+
userName?: string | null
|
|
24
|
+
recentDomainEvents: DomainEventLike[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeSummaryBlock(block?: string): string {
|
|
28
|
+
const normalized = block?.trim()
|
|
29
|
+
return normalized && normalized.length > 0 ? normalized : 'No workspace profile summary has been recorded yet.'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeUserName(name?: string | null): string {
|
|
33
|
+
const normalized = name?.trim()
|
|
34
|
+
return normalized && normalized.length > 0 ? normalized : 'Unknown'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeWorkspaceName(name?: string): string {
|
|
38
|
+
const normalized = name?.trim()
|
|
39
|
+
return normalized && normalized.length > 0 ? normalized : 'Workspace'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasStructuredProfileData(profile?: StructuredProfile): boolean {
|
|
43
|
+
return Boolean(profile && Object.keys(profile).length > 0)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatStructuredProfile(profile?: StructuredProfile): string {
|
|
47
|
+
if (!hasStructuredProfileData(profile)) {
|
|
48
|
+
return 'No structured workspace profile has been recorded yet.'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return JSON.stringify(profile, null, 2)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatDomainEvent(event: DomainEventLike, index: number): string {
|
|
55
|
+
const summary = typeof event.summary === 'string' && event.summary.trim().length > 0 ? event.summary.trim() : null
|
|
56
|
+
if (summary) {
|
|
57
|
+
const createdAt = toOptionalIsoDateTimeString(event.createdAt)
|
|
58
|
+
return createdAt ? `${index + 1}. ${summary} | createdAt=${createdAt}` : `${index + 1}. ${summary}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const createdAt = toOptionalIsoDateTimeString(event.createdAt)
|
|
62
|
+
const segments = [`${index + 1}.`]
|
|
63
|
+
if (typeof event.actor === 'string' && event.actor.trim().length > 0) {
|
|
64
|
+
segments.push(`actor=${event.actor}`)
|
|
65
|
+
}
|
|
66
|
+
if (typeof event.action === 'string' && event.action.trim().length > 0) {
|
|
67
|
+
segments.push(`action=${event.action}`)
|
|
68
|
+
}
|
|
69
|
+
if (typeof event.category === 'string' && event.category.trim().length > 0) {
|
|
70
|
+
segments.push(`category=${event.category}`)
|
|
71
|
+
}
|
|
72
|
+
if (typeof event.significance === 'string' && event.significance.trim().length > 0) {
|
|
73
|
+
segments.push(`significance=${event.significance}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (createdAt) {
|
|
77
|
+
segments.push(`createdAt=${createdAt}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (segments.length === 1) {
|
|
81
|
+
segments.push('No structured event details.')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return segments.join(' | ')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatRecentDomainEvents(events: DomainEventLike[]): string {
|
|
88
|
+
if (events.length === 0) {
|
|
89
|
+
return 'No domain events recorded yet.'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return events.map((event, index) => formatDomainEvent(event, index)).join('\n')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function buildAgentPromptContext(params: BuildAgentPromptContextParams): AgentPromptContext {
|
|
96
|
+
const workspaceName = normalizeWorkspaceName(params.workspaceName)
|
|
97
|
+
const summaryBlock = normalizeSummaryBlock(params.summaryBlock)
|
|
98
|
+
const structuredProfile = formatStructuredProfile(params.structuredProfile)
|
|
99
|
+
const promptSummary = params.promptSummary?.trim()
|
|
100
|
+
const userName = normalizeUserName(params.userName)
|
|
101
|
+
const recentDomainEvents = formatRecentDomainEvents(params.recentDomainEvents)
|
|
102
|
+
|
|
103
|
+
const workspaceDetailsBlock = [
|
|
104
|
+
'<workspace-profile>',
|
|
105
|
+
`name=${workspaceName}`,
|
|
106
|
+
'summary-block:',
|
|
107
|
+
summaryBlock,
|
|
108
|
+
'structured-profile-json:',
|
|
109
|
+
structuredProfile,
|
|
110
|
+
...(promptSummary ? ['prompt-summary:', promptSummary] : []),
|
|
111
|
+
'</workspace-profile>',
|
|
112
|
+
].join('\n')
|
|
113
|
+
|
|
114
|
+
const userDetailsBlock = ['<user-details>', `name=${userName}`, '</user-details>'].join('\n')
|
|
115
|
+
|
|
116
|
+
const domainEventsBlock = ['<domain-events>', 'last=5', recentDomainEvents, '</domain-events>'].join('\n')
|
|
117
|
+
|
|
118
|
+
return { systemWorkspaceDetails: [workspaceDetailsBlock, userDetailsBlock, domainEventsBlock].join('\n\n') }
|
|
119
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { ChatMode } from './agent-types'
|
|
2
|
+
import { resolveReasoningProfile } from './workstream-routing-policy'
|
|
3
|
+
import type { ReasoningProfileName } from './workstream-routing-policy'
|
|
4
|
+
|
|
5
|
+
export interface AgentRuntimeConfig<TAgent extends string> {
|
|
6
|
+
id: TAgent
|
|
7
|
+
displayName: string
|
|
8
|
+
mode: ChatMode
|
|
9
|
+
extraInstructions?: string
|
|
10
|
+
maxSteps: number
|
|
11
|
+
reasoningProfile: ReasoningProfileName
|
|
12
|
+
toolCallBudget: number
|
|
13
|
+
maxInputTokensHint: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AgentToolPolicy<TSkill extends PropertyKey> {
|
|
17
|
+
resolvedMode: ChatMode
|
|
18
|
+
skills: TSkill[]
|
|
19
|
+
includeMemorySearch: boolean
|
|
20
|
+
includeConversationSearch: boolean
|
|
21
|
+
includeMemoryRemember: boolean
|
|
22
|
+
includeOrgActionSearch: boolean
|
|
23
|
+
includeMemoryBlockAppend: boolean
|
|
24
|
+
includeReadFileParts: boolean
|
|
25
|
+
includeInspectWebsite: boolean
|
|
26
|
+
includeProceedInOnboarding: boolean
|
|
27
|
+
includeGithubIntegration: boolean
|
|
28
|
+
includeIndexRepositoryByURL: boolean
|
|
29
|
+
includeIndexedRepository: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toChatMode(workstreamMode: 'direct' | 'group'): ChatMode {
|
|
33
|
+
return workstreamMode === 'direct' ? 'fixedWorkstreamMode' : 'workstreamMode'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toMemoryBlockSection(memoryBlock: string | undefined): string | undefined {
|
|
37
|
+
if (memoryBlock === undefined) return undefined
|
|
38
|
+
const content = memoryBlock.trim()
|
|
39
|
+
return ['<memory-block>', content, '</memory-block>'].join('\n')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveActiveAgentSkills<TAgent extends string, TSkill extends PropertyKey>(params: {
|
|
43
|
+
agentId: TAgent
|
|
44
|
+
workstreamMode: 'direct' | 'group'
|
|
45
|
+
mode?: ChatMode
|
|
46
|
+
onboardingActive: boolean
|
|
47
|
+
linearInstalled: boolean
|
|
48
|
+
getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
|
|
49
|
+
}): TSkill[] {
|
|
50
|
+
const mode = params.mode ?? toChatMode(params.workstreamMode)
|
|
51
|
+
const baseSkills = params
|
|
52
|
+
.getAgentSkills(params.agentId, mode)
|
|
53
|
+
.filter((skill) => (params.linearInstalled ? true : skill !== ('linear' as TSkill)))
|
|
54
|
+
|
|
55
|
+
if (!params.onboardingActive) {
|
|
56
|
+
return baseSkills
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (params.agentId !== ('chief' as TAgent)) {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return baseSkills.filter((skill) => skill === ('asking-user-questions' as TSkill))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildAgentRuntimeConfig<TAgent extends string, TSkill extends PropertyKey>(params: {
|
|
67
|
+
agentId: TAgent
|
|
68
|
+
displayNameByAgent: Record<TAgent, string>
|
|
69
|
+
workstreamMode: 'direct' | 'group'
|
|
70
|
+
mode?: ChatMode
|
|
71
|
+
skills?: TSkill[]
|
|
72
|
+
onboardingActive: boolean
|
|
73
|
+
linearInstalled: boolean
|
|
74
|
+
reasoningProfile?: ReasoningProfileName
|
|
75
|
+
systemWorkspaceDetails?: string
|
|
76
|
+
preSeededMemoriesSection?: string
|
|
77
|
+
retrievedKnowledgeSection?: string
|
|
78
|
+
workstreamMemoryBlock?: string
|
|
79
|
+
workstreamStateSection?: string
|
|
80
|
+
responseGuardSection?: string
|
|
81
|
+
learnedSkillsSection?: string
|
|
82
|
+
additionalInstructionSections?: string[]
|
|
83
|
+
getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
|
|
84
|
+
buildGlobalRuleInstructionSection: () => string
|
|
85
|
+
buildSkillInstructionSection: (skills: TSkill[]) => string
|
|
86
|
+
buildOnboardingPromptSection: () => string
|
|
87
|
+
}): AgentRuntimeConfig<TAgent> {
|
|
88
|
+
const mode = params.mode ?? toChatMode(params.workstreamMode)
|
|
89
|
+
const profile = resolveReasoningProfile({ message: '', explicitProfile: params.reasoningProfile ?? 'standard' })
|
|
90
|
+
const rulesSection = params.buildGlobalRuleInstructionSection()
|
|
91
|
+
const skillsSection =
|
|
92
|
+
params.skills && params.skills.length > 0 ? params.buildSkillInstructionSection(params.skills) : ''
|
|
93
|
+
const onboardingPromptSection = params.onboardingActive ? params.buildOnboardingPromptSection() : ''
|
|
94
|
+
const instructionSections = [
|
|
95
|
+
rulesSection,
|
|
96
|
+
skillsSection,
|
|
97
|
+
params.learnedSkillsSection?.trim(),
|
|
98
|
+
onboardingPromptSection,
|
|
99
|
+
params.systemWorkspaceDetails?.trim(),
|
|
100
|
+
params.preSeededMemoriesSection?.trim(),
|
|
101
|
+
params.retrievedKnowledgeSection?.trim(),
|
|
102
|
+
toMemoryBlockSection(params.workstreamMemoryBlock),
|
|
103
|
+
params.workstreamStateSection?.trim(),
|
|
104
|
+
...(params.additionalInstructionSections?.map((section) => section.trim()) ?? []),
|
|
105
|
+
params.responseGuardSection?.trim(),
|
|
106
|
+
params.onboardingActive ? 'Onboarding is active. Keep responses onboarding-focused and concise.' : undefined,
|
|
107
|
+
].filter((value): value is string => Boolean(value && value.length > 0))
|
|
108
|
+
const extraInstructions = instructionSections.length > 0 ? instructionSections.join('\n\n') : undefined
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
id: params.agentId,
|
|
112
|
+
displayName: params.displayNameByAgent[params.agentId] ?? params.agentId,
|
|
113
|
+
mode,
|
|
114
|
+
extraInstructions,
|
|
115
|
+
maxSteps: profile.maxSteps,
|
|
116
|
+
reasoningProfile: profile.name,
|
|
117
|
+
toolCallBudget: profile.toolCallBudget,
|
|
118
|
+
maxInputTokensHint: profile.maxInputTokensHint,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function buildWorkstreamAgentToolPolicy<TAgent extends string, TSkill extends PropertyKey>(params: {
|
|
123
|
+
agentId: TAgent
|
|
124
|
+
workstreamMode: 'direct' | 'group'
|
|
125
|
+
mode?: ChatMode
|
|
126
|
+
onboardingActive: boolean
|
|
127
|
+
linearInstalled: boolean
|
|
128
|
+
githubInstalled: boolean
|
|
129
|
+
provideRepoTool: boolean
|
|
130
|
+
getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
|
|
131
|
+
}): AgentToolPolicy<TSkill> {
|
|
132
|
+
const resolvedMode = params.mode ?? toChatMode(params.workstreamMode)
|
|
133
|
+
const skills = resolveActiveAgentSkills({
|
|
134
|
+
agentId: params.agentId,
|
|
135
|
+
workstreamMode: params.workstreamMode,
|
|
136
|
+
mode: resolvedMode,
|
|
137
|
+
onboardingActive: params.onboardingActive,
|
|
138
|
+
linearInstalled: params.linearInstalled,
|
|
139
|
+
getAgentSkills: params.getAgentSkills,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
resolvedMode,
|
|
144
|
+
skills,
|
|
145
|
+
includeMemorySearch: !params.onboardingActive,
|
|
146
|
+
includeConversationSearch: !params.onboardingActive,
|
|
147
|
+
includeMemoryRemember: !params.onboardingActive,
|
|
148
|
+
includeOrgActionSearch: !params.onboardingActive,
|
|
149
|
+
includeMemoryBlockAppend: true,
|
|
150
|
+
includeReadFileParts: true,
|
|
151
|
+
includeInspectWebsite: params.onboardingActive && params.agentId === ('chief' as TAgent),
|
|
152
|
+
includeProceedInOnboarding: params.onboardingActive && params.agentId === ('chief' as TAgent),
|
|
153
|
+
includeGithubIntegration: params.onboardingActive && params.agentId === ('chief' as TAgent),
|
|
154
|
+
includeIndexRepositoryByURL: params.onboardingActive && params.agentId === ('chief' as TAgent),
|
|
155
|
+
includeIndexedRepository: params.githubInstalled && params.provideRepoTool,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function buildTeamConsultationAgentToolPolicy<TAgent extends string, TSkill extends PropertyKey>(params: {
|
|
160
|
+
agentId: TAgent
|
|
161
|
+
blockedSkills: readonly TSkill[]
|
|
162
|
+
blockedToolNames: readonly string[]
|
|
163
|
+
githubInstalled: boolean
|
|
164
|
+
provideRepoTool: boolean
|
|
165
|
+
getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
|
|
166
|
+
}): AgentToolPolicy<TSkill> & { blockedToolNames: Set<string> } {
|
|
167
|
+
const skills = resolveActiveAgentSkills({
|
|
168
|
+
agentId: params.agentId,
|
|
169
|
+
workstreamMode: 'group',
|
|
170
|
+
mode: 'fixedWorkstreamMode',
|
|
171
|
+
onboardingActive: false,
|
|
172
|
+
linearInstalled: false,
|
|
173
|
+
getAgentSkills: params.getAgentSkills,
|
|
174
|
+
}).filter((skill) => !params.blockedSkills.includes(skill))
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
resolvedMode: 'fixedWorkstreamMode',
|
|
178
|
+
skills,
|
|
179
|
+
includeMemorySearch: true,
|
|
180
|
+
includeConversationSearch: true,
|
|
181
|
+
includeMemoryRemember: false,
|
|
182
|
+
includeOrgActionSearch: true,
|
|
183
|
+
includeMemoryBlockAppend: false,
|
|
184
|
+
includeReadFileParts: true,
|
|
185
|
+
includeInspectWebsite: false,
|
|
186
|
+
includeProceedInOnboarding: false,
|
|
187
|
+
includeGithubIntegration: false,
|
|
188
|
+
includeIndexRepositoryByURL: false,
|
|
189
|
+
includeIndexedRepository: params.githubInstalled && params.provideRepoTool,
|
|
190
|
+
blockedToolNames: new Set(params.blockedToolNames),
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
2
|
+
import type { LanguageModelUsage, UIMessageStreamOptions } from 'ai'
|
|
3
|
+
|
|
4
|
+
import { agentDisplayNames } from '../config/agent-defaults'
|
|
5
|
+
|
|
6
|
+
export function readFiniteNumber(value: unknown): number | undefined {
|
|
7
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
11
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readOpenRouterUsageCost(usage: LanguageModelUsage): { cost?: number; upstreamInferenceCost?: number } {
|
|
15
|
+
const rawUsage = readRecord(usage.raw)
|
|
16
|
+
if (!rawUsage) return {}
|
|
17
|
+
|
|
18
|
+
const cost = readFiniteNumber(rawUsage.cost)
|
|
19
|
+
const costDetails = readRecord(rawUsage.cost_details)
|
|
20
|
+
const upstreamInferenceCost = readFiniteNumber(costDetails?.upstream_inference_cost)
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
...(cost !== undefined ? { cost } : {}),
|
|
24
|
+
...(upstreamInferenceCost !== undefined ? { upstreamInferenceCost } : {}),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createAgentMessageMetadata(params: {
|
|
29
|
+
agentId: string
|
|
30
|
+
agentName: string
|
|
31
|
+
}): NonNullable<UIMessageStreamOptions<ChatMessage>['messageMetadata']> {
|
|
32
|
+
return ({ part }) => {
|
|
33
|
+
if (part.type === 'start') {
|
|
34
|
+
return { agentId: params.agentId, agentName: params.agentName, createdAt: Date.now() }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (part.type === 'finish') {
|
|
38
|
+
const usageCost = readOpenRouterUsageCost(part.totalUsage)
|
|
39
|
+
return {
|
|
40
|
+
finishReason: part.finishReason,
|
|
41
|
+
...(part.totalUsage.inputTokens !== undefined ? { inputTokens: part.totalUsage.inputTokens } : {}),
|
|
42
|
+
...(part.totalUsage.outputTokens !== undefined ? { outputTokens: part.totalUsage.outputTokens } : {}),
|
|
43
|
+
...(part.totalUsage.totalTokens !== undefined ? { totalTokens: part.totalUsage.totalTokens } : {}),
|
|
44
|
+
...usageCost,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createServerRunAbortController(externalAbortSignal?: AbortSignal) {
|
|
53
|
+
const controller = new AbortController()
|
|
54
|
+
const abort = (reason?: unknown) => {
|
|
55
|
+
if (controller.signal.aborted) return
|
|
56
|
+
controller.abort(reason ?? new DOMException('Run stopped by user.', 'AbortError'))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const abortFromExternal = () => {
|
|
60
|
+
abort((externalAbortSignal as (AbortSignal & { reason?: unknown }) | undefined)?.reason)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (externalAbortSignal) {
|
|
64
|
+
if (externalAbortSignal.aborted) {
|
|
65
|
+
abortFromExternal()
|
|
66
|
+
} else {
|
|
67
|
+
externalAbortSignal.addEventListener('abort', abortFromExternal, { once: true })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
controller,
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
abort,
|
|
75
|
+
dispose: () => {
|
|
76
|
+
if (!externalAbortSignal) return
|
|
77
|
+
externalAbortSignal.removeEventListener('abort', abortFromExternal)
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createTimedAbortSignal(parentSignal: AbortSignal, timeoutMs: number) {
|
|
83
|
+
const controller = new AbortController()
|
|
84
|
+
let didTimeout = false
|
|
85
|
+
const abortFromParent = () => {
|
|
86
|
+
controller.abort((parentSignal as AbortSignal & { reason?: unknown }).reason)
|
|
87
|
+
}
|
|
88
|
+
const timeoutId = setTimeout(() => {
|
|
89
|
+
didTimeout = true
|
|
90
|
+
controller.abort(new Error(`Timed out after ${timeoutMs}ms`))
|
|
91
|
+
}, timeoutMs)
|
|
92
|
+
|
|
93
|
+
if (parentSignal.aborted) {
|
|
94
|
+
abortFromParent()
|
|
95
|
+
} else {
|
|
96
|
+
parentSignal.addEventListener('abort', abortFromParent, { once: true })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
signal: controller.signal,
|
|
101
|
+
didTimeout: () => didTimeout,
|
|
102
|
+
dispose: () => {
|
|
103
|
+
clearTimeout(timeoutId)
|
|
104
|
+
parentSignal.removeEventListener('abort', abortFromParent)
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildSpecialistTaskMessage(params: { agentId: string; task: string }): ChatMessage {
|
|
110
|
+
const displayName = agentDisplayNames[params.agentId] ?? params.agentId
|
|
111
|
+
return {
|
|
112
|
+
id: Bun.randomUUIDv7(),
|
|
113
|
+
role: 'user',
|
|
114
|
+
parts: [{ type: 'text', text: [`Chief of Staff request for ${displayName}:`, params.task.trim()].join('\n') }],
|
|
115
|
+
metadata: { createdAt: Date.now() },
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Output, PrepareStepFunction, StopCondition, ToolLoopAgentOnFinishCallback, ToolSet } from 'ai'
|
|
2
|
+
|
|
3
|
+
export type ChatMode = 'direct' | 'workstreamMode' | 'fixedWorkstreamMode'
|
|
4
|
+
|
|
5
|
+
export interface CreateRoutedAgentOptions<TTools extends ToolSet = ToolSet> {
|
|
6
|
+
mode: ChatMode
|
|
7
|
+
tools: TTools
|
|
8
|
+
extraInstructions?: string
|
|
9
|
+
stopWhen?: StopCondition<TTools> | Array<StopCondition<TTools>>
|
|
10
|
+
prepareStep?: PrepareStepFunction<TTools>
|
|
11
|
+
maxRetries?: number
|
|
12
|
+
modelOverride?: { model: unknown; providerOptions?: Record<string, unknown> }
|
|
13
|
+
onFinish?: ToolLoopAgentOnFinishCallback<TTools>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CreateHelperToolLoopAgentOptions {
|
|
17
|
+
instructions?: string
|
|
18
|
+
maxOutputTokens?: number
|
|
19
|
+
temperature?: number
|
|
20
|
+
output?: Output.Output
|
|
21
|
+
maxRetries?: number
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
2
|
+
|
|
3
|
+
export function hasApprovalRespondedParts(message: ChatMessage): boolean {
|
|
4
|
+
return message.parts.some((part) => 'state' in part && (part as { state: string }).state === 'approval-responded')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isApprovalContinuationRequest(messages: ChatMessage[]): boolean {
|
|
8
|
+
const lastAssistant = [...messages].reverse().find((message) => message.role === 'assistant')
|
|
9
|
+
if (!lastAssistant) return false
|
|
10
|
+
|
|
11
|
+
const lastMessageIndex = messages.lastIndexOf(lastAssistant)
|
|
12
|
+
const hasUserAfter = messages.slice(lastMessageIndex + 1).some((message) => message.role === 'user')
|
|
13
|
+
if (hasUserAfter) return false
|
|
14
|
+
|
|
15
|
+
return hasApprovalRespondedParts(lastAssistant)
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
2
|
+
|
|
3
|
+
import type { ReadableUploadMetadataLike } from './chat-types'
|
|
4
|
+
export type { ReadableUploadMetadataLike } from './chat-types'
|
|
5
|
+
|
|
6
|
+
export function buildReadableUploadMetadataText(uploads: ReadableUploadMetadataLike[]): string {
|
|
7
|
+
if (uploads.length === 0) return ''
|
|
8
|
+
|
|
9
|
+
const lines = uploads.map((upload) => {
|
|
10
|
+
const sizeSegment = upload.sizeBytes === null ? '' : `, sizeBytes=${upload.sizeBytes}`
|
|
11
|
+
return `- ${upload.filename} (${upload.mediaType}${sizeSegment}) storageKey=${upload.storageKey}`
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
return [
|
|
15
|
+
'Uploaded files are available via tool readFileParts.',
|
|
16
|
+
'Call readFileParts with storageKey and part. Each part contains up to 25 pages.',
|
|
17
|
+
'Available uploads:',
|
|
18
|
+
...lines,
|
|
19
|
+
].join('\n')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildModelInputMessagesWithUploadMetadata(params: {
|
|
23
|
+
messages: ChatMessage[]
|
|
24
|
+
latestUserMessageId: string
|
|
25
|
+
uploadMetadataText: string
|
|
26
|
+
}): ChatMessage[] {
|
|
27
|
+
return params.messages.map((message) => {
|
|
28
|
+
const fileParts = message.parts.filter((part) => part.type === 'file')
|
|
29
|
+
if (fileParts.length === 0) return message
|
|
30
|
+
|
|
31
|
+
const nonFileParts = message.parts.filter((part) => part.type !== 'file')
|
|
32
|
+
const attachmentNames = fileParts
|
|
33
|
+
.map((part) => (typeof part.filename === 'string' && part.filename.trim() ? part.filename.trim() : 'Attachment'))
|
|
34
|
+
.join(', ')
|
|
35
|
+
|
|
36
|
+
const nextParts: ChatMessage['parts'] = [...nonFileParts]
|
|
37
|
+
if (attachmentNames) {
|
|
38
|
+
nextParts.push({ type: 'text', text: `User attached files: ${attachmentNames}` })
|
|
39
|
+
}
|
|
40
|
+
if (message.id === params.latestUserMessageId && params.uploadMetadataText) {
|
|
41
|
+
nextParts.push({ type: 'text', text: params.uploadMetadataText })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { ...message, parts: nextParts }
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MessagePartLike } from './chat-types'
|
|
2
|
+
|
|
3
|
+
export function hasMessageContent(parts: readonly MessagePartLike[]): boolean {
|
|
4
|
+
for (const part of parts) {
|
|
5
|
+
if (part.type === 'file') return true
|
|
6
|
+
if (part.type === 'text' && typeof part.text === 'string' && part.text.trim().length > 0) return true
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
2
|
+
|
|
3
|
+
import { isApprovalContinuationRequest } from './approval-continuation'
|
|
4
|
+
|
|
5
|
+
export type RoutedChatRequest =
|
|
6
|
+
| { kind: 'approval-continuation'; approvalMessages: ChatMessage[] }
|
|
7
|
+
| { kind: 'turn'; inputMessage: ChatMessage }
|
|
8
|
+
| { kind: 'invalid'; message: string }
|
|
9
|
+
|
|
10
|
+
export function routeWorkstreamChatMessages(messages: ChatMessage[]): RoutedChatRequest {
|
|
11
|
+
if (isApprovalContinuationRequest(messages)) {
|
|
12
|
+
return { kind: 'approval-continuation', approvalMessages: messages }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const inputMessage = [...messages].reverse().find((message) => message.role === 'user')
|
|
16
|
+
if (!inputMessage) {
|
|
17
|
+
return { kind: 'invalid', message: 'Only user messages can be submitted to this endpoint.' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { kind: 'turn', inputMessage }
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const COMPACTION_POLL_INTERVAL_MS = 200
|
|
2
|
+
const COMPACTION_MAX_WAIT_MS = 120_000
|
|
3
|
+
|
|
4
|
+
export async function waitForCompactionIfNeeded<TEntity>(params: {
|
|
5
|
+
entityId: string
|
|
6
|
+
entityLabel: string
|
|
7
|
+
loadEntity: () => Promise<TEntity>
|
|
8
|
+
isCompacting: (entity: TEntity) => boolean
|
|
9
|
+
}): Promise<TEntity> {
|
|
10
|
+
let entity = await params.loadEntity()
|
|
11
|
+
if (!params.isCompacting(entity)) return entity
|
|
12
|
+
|
|
13
|
+
const deadline = Date.now() + COMPACTION_MAX_WAIT_MS
|
|
14
|
+
while (params.isCompacting(entity)) {
|
|
15
|
+
if (Date.now() > deadline) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`${params.entityLabel} ${params.entityId} compaction did not complete within ${COMPACTION_MAX_WAIT_MS}ms`,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
await Bun.sleep(COMPACTION_POLL_INTERVAL_MS)
|
|
21
|
+
entity = await params.loadEntity()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return entity
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class ChatRunRegistry {
|
|
2
|
+
private controllers = new Map<string, AbortController>()
|
|
3
|
+
|
|
4
|
+
register(runId: string, controller: AbortController): void {
|
|
5
|
+
this.controllers.set(runId, controller)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
unregister(runId: string): void {
|
|
9
|
+
this.controllers.delete(runId)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
stop(runId: string, reason?: unknown): boolean {
|
|
13
|
+
const controller = this.controllers.get(runId)
|
|
14
|
+
if (!controller) return false
|
|
15
|
+
|
|
16
|
+
this.controllers.delete(runId)
|
|
17
|
+
controller.abort(reason)
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface MessagePartLike {
|
|
2
|
+
type?: string
|
|
3
|
+
text?: string
|
|
4
|
+
[key: string]: unknown
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ChatMessageLike<TPart extends MessagePartLike = MessagePartLike> {
|
|
8
|
+
role: string
|
|
9
|
+
parts: TPart[]
|
|
10
|
+
metadata?: unknown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ReadableUploadMetadataLike {
|
|
14
|
+
storageKey: string
|
|
15
|
+
filename: string
|
|
16
|
+
mediaType: string
|
|
17
|
+
sizeBytes: number | null
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const CONTEXT_COMPACTION_THRESHOLD_RATIO = 0.7
|
|
2
|
+
export const CONTEXT_OUTPUT_RESERVE_TOKENS = 32_000
|
|
3
|
+
export const CONTEXT_SAFETY_MARGIN_TOKENS = 8_000
|
|
4
|
+
export const COMPACTION_CHUNK_MAX_CHARS = 120_000
|
|
5
|
+
export const SUMMARY_ROLLUP_MAX_TOKENS = 80_000
|
|
6
|
+
export const CONTEXT_COMPACTION_INCLUDED_TOOL_NAMES = ['userQuestions', 'proceedInOnboarding'] as const
|
|
7
|
+
export const CONTEXT_COMPACTION_INCLUDED_TOOL_PREFIXES = ['linear'] as const
|
|
8
|
+
export const MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES = 15
|
|
9
|
+
export const MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES = 10
|
|
10
|
+
export const CONTEXT_SIZE = 200_000
|
|
11
|
+
export const WORKSTREAM_RAW_TAIL_MESSAGES = 6
|