




























@@ -173,6 +173,7 @@ const QA_SKILL_WORKSHOP_GIF_PROMPT_RE =
173173const QA_SKILL_WORKSHOP_REVIEW_PROMPT_RE = /Review transcript for durable skill updates/i;
174174const QA_RELEASE_AUDIT_PROMPT_RE = /release readiness audit for the small project/i;
175175const QA_TOOL_SEARCH_PROMPT_RE = /tool search qa check/i;
176+const QA_TOOL_SEARCH_FAILURE_PROMPT_RE = /tool search qa failure/i;
176177177178type MockScenarioState = {
178179subagentFanoutPhase: number;
@@ -678,6 +679,69 @@ function extractToolSearchTarget(text: string): string | null {
678679return match?.[1]?.trim() || null;
679680}
680681682+function buildQaToolSearchArgs(targetTool: string, failureMode: boolean): Record<string, unknown> {
683+if (failureMode) {
684+return { __qaFailureMode: "denied-input" };
685+}
686+if (targetTool === "exec") {
687+return { command: "echo runtime-tool-fixture", timeout: 5 };
688+}
689+if (targetTool === "read") {
690+return { path: "QA_KICKOFF_TASK.md" };
691+}
692+if (targetTool === "write") {
693+return { path: "runtime-tool-fixture-write.txt", content: "runtime tool fixture\n" };
694+}
695+if (targetTool === "edit") {
696+return {
697+path: "runtime-tool-fixture-edit.txt",
698+edits: [{ oldText: "before edit\n", newText: "after edit\n" }],
699+};
700+}
701+if (targetTool === "apply_patch") {
702+return {
703+input: [
704+"*** Begin Patch",
705+"*** Add File: runtime-tool-fixture-patch.txt",
706+"+runtime patch",
707+"*** End Patch",
708+"",
709+].join("\n"),
710+};
711+}
712+if (targetTool === "web_search") {
713+return { query: "OpenClaw runtime parity fixed query", count: 1 };
714+}
715+if (targetTool === "web_fetch") {
716+return { url: "https://example.com/", maxChars: 500 };
717+}
718+if (targetTool === "image_generate") {
719+return { prompt: "QA lighthouse runtime parity fixture", filename: "runtime-tool-fixture" };
720+}
721+if (targetTool === "tts") {
722+return { text: "Runtime parity voice fixture." };
723+}
724+if (targetTool === "message") {
725+return { action: "send", message: "runtime parity message fixture" };
726+}
727+if (targetTool === "session_status") {
728+return { sessionKey: "current" };
729+}
730+if (targetTool === "sessions_spawn") {
731+return {
732+task: "Runtime tool fixture subagent: reply exactly RUNTIME-TOOL-FIXTURE.",
733+label: "runtime-tool-fixture",
734+mode: "run",
735+thread: false,
736+runTimeoutSeconds: 30,
737+};
738+}
739+if (targetTool === "memory_recall") {
740+return { query: "runtime parity memory fixture" };
741+}
742+return { marker: "normal" };
743+}
744+681745function isActiveMemorySubagentPrompt(text: string) {
682746return text.includes("You are a memory search agent.");
683747}
@@ -765,19 +829,42 @@ function extractBareToolArg(text: string, name: string) {
765829766830function hasDeclaredTool(body: Record<string, unknown>, name: string) {
767831const tools = Array.isArray(body.tools) ? body.tools : [];
768-return tools.some((tool) => {
769-if (!tool || typeof tool !== "object") {
770-return false;
771-}
772-const record = tool as Record<string, unknown>;
773-if (record.name === name) {
832+const dynamicTools = Array.isArray(body.dynamicTools) ? body.dynamicTools : [];
833+if (
834+[...tools, ...dynamicTools].some((tool) => toolDefinitionMentionsName(tool, name)) ||
835+instructionTextMentionsToolName(extractInstructionsText(body), name)
836+) {
837+return true;
838+}
839+return false;
840+}
841+842+function toolDefinitionMentionsName(value: unknown, name: string, depth = 0): boolean {
843+if (depth > 6 || !value || typeof value !== "object") {
844+return false;
845+}
846+if (Array.isArray(value)) {
847+return value.some((item) => toolDefinitionMentionsName(item, name, depth + 1));
848+}
849+const record = value as Record<string, unknown>;
850+for (const key of ["name", "tool", "functionName"]) {
851+if (record[key] === name) {
774852return true;
775853}
776-const nested = record.function;
777-return Boolean(
778-nested && typeof nested === "object" && (nested as { name?: unknown }).name === name,
779-);
780-});
854+}
855+return Object.values(record).some((item) => toolDefinitionMentionsName(item, name, depth + 1));
856+}
857+858+function instructionTextMentionsToolName(text: string, name: string) {
859+if (!text) {
860+return false;
861+}
862+const escapedName = escapeRegExp(name);
863+return new RegExp(`(^|[^A-Za-z0-9_])${escapedName}([^A-Za-z0-9_]|$)`).test(text);
864+}
865+866+function isQaToolSearchFixture(text: string) {
867+return QA_TOOL_SEARCH_PROMPT_RE.test(text) || QA_TOOL_SEARCH_FAILURE_PROMPT_RE.test(text);
781868}
782869783870function buildExplicitSessionsSpawnArgs(text: string): Record<string, unknown> | null {
@@ -1416,26 +1503,36 @@ async function buildResponsesPayload(
14161503const hasEmptyResponseRetryInstruction = allInputText.includes(QA_EMPTY_RESPONSE_RETRY_NEEDLE);
14171504const canCallSessionsSpawn = hasDeclaredTool(body, "sessions_spawn");
14181505const canCallSessionsYield = hasDeclaredTool(body, "sessions_yield");
1506+const canPlanQaSessionsSpawn =
1507+canCallSessionsSpawn ||
1508+/subagent fanout synthesis check|delegate one bounded qa task|subagent handoff/i.test(prompt);
14191509const buildToolProgressReadEvents = (pattern: RegExp) => {
14201510const toolProgressPrompt = extractLastMatchingUserText(extractAllUserTexts(input), pattern);
14211511return buildToolCallEventsWithArgs("read", {
14221512path: readTargetFromPrompt(toolProgressPrompt || prompt || allInputText),
14231513});
14241514};
1425-if (QA_TOOL_SEARCH_PROMPT_RE.test(allInputText) && !toolOutput) {
1515+if (
1516+(QA_TOOL_SEARCH_PROMPT_RE.test(allInputText) ||
1517+QA_TOOL_SEARCH_FAILURE_PROMPT_RE.test(allInputText)) &&
1518+!toolOutput
1519+) {
14261520const targetTool = extractToolSearchTarget(allInputText);
1521+const plannedArgs = targetTool
1522+ ? buildQaToolSearchArgs(targetTool, QA_TOOL_SEARCH_FAILURE_PROMPT_RE.test(allInputText))
1523+ : {};
14271524if (targetTool && hasDeclaredTool(body, "tool_search_code")) {
14281525return buildToolCallEventsWithArgs("tool_search_code", {
14291526code: [
14301527`const hits = await openclaw.tools.search(${JSON.stringify(targetTool)}, { limit: 1 });`,
14311528"const match = hits.find((tool) => tool.name === " + JSON.stringify(targetTool) + ");",
14321529"if (!match) throw new Error('target tool not found');",
1433-"return await openclaw.tools.call(match.id, { marker: 'code-mode' });",
1530+`return await openclaw.tools.call(match.id, ${JSON.stringify(plannedArgs)});`,
14341531].join("\n"),
14351532});
14361533}
1437-if (targetTool && hasDeclaredTool(body, targetTool)) {
1438-return buildToolCallEventsWithArgs(targetTool, { marker: "normal" });
1534+if (targetTool && (hasDeclaredTool(body, targetTool) || isQaToolSearchFixture(allInputText))) {
1535+return buildToolCallEventsWithArgs(targetTool, plannedArgs);
14391536}
14401537}
14411538if (
@@ -1905,7 +2002,7 @@ async function buildResponsesPayload(
19052002size: "1024x1024",
19062003});
19072004}
1908-if (canCallSessionsSpawn && /subagent fanout synthesis check/i.test(prompt)) {
2005+if (canPlanQaSessionsSpawn && /subagent fanout synthesis check/i.test(prompt)) {
19092006if (!toolOutput && scenarioState.subagentFanoutPhase === 0) {
19102007scenarioState.subagentFanoutPhase = 1;
19112008return buildToolCallEventsWithArgs("sessions_spawn", {
@@ -1924,7 +2021,7 @@ async function buildResponsesPayload(
19242021}
19252022}
19262023const explicitSessionsSpawnArgs = buildExplicitSessionsSpawnArgs(allInputText);
1927-if (canCallSessionsSpawn && explicitSessionsSpawnArgs && !toolOutput) {
2024+if (explicitSessionsSpawnArgs && !toolOutput) {
19282025return buildToolCallEventsWithArgs("sessions_spawn", explicitSessionsSpawnArgs);
19292026}
19302027if (canCallSessionsSpawn && /forked subagent context qa check/i.test(prompt) && !toolOutput) {
@@ -1981,7 +2078,7 @@ async function buildResponsesPayload(
19812078}
19822079}
19832080if (
1984-canCallSessionsSpawn &&
2081+canPlanQaSessionsSpawn &&
19852082(/\bdelegate\b/i.test(prompt) || /subagent handoff/i.test(prompt)) &&
19862083!toolOutput
19872084) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。