feat: add LoopQuest human-in-the-loop tool node#6592
Conversation
Test key for reviewA dedicated, disposable LoopQuest workspace for validating the node — please feel free to test all paths; I'll delete it after review: Auth is a Bearer API key (the |
There was a problem hiding this comment.
Code Review
This pull request introduces the LoopQuest Human Review tool and its associated API credential configuration, allowing agents to submit outputs for human review and await a verdict. The review feedback focuses on improving robustness and security: validating the presence of the API key, ensuring polling and timeout intervals are positive integers to prevent a potential Denial of Service (DoS) tight loop, wrapping API requests in try/catch blocks to handle transient network errors gracefully, and throwing errors on invalid external API response data to promote fail-fast behavior.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const apiKey = getCredentialParam('loopQuestApiKey', credentialData, nodeData) | ||
| const baseUrl = (getCredentialParam('baseUrl', credentialData, nodeData) || 'https://loopquest.tomphillips.uk').replace( | ||
| /\/+$/, | ||
| '' | ||
| ) |
There was a problem hiding this comment.
Add a check to ensure the apiKey is provided. Throwing a clear error when the API key is missing prevents confusing runtime failures.
| const apiKey = getCredentialParam('loopQuestApiKey', credentialData, nodeData) | |
| const baseUrl = (getCredentialParam('baseUrl', credentialData, nodeData) || 'https://loopquest.tomphillips.uk').replace( | |
| /\/+$/, | |
| '' | |
| ) | |
| const apiKey = getCredentialParam('loopQuestApiKey', credentialData, nodeData) | |
| if (!apiKey) { | |
| throw new Error('LoopQuest API Key is missing. Please configure the LoopQuest API credential.') | |
| } | |
| const baseUrl = (getCredentialParam('baseUrl', credentialData, nodeData) || 'https://loopquest.tomphillips.uk').replace( | |
| /\/+$/, | |
| '' | |
| ) |
References
- When a feature requires a specific configuration (e.g., an API key for a sandboxed environment), it is preferable to throw an error if the configuration is missing rather than silently falling back to a different implementation.
| const timeoutSeconds = Number(nodeData.inputs?.timeoutSeconds) || 3600 | ||
| const maxWaitSeconds = Number(nodeData.inputs?.maxWaitSeconds) || 300 | ||
| const pollSeconds = Number(nodeData.inputs?.pollSeconds) || 5 |
There was a problem hiding this comment.
Ensure that pollSeconds, maxWaitSeconds, and timeoutSeconds are positive integers. If pollSeconds is configured to 0 or a negative number, it will cause a tight loop that blocks the Node.js event loop, leading to a Denial of Service (DoS).
| const timeoutSeconds = Number(nodeData.inputs?.timeoutSeconds) || 3600 | |
| const maxWaitSeconds = Number(nodeData.inputs?.maxWaitSeconds) || 300 | |
| const pollSeconds = Number(nodeData.inputs?.pollSeconds) || 5 | |
| const timeoutSeconds = Math.max(1, Number(nodeData.inputs?.timeoutSeconds) || 3600) | |
| const maxWaitSeconds = Math.max(1, Number(nodeData.inputs?.maxWaitSeconds) || 300) | |
| const pollSeconds = Math.max(1, Number(nodeData.inputs?.pollSeconds) || 5) |
| func: async (input: { content: string; title?: string; claim?: string; source?: string }) => { | ||
| const body = buildTaskBody(input, { | ||
| module: gameModule, | ||
| mode, | ||
| timeoutSeconds, | ||
| onTimeout: 'escalate', | ||
| source: 'flowise' | ||
| }) | ||
| const created = await axios.post(`${baseUrl}/api/v1/tasks`, body, { headers }) | ||
| const taskId = created.data?.id | ||
| if (!taskId) return 'Failed to create the review task.' | ||
| if (mode !== 'gate') return `Review task ${taskId} created (monitor mode) — not waiting for a verdict.` | ||
|
|
||
| const deadline = Date.now() + maxWaitSeconds * 1000 | ||
| while (Date.now() < deadline) { | ||
| await new Promise((r) => setTimeout(r, pollSeconds * 1000)) | ||
| const res = await axios.get(`${baseUrl}/api/v1/tasks/${taskId}`, { headers }) | ||
| const verdict = verdictToString(res.data as TaskStatus) | ||
| if (verdict) return verdict | ||
| } | ||
| return 'No human verdict within the wait window — treat as NOT approved (fail closed).' | ||
| } |
There was a problem hiding this comment.
Wrap the API requests in try/catch blocks. Transient network errors during polling should be caught and ignored so they do not abort the entire wait window. Also, use handleErrorMessage to return a clear error message if the initial task creation fails.
func: async (input: { content: string; title?: string; claim?: string; source?: string }) => {
const body = buildTaskBody(input, {
module: gameModule,
mode,
timeoutSeconds,
onTimeout: 'escalate',
source: 'flowise'
})
try {
const created = await axios.post(baseUrl + "/api/v1/tasks", body, { headers })
const taskId = created.data?.id
if (!taskId) return "Failed to create the review task."
if (mode !== "gate") return "Review task " + taskId + " created (monitor mode) — not waiting for a verdict."
const deadline = Date.now() + maxWaitSeconds * 1000
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, pollSeconds * 1000))
try {
const res = await axios.get(baseUrl + "/api/v1/tasks/" + taskId, { headers })
const verdict = verdictToString(res.data as TaskStatus)
if (verdict) return verdict
} catch (pollError) {
// Ignore transient network errors during polling to avoid failing the entire gate
}
}
return "No human verdict within the wait window — treat as NOT approved (fail closed)."
} catch (error) {
return "Error in LoopQuest Human Review: " + handleErrorMessage(error)
}
}| import axios from 'axios' | ||
| import { DynamicStructuredTool } from '@langchain/core/tools' | ||
| import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' | ||
| import { getCredentialData, getCredentialParam } from '../../../src/utils' |
There was a problem hiding this comment.
| export function verdictToString(task: TaskStatus): string | null { | ||
| if (task.status === 'reviewed') { |
There was a problem hiding this comment.
When handling potentially invalid data from external sources (like an API response), prefer throwing an error for invalid input types rather than silently returning a default or empty value. This promotes fail-fast behavior.
| export function verdictToString(task: TaskStatus): string | null { | |
| if (task.status === 'reviewed') { | |
| export function verdictToString(task: TaskStatus | null | undefined): string { | |
| if (!task) { | |
| throw new Error('Invalid task data received'); | |
| } | |
| if (task.status === 'reviewed') { |
References
- When handling potentially invalid data from external sources (like an API response), prefer throwing an error for invalid input types rather than silently returning a default or empty value. This promotes fail-fast behavior.
|
Thanks @gemini-code-assist — applied 4 of the 5 in the latest commit:
Skipped one, with reasoning:
|
|
Thanks for the update, @TomPhillipsLabs. The changes to |
New tool node: LoopQuest Human Review
Adds a tool node for LoopQuest — a human-in-the-loop step for agents. Before a consequential action, the agent calls
request_human_review; a person reviews it, and the verdict returns to the agent.How it works
Flowise chatflows can't natively pause, so the tool runs a blocking gate: it creates a review task in
gatemode (POST /api/v1/tasks) and pollsGET /api/v1/tasks/{id}until it resolves (or a fail-closed timeout escalates), then returns a readable verdict string. A Monitor mode creates the review and returns immediately without waiting.Files
packages/components/nodes/tools/LoopQuest/LoopQuest.ts— theINodetool; returns aDynamicStructuredTool(request_human_review).packages/components/nodes/tools/LoopQuest/core.ts— pure helpers (task body + verdict formatting), unit-tested in the source repo.packages/components/nodes/tools/LoopQuest/loopquest.svg— icon.packages/components/credentials/LoopQuestApi.credential.ts— API key (loopQuestApiKey) + optionalbaseUrl.Config
Node inputs: Game (swiper/versus/sorter/detective/fixer/redact/grounding), Mode (gate/monitor), gate timeout, max wait, poll interval. The LLM supplies
content(+title, andclaim/sourcefor grounding).Notes
GET /api/v1/me.pnpm-lock.yamlregenerated for a new credential/node, flag it and I'll follow up.