@lota-sdk/core 0.1.20 → 0.1.22
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/02_execution_plan.surql +4 -0
- package/package.json +6 -6
- package/src/ai-gateway/ai-gateway.ts +2 -4
- package/src/create-runtime.ts +8 -0
- package/src/queues/document-processor.queue.ts +11 -8
- package/src/queues/index.ts +1 -0
- package/src/queues/plan-agent-heartbeat.queue.ts +100 -0
- package/src/queues/queue-factory.ts +12 -11
- package/src/redis/redis-lease-lock.ts +1 -1
- package/src/runtime/agent-runtime-policy.ts +41 -4
- package/src/runtime/execution-plan-visibility.ts +23 -0
- package/src/runtime/execution-plan.ts +1 -0
- package/src/runtime/runtime-extensions.ts +26 -0
- package/src/runtime/runtime-worker-registry.ts +9 -1
- package/src/services/agent-executor.service.ts +6 -0
- package/src/services/execution-plan.service.ts +51 -36
- package/src/services/index.ts +3 -0
- package/src/services/ownership-dispatcher.service.ts +50 -8
- package/src/services/plan-agent-heartbeat.service.ts +136 -0
- package/src/services/plan-agent-query.service.ts +238 -0
- package/src/services/plan-builder.service.ts +11 -1
- package/src/services/plan-compiler.service.ts +2 -0
- package/src/services/plan-deadline.service.ts +186 -44
- package/src/services/plan-event-delivery.service.ts +170 -0
- package/src/services/plan-executor.service.ts +107 -3
- package/src/services/plan-helpers.ts +13 -0
- package/src/services/plan-run.service.ts +4 -0
- package/src/services/plan-template.service.ts +0 -1
- package/src/services/workstream-turn-preparation.service.ts +452 -176
- package/src/services/workstream-turn.ts +101 -1
- package/src/services/workstream.service.ts +76 -16
- package/src/tools/execution-plan.tool.ts +0 -2
|
@@ -6,10 +6,19 @@ import {
|
|
|
6
6
|
ConsultSpecialistArgsSchema,
|
|
7
7
|
dataPartsSchemas,
|
|
8
8
|
messageMetadataSchema,
|
|
9
|
+
PlanNodeResultSubmissionSchema,
|
|
10
|
+
SUBMIT_PLAN_TURN_RESULT_TOOL_NAME,
|
|
9
11
|
toTimestamp,
|
|
10
12
|
withMessageCreatedAt,
|
|
11
13
|
} from '@lota-sdk/shared'
|
|
12
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
ChatMessage,
|
|
16
|
+
MessageMetadata,
|
|
17
|
+
PlanArtifactSubmission,
|
|
18
|
+
PlanNodeHandoffContext,
|
|
19
|
+
PlanNodeRunRecord,
|
|
20
|
+
PlanNodeSpecRecord,
|
|
21
|
+
} from '@lota-sdk/shared'
|
|
13
22
|
import { convertToModelMessages, readUIMessageStream, stepCountIs, tool as createTool, validateUIMessages } from 'ai'
|
|
14
23
|
import type { PrepareStepFunction, StopCondition, ToolLoopAgent, ToolSet, UIMessageStreamWriter } from 'ai'
|
|
15
24
|
import type { z } from 'zod'
|
|
@@ -36,6 +45,10 @@ import { enqueueRegularChatMemoryDigest } from '../queues/regular-chat-memory-di
|
|
|
36
45
|
import { enqueueSkillExtraction } from '../queues/skill-extraction.queue'
|
|
37
46
|
import { enqueueWorkstreamTitleGeneration } from '../queues/workstream-title-generation.queue'
|
|
38
47
|
import { buildAgentPromptContext } from '../runtime/agent-prompt-context'
|
|
48
|
+
import {
|
|
49
|
+
OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES,
|
|
50
|
+
buildCompletionCheckStructuredOutputHints,
|
|
51
|
+
} from '../runtime/agent-runtime-policy'
|
|
39
52
|
import {
|
|
40
53
|
buildSpecialistTaskMessage,
|
|
41
54
|
createAgentMessageMetadata,
|
|
@@ -85,7 +98,7 @@ import { memoryService } from './memory.service'
|
|
|
85
98
|
import { planRunService } from './plan-run.service'
|
|
86
99
|
import { recentActivityService } from './recent-activity.service'
|
|
87
100
|
import { workstreamMessageService } from './workstream-message.service'
|
|
88
|
-
import { workstreamService } from './workstream.service'
|
|
101
|
+
import { ActiveWorkstreamRunConflictError, workstreamService } from './workstream.service'
|
|
89
102
|
|
|
90
103
|
type ChatStreamChunk = Parameters<UIMessageStreamWriter<ChatMessage>['write']>[0]
|
|
91
104
|
|
|
@@ -183,6 +196,190 @@ function optionalInstructionSection(value: unknown): string[] | undefined {
|
|
|
183
196
|
return section ? [section] : undefined
|
|
184
197
|
}
|
|
185
198
|
|
|
199
|
+
export interface PlanTurnUpstreamHandoff {
|
|
200
|
+
nodeId: string
|
|
201
|
+
label: string
|
|
202
|
+
ownerRef: string
|
|
203
|
+
ownerType: PlanNodeSpecRecord['owner']['executorType']
|
|
204
|
+
handoffContext: PlanNodeHandoffContext
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface WorkstreamPlanTurnContext {
|
|
208
|
+
runId: string
|
|
209
|
+
nodeId: string
|
|
210
|
+
planTitle: string
|
|
211
|
+
nodeSpec: PlanNodeSpecRecord
|
|
212
|
+
nodeRun: PlanNodeRunRecord
|
|
213
|
+
resolvedInput: Record<string, unknown>
|
|
214
|
+
inputArtifacts: PlanArtifactSubmission[]
|
|
215
|
+
upstreamHandoffs: PlanTurnUpstreamHandoff[]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildPlanTurnExecutionSection(planTurn: WorkstreamPlanTurnContext): string {
|
|
219
|
+
const payload = {
|
|
220
|
+
runId: planTurn.runId,
|
|
221
|
+
planTitle: planTurn.planTitle,
|
|
222
|
+
node: {
|
|
223
|
+
id: planTurn.nodeSpec.nodeId,
|
|
224
|
+
label: planTurn.nodeSpec.label,
|
|
225
|
+
owner: planTurn.nodeSpec.owner,
|
|
226
|
+
objective: planTurn.nodeSpec.objective,
|
|
227
|
+
instructions: planTurn.nodeSpec.instructions,
|
|
228
|
+
outputSchemaRef: planTurn.nodeSpec.outputSchemaRef ?? null,
|
|
229
|
+
deliverables: planTurn.nodeSpec.deliverables,
|
|
230
|
+
successCriteria: planTurn.nodeSpec.successCriteria,
|
|
231
|
+
completionChecks: planTurn.nodeSpec.completionChecks,
|
|
232
|
+
toolPolicy: planTurn.nodeSpec.toolPolicy,
|
|
233
|
+
contextPolicy: planTurn.nodeSpec.contextPolicy,
|
|
234
|
+
},
|
|
235
|
+
resolvedInput: planTurn.resolvedInput,
|
|
236
|
+
inputArtifacts: planTurn.inputArtifacts,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return [
|
|
240
|
+
'<plan-turn-execution>',
|
|
241
|
+
'The runtime has activated a visible execution-plan node inside this workstream.',
|
|
242
|
+
`Complete node "${planTurn.nodeSpec.label}" for plan "${planTurn.planTitle}".`,
|
|
243
|
+
'Use only the node contract, resolved input, input artifacts, and upstream handoff context provided here.',
|
|
244
|
+
'Do not ask the user for more input and do not rely on unstated external context.',
|
|
245
|
+
'Do not submit placeholders, partial work, or speculative outputs.',
|
|
246
|
+
'Before submitting, satisfy every required deliverable, success criterion, and completion check for this node.',
|
|
247
|
+
'Deliverables must use the exact artifact names and kinds declared in the node contract.',
|
|
248
|
+
'If a deliverable declares schemaRef, include the same schemaRef and a payload that satisfies that schema.',
|
|
249
|
+
'If outputSchemaRef is declared, structuredOutput must satisfy that schema before you submit.',
|
|
250
|
+
`When finished, call ${SUBMIT_PLAN_TURN_RESULT_TOOL_NAME} exactly once.`,
|
|
251
|
+
'Always include durable handoffContext for downstream nodes when you submit the final result.',
|
|
252
|
+
'Do not ask the user for confirmation and do not create or replace execution plans in this turn.',
|
|
253
|
+
JSON.stringify(payload, null, 2),
|
|
254
|
+
'</plan-turn-execution>',
|
|
255
|
+
].join('\n')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function describePlanTurnDeliverable(deliverable: PlanNodeSpecRecord['deliverables'][number]): string {
|
|
259
|
+
return [
|
|
260
|
+
`- ${deliverable.name}`,
|
|
261
|
+
`kind=${deliverable.kind}`,
|
|
262
|
+
deliverable.required ? 'required' : 'optional',
|
|
263
|
+
deliverable.schemaRef ? `schemaRef=${deliverable.schemaRef}` : undefined,
|
|
264
|
+
deliverable.description ? `description=${deliverable.description}` : undefined,
|
|
265
|
+
]
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.join(' | ')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function describePlanTurnCompletionCheck(check: PlanNodeSpecRecord['completionChecks'][number]): string {
|
|
271
|
+
return [
|
|
272
|
+
`- ${check.description}`,
|
|
273
|
+
`type=${check.type}`,
|
|
274
|
+
check.blocking ? 'blocking' : 'warning',
|
|
275
|
+
Object.keys(check.config).length > 0 ? `config=${JSON.stringify(check.config)}` : undefined,
|
|
276
|
+
]
|
|
277
|
+
.filter(Boolean)
|
|
278
|
+
.join(' | ')
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildPlanTurnResultContractSection(planTurn: WorkstreamPlanTurnContext): string {
|
|
282
|
+
const requiredDeliverables = planTurn.nodeSpec.deliverables.filter((deliverable) => deliverable.required)
|
|
283
|
+
const completionCheckOutputHints = buildCompletionCheckStructuredOutputHints(planTurn.nodeSpec)
|
|
284
|
+
const deliverableLines =
|
|
285
|
+
planTurn.nodeSpec.deliverables.length > 0
|
|
286
|
+
? planTurn.nodeSpec.deliverables.map(describePlanTurnDeliverable)
|
|
287
|
+
: ['- none']
|
|
288
|
+
const completionCheckLines =
|
|
289
|
+
planTurn.nodeSpec.completionChecks.length > 0
|
|
290
|
+
? planTurn.nodeSpec.completionChecks.map(describePlanTurnCompletionCheck)
|
|
291
|
+
: ['- none']
|
|
292
|
+
|
|
293
|
+
return [
|
|
294
|
+
'<plan-turn-result-contract>',
|
|
295
|
+
`Call ${SUBMIT_PLAN_TURN_RESULT_TOOL_NAME} exactly once with a result object that passes node validation.`,
|
|
296
|
+
'Validation is strict. Missing required artifacts, schema mismatches, or failed completion checks will fail the node run.',
|
|
297
|
+
`Required artifacts: ${requiredDeliverables.length > 0 ? requiredDeliverables.map((deliverable) => deliverable.name).join(', ') : 'none'}`,
|
|
298
|
+
`Structured output: ${
|
|
299
|
+
planTurn.nodeSpec.outputSchemaRef
|
|
300
|
+
? `required and must match schema "${planTurn.nodeSpec.outputSchemaRef}"`
|
|
301
|
+
: 'optional unless needed by a completion check'
|
|
302
|
+
}`,
|
|
303
|
+
'Deliverables:',
|
|
304
|
+
...deliverableLines,
|
|
305
|
+
'Completion checks:',
|
|
306
|
+
...completionCheckLines,
|
|
307
|
+
...(completionCheckOutputHints.length > 0
|
|
308
|
+
? ['Structured output fields required by completion checks:', ...completionCheckOutputHints]
|
|
309
|
+
: []),
|
|
310
|
+
'Include notes with a concise completion summary grounded in the submitted artifacts and structuredOutput.',
|
|
311
|
+
'Always include handoffContext for downstream execution with a durable summary, key decisions, open questions, risks, recommendations, and references when relevant.',
|
|
312
|
+
'</plan-turn-result-contract>',
|
|
313
|
+
].join('\n')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function buildPlanTurnSubmitToolDescription(planTurn: WorkstreamPlanTurnContext): string {
|
|
317
|
+
const requiredArtifacts =
|
|
318
|
+
planTurn.nodeSpec.deliverables
|
|
319
|
+
.filter((deliverable) => deliverable.required)
|
|
320
|
+
.map((deliverable) => `${deliverable.name} (${deliverable.kind})`)
|
|
321
|
+
.join(', ') || 'none'
|
|
322
|
+
|
|
323
|
+
return [
|
|
324
|
+
'Submit the final result for the active plan-triggered node turn.',
|
|
325
|
+
'Call this exactly once when the node output is complete.',
|
|
326
|
+
`Required artifacts: ${requiredArtifacts}.`,
|
|
327
|
+
`Structured output: ${
|
|
328
|
+
planTurn.nodeSpec.outputSchemaRef ? `must satisfy ${planTurn.nodeSpec.outputSchemaRef}` : 'optional'
|
|
329
|
+
}.`,
|
|
330
|
+
'Do not submit partial results. Include durable handoffContext for downstream nodes.',
|
|
331
|
+
].join(' ')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildPlanTurnPromptMessage(planTurn: WorkstreamPlanTurnContext): ChatMessage {
|
|
335
|
+
return {
|
|
336
|
+
id: Bun.randomUUIDv7(),
|
|
337
|
+
role: 'user',
|
|
338
|
+
parts: [
|
|
339
|
+
{
|
|
340
|
+
type: 'text',
|
|
341
|
+
text: `Execute the active plan node "${planTurn.nodeSpec.label}" now and submit the result with ${SUBMIT_PLAN_TURN_RESULT_TOOL_NAME}.`,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
metadata: { trigger: 'plan-turn', planRunId: planTurn.runId, planNodeId: planTurn.nodeId, createdAt: Date.now() },
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function buildUpstreamHandoffSection(upstreamHandoffs: PlanTurnUpstreamHandoff[]): string | undefined {
|
|
349
|
+
if (upstreamHandoffs.length === 0) {
|
|
350
|
+
return undefined
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return [
|
|
354
|
+
'<upstream-handoff>',
|
|
355
|
+
JSON.stringify(
|
|
356
|
+
upstreamHandoffs.map((handoff) => ({
|
|
357
|
+
nodeId: handoff.nodeId,
|
|
358
|
+
label: handoff.label,
|
|
359
|
+
ownerRef: handoff.ownerRef,
|
|
360
|
+
ownerType: handoff.ownerType,
|
|
361
|
+
handoffContext: handoff.handoffContext,
|
|
362
|
+
})),
|
|
363
|
+
null,
|
|
364
|
+
2,
|
|
365
|
+
),
|
|
366
|
+
'</upstream-handoff>',
|
|
367
|
+
].join('\n')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function applyPlanTurnToolPolicy(tools: ToolSet, nodeSpec: PlanNodeSpecRecord): ToolSet {
|
|
371
|
+
const blockedToolNames = new Set([...OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES, ...nodeSpec.toolPolicy.deny])
|
|
372
|
+
const allowList = nodeSpec.toolPolicy.allow.length > 0 ? new Set(nodeSpec.toolPolicy.allow) : null
|
|
373
|
+
|
|
374
|
+
return Object.fromEntries(
|
|
375
|
+
Object.entries(tools).filter(
|
|
376
|
+
([toolName]) =>
|
|
377
|
+
!blockedToolNames.has(toolName) &&
|
|
378
|
+
(toolName === SUBMIT_PLAN_TURN_RESULT_TOOL_NAME || allowList === null || allowList.has(toolName)),
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
186
383
|
export interface WorkstreamTurnParams {
|
|
187
384
|
workstream: NormalizedWorkstream
|
|
188
385
|
workstreamRef: RecordIdRef
|
|
@@ -207,6 +404,17 @@ export interface WorkstreamApprovalContinuationParams {
|
|
|
207
404
|
streamId?: string
|
|
208
405
|
}
|
|
209
406
|
|
|
407
|
+
export interface WorkstreamPlanTurnParams {
|
|
408
|
+
workstream: NormalizedWorkstream
|
|
409
|
+
workstreamRef: RecordIdRef
|
|
410
|
+
orgRef: RecordIdRef
|
|
411
|
+
userRef: RecordIdRef
|
|
412
|
+
userName?: string | null
|
|
413
|
+
planTurn: WorkstreamPlanTurnContext
|
|
414
|
+
abortSignal?: AbortSignal
|
|
415
|
+
streamId?: string
|
|
416
|
+
}
|
|
417
|
+
|
|
210
418
|
type WorkstreamRunCoreParams = {
|
|
211
419
|
workstream: NormalizedWorkstream
|
|
212
420
|
workstreamRef: RecordIdRef
|
|
@@ -220,6 +428,7 @@ type WorkstreamRunCoreParams = {
|
|
|
220
428
|
| { kind: 'userTurn'; inputMessage: ChatMessage; skipInputMessagePersistence?: boolean }
|
|
221
429
|
| { kind: 'approvalContinuation'; approvalMessages: ChatMessage[] }
|
|
222
430
|
| { kind: 'nativeToolApprovalTurn'; approvalMessages: ChatMessage[] }
|
|
431
|
+
| { kind: 'planTurn'; planTurn: WorkstreamPlanTurnContext }
|
|
223
432
|
)
|
|
224
433
|
|
|
225
434
|
interface PreparedWorkstreamTurn {
|
|
@@ -617,7 +826,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
617
826
|
let inputMessage: ChatMessage | undefined
|
|
618
827
|
const shouldPersistInputMessage = params.kind === 'userTurn' ? params.skipInputMessagePersistence !== true : false
|
|
619
828
|
const shouldProcessPostRunSideEffects =
|
|
620
|
-
params.kind
|
|
829
|
+
params.kind !== 'planTurn' &&
|
|
830
|
+
(params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn' || shouldPersistInputMessage)
|
|
621
831
|
if (params.kind === 'userTurn') {
|
|
622
832
|
inputMessage = hydrateMessageFileUrls(withMessageCreatedAt(params.inputMessage, Date.now()))
|
|
623
833
|
if (inputMessage.role !== 'user') {
|
|
@@ -638,7 +848,10 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
638
848
|
|
|
639
849
|
const workstreamRecord = await waitForWorkstreamCompactionIfNeeded(workstreamRef)
|
|
640
850
|
timer.step('compaction-gate')
|
|
641
|
-
if (
|
|
851
|
+
if (
|
|
852
|
+
(await workstreamService.hasActiveRunLease(workstreamRef)) ||
|
|
853
|
+
toOptionalTrimmedString(workstreamRecord.activeRunId)
|
|
854
|
+
) {
|
|
642
855
|
const clearedStaleRun = await workstreamService.clearStaleActiveRunIfMissingFromRegistry(workstreamRef)
|
|
643
856
|
if (!clearedStaleRun) {
|
|
644
857
|
throw new WorkstreamTurnError('A chat run is already active.', 409)
|
|
@@ -713,10 +926,17 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
713
926
|
const originalMessages = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
|
|
714
927
|
let allAssistantMessages: ChatMessage[] = []
|
|
715
928
|
const referenceUserMessage =
|
|
716
|
-
params.kind === '
|
|
717
|
-
?
|
|
718
|
-
:
|
|
719
|
-
|
|
929
|
+
params.kind === 'planTurn'
|
|
930
|
+
? undefined
|
|
931
|
+
: params.kind === 'userTurn' && !shouldPersistInputMessage
|
|
932
|
+
? [...liveHistory].reverse().find((m) => m.role === 'user')
|
|
933
|
+
: (userMessage ?? [...liveHistory].reverse().find((m) => m.role === 'user'))
|
|
934
|
+
const messageText =
|
|
935
|
+
params.kind === 'planTurn'
|
|
936
|
+
? `${params.planTurn.nodeSpec.label}\n${params.planTurn.nodeSpec.objective}\n${params.planTurn.nodeSpec.instructions}`
|
|
937
|
+
: referenceUserMessage
|
|
938
|
+
? extractMessageText(referenceUserMessage).trim()
|
|
939
|
+
: ''
|
|
720
940
|
|
|
721
941
|
if (
|
|
722
942
|
params.kind === 'userTurn' &&
|
|
@@ -975,9 +1195,17 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
975
1195
|
},
|
|
976
1196
|
})
|
|
977
1197
|
|
|
978
|
-
const commitAssistantResponse = async (
|
|
1198
|
+
const commitAssistantResponse = async (
|
|
1199
|
+
response: ChatMessage,
|
|
1200
|
+
agentId: string,
|
|
1201
|
+
agentName: string,
|
|
1202
|
+
metadataPatch?: NonNullable<MessageMetadata>,
|
|
1203
|
+
) => {
|
|
979
1204
|
const committed = withMessageCreatedAt(
|
|
980
|
-
{
|
|
1205
|
+
{
|
|
1206
|
+
...response,
|
|
1207
|
+
metadata: { ...response.metadata, ...buildAgentMetadataPatch(agentId, agentName), ...metadataPatch },
|
|
1208
|
+
},
|
|
981
1209
|
Date.now(),
|
|
982
1210
|
)
|
|
983
1211
|
|
|
@@ -1017,11 +1245,15 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1017
1245
|
additionalInstructionSections?: string[]
|
|
1018
1246
|
extraMessages?: ChatMessage[]
|
|
1019
1247
|
extraTools?: ToolSet
|
|
1248
|
+
filterTools?: (tools: ToolSet) => ToolSet
|
|
1249
|
+
includeExecutionPlanTools?: boolean
|
|
1250
|
+
metadataPatch?: NonNullable<MessageMetadata>
|
|
1020
1251
|
}): Promise<ChatMessage> => {
|
|
1021
1252
|
const visibleTimer = lotaDebugLogger.timer(`visible:${runParams.agentId}`)
|
|
1022
1253
|
let runMemoryBlock = memoryBlock
|
|
1023
|
-
const includeExecutionPlanTools =
|
|
1024
|
-
|
|
1254
|
+
const includeExecutionPlanTools =
|
|
1255
|
+
runParams.includeExecutionPlanTools ?? (runParams.mode !== 'fixedWorkstreamMode' && !onboardingActive)
|
|
1256
|
+
const rawTools: ToolSet = {
|
|
1025
1257
|
...((await buildAgentTools({
|
|
1026
1258
|
agentId: runParams.agentId,
|
|
1027
1259
|
orgId: orgRef,
|
|
@@ -1049,6 +1281,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1049
1281
|
...toolProviders,
|
|
1050
1282
|
...runParams.extraTools,
|
|
1051
1283
|
}
|
|
1284
|
+
const tools = runParams.filterTools ? runParams.filterTools(rawTools) : rawTools
|
|
1052
1285
|
visibleTimer.step('build-agent-tools')
|
|
1053
1286
|
streamCtx.memoryBlock = memoryBlock
|
|
1054
1287
|
const responseMessage = await streamAgentResponse(streamCtx, {
|
|
@@ -1069,6 +1302,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1069
1302
|
responseMessage,
|
|
1070
1303
|
runParams.agentId,
|
|
1071
1304
|
agentDisplayNames[runParams.agentId] ?? runParams.agentId,
|
|
1305
|
+
runParams.metadataPatch,
|
|
1072
1306
|
)
|
|
1073
1307
|
}
|
|
1074
1308
|
|
|
@@ -1078,181 +1312,214 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1078
1312
|
return { inputMessageId: referenceUserMessage?.id, assistantMessages: [] }
|
|
1079
1313
|
}
|
|
1080
1314
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
workstreamId: workstreamRef,
|
|
1096
|
-
orgIdString,
|
|
1097
|
-
workstreamMode: workstream.mode,
|
|
1098
|
-
mode: 'fixedWorkstreamMode',
|
|
1099
|
-
linearInstalled,
|
|
1100
|
-
onboardingActive,
|
|
1101
|
-
githubInstalled,
|
|
1102
|
-
provideRepoTool: indexedRepoContext.provideRepoTool,
|
|
1103
|
-
defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[agentId],
|
|
1104
|
-
memoryBlock: specialistMemoryBlock,
|
|
1105
|
-
onAppendMemoryBlock: (value: string) => {
|
|
1106
|
-
specialistMemoryBlock = value
|
|
1107
|
-
},
|
|
1108
|
-
availableUploads: listReadableUploads([specialistTaskMessage]),
|
|
1109
|
-
includeExecutionPlanTools: false,
|
|
1110
|
-
context: buildContextResult,
|
|
1111
|
-
})
|
|
1112
|
-
|
|
1113
|
-
const [
|
|
1114
|
-
specialistExecutionPlanInstructionSections,
|
|
1115
|
-
specialistPreSeededMemories,
|
|
1116
|
-
specialistWorkstreamState,
|
|
1117
|
-
specialistLearnedSkills,
|
|
1118
|
-
] = await Promise.all([
|
|
1119
|
-
getExecutionPlanInstructionSections(),
|
|
1120
|
-
getPreSeededMemoriesSection(agentId),
|
|
1121
|
-
getWorkstreamStateSection(),
|
|
1122
|
-
getLearnedSkillsSection(agentId),
|
|
1123
|
-
])
|
|
1124
|
-
const specialistConfig = getAgentRuntimeConfig({
|
|
1125
|
-
agentId,
|
|
1126
|
-
workstreamMode: workstream.mode,
|
|
1127
|
-
mode: 'fixedWorkstreamMode',
|
|
1128
|
-
onboardingActive,
|
|
1129
|
-
linearInstalled,
|
|
1130
|
-
systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
|
|
1131
|
-
preSeededMemoriesSection: specialistPreSeededMemories,
|
|
1132
|
-
retrievedKnowledgeSection,
|
|
1133
|
-
workstreamMemoryBlock: specialistMemoryBlock,
|
|
1134
|
-
workstreamStateSection: specialistWorkstreamState,
|
|
1135
|
-
learnedSkillsSection: specialistLearnedSkills,
|
|
1136
|
-
additionalInstructionSections: mergeInstructionSections(
|
|
1137
|
-
specialistExecutionPlanInstructionSections,
|
|
1138
|
-
coreInstructionSections,
|
|
1139
|
-
hookInstructionSections,
|
|
1140
|
-
),
|
|
1141
|
-
context: buildContextResult,
|
|
1142
|
-
}) as Record<string, unknown>
|
|
1143
|
-
const observer = createObserver(agentId)
|
|
1144
|
-
const agent = createAgent[specialistConfig.id as string]({
|
|
1145
|
-
mode: 'fixedWorkstreamMode',
|
|
1146
|
-
tools: { ...(specialistTools as ToolSet), ...toolProviders },
|
|
1147
|
-
extraInstructions: specialistConfig.extraInstructions,
|
|
1148
|
-
stopWhen: [stepCountIs(specialistConfig.maxSteps as number)],
|
|
1149
|
-
}) as ToolLoopAgent<never, ToolSet>
|
|
1150
|
-
const modelMessages = await convertToModelMessages(buildRunInputMessages([specialistTaskMessage]), {
|
|
1151
|
-
ignoreIncompleteToolCalls: true,
|
|
1152
|
-
})
|
|
1153
|
-
const specialistAbortSignal = toolAbortSignal ?? runAbort.signal
|
|
1154
|
-
let result: unknown
|
|
1155
|
-
try {
|
|
1156
|
-
result = await observer.run(() =>
|
|
1157
|
-
agent.stream({ messages: modelMessages, abortSignal: specialistAbortSignal }),
|
|
1158
|
-
)
|
|
1159
|
-
} catch (error) {
|
|
1160
|
-
if (specialistAbortSignal.aborted) {
|
|
1161
|
-
observer.recordAbort(error)
|
|
1162
|
-
} else {
|
|
1163
|
-
observer.recordError(error)
|
|
1164
|
-
}
|
|
1165
|
-
throw error
|
|
1166
|
-
}
|
|
1167
|
-
if (!hasUIMessageStream(result)) {
|
|
1168
|
-
throw new Error(`Specialist ${agentId} did not expose a UI message stream.`)
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
let finalMessage: ChatMessage | null = null
|
|
1172
|
-
for await (const message of readUIMessageStream<ChatMessage>({
|
|
1173
|
-
stream: result.toUIMessageStream({
|
|
1174
|
-
generateMessageId: () => Bun.randomUUIDv7(),
|
|
1175
|
-
sendReasoning: true,
|
|
1176
|
-
sendSources: true,
|
|
1177
|
-
sendStart: false,
|
|
1178
|
-
sendFinish: false,
|
|
1179
|
-
messageMetadata: createAgentMessageMetadata({
|
|
1180
|
-
agentId,
|
|
1181
|
-
agentName: specialistConfig.displayName as string,
|
|
1182
|
-
}),
|
|
1183
|
-
}) as ReadableStream<never>,
|
|
1184
|
-
})) {
|
|
1185
|
-
finalMessage = withMessageCreatedAt(message, Date.now())
|
|
1186
|
-
yield finalMessage
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
if (!finalMessage) {
|
|
1190
|
-
throw new Error(`Specialist ${agentId} did not produce a response message.`)
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
for (const toolError of collectToolOutputErrors({ responseMessage: finalMessage })) {
|
|
1194
|
-
aiLogger.warn`Tool execution failed (agent=${agentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
memoryBlock = specialistMemoryBlock
|
|
1198
|
-
return finalMessage
|
|
1199
|
-
},
|
|
1200
|
-
toModelOutput: ({ output }) => {
|
|
1201
|
-
const result = getChatMessageFromToolOutput(output)
|
|
1202
|
-
const agentName =
|
|
1203
|
-
typeof result?.metadata?.agentName === 'string' && result.metadata.agentName.trim().length > 0
|
|
1204
|
-
? result.metadata.agentName.trim()
|
|
1205
|
-
: 'Specialist'
|
|
1206
|
-
const summary = result ? extractMessageText(result).trim() : ''
|
|
1207
|
-
return {
|
|
1208
|
-
type: 'text',
|
|
1209
|
-
value: summary ? `${agentName}: ${summary}` : `${agentName} completed the requested task.`,
|
|
1210
|
-
}
|
|
1211
|
-
},
|
|
1212
|
-
})
|
|
1315
|
+
if (params.kind === 'planTurn') {
|
|
1316
|
+
const planTurn = params.planTurn
|
|
1317
|
+
const submitPlanTurnNodeResultTool = createTool({
|
|
1318
|
+
description: buildPlanTurnSubmitToolDescription(planTurn),
|
|
1319
|
+
inputSchema: PlanNodeResultSubmissionSchema,
|
|
1320
|
+
execute: async (result) =>
|
|
1321
|
+
await executionPlanService.submitPlanTurnResult({
|
|
1322
|
+
workstreamId: workstreamRef,
|
|
1323
|
+
runId: planTurn.runId,
|
|
1324
|
+
nodeId: planTurn.nodeId,
|
|
1325
|
+
emittedBy: planTurn.nodeSpec.owner.ref,
|
|
1326
|
+
input: result,
|
|
1327
|
+
}),
|
|
1328
|
+
})
|
|
1213
1329
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
?
|
|
1217
|
-
|
|
1218
|
-
|
|
1330
|
+
await runVisibleAgent({
|
|
1331
|
+
agentId: planTurn.nodeSpec.owner.ref,
|
|
1332
|
+
mode: workstream.mode === 'direct' ? 'direct' : 'workstreamMode',
|
|
1333
|
+
additionalInstructionSections: mergeInstructionSections(
|
|
1334
|
+
[buildPlanTurnExecutionSection(planTurn), buildPlanTurnResultContractSection(planTurn)],
|
|
1335
|
+
optionalInstructionSection(buildUpstreamHandoffSection(planTurn.upstreamHandoffs)),
|
|
1336
|
+
),
|
|
1337
|
+
extraMessages: [buildPlanTurnPromptMessage(planTurn)],
|
|
1338
|
+
includeExecutionPlanTools: false,
|
|
1339
|
+
extraTools: { [SUBMIT_PLAN_TURN_RESULT_TOOL_NAME]: submitPlanTurnNodeResultTool },
|
|
1340
|
+
filterTools: (tools) => applyPlanTurnToolPolicy(tools, planTurn.nodeSpec),
|
|
1341
|
+
metadataPatch: { trigger: 'plan-turn', planRunId: planTurn.runId, planNodeId: planTurn.nodeId },
|
|
1342
|
+
})
|
|
1343
|
+
} else {
|
|
1344
|
+
const consultSpecialistTool = createTool({
|
|
1345
|
+
description: 'Consult one specialist teammate for domain-specific guidance before replying to the user.',
|
|
1346
|
+
inputSchema: ConsultSpecialistArgsSchema,
|
|
1347
|
+
execute: async function* (
|
|
1348
|
+
{ agentId, task }: z.infer<typeof ConsultSpecialistArgsSchema>,
|
|
1349
|
+
{ abortSignal: toolAbortSignal }: { abortSignal?: AbortSignal },
|
|
1350
|
+
) {
|
|
1351
|
+
let specialistMemoryBlock = memoryBlock
|
|
1352
|
+
const specialistTaskMessage = buildSpecialistTaskMessage({ agentId, task })
|
|
1353
|
+
const specialistTools = await buildAgentTools({
|
|
1354
|
+
agentId,
|
|
1219
1355
|
orgId: orgRef,
|
|
1220
1356
|
userId: userRef,
|
|
1357
|
+
userName: userName ?? 'there',
|
|
1221
1358
|
workstreamId: workstreamRef,
|
|
1359
|
+
orgIdString,
|
|
1360
|
+
workstreamMode: workstream.mode,
|
|
1361
|
+
mode: 'fixedWorkstreamMode',
|
|
1362
|
+
linearInstalled,
|
|
1363
|
+
onboardingActive,
|
|
1222
1364
|
githubInstalled,
|
|
1223
|
-
availableUploads: listReadableUploads(),
|
|
1224
1365
|
provideRepoTool: indexedRepoContext.provideRepoTool,
|
|
1225
|
-
|
|
1366
|
+
defaultRepoSections: indexedRepoContext.defaultSectionsByAgent[agentId],
|
|
1367
|
+
memoryBlock: specialistMemoryBlock,
|
|
1368
|
+
onAppendMemoryBlock: (value: string) => {
|
|
1369
|
+
specialistMemoryBlock = value
|
|
1370
|
+
},
|
|
1371
|
+
availableUploads: listReadableUploads([specialistTaskMessage]),
|
|
1372
|
+
includeExecutionPlanTools: false,
|
|
1373
|
+
context: buildContextResult,
|
|
1374
|
+
})
|
|
1375
|
+
|
|
1376
|
+
const [
|
|
1377
|
+
specialistExecutionPlanInstructionSections,
|
|
1378
|
+
specialistPreSeededMemories,
|
|
1379
|
+
specialistWorkstreamState,
|
|
1380
|
+
specialistLearnedSkills,
|
|
1381
|
+
] = await Promise.all([
|
|
1382
|
+
getExecutionPlanInstructionSections(),
|
|
1383
|
+
getPreSeededMemoriesSection(agentId),
|
|
1384
|
+
getWorkstreamStateSection(),
|
|
1385
|
+
getLearnedSkillsSection(agentId),
|
|
1386
|
+
])
|
|
1387
|
+
const specialistConfig = getAgentRuntimeConfig({
|
|
1388
|
+
agentId,
|
|
1389
|
+
workstreamMode: workstream.mode,
|
|
1390
|
+
mode: 'fixedWorkstreamMode',
|
|
1391
|
+
onboardingActive,
|
|
1392
|
+
linearInstalled,
|
|
1226
1393
|
systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
|
|
1227
|
-
|
|
1394
|
+
preSeededMemoriesSection: specialistPreSeededMemories,
|
|
1228
1395
|
retrievedKnowledgeSection,
|
|
1396
|
+
workstreamMemoryBlock: specialistMemoryBlock,
|
|
1397
|
+
workstreamStateSection: specialistWorkstreamState,
|
|
1398
|
+
learnedSkillsSection: specialistLearnedSkills,
|
|
1229
1399
|
additionalInstructionSections: mergeInstructionSections(
|
|
1400
|
+
specialistExecutionPlanInstructionSections,
|
|
1230
1401
|
coreInstructionSections,
|
|
1231
1402
|
hookInstructionSections,
|
|
1232
1403
|
),
|
|
1233
|
-
getAdditionalInstructionSections: getExecutionPlanInstructionSections,
|
|
1234
1404
|
context: buildContextResult,
|
|
1235
|
-
|
|
1236
|
-
|
|
1405
|
+
}) as Record<string, unknown>
|
|
1406
|
+
const observer = createObserver(agentId)
|
|
1407
|
+
const agent = createAgent[specialistConfig.id as string]({
|
|
1408
|
+
mode: 'fixedWorkstreamMode',
|
|
1409
|
+
tools: { ...(specialistTools as ToolSet), ...toolProviders },
|
|
1410
|
+
extraInstructions: specialistConfig.extraInstructions,
|
|
1411
|
+
stopWhen: [stepCountIs(specialistConfig.maxSteps as number)],
|
|
1412
|
+
}) as ToolLoopAgent<never, ToolSet>
|
|
1413
|
+
const modelMessages = await convertToModelMessages(buildRunInputMessages([specialistTaskMessage]), {
|
|
1414
|
+
ignoreIncompleteToolCalls: true,
|
|
1237
1415
|
})
|
|
1238
|
-
|
|
1416
|
+
const specialistAbortSignal = toolAbortSignal ?? runAbort.signal
|
|
1417
|
+
let result: unknown
|
|
1418
|
+
try {
|
|
1419
|
+
result = await observer.run(() =>
|
|
1420
|
+
agent.stream({ messages: modelMessages, abortSignal: specialistAbortSignal }),
|
|
1421
|
+
)
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
if (specialistAbortSignal.aborted) {
|
|
1424
|
+
observer.recordAbort(error)
|
|
1425
|
+
} else {
|
|
1426
|
+
observer.recordError(error)
|
|
1427
|
+
}
|
|
1428
|
+
throw error
|
|
1429
|
+
}
|
|
1430
|
+
if (!hasUIMessageStream(result)) {
|
|
1431
|
+
throw new Error(`Specialist ${agentId} did not expose a UI message stream.`)
|
|
1432
|
+
}
|
|
1239
1433
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1434
|
+
let finalMessage: ChatMessage | null = null
|
|
1435
|
+
for await (const message of readUIMessageStream<ChatMessage>({
|
|
1436
|
+
stream: result.toUIMessageStream({
|
|
1437
|
+
generateMessageId: () => Bun.randomUUIDv7(),
|
|
1438
|
+
sendReasoning: true,
|
|
1439
|
+
sendSources: true,
|
|
1440
|
+
sendStart: false,
|
|
1441
|
+
sendFinish: false,
|
|
1442
|
+
messageMetadata: createAgentMessageMetadata({
|
|
1443
|
+
agentId,
|
|
1444
|
+
agentName: specialistConfig.displayName as string,
|
|
1445
|
+
}),
|
|
1446
|
+
}) as ReadableStream<never>,
|
|
1447
|
+
})) {
|
|
1448
|
+
finalMessage = withMessageCreatedAt(message, Date.now())
|
|
1449
|
+
yield finalMessage
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (!finalMessage) {
|
|
1453
|
+
throw new Error(`Specialist ${agentId} did not produce a response message.`)
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
for (const toolError of collectToolOutputErrors({ responseMessage: finalMessage })) {
|
|
1457
|
+
aiLogger.warn`Tool execution failed (agent=${agentId}, tool=${toolError.toolName}, toolCallId=${toolError.toolCallId}): ${toolError.errorText}`
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
memoryBlock = specialistMemoryBlock
|
|
1461
|
+
return finalMessage
|
|
1462
|
+
},
|
|
1463
|
+
toModelOutput: ({ output }) => {
|
|
1464
|
+
const result = getChatMessageFromToolOutput(output)
|
|
1465
|
+
const agentName =
|
|
1466
|
+
typeof result?.metadata?.agentName === 'string' && result.metadata.agentName.trim().length > 0
|
|
1467
|
+
? result.metadata.agentName.trim()
|
|
1468
|
+
: 'Specialist'
|
|
1469
|
+
const summary = result ? extractMessageText(result).trim() : ''
|
|
1470
|
+
return {
|
|
1471
|
+
type: 'text',
|
|
1472
|
+
value: summary ? `${agentName}: ${summary}` : `${agentName} completed the requested task.`,
|
|
1473
|
+
}
|
|
1254
1474
|
},
|
|
1255
1475
|
})
|
|
1476
|
+
|
|
1477
|
+
const teamThinkTool =
|
|
1478
|
+
workstream.mode === 'group' && !onboardingActive
|
|
1479
|
+
? createTeamThinkTool({
|
|
1480
|
+
historyMessages: currentMessages,
|
|
1481
|
+
latestUserMessageId: referenceUserMessageId,
|
|
1482
|
+
orgId: orgRef,
|
|
1483
|
+
userId: userRef,
|
|
1484
|
+
workstreamId: workstreamRef,
|
|
1485
|
+
githubInstalled,
|
|
1486
|
+
availableUploads: listReadableUploads(),
|
|
1487
|
+
provideRepoTool: indexedRepoContext.provideRepoTool,
|
|
1488
|
+
defaultRepoSectionsByAgent: indexedRepoContext.defaultSectionsByAgent as never,
|
|
1489
|
+
systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
|
|
1490
|
+
getPreSeededMemoriesSection,
|
|
1491
|
+
retrievedKnowledgeSection,
|
|
1492
|
+
additionalInstructionSections: mergeInstructionSections(
|
|
1493
|
+
coreInstructionSections,
|
|
1494
|
+
hookInstructionSections,
|
|
1495
|
+
),
|
|
1496
|
+
getAdditionalInstructionSections: getExecutionPlanInstructionSections,
|
|
1497
|
+
context: buildContextResult,
|
|
1498
|
+
toolProviders,
|
|
1499
|
+
abortSignal: runAbort.signal,
|
|
1500
|
+
})
|
|
1501
|
+
: null
|
|
1502
|
+
|
|
1503
|
+
if (workstream.mode === 'direct') {
|
|
1504
|
+
if (!workstream.agentId) {
|
|
1505
|
+
throw new WorkstreamTurnError('Direct workstreams require an assigned agent.', 400)
|
|
1506
|
+
}
|
|
1507
|
+
await runVisibleAgent({ agentId: workstream.agentId, mode: 'direct' })
|
|
1508
|
+
} else {
|
|
1509
|
+
await runVisibleAgent({
|
|
1510
|
+
agentId: visibleWorkstreamAgentId ?? defaultLeadAgentId,
|
|
1511
|
+
mode: 'workstreamMode',
|
|
1512
|
+
skills: coreWorkstreamProfile?.skills ? [...coreWorkstreamProfile.skills] : undefined,
|
|
1513
|
+
additionalInstructionSections: mergeInstructionSections(
|
|
1514
|
+
coreInstructionSections,
|
|
1515
|
+
hookInstructionSections,
|
|
1516
|
+
),
|
|
1517
|
+
extraTools: {
|
|
1518
|
+
[CONSULT_SPECIALIST_TOOL_NAME]: consultSpecialistTool,
|
|
1519
|
+
...(teamThinkTool ? { [CONSULT_TEAM_TOOL_NAME]: teamThinkTool } : {}),
|
|
1520
|
+
},
|
|
1521
|
+
})
|
|
1522
|
+
}
|
|
1256
1523
|
}
|
|
1257
1524
|
} finally {
|
|
1258
1525
|
try {
|
|
@@ -1312,7 +1579,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1312
1579
|
})
|
|
1313
1580
|
}
|
|
1314
1581
|
|
|
1315
|
-
if (allAssistantMessages.length > 0) {
|
|
1582
|
+
if (allAssistantMessages.length > 0 && params.kind !== 'planTurn') {
|
|
1316
1583
|
await turnHooks.afterTurn?.({
|
|
1317
1584
|
workstream,
|
|
1318
1585
|
workstreamRef,
|
|
@@ -1333,12 +1600,21 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1333
1600
|
}
|
|
1334
1601
|
}
|
|
1335
1602
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1603
|
+
try {
|
|
1604
|
+
return await workstreamService.withActiveRunLease(workstreamRef, async () => {
|
|
1605
|
+
const runResult = await executeRun()
|
|
1606
|
+
if (runResult) {
|
|
1607
|
+
return runResult
|
|
1608
|
+
}
|
|
1340
1609
|
|
|
1341
|
-
|
|
1610
|
+
return { inputMessageId: referenceUserMessage?.id, assistantMessages: [...allAssistantMessages] }
|
|
1611
|
+
})
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
if (error instanceof ActiveWorkstreamRunConflictError) {
|
|
1614
|
+
throw new WorkstreamTurnError(error.message, 409)
|
|
1615
|
+
}
|
|
1616
|
+
throw error
|
|
1617
|
+
}
|
|
1342
1618
|
},
|
|
1343
1619
|
}
|
|
1344
1620
|
}
|