@lota-sdk/core 0.4.10 → 0.4.11

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 (108) hide show
  1. package/package.json +2 -2
  2. package/src/ai-gateway/ai-gateway.ts +149 -95
  3. package/src/ai-gateway/index.ts +16 -1
  4. package/src/config/agent-defaults.ts +4 -120
  5. package/src/config/logger.ts +18 -34
  6. package/src/config/thread-defaults.ts +1 -18
  7. package/src/create-runtime.ts +90 -28
  8. package/src/db/base.service.ts +30 -38
  9. package/src/db/service.ts +489 -545
  10. package/src/effect/index.ts +0 -2
  11. package/src/effect/layers.ts +6 -13
  12. package/src/embeddings/provider.ts +2 -7
  13. package/src/index.ts +4 -5
  14. package/src/queues/autonomous-job.queue.ts +159 -113
  15. package/src/queues/context-compaction.queue.ts +39 -25
  16. package/src/queues/delayed-node-promotion.queue.ts +56 -29
  17. package/src/queues/document-processor.queue.ts +5 -3
  18. package/src/queues/index.ts +1 -0
  19. package/src/queues/memory-consolidation.queue.ts +79 -53
  20. package/src/queues/organization-learning.queue.ts +63 -39
  21. package/src/queues/plan-agent-heartbeat.queue.ts +104 -79
  22. package/src/queues/plan-scheduler.queue.ts +100 -84
  23. package/src/queues/post-chat-memory.queue.ts +55 -33
  24. package/src/queues/queue-factory.ts +40 -41
  25. package/src/queues/queues.service.ts +61 -0
  26. package/src/queues/title-generation.queue.ts +42 -31
  27. package/src/redis/org-memory-lock.ts +24 -9
  28. package/src/redis/redis-lease-lock.ts +8 -1
  29. package/src/runtime/agent-identity-overrides.ts +7 -3
  30. package/src/runtime/agent-runtime-policy.ts +9 -4
  31. package/src/runtime/agent-stream-helpers.ts +9 -4
  32. package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
  33. package/src/runtime/context-compaction/context-compaction.ts +9 -7
  34. package/src/runtime/domain-layer.ts +15 -4
  35. package/src/runtime/execution-plan-visibility.ts +5 -2
  36. package/src/runtime/graph-designer.ts +0 -22
  37. package/src/runtime/index.ts +1 -0
  38. package/src/runtime/indexed-repositories-policy.ts +2 -6
  39. package/src/runtime/plugin-resolution.ts +29 -12
  40. package/src/runtime/post-turn-side-effects.ts +139 -141
  41. package/src/runtime/runtime-config.ts +0 -6
  42. package/src/runtime/runtime-extensions.ts +0 -54
  43. package/src/runtime/runtime-lifecycle.ts +4 -4
  44. package/src/runtime/runtime-services.ts +122 -53
  45. package/src/runtime/runtime-worker-registry.ts +113 -30
  46. package/src/runtime/social-chat/social-chat-agent-runner.ts +6 -3
  47. package/src/runtime/social-chat/social-chat-history.ts +3 -1
  48. package/src/runtime/social-chat/social-chat.ts +35 -20
  49. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +6 -5
  50. package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
  51. package/src/runtime/thread-chat-helpers.ts +18 -9
  52. package/src/runtime/thread-turn-context.ts +7 -47
  53. package/src/runtime/turn-lifecycle.ts +6 -14
  54. package/src/services/agent-activity.service.ts +168 -175
  55. package/src/services/agent-executor.service.ts +35 -16
  56. package/src/services/attachment.service.ts +4 -70
  57. package/src/services/autonomous-job.service.ts +53 -61
  58. package/src/services/context-compaction.service.ts +7 -9
  59. package/src/services/execution-plan/execution-plan-graph.ts +106 -115
  60. package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
  61. package/src/services/execution-plan/execution-plan.service.ts +67 -50
  62. package/src/services/global-orchestrator.service.ts +18 -7
  63. package/src/services/graph-full-routing.ts +7 -6
  64. package/src/services/memory/memory-conversation.ts +10 -5
  65. package/src/services/memory/memory.service.ts +11 -8
  66. package/src/services/ownership-dispatcher.service.ts +16 -5
  67. package/src/services/plan/plan-agent-heartbeat.service.ts +29 -15
  68. package/src/services/plan/plan-agent-query.service.ts +12 -8
  69. package/src/services/plan/plan-completion-side-effects.ts +93 -101
  70. package/src/services/plan/plan-cycle.service.ts +7 -45
  71. package/src/services/plan/plan-deadline.service.ts +28 -17
  72. package/src/services/plan/plan-event-delivery.service.ts +47 -40
  73. package/src/services/plan/plan-executor-context.ts +2 -0
  74. package/src/services/plan/plan-executor-graph.ts +366 -391
  75. package/src/services/plan/plan-executor.service.ts +13 -91
  76. package/src/services/plan/plan-scheduler.service.ts +62 -49
  77. package/src/services/plan/plan-transaction-events.ts +1 -1
  78. package/src/services/recent-activity-title.service.ts +6 -2
  79. package/src/services/thread/thread-bootstrap.ts +11 -9
  80. package/src/services/thread/thread-message.service.ts +6 -5
  81. package/src/services/thread/thread-turn-execution.ts +86 -82
  82. package/src/services/thread/thread-turn-preparation.service.ts +47 -24
  83. package/src/services/thread/thread-turn-streaming.ts +20 -25
  84. package/src/services/thread/thread-turn.ts +25 -44
  85. package/src/services/thread/thread.service.ts +21 -6
  86. package/src/system-agents/recent-activity-title-refiner.agent.ts +8 -5
  87. package/src/system-agents/thread-router.agent.ts +23 -20
  88. package/src/tools/execution-plan.tool.ts +8 -3
  89. package/src/tools/fetch-webpage.tool.ts +10 -9
  90. package/src/tools/firecrawl-client.ts +0 -15
  91. package/src/tools/remember-memory.tool.ts +3 -6
  92. package/src/tools/research-topic.tool.ts +12 -3
  93. package/src/tools/search-web.tool.ts +10 -9
  94. package/src/tools/search.tool.ts +4 -5
  95. package/src/tools/team-think.tool.ts +139 -121
  96. package/src/workers/bootstrap.ts +9 -10
  97. package/src/workers/memory-consolidation.worker.ts +4 -1
  98. package/src/workers/organization-learning.worker.ts +15 -2
  99. package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
  100. package/src/workers/regular-chat-memory-digest.runner.ts +21 -14
  101. package/src/workers/skill-extraction.runner.ts +13 -15
  102. package/src/workers/worker-utils.ts +6 -18
  103. package/src/effect/awaitable-effect.ts +0 -96
  104. package/src/effect/runtime-ref.ts +0 -25
  105. package/src/effect/runtime.ts +0 -46
  106. package/src/redis/runtime-connection.ts +0 -20
  107. package/src/runtime/runtime-accessors.ts +0 -92
  108. package/src/runtime/runtime-token.ts +0 -47
@@ -10,6 +10,7 @@ import type {
10
10
  import { PlanDraftSchema, PlanRunSchema, PlanSpecSchema } from '@lota-sdk/shared'
11
11
  import { Context, Schema, Effect, Layer, Match } from 'effect'
12
12
  import { RecordId } from 'surrealdb'
13
+ import type { z } from 'zod'
13
14
 
14
15
  import type { RecordIdInput } from '../../db/record-id'
15
16
  import { ensureRecordId, recordIdToString } from '../../db/record-id'
@@ -84,6 +85,17 @@ const effectTryPromise = makeEffectTryPromiseWithMessage(
84
85
  (message, cause) => new ExecutionPlanServiceError({ message, cause }),
85
86
  )
86
87
 
88
+ function parseRowEffect<TSchema extends z.ZodTypeAny>(
89
+ schema: TSchema,
90
+ value: unknown,
91
+ label: string,
92
+ ): Effect.Effect<z.infer<TSchema>, ExecutionPlanServiceError> {
93
+ return Effect.try({
94
+ try: () => schema.parse(value) as z.infer<TSchema>,
95
+ catch: (cause) => new ExecutionPlanServiceError({ message: `Failed to parse ${label}.`, cause }),
96
+ })
97
+ }
98
+
87
99
  function toSerializablePlanEffect(
88
100
  deps: ExecutionPlanDeps,
89
101
  run: PlanRunRecord,
@@ -285,7 +297,8 @@ function createPlanEffect(
285
297
  const databaseService = deps.db
286
298
  const requireApproval =
287
299
  params.requireApproval ?? hasCrossThreadSourceContext(params.sourceThreadId, params.threadId)
288
- const preparedDraft = deps.planBuilder.prepareDraft(PlanDraftSchema.parse(params.input))
300
+ const parsedDraft = yield* parseRowEffect(PlanDraftSchema, params.input, 'plan draft input')
301
+ const preparedDraft = deps.planBuilder.prepareDraft(parsedDraft)
289
302
  const validation = deps.planValidator.validateDraft(preparedDraft)
290
303
  if (validation.blocking.length > 0) {
291
304
  return yield* new BadRequestError({
@@ -319,7 +332,7 @@ function createPlanEffect(
319
332
  .output('after'),
320
333
  'Failed to create execution plan spec.',
321
334
  )
322
- const spec = PlanSpecSchema.parse(createdSpec)
335
+ const spec = yield* parseRowEffect(PlanSpecSchema, createdSpec, 'created plan spec')
323
336
 
324
337
  yield* effectTryPromise(
325
338
  () =>
@@ -391,7 +404,8 @@ function replacePlanEffect(
391
404
 
392
405
  const activeSpec = yield* deps.planRun.getPlanSpecById(activeRun.planSpecId)
393
406
  const { runId: _runId, reason: _reason, requireApproval: requestedRequireApproval, ...draftInput } = params.input
394
- const preparedDraft = deps.planBuilder.prepareDraft(PlanDraftSchema.parse(draftInput))
407
+ const parsedDraft = yield* parseRowEffect(PlanDraftSchema, draftInput, 'plan draft replacement input')
408
+ const preparedDraft = deps.planBuilder.prepareDraft(parsedDraft)
395
409
  const validation = deps.planValidator.validateDraft(preparedDraft)
396
410
  if (validation.blocking.length > 0) {
397
411
  return yield* new BadRequestError({
@@ -419,7 +433,7 @@ function replacePlanEffect(
419
433
  .output('after'),
420
434
  'Failed to update superseded execution plan spec.',
421
435
  )
422
- const supersededSpec = PlanSpecSchema.parse(updatedSpec)
436
+ const supersededSpec = yield* parseRowEffect(PlanSpecSchema, updatedSpec, 'superseded plan spec')
423
437
 
424
438
  const updatedRun = yield* effectTryPromise(
425
439
  () =>
@@ -437,7 +451,7 @@ function replacePlanEffect(
437
451
  .output('after'),
438
452
  'Failed to abort previous execution run.',
439
453
  )
440
- const abortedRun = PlanRunSchema.parse(updatedRun)
454
+ const abortedRun = yield* parseRowEffect(PlanRunSchema, updatedRun, 'aborted plan run')
441
455
 
442
456
  const createdSpec = yield* effectTryPromise(
443
457
  () =>
@@ -456,7 +470,7 @@ function replacePlanEffect(
456
470
  .output('after'),
457
471
  'Failed to create replacement execution plan spec.',
458
472
  )
459
- const spec = PlanSpecSchema.parse(createdSpec)
473
+ const spec = yield* parseRowEffect(PlanSpecSchema, createdSpec, 'replacement plan spec')
460
474
 
461
475
  yield* effectTryPromise(
462
476
  () =>
@@ -523,18 +537,19 @@ function submitPlanTurnResultEffect(
523
537
  },
524
538
  ) {
525
539
  return Effect.gen(function* () {
526
- const result = yield* Effect.tryPromise({
527
- try: () =>
528
- deps.planExecutor.submitNodeResult({
529
- threadId: params.threadId,
530
- runId: params.runId,
531
- nodeId: params.nodeId,
532
- emittedBy: params.emittedBy,
533
- result: params.input,
534
- }),
535
- catch: (cause) =>
536
- new ExecutionPlanServiceError({ message: 'Failed to submit execution plan node result.', cause }),
537
- })
540
+ const result = yield* deps.planExecutor
541
+ .submitNodeResult({
542
+ threadId: params.threadId,
543
+ runId: params.runId,
544
+ nodeId: params.nodeId,
545
+ emittedBy: params.emittedBy,
546
+ result: params.input,
547
+ })
548
+ .pipe(
549
+ Effect.mapError(
550
+ (cause) => new ExecutionPlanServiceError({ message: 'Failed to submit execution plan node result.', cause }),
551
+ ),
552
+ )
538
553
 
539
554
  const plan = yield* finalizePlanSnapshotEffect(deps, { runId: params.runId, emittedBy: params.emittedBy })
540
555
  return buildExecutionPlanToolResult({
@@ -550,15 +565,13 @@ function resumeRunEffect(
550
565
  params: { threadId: RecordIdInput; emittedBy: string; input: { runId: string } },
551
566
  ) {
552
567
  return Effect.gen(function* () {
553
- const result = yield* Effect.tryPromise({
554
- try: () =>
555
- deps.planExecutor.resumeRun({
556
- threadId: params.threadId,
557
- runId: params.input.runId,
558
- emittedBy: params.emittedBy,
559
- }),
560
- catch: (cause) => new ExecutionPlanServiceError({ message: 'Failed to resume execution run.', cause }),
561
- })
568
+ const result = yield* deps.planExecutor
569
+ .resumeRun({ threadId: params.threadId, runId: params.input.runId, emittedBy: params.emittedBy })
570
+ .pipe(
571
+ Effect.mapError(
572
+ (cause) => new ExecutionPlanServiceError({ message: 'Failed to resume execution run.', cause }),
573
+ ),
574
+ )
562
575
 
563
576
  const plan = yield* finalizePlanSnapshotEffect(deps, { runId: params.input.runId, emittedBy: params.emittedBy })
564
577
  return buildExecutionPlanToolResult({
@@ -609,7 +622,7 @@ function approvePlanEffect(
609
622
  .output('after'),
610
623
  'Failed to activate execution run.',
611
624
  )
612
- const activatedRun = PlanRunSchema.parse(updatedRun)
625
+ const activatedRun = yield* parseRowEffect(PlanRunSchema, updatedRun, 'activated plan run')
613
626
 
614
627
  const synced = yield* effectTryPromise(
615
628
  () =>
@@ -729,7 +742,7 @@ function rejectPlanEffect(
729
742
  .output('after'),
730
743
  'Failed to abort execution run.',
731
744
  )
732
- const rejectedRun = PlanRunSchema.parse(updatedRun)
745
+ const rejectedRun = yield* parseRowEffect(PlanRunSchema, updatedRun, 'rejected plan run')
733
746
 
734
747
  yield* emitEvent({
735
748
  tx,
@@ -778,17 +791,19 @@ function respondToApprovalEffect(
778
791
  const run = yield* deps.planRun.getActiveRunRecord(params.threadId)
779
792
  if (!run) return null
780
793
 
781
- const plan = yield* Effect.tryPromise({
782
- try: () =>
783
- deps.planExecutor.submitHumanNodeResponse({
784
- threadId: params.threadId,
785
- approvalId: params.input.approvalId,
786
- respondedBy: params.emittedBy,
787
- response: params.input.response,
788
- approvalMessageId: params.input.approvalMessageId,
789
- }),
790
- catch: (cause) => new ExecutionPlanServiceError({ message: 'Failed to submit human approval response.', cause }),
791
- })
794
+ const plan = yield* deps.planExecutor
795
+ .submitHumanNodeResponse({
796
+ threadId: params.threadId,
797
+ approvalId: params.input.approvalId,
798
+ respondedBy: params.emittedBy,
799
+ response: params.input.response,
800
+ approvalMessageId: params.input.approvalMessageId,
801
+ })
802
+ .pipe(
803
+ Effect.mapError(
804
+ (cause) => new ExecutionPlanServiceError({ message: 'Failed to submit human approval response.', cause }),
805
+ ),
806
+ )
792
807
  if (!plan) return null
793
808
 
794
809
  return yield* finalizePlanSnapshotEffect(deps, { runId: run.id, emittedBy: params.emittedBy })
@@ -825,16 +840,18 @@ function applyHumanInputFromUserMessageEffect(
825
840
  )
826
841
  if (!response) return null
827
842
 
828
- const plan = yield* Effect.tryPromise({
829
- try: () =>
830
- deps.planExecutor.submitHumanNodeResponse({
831
- threadId: params.threadId,
832
- respondedBy: params.respondedBy,
833
- response,
834
- approvalMessageId: params.message.id,
835
- }),
836
- catch: (cause) => new ExecutionPlanServiceError({ message: 'Failed to submit human node response.', cause }),
837
- })
843
+ const plan = yield* deps.planExecutor
844
+ .submitHumanNodeResponse({
845
+ threadId: params.threadId,
846
+ respondedBy: params.respondedBy,
847
+ response,
848
+ approvalMessageId: params.message.id,
849
+ })
850
+ .pipe(
851
+ Effect.mapError(
852
+ (cause) => new ExecutionPlanServiceError({ message: 'Failed to submit human node response.', cause }),
853
+ ),
854
+ )
838
855
  if (!plan) return null
839
856
 
840
857
  return yield* finalizePlanSnapshotEffect(deps, { runId: run.id, emittedBy: params.respondedBy })
@@ -9,7 +9,10 @@ import type {
9
9
  } from '@lota-sdk/shared'
10
10
  import { Context, Effect, Layer } from 'effect'
11
11
 
12
- import { runPromise } from '../effect/runtime'
12
+ import type { ResolvedAgentConfig } from '../config/agent-defaults'
13
+ import { AgentConfigServiceTag } from '../effect/services'
14
+ import type { PlanAgentHeartbeatQueueRuntime } from '../queues/plan-agent-heartbeat.queue'
15
+ import { LotaQueuesServiceTag } from '../queues/queues.service'
13
16
  import { routeGraphFullEffect } from './graph-full-routing'
14
17
  import type { OwnershipDispatcherService } from './ownership-dispatcher.service'
15
18
  import { OwnershipDispatcherServiceTag } from './ownership-dispatcher.service'
@@ -55,9 +58,11 @@ function decideRerouteAction(params: {
55
58
  }
56
59
 
57
60
  interface GlobalOrchestratorDeps {
61
+ agentConfig: ResolvedAgentConfig
58
62
  ownershipDispatcherService: Pick<OwnershipDispatcherService, 'dispatchReadyNode'>
59
63
  planExecutorService: ReturnType<typeof makePlanExecutorService>
60
64
  planRunService: ReturnType<typeof makePlanRunService>
65
+ planAgentHeartbeatQueue: PlanAgentHeartbeatQueueRuntime
61
66
  }
62
67
 
63
68
  type DispatchReadyNodeParams = {
@@ -78,9 +83,11 @@ export function makeGlobalOrchestratorService(deps: GlobalOrchestratorDeps) {
78
83
  decideRerouteAction,
79
84
  routeGraphFull: (params: { threadId: string; runId: string }) =>
80
85
  routeGraphFullEffect(params, {
86
+ agentConfig: deps.agentConfig,
81
87
  dispatchReadyNode,
82
88
  planExecutorService: deps.planExecutorService,
83
89
  planRunService: deps.planRunService,
90
+ planAgentHeartbeatQueue: deps.planAgentHeartbeatQueue,
84
91
  }),
85
92
  }
86
93
  }
@@ -93,21 +100,25 @@ export class GlobalOrchestratorServiceTag extends Context.Service<
93
100
  export const GlobalOrchestratorServiceLive = Layer.effect(
94
101
  GlobalOrchestratorServiceTag,
95
102
  Effect.gen(function* () {
103
+ const agentConfig = yield* AgentConfigServiceTag
96
104
  const ownershipDispatcherService = yield* OwnershipDispatcherServiceTag
97
105
  const planExecutorService = yield* PlanExecutorServiceTag
98
106
  const planRunService = yield* PlanRunServiceTag
99
- return makeGlobalOrchestratorService({ ownershipDispatcherService, planExecutorService, planRunService })
107
+ const queues = yield* LotaQueuesServiceTag
108
+ return makeGlobalOrchestratorService({
109
+ agentConfig,
110
+ ownershipDispatcherService,
111
+ planExecutorService,
112
+ planRunService,
113
+ planAgentHeartbeatQueue: queues.planAgentHeartbeat,
114
+ })
100
115
  }),
101
116
  )
102
117
 
103
- const routeGraphFullWithRuntime = Effect.fn('GlobalOrchestrator.routeGraphFullWithRuntime')(function* (params: {
118
+ export const routeGraphFull = Effect.fn('GlobalOrchestrator.routeGraphFull')(function* (params: {
104
119
  threadId: string
105
120
  runId: string
106
121
  }) {
107
122
  const globalOrchestratorService = yield* GlobalOrchestratorServiceTag
108
123
  return yield* globalOrchestratorService.routeGraphFull(params)
109
124
  })
110
-
111
- export function routeGraphFull(params: { threadId: string; runId: string }): Promise<void> {
112
- return runPromise(routeGraphFullWithRuntime(params))
113
- }
@@ -9,11 +9,13 @@ import type {
9
9
  } from '@lota-sdk/shared'
10
10
  import { Effect } from 'effect'
11
11
 
12
+ import type { ResolvedAgentConfig } from '../config/agent-defaults'
12
13
  import { serverLogger } from '../config/logger'
13
14
  import { recordIdToString } from '../db/record-id'
14
15
  import { TABLES } from '../db/tables'
15
16
  import { BadRequestError, ServiceError } from '../effect/errors'
16
17
  import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
18
+ import type { PlanAgentHeartbeatQueueRuntime } from '../queues/plan-agent-heartbeat.queue'
17
19
  import { shouldPlanNodeUseVisibleTurn } from '../runtime/execution-plan-visibility'
18
20
  import type { makePlanExecutorService } from './plan/plan-executor.service'
19
21
  import type { makePlanRunService } from './plan/plan-run.service'
@@ -34,6 +36,7 @@ function formatDispatchError(error: unknown): string {
34
36
  const STABLE_RUN_STATUSES = new Set(['pending-approval', 'awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
35
37
 
36
38
  interface GraphFullRoutingDeps<E = never> {
39
+ agentConfig: ResolvedAgentConfig
37
40
  dispatchReadyNode(params: {
38
41
  run: PlanRunRecord
39
42
  nodeSpecRecord: PlanNodeSpecRecord
@@ -43,6 +46,7 @@ interface GraphFullRoutingDeps<E = never> {
43
46
  }): Effect.Effect<PlanNodeResultSubmission, E, never>
44
47
  planExecutorService: ReturnType<typeof makePlanExecutorService>
45
48
  planRunService: ReturnType<typeof makePlanRunService>
49
+ planAgentHeartbeatQueue: PlanAgentHeartbeatQueueRuntime
46
50
  }
47
51
 
48
52
  export function routeGraphFullEffect<E>(params: { threadId: string; runId: string }, deps: GraphFullRoutingDeps<E>) {
@@ -68,11 +72,11 @@ export function routeGraphFullEffect<E>(params: { threadId: string; runId: strin
68
72
 
69
73
  const silentNodes = readyNodes.filter((nr: PlanNodeRunRecord) => {
70
74
  const ns = nodeSpecs.find((s: PlanNodeSpecRecord) => s.nodeId === nr.nodeId)
71
- return ns && !shouldPlanNodeUseVisibleTurn(spec, ns)
75
+ return ns && !shouldPlanNodeUseVisibleTurn(deps.agentConfig, spec, ns)
72
76
  })
73
77
  const visibleNodes = readyNodes.filter((nr: PlanNodeRunRecord) => {
74
78
  const ns = nodeSpecs.find((s: PlanNodeSpecRecord) => s.nodeId === nr.nodeId)
75
- return ns && shouldPlanNodeUseVisibleTurn(spec, ns)
79
+ return ns && shouldPlanNodeUseVisibleTurn(deps.agentConfig, spec, ns)
76
80
  })
77
81
 
78
82
  yield* Effect.forEach(
@@ -86,10 +90,7 @@ export function routeGraphFullEffect<E>(params: { threadId: string; runId: strin
86
90
  )
87
91
 
88
92
  if (visibleNodes.length > 0) {
89
- const { enqueuePlanAgentHeartbeatWake } = yield* tryGraphFullPromise(
90
- () => import('../queues/plan-agent-heartbeat.queue'),
91
- 'Failed to load plan agent heartbeat queue module.',
92
- )
93
+ const enqueuePlanAgentHeartbeatWake = deps.planAgentHeartbeatQueue.enqueuePlanAgentHeartbeatWake
93
94
  const updatedRunForWake = yield* deps.planRunService.getRunById(params.runId)
94
95
  yield* Effect.forEach(
95
96
  visibleNodes,
@@ -1,4 +1,5 @@
1
- import { getAgentRoster } from '../../config/agent-defaults'
1
+ import type { ResolvedAgentConfig } from '../../config/agent-defaults'
2
+ import { isAgentName } from '../../config/agent-defaults'
2
3
  import type { ExtractedFact, Message } from '../../db/memory-types'
3
4
  import { sanitizeAgentOutputForMemory } from '../../runtime/llm-content'
4
5
  import { compactWhitespace, truncateText } from '../../utils/string'
@@ -9,8 +10,8 @@ const MAX_CONVERSATION_MEMORY_BLOCK_CHARS = 2_000
9
10
  const MAX_CONVERSATION_ATTACHMENT_CONTEXT_CHARS = 6_000
10
11
  const LOW_VALUE_MEMORY_IMPORTANCE_THRESHOLD = 0.45
11
12
 
12
- export const isRoutableAgentName = (value?: string): value is string =>
13
- Boolean(value && getAgentRoster().includes(value))
13
+ export const isRoutableAgentName = (agentConfig: ResolvedAgentConfig, value?: string): value is string =>
14
+ isAgentName(agentConfig, value)
14
15
 
15
16
  function normalizeConversationText(value: string, maxChars: number): string {
16
17
  const normalized = compactWhitespace(value)
@@ -18,7 +19,11 @@ function normalizeConversationText(value: string, maxChars: number): string {
18
19
  return truncateText(normalized, maxChars)
19
20
  }
20
21
 
21
- export function resolveAgentScopeNames(agentName?: string, agentNames: string[] = []): string[] {
22
+ export function resolveAgentScopeNames(
23
+ agentConfig: ResolvedAgentConfig,
24
+ agentName?: string,
25
+ agentNames: string[] = [],
26
+ ): string[] {
22
27
  const unique = new Set<string>()
23
28
  if (typeof agentName === 'string' && agentName.trim()) {
24
29
  unique.add(agentName.trim())
@@ -29,7 +34,7 @@ export function resolveAgentScopeNames(agentName?: string, agentNames: string[]
29
34
  if (!normalized) continue
30
35
  unique.add(normalized)
31
36
  }
32
- return [...unique].filter((name) => isRoutableAgentName(name))
37
+ return [...unique].filter((name) => isRoutableAgentName(agentConfig, name))
33
38
  }
34
39
 
35
40
  export function buildConversationMessages(params: {
@@ -1,5 +1,6 @@
1
1
  import { Context, Schema, Effect, Layer } from 'effect'
2
2
 
3
+ import type { ResolvedAgentConfig } from '../../config/agent-defaults'
3
4
  import { aiLogger } from '../../config/logger'
4
5
  import type { Memory } from '../../db/memory'
5
6
  import { isUniqueIndexConflict } from '../../db/memory-store.helpers'
@@ -12,7 +13,7 @@ import type {
12
13
  MemoryType,
13
14
  RelationType,
14
15
  } from '../../db/memory-types'
15
- import { DatabaseServiceTag, RuntimeConfigServiceTag } from '../../effect/services'
16
+ import { AgentConfigServiceTag, DatabaseServiceTag, RuntimeConfigServiceTag } from '../../effect/services'
16
17
  import { withOrgMemoryLockEffect } from '../../redis/org-memory-lock'
17
18
  import type { HelperModelRuntime } from '../../runtime/helper-model'
18
19
  import { HelperModelTag } from '../../runtime/helper-model'
@@ -177,10 +178,11 @@ interface MemoryServiceDeps {
177
178
  rerankService: ReturnType<typeof makeRerankService>
178
179
  helperModelRuntime: HelperModelRuntime
179
180
  orgMemoryCache: OrgMemoryCache
181
+ agentConfig: ResolvedAgentConfig
180
182
  }
181
183
 
182
184
  export function createMemoryService(deps: MemoryServiceDeps) {
183
- const { runtimeConfig, helperModelRuntime, orgMemoryCache } = deps
185
+ const { runtimeConfig, helperModelRuntime, orgMemoryCache, agentConfig } = deps
184
186
  const resolveRerankService = () => deps.rerankService
185
187
  const service = {
186
188
  searchOrganizationMemories: Effect.fn('MemoryService.searchOrganizationMemories')(function* (
@@ -251,7 +253,7 @@ export function createMemoryService(deps: MemoryServiceDeps) {
251
253
  agentName: string,
252
254
  query: string,
253
255
  ) {
254
- if (!isRoutableAgentName(agentName)) {
256
+ if (!isRoutableAgentName(agentConfig, agentName)) {
255
257
  aiLogger.debug`Agent memory search skipped - invalid agentName: ${agentName}`
256
258
  return 'No stored memories.'
257
259
  }
@@ -329,7 +331,7 @@ export function createMemoryService(deps: MemoryServiceDeps) {
329
331
  },
330
332
  ]
331
333
 
332
- if (isRoutableAgentName(agentName)) {
334
+ if (isRoutableAgentName(agentConfig, agentName)) {
333
335
  const agentScoped = yield* agentScopeId(orgId, agentName)
334
336
  retrievalTasks.push({
335
337
  scopeTag: `agent:${agentName}`,
@@ -449,7 +451,7 @@ export function createMemoryService(deps: MemoryServiceDeps) {
449
451
  metadata?: Record<string, unknown>
450
452
  importance?: number
451
453
  }) {
452
- if (!isRoutableAgentName(agentName)) {
454
+ if (!isRoutableAgentName(agentConfig, agentName)) {
453
455
  return Effect.fail(new InvalidAgentNameError({ agentName }))
454
456
  }
455
457
 
@@ -536,7 +538,7 @@ export function createMemoryService(deps: MemoryServiceDeps) {
536
538
  },
537
539
  ]
538
540
 
539
- for (const scopedAgentName of resolveAgentScopeNames(undefined, params.agentNames ?? [])) {
541
+ for (const scopedAgentName of resolveAgentScopeNames(agentConfig, undefined, params.agentNames ?? [])) {
540
542
  const agentScopedValue = yield* agentScopeId(params.orgId, scopedAgentName)
541
543
  scopes.push({
542
544
  scopeId: agentScopedValue,
@@ -619,7 +621,7 @@ export function createMemoryService(deps: MemoryServiceDeps) {
619
621
  },
620
622
  ]
621
623
 
622
- for (const scopedAgentName of resolveAgentScopeNames(agentName, agentNames)) {
624
+ for (const scopedAgentName of resolveAgentScopeNames(agentConfig, agentName, agentNames)) {
623
625
  const agentId = yield* agentScopeId(orgId, scopedAgentName)
624
626
  scopes.push({
625
627
  scopeId: agentId,
@@ -668,7 +670,8 @@ export const MemoryServiceLive = Layer.effect(
668
670
  const rerankService = yield* RerankServiceTag
669
671
  const helperModelRuntime = yield* HelperModelTag
670
672
  const background = yield* BackgroundWorkService
673
+ const agentConfig = yield* AgentConfigServiceTag
671
674
  const orgMemoryCache = yield* makeOrgMemoryCache({ db, runtimeConfig, helperModelRuntime, background })
672
- return createMemoryService({ runtimeConfig, rerankService, helperModelRuntime, orgMemoryCache })
675
+ return createMemoryService({ runtimeConfig, rerankService, helperModelRuntime, orgMemoryCache, agentConfig })
673
676
  }),
674
677
  )
@@ -25,6 +25,8 @@ import { TABLES } from '../db/tables'
25
25
  import { BadRequestError, ConfigurationError, DatabaseError, ServiceError } from '../effect/errors'
26
26
  import { isPromiseLike, makeEffectTryPromiseWithMessage } from '../effect/helpers'
27
27
  import { AgentConfigServiceTag, DatabaseServiceTag, RuntimeAdaptersServiceTag } from '../effect/services'
28
+ import type { PlanAgentHeartbeatQueueRuntime } from '../queues/plan-agent-heartbeat.queue'
29
+ import { LotaQueuesServiceTag } from '../queues/queues.service'
28
30
  import { resolvePlanNodeExecutionVisibility, shouldPlanNodeUseVisibleTurn } from '../runtime/execution-plan-visibility'
29
31
  import type { LotaRuntimeAdapters } from '../runtime/runtime-extensions'
30
32
  import type { makeAgentExecutorService } from './agent-executor.service'
@@ -60,6 +62,7 @@ interface OwnershipDispatcherDeps {
60
62
  skillResolver: NoContextService<Pick<SkillResolverService, 'resolve'>>
61
63
  systemExecutor: NoContextService<Pick<ReturnType<typeof makeSystemExecutorService>, 'executeNode' | 'validateOwner'>>
62
64
  user: NoContextService<Pick<ReturnType<typeof makeUserService>, 'getUser'>>
65
+ planAgentHeartbeatQueue: PlanAgentHeartbeatQueueRuntime
63
66
  }
64
67
 
65
68
  type NoContextService<TService> = {
@@ -387,9 +390,11 @@ const dispatchRunToStableBoundaryEffect = (
387
390
  yield* routeGraphFullEffect(
388
391
  { threadId: recordIdToString(run.threadId, TABLES.THREAD), runId: recordIdToString(run.id, TABLES.PLAN_RUN) },
389
392
  {
393
+ agentConfig: deps.agentConfig,
390
394
  dispatchReadyNode: dispatchReadyNodeForGraphFull,
391
395
  planExecutorService: deps.planExecutor,
392
396
  planRunService: deps.planRun,
397
+ planAgentHeartbeatQueue: deps.planAgentHeartbeatQueue,
393
398
  },
394
399
  ).pipe(
395
400
  Effect.mapError(
@@ -427,7 +432,7 @@ const dispatchRunToStableBoundaryEffect = (
427
432
  if (nodeRun.status !== 'running') {
428
433
  return yield* serializeRunEffect(deps, run.id)
429
434
  }
430
- if (shouldPlanNodeUseVisibleTurn(spec, nodeSpecRecord)) {
435
+ if (shouldPlanNodeUseVisibleTurn(deps.agentConfig, spec, nodeSpecRecord)) {
431
436
  return yield* serializeRunEffect(deps, run.id)
432
437
  }
433
438
 
@@ -503,7 +508,7 @@ const dispatchReadyNodeEffect = (
503
508
  },
504
509
  ) =>
505
510
  Effect.gen(function* () {
506
- if (shouldPlanNodeUseVisibleTurn(params.spec, params.nodeSpecRecord)) {
511
+ if (shouldPlanNodeUseVisibleTurn(deps.agentConfig, params.spec, params.nodeSpecRecord)) {
507
512
  return yield* new BadRequestError({
508
513
  message: `Node "${params.nodeSpecRecord.nodeId}" requires a visible plan turn and cannot be silently dispatched.`,
509
514
  })
@@ -531,8 +536,12 @@ const dispatchReadyNodeEffect = (
531
536
  })
532
537
  })
533
538
 
534
- function resolveExecutionVisibility(params: { spec: PlanSpecRecord; nodeSpecRecord: PlanNodeSpecRecord }) {
535
- return resolvePlanNodeExecutionVisibility(params.spec, params.nodeSpecRecord)
539
+ function resolveExecutionVisibility(params: {
540
+ agentConfig: ResolvedAgentConfig
541
+ spec: PlanSpecRecord
542
+ nodeSpecRecord: PlanNodeSpecRecord
543
+ }) {
544
+ return resolvePlanNodeExecutionVisibility(params.agentConfig, params.spec, params.nodeSpecRecord)
536
545
  }
537
546
 
538
547
  export function makeOwnershipDispatcherService(deps: OwnershipDispatcherDeps) {
@@ -553,7 +562,7 @@ export function makeOwnershipDispatcherService(deps: OwnershipDispatcherDeps) {
553
562
  return dispatchReadyNodeEffect(deps, params)
554
563
  },
555
564
  resolveExecutionVisibility(params: { spec: PlanSpecRecord; nodeSpecRecord: PlanNodeSpecRecord }) {
556
- return resolveExecutionVisibility(params)
565
+ return resolveExecutionVisibility({ ...params, agentConfig: deps.agentConfig })
557
566
  },
558
567
  dispatchNode(params: {
559
568
  nodeSpec: PlanNodeSpec
@@ -593,6 +602,7 @@ export const OwnershipDispatcherServiceLive = Layer.effect(
593
602
  const skillResolver = yield* SkillResolverServiceTag
594
603
  const systemExecutor = yield* SystemExecutorServiceTag
595
604
  const user = yield* UserServiceTag
605
+ const queues = yield* LotaQueuesServiceTag
596
606
  const executeAgentNode = agentExecutor.executeNode as OwnershipDispatcherDeps['agentExecutor']['executeNode']
597
607
  return makeOwnershipDispatcherService({
598
608
  db,
@@ -614,6 +624,7 @@ export const OwnershipDispatcherServiceLive = Layer.effect(
614
624
  executeNode: (params) => provideCurrentContext(systemExecutor.executeNode(params)),
615
625
  },
616
626
  user: { getUser: (userId) => provideCurrentContext(user.getUser(userId)) },
627
+ planAgentHeartbeatQueue: queues.planAgentHeartbeat,
617
628
  })
618
629
  }),
619
630
  )
@@ -1,10 +1,13 @@
1
1
  import { Context, Schema, Effect, Layer } from 'effect'
2
2
 
3
+ import type { ResolvedAgentConfig } from '../../config/agent-defaults'
3
4
  import { serverLogger } from '../../config/logger'
4
5
  import { ensureRecordId } from '../../db/record-id'
5
6
  import { TABLES } from '../../db/tables'
6
7
  import { effectTryPromise } from '../../effect/helpers'
7
- import { RedisServiceTag } from '../../effect/services'
8
+ import { AgentConfigServiceTag, RedisServiceTag } from '../../effect/services'
9
+ import type { PlanAgentHeartbeatQueueRuntime } from '../../queues/plan-agent-heartbeat.queue'
10
+ import { LotaQueuesServiceTag } from '../../queues/queues.service'
8
11
  import type { RedisConnectionManager } from '../../redis/connection'
9
12
  import { withLeaseLock } from '../../redis/redis-lease-lock'
10
13
  import { resolvePlanNodeExecutionVisibility } from '../../runtime/execution-plan-visibility'
@@ -39,30 +42,40 @@ class PlanAgentHeartbeatError extends Schema.TaggedErrorClass<PlanAgentHeartbeat
39
42
  cause: Schema.Defect,
40
43
  }) {}
41
44
 
42
- function tryHeartbeatPromise<A>(
45
+ function tryHeartbeatPromise<A, R = never>(
43
46
  operation: string,
44
- thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
45
- ): Effect.Effect<A, PlanAgentHeartbeatError> {
47
+ thunk: () => PromiseLike<A> | Effect.Effect<A, unknown, R>,
48
+ ): Effect.Effect<A, PlanAgentHeartbeatError, R> {
46
49
  return effectTryPromise(thunk, (cause) => new PlanAgentHeartbeatError({ operation, cause }))
47
50
  }
48
51
 
49
- function heartbeatServiceEffect<A, E>(
52
+ function heartbeatServiceEffect<A, E, R = never>(
50
53
  operation: string,
51
- effect: Effect.Effect<A, E>,
52
- ): Effect.Effect<A, PlanAgentHeartbeatError> {
54
+ effect: Effect.Effect<A, E, R>,
55
+ ): Effect.Effect<A, PlanAgentHeartbeatError, R> {
53
56
  return effect.pipe(Effect.mapError((cause) => new PlanAgentHeartbeatError({ operation, cause })))
54
57
  }
55
58
 
56
59
  interface PlanAgentHeartbeatDeps {
60
+ agentConfig: ResolvedAgentConfig
57
61
  redis: RedisConnectionManager
58
62
  planAgentQueryService: ReturnType<typeof makePlanAgentQueryService>
59
63
  planExecutorService: ReturnType<typeof makePlanExecutorService>
60
64
  planRunService: ReturnType<typeof makePlanRunService>
61
65
  threadService: ReturnType<typeof makeThreadService>
66
+ planAgentHeartbeatQueue: PlanAgentHeartbeatQueueRuntime
62
67
  }
63
68
 
64
69
  export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
65
- const { planExecutorService, planRunService, redis, planAgentQueryService, threadService } = deps
70
+ const {
71
+ agentConfig,
72
+ planExecutorService,
73
+ planRunService,
74
+ redis,
75
+ planAgentQueryService,
76
+ threadService,
77
+ planAgentHeartbeatQueue,
78
+ } = deps
66
79
 
67
80
  const wakeNodeEffect = (params: {
68
81
  organizationId: string
@@ -71,7 +84,7 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
71
84
  nodeId: string
72
85
  agentId: string
73
86
  reason: string
74
- }): Effect.Effect<boolean, PlanAgentHeartbeatError> =>
87
+ }): Effect.Effect<boolean, PlanAgentHeartbeatError, unknown> =>
75
88
  Effect.gen(function* () {
76
89
  const threadRef = ensureRecordId(params.threadId, TABLES.THREAD)
77
90
  yield* heartbeatServiceEffect(
@@ -125,7 +138,7 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
125
138
  return false
126
139
  }
127
140
 
128
- const visibility = resolvePlanNodeExecutionVisibility(spec, nodeSpec)
141
+ const visibility = resolvePlanNodeExecutionVisibility(agentConfig, spec, nodeSpec)
129
142
  if (visibility !== 'visible') {
130
143
  return false
131
144
  }
@@ -153,12 +166,9 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
153
166
  ).pipe(Effect.mapError((cause) => new PlanAgentHeartbeatError({ operation: 'wake-node-lock', cause })))
154
167
  })
155
168
 
156
- const sweepEffect = (params?: { organizationId?: string }): Effect.Effect<void, PlanAgentHeartbeatError> =>
169
+ const sweepEffect = (params?: { organizationId?: string }): Effect.Effect<void, PlanAgentHeartbeatError, unknown> =>
157
170
  Effect.gen(function* () {
158
- const { enqueuePlanAgentHeartbeatWake } = yield* tryHeartbeatPromise(
159
- 'import-heartbeat-queue',
160
- () => import('../../queues/plan-agent-heartbeat.queue'),
161
- )
171
+ const enqueuePlanAgentHeartbeatWake = planAgentHeartbeatQueue.enqueuePlanAgentHeartbeatWake
162
172
  const [actionable, recentlyUnblocked, approachingDeadlines] = yield* Effect.all([
163
173
  heartbeatServiceEffect(
164
174
  'get-actionable-nodes-for-agent',
@@ -218,17 +228,21 @@ export class PlanAgentHeartbeatServiceTag extends Context.Service<
218
228
  export const PlanAgentHeartbeatServiceLive = Layer.effect(
219
229
  PlanAgentHeartbeatServiceTag,
220
230
  Effect.gen(function* () {
231
+ const agentConfig = yield* AgentConfigServiceTag
221
232
  const redis = yield* RedisServiceTag
222
233
  const planAgentQueryService = yield* PlanAgentQueryServiceTag
223
234
  const planRunService = yield* PlanRunServiceTag
224
235
  const planExecutor = yield* PlanExecutorServiceTag
225
236
  const threadSvc = yield* ThreadServiceTag
237
+ const queues = yield* LotaQueuesServiceTag
226
238
  return makePlanAgentHeartbeatService({
239
+ agentConfig,
227
240
  redis,
228
241
  planAgentQueryService,
229
242
  planExecutorService: planExecutor,
230
243
  planRunService,
231
244
  threadService: threadSvc,
245
+ planAgentHeartbeatQueue: queues.planAgentHeartbeat,
232
246
  })
233
247
  }),
234
248
  )