@lota-sdk/core 0.4.8 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/thread-defaults.ts +33 -21
  9. package/src/create-runtime.ts +725 -387
  10. package/src/db/base.service.ts +52 -28
  11. package/src/db/cursor-pagination.ts +71 -30
  12. package/src/db/memory-store.helpers.ts +4 -7
  13. package/src/db/memory-store.ts +856 -598
  14. package/src/db/memory.ts +398 -275
  15. package/src/db/record-id.ts +32 -10
  16. package/src/db/schema-fingerprint.ts +30 -12
  17. package/src/db/service-normalization.ts +255 -0
  18. package/src/db/service.ts +726 -761
  19. package/src/db/startup.ts +140 -66
  20. package/src/db/transaction-conflict.ts +15 -0
  21. package/src/effect/awaitable-effect.ts +87 -0
  22. package/src/effect/errors.ts +121 -0
  23. package/src/effect/helpers.ts +98 -0
  24. package/src/effect/index.ts +22 -0
  25. package/src/effect/layers.ts +228 -0
  26. package/src/effect/runtime-ref.ts +25 -0
  27. package/src/effect/runtime.ts +31 -0
  28. package/src/effect/services.ts +57 -0
  29. package/src/effect/zod.ts +43 -0
  30. package/src/embeddings/provider.ts +122 -76
  31. package/src/index.ts +46 -1
  32. package/src/openrouter/direct-provider.ts +11 -35
  33. package/src/queues/autonomous-job.queue.ts +130 -74
  34. package/src/queues/context-compaction.queue.ts +60 -15
  35. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  36. package/src/queues/document-processor.queue.ts +52 -77
  37. package/src/queues/memory-consolidation.queue.ts +47 -32
  38. package/src/queues/organization-learning.queue.ts +13 -4
  39. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  40. package/src/queues/plan-scheduler.queue.ts +107 -31
  41. package/src/queues/post-chat-memory.queue.ts +66 -24
  42. package/src/queues/queue-factory.ts +142 -52
  43. package/src/queues/standalone-worker.ts +39 -0
  44. package/src/queues/title-generation.queue.ts +54 -9
  45. package/src/redis/connection.ts +84 -32
  46. package/src/redis/index.ts +6 -8
  47. package/src/redis/org-memory-lock.ts +60 -27
  48. package/src/redis/redis-lease-lock.ts +200 -121
  49. package/src/redis/runtime-connection.ts +10 -0
  50. package/src/redis/stream-context.ts +84 -46
  51. package/src/runtime/agent-identity-overrides.ts +2 -2
  52. package/src/runtime/agent-runtime-policy.ts +4 -1
  53. package/src/runtime/agent-stream-helpers.ts +20 -9
  54. package/src/runtime/chat-run-orchestration.ts +102 -19
  55. package/src/runtime/chat-run-registry.ts +36 -2
  56. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  57. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  58. package/src/runtime/execution-plan-visibility.ts +2 -2
  59. package/src/runtime/execution-plan.ts +42 -15
  60. package/src/runtime/graph-designer.ts +11 -7
  61. package/src/runtime/helper-model.ts +135 -48
  62. package/src/runtime/index.ts +7 -7
  63. package/src/runtime/indexed-repositories-policy.ts +3 -3
  64. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  65. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  66. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  67. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  68. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  69. package/src/runtime/plugin-resolution.ts +144 -24
  70. package/src/runtime/plugin-types.ts +9 -1
  71. package/src/runtime/post-turn-side-effects.ts +197 -130
  72. package/src/runtime/retrieval-adapters.ts +38 -4
  73. package/src/runtime/runtime-config.ts +150 -61
  74. package/src/runtime/runtime-extensions.ts +21 -34
  75. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  76. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  77. package/src/runtime/social-chat/social-chat.ts +594 -0
  78. package/src/runtime/specialist-runner.ts +36 -10
  79. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  80. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  81. package/src/runtime/thread-chat-helpers.ts +2 -2
  82. package/src/runtime/thread-plan-turn.ts +2 -1
  83. package/src/runtime/thread-turn-context.ts +172 -94
  84. package/src/runtime/turn-lifecycle.ts +93 -27
  85. package/src/services/agent-activity.service.ts +287 -203
  86. package/src/services/agent-executor.service.ts +329 -217
  87. package/src/services/artifact.service.ts +225 -148
  88. package/src/services/attachment.service.ts +137 -115
  89. package/src/services/autonomous-job.service.ts +888 -491
  90. package/src/services/chat-run-registry.service.ts +11 -1
  91. package/src/services/context-compaction.service.ts +136 -86
  92. package/src/services/document-chunk.service.ts +162 -90
  93. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  94. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  95. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  96. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  97. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  98. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  99. package/src/services/feedback-loop.service.ts +132 -76
  100. package/src/services/global-orchestrator.service.ts +80 -170
  101. package/src/services/graph-full-routing.ts +182 -0
  102. package/src/services/index.ts +18 -21
  103. package/src/services/institutional-memory.service.ts +220 -123
  104. package/src/services/learned-skill.service.ts +364 -259
  105. package/src/services/memory/memory-conversation.ts +95 -0
  106. package/src/services/memory/memory-org-memory.ts +39 -0
  107. package/src/services/memory/memory-preseeded.ts +80 -0
  108. package/src/services/memory/memory-rerank.ts +297 -0
  109. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  110. package/src/services/memory/memory.service.ts +692 -0
  111. package/src/services/memory/rerank.service.ts +209 -0
  112. package/src/services/monitoring-window.service.ts +92 -70
  113. package/src/services/mutating-approval.service.ts +62 -53
  114. package/src/services/node-workspace.service.ts +141 -98
  115. package/src/services/notification.service.ts +17 -16
  116. package/src/services/organization-member.service.ts +120 -66
  117. package/src/services/organization.service.ts +144 -51
  118. package/src/services/ownership-dispatcher.service.ts +415 -264
  119. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  120. package/src/services/plan/plan-agent-query.service.ts +322 -0
  121. package/src/services/plan/plan-approval.service.ts +102 -0
  122. package/src/services/plan/plan-artifact.service.ts +60 -0
  123. package/src/services/plan/plan-builder.service.ts +76 -0
  124. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  125. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  126. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  127. package/src/services/plan/plan-coordination.service.ts +181 -0
  128. package/src/services/plan/plan-cycle.service.ts +398 -0
  129. package/src/services/plan/plan-deadline.service.ts +547 -0
  130. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  131. package/src/services/plan/plan-executor-context.ts +35 -0
  132. package/src/services/plan/plan-executor-graph.ts +475 -0
  133. package/src/services/plan/plan-executor-helpers.ts +322 -0
  134. package/src/services/plan/plan-executor-persistence.ts +209 -0
  135. package/src/services/plan/plan-executor.service.ts +1654 -0
  136. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  137. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  138. package/src/services/plan/plan-run-serialization.ts +15 -0
  139. package/src/services/plan/plan-run.service.ts +644 -0
  140. package/src/services/plan/plan-scheduler.service.ts +385 -0
  141. package/src/services/plan/plan-template.service.ts +224 -0
  142. package/src/services/plan/plan-transaction-events.ts +33 -0
  143. package/src/services/plan/plan-validator.service.ts +907 -0
  144. package/src/services/plan/plan-workspace.service.ts +125 -0
  145. package/src/services/plugin-executor.service.ts +97 -68
  146. package/src/services/quality-metrics.service.ts +112 -94
  147. package/src/services/queue-job.service.ts +296 -230
  148. package/src/services/recent-activity-title.service.ts +65 -36
  149. package/src/services/recent-activity.service.ts +274 -259
  150. package/src/services/skill-resolver.service.ts +38 -12
  151. package/src/services/social-chat-history.service.ts +176 -125
  152. package/src/services/system-executor.service.ts +91 -61
  153. package/src/services/thread/thread-active-run.ts +203 -0
  154. package/src/services/thread/thread-bootstrap.ts +369 -0
  155. package/src/services/thread/thread-listing.ts +198 -0
  156. package/src/services/thread/thread-memory-block.ts +117 -0
  157. package/src/services/thread/thread-message.service.ts +363 -0
  158. package/src/services/thread/thread-record-store.ts +155 -0
  159. package/src/services/thread/thread-title.service.ts +74 -0
  160. package/src/services/thread/thread-turn-execution.ts +280 -0
  161. package/src/services/thread/thread-turn-message-context.ts +73 -0
  162. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  163. package/src/services/thread/thread-turn-streaming.ts +402 -0
  164. package/src/services/thread/thread-turn-tracing.ts +35 -0
  165. package/src/services/thread/thread-turn.ts +343 -0
  166. package/src/services/thread/thread.service.ts +335 -0
  167. package/src/services/user.service.ts +82 -32
  168. package/src/services/write-intent-validator.service.ts +63 -51
  169. package/src/storage/attachment-parser.ts +69 -27
  170. package/src/storage/attachment-storage.service.ts +331 -275
  171. package/src/storage/generated-document-storage.service.ts +66 -34
  172. package/src/system-agents/agent-result.ts +3 -1
  173. package/src/system-agents/context-compaction.agent.ts +2 -2
  174. package/src/system-agents/delegated-agent-factory.ts +159 -90
  175. package/src/system-agents/memory-reranker.agent.ts +2 -2
  176. package/src/system-agents/memory.agent.ts +2 -2
  177. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  178. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  179. package/src/system-agents/skill-extractor.agent.ts +2 -2
  180. package/src/system-agents/skill-manager.agent.ts +2 -2
  181. package/src/system-agents/thread-router.agent.ts +157 -113
  182. package/src/system-agents/title-generator.agent.ts +2 -2
  183. package/src/tools/execution-plan.tool.ts +220 -161
  184. package/src/tools/fetch-webpage.tool.ts +21 -17
  185. package/src/tools/firecrawl-client.ts +16 -6
  186. package/src/tools/index.ts +1 -0
  187. package/src/tools/memory-block.tool.ts +14 -6
  188. package/src/tools/plan-approval.tool.ts +49 -47
  189. package/src/tools/read-file-parts.tool.ts +44 -33
  190. package/src/tools/remember-memory.tool.ts +65 -45
  191. package/src/tools/search-web.tool.ts +26 -22
  192. package/src/tools/search.tool.ts +41 -29
  193. package/src/tools/team-think.tool.ts +124 -83
  194. package/src/tools/user-questions.tool.ts +4 -3
  195. package/src/tools/web-tool-shared.ts +6 -0
  196. package/src/utils/async.ts +17 -23
  197. package/src/utils/crypto.ts +21 -0
  198. package/src/utils/date-time.ts +40 -1
  199. package/src/utils/errors.ts +95 -16
  200. package/src/utils/hono-error-handler.ts +24 -39
  201. package/src/utils/index.ts +2 -1
  202. package/src/utils/null-proto-record.ts +41 -0
  203. package/src/utils/sse-keepalive.ts +124 -21
  204. package/src/workers/bootstrap.ts +186 -51
  205. package/src/workers/memory-consolidation.worker.ts +325 -237
  206. package/src/workers/organization-learning.worker.ts +50 -16
  207. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  208. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  209. package/src/workers/skill-extraction.runner.ts +176 -93
  210. package/src/workers/utils/file-section-chunker.ts +8 -10
  211. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  212. package/src/workers/utils/repomix-file-sections.ts +2 -2
  213. package/src/workers/utils/thread-message-query.ts +97 -38
  214. package/src/workers/worker-utils.ts +56 -31
  215. package/src/config/debug-logger.ts +0 -47
  216. package/src/redis/connection-accessor.ts +0 -26
  217. package/src/runtime/context-compaction-runtime.ts +0 -87
  218. package/src/runtime/social-chat-agent-runner.ts +0 -118
  219. package/src/runtime/social-chat.ts +0 -516
  220. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  221. package/src/services/adaptive-playbook.service.ts +0 -152
  222. package/src/services/artifact-provenance.service.ts +0 -172
  223. package/src/services/chat-attachments.service.ts +0 -17
  224. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  225. package/src/services/execution-plan.service.ts +0 -1118
  226. package/src/services/memory.service.ts +0 -914
  227. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  228. package/src/services/plan-agent-query.service.ts +0 -267
  229. package/src/services/plan-approval.service.ts +0 -83
  230. package/src/services/plan-artifact.service.ts +0 -50
  231. package/src/services/plan-builder.service.ts +0 -67
  232. package/src/services/plan-checkpoint.service.ts +0 -81
  233. package/src/services/plan-completion-side-effects.ts +0 -80
  234. package/src/services/plan-coordination.service.ts +0 -157
  235. package/src/services/plan-cycle.service.ts +0 -284
  236. package/src/services/plan-deadline.service.ts +0 -430
  237. package/src/services/plan-event-delivery.service.ts +0 -166
  238. package/src/services/plan-executor.service.ts +0 -1950
  239. package/src/services/plan-run.service.ts +0 -515
  240. package/src/services/plan-scheduler.service.ts +0 -240
  241. package/src/services/plan-template.service.ts +0 -177
  242. package/src/services/plan-validator.service.ts +0 -818
  243. package/src/services/plan-workspace.service.ts +0 -83
  244. package/src/services/rerank.service.ts +0 -156
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,322 @@
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 } 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
+
29
+ function parseLiteralValue(raw: string): Effect.Effect<unknown> {
30
+ const trimmed = raw.trim()
31
+ if (!trimmed.length) return Effect.succeed(undefined)
32
+ if (trimmed === 'true') return Effect.succeed(true)
33
+ if (trimmed === 'false') return Effect.succeed(false)
34
+ if (trimmed === 'null') return Effect.succeed(null)
35
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Effect.succeed(Number(trimmed))
36
+ if (
37
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
38
+ (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
39
+ (trimmed.startsWith('`') && trimmed.endsWith('`'))
40
+ ) {
41
+ return Effect.succeed(trimmed.slice(1, -1))
42
+ }
43
+
44
+ return Effect.try({ try: (): unknown => JSON.parse(trimmed), catch: (cause) => cause }).pipe(
45
+ Effect.catch(() => Effect.succeed(trimmed)),
46
+ )
47
+ }
48
+
49
+ function buildArtifactContext(artifacts: Array<{ name: string; kind: string; payload?: unknown }>) {
50
+ return Object.fromEntries(
51
+ artifacts.map((artifact) => [artifact.name, { kind: artifact.kind, payload: artifact.payload }]),
52
+ )
53
+ }
54
+
55
+ export function buildNodeContext(params: {
56
+ nodeRun:
57
+ | { resolvedInput?: Record<string, unknown> | null; latestStructuredOutput?: Record<string, unknown> | null }
58
+ | undefined
59
+ artifacts: Array<{ name: string; kind: string; payload?: unknown }>
60
+ }) {
61
+ return {
62
+ input: params.nodeRun?.resolvedInput ?? {},
63
+ output: params.nodeRun?.latestStructuredOutput ?? {},
64
+ artifact: buildArtifactContext(params.artifacts),
65
+ }
66
+ }
67
+
68
+ export function buildPublishedArtifactContent(params: {
69
+ artifact: PlanNodeResultSubmission['artifacts'][number]
70
+ notes: string
71
+ }): string {
72
+ const { artifact, notes } = params
73
+ if (artifact.content?.trim()) {
74
+ return artifact.content
75
+ }
76
+
77
+ if (artifact.payload !== undefined) {
78
+ return `# ${artifact.name}\n\n\`\`\`json\n${JSON.stringify(artifact.payload, null, 2)}\n\`\`\``
79
+ }
80
+
81
+ return `# ${artifact.name}\n\n${notes.trim()}`
82
+ }
83
+
84
+ export function evaluateCondition(
85
+ expression: string | undefined,
86
+ context: Record<string, unknown>,
87
+ ): Effect.Effect<boolean> {
88
+ if (!expression?.trim()) return Effect.succeed(true)
89
+ const normalized = expression.trim()
90
+ if (normalized === 'always') return Effect.succeed(true)
91
+
92
+ const match = normalized.match(/^([a-zA-Z0-9_.]+)\s*(==|!=|>=|<=|>|<)\s*(.+)$/)
93
+ if (!match) {
94
+ if (!isExecutableConditionExpression(normalized)) {
95
+ return Effect.succeed(true)
96
+ }
97
+ return Effect.succeed(Boolean(readPathValue(context, normalized)))
98
+ }
99
+
100
+ return Effect.gen(function* () {
101
+ const [, leftPath, operator, rawRightValue] = match
102
+ const left = readPathValue(context, leftPath)
103
+ const right = yield* parseLiteralValue(rawRightValue)
104
+
105
+ if (operator === '==') return Object.is(left, right)
106
+ if (operator === '!=') return !Object.is(left, right)
107
+ if (typeof left !== 'number' || typeof right !== 'number') return false
108
+ if (operator === '>=') return left >= right
109
+ if (operator === '<=') return left <= right
110
+ if (operator === '>') return left > right
111
+ return left < right
112
+ })
113
+ }
114
+
115
+ export function isSuccessfulTerminalStatus(status: string): boolean {
116
+ return SUCCESSFUL_TERMINAL_NODE_STATUSES.has(status)
117
+ }
118
+
119
+ export function isHumanNodeType(type: string): boolean {
120
+ return HUMAN_NODE_TYPE_SET.has(type)
121
+ }
122
+
123
+ export function isStructuralNodeType(type: string): boolean {
124
+ return STRUCTURAL_NODE_TYPE_SET.has(type)
125
+ }
126
+
127
+ type PlanNodeRunUpdate = Omit<
128
+ Partial<PlanNodeRunRecord>,
129
+ | 'blockedReason'
130
+ | 'failureClass'
131
+ | 'resolvedInput'
132
+ | 'latestStructuredOutput'
133
+ | 'latestNotes'
134
+ | 'latestAttemptId'
135
+ | 'scheduledAt'
136
+ | 'readyAt'
137
+ | 'startedAt'
138
+ | 'completedAt'
139
+ > & {
140
+ blockedReason?: string | null
141
+ failureClass?: PlanFailureClass | null
142
+ resolvedInput?: Record<string, unknown> | null
143
+ latestStructuredOutput?: Record<string, unknown> | null
144
+ latestNotes?: string | null
145
+ latestAttemptId?: RecordIdInput | null
146
+ scheduledAt?: string | Date | null
147
+ readyAt?: string | Date | null
148
+ startedAt?: string | Date | null
149
+ completedAt?: string | Date | null
150
+ }
151
+
152
+ export function toNodeRunData(nodeRun: PlanNodeRunRecord, patch: PlanNodeRunUpdate) {
153
+ return {
154
+ runId: ensureRecordId(nodeRun.runId, TABLES.PLAN_RUN),
155
+ planSpecId: ensureRecordId(nodeRun.planSpecId, TABLES.PLAN_SPEC),
156
+ nodeId: nodeRun.nodeId,
157
+ status: patch.status ?? nodeRun.status,
158
+ attemptCount: patch.attemptCount ?? nodeRun.attemptCount,
159
+ retryCount: patch.retryCount ?? nodeRun.retryCount,
160
+ ...(patch.resolvedInput === null
161
+ ? {}
162
+ : patch.resolvedInput !== undefined
163
+ ? { resolvedInput: patch.resolvedInput }
164
+ : nodeRun.resolvedInput
165
+ ? { resolvedInput: nodeRun.resolvedInput }
166
+ : {}),
167
+ ...(patch.latestStructuredOutput === null
168
+ ? {}
169
+ : patch.latestStructuredOutput !== undefined
170
+ ? { latestStructuredOutput: patch.latestStructuredOutput }
171
+ : nodeRun.latestStructuredOutput
172
+ ? { latestStructuredOutput: nodeRun.latestStructuredOutput }
173
+ : {}),
174
+ ...(patch.latestNotes === null
175
+ ? {}
176
+ : patch.latestNotes !== undefined
177
+ ? { latestNotes: patch.latestNotes }
178
+ : nodeRun.latestNotes
179
+ ? { latestNotes: nodeRun.latestNotes }
180
+ : {}),
181
+ ...(patch.latestAttemptId === null
182
+ ? {}
183
+ : patch.latestAttemptId !== undefined
184
+ ? { latestAttemptId: ensureRecordId(patch.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT) }
185
+ : nodeRun.latestAttemptId
186
+ ? { latestAttemptId: ensureRecordId(nodeRun.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT) }
187
+ : {}),
188
+ ...(patch.blockedReason === null
189
+ ? {}
190
+ : patch.blockedReason !== undefined
191
+ ? { blockedReason: patch.blockedReason }
192
+ : nodeRun.blockedReason
193
+ ? { blockedReason: nodeRun.blockedReason }
194
+ : {}),
195
+ ...(patch.failureClass === null
196
+ ? {}
197
+ : patch.failureClass !== undefined
198
+ ? { failureClass: patch.failureClass }
199
+ : nodeRun.failureClass
200
+ ? { failureClass: nodeRun.failureClass }
201
+ : {}),
202
+ ...(patch.scheduledAt === null
203
+ ? {}
204
+ : patch.scheduledAt !== undefined
205
+ ? { scheduledAt: toDatabaseDateTime(patch.scheduledAt) }
206
+ : nodeRun.scheduledAt
207
+ ? { scheduledAt: toDatabaseDateTime(nodeRun.scheduledAt) }
208
+ : {}),
209
+ ...(patch.readyAt === null
210
+ ? {}
211
+ : patch.readyAt !== undefined
212
+ ? { readyAt: toDatabaseDateTime(patch.readyAt) }
213
+ : nodeRun.readyAt
214
+ ? { readyAt: toDatabaseDateTime(nodeRun.readyAt) }
215
+ : {}),
216
+ ...(patch.startedAt === null
217
+ ? {}
218
+ : patch.startedAt !== undefined
219
+ ? { startedAt: toDatabaseDateTime(patch.startedAt) }
220
+ : nodeRun.startedAt
221
+ ? { startedAt: toDatabaseDateTime(nodeRun.startedAt) }
222
+ : {}),
223
+ ...(patch.completedAt === null
224
+ ? {}
225
+ : patch.completedAt !== undefined
226
+ ? { completedAt: toDatabaseDateTime(patch.completedAt) }
227
+ : nodeRun.completedAt
228
+ ? { completedAt: toDatabaseDateTime(nodeRun.completedAt) }
229
+ : {}),
230
+ }
231
+ }
232
+
233
+ export interface HumanNodeResponsePayload {
234
+ approved?: boolean
235
+ comments?: string
236
+ reason?: string
237
+ requiredEdits?: readonly string[]
238
+ responseText?: string
239
+ messageId?: string
240
+ [key: string]: unknown
241
+ }
242
+
243
+ export function readHumanResponseComments(response: HumanNodeResponsePayload): string | undefined {
244
+ const comments = response.comments
245
+ if (typeof comments === 'string' && comments.trim().length > 0) {
246
+ return comments
247
+ }
248
+
249
+ const reason = response.reason
250
+ return typeof reason === 'string' && reason.trim().length > 0 ? reason : undefined
251
+ }
252
+
253
+ export function readHumanRequiredEdits(response: HumanNodeResponsePayload): string[] | undefined {
254
+ if (!Array.isArray(response.requiredEdits)) {
255
+ return undefined
256
+ }
257
+
258
+ const requiredEdits = response.requiredEdits.filter(
259
+ (entry): entry is string => typeof entry === 'string' && entry.trim().length > 0,
260
+ )
261
+
262
+ return requiredEdits.length > 0 ? requiredEdits : undefined
263
+ }
264
+
265
+ export function deriveApprovalStatus(
266
+ response: HumanNodeResponsePayload,
267
+ ): 'approved' | 'rejected' | 'changes-requested' {
268
+ const approved = response.approved === true
269
+ const requiredEdits = readHumanRequiredEdits(response) ?? []
270
+
271
+ if (approved && requiredEdits.length === 0) return 'approved'
272
+ if (requiredEdits.length > 0) return 'changes-requested'
273
+ return 'rejected'
274
+ }
275
+
276
+ export function resolveFailureAction(
277
+ nodeSpec: Pick<PlanNodeSpecRecord, 'failurePolicy'>,
278
+ failureClass: PlanFailureClass | null,
279
+ ): PlanFailureAction {
280
+ if (!failureClass) return 'abort'
281
+
282
+ const matchedRule = nodeSpec.failurePolicy.find((rule) => rule.on === failureClass)
283
+ return matchedRule?.action ?? 'abort'
284
+ }
285
+
286
+ export function buildResolvedInput(params: {
287
+ spec: Pick<PlanSpecRecord, 'edges'>
288
+ nodeSpec: Pick<PlanNodeSpecRecord, 'nodeId'>
289
+ nodeRunsById: Map<string, Pick<PlanNodeRunRecord, 'status' | 'resolvedInput' | 'latestStructuredOutput'>>
290
+ artifactsByNodeId: Map<string, Array<{ name: string; kind: string; payload?: unknown }>>
291
+ }): Effect.Effect<Record<string, unknown>, ValidationError> {
292
+ return Effect.gen(function* () {
293
+ const resolvedInput: Record<string, unknown> = createNullProtoRecord()
294
+
295
+ for (const edge of params.spec.edges.filter((candidate) => candidate.target === params.nodeSpec.nodeId)) {
296
+ const sourceRun = params.nodeRunsById.get(edge.source)
297
+ if (!sourceRun || !isSuccessfulTerminalStatus(sourceRun.status)) continue
298
+
299
+ const context = buildNodeContext({
300
+ nodeRun: sourceRun,
301
+ artifacts: params.artifactsByNodeId.get(edge.source) ?? [],
302
+ })
303
+ const matches = yield* evaluateCondition(edge.when, context)
304
+ if (!matches) continue
305
+
306
+ for (const [targetPath, sourcePath] of Object.entries(edge.map)) {
307
+ if (!isSafePath(targetPath)) {
308
+ return yield* new ValidationError({ message: `Unsafe edge target path "${targetPath}" in edge "${edge.id}"` })
309
+ }
310
+ if (!isSafePathOrRoot(sourcePath)) {
311
+ return yield* new ValidationError({ message: `Unsafe edge source path "${sourcePath}" in edge "${edge.id}"` })
312
+ }
313
+ const value = readPathValue(context, sourcePath)
314
+ if (value !== undefined) {
315
+ setPathValue(resolvedInput, targetPath, value)
316
+ }
317
+ }
318
+ }
319
+
320
+ return resolvedInput
321
+ })
322
+ }
@@ -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
+ }