@lota-sdk/core 0.1.15 → 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 (138) 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 +8 -7
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/index.ts +0 -2
  11. package/src/bifrost/bifrost.ts +2 -7
  12. package/src/config/agent-defaults.ts +31 -21
  13. package/src/config/agent-types.ts +11 -0
  14. package/src/config/constants.ts +2 -14
  15. package/src/config/debug-logger.ts +5 -1
  16. package/src/config/index.ts +3 -0
  17. package/src/config/model-constants.ts +16 -34
  18. package/src/config/search.ts +1 -15
  19. package/src/create-runtime.ts +244 -178
  20. package/src/db/cursor-pagination.ts +3 -6
  21. package/src/db/index.ts +2 -0
  22. package/src/db/memory-store.rows.ts +7 -7
  23. package/src/db/memory-store.ts +14 -18
  24. package/src/db/memory.ts +13 -13
  25. package/src/db/service.ts +153 -79
  26. package/src/db/startup.ts +6 -10
  27. package/src/db/surreal-mutation.ts +43 -0
  28. package/src/db/tables.ts +7 -0
  29. package/src/db/workstream-message-row.ts +15 -0
  30. package/src/embeddings/provider.ts +1 -1
  31. package/src/queues/context-compaction.queue.ts +15 -46
  32. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  33. package/src/queues/index.ts +3 -0
  34. package/src/queues/memory-consolidation.queue.ts +16 -51
  35. package/src/queues/plan-scheduler.queue.ts +97 -0
  36. package/src/queues/post-chat-memory.queue.ts +15 -56
  37. package/src/queues/queue-factory.ts +100 -0
  38. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  39. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  40. package/src/queues/skill-extraction.queue.ts +15 -47
  41. package/src/queues/workstream-title-generation.queue.ts +15 -47
  42. package/src/redis/connection.ts +6 -0
  43. package/src/redis/index.ts +1 -1
  44. package/src/redis/stream-context.ts +11 -0
  45. package/src/runtime/agent-runtime-policy.ts +106 -21
  46. package/src/runtime/approval-continuation.ts +12 -6
  47. package/src/runtime/context-compaction-runtime.ts +1 -1
  48. package/src/runtime/context-compaction.ts +22 -60
  49. package/src/runtime/execution-plan.ts +22 -18
  50. package/src/runtime/graph-designer.ts +15 -0
  51. package/src/runtime/helper-model.ts +9 -197
  52. package/src/runtime/index.ts +2 -0
  53. package/src/runtime/llm-content.ts +1 -1
  54. package/src/runtime/memory-block.ts +9 -11
  55. package/src/runtime/memory-pipeline.ts +6 -9
  56. package/src/runtime/plugin-resolution.ts +35 -0
  57. package/src/runtime/plugin-types.ts +72 -0
  58. package/src/runtime/retrieval-adapters.ts +1 -1
  59. package/src/runtime/runtime-config.ts +25 -12
  60. package/src/runtime/runtime-extensions.ts +2 -2
  61. package/src/runtime/runtime-worker-registry.ts +6 -0
  62. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  63. package/src/runtime/team-consultation-prompts.ts +11 -2
  64. package/src/runtime/title-helpers.ts +2 -4
  65. package/src/runtime/workstream-chat-helpers.ts +1 -1
  66. package/src/services/adaptive-playbook.service.ts +152 -0
  67. package/src/services/agent-executor.service.ts +293 -0
  68. package/src/services/artifact-provenance.service.ts +172 -0
  69. package/src/services/attachment.service.ts +6 -11
  70. package/src/services/context-compaction.service.ts +72 -55
  71. package/src/services/context-enrichment.service.ts +33 -0
  72. package/src/services/coordination-registry.service.ts +117 -0
  73. package/src/services/document-chunk.service.ts +1 -1
  74. package/src/services/domain-agent-executor.service.ts +71 -0
  75. package/src/services/execution-plan.service.ts +269 -50
  76. package/src/services/feedback-loop.service.ts +96 -0
  77. package/src/services/global-orchestrator.service.ts +148 -0
  78. package/src/services/index.ts +26 -0
  79. package/src/services/institutional-memory.service.ts +145 -0
  80. package/src/services/learned-skill.service.ts +24 -5
  81. package/src/services/memory-assessment.service.ts +3 -2
  82. package/src/services/memory-utils.ts +3 -8
  83. package/src/services/memory.service.ts +42 -59
  84. package/src/services/monitoring-window.service.ts +86 -0
  85. package/src/services/mutating-approval.service.ts +1 -1
  86. package/src/services/node-workspace.service.ts +155 -0
  87. package/src/services/notification.service.ts +39 -0
  88. package/src/services/organization-member.service.ts +11 -4
  89. package/src/services/organization.service.ts +5 -5
  90. package/src/services/ownership-dispatcher.service.ts +403 -0
  91. package/src/services/plan-approval.service.ts +1 -1
  92. package/src/services/plan-builder.service.ts +1 -0
  93. package/src/services/plan-checkpoint.service.ts +30 -2
  94. package/src/services/plan-compiler.service.ts +5 -0
  95. package/src/services/plan-coordination.service.ts +152 -0
  96. package/src/services/plan-cycle.service.ts +284 -0
  97. package/src/services/plan-deadline.service.ts +287 -0
  98. package/src/services/plan-executor.service.ts +384 -40
  99. package/src/services/plan-run.service.ts +41 -7
  100. package/src/services/plan-scheduler.service.ts +240 -0
  101. package/src/services/plan-template.service.ts +117 -0
  102. package/src/services/plan-validator.service.ts +84 -2
  103. package/src/services/plan-workspace.service.ts +83 -0
  104. package/src/services/playbook-registry.service.ts +67 -0
  105. package/src/services/plugin-executor.service.ts +103 -0
  106. package/src/services/quality-metrics.service.ts +132 -0
  107. package/src/services/recent-activity.service.ts +27 -31
  108. package/src/services/skill-resolver.service.ts +19 -0
  109. package/src/services/system-executor.service.ts +105 -0
  110. package/src/services/workstream-message.service.ts +12 -34
  111. package/src/services/workstream-plan-registry.service.ts +22 -0
  112. package/src/services/workstream-title.service.ts +3 -1
  113. package/src/services/workstream-turn-preparation.service.ts +34 -66
  114. package/src/services/workstream.service.ts +33 -55
  115. package/src/services/workstream.types.ts +9 -9
  116. package/src/services/write-intent-validator.service.ts +81 -0
  117. package/src/storage/attachment-parser.ts +1 -1
  118. package/src/storage/attachment-utils.ts +1 -1
  119. package/src/storage/generated-document-storage.service.ts +3 -2
  120. package/src/system-agents/delegated-agent-factory.ts +2 -0
  121. package/src/tools/execution-plan.tool.ts +17 -23
  122. package/src/tools/index.ts +0 -1
  123. package/src/tools/team-think.tool.ts +6 -4
  124. package/src/utils/async.ts +2 -1
  125. package/src/utils/date-time.ts +4 -32
  126. package/src/utils/env.ts +8 -0
  127. package/src/utils/errors.ts +42 -10
  128. package/src/utils/index.ts +9 -0
  129. package/src/utils/string.ts +114 -1
  130. package/src/workers/index.ts +1 -0
  131. package/src/workers/regular-chat-memory-digest.runner.ts +2 -2
  132. package/src/workers/skill-extraction.runner.ts +1 -1
  133. package/src/workers/utils/file-section-chunker.ts +2 -1
  134. package/src/workers/utils/repomix-file-sections.ts +2 -2
  135. package/src/workers/utils/sandbox-error.ts +11 -2
  136. package/src/workers/utils/workstream-message-query.ts +11 -20
  137. package/src/workers/worker-utils.ts +2 -2
  138. package/src/tools/log-hello-world.tool.ts +0 -17
@@ -1,17 +1,17 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  import type { CoreWorkstreamProfile } from '../config/agent-defaults'
4
+ import type { AgentFactory, AgentRuntimeConfigProvider, AgentToolBuilder } from '../config/agent-types'
4
5
  import type { LotaWorkstreamConfig, WorkstreamBootstrapWelcomeConfig } from '../config/workstream-defaults'
5
- import type { LotaPlugin } from './plugin-types'
6
+ import type { NotificationService } from '../services/notification.service'
7
+ import { isRecord } from '../utils/string'
8
+ import type { GraphDesigner } from './graph-designer'
9
+ import type { LotaPlugin, SystemNodeExecutor } from './plugin-types'
6
10
  import type { LotaRuntimeAdapters, LotaRuntimeTurnHooks } from './runtime-extensions'
7
11
  import type { LotaRuntimeWorkerExtensions } from './runtime-worker-registry'
8
12
 
9
13
  const logLevelValues = ['trace', 'debug', 'info', 'warning', 'error', 'fatal'] as const
10
14
 
11
- type LotaAgentFactoryRegistry = Record<string, (...args: unknown[]) => unknown>
12
-
13
- const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null
14
-
15
15
  function isStringOrUrl(value: unknown): value is string | URL {
16
16
  return typeof value === 'string' || value instanceof URL
17
17
  }
@@ -24,7 +24,7 @@ function isStringRecord(value: unknown): value is Record<string, string> {
24
24
  return isRecord(value) && Object.values(value).every((entry) => typeof entry === 'string')
25
25
  }
26
26
 
27
- function isAgentFactoryRegistry(value: unknown): value is LotaAgentFactoryRegistry {
27
+ function isAgentFactoryRegistry(value: unknown): value is AgentFactory {
28
28
  return isRecord(value) && Object.values(value).every((entry) => typeof entry === 'function')
29
29
  }
30
30
 
@@ -32,10 +32,22 @@ function isPluginRuntimeRecord(value: unknown): value is Record<string, LotaPlug
32
32
  return isRecord(value)
33
33
  }
34
34
 
35
+ function isSystemExecutorRecord(value: unknown): value is Record<string, SystemNodeExecutor> {
36
+ return isRecord(value)
37
+ }
38
+
35
39
  function isToolProviderRecord(value: unknown): value is Record<string, unknown> {
36
40
  return isRecord(value)
37
41
  }
38
42
 
43
+ function isNotificationService(value: unknown): value is NotificationService {
44
+ return isRecord(value) && isFunction(value.notify) && isFunction(value.remind) && isFunction(value.escalate)
45
+ }
46
+
47
+ function isGraphDesigner(value: unknown): value is GraphDesigner {
48
+ return isRecord(value) && isFunction(value.designGraph)
49
+ }
50
+
39
51
  function isWorkerExtensionRecord(value: unknown): value is LotaRuntimeWorkerExtensions {
40
52
  if (!isRecord(value)) return false
41
53
 
@@ -85,15 +97,13 @@ const agentsConfigSchema = z
85
97
  })
86
98
  .optional(),
87
99
  createAgent: z
88
- .custom<LotaAgentFactoryRegistry>(isAgentFactoryRegistry, {
89
- error: 'agents.createAgent must be a function registry',
90
- })
100
+ .custom<AgentFactory>(isAgentFactoryRegistry, { error: 'agents.createAgent must be a function registry' })
91
101
  .optional(),
92
102
  buildAgentTools: z
93
- .custom<(...args: unknown[]) => unknown>(isFunction, { error: 'agents.buildAgentTools must be a function' })
103
+ .custom<AgentToolBuilder>(isFunction, { error: 'agents.buildAgentTools must be a function' })
94
104
  .optional(),
95
105
  getAgentRuntimeConfig: z
96
- .custom<(...args: unknown[]) => unknown>(isFunction, { error: 'agents.getAgentRuntimeConfig must be a function' })
106
+ .custom<AgentRuntimeConfigProvider>(isFunction, { error: 'agents.getAgentRuntimeConfig must be a function' })
97
107
  .optional(),
98
108
  })
99
109
  .superRefine((value, ctx) => {
@@ -173,8 +183,11 @@ export const LotaRuntimeConfigSchema = z.object({
173
183
  extraSchemaFiles: z.array(z.custom<string | URL>(isStringOrUrl)).optional(),
174
184
  extraWorkers: z.custom<LotaRuntimeWorkerExtensions>(isWorkerExtensionRecord).optional(),
175
185
  pluginRuntime: z.custom<Record<string, LotaPlugin>>(isPluginRuntimeRecord).optional(),
186
+ systemExecutors: z.custom<Record<string, SystemNodeExecutor>>(isSystemExecutorRecord).optional(),
187
+ notificationService: z.custom<NotificationService>(isNotificationService).optional(),
176
188
  runtimeAdapters: z.custom<LotaRuntimeAdapters>(isRecord).optional(),
177
189
  turnHooks: z.custom<LotaRuntimeTurnHooks>(isRecord).optional(),
190
+ graphDesigner: z.custom<GraphDesigner>(isGraphDesigner).optional(),
178
191
  })
179
192
 
180
193
  export type LotaRuntimeConfig = z.input<typeof LotaRuntimeConfigSchema>
@@ -235,4 +248,4 @@ export function parseWorkerBootstrapEnv(env: Record<string, string | undefined>)
235
248
  return WORKER_BOOTSTRAP_ENV_SCHEMA.parse(env)
236
249
  }
237
250
 
238
- export type { LotaAgentFactoryRegistry, LotaWorkstreamConfig }
251
+ export type { LotaWorkstreamConfig }
@@ -224,8 +224,8 @@ export function getConfiguredPluginDatabaseConnector(): (() => Promise<void>) |
224
224
  export async function withConfiguredWorkspaceMemoryLock<T>(workspaceId: string, fn: () => Promise<T>): Promise<T> {
225
225
  const adapter = runtimeExtensionsState.adapters.workers?.withWorkspaceMemoryLock
226
226
  if (!adapter) {
227
- return await fn()
227
+ return fn()
228
228
  }
229
229
 
230
- return await adapter(workspaceId, fn)
230
+ return adapter(workspaceId, fn)
231
231
  }
@@ -1,5 +1,7 @@
1
1
  import { startContextCompactionWorker } from '../queues/context-compaction.queue'
2
+ import { startDelayedNodePromotionWorker } from '../queues/delayed-node-promotion.queue'
2
3
  import { scheduleRecurringConsolidation, startMemoryConsolidationWorker } from '../queues/memory-consolidation.queue'
4
+ import { startPlanSchedulerWorker } from '../queues/plan-scheduler.queue'
3
5
  import { startPostChatMemoryWorker } from '../queues/post-chat-memory.queue'
4
6
  import { startRecentActivityTitleRefinementWorker } from '../queues/recent-activity-title-refinement.queue'
5
7
  import { startRegularChatMemoryDigestWorker } from '../queues/regular-chat-memory-digest.queue'
@@ -8,7 +10,9 @@ import { startWorkstreamTitleGenerationWorker } from '../queues/workstream-title
8
10
 
9
11
  export interface LotaRuntimeWorkerStartRegistry {
10
12
  contextCompaction: typeof startContextCompactionWorker
13
+ delayedNodePromotion: typeof startDelayedNodePromotionWorker
11
14
  memoryConsolidation: typeof startMemoryConsolidationWorker
15
+ planScheduler: typeof startPlanSchedulerWorker
12
16
  postChatMemory: typeof startPostChatMemoryWorker
13
17
  regularChatMemoryDigest: typeof startRegularChatMemoryDigestWorker
14
18
  skillExtraction: typeof startSkillExtractionWorker
@@ -34,7 +38,9 @@ export function buildRuntimeWorkerRegistry(extraWorkers?: LotaRuntimeWorkerExten
34
38
  return {
35
39
  start: {
36
40
  contextCompaction: startContextCompactionWorker,
41
+ delayedNodePromotion: startDelayedNodePromotionWorker,
37
42
  memoryConsolidation: startMemoryConsolidationWorker,
43
+ planScheduler: startPlanSchedulerWorker,
38
44
  postChatMemory: startPostChatMemoryWorker,
39
45
  regularChatMemoryDigest: startRegularChatMemoryDigestWorker,
40
46
  skillExtraction: startSkillExtractionWorker,
@@ -1,6 +1,6 @@
1
1
  import { ConsultTeamArgsSchema, withMessageCreatedAt } from '@lota-sdk/shared'
2
2
  import type { ChatMessage, ConsultTeamResultData } from '@lota-sdk/shared'
3
- import { convertToModelMessages, readUIMessageStream, tool as createTool } from 'ai'
3
+ import { convertToModelMessages, tool as createTool } from 'ai'
4
4
 
5
5
  import { agentDisplayNames, teamConsultParticipants } from '../config/agent-defaults'
6
6
  import { createTimedAbortSignal } from './agent-stream-helpers'
@@ -33,6 +33,28 @@ function getConsultTeamOutput(output: unknown): ConsultTeamResultData | undefine
33
33
  return undefined
34
34
  }
35
35
 
36
+ function findLastUserMessage(
37
+ messages: ChatMessage[],
38
+ predicate: (message: ChatMessage) => boolean,
39
+ ): ChatMessage | null {
40
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
41
+ const message = messages[index]
42
+ if (predicate(message)) {
43
+ return message
44
+ }
45
+ }
46
+
47
+ return null
48
+ }
49
+
50
+ function selectTeamConsultationContextMessages(messages: ChatMessage[], latestUserMessageId: string): ChatMessage[] {
51
+ const latestUserMessage =
52
+ findLastUserMessage(messages, (message) => message.id === latestUserMessageId && message.role === 'user') ??
53
+ findLastUserMessage(messages, (message) => message.role === 'user')
54
+
55
+ return latestUserMessage ? [latestUserMessage] : []
56
+ }
57
+
36
58
  interface ParticipantObserver {
37
59
  run<T>(fn: () => T | Promise<T>): Promise<T>
38
60
  recordError?: (error: unknown) => void
@@ -51,6 +73,7 @@ export interface TeamConsultationParticipantRunner {
51
73
  },
52
74
  ): Promise<{
53
75
  agent: {
76
+ generate(params: Record<string, unknown>): Promise<{ text: string }>
54
77
  stream(
55
78
  params: Record<string, unknown>,
56
79
  ): Promise<{
@@ -119,16 +142,22 @@ export function createConsultTeamTool(params: CreateConsultTeamToolParams) {
119
142
  })
120
143
  const modelMessages = await convertToModelMessages(
121
144
  buildModelInputMessagesWithUploadMetadata({
122
- messages: params.historyMessages,
145
+ messages: selectTeamConsultationContextMessages(params.historyMessages, params.latestUserMessageId),
123
146
  latestUserMessageId: params.latestUserMessageId,
124
147
  uploadMetadataText,
125
148
  }),
126
149
  { ignoreIncompleteToolCalls: true },
127
150
  )
128
151
 
129
- let result: Awaited<ReturnType<typeof agent.stream>>
152
+ let result: Awaited<ReturnType<typeof agent.generate>>
130
153
  try {
131
- result = await observer.run(() => agent.stream({ messages: modelMessages, abortSignal: timedAbort.signal }))
154
+ result = await observer.run(() =>
155
+ agent.generate({
156
+ messages: modelMessages,
157
+ abortSignal: timedAbort.signal,
158
+ timeout: TEAM_CONSULTATION_TIMEOUT_MS,
159
+ }),
160
+ )
132
161
  } catch (error) {
133
162
  if (params.abortSignal.aborted || timedAbort.signal.aborted) {
134
163
  observer.recordAbort?.(error)
@@ -138,33 +167,21 @@ export function createConsultTeamTool(params: CreateConsultTeamToolParams) {
138
167
  throw error
139
168
  }
140
169
 
141
- for await (const message of readUIMessageStream<ChatMessage>({
142
- stream: result.toUIMessageStream({
143
- generateMessageId: () => Bun.randomUUIDv7(),
144
- sendReasoning: true,
145
- sendSources: true,
146
- sendStart: false,
147
- sendFinish: false,
148
- }) as ReadableStream<never>,
149
- onError: (error) => {
150
- params.onReadError?.(agentId, error)
151
- },
152
- })) {
153
- latestMessage = withMessageCreatedAt(message)
154
- responses[index] = {
155
- agentId,
156
- agentName,
157
- status: 'running',
158
- summary: extractMessageText(latestMessage).trim() || undefined,
159
- message: latestMessage,
160
- }
161
- pushSnapshot()
162
- }
163
-
164
- if (!latestMessage) {
170
+ const responseText = result.text.trim()
171
+ if (!responseText) {
165
172
  throw new Error(`Team participant ${agentId} did not produce a response.`)
166
173
  }
167
174
 
175
+ latestMessage = withMessageCreatedAt(
176
+ {
177
+ id: Bun.randomUUIDv7(),
178
+ role: 'assistant',
179
+ parts: [{ type: 'text', text: responseText }],
180
+ metadata: { agentId, agentName },
181
+ } satisfies ChatMessage,
182
+ Date.now(),
183
+ )
184
+
168
185
  responses[index] = {
169
186
  agentId,
170
187
  agentName,
@@ -3,12 +3,21 @@ import { agentDisplayNames, getLeadAgentDisplayName } from '../config/agent-defa
3
3
  export function buildTeamConsultationResponseGuard(params: { agentId: string; task: string }) {
4
4
  const agentName = agentDisplayNames[params.agentId] ?? params.agentId
5
5
  const leadAgentDisplayName = getLeadAgentDisplayName()
6
+ const mentorConstraint =
7
+ params.agentId === 'mentor'
8
+ ? ['- As Mentor, answer as an experienced operator reviewing launch discipline, not as a coach or therapist.']
9
+ : []
6
10
  return [
7
11
  '<team-consultation-agent-protocol>',
8
12
  `- You are participating in a structured internal team consultation led by ${leadAgentDisplayName}.`,
9
13
  `- Your role for this response is ${agentName}.`,
10
- '- Use markdown when it helps clarity.',
11
- '- Make the recommendation, explain key tradeoffs, and note the next decision.',
14
+ '- Chief is already coordinating this panel. Do not refer the task back to Chief or tell the user to consult another agent.',
15
+ '- Answer only from your role-specific perspective for the task below.',
16
+ '- Do not coach the user, ask follow-up questions, or challenge assumptions in this panel response. Give the recommendation directly.',
17
+ ...mentorConstraint,
18
+ '- Return exactly 3 short markdown bullets in this order: recommendation, key risk/tradeoff, next decision.',
19
+ '- Keep the whole response under 120 words.',
20
+ '- Do not use headings, code fences, XML, evidence blocks, gap lists, or preamble text.',
12
21
  '',
13
22
  '<team-consultation-task>',
14
23
  params.task.trim(),
@@ -1,4 +1,4 @@
1
- import { compactWhitespace } from '../utils/string'
1
+ import { compactWhitespace, truncateText } from '../utils/string'
2
2
 
3
3
  const TITLE_WORD_LIMIT = 5
4
4
 
@@ -8,9 +8,7 @@ export function limitTitleWords(text: string): string {
8
8
  }
9
9
 
10
10
  export function deriveTitle(text: string): string {
11
- const trimmed = compactWhitespace(text)
12
- if (trimmed.length <= 60) return trimmed
13
- return `${trimmed.slice(0, 57)}...`
11
+ return truncateText(compactWhitespace(text), 60)
14
12
  }
15
13
 
16
14
  export function normalizeTitle(value: string): string {
@@ -77,7 +77,7 @@ export function appendPersistedWorkstreamContextToHistoryMessages(
77
77
  nextHistoryMessages.push({ role: 'agent', content: `Compacted chat summary:\n${compactionSummary}` })
78
78
  }
79
79
 
80
- if (params.persistedState !== undefined && params.persistedState !== null) {
80
+ if (params.persistedState !== null && params.persistedState !== undefined) {
81
81
  nextHistoryMessages.push({
82
82
  role: 'agent',
83
83
  content: `Structured workstream state:\n${JSON.stringify(params.persistedState)}`,
@@ -0,0 +1,152 @@
1
+ import type { PlaybookVersion, Recommendation } from '@lota-sdk/shared'
2
+ import { PlaybookSchema, PlaybookVersionSchema } from '@lota-sdk/shared'
3
+
4
+ import { ensureRecordId, recordIdToString } from '../db/record-id'
5
+ import { databaseService } from '../db/service'
6
+ import { TABLES } from '../db/tables'
7
+
8
+ class AdaptivePlaybookService {
9
+ async refineFromCycle(params: {
10
+ playbookId: string
11
+ runId: string
12
+ recommendations: Recommendation[]
13
+ organizationId: string
14
+ }): Promise<PlaybookVersion> {
15
+ const playbook = await databaseService.findOne(
16
+ TABLES.PLAYBOOK,
17
+ { id: ensureRecordId(params.playbookId, TABLES.PLAYBOOK) },
18
+ PlaybookSchema,
19
+ )
20
+ if (!playbook) {
21
+ throw new Error(`Playbook not found: ${params.playbookId}`)
22
+ }
23
+
24
+ const currentVersion = await databaseService.findOne(
25
+ TABLES.PLAYBOOK_VERSION,
26
+ {
27
+ id: ensureRecordId(
28
+ recordIdToString(playbook.currentVersionId, TABLES.PLAYBOOK_VERSION),
29
+ TABLES.PLAYBOOK_VERSION,
30
+ ),
31
+ },
32
+ PlaybookVersionSchema,
33
+ )
34
+
35
+ const nextVersionNumber = currentVersion ? currentVersion.version + 1 : 1
36
+ const now = new Date()
37
+
38
+ const newVersion = await databaseService.create(
39
+ TABLES.PLAYBOOK_VERSION,
40
+ {
41
+ playbookId: ensureRecordId(params.playbookId, TABLES.PLAYBOOK),
42
+ version: nextVersionNumber,
43
+ parentVersionId: playbook.currentVersionId,
44
+ appliedRecommendations: params.recommendations.map((r) => r.description),
45
+ status: 'testing',
46
+ createdAt: now,
47
+ },
48
+ PlaybookVersionSchema,
49
+ )
50
+
51
+ await databaseService.update(
52
+ TABLES.PLAYBOOK,
53
+ ensureRecordId(params.playbookId, TABLES.PLAYBOOK),
54
+ {
55
+ currentVersionId: newVersion.id,
56
+ previousVersionId: playbook.currentVersionId,
57
+ cycleCount: playbook.cycleCount + 1,
58
+ },
59
+ PlaybookSchema,
60
+ )
61
+
62
+ return newVersion
63
+ }
64
+
65
+ evaluateRegression(params: { currentScore: number; previousScore: number; threshold?: number }): {
66
+ shouldRollback: boolean
67
+ } {
68
+ const threshold = params.threshold ?? 0.9
69
+ if (params.previousScore === 0) return { shouldRollback: false }
70
+ return { shouldRollback: params.currentScore < params.previousScore * threshold }
71
+ }
72
+
73
+ async rollback(params: {
74
+ playbookId: string
75
+ organizationId: string
76
+ maxLevels?: number
77
+ }): Promise<PlaybookVersion | null> {
78
+ const maxLevels = params.maxLevels ?? 3
79
+ return this.rollbackRecursive(params.playbookId, params.organizationId, maxLevels)
80
+ }
81
+
82
+ private async rollbackRecursive(
83
+ playbookId: string,
84
+ organizationId: string,
85
+ remainingLevels: number,
86
+ ): Promise<PlaybookVersion | null> {
87
+ if (remainingLevels <= 0) return null
88
+
89
+ const playbook = await databaseService.findOne(
90
+ TABLES.PLAYBOOK,
91
+ { id: ensureRecordId(playbookId, TABLES.PLAYBOOK) },
92
+ PlaybookSchema,
93
+ )
94
+ if (!playbook) return null
95
+
96
+ const currentVersion = await databaseService.findOne(
97
+ TABLES.PLAYBOOK_VERSION,
98
+ {
99
+ id: ensureRecordId(
100
+ recordIdToString(playbook.currentVersionId, TABLES.PLAYBOOK_VERSION),
101
+ TABLES.PLAYBOOK_VERSION,
102
+ ),
103
+ },
104
+ PlaybookVersionSchema,
105
+ )
106
+ if (!currentVersion?.parentVersionId) return null
107
+
108
+ // Mark current version as rolled-back
109
+ await databaseService.update(
110
+ TABLES.PLAYBOOK_VERSION,
111
+ ensureRecordId(recordIdToString(currentVersion.id, TABLES.PLAYBOOK_VERSION), TABLES.PLAYBOOK_VERSION),
112
+ { status: 'rolled-back' },
113
+ PlaybookVersionSchema,
114
+ )
115
+
116
+ // Restore parent version as active
117
+ const parentVersion = await databaseService.findOne(
118
+ TABLES.PLAYBOOK_VERSION,
119
+ {
120
+ id: ensureRecordId(
121
+ recordIdToString(currentVersion.parentVersionId, TABLES.PLAYBOOK_VERSION),
122
+ TABLES.PLAYBOOK_VERSION,
123
+ ),
124
+ },
125
+ PlaybookVersionSchema,
126
+ )
127
+ if (!parentVersion) return null
128
+
129
+ await databaseService.update(
130
+ TABLES.PLAYBOOK_VERSION,
131
+ ensureRecordId(recordIdToString(parentVersion.id, TABLES.PLAYBOOK_VERSION), TABLES.PLAYBOOK_VERSION),
132
+ { status: 'active' },
133
+ PlaybookVersionSchema,
134
+ )
135
+
136
+ await databaseService.update(
137
+ TABLES.PLAYBOOK,
138
+ ensureRecordId(playbookId, TABLES.PLAYBOOK),
139
+ { currentVersionId: parentVersion.id, previousVersionId: parentVersion.parentVersionId },
140
+ PlaybookSchema,
141
+ )
142
+
143
+ // Check if parent version is also regressed (caller can evaluate and re-invoke)
144
+ if (parentVersion.qualityScore !== undefined && parentVersion.qualityScore < 0.5) {
145
+ return this.rollbackRecursive(playbookId, organizationId, remainingLevels - 1)
146
+ }
147
+
148
+ return parentVersion
149
+ }
150
+ }
151
+
152
+ export const adaptivePlaybookService = new AdaptivePlaybookService()