@lota-sdk/core 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/infrastructure/schema/00_workstream.surql +55 -0
  2. package/infrastructure/schema/01_memory.surql +47 -0
  3. package/infrastructure/schema/02_execution_plan.surql +62 -0
  4. package/infrastructure/schema/03_learned_skill.surql +32 -0
  5. package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
  6. package/package.json +128 -0
  7. package/src/ai/definitions.ts +308 -0
  8. package/src/bifrost/bifrost.ts +256 -0
  9. package/src/config/agent-defaults.ts +99 -0
  10. package/src/config/constants.ts +33 -0
  11. package/src/config/env-shapes.ts +122 -0
  12. package/src/config/logger.ts +29 -0
  13. package/src/config/model-constants.ts +31 -0
  14. package/src/config/search.ts +17 -0
  15. package/src/config/workstream-defaults.ts +68 -0
  16. package/src/db/base.service.ts +55 -0
  17. package/src/db/cursor-pagination.ts +73 -0
  18. package/src/db/memory-query-builder.ts +207 -0
  19. package/src/db/memory-store.helpers.ts +118 -0
  20. package/src/db/memory-store.rows.ts +29 -0
  21. package/src/db/memory-store.ts +974 -0
  22. package/src/db/memory-types.ts +193 -0
  23. package/src/db/memory.ts +505 -0
  24. package/src/db/record-id.ts +78 -0
  25. package/src/db/service.ts +932 -0
  26. package/src/db/startup.ts +152 -0
  27. package/src/db/tables.ts +20 -0
  28. package/src/document/org-document-chunking.ts +224 -0
  29. package/src/document/parsing.ts +40 -0
  30. package/src/embeddings/provider.ts +76 -0
  31. package/src/index.ts +302 -0
  32. package/src/queues/context-compaction.queue.ts +82 -0
  33. package/src/queues/document-processor.queue.ts +118 -0
  34. package/src/queues/memory-consolidation.queue.ts +65 -0
  35. package/src/queues/post-chat-memory.queue.ts +128 -0
  36. package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
  37. package/src/queues/regular-chat-memory-digest.config.ts +12 -0
  38. package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
  39. package/src/queues/skill-extraction.config.ts +9 -0
  40. package/src/queues/skill-extraction.queue.ts +62 -0
  41. package/src/redis/connection.ts +176 -0
  42. package/src/redis/index.ts +30 -0
  43. package/src/redis/org-memory-lock.ts +43 -0
  44. package/src/redis/redis-lease-lock.ts +158 -0
  45. package/src/runtime/agent-contract.ts +1 -0
  46. package/src/runtime/agent-prompt-context.ts +119 -0
  47. package/src/runtime/agent-runtime-policy.ts +192 -0
  48. package/src/runtime/agent-stream-helpers.ts +117 -0
  49. package/src/runtime/agent-types.ts +22 -0
  50. package/src/runtime/approval-continuation.ts +16 -0
  51. package/src/runtime/chat-attachments.ts +46 -0
  52. package/src/runtime/chat-message.ts +10 -0
  53. package/src/runtime/chat-request-routing.ts +21 -0
  54. package/src/runtime/chat-run-orchestration.ts +25 -0
  55. package/src/runtime/chat-run-registry.ts +20 -0
  56. package/src/runtime/chat-types.ts +18 -0
  57. package/src/runtime/context-compaction-constants.ts +11 -0
  58. package/src/runtime/context-compaction-runtime.ts +86 -0
  59. package/src/runtime/context-compaction.ts +909 -0
  60. package/src/runtime/execution-plan.ts +59 -0
  61. package/src/runtime/helper-model.ts +405 -0
  62. package/src/runtime/indexed-repositories-policy.ts +28 -0
  63. package/src/runtime/instruction-sections.ts +8 -0
  64. package/src/runtime/llm-content.ts +71 -0
  65. package/src/runtime/memory-block.ts +264 -0
  66. package/src/runtime/memory-digest-policy.ts +14 -0
  67. package/src/runtime/memory-format.ts +8 -0
  68. package/src/runtime/memory-pipeline.ts +570 -0
  69. package/src/runtime/memory-prompts-fact.ts +47 -0
  70. package/src/runtime/memory-prompts-parse.ts +3 -0
  71. package/src/runtime/memory-prompts-update.ts +37 -0
  72. package/src/runtime/memory-scope.ts +43 -0
  73. package/src/runtime/plugin-types.ts +10 -0
  74. package/src/runtime/retrieval-adapters.ts +25 -0
  75. package/src/runtime/retrieval-pipeline.ts +3 -0
  76. package/src/runtime/runtime-extensions.ts +154 -0
  77. package/src/runtime/skill-extraction-policy.ts +3 -0
  78. package/src/runtime/team-consultation-orchestrator.ts +245 -0
  79. package/src/runtime/team-consultation-prompts.ts +32 -0
  80. package/src/runtime/title-helpers.ts +12 -0
  81. package/src/runtime/turn-lifecycle.ts +28 -0
  82. package/src/runtime/workstream-chat-helpers.ts +187 -0
  83. package/src/runtime/workstream-routing-policy.ts +301 -0
  84. package/src/runtime/workstream-state.ts +261 -0
  85. package/src/services/attachment.service.ts +159 -0
  86. package/src/services/chat-attachments.service.ts +17 -0
  87. package/src/services/chat-run-registry.service.ts +3 -0
  88. package/src/services/context-compaction-runtime.ts +13 -0
  89. package/src/services/context-compaction.service.ts +115 -0
  90. package/src/services/document-chunk.service.ts +141 -0
  91. package/src/services/execution-plan.service.ts +890 -0
  92. package/src/services/learned-skill.service.ts +328 -0
  93. package/src/services/memory-assessment.service.ts +43 -0
  94. package/src/services/memory.service.ts +807 -0
  95. package/src/services/memory.utils.ts +84 -0
  96. package/src/services/mutating-approval.service.ts +110 -0
  97. package/src/services/recent-activity-title.service.ts +74 -0
  98. package/src/services/recent-activity.service.ts +397 -0
  99. package/src/services/workstream-change-tracker.service.ts +313 -0
  100. package/src/services/workstream-message.service.ts +283 -0
  101. package/src/services/workstream-title.service.ts +58 -0
  102. package/src/services/workstream-turn-preparation.ts +1340 -0
  103. package/src/services/workstream-turn.ts +37 -0
  104. package/src/services/workstream.service.ts +854 -0
  105. package/src/services/workstream.types.ts +118 -0
  106. package/src/storage/attachment-parser.ts +101 -0
  107. package/src/storage/attachment-storage.service.ts +391 -0
  108. package/src/storage/attachments.types.ts +11 -0
  109. package/src/storage/attachments.utils.ts +58 -0
  110. package/src/storage/generated-document-storage.service.ts +55 -0
  111. package/src/system-agents/agent-result.ts +27 -0
  112. package/src/system-agents/context-compacter.agent.ts +46 -0
  113. package/src/system-agents/delegated-agent-factory.ts +177 -0
  114. package/src/system-agents/helper-agent-options.ts +20 -0
  115. package/src/system-agents/memory-reranker.agent.ts +38 -0
  116. package/src/system-agents/memory.agent.ts +58 -0
  117. package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
  118. package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
  119. package/src/system-agents/researcher.agent.ts +34 -0
  120. package/src/system-agents/skill-extractor.agent.ts +88 -0
  121. package/src/system-agents/skill-manager.agent.ts +80 -0
  122. package/src/system-agents/title-generator.agent.ts +42 -0
  123. package/src/system-agents/workstream-tracker.agent.ts +58 -0
  124. package/src/tools/execution-plan.tool.ts +163 -0
  125. package/src/tools/fetch-webpage.tool.ts +132 -0
  126. package/src/tools/firecrawl-client.ts +12 -0
  127. package/src/tools/memory-block.tool.ts +55 -0
  128. package/src/tools/read-file-parts.tool.ts +80 -0
  129. package/src/tools/remember-memory.tool.ts +85 -0
  130. package/src/tools/research-topic.tool.ts +15 -0
  131. package/src/tools/search-tools.ts +55 -0
  132. package/src/tools/search-web.tool.ts +175 -0
  133. package/src/tools/team-think.tool.ts +125 -0
  134. package/src/tools/tool-contract.ts +21 -0
  135. package/src/tools/user-questions.tool.ts +18 -0
  136. package/src/utils/async.ts +50 -0
  137. package/src/utils/date-time.ts +34 -0
  138. package/src/utils/error.ts +10 -0
  139. package/src/utils/errors.ts +28 -0
  140. package/src/utils/hono-error-handler.ts +71 -0
  141. package/src/utils/string.ts +51 -0
  142. package/src/workers/bootstrap.ts +44 -0
  143. package/src/workers/memory-consolidation.worker.ts +318 -0
  144. package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
  145. package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
  146. package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
  147. package/src/workers/skill-extraction.runner.ts +331 -0
  148. package/src/workers/skill-extraction.worker.ts +22 -0
  149. package/src/workers/utils/repo-indexer-chunker.ts +331 -0
  150. package/src/workers/utils/repo-structure-extractor.ts +645 -0
  151. package/src/workers/utils/repomix-process-concurrency.ts +65 -0
  152. package/src/workers/utils/sandbox-error.ts +5 -0
  153. package/src/workers/worker-utils.ts +182 -0
@@ -0,0 +1,85 @@
1
+ import { tool } from 'ai'
2
+ import { z } from 'zod'
3
+
4
+ import type { RecordIdRef } from '../db/record-id'
5
+ import { recordIdToString } from '../db/record-id'
6
+ import { TABLES } from '../db/tables'
7
+ import { assessMemoryImportance, clampMemoryImportance } from '../services/memory-assessment.service'
8
+ import { memoryService } from '../services/memory.service'
9
+ import { safeEnqueue } from '../utils/async'
10
+
11
+ const RememberMemoryInputSchema = z
12
+ .object({
13
+ content: z.string().min(1).describe('The memory to store.'),
14
+ targetScope: z.enum(['agent', 'global']).default('agent'),
15
+ })
16
+ .strict()
17
+
18
+ export function createRememberMemoryTool({ orgId, agentName }: { orgId: RecordIdRef; agentName: string }) {
19
+ return tool({
20
+ description:
21
+ 'Save a durable memory. Importance is scored automatically from memory significance. Use targetScope="agent" for role-specific memory, or targetScope="global" for organization-wide memory.',
22
+ inputSchema: RememberMemoryInputSchema,
23
+ execute: ({ content, targetScope }: z.infer<typeof RememberMemoryInputSchema>) => {
24
+ const trimmed = content.trim()
25
+ if (!trimmed) {
26
+ return { skipped: true, reason: 'Empty memory.' }
27
+ }
28
+
29
+ const orgIdString = recordIdToString(orgId, TABLES.ORGANIZATION)
30
+ void safeEnqueue(
31
+ async () => {
32
+ const assessment = await assessMemoryImportance({
33
+ content: trimmed,
34
+ targetScope,
35
+ tag: 'memory-importance-assessment',
36
+ })
37
+
38
+ if (assessment.classification === 'transient' && assessment.durability === 'ephemeral') {
39
+ return
40
+ }
41
+
42
+ const importance = Math.round(clampMemoryImportance(assessment.importance) * 100) / 100
43
+
44
+ if (targetScope === 'global') {
45
+ await memoryService.createOrganizationMemory({
46
+ orgId: orgIdString,
47
+ content: trimmed,
48
+ memoryType: 'fact',
49
+ importance,
50
+ metadata: {
51
+ source: 'agent_tool',
52
+ agentName,
53
+ memoryScope: 'global',
54
+ importanceSource: 'model_assessed',
55
+ durability: assessment.durability,
56
+ classification: assessment.classification,
57
+ rationale: assessment.rationale,
58
+ },
59
+ })
60
+ return
61
+ }
62
+
63
+ await memoryService.createAgentMemory({
64
+ orgId: orgIdString,
65
+ agentName,
66
+ content: trimmed,
67
+ memoryType: 'fact',
68
+ importance,
69
+ metadata: {
70
+ source: 'agent_tool',
71
+ memoryScope: 'agent',
72
+ importanceSource: 'model_assessed',
73
+ durability: assessment.durability,
74
+ classification: assessment.classification,
75
+ rationale: assessment.rationale,
76
+ },
77
+ })
78
+ },
79
+ { operationName: `remember memory (${targetScope})`, logPrefix: 'Background memoryRemember task failed' },
80
+ )
81
+
82
+ return { success: true, status: 'scheduled', scope: targetScope }
83
+ },
84
+ })
85
+ }
@@ -0,0 +1,15 @@
1
+ import { bifrostChatModel } from '../bifrost/bifrost'
2
+ import { OPENROUTER_WEB_RESEARCH_MODEL_ID } from '../config/model-constants'
3
+ import { createDelegatedAgentTool } from '../system-agents/delegated-agent-factory'
4
+ import { RESEARCHER_PROMPT } from '../system-agents/researcher.agent'
5
+ import { fetchWebpageTool } from './fetch-webpage.tool'
6
+ import { searchWebTool } from './search-web.tool'
7
+
8
+ export const researchTopicTool = createDelegatedAgentTool({
9
+ id: 'researchTopic',
10
+ description:
11
+ 'Delegate a research task to a dedicated research agent that searches the web, fetches pages, and returns a synthesized markdown report. Call multiple instances in parallel for broad research across different topics.',
12
+ model: bifrostChatModel(OPENROUTER_WEB_RESEARCH_MODEL_ID),
13
+ instructions: RESEARCHER_PROMPT,
14
+ tools: { searchWeb: searchWebTool.create(), fetchWebpage: fetchWebpageTool.create() },
15
+ })
@@ -0,0 +1,55 @@
1
+ import { tool } from 'ai'
2
+ import { z } from 'zod'
3
+
4
+ import { isAgentName } from '../config/agent-defaults'
5
+ import type { RecordIdRef } from '../db/record-id'
6
+ import { memoryService } from '../services/memory.service'
7
+ import { workstreamMessageService } from '../services/workstream-message.service'
8
+
9
+ const CONVERSATION_SEARCH_RESULT_LIMIT = 20
10
+ const MemorySearchInputSchema = z.object({ query: z.string().min(1) }).strict()
11
+ const ConversationSearchInputSchema = z.object({ query: z.string().min(1), type: z.enum(['user', 'agent']) }).strict()
12
+
13
+ export function createMemorySearchTool(
14
+ orgIdString: string,
15
+ agentName?: string,
16
+ options?: { fastMode?: boolean; allowMultiScopeRerank?: boolean },
17
+ ) {
18
+ return tool({
19
+ description: 'Search organization and agent memories relevant to a query.',
20
+ inputSchema: MemorySearchInputSchema,
21
+ execute: async ({ query }: z.infer<typeof MemorySearchInputSchema>) => {
22
+ const normalizedQuery = query.trim()
23
+ const retrieval = await memoryService.searchAllMemoriesBatched({
24
+ orgId: orgIdString,
25
+ agentName: isAgentName(agentName) ? (agentName as string) : undefined,
26
+ query: normalizedQuery,
27
+ ...(typeof options?.fastMode === 'boolean' ? { fastMode: options.fastMode } : {}),
28
+ ...(typeof options?.allowMultiScopeRerank === 'boolean'
29
+ ? { allowMultiScopeRerank: options.allowMultiScopeRerank }
30
+ : {}),
31
+ })
32
+ const reminder = `Remember: you searched for "${normalizedQuery}". Use the highest-relevance retrieved lines above to answer.`
33
+
34
+ return { query: normalizedQuery, retrieval, reminder }
35
+ },
36
+ })
37
+ }
38
+
39
+ export function createConversationSearchTool(workstreamId: RecordIdRef) {
40
+ return tool({
41
+ description: 'Search prior chat messages by role and query text.',
42
+ inputSchema: ConversationSearchInputSchema,
43
+ execute: async ({ query, type }: z.infer<typeof ConversationSearchInputSchema>) => {
44
+ const normalizedQuery = query.trim()
45
+ const results = await workstreamMessageService.searchMessages({
46
+ workstreamId,
47
+ role: type === 'user' ? 'user' : 'assistant',
48
+ query: normalizedQuery,
49
+ limit: CONVERSATION_SEARCH_RESULT_LIMIT,
50
+ })
51
+
52
+ return { query: normalizedQuery, type, count: results.length, results }
53
+ },
54
+ })
55
+ }
@@ -0,0 +1,175 @@
1
+ import { tool } from 'ai'
2
+ import { z } from 'zod'
3
+
4
+ import type { ToolDefinition } from '../ai/definitions'
5
+ import type { Citation } from '../services/workstream.types'
6
+ import { withTimeout } from '../utils/async'
7
+ import { readStringField, truncateOptionalText } from '../utils/string'
8
+ import { getFirecrawlClient } from './firecrawl-client'
9
+
10
+ const TOOL_TIMEOUT_MS = 30_000
11
+ const SourceSchema = z.enum(['web', 'news', 'images'])
12
+ const MAX_RESULTS_PER_SOURCE = 4
13
+ const MAX_SNIPPET_CHARS = 320
14
+
15
+ function readMetadata(item: unknown): Record<string, unknown> {
16
+ if (!item || typeof item !== 'object') return {}
17
+ const metadata = (item as Record<string, unknown>).metadata
18
+ return metadata && typeof metadata === 'object' && !Array.isArray(metadata)
19
+ ? (metadata as Record<string, unknown>)
20
+ : {}
21
+ }
22
+
23
+ function readSearchItemSnippet(record: Record<string, unknown>, metadata: Record<string, unknown>): string | undefined {
24
+ return truncateOptionalText(
25
+ [
26
+ readStringField(record, 'description'),
27
+ readStringField(record, 'snippet'),
28
+ readStringField(record, 'summary'),
29
+ readStringField(record, 'markdown'),
30
+ readStringField(metadata, 'description'),
31
+ readStringField(metadata, 'snippet'),
32
+ readStringField(metadata, 'summary'),
33
+ ].find((value) => typeof value === 'string' && value.trim().length > 0),
34
+ MAX_SNIPPET_CHARS,
35
+ )
36
+ }
37
+
38
+ function summarizeSearchItem(item: unknown): Record<string, string> | null {
39
+ if (!item || typeof item !== 'object' || Array.isArray(item)) return null
40
+
41
+ const record = item as Record<string, unknown>
42
+ const metadata = readMetadata(item)
43
+ const url = extractUrlFromSearchItem(item)
44
+
45
+ if (!url) return null
46
+
47
+ const title = truncateOptionalText(readStringField(record, 'title') ?? readStringField(metadata, 'title'), 140)
48
+ const snippet = readSearchItemSnippet(record, metadata)
49
+ const publishedAt = truncateOptionalText(
50
+ readStringField(record, 'publishedDate') ??
51
+ readStringField(record, 'publishedAt') ??
52
+ readStringField(metadata, 'publishedDate') ??
53
+ readStringField(metadata, 'publishedAt'),
54
+ 80,
55
+ )
56
+ const siteName = truncateOptionalText(
57
+ readStringField(record, 'siteName') ?? readStringField(metadata, 'siteName'),
58
+ 80,
59
+ )
60
+ const imageUrl = truncateOptionalText(
61
+ readStringField(record, 'imageUrl') ?? readStringField(metadata, 'imageUrl') ?? readStringField(metadata, 'image'),
62
+ 500,
63
+ )
64
+
65
+ return {
66
+ url,
67
+ ...(title ? { title } : {}),
68
+ ...(snippet ? { snippet } : {}),
69
+ ...(publishedAt ? { publishedAt } : {}),
70
+ ...(siteName ? { siteName } : {}),
71
+ ...(imageUrl ? { imageUrl } : {}),
72
+ }
73
+ }
74
+
75
+ function summarizeSearchItems(items: unknown[] | undefined): Record<string, unknown> {
76
+ const summarized = (items ?? []).map((item) => summarizeSearchItem(item)).filter((item) => item !== null)
77
+
78
+ return { total: summarized.length, items: summarized.slice(0, MAX_RESULTS_PER_SOURCE) }
79
+ }
80
+
81
+ function extractUrlFromSearchItem(item: unknown): string | undefined {
82
+ if (!item || typeof item !== 'object') return undefined
83
+
84
+ const record = item as Record<string, unknown>
85
+ if (typeof record.url === 'string' && record.url.trim().length > 0) {
86
+ return record.url.trim()
87
+ }
88
+ if (typeof record.imageUrl === 'string' && record.imageUrl.trim().length > 0) {
89
+ return record.imageUrl.trim()
90
+ }
91
+
92
+ const metadata = record.metadata
93
+ if (metadata && typeof metadata === 'object') {
94
+ const metadataRecord = metadata as Record<string, unknown>
95
+ if (typeof metadataRecord.url === 'string' && metadataRecord.url.trim().length > 0) {
96
+ return metadataRecord.url.trim()
97
+ }
98
+ if (typeof metadataRecord.sourceURL === 'string' && metadataRecord.sourceURL.trim().length > 0) {
99
+ return metadataRecord.sourceURL.trim()
100
+ }
101
+ if (typeof metadataRecord.imageUrl === 'string' && metadataRecord.imageUrl.trim().length > 0) {
102
+ return metadataRecord.imageUrl.trim()
103
+ }
104
+ }
105
+
106
+ return undefined
107
+ }
108
+
109
+ function buildWebCitations(results: { web?: unknown[]; news?: unknown[]; images?: unknown[] }): Citation[] {
110
+ const seen = new Set<string>()
111
+ const citations: Citation[] = []
112
+ const retrievedAt = new Date().toISOString()
113
+
114
+ const append = (items: unknown[] | undefined) => {
115
+ if (!items) return
116
+ for (const item of items) {
117
+ const url = extractUrlFromSearchItem(item)
118
+ if (!url || seen.has(url)) continue
119
+ seen.add(url)
120
+ citations.push({ source: 'web', sourceId: url, retrievedAt })
121
+ }
122
+ }
123
+
124
+ append(results.web)
125
+ append(results.news)
126
+ append(results.images)
127
+
128
+ return citations
129
+ }
130
+
131
+ export const searchWebTool = {
132
+ name: 'searchWeb',
133
+ create: () =>
134
+ tool({
135
+ description: 'Search the web for real-time information.',
136
+ inputSchema: z
137
+ .object({
138
+ query: z.string().min(1, 'Query is required'),
139
+ limit: z.number().int().min(1).max(10).optional(),
140
+ sources: z.array(SourceSchema).optional(),
141
+ location: z.string().optional(),
142
+ tbs: z.string().optional(),
143
+ })
144
+ .strict(),
145
+ execute: async ({
146
+ query,
147
+ limit,
148
+ sources,
149
+ location,
150
+ tbs,
151
+ }: {
152
+ query: string
153
+ limit?: number
154
+ sources?: ('web' | 'news' | 'images')[]
155
+ location?: string
156
+ tbs?: string
157
+ }) => {
158
+ const results = await withTimeout(
159
+ getFirecrawlClient().search(query, { limit, sources, location, tbs }),
160
+ TOOL_TIMEOUT_MS,
161
+ 'Web search',
162
+ )
163
+
164
+ return {
165
+ query,
166
+ results: {
167
+ web: summarizeSearchItems(results.web),
168
+ news: summarizeSearchItems(results.news),
169
+ images: summarizeSearchItems(results.images),
170
+ },
171
+ citations: buildWebCitations(results),
172
+ }
173
+ },
174
+ }),
175
+ } as const satisfies ToolDefinition<void>
@@ -0,0 +1,125 @@
1
+ import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
2
+ import { stepCountIs } from 'ai'
3
+ import type { ToolSet } from 'ai'
4
+
5
+ import { createAgent, getAgentRuntimeConfig } from '../config/agent-defaults'
6
+ import { aiLogger } from '../config/logger'
7
+ import type { RecordIdRef } from '../db/record-id'
8
+ import { recordIdToString } from '../db/record-id'
9
+ import { TABLES } from '../db/tables'
10
+ import { mergeInstructionSections } from '../runtime/instruction-sections'
11
+ import { getRuntimeAdapters } from '../runtime/runtime-extensions'
12
+ import { createConsultTeamTool as createConsultTeamToolSdk } from '../runtime/team-consultation-orchestrator'
13
+ import type { DefaultRepoSections, TeamConsultationParticipantRunner } from '../runtime/team-consultation-orchestrator'
14
+ import { buildTeamConsultationResponseGuard } from '../runtime/team-consultation-prompts'
15
+ import type { ReadableUploadMetadata } from '../services/attachment.service'
16
+
17
+ async function buildTeamThinkAgentTools(params: Record<string, unknown>): Promise<{ tools: Record<string, unknown> }> {
18
+ const builder = getRuntimeAdapters().workstream?.buildTeamThinkAgentTools
19
+ if (!builder) {
20
+ return { tools: {} }
21
+ }
22
+
23
+ const result = await builder(params as never)
24
+ return { tools: result.tools as Record<string, unknown> }
25
+ }
26
+
27
+ const TEAM_THINK_AGENT_MAX_RETRIES = 4
28
+
29
+ export function createTeamThinkTool(params: {
30
+ historyMessages: ChatMessage[]
31
+ latestUserMessageId: string
32
+ orgId: RecordIdRef
33
+ userId: RecordIdRef
34
+ workstreamId: RecordIdRef
35
+ githubInstalled: boolean
36
+ availableUploads: ReadableUploadMetadata[]
37
+ provideRepoTool: boolean
38
+ defaultRepoSectionsByAgent: Record<string, DefaultRepoSections | undefined>
39
+ reasoningProfile: 'fast' | 'standard' | 'deep'
40
+ systemWorkspaceDetails?: string
41
+ getPreSeededMemoriesSection: (agentId: string) => Promise<string | undefined>
42
+ retrievedKnowledgeSection?: string
43
+ additionalInstructionSections?: string[]
44
+ getAdditionalInstructionSections?: () => Promise<string[] | undefined>
45
+ context?: unknown
46
+ toolProviders?: ToolSet
47
+ abortSignal: AbortSignal
48
+ }) {
49
+ const participantRunner: TeamConsultationParticipantRunner = {
50
+ async buildParticipantAgent(agentId, runParams) {
51
+ const dynamicInstructionSections = await params.getAdditionalInstructionSections?.()
52
+ const config = getAgentRuntimeConfig({
53
+ agentId,
54
+ workstreamMode: 'group' as const,
55
+ mode: 'fixedWorkstreamMode',
56
+ onboardingActive: false,
57
+ linearInstalled: false,
58
+ reasoningProfile: runParams.reasoningProfile,
59
+ systemWorkspaceDetails: runParams.systemWorkspaceDetails,
60
+ preSeededMemoriesSection: runParams.preSeededMemoriesSection,
61
+ retrievedKnowledgeSection: runParams.retrievedKnowledgeSection,
62
+ additionalInstructionSections: mergeInstructionSections(
63
+ dynamicInstructionSections,
64
+ params.additionalInstructionSections,
65
+ ),
66
+ responseGuardSection: buildTeamConsultationResponseGuard({ agentId, task: runParams.task }),
67
+ })
68
+ const { tools } = await buildTeamThinkAgentTools({
69
+ agentId,
70
+ workspaceId: params.orgId,
71
+ userId: params.userId,
72
+ workspaceIdString: recordIdToString(params.orgId, TABLES.ORGANIZATION),
73
+ workstreamId: params.workstreamId,
74
+ githubInstalled: params.githubInstalled,
75
+ provideRepoTool: params.provideRepoTool,
76
+ availableUploads: params.availableUploads,
77
+ defaultRepoSections: params.defaultRepoSectionsByAgent[agentId],
78
+ context: params.context,
79
+ toolProviders: params.toolProviders,
80
+ })
81
+ const agentConfig = config as Record<string, unknown>
82
+ const agentFactory = createAgent as unknown as Record<string, (...args: unknown[]) => unknown>
83
+ const agent = agentFactory[agentConfig.id as string]({
84
+ mode: 'fixedWorkstreamMode',
85
+ tools,
86
+ extraInstructions: agentConfig.extraInstructions,
87
+ maxRetries: TEAM_THINK_AGENT_MAX_RETRIES,
88
+ stopWhen: [stepCountIs(agentConfig.maxSteps as number)],
89
+ })
90
+ const observer = {
91
+ run: async <T>(fn: () => T | Promise<T>): Promise<T> => fn(),
92
+ recordError: (error: unknown) => {
93
+ aiLogger.error`Team-think participant failed (${agentId}): ${error}`
94
+ },
95
+ recordAbort: (error: unknown) => {
96
+ aiLogger.info`Team-think participant aborted (${agentId}): ${
97
+ error instanceof Error ? error.message : String(error)
98
+ }`
99
+ },
100
+ }
101
+ return {
102
+ agent: agent as Awaited<ReturnType<TeamConsultationParticipantRunner['buildParticipantAgent']>>['agent'],
103
+ observer,
104
+ }
105
+ },
106
+ }
107
+
108
+ return createConsultTeamToolSdk({
109
+ historyMessages: params.historyMessages,
110
+ latestUserMessageId: params.latestUserMessageId,
111
+ availableUploads: params.availableUploads,
112
+ defaultRepoSectionsByAgent: params.defaultRepoSectionsByAgent,
113
+ reasoningProfile: params.reasoningProfile,
114
+ systemWorkspaceDetails: params.systemWorkspaceDetails,
115
+ getPreSeededMemoriesSection: params.getPreSeededMemoriesSection,
116
+ retrievedKnowledgeSection: params.retrievedKnowledgeSection,
117
+ abortSignal: params.abortSignal,
118
+ participantRunner,
119
+ onReadError: (agentId, error) => {
120
+ if (!(error instanceof Error && error.name === 'AbortError')) {
121
+ aiLogger.error`UI message read failed for team-think participant ${agentId}: ${error}`
122
+ }
123
+ },
124
+ })
125
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod'
2
+
3
+ /** @lintignore */
4
+ export const MutatingApprovalSchema = {
5
+ approvalReason: z.string().trim().min(1, 'approvalReason is required').max(500),
6
+ approvalToken: z.string().trim().min(1, 'approvalToken is required').max(500),
7
+ approvalMessageId: z.string().trim().min(1).max(200).optional(),
8
+ } as const
9
+
10
+ /** @lintignore */
11
+ export const CitationSchema = z
12
+ .object({
13
+ source: z.string().trim().min(1),
14
+ sourceId: z.string().trim().min(1),
15
+ version: z.string().trim().min(1).optional(),
16
+ retrievedAt: z.iso.datetime(),
17
+ })
18
+ .strict()
19
+
20
+ /** @lintignore */
21
+ export type Citation = z.infer<typeof CitationSchema>
@@ -0,0 +1,18 @@
1
+ import { USER_QUESTIONS_TOOL_NAME, UserQuestionsArgsSchema } from '@lota-sdk/shared/schemas/tools'
2
+ import type { UserQuestionsArgs } from '@lota-sdk/shared/schemas/tools'
3
+ import { tool } from 'ai'
4
+
5
+ import type { ToolDefinition } from '../ai/definitions'
6
+
7
+ export const userQuestionsTool = {
8
+ name: USER_QUESTIONS_TOOL_NAME,
9
+ create: () =>
10
+ tool({
11
+ description:
12
+ 'Present structured questions to the user and wait for their response. The chat UI already renders the questions, so do not repeat them as assistant text. Use this when you need clarification or input from the user. Execution stops after this tool is called.',
13
+ inputSchema: UserQuestionsArgsSchema,
14
+ execute: async (_args: UserQuestionsArgs) => {
15
+ return 'Questions have been presented to the user in the chat UI. Do NOT restate them in text, and do NOT generate further text or tool calls. Your turn is complete.'
16
+ },
17
+ }),
18
+ } as const satisfies ToolDefinition<void>
@@ -0,0 +1,50 @@
1
+ import { getErrorMessage } from './error'
2
+
3
+ class TimeoutError extends Error {
4
+ constructor(operation: string, ms: number) {
5
+ super(`${operation} timed out after ${ms}ms`)
6
+ this.name = 'TimeoutError'
7
+ }
8
+ }
9
+
10
+ export async function withTimeout<T>(promise: Promise<T>, ms: number, operation: string): Promise<T> {
11
+ const controller = new AbortController()
12
+ const timeoutId = setTimeout(() => {
13
+ controller.abort()
14
+ }, ms)
15
+
16
+ try {
17
+ return await Promise.race([
18
+ promise,
19
+ new Promise<never>((_, reject) => {
20
+ controller.signal.addEventListener('abort', () => {
21
+ reject(new TimeoutError(operation, ms))
22
+ })
23
+ }),
24
+ ])
25
+ } finally {
26
+ clearTimeout(timeoutId)
27
+ }
28
+ }
29
+
30
+ export function createSafeEnqueue(logger: { warn: (message: string) => void }) {
31
+ return function safeEnqueue<T>(
32
+ operation: () => Promise<T>,
33
+ options: { operationName: string; onError?: (error: unknown) => void; logPrefix?: string },
34
+ ): Promise<T | void> {
35
+ const { operationName, onError, logPrefix = 'Failed to enqueue' } = options
36
+
37
+ return operation().catch((error: unknown) => {
38
+ logger.warn(`${logPrefix} ${operationName} (non-fatal): ${getErrorMessage(error)}`)
39
+ onError?.(error)
40
+ })
41
+ }
42
+ }
43
+
44
+ const _defaultSafeEnqueue = createSafeEnqueue({ warn: console.warn })
45
+ export function safeEnqueue<T>(
46
+ operation: () => T | Promise<T>,
47
+ options: { operationName: string; onError?: (error: unknown) => void; logPrefix?: string },
48
+ ): Promise<T | void> {
49
+ return _defaultSafeEnqueue(() => Promise.resolve(operation()), options)
50
+ }
@@ -0,0 +1,34 @@
1
+ export function toIsoDateTimeString(value: unknown): string {
2
+ if (value instanceof Date) {
3
+ return value.toISOString()
4
+ }
5
+
6
+ // Assume API boundaries already use ISO strings.
7
+ if (typeof value === 'string') {
8
+ return value
9
+ }
10
+
11
+ // Support unix timestamps (seconds or milliseconds).
12
+ if (typeof value === 'number') {
13
+ const millis = value < 1_000_000_000_000 ? value * 1000 : value
14
+ return new Date(millis).toISOString()
15
+ }
16
+
17
+ // Support objects that expose toISOString (e.g., SurrealDB temporal types).
18
+ if (value && typeof value === 'object') {
19
+ const maybeToIso = (value as { toISOString?: unknown }).toISOString
20
+ if (typeof maybeToIso === 'function') {
21
+ return (value as { toISOString: () => string }).toISOString()
22
+ }
23
+ }
24
+
25
+ return String(value)
26
+ }
27
+
28
+ export function toOptionalIsoDateTimeString(value: unknown): string | undefined {
29
+ if (value === null || value === undefined || value === '') {
30
+ return undefined
31
+ }
32
+
33
+ return toIsoDateTimeString(value)
34
+ }
@@ -0,0 +1,10 @@
1
+ export function getErrorMessage(error: unknown): string {
2
+ if (error instanceof Error) return error.message
3
+ if (typeof error === 'string') return error
4
+
5
+ try {
6
+ return JSON.stringify(error)
7
+ } catch {
8
+ return String(error)
9
+ }
10
+ }
@@ -0,0 +1,28 @@
1
+ export class AppError extends Error {
2
+ public readonly code: string
3
+ public readonly statusCode: number
4
+
5
+ constructor(message: string, code: string, statusCode: number) {
6
+ super(message)
7
+ this.name = this.constructor.name
8
+ this.code = code
9
+ this.statusCode = statusCode
10
+ Error.captureStackTrace(this, this.constructor)
11
+ }
12
+
13
+ public toResponse() {
14
+ return { status: this.statusCode, body: { error: { code: this.code, message: this.message } } }
15
+ }
16
+ }
17
+
18
+ export class NotFoundError extends AppError {
19
+ constructor(message = 'Resource not found') {
20
+ super(message, 'NOT_FOUND', 404)
21
+ }
22
+ }
23
+
24
+ export class BadRequestError extends AppError {
25
+ constructor(message = 'Bad request') {
26
+ super(message, 'BAD_REQUEST', 400)
27
+ }
28
+ }