{"slug": "how-to-build-a-production-agent-harness", "title": "How to Build a Production Agent Harness", "summary": "A developer demonstrates how to build a production-grade AI agent harness using Bit's component-based architecture. The tutorial packages harness logic as versioned components—state tracker, context loader, and verifier—in a shared Bit scope, enabling automatic propagation of fixes via Ripple CI. This approach replaces one-off scripts with reusable, installable components, addressing common failure modes in agent loops.", "body_md": "AI agents don't usually become unreliable all at once. They degrade quietly. One session the agent repeats a step it already completed. The next, it loses track of where it was mid-run and starts over from scratch. Eventually it produces output with nothing in place to verify whether it actually succeeded.\n\nThe problem is structural. Harness components get built as one-off scripts instead of versioned, reusable units. Your state file lives in one project, a slightly different version lives in another, and when you fix a bug in one place the fix stays local. Teams end up maintaining the same harness logic across multiple projects and debugging whichever version happens to be in front of them.\n\nThis tutorial shows how to fix that at the source. Instead of writing harness logic as inline scripts, you'll package it as versioned components in a shared Bit scope. Fix a bug once, tag and export, and Ripple CI (Bit's built-in component CI/CD pipeline) propagates the update to every project that depends on it automatically. By the end, your harness stops being something you rebuild per project and becomes something you install with a single command.\n\nBefore you start, make sure you have the following in place so the setup steps run cleanly:\n\n**Node.js 18 or higher** installed on your machine. Run `node --version`\n\nto check.\n\n**A** **bit.cloud****account.** You'll create a scope during setup. If you don't have one yet, head to [bit.cloud/signup](https://bit.cloud/signup).\n\n**Basic familiarity with TypeScript.** The components in this tutorial use TypeScript. You don't need to be an expert, but you should be comfortable reading typed function signatures.\n\n**A terminal and a code editor.** The tutorial runs entirely from the command line with a few file edits along the way.\n\nThe three components are a state tracker, a context loader and a verifier. Each one targets a specific failure mode in the agent loop.\n\n**harness/state** tracks what the agent has done, what it's currently working on and what comes next. It reads at session start and writes at session end.\n\n**harness/context** pre-loads a structured map of your components and their relationships before the agent takes its first action.\n\n**harness/verifier** evaluates agent output against a done condition before the loop continues, returning one of four verdicts: `NO`\n\n, `YES`\n\n, `MAYBE`\n\nor `IFF`\n\n.\n\nOnce all three are released to a shared scope, your entire harness installs into any agent project with a single command:\n\n```\nbit install @your-username/agent-harness.harness.state @your-username/agent-harness.harness.context @your-username/agent-harness.harness.verifier\n```\n\nHere's how the three components fit into the agent loop:\n\nCreate a new [scope](https://bit.dev/reference/reference/scope/scope-bit-cloud) and give it any name you like. For this tutorial, the scope will be named `agent-harness`\n\n. You can also use an existing scope.\"\n\nWith your scope ready, install Bit's version manager by running this in your terminal. It handles Bit installations and keeps your version up to date across projects:\n\n```\nnpx @teambit/bvm install\n```\n\nNext, initialize your workspace. This creates a `workspace.jsonc`\n\nconfiguration file at your project root and sets `agent-harness`\n\nas the default scope for every component you create in this workspace:\n\n```\nbit init --default-scope your-username.agent-harness\n```\n\nReplace `your-username`\n\nwith your [bit.cloud](http://bit.cloud) username. A successful run looks like this:\n\n```\nsuccessfully initialized a bit workspace.\n```\n\nOpen `workspace.jsonc`\n\nand uncomment the Node environment line. This tells Bit which runtime to use when building and compiling your components:\n\n```\n\"bitdev.node/node-env\": {}\n```\n\nThen run this to pull the Node environment and resolve its full dependency tree. This is a one-time step per workspace:\n\n```\nbit install\n```\n\nThis will take a few minutes on first run. You'll see pnpm working through several hundred packages before it completes.\n\nBefore creating any components, run this to see the available templates for your environment:\n\n```\nbit templates\n```\n\nThe template you want is `module`\n\n, listed under `bitdev.node/node-env`\n\n. Now run this to scaffold all three harness components at once:\n\n```\nbit create module harness/state harness/context harness/verifier\n```\n\nEach command generates a component folder with a TypeScript entry file, a test file and the necessary Bit configuration. You'll see output confirming all three components were created under your scope, each assigned the Node environment automatically:\n\nOne thing worth noting before you move on: `harness/state`\n\n, `harness/context`\n\nand `harness/verifier`\n\nare the component paths inside your workspace. When Bit publishes them to your scope, it generates a fully qualified package name by combining your scope and component path, for example `@your-username/agent-harness.harness.state`\n\n.\n\nYour workspace is ready. Three component folders now exist under `agent-harness/harness/`\n\n, the Node environment is configured and you have a scope waiting to receive them once the implementations are complete.\n\nAn agent without externalized state forgets everything between sessions. Every run starts blind: no record of what was completed, no awareness of what's in progress and no queue of what comes next. This is the failure mode that [production systems](https://dev.to/hackmamba/the-three-layer-architecture-that-makes-software-production-ready-2pdh) are designed to prevent at the infrastructure level. The state component fixes that by reading a structured JSON file at session start and writing back to it before the session ends.\n\nOpen the `agent-harness/harness/state/state.ts`\n\nfile generated by `bit create module`\n\nin the previous step and replace the generated content with this:\n\n``` js\nimport { readFileSync, writeFileSync, existsSync } from 'fs';\n\nimport { resolve } from 'path';\n\nexport type HarnessState = {\n\n  done: string[];\n\n  inProgress: string[];\n\n  next: string[];\n\n};\n\nconst DEFAULT_STATE: HarnessState = {\n\n  done: [],\n\n  inProgress: [],\n\n  next: [],\n\n};\n\nexport function loadState(statePath = 'harness-state.json'): HarnessState {\n\n  const abs = resolve(statePath);\n\n  if (!existsSync(abs)) return { ...DEFAULT_STATE };\n\n  const raw = readFileSync(abs, 'utf-8');\n\n  return JSON.parse(raw) as HarnessState;\n\n}\n\nexport function saveState(state: HarnessState, statePath = 'harness-state.json'): void {\n\n  const abs = resolve(statePath);\n\n  writeFileSync(abs, JSON.stringify(state, null, 2), 'utf-8');\n\n}\n```\n\n`HarnessState`\n\nhas three fields. `done`\n\nholds everything the agent has completed. `inProgress`\n\nholds whatever the agent is currently working on. `next`\n\nholds the queue of work still to come.\n\n`loadState`\n\nreads the state file at the path you specify, defaulting to `harness-state.json`\n\nat the project root. If no file exists yet, it returns an empty default state rather than throwing an error. `saveState`\n\nwrites the updated state back to the same path before the session ends. Between those two calls, your agent has a persistent, structured record of exactly where it is in the work, regardless of how many sessions it takes to get there.\n\nWithout pre-loaded context, your agent rediscovers the same dependency relationships from scratch on every run. It wastes the first part of every session figuring out what it already knew. The context component solves that by querying your Bit workspace for component dependencies and dependents before the agent takes its first action, handing it a structured map it can reason over immediately.\n\nOpen the `agent-harness/harness/context/context.ts`\n\nfile generated by `bit create module`\n\nin the previous step, and replace the generated content with this:\n\n``` js\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\n\nconst execAsync = promisify(exec);\nexport type ComponentMeta = {\n\n  id: string;\n\n  description?: string;\n\n  dependencies: string[];\n\n  dependents: string[];\n\n};\n\nexport type DependencyMap = Record<string, ComponentMeta>;\nasync function runBitShow(componentId: string): Promise<{ id: string; description?: string; dependencies: string[] }> {\n\n  const { stdout } = await execAsyncbit show --json ${componentId}, { timeout: 30_000 });\n\n  const data = JSON.parse(stdout);\n\n  const deps: string[] = (data?.dependencies ?? []).map((d: { id: string }) => d.id);\n\n  return {\n\n    id: componentId,\n\n    description: data?.description,\n\n    dependencies: deps,\n\n  };\n\n}\n\nasync function runBitDependents(componentId: string): Promise<string[]> {\n\n  try {\n\n    const { stdout } = await execAsyncbit dependents ${componentId}, { timeout: 30_000 });\n\n    return stdout\n\n      .split('\\n')\n\n      .map((l) => l.trim())\n\n      .filter((l) => l.length > 0 && !l.startsWith('┌') && !l.startsWith('└') && !l.startsWith('│') && !l.startsWith('─'));\n\n  } catch {\n\n    return [];\n\n  }\n\n}\n\nexport async function buildDependencyMap(componentIds: string[]): Promise<DependencyMap> {\n\n  const map: DependencyMap = {};\n\n  await Promise.all(\n\n    componentIds.map(async (id) => {\n\n      const [meta, dependents] = await Promise.all([runBitShow(id), runBitDependents(id)]);\n\n      map[id] = { ...meta, dependents };\n\n    })\n\n  );\n\n  return map;\n\n}\n```\n\nThere are two things worth understanding about how this works before you move on.\n\n`runBitShow`\n\nshells out to `bit show --json`\n\nfor a given component ID and parses the JSON output into a structured object containing the component's ID, description and direct dependencies. `runBitDependents`\n\nshells out to `bit dependents`\n\nand parses the plain-text output, filtering out the table-border characters Bit uses in its CLI output to leave you with a clean list of dependent component IDs.\n\n`buildDependencyMap`\n\ntakes an array of component IDs and runs both queries concurrently for each one using `Promise.all`\n\n. The result is a `DependencyMap`\n\n: a keyed record where each entry gives your agent a full picture of what a component depends on and what depends on it, all resolved before the first agent action fires.\n\nMost agent loops use the same context to generate and judge output. That creates a real failure mode: the model can sound confident and still be wrong. A separate verifier keeps the check outside the generation loop and forces a second pass before the agent continues. The verifier component does that by sitting in a completely separate component, isolated from the generator, and evaluating output against a [done condition you define before the loop starts](http://).\n\n[Addy Osmani](https://www.oreilly.com/radar/loop-engineering/) argues for keeping the maker away from the checker. The verifier is where that principle lives in your harness.\n\nOpen the `agent-harness/harness/verifier/verifier.ts`\n\nfile generated by `bit create module`\n\nand replace the generated content with this:\n\n```\nexport type DoneCondition = 'NO' | 'YES' | 'MAYBE' | 'IFF';\n\nexport type VerifyResult = {\n\n  verdict: DoneCondition;\n\n  reason: string;\n\n  condition?: string;\n\n};\n\nexport type VerifierOptions = {\n\n  agentOutput: string;\n\n  doneKeywords?: string[];\n\n  failKeywords?: string[];\n\n  iffPattern?: RegExp;\n\n};\n\nexport function verify(opts: VerifierOptions): VerifyResult {\n\n  const { agentOutput, doneKeywords = [], failKeywords = [], iffPattern } = opts;\n\n  const normalized = agentOutput.toLowerCase();\n\n  if (failKeywords.some((kw) => normalized.includes(kw.toLowerCase()))) {\n\n    return {\n\n      verdict: 'NO',\n\n      reason: Output contains a failure signal.,\n\n    };\n\n  }\n\n  if (doneKeywords.length > 0 && doneKeywords.every((kw) => normalized.includes(kw.toLowerCase()))) {\n\n    return {\n\n      verdict: 'YES',\n\n      reason: All done keywords found in output.,\n\n    };\n\n  }\n\n  if (iffPattern) {\n\n    const match = agentOutput.match(iffPattern);\n\n    if (match) {\n\n      return {\n\n        verdict: 'IFF',\n\n        reason: Output satisfies pattern but requires conditional verification.,\n\n        condition: match[0],\n\n      };\n\n    }\n\n  }\n\n  if (doneKeywords.some((kw) => normalized.includes(kw.toLowerCase()))) {\n\n    return {\n\n      verdict: 'MAYBE',\n\n      reason: Some but not all done keywords found.,\n\n    };\n\n  }\n\n  return {\n\n    verdict: 'NO',\n\n    reason: No done signals detected in output.,\n\n  };\n\n}\n```\n\n`VerifierOptions`\n\ntakes four inputs:\n\n`agentOutput`\n\nis the raw string output from the agent.\n\n`doneKeywords`\n\nis a list of terms that must all appear in the output for it to pass.\n\n`failKeywords`\n\nis a list of terms that immediately fail the output if any one of them appears.\n\n`iffPattern`\n\nis a regular expression that triggers a conditional verdict when it matches.\n\nThe `verify`\n\nfunction evaluates in a fixed priority order. Failure signals are checked first: if any `failKeyword`\n\nappears in the output, the function returns `NO`\n\nimmediately without evaluating anything else. If all `doneKeywords`\n\nare present, it returns `YES`\n\n. If `iffPattern`\n\nmatches, it returns `IFF`\n\nalong with the matched string as the condition the agent needs to resolve before continuing. If only some `doneKeywords`\n\nare present, it returns `MAYBE`\n\n, pausing the loop for human review. If none of those conditions are met, it returns `NO`\n\n.\n\nThe four verdicts map directly to actions in your agent loop. `YES`\n\nupdates state and continues. `NO`\n\nretries the action. `MAYBE`\n\npauses and flags for human review. `IFF`\n\nchecks the named dependency before deciding either way.\n\nAll three components are built. Next, you version them and release them to your scope so [Ripple CI](https://bit.dev/reference/ci/ripple-ci/) can pick up the export automatically.\n\nTag all three components with a single command. This versions the modified components selected for tagging in one command:\n\n```\nbit tag --message \"initial implementation of harness components\"\n```\n\nYou'll see output confirming all three components were tagged at version `0.0.1`\n\n:\n\nNow push all three components to your scope on [bit.cloud](http://bit.cloud):\n\n```\nbit export\n```\n\nYou'll see Bit indexing your components and confirming a successful push:\n\nYou may see a warning about `node-env`\n\nnot being loaded during export. Run `bit install`\n\nlocally to clear it.\n\nThe moment `bit export`\n\ncompletes, [Ripple CI](https://bit.dev/reference/ci/ripple-ci/) picks up the push and kicks off a remote build job automatically. No configuration required. Head to the URL in the export output to watch compilation, tests and documentation generation run against all three components in the cloud.\n\nHere's what a successful build looks like:\n\nRipple CI dashboard showing three harness components built successfully in 2 minutes 41 seconds\n\nWith your components live on [bit.cloud](http://bit.cloud), install all three into any agent project with a single command:\n\n```\nbit install @your-username/agent-harness.harness.state @your-username/agent-harness.harness.context @your-username/agent-harness.harness.verifier\n```\n\nIf you're working outside a Bit workspace, install via npm with Bit's registry instead:\n\n```\nnpm install @your-username/agent-harness.harness.state \\\n\n            @your-username/agent-harness.harness.context \\\n\n            @your-username/agent-harness.harness.verifier \\\n\n            --registry https://node-registry.bit.cloud\n```\n\nWith all three components installed, here's how they wire together at the boundaries of an agent session:\n\n``` js\nimport { loadState, saveState } from '@your-username/agent-harness.harness.state';\n\nimport { buildDependencyMap } from '@your-username/agent-harness.harness.context';\n\nimport { verify } from '@your-username/agent-harness.harness.verifier';\n\n// At session start\n\nconst state = loadState();\n\nconst context = await buildDependencyMap(['harness/state', 'harness/context', 'harness/verifier']);\n\nconsole.log('Resuming from:', state);\n\nconsole.log('Dependency map:', context);\n\n// Agent does its work here\n\nconst agentOutput = 'scaffold auth module completed. tests passing.';\n\n// Verify before continuing\n\nconst result = verify({\n\n  agentOutput,\n\n  doneKeywords: ['completed', 'tests passing'],\n\n  failKeywords: ['error', 'failed'],\n\n});\n\nconsole.log('Verdict:', result.verdict);\n\n// Update state based on verdict\n\nif (result.verdict === 'YES') {\n\n  state.done.push('scaffold auth module');\n\n  state.inProgress = [];\n\n  state.next = ['write tests for auth module'];\n\n}\n\n// At session end\n\nsaveState(state);\n```\n\nThe context loads before the first action. The state picks up where the last session ended. The verifier evaluates output before the loop continues. When any of those three components changes in any project, tag and export from that project, and [Ripple CI](https://bit.dev/reference/ci/ripple-ci/) propagates the update to every downstream dependent automatically. That's the harness: three components, one install command, zero copy-paste.\n\nYou now have a production harness that lives outside your agent, versioned and shared across every project that needs it. When something breaks, you fix it in one place, tag it, export it and [Ripple CI](https://bit.dev/reference/ci/ripple-ci/) propagates the change automatically.\n\nThat's the difference between a harness you maintain and a harness that maintains itself.\n\nThe approach scales further than this tutorial goes. You can extend `HarnessState`\n\nto track token usage, session duration or retry counts. You can add a fourth component that handles context window management, trimming what gets loaded based on what the state says is already done. You can wire the verifier into a CI step so no agent output merges without passing a a done condition first.\n\nEvery extension is just another versioned component added to the same scope. Once the pattern is in place, growing the harness stops being a rewrite and starts being an addition.", "url": "https://wpnews.pro/news/how-to-build-a-production-agent-harness", "canonical_source": "https://dev.to/hackmamba/how-to-build-a-production-agent-harness-4k7o", "published_at": "2026-08-10 08:25:11+00:00", "updated_at": "2026-08-10 08:46:52.947295+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "mlops"], "entities": ["Bit", "Ripple CI", "bit.cloud", "Node.js", "TypeScript"], "alternates": {"html": "https://wpnews.pro/news/how-to-build-a-production-agent-harness", "markdown": "https://wpnews.pro/news/how-to-build-a-production-agent-harness.md", "text": "https://wpnews.pro/news/how-to-build-a-production-agent-harness.txt", "jsonld": "https://wpnews.pro/news/how-to-build-a-production-agent-harness.jsonld"}}