@lota-sdk/core 0.4.12 → 0.4.14

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/create-runtime.ts +259 -200
  8. package/src/db/cursor-pagination.ts +2 -9
  9. package/src/db/memory-store.ts +194 -175
  10. package/src/db/memory.ts +125 -71
  11. package/src/db/schema-fingerprint.ts +5 -4
  12. package/src/db/service-normalization.ts +4 -3
  13. package/src/db/service.ts +3 -2
  14. package/src/db/startup.ts +15 -16
  15. package/src/effect/errors.ts +161 -21
  16. package/src/effect/index.ts +0 -1
  17. package/src/embeddings/provider.ts +15 -7
  18. package/src/queues/autonomous-job.queue.ts +10 -22
  19. package/src/queues/delayed-node-promotion.queue.ts +8 -14
  20. package/src/queues/document-processor.queue.ts +13 -4
  21. package/src/queues/memory-consolidation.queue.ts +26 -14
  22. package/src/queues/plan-agent-heartbeat.queue.ts +10 -9
  23. package/src/queues/plan-scheduler.queue.ts +37 -15
  24. package/src/queues/queue-factory.ts +59 -35
  25. package/src/queues/standalone-worker.ts +3 -2
  26. package/src/redis/connection.ts +10 -3
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +5 -5
  29. package/src/redis/stream-context.ts +1 -1
  30. package/src/runtime/chat-message.ts +64 -1
  31. package/src/runtime/chat-run-orchestration.ts +33 -20
  32. package/src/runtime/context-compaction/context-compaction-runtime.ts +14 -7
  33. package/src/runtime/context-compaction/context-compaction.ts +78 -66
  34. package/src/runtime/domain-layer.ts +13 -7
  35. package/src/runtime/execution-plan.ts +7 -3
  36. package/src/runtime/live-turn-trace.ts +6 -49
  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 +26 -23
  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 +82 -85
  103. package/src/services/thread/thread-turn.ts +8 -8
  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 +10 -5
  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,7 +4,7 @@ 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'
@@ -37,17 +37,10 @@ function buildWakeDedupeKey(params: {
37
37
  return `${params.organizationId}:${params.threadId}:${params.runId}:${params.nodeId}:${params.agentId}`
38
38
  }
39
39
 
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
- }
40
+ class PlanAgentHeartbeatError extends Schema.TaggedErrorClass<PlanAgentHeartbeatError>()(
41
+ ERROR_TAGS.PlanAgentHeartbeatError,
42
+ { operation: Schema.String, cause: Schema.Defect },
43
+ ) {}
51
44
 
52
45
  function heartbeatServiceEffect<A, E, R = never>(
53
46
  operation: string,
@@ -58,6 +51,7 @@ function heartbeatServiceEffect<A, E, R = never>(
58
51
 
59
52
  interface PlanAgentHeartbeatDeps {
60
53
  agentConfig: ResolvedAgentConfig
54
+ provideCurrentContext: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, never>
61
55
  redis: RedisConnectionManager
62
56
  planAgentQueryService: ReturnType<typeof makePlanAgentQueryService>
63
57
  planExecutorService: ReturnType<typeof makePlanExecutorService>
@@ -69,6 +63,7 @@ interface PlanAgentHeartbeatDeps {
69
63
  export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
70
64
  const {
71
65
  agentConfig,
66
+ provideCurrentContext,
72
67
  planExecutorService,
73
68
  planRunService,
74
69
  redis,
@@ -84,7 +79,7 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
84
79
  nodeId: string
85
80
  agentId: string
86
81
  reason: string
87
- }): Effect.Effect<boolean, PlanAgentHeartbeatError, unknown> =>
82
+ }): Effect.Effect<boolean, PlanAgentHeartbeatError, never> =>
88
83
  Effect.gen(function* () {
89
84
  const threadRef = ensureRecordId(params.threadId, TABLES.THREAD)
90
85
  yield* heartbeatServiceEffect(
@@ -148,27 +143,28 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
148
143
  }
149
144
 
150
145
  if (nodeRun.status === 'ready' && run.currentNodeId === params.nodeId) {
151
- yield* tryHeartbeatPromise('transition-node-to-running', () =>
146
+ yield* heartbeatServiceEffect(
147
+ 'transition-node-to-running',
152
148
  planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: params.nodeId }),
153
149
  )
154
150
  }
155
151
 
156
- const { triggerPlanNodeTurn } = yield* tryHeartbeatPromise(
157
- 'import-thread-turn',
158
- () => import('../thread/thread-turn'),
159
- )
152
+ const { triggerPlanNodeTurn } = yield* Effect.tryPromise({
153
+ try: () => import('../thread/thread-turn'),
154
+ catch: (cause) => new PlanAgentHeartbeatError({ operation: 'import-thread-turn', cause }),
155
+ })
160
156
 
161
- yield* tryHeartbeatPromise('trigger-plan-node-turn', () =>
162
- triggerPlanNodeTurn({ runId: params.runId, nodeId: params.nodeId }),
157
+ yield* heartbeatServiceEffect(
158
+ 'trigger-plan-node-turn',
159
+ provideCurrentContext(triggerPlanNodeTurn({ runId: params.runId, nodeId: params.nodeId })),
163
160
  )
164
161
  return true
165
162
  }),
166
163
  ).pipe(Effect.mapError((cause) => new PlanAgentHeartbeatError({ operation: 'wake-node-lock', cause })))
167
164
  })
168
165
 
169
- const sweepEffect = (params?: { organizationId?: string }): Effect.Effect<void, PlanAgentHeartbeatError, unknown> =>
166
+ const sweepEffect = (params?: { organizationId?: string }): Effect.Effect<void, PlanAgentHeartbeatError, never> =>
170
167
  Effect.gen(function* () {
171
- const enqueuePlanAgentHeartbeatWake = planAgentHeartbeatQueue.enqueuePlanAgentHeartbeatWake
172
168
  const [actionable, recentlyUnblocked, approachingDeadlines] = yield* Effect.all([
173
169
  heartbeatServiceEffect(
174
170
  'get-actionable-nodes-for-agent',
@@ -213,7 +209,10 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
213
209
  }
214
210
 
215
211
  for (const target of wakeTargets.values()) {
216
- yield* tryHeartbeatPromise('enqueue-heartbeat-wake', () => enqueuePlanAgentHeartbeatWake(target))
212
+ yield* Effect.tryPromise({
213
+ try: () => planAgentHeartbeatQueue.enqueuePlanAgentHeartbeatWake(target),
214
+ catch: (cause) => new PlanAgentHeartbeatError({ operation: 'enqueue-heartbeat-wake', cause }),
215
+ })
217
216
  }
218
217
  })
219
218
 
@@ -228,6 +227,9 @@ export class PlanAgentHeartbeatServiceTag extends Context.Service<
228
227
  export const PlanAgentHeartbeatServiceLive = Layer.effect(
229
228
  PlanAgentHeartbeatServiceTag,
230
229
  Effect.gen(function* () {
230
+ const currentContext = yield* Effect.context()
231
+ const provideCurrentContext = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, never> =>
232
+ effect.pipe(Effect.provide(currentContext)) as Effect.Effect<A, E, never>
231
233
  const agentConfig = yield* AgentConfigServiceTag
232
234
  const redis = yield* RedisServiceTag
233
235
  const planAgentQueryService = yield* PlanAgentQueryServiceTag
@@ -237,6 +239,7 @@ export const PlanAgentHeartbeatServiceLive = Layer.effect(
237
239
  const queues = yield* LotaQueuesServiceTag
238
240
  return makePlanAgentHeartbeatService({
239
241
  agentConfig,
242
+ provideCurrentContext,
240
243
  redis,
241
244
  planAgentQueryService,
242
245
  planExecutorService: planExecutor,
@@ -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
  }) {}