@lota-sdk/core 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +8 -7
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/index.ts +0 -2
  11. package/src/bifrost/bifrost.ts +2 -7
  12. package/src/config/agent-defaults.ts +31 -21
  13. package/src/config/agent-types.ts +11 -0
  14. package/src/config/constants.ts +2 -14
  15. package/src/config/debug-logger.ts +5 -1
  16. package/src/config/index.ts +3 -0
  17. package/src/config/model-constants.ts +16 -34
  18. package/src/config/search.ts +1 -15
  19. package/src/create-runtime.ts +244 -178
  20. package/src/db/cursor-pagination.ts +3 -6
  21. package/src/db/index.ts +2 -0
  22. package/src/db/memory-store.rows.ts +7 -7
  23. package/src/db/memory-store.ts +14 -18
  24. package/src/db/memory.ts +13 -13
  25. package/src/db/service.ts +153 -79
  26. package/src/db/startup.ts +6 -10
  27. package/src/db/surreal-mutation.ts +43 -0
  28. package/src/db/tables.ts +7 -0
  29. package/src/db/workstream-message-row.ts +15 -0
  30. package/src/embeddings/provider.ts +1 -1
  31. package/src/queues/context-compaction.queue.ts +15 -46
  32. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  33. package/src/queues/index.ts +3 -0
  34. package/src/queues/memory-consolidation.queue.ts +16 -51
  35. package/src/queues/plan-scheduler.queue.ts +97 -0
  36. package/src/queues/post-chat-memory.queue.ts +15 -56
  37. package/src/queues/queue-factory.ts +100 -0
  38. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  39. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  40. package/src/queues/skill-extraction.queue.ts +15 -47
  41. package/src/queues/workstream-title-generation.queue.ts +15 -47
  42. package/src/redis/connection.ts +6 -0
  43. package/src/redis/index.ts +1 -1
  44. package/src/redis/stream-context.ts +11 -0
  45. package/src/runtime/agent-runtime-policy.ts +106 -21
  46. package/src/runtime/approval-continuation.ts +12 -6
  47. package/src/runtime/context-compaction-runtime.ts +1 -1
  48. package/src/runtime/context-compaction.ts +22 -60
  49. package/src/runtime/execution-plan.ts +22 -18
  50. package/src/runtime/graph-designer.ts +15 -0
  51. package/src/runtime/helper-model.ts +9 -197
  52. package/src/runtime/index.ts +2 -0
  53. package/src/runtime/llm-content.ts +1 -1
  54. package/src/runtime/memory-block.ts +9 -11
  55. package/src/runtime/memory-pipeline.ts +6 -9
  56. package/src/runtime/plugin-resolution.ts +35 -0
  57. package/src/runtime/plugin-types.ts +72 -0
  58. package/src/runtime/retrieval-adapters.ts +1 -1
  59. package/src/runtime/runtime-config.ts +25 -12
  60. package/src/runtime/runtime-extensions.ts +2 -2
  61. package/src/runtime/runtime-worker-registry.ts +6 -0
  62. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  63. package/src/runtime/team-consultation-prompts.ts +11 -2
  64. package/src/runtime/title-helpers.ts +2 -4
  65. package/src/runtime/workstream-chat-helpers.ts +1 -1
  66. package/src/services/adaptive-playbook.service.ts +152 -0
  67. package/src/services/agent-executor.service.ts +293 -0
  68. package/src/services/artifact-provenance.service.ts +172 -0
  69. package/src/services/attachment.service.ts +6 -11
  70. package/src/services/context-compaction.service.ts +72 -55
  71. package/src/services/context-enrichment.service.ts +33 -0
  72. package/src/services/coordination-registry.service.ts +117 -0
  73. package/src/services/document-chunk.service.ts +1 -1
  74. package/src/services/domain-agent-executor.service.ts +71 -0
  75. package/src/services/execution-plan.service.ts +269 -50
  76. package/src/services/feedback-loop.service.ts +96 -0
  77. package/src/services/global-orchestrator.service.ts +148 -0
  78. package/src/services/index.ts +26 -0
  79. package/src/services/institutional-memory.service.ts +145 -0
  80. package/src/services/learned-skill.service.ts +24 -5
  81. package/src/services/memory-assessment.service.ts +3 -2
  82. package/src/services/memory-utils.ts +3 -8
  83. package/src/services/memory.service.ts +42 -59
  84. package/src/services/monitoring-window.service.ts +86 -0
  85. package/src/services/mutating-approval.service.ts +1 -1
  86. package/src/services/node-workspace.service.ts +155 -0
  87. package/src/services/notification.service.ts +39 -0
  88. package/src/services/organization-member.service.ts +11 -4
  89. package/src/services/organization.service.ts +5 -5
  90. package/src/services/ownership-dispatcher.service.ts +403 -0
  91. package/src/services/plan-approval.service.ts +1 -1
  92. package/src/services/plan-builder.service.ts +1 -0
  93. package/src/services/plan-checkpoint.service.ts +30 -2
  94. package/src/services/plan-compiler.service.ts +5 -0
  95. package/src/services/plan-coordination.service.ts +152 -0
  96. package/src/services/plan-cycle.service.ts +284 -0
  97. package/src/services/plan-deadline.service.ts +287 -0
  98. package/src/services/plan-executor.service.ts +384 -40
  99. package/src/services/plan-run.service.ts +41 -7
  100. package/src/services/plan-scheduler.service.ts +240 -0
  101. package/src/services/plan-template.service.ts +117 -0
  102. package/src/services/plan-validator.service.ts +84 -2
  103. package/src/services/plan-workspace.service.ts +83 -0
  104. package/src/services/playbook-registry.service.ts +67 -0
  105. package/src/services/plugin-executor.service.ts +103 -0
  106. package/src/services/quality-metrics.service.ts +132 -0
  107. package/src/services/recent-activity.service.ts +27 -31
  108. package/src/services/skill-resolver.service.ts +19 -0
  109. package/src/services/system-executor.service.ts +105 -0
  110. package/src/services/workstream-message.service.ts +12 -34
  111. package/src/services/workstream-plan-registry.service.ts +22 -0
  112. package/src/services/workstream-title.service.ts +3 -1
  113. package/src/services/workstream-turn-preparation.service.ts +34 -66
  114. package/src/services/workstream.service.ts +33 -55
  115. package/src/services/workstream.types.ts +9 -9
  116. package/src/services/write-intent-validator.service.ts +81 -0
  117. package/src/storage/attachment-parser.ts +1 -1
  118. package/src/storage/attachment-utils.ts +1 -1
  119. package/src/storage/generated-document-storage.service.ts +3 -2
  120. package/src/system-agents/delegated-agent-factory.ts +2 -0
  121. package/src/tools/execution-plan.tool.ts +17 -23
  122. package/src/tools/index.ts +0 -1
  123. package/src/tools/team-think.tool.ts +6 -4
  124. package/src/utils/async.ts +2 -1
  125. package/src/utils/date-time.ts +4 -32
  126. package/src/utils/env.ts +8 -0
  127. package/src/utils/errors.ts +42 -10
  128. package/src/utils/index.ts +9 -0
  129. package/src/utils/string.ts +114 -1
  130. package/src/workers/index.ts +1 -0
  131. package/src/workers/regular-chat-memory-digest.runner.ts +2 -2
  132. package/src/workers/skill-extraction.runner.ts +1 -1
  133. package/src/workers/utils/file-section-chunker.ts +2 -1
  134. package/src/workers/utils/repomix-file-sections.ts +2 -2
  135. package/src/workers/utils/sandbox-error.ts +11 -2
  136. package/src/workers/utils/workstream-message-query.ts +11 -20
  137. package/src/workers/worker-utils.ts +2 -2
  138. package/src/tools/log-hello-world.tool.ts +0 -17
@@ -0,0 +1,22 @@
1
+ import type { PlanRunRecord, SerializableExecutionPlan } from '@lota-sdk/shared'
2
+
3
+ import type { RecordIdInput } from '../db/record-id'
4
+ import { planRunService } from './plan-run.service'
5
+
6
+ class WorkstreamPlanRegistryService {
7
+ async listActiveRuns(workstreamId: RecordIdInput): Promise<PlanRunRecord[]> {
8
+ return planRunService.getActiveRunRecords(workstreamId)
9
+ }
10
+
11
+ async countActiveRuns(workstreamId: RecordIdInput): Promise<number> {
12
+ const runs = await this.listActiveRuns(workstreamId)
13
+ return runs.length
14
+ }
15
+
16
+ async listActivePlans(workstreamId: RecordIdInput): Promise<SerializableExecutionPlan[]> {
17
+ const runs = await this.listActiveRuns(workstreamId)
18
+ return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run)))
19
+ }
20
+ }
21
+
22
+ export const workstreamPlanRegistryService = new WorkstreamPlanRegistryService()
@@ -10,6 +10,8 @@ import {
10
10
  } from '../system-agents/title-generator.agent'
11
11
  import { workstreamService } from './workstream.service'
12
12
 
13
+ const WORKSTREAM_TITLE_TIMEOUT_MS = 5_000
14
+
13
15
  class WorkstreamTitleService {
14
16
  helperRuntime = createHelperModelRuntime()
15
17
 
@@ -21,7 +23,7 @@ class WorkstreamTitleService {
21
23
  tag: 'workstream-title',
22
24
  createAgent: createWorkstreamTitleGeneratorAgent,
23
25
  defaultSystemPrompt: WORKSTREAM_TITLE_GENERATOR_PROMPT,
24
- timeoutMs: 30_000,
26
+ timeoutMs: WORKSTREAM_TITLE_TIMEOUT_MS,
25
27
  messages: [{ role: 'user', content: sourceText }],
26
28
  }),
27
29
  )
@@ -1,4 +1,5 @@
1
1
  import {
2
+ WORKSTREAM,
2
3
  baseChatMessageSchema,
3
4
  CONSULT_SPECIALIST_TOOL_NAME,
4
5
  CONSULT_TEAM_TOOL_NAME,
@@ -10,7 +11,7 @@ import {
10
11
  } from '@lota-sdk/shared'
11
12
  import type { ChatMessage, MessageMetadata } from '@lota-sdk/shared'
12
13
  import { convertToModelMessages, readUIMessageStream, stepCountIs, tool as createTool, validateUIMessages } from 'ai'
13
- import type { PrepareStepFunction, StopCondition, ToolSet, UIMessageStreamWriter } from 'ai'
14
+ import type { PrepareStepFunction, StopCondition, ToolLoopAgent, ToolSet, UIMessageStreamWriter } from 'ai'
14
15
  import type { z } from 'zod'
15
16
 
16
17
  import type { CoreWorkstreamProfile } from '../config/agent-defaults'
@@ -21,7 +22,6 @@ import {
21
22
  getLeadAgentId,
22
23
  getCoreWorkstreamProfile,
23
24
  getAgentRuntimeConfig,
24
- pluginRuntime,
25
25
  } from '../config/agent-defaults'
26
26
  import { lotaDebugLogger } from '../config/debug-logger'
27
27
  import { aiLogger } from '../config/logger'
@@ -55,6 +55,7 @@ import {
55
55
  shouldEnqueueOnboardingPostChatMemory,
56
56
  shouldEnqueueRegularDigestForWorkstream,
57
57
  } from '../runtime/memory-digest-policy'
58
+ import { buildIndexedRepositoriesContext, getPluginService } from '../runtime/plugin-resolution'
58
59
  import { getRuntimeAdapters, getToolProviders, getTurnHooks } from '../runtime/runtime-extensions'
59
60
  import { shouldEnqueueSkillExtraction } from '../runtime/skill-extraction-policy'
60
61
  import { finalizeTurnRun } from '../runtime/turn-lifecycle'
@@ -86,12 +87,11 @@ import { contextCompactionRuntime } from './context-compaction-runtime.singleton
86
87
  import { executionPlanService } from './execution-plan.service'
87
88
  import { learnedSkillService } from './learned-skill.service'
88
89
  import { memoryService } from './memory.service'
90
+ import { planRunService } from './plan-run.service'
89
91
  import { recentActivityService } from './recent-activity.service'
90
92
  import { workstreamMessageService } from './workstream-message.service'
91
93
  import { workstreamService } from './workstream.service'
92
94
 
93
- type AgentRuntimeConfig = Record<string, unknown>
94
- type AgentFactory = Record<string, (...args: unknown[]) => Record<string, (...args: unknown[]) => unknown>>
95
95
  type ChatStreamChunk = Parameters<UIMessageStreamWriter<ChatMessage>['write']>[0]
96
96
 
97
97
  interface UIMessageStreamResult {
@@ -106,45 +106,8 @@ function hasUIMessageStream(value: unknown): value is UIMessageStreamResult {
106
106
  )
107
107
  }
108
108
 
109
- function getPluginService(path: string[]): ((...args: unknown[]) => unknown) | undefined {
110
- let current: unknown = pluginRuntime
111
- let owner: unknown = undefined
112
- for (const key of path) {
113
- if (current === null || current === undefined || typeof current !== 'object') return undefined
114
- owner = current
115
- current = (current as Record<string, unknown>)[key]
116
- }
117
- if (typeof current !== 'function') {
118
- return undefined
119
- }
120
-
121
- return owner && typeof owner === 'object'
122
- ? (current as (...args: unknown[]) => unknown).bind(owner)
123
- : (current as (...args: unknown[]) => unknown)
124
- }
125
-
126
- async function buildIndexedRepositoriesContext(
127
- workspaceId: string,
128
- ): Promise<{ provideRepoTool: boolean; defaultSectionsByAgent: Record<string, unknown>; context: string }> {
129
- const buildContext = getRuntimeAdapters().workstream?.buildIndexedRepositoriesContext
130
- if (!buildContext) {
131
- return { provideRepoTool: false, defaultSectionsByAgent: {}, context: '' }
132
- }
133
-
134
- const context = await buildContext(workspaceId)
135
- return {
136
- provideRepoTool: context.provideRepoTool,
137
- defaultSectionsByAgent: context.defaultSectionsByAgent,
138
- context: context.context ?? '',
139
- }
140
- }
141
-
142
109
  const PRESEEDED_MEMORY_LOOKUP_LIMIT = 3
143
110
 
144
- function parsePersistedWorkstreamState(value: unknown): WorkstreamState | null {
145
- return parseWorkstreamState(value)
146
- }
147
-
148
111
  function stripExecutionPlanFieldsFromWorkstreamState(
149
112
  state: WorkstreamState | null | undefined,
150
113
  hasExecutionPlan: boolean,
@@ -155,7 +118,7 @@ function stripExecutionPlanFieldsFromWorkstreamState(
155
118
  }
156
119
 
157
120
  async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
158
- return await waitForCompactionIfNeeded({
121
+ return waitForCompactionIfNeeded({
159
122
  entityId: recordIdToString(workstreamId, TABLES.WORKSTREAM),
160
123
  entityLabel: 'Workstream',
161
124
  loadEntity: () => workstreamService.getById(workstreamId),
@@ -397,18 +360,18 @@ async function streamAgentResponse(
397
360
  optionalInstructionSection(agentResolution?.extraInstructions),
398
361
  ),
399
362
  context: ctx.buildContextResult,
400
- }) as AgentRuntimeConfig
363
+ }) as Record<string, unknown>
401
364
  agentTimer.step('build-agent-config')
402
365
  const modelMessages = await convertToModelMessages(streamParams.messages, { ignoreIncompleteToolCalls: true })
403
366
  agentTimer.step('convert-model-messages')
404
- const agent = (createAgent as unknown as AgentFactory)[config.id as string]({
367
+ const agent = createAgent[config.id as string]({
405
368
  mode: streamParams.mode,
406
369
  tools: streamParams.tools,
407
370
  extraInstructions: config.extraInstructions,
408
371
  stopWhen: (agentResolution?.stopWhen as StopCondition<ToolSet> | Array<StopCondition<ToolSet>> | undefined) ??
409
372
  streamParams.stopWhen ?? [stepCountIs(config.maxSteps as number)],
410
373
  prepareStep: (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
411
- })
374
+ }) as ToolLoopAgent<never, ToolSet>
412
375
  const agentAbortSignal = streamParams.abortSignal ?? ctx.runAbortSignal
413
376
  agentTimer.step('agent-construction')
414
377
 
@@ -443,7 +406,7 @@ async function streamAgentResponse(
443
406
  sendSources: true,
444
407
  messageMetadata: createAgentMessageMetadata({ agentId: resolvedAgentId, agentName: config.displayName as string }),
445
408
  onFinish: ({ responseMessage: finishedResponseMessage }: { responseMessage: ChatMessage }) => {
446
- responseMessage = withMessageCreatedAt(finishedResponseMessage)
409
+ responseMessage = withMessageCreatedAt(finishedResponseMessage, Date.now())
447
410
  resolveFinishedStream()
448
411
  },
449
412
  }) as ReadableStream<ChatStreamChunk>
@@ -474,7 +437,7 @@ async function streamAgentResponse(
474
437
  }
475
438
 
476
439
  for (const toolError of collectToolOutputErrors({ responseMessage: responseMessage })) {
477
- aiLogger.warn`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
440
+ aiLogger.error`Tool execution failed (agent=${resolvedAgentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
478
441
  }
479
442
 
480
443
  return responseMessage
@@ -657,7 +620,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
657
620
  const shouldProcessPostRunSideEffects =
658
621
  params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn' || shouldPersistInputMessage
659
622
  if (params.kind === 'userTurn') {
660
- inputMessage = hydrateMessageFileUrls(withMessageCreatedAt(params.inputMessage))
623
+ inputMessage = hydrateMessageFileUrls(withMessageCreatedAt(params.inputMessage, Date.now()))
661
624
  if (inputMessage.role !== 'user') {
662
625
  throw new WorkstreamTurnError('Only user messages can be submitted to the workstream runtime.', 400)
663
626
  }
@@ -691,7 +654,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
691
654
  timer.step('persist-approval-message')
692
655
  }
693
656
 
694
- const initialWorkstreamState = parsePersistedWorkstreamState(workstreamRecord.state)
657
+ const initialWorkstreamState = parseWorkstreamState(workstreamRecord.state)
695
658
  const persistedCompactionCursor = toOptionalTrimmedString(workstreamRecord.lastCompactedMessageId) ?? undefined
696
659
  const persistedLiveHistoryPromise = workstreamMessageService.listMessagesAfterCursor(
697
660
  workstreamRef,
@@ -703,10 +666,10 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
703
666
  if (inputMessage) {
704
667
  userMessage = {
705
668
  ...inputMessage,
706
- id: inputMessage.id || Bun.randomUUIDv7(),
669
+ id: inputMessage.id,
707
670
  role: 'user',
708
671
  parts: inputMessage.parts,
709
- metadata: { ...inputMessage.metadata, createdAt: toTimestamp(inputMessage.metadata?.createdAt) ?? Date.now() },
672
+ metadata: { ...inputMessage.metadata, createdAt: toTimestamp(inputMessage.metadata?.createdAt) },
710
673
  }
711
674
  }
712
675
 
@@ -758,6 +721,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
758
721
  workstream.mode === 'group' &&
759
722
  !workstream.core &&
760
723
  workstreamRecord.nameGenerated !== true &&
724
+ workstreamRecord.title === WORKSTREAM.DEFAULT_TITLE &&
761
725
  messageText.length > 0
762
726
  ) {
763
727
  void safeEnqueue(
@@ -896,18 +860,21 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
896
860
  let workstreamState = initialWorkstreamState
897
861
  const executionPlanInstructionSectionCache = createExecutionPlanInstructionSectionCache({
898
862
  disabled: onboardingActive,
899
- loadPlan: async () => await executionPlanService.getActivePlanForWorkstream(workstreamRef),
863
+ loadPlans: async () => {
864
+ const runs = await planRunService.getActiveRunRecords(workstreamRef)
865
+ return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run)))
866
+ },
900
867
  })
901
- const getExecutionPlan = async () => await executionPlanInstructionSectionCache.getPlan()
868
+ const getExecutionPlans = async () => await executionPlanInstructionSectionCache.getPlans()
902
869
  const getExecutionPlanInstructionSections = async (): Promise<string[] | undefined> =>
903
870
  await executionPlanInstructionSectionCache.getSections()
904
871
  const invalidateExecutionPlanInstructionSections = () => {
905
872
  executionPlanInstructionSectionCache.invalidate()
906
873
  }
907
874
  const getWorkstreamStateSection = async (): Promise<string | undefined> => {
908
- const executionPlan = await getExecutionPlan()
875
+ const executionPlans = await getExecutionPlans()
909
876
  return contextCompactionRuntime.formatWorkstreamStateForPrompt(
910
- stripExecutionPlanFieldsFromWorkstreamState(workstreamState, Boolean(executionPlan)),
877
+ stripExecutionPlanFieldsFromWorkstreamState(workstreamState, executionPlans.length > 0),
911
878
  )
912
879
  }
913
880
  const respondedBy = recordIdToString(userRef, TABLES.USER)
@@ -1018,10 +985,10 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1018
985
  })
1019
986
 
1020
987
  const commitAssistantResponse = async (response: ChatMessage, agentId: string, agentName: string) => {
1021
- const committed = withMessageCreatedAt({
1022
- ...response,
1023
- metadata: { ...response.metadata, ...buildAgentMetadataPatch(agentId, agentName) },
1024
- })
988
+ const committed = withMessageCreatedAt(
989
+ { ...response, metadata: { ...response.metadata, ...buildAgentMetadataPatch(agentId, agentName) } },
990
+ Date.now(),
991
+ )
1025
992
 
1026
993
  await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [committed] })
1027
994
  currentMessages = upsertChatHistoryMessage(currentMessages, committed)
@@ -1063,6 +1030,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1063
1030
  }): Promise<ChatMessage> => {
1064
1031
  const visibleTimer = lotaDebugLogger.timer(`visible:${runParams.agentId}`)
1065
1032
  let runMemoryBlock = memoryBlock
1033
+ const includeExecutionPlanTools = runParams.mode !== 'fixedWorkstreamMode' && !onboardingActive
1066
1034
  const tools: ToolSet = {
1067
1035
  ...((await buildAgentTools({
1068
1036
  agentId: runParams.agentId,
@@ -1084,7 +1052,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1084
1052
  runMemoryBlock = value
1085
1053
  },
1086
1054
  availableUploads: listReadableUploads(runParams.extraMessages),
1087
- includeExecutionPlanTools: runParams.mode !== 'fixedWorkstreamMode',
1055
+ includeExecutionPlanTools,
1088
1056
  onExecutionPlanChanged: invalidateExecutionPlanInstructionSections,
1089
1057
  context: buildContextResult,
1090
1058
  })) as ToolSet),
@@ -1107,7 +1075,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1107
1075
  visibleTimer.step('stream-agent-response')
1108
1076
  memoryBlock = runMemoryBlock
1109
1077
 
1110
- return await commitAssistantResponse(
1078
+ return commitAssistantResponse(
1111
1079
  responseMessage,
1112
1080
  runParams.agentId,
1113
1081
  agentDisplayNames[runParams.agentId] ?? runParams.agentId,
@@ -1182,14 +1150,14 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1182
1150
  hookInstructionSections,
1183
1151
  ),
1184
1152
  context: buildContextResult,
1185
- }) as AgentRuntimeConfig
1153
+ }) as Record<string, unknown>
1186
1154
  const observer = createObserver(agentId)
1187
- const agent = (createAgent as unknown as AgentFactory)[specialistConfig.id as string]({
1155
+ const agent = createAgent[specialistConfig.id as string]({
1188
1156
  mode: 'fixedWorkstreamMode',
1189
1157
  tools: { ...(specialistTools as ToolSet), ...toolProviders },
1190
1158
  extraInstructions: specialistConfig.extraInstructions,
1191
1159
  stopWhen: [stepCountIs(specialistConfig.maxSteps as number)],
1192
- })
1160
+ }) as ToolLoopAgent<never, ToolSet>
1193
1161
  const modelMessages = await convertToModelMessages(buildRunInputMessages([specialistTaskMessage]), {
1194
1162
  ignoreIncompleteToolCalls: true,
1195
1163
  })
@@ -1225,7 +1193,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1225
1193
  }),
1226
1194
  }) as ReadableStream<never>,
1227
1195
  })) {
1228
- finalMessage = withMessageCreatedAt(message)
1196
+ finalMessage = withMessageCreatedAt(message, Date.now())
1229
1197
  yield finalMessage
1230
1198
  }
1231
1199
 
@@ -1301,7 +1269,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1301
1269
  } finally {
1302
1270
  try {
1303
1271
  const latestWorkstreamRecord = await workstreamService.getById(workstreamRef)
1304
- const latestPersistedState = parsePersistedWorkstreamState(latestWorkstreamRecord.state)
1272
+ const latestPersistedState = parseWorkstreamState(latestWorkstreamRecord.state)
1305
1273
 
1306
1274
  await finalizeTurnRun({
1307
1275
  serverRunId,
@@ -99,7 +99,7 @@ function requireDirectAgentId(agentId: string | undefined): string {
99
99
  return agentId
100
100
  }
101
101
 
102
- function requirestring(coreType: string | undefined): string {
102
+ function requireString(coreType: string | undefined): string {
103
103
  if (!coreType) {
104
104
  throw new Error('Core workstreams require a coreType')
105
105
  }
@@ -174,7 +174,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
174
174
  return options.title
175
175
  }
176
176
  if (core) {
177
- return getCoreWorkstreamProfile(requirestring(coreType)).config.title
177
+ return getCoreWorkstreamProfile(requireString(coreType)).config.title
178
178
  }
179
179
  if (mode === 'direct') {
180
180
  return getAgentDisplayName(requireDirectAgentId(directAgentId))
@@ -220,7 +220,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
220
220
  }
221
221
 
222
222
  if (core) {
223
- const resolvedCoreType = requirestring(coreType)
223
+ const resolvedCoreType = requireString(coreType)
224
224
  const coreProfile = getCoreWorkstreamProfile(resolvedCoreType)
225
225
  const coreWorkstreamId = buildCoreWorkstreamId({ userId, orgId, coreType: resolvedCoreType })
226
226
  const existing = await this.findById(coreWorkstreamId)
@@ -273,6 +273,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
273
273
  core: false,
274
274
  title,
275
275
  status: 'regular',
276
+ nameGenerated: options?.title !== undefined && options.title !== WORKSTREAM.DEFAULT_TITLE,
276
277
  })
277
278
 
278
279
  return this.normalizeWorkstream(groupWorkstream)
@@ -294,7 +295,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
294
295
  )
295
296
 
296
297
  const hasStandardGroupWorkstream = existingWorkstreams.some(
297
- (workstream) => workstream.mode === 'group' && workstream.core !== true,
298
+ (workstream) => workstream.mode === 'group' && !workstream.core,
298
299
  )
299
300
  const directWorkstreamsByAgent = new Map<string, WorkstreamRecord>()
300
301
  const coreWorkstreamsByType = new Map<string, WorkstreamRecord>()
@@ -303,7 +304,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
303
304
  directWorkstreamsByAgent.set(workstream.agentId, workstream)
304
305
  }
305
306
  for (const workstream of existingWorkstreams) {
306
- if (workstream.mode !== 'group' || workstream.core !== true) continue
307
+ if (workstream.mode !== 'group' || !workstream.core) continue
307
308
  if (typeof workstream.coreType !== 'string') continue
308
309
  coreWorkstreamsByType.set(workstream.coreType, workstream)
309
310
  }
@@ -457,14 +458,10 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
457
458
  return this.normalizeWorkstream(workstream)
458
459
  }
459
460
 
460
- async getWorkstreamRecord(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
461
- return await this.getById(workstreamId)
462
- }
463
-
464
461
  async updateTitle(workstreamId: RecordIdRef, title: string): Promise<NormalizedWorkstream> {
465
462
  const existing = await this.getById(workstreamId)
466
463
  this.assertMutableWorkstream(existing, 'rename')
467
- const workstream = await this.update(workstreamId, { title })
464
+ const workstream = await this.update(workstreamId, { title, nameGenerated: true })
468
465
  return this.normalizeWorkstream(workstream)
469
466
  }
470
467
 
@@ -501,9 +498,8 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
501
498
  }
502
499
 
503
500
  async clearActiveRunIdIfMatches(workstreamId: RecordIdRef, runId: string): Promise<void> {
504
- const activeRunId = await this.getActiveRunId(workstreamId)
505
- if (activeRunId !== runId) return
506
- await this.setActiveRunId(workstreamId, null)
501
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
502
+ await databaseService.query(surql`UPDATE ONLY ${workstreamRef} SET activeRunId = NONE WHERE activeRunId = ${runId}`)
507
503
  }
508
504
 
509
505
  async setActiveStreamId(workstreamId: RecordIdRef, streamId: string): Promise<void> {
@@ -523,13 +519,10 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
523
519
  }
524
520
 
525
521
  async clearActiveStreamIdIfMatches(workstreamId: RecordIdRef, streamId: string): Promise<void> {
526
- const activeStreamId = await this.getActiveStreamId(workstreamId)
527
- if (activeStreamId !== streamId) return
528
522
  const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
529
- await databaseService.query<unknown>(surql`
530
- UPDATE ONLY ${workstreamRef}
531
- SET activeStreamId = NONE
532
- `)
523
+ await databaseService.query(
524
+ surql`UPDATE ONLY ${workstreamRef} SET activeStreamId = NONE WHERE activeStreamId = ${streamId}`,
525
+ )
533
526
  }
534
527
 
535
528
  async stopActiveRun(workstreamId: RecordIdRef): Promise<boolean> {
@@ -659,7 +652,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
659
652
  throw new Error(`Invalid record id for table ${table}`)
660
653
  }
661
654
 
662
- return recordIdToString(id, String(table))
655
+ return recordIdToString(id, table)
663
656
  }
664
657
 
665
658
  formatMemoryBlockForPrompt(workstream: Pick<WorkstreamRecord, 'memoryBlock' | 'memoryBlockSummary'>): string {
@@ -670,7 +663,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
670
663
  }
671
664
 
672
665
  private getDefaultTitle(workstream: Pick<WorkstreamRecord, 'core' | 'coreType'>): string {
673
- if (workstream.core === true && typeof workstream.coreType === 'string') {
666
+ if (workstream.core && typeof workstream.coreType === 'string') {
674
667
  return getCoreWorkstreamProfile(workstream.coreType).config.title
675
668
  }
676
669
 
@@ -683,10 +676,10 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
683
676
  ? workstream.activeRunId
684
677
  : null
685
678
  const isCompacting = workstream.isCompacting === true
686
- const mode = typeof workstream.mode === 'string' ? workstream.mode : 'group'
687
- const core = workstream.core === true
679
+ const mode = workstream.mode
680
+ const core = workstream.core
688
681
  const coreType = core && typeof workstream.coreType === 'string' ? workstream.coreType : undefined
689
- const status = typeof workstream.status === 'string' ? workstream.status : 'regular'
682
+ const status = workstream.status
690
683
  return {
691
684
  id: this.normalizeWorkstreamId(workstream.id),
692
685
  userId: this.normalizeRecordIdString(workstream.userId, TABLES.USER),
@@ -694,7 +687,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
694
687
  mode,
695
688
  core,
696
689
  ...(coreType ? { coreType } : {}),
697
- nameGenerated: workstream.nameGenerated === true,
690
+ nameGenerated: workstream.nameGenerated,
698
691
  isRunning: activeRunId !== null,
699
692
  isCompacting,
700
693
  ...(isAgentName(workstream.agentId) ? { agentId: workstream.agentId } : {}),
@@ -706,35 +699,20 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
706
699
  }
707
700
  }
708
701
 
709
- toPublicWorkstream(workstream: NormalizedWorkstream | WorkstreamRecord) {
710
- const id = typeof workstream.id === 'string' ? workstream.id : this.normalizeWorkstreamId(workstream.id)
711
- const createdAt = toIsoDateTimeString(workstream.createdAt)
712
- const updatedAt = toIsoDateTimeString(workstream.updatedAt)
713
- const activeRunId =
714
- 'activeRunId' in workstream &&
715
- typeof workstream.activeRunId === 'string' &&
716
- workstream.activeRunId.trim().length > 0
717
- ? workstream.activeRunId
718
- : null
719
- const isRunning = 'isRunning' in workstream ? workstream.isRunning : activeRunId !== null
720
- const isCompacting = workstream.isCompacting === true
721
- const mode = typeof workstream.mode === 'string' ? workstream.mode : 'group'
722
- const core = workstream.core === true
723
- const coreType = core && typeof workstream.coreType === 'string' ? workstream.coreType : undefined
724
- const nameGenerated = 'nameGenerated' in workstream ? workstream.nameGenerated === true : false
702
+ toPublicWorkstream(workstream: NormalizedWorkstream) {
725
703
  return {
726
- id,
727
- mode,
728
- core,
729
- ...(coreType ? { coreType } : {}),
730
- ...(isAgentName(workstream.agentId) ? { agentId: workstream.agentId } : {}),
731
- title: workstream.title ?? this.getDefaultTitle(workstream),
732
- status: workstream.status ?? 'regular',
733
- nameGenerated,
734
- isRunning,
735
- isCompacting,
736
- createdAt,
737
- updatedAt,
704
+ id: workstream.id,
705
+ mode: workstream.mode,
706
+ core: workstream.core,
707
+ ...(workstream.coreType ? { coreType: workstream.coreType } : {}),
708
+ ...(workstream.agentId ? { agentId: workstream.agentId } : {}),
709
+ title: workstream.title,
710
+ status: workstream.status,
711
+ nameGenerated: workstream.nameGenerated,
712
+ isRunning: workstream.isRunning,
713
+ isCompacting: workstream.isCompacting,
714
+ createdAt: workstream.createdAt,
715
+ updatedAt: workstream.updatedAt,
738
716
  }
739
717
  }
740
718
 
@@ -745,7 +723,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
745
723
  SET turnCount += 1
746
724
  RETURN turnCount
747
725
  `)
748
- return result[0]?.turnCount ?? 0
726
+ return result[0].turnCount
749
727
  }
750
728
 
751
729
  async persistGeneratedTitle(workstreamId: RecordIdRef, title: string): Promise<void> {
@@ -759,7 +737,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
759
737
  if (workstream.mode === 'direct') {
760
738
  throw new Error(`Direct workstreams cannot be ${action}d`)
761
739
  }
762
- if (workstream.core === true) {
740
+ if (workstream.core) {
763
741
  throw new Error(`Core workstreams cannot be ${action}d`)
764
742
  }
765
743
  }
@@ -1,4 +1,4 @@
1
- import { sdkWorkstreamStatusSchema } from '@lota-sdk/shared'
1
+ import { recordIdSchema, sdkWorkstreamStatusSchema } from '@lota-sdk/shared'
2
2
  import { z } from 'zod'
3
3
 
4
4
  const WorkstreamModeSchema = z.enum(['direct', 'group'])
@@ -23,27 +23,27 @@ export interface NormalizedWorkstream {
23
23
  }
24
24
 
25
25
  export const WorkstreamSchema = z.object({
26
- id: z.unknown(), // SurrealDB RecordId — validated structurally at DB boundary
27
- mode: WorkstreamModeSchema.optional().default('group'),
28
- core: z.boolean().optional().default(false),
26
+ id: recordIdSchema,
27
+ mode: WorkstreamModeSchema,
28
+ core: z.boolean(),
29
29
  coreType: CoreWorkstreamTypeSchema.nullish(),
30
30
  agentId: z.string().nullish(),
31
31
  title: z.string().nullish(),
32
- status: sdkWorkstreamStatusSchema.nullish(),
32
+ status: sdkWorkstreamStatusSchema,
33
33
  memoryBlock: z.string().nullish(),
34
34
  memoryBlockSummary: z.string().nullish(),
35
35
  activeRunId: z.string().nullish(),
36
36
  activeStreamId: z.string().nullish(),
37
37
  compactionSummary: z.string().nullish(),
38
38
  lastCompactedMessageId: z.string().nullish(),
39
- nameGenerated: z.boolean().optional().default(false), // Ideally `isNameGenerated`, but maps directly to SurrealDB column `nameGenerated`
39
+ nameGenerated: z.boolean(), // Ideally `isNameGenerated`, but maps directly to SurrealDB column `nameGenerated`
40
40
  isCompacting: z.boolean().optional(),
41
41
  state: z.unknown().optional(),
42
- turnCount: z.number().int().optional().default(0),
42
+ turnCount: z.number().int(),
43
43
  createdAt: z.coerce.date(),
44
44
  updatedAt: z.coerce.date(),
45
- userId: z.unknown(), // SurrealDB RecordId — validated structurally at DB boundary
46
- organizationId: z.unknown(), // SurrealDB RecordId — validated structurally at DB boundary
45
+ userId: recordIdSchema,
46
+ organizationId: recordIdSchema,
47
47
  })
48
48
 
49
49
  export type WorkstreamRecord = z.infer<typeof WorkstreamSchema>
@@ -0,0 +1,81 @@
1
+ import type { PlanDataSchema, PlanNodeSpec, PlanSchemaRegistry, WriteIntent } from '@lota-sdk/shared'
2
+
3
+ import { validateSchemaValue } from './plan-validator.service'
4
+
5
+ export interface WriteValidationIssue {
6
+ code: string
7
+ message: string
8
+ path?: string
9
+ }
10
+
11
+ export interface WriteValidationResult {
12
+ status: 'pass' | 'fail'
13
+ issues: WriteValidationIssue[]
14
+ suggestion?: string
15
+ validatedAt: string
16
+ }
17
+
18
+ class WriteIntentValidatorService {
19
+ validate(params: {
20
+ intent: WriteIntent
21
+ nodeSpec: PlanNodeSpec
22
+ schemaRegistry: PlanSchemaRegistry
23
+ existingDeliverables: Map<string, unknown>
24
+ }): WriteValidationResult {
25
+ const issues: WriteValidationIssue[] = []
26
+ const { intent, nodeSpec, schemaRegistry, existingDeliverables } = params
27
+
28
+ if (intent.targetPath.startsWith('structuredOutput')) {
29
+ if (nodeSpec.outputSchemaRef) {
30
+ const schema = schemaRegistry[nodeSpec.outputSchemaRef] as PlanDataSchema | undefined
31
+ if (schema) {
32
+ const schemaIssues = validateSchemaValue({ schema, value: intent.payload, path: intent.targetPath })
33
+ for (const message of schemaIssues) {
34
+ issues.push({ code: 'schema_validation_failed', message })
35
+ }
36
+ }
37
+ }
38
+ return this.buildResult(issues)
39
+ }
40
+
41
+ const deliverable = nodeSpec.deliverables.find((d) => d.name === intent.targetPath)
42
+ if (!deliverable) {
43
+ issues.push({
44
+ code: 'unknown_deliverable',
45
+ message: `"${intent.targetPath}" does not match any declared deliverable.`,
46
+ })
47
+ return this.buildResult(issues)
48
+ }
49
+
50
+ if (deliverable.schemaRef) {
51
+ const schema = schemaRegistry[deliverable.schemaRef] as PlanDataSchema | undefined
52
+ if (schema) {
53
+ const schemaIssues = validateSchemaValue({ schema, value: intent.payload, path: intent.targetPath })
54
+ for (const message of schemaIssues) {
55
+ issues.push({ code: 'schema_validation_failed', message })
56
+ }
57
+ }
58
+ }
59
+
60
+ if (intent.action === 'update' && !existingDeliverables.has(intent.targetPath)) {
61
+ issues.push({
62
+ code: 'update_target_not_found',
63
+ message: `Cannot update "${intent.targetPath}" — no prior write exists.`,
64
+ })
65
+ }
66
+
67
+ return this.buildResult(issues)
68
+ }
69
+
70
+ private buildResult(issues: WriteValidationIssue[]): WriteValidationResult {
71
+ const hasFailed = issues.length > 0
72
+ return {
73
+ status: hasFailed ? 'fail' : 'pass',
74
+ issues,
75
+ ...(hasFailed ? { suggestion: issues.map((i) => i.message).join('; ') } : {}),
76
+ validatedAt: new Date().toISOString(),
77
+ }
78
+ }
79
+ }
80
+
81
+ export const writeIntentValidatorService = new WriteIntentValidatorService()
@@ -64,7 +64,7 @@ export async function extractAttachmentText(file: File): Promise<string> {
64
64
  return normalizeExtractedText((await extractPdfPages(file)).join('\n\n'))
65
65
  }
66
66
  if (isDocxAttachmentFile(file)) {
67
- return await extractDocxText(file)
67
+ return extractDocxText(file)
68
68
  }
69
69
  return ''
70
70
  }
@@ -3,7 +3,7 @@ function sanitizeFilename(name: string): string {
3
3
  return ascii.replace(/[\\/:"*?<>|]+/g, '-').trim()
4
4
  }
5
5
 
6
- function toSafeSegment(value: string): string {
6
+ export function toSafeSegment(value: string): string {
7
7
  const cleaned = value.replace(/[^a-zA-Z0-9._-]/g, '-')
8
8
  return cleaned.length > 0 ? cleaned : 'unknown'
9
9
  }