Integrate a harness with Harbor#
A harness owns the agent loop: it sends model requests, executes tools and
decides when the task is finished. This guide connects that loop to AReno
through Harbor. You will select or implement a Harbor agent, register a
harness configuration and expose the three hooks used by areno train.
Prerequisites#
Use Python 3.12+ and Harbor 0.24.0. This guide describes the integration in
AReno PR #621; running it
requires a checkout containing areno.api.harness and the SVG version of
examples/agentic/pi/. Prepare AReno using
Installation; reuse
the PyTorch and attention packages matched to your accelerator environment.
AReno’s dependencies include Harbor on supported Python versions. Check the
installation from the repository root:
python -c 'from importlib.metadata import version; print(version("harbor"))'
python -c 'from areno.api import HarnessRunner; print(HarnessRunner.__name__)'
areno check
The harness must support an OpenAI-compatible Chat Completions endpoint,
including its tool-call format. The sample proxy currently serves
POST /v1/chat/completions with n=1; streaming is supported. A harness
that only speaks Responses or Anthropic Messages needs a protocol adapter.
1. Select an existing agent#
Start with a native Harbor agent when one already wraps your harness. For example, Pi can be composed with the SVG task adapter:
import os
from areno.api import HarnessRunner
from examples.agentic.pi import svg_dataset # Registers "svg_icon".
workflow = HarnessRunner(
harness="pi",
dataset="svg_icon",
environment="docker",
harness_options={
"kwargs": {"model_api": "openai-completions", "thinking": "off"},
},
environment_options={"image": os.environ["ARENO_HARBOR_IMAGE"]},
require_agent_success=False,
)
harness_options contains native Harbor AgentConfig fields, including
kwargs, env, skills and mcp_servers. Set agent-specific options
inside kwargs. list_harnesses() from areno.api lists native names
and locally registered aliases.
Agents and tools are preinstalled by default. preinstalled=True skips
the native installed agent’s package installation while retaining its
execution, session handling and trajectory conversion. Use
preinstalled=False if Harbor should install the agent in its environment.
Custom external agents still receive their setup hook.
2. Wrap a new harness#
If Harbor does not already support the harness, implement its BaseAgent
contract. Keep the existing agent loop and tools; adapt its client to the
sample’s model connection. For a CLI installed in a container, Harbor’s
BaseInstalledAgent additionally supplies command execution and log
collection. See Harbor’s agent guide for that interface.
Here is a small external shell harness illustrating the complete connection.
Save it as my_harness.py at the repository root. Its tool runs through
Harbor’s environment, so the same loop can operate on different tasks.
import json
from harbor.agents.base import BaseAgent
from harbor.agents.model_connection import ModelConnectionSpec
from harbor.agents.options import AgentOptions
from openai import AsyncOpenAI
from pydantic import Field
class ShellOptions(AgentOptions):
max_turns: int = Field(default=8, ge=1)
class ShellAgent(BaseAgent):
options_model = ShellOptions
MODEL_CONNECTION = ModelConnectionSpec(default_provider="openai")
@staticmethod
def name():
return "my-shell"
def version(self):
return "0.1.0"
async def setup(self, environment):
pass # The environment already supplies the shell and task tools.
async def run(self, instruction, environment, context):
connection = self.model_connection
messages = [{"role": "user", "content": instruction}]
tools = [{
"type": "function",
"function": {
"name": "shell",
"description": "Run a shell command in the task workspace.",
"parameters": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"],
"additionalProperties": False,
},
},
}]
async with AsyncOpenAI(
base_url=connection.base_url,
api_key=connection.api_key,
max_retries=0,
) as client:
for _ in range(self.options.max_turns):
response = await client.chat.completions.create(
model=self.model_name.removeprefix("openai/"),
messages=messages,
tools=tools,
)
message = response.choices[0].message
messages.append(message.model_dump(exclude_none=True))
if not message.tool_calls:
break
for call in message.tool_calls:
try:
command = json.loads(call.function.arguments)["command"]
if call.function.name != "shell" or not isinstance(command, str):
raise ValueError("expected a shell command string")
except (ValueError, KeyError, TypeError) as error:
output = f"Invalid tool arguments: {error}"
else:
result = await environment.exec(command, timeout_sec=30)
output = json.dumps({
"returncode": result.return_code,
"stdout": result.stdout,
"stderr": result.stderr,
})
messages.append({
"role": "tool", "tool_call_id": call.id, "content": output,
})
(self.logs_dir / "messages.json").write_text(json.dumps(messages))
run receives the task instruction, a Harbor environment and an
AgentContext for execution metadata. MODEL_CONNECTION makes the
runner-injected OPENAI_BASE_URL and OPENAI_API_KEY available through
self.model_connection. Every policy request must reach the sample proxy
to become training data. Create the model client per attempt and close it
before returning.
Preserve assistant tool calls and their corresponding tool observations in order. Do not repair malformed model output by fabricating calls. The example reports argument errors to the model and lets it try again. Its message log is useful for inspection; ATIF export requires an agent-specific converter, such as the one Harbor’s native Pi agent supplies.
3. Register the harness and choose an environment#
Save the configuration in an importable module, my_integration.py:
import os
from harbor.models.trial.config import AgentConfig
from areno.api import HarnessRunner, register_harness
from task_adapter import FileTasks # Registers "file-tasks"; see the task guide.
def shell_factory(attempt, **options):
return AgentConfig(import_path="my_harness:ShellAgent", **options)
register_harness("my-shell", shell_factory, replace=True)
workflow = HarnessRunner(
harness="my-shell",
dataset="file-tasks",
environment="docker",
environment_options={"image": os.environ["ARENO_HARBOR_IMAGE"]},
result_dir="runs/my-harness/results",
require_agent_success=False,
)
A harness factory receives HarnessAttempt and harness_options and
returns native AgentConfig. The attempt identifies the sample and its
record; its environment is created later. You can also pass
harness="my_harness:ShellAgent" directly without registering an alias.
replace=True allows CLI hook files that are loaded independently to
import the same registration safely.
The image must contain the task tools and use the task workspace as its
working directory. Native environment options are supplied through
environment_options. image sets the task’s docker_image;
task_env sets task environment variables; provider constructor options go
inside kwargs. Set environment="local" for the external shell example
to use per-sample host workspaces instead of containers, and omit image.
For a reusable environment preset, use
register_environment(name, factory). Its factory receives the attempt and
options and returns EnvironmentSpec. Reuse
native_environment_spec from areno.agent.harness_trial for native
Harbor providers, or pass a provider import path directly as environment.
Keep environment selection independent of the task adapter.
For example, add this Docker alias to my_integration.py and select it
without modifying the harness or dataset:
from areno.agent.harness_trial import native_environment_spec
from areno.api import register_environment
def docker_factory(attempt, **options):
return native_environment_spec("docker", **options)
register_environment("team-docker", docker_factory, replace=True)
workflow.environment = "team-docker"
Provider |
Requirements and routing |
|---|---|
Docker |
Reachable daemon and preinstalled agent/tools. The proxy binds to
|
Local |
Preinstalled host tools. Uses loopback and separate workspaces, but shares the host filesystem. AReno’s local provider reaps the process groups it starts; it does not provide OS isolation. |
Native installed agents with fixed configuration paths require an isolated
provider. The Pi example supplies a compatible local wrapper with private
home/config directories; use its create_runner rather than a generic
HarnessRunner(harness="pi", environment="local", ...).
4. Expose AReno’s training hooks#
Create hooks.py and use the same workflow for loading, rollout and reward:
from my_integration import workflow
def load_training_dataset(dataset_path, *, default_loader, **kwargs):
return workflow.dataset_loader(
dataset_path, default_loader=default_loader, **kwargs,
)
async def run_agent(ctx, batch):
return await workflow(ctx, batch)
def reward_fn(record):
return workflow.reward_fn(record)
Wire the module into the CLI with --dataset-loader-fn hooks.py,
--agent-fn hooks.py and --reward-fn-path hooks.py. The dataset adapter
in Adapt a task completes this example; Pi SVG walkthrough provides the
end-to-end training command and checks.
How the connection works#
For each sampled task, HarnessRunner creates an independent record,
environment configuration and authenticated model proxy, then starts a native
Harbor Trial. The task adapter prepares the workspace on AGENT_START
and grades it on AGENT_END. Harbor’s built-in verifier is disabled;
AReno’s reward hook reads the task adapter’s scalar result.
loader -> task records -> HarnessRunner -> Harbor Trial -> agent -> tools
^ |
+-- sample model proxy <-----+
-> AReno policy
task adapter -> reward -> AReno optimizer -> checkpoint
The proxy records AReno’s exact input/output token IDs and rollout log
probabilities. Training turns come from this trace, not from re-tokenizing
logs or importing an ATIF file. AReno owns context and generation limits;
harness max_tokens settings do not override the training configuration.
See Agentic rollout API for the underlying contract.
Before scaling up, verify one complete tool interaction, successful and failed task grades, two concurrent attempts with distinct workspaces, and cleanup after a timeout. Inspect a saved trace to confirm that policy requests passed through AReno. Then check two optimizer updates as described in Pi SVG walkthrough.