@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.
- package/infrastructure/schema/00_identity.surql +0 -2
- package/infrastructure/schema/01_memory.surql +1 -1
- package/infrastructure/schema/02_execution_plan.surql +62 -1
- package/infrastructure/schema/03_learned_skill.surql +1 -1
- package/infrastructure/schema/06_playbook.surql +25 -0
- package/infrastructure/schema/07_institutional_memory.surql +13 -0
- package/infrastructure/schema/08_quality_metrics.surql +17 -0
- package/package.json +12 -8
- package/src/ai/definitions.ts +81 -3
- package/src/ai/embedding-cache.ts +2 -4
- package/src/ai/index.ts +0 -2
- package/src/bifrost/bifrost.ts +2 -7
- package/src/bifrost/cache-headers.ts +8 -0
- package/src/bifrost/index.ts +1 -0
- package/src/config/agent-defaults.ts +31 -21
- package/src/config/agent-types.ts +11 -0
- package/src/config/constants.ts +2 -14
- package/src/config/debug-logger.ts +5 -1
- package/src/config/index.ts +3 -0
- package/src/config/model-constants.ts +16 -34
- package/src/config/search.ts +1 -15
- package/src/create-runtime.ts +269 -178
- package/src/db/cursor-pagination.ts +3 -6
- package/src/db/index.ts +2 -0
- package/src/db/memory-store.helpers.ts +1 -3
- package/src/db/memory-store.rows.ts +7 -7
- package/src/db/memory-store.ts +14 -18
- package/src/db/memory.ts +13 -13
- package/src/db/schema-fingerprint.ts +1 -3
- package/src/db/service.ts +153 -79
- package/src/db/startup.ts +6 -10
- package/src/db/surreal-mutation.ts +43 -0
- package/src/db/tables.ts +7 -0
- package/src/db/workstream-message-row.ts +15 -0
- package/src/embeddings/provider.ts +1 -1
- package/src/queues/context-compaction.queue.ts +15 -46
- package/src/queues/delayed-node-promotion.queue.ts +41 -0
- package/src/queues/document-processor.queue.ts +2 -4
- package/src/queues/index.ts +3 -0
- package/src/queues/memory-consolidation.queue.ts +16 -51
- package/src/queues/plan-scheduler.queue.ts +97 -0
- package/src/queues/post-chat-memory.queue.ts +20 -55
- package/src/queues/queue-factory.ts +100 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
- package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
- package/src/queues/skill-extraction.queue.ts +15 -47
- package/src/queues/workstream-title-generation.queue.ts +15 -47
- package/src/redis/connection.ts +6 -0
- package/src/redis/index.ts +1 -1
- package/src/redis/redis-lease-lock.ts +1 -2
- package/src/redis/stream-context.ts +11 -0
- package/src/runtime/agent-runtime-policy.ts +109 -35
- package/src/runtime/approval-continuation.ts +12 -6
- package/src/runtime/context-compaction-runtime.ts +1 -1
- package/src/runtime/context-compaction.ts +24 -64
- package/src/runtime/execution-plan.ts +22 -18
- package/src/runtime/graph-designer.ts +15 -0
- package/src/runtime/helper-model.ts +9 -197
- package/src/runtime/index.ts +3 -1
- package/src/runtime/llm-content.ts +1 -1
- package/src/runtime/memory-block.ts +9 -11
- package/src/runtime/memory-pipeline.ts +6 -9
- package/src/runtime/plugin-resolution.ts +35 -0
- package/src/runtime/plugin-types.ts +72 -0
- package/src/runtime/retrieval-adapters.ts +1 -1
- package/src/runtime/runtime-config.ts +111 -14
- package/src/runtime/runtime-extensions.ts +2 -3
- package/src/runtime/runtime-worker-registry.ts +6 -0
- package/src/runtime/social-chat.ts +752 -0
- package/src/runtime/team-consultation-orchestrator.ts +45 -32
- package/src/runtime/team-consultation-prompts.ts +11 -2
- package/src/runtime/title-helpers.ts +2 -4
- package/src/runtime/workstream-chat-helpers.ts +1 -1
- package/src/services/adaptive-playbook.service.ts +152 -0
- package/src/services/agent-executor.service.ts +292 -0
- package/src/services/artifact-provenance.service.ts +172 -0
- package/src/services/attachment.service.ts +6 -11
- package/src/services/context-compaction.service.ts +72 -55
- package/src/services/context-enrichment.service.ts +33 -0
- package/src/services/coordination-registry.service.ts +117 -0
- package/src/services/document-chunk.service.ts +2 -4
- package/src/services/domain-agent-executor.service.ts +71 -0
- package/src/services/execution-plan.service.ts +269 -50
- package/src/services/feedback-loop.service.ts +96 -0
- package/src/services/global-orchestrator.service.ts +148 -0
- package/src/services/index.ts +27 -0
- package/src/services/institutional-memory.service.ts +145 -0
- package/src/services/learned-skill.service.ts +24 -5
- package/src/services/memory-assessment.service.ts +3 -2
- package/src/services/memory-utils.ts +3 -8
- package/src/services/memory.service.ts +49 -61
- package/src/services/monitoring-window.service.ts +86 -0
- package/src/services/mutating-approval.service.ts +1 -1
- package/src/services/node-workspace.service.ts +155 -0
- package/src/services/notification.service.ts +39 -0
- package/src/services/organization-member.service.ts +11 -4
- package/src/services/organization.service.ts +5 -5
- package/src/services/ownership-dispatcher.service.ts +403 -0
- package/src/services/plan-approval.service.ts +1 -1
- package/src/services/plan-builder.service.ts +1 -0
- package/src/services/plan-checkpoint.service.ts +30 -2
- package/src/services/plan-compiler.service.ts +5 -0
- package/src/services/plan-coordination.service.ts +152 -0
- package/src/services/plan-cycle.service.ts +284 -0
- package/src/services/plan-deadline.service.ts +287 -0
- package/src/services/plan-executor.service.ts +384 -40
- package/src/services/plan-run.service.ts +41 -7
- package/src/services/plan-scheduler.service.ts +240 -0
- package/src/services/plan-template.service.ts +117 -0
- package/src/services/plan-validator.service.ts +84 -2
- package/src/services/plan-workspace.service.ts +83 -0
- package/src/services/playbook-registry.service.ts +67 -0
- package/src/services/plugin-executor.service.ts +103 -0
- package/src/services/quality-metrics.service.ts +132 -0
- package/src/services/recent-activity.service.ts +28 -34
- package/src/services/skill-resolver.service.ts +19 -0
- package/src/services/social-chat-history.service.ts +197 -0
- package/src/services/system-executor.service.ts +105 -0
- package/src/services/workstream-message.service.ts +13 -37
- package/src/services/workstream-plan-registry.service.ts +22 -0
- package/src/services/workstream-title.service.ts +3 -1
- package/src/services/workstream-turn-preparation.service.ts +34 -89
- package/src/services/workstream.service.ts +33 -55
- package/src/services/workstream.types.ts +9 -9
- package/src/services/write-intent-validator.service.ts +81 -0
- package/src/storage/attachment-parser.ts +1 -1
- package/src/storage/attachment-utils.ts +1 -1
- package/src/storage/generated-document-storage.service.ts +3 -2
- package/src/system-agents/context-compaction.agent.ts +2 -0
- package/src/system-agents/delegated-agent-factory.ts +5 -0
- package/src/system-agents/memory-reranker.agent.ts +4 -2
- package/src/system-agents/memory.agent.ts +2 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -0
- package/src/system-agents/skill-extractor.agent.ts +2 -0
- package/src/system-agents/skill-manager.agent.ts +2 -0
- package/src/system-agents/title-generator.agent.ts +2 -0
- package/src/tools/execution-plan.tool.ts +17 -23
- package/src/tools/index.ts +0 -1
- package/src/tools/research-topic.tool.ts +2 -0
- package/src/tools/team-think.tool.ts +5 -6
- package/src/utils/async.ts +2 -1
- package/src/utils/date-time.ts +4 -32
- package/src/utils/env.ts +8 -0
- package/src/utils/errors.ts +42 -10
- package/src/utils/index.ts +9 -0
- package/src/utils/string.ts +114 -1
- package/src/workers/index.ts +1 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
- package/src/workers/regular-chat-memory-digest.runner.ts +45 -12
- package/src/workers/skill-extraction.runner.ts +26 -6
- package/src/workers/utils/file-section-chunker.ts +2 -1
- package/src/workers/utils/repo-structure-extractor.ts +2 -2
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/sandbox-error.ts +11 -2
- package/src/workers/utils/workstream-message-query.ts +14 -25
- package/src/workers/worker-utils.ts +2 -2
- package/src/runtime/workstream-routing-policy.ts +0 -267
- 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,
|
|
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.
|
|
148
|
+
let result: Awaited<ReturnType<typeof agent.generate>>
|
|
130
149
|
try {
|
|
131
|
-
result = await observer.run(() =>
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
'-
|
|
11
|
-
'-
|
|
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
|
-
|
|
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 !==
|
|
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()
|