obi-jam pattern intermediate

Separate the Harness from the Agent

The moment we stopped hardcoding personality into the runtime was the moment we could run any agent on anything. Turns out the machine doesn't need to know who it is — it just needs to know how to listen, think, and reply.

We’d been building  Tony’s runtime for months. Every feature, every fix, every config tweak was wired directly into the codebase.  Tony’s name was in the logs. His personality was in the prompt assembly. His skills were hardcoded into the command router. The runtime and the agent were the same thing.

Then we needed to run a second agent.

The Problem

When your agent’s identity lives inside the runtime code, scaling from one agent to two means forking the entire codebase. You end up maintaining two copies of the same transport layer, the same memory system, the same health checks — all because the personality is tangled into the infrastructure.

The symptoms show up everywhere:

  • Agent name appears in log strings, error messages, and startup banners
  • Personality text is embedded in prompt construction functions
  • Skills are imported directly by the command router
  • Config files reference agent-specific paths and behaviors
  • Adding a second agent means duplicating the whole project

This isn’t a scaling problem you hit at agent number fifty. You hit it at agent number two.

Why This Happens

Most agent projects start as a single-agent prototype. You wire the personality into the prompt, hardcode the skills, and ship it. That’s fine for getting something running. But the architecture implicitly assumes there will only ever be one agent, and that assumption calcifies into every file.

The runtime becomes the agent. The agent becomes the runtime. Separating them later means surgery on a codebase that was never designed to be split.

The Fix

Draw a clean line between machine and mind.

The harness owns infrastructure:

// index.js — the harness never mentions an agent by name
const profile = await AgentLoader.load(config.agent.profileDir);
const transport = loadTransport(config.transport);
const claude = new ClaudeService(config.anthropic);

transport.onMessage(async (userId, text, reply) => {
  const history = memory.getHistory(userId);
  const response = await claude.chat(profile.systemPrompt, history, text);
  await reply(response);
});

The agent owns identity:

agents/your-agent/workspace/
  SOUL.md        — personality, voice, operating rules
  IDENTITY.md    — name and role (parsed at startup)
  MEMORY.md      — institutional knowledge
  PRIORITY.md    — how to triage competing tasks
  TASKS.md       — current active work
  skills/        — agent-specific commands

The connection contract is one config line:

{
  "agent": {
    "profileDir": "/path/to/agents/your-agent/workspace"
  }
}

Change the path. Restart. Different agent, same machine.

What the agent loader does at startup:

  1. Read SOUL.md — becomes the core system prompt
  2. Parse IDENTITY.md — extract name and role
  3. Read optional files (MEMORY.md, PRIORITY.md, TASKS.md, CALENDAR.md) — append to system prompt in sections
  4. Scan skills/ directory — discover commands from subdirectory names and SKILL.md frontmatter

The assembled system prompt follows a strict order: personality first, identity metadata, then context layers. Every interaction gets full context without the harness knowing anything about who it’s serving.

Key Takeaway

The harness is the machine. The agent is the mind. Keep them separate and you get portability for free — same code runs any agent, any agent runs on any machine. The moment you hardcode personality into infrastructure, you’ve built a prison that fits exactly one occupant.

FAQ

How do I build an agent runtime that can run any agent without code changes?

Split your code into two layers: the harness (infrastructure) and the agent (identity). The harness owns transport, auth, LLM routing, memory, and process management. The agent owns personality, knowledge, priorities, and skills — all defined in a profile directory of markdown files. Point the harness at a different directory and you have a different agent. Zero code changes.

What belongs in the harness vs the agent profile?

Harness: transport layer (Discord, iMessage), auth/allowlist, LLM API wrappers, conversation memory persistence, command parser, health endpoint, process management. Agent: SOUL.md (personality), IDENTITY.md (name/role), MEMORY.md (institutional knowledge), PRIORITY.md (how to triage), TASKS.md (active work), skills/ (agent-specific commands). If it's about how to run, it's harness. If it's about who to be, it's agent.

Can the same runtime code run multiple different agents?

Yes. The connection contract is a single config path: config.agent.profileDir pointing to the agent's workspace directory. Swap that path from one agent's directory to another and the harness loads a completely different personality, skill set, and knowledge base at startup. Same code, different mind.