Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions hermit/adapter/templates/ahe/hook_hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
the wires, and the same file re-runs both CLIs on a scripted model when they are installed and `--integration` is
given.

Pi reads no hook file: its deployment lists the hook commands in AGENTS.md for the agent to run by hand. Run so, with
no payload (the `pi` wire), session_start still hands over the memory; the other hooks have no tool call, prompt or
stop to act on.
Pi and DeepSeek Harness read no hook file: their deployments list the hook commands in AGENTS.md for the agent to
run by hand. Run so, with no payload (the `pi` and `dsh` wires), session_start still hands over the memory; the other
hooks have no tool call, prompt or stop to act on.

Stdlib only, beside hook_runner.py in the adapter's copy: nothing of the framework is installed where the agent runs.
"""
Expand Down Expand Up @@ -480,8 +480,14 @@ class Pi(Wire):
tool_kinds = ClaudeCode.tool_kinds


CODEX, CLAUDE_CODE, PI = Codex(), ClaudeCode(), Pi()
HOSTS = {CODEX.name: CODEX, CLAUDE_CODE.name: CLAUDE_CODE, PI.name: PI}
class Dsh(Pi):
"""DeepSeek Harness runs no hook file of its own either: its deployment lists each hook command in AGENTS.md, as
Pi's does, and a command run so has no payload."""
name, label = "dsh", "DeepSeek Harness"


CODEX, CLAUDE_CODE, PI, DSH = Codex(), ClaudeCode(), Pi(), Dsh()
HOSTS = {CODEX.name: CODEX, CLAUDE_CODE.name: CLAUDE_CODE, PI.name: PI, DSH.name: DSH}
HOOK_HOSTS = (CODEX.name, CLAUDE_CODE.name) # the hosts that run the hooks themselves


Expand Down
8 changes: 4 additions & 4 deletions hermit/agent/acting/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def run(self, task: dict, adapter: Path | None) -> RunOutput
A config names the agent:

agent:
name: react # a built-in: react | codex | claude-code | pi
name: react # a built-in: react | codex | claude-code | pi | dsh
name: agent/agent.py:CLBenchAgent # or the task's own class (file.py:Class, module:Class)
name: agent/pipeline.py:run # or a plain function run(task, adapter), wrapped as a FunctionAgent
concurrency: 4
Expand Down Expand Up @@ -75,9 +75,9 @@ def builtin(name: str):
if name == "react":
from hermit.agent.acting.react import ReactAgent
return ReactAgent
if name in ("cli", "codex", "claude-code", "claude", "pi"):
if name in ("cli", "codex", "claude-code", "claude", "pi", "dsh"):
from hermit.agent.acting.workspace import CliAgent
backend = {"codex": "codex", "claude-code": "claude", "claude": "claude", "pi": "pi"}.get(name)
backend = {"codex": "codex", "claude-code": "claude", "claude": "claude", "pi": "pi", "dsh": "dsh"}.get(name)
if backend is None:
return CliAgent
return lambda params, base: CliAgent({"backend": backend, **(params or {})}, base)
Expand All @@ -87,7 +87,7 @@ def builtin(name: str):
return None


BUILTIN_NAMES = ("react", "codex", "claude-code", "pi", "cli", "harbor")
BUILTIN_NAMES = ("react", "codex", "claude-code", "pi", "dsh", "cli", "harbor")


def load_agent(name: str, params: dict | None, base: Path) -> Agent:
Expand Down
28 changes: 21 additions & 7 deletions hermit/agent/acting/workspace.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""The `codex`, `claude-code` and `pi` agents: one task run by the agent CLI in a workspace the adapter is deployed
into. They are the framework's one way to act with a coding-agent CLI; a task adds only what is its own.
"""The `codex`, `claude-code`, `pi` and `dsh` agents: one task run by the agent CLI in a workspace the adapter is
deployed into. They are the framework's one way to act with a coding-agent CLI; a task adds only what is its own.

agent:
name: codex # or claude-code; `cli` with params.backend is the same
Expand Down Expand Up @@ -69,7 +69,7 @@
from hermit.agent.base.login import LOGINS
from hermit.task import RunOutput, load_callable

BACKENDS = ("codex", "claude", "pi")
BACKENDS = ("codex", "claude", "pi", "dsh")
DEPLOYS = ("host",)
ISOLATIONS = ("none", "container")
INPUT_FIELDS = ("question", "prompt", "instruction", "input", "query")
Expand Down Expand Up @@ -144,6 +144,9 @@ def deploy(adapter: Path, workspace: Path, backend: str, *, context: str | None
if backend == "pi":
from hermit.hosts.pi import prepare as prepare_pi
return prepare_pi(adapter, workspace, sys.executable, context=context)
if backend == "dsh":
from hermit.hosts.dsh import prepare as prepare_dsh
return prepare_dsh(adapter, workspace, sys.executable, context=context)
from hermit.hosts.claude_code import prepare
return prepare(adapter, workspace, sys.executable, context=context, serve_tools=serve_tools)

Expand All @@ -170,9 +173,12 @@ def _deploy(task: dict, adapter: Path | None, work: Path, backend: str, params:
def _session_args(backend: str, deployed: dict, work: Path) -> list[str]:
"""What the session needs to use what the deployment connected: the workspace's own Codex configuration (its
tools, roles and hooks) trusted, as a person trusts a deployed project, and its sub-agents enabled; Pi's project
configuration (.pi/mcp.json, .pi/skills) trusted for the session (--approve), without which Pi skips it."""
configuration (.pi/mcp.json, .pi/skills) trusted for the session (--approve), without which Pi skips it; dsh's
patch layer of the adapter's tools (.dsh/hermit.patch.yml), which dsh loads only when it is named (--patch)."""
if backend == "pi":
return ["--approve"]
if backend == "dsh":
return ["--patch", str(deployed["patch"])] if deployed.get("patch") else []
if backend == "codex":
return ["-c", f'projects={{"{work}" = {{ trust_level = "trusted" }} }}',
*(["--enable", "multi_agent"] if deployed.get("roles") else [])]
Expand Down Expand Up @@ -277,11 +283,17 @@ def _check(backend: str, params: dict) -> str:
if not isinstance(params.get("web_search"), bool):
raise ValueError("agent.params.web_search must be set, true or false: Codex's hosted search runs at the "
"provider, past any sandbox or proxy, so it is never left to a default")
if backend == "pi": # Pi signs in through a model reference, and runs on this machine
if backend in ("pi", "dsh"): # they sign in through a model reference (dsh: or its own), on this machine
if params.get("login") is not None:
raise ValueError("agent pi takes no login: its model is a <backend>/<model> reference")
raise ValueError(f"agent {backend} takes no login: its model is a <backend>/<model> reference"
+ (", or a model of dsh's own provider" if backend == "dsh" else ""))
if params.get("isolation") == "container":
raise ValueError("isolation: container runs Codex or Claude Code; agent pi takes isolation: none")
raise ValueError(f"isolation: container runs Codex or Claude Code; agent {backend} takes isolation: none")
if backend == "dsh" and params.get("permission_mode") is not None:
from hermit.agent.base.cli import DSH_PERMISSION_MODES
if params["permission_mode"] not in DSH_PERMISSION_MODES:
raise ValueError(f"agent.dsh.permission_mode is one of {', '.join(DSH_PERMISSION_MODES)} (dsh's own "
f"sandbox), not {params['permission_mode']!r}")
return backend
login, label = params.get("login"), {"codex": "Codex", "claude": "Claude Code"}[backend]
if login is not None and login not in LOGINS:
Expand Down Expand Up @@ -311,6 +323,8 @@ def base_agent(backend: str, params: dict) -> BaseAgent:
if k in params and (params[k] is not None or k == "plan_max_wait_s")}
if params.get("claude_permission_mode"):
settings["permission_mode"] = params["claude_permission_mode"]
if backend == "dsh" and params.get("permission_mode"): # dsh's own sandbox (agent.dsh.permission_mode)
settings["permission_mode"] = params["permission_mode"]
return make(backend, {**settings, "web_search": params["web_search"]})


Expand Down
Loading
Loading