@lota-sdk/core 0.4.8 → 0.4.9

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 (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/thread-defaults.ts +33 -21
  9. package/src/create-runtime.ts +725 -387
  10. package/src/db/base.service.ts +52 -28
  11. package/src/db/cursor-pagination.ts +71 -30
  12. package/src/db/memory-store.helpers.ts +4 -7
  13. package/src/db/memory-store.ts +856 -598
  14. package/src/db/memory.ts +398 -275
  15. package/src/db/record-id.ts +32 -10
  16. package/src/db/schema-fingerprint.ts +30 -12
  17. package/src/db/service-normalization.ts +255 -0
  18. package/src/db/service.ts +726 -761
  19. package/src/db/startup.ts +140 -66
  20. package/src/db/transaction-conflict.ts +15 -0
  21. package/src/effect/awaitable-effect.ts +87 -0
  22. package/src/effect/errors.ts +121 -0
  23. package/src/effect/helpers.ts +98 -0
  24. package/src/effect/index.ts +22 -0
  25. package/src/effect/layers.ts +228 -0
  26. package/src/effect/runtime-ref.ts +25 -0
  27. package/src/effect/runtime.ts +31 -0
  28. package/src/effect/services.ts +57 -0
  29. package/src/effect/zod.ts +43 -0
  30. package/src/embeddings/provider.ts +122 -76
  31. package/src/index.ts +46 -1
  32. package/src/openrouter/direct-provider.ts +11 -35
  33. package/src/queues/autonomous-job.queue.ts +130 -74
  34. package/src/queues/context-compaction.queue.ts +60 -15
  35. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  36. package/src/queues/document-processor.queue.ts +52 -77
  37. package/src/queues/memory-consolidation.queue.ts +47 -32
  38. package/src/queues/organization-learning.queue.ts +13 -4
  39. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  40. package/src/queues/plan-scheduler.queue.ts +107 -31
  41. package/src/queues/post-chat-memory.queue.ts +66 -24
  42. package/src/queues/queue-factory.ts +142 -52
  43. package/src/queues/standalone-worker.ts +39 -0
  44. package/src/queues/title-generation.queue.ts +54 -9
  45. package/src/redis/connection.ts +84 -32
  46. package/src/redis/index.ts +6 -8
  47. package/src/redis/org-memory-lock.ts +60 -27
  48. package/src/redis/redis-lease-lock.ts +200 -121
  49. package/src/redis/runtime-connection.ts +10 -0
  50. package/src/redis/stream-context.ts +84 -46
  51. package/src/runtime/agent-identity-overrides.ts +2 -2
  52. package/src/runtime/agent-runtime-policy.ts +4 -1
  53. package/src/runtime/agent-stream-helpers.ts +20 -9
  54. package/src/runtime/chat-run-orchestration.ts +102 -19
  55. package/src/runtime/chat-run-registry.ts +36 -2
  56. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  57. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  58. package/src/runtime/execution-plan-visibility.ts +2 -2
  59. package/src/runtime/execution-plan.ts +42 -15
  60. package/src/runtime/graph-designer.ts +11 -7
  61. package/src/runtime/helper-model.ts +135 -48
  62. package/src/runtime/index.ts +7 -7
  63. package/src/runtime/indexed-repositories-policy.ts +3 -3
  64. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  65. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  66. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  67. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  68. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  69. package/src/runtime/plugin-resolution.ts +144 -24
  70. package/src/runtime/plugin-types.ts +9 -1
  71. package/src/runtime/post-turn-side-effects.ts +197 -130
  72. package/src/runtime/retrieval-adapters.ts +38 -4
  73. package/src/runtime/runtime-config.ts +150 -61
  74. package/src/runtime/runtime-extensions.ts +21 -34
  75. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  76. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  77. package/src/runtime/social-chat/social-chat.ts +594 -0
  78. package/src/runtime/specialist-runner.ts +36 -10
  79. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  80. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  81. package/src/runtime/thread-chat-helpers.ts +2 -2
  82. package/src/runtime/thread-plan-turn.ts +2 -1
  83. package/src/runtime/thread-turn-context.ts +172 -94
  84. package/src/runtime/turn-lifecycle.ts +93 -27
  85. package/src/services/agent-activity.service.ts +287 -203
  86. package/src/services/agent-executor.service.ts +329 -217
  87. package/src/services/artifact.service.ts +225 -148
  88. package/src/services/attachment.service.ts +137 -115
  89. package/src/services/autonomous-job.service.ts +888 -491
  90. package/src/services/chat-run-registry.service.ts +11 -1
  91. package/src/services/context-compaction.service.ts +136 -86
  92. package/src/services/document-chunk.service.ts +162 -90
  93. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  94. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  95. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  96. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  97. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  98. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  99. package/src/services/feedback-loop.service.ts +132 -76
  100. package/src/services/global-orchestrator.service.ts +80 -170
  101. package/src/services/graph-full-routing.ts +182 -0
  102. package/src/services/index.ts +18 -21
  103. package/src/services/institutional-memory.service.ts +220 -123
  104. package/src/services/learned-skill.service.ts +364 -259
  105. package/src/services/memory/memory-conversation.ts +95 -0
  106. package/src/services/memory/memory-org-memory.ts +39 -0
  107. package/src/services/memory/memory-preseeded.ts +80 -0
  108. package/src/services/memory/memory-rerank.ts +297 -0
  109. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  110. package/src/services/memory/memory.service.ts +692 -0
  111. package/src/services/memory/rerank.service.ts +209 -0
  112. package/src/services/monitoring-window.service.ts +92 -70
  113. package/src/services/mutating-approval.service.ts +62 -53
  114. package/src/services/node-workspace.service.ts +141 -98
  115. package/src/services/notification.service.ts +17 -16
  116. package/src/services/organization-member.service.ts +120 -66
  117. package/src/services/organization.service.ts +144 -51
  118. package/src/services/ownership-dispatcher.service.ts +415 -264
  119. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  120. package/src/services/plan/plan-agent-query.service.ts +322 -0
  121. package/src/services/plan/plan-approval.service.ts +102 -0
  122. package/src/services/plan/plan-artifact.service.ts +60 -0
  123. package/src/services/plan/plan-builder.service.ts +76 -0
  124. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  125. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  126. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  127. package/src/services/plan/plan-coordination.service.ts +181 -0
  128. package/src/services/plan/plan-cycle.service.ts +398 -0
  129. package/src/services/plan/plan-deadline.service.ts +547 -0
  130. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  131. package/src/services/plan/plan-executor-context.ts +35 -0
  132. package/src/services/plan/plan-executor-graph.ts +475 -0
  133. package/src/services/plan/plan-executor-helpers.ts +322 -0
  134. package/src/services/plan/plan-executor-persistence.ts +209 -0
  135. package/src/services/plan/plan-executor.service.ts +1654 -0
  136. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  137. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  138. package/src/services/plan/plan-run-serialization.ts +15 -0
  139. package/src/services/plan/plan-run.service.ts +644 -0
  140. package/src/services/plan/plan-scheduler.service.ts +385 -0
  141. package/src/services/plan/plan-template.service.ts +224 -0
  142. package/src/services/plan/plan-transaction-events.ts +33 -0
  143. package/src/services/plan/plan-validator.service.ts +907 -0
  144. package/src/services/plan/plan-workspace.service.ts +125 -0
  145. package/src/services/plugin-executor.service.ts +97 -68
  146. package/src/services/quality-metrics.service.ts +112 -94
  147. package/src/services/queue-job.service.ts +296 -230
  148. package/src/services/recent-activity-title.service.ts +65 -36
  149. package/src/services/recent-activity.service.ts +274 -259
  150. package/src/services/skill-resolver.service.ts +38 -12
  151. package/src/services/social-chat-history.service.ts +176 -125
  152. package/src/services/system-executor.service.ts +91 -61
  153. package/src/services/thread/thread-active-run.ts +203 -0
  154. package/src/services/thread/thread-bootstrap.ts +369 -0
  155. package/src/services/thread/thread-listing.ts +198 -0
  156. package/src/services/thread/thread-memory-block.ts +117 -0
  157. package/src/services/thread/thread-message.service.ts +363 -0
  158. package/src/services/thread/thread-record-store.ts +155 -0
  159. package/src/services/thread/thread-title.service.ts +74 -0
  160. package/src/services/thread/thread-turn-execution.ts +280 -0
  161. package/src/services/thread/thread-turn-message-context.ts +73 -0
  162. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  163. package/src/services/thread/thread-turn-streaming.ts +402 -0
  164. package/src/services/thread/thread-turn-tracing.ts +35 -0
  165. package/src/services/thread/thread-turn.ts +343 -0
  166. package/src/services/thread/thread.service.ts +335 -0
  167. package/src/services/user.service.ts +82 -32
  168. package/src/services/write-intent-validator.service.ts +63 -51
  169. package/src/storage/attachment-parser.ts +69 -27
  170. package/src/storage/attachment-storage.service.ts +331 -275
  171. package/src/storage/generated-document-storage.service.ts +66 -34
  172. package/src/system-agents/agent-result.ts +3 -1
  173. package/src/system-agents/context-compaction.agent.ts +2 -2
  174. package/src/system-agents/delegated-agent-factory.ts +159 -90
  175. package/src/system-agents/memory-reranker.agent.ts +2 -2
  176. package/src/system-agents/memory.agent.ts +2 -2
  177. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  178. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  179. package/src/system-agents/skill-extractor.agent.ts +2 -2
  180. package/src/system-agents/skill-manager.agent.ts +2 -2
  181. package/src/system-agents/thread-router.agent.ts +157 -113
  182. package/src/system-agents/title-generator.agent.ts +2 -2
  183. package/src/tools/execution-plan.tool.ts +220 -161
  184. package/src/tools/fetch-webpage.tool.ts +21 -17
  185. package/src/tools/firecrawl-client.ts +16 -6
  186. package/src/tools/index.ts +1 -0
  187. package/src/tools/memory-block.tool.ts +14 -6
  188. package/src/tools/plan-approval.tool.ts +49 -47
  189. package/src/tools/read-file-parts.tool.ts +44 -33
  190. package/src/tools/remember-memory.tool.ts +65 -45
  191. package/src/tools/search-web.tool.ts +26 -22
  192. package/src/tools/search.tool.ts +41 -29
  193. package/src/tools/team-think.tool.ts +124 -83
  194. package/src/tools/user-questions.tool.ts +4 -3
  195. package/src/tools/web-tool-shared.ts +6 -0
  196. package/src/utils/async.ts +17 -23
  197. package/src/utils/crypto.ts +21 -0
  198. package/src/utils/date-time.ts +40 -1
  199. package/src/utils/errors.ts +95 -16
  200. package/src/utils/hono-error-handler.ts +24 -39
  201. package/src/utils/index.ts +2 -1
  202. package/src/utils/null-proto-record.ts +41 -0
  203. package/src/utils/sse-keepalive.ts +124 -21
  204. package/src/workers/bootstrap.ts +186 -51
  205. package/src/workers/memory-consolidation.worker.ts +325 -237
  206. package/src/workers/organization-learning.worker.ts +50 -16
  207. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  208. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  209. package/src/workers/skill-extraction.runner.ts +176 -93
  210. package/src/workers/utils/file-section-chunker.ts +8 -10
  211. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  212. package/src/workers/utils/repomix-file-sections.ts +2 -2
  213. package/src/workers/utils/thread-message-query.ts +97 -38
  214. package/src/workers/worker-utils.ts +56 -31
  215. package/src/config/debug-logger.ts +0 -47
  216. package/src/redis/connection-accessor.ts +0 -26
  217. package/src/runtime/context-compaction-runtime.ts +0 -87
  218. package/src/runtime/social-chat-agent-runner.ts +0 -118
  219. package/src/runtime/social-chat.ts +0 -516
  220. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  221. package/src/services/adaptive-playbook.service.ts +0 -152
  222. package/src/services/artifact-provenance.service.ts +0 -172
  223. package/src/services/chat-attachments.service.ts +0 -17
  224. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  225. package/src/services/execution-plan.service.ts +0 -1118
  226. package/src/services/memory.service.ts +0 -914
  227. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  228. package/src/services/plan-agent-query.service.ts +0 -267
  229. package/src/services/plan-approval.service.ts +0 -83
  230. package/src/services/plan-artifact.service.ts +0 -50
  231. package/src/services/plan-builder.service.ts +0 -67
  232. package/src/services/plan-checkpoint.service.ts +0 -81
  233. package/src/services/plan-completion-side-effects.ts +0 -80
  234. package/src/services/plan-coordination.service.ts +0 -157
  235. package/src/services/plan-cycle.service.ts +0 -284
  236. package/src/services/plan-deadline.service.ts +0 -430
  237. package/src/services/plan-event-delivery.service.ts +0 -166
  238. package/src/services/plan-executor.service.ts +0 -1950
  239. package/src/services/plan-run.service.ts +0 -515
  240. package/src/services/plan-scheduler.service.ts +0 -240
  241. package/src/services/plan-template.service.ts +0 -177
  242. package/src/services/plan-validator.service.ts +0 -818
  243. package/src/services/plan-workspace.service.ts +0 -83
  244. package/src/services/rerank.service.ts +0 -156
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,60 @@
1
+ import { PlanArtifactSchema } from '@lota-sdk/shared'
2
+ import type { PlanArtifactRecord, PlanArtifactSubmission } from '@lota-sdk/shared'
3
+ import { Context, Effect, Layer } from 'effect'
4
+ import { RecordId } from 'surrealdb'
5
+
6
+ import type { RecordIdInput } from '../../db/record-id'
7
+ import { ensureRecordId } from '../../db/record-id'
8
+ import type { DatabaseTransaction } from '../../db/service'
9
+ import { TABLES } from '../../db/tables'
10
+
11
+ export function makePlanArtifactService() {
12
+ return {
13
+ persistArtifacts(params: {
14
+ tx: DatabaseTransaction
15
+ runId: RecordIdInput
16
+ attemptId: RecordIdInput
17
+ nodeId: string
18
+ artifacts: PlanArtifactSubmission[]
19
+ }) {
20
+ return Effect.gen(function* () {
21
+ const records: PlanArtifactRecord[] = []
22
+
23
+ // Sequential: SurrealDB transactions require ordered operations
24
+ for (const artifact of params.artifacts) {
25
+ const artifactId = new RecordId(TABLES.PLAN_ARTIFACT, Bun.randomUUIDv7())
26
+ const created = yield* params.tx
27
+ .create(artifactId)
28
+ .content({
29
+ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
30
+ attemptId: ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT),
31
+ nodeId: params.nodeId,
32
+ name: artifact.name,
33
+ kind: artifact.kind,
34
+ pointer: artifact.publishedArtifactId
35
+ ? `venturos://artifact/${artifact.publishedArtifactId}`
36
+ : `artifact://${params.nodeId}/${artifact.name}`,
37
+ ...(artifact.description ? { description: artifact.description } : {}),
38
+ ...(artifact.content ? { content: artifact.content } : {}),
39
+ ...(artifact.payload ? { payload: artifact.payload } : {}),
40
+ ...(artifact.publishedArtifactId
41
+ ? { publishedArtifactId: ensureRecordId(artifact.publishedArtifactId, TABLES.ARTIFACT) }
42
+ : {}),
43
+ })
44
+ .output('after')
45
+
46
+ records.push(PlanArtifactSchema.parse(created))
47
+ }
48
+
49
+ return records
50
+ })
51
+ },
52
+ }
53
+ }
54
+
55
+ export class PlanArtifactServiceTag extends Context.Service<
56
+ PlanArtifactServiceTag,
57
+ ReturnType<typeof makePlanArtifactService>
58
+ >()('PlanArtifactService') {}
59
+
60
+ export const PlanArtifactServiceLive = Layer.succeed(PlanArtifactServiceTag, makePlanArtifactService())
@@ -0,0 +1,76 @@
1
+ import type { PlanDraft } from '@lota-sdk/shared'
2
+ import { Context, Layer } from 'effect'
3
+
4
+ import { isExecutableConditionExpression } from './plan-helpers'
5
+
6
+ function buildImplicitLinearEdges(draft: PlanDraft) {
7
+ if (draft.edges.length > 0 || draft.nodes.length <= 1) {
8
+ return draft.edges
9
+ }
10
+
11
+ return draft.nodes
12
+ .slice(0, -1)
13
+ .map((node, index) => ({
14
+ id: `edge_${node.id}_to_${draft.nodes[index + 1]?.id ?? index + 1}`,
15
+ source: node.id,
16
+ target: draft.nodes[index + 1].id,
17
+ map: {},
18
+ }))
19
+ }
20
+
21
+ function structureDesign(draft: PlanDraft): PlanDraft {
22
+ return {
23
+ ...draft,
24
+ executionMode: draft.executionMode ?? 'linear',
25
+ edges: buildImplicitLinearEdges(draft),
26
+ entryNodeIds: draft.entryNodeIds && draft.entryNodeIds.length > 0 ? draft.entryNodeIds : [draft.nodes[0].id],
27
+ }
28
+ }
29
+
30
+ function semanticCompletion(draft: PlanDraft): PlanDraft {
31
+ return {
32
+ ...draft,
33
+ nodes: draft.nodes.map((node) => ({
34
+ ...node,
35
+ deliverables: [...node.deliverables],
36
+ successCriteria: [...node.successCriteria],
37
+ completionChecks: [...node.completionChecks],
38
+ failurePolicy: [...node.failurePolicy],
39
+ retryPolicy: { ...node.retryPolicy, retryOn: [...node.retryPolicy.retryOn] },
40
+ toolPolicy: { allow: [...node.toolPolicy.allow], deny: [...node.toolPolicy.deny] },
41
+ contextPolicy: {
42
+ retrievalScopes: [...node.contextPolicy.retrievalScopes],
43
+ attachmentPolicy: node.contextPolicy.attachmentPolicy,
44
+ webPolicy: node.contextPolicy.webPolicy,
45
+ },
46
+ })),
47
+ edges: draft.edges.map((edge) => {
48
+ const normalizedWhen = edge.when?.trim()
49
+ const { when: _when, ...edgeWithoutWhen } = edge
50
+ return {
51
+ ...edgeWithoutWhen,
52
+ ...(normalizedWhen && isExecutableConditionExpression(normalizedWhen) ? { when: normalizedWhen } : {}),
53
+ map: { ...edge.map },
54
+ }
55
+ }),
56
+ schemas: structuredClone(draft.schemas),
57
+ entryNodeIds: [...(draft.entryNodeIds ?? [])],
58
+ }
59
+ }
60
+
61
+ export function makePlanBuilderService() {
62
+ return {
63
+ structureDesign,
64
+ semanticCompletion,
65
+ prepareDraft(draft: PlanDraft): PlanDraft {
66
+ return semanticCompletion(structureDesign(draft))
67
+ },
68
+ }
69
+ }
70
+
71
+ export class PlanBuilderServiceTag extends Context.Service<
72
+ PlanBuilderServiceTag,
73
+ ReturnType<typeof makePlanBuilderService>
74
+ >()('PlanBuilderService') {}
75
+
76
+ export const PlanBuilderServiceLive = Layer.succeed(PlanBuilderServiceTag, makePlanBuilderService())
@@ -0,0 +1,103 @@
1
+ import { PlanCheckpointSchema } from '@lota-sdk/shared'
2
+ import type { PlanRunStatus } from '@lota-sdk/shared'
3
+ import { Context, Effect, Layer } from 'effect'
4
+ import { RecordId } from 'surrealdb'
5
+
6
+ import { serverLogger } from '../../config/logger'
7
+ import type { RecordIdInput } from '../../db/record-id'
8
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
9
+ import type { SurrealDBService, DatabaseTransaction } from '../../db/service'
10
+ import { TABLES } from '../../db/tables'
11
+ import { DatabaseServiceTag } from '../../effect/services'
12
+ import type { makePlanWorkspaceService } from './plan-workspace.service'
13
+ import { PlanWorkspaceServiceTag } from './plan-workspace.service'
14
+
15
+ interface PlanCheckpointDeps {
16
+ db: SurrealDBService
17
+ planWorkspaceService: ReturnType<typeof makePlanWorkspaceService>
18
+ }
19
+
20
+ export function makePlanCheckpointService(deps: PlanCheckpointDeps) {
21
+ const { db, planWorkspaceService } = deps
22
+
23
+ return {
24
+ createCheckpoint: (params: {
25
+ tx: DatabaseTransaction
26
+ runId: RecordIdInput
27
+ sequence: number
28
+ runStatus: PlanRunStatus
29
+ readyNodeIds: string[]
30
+ activeNodeIds: string[]
31
+ artifactIds: RecordIdInput[]
32
+ lastCompletedNodeIds: string[]
33
+ snapshot: Record<string, unknown>
34
+ includeWorkspace?: boolean
35
+ }) =>
36
+ Effect.gen(function* () {
37
+ const snapshotData = { ...params.snapshot }
38
+
39
+ if (params.includeWorkspace) {
40
+ const runIdStr = recordIdToString(params.runId, TABLES.PLAN_RUN)
41
+ const workspaceSnapshotEffect = planWorkspaceService.currentRead({ runId: runIdStr }).pipe(
42
+ Effect.map((all: Record<string, { writeSequence: number }>) => {
43
+ const filtered: Record<string, unknown> = {}
44
+ for (const [key, entry] of Object.entries(all)) {
45
+ if (entry.writeSequence <= params.sequence) {
46
+ filtered[key] = entry
47
+ }
48
+ }
49
+ return filtered
50
+ }),
51
+ Effect.orElseSucceed(() => {
52
+ serverLogger.warn`Workspace snapshot failed for run ${runIdStr}`
53
+ return {} as Record<string, unknown>
54
+ }),
55
+ )
56
+ const workspaceSnapshot: Record<string, unknown> = yield* workspaceSnapshotEffect
57
+ if (Object.keys(workspaceSnapshot).length > 0) {
58
+ snapshotData.workspaceSnapshot = workspaceSnapshot
59
+ }
60
+ }
61
+
62
+ const checkpointId = new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7())
63
+ const created = yield* params.tx
64
+ .create(checkpointId)
65
+ .content({
66
+ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
67
+ sequence: params.sequence,
68
+ runStatus: params.runStatus,
69
+ readyNodeIds: [...params.readyNodeIds],
70
+ activeNodeIds: [...params.activeNodeIds],
71
+ artifactIds: params.artifactIds.map((artifactId) => ensureRecordId(artifactId, TABLES.PLAN_ARTIFACT)),
72
+ lastCompletedNodeIds: [...params.lastCompletedNodeIds],
73
+ snapshot: snapshotData,
74
+ })
75
+ .output('after')
76
+
77
+ return PlanCheckpointSchema.parse(created)
78
+ }),
79
+
80
+ loadLatestForRun: (runId: RecordIdInput) =>
81
+ db
82
+ .findMany(TABLES.PLAN_CHECKPOINT, { runId: ensureRecordId(runId, TABLES.PLAN_RUN) }, PlanCheckpointSchema, {
83
+ orderBy: 'sequence',
84
+ orderDir: 'DESC',
85
+ limit: 1,
86
+ })
87
+ .pipe(Effect.map((checkpoints) => checkpoints.at(0) ?? null)),
88
+ }
89
+ }
90
+
91
+ export class PlanCheckpointServiceTag extends Context.Service<
92
+ PlanCheckpointServiceTag,
93
+ ReturnType<typeof makePlanCheckpointService>
94
+ >()('PlanCheckpointService') {}
95
+
96
+ export const PlanCheckpointServiceLive = Layer.effect(
97
+ PlanCheckpointServiceTag,
98
+ Effect.gen(function* () {
99
+ const db = yield* DatabaseServiceTag
100
+ const planWorkspaceService = yield* PlanWorkspaceServiceTag
101
+ return makePlanCheckpointService({ db, planWorkspaceService })
102
+ }),
103
+ )
@@ -1,6 +1,9 @@
1
1
  import type { PlanDraft, PlanNodeSpec, PlanNodeSpecRecord } from '@lota-sdk/shared'
2
+ import { Context, Effect, Layer } from 'effect'
2
3
 
3
- import { planValidatorService } from './plan-validator.service'
4
+ import { ValidationError } from '../../effect/errors'
5
+ import type { makePlanValidatorService } from './plan-validator.service'
6
+ import { PlanValidatorServiceTag } from './plan-validator.service'
4
7
 
5
8
  export interface CompiledPlanNode {
6
9
  node: PlanNodeSpec
@@ -14,12 +17,14 @@ interface CompiledPlanDraft {
14
17
  nodes: CompiledPlanNode[]
15
18
  }
16
19
 
17
- class PlanCompilerService {
18
- compile(draft: PlanDraft): CompiledPlanDraft {
20
+ export function makePlanCompilerService(planValidatorService: ReturnType<typeof makePlanValidatorService>) {
21
+ function compile(draft: PlanDraft): Effect.Effect<CompiledPlanDraft, ValidationError> {
19
22
  const validation = planValidatorService.validateDraft(draft)
20
23
  if (validation.blocking.length > 0) {
21
- throw new Error(
22
- `Plan draft failed validation: ${validation.blocking.map((issue) => `${issue.code}: ${issue.message}`).join(' | ')}`,
24
+ return Effect.fail(
25
+ new ValidationError({
26
+ message: `Plan draft failed validation: ${validation.blocking.map((issue) => `${issue.code}: ${issue.message}`).join(' | ')}`,
27
+ }),
23
28
  )
24
29
  }
25
30
 
@@ -36,7 +41,7 @@ class PlanCompilerService {
36
41
  downstreamByNodeId.get(edge.source)?.push(edge.target)
37
42
  }
38
43
 
39
- return {
44
+ return Effect.succeed({
40
45
  draft,
41
46
  nodes: draft.nodes.map((node, index) => ({
42
47
  node,
@@ -44,10 +49,10 @@ class PlanCompilerService {
44
49
  upstreamNodeIds: [...(upstreamByNodeId.get(node.id) ?? [])],
45
50
  downstreamNodeIds: [...(downstreamByNodeId.get(node.id) ?? [])],
46
51
  })),
47
- }
52
+ })
48
53
  }
49
54
 
50
- toNodeSpecRecords(
55
+ function toNodeSpecRecords(
51
56
  compiled: CompiledPlanDraft,
52
57
  ): Array<Omit<PlanNodeSpecRecord, 'id' | 'planSpecId' | 'createdAt' | 'updatedAt'>> {
53
58
  return compiled.nodes.map(({ node, position, upstreamNodeIds, downstreamNodeIds }) => ({
@@ -83,6 +88,18 @@ class PlanCompilerService {
83
88
  downstreamNodeIds,
84
89
  }))
85
90
  }
91
+
92
+ return { compile, toNodeSpecRecords }
86
93
  }
87
94
 
88
- export const planCompilerService = new PlanCompilerService()
95
+ export class PlanCompilerServiceTag extends Context.Service<
96
+ PlanCompilerServiceTag,
97
+ ReturnType<typeof makePlanCompilerService>
98
+ >()('PlanCompilerService') {}
99
+
100
+ export const PlanCompilerServiceLive = Layer.effect(
101
+ PlanCompilerServiceTag,
102
+ Effect.map(PlanValidatorServiceTag.asEffect(), (planValidatorService) =>
103
+ makePlanCompilerService(planValidatorService),
104
+ ),
105
+ )
@@ -0,0 +1,175 @@
1
+ import type { PlanNodeOwner, PlanNodeType } from '@lota-sdk/shared'
2
+ import { PlanEventSchema } from '@lota-sdk/shared'
3
+ import { Schema, Effect, Metric } from 'effect'
4
+
5
+ import { aiLogger } from '../../config/logger'
6
+ import { ensureRecordId } from '../../db/record-id'
7
+ import type { SurrealDBService } from '../../db/service'
8
+ import { TABLES } from '../../db/tables'
9
+ import { runPromise } from '../../effect/runtime'
10
+ import { nowEpochMillis, unsafeDateFrom } from '../../utils/date-time'
11
+ import type { makeFeedbackLoopService } from '../feedback-loop.service'
12
+ import type { makeInstitutionalMemoryService } from '../institutional-memory.service'
13
+ import type { makeQualityMetricsService } from '../quality-metrics.service'
14
+ import type { makePlanEventDeliveryService } from './plan-event-delivery.service'
15
+ import type { makePlanRunService } from './plan-run.service'
16
+ import type { PlanValidationIssueInput } from './plan-validator.service'
17
+
18
+ const planNodeExecutionDuration = Metric.histogram('plan_node_execution_duration_ms', {
19
+ boundaries: Metric.boundariesFromIterable([100, 500, 1000, 5000, 10_000, 30_000, 60_000]),
20
+ })
21
+
22
+ interface PlanCompletionSideEffectsDeps {
23
+ databaseService: SurrealDBService
24
+ feedbackLoopService: ReturnType<typeof makeFeedbackLoopService>
25
+ institutionalMemoryService: ReturnType<typeof makeInstitutionalMemoryService>
26
+ planEventDeliveryService: ReturnType<typeof makePlanEventDeliveryService>
27
+ planRunService: ReturnType<typeof makePlanRunService>
28
+ qualityMetricsService: ReturnType<typeof makeQualityMetricsService>
29
+ }
30
+
31
+ class PlanCompletionSideEffectsError extends Schema.TaggedErrorClass<PlanCompletionSideEffectsError>()(
32
+ 'PlanCompletionSideEffectsError',
33
+ { message: Schema.String, cause: Schema.Defect },
34
+ ) {}
35
+
36
+ function tryPlanCompletionPromise<A>(
37
+ message: string,
38
+ thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
39
+ ): Effect.Effect<A, PlanCompletionSideEffectsError> {
40
+ return Effect.suspend(() => {
41
+ try {
42
+ const value = thunk()
43
+ if (Effect.isEffect(value)) {
44
+ return value.pipe(Effect.mapError((cause) => new PlanCompletionSideEffectsError({ message, cause })))
45
+ }
46
+
47
+ return Effect.tryPromise({
48
+ try: () => Promise.resolve(value),
49
+ catch: (cause) => new PlanCompletionSideEffectsError({ message, cause }),
50
+ })
51
+ } catch (cause) {
52
+ return Effect.fail(new PlanCompletionSideEffectsError({ message, cause }))
53
+ }
54
+ })
55
+ }
56
+
57
+ export function makePlanCompletionSideEffects({
58
+ databaseService,
59
+ feedbackLoopService,
60
+ institutionalMemoryService,
61
+ planEventDeliveryService,
62
+ planRunService,
63
+ qualityMetricsService,
64
+ }: PlanCompletionSideEffectsDeps) {
65
+ function runPlanNodeCompletionSideEffects(params: {
66
+ runId: string
67
+ organizationId: string
68
+ nodeId: string
69
+ nodeLabel: string
70
+ nodeOwnerRef: string
71
+ nodeOwnerType: PlanNodeOwner['executorType']
72
+ nodeType: PlanNodeType
73
+ nodeStartedAt?: string | Date | null
74
+ nodeAttemptCount: number
75
+ artifactCount: number
76
+ validationIssues: PlanValidationIssueInput[]
77
+ }): Promise<void> {
78
+ return runPromise(
79
+ Effect.gen(function* () {
80
+ const executionTimeMs = params.nodeStartedAt
81
+ ? nowEpochMillis() - unsafeDateFrom(params.nodeStartedAt).getTime()
82
+ : 0
83
+ if (executionTimeMs > 0) {
84
+ yield* Metric.update(
85
+ Metric.withAttributes(planNodeExecutionDuration, { nodeType: params.nodeType }),
86
+ executionTimeMs,
87
+ )
88
+ }
89
+ yield* qualityMetricsService.recordNodeMetrics({
90
+ organizationId: params.organizationId,
91
+ runId: params.runId,
92
+ nodeId: params.nodeId,
93
+ metrics: {
94
+ executionTimeMs: Math.max(0, executionTimeMs),
95
+ attemptCount: params.nodeAttemptCount,
96
+ artifactCount: params.artifactCount,
97
+ validationIssueCount: params.validationIssues.length,
98
+ ownerRef: params.nodeOwnerRef,
99
+ ownerType: params.nodeOwnerType,
100
+ nodeType: params.nodeType,
101
+ },
102
+ })
103
+ }),
104
+ )
105
+ }
106
+
107
+ function runPlanCompletionSideEffects(params: { runId: string; organizationId: string }): Promise<void> {
108
+ return runPromise(
109
+ Effect.gen(function* () {
110
+ yield* qualityMetricsService.recordCycleMetrics({ organizationId: params.organizationId, runId: params.runId })
111
+
112
+ const recommendations = yield* feedbackLoopService
113
+ .analyzeOutcomes({ runId: params.runId, organizationId: params.organizationId })
114
+ .pipe(
115
+ Effect.mapError(
116
+ (cause) =>
117
+ new PlanCompletionSideEffectsError({ message: 'Failed to analyze plan feedback outcomes.', cause }),
118
+ ),
119
+ )
120
+ if (recommendations.length > 0) {
121
+ const run = yield* planRunService.getRunById(params.runId)
122
+ const specRecord = yield* planRunService.getPlanSpecById(run.planSpecId)
123
+ const event = yield* tryPlanCompletionPromise('Failed to create feedback analyzed plan event.', () =>
124
+ databaseService.create(
125
+ TABLES.PLAN_EVENT,
126
+ {
127
+ planSpecId: ensureRecordId(specRecord.id, TABLES.PLAN_SPEC),
128
+ runId: ensureRecordId(run.id, TABLES.PLAN_RUN),
129
+ eventType: 'feedback-analyzed',
130
+ message: `Feedback analysis produced ${recommendations.length} recommendation(s).`,
131
+ detail: { recommendations },
132
+ emittedBy: 'system',
133
+ },
134
+ PlanEventSchema,
135
+ ),
136
+ )
137
+ yield* Effect.tryPromise({
138
+ try: () => planEventDeliveryService.dispatchEvent(event),
139
+ catch: (cause) =>
140
+ new PlanCompletionSideEffectsError({
141
+ message: 'Failed to dispatch feedback analyzed plan event.',
142
+ cause,
143
+ }),
144
+ })
145
+ }
146
+
147
+ yield* institutionalMemoryService
148
+ .extractPatterns({ organizationId: params.organizationId, runId: params.runId })
149
+ .pipe(
150
+ Effect.mapError(
151
+ (cause) =>
152
+ new PlanCompletionSideEffectsError({
153
+ message: 'Failed to extract institutional memory patterns.',
154
+ cause,
155
+ }),
156
+ ),
157
+ )
158
+ }),
159
+ )
160
+ }
161
+
162
+ function runPlanCompletionSideEffectsSafely(params: { runId: string; organizationId: string }): Promise<void> {
163
+ return runPromise(
164
+ Effect.catch(
165
+ tryPlanCompletionPromise('Plan completion side effects failed.', () => runPlanCompletionSideEffects(params)),
166
+ (error: unknown) =>
167
+ Effect.sync(() => {
168
+ aiLogger.warn`Plan completion side effects failed for run ${params.runId}: ${error instanceof Error ? error.message : String(error)}`
169
+ }),
170
+ ),
171
+ )
172
+ }
173
+
174
+ return { runPlanNodeCompletionSideEffects, runPlanCompletionSideEffectsSafely }
175
+ }
@@ -0,0 +1,181 @@
1
+ import type { PlanArtifactRecord, PlanDependency } from '@lota-sdk/shared'
2
+ import { Context, Schema, Effect, Layer } from 'effect'
3
+
4
+ import { serverLogger } from '../../config/logger'
5
+ import { recordIdToString } from '../../db/record-id'
6
+ import { TABLES } from '../../db/tables'
7
+ import { nowEpochMillis } from '../../utils/date-time'
8
+ import type { makePlanRunService } from './plan-run.service'
9
+ import { PlanRunServiceTag } from './plan-run.service'
10
+ import type { PlanValidationIssueInput } from './plan-validator.service'
11
+
12
+ export interface DependencyResolutionResult {
13
+ resolved: Map<string, PlanArtifactRecord>
14
+ unresolved: PlanDependency[]
15
+ notifications: Array<{ dependency: PlanDependency; reason: string }>
16
+ }
17
+
18
+ class PlanCoordinationError extends Schema.TaggedErrorClass<PlanCoordinationError>()('PlanCoordinationError', {
19
+ message: Schema.String,
20
+ cause: Schema.optional(Schema.Defect),
21
+ }) {}
22
+
23
+ function isStale(artifact: Pick<PlanArtifactRecord, 'createdAt'>, maxStalenessMs: number): boolean {
24
+ if (!maxStalenessMs) return false
25
+ return nowEpochMillis() - artifact.createdAt.getTime() > maxStalenessMs
26
+ }
27
+
28
+ function validateNoCycles(
29
+ specs: Array<{ id: string; title?: string; dependencies?: PlanDependency[] }>,
30
+ ): PlanValidationIssueInput[] {
31
+ const adj = new Map<string, Set<string>>()
32
+ const inDegree = new Map<string, number>()
33
+ const labels = new Map(specs.map((spec) => [spec.id, spec.title ?? spec.id]))
34
+
35
+ for (const spec of specs) {
36
+ if (!adj.has(spec.id)) adj.set(spec.id, new Set())
37
+ if (!inDegree.has(spec.id)) inDegree.set(spec.id, 0)
38
+
39
+ for (const dep of spec.dependencies ?? []) {
40
+ if (!adj.has(dep.sourcePlanSpecId)) adj.set(dep.sourcePlanSpecId, new Set())
41
+ if (!inDegree.has(dep.sourcePlanSpecId)) inDegree.set(dep.sourcePlanSpecId, 0)
42
+
43
+ adj.get(dep.sourcePlanSpecId)?.add(spec.id)
44
+ inDegree.set(spec.id, (inDegree.get(spec.id) ?? 0) + 1)
45
+ }
46
+ }
47
+
48
+ const queue = [...inDegree.entries()].filter(([, d]) => d === 0).map(([t]) => t)
49
+ const visited = new Set<string>()
50
+
51
+ while (queue.length > 0) {
52
+ const node = queue.shift()
53
+ if (!node) break
54
+ visited.add(node)
55
+ for (const dep of adj.get(node) ?? []) {
56
+ const d = (inDegree.get(dep) ?? 0) - 1
57
+ inDegree.set(dep, d)
58
+ if (d === 0) queue.push(dep)
59
+ }
60
+ }
61
+
62
+ const unvisited = specs.filter((s) => !visited.has(s.id))
63
+ if (unvisited.length === 0) return []
64
+
65
+ return [
66
+ {
67
+ severity: 'blocking',
68
+ code: 'circular_dependency',
69
+ message: `Circular plan dependencies detected involving: ${unvisited.map((s) => labels.get(s.id) ?? s.id).join(', ')}`,
70
+ },
71
+ ]
72
+ }
73
+
74
+ export function makePlanCoordinationService(planRunService: ReturnType<typeof makePlanRunService>) {
75
+ const resolveDependencies = (params: { dependencies: PlanDependency[]; threadId: string }) => {
76
+ const resolved = new Map<string, PlanArtifactRecord>()
77
+ const unresolved: PlanDependency[] = []
78
+ const notifications: DependencyResolutionResult['notifications'] = []
79
+
80
+ return Effect.gen(function* () {
81
+ const specs = yield* planRunService
82
+ .listPlanSpecsByThread(params.threadId)
83
+ .pipe(
84
+ Effect.mapError(
85
+ (cause) => new PlanCoordinationError({ message: 'Failed to list plan specs by thread.', cause }),
86
+ ),
87
+ )
88
+
89
+ for (const dep of params.dependencies) {
90
+ const depKey = `${dep.sourcePlanSpecId}:${dep.sourceNodeId}:${dep.artifactName}`
91
+ const sourceSpec = specs.find((spec) => recordIdToString(spec.id, TABLES.PLAN_SPEC) === dep.sourcePlanSpecId)
92
+ if (!sourceSpec) {
93
+ const reason = `Source plan "${dep.sourcePlanSpecId}" not found in thread.`
94
+ if (dep.triggerMode === 'block') {
95
+ unresolved.push(dep)
96
+ } else if (dep.triggerMode === 'notify') {
97
+ notifications.push({ dependency: dep, reason })
98
+ serverLogger.warn`Dependency unmet (notify): ${reason}`
99
+ }
100
+ continue
101
+ }
102
+
103
+ const runs = yield* planRunService
104
+ .listRunsBySpec(sourceSpec.id)
105
+ .pipe(
106
+ Effect.mapError(
107
+ (cause) =>
108
+ new PlanCoordinationError({ message: 'Failed to list plan runs for dependency resolution.', cause }),
109
+ ),
110
+ )
111
+ const activeRun = runs.find((run) => run.status === 'completed' || run.status === 'running')
112
+ if (!activeRun) {
113
+ const reason = `No active run found for plan "${sourceSpec.title}".`
114
+ if (dep.triggerMode === 'block') {
115
+ unresolved.push(dep)
116
+ } else if (dep.triggerMode === 'notify') {
117
+ notifications.push({ dependency: dep, reason })
118
+ serverLogger.warn`Dependency unmet (notify): ${reason}`
119
+ }
120
+ continue
121
+ }
122
+
123
+ const artifacts = yield* planRunService
124
+ .listArtifacts(activeRun.id)
125
+ .pipe(
126
+ Effect.mapError(
127
+ (cause) =>
128
+ new PlanCoordinationError({
129
+ message: 'Failed to list plan artifacts for dependency resolution.',
130
+ cause,
131
+ }),
132
+ ),
133
+ )
134
+ const artifact = artifacts.find(
135
+ (candidate) => candidate.nodeId === dep.sourceNodeId && candidate.name === dep.artifactName,
136
+ )
137
+ if (!artifact) {
138
+ const reason = `Artifact "${dep.artifactName}" not found on node "${dep.sourceNodeId}" in plan "${sourceSpec.title}".`
139
+ if (dep.triggerMode === 'block') {
140
+ unresolved.push(dep)
141
+ } else if (dep.triggerMode === 'notify') {
142
+ notifications.push({ dependency: dep, reason })
143
+ serverLogger.warn`Dependency unmet (notify): ${reason}`
144
+ }
145
+ continue
146
+ }
147
+
148
+ if (dep.maxStalenessMs && isStale(artifact, dep.maxStalenessMs)) {
149
+ const reason = `Artifact "${dep.artifactName}" from plan "${sourceSpec.title}" is stale.`
150
+ if (dep.triggerMode === 'block') {
151
+ unresolved.push(dep)
152
+ continue
153
+ }
154
+ if (dep.triggerMode === 'notify') {
155
+ notifications.push({ dependency: dep, reason })
156
+ serverLogger.warn`Dependency stale (notify): ${reason}`
157
+ }
158
+ }
159
+
160
+ resolved.set(depKey, artifact)
161
+ }
162
+
163
+ return { resolved, unresolved, notifications }
164
+ })
165
+ }
166
+
167
+ return { resolveDependencies, isStale, validateNoCycles }
168
+ }
169
+
170
+ export class PlanCoordinationServiceTag extends Context.Service<
171
+ PlanCoordinationServiceTag,
172
+ ReturnType<typeof makePlanCoordinationService>
173
+ >()('PlanCoordinationService') {}
174
+
175
+ export const PlanCoordinationServiceLive = Layer.effect(
176
+ PlanCoordinationServiceTag,
177
+ Effect.gen(function* () {
178
+ const planRunService = yield* PlanRunServiceTag
179
+ return makePlanCoordinationService(planRunService)
180
+ }),
181
+ )