@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,307 @@
1
+ import type {
2
+ PlanFailureAction,
3
+ PlanFailureClass,
4
+ PlanNodeResultSubmission,
5
+ PlanNodeRunRecord,
6
+ PlanNodeSpecRecord,
7
+ PlanSpecRecord,
8
+ } from '@lota-sdk/shared'
9
+ import {
10
+ HUMAN_NODE_TYPES as HUMAN_NODE_TYPE_VALUES,
11
+ STRUCTURAL_NODE_TYPES as STRUCTURAL_NODE_TYPE_VALUES,
12
+ isSafePath,
13
+ isSafePathOrRoot,
14
+ } from '@lota-sdk/shared'
15
+ import { Effect, Schema } from 'effect'
16
+
17
+ import type { RecordIdInput } from '../../db/record-id'
18
+ import { ensureRecordId } from '../../db/record-id'
19
+ import { TABLES } from '../../db/tables'
20
+ import { ValidationError } from '../../effect/errors'
21
+ import { toDatabaseDateTime } from '../../utils/date-time'
22
+ import { createNullProtoRecord, setPathValue } from '../../utils/null-proto-record'
23
+ import { isExecutableConditionExpression, readPathValue } from './plan-helpers'
24
+
25
+ const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skipped', 'scheduled', 'monitoring'])
26
+ const HUMAN_NODE_TYPE_SET = new Set<string>(HUMAN_NODE_TYPE_VALUES)
27
+ const STRUCTURAL_NODE_TYPE_SET = new Set<string>(STRUCTURAL_NODE_TYPE_VALUES)
28
+ const decodeJsonLiteralEffect = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)
29
+
30
+ function parseLiteralValue(raw: string): Effect.Effect<unknown> {
31
+ const trimmed = raw.trim()
32
+ if (!trimmed.length) return Effect.void
33
+ if (trimmed === 'true') return Effect.succeed(true)
34
+ if (trimmed === 'false') return Effect.succeed(false)
35
+ if (trimmed === 'null') return Effect.succeed(null)
36
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Effect.succeed(Number(trimmed))
37
+ if (
38
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
39
+ (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
40
+ (trimmed.startsWith('`') && trimmed.endsWith('`'))
41
+ ) {
42
+ return Effect.succeed(trimmed.slice(1, -1))
43
+ }
44
+
45
+ return decodeJsonLiteralEffect(trimmed).pipe(Effect.catch(() => Effect.succeed(trimmed)))
46
+ }
47
+
48
+ function buildArtifactContext(artifacts: Array<{ name: string; kind: string; payload?: unknown }>) {
49
+ return Object.fromEntries(
50
+ artifacts.map((artifact) => [artifact.name, { kind: artifact.kind, payload: artifact.payload }]),
51
+ )
52
+ }
53
+
54
+ export function buildNodeContext(params: {
55
+ nodeRun:
56
+ | { resolvedInput?: Record<string, unknown> | null; latestStructuredOutput?: Record<string, unknown> | null }
57
+ | undefined
58
+ artifacts: Array<{ name: string; kind: string; payload?: unknown }>
59
+ }) {
60
+ return {
61
+ input: params.nodeRun?.resolvedInput ?? {},
62
+ output: params.nodeRun?.latestStructuredOutput ?? {},
63
+ artifact: buildArtifactContext(params.artifacts),
64
+ }
65
+ }
66
+
67
+ export function buildPublishedArtifactContent(params: {
68
+ artifact: PlanNodeResultSubmission['artifacts'][number]
69
+ notes: string
70
+ }): string {
71
+ const { artifact, notes } = params
72
+ if (artifact.content?.trim()) {
73
+ return artifact.content
74
+ }
75
+
76
+ if (artifact.payload !== undefined) {
77
+ return `# ${artifact.name}\n\n\`\`\`json\n${JSON.stringify(artifact.payload, null, 2)}\n\`\`\``
78
+ }
79
+
80
+ return `# ${artifact.name}\n\n${notes.trim()}`
81
+ }
82
+
83
+ export function evaluateCondition(
84
+ expression: string | undefined,
85
+ context: Record<string, unknown>,
86
+ ): Effect.Effect<boolean> {
87
+ if (!expression?.trim()) return Effect.succeed(true)
88
+ const normalized = expression.trim()
89
+ if (normalized === 'always') return Effect.succeed(true)
90
+
91
+ const match = normalized.match(/^([a-zA-Z0-9_.]+)\s*(==|!=|>=|<=|>|<)\s*(.+)$/)
92
+ if (!match) {
93
+ if (!isExecutableConditionExpression(normalized)) {
94
+ return Effect.succeed(true)
95
+ }
96
+ return Effect.succeed(Boolean(readPathValue(context, normalized)))
97
+ }
98
+
99
+ return Effect.gen(function* () {
100
+ const [, leftPath, operator, rawRightValue] = match
101
+ const left = readPathValue(context, leftPath)
102
+ const right = yield* parseLiteralValue(rawRightValue)
103
+
104
+ if (operator === '==') return Object.is(left, right)
105
+ if (operator === '!=') return !Object.is(left, right)
106
+ if (typeof left !== 'number' || typeof right !== 'number') return false
107
+ if (operator === '>=') return left >= right
108
+ if (operator === '<=') return left <= right
109
+ if (operator === '>') return left > right
110
+ return left < right
111
+ })
112
+ }
113
+
114
+ export function isSuccessfulTerminalStatus(status: string): boolean {
115
+ return SUCCESSFUL_TERMINAL_NODE_STATUSES.has(status)
116
+ }
117
+
118
+ export function isHumanNodeType(type: string): boolean {
119
+ return HUMAN_NODE_TYPE_SET.has(type)
120
+ }
121
+
122
+ export function isStructuralNodeType(type: string): boolean {
123
+ return STRUCTURAL_NODE_TYPE_SET.has(type)
124
+ }
125
+
126
+ type PlanNodeRunUpdate = Omit<
127
+ Partial<PlanNodeRunRecord>,
128
+ | 'blockedReason'
129
+ | 'failureClass'
130
+ | 'resolvedInput'
131
+ | 'latestStructuredOutput'
132
+ | 'latestNotes'
133
+ | 'latestAttemptId'
134
+ | 'scheduledAt'
135
+ | 'readyAt'
136
+ | 'startedAt'
137
+ | 'completedAt'
138
+ > & {
139
+ blockedReason?: string | null
140
+ failureClass?: PlanFailureClass | null
141
+ resolvedInput?: Record<string, unknown> | null
142
+ latestStructuredOutput?: Record<string, unknown> | null
143
+ latestNotes?: string | null
144
+ latestAttemptId?: RecordIdInput | null
145
+ scheduledAt?: string | Date | null
146
+ readyAt?: string | Date | null
147
+ startedAt?: string | Date | null
148
+ completedAt?: string | Date | null
149
+ }
150
+
151
+ function patchField<K extends string, V>(
152
+ key: K,
153
+ patch: V | null | undefined,
154
+ existing: V | undefined,
155
+ ): Partial<Record<K, V>> {
156
+ if (patch === null) return {}
157
+ if (patch !== undefined) return { [key]: patch } as Partial<Record<K, V>>
158
+ if (existing !== undefined) return { [key]: existing } as Partial<Record<K, V>>
159
+ return {}
160
+ }
161
+
162
+ export function toNodeRunData(nodeRun: PlanNodeRunRecord, patch: PlanNodeRunUpdate) {
163
+ const patchedAttemptId =
164
+ patch.latestAttemptId === null
165
+ ? null
166
+ : patch.latestAttemptId !== undefined
167
+ ? ensureRecordId(patch.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT)
168
+ : undefined
169
+ const existingAttemptId =
170
+ nodeRun.latestAttemptId !== undefined
171
+ ? ensureRecordId(nodeRun.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT)
172
+ : undefined
173
+
174
+ const patchedScheduledAt =
175
+ patch.scheduledAt === null
176
+ ? null
177
+ : patch.scheduledAt !== undefined
178
+ ? toDatabaseDateTime(patch.scheduledAt)
179
+ : undefined
180
+ const existingScheduledAt = nodeRun.scheduledAt !== undefined ? toDatabaseDateTime(nodeRun.scheduledAt) : undefined
181
+
182
+ const patchedReadyAt =
183
+ patch.readyAt === null ? null : patch.readyAt !== undefined ? toDatabaseDateTime(patch.readyAt) : undefined
184
+ const existingReadyAt = nodeRun.readyAt !== undefined ? toDatabaseDateTime(nodeRun.readyAt) : undefined
185
+
186
+ const patchedStartedAt =
187
+ patch.startedAt === null ? null : patch.startedAt !== undefined ? toDatabaseDateTime(patch.startedAt) : undefined
188
+ const existingStartedAt = nodeRun.startedAt !== undefined ? toDatabaseDateTime(nodeRun.startedAt) : undefined
189
+
190
+ const patchedCompletedAt =
191
+ patch.completedAt === null
192
+ ? null
193
+ : patch.completedAt !== undefined
194
+ ? toDatabaseDateTime(patch.completedAt)
195
+ : undefined
196
+ const existingCompletedAt = nodeRun.completedAt !== undefined ? toDatabaseDateTime(nodeRun.completedAt) : undefined
197
+
198
+ return {
199
+ runId: ensureRecordId(nodeRun.runId, TABLES.PLAN_RUN),
200
+ planSpecId: ensureRecordId(nodeRun.planSpecId, TABLES.PLAN_SPEC),
201
+ nodeId: nodeRun.nodeId,
202
+ status: patch.status ?? nodeRun.status,
203
+ attemptCount: patch.attemptCount ?? nodeRun.attemptCount,
204
+ retryCount: patch.retryCount ?? nodeRun.retryCount,
205
+ ...patchField('resolvedInput', patch.resolvedInput, nodeRun.resolvedInput ?? undefined),
206
+ ...patchField('latestStructuredOutput', patch.latestStructuredOutput, nodeRun.latestStructuredOutput ?? undefined),
207
+ ...patchField('latestNotes', patch.latestNotes, nodeRun.latestNotes ?? undefined),
208
+ ...patchField('latestAttemptId', patchedAttemptId, existingAttemptId),
209
+ ...patchField('blockedReason', patch.blockedReason, nodeRun.blockedReason ?? undefined),
210
+ ...patchField('failureClass', patch.failureClass, nodeRun.failureClass ?? undefined),
211
+ ...patchField('scheduledAt', patchedScheduledAt, existingScheduledAt),
212
+ ...patchField('readyAt', patchedReadyAt, existingReadyAt),
213
+ ...patchField('startedAt', patchedStartedAt, existingStartedAt),
214
+ ...patchField('completedAt', patchedCompletedAt, existingCompletedAt),
215
+ }
216
+ }
217
+
218
+ export interface HumanNodeResponsePayload {
219
+ approved?: boolean
220
+ comments?: string
221
+ reason?: string
222
+ requiredEdits?: readonly string[]
223
+ responseText?: string
224
+ messageId?: string
225
+ [key: string]: unknown
226
+ }
227
+
228
+ export function readHumanResponseComments(response: HumanNodeResponsePayload): string | undefined {
229
+ const comments = response.comments
230
+ if (typeof comments === 'string' && comments.trim().length > 0) {
231
+ return comments
232
+ }
233
+
234
+ const reason = response.reason
235
+ return typeof reason === 'string' && reason.trim().length > 0 ? reason : undefined
236
+ }
237
+
238
+ export function readHumanRequiredEdits(response: HumanNodeResponsePayload): string[] | undefined {
239
+ if (!Array.isArray(response.requiredEdits)) {
240
+ return undefined
241
+ }
242
+
243
+ const requiredEdits = response.requiredEdits.filter(
244
+ (entry): entry is string => typeof entry === 'string' && entry.trim().length > 0,
245
+ )
246
+
247
+ return requiredEdits.length > 0 ? requiredEdits : undefined
248
+ }
249
+
250
+ export function deriveApprovalStatus(
251
+ response: HumanNodeResponsePayload,
252
+ ): 'approved' | 'rejected' | 'changes-requested' {
253
+ const approved = response.approved === true
254
+ const requiredEdits = readHumanRequiredEdits(response) ?? []
255
+
256
+ if (approved && requiredEdits.length === 0) return 'approved'
257
+ if (requiredEdits.length > 0) return 'changes-requested'
258
+ return 'rejected'
259
+ }
260
+
261
+ export function resolveFailureAction(
262
+ nodeSpec: Pick<PlanNodeSpecRecord, 'failurePolicy'>,
263
+ failureClass: PlanFailureClass | null,
264
+ ): PlanFailureAction {
265
+ if (!failureClass) return 'abort'
266
+
267
+ const matchedRule = nodeSpec.failurePolicy.find((rule) => rule.on === failureClass)
268
+ return matchedRule?.action ?? 'abort'
269
+ }
270
+
271
+ export function buildResolvedInput(params: {
272
+ spec: Pick<PlanSpecRecord, 'edges'>
273
+ nodeSpec: Pick<PlanNodeSpecRecord, 'nodeId'>
274
+ nodeRunsById: Map<string, Pick<PlanNodeRunRecord, 'status' | 'resolvedInput' | 'latestStructuredOutput'>>
275
+ artifactsByNodeId: Map<string, Array<{ name: string; kind: string; payload?: unknown }>>
276
+ }): Effect.Effect<Record<string, unknown>, ValidationError> {
277
+ return Effect.gen(function* () {
278
+ const resolvedInput: Record<string, unknown> = createNullProtoRecord()
279
+
280
+ for (const edge of params.spec.edges.filter((candidate) => candidate.target === params.nodeSpec.nodeId)) {
281
+ const sourceRun = params.nodeRunsById.get(edge.source)
282
+ if (!sourceRun || !isSuccessfulTerminalStatus(sourceRun.status)) continue
283
+
284
+ const context = buildNodeContext({
285
+ nodeRun: sourceRun,
286
+ artifacts: params.artifactsByNodeId.get(edge.source) ?? [],
287
+ })
288
+ const matches = yield* evaluateCondition(edge.when, context)
289
+ if (!matches) continue
290
+
291
+ for (const [targetPath, sourcePath] of Object.entries(edge.map)) {
292
+ if (!isSafePath(targetPath)) {
293
+ return yield* new ValidationError({ message: `Unsafe edge target path "${targetPath}" in edge "${edge.id}"` })
294
+ }
295
+ if (!isSafePathOrRoot(sourcePath)) {
296
+ return yield* new ValidationError({ message: `Unsafe edge source path "${sourcePath}" in edge "${edge.id}"` })
297
+ }
298
+ const value = readPathValue(context, sourcePath)
299
+ if (value !== undefined) {
300
+ setPathValue(resolvedInput, targetPath, value)
301
+ }
302
+ }
303
+ }
304
+
305
+ return resolvedInput
306
+ })
307
+ }
@@ -0,0 +1,209 @@
1
+ import type {
2
+ PlanEventRecord,
3
+ PlanEventType,
4
+ PlanFailureClass,
5
+ PlanNodeResultSubmission,
6
+ PlanNodeRunRecord,
7
+ PlanRunRecord,
8
+ PlanSpecRecord,
9
+ PlanValidationIssueRecord,
10
+ } from '@lota-sdk/shared'
11
+ import { PlanEventSchema, PlanNodeAttemptSchema, PlanRunSchema, PlanValidationIssueSchema } from '@lota-sdk/shared'
12
+ import { Effect } from 'effect'
13
+ import { RecordId } from 'surrealdb'
14
+
15
+ import type { RecordIdInput } from '../../db/record-id'
16
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
17
+ import type { DatabaseTransaction } from '../../db/service'
18
+ import { TABLES } from '../../db/tables'
19
+ import { DatabaseError } from '../../effect/errors'
20
+ import type { makePlanCheckpointService } from './plan-checkpoint.service'
21
+ import type { PlanRunUpdate } from './plan-run-data'
22
+ import { toRunData } from './plan-run-data'
23
+ import type { PlanValidationIssueInput } from './plan-validator.service'
24
+
25
+ export function createAttempt(params: {
26
+ tx: DatabaseTransaction
27
+ run: PlanRunRecord
28
+ nodeRun: PlanNodeRunRecord
29
+ emittedBy: string
30
+ result: PlanNodeResultSubmission
31
+ status: 'completed' | 'failed'
32
+ failureClass: PlanFailureClass | null
33
+ }) {
34
+ return Effect.gen(function* () {
35
+ const attemptId = new RecordId(TABLES.PLAN_NODE_ATTEMPT, Bun.randomUUIDv7())
36
+ const created = yield* params.tx
37
+ .create(attemptId)
38
+ .content({
39
+ runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
40
+ nodeRunId: ensureRecordId(params.nodeRun.id, TABLES.PLAN_NODE_RUN),
41
+ nodeId: params.nodeRun.nodeId,
42
+ emittedBy: params.emittedBy,
43
+ status: params.status,
44
+ ...(params.result.structuredOutput ? { structuredOutput: params.result.structuredOutput } : {}),
45
+ ...(params.result.notes ? { notes: params.result.notes } : {}),
46
+ validationIssueIds: [],
47
+ ...(params.failureClass ? { failureClass: params.failureClass } : {}),
48
+ })
49
+ .output('after')
50
+ return PlanNodeAttemptSchema.parse(created)
51
+ })
52
+ }
53
+
54
+ export function persistValidationIssues(params: {
55
+ tx: DatabaseTransaction
56
+ run: PlanRunRecord
57
+ spec: PlanSpecRecord
58
+ attemptId: RecordIdInput
59
+ nodeId: string
60
+ issues: PlanValidationIssueInput[]
61
+ }) {
62
+ return Effect.gen(function* () {
63
+ const records: PlanValidationIssueRecord[] = []
64
+ yield* Effect.forEach(params.issues, (issue) =>
65
+ Effect.gen(function* () {
66
+ const issueId = new RecordId(TABLES.PLAN_VALIDATION_ISSUE, Bun.randomUUIDv7())
67
+ const created = yield* params.tx
68
+ .create(issueId)
69
+ .content({
70
+ planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
71
+ runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
72
+ nodeId: issue.nodeId ?? params.nodeId,
73
+ attemptId: ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT),
74
+ severity: issue.severity,
75
+ code: issue.code,
76
+ message: issue.message,
77
+ ...(issue.detail ? { detail: issue.detail } : {}),
78
+ })
79
+ .output('after')
80
+ const parsed = PlanValidationIssueSchema.parse(created)
81
+ records.push(parsed)
82
+ return parsed
83
+ }),
84
+ )
85
+
86
+ return records
87
+ })
88
+ }
89
+
90
+ export function emitEvent(params: {
91
+ tx: DatabaseTransaction
92
+ run: PlanRunRecord
93
+ spec: PlanSpecRecord
94
+ eventType: PlanEventType
95
+ message: string
96
+ emittedBy: string
97
+ nodeId?: string
98
+ attemptId?: RecordIdInput
99
+ approvalId?: RecordIdInput
100
+ fromStatus?: string
101
+ toStatus?: string
102
+ detail?: Record<string, unknown>
103
+ capturedEvents?: PlanEventRecord[]
104
+ }) {
105
+ return Effect.gen(function* () {
106
+ const eventId = new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7())
107
+ const created = yield* params.tx
108
+ .create(eventId)
109
+ .content({
110
+ planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
111
+ runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
112
+ eventType: params.eventType,
113
+ message: params.message,
114
+ emittedBy: params.emittedBy,
115
+ ...(params.nodeId ? { nodeId: params.nodeId } : {}),
116
+ ...(params.attemptId ? { attemptId: ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT) } : {}),
117
+ ...(params.approvalId ? { approvalId: ensureRecordId(params.approvalId, TABLES.PLAN_APPROVAL) } : {}),
118
+ ...(params.fromStatus ? { fromStatus: params.fromStatus } : {}),
119
+ ...(params.toStatus ? { toStatus: params.toStatus } : {}),
120
+ ...(params.detail ? { detail: params.detail } : {}),
121
+ })
122
+ .output('after')
123
+ const event = PlanEventSchema.parse(created)
124
+ params.capturedEvents?.push(event)
125
+ return event
126
+ })
127
+ }
128
+
129
+ export function saveCheckpoint(params: {
130
+ tx: DatabaseTransaction
131
+ run: PlanRunRecord
132
+ spec: PlanSpecRecord
133
+ nodeRuns: PlanNodeRunRecord[]
134
+ artifacts: Array<{ id: RecordIdInput; nodeId: string }>
135
+ sequence: number
136
+ reason: string
137
+ includeWorkspace?: boolean
138
+ capturedEvents?: PlanEventRecord[]
139
+ planCheckpointService: ReturnType<typeof makePlanCheckpointService>
140
+ }) {
141
+ return Effect.gen(function* () {
142
+ const checkpoint = yield* params.planCheckpointService.createCheckpoint({
143
+ tx: params.tx,
144
+ runId: params.run.id,
145
+ sequence: params.sequence,
146
+ runStatus: params.run.status,
147
+ readyNodeIds: params.run.readyNodeIds,
148
+ activeNodeIds: params.run.currentNodeId ? [params.run.currentNodeId] : [],
149
+ artifactIds: params.artifacts.map((artifact) => artifact.id),
150
+ lastCompletedNodeIds: params.nodeRuns
151
+ .filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
152
+ .map((nodeRun) => nodeRun.nodeId),
153
+ snapshot: {
154
+ reason: params.reason,
155
+ runStatus: params.run.status,
156
+ currentNodeId: params.run.currentNodeId,
157
+ waitingNodeId: params.run.waitingNodeId,
158
+ readyNodeIds: params.run.readyNodeIds,
159
+ nodeStatuses: Object.fromEntries(params.nodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun.status])),
160
+ },
161
+ includeWorkspace: params.includeWorkspace,
162
+ })
163
+
164
+ yield* emitEvent({
165
+ tx: params.tx,
166
+ run: params.run,
167
+ spec: params.spec,
168
+ eventType: 'checkpoint-saved',
169
+ message: `Saved checkpoint ${checkpoint.sequence}.`,
170
+ detail: { checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT), reason: params.reason },
171
+ emittedBy: 'system',
172
+ capturedEvents: params.capturedEvents,
173
+ })
174
+
175
+ return checkpoint
176
+ })
177
+ }
178
+
179
+ export function attachCheckpoint(tx: DatabaseTransaction, run: PlanRunRecord, checkpoint: { id: RecordIdInput }) {
180
+ return Effect.asVoid(
181
+ tx
182
+ .update(ensureRecordId(run.id, TABLES.PLAN_RUN))
183
+ .content(toRunData(run, { lastCheckpointId: checkpoint.id }))
184
+ .output('after'),
185
+ )
186
+ }
187
+
188
+ export function replaceRun(
189
+ tx: DatabaseTransaction,
190
+ run: PlanRunRecord,
191
+ patch: PlanRunUpdate,
192
+ ): Effect.Effect<PlanRunRecord, DatabaseError, never> {
193
+ return Effect.gen(function* () {
194
+ const updated = yield* tx
195
+ .update(ensureRecordId(run.id, TABLES.PLAN_RUN))
196
+ .content(toRunData(run, patch))
197
+ .output('after')
198
+ .pipe(
199
+ Effect.mapError(
200
+ (cause) =>
201
+ new DatabaseError({
202
+ message: `Failed to update plan run ${recordIdToString(run.id, TABLES.PLAN_RUN)}.`,
203
+ cause,
204
+ }),
205
+ ),
206
+ )
207
+ return PlanRunSchema.parse(updated)
208
+ })
209
+ }