Skip to content

refine()

Run an iterative refinement loop — the evaluator-optimizer pattern codified as a primitive.

Calls step repeatedly. After each step, until decides whether the result is good enough. until receives both the current state and the previous state, so you can detect stalled progress and bail early rather than burning iterations.

When to use refine vs other primitives

refine()workflow()agent()
ShapeDynamic loop, N iterationsFixed stagesUnbounded tool loop
ControlCode-defined exit conditionCode-defined stagesLLM-decided
Use forIterate until quality thresholdFan-out/fan-in stagesAutonomous reasoning

refine(config)

ts
async function refine<TState, TOutput>(
  config: RefineConfig<TState, TOutput>,
): Promise<RefineResult<TOutput>>

RefineConfig

ts
interface RefineConfig<TState, TOutput> {
  state: TState;
  step: (state: TState, iteration: number) => Promise<TState>;
  until: (
    current: TState,
    previous: TState,
  ) => { done: true; output: TOutput } | { done: false };
  maxIterations?: number; // default: 10
}
FieldDescription
stateInitial state passed to the first step call.
stepWork function. Receives current state and 1-based iteration number. Returns next state.
untilExit condition. previous is the initial state on the first call, the prior state on subsequent calls. Return { done: true, output } to stop.
maxIterationsHard ceiling. Throws RefineLimitError if reached. Default 10.

RefineResult

ts
interface RefineResult<TOutput> {
  output: TOutput;
  iterations: number;
}

Examples

TDD — red / green / refactor

ts
import { refine, skill } from '@daedalus-ai-dev/ai-sdk';
import { z } from 'zod';

const implementer = skill({
  instructions: 'Write TypeScript code that satisfies the spec. Fix the errors if any are provided.',
  output: z.object({ code: z.string() }),
});

const testRunner = skill({
  instructions: 'Run the tests for the provided code. Return any errors.',
  output: z.object({ errors: z.array(z.string()) }),
});

const { output: code, iterations } = await refine({
  state: { spec: 'Write a function that adds two numbers.', code: '', errors: [] as string[] },
  step: async (s) => {
    const { code }   = (await implementer.invoke({ spec: s.spec, errors: s.errors })).structured;
    const { errors } = (await testRunner.invoke(code)).structured;
    return { ...s, code, errors };
  },
  until: (curr, prev) => {
    if (curr.errors.length === 0) return { done: true, output: curr.code };
    if (curr.code === prev.code)  return { done: true, output: curr.code }; // no progress
    return { done: false };
  },
  maxIterations: 5,
});

console.log(`Done in ${iterations} iteration(s).`);

BDD three-amigos — loop until consensus

ts
const { output: criteria } = await refine({
  state: {
    story: 'As a user I want to reset my password...',
    questions: [] as string[],
    consensus: false,
    criteria: '',
  },
  step: async (s) => {
    const [devFeedback, qaFeedback] = await Promise.all([
      dev.invoke(s).then(r => r.structured),
      qa.invoke(s).then(r => r.structured),
    ]);
    // PO answers questions and signals consensus when ready
    return po.invoke({ ...s, devFeedback, qaFeedback }).then(r => r.structured);
  },
  until: (curr, prev) => {
    if (curr.consensus) return { done: true, output: curr.criteria };
    // No new questions raised — stuck, bail out with what we have
    if (JSON.stringify(curr.questions) === JSON.stringify(prev.questions)) {
      return { done: true, output: curr.criteria };
    }
    return { done: false };
  },
  maxIterations: 6,
});

Search / replace / validate

ts
const { output: fixedCode } = await refine({
  state: { code: originalCode, errors: [] as string[] },
  step: async (s) => {
    const { code }   = (await editor.invoke(s)).structured;
    const { errors } = (await linter.invoke(code)).structured;
    return { code, errors };
  },
  until: (curr, prev) => {
    if (curr.errors.length === 0) return { done: true, output: curr.code };
    if (curr.code === prev.code)  return { done: true, output: curr.code }; // no progress
    return { done: false };
  },
});

RefineLimitError

Thrown when maxIterations is reached without until() returning { done: true }.

ts
class RefineLimitError extends Error {
  maxIterations: number;
  lastState: unknown;
}

Inspect lastState to understand what the loop was stuck on:

ts
import { refine, RefineLimitError } from '@daedalus-ai-dev/ai-sdk';

try {
  await refine({ ... });
} catch (e) {
  if (e instanceof RefineLimitError) {
    console.error(`Gave up after ${e.maxIterations} iterations.`);
    console.error('Last state:', e.lastState);
  }
}

Using previous to detect stalled progress

The most important feature of until over a simple while loop: you always know if the last step actually changed anything.

ts
until: (curr, prev) => {
  // Primary exit — goal reached
  if (curr.testsPass) return { done: true, output: curr.code };

  // Safety exit — LLM produced the same output twice, no point continuing
  if (curr.code === prev.code) return { done: true, output: curr.code };

  return { done: false };
},

Without the prev check you'd burn all remaining iterations producing identical results.

Released under the MIT License.