@lota-sdk/core 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infrastructure/schema/00_workstream.surql +55 -0
- package/infrastructure/schema/01_memory.surql +47 -0
- package/infrastructure/schema/02_execution_plan.surql +62 -0
- package/infrastructure/schema/03_learned_skill.surql +32 -0
- package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
- package/package.json +128 -0
- package/src/ai/definitions.ts +308 -0
- package/src/bifrost/bifrost.ts +256 -0
- package/src/config/agent-defaults.ts +99 -0
- package/src/config/constants.ts +33 -0
- package/src/config/env-shapes.ts +122 -0
- package/src/config/logger.ts +29 -0
- package/src/config/model-constants.ts +31 -0
- package/src/config/search.ts +17 -0
- package/src/config/workstream-defaults.ts +68 -0
- package/src/db/base.service.ts +55 -0
- package/src/db/cursor-pagination.ts +73 -0
- package/src/db/memory-query-builder.ts +207 -0
- package/src/db/memory-store.helpers.ts +118 -0
- package/src/db/memory-store.rows.ts +29 -0
- package/src/db/memory-store.ts +974 -0
- package/src/db/memory-types.ts +193 -0
- package/src/db/memory.ts +505 -0
- package/src/db/record-id.ts +78 -0
- package/src/db/service.ts +932 -0
- package/src/db/startup.ts +152 -0
- package/src/db/tables.ts +20 -0
- package/src/document/org-document-chunking.ts +224 -0
- package/src/document/parsing.ts +40 -0
- package/src/embeddings/provider.ts +76 -0
- package/src/index.ts +302 -0
- package/src/queues/context-compaction.queue.ts +82 -0
- package/src/queues/document-processor.queue.ts +118 -0
- package/src/queues/memory-consolidation.queue.ts +65 -0
- package/src/queues/post-chat-memory.queue.ts +128 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
- package/src/queues/regular-chat-memory-digest.config.ts +12 -0
- package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
- package/src/queues/skill-extraction.config.ts +9 -0
- package/src/queues/skill-extraction.queue.ts +62 -0
- package/src/redis/connection.ts +176 -0
- package/src/redis/index.ts +30 -0
- package/src/redis/org-memory-lock.ts +43 -0
- package/src/redis/redis-lease-lock.ts +158 -0
- package/src/runtime/agent-contract.ts +1 -0
- package/src/runtime/agent-prompt-context.ts +119 -0
- package/src/runtime/agent-runtime-policy.ts +192 -0
- package/src/runtime/agent-stream-helpers.ts +117 -0
- package/src/runtime/agent-types.ts +22 -0
- package/src/runtime/approval-continuation.ts +16 -0
- package/src/runtime/chat-attachments.ts +46 -0
- package/src/runtime/chat-message.ts +10 -0
- package/src/runtime/chat-request-routing.ts +21 -0
- package/src/runtime/chat-run-orchestration.ts +25 -0
- package/src/runtime/chat-run-registry.ts +20 -0
- package/src/runtime/chat-types.ts +18 -0
- package/src/runtime/context-compaction-constants.ts +11 -0
- package/src/runtime/context-compaction-runtime.ts +86 -0
- package/src/runtime/context-compaction.ts +909 -0
- package/src/runtime/execution-plan.ts +59 -0
- package/src/runtime/helper-model.ts +405 -0
- package/src/runtime/indexed-repositories-policy.ts +28 -0
- package/src/runtime/instruction-sections.ts +8 -0
- package/src/runtime/llm-content.ts +71 -0
- package/src/runtime/memory-block.ts +264 -0
- package/src/runtime/memory-digest-policy.ts +14 -0
- package/src/runtime/memory-format.ts +8 -0
- package/src/runtime/memory-pipeline.ts +570 -0
- package/src/runtime/memory-prompts-fact.ts +47 -0
- package/src/runtime/memory-prompts-parse.ts +3 -0
- package/src/runtime/memory-prompts-update.ts +37 -0
- package/src/runtime/memory-scope.ts +43 -0
- package/src/runtime/plugin-types.ts +10 -0
- package/src/runtime/retrieval-adapters.ts +25 -0
- package/src/runtime/retrieval-pipeline.ts +3 -0
- package/src/runtime/runtime-extensions.ts +154 -0
- package/src/runtime/skill-extraction-policy.ts +3 -0
- package/src/runtime/team-consultation-orchestrator.ts +245 -0
- package/src/runtime/team-consultation-prompts.ts +32 -0
- package/src/runtime/title-helpers.ts +12 -0
- package/src/runtime/turn-lifecycle.ts +28 -0
- package/src/runtime/workstream-chat-helpers.ts +187 -0
- package/src/runtime/workstream-routing-policy.ts +301 -0
- package/src/runtime/workstream-state.ts +261 -0
- package/src/services/attachment.service.ts +159 -0
- package/src/services/chat-attachments.service.ts +17 -0
- package/src/services/chat-run-registry.service.ts +3 -0
- package/src/services/context-compaction-runtime.ts +13 -0
- package/src/services/context-compaction.service.ts +115 -0
- package/src/services/document-chunk.service.ts +141 -0
- package/src/services/execution-plan.service.ts +890 -0
- package/src/services/learned-skill.service.ts +328 -0
- package/src/services/memory-assessment.service.ts +43 -0
- package/src/services/memory.service.ts +807 -0
- package/src/services/memory.utils.ts +84 -0
- package/src/services/mutating-approval.service.ts +110 -0
- package/src/services/recent-activity-title.service.ts +74 -0
- package/src/services/recent-activity.service.ts +397 -0
- package/src/services/workstream-change-tracker.service.ts +313 -0
- package/src/services/workstream-message.service.ts +283 -0
- package/src/services/workstream-title.service.ts +58 -0
- package/src/services/workstream-turn-preparation.ts +1340 -0
- package/src/services/workstream-turn.ts +37 -0
- package/src/services/workstream.service.ts +854 -0
- package/src/services/workstream.types.ts +118 -0
- package/src/storage/attachment-parser.ts +101 -0
- package/src/storage/attachment-storage.service.ts +391 -0
- package/src/storage/attachments.types.ts +11 -0
- package/src/storage/attachments.utils.ts +58 -0
- package/src/storage/generated-document-storage.service.ts +55 -0
- package/src/system-agents/agent-result.ts +27 -0
- package/src/system-agents/context-compacter.agent.ts +46 -0
- package/src/system-agents/delegated-agent-factory.ts +177 -0
- package/src/system-agents/helper-agent-options.ts +20 -0
- package/src/system-agents/memory-reranker.agent.ts +38 -0
- package/src/system-agents/memory.agent.ts +58 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
- package/src/system-agents/researcher.agent.ts +34 -0
- package/src/system-agents/skill-extractor.agent.ts +88 -0
- package/src/system-agents/skill-manager.agent.ts +80 -0
- package/src/system-agents/title-generator.agent.ts +42 -0
- package/src/system-agents/workstream-tracker.agent.ts +58 -0
- package/src/tools/execution-plan.tool.ts +163 -0
- package/src/tools/fetch-webpage.tool.ts +132 -0
- package/src/tools/firecrawl-client.ts +12 -0
- package/src/tools/memory-block.tool.ts +55 -0
- package/src/tools/read-file-parts.tool.ts +80 -0
- package/src/tools/remember-memory.tool.ts +85 -0
- package/src/tools/research-topic.tool.ts +15 -0
- package/src/tools/search-tools.ts +55 -0
- package/src/tools/search-web.tool.ts +175 -0
- package/src/tools/team-think.tool.ts +125 -0
- package/src/tools/tool-contract.ts +21 -0
- package/src/tools/user-questions.tool.ts +18 -0
- package/src/utils/async.ts +50 -0
- package/src/utils/date-time.ts +34 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/errors.ts +28 -0
- package/src/utils/hono-error-handler.ts +71 -0
- package/src/utils/string.ts +51 -0
- package/src/workers/bootstrap.ts +44 -0
- package/src/workers/memory-consolidation.worker.ts +318 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
- package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
- package/src/workers/skill-extraction.runner.ts +331 -0
- package/src/workers/skill-extraction.worker.ts +22 -0
- package/src/workers/utils/repo-indexer-chunker.ts +331 -0
- package/src/workers/utils/repo-structure-extractor.ts +645 -0
- package/src/workers/utils/repomix-process-concurrency.ts +65 -0
- package/src/workers/utils/sandbox-error.ts +5 -0
- package/src/workers/worker-utils.ts +182 -0
|
@@ -0,0 +1,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,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
|
+
}
|