agent-hitch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +236 -0
- package/bin/hitch.js +9 -0
- package/docs/schemas/artifact-manifest.schema.json +46 -0
- package/docs/schemas/eval-request.schema.json +21 -0
- package/docs/schemas/eval-result.schema.json +40 -0
- package/docs/schemas/event.schema.json +29 -0
- package/docs/schemas/resolved-revision.schema.json +60 -0
- package/docs/schemas/result.schema.json +44 -0
- package/docs/schemas/run-request.schema.json +56 -0
- package/docs/schemas/workspace.schema.json +55 -0
- package/integrations/harbor/hitch_harbor_agent.py +217 -0
- package/package.json +42 -0
- package/src/adapters.js +361 -0
- package/src/artifacts.js +948 -0
- package/src/cli.js +505 -0
- package/src/config.js +51 -0
- package/src/daemon.js +416 -0
- package/src/engine.js +386 -0
- package/src/errors.js +12 -0
- package/src/eval-tools.js +331 -0
- package/src/evals.js +276 -0
- package/src/events.js +69 -0
- package/src/fs.js +43 -0
- package/src/harbor-backend.js +285 -0
- package/src/harness-reference.js +78 -0
- package/src/line-stream.js +17 -0
- package/src/locks.js +33 -0
- package/src/process.js +49 -0
- package/src/registry.js +84 -0
- package/src/scheduler.js +156 -0
- package/src/workspaces.js +893 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://agent-hitch.local/schemas/result.schema.json",
|
|
4
|
+
"title": "Hitch run result",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": true,
|
|
7
|
+
"required": ["schema_version", "run_id", "status", "exit_code", "completed_at"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": {
|
|
10
|
+
"const": "1"
|
|
11
|
+
},
|
|
12
|
+
"run_id": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"pattern": "^run_[a-f0-9]{32}$"
|
|
15
|
+
},
|
|
16
|
+
"status": {
|
|
17
|
+
"enum": ["succeeded", "failed", "timed_out", "cancelled"]
|
|
18
|
+
},
|
|
19
|
+
"exit_code": {
|
|
20
|
+
"type": "integer",
|
|
21
|
+
"minimum": 0
|
|
22
|
+
},
|
|
23
|
+
"output": {
|
|
24
|
+
"type": "string"
|
|
25
|
+
},
|
|
26
|
+
"error": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"required": ["code", "message"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"code": { "type": "string", "minLength": 1 },
|
|
31
|
+
"message": { "type": "string" }
|
|
32
|
+
},
|
|
33
|
+
"additionalProperties": true
|
|
34
|
+
},
|
|
35
|
+
"started_at": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"format": "date-time"
|
|
38
|
+
},
|
|
39
|
+
"completed_at": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"format": "date-time"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://agent-hitch.local/schemas/run-request.schema.json",
|
|
4
|
+
"title": "Hitch run request",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["prompt"],
|
|
8
|
+
"oneOf": [
|
|
9
|
+
{
|
|
10
|
+
"required": ["harness_ref"],
|
|
11
|
+
"not": { "required": ["agent"] }
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"required": ["agent"],
|
|
15
|
+
"not": { "required": ["harness_ref"] }
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"schema_version": {
|
|
20
|
+
"const": "1"
|
|
21
|
+
},
|
|
22
|
+
"agent": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"minLength": 1
|
|
25
|
+
},
|
|
26
|
+
"harness_ref": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 1
|
|
29
|
+
},
|
|
30
|
+
"model": {
|
|
31
|
+
"type": "string"
|
|
32
|
+
},
|
|
33
|
+
"cwd": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"minLength": 1
|
|
36
|
+
},
|
|
37
|
+
"workspace_mode": {
|
|
38
|
+
"enum": ["shared", "worktree", "copy"],
|
|
39
|
+
"default": "shared"
|
|
40
|
+
},
|
|
41
|
+
"prompt": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"minLength": 1
|
|
44
|
+
},
|
|
45
|
+
"timeout_ms": {
|
|
46
|
+
"type": "number",
|
|
47
|
+
"minimum": 0
|
|
48
|
+
},
|
|
49
|
+
"agent_args": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"items": {
|
|
52
|
+
"type": "string"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://agent-hitch.local/schemas/workspace.schema.json",
|
|
4
|
+
"title": "Hitch run workspace record",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": true,
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"run_id",
|
|
10
|
+
"mode",
|
|
11
|
+
"status",
|
|
12
|
+
"source_workspace",
|
|
13
|
+
"source_realpath",
|
|
14
|
+
"execution_workspace",
|
|
15
|
+
"snapshot",
|
|
16
|
+
"retained"
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"schema_version": { "const": "1" },
|
|
20
|
+
"run_id": { "type": "string", "pattern": "^run_[a-f0-9]{32}$" },
|
|
21
|
+
"mode": { "enum": ["shared", "worktree", "copy"] },
|
|
22
|
+
"status": {
|
|
23
|
+
"enum": [
|
|
24
|
+
"planned",
|
|
25
|
+
"provisioning",
|
|
26
|
+
"ready",
|
|
27
|
+
"running",
|
|
28
|
+
"retained",
|
|
29
|
+
"orphaned",
|
|
30
|
+
"released",
|
|
31
|
+
"cancelled",
|
|
32
|
+
"failed",
|
|
33
|
+
"unused",
|
|
34
|
+
"removed"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"source_workspace": { "type": "string", "minLength": 1 },
|
|
38
|
+
"source_realpath": { "type": "string", "minLength": 1 },
|
|
39
|
+
"execution_workspace": { "type": "string", "minLength": 1 },
|
|
40
|
+
"changed": { "type": ["boolean", "null"] },
|
|
41
|
+
"retained": { "type": "boolean" },
|
|
42
|
+
"snapshot": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"required": ["consistency", "source_subdirectory"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"consistency": { "enum": ["none", "git_commit", "best_effort"] },
|
|
47
|
+
"commit": { "type": "string", "pattern": "^[a-f0-9]{40,64}$" },
|
|
48
|
+
"source_subdirectory": { "type": "string" },
|
|
49
|
+
"content_digest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
|
|
50
|
+
"captured_at": { "type": "string", "format": "date-time" }
|
|
51
|
+
},
|
|
52
|
+
"additionalProperties": true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Harbor custom agent that runs an immutable harness through Hitch in a trial container."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shlex
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from harbor.agents.base import BaseAgent
|
|
12
|
+
from harbor.environments.base import BaseEnvironment, ExecResult
|
|
13
|
+
from harbor.models.agent.context import AgentContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HitchHarborAgent(BaseAgent):
|
|
17
|
+
"""Upload Hitch into a Harbor environment and delegate the agent phase to it."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
logs_dir: Path,
|
|
22
|
+
harness_ref: str,
|
|
23
|
+
revision_identity: str,
|
|
24
|
+
hitch_runtime_dir: str,
|
|
25
|
+
candidate_id: str = "candidate-1",
|
|
26
|
+
hitch_timeout_ms: int = 900_000,
|
|
27
|
+
agent_args: list[str] | None = None,
|
|
28
|
+
workdir: str = "/app",
|
|
29
|
+
**kwargs: Any,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(logs_dir=logs_dir, **kwargs)
|
|
32
|
+
self.harness_ref = harness_ref
|
|
33
|
+
self.revision_identity = revision_identity
|
|
34
|
+
self.hitch_runtime_dir = Path(hitch_runtime_dir)
|
|
35
|
+
self.candidate_id = candidate_id
|
|
36
|
+
self.hitch_timeout_ms = int(hitch_timeout_ms)
|
|
37
|
+
self.agent_args = list(agent_args or [])
|
|
38
|
+
self.workdir = workdir
|
|
39
|
+
self._hitch_version: str | None = None
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def name() -> str:
|
|
43
|
+
return "hitch"
|
|
44
|
+
|
|
45
|
+
def version(self) -> str | None:
|
|
46
|
+
return self._hitch_version
|
|
47
|
+
|
|
48
|
+
async def setup(self, environment: BaseEnvironment) -> None:
|
|
49
|
+
if not self.hitch_runtime_dir.is_dir():
|
|
50
|
+
raise RuntimeError(f"Hitch runtime directory does not exist: {self.hitch_runtime_dir}")
|
|
51
|
+
await environment.upload_dir(self.hitch_runtime_dir, "/opt/hitch")
|
|
52
|
+
await self._ensure_node(environment)
|
|
53
|
+
version = await self._exec(environment, f"{self._node_prefix()} node /opt/hitch/bin/hitch.js --version")
|
|
54
|
+
self._hitch_version = (version.stdout or "").strip() or None
|
|
55
|
+
prepare = " ".join(
|
|
56
|
+
[
|
|
57
|
+
self._node_prefix(),
|
|
58
|
+
"HITCH_ROOT=/tmp/hitch-state",
|
|
59
|
+
"node /opt/hitch/bin/hitch.js prepare",
|
|
60
|
+
shlex.quote(self.harness_ref),
|
|
61
|
+
"--json",
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
await self._exec(environment, prepare)
|
|
65
|
+
|
|
66
|
+
async def run(
|
|
67
|
+
self,
|
|
68
|
+
instruction: str,
|
|
69
|
+
environment: BaseEnvironment,
|
|
70
|
+
context: AgentContext,
|
|
71
|
+
) -> None:
|
|
72
|
+
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
temporary: Path | None = None
|
|
74
|
+
try:
|
|
75
|
+
with tempfile.NamedTemporaryFile(
|
|
76
|
+
mode="w",
|
|
77
|
+
encoding="utf-8",
|
|
78
|
+
prefix="hitch-instruction-",
|
|
79
|
+
suffix=".txt",
|
|
80
|
+
dir=self.logs_dir,
|
|
81
|
+
delete=False,
|
|
82
|
+
) as handle:
|
|
83
|
+
handle.write(instruction)
|
|
84
|
+
temporary = Path(handle.name)
|
|
85
|
+
remote_instruction = "/tmp/hitch-instruction.txt"
|
|
86
|
+
await environment.upload_file(temporary, remote_instruction)
|
|
87
|
+
finally:
|
|
88
|
+
if temporary is not None:
|
|
89
|
+
temporary.unlink(missing_ok=True)
|
|
90
|
+
|
|
91
|
+
arguments = [
|
|
92
|
+
self._node_prefix(),
|
|
93
|
+
"HITCH_ROOT=/tmp/hitch-state",
|
|
94
|
+
"node /opt/hitch/bin/hitch.js run",
|
|
95
|
+
"--harness",
|
|
96
|
+
shlex.quote(self.harness_ref),
|
|
97
|
+
"--cwd",
|
|
98
|
+
shlex.quote(self.workdir),
|
|
99
|
+
"--workspace-mode",
|
|
100
|
+
"shared",
|
|
101
|
+
"--prompt-file",
|
|
102
|
+
shlex.quote(remote_instruction),
|
|
103
|
+
"--timeout",
|
|
104
|
+
str(self.hitch_timeout_ms),
|
|
105
|
+
"--output",
|
|
106
|
+
"jsonl",
|
|
107
|
+
]
|
|
108
|
+
if self.model_name:
|
|
109
|
+
arguments.extend(["--model", shlex.quote(self.model_name)])
|
|
110
|
+
for value in self.agent_args:
|
|
111
|
+
arguments.extend(["--agent-arg", shlex.quote(value)])
|
|
112
|
+
command = (
|
|
113
|
+
"set -o pipefail; "
|
|
114
|
+
+ " ".join(arguments)
|
|
115
|
+
+ " 2> >(tee /logs/agent/hitch-stderr.log >&2)"
|
|
116
|
+
+ " | tee /logs/agent/hitch-events.jsonl"
|
|
117
|
+
)
|
|
118
|
+
execution = await environment.exec(command, cwd=self.workdir)
|
|
119
|
+
events = self._events(execution.stdout or "")
|
|
120
|
+
run_id = next((event.get("run_id") for event in events if event.get("run_id")), None)
|
|
121
|
+
hitch_result = None
|
|
122
|
+
if run_id:
|
|
123
|
+
result_path = f"/tmp/hitch-state/runs/{run_id}/result.json"
|
|
124
|
+
result = await environment.exec(
|
|
125
|
+
f"cat {shlex.quote(result_path)} | tee /logs/agent/hitch-result.json"
|
|
126
|
+
)
|
|
127
|
+
if result.return_code == 0 and result.stdout:
|
|
128
|
+
hitch_result = json.loads(result.stdout)
|
|
129
|
+
context.metadata = {
|
|
130
|
+
"candidate_id": self.candidate_id,
|
|
131
|
+
"harness_ref": self.harness_ref,
|
|
132
|
+
"revision_identity": self.revision_identity,
|
|
133
|
+
"hitch_run_id": run_id,
|
|
134
|
+
"hitch_status": hitch_result.get("status") if hitch_result else None,
|
|
135
|
+
"hitch_artifact_id": hitch_result.get("artifact_id") if hitch_result else None,
|
|
136
|
+
}
|
|
137
|
+
if execution.return_code != 0:
|
|
138
|
+
message = (execution.stderr or "").strip()
|
|
139
|
+
if hitch_result and hitch_result.get("error", {}).get("message"):
|
|
140
|
+
message = hitch_result["error"]["message"]
|
|
141
|
+
raise RuntimeError(
|
|
142
|
+
f"Hitch agent run failed with code {execution.return_code}: {message or 'no diagnostic output'}"
|
|
143
|
+
)
|
|
144
|
+
if hitch_result is None:
|
|
145
|
+
raise RuntimeError("Hitch agent run completed without a persisted result")
|
|
146
|
+
if hitch_result.get("revision_identity") != self.revision_identity:
|
|
147
|
+
raise RuntimeError(
|
|
148
|
+
"Hitch resolved a different harness revision inside the trial container: "
|
|
149
|
+
f"expected {self.revision_identity}, got {hitch_result.get('revision_identity')}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def _ensure_node(self, environment: BaseEnvironment) -> None:
|
|
153
|
+
probe = await environment.exec(
|
|
154
|
+
"node -e 'process.exit(Number(process.versions.node.split(\".\")[0]) >= 22 ? 0 : 1)'"
|
|
155
|
+
)
|
|
156
|
+
needs_git = "@commit:" in self.harness_ref
|
|
157
|
+
git_probe = await environment.exec("command -v git >/dev/null 2>&1")
|
|
158
|
+
if probe.return_code == 0 and (not needs_git or git_probe.return_code == 0):
|
|
159
|
+
return
|
|
160
|
+
prerequisites = """
|
|
161
|
+
set -eu
|
|
162
|
+
if command -v curl >/dev/null 2>&1 && command -v git >/dev/null 2>&1; then exit 0; fi
|
|
163
|
+
if command -v apt-get >/dev/null 2>&1; then
|
|
164
|
+
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates git
|
|
165
|
+
elif command -v apk >/dev/null 2>&1; then
|
|
166
|
+
apk add --no-cache curl ca-certificates git bash
|
|
167
|
+
elif command -v dnf >/dev/null 2>&1; then
|
|
168
|
+
dnf install -y curl ca-certificates git
|
|
169
|
+
elif command -v yum >/dev/null 2>&1; then
|
|
170
|
+
yum install -y curl ca-certificates git
|
|
171
|
+
else
|
|
172
|
+
echo 'Hitch requires Node.js 22+ and could not install curl in this task image' >&2
|
|
173
|
+
exit 1
|
|
174
|
+
fi
|
|
175
|
+
"""
|
|
176
|
+
await self._exec(environment, prerequisites, user=0)
|
|
177
|
+
if probe.return_code == 0:
|
|
178
|
+
return
|
|
179
|
+
install = """
|
|
180
|
+
set -eu
|
|
181
|
+
export NVM_DIR=/opt/hitch-node
|
|
182
|
+
mkdir -p "$NVM_DIR"
|
|
183
|
+
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
|
184
|
+
. "$NVM_DIR/nvm.sh"
|
|
185
|
+
nvm install 22
|
|
186
|
+
nvm alias default 22
|
|
187
|
+
node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)'
|
|
188
|
+
"""
|
|
189
|
+
await self._exec(environment, install, user=0)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _node_prefix() -> str:
|
|
193
|
+
return "if [ -s /opt/hitch-node/nvm.sh ]; then export NVM_DIR=/opt/hitch-node; . /opt/hitch-node/nvm.sh; fi;"
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _events(output: str) -> list[dict[str, Any]]:
|
|
197
|
+
events: list[dict[str, Any]] = []
|
|
198
|
+
for line in output.splitlines():
|
|
199
|
+
try:
|
|
200
|
+
value = json.loads(line)
|
|
201
|
+
except json.JSONDecodeError:
|
|
202
|
+
continue
|
|
203
|
+
if isinstance(value, dict):
|
|
204
|
+
events.append(value)
|
|
205
|
+
return events
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
async def _exec(
|
|
209
|
+
environment: BaseEnvironment,
|
|
210
|
+
command: str,
|
|
211
|
+
user: str | int | None = None,
|
|
212
|
+
) -> ExecResult:
|
|
213
|
+
result = await environment.exec(command, user=user)
|
|
214
|
+
if result.return_code != 0:
|
|
215
|
+
diagnostic = (result.stderr or result.stdout or "no output").strip()
|
|
216
|
+
raise RuntimeError(f"container setup command failed ({result.return_code}): {diagnostic}")
|
|
217
|
+
return result
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-hitch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A local runtime and daemon for coding agents",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-agents",
|
|
7
|
+
"cli",
|
|
8
|
+
"coding-agents",
|
|
9
|
+
"daemon"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/rsi-gear/agent-hitch#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/rsi-gear/agent-hitch/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/rsi-gear/agent-hitch.git"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"hitch": "bin/hitch.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin/",
|
|
25
|
+
"src/",
|
|
26
|
+
"integrations/harbor/hitch_harbor_agent.py",
|
|
27
|
+
"docs/schemas/"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test",
|
|
31
|
+
"coverage": "node --experimental-test-coverage --test-coverage-include=src/*.js --test-coverage-lines=75 --test-coverage-branches=65 --test-coverage-functions=70 --test",
|
|
32
|
+
"check": "node scripts/check-syntax.js && node --test",
|
|
33
|
+
"prepublishOnly": "npm run check"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"license": "Apache-2.0"
|
|
42
|
+
}
|