metheus-governance-mcp-cli 0.2.147 → 0.2.148
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 +2 -0
- package/lib/local-ai-adapters.mjs +119 -0
- package/lib/runner-orchestration.mjs +105 -12
- package/lib/selftest-runner-scenarios.mjs +372 -0
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import https from "node:https";
|
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_LOCAL_AI_CLIENT,
|
|
14
14
|
analyzeHumanConversationIntentWithAI,
|
|
15
|
+
auditRoleExecutionPlanWithAI,
|
|
15
16
|
auditDirectHumanReplyWithAI,
|
|
16
17
|
normalizeExecutionArtifacts,
|
|
17
18
|
planRoleExecutionWithAI,
|
|
@@ -3001,6 +3002,7 @@ function buildRunnerDeliveryDeps() {
|
|
|
3001
3002
|
function buildRunnerExecutionDeps() {
|
|
3002
3003
|
return {
|
|
3003
3004
|
analyzeHumanConversationIntentWithAI,
|
|
3005
|
+
auditRoleExecutionPlanWithAI,
|
|
3004
3006
|
auditDirectHumanReplyWithAI,
|
|
3005
3007
|
planRoleExecutionWithAI,
|
|
3006
3008
|
normalizeRunnerRoleProfileName,
|
|
@@ -1513,6 +1513,7 @@ function buildRoleExecutionPlanPrompt({
|
|
|
1513
1513
|
currentAssignmentTasks = [],
|
|
1514
1514
|
contextComments = [],
|
|
1515
1515
|
conversationMode = "",
|
|
1516
|
+
planningGuardrail = null,
|
|
1516
1517
|
}) {
|
|
1517
1518
|
const bots = ensureArray(managedBots).map((item) => {
|
|
1518
1519
|
const bot = safeObject(item);
|
|
@@ -1524,6 +1525,7 @@ function buildRoleExecutionPlanPrompt({
|
|
|
1524
1525
|
const normalizedContext = ensureArray(contextComments)
|
|
1525
1526
|
.slice(0, 10)
|
|
1526
1527
|
.map((item) => formatContextCommentLine(item));
|
|
1528
|
+
const guardrail = safeObject(planningGuardrail);
|
|
1527
1529
|
return [
|
|
1528
1530
|
"You are an execution planner for managed local Telegram bots.",
|
|
1529
1531
|
"Read the human request and recent room context, then decide whether actual project execution is required now.",
|
|
@@ -1543,6 +1545,15 @@ function buildRoleExecutionPlanPrompt({
|
|
|
1543
1545
|
"Return strict JSON only with this shape:",
|
|
1544
1546
|
"{\"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
1547
|
"",
|
|
1548
|
+
guardrail.require_worker_step === true
|
|
1549
|
+
? "Guardrail: the final plan must include a worker step that produces or updates project artifacts now."
|
|
1550
|
+
: "",
|
|
1551
|
+
guardrail.reject_monitor_only_execution === true
|
|
1552
|
+
? "Guardrail: do not return a monitor-only plan for this request."
|
|
1553
|
+
: "",
|
|
1554
|
+
String(guardrail.reason || "").trim()
|
|
1555
|
+
? `Guardrail Reason: ${String(guardrail.reason || "").trim()}`
|
|
1556
|
+
: "",
|
|
1546
1557
|
`current_bot_username=${JSON.stringify(String(currentBotUsername || "").trim().replace(/^@+/, ""))}`,
|
|
1547
1558
|
`current_bot_route_role=${JSON.stringify(String(currentBotRole || "").trim().toLowerCase())}`,
|
|
1548
1559
|
`conversation_mode=${JSON.stringify(String(conversationMode || "").trim())}`,
|
|
@@ -1555,6 +1566,52 @@ function buildRoleExecutionPlanPrompt({
|
|
|
1555
1566
|
].join("\n");
|
|
1556
1567
|
}
|
|
1557
1568
|
|
|
1569
|
+
function buildRoleExecutionPlanAuditPrompt({
|
|
1570
|
+
messageText,
|
|
1571
|
+
currentBotUsername,
|
|
1572
|
+
currentBotRole,
|
|
1573
|
+
managedBots,
|
|
1574
|
+
workspaceDir,
|
|
1575
|
+
currentAssignmentTasks = [],
|
|
1576
|
+
contextComments = [],
|
|
1577
|
+
conversationMode = "",
|
|
1578
|
+
executionPlan = null,
|
|
1579
|
+
}) {
|
|
1580
|
+
const bots = ensureArray(managedBots).map((item) => {
|
|
1581
|
+
const bot = safeObject(item);
|
|
1582
|
+
return {
|
|
1583
|
+
username: String(bot.username || "").trim().replace(/^@+/, ""),
|
|
1584
|
+
display_name: String(bot.display_name || bot.displayName || bot.username || "").trim(),
|
|
1585
|
+
};
|
|
1586
|
+
}).filter((item) => item.username);
|
|
1587
|
+
const normalizedContext = ensureArray(contextComments)
|
|
1588
|
+
.slice(0, 10)
|
|
1589
|
+
.map((item) => formatContextCommentLine(item));
|
|
1590
|
+
return [
|
|
1591
|
+
"You are an execution plan adequacy auditor for managed local Telegram bots.",
|
|
1592
|
+
"Judge by meaning, not by keywords or @mentions alone.",
|
|
1593
|
+
"Decide whether the human request requires actual project execution now.",
|
|
1594
|
+
"Then decide whether the proposed execution plan is sufficient to satisfy that request.",
|
|
1595
|
+
"A plan that only inspects, analyzes, checks status, or says it will plan later does NOT satisfy an execution request that should produce concrete project work now.",
|
|
1596
|
+
"If satisfying the request requires creating/updating/deleting project files now, then requires_worker_step must be true.",
|
|
1597
|
+
"If the request is informational only, requires_execution should be false and requires_worker_step should be false.",
|
|
1598
|
+
"",
|
|
1599
|
+
"Return strict JSON only with this shape:",
|
|
1600
|
+
"{\"requires_execution\":true|false,\"requires_worker_step\":true|false,\"plan_satisfies_request\":true|false,\"reason\":\"short explanation\"}",
|
|
1601
|
+
"",
|
|
1602
|
+
`current_bot_username=${JSON.stringify(String(currentBotUsername || "").trim().replace(/^@+/, ""))}`,
|
|
1603
|
+
`current_bot_route_role=${JSON.stringify(String(currentBotRole || "").trim().toLowerCase())}`,
|
|
1604
|
+
`conversation_mode=${JSON.stringify(String(conversationMode || "").trim())}`,
|
|
1605
|
+
`workspace_dir=${JSON.stringify(String(workspaceDir || "").trim())}`,
|
|
1606
|
+
`managed_bots=${JSON.stringify(bots)}`,
|
|
1607
|
+
`current_assignment_tasks=${JSON.stringify(ensureArray(currentAssignmentTasks).map((item) => String(item || "").trim()).filter(Boolean))}`,
|
|
1608
|
+
`human_message=${JSON.stringify(String(messageText || "").trim())}`,
|
|
1609
|
+
`candidate_execution_plan=${JSON.stringify(safeObject(executionPlan))}`,
|
|
1610
|
+
"recent_context=",
|
|
1611
|
+
normalizedContext.length ? normalizedContext.join("\n") : "- none",
|
|
1612
|
+
].join("\n");
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1558
1615
|
function buildDirectReplyGuardrailPrompt({
|
|
1559
1616
|
humanMessageText,
|
|
1560
1617
|
botReplyText,
|
|
@@ -1695,6 +1752,7 @@ export function planRoleExecutionWithAI({
|
|
|
1695
1752
|
currentAssignmentTasks = [],
|
|
1696
1753
|
contextComments = [],
|
|
1697
1754
|
conversationMode = "",
|
|
1755
|
+
planningGuardrail = null,
|
|
1698
1756
|
client = "",
|
|
1699
1757
|
model = "",
|
|
1700
1758
|
env = process.env,
|
|
@@ -1725,6 +1783,7 @@ export function planRoleExecutionWithAI({
|
|
|
1725
1783
|
currentAssignmentTasks,
|
|
1726
1784
|
contextComments,
|
|
1727
1785
|
conversationMode,
|
|
1786
|
+
planningGuardrail,
|
|
1728
1787
|
}),
|
|
1729
1788
|
workspaceDir,
|
|
1730
1789
|
model: plannerModel,
|
|
@@ -1739,6 +1798,66 @@ export function planRoleExecutionWithAI({
|
|
|
1739
1798
|
return normalizeRoleExecutionPlan(parsed);
|
|
1740
1799
|
}
|
|
1741
1800
|
|
|
1801
|
+
export function auditRoleExecutionPlanWithAI({
|
|
1802
|
+
messageText,
|
|
1803
|
+
currentBotUsername,
|
|
1804
|
+
currentBotRole = "",
|
|
1805
|
+
managedBots,
|
|
1806
|
+
workspaceDir,
|
|
1807
|
+
currentAssignmentTasks = [],
|
|
1808
|
+
contextComments = [],
|
|
1809
|
+
conversationMode = "",
|
|
1810
|
+
executionPlan = null,
|
|
1811
|
+
client = "",
|
|
1812
|
+
model = "",
|
|
1813
|
+
env = process.env,
|
|
1814
|
+
}) {
|
|
1815
|
+
const bots = ensureArray(managedBots);
|
|
1816
|
+
if (!bots.length) {
|
|
1817
|
+
return null;
|
|
1818
|
+
}
|
|
1819
|
+
const plannerClient = normalizeLocalAIClientName(
|
|
1820
|
+
String(client || env?.METHEUS_ROLE_PLANNER_CLIENT || env?.METHEUS_INTENT_PARSER_CLIENT || "").trim(),
|
|
1821
|
+
"gpt",
|
|
1822
|
+
);
|
|
1823
|
+
const plannerModel = String(
|
|
1824
|
+
model
|
|
1825
|
+
|| env?.METHEUS_ROLE_PLANNER_MODEL
|
|
1826
|
+
|| env?.METHEUS_INTENT_PARSER_MODEL
|
|
1827
|
+
|| suggestLocalAIModelDisplayName(plannerClient, "")
|
|
1828
|
+
|| "",
|
|
1829
|
+
).trim();
|
|
1830
|
+
const rawText = runLocalAIPromptRawText({
|
|
1831
|
+
client: plannerClient,
|
|
1832
|
+
promptText: buildRoleExecutionPlanAuditPrompt({
|
|
1833
|
+
messageText,
|
|
1834
|
+
currentBotUsername,
|
|
1835
|
+
currentBotRole,
|
|
1836
|
+
managedBots: bots,
|
|
1837
|
+
workspaceDir,
|
|
1838
|
+
currentAssignmentTasks,
|
|
1839
|
+
contextComments,
|
|
1840
|
+
conversationMode,
|
|
1841
|
+
executionPlan,
|
|
1842
|
+
}),
|
|
1843
|
+
workspaceDir,
|
|
1844
|
+
model: plannerModel,
|
|
1845
|
+
permissionMode: "read_only",
|
|
1846
|
+
reasoningEffort: String(env?.METHEUS_ROLE_PLANNER_REASONING_EFFORT || "medium").trim() || "medium",
|
|
1847
|
+
env,
|
|
1848
|
+
});
|
|
1849
|
+
const parsed = tryJsonParse(rawText) || tryParseEmbeddedJsonObject(rawText);
|
|
1850
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1851
|
+
throw new Error("role execution plan auditor did not return a JSON object");
|
|
1852
|
+
}
|
|
1853
|
+
return {
|
|
1854
|
+
requires_execution: parsed.requires_execution === true || parsed.requiresExecution === true,
|
|
1855
|
+
requires_worker_step: parsed.requires_worker_step === true || parsed.requiresWorkerStep === true,
|
|
1856
|
+
plan_satisfies_request: parsed.plan_satisfies_request === true || parsed.planSatisfiesRequest === true,
|
|
1857
|
+
reason: String(parsed.reason || "").trim(),
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1742
1861
|
export function runLocalAIClient({
|
|
1743
1862
|
client,
|
|
1744
1863
|
inputPayload,
|
|
@@ -447,6 +447,32 @@ function requiresArtifactsForExecutionStep(step) {
|
|
|
447
447
|
return safeObject(step).artifactsRequired === true || role === "worker";
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
+
function executionPlanIncludesWorkerStep(plan) {
|
|
451
|
+
return ensureArray(safeObject(plan).steps).some((item) => requiresArtifactsForExecutionStep(item));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function describeExecutionPlanAdequacyFailure(plan, audit) {
|
|
455
|
+
const normalizedPlan = safeObject(plan);
|
|
456
|
+
const normalizedAudit = safeObject(audit);
|
|
457
|
+
const steps = ensureArray(normalizedPlan.steps).filter((item) => String(safeObject(item).role || "").trim());
|
|
458
|
+
if (normalizedAudit.requires_execution === true && normalizedPlan.requiresExecution !== true) {
|
|
459
|
+
return String(normalizedAudit.reason || "").trim()
|
|
460
|
+
|| "execution planner did not return an executable plan for a request that requires project work";
|
|
461
|
+
}
|
|
462
|
+
if (normalizedPlan.requiresExecution === true && !steps.length) {
|
|
463
|
+
return "execution planner required execution but did not return any executable steps";
|
|
464
|
+
}
|
|
465
|
+
if (normalizedAudit.requires_worker_step === true && !executionPlanIncludesWorkerStep(normalizedPlan)) {
|
|
466
|
+
return String(normalizedAudit.reason || "").trim()
|
|
467
|
+
|| "execution planner returned a monitor-only plan for a request that requires worker execution";
|
|
468
|
+
}
|
|
469
|
+
if (normalizedAudit.plan_satisfies_request === false) {
|
|
470
|
+
return String(normalizedAudit.reason || "").trim()
|
|
471
|
+
|| "execution plan auditor rejected the candidate execution plan";
|
|
472
|
+
}
|
|
473
|
+
return "";
|
|
474
|
+
}
|
|
475
|
+
|
|
450
476
|
function summarizeExecutedRolePlan(plan, stepResults) {
|
|
451
477
|
return {
|
|
452
478
|
requires_execution: safeObject(plan).requiresExecution === true,
|
|
@@ -560,6 +586,9 @@ async function maybeExecuteDynamicRolePlan({
|
|
|
560
586
|
const planner = typeof executionDeps.planRoleExecutionWithAI === "function"
|
|
561
587
|
? executionDeps.planRoleExecutionWithAI
|
|
562
588
|
: null;
|
|
589
|
+
const plannerAuditor = typeof executionDeps.auditRoleExecutionPlanWithAI === "function"
|
|
590
|
+
? executionDeps.auditRoleExecutionPlanWithAI
|
|
591
|
+
: null;
|
|
563
592
|
const resolveRunnerExecutionPlanForRole = typeof executionDeps.resolveRunnerExecutionPlanForRole === "function"
|
|
564
593
|
? executionDeps.resolveRunnerExecutionPlanForRole
|
|
565
594
|
: null;
|
|
@@ -579,23 +608,87 @@ async function maybeExecuteDynamicRolePlan({
|
|
|
579
608
|
? humanIntentContext.peerMap
|
|
580
609
|
: buildConversationPeerMap(bot, normalizedRoute, executionDeps);
|
|
581
610
|
const managedBots = buildConversationParticipantViews(Array.from(peerMap.keys()), peerMap);
|
|
611
|
+
const invokePlanner = async (planningGuardrail = null) => planner({
|
|
612
|
+
messageText: String(safeObject(selectedRecord?.parsedArchive).body || "").trim(),
|
|
613
|
+
currentBotUsername: String(bot?.username || bot?.name || "").trim(),
|
|
614
|
+
currentBotRole: String(executionPlan.roleProfileName || normalizedRoute.role || "").trim(),
|
|
615
|
+
managedBots,
|
|
616
|
+
workspaceDir: String(executionPlan.workspaceDir || process.cwd()).trim() || process.cwd(),
|
|
617
|
+
currentAssignmentTasks: assignmentTasks,
|
|
618
|
+
contextComments: buildRunnerContextWindow(pendingOrdered, selectedRecord, normalizedRoute.contextComments),
|
|
619
|
+
conversationMode: String(conversationContext?.mode || "").trim(),
|
|
620
|
+
planningGuardrail,
|
|
621
|
+
client: String(executionPlan.roleProfile?.client || "").trim(),
|
|
622
|
+
model: String(executionPlan.roleProfile?.model || "").trim(),
|
|
623
|
+
});
|
|
624
|
+
const auditPlannedExecution = async (candidatePlan) => {
|
|
625
|
+
if (!plannerAuditor) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
return await plannerAuditor({
|
|
630
|
+
messageText: String(safeObject(selectedRecord?.parsedArchive).body || "").trim(),
|
|
631
|
+
currentBotUsername: String(bot?.username || bot?.name || "").trim(),
|
|
632
|
+
currentBotRole: String(executionPlan.roleProfileName || normalizedRoute.role || "").trim(),
|
|
633
|
+
managedBots,
|
|
634
|
+
workspaceDir: String(executionPlan.workspaceDir || process.cwd()).trim() || process.cwd(),
|
|
635
|
+
currentAssignmentTasks: assignmentTasks,
|
|
636
|
+
contextComments: buildRunnerContextWindow(pendingOrdered, selectedRecord, normalizedRoute.contextComments),
|
|
637
|
+
conversationMode: String(conversationContext?.mode || "").trim(),
|
|
638
|
+
executionPlan: candidatePlan,
|
|
639
|
+
client: String(executionPlan.roleProfile?.client || "").trim(),
|
|
640
|
+
model: String(executionPlan.roleProfile?.model || "").trim(),
|
|
641
|
+
});
|
|
642
|
+
} catch {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
};
|
|
582
646
|
let executedPlan;
|
|
647
|
+
let executedPlanAudit = null;
|
|
583
648
|
try {
|
|
584
|
-
executedPlan = await
|
|
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
|
-
});
|
|
649
|
+
executedPlan = await invokePlanner();
|
|
596
650
|
} catch {
|
|
597
651
|
return null;
|
|
598
652
|
}
|
|
653
|
+
executedPlanAudit = await auditPlannedExecution(executedPlan);
|
|
654
|
+
let adequacyFailure = describeExecutionPlanAdequacyFailure(executedPlan, executedPlanAudit);
|
|
655
|
+
if (adequacyFailure) {
|
|
656
|
+
try {
|
|
657
|
+
executedPlan = await invokePlanner({
|
|
658
|
+
require_worker_step: safeObject(executedPlanAudit).requires_worker_step === true,
|
|
659
|
+
reject_monitor_only_execution: safeObject(executedPlanAudit).requires_execution === true,
|
|
660
|
+
reason: adequacyFailure,
|
|
661
|
+
});
|
|
662
|
+
executedPlanAudit = await auditPlannedExecution(executedPlan);
|
|
663
|
+
adequacyFailure = describeExecutionPlanAdequacyFailure(executedPlan, executedPlanAudit);
|
|
664
|
+
} catch {
|
|
665
|
+
executedPlan = null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (adequacyFailure) {
|
|
669
|
+
saveRunnerRouteState(
|
|
670
|
+
routeKey,
|
|
671
|
+
buildRunnerRouteStateFromComment(selectedRecord, {
|
|
672
|
+
last_action: "execution_failed",
|
|
673
|
+
last_reason: adequacyFailure,
|
|
674
|
+
last_conversation_id: String(conversationContext?.id || "").trim(),
|
|
675
|
+
last_conversation_stage: String(conversationContext?.stage || "").trim(),
|
|
676
|
+
last_workspace_dir: String(executionPlan.workspaceDir || "").trim(),
|
|
677
|
+
}),
|
|
678
|
+
);
|
|
679
|
+
return {
|
|
680
|
+
kind: "error",
|
|
681
|
+
result: {
|
|
682
|
+
route_key: routeKey,
|
|
683
|
+
route_name: normalizedRoute.name,
|
|
684
|
+
outcome: "execution_failed",
|
|
685
|
+
detail: adequacyFailure,
|
|
686
|
+
thread_id: archiveThread.threadID,
|
|
687
|
+
comment_id: selectedRecord.id,
|
|
688
|
+
trigger_kind: String(triggerDecision.trigger || "").trim(),
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
599
692
|
if (!safeObject(executedPlan).requiresExecution) {
|
|
600
693
|
return null;
|
|
601
694
|
}
|
|
@@ -3080,6 +3080,378 @@ export async function runSelftestRunnerScenarios(push, deps) {
|
|
|
3080
3080
|
push("single_bot_worker_step_requires_artifacts_in_multi_role_plan", false, String(err?.message || err));
|
|
3081
3081
|
}
|
|
3082
3082
|
|
|
3083
|
+
try {
|
|
3084
|
+
let deliveryCalls = 0;
|
|
3085
|
+
let plannerCalls = 0;
|
|
3086
|
+
let auditCalls = 0;
|
|
3087
|
+
let aiCalls = 0;
|
|
3088
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-multi-step-replan-"));
|
|
3089
|
+
const processed = await processRunnerSelectedRecord({
|
|
3090
|
+
routeKey: "single-bot-multi-step-replan-key",
|
|
3091
|
+
normalizedRoute: normalizeRunnerRoute({
|
|
3092
|
+
name: "telegram-monitor-single-bot-multi-step-replan",
|
|
3093
|
+
project_id: selftestProjectID,
|
|
3094
|
+
provider: "telegram",
|
|
3095
|
+
role: "monitor",
|
|
3096
|
+
role_profile: "monitor",
|
|
3097
|
+
destination_id: "dest-1",
|
|
3098
|
+
destination_label: "Main Room",
|
|
3099
|
+
server_bot_name: "RyoAI_bot",
|
|
3100
|
+
server_bot_id: "bot-lead-1",
|
|
3101
|
+
trigger_policy: {
|
|
3102
|
+
mentions_only: true,
|
|
3103
|
+
direct_messages: true,
|
|
3104
|
+
reply_to_bot_messages: true,
|
|
3105
|
+
},
|
|
3106
|
+
archive_policy: {
|
|
3107
|
+
mirror_replies: true,
|
|
3108
|
+
dedupe_inbound: true,
|
|
3109
|
+
dedupe_outbound: true,
|
|
3110
|
+
skip_bot_messages: true,
|
|
3111
|
+
},
|
|
3112
|
+
dry_run_delivery: true,
|
|
3113
|
+
}),
|
|
3114
|
+
selectedRecord: {
|
|
3115
|
+
id: "comment-single-bot-multi-step-replan",
|
|
3116
|
+
createdAt: "2026-03-17T01:00:02.000Z",
|
|
3117
|
+
parsedArchive: {
|
|
3118
|
+
kind: "telegram_message",
|
|
3119
|
+
chatID: "-100123",
|
|
3120
|
+
chatType: "supergroup",
|
|
3121
|
+
senderIsBot: false,
|
|
3122
|
+
body: "@RyoAI_bot redo the project work and create the README file now.",
|
|
3123
|
+
mentionUsernames: ["RyoAI_bot"],
|
|
3124
|
+
messageID: 1303,
|
|
3125
|
+
},
|
|
3126
|
+
},
|
|
3127
|
+
pendingOrdered: [],
|
|
3128
|
+
bot: {
|
|
3129
|
+
id: "bot-lead-1",
|
|
3130
|
+
name: "RyoAI_bot",
|
|
3131
|
+
username: "RyoAI_bot",
|
|
3132
|
+
role: "monitor",
|
|
3133
|
+
provider: "telegram",
|
|
3134
|
+
},
|
|
3135
|
+
destination: {
|
|
3136
|
+
id: "dest-1",
|
|
3137
|
+
label: "Main Room",
|
|
3138
|
+
provider: "telegram",
|
|
3139
|
+
chatID: "-100123",
|
|
3140
|
+
},
|
|
3141
|
+
archiveThread: {
|
|
3142
|
+
threadID: "thread-1",
|
|
3143
|
+
workItemID: "work-item-1",
|
|
3144
|
+
},
|
|
3145
|
+
executionPlan: {
|
|
3146
|
+
mode: "role_profile",
|
|
3147
|
+
roleProfileName: "monitor",
|
|
3148
|
+
roleProfile: {
|
|
3149
|
+
client: "sample",
|
|
3150
|
+
model: "",
|
|
3151
|
+
permissionMode: "read_only",
|
|
3152
|
+
reasoningEffort: "low",
|
|
3153
|
+
},
|
|
3154
|
+
workspaceDir,
|
|
3155
|
+
workspaceSource: "selftest",
|
|
3156
|
+
usedCommandFallback: false,
|
|
3157
|
+
},
|
|
3158
|
+
runtime: {
|
|
3159
|
+
baseURL: "https://example.test",
|
|
3160
|
+
token: "selftest-token",
|
|
3161
|
+
timeoutSeconds: 30,
|
|
3162
|
+
actor: { user_id: "user-1" },
|
|
3163
|
+
},
|
|
3164
|
+
deps: {
|
|
3165
|
+
saveRunnerRouteState: () => {},
|
|
3166
|
+
startRunnerTypingHeartbeat: () => ({ async stop() {} }),
|
|
3167
|
+
runRunnerAIExecution: async ({ inputPayload }) => {
|
|
3168
|
+
aiCalls += 1;
|
|
3169
|
+
const stepRole = String(inputPayload?.execution_step?.role || "").trim();
|
|
3170
|
+
if (stepRole === "monitor") {
|
|
3171
|
+
return {
|
|
3172
|
+
skip: false,
|
|
3173
|
+
reply: "Inspected the project requirements and identified the missing README work.",
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
if (stepRole === "worker") {
|
|
3177
|
+
const readmePath = path.join(workspaceDir, "README.md");
|
|
3178
|
+
fs.writeFileSync(readmePath, "# README\n", "utf8");
|
|
3179
|
+
return {
|
|
3180
|
+
skip: false,
|
|
3181
|
+
reply: "Created the README document.",
|
|
3182
|
+
artifacts: [{ path: "README.md", kind: "document", operation: "create" }],
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
throw new Error(`unexpected step role ${stepRole}`);
|
|
3186
|
+
},
|
|
3187
|
+
performLocalBotDelivery: async () => {
|
|
3188
|
+
deliveryCalls += 1;
|
|
3189
|
+
return {
|
|
3190
|
+
delivery: { dryRun: true, body: {} },
|
|
3191
|
+
archive: {},
|
|
3192
|
+
};
|
|
3193
|
+
},
|
|
3194
|
+
serializeRunnerTriggerPolicy: (value) => value,
|
|
3195
|
+
serializeRunnerArchivePolicy: (value) => value,
|
|
3196
|
+
buildRunnerExecutionDeps: () => ({
|
|
3197
|
+
validateWorkspaceArtifacts,
|
|
3198
|
+
analyzeHumanConversationIntentWithAI: async () => ({
|
|
3199
|
+
mode: "single_bot",
|
|
3200
|
+
lead_bot: "ryoai_bot",
|
|
3201
|
+
participants: ["ryoai_bot"],
|
|
3202
|
+
initial_responders: ["ryoai_bot"],
|
|
3203
|
+
allowed_responders: ["ryoai_bot"],
|
|
3204
|
+
summary_bot: "",
|
|
3205
|
+
allow_bot_to_bot: false,
|
|
3206
|
+
reply_expectation: "actionable",
|
|
3207
|
+
}),
|
|
3208
|
+
planRoleExecutionWithAI: async ({ planningGuardrail } = {}) => {
|
|
3209
|
+
plannerCalls += 1;
|
|
3210
|
+
if (safeObject(planningGuardrail).require_worker_step === true) {
|
|
3211
|
+
return {
|
|
3212
|
+
requiresExecution: true,
|
|
3213
|
+
summaryRole: "worker",
|
|
3214
|
+
steps: [
|
|
3215
|
+
{ role: "monitor", goal: "Inspect the request" },
|
|
3216
|
+
{ role: "worker", goal: "Create the README", artifactsRequired: true },
|
|
3217
|
+
],
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
return {
|
|
3221
|
+
requiresExecution: true,
|
|
3222
|
+
summaryRole: "monitor",
|
|
3223
|
+
steps: [
|
|
3224
|
+
{ role: "monitor", goal: "Inspect the request" },
|
|
3225
|
+
],
|
|
3226
|
+
};
|
|
3227
|
+
},
|
|
3228
|
+
auditRoleExecutionPlanWithAI: async ({ executionPlan }) => {
|
|
3229
|
+
auditCalls += 1;
|
|
3230
|
+
const steps = ensureArray(safeObject(executionPlan).steps).map((item) => String(safeObject(item).role || "").trim().toLowerCase());
|
|
3231
|
+
const hasWorker = steps.includes("worker");
|
|
3232
|
+
return {
|
|
3233
|
+
requires_execution: true,
|
|
3234
|
+
requires_worker_step: true,
|
|
3235
|
+
plan_satisfies_request: hasWorker,
|
|
3236
|
+
reason: hasWorker
|
|
3237
|
+
? "worker step present"
|
|
3238
|
+
: "execution request must include a worker step that creates project artifacts",
|
|
3239
|
+
};
|
|
3240
|
+
},
|
|
3241
|
+
resolveRunnerExecutionPlanForRole: (_route, _bot, roleName) => ({
|
|
3242
|
+
mode: "role_profile",
|
|
3243
|
+
roleProfileName: String(roleName || "").trim(),
|
|
3244
|
+
roleProfile: {
|
|
3245
|
+
client: "sample",
|
|
3246
|
+
model: "",
|
|
3247
|
+
permissionMode: String(roleName || "").trim() === "worker" ? "workspace_write" : "read_only",
|
|
3248
|
+
reasoningEffort: String(roleName || "").trim() === "worker" ? "medium" : "low",
|
|
3249
|
+
},
|
|
3250
|
+
workspaceDir,
|
|
3251
|
+
workspaceSource: "selftest",
|
|
3252
|
+
usedCommandFallback: false,
|
|
3253
|
+
}),
|
|
3254
|
+
loadBotRunnerConfig: () => ({
|
|
3255
|
+
roleProfiles: {},
|
|
3256
|
+
botBindings: {},
|
|
3257
|
+
routes: [],
|
|
3258
|
+
projectMappings: {},
|
|
3259
|
+
}),
|
|
3260
|
+
}),
|
|
3261
|
+
buildRunnerDeliveryDeps: () => ({}),
|
|
3262
|
+
buildRunnerRuntimeDeps: () => ({}),
|
|
3263
|
+
resolveConversationPeerBots: () => [
|
|
3264
|
+
{ id: "bot-lead-1", name: "RyoAI_bot" },
|
|
3265
|
+
],
|
|
3266
|
+
},
|
|
3267
|
+
});
|
|
3268
|
+
push(
|
|
3269
|
+
"single_bot_execution_request_replans_monitor_only_plan_into_worker_plan",
|
|
3270
|
+
processed.kind === "replied"
|
|
3271
|
+
&& plannerCalls === 2
|
|
3272
|
+
&& auditCalls >= 2
|
|
3273
|
+
&& aiCalls === 2
|
|
3274
|
+
&& deliveryCalls === 1
|
|
3275
|
+
&& ensureArray(processed.result?.artifact_paths).includes("README.md"),
|
|
3276
|
+
`kind=${String(processed.kind || "(none)")} planner_calls=${plannerCalls} audit_calls=${auditCalls} ai_calls=${aiCalls} delivery_calls=${deliveryCalls} artifacts=${ensureArray(processed.result?.artifact_paths).join(",")}`,
|
|
3277
|
+
);
|
|
3278
|
+
} catch (err) {
|
|
3279
|
+
push("single_bot_execution_request_replans_monitor_only_plan_into_worker_plan", false, String(err?.message || err));
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
try {
|
|
3283
|
+
let deliveryCalls = 0;
|
|
3284
|
+
let plannerCalls = 0;
|
|
3285
|
+
let auditCalls = 0;
|
|
3286
|
+
let aiCalls = 0;
|
|
3287
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-selftest-multi-step-replan-fail-"));
|
|
3288
|
+
const processed = await processRunnerSelectedRecord({
|
|
3289
|
+
routeKey: "single-bot-multi-step-replan-fail-key",
|
|
3290
|
+
normalizedRoute: normalizeRunnerRoute({
|
|
3291
|
+
name: "telegram-monitor-single-bot-multi-step-replan-fail",
|
|
3292
|
+
project_id: selftestProjectID,
|
|
3293
|
+
provider: "telegram",
|
|
3294
|
+
role: "monitor",
|
|
3295
|
+
role_profile: "monitor",
|
|
3296
|
+
destination_id: "dest-1",
|
|
3297
|
+
destination_label: "Main Room",
|
|
3298
|
+
server_bot_name: "RyoAI_bot",
|
|
3299
|
+
server_bot_id: "bot-lead-1",
|
|
3300
|
+
trigger_policy: {
|
|
3301
|
+
mentions_only: true,
|
|
3302
|
+
direct_messages: true,
|
|
3303
|
+
reply_to_bot_messages: true,
|
|
3304
|
+
},
|
|
3305
|
+
archive_policy: {
|
|
3306
|
+
mirror_replies: true,
|
|
3307
|
+
dedupe_inbound: true,
|
|
3308
|
+
dedupe_outbound: true,
|
|
3309
|
+
skip_bot_messages: true,
|
|
3310
|
+
},
|
|
3311
|
+
dry_run_delivery: true,
|
|
3312
|
+
}),
|
|
3313
|
+
selectedRecord: {
|
|
3314
|
+
id: "comment-single-bot-multi-step-replan-fail",
|
|
3315
|
+
createdAt: "2026-03-17T01:00:03.000Z",
|
|
3316
|
+
parsedArchive: {
|
|
3317
|
+
kind: "telegram_message",
|
|
3318
|
+
chatID: "-100123",
|
|
3319
|
+
chatType: "supergroup",
|
|
3320
|
+
senderIsBot: false,
|
|
3321
|
+
body: "@RyoAI_bot redo the project work and create the README file now.",
|
|
3322
|
+
mentionUsernames: ["RyoAI_bot"],
|
|
3323
|
+
messageID: 1304,
|
|
3324
|
+
},
|
|
3325
|
+
},
|
|
3326
|
+
pendingOrdered: [],
|
|
3327
|
+
bot: {
|
|
3328
|
+
id: "bot-lead-1",
|
|
3329
|
+
name: "RyoAI_bot",
|
|
3330
|
+
username: "RyoAI_bot",
|
|
3331
|
+
role: "monitor",
|
|
3332
|
+
provider: "telegram",
|
|
3333
|
+
},
|
|
3334
|
+
destination: {
|
|
3335
|
+
id: "dest-1",
|
|
3336
|
+
label: "Main Room",
|
|
3337
|
+
provider: "telegram",
|
|
3338
|
+
chatID: "-100123",
|
|
3339
|
+
},
|
|
3340
|
+
archiveThread: {
|
|
3341
|
+
threadID: "thread-1",
|
|
3342
|
+
workItemID: "work-item-1",
|
|
3343
|
+
},
|
|
3344
|
+
executionPlan: {
|
|
3345
|
+
mode: "role_profile",
|
|
3346
|
+
roleProfileName: "monitor",
|
|
3347
|
+
roleProfile: {
|
|
3348
|
+
client: "sample",
|
|
3349
|
+
model: "",
|
|
3350
|
+
permissionMode: "read_only",
|
|
3351
|
+
reasoningEffort: "low",
|
|
3352
|
+
},
|
|
3353
|
+
workspaceDir,
|
|
3354
|
+
workspaceSource: "selftest",
|
|
3355
|
+
usedCommandFallback: false,
|
|
3356
|
+
},
|
|
3357
|
+
runtime: {
|
|
3358
|
+
baseURL: "https://example.test",
|
|
3359
|
+
token: "selftest-token",
|
|
3360
|
+
timeoutSeconds: 30,
|
|
3361
|
+
actor: { user_id: "user-1" },
|
|
3362
|
+
},
|
|
3363
|
+
deps: {
|
|
3364
|
+
saveRunnerRouteState: () => {},
|
|
3365
|
+
startRunnerTypingHeartbeat: () => ({ async stop() {} }),
|
|
3366
|
+
runRunnerAIExecution: async () => {
|
|
3367
|
+
aiCalls += 1;
|
|
3368
|
+
return {
|
|
3369
|
+
skip: false,
|
|
3370
|
+
reply: "This path should not execute because the monitor-only plan must fail first.",
|
|
3371
|
+
};
|
|
3372
|
+
},
|
|
3373
|
+
performLocalBotDelivery: async () => {
|
|
3374
|
+
deliveryCalls += 1;
|
|
3375
|
+
return {
|
|
3376
|
+
delivery: { dryRun: true, body: {} },
|
|
3377
|
+
archive: {},
|
|
3378
|
+
};
|
|
3379
|
+
},
|
|
3380
|
+
serializeRunnerTriggerPolicy: (value) => value,
|
|
3381
|
+
serializeRunnerArchivePolicy: (value) => value,
|
|
3382
|
+
buildRunnerExecutionDeps: () => ({
|
|
3383
|
+
validateWorkspaceArtifacts,
|
|
3384
|
+
analyzeHumanConversationIntentWithAI: async () => ({
|
|
3385
|
+
mode: "single_bot",
|
|
3386
|
+
lead_bot: "ryoai_bot",
|
|
3387
|
+
participants: ["ryoai_bot"],
|
|
3388
|
+
initial_responders: ["ryoai_bot"],
|
|
3389
|
+
allowed_responders: ["ryoai_bot"],
|
|
3390
|
+
summary_bot: "",
|
|
3391
|
+
allow_bot_to_bot: false,
|
|
3392
|
+
reply_expectation: "actionable",
|
|
3393
|
+
}),
|
|
3394
|
+
planRoleExecutionWithAI: async () => {
|
|
3395
|
+
plannerCalls += 1;
|
|
3396
|
+
return {
|
|
3397
|
+
requiresExecution: true,
|
|
3398
|
+
summaryRole: "monitor",
|
|
3399
|
+
steps: [
|
|
3400
|
+
{ role: "monitor", goal: "Inspect the request" },
|
|
3401
|
+
],
|
|
3402
|
+
};
|
|
3403
|
+
},
|
|
3404
|
+
auditRoleExecutionPlanWithAI: async () => {
|
|
3405
|
+
auditCalls += 1;
|
|
3406
|
+
return {
|
|
3407
|
+
requires_execution: true,
|
|
3408
|
+
requires_worker_step: true,
|
|
3409
|
+
plan_satisfies_request: false,
|
|
3410
|
+
reason: "execution request must include a worker step that creates project artifacts",
|
|
3411
|
+
};
|
|
3412
|
+
},
|
|
3413
|
+
resolveRunnerExecutionPlanForRole: (_route, _bot, roleName) => ({
|
|
3414
|
+
mode: "role_profile",
|
|
3415
|
+
roleProfileName: String(roleName || "").trim(),
|
|
3416
|
+
roleProfile: {
|
|
3417
|
+
client: "sample",
|
|
3418
|
+
model: "",
|
|
3419
|
+
permissionMode: "read_only",
|
|
3420
|
+
reasoningEffort: "low",
|
|
3421
|
+
},
|
|
3422
|
+
workspaceDir,
|
|
3423
|
+
workspaceSource: "selftest",
|
|
3424
|
+
usedCommandFallback: false,
|
|
3425
|
+
}),
|
|
3426
|
+
loadBotRunnerConfig: () => ({
|
|
3427
|
+
roleProfiles: {},
|
|
3428
|
+
botBindings: {},
|
|
3429
|
+
routes: [],
|
|
3430
|
+
projectMappings: {},
|
|
3431
|
+
}),
|
|
3432
|
+
}),
|
|
3433
|
+
buildRunnerDeliveryDeps: () => ({}),
|
|
3434
|
+
buildRunnerRuntimeDeps: () => ({}),
|
|
3435
|
+
resolveConversationPeerBots: () => [
|
|
3436
|
+
{ id: "bot-lead-1", name: "RyoAI_bot" },
|
|
3437
|
+
],
|
|
3438
|
+
},
|
|
3439
|
+
});
|
|
3440
|
+
push(
|
|
3441
|
+
"single_bot_execution_request_fails_when_replanned_plan_still_lacks_worker_step",
|
|
3442
|
+
processed.kind === "error"
|
|
3443
|
+
&& String(processed.result?.outcome || "") === "execution_failed"
|
|
3444
|
+
&& plannerCalls === 2
|
|
3445
|
+
&& auditCalls >= 2
|
|
3446
|
+
&& aiCalls === 0
|
|
3447
|
+
&& deliveryCalls === 0
|
|
3448
|
+
&& /worker step/i.test(String(processed.result?.detail || "")),
|
|
3449
|
+
`kind=${String(processed.kind || "(none)")} outcome=${String(processed.result?.outcome || "(none)")} planner_calls=${plannerCalls} audit_calls=${auditCalls} ai_calls=${aiCalls} delivery_calls=${deliveryCalls} detail=${String(processed.result?.detail || "(none)")}`,
|
|
3450
|
+
);
|
|
3451
|
+
} catch (err) {
|
|
3452
|
+
push("single_bot_execution_request_fails_when_replanned_plan_still_lacks_worker_step", false, String(err?.message || err));
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3083
3455
|
try {
|
|
3084
3456
|
let aiCalls = 0;
|
|
3085
3457
|
const processed = await processRunnerSelectedRecord({
|