@lota-sdk/core 0.1.15 → 0.1.17

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 (159) 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 +12 -8
  9. package/src/ai/definitions.ts +81 -3
  10. package/src/ai/embedding-cache.ts +2 -4
  11. package/src/ai/index.ts +0 -2
  12. package/src/bifrost/bifrost.ts +2 -7
  13. package/src/bifrost/cache-headers.ts +8 -0
  14. package/src/bifrost/index.ts +1 -0
  15. package/src/config/agent-defaults.ts +31 -21
  16. package/src/config/agent-types.ts +11 -0
  17. package/src/config/constants.ts +2 -14
  18. package/src/config/debug-logger.ts +5 -1
  19. package/src/config/index.ts +3 -0
  20. package/src/config/model-constants.ts +16 -34
  21. package/src/config/search.ts +1 -15
  22. package/src/create-runtime.ts +269 -178
  23. package/src/db/cursor-pagination.ts +3 -6
  24. package/src/db/index.ts +2 -0
  25. package/src/db/memory-store.helpers.ts +1 -3
  26. package/src/db/memory-store.rows.ts +7 -7
  27. package/src/db/memory-store.ts +14 -18
  28. package/src/db/memory.ts +13 -13
  29. package/src/db/schema-fingerprint.ts +1 -3
  30. package/src/db/service.ts +153 -79
  31. package/src/db/startup.ts +6 -10
  32. package/src/db/surreal-mutation.ts +43 -0
  33. package/src/db/tables.ts +7 -0
  34. package/src/db/workstream-message-row.ts +15 -0
  35. package/src/embeddings/provider.ts +1 -1
  36. package/src/queues/context-compaction.queue.ts +15 -46
  37. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  38. package/src/queues/document-processor.queue.ts +2 -4
  39. package/src/queues/index.ts +3 -0
  40. package/src/queues/memory-consolidation.queue.ts +16 -51
  41. package/src/queues/plan-scheduler.queue.ts +97 -0
  42. package/src/queues/post-chat-memory.queue.ts +20 -55
  43. package/src/queues/queue-factory.ts +100 -0
  44. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  45. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  46. package/src/queues/skill-extraction.queue.ts +15 -47
  47. package/src/queues/workstream-title-generation.queue.ts +15 -47
  48. package/src/redis/connection.ts +6 -0
  49. package/src/redis/index.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +1 -2
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +109 -35
  53. package/src/runtime/approval-continuation.ts +12 -6
  54. package/src/runtime/context-compaction-runtime.ts +1 -1
  55. package/src/runtime/context-compaction.ts +24 -64
  56. package/src/runtime/execution-plan.ts +22 -18
  57. package/src/runtime/graph-designer.ts +15 -0
  58. package/src/runtime/helper-model.ts +9 -197
  59. package/src/runtime/index.ts +3 -1
  60. package/src/runtime/llm-content.ts +1 -1
  61. package/src/runtime/memory-block.ts +9 -11
  62. package/src/runtime/memory-pipeline.ts +6 -9
  63. package/src/runtime/plugin-resolution.ts +35 -0
  64. package/src/runtime/plugin-types.ts +72 -0
  65. package/src/runtime/retrieval-adapters.ts +1 -1
  66. package/src/runtime/runtime-config.ts +111 -14
  67. package/src/runtime/runtime-extensions.ts +2 -3
  68. package/src/runtime/runtime-worker-registry.ts +6 -0
  69. package/src/runtime/social-chat.ts +752 -0
  70. package/src/runtime/team-consultation-orchestrator.ts +45 -32
  71. package/src/runtime/team-consultation-prompts.ts +11 -2
  72. package/src/runtime/title-helpers.ts +2 -4
  73. package/src/runtime/workstream-chat-helpers.ts +1 -1
  74. package/src/services/adaptive-playbook.service.ts +152 -0
  75. package/src/services/agent-executor.service.ts +292 -0
  76. package/src/services/artifact-provenance.service.ts +172 -0
  77. package/src/services/attachment.service.ts +6 -11
  78. package/src/services/context-compaction.service.ts +72 -55
  79. package/src/services/context-enrichment.service.ts +33 -0
  80. package/src/services/coordination-registry.service.ts +117 -0
  81. package/src/services/document-chunk.service.ts +2 -4
  82. package/src/services/domain-agent-executor.service.ts +71 -0
  83. package/src/services/execution-plan.service.ts +269 -50
  84. package/src/services/feedback-loop.service.ts +96 -0
  85. package/src/services/global-orchestrator.service.ts +148 -0
  86. package/src/services/index.ts +27 -0
  87. package/src/services/institutional-memory.service.ts +145 -0
  88. package/src/services/learned-skill.service.ts +24 -5
  89. package/src/services/memory-assessment.service.ts +3 -2
  90. package/src/services/memory-utils.ts +3 -8
  91. package/src/services/memory.service.ts +49 -61
  92. package/src/services/monitoring-window.service.ts +86 -0
  93. package/src/services/mutating-approval.service.ts +1 -1
  94. package/src/services/node-workspace.service.ts +155 -0
  95. package/src/services/notification.service.ts +39 -0
  96. package/src/services/organization-member.service.ts +11 -4
  97. package/src/services/organization.service.ts +5 -5
  98. package/src/services/ownership-dispatcher.service.ts +403 -0
  99. package/src/services/plan-approval.service.ts +1 -1
  100. package/src/services/plan-builder.service.ts +1 -0
  101. package/src/services/plan-checkpoint.service.ts +30 -2
  102. package/src/services/plan-compiler.service.ts +5 -0
  103. package/src/services/plan-coordination.service.ts +152 -0
  104. package/src/services/plan-cycle.service.ts +284 -0
  105. package/src/services/plan-deadline.service.ts +287 -0
  106. package/src/services/plan-executor.service.ts +384 -40
  107. package/src/services/plan-run.service.ts +41 -7
  108. package/src/services/plan-scheduler.service.ts +240 -0
  109. package/src/services/plan-template.service.ts +117 -0
  110. package/src/services/plan-validator.service.ts +84 -2
  111. package/src/services/plan-workspace.service.ts +83 -0
  112. package/src/services/playbook-registry.service.ts +67 -0
  113. package/src/services/plugin-executor.service.ts +103 -0
  114. package/src/services/quality-metrics.service.ts +132 -0
  115. package/src/services/recent-activity.service.ts +28 -34
  116. package/src/services/skill-resolver.service.ts +19 -0
  117. package/src/services/social-chat-history.service.ts +197 -0
  118. package/src/services/system-executor.service.ts +105 -0
  119. package/src/services/workstream-message.service.ts +13 -37
  120. package/src/services/workstream-plan-registry.service.ts +22 -0
  121. package/src/services/workstream-title.service.ts +3 -1
  122. package/src/services/workstream-turn-preparation.service.ts +34 -89
  123. package/src/services/workstream.service.ts +33 -55
  124. package/src/services/workstream.types.ts +9 -9
  125. package/src/services/write-intent-validator.service.ts +81 -0
  126. package/src/storage/attachment-parser.ts +1 -1
  127. package/src/storage/attachment-utils.ts +1 -1
  128. package/src/storage/generated-document-storage.service.ts +3 -2
  129. package/src/system-agents/context-compaction.agent.ts +2 -0
  130. package/src/system-agents/delegated-agent-factory.ts +5 -0
  131. package/src/system-agents/memory-reranker.agent.ts +4 -2
  132. package/src/system-agents/memory.agent.ts +2 -0
  133. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -0
  134. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -0
  135. package/src/system-agents/skill-extractor.agent.ts +2 -0
  136. package/src/system-agents/skill-manager.agent.ts +2 -0
  137. package/src/system-agents/title-generator.agent.ts +2 -0
  138. package/src/tools/execution-plan.tool.ts +17 -23
  139. package/src/tools/index.ts +0 -1
  140. package/src/tools/research-topic.tool.ts +2 -0
  141. package/src/tools/team-think.tool.ts +5 -6
  142. package/src/utils/async.ts +2 -1
  143. package/src/utils/date-time.ts +4 -32
  144. package/src/utils/env.ts +8 -0
  145. package/src/utils/errors.ts +42 -10
  146. package/src/utils/index.ts +9 -0
  147. package/src/utils/string.ts +114 -1
  148. package/src/workers/index.ts +1 -0
  149. package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
  150. package/src/workers/regular-chat-memory-digest.runner.ts +45 -12
  151. package/src/workers/skill-extraction.runner.ts +26 -6
  152. package/src/workers/utils/file-section-chunker.ts +2 -1
  153. package/src/workers/utils/repo-structure-extractor.ts +2 -2
  154. package/src/workers/utils/repomix-file-sections.ts +2 -2
  155. package/src/workers/utils/sandbox-error.ts +11 -2
  156. package/src/workers/utils/workstream-message-query.ts +14 -25
  157. package/src/workers/worker-utils.ts +2 -2
  158. package/src/runtime/workstream-routing-policy.ts +0 -267
  159. package/src/tools/log-hello-world.tool.ts +0 -17
@@ -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'
@@ -9,7 +9,6 @@ import type { ReadableUploadMetadataLike } from './chat-attachments'
9
9
  import type { RepoSectionName } from './indexed-repositories-policy'
10
10
  import { buildTeamConsultationFailureMessage } from './team-consultation-prompts'
11
11
  import { extractMessageText } from './workstream-chat-helpers'
12
- import type { ReasoningProfileName } from './workstream-routing-policy'
13
12
 
14
13
  export type DefaultRepoSections = RepoSectionName[]
15
14
  const TEAM_CONSULTATION_TIMEOUT_MS = 90_000
@@ -33,6 +32,28 @@ function getConsultTeamOutput(output: unknown): ConsultTeamResultData | undefine
33
32
  return undefined
34
33
  }
35
34
 
35
+ function findLastUserMessage(
36
+ messages: ChatMessage[],
37
+ predicate: (message: ChatMessage) => boolean,
38
+ ): ChatMessage | null {
39
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
40
+ const message = messages[index]
41
+ if (predicate(message)) {
42
+ return message
43
+ }
44
+ }
45
+
46
+ return null
47
+ }
48
+
49
+ function selectTeamConsultationContextMessages(messages: ChatMessage[], latestUserMessageId: string): ChatMessage[] {
50
+ const latestUserMessage =
51
+ findLastUserMessage(messages, (message) => message.id === latestUserMessageId && message.role === 'user') ??
52
+ findLastUserMessage(messages, (message) => message.role === 'user')
53
+
54
+ return latestUserMessage ? [latestUserMessage] : []
55
+ }
56
+
36
57
  interface ParticipantObserver {
37
58
  run<T>(fn: () => T | Promise<T>): Promise<T>
38
59
  recordError?: (error: unknown) => void
@@ -44,13 +65,13 @@ export interface TeamConsultationParticipantRunner {
44
65
  agentId: string,
45
66
  params: {
46
67
  task: string
47
- reasoningProfile: ReasoningProfileName
48
68
  systemWorkspaceDetails?: string
49
69
  preSeededMemoriesSection?: string
50
70
  retrievedKnowledgeSection?: string
51
71
  },
52
72
  ): Promise<{
53
73
  agent: {
74
+ generate(params: Record<string, unknown>): Promise<{ text: string }>
54
75
  stream(
55
76
  params: Record<string, unknown>,
56
77
  ): Promise<{
@@ -67,7 +88,6 @@ export interface CreateConsultTeamToolParams {
67
88
  latestUserMessageId: string
68
89
  availableUploads: ReadableUploadMetadataLike[]
69
90
  defaultRepoSectionsByAgent: Record<string, DefaultRepoSections | undefined>
70
- reasoningProfile: 'fast' | 'standard' | 'deep'
71
91
  systemWorkspaceDetails?: string
72
92
  getPreSeededMemoriesSection: (agentId: string) => Promise<string | undefined>
73
93
  retrievedKnowledgeSection?: string
@@ -112,23 +132,28 @@ export function createConsultTeamTool(params: CreateConsultTeamToolParams) {
112
132
  try {
113
133
  const { agent, observer } = await params.participantRunner.buildParticipantAgent(agentId, {
114
134
  task,
115
- reasoningProfile: params.reasoningProfile,
116
135
  systemWorkspaceDetails: params.systemWorkspaceDetails,
117
136
  preSeededMemoriesSection: await params.getPreSeededMemoriesSection(agentId),
118
137
  retrievedKnowledgeSection: params.retrievedKnowledgeSection,
119
138
  })
120
139
  const modelMessages = await convertToModelMessages(
121
140
  buildModelInputMessagesWithUploadMetadata({
122
- messages: params.historyMessages,
141
+ messages: selectTeamConsultationContextMessages(params.historyMessages, params.latestUserMessageId),
123
142
  latestUserMessageId: params.latestUserMessageId,
124
143
  uploadMetadataText,
125
144
  }),
126
145
  { ignoreIncompleteToolCalls: true },
127
146
  )
128
147
 
129
- let result: Awaited<ReturnType<typeof agent.stream>>
148
+ let result: Awaited<ReturnType<typeof agent.generate>>
130
149
  try {
131
- result = await observer.run(() => agent.stream({ messages: modelMessages, abortSignal: timedAbort.signal }))
150
+ result = await observer.run(() =>
151
+ agent.generate({
152
+ messages: modelMessages,
153
+ abortSignal: timedAbort.signal,
154
+ timeout: TEAM_CONSULTATION_TIMEOUT_MS,
155
+ }),
156
+ )
132
157
  } catch (error) {
133
158
  if (params.abortSignal.aborted || timedAbort.signal.aborted) {
134
159
  observer.recordAbort?.(error)
@@ -138,33 +163,21 @@ export function createConsultTeamTool(params: CreateConsultTeamToolParams) {
138
163
  throw error
139
164
  }
140
165
 
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) {
166
+ const responseText = result.text.trim()
167
+ if (!responseText) {
165
168
  throw new Error(`Team participant ${agentId} did not produce a response.`)
166
169
  }
167
170
 
171
+ latestMessage = withMessageCreatedAt(
172
+ {
173
+ id: Bun.randomUUIDv7(),
174
+ role: 'assistant',
175
+ parts: [{ type: 'text', text: responseText }],
176
+ metadata: { agentId, agentName },
177
+ } satisfies ChatMessage,
178
+ Date.now(),
179
+ )
180
+
168
181
  responses[index] = {
169
182
  agentId,
170
183
  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()
@@ -0,0 +1,292 @@
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
+ additionalInstructionSections: [
150
+ buildOwnershipDispatchContextSection({
151
+ node: params.nodeSpec,
152
+ resolvedInput: params.resolvedInput,
153
+ inputArtifacts: params.inputArtifacts,
154
+ }),
155
+ ],
156
+ responseGuardSection: buildOwnershipDispatchResponseGuard({ node: params.nodeSpec, executionMode: mode }),
157
+ }) as Record<string, unknown>
158
+
159
+ const rawTools = (await buildAgentTools({
160
+ agentId,
161
+ orgId: organizationRef,
162
+ userId: userRef,
163
+ userName,
164
+ workstreamId: workstreamRef,
165
+ orgIdString: params.context.organizationId,
166
+ workstreamMode: workstream.mode,
167
+ mode: dispatchMode,
168
+ linearInstalled: Boolean(linearInstallation),
169
+ onboardingActive: false,
170
+ githubInstalled: Boolean(githubInstallation),
171
+ provideRepoTool: indexedRepoContext.provideRepoTool,
172
+ defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[agentId],
173
+ memoryBlock: '',
174
+ onAppendMemoryBlock: () => undefined,
175
+ availableUploads: [],
176
+ includeExecutionPlanTools: false,
177
+ })) as ToolSet
178
+ const tools = applyToolPolicy(rawTools, params.nodeSpec)
179
+
180
+ const agentFactory = createAgent[agentId] as ((...args: unknown[]) => unknown) | undefined
181
+ if (!agentFactory) {
182
+ throw new Error(`Agent factory "${agentId}" is not registered.`)
183
+ }
184
+
185
+ const maxSteps = typeof runtimeConfig.maxSteps === 'number' ? runtimeConfig.maxSteps : 8
186
+
187
+ if (mode === 'linear') {
188
+ const agent = agentFactory({
189
+ mode: dispatchMode,
190
+ tools,
191
+ extraInstructions:
192
+ typeof runtimeConfig.extraInstructions === 'string' ? runtimeConfig.extraInstructions : undefined,
193
+ maxRetries: 1,
194
+ stopWhen: [stepCountIs(maxSteps)],
195
+ }) as ToolLoopAgent<never, ToolSet>
196
+
197
+ const result = await agent.generate({ prompt: buildDispatchPrompt(params.nodeSpec) })
198
+ const outputCandidate = PlanNodeResultSubmissionSchema.safeParse(result.output)
199
+ if (outputCandidate.success) {
200
+ return outputCandidate.data
201
+ }
202
+
203
+ throw new Error(`Agent executor "${agentId}" returned an invalid node result.`)
204
+ }
205
+
206
+ // --- Graph-lite / Graph-full path ---
207
+
208
+ const workspace = nodeWorkspaceService.initialize({
209
+ nodeSpec: params.nodeSpec,
210
+ resolvedInput: params.resolvedInput,
211
+ inputArtifacts: params.inputArtifacts,
212
+ schemaRegistry: params.schemaRegistry ?? {},
213
+ })
214
+
215
+ const writeIntentTool = tool({
216
+ description:
217
+ 'Write a validated artifact or structured output field. Call this for each deliverable. If validation fails, correct your payload and try again.',
218
+ inputSchema: WriteIntentSchema,
219
+ execute: async (intent: WriteIntent) => {
220
+ const correctionCount = workspace.sys.correctionCounts.get(intent.targetPath) ?? 0
221
+
222
+ const validation = writeIntentValidatorService.validate({
223
+ intent,
224
+ nodeSpec: params.nodeSpec,
225
+ schemaRegistry: workspace.ctx.schemaRegistry,
226
+ existingDeliverables: workspace.deliverables,
227
+ })
228
+
229
+ if (validation.issues.length > 0) {
230
+ if (correctionCount >= MAX_SELF_CORRECTION_RETRIES) {
231
+ throw new Error(
232
+ `Write validation failed for "${intent.targetPath}" after ${MAX_SELF_CORRECTION_RETRIES} retries: ${validation.issues.map((i) => i.message).join(', ')}`,
233
+ )
234
+ }
235
+ workspace.sys.correctionCounts.set(intent.targetPath, correctionCount + 1)
236
+ workspace.sys.writeLog.push({
237
+ targetPath: intent.targetPath,
238
+ action: intent.action,
239
+ timestamp: new Date(),
240
+ result: 'rejected',
241
+ })
242
+ return {
243
+ status: 'validation_failed',
244
+ error: { targetPath: intent.targetPath, action: intent.action, issues: validation.issues },
245
+ hint: `Correct and re-emit. Attempt ${correctionCount + 1}/${MAX_SELF_CORRECTION_RETRIES}.`,
246
+ }
247
+ }
248
+
249
+ nodeWorkspaceService.stageWrite(workspace, intent, 'validated')
250
+ return { status: 'accepted', message: `Write to "${intent.targetPath}" validated and staged.` }
251
+ },
252
+ })
253
+
254
+ const graphTools = { ...tools, writeIntent: writeIntentTool }
255
+
256
+ const agent = agentFactory({
257
+ mode: dispatchMode,
258
+ tools: graphTools,
259
+ extraInstructions:
260
+ typeof runtimeConfig.extraInstructions === 'string' ? runtimeConfig.extraInstructions : undefined,
261
+ maxRetries: 1,
262
+ stopWhen: [stepCountIs(maxSteps)],
263
+ }) as ToolLoopAgent<never, ToolSet>
264
+
265
+ await agent.generate({ prompt: buildWriteIntentDispatchPrompt(params.nodeSpec) })
266
+
267
+ const finalResult = nodeWorkspaceService.finalize(workspace)
268
+
269
+ if (!finalResult.isComplete) {
270
+ const result: PlanNodeResult = {
271
+ structuredOutput: finalResult.structuredOutput,
272
+ artifacts: finalResult.artifacts,
273
+ notes: 'Execution incomplete: missing required deliverables or validation failures.',
274
+ quality: 'partial',
275
+ }
276
+ nodeWorkspaceService.cleanup(workspace)
277
+ return result
278
+ }
279
+
280
+ const result: PlanNodeResult = {
281
+ structuredOutput: finalResult.structuredOutput,
282
+ artifacts: finalResult.artifacts,
283
+ notes: finalResult.notes,
284
+ quality: finalResult.quality,
285
+ }
286
+
287
+ nodeWorkspaceService.cleanup(workspace)
288
+ return result
289
+ }
290
+ }
291
+
292
+ export const agentExecutorService = new AgentExecutorService()