@lota-sdk/core 0.1.9 → 0.1.11
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_workstream.surql +1 -0
- package/infrastructure/schema/02_execution_plan.surql +202 -52
- package/package.json +4 -2
- package/src/bifrost/bifrost.ts +94 -25
- package/src/config/model-constants.ts +8 -6
- package/src/db/memory-store.ts +3 -71
- package/src/db/service.ts +42 -2
- package/src/db/tables.ts +9 -2
- package/src/embeddings/provider.ts +92 -21
- package/src/index.ts +6 -0
- package/src/redis/stream-context.ts +44 -0
- package/src/runtime/approval-continuation.ts +59 -0
- package/src/runtime/chat-request-routing.ts +5 -1
- package/src/runtime/execution-plan.ts +21 -14
- package/src/runtime/turn-lifecycle.ts +12 -4
- package/src/services/document-chunk.service.ts +2 -2
- package/src/services/execution-plan.service.ts +579 -786
- package/src/services/learned-skill.service.ts +2 -2
- package/src/services/plan-approval.service.ts +83 -0
- package/src/services/plan-artifact.service.ts +45 -0
- package/src/services/plan-builder.service.ts +61 -0
- package/src/services/plan-checkpoint.service.ts +53 -0
- package/src/services/plan-compiler.service.ts +81 -0
- package/src/services/plan-executor.service.ts +1623 -0
- package/src/services/plan-run.service.ts +422 -0
- package/src/services/plan-validator.service.ts +760 -0
- package/src/services/workstream-turn-preparation.ts +57 -15
- package/src/services/workstream-turn.ts +12 -0
- package/src/services/workstream.service.ts +26 -0
- package/src/services/workstream.types.ts +1 -0
- package/src/system-agents/title-generator.agent.ts +2 -2
- package/src/tools/execution-plan.tool.ts +20 -46
- package/src/tools/log-hello-world.tool.ts +17 -0
- package/src/workers/skill-extraction.runner.ts +2 -2
|
@@ -1,889 +1,682 @@
|
|
|
1
|
+
import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
PlanCheckpointSchema,
|
|
4
|
+
PlanEventSchema,
|
|
5
|
+
PlanNodeRunSchema,
|
|
6
|
+
PlanNodeSpecRecordSchema,
|
|
7
|
+
PlanRunSchema,
|
|
8
|
+
PlanSpecSchema,
|
|
6
9
|
} from '@lota-sdk/shared/schemas/execution-plan'
|
|
7
10
|
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
ExecutionPlanTaskResultStatus,
|
|
13
|
-
ExecutionPlanTaskStatus,
|
|
11
|
+
PlanNodeRunRecord,
|
|
12
|
+
PlanNodeSpecRecord,
|
|
13
|
+
PlanRunRecord,
|
|
14
|
+
PlanSpecRecord,
|
|
14
15
|
SerializableExecutionPlan,
|
|
15
|
-
SerializableExecutionPlanCarriedTask,
|
|
16
|
-
SerializableExecutionPlanEvent,
|
|
17
|
-
SerializableExecutionPlanTask,
|
|
18
16
|
} from '@lota-sdk/shared/schemas/execution-plan'
|
|
19
17
|
import type {
|
|
20
18
|
CreateExecutionPlanArgs,
|
|
21
19
|
ExecutionPlanToolResultData,
|
|
20
|
+
GetActiveExecutionPlanArgs,
|
|
22
21
|
ReplaceExecutionPlanArgs,
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
ResumeExecutionPlanRunArgs,
|
|
23
|
+
SubmitExecutionNodeResultArgs,
|
|
25
24
|
} from '@lota-sdk/shared/schemas/tools'
|
|
26
|
-
import {
|
|
25
|
+
import { RecordId } from 'surrealdb'
|
|
27
26
|
|
|
28
|
-
import type { RecordIdInput
|
|
27
|
+
import type { RecordIdInput } from '../db/record-id'
|
|
29
28
|
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
30
29
|
import { databaseService } from '../db/service'
|
|
30
|
+
import type { DatabaseTransaction } from '../db/service'
|
|
31
31
|
import { TABLES } from '../db/tables'
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function normalizeTaskInput(
|
|
49
|
-
input: CreateExecutionPlanArgs['tasks'][number]['input'],
|
|
50
|
-
): string | Record<string, unknown> | undefined {
|
|
51
|
-
if (input === undefined) return undefined
|
|
52
|
-
return typeof input === 'string' ? input : structuredClone(input)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isExecutionPlanRecord(value: RecordIdInput | ExecutionPlanRecord): value is ExecutionPlanRecord {
|
|
56
|
-
return typeof value === 'object' && 'workstreamId' in value
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isExecutionPlanTaskRecord(value: RecordIdInput | ExecutionPlanTaskRecord): value is ExecutionPlanTaskRecord {
|
|
60
|
-
return typeof value === 'object' && 'position' in value
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function toPlanId(planId: RecordIdInput | ExecutionPlanRecord): RecordIdRef {
|
|
64
|
-
return ensureRecordId(isExecutionPlanRecord(planId) ? planId.id : planId, TABLES.PLAN)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function toTaskId(taskId: RecordIdInput | ExecutionPlanTaskRecord): RecordIdRef {
|
|
68
|
-
return ensureRecordId(isExecutionPlanTaskRecord(taskId) ? taskId.id : taskId, TABLES.PLAN_TASK)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function toPlanIdString(planId: RecordIdInput | ExecutionPlanRecord): string {
|
|
72
|
-
return recordIdToString(toPlanId(planId), TABLES.PLAN)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function toTaskIdString(taskId: RecordIdInput | ExecutionPlanTaskRecord): string {
|
|
76
|
-
return recordIdToString(toTaskId(taskId), TABLES.PLAN_TASK)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function setDefinedOptionValue<TValue>(
|
|
80
|
-
target: Record<string, unknown>,
|
|
81
|
-
key: string,
|
|
82
|
-
value: TValue | null | undefined,
|
|
83
|
-
mapValue?: (value: TValue) => unknown,
|
|
84
|
-
) {
|
|
85
|
-
if (value === undefined || value === null) return
|
|
86
|
-
target[key] = mapValue ? mapValue(value) : value
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function pushOptionalAssignment<TValue>(
|
|
90
|
-
assignments: string[],
|
|
91
|
-
bindings: Record<string, unknown>,
|
|
92
|
-
key: string,
|
|
93
|
-
value: TValue | null | undefined,
|
|
94
|
-
mapValue?: (value: TValue) => unknown,
|
|
95
|
-
) {
|
|
96
|
-
if (value === undefined) return
|
|
97
|
-
if (value === null) {
|
|
98
|
-
assignments.push(`${key} = NONE`)
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
assignments.push(`${key} = $${key}`)
|
|
103
|
-
bindings[key] = mapValue ? mapValue(value) : value
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function serializeTask(task: ExecutionPlanTaskRecord): SerializableExecutionPlanTask {
|
|
32
|
+
import { readApprovalContinuationResponse } from '../runtime/approval-continuation'
|
|
33
|
+
import { extractMessageText } from '../runtime/workstream-chat-helpers'
|
|
34
|
+
import { planBuilderService } from './plan-builder.service'
|
|
35
|
+
import type { CompiledPlanNode } from './plan-compiler.service'
|
|
36
|
+
import { planCompilerService } from './plan-compiler.service'
|
|
37
|
+
import { planExecutorService } from './plan-executor.service'
|
|
38
|
+
import { planRunService } from './plan-run.service'
|
|
39
|
+
import { planValidatorService } from './plan-validator.service'
|
|
40
|
+
|
|
41
|
+
function buildToolResult(params: {
|
|
42
|
+
action: ExecutionPlanToolResultData['action']
|
|
43
|
+
plan: SerializableExecutionPlan | null
|
|
44
|
+
message: string
|
|
45
|
+
changedNodeId?: string
|
|
46
|
+
}): ExecutionPlanToolResultData {
|
|
107
47
|
return {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
ownerRef: task.ownerRef,
|
|
115
|
-
status: task.status,
|
|
116
|
-
resultStatus: task.resultStatus,
|
|
117
|
-
...(task.input !== undefined ? { input: task.input } : {}),
|
|
118
|
-
outputSummary: task.outputSummary,
|
|
119
|
-
blockedReason: task.blockedReason,
|
|
120
|
-
externalRef: task.externalRef,
|
|
121
|
-
retryCount: task.retryCount,
|
|
122
|
-
idempotencyKey: task.idempotencyKey,
|
|
123
|
-
startedAt: toOptionalIsoDateTimeString(task.startedAt),
|
|
124
|
-
completedAt: toOptionalIsoDateTimeString(task.completedAt),
|
|
48
|
+
action: params.action,
|
|
49
|
+
message: params.message,
|
|
50
|
+
...(params.changedNodeId ? { changedNodeId: params.changedNodeId } : {}),
|
|
51
|
+
...(params.plan ? { plan: params.plan } : {}),
|
|
52
|
+
hasPlan: params.plan !== null,
|
|
53
|
+
status: params.plan?.status,
|
|
125
54
|
}
|
|
126
55
|
}
|
|
127
56
|
|
|
128
|
-
function
|
|
129
|
-
return {
|
|
130
|
-
id: recordIdToString(event.id, TABLES.PLAN_EVENT),
|
|
131
|
-
taskId: event.taskId ? recordIdToString(event.taskId, TABLES.PLAN_TASK) : undefined,
|
|
132
|
-
eventType: event.eventType,
|
|
133
|
-
fromStatus: event.fromStatus,
|
|
134
|
-
toStatus: event.toStatus,
|
|
135
|
-
message: event.message,
|
|
136
|
-
detail: event.detail,
|
|
137
|
-
emittedBy: event.emittedBy,
|
|
138
|
-
createdAt: toIsoDateTimeString(event.createdAt),
|
|
139
|
-
}
|
|
57
|
+
function aggregateBlockingIssues(issues: Array<{ code: string; message: string }>): string {
|
|
58
|
+
return issues.map((issue) => `${issue.code}: ${issue.message}`).join(' | ')
|
|
140
59
|
}
|
|
141
60
|
|
|
142
|
-
function
|
|
143
|
-
const counts = tasks.reduce(
|
|
144
|
-
(summary, task) => {
|
|
145
|
-
summary[task.status] += 1
|
|
146
|
-
return summary
|
|
147
|
-
},
|
|
148
|
-
{ pending: 0, 'in-progress': 0, completed: 0, blocked: 0, failed: 0, skipped: 0 } satisfies Record<
|
|
149
|
-
ExecutionPlanTaskStatus,
|
|
150
|
-
number
|
|
151
|
-
>,
|
|
152
|
-
)
|
|
153
|
-
const total = tasks.length
|
|
154
|
-
const completedWork = counts.completed + counts.skipped
|
|
155
|
-
|
|
61
|
+
function toSpecData(spec: PlanSpecRecord, patch: Partial<PlanSpecRecord> & { replacedSpecId?: RecordIdInput | null }) {
|
|
156
62
|
return {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
63
|
+
organizationId: ensureRecordId(spec.organizationId, TABLES.ORGANIZATION),
|
|
64
|
+
workstreamId: ensureRecordId(spec.workstreamId, TABLES.WORKSTREAM),
|
|
65
|
+
title: patch.title ?? spec.title,
|
|
66
|
+
objective: patch.objective ?? spec.objective,
|
|
67
|
+
version: patch.version ?? spec.version,
|
|
68
|
+
status: patch.status ?? spec.status,
|
|
69
|
+
leadAgentId: patch.leadAgentId ?? spec.leadAgentId,
|
|
70
|
+
schemaRegistry: patch.schemaRegistry ? structuredClone(patch.schemaRegistry) : structuredClone(spec.schemaRegistry),
|
|
71
|
+
edges: patch.edges ? [...patch.edges] : [...spec.edges],
|
|
72
|
+
entryNodeIds: patch.entryNodeIds ? [...patch.entryNodeIds] : [...spec.entryNodeIds],
|
|
73
|
+
...(patch.replacedSpecId
|
|
74
|
+
? { replacedSpecId: ensureRecordId(patch.replacedSpecId, TABLES.PLAN_SPEC) }
|
|
75
|
+
: spec.replacedSpecId
|
|
76
|
+
? { replacedSpecId: ensureRecordId(spec.replacedSpecId, TABLES.PLAN_SPEC) }
|
|
77
|
+
: {}),
|
|
78
|
+
...(patch.compiledAt !== undefined
|
|
79
|
+
? { compiledAt: patch.compiledAt }
|
|
80
|
+
: spec.compiledAt
|
|
81
|
+
? { compiledAt: spec.compiledAt }
|
|
82
|
+
: {}),
|
|
165
83
|
}
|
|
166
84
|
}
|
|
167
85
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return tasks.find((task) => task.status === 'in-progress') ?? tasks.find((task) => task.status === 'pending') ?? null
|
|
86
|
+
type PlanRunUpdate = Omit<
|
|
87
|
+
Partial<PlanRunRecord>,
|
|
88
|
+
'currentNodeId' | 'waitingNodeId' | 'replacedRunId' | 'lastCheckpointId' | 'startedAt' | 'completedAt'
|
|
89
|
+
> & {
|
|
90
|
+
currentNodeId?: string | null
|
|
91
|
+
waitingNodeId?: string | null
|
|
92
|
+
replacedRunId?: RecordIdInput | null
|
|
93
|
+
lastCheckpointId?: RecordIdInput | null
|
|
94
|
+
startedAt?: Date | null
|
|
95
|
+
completedAt?: Date | null
|
|
179
96
|
}
|
|
180
97
|
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
98
|
+
function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
|
|
99
|
+
return {
|
|
100
|
+
planSpecId: ensureRecordId(run.planSpecId, TABLES.PLAN_SPEC),
|
|
101
|
+
organizationId: ensureRecordId(run.organizationId, TABLES.ORGANIZATION),
|
|
102
|
+
workstreamId: ensureRecordId(run.workstreamId, TABLES.WORKSTREAM),
|
|
103
|
+
leadAgentId: patch.leadAgentId ?? run.leadAgentId,
|
|
104
|
+
status: patch.status ?? run.status,
|
|
105
|
+
...(patch.currentNodeId === null
|
|
106
|
+
? {}
|
|
107
|
+
: patch.currentNodeId !== undefined
|
|
108
|
+
? { currentNodeId: patch.currentNodeId }
|
|
109
|
+
: run.currentNodeId
|
|
110
|
+
? { currentNodeId: run.currentNodeId }
|
|
111
|
+
: {}),
|
|
112
|
+
...(patch.waitingNodeId === null
|
|
113
|
+
? {}
|
|
114
|
+
: patch.waitingNodeId !== undefined
|
|
115
|
+
? { waitingNodeId: patch.waitingNodeId }
|
|
116
|
+
: run.waitingNodeId
|
|
117
|
+
? { waitingNodeId: run.waitingNodeId }
|
|
118
|
+
: {}),
|
|
119
|
+
readyNodeIds: patch.readyNodeIds ? [...patch.readyNodeIds] : [...run.readyNodeIds],
|
|
120
|
+
failureCount: patch.failureCount ?? run.failureCount,
|
|
121
|
+
...(patch.replacedRunId === null
|
|
122
|
+
? {}
|
|
123
|
+
: patch.replacedRunId
|
|
124
|
+
? { replacedRunId: ensureRecordId(patch.replacedRunId, TABLES.PLAN_RUN) }
|
|
125
|
+
: run.replacedRunId
|
|
126
|
+
? { replacedRunId: ensureRecordId(run.replacedRunId, TABLES.PLAN_RUN) }
|
|
127
|
+
: {}),
|
|
128
|
+
...(patch.lastCheckpointId === null
|
|
129
|
+
? {}
|
|
130
|
+
: patch.lastCheckpointId
|
|
131
|
+
? { lastCheckpointId: ensureRecordId(patch.lastCheckpointId, TABLES.PLAN_CHECKPOINT) }
|
|
132
|
+
: run.lastCheckpointId
|
|
133
|
+
? { lastCheckpointId: ensureRecordId(run.lastCheckpointId, TABLES.PLAN_CHECKPOINT) }
|
|
134
|
+
: {}),
|
|
135
|
+
...(patch.startedAt === null
|
|
136
|
+
? {}
|
|
137
|
+
: patch.startedAt !== undefined
|
|
138
|
+
? { startedAt: patch.startedAt }
|
|
139
|
+
: run.startedAt
|
|
140
|
+
? { startedAt: run.startedAt }
|
|
141
|
+
: {}),
|
|
142
|
+
...(patch.completedAt === null
|
|
143
|
+
? {}
|
|
144
|
+
: patch.completedAt !== undefined
|
|
145
|
+
? { completedAt: patch.completedAt }
|
|
146
|
+
: run.completedAt
|
|
147
|
+
? { completedAt: run.completedAt }
|
|
148
|
+
: {}),
|
|
196
149
|
}
|
|
197
|
-
|
|
198
|
-
throw new Error(`Cannot change execution task "${taskTitle}" from ${task.status} to ${nextStatus}.`)
|
|
199
150
|
}
|
|
200
151
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
|
|
152
|
+
function buildApprovalResponseFromMessages(
|
|
153
|
+
messages: ChatMessage[],
|
|
154
|
+
): { approvalId: string; response: Record<string, unknown> } | null {
|
|
155
|
+
for (const message of [...messages].reverse()) {
|
|
156
|
+
if (message.role !== 'assistant') continue
|
|
157
|
+
const approvalResponse = readApprovalContinuationResponse(message)
|
|
158
|
+
if (!approvalResponse) continue
|
|
159
|
+
|
|
160
|
+
const response: Record<string, unknown> = {
|
|
161
|
+
approved: approvalResponse.approved,
|
|
162
|
+
requiredEdits: [...approvalResponse.requiredEdits],
|
|
207
163
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
private async listPlanTasks(planId: RecordIdInput): Promise<ExecutionPlanTaskRecord[]> {
|
|
212
|
-
return await databaseService.findMany(
|
|
213
|
-
TABLES.PLAN_TASK,
|
|
214
|
-
{ planId: ensureRecordId(planId, TABLES.PLAN) },
|
|
215
|
-
ExecutionPlanTaskSchema,
|
|
216
|
-
{ orderBy: 'position', orderDir: 'ASC' },
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private async listPlanEvents(planId: RecordIdInput, limit = 20): Promise<ExecutionPlanEventRecord[]> {
|
|
221
|
-
return (
|
|
222
|
-
await databaseService.findMany(
|
|
223
|
-
TABLES.PLAN_EVENT,
|
|
224
|
-
{ planId: ensureRecordId(planId, TABLES.PLAN) },
|
|
225
|
-
ExecutionPlanEventSchema,
|
|
226
|
-
{ orderBy: 'createdAt', orderDir: 'DESC', limit },
|
|
227
|
-
)
|
|
228
|
-
).reverse()
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
private async emitEvent(params: {
|
|
232
|
-
planId: RecordIdInput
|
|
233
|
-
taskId?: RecordIdInput | null
|
|
234
|
-
eventType: ExecutionPlanEventRecord['eventType']
|
|
235
|
-
fromStatus?: string | null
|
|
236
|
-
toStatus?: string | null
|
|
237
|
-
message: string
|
|
238
|
-
detail?: Record<string, unknown> | null
|
|
239
|
-
emittedBy: string
|
|
240
|
-
}): Promise<void> {
|
|
241
|
-
const eventData: Record<string, unknown> = {
|
|
242
|
-
planId: ensureRecordId(params.planId, TABLES.PLAN),
|
|
243
|
-
eventType: params.eventType,
|
|
244
|
-
message: params.message,
|
|
245
|
-
emittedBy: params.emittedBy,
|
|
164
|
+
if (approvalResponse.comments) {
|
|
165
|
+
response.comments = approvalResponse.comments
|
|
246
166
|
}
|
|
247
|
-
setDefinedOptionValue(eventData, 'taskId', params.taskId, (taskId) => ensureRecordId(taskId, TABLES.PLAN_TASK))
|
|
248
|
-
setDefinedOptionValue(eventData, 'fromStatus', params.fromStatus)
|
|
249
|
-
setDefinedOptionValue(eventData, 'toStatus', params.toStatus)
|
|
250
|
-
setDefinedOptionValue(eventData, 'detail', params.detail)
|
|
251
167
|
|
|
252
|
-
|
|
168
|
+
return { approvalId: approvalResponse.approvalId, response }
|
|
253
169
|
}
|
|
254
170
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
status: ExecutionPlanStatus
|
|
258
|
-
currentTaskId?: RecordIdInput | null
|
|
259
|
-
restartFromTaskId?: RecordIdInput | null
|
|
260
|
-
failureCount?: number
|
|
261
|
-
startedAt?: Date | null
|
|
262
|
-
completedAt?: Date | null
|
|
263
|
-
emittedBy: string
|
|
264
|
-
message?: string
|
|
265
|
-
detail?: Record<string, unknown>
|
|
266
|
-
}): Promise<ExecutionPlanRecord> {
|
|
267
|
-
const bindings: Record<string, unknown> = { status: params.status }
|
|
268
|
-
const assignments = ['status = $status']
|
|
269
|
-
pushOptionalAssignment(assignments, bindings, 'currentTaskId', params.currentTaskId, (taskId) =>
|
|
270
|
-
ensureRecordId(taskId, TABLES.PLAN_TASK),
|
|
271
|
-
)
|
|
272
|
-
pushOptionalAssignment(assignments, bindings, 'restartFromTaskId', params.restartFromTaskId, (taskId) =>
|
|
273
|
-
ensureRecordId(taskId, TABLES.PLAN_TASK),
|
|
274
|
-
)
|
|
275
|
-
if (params.failureCount !== undefined) {
|
|
276
|
-
assignments.push('failureCount = $failureCount')
|
|
277
|
-
bindings.failureCount = params.failureCount
|
|
278
|
-
}
|
|
279
|
-
pushOptionalAssignment(assignments, bindings, 'startedAt', params.startedAt)
|
|
280
|
-
pushOptionalAssignment(assignments, bindings, 'completedAt', params.completedAt)
|
|
281
|
-
|
|
282
|
-
const rows = await databaseService.query<unknown>(
|
|
283
|
-
new BoundQuery(`UPDATE ONLY ${toPlanIdString(params.plan)} SET ${assignments.join(', ')} RETURN AFTER`, bindings),
|
|
284
|
-
)
|
|
285
|
-
const next = rows.at(0) ? ExecutionPlanSchema.parse(rows[0]) : null
|
|
286
|
-
|
|
287
|
-
if (!next) {
|
|
288
|
-
throw new Error(`Failed to update execution plan ${recordIdToString(params.plan.id, TABLES.PLAN)}`)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (params.plan.status !== next.status) {
|
|
292
|
-
await this.emitEvent({
|
|
293
|
-
planId: next.id,
|
|
294
|
-
eventType: 'plan-status-changed',
|
|
295
|
-
fromStatus: params.plan.status,
|
|
296
|
-
toStatus: next.status,
|
|
297
|
-
message: params.message ?? `Plan status changed from ${params.plan.status} to ${next.status}.`,
|
|
298
|
-
detail: params.detail,
|
|
299
|
-
emittedBy: params.emittedBy,
|
|
300
|
-
})
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return next
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
private async updateTaskRecord(params: {
|
|
307
|
-
task: ExecutionPlanTaskRecord
|
|
308
|
-
status: ExecutionPlanTaskStatus
|
|
309
|
-
resultStatus: ExecutionPlanTaskResultStatus
|
|
310
|
-
outputSummary?: string | null
|
|
311
|
-
blockedReason?: string | null
|
|
312
|
-
externalRef?: string | null
|
|
313
|
-
startedAt?: Date | null
|
|
314
|
-
completedAt?: Date | null
|
|
315
|
-
}): Promise<ExecutionPlanTaskRecord> {
|
|
316
|
-
const bindings: Record<string, unknown> = { status: params.status, resultStatus: params.resultStatus }
|
|
317
|
-
const assignments = ['status = $status', 'resultStatus = $resultStatus']
|
|
318
|
-
pushOptionalAssignment(assignments, bindings, 'outputSummary', params.outputSummary)
|
|
319
|
-
pushOptionalAssignment(assignments, bindings, 'blockedReason', params.blockedReason)
|
|
320
|
-
pushOptionalAssignment(assignments, bindings, 'externalRef', params.externalRef)
|
|
321
|
-
pushOptionalAssignment(assignments, bindings, 'startedAt', params.startedAt)
|
|
322
|
-
pushOptionalAssignment(assignments, bindings, 'completedAt', params.completedAt)
|
|
323
|
-
|
|
324
|
-
const rows = await databaseService.query<unknown>(
|
|
325
|
-
new BoundQuery(`UPDATE ONLY ${toTaskIdString(params.task)} SET ${assignments.join(', ')} RETURN AFTER`, bindings),
|
|
326
|
-
)
|
|
327
|
-
const next = rows.at(0) ? ExecutionPlanTaskSchema.parse(rows[0]) : null
|
|
328
|
-
if (!next) {
|
|
329
|
-
throw new Error(`Failed to update execution task ${recordIdToString(params.task.id, TABLES.PLAN_TASK)}`)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return next
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
private async getActivePlanRecord(workstreamId: RecordIdInput): Promise<ExecutionPlanRecord | null> {
|
|
336
|
-
const plans = await databaseService.findMany(
|
|
337
|
-
TABLES.PLAN,
|
|
338
|
-
{ workstreamId: ensureRecordId(workstreamId, TABLES.WORKSTREAM) },
|
|
339
|
-
ExecutionPlanSchema,
|
|
340
|
-
{ orderBy: 'updatedAt', orderDir: 'DESC' },
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
return plans.find((plan) => ACTIVE_PLAN_STATUS_SET.has(plan.status)) ?? null
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private async collectCarriedTasks(plan: ExecutionPlanRecord): Promise<SerializableExecutionPlanCarriedTask[]> {
|
|
347
|
-
const carried: SerializableExecutionPlanCarriedTask[] = []
|
|
348
|
-
let currentReplacedPlanId = plan.replacedPlanId ? ensureRecordId(plan.replacedPlanId, TABLES.PLAN) : null
|
|
349
|
-
let depth = 0
|
|
350
|
-
|
|
351
|
-
while (currentReplacedPlanId && depth < 5) {
|
|
352
|
-
const previousPlan = await databaseService.findOne(
|
|
353
|
-
TABLES.PLAN,
|
|
354
|
-
{ id: currentReplacedPlanId },
|
|
355
|
-
ExecutionPlanSchema,
|
|
356
|
-
)
|
|
357
|
-
if (!previousPlan) break
|
|
358
|
-
|
|
359
|
-
const previousTasks = await this.listPlanTasks(previousPlan.id)
|
|
360
|
-
const currentPlanId = recordIdToString(previousPlan.id, TABLES.PLAN)
|
|
361
|
-
carried.unshift(
|
|
362
|
-
...previousTasks
|
|
363
|
-
.filter(
|
|
364
|
-
(task) =>
|
|
365
|
-
CARRIED_RESULT_STATUS_SET.has(task.resultStatus) &&
|
|
366
|
-
typeof task.outputSummary === 'string' &&
|
|
367
|
-
task.outputSummary.trim().length > 0,
|
|
368
|
-
)
|
|
369
|
-
.map((task) => ({
|
|
370
|
-
planId: currentPlanId,
|
|
371
|
-
taskId: recordIdToString(task.id, TABLES.PLAN_TASK),
|
|
372
|
-
title: task.title,
|
|
373
|
-
resultStatus: task.resultStatus,
|
|
374
|
-
outputSummary: task.outputSummary ?? '',
|
|
375
|
-
})),
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
currentReplacedPlanId = previousPlan.replacedPlanId
|
|
379
|
-
? ensureRecordId(previousPlan.replacedPlanId, TABLES.PLAN)
|
|
380
|
-
: null
|
|
381
|
-
depth += 1
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return carried
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
private async toSerializablePlan(
|
|
388
|
-
plan: ExecutionPlanRecord,
|
|
389
|
-
options?: { includeEvents?: boolean },
|
|
390
|
-
): Promise<SerializableExecutionPlan> {
|
|
391
|
-
const tasks = await this.listPlanTasks(plan.id)
|
|
392
|
-
const recentEvents = options?.includeEvents ? await this.listPlanEvents(plan.id, 20) : []
|
|
393
|
-
const carriedTasks = await this.collectCarriedTasks(plan)
|
|
394
|
-
const currentTask = findCurrentTask(tasks, plan.currentTaskId)
|
|
395
|
-
|
|
396
|
-
return {
|
|
397
|
-
planId: recordIdToString(plan.id, TABLES.PLAN),
|
|
398
|
-
workstreamId: recordIdToString(plan.workstreamId, TABLES.WORKSTREAM),
|
|
399
|
-
organizationId: recordIdToString(plan.organizationId, TABLES.ORGANIZATION),
|
|
400
|
-
title: plan.title,
|
|
401
|
-
objective: plan.objective,
|
|
402
|
-
status: plan.status,
|
|
403
|
-
leadAgentId: plan.leadAgentId,
|
|
404
|
-
currentTaskId: currentTask ? recordIdToString(currentTask.id, TABLES.PLAN_TASK) : undefined,
|
|
405
|
-
restartFromTaskId: plan.restartFromTaskId
|
|
406
|
-
? recordIdToString(plan.restartFromTaskId, TABLES.PLAN_TASK)
|
|
407
|
-
: undefined,
|
|
408
|
-
replacedPlanId: plan.replacedPlanId ? recordIdToString(plan.replacedPlanId, TABLES.PLAN) : undefined,
|
|
409
|
-
failureCount: plan.failureCount,
|
|
410
|
-
startedAt: toOptionalIsoDateTimeString(plan.startedAt),
|
|
411
|
-
completedAt: toOptionalIsoDateTimeString(plan.completedAt),
|
|
412
|
-
progress: buildProgress(tasks),
|
|
413
|
-
tasks: tasks.map(serializeTask),
|
|
414
|
-
recentEvents: recentEvents.map(serializeEvent),
|
|
415
|
-
carriedTasks,
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
private buildToolResult(params: {
|
|
420
|
-
action: ExecutionPlanToolResultData['action']
|
|
421
|
-
plan: SerializableExecutionPlan | null
|
|
422
|
-
message?: string
|
|
423
|
-
changedTaskId?: string
|
|
424
|
-
}): ExecutionPlanToolResultData {
|
|
425
|
-
return {
|
|
426
|
-
action: params.action,
|
|
427
|
-
...(params.message ? { message: params.message } : {}),
|
|
428
|
-
...(params.changedTaskId !== undefined ? { changedTaskId: params.changedTaskId } : {}),
|
|
429
|
-
...(params.plan ? { plan: params.plan } : {}),
|
|
430
|
-
hasPlan: params.plan !== null,
|
|
431
|
-
status: params.plan?.status,
|
|
432
|
-
}
|
|
433
|
-
}
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
434
173
|
|
|
174
|
+
class ExecutionPlanService {
|
|
435
175
|
async hasActivePlan(workstreamId: RecordIdInput): Promise<boolean> {
|
|
436
|
-
return (await
|
|
176
|
+
return (await planRunService.getActiveRunRecord(workstreamId)) !== null
|
|
437
177
|
}
|
|
438
178
|
|
|
439
179
|
async getActivePlanForWorkstream(
|
|
440
180
|
workstreamId: RecordIdInput,
|
|
441
|
-
options?:
|
|
181
|
+
options?: Partial<GetActiveExecutionPlanArgs>,
|
|
442
182
|
): Promise<SerializableExecutionPlan | null> {
|
|
443
|
-
const
|
|
444
|
-
if (!
|
|
445
|
-
|
|
183
|
+
const run = await planRunService.getActiveRunRecord(workstreamId)
|
|
184
|
+
if (!run) return null
|
|
185
|
+
|
|
186
|
+
return await planRunService.toSerializablePlan(run, {
|
|
187
|
+
includeEvents: options?.includeEvents,
|
|
188
|
+
includeArtifacts: options?.includeArtifacts,
|
|
189
|
+
includeApprovals: options?.includeApprovals,
|
|
190
|
+
includeCheckpoints: options?.includeCheckpoints,
|
|
191
|
+
includeValidationIssues: options?.includeValidationIssues,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async getActivePlanToolResult(params: {
|
|
196
|
+
workstreamId: RecordIdInput
|
|
197
|
+
includeEvents?: boolean
|
|
198
|
+
includeArtifacts?: boolean
|
|
199
|
+
includeApprovals?: boolean
|
|
200
|
+
includeCheckpoints?: boolean
|
|
201
|
+
includeValidationIssues?: boolean
|
|
202
|
+
}): Promise<ExecutionPlanToolResultData> {
|
|
203
|
+
const plan = await this.getActivePlanForWorkstream(params.workstreamId, params)
|
|
204
|
+
return buildToolResult({
|
|
205
|
+
action: plan ? 'loaded' : 'none',
|
|
206
|
+
plan,
|
|
207
|
+
message: plan ? `Loaded execution run "${plan.title}".` : 'No active execution run.',
|
|
208
|
+
})
|
|
446
209
|
}
|
|
447
210
|
|
|
448
211
|
async createPlan(params: {
|
|
449
212
|
organizationId: RecordIdInput
|
|
450
213
|
workstreamId: RecordIdInput
|
|
451
|
-
leadAgentId:
|
|
214
|
+
leadAgentId: string
|
|
452
215
|
input: CreateExecutionPlanArgs
|
|
453
216
|
}): Promise<ExecutionPlanToolResultData> {
|
|
454
|
-
const
|
|
455
|
-
if (
|
|
456
|
-
throw new Error('An active execution
|
|
217
|
+
const activeRun = await planRunService.getActiveRunRecord(params.workstreamId)
|
|
218
|
+
if (activeRun) {
|
|
219
|
+
throw new Error('An active execution run already exists for this workstream. Replace it instead.')
|
|
457
220
|
}
|
|
458
221
|
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const planData: Record<string, unknown> = {
|
|
464
|
-
organizationId,
|
|
465
|
-
workstreamId,
|
|
466
|
-
title: params.input.title,
|
|
467
|
-
objective: params.input.objective,
|
|
468
|
-
status: 'draft',
|
|
469
|
-
leadAgentId: params.leadAgentId,
|
|
470
|
-
currentTaskId: taskIds[0],
|
|
471
|
-
failureCount: 0,
|
|
222
|
+
const preparedDraft = planBuilderService.prepareDraft(params.input)
|
|
223
|
+
const validation = planValidatorService.validateDraft(preparedDraft)
|
|
224
|
+
if (validation.blocking.length > 0) {
|
|
225
|
+
throw new Error(`Plan draft failed validation: ${aggregateBlockingIssues(validation.blocking)}`)
|
|
472
226
|
}
|
|
227
|
+
const compiled = planCompilerService.compile(preparedDraft)
|
|
228
|
+
|
|
229
|
+
const specId = new RecordId(TABLES.PLAN_SPEC, Bun.randomUUIDv7())
|
|
230
|
+
const runId = new RecordId(TABLES.PLAN_RUN, Bun.randomUUIDv7())
|
|
231
|
+
|
|
232
|
+
await databaseService.withTransaction(async (tx) => {
|
|
233
|
+
const spec = PlanSpecSchema.parse(
|
|
234
|
+
await tx
|
|
235
|
+
.create(specId)
|
|
236
|
+
.content({
|
|
237
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
238
|
+
workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
|
|
239
|
+
title: compiled.draft.title,
|
|
240
|
+
objective: compiled.draft.objective,
|
|
241
|
+
version: 1,
|
|
242
|
+
status: 'compiled',
|
|
243
|
+
leadAgentId: params.leadAgentId,
|
|
244
|
+
schemaRegistry: structuredClone(compiled.draft.schemas),
|
|
245
|
+
edges: [...compiled.draft.edges],
|
|
246
|
+
entryNodeIds: [...(compiled.draft.entryNodeIds ?? [])],
|
|
247
|
+
compiledAt: new Date(),
|
|
248
|
+
})
|
|
249
|
+
.output('after'),
|
|
250
|
+
)
|
|
473
251
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
idempotencyKey: `${planId.toString()}:${index}`,
|
|
491
|
-
}
|
|
492
|
-
setDefinedOptionValue(taskData, 'input', task.input, normalizeTaskInput)
|
|
493
|
-
|
|
494
|
-
await databaseService.createWithId(TABLES.PLAN_TASK, taskIds[index], taskData, ExecutionPlanTaskSchema)
|
|
495
|
-
}
|
|
252
|
+
const nodeSpecs = await this.createNodeSpecs(tx, spec.id, compiled.nodes)
|
|
253
|
+
const run = PlanRunSchema.parse(
|
|
254
|
+
await tx
|
|
255
|
+
.create(runId)
|
|
256
|
+
.content({
|
|
257
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
258
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
259
|
+
workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
|
|
260
|
+
leadAgentId: params.leadAgentId,
|
|
261
|
+
status: 'running',
|
|
262
|
+
readyNodeIds: [],
|
|
263
|
+
failureCount: 0,
|
|
264
|
+
startedAt: new Date(),
|
|
265
|
+
})
|
|
266
|
+
.output('after'),
|
|
267
|
+
)
|
|
496
268
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
269
|
+
const nodeRuns = await this.createNodeRuns(tx, run.id, spec.id, nodeSpecs)
|
|
270
|
+
const synced = await planExecutorService.syncRunGraph({
|
|
271
|
+
tx,
|
|
272
|
+
run,
|
|
273
|
+
spec,
|
|
274
|
+
nodeSpecs,
|
|
275
|
+
nodeRuns,
|
|
276
|
+
artifacts: [],
|
|
277
|
+
emittedBy: params.leadAgentId,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const event = await tx
|
|
281
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
282
|
+
.content({
|
|
283
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
284
|
+
runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
|
|
285
|
+
eventType: 'plan-created',
|
|
286
|
+
message: `Created execution plan "${spec.title}".`,
|
|
287
|
+
emittedBy: params.leadAgentId,
|
|
288
|
+
detail: { title: spec.title, objective: spec.objective, nodeCount: nodeSpecs.length },
|
|
289
|
+
})
|
|
290
|
+
.output('after')
|
|
291
|
+
PlanEventSchema.parse(event)
|
|
292
|
+
|
|
293
|
+
const checkpoint = PlanCheckpointSchema.parse(
|
|
294
|
+
await tx
|
|
295
|
+
.create(new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7()))
|
|
296
|
+
.content({
|
|
297
|
+
runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
|
|
298
|
+
sequence: 1,
|
|
299
|
+
runStatus: synced.run.status,
|
|
300
|
+
readyNodeIds: [...synced.run.readyNodeIds],
|
|
301
|
+
activeNodeIds: synced.run.currentNodeId ? [synced.run.currentNodeId] : [],
|
|
302
|
+
artifactIds: [],
|
|
303
|
+
lastCompletedNodeIds: synced.nodeRuns
|
|
304
|
+
.filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
|
|
305
|
+
.map((nodeRun) => nodeRun.nodeId),
|
|
306
|
+
snapshot: {
|
|
307
|
+
reason: 'plan-created',
|
|
308
|
+
currentNodeId: synced.run.currentNodeId,
|
|
309
|
+
waitingNodeId: synced.run.waitingNodeId,
|
|
310
|
+
readyNodeIds: synced.run.readyNodeIds,
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
.output('after'),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const updatedRun = PlanRunSchema.parse(
|
|
317
|
+
await tx
|
|
318
|
+
.update(ensureRecordId(synced.run.id, TABLES.PLAN_RUN))
|
|
319
|
+
.content(toRunData(synced.run, { lastCheckpointId: checkpoint.id }))
|
|
320
|
+
.output('after'),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
const checkpointEvent = await tx
|
|
324
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
325
|
+
.content({
|
|
326
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
327
|
+
runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
|
|
328
|
+
eventType: 'checkpoint-saved',
|
|
329
|
+
message: 'Saved checkpoint 1.',
|
|
330
|
+
emittedBy: 'system',
|
|
331
|
+
detail: { checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT), reason: 'plan-created' },
|
|
332
|
+
})
|
|
333
|
+
.output('after')
|
|
334
|
+
PlanEventSchema.parse(checkpointEvent)
|
|
503
335
|
})
|
|
504
336
|
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
337
|
+
const plan = await planRunService.toSerializablePlan(await planRunService.getRunById(runId), {
|
|
338
|
+
includeEvents: true,
|
|
339
|
+
includeArtifacts: true,
|
|
340
|
+
includeApprovals: true,
|
|
341
|
+
includeCheckpoints: true,
|
|
342
|
+
includeValidationIssues: true,
|
|
510
343
|
})
|
|
344
|
+
|
|
345
|
+
return buildToolResult({ action: 'created', plan, message: `Created execution plan "${plan.title}".` })
|
|
511
346
|
}
|
|
512
347
|
|
|
513
348
|
async replacePlan(params: {
|
|
514
349
|
workstreamId: RecordIdInput
|
|
515
350
|
organizationId: RecordIdInput
|
|
516
|
-
leadAgentId:
|
|
351
|
+
leadAgentId: string
|
|
517
352
|
input: ReplaceExecutionPlanArgs
|
|
518
353
|
}): Promise<ExecutionPlanToolResultData> {
|
|
519
|
-
const
|
|
520
|
-
if (!
|
|
521
|
-
throw new Error('No active execution
|
|
354
|
+
const activeRun = await planRunService.getActiveRunRecord(params.workstreamId)
|
|
355
|
+
if (!activeRun) {
|
|
356
|
+
throw new Error('No active execution run exists for this workstream.')
|
|
522
357
|
}
|
|
523
|
-
if (recordIdToString(
|
|
524
|
-
throw new Error('Only the active execution
|
|
358
|
+
if (recordIdToString(activeRun.id, TABLES.PLAN_RUN) !== params.input.runId) {
|
|
359
|
+
throw new Error('Only the active execution run can be replaced.')
|
|
525
360
|
}
|
|
526
361
|
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
status: 'aborted',
|
|
530
|
-
currentTaskId: activePlan.currentTaskId,
|
|
531
|
-
restartFromTaskId: activePlan.restartFromTaskId,
|
|
532
|
-
emittedBy: params.leadAgentId,
|
|
533
|
-
message: `Plan "${activePlan.title}" was replaced.`,
|
|
534
|
-
detail: { reason: params.input.reason },
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
const planId = new RecordId(TABLES.PLAN, Bun.randomUUIDv7())
|
|
538
|
-
const taskIds = params.input.tasks.map(() => new RecordId(TABLES.PLAN_TASK, Bun.randomUUIDv7()))
|
|
539
|
-
const organizationId = ensureRecordId(params.organizationId, TABLES.ORGANIZATION)
|
|
540
|
-
const workstreamId = ensureRecordId(params.workstreamId, TABLES.WORKSTREAM)
|
|
541
|
-
const nextPlanData: Record<string, unknown> = {
|
|
542
|
-
organizationId,
|
|
543
|
-
workstreamId,
|
|
362
|
+
const activeSpec = await planRunService.getPlanSpecById(activeRun.planSpecId)
|
|
363
|
+
const preparedDraft = planBuilderService.prepareDraft({
|
|
544
364
|
title: params.input.title,
|
|
545
365
|
objective: params.input.objective,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
366
|
+
nodes: params.input.nodes,
|
|
367
|
+
edges: params.input.edges,
|
|
368
|
+
entryNodeIds: params.input.entryNodeIds,
|
|
369
|
+
schemas: params.input.schemas,
|
|
370
|
+
})
|
|
371
|
+
const validation = planValidatorService.validateDraft(preparedDraft)
|
|
372
|
+
if (validation.blocking.length > 0) {
|
|
373
|
+
throw new Error(`Plan draft failed validation: ${aggregateBlockingIssues(validation.blocking)}`)
|
|
551
374
|
}
|
|
375
|
+
const compiled = planCompilerService.compile(preparedDraft)
|
|
552
376
|
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
for (const [index, task] of params.input.tasks.entries()) {
|
|
556
|
-
const taskData: Record<string, unknown> = {
|
|
557
|
-
planId,
|
|
558
|
-
organizationId,
|
|
559
|
-
workstreamId,
|
|
560
|
-
position: index,
|
|
561
|
-
title: task.title,
|
|
562
|
-
rationale: task.rationale,
|
|
563
|
-
kind: task.kind,
|
|
564
|
-
ownerType: task.ownerType,
|
|
565
|
-
ownerRef: task.ownerRef,
|
|
566
|
-
status: 'pending',
|
|
567
|
-
resultStatus: 'not-started',
|
|
568
|
-
retryCount: 0,
|
|
569
|
-
idempotencyKey: `${planId.toString()}:${index}`,
|
|
570
|
-
}
|
|
571
|
-
setDefinedOptionValue(taskData, 'input', task.input, normalizeTaskInput)
|
|
572
|
-
|
|
573
|
-
await databaseService.createWithId(TABLES.PLAN_TASK, taskIds[index], taskData, ExecutionPlanTaskSchema)
|
|
574
|
-
}
|
|
377
|
+
const specId = new RecordId(TABLES.PLAN_SPEC, Bun.randomUUIDv7())
|
|
378
|
+
const runId = new RecordId(TABLES.PLAN_RUN, Bun.randomUUIDv7())
|
|
575
379
|
|
|
576
|
-
await
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
380
|
+
await databaseService.withTransaction(async (tx) => {
|
|
381
|
+
const supersededSpec = PlanSpecSchema.parse(
|
|
382
|
+
await tx
|
|
383
|
+
.update(ensureRecordId(activeSpec.id, TABLES.PLAN_SPEC))
|
|
384
|
+
.content(toSpecData(activeSpec, { status: 'superseded' }))
|
|
385
|
+
.output('after'),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
const abortedRun = PlanRunSchema.parse(
|
|
389
|
+
await tx
|
|
390
|
+
.update(ensureRecordId(activeRun.id, TABLES.PLAN_RUN))
|
|
391
|
+
.content(
|
|
392
|
+
toRunData(activeRun, {
|
|
393
|
+
status: 'aborted',
|
|
394
|
+
currentNodeId: null,
|
|
395
|
+
waitingNodeId: null,
|
|
396
|
+
readyNodeIds: [],
|
|
397
|
+
completedAt: new Date(),
|
|
398
|
+
}),
|
|
399
|
+
)
|
|
400
|
+
.output('after'),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
const spec = PlanSpecSchema.parse(
|
|
404
|
+
await tx
|
|
405
|
+
.create(specId)
|
|
406
|
+
.content({
|
|
407
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
408
|
+
workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
|
|
409
|
+
title: compiled.draft.title,
|
|
410
|
+
objective: compiled.draft.objective,
|
|
411
|
+
version: supersededSpec.version + 1,
|
|
412
|
+
status: 'compiled',
|
|
413
|
+
leadAgentId: params.leadAgentId,
|
|
414
|
+
schemaRegistry: structuredClone(compiled.draft.schemas),
|
|
415
|
+
edges: [...compiled.draft.edges],
|
|
416
|
+
entryNodeIds: [...(compiled.draft.entryNodeIds ?? [])],
|
|
417
|
+
replacedSpecId: ensureRecordId(supersededSpec.id, TABLES.PLAN_SPEC),
|
|
418
|
+
compiledAt: new Date(),
|
|
419
|
+
})
|
|
420
|
+
.output('after'),
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
const nodeSpecs = await this.createNodeSpecs(tx, spec.id, compiled.nodes)
|
|
424
|
+
const run = PlanRunSchema.parse(
|
|
425
|
+
await tx
|
|
426
|
+
.create(runId)
|
|
427
|
+
.content({
|
|
428
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
429
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
430
|
+
workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
|
|
431
|
+
leadAgentId: params.leadAgentId,
|
|
432
|
+
status: 'running',
|
|
433
|
+
readyNodeIds: [],
|
|
434
|
+
failureCount: 0,
|
|
435
|
+
replacedRunId: ensureRecordId(abortedRun.id, TABLES.PLAN_RUN),
|
|
436
|
+
startedAt: new Date(),
|
|
437
|
+
})
|
|
438
|
+
.output('after'),
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
const nodeRuns = await this.createNodeRuns(tx, run.id, spec.id, nodeSpecs)
|
|
442
|
+
const synced = await planExecutorService.syncRunGraph({
|
|
443
|
+
tx,
|
|
444
|
+
run,
|
|
445
|
+
spec,
|
|
446
|
+
nodeSpecs,
|
|
447
|
+
nodeRuns,
|
|
448
|
+
artifacts: [],
|
|
449
|
+
emittedBy: params.leadAgentId,
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
const replaceEvent = await tx
|
|
453
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
454
|
+
.content({
|
|
455
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
456
|
+
runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
|
|
457
|
+
eventType: 'plan-replaced',
|
|
458
|
+
message: `Replaced execution plan "${activeSpec.title}" with "${spec.title}".`,
|
|
459
|
+
emittedBy: params.leadAgentId,
|
|
460
|
+
detail: { reason: params.input.reason, replacedRunId: recordIdToString(abortedRun.id, TABLES.PLAN_RUN) },
|
|
461
|
+
})
|
|
462
|
+
.output('after')
|
|
463
|
+
PlanEventSchema.parse(replaceEvent)
|
|
464
|
+
|
|
465
|
+
const checkpoint = PlanCheckpointSchema.parse(
|
|
466
|
+
await tx
|
|
467
|
+
.create(new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7()))
|
|
468
|
+
.content({
|
|
469
|
+
runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
|
|
470
|
+
sequence: 1,
|
|
471
|
+
runStatus: synced.run.status,
|
|
472
|
+
readyNodeIds: [...synced.run.readyNodeIds],
|
|
473
|
+
activeNodeIds: synced.run.currentNodeId ? [synced.run.currentNodeId] : [],
|
|
474
|
+
artifactIds: [],
|
|
475
|
+
lastCompletedNodeIds: synced.nodeRuns
|
|
476
|
+
.filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
|
|
477
|
+
.map((nodeRun) => nodeRun.nodeId),
|
|
478
|
+
snapshot: {
|
|
479
|
+
reason: 'plan-replaced',
|
|
480
|
+
currentNodeId: synced.run.currentNodeId,
|
|
481
|
+
waitingNodeId: synced.run.waitingNodeId,
|
|
482
|
+
readyNodeIds: synced.run.readyNodeIds,
|
|
483
|
+
},
|
|
484
|
+
})
|
|
485
|
+
.output('after'),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
const updatedRun = PlanRunSchema.parse(
|
|
489
|
+
await tx
|
|
490
|
+
.update(ensureRecordId(synced.run.id, TABLES.PLAN_RUN))
|
|
491
|
+
.content(toRunData(synced.run, { lastCheckpointId: checkpoint.id }))
|
|
492
|
+
.output('after'),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
const checkpointEvent = await tx
|
|
496
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
497
|
+
.content({
|
|
498
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
499
|
+
runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
|
|
500
|
+
eventType: 'checkpoint-saved',
|
|
501
|
+
message: 'Saved checkpoint 1.',
|
|
502
|
+
emittedBy: 'system',
|
|
503
|
+
detail: { checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT), reason: 'plan-replaced' },
|
|
504
|
+
})
|
|
505
|
+
.output('after')
|
|
506
|
+
PlanEventSchema.parse(checkpointEvent)
|
|
605
507
|
})
|
|
606
508
|
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
509
|
+
const plan = await planRunService.toSerializablePlan(await planRunService.getRunById(runId), {
|
|
510
|
+
includeEvents: true,
|
|
511
|
+
includeArtifacts: true,
|
|
512
|
+
includeApprovals: true,
|
|
513
|
+
includeCheckpoints: true,
|
|
514
|
+
includeValidationIssues: true,
|
|
612
515
|
})
|
|
516
|
+
|
|
517
|
+
return buildToolResult({ action: 'replaced', plan, message: `Replaced execution plan with "${plan.title}".` })
|
|
613
518
|
}
|
|
614
519
|
|
|
615
|
-
async
|
|
520
|
+
async submitNodeResult(params: {
|
|
616
521
|
workstreamId: RecordIdInput
|
|
617
|
-
|
|
522
|
+
emittedBy: string
|
|
523
|
+
input: SubmitExecutionNodeResultArgs
|
|
618
524
|
}): Promise<ExecutionPlanToolResultData> {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
525
|
+
return await planExecutorService.submitNodeResult({
|
|
526
|
+
workstreamId: params.workstreamId,
|
|
527
|
+
runId: params.input.runId,
|
|
528
|
+
nodeId: params.input.nodeId,
|
|
529
|
+
emittedBy: params.emittedBy,
|
|
530
|
+
result: params.input.result,
|
|
624
531
|
})
|
|
625
532
|
}
|
|
626
533
|
|
|
627
|
-
async
|
|
534
|
+
async resumeRun(params: {
|
|
628
535
|
workstreamId: RecordIdInput
|
|
629
536
|
emittedBy: string
|
|
630
|
-
input:
|
|
537
|
+
input: ResumeExecutionPlanRunArgs
|
|
631
538
|
}): Promise<ExecutionPlanToolResultData> {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
if (recordIdToString(activePlan.id, TABLES.PLAN) !== params.input.planId) {
|
|
637
|
-
throw new Error('Task updates must target the active execution plan.')
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const task = await this.getTaskById(params.input.taskId)
|
|
641
|
-
if (recordIdToString(task.planId, TABLES.PLAN) !== params.input.planId) {
|
|
642
|
-
throw new Error('Execution task does not belong to the provided plan.')
|
|
643
|
-
}
|
|
644
|
-
assertTaskStatusTransition(task, params.input.status, task.title)
|
|
645
|
-
|
|
646
|
-
const tasks = await this.listPlanTasks(activePlan.id)
|
|
647
|
-
const otherInProgressTask = tasks.find(
|
|
648
|
-
(candidate) =>
|
|
649
|
-
candidate.status === 'in-progress' &&
|
|
650
|
-
recordIdToString(candidate.id, TABLES.PLAN_TASK) !== recordIdToString(task.id, TABLES.PLAN_TASK),
|
|
651
|
-
)
|
|
652
|
-
if (params.input.status === 'in-progress' && otherInProgressTask) {
|
|
653
|
-
throw new Error('Only one execution-plan task may be in progress at a time.')
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
if (params.input.status === 'pending') {
|
|
657
|
-
throw new Error('Use restartExecutionTask to reset a task back to pending.')
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if (params.input.status === 'completed') {
|
|
661
|
-
if (!params.input.resultStatus) {
|
|
662
|
-
throw new Error('Completed tasks require resultStatus.')
|
|
663
|
-
}
|
|
664
|
-
if (!SUCCESSFUL_RESULT_STATUS_SET.has(params.input.resultStatus)) {
|
|
665
|
-
throw new Error('Completed tasks require resultStatus to be success or partial.')
|
|
666
|
-
}
|
|
667
|
-
if (!params.input.outputSummary?.trim() && !params.input.externalRef?.trim()) {
|
|
668
|
-
throw new Error('Completed tasks require outputSummary or externalRef.')
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (params.input.status === 'blocked' && !params.input.blockedReason?.trim()) {
|
|
673
|
-
throw new Error('Blocked tasks require blockedReason.')
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
if (params.input.status === 'failed' && !params.input.outputSummary?.trim()) {
|
|
677
|
-
throw new Error('Failed tasks require outputSummary.')
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const now = new Date()
|
|
681
|
-
let nextPlan = activePlan
|
|
682
|
-
let nextResultStatus = task.resultStatus
|
|
683
|
-
let nextFailureCount = activePlan.failureCount
|
|
684
|
-
let taskStartedAt: Date | null | undefined = task.startedAt
|
|
685
|
-
? undefined
|
|
686
|
-
: params.input.status === 'in-progress'
|
|
687
|
-
? now
|
|
688
|
-
: undefined
|
|
689
|
-
let taskCompletedAt: Date | null | undefined = undefined
|
|
690
|
-
let message = ''
|
|
691
|
-
|
|
692
|
-
if (params.input.status === 'in-progress') {
|
|
693
|
-
nextPlan = await this.updatePlanStatus({
|
|
694
|
-
plan: activePlan,
|
|
695
|
-
status: 'executing',
|
|
696
|
-
currentTaskId: task.id,
|
|
697
|
-
restartFromTaskId: null,
|
|
698
|
-
...(activePlan.startedAt ? {} : { startedAt: now }),
|
|
699
|
-
emittedBy: params.emittedBy,
|
|
700
|
-
})
|
|
701
|
-
nextResultStatus = task.resultStatus
|
|
702
|
-
message = `Started task "${task.title}".`
|
|
703
|
-
} else if (params.input.status === 'completed' || params.input.status === 'skipped') {
|
|
704
|
-
nextResultStatus =
|
|
705
|
-
params.input.status === 'completed'
|
|
706
|
-
? ExecutionPlanTaskResultStatusSchema.parse(params.input.resultStatus)
|
|
707
|
-
: (params.input.resultStatus ?? 'not-started')
|
|
708
|
-
taskCompletedAt = now
|
|
709
|
-
|
|
710
|
-
const refreshedTasks = tasks.map((candidate) =>
|
|
711
|
-
recordIdToString(candidate.id, TABLES.PLAN_TASK) === recordIdToString(task.id, TABLES.PLAN_TASK)
|
|
712
|
-
? { ...candidate, status: params.input.status, resultStatus: nextResultStatus }
|
|
713
|
-
: candidate,
|
|
714
|
-
)
|
|
715
|
-
const nextPendingTask =
|
|
716
|
-
refreshedTasks.find((candidate) => candidate.position > task.position && candidate.status === 'pending') ??
|
|
717
|
-
refreshedTasks.find((candidate) => candidate.status === 'pending') ??
|
|
718
|
-
null
|
|
719
|
-
|
|
720
|
-
if (nextPendingTask) {
|
|
721
|
-
nextPlan = await this.updatePlanStatus({
|
|
722
|
-
plan: activePlan,
|
|
723
|
-
status: 'executing',
|
|
724
|
-
currentTaskId: nextPendingTask.id,
|
|
725
|
-
restartFromTaskId: null,
|
|
726
|
-
failureCount: 0,
|
|
727
|
-
emittedBy: params.emittedBy,
|
|
728
|
-
})
|
|
729
|
-
} else {
|
|
730
|
-
nextPlan = await this.updatePlanStatus({
|
|
731
|
-
plan: activePlan,
|
|
732
|
-
status: 'completed',
|
|
733
|
-
currentTaskId: null,
|
|
734
|
-
restartFromTaskId: null,
|
|
735
|
-
failureCount: 0,
|
|
736
|
-
completedAt: now,
|
|
737
|
-
...(activePlan.startedAt ? {} : { startedAt: now }),
|
|
738
|
-
emittedBy: params.emittedBy,
|
|
739
|
-
})
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
nextFailureCount = 0
|
|
743
|
-
message =
|
|
744
|
-
params.input.status === 'completed' ? `Completed task "${task.title}".` : `Skipped task "${task.title}".`
|
|
745
|
-
} else {
|
|
746
|
-
nextFailureCount = activePlan.failureCount + 1
|
|
747
|
-
nextResultStatus =
|
|
748
|
-
params.input.status === 'failed'
|
|
749
|
-
? 'failed'
|
|
750
|
-
: (params.input.resultStatus ?? (params.input.outputSummary?.trim() ? 'partial' : task.resultStatus))
|
|
751
|
-
nextPlan = await this.updatePlanStatus({
|
|
752
|
-
plan: activePlan,
|
|
753
|
-
status: 'blocked',
|
|
754
|
-
currentTaskId: task.id,
|
|
755
|
-
restartFromTaskId: task.id,
|
|
756
|
-
failureCount: nextFailureCount,
|
|
757
|
-
emittedBy: params.emittedBy,
|
|
758
|
-
})
|
|
759
|
-
if (params.input.status === 'failed') {
|
|
760
|
-
taskCompletedAt = now
|
|
761
|
-
}
|
|
762
|
-
message = params.input.status === 'failed' ? `Task "${task.title}" failed.` : `Task "${task.title}" is blocked.`
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const updatedTask = await this.updateTaskRecord({
|
|
766
|
-
task,
|
|
767
|
-
status: params.input.status,
|
|
768
|
-
resultStatus: nextResultStatus,
|
|
769
|
-
...(params.input.outputSummary !== undefined ? { outputSummary: params.input.outputSummary || null } : {}),
|
|
770
|
-
...(params.input.blockedReason !== undefined ? { blockedReason: params.input.blockedReason || null } : {}),
|
|
771
|
-
...(params.input.externalRef !== undefined ? { externalRef: params.input.externalRef || null } : {}),
|
|
772
|
-
...(params.input.status === 'in-progress' && taskStartedAt ? { startedAt: taskStartedAt } : {}),
|
|
773
|
-
...(taskCompletedAt !== undefined ? { completedAt: taskCompletedAt } : {}),
|
|
774
|
-
})
|
|
775
|
-
|
|
776
|
-
const eventType =
|
|
777
|
-
params.input.status === 'in-progress'
|
|
778
|
-
? 'task-started'
|
|
779
|
-
: params.input.status === 'completed'
|
|
780
|
-
? 'task-completed'
|
|
781
|
-
: params.input.status === 'blocked'
|
|
782
|
-
? 'task-blocked'
|
|
783
|
-
: params.input.status === 'failed'
|
|
784
|
-
? 'task-failed'
|
|
785
|
-
: 'task-skipped'
|
|
786
|
-
|
|
787
|
-
await this.emitEvent({
|
|
788
|
-
planId: nextPlan.id,
|
|
789
|
-
taskId: updatedTask.id,
|
|
790
|
-
eventType,
|
|
791
|
-
fromStatus: task.status,
|
|
792
|
-
toStatus: updatedTask.status,
|
|
793
|
-
message,
|
|
794
|
-
detail: {
|
|
795
|
-
resultStatus: updatedTask.resultStatus,
|
|
796
|
-
outputSummary: updatedTask.outputSummary,
|
|
797
|
-
blockedReason: updatedTask.blockedReason,
|
|
798
|
-
externalRef: updatedTask.externalRef,
|
|
799
|
-
failureCount: nextFailureCount,
|
|
800
|
-
},
|
|
539
|
+
return await planExecutorService.resumeRun({
|
|
540
|
+
workstreamId: params.workstreamId,
|
|
541
|
+
runId: params.input.runId,
|
|
801
542
|
emittedBy: params.emittedBy,
|
|
802
543
|
})
|
|
544
|
+
}
|
|
803
545
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
546
|
+
async applyApprovalResponseFromMessages(params: {
|
|
547
|
+
workstreamId: RecordIdInput
|
|
548
|
+
approvalMessages: ChatMessage[]
|
|
549
|
+
respondedBy: string
|
|
550
|
+
}): Promise<SerializableExecutionPlan | null> {
|
|
551
|
+
const approvalResponse = buildApprovalResponseFromMessages(params.approvalMessages)
|
|
552
|
+
if (!approvalResponse) return null
|
|
553
|
+
|
|
554
|
+
return await planExecutorService.submitHumanNodeResponse({
|
|
555
|
+
workstreamId: params.workstreamId,
|
|
556
|
+
approvalId: approvalResponse.approvalId,
|
|
557
|
+
respondedBy: params.respondedBy,
|
|
558
|
+
response: approvalResponse.response,
|
|
813
559
|
})
|
|
814
560
|
}
|
|
815
561
|
|
|
816
|
-
async
|
|
562
|
+
async respondToApproval(params: {
|
|
817
563
|
workstreamId: RecordIdInput
|
|
818
564
|
emittedBy: string
|
|
819
|
-
input:
|
|
820
|
-
}): Promise<
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
565
|
+
input: { approvalId: string; response: Record<string, unknown>; approvalMessageId?: string }
|
|
566
|
+
}): Promise<SerializableExecutionPlan | null> {
|
|
567
|
+
return await planExecutorService.submitHumanNodeResponse({
|
|
568
|
+
workstreamId: params.workstreamId,
|
|
569
|
+
approvalId: params.input.approvalId,
|
|
570
|
+
respondedBy: params.emittedBy,
|
|
571
|
+
response: params.input.response,
|
|
572
|
+
approvalMessageId: params.input.approvalMessageId,
|
|
573
|
+
})
|
|
574
|
+
}
|
|
828
575
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
576
|
+
async applyHumanInputFromUserMessage(params: {
|
|
577
|
+
workstreamId: RecordIdInput
|
|
578
|
+
message: ChatMessage
|
|
579
|
+
respondedBy: string
|
|
580
|
+
}): Promise<SerializableExecutionPlan | null> {
|
|
581
|
+
const run = await planRunService.getActiveRunRecord(params.workstreamId)
|
|
582
|
+
if (!run || run.status !== 'awaiting-human' || !run.waitingNodeId) return null
|
|
583
|
+
|
|
584
|
+
const nodeSpec = await planRunService.getNodeSpecByNodeId(run.planSpecId, run.waitingNodeId)
|
|
585
|
+
if (nodeSpec.type === 'human-approval') {
|
|
586
|
+
return null
|
|
833
587
|
}
|
|
834
|
-
if (
|
|
835
|
-
|
|
588
|
+
if (
|
|
589
|
+
nodeSpec.type !== 'human-input' &&
|
|
590
|
+
nodeSpec.type !== 'human-review-edit' &&
|
|
591
|
+
nodeSpec.type !== 'human-decision'
|
|
592
|
+
) {
|
|
593
|
+
return null
|
|
836
594
|
}
|
|
837
595
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
596
|
+
const response = {
|
|
597
|
+
responseText: extractMessageText(params.message).trim(),
|
|
598
|
+
messageId: params.message.id,
|
|
599
|
+
approved: nodeSpec.type === 'human-decision' ? true : undefined,
|
|
600
|
+
} satisfies Record<string, unknown>
|
|
601
|
+
|
|
602
|
+
return await planExecutorService.submitHumanNodeResponse({
|
|
603
|
+
workstreamId: params.workstreamId,
|
|
604
|
+
respondedBy: params.respondedBy,
|
|
605
|
+
response,
|
|
606
|
+
approvalMessageId: params.message.id,
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private async createNodeSpecs(
|
|
611
|
+
tx: DatabaseTransaction,
|
|
612
|
+
planSpecId: RecordIdInput,
|
|
613
|
+
nodes: CompiledPlanNode[],
|
|
614
|
+
): Promise<PlanNodeSpecRecord[]> {
|
|
615
|
+
const createdRecords: PlanNodeSpecRecord[] = []
|
|
616
|
+
|
|
617
|
+
for (const compiledNode of nodes) {
|
|
618
|
+
const nodeSpecId = new RecordId(TABLES.PLAN_NODE_SPEC, Bun.randomUUIDv7())
|
|
619
|
+
const created = await tx
|
|
620
|
+
.create(nodeSpecId)
|
|
621
|
+
.content({
|
|
622
|
+
planSpecId: ensureRecordId(planSpecId, TABLES.PLAN_SPEC),
|
|
623
|
+
nodeId: compiledNode.node.id,
|
|
624
|
+
position: compiledNode.position,
|
|
625
|
+
type: compiledNode.node.type,
|
|
626
|
+
label: compiledNode.node.label,
|
|
627
|
+
owner: compiledNode.node.owner,
|
|
628
|
+
objective: compiledNode.node.objective,
|
|
629
|
+
instructions: compiledNode.node.instructions,
|
|
630
|
+
...(compiledNode.node.inputSchemaRef ? { inputSchemaRef: compiledNode.node.inputSchemaRef } : {}),
|
|
631
|
+
...(compiledNode.node.outputSchemaRef ? { outputSchemaRef: compiledNode.node.outputSchemaRef } : {}),
|
|
632
|
+
deliverables: [...compiledNode.node.deliverables],
|
|
633
|
+
successCriteria: [...compiledNode.node.successCriteria],
|
|
634
|
+
completionChecks: [...compiledNode.node.completionChecks],
|
|
635
|
+
retryPolicy: { ...compiledNode.node.retryPolicy, retryOn: [...compiledNode.node.retryPolicy.retryOn] },
|
|
636
|
+
failurePolicy: [...compiledNode.node.failurePolicy],
|
|
637
|
+
...(compiledNode.node.timeoutMs ? { timeoutMs: compiledNode.node.timeoutMs } : {}),
|
|
638
|
+
toolPolicy: { allow: [...compiledNode.node.toolPolicy.allow], deny: [...compiledNode.node.toolPolicy.deny] },
|
|
639
|
+
contextPolicy: {
|
|
640
|
+
retrievalScopes: [...compiledNode.node.contextPolicy.retrievalScopes],
|
|
641
|
+
attachmentPolicy: compiledNode.node.contextPolicy.attachmentPolicy,
|
|
642
|
+
webPolicy: compiledNode.node.contextPolicy.webPolicy,
|
|
643
|
+
},
|
|
644
|
+
upstreamNodeIds: [...compiledNode.upstreamNodeIds],
|
|
645
|
+
downstreamNodeIds: [...compiledNode.downstreamNodeIds],
|
|
646
|
+
})
|
|
647
|
+
.output('after')
|
|
648
|
+
|
|
649
|
+
createdRecords.push(PlanNodeSpecRecordSchema.parse(created))
|
|
856
650
|
}
|
|
857
651
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
status: 'executing',
|
|
861
|
-
currentTaskId: targetTask.id,
|
|
862
|
-
restartFromTaskId: targetTask.id,
|
|
863
|
-
failureCount: 0,
|
|
864
|
-
completedAt: null,
|
|
865
|
-
...(activePlan.startedAt ? {} : { startedAt: new Date() }),
|
|
866
|
-
emittedBy: params.emittedBy,
|
|
867
|
-
})
|
|
652
|
+
return createdRecords
|
|
653
|
+
}
|
|
868
654
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
655
|
+
private async createNodeRuns(
|
|
656
|
+
tx: DatabaseTransaction,
|
|
657
|
+
runId: RecordIdInput,
|
|
658
|
+
planSpecId: RecordIdInput,
|
|
659
|
+
nodeSpecs: PlanNodeSpecRecord[],
|
|
660
|
+
): Promise<PlanNodeRunRecord[]> {
|
|
661
|
+
const createdNodeRuns: PlanNodeRunRecord[] = []
|
|
662
|
+
for (const nodeSpec of nodeSpecs) {
|
|
663
|
+
const nodeRunId = new RecordId(TABLES.PLAN_NODE_RUN, Bun.randomUUIDv7())
|
|
664
|
+
const created = await tx
|
|
665
|
+
.create(nodeRunId)
|
|
666
|
+
.content({
|
|
667
|
+
runId: ensureRecordId(runId, TABLES.PLAN_RUN),
|
|
668
|
+
planSpecId: ensureRecordId(planSpecId, TABLES.PLAN_SPEC),
|
|
669
|
+
nodeId: nodeSpec.nodeId,
|
|
670
|
+
status: 'pending',
|
|
671
|
+
attemptCount: 0,
|
|
672
|
+
retryCount: 0,
|
|
673
|
+
})
|
|
674
|
+
.output('after')
|
|
879
675
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
changedTaskId: recordIdToString(targetTask.id, TABLES.PLAN_TASK),
|
|
885
|
-
message: `Restarted task "${targetTask.title}".`,
|
|
886
|
-
})
|
|
676
|
+
createdNodeRuns.push(PlanNodeRunSchema.parse(created))
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return createdNodeRuns
|
|
887
680
|
}
|
|
888
681
|
}
|
|
889
682
|
|