ruvnet/ruflo · error · Error

at least one candidate is required

Error message

at least one candidate is required

What it means

Thrown by the agenticow_speculate tool handler (agenticow-speculate-tools.ts:146) when the candidates argument is not a non-empty array. Speculative exploration forks the base memory into one branch per candidate, so at least one {label, ingest} candidate must be supplied for explore() to have anything to score and merge back.

Solutions

  1. Pass at least one candidate: candidates: [{ label: 'exp-a', ingest: [{ vector: [...] }] }]
  2. Wrap a single candidate in an array rather than passing it bare
  3. Skip the call entirely when your candidate generator produced zero entries

Example fix

// before
await callTool('agenticow_speculate', {
  basePath: 'memory/main.rvf',
  candidates: [], // throws: at least one candidate is required
});

// after
await callTool('agenticow_speculate', {
  basePath: 'memory/main.rvf',
  candidates: [{ label: 'exp-a', ingest: [{ vector: [0.1, 0.2] }] }],
});
Defensive patterns

Strategy: validation

Validate before calling

function isCandidateList(v: unknown): v is Array<{ label: string; ingest: unknown[] }> {
  return Array.isArray(v) && v.length > 0 && v.every((c) => c && typeof c.label === 'string' && Array.isArray(c.ingest) && c.ingest.length > 0);
}
if (!isCandidateList(args.candidates)) throw new TypeError('candidates must be a non-empty array of {label, ingest[]}');

Type guard

const isNonEmptyArray = (v: unknown): v is unknown[] => Array.isArray(v) && v.length > 0;

Prevention

When it happens

Trigger: Calling agenticow_speculate with candidates omitted, candidates: [], or candidates as an object instead of an array (e.g. a single candidate passed unwrapped: candidates: {label: 'x', ingest: [...]}).

Common situations: Candidates built dynamically and a filter/map step returned an empty array; unwrapping a list into a single object by mistake; an LLM omitting the array when the plan had zero branches.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/9e8190755ae63357. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/mcp-tools/agenticow-speculate-tools.ts:146

          description: 'ADR-171 fail-closed gate. When true, the top-scored winner is NOT promoted (base stays unchanged) and a provenance-tagged receipt is emitted — score alone cannot graduate work. Use when speculating over TASK outcomes rather than pure memory A/B. Default false (score-only promotion, tagged `unverified`).',
          default: false,
        },
      },
      required: ['basePath', 'candidates'],
    },
    handler: async (input) => {
      const api = await loadAgenticow();
      if (!api) return degradedResult('agenticow-not-found');

      const basePath = resolveMemoryPath(String(input.basePath));
      const dimension = input.dimension as number | undefined;
      const scoreBy = (input.scoreBy as string) === 'count' ? 'count' : 'nearest';
      const k = Number.isInteger(input.k) && (input.k as number) > 0 ? (input.k as number) : 1;
      const probe = Array.isArray(input.probe) ? (input.probe as number[]) : null;

      const rawCandidates = input.candidates as CandidateInput[];
      if (!Array.isArray(rawCandidates) || rawCandidates.length === 0) {
        throw new Error('at least one candidate is required');
      }
      if (scoreBy === 'nearest' && !probe) {
        throw new Error("scoreBy='nearest' requires a probe vector");
      }

      // Build the generic {label, fn} candidates. Each fn ingests into its own
      // branch handle, then (for 'nearest') probes it so we can score.
      // Map validated label → explicit branchPath so the branchPath() resolver
      // below is O(1) instead of re-scanning + re-validating rawCandidates per
      // candidate (explore() calls branchPath once per candidate → was O(n²)).
      const explicitBranchPaths = new Map<string, string>();
      const candidates: SpeculativeCandidate<CandidateOutcome>[] = rawCandidates.map((c) => {
        const label = validateLabel(String(c.label));
        if (!Array.isArray(c.ingest) || c.ingest.length === 0) {
          throw new Error(`candidate ${label} must ingest at least one vector`);
        }
        if (typeof c.branchPath === 'string' && c.branchPath) {
          explicitBranchPaths.set(label, c.branchPath);

View on GitHub (pinned to fa13ee4ad6)