@lota-sdk/core 0.4.9 → 0.4.10

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 (158) hide show
  1. package/package.json +2 -2
  2. package/src/ai/embedding-cache.ts +3 -1
  3. package/src/ai-gateway/ai-gateway.ts +38 -10
  4. package/src/config/agent-defaults.ts +22 -9
  5. package/src/config/agent-types.ts +1 -1
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/index.ts +0 -1
  8. package/src/config/logger.ts +20 -7
  9. package/src/config/thread-defaults.ts +12 -4
  10. package/src/create-runtime.ts +69 -656
  11. package/src/db/memory-query-builder.ts +2 -1
  12. package/src/db/memory-store.ts +29 -20
  13. package/src/db/memory.ts +188 -195
  14. package/src/db/service-normalization.ts +97 -64
  15. package/src/db/service.ts +706 -538
  16. package/src/db/startup.ts +30 -19
  17. package/src/effect/awaitable-effect.ts +46 -37
  18. package/src/effect/helpers.ts +30 -5
  19. package/src/effect/index.ts +7 -5
  20. package/src/effect/layers.ts +82 -72
  21. package/src/effect/runtime.ts +18 -3
  22. package/src/effect/services.ts +15 -11
  23. package/src/embeddings/provider.ts +65 -66
  24. package/src/index.ts +13 -11
  25. package/src/queues/autonomous-job.queue.ts +59 -71
  26. package/src/queues/context-compaction.queue.ts +6 -18
  27. package/src/queues/delayed-node-promotion.queue.ts +9 -17
  28. package/src/queues/organization-learning.queue.ts +17 -4
  29. package/src/queues/plan-agent-heartbeat.queue.ts +23 -20
  30. package/src/queues/plan-scheduler.queue.ts +6 -18
  31. package/src/queues/post-chat-memory.queue.ts +6 -18
  32. package/src/queues/queue-factory.ts +128 -50
  33. package/src/queues/title-generation.queue.ts +6 -17
  34. package/src/redis/connection.ts +181 -164
  35. package/src/redis/runtime-connection.ts +13 -3
  36. package/src/redis/stream-context.ts +17 -9
  37. package/src/runtime/agent-runtime-policy.ts +1 -1
  38. package/src/runtime/agent-stream-helpers.ts +15 -11
  39. package/src/runtime/chat-run-orchestration.ts +1 -1
  40. package/src/runtime/context-compaction/context-compaction-runtime.ts +1 -1
  41. package/src/runtime/context-compaction/context-compaction.ts +126 -82
  42. package/src/runtime/domain-layer.ts +192 -0
  43. package/src/runtime/graph-designer.ts +15 -7
  44. package/src/runtime/helper-model.ts +8 -4
  45. package/src/runtime/index.ts +0 -1
  46. package/src/runtime/memory/memory-block.ts +19 -9
  47. package/src/runtime/memory/memory-pipeline.ts +53 -66
  48. package/src/runtime/memory/memory-scope.ts +33 -29
  49. package/src/runtime/plugin-resolution.ts +33 -54
  50. package/src/runtime/post-turn-side-effects.ts +6 -26
  51. package/src/runtime/retrieval-adapters.ts +4 -4
  52. package/src/runtime/runtime-accessors.ts +92 -0
  53. package/src/runtime/runtime-config.ts +3 -3
  54. package/src/runtime/runtime-extensions.ts +20 -9
  55. package/src/runtime/runtime-lifecycle.ts +124 -0
  56. package/src/runtime/runtime-services.ts +386 -0
  57. package/src/runtime/runtime-token.ts +47 -0
  58. package/src/runtime/social-chat/social-chat-agent-runner.ts +7 -5
  59. package/src/runtime/social-chat/social-chat-history.ts +21 -12
  60. package/src/runtime/social-chat/social-chat.ts +401 -365
  61. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +58 -52
  62. package/src/runtime/thread-turn-context.ts +21 -27
  63. package/src/services/agent-activity.service.ts +1 -1
  64. package/src/services/agent-executor.service.ts +179 -187
  65. package/src/services/artifact.service.ts +10 -5
  66. package/src/services/attachment.service.ts +35 -1
  67. package/src/services/autonomous-job.service.ts +58 -56
  68. package/src/services/background-work.service.ts +54 -0
  69. package/src/services/chat-run-registry.service.ts +3 -1
  70. package/src/services/context-compaction.service.ts +1 -1
  71. package/src/services/document-chunk.service.ts +8 -17
  72. package/src/services/execution-plan/execution-plan-graph.ts +74 -52
  73. package/src/services/execution-plan/execution-plan.service.ts +1 -1
  74. package/src/services/feedback-loop.service.ts +1 -1
  75. package/src/services/global-orchestrator.service.ts +33 -10
  76. package/src/services/graph-full-routing.ts +44 -33
  77. package/src/services/index.ts +1 -0
  78. package/src/services/institutional-memory.service.ts +8 -17
  79. package/src/services/learned-skill.service.ts +38 -35
  80. package/src/services/memory/memory-errors.ts +27 -0
  81. package/src/services/memory/memory-org-memory.ts +14 -3
  82. package/src/services/memory/memory-preseeded.ts +10 -4
  83. package/src/services/memory/memory-utils.ts +2 -1
  84. package/src/services/memory/memory.service.ts +26 -44
  85. package/src/services/memory/rerank.service.ts +3 -11
  86. package/src/services/monitoring-window.service.ts +1 -1
  87. package/src/services/mutating-approval.service.ts +1 -1
  88. package/src/services/node-workspace.service.ts +2 -2
  89. package/src/services/notification.service.ts +16 -4
  90. package/src/services/organization-member.service.ts +1 -1
  91. package/src/services/organization.service.ts +34 -51
  92. package/src/services/ownership-dispatcher.service.ts +132 -90
  93. package/src/services/plan/plan-agent-heartbeat.service.ts +1 -1
  94. package/src/services/plan/plan-agent-query.service.ts +1 -1
  95. package/src/services/plan/plan-approval.service.ts +52 -48
  96. package/src/services/plan/plan-artifact.service.ts +2 -2
  97. package/src/services/plan/plan-builder.service.ts +2 -2
  98. package/src/services/plan/plan-checkpoint.service.ts +1 -1
  99. package/src/services/plan/plan-compiler.service.ts +1 -1
  100. package/src/services/plan/plan-completion-side-effects.ts +18 -24
  101. package/src/services/plan/plan-coordination.service.ts +1 -1
  102. package/src/services/plan/plan-cycle.service.ts +171 -164
  103. package/src/services/plan/plan-deadline.service.ts +290 -304
  104. package/src/services/plan/plan-event-delivery.service.ts +44 -39
  105. package/src/services/plan/plan-executor-graph.ts +114 -67
  106. package/src/services/plan/plan-executor-helpers.ts +60 -75
  107. package/src/services/plan/plan-executor.service.ts +550 -467
  108. package/src/services/plan/plan-run.service.ts +12 -19
  109. package/src/services/plan/plan-scheduler.service.ts +27 -33
  110. package/src/services/plan/plan-template.service.ts +1 -1
  111. package/src/services/plan/plan-transaction-events.ts +8 -5
  112. package/src/services/plan/plan-validator.service.ts +1 -1
  113. package/src/services/plan/plan-workspace.service.ts +17 -11
  114. package/src/services/plugin-executor.service.ts +26 -21
  115. package/src/services/quality-metrics.service.ts +1 -1
  116. package/src/services/queue-job.service.ts +8 -17
  117. package/src/services/recent-activity-title.service.ts +17 -9
  118. package/src/services/recent-activity.service.ts +1 -1
  119. package/src/services/skill-resolver.service.ts +1 -1
  120. package/src/services/social-chat-history.service.ts +37 -20
  121. package/src/services/system-executor.service.ts +25 -20
  122. package/src/services/thread/thread-bootstrap.ts +26 -10
  123. package/src/services/thread/thread-listing.ts +2 -1
  124. package/src/services/thread/thread-memory-block.ts +18 -5
  125. package/src/services/thread/thread-message.service.ts +24 -8
  126. package/src/services/thread/thread-title.service.ts +1 -1
  127. package/src/services/thread/thread-turn-execution.ts +1 -1
  128. package/src/services/thread/thread-turn-preparation.service.ts +18 -16
  129. package/src/services/thread/thread-turn-streaming.ts +12 -11
  130. package/src/services/thread/thread-turn.ts +43 -10
  131. package/src/services/thread/thread.service.ts +11 -2
  132. package/src/services/user.service.ts +1 -1
  133. package/src/services/write-intent-validator.service.ts +1 -1
  134. package/src/storage/attachment-storage.service.ts +7 -4
  135. package/src/storage/generated-document-storage.service.ts +1 -1
  136. package/src/system-agents/context-compaction.agent.ts +1 -1
  137. package/src/system-agents/helper-agent-options.ts +1 -1
  138. package/src/system-agents/memory-reranker.agent.ts +1 -1
  139. package/src/system-agents/memory.agent.ts +1 -1
  140. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  141. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  142. package/src/system-agents/skill-extractor.agent.ts +1 -1
  143. package/src/system-agents/skill-manager.agent.ts +1 -1
  144. package/src/system-agents/title-generator.agent.ts +1 -1
  145. package/src/tools/execution-plan.tool.ts +28 -17
  146. package/src/tools/fetch-webpage.tool.ts +20 -13
  147. package/src/tools/firecrawl-client.ts +13 -3
  148. package/src/tools/plan-approval.tool.ts +9 -1
  149. package/src/tools/search-web.tool.ts +16 -9
  150. package/src/tools/team-think.tool.ts +2 -2
  151. package/src/utils/async.ts +15 -6
  152. package/src/utils/errors.ts +27 -15
  153. package/src/workers/bootstrap.ts +25 -48
  154. package/src/workers/organization-learning.worker.ts +1 -1
  155. package/src/workers/regular-chat-memory-digest.runner.ts +25 -15
  156. package/src/workers/worker-utils.ts +20 -2
  157. package/src/config/search.ts +0 -3
  158. package/src/runtime/agent-types.ts +0 -1
@@ -187,67 +187,72 @@ export function makePlanEventDeliveryService(deps: PlanEventDeliveryDeps) {
187
187
  })
188
188
  }
189
189
 
190
- function dispatchEventEffect(event: PlanEventRecord) {
191
- return Effect.gen(function* () {
192
- const eventId = recordIdToString(event.id, TABLES.PLAN_EVENT)
193
- const deliveredKey = buildDeliveredKey(eventId)
194
- const exists = yield* Effect.tryPromise({
195
- try: () => redis.getConnection().exists(deliveredKey),
196
- catch: (error) => new PlanEventDeliveryError({ message: getErrorMessage(error), cause: error }),
197
- })
198
- if (exists) {
199
- return
200
- }
190
+ const dispatchEventEffect = Effect.fn('PlanEventDelivery.dispatchEvent')(function* (event: PlanEventRecord) {
191
+ const eventId = recordIdToString(event.id, TABLES.PLAN_EVENT)
192
+ const deliveredKey = buildDeliveredKey(eventId)
193
+ const exists = yield* Effect.tryPromise({
194
+ try: () => redis.getConnection().exists(deliveredKey),
195
+ catch: (error) => new PlanEventDeliveryError({ message: getErrorMessage(error), cause: error }),
196
+ })
197
+ if (exists) {
198
+ return
199
+ }
201
200
 
202
- yield* deliverEvent(event)
203
- yield* Effect.tryPromise({
204
- try: () => redis.getConnection().set(deliveredKey, '1', 'PX', PLAN_EVENT_DELIVERED_TTL_MS),
205
- catch: (error) => new PlanEventDeliveryError({ message: getErrorMessage(error), cause: error }),
206
- })
201
+ yield* deliverEvent(event)
202
+ yield* Effect.tryPromise({
203
+ try: () => redis.getConnection().set(deliveredKey, '1', 'PX', PLAN_EVENT_DELIVERED_TTL_MS),
204
+ catch: (error) => new PlanEventDeliveryError({ message: getErrorMessage(error), cause: error }),
207
205
  })
208
- }
206
+ })
209
207
 
210
- function dispatchUndeliveredEventsEffect(runId: string, options?: { limit?: number }) {
211
- return Effect.gen(function* () {
212
- const run = yield* planRunService
213
- .getRunById(runId)
214
- .pipe(Effect.mapError((error) => new PlanEventDeliveryError({ message: getErrorMessage(error), cause: error })))
215
- const events = yield* planRunService
216
- .listEvents(run.id, options?.limit ?? 200)
217
- .pipe(Effect.mapError((error) => new PlanEventDeliveryError({ message: getErrorMessage(error), cause: error })))
208
+ const dispatchUndeliveredEventsEffect = Effect.fn('PlanEventDelivery.dispatchUndeliveredEvents')(function* (
209
+ runId: string,
210
+ options?: { limit?: number },
211
+ ) {
212
+ const run = yield* planRunService
213
+ .getRunById(runId)
214
+ .pipe(Effect.mapError((error) => new PlanEventDeliveryError({ message: getErrorMessage(error), cause: error })))
215
+ const events = yield* planRunService
216
+ .listEvents(run.id, options?.limit ?? 200)
217
+ .pipe(Effect.mapError((error) => new PlanEventDeliveryError({ message: getErrorMessage(error), cause: error })))
218
218
 
219
- for (const event of events) {
220
- yield* dispatchEventEffect(event)
221
- }
222
- })
223
- }
219
+ for (const event of events) {
220
+ yield* dispatchEventEffect(event)
221
+ }
222
+ })
223
+
224
+ const dispatchEventsEffect = Effect.fn('PlanEventDelivery.dispatchEvents')(function* (events: PlanEventRecord[]) {
225
+ const runIds = new Set(events.map((event) => recordIdToString(event.runId, TABLES.PLAN_RUN)))
226
+ for (const runId of runIds) {
227
+ yield* dispatchUndeliveredEventsEffect(runId)
228
+ }
229
+ })
224
230
 
225
231
  return {
226
232
  dispatchEvents(events: PlanEventRecord[]): Promise<void> {
227
- return runPromise(
228
- Effect.gen(function* () {
229
- const runIds = new Set(events.map((event) => recordIdToString(event.runId, TABLES.PLAN_RUN)))
230
- for (const runId of runIds) {
231
- yield* dispatchUndeliveredEventsEffect(runId)
232
- }
233
- }),
234
- )
233
+ return runPromise(dispatchEventsEffect(events))
235
234
  },
236
235
 
236
+ dispatchEventsEffect,
237
+
237
238
  dispatchEvent(event: PlanEventRecord): Promise<void> {
238
239
  return runPromise(dispatchEventEffect(event))
239
240
  },
240
241
 
242
+ dispatchEventEffect,
243
+
241
244
  dispatchUndeliveredEvents(runId: string, options?: { limit?: number }): Promise<void> {
242
245
  return runPromise(dispatchUndeliveredEventsEffect(runId, options))
243
246
  },
247
+
248
+ dispatchUndeliveredEventsEffect,
244
249
  }
245
250
  }
246
251
 
247
252
  export class PlanEventDeliveryServiceTag extends Context.Service<
248
253
  PlanEventDeliveryServiceTag,
249
254
  ReturnType<typeof makePlanEventDeliveryService>
250
- >()('PlanEventDeliveryService') {}
255
+ >()('@lota-sdk/core/PlanEventDeliveryService') {}
251
256
 
252
257
  export const PlanEventDeliveryServiceLive = Layer.effect(
253
258
  PlanEventDeliveryServiceTag,
@@ -6,7 +6,8 @@ import type {
6
6
  PlanSpecRecord,
7
7
  } from '@lota-sdk/shared'
8
8
  import { PlanNodeRunSchema } from '@lota-sdk/shared'
9
- import { Effect } from 'effect'
9
+ import { Effect, Match, Schema } from 'effect'
10
+ import type { z } from 'zod'
10
11
 
11
12
  import type { RecordIdInput } from '../../db/record-id'
12
13
  import { ensureRecordId, recordIdToString } from '../../db/record-id'
@@ -29,6 +30,22 @@ import { emitEvent, replaceRun } from './plan-executor-persistence'
29
30
 
30
31
  const delayedNodePromotionQueueModule = Effect.tryPromise(() => import('../../queues/delayed-node-promotion.queue'))
31
32
 
33
+ class PlanExecutorGraphError extends Schema.TaggedErrorClass<PlanExecutorGraphError>()('PlanExecutorGraphError', {
34
+ message: Schema.String,
35
+ cause: Schema.optional(Schema.Defect),
36
+ }) {}
37
+
38
+ function parseRowOrFail<T>(
39
+ schema: z.ZodType<T>,
40
+ value: unknown,
41
+ operation: string,
42
+ ): Effect.Effect<T, PlanExecutorGraphError> {
43
+ return Effect.try({
44
+ try: () => schema.parse(value),
45
+ catch: (cause) => new PlanExecutorGraphError({ message: `Failed to parse ${operation} row`, cause }),
46
+ })
47
+ }
48
+
32
49
  export function syncRunGraph(
33
50
  context: PlanExecutorContext,
34
51
  params: {
@@ -150,14 +167,13 @@ export function syncRunGraph(
150
167
  }
151
168
 
152
169
  if (nodeSpec.upstreamNodeIds.length > 0 && activeIncomingEdges.length === 0) {
153
- const skippedNodeRun = PlanNodeRunSchema.parse(
154
- yield* updateNodeRun(nodeRun, {
155
- status: 'skipped',
156
- completedAt: currentTime,
157
- blockedReason: null,
158
- failureClass: null,
159
- }),
160
- )
170
+ const skippedNodeRunRow = yield* updateNodeRun(nodeRun, {
171
+ status: 'skipped',
172
+ completedAt: currentTime,
173
+ blockedReason: null,
174
+ failureClass: null,
175
+ })
176
+ const skippedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, skippedNodeRunRow, 'plan node run')
161
177
  replaceNodeRun(skippedNodeRun)
162
178
  yield* emitEvent({
163
179
  tx: params.tx,
@@ -186,9 +202,12 @@ export function syncRunGraph(
186
202
  const hasNonImmediateSchedule = nodeSchedule && nodeSchedule.type !== 'immediate'
187
203
 
188
204
  if (hasNonImmediateSchedule) {
189
- const scheduledNodeRun = PlanNodeRunSchema.parse(
190
- yield* updateNodeRun(nodeRun, { status: 'scheduled', resolvedInput, scheduledAt: currentTime }),
191
- )
205
+ const scheduledNodeRunRow = yield* updateNodeRun(nodeRun, {
206
+ status: 'scheduled',
207
+ resolvedInput,
208
+ scheduledAt: currentTime,
209
+ })
210
+ const scheduledNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, scheduledNodeRunRow, 'plan node run')
192
211
  replaceNodeRun(scheduledNodeRun)
193
212
  yield* planSchedulerService.createSchedule({
194
213
  organizationId: currentRun.organizationId,
@@ -214,9 +233,12 @@ export function syncRunGraph(
214
233
  } else if (nodeSpec.delayAfterPredecessorMs) {
215
234
  const delayAfterPredecessorMs = nodeSpec.delayAfterPredecessorMs
216
235
  const { enqueueDelayedNodePromotion } = yield* delayedNodePromotionQueueModule
217
- const scheduledNodeRun = PlanNodeRunSchema.parse(
218
- yield* updateNodeRun(nodeRun, { status: 'scheduled', resolvedInput, scheduledAt: currentTime }),
219
- )
236
+ const scheduledNodeRunRow = yield* updateNodeRun(nodeRun, {
237
+ status: 'scheduled',
238
+ resolvedInput,
239
+ scheduledAt: currentTime,
240
+ })
241
+ const scheduledNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, scheduledNodeRunRow, 'plan node run')
220
242
  replaceNodeRun(scheduledNodeRun)
221
243
  yield* Effect.tryPromise(() =>
222
244
  enqueueDelayedNodePromotion(
@@ -242,9 +264,12 @@ export function syncRunGraph(
242
264
  })
243
265
  changed = true
244
266
  } else {
245
- const readyNodeRun = PlanNodeRunSchema.parse(
246
- yield* updateNodeRun(nodeRun, { status: 'ready', resolvedInput, readyAt: currentTime }),
247
- )
267
+ const readyNodeRunRow = yield* updateNodeRun(nodeRun, {
268
+ status: 'ready',
269
+ resolvedInput,
270
+ readyAt: currentTime,
271
+ })
272
+ const readyNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, readyNodeRunRow, 'plan node run')
248
273
  replaceNodeRun(readyNodeRun)
249
274
  yield* emitEvent({
250
275
  tx: params.tx,
@@ -271,13 +296,12 @@ export function syncRunGraph(
271
296
  const nodeRun = getNodeRunsById().get(nodeSpec.nodeId)
272
297
  if (!nodeRun) continue
273
298
 
274
- const completedNodeRun = PlanNodeRunSchema.parse(
275
- yield* updateNodeRun(nodeRun, {
276
- status: 'completed',
277
- startedAt: nodeRun.startedAt ?? currentTime,
278
- completedAt: currentTime,
279
- }),
280
- )
299
+ const completedNodeRunRow = yield* updateNodeRun(nodeRun, {
300
+ status: 'completed',
301
+ startedAt: nodeRun.startedAt ?? currentTime,
302
+ completedAt: currentTime,
303
+ })
304
+ const completedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, completedNodeRunRow, 'plan node run')
281
305
  replaceNodeRun(completedNodeRun)
282
306
  yield* emitEvent({
283
307
  tx: params.tx,
@@ -318,8 +342,14 @@ export function syncRunGraph(
318
342
  message: `Expected ready node run for "${nextHumanNodeSpec.nodeId}".`,
319
343
  })
320
344
  }
321
- const awaitingHumanNodeRun = PlanNodeRunSchema.parse(
322
- yield* updateNodeRun(nodeRun, { status: 'awaiting-human', startedAt: nodeRun.startedAt ?? currentTime }),
345
+ const awaitingHumanNodeRunRow = yield* updateNodeRun(nodeRun, {
346
+ status: 'awaiting-human',
347
+ startedAt: nodeRun.startedAt ?? currentTime,
348
+ })
349
+ const awaitingHumanNodeRun = yield* parseRowOrFail(
350
+ PlanNodeRunSchema,
351
+ awaitingHumanNodeRunRow,
352
+ 'plan node run',
323
353
  )
324
354
  replaceNodeRun(awaitingHumanNodeRun)
325
355
 
@@ -377,9 +407,11 @@ export function syncRunGraph(
377
407
  message: `Expected ready node run for "${nextActionNodeSpec.nodeId}".`,
378
408
  })
379
409
  }
380
- const runningNodeRun = PlanNodeRunSchema.parse(
381
- yield* updateNodeRun(nodeRun, { status: 'running', startedAt: nodeRun.startedAt ?? currentTime }),
382
- )
410
+ const runningNodeRunRow = yield* updateNodeRun(nodeRun, {
411
+ status: 'running',
412
+ startedAt: nodeRun.startedAt ?? currentTime,
413
+ })
414
+ const runningNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, runningNodeRunRow, 'plan node run')
383
415
  replaceNodeRun(runningNodeRun)
384
416
 
385
417
  currentRun = yield* replaceRun(params.tx, currentRun, {
@@ -418,44 +450,59 @@ export function syncRunGraph(
418
450
  emittedBy: params.emittedBy,
419
451
  capturedEvents: params.capturedEvents,
420
452
  })
421
- } else if (currentNodeRuns.every((nodeRun) => isSuccessfulTerminalStatus(nodeRun.status))) {
422
- currentRun = yield* replaceRun(params.tx, currentRun, {
423
- status: 'completed',
424
- currentNodeId: null,
425
- waitingNodeId: null,
426
- readyNodeIds: [],
427
- completedAt: currentTime,
428
- })
429
-
430
- yield* emitEvent({
431
- tx: params.tx,
432
- run: currentRun,
433
- spec: params.spec,
434
- eventType: 'run-status-changed',
435
- fromStatus: params.run.status,
436
- toStatus: currentRun.status,
437
- message: `Run "${params.spec.title}" completed.`,
438
- emittedBy: params.emittedBy,
439
- capturedEvents: params.capturedEvents,
440
- })
441
- } else if (hasScheduledOrMonitoring) {
442
- currentRun = yield* replaceRun(params.tx, currentRun, {
443
- status: 'running',
444
- currentNodeId: null,
445
- waitingNodeId: null,
446
- readyNodeIds: currentNodeRuns
447
- .filter((candidate) => candidate.status === 'ready')
448
- .map((candidate) => candidate.nodeId),
449
- })
450
453
  } else {
451
- currentRun = yield* replaceRun(params.tx, currentRun, {
452
- status: 'blocked',
453
- currentNodeId: null,
454
- waitingNodeId: null,
455
- readyNodeIds: currentNodeRuns
456
- .filter((candidate) => candidate.status === 'ready')
457
- .map((candidate) => candidate.nodeId),
458
- })
454
+ const allTerminalSuccess = currentNodeRuns.every((nodeRun) => isSuccessfulTerminalStatus(nodeRun.status))
455
+ const runStatus: 'completed' | 'running' | 'blocked' = allTerminalSuccess
456
+ ? 'completed'
457
+ : hasScheduledOrMonitoring
458
+ ? 'running'
459
+ : 'blocked'
460
+ const readyIds = currentNodeRuns
461
+ .filter((candidate) => candidate.status === 'ready')
462
+ .map((candidate) => candidate.nodeId)
463
+
464
+ currentRun = yield* Match.value(runStatus).pipe(
465
+ Match.when('completed', () =>
466
+ replaceRun(params.tx, currentRun, {
467
+ status: 'completed',
468
+ currentNodeId: null,
469
+ waitingNodeId: null,
470
+ readyNodeIds: [],
471
+ completedAt: currentTime,
472
+ }),
473
+ ),
474
+ Match.when('running', () =>
475
+ replaceRun(params.tx, currentRun, {
476
+ status: 'running',
477
+ currentNodeId: null,
478
+ waitingNodeId: null,
479
+ readyNodeIds: readyIds,
480
+ }),
481
+ ),
482
+ Match.when('blocked', () =>
483
+ replaceRun(params.tx, currentRun, {
484
+ status: 'blocked',
485
+ currentNodeId: null,
486
+ waitingNodeId: null,
487
+ readyNodeIds: readyIds,
488
+ }),
489
+ ),
490
+ Match.exhaustive,
491
+ )
492
+
493
+ if (runStatus === 'completed') {
494
+ yield* emitEvent({
495
+ tx: params.tx,
496
+ run: currentRun,
497
+ spec: params.spec,
498
+ eventType: 'run-status-changed',
499
+ fromStatus: params.run.status,
500
+ toStatus: currentRun.status,
501
+ message: `Run "${params.spec.title}" completed.`,
502
+ emittedBy: params.emittedBy,
503
+ capturedEvents: params.capturedEvents,
504
+ })
505
+ }
459
506
  }
460
507
  }
461
508
  } else {
@@ -12,7 +12,7 @@ import {
12
12
  isSafePath,
13
13
  isSafePathOrRoot,
14
14
  } from '@lota-sdk/shared'
15
- import { Effect } from 'effect'
15
+ import { Effect, Schema } from 'effect'
16
16
 
17
17
  import type { RecordIdInput } from '../../db/record-id'
18
18
  import { ensureRecordId } from '../../db/record-id'
@@ -25,10 +25,11 @@ import { isExecutableConditionExpression, readPathValue } from './plan-helpers'
25
25
  const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skipped', 'scheduled', 'monitoring'])
26
26
  const HUMAN_NODE_TYPE_SET = new Set<string>(HUMAN_NODE_TYPE_VALUES)
27
27
  const STRUCTURAL_NODE_TYPE_SET = new Set<string>(STRUCTURAL_NODE_TYPE_VALUES)
28
+ const decodeJsonLiteralEffect = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)
28
29
 
29
30
  function parseLiteralValue(raw: string): Effect.Effect<unknown> {
30
31
  const trimmed = raw.trim()
31
- if (!trimmed.length) return Effect.succeed(undefined)
32
+ if (!trimmed.length) return Effect.void
32
33
  if (trimmed === 'true') return Effect.succeed(true)
33
34
  if (trimmed === 'false') return Effect.succeed(false)
34
35
  if (trimmed === 'null') return Effect.succeed(null)
@@ -41,9 +42,7 @@ function parseLiteralValue(raw: string): Effect.Effect<unknown> {
41
42
  return Effect.succeed(trimmed.slice(1, -1))
42
43
  }
43
44
 
44
- return Effect.try({ try: (): unknown => JSON.parse(trimmed), catch: (cause) => cause }).pipe(
45
- Effect.catch(() => Effect.succeed(trimmed)),
46
- )
45
+ return decodeJsonLiteralEffect(trimmed).pipe(Effect.catch(() => Effect.succeed(trimmed)))
47
46
  }
48
47
 
49
48
  function buildArtifactContext(artifacts: Array<{ name: string; kind: string; payload?: unknown }>) {
@@ -149,7 +148,53 @@ type PlanNodeRunUpdate = Omit<
149
148
  completedAt?: string | Date | null
150
149
  }
151
150
 
151
+ function patchField<K extends string, V>(
152
+ key: K,
153
+ patch: V | null | undefined,
154
+ existing: V | undefined,
155
+ ): Partial<Record<K, V>> {
156
+ if (patch === null) return {}
157
+ if (patch !== undefined) return { [key]: patch } as Partial<Record<K, V>>
158
+ if (existing !== undefined) return { [key]: existing } as Partial<Record<K, V>>
159
+ return {}
160
+ }
161
+
152
162
  export function toNodeRunData(nodeRun: PlanNodeRunRecord, patch: PlanNodeRunUpdate) {
163
+ const patchedAttemptId =
164
+ patch.latestAttemptId === null
165
+ ? null
166
+ : patch.latestAttemptId !== undefined
167
+ ? ensureRecordId(patch.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT)
168
+ : undefined
169
+ const existingAttemptId =
170
+ nodeRun.latestAttemptId !== undefined
171
+ ? ensureRecordId(nodeRun.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT)
172
+ : undefined
173
+
174
+ const patchedScheduledAt =
175
+ patch.scheduledAt === null
176
+ ? null
177
+ : patch.scheduledAt !== undefined
178
+ ? toDatabaseDateTime(patch.scheduledAt)
179
+ : undefined
180
+ const existingScheduledAt = nodeRun.scheduledAt !== undefined ? toDatabaseDateTime(nodeRun.scheduledAt) : undefined
181
+
182
+ const patchedReadyAt =
183
+ patch.readyAt === null ? null : patch.readyAt !== undefined ? toDatabaseDateTime(patch.readyAt) : undefined
184
+ const existingReadyAt = nodeRun.readyAt !== undefined ? toDatabaseDateTime(nodeRun.readyAt) : undefined
185
+
186
+ const patchedStartedAt =
187
+ patch.startedAt === null ? null : patch.startedAt !== undefined ? toDatabaseDateTime(patch.startedAt) : undefined
188
+ const existingStartedAt = nodeRun.startedAt !== undefined ? toDatabaseDateTime(nodeRun.startedAt) : undefined
189
+
190
+ const patchedCompletedAt =
191
+ patch.completedAt === null
192
+ ? null
193
+ : patch.completedAt !== undefined
194
+ ? toDatabaseDateTime(patch.completedAt)
195
+ : undefined
196
+ const existingCompletedAt = nodeRun.completedAt !== undefined ? toDatabaseDateTime(nodeRun.completedAt) : undefined
197
+
153
198
  return {
154
199
  runId: ensureRecordId(nodeRun.runId, TABLES.PLAN_RUN),
155
200
  planSpecId: ensureRecordId(nodeRun.planSpecId, TABLES.PLAN_SPEC),
@@ -157,76 +202,16 @@ export function toNodeRunData(nodeRun: PlanNodeRunRecord, patch: PlanNodeRunUpda
157
202
  status: patch.status ?? nodeRun.status,
158
203
  attemptCount: patch.attemptCount ?? nodeRun.attemptCount,
159
204
  retryCount: patch.retryCount ?? nodeRun.retryCount,
160
- ...(patch.resolvedInput === null
161
- ? {}
162
- : patch.resolvedInput !== undefined
163
- ? { resolvedInput: patch.resolvedInput }
164
- : nodeRun.resolvedInput
165
- ? { resolvedInput: nodeRun.resolvedInput }
166
- : {}),
167
- ...(patch.latestStructuredOutput === null
168
- ? {}
169
- : patch.latestStructuredOutput !== undefined
170
- ? { latestStructuredOutput: patch.latestStructuredOutput }
171
- : nodeRun.latestStructuredOutput
172
- ? { latestStructuredOutput: nodeRun.latestStructuredOutput }
173
- : {}),
174
- ...(patch.latestNotes === null
175
- ? {}
176
- : patch.latestNotes !== undefined
177
- ? { latestNotes: patch.latestNotes }
178
- : nodeRun.latestNotes
179
- ? { latestNotes: nodeRun.latestNotes }
180
- : {}),
181
- ...(patch.latestAttemptId === null
182
- ? {}
183
- : patch.latestAttemptId !== undefined
184
- ? { latestAttemptId: ensureRecordId(patch.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT) }
185
- : nodeRun.latestAttemptId
186
- ? { latestAttemptId: ensureRecordId(nodeRun.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT) }
187
- : {}),
188
- ...(patch.blockedReason === null
189
- ? {}
190
- : patch.blockedReason !== undefined
191
- ? { blockedReason: patch.blockedReason }
192
- : nodeRun.blockedReason
193
- ? { blockedReason: nodeRun.blockedReason }
194
- : {}),
195
- ...(patch.failureClass === null
196
- ? {}
197
- : patch.failureClass !== undefined
198
- ? { failureClass: patch.failureClass }
199
- : nodeRun.failureClass
200
- ? { failureClass: nodeRun.failureClass }
201
- : {}),
202
- ...(patch.scheduledAt === null
203
- ? {}
204
- : patch.scheduledAt !== undefined
205
- ? { scheduledAt: toDatabaseDateTime(patch.scheduledAt) }
206
- : nodeRun.scheduledAt
207
- ? { scheduledAt: toDatabaseDateTime(nodeRun.scheduledAt) }
208
- : {}),
209
- ...(patch.readyAt === null
210
- ? {}
211
- : patch.readyAt !== undefined
212
- ? { readyAt: toDatabaseDateTime(patch.readyAt) }
213
- : nodeRun.readyAt
214
- ? { readyAt: toDatabaseDateTime(nodeRun.readyAt) }
215
- : {}),
216
- ...(patch.startedAt === null
217
- ? {}
218
- : patch.startedAt !== undefined
219
- ? { startedAt: toDatabaseDateTime(patch.startedAt) }
220
- : nodeRun.startedAt
221
- ? { startedAt: toDatabaseDateTime(nodeRun.startedAt) }
222
- : {}),
223
- ...(patch.completedAt === null
224
- ? {}
225
- : patch.completedAt !== undefined
226
- ? { completedAt: toDatabaseDateTime(patch.completedAt) }
227
- : nodeRun.completedAt
228
- ? { completedAt: toDatabaseDateTime(nodeRun.completedAt) }
229
- : {}),
205
+ ...patchField('resolvedInput', patch.resolvedInput, nodeRun.resolvedInput ?? undefined),
206
+ ...patchField('latestStructuredOutput', patch.latestStructuredOutput, nodeRun.latestStructuredOutput ?? undefined),
207
+ ...patchField('latestNotes', patch.latestNotes, nodeRun.latestNotes ?? undefined),
208
+ ...patchField('latestAttemptId', patchedAttemptId, existingAttemptId),
209
+ ...patchField('blockedReason', patch.blockedReason, nodeRun.blockedReason ?? undefined),
210
+ ...patchField('failureClass', patch.failureClass, nodeRun.failureClass ?? undefined),
211
+ ...patchField('scheduledAt', patchedScheduledAt, existingScheduledAt),
212
+ ...patchField('readyAt', patchedReadyAt, existingReadyAt),
213
+ ...patchField('startedAt', patchedStartedAt, existingStartedAt),
214
+ ...patchField('completedAt', patchedCompletedAt, existingCompletedAt),
230
215
  }
231
216
  }
232
217