@lota-sdk/core 0.4.7 → 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/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  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
+ )