agentmint-hermes-runner 0.4.0__py3-none-any.whl
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.
- agentmint_hermes_runner/__init__.py +40 -0
- agentmint_hermes_runner/auth/__init__.py +5 -0
- agentmint_hermes_runner/auth/base.py +18 -0
- agentmint_hermes_runner/auth/bearer.py +29 -0
- agentmint_hermes_runner/auth/tempo.py +37 -0
- agentmint_hermes_runner/client.py +40 -0
- agentmint_hermes_runner/dispatcher.py +226 -0
- agentmint_hermes_runner/exceptions.py +28 -0
- agentmint_hermes_runner/hermes_patch.py +213 -0
- agentmint_hermes_runner/models.py +61 -0
- agentmint_hermes_runner/translation.py +93 -0
- agentmint_hermes_runner-0.4.0.dist-info/METADATA +117 -0
- agentmint_hermes_runner-0.4.0.dist-info/RECORD +15 -0
- agentmint_hermes_runner-0.4.0.dist-info/WHEEL +4 -0
- agentmint_hermes_runner-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from .auth.bearer import BearerAuth
|
|
2
|
+
from .auth.tempo import TempoAuth
|
|
3
|
+
from .dispatcher import AgentMintDispatcher
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
AgentMintError,
|
|
6
|
+
DispatchInterrupted,
|
|
7
|
+
DispatchTimeout,
|
|
8
|
+
UnsupportedToolset,
|
|
9
|
+
)
|
|
10
|
+
from .hermes_patch import install_delegate_task_wrapper
|
|
11
|
+
from .models import AgentRecord, DispatchResult, Task
|
|
12
|
+
from .translation import (
|
|
13
|
+
DEFAULT_TOOLSETS,
|
|
14
|
+
ROLE_HINTS,
|
|
15
|
+
TOOLSET_RESTRICTION_HINTS,
|
|
16
|
+
UNSUPPORTED_TOOLSETS,
|
|
17
|
+
compose_prompt,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "0.4.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AgentMintDispatcher",
|
|
24
|
+
"BearerAuth",
|
|
25
|
+
"TempoAuth",
|
|
26
|
+
"AgentRecord",
|
|
27
|
+
"DispatchResult",
|
|
28
|
+
"Task",
|
|
29
|
+
"AgentMintError",
|
|
30
|
+
"DispatchTimeout",
|
|
31
|
+
"DispatchInterrupted",
|
|
32
|
+
"UnsupportedToolset",
|
|
33
|
+
"compose_prompt",
|
|
34
|
+
"DEFAULT_TOOLSETS",
|
|
35
|
+
"ROLE_HINTS",
|
|
36
|
+
"TOOLSET_RESTRICTION_HINTS",
|
|
37
|
+
"UNSUPPORTED_TOOLSETS",
|
|
38
|
+
"install_delegate_task_wrapper",
|
|
39
|
+
"__version__",
|
|
40
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Auth(Protocol):
|
|
5
|
+
"""Transport strategy for AgentMint /a2a calls.
|
|
6
|
+
|
|
7
|
+
Implementations own the full HTTP exchange so we can support both
|
|
8
|
+
header-based auth (Bearer JWT) and external-CLI auth (Tempo's
|
|
9
|
+
`tempo request` subprocess that handles the 402 dance end-to-end).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def call(self, endpoint: str, method: str, body: bytes) -> bytes:
|
|
13
|
+
"""POST `body` to `endpoint` and return the response body bytes.
|
|
14
|
+
|
|
15
|
+
`method` is the JSON-RPC method name (for logging only — the actual
|
|
16
|
+
envelope is already serialized into `body`).
|
|
17
|
+
"""
|
|
18
|
+
...
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BearerAuth:
|
|
5
|
+
"""Stripe-Link credit-wallet authentication via cached JWT.
|
|
6
|
+
|
|
7
|
+
Bootstrap the JWT once via `link-cli` calling `credits.topup`; every
|
|
8
|
+
subsequent /a2a call uses `Authorization: Bearer <jwt>` and debits the
|
|
9
|
+
caller-wide credit wallet keyed by `link_stripe:cus_...`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, jwt: str, timeout: float = 30.0):
|
|
13
|
+
if not jwt:
|
|
14
|
+
raise ValueError("jwt is required")
|
|
15
|
+
self.jwt = jwt
|
|
16
|
+
self.timeout = timeout
|
|
17
|
+
|
|
18
|
+
def call(self, endpoint: str, method: str, body: bytes) -> bytes:
|
|
19
|
+
with httpx.Client(timeout=self.timeout) as http:
|
|
20
|
+
resp = http.post(
|
|
21
|
+
endpoint,
|
|
22
|
+
content=body,
|
|
23
|
+
headers={
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"Authorization": f"Bearer {self.jwt}",
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
resp.raise_for_status()
|
|
29
|
+
return resp.content
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TempoAuth:
|
|
5
|
+
"""Per-call MPP payment via the `tempo request` CLI.
|
|
6
|
+
|
|
7
|
+
The Tempo CLI handles the 402 challenge / USDC.e transfer / signature
|
|
8
|
+
end-to-end as a single subprocess, then prints the merchant response on
|
|
9
|
+
stdout. We just shell out and return the stdout bytes.
|
|
10
|
+
|
|
11
|
+
Note: `tempo-request` plugin must be at version 0.5.2. Newer versions
|
|
12
|
+
(0.6.0+) hit "Invalid base64 JSON header" against AgentMint's challenge.
|
|
13
|
+
Downgrade via: `tempo cli 0.0.0 downgrade tempo request cli to 0.5.2`
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
executable: str = "tempo",
|
|
19
|
+
account: str | None = None,
|
|
20
|
+
timeout: float = 120.0,
|
|
21
|
+
):
|
|
22
|
+
self.executable = executable
|
|
23
|
+
self.account = account
|
|
24
|
+
self.timeout = timeout
|
|
25
|
+
|
|
26
|
+
def call(self, endpoint: str, method: str, body: bytes) -> bytes:
|
|
27
|
+
cmd: list[str] = [self.executable, "request", "-X", "POST", "--json", body.decode("utf-8")]
|
|
28
|
+
if self.account:
|
|
29
|
+
cmd.extend(["-n", self.account])
|
|
30
|
+
cmd.append(endpoint)
|
|
31
|
+
proc = subprocess.run(
|
|
32
|
+
cmd,
|
|
33
|
+
capture_output=True,
|
|
34
|
+
timeout=self.timeout,
|
|
35
|
+
check=True,
|
|
36
|
+
)
|
|
37
|
+
return proc.stdout
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .auth.base import Auth
|
|
6
|
+
from .exceptions import AgentMintError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Client:
|
|
10
|
+
"""Minimal JSON-RPC 2.0 client for AgentMint /a2a.
|
|
11
|
+
|
|
12
|
+
Auth strategy owns the HTTP transport; this class only handles envelope
|
|
13
|
+
serialization and error mapping.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, endpoint: str, auth: Auth):
|
|
17
|
+
self.endpoint = endpoint
|
|
18
|
+
self.auth = auth
|
|
19
|
+
|
|
20
|
+
def call(self, method: str, params: dict[str, Any] | None = None) -> Any:
|
|
21
|
+
body = json.dumps(
|
|
22
|
+
{
|
|
23
|
+
"jsonrpc": "2.0",
|
|
24
|
+
"id": str(uuid.uuid4()),
|
|
25
|
+
"method": method,
|
|
26
|
+
"params": params or {},
|
|
27
|
+
}
|
|
28
|
+
).encode("utf-8")
|
|
29
|
+
resp_bytes = self.auth.call(self.endpoint, method, body)
|
|
30
|
+
try:
|
|
31
|
+
data = json.loads(resp_bytes)
|
|
32
|
+
except json.JSONDecodeError as e:
|
|
33
|
+
raise AgentMintError(f"non-JSON response from {self.endpoint}: {e}") from e
|
|
34
|
+
if "error" in data:
|
|
35
|
+
err = data["error"]
|
|
36
|
+
raise AgentMintError(
|
|
37
|
+
f"{err.get('code', 'unknown')}: {err.get('message', '')}",
|
|
38
|
+
data=err.get("data"),
|
|
39
|
+
)
|
|
40
|
+
return data.get("result")
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from concurrent.futures import CancelledError, ThreadPoolExecutor
|
|
5
|
+
from concurrent.futures import TimeoutError as FutureTimeoutError
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .auth.base import Auth
|
|
9
|
+
from .client import Client
|
|
10
|
+
from .exceptions import DispatchInterrupted, DispatchTimeout
|
|
11
|
+
from .models import AgentRecord, DispatchResult, Task
|
|
12
|
+
from .translation import compose_prompt
|
|
13
|
+
|
|
14
|
+
CHILD_TIMEOUT_FLOOR = 30.0 # Hermes parity: floor 30s when timeout is enabled
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AgentMintDispatcher:
|
|
18
|
+
"""High-level wrapper around AgentMint's /a2a JSON-RPC surface.
|
|
19
|
+
|
|
20
|
+
Translates Hermes' `delegate_task` ergonomics (goal + context + toolsets
|
|
21
|
+
+ role) into AgentMint's single-`prompt` agent.run model. The
|
|
22
|
+
persistent-sandbox semantics are NOT abstracted — that's the value: a
|
|
23
|
+
Hermes operator calls `dispatch()` and the work lands in a sandbox
|
|
24
|
+
whose /workspace survives across calls.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
auth: Auth,
|
|
30
|
+
endpoint: str = "https://api.agentmint.store/a2a",
|
|
31
|
+
):
|
|
32
|
+
self.client = Client(endpoint, auth)
|
|
33
|
+
|
|
34
|
+
# ----- direct method passthroughs ------------------------------------
|
|
35
|
+
|
|
36
|
+
def create(self, name: str, **kwargs: Any) -> AgentRecord:
|
|
37
|
+
params = {"name": name, **kwargs}
|
|
38
|
+
result = self.client.call("agent.create", params)
|
|
39
|
+
return AgentRecord.from_dict(result or {})
|
|
40
|
+
|
|
41
|
+
def get(self, agent_name: str) -> AgentRecord:
|
|
42
|
+
result = self.client.call("agent.get", {"name": agent_name})
|
|
43
|
+
return AgentRecord.from_dict(result or {})
|
|
44
|
+
|
|
45
|
+
def list(self, limit: int = 50, offset: int = 0) -> list[AgentRecord]:
|
|
46
|
+
result = self.client.call("agent.list", {"limit": limit, "offset": offset})
|
|
47
|
+
agents = (result or {}).get("agents", [])
|
|
48
|
+
return [AgentRecord.from_dict(a) for a in agents]
|
|
49
|
+
|
|
50
|
+
def delete(self, agent_name: str) -> None:
|
|
51
|
+
self.client.call("agent.delete", {"name": agent_name})
|
|
52
|
+
|
|
53
|
+
def cancel(self, agent_name: str) -> None:
|
|
54
|
+
self.client.call("agent.cancel", {"name": agent_name})
|
|
55
|
+
|
|
56
|
+
def run_status(self, run_id: str) -> dict[str, Any]:
|
|
57
|
+
"""Read the status of an async dispatch by `run_id` (returned from
|
|
58
|
+
`dispatch(async_=True)`).
|
|
59
|
+
|
|
60
|
+
Bearer-only / Stripe-Link-only on the server side (v0.7.0+). Free.
|
|
61
|
+
Used by the polling delivery mode in `hermes_patch`.
|
|
62
|
+
"""
|
|
63
|
+
return self.client.call("agent.run.status", {"run_id": run_id})
|
|
64
|
+
|
|
65
|
+
# ----- single dispatch -----------------------------------------------
|
|
66
|
+
|
|
67
|
+
def dispatch(
|
|
68
|
+
self,
|
|
69
|
+
agent_name: str,
|
|
70
|
+
goal: str,
|
|
71
|
+
context: str | None = None,
|
|
72
|
+
toolsets: list[str] | None = None,
|
|
73
|
+
role: str = "leaf",
|
|
74
|
+
max_iterations: int | None = None,
|
|
75
|
+
files: list[dict] | None = None,
|
|
76
|
+
cleanup_paths: list[str] | None = None,
|
|
77
|
+
async_: bool = False,
|
|
78
|
+
hermes_context: dict | None = None,
|
|
79
|
+
child_timeout_seconds: float | None = None,
|
|
80
|
+
) -> DispatchResult:
|
|
81
|
+
"""Dispatch one task to a named subagent.
|
|
82
|
+
|
|
83
|
+
`goal` and `context` are concatenated client-side (Hermes' rule:
|
|
84
|
+
the parent passes everything). `toolsets` / `role` / `max_iterations`
|
|
85
|
+
become soft system-prompt hints — AgentMint sandboxes can't
|
|
86
|
+
structurally enforce them.
|
|
87
|
+
|
|
88
|
+
Async dispatches return immediately with a `run_id`; poll
|
|
89
|
+
completion via `run_status(run_id)`.
|
|
90
|
+
|
|
91
|
+
If `child_timeout_seconds` is set, the call is wrapped with a hard
|
|
92
|
+
cap (floor 30s); on expiry, `agent.cancel` is fired and
|
|
93
|
+
`DispatchTimeout` is raised.
|
|
94
|
+
"""
|
|
95
|
+
prompt = compose_prompt(goal, context, toolsets, role, max_iterations)
|
|
96
|
+
params: dict[str, Any] = {"name": agent_name, "prompt": prompt}
|
|
97
|
+
if files:
|
|
98
|
+
params["files"] = files
|
|
99
|
+
if cleanup_paths:
|
|
100
|
+
params["cleanup_paths"] = cleanup_paths
|
|
101
|
+
if async_:
|
|
102
|
+
params["async"] = True
|
|
103
|
+
if hermes_context:
|
|
104
|
+
params["metadata"] = {"hermes": hermes_context}
|
|
105
|
+
|
|
106
|
+
if child_timeout_seconds is None:
|
|
107
|
+
result = self.client.call("agent.run", params)
|
|
108
|
+
return DispatchResult.from_dict(result or {})
|
|
109
|
+
|
|
110
|
+
cap = max(float(child_timeout_seconds), CHILD_TIMEOUT_FLOOR)
|
|
111
|
+
with ThreadPoolExecutor(max_workers=1) as ex:
|
|
112
|
+
future = ex.submit(self.client.call, "agent.run", params)
|
|
113
|
+
try:
|
|
114
|
+
result = future.result(timeout=cap)
|
|
115
|
+
except FutureTimeoutError as e:
|
|
116
|
+
try:
|
|
117
|
+
self.client.call("agent.cancel", {"name": agent_name})
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
raise DispatchTimeout(
|
|
121
|
+
f"dispatch of {agent_name!r} exceeded {cap}s; agent.cancel issued"
|
|
122
|
+
) from e
|
|
123
|
+
return DispatchResult.from_dict(result or {})
|
|
124
|
+
|
|
125
|
+
# ----- batch dispatch ------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def dispatch_batch(
|
|
128
|
+
self,
|
|
129
|
+
tasks: list[Task],
|
|
130
|
+
max_concurrent_children: int = 3,
|
|
131
|
+
child_timeout_seconds: float | None = None,
|
|
132
|
+
cancel_event: threading.Event | None = None,
|
|
133
|
+
) -> list[DispatchResult]:
|
|
134
|
+
"""Fan out N tasks in parallel against named subagents.
|
|
135
|
+
|
|
136
|
+
- Results are returned in **input order** (matches Hermes' contract
|
|
137
|
+
"Results are sorted by task index to match input order regardless
|
|
138
|
+
of completion order").
|
|
139
|
+
- Up to `max_concurrent_children` run simultaneously
|
|
140
|
+
(`ThreadPoolExecutor`).
|
|
141
|
+
- On exception per task, a `DispatchResult` with `status="failed"`
|
|
142
|
+
(or `"timeout"` / `"interrupted"`) is inserted at that index — the
|
|
143
|
+
batch never aborts partial.
|
|
144
|
+
- `cancel_event` (a `threading.Event`) — when set, in-flight tasks
|
|
145
|
+
receive a best-effort `agent.cancel` and remaining tasks raise
|
|
146
|
+
`DispatchInterrupted`.
|
|
147
|
+
"""
|
|
148
|
+
if max_concurrent_children < 1:
|
|
149
|
+
raise ValueError("max_concurrent_children must be >= 1")
|
|
150
|
+
if not tasks:
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
results: list[DispatchResult | None] = [None] * len(tasks)
|
|
154
|
+
|
|
155
|
+
def run_one(idx: int, task: Task) -> None:
|
|
156
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
157
|
+
results[idx] = DispatchResult(
|
|
158
|
+
status="interrupted",
|
|
159
|
+
extra={"error": "cancel_event set before task started"},
|
|
160
|
+
)
|
|
161
|
+
return
|
|
162
|
+
try:
|
|
163
|
+
results[idx] = self.dispatch(
|
|
164
|
+
agent_name=task.agent_name,
|
|
165
|
+
goal=task.goal,
|
|
166
|
+
context=task.context,
|
|
167
|
+
toolsets=task.toolsets,
|
|
168
|
+
role=task.role,
|
|
169
|
+
max_iterations=task.max_iterations,
|
|
170
|
+
files=task.files,
|
|
171
|
+
cleanup_paths=task.cleanup_paths,
|
|
172
|
+
hermes_context=task.hermes_context,
|
|
173
|
+
child_timeout_seconds=child_timeout_seconds,
|
|
174
|
+
)
|
|
175
|
+
except DispatchTimeout as e:
|
|
176
|
+
results[idx] = DispatchResult(status="timeout", extra={"error": str(e)})
|
|
177
|
+
except DispatchInterrupted as e:
|
|
178
|
+
results[idx] = DispatchResult(status="interrupted", extra={"error": str(e)})
|
|
179
|
+
except Exception as e:
|
|
180
|
+
results[idx] = DispatchResult(
|
|
181
|
+
status="failed",
|
|
182
|
+
extra={"error": str(e), "type": type(e).__name__},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
watcher: threading.Thread | None = None
|
|
186
|
+
with ThreadPoolExecutor(max_workers=max_concurrent_children) as ex:
|
|
187
|
+
futures = [ex.submit(run_one, i, t) for i, t in enumerate(tasks)]
|
|
188
|
+
|
|
189
|
+
if cancel_event is not None:
|
|
190
|
+
def watch() -> None:
|
|
191
|
+
cancel_event.wait()
|
|
192
|
+
for f in futures:
|
|
193
|
+
f.cancel()
|
|
194
|
+
seen: set[str] = set()
|
|
195
|
+
for t in tasks:
|
|
196
|
+
if t.agent_name in seen:
|
|
197
|
+
continue
|
|
198
|
+
seen.add(t.agent_name)
|
|
199
|
+
try:
|
|
200
|
+
self.client.call("agent.cancel", {"name": t.agent_name})
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
watcher = threading.Thread(target=watch, daemon=True)
|
|
205
|
+
watcher.start()
|
|
206
|
+
|
|
207
|
+
for f in futures:
|
|
208
|
+
try:
|
|
209
|
+
f.result()
|
|
210
|
+
except CancelledError:
|
|
211
|
+
# The cancel_event watcher cancelled this pending future
|
|
212
|
+
# before run_one had a chance to set results[idx]. Leave
|
|
213
|
+
# the slot as None; the synthesizer below fills it.
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
# Fill any None slots — either because the watcher cancelled a
|
|
217
|
+
# pending future, or (defensively) some other untracked path.
|
|
218
|
+
cancelled = cancel_event is not None and cancel_event.is_set()
|
|
219
|
+
fallback_status = "interrupted" if cancelled else "failed"
|
|
220
|
+
return [
|
|
221
|
+
r if r is not None else DispatchResult(
|
|
222
|
+
status=fallback_status,
|
|
223
|
+
extra={"error": f"task did not start ({fallback_status})"},
|
|
224
|
+
)
|
|
225
|
+
for r in results
|
|
226
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AgentMintError(Exception):
|
|
5
|
+
def __init__(self, message: str, data: Any = None):
|
|
6
|
+
super().__init__(message)
|
|
7
|
+
self.data = data
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DispatchTimeout(AgentMintError):
|
|
11
|
+
"""Raised when a child_timeout_seconds limit fires for a single dispatch.
|
|
12
|
+
|
|
13
|
+
The adapter has already issued `agent.cancel` to the AgentMint server
|
|
14
|
+
by the time this is raised; the cancellation is best-effort (the
|
|
15
|
+
underlying run may still complete depending on timing).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DispatchInterrupted(AgentMintError):
|
|
20
|
+
"""Raised inside a batch dispatch when the caller's cancel_event was set."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UnsupportedToolset(AgentMintError):
|
|
24
|
+
"""Raised when the caller requests a Hermes toolset that AgentMint does
|
|
25
|
+
not yet support (e.g. `"web"` in v0.2 — there is no canonical AgentMint
|
|
26
|
+
web-fetch skill yet; the three supported harnesses have built-in web
|
|
27
|
+
capability but we don't expose it as a structured toolset).
|
|
28
|
+
"""
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Monkey-patch Hermes' `tools.async_delegation.dispatch_async_delegation` so
|
|
2
|
+
every `delegate_task(background=True, single-task)` routes to a named,
|
|
3
|
+
persistent AgentMint subagent.
|
|
4
|
+
|
|
5
|
+
Sync `delegate_task` is untouched (Hermes-native fan-out / batch behaviour
|
|
6
|
+
preserved). Multi-task `background=True` is rejected upstream in Hermes
|
|
7
|
+
itself, so we never see it.
|
|
8
|
+
|
|
9
|
+
Completion is delivered exclusively via **polling** against AgentMint's
|
|
10
|
+
`agent.run.status` endpoint (Bearer-only, free). No public HTTPS endpoint,
|
|
11
|
+
no webhook secret, no HTTP route to register.
|
|
12
|
+
"""
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .dispatcher import AgentMintDispatcher
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_TERMINAL_STATUSES = frozenset({"completed", "failed", "cancelled", "timeout"})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def install_delegate_task_wrapper(
|
|
27
|
+
dispatcher: AgentMintDispatcher,
|
|
28
|
+
default_agent_name: str,
|
|
29
|
+
poll_interval: float = 5.0,
|
|
30
|
+
) -> Callable[[], None]:
|
|
31
|
+
"""Patch Hermes' async-delegation rail to route through AgentMint.
|
|
32
|
+
|
|
33
|
+
Must be called ONCE at gateway startup, BEFORE any
|
|
34
|
+
`delegate_task(background=True)` call. Returns a callable that
|
|
35
|
+
reverses the patch (useful in tests / shutdown).
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
dispatcher : AgentMintDispatcher
|
|
40
|
+
Pre-built dispatcher with auth attached.
|
|
41
|
+
default_agent_name : str
|
|
42
|
+
Name of the pre-minted AgentMint subagent every background
|
|
43
|
+
delegation routes to. The subagent's persistent `/workspace`
|
|
44
|
+
accumulates context across all delegations.
|
|
45
|
+
poll_interval : float
|
|
46
|
+
Seconds between `agent.run.status` polls (default 5.0). The
|
|
47
|
+
polling thread uses exponential backoff on errors up to 60s.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
import tools.async_delegation as _ad
|
|
51
|
+
except ImportError as e:
|
|
52
|
+
raise RuntimeError(
|
|
53
|
+
"Hermes module 'tools.async_delegation' not importable. "
|
|
54
|
+
"Run install_delegate_task_wrapper() inside the same Python "
|
|
55
|
+
"environment + process as Hermes' gateway."
|
|
56
|
+
) from e
|
|
57
|
+
|
|
58
|
+
original = _ad.dispatch_async_delegation
|
|
59
|
+
|
|
60
|
+
def patched(**kwargs: Any) -> dict:
|
|
61
|
+
goal = kwargs.get("goal", "")
|
|
62
|
+
context = kwargs.get("context")
|
|
63
|
+
toolsets = kwargs.get("toolsets")
|
|
64
|
+
role = kwargs.get("role")
|
|
65
|
+
model = kwargs.get("model")
|
|
66
|
+
session_key = kwargs.get("session_key", "")
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
result = dispatcher.dispatch(
|
|
70
|
+
agent_name=default_agent_name,
|
|
71
|
+
goal=goal,
|
|
72
|
+
context=context,
|
|
73
|
+
toolsets=toolsets,
|
|
74
|
+
role=role or "leaf",
|
|
75
|
+
async_=True,
|
|
76
|
+
hermes_context={"session_key": session_key, "model": model},
|
|
77
|
+
)
|
|
78
|
+
run_id = result.run_id or result.delegation_id
|
|
79
|
+
if not run_id:
|
|
80
|
+
raise RuntimeError("AgentMint async dispatch returned no run_id")
|
|
81
|
+
|
|
82
|
+
_spawn_poller(
|
|
83
|
+
dispatcher=dispatcher,
|
|
84
|
+
run_id=run_id,
|
|
85
|
+
goal=goal,
|
|
86
|
+
context=context,
|
|
87
|
+
session_key=session_key,
|
|
88
|
+
poll_interval=poll_interval,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"status": "dispatched",
|
|
93
|
+
"delegation_id": run_id,
|
|
94
|
+
"goal": goal,
|
|
95
|
+
"mode": "background",
|
|
96
|
+
"source": "agentmint",
|
|
97
|
+
}
|
|
98
|
+
except Exception:
|
|
99
|
+
logger.exception(
|
|
100
|
+
"agentmint patched dispatch failed — falling back to Hermes-native"
|
|
101
|
+
)
|
|
102
|
+
return original(**kwargs)
|
|
103
|
+
|
|
104
|
+
_ad.dispatch_async_delegation = patched
|
|
105
|
+
logger.info(
|
|
106
|
+
"agentmint-hermes: installed delegate_task wrapper "
|
|
107
|
+
"(default_agent=%s, poll_interval=%.1fs)",
|
|
108
|
+
default_agent_name, poll_interval,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def uninstall() -> None:
|
|
112
|
+
_ad.dispatch_async_delegation = original
|
|
113
|
+
|
|
114
|
+
return uninstall
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _spawn_poller(
|
|
118
|
+
*,
|
|
119
|
+
dispatcher: AgentMintDispatcher,
|
|
120
|
+
run_id: str,
|
|
121
|
+
goal: str,
|
|
122
|
+
context: str | None,
|
|
123
|
+
session_key: str,
|
|
124
|
+
poll_interval: float,
|
|
125
|
+
) -> threading.Thread:
|
|
126
|
+
"""Background daemon thread: polls `agent.run.status` until terminal,
|
|
127
|
+
then pushes a Hermes async_delegation completion event onto Hermes'
|
|
128
|
+
completion_queue.
|
|
129
|
+
|
|
130
|
+
Returns the thread (mostly for tests). Exits on terminal status or
|
|
131
|
+
after a hard cap of 30 minutes — the AgentMint run's own 30-minute
|
|
132
|
+
server-side TTL means the record disappears anyway past that point.
|
|
133
|
+
"""
|
|
134
|
+
HARD_CAP_SECONDS = 30 * 60
|
|
135
|
+
|
|
136
|
+
def push_completion(status: str, payload: dict) -> None:
|
|
137
|
+
try:
|
|
138
|
+
from tools.async_delegation import _push_completion_event
|
|
139
|
+
_push_completion_event(
|
|
140
|
+
delegation_id=run_id,
|
|
141
|
+
status=status,
|
|
142
|
+
result=payload,
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
except Exception:
|
|
146
|
+
logger.warning(
|
|
147
|
+
"agentmint-hermes: _push_completion_event unavailable; "
|
|
148
|
+
"falling back to direct queue.put"
|
|
149
|
+
)
|
|
150
|
+
try:
|
|
151
|
+
from hermes.gateway.process_registry import completion_queue
|
|
152
|
+
completion_queue.put({
|
|
153
|
+
"type": "async_delegation",
|
|
154
|
+
"delegation_id": run_id,
|
|
155
|
+
"status": status,
|
|
156
|
+
"result": payload,
|
|
157
|
+
"task_source": {
|
|
158
|
+
"goal": goal,
|
|
159
|
+
"context": context,
|
|
160
|
+
"session_key": session_key,
|
|
161
|
+
"source": "agentmint",
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
except Exception:
|
|
165
|
+
logger.exception(
|
|
166
|
+
"agentmint-hermes: completion_queue push failed; "
|
|
167
|
+
"completion will not re-inject"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def loop() -> None:
|
|
171
|
+
backoff = poll_interval
|
|
172
|
+
started = time.monotonic()
|
|
173
|
+
while True:
|
|
174
|
+
time.sleep(backoff)
|
|
175
|
+
if time.monotonic() - started > HARD_CAP_SECONDS:
|
|
176
|
+
logger.warning(
|
|
177
|
+
"agentmint-hermes: poller for %s exceeded 30-minute cap; "
|
|
178
|
+
"emitting timeout completion", run_id,
|
|
179
|
+
)
|
|
180
|
+
push_completion("timeout", {"task_source": {
|
|
181
|
+
"goal": goal, "context": context, "session_key": session_key,
|
|
182
|
+
}})
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
resp = dispatcher.run_status(run_id)
|
|
186
|
+
status = (resp or {}).get("status", "pending")
|
|
187
|
+
if status in _TERMINAL_STATUSES:
|
|
188
|
+
push_completion(status, {
|
|
189
|
+
"billed_usdc": (resp or {}).get("billed_usdc"),
|
|
190
|
+
"completed_at": (resp or {}).get("completed_at"),
|
|
191
|
+
"task_source": {
|
|
192
|
+
"goal": goal,
|
|
193
|
+
"context": context,
|
|
194
|
+
"session_key": session_key,
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
return
|
|
198
|
+
# Reset backoff on a successful poll (status still pending).
|
|
199
|
+
backoff = poll_interval
|
|
200
|
+
except Exception:
|
|
201
|
+
logger.exception(
|
|
202
|
+
"agentmint-hermes: poller iteration failed for %s; "
|
|
203
|
+
"backing off", run_id,
|
|
204
|
+
)
|
|
205
|
+
backoff = min(backoff * 1.5, 60.0)
|
|
206
|
+
|
|
207
|
+
t = threading.Thread(
|
|
208
|
+
target=loop,
|
|
209
|
+
daemon=True,
|
|
210
|
+
name=f"agentmint-poll-{run_id[:12]}",
|
|
211
|
+
)
|
|
212
|
+
t.start()
|
|
213
|
+
return t
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from dataclasses import dataclass, field, fields
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class AgentRecord:
|
|
7
|
+
agent_id: str
|
|
8
|
+
name: str | None = None
|
|
9
|
+
mode: str | None = None
|
|
10
|
+
harness: str | None = None
|
|
11
|
+
model: str | None = None
|
|
12
|
+
runtime: str | None = None
|
|
13
|
+
size: str | None = None
|
|
14
|
+
created_at: int | None = None
|
|
15
|
+
last_used: int | None = None
|
|
16
|
+
lifetime_runs: int | None = None
|
|
17
|
+
lifetime_billed_usdc: float | None = None
|
|
18
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_dict(cls, d: dict) -> "AgentRecord":
|
|
22
|
+
known = {f.name for f in fields(cls)} - {"extra"}
|
|
23
|
+
kwargs = {k: v for k, v in d.items() if k in known}
|
|
24
|
+
extra = {k: v for k, v in d.items() if k not in known}
|
|
25
|
+
return cls(**kwargs, extra=extra)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class DispatchResult:
|
|
30
|
+
status: str | None = None
|
|
31
|
+
delegation_id: str | None = None
|
|
32
|
+
run_id: str | None = None
|
|
33
|
+
result: Any = None
|
|
34
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_dict(cls, d: dict) -> "DispatchResult":
|
|
38
|
+
known = {f.name for f in fields(cls)} - {"extra"}
|
|
39
|
+
kwargs = {k: v for k, v in d.items() if k in known}
|
|
40
|
+
extra = {k: v for k, v in d.items() if k not in known}
|
|
41
|
+
return cls(**kwargs, extra=extra)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Task:
|
|
46
|
+
"""One task in a `dispatch_batch` call.
|
|
47
|
+
|
|
48
|
+
Mirrors Hermes' batch-task shape (`{goal, context, toolsets}`) plus an
|
|
49
|
+
explicit `agent_name` — each AgentMint task targets a named, persistent
|
|
50
|
+
subagent (which is the inversion of Hermes' "fresh AIAgent per task").
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
goal: str
|
|
54
|
+
agent_name: str
|
|
55
|
+
context: str | None = None
|
|
56
|
+
toolsets: list[str] | None = None
|
|
57
|
+
max_iterations: int | None = None
|
|
58
|
+
role: str = "leaf"
|
|
59
|
+
files: list[dict] | None = None
|
|
60
|
+
cleanup_paths: list[str] | None = None
|
|
61
|
+
hermes_context: dict | None = None
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Translate Hermes `delegate_task` semantics into AgentMint /a2a calls.
|
|
2
|
+
|
|
3
|
+
The translation is deliberately client-side and prompt-based:
|
|
4
|
+
|
|
5
|
+
- AgentMint's `agent.run` has a single `prompt` field. Hermes' `goal`
|
|
6
|
+
+ `context` are concatenated into that prompt under labelled sections.
|
|
7
|
+
- AgentMint sandboxes always have terminal + filesystem. Restrictions
|
|
8
|
+
can't be enforced structurally — they become soft system-prompt hints
|
|
9
|
+
("Do not run shell commands.").
|
|
10
|
+
- Role (leaf vs orchestrator) and max_iterations are similarly soft —
|
|
11
|
+
the harness reads them out of the prompt if it knows to.
|
|
12
|
+
|
|
13
|
+
The `"web"` toolset is **unsupported in v0.2**. AgentMint's three
|
|
14
|
+
harnesses (claude-code / codex / opencode) all have a built-in WebFetch
|
|
15
|
+
tool, but we don't yet ship a Hermes-symmetric web-fetch skill that the
|
|
16
|
+
adapter can advertise or restrict — passing `"web"` in toolsets raises
|
|
17
|
+
`UnsupportedToolset`. Track: GitHub issues on agentmint-hermes.
|
|
18
|
+
|
|
19
|
+
This mirrors Hermes' load-bearing rule: "The parent agent must pass
|
|
20
|
+
everything the subagent needs in the call" — but where Hermes spawns a
|
|
21
|
+
fresh AIAgent, AgentMint dispatches to a persistent sandbox whose
|
|
22
|
+
`/workspace/MEMORY.md` accumulates context across calls.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from .exceptions import UnsupportedToolset
|
|
26
|
+
|
|
27
|
+
# Web is intentionally excluded — see module docstring + UnsupportedToolset.
|
|
28
|
+
DEFAULT_TOOLSETS: tuple[str, ...] = ("terminal", "file")
|
|
29
|
+
|
|
30
|
+
TOOLSET_RESTRICTION_HINTS: dict[str, str] = {
|
|
31
|
+
"terminal": "Do not run shell commands.",
|
|
32
|
+
"file": "Do not read or write files outside /workspace.",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
UNSUPPORTED_TOOLSETS: frozenset[str] = frozenset({"web"})
|
|
36
|
+
|
|
37
|
+
ROLE_HINTS: dict[str, str] = {
|
|
38
|
+
"leaf": (
|
|
39
|
+
"You are a leaf subagent. Focus on completing this task end-to-end. "
|
|
40
|
+
"Do not delegate further."
|
|
41
|
+
),
|
|
42
|
+
"orchestrator": (
|
|
43
|
+
"You are an orchestrator subagent. You may break complex subtasks "
|
|
44
|
+
"into further delegations if your installed skills allow it."
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compose_prompt(
|
|
50
|
+
goal: str,
|
|
51
|
+
context: str | None = None,
|
|
52
|
+
toolsets: list[str] | None = None,
|
|
53
|
+
role: str | None = None,
|
|
54
|
+
max_iterations: int | None = None,
|
|
55
|
+
) -> str:
|
|
56
|
+
"""Concat goal + context + soft-enforcement hints into one prompt string.
|
|
57
|
+
|
|
58
|
+
`toolsets` is interpreted as "the only allowed toolsets" — any default
|
|
59
|
+
toolset NOT listed becomes a restriction hint. Pass `None` to mean
|
|
60
|
+
"no restrictions" (Hermes default). Listing `"web"` raises
|
|
61
|
+
`UnsupportedToolset`.
|
|
62
|
+
"""
|
|
63
|
+
if not goal or not goal.strip():
|
|
64
|
+
raise ValueError("goal is required")
|
|
65
|
+
|
|
66
|
+
if toolsets is not None:
|
|
67
|
+
unsupported = sorted(t for t in toolsets if t in UNSUPPORTED_TOOLSETS)
|
|
68
|
+
if unsupported:
|
|
69
|
+
raise UnsupportedToolset(
|
|
70
|
+
f"toolset(s) {unsupported!r} not supported in this version — "
|
|
71
|
+
f"AgentMint does not yet ship a Hermes-symmetric skill for them. "
|
|
72
|
+
f"Drop them from the toolsets list to proceed."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
sections: list[str] = [f"## Goal\n{goal.strip()}"]
|
|
76
|
+
if context and context.strip():
|
|
77
|
+
sections.append(f"## Context\n{context.strip()}")
|
|
78
|
+
|
|
79
|
+
hints: list[str] = []
|
|
80
|
+
if toolsets is not None:
|
|
81
|
+
restricted = [t for t in DEFAULT_TOOLSETS if t not in toolsets]
|
|
82
|
+
hints.extend(TOOLSET_RESTRICTION_HINTS[t] for t in restricted)
|
|
83
|
+
if role:
|
|
84
|
+
if role not in ROLE_HINTS:
|
|
85
|
+
raise ValueError(f"unknown role: {role!r} (expected 'leaf' or 'orchestrator')")
|
|
86
|
+
hints.append(ROLE_HINTS[role])
|
|
87
|
+
if max_iterations:
|
|
88
|
+
hints.append(f"Soft iteration budget: ~{max_iterations} actions.")
|
|
89
|
+
|
|
90
|
+
if hints:
|
|
91
|
+
sections.append("## Constraints\n" + "\n".join(f"- {h}" for h in hints))
|
|
92
|
+
|
|
93
|
+
return "\n\n".join(sections)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentmint-hermes-runner
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Route Hermes delegate_task(background=True) to named, persistent AgentMint subagents.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mesutcelik/agentmint-hermes
|
|
6
|
+
Project-URL: Repository, https://github.com/mesutcelik/agentmint-hermes
|
|
7
|
+
Project-URL: Issues, https://github.com/mesutcelik/agentmint-hermes/issues
|
|
8
|
+
Author: AgentMint
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agentmint,ai-agents,delegation,hermes,mpp,stripe-link,subagents,tempo
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.7; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# agentmint-hermes-runner
|
|
27
|
+
|
|
28
|
+
Route Hermes `delegate_task(background=True)` to named, persistent AgentMint subagents.
|
|
29
|
+
|
|
30
|
+
> Positioning + full quickstart in [`agentmint-hermes/SKILL.md`](agentmint-hermes/SKILL.md).
|
|
31
|
+
|
|
32
|
+
## Status
|
|
33
|
+
|
|
34
|
+
**v0.4.0** — alpha. Auth backends: `BearerAuth` (Stripe-Link), `TempoAuth` (Tempo USDC.e). Polling-only delivery. Hermes feature coverage matrix in [`agentmint-hermes/SKILL.md`](agentmint-hermes/SKILL.md).
|
|
35
|
+
|
|
36
|
+
## Install in Hermes
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
hermes skills install mesutcelik/agentmint-hermes/agentmint-hermes
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
(The third segment is the skill subfolder inside the repo, per Hermes' `GitHubSource` convention.)
|
|
43
|
+
|
|
44
|
+
## Three-line Hermes wiring (Strategy B)
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import os
|
|
48
|
+
from agentmint_hermes_runner import (
|
|
49
|
+
AgentMintDispatcher, BearerAuth, install_delegate_task_wrapper,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
dispatcher = AgentMintDispatcher(auth=BearerAuth(jwt=os.environ["AGENTMINT_JWT"]))
|
|
53
|
+
install_delegate_task_wrapper(dispatcher, default_agent_name="default-worker")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Every `delegate_task(background=True)` inside Hermes now routes to AgentMint's `default-worker` subagent. Its `/workspace/MEMORY.md` accumulates across every delegation. No HTTPS, no ngrok, no webhook secret — a daemon thread polls `agent.run.status` (free, Bearer-only) every 5 s and pushes completions onto Hermes' `completion_queue` directly. Server-side requires AgentMint API ≥ 0.7.0 for the polling endpoint.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install agentmint-hermes-runner
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Surface
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from agentmint_hermes_runner import (
|
|
68
|
+
AgentMintDispatcher,
|
|
69
|
+
AgentMintWebhookReceiver,
|
|
70
|
+
BearerAuth, TempoAuth,
|
|
71
|
+
Task,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
dispatcher = AgentMintDispatcher(
|
|
75
|
+
auth=BearerAuth(jwt=os.environ["AGENTMINT_JWT"]),
|
|
76
|
+
webhook_url="https://my-gateway.example.com/agentmint-webhook", # optional
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Single dispatch (Hermes delegate_task analog):
|
|
80
|
+
result = dispatcher.dispatch(
|
|
81
|
+
agent_name="reviewer-myrepo",
|
|
82
|
+
goal="Review the diff in /workspace/pr-42 and flag risks.",
|
|
83
|
+
context="Project at /workspace, Python 3.11, uses Flask + PyJWT.",
|
|
84
|
+
toolsets=["terminal", "file"], # "web" raises UnsupportedToolset in v0.2
|
|
85
|
+
role="leaf", # or "orchestrator"
|
|
86
|
+
max_iterations=50,
|
|
87
|
+
child_timeout_seconds=600, # floor 30s; fires agent.cancel on expiry
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Batch dispatch (Hermes tasks=[…] analog):
|
|
91
|
+
results = dispatcher.dispatch_batch(
|
|
92
|
+
tasks=[
|
|
93
|
+
Task(agent_name="researcher-wasm", goal="WASM 2026 survey", context="…"),
|
|
94
|
+
Task(agent_name="researcher-riscv", goal="RISC-V 2026 survey", context="…"),
|
|
95
|
+
],
|
|
96
|
+
max_concurrent_children=3,
|
|
97
|
+
child_timeout_seconds=900,
|
|
98
|
+
)
|
|
99
|
+
# results in input order; failed/timeout/interrupted statuses returned in-band
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Test
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pip install -e ".[dev]"
|
|
106
|
+
pytest
|
|
107
|
+
ruff check .
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Known unsupported (v0.2)
|
|
111
|
+
|
|
112
|
+
- **`toolsets=["web"]`** — no canonical AgentMint web-fetch skill yet. The supported harnesses (claude-code / codex / opencode) all have built-in web access via the harness itself, but we don't expose a Hermes-symmetric toolset for it. Raises `UnsupportedToolset` at compose time so the gap is loud, not silent.
|
|
113
|
+
- **`max_spawn_depth`** — AgentMint sandboxes aren't structurally bounded by depth.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
agentmint_hermes_runner/__init__.py,sha256=CBpLrbDWN_J63Z42JkEMh65YRkyWIjMUH3bkYu5W5n8,918
|
|
2
|
+
agentmint_hermes_runner/client.py,sha256=P1rKGTic5UwyRHxF8jR7lj1s_oGKlOa6fumXis85FMA,1232
|
|
3
|
+
agentmint_hermes_runner/dispatcher.py,sha256=UWunqBBjAz1xHF9MXA10yGBS_o1zUksVcpYqWyk5KI4,9272
|
|
4
|
+
agentmint_hermes_runner/exceptions.py,sha256=vw6elB4i52NYffsF7v1gETV7qJU0MmLvgqPPlBNiO0k,973
|
|
5
|
+
agentmint_hermes_runner/hermes_patch.py,sha256=yOXWRj4IB2O7asutm9v7sT2D6TT5LPTu9132EYaYyPk,7462
|
|
6
|
+
agentmint_hermes_runner/models.py,sha256=H5xwrlbY3zFDsveSAls95YSY4K_DfE4ER6z7HhOXvgU,1888
|
|
7
|
+
agentmint_hermes_runner/translation.py,sha256=XBOZnlErWi_HXU6YBWhnwz7tCqk58tEd7h5ye5t6X_o,3721
|
|
8
|
+
agentmint_hermes_runner/auth/__init__.py,sha256=LpafSzNF3sKekADwwvXGUHqn24ayKIlETsAehP33gdE,130
|
|
9
|
+
agentmint_hermes_runner/auth/base.py,sha256=4Xn88CqujJhWTbOGuf7n42SX8gEgx-htlPH_WdSb4UE,620
|
|
10
|
+
agentmint_hermes_runner/auth/bearer.py,sha256=gsVvU696T2bgS5uzSAPbnHRyq21E9Je8WOqnV0XIUrE,952
|
|
11
|
+
agentmint_hermes_runner/auth/tempo.py,sha256=66kZkpVwgLEh9D2HE15IJwc_p20mRTS_XbsD2WvvmfA,1226
|
|
12
|
+
agentmint_hermes_runner-0.4.0.dist-info/METADATA,sha256=MWtrc7-gFzbz2Y6JULBDdO49nVPAZqT0tY302Syi7QY,4257
|
|
13
|
+
agentmint_hermes_runner-0.4.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
14
|
+
agentmint_hermes_runner-0.4.0.dist-info/licenses/LICENSE,sha256=I6Rrtz19ACJO9Vv8pJa8UpRIB2OCqkiswyGqm0qomXA,1066
|
|
15
|
+
agentmint_hermes_runner-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentMint
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|