@lota-sdk/core 0.4.10 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/package.json +2 -2
  2. package/src/ai-gateway/ai-gateway.ts +149 -95
  3. package/src/ai-gateway/index.ts +16 -1
  4. package/src/config/agent-defaults.ts +4 -120
  5. package/src/config/logger.ts +18 -34
  6. package/src/config/thread-defaults.ts +1 -18
  7. package/src/create-runtime.ts +90 -28
  8. package/src/db/base.service.ts +30 -38
  9. package/src/db/service.ts +489 -545
  10. package/src/effect/index.ts +0 -2
  11. package/src/effect/layers.ts +6 -13
  12. package/src/embeddings/provider.ts +2 -7
  13. package/src/index.ts +4 -5
  14. package/src/queues/autonomous-job.queue.ts +159 -113
  15. package/src/queues/context-compaction.queue.ts +39 -25
  16. package/src/queues/delayed-node-promotion.queue.ts +56 -29
  17. package/src/queues/document-processor.queue.ts +5 -3
  18. package/src/queues/index.ts +1 -0
  19. package/src/queues/memory-consolidation.queue.ts +79 -53
  20. package/src/queues/organization-learning.queue.ts +63 -39
  21. package/src/queues/plan-agent-heartbeat.queue.ts +104 -79
  22. package/src/queues/plan-scheduler.queue.ts +100 -84
  23. package/src/queues/post-chat-memory.queue.ts +55 -33
  24. package/src/queues/queue-factory.ts +40 -41
  25. package/src/queues/queues.service.ts +61 -0
  26. package/src/queues/title-generation.queue.ts +42 -31
  27. package/src/redis/org-memory-lock.ts +24 -9
  28. package/src/redis/redis-lease-lock.ts +8 -1
  29. package/src/runtime/agent-identity-overrides.ts +7 -3
  30. package/src/runtime/agent-runtime-policy.ts +9 -4
  31. package/src/runtime/agent-stream-helpers.ts +9 -4
  32. package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
  33. package/src/runtime/context-compaction/context-compaction.ts +9 -7
  34. package/src/runtime/domain-layer.ts +15 -4
  35. package/src/runtime/execution-plan-visibility.ts +5 -2
  36. package/src/runtime/graph-designer.ts +0 -22
  37. package/src/runtime/index.ts +1 -0
  38. package/src/runtime/indexed-repositories-policy.ts +2 -6
  39. package/src/runtime/plugin-resolution.ts +29 -12
  40. package/src/runtime/post-turn-side-effects.ts +139 -141
  41. package/src/runtime/runtime-config.ts +0 -6
  42. package/src/runtime/runtime-extensions.ts +0 -54
  43. package/src/runtime/runtime-lifecycle.ts +4 -4
  44. package/src/runtime/runtime-services.ts +122 -53
  45. package/src/runtime/runtime-worker-registry.ts +113 -30
  46. package/src/runtime/social-chat/social-chat-agent-runner.ts +6 -3
  47. package/src/runtime/social-chat/social-chat-history.ts +3 -1
  48. package/src/runtime/social-chat/social-chat.ts +35 -20
  49. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +6 -5
  50. package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
  51. package/src/runtime/thread-chat-helpers.ts +18 -9
  52. package/src/runtime/thread-turn-context.ts +7 -47
  53. package/src/runtime/turn-lifecycle.ts +6 -14
  54. package/src/services/agent-activity.service.ts +168 -175
  55. package/src/services/agent-executor.service.ts +35 -16
  56. package/src/services/attachment.service.ts +4 -70
  57. package/src/services/autonomous-job.service.ts +53 -61
  58. package/src/services/context-compaction.service.ts +7 -9
  59. package/src/services/execution-plan/execution-plan-graph.ts +106 -115
  60. package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
  61. package/src/services/execution-plan/execution-plan.service.ts +67 -50
  62. package/src/services/global-orchestrator.service.ts +18 -7
  63. package/src/services/graph-full-routing.ts +7 -6
  64. package/src/services/memory/memory-conversation.ts +10 -5
  65. package/src/services/memory/memory.service.ts +11 -8
  66. package/src/services/ownership-dispatcher.service.ts +16 -5
  67. package/src/services/plan/plan-agent-heartbeat.service.ts +29 -15
  68. package/src/services/plan/plan-agent-query.service.ts +12 -8
  69. package/src/services/plan/plan-completion-side-effects.ts +93 -101
  70. package/src/services/plan/plan-cycle.service.ts +7 -45
  71. package/src/services/plan/plan-deadline.service.ts +28 -17
  72. package/src/services/plan/plan-event-delivery.service.ts +47 -40
  73. package/src/services/plan/plan-executor-context.ts +2 -0
  74. package/src/services/plan/plan-executor-graph.ts +366 -391
  75. package/src/services/plan/plan-executor.service.ts +13 -91
  76. package/src/services/plan/plan-scheduler.service.ts +62 -49
  77. package/src/services/plan/plan-transaction-events.ts +1 -1
  78. package/src/services/recent-activity-title.service.ts +6 -2
  79. package/src/services/thread/thread-bootstrap.ts +11 -9
  80. package/src/services/thread/thread-message.service.ts +6 -5
  81. package/src/services/thread/thread-turn-execution.ts +86 -82
  82. package/src/services/thread/thread-turn-preparation.service.ts +47 -24
  83. package/src/services/thread/thread-turn-streaming.ts +20 -25
  84. package/src/services/thread/thread-turn.ts +25 -44
  85. package/src/services/thread/thread.service.ts +21 -6
  86. package/src/system-agents/recent-activity-title-refiner.agent.ts +8 -5
  87. package/src/system-agents/thread-router.agent.ts +23 -20
  88. package/src/tools/execution-plan.tool.ts +8 -3
  89. package/src/tools/fetch-webpage.tool.ts +10 -9
  90. package/src/tools/firecrawl-client.ts +0 -15
  91. package/src/tools/remember-memory.tool.ts +3 -6
  92. package/src/tools/research-topic.tool.ts +12 -3
  93. package/src/tools/search-web.tool.ts +10 -9
  94. package/src/tools/search.tool.ts +4 -5
  95. package/src/tools/team-think.tool.ts +139 -121
  96. package/src/workers/bootstrap.ts +9 -10
  97. package/src/workers/memory-consolidation.worker.ts +4 -1
  98. package/src/workers/organization-learning.worker.ts +15 -2
  99. package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
  100. package/src/workers/regular-chat-memory-digest.runner.ts +21 -14
  101. package/src/workers/skill-extraction.runner.ts +13 -15
  102. package/src/workers/worker-utils.ts +6 -18
  103. package/src/effect/awaitable-effect.ts +0 -96
  104. package/src/effect/runtime-ref.ts +0 -25
  105. package/src/effect/runtime.ts +0 -46
  106. package/src/redis/runtime-connection.ts +0 -20
  107. package/src/runtime/runtime-accessors.ts +0 -92
  108. package/src/runtime/runtime-token.ts +0 -47
@@ -14,7 +14,6 @@ import { ensureRecordId, recordIdToString } from '../../db/record-id'
14
14
  import type { DatabaseTransaction } from '../../db/service'
15
15
  import { TABLES } from '../../db/tables'
16
16
  import { NotFoundError } from '../../effect/errors'
17
- import { runPromise } from '../../effect/runtime'
18
17
  import { nowDate } from '../../utils/date-time'
19
18
  import type { PlanExecutorContext } from './plan-executor-context'
20
19
  import {
@@ -28,8 +27,6 @@ import {
28
27
  } from './plan-executor-helpers'
29
28
  import { emitEvent, replaceRun } from './plan-executor-persistence'
30
29
 
31
- const delayedNodePromotionQueueModule = Effect.tryPromise(() => import('../../queues/delayed-node-promotion.queue'))
32
-
33
30
  class PlanExecutorGraphError extends Schema.TaggedErrorClass<PlanExecutorGraphError>()('PlanExecutorGraphError', {
34
31
  message: Schema.String,
35
32
  cause: Schema.optional(Schema.Defect),
@@ -66,252 +63,203 @@ export function syncRunGraph(
66
63
  emittedBy: string
67
64
  capturedEvents?: PlanEventRecord[]
68
65
  },
69
- ): Promise<{
70
- run: PlanRunRecord
71
- nodeRuns: PlanNodeRunRecord[]
72
- artifacts: Array<{
73
- id: RecordIdInput
74
- nodeId: string
75
- name: string
76
- kind: string
77
- pointer: string
78
- schemaRef?: string
79
- payload?: unknown
80
- }>
81
- }> {
82
- return runPromise(
83
- Effect.gen(function* () {
84
- const { planApprovalService, planCoordinationService, planSchedulerService } = context
85
- const currentTime = nowDate()
86
- let currentRun = params.run
87
- let currentNodeRuns = [...params.nodeRuns]
88
- const currentArtifacts = [...params.artifacts]
89
- const sortedNodeSpecs = [...params.nodeSpecs].sort((left, right) => left.position - right.position)
90
- const dependencies = params.spec.dependencies
91
-
92
- const updateNodeRun = (nodeRun: PlanNodeRunRecord, patch: Parameters<typeof toNodeRunData>[1]) =>
93
- params.tx
94
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
95
- .content(toNodeRunData(nodeRun, patch))
96
- .output('after')
97
-
98
- const replaceNodeRun = (nextNodeRun: PlanNodeRunRecord) => {
99
- currentNodeRuns = currentNodeRuns.map((candidate) =>
100
- candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
101
- )
66
+ ) {
67
+ return Effect.gen(function* () {
68
+ const { planApprovalService, planCoordinationService, planSchedulerService, delayedNodePromotionQueue } = context
69
+ const currentTime = nowDate()
70
+ let currentRun = params.run
71
+ let currentNodeRuns = [...params.nodeRuns]
72
+ const currentArtifacts = [...params.artifacts]
73
+ const sortedNodeSpecs = [...params.nodeSpecs].sort((left, right) => left.position - right.position)
74
+ const dependencies = params.spec.dependencies
75
+
76
+ const updateNodeRun = (nodeRun: PlanNodeRunRecord, patch: Parameters<typeof toNodeRunData>[1]) =>
77
+ params.tx
78
+ .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
79
+ .content(toNodeRunData(nodeRun, patch))
80
+ .output('after')
81
+
82
+ const replaceNodeRun = (nextNodeRun: PlanNodeRunRecord) => {
83
+ currentNodeRuns = currentNodeRuns.map((candidate) =>
84
+ candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
85
+ )
86
+ }
87
+
88
+ const getNodeRunsById = () => new Map(currentNodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun]))
89
+ const getArtifactsByNodeId = () =>
90
+ currentArtifacts.reduce((groups, artifact) => {
91
+ const list = groups.get(artifact.nodeId) ?? []
92
+ list.push(artifact)
93
+ groups.set(artifact.nodeId, list)
94
+ return groups
95
+ }, new Map<string, typeof currentArtifacts>())
96
+
97
+ if (dependencies && dependencies.length > 0) {
98
+ const { unresolved } = yield* planCoordinationService.resolveDependencies({
99
+ dependencies,
100
+ threadId: recordIdToString(params.spec.threadId, TABLES.THREAD),
101
+ })
102
+ if (unresolved.length > 0) {
103
+ currentRun = yield* replaceRun(params.tx, currentRun, { status: 'blocked', readyNodeIds: [] })
104
+ yield* emitEvent({
105
+ tx: params.tx,
106
+ run: currentRun,
107
+ spec: params.spec,
108
+ eventType: 'run-status-changed',
109
+ fromStatus: params.run.status,
110
+ toStatus: currentRun.status,
111
+ message: `Run blocked: unresolved cross-plan dependencies (${unresolved.map((d) => d.sourcePlanSpecId).join(', ')}).`,
112
+ emittedBy: params.emittedBy,
113
+ capturedEvents: params.capturedEvents,
114
+ })
115
+ return { run: currentRun, nodeRuns: currentNodeRuns, artifacts: currentArtifacts }
102
116
  }
117
+ }
103
118
 
104
- const getNodeRunsById = () => new Map(currentNodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun]))
105
- const getArtifactsByNodeId = () =>
106
- currentArtifacts.reduce((groups, artifact) => {
107
- const list = groups.get(artifact.nodeId) ?? []
108
- list.push(artifact)
109
- groups.set(artifact.nodeId, list)
110
- return groups
111
- }, new Map<string, typeof currentArtifacts>())
119
+ let changed = true
120
+ while (changed) {
121
+ changed = false
122
+ const nodeRunsById = getNodeRunsById()
123
+ const artifactsByNodeId = getArtifactsByNodeId()
124
+
125
+ for (const nodeSpec of sortedNodeSpecs) {
126
+ const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
127
+ if (!nodeRun || nodeRun.status !== 'pending') continue
128
+
129
+ const upstreamRuns = nodeSpec.upstreamNodeIds
130
+ .map((nodeId) => nodeRunsById.get(nodeId))
131
+ .filter(Boolean) as PlanNodeRunRecord[]
132
+ if (
133
+ nodeSpec.upstreamNodeIds.length > 0 &&
134
+ !upstreamRuns.every((upstreamRun) => isSuccessfulTerminalStatus(upstreamRun.status))
135
+ ) {
136
+ continue
137
+ }
112
138
 
113
- if (dependencies && dependencies.length > 0) {
114
- const { unresolved } = yield* planCoordinationService.resolveDependencies({
115
- dependencies,
116
- threadId: recordIdToString(params.spec.threadId, TABLES.THREAD),
117
- })
118
- if (unresolved.length > 0) {
119
- currentRun = yield* replaceRun(params.tx, currentRun, { status: 'blocked', readyNodeIds: [] })
139
+ const activeIncomingEdges: typeof params.spec.edges = []
140
+ for (const edge of params.spec.edges) {
141
+ if (edge.target !== nodeSpec.nodeId) continue
142
+ const sourceRun = nodeRunsById.get(edge.source)
143
+ if (!sourceRun) continue
144
+ const context = buildNodeContext({ nodeRun: sourceRun, artifacts: artifactsByNodeId.get(edge.source) ?? [] })
145
+ if (yield* evaluateCondition(edge.when, context)) {
146
+ activeIncomingEdges.push(edge)
147
+ }
148
+ }
149
+
150
+ if (nodeSpec.upstreamNodeIds.length > 0 && activeIncomingEdges.length === 0) {
151
+ const skippedNodeRunRow = yield* updateNodeRun(nodeRun, {
152
+ status: 'skipped',
153
+ completedAt: currentTime,
154
+ blockedReason: null,
155
+ failureClass: null,
156
+ })
157
+ const skippedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, skippedNodeRunRow, 'plan node run')
158
+ replaceNodeRun(skippedNodeRun)
120
159
  yield* emitEvent({
121
160
  tx: params.tx,
122
161
  run: currentRun,
123
162
  spec: params.spec,
124
- eventType: 'run-status-changed',
125
- fromStatus: params.run.status,
126
- toStatus: currentRun.status,
127
- message: `Run blocked: unresolved cross-plan dependencies (${unresolved.map((d) => d.sourcePlanSpecId).join(', ')}).`,
163
+ nodeId: skippedNodeRun.nodeId,
164
+ eventType: 'node-skipped',
165
+ fromStatus: nodeRun.status,
166
+ toStatus: skippedNodeRun.status,
167
+ message: `Node "${nodeSpec.label}" was skipped because no inbound branch was activated.`,
128
168
  emittedBy: params.emittedBy,
129
169
  capturedEvents: params.capturedEvents,
130
170
  })
131
- return { run: currentRun, nodeRuns: currentNodeRuns, artifacts: currentArtifacts }
171
+ changed = true
172
+ continue
132
173
  }
133
- }
134
-
135
- let changed = true
136
- while (changed) {
137
- changed = false
138
- const nodeRunsById = getNodeRunsById()
139
- const artifactsByNodeId = getArtifactsByNodeId()
140
-
141
- for (const nodeSpec of sortedNodeSpecs) {
142
- const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
143
- if (!nodeRun || nodeRun.status !== 'pending') continue
144
174
 
145
- const upstreamRuns = nodeSpec.upstreamNodeIds
146
- .map((nodeId) => nodeRunsById.get(nodeId))
147
- .filter(Boolean) as PlanNodeRunRecord[]
148
- if (
149
- nodeSpec.upstreamNodeIds.length > 0 &&
150
- !upstreamRuns.every((upstreamRun) => isSuccessfulTerminalStatus(upstreamRun.status))
151
- ) {
152
- continue
153
- }
175
+ const resolvedInput = yield* buildResolvedInput({
176
+ spec: params.spec,
177
+ nodeSpec,
178
+ nodeRunsById,
179
+ artifactsByNodeId,
180
+ })
154
181
 
155
- const activeIncomingEdges: typeof params.spec.edges = []
156
- for (const edge of params.spec.edges) {
157
- if (edge.target !== nodeSpec.nodeId) continue
158
- const sourceRun = nodeRunsById.get(edge.source)
159
- if (!sourceRun) continue
160
- const context = buildNodeContext({
161
- nodeRun: sourceRun,
162
- artifacts: artifactsByNodeId.get(edge.source) ?? [],
163
- })
164
- if (yield* evaluateCondition(edge.when, context)) {
165
- activeIncomingEdges.push(edge)
166
- }
167
- }
182
+ const nodeSchedule = nodeSpec.schedule
183
+ const hasNonImmediateSchedule = nodeSchedule && nodeSchedule.type !== 'immediate'
168
184
 
169
- if (nodeSpec.upstreamNodeIds.length > 0 && activeIncomingEdges.length === 0) {
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')
177
- replaceNodeRun(skippedNodeRun)
178
- yield* emitEvent({
179
- tx: params.tx,
180
- run: currentRun,
181
- spec: params.spec,
182
- nodeId: skippedNodeRun.nodeId,
183
- eventType: 'node-skipped',
184
- fromStatus: nodeRun.status,
185
- toStatus: skippedNodeRun.status,
186
- message: `Node "${nodeSpec.label}" was skipped because no inbound branch was activated.`,
187
- emittedBy: params.emittedBy,
188
- capturedEvents: params.capturedEvents,
189
- })
190
- changed = true
191
- continue
192
- }
193
-
194
- const resolvedInput = yield* buildResolvedInput({
185
+ if (hasNonImmediateSchedule) {
186
+ const scheduledNodeRunRow = yield* updateNodeRun(nodeRun, {
187
+ status: 'scheduled',
188
+ resolvedInput,
189
+ scheduledAt: currentTime,
190
+ })
191
+ const scheduledNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, scheduledNodeRunRow, 'plan node run')
192
+ replaceNodeRun(scheduledNodeRun)
193
+ yield* planSchedulerService.createSchedule({
194
+ organizationId: currentRun.organizationId,
195
+ threadId: currentRun.threadId,
196
+ planSpecId: params.spec.id,
197
+ runId: currentRun.id,
198
+ nodeId: nodeSpec.nodeId,
199
+ scheduleSpec: nodeSchedule,
200
+ })
201
+ yield* emitEvent({
202
+ tx: params.tx,
203
+ run: currentRun,
195
204
  spec: params.spec,
196
- nodeSpec,
197
- nodeRunsById,
198
- artifactsByNodeId,
205
+ nodeId: scheduledNodeRun.nodeId,
206
+ eventType: 'node-scheduled',
207
+ fromStatus: nodeRun.status,
208
+ toStatus: scheduledNodeRun.status,
209
+ message: `Node "${nodeSpec.label}" is scheduled (${nodeSchedule.type}).`,
210
+ emittedBy: params.emittedBy,
211
+ capturedEvents: params.capturedEvents,
199
212
  })
200
-
201
- const nodeSchedule = nodeSpec.schedule
202
- const hasNonImmediateSchedule = nodeSchedule && nodeSchedule.type !== 'immediate'
203
-
204
- if (hasNonImmediateSchedule) {
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')
211
- replaceNodeRun(scheduledNodeRun)
212
- yield* planSchedulerService.createSchedule({
213
- organizationId: currentRun.organizationId,
214
- threadId: currentRun.threadId,
215
- planSpecId: params.spec.id,
216
- runId: currentRun.id,
217
- nodeId: nodeSpec.nodeId,
218
- scheduleSpec: nodeSchedule,
219
- })
220
- yield* emitEvent({
221
- tx: params.tx,
222
- run: currentRun,
223
- spec: params.spec,
224
- nodeId: scheduledNodeRun.nodeId,
225
- eventType: 'node-scheduled',
226
- fromStatus: nodeRun.status,
227
- toStatus: scheduledNodeRun.status,
228
- message: `Node "${nodeSpec.label}" is scheduled (${nodeSchedule.type}).`,
229
- emittedBy: params.emittedBy,
230
- capturedEvents: params.capturedEvents,
231
- })
232
- changed = true
233
- } else if (nodeSpec.delayAfterPredecessorMs) {
234
- const delayAfterPredecessorMs = nodeSpec.delayAfterPredecessorMs
235
- const { enqueueDelayedNodePromotion } = yield* delayedNodePromotionQueueModule
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')
242
- replaceNodeRun(scheduledNodeRun)
243
- yield* Effect.tryPromise(() =>
244
- enqueueDelayedNodePromotion(
245
- {
246
- runId: recordIdToString(currentRun.id, TABLES.PLAN_RUN),
247
- nodeId: nodeSpec.nodeId,
248
- emittedBy: params.emittedBy,
249
- },
250
- delayAfterPredecessorMs,
251
- ),
252
- )
253
- yield* emitEvent({
254
- tx: params.tx,
255
- run: currentRun,
256
- spec: params.spec,
257
- nodeId: scheduledNodeRun.nodeId,
258
- eventType: 'node-scheduled',
259
- fromStatus: nodeRun.status,
260
- toStatus: scheduledNodeRun.status,
261
- message: `Node "${nodeSpec.label}" is delayed by ${delayAfterPredecessorMs}ms after predecessor.`,
262
- emittedBy: params.emittedBy,
263
- capturedEvents: params.capturedEvents,
264
- })
265
- changed = true
266
- } else {
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')
273
- replaceNodeRun(readyNodeRun)
274
- yield* emitEvent({
275
- tx: params.tx,
276
- run: currentRun,
277
- spec: params.spec,
278
- nodeId: readyNodeRun.nodeId,
279
- eventType: 'node-ready',
280
- fromStatus: nodeRun.status,
281
- toStatus: readyNodeRun.status,
282
- message: `Node "${nodeSpec.label}" is ready to execute.`,
283
- emittedBy: params.emittedBy,
284
- capturedEvents: params.capturedEvents,
285
- })
286
- changed = true
287
- }
288
- }
289
-
290
- const readyStructuralNodes = sortedNodeSpecs.filter((nodeSpec) => {
291
- const nodeRun = getNodeRunsById().get(nodeSpec.nodeId)
292
- return nodeRun?.status === 'ready' && isStructuralNodeType(nodeSpec.type)
293
- })
294
-
295
- for (const nodeSpec of readyStructuralNodes) {
296
- const nodeRun = getNodeRunsById().get(nodeSpec.nodeId)
297
- if (!nodeRun) continue
298
-
299
- const completedNodeRunRow = yield* updateNodeRun(nodeRun, {
300
- status: 'completed',
301
- startedAt: nodeRun.startedAt ?? currentTime,
302
- completedAt: currentTime,
213
+ changed = true
214
+ } else if (nodeSpec.delayAfterPredecessorMs) {
215
+ const delayAfterPredecessorMs = nodeSpec.delayAfterPredecessorMs
216
+ const scheduledNodeRunRow = yield* updateNodeRun(nodeRun, {
217
+ status: 'scheduled',
218
+ resolvedInput,
219
+ scheduledAt: currentTime,
303
220
  })
304
- const completedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, completedNodeRunRow, 'plan node run')
305
- replaceNodeRun(completedNodeRun)
221
+ const scheduledNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, scheduledNodeRunRow, 'plan node run')
222
+ replaceNodeRun(scheduledNodeRun)
223
+ yield* Effect.tryPromise(() =>
224
+ delayedNodePromotionQueue.enqueueDelayedNodePromotion(
225
+ {
226
+ runId: recordIdToString(currentRun.id, TABLES.PLAN_RUN),
227
+ nodeId: nodeSpec.nodeId,
228
+ emittedBy: params.emittedBy,
229
+ },
230
+ delayAfterPredecessorMs,
231
+ ),
232
+ )
306
233
  yield* emitEvent({
307
234
  tx: params.tx,
308
235
  run: currentRun,
309
236
  spec: params.spec,
310
- nodeId: completedNodeRun.nodeId,
311
- eventType: 'node-auto-completed',
237
+ nodeId: scheduledNodeRun.nodeId,
238
+ eventType: 'node-scheduled',
312
239
  fromStatus: nodeRun.status,
313
- toStatus: completedNodeRun.status,
314
- message: `Structural node "${nodeSpec.label}" auto-completed.`,
240
+ toStatus: scheduledNodeRun.status,
241
+ message: `Node "${nodeSpec.label}" is delayed by ${delayAfterPredecessorMs}ms after predecessor.`,
242
+ emittedBy: params.emittedBy,
243
+ capturedEvents: params.capturedEvents,
244
+ })
245
+ changed = true
246
+ } else {
247
+ const readyNodeRunRow = yield* updateNodeRun(nodeRun, {
248
+ status: 'ready',
249
+ resolvedInput,
250
+ readyAt: currentTime,
251
+ })
252
+ const readyNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, readyNodeRunRow, 'plan node run')
253
+ replaceNodeRun(readyNodeRun)
254
+ yield* emitEvent({
255
+ tx: params.tx,
256
+ run: currentRun,
257
+ spec: params.spec,
258
+ nodeId: readyNodeRun.nodeId,
259
+ eventType: 'node-ready',
260
+ fromStatus: nodeRun.status,
261
+ toStatus: readyNodeRun.status,
262
+ message: `Node "${nodeSpec.label}" is ready to execute.`,
315
263
  emittedBy: params.emittedBy,
316
264
  capturedEvents: params.capturedEvents,
317
265
  })
@@ -319,63 +267,135 @@ export function syncRunGraph(
319
267
  }
320
268
  }
321
269
 
322
- const nodeRunsById = getNodeRunsById()
323
- const activeRunningNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'running')
324
- const activeHumanNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'awaiting-human')
325
- const activeMonitoringNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'monitoring')
326
- const hasScheduledOrMonitoring = currentNodeRuns.some(
327
- (nodeRun) => nodeRun.status === 'scheduled' || nodeRun.status === 'monitoring',
328
- )
270
+ const readyStructuralNodes = sortedNodeSpecs.filter((nodeSpec) => {
271
+ const nodeRun = getNodeRunsById().get(nodeSpec.nodeId)
272
+ return nodeRun?.status === 'ready' && isStructuralNodeType(nodeSpec.type)
273
+ })
274
+
275
+ for (const nodeSpec of readyStructuralNodes) {
276
+ const nodeRun = getNodeRunsById().get(nodeSpec.nodeId)
277
+ if (!nodeRun) continue
329
278
 
330
- if (!activeRunningNode && !activeHumanNode && !activeMonitoringNode) {
331
- const nextHumanNodeSpec = sortedNodeSpecs.find((nodeSpec) => {
279
+ const completedNodeRunRow = yield* updateNodeRun(nodeRun, {
280
+ status: 'completed',
281
+ startedAt: nodeRun.startedAt ?? currentTime,
282
+ completedAt: currentTime,
283
+ })
284
+ const completedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, completedNodeRunRow, 'plan node run')
285
+ replaceNodeRun(completedNodeRun)
286
+ yield* emitEvent({
287
+ tx: params.tx,
288
+ run: currentRun,
289
+ spec: params.spec,
290
+ nodeId: completedNodeRun.nodeId,
291
+ eventType: 'node-auto-completed',
292
+ fromStatus: nodeRun.status,
293
+ toStatus: completedNodeRun.status,
294
+ message: `Structural node "${nodeSpec.label}" auto-completed.`,
295
+ emittedBy: params.emittedBy,
296
+ capturedEvents: params.capturedEvents,
297
+ })
298
+ changed = true
299
+ }
300
+ }
301
+
302
+ const nodeRunsById = getNodeRunsById()
303
+ const activeRunningNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'running')
304
+ const activeHumanNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'awaiting-human')
305
+ const activeMonitoringNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'monitoring')
306
+ const hasScheduledOrMonitoring = currentNodeRuns.some(
307
+ (nodeRun) => nodeRun.status === 'scheduled' || nodeRun.status === 'monitoring',
308
+ )
309
+
310
+ if (!activeRunningNode && !activeHumanNode && !activeMonitoringNode) {
311
+ const nextHumanNodeSpec = sortedNodeSpecs.find((nodeSpec) => {
312
+ const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
313
+ return nodeRun?.status === 'ready' && isHumanNodeType(nodeSpec.type)
314
+ })
315
+
316
+ if (nextHumanNodeSpec) {
317
+ const nodeRun = nodeRunsById.get(nextHumanNodeSpec.nodeId)
318
+ if (!nodeRun) {
319
+ return yield* new NotFoundError({
320
+ resource: 'plan node run',
321
+ id: nextHumanNodeSpec.nodeId,
322
+ message: `Expected ready node run for "${nextHumanNodeSpec.nodeId}".`,
323
+ })
324
+ }
325
+ const awaitingHumanNodeRunRow = yield* updateNodeRun(nodeRun, {
326
+ status: 'awaiting-human',
327
+ startedAt: nodeRun.startedAt ?? currentTime,
328
+ })
329
+ const awaitingHumanNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, awaitingHumanNodeRunRow, 'plan node run')
330
+ replaceNodeRun(awaitingHumanNodeRun)
331
+
332
+ const approval = yield* planApprovalService.createPendingApproval({
333
+ tx: params.tx,
334
+ runId: currentRun.id,
335
+ nodeRunId: awaitingHumanNodeRun.id,
336
+ nodeId: awaitingHumanNodeRun.nodeId,
337
+ requestedBy: params.emittedBy,
338
+ presented: {
339
+ nodeId: nextHumanNodeSpec.nodeId,
340
+ label: nextHumanNodeSpec.label,
341
+ objective: nextHumanNodeSpec.objective,
342
+ instructions: nextHumanNodeSpec.instructions,
343
+ deliverables: nextHumanNodeSpec.deliverables,
344
+ successCriteria: nextHumanNodeSpec.successCriteria,
345
+ resolvedInput: awaitingHumanNodeRun.resolvedInput ?? {},
346
+ },
347
+ })
348
+
349
+ currentRun = yield* replaceRun(params.tx, currentRun, {
350
+ status: 'awaiting-human',
351
+ currentNodeId: awaitingHumanNodeRun.nodeId,
352
+ waitingNodeId: awaitingHumanNodeRun.nodeId,
353
+ readyNodeIds: currentNodeRuns
354
+ .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== awaitingHumanNodeRun.nodeId)
355
+ .map((candidate) => candidate.nodeId),
356
+ })
357
+
358
+ yield* emitEvent({
359
+ tx: params.tx,
360
+ run: currentRun,
361
+ spec: params.spec,
362
+ nodeId: awaitingHumanNodeRun.nodeId,
363
+ approvalId: approval.id,
364
+ eventType: 'approval-requested',
365
+ fromStatus: params.run.status,
366
+ toStatus: currentRun.status,
367
+ message: `Node "${nextHumanNodeSpec.label}" is awaiting human input.`,
368
+ emittedBy: params.emittedBy,
369
+ capturedEvents: params.capturedEvents,
370
+ })
371
+ } else {
372
+ const nextActionNodeSpec = sortedNodeSpecs.find((nodeSpec) => {
332
373
  const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
333
- return nodeRun?.status === 'ready' && isHumanNodeType(nodeSpec.type)
374
+ return nodeRun?.status === 'ready' && !isStructuralNodeType(nodeSpec.type)
334
375
  })
335
376
 
336
- if (nextHumanNodeSpec) {
337
- const nodeRun = nodeRunsById.get(nextHumanNodeSpec.nodeId)
377
+ if (nextActionNodeSpec) {
378
+ const nodeRun = nodeRunsById.get(nextActionNodeSpec.nodeId)
338
379
  if (!nodeRun) {
339
380
  return yield* new NotFoundError({
340
381
  resource: 'plan node run',
341
- id: nextHumanNodeSpec.nodeId,
342
- message: `Expected ready node run for "${nextHumanNodeSpec.nodeId}".`,
382
+ id: nextActionNodeSpec.nodeId,
383
+ message: `Expected ready node run for "${nextActionNodeSpec.nodeId}".`,
343
384
  })
344
385
  }
345
- const awaitingHumanNodeRunRow = yield* updateNodeRun(nodeRun, {
346
- status: 'awaiting-human',
386
+ const runningNodeRunRow = yield* updateNodeRun(nodeRun, {
387
+ status: 'running',
347
388
  startedAt: nodeRun.startedAt ?? currentTime,
348
389
  })
349
- const awaitingHumanNodeRun = yield* parseRowOrFail(
350
- PlanNodeRunSchema,
351
- awaitingHumanNodeRunRow,
352
- 'plan node run',
353
- )
354
- replaceNodeRun(awaitingHumanNodeRun)
355
-
356
- const approval = yield* planApprovalService.createPendingApproval({
357
- tx: params.tx,
358
- runId: currentRun.id,
359
- nodeRunId: awaitingHumanNodeRun.id,
360
- nodeId: awaitingHumanNodeRun.nodeId,
361
- requestedBy: params.emittedBy,
362
- presented: {
363
- nodeId: nextHumanNodeSpec.nodeId,
364
- label: nextHumanNodeSpec.label,
365
- objective: nextHumanNodeSpec.objective,
366
- instructions: nextHumanNodeSpec.instructions,
367
- deliverables: nextHumanNodeSpec.deliverables,
368
- successCriteria: nextHumanNodeSpec.successCriteria,
369
- resolvedInput: awaitingHumanNodeRun.resolvedInput ?? {},
370
- },
371
- })
390
+ const runningNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, runningNodeRunRow, 'plan node run')
391
+ replaceNodeRun(runningNodeRun)
372
392
 
373
393
  currentRun = yield* replaceRun(params.tx, currentRun, {
374
- status: 'awaiting-human',
375
- currentNodeId: awaitingHumanNodeRun.nodeId,
376
- waitingNodeId: awaitingHumanNodeRun.nodeId,
394
+ status: 'running',
395
+ currentNodeId: runningNodeRun.nodeId,
396
+ waitingNodeId: null,
377
397
  readyNodeIds: currentNodeRuns
378
- .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== awaitingHumanNodeRun.nodeId)
398
+ .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
379
399
  .map((candidate) => candidate.nodeId),
380
400
  })
381
401
 
@@ -383,140 +403,95 @@ export function syncRunGraph(
383
403
  tx: params.tx,
384
404
  run: currentRun,
385
405
  spec: params.spec,
386
- nodeId: awaitingHumanNodeRun.nodeId,
387
- approvalId: approval.id,
388
- eventType: 'approval-requested',
389
- fromStatus: params.run.status,
390
- toStatus: currentRun.status,
391
- message: `Node "${nextHumanNodeSpec.label}" is awaiting human input.`,
406
+ nodeId: runningNodeRun.nodeId,
407
+ eventType: 'node-running',
408
+ fromStatus: nodeRun.status,
409
+ toStatus: runningNodeRun.status,
410
+ message: `Node "${nextActionNodeSpec.label}" is now running.`,
392
411
  emittedBy: params.emittedBy,
393
412
  capturedEvents: params.capturedEvents,
394
413
  })
395
- } else {
396
- const nextActionNodeSpec = sortedNodeSpecs.find((nodeSpec) => {
397
- const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
398
- return nodeRun?.status === 'ready' && !isStructuralNodeType(nodeSpec.type)
414
+ yield* emitEvent({
415
+ tx: params.tx,
416
+ run: currentRun,
417
+ spec: params.spec,
418
+ nodeId: runningNodeRun.nodeId,
419
+ eventType: 'ownership-transition',
420
+ message: `Execution ownership transitioned to "${nextActionNodeSpec.label}".`,
421
+ detail: {
422
+ owner: nextActionNodeSpec.owner,
423
+ fromNodeId: params.run.currentNodeId ?? null,
424
+ toNodeId: runningNodeRun.nodeId,
425
+ },
426
+ emittedBy: params.emittedBy,
427
+ capturedEvents: params.capturedEvents,
399
428
  })
429
+ } else {
430
+ const allTerminalSuccess = currentNodeRuns.every((nodeRun) => isSuccessfulTerminalStatus(nodeRun.status))
431
+ const runStatus: 'completed' | 'running' | 'blocked' = allTerminalSuccess
432
+ ? 'completed'
433
+ : hasScheduledOrMonitoring
434
+ ? 'running'
435
+ : 'blocked'
436
+ const readyIds = currentNodeRuns
437
+ .filter((candidate) => candidate.status === 'ready')
438
+ .map((candidate) => candidate.nodeId)
439
+
440
+ currentRun = yield* Match.value(runStatus).pipe(
441
+ Match.when('completed', () =>
442
+ replaceRun(params.tx, currentRun, {
443
+ status: 'completed',
444
+ currentNodeId: null,
445
+ waitingNodeId: null,
446
+ readyNodeIds: [],
447
+ completedAt: currentTime,
448
+ }),
449
+ ),
450
+ Match.when('running', () =>
451
+ replaceRun(params.tx, currentRun, {
452
+ status: 'running',
453
+ currentNodeId: null,
454
+ waitingNodeId: null,
455
+ readyNodeIds: readyIds,
456
+ }),
457
+ ),
458
+ Match.when('blocked', () =>
459
+ replaceRun(params.tx, currentRun, {
460
+ status: 'blocked',
461
+ currentNodeId: null,
462
+ waitingNodeId: null,
463
+ readyNodeIds: readyIds,
464
+ }),
465
+ ),
466
+ Match.exhaustive,
467
+ )
400
468
 
401
- if (nextActionNodeSpec) {
402
- const nodeRun = nodeRunsById.get(nextActionNodeSpec.nodeId)
403
- if (!nodeRun) {
404
- return yield* new NotFoundError({
405
- resource: 'plan node run',
406
- id: nextActionNodeSpec.nodeId,
407
- message: `Expected ready node run for "${nextActionNodeSpec.nodeId}".`,
408
- })
409
- }
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')
415
- replaceNodeRun(runningNodeRun)
416
-
417
- currentRun = yield* replaceRun(params.tx, currentRun, {
418
- status: 'running',
419
- currentNodeId: runningNodeRun.nodeId,
420
- waitingNodeId: null,
421
- readyNodeIds: currentNodeRuns
422
- .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
423
- .map((candidate) => candidate.nodeId),
424
- })
425
-
469
+ if (runStatus === 'completed') {
426
470
  yield* emitEvent({
427
471
  tx: params.tx,
428
472
  run: currentRun,
429
473
  spec: params.spec,
430
- nodeId: runningNodeRun.nodeId,
431
- eventType: 'node-running',
432
- fromStatus: nodeRun.status,
433
- toStatus: runningNodeRun.status,
434
- message: `Node "${nextActionNodeSpec.label}" is now running.`,
474
+ eventType: 'run-status-changed',
475
+ fromStatus: params.run.status,
476
+ toStatus: currentRun.status,
477
+ message: `Run "${params.spec.title}" completed.`,
435
478
  emittedBy: params.emittedBy,
436
479
  capturedEvents: params.capturedEvents,
437
480
  })
438
- yield* emitEvent({
439
- tx: params.tx,
440
- run: currentRun,
441
- spec: params.spec,
442
- nodeId: runningNodeRun.nodeId,
443
- eventType: 'ownership-transition',
444
- message: `Execution ownership transitioned to "${nextActionNodeSpec.label}".`,
445
- detail: {
446
- owner: nextActionNodeSpec.owner,
447
- fromNodeId: params.run.currentNodeId ?? null,
448
- toNodeId: runningNodeRun.nodeId,
449
- },
450
- emittedBy: params.emittedBy,
451
- capturedEvents: params.capturedEvents,
452
- })
453
- } else {
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
- }
506
481
  }
507
482
  }
508
- } else {
509
- currentRun = yield* replaceRun(params.tx, currentRun, {
510
- status: activeHumanNode ? 'awaiting-human' : 'running',
511
- currentNodeId: activeHumanNode?.nodeId ?? activeMonitoringNode?.nodeId ?? activeRunningNode?.nodeId ?? null,
512
- waitingNodeId: activeHumanNode?.nodeId ?? null,
513
- readyNodeIds: currentNodeRuns
514
- .filter((candidate) => candidate.status === 'ready')
515
- .map((candidate) => candidate.nodeId),
516
- })
517
483
  }
518
-
519
- return { run: currentRun, nodeRuns: currentNodeRuns, artifacts: currentArtifacts }
520
- }),
521
- )
484
+ } else {
485
+ currentRun = yield* replaceRun(params.tx, currentRun, {
486
+ status: activeHumanNode ? 'awaiting-human' : 'running',
487
+ currentNodeId: activeHumanNode?.nodeId ?? activeMonitoringNode?.nodeId ?? activeRunningNode?.nodeId ?? null,
488
+ waitingNodeId: activeHumanNode?.nodeId ?? null,
489
+ readyNodeIds: currentNodeRuns
490
+ .filter((candidate) => candidate.status === 'ready')
491
+ .map((candidate) => candidate.nodeId),
492
+ })
493
+ }
494
+
495
+ return { run: currentRun, nodeRuns: currentNodeRuns, artifacts: currentArtifacts }
496
+ })
522
497
  }