@lota-sdk/core 0.1.14 → 0.1.16

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 (174) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +9 -8
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/embedding-cache.ts +7 -6
  11. package/src/ai/index.ts +0 -1
  12. package/src/bifrost/bifrost.ts +14 -14
  13. package/src/config/agent-defaults.ts +32 -22
  14. package/src/config/agent-types.ts +11 -0
  15. package/src/config/constants.ts +2 -14
  16. package/src/config/debug-logger.ts +5 -1
  17. package/src/config/index.ts +3 -0
  18. package/src/config/logger.ts +7 -9
  19. package/src/config/model-constants.ts +16 -34
  20. package/src/config/search.ts +1 -15
  21. package/src/create-runtime.ts +453 -0
  22. package/src/db/cursor-pagination.ts +3 -6
  23. package/src/db/index.ts +2 -0
  24. package/src/db/memory-store.rows.ts +7 -7
  25. package/src/db/memory-store.ts +24 -24
  26. package/src/db/memory.ts +18 -16
  27. package/src/db/schema-fingerprint.ts +1 -0
  28. package/src/db/service.ts +193 -122
  29. package/src/db/startup.ts +9 -13
  30. package/src/db/surreal-mutation.ts +43 -0
  31. package/src/db/tables.ts +7 -0
  32. package/src/db/workstream-message-row.ts +15 -0
  33. package/src/embeddings/provider.ts +1 -1
  34. package/src/index.ts +1 -1
  35. package/src/queues/context-compaction.queue.ts +17 -52
  36. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  37. package/src/queues/document-processor.queue.ts +7 -7
  38. package/src/queues/index.ts +3 -0
  39. package/src/queues/memory-consolidation.queue.ts +18 -54
  40. package/src/queues/plan-scheduler.queue.ts +97 -0
  41. package/src/queues/post-chat-memory.queue.ts +15 -60
  42. package/src/queues/queue-factory.ts +100 -0
  43. package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
  44. package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
  45. package/src/queues/skill-extraction.queue.ts +15 -50
  46. package/src/queues/workstream-title-generation.queue.ts +15 -51
  47. package/src/redis/connection.ts +12 -3
  48. package/src/redis/index.ts +2 -1
  49. package/src/redis/org-memory-lock.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +41 -8
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +106 -21
  53. package/src/runtime/agent-stream-helpers.ts +2 -1
  54. package/src/runtime/approval-continuation.ts +12 -6
  55. package/src/runtime/context-compaction-constants.ts +1 -1
  56. package/src/runtime/context-compaction-runtime.ts +7 -5
  57. package/src/runtime/context-compaction.ts +40 -97
  58. package/src/runtime/execution-plan.ts +23 -19
  59. package/src/runtime/graph-designer.ts +15 -0
  60. package/src/runtime/helper-model.ts +10 -196
  61. package/src/runtime/index.ts +14 -1
  62. package/src/runtime/llm-content.ts +1 -1
  63. package/src/runtime/memory-block.ts +11 -12
  64. package/src/runtime/memory-pipeline.ts +26 -10
  65. package/src/runtime/plugin-resolution.ts +35 -0
  66. package/src/runtime/plugin-types.ts +73 -1
  67. package/src/runtime/retrieval-adapters.ts +1 -1
  68. package/src/runtime/runtime-config.ts +25 -12
  69. package/src/runtime/runtime-extensions.ts +91 -15
  70. package/src/runtime/runtime-worker-registry.ts +6 -0
  71. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  72. package/src/runtime/team-consultation-prompts.ts +11 -2
  73. package/src/runtime/title-helpers.ts +11 -4
  74. package/src/runtime/workstream-chat-helpers.ts +6 -7
  75. package/src/runtime/workstream-routing-policy.ts +0 -30
  76. package/src/runtime/workstream-state.ts +17 -7
  77. package/src/services/adaptive-playbook.service.ts +152 -0
  78. package/src/services/agent-executor.service.ts +293 -0
  79. package/src/services/artifact-provenance.service.ts +172 -0
  80. package/src/services/attachment.service.ts +7 -12
  81. package/src/services/context-compaction.service.ts +75 -58
  82. package/src/services/context-enrichment.service.ts +33 -0
  83. package/src/services/coordination-registry.service.ts +117 -0
  84. package/src/services/document-chunk.service.ts +38 -33
  85. package/src/services/domain-agent-executor.service.ts +71 -0
  86. package/src/services/execution-plan.service.ts +271 -50
  87. package/src/services/feedback-loop.service.ts +96 -0
  88. package/src/services/global-orchestrator.service.ts +148 -0
  89. package/src/services/index.ts +26 -0
  90. package/src/services/institutional-memory.service.ts +145 -0
  91. package/src/services/learned-skill.service.ts +30 -15
  92. package/src/services/memory-assessment.service.ts +3 -2
  93. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
  94. package/src/services/memory.service.ts +55 -69
  95. package/src/services/monitoring-window.service.ts +86 -0
  96. package/src/services/mutating-approval.service.ts +1 -1
  97. package/src/services/node-workspace.service.ts +155 -0
  98. package/src/services/notification.service.ts +39 -0
  99. package/src/services/organization-member.service.ts +12 -5
  100. package/src/services/organization.service.ts +5 -5
  101. package/src/services/ownership-dispatcher.service.ts +403 -0
  102. package/src/services/plan-approval.service.ts +1 -1
  103. package/src/services/plan-artifact.service.ts +1 -0
  104. package/src/services/plan-builder.service.ts +1 -0
  105. package/src/services/plan-checkpoint.service.ts +30 -2
  106. package/src/services/plan-compiler.service.ts +5 -0
  107. package/src/services/plan-coordination.service.ts +152 -0
  108. package/src/services/plan-cycle.service.ts +284 -0
  109. package/src/services/plan-deadline.service.ts +287 -0
  110. package/src/services/plan-executor.service.ts +386 -58
  111. package/src/services/plan-helpers.ts +15 -0
  112. package/src/services/plan-run.service.ts +41 -7
  113. package/src/services/plan-scheduler.service.ts +240 -0
  114. package/src/services/plan-template.service.ts +117 -0
  115. package/src/services/plan-validator.service.ts +87 -20
  116. package/src/services/plan-workspace.service.ts +83 -0
  117. package/src/services/playbook-registry.service.ts +67 -0
  118. package/src/services/plugin-executor.service.ts +103 -0
  119. package/src/services/quality-metrics.service.ts +132 -0
  120. package/src/services/recent-activity-title.service.ts +3 -10
  121. package/src/services/recent-activity.service.ts +33 -43
  122. package/src/services/skill-resolver.service.ts +19 -0
  123. package/src/services/system-executor.service.ts +105 -0
  124. package/src/services/workstream-message.service.ts +29 -41
  125. package/src/services/workstream-plan-registry.service.ts +22 -0
  126. package/src/services/workstream-title.service.ts +3 -9
  127. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
  128. package/src/services/workstream-turn.ts +2 -2
  129. package/src/services/workstream.service.ts +55 -65
  130. package/src/services/workstream.types.ts +10 -19
  131. package/src/services/write-intent-validator.service.ts +81 -0
  132. package/src/storage/attachment-parser.ts +1 -1
  133. package/src/storage/attachment-storage.service.ts +4 -4
  134. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
  135. package/src/storage/generated-document-storage.service.ts +3 -2
  136. package/src/storage/index.ts +2 -2
  137. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  138. package/src/system-agents/delegated-agent-factory.ts +5 -2
  139. package/src/system-agents/index.ts +8 -0
  140. package/src/system-agents/memory-reranker.agent.ts +1 -1
  141. package/src/system-agents/memory.agent.ts +1 -1
  142. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  143. package/src/tools/execution-plan.tool.ts +17 -19
  144. package/src/tools/fetch-webpage.tool.ts +20 -18
  145. package/src/tools/index.ts +2 -3
  146. package/src/tools/read-file-parts.tool.ts +1 -1
  147. package/src/tools/search-web.tool.ts +18 -15
  148. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  149. package/src/tools/team-think.tool.ts +14 -8
  150. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  151. package/src/utils/async.ts +3 -2
  152. package/src/utils/date-time.ts +4 -32
  153. package/src/utils/env.ts +8 -0
  154. package/src/utils/errors.ts +47 -0
  155. package/src/utils/hono-error-handler.ts +1 -2
  156. package/src/utils/index.ts +19 -2
  157. package/src/utils/string.ts +128 -1
  158. package/src/workers/bootstrap.ts +2 -2
  159. package/src/workers/index.ts +1 -0
  160. package/src/workers/memory-consolidation.worker.ts +12 -12
  161. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  162. package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
  163. package/src/workers/skill-extraction.runner.ts +8 -102
  164. package/src/workers/utils/file-section-chunker.ts +6 -3
  165. package/src/workers/utils/repomix-file-sections.ts +2 -2
  166. package/src/workers/utils/sandbox-error.ts +11 -2
  167. package/src/workers/utils/workstream-message-query.ts +97 -0
  168. package/src/workers/worker-utils.ts +6 -2
  169. package/src/runtime/retrieval-pipeline.ts +0 -3
  170. package/src/runtime.ts +0 -387
  171. package/src/tools/log-hello-world.tool.ts +0 -17
  172. package/src/utils/error.ts +0 -10
  173. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  174. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -1,31 +1,17 @@
1
1
  import {
2
2
  CreateExecutionPlanArgsSchema,
3
3
  GetActiveExecutionPlanArgsSchema,
4
+ ListExecutionPlansArgsSchema,
4
5
  ResumeExecutionPlanRunArgsSchema,
5
6
  ReplaceExecutionPlanArgsSchema,
6
7
  SubmitExecutionNodeResultArgsSchema,
8
+ getLatestExecutionPlanResult,
7
9
  } from '@lota-sdk/shared'
8
- import type { ExecutionPlanToolResultData } from '@lota-sdk/shared'
9
10
  import { tool } from 'ai'
10
11
 
11
12
  import type { RecordIdRef } from '../db/record-id'
12
13
  import { executionPlanService } from '../services/execution-plan.service'
13
14
 
14
- function getLatestExecutionPlanToolResult(output: unknown): ExecutionPlanToolResultData | undefined {
15
- if (output && typeof output === 'object' && 'hasPlan' in output) {
16
- return output as ExecutionPlanToolResultData
17
- }
18
-
19
- if (Array.isArray(output)) {
20
- for (let index = output.length - 1; index >= 0; index -= 1) {
21
- const candidate = getLatestExecutionPlanToolResult(output[index])
22
- if (candidate) return candidate
23
- }
24
- }
25
-
26
- return undefined
27
- }
28
-
29
15
  export function createCreateExecutionPlanTool(params: {
30
16
  orgId: RecordIdRef
31
17
  workstreamId: RecordIdRef
@@ -41,6 +27,7 @@ export function createCreateExecutionPlanTool(params: {
41
27
  organizationId: params.orgId,
42
28
  workstreamId: params.workstreamId,
43
29
  leadAgentId: params.agentId,
30
+ dispatchMode: 'deferred',
44
31
  input,
45
32
  })
46
33
  params.onPlanChanged?.()
@@ -64,6 +51,7 @@ export function createReplaceExecutionPlanTool(params: {
64
51
  organizationId: params.orgId,
65
52
  workstreamId: params.workstreamId,
66
53
  leadAgentId: params.agentId,
54
+ dispatchMode: 'deferred',
67
55
  input,
68
56
  })
69
57
  params.onPlanChanged?.()
@@ -91,21 +79,31 @@ export function createSubmitExecutionNodeResultTool(params: {
91
79
  return result
92
80
  },
93
81
  toModelOutput: ({ output }) => {
94
- const result = getLatestExecutionPlanToolResult(output)
82
+ const result = getLatestExecutionPlanResult(output)
95
83
  const summary = result?.message?.trim()
96
84
  return { type: 'text', value: summary && summary.length > 0 ? summary : 'Execution node result submitted.' }
97
85
  },
98
86
  })
99
87
  }
100
88
 
101
- export function createGetActiveExecutionPlanTool(params: { workstreamId: RecordIdRef }) {
89
+ export function createListExecutionPlansTool(params: { workstreamId: RecordIdRef }) {
90
+ return tool({
91
+ description:
92
+ 'List all active execution plans for this workstream with summary info (title, status, objective, node counts). Use getExecutionPlanDetails to inspect a specific plan.',
93
+ inputSchema: ListExecutionPlansArgsSchema,
94
+ execute: async () => await executionPlanService.listActivePlanSummaries(params.workstreamId),
95
+ })
96
+ }
97
+
98
+ export function createGetExecutionPlanDetailsTool(params: { workstreamId: RecordIdRef }) {
102
99
  return tool({
103
100
  description:
104
- 'Load the active execution run for this workstream, including graph state, node contracts, recent events, approvals, artifacts, and checkpoints when requested.',
101
+ 'Load a specific execution run by runId, or the most recent active run if runId is omitted. Returns full graph state, node contracts, events, approvals, artifacts, and checkpoints.',
105
102
  inputSchema: GetActiveExecutionPlanArgsSchema,
106
103
  execute: async (input) =>
107
104
  await executionPlanService.getActivePlanToolResult({
108
105
  workstreamId: params.workstreamId,
106
+ runId: input.runId,
109
107
  includeEvents: input.includeEvents,
110
108
  includeArtifacts: input.includeArtifacts,
111
109
  includeApprovals: input.includeApprovals,
@@ -2,10 +2,10 @@ import { tool } from 'ai'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import type { ToolDefinition } from '../ai/definitions'
5
- import type { Citation } from '../services/workstream.types'
6
5
  import { withTimeout } from '../utils/async'
7
6
  import { readStringField, truncateOptionalText } from '../utils/string'
8
7
  import { getFirecrawlClient } from './firecrawl-client'
8
+ import type { WebCitation } from './tool-contracts'
9
9
 
10
10
  const TOOL_TIMEOUT_MS = 30_000
11
11
  const FormatSchema = z.enum(['markdown', 'html', 'rawHtml', 'links', 'images', 'screenshot', 'summary'])
@@ -14,6 +14,11 @@ const MAX_SUMMARY_CHARS = 1_200
14
14
  const MAX_LINKS = 25
15
15
  const MAX_IMAGES = 10
16
16
 
17
+ function toRecord(value: unknown): Record<string, unknown> | null {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
19
+ return value as Record<string, unknown>
20
+ }
21
+
17
22
  function readStringList(record: Record<string, unknown>, key: string, maxItems: number): string[] {
18
23
  const value = record[key]
19
24
  if (!Array.isArray(value)) return []
@@ -24,15 +29,12 @@ function readStringList(record: Record<string, unknown>, key: string, maxItems:
24
29
  }
25
30
 
26
31
  function summarizeDocument(url: string, document: unknown): Record<string, unknown> {
27
- if (!document || typeof document !== 'object' || Array.isArray(document)) {
32
+ const record = toRecord(document)
33
+ if (!record) {
28
34
  return { url }
29
35
  }
30
36
 
31
- const record = document as Record<string, unknown>
32
- const metadata =
33
- record.metadata && typeof record.metadata === 'object' && !Array.isArray(record.metadata)
34
- ? (record.metadata as Record<string, unknown>)
35
- : {}
37
+ const metadata = toRecord(record.metadata) ?? {}
36
38
 
37
39
  const canonicalUrl = truncateOptionalText(
38
40
  readStringField(metadata, 'url') ?? readStringField(metadata, 'sourceURL') ?? readStringField(record, 'url') ?? url,
@@ -51,8 +53,8 @@ function summarizeDocument(url: string, document: unknown): Record<string, unkno
51
53
  return truncateOptionalText(image, 500)
52
54
  }
53
55
 
54
- if (image && typeof image === 'object' && !Array.isArray(image)) {
55
- const imageRecord = image as Record<string, unknown>
56
+ const imageRecord = toRecord(image)
57
+ if (imageRecord) {
56
58
  return truncateOptionalText(readStringField(imageRecord, 'src') ?? readStringField(imageRecord, 'url'), 500)
57
59
  }
58
60
 
@@ -73,18 +75,18 @@ function summarizeDocument(url: string, document: unknown): Record<string, unkno
73
75
  }
74
76
  }
75
77
 
76
- function buildFetchCitations(url: string, document: unknown): Citation[] {
78
+ function buildFetchCitations(url: string, document: unknown): WebCitation[] {
77
79
  const fallbackUrl = url.trim()
78
80
  let sourceId = fallbackUrl
79
81
 
80
- if (document && typeof document === 'object') {
81
- const metadata = (document as Record<string, unknown>).metadata
82
- if (metadata && typeof metadata === 'object') {
83
- const record = metadata as Record<string, unknown>
84
- if (typeof record.url === 'string' && record.url.trim().length > 0) {
85
- sourceId = record.url.trim()
86
- } else if (typeof record.sourceURL === 'string' && record.sourceURL.trim().length > 0) {
87
- sourceId = record.sourceURL.trim()
82
+ const docRecord = toRecord(document)
83
+ if (docRecord) {
84
+ const metadataRecord = toRecord(docRecord.metadata)
85
+ if (metadataRecord) {
86
+ if (typeof metadataRecord.url === 'string' && metadataRecord.url.trim().length > 0) {
87
+ sourceId = metadataRecord.url.trim()
88
+ } else if (typeof metadataRecord.sourceURL === 'string' && metadataRecord.sourceURL.trim().length > 0) {
89
+ sourceId = metadataRecord.sourceURL.trim()
88
90
  }
89
91
  }
90
92
  }
@@ -1,12 +1,11 @@
1
1
  export * from './execution-plan.tool'
2
2
  export * from './fetch-webpage.tool'
3
- export * from './log-hello-world.tool'
4
3
  export * from './memory-block.tool'
5
4
  export * from './read-file-parts.tool'
6
5
  export * from './remember-memory.tool'
7
6
  export * from './research-topic.tool'
8
- export * from './search-tools'
7
+ export * from './search.tool'
9
8
  export * from './search-web.tool'
10
9
  export * from './team-think.tool'
11
- export * from './tool-contract'
10
+ export * from './tool-contracts'
12
11
  export * from './user-questions.tool'
@@ -3,7 +3,7 @@ import { z } from 'zod'
3
3
 
4
4
  import type { ToolDefinition } from '../ai/definitions'
5
5
  import { attachmentStorageService } from '../storage/attachment-storage.service'
6
- import type { ReadableUploadMetadata } from '../storage/attachments.types'
6
+ import type { ReadableUploadMetadata } from '../storage/attachment-types'
7
7
 
8
8
  const PAGES_PER_PART = 25
9
9
 
@@ -2,22 +2,26 @@ import { tool } from 'ai'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import type { ToolDefinition } from '../ai/definitions'
5
- import type { Citation } from '../services/workstream.types'
6
5
  import { withTimeout } from '../utils/async'
7
6
  import { readStringField, truncateOptionalText } from '../utils/string'
8
7
  import { getFirecrawlClient } from './firecrawl-client'
8
+ import type { WebCitation } from './tool-contracts'
9
9
 
10
10
  const TOOL_TIMEOUT_MS = 30_000
11
11
  const SourceSchema = z.enum(['web', 'news', 'images'])
12
12
  const MAX_RESULTS_PER_SOURCE = 4
13
13
  const MAX_SNIPPET_CHARS = 320
14
14
 
15
+ function toRecord(value: unknown): Record<string, unknown> | null {
16
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
17
+ return value as Record<string, unknown>
18
+ }
19
+
15
20
  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
+ const record = toRecord(item)
22
+ if (!record) return {}
23
+ const metadata = toRecord(record.metadata)
24
+ return metadata ?? {}
21
25
  }
22
26
 
23
27
  function readSearchItemSnippet(record: Record<string, unknown>, metadata: Record<string, unknown>): string | undefined {
@@ -36,9 +40,9 @@ function readSearchItemSnippet(record: Record<string, unknown>, metadata: Record
36
40
  }
37
41
 
38
42
  function summarizeSearchItem(item: unknown): Record<string, string> | null {
39
- if (!item || typeof item !== 'object' || Array.isArray(item)) return null
43
+ const record = toRecord(item)
44
+ if (!record) return null
40
45
 
41
- const record = item as Record<string, unknown>
42
46
  const metadata = readMetadata(item)
43
47
  const url = extractUrlFromSearchItem(item)
44
48
 
@@ -79,9 +83,9 @@ function summarizeSearchItems(items: unknown[] | undefined): Record<string, unkn
79
83
  }
80
84
 
81
85
  function extractUrlFromSearchItem(item: unknown): string | undefined {
82
- if (!item || typeof item !== 'object') return undefined
86
+ const record = toRecord(item)
87
+ if (!record) return undefined
83
88
 
84
- const record = item as Record<string, unknown>
85
89
  if (typeof record.url === 'string' && record.url.trim().length > 0) {
86
90
  return record.url.trim()
87
91
  }
@@ -89,9 +93,8 @@ function extractUrlFromSearchItem(item: unknown): string | undefined {
89
93
  return record.imageUrl.trim()
90
94
  }
91
95
 
92
- const metadata = record.metadata
93
- if (metadata && typeof metadata === 'object') {
94
- const metadataRecord = metadata as Record<string, unknown>
96
+ const metadataRecord = toRecord(record.metadata)
97
+ if (metadataRecord) {
95
98
  if (typeof metadataRecord.url === 'string' && metadataRecord.url.trim().length > 0) {
96
99
  return metadataRecord.url.trim()
97
100
  }
@@ -106,9 +109,9 @@ function extractUrlFromSearchItem(item: unknown): string | undefined {
106
109
  return undefined
107
110
  }
108
111
 
109
- function buildWebCitations(results: { web?: unknown[]; news?: unknown[]; images?: unknown[] }): Citation[] {
112
+ function buildWebCitations(results: { web?: unknown[]; news?: unknown[]; images?: unknown[] }): WebCitation[] {
110
113
  const seen = new Set<string>()
111
- const citations: Citation[] = []
114
+ const citations: WebCitation[] = []
112
115
  const retrievedAt = new Date().toISOString()
113
116
 
114
117
  const append = (items: unknown[] | undefined) => {
@@ -22,7 +22,7 @@ export function createMemorySearchTool(
22
22
  const normalizedQuery = query.trim()
23
23
  const retrieval = await memoryService.searchAllMemoriesBatched({
24
24
  orgId: orgIdString,
25
- agentName: isAgentName(agentName) ? (agentName as string) : undefined,
25
+ agentName: isAgentName(agentName) ? agentName : undefined,
26
26
  query: normalizedQuery,
27
27
  ...(typeof options?.fastMode === 'boolean' ? { fastMode: options.fastMode } : {}),
28
28
  ...(typeof options?.allowMultiScopeRerank === 'boolean'
@@ -9,22 +9,26 @@ import { recordIdToString } from '../db/record-id'
9
9
  import { TABLES } from '../db/tables'
10
10
  import { mergeInstructionSections } from '../runtime/instruction-sections'
11
11
  import { getRuntimeAdapters } from '../runtime/runtime-extensions'
12
+ import type { LotaRuntimeTeamThinkToolsParams } from '../runtime/runtime-extensions'
12
13
  import { createConsultTeamTool as createConsultTeamToolSdk } from '../runtime/team-consultation-orchestrator'
13
14
  import type { DefaultRepoSections, TeamConsultationParticipantRunner } from '../runtime/team-consultation-orchestrator'
14
15
  import { buildTeamConsultationResponseGuard } from '../runtime/team-consultation-prompts'
15
16
  import type { ReadableUploadMetadata } from '../services/attachment.service'
16
17
 
17
- async function buildTeamThinkAgentTools(params: Record<string, unknown>): Promise<{ tools: Record<string, unknown> }> {
18
+ async function buildTeamThinkAgentTools(
19
+ params: LotaRuntimeTeamThinkToolsParams,
20
+ ): Promise<{ tools: Record<string, unknown> }> {
18
21
  const builder = getRuntimeAdapters().workstream?.buildTeamThinkAgentTools
19
22
  if (!builder) {
20
23
  return { tools: {} }
21
24
  }
22
25
 
23
- const result = await builder(params as never)
26
+ const result = await builder(params)
24
27
  return { tools: result.tools as Record<string, unknown> }
25
28
  }
26
29
 
27
- const TEAM_THINK_AGENT_MAX_RETRIES = 4
30
+ const TEAM_THINK_AGENT_MAX_RETRIES = 1
31
+ const TEAM_THINK_AGENT_MAX_STEPS = 3
28
32
 
29
33
  export function createTeamThinkTool(params: {
30
34
  historyMessages: ChatMessage[]
@@ -55,7 +59,7 @@ export function createTeamThinkTool(params: {
55
59
  mode: 'fixedWorkstreamMode',
56
60
  onboardingActive: false,
57
61
  linearInstalled: false,
58
- reasoningProfile: runParams.reasoningProfile,
62
+ reasoningProfile: 'fast',
59
63
  systemWorkspaceDetails: runParams.systemWorkspaceDetails,
60
64
  preSeededMemoriesSection: runParams.preSeededMemoriesSection,
61
65
  retrievedKnowledgeSection: runParams.retrievedKnowledgeSection,
@@ -72,20 +76,22 @@ export function createTeamThinkTool(params: {
72
76
  workspaceIdString: recordIdToString(params.orgId, TABLES.ORGANIZATION),
73
77
  workstreamId: params.workstreamId,
74
78
  githubInstalled: params.githubInstalled,
75
- provideRepoTool: params.provideRepoTool,
79
+ provideRepoTool: agentId !== 'mentor' && params.provideRepoTool,
76
80
  availableUploads: params.availableUploads,
77
81
  defaultRepoSections: params.defaultRepoSectionsByAgent[agentId],
78
82
  context: params.context,
79
83
  toolProviders: params.toolProviders,
80
84
  })
81
85
  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]({
86
+ const agentId_ = typeof agentConfig.id === 'string' ? agentConfig.id : agentId
87
+ const configuredMaxSteps = typeof agentConfig.maxSteps === 'number' ? agentConfig.maxSteps : 10
88
+ const maxSteps = Math.min(configuredMaxSteps, TEAM_THINK_AGENT_MAX_STEPS)
89
+ const agent = createAgent[agentId_]({
84
90
  mode: 'fixedWorkstreamMode',
85
91
  tools,
86
92
  extraInstructions: agentConfig.extraInstructions,
87
93
  maxRetries: TEAM_THINK_AGENT_MAX_RETRIES,
88
- stopWhen: [stepCountIs(agentConfig.maxSteps as number)],
94
+ stopWhen: [stepCountIs(maxSteps)],
89
95
  })
90
96
  const observer = {
91
97
  run: async <T>(fn: () => T | Promise<T>): Promise<T> => fn(),
@@ -7,7 +7,6 @@ export const MutatingApprovalSchema = {
7
7
  approvalMessageId: z.string().trim().min(1).max(200).optional(),
8
8
  } as const
9
9
 
10
- /** @lintignore */
11
10
  export const CitationSchema = z
12
11
  .object({
13
12
  source: z.string().trim().min(1),
@@ -17,5 +16,13 @@ export const CitationSchema = z
17
16
  })
18
17
  .strict()
19
18
 
20
- /** @lintignore */
21
19
  export type Citation = z.infer<typeof CitationSchema>
20
+
21
+ export interface WebCitation {
22
+ title?: string
23
+ url?: string
24
+ snippet?: string
25
+ source?: string
26
+ sourceId?: string
27
+ retrievedAt?: string
28
+ }
@@ -1,4 +1,5 @@
1
- import { getErrorMessage } from './error'
1
+ import { serverLogger } from '../config/logger'
2
+ import { getErrorMessage } from './errors'
2
3
 
3
4
  class TimeoutError extends Error {
4
5
  constructor(operation: string, ms: number) {
@@ -41,7 +42,7 @@ export function createSafeEnqueue(logger: { warn: (message: string) => void }) {
41
42
  }
42
43
  }
43
44
 
44
- const _defaultSafeEnqueue = createSafeEnqueue({ warn: console.warn })
45
+ const _defaultSafeEnqueue = createSafeEnqueue({ warn: (message: string) => serverLogger.warn`${message}` })
45
46
  export function safeEnqueue<T>(
46
47
  operation: () => T | Promise<T>,
47
48
  options: { operationName: string; onError?: (error: unknown) => void; logPrefix?: string },
@@ -1,34 +1,6 @@
1
- export function toIsoDateTimeString(value: unknown): string {
2
- if (value instanceof Date) {
3
- return value.toISOString()
4
- }
1
+ export { toIsoDateTimeString, toOptionalIsoDateTimeString } from '@lota-sdk/shared'
5
2
 
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)
3
+ export function toDatabaseDateTime(value: string | Date | null | undefined): Date | undefined {
4
+ if (value === null || value === undefined) return undefined
5
+ return value instanceof Date ? value : new Date(value)
34
6
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Reads a required environment variable. Throws if the variable is missing or empty.
3
+ */
4
+ export function getRequiredEnv(key: string): string {
5
+ const value = process.env[key]
6
+ if (!value) throw new Error(`Missing required env var: ${key}`)
7
+ return value
8
+ }
@@ -1,3 +1,9 @@
1
+ export { getErrorMessage } from '@lota-sdk/shared'
2
+
3
+ export function toError(value: unknown): Error {
4
+ return value instanceof Error ? value : new Error(String(value))
5
+ }
6
+
1
7
  export class AppError extends Error {
2
8
  public readonly code: string
3
9
  public readonly statusCode: number
@@ -26,3 +32,44 @@ export class BadRequestError extends AppError {
26
32
  super(message, 'BAD_REQUEST', 400)
27
33
  }
28
34
  }
35
+
36
+ type ErrorCode =
37
+ | 'UNAUTHORIZED'
38
+ | 'FORBIDDEN'
39
+ | 'NOT_FOUND'
40
+ | 'CONFLICT'
41
+ | 'BAD_REQUEST'
42
+ | 'VALIDATION_ERROR'
43
+ | 'INTERNAL_SERVER_ERROR'
44
+ | 'HTTP_ERROR'
45
+ | 'TOO_MANY_REQUESTS'
46
+
47
+ interface ValidationIssue {
48
+ path: string
49
+ message: string
50
+ }
51
+
52
+ export interface ErrorBody {
53
+ error: { code: ErrorCode; message: string; issues?: ValidationIssue[] }
54
+ }
55
+
56
+ function createErrorResponse(code: ErrorCode, message: string): ErrorBody {
57
+ return { error: { code, message } }
58
+ }
59
+
60
+ function createValidationErrorResponse(message: string, issues: ValidationIssue[]): ErrorBody {
61
+ return { error: { code: 'VALIDATION_ERROR', message, issues } }
62
+ }
63
+
64
+ export const errorResponses = {
65
+ unauthorized: (message = 'Invalid credentials') => createErrorResponse('UNAUTHORIZED', message),
66
+ forbidden: (message = 'Access denied') => createErrorResponse('FORBIDDEN', message),
67
+ notFound: (message: string) => createErrorResponse('NOT_FOUND', message),
68
+ conflict: (message: string) => createErrorResponse('CONFLICT', message),
69
+ badRequest: (message: string) => createErrorResponse('BAD_REQUEST', message),
70
+ tooManyRequests: (message = 'Too many requests') => createErrorResponse('TOO_MANY_REQUESTS', message),
71
+ serverError: (message = 'Internal server error') => createErrorResponse('INTERNAL_SERVER_ERROR', message),
72
+ validationError: (issues: ValidationIssue[], message = 'Validation failed') =>
73
+ createValidationErrorResponse(message, issues),
74
+ httpError: (message: string) => createErrorResponse('HTTP_ERROR', message),
75
+ } as const
@@ -2,8 +2,7 @@ import type { ErrorHandler } from 'hono'
2
2
  import { HTTPException } from 'hono/http-exception'
3
3
  import { ZodError } from 'zod'
4
4
 
5
- import { getErrorMessage } from './error'
6
- import { AppError } from './errors'
5
+ import { AppError, getErrorMessage } from './errors'
7
6
 
8
7
  type AppErrorLike = Pick<AppError, 'code' | 'message' | 'statusCode' | 'toResponse'> & { name?: string }
9
8
 
@@ -1,6 +1,23 @@
1
1
  export * from './async'
2
2
  export * from './date-time'
3
- export * from './error'
3
+ export * from './env'
4
4
  export * from './errors'
5
5
  export * from './hono-error-handler'
6
- export * from './string'
6
+ export * from './sse-keepalive'
7
+ export {
8
+ CHARS_PER_TOKEN_ESTIMATE,
9
+ clampImportance,
10
+ compactRecord,
11
+ compactWhitespace,
12
+ isRecord,
13
+ parseLineList,
14
+ readRecordArray,
15
+ readString,
16
+ readStringArray,
17
+ readStringField,
18
+ slugify,
19
+ stringifyLineList,
20
+ stringifyUnknown,
21
+ truncateOptionalText,
22
+ truncateText,
23
+ } from './string'
@@ -29,7 +29,7 @@ export function readStringField(record: Record<string, unknown>, key: string): s
29
29
  * Returns the original string when it fits within maxChars.
30
30
  */
31
31
  export function truncateText(value: string, maxChars: number): string {
32
- return value.length <= maxChars ? value : `${value.slice(0, maxChars).trimEnd()}...`
32
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars - 3).trimEnd()}...`
33
33
  }
34
34
 
35
35
  /**
@@ -49,3 +49,130 @@ export function truncateOptionalText(value: string | undefined, maxChars: number
49
49
  export function compactWhitespace(value: string): string {
50
50
  return value.trim().replace(/\s+/g, ' ')
51
51
  }
52
+
53
+ /**
54
+ * Returns the value as a plain record if it is a non-null, non-array object,
55
+ * or null otherwise.
56
+ */
57
+ export function readRecord(value: unknown): Record<string, unknown> | null {
58
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
59
+ return value as Record<string, unknown>
60
+ }
61
+
62
+ /**
63
+ * Strips null and undefined values from a record, returning a new object with only defined entries.
64
+ */
65
+ export function compactRecord(value: Record<string, unknown>): Record<string, unknown> {
66
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined))
67
+ }
68
+
69
+ /**
70
+ * Converts a string into a URL-safe slug: lowercase, non-alphanumeric runs
71
+ * replaced with hyphens, leading/trailing hyphens stripped, capped at 80 chars.
72
+ */
73
+ export function slugify(value: string): string {
74
+ return value
75
+ .trim()
76
+ .toLowerCase()
77
+ .replace(/[^a-z0-9]+/g, '-')
78
+ .replace(/(^-|-$)/g, '')
79
+ .slice(0, 80)
80
+ }
81
+
82
+ /**
83
+ * Rough character-to-token estimate used for context budget calculations.
84
+ */
85
+ export const CHARS_PER_TOKEN_ESTIMATE = 3
86
+
87
+ /**
88
+ * Converts an unknown value to a trimmed, non-empty string representation.
89
+ * Returns null for null, undefined, empty, or whitespace-only results.
90
+ *
91
+ * When maxChars is provided, the result is compacted and truncated.
92
+ * Without maxChars, strings are trimmed and objects are JSON-serialized.
93
+ */
94
+ export function stringifyUnknown(value: unknown, maxChars?: number): string | null {
95
+ if (value === null || value === undefined) return null
96
+
97
+ const raw = (() => {
98
+ if (typeof value === 'string') return value
99
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
100
+ return String(value)
101
+ }
102
+ if (typeof value === 'symbol') return value.description ? `Symbol(${value.description})` : 'Symbol()'
103
+ if (typeof value === 'function') return value.name ? `[function ${value.name}]` : '[function anonymous]'
104
+ if (Array.isArray(value)) {
105
+ try {
106
+ return JSON.stringify(value)
107
+ } catch {
108
+ return `[array(${value.length})]`
109
+ }
110
+ }
111
+
112
+ try {
113
+ return JSON.stringify(value)
114
+ } catch {
115
+ const maybeName =
116
+ isRecord(value) && typeof (value as { constructor?: { name?: unknown } }).constructor?.name === 'string'
117
+ ? (value as { constructor?: { name?: string } }).constructor?.name
118
+ : 'Object'
119
+ return `[object ${maybeName}]`
120
+ }
121
+ })()
122
+
123
+ if (maxChars !== undefined) {
124
+ const normalized = compactWhitespace(raw)
125
+ if (!normalized) return null
126
+ return truncateText(normalized, maxChars)
127
+ }
128
+
129
+ const trimmed = raw.trim()
130
+ return trimmed.length > 0 ? trimmed : null
131
+ }
132
+
133
+ /**
134
+ * Clamps a numeric importance/confidence value to [0, 1].
135
+ * Returns 0 for non-finite values.
136
+ */
137
+ export function clampImportance(value: number): number {
138
+ if (!Number.isFinite(value)) return 0
139
+ return Math.max(0, Math.min(1, value))
140
+ }
141
+
142
+ /**
143
+ * Splits a string on newlines, trims each line, and filters empties.
144
+ */
145
+ export function parseLineList(value: string): string[] {
146
+ return value
147
+ .split('\n')
148
+ .map((entry) => entry.trim())
149
+ .filter((entry) => entry.length > 0)
150
+ }
151
+
152
+ /**
153
+ * Joins an array of values into a newline-separated string.
154
+ * Returns an empty string for non-array inputs.
155
+ */
156
+ export function stringifyLineList(value: unknown): string {
157
+ return Array.isArray(value) ? value.map(String).join('\n') : ''
158
+ }
159
+
160
+ /**
161
+ * Reads an array of strings from an unknown value.
162
+ * Returns an empty array when the input is not an array.
163
+ */
164
+ export function readStringArray(value: unknown): string[] {
165
+ if (!Array.isArray(value)) return []
166
+ return value.filter((entry): entry is string => typeof entry === 'string')
167
+ }
168
+
169
+ /**
170
+ * Reads an array of plain records from an unknown value.
171
+ * Returns an empty array when the input is not an array.
172
+ */
173
+ export function readRecordArray(value: unknown): Array<Record<string, unknown>> {
174
+ if (!Array.isArray(value)) return []
175
+ return value.filter(
176
+ (entry): entry is Record<string, unknown> => typeof entry === 'object' && entry !== null && !Array.isArray(entry),
177
+ )
178
+ }