@lota-sdk/core 0.1.31 → 0.1.33
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/package.json +2 -2
- package/src/runtime/agent-runtime-policy.ts +10 -12
- package/src/runtime/approval-continuation.ts +1 -8
- package/src/runtime/social-chat-agent-runner.ts +0 -1
- package/src/services/workstream-turn-preparation.service.ts +26 -16
- package/src/tools/execution-plan.tool.ts +124 -79
- package/src/tools/index.ts +0 -1
- package/src/tools/project-with-plan.tool.ts +0 -87
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@chat-adapter/slack": "^4.23.0",
|
|
33
33
|
"@chat-adapter/state-ioredis": "^4.23.0",
|
|
34
34
|
"@logtape/logtape": "^2.0.5",
|
|
35
|
-
"@lota-sdk/shared": "0.1.
|
|
35
|
+
"@lota-sdk/shared": "0.1.33",
|
|
36
36
|
"@mendable/firecrawl-js": "^4.18.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
38
|
"ai": "^6.0.141",
|
|
@@ -18,7 +18,6 @@ export interface AgentRuntimeConfig<TAgent extends string> {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface AgentRuntimeRuleOptions {
|
|
21
|
-
includeExecutionPlanRule?: boolean
|
|
22
21
|
includeMemr3Rule?: boolean
|
|
23
22
|
includeDomainReasoningFallbackRule?: boolean
|
|
24
23
|
}
|
|
@@ -36,17 +35,14 @@ export interface AgentToolPolicy<TSkill extends PropertyKey> {
|
|
|
36
35
|
includeProceedInOnboarding: boolean
|
|
37
36
|
includeGithubIntegration: boolean
|
|
38
37
|
includeIndexRepositoryByURL: boolean
|
|
38
|
+
includeExecutionPlanTools: boolean
|
|
39
39
|
includeIndexedRepository: boolean
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export const OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES = Object.freeze([
|
|
43
43
|
'conversationSearch',
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'submitExecutionNodeResult',
|
|
47
|
-
'listExecutionPlans',
|
|
48
|
-
'getExecutionPlanDetails',
|
|
49
|
-
'resumeExecutionPlanRun',
|
|
44
|
+
'executionPlan',
|
|
45
|
+
'executionPlanQuery',
|
|
50
46
|
'consultSpecialist',
|
|
51
47
|
'consultTeam',
|
|
52
48
|
'teamThink',
|
|
@@ -184,12 +180,13 @@ export function buildWorkstreamAgentToolPolicy<TAgent extends string, TSkill ext
|
|
|
184
180
|
return {
|
|
185
181
|
resolvedMode,
|
|
186
182
|
skills,
|
|
187
|
-
includeMemorySearch:
|
|
188
|
-
includeConversationSearch:
|
|
189
|
-
includeMemoryRemember:
|
|
190
|
-
includeOrgActionSearch:
|
|
183
|
+
includeMemorySearch: !params.onboardingActive,
|
|
184
|
+
includeConversationSearch: !params.onboardingActive,
|
|
185
|
+
includeMemoryRemember: !params.onboardingActive,
|
|
186
|
+
includeOrgActionSearch: !params.onboardingActive,
|
|
191
187
|
includeMemoryBlockAppend: true,
|
|
192
|
-
includeReadFileParts:
|
|
188
|
+
includeReadFileParts: !params.onboardingActive,
|
|
189
|
+
includeExecutionPlanTools: !params.onboardingActive,
|
|
193
190
|
includeInspectWebsite: params.onboardingActive && params.agentId === onboardingOwnerAgentId,
|
|
194
191
|
includeProceedInOnboarding: params.onboardingActive && params.agentId === onboardingOwnerAgentId,
|
|
195
192
|
includeGithubIntegration: params.agentId === onboardingOwnerAgentId,
|
|
@@ -216,6 +213,7 @@ export function buildTeamConsultationAgentToolPolicy({
|
|
|
216
213
|
includeOrgActionSearch: false,
|
|
217
214
|
includeMemoryBlockAppend: false,
|
|
218
215
|
includeReadFileParts: false,
|
|
216
|
+
includeExecutionPlanTools: false,
|
|
219
217
|
includeInspectWebsite: false,
|
|
220
218
|
includeProceedInOnboarding: false,
|
|
221
219
|
includeGithubIntegration: false,
|
|
@@ -60,14 +60,7 @@ export function isApprovalContinuationRequest(messages: ChatMessage[]): boolean
|
|
|
60
60
|
return hasApprovalRespondedParts(messages[lastAssistantIndex])
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const PLAN_TOOL_NAMES = new Set([
|
|
64
|
-
'createExecutionPlan',
|
|
65
|
-
'replaceExecutionPlan',
|
|
66
|
-
'submitExecutionNodeResult',
|
|
67
|
-
'listExecutionPlans',
|
|
68
|
-
'getExecutionPlanDetails',
|
|
69
|
-
'resumeExecutionPlanRun',
|
|
70
|
-
])
|
|
63
|
+
const PLAN_TOOL_NAMES = new Set(['executionPlan', 'executionPlanQuery'])
|
|
71
64
|
|
|
72
65
|
function isPlanApprovalId(value: unknown): value is string {
|
|
73
66
|
return typeof value === 'string' && value.startsWith(`${TABLES.PLAN_APPROVAL}:`)
|
|
@@ -85,7 +85,6 @@ export async function runSocialAgentTurn(params: {
|
|
|
85
85
|
learnedSkillsSection: params.learnedSkillsSection,
|
|
86
86
|
userMessageText: params.userMessageText,
|
|
87
87
|
ruleOptions: {
|
|
88
|
-
includeExecutionPlanRule: false,
|
|
89
88
|
includeMemr3Rule: SOCIAL_CHAT_RETRIEVAL_TOOL_NAMES.some((toolName) => toolName in params.tools),
|
|
90
89
|
includeDomainReasoningFallbackRule: params.agentId === 'cpo' || params.agentId === 'mentor',
|
|
91
90
|
},
|
|
@@ -342,11 +342,7 @@ async function streamAgentResponse(
|
|
|
342
342
|
workstreamStateSection,
|
|
343
343
|
learnedSkillsSection,
|
|
344
344
|
userMessageText: latestUserMessageText,
|
|
345
|
-
ruleOptions: {
|
|
346
|
-
includeExecutionPlanRule: streamParams.includeExecutionPlanTools,
|
|
347
|
-
includeMemr3Rule: hasRetrievalTools,
|
|
348
|
-
includeDomainReasoningFallbackRule: hasDomainRoutingSkills,
|
|
349
|
-
},
|
|
345
|
+
ruleOptions: { includeMemr3Rule: hasRetrievalTools, includeDomainReasoningFallbackRule: hasDomainRoutingSkills },
|
|
350
346
|
additionalInstructionSections: mergeInstructionSections(
|
|
351
347
|
executionPlanInstructionSections,
|
|
352
348
|
streamParams.additionalInstructionSections,
|
|
@@ -363,6 +359,7 @@ async function streamAgentResponse(
|
|
|
363
359
|
mode: streamParams.mode,
|
|
364
360
|
tools: streamParams.tools,
|
|
365
361
|
extraInstructions: config.extraInstructions,
|
|
362
|
+
maxRetries: 3,
|
|
366
363
|
stopWhen: (agentResolution?.stopWhen as StopCondition<ToolSet> | Array<StopCondition<ToolSet>> | undefined) ??
|
|
367
364
|
streamParams.stopWhen ?? [stepCountIs(config.maxSteps as number)],
|
|
368
365
|
prepareStep: (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
|
|
@@ -370,19 +367,32 @@ async function streamAgentResponse(
|
|
|
370
367
|
const agentAbortSignal = streamParams.abortSignal ?? ctx.runAbortSignal
|
|
371
368
|
agentTimer.step('agent-construction')
|
|
372
369
|
|
|
370
|
+
const MAX_STREAM_RETRIES = 3
|
|
373
371
|
let result: unknown
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
372
|
+
for (let attempt = 0; ; attempt++) {
|
|
373
|
+
try {
|
|
374
|
+
result = await streamParams.observer.run(() =>
|
|
375
|
+
agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
|
|
376
|
+
)
|
|
377
|
+
agentTimer.step('agent.stream()-resolved')
|
|
378
|
+
break
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (agentAbortSignal.aborted) {
|
|
381
|
+
streamParams.observer.recordAbort(error)
|
|
382
|
+
throw error
|
|
383
|
+
}
|
|
384
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
385
|
+
const isTransient =
|
|
386
|
+
errorMessage.includes('client disconnected') ||
|
|
387
|
+
errorMessage.includes('ECONNRESET') ||
|
|
388
|
+
errorMessage.includes('socket hang up') ||
|
|
389
|
+
errorMessage.includes('fetch failed')
|
|
390
|
+
if (!isTransient || attempt >= MAX_STREAM_RETRIES - 1) {
|
|
391
|
+
streamParams.observer.recordError(error)
|
|
392
|
+
throw error
|
|
393
|
+
}
|
|
394
|
+
aiLogger.warn`Transient stream error (attempt ${attempt + 1}/${MAX_STREAM_RETRIES}): ${errorMessage} — retrying`
|
|
384
395
|
}
|
|
385
|
-
throw error
|
|
386
396
|
}
|
|
387
397
|
if (!hasUIMessageStream(result)) {
|
|
388
398
|
throw new Error(`Agent run for ${resolvedAgentId} did not expose a UI message stream.`)
|
|
@@ -1,65 +1,157 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ListExecutionPlansArgsSchema,
|
|
5
|
-
ResumeExecutionPlanRunArgsSchema,
|
|
6
|
-
ReplaceExecutionPlanArgsSchema,
|
|
2
|
+
ExecutionPlanArgsSchema,
|
|
3
|
+
ExecutionPlanQueryArgsSchema,
|
|
7
4
|
SubmitExecutionNodeResultArgsSchema,
|
|
8
5
|
expandAgentPlanDraft,
|
|
9
6
|
getLatestExecutionPlanResult,
|
|
10
7
|
} from '@lota-sdk/shared'
|
|
8
|
+
import type { ExecutionPlanAction, ExecutionPlanArgs, CreateProjectWithPlanResultData } from '@lota-sdk/shared'
|
|
11
9
|
import { tool } from 'ai'
|
|
12
10
|
|
|
13
11
|
import type { RecordIdRef } from '../db/record-id'
|
|
12
|
+
import { recordIdToString } from '../db/record-id'
|
|
13
|
+
import { TABLES } from '../db/tables'
|
|
14
14
|
import { executionPlanService } from '../services/execution-plan.service'
|
|
15
|
+
import { workstreamService } from '../services/workstream.service'
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
type ExecutionPlanWorkstreamService = Pick<
|
|
18
|
+
typeof workstreamService,
|
|
19
|
+
'createWorkstream' | 'deleteWorkstream' | 'getWorkstream'
|
|
20
|
+
>
|
|
21
|
+
|
|
22
|
+
type ExecutionPlanExecutionPlanService = Pick<
|
|
23
|
+
typeof executionPlanService,
|
|
24
|
+
| 'createPlan'
|
|
25
|
+
| 'replacePlan'
|
|
26
|
+
| 'resumeRun'
|
|
27
|
+
| 'listActivePlanSummaries'
|
|
28
|
+
| 'getActivePlanToolResult'
|
|
29
|
+
| 'getActivePlansForWorkstream'
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
function extractDraft(input: ExecutionPlanArgs) {
|
|
33
|
+
const { action, projectTitle, targetWorkstreamId, runId, reason, ...draftInput } = input
|
|
34
|
+
return { action, projectTitle, targetWorkstreamId, runId, reason, draft: expandAgentPlanDraft(draftInput) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createExecutionPlanTool(params: {
|
|
17
38
|
orgId: RecordIdRef
|
|
39
|
+
userId: RecordIdRef
|
|
18
40
|
workstreamId: RecordIdRef
|
|
19
41
|
agentId: string
|
|
42
|
+
executionPlanService?: ExecutionPlanExecutionPlanService
|
|
43
|
+
workstreamService?: ExecutionPlanWorkstreamService
|
|
20
44
|
onPlanChanged?: () => void
|
|
21
45
|
validateInlinePlan?: (draft: ReturnType<typeof expandAgentPlanDraft>) => void
|
|
22
46
|
}) {
|
|
47
|
+
const resolvedEpService = params.executionPlanService ?? executionPlanService
|
|
48
|
+
const resolvedWsService = params.workstreamService ?? workstreamService
|
|
49
|
+
|
|
23
50
|
return tool({
|
|
24
51
|
description:
|
|
25
|
-
'
|
|
26
|
-
inputSchema:
|
|
52
|
+
'Manage execution plans. Actions: create (inline, 1-2 nodes), create-project (dedicated project workstream, 3+ nodes), replace (swap active plan), resume (resume interrupted plan).',
|
|
53
|
+
inputSchema: ExecutionPlanArgsSchema,
|
|
27
54
|
execute: async (input) => {
|
|
28
|
-
const { targetWorkstreamId,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
const { action, projectTitle, targetWorkstreamId, runId, reason, draft } = extractDraft(input)
|
|
56
|
+
|
|
57
|
+
const handler: Record<ExecutionPlanAction, () => Promise<unknown>> = {
|
|
58
|
+
create: async () => {
|
|
59
|
+
params.validateInlinePlan?.(draft)
|
|
60
|
+
return await resolvedEpService.createPlan({
|
|
61
|
+
organizationId: params.orgId,
|
|
62
|
+
workstreamId: targetWorkstreamId ?? params.workstreamId,
|
|
63
|
+
leadAgentId: params.agentId,
|
|
64
|
+
input: draft,
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
'create-project': async () => {
|
|
69
|
+
const targetWorkstream = targetWorkstreamId
|
|
70
|
+
? await resolvedWsService.getWorkstream(targetWorkstreamId)
|
|
71
|
+
: await resolvedWsService.createWorkstream(params.userId, params.orgId, {
|
|
72
|
+
title: projectTitle!,
|
|
73
|
+
mode: 'group',
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (targetWorkstream.organizationId !== recordIdToString(params.orgId, TABLES.ORGANIZATION)) {
|
|
77
|
+
throw new Error('Target workstream belongs to a different organization.')
|
|
78
|
+
}
|
|
79
|
+
if (targetWorkstream.userId !== recordIdToString(params.userId, TABLES.USER)) {
|
|
80
|
+
throw new Error('Target workstream belongs to a different user.')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const existingPlans = await resolvedEpService.getActivePlansForWorkstream(targetWorkstream.id)
|
|
84
|
+
if (!targetWorkstream.core && existingPlans.length > 0) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
'This workstream already has an active execution plan. Use action "replace" or target a core workstream.',
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const createdWorkstream = !targetWorkstreamId
|
|
91
|
+
try {
|
|
92
|
+
const result = await resolvedEpService.createPlan({
|
|
93
|
+
organizationId: params.orgId,
|
|
94
|
+
workstreamId: targetWorkstream.id,
|
|
95
|
+
leadAgentId: params.agentId,
|
|
96
|
+
input: draft,
|
|
97
|
+
})
|
|
98
|
+
return {
|
|
99
|
+
...result,
|
|
100
|
+
workstreamId: targetWorkstream.id,
|
|
101
|
+
workstreamTitle: targetWorkstream.title,
|
|
102
|
+
createdWorkstream,
|
|
103
|
+
} satisfies CreateProjectWithPlanResultData
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (createdWorkstream) {
|
|
106
|
+
await resolvedWsService.deleteWorkstream(targetWorkstream.id).catch(() => {})
|
|
107
|
+
}
|
|
108
|
+
throw error
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
replace: async () => {
|
|
113
|
+
return await resolvedEpService.replacePlan({
|
|
114
|
+
organizationId: params.orgId,
|
|
115
|
+
workstreamId: params.workstreamId,
|
|
116
|
+
leadAgentId: params.agentId,
|
|
117
|
+
input: { runId: runId!, reason: reason!, ...draft },
|
|
118
|
+
})
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
resume: async () => {
|
|
122
|
+
return await resolvedEpService.resumeRun({
|
|
123
|
+
workstreamId: params.workstreamId,
|
|
124
|
+
emittedBy: params.agentId,
|
|
125
|
+
input: { runId: runId! },
|
|
126
|
+
})
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = await handler[action]()
|
|
37
131
|
params.onPlanChanged?.()
|
|
38
132
|
return result
|
|
39
133
|
},
|
|
40
134
|
})
|
|
41
135
|
}
|
|
42
136
|
|
|
43
|
-
export function
|
|
44
|
-
orgId: RecordIdRef
|
|
45
|
-
workstreamId: RecordIdRef
|
|
46
|
-
agentId: string
|
|
47
|
-
onPlanChanged?: () => void
|
|
48
|
-
}) {
|
|
137
|
+
export function createExecutionPlanQueryTool(params: { workstreamId: RecordIdRef }) {
|
|
49
138
|
return tool({
|
|
50
139
|
description:
|
|
51
|
-
'
|
|
52
|
-
inputSchema:
|
|
140
|
+
'Query execution plans. Omit runId to list all active plans. Provide runId to load a specific plan run.',
|
|
141
|
+
inputSchema: ExecutionPlanQueryArgsSchema,
|
|
53
142
|
execute: async (input) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
143
|
+
if (!input.runId) {
|
|
144
|
+
return await executionPlanService.listActivePlanSummaries(params.workstreamId)
|
|
145
|
+
}
|
|
146
|
+
return await executionPlanService.getActivePlanToolResult({
|
|
57
147
|
workstreamId: params.workstreamId,
|
|
58
|
-
|
|
59
|
-
|
|
148
|
+
runId: input.runId,
|
|
149
|
+
includeEvents: input.includeEvents,
|
|
150
|
+
includeArtifacts: input.includeArtifacts,
|
|
151
|
+
includeApprovals: input.includeApprovals,
|
|
152
|
+
includeCheckpoints: input.includeCheckpoints,
|
|
153
|
+
includeValidationIssues: input.includeValidationIssues,
|
|
60
154
|
})
|
|
61
|
-
params.onPlanChanged?.()
|
|
62
|
-
return result
|
|
63
155
|
},
|
|
64
156
|
})
|
|
65
157
|
}
|
|
@@ -89,50 +181,3 @@ export function createSubmitExecutionNodeResultTool(params: {
|
|
|
89
181
|
},
|
|
90
182
|
})
|
|
91
183
|
}
|
|
92
|
-
|
|
93
|
-
export function createListExecutionPlansTool(params: { workstreamId: RecordIdRef }) {
|
|
94
|
-
return tool({
|
|
95
|
-
description:
|
|
96
|
-
'List all active execution plans for this workstream with summary info (title, status, objective, node counts). Use getExecutionPlanDetails to inspect a specific plan.',
|
|
97
|
-
inputSchema: ListExecutionPlansArgsSchema,
|
|
98
|
-
execute: async () => await executionPlanService.listActivePlanSummaries(params.workstreamId),
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function createGetExecutionPlanDetailsTool(params: { workstreamId: RecordIdRef }) {
|
|
103
|
-
return tool({
|
|
104
|
-
description: 'Load a plan run and its current state.',
|
|
105
|
-
inputSchema: GetActiveExecutionPlanArgsSchema,
|
|
106
|
-
execute: async (input) =>
|
|
107
|
-
await executionPlanService.getActivePlanToolResult({
|
|
108
|
-
workstreamId: params.workstreamId,
|
|
109
|
-
runId: input.runId,
|
|
110
|
-
includeEvents: input.includeEvents,
|
|
111
|
-
includeArtifacts: input.includeArtifacts,
|
|
112
|
-
includeApprovals: input.includeApprovals,
|
|
113
|
-
includeCheckpoints: input.includeCheckpoints,
|
|
114
|
-
includeValidationIssues: input.includeValidationIssues,
|
|
115
|
-
}),
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function createResumeExecutionPlanRunTool(params: {
|
|
120
|
-
workstreamId: RecordIdRef
|
|
121
|
-
agentId: string
|
|
122
|
-
onPlanChanged?: () => void
|
|
123
|
-
}) {
|
|
124
|
-
return tool({
|
|
125
|
-
description:
|
|
126
|
-
'Resume the active execution run from its latest checkpoint after interruption. The executor reconciles state before re-activating nodes.',
|
|
127
|
-
inputSchema: ResumeExecutionPlanRunArgsSchema,
|
|
128
|
-
execute: async (input) => {
|
|
129
|
-
const result = await executionPlanService.resumeRun({
|
|
130
|
-
workstreamId: params.workstreamId,
|
|
131
|
-
emittedBy: params.agentId,
|
|
132
|
-
input,
|
|
133
|
-
})
|
|
134
|
-
params.onPlanChanged?.()
|
|
135
|
-
return result
|
|
136
|
-
},
|
|
137
|
-
})
|
|
138
|
-
}
|
package/src/tools/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from './execution-plan.tool'
|
|
2
2
|
export * from './fetch-webpage.tool'
|
|
3
3
|
export * from './memory-block.tool'
|
|
4
|
-
export * from './project-with-plan.tool'
|
|
5
4
|
export * from './read-file-parts.tool'
|
|
6
5
|
export * from './remember-memory.tool'
|
|
7
6
|
export * from './research-topic.tool'
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { CreateProjectWithPlanResultData } from '@lota-sdk/shared'
|
|
2
|
-
import { CreateProjectWithPlanArgsSchema, expandAgentPlanDraft } from '@lota-sdk/shared'
|
|
3
|
-
import { tool } from 'ai'
|
|
4
|
-
|
|
5
|
-
import type { RecordIdRef } from '../db/record-id'
|
|
6
|
-
import { recordIdToString } from '../db/record-id'
|
|
7
|
-
import { TABLES } from '../db/tables'
|
|
8
|
-
import { executionPlanService } from '../services/execution-plan.service'
|
|
9
|
-
import { workstreamService } from '../services/workstream.service'
|
|
10
|
-
|
|
11
|
-
type ProjectWithPlanWorkstreamService = Pick<
|
|
12
|
-
typeof workstreamService,
|
|
13
|
-
'createWorkstream' | 'deleteWorkstream' | 'getWorkstream'
|
|
14
|
-
>
|
|
15
|
-
|
|
16
|
-
type ProjectWithPlanExecutionPlanService = Pick<
|
|
17
|
-
typeof executionPlanService,
|
|
18
|
-
'createPlan' | 'getActivePlansForWorkstream'
|
|
19
|
-
>
|
|
20
|
-
|
|
21
|
-
export function createCreateProjectWithPlanTool(params: {
|
|
22
|
-
orgId: RecordIdRef
|
|
23
|
-
userId: RecordIdRef
|
|
24
|
-
agentId: string
|
|
25
|
-
workstreamService?: ProjectWithPlanWorkstreamService
|
|
26
|
-
executionPlanService?: ProjectWithPlanExecutionPlanService
|
|
27
|
-
onPlanChanged?: () => void
|
|
28
|
-
}) {
|
|
29
|
-
const resolvedWorkstreamService = params.workstreamService ?? workstreamService
|
|
30
|
-
const resolvedExecutionPlanService = params.executionPlanService ?? executionPlanService
|
|
31
|
-
|
|
32
|
-
return tool({
|
|
33
|
-
description:
|
|
34
|
-
'Create a dedicated project workstream with an execution plan, or add a simplified agent-authored plan to an existing target workstream.',
|
|
35
|
-
inputSchema: CreateProjectWithPlanArgsSchema,
|
|
36
|
-
execute: async (input): Promise<CreateProjectWithPlanResultData> => {
|
|
37
|
-
const { projectTitle, targetWorkstreamId, ...draftInput } = input
|
|
38
|
-
const expandedDraft = expandAgentPlanDraft(draftInput)
|
|
39
|
-
|
|
40
|
-
const targetWorkstream = targetWorkstreamId
|
|
41
|
-
? await resolvedWorkstreamService.getWorkstream(targetWorkstreamId)
|
|
42
|
-
: await resolvedWorkstreamService.createWorkstream(params.userId, params.orgId, {
|
|
43
|
-
title: projectTitle,
|
|
44
|
-
mode: 'group',
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
if (targetWorkstream.organizationId !== recordIdToString(params.orgId, TABLES.ORGANIZATION)) {
|
|
48
|
-
throw new Error('Target workstream belongs to a different organization.')
|
|
49
|
-
}
|
|
50
|
-
if (targetWorkstream.userId !== recordIdToString(params.userId, TABLES.USER)) {
|
|
51
|
-
throw new Error('Target workstream belongs to a different user.')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const existingPlans = await resolvedExecutionPlanService.getActivePlansForWorkstream(targetWorkstream.id)
|
|
55
|
-
if (!targetWorkstream.core && existingPlans.length > 0) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
'This workstream already has an active execution plan. Use replaceExecutionPlan or target a core workstream.',
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const createdWorkstream = !targetWorkstreamId
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const result = await resolvedExecutionPlanService.createPlan({
|
|
65
|
-
organizationId: params.orgId,
|
|
66
|
-
workstreamId: targetWorkstream.id,
|
|
67
|
-
leadAgentId: params.agentId,
|
|
68
|
-
input: expandedDraft,
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
params.onPlanChanged?.()
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
...result,
|
|
75
|
-
workstreamId: targetWorkstream.id,
|
|
76
|
-
workstreamTitle: targetWorkstream.title,
|
|
77
|
-
createdWorkstream,
|
|
78
|
-
}
|
|
79
|
-
} catch (error) {
|
|
80
|
-
if (createdWorkstream) {
|
|
81
|
-
await resolvedWorkstreamService.deleteWorkstream(targetWorkstream.id).catch(() => {})
|
|
82
|
-
}
|
|
83
|
-
throw error
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
})
|
|
87
|
-
}
|