Skip to content
Quick Guides
Quick Guides

Harness orchestration

Dispatch a coding agent as a typed worker with turn caps and schema-validated output. AForge runs by default; Claude Code, Codex, Gemini CLI, and OpenCode are one field away.

Use a harness as a step inside a regular reasoner. The harness runs a coding agent, gives it tool access, bounds the run with a turn cap, and returns a schema-validated object -- not free-form text.

Nothing to install. By default the worker is AForge, AgentField's native harness, provisioned alongside the af binary by the curl installer, AgentField Desktop, and the official agent Docker images. Export OPENROUTER_API_KEY and the example below runs. Want Claude Code, Codex, Gemini CLI, or OpenCode driving it instead? That is one field -- see Bring your own coding agent.

from pydantic import BaseModel
from agentfield import Agent, HarnessConfig

# No provider, no model -- this agent's harness calls run on AForge.
app = Agent(node_id="migrator", harness_config=HarnessConfig())

class MigrationPlan(BaseModel):
    sql_statements: list[str]
    rollback_steps: list[str]
    risk_assessment: str

@app.reasoner()
async def plan_migration(description: str) -> dict:
    # Harness reads the schema, writes SQL, validates against the Pydantic model
    result = await app.harness(
        f"Analyze the database schema and produce a migration plan for: {description}",
        schema=MigrationPlan,
        max_turns=20,   # turn cap -- AForge stops cleanly when it is hit
    )

    if result.is_error:
        return {"error": result.failure_type, "message": result.error_message}

    plan: MigrationPlan = result.parsed
    return {
        "sql": plan.sql_statements,
        "rollback": plan.rollback_steps,
        "cost_usd": result.cost_usd,
        "turns": result.num_turns,
    }

# Swap workers per-call -- Codex for test generation, Gemini for big refactors
@app.reasoner()
async def write_tests(module: str) -> dict:
    result = await app.harness(
        f"Write a comprehensive test suite for {module}.",
        provider="codex",
        model="o4-mini",
        max_turns=40,
    )
    return {"output": result.text, "cost_usd": result.cost_usd}

app.run()
import { Agent, type HarnessConfig } from "@agentfield/sdk";
import { z } from "zod";

// No provider, no model -- this agent's harness calls run on AForge.
const harnessConfig: HarnessConfig = {};
const app = new Agent({ nodeId: "migrator", harnessConfig });

const MigrationPlan = z.object({
  sqlStatements: z.array(z.string()),
  rollbackSteps: z.array(z.string()),
  riskAssessment: z.string(),
});

app.reasoner("plan_migration", async (ctx) => {
  // Harness reads the schema, writes SQL, validates against the Zod model
  const result = await app.harness(
    `Analyze the database schema and produce a migration plan for: ${ctx.input.description}`,
    {
      schema: MigrationPlan,
      maxTurns: 20,   // turn cap -- AForge stops cleanly when it is hit
    },
  );

  if (result.isError) {
    return { error: result.errorMessage };
  }

  const plan = MigrationPlan.parse(result.parsed);
  return {
    sql: plan.sqlStatements,
    rollback: plan.rollbackSteps,
    costUsd: result.costUsd,
    turns: result.numTurns,
  };
});

// Swap workers per-call -- Codex for test generation, Gemini for big refactors
app.reasoner("write_tests", async (ctx) => {
  const result = await app.harness(
    `Write a comprehensive test suite for ${ctx.input.module}.`,
    { provider: "codex", model: "o4-mini", maxTurns: 40 },
  );
  return { output: result.text, costUsd: result.costUsd };
});

app.serve();
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/Agent-Field/agentfield/sdk/go/agent"
    "github.com/Agent-Field/agentfield/sdk/go/harness"
)

type MigrationPlan struct {
    SQLStatements  []string `json:"sql_statements"`
    RollbackSteps  []string `json:"rollback_steps"`
    RiskAssessment string   `json:"risk_assessment"`
}

func main() {
    a, err := agent.New(agent.Config{
        NodeID:        "migrator",
        Version:       "1.0.0",
        AgentFieldURL: "http://localhost:8080",
        // No Provider, no Model -- this agent's harness calls run on AForge.
        HarnessConfig: &agent.HarnessConfig{},
    })
    if err != nil {
        log.Fatal(err)
    }

    a.RegisterReasoner("plan_migration", func(ctx context.Context, input map[string]any) (any, error) {
        description := fmt.Sprintf("%v", input["description"])

        // Harness reads the schema, writes SQL, validates against the struct
        var plan MigrationPlan
        schema, _ := harness.StructToJSONSchema(plan)

        result, err := a.Harness(ctx,
            "Analyze the database schema and produce a migration plan for: "+description,
            schema, &plan,
            harness.Options{MaxTurns: 20}, // turn cap -- AForge stops cleanly when it is hit
        )
        if err != nil {
            return nil, err
        }
        if result.IsError {
            return map[string]any{"error": result.ErrorMessage}, nil
        }

        return map[string]any{
            "sql":      plan.SQLStatements,
            "rollback": plan.RollbackSteps,
            "turns":    result.NumTurns,
        }, nil
    })

    // Swap workers per-call -- Codex for test generation, Gemini for big refactors
    a.RegisterReasoner("write_tests", func(ctx context.Context, input map[string]any) (any, error) {
        module := fmt.Sprintf("%v", input["module"])
        result, err := a.Harness(ctx,
            "Write a comprehensive test suite for "+module+".",
            nil, nil,
            harness.Options{Provider: "codex", Model: "o4-mini", MaxTurns: 40},
        )
        if err != nil {
            return nil, err
        }
        return map[string]any{"output": result.Text()}, nil
    })

    a.Serve(context.Background())
}

What this gives you

  • A multi-turn harness looks like a regular function call -- input goes in, validated object comes out.
  • The default worker ships with af, so the first run needs no CLI install and no per-worker vendor account -- just one OPENROUTER_API_KEY.
  • max_turns bounds the run: on the default AForge worker it becomes --turns, and the agent stops cleanly when the cap is hit. The USD cost cap max_budget_usd is enforced by claude-code only -- on AForge, bound spend with max_turns plus AForge's own token budget (AFORGE_EXEC_BUDGET, default 150000 tokens).
  • failure_type distinguishes timeouts, crashes, schema-validation failures, and API errors.

Bring your own coding agent

The loop above is worker-agnostic. Set provider and the identical call drives Claude Code, Codex, Gemini CLI, or OpenCode instead -- which is how you orchestrate a fleet of Claude Codes (or mix workers) from a single AgentField reasoner.

# Same reasoner, different worker -- and claude-code is the provider that enforces max_budget_usd.
plan = await app.harness(
    prompt, schema=MigrationPlan, provider="claude-code", max_budget_usd=1.00
)

# Or set it once for the whole agent.
app = Agent(node_id="migrator", harness_config=HarnessConfig(provider="claude-code"))
// Same reasoner, different worker -- and claude-code is the provider that enforces maxBudgetUsd.
const plan = await app.harness(prompt, {
  schema: MigrationPlan,
  provider: "claude-code",
  maxBudgetUsd: 1.0,
});

// Or set it once for the whole agent.
const app = new Agent({ nodeId: "migrator", harnessConfig: { provider: "claude-code" } });
// Same reasoner, different worker -- and claude-code is the provider that enforces MaxBudgetUSD.
result, err := a.Harness(ctx, prompt, schema, &plan,
    harness.Options{Provider: "claude-code", MaxBudgetUSD: 1.0})

// Or set it once for the whole agent.
cfg := agent.Config{HarnessConfig: &agent.HarnessConfig{Provider: "claude-code"}}

Each override brings its own install and credential (Claude Code differs per SDK):

providerInstallCredential
aforge (default)ships with af -- nothing to doOPENROUTER_API_KEY
claude-codePython: pip install 'agentfield[harness-claude]' · TypeScript: npm install @anthropic-ai/claude-agent-sdk · Go: the claude CLI (npm install -g @anthropic-ai/claude-code)ANTHROPIC_API_KEY
codexnpm install -g @openai/codexOPENAI_API_KEY
gemininpm install -g @google/gemini-cliGEMINI_API_KEY
opencodecurl -fsSL https://opencode.ai/install | bashopencode auth login

AGENTFIELD_HARNESS_PROVIDER shifts the default for a whole process without touching code; an explicit provider on the config or the call still wins. Verify a worker before you depend on it:

af harness doctor --provider aforge     # or claude-code / codex / gemini / opencode

Next