criogenai

Reasoning

ReWOO

Plan every tool call up front; run them; solve once.

ReWOO (reasoning without observation) writes the whole plan, tool calls included, in one model call, using placeholders like #E1 for results that do not exist yet. Workers then run the tools with no model involved, and a solver writes the answer from the evidence.

Two model calls, however many tools: cheaper and faster than ReAct when the lookups are predictable.

Use it when
Lookups you can plan before seeing results
Cost-sensitive pipelines: the model is called twice
Tool results that should not steer the plan
Skip it when
The next step depends on what a tool returns (ReAct)
Tools that often fail: the plan cannot adapt mid-way

Run it

Scripted model · no key needed · CriogenAI engine in your browser
Ready
#E1…TaskPlannersearch()calc()SolverAnswer
The runmodel 0 · tools 0
  1. Press Run to watch the pattern work, step by step.
Try it with your own key optional: a real model makes the decisions; the tools stay simulated
  • Your key stays in this tab's memory: not saved, not in cookies or the address bar. Reload or close the tab and it is gone.
  • Your browser calls OpenAI directly. Nothing passes through criogenai.com, and nothing is logged.
  • Each run makes at most 16 short model calls, billed to your account. Tools, data and side effects are simulated.

This run is the TypeScript code below, running on the CriogenAI engine in your browser (WebAssembly). Without a key, a scripted model stands in for MODEL; with your key, your provider answers, called from this tab. The tools return made-up data.

Build it with CriogenAI

Built from primitives: Workflow: planner node, tool nodes, solver node
class Call(BaseModel):
    id: str  # "E1"
    tool: str  # "search" or "calc"
    arg: str  # may use earlier results: "population of #E1"

class Plan(BaseModel):
    calls: list[Call]

def search(query: str) -> str:
    return {"capital of France": "Paris", "population of Paris": "2.1 million"}.get(query, "no result")

def calc(expression: str) -> str:
    return "11.9"  # your calculator here

planner = Agent(MODEL, instructions="Plan every tool call up front. Tools: search, calc.", output=Plan)
solver = Agent(MODEL, instructions="Answer from the plan and the evidence only.")

async def plan(state: Dict[str, Any]) -> Dict[str, Any]:
    return {"calls": [c.model_dump() for c in (await planner.run(state["input"])).output.calls]}

def work(state: Dict[str, Any]) -> Dict[str, Any]:
    # No model here: run the tools, filling in #E placeholders.
    evidence: Dict[str, str] = {}
    for call in state["calls"]:
        arg = call["arg"]
        for key, value in evidence.items():
            arg = arg.replace(f"#{key}", value)
        evidence[call["id"]] = (calc if call["tool"] == "calc" else search)(arg)
    return {"evidence": evidence}

async def solve(state: Dict[str, Any]) -> Dict[str, Any]:
    out = await solver.run(f"Task: {state['input']}\nEvidence: {state['evidence']}")
    return {"output": out.text}

wf = Workflow()
wf.add_node("plan", plan)
wf.add_node("work", work)
wf.add_node("solve", solve)
wf.start("plan").edge("plan", "work").edge("work", "solve").edge("solve", END)
result = await wf.compile().run({"input": "Which capital has more people, France's or Japan's?"})
print(result.state["output"])  # two model calls, however many tools
type Call = { id: string; tool: "search" | "calc"; arg: string }; // arg may use "#E1"
const planner = new Agent<{ calls: Call[] }>({
  model: MODEL,
  instructions: "Plan every tool call up front. Tools: search, calc. Ids E1, E2…; args may use #E1.",
  output: {
    type: "object",
    properties: {
      calls: {
        type: "array",
        items: {
          type: "object",
          properties: { id: { type: "string" }, tool: { enum: ["search", "calc"] }, arg: { type: "string" } },
          required: ["id", "tool", "arg"],
        },
      },
    },
    required: ["calls"],
  },
});
const solver = new Agent({ model: MODEL, instructions: "Answer from the plan and the evidence only." });
const facts: Record<string, string> = {
  "capital of France": "Paris",
  "capital of Japan": "Tokyo",
  "population of Paris": "2.1 million",
  "population of Tokyo": "14 million",
};
const tools = { search: (q: string) => facts[q] ?? "no result", calc: (_expr: string) => "11.9" /* your calculator */ };

const app = new Workflow()
  .node("plan", async (state) => ({ calls: (await planner.run(String(state.input))).output?.calls ?? [] }))
  .node("work", (state: State) => {
    // No model here: run the tools, filling in #E placeholders.
    const evidence: Record<string, string> = {};
    for (const call of state.calls as Call[]) {
      const arg = Object.entries(evidence).reduce((a, [k, v]) => a.replaceAll(`#${k}`, v), call.arg);
      evidence[call.id] = tools[call.tool](arg);
    }
    return { evidence };
  })
  .node("solve", async (state) => ({ output: (await solver.run(`Task: ${state.input}\nEvidence: ${JSON.stringify(state.evidence)}`)).text }))
  .start("plan")
  .edge("plan", "work")
  .edge("work", "solve")
  .edge("solve", END)
  .compile();
const result = await app.run({ input: task });
console.log(result.state.output); // two model calls, however many tools