@lota-sdk/core 0.1.15 → 0.1.17

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 (159) 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 +12 -8
  9. package/src/ai/definitions.ts +81 -3
  10. package/src/ai/embedding-cache.ts +2 -4
  11. package/src/ai/index.ts +0 -2
  12. package/src/bifrost/bifrost.ts +2 -7
  13. package/src/bifrost/cache-headers.ts +8 -0
  14. package/src/bifrost/index.ts +1 -0
  15. package/src/config/agent-defaults.ts +31 -21
  16. package/src/config/agent-types.ts +11 -0
  17. package/src/config/constants.ts +2 -14
  18. package/src/config/debug-logger.ts +5 -1
  19. package/src/config/index.ts +3 -0
  20. package/src/config/model-constants.ts +16 -34
  21. package/src/config/search.ts +1 -15
  22. package/src/create-runtime.ts +269 -178
  23. package/src/db/cursor-pagination.ts +3 -6
  24. package/src/db/index.ts +2 -0
  25. package/src/db/memory-store.helpers.ts +1 -3
  26. package/src/db/memory-store.rows.ts +7 -7
  27. package/src/db/memory-store.ts +14 -18
  28. package/src/db/memory.ts +13 -13
  29. package/src/db/schema-fingerprint.ts +1 -3
  30. package/src/db/service.ts +153 -79
  31. package/src/db/startup.ts +6 -10
  32. package/src/db/surreal-mutation.ts +43 -0
  33. package/src/db/tables.ts +7 -0
  34. package/src/db/workstream-message-row.ts +15 -0
  35. package/src/embeddings/provider.ts +1 -1
  36. package/src/queues/context-compaction.queue.ts +15 -46
  37. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  38. package/src/queues/document-processor.queue.ts +2 -4
  39. package/src/queues/index.ts +3 -0
  40. package/src/queues/memory-consolidation.queue.ts +16 -51
  41. package/src/queues/plan-scheduler.queue.ts +97 -0
  42. package/src/queues/post-chat-memory.queue.ts +20 -55
  43. package/src/queues/queue-factory.ts +100 -0
  44. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  45. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  46. package/src/queues/skill-extraction.queue.ts +15 -47
  47. package/src/queues/workstream-title-generation.queue.ts +15 -47
  48. package/src/redis/connection.ts +6 -0
  49. package/src/redis/index.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +1 -2
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +109 -35
  53. package/src/runtime/approval-continuation.ts +12 -6
  54. package/src/runtime/context-compaction-runtime.ts +1 -1
  55. package/src/runtime/context-compaction.ts +24 -64
  56. package/src/runtime/execution-plan.ts +22 -18
  57. package/src/runtime/graph-designer.ts +15 -0
  58. package/src/runtime/helper-model.ts +9 -197
  59. package/src/runtime/index.ts +3 -1
  60. package/src/runtime/llm-content.ts +1 -1
  61. package/src/runtime/memory-block.ts +9 -11
  62. package/src/runtime/memory-pipeline.ts +6 -9
  63. package/src/runtime/plugin-resolution.ts +35 -0
  64. package/src/runtime/plugin-types.ts +72 -0
  65. package/src/runtime/retrieval-adapters.ts +1 -1
  66. package/src/runtime/runtime-config.ts +111 -14
  67. package/src/runtime/runtime-extensions.ts +2 -3
  68. package/src/runtime/runtime-worker-registry.ts +6 -0
  69. package/src/runtime/social-chat.ts +752 -0
  70. package/src/runtime/team-consultation-orchestrator.ts +45 -32
  71. package/src/runtime/team-consultation-prompts.ts +11 -2
  72. package/src/runtime/title-helpers.ts +2 -4
  73. package/src/runtime/workstream-chat-helpers.ts +1 -1
  74. package/src/services/adaptive-playbook.service.ts +152 -0
  75. package/src/services/agent-executor.service.ts +292 -0
  76. package/src/services/artifact-provenance.service.ts +172 -0
  77. package/src/services/attachment.service.ts +6 -11
  78. package/src/services/context-compaction.service.ts +72 -55
  79. package/src/services/context-enrichment.service.ts +33 -0
  80. package/src/services/coordination-registry.service.ts +117 -0
  81. package/src/services/document-chunk.service.ts +2 -4
  82. package/src/services/domain-agent-executor.service.ts +71 -0
  83. package/src/services/execution-plan.service.ts +269 -50
  84. package/src/services/feedback-loop.service.ts +96 -0
  85. package/src/services/global-orchestrator.service.ts +148 -0
  86. package/src/services/index.ts +27 -0
  87. package/src/services/institutional-memory.service.ts +145 -0
  88. package/src/services/learned-skill.service.ts +24 -5
  89. package/src/services/memory-assessment.service.ts +3 -2
  90. package/src/services/memory-utils.ts +3 -8
  91. package/src/services/memory.service.ts +49 -61
  92. package/src/services/monitoring-window.service.ts +86 -0
  93. package/src/services/mutating-approval.service.ts +1 -1
  94. package/src/services/node-workspace.service.ts +155 -0
  95. package/src/services/notification.service.ts +39 -0
  96. package/src/services/organization-member.service.ts +11 -4
  97. package/src/services/organization.service.ts +5 -5
  98. package/src/services/ownership-dispatcher.service.ts +403 -0
  99. package/src/services/plan-approval.service.ts +1 -1
  100. package/src/services/plan-builder.service.ts +1 -0
  101. package/src/services/plan-checkpoint.service.ts +30 -2
  102. package/src/services/plan-compiler.service.ts +5 -0
  103. package/src/services/plan-coordination.service.ts +152 -0
  104. package/src/services/plan-cycle.service.ts +284 -0
  105. package/src/services/plan-deadline.service.ts +287 -0
  106. package/src/services/plan-executor.service.ts +384 -40
  107. package/src/services/plan-run.service.ts +41 -7
  108. package/src/services/plan-scheduler.service.ts +240 -0
  109. package/src/services/plan-template.service.ts +117 -0
  110. package/src/services/plan-validator.service.ts +84 -2
  111. package/src/services/plan-workspace.service.ts +83 -0
  112. package/src/services/playbook-registry.service.ts +67 -0
  113. package/src/services/plugin-executor.service.ts +103 -0
  114. package/src/services/quality-metrics.service.ts +132 -0
  115. package/src/services/recent-activity.service.ts +28 -34
  116. package/src/services/skill-resolver.service.ts +19 -0
  117. package/src/services/social-chat-history.service.ts +197 -0
  118. package/src/services/system-executor.service.ts +105 -0
  119. package/src/services/workstream-message.service.ts +13 -37
  120. package/src/services/workstream-plan-registry.service.ts +22 -0
  121. package/src/services/workstream-title.service.ts +3 -1
  122. package/src/services/workstream-turn-preparation.service.ts +34 -89
  123. package/src/services/workstream.service.ts +33 -55
  124. package/src/services/workstream.types.ts +9 -9
  125. package/src/services/write-intent-validator.service.ts +81 -0
  126. package/src/storage/attachment-parser.ts +1 -1
  127. package/src/storage/attachment-utils.ts +1 -1
  128. package/src/storage/generated-document-storage.service.ts +3 -2
  129. package/src/system-agents/context-compaction.agent.ts +2 -0
  130. package/src/system-agents/delegated-agent-factory.ts +5 -0
  131. package/src/system-agents/memory-reranker.agent.ts +4 -2
  132. package/src/system-agents/memory.agent.ts +2 -0
  133. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -0
  134. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -0
  135. package/src/system-agents/skill-extractor.agent.ts +2 -0
  136. package/src/system-agents/skill-manager.agent.ts +2 -0
  137. package/src/system-agents/title-generator.agent.ts +2 -0
  138. package/src/tools/execution-plan.tool.ts +17 -23
  139. package/src/tools/index.ts +0 -1
  140. package/src/tools/research-topic.tool.ts +2 -0
  141. package/src/tools/team-think.tool.ts +5 -6
  142. package/src/utils/async.ts +2 -1
  143. package/src/utils/date-time.ts +4 -32
  144. package/src/utils/env.ts +8 -0
  145. package/src/utils/errors.ts +42 -10
  146. package/src/utils/index.ts +9 -0
  147. package/src/utils/string.ts +114 -1
  148. package/src/workers/index.ts +1 -0
  149. package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
  150. package/src/workers/regular-chat-memory-digest.runner.ts +45 -12
  151. package/src/workers/skill-extraction.runner.ts +26 -6
  152. package/src/workers/utils/file-section-chunker.ts +2 -1
  153. package/src/workers/utils/repo-structure-extractor.ts +2 -2
  154. package/src/workers/utils/repomix-file-sections.ts +2 -2
  155. package/src/workers/utils/sandbox-error.ts +11 -2
  156. package/src/workers/utils/workstream-message-query.ts +14 -25
  157. package/src/workers/worker-utils.ts +2 -2
  158. package/src/runtime/workstream-routing-policy.ts +0 -267
  159. package/src/tools/log-hello-world.tool.ts +0 -17
@@ -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
  }
@@ -1,8 +1,9 @@
1
1
  import { S3Client } from 'bun'
2
2
 
3
3
  import { getRuntimeConfig } from '../runtime/runtime-config'
4
+ import { toSafeSegment } from './attachment-utils'
4
5
 
5
- function toSafeSegment(value: string): string {
6
+ function toSafePathSegment(value: string): string {
6
7
  return value
7
8
  .trim()
8
9
  .replace(/\\/g, '/')
@@ -18,7 +19,7 @@ function buildGeneratedDocumentStorageKey(params: {
18
19
  }): string {
19
20
  const safeOrganizationId = toSafeSegment(params.organizationId)
20
21
  const safeNamespace = toSafeSegment(params.namespace)
21
- const safeRelativePath = toSafeSegment(params.relativePath)
22
+ const safeRelativePath = toSafePathSegment(params.relativePath)
22
23
  return `${safeOrganizationId}/generated/${safeNamespace}/${safeRelativePath}`
23
24
  }
24
25
 
@@ -1,6 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { bifrostOpenRouterResponseHealingModel } from '../bifrost/bifrost'
4
+ import { buildBifrostCacheHeaders } from '../bifrost/cache-headers'
4
5
  import {
5
6
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
6
7
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -40,6 +41,7 @@ export function createContextCompactionAgent(options: CreateHelperToolLoopAgentO
40
41
  return new ToolLoopAgent({
41
42
  id: 'context-compaction',
42
43
  model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
44
+ headers: buildBifrostCacheHeaders('context-compaction'),
43
45
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
44
46
  ...resolveHelperAgentOptions(options, { instructions: CONTEXT_COMPACTION_PROMPT }),
45
47
  })
@@ -3,6 +3,7 @@ import type { ModelMessage, LanguageModel, ToolLoopAgentSettings, ToolSet } from
3
3
  import { z } from 'zod'
4
4
 
5
5
  import type { ToolDefinition } from '../ai/definitions'
6
+ import { aiLogger } from '../config/logger'
6
7
  import { isRecord } from '../utils/string'
7
8
  import { assertSubstantiveAgentResult } from './agent-result'
8
9
 
@@ -16,6 +17,7 @@ interface DelegatedAgentDefinition {
16
17
  providerOptions?: AgentProviderOptions
17
18
  instructions: string
18
19
  tools?: ToolSet
20
+ headers?: Record<string, string>
19
21
  maxSteps?: number
20
22
  maxOutputTokens?: number
21
23
  temperature?: number
@@ -136,6 +138,7 @@ async function generateSubstantiveDelegatedAgentResult(params: {
136
138
  try {
137
139
  return assertSubstantiveAgentResult(result.text, params.label)
138
140
  } catch (error) {
141
+ aiLogger.error`Delegated agent returned non-substantive result (label=${params.label}, attempt=${attempt + 1}, textLength=${result.text.length}, textPreview=${result.text.slice(0, 200)})`
139
142
  lastError = error
140
143
  if (params.abortSignal?.aborted) {
141
144
  throw error
@@ -163,6 +166,7 @@ export function createDelegatedAgentTool(definition: DelegatedAgentDefinition):
163
166
  id: definition.id,
164
167
  model: resolveAgentModel(definition.model),
165
168
  ...(definition.providerOptions ? { providerOptions: definition.providerOptions } : {}),
169
+ ...(definition.headers ? { headers: definition.headers } : {}),
166
170
  instructions: buildDelegatedAgentInstructions(definition.instructions, agentTools),
167
171
  tools: agentTools,
168
172
  maxOutputTokens: definition.maxOutputTokens ?? DEFAULT_DELEGATED_AGENT_MAX_OUTPUT_TOKENS,
@@ -204,6 +208,7 @@ export function createDelegatedAgentToolWithContext<TContext>(
204
208
  id: definition.id,
205
209
  model: resolveAgentModel(definition.model),
206
210
  ...(definition.providerOptions ? { providerOptions: definition.providerOptions } : {}),
211
+ ...(definition.headers ? { headers: definition.headers } : {}),
207
212
  instructions: buildDelegatedAgentInstructions(definition.instructions, agentTools),
208
213
  tools: agentTools,
209
214
  maxOutputTokens: definition.maxOutputTokens ?? DEFAULT_DELEGATED_AGENT_MAX_OUTPUT_TOKENS,
@@ -1,9 +1,10 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { bifrostOpenRouterResponseHealingModel } from '../bifrost/bifrost'
4
+ import { buildBifrostCacheHeaders } from '../bifrost/cache-headers'
4
5
  import {
5
6
  OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
6
- OPENROUTER_STRUCTURED_REASONING_MODEL_ID,
7
+ OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
7
8
  } from '../config/model-constants'
8
9
  import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
9
10
  import { resolveHelperAgentOptions } from './helper-agent-options'
@@ -31,7 +32,8 @@ Set every item.relevance as a string; use empty string when no reason is needed.
31
32
  export function createMemoryRerankerAgent(options: CreateHelperToolLoopAgentOptions) {
32
33
  return new ToolLoopAgent({
33
34
  id: 'memory-reranker',
34
- model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_REASONING_MODEL_ID),
35
+ model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
36
+ headers: buildBifrostCacheHeaders('memory-reranker'),
35
37
  providerOptions: OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
36
38
  ...resolveHelperAgentOptions(options),
37
39
  })
@@ -1,6 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { bifrostOpenRouterResponseHealingModel } from '../bifrost/bifrost'
4
+ import { buildBifrostCacheHeaders } from '../bifrost/cache-headers'
4
5
  import {
5
6
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
6
7
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -52,6 +53,7 @@ export function createOrgMemoryAgent(options: CreateHelperToolLoopAgentOptions)
52
53
  return new ToolLoopAgent({
53
54
  id: 'org-memory',
54
55
  model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
56
+ headers: buildBifrostCacheHeaders('org-memory'),
55
57
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
56
58
  ...resolveHelperAgentOptions(options),
57
59
  })
@@ -1,6 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { bifrostModel } from '../bifrost/bifrost'
4
+ import { buildBifrostCacheHeaders } from '../bifrost/cache-headers'
4
5
  import { getLeadAgentDisplayName } from '../config/agent-defaults'
5
6
  import {
6
7
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
@@ -79,6 +80,7 @@ export function createRecentActivityTitleRefinerAgent(options: CreateHelperToolL
79
80
  return new ToolLoopAgent({
80
81
  id: 'recent-activity-title-refiner',
81
82
  model: bifrostModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
83
+ headers: buildBifrostCacheHeaders('recent-activity-title-refiner'),
82
84
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
83
85
  ...resolveHelperAgentOptions(options, {
84
86
  instructions: buildRecentActivityTitleRefinerPrompt(),
@@ -1,6 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { bifrostOpenRouterResponseHealingModel } from '../bifrost/bifrost'
4
+ import { buildBifrostCacheHeaders } from '../bifrost/cache-headers'
4
5
  import {
5
6
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
6
7
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -66,6 +67,7 @@ export function createRegularChatMemoryDigestAgent(options: CreateHelperToolLoop
66
67
  return new ToolLoopAgent({
67
68
  id: 'regular-chat-memory-digest',
68
69
  model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
70
+ headers: buildBifrostCacheHeaders('regular-chat-memory-digest'),
69
71
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
70
72
  ...resolveHelperAgentOptions(options, {
71
73
  instructions: regularChatMemoryDigestPrompt,
@@ -2,6 +2,7 @@ import { ToolLoopAgent } from 'ai'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { bifrostOpenRouterResponseHealingModel } from '../bifrost/bifrost'
5
+ import { buildBifrostCacheHeaders } from '../bifrost/cache-headers'
5
6
  import {
6
7
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
7
8
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -79,6 +80,7 @@ export function createSkillExtractorAgent(options: CreateHelperToolLoopAgentOpti
79
80
  return new ToolLoopAgent({
80
81
  id: 'skill-extractor',
81
82
  model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
83
+ headers: buildBifrostCacheHeaders('skill-extractor'),
82
84
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
83
85
  ...resolveHelperAgentOptions(options, {
84
86
  instructions: skillExtractorPrompt,
@@ -2,6 +2,7 @@ import { ToolLoopAgent } from 'ai'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { bifrostOpenRouterResponseHealingModel } from '../bifrost/bifrost'
5
+ import { buildBifrostCacheHeaders } from '../bifrost/cache-headers'
5
6
  import {
6
7
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
7
8
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -69,6 +70,7 @@ export function createSkillManagerAgent(options: CreateHelperToolLoopAgentOption
69
70
  return new ToolLoopAgent({
70
71
  id: 'skill-manager',
71
72
  model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
73
+ headers: buildBifrostCacheHeaders('skill-manager'),
72
74
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
73
75
  ...resolveHelperAgentOptions(options, {
74
76
  instructions: skillManagerPrompt,
@@ -1,6 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { bifrostModel } from '../bifrost/bifrost'
4
+ import { buildBifrostCacheHeaders } from '../bifrost/cache-headers'
4
5
  import {
5
6
  OPENROUTER_FAST_REASONING_MODEL_ID,
6
7
  OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
@@ -33,6 +34,7 @@ export function createWorkstreamTitleGeneratorAgent(options: CreateHelperToolLoo
33
34
  return new ToolLoopAgent({
34
35
  id: 'workstream-title-generator',
35
36
  model: bifrostModel(OPENROUTER_FAST_REASONING_MODEL_ID),
37
+ headers: buildBifrostCacheHeaders('workstream-title-generator'),
36
38
  providerOptions: OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
37
39
  ...resolveHelperAgentOptions(options, {
38
40
  instructions: WORKSTREAM_TITLE_GENERATOR_PROMPT,
@@ -1,35 +1,17 @@
1
1
  import {
2
2
  CreateExecutionPlanArgsSchema,
3
3
  GetActiveExecutionPlanArgsSchema,
4
+ ListExecutionPlansArgsSchema,
4
5
  ResumeExecutionPlanRunArgsSchema,
5
6
  ReplaceExecutionPlanArgsSchema,
6
7
  SubmitExecutionNodeResultArgsSchema,
8
+ getLatestExecutionPlanResult,
7
9
  } from '@lota-sdk/shared'
8
- import type { ExecutionPlanToolResultData } from '@lota-sdk/shared'
9
10
  import { tool } from 'ai'
10
11
 
11
12
  import type { RecordIdRef } from '../db/record-id'
12
13
  import { executionPlanService } from '../services/execution-plan.service'
13
14
 
14
- function isExecutionPlanResult(value: unknown): value is ExecutionPlanToolResultData {
15
- return value !== null && value !== undefined && typeof value === 'object' && 'hasPlan' in value
16
- }
17
-
18
- function getLatestExecutionPlanToolResult(output: unknown): ExecutionPlanToolResultData | undefined {
19
- if (isExecutionPlanResult(output)) {
20
- return output
21
- }
22
-
23
- if (Array.isArray(output)) {
24
- for (let index = output.length - 1; index >= 0; index -= 1) {
25
- const candidate = getLatestExecutionPlanToolResult(output[index])
26
- if (candidate) return candidate
27
- }
28
- }
29
-
30
- return undefined
31
- }
32
-
33
15
  export function createCreateExecutionPlanTool(params: {
34
16
  orgId: RecordIdRef
35
17
  workstreamId: RecordIdRef
@@ -45,6 +27,7 @@ export function createCreateExecutionPlanTool(params: {
45
27
  organizationId: params.orgId,
46
28
  workstreamId: params.workstreamId,
47
29
  leadAgentId: params.agentId,
30
+ dispatchMode: 'deferred',
48
31
  input,
49
32
  })
50
33
  params.onPlanChanged?.()
@@ -68,6 +51,7 @@ export function createReplaceExecutionPlanTool(params: {
68
51
  organizationId: params.orgId,
69
52
  workstreamId: params.workstreamId,
70
53
  leadAgentId: params.agentId,
54
+ dispatchMode: 'deferred',
71
55
  input,
72
56
  })
73
57
  params.onPlanChanged?.()
@@ -95,21 +79,31 @@ export function createSubmitExecutionNodeResultTool(params: {
95
79
  return result
96
80
  },
97
81
  toModelOutput: ({ output }) => {
98
- const result = getLatestExecutionPlanToolResult(output)
82
+ const result = getLatestExecutionPlanResult(output)
99
83
  const summary = result?.message?.trim()
100
84
  return { type: 'text', value: summary && summary.length > 0 ? summary : 'Execution node result submitted.' }
101
85
  },
102
86
  })
103
87
  }
104
88
 
105
- export function createGetActiveExecutionPlanTool(params: { workstreamId: RecordIdRef }) {
89
+ export function createListExecutionPlansTool(params: { workstreamId: RecordIdRef }) {
90
+ return tool({
91
+ description:
92
+ 'List all active execution plans for this workstream with summary info (title, status, objective, node counts). Use getExecutionPlanDetails to inspect a specific plan.',
93
+ inputSchema: ListExecutionPlansArgsSchema,
94
+ execute: async () => await executionPlanService.listActivePlanSummaries(params.workstreamId),
95
+ })
96
+ }
97
+
98
+ export function createGetExecutionPlanDetailsTool(params: { workstreamId: RecordIdRef }) {
106
99
  return tool({
107
100
  description:
108
- 'Load the active execution run for this workstream, including graph state, node contracts, recent events, approvals, artifacts, and checkpoints when requested.',
101
+ 'Load a specific execution run by runId, or the most recent active run if runId is omitted. Returns full graph state, node contracts, events, approvals, artifacts, and checkpoints.',
109
102
  inputSchema: GetActiveExecutionPlanArgsSchema,
110
103
  execute: async (input) =>
111
104
  await executionPlanService.getActivePlanToolResult({
112
105
  workstreamId: params.workstreamId,
106
+ runId: input.runId,
113
107
  includeEvents: input.includeEvents,
114
108
  includeArtifacts: input.includeArtifacts,
115
109
  includeApprovals: input.includeApprovals,
@@ -1,6 +1,5 @@
1
1
  export * from './execution-plan.tool'
2
2
  export * from './fetch-webpage.tool'
3
- export * from './log-hello-world.tool'
4
3
  export * from './memory-block.tool'
5
4
  export * from './read-file-parts.tool'
6
5
  export * from './remember-memory.tool'
@@ -1,4 +1,5 @@
1
1
  import { bifrostChatModel } from '../bifrost/bifrost'
2
+ import { buildBifrostCacheHeaders } from '../bifrost/cache-headers'
2
3
  import {
3
4
  OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS,
4
5
  OPENROUTER_WEB_RESEARCH_MODEL_ID,
@@ -14,6 +15,7 @@ export const researchTopicTool = createDelegatedAgentTool({
14
15
  'Delegate a research task to a dedicated research agent that searches the web, fetches pages, and returns a synthesized markdown report. Call multiple instances in parallel for broad research across different topics.',
15
16
  model: () => bifrostChatModel(OPENROUTER_WEB_RESEARCH_MODEL_ID),
16
17
  providerOptions: OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS,
18
+ headers: buildBifrostCacheHeaders('researchTopic'),
17
19
  instructions: RESEARCHER_PROMPT,
18
20
  tools: { searchWeb: searchWebTool.create(), fetchWebpage: fetchWebpageTool.create() },
19
21
  })