@lota-sdk/core 0.1.24 → 0.1.25

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 (74) hide show
  1. package/package.json +2 -2
  2. package/src/ai/definitions.ts +5 -59
  3. package/src/ai-gateway/ai-gateway.ts +36 -28
  4. package/src/ai-gateway/cache-headers.ts +9 -0
  5. package/src/config/model-constants.ts +6 -2
  6. package/src/create-runtime.ts +1 -17
  7. package/src/db/memory-types.ts +13 -8
  8. package/src/db/memory.ts +74 -53
  9. package/src/queues/autonomous-job.queue.ts +1 -8
  10. package/src/queues/context-compaction.queue.ts +2 -2
  11. package/src/queues/index.ts +2 -6
  12. package/src/queues/organization-learning.queue.ts +78 -0
  13. package/src/queues/plan-agent-heartbeat.queue.ts +10 -16
  14. package/src/queues/title-generation.queue.ts +62 -0
  15. package/src/runtime/agent-prompt-context.ts +0 -18
  16. package/src/runtime/agent-runtime-policy.ts +9 -2
  17. package/src/runtime/context-compaction-constants.ts +4 -2
  18. package/src/runtime/context-compaction.ts +135 -118
  19. package/src/runtime/memory-pipeline.ts +70 -1
  20. package/src/runtime/memory-prompts-fact.ts +16 -0
  21. package/src/runtime/plugin-resolution.ts +3 -2
  22. package/src/runtime/plugin-types.ts +1 -42
  23. package/src/runtime/post-turn-side-effects.ts +212 -0
  24. package/src/runtime/runtime-config.ts +0 -13
  25. package/src/runtime/runtime-extensions.ts +10 -16
  26. package/src/runtime/runtime-worker-registry.ts +8 -19
  27. package/src/runtime/social-chat-agent-runner.ts +119 -0
  28. package/src/runtime/social-chat-history.ts +110 -0
  29. package/src/runtime/social-chat-prompts.ts +58 -0
  30. package/src/runtime/social-chat.ts +104 -340
  31. package/src/runtime/specialist-runner.ts +18 -0
  32. package/src/runtime/workstream-chat-helpers.ts +19 -0
  33. package/src/runtime/workstream-plan-turn.ts +195 -0
  34. package/src/runtime/workstream-state.ts +11 -8
  35. package/src/runtime/workstream-turn-context.ts +183 -0
  36. package/src/services/autonomous-job.service.ts +1 -8
  37. package/src/services/execution-plan.service.ts +205 -334
  38. package/src/services/index.ts +1 -4
  39. package/src/services/memory.service.ts +54 -44
  40. package/src/services/ownership-dispatcher.service.ts +2 -19
  41. package/src/services/plan-completion-side-effects.ts +80 -0
  42. package/src/services/plan-event-delivery.service.ts +1 -1
  43. package/src/services/plan-executor.service.ts +42 -190
  44. package/src/services/plan-node-spec.ts +60 -0
  45. package/src/services/plan-run-data.ts +88 -0
  46. package/src/services/plan-validator.service.ts +10 -8
  47. package/src/services/workstream-constants.ts +2 -0
  48. package/src/services/workstream-title.service.ts +1 -1
  49. package/src/services/workstream-turn-preparation.service.ts +208 -715
  50. package/src/services/workstream.service.ts +162 -192
  51. package/src/services/workstream.types.ts +12 -44
  52. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -0
  53. package/src/tools/execution-plan.tool.ts +7 -6
  54. package/src/tools/remember-memory.tool.ts +7 -10
  55. package/src/tools/research-topic.tool.ts +1 -1
  56. package/src/tools/team-think.tool.ts +1 -1
  57. package/src/tools/user-questions.tool.ts +1 -1
  58. package/src/utils/autonomous-job-ids.ts +7 -0
  59. package/src/workers/organization-learning.worker.ts +31 -0
  60. package/src/workers/regular-chat-memory-digest.runner.ts +9 -3
  61. package/src/workers/skill-extraction.runner.ts +2 -2
  62. package/src/queues/recent-activity-title-refinement.queue.ts +0 -30
  63. package/src/queues/regular-chat-memory-digest.config.ts +0 -12
  64. package/src/queues/regular-chat-memory-digest.queue.ts +0 -34
  65. package/src/queues/skill-extraction.config.ts +0 -9
  66. package/src/queues/skill-extraction.queue.ts +0 -27
  67. package/src/queues/workstream-title-generation.queue.ts +0 -33
  68. package/src/services/context-enrichment.service.ts +0 -33
  69. package/src/services/coordination-registry.service.ts +0 -117
  70. package/src/services/domain-agent-executor.service.ts +0 -71
  71. package/src/services/memory-assessment.service.ts +0 -44
  72. package/src/services/playbook-registry.service.ts +0 -67
  73. package/src/workers/regular-chat-memory-digest.worker.ts +0 -22
  74. package/src/workers/skill-extraction.worker.ts +0 -22
@@ -1,16 +1,14 @@
1
1
  import type {
2
2
  ChatMessage,
3
- CreateExecutionPlanArgs,
4
3
  ExecutionPlanToolResultData,
5
4
  GetActiveExecutionPlanArgs,
6
5
  ListExecutionPlansSummary,
7
6
  ListExecutionPlansToolResultData,
8
7
  PlanEventRecord,
8
+ PlanDraft,
9
9
  PlanNodeRunRecord,
10
10
  PlanNodeSpecRecord,
11
- PlanRunRecord,
12
11
  PlanSpecRecord,
13
- ReplaceExecutionPlanArgs,
14
12
  ResumeExecutionPlanRunArgs,
15
13
  SerializableExecutionPlan,
16
14
  SubmitPlanTurnResultArgs,
@@ -26,7 +24,6 @@ import {
26
24
  } from '@lota-sdk/shared'
27
25
  import { RecordId } from 'surrealdb'
28
26
 
29
- import { serverLogger } from '../config/logger'
30
27
  import type { RecordIdInput } from '../db/record-id'
31
28
  import { ensureRecordId, recordIdToString } from '../db/record-id'
32
29
  import { databaseService } from '../db/service'
@@ -35,33 +32,17 @@ import { TABLES } from '../db/tables'
35
32
  import { readApprovalContinuationResponse } from '../runtime/approval-continuation'
36
33
  import { extractMessageText } from '../runtime/workstream-chat-helpers'
37
34
  import { toDatabaseDateTime } from '../utils/date-time'
38
- import { contextEnrichmentService } from './context-enrichment.service'
39
35
  import { ownershipDispatcherService } from './ownership-dispatcher.service'
40
36
  import { planBuilderService } from './plan-builder.service'
41
37
  import type { CompiledPlanNode } from './plan-compiler.service'
42
38
  import { planCompilerService } from './plan-compiler.service'
43
39
  import { planEventDeliveryService } from './plan-event-delivery.service'
44
40
  import { planExecutorService } from './plan-executor.service'
41
+ import { buildExecutionPlanToolResult, toRunData } from './plan-run-data'
45
42
  import { planRunService } from './plan-run.service'
46
43
  import { planSchedulerService } from './plan-scheduler.service'
47
44
  import { planValidatorService } from './plan-validator.service'
48
45
 
49
- function buildToolResult(params: {
50
- action: ExecutionPlanToolResultData['action']
51
- plan: SerializableExecutionPlan | null
52
- message: string
53
- changedNodeId?: string
54
- }): ExecutionPlanToolResultData {
55
- return {
56
- action: params.action,
57
- message: params.message,
58
- ...(params.changedNodeId ? { changedNodeId: params.changedNodeId } : {}),
59
- ...(params.plan ? { plan: params.plan } : {}),
60
- hasPlan: params.plan !== null,
61
- status: params.plan?.status,
62
- }
63
- }
64
-
65
46
  function aggregateBlockingIssues(issues: Array<{ code: string; message: string }>): string {
66
47
  return issues.map((issue) => `${issue.code}: ${issue.message}`).join(' | ')
67
48
  }
@@ -100,72 +81,6 @@ function toSpecData(spec: PlanSpecRecord, patch: Partial<PlanSpecRecord> & { rep
100
81
  }
101
82
  }
102
83
 
103
- type PlanRunUpdate = Omit<
104
- Partial<PlanRunRecord>,
105
- 'currentNodeId' | 'waitingNodeId' | 'replacedRunId' | 'lastCheckpointId' | 'startedAt' | 'completedAt'
106
- > & {
107
- currentNodeId?: string | null
108
- waitingNodeId?: string | null
109
- replacedRunId?: RecordIdInput | null
110
- lastCheckpointId?: RecordIdInput | null
111
- startedAt?: string | Date | null
112
- completedAt?: string | Date | null
113
- }
114
-
115
- function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
116
- return {
117
- planSpecId: ensureRecordId(run.planSpecId, TABLES.PLAN_SPEC),
118
- organizationId: ensureRecordId(run.organizationId, TABLES.ORGANIZATION),
119
- workstreamId: ensureRecordId(run.workstreamId, TABLES.WORKSTREAM),
120
- leadAgentId: patch.leadAgentId ?? run.leadAgentId,
121
- status: patch.status ?? run.status,
122
- ...(patch.currentNodeId === null
123
- ? {}
124
- : patch.currentNodeId !== undefined
125
- ? { currentNodeId: patch.currentNodeId }
126
- : run.currentNodeId
127
- ? { currentNodeId: run.currentNodeId }
128
- : {}),
129
- ...(patch.waitingNodeId === null
130
- ? {}
131
- : patch.waitingNodeId !== undefined
132
- ? { waitingNodeId: patch.waitingNodeId }
133
- : run.waitingNodeId
134
- ? { waitingNodeId: run.waitingNodeId }
135
- : {}),
136
- readyNodeIds: patch.readyNodeIds ? [...patch.readyNodeIds] : [...run.readyNodeIds],
137
- failureCount: patch.failureCount ?? run.failureCount,
138
- ...(patch.replacedRunId === null
139
- ? {}
140
- : patch.replacedRunId
141
- ? { replacedRunId: ensureRecordId(patch.replacedRunId, TABLES.PLAN_RUN) }
142
- : run.replacedRunId
143
- ? { replacedRunId: ensureRecordId(run.replacedRunId, TABLES.PLAN_RUN) }
144
- : {}),
145
- ...(patch.lastCheckpointId === null
146
- ? {}
147
- : patch.lastCheckpointId
148
- ? { lastCheckpointId: ensureRecordId(patch.lastCheckpointId, TABLES.PLAN_CHECKPOINT) }
149
- : run.lastCheckpointId
150
- ? { lastCheckpointId: ensureRecordId(run.lastCheckpointId, TABLES.PLAN_CHECKPOINT) }
151
- : {}),
152
- ...(patch.startedAt === null
153
- ? {}
154
- : patch.startedAt !== undefined
155
- ? { startedAt: toDatabaseDateTime(patch.startedAt) }
156
- : run.startedAt
157
- ? { startedAt: toDatabaseDateTime(run.startedAt) }
158
- : {}),
159
- ...(patch.completedAt === null
160
- ? {}
161
- : patch.completedAt !== undefined
162
- ? { completedAt: toDatabaseDateTime(patch.completedAt) }
163
- : run.completedAt
164
- ? { completedAt: toDatabaseDateTime(run.completedAt) }
165
- : {}),
166
- }
167
- }
168
-
169
84
  function buildApprovalResponseFromMessages(
170
85
  messages: ChatMessage[],
171
86
  ): { approvalId: string; response: Record<string, unknown> } | null {
@@ -188,6 +103,38 @@ function buildApprovalResponseFromMessages(
188
103
  return null
189
104
  }
190
105
 
106
+ function buildCompiledSpecCreateData(params: {
107
+ organizationId: RecordIdInput
108
+ workstreamId: RecordIdInput
109
+ leadAgentId: string
110
+ compiled: ReturnType<typeof planCompilerService.compile>
111
+ version: number
112
+ replacedSpecId?: RecordIdInput
113
+ contextEnrichments?: PlanSpecRecord['contextEnrichments']
114
+ }) {
115
+ return {
116
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
117
+ workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
118
+ title: params.compiled.draft.title,
119
+ objective: params.compiled.draft.objective,
120
+ version: params.version,
121
+ status: 'compiled' as const,
122
+ leadAgentId: params.leadAgentId,
123
+ schemaRegistry: structuredClone(params.compiled.draft.schemas),
124
+ defaultExecutionVisibility: params.compiled.draft.defaultExecutionVisibility,
125
+ ...(params.contextEnrichments?.length
126
+ ? { contextEnrichments: params.contextEnrichments.map((entry) => ({ ...entry })) }
127
+ : {}),
128
+ edges: [...params.compiled.draft.edges],
129
+ entryNodeIds: [...(params.compiled.draft.entryNodeIds ?? [])],
130
+ executionMode: params.compiled.draft.executionMode ?? 'linear',
131
+ ...(params.compiled.draft.schedule ? { schedule: params.compiled.draft.schedule } : {}),
132
+ ...(params.compiled.draft.dependencies ? { dependencies: params.compiled.draft.dependencies } : {}),
133
+ ...(params.replacedSpecId ? { replacedSpecId: ensureRecordId(params.replacedSpecId, TABLES.PLAN_SPEC) } : {}),
134
+ compiledAt: new Date(),
135
+ }
136
+ }
137
+
191
138
  class ExecutionPlanService {
192
139
  async hasActivePlan(workstreamId: RecordIdInput): Promise<boolean> {
193
140
  return (await planRunService.getActiveRunRecord(workstreamId)) !== null
@@ -275,12 +222,12 @@ class ExecutionPlanService {
275
222
  const plan = await planRunService
276
223
  .getRunById(params.runId)
277
224
  .then((run) => planRunService.toSerializablePlan(run, serializeOptions))
278
- return buildToolResult({ action: 'loaded', plan, message: `Loaded execution run "${plan.title}".` })
225
+ return buildExecutionPlanToolResult({ action: 'loaded', plan, message: `Loaded execution run "${plan.title}".` })
279
226
  }
280
227
 
281
228
  const runs = await planRunService.getActiveRunRecords(params.workstreamId)
282
229
  if (runs.length === 0) {
283
- return buildToolResult({ action: 'none', plan: null, message: 'No active execution run.' })
230
+ return buildExecutionPlanToolResult({ action: 'none', plan: null, message: 'No active execution run.' })
284
231
  }
285
232
 
286
233
  const plan = await planRunService.toSerializablePlan(runs[0], serializeOptions)
@@ -292,7 +239,7 @@ class ExecutionPlanService {
292
239
  )
293
240
 
294
241
  return {
295
- ...buildToolResult({
242
+ ...buildExecutionPlanToolResult({
296
243
  action: 'loaded',
297
244
  plan,
298
245
  message:
@@ -309,7 +256,7 @@ class ExecutionPlanService {
309
256
  organizationId: RecordIdInput
310
257
  workstreamId: RecordIdInput
311
258
  leadAgentId: string
312
- input: CreateExecutionPlanArgs
259
+ input: PlanDraft
313
260
  }): Promise<ExecutionPlanToolResultData> {
314
261
  const preparedDraft = planBuilderService.prepareDraft(params.input)
315
262
  const validation = planValidatorService.validateDraft(preparedDraft)
@@ -319,17 +266,6 @@ class ExecutionPlanService {
319
266
  await this.assertDispatchExecutors(preparedDraft)
320
267
  const compiled = planCompilerService.compile(preparedDraft)
321
268
 
322
- // Context enrichment — best-effort, failures do not block plan creation
323
- const enrichments = await contextEnrichmentService
324
- .enrichForPlanCreation({
325
- objective: compiled.draft.objective,
326
- organizationId: recordIdToString(params.organizationId, TABLES.ORGANIZATION),
327
- })
328
- .catch((error: unknown) => {
329
- serverLogger.error`Context enrichment failed: ${error instanceof Error ? error.message : String(error)}`
330
- return []
331
- })
332
-
333
269
  const specId = new RecordId(TABLES.PLAN_SPEC, Bun.randomUUIDv7())
334
270
  const runId = new RecordId(TABLES.PLAN_RUN, Bun.randomUUIDv7())
335
271
  const emittedEvents: PlanEventRecord[] = []
@@ -338,113 +274,31 @@ class ExecutionPlanService {
338
274
  const spec = PlanSpecSchema.parse(
339
275
  await tx
340
276
  .create(specId)
341
- .content({
342
- organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
343
- workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
344
- title: compiled.draft.title,
345
- objective: compiled.draft.objective,
346
- version: 1,
347
- status: 'compiled',
348
- leadAgentId: params.leadAgentId,
349
- schemaRegistry: structuredClone(compiled.draft.schemas),
350
- defaultExecutionVisibility: compiled.draft.defaultExecutionVisibility,
351
- ...(enrichments.length > 0
352
- ? { contextEnrichments: enrichments.map((e) => ({ type: e.domain, content: JSON.stringify(e.data) })) }
353
- : {}),
354
- edges: [...compiled.draft.edges],
355
- entryNodeIds: [...(compiled.draft.entryNodeIds ?? [])],
356
- executionMode: compiled.draft.executionMode ?? 'linear',
357
- ...(compiled.draft.schedule ? { schedule: compiled.draft.schedule } : {}),
358
- ...(compiled.draft.dependencies ? { dependencies: compiled.draft.dependencies } : {}),
359
- compiledAt: new Date(),
360
- })
361
- .output('after'),
362
- )
363
-
364
- const nodeSpecs = await this.createNodeSpecs(tx, spec.id, compiled.nodes)
365
- const run = PlanRunSchema.parse(
366
- await tx
367
- .create(runId)
368
- .content({
369
- planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
370
- organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
371
- workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
372
- leadAgentId: params.leadAgentId,
373
- status: 'running',
374
- readyNodeIds: [],
375
- failureCount: 0,
376
- startedAt: new Date(),
377
- })
277
+ .content(
278
+ buildCompiledSpecCreateData({
279
+ organizationId: params.organizationId,
280
+ workstreamId: params.workstreamId,
281
+ leadAgentId: params.leadAgentId,
282
+ compiled,
283
+ version: 1,
284
+ }),
285
+ )
378
286
  .output('after'),
379
287
  )
380
-
381
- const nodeRuns = await this.createNodeRuns(tx, run.id, spec.id, nodeSpecs)
382
- const synced = await planExecutorService.syncRunGraph({
288
+ await this.createInitializedRunGraph({
383
289
  tx,
384
- run,
290
+ runId,
385
291
  spec,
386
- nodeSpecs,
387
- nodeRuns,
388
- artifacts: [],
389
- emittedBy: params.leadAgentId,
390
- capturedEvents: emittedEvents,
292
+ organizationId: params.organizationId,
293
+ workstreamId: params.workstreamId,
294
+ leadAgentId: params.leadAgentId,
295
+ nodes: compiled.nodes,
296
+ emittedEvents,
297
+ createdEventType: 'plan-created',
298
+ createdEventMessage: `Created execution plan "${spec.title}".`,
299
+ createdEventDetail: { title: spec.title, objective: spec.objective },
300
+ checkpointReason: 'plan-created',
391
301
  })
392
-
393
- const event = await tx
394
- .create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
395
- .content({
396
- planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
397
- runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
398
- eventType: 'plan-created',
399
- message: `Created execution plan "${spec.title}".`,
400
- emittedBy: params.leadAgentId,
401
- detail: { title: spec.title, objective: spec.objective, nodeCount: nodeSpecs.length },
402
- })
403
- .output('after')
404
- emittedEvents.push(PlanEventSchema.parse(event))
405
-
406
- const checkpoint = PlanCheckpointSchema.parse(
407
- await tx
408
- .create(new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7()))
409
- .content({
410
- runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
411
- sequence: 1,
412
- runStatus: synced.run.status,
413
- readyNodeIds: [...synced.run.readyNodeIds],
414
- activeNodeIds: synced.run.currentNodeId ? [synced.run.currentNodeId] : [],
415
- artifactIds: [],
416
- lastCompletedNodeIds: synced.nodeRuns
417
- .filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
418
- .map((nodeRun) => nodeRun.nodeId),
419
- snapshot: {
420
- reason: 'plan-created',
421
- currentNodeId: synced.run.currentNodeId,
422
- waitingNodeId: synced.run.waitingNodeId,
423
- readyNodeIds: synced.run.readyNodeIds,
424
- },
425
- })
426
- .output('after'),
427
- )
428
-
429
- const updatedRun = PlanRunSchema.parse(
430
- await tx
431
- .update(ensureRecordId(synced.run.id, TABLES.PLAN_RUN))
432
- .content(toRunData(synced.run, { lastCheckpointId: checkpoint.id }))
433
- .output('after'),
434
- )
435
-
436
- const checkpointEvent = await tx
437
- .create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
438
- .content({
439
- planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
440
- runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
441
- eventType: 'checkpoint-saved',
442
- message: 'Saved checkpoint 1.',
443
- emittedBy: 'system',
444
- detail: { checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT), reason: 'plan-created' },
445
- })
446
- .output('after')
447
- emittedEvents.push(PlanEventSchema.parse(checkpointEvent))
448
302
  })
449
303
 
450
304
  await planEventDeliveryService.dispatchEvents(emittedEvents)
@@ -472,14 +326,14 @@ class ExecutionPlanService {
472
326
 
473
327
  const plan = await this.finalizePlanSnapshot({ runId, emittedBy: params.leadAgentId })
474
328
 
475
- return buildToolResult({ action: 'created', plan, message: `Created execution plan "${plan.title}".` })
329
+ return buildExecutionPlanToolResult({ action: 'created', plan, message: `Created execution plan "${plan.title}".` })
476
330
  }
477
331
 
478
332
  async replacePlan(params: {
479
333
  workstreamId: RecordIdInput
480
334
  organizationId: RecordIdInput
481
335
  leadAgentId: string
482
- input: ReplaceExecutionPlanArgs
336
+ input: PlanDraft & { runId: string; reason: string }
483
337
  }): Promise<ExecutionPlanToolResultData> {
484
338
  const activeRun = await planRunService.getRunById(params.input.runId)
485
339
  if (
@@ -547,119 +401,47 @@ class ExecutionPlanService {
547
401
  const spec = PlanSpecSchema.parse(
548
402
  await tx
549
403
  .create(specId)
550
- .content({
551
- organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
552
- workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
553
- title: compiled.draft.title,
554
- objective: compiled.draft.objective,
555
- version: supersededSpec.version + 1,
556
- status: 'compiled',
557
- leadAgentId: params.leadAgentId,
558
- schemaRegistry: structuredClone(compiled.draft.schemas),
559
- defaultExecutionVisibility: compiled.draft.defaultExecutionVisibility,
560
- edges: [...compiled.draft.edges],
561
- entryNodeIds: [...(compiled.draft.entryNodeIds ?? [])],
562
- executionMode: compiled.draft.executionMode ?? 'linear',
563
- ...(compiled.draft.schedule ? { schedule: compiled.draft.schedule } : {}),
564
- ...(compiled.draft.dependencies ? { dependencies: compiled.draft.dependencies } : {}),
565
- replacedSpecId: ensureRecordId(supersededSpec.id, TABLES.PLAN_SPEC),
566
- compiledAt: new Date(),
567
- })
568
- .output('after'),
569
- )
570
-
571
- const nodeSpecs = await this.createNodeSpecs(tx, spec.id, compiled.nodes)
572
- const run = PlanRunSchema.parse(
573
- await tx
574
- .create(runId)
575
- .content({
576
- planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
577
- organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
578
- workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
579
- leadAgentId: params.leadAgentId,
580
- status: 'running',
581
- readyNodeIds: [],
582
- failureCount: 0,
583
- replacedRunId: ensureRecordId(abortedRun.id, TABLES.PLAN_RUN),
584
- startedAt: new Date(),
585
- })
404
+ .content(
405
+ buildCompiledSpecCreateData({
406
+ organizationId: params.organizationId,
407
+ workstreamId: params.workstreamId,
408
+ leadAgentId: params.leadAgentId,
409
+ compiled,
410
+ version: supersededSpec.version + 1,
411
+ replacedSpecId: supersededSpec.id,
412
+ }),
413
+ )
586
414
  .output('after'),
587
415
  )
588
-
589
- const nodeRuns = await this.createNodeRuns(tx, run.id, spec.id, nodeSpecs)
590
- const synced = await planExecutorService.syncRunGraph({
416
+ await this.createInitializedRunGraph({
591
417
  tx,
592
- run,
418
+ runId,
593
419
  spec,
594
- nodeSpecs,
595
- nodeRuns,
596
- artifacts: [],
597
- emittedBy: params.leadAgentId,
598
- capturedEvents: emittedEvents,
420
+ organizationId: params.organizationId,
421
+ workstreamId: params.workstreamId,
422
+ leadAgentId: params.leadAgentId,
423
+ nodes: compiled.nodes,
424
+ emittedEvents,
425
+ runPatch: { replacedRunId: abortedRun.id },
426
+ createdEventType: 'plan-replaced',
427
+ createdEventMessage: `Replaced execution plan "${activeSpec.title}" with "${spec.title}".`,
428
+ createdEventDetail: {
429
+ reason: params.input.reason,
430
+ replacedRunId: recordIdToString(abortedRun.id, TABLES.PLAN_RUN),
431
+ },
432
+ checkpointReason: 'plan-replaced',
599
433
  })
600
-
601
- const replaceEvent = await tx
602
- .create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
603
- .content({
604
- planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
605
- runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
606
- eventType: 'plan-replaced',
607
- message: `Replaced execution plan "${activeSpec.title}" with "${spec.title}".`,
608
- emittedBy: params.leadAgentId,
609
- detail: { reason: params.input.reason, replacedRunId: recordIdToString(abortedRun.id, TABLES.PLAN_RUN) },
610
- })
611
- .output('after')
612
- emittedEvents.push(PlanEventSchema.parse(replaceEvent))
613
-
614
- const checkpoint = PlanCheckpointSchema.parse(
615
- await tx
616
- .create(new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7()))
617
- .content({
618
- runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
619
- sequence: 1,
620
- runStatus: synced.run.status,
621
- readyNodeIds: [...synced.run.readyNodeIds],
622
- activeNodeIds: synced.run.currentNodeId ? [synced.run.currentNodeId] : [],
623
- artifactIds: [],
624
- lastCompletedNodeIds: synced.nodeRuns
625
- .filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
626
- .map((nodeRun) => nodeRun.nodeId),
627
- snapshot: {
628
- reason: 'plan-replaced',
629
- currentNodeId: synced.run.currentNodeId,
630
- waitingNodeId: synced.run.waitingNodeId,
631
- readyNodeIds: synced.run.readyNodeIds,
632
- },
633
- })
634
- .output('after'),
635
- )
636
-
637
- const updatedRun = PlanRunSchema.parse(
638
- await tx
639
- .update(ensureRecordId(synced.run.id, TABLES.PLAN_RUN))
640
- .content(toRunData(synced.run, { lastCheckpointId: checkpoint.id }))
641
- .output('after'),
642
- )
643
-
644
- const checkpointEvent = await tx
645
- .create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
646
- .content({
647
- planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
648
- runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
649
- eventType: 'checkpoint-saved',
650
- message: 'Saved checkpoint 1.',
651
- emittedBy: 'system',
652
- detail: { checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT), reason: 'plan-replaced' },
653
- })
654
- .output('after')
655
- emittedEvents.push(PlanEventSchema.parse(checkpointEvent))
656
434
  })
657
435
 
658
436
  await planEventDeliveryService.dispatchEvents(emittedEvents)
659
437
 
660
438
  const plan = await this.finalizePlanSnapshot({ runId, emittedBy: params.leadAgentId })
661
439
 
662
- return buildToolResult({ action: 'replaced', plan, message: `Replaced execution plan with "${plan.title}".` })
440
+ return buildExecutionPlanToolResult({
441
+ action: 'replaced',
442
+ plan,
443
+ message: `Replaced execution plan with "${plan.title}".`,
444
+ })
663
445
  }
664
446
 
665
447
  async submitNodeResult(params: {
@@ -667,23 +449,12 @@ class ExecutionPlanService {
667
449
  emittedBy: string
668
450
  input: SubmitExecutionNodeResultArgs
669
451
  }): Promise<ExecutionPlanToolResultData> {
670
- const result = await planExecutorService.submitNodeResult({
452
+ return await this.submitPlanTurnResult({
671
453
  workstreamId: params.workstreamId,
672
- runId: params.input.runId,
673
- nodeId: params.input.nodeId,
674
454
  emittedBy: params.emittedBy,
675
- result: params.input.result,
676
- })
677
- const plan = await ownershipDispatcherService.dispatchRunToStableBoundary({
678
455
  runId: params.input.runId,
679
- emittedBy: params.emittedBy,
680
- })
681
-
682
- return buildToolResult({
683
- action: result.action,
684
- plan,
685
- message: result.message ?? `Submitted result for node "${params.input.nodeId}".`,
686
- changedNodeId: result.changedNodeId ?? undefined,
456
+ nodeId: params.input.nodeId,
457
+ input: params.input.result,
687
458
  })
688
459
  }
689
460
 
@@ -706,7 +477,7 @@ class ExecutionPlanService {
706
477
  emittedBy: params.emittedBy,
707
478
  })
708
479
 
709
- return buildToolResult({
480
+ return buildExecutionPlanToolResult({
710
481
  action: result.action,
711
482
  plan,
712
483
  message: result.message ?? `Submitted result for node "${params.nodeId}".`,
@@ -729,7 +500,7 @@ class ExecutionPlanService {
729
500
  emittedBy: params.emittedBy,
730
501
  })
731
502
 
732
- return buildToolResult({
503
+ return buildExecutionPlanToolResult({
733
504
  action: result.action,
734
505
  plan,
735
506
  message: result.message ?? `Resumed execution run "${params.input.runId}".`,
@@ -745,18 +516,11 @@ class ExecutionPlanService {
745
516
  const approvalResponse = buildApprovalResponseFromMessages(params.approvalMessages)
746
517
  if (!approvalResponse) return null
747
518
 
748
- const run = await planRunService.getActiveRunRecord(params.workstreamId)
749
- if (!run) return null
750
-
751
- const plan = await planExecutorService.submitHumanNodeResponse({
519
+ return await this.respondToApproval({
752
520
  workstreamId: params.workstreamId,
753
- approvalId: approvalResponse.approvalId,
754
- respondedBy: params.respondedBy,
755
- response: approvalResponse.response,
521
+ emittedBy: params.respondedBy,
522
+ input: approvalResponse,
756
523
  })
757
- if (!plan) return null
758
-
759
- return ownershipDispatcherService.dispatchRunToStableBoundary({ runId: run.id, emittedBy: params.respondedBy })
760
524
  }
761
525
 
762
526
  async respondToApproval(params: {
@@ -912,6 +676,113 @@ class ExecutionPlanService {
912
676
 
913
677
  return createdNodeRuns
914
678
  }
679
+
680
+ private async createInitializedRunGraph(params: {
681
+ tx: DatabaseTransaction
682
+ runId: RecordIdInput
683
+ spec: PlanSpecRecord
684
+ organizationId: RecordIdInput
685
+ workstreamId: RecordIdInput
686
+ leadAgentId: string
687
+ nodes: CompiledPlanNode[]
688
+ emittedEvents: PlanEventRecord[]
689
+ createdEventType: 'plan-created' | 'plan-replaced'
690
+ createdEventMessage: string
691
+ createdEventDetail: Record<string, unknown>
692
+ checkpointReason: 'plan-created' | 'plan-replaced'
693
+ runPatch?: { replacedRunId?: RecordIdInput }
694
+ }): Promise<void> {
695
+ const nodeSpecs = await this.createNodeSpecs(params.tx, params.spec.id, params.nodes)
696
+ const run = PlanRunSchema.parse(
697
+ await params.tx
698
+ .create(ensureRecordId(params.runId, TABLES.PLAN_RUN))
699
+ .content({
700
+ planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
701
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
702
+ workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
703
+ leadAgentId: params.leadAgentId,
704
+ status: 'running',
705
+ readyNodeIds: [],
706
+ failureCount: 0,
707
+ ...(params.runPatch?.replacedRunId
708
+ ? { replacedRunId: ensureRecordId(params.runPatch.replacedRunId, TABLES.PLAN_RUN) }
709
+ : {}),
710
+ startedAt: new Date(),
711
+ })
712
+ .output('after'),
713
+ )
714
+
715
+ const nodeRuns = await this.createNodeRuns(params.tx, run.id, params.spec.id, nodeSpecs)
716
+ const synced = await planExecutorService.syncRunGraph({
717
+ tx: params.tx,
718
+ run,
719
+ spec: params.spec,
720
+ nodeSpecs,
721
+ nodeRuns,
722
+ artifacts: [],
723
+ emittedBy: params.leadAgentId,
724
+ capturedEvents: params.emittedEvents,
725
+ })
726
+
727
+ const event = await params.tx
728
+ .create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
729
+ .content({
730
+ planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
731
+ runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
732
+ eventType: params.createdEventType,
733
+ message: params.createdEventMessage,
734
+ emittedBy: params.leadAgentId,
735
+ detail: { ...params.createdEventDetail, nodeCount: nodeSpecs.length },
736
+ })
737
+ .output('after')
738
+ params.emittedEvents.push(PlanEventSchema.parse(event))
739
+
740
+ const checkpoint = PlanCheckpointSchema.parse(
741
+ await params.tx
742
+ .create(new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7()))
743
+ .content({
744
+ runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
745
+ sequence: 1,
746
+ runStatus: synced.run.status,
747
+ readyNodeIds: [...synced.run.readyNodeIds],
748
+ activeNodeIds: synced.run.currentNodeId ? [synced.run.currentNodeId] : [],
749
+ artifactIds: [],
750
+ lastCompletedNodeIds: synced.nodeRuns
751
+ .filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
752
+ .map((nodeRun) => nodeRun.nodeId),
753
+ snapshot: {
754
+ reason: params.checkpointReason,
755
+ currentNodeId: synced.run.currentNodeId,
756
+ waitingNodeId: synced.run.waitingNodeId,
757
+ readyNodeIds: synced.run.readyNodeIds,
758
+ },
759
+ })
760
+ .output('after'),
761
+ )
762
+
763
+ const updatedRun = PlanRunSchema.parse(
764
+ await params.tx
765
+ .update(ensureRecordId(synced.run.id, TABLES.PLAN_RUN))
766
+ .content(toRunData(synced.run, { lastCheckpointId: checkpoint.id }))
767
+ .output('after'),
768
+ )
769
+
770
+ const checkpointEvent = await params.tx
771
+ .create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
772
+ .content({
773
+ planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
774
+ runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
775
+ eventType: 'checkpoint-saved',
776
+ message: 'Saved checkpoint 1.',
777
+ emittedBy: 'system',
778
+ detail: {
779
+ checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT),
780
+ reason: params.checkpointReason,
781
+ },
782
+ })
783
+ .output('after')
784
+ params.emittedEvents.push(PlanEventSchema.parse(checkpointEvent))
785
+ }
915
786
  }
916
787
 
917
788
  export const executionPlanService = new ExecutionPlanService()