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:
| Piece | coding | assistant |
|---|---|---|
Workspace (workspace) | the working directory of the process | ./workspace |
read_file, write_file, edit_file, list_files, glob, grep | yes | yes |
run_command (shell) | yes | no |
write_todos (todos) | yes | yes |
SKILL.md skills of the workspace (skills) | yes | yes |
Subagents (subagents) | Markdown files of agents/ folders and general-purpose, approvals bubble up | no |
Commands (commands) | Markdown files of commands/ folders | no |
AGENTS.md and CLAUDE.md (projectInstructions) | yes | no |
Permission engine (permissions) | default mode: reads run, edits and commands ask | the same |
Memory files (memoryFiles) | no | when the runtime has a memory adapter |
Compaction (compaction) | old tool outputs cleared past 100 000 tokens, the conversation compressed near the window | the same |
Long tool results (toolResults) | past 30 000 characters saved to the workspace, the model gets the start and the path | the same |
tool_search (toolSearch) | your tools deferred once the model would see more than 20 tools | the same |
Parallel read-only calls (parallelToolCalls) | yes | yes |
maxIterations | 100 | 30 |
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'] },
});workspacetakes aWorkspace(local, Docker, E2B, Daytona, Vercel Sandbox), a directory for aLocalWorkspace, or its options. A workspace you pass stays yours to start and dispose, one the harness made from a path is disposed byharness.dispose().falseleaves the agent without one, and its tools use the run's (RunOptions.workspace).workspaceToolspasses options toworkspaceTools()(skipDirs,execTimeout,tools), orfalsefor none.toolsadds your own: one named like a harness tool (read_file) replaces it.skillstakes aSkillRegistryconfiguration (itsdirsare searched before the workspace) or a list of skill sources,defineSkill()skills among them.subagentsandcommandsare merged over the preset's configuration,agentsby name:subagents: { approvalMode: 'decline' }keeps the files andgeneral-purpose, and ageneral-purposeof yours ({ description, model }) is merged over the built-in one.toolSearchcounts every tool the model sees against its threshold (20): the harness's file tools,write_todos, the four subagent tools,recall_tool_outputand the two skill tools, then yours. Past it, your tools are named in the agent'sdeferredTools, so their schemas wait fortool_searchwhile the tools themselves stay as they are.keepnames tools that are never deferred,truedefers yours always,falsenever.permissionstakes the mode and theallow,askanddenyrules of the agent, and the runtime'sclassifier,settingsand "always allow" layers.falseturns the engine off: then only a tool that needs approval asks (run_command).memory,hooks,budgetandcompactionare the runtime'smemory,hooks,budgetandcontext,hooks: falseandcompaction: falsedrop the base's too.memoryFileskeeps notes in memory files once the runtime has a memory adapter (assistantdefault),falsedrops the base'smemory.files.agentsets any other field of theAgent, 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 asprojectRoot - 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.mdand the skills of the workspace are read through the workspace, so they work remotely too projectRoot: falsereads 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 withrun_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
agenttool, and that a subagent sees nothing of the conversation. - More tools: to look for a tool with
tool_searchwhen 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.