@lota-sdk/core 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/package.json +2 -2
  2. package/src/ai/definitions.ts +5 -59
  3. package/src/ai-gateway/ai-gateway.ts +36 -28
  4. package/src/ai-gateway/cache-headers.ts +9 -0
  5. package/src/config/model-constants.ts +6 -2
  6. package/src/create-runtime.ts +1 -17
  7. package/src/db/memory-types.ts +13 -8
  8. package/src/db/memory.ts +74 -53
  9. package/src/queues/autonomous-job.queue.ts +1 -8
  10. package/src/queues/context-compaction.queue.ts +2 -2
  11. package/src/queues/index.ts +2 -6
  12. package/src/queues/organization-learning.queue.ts +78 -0
  13. package/src/queues/plan-agent-heartbeat.queue.ts +10 -16
  14. package/src/queues/title-generation.queue.ts +62 -0
  15. package/src/runtime/agent-prompt-context.ts +0 -18
  16. package/src/runtime/agent-runtime-policy.ts +9 -2
  17. package/src/runtime/context-compaction-constants.ts +4 -2
  18. package/src/runtime/context-compaction.ts +135 -118
  19. package/src/runtime/memory-pipeline.ts +70 -1
  20. package/src/runtime/memory-prompts-fact.ts +16 -0
  21. package/src/runtime/plugin-resolution.ts +3 -2
  22. package/src/runtime/plugin-types.ts +1 -42
  23. package/src/runtime/post-turn-side-effects.ts +212 -0
  24. package/src/runtime/runtime-config.ts +0 -13
  25. package/src/runtime/runtime-extensions.ts +10 -16
  26. package/src/runtime/runtime-worker-registry.ts +8 -19
  27. package/src/runtime/social-chat-agent-runner.ts +119 -0
  28. package/src/runtime/social-chat-history.ts +110 -0
  29. package/src/runtime/social-chat-prompts.ts +58 -0
  30. package/src/runtime/social-chat.ts +104 -340
  31. package/src/runtime/specialist-runner.ts +18 -0
  32. package/src/runtime/workstream-chat-helpers.ts +19 -0
  33. package/src/runtime/workstream-plan-turn.ts +195 -0
  34. package/src/runtime/workstream-state.ts +11 -8
  35. package/src/runtime/workstream-turn-context.ts +183 -0
  36. package/src/services/autonomous-job.service.ts +1 -8
  37. package/src/services/execution-plan.service.ts +205 -334
  38. package/src/services/index.ts +1 -4
  39. package/src/services/memory.service.ts +54 -44
  40. package/src/services/ownership-dispatcher.service.ts +2 -19
  41. package/src/services/plan-completion-side-effects.ts +80 -0
  42. package/src/services/plan-event-delivery.service.ts +1 -1
  43. package/src/services/plan-executor.service.ts +42 -190
  44. package/src/services/plan-node-spec.ts +60 -0
  45. package/src/services/plan-run-data.ts +88 -0
  46. package/src/services/plan-validator.service.ts +10 -8
  47. package/src/services/workstream-constants.ts +2 -0
  48. package/src/services/workstream-title.service.ts +1 -1
  49. package/src/services/workstream-turn-preparation.service.ts +208 -715
  50. package/src/services/workstream.service.ts +162 -192
  51. package/src/services/workstream.types.ts +12 -44
  52. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -0
  53. package/src/tools/execution-plan.tool.ts +7 -6
  54. package/src/tools/remember-memory.tool.ts +7 -10
  55. package/src/tools/research-topic.tool.ts +1 -1
  56. package/src/tools/team-think.tool.ts +1 -1
  57. package/src/tools/user-questions.tool.ts +1 -1
  58. package/src/utils/autonomous-job-ids.ts +7 -0
  59. package/src/workers/organization-learning.worker.ts +31 -0
  60. package/src/workers/regular-chat-memory-digest.runner.ts +9 -3
  61. package/src/workers/skill-extraction.runner.ts +2 -2
  62. package/src/queues/recent-activity-title-refinement.queue.ts +0 -30
  63. package/src/queues/regular-chat-memory-digest.config.ts +0 -12
  64. package/src/queues/regular-chat-memory-digest.queue.ts +0 -34
  65. package/src/queues/skill-extraction.config.ts +0 -9
  66. package/src/queues/skill-extraction.queue.ts +0 -27
  67. package/src/queues/workstream-title-generation.queue.ts +0 -33
  68. package/src/services/context-enrichment.service.ts +0 -33
  69. package/src/services/coordination-registry.service.ts +0 -117
  70. package/src/services/domain-agent-executor.service.ts +0 -71
  71. package/src/services/memory-assessment.service.ts +0 -44
  72. package/src/services/playbook-registry.service.ts +0 -67
  73. package/src/workers/regular-chat-memory-digest.worker.ts +0 -22
  74. package/src/workers/skill-extraction.worker.ts +0 -22
@@ -11,17 +11,9 @@ import {
11
11
  toTimestamp,
12
12
  withMessageCreatedAt,
13
13
  } from '@lota-sdk/shared'
14
- import type {
15
- ChatMessage,
16
- MessageMetadata,
17
- PlanArtifactSubmission,
18
- PlanNodeHandoffContext,
19
- PlanNodeRunRecord,
20
- PlanNodeSpecRecord,
21
- } from '@lota-sdk/shared'
22
- import { convertToModelMessages, readUIMessageStream, stepCountIs, tool as createTool, validateUIMessages } from 'ai'
14
+ import type { ChatMessage, ConsultSpecialistArgs, MessageMetadata, PlanNodeSpecRecord } from '@lota-sdk/shared'
15
+ import { convertToModelMessages, stepCountIs, tool as createTool, validateUIMessages } from 'ai'
23
16
  import type { PrepareStepFunction, StopCondition, ToolLoopAgent, ToolSet, UIMessageStreamWriter } from 'ai'
24
- import type { z } from 'zod'
25
17
 
26
18
  import type { CoreWorkstreamProfile } from '../config/agent-defaults'
27
19
  import {
@@ -38,17 +30,8 @@ import type { RecordIdRef } from '../db/record-id'
38
30
  import { recordIdToString } from '../db/record-id'
39
31
  import { TABLES } from '../db/tables'
40
32
  import { enqueueContextCompaction } from '../queues/context-compaction.queue'
41
- import { enqueueMemoryConsolidation } from '../queues/memory-consolidation.queue'
42
- import { enqueuePostChatMemory } from '../queues/post-chat-memory.queue'
43
- import { enqueueRecentActivityTitleRefinement } from '../queues/recent-activity-title-refinement.queue'
44
- import { enqueueRegularChatMemoryDigest } from '../queues/regular-chat-memory-digest.queue'
45
- import { enqueueSkillExtraction } from '../queues/skill-extraction.queue'
46
- import { enqueueWorkstreamTitleGeneration } from '../queues/workstream-title-generation.queue'
47
- import { buildAgentPromptContext } from '../runtime/agent-prompt-context'
48
- import {
49
- OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES,
50
- buildCompletionCheckStructuredOutputHints,
51
- } from '../runtime/agent-runtime-policy'
33
+ import { enqueueWorkstreamTitleGeneration } from '../queues/title-generation.queue'
34
+ import { OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES } from '../runtime/agent-runtime-policy'
52
35
  import {
53
36
  buildSpecialistTaskMessage,
54
37
  createAgentMessageMetadata,
@@ -62,32 +45,30 @@ import { parseWorkstreamState } from '../runtime/context-compaction'
62
45
  import { CONTEXT_WINDOW_TOKENS } from '../runtime/context-compaction-constants'
63
46
  import { createExecutionPlanInstructionSectionCache } from '../runtime/execution-plan'
64
47
  import { mergeInstructionSections } from '../runtime/instruction-sections'
65
- import {
66
- shouldEnqueueMemoryConsolidation,
67
- shouldEnqueueMemoryExtraction,
68
- shouldEnqueueOnboardingPostChatMemory,
69
- shouldEnqueueRegularDigestForWorkstream,
70
- } from '../runtime/memory-digest-policy'
71
- import { buildIndexedRepositoriesContext, getPluginService } from '../runtime/plugin-resolution'
48
+ import { runPostTurnSideEffects } from '../runtime/post-turn-side-effects'
72
49
  import { getRuntimeAdapters, getToolProviders, getTurnHooks } from '../runtime/runtime-extensions'
73
- import { shouldEnqueueSkillExtraction } from '../runtime/skill-extraction-policy'
50
+ import { runSpecialistSession } from '../runtime/specialist-runner'
74
51
  import { finalizeTurnRun } from '../runtime/turn-lifecycle'
75
52
  import {
76
- appendPersistedWorkstreamContextToHistoryMessages,
77
- buildAgentHistoryMessages,
78
- buildConversationSummary,
79
- buildReadableUploadMetadataContext,
53
+ asRecord,
80
54
  collectToolOutputErrors,
81
55
  extractMessageText,
82
- toHistoryMessages,
56
+ readInstructionSections,
57
+ readOptionalString,
83
58
  toOptionalTrimmedString,
84
59
  } from '../runtime/workstream-chat-helpers'
60
+ import {
61
+ buildPlanTurnInstructionSections,
62
+ buildPlanTurnPromptMessage,
63
+ buildPlanTurnSubmitToolDescription,
64
+ } from '../runtime/workstream-plan-turn'
65
+ import type { WorkstreamPlanTurnContext } from '../runtime/workstream-plan-turn'
85
66
  import type { WorkstreamState } from '../runtime/workstream-state'
67
+ import { assembleWorkstreamTurnContext } from '../runtime/workstream-turn-context'
86
68
  import { chatRunRegistry } from '../services/chat-run-registry.service'
87
69
  import type { NormalizedWorkstream, WorkstreamRecord } from '../services/workstream.types'
88
70
  import { createTeamThinkTool } from '../tools/team-think.tool'
89
71
  import { safeEnqueue } from '../utils/async'
90
- import { toIsoDateTimeString } from '../utils/date-time'
91
72
  import { AppError } from '../utils/errors'
92
73
  import { attachmentService } from './attachment.service'
93
74
  import { listReadableUploadsFromChatMessages } from './chat-attachments.service'
@@ -96,7 +77,6 @@ import { executionPlanService } from './execution-plan.service'
96
77
  import { learnedSkillService } from './learned-skill.service'
97
78
  import { memoryService } from './memory.service'
98
79
  import { planRunService } from './plan-run.service'
99
- import { recentActivityService } from './recent-activity.service'
100
80
  import { workstreamMessageService } from './workstream-message.service'
101
81
  import { ActiveWorkstreamRunConflictError, workstreamService } from './workstream.service'
102
82
 
@@ -168,205 +148,11 @@ class WorkstreamTurnError extends AppError {
168
148
  }
169
149
  }
170
150
 
171
- function asRecord(value: unknown): Record<string, unknown> | null {
172
- return value && typeof value === 'object' ? (value as Record<string, unknown>) : null
173
- }
174
-
175
- function readOptionalString(value: unknown): string | undefined {
176
- return typeof value === 'string' && value.trim().length > 0 ? value : undefined
177
- }
178
-
179
- function readOptionalBoolean(value: unknown): boolean | undefined {
180
- return typeof value === 'boolean' ? value : undefined
181
- }
182
-
183
- function readInstructionSections(value: unknown): string[] {
184
- if (!Array.isArray(value)) {
185
- return []
186
- }
187
-
188
- return value
189
- .filter((section): section is string => typeof section === 'string')
190
- .map((section) => section.trim())
191
- .filter((section) => section.length > 0)
192
- }
193
-
194
151
  function optionalInstructionSection(value: unknown): string[] | undefined {
195
152
  const section = readOptionalString(value)
196
153
  return section ? [section] : undefined
197
154
  }
198
155
 
199
- export interface PlanTurnUpstreamHandoff {
200
- nodeId: string
201
- label: string
202
- ownerRef: string
203
- ownerType: PlanNodeSpecRecord['owner']['executorType']
204
- handoffContext: PlanNodeHandoffContext
205
- }
206
-
207
- export interface WorkstreamPlanTurnContext {
208
- runId: string
209
- nodeId: string
210
- planTitle: string
211
- nodeSpec: PlanNodeSpecRecord
212
- nodeRun: PlanNodeRunRecord
213
- resolvedInput: Record<string, unknown>
214
- inputArtifacts: PlanArtifactSubmission[]
215
- upstreamHandoffs: PlanTurnUpstreamHandoff[]
216
- }
217
-
218
- function buildPlanTurnExecutionSection(planTurn: WorkstreamPlanTurnContext): string {
219
- const payload = {
220
- runId: planTurn.runId,
221
- planTitle: planTurn.planTitle,
222
- node: {
223
- id: planTurn.nodeSpec.nodeId,
224
- label: planTurn.nodeSpec.label,
225
- owner: planTurn.nodeSpec.owner,
226
- objective: planTurn.nodeSpec.objective,
227
- instructions: planTurn.nodeSpec.instructions,
228
- outputSchemaRef: planTurn.nodeSpec.outputSchemaRef ?? null,
229
- deliverables: planTurn.nodeSpec.deliverables,
230
- successCriteria: planTurn.nodeSpec.successCriteria,
231
- completionChecks: planTurn.nodeSpec.completionChecks,
232
- toolPolicy: planTurn.nodeSpec.toolPolicy,
233
- contextPolicy: planTurn.nodeSpec.contextPolicy,
234
- },
235
- resolvedInput: planTurn.resolvedInput,
236
- inputArtifacts: planTurn.inputArtifacts,
237
- }
238
-
239
- return [
240
- '<plan-turn-execution>',
241
- 'The runtime has activated a visible execution-plan node inside this workstream.',
242
- `Complete node "${planTurn.nodeSpec.label}" for plan "${planTurn.planTitle}".`,
243
- 'Use only the node contract, resolved input, input artifacts, and upstream handoff context provided here.',
244
- 'Do not ask the user for more input and do not rely on unstated external context.',
245
- 'Do not submit placeholders, partial work, or speculative outputs.',
246
- 'Before submitting, satisfy every required deliverable, success criterion, and completion check for this node.',
247
- 'Deliverables must use the exact artifact names and kinds declared in the node contract.',
248
- 'If a deliverable declares schemaRef, include the same schemaRef and a payload that satisfies that schema.',
249
- 'If outputSchemaRef is declared, structuredOutput must satisfy that schema before you submit.',
250
- `When finished, call ${SUBMIT_PLAN_TURN_RESULT_TOOL_NAME} exactly once.`,
251
- 'Always include durable handoffContext for downstream nodes when you submit the final result.',
252
- 'Do not ask the user for confirmation and do not create or replace execution plans in this turn.',
253
- JSON.stringify(payload, null, 2),
254
- '</plan-turn-execution>',
255
- ].join('\n')
256
- }
257
-
258
- function describePlanTurnDeliverable(deliverable: PlanNodeSpecRecord['deliverables'][number]): string {
259
- return [
260
- `- ${deliverable.name}`,
261
- `kind=${deliverable.kind}`,
262
- deliverable.required ? 'required' : 'optional',
263
- deliverable.schemaRef ? `schemaRef=${deliverable.schemaRef}` : undefined,
264
- deliverable.description ? `description=${deliverable.description}` : undefined,
265
- ]
266
- .filter(Boolean)
267
- .join(' | ')
268
- }
269
-
270
- function describePlanTurnCompletionCheck(check: PlanNodeSpecRecord['completionChecks'][number]): string {
271
- return [
272
- `- ${check.description}`,
273
- `type=${check.type}`,
274
- check.blocking ? 'blocking' : 'warning',
275
- Object.keys(check.config).length > 0 ? `config=${JSON.stringify(check.config)}` : undefined,
276
- ]
277
- .filter(Boolean)
278
- .join(' | ')
279
- }
280
-
281
- function buildPlanTurnResultContractSection(planTurn: WorkstreamPlanTurnContext): string {
282
- const requiredDeliverables = planTurn.nodeSpec.deliverables.filter((deliverable) => deliverable.required)
283
- const completionCheckOutputHints = buildCompletionCheckStructuredOutputHints(planTurn.nodeSpec)
284
- const deliverableLines =
285
- planTurn.nodeSpec.deliverables.length > 0
286
- ? planTurn.nodeSpec.deliverables.map(describePlanTurnDeliverable)
287
- : ['- none']
288
- const completionCheckLines =
289
- planTurn.nodeSpec.completionChecks.length > 0
290
- ? planTurn.nodeSpec.completionChecks.map(describePlanTurnCompletionCheck)
291
- : ['- none']
292
-
293
- return [
294
- '<plan-turn-result-contract>',
295
- `Call ${SUBMIT_PLAN_TURN_RESULT_TOOL_NAME} exactly once with a result object that passes node validation.`,
296
- 'Validation is strict. Missing required artifacts, schema mismatches, or failed completion checks will fail the node run.',
297
- `Required artifacts: ${requiredDeliverables.length > 0 ? requiredDeliverables.map((deliverable) => deliverable.name).join(', ') : 'none'}`,
298
- `Structured output: ${
299
- planTurn.nodeSpec.outputSchemaRef
300
- ? `required and must match schema "${planTurn.nodeSpec.outputSchemaRef}"`
301
- : 'optional unless needed by a completion check'
302
- }`,
303
- 'Deliverables:',
304
- ...deliverableLines,
305
- 'Completion checks:',
306
- ...completionCheckLines,
307
- ...(completionCheckOutputHints.length > 0
308
- ? ['Structured output fields required by completion checks:', ...completionCheckOutputHints]
309
- : []),
310
- 'Include notes with a concise completion summary grounded in the submitted artifacts and structuredOutput.',
311
- 'Always include handoffContext for downstream execution with a durable summary, key decisions, open questions, risks, recommendations, and references when relevant.',
312
- '</plan-turn-result-contract>',
313
- ].join('\n')
314
- }
315
-
316
- function buildPlanTurnSubmitToolDescription(planTurn: WorkstreamPlanTurnContext): string {
317
- const requiredArtifacts =
318
- planTurn.nodeSpec.deliverables
319
- .filter((deliverable) => deliverable.required)
320
- .map((deliverable) => `${deliverable.name} (${deliverable.kind})`)
321
- .join(', ') || 'none'
322
-
323
- return [
324
- 'Submit the final result for the active plan-triggered node turn.',
325
- 'Call this exactly once when the node output is complete.',
326
- `Required artifacts: ${requiredArtifacts}.`,
327
- `Structured output: ${
328
- planTurn.nodeSpec.outputSchemaRef ? `must satisfy ${planTurn.nodeSpec.outputSchemaRef}` : 'optional'
329
- }.`,
330
- 'Do not submit partial results. Include durable handoffContext for downstream nodes.',
331
- ].join(' ')
332
- }
333
-
334
- function buildPlanTurnPromptMessage(planTurn: WorkstreamPlanTurnContext): ChatMessage {
335
- return {
336
- id: Bun.randomUUIDv7(),
337
- role: 'user',
338
- parts: [
339
- {
340
- type: 'text',
341
- text: `Execute the active plan node "${planTurn.nodeSpec.label}" now and submit the result with ${SUBMIT_PLAN_TURN_RESULT_TOOL_NAME}.`,
342
- },
343
- ],
344
- metadata: { trigger: 'plan-turn', planRunId: planTurn.runId, planNodeId: planTurn.nodeId, createdAt: Date.now() },
345
- }
346
- }
347
-
348
- function buildUpstreamHandoffSection(upstreamHandoffs: PlanTurnUpstreamHandoff[]): string | undefined {
349
- if (upstreamHandoffs.length === 0) {
350
- return undefined
351
- }
352
-
353
- return [
354
- '<upstream-handoff>',
355
- JSON.stringify(
356
- upstreamHandoffs.map((handoff) => ({
357
- nodeId: handoff.nodeId,
358
- label: handoff.label,
359
- ownerRef: handoff.ownerRef,
360
- ownerType: handoff.ownerType,
361
- handoffContext: handoff.handoffContext,
362
- })),
363
- null,
364
- 2,
365
- ),
366
- '</upstream-handoff>',
367
- ].join('\n')
368
- }
369
-
370
156
  function applyPlanTurnToolPolicy(tools: ToolSet, nodeSpec: PlanNodeSpecRecord): ToolSet {
371
157
  const blockedToolNames = new Set([...OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES, ...nodeSpec.toolPolicy.deny])
372
158
  const allowList = nodeSpec.toolPolicy.allow.length > 0 ? new Set(nodeSpec.toolPolicy.allow) : null
@@ -452,29 +238,6 @@ function upsertChatHistoryMessage(messages: ChatMessage[], nextMessage: ChatMess
452
238
  return nextMessages
453
239
  }
454
240
 
455
- function buildRecentActivityChatDeepLink(params: {
456
- workstream: NormalizedWorkstream
457
- workstreamId: string
458
- visibleAgentId: string
459
- }): { route: string; search: Record<string, string> } {
460
- if (params.workstream.mode === 'direct') {
461
- return { route: 'direct-workstream', search: { workstreamId: params.workstreamId, agentId: params.visibleAgentId } }
462
- }
463
-
464
- return { route: 'group-workstream', search: { workstreamId: params.workstreamId } }
465
- }
466
-
467
- function buildRecentActivityChatSystemTitle(params: {
468
- workstream: NormalizedWorkstream
469
- visibleAgentId: string
470
- }): string {
471
- if (params.workstream.mode === 'direct') {
472
- return `Conversation with ${agentDisplayNames[params.visibleAgentId]}`
473
- }
474
-
475
- return params.workstream.title.trim() || 'Workstream update'
476
- }
477
-
478
241
  interface StreamAgentResponseContext {
479
242
  turnHooks: ReturnType<typeof getTurnHooks>
480
243
  workstream: NormalizedWorkstream
@@ -489,7 +252,7 @@ interface StreamAgentResponseContext {
489
252
  getExecutionPlanInstructionSections: () => Promise<string[] | undefined>
490
253
  getPreSeededMemoriesSection: (agentId: string) => Promise<string | undefined>
491
254
  getWorkstreamStateSection: () => Promise<string | undefined>
492
- getLearnedSkillsSection: (agentId: string) => Promise<string | undefined>
255
+ getLearnedSkillsSection: (agentId: string, queryText?: string) => Promise<string | undefined>
493
256
  promptContext: { systemWorkspaceDetails?: string }
494
257
  retrievedKnowledgeSection: string | undefined
495
258
  memoryBlock: string
@@ -509,6 +272,7 @@ interface StreamAgentResponseParams {
509
272
  }
510
273
  skills?: string[]
511
274
  additionalInstructionSections?: string[]
275
+ includeExecutionPlanTools?: boolean
512
276
  writer?: UIMessageStreamWriter<ChatMessage>
513
277
  stopWhen?: StopCondition<ToolSet> | Array<StopCondition<ToolSet>>
514
278
  prepareStep?: PrepareStepFunction<ToolSet>
@@ -541,12 +305,27 @@ async function streamAgentResponse(
541
305
  )
542
306
  agentTimer.step('hook:resolveAgent')
543
307
  const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? streamParams.agentId
308
+ const latestUserMessage = [...streamParams.messages].reverse().find((message) => message.role === 'user')
309
+ const latestUserMessageText = latestUserMessage ? extractMessageText(latestUserMessage).trim() : undefined
544
310
  const [preSeededMemoriesSection, workstreamStateSection, learnedSkillsSection] = await Promise.all([
545
311
  ctx.getPreSeededMemoriesSection(resolvedAgentId),
546
312
  ctx.getWorkstreamStateSection(),
547
- ctx.getLearnedSkillsSection(resolvedAgentId),
313
+ ctx.getLearnedSkillsSection(resolvedAgentId, latestUserMessageText),
548
314
  ])
549
315
  agentTimer.step('parallel-fetch(memories+state+skills)')
316
+ const toolNames = new Set(Object.keys(streamParams.tools))
317
+ const hasRetrievalTools = [
318
+ 'memorySearch',
319
+ 'conversationSearch',
320
+ 'queryKnowledge',
321
+ 'researchTopic',
322
+ 'fetchWebpage',
323
+ 'inspectWebsite',
324
+ ].some((toolName) => toolNames.has(toolName))
325
+ const hasDomainRoutingSkills =
326
+ (streamParams.skills ?? []).some((skill) => skill.startsWith('cpo-') || skill.startsWith('mentor-')) ||
327
+ resolvedAgentId === 'cpo' ||
328
+ resolvedAgentId === 'mentor'
550
329
  const config = getAgentRuntimeConfig({
551
330
  agentId: resolvedAgentId,
552
331
  workstreamMode: ctx.workstream.mode,
@@ -560,6 +339,12 @@ async function streamAgentResponse(
560
339
  workstreamMemoryBlock: ctx.memoryBlock,
561
340
  workstreamStateSection,
562
341
  learnedSkillsSection,
342
+ userMessageText: latestUserMessageText,
343
+ ruleOptions: {
344
+ includeExecutionPlanRule: streamParams.includeExecutionPlanTools,
345
+ includeMemr3Rule: hasRetrievalTools,
346
+ includeDomainReasoningFallbackRule: hasDomainRoutingSkills,
347
+ },
563
348
  additionalInstructionSections: mergeInstructionSections(
564
349
  executionPlanInstructionSections,
565
350
  streamParams.additionalInstructionSections,
@@ -651,165 +436,12 @@ async function streamAgentResponse(
651
436
  return responseMessage
652
437
  }
653
438
 
654
- interface PostTurnSideEffectsParams {
655
- workstream: NormalizedWorkstream
656
- workstreamRef: RecordIdRef
657
- orgRef: RecordIdRef
658
- userRef: RecordIdRef
659
- userName?: string | null
660
- orgIdString: string
661
- workstreamIdString: string
662
- onboardingActive: boolean
663
- workspace: unknown
664
- allAssistantMessages: ChatMessage[]
665
- referenceUserMessage: ChatMessage | undefined
666
- referenceUserMessageId: string
667
- recentHistory: ChatMessage[]
668
- listReadableUploads: () => ReturnType<typeof listReadableUploadsFromChatMessages>
669
- memoryBlock: string
670
- visibleWorkstreamAgentId: string | null | undefined
671
- defaultLeadAgentId: string
672
- latestWorkstreamRecord: WorkstreamRecord
673
- latestPersistedState: WorkstreamState | null
674
- turnHooks: ReturnType<typeof getTurnHooks>
675
- buildContextResult: Record<string, unknown> | null
676
- isUserTurn: boolean
677
- }
678
-
679
- async function runPostTurnSideEffects(params: PostTurnSideEffectsParams): Promise<void> {
680
- const turnCount = await workstreamService.incrementTurnCount(params.workstreamRef)
681
- const agentMessages = buildAgentHistoryMessages(params.allAssistantMessages)
682
- const historyMessagesForMemory = appendPersistedWorkstreamContextToHistoryMessages(
683
- toHistoryMessages(params.recentHistory),
684
- { compactionSummary: params.latestWorkstreamRecord.compactionSummary, persistedState: params.latestPersistedState },
685
- )
686
-
687
- const userMessageText = params.referenceUserMessage ? extractMessageText(params.referenceUserMessage).trim() : ''
688
- const readableUploads = params.listReadableUploads()
689
- const attachmentMetadataContext = buildReadableUploadMetadataContext(readableUploads)
690
- const hasAttachmentContext = Boolean(attachmentMetadataContext)
691
- const shouldExtractMemory = params.onboardingActive
692
- ? shouldEnqueueOnboardingPostChatMemory({
693
- onboardingActive: params.onboardingActive,
694
- userMessageText,
695
- hasAttachmentContext,
696
- agentMessageCount: agentMessages.length,
697
- })
698
- : shouldEnqueueMemoryExtraction({ onboardingActive: params.onboardingActive, turnCount }) &&
699
- userMessageText.length > 0
700
-
701
- if (shouldExtractMemory) {
702
- const memoryUserMessage = userMessageText || 'User uploaded attachment(s).'
703
- await safeEnqueue(
704
- () =>
705
- enqueuePostChatMemory({
706
- orgId: params.orgIdString,
707
- workstreamId: params.workstreamIdString,
708
- sourceId: params.referenceUserMessageId,
709
- onboardStatus: readOptionalString((params.workspace as { onboardStatus?: unknown }).onboardStatus),
710
- userMessage: memoryUserMessage,
711
- historyMessages: historyMessagesForMemory,
712
- agentMessages,
713
- memoryBlock: params.memoryBlock.trim() ? params.memoryBlock : undefined,
714
- attachmentContext: attachmentMetadataContext,
715
- }),
716
- { operationName: 'post-chat memory extraction enqueue' },
717
- )
718
- }
719
-
720
- if (params.isUserTurn && params.referenceUserMessage) {
721
- const conversationSummary = buildConversationSummary({
722
- userMessageText,
723
- assistantMessages: params.allAssistantMessages,
724
- })
725
- if (conversationSummary) {
726
- const effectiveAgentId = params.visibleWorkstreamAgentId ?? params.defaultLeadAgentId
727
- const recentActivityResult = await recentActivityService.recordEvent({
728
- orgId: params.orgRef,
729
- userId: params.userRef,
730
- source: 'system',
731
- event: {
732
- sourceEventId: `chat-turn:${params.referenceUserMessageId}`,
733
- kind: 'chat.turn.completed',
734
- targetKind: 'workstream',
735
- targetId: params.workstreamIdString,
736
- mergeKey: `workstream:${params.workstreamIdString}`,
737
- title: buildRecentActivityChatSystemTitle({
738
- workstream: params.workstream,
739
- visibleAgentId: effectiveAgentId,
740
- }),
741
- sourceLabel: agentDisplayNames[effectiveAgentId],
742
- deepLink: buildRecentActivityChatDeepLink({
743
- workstream: params.workstream,
744
- workstreamId: params.workstreamIdString,
745
- visibleAgentId: effectiveAgentId,
746
- }),
747
- metadata: {
748
- agentId: effectiveAgentId,
749
- agentName: agentDisplayNames[effectiveAgentId],
750
- workstreamId: params.workstreamIdString,
751
- workstreamTitle: params.latestWorkstreamRecord.title ?? params.workstream.title,
752
- workstreamMode: params.workstream.mode,
753
- ...(params.workstream.coreType ? { coreType: params.workstream.coreType } : {}),
754
- userMessageText,
755
- assistantSummary: conversationSummary,
756
- messageId: params.referenceUserMessageId,
757
- },
758
- occurredAt: toIsoDateTimeString(params.referenceUserMessage.metadata?.createdAt ?? Date.now()),
759
- },
760
- })
761
-
762
- await safeEnqueue(
763
- async () => {
764
- const enqueuePostChatOrgAction = getRuntimeAdapters().queues?.enqueuePostChatOrgAction
765
- if (!enqueuePostChatOrgAction) {
766
- return
767
- }
768
-
769
- await enqueuePostChatOrgAction({
770
- orgId: params.orgIdString,
771
- workstreamId: params.workstreamIdString,
772
- sourceId: params.referenceUserMessageId,
773
- sourceCreatedAt: params.referenceUserMessage?.metadata?.createdAt ?? Date.now(),
774
- conversationSummary,
775
- })
776
- },
777
- { operationName: 'post-chat org action enqueue' },
778
- )
779
-
780
- if (recentActivityService.isMeaningfulRefinementCandidate(recentActivityResult.item)) {
781
- await safeEnqueue(() => enqueueRecentActivityTitleRefinement({ activityId: recentActivityResult.item.id }), {
782
- operationName: 'recent activity title refinement enqueue',
783
- })
784
- }
785
- }
786
- }
787
-
788
- if (shouldEnqueueRegularDigestForWorkstream({ onboardingActive: params.onboardingActive, turnCount })) {
789
- await safeEnqueue(() => enqueueRegularChatMemoryDigest({ orgId: params.orgIdString }), {
790
- operationName: 'regular chat memory digest enqueue',
791
- })
792
- }
793
-
794
- if (shouldEnqueueSkillExtraction({ onboardingActive: params.onboardingActive, turnCount })) {
795
- await safeEnqueue(() => enqueueSkillExtraction({ orgId: params.orgIdString }), {
796
- operationName: 'skill extraction enqueue',
797
- })
798
- }
799
-
800
- if (shouldEnqueueMemoryConsolidation({ onboardingActive: params.onboardingActive, turnCount })) {
801
- await safeEnqueue(() => enqueueMemoryConsolidation({ scopeId: params.orgIdString }), {
802
- operationName: 'memory consolidation enqueue',
803
- })
804
- }
805
- }
806
-
807
439
  export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams): Promise<PreparedWorkstreamTurn> {
808
440
  const { workstream, workstreamRef, orgRef, userRef, userName } = params
809
441
  const runtimeAdapters = getRuntimeAdapters()
810
442
  const turnHooks = getTurnHooks()
811
443
  const toolProviders = getToolProviders()
812
- const workspaceProvider = runtimeAdapters.services?.workspaceProvider
444
+ const workspaceProvider = runtimeAdapters.workspaceProvider
813
445
  const orgIdString = recordIdToString(orgRef, TABLES.ORGANIZATION)
814
446
  const userIdString = recordIdToString(userRef, TABLES.USER)
815
447
  const workstreamIdString = recordIdToString(workstreamRef, TABLES.WORKSTREAM)
@@ -843,8 +475,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
843
475
 
844
476
  const timer = lotaDebugLogger.timer('prepare')
845
477
 
846
- // Start workspace fetch early does not depend on compaction gate
847
- const workspacePromise = workspaceProvider ? workspaceProvider.getWorkspace(orgRef) : Promise.resolve({})
478
+ // Start workspace fetch early unless approval handling will short-circuit the turn.
479
+ const workspacePromise =
480
+ params.kind !== 'approvalContinuation' && workspaceProvider
481
+ ? workspaceProvider.getWorkspace(orgRef)
482
+ : Promise.resolve({})
848
483
 
849
484
  const workstreamRecord = await waitForWorkstreamCompactionIfNeeded(workstreamRef)
850
485
  timer.step('compaction-gate')
@@ -875,7 +510,27 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
875
510
  workstreamRef,
876
511
  persistedCompactionCursor,
877
512
  )
878
- const persistedRecentHistoryPromise = workstreamMessageService.listRecentMessages(workstreamRef, 64)
513
+ let recentHistoryPromise: Promise<ChatMessage[]> | null = null
514
+ const loadRecentHistory = async (): Promise<ChatMessage[]> => {
515
+ if (!recentHistoryPromise) {
516
+ recentHistoryPromise = workstreamMessageService
517
+ .listRecentMessages(workstreamRef, 64)
518
+ .then(async (persistedRecentHistory) => {
519
+ if (persistedRecentHistory.length === 0) {
520
+ return [] as ChatMessage[]
521
+ }
522
+
523
+ const messages = await validateUIMessages<ChatMessage>({
524
+ messages: persistedRecentHistory,
525
+ metadataSchema: messageMetadataSchema,
526
+ dataSchemas: dataPartsSchemas,
527
+ })
528
+ return messages.map(hydrateMessageFileUrls)
529
+ })
530
+ }
531
+
532
+ return await recentHistoryPromise
533
+ }
879
534
 
880
535
  let userMessage: ChatMessage | undefined
881
536
  if (inputMessage) {
@@ -888,34 +543,15 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
888
543
  }
889
544
  }
890
545
 
891
- const [workspace, persistedLiveHistory, persistedRecentHistory] = await Promise.all([
892
- workspacePromise,
893
- persistedLiveHistoryPromise,
894
- persistedRecentHistoryPromise,
895
- ])
896
- timer.step('fetch-workspace+history')
897
- const workspaceLifecycleState = workspaceProvider ? await workspaceProvider.getLifecycleState?.(workspace) : undefined
898
- timer.step('workspace-lifecycle-state')
899
- const workspaceProfileState = workspaceProvider
900
- ? await workspaceProvider.readProfileProjectionState?.(workspace)
901
- : undefined
902
- timer.step('workspace-profile-state')
903
- const [liveHistory, recentHistory] = await Promise.all([
904
- persistedLiveHistory.length === 0
905
- ? Promise.resolve([] as ChatMessage[])
906
- : validateUIMessages<ChatMessage>({
907
- messages: persistedLiveHistory,
908
- metadataSchema: messageMetadataSchema,
909
- dataSchemas: dataPartsSchemas,
910
- }).then((messages) => messages.map(hydrateMessageFileUrls)),
911
- persistedRecentHistory.length === 0
912
- ? Promise.resolve([] as ChatMessage[])
913
- : validateUIMessages<ChatMessage>({
914
- messages: persistedRecentHistory,
915
- metadataSchema: messageMetadataSchema,
916
- dataSchemas: dataPartsSchemas,
917
- }).then((messages) => messages.map(hydrateMessageFileUrls)),
918
- ])
546
+ const persistedLiveHistory = await persistedLiveHistoryPromise
547
+ timer.step('fetch-history')
548
+ const liveHistory = await (persistedLiveHistory.length === 0
549
+ ? Promise.resolve([] as ChatMessage[])
550
+ : validateUIMessages<ChatMessage>({
551
+ messages: persistedLiveHistory,
552
+ metadataSchema: messageMetadataSchema,
553
+ dataSchemas: dataPartsSchemas,
554
+ }).then((messages) => messages.map(hydrateMessageFileUrls)))
919
555
  timer.step('validate+hydrate-history')
920
556
 
921
557
  if (userMessage && shouldPersistInputMessage) {
@@ -938,6 +574,18 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
938
574
  ? extractMessageText(referenceUserMessage).trim()
939
575
  : ''
940
576
 
577
+ const respondedBy = recordIdToString(userRef, TABLES.USER)
578
+ if (params.kind === 'approvalContinuation') {
579
+ await executionPlanService.applyApprovalResponseFromMessages({
580
+ workstreamId: workstreamRef,
581
+ approvalMessages: params.approvalMessages,
582
+ respondedBy,
583
+ })
584
+ timer.step('approval-continuation')
585
+
586
+ return { originalMessages, run: async () => ({ inputMessageId: referenceUserMessage?.id, assistantMessages: [] }) }
587
+ }
588
+
941
589
  if (
942
590
  params.kind === 'userTurn' &&
943
591
  workstream.mode === 'group' &&
@@ -952,7 +600,6 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
952
600
  )
953
601
  }
954
602
 
955
- const onboardingActive = workspaceLifecycleState?.bootstrapActive ?? false
956
603
  if (workstream.core && !workstream.coreType) {
957
604
  throw new WorkstreamTurnError('Core workstreams require a core type.', 400)
958
605
  }
@@ -963,112 +610,31 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
963
610
  params.agentIdOverride ??
964
611
  (workstream.mode === 'direct' ? workstream.agentId : (coreWorkstreamProfile?.config.agentId ?? defaultLeadAgentId))
965
612
  const coreInstructionSections = coreWorkstreamProfile ? [coreWorkstreamProfile.instructions] : undefined
966
- const getLinearInstallationByOrgId = getPluginService([
967
- 'linear',
968
- 'services',
969
- 'linearService',
970
- 'getInstallationByOrgId',
971
- ])
972
- const getGithubInstallationForOrganization = getPluginService([
973
- 'github',
974
- 'services',
975
- 'githubService',
976
- 'getInstallationForOrganization',
977
- ])
978
-
979
- const [linearInstallation, githubInstallation, indexedRepoContext, recentDomainEvents, promptSummary] =
980
- await Promise.all([
981
- getLinearInstallationByOrgId ? (getLinearInstallationByOrgId(orgRef) as Promise<unknown>) : Promise.resolve(null),
982
- getGithubInstallationForOrganization
983
- ? (getGithubInstallationForOrganization(orgIdString) as Promise<unknown>)
984
- : Promise.resolve(null),
985
- buildIndexedRepositoriesContext(orgIdString),
986
- workspaceProvider?.listRecentDomainEvents?.(orgRef, 5) ?? Promise.resolve([] as Array<Record<string, unknown>>),
987
- workspaceProvider?.buildPromptSummary
988
- ? workspaceProvider.buildPromptSummary(orgRef).catch(() => undefined)
989
- : Promise.resolve(undefined),
990
- ])
991
- timer.step('parallel-context-fetch(plugins+repos+events+summary)')
992
- let linearInstalled = Boolean(linearInstallation)
993
- let githubInstalled = Boolean(githubInstallation)
994
- let promptContext = buildAgentPromptContext({
995
- workspaceName: workspaceProfileState?.workspaceName ?? readOptionalString((workspace as { name?: unknown }).name),
996
- summaryBlock: workspaceProfileState?.summaryBlock,
997
- structuredProfile: workspaceProfileState?.structuredProfile,
998
- promptSummary,
999
- userName: userName ?? undefined,
1000
- recentDomainEvents,
613
+ const assembledContext = await assembleWorkstreamTurnContext({
614
+ workstream,
615
+ workstreamRef,
616
+ orgRef,
617
+ userRef,
618
+ userName,
619
+ orgIdString,
620
+ userIdString,
621
+ messageText,
622
+ workspacePromise,
623
+ workspaceProvider,
624
+ turnHooks,
625
+ onStep: (step) => timer.step(step),
1001
626
  })
1002
- let retrievedKnowledgeSection: string | undefined =
1003
- onboardingActive || !messageText
1004
- ? undefined
1005
- : await workspaceProvider?.buildRetrievedKnowledgeSection?.({
1006
- workspaceId: orgIdString,
1007
- userId: userIdString,
1008
- query: messageText,
1009
- })
1010
- timer.step('rag-knowledge-retrieval')
1011
- const buildContextResult = asRecord(
1012
- await turnHooks.buildContext?.({
1013
- workstream,
1014
- workstreamRef,
1015
- orgRef,
1016
- userRef,
1017
- userName,
1018
- workspace,
1019
- onboardingActive,
1020
- messageText,
1021
- linearInstalled,
1022
- githubInstalled,
1023
- indexedRepoContext,
1024
- promptContext,
1025
- workspaceLifecycleState,
1026
- workspaceProfileState,
1027
- promptSummary,
1028
- recentDomainEvents,
1029
- retrievedKnowledgeSection,
1030
- }),
1031
- )
1032
- timer.step('hook:buildContext')
1033
- const buildContextPromptDetails = readOptionalString(buildContextResult?.systemWorkspaceDetails)
1034
- if (buildContextPromptDetails) {
1035
- promptContext = { systemWorkspaceDetails: buildContextPromptDetails }
1036
- }
1037
- const buildContextRetrievedKnowledge = readOptionalString(buildContextResult?.retrievedKnowledgeSection)
1038
- if (buildContextRetrievedKnowledge !== undefined) {
1039
- retrievedKnowledgeSection = buildContextRetrievedKnowledge
1040
- }
1041
- const buildContextLinearInstalled = readOptionalBoolean(buildContextResult?.linearInstalled)
1042
- if (buildContextLinearInstalled !== undefined) {
1043
- linearInstalled = buildContextLinearInstalled
1044
- }
1045
- const buildContextGithubInstalled = readOptionalBoolean(buildContextResult?.githubInstalled)
1046
- if (buildContextGithubInstalled !== undefined) {
1047
- githubInstalled = buildContextGithubInstalled
1048
- }
1049
- const hookInstructionSections = readInstructionSections(
1050
- await turnHooks.buildExtraInstructionSections?.({
1051
- workstream,
1052
- workstreamRef,
1053
- orgRef,
1054
- userRef,
1055
- userName,
1056
- workspace,
1057
- onboardingActive,
1058
- messageText,
1059
- linearInstalled,
1060
- githubInstalled,
1061
- indexedRepoContext,
1062
- promptContext,
1063
- workspaceLifecycleState,
1064
- workspaceProfileState,
1065
- promptSummary,
1066
- recentDomainEvents,
1067
- retrievedKnowledgeSection,
1068
- context: buildContextResult,
1069
- }),
1070
- )
1071
- timer.step('hook:buildExtraInstructionSections')
627
+ const {
628
+ workspace,
629
+ onboardingActive,
630
+ linearInstalled,
631
+ githubInstalled,
632
+ indexedRepoContext,
633
+ promptContext,
634
+ retrievedKnowledgeSection,
635
+ buildContextResult,
636
+ hookInstructionSections,
637
+ } = assembledContext
1072
638
 
1073
639
  let memoryBlock = workstreamService.formatMemoryBlockForPrompt(workstreamRecord)
1074
640
  let workstreamState = initialWorkstreamState
@@ -1091,17 +657,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1091
657
  stripExecutionPlanFieldsFromWorkstreamState(workstreamState, executionPlans.length > 0),
1092
658
  )
1093
659
  }
1094
- const respondedBy = recordIdToString(userRef, TABLES.USER)
1095
- if (params.kind === 'approvalContinuation') {
1096
- const appliedApproval = await executionPlanService.applyApprovalResponseFromMessages({
1097
- workstreamId: workstreamRef,
1098
- approvalMessages: params.approvalMessages,
1099
- respondedBy,
1100
- })
1101
- if (appliedApproval) {
1102
- invalidateExecutionPlanInstructionSections()
1103
- }
1104
- } else if (userMessage) {
660
+ if (userMessage) {
1105
661
  const appliedHumanInput = await executionPlanService.applyHumanInputFromUserMessage({
1106
662
  workstreamId: workstreamRef,
1107
663
  message: userMessage,
@@ -1112,6 +668,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1112
668
  }
1113
669
  }
1114
670
  timer.step('execution-plan-input')
671
+
1115
672
  const preSeededMemoriesByAgent = new Map<string, string | undefined>()
1116
673
  const getPreSeededMemoriesSection = async (agentId: string): Promise<string | undefined> => {
1117
674
  if (preSeededMemoriesByAgent.has(agentId)) {
@@ -1128,17 +685,18 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1128
685
  }
1129
686
 
1130
687
  const learnedSkillsByAgent = new Map<string, string | undefined>()
1131
- const getLearnedSkillsSection = async (agentId: string): Promise<string | undefined> => {
688
+ const getLearnedSkillsSection = async (agentId: string, queryText = messageText): Promise<string | undefined> => {
1132
689
  if (onboardingActive) return undefined
1133
- if (learnedSkillsByAgent.has(agentId)) return learnedSkillsByAgent.get(agentId)
690
+ const cacheKey = `${agentId}::${queryText}`
691
+ if (learnedSkillsByAgent.has(cacheKey)) return learnedSkillsByAgent.get(cacheKey)
1134
692
 
1135
693
  const section = await learnedSkillService
1136
- .retrieveForTurn({ orgId: orgIdString, agentId, query: messageText, limit: 3, minConfidence: 0.6 })
694
+ .retrieveForTurn({ orgId: orgIdString, agentId, query: queryText, limit: 3, minConfidence: 0.6 })
1137
695
  .catch((error) => {
1138
696
  aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
1139
697
  return undefined
1140
698
  })
1141
- learnedSkillsByAgent.set(agentId, section)
699
+ learnedSkillsByAgent.set(cacheKey, section)
1142
700
  return section
1143
701
  }
1144
702
 
@@ -1161,6 +719,36 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1161
719
  latestUserMessageId: referenceUserMessageId,
1162
720
  uploadMetadataText: buildReadableUploadMetadataText(listReadableUploads(extraMessages)),
1163
721
  })
722
+ const buildTurnToolParams = (toolParams: {
723
+ agentId: string
724
+ mode: 'direct' | 'fixedWorkstreamMode' | 'workstreamMode'
725
+ memoryBlock: string
726
+ onAppendMemoryBlock: (value: string) => void
727
+ extraMessages?: ChatMessage[]
728
+ skills?: string[]
729
+ includeExecutionPlanTools: boolean
730
+ }) => ({
731
+ agentId: toolParams.agentId,
732
+ orgId: orgRef,
733
+ userId: userRef,
734
+ userName: userName ?? 'there',
735
+ workstreamId: workstreamRef,
736
+ orgIdString,
737
+ workstreamMode: workstream.mode,
738
+ mode: toolParams.mode,
739
+ linearInstalled,
740
+ onboardingActive,
741
+ githubInstalled,
742
+ provideRepoTool: indexedRepoContext.provideRepoTool,
743
+ skills: toolParams.skills,
744
+ defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[toolParams.agentId],
745
+ memoryBlock: toolParams.memoryBlock,
746
+ onAppendMemoryBlock: toolParams.onAppendMemoryBlock,
747
+ availableUploads: listReadableUploads(toolParams.extraMessages),
748
+ includeExecutionPlanTools: toolParams.includeExecutionPlanTools,
749
+ onExecutionPlanChanged: invalidateExecutionPlanInstructionSections,
750
+ context: buildContextResult,
751
+ })
1164
752
 
1165
753
  timer.step('preparation-complete')
1166
754
 
@@ -1171,10 +759,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1171
759
  const runTimer = lotaDebugLogger.timer('run')
1172
760
  const serverRunId = Bun.randomUUIDv7()
1173
761
  const runAbort = createServerRunAbortController(params.abortSignal)
1174
- await Promise.all([
1175
- workstreamService.setActiveRunId(workstreamRef, serverRunId),
1176
- params.streamId ? workstreamService.setActiveStreamId(workstreamRef, params.streamId) : undefined,
1177
- ])
762
+ await workstreamService.setActiveTurn(workstreamRef, serverRunId, params.streamId ?? null)
1178
763
  chatRunRegistry.register(serverRunId, runAbort.controller)
1179
764
  runTimer.step('set-active-run+stream')
1180
765
 
@@ -1254,30 +839,19 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1254
839
  const includeExecutionPlanTools =
1255
840
  runParams.includeExecutionPlanTools ?? (runParams.mode !== 'fixedWorkstreamMode' && !onboardingActive)
1256
841
  const rawTools: ToolSet = {
1257
- ...((await buildAgentTools({
1258
- agentId: runParams.agentId,
1259
- orgId: orgRef,
1260
- userId: userRef,
1261
- userName: userName ?? 'there',
1262
- workstreamId: workstreamRef,
1263
- orgIdString,
1264
- workstreamMode: workstream.mode,
1265
- mode: runParams.mode,
1266
- linearInstalled,
1267
- onboardingActive,
1268
- githubInstalled,
1269
- provideRepoTool: indexedRepoContext.provideRepoTool,
1270
- skills: runParams.skills,
1271
- defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[runParams.agentId],
1272
- memoryBlock: runMemoryBlock,
1273
- onAppendMemoryBlock: (value: string) => {
1274
- runMemoryBlock = value
1275
- },
1276
- availableUploads: listReadableUploads(runParams.extraMessages),
1277
- includeExecutionPlanTools,
1278
- onExecutionPlanChanged: invalidateExecutionPlanInstructionSections,
1279
- context: buildContextResult,
1280
- })) as ToolSet),
842
+ ...((await buildAgentTools(
843
+ buildTurnToolParams({
844
+ agentId: runParams.agentId,
845
+ mode: runParams.mode,
846
+ skills: runParams.skills,
847
+ memoryBlock: runMemoryBlock,
848
+ onAppendMemoryBlock: (value: string) => {
849
+ runMemoryBlock = value
850
+ },
851
+ extraMessages: runParams.extraMessages,
852
+ includeExecutionPlanTools,
853
+ }),
854
+ )) as ToolSet),
1281
855
  ...toolProviders,
1282
856
  ...runParams.extraTools,
1283
857
  }
@@ -1292,6 +866,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1292
866
  observer: createObserver(runParams.agentId),
1293
867
  skills: runParams.skills,
1294
868
  additionalInstructionSections: runParams.additionalInstructionSections,
869
+ includeExecutionPlanTools,
1295
870
  writer,
1296
871
  })
1297
872
 
@@ -1306,12 +881,6 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1306
881
  )
1307
882
  }
1308
883
 
1309
- // Execution-plan approval continuations mutate plan state and persist the approval message,
1310
- // but they do not begin a new visible agent turn.
1311
- if (params.kind === 'approvalContinuation') {
1312
- return { inputMessageId: referenceUserMessage?.id, assistantMessages: [] }
1313
- }
1314
-
1315
884
  if (params.kind === 'planTurn') {
1316
885
  const planTurn = params.planTurn
1317
886
  const submitPlanTurnNodeResultTool = createTool({
@@ -1330,10 +899,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1330
899
  await runVisibleAgent({
1331
900
  agentId: planTurn.nodeSpec.owner.ref,
1332
901
  mode: workstream.mode === 'direct' ? 'direct' : 'workstreamMode',
1333
- additionalInstructionSections: mergeInstructionSections(
1334
- [buildPlanTurnExecutionSection(planTurn), buildPlanTurnResultContractSection(planTurn)],
1335
- optionalInstructionSection(buildUpstreamHandoffSection(planTurn.upstreamHandoffs)),
1336
- ),
902
+ additionalInstructionSections: buildPlanTurnInstructionSections(planTurn),
1337
903
  extraMessages: [buildPlanTurnPromptMessage(planTurn)],
1338
904
  includeExecutionPlanTools: false,
1339
905
  extraTools: { [SUBMIT_PLAN_TURN_RESULT_TOOL_NAME]: submitPlanTurnNodeResultTool },
@@ -1345,120 +911,43 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1345
911
  description: 'Consult one specialist teammate for domain-specific guidance before replying to the user.',
1346
912
  inputSchema: ConsultSpecialistArgsSchema,
1347
913
  execute: async function* (
1348
- { agentId, task }: z.infer<typeof ConsultSpecialistArgsSchema>,
914
+ { agentId, task }: ConsultSpecialistArgs,
1349
915
  { abortSignal: toolAbortSignal }: { abortSignal?: AbortSignal },
1350
916
  ) {
1351
- let specialistMemoryBlock = memoryBlock
1352
917
  const specialistTaskMessage = buildSpecialistTaskMessage({ agentId, task })
1353
- const specialistTools = await buildAgentTools({
1354
- agentId,
1355
- orgId: orgRef,
1356
- userId: userRef,
1357
- userName: userName ?? 'there',
1358
- workstreamId: workstreamRef,
1359
- orgIdString,
1360
- workstreamMode: workstream.mode,
1361
- mode: 'fixedWorkstreamMode',
1362
- linearInstalled,
1363
- onboardingActive,
1364
- githubInstalled,
1365
- provideRepoTool: indexedRepoContext.provideRepoTool,
1366
- defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[agentId],
1367
- memoryBlock: specialistMemoryBlock,
1368
- onAppendMemoryBlock: (value: string) => {
1369
- specialistMemoryBlock = value
1370
- },
1371
- availableUploads: listReadableUploads([specialistTaskMessage]),
1372
- includeExecutionPlanTools: false,
1373
- context: buildContextResult,
1374
- })
1375
-
1376
- const [
1377
- specialistExecutionPlanInstructionSections,
1378
- specialistPreSeededMemories,
1379
- specialistWorkstreamState,
1380
- specialistLearnedSkills,
1381
- ] = await Promise.all([
1382
- getExecutionPlanInstructionSections(),
1383
- getPreSeededMemoriesSection(agentId),
1384
- getWorkstreamStateSection(),
1385
- getLearnedSkillsSection(agentId),
1386
- ])
1387
- const specialistConfig = getAgentRuntimeConfig({
1388
- agentId,
1389
- workstreamMode: workstream.mode,
1390
- mode: 'fixedWorkstreamMode',
1391
- onboardingActive,
1392
- linearInstalled,
1393
- systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
1394
- preSeededMemoriesSection: specialistPreSeededMemories,
1395
- retrievedKnowledgeSection,
1396
- workstreamMemoryBlock: specialistMemoryBlock,
1397
- workstreamStateSection: specialistWorkstreamState,
1398
- learnedSkillsSection: specialistLearnedSkills,
1399
- additionalInstructionSections: mergeInstructionSections(
1400
- specialistExecutionPlanInstructionSections,
1401
- coreInstructionSections,
1402
- hookInstructionSections,
1403
- ),
1404
- context: buildContextResult,
1405
- }) as Record<string, unknown>
1406
- const observer = createObserver(agentId)
1407
- const agent = createAgent[specialistConfig.id as string]({
1408
- mode: 'fixedWorkstreamMode',
1409
- tools: { ...(specialistTools as ToolSet), ...toolProviders },
1410
- extraInstructions: specialistConfig.extraInstructions,
1411
- stopWhen: [stepCountIs(specialistConfig.maxSteps as number)],
1412
- }) as ToolLoopAgent<never, ToolSet>
1413
- const modelMessages = await convertToModelMessages(buildRunInputMessages([specialistTaskMessage]), {
1414
- ignoreIncompleteToolCalls: true,
1415
- })
1416
918
  const specialistAbortSignal = toolAbortSignal ?? runAbort.signal
1417
- let result: unknown
1418
- try {
1419
- result = await observer.run(() =>
1420
- agent.stream({ messages: modelMessages, abortSignal: specialistAbortSignal }),
1421
- )
1422
- } catch (error) {
1423
- if (specialistAbortSignal.aborted) {
1424
- observer.recordAbort(error)
1425
- } else {
1426
- observer.recordError(error)
1427
- }
1428
- throw error
1429
- }
1430
- if (!hasUIMessageStream(result)) {
1431
- throw new Error(`Specialist ${agentId} did not expose a UI message stream.`)
1432
- }
1433
-
1434
- let finalMessage: ChatMessage | null = null
1435
- for await (const message of readUIMessageStream<ChatMessage>({
1436
- stream: result.toUIMessageStream({
1437
- generateMessageId: () => Bun.randomUUIDv7(),
1438
- sendReasoning: true,
1439
- sendSources: true,
1440
- sendStart: false,
1441
- sendFinish: false,
1442
- messageMetadata: createAgentMessageMetadata({
1443
- agentId,
1444
- agentName: specialistConfig.displayName as string,
1445
- }),
1446
- }) as ReadableStream<never>,
1447
- })) {
1448
- finalMessage = withMessageCreatedAt(message, Date.now())
1449
- yield finalMessage
1450
- }
1451
-
1452
- if (!finalMessage) {
1453
- throw new Error(`Specialist ${agentId} did not produce a response message.`)
1454
- }
1455
-
1456
- for (const toolError of collectToolOutputErrors({ responseMessage: finalMessage })) {
1457
- aiLogger.warn`Tool execution failed (agent=${agentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
1458
- }
1459
-
1460
- memoryBlock = specialistMemoryBlock
1461
- return finalMessage
919
+ const { result: finalMessage, memoryBlock: nextMemoryBlock } = await runSpecialistSession({
920
+ initialMemoryBlock: memoryBlock,
921
+ buildTools: async ({ memoryBlock: currentMemoryBlock, onAppendMemoryBlock }) =>
922
+ (await buildAgentTools(
923
+ buildTurnToolParams({
924
+ agentId,
925
+ mode: 'fixedWorkstreamMode',
926
+ memoryBlock: currentMemoryBlock,
927
+ onAppendMemoryBlock,
928
+ extraMessages: [specialistTaskMessage],
929
+ includeExecutionPlanTools: false,
930
+ }),
931
+ )) as ToolSet,
932
+ run: async ({ tools, memoryBlock: currentMemoryBlock }) =>
933
+ await streamAgentResponse(
934
+ { ...streamCtx, memoryBlock: currentMemoryBlock },
935
+ {
936
+ agentId,
937
+ mode: 'fixedWorkstreamMode',
938
+ messages: buildRunInputMessages([specialistTaskMessage]),
939
+ tools: { ...tools, ...toolProviders },
940
+ observer: createObserver(agentId),
941
+ additionalInstructionSections: coreInstructionSections,
942
+ includeExecutionPlanTools: false,
943
+ abortSignal: specialistAbortSignal,
944
+ },
945
+ ),
946
+ })
947
+ const committedFinalMessage = withMessageCreatedAt(finalMessage, Date.now())
948
+ memoryBlock = nextMemoryBlock
949
+ yield committedFinalMessage
950
+ return committedFinalMessage
1462
951
  },
1463
952
  toModelOutput: ({ output }) => {
1464
953
  const result = getChatMessageFromToolOutput(output)
@@ -1545,11 +1034,17 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1545
1034
  })
1546
1035
  },
1547
1036
  unregisterRun: (runId) => chatRunRegistry.unregister(runId),
1548
- clearActiveRunId: (runId) => workstreamService.clearActiveRunIdIfMatches(workstreamRef, runId),
1037
+ clearActiveRunId: async (runId) => {
1038
+ const activeStreamId = await workstreamService.getActiveStreamId(workstreamRef)
1039
+ await workstreamService.clearActiveTurn(workstreamRef, { runId, streamId: activeStreamId })
1040
+ },
1549
1041
  disposeAbort: () => runAbort.dispose(),
1550
1042
  activeStreamId: params.streamId,
1551
- clearActiveStreamId: (streamId) =>
1552
- workstreamService.clearActiveStreamIdIfMatches(workstreamRef, streamId),
1043
+ clearActiveStreamId: async (streamId) => {
1044
+ const activeRunId = await workstreamService.getActiveRunId(workstreamRef)
1045
+ if (!activeRunId) return
1046
+ await workstreamService.clearActiveTurn(workstreamRef, { runId: activeRunId, streamId })
1047
+ },
1553
1048
  })
1554
1049
 
1555
1050
  if (allAssistantMessages.length > 0 && shouldProcessPostRunSideEffects) {
@@ -1566,15 +1061,13 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1566
1061
  allAssistantMessages,
1567
1062
  referenceUserMessage,
1568
1063
  referenceUserMessageId,
1569
- recentHistory,
1064
+ loadRecentHistory,
1570
1065
  listReadableUploads: () => listReadableUploads(),
1571
1066
  memoryBlock,
1572
1067
  visibleWorkstreamAgentId,
1573
1068
  defaultLeadAgentId,
1574
1069
  latestWorkstreamRecord,
1575
1070
  latestPersistedState,
1576
- turnHooks,
1577
- buildContextResult,
1578
1071
  isUserTurn: params.kind === 'userTurn',
1579
1072
  })
1580
1073
  }