@leclabs/agent-flow-navigator-mcp 1.0.0
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/README.md +260 -0
- package/catalog/workflows/agile-task.json +130 -0
- package/catalog/workflows/bug-fix.json +153 -0
- package/catalog/workflows/context-optimization.json +130 -0
- package/catalog/workflows/feature-development.json +225 -0
- package/catalog/workflows/quick-task.json +115 -0
- package/catalog/workflows/test-coverage.json +153 -0
- package/catalog/workflows/ui-reconstruction.json +241 -0
- package/catalog.js +64 -0
- package/copier.js +179 -0
- package/diagram.js +146 -0
- package/dialog.js +63 -0
- package/engine.js +467 -0
- package/index.js +378 -0
- package/package.json +49 -0
- package/store.js +90 -0
- package/types.d.ts +133 -0
package/engine.js
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Engine
|
|
3
|
+
*
|
|
4
|
+
* Evaluates DAG transitions based on node outputs and retry state.
|
|
5
|
+
*
|
|
6
|
+
* Schema:
|
|
7
|
+
* - nodes: { [id]: { type, name, maxRetries?, ... } }
|
|
8
|
+
* - edges: [{ from, to, on?, label? }]
|
|
9
|
+
*
|
|
10
|
+
* Retry logic convention:
|
|
11
|
+
* - maxRetries on the node defines how many retries are allowed
|
|
12
|
+
* - Edge to non-end node = retry (taken if retries remaining)
|
|
13
|
+
* - Edge to end node = escalation (taken if retries exhausted)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync } from "fs";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read and parse a task file
|
|
20
|
+
* @param {string} taskFilePath - Path to task JSON file
|
|
21
|
+
* @returns {Object|null} Task object or null if not found/invalid
|
|
22
|
+
*/
|
|
23
|
+
export function readTaskFile(taskFilePath) {
|
|
24
|
+
if (!taskFilePath || !existsSync(taskFilePath)) return null;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(taskFilePath, "utf-8"));
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if a node is a terminal node (start or end)
|
|
34
|
+
*/
|
|
35
|
+
export function isTerminalNode(node) {
|
|
36
|
+
if (!node) return false;
|
|
37
|
+
return node.type === "start" || node.type === "end";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get terminal type for a node
|
|
42
|
+
* Returns: "start" | "success" | "hitl" | "failure" | null
|
|
43
|
+
*/
|
|
44
|
+
export function getTerminalType(node) {
|
|
45
|
+
if (!node) return null;
|
|
46
|
+
if (node.type === "start") return "start";
|
|
47
|
+
if (node.type === "end") {
|
|
48
|
+
if (node.escalation === "hitl") return "hitl";
|
|
49
|
+
return node.result === "success" ? "success" : "failure";
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert agent ID to subagent reference
|
|
56
|
+
* e.g., "developer" -> "@flow:developer"
|
|
57
|
+
*/
|
|
58
|
+
export function toSubagentRef(agentId) {
|
|
59
|
+
if (!agentId) return null;
|
|
60
|
+
if (agentId.startsWith("@")) return agentId;
|
|
61
|
+
return `@flow:${agentId}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate baseline instructions based on step type
|
|
66
|
+
*/
|
|
67
|
+
export function getBaselineInstructions(stepId, stepName) {
|
|
68
|
+
const id = stepId.toLowerCase();
|
|
69
|
+
const name = (stepName || "").toLowerCase();
|
|
70
|
+
|
|
71
|
+
// Analysis/Planning steps
|
|
72
|
+
if (id.includes("analyze") || id.includes("analysis") || name.includes("analyze")) {
|
|
73
|
+
return "Review the task requirements carefully. Identify key constraints, dependencies, and acceptance criteria. Create a clear plan before proceeding.";
|
|
74
|
+
}
|
|
75
|
+
if (id.includes("plan") || id.includes("design") || name.includes("plan")) {
|
|
76
|
+
return "Design the solution architecture. Consider edge cases, error handling, and how this fits with existing code. Document your approach.";
|
|
77
|
+
}
|
|
78
|
+
if (id.includes("investigate") || id.includes("reproduce")) {
|
|
79
|
+
return "Gather evidence and understand the root cause. Document reproduction steps and any patterns observed.";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Implementation steps
|
|
83
|
+
if (id.includes("implement") || id.includes("build") || id.includes("develop") || id.includes("fix")) {
|
|
84
|
+
return "Write clean, well-structured code following project conventions. Keep changes focused and minimal. Add comments only where the logic isn't self-evident.";
|
|
85
|
+
}
|
|
86
|
+
if (id.includes("refactor")) {
|
|
87
|
+
return "Improve code structure without changing behavior. Ensure all tests pass before and after changes.";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Testing steps
|
|
91
|
+
if (id.includes("test") || id.includes("verify") || id.includes("validate")) {
|
|
92
|
+
return "Verify the implementation works correctly. Test happy paths, edge cases, and error conditions. Document any issues found.";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Review steps
|
|
96
|
+
if (id.includes("review")) {
|
|
97
|
+
return "Check for correctness, code quality, and adherence to project standards. Verify the implementation meets requirements.";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Documentation steps
|
|
101
|
+
if (id.includes("document") || id.includes("readme")) {
|
|
102
|
+
return "Write clear, concise documentation. Focus on what users need to know, not implementation details.";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Commit/PR steps
|
|
106
|
+
if (id.includes("commit")) {
|
|
107
|
+
return "Stage relevant changes and create a descriptive commit message. Follow project commit conventions.";
|
|
108
|
+
}
|
|
109
|
+
if (id.includes("pr") || id.includes("pull_request") || id.includes("pull-request")) {
|
|
110
|
+
return "Create a pull request with a clear title and description. Link related issues and describe what was changed and why.";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Context/optimization steps
|
|
114
|
+
if (id.includes("context") || id.includes("optimize") || id.includes("compress")) {
|
|
115
|
+
return "Analyze the current state and identify improvements. Focus on clarity and efficiency.";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Extract/transform steps
|
|
119
|
+
if (id.includes("extract") || id.includes("ir_")) {
|
|
120
|
+
return "Extract the relevant information systematically. Preserve important details while filtering noise.";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Default
|
|
124
|
+
return "Complete this step thoroughly. Document your findings and any decisions made.";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build orchestrator instructions for task creation/update
|
|
129
|
+
* Returns null for terminal nodes (no further work)
|
|
130
|
+
*/
|
|
131
|
+
function buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description) {
|
|
132
|
+
if (!stepInstructions) return null; // Terminal nodes have no instructions
|
|
133
|
+
|
|
134
|
+
const delegationPrefix = subagent ? `Invoke ${subagent} to complete the following task: ` : "";
|
|
135
|
+
|
|
136
|
+
return `${delegationPrefix}${stepInstructions.guidance}
|
|
137
|
+
|
|
138
|
+
${description || "{task description}"}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build unified response shape for Navigate
|
|
143
|
+
* Minimal output: only what Orchestrator needs for control flow and delegation
|
|
144
|
+
*/
|
|
145
|
+
function buildNavigateResponse(
|
|
146
|
+
workflowType,
|
|
147
|
+
stepId,
|
|
148
|
+
stepDef,
|
|
149
|
+
action,
|
|
150
|
+
retriesIncremented = false,
|
|
151
|
+
retryCount = 0,
|
|
152
|
+
description = null
|
|
153
|
+
) {
|
|
154
|
+
const stage = stepDef.stage || null;
|
|
155
|
+
const subagent = stepDef.agent ? toSubagentRef(stepDef.agent) : null;
|
|
156
|
+
|
|
157
|
+
// Build step instructions from workflow definition + baseline
|
|
158
|
+
const isTerminal = isTerminalNode(stepDef);
|
|
159
|
+
const stepInstructions = isTerminal
|
|
160
|
+
? null
|
|
161
|
+
: {
|
|
162
|
+
name: stepDef.name || stepId,
|
|
163
|
+
description: stepDef.description || null,
|
|
164
|
+
guidance: stepDef.instructions || getBaselineInstructions(stepId, stepDef.name),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Build orchestrator instructions for all non-terminal actions
|
|
168
|
+
const orchestratorInstructions = isTerminal
|
|
169
|
+
? null
|
|
170
|
+
: buildOrchestratorInstructions(workflowType, stepId, stage, subagent, stepInstructions, description);
|
|
171
|
+
|
|
172
|
+
// Build metadata for task storage
|
|
173
|
+
const metadata = {
|
|
174
|
+
workflowType,
|
|
175
|
+
currentStep: stepId,
|
|
176
|
+
retryCount: retriesIncremented ? retryCount + 1 : retryCount,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
currentStep: stepId,
|
|
181
|
+
stage,
|
|
182
|
+
subagent,
|
|
183
|
+
stepInstructions,
|
|
184
|
+
terminal: getTerminalType(stepDef),
|
|
185
|
+
action,
|
|
186
|
+
retriesIncremented,
|
|
187
|
+
orchestratorInstructions,
|
|
188
|
+
metadata,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export class WorkflowEngine {
|
|
193
|
+
constructor(store) {
|
|
194
|
+
this.store = store;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Build adjacency list from edges array
|
|
199
|
+
*/
|
|
200
|
+
buildEdgeGraph(workflowId) {
|
|
201
|
+
const def = this.store.getDefinition(workflowId);
|
|
202
|
+
if (!def) throw new Error(`Workflow ${workflowId} not found`);
|
|
203
|
+
|
|
204
|
+
if (!def.nodes) {
|
|
205
|
+
throw new Error(`Workflow ${workflowId} must have nodes`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const graph = {
|
|
209
|
+
nodes: def.nodes,
|
|
210
|
+
edges: new Map(), // from -> [{ to, on, label }]
|
|
211
|
+
reverseEdges: new Map(), // to -> [from] for dependency checking
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (!def.edges || !Array.isArray(def.edges)) {
|
|
215
|
+
throw new Error(`Workflow ${workflowId} must have edges array`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const edge of def.edges) {
|
|
219
|
+
const { from, to, on = null, label = null } = edge;
|
|
220
|
+
|
|
221
|
+
if (!graph.edges.has(from)) {
|
|
222
|
+
graph.edges.set(from, []);
|
|
223
|
+
}
|
|
224
|
+
graph.edges.get(from).push({ to, on, label });
|
|
225
|
+
|
|
226
|
+
if (!graph.reverseEdges.has(to)) {
|
|
227
|
+
graph.reverseEdges.set(to, []);
|
|
228
|
+
}
|
|
229
|
+
graph.reverseEdges.get(to).push(from);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return graph;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if a node is an end node
|
|
237
|
+
*/
|
|
238
|
+
isEndNode(node) {
|
|
239
|
+
return node?.type === "end";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Evaluate which edge to take based on result and retry count
|
|
244
|
+
*/
|
|
245
|
+
evaluateEdge(workflowId, currentStep, result, retryCount = 0) {
|
|
246
|
+
const graph = this.buildEdgeGraph(workflowId);
|
|
247
|
+
const outgoingEdges = graph.edges.get(currentStep) || [];
|
|
248
|
+
const currentNode = graph.nodes[currentStep];
|
|
249
|
+
|
|
250
|
+
if (outgoingEdges.length === 0) {
|
|
251
|
+
return {
|
|
252
|
+
nextStep: null,
|
|
253
|
+
action: "no_outgoing_edges",
|
|
254
|
+
currentStep,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const maxRetries = currentNode?.maxRetries || 0;
|
|
259
|
+
const unconditionalEdges = outgoingEdges.filter((e) => !e.on);
|
|
260
|
+
const matchingEdges = outgoingEdges.filter((e) => e.on === result);
|
|
261
|
+
|
|
262
|
+
// No result provided - use unconditional edge
|
|
263
|
+
if (!result && unconditionalEdges.length > 0) {
|
|
264
|
+
const edge = unconditionalEdges[0];
|
|
265
|
+
return {
|
|
266
|
+
nextStep: edge.to,
|
|
267
|
+
action: "unconditional",
|
|
268
|
+
edge,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// No matching edges at all
|
|
273
|
+
if (matchingEdges.length === 0 && unconditionalEdges.length === 0) {
|
|
274
|
+
return {
|
|
275
|
+
nextStep: null,
|
|
276
|
+
action: "no_matching_edge",
|
|
277
|
+
currentStep,
|
|
278
|
+
result,
|
|
279
|
+
retryCount,
|
|
280
|
+
availableEdges: outgoingEdges.map((e) => ({ to: e.to, on: e.on })),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Separate retry edges (to non-end) from escalation edges (to end)
|
|
285
|
+
const retryEdges = matchingEdges.filter((e) => !this.isEndNode(graph.nodes[e.to]));
|
|
286
|
+
const escalateEdges = matchingEdges.filter((e) => this.isEndNode(graph.nodes[e.to]));
|
|
287
|
+
|
|
288
|
+
// Handle failed with both retry and escalation paths
|
|
289
|
+
if (result === "failed" && retryEdges.length > 0 && escalateEdges.length > 0) {
|
|
290
|
+
if (retryCount < maxRetries) {
|
|
291
|
+
const edge = retryEdges[0];
|
|
292
|
+
return {
|
|
293
|
+
nextStep: edge.to,
|
|
294
|
+
action: "retry",
|
|
295
|
+
retriesUsed: retryCount + 1,
|
|
296
|
+
retriesRemaining: maxRetries - retryCount - 1,
|
|
297
|
+
maxRetries,
|
|
298
|
+
edge,
|
|
299
|
+
};
|
|
300
|
+
} else {
|
|
301
|
+
const edge = escalateEdges[0];
|
|
302
|
+
return {
|
|
303
|
+
nextStep: edge.to,
|
|
304
|
+
action: "escalate",
|
|
305
|
+
reason: "max_retries_exceeded",
|
|
306
|
+
retriesUsed: retryCount,
|
|
307
|
+
maxRetries,
|
|
308
|
+
edge,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Single matching conditional edge
|
|
314
|
+
if (matchingEdges.length > 0) {
|
|
315
|
+
const edge = matchingEdges[0];
|
|
316
|
+
return {
|
|
317
|
+
nextStep: edge.to,
|
|
318
|
+
action: "conditional",
|
|
319
|
+
condition: result,
|
|
320
|
+
edge,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Fall back to unconditional
|
|
325
|
+
if (unconditionalEdges.length > 0) {
|
|
326
|
+
const edge = unconditionalEdges[0];
|
|
327
|
+
return {
|
|
328
|
+
nextStep: edge.to,
|
|
329
|
+
action: "unconditional",
|
|
330
|
+
edge,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
nextStep: null,
|
|
336
|
+
action: "no_matching_edge",
|
|
337
|
+
currentStep,
|
|
338
|
+
result,
|
|
339
|
+
retryCount,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Navigate through workflow: start, get current state, or advance
|
|
345
|
+
*
|
|
346
|
+
* Two calling modes:
|
|
347
|
+
* 1. Task file mode: Pass taskFilePath to read workflow state from task metadata
|
|
348
|
+
* 2. Direct mode: Pass workflowType directly (for starting workflows)
|
|
349
|
+
*
|
|
350
|
+
* @param {Object} options - Navigation options
|
|
351
|
+
* @param {string} [options.taskFilePath] - Path to task file (for advance/current)
|
|
352
|
+
* @param {string} [options.workflowType] - Workflow ID (for start only)
|
|
353
|
+
* @param {string} [options.result] - Step result: "passed" | "failed" (for advance)
|
|
354
|
+
* @param {string} [options.description] - User's task description
|
|
355
|
+
* @returns {Object} Navigation response with currentStep, stepInstructions, terminal, action, metadata, etc.
|
|
356
|
+
*/
|
|
357
|
+
navigate({ taskFilePath, workflowType, result, description } = {}) {
|
|
358
|
+
let currentStep = null;
|
|
359
|
+
let retryCount = 0;
|
|
360
|
+
|
|
361
|
+
// Task file mode: read workflow state from task metadata
|
|
362
|
+
if (taskFilePath) {
|
|
363
|
+
const task = readTaskFile(taskFilePath);
|
|
364
|
+
if (!task) {
|
|
365
|
+
throw new Error(`Task file not found: ${taskFilePath}`);
|
|
366
|
+
}
|
|
367
|
+
if (!task.metadata) {
|
|
368
|
+
throw new Error("Task has no metadata");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const {
|
|
372
|
+
userDescription,
|
|
373
|
+
workflowType: metaWorkflow,
|
|
374
|
+
currentStep: metaStep,
|
|
375
|
+
retryCount: metaRetry = 0,
|
|
376
|
+
} = task.metadata;
|
|
377
|
+
workflowType = metaWorkflow;
|
|
378
|
+
currentStep = metaStep;
|
|
379
|
+
retryCount = metaRetry;
|
|
380
|
+
description = description || userDescription;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Validate workflowType
|
|
384
|
+
if (!workflowType) {
|
|
385
|
+
throw new Error("workflowType is required (either directly or via task metadata)");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const wfDef = this.store.getDefinition(workflowType);
|
|
389
|
+
if (!wfDef) {
|
|
390
|
+
throw new Error(`Workflow '${workflowType}' not found. Use ListWorkflows to see available workflows.`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!wfDef.nodes) {
|
|
394
|
+
throw new Error(`Workflow '${workflowType}' must have nodes`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const { nodes } = wfDef;
|
|
398
|
+
|
|
399
|
+
// Case 1: No currentStep - start at first work step
|
|
400
|
+
if (!currentStep) {
|
|
401
|
+
const startEntry = Object.entries(nodes).find(([, node]) => node.type === "start");
|
|
402
|
+
if (!startEntry) {
|
|
403
|
+
throw new Error(`Workflow '${workflowType}' has no start node`);
|
|
404
|
+
}
|
|
405
|
+
const startStepId = startEntry[0];
|
|
406
|
+
|
|
407
|
+
const firstEdge = wfDef.edges.find((e) => e.from === startStepId);
|
|
408
|
+
if (!firstEdge) {
|
|
409
|
+
throw new Error(`No edge from start step in workflow '${workflowType}'`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const firstStepDef = nodes[firstEdge.to];
|
|
413
|
+
if (!firstStepDef) {
|
|
414
|
+
throw new Error(`First step '${firstEdge.to}' not found in workflow`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return buildNavigateResponse(workflowType, firstEdge.to, firstStepDef, "start", false, 0, description);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Case 2: currentStep but no result - return current state
|
|
421
|
+
if (!result) {
|
|
422
|
+
const stepDef = nodes[currentStep];
|
|
423
|
+
if (!stepDef) {
|
|
424
|
+
throw new Error(`Step '${currentStep}' not found in workflow '${workflowType}'`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return buildNavigateResponse(workflowType, currentStep, stepDef, "current", false, retryCount, description);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Case 3: currentStep and result - advance to next step
|
|
431
|
+
const evaluation = this.evaluateEdge(workflowType, currentStep, result, retryCount);
|
|
432
|
+
|
|
433
|
+
if (!evaluation.nextStep) {
|
|
434
|
+
return {
|
|
435
|
+
error: `No matching edge from '${currentStep}' with result '${result}'`,
|
|
436
|
+
currentStep,
|
|
437
|
+
evaluation,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const nextStepDef = nodes[evaluation.nextStep];
|
|
442
|
+
if (!nextStepDef) {
|
|
443
|
+
throw new Error(`Next step '${evaluation.nextStep}' not found in workflow`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Determine action and whether retries incremented
|
|
447
|
+
const isRetry = evaluation.action === "retry";
|
|
448
|
+
let action;
|
|
449
|
+
if (isRetry) {
|
|
450
|
+
action = "retry";
|
|
451
|
+
} else if (getTerminalType(nextStepDef) === "hitl") {
|
|
452
|
+
action = "escalate";
|
|
453
|
+
} else {
|
|
454
|
+
action = "advance";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return buildNavigateResponse(
|
|
458
|
+
workflowType,
|
|
459
|
+
evaluation.nextStep,
|
|
460
|
+
nextStepDef,
|
|
461
|
+
action,
|
|
462
|
+
isRetry,
|
|
463
|
+
retryCount,
|
|
464
|
+
description
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|