@lota-sdk/core 0.4.13 → 0.4.15

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 (139) hide show
  1. package/package.json +4 -4
  2. package/src/ai/embedding-cache.ts +17 -11
  3. package/src/ai-gateway/ai-gateway.ts +164 -94
  4. package/src/ai-gateway/index.ts +4 -1
  5. package/src/config/agent-defaults.ts +2 -2
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/config/constants.ts +1 -1
  8. package/src/create-runtime.ts +259 -200
  9. package/src/db/cursor-pagination.ts +2 -9
  10. package/src/db/memory-store.ts +194 -175
  11. package/src/db/memory.ts +125 -71
  12. package/src/db/schema-fingerprint.ts +5 -4
  13. package/src/db/service-normalization.ts +4 -3
  14. package/src/db/service.ts +3 -2
  15. package/src/db/startup.ts +15 -16
  16. package/src/effect/errors.ts +161 -21
  17. package/src/effect/index.ts +0 -1
  18. package/src/embeddings/provider.ts +15 -7
  19. package/src/queues/autonomous-job.queue.ts +10 -22
  20. package/src/queues/delayed-node-promotion.queue.ts +8 -14
  21. package/src/queues/document-processor.queue.ts +13 -4
  22. package/src/queues/memory-consolidation.queue.ts +26 -14
  23. package/src/queues/plan-agent-heartbeat.queue.ts +48 -31
  24. package/src/queues/plan-scheduler.queue.ts +37 -15
  25. package/src/queues/queue-factory.ts +59 -35
  26. package/src/queues/standalone-worker.ts +3 -2
  27. package/src/redis/connection.ts +10 -3
  28. package/src/redis/org-memory-lock.ts +1 -1
  29. package/src/redis/redis-lease-lock.ts +5 -5
  30. package/src/redis/stream-context.ts +1 -1
  31. package/src/runtime/chat-message.ts +64 -1
  32. package/src/runtime/chat-run-orchestration.ts +33 -20
  33. package/src/runtime/context-compaction/context-compaction-runtime.ts +14 -7
  34. package/src/runtime/context-compaction/context-compaction.ts +78 -66
  35. package/src/runtime/domain-layer.ts +19 -13
  36. package/src/runtime/execution-plan.ts +7 -3
  37. package/src/runtime/memory/memory-block.ts +3 -9
  38. package/src/runtime/memory/memory-scope.ts +3 -1
  39. package/src/runtime/plugin-resolution.ts +2 -1
  40. package/src/runtime/post-turn-side-effects.ts +6 -5
  41. package/src/runtime/retrieval-adapters.ts +8 -20
  42. package/src/runtime/runtime-config.ts +3 -9
  43. package/src/runtime/runtime-extensions.ts +2 -4
  44. package/src/runtime/runtime-lifecycle.ts +56 -16
  45. package/src/runtime/runtime-services.ts +180 -102
  46. package/src/runtime/runtime-worker-registry.ts +3 -1
  47. package/src/runtime/social-chat/social-chat-agent-runner.ts +1 -1
  48. package/src/runtime/social-chat/social-chat-history.ts +21 -18
  49. package/src/runtime/social-chat/social-chat.ts +356 -223
  50. package/src/runtime/specialist-runner.ts +3 -1
  51. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +3 -2
  52. package/src/runtime/thread-turn-context.ts +142 -102
  53. package/src/runtime/turn-lifecycle.ts +15 -46
  54. package/src/services/agent-activity.service.ts +1 -1
  55. package/src/services/agent-executor.service.ts +107 -77
  56. package/src/services/autonomous-job.service.ts +354 -293
  57. package/src/services/background-work.service.ts +3 -3
  58. package/src/services/context-compaction.service.ts +7 -2
  59. package/src/services/document-chunk.service.ts +50 -32
  60. package/src/services/execution-plan/execution-plan-schedule.ts +5 -3
  61. package/src/services/execution-plan/execution-plan.service.ts +162 -179
  62. package/src/services/feedback-loop.service.ts +5 -4
  63. package/src/services/graph-full-routing.ts +37 -36
  64. package/src/services/institutional-memory.service.ts +28 -30
  65. package/src/services/learned-skill.service.ts +107 -72
  66. package/src/services/memory/memory-errors.ts +4 -23
  67. package/src/services/memory/memory-org-memory.ts +10 -5
  68. package/src/services/memory/memory-rerank.ts +18 -6
  69. package/src/services/memory/memory.service.ts +170 -111
  70. package/src/services/memory/rerank.service.ts +29 -20
  71. package/src/services/organization-member.service.ts +1 -1
  72. package/src/services/organization.service.ts +69 -75
  73. package/src/services/ownership-dispatcher.service.ts +40 -39
  74. package/src/services/plan/plan-agent-heartbeat.service.ts +22 -24
  75. package/src/services/plan/plan-agent-query.service.ts +39 -31
  76. package/src/services/plan/plan-completion-side-effects.ts +13 -17
  77. package/src/services/plan/plan-coordination.service.ts +2 -1
  78. package/src/services/plan/plan-cycle.service.ts +6 -5
  79. package/src/services/plan/plan-deadline.service.ts +57 -54
  80. package/src/services/plan/plan-event-delivery.service.ts +5 -4
  81. package/src/services/plan/plan-executor-graph.ts +18 -15
  82. package/src/services/plan/plan-executor.service.ts +235 -262
  83. package/src/services/plan/plan-run.service.ts +169 -93
  84. package/src/services/plan/plan-scheduler.service.ts +192 -202
  85. package/src/services/plan/plan-template.service.ts +1 -1
  86. package/src/services/plan/plan-transaction-events.ts +1 -1
  87. package/src/services/plan/plan-workspace.service.ts +23 -14
  88. package/src/services/plugin-executor.service.ts +5 -9
  89. package/src/services/queue-job.service.ts +117 -59
  90. package/src/services/recent-activity-title.service.ts +13 -12
  91. package/src/services/recent-activity.service.ts +6 -1
  92. package/src/services/social-chat-history.service.ts +29 -25
  93. package/src/services/system-executor.service.ts +5 -9
  94. package/src/services/thread/thread-active-run.ts +2 -2
  95. package/src/services/thread/thread-listing.ts +61 -57
  96. package/src/services/thread/thread-memory-block.ts +73 -48
  97. package/src/services/thread/thread-message.service.ts +76 -65
  98. package/src/services/thread/thread-record-store.ts +8 -8
  99. package/src/services/thread/thread-title.service.ts +10 -4
  100. package/src/services/thread/thread-turn-execution.ts +43 -45
  101. package/src/services/thread/thread-turn-preparation.service.ts +257 -135
  102. package/src/services/thread/thread-turn-streaming.ts +83 -92
  103. package/src/services/thread/thread-turn.ts +18 -16
  104. package/src/services/thread/thread.service.ts +135 -100
  105. package/src/services/user.service.ts +45 -48
  106. package/src/storage/attachment-parser.ts +6 -2
  107. package/src/storage/attachment-storage.service.ts +5 -6
  108. package/src/storage/generated-document-storage.service.ts +1 -1
  109. package/src/system-agents/context-compaction.agent.ts +10 -9
  110. package/src/system-agents/delegated-agent-factory.ts +30 -6
  111. package/src/system-agents/memory-reranker.agent.ts +10 -9
  112. package/src/system-agents/memory.agent.ts +10 -9
  113. package/src/system-agents/recent-activity-title-refiner.agent.ts +13 -15
  114. package/src/system-agents/regular-chat-memory-digest.agent.ts +13 -12
  115. package/src/system-agents/skill-extractor.agent.ts +13 -12
  116. package/src/system-agents/skill-manager.agent.ts +13 -12
  117. package/src/system-agents/thread-router.agent.ts +11 -7
  118. package/src/system-agents/title-generator.agent.ts +13 -12
  119. package/src/tools/fetch-webpage.tool.ts +13 -13
  120. package/src/tools/memory-block.tool.ts +3 -1
  121. package/src/tools/plan-approval.tool.ts +4 -2
  122. package/src/tools/read-file-parts.tool.ts +10 -4
  123. package/src/tools/remember-memory.tool.ts +3 -1
  124. package/src/tools/research-topic.tool.ts +9 -5
  125. package/src/tools/search-web.tool.ts +16 -16
  126. package/src/tools/search.tool.ts +20 -5
  127. package/src/tools/team-think.tool.ts +61 -38
  128. package/src/utils/async.ts +5 -5
  129. package/src/utils/errors.ts +19 -18
  130. package/src/utils/sse-keepalive.ts +28 -25
  131. package/src/workers/bootstrap.ts +75 -11
  132. package/src/workers/memory-consolidation.worker.ts +82 -91
  133. package/src/workers/organization-learning.worker.ts +14 -4
  134. package/src/workers/regular-chat-memory-digest.runner.ts +105 -67
  135. package/src/workers/skill-extraction.runner.ts +97 -61
  136. package/src/workers/utils/repo-structure-extractor.ts +13 -8
  137. package/src/workers/utils/thread-message-query.ts +24 -24
  138. package/src/workers/worker-utils.ts +23 -4
  139. package/src/effect/helpers.ts +0 -123
@@ -6,7 +6,7 @@ import { ensureRecordId, recordIdToString } from '../db/record-id'
6
6
  import type { RecordIdInput, RecordIdRef } from '../db/record-id'
7
7
  import type { SurrealDBService } from '../db/service'
8
8
  import { TABLES } from '../db/tables'
9
- import { NotFoundError } from '../effect/errors'
9
+ import { ERROR_TAGS, NotFoundError } from '../effect/errors'
10
10
  import { DatabaseServiceTag } from '../effect/services'
11
11
  import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../utils/date-time'
12
12
 
@@ -17,10 +17,10 @@ interface BackgroundCursor {
17
17
  id: string
18
18
  }
19
19
 
20
- class OrganizationServiceError extends Schema.TaggedErrorClass<OrganizationServiceError>()('OrganizationServiceError', {
21
- operation: Schema.String,
22
- cause: Schema.Defect,
23
- }) {}
20
+ class OrganizationServiceError extends Schema.TaggedErrorClass<OrganizationServiceError>()(
21
+ ERROR_TAGS.OrganizationServiceError,
22
+ { operation: Schema.String, cause: Schema.Defect },
23
+ ) {}
24
24
 
25
25
  function toOptionalCursorTimestamp(value: unknown): string | null {
26
26
  return toOptionalIsoDateTimeString(value) ?? null
@@ -58,85 +58,80 @@ function toOrganizationServiceError(operation: string, cause: unknown): Organiza
58
58
  }
59
59
 
60
60
  export function makeOrganizationService(db: SurrealDBService) {
61
- function createOrganization(params: { name: string }) {
62
- return db.create(TABLES.ORGANIZATION, { name: params.name }, sdkOrganizationRecordSchema).pipe(
63
- Effect.mapError((cause) => toOrganizationServiceError('createOrganization', cause)),
64
- Effect.flatMap(toPublicEffect),
65
- )
66
- }
61
+ const createOrganization = Effect.fn('OrganizationService.create')(function* (params: { name: string }) {
62
+ const record = yield* db
63
+ .create(TABLES.ORGANIZATION, { name: params.name }, sdkOrganizationRecordSchema)
64
+ .pipe(Effect.mapError((cause) => toOrganizationServiceError('createOrganization', cause)))
65
+ return yield* toPublicEffect(record)
66
+ })
67
67
 
68
- function upsertOrganization(params: { id: RecordIdInput; name: string }) {
68
+ const upsertOrganization = Effect.fn('OrganizationService.upsert')(function* (params: {
69
+ id: RecordIdInput
70
+ name: string
71
+ }) {
69
72
  const organizationRef = ensureRecordId(params.id, TABLES.ORGANIZATION)
70
- return db.upsert(TABLES.ORGANIZATION, organizationRef, { name: params.name }, sdkOrganizationRecordSchema).pipe(
71
- Effect.mapError((cause) => toOrganizationServiceError('upsertOrganization', cause)),
72
- Effect.flatMap(toPublicEffect),
73
- )
74
- }
73
+ const record = yield* db
74
+ .upsert(TABLES.ORGANIZATION, organizationRef, { name: params.name }, sdkOrganizationRecordSchema)
75
+ .pipe(Effect.mapError((cause) => toOrganizationServiceError('upsertOrganization', cause)))
76
+ return yield* toPublicEffect(record)
77
+ })
75
78
 
76
- function listOrganizations() {
77
- return db
79
+ const listOrganizations = Effect.fn('OrganizationService.list')(function* () {
80
+ const records = yield* db
78
81
  .findMany(TABLES.ORGANIZATION, {}, sdkOrganizationRecordSchema, { orderBy: 'createdAt', orderDir: 'ASC' })
79
- .pipe(
80
- Effect.mapError((cause) => toOrganizationServiceError('listOrganizations', cause)),
81
- Effect.flatMap((records) => Effect.forEach(records, toPublicEffect)),
82
- )
83
- }
82
+ .pipe(Effect.mapError((cause) => toOrganizationServiceError('listOrganizations', cause)))
83
+ return yield* Effect.forEach(records, toPublicEffect)
84
+ })
84
85
 
85
- function getOrganizationRecord(organizationId: RecordIdRef) {
86
- return db
86
+ const getOrganizationRecord = Effect.fn('OrganizationService.getRecord')(function* (organizationId: RecordIdRef) {
87
+ const record = yield* db
87
88
  .findOne(
88
89
  TABLES.ORGANIZATION,
89
90
  { id: ensureRecordId(organizationId, TABLES.ORGANIZATION) },
90
91
  sdkOrganizationRecordSchema,
91
92
  )
92
- .pipe(
93
- Effect.mapError((cause) => toOrganizationServiceError('getOrganizationRecord', cause)),
94
- Effect.flatMap((record) =>
95
- record ? Effect.succeed(record) : Effect.fail(organizationNotFoundError(organizationId)),
96
- ),
97
- )
98
- }
93
+ .pipe(Effect.mapError((cause) => toOrganizationServiceError('getOrganizationRecord', cause)))
94
+ if (!record) return yield* organizationNotFoundError(organizationId)
95
+ return record
96
+ })
99
97
 
100
- function getOrganization(organizationId: RecordIdInput) {
101
- return getOrganizationRecord(ensureRecordId(organizationId, TABLES.ORGANIZATION)).pipe(
102
- Effect.flatMap(toPublicEffect),
103
- )
104
- }
98
+ const getOrganization = Effect.fn('OrganizationService.get')(function* (organizationId: RecordIdInput) {
99
+ const record = yield* getOrganizationRecord(ensureRecordId(organizationId, TABLES.ORGANIZATION))
100
+ return yield* toPublicEffect(record)
101
+ })
105
102
 
106
- function updateOrganization(organizationId: RecordIdInput, params: { name: string }) {
107
- return db
103
+ const updateOrganization = Effect.fn('OrganizationService.update')(function* (
104
+ organizationId: RecordIdInput,
105
+ params: { name: string },
106
+ ) {
107
+ const updated = yield* db
108
108
  .update(
109
109
  TABLES.ORGANIZATION,
110
110
  ensureRecordId(organizationId, TABLES.ORGANIZATION),
111
111
  { name: params.name },
112
112
  sdkOrganizationRecordSchema,
113
113
  )
114
- .pipe(
115
- Effect.mapError((cause) => toOrganizationServiceError('updateOrganization', cause)),
116
- Effect.flatMap((updated) =>
117
- updated ? Effect.succeed(updated) : Effect.fail(organizationNotFoundError(organizationId)),
118
- ),
119
- Effect.flatMap(toPublicEffect),
120
- )
121
- }
114
+ .pipe(Effect.mapError((cause) => toOrganizationServiceError('updateOrganization', cause)))
115
+ if (!updated) return yield* organizationNotFoundError(organizationId)
116
+ return yield* toPublicEffect(updated)
117
+ })
122
118
 
123
- function deleteOrganization(organizationId: RecordIdInput) {
119
+ const deleteOrganization = Effect.fn('OrganizationService.delete')(function* (organizationId: RecordIdInput) {
124
120
  const organizationRef = ensureRecordId(organizationId, TABLES.ORGANIZATION)
125
- return Effect.gen(function* () {
126
- yield* db
127
- .deleteWhere(TABLES.ORGANIZATION_MEMBER, { out: organizationRef })
128
- .pipe(Effect.mapError((cause) => toOrganizationServiceError('deleteOrganization.deleteMembers', cause)))
129
- const deleted = yield* db
130
- .deleteById(TABLES.ORGANIZATION, organizationRef)
131
- .pipe(Effect.mapError((cause) => toOrganizationServiceError('deleteOrganization.deleteOrganization', cause)))
132
- if (!deleted) {
133
- return yield* organizationNotFoundError(organizationId)
134
- }
135
- })
136
- }
121
+ yield* db
122
+ .deleteWhere(TABLES.ORGANIZATION_MEMBER, { out: organizationRef })
123
+ .pipe(Effect.mapError((cause) => toOrganizationServiceError('deleteOrganization.deleteMembers', cause)))
124
+ const deleted = yield* db
125
+ .deleteById(TABLES.ORGANIZATION, organizationRef)
126
+ .pipe(Effect.mapError((cause) => toOrganizationServiceError('deleteOrganization.deleteOrganization', cause)))
127
+ if (!deleted) return yield* organizationNotFoundError(organizationId)
128
+ })
137
129
 
138
- function updateRegularChatDigestCursor(organizationId: RecordIdRef, cursor: BackgroundCursor) {
139
- return db
130
+ const updateRegularChatDigestCursor = Effect.fn('OrganizationService.updateRegularChatDigestCursor')(function* (
131
+ organizationId: RecordIdRef,
132
+ cursor: BackgroundCursor,
133
+ ) {
134
+ const updated = yield* db
140
135
  .update(
141
136
  TABLES.ORGANIZATION,
142
137
  ensureRecordId(organizationId, TABLES.ORGANIZATION),
@@ -146,25 +141,24 @@ export function makeOrganizationService(db: SurrealDBService) {
146
141
  },
147
142
  sdkOrganizationRecordSchema,
148
143
  )
149
- .pipe(
150
- Effect.mapError((cause) => toOrganizationServiceError('updateRegularChatDigestCursor', cause)),
151
- Effect.flatMap((updated) => (updated ? Effect.void : Effect.fail(organizationNotFoundError(organizationId)))),
152
- )
153
- }
144
+ .pipe(Effect.mapError((cause) => toOrganizationServiceError('updateRegularChatDigestCursor', cause)))
145
+ if (!updated) return yield* organizationNotFoundError(organizationId)
146
+ })
154
147
 
155
- function updateSkillExtractionCursor(organizationId: RecordIdRef, cursor: BackgroundCursor) {
156
- return db
148
+ const updateSkillExtractionCursor = Effect.fn('OrganizationService.updateSkillExtractionCursor')(function* (
149
+ organizationId: RecordIdRef,
150
+ cursor: BackgroundCursor,
151
+ ) {
152
+ const updated = yield* db
157
153
  .update(
158
154
  TABLES.ORGANIZATION,
159
155
  ensureRecordId(organizationId, TABLES.ORGANIZATION),
160
156
  { skillExtractionLastCursorCreatedAt: cursor.createdAt, skillExtractionLastCursorId: cursor.id },
161
157
  sdkOrganizationRecordSchema,
162
158
  )
163
- .pipe(
164
- Effect.mapError((cause) => toOrganizationServiceError('updateSkillExtractionCursor', cause)),
165
- Effect.flatMap((updated) => (updated ? Effect.void : Effect.fail(organizationNotFoundError(organizationId)))),
166
- )
167
- }
159
+ .pipe(Effect.mapError((cause) => toOrganizationServiceError('updateSkillExtractionCursor', cause)))
160
+ if (!updated) return yield* organizationNotFoundError(organizationId)
161
+ })
168
162
 
169
163
  return {
170
164
  createOrganization,
@@ -23,7 +23,6 @@ import { ensureRecordId, recordIdToString } from '../db/record-id'
23
23
  import type { SurrealDBService } from '../db/service'
24
24
  import { TABLES } from '../db/tables'
25
25
  import { BadRequestError, ConfigurationError, DatabaseError, ServiceError } from '../effect/errors'
26
- import { isPromiseLike, makeEffectTryPromiseWithMessage } from '../effect/helpers'
27
26
  import { AgentConfigServiceTag, DatabaseServiceTag, RuntimeAdaptersServiceTag } from '../effect/services'
28
27
  import type { PlanAgentHeartbeatQueueRuntime } from '../queues/plan-agent-heartbeat.queue'
29
28
  import { LotaQueuesServiceTag } from '../queues/queues.service'
@@ -142,8 +141,6 @@ function toDispatchDatabaseError(message: string, cause: unknown) {
142
141
  return new DatabaseError({ message, cause })
143
142
  }
144
143
 
145
- const tryDispatchPromise = makeEffectTryPromiseWithMessage((message, cause) => new ServiceError({ message, cause }))
146
-
147
144
  const matchDraftExecutor = (deps: OwnershipDispatcherDeps, node: { id: string; label: string }) =>
148
145
  Match.type<PlanNodeOwner>().pipe(
149
146
  Match.discriminator('executorType')('agent', (owner): PlanValidationIssueInput[] =>
@@ -181,23 +178,25 @@ const shouldAutoDispatchEffect = (deps: OwnershipDispatcherDeps, run: PlanRunRec
181
178
  return true
182
179
  }
183
180
 
184
- const workspace = yield* tryDispatchPromise(
185
- () => workspaceProvider.getWorkspace(ensureRecordId(run.organizationId, TABLES.ORGANIZATION)),
186
- 'Failed to load workspace for dispatch eligibility.',
187
- )
188
- if (!workspaceProvider.getLifecycleState) {
181
+ const workspace = yield* Effect.tryPromise({
182
+ try: () => workspaceProvider.getWorkspace(ensureRecordId(run.organizationId, TABLES.ORGANIZATION)),
183
+ catch: (cause) => new ServiceError({ message: 'Failed to load workspace for dispatch eligibility.', cause }),
184
+ })
185
+ const getLifecycleState = workspaceProvider.getLifecycleState
186
+ ? (() => {
187
+ const run = workspaceProvider.getLifecycleState.bind(workspaceProvider)
188
+ return (workspaceRecord: Record<string, unknown>) => run(workspaceRecord)
189
+ })()
190
+ : undefined
191
+ if (!getLifecycleState) {
189
192
  return true
190
193
  }
191
194
 
192
- const lifecycleState = yield* Effect.gen(function* () {
193
- const result = workspaceProvider.getLifecycleState?.call(workspaceProvider, workspace)
194
- if (isPromiseLike(result)) {
195
- return yield* tryDispatchPromise(() => result, 'Failed to read workspace lifecycle state.')
196
- }
197
-
198
- return result
195
+ const lifecycleState = yield* Effect.tryPromise({
196
+ try: () => getLifecycleState(workspace),
197
+ catch: (cause) => new ServiceError({ message: 'Failed to read workspace lifecycle state.', cause }),
199
198
  })
200
- return lifecycleState ? lifecycleState.bootstrapActive !== true : true
199
+ return lifecycleState.bootstrapActive !== true
201
200
  })
202
201
 
203
202
  const buildDispatchContextEffect = (
@@ -457,34 +456,36 @@ const dispatchRunToStableBoundaryEffect = (
457
456
  schemaRegistry: spec.schemaRegistry,
458
457
  })
459
458
 
460
- yield* tryDispatchPromise(
461
- () =>
462
- deps.planExecutor.submitNodeResult({
463
- threadId: run.threadId,
464
- runId: recordIdToString(run.id, TABLES.PLAN_RUN),
465
- nodeId: planNode.id,
466
- emittedBy: planNode.owner.ref,
467
- result,
468
- }),
469
- 'Failed to submit plan node result.',
470
- )
459
+ yield* deps.planExecutor
460
+ .submitNodeResult({
461
+ threadId: run.threadId,
462
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
463
+ nodeId: planNode.id,
464
+ emittedBy: planNode.owner.ref,
465
+ result,
466
+ })
467
+ .pipe(
468
+ Effect.mapError((cause) => new ServiceError({ message: 'Failed to submit plan node result.', cause })),
469
+ )
471
470
  }),
472
471
  )
473
472
 
474
473
  if (Exit.isFailure(dispatchExit)) {
475
474
  const failure = Cause.squash(dispatchExit.cause)
476
- yield* tryDispatchPromise(
477
- () =>
478
- deps.planExecutor.blockNodeOnDispatchFailure({
479
- threadId: run.threadId,
480
- runId: recordIdToString(run.id, TABLES.PLAN_RUN),
481
- nodeId: planNode.id,
482
- emittedBy: planNode.owner.ref,
483
- message: formatDispatchError(failure),
484
- failureClass: classifyDispatchFailure({ ownerType: planNode.owner.executorType, error: failure }),
485
- }),
486
- 'Failed to block plan node on dispatch failure.',
487
- )
475
+ yield* deps.planExecutor
476
+ .blockNodeOnDispatchFailure({
477
+ threadId: run.threadId,
478
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
479
+ nodeId: planNode.id,
480
+ emittedBy: planNode.owner.ref,
481
+ message: formatDispatchError(failure),
482
+ failureClass: classifyDispatchFailure({ ownerType: planNode.owner.executorType, error: failure }),
483
+ })
484
+ .pipe(
485
+ Effect.mapError(
486
+ (cause) => new ServiceError({ message: 'Failed to block plan node on dispatch failure.', cause }),
487
+ ),
488
+ )
488
489
  return yield* serializeRunEffect(deps, run.id)
489
490
  }
490
491
  }
@@ -4,13 +4,15 @@ import type { ResolvedAgentConfig } from '../../config/agent-defaults'
4
4
  import { serverLogger } from '../../config/logger'
5
5
  import { ensureRecordId } from '../../db/record-id'
6
6
  import { TABLES } from '../../db/tables'
7
- import { effectTryPromise } from '../../effect/helpers'
7
+ import { ERROR_TAGS } from '../../effect/errors'
8
8
  import { AgentConfigServiceTag, RedisServiceTag } from '../../effect/services'
9
9
  import type { PlanAgentHeartbeatQueueRuntime } from '../../queues/plan-agent-heartbeat.queue'
10
10
  import { LotaQueuesServiceTag } from '../../queues/queues.service'
11
11
  import type { RedisConnectionManager } from '../../redis/connection'
12
12
  import { withLeaseLock } from '../../redis/redis-lease-lock'
13
13
  import { resolvePlanNodeExecutionVisibility } from '../../runtime/execution-plan-visibility'
14
+ import type { makeThreadTurnService } from '../thread/thread-turn'
15
+ import { ThreadTurnServiceTag } from '../thread/thread-turn'
14
16
  import type { makeThreadService } from '../thread/thread.service'
15
17
  import { ThreadServiceTag } from '../thread/thread.service'
16
18
  import type { makePlanAgentQueryService } from './plan-agent-query.service'
@@ -37,17 +39,10 @@ function buildWakeDedupeKey(params: {
37
39
  return `${params.organizationId}:${params.threadId}:${params.runId}:${params.nodeId}:${params.agentId}`
38
40
  }
39
41
 
40
- class PlanAgentHeartbeatError extends Schema.TaggedErrorClass<PlanAgentHeartbeatError>()('PlanAgentHeartbeatError', {
41
- operation: Schema.String,
42
- cause: Schema.Defect,
43
- }) {}
44
-
45
- function tryHeartbeatPromise<A, R = never>(
46
- operation: string,
47
- thunk: () => PromiseLike<A> | Effect.Effect<A, unknown, R>,
48
- ): Effect.Effect<A, PlanAgentHeartbeatError, R> {
49
- return effectTryPromise(thunk, (cause) => new PlanAgentHeartbeatError({ operation, cause }))
50
- }
42
+ class PlanAgentHeartbeatError extends Schema.TaggedErrorClass<PlanAgentHeartbeatError>()(
43
+ ERROR_TAGS.PlanAgentHeartbeatError,
44
+ { operation: Schema.String, cause: Schema.Defect },
45
+ ) {}
51
46
 
52
47
  function heartbeatServiceEffect<A, E, R = never>(
53
48
  operation: string,
@@ -63,6 +58,7 @@ interface PlanAgentHeartbeatDeps {
63
58
  planExecutorService: ReturnType<typeof makePlanExecutorService>
64
59
  planRunService: ReturnType<typeof makePlanRunService>
65
60
  threadService: ReturnType<typeof makeThreadService>
61
+ threadTurnService: ReturnType<typeof makeThreadTurnService>
66
62
  planAgentHeartbeatQueue: PlanAgentHeartbeatQueueRuntime
67
63
  }
68
64
 
@@ -74,6 +70,7 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
74
70
  redis,
75
71
  planAgentQueryService,
76
72
  threadService,
73
+ threadTurnService,
77
74
  planAgentHeartbeatQueue,
78
75
  } = deps
79
76
 
@@ -84,7 +81,7 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
84
81
  nodeId: string
85
82
  agentId: string
86
83
  reason: string
87
- }): Effect.Effect<boolean, PlanAgentHeartbeatError, unknown> =>
84
+ }): Effect.Effect<boolean, PlanAgentHeartbeatError, never> =>
88
85
  Effect.gen(function* () {
89
86
  const threadRef = ensureRecordId(params.threadId, TABLES.THREAD)
90
87
  yield* heartbeatServiceEffect(
@@ -148,27 +145,23 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
148
145
  }
149
146
 
150
147
  if (nodeRun.status === 'ready' && run.currentNodeId === params.nodeId) {
151
- yield* tryHeartbeatPromise('transition-node-to-running', () =>
148
+ yield* heartbeatServiceEffect(
149
+ 'transition-node-to-running',
152
150
  planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: params.nodeId }),
153
151
  )
154
152
  }
155
153
 
156
- const { triggerPlanNodeTurn } = yield* tryHeartbeatPromise(
157
- 'import-thread-turn',
158
- () => import('../thread/thread-turn'),
159
- )
160
-
161
- yield* tryHeartbeatPromise('trigger-plan-node-turn', () =>
162
- triggerPlanNodeTurn({ runId: params.runId, nodeId: params.nodeId }),
154
+ yield* heartbeatServiceEffect(
155
+ 'trigger-plan-node-turn',
156
+ threadTurnService.triggerPlanNodeTurn({ runId: params.runId, nodeId: params.nodeId }),
163
157
  )
164
158
  return true
165
159
  }),
166
160
  ).pipe(Effect.mapError((cause) => new PlanAgentHeartbeatError({ operation: 'wake-node-lock', cause })))
167
161
  })
168
162
 
169
- const sweepEffect = (params?: { organizationId?: string }): Effect.Effect<void, PlanAgentHeartbeatError, unknown> =>
163
+ const sweepEffect = (params?: { organizationId?: string }): Effect.Effect<void, PlanAgentHeartbeatError, never> =>
170
164
  Effect.gen(function* () {
171
- const enqueuePlanAgentHeartbeatWake = planAgentHeartbeatQueue.enqueuePlanAgentHeartbeatWake
172
165
  const [actionable, recentlyUnblocked, approachingDeadlines] = yield* Effect.all([
173
166
  heartbeatServiceEffect(
174
167
  'get-actionable-nodes-for-agent',
@@ -213,7 +206,10 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
213
206
  }
214
207
 
215
208
  for (const target of wakeTargets.values()) {
216
- yield* tryHeartbeatPromise('enqueue-heartbeat-wake', () => enqueuePlanAgentHeartbeatWake(target))
209
+ yield* Effect.tryPromise({
210
+ try: () => planAgentHeartbeatQueue.enqueuePlanAgentHeartbeatWake(target),
211
+ catch: (cause) => new PlanAgentHeartbeatError({ operation: 'enqueue-heartbeat-wake', cause }),
212
+ })
217
213
  }
218
214
  })
219
215
 
@@ -234,6 +230,7 @@ export const PlanAgentHeartbeatServiceLive = Layer.effect(
234
230
  const planRunService = yield* PlanRunServiceTag
235
231
  const planExecutor = yield* PlanExecutorServiceTag
236
232
  const threadSvc = yield* ThreadServiceTag
233
+ const threadTurnSvc = yield* ThreadTurnServiceTag
237
234
  const queues = yield* LotaQueuesServiceTag
238
235
  return makePlanAgentHeartbeatService({
239
236
  agentConfig,
@@ -242,6 +239,7 @@ export const PlanAgentHeartbeatServiceLive = Layer.effect(
242
239
  planExecutorService: planExecutor,
243
240
  planRunService,
244
241
  threadService: threadSvc,
242
+ threadTurnService: threadTurnSvc,
245
243
  planAgentHeartbeatQueue: queues.planAgentHeartbeat,
246
244
  })
247
245
  }),
@@ -8,7 +8,7 @@ import type { RecordIdInput } from '../../db/record-id'
8
8
  import { ensureRecordId, recordIdToString } from '../../db/record-id'
9
9
  import type { SurrealDBService } from '../../db/service'
10
10
  import { TABLES } from '../../db/tables'
11
- import { effectTryPromise } from '../../effect/helpers'
11
+ import { ERROR_TAGS } from '../../effect/errors'
12
12
  import { AgentConfigServiceTag, DatabaseServiceTag } from '../../effect/services'
13
13
  import { resolvePlanNodeExecutionVisibility } from '../../runtime/execution-plan-visibility'
14
14
  import { nowDate, unsafeDateFrom } from '../../utils/date-time'
@@ -71,7 +71,7 @@ function isVisibleAgentNode(params: {
71
71
  return { agentId: params.nodeSpec.owner.ref, visibility }
72
72
  }
73
73
 
74
- class PlanAgentQueryError extends Schema.TaggedErrorClass<PlanAgentQueryError>()('PlanAgentQueryError', {
74
+ class PlanAgentQueryError extends Schema.TaggedErrorClass<PlanAgentQueryError>()(ERROR_TAGS.PlanAgentQueryError, {
75
75
  operation: Schema.String,
76
76
  cause: Schema.Defect,
77
77
  }) {}
@@ -80,20 +80,6 @@ function toPlanAgentQueryError(operation: string, cause: unknown): PlanAgentQuer
80
80
  return new PlanAgentQueryError({ operation, cause })
81
81
  }
82
82
 
83
- function queryEffect<A>(
84
- operation: string,
85
- thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
86
- ): Effect.Effect<A, PlanAgentQueryError> {
87
- return effectTryPromise(thunk, (cause) => toPlanAgentQueryError(operation, cause))
88
- }
89
-
90
- function queryServiceEffect<A, E>(
91
- operation: string,
92
- effect: Effect.Effect<A, E>,
93
- ): Effect.Effect<A, PlanAgentQueryError> {
94
- return effect.pipe(Effect.mapError((cause) => toPlanAgentQueryError(operation, cause)))
95
- }
96
-
97
83
  interface PlanAgentQueryDeps {
98
84
  agentConfig: ResolvedAgentConfig
99
85
  db: SurrealDBService
@@ -110,15 +96,15 @@ export function makePlanAgentQueryService(deps: PlanAgentQueryDeps) {
110
96
  }
111
97
 
112
98
  const whereOrganization = organizationId ? ' AND organizationId = $organizationId' : ''
113
- return queryEffect('list-active-runs', () =>
114
- db.queryMany(
99
+ return db
100
+ .queryMany(
115
101
  new BoundQuery(
116
102
  `SELECT * FROM ${TABLES.PLAN_RUN} WHERE status INSIDE $statuses${whereOrganization} ORDER BY updatedAt DESC`,
117
103
  bindings,
118
104
  ),
119
105
  PlanRunSchema,
120
- ),
121
- )
106
+ )
107
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('list-active-runs', cause)))
122
108
  }
123
109
 
124
110
  const getActionableNodesForAgentEffect = (params: {
@@ -130,12 +116,18 @@ export function makePlanAgentQueryService(deps: PlanAgentQueryDeps) {
130
116
  const actionable: ActionablePlanAgentNode[] = []
131
117
 
132
118
  for (const run of runs) {
133
- const spec = yield* queryServiceEffect('get-plan-spec-by-id', planRunService.getPlanSpecById(run.planSpecId))
119
+ const spec = yield* planRunService
120
+ .getPlanSpecById(run.planSpecId)
121
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('get-plan-spec-by-id', cause)))
134
122
 
135
123
  if (spec.executionMode === 'graph-full') {
136
124
  const [nodeSpecs, nodeRuns] = yield* Effect.all([
137
- queryServiceEffect('list-node-specs', planRunService.listNodeSpecs(spec.id)),
138
- queryServiceEffect('list-node-runs', planRunService.listNodeRuns(run.id)),
125
+ planRunService
126
+ .listNodeSpecs(spec.id)
127
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('list-node-specs', cause))),
128
+ planRunService
129
+ .listNodeRuns(run.id)
130
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('list-node-runs', cause))),
139
131
  ])
140
132
  for (const nodeRun of nodeRuns) {
141
133
  if (!ACTIONABLE_NODE_STATUSES.has(nodeRun.status)) continue
@@ -163,8 +155,12 @@ export function makePlanAgentQueryService(deps: PlanAgentQueryDeps) {
163
155
  }
164
156
 
165
157
  const [nodeSpec, nodeRun] = yield* Effect.all([
166
- queryServiceEffect('get-node-spec-by-node-id', planRunService.getNodeSpecByNodeId(spec.id, currentNodeId)),
167
- queryServiceEffect('get-node-run-by-node-id', planRunService.getNodeRunByNodeId(run.id, currentNodeId)),
158
+ planRunService
159
+ .getNodeSpecByNodeId(spec.id, currentNodeId)
160
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('get-node-spec-by-node-id', cause))),
161
+ planRunService
162
+ .getNodeRunByNodeId(run.id, currentNodeId)
163
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('get-node-run-by-node-id', cause))),
168
164
  ])
169
165
  if (!ACTIONABLE_NODE_STATUSES.has(nodeRun.status)) {
170
166
  continue
@@ -202,10 +198,16 @@ export function makePlanAgentQueryService(deps: PlanAgentQueryDeps) {
202
198
  const matches: ApproachingDeadlineNode[] = []
203
199
 
204
200
  for (const run of runs) {
205
- const spec = yield* queryServiceEffect('get-plan-spec-by-id', planRunService.getPlanSpecById(run.planSpecId))
201
+ const spec = yield* planRunService
202
+ .getPlanSpecById(run.planSpecId)
203
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('get-plan-spec-by-id', cause)))
206
204
  const [nodeSpecs, nodeRuns] = yield* Effect.all([
207
- queryServiceEffect('list-node-specs', planRunService.listNodeSpecs(spec.id)),
208
- queryServiceEffect('list-node-runs', planRunService.listNodeRuns(run.id)),
205
+ planRunService
206
+ .listNodeSpecs(spec.id)
207
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('list-node-specs', cause))),
208
+ planRunService
209
+ .listNodeRuns(run.id)
210
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('list-node-runs', cause))),
209
211
  ])
210
212
  const nodeRunsById = new Map(nodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun]))
211
213
 
@@ -257,10 +259,16 @@ export function makePlanAgentQueryService(deps: PlanAgentQueryDeps) {
257
259
  const matches: RecentlyUnblockedNode[] = []
258
260
 
259
261
  for (const run of runs) {
260
- const spec = yield* queryServiceEffect('get-plan-spec-by-id', planRunService.getPlanSpecById(run.planSpecId))
262
+ const spec = yield* planRunService
263
+ .getPlanSpecById(run.planSpecId)
264
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('get-plan-spec-by-id', cause)))
261
265
  const [nodeSpecs, events] = yield* Effect.all([
262
- queryServiceEffect('list-node-specs', planRunService.listNodeSpecs(spec.id)),
263
- queryServiceEffect('list-events', planRunService.listEvents(run.id, 200)),
266
+ planRunService
267
+ .listNodeSpecs(spec.id)
268
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('list-node-specs', cause))),
269
+ planRunService
270
+ .listEvents(run.id, 200)
271
+ .pipe(Effect.mapError((cause) => toPlanAgentQueryError('list-events', cause))),
264
272
  ])
265
273
  const nodeSpecsById = new Map(nodeSpecs.map((nodeSpec) => [nodeSpec.nodeId, nodeSpec]))
266
274
 
@@ -6,7 +6,6 @@ import { aiLogger } from '../../config/logger'
6
6
  import { ensureRecordId } from '../../db/record-id'
7
7
  import type { SurrealDBService } from '../../db/service'
8
8
  import { TABLES } from '../../db/tables'
9
- import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
10
9
  import { nowEpochMillis, unsafeDateFrom } from '../../utils/date-time'
11
10
  import type { makeFeedbackLoopService } from '../feedback-loop.service'
12
11
  import type { makeInstitutionalMemoryService } from '../institutional-memory.service'
@@ -29,21 +28,10 @@ interface PlanCompletionSideEffectsDeps {
29
28
  }
30
29
 
31
30
  class PlanCompletionSideEffectsError extends Schema.TaggedErrorClass<PlanCompletionSideEffectsError>()(
32
- 'PlanCompletionSideEffectsError',
31
+ '@lota-sdk/core/PlanCompletionSideEffectsError',
33
32
  { message: Schema.String, cause: Schema.Defect },
34
33
  ) {}
35
34
 
36
- const effectTryPlanCompletionPromise = makeEffectTryPromiseWithMessage(
37
- (message, cause) => new PlanCompletionSideEffectsError({ message, cause }),
38
- )
39
-
40
- function tryPlanCompletionPromise<A>(
41
- message: string,
42
- evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
43
- ): Effect.Effect<A, PlanCompletionSideEffectsError> {
44
- return effectTryPlanCompletionPromise(evaluate, message)
45
- }
46
-
47
35
  export function makePlanCompletionSideEffects({
48
36
  databaseService,
49
37
  feedbackLoopService,
@@ -107,8 +95,8 @@ export function makePlanCompletionSideEffects({
107
95
  if (recommendations.length > 0) {
108
96
  const run = yield* planRunService.getRunById(params.runId)
109
97
  const specRecord = yield* planRunService.getPlanSpecById(run.planSpecId)
110
- const event = yield* tryPlanCompletionPromise('Failed to create feedback analyzed plan event.', () =>
111
- databaseService.create(
98
+ const event = yield* databaseService
99
+ .create(
112
100
  TABLES.PLAN_EVENT,
113
101
  {
114
102
  planSpecId: ensureRecordId(specRecord.id, TABLES.PLAN_SPEC),
@@ -119,8 +107,16 @@ export function makePlanCompletionSideEffects({
119
107
  emittedBy: 'system',
120
108
  },
121
109
  PlanEventSchema,
122
- ),
123
- )
110
+ )
111
+ .pipe(
112
+ Effect.mapError(
113
+ (cause) =>
114
+ new PlanCompletionSideEffectsError({
115
+ message: 'Failed to create feedback analyzed plan event.',
116
+ cause,
117
+ }),
118
+ ),
119
+ )
124
120
  yield* planEventDeliveryService
125
121
  .dispatchEvent(event)
126
122
  .pipe(
@@ -4,6 +4,7 @@ import { Context, Schema, Effect, Layer } from 'effect'
4
4
  import { serverLogger } from '../../config/logger'
5
5
  import { recordIdToString } from '../../db/record-id'
6
6
  import { TABLES } from '../../db/tables'
7
+ import { ERROR_TAGS } from '../../effect/errors'
7
8
  import { nowEpochMillis } from '../../utils/date-time'
8
9
  import type { makePlanRunService } from './plan-run.service'
9
10
  import { PlanRunServiceTag } from './plan-run.service'
@@ -15,7 +16,7 @@ export interface DependencyResolutionResult {
15
16
  notifications: Array<{ dependency: PlanDependency; reason: string }>
16
17
  }
17
18
 
18
- class PlanCoordinationError extends Schema.TaggedErrorClass<PlanCoordinationError>()('PlanCoordinationError', {
19
+ class PlanCoordinationError extends Schema.TaggedErrorClass<PlanCoordinationError>()(ERROR_TAGS.PlanCoordinationError, {
19
20
  message: Schema.String,
20
21
  cause: Schema.optional(Schema.Defect),
21
22
  }) {}