Skip to content

Agents ​

Reference for the agent layer, hmz.coganchor.agents, and for every coding agent CLI it drives: what each backend is, how a turn of it is run, and what each setting becomes on its command line. A flow never imports this module; the Agent and Session a flow is handed are views the flow runtime makes over it (hmz.runtime.flowing.harnesses.HarnessDriver), and Flows is the flow-level contract.

Terms ​

TermDefinition
BackendOne coding agent CLI, named by the command it is installed as (claude, codex, …), or an ACP CLI added on this machine.
Profilehmz.coganchor.backends.Profile: the facts about one backend that are not code (home, logs, ladders, hosts, ways in). backends.PROFILES holds the built-in ones.
AgentAn instance of a backend's AgentBase subclass: a backend at one config. Holds sessions.
ConfigA frozen AgentConfig subclass: model, effort, rung, account, machine, fence, budget, and backend fields.
SessionOne conversation with the backend, identified by the backend's own id once its first turn lands.
TurnOne prompt and everything the CLI does until it answers, ending on exactly one result event.
RungOne of the four permission levels: read-only, workspace-write, auto, bypass.
EffortHow hard the model is asked to think, in the backend's own words (Efforts).
MomentA point of a turn a hook may be hung on.
FenceWhat the agent's processes may reach (The fence).
HarnessA backend as the flow runtime drives it: HarnessKind names it, HARNESS_AGENTS gives its flow protocol.

Backends ​

hmz.coganchor.agents.DRIVEN maps each built-in backend to its agent and config class.

BackendAccepted asProductTransport
agyagy, antigravityAntigravity CLIone process held open (--input-format stream-json); a print command per shaped or slash-command turn
claudeclaude, claude-codeClaude Codeone claude --print held open per session (stream-json)
codexcodexCodexone codex app-server --stdio per agent, shared by its sessions
cursor-agentcursor-agent, cursor-cliCursor Agentone cursor-agent --print per turn
dshdsh, deepseek-harnessDeepSeek Harnessthe Python SDK in this process, one runtime per agent
grokgrok, grok-build, grokbuildGrok Buildone grok agent stdio held open; grok -p for turns it cannot express
kimikimi, kimi-codeKimi Codeone kimi web daemon per agent, shared by its sessions
litellmlitellma model, called directly (litellm)one chat completion per turn from this process, over the session's history; no CLI, no tools
mcodemcode, minimax, minimax-codeMiniMax Codeone mcode exec per turn
mimomimo, mimocode, mimo-codemimocodeone mimo run per turn
ompomp, oh-my-piOh My Pione omp --mode rpc held open per session
opencodeopencodeopencodeone opencode run per turn
pipipione pi --mode rpc held open per session
qwenqwen, qwen-codeQwen Codeone process held open (stream-json); a command per shaped turn
an ACP CLIits nameany Agent Client Protocol agentone process held open, JSON-RPC over stdio
the person——the interface
BackendAgent classConfig classSession classSession base
agyAntigravityCLIAgentAntigravityCLIAgentConfigAntigravityCLISessionStreamSessionBase
claudeClaudeCodeAgentClaudeCodeAgentConfigClaudeCodeSessionStreamSessionBase
codexCodexAgentCodexAgentConfigCodexSessionSessionBase
cursor-agentCursorAgentCursorAgentConfigCursorSessionCommandSessionBase
dshDshAgentDshAgentConfigDshSessionSessionBase
grokGrokBuildAgentGrokBuildAgentConfigGrokBuildSessionStreamSessionBase
kimiKimiCodeCLIAgentKimiCodeCLIAgentConfigKimiCodeCLISessionSessionBase
litellmLiteLLMAgentLiteLLMAgentConfigLiteLLMSessionSessionBase
mcodeMiniMaxCodeAgentMiniMaxCodeAgentConfigMiniMaxCodeSessionCommandSessionBase
mimoMimoCodeAgentMimoCodeAgentConfigMimoCodeSessionCommandSessionBase
ompOhMyPiAgentOhMyPiAgentConfigOhMyPiSessionStreamSessionBase
opencodeOpencodeAgentOpencodeAgentConfigOpencodeSessionCommandSessionBase
piPiAgentPiAgentConfigPiSessionStreamSessionBase
qwenQwenCodeAgentQwenCodeAgentConfigQwenCodeSessionStreamSessionBase
an ACP CLIAcpAgentAcpAgentConfigAcpSessionSessionBase
the personHumanAgentnone (name= only)HumanSessionSessionBase

All classes are importable from hmz.coganchor.agents.

Installation and versions ​

BackendInstall line (Profile.installs)Driver written against
agynone recorded1.1.28 – 1.2.12
claudenpm i -g @anthropic-ai/claude-code2.1.272 – 2.1.284
codexnpm i -g @openai/codex0.153.4
cursor-agentcurl https://cursor.com/install -fsS | bashnot recorded
dshpip install 'deepseek-harness-sdk>=0.1.5rc1,<0.1.6' 'python-dotenv>=1.2.3' (the [dsh] extra)SDK >=0.1.5rc1,<0.1.6 (backends.DSH_SDK)
groknpm i -g @xai-official/grok1.0.24
kiminpm i -g @moonshot-ai/kimi-code (plus the [kimi] extra)0.42.0
litellmpip install 'litellm>=1.104' (the [litellm] extra)>=1.104 (backends.LITELLM_SDK)
mcodenpm i -g @minimax-ai/code0.5.9
mimonpm i -g @mimo-ai/clinot recorded
ompcurl -fsSL https://omp.sh/install | sh18.2.8
opencodenpm i -g opencode-ainot recorded
pinpm i -g @earendil-works/pi-coding-agent0.84.0 – 0.85.1
qwennpm i -g @qwen-code/qwen-codenot recorded
  • A backend's program is found on PATH, else in ~/.local/bin, /usr/local/bin, /opt/homebrew/bin, /usr/bin, /bin (backends.program).
  • A turn whose CLI cannot be spawned exits 127 and fails with fault missing. Under a flow the session is refused first with HarnessNotInstalled: <cli> is not installed here: <install line>; a backend with no recorded line says install <cli> and put it on PATH.

Defined in src/hmz/coganchor/backends.py (PROFILES, installing, program), src/hmz/coganchor/agents/__init__.py (DRIVEN).

Capability matrix ​

Show only backends that can
15 of 15 backends. Read off what the harness protocols and the drivers declare; nothing runs.
Flow mixins servedDriver
BackendGoalLoopSteerPermissionSubagentAskUserSchema heldForkWeb offRungsFast tierToolsTrace
agyAntigravitynonononononoyesnoyesall 4noyesyes
claudeClaude Codeyesyesyesyesyesyesyes+cwdyesall 4yesyesyes
codexCodexyesnoyesyesyesyesyes+cwdyesall 4yesyesyes
cursor-agentCursor Agentnonononoyesnonononoall 4yesnono
dshDeepSeek Harnessyesnononononononoyesbypassnoyesyes
grokGrok Buildnonononononoyesyesyesall 4noheld turnsyes
kimiKimi Codeyesnoyesyesnoyesno+cwdyesall 4nonoyes
litellma model, called directlynonononononoyes+cwdnoall 4nonoyes
mcodeMiniMax Codenonononoyesnoyesnonono read-onlynonoyes
mimomimocodenononononononoyesyesall 4noyesyes
ompOh My Pinonoyesnonoyesnoyesnoall 4noyesyes
opencodeopencodenononononononoyesyesall 4noyesyes
pipinonoyesnonoyesnoyesnoall 4noyesyes
qwenQwen Codenonononononoyesyesyesall 4noyesyes
an ACP CLIadded on this machinenononononononoif servednobypassnoyesno
ColumnSourceMeaning
GoalGoalCommandAgentMixin in HARNESS_AGENTSa flow may send /goal <objective>: the CLI's own goal feature (Goals)
LoopLoopCommandAgentMixina flow may send /loop <interval> <task>: the CLI's own recurring task (Loops)
SteerSteeringAgentMixina flow may put words into a running turn
PermissionPermissionRequestHookAgentMixina flow may answer the CLI's permission requests
SubagentSubagentStartHookAgentMixin, SubagentStopHookAgentMixina flow is told when the CLI starts and ends agents of its own
AskUserAskUserHookAgentMixinthe CLI's questions to its user reach the flow
Schema heldSessionBase.shapesthe CLI validates the answer against the schema; elsewhere it is asked for in the prompt and validated after
ForkProfile.forks; +cwd is SessionBase.forks_elsewherethe CLI can branch a conversation; +cwd into another directory
Web offProfile.searchesweb_search=False can be said
RungsAgentBase.rungswhich rungs the backend takes
Fast tierAgentBase.service_tiersservice_tier="fast" is served
ToolsSessionBase.takes_tools; FakeToolAgentMixinfake tools can be offered, from Python or from a flow's _tools
Trace_READERS in hmz.runtime.tracing.collectorHmz().epics.trace() reads its logs

The flow mixins are what a role typed as that harness may declare; the driver columns are what hmz.coganchor.agents does. One moment is reachable from Python and from no flow mixin: grok runs PERMISSION_REQUEST. HumanAgent runs no moment at all.

Agent specs (-a) ​

An agent is written [ROLE=]CLI[@ACCOUNT]/MODEL[:EFFORT] wherever one is named: -a, a settings file, Runs, the TUI. backends.read parses one item; -a splits a value on , followed by KEY=.

PartRule
ROLERequired for -a. A Python identifier: a field of the flow's agent collection.
CLIAny alias in Backends, or an added ACP CLI's name. Never contains @.
ACCOUNTAfter @: a provider name. Omitted: the CLI as already signed in. @ with nothing after it is refused.
MODELEverything after the first /, less a trailing :EFFORT. May contain / (kimi-code/k3, opencode/big-pickle) and : (custom_provider:gateway/m). Must be non-empty.
EFFORTAfter the last :, where what follows it is spelled as an effort: words of letters joined by -, _ or a space (high, extra-high, as configured), or nothing. Otherwise -- custom_provider:gateway/m, qwen3:8b -- the : is the model's, and so is everything after it. Left off, or auto, or empty: no rung, stored as "". A model whose own name ends in :<word> is written with its effort after it: qwen3:latest:auto. Checked against the backend's ladder when the agent is built.
InputAgentSpecError (exit 2 from hmz exec)
no ROLE=-a 'claude/m:high': expected <role>=<harness>[@<provider>]/<model>[:<effort>]
a role that is not an identifier-a '9x=claude/m:high': '9x' is not a place a flow could declare: what is written before `=` is a field of the tuple of agents the flow declares, so it is a Python identifier
unknown CLI, no /, empty model-a 'agent=claude': expected [NAME=]CLI[@PROVIDER]/MODEL[:EFFORT]
@ with no account-a 'agent=claude@/m:high': expected an account after @, as in claude@deepseek/MODEL:EFFORT
a role given twice-a: the role 'agent' is given twice
an empty item-a '<value>': an item is empty

An effort off the ladder, an account that does not exist, or a setting the backend cannot carry is refused when the run builds the role's driver, as HarnessUnrecoverable: <spec>: <reason>, the reason being the AgentConfig refusal.

Defined in src/hmz/coganchor/backends.py (read, named), src/hmz/runtime/flowing/specs.py (parse_agents), src/hmz/runtime/flowing/harnesses.py (open_agent).

AgentConfig ​

hmz.coganchor.agents.AgentConfig is a frozen, keyword-only dataclass. Every backend config subclasses it; Backend notes lists each subclass's own fields.

FieldTypeDefaultMeaning
modelstrrequiredThe model, in the backend's spelling (Models).
effortstrrequiredA rung of the backend's ladder; "" or "auto" for none.
service_tierstr"default""default" or "fast" (Service tier).
machineMachineConfig | NoneNoneWhere turns land (Machines).
permissionstr""A rung, or "" for none said.
providerstr""The account; "" for the CLI as signed in.
goalsboolTrueWhether goals and work-past-the-turn tools are available.
web_searchbool | NoneNoneWeb search: on, off, or unsaid.
fenceFence | NoneNoneWhat the agent's processes may reach (The fence).
budgetBudget | NoneNoneWhat each turn may spend (Budgets).

The config is checked twice: by __post_init__ when it is built (ValueError), and by the agent when it is made, reconfigured, or has its effort moved (Unserved, a ValueError; Unfenced, a kind of Unserved).

RefusedError and message
a tier other than default/fastValueError: service_tier must be one of default, fast, not 'slow'
a rung that is not one of the fourValueError: permission must be one of read-only, workspace-write, auto, bypass, not 'all'
an effort off the ladderUnserved: claude cannot be asked to think at 'warp'; expected one of ultracode, max, xhigh, high, medium, low
a tier the backend does not serveUnserved: PiAgent does not support service tier 'fast'; expected default
a rung the backend does not takeUnserved: DshAgent cannot be held to 'read-only'; expected bypass, or left unsaid for the agent as whoever installed the CLI configured it
web_search=False on a backend that cannot be toldUnserved: PiAgent has no way of being told not to search the web; web_search must be on for it
a fence that cannot be heldUnfenced: … (conditions)
backend-specific combinationslisted under each backend in Backend notes

Budget refuses when other than next-response/immediately (when must be one of next-response, immediately, not 'later'), then other than end/fail, and a negative cap (a budget cannot be less than nothing).

Defined in src/hmz/coganchor/agents/config.py, src/hmz/coganchor/agents/base.py.

Models ​

model is passed in the CLI's own spelling.

BackendA model isExample
agy, claude, codex, dsh, grok, qwenthe id the CLI or its endpoint servesclaude-opus-5, gpt-5.6-sol, deepseek-v4-flash
kimiKimi Code's provider/idkimi-code/k3
litellma litellm model string, provider/id; under an account a bare id, sent to the account's provider -- openai/<id> under an OpenAI key, the protocol under a gateway accountopenai/gpt-5, anthropic/claude-sonnet-4-5
pi, omp, opencodeprovider/idopenai-codex/gpt-5.5, deepseek/deepseek-v4-flash, opencode/big-pickle
mimoprovider/id; under a gateway account, the id its endpoint serves (sent as humanize/<id>)xiaomi/mimo-v2.5, gpt-5.6-sol
cursor-agentan id from the account's list, the rung and tier written into itcomposer-2.5-high-fast
mcodeminimax/<id>, or custom_provider:<name>/<id> for an added provider; "" for its configured defaultminimax/MiniMax-M3
an ACP CLIas configured

pi's --provider defaults to google; a bare id is looked up among Gemini models. On a pi gateway account every turn carries --provider humanize-<hash>, so the model is the gateway's own id (Providers › Gateways).

Catalogues ​

hmz.coganchor.models keeps what each account runs in a cache on this machine (Providers › Model catalogues). It is asked when an account is made, when refreshed from the TUI's models sheet, and by the TUI at start-up, in the background, for each installed backend whose machine's-own catalogue is missing or older than 7 days. It is asked as a turn of that account is taken: under its paths, with its variables and without hushed ones.

BackendAsked withEfforts per model
claudeclaude -p --input-format stream-json --output-format stream-json --verbose, sent a control_request list_modelsthe model's supportedEffortLevels
codexcodex debug models (models marked for listing)per model
kimikimi provider list --jsonper model; swarm
litellmnothing: the catalogue litellm ships (model_prices_and_context_window_backup.json, read without importing it), chat models of the providers it has a way in for, as provider/idthe ladder where litellm says the model reasons; none otherwise
agyagy modelsthe ladder
grokgrok modelsthe ladder
cursor-agentcursor-agent --list-modelsthe rungs its listed variants carry (gpt-5.2 at those gpt-5.2-low … are listed for); none for a model with no variants
pipi --list-modelsthe ladder
ompomp models --jsonfor a model that reasons, the rungs omp lists for it, plus off, or the ladder where it lists none; none for a model that does not reason
opencode, mimo<cli> models (lines with a /)the ladder
mcodemcode provider list --json, then MiniMax's own four (minimax/MiniMax-M3, minimax/MiniMax-M3.1-Flash-Preview, minimax/MiniMax-M2.7, minimax/MiniMax-M2.7-highspeed)only MiniMax-M3.1-Flash-Preview takes rungs
dshnothing: deepseek-v4-flash, deepseek-v4-prothe ladder
qwennothing: qwen3-coder-plus, qwen3-coder-flashthe ladder
an ACP CLInothing: one row, as configuredas configured

Where the account sets the backend's endpoint variable, the endpoint is asked first:

BackendEndpoint variable
agyGOOGLE_GEMINI_BASE_URL
claudeANTHROPIC_BASE_URL
codexCODEX_PROVIDER_URL
dshDEEPSEEK_BASE_URL
grokGROK_XAI_API_BASE_URL
kimiKIMI_MODEL_BASE_URL
litellmLITELLM_GATEWAY_URL
qwenOPENAI_BASE_URL

The request is GET {base}/v1/models ({base}/models when the base ends in a version); every listed id is offered at the backend's whole ladder. See Providers › Gateways.

A Claude Code session whose system/init names a model other than the one asked for (not the model, the model dated, an alias opus, sonnet, haiku, default, best, opusplan with or without [1m], nor a catalogue name) emits one notice per session: Claude Code does not know the model '<asked>' and is running <actual>, its own default, in its place. The turn continues.

Defined in src/hmz/coganchor/models.py.

Efforts ​

The effort is checked against Profile.efforts (plus Profile.beyond) where the agent is made and wherever it is moved. auto is no rung: every backend takes it, and nothing is said to the CLI. An ACP CLI's ladder is the single word as configured and any effort is accepted.

BackendLadder, hardest firstHow it is sent
agyhigh, medium, low--effort, only where the model name carries no rung (gemini-3.7-flash-high runs at its own rung)
claudeultracode, max, xhigh, high, medium, low--effort. ultracode (in beyond: taken, never listed) is xhigh with the turn opted into orchestrating a fleet.
codexultra, max, xhigh, high, medium, lowper turn on the app server; ultra/max only on models that list them
cursor-agentmax, xhigh, extra-high, high, medium, low, minimal, nonewritten into the model id: <model>-<rung>, where the account lists that id
dshmax, high, low, offwritten onto the SDK composition
grokxhigh, high, medium, low--effort
kimimax, high, medium, low, each also swarm-prefixedper turn on the daemon; swarmmax is max run as a fleet of subagents (agents.SWARM == "swarm")
litellmhigh, medium, low, minimal, nonereasoning_effort on the call; litellm leaves it off for a model with none
mcodemax, xhigh, high, medium, low--effort, only where there is a rung; a rung the account's catalogue does not list for the model is Unserved
mimo, opencodexhigh, high, medium, low, minimal--variant
ompmax, xhigh, high, medium, low, minimal, off--thinking, only where there is a rung. off is also in beyond, so it stays on a model's ladder however its catalogue narrows it
pimax, xhigh, high, medium, low, minimal, off--thinking; pi clamps a rung the model lacks to the nearest it has
qwenmax, xhigh, high, medium, low, nonemodel.reasoningEffort in its virtual home's copy of settings.json

Moving the effort while it runs ​

agent.effort = "low" moves every session of the agent; session.effort = "max" moves one; session.effort = "" returns it to the agent's. agent.config.effort stays as configured. The change takes hold on the next turn; the running turn keeps its effort. The new effort is checked as a configured one is.

BackendHow a moved effort takes hold
codex, kimi, opencode, mimo, cursor-agent, mcode, litellmsent with the next turn
claude, agy, grok, qwenthe process is restarted and resumes the same conversation
dshthe runtime is restarted before the first turn; after it the turn is refused (Unrecoverable), see DeepSeek Harness
pi, ompa command to the held process, between turns

Defined in src/hmz/coganchor/backends.py (Profile.efforts, Profile.beyond, Profile.takes, AUTO, SWARM).

Service tier ​

Backendservice_tier="fast"
claudefastMode: true in the --settings literal; at default nothing is sent
codexthe priority service tier
cursor-agentthe -fast id the account lists for the model (composer-2.5-fast); Unserved where none is listed
every other backendUnserved

The field records the tier asked for; the provider decides the tier served.

Permission rungs ​

AgentConfig.permission is one of four rungs, loosest last, or "":

RungMeaning
read-onlymay read anything and change nothing
workspace-writemay change the workspace it was given
automay reach for anything; what it asks for is granted
bypassnothing is asked and nothing is checked
""nothing is said to the CLI: no mode, sandbox or approval flag

What each rung becomes, in the CLI's own terms (—: the same as the rung to its left; refused: Unserved):

Backendread-onlyworkspace-writeautobypass
agyplan mode, four read toolsaccept-edits modeskip permissions—
claudeplanacceptEditsautobypassPermissions; acceptEdits where the account or root forbids it; manual with asks
codexsandbox read-onlysandbox workspace-writesandbox workspace-write, approval on-requestsandbox danger-full-access
cursor-agentplansandbox onauto-reviewsandbox off
dshrefusedrefusedrefusednothing sent
grokthree read toolsweb search offevery tool—
kimimanual and plan modeautoyoloauto
litellmnothing sent———
mcoderefusedfullsmartfull
mimo, opencodeedit, bash deniedweb tools deniednothing denied—
ompalways-ask, every approval answered noyolo——
pifour tools withheldnothing withheld——
qwenfive tools withheldweb_fetch withheldnothing withheld—
an ACP CLIrefusedrefusedrefusedevery request granted

How it is sent:

BackendFlags or protocol
agy--mode plan --agent hmz-read-only; --mode accept-edits; --dangerously-skip-permissions
claude--permission-mode <mode>; at bypass also --permission-prompt-tool stdio, each request answered allow unless a PERMISSION_REQUEST hook refuses
codexsandbox and approvalPolicy on thread/start, thread/resume and turn/start; approval never at every rung but auto
cursor-agent--mode plan; --force --sandbox enabled; --auto-review; --force --sandbox disabled
grok--always-approve, plus --tools read_file,grep,list_dir at read-only and --disable-web-search at workspace-write
kimithe session's permission mode on the daemon, and plan mode at read-only
mcode--permission full, --permission smart
mimo, opencodethe MIMOCODE_PERMISSION / OPENCODE_PERMISSION table, plus --dangerously-skip-permissions / --auto
omp--approval-mode always-ask at read-only; --approval-mode yolo at workspace-write, auto and bypass
pi--exclude-tools bash,edit,write,powershell at read-only
qwen--approval-mode yolo, plus --exclude-tools edit,monitor,notebook_edit,run_shell_command,write_file at read-only or --exclude-tools web_fetch at workspace-write

Per-backend rules:

  • agy: --mode plan alone does not stop writes (agy 1.2 writes in it), so read-only also starts the turn as --agent hmz-read-only, an agent file Humanize writes into the agents/ of the turn's virtual home whose only tools are view_file, grep_search, find_by_name and list_dir. That file exists only on this machine, so read-only or web_search=False on another machine is Unserved: agy cannot run at read-only or off the web on another machine: what holds it there is an agent of Humanize's that is a file on this one. At workspace-write edits pass and commands are soft-denied (named in denied_actions).
  • claude: bypass is bypassPermissions. An account whose managed settings carry "disableBypassPermissionsMode": "disable" starts that turn at default and says so in its system/init; the driver then moves the process to acceptEdits with a set_permission_mode control request, starts every later process of the agent on that account at acceptEdits, and says once: claude: this account will not run an agent at bypass, so it runs at acceptEdits, where what it asks for is granted. The same holds where Claude runs as root without IS_SANDBOX=1: it exits before the turn with --dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons, and the turn is taken again at acceptEdits. With ClaudeCodeAgentConfig.asks, bypass runs at manual instead, so that every tool that would change something reaches a PERMISSION_REQUEST hook. Whatever is asked at bypass is answered allow unless such a hook refuses. The CLI still enforces an organisation's deny list.
  • codex: the approval policy is never at every rung but auto. On a machine whose requirements forbid danger-full-access, bypass runs at auto and says once: codex: this machine will not run an agent at bypass, so it runs at auto, where what it asks for is granted. CodexAgentConfig.approvals replaces the rung's approval policy.
  • kimi: Kimi's modes are, loosest last, manual, yolo, auto. At read-only plan mode vetoes Write, Edit, TaskStop and the cron tools, and manual turns everything else, Bash included, into an approval the driver answers no; ExitPlanMode is an approval too. Kimi auto denies AskUserQuestion, so at workspace-write and bypass no question and no NOTIFICATION occur.
  • mcode: has no rung that changes nothing and no sandbox. smart is its own reviewer, which fails the turn where it would have asked. read-only is refused; a flow holds it read-only with a fence that writes nothing.
  • mimo, opencode: the rung goes in a permission table (OPENCODE_PERMISSION / MIMOCODE_PERMISSION); any rung also adds --auto (opencode) or --dangerously-skip-permissions (mimo), unless unattended says otherwise.
  • omp: no sandbox. At read-only omp asks before every tool it does not class as a read, and the driver answers each such approval no without asking anyone. workspace-write, auto and bypass are one agent. With no rung no flag is sent and omp's own configuration decides; an approval it then asks for reaches the person as a question.
  • pi: no permission gate and no sandbox; workspace-write, auto and bypass are one agent.
  • qwen: always --approval-mode yolo; its asking modes would leave a held-open session waiting on an approval nothing can answer. Nothing confines an edit to the workspace.
  • dsh, ACP: the SDK exposes no per-session sandbox or approval control; an ACP CLI's only permission word is a per-call question, which is granted.

Flow permission to rung ​

Under a flow the rung is derived from the role's Permission by hmz.runtime.flowing.harnessing.rung(), and nothing is put to anybody for approval:

  1. local of READ or NONE gives read-only; local of ALL gives bypass.
  2. If that is not read-only and the harness is in ASKS and a hook in its ASKS set is hung, it becomes auto. ASKS is {kimi: {PERMISSION_REQUEST, ASK_USER}}.
  3. A rung the backend does not take becomes bypass (dsh, mcode, ACP CLIs).
localevery harness but dsh, mcode, ACPdsh, mcode, ACP
NONE, READread-onlybypass
ALLbypass (auto for kimi with a PermissionRequest or AskUser hook hung)bypass
  • user, system and online are held by the fence, not the rung; so is local on dsh, mcode and ACP CLIs.
  • Claude Code with a PermissionRequest hook hung is given asks=True (harnessing.prompting), so bypass runs at manual and every tool that would change something reaches the hook. It takes hold from the session's next turn (the process is restarted).
  • Codex with a PermissionRequest hook hung runs approval policy untrusted over its rung's sandbox (harnessing.approvals). With an AskUser hook hung it is started with the feature default_mode_request_user_input (harnessing.ASKING_FEATURE). Both take hold from the session's next turn (the app server is restarted).
  • Codex read-only under a fence, or where bubblewrap cannot get a user namespace, starts the app server with --enable use_legacy_landlock (asked once per process by running codex sandbox … -- true). Where the fence leaves the network on, each read-only turn is sent sandboxPolicy: {"type": "readOnly", "networkAccess": true}. On macOS, where Codex's own Seatbelt sandbox cannot start inside the fence's, each read-only or workspace-write turn under a fence is sent sandboxPolicy: {"type": "externalSandbox", "networkAccess": "enabled"|"restricted"} instead, the fence holding the rung.
  • online also decides the CLI's own web tools (Web search).

Defined in src/hmz/runtime/flowing/harnessing.py (rung, ASKS, approvals, prompting, searching, fenced), and each driver's _PERMITTED table.

Web search ​

web_searchMeaning
None (default)nothing said; the CLI searches as it would when run by hand
Trueon, said even to a CLI that ships with search off
Falseoff; Unserved on a backend with Profile.searches == False
BackendHow it is said
claude--disallowedTools WebSearch,WebFetch when off, or when the fence cuts the network
codex-c web_search="live" or "disabled"; disabled whenever the fence cuts the network
dshthe dsh-web plugin, its providers and dsh-tool-web mounted when on
grok--disable-web-search when off, or when the fence cuts the network
kimidisabled_tools on each prompt: WebSearch, FetchURL when off, or when the fence cuts the network
qwen--exclude-tools web_search,web_fetch when off, or when the fence cuts the network
opencodewebfetch: deny, websearch: deny in the permission table
mimothe same, and codesearch: deny
agy--agent hmz-offline (default tools less read_url_content, search_web and the subagent tools) when off or when the fence cuts the network; at read-only, hmz-read-only-web when on
cursor-agent, mcode, omp, pi, an ACP CLIcannot be told: False is refused

Under a flow, online of ALL (the default) is True and NONE is False, on a backend that can be told; elsewhere None. A command the agent runs reaches the network regardless of this setting; only the fence stops that.

The fence ​

AgentConfig.fence is what the agent's processes may reach: the CLI's own process and every command it runs. A flow sets one on every session from the role's Permission (harnessing.fenced).

hmz.coganchor.fence.Fence (frozen):

FieldTypeMeaning
readtuple[str, ...]absolute paths that may be read, listed and executed
writetuple[str, ...]absolute paths that may be read and changed; a path in both is written
onlineboolTrue: the network is unrestricted. False: cut, except through a proxy to hosts
hoststuple[str, ...]what the proxy passes while online is False: host, *.suffix, or host:port
tmpstrthe scratch directory, exported as TMPDIR, TMP, TEMP; "" for one made per run
scopestuple[str, ...](local, user, system) levels it was drawn from, or () for one drawn path by path
listentuple[int, ...]TCP ports that may be bound while online is False, on loopback

Fence.open is true when / is in write and online is true: nothing is put around the CLI.

Fence.of(*, local, user, system, online, workdir, home, cwd="", hosts=(), read=(), write=()):

  • Each scope is a root (system is /, user is home, local is workdir and cwd) and a level: read puts the root in read, all in write, none leaves it out.
  • local="all" also writes TEMPORARY (/tmp, /var/tmp) and $TMPDIR where it is absolute. user="all" also writes / for any user but root, the files' permissions holding it; for root, TEMPORARY, $TMPDIR and /run/user/0.
  • The levels must nest, local >= user >= system: `ValueError: scopes must nest, local >= user

    = system; got local=read, user=all, system=none. A level outside none/read/all: ValueError: a scope is one of none, read, all, not 'x'`.

  • Always added to read: /usr, /bin, /sbin, /lib, /lib32, /lib64, /libx32, /etc/ssl, /etc/ca-certificates, /etc/pki, /etc/resolv.conf, /etc/hosts, /etc/host.conf, /etc/gai.conf, /etc/nsswitch.conf, /etc/passwd, /etc/group, /etc/localtime, /etc/timezone, /etc/os-release, /etc/ld.so.cache, /etc/ld.so.conf, /etc/ld.so.conf.d, /etc/alternatives, /sys, and the Python running Humanize.
  • Always added to write: /dev/null, /dev/zero, /dev/full, /dev/random, /dev/urandom, /dev/tty, /dev/pts, /dev/ptmx, /dev/shm, /proc (a thread named by another is named through /proc/self/task/<tid>/comm, which CUDA's cuInit does), every /dev/nvidia*, /dev/dri, /dev/kfd, /dev/infiniband.

The agent widens the fence where it spawns a turn (agent.fenced()): its CLI's state and sign-in directories and its session directory to write, and on macOS ~/Library/Keychains, where Claude Code keeps its sign-in and rewrites it as it refreshes; the account's directory; the hosts its model and sign-in are at under the account (Network hosts); the skills it carries; its programs and their install trees to read.

Both a flow's fence and the agent's widening read the hosts from the same environment, providers.composed(provider, profile): this process's own, less the variables the account hushes, plus the ones it sets. A variable the turn runs without, such as an ANTHROPIC_BASE_URL left in the shell under an account that does not set one, opens no host.

Enforcement ​

agent.natively(fence) returns what the CLI does not enforce itself. That remainder is held from outside by putting python -Pm hmz internal fence --policy=<Fence JSON> -- outermost around the turn:

MechanismHolds
Landlock (Linux)paths, and TCP connect/bind by port (ABI >= 4 where the network is cut)
seccomp filter (Linux)every non-TCP socket family refused
seccomp user notification + pidfd_getfd (Linux)with online=False, every bind and listen stops for the wrapper, which allows loopback only (EACCES otherwise)
Seatbelt (macOS)paths (as given and as resolved, /tmp being /private/tmp); with online=False, outgoing TCP to the proxy's port on localhost only, every other outgoing connection and the system resolver's socket refused, and listening allowed on localhost, which Seatbelt also matches for every address and every port
proxy on loopbackwith online=False, the only way out: handed to the CLI as HTTPS_PROXY and related variables, passing only hosts (ports 443 and 80 for a host without a port)

The minimum granted whatever the scopes is LINUX_SYSTEM and LINUX_DEVICES on Linux, and DARWIN_SYSTEM (/usr, /bin, /sbin, /System, /private/var/select, /private/var/db/timezone and the handful of files under /etc a program reads to start) and DARWIN_DEVICES on macOS, where the root directory itself is also readable and the pseudo-terminals /dev/ttys* writable. Seatbelt also lets the current user's mds directory under DARWIN_USER_CACHE_DIR be written: the Security framework locks its module database there to initialize TLS, and a CLI refused it fails before its model starts.

No built-in CLI enforces any part natively: every driver's natively returns the whole fence, for the reasons below. litellm returns none of it: a turn is one request from this process and the model has no tool to reach anything with, so nothing is put around it.

BackendWhy its own confinement is not used
agy--sandbox confines commands only (its file tools run outside it), cannot start without unprivileged user namespaces, and its network lists are desktop-app settings
codexits sandbox holds the commands its agent runs, not Codex's own process, MCP servers or hooks; online=False also sends -c web_search="disabled", --disable apps, and --disable respect_system_proxy where codex features list names it
cursor-agentcursorsandbox wraps shell commands only and needs a user namespace; $XDG_CONFIG_HOME/cursor (else ~/.config/cursor) is granted to write
dshthe bundle has no confining shell executor; the runtime the SDK launches is wrapped, its composition file and native module cache (PKG_NATIVE_CACHE_PATH) put in the fence's tmp
grokits sandbox profiles write /tmp and /var/tmp, block only commands' network, and fail without user namespaces on 1.0.24
kimi0.42 has no sandbox; the daemon is fenced once, when it starts
mimo, opencodeneither confines its shell; the permission table additionally denies edit outside writable paths, read/external_directory outside readable ones (only where system is NONE), and web tools where online is NONE
ompno sandbox; no --offline under online NONE
pino gate and no sandbox; --offline under online NONE
qwen--sandbox is a container or Seatbelt; its rules hold its own tools only
an ACP CLInothing is known of it; declared hosts and state under clis in settings.yaml are granted

When a fence is refused ​

Unfenced is raised where the config arrives (construction, reconfiguration) when:

ConditionMessage
this machine has no Landlock (Linux < 5.13, or not in the lsm= list)<Agent> cannot be held to its permission on this machine: it does not enforce it natively, and fencing it from outside needs Landlock (Linux 5.13 or later, with landlock in its lsm= list)
macOS, where sandbox-exec cannot apply a profile (Humanize itself already runs inside a sandbox)… needs Seatbelt, which a process already inside a sandbox cannot apply
online=False and no Landlock ABI 4, no seccomp notification, or pidfd_getfd refused (a container's default seccomp profile, Yama ptrace_scope >= 2)… needs Landlock ABI 4 (Linux 6.7 or later) and seccomp to cut the network
a fence drawn path by path on an agent whose machine is set<Agent>: a fence drawn path by path cannot be held on another machine
a harness on another machine<Agent>: a fence cannot hold a harness that runs on another machine
online=False with the anchor's net="remote"<Agent>: an agent whose own connections are sent to the target cannot have its network cut here
cursor-agent with online=Falsecursor-agent: this permission cuts the network, and cursor-agent's web search and web fetch run on Cursor's own servers, through the same hosts as its model, where nothing here can tell them apart or switch them off; grant it online ALL to use cursor-agent
mcode with online=Falsemcode: this permission cuts the network, and mcode's web search runs on MiniMax's own service, through the same hosts as its model, where nothing here can take it away; grant it online ALL to use mcode
grok with leader=True and a non-open fenceGrokBuildAgent: a fenced conversation cannot join the leader, which runs its tools outside the fence; set leader to False or None

Under a flow, Unfenced becomes HarnessSandboxed. An ACP CLI with online NONE and no declared hosts is refused HarnessSandboxed, naming what to declare. For anchored agents, what each machine holds is in Remote execution › A fence on both machines.

Gaps are the kernel's: neither Landlock nor Seatbelt governs connecting to a Unix socket (on macOS, nor asking a system service over Mach); on Linux with the network cut the proxy's port is reachable on any address, by number; and on macOS with the network cut a program may listen on any address and port, Seatbelt telling neither apart.

Network hosts ​

Profile.hosts: what a fenced turn with online=False still reaches. The account adds its endpoint's host and cloud hosts (Providers › Hosts reachable under a cut network).

BackendHosts
agycloudcode-pa.googleapis.com, daily-cloudcode-pa.googleapis.com, oauth2.googleapis.com, www.googleapis.com, generativelanguage.googleapis.com, lh3.googleusercontent.com
claudeapi.anthropic.com, platform.claude.com, claude.ai
codexchatgpt.com, auth.openai.com, api.openai.com
cursor-agent*.cursor.sh (a cut network is refused anyway)
dshapi.deepseek.com
grokcli-chat-proxy.grok.com, auth.x.ai, api.x.ai
kimiapi.kimi.com, auth.kimi.com, api.kimi.ai, auth.kimi.ai, api.moonshot.ai, api.moonshot.cn
mcodeagent.minimax.io, agent.minimaxi.com, agent.minimax.cn, api.minimax.io, api.minimaxi.com, account.minimax.io, account.minimax.cn (a cut network is refused anyway)
mimoapi.xiaomimimo.com, token-plan-cn.xiaomimimo.com, token-plan-sgp.xiaomimimo.com, token-plan-ams.xiaomimimo.com, and for its vendor-key ways api.anthropic.com, api.openai.com, generativelanguage.googleapis.com, api.x.ai, openrouter.ai, api.deepseek.com; plus the options.baseURL of the provider mimocode/mimocode.json (under $XDG_CONFIG_HOME) declares for the model
ompapi.anthropic.com, platform.claude.com, chatgpt.com, auth.openai.com, api.openai.com, api.github.com, api.individual.githubcopilot.com, api.x.ai, auth.x.ai, api.kimi.com, auth.kimi.com, openrouter.ai, generativelanguage.googleapis.com, api.deepseek.com, api.groq.com, api.mistral.ai; plus the baseUrl of each provider omp's own models.yml (or models.yaml) declares for the model
opencodeopencode.ai, chatgpt.com, auth.openai.com, api.githubcopilot.com, api.anthropic.com, api.openai.com, generativelanguage.googleapis.com, api.x.ai, openrouter.ai, api.deepseek.com, api.mistral.ai; plus the options.baseURL of the provider opencode/opencode.json (under $XDG_CONFIG_HOME) declares for the model
piapi.anthropic.com, platform.claude.com, chatgpt.com, auth.openai.com, api.openai.com, api.github.com, api.individual.githubcopilot.com, api.x.ai, auth.x.ai, api.kimi.com, auth.kimi.com, openrouter.ai, generativelanguage.googleapis.com, api.deepseek.com, api.groq.com, api.mistral.ai; plus the baseUrl of each provider pi's own models.json declares for the model
qwendashscope.aliyuncs.com, dashscope-intl.aliyuncs.com, coding.dashscope.aliyuncs.com, coding-intl.dashscope.aliyuncs.com, token-plan.cn-beijing.maas.aliyuncs.com, token-plan.ap-southeast-1.maas.aliyuncs.com, generativelanguage.googleapis.com, aiplatform.googleapis.com, oauth2.googleapis.com
an ACP CLIits declared hosts

Defined in src/hmz/coganchor/fence/ (Fence, enforceable, wrapper, proxy, loopback), src/hmz/coganchor/agents/base.py (fenced, natively, _abroad), src/hmz/coganchor/backends.py (Profile.hosts, reachable).

Sessions and turns ​

python
agent(prompt)                       # a turn in a session nobody keeps
session = agent.new(cwd=None)       # nothing is opened with the backend yet
session(prompt)                     # opens the conversation, then resumes it
  • Both calls return the answer, stripped (or the schema model, with schema=).
  • session.id raises RuntimeError: session has not run a turn yet until a turn has landed; session.named is None instead. On claude, codex, dsh, kimi, omp and pi it is set as soon as the backend names the session, during the first turn; elsewhere when that turn lands.
  • A session runs one turn at a time; concurrent callers of one session are serialised.
  • An agent holds its sessions weakly; dropping a session closes it.
  • session.close() ends whatever holds the conversation open.

Failures ​

A failed turn raises Failed (a subprocess.CalledProcessError) and leaves the session unopened, so the next call retries rather than resuming something that may not exist. The message ends with what the CLI said and, where classified, (<fault>: <fix>).

AttributeMeaning
Failed.faultone of backends.FAULTS, or ""
Failed.fixwhat to do about it, or ""
FaultMeaning
contendedtwo turns at one local store (database is locked, SQLITE_BUSY), or at one sign-in that refreshes itself the other way round (signs in with a token that refreshes itself)
spenta quota or balance used up, said in so many words (quota, insufficient balance, credit balance is too low, billing); read ahead of throttled, since it comes as the same 429
throttledtoo many requests (429, 529, rate limit, resource exhausted, overloaded, usage limit, …)
refusedthe credential (401, 403, unauthorized, invalid api key, not logged in, token expired, token was revoked, forbidden, …)
unlistedthe model is not this account's (not allowed to access model, can only access models, is not supported when using, …)
retiredthe model is gone (404, model not found, unknown model, …)
missingnothing to run: exit 126 or 127
sandboxedthe CLI could not start its own sandbox (bwrap: , cannot create … namespace, …)
unmirroredthe copy of another machine's work a harness here keeps could not be made at its path (cannot keep the local copy of the work at); read first of all, ahead of the Permission denied it usually carries
fencedthe run's network fence kept it off a host (is not a host this run may reach, the fence proxy's 403); read ahead of the credentials
killeda signal, or exit 129–192, or out of memory, SIGKILL, segmentation fault, …
droppedthe wire (ECONNRESET, broken pipe, fetch failed, 502/503/504, timed out, …); last of all, Codex's stream disconnected before completion and responseStreamDisconnected, which it also wraps other failures in (not on a line that names content_filter)

Classification (backends.trouble) reads the last 4096 characters of each stream, the backend's own Profile.signs first, then the shared backends.SIGNS in order; the first match wins. Backend-specific signs: dsh needs a DeepSeek API key → refused; grokcouldn't set model.*unknown model id → throttled; mcode sign in to minimax and oauth bearer is not synced → refused. For agy, whose streams say nothing useful, the newest of cli.log / log/cli-*.log written within the turn's duration (at most 300 s, last 64 KiB) is read last (Profile.journal).

Also raisedWhen
Unrecoverable (a Failed)no retry could change it: a context-window overflow, a session id the backend will not resume, a budget spent with then="fail". Never retried, never carried to another account.
Stoppedthe agent was stopped. Not a CalledProcessError.
NotImplementedErrorthe backend lacks the feature (pursue without goals, interject without steering, fork without forks, offers without tools, shelves without a shell)

suppress=True returns "" (or None with a schema) for a Failed or a schema mismatch, and nothing else.

Forks ​

session.fork(*, into=None, cwd=None) returns an unopened child that carries the session's history; its first turn performs the fork.

BackendFork mechanismInto another cwd
claude--resume <parent> --fork-sessionyes: the transcript is copied to where --resume looks
codexthread/forkyes
kimikimi forkyes
grokgrok -p --resume <parent> --fork-sessionno
opencode, mimorun --session <parent> --forkno
pi--fork <parent>no
omp--fork <parent>no
qwen--resume <parent> --fork-sessionno
an ACP CLIsession/fork, where the agent serves itno
litellmthe conversation's file copied under the child's id by its first turnyes
agy, cursor-agent, dsh, mcodenone—
  • Forking a session that has not landed a turn: RuntimeError: session has not run a turn yet.
  • A backend with no fork: NotImplementedError: <cli> has no way of carrying a conversation into a second one. cwd= elsewhere where unsupported: NotImplementedError.
  • into= another agent of the same backend, account and machine; anything else is ValueError.
  • The child carries the session's effort, budget, skills and callbacks.
  • A child driven after its parent has taken another turn raises RuntimeError.
  • session.forks says whether this backend forks. Under a flow, agent.fork(session), cut by its first run(…, env=…); a fork onto another machine is UnsupportedOperation: <cli> cannot fork a session onto another machine. A session whose turn is given another workdir is carried there the same way, as a fork of itself, where the backend forks elsewhere.
  • agent.recall(session_id, kept, cwd=None) returns a session holding a conversation kept somewhere else -- an earlier run's sessions/<cli>/, or a copy of one -- which takes no turn of its own; its fork() carries it on. As that fork's first turn starts, every file under kept with the conversation's id in its path is copied into agent.kept() where it sat, with those of every conversation it was cut from up the forked_from_id its first line names (Codex), and the files they came from are left as they were. Each is linked into place whole, and only where nothing is there yet: a file agent.kept() holds already is never replaced, and one that differs refuses that turn before anything is copied, with a RuntimeError naming the CLI, the conversation and the copy already kept, which carrying this one on would replace. A Claude Code fork in another directory is cut from the transcript brought in, copied to where Claude runs for that directory: on an agent whose turns land on another machine, this machine's mirror of it; on one whose Claude runs there, natively or under a harness elsewhere, NotImplementedError. A backend with no fork: NotImplementedError; nothing of the conversation under kept: a RuntimeError naming the conversation and where it was looked for.
  • session.keep(into) is the other half: it copies the files recall would bring in -- the conversation's own, lineage and all, and nothing else of agent.kept() -- into into, as they stand, leaving them as they were. Before any turn has landed, or with nothing of the conversation kept: RuntimeError; a backend with no fork: NotImplementedError. Under a flow, both are what a session written into a flow's state comes to.

Working directory ​

agent.new(cwd), agent(prompt, cwd=…), agent.pursue(…, cwd=…), agent.batch(…, cwd=…) and agent.batch_new(n, cwd) open sessions at a directory; None is the directory the process (or flow) is in. session.cwd is the absolute path as the machine it lands on names it. For an anchored agent it is that machine's path and must be inside the anchor's workspace. Before the first turn:

text
/srv/nowhere: no directory to open a session in
/tmp/elsewhere is not inside /srv/project, which is the workspace this agent's turns land in

A project's own programs ​

Profile.launchers lists the files in a project that make a CLI start a program. Each entry is a path relative to the session's directory, plus dotted key paths into that file (* is any one key or item). A path ending in / is a directory of plugins. An anchored session whose CLI runs on another machine than its work is refused while any of these is set (Remote execution).

BackendFiles and keys
claude.claude/settings.json, .claude/settings.local.json: hooks.*.*.hooks.*.command, statusLine, subagentStatusLine, fileSuggestion, apiKeyHelper, awsAuthRefresh, awsCredentialExport, gcpAuthRefresh, otelHeadersHelper, enabledPlugins.*, extraKnownMarketplaces; .mcp.json: mcpServers.*.command, mcpServers.*.headersHelper
agy.agents/mcp_config.json: mcpServers.*.command; .agents/hooks.json: *.*.*.hooks.*.command; .agents/plugins/
codex.codex/config.toml: mcp_servers.*.command, mcp_servers.*.http_headers_helper, hooks; .codex/hooks.json: hooks.*.*.hooks.*.command
cursor-agent.cursor/hooks.json: hooks.*.*.command; .cursor/mcp.json: mcpServers.*.command
grok.grok/config.toml: mcp_servers.*.command, plugins; .grok/hooks/; .claude/settings.json, .claude/settings.local.json: hooks.*.*.hooks.*.command
kimi.kimi-code/mcp.json: mcpServers.*.command
pi.pi/settings.json: extensions, packages, npmCommand; .pi/mcp.json: mcpServers.*.command; .pi/extensions/
omp.omp/settings.json, .omp/config.yml: extensions; .omp/mcp.json, .omp/.mcp.json, mcp.json, .mcp.json: mcpServers.*.command; .omp/extensions/; hooks/ and tools/ under .omp, .claude, .codex, .gemini
qwen.qwen/settings.json: hooks.*.*.hooks.*.command, mcpServers.*.command, tools.discoveryCommand, tools.callCommand, ui.statusLine.command
opencodeopencode.json(c), .opencode/opencode.json(c): mcp.*.command, plugin, formatter.*.command, lsp.*.command; .opencode/plugins/, .opencode/plugin/
mimothe same as opencode, named mimocode
mcode.mcp.json: mcpServers.*.command, mcpServers.*.headersHelper
dsh, litellmnone

Async and batches ​

aturn, apursue and abatch are awaitable twins with the same arguments; each turn runs on a thread of its own. Two awaited turns on one session run one after the other; on two sessions, concurrently.

agent.batch(prompts, *, suppress=False, schema=…, at_once=0, cwd=None) runs one fresh session per prompt, at_once at a time (0: all at once), answers in order. Without suppress it raises the first failure after every turn has landed. batch_new(count, cwd=None) opens sessions without running a turn.

Schemas ​

schema=Model (a pydantic model) makes the turn answer with an instance.

BackendHow the shape is held
claude, agy, grok, qwen--json-schema (a separate print/command turn on agy, grok, qwen; a process restart on claude)
mcode--output-schema
codexoutputSchema on the turn
every otherthe schema is put in the prompt; the answer is validated after

grok and mcode close every object (additionalProperties: false, every property required, no defaults), as Codex requires. An answer that does not validate raises ValueError; suppress=True returns None.

Events ​

session.stream(prompt, *, schema=None) yields Event(kind, text, whose="", tokens={}, spent=Usage()).

kindMeaning
textthe agent talking; one whole utterance
reasoningthe agent thinking aloud, where the backend reports it (opencode and mimo with thinking=True)
toola tool reached for; where arguments stream, sent at the first fragment naming the path or command
subagent, subagent-endsbracket an agent this one started; whose pairs them
tookan interjected word is now in front of the model
resultthe answer; exactly one closes each turn; carries tokens (per model) and spent
failedthe turn closed the other way

agent.watch(listener) registers listener(agent, session | None, event) for every session, which also sees begins, ends (bracketing a turn), asks (a question) and notice (Humanize itself: a rate limit waited out, an account moved, a turn cut off, a wedged backend ended). With no watcher, a notice is written to stderr. A watcher that raises does not fail the flow.

claude alone announces a tool while its arguments are still being written (session.narrates, --include-partial-messages, ClaudeCodeAgentConfig.partial_messages).

Defined in src/hmz/coganchor/agents/base.py, src/hmz/coganchor/agents/event.py, src/hmz/coganchor/backends.py (FAULTS, SIGNS, trouble, journalled).

Steering ​

session.interject(text) puts words into the running turn; a took event follows once they are in front of the model. session.steers says whether the backend can.

BackendMechanismBetween turns
claudea user message written on the held streamaccepted while the process is up; RuntimeError: no turn is running to be talked to before the first turn
codexturn/steerRuntimeError
kimiqueued on the daemon, then steered inRuntimeError
pi, ompan RPC steer commandaccepted while the process is up
every othernone: NotImplementedError: <Session> cannot be talked to mid-turn

An anchored session's process ends with each turn, so an anchored claude, pi or omp hears words only during a turn. Under a flow, SteeringAgentMixin.steer(prompt, session=…, queued=True); queued=False interrupts the turn and continues from the prompt.

Goals ​

agent.pursue(objective, *, suppress=False, cwd=None) runs the backend's own goal feature: the agent decides when the objective is met; until then each turn that would end starts another, and pursue answers with the last.

BackendGoal feature
claude/goal
codexthread/goal/set
kimithe daemon's goal, set each time it is asked
dshthe goal service, create_goal tool and round driver mounted into the composition
every otherNotImplementedError: <Session> has no goal feature, even under suppress
  • type(agent).pursues says whether the backend has one.
  • goals=False makes pursue raise RuntimeError and removes work-past-the-turn tools: codex starts with --disable goals; claude gets --disallowedTools Agent,ScheduleWakeup, CronCreate,CronDelete,CronList,Workflow; dsh mounts no goal service.
  • A budget and interrupt do not end a goal; agent.stop() does, and so does cut on a backend whose transport it puts down.
  • A goal is watched as a turn is: what each of its turns says and reaches for reaches the agent's watchers as it happens (on codex, bracketed by one begins and one ends for the whole goal), and on stderr where nothing is watching.
  • Under a flow, /goal <objective> as the prompt of a role typed with GoalCommandAgentMixin.

Loops ​

LoopCommandAgentMixin is served by claude only. A flow prompt /loop <interval> <task> is passed to the CLI unchanged; no other prompt is interpreted, and no Python API corresponds.

Hooks ​

A hook is a Python callable hung on a moment of a turn:

python
hung = agent.hooks.on(Moment.PERMISSION_REQUEST, hook, tool="Bash")  # a context manager too
hung.off()                                                           # idempotent

hook(occasion: Occasion) -> Verdict | None. Hooks are on the agent and apply to every session; they may be hung and taken down mid-run.

MomentWhenWhat a refusing Verdict does
SESSION_STARTbefore a session's first turnnothing
USER_PROMPT_SUBMITbefore each prompt is sentskips the turn; adds is appended to the prompt
PRE_TOOL_USEthe agent reached for a toolstops the tool on a backend with a hook seam (below); nothing elsewhere
SUBAGENT_START, SUBAGENT_STOPan agent of its own started / came backnothing
PERMISSION_REQUESTthe backend asks whether a tool may rundenies it, because as the reason
NOTIFICATIONthe agent stopped to ask its user somethingnothing
STOPa turn endedsends the agent on, because as the next prompt
SESSION_ENDa session closednothing
Occasion fieldTypeMeaning
momentMoment
agentstrthe agent's id
sessionstrthe session id, or ""
promptstrthe prompt (USER_PROMPT_SUBMIT)
toolstrthe tool, or the subagent's name
aboutstrthe command or path, or the subagent's task
understrthe backend's id for a subagent, pairing start and stop
inputMapping[str, Any]the tool's input where known
saidstrwhat was said (NOTIFICATION, SUBAGENT_STOP)
againinthow many times this turn has been sent on by STOP

Verdict(refused=False, because="", adds=""). Two hooks on one moment combine: refused if either refused, every adds added. A hook that raises has said nothing, except Stopped, which propagates.

Moments per backend ​

agent.moments is what the backend runs; hooks.on for any other raises Unhooked: <agent id> does not run <Moment>.

Backendthe six common momentsPERMISSION_REQUESTSUBAGENT_START, SUBAGENT_STOP
claude, codexyesyesyes
cursor-agent, mcodeyes—yes
grok, kimiyesyes—
agy, dsh, mimo, omp, opencode, pi, qwen, an ACP CLIyes——
HumanAgent———

The six common moments are SESSION_START, USER_PROMPT_SUBMIT, PRE_TOOL_USE, NOTIFICATION, STOP, SESSION_END.

Permission requests ​

A refusal reaches the agent only on a backend that waits for the answer:

BackendWhen it asksDefault answer with no refusing hook
claudeat bypass: --permission-prompt-tool stdio routes every request over the streamyes; a request at read-only is answered no; with no rung nothing is routed
codexthrough the app server, at auto (on-request) and at any rung given approvalsyes at every rung; no with no rung
groksession/request_permission, on the held-open transport onlyyes at every rung; no with no rung
kimieach approval on the daemon's /approvals: at auto, and at read-only or no rungyes at auto; no at read-only and with no rung

A hook can turn a yes into a no, never a no into a yes. Under a flow, Codex's refusal carries no reason, so the reason is steered into the turn instead.

Refusing a tool ​

PRE_TOOL_USE stops a tool only on a backend that takes a hook table for one run (Profile.hooks):

BackendSeam
claude--settings, the settings literal on the command line, carrying a PreToolUse hook
qwenhooks in its virtual home's copy of settings.json, after the user's own
  • The table runs hmz internal hook --at <socket>, which relays the call to this process and the verdict back; a refusal means the tool does not run and PERMISSION_REQUEST is not asked.
  • The table is present only while a PRE_TOOL_USE hook is hung. A hook hung or removed between turns takes effect next turn; one hung mid-turn is read off the stream for that turn.
  • An anchored turn gets no table; there, and on every other backend, PRE_TOOL_USE is read off the stream after the tool was announced, and cannot stop it.
  • Nothing of the user's configuration is read, written or replaced.

Runtime reports ​

On backends whose CLI is a Node program (Profile.preloads == "NODE_OPTIONS": kimi, pi, qwen, mimo), hanging a PRE_TOOL_USE hook also preloads a script via NODE_OPTIONS that reports what the runtime did as additional PRE_TOOL_USE occasions:

occasion.tooloccasion.about
spawnthe command line
read, writethe path
connecthost:port
quietthe process stopped reporting (100,000 reports, or reports dropped)
kimiqwenpimimo
Watchedthe daemon (every session of the agent)the launcher and the bundle it re-execsthe held processits launcher only
  • Reports arrive after the call, on the socket-reading thread; a verdict does nothing.
  • Hang before the first turn: qwen, pi, mimo pick up later hooks on their next turn; kimi's daemon, started once, never does.
  • The CLI's reads of its own install and calls on descriptors are not reported; a Node program the agent runs reports nothing beyond its spawn. The preload fails open.
  • Not installed for an anchored turn. The socket is named by HMZ_PRELOAD_AT and HMZ_PRELOAD_IN.

Flow hooks ​

Under a flow, hooks are async functions on the flow agent (agent.on_stop(fn), agent.on_permission_request(fn), …). The harness driver fires SESSION_START, USER_PROMPT_SUBMIT, STOP and SESSION_END itself around each turn, and carries PreToolUse, PermissionRequest, Notification, SubagentStart and SubagentStop from the CLI's threads. A moment waits at most HOOK_TIMEOUT (900 s) for the flow's hook before it is answered as if nothing were hung; a question to the user waits indefinitely. A flow declaring a hook mixin its harness does not serve is refused before its first turn.

Defined in src/hmz/coganchor/agents/hooks.py, src/hmz/coganchor/agents/preload/, src/hmz/runtime/flowing/harnesses.py.

Questions ​

An agent may stop mid-turn to ask its user; Question(text, options=(), asker="").

CallbackCalledReturns
agent.askwhen the agent asksthe answer, or None; unset: the backend is told nobody answered
agent.waitingas each turn startslist[str] said while no turn was open, prepended to the turn
agent.promptingbetween turns, by agent.prompted()the next prompt, or None when there is none; prompted() raises Stopped for a stopped agent

A watcher sees each question as an asks event. Under a flow, questions go to the role's on_ask_user hook where the role declares AskUserHookAgentMixin (served by claude, codex, kimi, omp, pi); otherwise the backend is told nobody answered.

Stopping and budgets ​

agent.stop() ends the running turn of every session, closes them, and makes every later call raise Stopped. agent.stopped says whether it has happened. What the turn changed stays.

Budgets ​

hmz.coganchor.agents.Budget (frozen):

FieldDefaultMeaning
output0.0output tokens one turn may write; 0 is no cap
seconds0.0wall-clock seconds one turn may run (tool time counts); 0 is no cap
when"next-response"next-response: let the answer in progress land; immediately: end the turn where it stands
then"end"end: answer with what was said; fail: raise Unrecoverable
  • Per turn: every turn starts with the whole budget. Budget() caps nothing.
  • AgentConfig.budget is the agent's; session.budget = … overrides it for one conversation from its next turn.
  • Held off the live meter (Usage); seconds bites with no output arriving. On agy, grok, qwen and ACP CLIs, whose drivers do not feed the meter, output never bites.
  • A turn ended by its budget has landed: its edits stay and the next turn resumes the session. It is never retried.
  • A budget's cut-off wins over a STOP hook that would send the agent on.

Interrupting ​

session.interrupt(*, why) ends the running turn and leaves the session usable (no-op with no turn). session.cut(*, why) does the same and, on a backend with cuts_transport, puts the shared transport down, ending every turn on it and any goal; the next turn restarts it and resumes by id, except on dsh, whose next turn is refused (Unrecoverable).

How the backend holds a turninterrupt reachescut reaches
a command per turn: cursor-agent, mcode, opencode, mimo, and the command turns of agy, grok, qwenthe command and its childrenthe same
a held process: claude, pi, omp, the ordinary turns of agy, grok, qwenthat process; pi and omp are first sent abort and given 5 sthe same
a shared transport: codex (app server), dsh (SDK runtime)nothing is taken down; the turn stops at the transport's next messagethe transport and every turn on it
kimi (daemon)the prompt is aborted (POST …/prompts/<id>:abort)that, then the daemon's whole process tree
an ACP CLIsession/cancelthe same

A cut turn still ends on exactly one result, carrying what had been said; a CLI that sends whole messages only answers "" if cut during its first.

Allowances ​

A flow run's budget (-p budget.<limit>=) is held by the flow runtime, which hands each turn what remains. For agents driven directly, Ledger(Allowance(hours=0, tokens=0, dollars=0), agents) assigned to agent.allowance stops every agent of the set when any cap is reached.

Allowance fieldUnit0
hourswall-clock hoursno cap
tokensmillions of output tokensno cap
dollarsUS dollars (an unpriced model counts as nothing)no cap

Checked by SessionBase at both edges of every turn and as each session closes; a turn asked for under a spent allowance raises Stopped. Clones and stand-ins spend the same allowance. ledger.reads() has seconds, output, dollars (None where nothing is priced), floor (money is a lower bound), blind (caps nothing can read); ledger.over() says why, or ""; ledger.spent says whether it has been found over.

Usage and cost ​

session.spent() and agent.spent() return a Usage: a mapping of kind to tokens, with input, output and total attributes. agents.KINDS is ("input", "output", "cache_read", "cache_write", "reasoning"). A kind a backend does not report is absent.

BackendcountsThe meter moves
claudeinput, output, cache_read, cache_writeon each message
codexinput, output, cache_read (cached reads taken out of input)on thread/tokenUsage/updated
dshinput, output, cache_read, cache_writeon each finalised assistant message
litellminput, output, cache_read, cache_write (cached reads taken out of input)on the usage of the turn's one request
pi, ompinput, output, cache_read, cache_writeon each finalised assistant message
opencode, mimoall fiveon each step
kimiinput, output, cache_read, cache_writeon each turn.step.completed notification
cursor-agent, mcodeinput, output, cache_read, cache_writeonce, on the closing result
agyinput, output, cache_read, reasoningnever: usage is on the result event only
grok, qweninput, output, cache_read, cache_writenever: usage is on the result event only
an ACP CLInonenever
  • rate(over=WINDOW): tokens per second by kind, over the last over seconds of wall clock (agents.WINDOW == 300); a younger run is measured over its life.
  • juice(over=WINDOW): output tokens per model request; 0.0 for a window with none.
  • The result event's spent is the turn's usage and tokens its per-model totals.
  • hmz.coganchor.prices.cost(usage, model) prices a Usage kind by kind; price(model) returns the Price. Both read $TMPDIR/humanize-<uid>/prices.json (fetched from https://openllmprices.com/data/prices.json, refreshed after 24 h; HUMANIZE_PRICES points elsewhere or turns fetching off with off) and return None for an unlisted model.
  • The TUI's running cost reads the CLIs' own logs as they are written, for claude, codex, dsh, kimi and mcode; for the rest it moves as each turn lands. Each model request is counted once: Claude's rows sharing a message id (one per content block, each with the whole usage) and Codex's token_count rows with an unmoved total_token_usage are one request. Only rows the log timestamps at or after the moment the run opened that session count: a conversation carried on from an earlier run, or forked from another, keeps that one's rows, and a row with no timestamp is counted. What is shown per model is the higher of what the logs and the backends say, never the sum.

Defined in src/hmz/coganchor/agents/event.py (Usage, KINDS), src/hmz/coganchor/agents/allowance.py, src/hmz/coganchor/prices.py, src/hmz/runtime/watching/tally.py.

Skills ​

Skills installed for a CLI are the CLI's own; Humanize does not disable them or write its settings. hmz.coganchor.agents.skills.skills(cli) lists what the CLI would load here.

agent.loaded / agent.loads(loaded) are the skills whoever drives the agent brings (under a flow, the role's _skills). session.skills / session.loads(names | None) select which of those one conversation carries, from its next turn; a name not brought is ignored.

A backend whose profile names a user-level skills directory (Profile.lodges) is given the brought skills in its virtual home: linked into that home's skills directory beside links to the user's own, a brought skill taking the place of the user's of the same name. opencode and mimo are told where the brought skills are instead, under skills.paths in their config variable, which they read after their own skill directories. Any other backend gets none. Nothing is ever put in the workspace.

BackendPut atSkill directories read (Profile.works, relative to the workspace)
claude$CLAUDE_CONFIG_DIR/skills/ in its virtual home.claude/skills
codex$CODEX_HOME/skills/ in its virtual home.agents/skills, .codex/skills
grok$GROK_HOME/skills/ in its virtual home.grok/skills, .agents/skills, .claude/skills, .cursor/skills
qwen$QWEN_HOME/skills/ in its virtual home.qwen/skills, .agents/skills
pi, omp$PI_CODING_AGENT_DIR/skills/ in its virtual homepi: none read headless (trust-gated); omp: .omp/skills, .agents/skills
agyskills/ in its virtual home (--app_data_dir).agents/skills
cursor-agent$CURSOR_CONFIG_DIR/skills/ in its virtual home.cursor/skills
kimi$KIMI_CODE_HOME/skills/ in its daemon's virtual home.kimi-code/skills, .agents/skills
mcode$MINIMAX_DATA_DIR/skills/ in its virtual home.minimax/skills, .claude/skills, .agents/skills
opencode, mimoskills.paths in OPENCODE_CONFIG_CONTENT / MIMOCODE_CONFIG_CONTENT.agents/skills and their own (.opencode/skill[s], .mimocode/skill[s], and for opencode .claude/skills)
dsh, an added CLInone: the turn runs without themnone
  • A virtual home is built per process, so a session told to carry other skills restarts its process and resumes the conversation (agy), or carries them from its next command (cursor-agent, mcode). A Codex app server or a Kimi daemon is shared by an agent's sessions and carries every skill the agent was given; a change of those starts a new one for the next turn.

  • pi reads workspace skills only for a trusted project, which is why brought skills go to its user level; pass further paths with PiAgentConfig.skill_paths (--skill).

  • Under a native anchor, skills are written into the virtual home built on the target; under a supervised anchor a virtual home is built here, where the CLI runs; under a harness on another machine they are carried there and lodged in the virtual home built there. agy under a native anchor gets none (its home is moved by a flag, which the anchor does not write), and neither do opencode/mimo anywhere but here (the paths they are told are this machine's).

Virtual homes ​

A backend whose profile names files it rewrites as it runs (Profile.rewrites) or a user-level skills directory (Profile.lodges) is started, a process at a time, with its home variable (Profile.home_var) naming a directory built for that process on the machine it runs on (hmz.coganchor.homes.build, AgentBase.virtual, AgentBase.homed) and removed once the process is over. A CLI no variable moves names it by a flag instead (Profile.home_flag): agy's --app_data_dir, which takes it relative to ~/.gemini and is given one climbing out of there (Home.argv, put right after the program's name).

In the virtual homeIs
each of Profile.rewritesa copy of the user's file, taken from where the CLI would have read it: under Profile.outside's path while the home variable is unset, and a provider's copy where its account answers that path; a directory starts empty
Profile.lodgesa directory of links to each of the user's own skills, and to each brought skill
the session paths (Profile.sessions) the user's home lacks yetmade in the user's home, then linked; one named by a glob, which cannot be, is moved into the user's home as the virtual one is removed
the session paths of a turn a run keeps on a machine that cannot supervise itlinked into <epic>/sessions/<cli>/ instead, made there where a directory; a glob links each matching name of the user's home or of that directory, and what the CLI creates under a new matching name is moved there as the virtual home is removed (Where sessions are kept)
everything else in the user's homea link to it
  • What the CLI writes into a copy goes with the home; the user's own files are never written. Credentials, caches and sessions are reached through the links, so a refreshed token and a conversation land where they always do; a supervisor answering session or credential paths answers them under the virtual home too.
  • A CLI that saves a file by renaming another over it replaces the link with a file of the virtual home's own. As the home is removed (Home.drop, and homes.BACK on a native anchor's target), each link so replaced is written back to what it pointed at -- or to the provider's or account's copy a supervisor answers that path with: a file whole and atomically, keeping the mode the one there had; a directory a new or changed file at a time. Never over a file as new or newer, which another process of the same account saved since: the last to save wins. So a refreshed token (.credentials.json, auth.json, credentials) and a session file a CLI replaces are kept; the copies of Profile.rewrites still go.
  • A path of its home a CLI writes down to read back later (Profile.records) names the virtual home, which is gone once the process is. As the home is removed (Home.drop), each such path whose file is there is rewritten with the home's links followed, to where the file really is. Codex finds a thread's rollout by threads.rollout_path in its state_*.sqlite, so this is what lets a Codex conversation be resumed, forked or carried on after its app server has gone. On a native anchor's target, where no Humanize runs, homes.BACK does the same with the target's sqlite3, and leaves them as they are where it has none.
  • A brought skill sent to a native anchor's target is written a file at a time, each beside itself and renamed into place with its own mode (a script stays executable); one that cannot be written whole is removed rather than left half there.
  • Profile.secures names a variable set to the user's own value of the home variable (empty where unset), unless the environment sets it: Claude Code's CLAUDE_SECURESTORAGE_CONFIG_DIR, which keeps its keychain item and .credentials.json where they were.
  • The system level is never replaced, and the project level is the workspace, read as the CLI reads it. What Humanize itself manages for a CLI with no flag or variable for it is merged into the copy at the user level: Qwen Code's effort, hook table and humanize MCP server into settings.json, a gateway account's [model.*] entry into Grok Build's config.toml, a gateway account's provider into pi's models.json, and Humanize's agents into agy's agents/ (a directory of the home's own, with links to the user's agents beside them). Each is said as data (Virtual.merges, a session's _merges) and applied as the home is built, by a function its driver registers (homes.merger), so it is applied wherever the home is built; what a merge put in place of a link is never written back.
  • A fenced agent may write the directory its virtual homes are built in.
  • Under a native anchor whose account is projected, the virtual home is that account's: the session paths stay in it, and none of the target user's credentials is linked or copied. Under a native anchor nothing Humanize manages is merged into the copies there.
  • A harness on another machine (AnchorConfig.harness, hmz.coganchor.elsewhere.afar) is started in one built on that machine: AgentBase.homed builds none here, and the anchor's options carry what it is made of (AnchorConfig.home, the session's skills and merges) to the hmz internal anchor there, which builds it from that machine's user's own configuration around the CLI it supervises, keeps it on that machine, and removes it when the CLI exits. The brought skills are carried there first (Remote execution › Where the harness runs). A grok or pi account behind a gateway is still refused there.
BackendProfile.rewritesProfile.lodgesProfile.secures
claude.claude.json (from ~/.claude.json while CLAUDE_CONFIG_DIR is unset), backups, settings.jsonskillsCLAUDE_SECURESTORAGE_CONFIG_DIR
codexconfig.tomlskillsnone
grokconfig.tomlskillsnone
qwensettings.jsonskillsnone
pimodels.json, settings.jsonskillsnone
ompnoneskillsnone
agynoneskills (and Humanize's agents in agents/, beside links to the user's)none
cursor-agentnoneskillsnone
kiminoneskillsnone
mcodeconfig.yamlskillsnone

Fake tools ​

session.offers([FakeTool(name, about, call, takes=None)]) gives the agent a tool whose call runs call in this process; offers(None) withdraws them. FakeTool is one type, from hmz.coganchor.faking, exported here and by hmz.flows: a flow's _tools are handed to offers as they were declared, with only call wrapped to run on the flow's loop as the flow.

FakeTool fieldMeaning
namewhat the agent calls it
aboutwhat it is for, said to the model
takesa pydantic model of its arguments, or None for none
callcalled with the model (or nothing); its return value goes back as text; raising is the tool failing
  • Served where session.takes_tools: claude (--mcp-config), codex (-c mcp_servers.humanize.…), qwen (mcpServers.humanize, trust: true, in its virtual home's copy of settings.json; the user's mcp.allowed/mcp.excluded and folder trust still apply), opencode and mimo (mcp.humanize merged into OPENCODE_CONFIG_CONTENT / MIMOCODE_CONFIG_CONTENT), agy (a plugin, --add-dir <temp>/~humanize-agy-… holding .agents/plugins/humanize/mcp_config.json), grok and an ACP CLI (a humanize stdio server in the mcpServers of the call that opens the conversation), pi (--extension <hmz>/coganchor/agents/pi_tools.cjs, loaded under --no-extensions too; tools named humanize_<name>), omp (RPC set_host_tools; tools named humanize_<name>) and dsh (a @deepseek-ai/dsh-mcp-client row in its patch, tools named mcp__humanize__<name>); elsewhere NotImplementedError: <cli> has no way of being given a tool of a flow's own.
  • On grok and an ACP CLI, changing the offered names restarts the process and picks the conversation back up; an ACP CLI serving neither session/resume nor session/load fails that turn instead.
  • grok serves them on held-open turns only: a turn that needs grok -p (a shape, a fork, and the rest under Grok Build) is refused with Unrecoverable while any are offered.
  • The road is MCP: the CLI is given hmz internal tools --at <socket>, which pi's extension starts itself. omp asks for each call back over its own RPC pipe instead. Tools are per agent.
  • Offering other names between two turns starts pi again before the next one, and refuses dsh's next turn once its session has taken one; omp is told on the process already up.
  • A native anchored turn on a non-local target cannot be offered tools (refused), but for omp (AgentBase.pipes_tools), whose calls come back over its RPC pipe wherever it runs.

Fake command lines ​

session.shelves([FakeCli(name, about, commands)]) puts a program called name on the agent's PATH; every run of it is answered in this process. shelves(None) takes them back. FakeCli and FakeCommand are one type each, from hmz.coganchor.faking, exported here and by hmz.flows: a flow's _clis are handed to shelves as they were declared, with only each command's call wrapped to run as the flow.

python
class File(BaseModel):
    title: str = Field(description="what the ticket says")
    points: int = 3

session.shelves([FakeCli("tix", "Track tickets.", (FakeCommand("file", "File one.", file, File),))])
# the agent's shell: tix --help; tix file --help; tix file --title "red build" --points 5
FakeCommand fieldMeaning
namethe subcommand typed after the program's name
aboutits line in --help, and its own --help description
takesa pydantic model of its options, or None for none
callcalled with the model (or nothing)
Field of takesOption
any--field-name FIELD_NAME, help from Field(description=…) and the default
no defaultrequired
bool--flag / --no-flag
list[T], tuple[T, ...], set[T]--flag [V ...]
Literal[…], an Enum--flag {a,b}

The words are parsed by argparse and validated by the model, so a value the model refuses is a usage error like a missing flag.

RunStdoutStderrExit
--help, COMMAND --helpthe help0
a line the parser or the model refusesthe usage and NAME COMMAND: error: …2
call returns a strit, with a trailing newline0
call returns None0
call returns anything elseit as indented JSON (a model as model_dump(mode="json"))0
call raises SystemExit(n)a non-integer nn (None is 0, text is 1)
call raises anything elseNAME: <Type>: <message>1
a command line taken back since the shell found itNAME: command not found127
  • FakeCli refuses a name that is not a plain file name (letters, digits, ., _, -, not starting with . or -), no commands, and a command named twice (ValueError).
  • Served wherever there is a shell (session.takes_clis): every backend but litellm and the person at the prompt, where NotImplementedError: <cli> has no shell to run a command line of a flow's own in.
  • Command lines are per agent, as tools are. The first one shelved makes agent.shelf: a 0700 directory under $TMPDIR holding bin/NAME (a #!/bin/sh script running hmz internal cli) and the socket. bin is put first on PATH in every process of the agent from then on, and a process held open from before is started again for its next turn.
  • call runs on a thread of its own per run. Stdin and the working directory are not passed.
  • Under a fence the directory is readable. Under a supervised anchor it and the Python running Humanize are kept here (local_execs, local_paths). A native anchored turn on a non-local target with any shelved is refused.

Machine and account ​

AgentConfig.machine decides where turns land; agent.anchor brings the machine up on first read (Machines). Under a flow, the session's environment decides.

AgentConfig.provider names the account (Providers). agent.provider raises ValueError: <id>: no <cli> provider called '<name>' the first time a turn needs a missing account; it never falls back to the machine's own.

When a turn fails ​

Before a failure reaches the caller, two things are tried in order: retries at the same place, in the same conversation, and then each place of the fallback chain, in order.

Retries ​

A place is CLI[@ACCOUNT]/MODEL (agent.spec). Retries are set per place in fallbacks in $HUMANIZE_HOME/settings.yaml, by Hmz().fallbacks.retrying(place, tries, policy, timeout) or the Fallback page of /settings. Nothing is retried by default.

PolicyWaits (base 1 s, each capped at 60 s)
none0
constant1, 1, 1, …
linear1, 2, 3, …
exponential1, 2, 4, 8, …
exponential-jitter (default)uniformly random up to the exponential wait
fibonacci1, 1, 2, 3, 5, …

Each fault adjusts the place's retries (fallbacks.ANSWERS):

FaultTries (floor)WaitTransport reopenedFix
throttled1at least 30 snothe service asks it to slow down, with no quota spent; a wait is what answers it
spent1at least 30 snothis account has spent its quota; another one, or a wait, is what answers it
refusednone—nothat account needs signing in again
unlistednone—nothat model is not this account's to name; ask it what it runs and name one of those
retirednone—nothe model is gone or was never this account's; another place is what answers it
contended3constantnotwo turns of it are sharing one database
dropped1the place'syes
fencednone—nothe run's network fence keeps it off that host, which no sign-in answers; give the role online of ALL to let it through
killed1constantyesthe machine it runs on may be out of memory
missingnone—no
sandboxednone—nothis machine will not let it sandbox itself; run it without one, or somewhere it can
unmirrorednone—nothat path cannot be made here; use a workdir whose path you can create here, or set the harness of the runtime the work is on to env
unclassifiedthe place'sthe place'sno

Unrecoverable is never retried or carried.

Refused by fallbacksValueError
a place that does not parse'<text>' is not a place: expected CLI[@ACCOUNT]/MODEL
an unknown policy'<p>' is not a retry policy: none, constant, linear, exponential, exponential-jitter, fibonacci
negative or infinite numberstries and seconds are counts, not debts or infinities
a place falling back to itself<place> cannot fall back to itself
a place named twice on one chain<place> is already one of the places <spec> falls back to

Fallback chain ​

Hmz().fallbacks.points(place, [other, another]) writes the places a place's turns go to once its retries are spent, in order. fallbacks.chain(place) is [place, *to] for a place a rule is written against, and [place] for any other, including one that is only on somebody else's chain. agent.stands_in() returns the stand-in at the next place, built once and kept; a stand-in holds the rest of the chain it was reached by and carries on along it, never along a rule of its own place. Each stand-in is retried as its own place's rule says. The turn is taken in a new session at the other place, by an agent configured as this one (effort, rung, skills); settings that were measurements of the old model (of_model fields, e.g. Codex overrides) are dropped when the model changes. An account names no fallback of its own: another account of the same CLI is a place on the chain.

Defined in src/hmz/coganchor/fallbacks.py, src/hmz/coganchor/standin.py.

Watchdog ​

Every blocking read of a turn runs under a clock that restarts whenever the backend says anything.

SettingValue
Silence allowedProfile.silence: 900 s; 360 s for dsh
OverrideHUMANIZE_WATCHDOG=<seconds>; 0 or less disables it
Pausedwhile the turn waits on a person: a permission prompt, a question, a pausing hook

When the clock runs out, in order:

  1. Look. A process (or a descendant) burning CPU gets up to 4 more windows; a suspended or defunct one gets none.
  2. Interrupt the turn; the conversation is untouched.
  3. Drop the transport, signalling the process tree; on codex and kimi the shared server goes, ending the agent's other turns too, and that is said first.
  4. Kill what is left and wait.

Each step emits a notice, <backend> …: has said nothing for <n>s but is still working, has said nothing for <n>s, was asked to stop: …, is not answering; ending it[; <id> is picked back up on the next try], did not go when it was asked; killing it and everything under it. The turn then fails as an ordinary Failed, which retries take against the same conversation.

Defined in src/hmz/coganchor/agents/watchdog.py.

State, logs and traces ​

BackendHome (home_var, else default)Session log (logs, {ident} the session id)TraceSubagents in trace
agy~/.gemini/antigravity-cliconversations/{ident}.dbyesyes
claude$CLAUDE_CONFIG_DIR, ~/.claudeprojects/*/{ident}.jsonl, projects/*/{ident}/subagents/**/*.jsonlyesyes
codex$CODEX_HOME, ~/.codexsessions/**/rollout-*{ident}.jsonlyesyes
cursor-agent$CURSOR_CONFIG_DIR, ~/.cursornoneno—
dsh$DSH_HOME, ~/.dshsessions/*/{ident}/session.v*.jsonlyesyes
grok$GROK_HOME, ~/.groksessions/*/{ident}/updates.jsonlyesyes
kimi$KIMI_CODE_HOME, ~/.kimi-codeserver/events/{ident}.jsonlyesyes
litellm~/.cache/humanize/litellmsessions/{ident}.jsonl, written by Humanizeyes—
mcode$MINIMAX_DATA_DIR, ~/.minimaxv2/sessions/*/*/*/*-session_{ident}/messages.jsonl, {ident} URL-safe base64 without padding (Profile.encodes)yesno
mimo$XDG_DATA_HOME/mimocode, ~/.local/share/mimocodeSQLite mimocode.dbyesyes
omp$PI_CODING_AGENT_DIR, ~/.omp/agentsessions/*/*{ident}.jsonlyesno
opencode$XDG_DATA_HOME/opencode, ~/.local/share/opencodeSQLite opencode.dbyesyes
pi$PI_CODING_AGENT_DIR, ~/.pi/agentsessions/*/*{ident}.jsonlyesno
qwen$QWEN_HOME, ~/.qwenprojects/*/chats/{ident}.jsonlyesno
an ACP CLInonenoneno—

Backend notes ​

Antigravity ​

agy. Ordinary turns are lines on one held process started with --input-format stream-json; a slash command (a whole first word such as /help) and a shaped turn run as separate --print commands and resume with --conversation <id>. Changed configuration, customisations or flow resources restart the process; an anchored turn always ends it.

FieldDefaultMeaning
add_workspaceTrue--add-dir <session dir>, pinning the session's directory
print_timeout86400.0--print-timeout seconds (raised from the CLI's 300; since 1.1.28 a timed-out turn exits successfully with a partial answer)
disable_slash_commandsFalse--disable-slash-commands; with read-only: Unserved: agy cannot run at read-only with disable_slash_commands: its plan mode has no effect while expansion is off
sandboxFalse--sandbox, the CLI's terminal restrictions; not how a fence is held
  • Usage is cumulative per conversation; each result reports its own increment. Shaped answers validate the final structured_output.
  • A fence is held from outside in full. With online cut it still reaches its eligibility hosts (including lh3.googleusercontent.com) and starts a loopback language server on a kernel-chosen port; the turn is started as hmz-offline regardless of web_search.
  • Credential: antigravity-oauth-token under its home (only HOME moves it).
  • Each process runs in a virtual home named by --app_data_dir=<relative path>: the flow's skills are in its skills/, and the agents a turn is started as (hmz-read-only, hmz-read-only-web, hmz-offline) are written into its agents/ beside links to the user's own, so ~/.gemini/antigravity-cli/agents is never written. An account whose arguments carry --app_data_dir of their own runs in that home as before, without the flow's skills; there read-only or web_search=False is Unserved: agy cannot run at read-only or off the web under an account naming its own --app_data_dir: .... agy replaces jetbox_summaries_proto.pb (its conversation summaries index) rather than writing through it, so that index keeps what Humanize's turns wrote to it only for their process.
  • Fake tools are a plugin humanize in a temporary directory added with --add-dir (the session directory is then always added too); a change in the offered names restarts the process. agy runs commands in the first workspace directory by path, so the plugin's must sort after the session's: where it would sort before (a session under /workspace with TMPDIR=/tmp) the turn is refused with Unserved: agy cannot be offered tools in <dir>. Calls need auto or bypass (or the user's own mcp(...) allow rule); at other rungs agy denies them.

Claude Code ​

claude. One process per session:

text
claude --print --input-format stream-json --output-format stream-json --verbose
  [--include-partial-messages]
  (--session-id <new uuid> | --resume <id> | --resume <parent> --fork-session)
  [--permission-mode plan|acceptEdits|auto|bypassPermissions|manual]
  [--permission-prompt-tool stdio]
  --settings '<json>' --model <model> [--effort <rung>] [--json-schema '<json>']
  [--disallowedTools <list>] [--allowedTools <list>] [--mcp-config '<json>']
FieldDefaultMeaning
allowed_tools()--allowedTools rules; at most 32, sorted, unique, no ,, each at most 4096 characters (ValueError: allowed_tools must be unique sorted Claude tool rules)
partial_messagesTrue--include-partial-messages
asksNoneat bypass, --permission-mode manual instead of bypassPermissions, so every tool that would change something is asked about. None: while a PERMISSION_REQUEST hook is hung on the agent. A flow sets it True while its own PERMISSION_REQUEST hook is hung and False otherwise
  • Every turn runs with CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1, so background subagents and commands finish inside the turn.
  • --settings carries fastMode (fast tier) and the PreToolUse hook table only when needed.
  • --json-schema, a hook table change, an effort change, a change of asks, or a change of offered tools restarts the process and resumes the conversation.
  • A result with is_error, a non-success subtype, a terminal_reason other than completed, or a stop_reason of max_tokens, model_context_window_exceeded, pause_turn, tool_deferred or tool_use (without structured output) fails the turn.
  • An anchored session's process ends with each turn.
  • Each process runs in a virtual home (CLAUDE_CONFIG_DIR), so what Claude writes of .claude.json and settings.json is dropped with it. Hooks and the flow's MCP server still go through --settings and --mcp-config.

Codex ​

codex. One app server per agent, shared by its sessions:

text
codex app-server [--strict-config] [--disable goals] [--enable|--disable <feature>]...
  [--disable apps] [--disable respect_system_proxy] [--enable use_legacy_landlock]
  [--disable shell_snapshot]
  [-c web_search="live"|"disabled"] --stdio
  [-c <override>]... [-c mcp_servers.humanize.command=… -c mcp_servers.humanize.args=…]

Threads are thread/start, thread/resume and thread/fork; turns are turn/start (model, effort, rung, approval, service tier, outputSchema); steering is turn/steer. A turn ends on turn/completed, whose status and error say whether it failed; the thread may fall idle first, which ends nothing.

FieldDefaultMeaning
overrides()-c pairs; only model_context_window and model_auto_compact_token_limit, positive integers, the second below the first. Dropped when a fallback changes the model.
features()--enable/--disable pairs by codex features list names; goals and names beginning browser_use, computer_use, standalone_web_search, web_search are refused
strict_configFalse--strict-config
approvals""approval policy replacing the rung's: untrusted, on-request, on-failure, never
unsandboxed()names of fake command lines the agent shelves that run outside Codex's sandbox, by prefix_rule(pattern=["<name>"], decision="allow") in rules/hmz.rules of the virtual home, beside links to your own rule files; every other command stays in the sandbox
RefusalValueError message
another override keymodel is not a Codex override; expected model_auto_compact_token_limit, model_context_window
a non-positive valuemodel_context_window must be a positive integer, not '-1'
wrong ordermodel_auto_compact_token_limit must be below model_context_window
goals as a featuregoals is the flow's to say, as AgentConfig(goals=...) -- a second place for it would be two answers
a web or browser featureweb_search_request is what the agent may reach for, which the flow says as AgentConfig(web_search=...) and AgentConfig(permission=...) where it declares the place -- a feature is not a second place for it
bad approvalsapprovals must be one of '', 'untrusted', 'on-failure', 'on-request', 'never', not 'sometimes'
  • The server reads overrides, features, strict_config, web_search, the account and the fence once at start; a change starts a new server for the next turn and each conversation is resumed there by id. Model, effort, rung and approvals are per call.
  • -p/--profile and --add-dir are not offered; codex app-server does not take them.
  • Each app server runs in a virtual home (CODEX_HOME), so the trust Codex writes into config.toml for each workspace it opens a writing thread in is dropped with it. The project's AGENTS.md and .codex/config.toml are read as Codex reads them.
  • No hook table: Codex runs hooks only once trusted in the user's config.toml.
  • On another machine, a sandboxed rung whose fence is at least as narrow is sent sandboxPolicy: externalSandbox (Remote execution).
  • Supervised with its commands on another machine (any anchor but native), the server starts with --disable shell_snapshot unless features names it: Codex writes its shell capture into its own home and sources it from each command, which runs on the target, where that home is not. Each command is a login shell on the target instead.
  • An openai-gateway or azure account appends -c model_provider=humanize … (Providers › Gateways); a bedrock or bedrock-key one -c model_provider=amazon-bedrock …, and an ollama or lmstudio one -c model_provider=ollama or lmstudio (Providers › Ways by backend).

Cursor Agent ​

cursor-agent. One command per turn:

text
cursor-agent --print --output-format stream-json --workspace <dir> --model <id>
  [--resume=<chat id>] [<rung flags>] [--trust] [--stream-partial-output] [--approve-mcps]
  [--add-dir <dir>]... -- <prompt>
FieldDefaultMeaning
trustTrue--trust: the workspace is trusted without asking
partial_outputFalse--stream-partial-output; the gathered duplicate message is dropped
approve_mcpsFalse--approve-mcps
add_dirs()--add-dir per entry
  • --workspace is the directory on the machine cursor-agent runs on: for work on another machine with cursor-agent here, the copy of the work kept here, never the far path.
  • The effort and tier are written into the id: composer-2.5 at high is composer-2.5-high, at the fast tier composer-2.5-high-fast. A name already carrying a rung is used as is. Bracket syntax (gpt-5.2[effort=low]) is not built.
  • The id is checked against the account's last catalogue: an unlisted rung is Unserved, naming those listed. An account never asked refuses nothing.
  • On another machine, workspace-write runs --force --sandbox disabled where the fence writes nothing of the home or system.
  • Each turn runs in a virtual home (CURSOR_CONFIG_DIR) carrying the flow's skills in skills/ beside links to the user's.
  • cursor-agent-local, pointed at an OpenAI-compatible endpoint with CURSOR_LOCAL_AGENT_BASE_URL, CURSOR_LOCAL_AGENT_API_KEY and CURSOR_ENABLE_AUTHLESS=1, takes the id it serves; under a provider, CURSOR_LOCAL_AGENT_API_KEY is hushed unless set.

DeepSeek Harness ​

dsh. Driven through deepseek-harness-sdk (>=0.1.5rc1,<0.1.6) in this process; no CLI. Models: deepseek-v4-flash, deepseek-v4-pro. Each runtime starts under the SDK's own sdk profile with one patch of Humanize's over it, holding only what the agent's settings change; the effort goes in the handshake.

FieldDefaultMeaning
compactionTruekeeps dsh-compaction-basic and /compact, compacting at 0.8 of the context window; False unmounts them
session_compression"none"the session log's compression, none or zstd (the tally cannot read zstd)
  • goals keeps the goal service, create_goal, /goal and the round driver; False unmounts them. Re-read every turn, so a reconfigured agent gets a rebuilt runtime.
  • web_search: False unmounts the web; unset keeps the profile's. Under a gateway account the web keeps web_fetch and loses its DeepSeek search.
  • Every runtime runs at DSH_PERMISSION_MODE=danger-full-access (bypass): the SDK answers no approval request, so the fence is what confines it.
  • Under a fence that cuts the network, a model endpoint on loopback is unreachable: the runtime connects to loopback directly, never through the fence's proxy.
  • Unrecoverable: the length refusal, a session id the runtime will not resume, and any turn that would need a new runtime for a session that has already taken one.
  • A session that has taken a turn cannot leave its runtime. The SDK's --profile sdk serves only initialize, session/prompt and shutdown, and a fresh runtime's session/prompt creates the session, which its persistence refuses once the log exists; the runtime's own resume is unreachable. So a new effort, account, composition or set of offered tools, a cut, a closed transport, an anchored session's second turn and a session reopened in a later process are refused up front; start a new session. Moving onto the runtime's --profile acp, which has session/resume and per-session mcpServers, is the eventual fix.
  • DSH_HOME is the kept directory, and sessions go under its sessions (Profile.told).
  • interject is unsupported: session/prompt queues and steer is not on the SDK surface.
  • Offered fake tools insert a @deepseek-ai/dsh-mcp-client row (serverName: humanize, failOnStartupError: true) into the patch; other names restart the runtime before the first turn and are refused after it.
  • Without an account, the SDK's saved credentials or DEEPSEEK_API_KEY/DEEPSEEK_BASE_URL.

Grok Build ​

grok. Ordinary turns are session/prompt on a held grok agent stdio, which takes only a model, an effort, an approval, an agent profile, a plugin directory and the leader. A tool-withholding rung, web_search=False, a network-cutting fence, a shape, a fork, or any field below but leader set away from default sends the turn to grok -p --output-format streaming-json --resume <id> … instead. grok -p takes no MCP server, so such a turn is refused while fake tools are offered.

FieldDefaultMeaning
leaderFalseFalse: a process per conversation; True: join the shared leader; None: [cli] use_leader in config.toml (sent as --no-leader when fenced)
sandbox""--sandbox <profile>
max_turns0--max-turns; 0 uncapped
subagentsTrueFalse is --no-subagents
rules""--rules, appended to its system prompt
  • The -p prompt is one argument (--single=…); Linux caps an argument at 32 pages, so a prompt over 131062 bytes raises before the process starts.
  • --include-partial-messages, --agent-profile and --plugin-dir are not fields.
  • Every turn is run with GROK_FOLDER_TRUST=0: Grok Build reads a project's own skills only from a folder it trusts.
  • Each process runs in a virtual home (GROK_HOME); a gateway account's hidden [model.*] entry is written into its copy of config.toml, never into the user's.
  • A gateway account's session that grok agent opens or loads on a model other than its hidden [model.*] entry (--model is taken only for a model in the catalogue it fetched) is put on the entry with session/set_model.
  • Through a local LiteLLM gateway: see Through LiteLLM.

Kimi Code ​

kimi. A kimi web daemon per agent (needs the [kimi] extra), driven over REST and its WebSocket notifications: the daemon has a route into a running turn, a per-turn body for the rung, thinking level and swarm width, and question ids.

FieldDefault herekimi web's ownMeaning
port0 (free port)58627
open_browserFalseopens one
log_levelerrorsilentone of fatal, error, warn, info, debug, trace; silent is refused (the driver reads Kimi server: <url>/#token=<token>)
web_titleNone<dir> | Kimi Codethe web UI title
  • Notifications wake the driver's REST polling (1 s without them); heartbeats are answered.
  • Usage comes from turn.step.completed (inputOther, output, inputCacheRead, inputCacheCreation); the larger of the steps' sum and the session aggregate wins per kind.
  • A turn is over when the session is seen stopped twice, a wait apart, after it was seen to start. One whose turn.ended notification says reason: failed fails, with the daemon's error message, rather than answering with nothing.
  • The fence goes up once, when the daemon starts; another fence or account starts a new daemon. With online=False, the port is chosen before start and is the only one in listen, and WebSearch/FetchURL are in disabled_tools regardless of web_search.
  • --add-dir, --skills-dir, --agent, --agent-file are ignored by kimi web and not offered.
  • The daemon runs in a virtual home (KIMI_CODE_HOME) carrying every skill the agent was given; another set of them starts a new daemon. kimi fork runs in the user's own home, whose sessions the virtual one links.

litellm ​

litellm. A model called directly through litellm in this process: each turn is one streamed litellm.completion over the session's history and the prompt. No CLI, no tools, no filesystem, no hooks of its own; HarnessKind.LITELLM turns take no environment (env must be None, UnsupportedOperation otherwise). LiteLLMAgentConfig adds no fields.

  • The history is sessions/<id>.jsonl under the agent's kept directory: a session header, then a message row per message, each answer with its model and usage. Every turn after the first reads it back; a fork's first turn copies its parent's under a new id.
  • A shape is passed as response_format (json_schema), so SessionBase.shapes is True.
  • Under an account the credentials are passed on the call: api_key for a key way, api_base and api_key for a gateway, the cloud's parameters for bedrock, vertex, azure. Without one, litellm reads the environment as it always does (OPENAI_API_KEY, …).
  • drop_params=True: a parameter the provider does not take, such as an effort, is left off.
  • Unrecoverable: litellm's ContextWindowExceededError.
  • An interrupt or the watchdog closes the stream; the turn answers with what had arrived.

MiniMax Code ​

mcode. One command per turn, the prompt on stdin:

text
mcode exec --output-format stream-json --input - --cwd <dir> [--model <model>]
  [--session <id>] [--effort <rung>] [--permission full|smart] [--output-schema '<json>']

No fields of its own.

  • --cwd is the directory on the machine mcode runs on: for work on another machine with mcode here, the copy of the work kept here, never the far path.
  • The session is continued with --session and the id its first turn's lines carry.
  • Each turn runs in a virtual home (MINIMAX_DATA_DIR) holding a copy of config.yaml and the flow's skills in skills/.
  • Through a local LiteLLM gateway, --model is custom_provider:hmz-PORT-DIGEST/<model>, a provider the agent adds for the gateway with mcode provider add into the turn's virtual home, before the turn: it goes with that home, so the account's own config.yaml is never written and nothing is removed when the agent stops. See Through LiteLLM.
  • --effort is sent only where the account's catalogue lists the rung for the model (only minimax/MiniMax-M3.1-Flash-Preview takes rungs).
  • read-only is refused; web_search=False and a network-cutting fence are refused.
  • The stream reports its task tool as SUBAGENT_START/SUBAGENT_STOP; usage arrives on the closing result, and the running cost reads its session log.
  • Each start creates <data directory>.lock beside its data directory: in a virtual home, beside that home, where a fenced agent may write. Where mcode starts in the user's own (a harness on another machine), that is ~/.minimax.lock, which a fence that reads but does not write the home cannot grant. A fenced turn's supervisor answers that path from the kept session directory (sessions/mcode/minimax.lock), shared by every agent of the run, and for an agent no run drives from $TMPDIR/humanize-<uid>/mcode/minimax.lock, shared by every such agent of the machine. With HUMANIZE_SESSIONS=off the directory is still made and holds only that lock. On a machine that cannot supervise a turn the path is not answered, and only a fence that writes the home lets mcode start.
  • config.yaml and auth/ are credential files, so an account holds its own settings.

Oh My Pi ​

omp. One omp --mode rpc per session, spoken to in pi's RPC protocol:

text
omp --mode rpc --model <provider/id> [--thinking <rung>]
  [--resume <id> | --fork <parent>] [--approval-mode always-ask|yolo]

OhMyPiAgentConfig adds no fields; those PiAgentConfig adds are not offered.

  • omp takes no session id up front. Ahead of the first prompt of a session with no id, the driver sends get_state; the sessionId it answers is session.named, and the session's id once the turn lands. Later processes start with --resume <id>; a fork's first process starts with --fork <parent> and learns its id the same way.
  • A turn ends on the agent_end that is terminal (neither isTerminal nor yielded is false); omp sends no agent_settled. A prompt omp answers itself, a slash command of its own (agentInvoked: false), also ends the turn.
  • At read-only, an extension_ui_request offering exactly Approve, Deny is answered Deny without asking anyone. Any other is a question, as on pi.
  • Offered fake tools are set_host_tools (loadMode: essential) ahead of the prompt whenever their names change; each host_tool_call runs on a thread of its own and is answered host_tool_result. A refused set_host_tools fails the turn.
  • No NODE_COMPILE_CACHE and no NODE_OPTIONS preload: omp is one Bun-compiled executable, so PRE_TOOL_USE is read off the turn only.
  • The fence also lets through the baseUrl of the model's provider in models.yml (else models.yaml) in omp's home, and of that provider's models; for a model named without a provider, of every provider declared there. Nothing is sent to omp under online NONE.
  • No credential files: omp keeps its sign-ins in agent.db, beside its settings, which an account does not copy. An agent with no account runs as this machine's omp sign-in.

opencode and mimocode ​

opencode and mimo (one program under two names). One command per turn:

text
opencode run --format json --dir <dir> --model <provider/id> [--variant <effort>]
  [--session <id> | --session <parent> --fork] [--agent <name>] [--thinking] [--pure]
  [--auto | --dangerously-skip-permissions] <prompt>
FieldDefaultMeaning
cli_agent""--agent NAME
thinkingFalse--thinking: reasoning streamed as reasoning events
pureFalse--pure: no plugins
unattendedNone--auto (opencode) / --dangerously-skip-permissions (mimo); None: on where a rung is named
permission_tableNoneOPENCODE_PERMISSION / MIMOCODE_PERMISSION carrying the rung and web answer; None: written where either is said; False refused beside a withholding rung or web_search=False (Unserved: permission_table=False withholds the only table this backend hears permission='read-only' in)
  • The table is written for the turn, never to a settings file. Its read and edit rules are relative to the top of the git checkout the session is in, or /.
  • Fake tools are a local server humanize in mcp, merged into the OPENCODE_CONFIG_CONTENT / MIMOCODE_CONFIG_CONTENT the turn would otherwise have (the account's, else an exported one); the model sees them as humanize_<name>. The flow's skills the session carries are added to skills.paths there, after any it already names. A value that is not a JSON object (JSONC included) fails the turn with ValueError.
  • mimocode reads ~/.claude.json at start; where the fence does not let it, MIMOCODE_DISABLE_CLAUDE_CODE=1 is set.
  • Both keep sessions in SQLite; traces read it with a query.

pi ​

pi. One pi --mode rpc per session:

text
pi --mode rpc --model <model> --session-id <id> [--provider <gateway>] [--fork <parent>]
  [--thinking <rung>]
  [--exclude-tools bash,edit,write,powershell] [--no-context-files] [--no-extensions]
  [--offline] [--append-system-prompt <s>]... [--skill <path>]...
  [--extension <hmz>/coganchor/agents/pi_tools.cjs]
FieldDefaultMeaning
compiledTrueNODE_COMPILE_CACHE at $TMPDIR/humanize-<uid>/compiled/pi unless already set; not for anchored turns
context_filesTrueFalse is --no-context-files
extensionsTrueFalse is --no-extensions
offlineFalse--offline; forced under online NONE
append_system_prompt()--append-system-prompt per entry (text or path)
skill_paths()--skill per entry

--session-id rather than --continue, which resumes the newest session in the directory. Each process runs in a virtual home (PI_CODING_AGENT_DIR): a gateway account's provider (humanize-<hash>) is declared in its copy of models.json, never in the user's, and a models.json that is not plain JSON (pi takes comments) fails the turn with ValueError. pi 0.85.1 has no web tools of its own. Offered fake tools add the --extension, which reads the bridge's argv from HMZ_PI_TOOLS and registers each tool under its own name.

Qwen Code ​

qwen. Ordinary turns are lines on a held process (--input-format stream-json); a shaped turn runs as a separate command (--json-schema is refused with stream-json input). The opening turn is --session-id <new uuid>; later ones --resume <id>; a fork --resume <parent> --fork-session. An id already used in the project is retried under a new one.

FieldDefaultMeaning
headless_defaultsTruegeneral.preventSystemSleep: false and general.enableAutoUpdate: false, where the user's settings.json says nothing of either
compile_cacheTrueNODE_COMPILE_CACHE at $TMPDIR/humanize-<uid>/compiled/qwen unless set; not for anchored turns
partial_messagesFalse--include-partial-messages
  • Each process runs in a virtual home (QWEN_HOME). The effort (model.reasoningEffort), a PRE_TOOL_USE table (hooks, after the user's own, with disableAllHooks: false) and offered fake tools (mcpServers.humanize) are merged into its copy of the user's settings.json; the user's own other servers and hooks stay. A settings.json that is not plain JSON (Qwen takes comments) fails the turn with Unrecoverable. The system layers are the machine's: a QWEN_CODE_SYSTEM_SETTINGS_PATH or QWEN_CODE_SYSTEM_DEFAULTS_PATH of the user's is watched for changes and never set. Settings the driver cannot parse keep a fresh process per turn.
  • Being the user level, what is merged is outranked by the workspace's own .qwen/settings.json and the system layer: a project that sets disableAllHooks or model.reasoningEffort there gets its own way. A fenced agent may write its virtual home, that copy included; Qwen Code reads it as the process starts.
  • A change of effort, of the hook table or of which tool names are offered restarts the process.
  • Usage counts each assistant message once; the terminal summary fills only missing fields.

A CLI of your own ​

Any agent speaking the Agent Client Protocol can be added as a backend, from the TUI (/settings accounts → Add a custom CLI) or with backends.remember(name, command). It is written under clis in $HUMANIZE_HOME/settings.yaml and is a backend in every workspace from the next prompt.

clis is a mapping keyed by name; each value is the command (a list of strings), or a mapping:

yaml
clis:
  my-agent:
    command: [my-agent, --acp]
    hosts: [api.my-agent.example]
    state: [~/.my-agent]
KeyMeaning
commandargv that starts it
hostshosts reachable under online NONE, spelled as Profile.hosts spells them; none: a network-cutting permission is refused (HarnessSandboxed) naming what to declare
statepaths it writes its state to, granted whatever user says; none: nothing of the home is writable

backends.declared(name) returns (hosts, state); backends.forget(name) removes one. An unreadable settings file reads as no added CLIs.

remember refusalValueError message
no commandan added CLI needs a command to start it with
the command is a built-in backend (any alias, by its last path component)claude is already a backend Humanize drives
a name other than the command's ownan added CLI is called what it runs, so my-agent is added as my-agent rather than as foo

Protocol use: initialize; session/new; session/prompt per turn; session/update notifications for output; session/resume or session/load (whichever the agent advertises) to continue; session/fork; session/cancel. No model, effort or mode is sent (session/set_model, session/set_config_option, session/set_mode are unused): model and effort read as configured. Every permission request is granted, by the option's kind. It cannot be steered, has no goal, reports no usage and has no trace.

AcpAgentConfig fields, off by default:

FieldTypeMeaning
clistrthe name it was added under
commandtuple[str, ...]overrides the recorded command
reads_filesboolserve fs/read_text_file from this machine
writes_filesboolserve fs/write_text_file
terminalsboolserve terminal/* (start, read, wait, kill)
mcp_serverstuple[McpServer, ...]MCP servers added to the agent's own; fake tools add a humanize one

The first three are refused for an agent under a native anchor. Under a fence, what this client does for the agent is held to the same fence: a refused file or path is answered with reject_once, and a terminal command runs inside the fence.

Defined in src/hmz/coganchor/backends.py (speaking, declared, remember, forget, _speaks), src/hmz/coganchor/agents/acp.py.

Bundle patching ​

hmz.coganchor.agents.patching rewrites a copy of a Bun standalone executable (claude, opencode) where no shallower route reaches a behaviour. patched(profile, path, patches) returns the copy's path, or None.

  • Never the installed binary: the copy lives in a directory of Humanize's own for one session.
  • Fingerprinted against Profile.bundles (a path glob, a regular expression one module must match, an optional SHA-256); the system tests check them against the installed CLIs.
  • Any mismatch or failure returns None, is logged, and the shallower route is used.
  • Every rewrite is the same length as what it replaces; the module's precompiled bytecode is cleared.

Names, clones and the person ​

AttributeMeaning
agent.idthe given name=, or a codename; it never changes, and a flow's role is not it
agent.backendthe backend's name, or an ACP CLI's
agent.openedevery session id it opened, oldest first
agent.sessionsthe sessions still held
agent.configits config
agent.specCLI[@ACCOUNT]/MODEL

An unnamed agent gets a codename from Amphoreus (Honkai: Star Rail): 29 designations used verbatim while any is free (half the time), otherwise morphemes joined at a capital plus digits. No codename repeats in one process.

agent.clone(*, config=None, name=None, skills=None) makes a fresh agent: same settings unless given, no conversations, spending, watchers or hooks, a new codename unless named; it spends the same allowance. reconfigure(config), runs_on(machine), loads(skills) and disable_goals() change an agent in place; flows have none of them, only derive.

The person as an agent ​

HumanAgent(name="human") asks the person and answers with what they typed. It runs no model, spends nothing, runs no moment, and emits no begins/ends.

With schema=, the person is asked one question per field:

In the modelAsked as
description=the question (the field name where none)
Literal[…]those words as options
boolyes / no
a defaultor - for <default>; - takes it
list[str]one comma-separated line

A refused value is asked again a bounded number of times. Under suppress, an unfinished questionnaire answers None. In a flow the person is an Outworlder role; an away outworlder answers with the model built from its defaults, or raises OutworlderAway for a field without one.

Environment variables ​

The variables the agent layer reads or sets. Every variable Humanize reads is listed in Environment variables.

VariableRead byEffect
HUMANIZE_HOMEeverythingHumanize's home (default ~/.hmz): settings.yaml, providers/; prices.json and compiled/ are in $TMPDIR/humanize-<uid>/
HUMANIZE_WATCHDOGthe watchdogseconds of silence allowed; 0 or less disables it
HUMANIZE_SESSIONSsession keepingoff, 0 or no: sessions stay in the CLI's home
HUMANIZE_PRICESpricesthe price list's URL or path; off, 0, no, none or empty disables fetching
each backend's home variablethe drivers, traces, providerssee State, logs and traces
XDG_CONFIG_HOMEcredential and skill pathsthe config/ root

Set by drivers on a turn:

VariableBackendValue
CLAUDE_CODE_DISABLE_BACKGROUND_TASKSclaude1
NODE_OPTIONS, HMZ_PRELOAD_AT, HMZ_PRELOAD_INkimi, pi, qwen, mimothe runtime-report preload, only while a PRE_TOOL_USE hook is hung
NODE_COMPILE_CACHEpi, qwen$TMPDIR/humanize-<uid>/compiled/<cli>, unless already set
OPENCODE_PERMISSION, MIMOCODE_PERMISSIONopencode, mimothe permission table
MIMOCODE_DISABLE_CLAUDE_CODEmimo1 where the fence hides ~/.claude.json
PKG_NATIVE_CACHE_PATHdshinside the fence's tmp, when fenced
TMPDIR, TMP, TEMP, HTTPS_PROXY and relatedfenced turnsthe fence's scratch directory and proxy
HUMANIZE, HUMANIZE_TARGET, HUMANIZE_WORKSPACEanchored turnssee Remote execution

API summary ​

python
type Where = str | os.PathLike[str] | None

class AgentBase:
    moments: ClassVar[frozenset[Moment]]
    pursues: ClassVar[bool]
    service_tiers: ClassVar[tuple[str, ...]]
    rungs: ClassVar[tuple[str, ...]]
    counts: ClassVar[frozenset[str]]

    id: str; backend: str; config: AgentConfig; effort: str  # settable
    spec: str; opened: list[str]; sessions: list[SessionBase]; stopped: bool
    anchor: AnchorConfig | None; provider: Provider | None
    hooks: Hooks; loaded: tuple[Loaded, ...]; allowance: Ledger | None
    ask: Callable[[Question], str | None] | None
    waiting: Callable[[], list[str]] | None
    prompting: Callable[[], str | None] | None

    def __call__(prompt, *, suppress=False, schema=…, cwd: Where = None) -> str | T | None
    def pursue(objective, *, suppress=False, cwd: Where = None) -> str
    def new(cwd: Where = None) -> SessionBase
    async def aturn(...); async def apursue(...)
    def batch(prompts, *, suppress=False, schema=…, at_once=0, cwd: Where = None) -> list
    async def abatch(...)
    def batch_new(count, cwd: Where = None) -> list[SessionBase]
    def spent() -> Usage; def rate(over=WINDOW) -> Usage; def juice(over=WINDOW) -> float
    def clone(*, config=None, name=None, skills=None) -> Self
    def reconfigure(config); def runs_on(machine); def loads(skills); def rename(name)
    def disable_goals(); def stop()
    def watch(listener: Callable[[AgentBase, SessionBase | None, Event], None])
    def asked(question) -> str | None; def prompted() -> str | None
    def stands_in() -> AgentBase | None
    def node() -> Provider
    def environment() -> Mapping[str, str]; def hushed() -> frozenset[str]
    def fenced() -> Fence | None; def natively(fence: Fence) -> Fence

class SessionBase:
    shapes: ClassVar[bool]; steers: ClassVar[bool]; takes_tools: ClassVar[bool]
    narrates: ClassVar[bool]; forks_elsewhere: ClassVar[bool]; cuts_transport: ClassVar[bool]

    id: str; named: str | None; cwd: str; forks: bool
    effort: str; budget: Budget | None  # settable
    skills: tuple[str, ...]; tools: tuple[FakeTool, ...]

    def __call__(prompt, *, suppress=False, schema=…) -> str | T | None
    def stream(prompt, *, schema=None) -> Iterator[Event]
    def pursue(objective, *, suppress=False) -> str
    def fork(*, into=None, cwd: Where = None) -> SessionBase
    async def aturn(...); async def apursue(...)
    def interject(text); def interrupt(*, why); def cut(*, why)
    def loads(skills | None); def offers(tools | None)
    def spent() -> Usage; def rate(over=WINDOW) -> Usage; def juice(over=WINDOW) -> float
    def close()

class Failed(subprocess.CalledProcessError): fault: str; fix: str
class Unrecoverable(Failed): ...
class Stopped(Exception): ...
class Unserved(ValueError): ...
class Unfenced(Unserved): ...
class Unhooked(ValueError): ...

CommandSessionBase (one command per turn) and StreamSessionBase (one long-lived process fed a line at a time) are the two shapes a driver subclasses; the contract is specs/features/agents.md.

Released under the Apache-2.0 licence.