@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
@@ -13,232 +13,302 @@ import {
13
13
  } from '@lota-sdk/shared'
14
14
  import type { CreateProjectWithPlanResultData, ExecutionPlanArgs } from '@lota-sdk/shared'
15
15
  import { tool } from 'ai'
16
+ import { Effect } from 'effect'
16
17
 
18
+ import { serverLogger } from '../config/logger'
17
19
  import type { RecordIdRef } from '../db/record-id'
18
20
  import { recordIdToString } from '../db/record-id'
19
21
  import { TABLES } from '../db/tables'
20
- import { executionPlanService } from '../services/execution-plan.service'
21
- import { threadService } from '../services/thread.service'
22
-
23
- type ExecutionPlanThreadService = Pick<typeof threadService, 'createThread' | 'deleteThread' | 'getThread'>
24
-
25
- type ExecutionPlanExecutionPlanService = Pick<
26
- typeof executionPlanService,
27
- | 'createPlan'
28
- | 'replacePlan'
29
- | 'resumeRun'
30
- | 'submitNodeResult'
31
- | 'listActivePlanSummaries'
32
- | 'getActivePlanToolResult'
33
- | 'getActivePlansForThread'
22
+ import { BadRequestError, ConflictError, ForbiddenError } from '../effect/errors'
23
+ import { toValidationError } from '../effect/zod'
24
+ import type { makeExecutionPlanService } from '../services/execution-plan/execution-plan.service'
25
+ import type { makeThreadService } from '../services/thread/thread.service'
26
+
27
+ type NoContextService<TService> = {
28
+ [K in keyof TService]: TService[K] extends (...args: infer TArgs) => Effect.Effect<infer A, infer E, unknown>
29
+ ? (...args: TArgs) => Effect.Effect<A, E, never>
30
+ : TService[K]
31
+ }
32
+
33
+ type ExecutionPlanThreadService = NoContextService<
34
+ Pick<ReturnType<typeof makeThreadService>, 'createThread' | 'deleteThread' | 'getThread'>
34
35
  >
35
36
 
37
+ type ExecutionPlanExecutionPlanService = NoContextService<
38
+ Pick<
39
+ ReturnType<typeof makeExecutionPlanService>,
40
+ | 'createPlan'
41
+ | 'replacePlan'
42
+ | 'resumeRun'
43
+ | 'submitNodeResult'
44
+ | 'listActivePlanSummaries'
45
+ | 'getActivePlanToolResult'
46
+ | 'getActivePlansForThread'
47
+ >
48
+ >
49
+
50
+ type ExecutionPlanToolValidationError = ReturnType<typeof toValidationError>
51
+
36
52
  export function createExecutionPlanTool(params: {
37
53
  orgId: RecordIdRef
38
54
  userId: RecordIdRef
39
55
  threadId: RecordIdRef
40
56
  agentId: string
41
- executionPlanService?: ExecutionPlanExecutionPlanService
42
- threadService?: ExecutionPlanThreadService
57
+ executionPlanService: ExecutionPlanExecutionPlanService
58
+ threadService: ExecutionPlanThreadService
43
59
  onPlanChanged?: () => void
44
60
  validateInlinePlan?: (draft: ReturnType<typeof expandAgentPlanDraft>) => void
45
61
  }) {
46
- const resolvedEpService = params.executionPlanService ?? executionPlanService
47
- const resolvedWsService = params.threadService ?? threadService
48
-
49
62
  return tool({
50
63
  description:
51
64
  'Manage execution plans. Actions: create (inline, 1-2 nodes), create-project (dedicated project thread, 3+ nodes), replace (swap active plan), resume (resume interrupted plan), update-node (submit result for a specific node).',
52
65
  inputSchema: ExecutionPlanArgsSchema,
53
- execute: async (input) => {
54
- const parsed = parseExecutionPlanArgs(input)
55
- let result: unknown
56
-
57
- switch (parsed.action) {
58
- case 'create': {
59
- const draft = extractAgentPlanDraft(parsed)
60
- params.validateInlinePlan?.(draft)
61
- const targetThreadId = parsed.targetThreadId ?? params.threadId
62
- const isCrossThreadTarget =
63
- recordIdToString(targetThreadId, TABLES.THREAD) !== recordIdToString(params.threadId, TABLES.THREAD)
64
- result = await resolvedEpService.createPlan({
65
- organizationId: params.orgId,
66
- threadId: targetThreadId,
67
- ...(isCrossThreadTarget ? { sourceThreadId: params.threadId } : {}),
68
- leadAgentId: params.agentId,
69
- createdByAgentId: params.agentId,
70
- requireApproval: parsed.requireApproval ?? isCrossThreadTarget,
71
- input: draft,
72
- })
73
- break
74
- }
75
-
76
- case 'create-project': {
77
- const draft = extractAgentPlanDraft(parsed)
78
- const targetThread = parsed.targetThreadId
79
- ? await resolvedWsService.getThread(parsed.targetThreadId)
80
- : await (() => {
81
- if (!parsed.projectTitle) {
82
- throw new Error('projectTitle is required when action is "create-project".')
66
+ execute: (input) =>
67
+ Effect.runPromise(
68
+ Effect.gen(function* () {
69
+ const parsed = yield* parseExecutionPlanArgsEffect(input)
70
+
71
+ switch (parsed.action) {
72
+ case 'create': {
73
+ const draft = yield* extractAgentPlanDraftEffect(parsed)
74
+ params.validateInlinePlan?.(draft)
75
+ const targetThreadId = parsed.targetThreadId ?? params.threadId
76
+ const isCrossThreadTarget =
77
+ recordIdToString(targetThreadId, TABLES.THREAD) !== recordIdToString(params.threadId, TABLES.THREAD)
78
+ const result = yield* params.executionPlanService.createPlan({
79
+ organizationId: params.orgId,
80
+ threadId: targetThreadId,
81
+ ...(isCrossThreadTarget ? { sourceThreadId: params.threadId } : {}),
82
+ leadAgentId: params.agentId,
83
+ createdByAgentId: params.agentId,
84
+ requireApproval: parsed.requireApproval ?? isCrossThreadTarget,
85
+ input: draft,
86
+ })
87
+ params.onPlanChanged?.()
88
+ return result
89
+ }
90
+
91
+ case 'create-project': {
92
+ const draft = yield* extractAgentPlanDraftEffect(parsed)
93
+ let createdThreadId: string | null = null
94
+
95
+ const cleanupLeakedThread = (threadId: string) =>
96
+ params.threadService.deleteThread(threadId).pipe(
97
+ Effect.catch((cleanupError: unknown) =>
98
+ Effect.sync(() => {
99
+ serverLogger.warn`Failed to clean up leaked execution-plan project thread ${threadId}: ${cleanupError}`
100
+ }),
101
+ ),
102
+ )
103
+
104
+ const projectEffect = Effect.gen(function* () {
105
+ const projectTitle = parsed.projectTitle
106
+ const targetThreadId = parsed.targetThreadId
107
+ const targetThread = targetThreadId
108
+ ? yield* params.threadService.getThread(targetThreadId)
109
+ : yield* (() => {
110
+ if (!projectTitle) {
111
+ return new BadRequestError({
112
+ message: 'projectTitle is required when action is "create-project".',
113
+ })
114
+ }
115
+
116
+ return params.threadService.createThread({
117
+ userId: params.userId,
118
+ organizationId: params.orgId,
119
+ title: projectTitle,
120
+ type: 'group',
121
+ })
122
+ })()
123
+
124
+ if (!parsed.targetThreadId) {
125
+ createdThreadId = targetThread.id
126
+ }
127
+
128
+ if (targetThread.organizationId !== recordIdToString(params.orgId, TABLES.ORGANIZATION)) {
129
+ return yield* new ForbiddenError({ message: 'Target thread belongs to a different organization.' })
130
+ }
131
+ if (targetThread.userId !== recordIdToString(params.userId, TABLES.USER)) {
132
+ return yield* new ForbiddenError({ message: 'Target thread belongs to a different user.' })
83
133
  }
84
134
 
85
- return resolvedWsService.createThread({
86
- userId: params.userId,
135
+ const existingPlans = yield* params.executionPlanService.getActivePlansForThread(targetThread.id)
136
+ if (targetThread.type !== 'thread' && existingPlans.length > 0) {
137
+ return yield* new ConflictError({
138
+ message:
139
+ 'This thread already has an active execution plan. Use action "replace" or target a core thread.',
140
+ })
141
+ }
142
+
143
+ const created = yield* params.executionPlanService.createPlan({
87
144
  organizationId: params.orgId,
88
- title: parsed.projectTitle,
89
- type: 'group',
145
+ threadId: targetThread.id,
146
+ sourceThreadId: params.threadId,
147
+ leadAgentId: params.agentId,
148
+ createdByAgentId: params.agentId,
149
+ requireApproval: parsed.requireApproval ?? true,
150
+ input: draft,
90
151
  })
91
- })()
92
152
 
93
- if (targetThread.organizationId !== recordIdToString(params.orgId, TABLES.ORGANIZATION)) {
94
- throw new Error('Target thread belongs to a different organization.')
95
- }
96
- if (targetThread.userId !== recordIdToString(params.userId, TABLES.USER)) {
97
- throw new Error('Target thread belongs to a different user.')
98
- }
99
-
100
- const existingPlans = await resolvedEpService.getActivePlansForThread(targetThread.id)
101
- if (targetThread.type !== 'thread' && existingPlans.length > 0) {
102
- throw new Error(
103
- 'This thread already has an active execution plan. Use action "replace" or target a core thread.',
104
- )
105
- }
153
+ const result = {
154
+ ...created,
155
+ runId: created.plan?.runId ?? '',
156
+ threadId: targetThread.id,
157
+ threadTitle: targetThread.title,
158
+ createdThread: createdThreadId !== null,
159
+ } satisfies CreateProjectWithPlanResultData
160
+ params.onPlanChanged?.()
161
+ return result
162
+ }).pipe(Effect.tapError(() => (createdThreadId ? cleanupLeakedThread(createdThreadId) : Effect.void)))
106
163
 
107
- const createdThread = !parsed.targetThreadId
108
- try {
109
- const created = await resolvedEpService.createPlan({
110
- organizationId: params.orgId,
111
- threadId: targetThread.id,
112
- sourceThreadId: params.threadId,
113
- leadAgentId: params.agentId,
114
- createdByAgentId: params.agentId,
115
- requireApproval: parsed.requireApproval ?? true,
116
- input: draft,
117
- })
118
- result = {
119
- ...created,
120
- runId: created.plan?.runId ?? '',
121
- threadId: targetThread.id,
122
- threadTitle: targetThread.title,
123
- createdThread,
124
- } satisfies CreateProjectWithPlanResultData
125
- } catch (error) {
126
- if (createdThread) {
127
- await resolvedWsService.deleteThread(targetThread.id).catch(() => {})
164
+ return yield* projectEffect
128
165
  }
129
- throw error
130
- }
131
- break
132
- }
133
166
 
134
- case 'replace': {
135
- const draft = extractAgentPlanDraft(parsed)
136
- result = await resolvedEpService.replacePlan({
137
- organizationId: params.orgId,
138
- threadId: params.threadId,
139
- leadAgentId: params.agentId,
140
- createdByAgentId: params.agentId,
141
- input: { runId: parsed.runId, reason: parsed.reason, requireApproval: parsed.requireApproval, ...draft },
142
- })
143
- break
144
- }
145
-
146
- case 'resume':
147
- result = await resolvedEpService.resumeRun({
148
- threadId: params.threadId,
149
- emittedBy: params.agentId,
150
- input: { runId: parsed.runId },
151
- })
152
- break
167
+ case 'replace': {
168
+ const draft = yield* extractAgentPlanDraftEffect(parsed)
169
+ const result = yield* params.executionPlanService.replacePlan({
170
+ organizationId: params.orgId,
171
+ threadId: params.threadId,
172
+ leadAgentId: params.agentId,
173
+ createdByAgentId: params.agentId,
174
+ input: {
175
+ runId: parsed.runId,
176
+ reason: parsed.reason,
177
+ requireApproval: parsed.requireApproval,
178
+ ...draft,
179
+ },
180
+ })
181
+ params.onPlanChanged?.()
182
+ return result
183
+ }
153
184
 
154
- case 'update-node':
155
- result = await resolvedEpService.submitNodeResult({
156
- threadId: params.threadId,
157
- emittedBy: params.agentId,
158
- input: {
159
- runId: parsed.runId,
160
- nodeId: parsed.node.id,
161
- notes: parsed.node.latestNotes,
162
- artifacts: parsed.node.deliverables ?? [],
163
- },
164
- })
165
- break
166
- }
185
+ case 'resume': {
186
+ const result = yield* params.executionPlanService.resumeRun({
187
+ threadId: params.threadId,
188
+ emittedBy: params.agentId,
189
+ input: { runId: parsed.runId },
190
+ })
191
+ params.onPlanChanged?.()
192
+ return result
193
+ }
167
194
 
168
- params.onPlanChanged?.()
169
- return result
170
- },
195
+ case 'update-node': {
196
+ const result = yield* params.executionPlanService.submitNodeResult({
197
+ threadId: params.threadId,
198
+ emittedBy: params.agentId,
199
+ input: {
200
+ runId: parsed.runId,
201
+ nodeId: parsed.node.id,
202
+ notes: parsed.node.latestNotes,
203
+ artifacts: parsed.node.deliverables ?? [],
204
+ },
205
+ })
206
+ params.onPlanChanged?.()
207
+ return result
208
+ }
209
+ }
210
+ }).pipe(Effect.withSpan('tool.executionPlan.execute')),
211
+ ),
171
212
  })
172
213
  }
173
214
 
174
- function parseExecutionPlanArgs(input: unknown): ExecutionPlanArgs {
175
- const parsed = ExecutionPlanArgsSchema.parse(input)
176
-
177
- switch (parsed.action) {
178
- case 'create':
179
- return ExecutionPlanCreateArgsSchema.parse(parsed)
180
- case 'create-project':
181
- return ExecutionPlanCreateProjectArgsSchema.parse(parsed)
182
- case 'replace':
183
- return ExecutionPlanReplaceArgsSchema.parse(parsed)
184
- case 'resume':
185
- return ExecutionPlanResumeArgsSchema.parse(parsed)
186
- case 'update-node':
187
- return ExecutionPlanUpdateNodeArgsSchema.parse(parsed)
188
- }
215
+ function parseExecutionPlanArgsEffect(
216
+ input: unknown,
217
+ ): Effect.Effect<ExecutionPlanArgs, ExecutionPlanToolValidationError> {
218
+ return Effect.gen(function* () {
219
+ const parsedArgs = ExecutionPlanArgsSchema.safeParse(input)
220
+ if (!parsedArgs.success) {
221
+ return yield* toValidationError(parsedArgs.error, 'Invalid execution plan tool input')
222
+ }
223
+ const parsed = parsedArgs.data
224
+
225
+ const parseAction = <T>(result: { success: true; data: T } | { success: false; error: unknown }) =>
226
+ result.success
227
+ ? Effect.succeed(result.data)
228
+ : Effect.fail(toValidationError(result.error, 'Invalid execution plan action input'))
229
+
230
+ switch (parsed.action) {
231
+ case 'create':
232
+ return yield* parseAction(ExecutionPlanCreateArgsSchema.safeParse(parsed))
233
+ case 'create-project':
234
+ return yield* parseAction(ExecutionPlanCreateProjectArgsSchema.safeParse(parsed))
235
+ case 'replace':
236
+ return yield* parseAction(ExecutionPlanReplaceArgsSchema.safeParse(parsed))
237
+ case 'resume':
238
+ return yield* parseAction(ExecutionPlanResumeArgsSchema.safeParse(parsed))
239
+ case 'update-node':
240
+ return yield* parseAction(ExecutionPlanUpdateNodeArgsSchema.safeParse(parsed))
241
+ }
242
+ })
189
243
  }
190
244
 
191
- function extractAgentPlanDraft(input: Extract<ExecutionPlanArgs, { action: 'create' | 'create-project' | 'replace' }>) {
192
- return expandAgentPlanDraft(
193
- AgentPlanDraftSchema.parse({
245
+ function extractAgentPlanDraftEffect(
246
+ input: Extract<ExecutionPlanArgs, { action: 'create' | 'create-project' | 'replace' }>,
247
+ ): Effect.Effect<ReturnType<typeof expandAgentPlanDraft>, ExecutionPlanToolValidationError> {
248
+ return Effect.gen(function* () {
249
+ const parsedDraft = AgentPlanDraftSchema.safeParse({
194
250
  title: input.title,
195
251
  objective: input.objective,
196
252
  nodes: input.nodes,
197
253
  edges: input.edges,
198
- }),
199
- )
254
+ })
255
+ if (!parsedDraft.success) {
256
+ return yield* toValidationError(parsedDraft.error, 'Invalid execution plan draft')
257
+ }
258
+ return expandAgentPlanDraft(parsedDraft.data)
259
+ })
200
260
  }
201
261
 
202
- export function createExecutionPlanQueryTool(params: { threadId: RecordIdRef }) {
262
+ export function createExecutionPlanQueryTool(params: {
263
+ threadId: RecordIdRef
264
+ executionPlanService: Pick<ExecutionPlanExecutionPlanService, 'listActivePlanSummaries' | 'getActivePlanToolResult'>
265
+ }) {
203
266
  return tool({
204
267
  description:
205
268
  'Query execution plans. Omit runId to list all active plans. Provide runId to load a specific plan run.',
206
269
  inputSchema: ExecutionPlanQueryArgsSchema,
207
- execute: async (input) => {
208
- if (!input.runId) {
209
- return await executionPlanService.listActivePlanSummaries(params.threadId)
210
- }
211
- return await executionPlanService.getActivePlanToolResult({
212
- threadId: params.threadId,
213
- runId: input.runId,
214
- includeEvents: input.includeEvents,
215
- includeArtifacts: input.includeArtifacts,
216
- includeApprovals: input.includeApprovals,
217
- includeCheckpoints: input.includeCheckpoints,
218
- includeValidationIssues: input.includeValidationIssues,
219
- })
220
- },
270
+ execute: (input) =>
271
+ Effect.runPromise(
272
+ Effect.gen(function* () {
273
+ if (!input.runId) {
274
+ return yield* params.executionPlanService.listActivePlanSummaries(params.threadId)
275
+ }
276
+ return yield* params.executionPlanService.getActivePlanToolResult({
277
+ threadId: params.threadId,
278
+ runId: input.runId,
279
+ includeEvents: input.includeEvents,
280
+ includeArtifacts: input.includeArtifacts,
281
+ includeApprovals: input.includeApprovals,
282
+ includeCheckpoints: input.includeCheckpoints,
283
+ includeValidationIssues: input.includeValidationIssues,
284
+ })
285
+ }).pipe(Effect.withSpan('tool.executionPlanQuery.execute')),
286
+ ),
221
287
  })
222
288
  }
223
289
 
224
290
  export function createSubmitExecutionNodeResultTool(params: {
225
291
  threadId: RecordIdRef
226
292
  agentId: string
293
+ executionPlanService: Pick<ExecutionPlanExecutionPlanService, 'submitNodeResult'>
227
294
  onPlanChanged?: () => void
228
295
  }) {
229
296
  return tool({
230
297
  description:
231
298
  'Submit the result for the currently running execution node. The executor validates outputs, artifacts, and completion checks before advancing the run.',
232
299
  inputSchema: SubmitExecutionNodeResultArgsSchema,
233
- execute: async (input) => {
234
- const result = await executionPlanService.submitNodeResult({
235
- threadId: params.threadId,
236
- emittedBy: params.agentId,
237
- input,
238
- })
239
- params.onPlanChanged?.()
240
- return result
241
- },
300
+ execute: (input) =>
301
+ Effect.runPromise(
302
+ Effect.gen(function* () {
303
+ const result = yield* params.executionPlanService.submitNodeResult({
304
+ threadId: params.threadId,
305
+ emittedBy: params.agentId,
306
+ input,
307
+ })
308
+ params.onPlanChanged?.()
309
+ return result
310
+ }).pipe(Effect.withSpan('tool.submitExecutionNodeResult.execute')),
311
+ ),
242
312
  toModelOutput: ({ output }) => {
243
313
  const result = getLatestExecutionPlanResult(output)
244
314
  const summary = result?.message?.trim()
@@ -1,24 +1,26 @@
1
1
  import { tool } from 'ai'
2
+ import { Effect, Schema } from 'effect'
2
3
  import { z } from 'zod'
3
4
 
4
5
  import type { ToolDefinition } from '../ai/definitions'
5
6
  import { withTimeout } from '../utils/async'
7
+ import { nowIsoDateTimeString } from '../utils/date-time'
6
8
  import { readStringField, truncateOptionalText } from '../utils/string'
7
9
  import { getFirecrawlClient } from './firecrawl-client'
8
10
  import type { WebCitation } from './tool-contracts'
11
+ import { toRecord, WEB_TOOL_TIMEOUT_MS } from './web-tool-shared'
12
+
13
+ class FetchWebpageToolError extends Schema.TaggedErrorClass<FetchWebpageToolError>()(
14
+ '@lota-sdk/core/FetchWebpageToolError',
15
+ { message: Schema.String, cause: Schema.optional(Schema.Defect) },
16
+ ) {}
9
17
 
10
- const TOOL_TIMEOUT_MS = 30_000
11
18
  const FormatSchema = z.enum(['markdown', 'html', 'rawHtml', 'links', 'images', 'screenshot', 'summary'])
12
19
  const MAX_MARKDOWN_CHARS = 6_000
13
20
  const MAX_SUMMARY_CHARS = 1_200
14
21
  const MAX_LINKS = 25
15
22
  const MAX_IMAGES = 10
16
23
 
17
- function toRecord(value: unknown): Record<string, unknown> | null {
18
- if (!value || typeof value !== 'object' || Array.isArray(value)) return null
19
- return value as Record<string, unknown>
20
- }
21
-
22
24
  function readStringList(record: Record<string, unknown>, key: string, maxItems: number): string[] {
23
25
  const value = record[key]
24
26
  if (!Array.isArray(value)) return []
@@ -91,7 +93,7 @@ function buildFetchCitations(url: string, document: unknown): WebCitation[] {
91
93
  }
92
94
  }
93
95
 
94
- return [{ source: 'web', sourceId, retrievedAt: new Date().toISOString() }]
96
+ return [{ source: 'web', sourceId, retrievedAt: nowIsoDateTimeString() }]
95
97
  }
96
98
 
97
99
  export const fetchWebpageTool = {
@@ -107,7 +109,7 @@ export const fetchWebpageTool = {
107
109
  maxAge: z.number().int().min(1).optional(),
108
110
  })
109
111
  .strict(),
110
- execute: async ({
112
+ execute: ({
111
113
  url,
112
114
  formats,
113
115
  onlyMainContent,
@@ -118,17 +120,26 @@ export const fetchWebpageTool = {
118
120
  onlyMainContent?: boolean
119
121
  maxAge?: number
120
122
  }) => {
121
- const result = await withTimeout(
122
- getFirecrawlClient().scrape(url, {
123
- formats: formats?.length ? formats : ['markdown'],
124
- onlyMainContent: onlyMainContent ?? true,
125
- maxAge,
126
- }),
127
- TOOL_TIMEOUT_MS,
128
- 'Webpage fetch',
123
+ const firecrawl = getFirecrawlClient()
124
+
125
+ return Effect.runPromise(
126
+ Effect.gen(function* () {
127
+ const result = yield* Effect.tryPromise({
128
+ try: () =>
129
+ withTimeout(
130
+ firecrawl.scrape(url, {
131
+ formats: formats?.length ? formats : ['markdown'],
132
+ onlyMainContent: onlyMainContent ?? true,
133
+ maxAge,
134
+ }),
135
+ WEB_TOOL_TIMEOUT_MS,
136
+ 'Webpage fetch',
137
+ ),
138
+ catch: (cause) => new FetchWebpageToolError({ message: `Webpage fetch failed for ${url}.`, cause }),
139
+ })
140
+ return { url, document: summarizeDocument(url, result), citations: buildFetchCitations(url, result) }
141
+ }).pipe(Effect.withSpan('tool.fetchWebpage.execute')),
129
142
  )
130
-
131
- return { url, document: summarizeDocument(url, result), citations: buildFetchCitations(url, result) }
132
143
  },
133
144
  }),
134
145
  } as const satisfies ToolDefinition<void>
@@ -1,12 +1,32 @@
1
1
  import Firecrawl from '@mendable/firecrawl-js'
2
+ import { Context, Effect, Layer } from 'effect'
2
3
 
3
- import { getRuntimeConfig } from '../runtime/runtime-config'
4
+ import { resolveLotaService } from '../effect/runtime'
5
+ import { RuntimeConfigServiceTag } from '../effect/services'
4
6
 
5
- let _firecrawlClient: Firecrawl | undefined
7
+ export class FirecrawlTag extends Context.Service<FirecrawlTag, Firecrawl>()('@lota-sdk/core/Firecrawl') {}
8
+
9
+ export const FirecrawlLive = Layer.effect(
10
+ FirecrawlTag,
11
+ Effect.gen(function* () {
12
+ const config = yield* RuntimeConfigServiceTag
13
+ return new Firecrawl({
14
+ apiKey: config.firecrawl.apiKey,
15
+ ...(config.firecrawl.apiBaseUrl ? { apiUrl: config.firecrawl.apiBaseUrl } : {}),
16
+ })
17
+ }),
18
+ )
19
+
20
+ let currentFirecrawlClient: Firecrawl | null = null
21
+
22
+ export function configureFirecrawlClient(client: Firecrawl): void {
23
+ currentFirecrawlClient = client
24
+ }
25
+
26
+ export function clearFirecrawlClient(): void {
27
+ currentFirecrawlClient = null
28
+ }
6
29
 
7
30
  export function getFirecrawlClient(): Firecrawl {
8
- if (!_firecrawlClient) {
9
- _firecrawlClient = new Firecrawl({ apiKey: getRuntimeConfig().firecrawl.apiKey })
10
- }
11
- return _firecrawlClient
31
+ return currentFirecrawlClient ?? resolveLotaService(FirecrawlTag)
12
32
  }
@@ -1,5 +1,6 @@
1
1
  export * from './execution-plan.tool'
2
2
  export * from './fetch-webpage.tool'
3
+ export * from './firecrawl-client'
3
4
  export * from './memory-block.tool'
4
5
  export * from './plan-approval.tool'
5
6
  export * from './read-file-parts.tool'
@@ -1,4 +1,5 @@
1
1
  import { tool } from 'ai'
2
+ import { Effect } from 'effect'
2
3
  import { z } from 'zod'
3
4
 
4
5
  import type { RecordIdRef } from '../db/record-id'
@@ -7,18 +8,22 @@ import {
7
8
  normalizeMemoryBlockEntry,
8
9
  prepareMemoryBlockAppend,
9
10
  validateMemoryBlockEntry,
10
- } from '../runtime/memory-block'
11
- import { threadService } from '../services/thread.service'
11
+ } from '../runtime/memory/memory-block'
12
+ import type { makeThreadService } from '../services/thread/thread.service'
12
13
  import { safeEnqueue } from '../utils/async'
13
14
 
15
+ type MemoryBlockThreadService = Pick<ReturnType<typeof makeThreadService>, 'appendMemoryBlock'>
16
+
14
17
  export function createMemoryBlockTool({
15
18
  threadId,
16
19
  agentLabel,
20
+ threadService,
17
21
  getCurrentBlock,
18
22
  onAppend,
19
23
  }: {
20
24
  threadId: RecordIdRef
21
25
  agentLabel: string
26
+ threadService: MemoryBlockThreadService
22
27
  getCurrentBlock?: () => string
23
28
  onAppend?: (value: string) => void
24
29
  }) {
@@ -42,10 +47,13 @@ export function createMemoryBlockTool({
42
47
  onAppend?.(prepared.optimisticBlock)
43
48
 
44
49
  void safeEnqueue(
45
- async () => {
46
- const updated = await threadService.appendMemoryBlock(threadId, prepared.formatted)
47
- onAppend?.(updated)
48
- },
50
+ () =>
51
+ Effect.runPromise(
52
+ Effect.gen(function* () {
53
+ const updated = yield* threadService.appendMemoryBlock(threadId, prepared.formatted)
54
+ onAppend?.(updated)
55
+ }),
56
+ ),
49
57
  { operationName: 'append memory block entry', logPrefix: 'Background memoryBlockAppend task failed' },
50
58
  )
51
59