Cogitator
Core

Harness Agent

createHarnessAgent() assembles a coding or assistant agent from the workspace, todos, skills, subagents, commands, project instructions, permissions, compaction and tool search in one call, with presets and every piece overridable.

Overview

The runtime has every piece a coding agent needs: a workspace with file tools and a shell, todo lists, skills, subagents and commands, project instructions, permissions, compaction, long tool results saved as files and tool_search. createHarnessAgent() puts them together with defaults that work, so you start from a working agent and change what you need.

import { Cogitator, createHarnessAgent } from '@cogitator-ai/core';

const harness = createHarnessAgent({ preset: 'coding', workspace: './my-app' });
const cog = new Cogitator(
  harness.mergeConfig({ llm: { defaultModel: 'anthropic/claude-sonnet-5-5' } })
);

const result = await cog.run(harness.agent, {
  input: 'The add() test fails, fix it',
  ...harness.runOptions,
  onApproval: (request) => askTheUser(request),
});

console.log(result.output, result.todos);
await cog.close();
await harness.dispose();

It is a composition layer and nothing more: the agent is an ordinary Agent, the runtime an ordinary Cogitator. Everything the harness sets is a value you can read, log and change.

Presets

preset picks the defaults, coding when left out:

Piececodingassistant
Workspace (workspace)the working directory of the process./workspace
read_file, write_file, edit_file, list_files, glob, grepyesyes
run_command (shell)yesno
write_todos (todos)yesyes
SKILL.md skills of the workspace (skills)yesyes
Subagents (subagents)Markdown files of agents/ folders and general-purpose, approvals bubble upno
Commands (commands)Markdown files of commands/ foldersno
AGENTS.md and CLAUDE.md (projectInstructions)yesno
Permission engine (permissions)default mode: reads run, edits and commands askthe same
Memory files (memoryFiles)nowhen the runtime has a memory adapter
Compaction (compaction)old tool outputs cleared past 100 000 tokens, the conversation compressed near the windowthe same
Long tool results (toolResults)past 30 000 characters saved to the workspace, the model gets the start and the paththe same
tool_search (toolSearch)your tools deferred once the model would see more than 20 toolsthe same
Parallel read-only calls (parallelToolCalls)yesyes
maxIterations10030

Pieces that are off in a preset stay off unless you turn them on: createHarnessAgent({ preset: 'assistant', shell: true }) is an assistant that runs commands, subagents: false a coding agent that does everything itself.

Options

Every row of the table is an option. false turns a piece off, also in the configuration mergeConfig() merges into (subagents: false drops a subagents section of your cogitator.yml, settings: false its settings files), true turns it on with the preset's defaults, an object changes it:

const harness = createHarnessAgent({
  preset: 'coding',
  name: 'fixer',
  model: 'openai/gpt-6.1-sol',
  instructions: 'You fix bugs in this repository. Keep diffs small.',
  workspace: new DockerWorkspace({
    host: new LocalWorkspace({ root: './repo' }),
    image: 'node:24',
  }),
  tools: [searchIssues, openPullRequest],
  skills: { dirs: ['./skills'] },
  subagents: { agents: { reviewer: { description: 'Reviews a diff', tools: ['Read', 'Grep'] } } },
  permissions: { mode: 'acceptEdits', deny: ['Bash(git push *)', 'Read(.env)'] },
  budget: { session: { maxCostUsd: 5 } },
  toolResults: { maxLength: 20_000 },
  parallelToolCalls: 4,
  timeouts: { toolMs: 120_000 },
  agent: { temperature: 0.2, fallbackModels: ['anthropic/claude-sonnet-5-5'] },
});
  • workspace takes a Workspace (local, Docker, E2B, Daytona, Vercel Sandbox), a directory for a LocalWorkspace, or its options. A workspace you pass stays yours to start and dispose, one the harness made from a path is disposed by harness.dispose(). false leaves the agent without one, and its tools use the run's (RunOptions.workspace).
  • workspaceTools passes options to workspaceTools() (skipDirs, execTimeout, tools), or false for none. tools adds your own: one named like a harness tool (read_file) replaces it.
  • skills takes a SkillRegistry configuration (its dirs are searched before the workspace) or a list of skill sources, defineSkill() skills among them.
  • subagents and commands are merged over the preset's configuration, agents by name: subagents: { approvalMode: 'decline' } keeps the files and general-purpose, and a general-purpose of yours ({ description, model }) is merged over the built-in one.
  • toolSearch counts every tool the model sees against its threshold (20): the harness's file tools, write_todos, the four subagent tools, recall_tool_output and the two skill tools, then yours. Past it, your tools are named in the agent's deferredTools, so their schemas wait for tool_search while the tools themselves stay as they are. keep names tools that are never deferred, true defers yours always, false never.
  • permissions takes the mode and the allow, ask and deny rules of the agent, and the runtime's classifier, settings and "always allow" layers. false turns the engine off: then only a tool that needs approval asks (run_command).
  • memory, hooks, budget and compaction are the runtime's memory, hooks, budget and context, hooks: false and compaction: false drop the base's too. memoryFiles keeps notes in memory files once the runtime has a memory adapter (assistant default), false drops the base's memory.files.
  • agent sets any other field of the Agent, last, over what the harness built.

Trust and settings files

A repository you open may hold subagents, commands, hooks and permission rules of its own. They apply only with trusted: true: subagents and commands of the project's .claude/agents and .claude/commands are read, and with settings: true the hooks and allow rules of its .claude/settings.json. The user's and the organization's managed files are read either way once their piece is on. settings is off by default, as in the Claude Agent SDK. With permissions: false it reads only the hooks, the permission engine stays off.

The project is the directory projectRoot names on this machine, by default the root of a LocalWorkspace, wherever the process runs. Subagents, commands and settings files are read from the host's file system, so:

  • for a DockerWorkspace, pass its host directory as projectRoot
  • for a remote workspace (E2B, Daytona, Vercel Sandbox) there is no project on this machine: without projectRoot (a checkout on the host, say) only the user's and the managed files are read. AGENTS.md and the skills of the workspace are read through the workspace, so they work remotely too
  • projectRoot: false reads no project files for any workspace
createHarnessAgent({ workspace: '.', trusted: true, settings: true });

The System Prompt

The agent's instructions are your instructions (or the preset's), then a short section on what the harness gave it, only for what it has:

  • Workspace: how to find files (glob, grep, list_files), to read before editing, to check changes with run_command, that a long result is saved to a file it can page through, and to batch calls that only read.
  • Planning: when to keep a todo list with write_todos.
  • Subagents: when to hand a task to the agent tool, and that a subagent sees nothing of the conversation.
  • More tools: to look for a tool with tool_search when none fits.
  • Approvals: not to repeat a call that was declined or refused.

It names no paths and no dates, so it stays in the provider's prompt cache. The runtime then adds the project's AGENTS.md and the skills the agent can activate. harnessPrompt: false leaves the section out, harnessPrompt(features) builds it for an agent of your own.

What It Returns

interface HarnessAgent {
  preset: 'coding' | 'assistant';
  agent: Agent; // tools, prompt, workspace, todos, skills, permission mode and rules, limits
  config: HarnessRuntimeConfig; // the Cogitator fields the pieces need
  runOptions: { parallelToolCalls?: boolean; maxParallelToolCalls?: number };
  workspace: Workspace | undefined;
  mergeConfig(base?: CogitatorConfig): CogitatorConfig;
  dispose(): Promise<void>;
}

There are three parts because the runtime reads each setting in one place. An agent carries what is its own (tools, prompt, workspace, permissions), and goes as it is to cog.run(), a server adapter, Studio, a workflow node or agentAsTool(). Subagents, commands, hooks, project instructions, settings files, memory, compaction and the tool result limit are read by the Cogitator (config): it builds the subagent catalog and the hooks once, not per agent. How a run schedules its tool calls is a run option (runOptions), so pass it to each run.

mergeConfig(base) merges config into a configuration of yours, field by field and deep into nested objects, maps such as subagents.agents by key. The preset's defaults (limits.maxToolResultLength, context.clearToolOutputs, the subagents and commands files) give way to base, and what you passed to createHarnessAgent() (toolResults, compaction, subagents, budget) wins over base. Arrays and class instances, such as a memory adapter or a classifier, are replaced, not merged. The assistant preset adds memory files (files: { scope: 'user' }) once the merged memory has an adapter and no files of its own. A project generated by create-cogitator-app merges it into cogitator.yml:

const cog = new Cogitator(assistantHarness.mergeConfig(loadConfig()));

In a Generated Project

The assistant preset of create-cogitator-app (the harness feature) builds its agent with createHarnessAgent({ preset: 'assistant' }) in src/agents/assistant.ts, createAssistant(workspace) lets its tests give it a directory of their own, and the terminal chat asks before every write.

On this page