@lota-sdk/core 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +96 -22
  3. package/src/ai-gateway/ai-gateway.ts +766 -223
  4. package/src/config/agent-defaults.ts +189 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/constants.ts +8 -2
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +299 -19
  10. package/src/config/thread-defaults.ts +40 -20
  11. package/src/create-runtime.ts +200 -449
  12. package/src/db/base.service.ts +52 -28
  13. package/src/db/cursor-pagination.ts +71 -30
  14. package/src/db/memory-query-builder.ts +2 -1
  15. package/src/db/memory-store.helpers.ts +4 -7
  16. package/src/db/memory-store.ts +868 -601
  17. package/src/db/memory.ts +396 -280
  18. package/src/db/record-id.ts +32 -10
  19. package/src/db/schema-fingerprint.ts +30 -12
  20. package/src/db/service-normalization.ts +288 -0
  21. package/src/db/service.ts +912 -779
  22. package/src/db/startup.ts +153 -68
  23. package/src/db/transaction-conflict.ts +15 -0
  24. package/src/effect/awaitable-effect.ts +96 -0
  25. package/src/effect/errors.ts +121 -0
  26. package/src/effect/helpers.ts +123 -0
  27. package/src/effect/index.ts +24 -0
  28. package/src/effect/layers.ts +238 -0
  29. package/src/effect/runtime-ref.ts +25 -0
  30. package/src/effect/runtime.ts +46 -0
  31. package/src/effect/services.ts +61 -0
  32. package/src/effect/zod.ts +43 -0
  33. package/src/embeddings/provider.ts +128 -83
  34. package/src/index.ts +48 -1
  35. package/src/openrouter/direct-provider.ts +11 -35
  36. package/src/queues/autonomous-job.queue.ts +117 -73
  37. package/src/queues/context-compaction.queue.ts +50 -17
  38. package/src/queues/delayed-node-promotion.queue.ts +46 -17
  39. package/src/queues/document-processor.queue.ts +52 -77
  40. package/src/queues/memory-consolidation.queue.ts +47 -32
  41. package/src/queues/organization-learning.queue.ts +26 -4
  42. package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
  43. package/src/queues/plan-scheduler.queue.ts +97 -33
  44. package/src/queues/post-chat-memory.queue.ts +56 -26
  45. package/src/queues/queue-factory.ts +227 -59
  46. package/src/queues/standalone-worker.ts +39 -0
  47. package/src/queues/title-generation.queue.ts +45 -11
  48. package/src/redis/connection.ts +182 -113
  49. package/src/redis/index.ts +6 -8
  50. package/src/redis/org-memory-lock.ts +60 -27
  51. package/src/redis/redis-lease-lock.ts +200 -121
  52. package/src/redis/runtime-connection.ts +20 -0
  53. package/src/redis/stream-context.ts +92 -46
  54. package/src/runtime/agent-identity-overrides.ts +2 -2
  55. package/src/runtime/agent-runtime-policy.ts +5 -2
  56. package/src/runtime/agent-stream-helpers.ts +24 -9
  57. package/src/runtime/chat-run-orchestration.ts +102 -19
  58. package/src/runtime/chat-run-registry.ts +36 -2
  59. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  60. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
  61. package/src/runtime/domain-layer.ts +192 -0
  62. package/src/runtime/execution-plan-visibility.ts +2 -2
  63. package/src/runtime/execution-plan.ts +42 -15
  64. package/src/runtime/graph-designer.ts +16 -4
  65. package/src/runtime/helper-model.ts +139 -48
  66. package/src/runtime/index.ts +7 -8
  67. package/src/runtime/indexed-repositories-policy.ts +3 -3
  68. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
  69. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  70. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
  71. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  72. package/src/runtime/memory/memory-scope.ts +53 -0
  73. package/src/runtime/plugin-resolution.ts +124 -25
  74. package/src/runtime/plugin-types.ts +9 -1
  75. package/src/runtime/post-turn-side-effects.ts +177 -130
  76. package/src/runtime/retrieval-adapters.ts +40 -6
  77. package/src/runtime/runtime-accessors.ts +92 -0
  78. package/src/runtime/runtime-config.ts +150 -61
  79. package/src/runtime/runtime-extensions.ts +23 -25
  80. package/src/runtime/runtime-lifecycle.ts +124 -0
  81. package/src/runtime/runtime-services.ts +386 -0
  82. package/src/runtime/runtime-token.ts +47 -0
  83. package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
  84. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
  85. package/src/runtime/social-chat/social-chat.ts +630 -0
  86. package/src/runtime/specialist-runner.ts +36 -10
  87. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
  88. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  89. package/src/runtime/thread-chat-helpers.ts +2 -2
  90. package/src/runtime/thread-plan-turn.ts +2 -1
  91. package/src/runtime/thread-turn-context.ts +183 -111
  92. package/src/runtime/turn-lifecycle.ts +93 -27
  93. package/src/services/agent-activity.service.ts +287 -203
  94. package/src/services/agent-executor.service.ts +253 -149
  95. package/src/services/artifact.service.ts +231 -149
  96. package/src/services/attachment.service.ts +171 -115
  97. package/src/services/autonomous-job.service.ts +890 -491
  98. package/src/services/background-work.service.ts +54 -0
  99. package/src/services/chat-run-registry.service.ts +13 -1
  100. package/src/services/context-compaction.service.ts +136 -86
  101. package/src/services/document-chunk.service.ts +151 -88
  102. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  103. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  104. package/src/services/execution-plan/execution-plan-graph.ts +278 -0
  105. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  106. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  107. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  108. package/src/services/feedback-loop.service.ts +132 -76
  109. package/src/services/global-orchestrator.service.ts +101 -168
  110. package/src/services/graph-full-routing.ts +193 -0
  111. package/src/services/index.ts +19 -21
  112. package/src/services/institutional-memory.service.ts +213 -125
  113. package/src/services/learned-skill.service.ts +368 -260
  114. package/src/services/memory/memory-conversation.ts +95 -0
  115. package/src/services/memory/memory-errors.ts +27 -0
  116. package/src/services/memory/memory-org-memory.ts +50 -0
  117. package/src/services/memory/memory-preseeded.ts +86 -0
  118. package/src/services/memory/memory-rerank.ts +297 -0
  119. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
  120. package/src/services/memory/memory.service.ts +674 -0
  121. package/src/services/memory/rerank.service.ts +201 -0
  122. package/src/services/monitoring-window.service.ts +92 -70
  123. package/src/services/mutating-approval.service.ts +62 -53
  124. package/src/services/node-workspace.service.ts +141 -98
  125. package/src/services/notification.service.ts +29 -16
  126. package/src/services/organization-member.service.ts +120 -66
  127. package/src/services/organization.service.ts +153 -77
  128. package/src/services/ownership-dispatcher.service.ts +456 -263
  129. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  130. package/src/services/plan/plan-agent-query.service.ts +322 -0
  131. package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
  132. package/src/services/plan/plan-artifact.service.ts +60 -0
  133. package/src/services/plan/plan-builder.service.ts +76 -0
  134. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  135. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  136. package/src/services/plan/plan-completion-side-effects.ts +169 -0
  137. package/src/services/plan/plan-coordination.service.ts +181 -0
  138. package/src/services/plan/plan-cycle.service.ts +405 -0
  139. package/src/services/plan/plan-deadline.service.ts +533 -0
  140. package/src/services/plan/plan-event-delivery.service.ts +266 -0
  141. package/src/services/plan/plan-executor-context.ts +35 -0
  142. package/src/services/plan/plan-executor-graph.ts +522 -0
  143. package/src/services/plan/plan-executor-helpers.ts +307 -0
  144. package/src/services/plan/plan-executor-persistence.ts +209 -0
  145. package/src/services/plan/plan-executor.service.ts +1737 -0
  146. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  147. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  148. package/src/services/plan/plan-run-serialization.ts +15 -0
  149. package/src/services/plan/plan-run.service.ts +637 -0
  150. package/src/services/plan/plan-scheduler.service.ts +379 -0
  151. package/src/services/plan/plan-template.service.ts +224 -0
  152. package/src/services/plan/plan-transaction-events.ts +36 -0
  153. package/src/services/plan/plan-validator.service.ts +907 -0
  154. package/src/services/plan/plan-workspace.service.ts +131 -0
  155. package/src/services/plugin-executor.service.ts +102 -68
  156. package/src/services/quality-metrics.service.ts +112 -94
  157. package/src/services/queue-job.service.ts +288 -231
  158. package/src/services/recent-activity-title.service.ts +73 -36
  159. package/src/services/recent-activity.service.ts +274 -259
  160. package/src/services/skill-resolver.service.ts +38 -12
  161. package/src/services/social-chat-history.service.ts +190 -122
  162. package/src/services/system-executor.service.ts +96 -61
  163. package/src/services/thread/thread-active-run.ts +203 -0
  164. package/src/services/thread/thread-bootstrap.ts +385 -0
  165. package/src/services/thread/thread-listing.ts +199 -0
  166. package/src/services/thread/thread-memory-block.ts +130 -0
  167. package/src/services/thread/thread-message.service.ts +379 -0
  168. package/src/services/thread/thread-record-store.ts +155 -0
  169. package/src/services/thread/thread-title.service.ts +74 -0
  170. package/src/services/thread/thread-turn-execution.ts +280 -0
  171. package/src/services/thread/thread-turn-message-context.ts +73 -0
  172. package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
  173. package/src/services/thread/thread-turn-streaming.ts +403 -0
  174. package/src/services/thread/thread-turn-tracing.ts +35 -0
  175. package/src/services/thread/thread-turn.ts +376 -0
  176. package/src/services/thread/thread.service.ts +344 -0
  177. package/src/services/user.service.ts +82 -32
  178. package/src/services/write-intent-validator.service.ts +63 -51
  179. package/src/storage/attachment-parser.ts +69 -27
  180. package/src/storage/attachment-storage.service.ts +334 -275
  181. package/src/storage/generated-document-storage.service.ts +66 -34
  182. package/src/system-agents/agent-result.ts +3 -1
  183. package/src/system-agents/context-compaction.agent.ts +3 -3
  184. package/src/system-agents/delegated-agent-factory.ts +159 -90
  185. package/src/system-agents/helper-agent-options.ts +1 -1
  186. package/src/system-agents/memory-reranker.agent.ts +3 -3
  187. package/src/system-agents/memory.agent.ts +3 -3
  188. package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
  189. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
  190. package/src/system-agents/skill-extractor.agent.ts +3 -3
  191. package/src/system-agents/skill-manager.agent.ts +3 -3
  192. package/src/system-agents/thread-router.agent.ts +157 -113
  193. package/src/system-agents/title-generator.agent.ts +3 -3
  194. package/src/tools/execution-plan.tool.ts +241 -171
  195. package/src/tools/fetch-webpage.tool.ts +29 -18
  196. package/src/tools/firecrawl-client.ts +26 -6
  197. package/src/tools/index.ts +1 -0
  198. package/src/tools/memory-block.tool.ts +14 -6
  199. package/src/tools/plan-approval.tool.ts +57 -47
  200. package/src/tools/read-file-parts.tool.ts +44 -33
  201. package/src/tools/remember-memory.tool.ts +65 -45
  202. package/src/tools/search-web.tool.ts +33 -22
  203. package/src/tools/search.tool.ts +41 -29
  204. package/src/tools/team-think.tool.ts +125 -84
  205. package/src/tools/user-questions.tool.ts +4 -3
  206. package/src/tools/web-tool-shared.ts +6 -0
  207. package/src/utils/async.ts +25 -22
  208. package/src/utils/crypto.ts +21 -0
  209. package/src/utils/date-time.ts +40 -1
  210. package/src/utils/errors.ts +111 -20
  211. package/src/utils/hono-error-handler.ts +24 -39
  212. package/src/utils/index.ts +2 -1
  213. package/src/utils/null-proto-record.ts +41 -0
  214. package/src/utils/sse-keepalive.ts +124 -21
  215. package/src/workers/bootstrap.ts +164 -52
  216. package/src/workers/memory-consolidation.worker.ts +325 -237
  217. package/src/workers/organization-learning.worker.ts +50 -16
  218. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  219. package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
  220. package/src/workers/skill-extraction.runner.ts +176 -93
  221. package/src/workers/utils/file-section-chunker.ts +8 -10
  222. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  223. package/src/workers/utils/repomix-file-sections.ts +2 -2
  224. package/src/workers/utils/thread-message-query.ts +97 -38
  225. package/src/workers/worker-utils.ts +74 -31
  226. package/src/config/debug-logger.ts +0 -47
  227. package/src/config/search.ts +0 -3
  228. package/src/redis/connection-accessor.ts +0 -26
  229. package/src/runtime/agent-types.ts +0 -1
  230. package/src/runtime/context-compaction-runtime.ts +0 -87
  231. package/src/runtime/memory-scope.ts +0 -43
  232. package/src/runtime/social-chat-agent-runner.ts +0 -118
  233. package/src/runtime/social-chat.ts +0 -516
  234. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  235. package/src/services/adaptive-playbook.service.ts +0 -152
  236. package/src/services/artifact-provenance.service.ts +0 -172
  237. package/src/services/chat-attachments.service.ts +0 -17
  238. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  239. package/src/services/execution-plan.service.ts +0 -1118
  240. package/src/services/memory.service.ts +0 -914
  241. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  242. package/src/services/plan-agent-query.service.ts +0 -267
  243. package/src/services/plan-artifact.service.ts +0 -50
  244. package/src/services/plan-builder.service.ts +0 -67
  245. package/src/services/plan-checkpoint.service.ts +0 -81
  246. package/src/services/plan-completion-side-effects.ts +0 -80
  247. package/src/services/plan-coordination.service.ts +0 -157
  248. package/src/services/plan-cycle.service.ts +0 -284
  249. package/src/services/plan-deadline.service.ts +0 -430
  250. package/src/services/plan-event-delivery.service.ts +0 -166
  251. package/src/services/plan-executor.service.ts +0 -1950
  252. package/src/services/plan-run.service.ts +0 -515
  253. package/src/services/plan-scheduler.service.ts +0 -240
  254. package/src/services/plan-template.service.ts +0 -177
  255. package/src/services/plan-validator.service.ts +0 -818
  256. package/src/services/plan-workspace.service.ts +0 -83
  257. package/src/services/rerank.service.ts +0 -156
  258. package/src/services/thread-message.service.ts +0 -275
  259. package/src/services/thread-plan-registry.service.ts +0 -22
  260. package/src/services/thread-title.service.ts +0 -39
  261. package/src/services/thread-turn-preparation.service.ts +0 -1147
  262. package/src/services/thread-turn.ts +0 -172
  263. package/src/services/thread.service.ts +0 -869
  264. package/src/utils/env.ts +0 -8
  265. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  266. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  267. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  268. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  269. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  270. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  271. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  272. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,131 @@
1
+ import { Context, Effect, Layer, Schema } from 'effect'
2
+
3
+ import { ServiceError } from '../../effect/errors'
4
+ import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
5
+ import { RedisServiceTag } from '../../effect/services'
6
+ import { toValidationError } from '../../effect/zod'
7
+ import type { RedisConnectionManager } from '../../redis/connection'
8
+ import { nowEpochMillis } from '../../utils/date-time'
9
+
10
+ const tryWorkspacePromise = makeEffectTryPromiseWithMessage((message, cause) => new ServiceError({ message, cause }))
11
+
12
+ const PlanWorkspaceEntrySchema = Schema.Struct({
13
+ value: Schema.Unknown,
14
+ version: Schema.Number,
15
+ writeSequence: Schema.Number,
16
+ nodeId: Schema.String,
17
+ timestamp: Schema.Number,
18
+ })
19
+
20
+ const PlanWorkspaceEntryJsonSchema = Schema.fromJsonString(PlanWorkspaceEntrySchema)
21
+
22
+ export type PlanWorkspaceEntry = {
23
+ value: unknown
24
+ version: number
25
+ writeSequence: number
26
+ nodeId: string
27
+ timestamp: number
28
+ }
29
+
30
+ const PLAN_WORKSPACE_KEY_PREFIX = 'plan-workspace:'
31
+
32
+ function parsePlanWorkspaceEntryEffect(
33
+ fieldKey: string,
34
+ raw: string,
35
+ ): Effect.Effect<PlanWorkspaceEntry, ReturnType<typeof toValidationError>> {
36
+ return Schema.decodeUnknownEffect(PlanWorkspaceEntryJsonSchema)(raw).pipe(
37
+ Effect.mapError((error) => toValidationError(error, `Invalid plan workspace entry JSON for "${fieldKey}"`)),
38
+ )
39
+ }
40
+
41
+ export function makePlanWorkspaceService(redis: RedisConnectionManager) {
42
+ const writeEffect = (params: {
43
+ runId: string
44
+ nodeId: string
45
+ key: string
46
+ value: unknown
47
+ version: number
48
+ checkpointSequence: number
49
+ }) =>
50
+ Effect.gen(function* () {
51
+ const conn = redis.getConnection()
52
+ const hashKey = `${PLAN_WORKSPACE_KEY_PREFIX}${params.runId}`
53
+ const fieldKey = `${params.nodeId}:${params.key}`
54
+ const entry: PlanWorkspaceEntry = {
55
+ value: params.value,
56
+ version: params.version,
57
+ writeSequence: params.checkpointSequence,
58
+ nodeId: params.nodeId,
59
+ timestamp: nowEpochMillis(),
60
+ }
61
+ const serialized = yield* Schema.encodeEffect(PlanWorkspaceEntryJsonSchema)(entry)
62
+ yield* tryWorkspacePromise(
63
+ () => conn.hset(hashKey, fieldKey, serialized),
64
+ 'Failed to write plan workspace entry.',
65
+ )
66
+ })
67
+
68
+ const snapshotReadEffect = (params: { runId: string; readerNodeId: string; snapshotSequence?: number }) =>
69
+ Effect.gen(function* () {
70
+ const conn = redis.getConnection()
71
+ const hashKey = `${PLAN_WORKSPACE_KEY_PREFIX}${params.runId}`
72
+ const nodePrefix = `${params.readerNodeId}:`
73
+ const all = yield* tryWorkspacePromise(() => conn.hgetall(hashKey), 'Failed to read plan workspace snapshot.')
74
+ const result: Record<string, PlanWorkspaceEntry> = {}
75
+ for (const [fieldKey, raw] of Object.entries(all)) {
76
+ if (!fieldKey.startsWith(nodePrefix)) continue
77
+ const entry = yield* parsePlanWorkspaceEntryEffect(fieldKey, raw)
78
+ if (params.snapshotSequence !== undefined && entry.writeSequence > params.snapshotSequence) {
79
+ continue
80
+ }
81
+ result[fieldKey] = entry
82
+ }
83
+ return result
84
+ })
85
+
86
+ const currentReadEffect = (params: { runId: string; key?: string }) =>
87
+ Effect.gen(function* () {
88
+ const conn = redis.getConnection()
89
+ const hashKey = `${PLAN_WORKSPACE_KEY_PREFIX}${params.runId}`
90
+ const key = params.key
91
+ if (key !== undefined) {
92
+ const raw = yield* tryWorkspacePromise(() => conn.hget(hashKey, key), 'Failed to read plan workspace entry.')
93
+ if (!raw) return {}
94
+ return { [key]: yield* parsePlanWorkspaceEntryEffect(key, raw) }
95
+ }
96
+ const all = yield* tryWorkspacePromise(() => conn.hgetall(hashKey), 'Failed to read plan workspace entries.')
97
+ const result: Record<string, PlanWorkspaceEntry> = {}
98
+ for (const [k, v] of Object.entries(all)) {
99
+ result[k] = yield* parsePlanWorkspaceEntryEffect(k, v)
100
+ }
101
+ return result
102
+ })
103
+
104
+ const cleanupEffect = (runId: string) =>
105
+ Effect.asVoid(
106
+ tryWorkspacePromise(() => {
107
+ const conn = redis.getConnection()
108
+ return conn.del(`${PLAN_WORKSPACE_KEY_PREFIX}${runId}`)
109
+ }, 'Failed to clean up plan workspace.'),
110
+ )
111
+
112
+ return {
113
+ write: writeEffect,
114
+ snapshotRead: snapshotReadEffect,
115
+ currentRead: currentReadEffect,
116
+ cleanup: cleanupEffect,
117
+ }
118
+ }
119
+
120
+ export class PlanWorkspaceServiceTag extends Context.Service<
121
+ PlanWorkspaceServiceTag,
122
+ ReturnType<typeof makePlanWorkspaceService>
123
+ >()('@lota-sdk/core/PlanWorkspaceService') {}
124
+
125
+ export const PlanWorkspaceServiceLive = Layer.effect(
126
+ PlanWorkspaceServiceTag,
127
+ Effect.gen(function* () {
128
+ const redis = yield* RedisServiceTag
129
+ return makePlanWorkspaceService(redis)
130
+ }),
131
+ )
@@ -1,11 +1,21 @@
1
- import type { OwnershipDispatchContext, PlanNodeResult, PlanNodeSpec, PluginPlanNodeOwner } from '@lota-sdk/shared'
1
+ import type { OwnershipDispatchContext, PlanNodeSpec, PluginPlanNodeOwner } from '@lota-sdk/shared'
2
+ import { Context, Effect, Layer } from 'effect'
2
3
 
4
+ import { BadRequestError, ServiceError } from '../effect/errors'
5
+ import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
6
+ import { RuntimeConfigServiceTag } from '../effect/services'
7
+
8
+ const tryPluginExecutorPromise = makeEffectTryPromiseWithMessage(
9
+ (message, cause) => new ServiceError({ message, cause }),
10
+ )
3
11
  import type { LotaPlugin, PluginNodeExecutionParams } from '../runtime/plugin-types'
4
- import { getRuntimeConfig } from '../runtime/runtime-config'
5
- import type { PlanValidationIssueInput } from './plan-validator.service'
12
+ import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
13
+ import type { PlanValidationIssueInput } from './plan/plan-validator.service'
6
14
 
7
- function getPluginRuntime() {
8
- return (getRuntimeConfig().pluginRuntime ?? {}) as Record<string, LotaPlugin | undefined>
15
+ function isPluginOwner(
16
+ owner: PlanNodeSpec['owner'],
17
+ ): owner is Extract<PlanNodeSpec['owner'], { executorType: 'plugin' }> {
18
+ return owner.executorType === 'plugin'
9
19
  }
10
20
 
11
21
  function buildPluginExecutionParams(params: {
@@ -29,75 +39,99 @@ function buildPluginExecutionParams(params: {
29
39
  }
30
40
  }
31
41
 
32
- class PluginExecutorService {
33
- validateOwner(owner: PluginPlanNodeOwner, nodeId: string): PlanValidationIssueInput[] {
34
- const plugin = getPluginRuntime()[owner.ref]
35
- if (!plugin) {
36
- return [
37
- {
38
- severity: 'blocking',
39
- code: 'plugin_executor_missing',
40
- message: `Node "${nodeId}" references unknown plugin executor "${owner.ref}".`,
41
- nodeId,
42
- detail: { pluginRef: owner.ref },
43
- },
44
- ]
45
- }
42
+ export function makePluginExecutorService(config: ResolvedLotaRuntimeConfig) {
43
+ function getPluginRuntime() {
44
+ return (config.pluginRuntime ?? {}) as Record<string, LotaPlugin | undefined>
45
+ }
46
46
 
47
- const nodeExecutor = plugin.nodeExecutor
48
- if (!nodeExecutor) {
49
- return [
50
- {
51
- severity: 'blocking',
52
- code: 'plugin_node_executor_missing',
53
- message: `Plugin "${owner.ref}" does not expose a node executor.`,
54
- nodeId,
55
- detail: { pluginRef: owner.ref },
56
- },
57
- ]
58
- }
47
+ return {
48
+ validateOwner(owner: PluginPlanNodeOwner, nodeId: string): PlanValidationIssueInput[] {
49
+ const plugin = getPluginRuntime()[owner.ref]
50
+ if (!plugin) {
51
+ return [
52
+ {
53
+ severity: 'blocking',
54
+ code: 'plugin_executor_missing',
55
+ message: `Node "${nodeId}" references unknown plugin executor "${owner.ref}".`,
56
+ nodeId,
57
+ detail: { pluginRef: owner.ref },
58
+ },
59
+ ]
60
+ }
59
61
 
60
- if (!nodeExecutor.supportedOperations.includes(owner.operation)) {
61
- return [
62
- {
63
- severity: 'blocking',
64
- code: 'plugin_operation_missing',
65
- message: `Plugin "${owner.ref}" does not support operation "${owner.operation}".`,
66
- nodeId,
67
- detail: { pluginRef: owner.ref, operation: owner.operation },
68
- },
69
- ]
70
- }
62
+ const nodeExecutor = plugin.nodeExecutor
63
+ if (!nodeExecutor) {
64
+ return [
65
+ {
66
+ severity: 'blocking',
67
+ code: 'plugin_node_executor_missing',
68
+ message: `Plugin "${owner.ref}" does not expose a node executor.`,
69
+ nodeId,
70
+ detail: { pluginRef: owner.ref },
71
+ },
72
+ ]
73
+ }
71
74
 
72
- return []
73
- }
75
+ if (!nodeExecutor.supportedOperations.includes(owner.operation)) {
76
+ return [
77
+ {
78
+ severity: 'blocking',
79
+ code: 'plugin_operation_missing',
80
+ message: `Plugin "${owner.ref}" does not support operation "${owner.operation}".`,
81
+ nodeId,
82
+ detail: { pluginRef: owner.ref, operation: owner.operation },
83
+ },
84
+ ]
85
+ }
74
86
 
75
- async executeNode(params: {
76
- nodeSpec: PlanNodeSpec
77
- resolvedInput: Record<string, unknown>
78
- context: OwnershipDispatchContext
79
- }): Promise<PlanNodeResult> {
80
- if (params.nodeSpec.owner.executorType !== 'plugin') {
81
- throw new Error(`PluginExecutor cannot execute owner type "${params.nodeSpec.owner.executorType}".`)
82
- }
87
+ return []
88
+ },
89
+
90
+ executeNode: Effect.fn('PluginExecutor.executeNode')(function* (params: {
91
+ nodeSpec: PlanNodeSpec
92
+ resolvedInput: Record<string, unknown>
93
+ context: OwnershipDispatchContext
94
+ }) {
95
+ const owner = params.nodeSpec.owner
96
+ if (!isPluginOwner(owner)) {
97
+ return yield* new BadRequestError({
98
+ message: `PluginExecutor cannot execute owner type "${owner.executorType}".`,
99
+ })
100
+ }
83
101
 
84
- const plugin = getPluginRuntime()[params.nodeSpec.owner.ref]
85
- const nodeExecutor = plugin?.nodeExecutor
86
- if (!plugin || !nodeExecutor || !nodeExecutor.supportedOperations.includes(params.nodeSpec.owner.operation)) {
87
- throw new Error(
88
- `Plugin executor ${params.nodeSpec.owner.ref}.${params.nodeSpec.owner.operation} is not registered.`,
89
- )
90
- }
102
+ const plugin = getPluginRuntime()[owner.ref]
103
+ const nodeExecutor = plugin?.nodeExecutor
104
+ if (!plugin || !nodeExecutor || !nodeExecutor.supportedOperations.includes(owner.operation)) {
105
+ return yield* new BadRequestError({
106
+ message: `Plugin executor ${owner.ref}.${owner.operation} is not registered.`,
107
+ })
108
+ }
91
109
 
92
- return nodeExecutor.executeNode(
93
- buildPluginExecutionParams({
94
- owner: params.nodeSpec.owner,
95
- nodeSpec: params.nodeSpec,
96
- resolvedInput: params.resolvedInput,
97
- context: params.context,
98
- }),
99
- )
110
+ return yield* tryPluginExecutorPromise(
111
+ () =>
112
+ nodeExecutor.executeNode(
113
+ buildPluginExecutionParams({
114
+ owner,
115
+ nodeSpec: params.nodeSpec,
116
+ resolvedInput: params.resolvedInput,
117
+ context: params.context,
118
+ }),
119
+ ),
120
+ `Plugin executor "${owner.ref}.${owner.operation}" failed.`,
121
+ ).pipe(Effect.withSpan('PluginExecutor.invoke'))
122
+ }),
100
123
  }
101
124
  }
102
125
 
103
- export const pluginExecutorService = new PluginExecutorService()
126
+ export class PluginExecutorServiceTag extends Context.Service<
127
+ PluginExecutorServiceTag,
128
+ ReturnType<typeof makePluginExecutorService>
129
+ >()('@lota-sdk/core/PluginExecutorService') {}
130
+
131
+ export const PluginExecutorServiceLive = Layer.effect(
132
+ PluginExecutorServiceTag,
133
+ Effect.gen(function* () {
134
+ const runtimeConfig = yield* RuntimeConfigServiceTag
135
+ return makePluginExecutorService(runtimeConfig)
136
+ }),
137
+ )
@@ -1,10 +1,12 @@
1
1
  import { NodeQualityMetricsSchema } from '@lota-sdk/shared'
2
2
  import type { NodeQualityMetrics } from '@lota-sdk/shared'
3
+ import { Context, Effect, Layer } from 'effect'
3
4
  import { z } from 'zod'
4
5
 
5
6
  import { ensureRecordId } from '../db/record-id'
6
- import { databaseService } from '../db/service'
7
+ import type { SurrealDBService } from '../db/service'
7
8
  import { TABLES } from '../db/tables'
9
+ import { DatabaseServiceTag } from '../effect/services'
8
10
 
9
11
  const QualityMetricRowSchema = NodeQualityMetricsSchema.extend({
10
12
  id: z.unknown(),
@@ -15,118 +17,134 @@ const QualityMetricRowSchema = NodeQualityMetricsSchema.extend({
15
17
  updatedAt: z.coerce.date().optional(),
16
18
  })
17
19
 
18
- class QualityMetricsService {
19
- async recordNodeMetrics(params: {
20
+ export function makeQualityMetricsService(db: SurrealDBService) {
21
+ const recordNodeMetricsEffect = (params: {
20
22
  organizationId: string
21
23
  runId: string
22
24
  nodeId: string
23
25
  metrics: NodeQualityMetrics
24
- }): Promise<void> {
25
- await databaseService.create(
26
- TABLES.QUALITY_METRIC,
27
- {
28
- organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
29
- runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
30
- nodeId: params.nodeId,
31
- ownerRef: params.metrics.ownerRef,
32
- ownerType: params.metrics.ownerType,
33
- nodeType: params.metrics.nodeType,
34
- executionTimeMs: params.metrics.executionTimeMs,
35
- attemptCount: params.metrics.attemptCount,
36
- artifactCount: params.metrics.artifactCount,
37
- validationIssueCount: params.metrics.validationIssueCount,
38
- },
39
- QualityMetricRowSchema,
40
- )
41
- }
42
-
43
- async recordCycleMetrics(params: { organizationId: string; runId: string }): Promise<void> {
44
- const existing = await databaseService.findMany(
45
- TABLES.QUALITY_METRIC,
46
- { runId: ensureRecordId(params.runId, TABLES.PLAN_RUN) },
47
- QualityMetricRowSchema,
48
- { orderBy: 'createdAt', orderDir: 'ASC' },
49
- )
50
-
51
- if (!Array.isArray(existing) || existing.length === 0) return
52
-
53
- const totalExecutionTime = existing.reduce(
54
- (sum, m) => sum + (typeof m.executionTimeMs === 'number' ? m.executionTimeMs : 0),
55
- 0,
56
- )
57
- const totalAttempts = existing.reduce(
58
- (sum, m) => sum + (typeof m.attemptCount === 'number' ? m.attemptCount : 0),
59
- 0,
60
- )
61
- const totalIssues = existing.reduce(
62
- (sum, m) => sum + (typeof m.validationIssueCount === 'number' ? m.validationIssueCount : 0),
63
- 0,
26
+ }) =>
27
+ Effect.asVoid(
28
+ db.create(
29
+ TABLES.QUALITY_METRIC,
30
+ {
31
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
32
+ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
33
+ nodeId: params.nodeId,
34
+ ownerRef: params.metrics.ownerRef,
35
+ ownerType: params.metrics.ownerType,
36
+ nodeType: params.metrics.nodeType,
37
+ executionTimeMs: params.metrics.executionTimeMs,
38
+ attemptCount: params.metrics.attemptCount,
39
+ artifactCount: params.metrics.artifactCount,
40
+ validationIssueCount: params.metrics.validationIssueCount,
41
+ },
42
+ QualityMetricRowSchema,
43
+ ),
64
44
  )
65
45
 
66
- await databaseService.create(
46
+ const recordCycleMetricsEffect = (params: { organizationId: string; runId: string }) =>
47
+ Effect.gen(function* () {
48
+ const existing = yield* db.findMany(
49
+ TABLES.QUALITY_METRIC,
50
+ { runId: ensureRecordId(params.runId, TABLES.PLAN_RUN) },
51
+ QualityMetricRowSchema,
52
+ { orderBy: 'createdAt', orderDir: 'ASC' },
53
+ )
54
+
55
+ if (!Array.isArray(existing) || existing.length === 0) return
56
+
57
+ const totalExecutionTime = existing.reduce(
58
+ (sum, m) => sum + (typeof m.executionTimeMs === 'number' ? m.executionTimeMs : 0),
59
+ 0,
60
+ )
61
+ const totalAttempts = existing.reduce(
62
+ (sum, m) => sum + (typeof m.attemptCount === 'number' ? m.attemptCount : 0),
63
+ 0,
64
+ )
65
+ const totalIssues = existing.reduce(
66
+ (sum, m) => sum + (typeof m.validationIssueCount === 'number' ? m.validationIssueCount : 0),
67
+ 0,
68
+ )
69
+
70
+ yield* Effect.asVoid(
71
+ db.create(
72
+ TABLES.QUALITY_METRIC,
73
+ {
74
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
75
+ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
76
+ ownerRef: 'cycle-summary',
77
+ ownerType: 'system',
78
+ nodeType: 'action',
79
+ executionTimeMs: totalExecutionTime,
80
+ attemptCount: totalAttempts,
81
+ artifactCount: existing.length,
82
+ validationIssueCount: totalIssues,
83
+ },
84
+ QualityMetricRowSchema,
85
+ ),
86
+ )
87
+ })
88
+
89
+ const getMetricsByOwnerEffect = (params: { organizationId: string; ownerRef: string; limit?: number }) =>
90
+ db.findMany(
67
91
  TABLES.QUALITY_METRIC,
68
- {
69
- organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
70
- runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
71
- ownerRef: 'cycle-summary',
72
- ownerType: 'system',
73
- nodeType: 'action',
74
- executionTimeMs: totalExecutionTime,
75
- attemptCount: totalAttempts,
76
- artifactCount: existing.length,
77
- validationIssueCount: totalIssues,
78
- },
92
+ { organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION), ownerRef: params.ownerRef },
79
93
  QualityMetricRowSchema,
94
+ { orderBy: 'createdAt', orderDir: 'DESC', limit: params.limit ?? 20 },
80
95
  )
81
- }
82
96
 
83
- detectRegression(params: {
84
- metrics: Array<{ executionTimeMs: number; validationIssueCount: number }>
85
- window?: number
86
- }): { detected: boolean; trend?: 'improving' | 'stable' | 'regressing' } {
87
- const window = params.window ?? 5
88
- const recent = params.metrics.slice(-window)
89
- if (recent.length < 2) return { detected: false, trend: 'stable' }
97
+ return {
98
+ recordNodeMetrics: recordNodeMetricsEffect,
99
+ recordCycleMetrics: recordCycleMetricsEffect,
100
+ getMetricsByOwner: getMetricsByOwnerEffect,
90
101
 
91
- const mid = Math.floor(recent.length / 2)
92
- const firstHalf = recent.slice(0, mid)
93
- const secondHalf = recent.slice(mid)
102
+ detectRegression(params: {
103
+ metrics: Array<{ executionTimeMs: number; validationIssueCount: number }>
104
+ window?: number
105
+ }): { detected: boolean; trend?: 'improving' | 'stable' | 'regressing' } {
106
+ const window = params.window ?? 5
107
+ const recent = params.metrics.slice(-window)
108
+ if (recent.length < 2) return { detected: false, trend: 'stable' }
94
109
 
95
- const avgFirst = firstHalf.reduce((sum, m) => sum + m.validationIssueCount, 0) / firstHalf.length
96
- const avgSecond = secondHalf.reduce((sum, m) => sum + m.validationIssueCount, 0) / secondHalf.length
110
+ const mid = Math.floor(recent.length / 2)
111
+ const firstHalf = recent.slice(0, mid)
112
+ const secondHalf = recent.slice(mid)
97
113
 
98
- const timeFirst = firstHalf.reduce((sum, m) => sum + m.executionTimeMs, 0) / firstHalf.length
99
- const timeSecond = secondHalf.reduce((sum, m) => sum + m.executionTimeMs, 0) / secondHalf.length
114
+ const avgFirst = firstHalf.reduce((sum, m) => sum + m.validationIssueCount, 0) / firstHalf.length
115
+ const avgSecond = secondHalf.reduce((sum, m) => sum + m.validationIssueCount, 0) / secondHalf.length
100
116
 
101
- const issueRegression = avgFirst > 0 && avgSecond > avgFirst * 1.5
102
- const timeRegression = timeFirst > 0 && timeSecond > timeFirst * 2
117
+ const timeFirst = firstHalf.reduce((sum, m) => sum + m.executionTimeMs, 0) / firstHalf.length
118
+ const timeSecond = secondHalf.reduce((sum, m) => sum + m.executionTimeMs, 0) / secondHalf.length
103
119
 
104
- if (issueRegression || timeRegression) {
105
- return { detected: true, trend: 'regressing' }
106
- }
120
+ const issueRegression = avgFirst > 0 && avgSecond > avgFirst * 1.5
121
+ const timeRegression = timeFirst > 0 && timeSecond > timeFirst * 2
107
122
 
108
- const issueImproving = avgFirst > 0 && avgSecond < avgFirst * 0.7
109
- const timeImproving = timeFirst > 0 && timeSecond < timeFirst * 0.7
123
+ if (issueRegression || timeRegression) {
124
+ return { detected: true, trend: 'regressing' }
125
+ }
110
126
 
111
- if (issueImproving || timeImproving) {
112
- return { detected: false, trend: 'improving' }
113
- }
127
+ const issueImproving = avgFirst > 0 && avgSecond < avgFirst * 0.7
128
+ const timeImproving = timeFirst > 0 && timeSecond < timeFirst * 0.7
114
129
 
115
- return { detected: false, trend: 'stable' }
116
- }
130
+ if (issueImproving || timeImproving) {
131
+ return { detected: false, trend: 'improving' }
132
+ }
117
133
 
118
- async getMetricsByOwner(params: {
119
- organizationId: string
120
- ownerRef: string
121
- limit?: number
122
- }): Promise<z.infer<typeof QualityMetricRowSchema>[]> {
123
- return databaseService.findMany(
124
- TABLES.QUALITY_METRIC,
125
- { organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION), ownerRef: params.ownerRef },
126
- QualityMetricRowSchema,
127
- { orderBy: 'createdAt', orderDir: 'DESC', limit: params.limit ?? 20 },
128
- )
134
+ return { detected: false, trend: 'stable' }
135
+ },
129
136
  }
130
137
  }
131
138
 
132
- export const qualityMetricsService = new QualityMetricsService()
139
+ export class QualityMetricsServiceTag extends Context.Service<
140
+ QualityMetricsServiceTag,
141
+ ReturnType<typeof makeQualityMetricsService>
142
+ >()('@lota-sdk/core/QualityMetricsService') {}
143
+
144
+ export const QualityMetricsServiceLive = Layer.effect(
145
+ QualityMetricsServiceTag,
146
+ Effect.gen(function* () {
147
+ const db = yield* DatabaseServiceTag
148
+ return makeQualityMetricsService(db)
149
+ }),
150
+ )