@lota-sdk/core 0.1.23 → 0.1.24
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/create-runtime.ts +4 -0
- package/src/runtime/execution-plan.ts +2 -1
- package/src/services/agent-activity.service.ts +350 -0
- package/src/services/index.ts +1 -0
- package/src/tools/execution-plan.tool.ts +7 -3
- package/src/tools/index.ts +1 -0
- package/src/tools/project-with-plan.tool.ts +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
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.24",
|
|
36
36
|
"@mendable/firecrawl-js": "^4.18.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
38
|
"ai": "^6.0.141",
|
package/src/create-runtime.ts
CHANGED
|
@@ -27,6 +27,8 @@ import type { LotaRuntimeWorkers } from './runtime/runtime-worker-registry'
|
|
|
27
27
|
import { buildRuntimeWorkerRegistry } from './runtime/runtime-worker-registry'
|
|
28
28
|
import type { LotaRuntimeSocialChat } from './runtime/social-chat'
|
|
29
29
|
import { createSocialChatRuntime } from './runtime/social-chat'
|
|
30
|
+
import type { agentActivityService } from './services/agent-activity.service'
|
|
31
|
+
import { agentActivityService as agentActivityServiceSingleton } from './services/agent-activity.service'
|
|
30
32
|
import type { attachmentService } from './services/attachment.service'
|
|
31
33
|
import { attachmentService as attachmentServiceSingleton } from './services/attachment.service'
|
|
32
34
|
import type { autonomousJobService } from './services/autonomous-job.service'
|
|
@@ -117,6 +119,7 @@ export interface LotaRuntime {
|
|
|
117
119
|
database: SurrealDBService
|
|
118
120
|
redis: RedisConnectionManager
|
|
119
121
|
closeRedisConnection: () => Promise<void>
|
|
122
|
+
agentActivityService: typeof agentActivityService
|
|
120
123
|
attachmentService: typeof attachmentService
|
|
121
124
|
autonomousJobService: typeof autonomousJobService
|
|
122
125
|
documentChunkService: typeof documentChunkService
|
|
@@ -390,6 +393,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
390
393
|
database: db,
|
|
391
394
|
redis: redisManager,
|
|
392
395
|
closeRedisConnection: async () => await redisManager.closeConnection(),
|
|
396
|
+
agentActivityService: agentActivityServiceSingleton,
|
|
393
397
|
attachmentService: attachmentServiceSingleton,
|
|
394
398
|
autonomousJobService: autonomousJobServiceSingleton,
|
|
395
399
|
documentChunkService: documentChunkServiceSingleton,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PROJECT_PLAN_ROUTING_PROMPT } from '@lota-sdk/shared'
|
|
1
2
|
import type { SerializableExecutionPlan } from '@lota-sdk/shared'
|
|
2
3
|
|
|
3
4
|
const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
|
|
@@ -35,7 +36,7 @@ export function buildExecutionPlanInstructionSections(
|
|
|
35
36
|
plans: SerializableExecutionPlan[] | null | undefined,
|
|
36
37
|
): string[] | undefined {
|
|
37
38
|
const normalized = plans ?? []
|
|
38
|
-
const sections = [EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT]
|
|
39
|
+
const sections = [EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT, PROJECT_PLAN_ROUTING_PROMPT]
|
|
39
40
|
const stateSection = formatExecutionPlansForPrompt(normalized)
|
|
40
41
|
if (stateSection) {
|
|
41
42
|
sections.push(stateSection)
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentActivityCounts,
|
|
3
|
+
AgentActivityEntry,
|
|
4
|
+
AgentActivityResponse,
|
|
5
|
+
AgentProjectEntry,
|
|
6
|
+
MyTasksResponse,
|
|
7
|
+
PlanBoardColumn,
|
|
8
|
+
PlanBoardResponse,
|
|
9
|
+
PlanNodeCard,
|
|
10
|
+
PlanViewNode,
|
|
11
|
+
PlanViewResponse,
|
|
12
|
+
SerializableExecutionPlan,
|
|
13
|
+
SerializablePlanNode,
|
|
14
|
+
} from '@lota-sdk/shared'
|
|
15
|
+
|
|
16
|
+
import { agentRoster } from '../config/agent-defaults'
|
|
17
|
+
import { serverLogger } from '../config/logger'
|
|
18
|
+
import { executionPlanService } from './execution-plan.service'
|
|
19
|
+
import { workstreamService } from './workstream.service'
|
|
20
|
+
import type { NormalizedWorkstream } from './workstream.types'
|
|
21
|
+
|
|
22
|
+
const BOARD_COLUMN_ORDER = ['ready', 'running', 'awaiting-human', 'completed', 'blocked', 'failed'] as const
|
|
23
|
+
type BoardColumnStatus = (typeof BOARD_COLUMN_ORDER)[number]
|
|
24
|
+
|
|
25
|
+
const COLUMN_LABELS: Record<BoardColumnStatus, string> = {
|
|
26
|
+
ready: 'Ready',
|
|
27
|
+
running: 'Running',
|
|
28
|
+
'awaiting-human': 'Awaiting Human',
|
|
29
|
+
completed: 'Completed',
|
|
30
|
+
blocked: 'Blocked',
|
|
31
|
+
failed: 'Failed',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ActivePlanEntry = {
|
|
35
|
+
plan: SerializableExecutionPlan
|
|
36
|
+
workstream: Pick<NormalizedWorkstream, 'id' | 'title' | 'isRunning' | 'updatedAt'>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type AgentActivityDeps = {
|
|
40
|
+
executionPlanService: Pick<typeof executionPlanService, 'getActivePlansForWorkstream'>
|
|
41
|
+
workstreamService: Pick<typeof workstreamService, 'listWorkstreams'>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeCardStatus(status: string): BoardColumnStatus {
|
|
45
|
+
if (status === 'pending' || status === 'scheduled') return 'ready'
|
|
46
|
+
if (status === 'partial') return 'completed'
|
|
47
|
+
if (status === 'running' || status === 'awaiting-human' || status === 'completed' || status === 'blocked') {
|
|
48
|
+
return status
|
|
49
|
+
}
|
|
50
|
+
if (status === 'failed') return 'failed'
|
|
51
|
+
return 'blocked'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isCompletedStatus(status: string): boolean {
|
|
55
|
+
return status === 'completed' || status === 'partial' || status === 'skipped'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createEmptyCounts(): AgentActivityCounts {
|
|
59
|
+
return { running: 0, ready: 0, pending: 0, awaitingHuman: 0, blocked: 0, completed: 0, failed: 0 }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function maxIsoDate(current: string | null, candidate: string): string {
|
|
63
|
+
if (!current) return candidate
|
|
64
|
+
return new Date(candidate).getTime() > new Date(current).getTime() ? candidate : current
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function incrementCounts(counts: AgentActivityCounts, rawStatus: string): void {
|
|
68
|
+
if (rawStatus === 'running') {
|
|
69
|
+
counts.running += 1
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
if (rawStatus === 'ready') {
|
|
73
|
+
counts.ready += 1
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (rawStatus === 'pending' || rawStatus === 'scheduled') {
|
|
77
|
+
counts.pending += 1
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
if (rawStatus === 'awaiting-human') {
|
|
81
|
+
counts.awaitingHuman += 1
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
if (rawStatus === 'blocked') {
|
|
85
|
+
counts.blocked += 1
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
if (rawStatus === 'failed') {
|
|
89
|
+
counts.failed += 1
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
if (rawStatus === 'completed' || rawStatus === 'partial') {
|
|
93
|
+
counts.completed += 1
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function planNodeToCard(
|
|
98
|
+
node: SerializablePlanNode,
|
|
99
|
+
plan: SerializableExecutionPlan,
|
|
100
|
+
workstreamId: string,
|
|
101
|
+
workstreamTitle: string,
|
|
102
|
+
): PlanNodeCard {
|
|
103
|
+
const approval = plan.approvals.find((candidate) => candidate.nodeId === node.id && candidate.status === 'pending')
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
nodeId: node.id,
|
|
107
|
+
label: node.label,
|
|
108
|
+
objective: node.objective,
|
|
109
|
+
status: normalizeCardStatus(node.status),
|
|
110
|
+
ownerType: node.owner.executorType,
|
|
111
|
+
ownerRef: node.owner.ref,
|
|
112
|
+
planRunId: plan.runId,
|
|
113
|
+
planTitle: plan.title,
|
|
114
|
+
workstreamId,
|
|
115
|
+
workstreamTitle,
|
|
116
|
+
nodeType: node.type,
|
|
117
|
+
artifactCount: plan.artifacts.filter((artifact) => artifact.nodeId === node.id).length,
|
|
118
|
+
hasApproval: Boolean(approval),
|
|
119
|
+
approvalId: approval?.id ?? null,
|
|
120
|
+
approvalStatus: approval?.status ?? null,
|
|
121
|
+
blockedReason: node.blockedReason ?? null,
|
|
122
|
+
latestNotes: node.latestNotes ?? null,
|
|
123
|
+
startedAt: node.startedAt ?? null,
|
|
124
|
+
completedAt: node.completedAt ?? null,
|
|
125
|
+
readyAt: node.readyAt ?? null,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildPlanViewNode(
|
|
130
|
+
node: SerializablePlanNode,
|
|
131
|
+
plan: SerializableExecutionPlan,
|
|
132
|
+
workstreamId: string,
|
|
133
|
+
workstreamTitle: string,
|
|
134
|
+
): PlanViewNode {
|
|
135
|
+
return {
|
|
136
|
+
...planNodeToCard(node, plan, workstreamId, workstreamTitle),
|
|
137
|
+
deliverableNames: node.deliverables.map((deliverable) => deliverable.name),
|
|
138
|
+
upstreamNodeIds: node.upstreamNodeIds,
|
|
139
|
+
downstreamNodeIds: node.downstreamNodeIds,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class AgentActivityService {
|
|
144
|
+
constructor(private readonly deps: AgentActivityDeps = { executionPlanService, workstreamService }) {}
|
|
145
|
+
|
|
146
|
+
async getBoard(userRef: string, orgRef: string): Promise<PlanBoardResponse> {
|
|
147
|
+
const activePlans = await this.getAllActivePlans(userRef, orgRef)
|
|
148
|
+
const cards = activePlans.flatMap(({ plan, workstream }) =>
|
|
149
|
+
plan.nodes.map((node) => planNodeToCard(node, plan, workstream.id, workstream.title)),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const columns: PlanBoardColumn[] = BOARD_COLUMN_ORDER.map((status) => ({
|
|
153
|
+
status,
|
|
154
|
+
label: COLUMN_LABELS[status],
|
|
155
|
+
nodes: cards.filter((card) => card.status === status),
|
|
156
|
+
}))
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
columns,
|
|
160
|
+
summary: {
|
|
161
|
+
totalNodes: cards.length,
|
|
162
|
+
completedNodes: cards.filter((card) => card.status === 'completed').length,
|
|
163
|
+
activePlanCount: activePlans.length,
|
|
164
|
+
pendingApprovalCount: cards.filter((card) => card.hasApproval).length,
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async getPlanView(orgRef: string, planRunId: string, userRef: string): Promise<PlanViewResponse | null> {
|
|
170
|
+
const activePlans = await this.getAllActivePlans(userRef, orgRef)
|
|
171
|
+
const match = activePlans.find(({ plan }) => plan.runId === planRunId)
|
|
172
|
+
if (!match) return null
|
|
173
|
+
|
|
174
|
+
const { plan, workstream } = match
|
|
175
|
+
return {
|
|
176
|
+
planRunId: plan.runId,
|
|
177
|
+
title: plan.title,
|
|
178
|
+
objective: plan.objective,
|
|
179
|
+
status: plan.status,
|
|
180
|
+
leadAgentId: plan.leadAgentId,
|
|
181
|
+
progress: { completed: plan.progress.completed + plan.progress.partial, total: plan.progress.total },
|
|
182
|
+
nodes: plan.nodes.map((node) => buildPlanViewNode(node, plan, workstream.id, workstream.title)),
|
|
183
|
+
edges: plan.edges.map((edge) => ({ from: edge.source, to: edge.target })),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async getMyTasks(userRef: string, orgRef: string): Promise<MyTasksResponse> {
|
|
188
|
+
const activePlans = await this.getAllActivePlans(userRef, orgRef)
|
|
189
|
+
const tasks: PlanNodeCard[] = []
|
|
190
|
+
|
|
191
|
+
for (const { plan, workstream } of activePlans) {
|
|
192
|
+
for (const node of plan.nodes) {
|
|
193
|
+
const humanOwned = node.owner.executorType === 'user'
|
|
194
|
+
const awaitingHuman = node.status === 'awaiting-human'
|
|
195
|
+
if (!humanOwned && !awaitingHuman) continue
|
|
196
|
+
|
|
197
|
+
tasks.push(planNodeToCard(node, plan, workstream.id, workstream.title))
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { tasks, pendingApprovalCount: tasks.filter((task) => task.hasApproval).length }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async getAgentActivity(userRef: string, orgRef: string): Promise<AgentActivityResponse> {
|
|
205
|
+
const activePlans = await this.getAllActivePlans(userRef, orgRef)
|
|
206
|
+
const activityByAgent = new Map<string, AgentActivityEntry>()
|
|
207
|
+
|
|
208
|
+
for (const agentId of agentRoster) {
|
|
209
|
+
activityByAgent.set(agentId, this.createEmptyEntry(agentId))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const { plan, workstream } of activePlans) {
|
|
213
|
+
const involvedAgents = new Set<string>()
|
|
214
|
+
|
|
215
|
+
for (const node of plan.nodes) {
|
|
216
|
+
if (node.owner.executorType !== 'agent') continue
|
|
217
|
+
|
|
218
|
+
const agentId = node.owner.ref
|
|
219
|
+
const entry = this.ensureEntry(activityByAgent, agentId)
|
|
220
|
+
involvedAgents.add(agentId)
|
|
221
|
+
incrementCounts(entry.counts, node.status)
|
|
222
|
+
|
|
223
|
+
if (!isCompletedStatus(node.status)) {
|
|
224
|
+
entry.tasks.push(planNodeToCard(node, plan, workstream.id, workstream.title))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (plan.leadAgentId.trim()) {
|
|
229
|
+
const leadEntry = this.ensureEntry(activityByAgent, plan.leadAgentId)
|
|
230
|
+
leadEntry.isLeadingActivePlan = true
|
|
231
|
+
involvedAgents.add(plan.leadAgentId)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const agentId of involvedAgents) {
|
|
235
|
+
const entry = this.ensureEntry(activityByAgent, agentId)
|
|
236
|
+
this.ensureProjectEntry(entry.projects, {
|
|
237
|
+
workstreamId: workstream.id,
|
|
238
|
+
workstreamTitle: workstream.title,
|
|
239
|
+
planRunId: plan.runId,
|
|
240
|
+
planTitle: plan.title,
|
|
241
|
+
status: plan.status,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
entry.isRunning = entry.isRunning || workstream.isRunning
|
|
245
|
+
entry.lastActiveAt = maxIsoDate(entry.lastActiveAt, workstream.updatedAt)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const userTasks = await this.getMyTasks(userRef, orgRef)
|
|
250
|
+
const agents = [...activityByAgent.values()].sort((left, right) => {
|
|
251
|
+
const leftIndex = agentRoster.indexOf(left.agentId)
|
|
252
|
+
const rightIndex = agentRoster.indexOf(right.agentId)
|
|
253
|
+
if (leftIndex !== -1 || rightIndex !== -1) {
|
|
254
|
+
if (leftIndex === -1) return 1
|
|
255
|
+
if (rightIndex === -1) return -1
|
|
256
|
+
return leftIndex - rightIndex
|
|
257
|
+
}
|
|
258
|
+
return left.agentId.localeCompare(right.agentId)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
agents,
|
|
263
|
+
userActivity: {
|
|
264
|
+
taskCount: userTasks.tasks.length,
|
|
265
|
+
pendingApprovalCount: userTasks.pendingApprovalCount,
|
|
266
|
+
awaitingHumanCount: userTasks.tasks.filter((task) => task.status === 'awaiting-human').length,
|
|
267
|
+
lastActiveAt: activePlans.reduce<string | null>(
|
|
268
|
+
(latest, entry) => maxIsoDate(latest, entry.workstream.updatedAt),
|
|
269
|
+
null,
|
|
270
|
+
),
|
|
271
|
+
},
|
|
272
|
+
totalActivePlans: activePlans.length,
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async getAllActivePlans(userRef: string, orgRef: string): Promise<ActivePlanEntry[]> {
|
|
277
|
+
const workstreams = await this.listRelevantWorkstreams(userRef, orgRef)
|
|
278
|
+
const planResults = await Promise.all(
|
|
279
|
+
workstreams.map(async (workstream) => {
|
|
280
|
+
try {
|
|
281
|
+
const plans = await this.deps.executionPlanService.getActivePlansForWorkstream(workstream.id)
|
|
282
|
+
return plans.map((plan) => ({ plan, workstream }))
|
|
283
|
+
} catch (error) {
|
|
284
|
+
serverLogger.error`Failed to load active plans for workstream ${workstream.id}: ${error}`
|
|
285
|
+
return []
|
|
286
|
+
}
|
|
287
|
+
}),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return planResults.flat()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async listRelevantWorkstreams(userRef: string, orgRef: string): Promise<NormalizedWorkstream[]> {
|
|
294
|
+
const [direct, core, group] = await Promise.all([
|
|
295
|
+
this.deps.workstreamService.listWorkstreams(userRef, orgRef, { mode: 'direct', includeArchived: false }),
|
|
296
|
+
this.deps.workstreamService.listWorkstreams(userRef, orgRef, {
|
|
297
|
+
mode: 'group',
|
|
298
|
+
core: true,
|
|
299
|
+
includeArchived: false,
|
|
300
|
+
}),
|
|
301
|
+
this.deps.workstreamService.listWorkstreams(userRef, orgRef, {
|
|
302
|
+
mode: 'group',
|
|
303
|
+
core: false,
|
|
304
|
+
includeArchived: false,
|
|
305
|
+
take: 500,
|
|
306
|
+
page: 1,
|
|
307
|
+
}),
|
|
308
|
+
])
|
|
309
|
+
|
|
310
|
+
const deduped = new Map<string, NormalizedWorkstream>()
|
|
311
|
+
for (const workstream of [...direct.workstreams, ...core.workstreams, ...group.workstreams]) {
|
|
312
|
+
deduped.set(workstream.id, workstream)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return [...deduped.values()]
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private createEmptyEntry(agentId: string): AgentActivityEntry {
|
|
319
|
+
return {
|
|
320
|
+
agentId,
|
|
321
|
+
counts: createEmptyCounts(),
|
|
322
|
+
tasks: [],
|
|
323
|
+
projects: [],
|
|
324
|
+
isLeadingActivePlan: false,
|
|
325
|
+
isRunning: false,
|
|
326
|
+
lastActiveAt: null,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private ensureEntry(entries: Map<string, AgentActivityEntry>, agentId: string): AgentActivityEntry {
|
|
331
|
+
const existing = entries.get(agentId)
|
|
332
|
+
if (existing) {
|
|
333
|
+
return existing
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const created = this.createEmptyEntry(agentId)
|
|
337
|
+
entries.set(agentId, created)
|
|
338
|
+
return created
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private ensureProjectEntry(projects: AgentProjectEntry[], next: AgentProjectEntry): void {
|
|
342
|
+
if (projects.some((project) => project.planRunId === next.planRunId)) {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
projects.push(next)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const agentActivityService = new AgentActivityService()
|
package/src/services/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SubmitExecutionNodeResultArgsSchema,
|
|
8
8
|
getLatestExecutionPlanResult,
|
|
9
9
|
} from '@lota-sdk/shared'
|
|
10
|
+
import type { PlanDraft } from '@lota-sdk/shared'
|
|
10
11
|
import { tool } from 'ai'
|
|
11
12
|
|
|
12
13
|
import type { RecordIdRef } from '../db/record-id'
|
|
@@ -17,17 +18,20 @@ export function createCreateExecutionPlanTool(params: {
|
|
|
17
18
|
workstreamId: RecordIdRef
|
|
18
19
|
agentId: string
|
|
19
20
|
onPlanChanged?: () => void
|
|
21
|
+
validateInlinePlan?: (draft: PlanDraft) => void
|
|
20
22
|
}) {
|
|
21
23
|
return tool({
|
|
22
24
|
description:
|
|
23
|
-
'Create a contract-driven execution plan for this workstream.
|
|
25
|
+
'Create a contract-driven execution plan for this workstream. Use createProjectWithPlan instead when the work needs a dedicated project workstream or a larger 3+ node plan.',
|
|
24
26
|
inputSchema: CreateExecutionPlanArgsSchema,
|
|
25
27
|
execute: async (input) => {
|
|
28
|
+
const { targetWorkstreamId, ...draft } = input
|
|
29
|
+
params.validateInlinePlan?.(draft)
|
|
26
30
|
const result = await executionPlanService.createPlan({
|
|
27
31
|
organizationId: params.orgId,
|
|
28
|
-
workstreamId: params.workstreamId,
|
|
32
|
+
workstreamId: targetWorkstreamId ?? params.workstreamId,
|
|
29
33
|
leadAgentId: params.agentId,
|
|
30
|
-
input,
|
|
34
|
+
input: draft,
|
|
31
35
|
})
|
|
32
36
|
params.onPlanChanged?.()
|
|
33
37
|
return result
|
package/src/tools/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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'
|
|
4
5
|
export * from './read-file-parts.tool'
|
|
5
6
|
export * from './remember-memory.tool'
|
|
6
7
|
export * from './research-topic.tool'
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
}
|