@lota-sdk/core 0.1.13 → 0.1.15

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 (95) hide show
  1. package/package.json +5 -5
  2. package/src/ai/embedding-cache.ts +7 -6
  3. package/src/ai/index.ts +1 -0
  4. package/src/bifrost/bifrost.ts +12 -7
  5. package/src/config/agent-defaults.ts +1 -1
  6. package/src/config/logger.ts +7 -9
  7. package/src/{runtime.ts → create-runtime.ts} +6 -6
  8. package/src/db/cursor-pagination.ts +1 -1
  9. package/src/db/memory-store.ts +10 -6
  10. package/src/db/memory.ts +6 -4
  11. package/src/db/schema-fingerprint.ts +1 -0
  12. package/src/db/service.ts +45 -51
  13. package/src/db/startup.ts +3 -3
  14. package/src/index.ts +1 -1
  15. package/src/queues/context-compaction.queue.ts +4 -8
  16. package/src/queues/document-processor.queue.ts +7 -7
  17. package/src/queues/memory-consolidation.queue.ts +7 -8
  18. package/src/queues/post-chat-memory.queue.ts +2 -6
  19. package/src/queues/recent-activity-title-refinement.queue.ts +2 -6
  20. package/src/queues/regular-chat-memory-digest.queue.ts +4 -7
  21. package/src/queues/skill-extraction.queue.ts +4 -7
  22. package/src/queues/workstream-title-generation.queue.ts +2 -6
  23. package/src/redis/connection.ts +6 -3
  24. package/src/redis/index.ts +1 -0
  25. package/src/redis/org-memory-lock.ts +1 -1
  26. package/src/redis/redis-lease-lock.ts +41 -8
  27. package/src/runtime/agent-stream-helpers.ts +2 -1
  28. package/src/runtime/context-compaction-constants.ts +1 -1
  29. package/src/runtime/context-compaction-runtime.ts +6 -4
  30. package/src/runtime/context-compaction.ts +19 -38
  31. package/src/runtime/execution-plan.ts +2 -2
  32. package/src/runtime/helper-model.ts +3 -1
  33. package/src/runtime/index.ts +12 -1
  34. package/src/runtime/memory-block.ts +3 -2
  35. package/src/runtime/memory-pipeline.ts +24 -5
  36. package/src/runtime/plugin-types.ts +1 -1
  37. package/src/runtime/runtime-extensions.ts +89 -13
  38. package/src/runtime/title-helpers.ts +11 -2
  39. package/src/runtime/workstream-chat-helpers.ts +5 -6
  40. package/src/runtime/workstream-routing-policy.ts +0 -30
  41. package/src/runtime/workstream-state.ts +17 -7
  42. package/src/services/attachment.service.ts +1 -1
  43. package/src/services/context-compaction.service.ts +3 -3
  44. package/src/services/document-chunk.service.ts +37 -32
  45. package/src/services/execution-plan.service.ts +2 -0
  46. package/src/services/learned-skill.service.ts +6 -10
  47. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -8
  48. package/src/services/memory.service.ts +21 -18
  49. package/src/services/organization-member.service.ts +1 -1
  50. package/src/services/plan-artifact.service.ts +1 -0
  51. package/src/services/plan-executor.service.ts +2 -18
  52. package/src/services/plan-helpers.ts +15 -0
  53. package/src/services/plan-validator.service.ts +3 -18
  54. package/src/services/recent-activity-title.service.ts +3 -10
  55. package/src/services/recent-activity.service.ts +6 -12
  56. package/src/services/workstream-message.service.ts +26 -16
  57. package/src/services/workstream-title.service.ts +1 -9
  58. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +401 -314
  59. package/src/services/workstream-turn.ts +2 -2
  60. package/src/services/workstream.service.ts +22 -10
  61. package/src/services/workstream.types.ts +7 -16
  62. package/src/storage/attachment-storage.service.ts +4 -4
  63. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +1 -4
  64. package/src/storage/index.ts +2 -2
  65. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  66. package/src/system-agents/delegated-agent-factory.ts +3 -2
  67. package/src/system-agents/index.ts +8 -0
  68. package/src/system-agents/memory-reranker.agent.ts +1 -1
  69. package/src/system-agents/memory.agent.ts +1 -1
  70. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  71. package/src/tools/execution-plan.tool.ts +6 -2
  72. package/src/tools/fetch-webpage.tool.ts +20 -18
  73. package/src/tools/index.ts +2 -2
  74. package/src/tools/read-file-parts.tool.ts +1 -1
  75. package/src/tools/search-web.tool.ts +18 -15
  76. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  77. package/src/tools/team-think.tool.ts +9 -5
  78. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  79. package/src/utils/async.ts +1 -1
  80. package/src/utils/errors.ts +15 -0
  81. package/src/utils/hono-error-handler.ts +1 -2
  82. package/src/utils/index.ts +10 -2
  83. package/src/utils/string.ts +14 -0
  84. package/src/workers/bootstrap.ts +2 -2
  85. package/src/workers/memory-consolidation.worker.ts +12 -12
  86. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  87. package/src/workers/regular-chat-memory-digest.runner.ts +9 -103
  88. package/src/workers/skill-extraction.runner.ts +7 -101
  89. package/src/workers/utils/file-section-chunker.ts +5 -3
  90. package/src/workers/utils/workstream-message-query.ts +106 -0
  91. package/src/workers/worker-utils.ts +4 -0
  92. package/src/runtime/retrieval-pipeline.ts +0 -3
  93. package/src/utils/error.ts +0 -10
  94. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  95. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -4,8 +4,8 @@ import { createUIMessageStream } from 'ai'
4
4
  import { lotaDebugLogger } from '../config/debug-logger'
5
5
  import { hasApprovalRespondedParts, isApprovalContinuationRequest } from '../runtime/approval-continuation'
6
6
  import { wrapResponseWithKeepalive } from '../utils/sse-keepalive'
7
- import { prepareWorkstreamRunCore } from './workstream-turn-preparation'
8
- import type { WorkstreamTurnParams, WorkstreamApprovalContinuationParams } from './workstream-turn-preparation'
7
+ import { prepareWorkstreamRunCore } from './workstream-turn-preparation.service'
8
+ import type { WorkstreamTurnParams, WorkstreamApprovalContinuationParams } from './workstream-turn-preparation.service'
9
9
 
10
10
  export { hasApprovalRespondedParts, isApprovalContinuationRequest }
11
11
  export { wrapResponseWithKeepalive }
@@ -1,7 +1,8 @@
1
- import { WORKSTREAM } from '@lota-sdk/shared'
1
+ import { WORKSTREAM, sdkWorkstreamStatusSchema } from '@lota-sdk/shared'
2
2
  import { BoundQuery, RecordId, StringRecordId, surql } from 'surrealdb'
3
3
 
4
4
  import { agentDisplayNames, getCoreWorkstreamProfile, isAgentName } from '../config/agent-defaults'
5
+ import { serverLogger } from '../config/logger'
5
6
  import { getWorkstreamBootstrapConfig } from '../config/workstream-defaults'
6
7
  import { BaseService } from '../db/base.service'
7
8
  import { ensureRecordId, recordIdToString } from '../db/record-id'
@@ -24,7 +25,7 @@ import { toIsoDateTimeString } from '../utils/date-time'
24
25
  import { chatRunRegistry } from './chat-run-registry.service'
25
26
  import { contextCompactionService } from './context-compaction.service'
26
27
  import { workstreamMessageService } from './workstream-message.service'
27
- import { WorkstreamSchema, WorkstreamStatusSchema } from './workstream.types'
28
+ import { WorkstreamSchema } from './workstream.types'
28
29
  import type { NormalizedWorkstream, WorkstreamRecord } from './workstream.types'
29
30
 
30
31
  // Uses SurrealQL directly to keep pagination/order logic close to queries.
@@ -368,15 +369,16 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
368
369
  async listWorkstreams(
369
370
  userId: RecordIdRef,
370
371
  orgId: RecordIdRef,
371
- options: { mode: string; core?: boolean; take?: number; page?: number; includeArchived: boolean },
372
+ options: { mode: string; core?: boolean; take?: number; page?: number; includeArchived?: boolean },
372
373
  ): Promise<{ workstreams: NormalizedWorkstream[]; hasMore: boolean }> {
373
374
  const core = options.core === true
375
+ const includeArchived = options.includeArchived ?? false
374
376
  if (options.mode === 'direct' && core) {
375
377
  throw new Error('Direct workstreams cannot be queried as core workstreams')
376
378
  }
377
379
 
378
380
  if (options.mode === 'direct' || core) {
379
- const query = options.includeArchived
381
+ const query = includeArchived
380
382
  ? LIST_ALL_WORKSTREAMS_BY_MODE_QUERY
381
383
  : LIST_ALL_WORKSTREAMS_BY_MODE_EXCLUDE_ARCHIVED_QUERY
382
384
  const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
@@ -389,7 +391,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
389
391
 
390
392
  const take = options.take ?? WORKSTREAM.DEFAULT_PAGE_LIMIT
391
393
  const page = options.page ?? 1
392
- const query = options.includeArchived ? LIST_WORKSTREAMS_QUERY : LIST_WORKSTREAMS_EXCLUDE_ARCHIVED_QUERY
394
+ const query = includeArchived ? LIST_WORKSTREAMS_QUERY : LIST_WORKSTREAMS_EXCLUDE_ARCHIVED_QUERY
393
395
  const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
394
396
  new BoundQuery(query, {
395
397
  userId,
@@ -467,7 +469,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
467
469
  }
468
470
 
469
471
  async updateStatus(workstreamId: RecordIdRef, status: string): Promise<NormalizedWorkstream> {
470
- const validStatus = WorkstreamStatusSchema.parse(status)
472
+ const validStatus = sdkWorkstreamStatusSchema.parse(status)
471
473
  const existing = await this.getById(workstreamId)
472
474
  this.assertMutableWorkstream(existing, validStatus === 'archived' ? 'archive' : 'unarchive')
473
475
  const workstream = await this.update(workstreamId, { status: validStatus })
@@ -543,11 +545,19 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
543
545
  return false
544
546
  }
545
547
 
546
- async setCompacting(workstreamId: RecordIdRef, value: boolean): Promise<void> {
548
+ async markCompacting(workstreamId: RecordIdRef): Promise<void> {
547
549
  const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
548
550
  await databaseService.query<unknown>(surql`
549
551
  UPDATE ONLY ${workstreamRef}
550
- SET isCompacting = ${value}
552
+ SET isCompacting = ${true}
553
+ `)
554
+ }
555
+
556
+ async clearCompacting(workstreamId: RecordIdRef): Promise<void> {
557
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
558
+ await databaseService.query<unknown>(surql`
559
+ UPDATE ONLY ${workstreamRef}
560
+ SET isCompacting = ${false}
551
561
  `)
552
562
  }
553
563
 
@@ -566,7 +576,9 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
566
576
  await this.update(workstreamRef, { memoryBlock: serialized })
567
577
 
568
578
  if (updatedEntries.length >= MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES) {
569
- void this.compactMemoryBlock(workstreamRef).catch(() => {})
579
+ void this.compactMemoryBlock(workstreamRef).catch((err: unknown) => {
580
+ serverLogger.warn`Memory block compaction failed for ${workstreamRef}: ${err}`
581
+ })
570
582
  }
571
583
 
572
584
  return this.formatMemoryBlockForPrompt({
@@ -612,7 +624,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
612
624
  orgId: RecordIdRef
613
625
  excludeWorkstreamId?: RecordIdRef
614
626
  limit: number
615
- }) {
627
+ }): Promise<NormalizedWorkstream[]> {
616
628
  let excludeCondition = ''
617
629
  const vars: Record<string, unknown> = { userId, orgId, limit }
618
630
 
@@ -1,16 +1,7 @@
1
+ import { sdkWorkstreamStatusSchema } from '@lota-sdk/shared'
1
2
  import { z } from 'zod'
2
3
 
3
- export interface Citation {
4
- title?: string
5
- url?: string
6
- snippet?: string
7
- source?: string
8
- sourceId?: string
9
- retrievedAt?: string
10
- }
11
-
12
4
  const WorkstreamModeSchema = z.enum(['direct', 'group'])
13
- export const WorkstreamStatusSchema = z.enum(['regular', 'archived'])
14
5
  const CoreWorkstreamTypeSchema = z.string()
15
6
 
16
7
  export interface NormalizedWorkstream {
@@ -20,7 +11,7 @@ export interface NormalizedWorkstream {
20
11
  mode: 'direct' | 'group'
21
12
  core: boolean
22
13
  coreType?: string
23
- nameGenerated: boolean
14
+ nameGenerated: boolean // Ideally `isNameGenerated`, but maps directly to SurrealDB column `nameGenerated`
24
15
  isRunning: boolean
25
16
  isCompacting: boolean
26
17
  agentId?: string | null
@@ -32,27 +23,27 @@ export interface NormalizedWorkstream {
32
23
  }
33
24
 
34
25
  export const WorkstreamSchema = z.object({
35
- id: z.any(), // RecordId
26
+ id: z.unknown(), // SurrealDB RecordId — validated structurally at DB boundary
36
27
  mode: WorkstreamModeSchema.optional().default('group'),
37
28
  core: z.boolean().optional().default(false),
38
29
  coreType: CoreWorkstreamTypeSchema.nullish(),
39
30
  agentId: z.string().nullish(),
40
31
  title: z.string().nullish(),
41
- status: WorkstreamStatusSchema.nullish(),
32
+ status: sdkWorkstreamStatusSchema.nullish(),
42
33
  memoryBlock: z.string().nullish(),
43
34
  memoryBlockSummary: z.string().nullish(),
44
35
  activeRunId: z.string().nullish(),
45
36
  activeStreamId: z.string().nullish(),
46
37
  compactionSummary: z.string().nullish(),
47
38
  lastCompactedMessageId: z.string().nullish(),
48
- nameGenerated: z.boolean().optional().default(false),
39
+ nameGenerated: z.boolean().optional().default(false), // Ideally `isNameGenerated`, but maps directly to SurrealDB column `nameGenerated`
49
40
  isCompacting: z.boolean().optional(),
50
41
  state: z.unknown().optional(),
51
42
  turnCount: z.number().int().optional().default(0),
52
43
  createdAt: z.coerce.date(),
53
44
  updatedAt: z.coerce.date(),
54
- userId: z.any(), // RecordId
55
- organizationId: z.any(), // RecordId
45
+ userId: z.unknown(), // SurrealDB RecordId — validated structurally at DB boundary
46
+ organizationId: z.unknown(), // SurrealDB RecordId — validated structurally at DB boundary
56
47
  })
57
48
 
58
49
  export type WorkstreamRecord = z.infer<typeof WorkstreamSchema>
@@ -15,14 +15,14 @@ import type {
15
15
  ReadableUploadMetadata,
16
16
  ReadableUploadPageMode,
17
17
  ReadableUploadPagePart,
18
- } from './attachments.types'
18
+ } from './attachment-types'
19
19
  import {
20
20
  buildOrganizationDocumentStorageKey,
21
21
  buildUploadStorageKey,
22
22
  buildUploadStoragePrefix,
23
23
  readNonNegativeInteger,
24
24
  readRecord,
25
- } from './attachments.utils'
25
+ } from './attachment-utils'
26
26
 
27
27
  const READ_FILE_PARTS_PAGES_PER_PART = 25
28
28
 
@@ -386,7 +386,7 @@ export function getAttachmentStorageService(): AttachmentStorageService {
386
386
  }
387
387
 
388
388
  export const attachmentStorageService = new Proxy({} as AttachmentStorageService, {
389
- get(_target, prop: string) {
390
- return (getAttachmentStorageService() as unknown as Record<string, unknown>)[prop]
389
+ get(_target, prop: string): unknown {
390
+ return Reflect.get(getAttachmentStorageService(), prop)
391
391
  },
392
392
  })
@@ -48,10 +48,7 @@ export function buildOrganizationDocumentStorageKey(params: {
48
48
  return `${buildOrganizationDocumentStoragePrefix({ orgId: params.orgId, namespace: params.namespace })}${relativePath || 'document.txt'}`
49
49
  }
50
50
 
51
- export function readRecord(value: unknown): Record<string, unknown> | null {
52
- if (!value || typeof value !== 'object' || Array.isArray(value)) return null
53
- return value as Record<string, unknown>
54
- }
51
+ export { readRecord } from '../utils/string'
55
52
 
56
53
  export function readNonNegativeInteger(value: unknown): number | null {
57
54
  return Number.isInteger(value) && typeof value === 'number' && value >= 0 ? value : null
@@ -1,10 +1,10 @@
1
1
  export * from './attachment-parser'
2
2
  export * from './attachment-storage.service'
3
3
  export * from './generated-document-storage.service'
4
- export type { MessagePartLike, ReadableUploadPageMode, ReadableUploadPagePart } from './attachments.types'
4
+ export type { MessagePartLike, ReadableUploadPageMode, ReadableUploadPagePart } from './attachment-types'
5
5
  export {
6
6
  buildOrganizationDocumentStorageKey,
7
7
  buildUploadStorageKey,
8
8
  buildUploadStoragePrefix,
9
9
  readNonNegativeInteger,
10
- } from './attachments.utils'
10
+ } from './attachment-utils'
@@ -8,7 +8,7 @@ import {
8
8
  import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
9
9
  import { resolveHelperAgentOptions } from './helper-agent-options'
10
10
 
11
- const contextCompacterPrompt = `<agent-instructions>
11
+ const CONTEXT_COMPACTION_PROMPT = `<agent-instructions>
12
12
  You are a **Context Compacter** that produces both:
13
13
  1) a dense but shorter summary of prior context
14
14
  2) a structured state delta for durable execution continuity
@@ -36,11 +36,11 @@ Return valid data for:
36
36
  </output-format>
37
37
  </agent-instructions>`
38
38
 
39
- export function createContextCompacterAgent(options: CreateHelperToolLoopAgentOptions) {
39
+ export function createContextCompactionAgent(options: CreateHelperToolLoopAgentOptions) {
40
40
  return new ToolLoopAgent({
41
- id: 'context-compacter',
41
+ id: 'context-compaction',
42
42
  model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
43
43
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
44
- ...resolveHelperAgentOptions(options, { instructions: contextCompacterPrompt }),
44
+ ...resolveHelperAgentOptions(options, { instructions: CONTEXT_COMPACTION_PROMPT }),
45
45
  })
46
46
  }
@@ -79,6 +79,7 @@ const TERMINATION_SUFFIX = `
79
79
  When your analysis is complete, return your final answer directly as markdown text. Do not leave the task unfinished or end on a tool result without a final response.
80
80
  </termination>`
81
81
 
82
+ const DEFAULT_DELEGATED_AGENT_MAX_OUTPUT_TOKENS = 4096
82
83
  const MAX_RETAINED_AGENT_MESSAGES = 10
83
84
  const MAX_NON_SUBSTANTIVE_AGENT_RESULT_ATTEMPTS = 2
84
85
  const NON_SUBSTANTIVE_AGENT_RESULT_RETRY_PROMPT =
@@ -164,7 +165,7 @@ export function createDelegatedAgentTool(definition: DelegatedAgentDefinition):
164
165
  ...(definition.providerOptions ? { providerOptions: definition.providerOptions } : {}),
165
166
  instructions: buildDelegatedAgentInstructions(definition.instructions, agentTools),
166
167
  tools: agentTools,
167
- maxOutputTokens: definition.maxOutputTokens ?? 4096,
168
+ maxOutputTokens: definition.maxOutputTokens ?? DEFAULT_DELEGATED_AGENT_MAX_OUTPUT_TOKENS,
168
169
  ...(typeof temperature === 'number' ? { temperature } : {}),
169
170
  stopWhen: [stepCountIs(maxSteps)],
170
171
  prepareStep: async ({ messages }) => ({ messages: retainCriticalAgentMessages(messages) }),
@@ -205,7 +206,7 @@ export function createDelegatedAgentToolWithContext<TContext>(
205
206
  ...(definition.providerOptions ? { providerOptions: definition.providerOptions } : {}),
206
207
  instructions: buildDelegatedAgentInstructions(definition.instructions, agentTools),
207
208
  tools: agentTools,
208
- maxOutputTokens: definition.maxOutputTokens ?? 4096,
209
+ maxOutputTokens: definition.maxOutputTokens ?? DEFAULT_DELEGATED_AGENT_MAX_OUTPUT_TOKENS,
209
210
  ...(typeof temperature === 'number' ? { temperature } : {}),
210
211
  stopWhen: [stepCountIs(maxSteps)],
211
212
  prepareStep: async ({ messages }) => ({ messages: retainCriticalAgentMessages(messages) }),
@@ -1,4 +1,12 @@
1
1
  export * from './agent-result'
2
+ export * from './context-compaction.agent'
2
3
  export * from './delegated-agent-factory'
3
4
  export * from './helper-agent-options'
5
+ export * from './memory-reranker.agent'
6
+ export * from './memory.agent'
4
7
  export * from './recent-activity-title-refiner.agent'
8
+ export * from './regular-chat-memory-digest.agent'
9
+ export * from './researcher.agent'
10
+ export * from './skill-extractor.agent'
11
+ export * from './skill-manager.agent'
12
+ export * from './title-generator.agent'
@@ -8,7 +8,7 @@ import {
8
8
  import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
9
9
  import { resolveHelperAgentOptions } from './helper-agent-options'
10
10
 
11
- export const memoryRerankerPrompt = `<agent-instructions>
11
+ export const MEMORY_RERANKER_PROMPT = `<agent-instructions>
12
12
  You are a **Memory Reranker** that selects and organizes the most relevant memories for a user query.
13
13
 
14
14
  <task>
@@ -8,7 +8,7 @@ import {
8
8
  import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
9
9
  import { resolveHelperAgentOptions } from './helper-agent-options'
10
10
 
11
- export const orgMemoryPrompt = `<agent-instructions>
11
+ export const ORG_MEMORY_PROMPT = `<agent-instructions>
12
12
  You are an **Organization Fact Extractor** that captures only durable, explicitly stated organization facts.
13
13
 
14
14
  <task>
@@ -45,7 +45,7 @@ Return only the title text. No quotes, labels, JSON, markdown, or explanation.
45
45
  </agent-instructions>`
46
46
  }
47
47
 
48
- export const recentActivityTitleRefinerPrompt = `<agent-instructions>
48
+ export const RECENT_ACTIVITY_TITLE_REFINER_PROMPT = `<agent-instructions>
49
49
  You are the lead agent writing the visible title for a recent activity item.
50
50
 
51
51
  <goal>
@@ -11,9 +11,13 @@ import { tool } from 'ai'
11
11
  import type { RecordIdRef } from '../db/record-id'
12
12
  import { executionPlanService } from '../services/execution-plan.service'
13
13
 
14
+ function isExecutionPlanResult(value: unknown): value is ExecutionPlanToolResultData {
15
+ return value !== null && value !== undefined && typeof value === 'object' && 'hasPlan' in value
16
+ }
17
+
14
18
  function getLatestExecutionPlanToolResult(output: unknown): ExecutionPlanToolResultData | undefined {
15
- if (output && typeof output === 'object' && 'hasPlan' in output) {
16
- return output as ExecutionPlanToolResultData
19
+ if (isExecutionPlanResult(output)) {
20
+ return output
17
21
  }
18
22
 
19
23
  if (Array.isArray(output)) {
@@ -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
  }
@@ -5,8 +5,8 @@ export * from './memory-block.tool'
5
5
  export * from './read-file-parts.tool'
6
6
  export * from './remember-memory.tool'
7
7
  export * from './research-topic.tool'
8
- export * from './search-tools'
8
+ export * from './search.tool'
9
9
  export * from './search-web.tool'
10
10
  export * from './team-think.tool'
11
- export * from './tool-contract'
11
+ export * from './tool-contracts'
12
12
  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,18 +9,21 @@ 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
 
@@ -79,13 +82,14 @@ export function createTeamThinkTool(params: {
79
82
  toolProviders: params.toolProviders,
80
83
  })
81
84
  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]({
85
+ const agentId_ = typeof agentConfig.id === 'string' ? agentConfig.id : agentId
86
+ const maxSteps = typeof agentConfig.maxSteps === 'number' ? agentConfig.maxSteps : 10
87
+ const agent = createAgent[agentId_]({
84
88
  mode: 'fixedWorkstreamMode',
85
89
  tools,
86
90
  extraInstructions: agentConfig.extraInstructions,
87
91
  maxRetries: TEAM_THINK_AGENT_MAX_RETRIES,
88
- stopWhen: [stepCountIs(agentConfig.maxSteps as number)],
92
+ stopWhen: [stepCountIs(maxSteps)],
89
93
  })
90
94
  const observer = {
91
95
  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,4 @@
1
- import { getErrorMessage } from './error'
1
+ import { getErrorMessage } from './errors'
2
2
 
3
3
  class TimeoutError extends Error {
4
4
  constructor(operation: string, ms: number) {
@@ -1,3 +1,18 @@
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
+ }
11
+
12
+ export function toError(value: unknown): Error {
13
+ return value instanceof Error ? value : new Error(String(value))
14
+ }
15
+
1
16
  export class AppError extends Error {
2
17
  public readonly code: string
3
18
  public readonly statusCode: number
@@ -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