metheus-governance-mcp-cli 0.2.146 → 0.2.147
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/cli.mjs +5 -0
- package/lib/local-ai-adapters.mjs +183 -1
- package/lib/runner-execution.mjs +17 -0
- package/lib/runner-orchestration.mjs +475 -33
- package/lib/selftest-runner-scenarios.mjs +349 -0
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
analyzeHumanConversationIntentWithAI,
|
|
15
15
|
auditDirectHumanReplyWithAI,
|
|
16
16
|
normalizeExecutionArtifacts,
|
|
17
|
+
planRoleExecutionWithAI,
|
|
17
18
|
resolveLocalAIExecutionModel,
|
|
18
19
|
resolveGeminiReasoningConfig,
|
|
19
20
|
suggestLocalAIModelDisplayName,
|
|
@@ -143,6 +144,7 @@ import {
|
|
|
143
144
|
} from "./lib/runner-delivery.mjs";
|
|
144
145
|
import {
|
|
145
146
|
resolveRunnerExecutionPlan,
|
|
147
|
+
resolveRunnerExecutionPlanForRole,
|
|
146
148
|
resolveRunnerRoleProfile,
|
|
147
149
|
resolveRunnerWorkspaceSelection,
|
|
148
150
|
runRunnerAIExecution,
|
|
@@ -3000,16 +3002,19 @@ function buildRunnerExecutionDeps() {
|
|
|
3000
3002
|
return {
|
|
3001
3003
|
analyzeHumanConversationIntentWithAI,
|
|
3002
3004
|
auditDirectHumanReplyWithAI,
|
|
3005
|
+
planRoleExecutionWithAI,
|
|
3003
3006
|
normalizeRunnerRoleProfileName,
|
|
3004
3007
|
normalizeRunnerRoleProfile,
|
|
3005
3008
|
normalizeBotRunnerProjectMapping,
|
|
3006
3009
|
sanitizeWorkspaceCandidate,
|
|
3007
3010
|
loadProviderEnvConfig,
|
|
3011
|
+
loadBotRunnerConfig,
|
|
3008
3012
|
canUseLegacyRunnerCommand,
|
|
3009
3013
|
hasLegacyRunnerCommand,
|
|
3010
3014
|
isLegacyRunnerCommandOptInEnabled,
|
|
3011
3015
|
legacyRunnerCommandDisabledMessage,
|
|
3012
3016
|
normalizeExecutionArtifacts,
|
|
3017
|
+
resolveRunnerExecutionPlanForRole,
|
|
3013
3018
|
validateWorkspaceArtifacts,
|
|
3014
3019
|
tryJsonParse,
|
|
3015
3020
|
};
|
|
@@ -718,6 +718,50 @@ function normalizeExecutionContract(rawContract) {
|
|
|
718
718
|
};
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
+
const RUNNER_EXECUTION_STEP_ROLES = new Set(["monitor", "worker", "review", "approval"]);
|
|
722
|
+
|
|
723
|
+
function normalizeRoleExecutionStepRole(rawValue) {
|
|
724
|
+
const normalized = String(rawValue || "").trim().toLowerCase();
|
|
725
|
+
return RUNNER_EXECUTION_STEP_ROLES.has(normalized) ? normalized : "";
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function normalizeRoleExecutionStep(rawStep) {
|
|
729
|
+
const step = safeObject(rawStep);
|
|
730
|
+
const role = normalizeRoleExecutionStepRole(step.role || step.profile || step.role_profile || step.roleProfile);
|
|
731
|
+
const goal = firstNonEmptyString([
|
|
732
|
+
step.goal,
|
|
733
|
+
step.task,
|
|
734
|
+
step.objective,
|
|
735
|
+
step.description,
|
|
736
|
+
]);
|
|
737
|
+
if (!role || !goal) {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
role,
|
|
742
|
+
goal,
|
|
743
|
+
artifactsRequired: step.artifacts_required === true || step.artifactsRequired === true,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export function normalizeRoleExecutionPlan(rawPlan) {
|
|
748
|
+
const plan = safeObject(rawPlan);
|
|
749
|
+
if (!Object.keys(plan).length) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
const steps = ensureArray(plan.steps)
|
|
753
|
+
.map((item) => normalizeRoleExecutionStep(item))
|
|
754
|
+
.filter(Boolean);
|
|
755
|
+
const summaryRole = normalizeRoleExecutionStepRole(plan.summary_role || plan.summaryRole);
|
|
756
|
+
const requiresExecution = plan.requires_execution === true || plan.requiresExecution === true;
|
|
757
|
+
return {
|
|
758
|
+
requiresExecution,
|
|
759
|
+
summaryRole: summaryRole || (steps.length > 0 ? steps[steps.length - 1].role : ""),
|
|
760
|
+
steps,
|
|
761
|
+
reason: String(plan.reason || "").trim(),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
721
765
|
function normalizeModelAliasText(rawValue) {
|
|
722
766
|
return String(rawValue || "")
|
|
723
767
|
.trim()
|
|
@@ -1070,6 +1114,7 @@ function inferCurrentTurnPurpose({ trigger, conversation, selfBotUsername, other
|
|
|
1070
1114
|
|
|
1071
1115
|
export function buildLocalBotPrompt(payload, { terse = true } = {}) {
|
|
1072
1116
|
const safePayload = payload && typeof payload === "object" ? payload : {};
|
|
1117
|
+
const taskName = String(safePayload.task || "").trim();
|
|
1073
1118
|
const trigger = safePayload.trigger && typeof safePayload.trigger === "object" ? safePayload.trigger : {};
|
|
1074
1119
|
const responseContract = safePayload.response_contract && typeof safePayload.response_contract === "object"
|
|
1075
1120
|
? safePayload.response_contract
|
|
@@ -1156,9 +1201,14 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
|
|
|
1156
1201
|
const assignmentsForThisBot = currentExecutionAssignments.filter((item) => (
|
|
1157
1202
|
String(item.target_bot || item.targetBot || "").trim().replace(/^@+/, "").toLowerCase() === selfSelector.toLowerCase()
|
|
1158
1203
|
));
|
|
1204
|
+
const executionStep = safeObject(safePayload.execution_step);
|
|
1205
|
+
const executionPlan = safeObject(safePayload.execution_plan);
|
|
1206
|
+
const executionProgress = safeObject(safePayload.execution_progress);
|
|
1207
|
+
const isInternalExecutionStep = taskName === "execute_project_role_step";
|
|
1159
1208
|
|
|
1160
1209
|
const lines = [
|
|
1161
1210
|
"You are a Metheus local bot runner.",
|
|
1211
|
+
`Task: ${taskName || "reply_to_project_chat_message"}`,
|
|
1162
1212
|
`Provider: ${provider}`,
|
|
1163
1213
|
`Role: ${role}`,
|
|
1164
1214
|
`Role Profile: ${roleProfile || "-"}`,
|
|
@@ -1189,6 +1239,32 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
|
|
|
1189
1239
|
recentBotMessages.length ? recentBotMessages.map(formatContextCommentLine).join("\n") : "- none",
|
|
1190
1240
|
"",
|
|
1191
1241
|
];
|
|
1242
|
+
if (isInternalExecutionStep) {
|
|
1243
|
+
const stepIndex = intFromRawAllowZero(executionStep.index, 0);
|
|
1244
|
+
const stepTotal = intFromRawAllowZero(executionStep.total, 0);
|
|
1245
|
+
const stepRole = firstNonEmptyString([executionStep.role, roleProfile, role]);
|
|
1246
|
+
const stepGoal = String(executionStep.goal || "").trim();
|
|
1247
|
+
const priorArtifacts = ensureArray(executionProgress.produced_artifacts)
|
|
1248
|
+
.map((item) => firstNonEmptyString([safeObject(item).relativePath, safeObject(item).path]))
|
|
1249
|
+
.filter(Boolean);
|
|
1250
|
+
lines.push(
|
|
1251
|
+
"Internal execution step is active.",
|
|
1252
|
+
"This is not a placeholder chat reply task. Complete the assigned step now.",
|
|
1253
|
+
`Execution Step: ${stepIndex > 0 ? stepIndex : "-"} / ${stepTotal > 0 ? stepTotal : "-"}`,
|
|
1254
|
+
`Execution Step Role: ${stepRole || "-"}`,
|
|
1255
|
+
`Execution Step Goal: ${stepGoal || "-"}`,
|
|
1256
|
+
`Execution Step Requires Artifacts: ${executionStep.artifacts_required === true || executionStep.artifactsRequired === true ? "yes" : "no"}`,
|
|
1257
|
+
`Execution Plan Requires Execution: ${executionPlan.requires_execution === true || executionPlan.requiresExecution === true ? "yes" : "no"}`,
|
|
1258
|
+
`Execution Plan Summary Role: ${String(executionPlan.summary_role || executionPlan.summaryRole || "").trim() || "-"}`,
|
|
1259
|
+
`Prior Produced Artifacts: ${priorArtifacts.length ? priorArtifacts.join(", ") : "-"}`,
|
|
1260
|
+
"Do the work for this step now. Do not say you will start later, wait, come back later, or plan first.",
|
|
1261
|
+
"If this is a worker step, actually create/update the required project artifacts now and include them in artifacts.",
|
|
1262
|
+
"If this is a review step, provide the review findings now.",
|
|
1263
|
+
"If this is an approval step, provide the approval decision now.",
|
|
1264
|
+
"If this is the final step, the reply will be shown to humans. If it is not the final step, the reply is still an internal execution note and must describe what was actually completed in this step.",
|
|
1265
|
+
"",
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1192
1268
|
if (responseContract.must_reply === true) {
|
|
1193
1269
|
lines.push(
|
|
1194
1270
|
"This bot was explicitly addressed by the trigger message.",
|
|
@@ -1232,7 +1308,9 @@ export function buildLocalBotPrompt(payload, { terse = true } = {}) {
|
|
|
1232
1308
|
"- If no project files changed, return artifacts: [].",
|
|
1233
1309
|
"- Do not claim that a file, plan, document, or code change is complete unless the corresponding artifact path is present in artifacts.",
|
|
1234
1310
|
"",
|
|
1235
|
-
|
|
1311
|
+
isInternalExecutionStep
|
|
1312
|
+
? "Return JSON only in one line: {\"reply\":\"what was completed in this step\",\"artifacts\":[{\"path\":\"relative/or/absolute/path\",\"kind\":\"plan|code|doc|spec|test\",\"operation\":\"create|update|delete\"}],\"contract\":{\"type\":\"direct_result|summary_request|final_summary\",\"actionable\":true,\"summary_bot\":\"username\",\"next_responders\":[\"username\"]}}. Use artifacts: [] only if this step truly changes no project files."
|
|
1313
|
+
: responseContract.must_reply === true
|
|
1236
1314
|
? "Return JSON only in one line: {\"reply\":\"...\",\"artifacts\":[]} or {\"reply\":\"...\",\"artifacts\":[],\"contract\":{\"type\":\"direct_result|delegation|summary_request|final_summary\",\"actionable\":true,\"assignments\":[{\"target_bot\":\"username\",\"task\":\"...\"}],\"summary_bot\":\"username\",\"next_responders\":[\"username\"]}}."
|
|
1237
1315
|
: terse
|
|
1238
1316
|
? "Return JSON only in one line: {\"reply\":\"...\",\"artifacts\":[]} or {\"skip\":true,\"reason\":\"...\"}."
|
|
@@ -1426,6 +1504,57 @@ function buildConversationIntentAnalysisPrompt({
|
|
|
1426
1504
|
].join("\n");
|
|
1427
1505
|
}
|
|
1428
1506
|
|
|
1507
|
+
function buildRoleExecutionPlanPrompt({
|
|
1508
|
+
messageText,
|
|
1509
|
+
currentBotUsername,
|
|
1510
|
+
currentBotRole,
|
|
1511
|
+
managedBots,
|
|
1512
|
+
workspaceDir,
|
|
1513
|
+
currentAssignmentTasks = [],
|
|
1514
|
+
contextComments = [],
|
|
1515
|
+
conversationMode = "",
|
|
1516
|
+
}) {
|
|
1517
|
+
const bots = ensureArray(managedBots).map((item) => {
|
|
1518
|
+
const bot = safeObject(item);
|
|
1519
|
+
return {
|
|
1520
|
+
username: String(bot.username || "").trim().replace(/^@+/, ""),
|
|
1521
|
+
display_name: String(bot.display_name || bot.displayName || bot.username || "").trim(),
|
|
1522
|
+
};
|
|
1523
|
+
}).filter((item) => item.username);
|
|
1524
|
+
const normalizedContext = ensureArray(contextComments)
|
|
1525
|
+
.slice(0, 10)
|
|
1526
|
+
.map((item) => formatContextCommentLine(item));
|
|
1527
|
+
return [
|
|
1528
|
+
"You are an execution planner for managed local Telegram bots.",
|
|
1529
|
+
"Read the human request and recent room context, then decide whether actual project execution is required now.",
|
|
1530
|
+
"Judge by meaning, not by keywords or @mentions alone.",
|
|
1531
|
+
"If the request only asks for status, explanation, location, or clarification, then requires_execution should be false.",
|
|
1532
|
+
"If the request asks the bot to create/update/delete project files, write plans/docs/code, delegate concrete tasks, continue execution, or otherwise do work now, then requires_execution should be true.",
|
|
1533
|
+
"When requires_execution=true, return the minimal role step plan using only these roles: monitor, worker, review, approval.",
|
|
1534
|
+
"Role guidance:",
|
|
1535
|
+
"- monitor: understand context, scope the work, plan, coordinate, or summarize.",
|
|
1536
|
+
"- worker: actually create/update/delete project artifacts now.",
|
|
1537
|
+
"- review: inspect completed work and report findings, issues, or improvements.",
|
|
1538
|
+
"- approval: make a go/no-go, approval, or explicit decision when the request calls for it.",
|
|
1539
|
+
"worker is required if project artifacts must be created or updated now.",
|
|
1540
|
+
"review is required if the request implies checking or validating the created result before finalizing.",
|
|
1541
|
+
"approval is required only when the request asks for approval, release, confirmation, or a formal decision.",
|
|
1542
|
+
"",
|
|
1543
|
+
"Return strict JSON only with this shape:",
|
|
1544
|
+
"{\"requires_execution\":true|false,\"summary_role\":\"monitor|worker|review|approval\",\"reason\":\"short explanation\",\"steps\":[{\"role\":\"monitor|worker|review|approval\",\"goal\":\"short goal\",\"artifacts_required\":true|false}]}",
|
|
1545
|
+
"",
|
|
1546
|
+
`current_bot_username=${JSON.stringify(String(currentBotUsername || "").trim().replace(/^@+/, ""))}`,
|
|
1547
|
+
`current_bot_route_role=${JSON.stringify(String(currentBotRole || "").trim().toLowerCase())}`,
|
|
1548
|
+
`conversation_mode=${JSON.stringify(String(conversationMode || "").trim())}`,
|
|
1549
|
+
`workspace_dir=${JSON.stringify(String(workspaceDir || "").trim())}`,
|
|
1550
|
+
`managed_bots=${JSON.stringify(bots)}`,
|
|
1551
|
+
`current_assignment_tasks=${JSON.stringify(ensureArray(currentAssignmentTasks).map((item) => String(item || "").trim()).filter(Boolean))}`,
|
|
1552
|
+
`human_message=${JSON.stringify(String(messageText || "").trim())}`,
|
|
1553
|
+
"recent_context=",
|
|
1554
|
+
normalizedContext.length ? normalizedContext.join("\n") : "- none",
|
|
1555
|
+
].join("\n");
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1429
1558
|
function buildDirectReplyGuardrailPrompt({
|
|
1430
1559
|
humanMessageText,
|
|
1431
1560
|
botReplyText,
|
|
@@ -1557,6 +1686,59 @@ export function auditDirectHumanReplyWithAI({
|
|
|
1557
1686
|
};
|
|
1558
1687
|
}
|
|
1559
1688
|
|
|
1689
|
+
export function planRoleExecutionWithAI({
|
|
1690
|
+
messageText,
|
|
1691
|
+
currentBotUsername,
|
|
1692
|
+
currentBotRole = "",
|
|
1693
|
+
managedBots,
|
|
1694
|
+
workspaceDir,
|
|
1695
|
+
currentAssignmentTasks = [],
|
|
1696
|
+
contextComments = [],
|
|
1697
|
+
conversationMode = "",
|
|
1698
|
+
client = "",
|
|
1699
|
+
model = "",
|
|
1700
|
+
env = process.env,
|
|
1701
|
+
}) {
|
|
1702
|
+
const bots = ensureArray(managedBots);
|
|
1703
|
+
if (!bots.length) {
|
|
1704
|
+
return null;
|
|
1705
|
+
}
|
|
1706
|
+
const plannerClient = normalizeLocalAIClientName(
|
|
1707
|
+
String(client || env?.METHEUS_ROLE_PLANNER_CLIENT || env?.METHEUS_INTENT_PARSER_CLIENT || "").trim(),
|
|
1708
|
+
"gpt",
|
|
1709
|
+
);
|
|
1710
|
+
const plannerModel = String(
|
|
1711
|
+
model
|
|
1712
|
+
|| env?.METHEUS_ROLE_PLANNER_MODEL
|
|
1713
|
+
|| env?.METHEUS_INTENT_PARSER_MODEL
|
|
1714
|
+
|| suggestLocalAIModelDisplayName(plannerClient, "")
|
|
1715
|
+
|| "",
|
|
1716
|
+
).trim();
|
|
1717
|
+
const rawText = runLocalAIPromptRawText({
|
|
1718
|
+
client: plannerClient,
|
|
1719
|
+
promptText: buildRoleExecutionPlanPrompt({
|
|
1720
|
+
messageText,
|
|
1721
|
+
currentBotUsername,
|
|
1722
|
+
currentBotRole,
|
|
1723
|
+
managedBots: bots,
|
|
1724
|
+
workspaceDir,
|
|
1725
|
+
currentAssignmentTasks,
|
|
1726
|
+
contextComments,
|
|
1727
|
+
conversationMode,
|
|
1728
|
+
}),
|
|
1729
|
+
workspaceDir,
|
|
1730
|
+
model: plannerModel,
|
|
1731
|
+
permissionMode: "read_only",
|
|
1732
|
+
reasoningEffort: String(env?.METHEUS_ROLE_PLANNER_REASONING_EFFORT || "medium").trim() || "medium",
|
|
1733
|
+
env,
|
|
1734
|
+
});
|
|
1735
|
+
const parsed = tryJsonParse(rawText) || tryParseEmbeddedJsonObject(rawText);
|
|
1736
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1737
|
+
throw new Error("role execution planner did not return a JSON object");
|
|
1738
|
+
}
|
|
1739
|
+
return normalizeRoleExecutionPlan(parsed);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1560
1742
|
export function runLocalAIClient({
|
|
1561
1743
|
client,
|
|
1562
1744
|
inputPayload,
|
package/lib/runner-execution.mjs
CHANGED
|
@@ -241,6 +241,23 @@ export function resolveRunnerExecutionPlan(route, bot, config, deps) {
|
|
|
241
241
|
throw new Error(detail.join("; "));
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
export function resolveRunnerExecutionPlanForRole(route, bot, roleName, config, deps) {
|
|
245
|
+
const normalizedRoleName = String(roleName || "").trim().toLowerCase();
|
|
246
|
+
if (!normalizedRoleName) {
|
|
247
|
+
throw new Error("roleName is required to resolve a role execution plan");
|
|
248
|
+
}
|
|
249
|
+
return resolveRunnerExecutionPlan(
|
|
250
|
+
{
|
|
251
|
+
...safeObject(route),
|
|
252
|
+
role: normalizedRoleName,
|
|
253
|
+
roleProfile: normalizedRoleName,
|
|
254
|
+
},
|
|
255
|
+
bot,
|
|
256
|
+
config,
|
|
257
|
+
deps,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
244
261
|
function buildRunnerExecutionEnv({ route, destination, executionPlan }) {
|
|
245
262
|
return {
|
|
246
263
|
...process.env,
|
|
@@ -431,6 +431,406 @@ function buildDirectHumanResponseContract({
|
|
|
431
431
|
allowedContractTypes,
|
|
432
432
|
};
|
|
433
433
|
}
|
|
434
|
+
|
|
435
|
+
function extractCurrentAssignmentTasks(conversationContext, currentBotSelector) {
|
|
436
|
+
const currentSelector = normalizeMentionSelector(currentBotSelector);
|
|
437
|
+
if (!currentSelector) return [];
|
|
438
|
+
return ensureArray(safeObject(conversationContext?.executionContract).assignments)
|
|
439
|
+
.map((item) => safeObject(item))
|
|
440
|
+
.filter((item) => normalizeMentionSelector(item.targetBot || item.target_bot) === currentSelector)
|
|
441
|
+
.map((item) => String(item.task || item.instruction || "").trim())
|
|
442
|
+
.filter(Boolean);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function requiresArtifactsForExecutionStep(step) {
|
|
446
|
+
const role = String(safeObject(step).role || "").trim().toLowerCase();
|
|
447
|
+
return safeObject(step).artifactsRequired === true || role === "worker";
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function summarizeExecutedRolePlan(plan, stepResults) {
|
|
451
|
+
return {
|
|
452
|
+
requires_execution: safeObject(plan).requiresExecution === true,
|
|
453
|
+
summary_role: String(safeObject(plan).summaryRole || "").trim(),
|
|
454
|
+
steps: ensureArray(safeObject(plan).steps).map((item, index) => ({
|
|
455
|
+
index: index + 1,
|
|
456
|
+
role: String(safeObject(item).role || "").trim(),
|
|
457
|
+
goal: String(safeObject(item).goal || "").trim(),
|
|
458
|
+
artifacts_required: safeObject(item).artifactsRequired === true,
|
|
459
|
+
})),
|
|
460
|
+
step_results: ensureArray(stepResults).map((item) => ({
|
|
461
|
+
role: String(safeObject(item).role || "").trim(),
|
|
462
|
+
goal: String(safeObject(item).goal || "").trim(),
|
|
463
|
+
artifact_paths: ensureArray(safeObject(item).artifactPaths),
|
|
464
|
+
artifact_validation: String(safeObject(item).artifactValidation || "").trim(),
|
|
465
|
+
reply_chars: intFromRawAllowZero(String(safeObject(item).reply || "").length, 0),
|
|
466
|
+
})),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function buildRoleExecutionStepPayload({
|
|
471
|
+
aiPayload,
|
|
472
|
+
step,
|
|
473
|
+
stepIndex,
|
|
474
|
+
stepTotal,
|
|
475
|
+
stepExecutionPlan,
|
|
476
|
+
executedPlan,
|
|
477
|
+
priorStepResults,
|
|
478
|
+
producedArtifacts,
|
|
479
|
+
}) {
|
|
480
|
+
return {
|
|
481
|
+
...aiPayload,
|
|
482
|
+
task: "execute_project_role_step",
|
|
483
|
+
role: String(step.role || "").trim(),
|
|
484
|
+
requested_role: String(step.role || "").trim(),
|
|
485
|
+
role_profile: String(stepExecutionPlan.roleProfileName || step.role || "").trim(),
|
|
486
|
+
execution: {
|
|
487
|
+
mode: String(stepExecutionPlan.mode || "").trim(),
|
|
488
|
+
role_profile: String(stepExecutionPlan.roleProfileName || step.role || "").trim(),
|
|
489
|
+
client: String(stepExecutionPlan.roleProfile?.client || "").trim(),
|
|
490
|
+
model: String(stepExecutionPlan.roleProfile?.model || "").trim(),
|
|
491
|
+
permission_mode: String(stepExecutionPlan.roleProfile?.permissionMode || "").trim(),
|
|
492
|
+
reasoning_effort: String(stepExecutionPlan.roleProfile?.reasoningEffort || "").trim(),
|
|
493
|
+
workspace_dir: String(stepExecutionPlan.workspaceDir || "").trim(),
|
|
494
|
+
workspace_source: String(stepExecutionPlan.workspaceSource || "").trim(),
|
|
495
|
+
command_fallback: stepExecutionPlan.usedCommandFallback === true,
|
|
496
|
+
dry_run_delivery: Boolean(safeObject(aiPayload.execution).dry_run_delivery),
|
|
497
|
+
},
|
|
498
|
+
execution_plan: {
|
|
499
|
+
requires_execution: safeObject(executedPlan).requiresExecution === true,
|
|
500
|
+
summary_role: String(safeObject(executedPlan).summaryRole || "").trim(),
|
|
501
|
+
steps: ensureArray(safeObject(executedPlan).steps).map((item) => ({
|
|
502
|
+
role: String(safeObject(item).role || "").trim(),
|
|
503
|
+
goal: String(safeObject(item).goal || "").trim(),
|
|
504
|
+
artifacts_required: safeObject(item).artifactsRequired === true,
|
|
505
|
+
})),
|
|
506
|
+
reason: String(safeObject(executedPlan).reason || "").trim(),
|
|
507
|
+
},
|
|
508
|
+
execution_step: {
|
|
509
|
+
index: stepIndex,
|
|
510
|
+
total: stepTotal,
|
|
511
|
+
role: String(step.role || "").trim(),
|
|
512
|
+
goal: String(step.goal || "").trim(),
|
|
513
|
+
artifacts_required: safeObject(step).artifactsRequired === true,
|
|
514
|
+
is_final_step: stepIndex === stepTotal,
|
|
515
|
+
},
|
|
516
|
+
execution_progress: {
|
|
517
|
+
completed_steps: ensureArray(priorStepResults).map((item) => ({
|
|
518
|
+
role: String(safeObject(item).role || "").trim(),
|
|
519
|
+
goal: String(safeObject(item).goal || "").trim(),
|
|
520
|
+
reply: String(safeObject(item).reply || "").trim(),
|
|
521
|
+
artifact_paths: ensureArray(safeObject(item).artifactPaths),
|
|
522
|
+
})),
|
|
523
|
+
produced_artifacts: ensureArray(producedArtifacts).map((item) => ({
|
|
524
|
+
path: String(safeObject(item).path || "").trim(),
|
|
525
|
+
relative_path: String(safeObject(item).relativePath || "").trim(),
|
|
526
|
+
kind: String(safeObject(item).kind || "").trim(),
|
|
527
|
+
operation: String(safeObject(item).operation || "").trim(),
|
|
528
|
+
})),
|
|
529
|
+
},
|
|
530
|
+
response_contract: {
|
|
531
|
+
...safeObject(aiPayload.response_contract),
|
|
532
|
+
allow_skip: false,
|
|
533
|
+
must_reply: true,
|
|
534
|
+
require_actionable_contract: true,
|
|
535
|
+
allowed_contract_types: ["direct_result", "summary_request", "final_summary"],
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function maybeExecuteDynamicRolePlan({
|
|
541
|
+
aiPayload,
|
|
542
|
+
routeKey,
|
|
543
|
+
normalizedRoute,
|
|
544
|
+
routeState,
|
|
545
|
+
selectedRecord,
|
|
546
|
+
pendingOrdered,
|
|
547
|
+
bot,
|
|
548
|
+
destination,
|
|
549
|
+
archiveThread,
|
|
550
|
+
executionPlan,
|
|
551
|
+
runtime,
|
|
552
|
+
executionDeps,
|
|
553
|
+
triggerDecision,
|
|
554
|
+
humanIntentContext,
|
|
555
|
+
conversationContext,
|
|
556
|
+
saveRunnerRouteState,
|
|
557
|
+
runRunnerAIExecution,
|
|
558
|
+
validateWorkspaceArtifacts,
|
|
559
|
+
}) {
|
|
560
|
+
const planner = typeof executionDeps.planRoleExecutionWithAI === "function"
|
|
561
|
+
? executionDeps.planRoleExecutionWithAI
|
|
562
|
+
: null;
|
|
563
|
+
const resolveRunnerExecutionPlanForRole = typeof executionDeps.resolveRunnerExecutionPlanForRole === "function"
|
|
564
|
+
? executionDeps.resolveRunnerExecutionPlanForRole
|
|
565
|
+
: null;
|
|
566
|
+
const loadBotRunnerConfig = typeof executionDeps.loadBotRunnerConfig === "function"
|
|
567
|
+
? executionDeps.loadBotRunnerConfig
|
|
568
|
+
: null;
|
|
569
|
+
if (!planner || !resolveRunnerExecutionPlanForRole) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
const currentBotSelector = normalizeMentionSelector(bot?.username || bot?.name);
|
|
573
|
+
const assignmentTasks = extractCurrentAssignmentTasks(conversationContext, currentBotSelector);
|
|
574
|
+
const shouldPlanExecution = triggerDecision.requiresDirectReply === true || assignmentTasks.length > 0;
|
|
575
|
+
if (!shouldPlanExecution) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
const peerMap = humanIntentContext?.peerMap instanceof Map
|
|
579
|
+
? humanIntentContext.peerMap
|
|
580
|
+
: buildConversationPeerMap(bot, normalizedRoute, executionDeps);
|
|
581
|
+
const managedBots = buildConversationParticipantViews(Array.from(peerMap.keys()), peerMap);
|
|
582
|
+
let executedPlan;
|
|
583
|
+
try {
|
|
584
|
+
executedPlan = await planner({
|
|
585
|
+
messageText: String(safeObject(selectedRecord?.parsedArchive).body || "").trim(),
|
|
586
|
+
currentBotUsername: String(bot?.username || bot?.name || "").trim(),
|
|
587
|
+
currentBotRole: String(executionPlan.roleProfileName || normalizedRoute.role || "").trim(),
|
|
588
|
+
managedBots,
|
|
589
|
+
workspaceDir: String(executionPlan.workspaceDir || process.cwd()).trim() || process.cwd(),
|
|
590
|
+
currentAssignmentTasks: assignmentTasks,
|
|
591
|
+
contextComments: buildRunnerContextWindow(pendingOrdered, selectedRecord, normalizedRoute.contextComments),
|
|
592
|
+
conversationMode: String(conversationContext?.mode || "").trim(),
|
|
593
|
+
client: String(executionPlan.roleProfile?.client || "").trim(),
|
|
594
|
+
model: String(executionPlan.roleProfile?.model || "").trim(),
|
|
595
|
+
});
|
|
596
|
+
} catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
if (!safeObject(executedPlan).requiresExecution) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
const plannedSteps = ensureArray(safeObject(executedPlan).steps).filter((item) => String(safeObject(item).role || "").trim());
|
|
603
|
+
if (!plannedSteps.length) {
|
|
604
|
+
const reason = "execution planner required execution but did not return any executable steps";
|
|
605
|
+
saveRunnerRouteState(
|
|
606
|
+
routeKey,
|
|
607
|
+
buildRunnerRouteStateFromComment(selectedRecord, {
|
|
608
|
+
last_action: "execution_failed",
|
|
609
|
+
last_reason: reason,
|
|
610
|
+
last_conversation_id: String(conversationContext?.id || "").trim(),
|
|
611
|
+
last_conversation_stage: String(conversationContext?.stage || "").trim(),
|
|
612
|
+
last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
|
|
613
|
+
}),
|
|
614
|
+
);
|
|
615
|
+
return {
|
|
616
|
+
kind: "error",
|
|
617
|
+
result: {
|
|
618
|
+
route_key: routeKey,
|
|
619
|
+
route_name: normalizedRoute.name,
|
|
620
|
+
outcome: "execution_failed",
|
|
621
|
+
detail: reason,
|
|
622
|
+
thread_id: archiveThread.threadID,
|
|
623
|
+
comment_id: selectedRecord.id,
|
|
624
|
+
trigger_kind: String(triggerDecision.trigger || "").trim(),
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const config = loadBotRunnerConfig ? loadBotRunnerConfig() : { roleProfiles: {}, botBindings: {}, routes: [], projectMappings: {} };
|
|
629
|
+
const stepResults = [];
|
|
630
|
+
const mergedArtifacts = [];
|
|
631
|
+
const mergedArtifactPaths = new Set();
|
|
632
|
+
const mergedBoundaryViolations = [];
|
|
633
|
+
const totalSteps = plannedSteps.length;
|
|
634
|
+
for (const [index, rawStep] of plannedSteps.entries()) {
|
|
635
|
+
const step = safeObject(rawStep);
|
|
636
|
+
let stepExecutionPlan;
|
|
637
|
+
try {
|
|
638
|
+
stepExecutionPlan = resolveRunnerExecutionPlanForRole(
|
|
639
|
+
normalizedRoute,
|
|
640
|
+
bot,
|
|
641
|
+
String(step.role || "").trim(),
|
|
642
|
+
config,
|
|
643
|
+
executionDeps,
|
|
644
|
+
);
|
|
645
|
+
} catch (err) {
|
|
646
|
+
const reason = `failed to resolve execution step "${String(step.role || "").trim()}": ${String(err?.message || err)}`;
|
|
647
|
+
saveRunnerRouteState(
|
|
648
|
+
routeKey,
|
|
649
|
+
buildRunnerRouteStateFromComment(selectedRecord, {
|
|
650
|
+
last_action: "execution_failed",
|
|
651
|
+
last_reason: reason,
|
|
652
|
+
last_conversation_id: String(conversationContext?.id || "").trim(),
|
|
653
|
+
last_conversation_stage: String(conversationContext?.stage || "").trim(),
|
|
654
|
+
last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
|
|
655
|
+
}),
|
|
656
|
+
);
|
|
657
|
+
return {
|
|
658
|
+
kind: "error",
|
|
659
|
+
result: {
|
|
660
|
+
route_key: routeKey,
|
|
661
|
+
route_name: normalizedRoute.name,
|
|
662
|
+
outcome: "execution_failed",
|
|
663
|
+
detail: reason,
|
|
664
|
+
thread_id: archiveThread.threadID,
|
|
665
|
+
comment_id: selectedRecord.id,
|
|
666
|
+
trigger_kind: String(triggerDecision.trigger || "").trim(),
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const stepPayload = buildRoleExecutionStepPayload({
|
|
671
|
+
aiPayload,
|
|
672
|
+
step,
|
|
673
|
+
stepIndex: index + 1,
|
|
674
|
+
stepTotal: totalSteps,
|
|
675
|
+
stepExecutionPlan,
|
|
676
|
+
executedPlan,
|
|
677
|
+
priorStepResults: stepResults,
|
|
678
|
+
producedArtifacts: mergedArtifacts,
|
|
679
|
+
});
|
|
680
|
+
const stepResult = await runRunnerAIExecution({
|
|
681
|
+
inputPayload: stepPayload,
|
|
682
|
+
route: {
|
|
683
|
+
...normalizedRoute,
|
|
684
|
+
role: String(step.role || "").trim(),
|
|
685
|
+
roleProfile: String(stepExecutionPlan.roleProfileName || step.role || "").trim(),
|
|
686
|
+
},
|
|
687
|
+
destination,
|
|
688
|
+
executionPlan: stepExecutionPlan,
|
|
689
|
+
deps: executionDeps,
|
|
690
|
+
});
|
|
691
|
+
if (stepResult.skip) {
|
|
692
|
+
const reason = `${String(step.role || "").trim()} step skipped: ${String(stepResult.reason || "execution step returned skip").trim() || "execution step returned skip"}`;
|
|
693
|
+
saveRunnerRouteState(
|
|
694
|
+
routeKey,
|
|
695
|
+
buildRunnerRouteStateFromComment(selectedRecord, {
|
|
696
|
+
last_action: "execution_failed",
|
|
697
|
+
last_reason: reason,
|
|
698
|
+
last_conversation_id: String(conversationContext?.id || "").trim(),
|
|
699
|
+
last_conversation_stage: String(conversationContext?.stage || "").trim(),
|
|
700
|
+
last_workspace_dir: String(stepExecutionPlan.workspaceDir || executionPlan.workspaceDir || "").trim(),
|
|
701
|
+
}),
|
|
702
|
+
);
|
|
703
|
+
return {
|
|
704
|
+
kind: "error",
|
|
705
|
+
result: {
|
|
706
|
+
route_key: routeKey,
|
|
707
|
+
route_name: normalizedRoute.name,
|
|
708
|
+
outcome: "execution_failed",
|
|
709
|
+
detail: reason,
|
|
710
|
+
thread_id: archiveThread.threadID,
|
|
711
|
+
comment_id: selectedRecord.id,
|
|
712
|
+
trigger_kind: String(triggerDecision.trigger || "").trim(),
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const stepValidation = validateWorkspaceArtifacts && String(stepExecutionPlan.workspaceDir || "").trim()
|
|
717
|
+
? validateWorkspaceArtifacts(
|
|
718
|
+
ensureArray(stepResult?.artifacts),
|
|
719
|
+
stepExecutionPlan.workspaceDir,
|
|
720
|
+
{
|
|
721
|
+
permissionMode: String(stepExecutionPlan.roleProfile?.permissionMode || "workspace_write").trim() || "workspace_write",
|
|
722
|
+
},
|
|
723
|
+
)
|
|
724
|
+
: {
|
|
725
|
+
ok: true,
|
|
726
|
+
status: "none",
|
|
727
|
+
artifacts: ensureArray(stepResult?.artifacts),
|
|
728
|
+
errors: [],
|
|
729
|
+
};
|
|
730
|
+
const stepBoundaryViolations = normalizeBoundaryViolations(stepResult?.boundaryViolations);
|
|
731
|
+
const stepErrors = [
|
|
732
|
+
...ensureArray(stepValidation.errors).map((item) => String(item || "").trim()).filter(Boolean),
|
|
733
|
+
...stepBoundaryViolations.map((item) => `${item.detail}${item.path ? `: ${item.path}` : ""}`),
|
|
734
|
+
];
|
|
735
|
+
if (requiresArtifactsForExecutionStep(step) && ensureArray(stepValidation.artifacts).length === 0) {
|
|
736
|
+
stepErrors.push(`${String(step.role || "").trim()} step completed without any validated project artifacts`);
|
|
737
|
+
}
|
|
738
|
+
if (stepErrors.length > 0) {
|
|
739
|
+
const reason = stepErrors.join("; ");
|
|
740
|
+
saveRunnerRouteState(
|
|
741
|
+
routeKey,
|
|
742
|
+
buildRunnerRouteStateFromComment(selectedRecord, {
|
|
743
|
+
last_action: "execution_failed",
|
|
744
|
+
last_reason: reason,
|
|
745
|
+
last_conversation_id: String(conversationContext?.id || "").trim(),
|
|
746
|
+
last_conversation_stage: String(conversationContext?.stage || "").trim(),
|
|
747
|
+
last_artifact_validation: String(stepValidation.status || "").trim() || "execution_failed",
|
|
748
|
+
last_artifact_paths: ensureArray(stepValidation.artifacts).map((item) => String(item.path || item.relativePath || "").trim()).filter(Boolean),
|
|
749
|
+
last_artifact_errors: stepErrors,
|
|
750
|
+
last_boundary_violations: stepBoundaryViolations,
|
|
751
|
+
last_workspace_dir: String(stepExecutionPlan.workspaceDir || executionPlan.workspaceDir || "").trim(),
|
|
752
|
+
}),
|
|
753
|
+
);
|
|
754
|
+
return {
|
|
755
|
+
kind: "error",
|
|
756
|
+
result: {
|
|
757
|
+
route_key: routeKey,
|
|
758
|
+
route_name: normalizedRoute.name,
|
|
759
|
+
outcome: "execution_failed",
|
|
760
|
+
detail: reason,
|
|
761
|
+
thread_id: archiveThread.threadID,
|
|
762
|
+
comment_id: selectedRecord.id,
|
|
763
|
+
trigger_kind: String(triggerDecision.trigger || "").trim(),
|
|
764
|
+
artifact_validation: String(stepValidation.status || "").trim() || "execution_failed",
|
|
765
|
+
artifact_paths: ensureArray(stepValidation.artifacts).map((item) => String(item.path || item.relativePath || "").trim()).filter(Boolean),
|
|
766
|
+
artifact_errors: stepErrors,
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
ensureArray(stepValidation.artifacts).forEach((item) => {
|
|
771
|
+
const artifact = safeObject(item);
|
|
772
|
+
const artifactPath = String(artifact.path || artifact.relativePath || "").trim();
|
|
773
|
+
if (!artifactPath || mergedArtifactPaths.has(artifactPath)) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
mergedArtifactPaths.add(artifactPath);
|
|
777
|
+
mergedArtifacts.push(artifact);
|
|
778
|
+
});
|
|
779
|
+
mergedBoundaryViolations.push(...stepBoundaryViolations);
|
|
780
|
+
stepResults.push({
|
|
781
|
+
role: String(step.role || "").trim(),
|
|
782
|
+
goal: String(step.goal || "").trim(),
|
|
783
|
+
reply: String(stepResult.reply || "").trim(),
|
|
784
|
+
artifactPaths: ensureArray(stepValidation.artifacts).map((item) => String(safeObject(item).relativePath || safeObject(item).path || "").trim()).filter(Boolean),
|
|
785
|
+
artifactValidation: String(stepValidation.status || "").trim() || "none",
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
const finalStepResult = stepResults[stepResults.length - 1] || {};
|
|
789
|
+
const summaryRole = String(safeObject(executedPlan).summaryRole || "").trim().toLowerCase();
|
|
790
|
+
const summaryStepResult = stepResults.findLast
|
|
791
|
+
? stepResults.findLast((item) => String(safeObject(item).role || "").trim().toLowerCase() === summaryRole)
|
|
792
|
+
: [...stepResults].reverse().find((item) => String(safeObject(item).role || "").trim().toLowerCase() === summaryRole);
|
|
793
|
+
const finalReply = String((summaryStepResult || finalStepResult).reply || "").trim();
|
|
794
|
+
const summaryBotSelector = normalizeMentionSelector(conversationContext?.summaryBotUsername || conversationContext?.leadBotUsername);
|
|
795
|
+
const finalContract = conversationContext?.mode === "public_multi_bot" && summaryBotSelector && summaryBotSelector !== currentBotSelector
|
|
796
|
+
? {
|
|
797
|
+
type: "summary_request",
|
|
798
|
+
actionable: true,
|
|
799
|
+
assignments: [],
|
|
800
|
+
summaryBot: summaryBotSelector,
|
|
801
|
+
nextResponders: [summaryBotSelector],
|
|
802
|
+
}
|
|
803
|
+
: {
|
|
804
|
+
type: summaryRole === "approval" ? "final_summary" : "direct_result",
|
|
805
|
+
actionable: true,
|
|
806
|
+
assignments: [],
|
|
807
|
+
summaryBot: summaryBotSelector,
|
|
808
|
+
nextResponders: [],
|
|
809
|
+
};
|
|
810
|
+
return {
|
|
811
|
+
kind: "executed",
|
|
812
|
+
aiResult: {
|
|
813
|
+
skip: false,
|
|
814
|
+
reply: finalReply,
|
|
815
|
+
replyToMessageID: 0,
|
|
816
|
+
artifacts: mergedArtifacts.map((item) => ({
|
|
817
|
+
path: String(safeObject(item).path || safeObject(item).relativePath || "").trim(),
|
|
818
|
+
kind: String(safeObject(item).kind || "").trim(),
|
|
819
|
+
operation: String(safeObject(item).operation || "").trim(),
|
|
820
|
+
})),
|
|
821
|
+
boundaryViolations: mergedBoundaryViolations,
|
|
822
|
+
contract: finalContract,
|
|
823
|
+
executionPlanSummary: summarizeExecutedRolePlan(executedPlan, stepResults),
|
|
824
|
+
},
|
|
825
|
+
artifactValidation: {
|
|
826
|
+
ok: true,
|
|
827
|
+
status: mergedArtifacts.length > 0 ? "validated" : "none",
|
|
828
|
+
artifacts: mergedArtifacts,
|
|
829
|
+
errors: [],
|
|
830
|
+
},
|
|
831
|
+
executedPlan: summarizeExecutedRolePlan(executedPlan, stepResults),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
434
834
|
|
|
435
835
|
function buildConversationPeerMap(bot, normalizedRoute, deps) {
|
|
436
836
|
const peers = typeof deps?.resolveConversationPeerBots === "function"
|
|
@@ -1456,22 +1856,60 @@ export async function processRunnerSelectedRecord({
|
|
|
1456
1856
|
intervalMs: 4000,
|
|
1457
1857
|
deps: runtimeDeps,
|
|
1458
1858
|
});
|
|
1459
|
-
try {
|
|
1460
|
-
await Promise.resolve(typingHeartbeat.ready);
|
|
1461
|
-
} catch {}
|
|
1462
|
-
let aiResult;
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1859
|
+
try {
|
|
1860
|
+
await Promise.resolve(typingHeartbeat.ready);
|
|
1861
|
+
} catch {}
|
|
1862
|
+
let aiResult;
|
|
1863
|
+
let dynamicRoleExecution = null;
|
|
1864
|
+
let dynamicExecutionError = null;
|
|
1865
|
+
let artifactValidationOverride = null;
|
|
1866
|
+
let executedRolePlan = null;
|
|
1867
|
+
try {
|
|
1868
|
+
dynamicRoleExecution = await maybeExecuteDynamicRolePlan({
|
|
1869
|
+
aiPayload,
|
|
1870
|
+
routeKey,
|
|
1871
|
+
normalizedRoute,
|
|
1872
|
+
routeState,
|
|
1873
|
+
selectedRecord,
|
|
1874
|
+
pendingOrdered,
|
|
1875
|
+
bot,
|
|
1876
|
+
destination,
|
|
1877
|
+
archiveThread,
|
|
1878
|
+
executionPlan,
|
|
1879
|
+
runtime,
|
|
1880
|
+
executionDeps,
|
|
1881
|
+
triggerDecision,
|
|
1882
|
+
humanIntentContext,
|
|
1883
|
+
conversationContext,
|
|
1884
|
+
saveRunnerRouteState,
|
|
1885
|
+
runRunnerAIExecution,
|
|
1886
|
+
validateWorkspaceArtifacts,
|
|
1887
|
+
});
|
|
1888
|
+
if (dynamicRoleExecution?.kind === "error") {
|
|
1889
|
+
dynamicExecutionError = dynamicRoleExecution.result;
|
|
1890
|
+
} else if (dynamicRoleExecution?.kind === "executed") {
|
|
1891
|
+
aiResult = dynamicRoleExecution.aiResult;
|
|
1892
|
+
artifactValidationOverride = safeObject(dynamicRoleExecution.artifactValidation);
|
|
1893
|
+
executedRolePlan = safeObject(dynamicRoleExecution.executedPlan);
|
|
1894
|
+
} else {
|
|
1895
|
+
aiResult = await runRunnerAIExecution({
|
|
1896
|
+
inputPayload: aiPayload,
|
|
1897
|
+
route: normalizedRoute,
|
|
1898
|
+
destination,
|
|
1899
|
+
executionPlan,
|
|
1900
|
+
deps: executionDeps,
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
} finally {
|
|
1904
|
+
await typingHeartbeat.stop();
|
|
1905
|
+
}
|
|
1906
|
+
if (dynamicExecutionError) {
|
|
1907
|
+
return {
|
|
1908
|
+
kind: "error",
|
|
1909
|
+
result: dynamicExecutionError,
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1475
1913
|
if (aiResult.skip) {
|
|
1476
1914
|
if (triggerDecision.requiresDirectReply) {
|
|
1477
1915
|
const forcedReplyPayload = {
|
|
@@ -1524,20 +1962,22 @@ export async function processRunnerSelectedRecord({
|
|
|
1524
1962
|
};
|
|
1525
1963
|
}
|
|
1526
1964
|
|
|
1527
|
-
const artifactValidation =
|
|
1528
|
-
?
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1965
|
+
const artifactValidation = artifactValidationOverride && Object.keys(artifactValidationOverride).length > 0
|
|
1966
|
+
? artifactValidationOverride
|
|
1967
|
+
: validateWorkspaceArtifacts && String(executionPlan.workspaceDir || "").trim()
|
|
1968
|
+
? validateWorkspaceArtifacts(
|
|
1969
|
+
ensureArray(aiResult?.artifacts),
|
|
1970
|
+
executionPlan.workspaceDir,
|
|
1971
|
+
{
|
|
1972
|
+
permissionMode: String(executionPlan.roleProfile?.permissionMode || "workspace_write").trim() || "workspace_write",
|
|
1973
|
+
},
|
|
1974
|
+
)
|
|
1975
|
+
: {
|
|
1976
|
+
ok: true,
|
|
1977
|
+
status: "none",
|
|
1978
|
+
artifacts: ensureArray(aiResult?.artifacts),
|
|
1979
|
+
errors: [],
|
|
1980
|
+
};
|
|
1541
1981
|
const boundaryViolations = normalizeBoundaryViolations(aiResult?.boundaryViolations);
|
|
1542
1982
|
const artifactErrors = [
|
|
1543
1983
|
...ensureArray(artifactValidation.errors).map((item) => String(item || "").trim()).filter(Boolean),
|
|
@@ -1931,6 +2371,7 @@ export async function processRunnerSelectedRecord({
|
|
|
1931
2371
|
last_artifact_errors: [],
|
|
1932
2372
|
last_boundary_violations: [],
|
|
1933
2373
|
last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
|
|
2374
|
+
last_execution_plan: executedRolePlan && Object.keys(executedRolePlan).length > 0 ? executedRolePlan : undefined,
|
|
1934
2375
|
}),
|
|
1935
2376
|
...(nextConversationSessions ? { conversation_sessions: nextConversationSessions } : {}),
|
|
1936
2377
|
},
|
|
@@ -1973,9 +2414,10 @@ export async function processRunnerSelectedRecord({
|
|
|
1973
2414
|
reply_chars: String(sanitizedReplyText || "").length,
|
|
1974
2415
|
execution_mode: executionPlan.mode,
|
|
1975
2416
|
role_profile: executionPlan.roleProfileName,
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2417
|
+
executed_role_plan: executedRolePlan && Object.keys(executedRolePlan).length > 0 ? executedRolePlan : undefined,
|
|
2418
|
+
archive_status: deliveryResult.archive?.dry_run
|
|
2419
|
+
? "dry_run"
|
|
2420
|
+
: deliveryResult.archive?.deduped
|
|
1979
2421
|
? "deduped"
|
|
1980
2422
|
: deliveryResult.archive?.ok === false
|
|
1981
2423
|
? "archive_error"
|
|
@@ -2731,6 +2731,355 @@ export async function runSelftestRunnerScenarios(push, deps) {
|
|
|
2731
2731
|
push("single_bot_human_request_guardrail_audit_upgrades_to_actionable", false, String(err?.message || err));
|
|
2732
2732
|
}
|
|
2733
2733
|
|
|
2734
|
+
try {
|
|
2735
|
+
let deliveryCalls = 0;
|
|
2736
|
+
let aiCalls = 0;
|
|
2737
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-multi-step-success-"));
|
|
2738
|
+
const processed = await processRunnerSelectedRecord({
|
|
2739
|
+
routeKey: "single-bot-multi-step-execution-success-key",
|
|
2740
|
+
normalizedRoute: normalizeRunnerRoute({
|
|
2741
|
+
name: "telegram-monitor-single-bot-multi-step-success",
|
|
2742
|
+
project_id: selftestProjectID,
|
|
2743
|
+
provider: "telegram",
|
|
2744
|
+
role: "monitor",
|
|
2745
|
+
role_profile: "monitor",
|
|
2746
|
+
destination_id: "dest-1",
|
|
2747
|
+
destination_label: "Main Room",
|
|
2748
|
+
server_bot_name: "RyoAI_bot",
|
|
2749
|
+
server_bot_id: "bot-lead-1",
|
|
2750
|
+
trigger_policy: {
|
|
2751
|
+
mentions_only: true,
|
|
2752
|
+
direct_messages: true,
|
|
2753
|
+
reply_to_bot_messages: true,
|
|
2754
|
+
},
|
|
2755
|
+
archive_policy: {
|
|
2756
|
+
mirror_replies: true,
|
|
2757
|
+
dedupe_inbound: true,
|
|
2758
|
+
dedupe_outbound: true,
|
|
2759
|
+
skip_bot_messages: true,
|
|
2760
|
+
},
|
|
2761
|
+
dry_run_delivery: true,
|
|
2762
|
+
}),
|
|
2763
|
+
selectedRecord: {
|
|
2764
|
+
id: "comment-single-bot-multi-step-execution-success",
|
|
2765
|
+
createdAt: "2026-03-17T01:00:00.000Z",
|
|
2766
|
+
parsedArchive: {
|
|
2767
|
+
kind: "telegram_message",
|
|
2768
|
+
chatID: "-100123",
|
|
2769
|
+
chatType: "supergroup",
|
|
2770
|
+
senderIsBot: false,
|
|
2771
|
+
body: "@RyoAI_bot create the project README now.",
|
|
2772
|
+
mentionUsernames: ["RyoAI_bot"],
|
|
2773
|
+
messageID: 1301,
|
|
2774
|
+
},
|
|
2775
|
+
},
|
|
2776
|
+
pendingOrdered: [],
|
|
2777
|
+
bot: {
|
|
2778
|
+
id: "bot-lead-1",
|
|
2779
|
+
name: "RyoAI_bot",
|
|
2780
|
+
username: "RyoAI_bot",
|
|
2781
|
+
role: "monitor",
|
|
2782
|
+
provider: "telegram",
|
|
2783
|
+
},
|
|
2784
|
+
destination: {
|
|
2785
|
+
id: "dest-1",
|
|
2786
|
+
label: "Main Room",
|
|
2787
|
+
provider: "telegram",
|
|
2788
|
+
chatID: "-100123",
|
|
2789
|
+
},
|
|
2790
|
+
archiveThread: {
|
|
2791
|
+
threadID: "thread-1",
|
|
2792
|
+
workItemID: "work-item-1",
|
|
2793
|
+
},
|
|
2794
|
+
executionPlan: {
|
|
2795
|
+
mode: "role_profile",
|
|
2796
|
+
roleProfileName: "monitor",
|
|
2797
|
+
roleProfile: {
|
|
2798
|
+
client: "sample",
|
|
2799
|
+
model: "",
|
|
2800
|
+
permissionMode: "read_only",
|
|
2801
|
+
reasoningEffort: "low",
|
|
2802
|
+
},
|
|
2803
|
+
workspaceDir,
|
|
2804
|
+
workspaceSource: "selftest",
|
|
2805
|
+
usedCommandFallback: false,
|
|
2806
|
+
},
|
|
2807
|
+
runtime: {
|
|
2808
|
+
baseURL: "https://example.test",
|
|
2809
|
+
token: "selftest-token",
|
|
2810
|
+
timeoutSeconds: 30,
|
|
2811
|
+
actor: { user_id: "user-1" },
|
|
2812
|
+
},
|
|
2813
|
+
deps: {
|
|
2814
|
+
saveRunnerRouteState: () => {},
|
|
2815
|
+
startRunnerTypingHeartbeat: () => ({ async stop() {} }),
|
|
2816
|
+
runRunnerAIExecution: async ({ inputPayload }) => {
|
|
2817
|
+
aiCalls += 1;
|
|
2818
|
+
if (String(inputPayload?.task || "").trim() !== "execute_project_role_step") {
|
|
2819
|
+
throw new Error(`unexpected task ${String(inputPayload?.task || "(none)")}`);
|
|
2820
|
+
}
|
|
2821
|
+
const stepRole = String(inputPayload?.execution_step?.role || "").trim();
|
|
2822
|
+
if (stepRole === "monitor") {
|
|
2823
|
+
return {
|
|
2824
|
+
skip: false,
|
|
2825
|
+
reply: "Inspected the project requirements and confirmed the target README scope.",
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
if (stepRole === "worker") {
|
|
2829
|
+
const readmePath = path.join(workspaceDir, "README.md");
|
|
2830
|
+
fs.writeFileSync(readmePath, "# README\n", "utf8");
|
|
2831
|
+
return {
|
|
2832
|
+
skip: false,
|
|
2833
|
+
reply: "Created the README document.",
|
|
2834
|
+
artifacts: [{ path: "README.md", kind: "document", operation: "create" }],
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
if (stepRole === "review") {
|
|
2838
|
+
return {
|
|
2839
|
+
skip: false,
|
|
2840
|
+
reply: "Reviewed the README and confirmed it is ready to share.",
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
throw new Error(`unexpected step role ${stepRole}`);
|
|
2844
|
+
},
|
|
2845
|
+
performLocalBotDelivery: async () => {
|
|
2846
|
+
deliveryCalls += 1;
|
|
2847
|
+
return {
|
|
2848
|
+
delivery: { dryRun: true, body: {} },
|
|
2849
|
+
archive: {},
|
|
2850
|
+
};
|
|
2851
|
+
},
|
|
2852
|
+
serializeRunnerTriggerPolicy: (value) => value,
|
|
2853
|
+
serializeRunnerArchivePolicy: (value) => value,
|
|
2854
|
+
buildRunnerExecutionDeps: () => ({
|
|
2855
|
+
validateWorkspaceArtifacts,
|
|
2856
|
+
analyzeHumanConversationIntentWithAI: async () => ({
|
|
2857
|
+
mode: "single_bot",
|
|
2858
|
+
lead_bot: "ryoai_bot",
|
|
2859
|
+
participants: ["ryoai_bot"],
|
|
2860
|
+
initial_responders: ["ryoai_bot"],
|
|
2861
|
+
allowed_responders: ["ryoai_bot"],
|
|
2862
|
+
summary_bot: "",
|
|
2863
|
+
allow_bot_to_bot: false,
|
|
2864
|
+
reply_expectation: "actionable",
|
|
2865
|
+
}),
|
|
2866
|
+
planRoleExecutionWithAI: async () => ({
|
|
2867
|
+
requiresExecution: true,
|
|
2868
|
+
summaryRole: "review",
|
|
2869
|
+
steps: [
|
|
2870
|
+
{ role: "monitor", goal: "Inspect project requirements" },
|
|
2871
|
+
{ role: "worker", goal: "Create README", artifactsRequired: true },
|
|
2872
|
+
{ role: "review", goal: "Review generated README" },
|
|
2873
|
+
],
|
|
2874
|
+
}),
|
|
2875
|
+
resolveRunnerExecutionPlanForRole: (_route, _bot, roleName) => ({
|
|
2876
|
+
mode: "role_profile",
|
|
2877
|
+
roleProfileName: String(roleName || "").trim(),
|
|
2878
|
+
roleProfile: {
|
|
2879
|
+
client: "sample",
|
|
2880
|
+
model: "",
|
|
2881
|
+
permissionMode: String(roleName || "").trim() === "worker" ? "workspace_write" : "read_only",
|
|
2882
|
+
reasoningEffort: String(roleName || "").trim() === "worker" ? "medium" : "low",
|
|
2883
|
+
},
|
|
2884
|
+
workspaceDir,
|
|
2885
|
+
workspaceSource: "selftest",
|
|
2886
|
+
usedCommandFallback: false,
|
|
2887
|
+
}),
|
|
2888
|
+
loadBotRunnerConfig: () => ({
|
|
2889
|
+
roleProfiles: {},
|
|
2890
|
+
botBindings: {},
|
|
2891
|
+
routes: [],
|
|
2892
|
+
projectMappings: {},
|
|
2893
|
+
}),
|
|
2894
|
+
}),
|
|
2895
|
+
buildRunnerDeliveryDeps: () => ({}),
|
|
2896
|
+
buildRunnerRuntimeDeps: () => ({}),
|
|
2897
|
+
resolveConversationPeerBots: () => [
|
|
2898
|
+
{ id: "bot-lead-1", name: "RyoAI_bot" },
|
|
2899
|
+
],
|
|
2900
|
+
},
|
|
2901
|
+
});
|
|
2902
|
+
push(
|
|
2903
|
+
"single_bot_human_execution_request_runs_multi_role_plan",
|
|
2904
|
+
processed.kind === "replied"
|
|
2905
|
+
&& aiCalls === 3
|
|
2906
|
+
&& deliveryCalls === 1
|
|
2907
|
+
&& ensureArray(processed.result?.artifact_paths).includes("README.md")
|
|
2908
|
+
&& ensureArray(processed.result?.executed_role_plan?.steps).length === 3,
|
|
2909
|
+
`kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} delivery_calls=${deliveryCalls} artifacts=${ensureArray(processed.result?.artifact_paths).join(",")} steps=${ensureArray(processed.result?.executed_role_plan?.steps).length}`,
|
|
2910
|
+
);
|
|
2911
|
+
} catch (err) {
|
|
2912
|
+
push("single_bot_human_execution_request_runs_multi_role_plan", false, String(err?.message || err));
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
try {
|
|
2916
|
+
let deliveryCalls = 0;
|
|
2917
|
+
let aiCalls = 0;
|
|
2918
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-multi-step-worker-fail-"));
|
|
2919
|
+
const processed = await processRunnerSelectedRecord({
|
|
2920
|
+
routeKey: "single-bot-multi-step-worker-artifact-required-key",
|
|
2921
|
+
normalizedRoute: normalizeRunnerRoute({
|
|
2922
|
+
name: "telegram-monitor-single-bot-multi-step-worker-fail",
|
|
2923
|
+
project_id: selftestProjectID,
|
|
2924
|
+
provider: "telegram",
|
|
2925
|
+
role: "monitor",
|
|
2926
|
+
role_profile: "monitor",
|
|
2927
|
+
destination_id: "dest-1",
|
|
2928
|
+
destination_label: "Main Room",
|
|
2929
|
+
server_bot_name: "RyoAI_bot",
|
|
2930
|
+
server_bot_id: "bot-lead-1",
|
|
2931
|
+
trigger_policy: {
|
|
2932
|
+
mentions_only: true,
|
|
2933
|
+
direct_messages: true,
|
|
2934
|
+
reply_to_bot_messages: true,
|
|
2935
|
+
},
|
|
2936
|
+
archive_policy: {
|
|
2937
|
+
mirror_replies: true,
|
|
2938
|
+
dedupe_inbound: true,
|
|
2939
|
+
dedupe_outbound: true,
|
|
2940
|
+
skip_bot_messages: true,
|
|
2941
|
+
},
|
|
2942
|
+
dry_run_delivery: true,
|
|
2943
|
+
}),
|
|
2944
|
+
selectedRecord: {
|
|
2945
|
+
id: "comment-single-bot-multi-step-worker-artifact-required",
|
|
2946
|
+
createdAt: "2026-03-17T01:00:01.000Z",
|
|
2947
|
+
parsedArchive: {
|
|
2948
|
+
kind: "telegram_message",
|
|
2949
|
+
chatID: "-100123",
|
|
2950
|
+
chatType: "supergroup",
|
|
2951
|
+
senderIsBot: false,
|
|
2952
|
+
body: "@RyoAI_bot write the README file now.",
|
|
2953
|
+
mentionUsernames: ["RyoAI_bot"],
|
|
2954
|
+
messageID: 1302,
|
|
2955
|
+
},
|
|
2956
|
+
},
|
|
2957
|
+
pendingOrdered: [],
|
|
2958
|
+
bot: {
|
|
2959
|
+
id: "bot-lead-1",
|
|
2960
|
+
name: "RyoAI_bot",
|
|
2961
|
+
username: "RyoAI_bot",
|
|
2962
|
+
role: "monitor",
|
|
2963
|
+
provider: "telegram",
|
|
2964
|
+
},
|
|
2965
|
+
destination: {
|
|
2966
|
+
id: "dest-1",
|
|
2967
|
+
label: "Main Room",
|
|
2968
|
+
provider: "telegram",
|
|
2969
|
+
chatID: "-100123",
|
|
2970
|
+
},
|
|
2971
|
+
archiveThread: {
|
|
2972
|
+
threadID: "thread-1",
|
|
2973
|
+
workItemID: "work-item-1",
|
|
2974
|
+
},
|
|
2975
|
+
executionPlan: {
|
|
2976
|
+
mode: "role_profile",
|
|
2977
|
+
roleProfileName: "monitor",
|
|
2978
|
+
roleProfile: {
|
|
2979
|
+
client: "sample",
|
|
2980
|
+
model: "",
|
|
2981
|
+
permissionMode: "read_only",
|
|
2982
|
+
reasoningEffort: "low",
|
|
2983
|
+
},
|
|
2984
|
+
workspaceDir,
|
|
2985
|
+
workspaceSource: "selftest",
|
|
2986
|
+
usedCommandFallback: false,
|
|
2987
|
+
},
|
|
2988
|
+
runtime: {
|
|
2989
|
+
baseURL: "https://example.test",
|
|
2990
|
+
token: "selftest-token",
|
|
2991
|
+
timeoutSeconds: 30,
|
|
2992
|
+
actor: { user_id: "user-1" },
|
|
2993
|
+
},
|
|
2994
|
+
deps: {
|
|
2995
|
+
saveRunnerRouteState: () => {},
|
|
2996
|
+
startRunnerTypingHeartbeat: () => ({ async stop() {} }),
|
|
2997
|
+
runRunnerAIExecution: async ({ inputPayload }) => {
|
|
2998
|
+
aiCalls += 1;
|
|
2999
|
+
const stepRole = String(inputPayload?.execution_step?.role || "").trim();
|
|
3000
|
+
if (stepRole === "monitor") {
|
|
3001
|
+
return {
|
|
3002
|
+
skip: false,
|
|
3003
|
+
reply: "Inspected the request.",
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
if (stepRole === "worker") {
|
|
3007
|
+
return {
|
|
3008
|
+
skip: false,
|
|
3009
|
+
reply: "Started the README work.",
|
|
3010
|
+
artifacts: [],
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
3013
|
+
throw new Error(`unexpected step role ${stepRole}`);
|
|
3014
|
+
},
|
|
3015
|
+
performLocalBotDelivery: async () => {
|
|
3016
|
+
deliveryCalls += 1;
|
|
3017
|
+
return {
|
|
3018
|
+
delivery: { dryRun: true, body: {} },
|
|
3019
|
+
archive: {},
|
|
3020
|
+
};
|
|
3021
|
+
},
|
|
3022
|
+
serializeRunnerTriggerPolicy: (value) => value,
|
|
3023
|
+
serializeRunnerArchivePolicy: (value) => value,
|
|
3024
|
+
buildRunnerExecutionDeps: () => ({
|
|
3025
|
+
validateWorkspaceArtifacts,
|
|
3026
|
+
analyzeHumanConversationIntentWithAI: async () => ({
|
|
3027
|
+
mode: "single_bot",
|
|
3028
|
+
lead_bot: "ryoai_bot",
|
|
3029
|
+
participants: ["ryoai_bot"],
|
|
3030
|
+
initial_responders: ["ryoai_bot"],
|
|
3031
|
+
allowed_responders: ["ryoai_bot"],
|
|
3032
|
+
summary_bot: "",
|
|
3033
|
+
allow_bot_to_bot: false,
|
|
3034
|
+
reply_expectation: "actionable",
|
|
3035
|
+
}),
|
|
3036
|
+
planRoleExecutionWithAI: async () => ({
|
|
3037
|
+
requiresExecution: true,
|
|
3038
|
+
summaryRole: "monitor",
|
|
3039
|
+
steps: [
|
|
3040
|
+
{ role: "monitor", goal: "Inspect request" },
|
|
3041
|
+
{ role: "worker", goal: "Write README", artifactsRequired: true },
|
|
3042
|
+
],
|
|
3043
|
+
}),
|
|
3044
|
+
resolveRunnerExecutionPlanForRole: (_route, _bot, roleName) => ({
|
|
3045
|
+
mode: "role_profile",
|
|
3046
|
+
roleProfileName: String(roleName || "").trim(),
|
|
3047
|
+
roleProfile: {
|
|
3048
|
+
client: "sample",
|
|
3049
|
+
model: "",
|
|
3050
|
+
permissionMode: String(roleName || "").trim() === "worker" ? "workspace_write" : "read_only",
|
|
3051
|
+
reasoningEffort: "low",
|
|
3052
|
+
},
|
|
3053
|
+
workspaceDir,
|
|
3054
|
+
workspaceSource: "selftest",
|
|
3055
|
+
usedCommandFallback: false,
|
|
3056
|
+
}),
|
|
3057
|
+
loadBotRunnerConfig: () => ({
|
|
3058
|
+
roleProfiles: {},
|
|
3059
|
+
botBindings: {},
|
|
3060
|
+
routes: [],
|
|
3061
|
+
projectMappings: {},
|
|
3062
|
+
}),
|
|
3063
|
+
}),
|
|
3064
|
+
buildRunnerDeliveryDeps: () => ({}),
|
|
3065
|
+
buildRunnerRuntimeDeps: () => ({}),
|
|
3066
|
+
resolveConversationPeerBots: () => [
|
|
3067
|
+
{ id: "bot-lead-1", name: "RyoAI_bot" },
|
|
3068
|
+
],
|
|
3069
|
+
},
|
|
3070
|
+
});
|
|
3071
|
+
push(
|
|
3072
|
+
"single_bot_worker_step_requires_artifacts_in_multi_role_plan",
|
|
3073
|
+
processed.kind === "error"
|
|
3074
|
+
&& String(processed.result?.outcome || "") === "execution_failed"
|
|
3075
|
+
&& deliveryCalls === 0
|
|
3076
|
+
&& /validated project artifacts/i.test(String(processed.result?.detail || "")),
|
|
3077
|
+
`kind=${String(processed.kind || "(none)")} outcome=${String(processed.result?.outcome || "(none)")} ai_calls=${aiCalls} delivery_calls=${deliveryCalls} detail=${String(processed.result?.detail || "(none)")}`,
|
|
3078
|
+
);
|
|
3079
|
+
} catch (err) {
|
|
3080
|
+
push("single_bot_worker_step_requires_artifacts_in_multi_role_plan", false, String(err?.message || err));
|
|
3081
|
+
}
|
|
3082
|
+
|
|
2734
3083
|
try {
|
|
2735
3084
|
let aiCalls = 0;
|
|
2736
3085
|
const processed = await processRunnerSelectedRecord({
|