@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,1340 @@
|
|
|
1
|
+
import { toTimestamp, withMessageCreatedAt } from '@lota-sdk/shared/runtime/chat-message-metadata'
|
|
2
|
+
import { baseChatMessageSchema } from '@lota-sdk/shared/schemas/chat-api'
|
|
3
|
+
import { messageMetadataSchema, dataPartsSchema } from '@lota-sdk/shared/schemas/chat-message'
|
|
4
|
+
import type { ChatMessage, MessageMetadata } from '@lota-sdk/shared/schemas/chat-message'
|
|
5
|
+
import {
|
|
6
|
+
CONSULT_TEAM_TOOL_NAME,
|
|
7
|
+
CONSULT_SPECIALIST_TOOL_NAME,
|
|
8
|
+
ConsultSpecialistArgsSchema,
|
|
9
|
+
} from '@lota-sdk/shared/schemas/tools'
|
|
10
|
+
import { convertToModelMessages, readUIMessageStream, stepCountIs, tool as createTool, validateUIMessages } from 'ai'
|
|
11
|
+
import type { PrepareStepFunction, StopCondition, ToolSet, UIMessageStreamWriter } from 'ai'
|
|
12
|
+
import type { z } from 'zod'
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
agentDisplayNames,
|
|
16
|
+
buildAgentTools,
|
|
17
|
+
createAgent,
|
|
18
|
+
getCoreWorkstreamProfile,
|
|
19
|
+
getAgentRuntimeConfig,
|
|
20
|
+
pluginRuntime,
|
|
21
|
+
} from '../config/agent-defaults'
|
|
22
|
+
import { aiLogger } from '../config/logger'
|
|
23
|
+
import type { RecordIdRef } from '../db/record-id'
|
|
24
|
+
import { recordIdToString } from '../db/record-id'
|
|
25
|
+
import { TABLES } from '../db/tables'
|
|
26
|
+
import { enqueueContextCompaction } from '../queues/context-compaction.queue'
|
|
27
|
+
import { enqueuePostChatMemory } from '../queues/post-chat-memory.queue'
|
|
28
|
+
import { enqueueRecentActivityTitleRefinement } from '../queues/recent-activity-title-refinement.queue'
|
|
29
|
+
import { enqueueRegularChatMemoryDigest } from '../queues/regular-chat-memory-digest.queue'
|
|
30
|
+
import { enqueueSkillExtraction } from '../queues/skill-extraction.queue'
|
|
31
|
+
import { buildAgentPromptContext } from '../runtime/agent-prompt-context'
|
|
32
|
+
import {
|
|
33
|
+
buildSpecialistTaskMessage,
|
|
34
|
+
createAgentMessageMetadata,
|
|
35
|
+
createServerRunAbortController,
|
|
36
|
+
} from '../runtime/agent-stream-helpers'
|
|
37
|
+
import { hasApprovalRespondedParts } from '../runtime/approval-continuation'
|
|
38
|
+
import { buildModelInputMessagesWithUploadMetadata, buildReadableUploadMetadataText } from '../runtime/chat-attachments'
|
|
39
|
+
import { hasMessageContent } from '../runtime/chat-message'
|
|
40
|
+
import { waitForCompactionIfNeeded } from '../runtime/chat-run-orchestration'
|
|
41
|
+
import { mergeStateDelta, parseWorkstreamState } from '../runtime/context-compaction'
|
|
42
|
+
import { CONTEXT_SIZE } from '../runtime/context-compaction-constants'
|
|
43
|
+
import { createExecutionPlanInstructionSectionCache } from '../runtime/execution-plan'
|
|
44
|
+
import { mergeInstructionSections } from '../runtime/instruction-sections'
|
|
45
|
+
import {
|
|
46
|
+
shouldEnqueueOnboardingPostChatMemory,
|
|
47
|
+
shouldEnqueueRegularDigestForWorkstream,
|
|
48
|
+
} from '../runtime/memory-digest-policy'
|
|
49
|
+
import { getRuntimeAdapters, getToolProviders, getTurnHooks } from '../runtime/runtime-extensions'
|
|
50
|
+
import { shouldEnqueueSkillExtraction } from '../runtime/skill-extraction-policy'
|
|
51
|
+
import { finalizeTurnRun } from '../runtime/turn-lifecycle'
|
|
52
|
+
import {
|
|
53
|
+
appendCompactionContextToHistoryMessages,
|
|
54
|
+
buildAgentHistoryMessages,
|
|
55
|
+
buildConversationSummary,
|
|
56
|
+
buildReadableUploadMetadataContext,
|
|
57
|
+
collectToolOutputErrors,
|
|
58
|
+
extractMessageText,
|
|
59
|
+
extractTrackerMessageText,
|
|
60
|
+
toHistoryMessages,
|
|
61
|
+
toOptionalTrimmedString,
|
|
62
|
+
} from '../runtime/workstream-chat-helpers'
|
|
63
|
+
import {
|
|
64
|
+
classifyHighImpactResponse,
|
|
65
|
+
classifyPolicyClasses,
|
|
66
|
+
resolveReasoningProfile,
|
|
67
|
+
} from '../runtime/workstream-routing-policy'
|
|
68
|
+
import { WorkstreamStateSchema } from '../runtime/workstream-state'
|
|
69
|
+
import type { WorkstreamState, WorkstreamStateDelta } from '../runtime/workstream-state'
|
|
70
|
+
import { chatRunRegistry } from '../services/chat-run-registry.service'
|
|
71
|
+
import type { NormalizedWorkstream, WorkstreamRecord } from '../services/workstream.types'
|
|
72
|
+
import { createTeamThinkTool } from '../tools/team-think.tool'
|
|
73
|
+
import { safeEnqueue } from '../utils/async'
|
|
74
|
+
import { toIsoDateTimeString } from '../utils/date-time'
|
|
75
|
+
import { AppError } from '../utils/errors'
|
|
76
|
+
import { attachmentService } from './attachment.service'
|
|
77
|
+
import { listReadableUploadsFromChatMessages } from './chat-attachments.service'
|
|
78
|
+
import { contextCompactionRuntime } from './context-compaction-runtime'
|
|
79
|
+
import { executionPlanService } from './execution-plan.service'
|
|
80
|
+
import { learnedSkillService } from './learned-skill.service'
|
|
81
|
+
import { memoryService } from './memory.service'
|
|
82
|
+
import { recentActivityService } from './recent-activity.service'
|
|
83
|
+
import { updateWorkstreamChangeTracker } from './workstream-change-tracker.service'
|
|
84
|
+
import { workstreamMessageService } from './workstream-message.service'
|
|
85
|
+
import { workstreamService } from './workstream.service'
|
|
86
|
+
|
|
87
|
+
type AgentRuntimeConfig = Record<string, unknown>
|
|
88
|
+
type AgentFactory = Record<string, (...args: unknown[]) => Record<string, (...args: unknown[]) => unknown>>
|
|
89
|
+
type ChatStreamChunk = Parameters<UIMessageStreamWriter<ChatMessage>['write']>[0]
|
|
90
|
+
|
|
91
|
+
interface UIMessageStreamResult {
|
|
92
|
+
toUIMessageStream(options: Record<string, unknown>): ReadableStream<unknown>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function hasUIMessageStream(value: unknown): value is UIMessageStreamResult {
|
|
96
|
+
return (
|
|
97
|
+
typeof value === 'object' &&
|
|
98
|
+
value !== null &&
|
|
99
|
+
typeof (value as { toUIMessageStream?: unknown }).toUIMessageStream === 'function'
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getPluginService(path: string[]): ((...args: unknown[]) => unknown) | undefined {
|
|
104
|
+
let current: unknown = pluginRuntime
|
|
105
|
+
let owner: unknown = undefined
|
|
106
|
+
for (const key of path) {
|
|
107
|
+
if (current === null || current === undefined || typeof current !== 'object') return undefined
|
|
108
|
+
owner = current
|
|
109
|
+
current = (current as Record<string, unknown>)[key]
|
|
110
|
+
}
|
|
111
|
+
if (typeof current !== 'function') {
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return owner && typeof owner === 'object'
|
|
116
|
+
? (current as (...args: unknown[]) => unknown).bind(owner)
|
|
117
|
+
: (current as (...args: unknown[]) => unknown)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function buildIndexedRepositoriesContext(
|
|
121
|
+
workspaceId: string,
|
|
122
|
+
): Promise<{ provideRepoTool: boolean; defaultSectionsByAgent: Record<string, unknown>; context: string }> {
|
|
123
|
+
const buildContext = getRuntimeAdapters().workstream?.buildIndexedRepositoriesContext
|
|
124
|
+
if (!buildContext) {
|
|
125
|
+
return { provideRepoTool: false, defaultSectionsByAgent: {}, context: '' }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const context = await buildContext(workspaceId)
|
|
129
|
+
return {
|
|
130
|
+
provideRepoTool: context.provideRepoTool,
|
|
131
|
+
defaultSectionsByAgent: context.defaultSectionsByAgent,
|
|
132
|
+
context: context.context ?? '',
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const PRESEEDED_MEMORY_LOOKUP_LIMIT = 3
|
|
137
|
+
|
|
138
|
+
export function parsePersistedWorkstreamState(value: unknown): WorkstreamState | null {
|
|
139
|
+
return parseWorkstreamState(value)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function extractFirstAbsoluteUrl(text: string): URL | null {
|
|
143
|
+
const match = text.match(/https?:\/\/[^\s)]+/i)
|
|
144
|
+
if (!match) return null
|
|
145
|
+
|
|
146
|
+
const normalized = match[0].replace(/[.,;!?]+$/g, '')
|
|
147
|
+
try {
|
|
148
|
+
return new URL(normalized)
|
|
149
|
+
} catch {
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function taskRequestsWebsiteRefresh(task: string, forceRefresh: boolean): boolean {
|
|
155
|
+
return forceRefresh || /\b(refresh|re-run|rerun|run again|extract again|re-extract|overwrite|recrawl)\b/i.test(task)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function buildInspectWebsiteTrackerTitle(input: unknown): string {
|
|
159
|
+
if (!input || typeof input !== 'object') return 'Inspect website intelligence'
|
|
160
|
+
|
|
161
|
+
const task = typeof (input as { task?: unknown }).task === 'string' ? (input as { task: string }).task.trim() : ''
|
|
162
|
+
const forceRefresh = (input as { forceRefresh?: unknown }).forceRefresh === true
|
|
163
|
+
const hostname = extractFirstAbsoluteUrl(task)?.hostname.replace(/^www\./, '') ?? null
|
|
164
|
+
|
|
165
|
+
if (hostname && taskRequestsWebsiteRefresh(task, forceRefresh)) {
|
|
166
|
+
return `Overwrite website-intelligence artifacts for ${hostname}`
|
|
167
|
+
}
|
|
168
|
+
if (hostname) {
|
|
169
|
+
return `Inspect website intelligence for ${hostname}`
|
|
170
|
+
}
|
|
171
|
+
return taskRequestsWebsiteRefresh(task, forceRefresh)
|
|
172
|
+
? 'Overwrite website-intelligence artifacts'
|
|
173
|
+
: 'Inspect website intelligence'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function emitTransientWorkstreamTrackerState(params: {
|
|
177
|
+
writer?: UIMessageStreamWriter<ChatMessage>
|
|
178
|
+
workstreamRecord: WorkstreamRecord
|
|
179
|
+
existingState: WorkstreamState
|
|
180
|
+
delta: WorkstreamStateDelta
|
|
181
|
+
}): WorkstreamState {
|
|
182
|
+
const nextState = mergeStateDelta(params.existingState, params.delta, () => Date.now())
|
|
183
|
+
|
|
184
|
+
if (params.writer) {
|
|
185
|
+
params.writer.write({
|
|
186
|
+
type: 'data-workstreamTracker',
|
|
187
|
+
data: workstreamService.toPublicWorkstreamDetail({ ...params.workstreamRecord, state: nextState })
|
|
188
|
+
.workstreamState as unknown as Record<string, unknown>,
|
|
189
|
+
transient: true,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return nextState
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
|
|
197
|
+
return await waitForCompactionIfNeeded({
|
|
198
|
+
entityId: recordIdToString(workstreamId, TABLES.WORKSTREAM),
|
|
199
|
+
entityLabel: 'Workstream',
|
|
200
|
+
loadEntity: () => workstreamService.getById(workstreamId),
|
|
201
|
+
isCompacting: (workstream) => workstream.isCompacting === true,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function parseChatMessageCandidate(value: unknown): ChatMessage | undefined {
|
|
206
|
+
const parsed = baseChatMessageSchema.safeParse(value)
|
|
207
|
+
if (!parsed.success) return undefined
|
|
208
|
+
return parsed.data as ChatMessage
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function getChatMessageFromToolOutput(output: unknown): ChatMessage | undefined {
|
|
212
|
+
const directCandidate = parseChatMessageCandidate(output)
|
|
213
|
+
if (directCandidate) return directCandidate
|
|
214
|
+
|
|
215
|
+
if (Array.isArray(output)) {
|
|
216
|
+
for (let index = output.length - 1; index >= 0; index -= 1) {
|
|
217
|
+
const candidate = getChatMessageFromToolOutput(output[index])
|
|
218
|
+
if (candidate) return candidate
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (output && typeof output === 'object' && 'message' in output) {
|
|
223
|
+
return getChatMessageFromToolOutput((output as { message?: unknown }).message)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return undefined
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class WorkstreamTurnError extends AppError {
|
|
230
|
+
constructor(
|
|
231
|
+
message: string,
|
|
232
|
+
readonly statusCode: 400 | 409,
|
|
233
|
+
) {
|
|
234
|
+
super(message, statusCode === 409 ? 'CONFLICT' : 'BAD_REQUEST', statusCode)
|
|
235
|
+
this.name = 'WorkstreamTurnError'
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function asRecord(value: unknown): Record<string, unknown> | null {
|
|
240
|
+
return value && typeof value === 'object' ? (value as Record<string, unknown>) : null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function readOptionalString(value: unknown): string | undefined {
|
|
244
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : undefined
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function readOptionalBoolean(value: unknown): boolean | undefined {
|
|
248
|
+
return typeof value === 'boolean' ? value : undefined
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function readInstructionSections(value: unknown): string[] {
|
|
252
|
+
if (!Array.isArray(value)) {
|
|
253
|
+
return []
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return value
|
|
257
|
+
.filter((section): section is string => typeof section === 'string')
|
|
258
|
+
.map((section) => section.trim())
|
|
259
|
+
.filter((section) => section.length > 0)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function optionalInstructionSection(value: unknown): string[] | undefined {
|
|
263
|
+
const section = readOptionalString(value)
|
|
264
|
+
return section ? [section] : undefined
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface WorkstreamTurnParams {
|
|
268
|
+
workstream: NormalizedWorkstream
|
|
269
|
+
workstreamRef: RecordIdRef
|
|
270
|
+
orgRef: RecordIdRef
|
|
271
|
+
userRef: RecordIdRef
|
|
272
|
+
userName?: string | null
|
|
273
|
+
inputMessage: ChatMessage
|
|
274
|
+
persistInputMessage?: boolean
|
|
275
|
+
abortSignal?: AbortSignal
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface WorkstreamApprovalContinuationParams {
|
|
279
|
+
workstream: NormalizedWorkstream
|
|
280
|
+
workstreamRef: RecordIdRef
|
|
281
|
+
orgRef: RecordIdRef
|
|
282
|
+
userRef: RecordIdRef
|
|
283
|
+
userName?: string | null
|
|
284
|
+
approvalMessages: ChatMessage[]
|
|
285
|
+
abortSignal?: AbortSignal
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export type WorkstreamRunCoreParams = {
|
|
289
|
+
workstream: NormalizedWorkstream
|
|
290
|
+
workstreamRef: RecordIdRef
|
|
291
|
+
orgRef: RecordIdRef
|
|
292
|
+
userRef: RecordIdRef
|
|
293
|
+
userName?: string | null
|
|
294
|
+
abortSignal?: AbortSignal
|
|
295
|
+
} & (
|
|
296
|
+
| { kind: 'userTurn'; inputMessage: ChatMessage; persistInputMessage?: boolean }
|
|
297
|
+
| { kind: 'approvalContinuation'; approvalMessages: ChatMessage[] }
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
export interface PreparedWorkstreamTurn {
|
|
301
|
+
originalMessages: ChatMessage[]
|
|
302
|
+
run: (writer?: UIMessageStreamWriter<ChatMessage>) => Promise<void>
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function upsertChatHistoryMessage(messages: ChatMessage[], nextMessage: ChatMessage): ChatMessage[] {
|
|
306
|
+
const existingIndex = messages.findIndex((message) => message.id === nextMessage.id)
|
|
307
|
+
if (existingIndex === -1) {
|
|
308
|
+
return [...messages, nextMessage]
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const nextMessages = [...messages]
|
|
312
|
+
nextMessages[existingIndex] = nextMessage
|
|
313
|
+
return nextMessages
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function buildRecentActivityChatDeepLink(params: {
|
|
317
|
+
workstream: NormalizedWorkstream
|
|
318
|
+
workstreamId: string
|
|
319
|
+
visibleAgentId: string
|
|
320
|
+
}): { route: '/chat'; search: { chat?: string; agent?: string; tab: 'cos' | 'team' | 'workstreams' } } {
|
|
321
|
+
if (params.workstream.mode === 'direct') {
|
|
322
|
+
return {
|
|
323
|
+
route: '/chat',
|
|
324
|
+
search: { agent: params.visibleAgentId, tab: params.visibleAgentId === 'chief' ? 'cos' : 'team' },
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { route: '/chat', search: { chat: params.workstreamId, tab: 'workstreams' } }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function buildRecentActivityChatSystemTitle(params: {
|
|
332
|
+
workstream: NormalizedWorkstream
|
|
333
|
+
visibleAgentId: string
|
|
334
|
+
}): string {
|
|
335
|
+
if (params.workstream.mode === 'direct') {
|
|
336
|
+
return `Conversation with ${agentDisplayNames[params.visibleAgentId]}`
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return params.workstream.title.trim() || 'Workstream update'
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams): Promise<PreparedWorkstreamTurn> {
|
|
343
|
+
const { workstream, workstreamRef, orgRef, userRef, userName } = params
|
|
344
|
+
const runtimeAdapters = getRuntimeAdapters()
|
|
345
|
+
const turnHooks = getTurnHooks()
|
|
346
|
+
const toolProviders = getToolProviders()
|
|
347
|
+
const workspaceProvider = runtimeAdapters.services?.workspaceProvider
|
|
348
|
+
const orgIdString = recordIdToString(orgRef, TABLES.ORGANIZATION)
|
|
349
|
+
const userIdString = recordIdToString(userRef, TABLES.USER)
|
|
350
|
+
const workstreamIdString = recordIdToString(workstreamRef, TABLES.WORKSTREAM)
|
|
351
|
+
|
|
352
|
+
const hydrateMessageFileUrls = (message: ChatMessage): ChatMessage => ({
|
|
353
|
+
...message,
|
|
354
|
+
parts: attachmentService.hydrateSignedFileUrlsInMessageParts({
|
|
355
|
+
parts: message.parts as Array<Record<string, unknown>>,
|
|
356
|
+
orgId: orgRef,
|
|
357
|
+
userId: userRef,
|
|
358
|
+
}) as ChatMessage['parts'],
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
let inputMessage: ChatMessage | undefined
|
|
362
|
+
const shouldPersistInputMessage = params.kind === 'userTurn' ? params.persistInputMessage !== false : false
|
|
363
|
+
const shouldProcessPostRunSideEffects = params.kind === 'approvalContinuation' || shouldPersistInputMessage
|
|
364
|
+
if (params.kind === 'userTurn') {
|
|
365
|
+
inputMessage = hydrateMessageFileUrls(withMessageCreatedAt(params.inputMessage))
|
|
366
|
+
if (inputMessage.role !== 'user') {
|
|
367
|
+
throw new WorkstreamTurnError('Only user messages can be submitted to the workstream runtime.', 400)
|
|
368
|
+
}
|
|
369
|
+
if (!hasMessageContent(inputMessage.parts)) {
|
|
370
|
+
throw new WorkstreamTurnError('Workstream messages must include text or attachments.', 400)
|
|
371
|
+
}
|
|
372
|
+
if (workstream.mode === 'direct' && !workstream.agentId) {
|
|
373
|
+
throw new WorkstreamTurnError('Direct workstreams require an assigned agent.', 400)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const workstreamRecord = await waitForWorkstreamCompactionIfNeeded(workstreamRef)
|
|
378
|
+
if (toOptionalTrimmedString(workstreamRecord.activeRunId)) {
|
|
379
|
+
throw new WorkstreamTurnError('A chat run is already active.', 409)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (params.kind === 'approvalContinuation') {
|
|
383
|
+
const approvedAssistantMessage = [...params.approvalMessages]
|
|
384
|
+
.reverse()
|
|
385
|
+
.find((m) => m.role === 'assistant' && hasApprovalRespondedParts(m))
|
|
386
|
+
if (!approvedAssistantMessage) {
|
|
387
|
+
throw new WorkstreamTurnError('No approval-responded message found.', 400)
|
|
388
|
+
}
|
|
389
|
+
await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [approvedAssistantMessage] })
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const workspacePromise = workspaceProvider ? workspaceProvider.getWorkspace(orgRef) : Promise.resolve({})
|
|
393
|
+
const initialWorkstreamState = parsePersistedWorkstreamState(workstreamRecord.state)
|
|
394
|
+
const persistedLiveHistoryPromise = workstreamMessageService.listMessagesAfterCursor(
|
|
395
|
+
workstreamRef,
|
|
396
|
+
toOptionalTrimmedString(workstreamRecord.lastCompactedMessageId) ?? undefined,
|
|
397
|
+
)
|
|
398
|
+
const persistedRecentHistoryPromise = workstreamMessageService.listRecentMessages(workstreamRef, 64)
|
|
399
|
+
|
|
400
|
+
let userMessage: ChatMessage | undefined
|
|
401
|
+
if (inputMessage) {
|
|
402
|
+
userMessage = {
|
|
403
|
+
...inputMessage,
|
|
404
|
+
id: inputMessage.id || Bun.randomUUIDv7(),
|
|
405
|
+
role: 'user',
|
|
406
|
+
parts: inputMessage.parts,
|
|
407
|
+
metadata: { ...inputMessage.metadata, createdAt: toTimestamp(inputMessage.metadata?.createdAt) },
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const [workspace, persistedLiveHistory, persistedRecentHistory] = await Promise.all([
|
|
412
|
+
workspacePromise,
|
|
413
|
+
persistedLiveHistoryPromise,
|
|
414
|
+
persistedRecentHistoryPromise,
|
|
415
|
+
])
|
|
416
|
+
const workspaceLifecycleState = workspaceProvider ? await workspaceProvider.getLifecycleState?.(workspace) : undefined
|
|
417
|
+
const workspaceProfileState = workspaceProvider
|
|
418
|
+
? await workspaceProvider.readProfileProjectionState?.(workspace)
|
|
419
|
+
: undefined
|
|
420
|
+
const [liveHistory, recentHistory] = await Promise.all([
|
|
421
|
+
persistedLiveHistory.length === 0
|
|
422
|
+
? Promise.resolve([] as ChatMessage[])
|
|
423
|
+
: validateUIMessages<ChatMessage>({
|
|
424
|
+
messages: persistedLiveHistory,
|
|
425
|
+
metadataSchema: messageMetadataSchema,
|
|
426
|
+
dataSchemas: dataPartsSchema,
|
|
427
|
+
}).then((messages) => messages.map(hydrateMessageFileUrls)),
|
|
428
|
+
persistedRecentHistory.length === 0
|
|
429
|
+
? Promise.resolve([] as ChatMessage[])
|
|
430
|
+
: validateUIMessages<ChatMessage>({
|
|
431
|
+
messages: persistedRecentHistory,
|
|
432
|
+
metadataSchema: messageMetadataSchema,
|
|
433
|
+
dataSchemas: dataPartsSchema,
|
|
434
|
+
}).then((messages) => messages.map(hydrateMessageFileUrls)),
|
|
435
|
+
])
|
|
436
|
+
|
|
437
|
+
if (userMessage && shouldPersistInputMessage) {
|
|
438
|
+
await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [userMessage] })
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const originalMessages = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
|
|
442
|
+
const allAssistantMessages: ChatMessage[] = []
|
|
443
|
+
const referenceUserMessage =
|
|
444
|
+
params.kind === 'userTurn' && !shouldPersistInputMessage
|
|
445
|
+
? [...liveHistory].reverse().find((m) => m.role === 'user')
|
|
446
|
+
: (userMessage ?? [...liveHistory].reverse().find((m) => m.role === 'user'))
|
|
447
|
+
const messageText = referenceUserMessage ? extractMessageText(referenceUserMessage).trim() : ''
|
|
448
|
+
const onboardingActive = workspaceLifecycleState?.bootstrapActive ?? false
|
|
449
|
+
if (workstream.core && !workstream.coreType) {
|
|
450
|
+
throw new WorkstreamTurnError('Core workstreams require a core type.', 400)
|
|
451
|
+
}
|
|
452
|
+
const coreWorkstreamProfile: { config: { agentId: string }; instructions: string; skills?: string[] } | null =
|
|
453
|
+
workstream.core && workstream.coreType
|
|
454
|
+
? (getCoreWorkstreamProfile(workstream.coreType) as unknown as {
|
|
455
|
+
config: { agentId: string }
|
|
456
|
+
instructions: string
|
|
457
|
+
skills?: string[]
|
|
458
|
+
})
|
|
459
|
+
: null
|
|
460
|
+
const visibleWorkstreamAgentId =
|
|
461
|
+
workstream.mode === 'direct' ? workstream.agentId : (coreWorkstreamProfile?.config.agentId ?? 'chief')
|
|
462
|
+
const coreInstructionSections = coreWorkstreamProfile ? [coreWorkstreamProfile.instructions] : undefined
|
|
463
|
+
const getLinearInstallationByOrgId = getPluginService([
|
|
464
|
+
'linear',
|
|
465
|
+
'services',
|
|
466
|
+
'linearService',
|
|
467
|
+
'getInstallationByOrgId',
|
|
468
|
+
])
|
|
469
|
+
const getGithubInstallationForOrganization = getPluginService([
|
|
470
|
+
'github',
|
|
471
|
+
'services',
|
|
472
|
+
'githubService',
|
|
473
|
+
'getInstallationForOrganization',
|
|
474
|
+
])
|
|
475
|
+
|
|
476
|
+
const highImpactAssessment = classifyHighImpactResponse({ message: messageText })
|
|
477
|
+
const policyAssessment = classifyPolicyClasses({ message: messageText })
|
|
478
|
+
const reasoningProfile = resolveReasoningProfile({
|
|
479
|
+
message: messageText,
|
|
480
|
+
forceDeep: highImpactAssessment.classes.length > 0 || policyAssessment.classes.length > 0,
|
|
481
|
+
explicitProfile: onboardingActive ? 'standard' : undefined,
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
const [linearInstallation, githubInstallation, indexedRepoContext, recentDomainEvents, promptSummary] =
|
|
485
|
+
await Promise.all([
|
|
486
|
+
getLinearInstallationByOrgId ? (getLinearInstallationByOrgId(orgRef) as Promise<unknown>) : Promise.resolve(null),
|
|
487
|
+
getGithubInstallationForOrganization
|
|
488
|
+
? (getGithubInstallationForOrganization(orgIdString) as Promise<unknown>)
|
|
489
|
+
: Promise.resolve(null),
|
|
490
|
+
buildIndexedRepositoriesContext(orgIdString),
|
|
491
|
+
workspaceProvider?.listRecentDomainEvents?.(orgRef, 5) ?? Promise.resolve([] as Array<Record<string, unknown>>),
|
|
492
|
+
workspaceProvider?.buildPromptSummary
|
|
493
|
+
? workspaceProvider.buildPromptSummary(orgRef).catch(() => undefined)
|
|
494
|
+
: Promise.resolve(undefined),
|
|
495
|
+
])
|
|
496
|
+
let linearInstalled = Boolean(linearInstallation)
|
|
497
|
+
let githubInstalled = Boolean(githubInstallation)
|
|
498
|
+
let promptContext = buildAgentPromptContext({
|
|
499
|
+
workspaceName: workspaceProfileState?.workspaceName ?? readOptionalString((workspace as { name?: unknown }).name),
|
|
500
|
+
summaryBlock: workspaceProfileState?.summaryBlock,
|
|
501
|
+
structuredProfile: workspaceProfileState?.structuredProfile,
|
|
502
|
+
promptSummary,
|
|
503
|
+
userName: userName ?? undefined,
|
|
504
|
+
recentDomainEvents,
|
|
505
|
+
})
|
|
506
|
+
let retrievedKnowledgeSection: string | undefined =
|
|
507
|
+
onboardingActive || !messageText
|
|
508
|
+
? undefined
|
|
509
|
+
: await workspaceProvider?.buildRetrievedKnowledgeSection?.({
|
|
510
|
+
workspaceId: orgIdString,
|
|
511
|
+
userId: userIdString,
|
|
512
|
+
query: messageText,
|
|
513
|
+
})
|
|
514
|
+
const buildContextResult = asRecord(
|
|
515
|
+
await turnHooks.buildContext?.({
|
|
516
|
+
workstream,
|
|
517
|
+
workstreamRef,
|
|
518
|
+
orgRef,
|
|
519
|
+
userRef,
|
|
520
|
+
userName,
|
|
521
|
+
workspace,
|
|
522
|
+
onboardingActive,
|
|
523
|
+
messageText,
|
|
524
|
+
linearInstalled,
|
|
525
|
+
githubInstalled,
|
|
526
|
+
indexedRepoContext,
|
|
527
|
+
promptContext,
|
|
528
|
+
workspaceLifecycleState,
|
|
529
|
+
workspaceProfileState,
|
|
530
|
+
promptSummary,
|
|
531
|
+
recentDomainEvents,
|
|
532
|
+
retrievedKnowledgeSection,
|
|
533
|
+
}),
|
|
534
|
+
)
|
|
535
|
+
const buildContextPromptDetails = readOptionalString(buildContextResult?.systemWorkspaceDetails)
|
|
536
|
+
if (buildContextPromptDetails) {
|
|
537
|
+
promptContext = { systemWorkspaceDetails: buildContextPromptDetails }
|
|
538
|
+
}
|
|
539
|
+
const buildContextRetrievedKnowledge = readOptionalString(buildContextResult?.retrievedKnowledgeSection)
|
|
540
|
+
if (buildContextRetrievedKnowledge !== undefined) {
|
|
541
|
+
retrievedKnowledgeSection = buildContextRetrievedKnowledge
|
|
542
|
+
}
|
|
543
|
+
const buildContextLinearInstalled = readOptionalBoolean(buildContextResult?.linearInstalled)
|
|
544
|
+
if (buildContextLinearInstalled !== undefined) {
|
|
545
|
+
linearInstalled = buildContextLinearInstalled
|
|
546
|
+
}
|
|
547
|
+
const buildContextGithubInstalled = readOptionalBoolean(buildContextResult?.githubInstalled)
|
|
548
|
+
if (buildContextGithubInstalled !== undefined) {
|
|
549
|
+
githubInstalled = buildContextGithubInstalled
|
|
550
|
+
}
|
|
551
|
+
const hookInstructionSections = readInstructionSections(
|
|
552
|
+
await turnHooks.buildExtraInstructionSections?.({
|
|
553
|
+
workstream,
|
|
554
|
+
workstreamRef,
|
|
555
|
+
orgRef,
|
|
556
|
+
userRef,
|
|
557
|
+
userName,
|
|
558
|
+
workspace,
|
|
559
|
+
onboardingActive,
|
|
560
|
+
messageText,
|
|
561
|
+
linearInstalled,
|
|
562
|
+
githubInstalled,
|
|
563
|
+
indexedRepoContext,
|
|
564
|
+
promptContext,
|
|
565
|
+
workspaceLifecycleState,
|
|
566
|
+
workspaceProfileState,
|
|
567
|
+
promptSummary,
|
|
568
|
+
recentDomainEvents,
|
|
569
|
+
retrievedKnowledgeSection,
|
|
570
|
+
context: buildContextResult,
|
|
571
|
+
}),
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
let memoryBlock = workstreamService.formatMemoryBlockForPrompt(workstreamRecord)
|
|
575
|
+
let workstreamState = initialWorkstreamState
|
|
576
|
+
const executionPlanInstructionSectionCache = createExecutionPlanInstructionSectionCache({
|
|
577
|
+
disabled: onboardingActive,
|
|
578
|
+
loadPlan: async () => await executionPlanService.getActivePlanForWorkstream(workstreamRef),
|
|
579
|
+
})
|
|
580
|
+
const getExecutionPlanInstructionSections = async (): Promise<string[] | undefined> =>
|
|
581
|
+
await executionPlanInstructionSectionCache.getSections()
|
|
582
|
+
const invalidateExecutionPlanInstructionSections = () => {
|
|
583
|
+
executionPlanInstructionSectionCache.invalidate()
|
|
584
|
+
}
|
|
585
|
+
const preSeededMemoriesByAgent = new Map<string, string | undefined>()
|
|
586
|
+
const getPreSeededMemoriesSection = async (agentId: string): Promise<string | undefined> => {
|
|
587
|
+
if (preSeededMemoriesByAgent.has(agentId)) {
|
|
588
|
+
return preSeededMemoriesByAgent.get(agentId)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const preSeededMemories = await memoryService.getTopMemories({
|
|
592
|
+
orgId: orgIdString,
|
|
593
|
+
agentName: agentId,
|
|
594
|
+
limit: PRESEEDED_MEMORY_LOOKUP_LIMIT,
|
|
595
|
+
})
|
|
596
|
+
preSeededMemoriesByAgent.set(agentId, preSeededMemories)
|
|
597
|
+
return preSeededMemories
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const learnedSkillsByAgent = new Map<string, string | undefined>()
|
|
601
|
+
const getLearnedSkillsSection = async (agentId: string): Promise<string | undefined> => {
|
|
602
|
+
if (onboardingActive) return undefined
|
|
603
|
+
if (learnedSkillsByAgent.has(agentId)) return learnedSkillsByAgent.get(agentId)
|
|
604
|
+
|
|
605
|
+
const section = await learnedSkillService
|
|
606
|
+
.retrieveForTurn({ orgId: orgIdString, agentId, query: messageText, limit: 3, minConfidence: 0.6 })
|
|
607
|
+
.catch((error: unknown) => {
|
|
608
|
+
aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
|
|
609
|
+
return undefined
|
|
610
|
+
})
|
|
611
|
+
learnedSkillsByAgent.set(agentId, section)
|
|
612
|
+
return section
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const persistedChatSummary = typeof workstreamRecord.chatSummary === 'string' ? workstreamRecord.chatSummary : ''
|
|
616
|
+
const messagesForContext = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
|
|
617
|
+
let currentMessages = contextCompactionRuntime.prependSummaryMessage(persistedChatSummary, messagesForContext)
|
|
618
|
+
const referenceUserMessageId = referenceUserMessage?.id ?? ''
|
|
619
|
+
const listReadableUploads = (extraMessages: ChatMessage[] = []) =>
|
|
620
|
+
listReadableUploadsFromChatMessages({
|
|
621
|
+
messages: [...currentMessages, ...extraMessages],
|
|
622
|
+
orgId: orgRef,
|
|
623
|
+
userId: userRef,
|
|
624
|
+
})
|
|
625
|
+
const buildRunInputMessages = (extraMessages: ChatMessage[] = []): ChatMessage[] =>
|
|
626
|
+
buildModelInputMessagesWithUploadMetadata({
|
|
627
|
+
messages: [...currentMessages, ...extraMessages],
|
|
628
|
+
latestUserMessageId: referenceUserMessageId,
|
|
629
|
+
uploadMetadataText: buildReadableUploadMetadataText(listReadableUploads(extraMessages)),
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
return {
|
|
633
|
+
originalMessages,
|
|
634
|
+
run: async (writer?: UIMessageStreamWriter<ChatMessage>) => {
|
|
635
|
+
const executeRun = async () => {
|
|
636
|
+
const serverRunId = Bun.randomUUIDv7()
|
|
637
|
+
const runAbort = createServerRunAbortController(params.abortSignal)
|
|
638
|
+
await workstreamService.setActiveRunId(workstreamRef, serverRunId)
|
|
639
|
+
chatRunRegistry.register(serverRunId, runAbort.controller)
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
const buildAgentMetadataPatch = (agentId: string, agentName: string): NonNullable<MessageMetadata> => ({
|
|
643
|
+
agentId,
|
|
644
|
+
agentName,
|
|
645
|
+
reasoningProfile: reasoningProfile.name,
|
|
646
|
+
highImpactClasses: highImpactAssessment.classes,
|
|
647
|
+
policyClasses: policyAssessment.classes,
|
|
648
|
+
semanticTerminationReason: 'none',
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
const createObserver = (agentId: string) => ({
|
|
652
|
+
run: <T>(fn: () => T | Promise<T>) => Promise.resolve(fn()),
|
|
653
|
+
recordError: (error: unknown) => {
|
|
654
|
+
aiLogger.error`Agent run failed (agent=${agentId}): ${error}`
|
|
655
|
+
},
|
|
656
|
+
recordAbort: (error: unknown) => {
|
|
657
|
+
aiLogger.info`Agent run aborted (agent=${agentId}): ${error instanceof Error ? error.message : String(error)}`
|
|
658
|
+
},
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
const commitAssistantResponse = async (response: ChatMessage, agentId: string, agentName: string) => {
|
|
662
|
+
const committed = withMessageCreatedAt({
|
|
663
|
+
...response,
|
|
664
|
+
metadata: { ...response.metadata, ...buildAgentMetadataPatch(agentId, agentName) },
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [committed] })
|
|
668
|
+
const currentMessageIndex = currentMessages.findIndex((item) => item.id === committed.id)
|
|
669
|
+
if (currentMessageIndex >= 0) {
|
|
670
|
+
currentMessages[currentMessageIndex] = committed
|
|
671
|
+
} else {
|
|
672
|
+
currentMessages = [...currentMessages, committed]
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const assistantIndex = allAssistantMessages.findIndex((item) => item.id === committed.id)
|
|
676
|
+
if (assistantIndex >= 0) {
|
|
677
|
+
allAssistantMessages[assistantIndex] = committed
|
|
678
|
+
} else {
|
|
679
|
+
allAssistantMessages.push(committed)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return committed
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const streamAgentResponse = async (streamParams: {
|
|
686
|
+
agentId: string
|
|
687
|
+
mode: 'direct' | 'fixedWorkstreamMode' | 'workstreamMode'
|
|
688
|
+
messages: ChatMessage[]
|
|
689
|
+
tools: ToolSet
|
|
690
|
+
observer: ReturnType<typeof createObserver>
|
|
691
|
+
skills?: string[]
|
|
692
|
+
additionalInstructionSections?: string[]
|
|
693
|
+
writer?: UIMessageStreamWriter<ChatMessage>
|
|
694
|
+
stopWhen?: StopCondition<ToolSet> | Array<StopCondition<ToolSet>>
|
|
695
|
+
prepareStep?: PrepareStepFunction<ToolSet>
|
|
696
|
+
abortSignal?: AbortSignal
|
|
697
|
+
}): Promise<ChatMessage> => {
|
|
698
|
+
const executionPlanInstructionSections = await getExecutionPlanInstructionSections()
|
|
699
|
+
const agentResolution = asRecord(
|
|
700
|
+
await turnHooks.resolveAgent?.({
|
|
701
|
+
agentId: streamParams.agentId,
|
|
702
|
+
mode: streamParams.mode,
|
|
703
|
+
workstream,
|
|
704
|
+
workstreamRef,
|
|
705
|
+
orgRef,
|
|
706
|
+
userRef,
|
|
707
|
+
userName,
|
|
708
|
+
onboardingActive,
|
|
709
|
+
linearInstalled,
|
|
710
|
+
githubInstalled,
|
|
711
|
+
reasoningProfile: reasoningProfile.name,
|
|
712
|
+
skills: streamParams.skills,
|
|
713
|
+
additionalInstructionSections: streamParams.additionalInstructionSections,
|
|
714
|
+
context: buildContextResult,
|
|
715
|
+
}),
|
|
716
|
+
)
|
|
717
|
+
const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? streamParams.agentId
|
|
718
|
+
const config = getAgentRuntimeConfig({
|
|
719
|
+
agentId: resolvedAgentId,
|
|
720
|
+
workstreamMode: workstream.mode,
|
|
721
|
+
mode: streamParams.mode,
|
|
722
|
+
skills: streamParams.skills,
|
|
723
|
+
onboardingActive,
|
|
724
|
+
linearInstalled,
|
|
725
|
+
reasoningProfile: reasoningProfile.name,
|
|
726
|
+
systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
|
|
727
|
+
preSeededMemoriesSection: await getPreSeededMemoriesSection(resolvedAgentId),
|
|
728
|
+
retrievedKnowledgeSection,
|
|
729
|
+
workstreamMemoryBlock: memoryBlock,
|
|
730
|
+
workstreamStateSection: contextCompactionRuntime.formatWorkstreamStateForPrompt(workstreamState),
|
|
731
|
+
learnedSkillsSection: await getLearnedSkillsSection(resolvedAgentId),
|
|
732
|
+
additionalInstructionSections: mergeInstructionSections(
|
|
733
|
+
executionPlanInstructionSections,
|
|
734
|
+
streamParams.additionalInstructionSections,
|
|
735
|
+
hookInstructionSections,
|
|
736
|
+
readInstructionSections(agentResolution?.additionalInstructionSections),
|
|
737
|
+
optionalInstructionSection(agentResolution?.extraInstructions),
|
|
738
|
+
),
|
|
739
|
+
context: buildContextResult,
|
|
740
|
+
}) as AgentRuntimeConfig
|
|
741
|
+
const modelMessages = await convertToModelMessages(streamParams.messages, {
|
|
742
|
+
ignoreIncompleteToolCalls: true,
|
|
743
|
+
})
|
|
744
|
+
const agent = (createAgent as unknown as AgentFactory)[config.id as string]({
|
|
745
|
+
mode: streamParams.mode,
|
|
746
|
+
tools: streamParams.tools,
|
|
747
|
+
extraInstructions: config.extraInstructions,
|
|
748
|
+
stopWhen: (agentResolution?.stopWhen as
|
|
749
|
+
| StopCondition<ToolSet>
|
|
750
|
+
| Array<StopCondition<ToolSet>>
|
|
751
|
+
| undefined) ??
|
|
752
|
+
streamParams.stopWhen ?? [stepCountIs(config.maxSteps as number)],
|
|
753
|
+
prepareStep:
|
|
754
|
+
(agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
|
|
755
|
+
})
|
|
756
|
+
const agentAbortSignal = streamParams.abortSignal ?? runAbort.signal
|
|
757
|
+
|
|
758
|
+
let result: unknown
|
|
759
|
+
try {
|
|
760
|
+
result = await streamParams.observer.run(() =>
|
|
761
|
+
agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
|
|
762
|
+
)
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (agentAbortSignal.aborted) {
|
|
765
|
+
streamParams.observer.recordAbort(error)
|
|
766
|
+
} else {
|
|
767
|
+
streamParams.observer.recordError(error)
|
|
768
|
+
}
|
|
769
|
+
throw error
|
|
770
|
+
}
|
|
771
|
+
if (!hasUIMessageStream(result)) {
|
|
772
|
+
throw new Error(`Agent run for ${resolvedAgentId} did not expose a UI message stream.`)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
let responseMessage: ChatMessage | null = null
|
|
776
|
+
let resolveFinishedStream!: () => void
|
|
777
|
+
const finishedStream = new Promise<void>((resolve) => {
|
|
778
|
+
resolveFinishedStream = resolve
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
const uiStream = result.toUIMessageStream({
|
|
782
|
+
generateMessageId: () => Bun.randomUUIDv7(),
|
|
783
|
+
originalMessages: streamParams.messages,
|
|
784
|
+
sendReasoning: true,
|
|
785
|
+
sendSources: true,
|
|
786
|
+
messageMetadata: createAgentMessageMetadata({
|
|
787
|
+
agentId: resolvedAgentId,
|
|
788
|
+
agentName: config.displayName as string,
|
|
789
|
+
}),
|
|
790
|
+
onFinish: ({ responseMessage: finishedResponseMessage }: { responseMessage: ChatMessage }) => {
|
|
791
|
+
responseMessage = withMessageCreatedAt(finishedResponseMessage)
|
|
792
|
+
resolveFinishedStream()
|
|
793
|
+
},
|
|
794
|
+
}) as ReadableStream<ChatStreamChunk>
|
|
795
|
+
const reader = uiStream.getReader()
|
|
796
|
+
let liveTrackerState: WorkstreamState = workstreamState ?? parseWorkstreamState(null)
|
|
797
|
+
const liveTrackedToolCalls = new Map<string, string>()
|
|
798
|
+
try {
|
|
799
|
+
for (;;) {
|
|
800
|
+
const { done, value } = await reader.read()
|
|
801
|
+
if (done) break
|
|
802
|
+
if (streamParams.writer) {
|
|
803
|
+
streamParams.writer.write(value)
|
|
804
|
+
}
|
|
805
|
+
if (value.type === 'tool-input-available' && value.toolName === 'inspectWebsite') {
|
|
806
|
+
const title = buildInspectWebsiteTrackerTitle(value.input)
|
|
807
|
+
liveTrackedToolCalls.set(value.toolCallId, title)
|
|
808
|
+
liveTrackerState = emitTransientWorkstreamTrackerState({
|
|
809
|
+
writer: streamParams.writer,
|
|
810
|
+
workstreamRecord,
|
|
811
|
+
existingState: liveTrackerState,
|
|
812
|
+
delta: {
|
|
813
|
+
taskUpdates: [
|
|
814
|
+
{
|
|
815
|
+
title,
|
|
816
|
+
status: 'in-progress',
|
|
817
|
+
owner: streamParams.agentId,
|
|
818
|
+
externalId: value.toolCallId,
|
|
819
|
+
sourceMessageIds: [],
|
|
820
|
+
},
|
|
821
|
+
],
|
|
822
|
+
},
|
|
823
|
+
})
|
|
824
|
+
}
|
|
825
|
+
if (
|
|
826
|
+
value.type === 'tool-output-available' &&
|
|
827
|
+
value.preliminary !== true &&
|
|
828
|
+
liveTrackedToolCalls.has(value.toolCallId)
|
|
829
|
+
) {
|
|
830
|
+
liveTrackerState = emitTransientWorkstreamTrackerState({
|
|
831
|
+
writer: streamParams.writer,
|
|
832
|
+
workstreamRecord,
|
|
833
|
+
existingState: liveTrackerState,
|
|
834
|
+
delta: {
|
|
835
|
+
taskUpdates: [
|
|
836
|
+
{
|
|
837
|
+
title: liveTrackedToolCalls.get(value.toolCallId) ?? 'Inspect website intelligence',
|
|
838
|
+
status: 'done',
|
|
839
|
+
owner: streamParams.agentId,
|
|
840
|
+
externalId: value.toolCallId,
|
|
841
|
+
sourceMessageIds: [],
|
|
842
|
+
},
|
|
843
|
+
],
|
|
844
|
+
},
|
|
845
|
+
})
|
|
846
|
+
liveTrackedToolCalls.delete(value.toolCallId)
|
|
847
|
+
}
|
|
848
|
+
if (
|
|
849
|
+
(value.type === 'tool-output-error' || value.type === 'tool-output-denied') &&
|
|
850
|
+
liveTrackedToolCalls.has(value.toolCallId)
|
|
851
|
+
) {
|
|
852
|
+
liveTrackerState = emitTransientWorkstreamTrackerState({
|
|
853
|
+
writer: streamParams.writer,
|
|
854
|
+
workstreamRecord,
|
|
855
|
+
existingState: liveTrackerState,
|
|
856
|
+
delta: {
|
|
857
|
+
taskUpdates: [
|
|
858
|
+
{
|
|
859
|
+
title: liveTrackedToolCalls.get(value.toolCallId) ?? 'Inspect website intelligence',
|
|
860
|
+
status: 'blocked',
|
|
861
|
+
owner: streamParams.agentId,
|
|
862
|
+
externalId: value.toolCallId,
|
|
863
|
+
sourceMessageIds: [],
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
},
|
|
867
|
+
})
|
|
868
|
+
liveTrackedToolCalls.delete(value.toolCallId)
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
} finally {
|
|
872
|
+
reader.releaseLock()
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const finalizedResponseMessage = await finishedStream.then(() => responseMessage)
|
|
876
|
+
if (finalizedResponseMessage === null) {
|
|
877
|
+
throw new Error(`Agent run for ${resolvedAgentId} did not produce a response message.`)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
for (const toolError of collectToolOutputErrors({ responseMessage: finalizedResponseMessage })) {
|
|
881
|
+
aiLogger.warn`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return finalizedResponseMessage
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const runVisibleAgent = async (runParams: {
|
|
888
|
+
agentId: string
|
|
889
|
+
mode: 'direct' | 'fixedWorkstreamMode' | 'workstreamMode'
|
|
890
|
+
skills?: string[]
|
|
891
|
+
additionalInstructionSections?: string[]
|
|
892
|
+
extraMessages?: ChatMessage[]
|
|
893
|
+
extraTools?: ToolSet
|
|
894
|
+
}): Promise<ChatMessage> => {
|
|
895
|
+
let runMemoryBlock = memoryBlock
|
|
896
|
+
const tools: ToolSet = {
|
|
897
|
+
...((await buildAgentTools({
|
|
898
|
+
agentId: runParams.agentId,
|
|
899
|
+
orgId: orgRef,
|
|
900
|
+
userId: userRef,
|
|
901
|
+
userName: userName ?? 'there',
|
|
902
|
+
workstreamId: workstreamRef,
|
|
903
|
+
orgIdString,
|
|
904
|
+
workstreamMode: workstream.mode,
|
|
905
|
+
mode: runParams.mode,
|
|
906
|
+
linearInstalled,
|
|
907
|
+
onboardingActive,
|
|
908
|
+
githubInstalled,
|
|
909
|
+
provideRepoTool: indexedRepoContext.provideRepoTool,
|
|
910
|
+
skills: runParams.skills,
|
|
911
|
+
defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[runParams.agentId],
|
|
912
|
+
memoryBlock: runMemoryBlock,
|
|
913
|
+
onAppendMemoryBlock: (value: string) => {
|
|
914
|
+
runMemoryBlock = value
|
|
915
|
+
},
|
|
916
|
+
availableUploads: listReadableUploads(runParams.extraMessages),
|
|
917
|
+
includeExecutionPlanTools: runParams.mode !== 'fixedWorkstreamMode',
|
|
918
|
+
onExecutionPlanChanged: invalidateExecutionPlanInstructionSections,
|
|
919
|
+
context: buildContextResult,
|
|
920
|
+
})) as ToolSet),
|
|
921
|
+
...toolProviders,
|
|
922
|
+
...runParams.extraTools,
|
|
923
|
+
}
|
|
924
|
+
const responseMessage = await streamAgentResponse({
|
|
925
|
+
agentId: runParams.agentId,
|
|
926
|
+
mode: runParams.mode,
|
|
927
|
+
messages: buildRunInputMessages(runParams.extraMessages),
|
|
928
|
+
tools,
|
|
929
|
+
observer: createObserver(runParams.agentId),
|
|
930
|
+
skills: runParams.skills,
|
|
931
|
+
additionalInstructionSections: runParams.additionalInstructionSections,
|
|
932
|
+
writer,
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
memoryBlock = runMemoryBlock
|
|
936
|
+
|
|
937
|
+
return await commitAssistantResponse(
|
|
938
|
+
responseMessage,
|
|
939
|
+
runParams.agentId,
|
|
940
|
+
agentDisplayNames[runParams.agentId] ?? runParams.agentId,
|
|
941
|
+
)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const consultSpecialistTool = createTool({
|
|
945
|
+
description: 'Consult one specialist teammate for domain-specific guidance before replying to the user.',
|
|
946
|
+
inputSchema: ConsultSpecialistArgsSchema,
|
|
947
|
+
execute: async function* (
|
|
948
|
+
{ agentId, task }: z.infer<typeof ConsultSpecialistArgsSchema>,
|
|
949
|
+
{ abortSignal: toolAbortSignal }: { abortSignal?: AbortSignal },
|
|
950
|
+
) {
|
|
951
|
+
let specialistMemoryBlock = memoryBlock
|
|
952
|
+
const specialistTaskMessage = buildSpecialistTaskMessage({ agentId, task })
|
|
953
|
+
const specialistTools = await buildAgentTools({
|
|
954
|
+
agentId,
|
|
955
|
+
orgId: orgRef,
|
|
956
|
+
userId: userRef,
|
|
957
|
+
userName: userName ?? 'there',
|
|
958
|
+
workstreamId: workstreamRef,
|
|
959
|
+
orgIdString,
|
|
960
|
+
workstreamMode: workstream.mode,
|
|
961
|
+
mode: 'fixedWorkstreamMode',
|
|
962
|
+
linearInstalled,
|
|
963
|
+
onboardingActive,
|
|
964
|
+
githubInstalled,
|
|
965
|
+
provideRepoTool: indexedRepoContext.provideRepoTool,
|
|
966
|
+
defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[agentId],
|
|
967
|
+
memoryBlock: specialistMemoryBlock,
|
|
968
|
+
onAppendMemoryBlock: (value: string) => {
|
|
969
|
+
specialistMemoryBlock = value
|
|
970
|
+
},
|
|
971
|
+
availableUploads: listReadableUploads([specialistTaskMessage]),
|
|
972
|
+
includeExecutionPlanTools: false,
|
|
973
|
+
context: buildContextResult,
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
const specialistExecutionPlanInstructionSections = await getExecutionPlanInstructionSections()
|
|
977
|
+
const specialistConfig = getAgentRuntimeConfig({
|
|
978
|
+
agentId,
|
|
979
|
+
workstreamMode: workstream.mode,
|
|
980
|
+
mode: 'fixedWorkstreamMode',
|
|
981
|
+
onboardingActive,
|
|
982
|
+
linearInstalled,
|
|
983
|
+
reasoningProfile: reasoningProfile.name,
|
|
984
|
+
systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
|
|
985
|
+
preSeededMemoriesSection: await getPreSeededMemoriesSection(agentId),
|
|
986
|
+
retrievedKnowledgeSection,
|
|
987
|
+
workstreamMemoryBlock: specialistMemoryBlock,
|
|
988
|
+
workstreamStateSection: contextCompactionRuntime.formatWorkstreamStateForPrompt(workstreamState),
|
|
989
|
+
learnedSkillsSection: await getLearnedSkillsSection(agentId),
|
|
990
|
+
additionalInstructionSections: mergeInstructionSections(
|
|
991
|
+
specialistExecutionPlanInstructionSections,
|
|
992
|
+
coreInstructionSections,
|
|
993
|
+
hookInstructionSections,
|
|
994
|
+
),
|
|
995
|
+
context: buildContextResult,
|
|
996
|
+
}) as AgentRuntimeConfig
|
|
997
|
+
const observer = createObserver(agentId)
|
|
998
|
+
const agent = (createAgent as unknown as AgentFactory)[specialistConfig.id as string]({
|
|
999
|
+
mode: 'fixedWorkstreamMode',
|
|
1000
|
+
tools: { ...(specialistTools as ToolSet), ...toolProviders },
|
|
1001
|
+
extraInstructions: specialistConfig.extraInstructions,
|
|
1002
|
+
stopWhen: [stepCountIs(specialistConfig.maxSteps as number)],
|
|
1003
|
+
})
|
|
1004
|
+
const modelMessages = await convertToModelMessages(buildRunInputMessages([specialistTaskMessage]), {
|
|
1005
|
+
ignoreIncompleteToolCalls: true,
|
|
1006
|
+
})
|
|
1007
|
+
const specialistAbortSignal = toolAbortSignal ?? runAbort.signal
|
|
1008
|
+
let result: unknown
|
|
1009
|
+
try {
|
|
1010
|
+
result = await observer.run(() =>
|
|
1011
|
+
agent.stream({ messages: modelMessages, abortSignal: specialistAbortSignal }),
|
|
1012
|
+
)
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
if (specialistAbortSignal.aborted) {
|
|
1015
|
+
observer.recordAbort(error)
|
|
1016
|
+
} else {
|
|
1017
|
+
observer.recordError(error)
|
|
1018
|
+
}
|
|
1019
|
+
throw error
|
|
1020
|
+
}
|
|
1021
|
+
if (!hasUIMessageStream(result)) {
|
|
1022
|
+
throw new Error(`Specialist ${agentId} did not expose a UI message stream.`)
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
let finalMessage: ChatMessage | null = null
|
|
1026
|
+
for await (const message of readUIMessageStream<ChatMessage>({
|
|
1027
|
+
stream: result.toUIMessageStream({
|
|
1028
|
+
generateMessageId: () => Bun.randomUUIDv7(),
|
|
1029
|
+
sendReasoning: true,
|
|
1030
|
+
sendSources: true,
|
|
1031
|
+
sendStart: false,
|
|
1032
|
+
sendFinish: false,
|
|
1033
|
+
messageMetadata: createAgentMessageMetadata({
|
|
1034
|
+
agentId,
|
|
1035
|
+
agentName: specialistConfig.displayName as string,
|
|
1036
|
+
}),
|
|
1037
|
+
}) as ReadableStream<never>,
|
|
1038
|
+
})) {
|
|
1039
|
+
finalMessage = withMessageCreatedAt(message)
|
|
1040
|
+
yield finalMessage
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (!finalMessage) {
|
|
1044
|
+
throw new Error(`Specialist ${agentId} did not produce a response message.`)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
for (const toolError of collectToolOutputErrors({ responseMessage: finalMessage })) {
|
|
1048
|
+
aiLogger.warn`Tool execution failed (agent=${agentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
memoryBlock = specialistMemoryBlock
|
|
1052
|
+
return finalMessage
|
|
1053
|
+
},
|
|
1054
|
+
toModelOutput: ({ output }) => {
|
|
1055
|
+
const result = getChatMessageFromToolOutput(output)
|
|
1056
|
+
const agentName =
|
|
1057
|
+
typeof result?.metadata?.agentName === 'string' && result.metadata.agentName.trim().length > 0
|
|
1058
|
+
? result.metadata.agentName.trim()
|
|
1059
|
+
: 'Specialist'
|
|
1060
|
+
const summary = result ? extractMessageText(result).trim() : ''
|
|
1061
|
+
return {
|
|
1062
|
+
type: 'text',
|
|
1063
|
+
value: summary ? `${agentName}: ${summary}` : `${agentName} completed the requested task.`,
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
const teamThinkTool =
|
|
1069
|
+
workstream.mode === 'group' && !onboardingActive
|
|
1070
|
+
? createTeamThinkTool({
|
|
1071
|
+
historyMessages: currentMessages,
|
|
1072
|
+
latestUserMessageId: referenceUserMessageId,
|
|
1073
|
+
orgId: orgRef,
|
|
1074
|
+
userId: userRef,
|
|
1075
|
+
workstreamId: workstreamRef,
|
|
1076
|
+
githubInstalled,
|
|
1077
|
+
availableUploads: listReadableUploads(),
|
|
1078
|
+
provideRepoTool: indexedRepoContext.provideRepoTool,
|
|
1079
|
+
defaultRepoSectionsByAgent: indexedRepoContext.defaultSectionsByAgent as never,
|
|
1080
|
+
reasoningProfile: reasoningProfile.name,
|
|
1081
|
+
systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
|
|
1082
|
+
getPreSeededMemoriesSection,
|
|
1083
|
+
retrievedKnowledgeSection,
|
|
1084
|
+
additionalInstructionSections: mergeInstructionSections(
|
|
1085
|
+
coreInstructionSections,
|
|
1086
|
+
hookInstructionSections,
|
|
1087
|
+
),
|
|
1088
|
+
getAdditionalInstructionSections: getExecutionPlanInstructionSections,
|
|
1089
|
+
context: buildContextResult,
|
|
1090
|
+
toolProviders,
|
|
1091
|
+
abortSignal: runAbort.signal,
|
|
1092
|
+
})
|
|
1093
|
+
: null
|
|
1094
|
+
|
|
1095
|
+
if (workstream.mode === 'direct') {
|
|
1096
|
+
if (!workstream.agentId) {
|
|
1097
|
+
throw new WorkstreamTurnError('Direct workstreams require an assigned agent.', 400)
|
|
1098
|
+
}
|
|
1099
|
+
await runVisibleAgent({ agentId: workstream.agentId, mode: 'direct' })
|
|
1100
|
+
} else if (params.kind === 'userTurn') {
|
|
1101
|
+
await runVisibleAgent({
|
|
1102
|
+
agentId: visibleWorkstreamAgentId ?? 'chief',
|
|
1103
|
+
mode: 'workstreamMode',
|
|
1104
|
+
skills: coreWorkstreamProfile?.skills ? [...coreWorkstreamProfile.skills] : undefined,
|
|
1105
|
+
additionalInstructionSections: mergeInstructionSections(coreInstructionSections, hookInstructionSections),
|
|
1106
|
+
extraTools: {
|
|
1107
|
+
[CONSULT_SPECIALIST_TOOL_NAME]: consultSpecialistTool,
|
|
1108
|
+
...(teamThinkTool ? { [CONSULT_TEAM_TOOL_NAME]: teamThinkTool } : {}),
|
|
1109
|
+
},
|
|
1110
|
+
})
|
|
1111
|
+
} else {
|
|
1112
|
+
await runVisibleAgent({
|
|
1113
|
+
agentId: visibleWorkstreamAgentId ?? 'chief',
|
|
1114
|
+
mode: 'workstreamMode',
|
|
1115
|
+
skills: coreWorkstreamProfile?.skills ? [...coreWorkstreamProfile.skills] : undefined,
|
|
1116
|
+
additionalInstructionSections: mergeInstructionSections(coreInstructionSections, hookInstructionSections),
|
|
1117
|
+
extraTools: {
|
|
1118
|
+
[CONSULT_SPECIALIST_TOOL_NAME]: consultSpecialistTool,
|
|
1119
|
+
...(teamThinkTool ? { [CONSULT_TEAM_TOOL_NAME]: teamThinkTool } : {}),
|
|
1120
|
+
},
|
|
1121
|
+
})
|
|
1122
|
+
}
|
|
1123
|
+
} finally {
|
|
1124
|
+
try {
|
|
1125
|
+
const latestWorkstreamRecord = await workstreamService.getById(workstreamRef)
|
|
1126
|
+
const latestPersistedState = WorkstreamStateSchema.safeParse(latestWorkstreamRecord.state).success
|
|
1127
|
+
? WorkstreamStateSchema.parse(latestWorkstreamRecord.state)
|
|
1128
|
+
: null
|
|
1129
|
+
|
|
1130
|
+
await finalizeTurnRun({
|
|
1131
|
+
serverRunId,
|
|
1132
|
+
getEntity: async () => latestWorkstreamRecord,
|
|
1133
|
+
getUncompactedMessages: (cursor) =>
|
|
1134
|
+
workstreamMessageService.listMessagesAfterCursor(workstreamRef, cursor),
|
|
1135
|
+
assessCompaction: (summaryText, messages) =>
|
|
1136
|
+
contextCompactionRuntime.shouldCompactHistory({
|
|
1137
|
+
summaryText,
|
|
1138
|
+
liveMessages: messages,
|
|
1139
|
+
contextSize: CONTEXT_SIZE,
|
|
1140
|
+
}),
|
|
1141
|
+
enqueueCompaction: () =>
|
|
1142
|
+
enqueueContextCompaction({
|
|
1143
|
+
domain: 'workstream',
|
|
1144
|
+
entityId: workstreamIdString,
|
|
1145
|
+
contextSize: CONTEXT_SIZE,
|
|
1146
|
+
}).then(() => {}),
|
|
1147
|
+
unregisterRun: (runId) => chatRunRegistry.unregister(runId),
|
|
1148
|
+
clearActiveRunId: (runId) => workstreamService.clearActiveRunIdIfMatches(workstreamRef, runId),
|
|
1149
|
+
disposeAbort: () => runAbort.dispose(),
|
|
1150
|
+
})
|
|
1151
|
+
|
|
1152
|
+
let trackerAwareWorkstreamRecord = latestWorkstreamRecord
|
|
1153
|
+
let trackerAwarePersistedState = latestPersistedState
|
|
1154
|
+
|
|
1155
|
+
if (!onboardingActive && allAssistantMessages.length > 0) {
|
|
1156
|
+
const activeExecutionPlan = await executionPlanService.getActivePlanForWorkstream(workstreamRef)
|
|
1157
|
+
const trackerUpdated = await updateWorkstreamChangeTracker({
|
|
1158
|
+
workstreamId: workstreamRef,
|
|
1159
|
+
title: latestWorkstreamRecord.title ?? workstream.title,
|
|
1160
|
+
mode: workstream.mode,
|
|
1161
|
+
...(workstream.coreType ? { coreType: workstream.coreType } : {}),
|
|
1162
|
+
...(visibleWorkstreamAgentId ? { visibleAgentId: visibleWorkstreamAgentId } : {}),
|
|
1163
|
+
hasActiveExecutionPlan: activeExecutionPlan !== null,
|
|
1164
|
+
previousSummary:
|
|
1165
|
+
typeof latestWorkstreamRecord.chatSummary === 'string' ? latestWorkstreamRecord.chatSummary : null,
|
|
1166
|
+
existingState: latestPersistedState,
|
|
1167
|
+
userMessageText: referenceUserMessage ? extractMessageText(referenceUserMessage).trim() : null,
|
|
1168
|
+
assistantMessages: allAssistantMessages
|
|
1169
|
+
.map((message) => {
|
|
1170
|
+
const text = extractTrackerMessageText(message).trim()
|
|
1171
|
+
if (!text) return null
|
|
1172
|
+
const label =
|
|
1173
|
+
typeof message.metadata?.agentName === 'string' && message.metadata.agentName.trim().length > 0
|
|
1174
|
+
? message.metadata.agentName.trim()
|
|
1175
|
+
: typeof message.metadata?.agentId === 'string' && message.metadata.agentId.trim().length > 0
|
|
1176
|
+
? message.metadata.agentId.trim()
|
|
1177
|
+
: (visibleWorkstreamAgentId ?? 'Assistant')
|
|
1178
|
+
return { label, text }
|
|
1179
|
+
})
|
|
1180
|
+
.filter((message): message is { label: string; text: string } => Boolean(message)),
|
|
1181
|
+
})
|
|
1182
|
+
|
|
1183
|
+
if (trackerUpdated) {
|
|
1184
|
+
const trackedWorkstreamRecord = await workstreamService.getWorkstreamRecord(workstreamRef)
|
|
1185
|
+
trackerAwareWorkstreamRecord = trackedWorkstreamRecord
|
|
1186
|
+
trackerAwarePersistedState = parsePersistedWorkstreamState(trackedWorkstreamRecord.state)
|
|
1187
|
+
if (writer) {
|
|
1188
|
+
writer.write({
|
|
1189
|
+
type: 'data-workstreamTracker',
|
|
1190
|
+
data: workstreamService.toPublicWorkstreamDetail(trackedWorkstreamRecord)
|
|
1191
|
+
.workstreamState as unknown as Record<string, unknown>,
|
|
1192
|
+
transient: true,
|
|
1193
|
+
})
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (allAssistantMessages.length > 0 && shouldProcessPostRunSideEffects) {
|
|
1199
|
+
const agentMessages = buildAgentHistoryMessages(allAssistantMessages)
|
|
1200
|
+
const historyMessagesForMemory = appendCompactionContextToHistoryMessages(
|
|
1201
|
+
toHistoryMessages(recentHistory),
|
|
1202
|
+
{ chatSummary: trackerAwareWorkstreamRecord.chatSummary, persistedState: trackerAwarePersistedState },
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
const userMessageText = referenceUserMessage ? extractMessageText(referenceUserMessage).trim() : ''
|
|
1206
|
+
const readableUploads = listReadableUploads()
|
|
1207
|
+
const attachmentMetadataContext = buildReadableUploadMetadataContext(readableUploads)
|
|
1208
|
+
const hasAttachmentContext = Boolean(attachmentMetadataContext)
|
|
1209
|
+
if (
|
|
1210
|
+
shouldEnqueueOnboardingPostChatMemory({
|
|
1211
|
+
onboardingActive,
|
|
1212
|
+
userMessageText,
|
|
1213
|
+
hasAttachmentContext,
|
|
1214
|
+
agentMessageCount: agentMessages.length,
|
|
1215
|
+
})
|
|
1216
|
+
) {
|
|
1217
|
+
const memoryUserMessage = userMessageText || 'User uploaded attachment(s).'
|
|
1218
|
+
await safeEnqueue(
|
|
1219
|
+
() =>
|
|
1220
|
+
enqueuePostChatMemory({
|
|
1221
|
+
orgId: orgIdString,
|
|
1222
|
+
workstreamId: workstreamIdString,
|
|
1223
|
+
sourceId: referenceUserMessageId,
|
|
1224
|
+
onboardStatus: readOptionalString((workspace as { onboardStatus?: unknown }).onboardStatus),
|
|
1225
|
+
userMessage: memoryUserMessage,
|
|
1226
|
+
historyMessages: historyMessagesForMemory,
|
|
1227
|
+
agentMessages,
|
|
1228
|
+
memoryBlock: memoryBlock.trim() ? memoryBlock : undefined,
|
|
1229
|
+
attachmentContext: attachmentMetadataContext,
|
|
1230
|
+
}),
|
|
1231
|
+
{ operationName: 'post-chat memory extraction enqueue' },
|
|
1232
|
+
)
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (params.kind === 'userTurn' && referenceUserMessage) {
|
|
1236
|
+
const conversationSummary = buildConversationSummary({
|
|
1237
|
+
userMessageText,
|
|
1238
|
+
assistantMessages: allAssistantMessages,
|
|
1239
|
+
})
|
|
1240
|
+
if (conversationSummary) {
|
|
1241
|
+
const recentActivityResult = await recentActivityService.recordEvent({
|
|
1242
|
+
orgId: orgRef,
|
|
1243
|
+
userId: userRef,
|
|
1244
|
+
source: 'system',
|
|
1245
|
+
event: {
|
|
1246
|
+
sourceEventId: `chat-turn:${referenceUserMessageId}`,
|
|
1247
|
+
kind: 'chat.turn.completed',
|
|
1248
|
+
targetKind: 'workstream',
|
|
1249
|
+
targetId: workstreamIdString,
|
|
1250
|
+
mergeKey: `workstream:${workstreamIdString}`,
|
|
1251
|
+
title: buildRecentActivityChatSystemTitle({
|
|
1252
|
+
workstream,
|
|
1253
|
+
visibleAgentId: visibleWorkstreamAgentId ?? 'chief',
|
|
1254
|
+
}),
|
|
1255
|
+
sourceLabel: agentDisplayNames[visibleWorkstreamAgentId ?? 'chief'],
|
|
1256
|
+
deepLink: buildRecentActivityChatDeepLink({
|
|
1257
|
+
workstream,
|
|
1258
|
+
workstreamId: workstreamIdString,
|
|
1259
|
+
visibleAgentId: visibleWorkstreamAgentId ?? 'chief',
|
|
1260
|
+
}),
|
|
1261
|
+
metadata: {
|
|
1262
|
+
agentId: visibleWorkstreamAgentId ?? 'chief',
|
|
1263
|
+
agentName: agentDisplayNames[visibleWorkstreamAgentId ?? 'chief'],
|
|
1264
|
+
workstreamId: workstreamIdString,
|
|
1265
|
+
workstreamTitle: trackerAwareWorkstreamRecord.title ?? workstream.title,
|
|
1266
|
+
workstreamMode: workstream.mode,
|
|
1267
|
+
...(workstream.coreType ? { coreType: workstream.coreType } : {}),
|
|
1268
|
+
userMessageText,
|
|
1269
|
+
assistantSummary: conversationSummary,
|
|
1270
|
+
messageId: referenceUserMessageId,
|
|
1271
|
+
},
|
|
1272
|
+
occurredAt: toIsoDateTimeString(referenceUserMessage.metadata?.createdAt ?? Date.now()),
|
|
1273
|
+
},
|
|
1274
|
+
})
|
|
1275
|
+
|
|
1276
|
+
await safeEnqueue(
|
|
1277
|
+
async () => {
|
|
1278
|
+
const enqueuePostChatOrgAction = getRuntimeAdapters().queues?.enqueuePostChatOrgAction
|
|
1279
|
+
if (!enqueuePostChatOrgAction) {
|
|
1280
|
+
return
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
await enqueuePostChatOrgAction({
|
|
1284
|
+
orgId: orgIdString,
|
|
1285
|
+
workstreamId: workstreamIdString,
|
|
1286
|
+
sourceId: referenceUserMessageId,
|
|
1287
|
+
sourceCreatedAt: referenceUserMessage.metadata?.createdAt ?? Date.now(),
|
|
1288
|
+
conversationSummary,
|
|
1289
|
+
})
|
|
1290
|
+
},
|
|
1291
|
+
{ operationName: 'post-chat org action enqueue' },
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
if (recentActivityService.isMeaningfulRefinementCandidate(recentActivityResult.item)) {
|
|
1295
|
+
await safeEnqueue(
|
|
1296
|
+
() => enqueueRecentActivityTitleRefinement({ activityId: recentActivityResult.item.id }),
|
|
1297
|
+
{ operationName: 'recent activity title refinement enqueue' },
|
|
1298
|
+
)
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (shouldEnqueueRegularDigestForWorkstream({ onboardingActive })) {
|
|
1304
|
+
await safeEnqueue(() => enqueueRegularChatMemoryDigest({ orgId: orgIdString }), {
|
|
1305
|
+
operationName: 'regular chat memory digest enqueue',
|
|
1306
|
+
})
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (shouldEnqueueSkillExtraction({ onboardingActive })) {
|
|
1310
|
+
await safeEnqueue(() => enqueueSkillExtraction({ orgId: orgIdString }), {
|
|
1311
|
+
operationName: 'skill extraction enqueue',
|
|
1312
|
+
})
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (allAssistantMessages.length > 0) {
|
|
1317
|
+
await turnHooks.afterTurn?.({
|
|
1318
|
+
workstream,
|
|
1319
|
+
workstreamRef,
|
|
1320
|
+
orgRef,
|
|
1321
|
+
userRef,
|
|
1322
|
+
userName,
|
|
1323
|
+
onboardingActive,
|
|
1324
|
+
referenceUserMessage,
|
|
1325
|
+
assistantMessages: allAssistantMessages,
|
|
1326
|
+
latestWorkstreamRecord: trackerAwareWorkstreamRecord,
|
|
1327
|
+
latestPersistedState: trackerAwarePersistedState,
|
|
1328
|
+
context: buildContextResult,
|
|
1329
|
+
})
|
|
1330
|
+
}
|
|
1331
|
+
} catch (postRunError) {
|
|
1332
|
+
aiLogger.error`Workstream post-run cleanup failed: ${postRunError}`
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
await executeRun()
|
|
1338
|
+
},
|
|
1339
|
+
}
|
|
1340
|
+
}
|