@lota-sdk/core 0.4.7 → 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/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  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
+ }