test(e2e): stop ancillary LLM calls draining the mock forced-response queue (#4517) (#4519)

This commit is contained in:
Steven Enamakel
2026-07-04 14:34:03 -07:00
committed by GitHub
parent 38624115a6
commit 42ce5c0e97
2 changed files with 49 additions and 10 deletions
+31 -9
View File
@@ -9,6 +9,17 @@ import {
import { buildDynamicCompletion } from "./llm/dynamic.mjs";
import { headerValue, pickProbeText, resolveThreadKey } from "./llm/shared.mjs";
// The scripted `llmForcedResponses` FIFO models the *interactive* agent turn,
// which always advertises tools (the orchestrator's delegate_* tools). Ancillary
// completions that share the endpoint but carry no tools — thread-title/summary
// generation via `chat_with_system` (tools: None), fired fire-and-forget and
// racing the visible turn — must NOT drain the queue, or the scripted responses
// desync and the turn falls through to the dynamic fallback
// (tinyhumansai/openhuman#4517).
function isPrimaryTurn(parsedBody) {
return Array.isArray(parsedBody?.tools) && parsedBody.tools.length > 0;
}
function requestRuleMatches(rule, ctx) {
if (!rule || typeof rule !== "object") return false;
const { url, parsedBody, req } = ctx;
@@ -242,14 +253,22 @@ function handleStreamingCompletion({
if (!Array.isArray(script)) {
// 2. Forced queue: pop the next entry and convert it into a script.
const forced = parseBehaviorJson("llmForcedResponses", []);
if (Array.isArray(forced) && forced.length > 0) {
const next = forced.shift();
setMockBehavior("llmForcedResponses", JSON.stringify(forced));
script = defaultStreamScript({
content: next.content,
toolCalls: next.toolCalls,
});
// Only the primary *interactive* turn drains the queue. The agent
// (orchestrator) always sends a `tools` array; ancillary completions
// that race the turn — notably fire-and-forget thread-title generation
// (`chat_with_system`, tools: None) dispatched on send — carry no tools
// and must NOT consume a scripted response, or the queue desyncs and the
// turn falls through to the dynamic fallback (tinyhumansai/openhuman#4517).
if (isPrimaryTurn(parsedBody)) {
const forced = parseBehaviorJson("llmForcedResponses", []);
if (Array.isArray(forced) && forced.length > 0) {
const next = forced.shift();
setMockBehavior("llmForcedResponses", JSON.stringify(forced));
script = defaultStreamScript({
content: next.content,
toolCalls: next.toolCalls,
});
}
}
}
@@ -620,8 +639,11 @@ export function handleLlmCompletions(ctx) {
}
// 1. Forced queue — replay exact ChatResponse objects in order.
// Only the primary interactive turn drains the queue; ancillary no-tools
// completions (e.g. fire-and-forget thread-title generation racing the
// turn) must not consume a scripted response (tinyhumansai/openhuman#4517).
const forced = parseBehaviorJson("llmForcedResponses", []);
if (Array.isArray(forced) && forced.length > 0) {
if (isPrimaryTurn(parsedBody) && Array.isArray(forced) && forced.length > 0) {
const next = forced.shift();
// Persist the shrunk queue back so subsequent requests advance.
setMockBehavior("llmForcedResponses", JSON.stringify(forced));
+18 -1
View File
@@ -137,9 +137,26 @@ function buildReasoningSummary(prompt, toolText, thread) {
].join(" ");
}
// Orchestration/delegate tools (analyze_image, delegate_*) require a `prompt`
// argument (and carry `citation_requirement`). The dynamic fallback can only
// synthesize simple worker-tool args (q/path/cmd/url), so a fabricated call to a
// delegate tool fails schema validation with a confusing "arguments.prompt is
// required" bubble (tinyhumansai/openhuman#4517). Treat only tools that do NOT
// require `prompt` as fabricatable; otherwise fall through to benign text.
function isSimpleWorkerTool(toolDef) {
const params = toolDef?.function?.parameters || toolDef?.parameters || {};
const required = Array.isArray(params.required) ? params.required : [];
return !required.includes("prompt");
}
function inferToolCalls(prompt, toolDefs = []) {
const lower = String(prompt || "").toLowerCase();
const declared = Array.isArray(toolDefs) ? toolDefs : [];
const declared = (Array.isArray(toolDefs) ? toolDefs : []).filter(
isSimpleWorkerTool,
);
// Only delegate/orchestration tools are available — the fallback can't build a
// valid call, so return benign text rather than a schema-error tool call.
if (declared.length === 0) return [];
const picked = (name, args) => ({
id: createMockId("tool"),
name,