@lota-sdk/core 0.1.14 → 0.1.16

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 (174) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +9 -8
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/embedding-cache.ts +7 -6
  11. package/src/ai/index.ts +0 -1
  12. package/src/bifrost/bifrost.ts +14 -14
  13. package/src/config/agent-defaults.ts +32 -22
  14. package/src/config/agent-types.ts +11 -0
  15. package/src/config/constants.ts +2 -14
  16. package/src/config/debug-logger.ts +5 -1
  17. package/src/config/index.ts +3 -0
  18. package/src/config/logger.ts +7 -9
  19. package/src/config/model-constants.ts +16 -34
  20. package/src/config/search.ts +1 -15
  21. package/src/create-runtime.ts +453 -0
  22. package/src/db/cursor-pagination.ts +3 -6
  23. package/src/db/index.ts +2 -0
  24. package/src/db/memory-store.rows.ts +7 -7
  25. package/src/db/memory-store.ts +24 -24
  26. package/src/db/memory.ts +18 -16
  27. package/src/db/schema-fingerprint.ts +1 -0
  28. package/src/db/service.ts +193 -122
  29. package/src/db/startup.ts +9 -13
  30. package/src/db/surreal-mutation.ts +43 -0
  31. package/src/db/tables.ts +7 -0
  32. package/src/db/workstream-message-row.ts +15 -0
  33. package/src/embeddings/provider.ts +1 -1
  34. package/src/index.ts +1 -1
  35. package/src/queues/context-compaction.queue.ts +17 -52
  36. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  37. package/src/queues/document-processor.queue.ts +7 -7
  38. package/src/queues/index.ts +3 -0
  39. package/src/queues/memory-consolidation.queue.ts +18 -54
  40. package/src/queues/plan-scheduler.queue.ts +97 -0
  41. package/src/queues/post-chat-memory.queue.ts +15 -60
  42. package/src/queues/queue-factory.ts +100 -0
  43. package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
  44. package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
  45. package/src/queues/skill-extraction.queue.ts +15 -50
  46. package/src/queues/workstream-title-generation.queue.ts +15 -51
  47. package/src/redis/connection.ts +12 -3
  48. package/src/redis/index.ts +2 -1
  49. package/src/redis/org-memory-lock.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +41 -8
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +106 -21
  53. package/src/runtime/agent-stream-helpers.ts +2 -1
  54. package/src/runtime/approval-continuation.ts +12 -6
  55. package/src/runtime/context-compaction-constants.ts +1 -1
  56. package/src/runtime/context-compaction-runtime.ts +7 -5
  57. package/src/runtime/context-compaction.ts +40 -97
  58. package/src/runtime/execution-plan.ts +23 -19
  59. package/src/runtime/graph-designer.ts +15 -0
  60. package/src/runtime/helper-model.ts +10 -196
  61. package/src/runtime/index.ts +14 -1
  62. package/src/runtime/llm-content.ts +1 -1
  63. package/src/runtime/memory-block.ts +11 -12
  64. package/src/runtime/memory-pipeline.ts +26 -10
  65. package/src/runtime/plugin-resolution.ts +35 -0
  66. package/src/runtime/plugin-types.ts +73 -1
  67. package/src/runtime/retrieval-adapters.ts +1 -1
  68. package/src/runtime/runtime-config.ts +25 -12
  69. package/src/runtime/runtime-extensions.ts +91 -15
  70. package/src/runtime/runtime-worker-registry.ts +6 -0
  71. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  72. package/src/runtime/team-consultation-prompts.ts +11 -2
  73. package/src/runtime/title-helpers.ts +11 -4
  74. package/src/runtime/workstream-chat-helpers.ts +6 -7
  75. package/src/runtime/workstream-routing-policy.ts +0 -30
  76. package/src/runtime/workstream-state.ts +17 -7
  77. package/src/services/adaptive-playbook.service.ts +152 -0
  78. package/src/services/agent-executor.service.ts +293 -0
  79. package/src/services/artifact-provenance.service.ts +172 -0
  80. package/src/services/attachment.service.ts +7 -12
  81. package/src/services/context-compaction.service.ts +75 -58
  82. package/src/services/context-enrichment.service.ts +33 -0
  83. package/src/services/coordination-registry.service.ts +117 -0
  84. package/src/services/document-chunk.service.ts +38 -33
  85. package/src/services/domain-agent-executor.service.ts +71 -0
  86. package/src/services/execution-plan.service.ts +271 -50
  87. package/src/services/feedback-loop.service.ts +96 -0
  88. package/src/services/global-orchestrator.service.ts +148 -0
  89. package/src/services/index.ts +26 -0
  90. package/src/services/institutional-memory.service.ts +145 -0
  91. package/src/services/learned-skill.service.ts +30 -15
  92. package/src/services/memory-assessment.service.ts +3 -2
  93. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
  94. package/src/services/memory.service.ts +55 -69
  95. package/src/services/monitoring-window.service.ts +86 -0
  96. package/src/services/mutating-approval.service.ts +1 -1
  97. package/src/services/node-workspace.service.ts +155 -0
  98. package/src/services/notification.service.ts +39 -0
  99. package/src/services/organization-member.service.ts +12 -5
  100. package/src/services/organization.service.ts +5 -5
  101. package/src/services/ownership-dispatcher.service.ts +403 -0
  102. package/src/services/plan-approval.service.ts +1 -1
  103. package/src/services/plan-artifact.service.ts +1 -0
  104. package/src/services/plan-builder.service.ts +1 -0
  105. package/src/services/plan-checkpoint.service.ts +30 -2
  106. package/src/services/plan-compiler.service.ts +5 -0
  107. package/src/services/plan-coordination.service.ts +152 -0
  108. package/src/services/plan-cycle.service.ts +284 -0
  109. package/src/services/plan-deadline.service.ts +287 -0
  110. package/src/services/plan-executor.service.ts +386 -58
  111. package/src/services/plan-helpers.ts +15 -0
  112. package/src/services/plan-run.service.ts +41 -7
  113. package/src/services/plan-scheduler.service.ts +240 -0
  114. package/src/services/plan-template.service.ts +117 -0
  115. package/src/services/plan-validator.service.ts +87 -20
  116. package/src/services/plan-workspace.service.ts +83 -0
  117. package/src/services/playbook-registry.service.ts +67 -0
  118. package/src/services/plugin-executor.service.ts +103 -0
  119. package/src/services/quality-metrics.service.ts +132 -0
  120. package/src/services/recent-activity-title.service.ts +3 -10
  121. package/src/services/recent-activity.service.ts +33 -43
  122. package/src/services/skill-resolver.service.ts +19 -0
  123. package/src/services/system-executor.service.ts +105 -0
  124. package/src/services/workstream-message.service.ts +29 -41
  125. package/src/services/workstream-plan-registry.service.ts +22 -0
  126. package/src/services/workstream-title.service.ts +3 -9
  127. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
  128. package/src/services/workstream-turn.ts +2 -2
  129. package/src/services/workstream.service.ts +55 -65
  130. package/src/services/workstream.types.ts +10 -19
  131. package/src/services/write-intent-validator.service.ts +81 -0
  132. package/src/storage/attachment-parser.ts +1 -1
  133. package/src/storage/attachment-storage.service.ts +4 -4
  134. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
  135. package/src/storage/generated-document-storage.service.ts +3 -2
  136. package/src/storage/index.ts +2 -2
  137. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  138. package/src/system-agents/delegated-agent-factory.ts +5 -2
  139. package/src/system-agents/index.ts +8 -0
  140. package/src/system-agents/memory-reranker.agent.ts +1 -1
  141. package/src/system-agents/memory.agent.ts +1 -1
  142. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  143. package/src/tools/execution-plan.tool.ts +17 -19
  144. package/src/tools/fetch-webpage.tool.ts +20 -18
  145. package/src/tools/index.ts +2 -3
  146. package/src/tools/read-file-parts.tool.ts +1 -1
  147. package/src/tools/search-web.tool.ts +18 -15
  148. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  149. package/src/tools/team-think.tool.ts +14 -8
  150. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  151. package/src/utils/async.ts +3 -2
  152. package/src/utils/date-time.ts +4 -32
  153. package/src/utils/env.ts +8 -0
  154. package/src/utils/errors.ts +47 -0
  155. package/src/utils/hono-error-handler.ts +1 -2
  156. package/src/utils/index.ts +19 -2
  157. package/src/utils/string.ts +128 -1
  158. package/src/workers/bootstrap.ts +2 -2
  159. package/src/workers/index.ts +1 -0
  160. package/src/workers/memory-consolidation.worker.ts +12 -12
  161. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  162. package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
  163. package/src/workers/skill-extraction.runner.ts +8 -102
  164. package/src/workers/utils/file-section-chunker.ts +6 -3
  165. package/src/workers/utils/repomix-file-sections.ts +2 -2
  166. package/src/workers/utils/sandbox-error.ts +11 -2
  167. package/src/workers/utils/workstream-message-query.ts +97 -0
  168. package/src/workers/worker-utils.ts +6 -2
  169. package/src/runtime/retrieval-pipeline.ts +0 -3
  170. package/src/runtime.ts +0 -387
  171. package/src/tools/log-hello-world.tool.ts +0 -17
  172. package/src/utils/error.ts +0 -10
  173. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  174. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -0,0 +1,293 @@
1
+ import type {
2
+ ExecutionMode,
3
+ OwnershipDispatchContext,
4
+ PlanArtifactSubmission,
5
+ PlanNodeResult,
6
+ PlanNodeSpec,
7
+ PlanSchemaRegistry,
8
+ WriteIntent,
9
+ } from '@lota-sdk/shared'
10
+ import { PlanNodeResultSubmissionSchema, WriteIntentSchema } from '@lota-sdk/shared'
11
+ import { stepCountIs, tool } from 'ai'
12
+ import type { ToolLoopAgent, ToolSet } from 'ai'
13
+
14
+ import { agentRoster, buildAgentTools, createAgent, getAgentRuntimeConfig } from '../config/agent-defaults'
15
+ import { ensureRecordId } from '../db/record-id'
16
+ import { databaseService } from '../db/service'
17
+ import { TABLES } from '../db/tables'
18
+ import {
19
+ OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES,
20
+ buildOwnershipDispatchContextSection,
21
+ buildOwnershipDispatchResponseGuard,
22
+ } from '../runtime/agent-runtime-policy'
23
+ import { buildIndexedRepositoriesContext, getPluginService } from '../runtime/plugin-resolution'
24
+ import { nodeWorkspaceService } from './node-workspace.service'
25
+ import type { PlanValidationIssueInput } from './plan-validator.service'
26
+ import { WorkstreamSchema } from './workstream.types'
27
+ import { writeIntentValidatorService } from './write-intent-validator.service'
28
+
29
+ function applyToolPolicy(tools: ToolSet, nodeSpec: PlanNodeSpec): ToolSet {
30
+ const blockedToolNames = new Set([...OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES, ...nodeSpec.toolPolicy.deny])
31
+ const allowList = nodeSpec.toolPolicy.allow.length > 0 ? new Set(nodeSpec.toolPolicy.allow) : null
32
+
33
+ return Object.fromEntries(
34
+ Object.entries(tools).filter(
35
+ ([toolName]) => !blockedToolNames.has(toolName) && (allowList === null || allowList.has(toolName)),
36
+ ),
37
+ )
38
+ }
39
+
40
+ function buildDispatchPrompt(nodeSpec: PlanNodeSpec): string {
41
+ return [
42
+ `Execute the execution-plan node "${nodeSpec.label}".`,
43
+ `Objective: ${nodeSpec.objective}`,
44
+ `Instructions: ${nodeSpec.instructions}`,
45
+ 'Return the final node result JSON only.',
46
+ ].join('\n\n')
47
+ }
48
+
49
+ export function buildWriteIntentDispatchPrompt(nodeSpec: PlanNodeSpec): string {
50
+ const deliverables = nodeSpec.deliverables
51
+ .map((d) => `- ${d.name} (${d.kind}${d.required ? ', required' : ''})`)
52
+ .join('\n')
53
+ return [
54
+ `Execute the execution-plan node "${nodeSpec.label}".`,
55
+ `Objective: ${nodeSpec.objective}`,
56
+ `Instructions: ${nodeSpec.instructions}`,
57
+ '',
58
+ 'Produce each deliverable by calling the writeIntent tool:',
59
+ deliverables,
60
+ '',
61
+ 'For each, call writeIntent with targetPath matching the deliverable name.',
62
+ 'If writeIntent returns validation_failed, correct the payload and try again.',
63
+ 'When all deliverables are written, end with a brief completion summary.',
64
+ ].join('\n')
65
+ }
66
+
67
+ const MAX_SELF_CORRECTION_RETRIES = 3
68
+
69
+ class AgentExecutorService {
70
+ validateOwner(agentId: string, nodeId: string): PlanValidationIssueInput[] {
71
+ if (!agentRoster.includes(agentId)) {
72
+ return [
73
+ {
74
+ severity: 'blocking',
75
+ code: 'agent_executor_missing',
76
+ message: `Node "${nodeId}" references unknown agent executor "${agentId}".`,
77
+ nodeId,
78
+ detail: { agentId },
79
+ },
80
+ ]
81
+ }
82
+
83
+ return []
84
+ }
85
+
86
+ async executeNode(params: {
87
+ nodeSpec: PlanNodeSpec
88
+ resolvedInput: Record<string, unknown>
89
+ inputArtifacts: PlanArtifactSubmission[]
90
+ context: OwnershipDispatchContext
91
+ executionMode?: ExecutionMode
92
+ schemaRegistry?: PlanSchemaRegistry
93
+ }): Promise<PlanNodeResult> {
94
+ if (params.nodeSpec.owner.executorType !== 'agent') {
95
+ throw new Error(`AgentExecutor cannot execute owner type "${params.nodeSpec.owner.executorType}".`)
96
+ }
97
+
98
+ const agentId = params.nodeSpec.owner.ref
99
+ if (!agentRoster.includes(agentId)) {
100
+ throw new Error(`Agent executor "${agentId}" is not registered.`)
101
+ }
102
+
103
+ const workstream = await databaseService.findOne(
104
+ TABLES.WORKSTREAM,
105
+ { id: ensureRecordId(params.context.workstreamId, TABLES.WORKSTREAM) },
106
+ WorkstreamSchema,
107
+ )
108
+ if (!workstream) {
109
+ throw new Error(`Workstream ${params.context.workstreamId} not found for dispatched execution.`)
110
+ }
111
+
112
+ const organizationRef = ensureRecordId(params.context.organizationId, TABLES.ORGANIZATION)
113
+ const workstreamRef = ensureRecordId(params.context.workstreamId, TABLES.WORKSTREAM)
114
+ const userRefSource = params.context.userId ?? workstream.userId
115
+ if (!userRefSource) {
116
+ throw new Error(`Workstream ${params.context.workstreamId} is missing a user context for dispatched execution.`)
117
+ }
118
+ const userRef = ensureRecordId(userRefSource, TABLES.USER)
119
+ const userName = params.context.userName ?? 'User'
120
+ const getLinearInstallationByOrgId = getPluginService([
121
+ 'linear',
122
+ 'services',
123
+ 'linearService',
124
+ 'getInstallationByOrgId',
125
+ ])
126
+ const getGithubInstallationForOrganization = getPluginService([
127
+ 'github',
128
+ 'services',
129
+ 'githubService',
130
+ 'getInstallationForOrganization',
131
+ ])
132
+ const [linearInstallation, githubInstallation, indexedRepoContext] = await Promise.all([
133
+ getLinearInstallationByOrgId ? getLinearInstallationByOrgId(organizationRef) : Promise.resolve(null),
134
+ getGithubInstallationForOrganization
135
+ ? getGithubInstallationForOrganization(params.context.organizationId)
136
+ : Promise.resolve(null),
137
+ buildIndexedRepositoriesContext(params.context.organizationId),
138
+ ])
139
+
140
+ const mode = params.executionMode ?? 'linear'
141
+
142
+ const dispatchMode = workstream.mode === 'group' ? 'fixedWorkstreamMode' : 'direct'
143
+ const runtimeConfig = getAgentRuntimeConfig({
144
+ agentId,
145
+ workstreamMode: workstream.mode,
146
+ mode: dispatchMode,
147
+ onboardingActive: false,
148
+ linearInstalled: Boolean(linearInstallation),
149
+ reasoningProfile: 'standard',
150
+ additionalInstructionSections: [
151
+ buildOwnershipDispatchContextSection({
152
+ node: params.nodeSpec,
153
+ resolvedInput: params.resolvedInput,
154
+ inputArtifacts: params.inputArtifacts,
155
+ }),
156
+ ],
157
+ responseGuardSection: buildOwnershipDispatchResponseGuard({ node: params.nodeSpec, executionMode: mode }),
158
+ }) as Record<string, unknown>
159
+
160
+ const rawTools = (await buildAgentTools({
161
+ agentId,
162
+ orgId: organizationRef,
163
+ userId: userRef,
164
+ userName,
165
+ workstreamId: workstreamRef,
166
+ orgIdString: params.context.organizationId,
167
+ workstreamMode: workstream.mode,
168
+ mode: dispatchMode,
169
+ linearInstalled: Boolean(linearInstallation),
170
+ onboardingActive: false,
171
+ githubInstalled: Boolean(githubInstallation),
172
+ provideRepoTool: indexedRepoContext.provideRepoTool,
173
+ defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[agentId],
174
+ memoryBlock: '',
175
+ onAppendMemoryBlock: () => undefined,
176
+ availableUploads: [],
177
+ includeExecutionPlanTools: false,
178
+ })) as ToolSet
179
+ const tools = applyToolPolicy(rawTools, params.nodeSpec)
180
+
181
+ const agentFactory = createAgent[agentId] as ((...args: unknown[]) => unknown) | undefined
182
+ if (!agentFactory) {
183
+ throw new Error(`Agent factory "${agentId}" is not registered.`)
184
+ }
185
+
186
+ const maxSteps = typeof runtimeConfig.maxSteps === 'number' ? runtimeConfig.maxSteps : 8
187
+
188
+ if (mode === 'linear') {
189
+ const agent = agentFactory({
190
+ mode: dispatchMode,
191
+ tools,
192
+ extraInstructions:
193
+ typeof runtimeConfig.extraInstructions === 'string' ? runtimeConfig.extraInstructions : undefined,
194
+ maxRetries: 1,
195
+ stopWhen: [stepCountIs(maxSteps)],
196
+ }) as ToolLoopAgent<never, ToolSet>
197
+
198
+ const result = await agent.generate({ prompt: buildDispatchPrompt(params.nodeSpec) })
199
+ const outputCandidate = PlanNodeResultSubmissionSchema.safeParse(result.output)
200
+ if (outputCandidate.success) {
201
+ return outputCandidate.data
202
+ }
203
+
204
+ throw new Error(`Agent executor "${agentId}" returned an invalid node result.`)
205
+ }
206
+
207
+ // --- Graph-lite / Graph-full path ---
208
+
209
+ const workspace = nodeWorkspaceService.initialize({
210
+ nodeSpec: params.nodeSpec,
211
+ resolvedInput: params.resolvedInput,
212
+ inputArtifacts: params.inputArtifacts,
213
+ schemaRegistry: params.schemaRegistry ?? {},
214
+ })
215
+
216
+ const writeIntentTool = tool({
217
+ description:
218
+ 'Write a validated artifact or structured output field. Call this for each deliverable. If validation fails, correct your payload and try again.',
219
+ inputSchema: WriteIntentSchema,
220
+ execute: async (intent: WriteIntent) => {
221
+ const correctionCount = workspace.sys.correctionCounts.get(intent.targetPath) ?? 0
222
+
223
+ const validation = writeIntentValidatorService.validate({
224
+ intent,
225
+ nodeSpec: params.nodeSpec,
226
+ schemaRegistry: workspace.ctx.schemaRegistry,
227
+ existingDeliverables: workspace.deliverables,
228
+ })
229
+
230
+ if (validation.issues.length > 0) {
231
+ if (correctionCount >= MAX_SELF_CORRECTION_RETRIES) {
232
+ throw new Error(
233
+ `Write validation failed for "${intent.targetPath}" after ${MAX_SELF_CORRECTION_RETRIES} retries: ${validation.issues.map((i) => i.message).join(', ')}`,
234
+ )
235
+ }
236
+ workspace.sys.correctionCounts.set(intent.targetPath, correctionCount + 1)
237
+ workspace.sys.writeLog.push({
238
+ targetPath: intent.targetPath,
239
+ action: intent.action,
240
+ timestamp: new Date(),
241
+ result: 'rejected',
242
+ })
243
+ return {
244
+ status: 'validation_failed',
245
+ error: { targetPath: intent.targetPath, action: intent.action, issues: validation.issues },
246
+ hint: `Correct and re-emit. Attempt ${correctionCount + 1}/${MAX_SELF_CORRECTION_RETRIES}.`,
247
+ }
248
+ }
249
+
250
+ nodeWorkspaceService.stageWrite(workspace, intent, 'validated')
251
+ return { status: 'accepted', message: `Write to "${intent.targetPath}" validated and staged.` }
252
+ },
253
+ })
254
+
255
+ const graphTools = { ...tools, writeIntent: writeIntentTool }
256
+
257
+ const agent = agentFactory({
258
+ mode: dispatchMode,
259
+ tools: graphTools,
260
+ extraInstructions:
261
+ typeof runtimeConfig.extraInstructions === 'string' ? runtimeConfig.extraInstructions : undefined,
262
+ maxRetries: 1,
263
+ stopWhen: [stepCountIs(maxSteps)],
264
+ }) as ToolLoopAgent<never, ToolSet>
265
+
266
+ await agent.generate({ prompt: buildWriteIntentDispatchPrompt(params.nodeSpec) })
267
+
268
+ const finalResult = nodeWorkspaceService.finalize(workspace)
269
+
270
+ if (!finalResult.isComplete) {
271
+ const result: PlanNodeResult = {
272
+ structuredOutput: finalResult.structuredOutput,
273
+ artifacts: finalResult.artifacts,
274
+ notes: 'Execution incomplete: missing required deliverables or validation failures.',
275
+ quality: 'partial',
276
+ }
277
+ nodeWorkspaceService.cleanup(workspace)
278
+ return result
279
+ }
280
+
281
+ const result: PlanNodeResult = {
282
+ structuredOutput: finalResult.structuredOutput,
283
+ artifacts: finalResult.artifacts,
284
+ notes: finalResult.notes,
285
+ quality: finalResult.quality,
286
+ }
287
+
288
+ nodeWorkspaceService.cleanup(workspace)
289
+ return result
290
+ }
291
+ }
292
+
293
+ export const agentExecutorService = new AgentExecutorService()
@@ -0,0 +1,172 @@
1
+ import type { ImpactAnalysis, PlanNodeRunRecord, ProvenanceChain, ProvenanceNode } from '@lota-sdk/shared'
2
+
3
+ import { recordIdToString } from '../db/record-id'
4
+
5
+ class ArtifactProvenanceService {
6
+ /**
7
+ * Backward: trace which upstream nodes/artifacts produced the inputs for this artifact's node.
8
+ * From nodeId, follow edges upstream. At each upstream node, collect its artifacts.
9
+ * Continue until maxDepth or no more upstreams.
10
+ */
11
+ async traceBackward(params: { runId: string; nodeId: string; maxDepth?: number }): Promise<ProvenanceChain> {
12
+ const { planRunService } = await import('./plan-run.service')
13
+
14
+ const run = await planRunService.getRunById(params.runId)
15
+ const spec = await planRunService.getPlanSpecById(run.planSpecId)
16
+ const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
17
+ const artifacts = await planRunService.listArtifacts(run.id)
18
+
19
+ const nodeSpecsByNodeId = new Map(nodeSpecs.map((n) => [n.nodeId, n]))
20
+ const artifactsByNodeId = new Map<string, typeof artifacts>()
21
+ for (const artifact of artifacts) {
22
+ const list = artifactsByNodeId.get(artifact.nodeId) ?? []
23
+ list.push(artifact)
24
+ artifactsByNodeId.set(artifact.nodeId, list)
25
+ }
26
+
27
+ const maxDepth = params.maxDepth ?? 10
28
+ const chain: ProvenanceNode[] = []
29
+ const visited = new Set<string>()
30
+ const queue: Array<{ nodeId: string; depth: number }> = [{ nodeId: params.nodeId, depth: 0 }]
31
+
32
+ while (queue.length > 0) {
33
+ const entry = queue.shift()
34
+ if (!entry || visited.has(entry.nodeId) || entry.depth > maxDepth) continue
35
+ visited.add(entry.nodeId)
36
+
37
+ const nodeSpec = nodeSpecsByNodeId.get(entry.nodeId)
38
+ if (!nodeSpec) continue
39
+
40
+ const nodeArtifacts = (artifactsByNodeId.get(entry.nodeId) ?? []).map((a) => ({
41
+ name: a.name,
42
+ kind: a.kind,
43
+ id: recordIdToString(a.id),
44
+ }))
45
+
46
+ chain.push({ nodeId: entry.nodeId, label: nodeSpec.label, artifacts: nodeArtifacts })
47
+
48
+ // Walk upstream
49
+ for (const upstreamId of nodeSpec.upstreamNodeIds) {
50
+ if (!visited.has(upstreamId)) {
51
+ queue.push({ nodeId: upstreamId, depth: entry.depth + 1 })
52
+ }
53
+ }
54
+ }
55
+
56
+ return { root: { nodeId: params.nodeId }, chain, depth: Math.max(0, chain.length - 1) }
57
+ }
58
+
59
+ /**
60
+ * Forward: trace which downstream nodes consume this artifact's node output.
61
+ * Same as backward but follow edges downstream.
62
+ */
63
+ async traceForward(params: { runId: string; nodeId: string; maxDepth?: number }): Promise<ProvenanceChain> {
64
+ const { planRunService } = await import('./plan-run.service')
65
+
66
+ const run = await planRunService.getRunById(params.runId)
67
+ const spec = await planRunService.getPlanSpecById(run.planSpecId)
68
+ const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
69
+ const artifacts = await planRunService.listArtifacts(run.id)
70
+
71
+ const nodeSpecsByNodeId = new Map(nodeSpecs.map((n) => [n.nodeId, n]))
72
+ const artifactsByNodeId = new Map<string, typeof artifacts>()
73
+ for (const artifact of artifacts) {
74
+ const list = artifactsByNodeId.get(artifact.nodeId) ?? []
75
+ list.push(artifact)
76
+ artifactsByNodeId.set(artifact.nodeId, list)
77
+ }
78
+
79
+ const maxDepth = params.maxDepth ?? 10
80
+ const chain: ProvenanceNode[] = []
81
+ const visited = new Set<string>()
82
+ const queue: Array<{ nodeId: string; depth: number }> = [{ nodeId: params.nodeId, depth: 0 }]
83
+
84
+ while (queue.length > 0) {
85
+ const entry = queue.shift()
86
+ if (!entry || visited.has(entry.nodeId) || entry.depth > maxDepth) continue
87
+ visited.add(entry.nodeId)
88
+
89
+ const nodeSpec = nodeSpecsByNodeId.get(entry.nodeId)
90
+ if (!nodeSpec) continue
91
+
92
+ const nodeArtifacts = (artifactsByNodeId.get(entry.nodeId) ?? []).map((a) => ({
93
+ name: a.name,
94
+ kind: a.kind,
95
+ id: recordIdToString(a.id),
96
+ }))
97
+
98
+ chain.push({ nodeId: entry.nodeId, label: nodeSpec.label, artifacts: nodeArtifacts })
99
+
100
+ // Walk downstream
101
+ for (const downstreamId of nodeSpec.downstreamNodeIds) {
102
+ if (!visited.has(downstreamId)) {
103
+ queue.push({ nodeId: downstreamId, depth: entry.depth + 1 })
104
+ }
105
+ }
106
+ }
107
+
108
+ return { root: { nodeId: params.nodeId }, chain, depth: Math.max(0, chain.length - 1) }
109
+ }
110
+
111
+ /**
112
+ * Impact: which downstream nodes/artifacts would be affected.
113
+ * Forward traversal collecting all affected nodes and their artifacts.
114
+ * Includes non-completed nodes.
115
+ */
116
+ async analyzeImpact(params: { runId: string; nodeId: string }): Promise<ImpactAnalysis> {
117
+ const { planRunService } = await import('./plan-run.service')
118
+
119
+ const run = await planRunService.getRunById(params.runId)
120
+ const spec = await planRunService.getPlanSpecById(run.planSpecId)
121
+ const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
122
+ const nodeRuns = await planRunService.listNodeRuns(run.id)
123
+ const artifacts = await planRunService.listArtifacts(run.id)
124
+
125
+ const nodeSpecsByNodeId = new Map(nodeSpecs.map((n) => [n.nodeId, n]))
126
+ const nodeRunsByNodeId = new Map(nodeRuns.map((n: PlanNodeRunRecord) => [n.nodeId, n]))
127
+
128
+ const affectedNodes: Array<{ nodeId: string; label: string; status: PlanNodeRunRecord['status'] }> = []
129
+ const affectedArtifacts: Array<{ name: string; nodeId: string }> = []
130
+
131
+ const visited = new Set<string>()
132
+ const queue: string[] = []
133
+
134
+ // Seed: downstream neighbors of the source node
135
+ const sourceSpec = nodeSpecsByNodeId.get(params.nodeId)
136
+ if (sourceSpec) {
137
+ for (const downstreamId of sourceSpec.downstreamNodeIds) {
138
+ queue.push(downstreamId)
139
+ }
140
+ }
141
+
142
+ while (queue.length > 0) {
143
+ const nodeId = queue.shift()
144
+ if (!nodeId || visited.has(nodeId)) continue
145
+ visited.add(nodeId)
146
+
147
+ const nodeSpec = nodeSpecsByNodeId.get(nodeId)
148
+ if (!nodeSpec) continue
149
+
150
+ const nodeRun = nodeRunsByNodeId.get(nodeId)
151
+ affectedNodes.push({ nodeId, label: nodeSpec.label, status: nodeRun?.status ?? 'pending' })
152
+
153
+ // Collect artifacts for this node
154
+ for (const artifact of artifacts.filter((a) => a.nodeId === nodeId)) {
155
+ affectedArtifacts.push({ name: artifact.name, nodeId })
156
+ }
157
+
158
+ // Continue downstream
159
+ for (const downstreamId of nodeSpec.downstreamNodeIds) {
160
+ if (!visited.has(downstreamId)) queue.push(downstreamId)
161
+ }
162
+ }
163
+
164
+ return {
165
+ sourceNodeId: params.nodeId,
166
+ affectedNodes: affectedNodes as ImpactAnalysis['affectedNodes'],
167
+ affectedArtifacts,
168
+ }
169
+ }
170
+ }
171
+
172
+ export const artifactProvenanceService = new ArtifactProvenanceService()
@@ -3,7 +3,7 @@ import { recordIdToString } from '../db/record-id'
3
3
  import { TABLES } from '../db/tables'
4
4
  import { attachmentStorageService } from '../storage/attachment-storage.service'
5
5
  import type { UploadedWorkstreamAttachment as SdkUploadedWorkstreamAttachment } from '../storage/attachment-storage.service'
6
- import type { MessagePartLike, ReadableUploadMetadata as SdkReadableUploadMetadata } from '../storage/attachments.types'
6
+ import type { MessagePartLike, ReadableUploadMetadata as SdkReadableUploadMetadata } from '../storage/attachment-types'
7
7
 
8
8
  export type ReadableUploadMetadata = SdkReadableUploadMetadata
9
9
 
@@ -57,7 +57,7 @@ class AttachmentService {
57
57
  name: string
58
58
  contentType: string
59
59
  }): Promise<string> {
60
- return await attachmentStorageService.extractStoredAttachmentText({ storageKey, name, contentType })
60
+ return attachmentStorageService.extractStoredAttachmentText({ storageKey, name, contentType })
61
61
  }
62
62
 
63
63
  async extractStoredAttachmentPages({
@@ -69,7 +69,7 @@ class AttachmentService {
69
69
  name: string
70
70
  contentType: string
71
71
  }): Promise<{ pageMode: 'logical' | 'pdf'; pages: string[] }> {
72
- return await attachmentStorageService.extractStoredAttachmentPages({ storageKey, name, contentType })
72
+ return attachmentStorageService.extractStoredAttachmentPages({ storageKey, name, contentType })
73
73
  }
74
74
 
75
75
  async readFilePartsFromUpload({
@@ -85,7 +85,7 @@ class AttachmentService {
85
85
  part?: number
86
86
  pagesPerPart?: number
87
87
  }) {
88
- return await attachmentStorageService.readFilePartsFromUpload({
88
+ return attachmentStorageService.readFilePartsFromUpload({
89
89
  upload,
90
90
  orgId: toOrgId(orgId),
91
91
  userId: toUserId(userId),
@@ -111,7 +111,7 @@ class AttachmentService {
111
111
  content: string
112
112
  contentType: string
113
113
  }): Promise<{ storageKey: string; sizeBytes: number }> {
114
- return await attachmentStorageService.writeOrganizationDocument({
114
+ return attachmentStorageService.writeOrganizationDocument({
115
115
  orgId: toOrgId(orgId),
116
116
  namespace,
117
117
  relativePath,
@@ -131,12 +131,7 @@ class AttachmentService {
131
131
  namespace: string
132
132
  relativePath: string
133
133
  }) {
134
- return await attachmentStorageService.uploadOrganizationDocument({
135
- file,
136
- orgId: toOrgId(orgId),
137
- namespace,
138
- relativePath,
139
- })
134
+ return attachmentStorageService.uploadOrganizationDocument({ file, orgId: toOrgId(orgId), namespace, relativePath })
140
135
  }
141
136
 
142
137
  async uploadWorkstreamAttachment({
@@ -148,7 +143,7 @@ class AttachmentService {
148
143
  orgId: RecordIdRef
149
144
  userId: RecordIdRef
150
145
  }): Promise<SdkUploadedWorkstreamAttachment> {
151
- return await attachmentStorageService.uploadWorkstreamAttachment({
146
+ return attachmentStorageService.uploadWorkstreamAttachment({
152
147
  file,
153
148
  orgId: toOrgId(orgId),
154
149
  userId: toUserId(userId),
@@ -5,10 +5,12 @@ import type { RecordIdRef } from '../db/record-id'
5
5
  import { recordIdToString } from '../db/record-id'
6
6
  import { databaseService } from '../db/service'
7
7
  import { TABLES } from '../db/tables'
8
+ import { getRedisConnection } from '../redis/connection-accessor'
9
+ import { withRedisLeaseLock } from '../redis/redis-lease-lock'
8
10
  import { parseWorkstreamState, toStateFieldsUpdated } from '../runtime/context-compaction'
9
- import { CONTEXT_SIZE, WORKSTREAM_RAW_TAIL_MESSAGES } from '../runtime/context-compaction-constants'
11
+ import { CONTEXT_WINDOW_TOKENS, WORKSTREAM_RAW_TAIL_MESSAGES } from '../runtime/context-compaction-constants'
10
12
  import type { WorkstreamState } from '../runtime/workstream-state'
11
- import { compactMemoryBlockSummary, contextCompactionRuntime } from './context-compaction-runtime'
13
+ import { compactMemoryBlockSummary, contextCompactionRuntime } from './context-compaction-runtime.singleton'
12
14
  import { workstreamMessageService } from './workstream-message.service'
13
15
  import { WorkstreamSchema } from './workstream.types'
14
16
 
@@ -35,7 +37,7 @@ class ContextCompactionService {
35
37
  return contextCompactionRuntime.formatWorkstreamStateForPrompt(state)
36
38
  }
37
39
 
38
- estimateThreshold(contextSize = CONTEXT_SIZE): number {
40
+ estimateThreshold(contextSize = CONTEXT_WINDOW_TOKENS): number {
39
41
  return contextCompactionRuntime.estimateThreshold(contextSize)
40
42
  }
41
43
 
@@ -47,68 +49,83 @@ class ContextCompactionService {
47
49
  workstreamId: RecordIdRef
48
50
  contextSize?: number
49
51
  }): Promise<{ compacted: boolean; state: WorkstreamState | null }> {
50
- const workstream = await databaseService.findOne(TABLES.WORKSTREAM, { id: params.workstreamId }, WorkstreamSchema)
51
- if (!workstream) {
52
- throw new Error(
53
- `Workstream not found for compaction: ${recordIdToString(params.workstreamId, TABLES.WORKSTREAM)}`,
54
- )
55
- }
56
-
57
- const currentState = parseWorkstreamState(workstream.state)
58
- const liveMessages = await workstreamMessageService.listMessagesAfterCursor(
59
- params.workstreamId,
60
- typeof workstream.lastCompactedMessageId === 'string' ? workstream.lastCompactedMessageId : undefined,
61
- )
52
+ const entityId = recordIdToString(params.workstreamId, TABLES.WORKSTREAM)
62
53
 
63
- const result = await contextCompactionRuntime.compactHistory({
64
- summaryText: typeof workstream.compactionSummary === 'string' ? workstream.compactionSummary : '',
65
- liveMessages,
66
- tailMessageCount: WORKSTREAM_RAW_TAIL_MESSAGES,
67
- contextSize: params.contextSize,
68
- existingState: currentState,
69
- })
70
-
71
- if (!result.compacted || !result.lastCompactedMessageId) {
72
- return { compacted: false, state: currentState }
73
- }
74
-
75
- if (result.compactedMessages.length > 0) {
76
- await workstreamMessageService.upsertMessages({
77
- workstreamId: params.workstreamId,
78
- messages: result.compactedMessages,
79
- })
80
- }
81
-
82
- await databaseService.update(
83
- TABLES.WORKSTREAM,
84
- params.workstreamId,
54
+ return withRedisLeaseLock(
85
55
  {
86
- compactionSummary: result.summaryText,
87
- lastCompactedMessageId: result.lastCompactedMessageId,
88
- state: result.state,
56
+ redis: getRedisConnection(),
57
+ lockKey: `compaction:lock:${entityId}`,
58
+ lockTtlMs: 120_000,
59
+ maxWaitMs: 30_000,
60
+ label: 'context-compaction',
61
+ },
62
+ async () => {
63
+ const workstream = await databaseService.findOne(
64
+ TABLES.WORKSTREAM,
65
+ { id: params.workstreamId },
66
+ WorkstreamSchema,
67
+ )
68
+ if (!workstream) {
69
+ throw new Error(`Workstream not found for compaction: ${entityId}`)
70
+ }
71
+
72
+ const currentState = parseWorkstreamState(workstream.state)
73
+ const liveMessages = await workstreamMessageService.listMessagesAfterCursor(
74
+ params.workstreamId,
75
+ typeof workstream.lastCompactedMessageId === 'string' ? workstream.lastCompactedMessageId : undefined,
76
+ )
77
+
78
+ const result = await contextCompactionRuntime.compactHistory({
79
+ summaryText: typeof workstream.compactionSummary === 'string' ? workstream.compactionSummary : '',
80
+ liveMessages,
81
+ tailMessageCount: WORKSTREAM_RAW_TAIL_MESSAGES,
82
+ contextSize: params.contextSize,
83
+ existingState: currentState,
84
+ })
85
+
86
+ if (!result.compacted || !result.lastCompactedMessageId) {
87
+ return { compacted: false, state: currentState }
88
+ }
89
+
90
+ if (result.compactedMessages.length > 0) {
91
+ await workstreamMessageService.upsertMessages({
92
+ workstreamId: params.workstreamId,
93
+ messages: result.compactedMessages,
94
+ })
95
+ }
96
+
97
+ await databaseService.update(
98
+ TABLES.WORKSTREAM,
99
+ params.workstreamId,
100
+ {
101
+ compactionSummary: result.summaryText,
102
+ lastCompactedMessageId: result.lastCompactedMessageId,
103
+ state: result.state,
104
+ },
105
+ WorkstreamSchema,
106
+ )
107
+
108
+ this.logCompactionMetrics({
109
+ domain: 'workstream',
110
+ entityId,
111
+ inputChars: result.inputChars,
112
+ outputChars: result.outputChars,
113
+ savedChars: Math.max(0, result.inputChars - result.outputChars),
114
+ summaryLength: result.summaryText.length,
115
+ compactedMessageCount: result.compactedMessageCount,
116
+ remainingMessageCount: result.remainingMessageCount,
117
+ estimatedTokens: result.estimatedTokens,
118
+ stateFieldsUpdated: toStateFieldsUpdated(result.stateDelta),
119
+ conflictsDetected: result.stateDelta.conflicts?.length ?? 0,
120
+ })
121
+
122
+ return { compacted: true, state: result.state }
89
123
  },
90
- WorkstreamSchema,
91
124
  )
92
-
93
- this.logCompactionMetrics({
94
- domain: 'workstream',
95
- entityId: recordIdToString(params.workstreamId, TABLES.WORKSTREAM),
96
- inputChars: result.inputChars,
97
- outputChars: result.outputChars,
98
- savedChars: Math.max(0, result.inputChars - result.outputChars),
99
- summaryLength: result.summaryText.length,
100
- compactedMessageCount: result.compactedMessageCount,
101
- remainingMessageCount: result.remainingMessageCount,
102
- estimatedTokens: result.estimatedTokens,
103
- stateFieldsUpdated: toStateFieldsUpdated(result.stateDelta),
104
- conflictsDetected: result.stateDelta.conflicts?.length ?? 0,
105
- })
106
-
107
- return { compacted: true, state: result.state }
108
125
  }
109
126
 
110
127
  async compactMemoryBlock(params: { previousSummary: string; newEntriesText: string }): Promise<string> {
111
- return await compactMemoryBlockSummary(params)
128
+ return compactMemoryBlockSummary(params)
112
129
  }
113
130
 
114
131
  private logCompactionMetrics(metrics: PersistedCompactionMetrics): void {