Skip to content

feat: @templar/engine-loop — simple ReAct loop adapter (OpenClaw/NanoClaw parity) #247

@windoliver

Description

@windoliver

Parent: #235 (Swappable Engine)

Summary

A minimal loop-based EngineAdapter that proves the interface works without any graph concepts. This is the simplest possible engine: prompt → LLM → tool calls → repeat until done. No state machine, no graph, no checkpoints.

Inspired by OpenClaw's Docker-based ReAct loop and NanoClaw's VM-isolated agent loop. Proves that Templar's EngineAdapter contract is truly engine-agnostic — if a while(true) loop can implement it, nothing graph-specific has leaked in.

Estimated scope: ~300 LOC + tests

The Loop

class LoopEngineAdapter implements EngineAdapter {
  readonly engineId = "loop";

  async *stream(input: EngineInput): AsyncGenerator<EngineEvent> {
    let messages = toMessages(input);
    let turn = 0;

    while (turn < this.maxTurns) {
      // 1. Call LLM
      const response = await this.llm.chat(messages);

      // 2. Yield text deltas
      for (const delta of response.textDeltas) {
        yield { kind: "text_delta", delta };
      }

      // 3. If no tool calls, we're done
      if (!response.toolCalls?.length) {
        yield { kind: "done", output: { text: response.text, stopReason: "completed", usage: response.usage } };
        return;
      }

      // 4. Execute tool calls
      for (const call of response.toolCalls) {
        yield { kind: "tool_call_start", toolName: call.name, callId: call.id };
        const result = await this.toolExecutor.execute(call);
        yield { kind: "tool_call_end", callId: call.id, result };
        messages.push(toolResultMessage(call, result));
      }

      yield { kind: "turn_end", turnIndex: turn++ };
    }

    yield { kind: "done", output: { text: "", stopReason: "max_turns" } };
  }
}

Why This Matters

  1. Anti-leak proof — If the simplest possible loop implements EngineAdapter, the interface is clean
  2. OpenClaw parity — OpenClaw's Docker ReAct loop is essentially this pattern
  3. NanoClaw parity — NanoClaw's VM-isolated agent is a loop inside a sandbox
  4. Lightweight agents — Not every agent needs a graph state machine. Simple tasks deserve simple engines.
  5. Testing — Easy to test middleware in isolation (predictable execution)
  6. Edge/embedded — Minimal footprint for resource-constrained environments

Deliverables

  1. @templar/engine-loop package (~300 LOC)

    • LoopEngineAdapter implementing EngineAdapter
    • Configurable: maxTurns, llmProvider, toolExecutor
    • No dependencies on LangGraph or DeepAgents
  2. Tests (~200 LOC)

    • Basic loop: prompt → response → done
    • Tool calling: prompt → tool call → tool result → response → done
    • Max turns: loop hits limit → stops with max_turns reason
    • Interruption: external signal stops the loop
    • State save/load (optional methods)
  3. Integration test

    • Run same agent manifest with engine: loop and engine: deepagents
    • Verify identical tool call sequences for deterministic prompts

Manifest Usage

# templar.yaml
name: simple-agent
engine: loop
model:
  provider: anthropic
  name: claude-sonnet-4-5-20250929
tools:
  - name: web_search
    description: Search the web

Dependencies

Linux Parallel

This is like Linux's SCHED_FIFO — the simplest scheduler. No time-slicing, no priorities, no load balancing. Just run until done. It exists because sometimes simple is exactly right, and it proves the sched_class interface doesn't require CFS complexity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions