@lota-sdk/core 0.1.9 → 0.1.12
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 -87
- package/src/ai/index.ts +3 -0
- package/src/bifrost/bifrost.ts +94 -25
- package/src/bifrost/index.ts +1 -0
- package/src/config/agent-defaults.ts +30 -7
- package/src/config/constants.ts +0 -9
- package/src/config/debug-logger.ts +43 -0
- package/src/config/index.ts +5 -0
- package/src/config/model-constants.ts +8 -9
- package/src/config/workstream-defaults.ts +4 -0
- package/src/db/cursor-pagination.ts +2 -2
- package/src/db/index.ts +10 -0
- package/src/db/memory-store.ts +3 -71
- package/src/db/memory.ts +9 -15
- package/src/db/service.ts +42 -2
- package/src/db/tables.ts +9 -2
- package/src/document/index.ts +2 -0
- package/src/document/parsing.ts +0 -25
- package/src/embeddings/provider.ts +102 -22
- package/src/index.ts +15 -499
- package/src/queues/index.ts +10 -0
- package/src/redis/connection-accessor.ts +26 -0
- package/src/redis/connection.ts +1 -1
- package/src/redis/index.ts +9 -25
- package/src/redis/org-memory-lock.ts +1 -1
- package/src/redis/redis-lease-lock.ts +1 -1
- package/src/redis/stream-context.ts +54 -0
- package/src/runtime/agent-runtime-policy.ts +9 -5
- package/src/runtime/agent-stream-helpers.ts +6 -3
- package/src/runtime/agent-types.ts +1 -5
- package/src/runtime/approval-continuation.ts +68 -1
- package/src/runtime/chat-attachments.ts +1 -1
- package/src/runtime/chat-request-routing.ts +6 -2
- package/src/runtime/context-compaction-runtime.ts +2 -2
- package/src/runtime/context-compaction.ts +1 -1
- package/src/runtime/execution-plan.ts +22 -15
- package/src/runtime/index.ts +26 -0
- package/src/runtime/indexed-repositories-policy.ts +10 -10
- package/src/runtime/memory-pipeline.ts +0 -2
- package/src/runtime/runtime-config.ts +238 -0
- package/src/runtime/runtime-extensions.ts +3 -2
- package/src/runtime/runtime-worker-registry.ts +47 -0
- package/src/runtime/team-consultation-orchestrator.ts +9 -6
- package/src/runtime/team-consultation-prompts.ts +3 -2
- package/src/runtime/turn-lifecycle.ts +13 -5
- package/src/runtime/workstream-chat-helpers.ts +0 -54
- package/src/runtime/workstream-routing-policy.ts +3 -7
- package/src/runtime.ts +387 -0
- package/src/services/chat-attachments.service.ts +1 -1
- package/src/services/context-compaction.service.ts +1 -1
- package/src/services/document-chunk.service.ts +2 -2
- package/src/services/execution-plan.service.ts +584 -793
- package/src/services/index.ts +14 -0
- package/src/services/learned-skill.service.ts +82 -39
- package/src/services/memory.service.ts +5 -4
- package/src/services/mutating-approval.service.ts +1 -1
- package/src/services/organization-member.service.ts +1 -1
- package/src/services/organization.service.ts +1 -1
- package/src/services/plan-approval.service.ts +83 -0
- package/src/services/plan-artifact.service.ts +44 -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 +1624 -0
- package/src/services/plan-run.service.ts +422 -0
- package/src/services/plan-validator.service.ts +760 -0
- package/src/services/recent-activity-title.service.ts +1 -1
- package/src/services/recent-activity.service.ts +14 -16
- package/src/services/user.service.ts +2 -2
- package/src/services/workstream-message.service.ts +2 -3
- package/src/services/workstream-title.service.ts +1 -1
- package/src/services/workstream-turn-preparation.ts +156 -59
- package/src/services/workstream-turn.ts +26 -1
- package/src/services/workstream.service.ts +35 -9
- package/src/services/workstream.types.ts +1 -0
- package/src/storage/attachment-parser.ts +1 -1
- package/src/storage/attachment-storage.service.ts +11 -10
- package/src/storage/generated-document-storage.service.ts +7 -6
- package/src/storage/index.ts +10 -0
- package/src/system-agents/delegated-agent-factory.ts +78 -29
- package/src/system-agents/index.ts +4 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
- package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
- package/src/system-agents/skill-extractor.agent.ts +1 -1
- package/src/system-agents/skill-manager.agent.ts +2 -4
- package/src/system-agents/title-generator.agent.ts +2 -2
- package/src/tools/execution-plan.tool.ts +22 -48
- package/src/tools/firecrawl-client.ts +2 -2
- package/src/tools/index.ts +12 -0
- package/src/tools/log-hello-world.tool.ts +17 -0
- package/src/tools/research-topic.tool.ts +1 -1
- package/src/tools/team-think.tool.ts +1 -1
- package/src/tools/user-questions.tool.ts +2 -2
- package/src/utils/index.ts +6 -0
- package/src/workers/bootstrap.ts +8 -16
- package/src/workers/index.ts +7 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
- package/src/workers/skill-extraction.runner.ts +3 -3
- package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
- package/src/workers/utils/repo-structure-extractor.ts +2 -5
- package/src/workers/utils/repomix-file-sections.ts +42 -0
- package/src/config/env-shapes.ts +0 -121
- package/src/runtime/agent-contract.ts +0 -1
|
@@ -1,889 +1,680 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ExecutionPlanEventSchema,
|
|
3
|
-
ExecutionPlanSchema,
|
|
4
|
-
ExecutionPlanTaskResultStatusSchema,
|
|
5
|
-
ExecutionPlanTaskSchema,
|
|
6
|
-
} from '@lota-sdk/shared/schemas/execution-plan'
|
|
7
|
-
import type {
|
|
8
|
-
ExecutionPlanEventRecord,
|
|
9
|
-
ExecutionPlanRecord,
|
|
10
|
-
ExecutionPlanStatus,
|
|
11
|
-
ExecutionPlanTaskRecord,
|
|
12
|
-
ExecutionPlanTaskResultStatus,
|
|
13
|
-
ExecutionPlanTaskStatus,
|
|
14
|
-
SerializableExecutionPlan,
|
|
15
|
-
SerializableExecutionPlanCarriedTask,
|
|
16
|
-
SerializableExecutionPlanEvent,
|
|
17
|
-
SerializableExecutionPlanTask,
|
|
18
|
-
} from '@lota-sdk/shared/schemas/execution-plan'
|
|
19
1
|
import type {
|
|
2
|
+
ChatMessage,
|
|
20
3
|
CreateExecutionPlanArgs,
|
|
21
4
|
ExecutionPlanToolResultData,
|
|
5
|
+
GetActiveExecutionPlanArgs,
|
|
6
|
+
PlanNodeRunRecord,
|
|
7
|
+
PlanNodeSpecRecord,
|
|
8
|
+
PlanRunRecord,
|
|
9
|
+
PlanSpecRecord,
|
|
22
10
|
ReplaceExecutionPlanArgs,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
11
|
+
ResumeExecutionPlanRunArgs,
|
|
12
|
+
SerializableExecutionPlan,
|
|
13
|
+
SubmitExecutionNodeResultArgs,
|
|
14
|
+
} from '@lota-sdk/shared'
|
|
15
|
+
import {
|
|
16
|
+
PlanCheckpointSchema,
|
|
17
|
+
PlanEventSchema,
|
|
18
|
+
PlanNodeRunSchema,
|
|
19
|
+
PlanNodeSpecRecordSchema,
|
|
20
|
+
PlanRunSchema,
|
|
21
|
+
PlanSpecSchema,
|
|
22
|
+
} from '@lota-sdk/shared'
|
|
23
|
+
import { RecordId } from 'surrealdb'
|
|
24
|
+
|
|
25
|
+
import type { RecordIdInput } from '../db/record-id'
|
|
29
26
|
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
30
27
|
import { databaseService } from '../db/service'
|
|
28
|
+
import type { DatabaseTransaction } from '../db/service'
|
|
31
29
|
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 {
|
|
30
|
+
import { readApprovalContinuationResponse } from '../runtime/approval-continuation'
|
|
31
|
+
import { extractMessageText } from '../runtime/workstream-chat-helpers'
|
|
32
|
+
import { planBuilderService } from './plan-builder.service'
|
|
33
|
+
import type { CompiledPlanNode } from './plan-compiler.service'
|
|
34
|
+
import { planCompilerService } from './plan-compiler.service'
|
|
35
|
+
import { planExecutorService } from './plan-executor.service'
|
|
36
|
+
import { planRunService } from './plan-run.service'
|
|
37
|
+
import { planValidatorService } from './plan-validator.service'
|
|
38
|
+
|
|
39
|
+
function buildToolResult(params: {
|
|
40
|
+
action: ExecutionPlanToolResultData['action']
|
|
41
|
+
plan: SerializableExecutionPlan | null
|
|
42
|
+
message: string
|
|
43
|
+
changedNodeId?: string
|
|
44
|
+
}): ExecutionPlanToolResultData {
|
|
107
45
|
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),
|
|
46
|
+
action: params.action,
|
|
47
|
+
message: params.message,
|
|
48
|
+
...(params.changedNodeId ? { changedNodeId: params.changedNodeId } : {}),
|
|
49
|
+
...(params.plan ? { plan: params.plan } : {}),
|
|
50
|
+
hasPlan: params.plan !== null,
|
|
51
|
+
status: params.plan?.status,
|
|
125
52
|
}
|
|
126
53
|
}
|
|
127
54
|
|
|
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
|
-
}
|
|
55
|
+
function aggregateBlockingIssues(issues: Array<{ code: string; message: string }>): string {
|
|
56
|
+
return issues.map((issue) => `${issue.code}: ${issue.message}`).join(' | ')
|
|
140
57
|
}
|
|
141
58
|
|
|
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
|
-
|
|
59
|
+
function toSpecData(spec: PlanSpecRecord, patch: Partial<PlanSpecRecord> & { replacedSpecId?: RecordIdInput | null }) {
|
|
156
60
|
return {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
61
|
+
organizationId: ensureRecordId(spec.organizationId, TABLES.ORGANIZATION),
|
|
62
|
+
workstreamId: ensureRecordId(spec.workstreamId, TABLES.WORKSTREAM),
|
|
63
|
+
title: patch.title ?? spec.title,
|
|
64
|
+
objective: patch.objective ?? spec.objective,
|
|
65
|
+
version: patch.version ?? spec.version,
|
|
66
|
+
status: patch.status ?? spec.status,
|
|
67
|
+
leadAgentId: patch.leadAgentId ?? spec.leadAgentId,
|
|
68
|
+
schemaRegistry: patch.schemaRegistry ? structuredClone(patch.schemaRegistry) : structuredClone(spec.schemaRegistry),
|
|
69
|
+
edges: patch.edges ? [...patch.edges] : [...spec.edges],
|
|
70
|
+
entryNodeIds: patch.entryNodeIds ? [...patch.entryNodeIds] : [...spec.entryNodeIds],
|
|
71
|
+
...(patch.replacedSpecId
|
|
72
|
+
? { replacedSpecId: ensureRecordId(patch.replacedSpecId, TABLES.PLAN_SPEC) }
|
|
73
|
+
: spec.replacedSpecId
|
|
74
|
+
? { replacedSpecId: ensureRecordId(spec.replacedSpecId, TABLES.PLAN_SPEC) }
|
|
75
|
+
: {}),
|
|
76
|
+
...(patch.compiledAt !== undefined
|
|
77
|
+
? { compiledAt: patch.compiledAt }
|
|
78
|
+
: spec.compiledAt
|
|
79
|
+
? { compiledAt: spec.compiledAt }
|
|
80
|
+
: {}),
|
|
165
81
|
}
|
|
166
82
|
}
|
|
167
83
|
|
|
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
|
|
84
|
+
type PlanRunUpdate = Omit<
|
|
85
|
+
Partial<PlanRunRecord>,
|
|
86
|
+
'currentNodeId' | 'waitingNodeId' | 'replacedRunId' | 'lastCheckpointId' | 'startedAt' | 'completedAt'
|
|
87
|
+
> & {
|
|
88
|
+
currentNodeId?: string | null
|
|
89
|
+
waitingNodeId?: string | null
|
|
90
|
+
replacedRunId?: RecordIdInput | null
|
|
91
|
+
lastCheckpointId?: RecordIdInput | null
|
|
92
|
+
startedAt?: Date | null
|
|
93
|
+
completedAt?: Date | null
|
|
179
94
|
}
|
|
180
95
|
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
96
|
+
function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
|
|
97
|
+
return {
|
|
98
|
+
planSpecId: ensureRecordId(run.planSpecId, TABLES.PLAN_SPEC),
|
|
99
|
+
organizationId: ensureRecordId(run.organizationId, TABLES.ORGANIZATION),
|
|
100
|
+
workstreamId: ensureRecordId(run.workstreamId, TABLES.WORKSTREAM),
|
|
101
|
+
leadAgentId: patch.leadAgentId ?? run.leadAgentId,
|
|
102
|
+
status: patch.status ?? run.status,
|
|
103
|
+
...(patch.currentNodeId === null
|
|
104
|
+
? {}
|
|
105
|
+
: patch.currentNodeId !== undefined
|
|
106
|
+
? { currentNodeId: patch.currentNodeId }
|
|
107
|
+
: run.currentNodeId
|
|
108
|
+
? { currentNodeId: run.currentNodeId }
|
|
109
|
+
: {}),
|
|
110
|
+
...(patch.waitingNodeId === null
|
|
111
|
+
? {}
|
|
112
|
+
: patch.waitingNodeId !== undefined
|
|
113
|
+
? { waitingNodeId: patch.waitingNodeId }
|
|
114
|
+
: run.waitingNodeId
|
|
115
|
+
? { waitingNodeId: run.waitingNodeId }
|
|
116
|
+
: {}),
|
|
117
|
+
readyNodeIds: patch.readyNodeIds ? [...patch.readyNodeIds] : [...run.readyNodeIds],
|
|
118
|
+
failureCount: patch.failureCount ?? run.failureCount,
|
|
119
|
+
...(patch.replacedRunId === null
|
|
120
|
+
? {}
|
|
121
|
+
: patch.replacedRunId
|
|
122
|
+
? { replacedRunId: ensureRecordId(patch.replacedRunId, TABLES.PLAN_RUN) }
|
|
123
|
+
: run.replacedRunId
|
|
124
|
+
? { replacedRunId: ensureRecordId(run.replacedRunId, TABLES.PLAN_RUN) }
|
|
125
|
+
: {}),
|
|
126
|
+
...(patch.lastCheckpointId === null
|
|
127
|
+
? {}
|
|
128
|
+
: patch.lastCheckpointId
|
|
129
|
+
? { lastCheckpointId: ensureRecordId(patch.lastCheckpointId, TABLES.PLAN_CHECKPOINT) }
|
|
130
|
+
: run.lastCheckpointId
|
|
131
|
+
? { lastCheckpointId: ensureRecordId(run.lastCheckpointId, TABLES.PLAN_CHECKPOINT) }
|
|
132
|
+
: {}),
|
|
133
|
+
...(patch.startedAt === null
|
|
134
|
+
? {}
|
|
135
|
+
: patch.startedAt !== undefined
|
|
136
|
+
? { startedAt: patch.startedAt }
|
|
137
|
+
: run.startedAt
|
|
138
|
+
? { startedAt: run.startedAt }
|
|
139
|
+
: {}),
|
|
140
|
+
...(patch.completedAt === null
|
|
141
|
+
? {}
|
|
142
|
+
: patch.completedAt !== undefined
|
|
143
|
+
? { completedAt: patch.completedAt }
|
|
144
|
+
: run.completedAt
|
|
145
|
+
? { completedAt: run.completedAt }
|
|
146
|
+
: {}),
|
|
196
147
|
}
|
|
197
|
-
|
|
198
|
-
throw new Error(`Cannot change execution task "${taskTitle}" from ${task.status} to ${nextStatus}.`)
|
|
199
148
|
}
|
|
200
149
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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,
|
|
246
|
-
}
|
|
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
|
-
|
|
252
|
-
await databaseService.create(TABLES.PLAN_EVENT, eventData, ExecutionPlanEventSchema)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
private async updatePlanStatus(params: {
|
|
256
|
-
plan: ExecutionPlanRecord
|
|
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)}`)
|
|
150
|
+
function buildApprovalResponseFromMessages(
|
|
151
|
+
messages: ChatMessage[],
|
|
152
|
+
): { approvalId: string; response: Record<string, unknown> } | null {
|
|
153
|
+
for (const message of [...messages].reverse()) {
|
|
154
|
+
if (message.role !== 'assistant') continue
|
|
155
|
+
const approvalResponse = readApprovalContinuationResponse(message)
|
|
156
|
+
if (!approvalResponse) continue
|
|
157
|
+
|
|
158
|
+
const response: Record<string, unknown> = {
|
|
159
|
+
approved: approvalResponse.approved,
|
|
160
|
+
requiredEdits: [...approvalResponse.requiredEdits],
|
|
289
161
|
}
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
162
|
+
if (approvalResponse.comments) {
|
|
163
|
+
response.comments = approvalResponse.comments
|
|
382
164
|
}
|
|
383
165
|
|
|
384
|
-
return
|
|
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
|
-
}
|
|
166
|
+
return { approvalId: approvalResponse.approvalId, response }
|
|
417
167
|
}
|
|
418
168
|
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
}
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
434
171
|
|
|
172
|
+
class ExecutionPlanService {
|
|
435
173
|
async hasActivePlan(workstreamId: RecordIdInput): Promise<boolean> {
|
|
436
|
-
return (await
|
|
174
|
+
return (await planRunService.getActiveRunRecord(workstreamId)) !== null
|
|
437
175
|
}
|
|
438
176
|
|
|
439
177
|
async getActivePlanForWorkstream(
|
|
440
178
|
workstreamId: RecordIdInput,
|
|
441
|
-
options?:
|
|
179
|
+
options?: Partial<GetActiveExecutionPlanArgs>,
|
|
442
180
|
): Promise<SerializableExecutionPlan | null> {
|
|
443
|
-
const
|
|
444
|
-
if (!
|
|
445
|
-
|
|
181
|
+
const run = await planRunService.getActiveRunRecord(workstreamId)
|
|
182
|
+
if (!run) return null
|
|
183
|
+
|
|
184
|
+
return await planRunService.toSerializablePlan(run, {
|
|
185
|
+
includeEvents: options?.includeEvents,
|
|
186
|
+
includeArtifacts: options?.includeArtifacts,
|
|
187
|
+
includeApprovals: options?.includeApprovals,
|
|
188
|
+
includeCheckpoints: options?.includeCheckpoints,
|
|
189
|
+
includeValidationIssues: options?.includeValidationIssues,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async getActivePlanToolResult(params: {
|
|
194
|
+
workstreamId: RecordIdInput
|
|
195
|
+
includeEvents?: boolean
|
|
196
|
+
includeArtifacts?: boolean
|
|
197
|
+
includeApprovals?: boolean
|
|
198
|
+
includeCheckpoints?: boolean
|
|
199
|
+
includeValidationIssues?: boolean
|
|
200
|
+
}): Promise<ExecutionPlanToolResultData> {
|
|
201
|
+
const plan = await this.getActivePlanForWorkstream(params.workstreamId, params)
|
|
202
|
+
return buildToolResult({
|
|
203
|
+
action: plan ? 'loaded' : 'none',
|
|
204
|
+
plan,
|
|
205
|
+
message: plan ? `Loaded execution run "${plan.title}".` : 'No active execution run.',
|
|
206
|
+
})
|
|
446
207
|
}
|
|
447
208
|
|
|
448
209
|
async createPlan(params: {
|
|
449
210
|
organizationId: RecordIdInput
|
|
450
211
|
workstreamId: RecordIdInput
|
|
451
|
-
leadAgentId:
|
|
212
|
+
leadAgentId: string
|
|
452
213
|
input: CreateExecutionPlanArgs
|
|
453
214
|
}): Promise<ExecutionPlanToolResultData> {
|
|
454
|
-
const
|
|
455
|
-
if (
|
|
456
|
-
throw new Error('An active execution
|
|
215
|
+
const activeRun = await planRunService.getActiveRunRecord(params.workstreamId)
|
|
216
|
+
if (activeRun) {
|
|
217
|
+
throw new Error('An active execution run already exists for this workstream. Replace it instead.')
|
|
457
218
|
}
|
|
458
219
|
|
|
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,
|
|
220
|
+
const preparedDraft = planBuilderService.prepareDraft(params.input)
|
|
221
|
+
const validation = planValidatorService.validateDraft(preparedDraft)
|
|
222
|
+
if (validation.blocking.length > 0) {
|
|
223
|
+
throw new Error(`Plan draft failed validation: ${aggregateBlockingIssues(validation.blocking)}`)
|
|
472
224
|
}
|
|
225
|
+
const compiled = planCompilerService.compile(preparedDraft)
|
|
226
|
+
|
|
227
|
+
const specId = new RecordId(TABLES.PLAN_SPEC, Bun.randomUUIDv7())
|
|
228
|
+
const runId = new RecordId(TABLES.PLAN_RUN, Bun.randomUUIDv7())
|
|
229
|
+
|
|
230
|
+
await databaseService.withTransaction(async (tx) => {
|
|
231
|
+
const spec = PlanSpecSchema.parse(
|
|
232
|
+
await tx
|
|
233
|
+
.create(specId)
|
|
234
|
+
.content({
|
|
235
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
236
|
+
workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
|
|
237
|
+
title: compiled.draft.title,
|
|
238
|
+
objective: compiled.draft.objective,
|
|
239
|
+
version: 1,
|
|
240
|
+
status: 'compiled',
|
|
241
|
+
leadAgentId: params.leadAgentId,
|
|
242
|
+
schemaRegistry: structuredClone(compiled.draft.schemas),
|
|
243
|
+
edges: [...compiled.draft.edges],
|
|
244
|
+
entryNodeIds: [...(compiled.draft.entryNodeIds ?? [])],
|
|
245
|
+
compiledAt: new Date(),
|
|
246
|
+
})
|
|
247
|
+
.output('after'),
|
|
248
|
+
)
|
|
473
249
|
|
|
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
|
-
}
|
|
250
|
+
const nodeSpecs = await this.createNodeSpecs(tx, spec.id, compiled.nodes)
|
|
251
|
+
const run = PlanRunSchema.parse(
|
|
252
|
+
await tx
|
|
253
|
+
.create(runId)
|
|
254
|
+
.content({
|
|
255
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
256
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
257
|
+
workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
|
|
258
|
+
leadAgentId: params.leadAgentId,
|
|
259
|
+
status: 'running',
|
|
260
|
+
readyNodeIds: [],
|
|
261
|
+
failureCount: 0,
|
|
262
|
+
startedAt: new Date(),
|
|
263
|
+
})
|
|
264
|
+
.output('after'),
|
|
265
|
+
)
|
|
496
266
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
267
|
+
const nodeRuns = await this.createNodeRuns(tx, run.id, spec.id, nodeSpecs)
|
|
268
|
+
const synced = await planExecutorService.syncRunGraph({
|
|
269
|
+
tx,
|
|
270
|
+
run,
|
|
271
|
+
spec,
|
|
272
|
+
nodeSpecs,
|
|
273
|
+
nodeRuns,
|
|
274
|
+
artifacts: [],
|
|
275
|
+
emittedBy: params.leadAgentId,
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const event = await tx
|
|
279
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
280
|
+
.content({
|
|
281
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
282
|
+
runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
|
|
283
|
+
eventType: 'plan-created',
|
|
284
|
+
message: `Created execution plan "${spec.title}".`,
|
|
285
|
+
emittedBy: params.leadAgentId,
|
|
286
|
+
detail: { title: spec.title, objective: spec.objective, nodeCount: nodeSpecs.length },
|
|
287
|
+
})
|
|
288
|
+
.output('after')
|
|
289
|
+
PlanEventSchema.parse(event)
|
|
290
|
+
|
|
291
|
+
const checkpoint = PlanCheckpointSchema.parse(
|
|
292
|
+
await tx
|
|
293
|
+
.create(new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7()))
|
|
294
|
+
.content({
|
|
295
|
+
runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
|
|
296
|
+
sequence: 1,
|
|
297
|
+
runStatus: synced.run.status,
|
|
298
|
+
readyNodeIds: [...synced.run.readyNodeIds],
|
|
299
|
+
activeNodeIds: synced.run.currentNodeId ? [synced.run.currentNodeId] : [],
|
|
300
|
+
artifactIds: [],
|
|
301
|
+
lastCompletedNodeIds: synced.nodeRuns
|
|
302
|
+
.filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
|
|
303
|
+
.map((nodeRun) => nodeRun.nodeId),
|
|
304
|
+
snapshot: {
|
|
305
|
+
reason: 'plan-created',
|
|
306
|
+
currentNodeId: synced.run.currentNodeId,
|
|
307
|
+
waitingNodeId: synced.run.waitingNodeId,
|
|
308
|
+
readyNodeIds: synced.run.readyNodeIds,
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
.output('after'),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
const updatedRun = PlanRunSchema.parse(
|
|
315
|
+
await tx
|
|
316
|
+
.update(ensureRecordId(synced.run.id, TABLES.PLAN_RUN))
|
|
317
|
+
.content(toRunData(synced.run, { lastCheckpointId: checkpoint.id }))
|
|
318
|
+
.output('after'),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
const checkpointEvent = await tx
|
|
322
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
323
|
+
.content({
|
|
324
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
325
|
+
runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
|
|
326
|
+
eventType: 'checkpoint-saved',
|
|
327
|
+
message: 'Saved checkpoint 1.',
|
|
328
|
+
emittedBy: 'system',
|
|
329
|
+
detail: { checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT), reason: 'plan-created' },
|
|
330
|
+
})
|
|
331
|
+
.output('after')
|
|
332
|
+
PlanEventSchema.parse(checkpointEvent)
|
|
503
333
|
})
|
|
504
334
|
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
335
|
+
const plan = await planRunService.toSerializablePlan(await planRunService.getRunById(runId), {
|
|
336
|
+
includeEvents: true,
|
|
337
|
+
includeArtifacts: true,
|
|
338
|
+
includeApprovals: true,
|
|
339
|
+
includeCheckpoints: true,
|
|
340
|
+
includeValidationIssues: true,
|
|
510
341
|
})
|
|
342
|
+
|
|
343
|
+
return buildToolResult({ action: 'created', plan, message: `Created execution plan "${plan.title}".` })
|
|
511
344
|
}
|
|
512
345
|
|
|
513
346
|
async replacePlan(params: {
|
|
514
347
|
workstreamId: RecordIdInput
|
|
515
348
|
organizationId: RecordIdInput
|
|
516
|
-
leadAgentId:
|
|
349
|
+
leadAgentId: string
|
|
517
350
|
input: ReplaceExecutionPlanArgs
|
|
518
351
|
}): Promise<ExecutionPlanToolResultData> {
|
|
519
|
-
const
|
|
520
|
-
if (!
|
|
521
|
-
throw new Error('No active execution
|
|
352
|
+
const activeRun = await planRunService.getActiveRunRecord(params.workstreamId)
|
|
353
|
+
if (!activeRun) {
|
|
354
|
+
throw new Error('No active execution run exists for this workstream.')
|
|
522
355
|
}
|
|
523
|
-
if (recordIdToString(
|
|
524
|
-
throw new Error('Only the active execution
|
|
356
|
+
if (recordIdToString(activeRun.id, TABLES.PLAN_RUN) !== params.input.runId) {
|
|
357
|
+
throw new Error('Only the active execution run can be replaced.')
|
|
525
358
|
}
|
|
526
359
|
|
|
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,
|
|
360
|
+
const activeSpec = await planRunService.getPlanSpecById(activeRun.planSpecId)
|
|
361
|
+
const preparedDraft = planBuilderService.prepareDraft({
|
|
544
362
|
title: params.input.title,
|
|
545
363
|
objective: params.input.objective,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
364
|
+
nodes: params.input.nodes,
|
|
365
|
+
edges: params.input.edges,
|
|
366
|
+
entryNodeIds: params.input.entryNodeIds,
|
|
367
|
+
schemas: params.input.schemas,
|
|
368
|
+
})
|
|
369
|
+
const validation = planValidatorService.validateDraft(preparedDraft)
|
|
370
|
+
if (validation.blocking.length > 0) {
|
|
371
|
+
throw new Error(`Plan draft failed validation: ${aggregateBlockingIssues(validation.blocking)}`)
|
|
551
372
|
}
|
|
373
|
+
const compiled = planCompilerService.compile(preparedDraft)
|
|
552
374
|
|
|
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
|
-
}
|
|
375
|
+
const specId = new RecordId(TABLES.PLAN_SPEC, Bun.randomUUIDv7())
|
|
376
|
+
const runId = new RecordId(TABLES.PLAN_RUN, Bun.randomUUIDv7())
|
|
575
377
|
|
|
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
|
-
|
|
378
|
+
await databaseService.withTransaction(async (tx) => {
|
|
379
|
+
const supersededSpec = PlanSpecSchema.parse(
|
|
380
|
+
await tx
|
|
381
|
+
.update(ensureRecordId(activeSpec.id, TABLES.PLAN_SPEC))
|
|
382
|
+
.content(toSpecData(activeSpec, { status: 'superseded' }))
|
|
383
|
+
.output('after'),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
const abortedRun = PlanRunSchema.parse(
|
|
387
|
+
await tx
|
|
388
|
+
.update(ensureRecordId(activeRun.id, TABLES.PLAN_RUN))
|
|
389
|
+
.content(
|
|
390
|
+
toRunData(activeRun, {
|
|
391
|
+
status: 'aborted',
|
|
392
|
+
currentNodeId: null,
|
|
393
|
+
waitingNodeId: null,
|
|
394
|
+
readyNodeIds: [],
|
|
395
|
+
completedAt: new Date(),
|
|
396
|
+
}),
|
|
397
|
+
)
|
|
398
|
+
.output('after'),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
const spec = PlanSpecSchema.parse(
|
|
402
|
+
await tx
|
|
403
|
+
.create(specId)
|
|
404
|
+
.content({
|
|
405
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
406
|
+
workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
|
|
407
|
+
title: compiled.draft.title,
|
|
408
|
+
objective: compiled.draft.objective,
|
|
409
|
+
version: supersededSpec.version + 1,
|
|
410
|
+
status: 'compiled',
|
|
411
|
+
leadAgentId: params.leadAgentId,
|
|
412
|
+
schemaRegistry: structuredClone(compiled.draft.schemas),
|
|
413
|
+
edges: [...compiled.draft.edges],
|
|
414
|
+
entryNodeIds: [...(compiled.draft.entryNodeIds ?? [])],
|
|
415
|
+
replacedSpecId: ensureRecordId(supersededSpec.id, TABLES.PLAN_SPEC),
|
|
416
|
+
compiledAt: new Date(),
|
|
417
|
+
})
|
|
418
|
+
.output('after'),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
const nodeSpecs = await this.createNodeSpecs(tx, spec.id, compiled.nodes)
|
|
422
|
+
const run = PlanRunSchema.parse(
|
|
423
|
+
await tx
|
|
424
|
+
.create(runId)
|
|
425
|
+
.content({
|
|
426
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
427
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
428
|
+
workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
|
|
429
|
+
leadAgentId: params.leadAgentId,
|
|
430
|
+
status: 'running',
|
|
431
|
+
readyNodeIds: [],
|
|
432
|
+
failureCount: 0,
|
|
433
|
+
replacedRunId: ensureRecordId(abortedRun.id, TABLES.PLAN_RUN),
|
|
434
|
+
startedAt: new Date(),
|
|
435
|
+
})
|
|
436
|
+
.output('after'),
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
const nodeRuns = await this.createNodeRuns(tx, run.id, spec.id, nodeSpecs)
|
|
440
|
+
const synced = await planExecutorService.syncRunGraph({
|
|
441
|
+
tx,
|
|
442
|
+
run,
|
|
443
|
+
spec,
|
|
444
|
+
nodeSpecs,
|
|
445
|
+
nodeRuns,
|
|
446
|
+
artifacts: [],
|
|
447
|
+
emittedBy: params.leadAgentId,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const replaceEvent = await tx
|
|
451
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
452
|
+
.content({
|
|
453
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
454
|
+
runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
|
|
455
|
+
eventType: 'plan-replaced',
|
|
456
|
+
message: `Replaced execution plan "${activeSpec.title}" with "${spec.title}".`,
|
|
457
|
+
emittedBy: params.leadAgentId,
|
|
458
|
+
detail: { reason: params.input.reason, replacedRunId: recordIdToString(abortedRun.id, TABLES.PLAN_RUN) },
|
|
459
|
+
})
|
|
460
|
+
.output('after')
|
|
461
|
+
PlanEventSchema.parse(replaceEvent)
|
|
462
|
+
|
|
463
|
+
const checkpoint = PlanCheckpointSchema.parse(
|
|
464
|
+
await tx
|
|
465
|
+
.create(new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7()))
|
|
466
|
+
.content({
|
|
467
|
+
runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
|
|
468
|
+
sequence: 1,
|
|
469
|
+
runStatus: synced.run.status,
|
|
470
|
+
readyNodeIds: [...synced.run.readyNodeIds],
|
|
471
|
+
activeNodeIds: synced.run.currentNodeId ? [synced.run.currentNodeId] : [],
|
|
472
|
+
artifactIds: [],
|
|
473
|
+
lastCompletedNodeIds: synced.nodeRuns
|
|
474
|
+
.filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
|
|
475
|
+
.map((nodeRun) => nodeRun.nodeId),
|
|
476
|
+
snapshot: {
|
|
477
|
+
reason: 'plan-replaced',
|
|
478
|
+
currentNodeId: synced.run.currentNodeId,
|
|
479
|
+
waitingNodeId: synced.run.waitingNodeId,
|
|
480
|
+
readyNodeIds: synced.run.readyNodeIds,
|
|
481
|
+
},
|
|
482
|
+
})
|
|
483
|
+
.output('after'),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
const updatedRun = PlanRunSchema.parse(
|
|
487
|
+
await tx
|
|
488
|
+
.update(ensureRecordId(synced.run.id, TABLES.PLAN_RUN))
|
|
489
|
+
.content(toRunData(synced.run, { lastCheckpointId: checkpoint.id }))
|
|
490
|
+
.output('after'),
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
const checkpointEvent = await tx
|
|
494
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
495
|
+
.content({
|
|
496
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
497
|
+
runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
|
|
498
|
+
eventType: 'checkpoint-saved',
|
|
499
|
+
message: 'Saved checkpoint 1.',
|
|
500
|
+
emittedBy: 'system',
|
|
501
|
+
detail: { checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT), reason: 'plan-replaced' },
|
|
502
|
+
})
|
|
503
|
+
.output('after')
|
|
504
|
+
PlanEventSchema.parse(checkpointEvent)
|
|
605
505
|
})
|
|
606
506
|
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
507
|
+
const plan = await planRunService.toSerializablePlan(await planRunService.getRunById(runId), {
|
|
508
|
+
includeEvents: true,
|
|
509
|
+
includeArtifacts: true,
|
|
510
|
+
includeApprovals: true,
|
|
511
|
+
includeCheckpoints: true,
|
|
512
|
+
includeValidationIssues: true,
|
|
612
513
|
})
|
|
514
|
+
|
|
515
|
+
return buildToolResult({ action: 'replaced', plan, message: `Replaced execution plan with "${plan.title}".` })
|
|
613
516
|
}
|
|
614
517
|
|
|
615
|
-
async
|
|
518
|
+
async submitNodeResult(params: {
|
|
616
519
|
workstreamId: RecordIdInput
|
|
617
|
-
|
|
520
|
+
emittedBy: string
|
|
521
|
+
input: SubmitExecutionNodeResultArgs
|
|
618
522
|
}): Promise<ExecutionPlanToolResultData> {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
523
|
+
return await planExecutorService.submitNodeResult({
|
|
524
|
+
workstreamId: params.workstreamId,
|
|
525
|
+
runId: params.input.runId,
|
|
526
|
+
nodeId: params.input.nodeId,
|
|
527
|
+
emittedBy: params.emittedBy,
|
|
528
|
+
result: params.input.result,
|
|
624
529
|
})
|
|
625
530
|
}
|
|
626
531
|
|
|
627
|
-
async
|
|
532
|
+
async resumeRun(params: {
|
|
628
533
|
workstreamId: RecordIdInput
|
|
629
534
|
emittedBy: string
|
|
630
|
-
input:
|
|
535
|
+
input: ResumeExecutionPlanRunArgs
|
|
631
536
|
}): 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
|
-
},
|
|
537
|
+
return await planExecutorService.resumeRun({
|
|
538
|
+
workstreamId: params.workstreamId,
|
|
539
|
+
runId: params.input.runId,
|
|
801
540
|
emittedBy: params.emittedBy,
|
|
802
541
|
})
|
|
542
|
+
}
|
|
803
543
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
544
|
+
async applyApprovalResponseFromMessages(params: {
|
|
545
|
+
workstreamId: RecordIdInput
|
|
546
|
+
approvalMessages: ChatMessage[]
|
|
547
|
+
respondedBy: string
|
|
548
|
+
}): Promise<SerializableExecutionPlan | null> {
|
|
549
|
+
const approvalResponse = buildApprovalResponseFromMessages(params.approvalMessages)
|
|
550
|
+
if (!approvalResponse) return null
|
|
551
|
+
|
|
552
|
+
return await planExecutorService.submitHumanNodeResponse({
|
|
553
|
+
workstreamId: params.workstreamId,
|
|
554
|
+
approvalId: approvalResponse.approvalId,
|
|
555
|
+
respondedBy: params.respondedBy,
|
|
556
|
+
response: approvalResponse.response,
|
|
813
557
|
})
|
|
814
558
|
}
|
|
815
559
|
|
|
816
|
-
async
|
|
560
|
+
async respondToApproval(params: {
|
|
817
561
|
workstreamId: RecordIdInput
|
|
818
562
|
emittedBy: string
|
|
819
|
-
input:
|
|
820
|
-
}): Promise<
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
563
|
+
input: { approvalId: string; response: Record<string, unknown>; approvalMessageId?: string }
|
|
564
|
+
}): Promise<SerializableExecutionPlan | null> {
|
|
565
|
+
return await planExecutorService.submitHumanNodeResponse({
|
|
566
|
+
workstreamId: params.workstreamId,
|
|
567
|
+
approvalId: params.input.approvalId,
|
|
568
|
+
respondedBy: params.emittedBy,
|
|
569
|
+
response: params.input.response,
|
|
570
|
+
approvalMessageId: params.input.approvalMessageId,
|
|
571
|
+
})
|
|
572
|
+
}
|
|
828
573
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
574
|
+
async applyHumanInputFromUserMessage(params: {
|
|
575
|
+
workstreamId: RecordIdInput
|
|
576
|
+
message: ChatMessage
|
|
577
|
+
respondedBy: string
|
|
578
|
+
}): Promise<SerializableExecutionPlan | null> {
|
|
579
|
+
const run = await planRunService.getActiveRunRecord(params.workstreamId)
|
|
580
|
+
if (!run || run.status !== 'awaiting-human' || !run.waitingNodeId) return null
|
|
581
|
+
|
|
582
|
+
const nodeSpec = await planRunService.getNodeSpecByNodeId(run.planSpecId, run.waitingNodeId)
|
|
583
|
+
if (nodeSpec.type === 'human-approval') {
|
|
584
|
+
return null
|
|
833
585
|
}
|
|
834
|
-
if (
|
|
835
|
-
|
|
586
|
+
if (
|
|
587
|
+
nodeSpec.type !== 'human-input' &&
|
|
588
|
+
nodeSpec.type !== 'human-review-edit' &&
|
|
589
|
+
nodeSpec.type !== 'human-decision'
|
|
590
|
+
) {
|
|
591
|
+
return null
|
|
836
592
|
}
|
|
837
593
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
594
|
+
const response = {
|
|
595
|
+
responseText: extractMessageText(params.message).trim(),
|
|
596
|
+
messageId: params.message.id,
|
|
597
|
+
approved: nodeSpec.type === 'human-decision' ? true : undefined,
|
|
598
|
+
} satisfies Record<string, unknown>
|
|
599
|
+
|
|
600
|
+
return await planExecutorService.submitHumanNodeResponse({
|
|
601
|
+
workstreamId: params.workstreamId,
|
|
602
|
+
respondedBy: params.respondedBy,
|
|
603
|
+
response,
|
|
604
|
+
approvalMessageId: params.message.id,
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private async createNodeSpecs(
|
|
609
|
+
tx: DatabaseTransaction,
|
|
610
|
+
planSpecId: RecordIdInput,
|
|
611
|
+
nodes: CompiledPlanNode[],
|
|
612
|
+
): Promise<PlanNodeSpecRecord[]> {
|
|
613
|
+
const createdRecords: PlanNodeSpecRecord[] = []
|
|
614
|
+
|
|
615
|
+
for (const compiledNode of nodes) {
|
|
616
|
+
const nodeSpecId = new RecordId(TABLES.PLAN_NODE_SPEC, Bun.randomUUIDv7())
|
|
617
|
+
const created = await tx
|
|
618
|
+
.create(nodeSpecId)
|
|
619
|
+
.content({
|
|
620
|
+
planSpecId: ensureRecordId(planSpecId, TABLES.PLAN_SPEC),
|
|
621
|
+
nodeId: compiledNode.node.id,
|
|
622
|
+
position: compiledNode.position,
|
|
623
|
+
type: compiledNode.node.type,
|
|
624
|
+
label: compiledNode.node.label,
|
|
625
|
+
owner: compiledNode.node.owner,
|
|
626
|
+
objective: compiledNode.node.objective,
|
|
627
|
+
instructions: compiledNode.node.instructions,
|
|
628
|
+
...(compiledNode.node.inputSchemaRef ? { inputSchemaRef: compiledNode.node.inputSchemaRef } : {}),
|
|
629
|
+
...(compiledNode.node.outputSchemaRef ? { outputSchemaRef: compiledNode.node.outputSchemaRef } : {}),
|
|
630
|
+
deliverables: [...compiledNode.node.deliverables],
|
|
631
|
+
successCriteria: [...compiledNode.node.successCriteria],
|
|
632
|
+
completionChecks: [...compiledNode.node.completionChecks],
|
|
633
|
+
retryPolicy: { ...compiledNode.node.retryPolicy, retryOn: [...compiledNode.node.retryPolicy.retryOn] },
|
|
634
|
+
failurePolicy: [...compiledNode.node.failurePolicy],
|
|
635
|
+
...(compiledNode.node.timeoutMs ? { timeoutMs: compiledNode.node.timeoutMs } : {}),
|
|
636
|
+
toolPolicy: { allow: [...compiledNode.node.toolPolicy.allow], deny: [...compiledNode.node.toolPolicy.deny] },
|
|
637
|
+
contextPolicy: {
|
|
638
|
+
retrievalScopes: [...compiledNode.node.contextPolicy.retrievalScopes],
|
|
639
|
+
attachmentPolicy: compiledNode.node.contextPolicy.attachmentPolicy,
|
|
640
|
+
webPolicy: compiledNode.node.contextPolicy.webPolicy,
|
|
641
|
+
},
|
|
642
|
+
upstreamNodeIds: [...compiledNode.upstreamNodeIds],
|
|
643
|
+
downstreamNodeIds: [...compiledNode.downstreamNodeIds],
|
|
644
|
+
})
|
|
645
|
+
.output('after')
|
|
646
|
+
|
|
647
|
+
createdRecords.push(PlanNodeSpecRecordSchema.parse(created))
|
|
856
648
|
}
|
|
857
649
|
|
|
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
|
-
})
|
|
650
|
+
return createdRecords
|
|
651
|
+
}
|
|
868
652
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
653
|
+
private async createNodeRuns(
|
|
654
|
+
tx: DatabaseTransaction,
|
|
655
|
+
runId: RecordIdInput,
|
|
656
|
+
planSpecId: RecordIdInput,
|
|
657
|
+
nodeSpecs: PlanNodeSpecRecord[],
|
|
658
|
+
): Promise<PlanNodeRunRecord[]> {
|
|
659
|
+
const createdNodeRuns: PlanNodeRunRecord[] = []
|
|
660
|
+
for (const nodeSpec of nodeSpecs) {
|
|
661
|
+
const nodeRunId = new RecordId(TABLES.PLAN_NODE_RUN, Bun.randomUUIDv7())
|
|
662
|
+
const created = await tx
|
|
663
|
+
.create(nodeRunId)
|
|
664
|
+
.content({
|
|
665
|
+
runId: ensureRecordId(runId, TABLES.PLAN_RUN),
|
|
666
|
+
planSpecId: ensureRecordId(planSpecId, TABLES.PLAN_SPEC),
|
|
667
|
+
nodeId: nodeSpec.nodeId,
|
|
668
|
+
status: 'pending',
|
|
669
|
+
attemptCount: 0,
|
|
670
|
+
retryCount: 0,
|
|
671
|
+
})
|
|
672
|
+
.output('after')
|
|
879
673
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
changedTaskId: recordIdToString(targetTask.id, TABLES.PLAN_TASK),
|
|
885
|
-
message: `Restarted task "${targetTask.title}".`,
|
|
886
|
-
})
|
|
674
|
+
createdNodeRuns.push(PlanNodeRunSchema.parse(created))
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return createdNodeRuns
|
|
887
678
|
}
|
|
888
679
|
}
|
|
889
680
|
|