@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
@@ -1,96 +1,152 @@
1
1
  import type { Recommendation } from '@lota-sdk/shared'
2
+ import { Context, Schema, Effect, Layer } from 'effect'
2
3
 
3
4
  import { ensureRecordId, recordIdToString } from '../db/record-id'
4
5
  import { TABLES } from '../db/tables'
5
- import { toIsoDateTimeString } from '../utils/date-time'
6
- import { planRunService } from './plan-run.service'
6
+ import { toIsoDateTimeString, unsafeDateFrom } from '../utils/date-time'
7
+ import type { makePlanRunService } from './plan/plan-run.service'
8
+ import { PlanRunServiceTag } from './plan/plan-run.service'
7
9
 
8
- class FeedbackLoopService {
9
- async analyzeOutcomes(params: { runId: string; organizationId: string }): Promise<Recommendation[]> {
10
- const run = await planRunService.getRunById(params.runId)
11
- const nodeRuns = await planRunService.listNodeRuns(run.id)
12
- const attempts = await planRunService.listAttempts(run.id)
13
- const nodeSpecs = await planRunService.listNodeSpecs(ensureRecordId(run.planSpecId, TABLES.PLAN_SPEC))
14
- const nodeSpecsByNodeId = new Map(nodeSpecs.map((ns) => [ns.nodeId, ns]))
10
+ class FeedbackLoopServiceError extends Schema.TaggedErrorClass<FeedbackLoopServiceError>()('FeedbackLoopServiceError', {
11
+ message: Schema.String,
12
+ cause: Schema.Defect,
13
+ }) {}
15
14
 
16
- const recommendations: Recommendation[] = []
15
+ export function makeFeedbackLoopService(planRunService: ReturnType<typeof makePlanRunService>) {
16
+ return {
17
+ analyzeOutcomes(params: { runId: string; organizationId: string }) {
18
+ return Effect.gen(function* () {
19
+ const run = yield* planRunService
20
+ .getRunById(params.runId)
21
+ .pipe(
22
+ Effect.mapError(
23
+ (cause) =>
24
+ new FeedbackLoopServiceError({ message: 'Failed to load plan run for feedback analysis.', cause }),
25
+ ),
26
+ )
27
+ const [nodeRuns, attempts, nodeSpecs] = yield* Effect.all([
28
+ planRunService
29
+ .listNodeRuns(run.id)
30
+ .pipe(
31
+ Effect.mapError(
32
+ (cause) =>
33
+ new FeedbackLoopServiceError({ message: 'Failed to load node runs for feedback analysis.', cause }),
34
+ ),
35
+ ),
36
+ planRunService
37
+ .listAttempts(run.id)
38
+ .pipe(
39
+ Effect.mapError(
40
+ (cause) =>
41
+ new FeedbackLoopServiceError({
42
+ message: 'Failed to load node attempts for feedback analysis.',
43
+ cause,
44
+ }),
45
+ ),
46
+ ),
47
+ planRunService
48
+ .listNodeSpecs(ensureRecordId(run.planSpecId, TABLES.PLAN_SPEC))
49
+ .pipe(
50
+ Effect.mapError(
51
+ (cause) =>
52
+ new FeedbackLoopServiceError({ message: 'Failed to load node specs for feedback analysis.', cause }),
53
+ ),
54
+ ),
55
+ ])
17
56
 
18
- for (const nodeRun of nodeRuns) {
19
- const nodeSpec = nodeSpecsByNodeId.get(nodeRun.nodeId)
20
- if (!nodeSpec) continue
57
+ const nodeSpecsByNodeId = new Map(nodeSpecs.map((ns) => [ns.nodeId, ns]))
21
58
 
22
- const nodeAttempts = attempts.filter((a) => a.nodeId === nodeRun.nodeId)
23
- const failedAttempts = nodeAttempts.filter((a) => a.status === 'failed')
59
+ const recommendations: Recommendation[] = []
24
60
 
25
- if (failedAttempts.length >= 2) {
26
- recommendations.push({
27
- type: 'warning',
28
- target: 'node',
29
- targetId: nodeRun.nodeId,
30
- description: `Node "${nodeSpec.label}" failed ${failedAttempts.length} times before ${nodeRun.status === 'completed' || nodeRun.status === 'partial' ? 'succeeding' : 'giving up'}. Consider adjusting retry policy or node instructions.`,
31
- evidence: failedAttempts.map((a) => ({
32
- sourceType: 'metric',
33
- sourceId: recordIdToString(a.id, TABLES.PLAN_NODE_ATTEMPT),
34
- summary: `Attempt failed with class: ${a.failureClass ?? 'unknown'}`,
35
- confidence: 0.9,
36
- })),
37
- confidence: Math.min(0.5 + failedAttempts.length * 0.15, 0.95),
38
- })
39
- }
40
- }
61
+ for (const nodeRun of nodeRuns) {
62
+ const nodeSpec = nodeSpecsByNodeId.get(nodeRun.nodeId)
63
+ if (!nodeSpec) continue
41
64
 
42
- const completedNodeRuns = nodeRuns.filter(
43
- (nr) => nr.startedAt && nr.completedAt && (nr.status === 'completed' || nr.status === 'partial'),
44
- )
45
- if (completedNodeRuns.length >= 2) {
46
- const durations = completedNodeRuns.map((nr) => {
47
- const start = new Date(toIsoDateTimeString(nr.startedAt)).getTime()
48
- const end = new Date(toIsoDateTimeString(nr.completedAt)).getTime()
49
- return { nodeId: nr.nodeId, durationMs: end - start }
50
- })
65
+ const nodeAttempts = attempts.filter((a) => a.nodeId === nodeRun.nodeId)
66
+ const failedAttempts = nodeAttempts.filter((a) => a.status === 'failed')
67
+
68
+ if (failedAttempts.length >= 2) {
69
+ recommendations.push({
70
+ type: 'warning',
71
+ target: 'node',
72
+ targetId: nodeRun.nodeId,
73
+ description: `Node "${nodeSpec.label}" failed ${failedAttempts.length} times before ${nodeRun.status === 'completed' || nodeRun.status === 'partial' ? 'succeeding' : 'giving up'}. Consider adjusting retry policy or node instructions.`,
74
+ evidence: failedAttempts.map((a) => ({
75
+ sourceType: 'metric',
76
+ sourceId: recordIdToString(a.id, TABLES.PLAN_NODE_ATTEMPT),
77
+ summary: `Attempt failed with class: ${a.failureClass ?? 'unknown'}`,
78
+ confidence: 0.9,
79
+ })),
80
+ confidence: Math.min(0.5 + failedAttempts.length * 0.15, 0.95),
81
+ })
82
+ }
83
+ }
84
+
85
+ const completedNodeRuns = nodeRuns.filter(
86
+ (nr) => nr.startedAt && nr.completedAt && (nr.status === 'completed' || nr.status === 'partial'),
87
+ )
88
+ if (completedNodeRuns.length >= 2) {
89
+ const durations = completedNodeRuns.map((nr) => {
90
+ const start = unsafeDateFrom(toIsoDateTimeString(nr.startedAt)).getTime()
91
+ const end = unsafeDateFrom(toIsoDateTimeString(nr.completedAt)).getTime()
92
+ return { nodeId: nr.nodeId, durationMs: end - start }
93
+ })
51
94
 
52
- const avgDuration = durations.reduce((sum, d) => sum + d.durationMs, 0) / durations.length
95
+ const avgDuration = durations.reduce((sum, d) => sum + d.durationMs, 0) / durations.length
53
96
 
54
- for (const d of durations) {
55
- if (d.durationMs > avgDuration * 2.5 && d.durationMs > 5000) {
56
- const nodeSpec = nodeSpecsByNodeId.get(d.nodeId)
97
+ for (const d of durations) {
98
+ if (d.durationMs > avgDuration * 2.5 && d.durationMs > 5000) {
99
+ const nodeSpec = nodeSpecsByNodeId.get(d.nodeId)
100
+ recommendations.push({
101
+ type: 'optimization',
102
+ target: 'node',
103
+ targetId: d.nodeId,
104
+ description: `Node "${nodeSpec?.label ?? d.nodeId}" took ${Math.round(d.durationMs / 1000)}s, which is ${Math.round((d.durationMs / avgDuration) * 10) / 10}x the average. Consider splitting or optimizing.`,
105
+ evidence: [
106
+ {
107
+ sourceType: 'metric',
108
+ sourceId: d.nodeId,
109
+ summary: `Execution time: ${d.durationMs}ms vs average ${Math.round(avgDuration)}ms`,
110
+ confidence: 0.85,
111
+ },
112
+ ],
113
+ confidence: 0.7,
114
+ })
115
+ }
116
+ }
117
+ }
118
+
119
+ const skippedNodes = nodeRuns.filter((nr) => nr.status === 'skipped')
120
+ if (skippedNodes.length > 0 && skippedNodes.length >= nodeRuns.length * 0.3) {
57
121
  recommendations.push({
58
- type: 'optimization',
59
- target: 'node',
60
- targetId: d.nodeId,
61
- description: `Node "${nodeSpec?.label ?? d.nodeId}" took ${Math.round(d.durationMs / 1000)}s, which is ${Math.round((d.durationMs / avgDuration) * 10) / 10}x the average. Consider splitting or optimizing.`,
62
- evidence: [
63
- {
64
- sourceType: 'metric',
65
- sourceId: d.nodeId,
66
- summary: `Execution time: ${d.durationMs}ms vs average ${Math.round(avgDuration)}ms`,
67
- confidence: 0.85,
68
- },
69
- ],
70
- confidence: 0.7,
122
+ type: 'pattern',
123
+ target: 'plan',
124
+ description: `${skippedNodes.length} of ${nodeRuns.length} nodes were skipped. The plan may have overly broad conditional branches.`,
125
+ evidence: skippedNodes.map((nr) => ({
126
+ sourceType: 'pattern',
127
+ sourceId: nr.nodeId,
128
+ summary: `Node "${nodeSpecsByNodeId.get(nr.nodeId)?.label ?? nr.nodeId}" was skipped`,
129
+ confidence: 0.8,
130
+ })),
131
+ confidence: 0.65,
71
132
  })
72
133
  }
73
- }
74
- }
75
134
 
76
- const skippedNodes = nodeRuns.filter((nr) => nr.status === 'skipped')
77
- if (skippedNodes.length > 0 && skippedNodes.length >= nodeRuns.length * 0.3) {
78
- recommendations.push({
79
- type: 'pattern',
80
- target: 'plan',
81
- description: `${skippedNodes.length} of ${nodeRuns.length} nodes were skipped. The plan may have overly broad conditional branches.`,
82
- evidence: skippedNodes.map((nr) => ({
83
- sourceType: 'pattern',
84
- sourceId: nr.nodeId,
85
- summary: `Node "${nodeSpecsByNodeId.get(nr.nodeId)?.label ?? nr.nodeId}" was skipped`,
86
- confidence: 0.8,
87
- })),
88
- confidence: 0.65,
135
+ return recommendations
89
136
  })
90
- }
91
-
92
- return recommendations
137
+ },
93
138
  }
94
139
  }
95
140
 
96
- export const feedbackLoopService = new FeedbackLoopService()
141
+ export class FeedbackLoopServiceTag extends Context.Service<
142
+ FeedbackLoopServiceTag,
143
+ ReturnType<typeof makeFeedbackLoopService>
144
+ >()('@lota-sdk/core/FeedbackLoopService') {}
145
+
146
+ export const FeedbackLoopServiceLive = Layer.effect(
147
+ FeedbackLoopServiceTag,
148
+ Effect.gen(function* () {
149
+ const planRunService = yield* PlanRunServiceTag
150
+ return makeFeedbackLoopService(planRunService)
151
+ }),
152
+ )
@@ -1,180 +1,113 @@
1
- import type { ConvergenceState, PlanFailureClass } from '@lota-sdk/shared'
2
-
3
- import { serverLogger } from '../config/logger'
4
- import { recordIdToString } from '../db/record-id'
5
- import { TABLES } from '../db/tables'
6
- import { shouldPlanNodeUseVisibleTurn } from '../runtime/execution-plan-visibility'
1
+ import type {
2
+ ConvergenceState,
3
+ ExecutionMode,
4
+ PlanNodeResultSubmission,
5
+ PlanNodeRunRecord,
6
+ PlanNodeSpecRecord,
7
+ PlanRunRecord,
8
+ PlanSpecRecord,
9
+ } from '@lota-sdk/shared'
10
+ import { Context, Effect, Layer } from 'effect'
11
+
12
+ import { runPromise } from '../effect/runtime'
13
+ import { routeGraphFullEffect } from './graph-full-routing'
14
+ import type { OwnershipDispatcherService } from './ownership-dispatcher.service'
15
+ import { OwnershipDispatcherServiceTag } from './ownership-dispatcher.service'
16
+ import type { makePlanExecutorService } from './plan/plan-executor.service'
17
+ import { PlanExecutorServiceTag } from './plan/plan-executor.service'
18
+ import type { makePlanRunService } from './plan/plan-run.service'
19
+ import { PlanRunServiceTag } from './plan/plan-run.service'
20
+
21
+ function detectConvergence(params: {
22
+ totalNodes: number
23
+ completedNodes: number
24
+ failedNodes: number
25
+ previousCompletedNodes?: number
26
+ previousFailedNodes?: number
27
+ }): ConvergenceState {
28
+ const completionRatio = params.totalNodes > 0 ? params.completedNodes / params.totalNodes : 0
29
+ const failureRatio = params.totalNodes > 0 ? params.failedNodes / params.totalNodes : 0
30
+
31
+ if (params.previousCompletedNodes !== undefined) {
32
+ const completionVelocity = params.completedNodes - params.previousCompletedNodes
33
+ const failureVelocity = params.failedNodes - (params.previousFailedNodes ?? 0)
34
+
35
+ if (completionVelocity > 0 && failureVelocity === 0) return 'converging'
36
+ if (completionVelocity === 0 && failureVelocity === 0) return 'stalled'
37
+ if (failureVelocity > 0) return 'diverging'
38
+ }
7
39
 
8
- function classifyDispatchFailure(ownerType: string, error: unknown): PlanFailureClass {
9
- const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase()
10
- if (errorMessage.includes('timeout')) return 'timeout_exceeded'
11
- if (ownerType === 'plugin' || ownerType === 'system') return 'external_system_unavailable'
12
- return 'non_recoverable_logic_error'
40
+ if (completionRatio > 0.5 && failureRatio === 0) return 'converging'
41
+ if (failureRatio > 0.3) return 'diverging'
42
+ return 'progressing'
13
43
  }
14
44
 
15
- function formatDispatchError(error: unknown): string {
16
- return error instanceof Error ? error.message : String(error)
45
+ function decideRerouteAction(params: {
46
+ failedNodeId: string
47
+ retryCount: number
48
+ maxRetries: number
49
+ convergenceState: ConvergenceState
50
+ }): 'retry' | 'skip' | 'abort' {
51
+ if (params.retryCount < params.maxRetries) return 'retry'
52
+ if (params.convergenceState === 'converging') return 'skip'
53
+ if (params.convergenceState === 'diverging') return 'abort'
54
+ return 'skip'
17
55
  }
18
56
 
19
- const STABLE_RUN_STATUSES = new Set(['pending-approval', 'awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
20
-
21
- class GlobalOrchestratorService {
22
- detectConvergence(params: {
23
- totalNodes: number
24
- completedNodes: number
25
- failedNodes: number
26
- previousCompletedNodes?: number
27
- previousFailedNodes?: number
28
- }): ConvergenceState {
29
- const completionRatio = params.totalNodes > 0 ? params.completedNodes / params.totalNodes : 0
30
- const failureRatio = params.totalNodes > 0 ? params.failedNodes / params.totalNodes : 0
31
-
32
- if (params.previousCompletedNodes !== undefined) {
33
- const completionVelocity = params.completedNodes - params.previousCompletedNodes
34
- const failureVelocity = params.failedNodes - (params.previousFailedNodes ?? 0)
35
-
36
- if (completionVelocity > 0 && failureVelocity === 0) return 'converging'
37
- if (completionVelocity === 0 && failureVelocity === 0) return 'stalled'
38
- if (failureVelocity > 0) return 'diverging'
39
- }
40
-
41
- if (completionRatio > 0.5 && failureRatio === 0) return 'converging'
42
- if (failureRatio > 0.3) return 'diverging'
43
- return 'progressing'
44
- }
45
-
46
- decideRerouteAction(params: {
47
- failedNodeId: string
48
- retryCount: number
49
- maxRetries: number
50
- convergenceState: ConvergenceState
51
- }): 'retry' | 'skip' | 'abort' {
52
- if (params.retryCount < params.maxRetries) return 'retry'
53
- if (params.convergenceState === 'converging') return 'skip'
54
- if (params.convergenceState === 'diverging') return 'abort'
55
- return 'skip'
56
- }
57
-
58
- async routeGraphFull(params: { threadId: string; runId: string }): Promise<void> {
59
- const MAX_ROUNDS = 32
60
- const STRUCTURAL_TYPES = new Set(['switch', 'join', 'deliberation-fork'])
61
-
62
- // Dynamic imports to avoid circular dependencies
63
- const { planRunService } = await import('./plan-run.service')
64
- const { ownershipDispatcherService } = await import('./ownership-dispatcher.service')
65
- const { planExecutorService } = await import('./plan-executor.service')
66
-
67
- let round = 0
68
- for (; round < MAX_ROUNDS; round++) {
69
- const run = await planRunService.getRunById(params.runId)
70
- if (STABLE_RUN_STATUSES.has(run.status)) break
71
-
72
- const spec = await planRunService.getPlanSpecById(run.planSpecId)
73
- const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
74
- const nodeRuns = await planRunService.listNodeRuns(run.id)
75
-
76
- // Find ready action nodes (not structural, not human — those are handled by syncRunGraph)
77
- const readyNodes = nodeRuns.filter((nr) => {
78
- if (nr.status !== 'ready') return false
79
- const ns = nodeSpecs.find((s) => s.nodeId === nr.nodeId)
80
- return ns && ns.owner.executorType !== 'user' && !STRUCTURAL_TYPES.has(ns.type)
81
- })
82
- if (readyNodes.length === 0) break
83
-
84
- // Split into silent (dispatch now) and visible (enqueue heartbeat wake)
85
- const silentNodes = readyNodes.filter((nr) => {
86
- const ns = nodeSpecs.find((s) => s.nodeId === nr.nodeId)
87
- return ns && !shouldPlanNodeUseVisibleTurn(spec, ns)
88
- })
89
- const visibleNodes = readyNodes.filter((nr) => {
90
- const ns = nodeSpecs.find((s) => s.nodeId === nr.nodeId)
91
- return ns && shouldPlanNodeUseVisibleTurn(spec, ns)
92
- })
93
-
94
- // Transition all ready nodes to 'running' BEFORE dispatching
95
- for (const nodeRun of readyNodes) {
96
- await planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: nodeRun.nodeId })
97
- }
98
-
99
- // Enqueue heartbeat wakes for visible agent nodes — they need a streaming turn
100
- if (visibleNodes.length > 0) {
101
- const { enqueuePlanAgentHeartbeatWake } = await import('../queues/plan-agent-heartbeat.queue')
102
- const updatedRunForWake = await planRunService.getRunById(params.runId)
103
- for (const nodeRun of visibleNodes) {
104
- const ns = nodeSpecs.find((s) => s.nodeId === nodeRun.nodeId)
105
- if (!ns || ns.owner.executorType !== 'agent') continue
106
- await enqueuePlanAgentHeartbeatWake({
107
- organizationId: recordIdToString(updatedRunForWake.organizationId, TABLES.ORGANIZATION),
108
- threadId: recordIdToString(updatedRunForWake.threadId, TABLES.THREAD),
109
- runId: recordIdToString(updatedRunForWake.id, TABLES.PLAN_RUN),
110
- nodeId: nodeRun.nodeId,
111
- agentId: ns.owner.ref,
112
- reason: 'graph-full-visible',
113
- })
114
- }
115
- }
116
-
117
- // If no silent nodes to dispatch, break and let heartbeat handle visible ones
118
- if (silentNodes.length === 0) break
119
-
120
- // Re-fetch run after transitions for accurate state in dispatch context
121
- const updatedRun = await planRunService.getRunById(params.runId)
122
-
123
- // Dispatch silent nodes in parallel with LINEAR mode override (prevents recursion)
124
- const results = await Promise.allSettled(
125
- silentNodes.map(async (nodeRun) => {
126
- const nodeSpecRecord = nodeSpecs.find((ns) => ns.nodeId === nodeRun.nodeId)
127
- if (!nodeSpecRecord) {
128
- throw new Error(`Node spec not found for node "${nodeRun.nodeId}".`)
129
- }
130
- // Re-fetch the node run to get the updated 'running' state with resolvedInput
131
- const updatedNodeRun = await planRunService.getNodeRunByNodeId(updatedRun.id, nodeRun.nodeId)
132
- const result = await ownershipDispatcherService.dispatchReadyNode({
133
- run: updatedRun,
134
- nodeSpecRecord,
135
- nodeRun: updatedNodeRun,
136
- spec,
137
- executionModeOverride: 'linear',
138
- })
139
- return { nodeId: nodeRun.nodeId, ownerRef: nodeSpecRecord.owner.ref, result }
140
- }),
141
- )
57
+ interface GlobalOrchestratorDeps {
58
+ ownershipDispatcherService: Pick<OwnershipDispatcherService, 'dispatchReadyNode'>
59
+ planExecutorService: ReturnType<typeof makePlanExecutorService>
60
+ planRunService: ReturnType<typeof makePlanRunService>
61
+ }
142
62
 
143
- const threadId = recordIdToString(updatedRun.threadId, TABLES.THREAD)
144
- const runId = recordIdToString(updatedRun.id, TABLES.PLAN_RUN)
63
+ type DispatchReadyNodeParams = {
64
+ run: PlanRunRecord
65
+ nodeSpecRecord: PlanNodeSpecRecord
66
+ nodeRun: PlanNodeRunRecord
67
+ spec: PlanSpecRecord
68
+ executionModeOverride?: ExecutionMode
69
+ }
145
70
 
146
- // Submit results sequentially (each triggers syncRunGraph internally)
147
- for (let i = 0; i < results.length; i++) {
148
- const settled = results[i]
149
- const nodeRun = silentNodes[i]
150
- const nodeSpecRecord = nodeSpecs.find((ns) => ns.nodeId === nodeRun.nodeId)
71
+ type DispatchReadyNode = (params: DispatchReadyNodeParams) => Effect.Effect<PlanNodeResultSubmission, unknown, never>
151
72
 
152
- if (settled.status === 'fulfilled') {
153
- await planExecutorService.submitNodeResult({
154
- threadId,
155
- runId,
156
- nodeId: settled.value.nodeId,
157
- emittedBy: settled.value.ownerRef,
158
- result: settled.value.result,
159
- })
160
- } else {
161
- serverLogger.warn`routeGraphFull: dispatch failed for node "${nodeRun.nodeId}": ${settled.reason}`
162
- await planExecutorService.blockNodeOnDispatchFailure({
163
- threadId,
164
- runId,
165
- nodeId: nodeRun.nodeId,
166
- emittedBy: nodeSpecRecord?.owner.ref ?? 'unknown',
167
- message: formatDispatchError(settled.reason),
168
- failureClass: classifyDispatchFailure(nodeSpecRecord?.owner.executorType ?? 'agent', settled.reason),
169
- })
170
- }
171
- }
172
- }
73
+ export function makeGlobalOrchestratorService(deps: GlobalOrchestratorDeps) {
74
+ const dispatchReadyNode: DispatchReadyNode = (params) => deps.ownershipDispatcherService.dispatchReadyNode(params)
173
75
 
174
- if (round === MAX_ROUNDS - 1) {
175
- serverLogger.warn`graph-full execution reached max rounds (${MAX_ROUNDS}) for run ${params.runId} — possible non-converging graph`
176
- }
76
+ return {
77
+ detectConvergence,
78
+ decideRerouteAction,
79
+ routeGraphFull: (params: { threadId: string; runId: string }) =>
80
+ routeGraphFullEffect(params, {
81
+ dispatchReadyNode,
82
+ planExecutorService: deps.planExecutorService,
83
+ planRunService: deps.planRunService,
84
+ }),
177
85
  }
178
86
  }
179
87
 
180
- export const globalOrchestratorService = new GlobalOrchestratorService()
88
+ export class GlobalOrchestratorServiceTag extends Context.Service<
89
+ GlobalOrchestratorServiceTag,
90
+ ReturnType<typeof makeGlobalOrchestratorService>
91
+ >()('@lota-sdk/core/GlobalOrchestratorService') {}
92
+
93
+ export const GlobalOrchestratorServiceLive = Layer.effect(
94
+ GlobalOrchestratorServiceTag,
95
+ Effect.gen(function* () {
96
+ const ownershipDispatcherService = yield* OwnershipDispatcherServiceTag
97
+ const planExecutorService = yield* PlanExecutorServiceTag
98
+ const planRunService = yield* PlanRunServiceTag
99
+ return makeGlobalOrchestratorService({ ownershipDispatcherService, planExecutorService, planRunService })
100
+ }),
101
+ )
102
+
103
+ const routeGraphFullWithRuntime = Effect.fn('GlobalOrchestrator.routeGraphFullWithRuntime')(function* (params: {
104
+ threadId: string
105
+ runId: string
106
+ }) {
107
+ const globalOrchestratorService = yield* GlobalOrchestratorServiceTag
108
+ return yield* globalOrchestratorService.routeGraphFull(params)
109
+ })
110
+
111
+ export function routeGraphFull(params: { threadId: string; runId: string }): Promise<void> {
112
+ return runPromise(routeGraphFullWithRuntime(params))
113
+ }